diff --git a/packages/@aws-cdk/aws-servicecatalogappregistry-alpha/test/application-associator.test.ts b/packages/@aws-cdk/aws-servicecatalogappregistry-alpha/test/application-associator.test.ts index 41db8ade2d91d..828247b32b3ed 100644 --- a/packages/@aws-cdk/aws-servicecatalogappregistry-alpha/test/application-associator.test.ts +++ b/packages/@aws-cdk/aws-servicecatalogappregistry-alpha/test/application-associator.test.ts @@ -268,7 +268,7 @@ describe('Scope based Associations with Application with Cross Region/Account', associateStage: true, }); app.synth(); - Template.fromStack(application.appRegistryApplication.stack).hasOutput('DefaultCdkApplicationApplicationManagerUrl27C138EF', {}); + Template.fromStack(application.appRegistryApplication.stack, { skipClean: true }).hasOutput('DefaultCdkApplicationApplicationManagerUrl27C138EF', {}); Template.fromStack(pipelineStack).resourceCountIs('AWS::ServiceCatalogAppRegistry::ResourceAssociation', 1); }); }); diff --git a/packages/@aws-cdk/integ-tests-alpha/lib/assertions/assertions.ts b/packages/@aws-cdk/integ-tests-alpha/lib/assertions/assertions.ts index 69ff247f8b829..75f728ebee787 100644 --- a/packages/@aws-cdk/integ-tests-alpha/lib/assertions/assertions.ts +++ b/packages/@aws-cdk/integ-tests-alpha/lib/assertions/assertions.ts @@ -61,6 +61,7 @@ export class EqualsAssertion extends Construct { new CfnOutput(this, 'AssertionResults', { value: this.result, - }).overrideLogicalId(`AssertionResults${id}${md5hash({ actual: props.actual.result, expected: props.expected.result })}`); + key: `AssertionResults${id}${md5hash({ actual: props.actual.result, expected: props.expected.result })}`, + }); } } diff --git a/packages/@aws-cdk/integ-tests-alpha/lib/assertions/http-call.ts b/packages/@aws-cdk/integ-tests-alpha/lib/assertions/http-call.ts index 473131ecbbe4c..a67062625b8f4 100644 --- a/packages/@aws-cdk/integ-tests-alpha/lib/assertions/http-call.ts +++ b/packages/@aws-cdk/integ-tests-alpha/lib/assertions/http-call.ts @@ -47,7 +47,8 @@ export class HttpApiCall extends ApiCallBase { new CfnOutput(node, 'AssertionResults', { value: result, - }).overrideLogicalId(`AssertionResults${id.replace(/[\W_]+/g, '')}`); + key: `AssertionResults${id.replace(/[\W_]+/g, '')}`, + }); } } }, diff --git a/packages/@aws-cdk/integ-tests-alpha/lib/assertions/sdk.ts b/packages/@aws-cdk/integ-tests-alpha/lib/assertions/sdk.ts index 6a535f8fb2fac..2c484586dcd43 100644 --- a/packages/@aws-cdk/integ-tests-alpha/lib/assertions/sdk.ts +++ b/packages/@aws-cdk/integ-tests-alpha/lib/assertions/sdk.ts @@ -121,7 +121,8 @@ export class AwsApiCall extends ApiCallBase { // Remove the at sign, slash, and hyphen because when using the v3 package name or client name as the service name, // the `id` includes them, but they are not allowed in the `CfnOutput` logical id // See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html#outputs-section-syntax - }).overrideLogicalId(`AssertionResults${id}`.replace(/[\@\/\-]/g, '')); + key: `AssertionResults${id}`.replace(/[\@\/\-]/g, ''), + }); } } }, diff --git a/packages/@aws-cdk/integ-tests-alpha/test/assertions/deploy-assert.test.ts b/packages/@aws-cdk/integ-tests-alpha/test/assertions/deploy-assert.test.ts index 5f6bbf064b2f2..8be678ccd662c 100644 --- a/packages/@aws-cdk/integ-tests-alpha/test/assertions/deploy-assert.test.ts +++ b/packages/@aws-cdk/integ-tests-alpha/test/assertions/deploy-assert.test.ts @@ -412,7 +412,7 @@ describe('User provided assertions stack', () => { integ.assertions.awsApiCall('Service', 'Api', { Reference: cr.ref }); // THEN - const integTemplate = Template.fromStack(integStack); + const integTemplate = Template.fromStack(integStack, { skipClean: true }); const assertionTemplate = Template.fromStack(assertionStack); integTemplate.resourceCountIs('Custom::Bar', 1); assertionTemplate.resourceCountIs('Custom::DeployAssert@SdkCallServiceApi', 1); diff --git a/packages/@aws-cdk/integ-tests-alpha/test/assertions/sdk.test.ts b/packages/@aws-cdk/integ-tests-alpha/test/assertions/sdk.test.ts index 2662606178621..f0360d059944d 100644 --- a/packages/@aws-cdk/integ-tests-alpha/test/assertions/sdk.test.ts +++ b/packages/@aws-cdk/integ-tests-alpha/test/assertions/sdk.test.ts @@ -171,7 +171,8 @@ describe('AwsApiCall', () => { }); // THEN - Template.fromStack(deplossert.scope).hasResourceProperties('Custom::DeployAssert@SdkCallMyServiceMyApi', { + const template = Template.fromStack(deplossert.scope); + template.hasResourceProperties('Custom::DeployAssert@SdkCallMyServiceMyApi', { service: 'MyService', api: 'MyApi', parameters: { @@ -180,7 +181,7 @@ describe('AwsApiCall', () => { }, expected: JSON.stringify({ $ObjectLike: { Key: 'Value' } }), }); - Template.fromStack(deplossert.scope).findResources('AWS::IAM::Role', { + template.findResources('AWS::IAM::Role', { SingletonFunction1488541a7b23466481b69b4408076b81Role37ABCE73: { Properties: { Policies: [ diff --git a/packages/aws-cdk-lib/assertions/lib/annotations.ts b/packages/aws-cdk-lib/assertions/lib/annotations.ts index 08f9b75f95f47..53b1eb833a08a 100644 --- a/packages/aws-cdk-lib/assertions/lib/annotations.ts +++ b/packages/aws-cdk-lib/assertions/lib/annotations.ts @@ -1,3 +1,4 @@ +import * as fs from 'fs-extra'; import { Messages } from './private/message'; import { findMessage, hasMessage, hasNoMessage } from './private/messages'; import { Stack, Stage } from '../../core'; @@ -12,8 +13,8 @@ export class Annotations { * Base your assertions on the messages returned by a synthesized CDK `Stack`. * @param stack the CDK Stack to run assertions on */ - public static fromStack(stack: Stack): Annotations { - return new Annotations(toMessages(stack)); + public static fromStack(stack: Stack, skipClean?: boolean): Annotations { + return new Annotations(toMessages(stack, skipClean)); } private readonly _messages: Messages; @@ -153,16 +154,33 @@ function convertMessagesTypeToArray(messages: Messages): SynthesisMessage[] { return Object.values(messages) as SynthesisMessage[]; } -function toMessages(stack: Stack): any { +function toMessages(stack: Stack, skipClean?: boolean): any { const root = stack.node.root; if (!Stage.isStage(root)) { throw new Error('unexpected: all stacks must be part of a Stage or an App'); } - // to support incremental assertions (i.e. "expect(stack).toNotContainSomething(); doSomething(); expect(stack).toContainSomthing()") - const force = true; + // We may have deleted all of this in a prior run so check and remake it if + // that is the case. + const outdir = root!.outdir; + const assetOutdir = root!.assetOutdir; - const assembly = root.synth({ force }); + if (!fs.existsSync(outdir)) { + fs.mkdirSync(outdir, { recursive: true }); + } + + if (!fs.existsSync(assetOutdir)) { + fs.mkdirSync(assetOutdir, { recursive: true }); + } + + const assembly = root.synth({ force: true }); + const messages = assembly.getStackArtifact(stack.artifactId).messages; + + if (skipClean !== true) { + // Now clean up after yourself + fs.rmSync(outdir, { recursive: true, force: true }); + fs.rmSync(assetOutdir, { recursive: true, force: true }); + } - return assembly.getStackArtifact(stack.artifactId).messages; + return messages; } diff --git a/packages/aws-cdk-lib/assertions/lib/tags.ts b/packages/aws-cdk-lib/assertions/lib/tags.ts index 82ce9a6146242..54ba8412a9bad 100644 --- a/packages/aws-cdk-lib/assertions/lib/tags.ts +++ b/packages/aws-cdk-lib/assertions/lib/tags.ts @@ -1,3 +1,4 @@ +import * as fs from 'fs-extra'; import { Match } from './match'; import { Matcher } from './matcher'; import { Stack, Stage } from '../../core'; @@ -76,16 +77,33 @@ export class Tags { } } -function getManifestTags(stack: Stack): ManifestTags { +function getManifestTags(stack: Stack, skipClean?: boolean): ManifestTags { const root = stack.node.root; if (!Stage.isStage(root)) { throw new Error('unexpected: all stacks must be part of a Stage or an App'); } - // synthesis is not forced: the stack will only be synthesized once regardless - // of the number of times this is called. - const assembly = root.synth(); + // We may have deleted all of this in a prior run so check and remake it if + // that is the case. + const outdir = root!.outdir; + const assetOutdir = root!.assetOutdir; + if (!fs.existsSync(outdir)) { + fs.mkdirSync(outdir, { recursive: true }); + } + + if (!fs.existsSync(assetOutdir)) { + fs.mkdirSync(assetOutdir, { recursive: true }); + } + + const assembly = root.synth({ force: true }); const artifact = assembly.getStackArtifact(stack.artifactId); + + if (skipClean !== true) { + // Now clean up after yourself + fs.rmSync(outdir, { recursive: true, force: true }); + fs.rmSync(assetOutdir, { recursive: true, force: true }); + } + return artifact.tags; } diff --git a/packages/aws-cdk-lib/assertions/lib/template.ts b/packages/aws-cdk-lib/assertions/lib/template.ts index 7395aa469e212..a5aa62a96261b 100644 --- a/packages/aws-cdk-lib/assertions/lib/template.ts +++ b/packages/aws-cdk-lib/assertions/lib/template.ts @@ -25,7 +25,7 @@ export class Template { * dependencies. */ public static fromStack(stack: Stack, templateParsingOptions?: TemplateParsingOptions): Template { - return new Template(toTemplate(stack), templateParsingOptions); + return new Template(toTemplate(stack, templateParsingOptions?.skipClean), templateParsingOptions); } /** @@ -50,6 +50,21 @@ export class Template { return new Template(JSON.parse(template), templateParsingOptions); } + /** + * Cleans up the stack created by this test + * @param stack + */ + public static clean(stack: Stack): void { + const stage = Stage.of(stack); + + if (!Stage.isStage(stage)) { + throw new Error('unexpected: all stacks must be part of a Stage or an App'); + } + + fs.rmSync(stage.outdir, { recursive: true, force: true }); + fs.rmSync(stage.assetOutdir, { recursive: true, force: true }); + } + private readonly template: TemplateType; private constructor(template: { [key: string]: any }, templateParsingOptions: TemplateParsingOptions = {}) { @@ -293,18 +308,54 @@ export interface TemplateParsingOptions { * @default false */ readonly skipCyclicalDependenciesCheck?: boolean; + + /** + * If set to true, will skip cleanup of the synthesized app generated by the + * + * @default false + */ + readonly skipClean?: boolean; } -function toTemplate(stack: Stack): any { +function toTemplate(stack: Stack, skipClean?: boolean): any { const stage = Stage.of(stack); + if (!Stage.isStage(stage)) { throw new Error('unexpected: all stacks must be part of a Stage or an App'); } - const assembly = stage.synth(); - if (stack.nestedStackParent) { - // if this is a nested stack (it has a parent), then just read the template as a string - return JSON.parse(fs.readFileSync(path.join(assembly.directory, stack.templateFile)).toString('utf-8')); + // We may have deleted all of this in a prior run so check and remake it if + // that is the case. + const outdir = stage!.outdir; + const assetOutdir = stage!.assetOutdir; + + if (!fs.existsSync(outdir)) { + fs.mkdirSync(outdir, { recursive: true }); + } + + if (!fs.existsSync(assetOutdir)) { + fs.mkdirSync(assetOutdir, { recursive: true }); } - return assembly.getStackArtifact(stack.artifactId).template; + + let assembly; + try { + // The only case this will fail for is integ-test-alpha's use of custom synthesis + assembly = stage.synth({ force: true }); + } catch (e) { + assembly = stage.synth(); + } + const template = stack.nestedStackParent ? + JSON.parse(fs.readFileSync(path.join(assembly.directory, stack.templateFile)).toString('utf-8')) : + assembly.getStackArtifact(stack.artifactId).template; + + if (skipClean !== true) { + // Now clean up after yourself + fs.rmSync(outdir, { recursive: true, force: true }); + fs.rmSync(assetOutdir, { recursive: true, force: true }); + } + + return template; } + +// https://github.com/aws/aws-cdk/pull/30836 +// https://github.com/aws/aws-cdk/pull/30758 \ No newline at end of file diff --git a/packages/aws-cdk-lib/aws-cloudformation/test/nested-stack.test.ts b/packages/aws-cdk-lib/aws-cloudformation/test/nested-stack.test.ts index 13ff15dd7883c..92e6061228347 100644 --- a/packages/aws-cdk-lib/aws-cloudformation/test/nested-stack.test.ts +++ b/packages/aws-cdk-lib/aws-cloudformation/test/nested-stack.test.ts @@ -460,7 +460,7 @@ describeDeprecated('NestedStack', () => { const assembly = app.synth(); // nested stack should output this value as if it was referenced by the parent (without the export) - Template.fromStack(nestedUnderStack1).templateMatches({ + Template.fromStack(nestedUnderStack1, { skipClean: true }).templateMatches({ Resources: { ResourceInNestedStack: { Type: 'MyResource', @@ -487,7 +487,7 @@ describeDeprecated('NestedStack', () => { }); // consuming stack should use ImportValue to import the value from the parent stack - Template.fromStack(stack2).templateMatches({ + Template.fromStack(stack2, { skipClean: true }).templateMatches({ Resources: { ResourceInStack2: { Type: 'JustResource', @@ -506,6 +506,8 @@ describeDeprecated('NestedStack', () => { expect(stack1Artifact.dependencies.length).toEqual(0); expect(stack2Artifact.dependencies.length).toEqual(1); expect(stack2Artifact.dependencies[0]).toEqual(stack1Artifact); + Template.clean(stack1); + Template.clean(stack2); }); test('references between sibling nested stacks should output from one and getAtt from the other', () => { diff --git a/packages/aws-cdk-lib/aws-iam/test/precreated-role.test.ts b/packages/aws-cdk-lib/aws-iam/test/precreated-role.test.ts index b173a63e55c04..dcd31c7771646 100644 --- a/packages/aws-cdk-lib/aws-iam/test/precreated-role.test.ts +++ b/packages/aws-cdk-lib/aws-iam/test/precreated-role.test.ts @@ -46,7 +46,7 @@ describe('precreatedRole report created', () => { ], }); - Template.fromStack(otherStack).resourceCountIs('AWS::IAM::Role', 0); + Template.fromStack(otherStack, { skipClean: true }).resourceCountIs('AWS::IAM::Role', 0); const assembly = app.synth(); const filePath = path.join(assembly.directory, 'iam-policy-report.json'); const file = fs.readFileSync(filePath, { encoding: 'utf-8' }); @@ -73,6 +73,7 @@ describe('precreatedRole report created', () => { }], }], }); + Template.clean(otherStack); }); test('with managed policies', () => { diff --git a/packages/aws-cdk-lib/aws-lambda/test/function.test.ts b/packages/aws-cdk-lib/aws-lambda/test/function.test.ts index f3ad8836102e5..c81a796c59cea 100644 --- a/packages/aws-cdk-lib/aws-lambda/test/function.test.ts +++ b/packages/aws-cdk-lib/aws-lambda/test/function.test.ts @@ -2639,7 +2639,7 @@ describe('function', () => { ], }); - Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { + Template.fromStack(stack, { skipClean: true }).hasResourceProperties('AWS::Lambda::Function', { Environment: { Variables: { AWS_CODEGURU_PROFILER_GROUP_NAME: 'profiler_group', @@ -2651,6 +2651,7 @@ describe('function', () => { }); Annotations.fromStack(stack).hasWarning('/Default/MyLambda', Match.stringLikeRegexp('AWS_CODEGURU_PROFILER_GROUP_NAME, AWS_CODEGURU_PROFILER_GROUP_ARN, AWS_CODEGURU_PROFILER_TARGET_REGION, and AWS_CODEGURU_PROFILER_ENABLED should not be set when profiling options enabled')); + Template.clean(stack); }); test('default function with client provided Profiling Group and client provided env vars', () => { diff --git a/packages/aws-cdk-lib/aws-synthetics/test/code.test.ts b/packages/aws-cdk-lib/aws-synthetics/test/code.test.ts index 3b3573c65cf7b..b68f9a2cb6d7b 100644 --- a/packages/aws-cdk-lib/aws-synthetics/test/code.test.ts +++ b/packages/aws-cdk-lib/aws-synthetics/test/code.test.ts @@ -55,13 +55,14 @@ describe(synthetics.Code.fromAsset, () => { }); // THEN - Template.fromStack(stack).hasResourceProperties('AWS::Synthetics::Canary', { + Template.fromStack(stack, { skipClean: true }).hasResourceProperties('AWS::Synthetics::Canary', { Code: { Handler: 'canary.handler', S3Bucket: stack.resolve(directoryAsset.bind(stack, 'canary.handler', synthetics.RuntimeFamily.NODEJS).s3Location?.bucketName), S3Key: stack.resolve(directoryAsset.bind(stack, 'canary.handler', synthetics.RuntimeFamily.NODEJS).s3Location?.objectKey), }, }); + Template.clean(stack); }); test('fromAsset works for python runtimes', () => { @@ -79,13 +80,14 @@ describe(synthetics.Code.fromAsset, () => { }); // THEN - Template.fromStack(stack).hasResourceProperties('AWS::Synthetics::Canary', { + Template.fromStack(stack, { skipClean: true }).hasResourceProperties('AWS::Synthetics::Canary', { Code: { Handler: 'canary.handler', S3Bucket: stack.resolve(directoryAsset.bind(stack, 'canary.handler', synthetics.RuntimeFamily.PYTHON).s3Location?.bucketName), S3Key: stack.resolve(directoryAsset.bind(stack, 'canary.handler', synthetics.RuntimeFamily.PYTHON).s3Location?.objectKey), }, }); + Template.clean(stack); }); test('only one Asset object gets created even if multiple canaries use the same AssetCode', () => { diff --git a/packages/aws-cdk-lib/pipelines/test/codepipeline/codepipeline.test.ts b/packages/aws-cdk-lib/pipelines/test/codepipeline/codepipeline.test.ts index ed265b78327f0..6a9aa2cd064c8 100644 --- a/packages/aws-cdk-lib/pipelines/test/codepipeline/codepipeline.test.ts +++ b/packages/aws-cdk-lib/pipelines/test/codepipeline/codepipeline.test.ts @@ -133,7 +133,7 @@ test('Policy sizes do not exceed the maximum size', () => { } // THEN - const template = Template.fromStack(pipelineStack); + const template = Template.fromStack(pipelineStack, { skipClean: true }); // Collect policies by role const rolePolicies: Record = {}; @@ -183,7 +183,7 @@ test('CodeBuild action role has the right AssumeRolePolicyDocument', () => { const pipelineStack = new cdk.Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); - const template = Template.fromStack(pipelineStack); + const template = Template.fromStack(pipelineStack, { skipClean: true }); template.hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ @@ -204,7 +204,7 @@ test('CodeBuild asset role has the right Principal with the feature enabled', () const pipelineStack = new cdk.Stack(stack, 'PipelineStack', { env: PIPELINE_ENV }); const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); pipeline.addStage(new FileAssetApp(pipelineStack, 'App', {}));; - const template = Template.fromStack(pipelineStack); + const template = Template.fromStack(pipelineStack, { skipClean: true }); const assetRole = template.toJSON().Resources.CdkAssetsFileRole6BE17A07; const statementLength = assetRole.Properties.AssumeRolePolicyDocument.Statement; expect(statementLength).toStrictEqual( @@ -226,7 +226,7 @@ test('CodeBuild asset role has the right Principal with the feature disabled', ( const pipelineStack = new cdk.Stack(stack, 'PipelineStack', { env: PIPELINE_ENV }); const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); pipeline.addStage(new FileAssetApp(pipelineStack, 'App', {}));; - const template = Template.fromStack(pipelineStack); + const template = Template.fromStack(pipelineStack, { skipClean: true }); const assetRole = template.toJSON().Resources.CdkAssetsFileRole6BE17A07; const statementLength = assetRole.Properties.AssumeRolePolicyDocument.Statement; expect(statementLength).toStrictEqual( @@ -299,7 +299,7 @@ test('CodePipeline enables key rotation on cross account keys', ()=>{ }), }); - const template = Template.fromStack(pipelineStack); + const template = Template.fromStack(pipelineStack, { skipClean: true }); template.hasResourceProperties('AWS::KMS::Key', { EnableKeyRotation: true, @@ -331,7 +331,7 @@ test('CodePipeline supports use of existing role', () => { }), }); - const template = Template.fromStack(pipelineStack); + const template = Template.fromStack(pipelineStack, { skipClean: true }); template.hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ @@ -360,7 +360,7 @@ describe('deployment of stack', () => { pipeline.addStage(new FileAssetApp(pipelineStack, 'App', {})); // THEN - const template = Template.fromStack(pipelineStack); + const template = Template.fromStack(pipelineStack, { skipClean: true }); // There should be Prepare step in piepline template.hasResourceProperties('AWS::CodePipeline::Pipeline', { @@ -393,7 +393,7 @@ describe('deployment of stack', () => { pipeline.addStage(new FileAssetApp(pipelineStack, 'App', {})); // THEN - const template = Template.fromStack(pipelineStack); + const template = Template.fromStack(pipelineStack, { skipClean: true }); // There should be Prepare step in piepline template.hasResourceProperties('AWS::CodePipeline::Pipeline', { @@ -435,7 +435,7 @@ test('action name is calculated properly if it has cross-stack dependencies', () }); // THEN - const template = Template.fromStack(pipelineStack); + const template = Template.fromStack(pipelineStack, { skipClean: true }); template.hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: Match.arrayWith([{ Name: 'TheApp', @@ -466,7 +466,7 @@ test('synths with change set approvers', () => { }); // THEN - const template = Template.fromStack(pipelineStack); + const template = Template.fromStack(pipelineStack, { skipClean: true }); template.hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: Match.arrayWith([{ Name: 'TheApp', @@ -517,7 +517,7 @@ test('artifactBucket can be overridden', () => { }), }); // THEN - const template = Template.fromStack(pipelineStack); + const template = Template.fromStack(pipelineStack, { skipClean: true }); template.hasResourceProperties('AWS::S3::Bucket', { BucketName: 'my-custom-artifact-bucket', }); diff --git a/packages/aws-cdk-lib/pipelines/test/compliance/assets.test.ts b/packages/aws-cdk-lib/pipelines/test/compliance/assets.test.ts index 1f222fee6c439..4da4bd0ebb675 100644 --- a/packages/aws-cdk-lib/pipelines/test/compliance/assets.test.ts +++ b/packages/aws-cdk-lib/pipelines/test/compliance/assets.test.ts @@ -42,7 +42,7 @@ describe('basic pipeline', () => { function THEN_codePipelineExpectation() { // THEN - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: Match.not(Match.arrayWith([Match.objectLike({ Name: 'Assets', })])), @@ -67,7 +67,7 @@ describe('basic pipeline', () => { }); function THEN_codePipelineExpectation() { - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: [ Match.objectLike({ Name: 'Source' }), Match.objectLike({ Name: 'Build' }), @@ -96,7 +96,7 @@ describe('basic pipeline', () => { }); function THEN_codePipelineExpectation() { - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: [ Match.objectLike({ Name: 'Source' }), Match.objectLike({ Name: 'Build' }), @@ -126,7 +126,7 @@ describe('basic pipeline', () => { }); function THEN_codePipelineExpectation() { - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: [ Match.objectLike({ Name: 'Source' }), Match.objectLike({ Name: 'Build' }), @@ -155,7 +155,7 @@ describe('basic pipeline', () => { }); function THEN_codePipelineExpectation() { - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: [ Match.objectLike({ Name: 'Source' }), Match.objectLike({ Name: 'Build' }), @@ -186,7 +186,7 @@ describe('basic pipeline', () => { }); function THEN_codePipelineExpectation() { - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { Image: CDKP_DEFAULT_CODEBUILD_IMAGE.imageId, }, @@ -220,7 +220,7 @@ describe('basic pipeline', () => { }); function THEN_codePipelineExpectation() { - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: Match.arrayWith([{ Name: 'Assets', Actions: [ @@ -242,7 +242,7 @@ describe('basic pipeline', () => { pipeline.addStage('SomeStage').addStackArtifactDeployment(asm.getStackByName('FileAssetApp-Stack')); // THEN - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: Match.arrayWith([{ Name: 'Assets', Actions: [ @@ -277,7 +277,7 @@ describe('basic pipeline', () => { }); function THEN_codePipelineExpectation() { - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodeBuild::Project', { Source: { BuildSpec: Match.serializedJson(Match.objectLike({ phases: { @@ -311,7 +311,7 @@ describe('basic pipeline', () => { }); function THEN_codePipelineExpectation() { - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodeBuild::Project', { Source: { BuildSpec: Match.serializedJson(Match.objectLike({ phases: { @@ -349,7 +349,7 @@ describe('basic pipeline', () => { }); function THEN_codePipelineExpectation() { - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { Image: CDKP_DEFAULT_CODEBUILD_IMAGE.imageId, }, @@ -386,7 +386,7 @@ describe('basic pipeline', () => { }); function THEN_codePipelineExpectation() { - Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Role', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -399,7 +399,7 @@ describe('basic pipeline', () => { ], }, }); - Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Policy', + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::IAM::Policy', expectedAssetRolePolicy(FILE_PUBLISHING_ROLE, 'CdkAssetsFileRole6BE17A07')); } }); @@ -436,7 +436,7 @@ describe('basic pipeline', () => { }); function THEN_codePipelineExpectation() { - Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Policy', + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::IAM::Policy', expectedAssetRolePolicy([FILE_PUBLISHING_ROLE, 'arn:${AWS::Partition}:iam::0123456789012:role/cdk-hnb659fds-file-publishing-role-0123456789012-eu-west-1'], 'CdkAssetsFileRole6BE17A07')); } @@ -465,7 +465,7 @@ describe('basic pipeline', () => { }); function THEN_codePipelineExpectation() { - Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Policy', + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::IAM::Policy', expectedAssetRolePolicy(FILE_PUBLISHING_ROLE, 'CdkAssetsFileRole6BE17A07')); } }); @@ -489,7 +489,7 @@ describe('basic pipeline', () => { }); function THEN_codePipelineExpectation() { - Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Role', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -502,7 +502,7 @@ describe('basic pipeline', () => { ], }, }); - Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Policy', + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::IAM::Policy', expectedAssetRolePolicy(IMAGE_PUBLISHING_ROLE, 'CdkAssetsDockerRole484B6DD3')); } }); @@ -528,9 +528,9 @@ describe('basic pipeline', () => { }); function THEN_codePipelineExpectation() { - Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Policy', + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::IAM::Policy', expectedAssetRolePolicy(FILE_PUBLISHING_ROLE, 'CdkAssetsFileRole6BE17A07')); - Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Policy', + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::IAM::Policy', expectedAssetRolePolicy(IMAGE_PUBLISHING_ROLE, 'CdkAssetsDockerRole484B6DD3')); } }); @@ -570,7 +570,7 @@ behavior('can supply pre-install scripts to asset upload', (suite) => { }); function THEN_codePipelineExpectation() { - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { Image: CDKP_DEFAULT_CODEBUILD_IMAGE.imageId, }, @@ -614,7 +614,7 @@ describe('pipeline with VPC', () => { function THEN_codePipelineExpectation() { // THEN - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodeBuild::Project', { VpcConfig: Match.objectLike({ SecurityGroupIds: [ { 'Fn::GetAtt': ['CdkAssetsDockerAsset1SecurityGroup078F5C66', 'GroupId'] }, @@ -649,7 +649,7 @@ describe('pipeline with VPC', () => { function THEN_codePipelineExpectation() { // Assets Project - Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Policy', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::IAM::Policy', { Roles: [ { Ref: 'CdkAssetsDockerRole484B6DD3' }, ], @@ -684,7 +684,7 @@ describe('pipeline with VPC', () => { function THEN_codePipelineExpectation() { // Assets Project - Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Policy', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ Match.objectLike({ @@ -703,7 +703,7 @@ describe('pipeline with VPC', () => { }, Roles: [{ Ref: 'CdkAssetsDockerRole484B6DD3' }], }); - Template.fromStack(pipelineStack).hasResource('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResource('AWS::CodeBuild::Project', { Properties: { ServiceRole: { 'Fn::GetAtt': ['CdkAssetsDockerRole484B6DD3', 'Arn'] }, }, @@ -738,7 +738,7 @@ describe('pipeline with single asset publisher', () => { function THEN_codePipelineExpectation() { // THEN const buildSpecName = new Capture(stringLike('buildspec-*.yaml')); - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: Match.arrayWith([{ Name: 'Assets', Actions: [ @@ -747,7 +747,7 @@ describe('pipeline with single asset publisher', () => { ], }]), }); - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { Image: CDKP_DEFAULT_CODEBUILD_IMAGE.imageId, }, @@ -762,6 +762,7 @@ describe('pipeline with single asset publisher', () => { const buildSpec = JSON.parse(fs.readFileSync(path.join(assembly.directory, actualFileName), { encoding: 'utf-8' })); expect(buildSpec.phases.build.commands).toContain(`cdk-assets --path "assembly-FileAssetApp/FileAssetAppStackEADD68C5.assets.json" --verbose publish "${FILE_ASSET_SOURCE_HASH}:current_account-current_region"`); expect(buildSpec.phases.build.commands).toContain(`cdk-assets --path "assembly-FileAssetApp/FileAssetAppStackEADD68C5.assets.json" --verbose publish "${FILE_ASSET_SOURCE_HASH2}:current_account-current_region"`); + Template.clean(pipelineStack); } }); @@ -800,7 +801,7 @@ describe('pipeline with single asset publisher', () => { // THEN const buildSpecName1 = new Capture(stringLike('buildspec-*.yaml')); const buildSpecName2 = new Capture(stringLike('buildspec-*.yaml')); - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodeBuild::Project', { Source: { BuildSpec: buildSpecName1, }, @@ -864,7 +865,7 @@ describe('pipeline with custom asset publisher BuildSpec', () => { function THEN_codePipelineExpectation() { const buildSpecName = new Capture(stringLike('buildspec-*')); - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: Match.arrayWith([{ Name: 'Assets', Actions: [ @@ -873,7 +874,7 @@ describe('pipeline with custom asset publisher BuildSpec', () => { ], }]), }); - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { Image: CDKP_DEFAULT_CODEBUILD_IMAGE.imageId, }, @@ -887,6 +888,7 @@ describe('pipeline with custom asset publisher BuildSpec', () => { expect(buildSpec.phases.build.commands).toContain(`cdk-assets --path "assembly-FileAssetApp/FileAssetAppStackEADD68C5.assets.json" --verbose publish "${FILE_ASSET_SOURCE_HASH2}:current_account-current_region"`); expect(buildSpec.phases.pre_install.commands).toContain('preinstall'); expect(buildSpec.cache.paths).toContain('node_modules'); + Template.clean(pipelineStack); } }); }); @@ -969,7 +971,7 @@ behavior('necessary secrets manager permissions get added to asset roles', suite }); function THEN_codePipelineExpectation() { - Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Policy', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: Match.arrayWith([{ Action: 'secretsmanager:GetSecretValue', @@ -1012,7 +1014,7 @@ behavior('adding environment variable to assets job adds SecretsManager permissi }); pipeline.addStage(new FileAssetApp(pipelineStack, 'MyApp')); - Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Policy', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: Match.arrayWith([ Match.objectLike({ @@ -1038,7 +1040,20 @@ function synthesize(stack: Stack) { throw new Error('unexpected: all stacks must be part of a Stage'); } - return root.synth({ skipValidation: true }); + // We may have deleted all of this in a prior run so check and remake it if + // that is the case. + const outdir = root!.outdir; + const assetOutdir = root!.assetOutdir; + + if (!fs.existsSync(outdir)) { + fs.mkdirSync(outdir, { recursive: true }); + } + + if (!fs.existsSync(assetOutdir)) { + fs.mkdirSync(assetOutdir, { recursive: true }); + } + + return root.synth({ skipValidation: true, force: true }); } function unsingleton(xs: A[]): A | A[] { diff --git a/packages/aws-cdk-lib/pipelines/test/compliance/basic-behavior.test.ts b/packages/aws-cdk-lib/pipelines/test/compliance/basic-behavior.test.ts index 75d58084dadfb..0e96461442429 100644 --- a/packages/aws-cdk-lib/pipelines/test/compliance/basic-behavior.test.ts +++ b/packages/aws-cdk-lib/pipelines/test/compliance/basic-behavior.test.ts @@ -190,7 +190,7 @@ behavior('tags get reflected in pipeline', (suite) => { function THEN_codePipelineExpectation() { // THEN const templateConfig = new Capture(); - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: Match.arrayWith([{ Name: 'App', Actions: Match.arrayWith([ @@ -215,6 +215,7 @@ behavior('tags get reflected in pipeline', (suite) => { CostCenter: 'F00B4R', }, })); + Template.clean(pipelineStack); } }); diff --git a/packages/aws-cdk-lib/pipelines/test/compliance/docker-credentials.test.ts b/packages/aws-cdk-lib/pipelines/test/compliance/docker-credentials.test.ts index e0266239dff2d..2951acc8d8aaf 100644 --- a/packages/aws-cdk-lib/pipelines/test/compliance/docker-credentials.test.ts +++ b/packages/aws-cdk-lib/pipelines/test/compliance/docker-credentials.test.ts @@ -51,7 +51,7 @@ behavior('synth action receives install commands and access to relevant credenti domainCredentials: { 'synth.example.com': { secretsManagerSecretId: secretSynthArn } }, }); - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { Image: CDKP_DEFAULT_CODEBUILD_IMAGE.imageId }, Source: { BuildSpec: Match.serializedJson(Match.objectLike({ @@ -70,7 +70,7 @@ behavior('synth action receives install commands and access to relevant credenti })), }, }); - Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Policy', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: Match.arrayWith([{ Action: ['secretsmanager:GetSecretValue', 'secretsmanager:DescribeSecret'], @@ -121,7 +121,7 @@ behavior('synth action receives Windows install commands if a Windows image is d domainCredentials: { 'synth.example.com': { secretsManagerSecretId: secretSynthArn } }, }); - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { Image: 'aws/codebuild/windows-base:2.0' }, Source: { BuildSpec: Match.serializedJson(Match.objectLike({ @@ -164,7 +164,7 @@ behavior('self-update receives install commands and access to relevant credentia domainCredentials: { 'selfupdate.example.com': { secretsManagerSecretId: secretUpdateArn } }, }); - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { Image: CDKP_DEFAULT_CODEBUILD_IMAGE.imageId }, Source: { BuildSpec: Match.serializedJson(Match.objectLike({ @@ -185,7 +185,7 @@ behavior('self-update receives install commands and access to relevant credentia })), }, }); - Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Policy', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: Match.arrayWith([{ Action: ['secretsmanager:GetSecretValue', 'secretsmanager:DescribeSecret'], @@ -220,7 +220,7 @@ behavior('asset publishing receives install commands and access to relevant cred domainCredentials: { 'publish.example.com': { secretsManagerSecretId: secretPublishArn } }, }); - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { Image: CDKP_DEFAULT_CODEBUILD_IMAGE.imageId }, Source: { BuildSpec: Match.serializedJson(Match.objectLike({ @@ -239,7 +239,7 @@ behavior('asset publishing receives install commands and access to relevant cred })), }, }); - Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Policy', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: Match.arrayWith([{ Action: ['secretsmanager:GetSecretValue', 'secretsmanager:DescribeSecret'], diff --git a/packages/aws-cdk-lib/pipelines/test/compliance/environments.test.ts b/packages/aws-cdk-lib/pipelines/test/compliance/environments.test.ts index 777ffb83a0d2c..abeeb8e07ec04 100644 --- a/packages/aws-cdk-lib/pipelines/test/compliance/environments.test.ts +++ b/packages/aws-cdk-lib/pipelines/test/compliance/environments.test.ts @@ -20,7 +20,7 @@ behavior('action has right settings for same-env deployment', (suite) => { const pipeline = new LegacyTestGitHubNpmPipeline(pipelineStack, 'Cdk'); pipeline.addApplicationStage(new OneStackApp(app, 'Same')); - THEN_codePipelineExpection(agnosticRole); + THEN_codePipelineExpectation(agnosticRole); }); suite.additional('legacy: even if env is specified but the same as the pipeline', () => { @@ -29,14 +29,14 @@ behavior('action has right settings for same-env deployment', (suite) => { env: PIPELINE_ENV, })); - THEN_codePipelineExpection(pipelineEnvRole); + THEN_codePipelineExpectation(pipelineEnvRole); }); suite.modern(() => { const pipeline = new ModernTestGitHubNpmPipeline(pipelineStack, 'Cdk'); pipeline.addStage(new OneStackApp(app, 'Same')); - THEN_codePipelineExpection(agnosticRole); + THEN_codePipelineExpectation(agnosticRole); }); suite.additional('modern: even if env is specified but the same as the pipeline', () => { @@ -45,12 +45,12 @@ behavior('action has right settings for same-env deployment', (suite) => { env: PIPELINE_ENV, })); - THEN_codePipelineExpection(pipelineEnvRole); + THEN_codePipelineExpectation(pipelineEnvRole); }); - function THEN_codePipelineExpection(roleArn: (x: string) => any) { + function THEN_codePipelineExpectation(roleArn: (x: string) => any) { // THEN: pipeline structure is correct - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: Match.arrayWith([{ Name: 'Same', Actions: [ @@ -74,7 +74,7 @@ behavior('action has right settings for same-env deployment', (suite) => { }); // THEN: artifact bucket can be read by deploy role - Template.fromStack(pipelineStack).hasResourceProperties('AWS::S3::BucketPolicy', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::S3::BucketPolicy', { PolicyDocument: { Statement: Match.arrayWith([Match.objectLike({ Action: ['s3:GetObject*', 's3:GetBucket*', 's3:List*'], @@ -107,8 +107,8 @@ behavior('action has right settings for cross-account deployment', (suite) => { }); function THEN_codePipelineExpectation() { - // THEN: Pipelien structure is correct - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + // THEN: Pipeline structure is correct + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: Match.arrayWith([{ Name: 'CrossAccount', Actions: [ @@ -153,7 +153,7 @@ behavior('action has right settings for cross-account deployment', (suite) => { }); // THEN: Artifact bucket can be read by deploy role - Template.fromStack(pipelineStack).hasResourceProperties('AWS::S3::BucketPolicy', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::S3::BucketPolicy', { PolicyDocument: { Statement: Match.arrayWith([Match.objectLike({ Action: ['s3:GetObject*', 's3:GetBucket*', 's3:List*'], @@ -193,7 +193,7 @@ behavior('action has right settings for cross-region deployment', (suite) => { function THEN_codePipelineExpectation() { // THEN - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: Match.arrayWith([{ Name: 'CrossRegion', Actions: [ @@ -283,7 +283,7 @@ behavior('action has right settings for cross-account/cross-region deployment', // THEN: pipeline structure must be correct const stack = app.stackArtifact(pipelineStack); expect(stack).toBeDefined(); - Template.fromStack(stack!).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Template.fromStack(stack!, { skipClean: true }).hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: Match.arrayWith([{ Name: 'CrossBoth', Actions: [ @@ -329,7 +329,7 @@ behavior('action has right settings for cross-account/cross-region deployment', // THEN: artifact bucket can be read by deploy role const supportStack = app.stackArtifact('PipelineStack-support-elsewhere'); expect(supportStack).toBeDefined(); - Template.fromStack(supportStack!).hasResourceProperties('AWS::S3::BucketPolicy', { + Template.fromStack(supportStack!, { skipClean: true }).hasResourceProperties('AWS::S3::BucketPolicy', { PolicyDocument: { Statement: Match.arrayWith([Match.objectLike({ Action: Match.arrayWith(['s3:GetObject*', 's3:GetBucket*', 's3:List*']), @@ -347,7 +347,7 @@ behavior('action has right settings for cross-account/cross-region deployment', }); // And the key to go along with it - Template.fromStack(supportStack!).hasResourceProperties('AWS::KMS::Key', { + Template.fromStack(supportStack!, { skipClean: true }).hasResourceProperties('AWS::KMS::Key', { KeyPolicy: { Statement: Match.arrayWith([Match.objectLike({ Action: Match.arrayWith(['kms:Decrypt', 'kms:DescribeKey']), diff --git a/packages/aws-cdk-lib/pipelines/test/compliance/security-check.test.ts b/packages/aws-cdk-lib/pipelines/test/compliance/security-check.test.ts index 88138cb2b840f..cf2fd669fdf06 100644 --- a/packages/aws-cdk-lib/pipelines/test/compliance/security-check.test.ts +++ b/packages/aws-cdk-lib/pipelines/test/compliance/security-check.test.ts @@ -41,7 +41,7 @@ behavior('security check option generates lambda/codebuild at pipeline scope', ( }); function THEN_codePipelineExpectation() { - const template = Template.fromStack(pipelineStack); + const template = Template.fromStack(pipelineStack, { skipClean: true }); template.resourceCountIs('AWS::Lambda::Function', 1); template.hasResourceProperties('AWS::Lambda::Function', { Role: { @@ -89,7 +89,7 @@ behavior('security check option passes correct environment variables to check pr }); function THEN_codePipelineExpectation() { - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: Match.arrayWith([ { Name: 'App', @@ -135,7 +135,7 @@ behavior('pipeline created with auto approve tags and lambda/codebuild w/ valid function THEN_codePipelineExpectation() { // CodePipeline must be tagged as SECURITY_CHECK=ALLOW_APPROVE - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodePipeline::Pipeline', { Tags: [ { Key: 'SECURITY_CHECK', @@ -144,7 +144,7 @@ behavior('pipeline created with auto approve tags and lambda/codebuild w/ valid ], }); // Lambda Function only has access to pipelines tagged SECURITY_CHECK=ALLOW_APPROVE - Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Policy', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -159,7 +159,7 @@ behavior('pipeline created with auto approve tags and lambda/codebuild w/ valid }, }); // CodeBuild must have access to the stacks and invoking the lambda function - Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Policy', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: Match.arrayWith([ { @@ -220,7 +220,7 @@ behavior('confirmBroadeningPermissions option at addApplicationStage runs securi suite.doesNotApply.modern(); function THEN_codePipelineExpectation() { - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: [ { Actions: [Match.objectLike({ Name: 'GitHub', RunOrder: 1 })], @@ -267,7 +267,7 @@ behavior('confirmBroadeningPermissions option at addApplication runs security ch suite.doesNotApply.modern(); function THEN_codePipelineExpectation() { - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: [ { Actions: [Match.objectLike({ Name: 'GitHub', RunOrder: 1 })], @@ -326,8 +326,8 @@ behavior('confirmBroadeningPermissions and notification topic options generates }); function THEN_codePipelineExpectation() { - Template.fromStack(pipelineStack).resourceCountIs('AWS::SNS::Topic', 1); - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack, { skipClean: true }).resourceCountIs('AWS::SNS::Topic', 1); + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: Match.arrayWith([ { Name: 'MyStack', @@ -392,10 +392,10 @@ behavior('Stages declared outside the pipeline create their own ApplicationSecur suite.doesNotApply.modern(); function THEN_codePipelineExpectation() { - Template.fromStack(pipelineStack).resourceCountIs('AWS::Lambda::Function', 1); + Template.fromStack(pipelineStack, { skipClean: true }).resourceCountIs('AWS::Lambda::Function', 1); // 1 for github build, 1 for synth stage, and 1 for the application security check - Template.fromStack(pipelineStack).resourceCountIs('AWS::CodeBuild::Project', 3); - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack, { skipClean: true }).resourceCountIs('AWS::CodeBuild::Project', 3); + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodePipeline::Pipeline', { Tags: [ { Key: 'SECURITY_CHECK', diff --git a/packages/aws-cdk-lib/pipelines/test/compliance/validations.test.ts b/packages/aws-cdk-lib/pipelines/test/compliance/validations.test.ts index f1a560fdae911..0d654e4a2a947 100644 --- a/packages/aws-cdk-lib/pipelines/test/compliance/validations.test.ts +++ b/packages/aws-cdk-lib/pipelines/test/compliance/validations.test.ts @@ -37,7 +37,7 @@ behavior('can add manual approval after app', (suite) => { }); // THEN - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: Match.arrayWith([{ Name: 'MyApp', Actions: sortByRunOrder([ @@ -69,7 +69,7 @@ behavior('can add steps to wave', (suite) => { wave.addStage(new OneStackApp(pipelineStack, 'Stage3')); // THEN - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: Match.arrayWith([{ Name: 'MyWave', Actions: sortByRunOrder([ @@ -103,7 +103,7 @@ behavior('script validation steps can use stack outputs as environment variables })); // THEN - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: Match.arrayWith([{ Name: 'MyApp', Actions: Match.arrayWith([ @@ -128,7 +128,7 @@ behavior('script validation steps can use stack outputs as environment variables }]), }); - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { Image: CDKP_DEFAULT_CODEBUILD_IMAGE.imageId, }, @@ -163,7 +163,7 @@ behavior('script validation steps can use stack outputs as environment variables }); // THEN - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: Match.arrayWith([{ Name: 'Alpha', Actions: Match.arrayWith([ @@ -199,7 +199,7 @@ behavior('stackOutput generates names limited to 100 characters', (suite) => { })); // THEN - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: Match.arrayWith([{ Name: 'APreposterouslyLongAndComplicatedNameMadeUpJustToMakeItExceedTheLimitDefinedByCodeBuild', Actions: Match.arrayWith([ @@ -239,7 +239,7 @@ behavior('stackOutput generates names limited to 100 characters', (suite) => { ], }); - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: Match.arrayWith([{ Name: 'APreposterouslyLongAndComplicatedNameMadeUpJustToMakeItExceedTheLimitDefinedByCodeBuild', Actions: Match.arrayWith([ @@ -284,7 +284,7 @@ behavior('validation step can run from scripts in source', (suite) => { function THEN_codePipelineExpectation() { const sourceArtifact = new Capture(); - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: Match.arrayWith([{ Name: 'Source', Actions: [ @@ -294,7 +294,7 @@ behavior('validation step can run from scripts in source', (suite) => { ], }]), }); - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: Match.arrayWith([{ Name: 'Test', Actions: Match.arrayWith([ @@ -305,7 +305,7 @@ behavior('validation step can run from scripts in source', (suite) => { ]), }]), }); - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { Image: CDKP_DEFAULT_CODEBUILD_IMAGE.imageId, }, @@ -362,7 +362,7 @@ behavior('can use additional output artifacts from build', (suite) => { function THEN_codePipelineExpectation() { const integArtifact = new Capture(); - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: Match.arrayWith([{ Name: 'Build', Actions: [ @@ -377,7 +377,7 @@ behavior('can use additional output artifacts from build', (suite) => { }]), }); - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: Match.arrayWith([{ Name: 'Test', Actions: Match.arrayWith([ @@ -388,7 +388,7 @@ behavior('can use additional output artifacts from build', (suite) => { ]), }]), }); - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { Image: CDKP_DEFAULT_CODEBUILD_IMAGE.imageId, }, @@ -449,7 +449,7 @@ behavior('can add policy statements to shell script action', (suite) => { function THEN_codePipelineExpectation() { // THEN - Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Policy', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: Match.arrayWith([Match.objectLike({ Action: 's3:Banana', @@ -501,7 +501,7 @@ behavior('can grant permissions to shell script action', (suite) => { function THEN_codePipelineExpectation() { // THEN - Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Policy', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: Match.arrayWith([Match.objectLike({ Action: ['s3:GetObject*', 's3:GetBucket*', 's3:List*'], @@ -561,7 +561,7 @@ behavior('can run shell script actions in a VPC', (suite) => { }); function THEN_codePipelineExpectation() { - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { Image: CDKP_DEFAULT_CODEBUILD_IMAGE.imageId, }, @@ -635,7 +635,7 @@ behavior('can run shell script actions with a specific SecurityGroup', (suite) = }); function THEN_codePipelineExpectation() { - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: Match.arrayWith([{ Name: 'Test', Actions: Match.arrayWith([ @@ -645,7 +645,7 @@ behavior('can run shell script actions with a specific SecurityGroup', (suite) = ]), }]), }); - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodeBuild::Project', { VpcConfig: { SecurityGroupIds: [ { @@ -713,7 +713,7 @@ behavior('can run scripts with specified BuildEnvironment', (suite) => { }); function THEN_codePipelineExpectation() { - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { Image: 'aws/codebuild/standard:2.0', }, @@ -754,7 +754,7 @@ behavior('can run scripts with magic environment variables', (suite) => { function THEN_codePipelineExpectation() { // THEN - Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack, { skipClean: true }).hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: Match.arrayWith([{ Name: 'Test', Actions: Match.arrayWith([