From 30301d9e5ab4af86c5e48a5ad47013924acdfed7 Mon Sep 17 00:00:00 2001 From: santanugho Date: Wed, 14 Dec 2022 09:50:17 -0800 Subject: [PATCH] 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* --- .../lib/application.ts | 13 +++- .../test/application.test.ts | 67 +++++++++++++++++-- .../integ.application.js.snapshot/cdk.out | 2 +- ...catalogappregistry-application.assets.json | 6 +- ...talogappregistry-application.template.json | 25 +++++-- .../integ.application.js.snapshot/integ.json | 2 +- .../manifest.json | 26 ++++--- .../integ.application.js.snapshot/tree.json | 66 +++++++++++++----- .../test/integ.application.ts | 4 +- 9 files changed, 164 insertions(+), 47 deletions(-) diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/lib/application.ts b/packages/@aws-cdk/aws-servicecatalogappregistry/lib/application.ts index eb5112e45287e..ee0465eae5d12 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/lib/application.ts +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/lib/application.ts @@ -134,7 +134,6 @@ abstract class ApplicationBase extends cdk.Resource implements IApplication { /** * Associate stack with the application in the stack passed as parameter. * - * If the stack is already associated, it will ignore duplicate request. * A stack can only be associated with one application. */ public associateApplicationWithStack(stack: cdk.Stack): void { @@ -146,7 +145,7 @@ abstract class ApplicationBase extends cdk.Resource implements IApplication { }); this.associatedResources.add(stack.node.addr); - if (stack !== cdk.Stack.of(this) && this.isSameAccount(stack) && !this.isStageScope(stack)) { + if (stack !== cdk.Stack.of(this) && this.isSameAccount(stack) && !this.isStageScope(stack) && !stack.nested) { stack.addDependency(cdk.Stack.of(this)); } } @@ -251,6 +250,11 @@ export class Application extends ApplicationBase { }); } + /** + * Application manager URL for the Application. + * @attribute + */ + public readonly applicationManagerUrl?: cdk.CfnOutput; public readonly applicationArn: string; public readonly applicationId: string; public readonly applicationName?: string; @@ -270,6 +274,11 @@ export class Application extends ApplicationBase { this.applicationId = application.attrId; this.applicationName = props.applicationName; this.nodeAddress = cdk.Names.nodeUniqueId(application.node); + + this.applicationManagerUrl = new cdk.CfnOutput(this, 'ApplicationManagerUrl', { + value: `https://${this.env.region}.console.aws.amazon.com/systems-manager/appmanager/application/AWS_AppRegistry_Application-${this.applicationName}`, + description: `Application manager url for application ${this.applicationName}`, + }); } protected generateUniqueHash(resourceAddress: string): string { diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/application.test.ts b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application.test.ts index dce0d1147ec44..7a14cda920d28 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/test/application.test.ts +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application.test.ts @@ -34,11 +34,13 @@ describe('Application', () => { test('application with explicit description', () => { const description = 'my test application description'; - new appreg.Application(stack, 'MyApplication', { + const application = new appreg.Application(stack, 'MyApplication', { applicationName: 'testApplication', description: description, }); + Template.fromStack(stack).hasOutput('MyApplicationApplicationManagerUrlB79EF34D', {}); + expect(application.applicationManagerUrl?.value).toContain('AWS_AppRegistry_Application-testApplication'); Template.fromStack(stack).hasResourceProperties('AWS::ServiceCatalogAppRegistry::Application', { Description: description, }); @@ -254,7 +256,7 @@ describe('Application', () => { Template.fromStack(stack).hasResourceProperties('AWS::RAM::ResourceShare', { AllowExternalPrincipals: false, - Name: 'RAMSharee6e0e560e6f8', + Name: 'RAMShare5bb637032063', Principals: ['arn:aws:organizations::123456789012:organization/o-70oi5564q1'], ResourceArns: [{ 'Fn::GetAtt': ['MyApplication5C63EC1D', 'Arn'] }], PermissionArns: ['arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryApplicationReadOnly'], @@ -268,7 +270,7 @@ describe('Application', () => { Template.fromStack(stack).hasResourceProperties('AWS::RAM::ResourceShare', { AllowExternalPrincipals: false, - Name: 'RAMSharee6e0e560e6f8', + Name: 'RAMShare5bb637032063', Principals: ['123456789012'], ResourceArns: [{ 'Fn::GetAtt': ['MyApplication5C63EC1D', 'Arn'] }], PermissionArns: ['arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryApplicationReadOnly'], @@ -284,7 +286,7 @@ describe('Application', () => { Template.fromStack(stack).hasResourceProperties('AWS::RAM::ResourceShare', { AllowExternalPrincipals: false, - Name: 'RAMSharee6e0e560e6f8', + Name: 'RAMShare5bb637032063', Principals: ['arn:aws:iam::123456789012:role/myRole'], ResourceArns: [{ 'Fn::GetAtt': ['MyApplication5C63EC1D', 'Arn'] }], PermissionArns: ['arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryApplicationReadOnly'], @@ -300,7 +302,7 @@ describe('Application', () => { Template.fromStack(stack).hasResourceProperties('AWS::RAM::ResourceShare', { AllowExternalPrincipals: false, - Name: 'RAMSharee6e0e560e6f8', + Name: 'RAMShare5bb637032063', Principals: ['arn:aws:iam::123456789012:user/myUser'], ResourceArns: [{ 'Fn::GetAtt': ['MyApplication5C63EC1D', 'Arn'] }], PermissionArns: ['arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryApplicationReadOnly'], @@ -315,7 +317,7 @@ describe('Application', () => { Template.fromStack(stack).hasResourceProperties('AWS::RAM::ResourceShare', { AllowExternalPrincipals: false, - Name: 'RAMSharee6e0e560e6f8', + Name: 'RAMShare5bb637032063', Principals: ['arn:aws:organizations::123456789012:organization/o-70oi5564q1'], ResourceArns: [{ 'Fn::GetAtt': ['MyApplication5C63EC1D', 'Arn'] }], PermissionArns: ['arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryApplicationReadOnly'], @@ -330,7 +332,7 @@ describe('Application', () => { Template.fromStack(stack).hasResourceProperties('AWS::RAM::ResourceShare', { AllowExternalPrincipals: false, - Name: 'RAMSharee6e0e560e6f8', + Name: 'RAMShare5bb637032063', Principals: ['arn:aws:organizations::123456789012:organization/o-70oi5564q1'], ResourceArns: [{ 'Fn::GetAtt': ['MyApplication5C63EC1D', 'Arn'] }], PermissionArns: ['arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryApplicationAllowAssociation'], @@ -446,8 +448,59 @@ describe('Scope based Associations with Application with Cross Region/Account', }); }); +describe('Conditional nested stack Associations with Application within Same Account', () => { + let app: cdk.App; + beforeEach(() => { + app = new cdk.App({ + context: { + '@aws-cdk/core:newStyleStackSynthesis': false, + }, + }); + }); + + test('Associate conditional nested stack with application', () => { + const stack = new MainStack(app, 'cdkApplication'); + const application = new appreg.Application(stack, 'MyApplication', { + applicationName: 'MyApplication', + }); + application.associateApplicationWithStack(stack); + application.associateApplicationWithStack(stack.nestedStack); + Template.fromStack(stack.nestedStack).hasResource('AWS::ServiceCatalogAppRegistry::ResourceAssociation', { + Properties: { + Application: 'MyApplication', + Resource: { Ref: 'AWS::StackId' }, + ResourceType: 'CFN_STACK', + }, + }); + Template.fromStack(stack.nestedStack).hasCondition('ShouldCreateStackCondition', { + 'Fn::Equals': ['us-east-1'], + }); + }); + +}); + + class AppRegistrySampleStack extends cdk.Stack { public constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); } } + +class MainStack extends cdk.Stack { + public readonly nestedStack: cdk.Stack; + public constructor(parent: cdk.App, id: string, props?: cdk.StackProps) { + super(parent, id, props); + this.nestedStack = new AppRegistryNestedStack(this, 'nested-stack'); + } +} + +class AppRegistryNestedStack extends cdk.NestedStack { + public constructor(scope: Construct, id: string, props?: cdk.NestedStackProps) { + super(scope, id, props); + + const shouldCreateStack = new cdk.CfnCondition(this, 'ShouldCreateStackCondition', { + expression: cdk.Fn.conditionEquals(process.env.CDK_DEFAULT_REGION, 'us-east-1'), + }); + (this.nestedStackResource as cdk.CfnStack).cfnOptions.condition = shouldCreateStack; + } +} diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/cdk.out b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/cdk.out index 8ecc185e9dbee..145739f539580 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/cdk.out +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/cdk.out @@ -1 +1 @@ -{"version":"21.0.0"} \ No newline at end of file +{"version":"22.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/integ-servicecatalogappregistry-application.assets.json b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/integ-servicecatalogappregistry-application.assets.json index 7f5db7d1e1328..225ef80716295 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/integ-servicecatalogappregistry-application.assets.json +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/integ-servicecatalogappregistry-application.assets.json @@ -1,7 +1,7 @@ { - "version": "20.0.0", + "version": "22.0.0", "files": { - "d03aa6239eb3b20f4b72fb3dd44a4082d06d7a5451d0ac3855bd1aa78aecfbe9": { + "806ae543572346400832fe865c1bdfa243ef2c1f07c3ce10aa0bd27ed4613a42": { "source": { "path": "integ-servicecatalogappregistry-application.template.json", "packaging": "file" @@ -9,7 +9,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "d03aa6239eb3b20f4b72fb3dd44a4082d06d7a5451d0ac3855bd1aa78aecfbe9.json", + "objectKey": "806ae543572346400832fe865c1bdfa243ef2c1f07c3ce10aa0bd27ed4613a42.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/integ-servicecatalogappregistry-application.template.json b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/integ-servicecatalogappregistry-application.template.json index e36d7a3b58794..33f4f2a1912fb 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/integ-servicecatalogappregistry-application.template.json +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/integ-servicecatalogappregistry-application.template.json @@ -3,7 +3,7 @@ "TestApplication2FBC585F": { "Type": "AWS::ServiceCatalogAppRegistry::Application", "Properties": { - "Name": "myApplicationtest", + "Name": "myCdkApplication", "Description": "my application description" } }, @@ -39,10 +39,10 @@ } } }, - "TestApplicationRAMSharead8ba81b8cdd40199FD1": { + "TestApplicationRAMShare3dc6227daec11BF3E108": { "Type": "AWS::RAM::ResourceShare", "Properties": { - "Name": "RAMSharead8ba81b8cdd", + "Name": "RAMShare3dc6227daec1", "AllowExternalPrincipals": false, "PermissionArns": [ "arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryApplicationReadOnly" @@ -84,7 +84,7 @@ "release": "go time" } }, - "Name": "myAttributeGroupTest", + "Name": "myCdkAttributeGroup", "Description": "my attribute group description" } }, @@ -121,6 +121,23 @@ } } }, + "Outputs": { + "TestApplicationApplicationManagerUrlE1058321": { + "Description": "Application manager url for application myCdkApplication", + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "AWS::Region" + }, + ".console.aws.amazon.com/systems-manager/appmanager/application/AWS_AppRegistry_Application-myCdkApplication" + ] + ] + } + } + }, "Parameters": { "BootstrapVersion": { "Type": "AWS::SSM::Parameter::Value", diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/integ.json b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/integ.json index 5178be112c70a..7db84c3e5dfa8 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/integ.json +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/integ.json @@ -1,5 +1,5 @@ { - "version": "20.0.0", + "version": "22.0.0", "testCases": { "integ.application": { "stacks": [ diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/manifest.json b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/manifest.json index d78791dfe49aa..a8cd9a83688d7 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/manifest.json +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/manifest.json @@ -1,12 +1,6 @@ { - "version": "20.0.0", + "version": "22.0.0", "artifacts": { - "Tree": { - "type": "cdk:tree", - "properties": { - "file": "tree.json" - } - }, "integ-servicecatalogappregistry-application.assets": { "type": "cdk:asset-manifest", "properties": { @@ -23,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}/d03aa6239eb3b20f4b72fb3dd44a4082d06d7a5451d0ac3855bd1aa78aecfbe9.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/806ae543572346400832fe865c1bdfa243ef2c1f07c3ce10aa0bd27ed4613a42.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ @@ -45,6 +39,12 @@ "data": "TestApplication2FBC585F" } ], + "/integ-servicecatalogappregistry-application/TestApplication/ApplicationManagerUrl": [ + { + "type": "aws:cdk:logicalId", + "data": "TestApplicationApplicationManagerUrlE1058321" + } + ], "/integ-servicecatalogappregistry-application/TestApplication/ResourceAssociationd232b63e52a8": [ { "type": "aws:cdk:logicalId", @@ -57,10 +57,10 @@ "data": "TestApplicationAttributeGroupAssociation4ba7f5842818B8EE1C6F" } ], - "/integ-servicecatalogappregistry-application/TestApplication/RAMSharead8ba81b8cdd": [ + "/integ-servicecatalogappregistry-application/TestApplication/RAMShare3dc6227daec1": [ { "type": "aws:cdk:logicalId", - "data": "TestApplicationRAMSharead8ba81b8cdd40199FD1" + "data": "TestApplicationRAMShare3dc6227daec11BF3E108" } ], "/integ-servicecatalogappregistry-application/TestAttributeGroup/Resource": [ @@ -89,6 +89,12 @@ ] }, "displayName": "integ-servicecatalogappregistry-application" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/tree.json b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/tree.json index f54792dcdef5c..da9f344585ed2 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/tree.json +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.js.snapshot/tree.json @@ -4,14 +4,6 @@ "id": "App", "path": "", "children": { - "Tree": { - "id": "Tree", - "path": "Tree", - "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.1.85" - } - }, "integ-servicecatalogappregistry-application": { "id": "integ-servicecatalogappregistry-application", "path": "integ-servicecatalogappregistry-application", @@ -26,7 +18,7 @@ "attributes": { "aws:cdk:cloudformation:type": "AWS::ServiceCatalogAppRegistry::Application", "aws:cdk:cloudformation:props": { - "name": "myApplicationtest", + "name": "myCdkApplication", "description": "my application description" } }, @@ -35,6 +27,14 @@ "version": "0.0.0" } }, + "ApplicationManagerUrl": { + "id": "ApplicationManagerUrl", + "path": "integ-servicecatalogappregistry-application/TestApplication/ApplicationManagerUrl", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnOutput", + "version": "0.0.0" + } + }, "ResourceAssociationd232b63e52a8": { "id": "ResourceAssociationd232b63e52a8", "path": "integ-servicecatalogappregistry-application/TestApplication/ResourceAssociationd232b63e52a8", @@ -83,13 +83,13 @@ "version": "0.0.0" } }, - "RAMSharead8ba81b8cdd": { - "id": "RAMSharead8ba81b8cdd", - "path": "integ-servicecatalogappregistry-application/TestApplication/RAMSharead8ba81b8cdd", + "RAMShare3dc6227daec1": { + "id": "RAMShare3dc6227daec1", + "path": "integ-servicecatalogappregistry-application/TestApplication/RAMShare3dc6227daec1", "attributes": { "aws:cdk:cloudformation:type": "AWS::RAM::ResourceShare", "aws:cdk:cloudformation:props": { - "name": "RAMSharead8ba81b8cdd", + "name": "RAMShare3dc6227daec1", "allowExternalPrincipals": false, "permissionArns": [ "arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryApplicationReadOnly" @@ -149,7 +149,7 @@ "release": "go time" } }, - "name": "myAttributeGroupTest", + "name": "myCdkAttributeGroup", "description": "my attribute group description" } }, @@ -168,6 +168,14 @@ "id": "MyRole", "path": "integ-servicecatalogappregistry-application/MyRole", "children": { + "ImportMyRole": { + "id": "ImportMyRole", + "path": "integ-servicecatalogappregistry-application/MyRole/ImportMyRole", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, "Resource": { "id": "Resource", "path": "integ-servicecatalogappregistry-application/MyRole/Resource", @@ -213,17 +221,41 @@ "fqn": "@aws-cdk/aws-iam.Role", "version": "0.0.0" } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "integ-servicecatalogappregistry-application/BootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "integ-servicecatalogappregistry-application/CheckBootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnRule", + "version": "0.0.0" + } } }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", "constructInfo": { "fqn": "constructs.Construct", - "version": "10.1.85" + "version": "10.1.168" } } }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.1.85" + "fqn": "@aws-cdk/core.App", + "version": "0.0.0" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.ts b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.ts index d51cad051252c..30f5e2bb88829 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.ts +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application.ts @@ -6,12 +6,12 @@ const app = new cdk.App(); const stack = new cdk.Stack(app, 'integ-servicecatalogappregistry-application'); const application = new appreg.Application(stack, 'TestApplication', { - applicationName: 'myApplicationtest', + applicationName: 'myCdkApplication', description: 'my application description', }); const attributeGroup = new appreg.AttributeGroup(stack, 'TestAttributeGroup', { - attributeGroupName: 'myAttributeGroupTest', + attributeGroupName: 'myCdkAttributeGroup', description: 'my attribute group description', attributes: { stage: 'alpha',