From c4e07a65f9580d3e39d4077d872bee6ad17d8924 Mon Sep 17 00:00:00 2001 From: Rohit Aggarwal Date: Thu, 3 Nov 2022 04:13:42 -0700 Subject: [PATCH] refactor(servicecatalogappregistry): prepare ApplicationAssociator for future extensions (#22644) Updated Input Structure for Application Associator L2 Construct * This helps user to pass the input using factory pattern * Factory Pattern makes construct extensible ---- ### All Submissions: * [x] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) ### Adding new Unconventional Dependencies: * [ ] This PR adds new unconventional dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md/#adding-new-unconventional-dependencies) ### New Features * [ ] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/main/INTEGRATION_TESTS.md)? * [x] 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* Co-authored by: Santanu Ghosh --- .../aws-servicecatalogappregistry/README.md | 30 ++--- .../lib/application-associator.ts | 48 ++----- .../lib/aspects/stack-associator.ts | 2 +- .../lib/index.ts | 1 + .../lib/target-application.ts | 121 ++++++++++++++++++ .../test/application-associator.test.ts | 62 +++++---- ...ation-associator.all-stacks-association.ts | 8 +- 7 files changed, 187 insertions(+), 85 deletions(-) create mode 100644 packages/@aws-cdk/aws-servicecatalogappregistry/lib/target-application.ts diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/README.md b/packages/@aws-cdk/aws-servicecatalogappregistry/README.md index c65fd7a66b2a6..44984461fbaf4 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/README.md +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/README.md @@ -73,12 +73,11 @@ and want to associate all stacks in the `App` scope to `MyAssociatedApplication` ```ts const app = new App(); const associatedApp = new appreg.ApplicationAssociator(app, 'AssociatedApplication', { + applications: [appreg.TargetApplication.createApplicationStack({ applicationName: 'MyAssociatedApplication', - description: 'Testing associated application', - stackProps: { - stackName: 'MyAssociatedApplicationStack', - env: {account: '123456789012', region: 'us-east-1'}, - }, + stackName: 'MyAssociatedApplicationStack', + env: { account: '123456789012', region: 'us-east-1' }, + })], }); ``` @@ -88,15 +87,15 @@ and want to associate all stacks in the `App` scope to your imported application ```ts const app = new App(); const associatedApp = new appreg.ApplicationAssociator(app, 'AssociatedApplication', { + applications: [appreg.TargetApplication.existingApplicationFromArn({ applicationArnValue: 'arn:aws:servicecatalog:us-east-1:123456789012:/applications/applicationId', - stackProps: { - stackName: 'MyAssociatedApplicationStack', - }, + stackName: 'MyAssociatedApplicationStack', + })], }); ``` -If you are using CDK Pipelines to deploy your application, the application stacks will be inside Stages, and -ApplicationAssociator will not be able to find them. Call `associateStage` on each Stage object before adding it to the +If you are using CDK Pipelines to deploy your application, the application stacks will be inside Stages, and +ApplicationAssociator will not be able to find them. Call `associateStage` on each Stage object before adding it to the Pipeline, as shown in the example below: ```ts @@ -109,7 +108,7 @@ declare const beta: cdk.Stage; class ApplicationPipelineStack extends cdk.Stack { constructor(scope: cdk.App, id: string, props: ApplicationPipelineStackProps) { super(scope, id, props); - + //associate the stage to application associator. props.application.associateStage(beta); pipeline.addStage(beta); @@ -122,12 +121,11 @@ interface ApplicationPipelineStackProps extends cdk.StackProps { const app = new App(); const associatedApp = new appreg.ApplicationAssociator(app, 'AssociatedApplication', { + applications: [appreg.TargetApplication.createApplicationStack({ applicationName: 'MyPipelineAssociatedApplication', - description: 'Testing pipeline associated app', - stackProps: { - stackName: 'MyPipelineAssociatedApplicationStack', - env: {account: '123456789012', region: 'us-east-1'}, - }, + stackName: 'MyPipelineAssociatedApplicationStack', + env: { account: '123456789012', region: 'us-east-1' }, + })], }); const cdkPipeline = new ApplicationPipelineStack(app, 'CDKApplicationPipelineStack', { diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/lib/application-associator.ts b/packages/@aws-cdk/aws-servicecatalogappregistry/lib/application-associator.ts index 226e9d6c452ec..2d6fafdac34e8 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/lib/application-associator.ts +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/lib/application-associator.ts @@ -1,38 +1,19 @@ import * as cdk from '@aws-cdk/core'; import { Construct } from 'constructs'; -import { IApplication, Application } from './application'; +import { IApplication } from './application'; import { CheckedStageStackAssociator } from './aspects/stack-associator'; +import { TargetApplication } from './target-application'; /** - * Properties for a Service Catalog AppRegistry AutoApplication + * Properties for Service Catalog AppRegistry Application Associator */ export interface ApplicationAssociatorProps { /** - * Enforces a particular physical application name. + * Application associator properties. * - * @default - No name. + * @default - Empty array. */ - readonly applicationName?: string; - - /** - * Enforces a particular application arn. - * - * @default - No application arn. - */ - readonly applicationArnValue?: string; - - /** - * Application description. - * - * @default - No description. - */ - readonly description?: string; - - /** - * Stack properties. - * - */ - readonly stackProps: cdk.StackProps; + readonly applications: TargetApplication[]; } /** @@ -56,19 +37,12 @@ export class ApplicationAssociator extends Construct { constructor(scope: cdk.App, id: string, props: ApplicationAssociatorProps) { super(scope, id); - const applicationStack = new cdk.Stack(scope, 'ApplicationAssociatorStack', props.stackProps); - - if (!!props.applicationArnValue) { - this.application = Application.fromApplicationArn(applicationStack, 'ImportedApplication', props.applicationArnValue); - } else if (!!props.applicationName) { - this.application = new Application(applicationStack, 'DefaultCdkApplication', { - applicationName: props.applicationName, - description: props.description, - }); - } else { - throw new Error('Please provide either ARN or application name.'); + if (props.applications.length != 1) { + throw new Error('Please pass exactly 1 instance of TargetApplication.createApplicationStack() or TargetApplication.existingApplicationFromArn() into the "applications" property'); } + const targetApplication = props.applications[0]; + this.application = targetApplication.bind(this).application; cdk.Aspects.of(scope).add(new CheckedStageStackAssociator(this)); } @@ -94,7 +68,7 @@ export class ApplicationAssociator extends Construct { * Get the AppRegistry application. * */ - get appRegistryApplication() { + public appRegistryApplication(): IApplication { return this.application; } } diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/lib/aspects/stack-associator.ts b/packages/@aws-cdk/aws-servicecatalogappregistry/lib/aspects/stack-associator.ts index 593bf8a8265d6..7694a519802b4 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/lib/aspects/stack-associator.ts +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/lib/aspects/stack-associator.ts @@ -115,7 +115,7 @@ export class CheckedStageStackAssociator extends StackAssociatorBase { constructor(app: ApplicationAssociator) { super(); - this.application = app.appRegistryApplication; + this.application = app.appRegistryApplication(); this.applicationAssociator = app; } } diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/lib/index.ts b/packages/@aws-cdk/aws-servicecatalogappregistry/lib/index.ts index 72239c4a1d110..57a975bc64d64 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/lib/index.ts +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/lib/index.ts @@ -1,6 +1,7 @@ export * from './application'; export * from './attribute-group'; export * from './application-associator'; +export * from './target-application'; export * from './common'; // AWS::ServiceCatalogAppRegistry CloudFormation Resources: diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/lib/target-application.ts b/packages/@aws-cdk/aws-servicecatalogappregistry/lib/target-application.ts new file mode 100644 index 0000000000000..71fe68e636ab1 --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/lib/target-application.ts @@ -0,0 +1,121 @@ +import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { IApplication, Application } from './application'; + +/** + * Properties used to define targetapplication. + */ +export interface TargetApplicationCommonOptions extends cdk.StackProps { + /** + * Stack ID in which application will be created or imported. + * + * @default - ApplicationAssociatorStack + */ + readonly stackId?: string; +} + + +/** + * Properties used to define New TargetApplication. + */ +export interface CreateTargetApplicationOptions extends TargetApplicationCommonOptions { + /** + * Enforces a particular physical application name. + */ + readonly applicationName: string; + + /** + * Application description. + * + * @default - No description. + */ + readonly applicationDescription?: string; +} + +/** + * Properties used to define Existing TargetApplication. + */ +export interface ExistingTargetApplicationOptions extends TargetApplicationCommonOptions { + /** + * Enforces a particular application arn. + */ + readonly applicationArnValue: string; +} + +/** + * Contains static factory methods with which you can build the input + * needed for application associator to work + */ +export abstract class TargetApplication { + /** + * Factory method to build the input using the provided + * application ARN. + */ + public static existingApplicationFromArn(options: ExistingTargetApplicationOptions) : TargetApplication { + return new ExistingTargetApplication(options); + } + + /** + * Factory method to build the input using the provided + * application name and stack props. + */ + public static createApplicationStack(options: CreateTargetApplicationOptions) : TargetApplication { + return new CreateTargetApplication(options); + } + + /** + * Called when the ApplicationAssociator is initialized + */ + public abstract bind(scope: Construct): BindTargetApplicationResult; +} + +/** + * Properties for Service Catalog AppRegistry Application Associator to work with + */ +export interface BindTargetApplicationResult { + /** + * Created or imported application. + */ + readonly application: IApplication; +} + +/** + * Class which constructs the input from provided application name and stack props. + * With this input, the construct will create the Application. + */ +class CreateTargetApplication extends TargetApplication { + constructor( + private readonly applicationOptions: CreateTargetApplicationOptions) { + super(); + } + public bind(scope: Construct): BindTargetApplicationResult { + const stackId = this.applicationOptions.stackId ?? 'ApplicationAssociatorStack'; + const applicationStack = new cdk.Stack(scope, stackId, this.applicationOptions); + const appRegApplication = new Application(applicationStack, 'DefaultCdkApplication', { + applicationName: this.applicationOptions.applicationName, + description: this.applicationOptions.applicationDescription, + }); + + return { + application: appRegApplication, + }; + } +} + +/** + * Class which constructs the input from provided Application ARN. + */ +class ExistingTargetApplication extends TargetApplication { + constructor( + private readonly applicationOptions: ExistingTargetApplicationOptions) { + super(); + } + public bind(scope: Construct): BindTargetApplicationResult { + const stackId = this.applicationOptions.stackId ?? 'ApplicationAssociatorStack'; + const applicationStack = new cdk.Stack(scope, stackId, this.applicationOptions); + const appRegApplication = Application.fromApplicationArn(applicationStack, 'ExistingApplication', this.applicationOptions.applicationArnValue); + return { + application: appRegApplication, + }; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.test.ts b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.test.ts index de64c0c35b68b..0d0cf74062e29 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.test.ts +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.test.ts @@ -16,10 +16,10 @@ describe('Scope based Associations with Application within Same Account', () => }); test('ApplicationAssociator will associate allStacks created inside cdkApp', () => { new appreg.ApplicationAssociator(app, 'MyApplication', { - applicationName: 'MyAssociatedApplication', - stackProps: { + applications: [appreg.TargetApplication.createApplicationStack({ + applicationName: 'MyAssociatedApplication', stackName: 'MyAssociatedApplicationStack', - }, + })], }); const anotherStack = new AppRegistrySampleStack(app, 'SampleStack'); Template.fromStack(anotherStack).resourceCountIs('AWS::ServiceCatalogAppRegistry::ResourceAssociation', 1); @@ -40,10 +40,10 @@ describe('Scope based Associations with Application with Cross Region/Account', }); test('ApplicationAssociator in cross-account associates all stacks created inside cdk app', () => { new appreg.ApplicationAssociator(app, 'MyApplication', { - applicationName: 'MyAssociatedApplication', - stackProps: { + applications: [appreg.TargetApplication.createApplicationStack({ + applicationName: 'MyAssociatedApplication', stackName: 'MyAssociatedApplicationStack', - }, + })], }); const firstStack = new cdk.Stack(app, 'testStack', { env: { account: 'account2', region: 'region' }, @@ -58,21 +58,31 @@ describe('Scope based Associations with Application with Cross Region/Account', test('ApplicationAssociator creation failed when neither Application name nor ARN is provided', () => { expect(() => { new appreg.ApplicationAssociator(app, 'MyApplication', { - stackProps: { - stackName: 'MyAssociatedApplicationStack', - }, + applications: [], + }); + }).toThrow('Please pass exactly 1 instance of TargetApplication.createApplicationStack() or TargetApplication.existingApplicationFromArn() into the "applications" property'); + }); + + test('ApplicationAssociator creation failed when both Application name and ARN is provided', () => { + expect(() => { + new appreg.ApplicationAssociator(app, 'MyApplication', { + applications: [appreg.TargetApplication.existingApplicationFromArn({ + applicationArnValue: 'arn:aws:servicecatalog:us-east-1:482211128593:/applications/0a17wtxeg5vilok0sbxfozwpq9', + }), + appreg.TargetApplication.createApplicationStack({ + applicationName: 'MyAssociatedApplication', + })], }); - }).toThrow(/Please provide either ARN or application name./); + }).toThrow('Please pass exactly 1 instance of TargetApplication.createApplicationStack() or TargetApplication.existingApplicationFromArn() into the "applications" property'); }); test('associate resource on imported application', () => { const resource = new cdk.Stack(app, 'MyStack'); new appreg.ApplicationAssociator(app, 'MyApplication', { - applicationArnValue: 'arn:aws:servicecatalog:us-east-1:482211128593:/applications/0a17wtxeg5vilok0sbxfozwpq9', - stackProps: { - stackName: 'MyAssociatedApplicationStack', - }, + applications: [appreg.TargetApplication.existingApplicationFromArn({ + applicationArnValue: 'arn:aws:servicecatalog:us-east-1:482211128593:/applications/0a17wtxeg5vilok0sbxfozwpq9', + })], }); Template.fromStack(resource).hasResourceProperties('AWS::ServiceCatalogAppRegistry::ResourceAssociation', { @@ -83,11 +93,11 @@ describe('Scope based Associations with Application with Cross Region/Account', test('ApplicationAssociator with cross region stacks inside cdkApp throws error', () => { new appreg.ApplicationAssociator(app, 'MyApplication', { - applicationName: 'MyAssociatedApplication', - stackProps: { + applications: [appreg.TargetApplication.createApplicationStack({ + applicationName: 'MyAssociatedApplication', stackName: 'MyAssociatedApplicationStack', env: { account: 'account2', region: 'region2' }, - }, + })], }); const crossRegionStack = new cdk.Stack(app, 'crossRegionStack', { @@ -98,10 +108,10 @@ describe('Scope based Associations with Application with Cross Region/Account', test('Environment Agnostic ApplicationAssociator with cross region stacks inside cdkApp gives warning', () => { new appreg.ApplicationAssociator(app, 'MyApplication', { - applicationName: 'MyAssociatedApplication', - stackProps: { + applications: [appreg.TargetApplication.createApplicationStack({ + applicationName: 'MyAssociatedApplication', stackName: 'MyAssociatedApplicationStack', - }, + })], }); const crossRegionStack = new cdk.Stack(app, 'crossRegionStack', { @@ -112,10 +122,10 @@ describe('Scope based Associations with Application with Cross Region/Account', test('Cdk App Containing Pipeline with stage but stage not associated throws error', () => { const application = new appreg.ApplicationAssociator(app, 'MyApplication', { - applicationName: 'MyAssociatedApplication', - stackProps: { + applications: [appreg.TargetApplication.createApplicationStack({ + applicationName: 'MyAssociatedApplication', stackName: 'MyAssociatedApplicationStack', - }, + })], }); const pipelineStack = new AppRegistrySampleCodePipelineStack(app, 'PipelineStackA', { application: application, @@ -128,10 +138,10 @@ describe('Scope based Associations with Application with Cross Region/Account', test('Cdk App Containing Pipeline with stage and stage associated successfully gets synthesized', () => { const application = new appreg.ApplicationAssociator(app, 'MyApplication', { - applicationName: 'MyAssociatedApplication', - stackProps: { + applications: [appreg.TargetApplication.createApplicationStack({ + applicationName: 'MyAssociatedApplication', stackName: 'MyAssociatedApplicationStack', - }, + })], }); const pipelineStack = new AppRegistrySampleCodePipelineStack(app, 'PipelineStackA', { application: application, diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association.ts b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association.ts index afd822dc6dc16..d3849f5aed251 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association.ts +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association.ts @@ -2,17 +2,15 @@ import * as cdk from '@aws-cdk/core'; import * as appreg from '../lib'; - const app = new cdk.App(); const stack = new cdk.Stack(app, 'integ-servicecatalogappregistry-application'); new appreg.ApplicationAssociator(app, 'RegisterCdkApplication', { - applicationName: 'AppRegistryAssociatedApplication', - description: 'Testing AppRegistry ApplicationAssociator', - stackProps: { + applications: [appreg.TargetApplication.createApplicationStack({ + applicationName: 'AppRegistryAssociatedApplication', stackName: 'AppRegistryApplicationAssociatorStack', - }, + })], }); new cdk.Stack(stack, 'resourcesStack');