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 } 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 = { 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' } }); 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' } }); })); // testing a construct with a builtin Nodejs Lambda Function. // In this case we are testing the s3.Bucket construct with the // autoDeleteObjects prop set to true, which creates a Lambda backed // CustomResource. Since the compiled Lambda code (e.g. __entrypoint__.js) // 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'); })); // 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`, }, }, }, })); 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`, }, }, }, })); })); 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'); // 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', 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('deploy --method=direct', withDefaultFixture(async (fixture) => { const stackArn = await fixture.cdkDeploy('test-2', { options: ['--method=direct'], captureStderr: false, }); // 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); })); 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 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'); //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], }); 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, }); 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'), }); const stackArn = response.Stacks?.[0].StackId; expect(response.Stacks?.[0].StackStatus).toEqual('ROLLBACK_COMPLETE'); // WHEN const newStackArn = await fixture.cdkDeploy('param-test-1', { options: [ '--parameters', `TopicNameParam=${fixture.stackNamePrefix}allgood`, ], captureStderr: false, }); 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, }); let response = await fixture.aws.cloudFormation('describeStacks', { StackName: stackArn, }); 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');; response = await fixture.aws.cloudFormation('describeStacks', { StackName: stackArn, }); expect(response.Stacks?.[0].StackStatus).toEqual('UPDATE_ROLLBACK_COMPLETE'); // WHEN await fixture.cdkDeploy('param-test-1', { options: [ '--parameters', `TopicNameParam=${fixture.stackNamePrefix}allgood`, ], captureStderr: false, }); response = await fixture.aws.cloudFormation('describeStacks', { StackName: stackArn, }); // 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, }); const response = await fixture.aws.cloudFormation('describeStacks', { StackName: stackArn, }); expect(response.Stacks?.[0].Parameters).toContainEqual( { ParameterKey: 'DisplayNameParam', ParameterValue: paramVal1, }, ); expect(response.Stacks?.[0].Parameters).toContainEqual( { ParameterKey: 'OtherDisplayNameParam', ParameterValue: paramVal2, }, ); })); 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], }); // 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 } 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', { RoleName: roleName, PolicyName: 'DefaultPolicy', PolicyDocument: JSON.stringify({ Version: '2012-10-17', Statement: [{ Action: '*', Resource: '*', Effect: 'Allow', }], }), }); await retry(fixture.output, 'Trying to assume fresh role', retry.forSeconds(300), async () => { await fixture.aws.sts('assumeRole', { RoleArn: roleArn, RoleSessionName: 'testing', }); }); // 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 fixture.cdkDeploy('test-2', { options: ['--role-arn', roleArn], }); // 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, }); } 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']); } 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); })); }); 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); })); // integTest('cdk migrate --from-scan with AND/OR filters correctly filters resources', withExtendedTimeoutFixture(async (fixture) => { // const stackName = `cdk-migrate-integ-${fixture.randomString}`; // await fixture.cdkDeploy('migrate-stack', { // modEnv: { SAMPLE_RESOURCES: '1' }, // }); // await fixture.cdk( // ['migrate', '--stack-name', stackName, '--from-scan', 'new', '--filter', 'type=AWS::SNS::Topic,tag-key=tag1', 'type=AWS::SQS::Queue,tag-key=tag3'], // { modEnv: { MIGRATE_INTEG_TEST: '1' }, neverRequireApproval: true, verbose: true, captureStderr: false }, // ); // try { // const response = await fixture.aws.cloudFormation('describeGeneratedTemplate', { // GeneratedTemplateName: stackName, // }); // const resourceNames = []; // for (const resource of response.Resources || []) { // if (resource.LogicalResourceId) { // resourceNames.push(resource.LogicalResourceId); // } // } // fixture.log(`Resources: ${resourceNames}`); // expect(resourceNames.some(ele => ele && ele.includes('migratetopic1'))).toBeTruthy(); // expect(resourceNames.some(ele => ele && ele.includes('migratequeue1'))).toBeTruthy(); // } finally { // await fixture.cdkDestroy('migrate-stack'); // await fixture.aws.cloudFormation('deleteGeneratedTemplate', { // GeneratedTemplateName: stackName, // }); // } // })); // integTest('cdk migrate --from-scan for resources with Write Only Properties generates warnings', withExtendedTimeoutFixture(async (fixture) => { // const stackName = `cdk-migrate-integ-${fixture.randomString}`; // await fixture.cdkDeploy('migrate-stack', { // modEnv: { // LAMBDA_RESOURCES: '1', // }, // }); // await fixture.cdk( // ['migrate', '--stack-name', stackName, '--from-scan', 'new', '--filter', 'type=AWS::Lambda::Function,tag-key=lambda-tag'], // { modEnv: { MIGRATE_INTEG_TEST: '1' }, neverRequireApproval: true, verbose: true, captureStderr: false }, // ); // try { // const response = await fixture.aws.cloudFormation('describeGeneratedTemplate', { // GeneratedTemplateName: stackName, // }); // const resourceNames = []; // for (const resource of response.Resources || []) { // if (resource.LogicalResourceId && resource.ResourceType === 'AWS::Lambda::Function') { // resourceNames.push(resource.LogicalResourceId); // } // } // fixture.log(`Resources: ${resourceNames}`); // const readmePath = path.join(fixture.integTestDir, stackName, 'README.md'); // const readme = await fs.readFile(readmePath, 'utf8'); // expect(readme).toContain('## Warnings'); // for (const resourceName of resourceNames) { // expect(readme).toContain(`### ${resourceName}`); // } // } finally { // await fixture.cdkDestroy('migrate-stack'); // await fixture.aws.cloudFormation('deleteGeneratedTemplate', { // GeneratedTemplateName: stackName, // }); // } // })); ['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'); } })); }); 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<string> { 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'); // 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 --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'); } 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 */ type DependencyDetails = { id: string; dependencies: DependencyDetails[]; }; type StackDetails = { id: string; 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: [], }, ], }, ]; 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); } } } 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', }, }); 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', }, }); })); 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, }, }); } // Use this directory as a Cloud Assembly const output = await fixture.cdk([ '--app', cxAsmDir, '-v', 'synth', ]); // 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'); // Deploy a Lambda from the copied asm await fixture.cdkDeploy('lambda', { options: ['-a', '.'], cwd: asmOutputDir }); // 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); } })); 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', ], }, ]; 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, }); // 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 with a fresh name, redeploy orphaning the queue, then import it again */ integTest('test resource import', 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('importable-stack', { modEnv: { ORPHAN_TOPIC: '1' }, options: ['--outputs-file', outputsFile], }); const outputs = JSON.parse((await fs.readFile(outputsFile, { encoding: 'utf-8' })).toString()); const queueName = outputs.QueueName; const queueLogicalId = outputs.QueueLogicalId; fixture.log(`Setup complete, created queue ${queueName}`); try { // Deploy again, orphaning the queue await fixture.cdkDeploy('importable-stack', { modEnv: { OMIT_TOPIC: '1' }, }); // Write a resource mapping file based on the ID from step one, then run an import const mappingFile = path.join(fixture.integTestDir, 'outputs', 'mapping.json'); await fs.writeFile(mappingFile, JSON.stringify({ [queueLogicalId]: { QueueName: queueName } }), { encoding: 'utf-8' }); await fixture.cdk(['import', '--resource-mapping', mappingFile, fixture.fullStackName('importable-stack')]); } finally { // Cleanup 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], }); 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' }, ); await fixture.cdkDestroy('migrate-stack'); fixture.log(`Deleted stack ${fixture.fullStackName}, orphaning ${queueName}`); // 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 // 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!`); })); 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', }, }); // 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', }, }); const response = await fixture.aws.cloudFormation('describeStacks', { StackName: stackArn, }); const functionName = response.Stacks?.[0].Outputs?.[0].OutputValue; // 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'); } })); async function listChildren(parent: string, pred: (x: string) => Promise<boolean>) { const ret = new Array<string>(); for (const child of await fs.readdir(parent, { encoding: 'utf-8' })) { const fullPath = path.join(parent, child.toString()); if (await pred(fullPath)) { ret.push(fullPath); } } return ret; } async function listChildDirs(parent: string) { return listChildren(parent, async (fullPath: string) => (await fs.stat(fullPath)).isDirectory()); }