From 48f5b63fc356efdf9c5137d6f415328d0d350e48 Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Tue, 11 Jun 2024 15:59:51 -0700 Subject: [PATCH 01/51] working basic implementation --- .../cli-lib-alpha/THIRD_PARTY_LICENSES | 8 +-- packages/@aws-cdk/cx-api/FEATURE_FLAGS.md | 66 +++++++++++++++++-- .../lib/cloud-assembly/artifact-schema.ts | 7 ++ .../schema/cloud-assembly.schema.json | 6 ++ .../schema/cloud-assembly.version.json | 2 +- .../core/lib/stack-synthesizers/_shared.ts | 1 + packages/aws-cdk-lib/core/lib/stack.ts | 13 +++- packages/aws-cdk-lib/core/test/stack.test.ts | 21 ++++++ .../lib/artifacts/cloudformation-artifact.ts | 6 ++ packages/aws-cdk/lib/cdk-toolkit.ts | 6 +- 10 files changed, 125 insertions(+), 11 deletions(-) diff --git a/packages/@aws-cdk/cli-lib-alpha/THIRD_PARTY_LICENSES b/packages/@aws-cdk/cli-lib-alpha/THIRD_PARTY_LICENSES index 2c9a5a75dad60..c037af426d656 100644 --- a/packages/@aws-cdk/cli-lib-alpha/THIRD_PARTY_LICENSES +++ b/packages/@aws-cdk/cli-lib-alpha/THIRD_PARTY_LICENSES @@ -207,7 +207,7 @@ The @aws-cdk/cli-lib-alpha package includes the following third-party software/l ---------------- -** @jsii/check-node@1.97.0 - https://www.npmjs.com/package/@jsii/check-node/v/1.97.0 | Apache-2.0 +** @jsii/check-node@1.98.0 - https://www.npmjs.com/package/@jsii/check-node/v/1.98.0 | Apache-2.0 jsii Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. @@ -266,7 +266,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------- -** ajv@8.12.0 - https://www.npmjs.com/package/ajv/v/8.12.0 | MIT +** ajv@8.13.0 - https://www.npmjs.com/package/ajv/v/8.13.0 | MIT The MIT License (MIT) Copyright (c) 2015-2021 Evgeny Poberezkin @@ -493,7 +493,7 @@ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH RE ---------------- -** aws-sdk@2.1596.0 - https://www.npmjs.com/package/aws-sdk/v/2.1596.0 | Apache-2.0 +** aws-sdk@2.1610.0 - https://www.npmjs.com/package/aws-sdk/v/2.1610.0 | Apache-2.0 AWS SDK for JavaScript Copyright 2012-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. @@ -691,7 +691,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI ---------------- -** cdk-from-cfn@0.156.0 - https://www.npmjs.com/package/cdk-from-cfn/v/0.156.0 | MIT OR Apache-2.0 +** cdk-from-cfn@0.159.0 - https://www.npmjs.com/package/cdk-from-cfn/v/0.159.0 | MIT OR Apache-2.0 ---------------- diff --git a/packages/@aws-cdk/cx-api/FEATURE_FLAGS.md b/packages/@aws-cdk/cx-api/FEATURE_FLAGS.md index 3678e750e3617..e67806c3ceca1 100644 --- a/packages/@aws-cdk/cx-api/FEATURE_FLAGS.md +++ b/packages/@aws-cdk/cx-api/FEATURE_FLAGS.md @@ -68,7 +68,10 @@ Flags come in three types: | [@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2](#aws-cdkaws-codepipelinedefaultpipelinetypetov2) | Enables Pipeline to set the default pipeline type to V2. | 2.133.0 | (default) | | [@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope](#aws-cdkaws-kmsreducecrossaccountregionpolicyscope) | When enabled, IAM Policy created from KMS key grant will reduce the resource scope to this key only. | 2.134.0 | (fix) | | [@aws-cdk/aws-eks:nodegroupNameAttribute](#aws-cdkaws-eksnodegroupnameattribute) | When enabled, nodegroupName attribute of the provisioned EKS NodeGroup will not have the cluster name prefix. | 2.139.0 | (fix) | -| [@aws-cdk/aws-ec2:ebsDefaultGp3Volume](#aws-cdkaws-ec2ebsdefaultgp3volume) | When enabled, the default volume type of the EBS volume will be GP3 | V2NEXT | (default) | +| [@aws-cdk/aws-ec2:ebsDefaultGp3Volume](#aws-cdkaws-ec2ebsdefaultgp3volume) | When enabled, the default volume type of the EBS volume will be GP3 | 2.140.0 | (default) | +| [@aws-cdk/pipelines:reduceAssetRoleTrustScope](#aws-cdkpipelinesreduceassetroletrustscope) | Remove the root account principal from PipelineAssetsFileRole trust policy | 2.141.0 | (default) | +| [@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm](#aws-cdkaws-ecsremovedefaultdeploymentalarm) | When enabled, remove default deployment alarm settings | 2.143.0 | (default) | +| [@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault](#aws-cdkcustom-resourceslogapiresponsedatapropertytruedefault) | When enabled, the custom resource used for `AwsCustomResource` will configure the `logApiResponseData` property as true by default | 2.145.0 | (fix) | @@ -128,7 +131,9 @@ The following json shows the current recommended set of flags, as `cdk init` wou "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, "@aws-cdk/aws-eks:nodegroupNameAttribute": true, - "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true + "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, + "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, + "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false } } ``` @@ -171,6 +176,7 @@ are migrating a v1 CDK project to v2, explicitly set any of these flags which do | [@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId](#aws-cdkaws-apigatewayusageplankeyorderinsensitiveid) | Allow adding/removing multiple UsagePlanKeys independently | (fix) | 1.98.0 | `false` | `true` | | [@aws-cdk/aws-lambda:recognizeVersionProps](#aws-cdkaws-lambdarecognizeversionprops) | Enable this feature flag to opt in to the updated logical id calculation for Lambda Version created using the `fn.currentVersion`. | (fix) | 1.106.0 | `false` | `true` | | [@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2\_2021](#aws-cdkaws-cloudfrontdefaultsecuritypolicytlsv12_2021) | Enable this feature flag to have cloudfront distributions use the security policy TLSv1.2_2021 by default. | (fix) | 1.117.0 | `false` | `true` | +| [@aws-cdk/pipelines:reduceAssetRoleTrustScope](#aws-cdkpipelinesreduceassetroletrustscope) | Remove the root account principal from PipelineAssetsFileRole trust policy | (default) | | `false` | `true` | @@ -185,7 +191,8 @@ Here is an example of a `cdk.json` file that restores v1 behavior for these flag "@aws-cdk/aws-rds:lowercaseDbIdentifier": false, "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": false, "@aws-cdk/aws-lambda:recognizeVersionProps": false, - "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": false + "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": false, + "@aws-cdk/pipelines:reduceAssetRoleTrustScope": false } } ``` @@ -1293,9 +1300,60 @@ When this featuer flag is enabled, the default volume type of the EBS volume wil | Since | Default | Recommended | | ----- | ----- | ----- | | (not in v1) | | | -| V2NEXT | `false` | `true` | +| 2.140.0 | `false` | `true` | **Compatibility with old behavior:** Pass `volumeType: EbsDeviceVolumeType.GENERAL_PURPOSE_SSD` to `Volume` construct to restore the previous behavior. +### @aws-cdk/pipelines:reduceAssetRoleTrustScope + +*Remove the root account principal from PipelineAssetsFileRole trust policy* (default) + +When this feature flag is enabled, the root account principal will not be added to the trust policy of asset role. +When this feature flag is disabled, it will keep the root account principal in the trust policy. + + +| Since | Default | Recommended | +| ----- | ----- | ----- | +| (not in v1) | | | +| 2.141.0 | `true` | `true` | + +**Compatibility with old behavior:** Disable the feature flag to add the root account principal back + + +### @aws-cdk/aws-ecs:removeDefaultDeploymentAlarm + +*When enabled, remove default deployment alarm settings* (default) + +When this featuer flag is enabled, remove the default deployment alarm settings when creating a AWS ECS service. + + +| Since | Default | Recommended | +| ----- | ----- | ----- | +| (not in v1) | | | +| 2.143.0 | `false` | `true` | + +**Compatibility with old behavior:** Set AWS::ECS::Service 'DeploymentAlarms' manually to restore the previous behavior. + + +### @aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault + +*When enabled, the custom resource used for `AwsCustomResource` will configure the `logApiResponseData` property as true by default* (fix) + +This results in 'logApiResponseData' being passed as true to the custom resource provider. This will cause the custom resource handler to receive an 'Update' event. If you don't +have an SDK call configured for the 'Update' event and you're dependent on specific SDK call response data, you will see this error from CFN: + +CustomResource attribute error: Vendor response doesn't contain attribute in object. See https://github.com/aws/aws-cdk/issues/29949) for more details. + +Unlike most feature flags, we don't recommend setting this feature flag to true. However, if you're using the 'AwsCustomResource' construct with 'logApiResponseData' as true in +the event object, then setting this feature flag will keep this behavior. Otherwise, setting this feature flag to false will trigger an 'Update' event by removing the 'logApiResponseData' +property from the event object. + + +| Since | Default | Recommended | +| ----- | ----- | ----- | +| (not in v1) | | | +| 2.145.0 | `false` | `false` | + + diff --git a/packages/aws-cdk-lib/cloud-assembly-schema/lib/cloud-assembly/artifact-schema.ts b/packages/aws-cdk-lib/cloud-assembly-schema/lib/cloud-assembly/artifact-schema.ts index 66872401251aa..319cefb179376 100644 --- a/packages/aws-cdk-lib/cloud-assembly-schema/lib/cloud-assembly/artifact-schema.ts +++ b/packages/aws-cdk-lib/cloud-assembly-schema/lib/cloud-assembly/artifact-schema.ts @@ -55,6 +55,13 @@ export interface AwsCloudFormationStackProperties { */ readonly tags?: { [id: string]: string }; + /** + * Values for CloudFormation stack tags that should be passed when the stack is deployed. + * + * @default - No tags + */ + readonly notificationArns?: string[]; + /** * The name to use for the CloudFormation stack. * @default - name derived from artifact ID diff --git a/packages/aws-cdk-lib/cloud-assembly-schema/schema/cloud-assembly.schema.json b/packages/aws-cdk-lib/cloud-assembly-schema/schema/cloud-assembly.schema.json index 279dfbe369073..240834b8c4830 100644 --- a/packages/aws-cdk-lib/cloud-assembly-schema/schema/cloud-assembly.schema.json +++ b/packages/aws-cdk-lib/cloud-assembly-schema/schema/cloud-assembly.schema.json @@ -345,6 +345,12 @@ "type": "string" } }, + "notificationArns": { + "type": "array", + "items": { + "type": "string" + } + }, "stackName": { "description": "The name to use for the CloudFormation stack. (Default - name derived from artifact ID)", "type": "string" diff --git a/packages/aws-cdk-lib/cloud-assembly-schema/schema/cloud-assembly.version.json b/packages/aws-cdk-lib/cloud-assembly-schema/schema/cloud-assembly.version.json index 1f0068d32659a..079dd58c72d69 100644 --- a/packages/aws-cdk-lib/cloud-assembly-schema/schema/cloud-assembly.version.json +++ b/packages/aws-cdk-lib/cloud-assembly-schema/schema/cloud-assembly.version.json @@ -1 +1 @@ -{"version":"36.0.0"} \ No newline at end of file +{"version":"37.0.0"} \ No newline at end of file diff --git a/packages/aws-cdk-lib/core/lib/stack-synthesizers/_shared.ts b/packages/aws-cdk-lib/core/lib/stack-synthesizers/_shared.ts index 1017f172a850e..1d2b10bbbacb6 100644 --- a/packages/aws-cdk-lib/core/lib/stack-synthesizers/_shared.ts +++ b/packages/aws-cdk-lib/core/lib/stack-synthesizers/_shared.ts @@ -48,6 +48,7 @@ export function addStackArtifactToAssembly( terminationProtection: stack.terminationProtection, tags: nonEmptyDict(stack.tags.tagValues()), validateOnSynth: session.validateOnSynth, + notificationArns: stack.notificationArns, ...stackProps, ...stackNameProperty, }; diff --git a/packages/aws-cdk-lib/core/lib/stack.ts b/packages/aws-cdk-lib/core/lib/stack.ts index b946d73974973..67bcdecfdff6c 100644 --- a/packages/aws-cdk-lib/core/lib/stack.ts +++ b/packages/aws-cdk-lib/core/lib/stack.ts @@ -127,6 +127,13 @@ export interface StackProps { */ readonly tags?: { [key: string]: string }; + /** + * foofoo + * + * @default wowowowowo + */ + readonly notificationArns?: string[]; + /** * Synthesis method to use while deploying this stack * @@ -383,6 +390,8 @@ export class Stack extends Construct implements ITaggable { private readonly _stackName: string; + private readonly _notificationArns: string[]; + /** * Enable this flag to suppress indentation in generated * CloudFormation templates. @@ -450,6 +459,7 @@ export class Stack extends Construct implements ITaggable { throw new Error(`Stack name must be <= 128 characters. Stack name: '${this._stackName}'`); } this.tags = new TagManager(TagType.KEY_VALUE, 'aws:cdk:stack', props.tags); + this._notificationArns = props.notificationArns ?? []; if (!VALID_STACK_NAME_REGEX.test(this.stackName)) { throw new Error(`Stack name must match the regular expression: ${VALID_STACK_NAME_REGEX.toString()}, got '${this.stackName}'`); @@ -716,7 +726,8 @@ export class Stack extends Construct implements ITaggable { * Returns the list of notification Amazon Resource Names (ARNs) for the current stack. */ public get notificationArns(): string[] { - return new ScopedAws(this).notificationArns; + //return (new ScopedAws(this).notificationArns).concat(this._notificationArns); // .join(this._notificationArns) + return this._notificationArns; } /** diff --git a/packages/aws-cdk-lib/core/test/stack.test.ts b/packages/aws-cdk-lib/core/test/stack.test.ts index 82be67b19499b..a8de31257b0d8 100644 --- a/packages/aws-cdk-lib/core/test/stack.test.ts +++ b/packages/aws-cdk-lib/core/test/stack.test.ts @@ -2075,6 +2075,27 @@ describe('stack', () => { expect(asm.getStackArtifact(stack2.artifactId).tags).toEqual(expected); }); + test('stack notification arns are reflected in the stack artifact properties', () => { + // GIVEN + const NOTIFICATION_ARNS = ['arn:aws:sns:bermuda-triangle-1337:123456789012:MyTopic']; + const app = new App({ stackTraces: false }); + const stack1 = new Stack(app, 'stack1', { + notificationArns: NOTIFICATION_ARNS, + }); + // might want stack notification arns to reflect in the nested stacks too + //const stack2 = new Stack(stack1, 'stack2'); + + // WHEN + //Tags.of(app).add('foo', 'bar'); + + // THEN + const asm = app.synth(); + const expected = { foo: 'bar' }; + + expect(asm.getStackArtifact(stack1.artifactId).notificationArns).toEqual(NOTIFICATION_ARNS); + //expect(asm.getStackArtifact(stack2.artifactId).tags).toEqual(expected); + }); + test('Termination Protection is reflected in Cloud Assembly artifact', () => { // if the root is an app, invoke "synth" to avoid double synthesis const app = new App(); diff --git a/packages/aws-cdk-lib/cx-api/lib/artifacts/cloudformation-artifact.ts b/packages/aws-cdk-lib/cx-api/lib/artifacts/cloudformation-artifact.ts index 7cf279c96d924..b7e632a90dc1c 100644 --- a/packages/aws-cdk-lib/cx-api/lib/artifacts/cloudformation-artifact.ts +++ b/packages/aws-cdk-lib/cx-api/lib/artifacts/cloudformation-artifact.ts @@ -54,6 +54,11 @@ export class CloudFormationStackArtifact extends CloudArtifact { */ public readonly tags: { [id: string]: string }; + /** + * CloudFormation tags to pass to the stack. + */ + public readonly notificationArns: string[]; + /** * The physical name of this stack. */ @@ -158,6 +163,7 @@ export class CloudFormationStackArtifact extends CloudArtifact { // We get the tags from 'properties' if available (cloud assembly format >= 6.0.0), otherwise // from the stack metadata this.tags = properties.tags ?? this.tagsFromMetadata(); + this.notificationArns = properties.notificationArns ?? []; this.assumeRoleArn = properties.assumeRoleArn; this.assumeRoleExternalId = properties.assumeRoleExternalId; this.cloudFormationExecutionRoleArn = properties.cloudFormationExecutionRoleArn; diff --git a/packages/aws-cdk/lib/cdk-toolkit.ts b/packages/aws-cdk/lib/cdk-toolkit.ts index 854b7ec6419c2..a91de159effd7 100644 --- a/packages/aws-cdk/lib/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cdk-toolkit.ts @@ -327,6 +327,10 @@ export class CdkToolkit { tags = tagsForStack(stack); } + const notificationArns: string[] = []; + notificationArns.concat(options.notificationArns ?? []); + notificationArns.concat(stack.notificationArns); + let elapsedDeployTime = 0; try { const result = await this.props.deployments.deployStack({ @@ -335,7 +339,7 @@ export class CdkToolkit { roleArn: options.roleArn, toolkitStackName: options.toolkitStackName, reuseAssets: options.reuseAssets, - notificationArns: options.notificationArns, + notificationArns, tags, execute: options.execute, changeSetName: options.changeSetName, From f5c930b5cfea43558029ce052fc0ec6d2b42e4cc Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Wed, 12 Jun 2024 12:15:53 -0700 Subject: [PATCH 02/51] fix --- .../core/lib/stack-synthesizers/_shared.ts | 2 +- packages/aws-cdk-lib/core/lib/stack.ts | 10 +++++++--- packages/aws-cdk-lib/core/test/stack.test.ts | 6 ------ .../cx-api/test/stack-artifact.test.ts | 18 ++++++++++++++++++ packages/aws-cdk/lib/cdk-toolkit.ts | 6 +++--- 5 files changed, 29 insertions(+), 13 deletions(-) diff --git a/packages/aws-cdk-lib/core/lib/stack-synthesizers/_shared.ts b/packages/aws-cdk-lib/core/lib/stack-synthesizers/_shared.ts index 1d2b10bbbacb6..bdbcd50c11c9e 100644 --- a/packages/aws-cdk-lib/core/lib/stack-synthesizers/_shared.ts +++ b/packages/aws-cdk-lib/core/lib/stack-synthesizers/_shared.ts @@ -48,7 +48,7 @@ export function addStackArtifactToAssembly( terminationProtection: stack.terminationProtection, tags: nonEmptyDict(stack.tags.tagValues()), validateOnSynth: session.validateOnSynth, - notificationArns: stack.notificationArns, + notificationArns: (stack as any)._notificationArns, ...stackProps, ...stackNameProperty, }; diff --git a/packages/aws-cdk-lib/core/lib/stack.ts b/packages/aws-cdk-lib/core/lib/stack.ts index 67bcdecfdff6c..86bc6128b9288 100644 --- a/packages/aws-cdk-lib/core/lib/stack.ts +++ b/packages/aws-cdk-lib/core/lib/stack.ts @@ -390,7 +390,12 @@ export class Stack extends Construct implements ITaggable { private readonly _stackName: string; - private readonly _notificationArns: string[]; + /** + * notification arns passed through stack props. + * + * @internal + */ + public readonly _notificationArns: string[]; /** * Enable this flag to suppress indentation in generated @@ -726,8 +731,7 @@ export class Stack extends Construct implements ITaggable { * Returns the list of notification Amazon Resource Names (ARNs) for the current stack. */ public get notificationArns(): string[] { - //return (new ScopedAws(this).notificationArns).concat(this._notificationArns); // .join(this._notificationArns) - return this._notificationArns; + return (new ScopedAws(this).notificationArns); } /** diff --git a/packages/aws-cdk-lib/core/test/stack.test.ts b/packages/aws-cdk-lib/core/test/stack.test.ts index a8de31257b0d8..4846ff3b0cc05 100644 --- a/packages/aws-cdk-lib/core/test/stack.test.ts +++ b/packages/aws-cdk-lib/core/test/stack.test.ts @@ -2082,18 +2082,12 @@ describe('stack', () => { const stack1 = new Stack(app, 'stack1', { notificationArns: NOTIFICATION_ARNS, }); - // might want stack notification arns to reflect in the nested stacks too - //const stack2 = new Stack(stack1, 'stack2'); - - // WHEN - //Tags.of(app).add('foo', 'bar'); // THEN const asm = app.synth(); const expected = { foo: 'bar' }; expect(asm.getStackArtifact(stack1.artifactId).notificationArns).toEqual(NOTIFICATION_ARNS); - //expect(asm.getStackArtifact(stack2.artifactId).tags).toEqual(expected); }); test('Termination Protection is reflected in Cloud Assembly artifact', () => { diff --git a/packages/aws-cdk-lib/cx-api/test/stack-artifact.test.ts b/packages/aws-cdk-lib/cx-api/test/stack-artifact.test.ts index 85009cedd7c23..81d5b4a0c3186 100644 --- a/packages/aws-cdk-lib/cx-api/test/stack-artifact.test.ts +++ b/packages/aws-cdk-lib/cx-api/test/stack-artifact.test.ts @@ -21,6 +21,24 @@ afterEach(() => { rimraf(builder.outdir); }); +test('read notification arns from artifact properties', () => { +// GIVEN + const NOTIFICATION_ARNS = ['arn:aws:sns:bermuda-triangle-1337:123456789012:MyTopic']; + builder.addArtifact('Stack', { + ...stackBase, + properties: { + ...stackBase.properties, + notificationArns: NOTIFICATION_ARNS, + }, + }); + + // WHEN + const assembly = builder.buildAssembly(); + + // THEN + expect(assembly.getStackByName('Stack').notificationArns).toEqual(NOTIFICATION_ARNS); +}); + test('read tags from artifact properties', () => { // GIVEN builder.addArtifact('Stack', { diff --git a/packages/aws-cdk/lib/cdk-toolkit.ts b/packages/aws-cdk/lib/cdk-toolkit.ts index a91de159effd7..d2f20632ec32b 100644 --- a/packages/aws-cdk/lib/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cdk-toolkit.ts @@ -327,9 +327,9 @@ export class CdkToolkit { tags = tagsForStack(stack); } - const notificationArns: string[] = []; - notificationArns.concat(options.notificationArns ?? []); - notificationArns.concat(stack.notificationArns); + let notificationArns: string[] = []; + notificationArns = notificationArns.concat(options.notificationArns ?? []); + notificationArns = notificationArns.concat(stack.notificationArns); let elapsedDeployTime = 0; try { From 662697d77547fb415d089ef385bd9638ee86eb89 Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Wed, 12 Jun 2024 14:23:03 -0700 Subject: [PATCH 03/51] don't skip deploy if stack notifications arnrns have changed --- .../lib/cloud-assembly/artifact-schema.ts | 4 +- packages/aws-cdk/lib/api/deploy-stack.ts | 10 ++++ .../aws-cdk/lib/api/util/cloudformation.ts | 11 +++- packages/aws-cdk/lib/cdk-toolkit.ts | 24 ++++----- .../aws-cdk/test/api/deploy-stack.test.ts | 53 +++++++++++++++++++ 5 files changed, 86 insertions(+), 16 deletions(-) diff --git a/packages/aws-cdk-lib/cloud-assembly-schema/lib/cloud-assembly/artifact-schema.ts b/packages/aws-cdk-lib/cloud-assembly-schema/lib/cloud-assembly/artifact-schema.ts index 319cefb179376..644d110e70bd9 100644 --- a/packages/aws-cdk-lib/cloud-assembly-schema/lib/cloud-assembly/artifact-schema.ts +++ b/packages/aws-cdk-lib/cloud-assembly-schema/lib/cloud-assembly/artifact-schema.ts @@ -56,9 +56,9 @@ export interface AwsCloudFormationStackProperties { readonly tags?: { [id: string]: string }; /** - * Values for CloudFormation stack tags that should be passed when the stack is deployed. + * SNS Notification ARNs that should receive CloudFormation Stack Events. * - * @default - No tags + * @default - No notification arns */ readonly notificationArns?: string[]; diff --git a/packages/aws-cdk/lib/api/deploy-stack.ts b/packages/aws-cdk/lib/api/deploy-stack.ts index 28af2d39616b0..abb2b4e7abe4a 100644 --- a/packages/aws-cdk/lib/api/deploy-stack.ts +++ b/packages/aws-cdk/lib/api/deploy-stack.ts @@ -644,6 +644,16 @@ async function canSkipDeploy( return false; } + function arrayEquals(a: any[], b: any[]) { + return a.every(item => b.includes(item)) && b.every(item => a.includes(item)); + } + + // Notification arns have changed + if (!arrayEquals(cloudFormationStack.notificationArns, deployStackOptions.notificationArns ?? [])) { + debug(`${deployName}: notification arns have changed`); + return false; + } + // Termination protection has been updated if (!!deployStackOptions.stack.terminationProtection !== !!cloudFormationStack.terminationProtection) { debug(`${deployName}: termination protection has been updated`); diff --git a/packages/aws-cdk/lib/api/util/cloudformation.ts b/packages/aws-cdk/lib/api/util/cloudformation.ts index 23e95f6d618e5..7b5a1d9dd3fcd 100644 --- a/packages/aws-cdk/lib/api/util/cloudformation.ts +++ b/packages/aws-cdk/lib/api/util/cloudformation.ts @@ -138,12 +138,21 @@ export class CloudFormationStack { /** * The stack's current tags * - * Empty list of the stack does not exist + * Empty list if the stack does not exist */ public get tags(): CloudFormation.Tags { return this.stack?.Tags || []; } + /** + * The stack's current notification ARNs. + * + * Empty list if the stack does not exist + */ + public get notificationArns(): CloudFormation.NotificationARNs { + return this.stack?.NotificationARNs ?? []; + } + /** * Return the names of all current parameters to the stack * diff --git a/packages/aws-cdk/lib/cdk-toolkit.ts b/packages/aws-cdk/lib/cdk-toolkit.ts index d2f20632ec32b..2e7e37fc24c5d 100644 --- a/packages/aws-cdk/lib/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cdk-toolkit.ts @@ -214,14 +214,6 @@ export class CdkToolkit { return this.watch(options); } - if (options.notificationArns) { - options.notificationArns.map( arn => { - if (!validateSnsTopicArn(arn)) { - throw new Error(`Notification arn ${arn} is not a valid arn for an SNS topic`); - } - }); - } - const startSynthTime = new Date().getTime(); const stackCollection = await this.selectStacksForDeploy(options.selector, options.exclusively, options.cacheCloudAssembly, options.ignoreNoStacks); @@ -318,7 +310,17 @@ export class CdkToolkit { } } - const stackIndex = stacks.indexOf(stack)+1; + let notificationArns: string[] = []; + notificationArns = notificationArns.concat(options.notificationArns ?? []); + notificationArns = notificationArns.concat(stack.notificationArns); + + notificationArns.map(arn => { + if (!validateSnsTopicArn(arn)) { + throw new Error(`Notification arn ${arn} is not a valid arn for an SNS topic`); + } + }); + + const stackIndex = stacks.indexOf(stack) + 1; print('%s: deploying... [%s/%s]', chalk.bold(stack.displayName), stackIndex, stackCollection.stackCount); const startDeployTime = new Date().getTime(); @@ -327,10 +329,6 @@ export class CdkToolkit { tags = tagsForStack(stack); } - let notificationArns: string[] = []; - notificationArns = notificationArns.concat(options.notificationArns ?? []); - notificationArns = notificationArns.concat(stack.notificationArns); - let elapsedDeployTime = 0; try { const result = await this.props.deployments.deployStack({ diff --git a/packages/aws-cdk/test/api/deploy-stack.test.ts b/packages/aws-cdk/test/api/deploy-stack.test.ts index 666d4f43410ec..208b0817acfa8 100644 --- a/packages/aws-cdk/test/api/deploy-stack.test.ts +++ b/packages/aws-cdk/test/api/deploy-stack.test.ts @@ -460,6 +460,59 @@ test('deploy is not skipped if parameters are different', async () => { })); }); +/* +test('deploy is skipped if notificationArns are the same', async () => { + // GIVEN + givenTemplateIs(FAKE_STACK_WITH_PARAMETERS.template); + givenStackExists({ + NotificationARNs: ['arn:aws:sns:bermuda-triangle-1337:123456789012:TestTopic'], + }); + + // WHEN + await deployStack({ + ...standardDeployStackArguments(), + stack: FAKE_STACK_WITH_PARAMETERS, + parameters: {}, + usePreviousParameters: true, + }); + + // THEN + expect(cfnMocks.createChangeSet).not.toHaveBeenCalled(); +}); + +test('deploy is not skipped if notificationArns are different', async () => { + // GIVEN + givenTemplateIs(FAKE_STACK_WITH_PARAMETERS.template); + givenStackExists({ + Parameters: [ + { ParameterKey: 'HasValue', ParameterValue: 'HasValue' }, + { ParameterKey: 'HasDefault', ParameterValue: 'HasDefault' }, + { ParameterKey: 'OtherParameter', ParameterValue: 'OtherParameter' }, + ], + }); + + // WHEN + await deployStack({ + ...standardDeployStackArguments(), + stack: FAKE_STACK_WITH_PARAMETERS, + parameters: { + HasValue: 'NewValue', + }, + usePreviousParameters: true, + }); + + // THEN + expect(cfnMocks.createChangeSet).toHaveBeenCalledWith(expect.objectContaining({ + Parameters: [ + { ParameterKey: 'HasValue', ParameterValue: 'NewValue' }, + { ParameterKey: 'HasDefault', UsePreviousValue: true }, + { ParameterKey: 'OtherParameter', UsePreviousValue: true }, + ], + })); +}); + +*/ + test('if existing stack failed to create, it is deleted and recreated', async () => { // GIVEN givenStackExists( From bc0afeabf1aad6f1c7dc9dd65e76dbb86811eed6 Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Wed, 12 Jun 2024 15:02:09 -0700 Subject: [PATCH 04/51] more changes --- packages/aws-cdk/lib/api/deploy-stack.ts | 2 +- packages/aws-cdk/lib/cdk-toolkit.ts | 1 - .../aws-cdk/test/api/deploy-stack.test.ts | 35 +++++-------------- 3 files changed, 10 insertions(+), 28 deletions(-) diff --git a/packages/aws-cdk/lib/api/deploy-stack.ts b/packages/aws-cdk/lib/api/deploy-stack.ts index abb2b4e7abe4a..62019c83380e3 100644 --- a/packages/aws-cdk/lib/api/deploy-stack.ts +++ b/packages/aws-cdk/lib/api/deploy-stack.ts @@ -644,7 +644,7 @@ async function canSkipDeploy( return false; } - function arrayEquals(a: any[], b: any[]) { + function arrayEquals(a: any[], b: any[]): boolean { return a.every(item => b.includes(item)) && b.every(item => a.includes(item)); } diff --git a/packages/aws-cdk/lib/cdk-toolkit.ts b/packages/aws-cdk/lib/cdk-toolkit.ts index 2e7e37fc24c5d..f876634484c46 100644 --- a/packages/aws-cdk/lib/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cdk-toolkit.ts @@ -161,7 +161,6 @@ export class CdkToolkit { let changeSet = undefined; if (options.changeSet) { - let stackExists = false; try { stackExists = await this.props.deployments.stackExists({ diff --git a/packages/aws-cdk/test/api/deploy-stack.test.ts b/packages/aws-cdk/test/api/deploy-stack.test.ts index 208b0817acfa8..4aec7cc9ff7d1 100644 --- a/packages/aws-cdk/test/api/deploy-stack.test.ts +++ b/packages/aws-cdk/test/api/deploy-stack.test.ts @@ -460,10 +460,9 @@ test('deploy is not skipped if parameters are different', async () => { })); }); -/* test('deploy is skipped if notificationArns are the same', async () => { // GIVEN - givenTemplateIs(FAKE_STACK_WITH_PARAMETERS.template); + givenTemplateIs(FAKE_STACK.template); givenStackExists({ NotificationARNs: ['arn:aws:sns:bermuda-triangle-1337:123456789012:TestTopic'], }); @@ -471,9 +470,8 @@ test('deploy is skipped if notificationArns are the same', async () => { // WHEN await deployStack({ ...standardDeployStackArguments(), - stack: FAKE_STACK_WITH_PARAMETERS, - parameters: {}, - usePreviousParameters: true, + stack: FAKE_STACK, + notificationArns: ['arn:aws:sns:bermuda-triangle-1337:123456789012:TestTopic'], }); // THEN @@ -482,37 +480,22 @@ test('deploy is skipped if notificationArns are the same', async () => { test('deploy is not skipped if notificationArns are different', async () => { // GIVEN - givenTemplateIs(FAKE_STACK_WITH_PARAMETERS.template); + givenTemplateIs(FAKE_STACK.template); givenStackExists({ - Parameters: [ - { ParameterKey: 'HasValue', ParameterValue: 'HasValue' }, - { ParameterKey: 'HasDefault', ParameterValue: 'HasDefault' }, - { ParameterKey: 'OtherParameter', ParameterValue: 'OtherParameter' }, - ], + NotificationARNs: ['arn:aws:sns:bermuda-triangle-1337:123456789012:TestTopic'], }); // WHEN await deployStack({ ...standardDeployStackArguments(), - stack: FAKE_STACK_WITH_PARAMETERS, - parameters: { - HasValue: 'NewValue', - }, - usePreviousParameters: true, + stack: FAKE_STACK, + notificationArns: ['arn:aws:sns:bermuda-triangle-1337:123456789012:MagicTopic'], }); // THEN - expect(cfnMocks.createChangeSet).toHaveBeenCalledWith(expect.objectContaining({ - Parameters: [ - { ParameterKey: 'HasValue', ParameterValue: 'NewValue' }, - { ParameterKey: 'HasDefault', UsePreviousValue: true }, - { ParameterKey: 'OtherParameter', UsePreviousValue: true }, - ], - })); + expect(cfnMocks.createChangeSet).toHaveBeenCalled(); }); -*/ - test('if existing stack failed to create, it is deleted and recreated', async () => { // GIVEN givenStackExists( @@ -677,7 +660,7 @@ test('deploy is not skipped if stack is in a _FAILED state', async () => { await deployStack({ ...standardDeployStackArguments(), usePreviousParameters: true, - }).catch(() => {}); + }).catch(() => { }); // THEN expect(cfnMocks.createChangeSet).toHaveBeenCalled(); From b3a571600f3dac36ffbff69bfb9a46585b058f58 Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Wed, 12 Jun 2024 15:04:40 -0700 Subject: [PATCH 05/51] cleanup --- packages/aws-cdk-lib/core/lib/stack.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/aws-cdk-lib/core/lib/stack.ts b/packages/aws-cdk-lib/core/lib/stack.ts index 86bc6128b9288..0982dc7714c53 100644 --- a/packages/aws-cdk-lib/core/lib/stack.ts +++ b/packages/aws-cdk-lib/core/lib/stack.ts @@ -731,7 +731,7 @@ export class Stack extends Construct implements ITaggable { * Returns the list of notification Amazon Resource Names (ARNs) for the current stack. */ public get notificationArns(): string[] { - return (new ScopedAws(this).notificationArns); + return new ScopedAws(this).notificationArns; } /** From 4b88b034aa241c5f86a943374c0c87a4f2643d0e Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Thu, 13 Jun 2024 10:19:05 -0700 Subject: [PATCH 06/51] most tests working --- packages/aws-cdk/test/cdk-toolkit.test.ts | 278 ++++++++++++++++++---- packages/aws-cdk/test/util.ts | 2 + 2 files changed, 232 insertions(+), 48 deletions(-) diff --git a/packages/aws-cdk/test/cdk-toolkit.test.ts b/packages/aws-cdk/test/cdk-toolkit.test.ts index 7f70bbc8434de..d9fb85eaf9eb2 100644 --- a/packages/aws-cdk/test/cdk-toolkit.test.ts +++ b/packages/aws-cdk/test/cdk-toolkit.test.ts @@ -482,7 +482,7 @@ describe('deploy', () => { }); }); - test('with sns notification arns', async () => { + test('with sns notification arns as options', async () => { // GIVEN const notificationArns = [ 'arn:aws:sns:us-east-2:444455556666:MyTopic', @@ -506,7 +506,7 @@ describe('deploy', () => { }); }); - test('fail with incorrect sns notification arns', async () => { + test('fail with incorrect sns notification arns as options', async () => { // GIVEN const notificationArns = ['arn:::cfn-my-cool-topic']; const toolkit = new CdkToolkit({ @@ -526,64 +526,206 @@ describe('deploy', () => { hotswap: HotswapMode.FULL_DEPLOYMENT, }), ).rejects.toThrow('Notification arn arn:::cfn-my-cool-topic is not a valid arn for an SNS topic'); - }); - test('globless bootstrap uses environment without question', async () => { - // GIVEN - const toolkit = defaultToolkitSetup(); + describe('sns notification arns', () => { + beforeEach(() => { + cloudExecutable = new MockCloudExecutable({ + stacks: [ + MockStack.MOCK_STACK_WITH_NOTIFICATION_ARNS, + MockStack.MOCK_STACK_WITH_BAD_NOTIFICATION_ARNS, + ], + }); + }); - // WHEN - await toolkit.bootstrap(['aws://56789/south-pole'], bootstrapper, {}); + test('with sns notification arns in the executable', async () => { + // GIVEN + const expectedNotificationArns = [ + 'arn:aws:sns:bermuda-triangle-1337:123456789012:MyTopic', + ]; + const toolkit = new CdkToolkit({ + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: new FakeCloudFormation({ + 'Test-Stack-Notification-Arns': { Foo: 'Bar' }, + }, expectedNotificationArns), + }); + + // WHEN + await toolkit.deploy({ + selector: { patterns: ['Test-Stack-Notification-Arns'] }, + hotswap: HotswapMode.FULL_DEPLOYMENT, + }); + }); - // THEN - expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledWith({ - account: '56789', - region: 'south-pole', - name: 'aws://56789/south-pole', - }, expect.anything(), expect.anything()); - expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledTimes(1); - }); + test('fail with incorrect sns notification arns in the executable', async () => { + // GIVEN + const toolkit = new CdkToolkit({ + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: new FakeCloudFormation({ + 'Test-Stack-Bad-Notification-Arns': { Foo: 'Bar' }, + }), + }); + + // WHEN + await expect(() => + toolkit.deploy({ + selector: { patterns: ['Test-Stack-Bad-Notification-Arns'] }, + hotswap: HotswapMode.FULL_DEPLOYMENT, + }), + ).rejects.toThrow('Notification arn arn:1337:123456789012:sns:bad is not a valid arn for an SNS topic'); + }); - test('globby bootstrap uses whats in the stacks', async () => { - // GIVEN - const toolkit = defaultToolkitSetup(); - cloudExecutable.configuration.settings.set(['app'], 'something'); + test('with sns notification arns in the executable and as options', async () => { + // GIVEN + const notificationArns = [ + 'arn:aws:sns:us-east-2:444455556666:MyTopic', + 'arn:aws:sns:eu-west-1:111155556666:my-great-topic', + ]; + + const expectedNotificationArns = notificationArns.concat(['arn:aws:sns:bermuda-triangle-1337:123456789012:MyTopic']); + const toolkit = new CdkToolkit({ + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: new FakeCloudFormation({ + 'Test-Stack-Notification-Arns': { Foo: 'Bar' }, + }, expectedNotificationArns), + }); + + // WHEN + await toolkit.deploy({ + selector: { patterns: ['Test-Stack-Notification-Arns'] }, + notificationArns, + hotswap: HotswapMode.FULL_DEPLOYMENT, + }); + }); - // WHEN - await toolkit.bootstrap(['aws://*/bermuda-triangle-1'], bootstrapper, {}); + test('fail with incorrect sns notification arns in the executable and incorrect sns notification arns as options', async () => { + // GIVEN + const notificationArns = ['arn:::cfn-my-cool-topic']; + const toolkit = new CdkToolkit({ + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: new FakeCloudFormation({ + 'Test-Stack-Bad-Notification-Arns': { Foo: 'Bar' }, + }, notificationArns), + }); + + // WHEN + await expect(() => + toolkit.deploy({ + selector: { patterns: ['Test-Stack-Bad-Notification-Arns'] }, + notificationArns, + hotswap: HotswapMode.FULL_DEPLOYMENT, + }), + ).rejects.toThrow('Notification arn arn:::cfn-my-cool-topic is not a valid arn for an SNS topic'); + }); - // THEN - expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledWith({ - account: '123456789012', - region: 'bermuda-triangle-1', - name: 'aws://123456789012/bermuda-triangle-1', - }, expect.anything(), expect.anything()); - expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledTimes(1); + test('fail with incorrect sns notification arns in the executable and correct sns notification arns as options', async () => { + // GIVEN + const notificationArns = ['arn:aws:sns:bermuda-triangle-1337:123456789012:MyTopic']; + const toolkit = new CdkToolkit({ + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: new FakeCloudFormation({ + 'Test-Stack-Bad-Notification-Arns': { Foo: 'Bar' }, + }, notificationArns), + }); + + // WHEN + await expect(() => + toolkit.deploy({ + selector: { patterns: ['Test-Stack-Bad-Notification-Arns'] }, + notificationArns, + hotswap: HotswapMode.FULL_DEPLOYMENT, + }), + ).rejects.toThrow('Notification arn arn:1337:123456789012:sns:bad is not a valid arn for an SNS topic'); + }); + + test('fail with correct sns notification arns in the executable and incorrect sns notification arns as options', async () => { + // GIVEN + const notificationArns = ['arn:::cfn-my-cool-topic']; + const toolkit = new CdkToolkit({ + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: new FakeCloudFormation({ + 'Test-Stack-Notification-Arns': { Foo: 'Bar' }, + }, notificationArns), + }); + + // WHEN + await expect(() => + toolkit.deploy({ + selector: { patterns: ['Test-Stack-Notification-Arns'] }, + notificationArns, + hotswap: HotswapMode.FULL_DEPLOYMENT, + }), + ).rejects.toThrow('Notification arn arn:::cfn-my-cool-topic is not a valid arn for an SNS topic'); + }); }); + }); - test('bootstrap can be invoked without the --app argument', async () => { - // GIVEN - cloudExecutable.configuration.settings.clear(); - const mockSynthesize = jest.fn(); - cloudExecutable.synthesize = mockSynthesize; + test('globless bootstrap uses environment without question', async () => { + // GIVEN + const toolkit = defaultToolkitSetup(); - const toolkit = defaultToolkitSetup(); + // WHEN + await toolkit.bootstrap(['aws://56789/south-pole'], bootstrapper, {}); - // WHEN - await toolkit.bootstrap(['aws://123456789012/west-pole'], bootstrapper, {}); + // THEN + expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledWith({ + account: '56789', + region: 'south-pole', + name: 'aws://56789/south-pole', + }, expect.anything(), expect.anything()); + expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledTimes(1); + }); - // THEN - expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledWith({ - account: '123456789012', - region: 'west-pole', - name: 'aws://123456789012/west-pole', - }, expect.anything(), expect.anything()); - expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledTimes(1); + test('globby bootstrap uses whats in the stacks', async () => { + // GIVEN + const toolkit = defaultToolkitSetup(); + cloudExecutable.configuration.settings.set(['app'], 'something'); - expect(cloudExecutable.hasApp).toEqual(false); - expect(mockSynthesize).not.toHaveBeenCalled(); - }); + // WHEN + await toolkit.bootstrap(['aws://*/bermuda-triangle-1'], bootstrapper, {}); + + // THEN + expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledWith({ + account: '123456789012', + region: 'bermuda-triangle-1', + name: 'aws://123456789012/bermuda-triangle-1', + }, expect.anything(), expect.anything()); + expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledTimes(1); + }); + + test('bootstrap can be invoked without the --app argument', async () => { + // GIVEN + cloudExecutable.configuration.settings.clear(); + const mockSynthesize = jest.fn(); + cloudExecutable.synthesize = mockSynthesize; + + const toolkit = defaultToolkitSetup(); + + // WHEN + await toolkit.bootstrap(['aws://123456789012/west-pole'], bootstrapper, {}); + + // THEN + expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledWith({ + account: '123456789012', + region: 'west-pole', + name: 'aws://123456789012/west-pole', + }, expect.anything(), expect.anything()); + expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledTimes(1); + + expect(cloudExecutable.hasApp).toEqual(false); + expect(mockSynthesize).not.toHaveBeenCalled(); }); }); @@ -1155,6 +1297,39 @@ class MockStack { }, }, } + public static readonly MOCK_STACK_WITH_NOTIFICATION_ARNS: TestStackArtifact = { + stackName: 'Test-Stack-Notification-Arns', + notificationArns: ['arn:aws:sns:bermuda-triangle-1337:123456789012:MyTopic'], + template: { Resources: { TemplateName: 'Test-Stack-Notification-Arns' } }, + env: 'aws://123456789012/bermuda-triangle-1337', + metadata: { + '/Test-Stack-Notification-Arns': [ + { + type: cxschema.ArtifactMetadataEntryType.STACK_TAGS, + data: [ + { key: 'Foo', value: 'Bar' }, + ], + }, + ], + }, + } + + public static readonly MOCK_STACK_WITH_BAD_NOTIFICATION_ARNS: TestStackArtifact = { + stackName: 'Test-Stack-Bad-Notification-Arns', + notificationArns: ['arn:1337:123456789012:sns:bad'], + template: { Resources: { TemplateName: 'Test-Stack-Bad-Notification-Arns' } }, + env: 'aws://123456789012/bermuda-triangle-1337', + metadata: { + '/Test-Stack-Bad-Notification-Arns': [ + { + type: cxschema.ArtifactMetadataEntryType.STACK_TAGS, + data: [ + { key: 'Foo', value: 'Bar' }, + ], + }, + ], + }, + } } class FakeCloudFormation extends Deployments { @@ -1183,6 +1358,9 @@ class FakeCloudFormation extends Deployments { MockStack.MOCK_STACK_B.stackName, MockStack.MOCK_STACK_C.stackName, MockStack.MOCK_STACK_WITH_ASSET.stackName, + MockStack.MOCK_STACK_WITH_ERROR.stackName, + MockStack.MOCK_STACK_WITH_NOTIFICATION_ARNS.stackName, + MockStack.MOCK_STACK_WITH_BAD_NOTIFICATION_ARNS.stackName, ]).toContain(options.stack.stackName); if (this.expectedTags[options.stack.stackName]) { @@ -1213,8 +1391,12 @@ class FakeCloudFormation extends Deployments { return Promise.resolve({}); case MockStack.MOCK_STACK_WITH_ASSET.stackName: return Promise.resolve({}); + case MockStack.MOCK_STACK_WITH_NOTIFICATION_ARNS.stackName: + return Promise.resolve({}); + case MockStack.MOCK_STACK_WITH_BAD_NOTIFICATION_ARNS.stackName: + return Promise.resolve({}); default: - return Promise.reject(`Not an expected mock stack: ${stack.stackName}`); + throw new Error(`not an expected mock stack: ${stack.stackName}`); } } } diff --git a/packages/aws-cdk/test/util.ts b/packages/aws-cdk/test/util.ts index 879d6572f369b..1f059836d670d 100644 --- a/packages/aws-cdk/test/util.ts +++ b/packages/aws-cdk/test/util.ts @@ -16,6 +16,7 @@ export interface TestStackArtifact { env?: string; depends?: string[]; metadata?: cxapi.StackMetadata; + notificationArns?: string[]; /** Old-style assets */ assets?: cxschema.AssetMetadataEntry[]; @@ -101,6 +102,7 @@ function addAttributes(assembly: TestAssembly, builder: cxapi.CloudAssemblyBuild ...stack.properties, templateFile, terminationProtection: stack.terminationProtection, + notificationArns: stack.notificationArns, }, displayName: stack.displayName, }); From af40c5e3285887029eaca48636cb91de5b58ba32 Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Thu, 13 Jun 2024 10:21:47 -0700 Subject: [PATCH 07/51] toolkit tests --- packages/aws-cdk/test/cdk-toolkit.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/aws-cdk/test/cdk-toolkit.test.ts b/packages/aws-cdk/test/cdk-toolkit.test.ts index d9fb85eaf9eb2..9e29d65a29c51 100644 --- a/packages/aws-cdk/test/cdk-toolkit.test.ts +++ b/packages/aws-cdk/test/cdk-toolkit.test.ts @@ -1347,9 +1347,7 @@ class FakeCloudFormation extends Deployments { Object.entries(tags).map(([Key, Value]) => ({ Key, Value })) .sort((l, r) => l.Key.localeCompare(r.Key)); } - if (expectedNotificationArns) { - this.expectedNotificationArns = expectedNotificationArns; - } + this.expectedNotificationArns = expectedNotificationArns ?? []; } public deployStack(options: DeployStackOptions): Promise { From ffaf14e51e7f6a1aa6e2b8593d49e76990e7638e Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Thu, 13 Jun 2024 14:50:50 -0700 Subject: [PATCH 08/51] docs --- packages/aws-cdk-lib/core/lib/stack.ts | 16 ++++++++-------- .../lib/artifacts/cloudformation-artifact.ts | 2 +- packages/aws-cdk/lib/api/util/cloudformation.ts | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/aws-cdk-lib/core/lib/stack.ts b/packages/aws-cdk-lib/core/lib/stack.ts index 0982dc7714c53..098f8c090fd82 100644 --- a/packages/aws-cdk-lib/core/lib/stack.ts +++ b/packages/aws-cdk-lib/core/lib/stack.ts @@ -128,7 +128,7 @@ export interface StackProps { readonly tags?: { [key: string]: string }; /** - * foofoo + * SNS Topic ARNs that will receive stack events. * * @default wowowowowo */ @@ -371,6 +371,13 @@ export class Stack extends Construct implements ITaggable { */ public readonly _crossRegionReferences: boolean; + /** + * SNS Notification ARNs to receive stack events. + * + * @internal + */ + public readonly _notificationArns: string[]; + /** * Logical ID generation strategy */ @@ -390,13 +397,6 @@ export class Stack extends Construct implements ITaggable { private readonly _stackName: string; - /** - * notification arns passed through stack props. - * - * @internal - */ - public readonly _notificationArns: string[]; - /** * Enable this flag to suppress indentation in generated * CloudFormation templates. diff --git a/packages/aws-cdk-lib/cx-api/lib/artifacts/cloudformation-artifact.ts b/packages/aws-cdk-lib/cx-api/lib/artifacts/cloudformation-artifact.ts index b7e632a90dc1c..d73e2a5b33dd7 100644 --- a/packages/aws-cdk-lib/cx-api/lib/artifacts/cloudformation-artifact.ts +++ b/packages/aws-cdk-lib/cx-api/lib/artifacts/cloudformation-artifact.ts @@ -55,7 +55,7 @@ export class CloudFormationStackArtifact extends CloudArtifact { public readonly tags: { [id: string]: string }; /** - * CloudFormation tags to pass to the stack. + * SNS Topics that will receive stack events. */ public readonly notificationArns: string[]; diff --git a/packages/aws-cdk/lib/api/util/cloudformation.ts b/packages/aws-cdk/lib/api/util/cloudformation.ts index 7b5a1d9dd3fcd..2361871e2bef0 100644 --- a/packages/aws-cdk/lib/api/util/cloudformation.ts +++ b/packages/aws-cdk/lib/api/util/cloudformation.ts @@ -145,7 +145,7 @@ export class CloudFormationStack { } /** - * The stack's current notification ARNs. + * SNS Topic ARNs that will receive stack events. * * Empty list if the stack does not exist */ From 055d4968aec639230f67466009b7ea0759a5975b Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Thu, 13 Jun 2024 15:07:39 -0700 Subject: [PATCH 09/51] remove the bad cast, it is evil --- packages/aws-cdk-lib/core/lib/stack-synthesizers/_shared.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/aws-cdk-lib/core/lib/stack-synthesizers/_shared.ts b/packages/aws-cdk-lib/core/lib/stack-synthesizers/_shared.ts index bdbcd50c11c9e..c985c538cac81 100644 --- a/packages/aws-cdk-lib/core/lib/stack-synthesizers/_shared.ts +++ b/packages/aws-cdk-lib/core/lib/stack-synthesizers/_shared.ts @@ -48,7 +48,7 @@ export function addStackArtifactToAssembly( terminationProtection: stack.terminationProtection, tags: nonEmptyDict(stack.tags.tagValues()), validateOnSynth: session.validateOnSynth, - notificationArns: (stack as any)._notificationArns, + notificationArns: stack._notificationArns, ...stackProps, ...stackNameProperty, }; From fc184cc7216bf7d48ca16c0c875fb7e8c7ceac11 Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Thu, 13 Jun 2024 15:08:29 -0700 Subject: [PATCH 10/51] cleanup --- packages/aws-cdk-lib/core/lib/stack.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/aws-cdk-lib/core/lib/stack.ts b/packages/aws-cdk-lib/core/lib/stack.ts index 098f8c090fd82..7283da71844ac 100644 --- a/packages/aws-cdk-lib/core/lib/stack.ts +++ b/packages/aws-cdk-lib/core/lib/stack.ts @@ -130,7 +130,7 @@ export interface StackProps { /** * SNS Topic ARNs that will receive stack events. * - * @default wowowowowo + * @default - no notfication arns. */ readonly notificationArns?: string[]; From 5e606af738baf3203a2c96429e9678392c9ff9bb Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Thu, 13 Jun 2024 15:26:14 -0700 Subject: [PATCH 11/51] silly licenses --- packages/@aws-cdk/cli-lib-alpha/THIRD_PARTY_LICENSES | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk/cli-lib-alpha/THIRD_PARTY_LICENSES b/packages/@aws-cdk/cli-lib-alpha/THIRD_PARTY_LICENSES index c037af426d656..30c90c16203a6 100644 --- a/packages/@aws-cdk/cli-lib-alpha/THIRD_PARTY_LICENSES +++ b/packages/@aws-cdk/cli-lib-alpha/THIRD_PARTY_LICENSES @@ -207,7 +207,7 @@ The @aws-cdk/cli-lib-alpha package includes the following third-party software/l ---------------- -** @jsii/check-node@1.98.0 - https://www.npmjs.com/package/@jsii/check-node/v/1.98.0 | Apache-2.0 +** @jsii/check-node@1.97.0 - https://www.npmjs.com/package/@jsii/check-node/v/1.97.0 | Apache-2.0 jsii Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. @@ -266,7 +266,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------- -** ajv@8.13.0 - https://www.npmjs.com/package/ajv/v/8.13.0 | MIT +** ajv@8.12.0 - https://www.npmjs.com/package/ajv/v/8.12.0 | MIT The MIT License (MIT) Copyright (c) 2015-2021 Evgeny Poberezkin @@ -493,7 +493,7 @@ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH RE ---------------- -** aws-sdk@2.1610.0 - https://www.npmjs.com/package/aws-sdk/v/2.1610.0 | Apache-2.0 +** aws-sdk@2.1596.0 - https://www.npmjs.com/package/aws-sdk/v/2.1596.0 | Apache-2.0 AWS SDK for JavaScript Copyright 2012-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. @@ -691,7 +691,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI ---------------- -** cdk-from-cfn@0.159.0 - https://www.npmjs.com/package/cdk-from-cfn/v/0.159.0 | MIT OR Apache-2.0 +** cdk-from-cfn@0.156.0 - https://www.npmjs.com/package/cdk-from-cfn/v/0.156.0 | MIT OR Apache-2.0 ---------------- @@ -3832,4 +3832,4 @@ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------- +---------------- \ No newline at end of file From 180066b24b7e7966304d77a25b621bca7702df15 Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Thu, 13 Jun 2024 15:26:42 -0700 Subject: [PATCH 12/51] newline\n... --- packages/@aws-cdk/cli-lib-alpha/THIRD_PARTY_LICENSES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/cli-lib-alpha/THIRD_PARTY_LICENSES b/packages/@aws-cdk/cli-lib-alpha/THIRD_PARTY_LICENSES index 30c90c16203a6..2c9a5a75dad60 100644 --- a/packages/@aws-cdk/cli-lib-alpha/THIRD_PARTY_LICENSES +++ b/packages/@aws-cdk/cli-lib-alpha/THIRD_PARTY_LICENSES @@ -3832,4 +3832,4 @@ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------- \ No newline at end of file +---------------- From 2bc9320dd38fb6970128db5f63dfc1293b23798b Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Thu, 13 Jun 2024 16:01:54 -0700 Subject: [PATCH 13/51] foo --- packages/aws-cdk-lib/core/README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/aws-cdk-lib/core/README.md b/packages/aws-cdk-lib/core/README.md index 7f6f1b486166b..7075ee6c24f18 100644 --- a/packages/aws-cdk-lib/core/README.md +++ b/packages/aws-cdk-lib/core/README.md @@ -1242,6 +1242,18 @@ const stack = new Stack(app, 'StackName', { }); ``` +### Receiving CloudFormation Stack Events + +You can add SNS Topic ARNs to any Stack: + +```ts +const stack = new Stack(app, 'StackName', { + notificationArns: ['arn:aws:sns:us-east-1:23456789012:Topic'], +}); +``` + +Stack events will be sent to any SNS Topics in this list. + ### CfnJson `CfnJson` allows you to postpone the resolution of a JSON blob from From e9f9fa4a0b17e15eb8e30cfb659fd9d320f394f1 Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Fri, 14 Jun 2024 11:38:03 -0700 Subject: [PATCH 14/51] small change to trigger test pipeline --- packages/aws-cdk-lib/core/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/aws-cdk-lib/core/README.md b/packages/aws-cdk-lib/core/README.md index 7075ee6c24f18..77489c356f4ac 100644 --- a/packages/aws-cdk-lib/core/README.md +++ b/packages/aws-cdk-lib/core/README.md @@ -1244,7 +1244,7 @@ const stack = new Stack(app, 'StackName', { ### Receiving CloudFormation Stack Events -You can add SNS Topic ARNs to any Stack: +You can add one or more SNS Topic ARNs to any Stack: ```ts const stack = new Stack(app, 'StackName', { From 48b8e49dbc1fb0d7fff88ed4651f0d8ff71d01e7 Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Mon, 17 Jun 2024 09:06:43 -0700 Subject: [PATCH 15/51] destroy fix hopefully --- packages/aws-cdk/test/cdk-toolkit.test.ts | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/aws-cdk/test/cdk-toolkit.test.ts b/packages/aws-cdk/test/cdk-toolkit.test.ts index 9e29d65a29c51..8361104e00c7a 100644 --- a/packages/aws-cdk/test/cdk-toolkit.test.ts +++ b/packages/aws-cdk/test/cdk-toolkit.test.ts @@ -110,6 +110,17 @@ function defaultToolkitSetup() { }); } +function destroyToolkitSetup() { + return new CdkToolkit({ + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: new FakeCloudFormation({ + 'Test-Stack-Destroy': { Foo: 'Bar' }, + }), + }); +} + describe('readCurrentTemplate', () => { let template: any; let mockForEnvironment = jest.fn(); @@ -731,11 +742,11 @@ describe('deploy', () => { describe('destroy', () => { test('destroy correct stack', async () => { - const toolkit = defaultToolkitSetup(); + const toolkit = destroyToolkitSetup(); - await expect(() => { + expect(() => { return toolkit.destroy({ - selector: { patterns: ['Test-Stack-A/Test-Stack-C'] }, + selector: { patterns: ['Test-Stack-Destroy'] }, exclusively: true, force: true, fromDeploy: true, @@ -996,10 +1007,6 @@ describe('synth', () => { expect(mockData.mock.calls.length).toEqual(0); }); - afterEach(() => { - process.env.STACKS_TO_VALIDATE = undefined; - }); - describe('migrate', () => { const testResourcePath = [__dirname, 'commands', 'test-resources']; const templatePath = [...testResourcePath, 'templates']; From effc51070dcd13107ffa754c9914ef017f6d80ae Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Mon, 17 Jun 2024 12:45:37 -0700 Subject: [PATCH 16/51] jest...... --- packages/aws-cdk/test/cdk-toolkit.test.ts | 94 ++++++++++++----------- 1 file changed, 48 insertions(+), 46 deletions(-) diff --git a/packages/aws-cdk/test/cdk-toolkit.test.ts b/packages/aws-cdk/test/cdk-toolkit.test.ts index 8361104e00c7a..fc6942b3723c7 100644 --- a/packages/aws-cdk/test/cdk-toolkit.test.ts +++ b/packages/aws-cdk/test/cdk-toolkit.test.ts @@ -493,62 +493,64 @@ describe('deploy', () => { }); }); - test('with sns notification arns as options', async () => { - // GIVEN - const notificationArns = [ - 'arn:aws:sns:us-east-2:444455556666:MyTopic', - 'arn:aws:sns:eu-west-1:111155556666:my-great-topic', - ]; - const toolkit = new CdkToolkit({ - cloudExecutable, - configuration: cloudExecutable.configuration, - sdkProvider: cloudExecutable.sdkProvider, - deployments: new FakeCloudFormation({ - 'Test-Stack-A': { Foo: 'Bar' }, - 'Test-Stack-B': { Baz: 'Zinga!' }, - }, notificationArns), - }); - - // WHEN - await toolkit.deploy({ - selector: { patterns: ['Test-Stack-A', 'Test-Stack-B'] }, - notificationArns, - hotswap: HotswapMode.FULL_DEPLOYMENT, - }); - }); - - test('fail with incorrect sns notification arns as options', async () => { - // GIVEN - const notificationArns = ['arn:::cfn-my-cool-topic']; - const toolkit = new CdkToolkit({ - cloudExecutable, - configuration: cloudExecutable.configuration, - sdkProvider: cloudExecutable.sdkProvider, - deployments: new FakeCloudFormation({ - 'Test-Stack-A': { Foo: 'Bar' }, - }, notificationArns), - }); - - // WHEN - await expect(() => - toolkit.deploy({ - selector: { patterns: ['Test-Stack-A'] }, - notificationArns, - hotswap: HotswapMode.FULL_DEPLOYMENT, - }), - ).rejects.toThrow('Notification arn arn:::cfn-my-cool-topic is not a valid arn for an SNS topic'); - }); - describe('sns notification arns', () => { beforeEach(() => { cloudExecutable = new MockCloudExecutable({ stacks: [ + MockStack.MOCK_STACK_A, + MockStack.MOCK_STACK_B, MockStack.MOCK_STACK_WITH_NOTIFICATION_ARNS, MockStack.MOCK_STACK_WITH_BAD_NOTIFICATION_ARNS, ], }); }); + test('with sns notification arns as options', async () => { + // GIVEN + const notificationArns = [ + 'arn:aws:sns:us-east-2:444455556666:MyTopic', + 'arn:aws:sns:eu-west-1:111155556666:my-great-topic', + ]; + const toolkit = new CdkToolkit({ + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: new FakeCloudFormation({ + 'Test-Stack-A': { Foo: 'Bar' }, + 'Test-Stack-B': { Baz: 'Zinga!' }, + }, notificationArns), + }); + + // WHEN + await toolkit.deploy({ + selector: { patterns: ['Test-Stack-A', 'Test-Stack-B'] }, + notificationArns, + hotswap: HotswapMode.FULL_DEPLOYMENT, + }); + }); + + test('fail with incorrect sns notification arns as options', async () => { + // GIVEN + const notificationArns = ['arn:::cfn-my-cool-topic']; + const toolkit = new CdkToolkit({ + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: new FakeCloudFormation({ + 'Test-Stack-A': { Foo: 'Bar' }, + }, notificationArns), + }); + + // WHEN + await expect(() => + toolkit.deploy({ + selector: { patterns: ['Test-Stack-A'] }, + notificationArns, + hotswap: HotswapMode.FULL_DEPLOYMENT, + }), + ).rejects.toThrow('Notification arn arn:::cfn-my-cool-topic is not a valid arn for an SNS topic'); + }); + test('with sns notification arns in the executable', async () => { // GIVEN const expectedNotificationArns = [ From 8e7fc27ac8c777f002648c65489964449576124c Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Mon, 17 Jun 2024 14:14:04 -0700 Subject: [PATCH 17/51] tests...jest.... --- packages/aws-cdk/test/cdk-toolkit.test.ts | 56 +++++++++++++++++++---- 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/packages/aws-cdk/test/cdk-toolkit.test.ts b/packages/aws-cdk/test/cdk-toolkit.test.ts index fc6942b3723c7..46524a0eceff4 100644 --- a/packages/aws-cdk/test/cdk-toolkit.test.ts +++ b/packages/aws-cdk/test/cdk-toolkit.test.ts @@ -497,8 +497,8 @@ describe('deploy', () => { beforeEach(() => { cloudExecutable = new MockCloudExecutable({ stacks: [ - MockStack.MOCK_STACK_A, - MockStack.MOCK_STACK_B, + MockStack.MOCK_STACK_E, + MockStack.MOCK_STACK_F, MockStack.MOCK_STACK_WITH_NOTIFICATION_ARNS, MockStack.MOCK_STACK_WITH_BAD_NOTIFICATION_ARNS, ], @@ -516,14 +516,14 @@ describe('deploy', () => { configuration: cloudExecutable.configuration, sdkProvider: cloudExecutable.sdkProvider, deployments: new FakeCloudFormation({ - 'Test-Stack-A': { Foo: 'Bar' }, - 'Test-Stack-B': { Baz: 'Zinga!' }, + 'Test-Stack-E': { Foo: 'Bar' }, + 'Test-Stack-F': { Foo: 'Bar' }, }, notificationArns), }); // WHEN await toolkit.deploy({ - selector: { patterns: ['Test-Stack-A', 'Test-Stack-B'] }, + selector: { patterns: ['Test-Stack-E', 'Test-Stack-F'] }, notificationArns, hotswap: HotswapMode.FULL_DEPLOYMENT, }); @@ -537,14 +537,14 @@ describe('deploy', () => { configuration: cloudExecutable.configuration, sdkProvider: cloudExecutable.sdkProvider, deployments: new FakeCloudFormation({ - 'Test-Stack-A': { Foo: 'Bar' }, + 'Test-Stack-E': { Foo: 'Bar' }, }, notificationArns), }); // WHEN await expect(() => toolkit.deploy({ - selector: { patterns: ['Test-Stack-A'] }, + selector: { patterns: ['Test-Stack-E'] }, notificationArns, hotswap: HotswapMode.FULL_DEPLOYMENT, }), @@ -1274,7 +1274,40 @@ class MockStack { ], }, depends: [MockStack.MOCK_STACK_C.stackName], - } + }; + public static readonly MOCK_STACK_E: TestStackArtifact = { + stackName: 'Test-Stack-E', + template: { Resources: { TemplateName: 'Test-Stack-E' } }, + env: 'aws://123456789012/bermuda-triangle-1', + metadata: { + '/Test-Stack-E': [ + { + type: cxschema.ArtifactMetadataEntryType.STACK_TAGS, + data: [ + { key: 'Foo', value: 'Bar' }, + ], + }, + ], + }, + displayName: 'Test-Stack-E-Display-Name', + }; + + public static readonly MOCK_STACK_F: TestStackArtifact = { + stackName: 'Test-Stack-F', + template: { Resources: { TemplateName: 'Test-Stack-F' } }, + env: 'aws://123456789012/bermuda-triangle-1', + metadata: { + '/Test-Stack-F': [ + { + type: cxschema.ArtifactMetadataEntryType.STACK_TAGS, + data: [ + { key: 'Foo', value: 'Bar' }, + ], + }, + ], + }, + displayName: 'Test-Stack-F-Display-Name', + }; public static readonly MOCK_STACK_WITH_ERROR: TestStackArtifact = { stackName: 'witherrors', env: 'aws://123456789012/bermuda-triangle-1', @@ -1364,6 +1397,9 @@ class FakeCloudFormation extends Deployments { MockStack.MOCK_STACK_A.stackName, MockStack.MOCK_STACK_B.stackName, MockStack.MOCK_STACK_C.stackName, + // MockStack.MOCK_STACK_D deliberately omitted. + MockStack.MOCK_STACK_E.stackName, + MockStack.MOCK_STACK_F.stackName, MockStack.MOCK_STACK_WITH_ASSET.stackName, MockStack.MOCK_STACK_WITH_ERROR.stackName, MockStack.MOCK_STACK_WITH_NOTIFICATION_ARNS.stackName, @@ -1396,6 +1432,10 @@ class FakeCloudFormation extends Deployments { return Promise.resolve({}); case MockStack.MOCK_STACK_C.stackName: return Promise.resolve({}); + case MockStack.MOCK_STACK_E.stackName: + return Promise.resolve({}); + case MockStack.MOCK_STACK_F.stackName: + return Promise.resolve({}); case MockStack.MOCK_STACK_WITH_ASSET.stackName: return Promise.resolve({}); case MockStack.MOCK_STACK_WITH_NOTIFICATION_ARNS.stackName: From e3f2a5c28d78b210f875732bd16ceaf75038c3c9 Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Mon, 17 Jun 2024 16:34:29 -0700 Subject: [PATCH 18/51] ok, fine then... --- packages/aws-cdk/test/cdk-toolkit.test.ts | 1699 +++++++++++---------- 1 file changed, 851 insertions(+), 848 deletions(-) diff --git a/packages/aws-cdk/test/cdk-toolkit.test.ts b/packages/aws-cdk/test/cdk-toolkit.test.ts index 46524a0eceff4..ab7b7cbc7e115 100644 --- a/packages/aws-cdk/test/cdk-toolkit.test.ts +++ b/packages/aws-cdk/test/cdk-toolkit.test.ts @@ -121,1016 +121,1046 @@ function destroyToolkitSetup() { }); } -describe('readCurrentTemplate', () => { - let template: any; - let mockForEnvironment = jest.fn(); - let mockCloudExecutable: MockCloudExecutable; - beforeEach(() => { - - template = { - Resources: { - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Key: 'Value', - }, - }, - }, - }; - mockCloudExecutable = new MockCloudExecutable({ - stacks: [ - { - stackName: 'Test-Stack-C', - template, - properties: { - assumeRoleArn: 'bloop:${AWS::Region}:${AWS::AccountId}', - lookupRole: { - arn: 'bloop-lookup:${AWS::Region}:${AWS::AccountId}', - requiresBootstrapStackVersion: 5, - bootstrapStackVersionSsmParameter: '/bootstrap/parameter', +// everything within this descirbe block will run synchronously, preventing race conditions between tests +describe('toolkit', () => { + describe('readCurrentTemplate', () => { + let template: any; + let mockForEnvironment = jest.fn(); + let mockCloudExecutable: MockCloudExecutable; + beforeEach(() => { + + template = { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Key: 'Value', }, }, }, - { - stackName: 'Test-Stack-A', - template, - properties: { - assumeRoleArn: 'bloop:${AWS::Region}:${AWS::AccountId}', - }, - }, - ], - }); - mockForEnvironment = jest.fn().mockImplementation(() => { - return { sdk: mockCloudExecutable.sdkProvider.sdk, didAssumeRole: true }; - }); - mockCloudExecutable.sdkProvider.forEnvironment = mockForEnvironment; - mockCloudExecutable.sdkProvider.stubCloudFormation({ - getTemplate() { - return { - TemplateBody: JSON.stringify(template), - }; - }, - describeStacks() { - return { - Stacks: [ - { - StackName: 'Test-Stack-C', - StackStatus: 'CREATE_COMPLETE', - CreationTime: new Date(), - }, - { - StackName: 'Test-Stack-A', - StackStatus: 'CREATE_COMPLETE', - CreationTime: new Date(), + }; + mockCloudExecutable = new MockCloudExecutable({ + stacks: [ + { + stackName: 'Test-Stack-C', + template, + properties: { + assumeRoleArn: 'bloop:${AWS::Region}:${AWS::AccountId}', + lookupRole: { + arn: 'bloop-lookup:${AWS::Region}:${AWS::AccountId}', + requiresBootstrapStackVersion: 5, + bootstrapStackVersionSsmParameter: '/bootstrap/parameter', + }, }, - ], - }; - }, - }); - }); - - test('lookup role is used', async () => { - // GIVEN - let requestedParameterName: string; - mockCloudExecutable.sdkProvider.stubSSM({ - getParameter(request) { - requestedParameterName = request.Name; - return { - Parameter: { - Value: '6', }, - }; - }, - }); - const cdkToolkit = new CdkToolkit({ - cloudExecutable: mockCloudExecutable, - configuration: mockCloudExecutable.configuration, - sdkProvider: mockCloudExecutable.sdkProvider, - deployments: new Deployments({ sdkProvider: mockCloudExecutable.sdkProvider }), - }); - - // WHEN - await cdkToolkit.deploy({ - selector: { patterns: ['Test-Stack-C'] }, - hotswap: HotswapMode.FULL_DEPLOYMENT, - }); - - // THEN - expect(requestedParameterName!).toEqual('/bootstrap/parameter'); - expect(mockForEnvironment.mock.calls.length).toEqual(2); - expect(mockForEnvironment.mock.calls[0][2]).toEqual({ - assumeRoleArn: 'bloop-lookup:here:123456789012', - }); - }); - - test('fallback to deploy role if bootstrap stack version is not valid', async () => { - // GIVEN - let requestedParameterName: string; - mockCloudExecutable.sdkProvider.stubSSM({ - getParameter(request) { - requestedParameterName = request.Name; - return { - Parameter: { - Value: '1', + { + stackName: 'Test-Stack-A', + template, + properties: { + assumeRoleArn: 'bloop:${AWS::Region}:${AWS::AccountId}', + }, }, - }; - }, - }); - const cdkToolkit = new CdkToolkit({ - cloudExecutable: mockCloudExecutable, - configuration: mockCloudExecutable.configuration, - sdkProvider: mockCloudExecutable.sdkProvider, - deployments: new Deployments({ sdkProvider: mockCloudExecutable.sdkProvider }), - }); - - // WHEN - await cdkToolkit.deploy({ - selector: { patterns: ['Test-Stack-C'] }, - hotswap: HotswapMode.FULL_DEPLOYMENT, - }); - - // THEN - expect(flatten(stderrMock.mock.calls)).toEqual(expect.arrayContaining([ - expect.stringMatching(/Could not assume bloop-lookup:here:123456789012/), - expect.stringContaining("Bootstrap stack version '5' is required, found version '1'. To get rid of this error, please upgrade to bootstrap version >= 5"), - ])); - expect(requestedParameterName!).toEqual('/bootstrap/parameter'); - expect(mockForEnvironment.mock.calls.length).toEqual(3); - expect(mockForEnvironment.mock.calls[0][2]).toEqual({ - assumeRoleArn: 'bloop-lookup:here:123456789012', - }); - expect(mockForEnvironment.mock.calls[1][2]).toEqual({ - assumeRoleArn: 'bloop:here:123456789012', - }); - }); - - test('fallback to deploy role if bootstrap version parameter not found', async () => { - // GIVEN - mockCloudExecutable.sdkProvider.stubSSM({ - getParameter() { - throw new Error('not found'); - }, - }); - const cdkToolkit = new CdkToolkit({ - cloudExecutable: mockCloudExecutable, - configuration: mockCloudExecutable.configuration, - sdkProvider: mockCloudExecutable.sdkProvider, - deployments: new Deployments({ sdkProvider: mockCloudExecutable.sdkProvider }), - }); - - // WHEN - await cdkToolkit.deploy({ - selector: { patterns: ['Test-Stack-C'] }, - hotswap: HotswapMode.FULL_DEPLOYMENT, - }); - - // THEN - expect(flatten(stderrMock.mock.calls)).toEqual(expect.arrayContaining([ - expect.stringMatching(/Could not assume bloop-lookup:here:123456789012/), - ])); - expect(mockForEnvironment.mock.calls.length).toEqual(3); - expect(mockForEnvironment.mock.calls[0][2]).toEqual({ - assumeRoleArn: 'bloop-lookup:here:123456789012', - }); - expect(mockForEnvironment.mock.calls[1][2]).toEqual({ - assumeRoleArn: 'bloop:here:123456789012', - }); - }); - - test('fallback to deploy role if forEnvironment throws', async () => { - // GIVEN - // throw error first for the 'prepareSdkWithLookupRoleFor' call and succeed for the rest - mockForEnvironment = jest.fn().mockImplementationOnce(() => { throw new Error('error'); }) - .mockImplementation(() => { return { sdk: mockCloudExecutable.sdkProvider.sdk, didAssumeRole: true };}); - mockCloudExecutable.sdkProvider.forEnvironment = mockForEnvironment; - mockCloudExecutable.sdkProvider.stubSSM({ - getParameter() { - return { }; - }, - }); - const cdkToolkit = new CdkToolkit({ - cloudExecutable: mockCloudExecutable, - configuration: mockCloudExecutable.configuration, - sdkProvider: mockCloudExecutable.sdkProvider, - deployments: new Deployments({ sdkProvider: mockCloudExecutable.sdkProvider }), - }); - - // WHEN - await cdkToolkit.deploy({ - selector: { patterns: ['Test-Stack-C'] }, - hotswap: HotswapMode.FULL_DEPLOYMENT, - }); - - // THEN - expect(mockCloudExecutable.sdkProvider.sdk.ssm).not.toHaveBeenCalled(); - expect(flatten(stderrMock.mock.calls)).toEqual(expect.arrayContaining([ - expect.stringMatching(/Could not assume bloop-lookup:here:123456789012/), - ])); - expect(mockForEnvironment.mock.calls.length).toEqual(3); - expect(mockForEnvironment.mock.calls[0][2]).toEqual({ - assumeRoleArn: 'bloop-lookup:here:123456789012', - }); - expect(mockForEnvironment.mock.calls[1][2]).toEqual({ - assumeRoleArn: 'bloop:here:123456789012', + ], + }); + mockForEnvironment = jest.fn().mockImplementation(() => { + return { sdk: mockCloudExecutable.sdkProvider.sdk, didAssumeRole: true }; + }); + mockCloudExecutable.sdkProvider.forEnvironment = mockForEnvironment; + mockCloudExecutable.sdkProvider.stubCloudFormation({ + getTemplate() { + return { + TemplateBody: JSON.stringify(template), + }; + }, + describeStacks() { + return { + Stacks: [ + { + StackName: 'Test-Stack-C', + StackStatus: 'CREATE_COMPLETE', + CreationTime: new Date(), + }, + { + StackName: 'Test-Stack-A', + StackStatus: 'CREATE_COMPLETE', + CreationTime: new Date(), + }, + ], + }; + }, + }); }); - }); - test('dont lookup bootstrap version parameter if default credentials are used', async () => { - // GIVEN - mockForEnvironment = jest.fn().mockImplementation(() => { return { sdk: mockCloudExecutable.sdkProvider.sdk, didAssumeRole: false }; }); - mockCloudExecutable.sdkProvider.forEnvironment = mockForEnvironment; - const cdkToolkit = new CdkToolkit({ - cloudExecutable: mockCloudExecutable, - configuration: mockCloudExecutable.configuration, - sdkProvider: mockCloudExecutable.sdkProvider, - deployments: new Deployments({ sdkProvider: mockCloudExecutable.sdkProvider }), - }); - mockCloudExecutable.sdkProvider.stubSSM({ - getParameter() { - return { }; - }, - }); + test('lookup role is used', async () => { + // GIVEN + let requestedParameterName: string; + mockCloudExecutable.sdkProvider.stubSSM({ + getParameter(request) { + requestedParameterName = request.Name; + return { + Parameter: { + Value: '6', + }, + }; + }, + }); + const cdkToolkit = new CdkToolkit({ + cloudExecutable: mockCloudExecutable, + configuration: mockCloudExecutable.configuration, + sdkProvider: mockCloudExecutable.sdkProvider, + deployments: new Deployments({ sdkProvider: mockCloudExecutable.sdkProvider }), + }); - // WHEN - await cdkToolkit.deploy({ - selector: { patterns: ['Test-Stack-C'] }, - hotswap: HotswapMode.FULL_DEPLOYMENT, - }); + // WHEN + await cdkToolkit.deploy({ + selector: { patterns: ['Test-Stack-C'] }, + hotswap: HotswapMode.FULL_DEPLOYMENT, + }); - // THEN - expect(flatten(stderrMock.mock.calls)).toEqual(expect.arrayContaining([ - expect.stringMatching(/Lookup role exists but was not assumed. Proceeding with default credentials./), - ])); - expect(mockCloudExecutable.sdkProvider.sdk.ssm).not.toHaveBeenCalled(); - expect(mockForEnvironment.mock.calls.length).toEqual(3); - expect(mockForEnvironment.mock.calls[0][2]).toEqual({ - assumeRoleArn: 'bloop-lookup:here:123456789012', - }); - expect(mockForEnvironment.mock.calls[1][2]).toEqual({ - assumeRoleArn: 'bloop:here:123456789012', + // THEN + expect(requestedParameterName!).toEqual('/bootstrap/parameter'); + expect(mockForEnvironment.mock.calls.length).toEqual(2); + expect(mockForEnvironment.mock.calls[0][2]).toEqual({ + assumeRoleArn: 'bloop-lookup:here:123456789012', + }); }); - }); - test('do not print warnings if lookup role not provided in stack artifact', async () => { - // GIVEN - mockCloudExecutable.sdkProvider.stubSSM({ - getParameter() { - return {}; - }, - }); - const cdkToolkit = new CdkToolkit({ - cloudExecutable: mockCloudExecutable, - configuration: mockCloudExecutable.configuration, - sdkProvider: mockCloudExecutable.sdkProvider, - deployments: new Deployments({ sdkProvider: mockCloudExecutable.sdkProvider }), - }); + test('fallback to deploy role if bootstrap stack version is not valid', async () => { + // GIVEN + let requestedParameterName: string; + mockCloudExecutable.sdkProvider.stubSSM({ + getParameter(request) { + requestedParameterName = request.Name; + return { + Parameter: { + Value: '1', + }, + }; + }, + }); + const cdkToolkit = new CdkToolkit({ + cloudExecutable: mockCloudExecutable, + configuration: mockCloudExecutable.configuration, + sdkProvider: mockCloudExecutable.sdkProvider, + deployments: new Deployments({ sdkProvider: mockCloudExecutable.sdkProvider }), + }); - // WHEN - await cdkToolkit.deploy({ - selector: { patterns: ['Test-Stack-A'] }, - hotswap: HotswapMode.FULL_DEPLOYMENT, - }); + // WHEN + await cdkToolkit.deploy({ + selector: { patterns: ['Test-Stack-C'] }, + hotswap: HotswapMode.FULL_DEPLOYMENT, + }); - // THEN - expect(flatten(stderrMock.mock.calls)).not.toEqual(expect.arrayContaining([ - expect.stringMatching(/Could not assume/), - expect.stringMatching(/please upgrade to bootstrap version/), - ])); - expect(mockCloudExecutable.sdkProvider.sdk.ssm).not.toHaveBeenCalled(); - expect(mockForEnvironment.mock.calls.length).toEqual(2); - expect(mockForEnvironment.mock.calls[0][2]).toEqual({ - assumeRoleArn: undefined, - assumeRoleExternalId: undefined, + // THEN + expect(flatten(stderrMock.mock.calls)).toEqual(expect.arrayContaining([ + expect.stringMatching(/Could not assume bloop-lookup:here:123456789012/), + expect.stringContaining("Bootstrap stack version '5' is required, found version '1'. To get rid of this error, please upgrade to bootstrap version >= 5"), + ])); + expect(requestedParameterName!).toEqual('/bootstrap/parameter'); + expect(mockForEnvironment.mock.calls.length).toEqual(3); + expect(mockForEnvironment.mock.calls[0][2]).toEqual({ + assumeRoleArn: 'bloop-lookup:here:123456789012', + }); + expect(mockForEnvironment.mock.calls[1][2]).toEqual({ + assumeRoleArn: 'bloop:here:123456789012', + }); }); - }); -}); - -describe('deploy', () => { - test('fails when no valid stack names are given', async () => { - // GIVEN - const toolkit = defaultToolkitSetup(); - - // WHEN - await expect(() => toolkit.deploy({ - selector: { patterns: ['Test-Stack-D'] }, - hotswap: HotswapMode.FULL_DEPLOYMENT, - })).rejects.toThrow('No stacks match the name(s) Test-Stack-D'); - }); - describe('with hotswap deployment', () => { - test("passes through the 'hotswap' option to CloudFormationDeployments.deployStack()", async () => { + test('fallback to deploy role if bootstrap version parameter not found', async () => { // GIVEN - const mockCfnDeployments = instanceMockFrom(Deployments); - mockCfnDeployments.deployStack.mockReturnValue(Promise.resolve({ - noOp: false, - outputs: {}, - stackArn: 'stackArn', - stackArtifact: instanceMockFrom(cxapi.CloudFormationStackArtifact), - })); + mockCloudExecutable.sdkProvider.stubSSM({ + getParameter() { + throw new Error('not found'); + }, + }); const cdkToolkit = new CdkToolkit({ - cloudExecutable, - configuration: cloudExecutable.configuration, - sdkProvider: cloudExecutable.sdkProvider, - deployments: mockCfnDeployments, + cloudExecutable: mockCloudExecutable, + configuration: mockCloudExecutable.configuration, + sdkProvider: mockCloudExecutable.sdkProvider, + deployments: new Deployments({ sdkProvider: mockCloudExecutable.sdkProvider }), }); // WHEN await cdkToolkit.deploy({ - selector: { patterns: ['Test-Stack-A-Display-Name'] }, - requireApproval: RequireApproval.Never, - hotswap: HotswapMode.FALL_BACK, + selector: { patterns: ['Test-Stack-C'] }, + hotswap: HotswapMode.FULL_DEPLOYMENT, }); // THEN - expect(mockCfnDeployments.deployStack).toHaveBeenCalledWith(expect.objectContaining({ - hotswap: HotswapMode.FALL_BACK, - })); + expect(flatten(stderrMock.mock.calls)).toEqual(expect.arrayContaining([ + expect.stringMatching(/Could not assume bloop-lookup:here:123456789012/), + ])); + expect(mockForEnvironment.mock.calls.length).toEqual(3); + expect(mockForEnvironment.mock.calls[0][2]).toEqual({ + assumeRoleArn: 'bloop-lookup:here:123456789012', + }); + expect(mockForEnvironment.mock.calls[1][2]).toEqual({ + assumeRoleArn: 'bloop:here:123456789012', + }); }); - }); - describe('makes correct CloudFormation calls', () => { - test('without options', async () => { + test('fallback to deploy role if forEnvironment throws', async () => { // GIVEN - const toolkit = defaultToolkitSetup(); + // throw error first for the 'prepareSdkWithLookupRoleFor' call and succeed for the rest + mockForEnvironment = jest.fn().mockImplementationOnce(() => { throw new Error('error'); }) + .mockImplementation(() => { return { sdk: mockCloudExecutable.sdkProvider.sdk, didAssumeRole: true }; }); + mockCloudExecutable.sdkProvider.forEnvironment = mockForEnvironment; + mockCloudExecutable.sdkProvider.stubSSM({ + getParameter() { + return {}; + }, + }); + const cdkToolkit = new CdkToolkit({ + cloudExecutable: mockCloudExecutable, + configuration: mockCloudExecutable.configuration, + sdkProvider: mockCloudExecutable.sdkProvider, + deployments: new Deployments({ sdkProvider: mockCloudExecutable.sdkProvider }), + }); // WHEN - await toolkit.deploy({ - selector: { patterns: ['Test-Stack-A', 'Test-Stack-B'] }, + await cdkToolkit.deploy({ + selector: { patterns: ['Test-Stack-C'] }, hotswap: HotswapMode.FULL_DEPLOYMENT, }); + + // THEN + expect(mockCloudExecutable.sdkProvider.sdk.ssm).not.toHaveBeenCalled(); + expect(flatten(stderrMock.mock.calls)).toEqual(expect.arrayContaining([ + expect.stringMatching(/Could not assume bloop-lookup:here:123456789012/), + ])); + expect(mockForEnvironment.mock.calls.length).toEqual(3); + expect(mockForEnvironment.mock.calls[0][2]).toEqual({ + assumeRoleArn: 'bloop-lookup:here:123456789012', + }); + expect(mockForEnvironment.mock.calls[1][2]).toEqual({ + assumeRoleArn: 'bloop:here:123456789012', + }); }); - test('with stacks all stacks specified as double wildcard', async () => { + test('dont lookup bootstrap version parameter if default credentials are used', async () => { // GIVEN - const toolkit = defaultToolkitSetup(); + mockForEnvironment = jest.fn().mockImplementation(() => { return { sdk: mockCloudExecutable.sdkProvider.sdk, didAssumeRole: false }; }); + mockCloudExecutable.sdkProvider.forEnvironment = mockForEnvironment; + const cdkToolkit = new CdkToolkit({ + cloudExecutable: mockCloudExecutable, + configuration: mockCloudExecutable.configuration, + sdkProvider: mockCloudExecutable.sdkProvider, + deployments: new Deployments({ sdkProvider: mockCloudExecutable.sdkProvider }), + }); + mockCloudExecutable.sdkProvider.stubSSM({ + getParameter() { + return {}; + }, + }); // WHEN - await toolkit.deploy({ - selector: { patterns: ['**'] }, + await cdkToolkit.deploy({ + selector: { patterns: ['Test-Stack-C'] }, hotswap: HotswapMode.FULL_DEPLOYMENT, }); + + // THEN + expect(flatten(stderrMock.mock.calls)).toEqual(expect.arrayContaining([ + expect.stringMatching(/Lookup role exists but was not assumed. Proceeding with default credentials./), + ])); + expect(mockCloudExecutable.sdkProvider.sdk.ssm).not.toHaveBeenCalled(); + expect(mockForEnvironment.mock.calls.length).toEqual(3); + expect(mockForEnvironment.mock.calls[0][2]).toEqual({ + assumeRoleArn: 'bloop-lookup:here:123456789012', + }); + expect(mockForEnvironment.mock.calls[1][2]).toEqual({ + assumeRoleArn: 'bloop:here:123456789012', + }); }); - test('with one stack specified', async () => { + test('do not print warnings if lookup role not provided in stack artifact', async () => { // GIVEN - const toolkit = defaultToolkitSetup(); + mockCloudExecutable.sdkProvider.stubSSM({ + getParameter() { + return {}; + }, + }); + const cdkToolkit = new CdkToolkit({ + cloudExecutable: mockCloudExecutable, + configuration: mockCloudExecutable.configuration, + sdkProvider: mockCloudExecutable.sdkProvider, + deployments: new Deployments({ sdkProvider: mockCloudExecutable.sdkProvider }), + }); // WHEN - await toolkit.deploy({ - selector: { patterns: ['Test-Stack-A-Display-Name'] }, + await cdkToolkit.deploy({ + selector: { patterns: ['Test-Stack-A'] }, hotswap: HotswapMode.FULL_DEPLOYMENT, }); + + // THEN + expect(flatten(stderrMock.mock.calls)).not.toEqual(expect.arrayContaining([ + expect.stringMatching(/Could not assume/), + expect.stringMatching(/please upgrade to bootstrap version/), + ])); + expect(mockCloudExecutable.sdkProvider.sdk.ssm).not.toHaveBeenCalled(); + expect(mockForEnvironment.mock.calls.length).toEqual(2); + expect(mockForEnvironment.mock.calls[0][2]).toEqual({ + assumeRoleArn: undefined, + assumeRoleExternalId: undefined, + }); }); + }); - test('with stacks all stacks specified as wildcard', async () => { + describe('deploy', () => { + test('fails when no valid stack names are given', async () => { // GIVEN const toolkit = defaultToolkitSetup(); // WHEN - await toolkit.deploy({ - selector: { patterns: ['*'] }, + await expect(() => toolkit.deploy({ + selector: { patterns: ['Test-Stack-D'] }, hotswap: HotswapMode.FULL_DEPLOYMENT, - }); + })).rejects.toThrow('No stacks match the name(s) Test-Stack-D'); }); - describe('sns notification arns', () => { - beforeEach(() => { - cloudExecutable = new MockCloudExecutable({ - stacks: [ - MockStack.MOCK_STACK_E, - MockStack.MOCK_STACK_F, - MockStack.MOCK_STACK_WITH_NOTIFICATION_ARNS, - MockStack.MOCK_STACK_WITH_BAD_NOTIFICATION_ARNS, - ], - }); - }); - - test('with sns notification arns as options', async () => { + describe('with hotswap deployment', () => { + test("passes through the 'hotswap' option to CloudFormationDeployments.deployStack()", async () => { // GIVEN - const notificationArns = [ - 'arn:aws:sns:us-east-2:444455556666:MyTopic', - 'arn:aws:sns:eu-west-1:111155556666:my-great-topic', - ]; - const toolkit = new CdkToolkit({ + const mockCfnDeployments = instanceMockFrom(Deployments); + mockCfnDeployments.deployStack.mockReturnValue(Promise.resolve({ + noOp: false, + outputs: {}, + stackArn: 'stackArn', + stackArtifact: instanceMockFrom(cxapi.CloudFormationStackArtifact), + })); + const cdkToolkit = new CdkToolkit({ cloudExecutable, configuration: cloudExecutable.configuration, sdkProvider: cloudExecutable.sdkProvider, - deployments: new FakeCloudFormation({ - 'Test-Stack-E': { Foo: 'Bar' }, - 'Test-Stack-F': { Foo: 'Bar' }, - }, notificationArns), + deployments: mockCfnDeployments, }); // WHEN - await toolkit.deploy({ - selector: { patterns: ['Test-Stack-E', 'Test-Stack-F'] }, - notificationArns, - hotswap: HotswapMode.FULL_DEPLOYMENT, + await cdkToolkit.deploy({ + selector: { patterns: ['Test-Stack-A-Display-Name'] }, + requireApproval: RequireApproval.Never, + hotswap: HotswapMode.FALL_BACK, }); + + // THEN + expect(mockCfnDeployments.deployStack).toHaveBeenCalledWith(expect.objectContaining({ + hotswap: HotswapMode.FALL_BACK, + })); }); + }); - test('fail with incorrect sns notification arns as options', async () => { + describe('makes correct CloudFormation calls', () => { + test('without options', async () => { // GIVEN - const notificationArns = ['arn:::cfn-my-cool-topic']; - const toolkit = new CdkToolkit({ - cloudExecutable, - configuration: cloudExecutable.configuration, - sdkProvider: cloudExecutable.sdkProvider, - deployments: new FakeCloudFormation({ - 'Test-Stack-E': { Foo: 'Bar' }, - }, notificationArns), - }); + const toolkit = defaultToolkitSetup(); // WHEN - await expect(() => - toolkit.deploy({ - selector: { patterns: ['Test-Stack-E'] }, - notificationArns, - hotswap: HotswapMode.FULL_DEPLOYMENT, - }), - ).rejects.toThrow('Notification arn arn:::cfn-my-cool-topic is not a valid arn for an SNS topic'); + await toolkit.deploy({ + selector: { patterns: ['Test-Stack-A', 'Test-Stack-B'] }, + hotswap: HotswapMode.FULL_DEPLOYMENT, + }); }); - test('with sns notification arns in the executable', async () => { + test('with stacks all stacks specified as double wildcard', async () => { // GIVEN - const expectedNotificationArns = [ - 'arn:aws:sns:bermuda-triangle-1337:123456789012:MyTopic', - ]; - const toolkit = new CdkToolkit({ - cloudExecutable, - configuration: cloudExecutable.configuration, - sdkProvider: cloudExecutable.sdkProvider, - deployments: new FakeCloudFormation({ - 'Test-Stack-Notification-Arns': { Foo: 'Bar' }, - }, expectedNotificationArns), - }); + const toolkit = defaultToolkitSetup(); // WHEN await toolkit.deploy({ - selector: { patterns: ['Test-Stack-Notification-Arns'] }, + selector: { patterns: ['**'] }, hotswap: HotswapMode.FULL_DEPLOYMENT, }); }); - test('fail with incorrect sns notification arns in the executable', async () => { + test('with one stack specified', async () => { // GIVEN - const toolkit = new CdkToolkit({ - cloudExecutable, - configuration: cloudExecutable.configuration, - sdkProvider: cloudExecutable.sdkProvider, - deployments: new FakeCloudFormation({ - 'Test-Stack-Bad-Notification-Arns': { Foo: 'Bar' }, - }), - }); + const toolkit = defaultToolkitSetup(); // WHEN - await expect(() => - toolkit.deploy({ - selector: { patterns: ['Test-Stack-Bad-Notification-Arns'] }, - hotswap: HotswapMode.FULL_DEPLOYMENT, - }), - ).rejects.toThrow('Notification arn arn:1337:123456789012:sns:bad is not a valid arn for an SNS topic'); + await toolkit.deploy({ + selector: { patterns: ['Test-Stack-A-Display-Name'] }, + hotswap: HotswapMode.FULL_DEPLOYMENT, + }); }); - test('with sns notification arns in the executable and as options', async () => { + test('with stacks all stacks specified as wildcard', async () => { // GIVEN - const notificationArns = [ - 'arn:aws:sns:us-east-2:444455556666:MyTopic', - 'arn:aws:sns:eu-west-1:111155556666:my-great-topic', - ]; - - const expectedNotificationArns = notificationArns.concat(['arn:aws:sns:bermuda-triangle-1337:123456789012:MyTopic']); - const toolkit = new CdkToolkit({ - cloudExecutable, - configuration: cloudExecutable.configuration, - sdkProvider: cloudExecutable.sdkProvider, - deployments: new FakeCloudFormation({ - 'Test-Stack-Notification-Arns': { Foo: 'Bar' }, - }, expectedNotificationArns), - }); + const toolkit = defaultToolkitSetup(); // WHEN await toolkit.deploy({ - selector: { patterns: ['Test-Stack-Notification-Arns'] }, - notificationArns, + selector: { patterns: ['*'] }, hotswap: HotswapMode.FULL_DEPLOYMENT, }); }); - test('fail with incorrect sns notification arns in the executable and incorrect sns notification arns as options', async () => { - // GIVEN - const notificationArns = ['arn:::cfn-my-cool-topic']; - const toolkit = new CdkToolkit({ - cloudExecutable, - configuration: cloudExecutable.configuration, - sdkProvider: cloudExecutable.sdkProvider, - deployments: new FakeCloudFormation({ - 'Test-Stack-Bad-Notification-Arns': { Foo: 'Bar' }, - }, notificationArns), + describe('sns notification arns', () => { + beforeEach(() => { + cloudExecutable = new MockCloudExecutable({ + stacks: [ + MockStack.MOCK_STACK_E, + MockStack.MOCK_STACK_F, + MockStack.MOCK_STACK_WITH_NOTIFICATION_ARNS, + MockStack.MOCK_STACK_WITH_BAD_NOTIFICATION_ARNS, + ], + }); }); - // WHEN - await expect(() => - toolkit.deploy({ - selector: { patterns: ['Test-Stack-Bad-Notification-Arns'] }, + test('with sns notification arns as options', async () => { + // GIVEN + const notificationArns = [ + 'arn:aws:sns:us-east-2:444455556666:MyTopic', + 'arn:aws:sns:eu-west-1:111155556666:my-great-topic', + ]; + const toolkit = new CdkToolkit({ + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: new FakeCloudFormation({ + 'Test-Stack-E': { Foo: 'Bar' }, + 'Test-Stack-F': { Foo: 'Bar' }, + }, notificationArns), + }); + + // WHEN + await toolkit.deploy({ + selector: { patterns: ['Test-Stack-E', 'Test-Stack-F'] }, notificationArns, hotswap: HotswapMode.FULL_DEPLOYMENT, - }), - ).rejects.toThrow('Notification arn arn:::cfn-my-cool-topic is not a valid arn for an SNS topic'); - }); + }); + }); - test('fail with incorrect sns notification arns in the executable and correct sns notification arns as options', async () => { - // GIVEN - const notificationArns = ['arn:aws:sns:bermuda-triangle-1337:123456789012:MyTopic']; - const toolkit = new CdkToolkit({ - cloudExecutable, - configuration: cloudExecutable.configuration, - sdkProvider: cloudExecutable.sdkProvider, - deployments: new FakeCloudFormation({ - 'Test-Stack-Bad-Notification-Arns': { Foo: 'Bar' }, - }, notificationArns), + test('fail with incorrect sns notification arns as options', async () => { + // GIVEN + const notificationArns = ['arn:::cfn-my-cool-topic']; + const toolkit = new CdkToolkit({ + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: new FakeCloudFormation({ + 'Test-Stack-E': { Foo: 'Bar' }, + }, notificationArns), + }); + + // WHEN + await expect(() => + toolkit.deploy({ + selector: { patterns: ['Test-Stack-E'] }, + notificationArns, + hotswap: HotswapMode.FULL_DEPLOYMENT, + }), + ).rejects.toThrow('Notification arn arn:::cfn-my-cool-topic is not a valid arn for an SNS topic'); }); - // WHEN - await expect(() => - toolkit.deploy({ - selector: { patterns: ['Test-Stack-Bad-Notification-Arns'] }, - notificationArns, + test('with sns notification arns in the executable', async () => { + // GIVEN + const expectedNotificationArns = [ + 'arn:aws:sns:bermuda-triangle-1337:123456789012:MyTopic', + ]; + const toolkit = new CdkToolkit({ + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: new FakeCloudFormation({ + 'Test-Stack-Notification-Arns': { Foo: 'Bar' }, + }, expectedNotificationArns), + }); + + // WHEN + await toolkit.deploy({ + selector: { patterns: ['Test-Stack-Notification-Arns'] }, hotswap: HotswapMode.FULL_DEPLOYMENT, - }), - ).rejects.toThrow('Notification arn arn:1337:123456789012:sns:bad is not a valid arn for an SNS topic'); - }); + }); + }); - test('fail with correct sns notification arns in the executable and incorrect sns notification arns as options', async () => { - // GIVEN - const notificationArns = ['arn:::cfn-my-cool-topic']; - const toolkit = new CdkToolkit({ - cloudExecutable, - configuration: cloudExecutable.configuration, - sdkProvider: cloudExecutable.sdkProvider, - deployments: new FakeCloudFormation({ - 'Test-Stack-Notification-Arns': { Foo: 'Bar' }, - }, notificationArns), + test('fail with incorrect sns notification arns in the executable', async () => { + // GIVEN + const toolkit = new CdkToolkit({ + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: new FakeCloudFormation({ + 'Test-Stack-Bad-Notification-Arns': { Foo: 'Bar' }, + }), + }); + + // WHEN + await expect(() => + toolkit.deploy({ + selector: { patterns: ['Test-Stack-Bad-Notification-Arns'] }, + hotswap: HotswapMode.FULL_DEPLOYMENT, + }), + ).rejects.toThrow('Notification arn arn:1337:123456789012:sns:bad is not a valid arn for an SNS topic'); }); - // WHEN - await expect(() => - toolkit.deploy({ + test('with sns notification arns in the executable and as options', async () => { + // GIVEN + const notificationArns = [ + 'arn:aws:sns:us-east-2:444455556666:MyTopic', + 'arn:aws:sns:eu-west-1:111155556666:my-great-topic', + ]; + + const expectedNotificationArns = notificationArns.concat(['arn:aws:sns:bermuda-triangle-1337:123456789012:MyTopic']); + const toolkit = new CdkToolkit({ + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: new FakeCloudFormation({ + 'Test-Stack-Notification-Arns': { Foo: 'Bar' }, + }, expectedNotificationArns), + }); + + // WHEN + await toolkit.deploy({ selector: { patterns: ['Test-Stack-Notification-Arns'] }, notificationArns, hotswap: HotswapMode.FULL_DEPLOYMENT, - }), - ).rejects.toThrow('Notification arn arn:::cfn-my-cool-topic is not a valid arn for an SNS topic'); + }); + }); + + test('fail with incorrect sns notification arns in the executable and incorrect sns notification arns as options', async () => { + // GIVEN + const notificationArns = ['arn:::cfn-my-cool-topic']; + const toolkit = new CdkToolkit({ + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: new FakeCloudFormation({ + 'Test-Stack-Bad-Notification-Arns': { Foo: 'Bar' }, + }, notificationArns), + }); + + // WHEN + await expect(() => + toolkit.deploy({ + selector: { patterns: ['Test-Stack-Bad-Notification-Arns'] }, + notificationArns, + hotswap: HotswapMode.FULL_DEPLOYMENT, + }), + ).rejects.toThrow('Notification arn arn:::cfn-my-cool-topic is not a valid arn for an SNS topic'); + }); + + test('fail with incorrect sns notification arns in the executable and correct sns notification arns as options', async () => { + // GIVEN + const notificationArns = ['arn:aws:sns:bermuda-triangle-1337:123456789012:MyTopic']; + const toolkit = new CdkToolkit({ + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: new FakeCloudFormation({ + 'Test-Stack-Bad-Notification-Arns': { Foo: 'Bar' }, + }, notificationArns), + }); + + // WHEN + await expect(() => + toolkit.deploy({ + selector: { patterns: ['Test-Stack-Bad-Notification-Arns'] }, + notificationArns, + hotswap: HotswapMode.FULL_DEPLOYMENT, + }), + ).rejects.toThrow('Notification arn arn:1337:123456789012:sns:bad is not a valid arn for an SNS topic'); + }); + + test('fail with correct sns notification arns in the executable and incorrect sns notification arns as options', async () => { + // GIVEN + const notificationArns = ['arn:::cfn-my-cool-topic']; + const toolkit = new CdkToolkit({ + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: new FakeCloudFormation({ + 'Test-Stack-Notification-Arns': { Foo: 'Bar' }, + }, notificationArns), + }); + + // WHEN + await expect(() => + toolkit.deploy({ + selector: { patterns: ['Test-Stack-Notification-Arns'] }, + notificationArns, + hotswap: HotswapMode.FULL_DEPLOYMENT, + }), + ).rejects.toThrow('Notification arn arn:::cfn-my-cool-topic is not a valid arn for an SNS topic'); + }); }); }); - }); - test('globless bootstrap uses environment without question', async () => { - // GIVEN - const toolkit = defaultToolkitSetup(); + test('globless bootstrap uses environment without question', async () => { + // GIVEN + const toolkit = defaultToolkitSetup(); - // WHEN - await toolkit.bootstrap(['aws://56789/south-pole'], bootstrapper, {}); + // WHEN + await toolkit.bootstrap(['aws://56789/south-pole'], bootstrapper, {}); - // THEN - expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledWith({ - account: '56789', - region: 'south-pole', - name: 'aws://56789/south-pole', - }, expect.anything(), expect.anything()); - expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledTimes(1); - }); + // THEN + expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledWith({ + account: '56789', + region: 'south-pole', + name: 'aws://56789/south-pole', + }, expect.anything(), expect.anything()); + expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledTimes(1); + }); - test('globby bootstrap uses whats in the stacks', async () => { - // GIVEN - const toolkit = defaultToolkitSetup(); - cloudExecutable.configuration.settings.set(['app'], 'something'); - - // WHEN - await toolkit.bootstrap(['aws://*/bermuda-triangle-1'], bootstrapper, {}); - - // THEN - expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledWith({ - account: '123456789012', - region: 'bermuda-triangle-1', - name: 'aws://123456789012/bermuda-triangle-1', - }, expect.anything(), expect.anything()); - expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledTimes(1); - }); + test('globby bootstrap uses whats in the stacks', async () => { + // GIVEN + const toolkit = defaultToolkitSetup(); + cloudExecutable.configuration.settings.set(['app'], 'something'); - test('bootstrap can be invoked without the --app argument', async () => { - // GIVEN - cloudExecutable.configuration.settings.clear(); - const mockSynthesize = jest.fn(); - cloudExecutable.synthesize = mockSynthesize; + // WHEN + await toolkit.bootstrap(['aws://*/bermuda-triangle-1'], bootstrapper, {}); - const toolkit = defaultToolkitSetup(); + // THEN + expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledWith({ + account: '123456789012', + region: 'bermuda-triangle-1', + name: 'aws://123456789012/bermuda-triangle-1', + }, expect.anything(), expect.anything()); + expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledTimes(1); + }); - // WHEN - await toolkit.bootstrap(['aws://123456789012/west-pole'], bootstrapper, {}); + test('bootstrap can be invoked without the --app argument', async () => { + // GIVEN + cloudExecutable.configuration.settings.clear(); + const mockSynthesize = jest.fn(); + cloudExecutable.synthesize = mockSynthesize; - // THEN - expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledWith({ - account: '123456789012', - region: 'west-pole', - name: 'aws://123456789012/west-pole', - }, expect.anything(), expect.anything()); - expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledTimes(1); + const toolkit = defaultToolkitSetup(); - expect(cloudExecutable.hasApp).toEqual(false); - expect(mockSynthesize).not.toHaveBeenCalled(); - }); -}); + // WHEN + await toolkit.bootstrap(['aws://123456789012/west-pole'], bootstrapper, {}); -describe('destroy', () => { - test('destroy correct stack', async () => { - const toolkit = destroyToolkitSetup(); + // THEN + expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledWith({ + account: '123456789012', + region: 'west-pole', + name: 'aws://123456789012/west-pole', + }, expect.anything(), expect.anything()); + expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledTimes(1); - expect(() => { - return toolkit.destroy({ - selector: { patterns: ['Test-Stack-Destroy'] }, - exclusively: true, - force: true, - fromDeploy: true, - }); - }).resolves; + expect(cloudExecutable.hasApp).toEqual(false); + expect(mockSynthesize).not.toHaveBeenCalled(); + }); }); -}); -describe('watch', () => { - test("fails when no 'watch' settings are found", async () => { - const toolkit = defaultToolkitSetup(); + describe('destroy', () => { + test('destroy correct stack', async () => { + const toolkit = destroyToolkitSetup(); - await expect(() => { - return toolkit.watch({ - selector: { patterns: [] }, - hotswap: HotswapMode.HOTSWAP_ONLY, - }); - }).rejects.toThrow("Cannot use the 'watch' command without specifying at least one directory to monitor. " + - 'Make sure to add a "watch" key to your cdk.json'); + expect(() => { + return toolkit.destroy({ + selector: { patterns: ['Test-Stack-Destroy'] }, + exclusively: true, + force: true, + fromDeploy: true, + }); + }).resolves; + }); }); - test('observes only the root directory by default', async () => { - cloudExecutable.configuration.settings.set(['watch'], {}); - const toolkit = defaultToolkitSetup(); + describe('watch', () => { + test("fails when no 'watch' settings are found", async () => { + const toolkit = defaultToolkitSetup(); - await toolkit.watch({ - selector: { patterns: [] }, - hotswap: HotswapMode.HOTSWAP_ONLY, + await expect(() => { + return toolkit.watch({ + selector: { patterns: [] }, + hotswap: HotswapMode.HOTSWAP_ONLY, + }); + }).rejects.toThrow("Cannot use the 'watch' command without specifying at least one directory to monitor. " + + 'Make sure to add a "watch" key to your cdk.json'); }); - const includeArgs = fakeChokidarWatch.includeArgs; - expect(includeArgs.length).toBe(1); - }); + test('observes only the root directory by default', async () => { + cloudExecutable.configuration.settings.set(['watch'], {}); + const toolkit = defaultToolkitSetup(); - test("allows providing a single string in 'watch.include'", async () => { - cloudExecutable.configuration.settings.set(['watch'], { - include: 'my-dir', - }); - const toolkit = defaultToolkitSetup(); + await toolkit.watch({ + selector: { patterns: [] }, + hotswap: HotswapMode.HOTSWAP_ONLY, + }); - await toolkit.watch({ - selector: { patterns: [] }, - hotswap: HotswapMode.HOTSWAP_ONLY, + const includeArgs = fakeChokidarWatch.includeArgs; + expect(includeArgs.length).toBe(1); }); - expect(fakeChokidarWatch.includeArgs).toStrictEqual(['my-dir']); - }); + test("allows providing a single string in 'watch.include'", async () => { + cloudExecutable.configuration.settings.set(['watch'], { + include: 'my-dir', + }); + const toolkit = defaultToolkitSetup(); - test("allows providing an array of strings in 'watch.include'", async () => { - cloudExecutable.configuration.settings.set(['watch'], { - include: ['my-dir1', '**/my-dir2/*'], - }); - const toolkit = defaultToolkitSetup(); + await toolkit.watch({ + selector: { patterns: [] }, + hotswap: HotswapMode.HOTSWAP_ONLY, + }); - await toolkit.watch({ - selector: { patterns: [] }, - hotswap: HotswapMode.HOTSWAP_ONLY, + expect(fakeChokidarWatch.includeArgs).toStrictEqual(['my-dir']); }); - expect(fakeChokidarWatch.includeArgs).toStrictEqual(['my-dir1', '**/my-dir2/*']); - }); + test("allows providing an array of strings in 'watch.include'", async () => { + cloudExecutable.configuration.settings.set(['watch'], { + include: ['my-dir1', '**/my-dir2/*'], + }); + const toolkit = defaultToolkitSetup(); - test('ignores the output dir, dot files, dot directories, and node_modules by default', async () => { - cloudExecutable.configuration.settings.set(['watch'], {}); - cloudExecutable.configuration.settings.set(['output'], 'cdk.out'); - const toolkit = defaultToolkitSetup(); + await toolkit.watch({ + selector: { patterns: [] }, + hotswap: HotswapMode.HOTSWAP_ONLY, + }); - await toolkit.watch({ - selector: { patterns: [] }, - hotswap: HotswapMode.HOTSWAP_ONLY, + expect(fakeChokidarWatch.includeArgs).toStrictEqual(['my-dir1', '**/my-dir2/*']); }); - expect(fakeChokidarWatch.excludeArgs).toStrictEqual([ - 'cdk.out/**', - '**/.*', - '**/.*/**', - '**/node_modules/**', - ]); - }); + test('ignores the output dir, dot files, dot directories, and node_modules by default', async () => { + cloudExecutable.configuration.settings.set(['watch'], {}); + cloudExecutable.configuration.settings.set(['output'], 'cdk.out'); + const toolkit = defaultToolkitSetup(); - test("allows providing a single string in 'watch.exclude'", async () => { - cloudExecutable.configuration.settings.set(['watch'], { - exclude: 'my-dir', - }); - const toolkit = defaultToolkitSetup(); + await toolkit.watch({ + selector: { patterns: [] }, + hotswap: HotswapMode.HOTSWAP_ONLY, + }); - await toolkit.watch({ - selector: { patterns: [] }, - hotswap: HotswapMode.HOTSWAP_ONLY, + expect(fakeChokidarWatch.excludeArgs).toStrictEqual([ + 'cdk.out/**', + '**/.*', + '**/.*/**', + '**/node_modules/**', + ]); }); - const excludeArgs = fakeChokidarWatch.excludeArgs; - expect(excludeArgs.length).toBe(5); - expect(excludeArgs[0]).toBe('my-dir'); - }); + test("allows providing a single string in 'watch.exclude'", async () => { + cloudExecutable.configuration.settings.set(['watch'], { + exclude: 'my-dir', + }); + const toolkit = defaultToolkitSetup(); - test("allows providing an array of strings in 'watch.exclude'", async () => { - cloudExecutable.configuration.settings.set(['watch'], { - exclude: ['my-dir1', '**/my-dir2'], - }); - const toolkit = defaultToolkitSetup(); + await toolkit.watch({ + selector: { patterns: [] }, + hotswap: HotswapMode.HOTSWAP_ONLY, + }); - await toolkit.watch({ - selector: { patterns: [] }, - hotswap: HotswapMode.HOTSWAP_ONLY, + const excludeArgs = fakeChokidarWatch.excludeArgs; + expect(excludeArgs.length).toBe(5); + expect(excludeArgs[0]).toBe('my-dir'); }); - const excludeArgs = fakeChokidarWatch.excludeArgs; - expect(excludeArgs.length).toBe(6); - expect(excludeArgs[0]).toBe('my-dir1'); - expect(excludeArgs[1]).toBe('**/my-dir2'); - }); + test("allows providing an array of strings in 'watch.exclude'", async () => { + cloudExecutable.configuration.settings.set(['watch'], { + exclude: ['my-dir1', '**/my-dir2'], + }); + const toolkit = defaultToolkitSetup(); - test('allows watching with deploy concurrency', async () => { - cloudExecutable.configuration.settings.set(['watch'], {}); - const toolkit = defaultToolkitSetup(); - const cdkDeployMock = jest.fn(); - toolkit.deploy = cdkDeployMock; + await toolkit.watch({ + selector: { patterns: [] }, + hotswap: HotswapMode.HOTSWAP_ONLY, + }); - await toolkit.watch({ - selector: { patterns: [] }, - concurrency: 3, - hotswap: HotswapMode.HOTSWAP_ONLY, + const excludeArgs = fakeChokidarWatch.excludeArgs; + expect(excludeArgs.length).toBe(6); + expect(excludeArgs[0]).toBe('my-dir1'); + expect(excludeArgs[1]).toBe('**/my-dir2'); }); - fakeChokidarWatcherOn.readyCallback(); - expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ concurrency: 3 })); - }); - - describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hotswapMode) => { - test('passes through the correct hotswap mode to deployStack()', async () => { + test('allows watching with deploy concurrency', async () => { cloudExecutable.configuration.settings.set(['watch'], {}); const toolkit = defaultToolkitSetup(); const cdkDeployMock = jest.fn(); toolkit.deploy = cdkDeployMock; - await toolkit.watch({ selector: { patterns: [] }, hotswap: hotswapMode }); + await toolkit.watch({ + selector: { patterns: [] }, + concurrency: 3, + hotswap: HotswapMode.HOTSWAP_ONLY, + }); fakeChokidarWatcherOn.readyCallback(); - expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ hotswap: hotswapMode })); + expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ concurrency: 3 })); }); - }); - - test('respects HotswapMode.HOTSWAP_ONLY', async () => { - cloudExecutable.configuration.settings.set(['watch'], {}); - const toolkit = defaultToolkitSetup(); - const cdkDeployMock = jest.fn(); - toolkit.deploy = cdkDeployMock; - await toolkit.watch({ selector: { patterns: [] }, hotswap: HotswapMode.HOTSWAP_ONLY }); - fakeChokidarWatcherOn.readyCallback(); + describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hotswapMode) => { + test('passes through the correct hotswap mode to deployStack()', async () => { + cloudExecutable.configuration.settings.set(['watch'], {}); + const toolkit = defaultToolkitSetup(); + const cdkDeployMock = jest.fn(); + toolkit.deploy = cdkDeployMock; - expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ hotswap: HotswapMode.HOTSWAP_ONLY })); - }); + await toolkit.watch({ selector: { patterns: [] }, hotswap: hotswapMode }); + fakeChokidarWatcherOn.readyCallback(); - test('respects HotswapMode.FALL_BACK', async () => { - cloudExecutable.configuration.settings.set(['watch'], {}); - const toolkit = defaultToolkitSetup(); - const cdkDeployMock = jest.fn(); - toolkit.deploy = cdkDeployMock; + expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ hotswap: hotswapMode })); + }); + }); - await toolkit.watch({ selector: { patterns: [] }, hotswap: HotswapMode.FALL_BACK }); - fakeChokidarWatcherOn.readyCallback(); + test('respects HotswapMode.HOTSWAP_ONLY', async () => { + cloudExecutable.configuration.settings.set(['watch'], {}); + const toolkit = defaultToolkitSetup(); + const cdkDeployMock = jest.fn(); + toolkit.deploy = cdkDeployMock; - expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ hotswap: HotswapMode.FALL_BACK })); - }); + await toolkit.watch({ selector: { patterns: [] }, hotswap: HotswapMode.HOTSWAP_ONLY }); + fakeChokidarWatcherOn.readyCallback(); - test('respects HotswapMode.FULL_DEPLOYMENT', async () => { - cloudExecutable.configuration.settings.set(['watch'], {}); - const toolkit = defaultToolkitSetup(); - const cdkDeployMock = jest.fn(); - toolkit.deploy = cdkDeployMock; + expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ hotswap: HotswapMode.HOTSWAP_ONLY })); + }); - await toolkit.watch({ selector: { patterns: [] }, hotswap: HotswapMode.FULL_DEPLOYMENT }); - fakeChokidarWatcherOn.readyCallback(); + test('respects HotswapMode.FALL_BACK', async () => { + cloudExecutable.configuration.settings.set(['watch'], {}); + const toolkit = defaultToolkitSetup(); + const cdkDeployMock = jest.fn(); + toolkit.deploy = cdkDeployMock; - expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ hotswap: HotswapMode.FULL_DEPLOYMENT })); - }); + await toolkit.watch({ selector: { patterns: [] }, hotswap: HotswapMode.FALL_BACK }); + fakeChokidarWatcherOn.readyCallback(); - describe('with file change events', () => { - let toolkit: CdkToolkit; - let cdkDeployMock: jest.Mock; + expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ hotswap: HotswapMode.FALL_BACK })); + }); - beforeEach(async () => { + test('respects HotswapMode.FULL_DEPLOYMENT', async () => { cloudExecutable.configuration.settings.set(['watch'], {}); - toolkit = defaultToolkitSetup(); - cdkDeployMock = jest.fn(); + const toolkit = defaultToolkitSetup(); + const cdkDeployMock = jest.fn(); toolkit.deploy = cdkDeployMock; - await toolkit.watch({ - selector: { patterns: [] }, - hotswap: HotswapMode.HOTSWAP_ONLY, - }); - }); - test("does not trigger a 'deploy' before the 'ready' event has fired", async () => { - await fakeChokidarWatcherOn.fileEventCallback('add', 'my-file'); + await toolkit.watch({ selector: { patterns: [] }, hotswap: HotswapMode.FULL_DEPLOYMENT }); + fakeChokidarWatcherOn.readyCallback(); - expect(cdkDeployMock).not.toHaveBeenCalled(); + expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ hotswap: HotswapMode.FULL_DEPLOYMENT })); }); - describe("when the 'ready' event has already fired", () => { - beforeEach(() => { - // The ready callback triggers a deployment so each test - // that uses this function will see 'cdkDeployMock' called - // an additional time. - fakeChokidarWatcherOn.readyCallback(); - }); + describe('with file change events', () => { + let toolkit: CdkToolkit; + let cdkDeployMock: jest.Mock; - test("an initial 'deploy' is triggered, without any file changes", async () => { - expect(cdkDeployMock).toHaveBeenCalledTimes(1); + beforeEach(async () => { + cloudExecutable.configuration.settings.set(['watch'], {}); + toolkit = defaultToolkitSetup(); + cdkDeployMock = jest.fn(); + toolkit.deploy = cdkDeployMock; + await toolkit.watch({ + selector: { patterns: [] }, + hotswap: HotswapMode.HOTSWAP_ONLY, + }); }); - test("does trigger a 'deploy' for a file change", async () => { + test("does not trigger a 'deploy' before the 'ready' event has fired", async () => { await fakeChokidarWatcherOn.fileEventCallback('add', 'my-file'); - expect(cdkDeployMock).toHaveBeenCalledTimes(2); + expect(cdkDeployMock).not.toHaveBeenCalled(); }); - test("triggers a 'deploy' twice for two file changes", async () => { - await Promise.all([ - fakeChokidarWatcherOn.fileEventCallback('add', 'my-file1'), - fakeChokidarWatcherOn.fileEventCallback('change', 'my-file2'), - ]); + describe("when the 'ready' event has already fired", () => { + beforeEach(() => { + // The ready callback triggers a deployment so each test + // that uses this function will see 'cdkDeployMock' called + // an additional time. + fakeChokidarWatcherOn.readyCallback(); + }); - expect(cdkDeployMock).toHaveBeenCalledTimes(3); - }); + test("an initial 'deploy' is triggered, without any file changes", async () => { + expect(cdkDeployMock).toHaveBeenCalledTimes(1); + }); - test("batches file changes that happen during 'deploy'", async () => { - await Promise.all([ - fakeChokidarWatcherOn.fileEventCallback('add', 'my-file1'), - fakeChokidarWatcherOn.fileEventCallback('change', 'my-file2'), - fakeChokidarWatcherOn.fileEventCallback('unlink', 'my-file3'), - fakeChokidarWatcherOn.fileEventCallback('add', 'my-file4'), - ]); + test("does trigger a 'deploy' for a file change", async () => { + await fakeChokidarWatcherOn.fileEventCallback('add', 'my-file'); - expect(cdkDeployMock).toHaveBeenCalledTimes(3); - }); - }); - }); -}); + expect(cdkDeployMock).toHaveBeenCalledTimes(2); + }); -describe('synth', () => { - test('successful synth outputs hierarchical stack ids', async () => { - const toolkit = defaultToolkitSetup(); - await toolkit.synth([], false, false); + test("triggers a 'deploy' twice for two file changes", async () => { + await Promise.all([ + fakeChokidarWatcherOn.fileEventCallback('add', 'my-file1'), + fakeChokidarWatcherOn.fileEventCallback('change', 'my-file2'), + ]); - // Separate tests as colorizing hampers detection - expect(stderrMock.mock.calls[1][0]).toMatch('Test-Stack-A-Display-Name'); - expect(stderrMock.mock.calls[1][0]).toMatch('Test-Stack-B'); - }); + expect(cdkDeployMock).toHaveBeenCalledTimes(3); + }); - test('with no stdout option', async () => { - // GIVE - const toolkit = defaultToolkitSetup(); + test("batches file changes that happen during 'deploy'", async () => { + await Promise.all([ + fakeChokidarWatcherOn.fileEventCallback('add', 'my-file1'), + fakeChokidarWatcherOn.fileEventCallback('change', 'my-file2'), + fakeChokidarWatcherOn.fileEventCallback('unlink', 'my-file3'), + fakeChokidarWatcherOn.fileEventCallback('add', 'my-file4'), + ]); - // THEN - await toolkit.synth(['Test-Stack-A-Display-Name'], false, true); - expect(mockData.mock.calls.length).toEqual(0); + expect(cdkDeployMock).toHaveBeenCalledTimes(3); + }); + }); + }); }); - describe('migrate', () => { - const testResourcePath = [__dirname, 'commands', 'test-resources']; - const templatePath = [...testResourcePath, 'templates']; - const sqsTemplatePath = path.join(...templatePath, 'sqs-template.json'); - const autoscalingTemplatePath = path.join(...templatePath, 'autoscaling-template.yml'); - const s3TemplatePath = path.join(...templatePath, 's3-template.json'); - - test('migrate fails when both --from-path and --from-stack are provided', async () => { + describe('synth', () => { + test('successful synth outputs hierarchical stack ids', async () => { const toolkit = defaultToolkitSetup(); - await expect(() => toolkit.migrate({ - stackName: 'no-source', - fromPath: './here/template.yml', - fromStack: true, - })).rejects.toThrow('Only one of `--from-path` or `--from-stack` may be provided.'); - expect(stderrMock.mock.calls[1][0]).toContain(' ❌ Migrate failed for `no-source`: Only one of `--from-path` or `--from-stack` may be provided.'); + await toolkit.synth([], false, false); + + // Separate tests as colorizing hampers detection + expect(stderrMock.mock.calls[1][0]).toMatch('Test-Stack-A-Display-Name'); + expect(stderrMock.mock.calls[1][0]).toMatch('Test-Stack-B'); }); - test('migrate fails when --from-path is invalid', async () => { + test('with no stdout option', async () => { + // GIVE const toolkit = defaultToolkitSetup(); - await expect(() => toolkit.migrate({ - stackName: 'bad-local-source', - fromPath: './here/template.yml', - })).rejects.toThrow('\'./here/template.yml\' is not a valid path.'); - expect(stderrMock.mock.calls[1][0]).toContain(' ❌ Migrate failed for `bad-local-source`: \'./here/template.yml\' is not a valid path.'); + + // THEN + await toolkit.synth(['Test-Stack-A-Display-Name'], false, true); + expect(mockData.mock.calls.length).toEqual(0); }); - test('migrate fails when --from-stack is used and stack does not exist in account', async () => { - const mockSdkProvider = new MockSdkProvider(); - mockSdkProvider.stubCloudFormation({ - describeStacks(_request) { - throw new Error('Stack does not exist in this environment'); - }, + describe('migrate', () => { + const testResourcePath = [__dirname, 'commands', 'test-resources']; + const templatePath = [...testResourcePath, 'templates']; + const sqsTemplatePath = path.join(...templatePath, 'sqs-template.json'); + const autoscalingTemplatePath = path.join(...templatePath, 'autoscaling-template.yml'); + const s3TemplatePath = path.join(...templatePath, 's3-template.json'); + + test('migrate fails when both --from-path and --from-stack are provided', async () => { + const toolkit = defaultToolkitSetup(); + await expect(() => toolkit.migrate({ + stackName: 'no-source', + fromPath: './here/template.yml', + fromStack: true, + })).rejects.toThrow('Only one of `--from-path` or `--from-stack` may be provided.'); + expect(stderrMock.mock.calls[1][0]).toContain(' ❌ Migrate failed for `no-source`: Only one of `--from-path` or `--from-stack` may be provided.'); }); - const mockCloudExecutable = new MockCloudExecutable({ - stacks: [], + test('migrate fails when --from-path is invalid', async () => { + const toolkit = defaultToolkitSetup(); + await expect(() => toolkit.migrate({ + stackName: 'bad-local-source', + fromPath: './here/template.yml', + })).rejects.toThrow('\'./here/template.yml\' is not a valid path.'); + expect(stderrMock.mock.calls[1][0]).toContain(' ❌ Migrate failed for `bad-local-source`: \'./here/template.yml\' is not a valid path.'); }); - const cdkToolkit = new CdkToolkit({ - cloudExecutable: mockCloudExecutable, - deployments: new Deployments({ sdkProvider: mockSdkProvider }), - sdkProvider: mockSdkProvider, - configuration: mockCloudExecutable.configuration, + test('migrate fails when --from-stack is used and stack does not exist in account', async () => { + const mockSdkProvider = new MockSdkProvider(); + mockSdkProvider.stubCloudFormation({ + describeStacks(_request) { + throw new Error('Stack does not exist in this environment'); + }, + }); + + const mockCloudExecutable = new MockCloudExecutable({ + stacks: [], + }); + + const cdkToolkit = new CdkToolkit({ + cloudExecutable: mockCloudExecutable, + deployments: new Deployments({ sdkProvider: mockSdkProvider }), + sdkProvider: mockSdkProvider, + configuration: mockCloudExecutable.configuration, + }); + + await expect(() => cdkToolkit.migrate({ + stackName: 'bad-cloudformation-source', + fromStack: true, + })).rejects.toThrowError('Stack does not exist in this environment'); + expect(stderrMock.mock.calls[1][0]).toContain(' ❌ Migrate failed for `bad-cloudformation-source`: Stack does not exist in this environment'); }); - await expect(() => cdkToolkit.migrate({ - stackName: 'bad-cloudformation-source', - fromStack: true, - })).rejects.toThrowError('Stack does not exist in this environment'); - expect(stderrMock.mock.calls[1][0]).toContain(' ❌ Migrate failed for `bad-cloudformation-source`: Stack does not exist in this environment'); - }); + test('migrate fails when stack cannot be generated', async () => { + const toolkit = defaultToolkitSetup(); + await expect(() => toolkit.migrate({ + stackName: 'cannot-generate-template', + fromPath: path.join(__dirname, 'commands', 'test-resources', 'templates', 'sqs-template.json'), + language: 'rust', + })).rejects.toThrowError('CannotGenerateTemplateStack could not be generated because rust is not a supported language'); + expect(stderrMock.mock.calls[1][0]).toContain(' ❌ Migrate failed for `cannot-generate-template`: CannotGenerateTemplateStack could not be generated because rust is not a supported language'); + }); - test('migrate fails when stack cannot be generated', async () => { - const toolkit = defaultToolkitSetup(); - await expect(() => toolkit.migrate({ - stackName: 'cannot-generate-template', - fromPath: path.join(__dirname, 'commands', 'test-resources', 'templates', 'sqs-template.json'), - language: 'rust', - })).rejects.toThrowError('CannotGenerateTemplateStack could not be generated because rust is not a supported language'); - expect(stderrMock.mock.calls[1][0]).toContain(' ❌ Migrate failed for `cannot-generate-template`: CannotGenerateTemplateStack could not be generated because rust is not a supported language'); - }); + cliTest('migrate succeeds for valid template from local path when no lanugage is provided', async (workDir) => { + const toolkit = defaultToolkitSetup(); + await toolkit.migrate({ + stackName: 'SQSTypeScript', + fromPath: sqsTemplatePath, + outputPath: workDir, + }); - cliTest('migrate succeeds for valid template from local path when no lanugage is provided', async (workDir) => { - const toolkit = defaultToolkitSetup(); - await toolkit.migrate({ - stackName: 'SQSTypeScript', - fromPath: sqsTemplatePath, - outputPath: workDir, + // Packages created for typescript + expect(fs.pathExistsSync(path.join(workDir, 'SQSTypeScript', 'package.json'))).toBeTruthy(); + expect(fs.pathExistsSync(path.join(workDir, 'SQSTypeScript', 'bin', 'sqs_type_script.ts'))).toBeTruthy(); + expect(fs.pathExistsSync(path.join(workDir, 'SQSTypeScript', 'lib', 'sqs_type_script-stack.ts'))).toBeTruthy(); }); - // Packages created for typescript - expect(fs.pathExistsSync(path.join(workDir, 'SQSTypeScript', 'package.json'))).toBeTruthy(); - expect(fs.pathExistsSync(path.join(workDir, 'SQSTypeScript', 'bin', 'sqs_type_script.ts'))).toBeTruthy(); - expect(fs.pathExistsSync(path.join(workDir, 'SQSTypeScript', 'lib', 'sqs_type_script-stack.ts'))).toBeTruthy(); - }); + cliTest('migrate succeeds for valid template from local path when lanugage is provided', async (workDir) => { + const toolkit = defaultToolkitSetup(); + await toolkit.migrate({ + stackName: 'S3Python', + fromPath: s3TemplatePath, + outputPath: workDir, + language: 'python', + }); - cliTest('migrate succeeds for valid template from local path when lanugage is provided', async (workDir) => { - const toolkit = defaultToolkitSetup(); - await toolkit.migrate({ - stackName: 'S3Python', - fromPath: s3TemplatePath, - outputPath: workDir, - language: 'python', + // Packages created for typescript + expect(fs.pathExistsSync(path.join(workDir, 'S3Python', 'requirements.txt'))).toBeTruthy(); + expect(fs.pathExistsSync(path.join(workDir, 'S3Python', 'app.py'))).toBeTruthy(); + expect(fs.pathExistsSync(path.join(workDir, 'S3Python', 's3_python', 's3_python_stack.py'))).toBeTruthy(); }); - // Packages created for typescript - expect(fs.pathExistsSync(path.join(workDir, 'S3Python', 'requirements.txt'))).toBeTruthy(); - expect(fs.pathExistsSync(path.join(workDir, 'S3Python', 'app.py'))).toBeTruthy(); - expect(fs.pathExistsSync(path.join(workDir, 'S3Python', 's3_python', 's3_python_stack.py'))).toBeTruthy(); + cliTest('migrate call is idempotent', async (workDir) => { + const toolkit = defaultToolkitSetup(); + await toolkit.migrate({ + stackName: 'AutoscalingCSharp', + fromPath: autoscalingTemplatePath, + outputPath: workDir, + language: 'csharp', + }); + + // Packages created for typescript + expect(fs.pathExistsSync(path.join(workDir, 'AutoscalingCSharp', 'src', 'AutoscalingCSharp.sln'))).toBeTruthy(); + expect(fs.pathExistsSync(path.join(workDir, 'AutoscalingCSharp', 'src', 'AutoscalingCSharp', 'Program.cs'))).toBeTruthy(); + expect(fs.pathExistsSync(path.join(workDir, 'AutoscalingCSharp', 'src', 'AutoscalingCSharp', 'AutoscalingCSharpStack.cs'))).toBeTruthy(); + + // One more time + await toolkit.migrate({ + stackName: 'AutoscalingCSharp', + fromPath: autoscalingTemplatePath, + outputPath: workDir, + language: 'csharp', + }); + + // Packages created for typescript + expect(fs.pathExistsSync(path.join(workDir, 'AutoscalingCSharp', 'src', 'AutoscalingCSharp.sln'))).toBeTruthy(); + expect(fs.pathExistsSync(path.join(workDir, 'AutoscalingCSharp', 'src', 'AutoscalingCSharp', 'Program.cs'))).toBeTruthy(); + expect(fs.pathExistsSync(path.join(workDir, 'AutoscalingCSharp', 'src', 'AutoscalingCSharp', 'AutoscalingCSharpStack.cs'))).toBeTruthy(); + }); }); - cliTest('migrate call is idempotent', async (workDir) => { - const toolkit = defaultToolkitSetup(); - await toolkit.migrate({ - stackName: 'AutoscalingCSharp', - fromPath: autoscalingTemplatePath, - outputPath: workDir, - language: 'csharp', - }); - - // Packages created for typescript - expect(fs.pathExistsSync(path.join(workDir, 'AutoscalingCSharp', 'src', 'AutoscalingCSharp.sln'))).toBeTruthy(); - expect(fs.pathExistsSync(path.join(workDir, 'AutoscalingCSharp', 'src', 'AutoscalingCSharp', 'Program.cs'))).toBeTruthy(); - expect(fs.pathExistsSync(path.join(workDir, 'AutoscalingCSharp', 'src', 'AutoscalingCSharp', 'AutoscalingCSharpStack.cs'))).toBeTruthy(); - - // One more time - await toolkit.migrate({ - stackName: 'AutoscalingCSharp', - fromPath: autoscalingTemplatePath, - outputPath: workDir, - language: 'csharp', - }); - - // Packages created for typescript - expect(fs.pathExistsSync(path.join(workDir, 'AutoscalingCSharp', 'src', 'AutoscalingCSharp.sln'))).toBeTruthy(); - expect(fs.pathExistsSync(path.join(workDir, 'AutoscalingCSharp', 'src', 'AutoscalingCSharp', 'Program.cs'))).toBeTruthy(); - expect(fs.pathExistsSync(path.join(workDir, 'AutoscalingCSharp', 'src', 'AutoscalingCSharp', 'AutoscalingCSharpStack.cs'))).toBeTruthy(); + describe('stack with error and flagged for validation', () => { + beforeEach(() => { + cloudExecutable = new MockCloudExecutable({ + stacks: [ + MockStack.MOCK_STACK_A, + MockStack.MOCK_STACK_B, + ], + nestedAssemblies: [{ + stacks: [ + { properties: { validateOnSynth: true }, ...MockStack.MOCK_STACK_WITH_ERROR }, + ], + }], + }); + }); + + test('causes synth to fail if autoValidate=true', async () => { + const toolkit = defaultToolkitSetup(); + const autoValidate = true; + await expect(toolkit.synth([], false, true, autoValidate)).rejects.toBeDefined(); + }); + + test('causes synth to succeed if autoValidate=false', async () => { + const toolkit = defaultToolkitSetup(); + const autoValidate = false; + await toolkit.synth([], false, true, autoValidate); + expect(mockData.mock.calls.length).toEqual(0); + }); }); - }); - describe('stack with error and flagged for validation', () => { - beforeEach(() => { + test('stack has error and was explicitly selected', async () => { cloudExecutable = new MockCloudExecutable({ stacks: [ MockStack.MOCK_STACK_A, @@ -1138,76 +1168,49 @@ describe('synth', () => { ], nestedAssemblies: [{ stacks: [ - { properties: { validateOnSynth: true }, ...MockStack.MOCK_STACK_WITH_ERROR }, + { properties: { validateOnSynth: false }, ...MockStack.MOCK_STACK_WITH_ERROR }, ], }], }); - }); - test('causes synth to fail if autoValidate=true', async() => { const toolkit = defaultToolkitSetup(); - const autoValidate = true; - await expect(toolkit.synth([], false, true, autoValidate)).rejects.toBeDefined(); - }); - test('causes synth to succeed if autoValidate=false', async() => { - const toolkit = defaultToolkitSetup(); - const autoValidate = false; - await toolkit.synth([], false, true, autoValidate); - expect(mockData.mock.calls.length).toEqual(0); + await expect(toolkit.synth(['Test-Stack-A/witherrors'], false, true)).rejects.toBeDefined(); }); - }); - test('stack has error and was explicitly selected', async() => { - cloudExecutable = new MockCloudExecutable({ - stacks: [ - MockStack.MOCK_STACK_A, - MockStack.MOCK_STACK_B, - ], - nestedAssemblies: [{ + test('stack has error, is not flagged for validation and was not explicitly selected', async () => { + cloudExecutable = new MockCloudExecutable({ stacks: [ - { properties: { validateOnSynth: false }, ...MockStack.MOCK_STACK_WITH_ERROR }, + MockStack.MOCK_STACK_A, + MockStack.MOCK_STACK_B, ], - }], - }); + nestedAssemblies: [{ + stacks: [ + { properties: { validateOnSynth: false }, ...MockStack.MOCK_STACK_WITH_ERROR }, + ], + }], + }); - const toolkit = defaultToolkitSetup(); + const toolkit = defaultToolkitSetup(); - await expect(toolkit.synth(['Test-Stack-A/witherrors'], false, true)).rejects.toBeDefined(); - }); + await toolkit.synth([], false, true); + }); - test('stack has error, is not flagged for validation and was not explicitly selected', async () => { - cloudExecutable = new MockCloudExecutable({ - stacks: [ - MockStack.MOCK_STACK_A, - MockStack.MOCK_STACK_B, - ], - nestedAssemblies: [{ + test('stack has dependency and was explicitly selected', async () => { + cloudExecutable = new MockCloudExecutable({ stacks: [ - { properties: { validateOnSynth: false }, ...MockStack.MOCK_STACK_WITH_ERROR }, + MockStack.MOCK_STACK_C, + MockStack.MOCK_STACK_D, ], - }], - }); + }); - const toolkit = defaultToolkitSetup(); + const toolkit = defaultToolkitSetup(); - await toolkit.synth([], false, true); - }); + await toolkit.synth([MockStack.MOCK_STACK_D.stackName], true, false); - test('stack has dependency and was explicitly selected', async () => { - cloudExecutable = new MockCloudExecutable({ - stacks: [ - MockStack.MOCK_STACK_C, - MockStack.MOCK_STACK_D, - ], + expect(mockData.mock.calls.length).toEqual(1); + expect(mockData.mock.calls[0][0]).toBeDefined(); }); - - const toolkit = defaultToolkitSetup(); - - await toolkit.synth([MockStack.MOCK_STACK_D.stackName], true, false); - - expect(mockData.mock.calls.length).toEqual(1); - expect(mockData.mock.calls[0][0]).toBeDefined(); }); }); From 8cd8f6e12aaf055f67980766cd578d48b0885c5d Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Tue, 18 Jun 2024 10:31:33 -0700 Subject: [PATCH 19/51] logging for this at this point --- packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts b/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts index 6b827d4a2e363..a6cb01dcb6462 100644 --- a/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts +++ b/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts @@ -113,6 +113,11 @@ export class CloudAssembly { throw new Error('This app contains no stacks'); } + if (selector.patterns.includes('Test-Stack-E')) { + // eslint-disable-next-line no-console + console.log(topLevelStacks.map(stack => stack.stackName)); + } + if (allTopLevel) { return this.selectTopLevelStacks(stacks, topLevelStacks, options.extend); } else if (patterns.length > 0) { From f605e1dcb6489aa1e810e3f5374b6d9a57dcc0e6 Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Tue, 18 Jun 2024 10:57:59 -0700 Subject: [PATCH 20/51] throw the error then --- packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts b/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts index a6cb01dcb6462..519e6d3223ed4 100644 --- a/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts +++ b/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts @@ -114,8 +114,7 @@ export class CloudAssembly { } if (selector.patterns.includes('Test-Stack-E')) { - // eslint-disable-next-line no-console - console.log(topLevelStacks.map(stack => stack.stackName)); + throw new Error(topLevelStacks.map(stack => stack.stackName)); } if (allTopLevel) { From 8848d0a999c65a9a82c826bfe2ef309414586c89 Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Tue, 18 Jun 2024 10:59:13 -0700 Subject: [PATCH 21/51] type error --- packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts b/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts index 519e6d3223ed4..092359fbe2646 100644 --- a/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts +++ b/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts @@ -114,7 +114,7 @@ export class CloudAssembly { } if (selector.patterns.includes('Test-Stack-E')) { - throw new Error(topLevelStacks.map(stack => stack.stackName)); + throw new Error(topLevelStacks.map(stack => stack.stackName).join(' ')); } if (allTopLevel) { From c5201ed91f846ecbd78e73b68443ec4b1015e907 Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Tue, 18 Jun 2024 11:59:11 -0700 Subject: [PATCH 22/51] man this is painful --- packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts | 4 ---- packages/aws-cdk/lib/cdk-toolkit.ts | 13 ++++++++++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts b/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts index 092359fbe2646..6b827d4a2e363 100644 --- a/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts +++ b/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts @@ -113,10 +113,6 @@ export class CloudAssembly { throw new Error('This app contains no stacks'); } - if (selector.patterns.includes('Test-Stack-E')) { - throw new Error(topLevelStacks.map(stack => stack.stackName).join(' ')); - } - if (allTopLevel) { return this.selectTopLevelStacks(stacks, topLevelStacks, options.extend); } else if (patterns.length > 0) { diff --git a/packages/aws-cdk/lib/cdk-toolkit.ts b/packages/aws-cdk/lib/cdk-toolkit.ts index f876634484c46..34c7141e34c53 100644 --- a/packages/aws-cdk/lib/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cdk-toolkit.ts @@ -864,7 +864,18 @@ export class CdkToolkit { ignoreNoStacks, }); - this.validateStacksSelected(stacks, selector.patterns); + try { + this.validateStacksSelected(stacks, selector.patterns); + } catch (e) { + if (selector.patterns.includes('Test-Stack-E')) { + const allStacks = await assembly.selectStacks({ patterns: [], allTopLevel: true }, { + defaultBehavior: DefaultSelection.AllStacks, + }); + throw new Error(allStacks.stackArtifacts.map(stack => stack.stackName).join(' ')); + } + + throw e; + } this.validateStacks(stacks); return stacks; From 6c6616e504e29ff0092bce37a3cdf4c6cb672081 Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Tue, 18 Jun 2024 13:34:40 -0700 Subject: [PATCH 23/51] woowoowoowoo --- packages/aws-cdk/lib/cdk-toolkit.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/aws-cdk/lib/cdk-toolkit.ts b/packages/aws-cdk/lib/cdk-toolkit.ts index 34c7141e34c53..4c00baea5e211 100644 --- a/packages/aws-cdk/lib/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cdk-toolkit.ts @@ -866,12 +866,12 @@ export class CdkToolkit { try { this.validateStacksSelected(stacks, selector.patterns); - } catch (e) { + } catch (e: any) { if (selector.patterns.includes('Test-Stack-E')) { const allStacks = await assembly.selectStacks({ patterns: [], allTopLevel: true }, { defaultBehavior: DefaultSelection.AllStacks, }); - throw new Error(allStacks.stackArtifacts.map(stack => stack.stackName).join(' ')); + throw new Error(allStacks.stackArtifacts.map(stack => stack.stackName).join(' ').concat(e.message)); } throw e; From d5b9a80988fe1e634a7fd55e04e160207ee55545 Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Wed, 19 Jun 2024 08:45:19 -0700 Subject: [PATCH 24/51] selector --- packages/aws-cdk/lib/cdk-toolkit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/aws-cdk/lib/cdk-toolkit.ts b/packages/aws-cdk/lib/cdk-toolkit.ts index 4c00baea5e211..f11d861ddfaa9 100644 --- a/packages/aws-cdk/lib/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cdk-toolkit.ts @@ -868,7 +868,7 @@ export class CdkToolkit { this.validateStacksSelected(stacks, selector.patterns); } catch (e: any) { if (selector.patterns.includes('Test-Stack-E')) { - const allStacks = await assembly.selectStacks({ patterns: [], allTopLevel: true }, { + const allStacks = await assembly.selectStacks(selector, { defaultBehavior: DefaultSelection.AllStacks, }); throw new Error(allStacks.stackArtifacts.map(stack => stack.stackName).join(' ').concat(e.message)); From c718b20fbf1a0ce925231dba8e9ef2fb28a41356 Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Wed, 19 Jun 2024 08:47:31 -0700 Subject: [PATCH 25/51] jest --- packages/aws-cdk/lib/cdk-toolkit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/aws-cdk/lib/cdk-toolkit.ts b/packages/aws-cdk/lib/cdk-toolkit.ts index f11d861ddfaa9..79d0522e24cd1 100644 --- a/packages/aws-cdk/lib/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cdk-toolkit.ts @@ -871,7 +871,7 @@ export class CdkToolkit { const allStacks = await assembly.selectStacks(selector, { defaultBehavior: DefaultSelection.AllStacks, }); - throw new Error(allStacks.stackArtifacts.map(stack => stack.stackName).join(' ').concat(e.message)); + throw new Error('allStacks: '.concat(allStacks.stackArtifacts.map(stack => stack.stackName).join(' ')).concat('actual stacks: ').concat(stacks.stackArtifacts.map(stack => stack.stackName).join(' ')).concat(e.message)); } throw e; From 335d6dd9adeea1b608a3bcb63b42e312de474df1 Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Wed, 19 Jun 2024 09:43:28 -0700 Subject: [PATCH 26/51] jest --- packages/aws-cdk/lib/cdk-toolkit.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/aws-cdk/lib/cdk-toolkit.ts b/packages/aws-cdk/lib/cdk-toolkit.ts index 79d0522e24cd1..7611bce2d36b4 100644 --- a/packages/aws-cdk/lib/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cdk-toolkit.ts @@ -871,7 +871,8 @@ export class CdkToolkit { const allStacks = await assembly.selectStacks(selector, { defaultBehavior: DefaultSelection.AllStacks, }); - throw new Error('allStacks: '.concat(allStacks.stackArtifacts.map(stack => stack.stackName).join(' ')).concat('actual stacks: ').concat(stacks.stackArtifacts.map(stack => stack.stackName).join(' ')).concat(e.message)); + const customStacks = await assembly.selectStacks({ patterns: [], allTopLevel: true }, { defaultBehavior: DefaultSelection.AllStacks }); + throw new Error('allStacks: '.concat(allStacks.stackArtifacts.map(stack => stack.stackName).join(' ')).concat('actual stacks: ').concat(stacks.stackArtifacts.map(stack => stack.stackName).join(' ')).concat('custom stacks: ').concat(customStacks.stackArtifacts.map(stack => stack.stackName).join(' ')).concat(e.message)); } throw e; From cbde0132010c016635dee25d2954be0ab00bdcb5 Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Wed, 19 Jun 2024 10:24:58 -0700 Subject: [PATCH 27/51] jest --- packages/aws-cdk/lib/cdk-toolkit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/aws-cdk/lib/cdk-toolkit.ts b/packages/aws-cdk/lib/cdk-toolkit.ts index 7611bce2d36b4..27accddd875be 100644 --- a/packages/aws-cdk/lib/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cdk-toolkit.ts @@ -872,7 +872,7 @@ export class CdkToolkit { defaultBehavior: DefaultSelection.AllStacks, }); const customStacks = await assembly.selectStacks({ patterns: [], allTopLevel: true }, { defaultBehavior: DefaultSelection.AllStacks }); - throw new Error('allStacks: '.concat(allStacks.stackArtifacts.map(stack => stack.stackName).join(' ')).concat('actual stacks: ').concat(stacks.stackArtifacts.map(stack => stack.stackName).join(' ')).concat('custom stacks: ').concat(customStacks.stackArtifacts.map(stack => stack.stackName).join(' ')).concat(e.message)); + throw new Error(`selector: ${selector}, `.concat('allStacks: ').concat(allStacks.stackArtifacts.map(stack => stack.stackName).join(' ')).concat('actual stacks: ').concat(stacks.stackArtifacts.map(stack => stack.stackName).join(' ')).concat('custom stacks: ').concat(customStacks.stackArtifacts.map(stack => stack.stackName).join(' ')).concat(e.message)); } throw e; From e9b0ddff8145aa933531ae57e075d8b9b629e407 Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Wed, 19 Jun 2024 11:02:19 -0700 Subject: [PATCH 28/51] jest --- packages/aws-cdk/lib/cdk-toolkit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/aws-cdk/lib/cdk-toolkit.ts b/packages/aws-cdk/lib/cdk-toolkit.ts index 27accddd875be..9c3beb8229c76 100644 --- a/packages/aws-cdk/lib/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cdk-toolkit.ts @@ -872,7 +872,7 @@ export class CdkToolkit { defaultBehavior: DefaultSelection.AllStacks, }); const customStacks = await assembly.selectStacks({ patterns: [], allTopLevel: true }, { defaultBehavior: DefaultSelection.AllStacks }); - throw new Error(`selector: ${selector}, `.concat('allStacks: ').concat(allStacks.stackArtifacts.map(stack => stack.stackName).join(' ')).concat('actual stacks: ').concat(stacks.stackArtifacts.map(stack => stack.stackName).join(' ')).concat('custom stacks: ').concat(customStacks.stackArtifacts.map(stack => stack.stackName).join(' ')).concat(e.message)); + throw new Error(`selector patterns: ${selector.patterns} selector allTopLevel: ${selector.allTopLevel}, `.concat('allStacks: ').concat(allStacks.stackArtifacts.map(stack => stack.stackName).join(' ')).concat('actual stacks: ').concat(stacks.stackArtifacts.map(stack => stack.stackName).join(' ')).concat('custom stacks: ').concat(customStacks.stackArtifacts.map(stack => stack.stackName).join(' ')).concat(e.message)); } throw e; From 8bd64ea840266be376250f32bdf63044fcdc2d65 Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Thu, 20 Jun 2024 10:04:50 -0700 Subject: [PATCH 29/51] cloud assem --- packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts b/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts index 6b827d4a2e363..d26407c265999 100644 --- a/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts +++ b/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts @@ -116,6 +116,10 @@ export class CloudAssembly { if (allTopLevel) { return this.selectTopLevelStacks(stacks, topLevelStacks, options.extend); } else if (patterns.length > 0) { + if (patterns.includes('Test-Stack-E')) { + // eslint-disable-next-line no-console + console.error(`WOW found stacks: ${stacks.map(stack => stack.stackName)}`); + } return this.selectMatchingStacks(stacks, patterns, options.extend); } else { return this.selectDefaultStacks(stacks, topLevelStacks, options.defaultBehavior); From 83cc722e444a5f56e09fd9dbbd46439cb506acbc Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Thu, 20 Jun 2024 10:28:37 -0700 Subject: [PATCH 30/51] wowzers --- packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts b/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts index d26407c265999..e28d799dbfd82 100644 --- a/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts +++ b/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts @@ -118,7 +118,7 @@ export class CloudAssembly { } else if (patterns.length > 0) { if (patterns.includes('Test-Stack-E')) { // eslint-disable-next-line no-console - console.error(`WOW found stacks: ${stacks.map(stack => stack.stackName)}`); + console.error(`WOW found stacks: ${stacks.map(stack => stack.stackName)}. They have heirarchical IDs: ${stacks.map(stack => stack.hierarchicalId)}`); } return this.selectMatchingStacks(stacks, patterns, options.extend); } else { @@ -143,6 +143,10 @@ export class CloudAssembly { // cli tests use this to ensure tests do not depend on legacy behavior // (otherwise they will fail in v2) const disableLegacy = process.env.CXAPI_DISABLE_SELECT_BY_ID === '1'; + if (patterns.includes('Test-Stack-E')) { + // eslint-disable-next-line no-console + console.error(`selecting stacks. extend: ${extend}`); + } const matchingPattern = (pattern: string) => (stack: cxapi.CloudFormationStackArtifact) => { if (minimatch(stack.hierarchicalId, pattern)) { From 2c9884f57424e665692f846efcebd2ff81ace924 Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Thu, 20 Jun 2024 11:12:14 -0700 Subject: [PATCH 31/51] asm --- packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts b/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts index e28d799dbfd82..1eee750204725 100644 --- a/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts +++ b/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts @@ -106,8 +106,17 @@ export class CloudAssembly { const allTopLevel = selector.allTopLevel ?? false; const patterns = sanitizePatterns(selector.patterns); + if (patterns.includes('Test-Stack-E')) { + // eslint-disable-next-line no-console + console.error(`WOW found stacks: ${stacks.map(stack => stack.stackName)}. They have heirarchical IDs: ${stacks.map(stack => stack.hierarchicalId)}`); + } + if (stacks.length === 0) { if (options.ignoreNoStacks) { + if (patterns.includes('Test-Stack-E')) { + // eslint-disable-next-line no-console + console.error('no stacks found!'); + } return new StackCollection(this, []); } throw new Error('This app contains no stacks'); @@ -116,10 +125,6 @@ export class CloudAssembly { if (allTopLevel) { return this.selectTopLevelStacks(stacks, topLevelStacks, options.extend); } else if (patterns.length > 0) { - if (patterns.includes('Test-Stack-E')) { - // eslint-disable-next-line no-console - console.error(`WOW found stacks: ${stacks.map(stack => stack.stackName)}. They have heirarchical IDs: ${stacks.map(stack => stack.hierarchicalId)}`); - } return this.selectMatchingStacks(stacks, patterns, options.extend); } else { return this.selectDefaultStacks(stacks, topLevelStacks, options.defaultBehavior); From 2539c9cc640e3d9e72404f6d52f67f433b7ce955 Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Thu, 20 Jun 2024 11:21:23 -0700 Subject: [PATCH 32/51] asm --- .../aws-cdk/lib/api/cxapp/cloud-assembly.ts | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts b/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts index 1eee750204725..caa05095709af 100644 --- a/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts +++ b/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts @@ -106,17 +106,8 @@ export class CloudAssembly { const allTopLevel = selector.allTopLevel ?? false; const patterns = sanitizePatterns(selector.patterns); - if (patterns.includes('Test-Stack-E')) { - // eslint-disable-next-line no-console - console.error(`WOW found stacks: ${stacks.map(stack => stack.stackName)}. They have heirarchical IDs: ${stacks.map(stack => stack.hierarchicalId)}`); - } - if (stacks.length === 0) { if (options.ignoreNoStacks) { - if (patterns.includes('Test-Stack-E')) { - // eslint-disable-next-line no-console - console.error('no stacks found!'); - } return new StackCollection(this, []); } throw new Error('This app contains no stacks'); @@ -125,6 +116,10 @@ export class CloudAssembly { if (allTopLevel) { return this.selectTopLevelStacks(stacks, topLevelStacks, options.extend); } else if (patterns.length > 0) { + if (patterns.includes('Test-Stack-E')) { + // eslint-disable-next-line no-console + console.error(`WOW found stacks: ${stacks.map(stack => stack.stackName)}. They have heirarchical IDs: ${stacks.map(stack => stack.hierarchicalId)}`); + } return this.selectMatchingStacks(stacks, patterns, options.extend); } else { return this.selectDefaultStacks(stacks, topLevelStacks, options.defaultBehavior); @@ -154,6 +149,10 @@ export class CloudAssembly { } const matchingPattern = (pattern: string) => (stack: cxapi.CloudFormationStackArtifact) => { + if (pattern.includes('Test-Stack-E')) { + // eslint-disable-next-line no-console + console.error(`attempting to minimatch '${pattern}' against '${stack.hierarchicalId}': result is '${minimatch(stack.hierarchicalId, pattern)}'`); + } if (minimatch(stack.hierarchicalId, pattern)) { return true; } else if (!disableLegacy && stack.id === pattern && semver.major(versionNumber()) < 2) { @@ -166,6 +165,11 @@ export class CloudAssembly { const matchedStacks = flatten(patterns.map(pattern => stacks.filter(matchingPattern(pattern)))); + if (patterns.includes('Test-Stack-E')) { + // eslint-disable-next-line no-console + console.error(`matched these stacks: ${matchedStacks.map(stack => stack.stackName)}`); + } + return this.extendStacks(matchedStacks, stacks, extend); } From ad7112261b2479d3f448b84a5755e9d79e89397f Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Thu, 20 Jun 2024 13:15:22 -0700 Subject: [PATCH 33/51] asm --- packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts b/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts index caa05095709af..83305e3069634 100644 --- a/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts +++ b/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts @@ -154,10 +154,20 @@ export class CloudAssembly { console.error(`attempting to minimatch '${pattern}' against '${stack.hierarchicalId}': result is '${minimatch(stack.hierarchicalId, pattern)}'`); } if (minimatch(stack.hierarchicalId, pattern)) { + // eslint-disable-next-line no-console + if (pattern.includes('Test-Stack-E')) { + // eslint-disable-next-line no-console + console.error(`no way! ${pattern} matched`); + } return true; } else if (!disableLegacy && stack.id === pattern && semver.major(versionNumber()) < 2) { warning('Selecting stack by identifier "%s". This identifier is deprecated and will be removed in v2. Please use "%s" instead.', chalk.bold(stack.id), chalk.bold(stack.hierarchicalId)); warning('Run "cdk ls" to see a list of all stack identifiers'); + // eslint-disable-next-line no-console + if (pattern.includes('Test-Stack-E')) { + // eslint-disable-next-line no-console + console.error(`wow really no way! ${pattern} and semver matched, disableLegacy is ${disableLegacy}, semver.major is ${semver.major(versionNumber())}, vesrionNumber is ${versionNumber()}`); + } return true; } return false; From 45357e1b97b7473efd6c5af55e70fc9fa3229226 Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Thu, 20 Jun 2024 16:11:03 -0700 Subject: [PATCH 34/51] asm --- packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts b/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts index 83305e3069634..7b8160ffda8c5 100644 --- a/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts +++ b/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts @@ -152,6 +152,8 @@ export class CloudAssembly { if (pattern.includes('Test-Stack-E')) { // eslint-disable-next-line no-console console.error(`attempting to minimatch '${pattern}' against '${stack.hierarchicalId}': result is '${minimatch(stack.hierarchicalId, pattern)}'`); + // eslint-disable-next-line no-console + console.error(`stack.id is ${stack.id}, disableLegacy is ${disableLegacy}, semver.major is ${semver.major(versionNumber())}, versionNumber is ${versionNumber()}`); } if (minimatch(stack.hierarchicalId, pattern)) { // eslint-disable-next-line no-console From abd6dc42d73b0afd7f1f6bb35bcef96ba16c0c07 Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Thu, 20 Jun 2024 17:01:58 -0700 Subject: [PATCH 35/51] finally fixed it...... --- packages/aws-cdk/test/cdk-toolkit.test.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/aws-cdk/test/cdk-toolkit.test.ts b/packages/aws-cdk/test/cdk-toolkit.test.ts index ab7b7cbc7e115..488e189e70f7c 100644 --- a/packages/aws-cdk/test/cdk-toolkit.test.ts +++ b/packages/aws-cdk/test/cdk-toolkit.test.ts @@ -518,8 +518,9 @@ describe('toolkit', () => { configuration: cloudExecutable.configuration, sdkProvider: cloudExecutable.sdkProvider, deployments: new FakeCloudFormation({ - 'Test-Stack-E': { Foo: 'Bar' }, - 'Test-Stack-F': { Foo: 'Bar' }, + // Stacks should be selected by their hierarchical ID, which is their displayName, not by the stack ID. + 'Test-Stack-E-Display-Name': { Foo: 'Bar' }, + 'Test-Stack-F-Display-Name': { Foo: 'Bar' }, }, notificationArns), }); @@ -539,7 +540,7 @@ describe('toolkit', () => { configuration: cloudExecutable.configuration, sdkProvider: cloudExecutable.sdkProvider, deployments: new FakeCloudFormation({ - 'Test-Stack-E': { Foo: 'Bar' }, + 'Test-Stack-E-Display-Name': { Foo: 'Bar' }, }, notificationArns), }); From 178e712a275b6f81970c02456ef38570d16af265 Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Fri, 21 Jun 2024 09:57:47 -0700 Subject: [PATCH 36/51] fixed --- .../aws-cdk/lib/api/cxapp/cloud-assembly.ts | 29 - packages/aws-cdk/lib/cdk-toolkit.ts | 14 +- packages/aws-cdk/test/cdk-toolkit.test.ts | 1781 +++++++++-------- 3 files changed, 897 insertions(+), 927 deletions(-) diff --git a/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts b/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts index 7b8160ffda8c5..6b827d4a2e363 100644 --- a/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts +++ b/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts @@ -116,10 +116,6 @@ export class CloudAssembly { if (allTopLevel) { return this.selectTopLevelStacks(stacks, topLevelStacks, options.extend); } else if (patterns.length > 0) { - if (patterns.includes('Test-Stack-E')) { - // eslint-disable-next-line no-console - console.error(`WOW found stacks: ${stacks.map(stack => stack.stackName)}. They have heirarchical IDs: ${stacks.map(stack => stack.hierarchicalId)}`); - } return this.selectMatchingStacks(stacks, patterns, options.extend); } else { return this.selectDefaultStacks(stacks, topLevelStacks, options.defaultBehavior); @@ -143,33 +139,13 @@ export class CloudAssembly { // cli tests use this to ensure tests do not depend on legacy behavior // (otherwise they will fail in v2) const disableLegacy = process.env.CXAPI_DISABLE_SELECT_BY_ID === '1'; - if (patterns.includes('Test-Stack-E')) { - // eslint-disable-next-line no-console - console.error(`selecting stacks. extend: ${extend}`); - } const matchingPattern = (pattern: string) => (stack: cxapi.CloudFormationStackArtifact) => { - if (pattern.includes('Test-Stack-E')) { - // eslint-disable-next-line no-console - console.error(`attempting to minimatch '${pattern}' against '${stack.hierarchicalId}': result is '${minimatch(stack.hierarchicalId, pattern)}'`); - // eslint-disable-next-line no-console - console.error(`stack.id is ${stack.id}, disableLegacy is ${disableLegacy}, semver.major is ${semver.major(versionNumber())}, versionNumber is ${versionNumber()}`); - } if (minimatch(stack.hierarchicalId, pattern)) { - // eslint-disable-next-line no-console - if (pattern.includes('Test-Stack-E')) { - // eslint-disable-next-line no-console - console.error(`no way! ${pattern} matched`); - } return true; } else if (!disableLegacy && stack.id === pattern && semver.major(versionNumber()) < 2) { warning('Selecting stack by identifier "%s". This identifier is deprecated and will be removed in v2. Please use "%s" instead.', chalk.bold(stack.id), chalk.bold(stack.hierarchicalId)); warning('Run "cdk ls" to see a list of all stack identifiers'); - // eslint-disable-next-line no-console - if (pattern.includes('Test-Stack-E')) { - // eslint-disable-next-line no-console - console.error(`wow really no way! ${pattern} and semver matched, disableLegacy is ${disableLegacy}, semver.major is ${semver.major(versionNumber())}, vesrionNumber is ${versionNumber()}`); - } return true; } return false; @@ -177,11 +153,6 @@ export class CloudAssembly { const matchedStacks = flatten(patterns.map(pattern => stacks.filter(matchingPattern(pattern)))); - if (patterns.includes('Test-Stack-E')) { - // eslint-disable-next-line no-console - console.error(`matched these stacks: ${matchedStacks.map(stack => stack.stackName)}`); - } - return this.extendStacks(matchedStacks, stacks, extend); } diff --git a/packages/aws-cdk/lib/cdk-toolkit.ts b/packages/aws-cdk/lib/cdk-toolkit.ts index 9c3beb8229c76..f876634484c46 100644 --- a/packages/aws-cdk/lib/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cdk-toolkit.ts @@ -864,19 +864,7 @@ export class CdkToolkit { ignoreNoStacks, }); - try { - this.validateStacksSelected(stacks, selector.patterns); - } catch (e: any) { - if (selector.patterns.includes('Test-Stack-E')) { - const allStacks = await assembly.selectStacks(selector, { - defaultBehavior: DefaultSelection.AllStacks, - }); - const customStacks = await assembly.selectStacks({ patterns: [], allTopLevel: true }, { defaultBehavior: DefaultSelection.AllStacks }); - throw new Error(`selector patterns: ${selector.patterns} selector allTopLevel: ${selector.allTopLevel}, `.concat('allStacks: ').concat(allStacks.stackArtifacts.map(stack => stack.stackName).join(' ')).concat('actual stacks: ').concat(stacks.stackArtifacts.map(stack => stack.stackName).join(' ')).concat('custom stacks: ').concat(customStacks.stackArtifacts.map(stack => stack.stackName).join(' ')).concat(e.message)); - } - - throw e; - } + this.validateStacksSelected(stacks, selector.patterns); this.validateStacks(stacks); return stacks; diff --git a/packages/aws-cdk/test/cdk-toolkit.test.ts b/packages/aws-cdk/test/cdk-toolkit.test.ts index 488e189e70f7c..ba548215e7634 100644 --- a/packages/aws-cdk/test/cdk-toolkit.test.ts +++ b/packages/aws-cdk/test/cdk-toolkit.test.ts @@ -71,6 +71,8 @@ import { CdkToolkit, Tag } from '../lib/cdk-toolkit'; import { RequireApproval } from '../lib/diff'; import { flatten } from '../lib/util'; +process.env.CXAPI_DISABLE_SELECT_BY_ID = '1'; + let cloudExecutable: MockCloudExecutable; let bootstrapper: jest.Mocked; let stderrMock: jest.SpyInstance; @@ -121,1047 +123,1067 @@ function destroyToolkitSetup() { }); } -// everything within this descirbe block will run synchronously, preventing race conditions between tests -describe('toolkit', () => { - describe('readCurrentTemplate', () => { - let template: any; - let mockForEnvironment = jest.fn(); - let mockCloudExecutable: MockCloudExecutable; - beforeEach(() => { - - template = { - Resources: { - Func: { - Type: 'AWS::Lambda::Function', - Properties: { - Key: 'Value', - }, +describe('readCurrentTemplate', () => { + let template: any; + let mockForEnvironment = jest.fn(); + let mockCloudExecutable: MockCloudExecutable; + beforeEach(() => { + + template = { + Resources: { + Func: { + Type: 'AWS::Lambda::Function', + Properties: { + Key: 'Value', }, }, - }; - mockCloudExecutable = new MockCloudExecutable({ - stacks: [ - { - stackName: 'Test-Stack-C', - template, - properties: { - assumeRoleArn: 'bloop:${AWS::Region}:${AWS::AccountId}', - lookupRole: { - arn: 'bloop-lookup:${AWS::Region}:${AWS::AccountId}', - requiresBootstrapStackVersion: 5, - bootstrapStackVersionSsmParameter: '/bootstrap/parameter', - }, - }, - }, - { - stackName: 'Test-Stack-A', - template, - properties: { - assumeRoleArn: 'bloop:${AWS::Region}:${AWS::AccountId}', + }, + }; + mockCloudExecutable = new MockCloudExecutable({ + stacks: [ + { + stackName: 'Test-Stack-C', + template, + properties: { + assumeRoleArn: 'bloop:${AWS::Region}:${AWS::AccountId}', + lookupRole: { + arn: 'bloop-lookup:${AWS::Region}:${AWS::AccountId}', + requiresBootstrapStackVersion: 5, + bootstrapStackVersionSsmParameter: '/bootstrap/parameter', }, }, - ], - }); - mockForEnvironment = jest.fn().mockImplementation(() => { - return { sdk: mockCloudExecutable.sdkProvider.sdk, didAssumeRole: true }; - }); - mockCloudExecutable.sdkProvider.forEnvironment = mockForEnvironment; - mockCloudExecutable.sdkProvider.stubCloudFormation({ - getTemplate() { - return { - TemplateBody: JSON.stringify(template), - }; }, - describeStacks() { - return { - Stacks: [ - { - StackName: 'Test-Stack-C', - StackStatus: 'CREATE_COMPLETE', - CreationTime: new Date(), - }, - { - StackName: 'Test-Stack-A', - StackStatus: 'CREATE_COMPLETE', - CreationTime: new Date(), - }, - ], - }; + { + stackName: 'Test-Stack-A', + template, + properties: { + assumeRoleArn: 'bloop:${AWS::Region}:${AWS::AccountId}', + }, }, - }); + ], }); - - test('lookup role is used', async () => { - // GIVEN - let requestedParameterName: string; - mockCloudExecutable.sdkProvider.stubSSM({ - getParameter(request) { - requestedParameterName = request.Name; - return { - Parameter: { - Value: '6', + mockForEnvironment = jest.fn().mockImplementation(() => { + return { sdk: mockCloudExecutable.sdkProvider.sdk, didAssumeRole: true }; + }); + mockCloudExecutable.sdkProvider.forEnvironment = mockForEnvironment; + mockCloudExecutable.sdkProvider.stubCloudFormation({ + getTemplate() { + return { + TemplateBody: JSON.stringify(template), + }; + }, + describeStacks() { + return { + Stacks: [ + { + StackName: 'Test-Stack-C', + StackStatus: 'CREATE_COMPLETE', + CreationTime: new Date(), }, - }; - }, - }); - const cdkToolkit = new CdkToolkit({ - cloudExecutable: mockCloudExecutable, - configuration: mockCloudExecutable.configuration, - sdkProvider: mockCloudExecutable.sdkProvider, - deployments: new Deployments({ sdkProvider: mockCloudExecutable.sdkProvider }), - }); + { + StackName: 'Test-Stack-A', + StackStatus: 'CREATE_COMPLETE', + CreationTime: new Date(), + }, + ], + }; + }, + }); + }); - // WHEN - await cdkToolkit.deploy({ - selector: { patterns: ['Test-Stack-C'] }, - hotswap: HotswapMode.FULL_DEPLOYMENT, - }); + test('lookup role is used', async () => { + // GIVEN + let requestedParameterName: string; + mockCloudExecutable.sdkProvider.stubSSM({ + getParameter(request) { + requestedParameterName = request.Name; + return { + Parameter: { + Value: '6', + }, + }; + }, + }); + const cdkToolkit = new CdkToolkit({ + cloudExecutable: mockCloudExecutable, + configuration: mockCloudExecutable.configuration, + sdkProvider: mockCloudExecutable.sdkProvider, + deployments: new Deployments({ sdkProvider: mockCloudExecutable.sdkProvider }), + }); - // THEN - expect(requestedParameterName!).toEqual('/bootstrap/parameter'); - expect(mockForEnvironment.mock.calls.length).toEqual(2); - expect(mockForEnvironment.mock.calls[0][2]).toEqual({ - assumeRoleArn: 'bloop-lookup:here:123456789012', - }); + // WHEN + await cdkToolkit.deploy({ + selector: { patterns: ['Test-Stack-C'] }, + hotswap: HotswapMode.FULL_DEPLOYMENT, }); - test('fallback to deploy role if bootstrap stack version is not valid', async () => { - // GIVEN - let requestedParameterName: string; - mockCloudExecutable.sdkProvider.stubSSM({ - getParameter(request) { - requestedParameterName = request.Name; - return { - Parameter: { - Value: '1', - }, - }; - }, - }); - const cdkToolkit = new CdkToolkit({ - cloudExecutable: mockCloudExecutable, - configuration: mockCloudExecutable.configuration, - sdkProvider: mockCloudExecutable.sdkProvider, - deployments: new Deployments({ sdkProvider: mockCloudExecutable.sdkProvider }), - }); + // THEN + expect(requestedParameterName!).toEqual('/bootstrap/parameter'); + expect(mockForEnvironment.mock.calls.length).toEqual(2); + expect(mockForEnvironment.mock.calls[0][2]).toEqual({ + assumeRoleArn: 'bloop-lookup:here:123456789012', + }); + }); - // WHEN - await cdkToolkit.deploy({ - selector: { patterns: ['Test-Stack-C'] }, - hotswap: HotswapMode.FULL_DEPLOYMENT, - }); + test('fallback to deploy role if bootstrap stack version is not valid', async () => { + // GIVEN + let requestedParameterName: string; + mockCloudExecutable.sdkProvider.stubSSM({ + getParameter(request) { + requestedParameterName = request.Name; + return { + Parameter: { + Value: '1', + }, + }; + }, + }); + const cdkToolkit = new CdkToolkit({ + cloudExecutable: mockCloudExecutable, + configuration: mockCloudExecutable.configuration, + sdkProvider: mockCloudExecutable.sdkProvider, + deployments: new Deployments({ sdkProvider: mockCloudExecutable.sdkProvider }), + }); - // THEN - expect(flatten(stderrMock.mock.calls)).toEqual(expect.arrayContaining([ - expect.stringMatching(/Could not assume bloop-lookup:here:123456789012/), - expect.stringContaining("Bootstrap stack version '5' is required, found version '1'. To get rid of this error, please upgrade to bootstrap version >= 5"), - ])); - expect(requestedParameterName!).toEqual('/bootstrap/parameter'); - expect(mockForEnvironment.mock.calls.length).toEqual(3); - expect(mockForEnvironment.mock.calls[0][2]).toEqual({ - assumeRoleArn: 'bloop-lookup:here:123456789012', - }); - expect(mockForEnvironment.mock.calls[1][2]).toEqual({ - assumeRoleArn: 'bloop:here:123456789012', - }); + // WHEN + await cdkToolkit.deploy({ + selector: { patterns: ['Test-Stack-C'] }, + hotswap: HotswapMode.FULL_DEPLOYMENT, + }); + + // THEN + expect(flatten(stderrMock.mock.calls)).toEqual(expect.arrayContaining([ + expect.stringMatching(/Could not assume bloop-lookup:here:123456789012/), + expect.stringContaining("Bootstrap stack version '5' is required, found version '1'. To get rid of this error, please upgrade to bootstrap version >= 5"), + ])); + expect(requestedParameterName!).toEqual('/bootstrap/parameter'); + expect(mockForEnvironment.mock.calls.length).toEqual(3); + expect(mockForEnvironment.mock.calls[0][2]).toEqual({ + assumeRoleArn: 'bloop-lookup:here:123456789012', + }); + expect(mockForEnvironment.mock.calls[1][2]).toEqual({ + assumeRoleArn: 'bloop:here:123456789012', + }); + }); + + test('fallback to deploy role if bootstrap version parameter not found', async () => { + // GIVEN + mockCloudExecutable.sdkProvider.stubSSM({ + getParameter() { + throw new Error('not found'); + }, + }); + const cdkToolkit = new CdkToolkit({ + cloudExecutable: mockCloudExecutable, + configuration: mockCloudExecutable.configuration, + sdkProvider: mockCloudExecutable.sdkProvider, + deployments: new Deployments({ sdkProvider: mockCloudExecutable.sdkProvider }), + }); + + // WHEN + await cdkToolkit.deploy({ + selector: { patterns: ['Test-Stack-C'] }, + hotswap: HotswapMode.FULL_DEPLOYMENT, + }); + + // THEN + expect(flatten(stderrMock.mock.calls)).toEqual(expect.arrayContaining([ + expect.stringMatching(/Could not assume bloop-lookup:here:123456789012/), + ])); + expect(mockForEnvironment.mock.calls.length).toEqual(3); + expect(mockForEnvironment.mock.calls[0][2]).toEqual({ + assumeRoleArn: 'bloop-lookup:here:123456789012', + }); + expect(mockForEnvironment.mock.calls[1][2]).toEqual({ + assumeRoleArn: 'bloop:here:123456789012', + }); + }); + + test('fallback to deploy role if forEnvironment throws', async () => { + // GIVEN + // throw error first for the 'prepareSdkWithLookupRoleFor' call and succeed for the rest + mockForEnvironment = jest.fn().mockImplementationOnce(() => { throw new Error('error'); }) + .mockImplementation(() => { return { sdk: mockCloudExecutable.sdkProvider.sdk, didAssumeRole: true }; }); + mockCloudExecutable.sdkProvider.forEnvironment = mockForEnvironment; + mockCloudExecutable.sdkProvider.stubSSM({ + getParameter() { + return {}; + }, + }); + const cdkToolkit = new CdkToolkit({ + cloudExecutable: mockCloudExecutable, + configuration: mockCloudExecutable.configuration, + sdkProvider: mockCloudExecutable.sdkProvider, + deployments: new Deployments({ sdkProvider: mockCloudExecutable.sdkProvider }), + }); + + // WHEN + await cdkToolkit.deploy({ + selector: { patterns: ['Test-Stack-C'] }, + hotswap: HotswapMode.FULL_DEPLOYMENT, + }); + + // THEN + expect(mockCloudExecutable.sdkProvider.sdk.ssm).not.toHaveBeenCalled(); + expect(flatten(stderrMock.mock.calls)).toEqual(expect.arrayContaining([ + expect.stringMatching(/Could not assume bloop-lookup:here:123456789012/), + ])); + expect(mockForEnvironment.mock.calls.length).toEqual(3); + expect(mockForEnvironment.mock.calls[0][2]).toEqual({ + assumeRoleArn: 'bloop-lookup:here:123456789012', + }); + expect(mockForEnvironment.mock.calls[1][2]).toEqual({ + assumeRoleArn: 'bloop:here:123456789012', + }); + }); + + test('dont lookup bootstrap version parameter if default credentials are used', async () => { + // GIVEN + mockForEnvironment = jest.fn().mockImplementation(() => { return { sdk: mockCloudExecutable.sdkProvider.sdk, didAssumeRole: false }; }); + mockCloudExecutable.sdkProvider.forEnvironment = mockForEnvironment; + const cdkToolkit = new CdkToolkit({ + cloudExecutable: mockCloudExecutable, + configuration: mockCloudExecutable.configuration, + sdkProvider: mockCloudExecutable.sdkProvider, + deployments: new Deployments({ sdkProvider: mockCloudExecutable.sdkProvider }), + }); + mockCloudExecutable.sdkProvider.stubSSM({ + getParameter() { + return {}; + }, + }); + + // WHEN + await cdkToolkit.deploy({ + selector: { patterns: ['Test-Stack-C'] }, + hotswap: HotswapMode.FULL_DEPLOYMENT, + }); + + // THEN + expect(flatten(stderrMock.mock.calls)).toEqual(expect.arrayContaining([ + expect.stringMatching(/Lookup role exists but was not assumed. Proceeding with default credentials./), + ])); + expect(mockCloudExecutable.sdkProvider.sdk.ssm).not.toHaveBeenCalled(); + expect(mockForEnvironment.mock.calls.length).toEqual(3); + expect(mockForEnvironment.mock.calls[0][2]).toEqual({ + assumeRoleArn: 'bloop-lookup:here:123456789012', + }); + expect(mockForEnvironment.mock.calls[1][2]).toEqual({ + assumeRoleArn: 'bloop:here:123456789012', + }); + }); + + test('do not print warnings if lookup role not provided in stack artifact', async () => { + // GIVEN + mockCloudExecutable.sdkProvider.stubSSM({ + getParameter() { + return {}; + }, + }); + const cdkToolkit = new CdkToolkit({ + cloudExecutable: mockCloudExecutable, + configuration: mockCloudExecutable.configuration, + sdkProvider: mockCloudExecutable.sdkProvider, + deployments: new Deployments({ sdkProvider: mockCloudExecutable.sdkProvider }), + }); + + // WHEN + await cdkToolkit.deploy({ + selector: { patterns: ['Test-Stack-A'] }, + hotswap: HotswapMode.FULL_DEPLOYMENT, }); - test('fallback to deploy role if bootstrap version parameter not found', async () => { + // THEN + expect(flatten(stderrMock.mock.calls)).not.toEqual(expect.arrayContaining([ + expect.stringMatching(/Could not assume/), + expect.stringMatching(/please upgrade to bootstrap version/), + ])); + expect(mockCloudExecutable.sdkProvider.sdk.ssm).not.toHaveBeenCalled(); + expect(mockForEnvironment.mock.calls.length).toEqual(2); + expect(mockForEnvironment.mock.calls[0][2]).toEqual({ + assumeRoleArn: undefined, + assumeRoleExternalId: undefined, + }); + }); +}); + +describe('deploy', () => { + test('fails when no valid stack names are given', async () => { + // GIVEN + const toolkit = defaultToolkitSetup(); + + // WHEN + await expect(() => toolkit.deploy({ + selector: { patterns: ['Test-Stack-D'] }, + hotswap: HotswapMode.FULL_DEPLOYMENT, + })).rejects.toThrow('No stacks match the name(s) Test-Stack-D'); + }); + + describe('with hotswap deployment', () => { + test("passes through the 'hotswap' option to CloudFormationDeployments.deployStack()", async () => { // GIVEN - mockCloudExecutable.sdkProvider.stubSSM({ - getParameter() { - throw new Error('not found'); - }, - }); + const mockCfnDeployments = instanceMockFrom(Deployments); + mockCfnDeployments.deployStack.mockReturnValue(Promise.resolve({ + noOp: false, + outputs: {}, + stackArn: 'stackArn', + stackArtifact: instanceMockFrom(cxapi.CloudFormationStackArtifact), + })); const cdkToolkit = new CdkToolkit({ - cloudExecutable: mockCloudExecutable, - configuration: mockCloudExecutable.configuration, - sdkProvider: mockCloudExecutable.sdkProvider, - deployments: new Deployments({ sdkProvider: mockCloudExecutable.sdkProvider }), + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: mockCfnDeployments, }); // WHEN await cdkToolkit.deploy({ - selector: { patterns: ['Test-Stack-C'] }, - hotswap: HotswapMode.FULL_DEPLOYMENT, + selector: { patterns: ['Test-Stack-A-Display-Name'] }, + requireApproval: RequireApproval.Never, + hotswap: HotswapMode.FALL_BACK, }); // THEN - expect(flatten(stderrMock.mock.calls)).toEqual(expect.arrayContaining([ - expect.stringMatching(/Could not assume bloop-lookup:here:123456789012/), - ])); - expect(mockForEnvironment.mock.calls.length).toEqual(3); - expect(mockForEnvironment.mock.calls[0][2]).toEqual({ - assumeRoleArn: 'bloop-lookup:here:123456789012', - }); - expect(mockForEnvironment.mock.calls[1][2]).toEqual({ - assumeRoleArn: 'bloop:here:123456789012', - }); + expect(mockCfnDeployments.deployStack).toHaveBeenCalledWith(expect.objectContaining({ + hotswap: HotswapMode.FALL_BACK, + })); }); + }); - test('fallback to deploy role if forEnvironment throws', async () => { + describe('makes correct CloudFormation calls', () => { + test('without options', async () => { // GIVEN - // throw error first for the 'prepareSdkWithLookupRoleFor' call and succeed for the rest - mockForEnvironment = jest.fn().mockImplementationOnce(() => { throw new Error('error'); }) - .mockImplementation(() => { return { sdk: mockCloudExecutable.sdkProvider.sdk, didAssumeRole: true }; }); - mockCloudExecutable.sdkProvider.forEnvironment = mockForEnvironment; - mockCloudExecutable.sdkProvider.stubSSM({ - getParameter() { - return {}; - }, - }); - const cdkToolkit = new CdkToolkit({ - cloudExecutable: mockCloudExecutable, - configuration: mockCloudExecutable.configuration, - sdkProvider: mockCloudExecutable.sdkProvider, - deployments: new Deployments({ sdkProvider: mockCloudExecutable.sdkProvider }), - }); + const toolkit = defaultToolkitSetup(); // WHEN - await cdkToolkit.deploy({ - selector: { patterns: ['Test-Stack-C'] }, + await toolkit.deploy({ + selector: { patterns: ['Test-Stack-A', 'Test-Stack-B'] }, hotswap: HotswapMode.FULL_DEPLOYMENT, }); - - // THEN - expect(mockCloudExecutable.sdkProvider.sdk.ssm).not.toHaveBeenCalled(); - expect(flatten(stderrMock.mock.calls)).toEqual(expect.arrayContaining([ - expect.stringMatching(/Could not assume bloop-lookup:here:123456789012/), - ])); - expect(mockForEnvironment.mock.calls.length).toEqual(3); - expect(mockForEnvironment.mock.calls[0][2]).toEqual({ - assumeRoleArn: 'bloop-lookup:here:123456789012', - }); - expect(mockForEnvironment.mock.calls[1][2]).toEqual({ - assumeRoleArn: 'bloop:here:123456789012', - }); }); - test('dont lookup bootstrap version parameter if default credentials are used', async () => { + test('with stacks all stacks specified as double wildcard', async () => { // GIVEN - mockForEnvironment = jest.fn().mockImplementation(() => { return { sdk: mockCloudExecutable.sdkProvider.sdk, didAssumeRole: false }; }); - mockCloudExecutable.sdkProvider.forEnvironment = mockForEnvironment; - const cdkToolkit = new CdkToolkit({ - cloudExecutable: mockCloudExecutable, - configuration: mockCloudExecutable.configuration, - sdkProvider: mockCloudExecutable.sdkProvider, - deployments: new Deployments({ sdkProvider: mockCloudExecutable.sdkProvider }), - }); - mockCloudExecutable.sdkProvider.stubSSM({ - getParameter() { - return {}; - }, - }); + const toolkit = defaultToolkitSetup(); // WHEN - await cdkToolkit.deploy({ - selector: { patterns: ['Test-Stack-C'] }, + await toolkit.deploy({ + selector: { patterns: ['**'] }, hotswap: HotswapMode.FULL_DEPLOYMENT, }); - - // THEN - expect(flatten(stderrMock.mock.calls)).toEqual(expect.arrayContaining([ - expect.stringMatching(/Lookup role exists but was not assumed. Proceeding with default credentials./), - ])); - expect(mockCloudExecutable.sdkProvider.sdk.ssm).not.toHaveBeenCalled(); - expect(mockForEnvironment.mock.calls.length).toEqual(3); - expect(mockForEnvironment.mock.calls[0][2]).toEqual({ - assumeRoleArn: 'bloop-lookup:here:123456789012', - }); - expect(mockForEnvironment.mock.calls[1][2]).toEqual({ - assumeRoleArn: 'bloop:here:123456789012', - }); }); - test('do not print warnings if lookup role not provided in stack artifact', async () => { + test('with one stack specified', async () => { // GIVEN - mockCloudExecutable.sdkProvider.stubSSM({ - getParameter() { - return {}; - }, - }); - const cdkToolkit = new CdkToolkit({ - cloudExecutable: mockCloudExecutable, - configuration: mockCloudExecutable.configuration, - sdkProvider: mockCloudExecutable.sdkProvider, - deployments: new Deployments({ sdkProvider: mockCloudExecutable.sdkProvider }), - }); + const toolkit = defaultToolkitSetup(); // WHEN - await cdkToolkit.deploy({ - selector: { patterns: ['Test-Stack-A'] }, + await toolkit.deploy({ + selector: { patterns: ['Test-Stack-A-Display-Name'] }, hotswap: HotswapMode.FULL_DEPLOYMENT, }); - - // THEN - expect(flatten(stderrMock.mock.calls)).not.toEqual(expect.arrayContaining([ - expect.stringMatching(/Could not assume/), - expect.stringMatching(/please upgrade to bootstrap version/), - ])); - expect(mockCloudExecutable.sdkProvider.sdk.ssm).not.toHaveBeenCalled(); - expect(mockForEnvironment.mock.calls.length).toEqual(2); - expect(mockForEnvironment.mock.calls[0][2]).toEqual({ - assumeRoleArn: undefined, - assumeRoleExternalId: undefined, - }); }); - }); - describe('deploy', () => { - test('fails when no valid stack names are given', async () => { + test('with stacks all stacks specified as wildcard', async () => { // GIVEN const toolkit = defaultToolkitSetup(); // WHEN - await expect(() => toolkit.deploy({ - selector: { patterns: ['Test-Stack-D'] }, + await toolkit.deploy({ + selector: { patterns: ['*'] }, hotswap: HotswapMode.FULL_DEPLOYMENT, - })).rejects.toThrow('No stacks match the name(s) Test-Stack-D'); + }); }); - describe('with hotswap deployment', () => { - test("passes through the 'hotswap' option to CloudFormationDeployments.deployStack()", async () => { + + describe('sns notification arns', () => { + beforeEach(() => { + cloudExecutable = new MockCloudExecutable({ + stacks: [ + MockStack.MOCK_STACK_A, + MockStack.MOCK_STACK_B, + MockStack.MOCK_STACK_WITH_NOTIFICATION_ARNS, + MockStack.MOCK_STACK_WITH_BAD_NOTIFICATION_ARNS, + ], + }); + }); + + test('with sns notification arns as options', async () => { // GIVEN - const mockCfnDeployments = instanceMockFrom(Deployments); - mockCfnDeployments.deployStack.mockReturnValue(Promise.resolve({ - noOp: false, - outputs: {}, - stackArn: 'stackArn', - stackArtifact: instanceMockFrom(cxapi.CloudFormationStackArtifact), - })); - const cdkToolkit = new CdkToolkit({ + const notificationArns = [ + 'arn:aws:sns:us-east-2:444455556666:MyTopic', + 'arn:aws:sns:eu-west-1:111155556666:my-great-topic', + ]; + const toolkit = new CdkToolkit({ cloudExecutable, configuration: cloudExecutable.configuration, sdkProvider: cloudExecutable.sdkProvider, - deployments: mockCfnDeployments, + deployments: new FakeCloudFormation({ + 'Test-Stack-A': { Foo: 'Bar' }, + }, notificationArns), }); // WHEN - await cdkToolkit.deploy({ + await toolkit.deploy({ + // Stacks should be selected by their hierarchical ID, which is their displayName, not by the stack ID. selector: { patterns: ['Test-Stack-A-Display-Name'] }, - requireApproval: RequireApproval.Never, - hotswap: HotswapMode.FALL_BACK, + notificationArns, + hotswap: HotswapMode.FULL_DEPLOYMENT, + }); + }); + + test('fail with incorrect sns notification arns as options', async () => { + // GIVEN + const notificationArns = ['arn:::cfn-my-cool-topic']; + const toolkit = new CdkToolkit({ + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: new FakeCloudFormation({ + 'Test-Stack-A': { Foo: 'Bar' }, + }, notificationArns), }); - // THEN - expect(mockCfnDeployments.deployStack).toHaveBeenCalledWith(expect.objectContaining({ - hotswap: HotswapMode.FALL_BACK, - })); + // WHEN + await expect(() => + toolkit.deploy({ + // Stacks should be selected by their hierarchical ID, which is their displayName, not by the stack ID. + selector: { patterns: ['Test-Stack-A-Display-Name'] }, + notificationArns, + hotswap: HotswapMode.FULL_DEPLOYMENT, + }), + ).rejects.toThrow('Notification arn arn:::cfn-my-cool-topic is not a valid arn for an SNS topic'); }); - }); - describe('makes correct CloudFormation calls', () => { - test('without options', async () => { + /* + test('with sns notification arns as options', async () => { // GIVEN - const toolkit = defaultToolkitSetup(); + const notificationArns = [ + 'arn:aws:sns:us-east-2:444455556666:MyTopic', + 'arn:aws:sns:eu-west-1:111155556666:my-great-topic', + ]; + const toolkit = new CdkToolkit({ + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: new FakeCloudFormation({ + // Stacks should be selected by their hierarchical ID, which is their displayName, not by the stack ID. + 'Test-Stack-E-Display-Name': { Foo: 'Bar' }, + 'Test-Stack-F-Display-Name': { Foo: 'Bar' }, + }, notificationArns), + }); // WHEN await toolkit.deploy({ - selector: { patterns: ['Test-Stack-A', 'Test-Stack-B'] }, + selector: { patterns: ['Test-Stack-E', 'Test-Stack-F'] }, + notificationArns, hotswap: HotswapMode.FULL_DEPLOYMENT, }); }); - test('with stacks all stacks specified as double wildcard', async () => { + test('fail with incorrect sns notification arns as options', async () => { // GIVEN - const toolkit = defaultToolkitSetup(); + const notificationArns = ['arn:::cfn-my-cool-topic']; + const toolkit = new CdkToolkit({ + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: new FakeCloudFormation({ + 'Test-Stack-E-Display-Name': { Foo: 'Bar' }, + }, notificationArns), + }); // WHEN - await toolkit.deploy({ - selector: { patterns: ['**'] }, - hotswap: HotswapMode.FULL_DEPLOYMENT, - }); + await expect(() => + toolkit.deploy({ + selector: { patterns: ['Test-Stack-E'] }, + notificationArns, + hotswap: HotswapMode.FULL_DEPLOYMENT, + }), + ).rejects.toThrow('Notification arn arn:::cfn-my-cool-topic is not a valid arn for an SNS topic'); }); + */ - test('with one stack specified', async () => { + test('with sns notification arns in the executable', async () => { // GIVEN - const toolkit = defaultToolkitSetup(); + const expectedNotificationArns = [ + 'arn:aws:sns:bermuda-triangle-1337:123456789012:MyTopic', + ]; + const toolkit = new CdkToolkit({ + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: new FakeCloudFormation({ + 'Test-Stack-Notification-Arns': { Foo: 'Bar' }, + }, expectedNotificationArns), + }); // WHEN await toolkit.deploy({ - selector: { patterns: ['Test-Stack-A-Display-Name'] }, + selector: { patterns: ['Test-Stack-Notification-Arns'] }, hotswap: HotswapMode.FULL_DEPLOYMENT, }); }); - test('with stacks all stacks specified as wildcard', async () => { + test('fail with incorrect sns notification arns in the executable', async () => { // GIVEN - const toolkit = defaultToolkitSetup(); + const toolkit = new CdkToolkit({ + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: new FakeCloudFormation({ + 'Test-Stack-Bad-Notification-Arns': { Foo: 'Bar' }, + }), + }); + + // WHEN + await expect(() => + toolkit.deploy({ + selector: { patterns: ['Test-Stack-Bad-Notification-Arns'] }, + hotswap: HotswapMode.FULL_DEPLOYMENT, + }), + ).rejects.toThrow('Notification arn arn:1337:123456789012:sns:bad is not a valid arn for an SNS topic'); + }); + + test('with sns notification arns in the executable and as options', async () => { + // GIVEN + const notificationArns = [ + 'arn:aws:sns:us-east-2:444455556666:MyTopic', + 'arn:aws:sns:eu-west-1:111155556666:my-great-topic', + ]; + + const expectedNotificationArns = notificationArns.concat(['arn:aws:sns:bermuda-triangle-1337:123456789012:MyTopic']); + const toolkit = new CdkToolkit({ + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: new FakeCloudFormation({ + 'Test-Stack-Notification-Arns': { Foo: 'Bar' }, + }, expectedNotificationArns), + }); // WHEN await toolkit.deploy({ - selector: { patterns: ['*'] }, + selector: { patterns: ['Test-Stack-Notification-Arns'] }, + notificationArns, hotswap: HotswapMode.FULL_DEPLOYMENT, }); }); - describe('sns notification arns', () => { - beforeEach(() => { - cloudExecutable = new MockCloudExecutable({ - stacks: [ - MockStack.MOCK_STACK_E, - MockStack.MOCK_STACK_F, - MockStack.MOCK_STACK_WITH_NOTIFICATION_ARNS, - MockStack.MOCK_STACK_WITH_BAD_NOTIFICATION_ARNS, - ], - }); + test('fail with incorrect sns notification arns in the executable and incorrect sns notification arns as options', async () => { + // GIVEN + const notificationArns = ['arn:::cfn-my-cool-topic']; + const toolkit = new CdkToolkit({ + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: new FakeCloudFormation({ + 'Test-Stack-Bad-Notification-Arns': { Foo: 'Bar' }, + }, notificationArns), }); - test('with sns notification arns as options', async () => { - // GIVEN - const notificationArns = [ - 'arn:aws:sns:us-east-2:444455556666:MyTopic', - 'arn:aws:sns:eu-west-1:111155556666:my-great-topic', - ]; - const toolkit = new CdkToolkit({ - cloudExecutable, - configuration: cloudExecutable.configuration, - sdkProvider: cloudExecutable.sdkProvider, - deployments: new FakeCloudFormation({ - // Stacks should be selected by their hierarchical ID, which is their displayName, not by the stack ID. - 'Test-Stack-E-Display-Name': { Foo: 'Bar' }, - 'Test-Stack-F-Display-Name': { Foo: 'Bar' }, - }, notificationArns), - }); - - // WHEN - await toolkit.deploy({ - selector: { patterns: ['Test-Stack-E', 'Test-Stack-F'] }, + // WHEN + await expect(() => + toolkit.deploy({ + selector: { patterns: ['Test-Stack-Bad-Notification-Arns'] }, notificationArns, hotswap: HotswapMode.FULL_DEPLOYMENT, - }); - }); + }), + ).rejects.toThrow('Notification arn arn:::cfn-my-cool-topic is not a valid arn for an SNS topic'); + }); - test('fail with incorrect sns notification arns as options', async () => { - // GIVEN - const notificationArns = ['arn:::cfn-my-cool-topic']; - const toolkit = new CdkToolkit({ - cloudExecutable, - configuration: cloudExecutable.configuration, - sdkProvider: cloudExecutable.sdkProvider, - deployments: new FakeCloudFormation({ - 'Test-Stack-E-Display-Name': { Foo: 'Bar' }, - }, notificationArns), - }); - - // WHEN - await expect(() => - toolkit.deploy({ - selector: { patterns: ['Test-Stack-E'] }, - notificationArns, - hotswap: HotswapMode.FULL_DEPLOYMENT, - }), - ).rejects.toThrow('Notification arn arn:::cfn-my-cool-topic is not a valid arn for an SNS topic'); + test('fail with incorrect sns notification arns in the executable and correct sns notification arns as options', async () => { + // GIVEN + const notificationArns = ['arn:aws:sns:bermuda-triangle-1337:123456789012:MyTopic']; + const toolkit = new CdkToolkit({ + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: new FakeCloudFormation({ + 'Test-Stack-Bad-Notification-Arns': { Foo: 'Bar' }, + }, notificationArns), }); - test('with sns notification arns in the executable', async () => { - // GIVEN - const expectedNotificationArns = [ - 'arn:aws:sns:bermuda-triangle-1337:123456789012:MyTopic', - ]; - const toolkit = new CdkToolkit({ - cloudExecutable, - configuration: cloudExecutable.configuration, - sdkProvider: cloudExecutable.sdkProvider, - deployments: new FakeCloudFormation({ - 'Test-Stack-Notification-Arns': { Foo: 'Bar' }, - }, expectedNotificationArns), - }); - - // WHEN - await toolkit.deploy({ - selector: { patterns: ['Test-Stack-Notification-Arns'] }, + // WHEN + await expect(() => + toolkit.deploy({ + selector: { patterns: ['Test-Stack-Bad-Notification-Arns'] }, + notificationArns, hotswap: HotswapMode.FULL_DEPLOYMENT, - }); - }); + }), + ).rejects.toThrow('Notification arn arn:1337:123456789012:sns:bad is not a valid arn for an SNS topic'); + }); - test('fail with incorrect sns notification arns in the executable', async () => { - // GIVEN - const toolkit = new CdkToolkit({ - cloudExecutable, - configuration: cloudExecutable.configuration, - sdkProvider: cloudExecutable.sdkProvider, - deployments: new FakeCloudFormation({ - 'Test-Stack-Bad-Notification-Arns': { Foo: 'Bar' }, - }), - }); - - // WHEN - await expect(() => - toolkit.deploy({ - selector: { patterns: ['Test-Stack-Bad-Notification-Arns'] }, - hotswap: HotswapMode.FULL_DEPLOYMENT, - }), - ).rejects.toThrow('Notification arn arn:1337:123456789012:sns:bad is not a valid arn for an SNS topic'); + test('fail with correct sns notification arns in the executable and incorrect sns notification arns as options', async () => { + // GIVEN + const notificationArns = ['arn:::cfn-my-cool-topic']; + const toolkit = new CdkToolkit({ + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: new FakeCloudFormation({ + 'Test-Stack-Notification-Arns': { Foo: 'Bar' }, + }, notificationArns), }); - test('with sns notification arns in the executable and as options', async () => { - // GIVEN - const notificationArns = [ - 'arn:aws:sns:us-east-2:444455556666:MyTopic', - 'arn:aws:sns:eu-west-1:111155556666:my-great-topic', - ]; - - const expectedNotificationArns = notificationArns.concat(['arn:aws:sns:bermuda-triangle-1337:123456789012:MyTopic']); - const toolkit = new CdkToolkit({ - cloudExecutable, - configuration: cloudExecutable.configuration, - sdkProvider: cloudExecutable.sdkProvider, - deployments: new FakeCloudFormation({ - 'Test-Stack-Notification-Arns': { Foo: 'Bar' }, - }, expectedNotificationArns), - }); - - // WHEN - await toolkit.deploy({ + // WHEN + await expect(() => + toolkit.deploy({ selector: { patterns: ['Test-Stack-Notification-Arns'] }, notificationArns, hotswap: HotswapMode.FULL_DEPLOYMENT, - }); - }); - - test('fail with incorrect sns notification arns in the executable and incorrect sns notification arns as options', async () => { - // GIVEN - const notificationArns = ['arn:::cfn-my-cool-topic']; - const toolkit = new CdkToolkit({ - cloudExecutable, - configuration: cloudExecutable.configuration, - sdkProvider: cloudExecutable.sdkProvider, - deployments: new FakeCloudFormation({ - 'Test-Stack-Bad-Notification-Arns': { Foo: 'Bar' }, - }, notificationArns), - }); - - // WHEN - await expect(() => - toolkit.deploy({ - selector: { patterns: ['Test-Stack-Bad-Notification-Arns'] }, - notificationArns, - hotswap: HotswapMode.FULL_DEPLOYMENT, - }), - ).rejects.toThrow('Notification arn arn:::cfn-my-cool-topic is not a valid arn for an SNS topic'); - }); - - test('fail with incorrect sns notification arns in the executable and correct sns notification arns as options', async () => { - // GIVEN - const notificationArns = ['arn:aws:sns:bermuda-triangle-1337:123456789012:MyTopic']; - const toolkit = new CdkToolkit({ - cloudExecutable, - configuration: cloudExecutable.configuration, - sdkProvider: cloudExecutable.sdkProvider, - deployments: new FakeCloudFormation({ - 'Test-Stack-Bad-Notification-Arns': { Foo: 'Bar' }, - }, notificationArns), - }); - - // WHEN - await expect(() => - toolkit.deploy({ - selector: { patterns: ['Test-Stack-Bad-Notification-Arns'] }, - notificationArns, - hotswap: HotswapMode.FULL_DEPLOYMENT, - }), - ).rejects.toThrow('Notification arn arn:1337:123456789012:sns:bad is not a valid arn for an SNS topic'); - }); - - test('fail with correct sns notification arns in the executable and incorrect sns notification arns as options', async () => { - // GIVEN - const notificationArns = ['arn:::cfn-my-cool-topic']; - const toolkit = new CdkToolkit({ - cloudExecutable, - configuration: cloudExecutable.configuration, - sdkProvider: cloudExecutable.sdkProvider, - deployments: new FakeCloudFormation({ - 'Test-Stack-Notification-Arns': { Foo: 'Bar' }, - }, notificationArns), - }); - - // WHEN - await expect(() => - toolkit.deploy({ - selector: { patterns: ['Test-Stack-Notification-Arns'] }, - notificationArns, - hotswap: HotswapMode.FULL_DEPLOYMENT, - }), - ).rejects.toThrow('Notification arn arn:::cfn-my-cool-topic is not a valid arn for an SNS topic'); - }); + }), + ).rejects.toThrow('Notification arn arn:::cfn-my-cool-topic is not a valid arn for an SNS topic'); }); }); + }); - test('globless bootstrap uses environment without question', async () => { - // GIVEN - const toolkit = defaultToolkitSetup(); + test('globless bootstrap uses environment without question', async () => { + // GIVEN + const toolkit = defaultToolkitSetup(); - // WHEN - await toolkit.bootstrap(['aws://56789/south-pole'], bootstrapper, {}); + // WHEN + await toolkit.bootstrap(['aws://56789/south-pole'], bootstrapper, {}); - // THEN - expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledWith({ - account: '56789', - region: 'south-pole', - name: 'aws://56789/south-pole', - }, expect.anything(), expect.anything()); - expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledTimes(1); - }); + // THEN + expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledWith({ + account: '56789', + region: 'south-pole', + name: 'aws://56789/south-pole', + }, expect.anything(), expect.anything()); + expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledTimes(1); + }); - test('globby bootstrap uses whats in the stacks', async () => { - // GIVEN - const toolkit = defaultToolkitSetup(); - cloudExecutable.configuration.settings.set(['app'], 'something'); + test('globby bootstrap uses whats in the stacks', async () => { + // GIVEN + const toolkit = defaultToolkitSetup(); + cloudExecutable.configuration.settings.set(['app'], 'something'); + + // WHEN + await toolkit.bootstrap(['aws://*/bermuda-triangle-1'], bootstrapper, {}); + + // THEN + expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledWith({ + account: '123456789012', + region: 'bermuda-triangle-1', + name: 'aws://123456789012/bermuda-triangle-1', + }, expect.anything(), expect.anything()); + expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledTimes(1); + }); - // WHEN - await toolkit.bootstrap(['aws://*/bermuda-triangle-1'], bootstrapper, {}); + test('bootstrap can be invoked without the --app argument', async () => { + // GIVEN + cloudExecutable.configuration.settings.clear(); + const mockSynthesize = jest.fn(); + cloudExecutable.synthesize = mockSynthesize; - // THEN - expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledWith({ - account: '123456789012', - region: 'bermuda-triangle-1', - name: 'aws://123456789012/bermuda-triangle-1', - }, expect.anything(), expect.anything()); - expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledTimes(1); - }); + const toolkit = defaultToolkitSetup(); - test('bootstrap can be invoked without the --app argument', async () => { - // GIVEN - cloudExecutable.configuration.settings.clear(); - const mockSynthesize = jest.fn(); - cloudExecutable.synthesize = mockSynthesize; + // WHEN + await toolkit.bootstrap(['aws://123456789012/west-pole'], bootstrapper, {}); - const toolkit = defaultToolkitSetup(); + // THEN + expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledWith({ + account: '123456789012', + region: 'west-pole', + name: 'aws://123456789012/west-pole', + }, expect.anything(), expect.anything()); + expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledTimes(1); - // WHEN - await toolkit.bootstrap(['aws://123456789012/west-pole'], bootstrapper, {}); + expect(cloudExecutable.hasApp).toEqual(false); + expect(mockSynthesize).not.toHaveBeenCalled(); + }); +}); - // THEN - expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledWith({ - account: '123456789012', - region: 'west-pole', - name: 'aws://123456789012/west-pole', - }, expect.anything(), expect.anything()); - expect(bootstrapper.bootstrapEnvironment).toHaveBeenCalledTimes(1); +describe('destroy', () => { + test('destroy correct stack', async () => { + const toolkit = destroyToolkitSetup(); - expect(cloudExecutable.hasApp).toEqual(false); - expect(mockSynthesize).not.toHaveBeenCalled(); - }); + expect(() => { + return toolkit.destroy({ + selector: { patterns: ['Test-Stack-Destroy'] }, + exclusively: true, + force: true, + fromDeploy: true, + }); + }).resolves; }); +}); - describe('destroy', () => { - test('destroy correct stack', async () => { - const toolkit = destroyToolkitSetup(); +describe('watch', () => { + test("fails when no 'watch' settings are found", async () => { + const toolkit = defaultToolkitSetup(); - expect(() => { - return toolkit.destroy({ - selector: { patterns: ['Test-Stack-Destroy'] }, - exclusively: true, - force: true, - fromDeploy: true, - }); - }).resolves; - }); + await expect(() => { + return toolkit.watch({ + selector: { patterns: [] }, + hotswap: HotswapMode.HOTSWAP_ONLY, + }); + }).rejects.toThrow("Cannot use the 'watch' command without specifying at least one directory to monitor. " + + 'Make sure to add a "watch" key to your cdk.json'); }); - describe('watch', () => { - test("fails when no 'watch' settings are found", async () => { - const toolkit = defaultToolkitSetup(); + test('observes only the root directory by default', async () => { + cloudExecutable.configuration.settings.set(['watch'], {}); + const toolkit = defaultToolkitSetup(); - await expect(() => { - return toolkit.watch({ - selector: { patterns: [] }, - hotswap: HotswapMode.HOTSWAP_ONLY, - }); - }).rejects.toThrow("Cannot use the 'watch' command without specifying at least one directory to monitor. " + - 'Make sure to add a "watch" key to your cdk.json'); + await toolkit.watch({ + selector: { patterns: [] }, + hotswap: HotswapMode.HOTSWAP_ONLY, }); - test('observes only the root directory by default', async () => { - cloudExecutable.configuration.settings.set(['watch'], {}); - const toolkit = defaultToolkitSetup(); + const includeArgs = fakeChokidarWatch.includeArgs; + expect(includeArgs.length).toBe(1); + }); - await toolkit.watch({ - selector: { patterns: [] }, - hotswap: HotswapMode.HOTSWAP_ONLY, - }); + test("allows providing a single string in 'watch.include'", async () => { + cloudExecutable.configuration.settings.set(['watch'], { + include: 'my-dir', + }); + const toolkit = defaultToolkitSetup(); - const includeArgs = fakeChokidarWatch.includeArgs; - expect(includeArgs.length).toBe(1); + await toolkit.watch({ + selector: { patterns: [] }, + hotswap: HotswapMode.HOTSWAP_ONLY, }); - test("allows providing a single string in 'watch.include'", async () => { - cloudExecutable.configuration.settings.set(['watch'], { - include: 'my-dir', - }); - const toolkit = defaultToolkitSetup(); + expect(fakeChokidarWatch.includeArgs).toStrictEqual(['my-dir']); + }); - await toolkit.watch({ - selector: { patterns: [] }, - hotswap: HotswapMode.HOTSWAP_ONLY, - }); + test("allows providing an array of strings in 'watch.include'", async () => { + cloudExecutable.configuration.settings.set(['watch'], { + include: ['my-dir1', '**/my-dir2/*'], + }); + const toolkit = defaultToolkitSetup(); - expect(fakeChokidarWatch.includeArgs).toStrictEqual(['my-dir']); + await toolkit.watch({ + selector: { patterns: [] }, + hotswap: HotswapMode.HOTSWAP_ONLY, }); - test("allows providing an array of strings in 'watch.include'", async () => { - cloudExecutable.configuration.settings.set(['watch'], { - include: ['my-dir1', '**/my-dir2/*'], - }); - const toolkit = defaultToolkitSetup(); + expect(fakeChokidarWatch.includeArgs).toStrictEqual(['my-dir1', '**/my-dir2/*']); + }); - await toolkit.watch({ - selector: { patterns: [] }, - hotswap: HotswapMode.HOTSWAP_ONLY, - }); + test('ignores the output dir, dot files, dot directories, and node_modules by default', async () => { + cloudExecutable.configuration.settings.set(['watch'], {}); + cloudExecutable.configuration.settings.set(['output'], 'cdk.out'); + const toolkit = defaultToolkitSetup(); - expect(fakeChokidarWatch.includeArgs).toStrictEqual(['my-dir1', '**/my-dir2/*']); + await toolkit.watch({ + selector: { patterns: [] }, + hotswap: HotswapMode.HOTSWAP_ONLY, }); - test('ignores the output dir, dot files, dot directories, and node_modules by default', async () => { - cloudExecutable.configuration.settings.set(['watch'], {}); - cloudExecutable.configuration.settings.set(['output'], 'cdk.out'); - const toolkit = defaultToolkitSetup(); + expect(fakeChokidarWatch.excludeArgs).toStrictEqual([ + 'cdk.out/**', + '**/.*', + '**/.*/**', + '**/node_modules/**', + ]); + }); - await toolkit.watch({ - selector: { patterns: [] }, - hotswap: HotswapMode.HOTSWAP_ONLY, - }); + test("allows providing a single string in 'watch.exclude'", async () => { + cloudExecutable.configuration.settings.set(['watch'], { + exclude: 'my-dir', + }); + const toolkit = defaultToolkitSetup(); - expect(fakeChokidarWatch.excludeArgs).toStrictEqual([ - 'cdk.out/**', - '**/.*', - '**/.*/**', - '**/node_modules/**', - ]); + await toolkit.watch({ + selector: { patterns: [] }, + hotswap: HotswapMode.HOTSWAP_ONLY, }); - test("allows providing a single string in 'watch.exclude'", async () => { - cloudExecutable.configuration.settings.set(['watch'], { - exclude: 'my-dir', - }); - const toolkit = defaultToolkitSetup(); + const excludeArgs = fakeChokidarWatch.excludeArgs; + expect(excludeArgs.length).toBe(5); + expect(excludeArgs[0]).toBe('my-dir'); + }); - await toolkit.watch({ - selector: { patterns: [] }, - hotswap: HotswapMode.HOTSWAP_ONLY, - }); + test("allows providing an array of strings in 'watch.exclude'", async () => { + cloudExecutable.configuration.settings.set(['watch'], { + exclude: ['my-dir1', '**/my-dir2'], + }); + const toolkit = defaultToolkitSetup(); - const excludeArgs = fakeChokidarWatch.excludeArgs; - expect(excludeArgs.length).toBe(5); - expect(excludeArgs[0]).toBe('my-dir'); + await toolkit.watch({ + selector: { patterns: [] }, + hotswap: HotswapMode.HOTSWAP_ONLY, }); - test("allows providing an array of strings in 'watch.exclude'", async () => { - cloudExecutable.configuration.settings.set(['watch'], { - exclude: ['my-dir1', '**/my-dir2'], - }); - const toolkit = defaultToolkitSetup(); + const excludeArgs = fakeChokidarWatch.excludeArgs; + expect(excludeArgs.length).toBe(6); + expect(excludeArgs[0]).toBe('my-dir1'); + expect(excludeArgs[1]).toBe('**/my-dir2'); + }); - await toolkit.watch({ - selector: { patterns: [] }, - hotswap: HotswapMode.HOTSWAP_ONLY, - }); + test('allows watching with deploy concurrency', async () => { + cloudExecutable.configuration.settings.set(['watch'], {}); + const toolkit = defaultToolkitSetup(); + const cdkDeployMock = jest.fn(); + toolkit.deploy = cdkDeployMock; - const excludeArgs = fakeChokidarWatch.excludeArgs; - expect(excludeArgs.length).toBe(6); - expect(excludeArgs[0]).toBe('my-dir1'); - expect(excludeArgs[1]).toBe('**/my-dir2'); + await toolkit.watch({ + selector: { patterns: [] }, + concurrency: 3, + hotswap: HotswapMode.HOTSWAP_ONLY, }); + fakeChokidarWatcherOn.readyCallback(); + + expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ concurrency: 3 })); + }); - test('allows watching with deploy concurrency', async () => { + describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hotswapMode) => { + test('passes through the correct hotswap mode to deployStack()', async () => { cloudExecutable.configuration.settings.set(['watch'], {}); const toolkit = defaultToolkitSetup(); const cdkDeployMock = jest.fn(); toolkit.deploy = cdkDeployMock; - await toolkit.watch({ - selector: { patterns: [] }, - concurrency: 3, - hotswap: HotswapMode.HOTSWAP_ONLY, - }); + await toolkit.watch({ selector: { patterns: [] }, hotswap: hotswapMode }); fakeChokidarWatcherOn.readyCallback(); - expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ concurrency: 3 })); + expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ hotswap: hotswapMode })); }); + }); - describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hotswapMode) => { - test('passes through the correct hotswap mode to deployStack()', async () => { - cloudExecutable.configuration.settings.set(['watch'], {}); - const toolkit = defaultToolkitSetup(); - const cdkDeployMock = jest.fn(); - toolkit.deploy = cdkDeployMock; + test('respects HotswapMode.HOTSWAP_ONLY', async () => { + cloudExecutable.configuration.settings.set(['watch'], {}); + const toolkit = defaultToolkitSetup(); + const cdkDeployMock = jest.fn(); + toolkit.deploy = cdkDeployMock; - await toolkit.watch({ selector: { patterns: [] }, hotswap: hotswapMode }); - fakeChokidarWatcherOn.readyCallback(); + await toolkit.watch({ selector: { patterns: [] }, hotswap: HotswapMode.HOTSWAP_ONLY }); + fakeChokidarWatcherOn.readyCallback(); - expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ hotswap: hotswapMode })); - }); - }); + expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ hotswap: HotswapMode.HOTSWAP_ONLY })); + }); - test('respects HotswapMode.HOTSWAP_ONLY', async () => { - cloudExecutable.configuration.settings.set(['watch'], {}); - const toolkit = defaultToolkitSetup(); - const cdkDeployMock = jest.fn(); - toolkit.deploy = cdkDeployMock; + test('respects HotswapMode.FALL_BACK', async () => { + cloudExecutable.configuration.settings.set(['watch'], {}); + const toolkit = defaultToolkitSetup(); + const cdkDeployMock = jest.fn(); + toolkit.deploy = cdkDeployMock; - await toolkit.watch({ selector: { patterns: [] }, hotswap: HotswapMode.HOTSWAP_ONLY }); - fakeChokidarWatcherOn.readyCallback(); + await toolkit.watch({ selector: { patterns: [] }, hotswap: HotswapMode.FALL_BACK }); + fakeChokidarWatcherOn.readyCallback(); - expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ hotswap: HotswapMode.HOTSWAP_ONLY })); - }); + expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ hotswap: HotswapMode.FALL_BACK })); + }); - test('respects HotswapMode.FALL_BACK', async () => { - cloudExecutable.configuration.settings.set(['watch'], {}); - const toolkit = defaultToolkitSetup(); - const cdkDeployMock = jest.fn(); - toolkit.deploy = cdkDeployMock; + test('respects HotswapMode.FULL_DEPLOYMENT', async () => { + cloudExecutable.configuration.settings.set(['watch'], {}); + const toolkit = defaultToolkitSetup(); + const cdkDeployMock = jest.fn(); + toolkit.deploy = cdkDeployMock; - await toolkit.watch({ selector: { patterns: [] }, hotswap: HotswapMode.FALL_BACK }); - fakeChokidarWatcherOn.readyCallback(); + await toolkit.watch({ selector: { patterns: [] }, hotswap: HotswapMode.FULL_DEPLOYMENT }); + fakeChokidarWatcherOn.readyCallback(); - expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ hotswap: HotswapMode.FALL_BACK })); - }); + expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ hotswap: HotswapMode.FULL_DEPLOYMENT })); + }); + + describe('with file change events', () => { + let toolkit: CdkToolkit; + let cdkDeployMock: jest.Mock; - test('respects HotswapMode.FULL_DEPLOYMENT', async () => { + beforeEach(async () => { cloudExecutable.configuration.settings.set(['watch'], {}); - const toolkit = defaultToolkitSetup(); - const cdkDeployMock = jest.fn(); + toolkit = defaultToolkitSetup(); + cdkDeployMock = jest.fn(); toolkit.deploy = cdkDeployMock; + await toolkit.watch({ + selector: { patterns: [] }, + hotswap: HotswapMode.HOTSWAP_ONLY, + }); + }); - await toolkit.watch({ selector: { patterns: [] }, hotswap: HotswapMode.FULL_DEPLOYMENT }); - fakeChokidarWatcherOn.readyCallback(); + test("does not trigger a 'deploy' before the 'ready' event has fired", async () => { + await fakeChokidarWatcherOn.fileEventCallback('add', 'my-file'); - expect(cdkDeployMock).toBeCalledWith(expect.objectContaining({ hotswap: HotswapMode.FULL_DEPLOYMENT })); + expect(cdkDeployMock).not.toHaveBeenCalled(); }); - describe('with file change events', () => { - let toolkit: CdkToolkit; - let cdkDeployMock: jest.Mock; + describe("when the 'ready' event has already fired", () => { + beforeEach(() => { + // The ready callback triggers a deployment so each test + // that uses this function will see 'cdkDeployMock' called + // an additional time. + fakeChokidarWatcherOn.readyCallback(); + }); - beforeEach(async () => { - cloudExecutable.configuration.settings.set(['watch'], {}); - toolkit = defaultToolkitSetup(); - cdkDeployMock = jest.fn(); - toolkit.deploy = cdkDeployMock; - await toolkit.watch({ - selector: { patterns: [] }, - hotswap: HotswapMode.HOTSWAP_ONLY, - }); + test("an initial 'deploy' is triggered, without any file changes", async () => { + expect(cdkDeployMock).toHaveBeenCalledTimes(1); }); - test("does not trigger a 'deploy' before the 'ready' event has fired", async () => { + test("does trigger a 'deploy' for a file change", async () => { await fakeChokidarWatcherOn.fileEventCallback('add', 'my-file'); - expect(cdkDeployMock).not.toHaveBeenCalled(); + expect(cdkDeployMock).toHaveBeenCalledTimes(2); }); - describe("when the 'ready' event has already fired", () => { - beforeEach(() => { - // The ready callback triggers a deployment so each test - // that uses this function will see 'cdkDeployMock' called - // an additional time. - fakeChokidarWatcherOn.readyCallback(); - }); + test("triggers a 'deploy' twice for two file changes", async () => { + await Promise.all([ + fakeChokidarWatcherOn.fileEventCallback('add', 'my-file1'), + fakeChokidarWatcherOn.fileEventCallback('change', 'my-file2'), + ]); - test("an initial 'deploy' is triggered, without any file changes", async () => { - expect(cdkDeployMock).toHaveBeenCalledTimes(1); - }); + expect(cdkDeployMock).toHaveBeenCalledTimes(3); + }); - test("does trigger a 'deploy' for a file change", async () => { - await fakeChokidarWatcherOn.fileEventCallback('add', 'my-file'); + test("batches file changes that happen during 'deploy'", async () => { + await Promise.all([ + fakeChokidarWatcherOn.fileEventCallback('add', 'my-file1'), + fakeChokidarWatcherOn.fileEventCallback('change', 'my-file2'), + fakeChokidarWatcherOn.fileEventCallback('unlink', 'my-file3'), + fakeChokidarWatcherOn.fileEventCallback('add', 'my-file4'), + ]); - expect(cdkDeployMock).toHaveBeenCalledTimes(2); - }); + expect(cdkDeployMock).toHaveBeenCalledTimes(3); + }); + }); + }); +}); - test("triggers a 'deploy' twice for two file changes", async () => { - await Promise.all([ - fakeChokidarWatcherOn.fileEventCallback('add', 'my-file1'), - fakeChokidarWatcherOn.fileEventCallback('change', 'my-file2'), - ]); +describe('synth', () => { + test('successful synth outputs hierarchical stack ids', async () => { + const toolkit = defaultToolkitSetup(); + await toolkit.synth([], false, false); - expect(cdkDeployMock).toHaveBeenCalledTimes(3); - }); + // Separate tests as colorizing hampers detection + expect(stderrMock.mock.calls[1][0]).toMatch('Test-Stack-A-Display-Name'); + expect(stderrMock.mock.calls[1][0]).toMatch('Test-Stack-B'); + }); - test("batches file changes that happen during 'deploy'", async () => { - await Promise.all([ - fakeChokidarWatcherOn.fileEventCallback('add', 'my-file1'), - fakeChokidarWatcherOn.fileEventCallback('change', 'my-file2'), - fakeChokidarWatcherOn.fileEventCallback('unlink', 'my-file3'), - fakeChokidarWatcherOn.fileEventCallback('add', 'my-file4'), - ]); + test('with no stdout option', async () => { + // GIVE + const toolkit = defaultToolkitSetup(); - expect(cdkDeployMock).toHaveBeenCalledTimes(3); - }); - }); - }); + // THEN + await toolkit.synth(['Test-Stack-A-Display-Name'], false, true); + expect(mockData.mock.calls.length).toEqual(0); }); - describe('synth', () => { - test('successful synth outputs hierarchical stack ids', async () => { - const toolkit = defaultToolkitSetup(); - await toolkit.synth([], false, false); + describe('migrate', () => { + const testResourcePath = [__dirname, 'commands', 'test-resources']; + const templatePath = [...testResourcePath, 'templates']; + const sqsTemplatePath = path.join(...templatePath, 'sqs-template.json'); + const autoscalingTemplatePath = path.join(...templatePath, 'autoscaling-template.yml'); + const s3TemplatePath = path.join(...templatePath, 's3-template.json'); - // Separate tests as colorizing hampers detection - expect(stderrMock.mock.calls[1][0]).toMatch('Test-Stack-A-Display-Name'); - expect(stderrMock.mock.calls[1][0]).toMatch('Test-Stack-B'); + test('migrate fails when both --from-path and --from-stack are provided', async () => { + const toolkit = defaultToolkitSetup(); + await expect(() => toolkit.migrate({ + stackName: 'no-source', + fromPath: './here/template.yml', + fromStack: true, + })).rejects.toThrow('Only one of `--from-path` or `--from-stack` may be provided.'); + expect(stderrMock.mock.calls[1][0]).toContain(' ❌ Migrate failed for `no-source`: Only one of `--from-path` or `--from-stack` may be provided.'); }); - test('with no stdout option', async () => { - // GIVE + test('migrate fails when --from-path is invalid', async () => { const toolkit = defaultToolkitSetup(); - - // THEN - await toolkit.synth(['Test-Stack-A-Display-Name'], false, true); - expect(mockData.mock.calls.length).toEqual(0); + await expect(() => toolkit.migrate({ + stackName: 'bad-local-source', + fromPath: './here/template.yml', + })).rejects.toThrow('\'./here/template.yml\' is not a valid path.'); + expect(stderrMock.mock.calls[1][0]).toContain(' ❌ Migrate failed for `bad-local-source`: \'./here/template.yml\' is not a valid path.'); }); - describe('migrate', () => { - const testResourcePath = [__dirname, 'commands', 'test-resources']; - const templatePath = [...testResourcePath, 'templates']; - const sqsTemplatePath = path.join(...templatePath, 'sqs-template.json'); - const autoscalingTemplatePath = path.join(...templatePath, 'autoscaling-template.yml'); - const s3TemplatePath = path.join(...templatePath, 's3-template.json'); - - test('migrate fails when both --from-path and --from-stack are provided', async () => { - const toolkit = defaultToolkitSetup(); - await expect(() => toolkit.migrate({ - stackName: 'no-source', - fromPath: './here/template.yml', - fromStack: true, - })).rejects.toThrow('Only one of `--from-path` or `--from-stack` may be provided.'); - expect(stderrMock.mock.calls[1][0]).toContain(' ❌ Migrate failed for `no-source`: Only one of `--from-path` or `--from-stack` may be provided.'); - }); - - test('migrate fails when --from-path is invalid', async () => { - const toolkit = defaultToolkitSetup(); - await expect(() => toolkit.migrate({ - stackName: 'bad-local-source', - fromPath: './here/template.yml', - })).rejects.toThrow('\'./here/template.yml\' is not a valid path.'); - expect(stderrMock.mock.calls[1][0]).toContain(' ❌ Migrate failed for `bad-local-source`: \'./here/template.yml\' is not a valid path.'); + test('migrate fails when --from-stack is used and stack does not exist in account', async () => { + const mockSdkProvider = new MockSdkProvider(); + mockSdkProvider.stubCloudFormation({ + describeStacks(_request) { + throw new Error('Stack does not exist in this environment'); + }, }); - test('migrate fails when --from-stack is used and stack does not exist in account', async () => { - const mockSdkProvider = new MockSdkProvider(); - mockSdkProvider.stubCloudFormation({ - describeStacks(_request) { - throw new Error('Stack does not exist in this environment'); - }, - }); - - const mockCloudExecutable = new MockCloudExecutable({ - stacks: [], - }); - - const cdkToolkit = new CdkToolkit({ - cloudExecutable: mockCloudExecutable, - deployments: new Deployments({ sdkProvider: mockSdkProvider }), - sdkProvider: mockSdkProvider, - configuration: mockCloudExecutable.configuration, - }); - - await expect(() => cdkToolkit.migrate({ - stackName: 'bad-cloudformation-source', - fromStack: true, - })).rejects.toThrowError('Stack does not exist in this environment'); - expect(stderrMock.mock.calls[1][0]).toContain(' ❌ Migrate failed for `bad-cloudformation-source`: Stack does not exist in this environment'); + const mockCloudExecutable = new MockCloudExecutable({ + stacks: [], }); - test('migrate fails when stack cannot be generated', async () => { - const toolkit = defaultToolkitSetup(); - await expect(() => toolkit.migrate({ - stackName: 'cannot-generate-template', - fromPath: path.join(__dirname, 'commands', 'test-resources', 'templates', 'sqs-template.json'), - language: 'rust', - })).rejects.toThrowError('CannotGenerateTemplateStack could not be generated because rust is not a supported language'); - expect(stderrMock.mock.calls[1][0]).toContain(' ❌ Migrate failed for `cannot-generate-template`: CannotGenerateTemplateStack could not be generated because rust is not a supported language'); + const cdkToolkit = new CdkToolkit({ + cloudExecutable: mockCloudExecutable, + deployments: new Deployments({ sdkProvider: mockSdkProvider }), + sdkProvider: mockSdkProvider, + configuration: mockCloudExecutable.configuration, }); - cliTest('migrate succeeds for valid template from local path when no lanugage is provided', async (workDir) => { - const toolkit = defaultToolkitSetup(); - await toolkit.migrate({ - stackName: 'SQSTypeScript', - fromPath: sqsTemplatePath, - outputPath: workDir, - }); - - // Packages created for typescript - expect(fs.pathExistsSync(path.join(workDir, 'SQSTypeScript', 'package.json'))).toBeTruthy(); - expect(fs.pathExistsSync(path.join(workDir, 'SQSTypeScript', 'bin', 'sqs_type_script.ts'))).toBeTruthy(); - expect(fs.pathExistsSync(path.join(workDir, 'SQSTypeScript', 'lib', 'sqs_type_script-stack.ts'))).toBeTruthy(); - }); + await expect(() => cdkToolkit.migrate({ + stackName: 'bad-cloudformation-source', + fromStack: true, + })).rejects.toThrowError('Stack does not exist in this environment'); + expect(stderrMock.mock.calls[1][0]).toContain(' ❌ Migrate failed for `bad-cloudformation-source`: Stack does not exist in this environment'); + }); - cliTest('migrate succeeds for valid template from local path when lanugage is provided', async (workDir) => { - const toolkit = defaultToolkitSetup(); - await toolkit.migrate({ - stackName: 'S3Python', - fromPath: s3TemplatePath, - outputPath: workDir, - language: 'python', - }); + test('migrate fails when stack cannot be generated', async () => { + const toolkit = defaultToolkitSetup(); + await expect(() => toolkit.migrate({ + stackName: 'cannot-generate-template', + fromPath: path.join(__dirname, 'commands', 'test-resources', 'templates', 'sqs-template.json'), + language: 'rust', + })).rejects.toThrowError('CannotGenerateTemplateStack could not be generated because rust is not a supported language'); + expect(stderrMock.mock.calls[1][0]).toContain(' ❌ Migrate failed for `cannot-generate-template`: CannotGenerateTemplateStack could not be generated because rust is not a supported language'); + }); - // Packages created for typescript - expect(fs.pathExistsSync(path.join(workDir, 'S3Python', 'requirements.txt'))).toBeTruthy(); - expect(fs.pathExistsSync(path.join(workDir, 'S3Python', 'app.py'))).toBeTruthy(); - expect(fs.pathExistsSync(path.join(workDir, 'S3Python', 's3_python', 's3_python_stack.py'))).toBeTruthy(); + cliTest('migrate succeeds for valid template from local path when no lanugage is provided', async (workDir) => { + const toolkit = defaultToolkitSetup(); + await toolkit.migrate({ + stackName: 'SQSTypeScript', + fromPath: sqsTemplatePath, + outputPath: workDir, }); - cliTest('migrate call is idempotent', async (workDir) => { - const toolkit = defaultToolkitSetup(); - await toolkit.migrate({ - stackName: 'AutoscalingCSharp', - fromPath: autoscalingTemplatePath, - outputPath: workDir, - language: 'csharp', - }); - - // Packages created for typescript - expect(fs.pathExistsSync(path.join(workDir, 'AutoscalingCSharp', 'src', 'AutoscalingCSharp.sln'))).toBeTruthy(); - expect(fs.pathExistsSync(path.join(workDir, 'AutoscalingCSharp', 'src', 'AutoscalingCSharp', 'Program.cs'))).toBeTruthy(); - expect(fs.pathExistsSync(path.join(workDir, 'AutoscalingCSharp', 'src', 'AutoscalingCSharp', 'AutoscalingCSharpStack.cs'))).toBeTruthy(); - - // One more time - await toolkit.migrate({ - stackName: 'AutoscalingCSharp', - fromPath: autoscalingTemplatePath, - outputPath: workDir, - language: 'csharp', - }); - - // Packages created for typescript - expect(fs.pathExistsSync(path.join(workDir, 'AutoscalingCSharp', 'src', 'AutoscalingCSharp.sln'))).toBeTruthy(); - expect(fs.pathExistsSync(path.join(workDir, 'AutoscalingCSharp', 'src', 'AutoscalingCSharp', 'Program.cs'))).toBeTruthy(); - expect(fs.pathExistsSync(path.join(workDir, 'AutoscalingCSharp', 'src', 'AutoscalingCSharp', 'AutoscalingCSharpStack.cs'))).toBeTruthy(); - }); + // Packages created for typescript + expect(fs.pathExistsSync(path.join(workDir, 'SQSTypeScript', 'package.json'))).toBeTruthy(); + expect(fs.pathExistsSync(path.join(workDir, 'SQSTypeScript', 'bin', 'sqs_type_script.ts'))).toBeTruthy(); + expect(fs.pathExistsSync(path.join(workDir, 'SQSTypeScript', 'lib', 'sqs_type_script-stack.ts'))).toBeTruthy(); }); - describe('stack with error and flagged for validation', () => { - beforeEach(() => { - cloudExecutable = new MockCloudExecutable({ - stacks: [ - MockStack.MOCK_STACK_A, - MockStack.MOCK_STACK_B, - ], - nestedAssemblies: [{ - stacks: [ - { properties: { validateOnSynth: true }, ...MockStack.MOCK_STACK_WITH_ERROR }, - ], - }], - }); + cliTest('migrate succeeds for valid template from local path when lanugage is provided', async (workDir) => { + const toolkit = defaultToolkitSetup(); + await toolkit.migrate({ + stackName: 'S3Python', + fromPath: s3TemplatePath, + outputPath: workDir, + language: 'python', }); - test('causes synth to fail if autoValidate=true', async () => { - const toolkit = defaultToolkitSetup(); - const autoValidate = true; - await expect(toolkit.synth([], false, true, autoValidate)).rejects.toBeDefined(); - }); + // Packages created for typescript + expect(fs.pathExistsSync(path.join(workDir, 'S3Python', 'requirements.txt'))).toBeTruthy(); + expect(fs.pathExistsSync(path.join(workDir, 'S3Python', 'app.py'))).toBeTruthy(); + expect(fs.pathExistsSync(path.join(workDir, 'S3Python', 's3_python', 's3_python_stack.py'))).toBeTruthy(); + }); - test('causes synth to succeed if autoValidate=false', async () => { - const toolkit = defaultToolkitSetup(); - const autoValidate = false; - await toolkit.synth([], false, true, autoValidate); - expect(mockData.mock.calls.length).toEqual(0); - }); + cliTest('migrate call is idempotent', async (workDir) => { + const toolkit = defaultToolkitSetup(); + await toolkit.migrate({ + stackName: 'AutoscalingCSharp', + fromPath: autoscalingTemplatePath, + outputPath: workDir, + language: 'csharp', + }); + + // Packages created for typescript + expect(fs.pathExistsSync(path.join(workDir, 'AutoscalingCSharp', 'src', 'AutoscalingCSharp.sln'))).toBeTruthy(); + expect(fs.pathExistsSync(path.join(workDir, 'AutoscalingCSharp', 'src', 'AutoscalingCSharp', 'Program.cs'))).toBeTruthy(); + expect(fs.pathExistsSync(path.join(workDir, 'AutoscalingCSharp', 'src', 'AutoscalingCSharp', 'AutoscalingCSharpStack.cs'))).toBeTruthy(); + + // One more time + await toolkit.migrate({ + stackName: 'AutoscalingCSharp', + fromPath: autoscalingTemplatePath, + outputPath: workDir, + language: 'csharp', + }); + + // Packages created for typescript + expect(fs.pathExistsSync(path.join(workDir, 'AutoscalingCSharp', 'src', 'AutoscalingCSharp.sln'))).toBeTruthy(); + expect(fs.pathExistsSync(path.join(workDir, 'AutoscalingCSharp', 'src', 'AutoscalingCSharp', 'Program.cs'))).toBeTruthy(); + expect(fs.pathExistsSync(path.join(workDir, 'AutoscalingCSharp', 'src', 'AutoscalingCSharp', 'AutoscalingCSharpStack.cs'))).toBeTruthy(); }); + }); - test('stack has error and was explicitly selected', async () => { + describe('stack with error and flagged for validation', () => { + beforeEach(() => { cloudExecutable = new MockCloudExecutable({ stacks: [ MockStack.MOCK_STACK_A, @@ -1169,49 +1191,76 @@ describe('toolkit', () => { ], nestedAssemblies: [{ stacks: [ - { properties: { validateOnSynth: false }, ...MockStack.MOCK_STACK_WITH_ERROR }, + { properties: { validateOnSynth: true }, ...MockStack.MOCK_STACK_WITH_ERROR }, ], }], }); + }); + test('causes synth to fail if autoValidate=true', async () => { const toolkit = defaultToolkitSetup(); + const autoValidate = true; + await expect(toolkit.synth([], false, true, autoValidate)).rejects.toBeDefined(); + }); - await expect(toolkit.synth(['Test-Stack-A/witherrors'], false, true)).rejects.toBeDefined(); + test('causes synth to succeed if autoValidate=false', async () => { + const toolkit = defaultToolkitSetup(); + const autoValidate = false; + await toolkit.synth([], false, true, autoValidate); + expect(mockData.mock.calls.length).toEqual(0); }); + }); - test('stack has error, is not flagged for validation and was not explicitly selected', async () => { - cloudExecutable = new MockCloudExecutable({ + test('stack has error and was explicitly selected', async () => { + cloudExecutable = new MockCloudExecutable({ + stacks: [ + MockStack.MOCK_STACK_A, + MockStack.MOCK_STACK_B, + ], + nestedAssemblies: [{ stacks: [ - MockStack.MOCK_STACK_A, - MockStack.MOCK_STACK_B, + { properties: { validateOnSynth: false }, ...MockStack.MOCK_STACK_WITH_ERROR }, ], - nestedAssemblies: [{ - stacks: [ - { properties: { validateOnSynth: false }, ...MockStack.MOCK_STACK_WITH_ERROR }, - ], - }], - }); + }], + }); - const toolkit = defaultToolkitSetup(); + const toolkit = defaultToolkitSetup(); - await toolkit.synth([], false, true); - }); + await expect(toolkit.synth(['Test-Stack-A/witherrors'], false, true)).rejects.toBeDefined(); + }); - test('stack has dependency and was explicitly selected', async () => { - cloudExecutable = new MockCloudExecutable({ + test('stack has error, is not flagged for validation and was not explicitly selected', async () => { + cloudExecutable = new MockCloudExecutable({ + stacks: [ + MockStack.MOCK_STACK_A, + MockStack.MOCK_STACK_B, + ], + nestedAssemblies: [{ stacks: [ - MockStack.MOCK_STACK_C, - MockStack.MOCK_STACK_D, + { properties: { validateOnSynth: false }, ...MockStack.MOCK_STACK_WITH_ERROR }, ], - }); + }], + }); - const toolkit = defaultToolkitSetup(); + const toolkit = defaultToolkitSetup(); - await toolkit.synth([MockStack.MOCK_STACK_D.stackName], true, false); + await toolkit.synth([], false, true); + }); - expect(mockData.mock.calls.length).toEqual(1); - expect(mockData.mock.calls[0][0]).toBeDefined(); + test('stack has dependency and was explicitly selected', async () => { + cloudExecutable = new MockCloudExecutable({ + stacks: [ + MockStack.MOCK_STACK_C, + MockStack.MOCK_STACK_D, + ], }); + + const toolkit = defaultToolkitSetup(); + + await toolkit.synth([MockStack.MOCK_STACK_D.stackName], true, false); + + expect(mockData.mock.calls.length).toEqual(1); + expect(mockData.mock.calls[0][0]).toBeDefined(); }); }); @@ -1279,39 +1328,7 @@ class MockStack { }, depends: [MockStack.MOCK_STACK_C.stackName], }; - public static readonly MOCK_STACK_E: TestStackArtifact = { - stackName: 'Test-Stack-E', - template: { Resources: { TemplateName: 'Test-Stack-E' } }, - env: 'aws://123456789012/bermuda-triangle-1', - metadata: { - '/Test-Stack-E': [ - { - type: cxschema.ArtifactMetadataEntryType.STACK_TAGS, - data: [ - { key: 'Foo', value: 'Bar' }, - ], - }, - ], - }, - displayName: 'Test-Stack-E-Display-Name', - }; - public static readonly MOCK_STACK_F: TestStackArtifact = { - stackName: 'Test-Stack-F', - template: { Resources: { TemplateName: 'Test-Stack-F' } }, - env: 'aws://123456789012/bermuda-triangle-1', - metadata: { - '/Test-Stack-F': [ - { - type: cxschema.ArtifactMetadataEntryType.STACK_TAGS, - data: [ - { key: 'Foo', value: 'Bar' }, - ], - }, - ], - }, - displayName: 'Test-Stack-F-Display-Name', - }; public static readonly MOCK_STACK_WITH_ERROR: TestStackArtifact = { stackName: 'witherrors', env: 'aws://123456789012/bermuda-triangle-1', @@ -1402,8 +1419,6 @@ class FakeCloudFormation extends Deployments { MockStack.MOCK_STACK_B.stackName, MockStack.MOCK_STACK_C.stackName, // MockStack.MOCK_STACK_D deliberately omitted. - MockStack.MOCK_STACK_E.stackName, - MockStack.MOCK_STACK_F.stackName, MockStack.MOCK_STACK_WITH_ASSET.stackName, MockStack.MOCK_STACK_WITH_ERROR.stackName, MockStack.MOCK_STACK_WITH_NOTIFICATION_ARNS.stackName, @@ -1436,10 +1451,6 @@ class FakeCloudFormation extends Deployments { return Promise.resolve({}); case MockStack.MOCK_STACK_C.stackName: return Promise.resolve({}); - case MockStack.MOCK_STACK_E.stackName: - return Promise.resolve({}); - case MockStack.MOCK_STACK_F.stackName: - return Promise.resolve({}); case MockStack.MOCK_STACK_WITH_ASSET.stackName: return Promise.resolve({}); case MockStack.MOCK_STACK_WITH_NOTIFICATION_ARNS.stackName: From 4fbdaa4bb13e929055c80175a777c2eb5eac190f Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Fri, 21 Jun 2024 10:50:44 -0700 Subject: [PATCH 37/51] done --- packages/aws-cdk/test/cdk-toolkit.test.ts | 50 ----------------------- 1 file changed, 50 deletions(-) diff --git a/packages/aws-cdk/test/cdk-toolkit.test.ts b/packages/aws-cdk/test/cdk-toolkit.test.ts index ba548215e7634..da416fedee090 100644 --- a/packages/aws-cdk/test/cdk-toolkit.test.ts +++ b/packages/aws-cdk/test/cdk-toolkit.test.ts @@ -495,7 +495,6 @@ describe('deploy', () => { }); }); - describe('sns notification arns', () => { beforeEach(() => { cloudExecutable = new MockCloudExecutable({ @@ -555,55 +554,6 @@ describe('deploy', () => { ).rejects.toThrow('Notification arn arn:::cfn-my-cool-topic is not a valid arn for an SNS topic'); }); - /* - test('with sns notification arns as options', async () => { - // GIVEN - const notificationArns = [ - 'arn:aws:sns:us-east-2:444455556666:MyTopic', - 'arn:aws:sns:eu-west-1:111155556666:my-great-topic', - ]; - const toolkit = new CdkToolkit({ - cloudExecutable, - configuration: cloudExecutable.configuration, - sdkProvider: cloudExecutable.sdkProvider, - deployments: new FakeCloudFormation({ - // Stacks should be selected by their hierarchical ID, which is their displayName, not by the stack ID. - 'Test-Stack-E-Display-Name': { Foo: 'Bar' }, - 'Test-Stack-F-Display-Name': { Foo: 'Bar' }, - }, notificationArns), - }); - - // WHEN - await toolkit.deploy({ - selector: { patterns: ['Test-Stack-E', 'Test-Stack-F'] }, - notificationArns, - hotswap: HotswapMode.FULL_DEPLOYMENT, - }); - }); - - test('fail with incorrect sns notification arns as options', async () => { - // GIVEN - const notificationArns = ['arn:::cfn-my-cool-topic']; - const toolkit = new CdkToolkit({ - cloudExecutable, - configuration: cloudExecutable.configuration, - sdkProvider: cloudExecutable.sdkProvider, - deployments: new FakeCloudFormation({ - 'Test-Stack-E-Display-Name': { Foo: 'Bar' }, - }, notificationArns), - }); - - // WHEN - await expect(() => - toolkit.deploy({ - selector: { patterns: ['Test-Stack-E'] }, - notificationArns, - hotswap: HotswapMode.FULL_DEPLOYMENT, - }), - ).rejects.toThrow('Notification arn arn:::cfn-my-cool-topic is not a valid arn for an SNS topic'); - }); - */ - test('with sns notification arns in the executable', async () => { // GIVEN const expectedNotificationArns = [ From d1bf1a00e137e98278c0bb741c9d88e3a32d07d3 Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Fri, 21 Jun 2024 11:01:14 -0700 Subject: [PATCH 38/51] code teleports to a different line in the file --- packages/aws-cdk/lib/api/deploy-stack.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/aws-cdk/lib/api/deploy-stack.ts b/packages/aws-cdk/lib/api/deploy-stack.ts index 62019c83380e3..93a57eb2e4629 100644 --- a/packages/aws-cdk/lib/api/deploy-stack.ts +++ b/packages/aws-cdk/lib/api/deploy-stack.ts @@ -644,10 +644,6 @@ async function canSkipDeploy( return false; } - function arrayEquals(a: any[], b: any[]): boolean { - return a.every(item => b.includes(item)) && b.every(item => a.includes(item)); - } - // Notification arns have changed if (!arrayEquals(cloudFormationStack.notificationArns, deployStackOptions.notificationArns ?? [])) { debug(`${deployName}: notification arns have changed`); @@ -704,3 +700,7 @@ function suffixWithErrors(msg: string, errors?: string[]) { ? `${msg}: ${errors.join(', ')}` : msg; } + +function arrayEquals(a: any[], b: any[]): boolean { + return a.every(item => b.includes(item)) && b.every(item => a.includes(item)); +} From 67188db3ae4d28c61f1d215ccdecceef55f4267a Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Fri, 21 Jun 2024 11:04:58 -0700 Subject: [PATCH 39/51] toolkit test --- packages/aws-cdk/test/cdk-toolkit.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/aws-cdk/test/cdk-toolkit.test.ts b/packages/aws-cdk/test/cdk-toolkit.test.ts index da416fedee090..43f7353370a13 100644 --- a/packages/aws-cdk/test/cdk-toolkit.test.ts +++ b/packages/aws-cdk/test/cdk-toolkit.test.ts @@ -747,11 +747,11 @@ describe('deploy', () => { describe('destroy', () => { test('destroy correct stack', async () => { - const toolkit = destroyToolkitSetup(); + const toolkit = defaultToolkitSetup(); expect(() => { return toolkit.destroy({ - selector: { patterns: ['Test-Stack-Destroy'] }, + selector: { patterns: ['Test-Stack-A/Test-Stack-C'] }, exclusively: true, force: true, fromDeploy: true, From f00ed25289fafa9c3bcf32f0565f5a12e53257b5 Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Fri, 21 Jun 2024 11:05:50 -0700 Subject: [PATCH 40/51] toolkit --- packages/aws-cdk/test/cdk-toolkit.test.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/packages/aws-cdk/test/cdk-toolkit.test.ts b/packages/aws-cdk/test/cdk-toolkit.test.ts index 43f7353370a13..b8d998b97ef84 100644 --- a/packages/aws-cdk/test/cdk-toolkit.test.ts +++ b/packages/aws-cdk/test/cdk-toolkit.test.ts @@ -112,17 +112,6 @@ function defaultToolkitSetup() { }); } -function destroyToolkitSetup() { - return new CdkToolkit({ - cloudExecutable, - configuration: cloudExecutable.configuration, - sdkProvider: cloudExecutable.sdkProvider, - deployments: new FakeCloudFormation({ - 'Test-Stack-Destroy': { Foo: 'Bar' }, - }), - }); -} - describe('readCurrentTemplate', () => { let template: any; let mockForEnvironment = jest.fn(); From c38e7a4f9adf7865ad7366514577c41944a615f8 Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Tue, 2 Jul 2024 08:57:51 -0700 Subject: [PATCH 41/51] wowowo cli integ test --- .../cli-integ/resources/cdk-apps/app/app.js | 9 +++++++ .../tests/cli-integ-tests/cli.integtest.ts | 24 +++++++++++++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js index d094055795e27..d8e2724c0cf5d 100755 --- a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js @@ -637,6 +637,13 @@ class BuiltinLambdaStack extends cdk.Stack { } } +class NotificationArnPropStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + new sns.Topic(this, 'topic'); + } +} + const app = new cdk.App({ context: { '@aws-cdk/core:assetHashSalt': process.env.CODEBUILD_BUILD_ID, // Force all assets to be unique, but consistent in one build @@ -677,6 +684,8 @@ switch (stackSet) { new DockerStack(app, `${stackPrefix}-docker`); new DockerStackWithCustomFile(app, `${stackPrefix}-docker-with-custom-file`); + new NotificationArnPropStack(app, `${stackPrefix}-test-notification-arn-prop`); + // SSO stacks new SsoInstanceAccessControlConfig(app, `${stackPrefix}-sso-access-control`); new SsoAssignment(app, `${stackPrefix}-sso-assignment`); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts index f323110eecfa4..a44d6c4a47a7d 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts @@ -466,8 +466,8 @@ integTest('deploy with parameters multi', withDefaultFixture(async (fixture) => ); })); -integTest('deploy with notification ARN', withDefaultFixture(async (fixture) => { - const topicName = `${fixture.stackNamePrefix}-test-topic`; +integTest('deploy with notification ARN as flag', withDefaultFixture(async (fixture) => { + const topicName = `${fixture.stackNamePrefix}-test-topic-flag`; const response = await fixture.aws.sns('createTopic', { Name: topicName }); const topicArn = response.TopicArn!; @@ -488,6 +488,26 @@ integTest('deploy with notification ARN', withDefaultFixture(async (fixture) => } })); +integTest('deploy with notification ARN as prop', withDefaultFixture(async (fixture) => { + const topicName = `${fixture.stackNamePrefix}-test-topic-prop`; + + const response = await fixture.aws.sns('createTopic', { Name: topicName }); + const topicArn = response.TopicArn!; + try { + await fixture.cdkDeploy('test-notification-arn-prop'); + + // verify that the stack we deployed has our notification ARN + const describeResponse = await fixture.aws.cloudFormation('describeStacks', { + StackName: fixture.fullStackName('test-notification-arn-prop'), + }); + expect(describeResponse.Stacks?.[0].NotificationARNs).toEqual([topicArn]); + } finally { + await fixture.aws.sns('deleteTopic', { + TopicArn: topicArn, + }); + } +})); + // NOTE: this doesn't currently work with modern-style synthesis, as the bootstrap // role by default will not have permission to iam:PassRole the created role. integTest('deploy with role', withDefaultFixture(async (fixture) => { From f79368f6b34a0895ebb4e2168119d5b5a8dd5c09 Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Tue, 2 Jul 2024 13:18:55 -0700 Subject: [PATCH 42/51] integ-fix --- .../@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js index d8e2724c0cf5d..d41aab4c38786 100755 --- a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js @@ -684,7 +684,9 @@ switch (stackSet) { new DockerStack(app, `${stackPrefix}-docker`); new DockerStackWithCustomFile(app, `${stackPrefix}-docker-with-custom-file`); - new NotificationArnPropStack(app, `${stackPrefix}-test-notification-arn-prop`); + new NotificationArnPropStack(app, `${stackPrefix}-test-notification-arn-prop`, { + notificationArns: [`${stackPrefix}-test-topic-prop`], + }); // SSO stacks new SsoInstanceAccessControlConfig(app, `${stackPrefix}-sso-access-control`); From b03209deb4eafffb4ca508638145e9245240a3f2 Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Tue, 2 Jul 2024 13:23:24 -0700 Subject: [PATCH 43/51] fix deploy all test --- .../@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js index d41aab4c38786..fd3c5e32ae464 100755 --- a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js @@ -684,7 +684,7 @@ switch (stackSet) { new DockerStack(app, `${stackPrefix}-docker`); new DockerStackWithCustomFile(app, `${stackPrefix}-docker-with-custom-file`); - new NotificationArnPropStack(app, `${stackPrefix}-test-notification-arn-prop`, { + new NotificationArnPropStack(app, `${stackPrefix}-notification-arn-prop`, { notificationArns: [`${stackPrefix}-test-topic-prop`], }); From 22bb7e8674941a638125a1c092a026e1e07e34f4 Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Tue, 2 Jul 2024 13:25:27 -0700 Subject: [PATCH 44/51] wowowo --- .../cli-integ/tests/cli-integ-tests/cli.integtest.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts index a44d6c4a47a7d..fa8a2e47fb660 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts @@ -494,11 +494,11 @@ integTest('deploy with notification ARN as prop', withDefaultFixture(async (fixt const response = await fixture.aws.sns('createTopic', { Name: topicName }); const topicArn = response.TopicArn!; try { - await fixture.cdkDeploy('test-notification-arn-prop'); + await fixture.cdkDeploy('notification-arn-prop'); // verify that the stack we deployed has our notification ARN const describeResponse = await fixture.aws.cloudFormation('describeStacks', { - StackName: fixture.fullStackName('test-notification-arn-prop'), + StackName: fixture.fullStackName('notification-arn-prop'), }); expect(describeResponse.Stacks?.[0].NotificationARNs).toEqual([topicArn]); } finally { From bf5deb95b3da585f0f5fbf5453b7c92150df0aa7 Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Wed, 10 Jul 2024 16:04:04 -0700 Subject: [PATCH 45/51] fixing the cli test framework --- .../cli-integ/lib/package-sources/repo-source.ts | 9 +++++---- .../@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts | 12 ++++++++++++ .../cli-integ/resources/cdk-apps/app/app.js | 2 +- .../cli-integ/tests/cli-integ-tests/cli.integtest.ts | 2 ++ packages/aws-cdk/lib/cdk-toolkit.ts | 4 ++++ 5 files changed, 24 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk-testing/cli-integ/lib/package-sources/repo-source.ts b/packages/@aws-cdk-testing/cli-integ/lib/package-sources/repo-source.ts index 45a8f4e5d4dfc..7a5f08ec71b98 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/package-sources/repo-source.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/package-sources/repo-source.ts @@ -75,13 +75,14 @@ const YARN_MONOREPO_CACHE: Record = {}; * * Cached in YARN_MONOREPO_CACHE. */ -async function findYarnPackages(root: string): Promise> { +export async function findYarnPackages(root: string): Promise> { if (!(root in YARN_MONOREPO_CACHE)) { - const output: YarnWorkspacesOutput = JSON.parse(await shell(['yarn', 'workspaces', '--silent', 'info'], { + const outputDataString: string = JSON.parse(await shell(['yarn', 'workspaces', '--json', 'info'], { captureStderr: false, cwd: root, show: 'error', - })); + })).data; + const output: YarnWorkspacesOutput = JSON.parse(outputDataString); const ret: Record = {}; for (const [k, v] of Object.entries(output)) { @@ -96,7 +97,7 @@ async function findYarnPackages(root: string): Promise> { * Find the root directory of the repo from the current directory */ export async function autoFindRoot() { - const found = await findUp('release.json'); + const found = findUp('release.json'); if (!found) { throw new Error(`Could not determine repository root: 'release.json' not found from ${process.cwd()}`); } diff --git a/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts b/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts index 16226c4cde259..84d3386b72fe5 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts @@ -10,6 +10,7 @@ import { RESOURCES_DIR } from './resources'; import { shell, ShellOptions, ShellHelper, rimraf } from './shell'; import { AwsContext, withAws } from './with-aws'; import { withTimeout } from './with-timeout'; +import { findYarnPackages/*, getCache*/ } from './package-sources/repo-source'; export const DEFAULT_TEST_TIMEOUT_S = 10 * 60; export const EXTENDED_TEST_TIMEOUT_S = 30 * 60; @@ -612,6 +613,17 @@ function defined(x: A): x is NonNullable { * for Node's dependency lookup mechanism). */ export async function installNpmPackages(fixture: TestFixture, packages: Record) { + if (process.env.REPO_ROOT) { + const monoRepo = await findYarnPackages(process.env.REPO_ROOT ?? ''); + + // Replace the install target with the physical location of this package + for (const key of Object.keys(packages)) { + if (key in monoRepo) { + packages[key] = monoRepo[key]; + } + } + } + fs.writeFileSync(path.join(fixture.integTestDir, 'package.json'), JSON.stringify({ name: 'cdk-integ-tests', private: true, diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js index fd3c5e32ae464..7e91514bb6d94 100755 --- a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js @@ -685,7 +685,7 @@ switch (stackSet) { new DockerStackWithCustomFile(app, `${stackPrefix}-docker-with-custom-file`); new NotificationArnPropStack(app, `${stackPrefix}-notification-arn-prop`, { - notificationArns: [`${stackPrefix}-test-topic-prop`], + notificationArns: [`arn:aws:sns:${defaultEnv.region}:${defaultEnv.account}:${stackPrefix}-test-topic-prop`], }); // SSO stacks diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts index fa8a2e47fb660..9db27b896c279 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts @@ -471,6 +471,7 @@ integTest('deploy with notification ARN as flag', withDefaultFixture(async (fixt const response = await fixture.aws.sns('createTopic', { Name: topicName }); const topicArn = response.TopicArn!; + try { await fixture.cdkDeploy('test-2', { options: ['--notification-arns', topicArn], @@ -493,6 +494,7 @@ integTest('deploy with notification ARN as prop', withDefaultFixture(async (fixt const response = await fixture.aws.sns('createTopic', { Name: topicName }); const topicArn = response.TopicArn!; + try { await fixture.cdkDeploy('notification-arn-prop'); diff --git a/packages/aws-cdk/lib/cdk-toolkit.ts b/packages/aws-cdk/lib/cdk-toolkit.ts index f876634484c46..aa2eabc5ad29a 100644 --- a/packages/aws-cdk/lib/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cdk-toolkit.ts @@ -313,6 +313,10 @@ export class CdkToolkit { notificationArns = notificationArns.concat(options.notificationArns ?? []); notificationArns = notificationArns.concat(stack.notificationArns); + if (notificationArns.length == 0) { + throw new Error(`we must hit this ${stack.notificationArns.length}`); + } + notificationArns.map(arn => { if (!validateSnsTopicArn(arn)) { throw new Error(`Notification arn ${arn} is not a valid arn for an SNS topic`); From 95e8dacf0e19bf777e75134857de03633d421c93 Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Wed, 10 Jul 2024 16:28:57 -0700 Subject: [PATCH 46/51] fix --- packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts b/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts index 84d3386b72fe5..ed2cf882c693a 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts @@ -4,13 +4,13 @@ import * as os from 'os'; import * as path from 'path'; import { outputFromStack, AwsClients } from './aws'; import { TestContext } from './integ-test'; +import { findYarnPackages } from './package-sources/repo-source'; import { IPackageSource } from './package-sources/source'; import { packageSourceInSubprocess } from './package-sources/subprocess'; import { RESOURCES_DIR } from './resources'; import { shell, ShellOptions, ShellHelper, rimraf } from './shell'; import { AwsContext, withAws } from './with-aws'; import { withTimeout } from './with-timeout'; -import { findYarnPackages/*, getCache*/ } from './package-sources/repo-source'; export const DEFAULT_TEST_TIMEOUT_S = 10 * 60; export const EXTENDED_TEST_TIMEOUT_S = 30 * 60; From 4331c917cd10de989789c614e0525100a8818a3b Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Fri, 12 Jul 2024 14:07:19 -0700 Subject: [PATCH 47/51] fix the busted test --- .../cli-integ/tests/cli-integ-tests/cli.integtest.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts index 9db27b896c279..d674e16810960 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts @@ -1,7 +1,7 @@ import { promises as fs, existsSync } from 'fs'; import * as os from 'os'; import * as path from 'path'; -import { integTest, cloneDirectory, shell, withDefaultFixture, retry, sleep, randomInteger, withSamIntegrationFixture, RESOURCES_DIR, withCDKMigrateFixture, withExtendedTimeoutFixture, randomString } from '../../lib'; +import { integTest, cloneDirectory, shell, withDefaultFixture, retry, sleep, randomInteger, withSamIntegrationFixture, RESOURCES_DIR, withCDKMigrateFixture, withExtendedTimeoutFixture, randomString, withoutBootstrap } from '../../lib'; jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime @@ -187,7 +187,10 @@ integTest('context setting', withDefaultFixture(async (fixture) => { } })); -integTest('context in stage propagates to top', withDefaultFixture(async (fixture) => { +// bootstrapping also performs synthesis. As it turns out, bootstarp-stage synthesis still causes the lookups to be cached, meaning that the lookup never +// happens when we actually call `cdk synth --no-lookups`. This results in the error never being thrown, because it never tries to lookup anything. +// Fix this by not trying to bootstrap; there's no need to bootstrap anyway, since the test never tries to deploy anything. +integTest('context in stage propagates to top', withoutBootstrap(async (fixture) => { await expect(fixture.cdkSynth({ // This will make it error to prove that the context bubbles up, and also that we can fail on command options: ['--no-lookups'], From f4c65a2823f23963da6442383e3fb7e048160cc4 Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Fri, 12 Jul 2024 16:32:51 -0700 Subject: [PATCH 48/51] fix --- packages/aws-cdk/lib/cdk-toolkit.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/aws-cdk/lib/cdk-toolkit.ts b/packages/aws-cdk/lib/cdk-toolkit.ts index aa2eabc5ad29a..f876634484c46 100644 --- a/packages/aws-cdk/lib/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cdk-toolkit.ts @@ -313,10 +313,6 @@ export class CdkToolkit { notificationArns = notificationArns.concat(options.notificationArns ?? []); notificationArns = notificationArns.concat(stack.notificationArns); - if (notificationArns.length == 0) { - throw new Error(`we must hit this ${stack.notificationArns.length}`); - } - notificationArns.map(arn => { if (!validateSnsTopicArn(arn)) { throw new Error(`Notification arn ${arn} is not a valid arn for an SNS topic`); From b1c116edc90b8139cabd20801be7c5f99bf2dfae Mon Sep 17 00:00:00 2001 From: Calvin Combs <66279577+comcalvi@users.noreply.github.com> Date: Mon, 12 Aug 2024 13:24:36 -0700 Subject: [PATCH 49/51] typo Co-authored-by: Parker Scanlon <69879391+scanlonp@users.noreply.github.com> --- .../cli-integ/tests/cli-integ-tests/cli.integtest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts index d674e16810960..1ce7fe3ef7751 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts @@ -187,7 +187,7 @@ integTest('context setting', withDefaultFixture(async (fixture) => { } })); -// bootstrapping also performs synthesis. As it turns out, bootstarp-stage synthesis still causes the lookups to be cached, meaning that the lookup never +// bootstrapping also performs synthesis. As it turns out, bootstrap-stage synthesis still causes the lookups to be cached, meaning that the lookup never // happens when we actually call `cdk synth --no-lookups`. This results in the error never being thrown, because it never tries to lookup anything. // Fix this by not trying to bootstrap; there's no need to bootstrap anyway, since the test never tries to deploy anything. integTest('context in stage propagates to top', withoutBootstrap(async (fixture) => { From 79b2e7d88bfc09eee63fc3141add5d9886c27d73 Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Mon, 12 Aug 2024 16:17:19 -0700 Subject: [PATCH 50/51] changes: ' --- packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts | 2 +- packages/aws-cdk/test/cdk-toolkit.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts b/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts index ed2cf882c693a..f2b5263df06a5 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts @@ -614,7 +614,7 @@ function defined(x: A): x is NonNullable { */ export async function installNpmPackages(fixture: TestFixture, packages: Record) { if (process.env.REPO_ROOT) { - const monoRepo = await findYarnPackages(process.env.REPO_ROOT ?? ''); + const monoRepo = await findYarnPackages(process.env.REPO_ROOT); // Replace the install target with the physical location of this package for (const key of Object.keys(packages)) { diff --git a/packages/aws-cdk/test/cdk-toolkit.test.ts b/packages/aws-cdk/test/cdk-toolkit.test.ts index b8d998b97ef84..6f446411ab277 100644 --- a/packages/aws-cdk/test/cdk-toolkit.test.ts +++ b/packages/aws-cdk/test/cdk-toolkit.test.ts @@ -508,7 +508,7 @@ describe('deploy', () => { sdkProvider: cloudExecutable.sdkProvider, deployments: new FakeCloudFormation({ 'Test-Stack-A': { Foo: 'Bar' }, - }, notificationArns), + }), }); // WHEN @@ -529,7 +529,7 @@ describe('deploy', () => { sdkProvider: cloudExecutable.sdkProvider, deployments: new FakeCloudFormation({ 'Test-Stack-A': { Foo: 'Bar' }, - }, notificationArns), + }), }); // WHEN From e933382846ce76eadafac22eece328b70609ac29 Mon Sep 17 00:00:00 2001 From: Calvin Combs Date: Tue, 13 Aug 2024 12:20:46 -0700 Subject: [PATCH 51/51] fix --- packages/aws-cdk/test/cdk-toolkit.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/aws-cdk/test/cdk-toolkit.test.ts b/packages/aws-cdk/test/cdk-toolkit.test.ts index 6f446411ab277..b8d998b97ef84 100644 --- a/packages/aws-cdk/test/cdk-toolkit.test.ts +++ b/packages/aws-cdk/test/cdk-toolkit.test.ts @@ -508,7 +508,7 @@ describe('deploy', () => { sdkProvider: cloudExecutable.sdkProvider, deployments: new FakeCloudFormation({ 'Test-Stack-A': { Foo: 'Bar' }, - }), + }, notificationArns), }); // WHEN @@ -529,7 +529,7 @@ describe('deploy', () => { sdkProvider: cloudExecutable.sdkProvider, deployments: new FakeCloudFormation({ 'Test-Stack-A': { Foo: 'Bar' }, - }), + }, notificationArns), }); // WHEN