Skip to content

Commit d1264c1

Browse files
authored
fix(servicecatalogappregistry): Associate an application with attribute group (#24378)
`Application-AttributeGroup` association happens in `ApplicationStack`. Therefore before the deployment of `ApplicationStack`, `AttributeGroup` stack should have been deployed. But with `ApplicationAssociator `where we associate all the stacks with the application (created as a part of `ApplicationAssociator`), attributeGroup stack now depends upon `ApplicationAssociator` stack to be created (since ResourceAssociation happens inside ResourceStack). This creates a circular dependency and hence cdk synth fails. We found this bug during internal testing This PR address this circular dependency issue, by allowing the customers of `ApplicationAssociator` to associate an attribute group in attribute group stack. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 29bdd6c commit d1264c1

17 files changed

+180
-25
lines changed

Diff for: packages/@aws-cdk/aws-servicecatalogappregistry/README.md

+49
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,45 @@ const associatedApp = new appreg.ApplicationAssociator(app, 'AssociatedApplicati
9999
});
100100
```
101101

102+
If you want to associate an Attribute Group with application created by `ApplicationAssociator`, then use as shown in the example below:
103+
104+
```ts
105+
import * as cdk from "@aws-cdk/core";
106+
107+
const app = new App();
108+
109+
class CustomAppRegistryAttributeGroup extends cdk.Stack {
110+
public readonly attributeGroup: appreg.AttributeGroup
111+
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
112+
super(scope, id, props);
113+
const myAttributeGroup = new appreg.AttributeGroup(app, 'MyFirstAttributeGroup', {
114+
attributeGroupName: 'MyAttributeGroupName',
115+
description: 'Test attribute group',
116+
attributes: {},
117+
});
118+
119+
this.attributeGroup = myAttributeGroup;
120+
}
121+
}
122+
123+
const customAttributeGroup = new CustomAppRegistryAttributeGroup(app, 'AppRegistryAttributeGroup');
124+
125+
const associatedApp = new appreg.ApplicationAssociator(app, 'AssociatedApplication', {
126+
applications: [appreg.TargetApplication.createApplicationStack({
127+
applicationName: 'MyAssociatedApplication',
128+
// 'Application containing stacks deployed via CDK.' is the default
129+
applicationDescription: 'Associated Application description',
130+
stackName: 'MyAssociatedApplicationStack',
131+
// AWS Account and Region that are implied by the current CLI configuration is the default
132+
env: { account: '123456789012', region: 'us-east-1' },
133+
})],
134+
});
135+
136+
// Associate application to the attribute group.
137+
customAttributeGroup.attributeGroup.associateWith(associatedApp.appRegistryApplication());
138+
139+
```
140+
102141
If you are using CDK Pipelines to deploy your application, the application stacks will be inside Stages, and
103142
ApplicationAssociator will not be able to find them. Call `associateStage` on each Stage object before adding it to the
104143
Pipeline, as shown in the example below:
@@ -191,6 +230,16 @@ declare const attributeGroup: appreg.AttributeGroup;
191230
application.associateAttributeGroup(attributeGroup);
192231
```
193232

233+
### Associating an attribute group with application
234+
235+
You can associate an application with an attribute group with `associateWith`:
236+
237+
```ts
238+
declare const application: appreg.Application;
239+
declare const attributeGroup: appreg.AttributeGroup;
240+
attributeGroup.associateWith(application);
241+
```
242+
194243
### Associating application with a Stack
195244

196245
You can associate a stack with an application with the `associateStack()` API:

Diff for: packages/@aws-cdk/aws-servicecatalogappregistry/lib/application.ts

+2
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ abstract class ApplicationBase extends cdk.Resource implements IApplication {
100100
/**
101101
* Associate an attribute group with application
102102
* If the attribute group is already associated, it will ignore duplicate request.
103+
*
104+
* @deprecated Use `AttributeGroup.associateWith` instead.
103105
*/
104106
public associateAttributeGroup(attributeGroup: IAttributeGroup): void {
105107
if (!this.associatedAttributeGroups.has(attributeGroup.node.addr)) {

Diff for: packages/@aws-cdk/aws-servicecatalogappregistry/lib/attribute-group.ts

+34-1
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import { CfnResourceShare } from '@aws-cdk/aws-ram';
22
import * as cdk from '@aws-cdk/core';
33
import { Names } from '@aws-cdk/core';
44
import { Construct } from 'constructs';
5+
import { IApplication } from './application';
56
import { getPrincipalsforSharing, hashValues, ShareOptions, SharePermission } from './common';
67
import { InputValidator } from './private/validation';
7-
import { CfnAttributeGroup } from './servicecatalogappregistry.generated';
8+
import { CfnAttributeGroup, CfnAttributeGroupAssociation } from './servicecatalogappregistry.generated';
89

910
const ATTRIBUTE_GROUP_READ_ONLY_RAM_PERMISSION_ARN = 'arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryAttributeGroupReadOnly';
1011
const ATTRIBUTE_GROUP_ALLOW_ACCESS_RAM_PERMISSION_ARN = 'arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryAttributeGroupAllowAssociation';
@@ -58,6 +59,23 @@ export interface AttributeGroupProps {
5859
abstract class AttributeGroupBase extends cdk.Resource implements IAttributeGroup {
5960
public abstract readonly attributeGroupArn: string;
6061
public abstract readonly attributeGroupId: string;
62+
private readonly associatedApplications: Set<string> = new Set();
63+
64+
/**
65+
* Associate an application with attribute group
66+
* If the attribute group is already associated, it will ignore duplicate request.
67+
*/
68+
public associateWith(application: IApplication): void {
69+
if (!this.associatedApplications.has(application.node.addr)) {
70+
const hashId = this.generateUniqueHash(application.node.addr);
71+
new CfnAttributeGroupAssociation(this, `ApplicationAttributeGroupAssociation${hashId}`, {
72+
application: application.stack === cdk.Stack.of(this) ? application.applicationId : application.applicationName ?? application.applicationId,
73+
attributeGroup: this.attributeGroupId,
74+
});
75+
76+
this.associatedApplications.add(application.node.addr);
77+
}
78+
}
6179

6280
public shareAttributeGroup(shareOptions: ShareOptions): void {
6381
const principals = getPrincipalsforSharing(shareOptions);
@@ -85,6 +103,11 @@ abstract class AttributeGroupBase extends cdk.Resource implements IAttributeGrou
85103
return shareOptions.sharePermission ?? ATTRIBUTE_GROUP_READ_ONLY_RAM_PERMISSION_ARN;
86104
}
87105
}
106+
107+
/**
108+
* Create a unique hash
109+
*/
110+
protected abstract generateUniqueHash(resourceAddress: string): string;
88111
}
89112

90113
/**
@@ -109,6 +132,10 @@ export class AttributeGroup extends AttributeGroupBase implements IAttributeGrou
109132
class Import extends AttributeGroupBase {
110133
public readonly attributeGroupArn = attributeGroupArn;
111134
public readonly attributeGroupId = attributeGroupId!;
135+
136+
protected generateUniqueHash(resourceAddress: string): string {
137+
return hashValues(this.attributeGroupArn, resourceAddress);
138+
}
112139
}
113140

114141
return new Import(scope, id, {
@@ -118,6 +145,7 @@ export class AttributeGroup extends AttributeGroupBase implements IAttributeGrou
118145

119146
public readonly attributeGroupArn: string;
120147
public readonly attributeGroupId: string;
148+
private readonly nodeAddress: string;
121149

122150
constructor(scope: Construct, id: string, props: AttributeGroupProps) {
123151
super(scope, id);
@@ -132,6 +160,11 @@ export class AttributeGroup extends AttributeGroupBase implements IAttributeGrou
132160

133161
this.attributeGroupArn = attributeGroup.attrArn;
134162
this.attributeGroupId = attributeGroup.attrId;
163+
this.nodeAddress = cdk.Names.nodeUniqueId(attributeGroup.node);
164+
}
165+
166+
protected generateUniqueHash(resourceAddress: string): string {
167+
return hashValues(this.nodeAddress, resourceAddress);
135168
}
136169

137170
private validateAttributeGroupProps(props: AttributeGroupProps) {

Diff for: packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.test.ts

+47
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,38 @@ describe('Scope based Associations with Application within Same Account', () =>
3535
});
3636
});
3737
});
38+
39+
describe('Associate attribute group with Application', () => {
40+
let app: cdk.App;
41+
beforeEach(() => {
42+
app = new cdk.App({
43+
context: {
44+
'@aws-cdk/core:newStyleStackSynthesis': false,
45+
},
46+
});
47+
});
48+
49+
test('Associate Attribute Group with application created by ApplicationAssociator', () => {
50+
51+
const customAttributeGroup = new CustomAppRegistryAttributeGroup(app, 'AppRegistryAttributeGroup');
52+
53+
const appAssociator = new appreg.ApplicationAssociator(app, 'TestApplication', {
54+
applications: [appreg.TargetApplication.createApplicationStack({
55+
applicationName: 'TestAssociatedApplication',
56+
stackName: 'TestAssociatedApplicationStack',
57+
})],
58+
});
59+
60+
customAttributeGroup.attributeGroup.associateWith(appAssociator.appRegistryApplication());
61+
Template.fromStack(customAttributeGroup.attributeGroup.stack).resourceCountIs('AWS::ServiceCatalogAppRegistry::AttributeGroupAssociation', 1);
62+
Template.fromStack(customAttributeGroup.attributeGroup.stack).hasResourceProperties('AWS::ServiceCatalogAppRegistry::AttributeGroupAssociation', {
63+
Application: 'TestAssociatedApplication',
64+
AttributeGroup: { 'Fn::GetAtt': ['MyFirstAttributeGroupDBC21379', 'Id'] },
65+
});
66+
67+
});
68+
});
69+
3870
describe('Scope based Associations with Application with Cross Region/Account', () => {
3971
let app: cdk.App;
4072
beforeEach(() => {
@@ -211,3 +243,18 @@ class AppRegistrySampleStack extends cdk.Stack {
211243
super(scope, id, props);
212244
}
213245
}
246+
247+
class CustomAppRegistryAttributeGroup extends cdk.Stack {
248+
public readonly attributeGroup: appreg.AttributeGroup;
249+
250+
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
251+
super(scope, id, props);
252+
const myAttributeGroup = new appreg.AttributeGroup(this, 'MyFirstAttributeGroup', {
253+
attributeGroupName: 'MyFirstAttributeGroupName',
254+
description: 'Test attribute group',
255+
attributes: {},
256+
});
257+
258+
this.attributeGroup = myAttributeGroup;
259+
}
260+
}

Diff for: packages/@aws-cdk/aws-servicecatalogappregistry/test/attribute-group.test.ts

+24
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,30 @@ describe('Attribute Group', () => {
176176
});
177177
});
178178

179+
describe('Associate application to an attribute group', () => {
180+
let attributeGroup: appreg.AttributeGroup;
181+
182+
beforeEach(() => {
183+
attributeGroup = new appreg.AttributeGroup(stack, 'MyAttributeGroupForAssociation', {
184+
attributeGroupName: 'MyAttributeGroupForAssociation',
185+
attributes: {},
186+
});
187+
});
188+
189+
test('Associate an application to an attribute group', () => {
190+
const application = new appreg.Application(stack, 'MyApplication', {
191+
applicationName: 'MyTestApplication',
192+
});
193+
attributeGroup.associateWith(application);
194+
Template.fromStack(stack).hasResourceProperties('AWS::ServiceCatalogAppRegistry::AttributeGroupAssociation', {
195+
Application: { 'Fn::GetAtt': ['MyApplication5C63EC1D', 'Id'] },
196+
AttributeGroup: { 'Fn::GetAtt': ['MyAttributeGroupForAssociation6B3E1329', 'Id'] },
197+
});
198+
199+
});
200+
201+
});
202+
179203
describe('Resource sharing of an attribute group', () => {
180204
let attributeGroup: appreg.AttributeGroup;
181205

Diff for: packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/integ-servicecatalogappregistry-application.assets.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
{
22
"version": "30.1.0",
33
"files": {
4-
"6a426aab2239a5fb580c074adbf5b8e3acefa04209423d2b53989c73aed3f95b": {
4+
"0d4e060fe5da6b164b9df46b0dc0cd20e7962c6cb531ffe08e6e5b99418f13de": {
55
"source": {
66
"path": "integ-servicecatalogappregistry-application.template.json",
77
"packaging": "file"
88
},
99
"destinations": {
1010
"current_account-current_region": {
1111
"bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}",
12-
"objectKey": "6a426aab2239a5fb580c074adbf5b8e3acefa04209423d2b53989c73aed3f95b.json",
12+
"objectKey": "0d4e060fe5da6b164b9df46b0dc0cd20e7962c6cb531ffe08e6e5b99418f13de.json",
1313
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}"
1414
}
1515
}

Diff for: packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/integ-servicecatalogappregistry-application.template.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
"TestApplication2FBC585F": {
44
"Type": "AWS::ServiceCatalogAppRegistry::Application",
55
"Properties": {
6-
"Name": "MyTestApplication",
7-
"Description": "Test application description"
6+
"Name": "TestApplication",
7+
"Description": "My application description"
88
}
99
},
1010
"TestApplicationResourceAssociationd232b63e52a8414E905D": {
@@ -132,7 +132,7 @@
132132
{
133133
"Ref": "AWS::Region"
134134
},
135-
".console.aws.amazon.com/systems-manager/appmanager/application/AWS_AppRegistry_Application-MyTestApplication"
135+
".console.aws.amazon.com/systems-manager/appmanager/application/AWS_AppRegistry_Application-TestApplication"
136136
]
137137
]
138138
}

Diff for: packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/manifest.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"validateOnSynth": false,
1818
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}",
1919
"cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}",
20-
"stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/6a426aab2239a5fb580c074adbf5b8e3acefa04209423d2b53989c73aed3f95b.json",
20+
"stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/0d4e060fe5da6b164b9df46b0dc0cd20e7962c6cb531ffe08e6e5b99418f13de.json",
2121
"requiresBootstrapStackVersion": 6,
2222
"bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version",
2323
"additionalDependencies": [

Diff for: packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/tree.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
"attributes": {
1919
"aws:cdk:cloudformation:type": "AWS::ServiceCatalogAppRegistry::Application",
2020
"aws:cdk:cloudformation:props": {
21-
"name": "MyTestApplication",
22-
"description": "Test application description"
21+
"name": "TestApplication",
22+
"description": "My application description"
2323
}
2424
},
2525
"constructInfo": {

Diff for: packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ const app = new cdk.App();
66
const stack = new cdk.Stack(app, 'integ-servicecatalogappregistry-application');
77

88
const application = new appreg.Application(stack, 'TestApplication', {
9-
applicationName: 'MyTestApplication',
10-
description: 'Test application description',
9+
applicationName: 'TestApplication',
10+
description: 'My application description',
1111
});
1212

1313
const attributeGroup = new appreg.AttributeGroup(stack, 'TestAttributeGroup', {
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"version":"22.0.0"}
1+
{"version":"30.1.0"}

Diff for: packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.attribute-group.js.snapshot/integ-servicecatalogappregistry-attribute-group.assets.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
{
2-
"version": "22.0.0",
2+
"version": "30.1.0",
33
"files": {
4-
"3dece22dad73361a79cb380f2880362a20ffc5c0cc75ddc6707e26b5a88cf93f": {
4+
"9d37fdefa4311937f8f73f9556f1d9a03a2874545a0a262fd42bfde3823ab551": {
55
"source": {
66
"path": "integ-servicecatalogappregistry-attribute-group.template.json",
77
"packaging": "file"
88
},
99
"destinations": {
1010
"current_account-current_region": {
1111
"bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}",
12-
"objectKey": "3dece22dad73361a79cb380f2880362a20ffc5c0cc75ddc6707e26b5a88cf93f.json",
12+
"objectKey": "9d37fdefa4311937f8f73f9556f1d9a03a2874545a0a262fd42bfde3823ab551.json",
1313
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}"
1414
}
1515
}

Diff for: packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.attribute-group.js.snapshot/integ-servicecatalogappregistry-attribute-group.template.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
"beta": "time2"
1616
}
1717
},
18-
"Name": "myAttributeGroupTest",
19-
"Description": "my attribute group description"
18+
"Name": "myFirstAttributeGroup",
19+
"Description": "test attribute group description"
2020
}
2121
},
2222
"TestAttributeGroupRAMSharec67f7d80e5baA10EFB4E": {

Diff for: packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.attribute-group.js.snapshot/integ.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"version": "22.0.0",
2+
"version": "30.1.0",
33
"testCases": {
44
"integ.attribute-group": {
55
"stacks": [

Diff for: packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.attribute-group.js.snapshot/manifest.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"version": "22.0.0",
2+
"version": "30.1.0",
33
"artifacts": {
44
"integ-servicecatalogappregistry-attribute-group.assets": {
55
"type": "cdk:asset-manifest",
@@ -17,7 +17,7 @@
1717
"validateOnSynth": false,
1818
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}",
1919
"cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}",
20-
"stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/3dece22dad73361a79cb380f2880362a20ffc5c0cc75ddc6707e26b5a88cf93f.json",
20+
"stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/9d37fdefa4311937f8f73f9556f1d9a03a2874545a0a262fd42bfde3823ab551.json",
2121
"requiresBootstrapStackVersion": 6,
2222
"bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version",
2323
"additionalDependencies": [

Diff for: packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.attribute-group.js.snapshot/tree.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@
3030
"beta": "time2"
3131
}
3232
},
33-
"name": "myAttributeGroupTest",
34-
"description": "my attribute group description"
33+
"name": "myFirstAttributeGroup",
34+
"description": "test attribute group description"
3535
}
3636
},
3737
"constructInfo": {
@@ -228,7 +228,7 @@
228228
"path": "Tree",
229229
"constructInfo": {
230230
"fqn": "constructs.Construct",
231-
"version": "10.1.189"
231+
"version": "10.1.252"
232232
}
233233
}
234234
},

Diff for: packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.attribute-group.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ const app = new cdk.App();
66
const stack = new cdk.Stack(app, 'integ-servicecatalogappregistry-attribute-group');
77

88
const attributeGroup = new appreg.AttributeGroup(stack, 'TestAttributeGroup', {
9-
attributeGroupName: 'myAttributeGroupTest',
10-
description: 'my attribute group description',
9+
attributeGroupName: 'myFirstAttributeGroup',
10+
description: 'test attribute group description',
1111
attributes: {
1212
stage: 'alpha',
1313
teamMembers: [

0 commit comments

Comments
 (0)