Skip to content

Commit 30301d9

Browse files
authored
fix(servicecatalogappregistry): synth error when associating a nested stack (#23248)
In this PR, we are fixing the following: - For nested stack association, customers have observed a synth error `Nested stack cannot depend on a parent stack`. In this PR we are providing the fix for the same. - Adding Application manager URL as CDK output. Customers will now have ability to directly access Application Manager for the application created by AppRegistry L2 construct using the link provide in CDK and CFN output. - Unit test to verify conditional nested stack association can be handled by this L2 construct gracefully as the `ResourceAssociation` is getting created within the child stack. ### All Submissions: * [ X] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) ### Adding new Construct Runtime Dependencies: * [ ] This PR adds new construct runtime dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md/#adding-construct-runtime-dependencies) ### New Features * [ ] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/main/INTEGRATION_TESTS.md)? * [ ] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)? *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent b34d0b7 commit 30301d9

File tree

9 files changed

+164
-47
lines changed

9 files changed

+164
-47
lines changed

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

+11-2
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,6 @@ abstract class ApplicationBase extends cdk.Resource implements IApplication {
134134
/**
135135
* Associate stack with the application in the stack passed as parameter.
136136
*
137-
* If the stack is already associated, it will ignore duplicate request.
138137
* A stack can only be associated with one application.
139138
*/
140139
public associateApplicationWithStack(stack: cdk.Stack): void {
@@ -146,7 +145,7 @@ abstract class ApplicationBase extends cdk.Resource implements IApplication {
146145
});
147146

148147
this.associatedResources.add(stack.node.addr);
149-
if (stack !== cdk.Stack.of(this) && this.isSameAccount(stack) && !this.isStageScope(stack)) {
148+
if (stack !== cdk.Stack.of(this) && this.isSameAccount(stack) && !this.isStageScope(stack) && !stack.nested) {
150149
stack.addDependency(cdk.Stack.of(this));
151150
}
152151
}
@@ -251,6 +250,11 @@ export class Application extends ApplicationBase {
251250
});
252251
}
253252

253+
/**
254+
* Application manager URL for the Application.
255+
* @attribute
256+
*/
257+
public readonly applicationManagerUrl?: cdk.CfnOutput;
254258
public readonly applicationArn: string;
255259
public readonly applicationId: string;
256260
public readonly applicationName?: string;
@@ -270,6 +274,11 @@ export class Application extends ApplicationBase {
270274
this.applicationId = application.attrId;
271275
this.applicationName = props.applicationName;
272276
this.nodeAddress = cdk.Names.nodeUniqueId(application.node);
277+
278+
this.applicationManagerUrl = new cdk.CfnOutput(this, 'ApplicationManagerUrl', {
279+
value: `https://${this.env.region}.console.aws.amazon.com/systems-manager/appmanager/application/AWS_AppRegistry_Application-${this.applicationName}`,
280+
description: `Application manager url for application ${this.applicationName}`,
281+
});
273282
}
274283

275284
protected generateUniqueHash(resourceAddress: string): string {

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

+60-7
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,13 @@ describe('Application', () => {
3434

3535
test('application with explicit description', () => {
3636
const description = 'my test application description';
37-
new appreg.Application(stack, 'MyApplication', {
37+
const application = new appreg.Application(stack, 'MyApplication', {
3838
applicationName: 'testApplication',
3939
description: description,
4040
});
4141

42+
Template.fromStack(stack).hasOutput('MyApplicationApplicationManagerUrlB79EF34D', {});
43+
expect(application.applicationManagerUrl?.value).toContain('AWS_AppRegistry_Application-testApplication');
4244
Template.fromStack(stack).hasResourceProperties('AWS::ServiceCatalogAppRegistry::Application', {
4345
Description: description,
4446
});
@@ -254,7 +256,7 @@ describe('Application', () => {
254256

255257
Template.fromStack(stack).hasResourceProperties('AWS::RAM::ResourceShare', {
256258
AllowExternalPrincipals: false,
257-
Name: 'RAMSharee6e0e560e6f8',
259+
Name: 'RAMShare5bb637032063',
258260
Principals: ['arn:aws:organizations::123456789012:organization/o-70oi5564q1'],
259261
ResourceArns: [{ 'Fn::GetAtt': ['MyApplication5C63EC1D', 'Arn'] }],
260262
PermissionArns: ['arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryApplicationReadOnly'],
@@ -268,7 +270,7 @@ describe('Application', () => {
268270

269271
Template.fromStack(stack).hasResourceProperties('AWS::RAM::ResourceShare', {
270272
AllowExternalPrincipals: false,
271-
Name: 'RAMSharee6e0e560e6f8',
273+
Name: 'RAMShare5bb637032063',
272274
Principals: ['123456789012'],
273275
ResourceArns: [{ 'Fn::GetAtt': ['MyApplication5C63EC1D', 'Arn'] }],
274276
PermissionArns: ['arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryApplicationReadOnly'],
@@ -284,7 +286,7 @@ describe('Application', () => {
284286

285287
Template.fromStack(stack).hasResourceProperties('AWS::RAM::ResourceShare', {
286288
AllowExternalPrincipals: false,
287-
Name: 'RAMSharee6e0e560e6f8',
289+
Name: 'RAMShare5bb637032063',
288290
Principals: ['arn:aws:iam::123456789012:role/myRole'],
289291
ResourceArns: [{ 'Fn::GetAtt': ['MyApplication5C63EC1D', 'Arn'] }],
290292
PermissionArns: ['arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryApplicationReadOnly'],
@@ -300,7 +302,7 @@ describe('Application', () => {
300302

301303
Template.fromStack(stack).hasResourceProperties('AWS::RAM::ResourceShare', {
302304
AllowExternalPrincipals: false,
303-
Name: 'RAMSharee6e0e560e6f8',
305+
Name: 'RAMShare5bb637032063',
304306
Principals: ['arn:aws:iam::123456789012:user/myUser'],
305307
ResourceArns: [{ 'Fn::GetAtt': ['MyApplication5C63EC1D', 'Arn'] }],
306308
PermissionArns: ['arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryApplicationReadOnly'],
@@ -315,7 +317,7 @@ describe('Application', () => {
315317

316318
Template.fromStack(stack).hasResourceProperties('AWS::RAM::ResourceShare', {
317319
AllowExternalPrincipals: false,
318-
Name: 'RAMSharee6e0e560e6f8',
320+
Name: 'RAMShare5bb637032063',
319321
Principals: ['arn:aws:organizations::123456789012:organization/o-70oi5564q1'],
320322
ResourceArns: [{ 'Fn::GetAtt': ['MyApplication5C63EC1D', 'Arn'] }],
321323
PermissionArns: ['arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryApplicationReadOnly'],
@@ -330,7 +332,7 @@ describe('Application', () => {
330332

331333
Template.fromStack(stack).hasResourceProperties('AWS::RAM::ResourceShare', {
332334
AllowExternalPrincipals: false,
333-
Name: 'RAMSharee6e0e560e6f8',
335+
Name: 'RAMShare5bb637032063',
334336
Principals: ['arn:aws:organizations::123456789012:organization/o-70oi5564q1'],
335337
ResourceArns: [{ 'Fn::GetAtt': ['MyApplication5C63EC1D', 'Arn'] }],
336338
PermissionArns: ['arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryApplicationAllowAssociation'],
@@ -446,8 +448,59 @@ describe('Scope based Associations with Application with Cross Region/Account',
446448
});
447449
});
448450

451+
describe('Conditional nested stack Associations with Application within Same Account', () => {
452+
let app: cdk.App;
453+
beforeEach(() => {
454+
app = new cdk.App({
455+
context: {
456+
'@aws-cdk/core:newStyleStackSynthesis': false,
457+
},
458+
});
459+
});
460+
461+
test('Associate conditional nested stack with application', () => {
462+
const stack = new MainStack(app, 'cdkApplication');
463+
const application = new appreg.Application(stack, 'MyApplication', {
464+
applicationName: 'MyApplication',
465+
});
466+
application.associateApplicationWithStack(stack);
467+
application.associateApplicationWithStack(stack.nestedStack);
468+
Template.fromStack(stack.nestedStack).hasResource('AWS::ServiceCatalogAppRegistry::ResourceAssociation', {
469+
Properties: {
470+
Application: 'MyApplication',
471+
Resource: { Ref: 'AWS::StackId' },
472+
ResourceType: 'CFN_STACK',
473+
},
474+
});
475+
Template.fromStack(stack.nestedStack).hasCondition('ShouldCreateStackCondition', {
476+
'Fn::Equals': ['us-east-1'],
477+
});
478+
});
479+
480+
});
481+
482+
449483
class AppRegistrySampleStack extends cdk.Stack {
450484
public constructor(scope: Construct, id: string, props?: cdk.StackProps) {
451485
super(scope, id, props);
452486
}
453487
}
488+
489+
class MainStack extends cdk.Stack {
490+
public readonly nestedStack: cdk.Stack;
491+
public constructor(parent: cdk.App, id: string, props?: cdk.StackProps) {
492+
super(parent, id, props);
493+
this.nestedStack = new AppRegistryNestedStack(this, 'nested-stack');
494+
}
495+
}
496+
497+
class AppRegistryNestedStack extends cdk.NestedStack {
498+
public constructor(scope: Construct, id: string, props?: cdk.NestedStackProps) {
499+
super(scope, id, props);
500+
501+
const shouldCreateStack = new cdk.CfnCondition(this, 'ShouldCreateStackCondition', {
502+
expression: cdk.Fn.conditionEquals(process.env.CDK_DEFAULT_REGION, 'us-east-1'),
503+
});
504+
(this.nestedStackResource as cdk.CfnStack).cfnOptions.condition = shouldCreateStack;
505+
}
506+
}
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"version":"21.0.0"}
1+
{"version":"22.0.0"}

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
{
2-
"version": "20.0.0",
2+
"version": "22.0.0",
33
"files": {
4-
"d03aa6239eb3b20f4b72fb3dd44a4082d06d7a5451d0ac3855bd1aa78aecfbe9": {
4+
"806ae543572346400832fe865c1bdfa243ef2c1f07c3ce10aa0bd27ed4613a42": {
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": "d03aa6239eb3b20f4b72fb3dd44a4082d06d7a5451d0ac3855bd1aa78aecfbe9.json",
12+
"objectKey": "806ae543572346400832fe865c1bdfa243ef2c1f07c3ce10aa0bd27ed4613a42.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

+21-4
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"TestApplication2FBC585F": {
44
"Type": "AWS::ServiceCatalogAppRegistry::Application",
55
"Properties": {
6-
"Name": "myApplicationtest",
6+
"Name": "myCdkApplication",
77
"Description": "my application description"
88
}
99
},
@@ -39,10 +39,10 @@
3939
}
4040
}
4141
},
42-
"TestApplicationRAMSharead8ba81b8cdd40199FD1": {
42+
"TestApplicationRAMShare3dc6227daec11BF3E108": {
4343
"Type": "AWS::RAM::ResourceShare",
4444
"Properties": {
45-
"Name": "RAMSharead8ba81b8cdd",
45+
"Name": "RAMShare3dc6227daec1",
4646
"AllowExternalPrincipals": false,
4747
"PermissionArns": [
4848
"arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryApplicationReadOnly"
@@ -84,7 +84,7 @@
8484
"release": "go time"
8585
}
8686
},
87-
"Name": "myAttributeGroupTest",
87+
"Name": "myCdkAttributeGroup",
8888
"Description": "my attribute group description"
8989
}
9090
},
@@ -121,6 +121,23 @@
121121
}
122122
}
123123
},
124+
"Outputs": {
125+
"TestApplicationApplicationManagerUrlE1058321": {
126+
"Description": "Application manager url for application myCdkApplication",
127+
"Value": {
128+
"Fn::Join": [
129+
"",
130+
[
131+
"https://",
132+
{
133+
"Ref": "AWS::Region"
134+
},
135+
".console.aws.amazon.com/systems-manager/appmanager/application/AWS_AppRegistry_Application-myCdkApplication"
136+
]
137+
]
138+
}
139+
}
140+
},
124141
"Parameters": {
125142
"BootstrapVersion": {
126143
"Type": "AWS::SSM::Parameter::Value<String>",

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

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

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

+16-10
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
{
2-
"version": "20.0.0",
2+
"version": "22.0.0",
33
"artifacts": {
4-
"Tree": {
5-
"type": "cdk:tree",
6-
"properties": {
7-
"file": "tree.json"
8-
}
9-
},
104
"integ-servicecatalogappregistry-application.assets": {
115
"type": "cdk:asset-manifest",
126
"properties": {
@@ -23,7 +17,7 @@
2317
"validateOnSynth": false,
2418
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}",
2519
"cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}",
26-
"stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/d03aa6239eb3b20f4b72fb3dd44a4082d06d7a5451d0ac3855bd1aa78aecfbe9.json",
20+
"stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/806ae543572346400832fe865c1bdfa243ef2c1f07c3ce10aa0bd27ed4613a42.json",
2721
"requiresBootstrapStackVersion": 6,
2822
"bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version",
2923
"additionalDependencies": [
@@ -45,6 +39,12 @@
4539
"data": "TestApplication2FBC585F"
4640
}
4741
],
42+
"/integ-servicecatalogappregistry-application/TestApplication/ApplicationManagerUrl": [
43+
{
44+
"type": "aws:cdk:logicalId",
45+
"data": "TestApplicationApplicationManagerUrlE1058321"
46+
}
47+
],
4848
"/integ-servicecatalogappregistry-application/TestApplication/ResourceAssociationd232b63e52a8": [
4949
{
5050
"type": "aws:cdk:logicalId",
@@ -57,10 +57,10 @@
5757
"data": "TestApplicationAttributeGroupAssociation4ba7f5842818B8EE1C6F"
5858
}
5959
],
60-
"/integ-servicecatalogappregistry-application/TestApplication/RAMSharead8ba81b8cdd": [
60+
"/integ-servicecatalogappregistry-application/TestApplication/RAMShare3dc6227daec1": [
6161
{
6262
"type": "aws:cdk:logicalId",
63-
"data": "TestApplicationRAMSharead8ba81b8cdd40199FD1"
63+
"data": "TestApplicationRAMShare3dc6227daec11BF3E108"
6464
}
6565
],
6666
"/integ-servicecatalogappregistry-application/TestAttributeGroup/Resource": [
@@ -89,6 +89,12 @@
8989
]
9090
},
9191
"displayName": "integ-servicecatalogappregistry-application"
92+
},
93+
"Tree": {
94+
"type": "cdk:tree",
95+
"properties": {
96+
"file": "tree.json"
97+
}
9298
}
9399
}
94100
}

0 commit comments

Comments
 (0)