diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/README.md b/packages/@aws-cdk/aws-servicecatalogappregistry/README.md index e96a13bc8b2db..c65fd7a66b2a6 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/README.md +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/README.md @@ -21,12 +21,13 @@ -[AWS Service Catalog App Registry](https://docs.aws.amazon.com/servicecatalog/latest/adminguide/appregistry.html) +[AWS Service Catalog App Registry](https://docs.aws.amazon.com/servicecatalog/latest/adminguide/appregistry.html) enables organizations to create and manage repositores of applications and associated resources. ## Table Of Contents - [Application](#application) +- [Application-Associator](#application-associator) - [Attribute-Group](#attribute-group) - [Associations](#associations) - [Associating application with an attribute group](#attribute-group-association) @@ -44,11 +45,11 @@ import * as appreg from '@aws-cdk/aws-servicecatalogappregistry'; ## Application An AppRegistry application enables you to define your applications and associated resources. -The application name must be unique at the account level, but is mutable. +The application name must be unique at the account level and it's immutable. ```ts const application = new appreg.Application(this, 'MyFirstApplication', { - applicationName: 'MyFirstApplicationName', + applicationName: 'MyFirstApplicationName', description: 'description for my application', // the description is optional }); ``` @@ -64,6 +65,77 @@ const importedApplication = appreg.Application.fromApplicationArn( ); ``` +## Application-Associator + +If you want to create an Application named `MyAssociatedApplication` in account `123456789012` and region `us-east-1` +and want to associate all stacks in the `App` scope to `MyAssociatedApplication`, then use as shown in the example below: + +```ts +const app = new App(); +const associatedApp = new appreg.ApplicationAssociator(app, 'AssociatedApplication', { + applicationName: 'MyAssociatedApplication', + description: 'Testing associated application', + stackProps: { + stackName: 'MyAssociatedApplicationStack', + env: {account: '123456789012', region: 'us-east-1'}, + }, +}); +``` + +If you want to re-use an existing Application with ARN: `arn:aws:servicecatalog:us-east-1:123456789012:/applications/applicationId` +and want to associate all stacks in the `App` scope to your imported application, then use as shown in the example below: + +```ts +const app = new App(); +const associatedApp = new appreg.ApplicationAssociator(app, 'AssociatedApplication', { + applicationArnValue: 'arn:aws:servicecatalog:us-east-1:123456789012:/applications/applicationId', + stackProps: { + 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 +Pipeline, as shown in the example below: + +```ts +import * as cdk from "@aws-cdk/core"; +import * as codepipeline from "@aws-cdk/pipelines"; +import * as codecommit from "@aws-cdk/aws-codecommit"; +declare const repo: codecommit.Repository; +declare const pipeline: codepipeline.CodePipeline; +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); + } +}; + +interface ApplicationPipelineStackProps extends cdk.StackProps { + application: appreg.ApplicationAssociator; +}; + +const app = new App(); +const associatedApp = new appreg.ApplicationAssociator(app, 'AssociatedApplication', { + applicationName: 'MyPipelineAssociatedApplication', + description: 'Testing pipeline associated app', + stackProps: { + stackName: 'MyPipelineAssociatedApplicationStack', + env: {account: '123456789012', region: 'us-east-1'}, + }, +}); + +const cdkPipeline = new ApplicationPipelineStack(app, 'CDKApplicationPipelineStack', { + application: associatedApp, + env: {account: '123456789012', region: 'us-east-1'}, +}); +``` + ## Attribute Group An AppRegistry attribute group acts as a container for user-defined attributes for an application. @@ -71,7 +143,7 @@ Metadata is attached in a machine-readble format to integrate with automated wor ```ts const attributeGroup = new appreg.AttributeGroup(this, 'MyFirstAttributeGroup', { - attributeGroupName: 'MyFirstAttributeGroupName', + attributeGroupName: 'MyFirstAttributeGroupName', description: 'description for my attribute group', // the description is optional, attributes: { project: 'foo', @@ -104,7 +176,7 @@ Resources are CloudFormation stacks that you can associate with an application t stacks together to enable metadata rich insights into your applications and resources. A Cloudformation stack can only be associated with one appregistry application. If a stack is associated with multiple applications in your app or is already associated with one, -CDK will fail at deploy time. +CDK will fail at deploy time. ### Associating application with an attribute group diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/lib/application-associator.ts b/packages/@aws-cdk/aws-servicecatalogappregistry/lib/application-associator.ts new file mode 100644 index 0000000000000..226e9d6c452ec --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/lib/application-associator.ts @@ -0,0 +1,100 @@ +import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { IApplication, Application } from './application'; +import { CheckedStageStackAssociator } from './aspects/stack-associator'; + +/** + * Properties for a Service Catalog AppRegistry AutoApplication + */ +export interface ApplicationAssociatorProps { + /** + * Enforces a particular physical application name. + * + * @default - No name. + */ + 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; +} + +/** + * An AppRegistry construct to automatically create an application with the given name and description. + * + * The application name must be unique at the account level and it's immutable. + * This construct will automatically associate all stacks in the given scope, however + * in case of a `Pipeline` stack, stage underneath the pipeline will not automatically be associated and + * needs to be associated separately. + * + * If cross account stack is detected, then this construct will automatically share the application to consumer accounts. + * Cross account feature will only work for non environment agnostic stacks. + */ +export class ApplicationAssociator extends Construct { + /** + * Created or imported application. + */ + private readonly application: IApplication; + private readonly associatedStages: Set = new Set(); + + 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.'); + } + + cdk.Aspects.of(scope).add(new CheckedStageStackAssociator(this)); + } + + /** + * Associate this application with the given stage. + * + */ + public associateStage(stage: cdk.Stage): cdk.Stage { + this.associatedStages.add(stage); + cdk.Aspects.of(stage).add(new CheckedStageStackAssociator(this)); + return stage; + } + + /** + * Validates if a stage is already associated to the application. + * + */ + public isStageAssociated(stage: cdk.Stage): boolean { + return this.associatedStages.has(stage); + } + + /** + * Get the AppRegistry application. + * + */ + get appRegistryApplication() { + return this.application; + } +} diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/lib/application.ts b/packages/@aws-cdk/aws-servicecatalogappregistry/lib/application.ts index 256de4099afd0..eb5112e45287e 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/lib/application.ts +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/lib/application.ts @@ -2,8 +2,10 @@ import { CfnResourceShare } from '@aws-cdk/aws-ram'; 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 { getPrincipalsforSharing, hashValues, ShareOptions, SharePermission } from './common'; +import { isAccountUnresolved } from './private/utils'; import { InputValidator } from './private/validation'; import { CfnApplication, CfnAttributeGroupAssociation, CfnResourceAssociation } from './servicecatalogappregistry.generated'; @@ -27,7 +29,13 @@ export interface IApplication extends cdk.IResource { readonly applicationId: string; /** - * Associate thisapplication with an attribute group. + * The name of the application. + * @attribute + */ + readonly applicationName?: string; + + /** + * Associate this application with an attribute group. * * @param attributeGroup AppRegistry attribute group */ @@ -36,16 +44,34 @@ export interface IApplication extends cdk.IResource { /** * Associate this application with a CloudFormation stack. * + * @deprecated Use `associateApplicationWithStack` instead. * @param stack a CFN stack */ associateStack(stack: cdk.Stack): void; + /** + * Associate a Cloudformation statck with the application in the given stack. + * + * @param stack a CFN stack + */ + associateApplicationWithStack(stack: cdk.Stack): void; + /** * Share this application with other IAM entities, accounts, or OUs. * * @param shareOptions The options for the share. */ shareApplication(shareOptions: ShareOptions): void; + + /** + * Associate this application with all stacks under the construct node. + * NOTE: This method won't automatically register stacks under pipeline stages, + * and requires association of each pipeline stage by calling this method with stage Construct. + * + * @param construct cdk Construct + */ + associateAllStacksInScope(construct: Construct): void; + } /** @@ -67,6 +93,7 @@ export interface ApplicationProps { abstract class ApplicationBase extends cdk.Resource implements IApplication { public abstract readonly applicationArn: string; public abstract readonly applicationId: string; + public abstract readonly applicationName?: string; private readonly associatedAttributeGroups: Set = new Set(); private readonly associatedResources: Set = new Set(); @@ -89,6 +116,8 @@ abstract class ApplicationBase extends cdk.Resource implements IApplication { * Associate a stack with the application * If the resource is already associated, it will ignore duplicate request. * A stack can only be associated with one application. + * + * @deprecated Use `associateApplicationWithStack` instead. */ public associateStack(stack: cdk.Stack): void { if (!this.associatedResources.has(stack.node.addr)) { @@ -102,6 +131,27 @@ 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 { + if (!this.associatedResources.has(stack.node.addr)) { + new CfnResourceAssociation(stack, 'AppRegistryAssociation', { + application: stack === cdk.Stack.of(this) ? this.applicationId : this.applicationName ?? this.applicationId, + resource: stack.stackId, + resourceType: 'CFN_STACK', + }); + + this.associatedResources.add(stack.node.addr); + if (stack !== cdk.Stack.of(this) && this.isSameAccount(stack) && !this.isStageScope(stack)) { + stack.addDependency(cdk.Stack.of(this)); + } + } + } + /** * Share an application with accounts, organizations and OUs, and IAM roles and users. * The application will become available to end users within those principals. @@ -120,6 +170,17 @@ abstract class ApplicationBase extends cdk.Resource implements IApplication { }); } + /** + * Associate all stacks present in construct's aspect with application. + * + * NOTE: This method won't automatically register stacks under pipeline stages, + * and requires association of each pipeline stage by calling this method with stage Construct. + * + */ + public associateAllStacksInScope(scope: Construct): void { + cdk.Aspects.of(scope).add(new StageStackAssociator(this)); + } + /** * Create a unique id */ @@ -139,6 +200,21 @@ abstract class ApplicationBase extends cdk.Resource implements IApplication { return shareOptions.sharePermission ?? APPLICATION_READ_ONLY_RAM_PERMISSION_ARN; } } + + /** + * Checks whether a stack is defined in a Stage or not. + */ + private isStageScope(stack : cdk.Stack): boolean { + return !(stack.node.scope instanceof cdk.App) && (stack.node.scope instanceof cdk.Stage); + } + + /** + * Verifies if application and the visited node is deployed in different account. + */ + private isSameAccount(stack: cdk.Stack): boolean { + return isAccountUnresolved(this.env.account, stack.account) || this.env.account === stack.account; + } + } /** @@ -163,6 +239,7 @@ export class Application extends ApplicationBase { class Import extends ApplicationBase { public readonly applicationArn = applicationArn; public readonly applicationId = applicationId!; + public readonly applicationName = undefined; protected generateUniqueHash(resourceAddress: string): string { return hashValues(this.applicationArn, resourceAddress); @@ -176,6 +253,7 @@ export class Application extends ApplicationBase { public readonly applicationArn: string; public readonly applicationId: string; + public readonly applicationName?: string; private readonly nodeAddress: string; constructor(scope: Construct, id: string, props: ApplicationProps) { @@ -190,6 +268,7 @@ export class Application extends ApplicationBase { this.applicationArn = application.attrArn; this.applicationId = application.attrId; + this.applicationName = props.applicationName; this.nodeAddress = cdk.Names.nodeUniqueId(application.node); } diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/lib/aspects/stack-associator.ts b/packages/@aws-cdk/aws-servicecatalogappregistry/lib/aspects/stack-associator.ts new file mode 100644 index 0000000000000..593bf8a8265d6 --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/lib/aspects/stack-associator.ts @@ -0,0 +1,131 @@ +import { IAspect, Stack, Stage, Annotations } from '@aws-cdk/core'; +import { IConstruct } from 'constructs'; +import { IApplication } from '../application'; +import { ApplicationAssociator } from '../application-associator'; +import { SharePermission } from '../common'; +import { isRegionUnresolved, isAccountUnresolved } from '../private/utils'; + +/** + * Aspect class, this will visit each node from the provided construct once. + * + * For every stack node visited, this class will be responsible to associate + * the stack to the application. + */ +abstract class StackAssociatorBase implements IAspect { + protected abstract readonly application: IApplication; + protected abstract readonly applicationAssociator?: ApplicationAssociator; + + protected readonly sharedAccounts: Set = new Set(); + + public visit(node: IConstruct): void { + // verify if a stage in a particular stack is associated to Application. + node.node.children.forEach((childNode) => { + if (Stage.isStage(childNode)) { + var stageAssociated = this.applicationAssociator?.isStageAssociated(childNode); + if (stageAssociated === false) { + this.error(childNode, 'Associate Stage: ' + childNode.stageName + ' to ensure all stacks in your cdk app are associated with AppRegistry. ' + + 'You can use ApplicationAssociator.associateStage to associate any stage.'); + } + } + }); + + if (Stack.isStack(node)) { + this.handleCrossRegionStack(node); + this.handleCrossAccountStack(node); + this.associate(node); + } + } + + /** + * Associate a stage stack to the given application. + * + * @param node A Stage stack. + */ + private associate(node: Stack): void { + this.application.associateApplicationWithStack(node); + } + + /** + * Adds an error annotation to a node. + * + * @param node The scope to add the error to. + * @param message The error message. + */ + private error(node: IConstruct, message: string): void { + Annotations.of(node).addError(message); + } + + /** + * Adds a warning annotation to a node. + * + * @param node The scope to add the warning to. + * @param message The error message. + */ + private warning(node: IConstruct, message: string): void { + Annotations.of(node).addWarning(message); + } + + /** + * Handle cross-region association. AppRegistry do not support + * cross region association at this moment, + * If any stack is evaluated as cross-region than that of application, + * we will throw an error. + * + * @param node Cfn stack. + */ + private handleCrossRegionStack(node: Stack): void { + if (isRegionUnresolved(this.application.env.region, node.region)) { + this.warning(node, 'Environment agnostic stack determined, AppRegistry association might not work as expected in case you deploy cross-region or cross-account stack.'); + return; + } + + if (node.region != this.application.env.region) { + this.error(node, 'AppRegistry does not support cross region associations. Application region ' + + this.application.env.region + ', stack region ' + node.region); + } + } + + /** + * Handle cross-account association. + * If any stack is evaluated as cross-account than that of application, + * then we will share the application to the stack owning account. + * + * @param node Cfn stack. + */ + private handleCrossAccountStack(node: Stack): void { + if (isAccountUnresolved(this.application.env.account!, node.account)) { + this.warning(node, 'Environment agnostic stack determined, AppRegistry association might not work as expected in case you deploy cross-region or cross-account stack.'); + return; + } + + if (node.account != this.application.env.account && !this.sharedAccounts.has(node.account)) { + this.application.shareApplication({ + accounts: [node.account], + sharePermission: SharePermission.ALLOW_ACCESS, + }); + + this.sharedAccounts.add(node.account); + } + } +} + +export class CheckedStageStackAssociator extends StackAssociatorBase { + protected readonly application: IApplication; + protected readonly applicationAssociator?: ApplicationAssociator; + + constructor(app: ApplicationAssociator) { + super(); + this.application = app.appRegistryApplication; + this.applicationAssociator = app; + } +} + +export class StageStackAssociator extends StackAssociatorBase { + protected readonly application: IApplication; + protected readonly applicationAssociator?: ApplicationAssociator; + + constructor(app: IApplication) { + super(); + this.application = app; + } +} diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/lib/attribute-group.ts b/packages/@aws-cdk/aws-servicecatalogappregistry/lib/attribute-group.ts index ea5a893422aa5..d6dda21fe797d 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/lib/attribute-group.ts +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/lib/attribute-group.ts @@ -1,10 +1,10 @@ import { CfnResourceShare } from '@aws-cdk/aws-ram'; import * as cdk from '@aws-cdk/core'; -import { getPrincipalsforSharing, hashValues, ShareOptions, SharePermission } from './common'; +import { Names } from '@aws-cdk/core'; import { Construct } from 'constructs'; +import { getPrincipalsforSharing, hashValues, ShareOptions, SharePermission } from './common'; import { InputValidator } from './private/validation'; import { CfnAttributeGroup } from './servicecatalogappregistry.generated'; -import { Names } from '@aws-cdk/core'; const ATTRIBUTE_GROUP_READ_ONLY_RAM_PERMISSION_ARN = 'arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryAttributeGroupReadOnly'; const ATTRIBUTE_GROUP_ALLOW_ACCESS_RAM_PERMISSION_ARN = 'arn:aws:ram::aws:permission/AWSRAMPermissionServiceCatalogAppRegistryAttributeGroupAllowAssociation'; diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/lib/index.ts b/packages/@aws-cdk/aws-servicecatalogappregistry/lib/index.ts index adbf2a9febfe6..72239c4a1d110 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/lib/index.ts +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/lib/index.ts @@ -1,5 +1,6 @@ export * from './application'; export * from './attribute-group'; +export * from './application-associator'; export * from './common'; // AWS::ServiceCatalogAppRegistry CloudFormation Resources: diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/lib/private/utils.ts b/packages/@aws-cdk/aws-servicecatalogappregistry/lib/private/utils.ts new file mode 100644 index 0000000000000..cdbf97a0f7337 --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/lib/private/utils.ts @@ -0,0 +1,22 @@ +import { Token } from '@aws-cdk/core'; + + +/** + * Verifies if application or the visited node is region agnostic. + * + * @param applicationRegion Region of the application. + * @param nodeRegion Region of the visited node. + */ +export function isRegionUnresolved(applicationRegion: string, nodeRegion: string): boolean { + return Token.isUnresolved(applicationRegion) || Token.isUnresolved(nodeRegion); +} + +/** + * Verifies if application or the visited node is account agnostic. + * + * @param applicationAccount Account of the application. + * @param nodeAccount Account of the visited node. + */ +export function isAccountUnresolved(applicationAccount: string, nodeAccount: string): boolean { + return Token.isUnresolved(applicationAccount) || Token.isUnresolved(nodeAccount); +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/package.json b/packages/@aws-cdk/aws-servicecatalogappregistry/package.json index f4d1243ef7542..aa5401b6ac812 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/package.json +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/package.json @@ -88,19 +88,24 @@ "@aws-cdk/integ-runner": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", + "@aws-cdk/aws-codecommit": "0.0.0", "@types/jest": "^27.5.2" }, "dependencies": { "@aws-cdk/core": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-ram": "0.0.0", + "@aws-cdk/aws-codepipeline": "0.0.0", + "@aws-cdk/pipelines": "0.0.0", "constructs": "^10.0.0" }, "peerDependencies": { "@aws-cdk/core": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-ram": "0.0.0", - "constructs": "^10.0.0" + "@aws-cdk/aws-codepipeline": "0.0.0", + "constructs": "^10.0.0", + "@aws-cdk/pipelines": "0.0.0" }, "engines": { "node": ">= 14.15.0" @@ -113,5 +118,10 @@ "publishConfig": { "tag": "latest" }, + "awslint": { + "exclude": [ + "construct-ctor:@aws-cdk/aws-servicecatalogappregistry.ApplicationAssociator..params[0]" + ] + }, "private": true } diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.all-stacks-association.integ.snapshot/ApplicationAssociatorStack.assets.json b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.all-stacks-association.integ.snapshot/ApplicationAssociatorStack.assets.json new file mode 100644 index 0000000000000..2f99ef79e2174 --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.all-stacks-association.integ.snapshot/ApplicationAssociatorStack.assets.json @@ -0,0 +1,19 @@ +{ + "version": "21.0.0", + "files": { + "4285054f947789e255e76c75b889b3b216adabb0b3f990c8966c18459cdf7b35": { + "source": { + "path": "ApplicationAssociatorStack.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "4285054f947789e255e76c75b889b3b216adabb0b3f990c8966c18459cdf7b35.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.all-stacks-association.integ.snapshot/ApplicationAssociatorStack.template.json b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.all-stacks-association.integ.snapshot/ApplicationAssociatorStack.template.json new file mode 100644 index 0000000000000..79c9dec209b9e --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.all-stacks-association.integ.snapshot/ApplicationAssociatorStack.template.json @@ -0,0 +1,60 @@ +{ + "Resources": { + "DefaultCdkApplication4573D5A3": { + "Type": "AWS::ServiceCatalogAppRegistry::Application", + "Properties": { + "Name": "AppRegistryAssociatedApplication", + "Description": "Testing AppRegistry ApplicationAssociator" + } + }, + "AppRegistryAssociation": { + "Type": "AWS::ServiceCatalogAppRegistry::ResourceAssociation", + "Properties": { + "Application": { + "Fn::GetAtt": [ + "DefaultCdkApplication4573D5A3", + "Id" + ] + }, + "Resource": { + "Ref": "AWS::StackId" + }, + "ResourceType": "CFN_STACK" + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.all-stacks-association.integ.snapshot/cdk.out b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.all-stacks-association.integ.snapshot/cdk.out new file mode 100644 index 0000000000000..8ecc185e9dbee --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.all-stacks-association.integ.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"21.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.all-stacks-association.integ.snapshot/integ-servicecatalogappregistry-application.assets.json b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.all-stacks-association.integ.snapshot/integ-servicecatalogappregistry-application.assets.json new file mode 100644 index 0000000000000..5dec59ac54517 --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.all-stacks-association.integ.snapshot/integ-servicecatalogappregistry-application.assets.json @@ -0,0 +1,19 @@ +{ + "version": "21.0.0", + "files": { + "19dd33f3c17e59cafd22b9459b0a8d9bedbd42252737fedb06b2bcdbcf7809cc": { + "source": { + "path": "integ-servicecatalogappregistry-application.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "19dd33f3c17e59cafd22b9459b0a8d9bedbd42252737fedb06b2bcdbcf7809cc.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.all-stacks-association.integ.snapshot/integ-servicecatalogappregistry-application.template.json b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.all-stacks-association.integ.snapshot/integ-servicecatalogappregistry-application.template.json new file mode 100644 index 0000000000000..ecc817b74774a --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.all-stacks-association.integ.snapshot/integ-servicecatalogappregistry-application.template.json @@ -0,0 +1,48 @@ +{ + "Resources": { + "AppRegistryAssociation": { + "Type": "AWS::ServiceCatalogAppRegistry::ResourceAssociation", + "Properties": { + "Application": "AppRegistryAssociatedApplication", + "Resource": { + "Ref": "AWS::StackId" + }, + "ResourceType": "CFN_STACK" + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.all-stacks-association.integ.snapshot/integ.json b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.all-stacks-association.integ.snapshot/integ.json new file mode 100644 index 0000000000000..1f1a144803dcd --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.all-stacks-association.integ.snapshot/integ.json @@ -0,0 +1,14 @@ +{ + "version": "21.0.0", + "testCases": { + "integ.application-associator.all-stacks-association": { + "stacks": [ + "integ-servicecatalogappregistry-application" + ], + "diffAssets": false, + "stackUpdateWorkflow": true + } + }, + "synthContext": {}, + "enableLookups": false +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.all-stacks-association.integ.snapshot/integservicecatalogappregistryapplicationresourcesStack4399A149.assets.json b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.all-stacks-association.integ.snapshot/integservicecatalogappregistryapplicationresourcesStack4399A149.assets.json new file mode 100644 index 0000000000000..65c6d2c1ecf64 --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.all-stacks-association.integ.snapshot/integservicecatalogappregistryapplicationresourcesStack4399A149.assets.json @@ -0,0 +1,19 @@ +{ + "version": "21.0.0", + "files": { + "19dd33f3c17e59cafd22b9459b0a8d9bedbd42252737fedb06b2bcdbcf7809cc": { + "source": { + "path": "integservicecatalogappregistryapplicationresourcesStack4399A149.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "19dd33f3c17e59cafd22b9459b0a8d9bedbd42252737fedb06b2bcdbcf7809cc.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.all-stacks-association.integ.snapshot/integservicecatalogappregistryapplicationresourcesStack4399A149.template.json b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.all-stacks-association.integ.snapshot/integservicecatalogappregistryapplicationresourcesStack4399A149.template.json new file mode 100644 index 0000000000000..ecc817b74774a --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.all-stacks-association.integ.snapshot/integservicecatalogappregistryapplicationresourcesStack4399A149.template.json @@ -0,0 +1,48 @@ +{ + "Resources": { + "AppRegistryAssociation": { + "Type": "AWS::ServiceCatalogAppRegistry::ResourceAssociation", + "Properties": { + "Application": "AppRegistryAssociatedApplication", + "Resource": { + "Ref": "AWS::StackId" + }, + "ResourceType": "CFN_STACK" + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.all-stacks-association.integ.snapshot/manifest.json b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.all-stacks-association.integ.snapshot/manifest.json new file mode 100644 index 0000000000000..65db06b047d68 --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.all-stacks-association.integ.snapshot/manifest.json @@ -0,0 +1,209 @@ +{ + "version": "21.0.0", + "artifacts": { + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + }, + "integservicecatalogappregistryapplicationresourcesStack4399A149.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "integservicecatalogappregistryapplicationresourcesStack4399A149.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "integservicecatalogappregistryapplicationresourcesStack4399A149": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "integservicecatalogappregistryapplicationresourcesStack4399A149.template.json", + "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}/19dd33f3c17e59cafd22b9459b0a8d9bedbd42252737fedb06b2bcdbcf7809cc.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "integservicecatalogappregistryapplicationresourcesStack4399A149.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "ApplicationAssociatorStack", + "integservicecatalogappregistryapplicationresourcesStack4399A149.assets" + ], + "metadata": { + "/integ-servicecatalogappregistry-application/resourcesStack": [ + { + "type": "aws:cdk:warning", + "data": "Environment agnostic stack determined, AppRegistry association might not work as expected in case you deploy cross-region or cross-account stack." + }, + { + "type": "aws:cdk:warning", + "data": "Environment agnostic stack determined, AppRegistry association might not work as expected in case you deploy cross-region or cross-account stack." + } + ], + "/integ-servicecatalogappregistry-application/resourcesStack/AppRegistryAssociation": [ + { + "type": "aws:cdk:logicalId", + "data": "AppRegistryAssociation" + } + ], + "/integ-servicecatalogappregistry-application/resourcesStack/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/integ-servicecatalogappregistry-application/resourcesStack/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "integ-servicecatalogappregistry-application/resourcesStack" + }, + "integ-servicecatalogappregistry-application.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "integ-servicecatalogappregistry-application.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "integ-servicecatalogappregistry-application": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "integ-servicecatalogappregistry-application.template.json", + "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}/19dd33f3c17e59cafd22b9459b0a8d9bedbd42252737fedb06b2bcdbcf7809cc.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "integ-servicecatalogappregistry-application.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "ApplicationAssociatorStack", + "integ-servicecatalogappregistry-application.assets" + ], + "metadata": { + "/integ-servicecatalogappregistry-application": [ + { + "type": "aws:cdk:warning", + "data": "Environment agnostic stack determined, AppRegistry association might not work as expected in case you deploy cross-region or cross-account stack." + }, + { + "type": "aws:cdk:warning", + "data": "Environment agnostic stack determined, AppRegistry association might not work as expected in case you deploy cross-region or cross-account stack." + } + ], + "/integ-servicecatalogappregistry-application/AppRegistryAssociation": [ + { + "type": "aws:cdk:logicalId", + "data": "AppRegistryAssociation" + } + ], + "/integ-servicecatalogappregistry-application/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/integ-servicecatalogappregistry-application/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "integ-servicecatalogappregistry-application" + }, + "ApplicationAssociatorStack.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "ApplicationAssociatorStack.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "ApplicationAssociatorStack": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "ApplicationAssociatorStack.template.json", + "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}/4285054f947789e255e76c75b889b3b216adabb0b3f990c8966c18459cdf7b35.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "ApplicationAssociatorStack.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + }, + "stackName": "AppRegistryApplicationAssociatorStack" + }, + "dependencies": [ + "ApplicationAssociatorStack.assets" + ], + "metadata": { + "/ApplicationAssociatorStack": [ + { + "type": "aws:cdk:warning", + "data": "Environment agnostic stack determined, AppRegistry association might not work as expected in case you deploy cross-region or cross-account stack." + }, + { + "type": "aws:cdk:warning", + "data": "Environment agnostic stack determined, AppRegistry association might not work as expected in case you deploy cross-region or cross-account stack." + } + ], + "/ApplicationAssociatorStack/DefaultCdkApplication/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "DefaultCdkApplication4573D5A3" + } + ], + "/ApplicationAssociatorStack/AppRegistryAssociation": [ + { + "type": "aws:cdk:logicalId", + "data": "AppRegistryAssociation" + } + ], + "/ApplicationAssociatorStack/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/ApplicationAssociatorStack/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "ApplicationAssociatorStack" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.all-stacks-association.integ.snapshot/tree.json b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.all-stacks-association.integ.snapshot/tree.json new file mode 100644 index 0000000000000..078cc92093b86 --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.all-stacks-association.integ.snapshot/tree.json @@ -0,0 +1,143 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.95" + } + }, + "integ-servicecatalogappregistry-application": { + "id": "integ-servicecatalogappregistry-application", + "path": "integ-servicecatalogappregistry-application", + "children": { + "resourcesStack": { + "id": "resourcesStack", + "path": "integ-servicecatalogappregistry-application/resourcesStack", + "children": { + "AppRegistryAssociation": { + "id": "AppRegistryAssociation", + "path": "integ-servicecatalogappregistry-application/resourcesStack/AppRegistryAssociation", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ServiceCatalogAppRegistry::ResourceAssociation", + "aws:cdk:cloudformation:props": { + "application": "AppRegistryAssociatedApplication", + "resource": { + "Ref": "AWS::StackId" + }, + "resourceType": "CFN_STACK" + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-servicecatalogappregistry.CfnResourceAssociation", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + }, + "AppRegistryAssociation": { + "id": "AppRegistryAssociation", + "path": "integ-servicecatalogappregistry-application/AppRegistryAssociation", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ServiceCatalogAppRegistry::ResourceAssociation", + "aws:cdk:cloudformation:props": { + "application": "AppRegistryAssociatedApplication", + "resource": { + "Ref": "AWS::StackId" + }, + "resourceType": "CFN_STACK" + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-servicecatalogappregistry.CfnResourceAssociation", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + }, + "RegisterCdkApplication": { + "id": "RegisterCdkApplication", + "path": "RegisterCdkApplication", + "constructInfo": { + "fqn": "@aws-cdk/aws-servicecatalogappregistry.ApplicationAssociator", + "version": "0.0.0" + } + }, + "ApplicationAssociatorStack": { + "id": "ApplicationAssociatorStack", + "path": "ApplicationAssociatorStack", + "children": { + "DefaultCdkApplication": { + "id": "DefaultCdkApplication", + "path": "ApplicationAssociatorStack/DefaultCdkApplication", + "children": { + "Resource": { + "id": "Resource", + "path": "ApplicationAssociatorStack/DefaultCdkApplication/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ServiceCatalogAppRegistry::Application", + "aws:cdk:cloudformation:props": { + "name": "AppRegistryAssociatedApplication", + "description": "Testing AppRegistry ApplicationAssociator" + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-servicecatalogappregistry.CfnApplication", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-servicecatalogappregistry.Application", + "version": "0.0.0" + } + }, + "AppRegistryAssociation": { + "id": "AppRegistryAssociation", + "path": "ApplicationAssociatorStack/AppRegistryAssociation", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ServiceCatalogAppRegistry::ResourceAssociation", + "aws:cdk:cloudformation:props": { + "application": { + "Fn::GetAtt": [ + "DefaultCdkApplication4573D5A3", + "Id" + ] + }, + "resource": { + "Ref": "AWS::StackId" + }, + "resourceType": "CFN_STACK" + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-servicecatalogappregistry.CfnResourceAssociation", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.App", + "version": "0.0.0" + } + } +} \ 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 new file mode 100644 index 0000000000000..de64c0c35b68b --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application-associator.test.ts @@ -0,0 +1,196 @@ +import { Annotations, Template } from '@aws-cdk/assertions'; +import * as codecommit from '@aws-cdk/aws-codecommit'; +import * as cdk from '@aws-cdk/core'; +import * as codepipeline from '@aws-cdk/pipelines'; +import { Construct } from 'constructs'; +import * as appreg from '../lib'; + +describe('Scope based Associations with Application within Same Account', () => { + let app: cdk.App; + beforeEach(() => { + app = new cdk.App({ + context: { + '@aws-cdk/core:newStyleStackSynthesis': false, + }, + }); + }); + test('ApplicationAssociator will associate allStacks created inside cdkApp', () => { + new appreg.ApplicationAssociator(app, 'MyApplication', { + applicationName: 'MyAssociatedApplication', + stackProps: { + stackName: 'MyAssociatedApplicationStack', + }, + }); + const anotherStack = new AppRegistrySampleStack(app, 'SampleStack'); + Template.fromStack(anotherStack).resourceCountIs('AWS::ServiceCatalogAppRegistry::ResourceAssociation', 1); + Template.fromStack(anotherStack).hasResourceProperties('AWS::ServiceCatalogAppRegistry::ResourceAssociation', { + Application: 'MyAssociatedApplication', + Resource: { Ref: 'AWS::StackId' }, + }); + }); +}); +describe('Scope based Associations with Application with Cross Region/Account', () => { + let app: cdk.App; + beforeEach(() => { + app = new cdk.App({ + context: { + '@aws-cdk/core:newStyleStackSynthesis': false, + }, + }); + }); + test('ApplicationAssociator in cross-account associates all stacks created inside cdk app', () => { + new appreg.ApplicationAssociator(app, 'MyApplication', { + applicationName: 'MyAssociatedApplication', + stackProps: { + stackName: 'MyAssociatedApplicationStack', + }, + }); + const firstStack = new cdk.Stack(app, 'testStack', { + env: { account: 'account2', region: 'region' }, + }); + const nestedStack = new cdk.Stack(firstStack, 'MyFirstStack', { + env: { account: 'account2', region: 'region' }, + }); + Template.fromStack(firstStack).resourceCountIs('AWS::ServiceCatalogAppRegistry::ResourceAssociation', 1); + Template.fromStack(nestedStack).resourceCountIs('AWS::ServiceCatalogAppRegistry::ResourceAssociation', 1); + }); + + test('ApplicationAssociator creation failed when neither Application name nor ARN is provided', () => { + expect(() => { + new appreg.ApplicationAssociator(app, 'MyApplication', { + stackProps: { + stackName: 'MyAssociatedApplicationStack', + }, + }); + }).toThrow(/Please provide either ARN or application name./); + }); + + 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', + }, + }); + + Template.fromStack(resource).hasResourceProperties('AWS::ServiceCatalogAppRegistry::ResourceAssociation', { + Application: '0a17wtxeg5vilok0sbxfozwpq9', + Resource: { Ref: 'AWS::StackId' }, + }); + }), + + test('ApplicationAssociator with cross region stacks inside cdkApp throws error', () => { + new appreg.ApplicationAssociator(app, 'MyApplication', { + applicationName: 'MyAssociatedApplication', + stackProps: { + stackName: 'MyAssociatedApplicationStack', + env: { account: 'account2', region: 'region2' }, + }, + }); + + const crossRegionStack = new cdk.Stack(app, 'crossRegionStack', { + env: { account: 'account', region: 'region' }, + }); + Annotations.fromStack(crossRegionStack).hasError('*', 'AppRegistry does not support cross region associations. Application region region2, stack region region'); + }); + + test('Environment Agnostic ApplicationAssociator with cross region stacks inside cdkApp gives warning', () => { + new appreg.ApplicationAssociator(app, 'MyApplication', { + applicationName: 'MyAssociatedApplication', + stackProps: { + stackName: 'MyAssociatedApplicationStack', + }, + }); + + const crossRegionStack = new cdk.Stack(app, 'crossRegionStack', { + env: { account: 'account', region: 'region' }, + }); + Annotations.fromStack(crossRegionStack).hasWarning('*', 'Environment agnostic stack determined, AppRegistry association might not work as expected in case you deploy cross-region or cross-account stack.'); + }); + + test('Cdk App Containing Pipeline with stage but stage not associated throws error', () => { + const application = new appreg.ApplicationAssociator(app, 'MyApplication', { + applicationName: 'MyAssociatedApplication', + stackProps: { + stackName: 'MyAssociatedApplicationStack', + }, + }); + const pipelineStack = new AppRegistrySampleCodePipelineStack(app, 'PipelineStackA', { + application: application, + associateStage: false, + }); + app.synth(); + Annotations.fromStack(pipelineStack).hasError('*', + 'Associate Stage: SampleStage to ensure all stacks in your cdk app are associated with AppRegistry. You can use ApplicationAssociator.associateStage to associate any stage.'); + }); + + test('Cdk App Containing Pipeline with stage and stage associated successfully gets synthesized', () => { + const application = new appreg.ApplicationAssociator(app, 'MyApplication', { + applicationName: 'MyAssociatedApplication', + stackProps: { + stackName: 'MyAssociatedApplicationStack', + }, + }); + const pipelineStack = new AppRegistrySampleCodePipelineStack(app, 'PipelineStackA', { + application: application, + associateStage: true, + }); + app.synth(); + Template.fromStack(pipelineStack).resourceCountIs('AWS::ServiceCatalogAppRegistry::ResourceAssociation', 1); + }); +}); + +interface AppRegistrySampleCodePipelineStackProps extends cdk.StackProps { + application: appreg.ApplicationAssociator; + associateStage: boolean; +} + +class AppRegistrySampleCodePipelineStack extends cdk.Stack { + public constructor(scope: Construct, id: string, props: AppRegistrySampleCodePipelineStackProps ) { + super(scope, id, props); + const repo = new codecommit.Repository(this, 'Repo', { + repositoryName: 'MyRepo', + }); + + const pipeline = new codepipeline.CodePipeline(this, 'Pipeline', { + pipelineName: 'MyPipeline', + synth: new codepipeline.CodeBuildStep('SynthStep', { + input: codepipeline.CodePipelineSource.codeCommit(repo, 'main'), + installCommands: [ + 'npm install -g aws-cdk', + ], + commands: [ + 'npm ci', + 'npm run build', + 'npx cdk synth', + ], + }, + ), + }); + + const stage = new AppRegistrySampleStage( + this, + 'SampleStage', + ); + + if (props.associateStage) { + props.application.associateStage(stage); + } + pipeline.addStage(stage); + } +} + +class AppRegistrySampleStage extends cdk.Stage { + public constructor(scope: Construct, id: string, props?: cdk.StageProps) { + super(scope, id, props); + new AppRegistrySampleStack(this, 'SampleStack', {}); + } +} + +class AppRegistrySampleStack extends cdk.Stack { + public constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + } +} diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry/test/application.test.ts b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application.test.ts index 33f1ca2628cdb..dce0d1147ec44 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry/test/application.test.ts +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/application.test.ts @@ -1,11 +1,11 @@ -import { Template } from '@aws-cdk/assertions'; +import { Annotations, Template } from '@aws-cdk/assertions'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; import * as appreg from '../lib'; describe('Application', () => { let stack: cdk.Stack; - beforeEach(() => { const app = new cdk.App({ context: { @@ -207,6 +207,20 @@ describe('Application', () => { }); }), + test('associate resource on imported application', () => { + const resource = new cdk.Stack(stack, 'MyStack'); + + const importedApplication = appreg.Application.fromApplicationArn(stack, 'ImportedApplication', + 'arn:aws:servicecatalog:us-east-1:123456789012:/applications/0bqmvxvgmry0ecc4mjhwypun6i'); + + importedApplication.associateStack(resource); + + Template.fromStack(stack).hasResourceProperties('AWS::ServiceCatalogAppRegistry::ResourceAssociation', { + Application: '0bqmvxvgmry0ecc4mjhwypun6i', + Resource: { 'Fn::ImportValue': 'MyStack:ExportsOutputRefAWSStackIdB2DD5BAA' }, + }); + }), + test('duplicate resource assocations are idempotent', () => { const resource = new cdk.Stack(stack, 'MyStack'); @@ -324,3 +338,116 @@ describe('Application', () => { }); }); }); + +describe('Scope based Associations with Application within Same Account', () => { + let stack: cdk.Stack; + let app: cdk.App; + beforeEach(() => { + app = new cdk.App({ + context: { + '@aws-cdk/core:newStyleStackSynthesis': false, + }, + }); + stack = new cdk.Stack(app, 'cdkApplication'); + }); + + test('Associate Stage in same account will associate allStacks Inside it', () => { + const application = new appreg.Application(stack, 'MyApplication', { + applicationName: 'MyApplication', + }); + const stage = new cdk.Stage(stack, 'MyStage'); + const stageStack = new cdk.Stack(stage, 'MyStack'); + application.associateAllStacksInScope(stage); + expect(stageStack.stackName).toEqual('MyStage-MyStack'); + Template.fromStack(stageStack).hasResourceProperties('AWS::ServiceCatalogAppRegistry::ResourceAssociation', { + Application: 'MyApplication', + Resource: { Ref: 'AWS::StackId' }, + }); + }); + + + test('Associate Stack in same account will associate allStacks Inside it', () => { + const application = new appreg.Application(stack, 'MyApplication', { + applicationName: 'MyApplication', + }); + + const anotherStack = new AppRegistrySampleStack(app, 'SampleStack'); + application.associateAllStacksInScope(app); + Template.fromStack(stack).resourceCountIs('AWS::ServiceCatalogAppRegistry::ResourceAssociation', 1); + Template.fromStack(anotherStack).resourceCountIs('AWS::ServiceCatalogAppRegistry::ResourceAssociation', 1); + Template.fromStack(stack).hasResourceProperties('AWS::ServiceCatalogAppRegistry::ResourceAssociation', { + Application: { 'Fn::GetAtt': ['MyApplication5C63EC1D', 'Id'] }, + Resource: { Ref: 'AWS::StackId' }, + }); + Template.fromStack(anotherStack).hasResourceProperties('AWS::ServiceCatalogAppRegistry::ResourceAssociation', { + Application: 'MyApplication', + Resource: { Ref: 'AWS::StackId' }, + }); + }); +}); + +describe('Scope based Associations with Application with Cross Region/Account', () => { + let stack: cdk.Stack; + let app: cdk.App; + beforeEach(() => { + app = new cdk.App({ + context: { + '@aws-cdk/core:newStyleStackSynthesis': false, + }, + }); + stack = new cdk.Stack(app, 'CdkApplication', { + env: { account: 'account', region: 'region' }, + }); + }); + + test('associateAllStacksInScope in cross-account associates all stacks from the context passed', () => { + const application = new appreg.Application(stack, 'MyApplication', { + applicationName: 'MyApplication', + }); + const firstStack = new cdk.Stack(app, 'testStack', { + env: { account: 'account2', region: 'region' }, + }); + const nestedStack = new cdk.Stack(firstStack, 'MyFirstStack', { + env: { account: 'account2', region: 'region' }, + }); + application.associateAllStacksInScope(app); + Template.fromStack(stack).resourceCountIs('AWS::ServiceCatalogAppRegistry::ResourceAssociation', 1); + Template.fromStack(firstStack).resourceCountIs('AWS::ServiceCatalogAppRegistry::ResourceAssociation', 1); + Template.fromStack(nestedStack).resourceCountIs('AWS::ServiceCatalogAppRegistry::ResourceAssociation', 1); + }); + + test('Associate Stage in cross account association will associate allStacks Inside it', () => { + const application = new appreg.Application(stack, 'MyApplication', { + applicationName: 'MyApplication', + }); + const stage = new cdk.Stage(app, 'MyStage', { + env: { account: 'account2', region: 'region' }, + }); + const stageStack = new cdk.Stack(stage, 'MyStack'); + application.associateAllStacksInScope(stage); + Template.fromStack(stageStack).resourceCountIs('AWS::ServiceCatalogAppRegistry::ResourceAssociation', 1); + Template.fromStack(stageStack).hasResourceProperties('AWS::ServiceCatalogAppRegistry::ResourceAssociation', { + Application: 'MyApplication', + Resource: { Ref: 'AWS::StackId' }, + }); + }); + + test('Associate Stage in cross region throw error', () => { + const application = new appreg.Application(stack, 'MyApplication', { + applicationName: 'MyApplication', + }); + const stage = new cdk.Stage(stack, 'MyStage', { + env: { account: 'account1', region: 'region1' }, + }); + const stageStack = new cdk.Stack(stage, 'MyStack'); + application.associateAllStacksInScope(stage); + Annotations.fromStack(stageStack).hasError('*', + 'AppRegistry does not support cross region associations. Application region region, stack region region1'); + }); +}); + +class AppRegistrySampleStack extends cdk.Stack { + public constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + } +} 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 new file mode 100644 index 0000000000000..afd822dc6dc16 --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalogappregistry/test/integ.application-associator.all-stacks-association.ts @@ -0,0 +1,20 @@ +/// !cdk-integ integ-servicecatalogappregistry-application +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: { + stackName: 'AppRegistryApplicationAssociatorStack', + }, +}); + +new cdk.Stack(stack, 'resourcesStack'); + +app.synth();