diff --git a/packages/@aws-cdk-testing/cli-integ/package.json b/packages/@aws-cdk-testing/cli-integ/package.json index 162353a78423c..bb22a3e5eafee 100644 --- a/packages/@aws-cdk-testing/cli-integ/package.json +++ b/packages/@aws-cdk-testing/cli-integ/package.json @@ -41,6 +41,7 @@ "@octokit/rest": "^18.12.0", "aws-sdk": "^2.1653.0", "axios": "^1.7.2", + "chalk": "^4", "fs-extra": "^9.1.0", "glob": "^7.2.3", "jest": "^29.7.0", @@ -72,4 +73,4 @@ "publishConfig": { "tag": "latest" } -} \ No newline at end of file +} 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 609cf50d297c6..2db469867234f 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,54 +1,79 @@ 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 * as chalk from 'chalk'; +import { + integTest, + cloneDirectory, + shell, + withDefaultFixture, + retry, + sleep, + randomInteger, + withSamIntegrationFixture, + RESOURCES_DIR, + withCDKMigrateFixture, + withExtendedTimeoutFixture, + randomString, +} from '../../lib'; jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime describe('ci', () => { - integTest('output to stderr', withDefaultFixture(async (fixture) => { - const deployOutput = await fixture.cdkDeploy('test-2', { captureStderr: true, onlyStderr: true }); - const diffOutput = await fixture.cdk(['diff', fixture.fullStackName('test-2')], { captureStderr: true, onlyStderr: true }); - const destroyOutput = await fixture.cdkDestroy('test-2', { captureStderr: true, onlyStderr: true }); - expect(deployOutput).not.toEqual(''); - expect(destroyOutput).not.toEqual(''); - expect(diffOutput).not.toEqual(''); - })); - describe('ci=true', () => { - integTest('output to stdout', withDefaultFixture(async (fixture) => { - - const execOptions = { + integTest( + 'output to stderr', + withDefaultFixture(async (fixture) => { + const deployOutput = await fixture.cdkDeploy('test-2', { captureStderr: true, onlyStderr: true }); + const diffOutput = await fixture.cdk(['diff', fixture.fullStackName('test-2')], { captureStderr: true, onlyStderr: true, - modEnv: { - CI: 'true', - JSII_SILENCE_WARNING_KNOWN_BROKEN_NODE_VERSION: 'true', - JSII_SILENCE_WARNING_UNTESTED_NODE_VERSION: 'true', - JSII_SILENCE_WARNING_DEPRECATED_NODE_VERSION: 'true', - }, - }; - - const deployOutput = await fixture.cdkDeploy('test-2', execOptions); - const diffOutput = await fixture.cdk(['diff', fixture.fullStackName('test-2')], execOptions); - const destroyOutput = await fixture.cdkDestroy('test-2', execOptions); - expect(deployOutput).toEqual(''); - expect(destroyOutput).toEqual(''); - expect(diffOutput).toEqual(''); - })); + }); + const destroyOutput = await fixture.cdkDestroy('test-2', { captureStderr: true, onlyStderr: true }); + expect(deployOutput).not.toEqual(''); + expect(destroyOutput).not.toEqual(''); + expect(diffOutput).not.toEqual(''); + }), + ); + describe('ci=true', () => { + integTest( + 'output to stdout', + withDefaultFixture(async (fixture) => { + const execOptions = { + captureStderr: true, + onlyStderr: true, + modEnv: { + CI: 'true', + JSII_SILENCE_WARNING_KNOWN_BROKEN_NODE_VERSION: 'true', + JSII_SILENCE_WARNING_UNTESTED_NODE_VERSION: 'true', + JSII_SILENCE_WARNING_DEPRECATED_NODE_VERSION: 'true', + }, + }; + + const deployOutput = await fixture.cdkDeploy('test-2', execOptions); + const diffOutput = await fixture.cdk(['diff', fixture.fullStackName('test-2')], execOptions); + const destroyOutput = await fixture.cdkDestroy('test-2', execOptions); + expect(deployOutput).toEqual(''); + expect(destroyOutput).toEqual(''); + expect(diffOutput).toEqual(''); + }), + ); }); }); -integTest('VPC Lookup', withDefaultFixture(async (fixture) => { - fixture.log('Making sure we are clean before starting.'); - await fixture.cdkDestroy('define-vpc', { modEnv: { ENABLE_VPC_TESTING: 'DEFINE' } }); +integTest( + 'VPC Lookup', + withDefaultFixture(async (fixture) => { + fixture.log('Making sure we are clean before starting.'); + await fixture.cdkDestroy('define-vpc', { modEnv: { ENABLE_VPC_TESTING: 'DEFINE' } }); - fixture.log('Setting up: creating a VPC with known tags'); - await fixture.cdkDeploy('define-vpc', { modEnv: { ENABLE_VPC_TESTING: 'DEFINE' } }); - fixture.log('Setup complete!'); + fixture.log('Setting up: creating a VPC with known tags'); + await fixture.cdkDeploy('define-vpc', { modEnv: { ENABLE_VPC_TESTING: 'DEFINE' } }); + fixture.log('Setup complete!'); - fixture.log('Verifying we can now import that VPC'); - await fixture.cdkDeploy('import-vpc', { modEnv: { ENABLE_VPC_TESTING: 'IMPORT' } }); -})); + fixture.log('Verifying we can now import that VPC'); + await fixture.cdkDeploy('import-vpc', { modEnv: { ENABLE_VPC_TESTING: 'IMPORT' } }); + }), +); // testing a construct with a builtin Nodejs Lambda Function. // In this case we are testing the s3.Bucket construct with the @@ -57,553 +82,645 @@ integTest('VPC Lookup', withDefaultFixture(async (fixture) => { // is bundled as part of the CDK package, we want to make sure we don't // introduce changes to the compiled code that could prevent the Lambda from // executing. If we do, this test will timeout and fail. -integTest('Construct with builtin Lambda function', withDefaultFixture(async (fixture) => { - await fixture.cdkDeploy('builtin-lambda-function'); - fixture.log('Setup complete!'); - await fixture.cdkDestroy('builtin-lambda-function'); -})); +integTest( + 'Construct with builtin Lambda function', + withDefaultFixture(async (fixture) => { + await fixture.cdkDeploy('builtin-lambda-function'); + fixture.log('Setup complete!'); + await fixture.cdkDestroy('builtin-lambda-function'); + }), +); // this is to ensure that asset bundling for apps under a stage does not break -integTest('Stage with bundled Lambda function', withDefaultFixture(async (fixture) => { - await fixture.cdkDeploy('bundling-stage/BundlingStack'); - fixture.log('Setup complete!'); - await fixture.cdkDestroy('bundling-stage/BundlingStack'); -})); - -integTest('Two ways of showing the version', withDefaultFixture(async (fixture) => { - const version1 = await fixture.cdk(['version'], { verbose: false }); - const version2 = await fixture.cdk(['--version'], { verbose: false }); - - expect(version1).toEqual(version2); -})); - -integTest('Termination protection', withDefaultFixture(async (fixture) => { - const stackName = 'termination-protection'; - await fixture.cdkDeploy(stackName); - - // Try a destroy that should fail - await expect(fixture.cdkDestroy(stackName)).rejects.toThrow('exited with error'); - - // Can update termination protection even though the change set doesn't contain changes - await fixture.cdkDeploy(stackName, { modEnv: { TERMINATION_PROTECTION: 'FALSE' } }); - await fixture.cdkDestroy(stackName); -})); - -integTest('cdk synth', withDefaultFixture(async (fixture) => { - await fixture.cdk(['synth', fixture.fullStackName('test-1')]); - expect(fixture.template('test-1')).toEqual(expect.objectContaining({ - Resources: { - topic69831491: { - Type: 'AWS::SNS::Topic', - Metadata: { - 'aws:cdk:path': `${fixture.stackNamePrefix}-test-1/topic/Resource`, +integTest( + 'Stage with bundled Lambda function', + withDefaultFixture(async (fixture) => { + await fixture.cdkDeploy('bundling-stage/BundlingStack'); + fixture.log('Setup complete!'); + await fixture.cdkDestroy('bundling-stage/BundlingStack'); + }), +); + +integTest( + 'Two ways of showing the version', + withDefaultFixture(async (fixture) => { + const version1 = await fixture.cdk(['version'], { verbose: false }); + const version2 = await fixture.cdk(['--version'], { verbose: false }); + + expect(version1).toEqual(version2); + }), +); + +integTest( + 'Termination protection', + withDefaultFixture(async (fixture) => { + const stackName = 'termination-protection'; + await fixture.cdkDeploy(stackName); + + // Try a destroy that should fail + await expect(fixture.cdkDestroy(stackName)).rejects.toThrow('exited with error'); + + // Can update termination protection even though the change set doesn't contain changes + await fixture.cdkDeploy(stackName, { modEnv: { TERMINATION_PROTECTION: 'FALSE' } }); + await fixture.cdkDestroy(stackName); + }), +); + +integTest( + 'cdk synth', + withDefaultFixture(async (fixture) => { + await fixture.cdk(['synth', fixture.fullStackName('test-1')]); + expect(fixture.template('test-1')).toEqual( + expect.objectContaining({ + Resources: { + topic69831491: { + Type: 'AWS::SNS::Topic', + Metadata: { + 'aws:cdk:path': `${fixture.stackNamePrefix}-test-1/topic/Resource`, + }, + }, }, - }, - }, - })); + }), + ); - expect(await fixture.cdkSynth({ - options: [fixture.fullStackName('test-1')], - })).not.toEqual(expect.stringContaining(` + expect( + await fixture.cdkSynth({ + options: [fixture.fullStackName('test-1')], + }), + ).not.toEqual( + expect.stringContaining(` Rules: - CheckBootstrapVersion:`)); + CheckBootstrapVersion:`), + ); - await fixture.cdk(['synth', fixture.fullStackName('test-2')], { verbose: false }); - expect(fixture.template('test-2')).toEqual(expect.objectContaining({ - Resources: { - topic152D84A37: { - Type: 'AWS::SNS::Topic', - Metadata: { - 'aws:cdk:path': `${fixture.stackNamePrefix}-test-2/topic1/Resource`, + await fixture.cdk(['synth', fixture.fullStackName('test-2')], { verbose: false }); + expect(fixture.template('test-2')).toEqual( + expect.objectContaining({ + Resources: { + topic152D84A37: { + Type: 'AWS::SNS::Topic', + Metadata: { + 'aws:cdk:path': `${fixture.stackNamePrefix}-test-2/topic1/Resource`, + }, + }, + topic2A4FB547F: { + Type: 'AWS::SNS::Topic', + Metadata: { + 'aws:cdk:path': `${fixture.stackNamePrefix}-test-2/topic2/Resource`, + }, + }, }, - }, - topic2A4FB547F: { - Type: 'AWS::SNS::Topic', - Metadata: { - 'aws:cdk:path': `${fixture.stackNamePrefix}-test-2/topic2/Resource`, + }), + ); + }), +); + +integTest( + 'ssm parameter provider error', + withDefaultFixture(async (fixture) => { + await expect( + fixture.cdk( + ['synth', fixture.fullStackName('missing-ssm-parameter'), '-c', 'test:ssm-parameter-name=/does/not/exist'], + { + allowErrExit: true, }, - }, - }, - })); -})); - -integTest('ssm parameter provider error', withDefaultFixture(async (fixture) => { - await expect(fixture.cdk(['synth', - fixture.fullStackName('missing-ssm-parameter'), - '-c', 'test:ssm-parameter-name=/does/not/exist'], { - allowErrExit: true, - })).resolves.toContain('SSM parameter not available in account'); -})); - -integTest('automatic ordering', withDefaultFixture(async (fixture) => { - // Deploy the consuming stack which will include the producing stack - await fixture.cdkDeploy('order-consuming'); - - // Destroy the providing stack which will include the consuming stack - await fixture.cdkDestroy('order-providing'); -})); - -integTest('automatic ordering with concurrency', withDefaultFixture(async (fixture) => { - // Deploy the consuming stack which will include the producing stack - await fixture.cdkDeploy('order-consuming', { options: ['--concurrency', '2'] }); - - // Destroy the providing stack which will include the consuming stack - await fixture.cdkDestroy('order-providing'); -})); - -integTest('--exclusively selects only selected stack', withDefaultFixture(async (fixture) => { - // Deploy the "depends-on-failed" stack, with --exclusively. It will NOT fail (because - // of --exclusively) and it WILL create an output we can check for to confirm that it did - // get deployed. - const outputsFile = path.join(fixture.integTestDir, 'outputs', 'outputs.json'); - await fs.mkdir(path.dirname(outputsFile), { recursive: true }); - - await fixture.cdkDeploy('depends-on-failed', { - options: [ - '--exclusively', - '--outputs-file', outputsFile, - ], - }); - - // Verify the output to see that the stack deployed - const outputs = JSON.parse((await fs.readFile(outputsFile, { encoding: 'utf-8' })).toString()); - expect(outputs).toEqual({ - [`${fixture.stackNamePrefix}-depends-on-failed`]: { - TopicName: `${fixture.stackNamePrefix}-depends-on-failedMyTopic`, - }, - }); -})); - -integTest('context setting', withDefaultFixture(async (fixture) => { - await fs.writeFile(path.join(fixture.integTestDir, 'cdk.context.json'), JSON.stringify({ - contextkey: 'this is the context value', - })); - try { - await expect(fixture.cdk(['context'])).resolves.toContain('this is the context value'); - - // Test that deleting the contextkey works - await fixture.cdk(['context', '--reset', 'contextkey']); - await expect(fixture.cdk(['context'])).resolves.not.toContain('this is the context value'); + ), + ).resolves.toContain('SSM parameter not available in account'); + }), +); + +integTest( + 'automatic ordering', + withDefaultFixture(async (fixture) => { + // Deploy the consuming stack which will include the producing stack + await fixture.cdkDeploy('order-consuming'); + + // Destroy the providing stack which will include the consuming stack + await fixture.cdkDestroy('order-providing'); + }), +); + +integTest( + 'automatic ordering with concurrency', + withDefaultFixture(async (fixture) => { + // Deploy the consuming stack which will include the producing stack + await fixture.cdkDeploy('order-consuming', { options: ['--concurrency', '2'] }); + + // Destroy the providing stack which will include the consuming stack + await fixture.cdkDestroy('order-providing'); + }), +); + +integTest( + '--exclusively selects only selected stack', + withDefaultFixture(async (fixture) => { + // Deploy the "depends-on-failed" stack, with --exclusively. It will NOT fail (because + // of --exclusively) and it WILL create an output we can check for to confirm that it did + // get deployed. + const outputsFile = path.join(fixture.integTestDir, 'outputs', 'outputs.json'); + await fs.mkdir(path.dirname(outputsFile), { recursive: true }); + + await fixture.cdkDeploy('depends-on-failed', { + options: ['--exclusively', '--outputs-file', outputsFile], + }); - // Test that forced delete of the context key does not throw - await fixture.cdk(['context', '-f', '--reset', 'contextkey']); + // Verify the output to see that the stack deployed + const outputs = JSON.parse((await fs.readFile(outputsFile, { encoding: 'utf-8' })).toString()); + expect(outputs).toEqual({ + [`${fixture.stackNamePrefix}-depends-on-failed`]: { + TopicName: `${fixture.stackNamePrefix}-depends-on-failedMyTopic`, + }, + }); + }), +); - } finally { - await fs.unlink(path.join(fixture.integTestDir, 'cdk.context.json')); - } -})); - -integTest('context in stage propagates to top', withDefaultFixture(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'], - modEnv: { - INTEG_STACK_SET: 'stage-using-context', - }, - allowErrExit: true, - })).resolves.toContain('Context lookups have been disabled'); -})); - -integTest('deploy', withDefaultFixture(async (fixture) => { - const stackArn = await fixture.cdkDeploy('test-2', { captureStderr: false }); - - // verify the number of resources in the stack - const response = await fixture.aws.cloudFormation('describeStackResources', { - StackName: stackArn, - }); - expect(response.StackResources?.length).toEqual(2); -})); +integTest( + 'context setting', + withDefaultFixture(async (fixture) => { + await fs.writeFile( + path.join(fixture.integTestDir, 'cdk.context.json'), + JSON.stringify({ + contextkey: 'this is the context value', + }), + ); + try { + await expect(fixture.cdk(['context'])).resolves.toContain('this is the context value'); -integTest('deploy --method=direct', withDefaultFixture(async (fixture) => { - const stackArn = await fixture.cdkDeploy('test-2', { - options: ['--method=direct'], - captureStderr: false, - }); + // Test that deleting the contextkey works + await fixture.cdk(['context', '--reset', 'contextkey']); + await expect(fixture.cdk(['context'])).resolves.not.toContain('this is the context value'); - // verify the number of resources in the stack - const response = await fixture.aws.cloudFormation('describeStackResources', { - StackName: stackArn, - }); - expect(response.StackResources?.length).toBeGreaterThan(0); -})); + // Test that forced delete of the context key does not throw + await fixture.cdk(['context', '-f', '--reset', 'contextkey']); + } finally { + await fs.unlink(path.join(fixture.integTestDir, 'cdk.context.json')); + } + }), +); + +integTest( + 'context in stage propagates to top', + withDefaultFixture(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'], + modEnv: { + INTEG_STACK_SET: 'stage-using-context', + }, + allowErrExit: true, + }), + ).resolves.toContain('Context lookups have been disabled'); + }), +); -integTest('deploy all', withDefaultFixture(async (fixture) => { - const arns = await fixture.cdkDeploy('test-*', { captureStderr: false }); +integTest( + 'deploy', + withDefaultFixture(async (fixture) => { + const stackArn = await fixture.cdkDeploy('test-2', { captureStderr: false }); - // verify that we only deployed both stacks (there are 2 ARNs in the output) - expect(arns.split('\n').length).toEqual(2); -})); + // verify the number of resources in the stack + const response = await fixture.aws.cloudFormation('describeStackResources', { + StackName: stackArn, + }); + expect(response.StackResources?.length).toEqual(2); + }), +); + +integTest( + 'deploy --method=direct', + withDefaultFixture(async (fixture) => { + const stackArn = await fixture.cdkDeploy('test-2', { + options: ['--method=direct'], + captureStderr: false, + }); -integTest('deploy all concurrently', withDefaultFixture(async (fixture) => { - const arns = await fixture.cdkDeploy('test-*', { - captureStderr: false, - options: ['--concurrency', '2'], - }); + // verify the number of resources in the stack + const response = await fixture.aws.cloudFormation('describeStackResources', { + StackName: stackArn, + }); + expect(response.StackResources?.length).toBeGreaterThan(0); + }), +); + +integTest( + 'deploy all', + withDefaultFixture(async (fixture) => { + const arns = await fixture.cdkDeploy('test-*', { captureStderr: false }); + + // verify that we only deployed both stacks (there are 2 ARNs in the output) + expect(arns.split('\n').length).toEqual(2); + }), +); + +integTest( + 'deploy all concurrently', + withDefaultFixture(async (fixture) => { + const arns = await fixture.cdkDeploy('test-*', { + captureStderr: false, + options: ['--concurrency', '2'], + }); - // verify that we only deployed both stacks (there are 2 ARNs in the output) - expect(arns.split('\n').length).toEqual(2); -})); + // verify that we only deployed both stacks (there are 2 ARNs in the output) + expect(arns.split('\n').length).toEqual(2); + }), +); + +integTest( + 'nested stack with parameters', + withDefaultFixture(async (fixture) => { + // STACK_NAME_PREFIX is used in MyTopicParam to allow multiple instances + // of this test to run in parallel, othewise they will attempt to create the same SNS topic. + const stackArn = await fixture.cdkDeploy('with-nested-stack-using-parameters', { + options: ['--parameters', `MyTopicParam=${fixture.stackNamePrefix}ThereIsNoSpoon`], + captureStderr: false, + }); -integTest('nested stack with parameters', withDefaultFixture(async (fixture) => { - // STACK_NAME_PREFIX is used in MyTopicParam to allow multiple instances - // of this test to run in parallel, othewise they will attempt to create the same SNS topic. - const stackArn = await fixture.cdkDeploy('with-nested-stack-using-parameters', { - options: ['--parameters', `MyTopicParam=${fixture.stackNamePrefix}ThereIsNoSpoon`], - captureStderr: false, - }); + // verify that we only deployed a single stack (there's a single ARN in the output) + expect(stackArn.split('\n').length).toEqual(1); - // verify that we only deployed a single stack (there's a single ARN in the output) - expect(stackArn.split('\n').length).toEqual(1); + // verify the number of resources in the stack + const response = await fixture.aws.cloudFormation('describeStackResources', { + StackName: stackArn, + }); + expect(response.StackResources?.length).toEqual(1); + }), +); + +integTest( + 'deploy without execute a named change set', + withDefaultFixture(async (fixture) => { + const changeSetName = 'custom-change-set-name'; + const stackArn = await fixture.cdkDeploy('test-2', { + options: ['--no-execute', '--change-set-name', changeSetName], + captureStderr: false, + }); + // verify that we only deployed a single stack (there's a single ARN in the output) + expect(stackArn.split('\n').length).toEqual(1); - // verify the number of resources in the stack - const response = await fixture.aws.cloudFormation('describeStackResources', { - StackName: stackArn, - }); - expect(response.StackResources?.length).toEqual(1); -})); - -integTest('deploy without execute a named change set', withDefaultFixture(async (fixture) => { - const changeSetName = 'custom-change-set-name'; - const stackArn = await fixture.cdkDeploy('test-2', { - options: ['--no-execute', '--change-set-name', changeSetName], - captureStderr: false, - }); - // verify that we only deployed a single stack (there's a single ARN in the output) - expect(stackArn.split('\n').length).toEqual(1); + const response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + expect(response.Stacks?.[0].StackStatus).toEqual('REVIEW_IN_PROGRESS'); - const response = await fixture.aws.cloudFormation('describeStacks', { - StackName: stackArn, - }); - expect(response.Stacks?.[0].StackStatus).toEqual('REVIEW_IN_PROGRESS'); + //verify a change set was created with the provided name + const changeSetResponse = await fixture.aws.cloudFormation('listChangeSets', { + StackName: stackArn, + }); + const changeSets = changeSetResponse.Summaries || []; + expect(changeSets.length).toEqual(1); + expect(changeSets[0].ChangeSetName).toEqual(changeSetName); + expect(changeSets[0].Status).toEqual('CREATE_COMPLETE'); + }), +); + +integTest( + 'security related changes without a CLI are expected to fail', + withDefaultFixture(async (fixture) => { + // redirect /dev/null to stdin, which means there will not be tty attached + // since this stack includes security-related changes, the deployment should + // immediately fail because we can't confirm the changes + const stackName = 'iam-test'; + await expect( + fixture.cdkDeploy(stackName, { + options: ['<', '/dev/null'], // H4x, this only works because I happen to know we pass shell: true. + neverRequireApproval: false, + }), + ).rejects.toThrow('exited with error'); - //verify a change set was created with the provided name - const changeSetResponse = await fixture.aws.cloudFormation('listChangeSets', { - StackName: stackArn, - }); - const changeSets = changeSetResponse.Summaries || []; - expect(changeSets.length).toEqual(1); - expect(changeSets[0].ChangeSetName).toEqual(changeSetName); - expect(changeSets[0].Status).toEqual('CREATE_COMPLETE'); -})); - -integTest('security related changes without a CLI are expected to fail', withDefaultFixture(async (fixture) => { - // redirect /dev/null to stdin, which means there will not be tty attached - // since this stack includes security-related changes, the deployment should - // immediately fail because we can't confirm the changes - const stackName = 'iam-test'; - await expect(fixture.cdkDeploy(stackName, { - options: ['<', '/dev/null'], // H4x, this only works because I happen to know we pass shell: true. - neverRequireApproval: false, - })).rejects.toThrow('exited with error'); - - // Ensure stack was not deployed - await expect(fixture.aws.cloudFormation('describeStacks', { - StackName: fixture.fullStackName(stackName), - })).rejects.toThrow('does not exist'); -})); - -integTest('deploy wildcard with outputs', withDefaultFixture(async (fixture) => { - const outputsFile = path.join(fixture.integTestDir, 'outputs', 'outputs.json'); - await fs.mkdir(path.dirname(outputsFile), { recursive: true }); - - await fixture.cdkDeploy(['outputs-test-*'], { - options: ['--outputs-file', outputsFile], - }); + // Ensure stack was not deployed + await expect( + fixture.aws.cloudFormation('describeStacks', { + StackName: fixture.fullStackName(stackName), + }), + ).rejects.toThrow('does not exist'); + }), +); + +integTest( + 'deploy wildcard with outputs', + withDefaultFixture(async (fixture) => { + const outputsFile = path.join(fixture.integTestDir, 'outputs', 'outputs.json'); + await fs.mkdir(path.dirname(outputsFile), { recursive: true }); + + await fixture.cdkDeploy(['outputs-test-*'], { + options: ['--outputs-file', outputsFile], + }); - const outputs = JSON.parse((await fs.readFile(outputsFile, { encoding: 'utf-8' })).toString()); - expect(outputs).toEqual({ - [`${fixture.stackNamePrefix}-outputs-test-1`]: { - TopicName: `${fixture.stackNamePrefix}-outputs-test-1MyTopic`, - }, - [`${fixture.stackNamePrefix}-outputs-test-2`]: { - TopicName: `${fixture.stackNamePrefix}-outputs-test-2MyOtherTopic`, - }, - }); -})); - -integTest('deploy with parameters', withDefaultFixture(async (fixture) => { - const stackArn = await fixture.cdkDeploy('param-test-1', { - options: [ - '--parameters', `TopicNameParam=${fixture.stackNamePrefix}bazinga`, - ], - captureStderr: false, - }); + const outputs = JSON.parse((await fs.readFile(outputsFile, { encoding: 'utf-8' })).toString()); + expect(outputs).toEqual({ + [`${fixture.stackNamePrefix}-outputs-test-1`]: { + TopicName: `${fixture.stackNamePrefix}-outputs-test-1MyTopic`, + }, + [`${fixture.stackNamePrefix}-outputs-test-2`]: { + TopicName: `${fixture.stackNamePrefix}-outputs-test-2MyOtherTopic`, + }, + }); + }), +); + +integTest( + 'deploy with parameters', + withDefaultFixture(async (fixture) => { + const stackArn = await fixture.cdkDeploy('param-test-1', { + options: ['--parameters', `TopicNameParam=${fixture.stackNamePrefix}bazinga`], + captureStderr: false, + }); - const response = await fixture.aws.cloudFormation('describeStacks', { - StackName: stackArn, - }); + const response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); - expect(response.Stacks?.[0].Parameters).toContainEqual( - { + expect(response.Stacks?.[0].Parameters).toContainEqual({ ParameterKey: 'TopicNameParam', ParameterValue: `${fixture.stackNamePrefix}bazinga`, - }, - ); -})); - -integTest('update to stack in ROLLBACK_COMPLETE state will delete stack and create a new one', withDefaultFixture(async (fixture) => { - // GIVEN - await expect(fixture.cdkDeploy('param-test-1', { - options: [ - '--parameters', `TopicNameParam=${fixture.stackNamePrefix}@aww`, - ], - captureStderr: false, - })).rejects.toThrow('exited with error'); - - const response = await fixture.aws.cloudFormation('describeStacks', { - StackName: fixture.fullStackName('param-test-1'), - }); + }); + }), +); + +integTest( + 'update to stack in ROLLBACK_COMPLETE state will delete stack and create a new one', + withDefaultFixture(async (fixture) => { + // GIVEN + await expect( + fixture.cdkDeploy('param-test-1', { + options: ['--parameters', `TopicNameParam=${fixture.stackNamePrefix}@aww`], + captureStderr: false, + }), + ).rejects.toThrow('exited with error'); - const stackArn = response.Stacks?.[0].StackId; - expect(response.Stacks?.[0].StackStatus).toEqual('ROLLBACK_COMPLETE'); + const response = await fixture.aws.cloudFormation('describeStacks', { + StackName: fixture.fullStackName('param-test-1'), + }); - // WHEN - const newStackArn = await fixture.cdkDeploy('param-test-1', { - options: [ - '--parameters', `TopicNameParam=${fixture.stackNamePrefix}allgood`, - ], - captureStderr: false, - }); + const stackArn = response.Stacks?.[0].StackId; + expect(response.Stacks?.[0].StackStatus).toEqual('ROLLBACK_COMPLETE'); - const newStackResponse = await fixture.aws.cloudFormation('describeStacks', { - StackName: newStackArn, - }); + // WHEN + const newStackArn = await fixture.cdkDeploy('param-test-1', { + options: ['--parameters', `TopicNameParam=${fixture.stackNamePrefix}allgood`], + captureStderr: false, + }); - // THEN - expect(stackArn).not.toEqual(newStackArn); // new stack was created - expect(newStackResponse.Stacks?.[0].StackStatus).toEqual('CREATE_COMPLETE'); - expect(newStackResponse.Stacks?.[0].Parameters).toContainEqual( - { + const newStackResponse = await fixture.aws.cloudFormation('describeStacks', { + StackName: newStackArn, + }); + + // THEN + expect(stackArn).not.toEqual(newStackArn); // new stack was created + expect(newStackResponse.Stacks?.[0].StackStatus).toEqual('CREATE_COMPLETE'); + expect(newStackResponse.Stacks?.[0].Parameters).toContainEqual({ ParameterKey: 'TopicNameParam', ParameterValue: `${fixture.stackNamePrefix}allgood`, - }, - ); -})); - -integTest('stack in UPDATE_ROLLBACK_COMPLETE state can be updated', withDefaultFixture(async (fixture) => { - // GIVEN - const stackArn = await fixture.cdkDeploy('param-test-1', { - options: [ - '--parameters', `TopicNameParam=${fixture.stackNamePrefix}nice`, - ], - captureStderr: false, - }); + }); + }), +); + +integTest( + 'stack in UPDATE_ROLLBACK_COMPLETE state can be updated', + withDefaultFixture(async (fixture) => { + // GIVEN + const stackArn = await fixture.cdkDeploy('param-test-1', { + options: ['--parameters', `TopicNameParam=${fixture.stackNamePrefix}nice`], + captureStderr: false, + }); - let response = await fixture.aws.cloudFormation('describeStacks', { - StackName: stackArn, - }); + let response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); - expect(response.Stacks?.[0].StackStatus).toEqual('CREATE_COMPLETE'); + expect(response.Stacks?.[0].StackStatus).toEqual('CREATE_COMPLETE'); - // bad parameter name with @ will put stack into UPDATE_ROLLBACK_COMPLETE - await expect(fixture.cdkDeploy('param-test-1', { - options: [ - '--parameters', `TopicNameParam=${fixture.stackNamePrefix}@aww`, - ], - captureStderr: false, - })).rejects.toThrow('exited with error');; + // bad parameter name with @ will put stack into UPDATE_ROLLBACK_COMPLETE + await expect( + fixture.cdkDeploy('param-test-1', { + options: ['--parameters', `TopicNameParam=${fixture.stackNamePrefix}@aww`], + captureStderr: false, + }), + ).rejects.toThrow('exited with error'); - response = await fixture.aws.cloudFormation('describeStacks', { - StackName: stackArn, - }); + response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); - expect(response.Stacks?.[0].StackStatus).toEqual('UPDATE_ROLLBACK_COMPLETE'); + expect(response.Stacks?.[0].StackStatus).toEqual('UPDATE_ROLLBACK_COMPLETE'); - // WHEN - await fixture.cdkDeploy('param-test-1', { - options: [ - '--parameters', `TopicNameParam=${fixture.stackNamePrefix}allgood`, - ], - captureStderr: false, - }); + // WHEN + await fixture.cdkDeploy('param-test-1', { + options: ['--parameters', `TopicNameParam=${fixture.stackNamePrefix}allgood`], + captureStderr: false, + }); - response = await fixture.aws.cloudFormation('describeStacks', { - StackName: stackArn, - }); + response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); - // THEN - expect(response.Stacks?.[0].StackStatus).toEqual('UPDATE_COMPLETE'); - expect(response.Stacks?.[0].Parameters).toContainEqual( - { + // THEN + expect(response.Stacks?.[0].StackStatus).toEqual('UPDATE_COMPLETE'); + expect(response.Stacks?.[0].Parameters).toContainEqual({ ParameterKey: 'TopicNameParam', ParameterValue: `${fixture.stackNamePrefix}allgood`, - }, - ); -})); - -integTest('deploy with wildcard and parameters', withDefaultFixture(async (fixture) => { - await fixture.cdkDeploy('param-test-*', { - options: [ - '--parameters', `${fixture.stackNamePrefix}-param-test-1:TopicNameParam=${fixture.stackNamePrefix}bazinga`, - '--parameters', `${fixture.stackNamePrefix}-param-test-2:OtherTopicNameParam=${fixture.stackNamePrefix}ThatsMySpot`, - '--parameters', `${fixture.stackNamePrefix}-param-test-3:DisplayNameParam=${fixture.stackNamePrefix}HeyThere`, - '--parameters', `${fixture.stackNamePrefix}-param-test-3:OtherDisplayNameParam=${fixture.stackNamePrefix}AnotherOne`, - ], - }); -})); - -integTest('deploy with parameters multi', withDefaultFixture(async (fixture) => { - const paramVal1 = `${fixture.stackNamePrefix}bazinga`; - const paramVal2 = `${fixture.stackNamePrefix}=jagshemash`; - - const stackArn = await fixture.cdkDeploy('param-test-3', { - options: [ - '--parameters', `DisplayNameParam=${paramVal1}`, - '--parameters', `OtherDisplayNameParam=${paramVal2}`, - ], - captureStderr: false, - }); + }); + }), +); + +integTest( + 'deploy with wildcard and parameters', + withDefaultFixture(async (fixture) => { + await fixture.cdkDeploy('param-test-*', { + options: [ + '--parameters', + `${fixture.stackNamePrefix}-param-test-1:TopicNameParam=${fixture.stackNamePrefix}bazinga`, + '--parameters', + `${fixture.stackNamePrefix}-param-test-2:OtherTopicNameParam=${fixture.stackNamePrefix}ThatsMySpot`, + '--parameters', + `${fixture.stackNamePrefix}-param-test-3:DisplayNameParam=${fixture.stackNamePrefix}HeyThere`, + '--parameters', + `${fixture.stackNamePrefix}-param-test-3:OtherDisplayNameParam=${fixture.stackNamePrefix}AnotherOne`, + ], + }); + }), +); - const response = await fixture.aws.cloudFormation('describeStacks', { - StackName: stackArn, - }); +integTest( + 'deploy with parameters multi', + withDefaultFixture(async (fixture) => { + const paramVal1 = `${fixture.stackNamePrefix}bazinga`; + const paramVal2 = `${fixture.stackNamePrefix}=jagshemash`; + + const stackArn = await fixture.cdkDeploy('param-test-3', { + options: ['--parameters', `DisplayNameParam=${paramVal1}`, '--parameters', `OtherDisplayNameParam=${paramVal2}`], + captureStderr: false, + }); + + const response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); - expect(response.Stacks?.[0].Parameters).toContainEqual( - { + expect(response.Stacks?.[0].Parameters).toContainEqual({ ParameterKey: 'DisplayNameParam', ParameterValue: paramVal1, - }, - ); - expect(response.Stacks?.[0].Parameters).toContainEqual( - { + }); + expect(response.Stacks?.[0].Parameters).toContainEqual({ ParameterKey: 'OtherDisplayNameParam', ParameterValue: paramVal2, - }, - ); -})); + }); + }), +); -integTest('deploy with notification ARN', withDefaultFixture(async (fixture) => { - const topicName = `${fixture.stackNamePrefix}-test-topic`; +integTest( + 'deploy with notification ARN', + withDefaultFixture(async (fixture) => { + const topicName = `${fixture.stackNamePrefix}-test-topic`; - const response = await fixture.aws.sns('createTopic', { Name: topicName }); - const topicArn = response.TopicArn!; - try { - await fixture.cdkDeploy('test-2', { - options: ['--notification-arns', topicArn], - }); + const response = await fixture.aws.sns('createTopic', { Name: topicName }); + const topicArn = response.TopicArn!; + try { + await fixture.cdkDeploy('test-2', { + options: ['--notification-arns', topicArn], + }); - // verify that the stack we deployed has our notification ARN - const describeResponse = await fixture.aws.cloudFormation('describeStacks', { - StackName: fixture.fullStackName('test-2'), - }); - expect(describeResponse.Stacks?.[0].NotificationARNs).toEqual([topicArn]); - } finally { - await fixture.aws.sns('deleteTopic', { - TopicArn: topicArn, - }); - } -})); + // verify that the stack we deployed has our notification ARN + const describeResponse = await fixture.aws.cloudFormation('describeStacks', { + StackName: fixture.fullStackName('test-2'), + }); + 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) => { - if (fixture.packages.majorVersion() !== '1') { - return; // Nothing to do - } +integTest( + 'deploy with role', + withDefaultFixture(async (fixture) => { + if (fixture.packages.majorVersion() !== '1') { + return; // Nothing to do + } - const roleName = `${fixture.stackNamePrefix}-test-role`; - - await deleteRole(); - - const createResponse = await fixture.aws.iam('createRole', { - RoleName: roleName, - AssumeRolePolicyDocument: JSON.stringify({ - Version: '2012-10-17', - Statement: [{ - Action: 'sts:AssumeRole', - Principal: { Service: 'cloudformation.amazonaws.com' }, - Effect: 'Allow', - }, { - Action: 'sts:AssumeRole', - Principal: { AWS: (await fixture.aws.sts('getCallerIdentity', {})).Arn }, - Effect: 'Allow', - }], - }), - }); - const roleArn = createResponse.Role.Arn; - try { - await fixture.aws.iam('putRolePolicy', { + const roleName = `${fixture.stackNamePrefix}-test-role`; + + await deleteRole(); + + const createResponse = await fixture.aws.iam('createRole', { RoleName: roleName, - PolicyName: 'DefaultPolicy', - PolicyDocument: JSON.stringify({ + AssumeRolePolicyDocument: JSON.stringify({ Version: '2012-10-17', - Statement: [{ - Action: '*', - Resource: '*', - Effect: 'Allow', - }], + Statement: [ + { + Action: 'sts:AssumeRole', + Principal: { Service: 'cloudformation.amazonaws.com' }, + Effect: 'Allow', + }, + { + Action: 'sts:AssumeRole', + Principal: { AWS: (await fixture.aws.sts('getCallerIdentity', {})).Arn }, + Effect: 'Allow', + }, + ], }), }); - - await retry(fixture.output, 'Trying to assume fresh role', retry.forSeconds(300), async () => { - await fixture.aws.sts('assumeRole', { - RoleArn: roleArn, - RoleSessionName: 'testing', + const roleArn = createResponse.Role.Arn; + try { + await fixture.aws.iam('putRolePolicy', { + RoleName: roleName, + PolicyName: 'DefaultPolicy', + PolicyDocument: JSON.stringify({ + Version: '2012-10-17', + Statement: [ + { + Action: '*', + Resource: '*', + Effect: 'Allow', + }, + ], + }), }); - }); - // In principle, the role has replicated from 'us-east-1' to wherever we're testing. - // Give it a little more sleep to make sure CloudFormation is not hitting a box - // that doesn't have it yet. - await sleep(5000); + await retry(fixture.output, 'Trying to assume fresh role', retry.forSeconds(300), async () => { + await fixture.aws.sts('assumeRole', { + RoleArn: roleArn, + RoleSessionName: 'testing', + }); + }); - await fixture.cdkDeploy('test-2', { - options: ['--role-arn', roleArn], - }); + // In principle, the role has replicated from 'us-east-1' to wherever we're testing. + // Give it a little more sleep to make sure CloudFormation is not hitting a box + // that doesn't have it yet. + await sleep(5000); - // Immediately delete the stack again before we delete the role. - // - // Since roles are sticky, if we delete the role before the stack, subsequent DeleteStack - // operations will fail when CloudFormation tries to assume the role that's already gone. - await fixture.cdkDestroy('test-2'); + await fixture.cdkDeploy('test-2', { + options: ['--role-arn', roleArn], + }); - } finally { - await deleteRole(); - } + // Immediately delete the stack again before we delete the role. + // + // Since roles are sticky, if we delete the role before the stack, subsequent DeleteStack + // operations will fail when CloudFormation tries to assume the role that's already gone. + await fixture.cdkDestroy('test-2'); + } finally { + await deleteRole(); + } - async function deleteRole() { - try { - for (const policyName of (await fixture.aws.iam('listRolePolicies', { RoleName: roleName })).PolicyNames) { - await fixture.aws.iam('deleteRolePolicy', { - RoleName: roleName, - PolicyName: policyName, - }); + async function deleteRole() { + try { + for (const policyName of (await fixture.aws.iam('listRolePolicies', { RoleName: roleName })).PolicyNames) { + await fixture.aws.iam('deleteRolePolicy', { + RoleName: roleName, + PolicyName: policyName, + }); + } + await fixture.aws.iam('deleteRole', { RoleName: roleName }); + } catch (e: any) { + if (e.message.indexOf('cannot be found') > -1) { + return; + } + throw e; } - await fixture.aws.iam('deleteRole', { RoleName: roleName }); - } catch (e: any) { - if (e.message.indexOf('cannot be found') > -1) { return; } - throw e; } - } -})); + }), +); // TODO add more testing that ensures the symmetry of the generated constructs to the resources. -['typescript', 'python', 'csharp', 'java'].forEach(language => { - integTest(`cdk migrate ${language} deploys successfully`, withCDKMigrateFixture(language, async (fixture) => { - if (language === 'python') { - await fixture.shell(['pip', 'install', '-r', 'requirements.txt']); - } +['typescript', 'python', 'csharp', 'java'].forEach((language) => { + integTest( + `cdk migrate ${language} deploys successfully`, + withCDKMigrateFixture(language, async (fixture) => { + if (language === 'python') { + await fixture.shell(['pip', 'install', '-r', 'requirements.txt']); + } - const stackArn = await fixture.cdkDeploy(fixture.stackNamePrefix, { neverRequireApproval: true, verbose: true, captureStderr: false }, true); - const response = await fixture.aws.cloudFormation('describeStacks', { - StackName: stackArn, - }); + const stackArn = await fixture.cdkDeploy( + fixture.stackNamePrefix, + { neverRequireApproval: true, verbose: true, captureStderr: false }, + true, + ); + const response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); - expect(response.Stacks?.[0].StackStatus).toEqual('CREATE_COMPLETE'); - await fixture.cdkDestroy(fixture.stackNamePrefix); - })); + expect(response.Stacks?.[0].StackStatus).toEqual('CREATE_COMPLETE'); + await fixture.cdkDestroy(fixture.stackNamePrefix); + }), + ); }); -integTest('cdk migrate generates migrate.json', withCDKMigrateFixture('typescript', async (fixture) => { - - const migrateFile = await fs.readFile(path.join(fixture.integTestDir, 'migrate.json'), 'utf8'); - const expectedFile = `{ +integTest( + 'cdk migrate generates migrate.json', + withCDKMigrateFixture('typescript', async (fixture) => { + const migrateFile = await fs.readFile(path.join(fixture.integTestDir, 'migrate.json'), 'utf8'); + const expectedFile = `{ \"//\": \"This file is generated by cdk migrate. It will be automatically deleted after the first successful deployment of this app to the environment of the original resources.\", \"Source\": \"localfile\" }`; - expect(JSON.parse(migrateFile)).toEqual(JSON.parse(expectedFile)); - await fixture.cdkDestroy(fixture.stackNamePrefix); -})); + expect(JSON.parse(migrateFile)).toEqual(JSON.parse(expectedFile)); + await fixture.cdkDestroy(fixture.stackNamePrefix); + }), +); // integTest('cdk migrate --from-scan with AND/OR filters correctly filters resources', withExtendedTimeoutFixture(async (fixture) => { // const stackName = `cdk-migrate-integ-${fixture.randomString}`; @@ -676,202 +793,240 @@ integTest('cdk migrate generates migrate.json', withCDKMigrateFixture('typescrip // } // })); -['typescript', 'python', 'csharp', 'java'].forEach(language => { - integTest(`cdk migrate --from-stack creates deployable ${language} app`, withExtendedTimeoutFixture(async (fixture) => { - const migrateStackName = fixture.fullStackName('migrate-stack'); - await fixture.aws.cloudFormation('createStack', { - StackName: migrateStackName, - TemplateBody: await fs.readFile(path.join(__dirname, '..', '..', 'resources', 'templates', 'sqs-template.json'), 'utf8'), - }); - try { - let stackStatus = 'CREATE_IN_PROGRESS'; - while (stackStatus === 'CREATE_IN_PROGRESS') { - stackStatus = await (await (fixture.aws.cloudFormation('describeStacks', { StackName: migrateStackName }))).Stacks?.[0].StackStatus!; - await sleep(1000); - } - await fixture.cdk( - ['migrate', '--stack-name', migrateStackName, '--from-stack'], - { modEnv: { MIGRATE_INTEG_TEST: '1' }, neverRequireApproval: true, verbose: true, captureStderr: false }, - ); - await fixture.shell(['cd', path.join(fixture.integTestDir, migrateStackName)]); - await fixture.cdk(['deploy', migrateStackName], { neverRequireApproval: true, verbose: true, captureStderr: false }); - const response = await fixture.aws.cloudFormation('describeStacks', { +['typescript', 'python', 'csharp', 'java'].forEach((language) => { + integTest( + `cdk migrate --from-stack creates deployable ${language} app`, + withExtendedTimeoutFixture(async (fixture) => { + const migrateStackName = fixture.fullStackName('migrate-stack'); + await fixture.aws.cloudFormation('createStack', { StackName: migrateStackName, + TemplateBody: await fs.readFile( + path.join(__dirname, '..', '..', 'resources', 'templates', 'sqs-template.json'), + 'utf8', + ), }); + try { + let stackStatus = 'CREATE_IN_PROGRESS'; + while (stackStatus === 'CREATE_IN_PROGRESS') { + stackStatus = await ( + await fixture.aws.cloudFormation('describeStacks', { StackName: migrateStackName }) + ).Stacks?.[0].StackStatus!; + await sleep(1000); + } + await fixture.cdk(['migrate', '--stack-name', migrateStackName, '--from-stack'], { + modEnv: { MIGRATE_INTEG_TEST: '1' }, + neverRequireApproval: true, + verbose: true, + captureStderr: false, + }); + await fixture.shell(['cd', path.join(fixture.integTestDir, migrateStackName)]); + await fixture.cdk(['deploy', migrateStackName], { + neverRequireApproval: true, + verbose: true, + captureStderr: false, + }); + const response = await fixture.aws.cloudFormation('describeStacks', { + StackName: migrateStackName, + }); - expect(response.Stacks?.[0].StackStatus).toEqual('UPDATE_COMPLETE'); - } finally { - await fixture.cdkDestroy('migrate-stack'); - } - })); + expect(response.Stacks?.[0].StackStatus).toEqual('UPDATE_COMPLETE'); + } finally { + await fixture.cdkDestroy('migrate-stack'); + } + }), + ); }); -integTest('cdk diff', withDefaultFixture(async (fixture) => { - const diff1 = await fixture.cdk(['diff', fixture.fullStackName('test-1')]); - expect(diff1).toContain('AWS::SNS::Topic'); - - const diff2 = await fixture.cdk(['diff', fixture.fullStackName('test-2')]); - expect(diff2).toContain('AWS::SNS::Topic'); - - // We can make it fail by passing --fail - await expect(fixture.cdk(['diff', '--fail', fixture.fullStackName('test-1')])) - .rejects.toThrow('exited with error'); -})); - -integTest('enableDiffNoFail', withDefaultFixture(async (fixture) => { - await diffShouldSucceedWith({ fail: false, enableDiffNoFail: false }); - await diffShouldSucceedWith({ fail: false, enableDiffNoFail: true }); - await diffShouldFailWith({ fail: true, enableDiffNoFail: false }); - await diffShouldFailWith({ fail: true, enableDiffNoFail: true }); - await diffShouldFailWith({ fail: undefined, enableDiffNoFail: false }); - await diffShouldSucceedWith({ fail: undefined, enableDiffNoFail: true }); - - async function diffShouldSucceedWith(props: DiffParameters) { - await expect(diff(props)).resolves.not.toThrowError(); - } - - async function diffShouldFailWith(props: DiffParameters) { - await expect(diff(props)).rejects.toThrow('exited with error'); - } - - async function diff(props: DiffParameters): Promise { - await updateContext(props.enableDiffNoFail); - const flag = props.fail != null - ? (props.fail ? '--fail' : '--no-fail') - : ''; - - return fixture.cdk(['diff', flag, fixture.fullStackName('test-1')]); - } - - async function updateContext(enableDiffNoFail: boolean) { - const cdkJson = JSON.parse(await fs.readFile(path.join(fixture.integTestDir, 'cdk.json'), 'utf8')); - cdkJson.context = { - ...cdkJson.context, - 'aws-cdk:enableDiffNoFail': enableDiffNoFail, - }; - await fs.writeFile(path.join(fixture.integTestDir, 'cdk.json'), JSON.stringify(cdkJson)); - } - - type DiffParameters = { fail?: boolean; enableDiffNoFail: boolean }; -})); - -integTest('cdk diff --fail on multiple stacks exits with error if any of the stacks contains a diff', withDefaultFixture(async (fixture) => { - // GIVEN - const diff1 = await fixture.cdk(['diff', fixture.fullStackName('test-1')]); - expect(diff1).toContain('AWS::SNS::Topic'); - - await fixture.cdkDeploy('test-2'); - const diff2 = await fixture.cdk(['diff', fixture.fullStackName('test-2')]); - expect(diff2).toContain('There were no differences'); +integTest( + 'cdk diff', + withDefaultFixture(async (fixture) => { + const diff1 = await fixture.cdk(['diff', fixture.fullStackName('test-1')]); + expect(diff1).toContain('AWS::SNS::Topic'); + + const diff2 = await fixture.cdk(['diff', fixture.fullStackName('test-2')]); + expect(diff2).toContain('AWS::SNS::Topic'); + + // We can make it fail by passing --fail + await expect(fixture.cdk(['diff', '--fail', fixture.fullStackName('test-1')])).rejects.toThrow('exited with error'); + }), +); + +integTest( + 'enableDiffNoFail', + withDefaultFixture(async (fixture) => { + await diffShouldSucceedWith({ fail: false, enableDiffNoFail: false }); + await diffShouldSucceedWith({ fail: false, enableDiffNoFail: true }); + await diffShouldFailWith({ fail: true, enableDiffNoFail: false }); + await diffShouldFailWith({ fail: true, enableDiffNoFail: true }); + await diffShouldFailWith({ fail: undefined, enableDiffNoFail: false }); + await diffShouldSucceedWith({ fail: undefined, enableDiffNoFail: true }); + + async function diffShouldSucceedWith(props: DiffParameters) { + await expect(diff(props)).resolves.not.toThrowError(); + } - // WHEN / THEN - await expect(fixture.cdk(['diff', '--fail', fixture.fullStackName('test-1'), fixture.fullStackName('test-2')])).rejects.toThrow('exited with error'); -})); + async function diffShouldFailWith(props: DiffParameters) { + await expect(diff(props)).rejects.toThrow('exited with error'); + } -integTest('cdk diff --fail with multiple stack exits with if any of the stacks contains a diff', withDefaultFixture(async (fixture) => { - // GIVEN - await fixture.cdkDeploy('test-1'); - const diff1 = await fixture.cdk(['diff', fixture.fullStackName('test-1')]); - expect(diff1).toContain('There were no differences'); + async function diff(props: DiffParameters): Promise { + await updateContext(props.enableDiffNoFail); + const flag = props.fail != null ? (props.fail ? '--fail' : '--no-fail') : ''; - const diff2 = await fixture.cdk(['diff', fixture.fullStackName('test-2')]); - expect(diff2).toContain('AWS::SNS::Topic'); + return fixture.cdk(['diff', flag, fixture.fullStackName('test-1')]); + } - // WHEN / THEN - await expect(fixture.cdk(['diff', '--fail', fixture.fullStackName('test-1'), fixture.fullStackName('test-2')])).rejects.toThrow('exited with error'); -})); + async function updateContext(enableDiffNoFail: boolean) { + const cdkJson = JSON.parse(await fs.readFile(path.join(fixture.integTestDir, 'cdk.json'), 'utf8')); + cdkJson.context = { + ...cdkJson.context, + 'aws-cdk:enableDiffNoFail': enableDiffNoFail, + }; + await fs.writeFile(path.join(fixture.integTestDir, 'cdk.json'), JSON.stringify(cdkJson)); + } -integTest('cdk diff --security-only successfully outputs sso-permission-set-without-managed-policy information', withDefaultFixture(async (fixture) => { - const diff = await fixture.cdk( - ['diff', '--security-only', fixture.fullStackName('sso-perm-set-without-managed-policy')], - ); - `┌───┬──────────────────────────────────────────┬──────────────────────────────────┬────────────────────┬───────────────────────────────────┬─────────────────────────────────┐ + type DiffParameters = { fail?: boolean; enableDiffNoFail: boolean }; + }), +); + +integTest( + 'cdk diff --fail on multiple stacks exits with error if any of the stacks contains a diff', + withDefaultFixture(async (fixture) => { + // GIVEN + const diff1 = await fixture.cdk(['diff', fixture.fullStackName('test-1')]); + expect(diff1).toContain('AWS::SNS::Topic'); + + await fixture.cdkDeploy('test-2'); + const diff2 = await fixture.cdk(['diff', fixture.fullStackName('test-2')]); + expect(diff2).toContain('There were no differences'); + + // WHEN / THEN + await expect( + fixture.cdk(['diff', '--fail', fixture.fullStackName('test-1'), fixture.fullStackName('test-2')]), + ).rejects.toThrow('exited with error'); + }), +); + +integTest( + 'cdk diff --fail with multiple stack exits with if any of the stacks contains a diff', + withDefaultFixture(async (fixture) => { + // GIVEN + await fixture.cdkDeploy('test-1'); + const diff1 = await fixture.cdk(['diff', fixture.fullStackName('test-1')]); + expect(diff1).toContain('There were no differences'); + + const diff2 = await fixture.cdk(['diff', fixture.fullStackName('test-2')]); + expect(diff2).toContain('AWS::SNS::Topic'); + + // WHEN / THEN + await expect( + fixture.cdk(['diff', '--fail', fixture.fullStackName('test-1'), fixture.fullStackName('test-2')]), + ).rejects.toThrow('exited with error'); + }), +); + +integTest( + 'cdk diff --security-only successfully outputs sso-permission-set-without-managed-policy information', + withDefaultFixture(async (fixture) => { + const diff = await fixture.cdk([ + 'diff', + '--security-only', + fixture.fullStackName('sso-perm-set-without-managed-policy'), + ]); + `┌───┬──────────────────────────────────────────┬──────────────────────────────────┬────────────────────┬───────────────────────────────────┬─────────────────────────────────┐ │ │ Resource │ InstanceArn │ PermissionSet name │ PermissionsBoundary │ CustomerManagedPolicyReferences │ ├───┼──────────────────────────────────────────┼──────────────────────────────────┼────────────────────┼───────────────────────────────────┼─────────────────────────────────┤ │ + │\${permission-set-without-managed-policy} │ arn:aws:sso:::instance/testvalue │ testName │ CustomerManagedPolicyReference: { │ │ │ │ │ │ │ Name: why, Path: /how/ │ │ │ │ │ │ │ } │ │ `; - expect(diff).toContain('Resource'); - expect(diff).toContain('permission-set-without-managed-policy'); - - expect(diff).toContain('InstanceArn'); - expect(diff).toContain('arn:aws:sso:::instance/testvalue'); - - expect(diff).toContain('PermissionSet name'); - expect(diff).toContain('testName'); - - expect(diff).toContain('PermissionsBoundary'); - expect(diff).toContain('CustomerManagedPolicyReference: {'); - expect(diff).toContain('Name: why, Path: /how/'); - expect(diff).toContain('}'); - - expect(diff).toContain('CustomerManagedPolicyReferences'); -})); - -integTest('cdk diff --security-only successfully outputs sso-permission-set-with-managed-policy information', withDefaultFixture(async (fixture) => { - const diff = await fixture.cdk( - ['diff', '--security-only', fixture.fullStackName('sso-perm-set-with-managed-policy')], - ); - `┌───┬──────────────────────────────────────────┬──────────────────────────────────┬────────────────────┬───────────────────────────────────────────────────────────────┬─────────────────────────────────┐ + expect(diff).toContain('Resource'); + expect(diff).toContain('permission-set-without-managed-policy'); + + expect(diff).toContain('InstanceArn'); + expect(diff).toContain('arn:aws:sso:::instance/testvalue'); + + expect(diff).toContain('PermissionSet name'); + expect(diff).toContain('testName'); + + expect(diff).toContain('PermissionsBoundary'); + expect(diff).toContain('CustomerManagedPolicyReference: {'); + expect(diff).toContain('Name: why, Path: /how/'); + expect(diff).toContain('}'); + + expect(diff).toContain('CustomerManagedPolicyReferences'); + }), +); + +integTest( + 'cdk diff --security-only successfully outputs sso-permission-set-with-managed-policy information', + withDefaultFixture(async (fixture) => { + const diff = await fixture.cdk([ + 'diff', + '--security-only', + fixture.fullStackName('sso-perm-set-with-managed-policy'), + ]); + `┌───┬──────────────────────────────────────────┬──────────────────────────────────┬────────────────────┬───────────────────────────────────────────────────────────────┬─────────────────────────────────┐ │ │ Resource │ InstanceArn │ PermissionSet name │ PermissionsBoundary │ CustomerManagedPolicyReferences │ ├───┼──────────────────────────────────────────┼──────────────────────────────────┼────────────────────┼───────────────────────────────────────────────────────────────┼─────────────────────────────────┤ │ + │\${permission-set-with-managed-policy} │ arn:aws:sso:::instance/testvalue │ niceWork │ ManagedPolicyArn: arn:aws:iam::aws:policy/AdministratorAccess │ Name: forSSO, Path: │ `; - expect(diff).toContain('Resource'); - expect(diff).toContain('permission-set-with-managed-policy'); + expect(diff).toContain('Resource'); + expect(diff).toContain('permission-set-with-managed-policy'); - expect(diff).toContain('InstanceArn'); - expect(diff).toContain('arn:aws:sso:::instance/testvalue'); + expect(diff).toContain('InstanceArn'); + expect(diff).toContain('arn:aws:sso:::instance/testvalue'); - expect(diff).toContain('PermissionSet name'); - expect(diff).toContain('niceWork'); + expect(diff).toContain('PermissionSet name'); + expect(diff).toContain('niceWork'); - expect(diff).toContain('PermissionsBoundary'); - expect(diff).toContain('ManagedPolicyArn: arn:aws:iam::aws:policy/AdministratorAccess'); + expect(diff).toContain('PermissionsBoundary'); + expect(diff).toContain('ManagedPolicyArn: arn:aws:iam::aws:policy/AdministratorAccess'); - expect(diff).toContain('CustomerManagedPolicyReferences'); - expect(diff).toContain('Name: forSSO, Path:'); -})); + expect(diff).toContain('CustomerManagedPolicyReferences'); + expect(diff).toContain('Name: forSSO, Path:'); + }), +); -integTest('cdk diff --security-only successfully outputs sso-assignment information', withDefaultFixture(async (fixture) => { - const diff = await fixture.cdk( - ['diff', '--security-only', fixture.fullStackName('sso-assignment')], - ); - `┌───┬───────────────┬──────────────────────────────────┬─────────────────────────┬──────────────────────────────┬───────────────┬──────────────┬─────────────┐ +integTest( + 'cdk diff --security-only successfully outputs sso-assignment information', + withDefaultFixture(async (fixture) => { + const diff = await fixture.cdk(['diff', '--security-only', fixture.fullStackName('sso-assignment')]); + `┌───┬───────────────┬──────────────────────────────────┬─────────────────────────┬──────────────────────────────┬───────────────┬──────────────┬─────────────┐ │ │ Resource │ InstanceArn │ PermissionSetArn │ PrincipalId │ PrincipalType │ TargetId │ TargetType │ ├───┼───────────────┼──────────────────────────────────┼─────────────────────────┼──────────────────────────────┼───────────────┼──────────────┼─────────────┤ │ + │\${assignment} │ arn:aws:sso:::instance/testvalue │ arn:aws:sso:::testvalue │ 11111111-2222-3333-4444-test │ USER │ 111111111111 │ AWS_ACCOUNT │ └───┴───────────────┴──────────────────────────────────┴─────────────────────────┴──────────────────────────────┴───────────────┴──────────────┴─────────────┘ `; - expect(diff).toContain('Resource'); - expect(diff).toContain('assignment'); + expect(diff).toContain('Resource'); + expect(diff).toContain('assignment'); - expect(diff).toContain('InstanceArn'); - expect(diff).toContain('arn:aws:sso:::instance/testvalue'); + expect(diff).toContain('InstanceArn'); + expect(diff).toContain('arn:aws:sso:::instance/testvalue'); - expect(diff).toContain('PermissionSetArn'); - expect(diff).toContain('arn:aws:sso:::testvalue'); + expect(diff).toContain('PermissionSetArn'); + expect(diff).toContain('arn:aws:sso:::testvalue'); - expect(diff).toContain('PrincipalId'); - expect(diff).toContain('11111111-2222-3333-4444-test'); + expect(diff).toContain('PrincipalId'); + expect(diff).toContain('11111111-2222-3333-4444-test'); - expect(diff).toContain('PrincipalType'); - expect(diff).toContain('USER'); + expect(diff).toContain('PrincipalType'); + expect(diff).toContain('USER'); - expect(diff).toContain('TargetId'); - expect(diff).toContain('111111111111'); + expect(diff).toContain('TargetId'); + expect(diff).toContain('111111111111'); - expect(diff).toContain('TargetType'); - expect(diff).toContain('AWS_ACCOUNT'); -})); + expect(diff).toContain('TargetType'); + expect(diff).toContain('AWS_ACCOUNT'); + }), +); -integTest('cdk diff --security-only successfully outputs sso-access-control information', withDefaultFixture(async (fixture) => { - const diff = await fixture.cdk( - ['diff', '--security-only', fixture.fullStackName('sso-access-control')], - ); - `┌───┬────────────────────────────────┬────────────────────────┬─────────────────────────────────┐ +integTest( + 'cdk diff --security-only successfully outputs sso-access-control information', + withDefaultFixture(async (fixture) => { + const diff = await fixture.cdk(['diff', '--security-only', fixture.fullStackName('sso-access-control')]); + `┌───┬────────────────────────────────┬────────────────────────┬─────────────────────────────────┐ │ │ Resource │ InstanceArn │ AccessControlAttributes │ ├───┼────────────────────────────────┼────────────────────────┼─────────────────────────────────┤ │ + │\${instanceAccessControlConfig} │ arn:aws:test:testvalue │ Key: first, Values: [a] │ @@ -882,125 +1037,143 @@ integTest('cdk diff --security-only successfully outputs sso-access-control info │ │ │ │ Key: sixth, Values: [f] │ └───┴────────────────────────────────┴────────────────────────┴─────────────────────────────────┘ `; - expect(diff).toContain('Resource'); - expect(diff).toContain('instanceAccessControlConfig'); - - expect(diff).toContain('InstanceArn'); - expect(diff).toContain('arn:aws:sso:::instance/testvalue'); - - expect(diff).toContain('AccessControlAttributes'); - expect(diff).toContain('Key: first, Values: [a]'); - expect(diff).toContain('Key: second, Values: [b]'); - expect(diff).toContain('Key: third, Values: [c]'); - expect(diff).toContain('Key: fourth, Values: [d]'); - expect(diff).toContain('Key: fifth, Values: [e]'); - expect(diff).toContain('Key: sixth, Values: [f]'); -})); - -integTest('cdk diff --security-only --fail exits when security diff for sso access control config', withDefaultFixture(async (fixture) => { - await expect( - fixture.cdk( - ['diff', '--security-only', '--fail', fixture.fullStackName('sso-access-control')], - ), - ).rejects - .toThrow('exited with error'); -})); - -integTest('cdk diff --security-only --fail exits when security diff for sso-perm-set-without-managed-policy', withDefaultFixture(async (fixture) => { - await expect( - fixture.cdk( - ['diff', '--security-only', '--fail', fixture.fullStackName('sso-perm-set-without-managed-policy')], - ), - ).rejects - .toThrow('exited with error'); -})); - -integTest('cdk diff --security-only --fail exits when security diff for sso-perm-set-with-managed-policy', withDefaultFixture(async (fixture) => { - await expect( - fixture.cdk( - ['diff', '--security-only', '--fail', fixture.fullStackName('sso-perm-set-with-managed-policy')], - ), - ).rejects - .toThrow('exited with error'); -})); - -integTest('cdk diff --security-only --fail exits when security diff for sso-assignment', withDefaultFixture(async (fixture) => { - await expect( - fixture.cdk( - ['diff', '--security-only', '--fail', fixture.fullStackName('sso-assignment')], - ), - ).rejects - .toThrow('exited with error'); -})); - -integTest('cdk diff --security-only --fail exits when security changes are present', withDefaultFixture(async (fixture) => { - const stackName = 'iam-test'; - await expect(fixture.cdk(['diff', '--security-only', '--fail', fixture.fullStackName(stackName)])).rejects.toThrow('exited with error'); -})); - -integTest('cdk diff --quiet does not print \'There were no differences\' message for stacks which have no differences', withDefaultFixture(async (fixture) => { - // GIVEN - await fixture.cdkDeploy('test-1'); - - // WHEN - const diff = await fixture.cdk(['diff', '--quiet', fixture.fullStackName('test-1')]); - - // THEN - expect(diff).not.toContain('Stack test-1'); - expect(diff).not.toContain('There were no differences'); -})); - -integTest('deploy stack with docker asset', withDefaultFixture(async (fixture) => { - await fixture.cdkDeploy('docker'); -})); - -integTest('deploy and test stack with lambda asset', withDefaultFixture(async (fixture) => { - const stackArn = await fixture.cdkDeploy('lambda', { captureStderr: false }); - - const response = await fixture.aws.cloudFormation('describeStacks', { - StackName: stackArn, - }); - const lambdaArn = response.Stacks?.[0].Outputs?.[0].OutputValue; - if (lambdaArn === undefined) { - throw new Error('Stack did not have expected Lambda ARN output'); - } + expect(diff).toContain('Resource'); + expect(diff).toContain('instanceAccessControlConfig'); + + expect(diff).toContain('InstanceArn'); + expect(diff).toContain('arn:aws:sso:::instance/testvalue'); + + expect(diff).toContain('AccessControlAttributes'); + expect(diff).toContain('Key: first, Values: [a]'); + expect(diff).toContain('Key: second, Values: [b]'); + expect(diff).toContain('Key: third, Values: [c]'); + expect(diff).toContain('Key: fourth, Values: [d]'); + expect(diff).toContain('Key: fifth, Values: [e]'); + expect(diff).toContain('Key: sixth, Values: [f]'); + }), +); + +integTest( + 'cdk diff --security-only --fail exits when security diff for sso access control config', + withDefaultFixture(async (fixture) => { + await expect( + fixture.cdk(['diff', '--security-only', '--fail', fixture.fullStackName('sso-access-control')]), + ).rejects.toThrow('exited with error'); + }), +); + +integTest( + 'cdk diff --security-only --fail exits when security diff for sso-perm-set-without-managed-policy', + withDefaultFixture(async (fixture) => { + await expect( + fixture.cdk(['diff', '--security-only', '--fail', fixture.fullStackName('sso-perm-set-without-managed-policy')]), + ).rejects.toThrow('exited with error'); + }), +); + +integTest( + 'cdk diff --security-only --fail exits when security diff for sso-perm-set-with-managed-policy', + withDefaultFixture(async (fixture) => { + await expect( + fixture.cdk(['diff', '--security-only', '--fail', fixture.fullStackName('sso-perm-set-with-managed-policy')]), + ).rejects.toThrow('exited with error'); + }), +); + +integTest( + 'cdk diff --security-only --fail exits when security diff for sso-assignment', + withDefaultFixture(async (fixture) => { + await expect( + fixture.cdk(['diff', '--security-only', '--fail', fixture.fullStackName('sso-assignment')]), + ).rejects.toThrow('exited with error'); + }), +); + +integTest( + 'cdk diff --security-only --fail exits when security changes are present', + withDefaultFixture(async (fixture) => { + const stackName = 'iam-test'; + await expect(fixture.cdk(['diff', '--security-only', '--fail', fixture.fullStackName(stackName)])).rejects.toThrow( + 'exited with error', + ); + }), +); - const output = await fixture.aws.lambda('invoke', { - FunctionName: lambdaArn, - }); +integTest( + "cdk diff --quiet does not print 'There were no differences' message for stacks which have no differences", + withDefaultFixture(async (fixture) => { + // GIVEN + await fixture.cdkDeploy('test-1'); - expect(JSON.stringify(output.Payload)).toContain('dear asset'); -})); - -integTest('cdk ls', withDefaultFixture(async (fixture) => { - const listing = await fixture.cdk(['ls'], { captureStderr: false }); - - const expectedStacks = [ - 'conditional-resource', - 'docker', - 'docker-with-custom-file', - 'failed', - 'iam-test', - 'lambda', - 'missing-ssm-parameter', - 'order-providing', - 'outputs-test-1', - 'outputs-test-2', - 'param-test-1', - 'param-test-2', - 'param-test-3', - 'termination-protection', - 'test-1', - 'test-2', - 'with-nested-stack', - 'with-nested-stack-using-parameters', - 'order-consuming', - ]; - - for (const stack of expectedStacks) { - expect(listing).toContain(fixture.fullStackName(stack)); - } -})); + // WHEN + const diff = await fixture.cdk(['diff', '--quiet', fixture.fullStackName('test-1')]); + + // THEN + expect(diff).not.toContain('Stack test-1'); + expect(diff).not.toContain('There were no differences'); + }), +); + +integTest( + 'deploy stack with docker asset', + withDefaultFixture(async (fixture) => { + await fixture.cdkDeploy('docker'); + }), +); + +integTest( + 'deploy and test stack with lambda asset', + withDefaultFixture(async (fixture) => { + const stackArn = await fixture.cdkDeploy('lambda', { captureStderr: false }); + + const response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + const lambdaArn = response.Stacks?.[0].Outputs?.[0].OutputValue; + if (lambdaArn === undefined) { + throw new Error('Stack did not have expected Lambda ARN output'); + } + + const output = await fixture.aws.lambda('invoke', { + FunctionName: lambdaArn, + }); + + expect(JSON.stringify(output.Payload)).toContain('dear asset'); + }), +); + +integTest( + 'cdk ls', + withDefaultFixture(async (fixture) => { + const listing = await fixture.cdk(['ls'], { captureStderr: false }); + + const expectedStacks = [ + 'conditional-resource', + 'docker', + 'docker-with-custom-file', + 'failed', + 'iam-test', + 'lambda', + 'missing-ssm-parameter', + 'order-providing', + 'outputs-test-1', + 'outputs-test-2', + 'param-test-1', + 'param-test-2', + 'param-test-3', + 'termination-protection', + 'test-1', + 'test-2', + 'with-nested-stack', + 'with-nested-stack-using-parameters', + 'order-consuming', + ]; + + for (const stack of expectedStacks) { + expect(listing).toContain(fixture.fullStackName(stack)); + } + }), +); /** * Type to store stack dependencies recursively @@ -1015,710 +1188,731 @@ type StackDetails = { dependencies: DependencyDetails[]; }; -integTest('cdk ls --show-dependencies --json', withDefaultFixture(async (fixture) => { - const listing = await fixture.cdk(['ls --show-dependencies --json'], { captureStderr: false }); - - const expectedStacks = [ - { - id: 'test-1', - dependencies: [], - }, - { - id: 'order-providing', - dependencies: [], - }, - { - id: 'order-consuming', - dependencies: [ - { - id: 'order-providing', - dependencies: [], - }, - ], - }, - { - id: 'with-nested-stack', - dependencies: [], - }, - { - id: 'list-stacks', - dependencies: [ - { - id: 'list-stacks/DependentStack', - dependencies: [ - { - id: 'list-stacks/DependentStack/InnerDependentStack', - dependencies: [], - }, - ], - }, - ], - }, - { - id: 'list-multiple-dependent-stacks', - dependencies: [ - { - id: 'list-multiple-dependent-stacks/DependentStack1', - dependencies: [], - }, - { - id: 'list-multiple-dependent-stacks/DependentStack2', - dependencies: [], - }, - ], - }, - ]; +integTest( + 'cdk ls --show-dependencies --json', + withDefaultFixture(async (fixture) => { + const listing = await fixture.cdk(['ls --show-dependencies --json'], { captureStderr: false }); - function validateStackDependencies(stack: StackDetails) { - expect(listing).toContain(stack.id); + const expectedStacks = [ + { + id: 'test-1', + dependencies: [], + }, + { + id: 'order-providing', + dependencies: [], + }, + { + id: 'order-consuming', + dependencies: [ + { + id: 'order-providing', + dependencies: [], + }, + ], + }, + { + id: 'with-nested-stack', + dependencies: [], + }, + { + id: 'list-stacks', + dependencies: [ + { + id: 'list-stacks/DependentStack', + dependencies: [ + { + id: 'list-stacks/DependentStack/InnerDependentStack', + dependencies: [], + }, + ], + }, + ], + }, + { + id: 'list-multiple-dependent-stacks', + dependencies: [ + { + id: 'list-multiple-dependent-stacks/DependentStack1', + dependencies: [], + }, + { + id: 'list-multiple-dependent-stacks/DependentStack2', + dependencies: [], + }, + ], + }, + ]; + + function validateStackDependencies(stack: StackDetails) { + expect(listing).toContain(stack.id); - function validateDependencies(dependencies: DependencyDetails[]) { - for (const dependency of dependencies) { - expect(listing).toContain(dependency.id); - if (dependency.dependencies.length > 0) { - validateDependencies(dependency.dependencies); + function validateDependencies(dependencies: DependencyDetails[]) { + for (const dependency of dependencies) { + expect(listing).toContain(dependency.id); + if (dependency.dependencies.length > 0) { + validateDependencies(dependency.dependencies); + } } } + + if (stack.dependencies.length > 0) { + validateDependencies(stack.dependencies); + } } - if (stack.dependencies.length > 0) { - validateDependencies(stack.dependencies); + for (const stack of expectedStacks) { + validateStackDependencies(stack); } - } + }), +); + +integTest( + 'cdk ls --show-dependencies --json --long', + withDefaultFixture(async (fixture) => { + const listing = await fixture.cdk(['ls --show-dependencies --json --long'], { captureStderr: false }); + + const expectedStacks = [ + { + id: 'order-providing', + name: 'order-providing', + enviroment: { + account: 'unknown-account', + region: 'unknown-region', + name: 'aws://unknown-account/unknown-region', + }, + dependencies: [], + }, + { + id: 'order-consuming', + name: 'order-consuming', + enviroment: { + account: 'unknown-account', + region: 'unknown-region', + name: 'aws://unknown-account/unknown-region', + }, + dependencies: [ + { + id: 'order-providing', + dependencies: [], + }, + ], + }, + ]; + + for (const stack of expectedStacks) { + expect(listing).toContain(fixture.fullStackName(stack.id)); + expect(listing).toContain(fixture.fullStackName(stack.name)); + expect(listing).toContain(stack.enviroment.account); + expect(listing).toContain(stack.enviroment.name); + expect(listing).toContain(stack.enviroment.region); + for (const dependency of stack.dependencies) { + expect(listing).toContain(fixture.fullStackName(dependency.id)); + } + } + }), +); + +integTest( + 'synthing a stage with errors leads to failure', + withDefaultFixture(async (fixture) => { + const output = await fixture.cdk(['synth'], { + allowErrExit: true, + modEnv: { + INTEG_STACK_SET: 'stage-with-errors', + }, + }); - for (const stack of expectedStacks) { - validateStackDependencies(stack); - } -})); - -integTest('cdk ls --show-dependencies --json --long', withDefaultFixture(async (fixture) => { - const listing = await fixture.cdk(['ls --show-dependencies --json --long'], { captureStderr: false }); - - const expectedStacks = [ - { - id: 'order-providing', - name: 'order-providing', - enviroment: { - account: 'unknown-account', - region: 'unknown-region', - name: 'aws://unknown-account/unknown-region', + expect(output).toContain('This is an error'); + }), +); + +integTest( + 'synthing a stage with errors can be suppressed', + withDefaultFixture(async (fixture) => { + await fixture.cdk(['synth', '--no-validation'], { + modEnv: { + INTEG_STACK_SET: 'stage-with-errors', }, - dependencies: [], - }, - { - id: 'order-consuming', - name: 'order-consuming', - enviroment: { - account: 'unknown-account', - region: 'unknown-region', - name: 'aws://unknown-account/unknown-region', + }); + }), +); + +integTest( + 'synth --quiet can be specified in cdk.json', + withDefaultFixture(async (fixture) => { + let cdkJson = JSON.parse(await fs.readFile(path.join(fixture.integTestDir, 'cdk.json'), 'utf8')); + cdkJson = { + ...cdkJson, + quiet: true, + }; + await fs.writeFile(path.join(fixture.integTestDir, 'cdk.json'), JSON.stringify(cdkJson)); + const synthOutput = await fixture.cdk(['synth', fixture.fullStackName('test-2')]); + expect(synthOutput).not.toContain('topic152D84A37'); + }), +); + +integTest( + 'deploy stack without resource', + withDefaultFixture(async (fixture) => { + // Deploy the stack without resources + await fixture.cdkDeploy('conditional-resource', { modEnv: { NO_RESOURCE: 'TRUE' } }); + + // This should have succeeded but not deployed the stack. + await expect( + fixture.aws.cloudFormation('describeStacks', { StackName: fixture.fullStackName('conditional-resource') }), + ).rejects.toThrow('conditional-resource does not exist'); + + // Deploy the stack with resources + await fixture.cdkDeploy('conditional-resource'); + + // Then again WITHOUT resources (this should destroy the stack) + await fixture.cdkDeploy('conditional-resource', { modEnv: { NO_RESOURCE: 'TRUE' } }); + + await expect( + fixture.aws.cloudFormation('describeStacks', { StackName: fixture.fullStackName('conditional-resource') }), + ).rejects.toThrow('conditional-resource does not exist'); + }), +); + +integTest( + 'deploy no stacks with --ignore-no-stacks', + withDefaultFixture(async (fixture) => { + // empty array for stack names + await fixture.cdkDeploy([], { + options: ['--ignore-no-stacks'], + modEnv: { + INTEG_STACK_SET: 'stage-with-no-stacks', }, - dependencies: [ - { - id: 'order-providing', - dependencies: [], + }); + }), +); + +integTest( + 'deploy no stacks error', + withDefaultFixture(async (fixture) => { + // empty array for stack names + await expect( + fixture.cdkDeploy([], { + modEnv: { + INTEG_STACK_SET: 'stage-with-no-stacks', }, - ], - }, - ]; - - for (const stack of expectedStacks) { - expect(listing).toContain(fixture.fullStackName(stack.id)); - expect(listing).toContain(fixture.fullStackName(stack.name)); - expect(listing).toContain(stack.enviroment.account); - expect(listing).toContain(stack.enviroment.name); - expect(listing).toContain(stack.enviroment.region); - for (const dependency of stack.dependencies) { - expect(listing).toContain(fixture.fullStackName(dependency.id)); + }), + ).rejects.toThrow('exited with error'); + }), +); + +integTest( + 'IAM diff', + withDefaultFixture(async (fixture) => { + const output = await fixture.cdk(['diff', fixture.fullStackName('iam-test')]); + + // Roughly check for a table like this: + // + // ┌───┬─────────────────┬────────┬────────────────┬────────────────────────────-──┬───────────┐ + // │ │ Resource │ Effect │ Action │ Principal │ Condition │ + // ├───┼─────────────────┼────────┼────────────────┼───────────────────────────────┼───────────┤ + // │ + │ ${SomeRole.Arn} │ Allow │ sts:AssumeRole │ Service:ec2.amazonaws.com │ │ + // └───┴─────────────────┴────────┴────────────────┴───────────────────────────────┴───────────┘ + + expect(output).toContain('${SomeRole.Arn}'); + expect(output).toContain('sts:AssumeRole'); + expect(output).toContain('ec2.amazonaws.com'); + }), +); + +integTest( + 'fast deploy', + withDefaultFixture(async (fixture) => { + // we are using a stack with a nested stack because CFN will always attempt to + // update a nested stack, which will allow us to verify that updates are actually + // skipped unless --force is specified. + const stackArn = await fixture.cdkDeploy('with-nested-stack', { captureStderr: false }); + const changeSet1 = await getLatestChangeSet(); + + // Deploy the same stack again, there should be no new change set created + await fixture.cdkDeploy('with-nested-stack'); + const changeSet2 = await getLatestChangeSet(); + expect(changeSet2.ChangeSetId).toEqual(changeSet1.ChangeSetId); + + // Deploy the stack again with --force, now we should create a changeset + await fixture.cdkDeploy('with-nested-stack', { options: ['--force'] }); + const changeSet3 = await getLatestChangeSet(); + expect(changeSet3.ChangeSetId).not.toEqual(changeSet2.ChangeSetId); + + // Deploy the stack again with tags, expected to create a new changeset + // even though the resources didn't change. + await fixture.cdkDeploy('with-nested-stack', { options: ['--tags', 'key=value'] }); + const changeSet4 = await getLatestChangeSet(); + expect(changeSet4.ChangeSetId).not.toEqual(changeSet3.ChangeSetId); + + async function getLatestChangeSet() { + const response = await fixture.aws.cloudFormation('describeStacks', { StackName: stackArn }); + if (!response.Stacks?.[0]) { + throw new Error('Did not get a ChangeSet at all'); + } + fixture.log(`Found Change Set ${response.Stacks?.[0].ChangeSetId}`); + return response.Stacks?.[0]; } - } + }), +); + +integTest( + 'failed deploy does not hang', + withDefaultFixture(async (fixture) => { + // this will hang if we introduce https://github.com/aws/aws-cdk/issues/6403 again. + await expect(fixture.cdkDeploy('failed')).rejects.toThrow('exited with error'); + }), +); + +integTest( + 'can still load old assemblies', + withDefaultFixture(async (fixture) => { + const cxAsmDir = path.join(os.tmpdir(), 'cdk-integ-cx'); + + const testAssembliesDirectory = path.join(RESOURCES_DIR, 'cloud-assemblies'); + for (const asmdir of await listChildDirs(testAssembliesDirectory)) { + fixture.log(`ASSEMBLY ${asmdir}`); + await cloneDirectory(asmdir, cxAsmDir); + + // Some files in the asm directory that have a .js extension are + // actually treated as templates. Evaluate them using NodeJS. + const templates = await listChildren(cxAsmDir, (fullPath) => Promise.resolve(fullPath.endsWith('.js'))); + for (const template of templates) { + const targetName = template.replace(/.js$/, ''); + await shell([process.execPath, template, '>', targetName], { + cwd: cxAsmDir, + output: fixture.output, + modEnv: { + TEST_ACCOUNT: await fixture.aws.account(), + TEST_REGION: fixture.aws.region, + }, + }); + } -})); + // Use this directory as a Cloud Assembly + const output = await fixture.cdk(['--app', cxAsmDir, '-v', 'synth']); -integTest('synthing a stage with errors leads to failure', withDefaultFixture(async (fixture) => { - const output = await fixture.cdk(['synth'], { - allowErrExit: true, - modEnv: { - INTEG_STACK_SET: 'stage-with-errors', - }, - }); + // Assert that there was no providerError in CDK's stderr + // Because we rely on the app/framework to actually error in case the + // provider fails, we inspect the logs here. + expect(output).not.toContain('$providerError'); + } + }), +); + +integTest( + 'generating and loading assembly', + withDefaultFixture(async (fixture) => { + const asmOutputDir = `${fixture.integTestDir}-cdk-integ-asm`; + await fixture.shell(['rm', '-rf', asmOutputDir]); + + // Synthesize a Cloud Assembly tothe default directory (cdk.out) and a specific directory. + await fixture.cdk(['synth']); + await fixture.cdk(['synth', '--output', asmOutputDir]); + + // cdk.out in the current directory and the indicated --output should be the same + await fixture.shell(['diff', 'cdk.out', asmOutputDir]); + + // Check that we can 'ls' the synthesized asm. + // Change to some random directory to make sure we're not accidentally loading cdk.json + const list = await fixture.cdk(['--app', asmOutputDir, 'ls'], { cwd: os.tmpdir() }); + // Same stacks we know are in the app + expect(list).toContain(`${fixture.stackNamePrefix}-lambda`); + expect(list).toContain(`${fixture.stackNamePrefix}-test-1`); + expect(list).toContain(`${fixture.stackNamePrefix}-test-2`); + + // Check that we can use '.' and just synth ,the generated asm + const stackTemplate = await fixture.cdk(['--app', '.', 'synth', fixture.fullStackName('test-2')], { + cwd: asmOutputDir, + }); + expect(stackTemplate).toContain('topic152D84A37'); - expect(output).toContain('This is an error'); -})); + // Deploy a Lambda from the copied asm + await fixture.cdkDeploy('lambda', { options: ['-a', '.'], cwd: asmOutputDir }); -integTest('synthing a stage with errors can be suppressed', withDefaultFixture(async (fixture) => { - await fixture.cdk(['synth', '--no-validation'], { - modEnv: { - INTEG_STACK_SET: 'stage-with-errors', - }, - }); -})); - -integTest('synth --quiet can be specified in cdk.json', withDefaultFixture(async (fixture) => { - let cdkJson = JSON.parse(await fs.readFile(path.join(fixture.integTestDir, 'cdk.json'), 'utf8')); - cdkJson = { - ...cdkJson, - quiet: true, - }; - await fs.writeFile(path.join(fixture.integTestDir, 'cdk.json'), JSON.stringify(cdkJson)); - const synthOutput = await fixture.cdk(['synth', fixture.fullStackName('test-2')]); - expect(synthOutput).not.toContain('topic152D84A37'); -})); - -integTest('deploy stack without resource', withDefaultFixture(async (fixture) => { - // Deploy the stack without resources - await fixture.cdkDeploy('conditional-resource', { modEnv: { NO_RESOURCE: 'TRUE' } }); - - // This should have succeeded but not deployed the stack. - await expect(fixture.aws.cloudFormation('describeStacks', { StackName: fixture.fullStackName('conditional-resource') })) - .rejects.toThrow('conditional-resource does not exist'); - - // Deploy the stack with resources - await fixture.cdkDeploy('conditional-resource'); - - // Then again WITHOUT resources (this should destroy the stack) - await fixture.cdkDeploy('conditional-resource', { modEnv: { NO_RESOURCE: 'TRUE' } }); - - await expect(fixture.aws.cloudFormation('describeStacks', { StackName: fixture.fullStackName('conditional-resource') })) - .rejects.toThrow('conditional-resource does not exist'); -})); - -integTest('deploy no stacks with --ignore-no-stacks', withDefaultFixture(async (fixture) => { - // empty array for stack names - await fixture.cdkDeploy([], { - options: ['--ignore-no-stacks'], - modEnv: { - INTEG_STACK_SET: 'stage-with-no-stacks', - }, - }); -})); - -integTest('deploy no stacks error', withDefaultFixture(async (fixture) => { - // empty array for stack names - await expect(fixture.cdkDeploy([], { - modEnv: { - INTEG_STACK_SET: 'stage-with-no-stacks', - }, - })).rejects.toThrow('exited with error'); -})); - -integTest('IAM diff', withDefaultFixture(async (fixture) => { - const output = await fixture.cdk(['diff', fixture.fullStackName('iam-test')]); - - // Roughly check for a table like this: - // - // ┌───┬─────────────────┬────────┬────────────────┬────────────────────────────-──┬───────────┐ - // │ │ Resource │ Effect │ Action │ Principal │ Condition │ - // ├───┼─────────────────┼────────┼────────────────┼───────────────────────────────┼───────────┤ - // │ + │ ${SomeRole.Arn} │ Allow │ sts:AssumeRole │ Service:ec2.amazonaws.com │ │ - // └───┴─────────────────┴────────┴────────────────┴───────────────────────────────┴───────────┘ - - expect(output).toContain('${SomeRole.Arn}'); - expect(output).toContain('sts:AssumeRole'); - expect(output).toContain('ec2.amazonaws.com'); -})); - -integTest('fast deploy', withDefaultFixture(async (fixture) => { - // we are using a stack with a nested stack because CFN will always attempt to - // update a nested stack, which will allow us to verify that updates are actually - // skipped unless --force is specified. - const stackArn = await fixture.cdkDeploy('with-nested-stack', { captureStderr: false }); - const changeSet1 = await getLatestChangeSet(); - - // Deploy the same stack again, there should be no new change set created - await fixture.cdkDeploy('with-nested-stack'); - const changeSet2 = await getLatestChangeSet(); - expect(changeSet2.ChangeSetId).toEqual(changeSet1.ChangeSetId); - - // Deploy the stack again with --force, now we should create a changeset - await fixture.cdkDeploy('with-nested-stack', { options: ['--force'] }); - const changeSet3 = await getLatestChangeSet(); - expect(changeSet3.ChangeSetId).not.toEqual(changeSet2.ChangeSetId); - - // Deploy the stack again with tags, expected to create a new changeset - // even though the resources didn't change. - await fixture.cdkDeploy('with-nested-stack', { options: ['--tags', 'key=value'] }); - const changeSet4 = await getLatestChangeSet(); - expect(changeSet4.ChangeSetId).not.toEqual(changeSet3.ChangeSetId); - - async function getLatestChangeSet() { - const response = await fixture.aws.cloudFormation('describeStacks', { StackName: stackArn }); - if (!response.Stacks?.[0]) { throw new Error('Did not get a ChangeSet at all'); } - fixture.log(`Found Change Set ${response.Stacks?.[0].ChangeSetId}`); - return response.Stacks?.[0]; - } -})); - -integTest('failed deploy does not hang', withDefaultFixture(async (fixture) => { - // this will hang if we introduce https://github.com/aws/aws-cdk/issues/6403 again. - await expect(fixture.cdkDeploy('failed')).rejects.toThrow('exited with error'); -})); - -integTest('can still load old assemblies', withDefaultFixture(async (fixture) => { - const cxAsmDir = path.join(os.tmpdir(), 'cdk-integ-cx'); - - const testAssembliesDirectory = path.join(RESOURCES_DIR, 'cloud-assemblies'); - for (const asmdir of await listChildDirs(testAssembliesDirectory)) { - fixture.log(`ASSEMBLY ${asmdir}`); - await cloneDirectory(asmdir, cxAsmDir); - - // Some files in the asm directory that have a .js extension are - // actually treated as templates. Evaluate them using NodeJS. - const templates = await listChildren(cxAsmDir, fullPath => Promise.resolve(fullPath.endsWith('.js'))); - for (const template of templates) { - const targetName = template.replace(/.js$/, ''); - await shell([process.execPath, template, '>', targetName], { - cwd: cxAsmDir, - output: fixture.output, - modEnv: { - TEST_ACCOUNT: await fixture.aws.account(), - TEST_REGION: fixture.aws.region, - }, - }); + // Remove (rename) the original custom docker file that was used during synth. + // this verifies that the assemly has a copy of it and that the manifest uses + // relative paths to reference to it. + const customDockerFile = path.join(fixture.integTestDir, 'docker', 'Dockerfile.Custom'); + await fs.rename(customDockerFile, `${customDockerFile}~`); + try { + // deploy a docker image with custom file without synth (uses assets) + await fixture.cdkDeploy('docker-with-custom-file', { options: ['-a', '.'], cwd: asmOutputDir }); + } finally { + // Rename back to restore fixture to original state + await fs.rename(`${customDockerFile}~`, customDockerFile); } + }), +); - // Use this directory as a Cloud Assembly - const output = await fixture.cdk([ - '--app', cxAsmDir, - '-v', - 'synth', - ]); +integTest( + 'templates on disk contain metadata resource, also in nested assemblies', + withDefaultFixture(async (fixture) => { + // Synth first, and switch on version reporting because cdk.json is disabling it + await fixture.cdk(['synth', '--version-reporting=true']); - // Assert that there was no providerError in CDK's stderr - // Because we rely on the app/framework to actually error in case the - // provider fails, we inspect the logs here. - expect(output).not.toContain('$providerError'); - } -})); - -integTest('generating and loading assembly', withDefaultFixture(async (fixture) => { - const asmOutputDir = `${fixture.integTestDir}-cdk-integ-asm`; - await fixture.shell(['rm', '-rf', asmOutputDir]); - - // Synthesize a Cloud Assembly tothe default directory (cdk.out) and a specific directory. - await fixture.cdk(['synth']); - await fixture.cdk(['synth', '--output', asmOutputDir]); - - // cdk.out in the current directory and the indicated --output should be the same - await fixture.shell(['diff', 'cdk.out', asmOutputDir]); - - // Check that we can 'ls' the synthesized asm. - // Change to some random directory to make sure we're not accidentally loading cdk.json - const list = await fixture.cdk(['--app', asmOutputDir, 'ls'], { cwd: os.tmpdir() }); - // Same stacks we know are in the app - expect(list).toContain(`${fixture.stackNamePrefix}-lambda`); - expect(list).toContain(`${fixture.stackNamePrefix}-test-1`); - expect(list).toContain(`${fixture.stackNamePrefix}-test-2`); - - // Check that we can use '.' and just synth ,the generated asm - const stackTemplate = await fixture.cdk(['--app', '.', 'synth', fixture.fullStackName('test-2')], { - cwd: asmOutputDir, - }); - expect(stackTemplate).toContain('topic152D84A37'); + // Load template from disk from root assembly + const templateContents = await fixture.shell(['cat', 'cdk.out/*-lambda.template.json']); - // Deploy a Lambda from the copied asm - await fixture.cdkDeploy('lambda', { options: ['-a', '.'], cwd: asmOutputDir }); + expect(JSON.parse(templateContents).Resources.CDKMetadata).toBeTruthy(); - // Remove (rename) the original custom docker file that was used during synth. - // this verifies that the assemly has a copy of it and that the manifest uses - // relative paths to reference to it. - const customDockerFile = path.join(fixture.integTestDir, 'docker', 'Dockerfile.Custom'); - await fs.rename(customDockerFile, `${customDockerFile}~`); - try { + // Load template from nested assembly + const nestedTemplateContents = await fixture.shell([ + 'cat', + 'cdk.out/assembly-*-stage/*StackInStage*.template.json', + ]); - // deploy a docker image with custom file without synth (uses assets) - await fixture.cdkDeploy('docker-with-custom-file', { options: ['-a', '.'], cwd: asmOutputDir }); + expect(JSON.parse(nestedTemplateContents).Resources.CDKMetadata).toBeTruthy(); + }), +); + +integTest( + 'CDK synth add the metadata properties expected by sam', + withSamIntegrationFixture(async (fixture) => { + // Synth first + await fixture.cdkSynth(); + + const template = fixture.template('TestStack'); + + const expectedResources = [ + { + // Python Layer Version + id: 'PythonLayerVersion39495CEF', + cdkId: 'PythonLayerVersion', + isBundled: true, + property: 'Content', + }, + { + // Layer Version + id: 'LayerVersion3878DA3A', + cdkId: 'LayerVersion', + isBundled: false, + property: 'Content', + }, + { + // Bundled layer version + id: 'BundledLayerVersionPythonRuntime6BADBD6E', + cdkId: 'BundledLayerVersionPythonRuntime', + isBundled: true, + property: 'Content', + }, + { + // Python Function + id: 'PythonFunction0BCF77FD', + cdkId: 'PythonFunction', + isBundled: true, + property: 'Code', + }, + { + // Log Retention Function + id: 'LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A', + cdkId: 'LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a', + isBundled: false, + property: 'Code', + }, + { + // Function + id: 'FunctionPythonRuntime28CBDA05', + cdkId: 'FunctionPythonRuntime', + isBundled: false, + property: 'Code', + }, + { + // Bundled Function + id: 'BundledFunctionPythonRuntime4D9A0918', + cdkId: 'BundledFunctionPythonRuntime', + isBundled: true, + property: 'Code', + }, + { + // NodeJs Function + id: 'NodejsFunction09C1F20F', + cdkId: 'NodejsFunction', + isBundled: true, + property: 'Code', + }, + { + // Go Function + id: 'GoFunctionCA95FBAA', + cdkId: 'GoFunction', + isBundled: true, + property: 'Code', + }, + { + // Docker Image Function + id: 'DockerImageFunction28B773E6', + cdkId: 'DockerImageFunction', + dockerFilePath: 'Dockerfile', + property: 'Code.ImageUri', + }, + { + // Spec Rest Api + id: 'SpecRestAPI7D4B3A34', + cdkId: 'SpecRestAPI', + property: 'BodyS3Location', + }, + ]; + + for (const resource of expectedResources) { + fixture.output.write(`validate assets metadata for resource ${resource}`); + expect(resource.id in template.Resources).toBeTruthy(); + expect(template.Resources[resource.id]).toEqual( + expect.objectContaining({ + Metadata: { + 'aws:cdk:path': `${fixture.fullStackName('TestStack')}/${resource.cdkId}/Resource`, + 'aws:asset:path': expect.stringMatching(/asset\.[0-9a-zA-Z]{64}/), + 'aws:asset:is-bundled': resource.isBundled, + 'aws:asset:dockerfile-path': resource.dockerFilePath, + 'aws:asset:property': resource.property, + }, + }), + ); + } - } finally { - // Rename back to restore fixture to original state - await fs.rename(`${customDockerFile}~`, customDockerFile); - } -})); - -integTest('templates on disk contain metadata resource, also in nested assemblies', withDefaultFixture(async (fixture) => { - // Synth first, and switch on version reporting because cdk.json is disabling it - await fixture.cdk(['synth', '--version-reporting=true']); - - // Load template from disk from root assembly - const templateContents = await fixture.shell(['cat', 'cdk.out/*-lambda.template.json']); - - expect(JSON.parse(templateContents).Resources.CDKMetadata).toBeTruthy(); - - // Load template from nested assembly - const nestedTemplateContents = await fixture.shell(['cat', 'cdk.out/assembly-*-stage/*StackInStage*.template.json']); - - expect(JSON.parse(nestedTemplateContents).Resources.CDKMetadata).toBeTruthy(); -})); - -integTest('CDK synth add the metadata properties expected by sam', withSamIntegrationFixture(async (fixture) => { - // Synth first - await fixture.cdkSynth(); - - const template = fixture.template('TestStack'); - - const expectedResources = [ - { - // Python Layer Version - id: 'PythonLayerVersion39495CEF', - cdkId: 'PythonLayerVersion', - isBundled: true, - property: 'Content', - }, - { - // Layer Version - id: 'LayerVersion3878DA3A', - cdkId: 'LayerVersion', - isBundled: false, - property: 'Content', - }, - { - // Bundled layer version - id: 'BundledLayerVersionPythonRuntime6BADBD6E', - cdkId: 'BundledLayerVersionPythonRuntime', - isBundled: true, - property: 'Content', - }, - { - // Python Function - id: 'PythonFunction0BCF77FD', - cdkId: 'PythonFunction', - isBundled: true, - property: 'Code', - }, - { - // Log Retention Function - id: 'LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A', - cdkId: 'LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a', - isBundled: false, - property: 'Code', - }, - { - // Function - id: 'FunctionPythonRuntime28CBDA05', - cdkId: 'FunctionPythonRuntime', - isBundled: false, - property: 'Code', - }, - { - // Bundled Function - id: 'BundledFunctionPythonRuntime4D9A0918', - cdkId: 'BundledFunctionPythonRuntime', - isBundled: true, - property: 'Code', - }, - { - // NodeJs Function - id: 'NodejsFunction09C1F20F', - cdkId: 'NodejsFunction', - isBundled: true, - property: 'Code', - }, - { - // Go Function - id: 'GoFunctionCA95FBAA', - cdkId: 'GoFunction', - isBundled: true, - property: 'Code', - }, - { - // Docker Image Function - id: 'DockerImageFunction28B773E6', - cdkId: 'DockerImageFunction', - dockerFilePath: 'Dockerfile', - property: 'Code.ImageUri', - }, - { - // Spec Rest Api - id: 'SpecRestAPI7D4B3A34', - cdkId: 'SpecRestAPI', - property: 'BodyS3Location', - }, - ]; - - for (const resource of expectedResources) { - fixture.output.write(`validate assets metadata for resource ${resource}`); - expect(resource.id in template.Resources).toBeTruthy(); - expect(template.Resources[resource.id]).toEqual(expect.objectContaining({ - Metadata: { - 'aws:cdk:path': `${fixture.fullStackName('TestStack')}/${resource.cdkId}/Resource`, - 'aws:asset:path': expect.stringMatching(/asset\.[0-9a-zA-Z]{64}/), - 'aws:asset:is-bundled': resource.isBundled, - 'aws:asset:dockerfile-path': resource.dockerFilePath, - 'aws:asset:property': resource.property, + // Nested Stack + fixture.output.write('validate assets metadata for nested stack resource'); + expect('NestedStackNestedStackNestedStackNestedStackResourceB70834FD' in template.Resources).toBeTruthy(); + expect(template.Resources.NestedStackNestedStackNestedStackNestedStackResourceB70834FD).toEqual( + expect.objectContaining({ + Metadata: { + 'aws:cdk:path': `${fixture.fullStackName('TestStack')}/NestedStack.NestedStack/NestedStack.NestedStackResource`, + 'aws:asset:path': expect.stringMatching( + `${fixture.stackNamePrefix.replace(/-/, '')}TestStackNestedStack[0-9A-Z]{8}\.nested\.template\.json`, + ), + 'aws:asset:property': 'TemplateURL', + }, + }), + ); + }), +); + +integTest( + 'CDK synth bundled functions as expected', + withSamIntegrationFixture(async (fixture) => { + // Synth first + await fixture.cdkSynth(); + + const template = fixture.template('TestStack'); + + const expectedBundledAssets = [ + { + // Python Layer Version + id: 'PythonLayerVersion39495CEF', + files: [ + 'python/layer_version_dependency.py', + 'python/geonamescache/__init__.py', + 'python/geonamescache-1.3.0.dist-info', + ], }, - })); - } + { + // Layer Version + id: 'LayerVersion3878DA3A', + files: ['layer_version_dependency.py', 'requirements.txt'], + }, + { + // Bundled layer version + id: 'BundledLayerVersionPythonRuntime6BADBD6E', + files: [ + 'python/layer_version_dependency.py', + 'python/geonamescache/__init__.py', + 'python/geonamescache-1.3.0.dist-info', + ], + }, + { + // Python Function + id: 'PythonFunction0BCF77FD', + files: ['app.py', 'geonamescache/__init__.py', 'geonamescache-1.3.0.dist-info'], + }, + { + // Function + id: 'FunctionPythonRuntime28CBDA05', + files: ['app.py', 'requirements.txt'], + }, + { + // Bundled Function + id: 'BundledFunctionPythonRuntime4D9A0918', + files: ['app.py', 'geonamescache/__init__.py', 'geonamescache-1.3.0.dist-info'], + }, + { + // NodeJs Function + id: 'NodejsFunction09C1F20F', + files: ['index.js'], + }, + { + // Go Function + id: 'GoFunctionCA95FBAA', + files: ['bootstrap'], + }, + { + // Docker Image Function + id: 'DockerImageFunction28B773E6', + files: ['app.js', 'Dockerfile', 'package.json'], + }, + ]; - // Nested Stack - fixture.output.write('validate assets metadata for nested stack resource'); - expect('NestedStackNestedStackNestedStackNestedStackResourceB70834FD' in template.Resources).toBeTruthy(); - expect(template.Resources.NestedStackNestedStackNestedStackNestedStackResourceB70834FD).toEqual(expect.objectContaining({ - Metadata: { - 'aws:cdk:path': `${fixture.fullStackName('TestStack')}/NestedStack.NestedStack/NestedStack.NestedStackResource`, - 'aws:asset:path': expect.stringMatching(`${fixture.stackNamePrefix.replace(/-/, '')}TestStackNestedStack[0-9A-Z]{8}\.nested\.template\.json`), - 'aws:asset:property': 'TemplateURL', - }, - })); -})); - -integTest('CDK synth bundled functions as expected', withSamIntegrationFixture(async (fixture) => { - // Synth first - await fixture.cdkSynth(); - - const template = fixture.template('TestStack'); - - const expectedBundledAssets = [ - { - // Python Layer Version - id: 'PythonLayerVersion39495CEF', - files: [ - 'python/layer_version_dependency.py', - 'python/geonamescache/__init__.py', - 'python/geonamescache-1.3.0.dist-info', - ], - }, - { - // Layer Version - id: 'LayerVersion3878DA3A', - files: [ - 'layer_version_dependency.py', - 'requirements.txt', - ], - }, - { - // Bundled layer version - id: 'BundledLayerVersionPythonRuntime6BADBD6E', - files: [ - 'python/layer_version_dependency.py', - 'python/geonamescache/__init__.py', - 'python/geonamescache-1.3.0.dist-info', - ], - }, - { - // Python Function - id: 'PythonFunction0BCF77FD', - files: [ - 'app.py', - 'geonamescache/__init__.py', - 'geonamescache-1.3.0.dist-info', - ], - }, - { - // Function - id: 'FunctionPythonRuntime28CBDA05', - files: [ - 'app.py', - 'requirements.txt', - ], - }, - { - // Bundled Function - id: 'BundledFunctionPythonRuntime4D9A0918', - files: [ - 'app.py', - 'geonamescache/__init__.py', - 'geonamescache-1.3.0.dist-info', - ], - }, - { - // NodeJs Function - id: 'NodejsFunction09C1F20F', - files: [ - 'index.js', - ], - }, - { - // Go Function - id: 'GoFunctionCA95FBAA', - files: [ - 'bootstrap', - ], - }, - { - // Docker Image Function - id: 'DockerImageFunction28B773E6', - files: [ - 'app.js', - 'Dockerfile', - 'package.json', - ], - }, - ]; - - for (const resource of expectedBundledAssets) { - const assetPath = template.Resources[resource.id].Metadata['aws:asset:path']; - for (const file of resource.files) { - fixture.output.write(`validate Path ${file} for resource ${resource}`); - expect(existsSync(path.join(fixture.integTestDir, 'cdk.out', assetPath, file))).toBeTruthy(); + for (const resource of expectedBundledAssets) { + const assetPath = template.Resources[resource.id].Metadata['aws:asset:path']; + for (const file of resource.files) { + fixture.output.write(`validate Path ${file} for resource ${resource}`); + expect(existsSync(path.join(fixture.integTestDir, 'cdk.out', assetPath, file))).toBeTruthy(); + } } - } -})); - -integTest('sam can locally test the synthesized cdk application', withSamIntegrationFixture(async (fixture) => { - // Synth first - await fixture.cdkSynth(); - - const result = await fixture.samLocalStartApi( - 'TestStack', false, randomInteger(30000, 40000), '/restapis/spec/pythonFunction'); - expect(result.actionSucceeded).toBeTruthy(); - expect(result.actionOutput).toEqual(expect.objectContaining({ - message: 'Hello World', - })); -})); - -integTest('skips notice refresh', withDefaultFixture(async (fixture) => { - const output = await fixture.cdkSynth({ - options: ['--no-notices'], - modEnv: { - INTEG_STACK_SET: 'stage-using-context', - }, - allowErrExit: true, - }); + }), +); + +integTest( + 'sam can locally test the synthesized cdk application', + withSamIntegrationFixture(async (fixture) => { + // Synth first + await fixture.cdkSynth(); + + const result = await fixture.samLocalStartApi( + 'TestStack', + false, + randomInteger(30000, 40000), + '/restapis/spec/pythonFunction', + ); + expect(result.actionSucceeded).toBeTruthy(); + expect(result.actionOutput).toEqual( + expect.objectContaining({ + message: 'Hello World', + }), + ); + }), +); + +integTest( + 'skips notice refresh', + withDefaultFixture(async (fixture) => { + const output = await fixture.cdkSynth({ + options: ['--no-notices'], + modEnv: { + INTEG_STACK_SET: 'stage-using-context', + }, + allowErrExit: true, + }); - // Neither succeeds nor fails, but skips the refresh - await expect(output).not.toContain('Notices refreshed'); - await expect(output).not.toContain('Notices refresh failed'); -})); + // Neither succeeds nor fails, but skips the refresh + await expect(output).not.toContain('Notices refreshed'); + await expect(output).not.toContain('Notices refresh failed'); + }), +); /** * Create a queue, orphan that queue, then import the queue. * * We want to test with a large template to make sure large templates can work with import. */ -integTest('test resource import', withDefaultFixture(async (fixture) => { - // GIVEN - const randomPrefix = randomString(); - const uniqueOutputsFileName = `${randomPrefix}Outputs.json`; // other tests use the outputs file. Make sure we don't collide. - const outputsFile = path.join(fixture.integTestDir, 'outputs', uniqueOutputsFileName); - await fs.mkdir(path.dirname(outputsFile), { recursive: true }); - - // First, create a stack that includes many queues, and one queue that will be removed from the stack but NOT deleted from AWS. - await fixture.cdkDeploy('importable-stack', { - modEnv: { LARGE_TEMPLATE: '1', INCLUDE_SINGLE_QUEUE: '1', RETAIN_SINGLE_QUEUE: '1' }, - options: ['--outputs-file', outputsFile], - }); - - try { - - // Second, now the queue we will remove is in the stack and has a logicalId. We can now make the resource mapping file. - // This resource mapping file will be used to tell the import operation what queue to bring into the stack. - const fullStackName = fixture.fullStackName('importable-stack'); - const outputs = JSON.parse((await fs.readFile(outputsFile, { encoding: 'utf-8' })).toString()); - const queueLogicalId = outputs[fullStackName].QueueLogicalId; - const queueResourceMap = { - [queueLogicalId]: { QueueUrl: outputs[fullStackName].QueueUrl }, - }; - const mappingFile = path.join(fixture.integTestDir, 'outputs', `${randomPrefix}Mapping.json`); - await fs.writeFile( - mappingFile, - JSON.stringify(queueResourceMap), - { encoding: 'utf-8' }, - ); - - // Third, remove the queue from the stack, but don't delete the queue from AWS. +integTest( + 'test resource import', + withDefaultFixture(async (fixture) => { + // GIVEN + const randomPrefix = randomString(); + const uniqueOutputsFileName = `${randomPrefix}Outputs.json`; // other tests use the outputs file. Make sure we don't collide. + const outputsFile = path.join(fixture.integTestDir, 'outputs', uniqueOutputsFileName); + await fs.mkdir(path.dirname(outputsFile), { recursive: true }); + + // First, create a stack that includes many queues, and one queue that will be removed from the stack but NOT deleted from AWS. await fixture.cdkDeploy('importable-stack', { - modEnv: { LARGE_TEMPLATE: '1', INCLUDE_SINGLE_QUEUE: '0', RETAIN_SINGLE_QUEUE: '0' }, + modEnv: { LARGE_TEMPLATE: '1', INCLUDE_SINGLE_QUEUE: '1', RETAIN_SINGLE_QUEUE: '1' }, + options: ['--outputs-file', outputsFile], }); - const cfnTemplateBeforeImport = await fixture.aws.cloudFormation('getTemplate', { StackName: fullStackName }); - expect(cfnTemplateBeforeImport.TemplateBody).not.toContain(queueLogicalId); - // WHEN - await fixture.cdk( - ['import', '--resource-mapping', mappingFile, fixture.fullStackName('importable-stack')], - { modEnv: { LARGE_TEMPLATE: '1', INCLUDE_SINGLE_QUEUE: '1', RETAIN_SINGLE_QUEUE: '0' } }, - ); - - // THEN - const describeStacksResponse = await fixture.aws.cloudFormation('describeStacks', { StackName: fullStackName }); - const cfnTemplateAfterImport = await fixture.aws.cloudFormation('getTemplate', { StackName: fullStackName }); - expect(describeStacksResponse.Stacks![0].StackStatus).toEqual('IMPORT_COMPLETE'); - expect(cfnTemplateAfterImport.TemplateBody).toContain(queueLogicalId); - } finally { - // Clean up - await fixture.cdkDestroy('importable-stack'); - } -})); + try { + // Second, now the queue we will remove is in the stack and has a logicalId. We can now make the resource mapping file. + // This resource mapping file will be used to tell the import operation what queue to bring into the stack. + const fullStackName = fixture.fullStackName('importable-stack'); + const outputs = JSON.parse((await fs.readFile(outputsFile, { encoding: 'utf-8' })).toString()); + const queueLogicalId = outputs[fullStackName].QueueLogicalId; + const queueResourceMap = { + [queueLogicalId]: { QueueUrl: outputs[fullStackName].QueueUrl }, + }; + const mappingFile = path.join(fixture.integTestDir, 'outputs', `${randomPrefix}Mapping.json`); + await fs.writeFile(mappingFile, JSON.stringify(queueResourceMap), { encoding: 'utf-8' }); -integTest('test migrate deployment for app with localfile source in migrate.json', withDefaultFixture(async (fixture) => { - const outputsFile = path.join(fixture.integTestDir, 'outputs', 'outputs.json'); - await fs.mkdir(path.dirname(outputsFile), { recursive: true }); + // Third, remove the queue from the stack, but don't delete the queue from AWS. + await fixture.cdkDeploy('importable-stack', { + modEnv: { LARGE_TEMPLATE: '1', INCLUDE_SINGLE_QUEUE: '0', RETAIN_SINGLE_QUEUE: '0' }, + }); + const cfnTemplateBeforeImport = await fixture.aws.cloudFormation('getTemplate', { StackName: fullStackName }); + expect(cfnTemplateBeforeImport.TemplateBody).not.toContain(queueLogicalId); - // Initial deploy - await fixture.cdkDeploy('migrate-stack', { - modEnv: { ORPHAN_TOPIC: '1' }, - options: ['--outputs-file', outputsFile], - }); + // WHEN + await fixture.cdk(['import', '--resource-mapping', mappingFile, fixture.fullStackName('importable-stack')], { + modEnv: { LARGE_TEMPLATE: '1', INCLUDE_SINGLE_QUEUE: '1', RETAIN_SINGLE_QUEUE: '0' }, + }); - const outputs = JSON.parse((await fs.readFile(outputsFile, { encoding: 'utf-8' })).toString()); - const stackName = fixture.fullStackName('migrate-stack'); - const queueName = outputs[stackName].QueueName; - const queueUrl = outputs[stackName].QueueUrl; - const queueLogicalId = outputs[stackName].QueueLogicalId; - fixture.log(`Created queue ${queueUrl} in stack ${fixture.fullStackName}`); - - // Write the migrate file based on the ID from step one, then deploy the app with migrate - const migrateFile = path.join(fixture.integTestDir, 'migrate.json'); - await fs.writeFile( - migrateFile, JSON.stringify( - { Source: 'localfile', Resources: [{ ResourceType: 'AWS::SQS::Queue', LogicalResourceId: queueLogicalId, ResourceIdentifier: { QueueUrl: queueUrl } }] }, - ), - { encoding: 'utf-8' }, - ); + // THEN + const describeStacksResponse = await fixture.aws.cloudFormation('describeStacks', { StackName: fullStackName }); + const cfnTemplateAfterImport = await fixture.aws.cloudFormation('getTemplate', { StackName: fullStackName }); + expect(describeStacksResponse.Stacks![0].StackStatus).toEqual('IMPORT_COMPLETE'); + expect(cfnTemplateAfterImport.TemplateBody).toContain(queueLogicalId); + } finally { + // Clean up + await fixture.cdkDestroy('importable-stack'); + } + }), +); + +integTest( + 'test migrate deployment for app with localfile source in migrate.json', + withDefaultFixture(async (fixture) => { + const outputsFile = path.join(fixture.integTestDir, 'outputs', 'outputs.json'); + await fs.mkdir(path.dirname(outputsFile), { recursive: true }); + + // Initial deploy + await fixture.cdkDeploy('migrate-stack', { + modEnv: { ORPHAN_TOPIC: '1' }, + options: ['--outputs-file', outputsFile], + }); - await fixture.cdkDestroy('migrate-stack'); - fixture.log(`Deleted stack ${fixture.fullStackName}, orphaning ${queueName}`); + const outputs = JSON.parse((await fs.readFile(outputsFile, { encoding: 'utf-8' })).toString()); + const stackName = fixture.fullStackName('migrate-stack'); + const queueName = outputs[stackName].QueueName; + const queueUrl = outputs[stackName].QueueUrl; + const queueLogicalId = outputs[stackName].QueueLogicalId; + fixture.log(`Created queue ${queueUrl} in stack ${fixture.fullStackName}`); + + // Write the migrate file based on the ID from step one, then deploy the app with migrate + const migrateFile = path.join(fixture.integTestDir, 'migrate.json'); + await fs.writeFile( + migrateFile, + JSON.stringify({ + Source: 'localfile', + Resources: [ + { + ResourceType: 'AWS::SQS::Queue', + LogicalResourceId: queueLogicalId, + ResourceIdentifier: { QueueUrl: queueUrl }, + }, + ], + }), + { encoding: 'utf-8' }, + ); - // Create new stack from existing queue - try { - fixture.log(`Deploying new stack ${fixture.fullStackName}, migrating ${queueName} into stack`); - await fixture.cdkDeploy('migrate-stack'); - } finally { - // Cleanup await fixture.cdkDestroy('migrate-stack'); - } -})); - -integTest('hotswap deployment supports Lambda function\'s description and environment variables', withDefaultFixture(async (fixture) => { - // GIVEN - const stackArn = await fixture.cdkDeploy('lambda-hotswap', { - captureStderr: false, - modEnv: { - DYNAMIC_LAMBDA_PROPERTY_VALUE: 'original value', - }, - }); - - // WHEN - const deployOutput = await fixture.cdkDeploy('lambda-hotswap', { - options: ['--hotswap'], - captureStderr: true, - onlyStderr: true, - modEnv: { - DYNAMIC_LAMBDA_PROPERTY_VALUE: 'new value', - }, - }); - - const response = await fixture.aws.cloudFormation('describeStacks', { - StackName: stackArn, - }); - const functionName = response.Stacks?.[0].Outputs?.[0].OutputValue; - - // THEN + fixture.log(`Deleted stack ${fixture.fullStackName}, orphaning ${queueName}`); - // The deployment should not trigger a full deployment, thus the stack's status must remains - // "CREATE_COMPLETE" - expect(response.Stacks?.[0].StackStatus).toEqual('CREATE_COMPLETE'); - expect(deployOutput).toContain(`Lambda Function '${functionName}' hotswapped!`); -})); + // Create new stack from existing queue + try { + fixture.log(`Deploying new stack ${fixture.fullStackName}, migrating ${queueName} into stack`); + await fixture.cdkDeploy('migrate-stack'); + } finally { + // Cleanup + await fixture.cdkDestroy('migrate-stack'); + } + }), +); -integTest('hotswap deployment supports Fn::ImportValue intrinsic', withDefaultFixture(async (fixture) => { - // GIVEN - try { - await fixture.cdkDeploy('export-value-stack'); +integTest( + "hotswap deployment supports Lambda function's description and environment variables", + withDefaultFixture(async (fixture) => { + // GIVEN const stackArn = await fixture.cdkDeploy('lambda-hotswap', { captureStderr: false, modEnv: { DYNAMIC_LAMBDA_PROPERTY_VALUE: 'original value', - USE_IMPORT_VALUE_LAMBDA_PROPERTY: 'true', }, }); @@ -1729,7 +1923,6 @@ integTest('hotswap deployment supports Fn::ImportValue intrinsic', withDefaultFi onlyStderr: true, modEnv: { DYNAMIC_LAMBDA_PROPERTY_VALUE: 'new value', - USE_IMPORT_VALUE_LAMBDA_PROPERTY: 'true', }, }); @@ -1744,90 +1937,166 @@ integTest('hotswap deployment supports Fn::ImportValue intrinsic', withDefaultFi // "CREATE_COMPLETE" expect(response.Stacks?.[0].StackStatus).toEqual('CREATE_COMPLETE'); expect(deployOutput).toContain(`Lambda Function '${functionName}' hotswapped!`); + }), +); - } finally { - // Ensure cleanup in reverse order due to use of import/export - await fixture.cdkDestroy('lambda-hotswap'); - await fixture.cdkDestroy('export-value-stack'); - } -})); +integTest( + 'hotswap deployment supports Fn::ImportValue intrinsic', + withDefaultFixture(async (fixture) => { + // GIVEN + try { + await fixture.cdkDeploy('export-value-stack'); + const stackArn = await fixture.cdkDeploy('lambda-hotswap', { + captureStderr: false, + modEnv: { + DYNAMIC_LAMBDA_PROPERTY_VALUE: 'original value', + USE_IMPORT_VALUE_LAMBDA_PROPERTY: 'true', + }, + }); -integTest('hotswap deployment supports ecs service', withDefaultFixture(async (fixture) => { - // GIVEN - const stackArn = await fixture.cdkDeploy('ecs-hotswap', { - captureStderr: false, - }); + // WHEN + const deployOutput = await fixture.cdkDeploy('lambda-hotswap', { + options: ['--hotswap'], + captureStderr: true, + onlyStderr: true, + modEnv: { + DYNAMIC_LAMBDA_PROPERTY_VALUE: 'new value', + USE_IMPORT_VALUE_LAMBDA_PROPERTY: 'true', + }, + }); - // WHEN - const deployOutput = await fixture.cdkDeploy('ecs-hotswap', { - options: ['--hotswap'], - captureStderr: true, - onlyStderr: true, - modEnv: { - DYNAMIC_ECS_PROPERTY_VALUE: 'new value', - }, - }); + const response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + const functionName = response.Stacks?.[0].Outputs?.[0].OutputValue; - const response = await fixture.aws.cloudFormation('describeStacks', { - StackName: stackArn, - }); - const serviceName = response.Stacks?.[0].Outputs?.find(output => output.OutputKey == 'ServiceName')?.OutputValue; + // THEN - // THEN + // The deployment should not trigger a full deployment, thus the stack's status must remains + // "CREATE_COMPLETE" + expect(response.Stacks?.[0].StackStatus).toEqual('CREATE_COMPLETE'); + expect(deployOutput).toContain(`Lambda Function '${functionName}' hotswapped!`); + } finally { + // Ensure cleanup in reverse order due to use of import/export + await fixture.cdkDestroy('lambda-hotswap'); + await fixture.cdkDestroy('export-value-stack'); + } + }), +); + +integTest( + 'hotswap deployment supports ecs service', + withDefaultFixture(async (fixture) => { + // GIVEN + const stackArn = await fixture.cdkDeploy('ecs-hotswap', { + captureStderr: false, + }); - // The deployment should not trigger a full deployment, thus the stack's status must remains - // "CREATE_COMPLETE" - expect(response.Stacks?.[0].StackStatus).toEqual('CREATE_COMPLETE'); - expect(deployOutput).toContain(`ECS Service '${serviceName}' hotswapped!`); -})); + // WHEN + const deployOutput = await fixture.cdkDeploy('ecs-hotswap', { + options: ['--hotswap'], + captureStderr: true, + onlyStderr: true, + modEnv: { + DYNAMIC_ECS_PROPERTY_VALUE: 'new value', + }, + }); -integTest('hotswap deployment for ecs service waits for deployment to complete', withDefaultFixture(async (fixture) => { - // GIVEN - const stackArn = await fixture.cdkDeploy('ecs-hotswap', { - captureStderr: false, - }); + const response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + const serviceName = response.Stacks?.[0].Outputs?.find((output) => output.OutputKey == 'ServiceName')?.OutputValue; - // WHEN - await fixture.cdkDeploy('ecs-hotswap', { - options: ['--hotswap'], - modEnv: { - DYNAMIC_ECS_PROPERTY_VALUE: 'new value', - }, - }); + // THEN - const describeStacksResponse = await fixture.aws.cloudFormation('describeStacks', { - StackName: stackArn, - }); - const clusterName = describeStacksResponse.Stacks?.[0].Outputs?.find(output => output.OutputKey == 'ClusterName')?.OutputValue!; - const serviceName = describeStacksResponse.Stacks?.[0].Outputs?.find(output => output.OutputKey == 'ServiceName')?.OutputValue!; + // The deployment should not trigger a full deployment, thus the stack's status must remains + // "CREATE_COMPLETE" + expect(response.Stacks?.[0].StackStatus).toEqual('CREATE_COMPLETE'); + expect(deployOutput).toContain(`ECS Service '${serviceName}' hotswapped!`); + }), +); + +integTest( + 'hotswap deployment for ecs service waits for deployment to complete', + withDefaultFixture(async (fixture) => { + // GIVEN + const stackArn = await fixture.cdkDeploy('ecs-hotswap', { + captureStderr: false, + }); - // THEN + // WHEN + await fixture.cdkDeploy('ecs-hotswap', { + options: ['--hotswap'], + modEnv: { + DYNAMIC_ECS_PROPERTY_VALUE: 'new value', + }, + }); - const describeServicesResponse = await fixture.aws.ecs('describeServices', { - cluster: clusterName, - services: [serviceName], - }); - expect(describeServicesResponse.services?.[0].deployments).toHaveLength(1); // only one deployment present + const describeStacksResponse = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + const clusterName = describeStacksResponse.Stacks?.[0].Outputs?.find( + (output) => output.OutputKey == 'ClusterName', + )?.OutputValue!; + const serviceName = describeStacksResponse.Stacks?.[0].Outputs?.find( + (output) => output.OutputKey == 'ServiceName', + )?.OutputValue!; -})); + // THEN -integTest('hotswap deployment for ecs service detects failed deployment and errors', withDefaultFixture(async (fixture) => { - // GIVEN - await fixture.cdkDeploy('ecs-hotswap'); + const describeServicesResponse = await fixture.aws.ecs('describeServices', { + cluster: clusterName, + services: [serviceName], + }); + expect(describeServicesResponse.services?.[0].deployments).toHaveLength(1); // only one deployment present + }), +); - // WHEN - const deployOutput = await fixture.cdkDeploy('ecs-hotswap', { - options: ['--hotswap'], - modEnv: { - USE_INVALID_ECS_HOTSWAP_IMAGE: 'true', - }, - allowErrExit: true, - }); +integTest( + 'hotswap deployment for ecs service detects failed deployment and errors', + withDefaultFixture(async (fixture) => { + // GIVEN + await fixture.cdkDeploy('ecs-hotswap'); + + // WHEN + const deployOutput = await fixture.cdkDeploy('ecs-hotswap', { + options: ['--hotswap'], + modEnv: { + USE_INVALID_ECS_HOTSWAP_IMAGE: 'true', + }, + allowErrExit: true, + }); + + // THEN + expect(deployOutput).toContain( + `❌ ${fixture.stackNamePrefix}-ecs-hotswap failed: ResourceNotReady: Resource is not in the state deploymentCompleted`, + ); + expect(deployOutput).not.toContain('hotswapped!'); + }), +); + +integTest( + 'hotswap deployment for ecs service detects failed deployment and errors', + withDefaultFixture(async (fixture) => { + // GIVEN + await fixture.cdkDeploy('ecs-hotswap'); + + // WHEN + const deployOutput = await fixture.cdkDeploy('ecs-hotswap', { + options: ['--hotswap'], + modEnv: { + USE_INVALID_ECS_HOTSWAP_IMAGE: 'true', + }, + allowErrExit: true, + }); + + const stackName = `${fixture.stackNamePrefix}-ecs-hotswap`; + const expectedSubstring = `❌ ${chalk.bold(stackName)} failed: ResourceNotReady: Resource is not in the state deploymentCompleted`; - // THEN - expect(deployOutput).toContain(`❌ ${fixture.stackNamePrefix}-ecs-hotswap failed: ResourceNotReady: Resource is not in the state deploymentCompleted`); - expect(deployOutput).not.toContain('hotswapped!'); -})); + expect(deployOutput).toContain(expectedSubstring); + expect(deployOutput).not.toContain('hotswapped!'); + }), +); async function listChildren(parent: string, pred: (x: string) => Promise) { const ret = new Array();