Skip to content

Commit

Permalink
feat(servicecatalogappregistry): add attribute groups to an applicati…
Browse files Browse the repository at this point in the history
…on (#24672)

To associate a attribute group to an application created in `ApplicationAssociator`, customers have to use `AttributeGroup` construct separately to create and associate the attribute group separately. This makes the `AttributeGroup` and `AttributeGroupAssociation` created in another stack than `ApplicationAssociator` stack.
This commits provides an one-stop action, i.e. `Application.addAttributeGroup()`, to create and associate attribute group on `Application` Construct. This solution not only makes attribute group creation and association easier for customer who uses `Application` construct, but also lets customer to have attribute groups and attribute group associations for the `ApplicationAssociator` applications in the same stack.

`Application.addAttributeGroup()` has `id` in the parameters, for two reasons:
- consistent with the experience where customer can define logical ID when using `new AttributeGroup()`
- complexity of deciding logical ID from the attribute group input:
  - We have to make sure update attributes/description won't trigger create and then delete but update, which will cause name conflict exception. 
  - We also don't want to generate logical ID from attribute group name only, as if two `Application.addAttributeGroup()` method calls with the same name will result in construct ID conflict. This exposes implementation details and makes it hard to customers to debug and resolve.

BREAKING CHANGE: This commit contains destructive changes to the RAM Share.
Since the application RAM share name is calculated by the application construct, where one method is added. Integration test detects a breaking change where RAM share will be created. Integration test snapshot is updated to cater this destructive change.

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
liwewang-amazon authored Mar 22, 2023
1 parent 7de5b00 commit 7baffa2
Show file tree
Hide file tree
Showing 14 changed files with 254 additions and 46 deletions.
22 changes: 5 additions & 17 deletions packages/@aws-cdk/aws-servicecatalogappregistry/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,22 +126,6 @@ import * as cdk from "@aws-cdk/core";

const app = new App();

class CustomAppRegistryAttributeGroup extends cdk.Stack {
public readonly attributeGroup: appreg.AttributeGroup
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const myAttributeGroup = new appreg.AttributeGroup(app, 'MyFirstAttributeGroup', {
attributeGroupName: 'MyAttributeGroupName',
description: 'Test attribute group',
attributes: {},
});

this.attributeGroup = myAttributeGroup;
}
}

const customAttributeGroup = new CustomAppRegistryAttributeGroup(app, 'AppRegistryAttributeGroup');

const associatedApp = new appreg.ApplicationAssociator(app, 'AssociatedApplication', {
applications: [appreg.TargetApplication.createApplicationStack({
applicationName: 'MyAssociatedApplication',
Expand All @@ -154,7 +138,11 @@ const associatedApp = new appreg.ApplicationAssociator(app, 'AssociatedApplicati
});

// Associate application to the attribute group.
customAttributeGroup.attributeGroup.associateWith(associatedApp.appRegistryApplication());
associatedApp.appRegistryApplication.addAttributeGroup('MyAttributeGroup' , {
attributeGroupName: 'MyAttributeGroupName',
description: 'Test attribute group',
attributes: {},
});

```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export class ApplicationAssociator extends Construct {
* Get the AppRegistry application.
*
*/
public appRegistryApplication(): IApplication {
public get appRegistryApplication(): IApplication {
return this.application;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as cdk from '@aws-cdk/core';
import { Names } from '@aws-cdk/core';
import { Construct } from 'constructs';
import { StageStackAssociator } from './aspects/stack-associator';
import { IAttributeGroup } from './attribute-group';
import { AttributeGroup, IAttributeGroup } from './attribute-group';
import { getPrincipalsforSharing, hashValues, ShareOptions, SharePermission } from './common';
import { isAccountUnresolved } from './private/utils';
import { InputValidator } from './private/validation';
Expand All @@ -12,6 +12,29 @@ import { CfnApplication, CfnAttributeGroupAssociation, CfnResourceAssociation }
const APPLICATION_READ_ONLY_RAM_PERMISSION_ARN = 'arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryApplicationReadOnly';
const APPLICATION_ALLOW_ACCESS_RAM_PERMISSION_ARN = 'arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryApplicationAllowAssociation';

/**
* Properties for a Service Catalog AppRegistry Attribute Group
*/
export interface AttributeGroupAssociationProps {
/**
* Name for attribute group.
*
*/
readonly attributeGroupName: string;

/**
* Description for attribute group.
* @default - No description provided
*/
readonly description?: string;

/**
* A JSON of nested key-value pairs that represent the attributes in the group.
* Attributes maybe an empty JSON '{}', but must be explicitly stated.
*/
readonly attributes: { [key: string]: any };
}

/**
* A Service Catalog AppRegistry Application.
*/
Expand Down Expand Up @@ -41,6 +64,14 @@ export interface IApplication extends cdk.IResource {
*/
associateAttributeGroup(attributeGroup: IAttributeGroup): void;

/**
* Create an attribute group and associate this application with the created attribute group.
*
* @param id name of the AttributeGroup construct to be created.
* @param attributeGroupProps AppRegistry attribute group props
*/
addAttributeGroup(id: string, attributeGroupProps: AttributeGroupAssociationProps): IAttributeGroup;

/**
* Associate this application with a CloudFormation stack.
*
Expand Down Expand Up @@ -114,6 +145,23 @@ abstract class ApplicationBase extends cdk.Resource implements IApplication {
}
}

/**
* Create an attribute group and associate this application with the created attribute group.
*/
public addAttributeGroup(id: string, props: AttributeGroupAssociationProps): IAttributeGroup {
const attributeGroup = new AttributeGroup(this, id, {
attributeGroupName: props.attributeGroupName,
attributes: props.attributes,
description: props.description,
});
new CfnAttributeGroupAssociation(this, `AttributeGroupAssociation${this.generateUniqueHash(attributeGroup.node.addr)}`, {
application: this.applicationId,
attributeGroup: attributeGroup.attributeGroupId,
});
this.associatedAttributeGroups.add(attributeGroup.node.addr);
return attributeGroup;
}

/**
* Associate a stack with the application
* If the resource is already associated, it will ignore duplicate request.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export class CheckedStageStackAssociator extends StackAssociatorBase {

constructor(app: ApplicationAssociator, props?: StackAssociatorBaseProps) {
super(props);
this.application = app.appRegistryApplication();
this.application = app.appRegistryApplication;
this.applicationAssociator = app;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export interface CreateTargetApplicationOptions extends TargetApplicationCommonO
/**
* Whether create cloudFormation Output for application manager URL.
*
* @default - Application containing stacks deployed via CDK.
* @default - true
*/
readonly emitApplicationManagerUrlAsOutput?: boolean;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ describe('Scope based Associations with Application within Same Account', () =>
});

const anotherStack = new AppRegistrySampleStack(app, 'SampleStack');
Template.fromStack(appAssociator.appRegistryApplication().stack).resourceCountIs('AWS::ServiceCatalogAppRegistry::Application', 1);
Template.fromStack(appAssociator.appRegistryApplication().stack).hasResourceProperties('AWS::ServiceCatalogAppRegistry::Application', {
Template.fromStack(appAssociator.appRegistryApplication.stack).resourceCountIs('AWS::ServiceCatalogAppRegistry::Application', 1);
Template.fromStack(appAssociator.appRegistryApplication.stack).hasResourceProperties('AWS::ServiceCatalogAppRegistry::Application', {
Name: 'MyAssociatedApplication',
Tags: { managedBy: 'CDK_Application_Associator' },
});
Template.fromStack(appAssociator.appRegistryApplication().stack).hasOutput('DefaultCdkApplicationApplicationManagerUrl27C138EF', {});
Template.fromStack(appAssociator.appRegistryApplication.stack).hasOutput('DefaultCdkApplicationApplicationManagerUrl27C138EF', {});
Template.fromStack(anotherStack).resourceCountIs('AWS::ServiceCatalogAppRegistry::ResourceAssociation', 1);
Template.fromStack(anotherStack).hasResourceProperties('AWS::ServiceCatalogAppRegistry::ResourceAssociation', {
Application: 'MyAssociatedApplication',
Expand All @@ -46,14 +46,14 @@ describe('Scope based Associations with Application within Same Account', () =>
});

const anotherStack = new AppRegistrySampleStack(app, 'SampleStack');
Template.fromStack(appAssociator.appRegistryApplication().stack).resourceCountIs('AWS::ServiceCatalogAppRegistry::Application', 1);
Template.fromStack(appAssociator.appRegistryApplication().stack).hasResourceProperties('AWS::ServiceCatalogAppRegistry::Application', {
Template.fromStack(appAssociator.appRegistryApplication.stack).resourceCountIs('AWS::ServiceCatalogAppRegistry::Application', 1);
Template.fromStack(appAssociator.appRegistryApplication.stack).hasResourceProperties('AWS::ServiceCatalogAppRegistry::Application', {
Name: 'MyAssociatedApplication',
Tags: { managedBy: 'CDK_Application_Associator' },
});

expect(
Template.fromStack(appAssociator.appRegistryApplication().stack)
Template.fromStack(appAssociator.appRegistryApplication.stack)
.findOutputs('*', {}),
).toEqual({});
Template.fromStack(anotherStack).resourceCountIs('AWS::ServiceCatalogAppRegistry::ResourceAssociation', 1);
Expand Down Expand Up @@ -85,7 +85,7 @@ describe('Associate attribute group with Application', () => {
})],
});

customAttributeGroup.attributeGroup.associateWith(appAssociator.appRegistryApplication());
customAttributeGroup.attributeGroup.associateWith(appAssociator.appRegistryApplication);
Template.fromStack(customAttributeGroup.attributeGroup.stack).resourceCountIs('AWS::ServiceCatalogAppRegistry::AttributeGroupAssociation', 1);
Template.fromStack(customAttributeGroup.attributeGroup.stack).hasResourceProperties('AWS::ServiceCatalogAppRegistry::AttributeGroupAssociation', {
Application: 'TestAssociatedApplication',
Expand Down Expand Up @@ -137,7 +137,7 @@ describe('Scope based Associations with Application with Cross Region/Account',
});

expect(
Template.fromStack(appAssociator.appRegistryApplication().stack).findOutputs('*', {}),
Template.fromStack(appAssociator.appRegistryApplication.stack).findOutputs('*', {}),
).toEqual({});
Template.fromStack(firstStack).resourceCountIs('AWS::ServiceCatalogAppRegistry::ResourceAssociation', 1);
Template.fromStack(nestedStack).resourceCountIs('AWS::ServiceCatalogAppRegistry::ResourceAssociation', 1);
Expand Down Expand Up @@ -268,7 +268,7 @@ describe('Scope based Associations with Application with Cross Region/Account',
associateStage: true,
});
app.synth();
Template.fromStack(application.appRegistryApplication().stack).hasOutput('DefaultCdkApplicationApplicationManagerUrl27C138EF', {});
Template.fromStack(application.appRegistryApplication.stack).hasOutput('DefaultCdkApplicationApplicationManagerUrl27C138EF', {});
Template.fromStack(pipelineStack).resourceCountIs('AWS::ServiceCatalogAppRegistry::ResourceAssociation', 1);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,31 @@ describe('Application', () => {
});
}),

test('associate new attribute group', () => {
application.addAttributeGroup('AttributeGroup', {
attributeGroupName: 'AttributeGroupName',
attributes: {},
description: 'Description for Attribute Group',
});

Template.fromStack(stack).hasResourceProperties('AWS::ServiceCatalogAppRegistry::AttributeGroupAssociation', {
Application: { 'Fn::GetAtt': ['MyApplication5C63EC1D', 'Id'] },
AttributeGroup: { 'Fn::GetAtt': ['MyApplicationAttributeGroup0BD166B6', 'Id'] },
});

Template.fromStack(stack).templateMatches({
Resources: {
MyApplicationAttributeGroup0BD166B6: {
Type: 'AWS::ServiceCatalogAppRegistry::AttributeGroup',
Properties: {
Name: 'AttributeGroupName',
Attributes: {},
},
},
},
});
}),

test('duplicate attribute group association are idempotent', () => {
const attributeGroup = new appreg.AttributeGroup(stack, 'AttributeGroup', {
attributeGroupName: 'attributeGroupName',
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"version":"30.1.0"}
{"version":"31.0.0"}
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
{
"version": "30.1.0",
"version": "31.0.0",
"files": {
"2332c6df6777cc571585060fa4888d6d3b9ef548aa00dcbfc53fbdde386d7591": {
"5fbf2a286122f4bc412b1730f96351e289444b1122006f36e4ade8fae8442765": {
"source": {
"path": "integ-servicecatalogappregistry-application.template.json",
"packaging": "file"
},
"destinations": {
"current_account-current_region": {
"bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}",
"objectKey": "2332c6df6777cc571585060fa4888d6d3b9ef548aa00dcbfc53fbdde386d7591.json",
"objectKey": "5fbf2a286122f4bc412b1730f96351e289444b1122006f36e4ade8fae8442765.json",
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}"
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,50 @@
}
}
},
"TestApplicationRAMSharead8ba81b8cdd40199FD1": {
"TestApplicationmyAnotherAttributeGroup375F79DB": {
"Type": "AWS::ServiceCatalogAppRegistry::AttributeGroup",
"Properties": {
"Attributes": {
"stage": "alpha",
"teamMembers": [
"markI",
"markII",
"markIII"
],
"public": false,
"publishYear": 2021,
"plannedRoadMap": {
"alpha": "some time",
"beta": "another time",
"gamma": "penultimate time",
"release": "go time"
}
},
"Name": "myAnotherAttributeGroup",
"Description": "my another attribute group description"
}
},
"TestApplicationAttributeGroupAssociationb6f47e836a8c4FCAC29E": {
"Type": "AWS::ServiceCatalogAppRegistry::AttributeGroupAssociation",
"Properties": {
"Application": {
"Fn::GetAtt": [
"TestApplication2FBC585F",
"Id"
]
},
"AttributeGroup": {
"Fn::GetAtt": [
"TestApplicationmyAnotherAttributeGroup375F79DB",
"Id"
]
}
}
},
"TestApplicationRAMShare004736f08f8e57044D5D": {
"Type": "AWS::RAM::ResourceShare",
"Properties": {
"Name": "RAMSharead8ba81b8cdd",
"Name": "RAMShare004736f08f8e",
"AllowExternalPrincipals": false,
"PermissionArns": [
"arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryApplicationReadOnly"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "30.1.0",
"version": "31.0.0",
"testCases": {
"integ.application": {
"stacks": [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "30.1.0",
"version": "31.0.0",
"artifacts": {
"integ-servicecatalogappregistry-application.assets": {
"type": "cdk:asset-manifest",
Expand All @@ -17,7 +17,7 @@
"validateOnSynth": false,
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}",
"cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}",
"stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/2332c6df6777cc571585060fa4888d6d3b9ef548aa00dcbfc53fbdde386d7591.json",
"stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/5fbf2a286122f4bc412b1730f96351e289444b1122006f36e4ade8fae8442765.json",
"requiresBootstrapStackVersion": 6,
"bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version",
"additionalDependencies": [
Expand Down Expand Up @@ -51,10 +51,22 @@
"data": "TestApplicationAttributeGroupAssociation4ba7f5842818B8EE1C6F"
}
],
"/integ-servicecatalogappregistry-application/TestApplication/RAMSharead8ba81b8cdd": [
"/integ-servicecatalogappregistry-application/TestApplication/myAnotherAttributeGroup/Resource": [
{
"type": "aws:cdk:logicalId",
"data": "TestApplicationRAMSharead8ba81b8cdd40199FD1"
"data": "TestApplicationmyAnotherAttributeGroup375F79DB"
}
],
"/integ-servicecatalogappregistry-application/TestApplication/AttributeGroupAssociationb6f47e836a8c": [
{
"type": "aws:cdk:logicalId",
"data": "TestApplicationAttributeGroupAssociationb6f47e836a8c4FCAC29E"
}
],
"/integ-servicecatalogappregistry-application/TestApplication/RAMShare004736f08f8e": [
{
"type": "aws:cdk:logicalId",
"data": "TestApplicationRAMShare004736f08f8e57044D5D"
}
],
"/integ-servicecatalogappregistry-application/TestAttributeGroup/Resource": [
Expand All @@ -80,6 +92,15 @@
"type": "aws:cdk:logicalId",
"data": "CheckBootstrapVersion"
}
],
"TestApplicationRAMSharead8ba81b8cdd40199FD1": [
{
"type": "aws:cdk:logicalId",
"data": "TestApplicationRAMSharead8ba81b8cdd40199FD1",
"trace": [
"!!DESTRUCTIVE_CHANGES: WILL_DESTROY"
]
}
]
},
"displayName": "integ-servicecatalogappregistry-application"
Expand Down
Loading

0 comments on commit 7baffa2

Please sign in to comment.