From 7dd5ca0356f81825f0d0338696e982db45765e1d Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 12 May 2020 10:10:43 +0200 Subject: [PATCH] chore: change CLI integ tests from Bash to TypeScript Convert most tests from Bash to TypeScript. It's much more comfortable to write complex tests in a programming language than bash. Plus, `jest` is a better test runner than bash, too. Even though tests are now written in TypeScript, this does not conceptually change their SUT! They are still testing the CLI via running it as a subprocess, they are NOT reaching directly into the CLI code to test its private methods. It's just that setup/teardown/error handling/assertions/AWS calls are much more convenient to do in a real programming language. Compilation of the tests is done as part of the normal package build, at which point it is using the dependencies brought in by the containing `aws-cdk` package's `package.json`. When run in a non-develompent repo (as done during integ tests or canary runs), the required dependencies are brought in just-in-time via `test-jest.sh`. Using a separate `package.json` just for the tests makes it a package in its own right for `yarn`, and I didn't want to go there. --- packages/aws-cdk/test/integ/cli/.gitignore | 3 + packages/aws-cdk/test/integ/cli/README.md | 24 +- .../aws-cdk/test/integ/cli/aws-helpers.ts | 143 ++++++ .../test/integ/cli/bootstrapping.integtest.ts | 49 ++ ...oy-wildcard-with-outputs-expected.template | 8 - .../cdk-deploy-with-outputs-expected.template | 5 - .../aws-cdk/test/integ/cli/cdk-helpers.ts | 189 +++++++ .../aws-cdk/test/integ/cli/cli.integtest.ts | 463 ++++++++++++++++++ .../cli/test-cdk-bootstrap-no-execute.sh | 25 - .../test/integ/cli/test-cdk-context.sh | 26 - .../test/integ/cli/test-cdk-deploy-all.sh | 23 - ...cdk-deploy-nested-stack-with-parameters.sh | 28 -- .../integ/cli/test-cdk-deploy-no-execute.sh | 27 - .../test/integ/cli/test-cdk-deploy-no-tty.sh | 16 - .../test-cdk-deploy-wildcard-with-outputs.sh | 38 -- ...est-cdk-deploy-wildcard-with-parameters.sh | 25 - .../test-cdk-deploy-with-notification-arns.sh | 34 -- .../integ/cli/test-cdk-deploy-with-outputs.sh | 36 -- .../test-cdk-deploy-with-parameters-multi.sh | 40 -- .../cli/test-cdk-deploy-with-parameters.sh | 28 -- .../integ/cli/test-cdk-deploy-with-role.sh | 79 --- .../aws-cdk/test/integ/cli/test-cdk-deploy.sh | 26 - .../aws-cdk/test/integ/cli/test-cdk-diff.sh | 19 - .../aws-cdk/test/integ/cli/test-cdk-docker.sh | 17 - .../cli/test-cdk-failed-deploy-doesnt-hang.sh | 22 - .../test/integ/cli/test-cdk-fast-deploy.sh | 61 --- .../test/integ/cli/test-cdk-iam-diff.sh | 28 -- .../aws-cdk/test/integ/cli/test-cdk-lambda.sh | 27 - .../aws-cdk/test/integ/cli/test-cdk-ls.sh | 31 -- .../cli/test-cdk-multiple-toolkit-stacks.sh | 31 -- .../test/integ/cli/test-cdk-no-resource.sh | 33 -- .../aws-cdk/test/integ/cli/test-cdk-order.sh | 15 - .../test-cdk-ssm-parameter-provider-error.sh | 20 - .../aws-cdk/test/integ/cli/test-cdk-synth.sh | 31 -- .../cli/test-cdk-termination-protection.sh | 26 - .../test/integ/cli/test-cdk-version.sh | 14 - packages/aws-cdk/test/integ/cli/test-jest.sh | 19 + .../aws-cdk/test/integ/cli/test-vpc-lookup.sh | 24 - 38 files changed, 889 insertions(+), 864 deletions(-) create mode 100644 packages/aws-cdk/test/integ/cli/aws-helpers.ts create mode 100644 packages/aws-cdk/test/integ/cli/bootstrapping.integtest.ts delete mode 100644 packages/aws-cdk/test/integ/cli/cdk-deploy-wildcard-with-outputs-expected.template delete mode 100644 packages/aws-cdk/test/integ/cli/cdk-deploy-with-outputs-expected.template create mode 100644 packages/aws-cdk/test/integ/cli/cdk-helpers.ts create mode 100644 packages/aws-cdk/test/integ/cli/cli.integtest.ts delete mode 100755 packages/aws-cdk/test/integ/cli/test-cdk-bootstrap-no-execute.sh delete mode 100755 packages/aws-cdk/test/integ/cli/test-cdk-context.sh delete mode 100755 packages/aws-cdk/test/integ/cli/test-cdk-deploy-all.sh delete mode 100755 packages/aws-cdk/test/integ/cli/test-cdk-deploy-nested-stack-with-parameters.sh delete mode 100755 packages/aws-cdk/test/integ/cli/test-cdk-deploy-no-execute.sh delete mode 100755 packages/aws-cdk/test/integ/cli/test-cdk-deploy-no-tty.sh delete mode 100755 packages/aws-cdk/test/integ/cli/test-cdk-deploy-wildcard-with-outputs.sh delete mode 100755 packages/aws-cdk/test/integ/cli/test-cdk-deploy-wildcard-with-parameters.sh delete mode 100755 packages/aws-cdk/test/integ/cli/test-cdk-deploy-with-notification-arns.sh delete mode 100755 packages/aws-cdk/test/integ/cli/test-cdk-deploy-with-outputs.sh delete mode 100755 packages/aws-cdk/test/integ/cli/test-cdk-deploy-with-parameters-multi.sh delete mode 100755 packages/aws-cdk/test/integ/cli/test-cdk-deploy-with-parameters.sh delete mode 100755 packages/aws-cdk/test/integ/cli/test-cdk-deploy-with-role.sh delete mode 100755 packages/aws-cdk/test/integ/cli/test-cdk-deploy.sh delete mode 100755 packages/aws-cdk/test/integ/cli/test-cdk-diff.sh delete mode 100755 packages/aws-cdk/test/integ/cli/test-cdk-docker.sh delete mode 100755 packages/aws-cdk/test/integ/cli/test-cdk-failed-deploy-doesnt-hang.sh delete mode 100755 packages/aws-cdk/test/integ/cli/test-cdk-fast-deploy.sh delete mode 100755 packages/aws-cdk/test/integ/cli/test-cdk-iam-diff.sh delete mode 100755 packages/aws-cdk/test/integ/cli/test-cdk-lambda.sh delete mode 100755 packages/aws-cdk/test/integ/cli/test-cdk-ls.sh delete mode 100755 packages/aws-cdk/test/integ/cli/test-cdk-multiple-toolkit-stacks.sh delete mode 100755 packages/aws-cdk/test/integ/cli/test-cdk-no-resource.sh delete mode 100755 packages/aws-cdk/test/integ/cli/test-cdk-order.sh delete mode 100755 packages/aws-cdk/test/integ/cli/test-cdk-ssm-parameter-provider-error.sh delete mode 100755 packages/aws-cdk/test/integ/cli/test-cdk-synth.sh delete mode 100644 packages/aws-cdk/test/integ/cli/test-cdk-termination-protection.sh delete mode 100755 packages/aws-cdk/test/integ/cli/test-cdk-version.sh create mode 100755 packages/aws-cdk/test/integ/cli/test-jest.sh delete mode 100755 packages/aws-cdk/test/integ/cli/test-vpc-lookup.sh diff --git a/packages/aws-cdk/test/integ/cli/.gitignore b/packages/aws-cdk/test/integ/cli/.gitignore index e136cb6cd26fb..7d9a78e2061f1 100644 --- a/packages/aws-cdk/test/integ/cli/.gitignore +++ b/packages/aws-cdk/test/integ/cli/.gitignore @@ -1 +1,4 @@ cdk.context.json +node_modules +*.d.ts +*.js diff --git a/packages/aws-cdk/test/integ/cli/README.md b/packages/aws-cdk/test/integ/cli/README.md index f22e59447b380..a0557214897cc 100644 --- a/packages/aws-cdk/test/integ/cli/README.md +++ b/packages/aws-cdk/test/integ/cli/README.md @@ -6,9 +6,31 @@ CLI on a simple JavaScript CDK app (stored in `app/`). ## Entry point ``` -./test.sh +../run-against-repo ./test.sh ``` +## Adding tests + +Older tests were written in bash; new tests should be written in +TypeScript/Jest, that is much more comfortable to write in. + +Even though tests are now written in TypeScript, this does not +conceptually change their SUT! They are still testing the CLI via +running it as a subprocess, they are NOT reaching directly into the CLI +code to test its private methods. It's just that setup/teardown/error +handling/assertions/AWS calls are much more convenient to do in a real +programming language. + +Compilation of the tests is done as part of the normal package build, at +which point it is using the dependencies brought in by the containing +`aws-cdk` package's `package.json`. + +When run in a non-develompent repo (as done during integ tests or canary runs), +the required dependencies are brought in just-in-time via `test-jest.sh`. Any +new dependencies added for the tests should be added there as well. But, better +yet, don't add any dependencies at all. You shouldn't need to, these tests +are simple. + ## Configuration AWS credentials must be configured. diff --git a/packages/aws-cdk/test/integ/cli/aws-helpers.ts b/packages/aws-cdk/test/integ/cli/aws-helpers.ts new file mode 100644 index 0000000000000..40e83cc7c0441 --- /dev/null +++ b/packages/aws-cdk/test/integ/cli/aws-helpers.ts @@ -0,0 +1,143 @@ +import * as AWS from 'aws-sdk'; +import { log } from './cdk-helpers'; + +interface Env { + account: string; + region: string; +} + +export let testEnv = async (): Promise => { + const response = await new AWS.STS().getCallerIdentity().promise(); + + const ret: Env = { + account: response.Account!, + region: process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION ?? 'us-east-1', + }; + + testEnv = () => Promise.resolve(ret); + return ret; +}; + +export const cloudFormation = makeAwsCaller(AWS.CloudFormation); +export const sns = makeAwsCaller(AWS.SNS); +export const iam = makeAwsCaller(AWS.IAM); +export const lambda = makeAwsCaller(AWS.Lambda); +export const sts = makeAwsCaller(AWS.STS); + +/** + * Perform an AWS call from nothing + * + * Create the correct client, do the call and resole the promise(). + */ +async function awsCall< + A extends AWS.Service, + B extends keyof ServiceCalls, +>(ctor: new (config: any) => A, call: B, request: First[B]>): Promise[B]>> { + const env = await testEnv(); + const cfn = new ctor({ region: env.region }); + const response = cfn[call](request); + try { + return await response.promise(); + } catch (e) { + const newErr = new Error(`${call}(${JSON.stringify(request)}): ${e.message}`); + (newErr as any).code = e.code; + throw newErr; + } +} + +/** + * Factory function to invoke 'awsCall' for specific services. + * + * Not strictly necessary but calling this replaces a whole bunch of annoying generics you otherwise have to type: + * + * ```ts + * export function cloudFormation< + * C extends keyof ServiceCalls, + * >(call: C, request: First[C]>): Promise[C]>> { + * return awsCall(AWS.CloudFormation, call, request); + * } + * ``` + */ +function makeAwsCaller(ctor: new (config: any) => A) { + return >(call: B, request: First[B]>): Promise[B]>> => { + return awsCall(ctor, call, request); + }; +} + +type ServiceCalls = NoNayNever>; +// Map ever member in the type to the important AWS call overload, or to 'never' +type SimplifiedService = {[k in keyof T]: AwsCallIO}; +// Remove all 'never' types from an object type +type NoNayNever = Pick; + +// Because of the overloads an AWS handler type looks like this: +// +// { +// (params: INPUTSTRUCT, callback?: ((err: AWSError, data: {}) => void) | undefined): Request; +// (callback?: ((err: AWS.AWSError, data: {}) => void) | undefined): AWS.Request<...>; +// } +// +// Get the first overload and extract the input and output struct types +type AwsCallIO = + T extends { + (args: infer INPUT, callback?: ((err: AWS.AWSError, data: any) => void) | undefined): AWS.Request; + (callback?: ((err: AWS.AWSError, data: {}) => void) | undefined): AWS.Request; + } ? [INPUT, OUTPUT] : never; + +type First = T extends [any, any] ? T[0] : never; +type Second = T extends [any, any] ? T[1] : never; + +export async function deleteStacks(...stackNames: string[]) { + if (stackNames.length === 0) { return; } + + for (const stackName of stackNames) { + await cloudFormation('deleteStack', { + StackName: stackName, + }); + } + + await retry(`Deleting ${stackNames}`, afterSeconds(300), async () => { + for (const stackName of stackNames) { + if (await stackExists(stackName)) { + throw new Error(`Delete of '${stackName}' not complete yet`); + } + } + }); +} + +export async function stackExists(stackName: string) { + try { + await cloudFormation('describeStacks', { StackName: stackName }); + return true; + } catch (e) { + if (e.message.indexOf('does not exist') > -1) { return false; } + throw e; + } +} + +export function afterSeconds(seconds: number): Date { + return new Date(Date.now() + seconds * 1000); +} + +export async function retry(operation: string, deadline: Date, block: () => Promise): Promise { + let i = 0; + log(`💈 ${operation}`); + while (true) { + try { + i++; + const ret = await block(); + log(`💈 ${operation}: succeeded after ${i} attempts`); + return ret; + } catch (e) { + if (Date.now() > deadline.getTime()) { + throw new Error(`${operation}: did not succeed after ${i} attempts: ${e}`); + } + log(`⏳ ${operation} (${e.message})`); + await sleep(5000); + } + } +} + +export async function sleep(ms: number) { + return new Promise(ok => setTimeout(ok, ms)); +} \ No newline at end of file diff --git a/packages/aws-cdk/test/integ/cli/bootstrapping.integtest.ts b/packages/aws-cdk/test/integ/cli/bootstrapping.integtest.ts new file mode 100644 index 0000000000000..5f5707799f8be --- /dev/null +++ b/packages/aws-cdk/test/integ/cli/bootstrapping.integtest.ts @@ -0,0 +1,49 @@ +import { cloudFormation, deleteStacks } from './aws-helpers'; +import { cdk, cleanupOldStacks, fullStackName, prepareAppFixture } from './cdk-helpers'; + +jest.setTimeout(600 * 1000); + +beforeEach(async () => { + await prepareAppFixture(); + await cleanupOldStacks(); +}); + +afterEach(async () => { + await cleanupOldStacks(); +}); + +test('can bootstrap without execution', async () => { + const bootstrapStackName = fullStackName('toolkit-stack-1'); + + await cdk(['bootstrap', '-v', + '--toolkit-stack-name', bootstrapStackName, + '--no-execute']); + try { + + const resp = await cloudFormation('describeStacks', { + StackName: bootstrapStackName, + }); + + expect(resp.Stacks?.[0].StackStatus).toEqual('REVIEW_IN_PROGRESS'); + } finally { + await deleteStacks(bootstrapStackName); + } +}); + +test('can bootstrap multiple toolkit stacks', async () => { + const bootstrapStackName1 = fullStackName('toolkit-stack-1'); + const bootstrapStackName2 = fullStackName('toolkit-stack-2'); + try { + // deploy two toolkit stacks into the same environment (see #1416) + // one with tags + await cdk(['bootstrap', '-v', '--toolkit-stack-name', bootstrapStackName1, '--tags', 'Foo=Bar']); + await cdk(['bootstrap', '-v', '--toolkit-stack-name', bootstrapStackName2]); + + const response = await cloudFormation('describeStacks', { StackName: bootstrapStackName1 }); + expect(response.Stacks?.[0].Tags).toEqual([ + { Key: 'Foo', Value: 'Bar' }, + ]); + } finally { + await deleteStacks(bootstrapStackName1, bootstrapStackName2); + } +}); \ No newline at end of file diff --git a/packages/aws-cdk/test/integ/cli/cdk-deploy-wildcard-with-outputs-expected.template b/packages/aws-cdk/test/integ/cli/cdk-deploy-wildcard-with-outputs-expected.template deleted file mode 100644 index 6e682e2a96d4c..0000000000000 --- a/packages/aws-cdk/test/integ/cli/cdk-deploy-wildcard-with-outputs-expected.template +++ /dev/null @@ -1,8 +0,0 @@ -{ - "%STACK_NAME_PREFIX%-outputs-test-1": { - "TopicName": "%STACK_NAME_PREFIX%-outputs-test-1MyTopic" - }, - "%STACK_NAME_PREFIX%-outputs-test-2": { - "TopicName": "%STACK_NAME_PREFIX%-outputs-test-2MyOtherTopic" - } -} \ No newline at end of file diff --git a/packages/aws-cdk/test/integ/cli/cdk-deploy-with-outputs-expected.template b/packages/aws-cdk/test/integ/cli/cdk-deploy-with-outputs-expected.template deleted file mode 100644 index 47b2f6e574e17..0000000000000 --- a/packages/aws-cdk/test/integ/cli/cdk-deploy-with-outputs-expected.template +++ /dev/null @@ -1,5 +0,0 @@ -{ - "%STACK_NAME_PREFIX%-outputs-test-1": { - "TopicName": "%STACK_NAME_PREFIX%-outputs-test-1MyTopic" - } -} diff --git a/packages/aws-cdk/test/integ/cli/cdk-helpers.ts b/packages/aws-cdk/test/integ/cli/cdk-helpers.ts new file mode 100644 index 0000000000000..b7c5ed6f20c83 --- /dev/null +++ b/packages/aws-cdk/test/integ/cli/cdk-helpers.ts @@ -0,0 +1,189 @@ +import * as child_process from 'child_process'; +import * as path from 'path'; +import { cloudFormation, deleteStacks, testEnv } from './aws-helpers'; + +export const INTEG_TEST_DIR = '/tmp/cdk-integ-test2'; + +export const STACK_NAME_PREFIX = process.env.STACK_NAME_PREFIX || (() => { + // Make the stack names unique based on the codebuild project name + // (if it exists). This prevents multiple codebuild projects stomping + // on each other's stacks and failing them. + // + // The get codebuild project name from the ID: PROJECT_NAME:1238a83 + if (process.env.CODEBUILD_BUILD_ID) { return process.env.CODEBUILD_BUILD_ID.split(':')[0]; } + if (process.env.IS_CANARY === 'true') { return 'cdk-toolkit-canary'; } + return 'cdk-toolkit-integration'; +})(); + +export interface ShellOptions extends child_process.SpawnOptions { + /** + * Properties to add to 'env' + */ + modEnv?: Record; + + /** + * Don't fail when exiting with an error + * + * @default false + */ + allowErrExit?: boolean; + + /** + * Whether to capture stderr + * + * @default true + */ + captureStderr?: boolean; +} + +export interface CdkCliOptions extends ShellOptions { + options?: string[]; +} + +export function log(x: string) { + process.stderr.write(x + '\n'); +} + +export async function cdkDeploy(stackNames: string | string[], options: CdkCliOptions = {}) { + stackNames = typeof stackNames === 'string' ? [stackNames] : stackNames; + + return await cdk(['deploy', + '--require-approval=never', // We never want a prompt in an unattended test + ...(options.options ?? []), + ...fullStackName(stackNames)], options); +} + +export async function cdkDestroy(stackNames: string | string[], options: CdkCliOptions = {}) { + stackNames = typeof stackNames === 'string' ? [stackNames] : stackNames; + + return await cdk(['destroy', + '-f', // We never want a prompt in an unattended test + ...(options.options ?? []), + ...fullStackName(stackNames)], options); +} + +export async function cdk(args: string[], options: CdkCliOptions = {}) { + return await shell(['cdk', ...args], { + cwd: INTEG_TEST_DIR, + ...options, + modEnv: { + AWS_REGION: (await testEnv()).region, + AWS_DEFAULT_REGION: (await testEnv()).region, + ...options.modEnv, + }, + }); +} + +export function fullStackName(stackName: string): string; +export function fullStackName(stackNames: string[]): string[]; +export function fullStackName(stackNames: string | string[]): string | string[] { + if (typeof stackNames === 'string') { + return `${STACK_NAME_PREFIX}-${stackNames}`; + } else { + return stackNames.map(s => `${STACK_NAME_PREFIX}-${s}`); + } +} + +/** + * Prepare the app fixture + * + * If this is done in the main test script, it will be skipped + * in the subprocess scripts since the app fixture can just be reused. + */ +export async function prepareAppFixture() { + if (!process.env.FIXTURE_PREPARED) { + await shell(['rm', '-rf', INTEG_TEST_DIR]); + await shell(['mkdir', '-p', INTEG_TEST_DIR]); + await shell(['cp', '-R', path.join(__dirname, 'app') + '/', INTEG_TEST_DIR]); + + await shell(['npm', 'install', + '@aws-cdk/core', + '@aws-cdk/aws-sns', + '@aws-cdk/aws-iam', + '@aws-cdk/aws-lambda', + '@aws-cdk/aws-ssm', + '@aws-cdk/aws-ecr-assets', + '@aws-cdk/aws-cloudformation', + '@aws-cdk/aws-ec2'], { + cwd: INTEG_TEST_DIR, + }); + + process.env.FIXTURE_PREPARED = '1'; + } +} + +/** + * Cleanup leftover stacks + */ +export async function cleanupOldStacks() { + const response = await cloudFormation('listStacks', { + StackStatusFilter: [ + 'CREATE_IN_PROGRESS' , 'CREATE_FAILED' , 'CREATE_COMPLETE' , + 'ROLLBACK_IN_PROGRESS' , 'ROLLBACK_FAILED' , 'ROLLBACK_COMPLETE' , + 'DELETE_FAILED', + 'UPDATE_IN_PROGRESS' , 'UPDATE_COMPLETE_CLEANUP_IN_PROGRESS' , + 'UPDATE_COMPLETE' , 'UPDATE_ROLLBACK_IN_PROGRESS' , + 'UPDATE_ROLLBACK_FAILED' , + 'UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS' , + 'UPDATE_ROLLBACK_COMPLETE' , 'REVIEW_IN_PROGRESS' , + 'IMPORT_IN_PROGRESS' , 'IMPORT_COMPLETE' , + 'IMPORT_ROLLBACK_IN_PROGRESS' , 'IMPORT_ROLLBACK_FAILED' , + 'IMPORT_ROLLBACK_COMPLETE' , + ], + }); + + const stacksToDelete = (response.StackSummaries ?? []) + .filter(s => s.StackName.startsWith(STACK_NAME_PREFIX)) + .map(s => s.StackName); + await deleteStacks(...stacksToDelete); +} + +/** + * A shell command that does what you want + * + * Is platform-aware, handles errors nicely. + */ +export async function shell(command: string[], options: ShellOptions = {}): Promise { + if (options.modEnv && options.env) { + throw new Error('Use either env or modEnv but not both'); + } + + log(`💻 ${command.join(' ')}`); + + const env = options.env ?? (options.modEnv ? {...process.env, ...options.modEnv} : undefined); + + const child = child_process.spawn(command[0], command.slice(1), { + ...options, + env, + // Need this for Windows where we want .cmd and .bat to be found as well. + shell: true, + stdio: [ 'ignore', 'pipe', 'pipe' ], + }); + + return new Promise((resolve, reject) => { + const stdout = new Array(); + const stderr = new Array(); + + child.stdout!.on('data', chunk => { + process.stdout.write(chunk); + stdout.push(chunk); + }); + + child.stderr!.on('data', chunk => { + process.stderr.write(chunk); + if (options.captureStderr ?? true) { + stderr.push(chunk); + } + }); + + child.once('error', reject); + + child.once('close', code => { + if (code === 0 || options.allowErrExit) { + resolve((Buffer.concat(stdout).toString('utf-8') + Buffer.concat(stderr).toString('utf-8')).trim()); + } else { + reject(new Error(`'${command.join(' ')}' exited with error code ${code}`)); + } + }); + }); +} \ No newline at end of file diff --git a/packages/aws-cdk/test/integ/cli/cli.integtest.ts b/packages/aws-cdk/test/integ/cli/cli.integtest.ts new file mode 100644 index 0000000000000..582b8192dfc74 --- /dev/null +++ b/packages/aws-cdk/test/integ/cli/cli.integtest.ts @@ -0,0 +1,463 @@ +import { promises as fs } from 'fs'; +import * as path from 'path'; +import { afterSeconds, cloudFormation, iam, lambda, retry, sns, sts } from './aws-helpers'; +import { cdk, cdkDeploy, cdkDestroy, cleanupOldStacks, fullStackName, INTEG_TEST_DIR, + log, prepareAppFixture, STACK_NAME_PREFIX } from './cdk-helpers'; + +jest.setTimeout(600 * 1000); + +beforeEach(async () => { + await prepareAppFixture(); + await cleanupOldStacks(); +}); + +afterEach(async () => { + await cleanupOldStacks(); +}); + +test('VPC Lookup', async () => { + log('Making sure we are clean before starting.'); + await cdkDestroy('define-vpc', { modEnv: { ENABLE_VPC_TESTING: 'DEFINE' }}); + + log('Setting up: creating a VPC with known tags'); + await cdkDeploy('define-vpc', { modEnv: { ENABLE_VPC_TESTING: 'DEFINE' }}); + log('Setup complete!'); + + log('Verifying we can now import that VPC'); + await cdkDeploy('import-vpc', { modEnv: { ENABLE_VPC_TESTING: 'IMPORT' }}); +}); + +test('Two ways of shoing the version', async () => { + const version1 = await cdk(['version']); + const version2 = await cdk(['--version']); + + expect(version1).toEqual(version2); +}); + +test('Termination protection', async () => { + await cdkDeploy('termination-protection'); + + // Try a destroy that should fail + await expect(cdkDestroy('termination-protection')).rejects.toThrow('exited with error'); + + await cloudFormation('updateTerminationProtection', { + EnableTerminationProtection: false, + StackName: fullStackName('termination-protection'), + }); +}); + +test('cdk synth', async () => { + await expect(cdk(['synth', fullStackName('test-1')])).resolves.toEqual( + `Resources: + topic69831491: + Type: AWS::SNS::Topic + Metadata: + aws:cdk:path: ${STACK_NAME_PREFIX}-test-1/topic/Resource`); + + await expect(cdk(['synth', fullStackName('test-2')])).resolves.toEqual( + `Resources: + topic152D84A37: + Type: AWS::SNS::Topic + Metadata: + aws:cdk:path: ${STACK_NAME_PREFIX}-test-2/topic1/Resource + topic2A4FB547F: + Type: AWS::SNS::Topic + Metadata: + aws:cdk:path: ${STACK_NAME_PREFIX}-test-2/topic2/Resource`); +}); + +test('ssm parameter provider error', async () => { + await expect(cdk(['synth', + fullStackName('missing-ssm-parameter'), + '-c', 'test:ssm-parameter-name=/does/not/exist', + ], { + allowErrExit: true, + })).resolves.toContain('SSM parameter not available in account'); +}); + +test('automatic ordering', async () => { + // Deploy the consuming stack which will include the producing stack + await cdkDeploy('order-consuming'); + + // Destroy the providing stack which will include the consuming stack + await cdkDestroy('order-providing'); +}); + +test('context setting', async () => { + await fs.writeFile(path.join(INTEG_TEST_DIR, 'cdk.context.json'), JSON.stringify({ + contextkey: 'this is the context value', + })); + try { + await expect(cdk(['context'])).resolves.toContain('this is the context value'); + + // Test that deleting the contextkey works + await cdk(['context', '--reset', 'contextkey']); + await expect(cdk(['context'])).resolves.not.toContain('this is the context value'); + + // Test that forced delete of the context key does not throw + await cdk(['context', '-f', '--reset', 'contextkey']); + + } finally { + await fs.unlink(path.join(INTEG_TEST_DIR, 'cdk.context.json')); + } +}); + +test('deploy', async () => { + const stackArn = await cdkDeploy('test-2', { captureStderr: false }); + + // verify the number of resources in the stack + const response = await cloudFormation('describeStackResources', { + StackName: stackArn, + }); + expect(response.StackResources?.length).toEqual(2); +}); + +test('deploy all', async () => { + const arns = await cdkDeploy('test-*', { captureStderr: false }); + + // verify that we only deployed a single stack (there's a single ARN in the output) + expect(arns.split('\n').length).toEqual(2); +}); + +test('nested stack with parameters', async () => { +// 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 cdkDeploy('with-nested-stack-using-parameters', { + options: ['--parameters', 'MyTopicParam=${STACK_NAME_PREFIX}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 cloudFormation('describeStackResources', { + StackName: stackArn, + }); + expect(response.StackResources?.length).toEqual(1); +}); + +test('deploy without execute', async () => { + const stackArn = await cdkDeploy('test-2', { + options: ['--no-execute'], + 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 cloudFormation('describeStacks', { + StackName: stackArn, + }); + + expect(response.Stacks?.[0].StackStatus).toEqual('REVIEW_IN_PROGRESS'); +}); + +test('security related changes without a CLI are expected to fail', async () => { + // 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 + await expect(cdkDeploy('iam-test', { + options: ['<', '/dev/null'], // H4x, this only works because I happen to know we pass shell: true. + })).rejects.toThrow('exited with error'); +}); + +test('deploy wildcard with outputs', async () => { + const outputsFile = path.join(INTEG_TEST_DIR, 'outputs', 'outputs.json'); + await fs.mkdir(path.dirname(outputsFile), { recursive: true }); + + await cdkDeploy(['outputs-test-*'], { + options: ['--outputs-file', outputsFile], + }); + + const outputs = JSON.parse((await fs.readFile(outputsFile, { encoding: 'utf-8' })).toString()); + expect(outputs).toEqual({ + [`${STACK_NAME_PREFIX}-outputs-test-1`]: { + TopicName: `${STACK_NAME_PREFIX}-outputs-test-1MyTopic`, + }, + [`${STACK_NAME_PREFIX}-outputs-test-2`]: { + TopicName: `${STACK_NAME_PREFIX}-outputs-test-2MyOtherTopic`, + }, + }); +}); + +test('deploy with parameters', async () => { + const stackArn = await cdkDeploy('param-test-1', { + options: [ + '--parameters', `TopicNameParam=${STACK_NAME_PREFIX}bazinga`, + ], + captureStderr: false, + }); + + const response = await cloudFormation('describeStacks', { + StackName: stackArn, + }); + + expect(response.Stacks?.[0].Parameters).toEqual([ + { + ParameterKey: 'TopicNameParam', + ParameterValue: `${STACK_NAME_PREFIX}bazinga`, + }, + ]); +}); + +test('deploy with wildcard and parameters', async () => { + await cdkDeploy('param-test-*', { + options: [ + '--parameters', `${STACK_NAME_PREFIX}-param-test-1:TopicNameParam=${STACK_NAME_PREFIX}bazinga`, + '--parameters', `${STACK_NAME_PREFIX}-param-test-2:OtherTopicNameParam=${STACK_NAME_PREFIX}ThatsMySpot`, + '--parameters', `${STACK_NAME_PREFIX}-param-test-3:DisplayNameParam=${STACK_NAME_PREFIX}HeyThere`, + '--parameters', `${STACK_NAME_PREFIX}-param-test-3:OtherDisplayNameParam=${STACK_NAME_PREFIX}AnotherOne`, + ], + }); +}); + +test('deploy with parameters multi', async () => { + const paramVal1 = `${STACK_NAME_PREFIX}bazinga`; + const paramVal2 = `${STACK_NAME_PREFIX}=jagshemash`; + + const stackArn = await cdkDeploy('param-test-3', { + options: [ + '--parameters', `DisplayNameParam=${paramVal1}`, + '--parameters', `OtherDisplayNameParam=${paramVal2}`, + ], + captureStderr: false, + }); + + const response = await cloudFormation('describeStacks', { + StackName: stackArn, + }); + + expect(response.Stacks?.[0].Parameters).toEqual([ + { + ParameterKey: 'DisplayNameParam', + ParameterValue: paramVal1, + }, + { + ParameterKey: 'OtherDisplayNameParam', + ParameterValue: paramVal2, + }, + ]); +}); + +test('deploy with notification ARN', async () => { + const topicName = `${STACK_NAME_PREFIX}-test-topic`; + + const response = await sns('createTopic', { Name: topicName }); + const topicArn = response.TopicArn!; + try { + await cdkDeploy('test-2', { + options: ['--notification-arns', topicArn], + }); + + // verify that the stack we deployed has our notification ARN + const describeResponse = await cloudFormation('describeStacks', { + StackName: fullStackName('test-2'), + }); + expect(describeResponse.Stacks?.[0].NotificationARNs).toEqual([topicArn]); + } finally { + await sns('deleteTopic', { + TopicArn: topicArn, + }); + } +}); + +test('deploy with role', async () => { + const roleName = `${STACK_NAME_PREFIX}-test-role`; + + await deleteRole(); + + const createResponse = await 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 sts('getCallerIdentity', {})).Arn }, + Effect: 'Allow', + }], + }), + }); + const roleArn = createResponse.Role.Arn; + try { + await iam('putRolePolicy', { + RoleName: roleName, + PolicyName: 'DefaultPolicy', + PolicyDocument: JSON.stringify({ + Version: '2012-10-17', + Statement: [{ + Action: '*', + Resource: '*', + Effect: 'Allow', + }], + }), + }); + + await retry('Trying to assume fresh role', afterSeconds(300), async () => { + await sts('assumeRole', { + RoleArn: roleArn, + RoleSessionName: 'testing', + }); + }); + + await cdkDeploy('test-2', { + options: ['--role-arn', roleArn], + }); + + } finally { + deleteRole(); + } + + async function deleteRole() { + try { + // tslint:disable-next-line: forin + for (const policyName of (await iam('listRolePolicies', { RoleName: roleName })).PolicyNames) { + await iam('deleteRolePolicy', { + RoleName: roleName, + PolicyName: policyName, + }); + } + await iam('deleteRole', { RoleName: roleName }); + } catch (e) { + if (e.message.indexOf('cannot be found') > -1) { return; } + throw e; + } + } +}); + +test('cdk diff', async () => { + const diff1 = await cdk(['diff', fullStackName('test-1')]); + expect(diff1).toContain('AWS::SNS::Topic'); + + const diff2 = await cdk(['diff', fullStackName('test-2')]); + expect(diff2).toContain('AWS::SNS::Topic'); + + // We can make it fail by passing --fail + await expect(cdk(['diff', '--fail', fullStackName('test-1')])) + .rejects.toThrow('exited with error'); +}); + +test('deploy stack with docker asset', async () => { + await cdkDeploy('docker'); +}); + +test('deploy and test stack with lambda asset', async () => { + const stackArn = await cdkDeploy('lambda', { captureStderr: false }); + + const response = await 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 lambda('invoke', { + FunctionName: lambdaArn, + }); + + expect(JSON.stringify(output.Payload)).toContain('dear asset'); +}); + +test('cdk ls', async () => { + const listing = await 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(fullStackName(stack)); + } +}); + +test('deploy stack without resource', async () => { + // Deploy the stack without resources + await cdkDeploy('conditional-resource', { modEnv: { NO_RESOURCE: 'TRUE' }}); + + // This should have succeeded but not deployed the stack. + await expect(cloudFormation('describeStacks', { StackName: fullStackName('conditional-resource') })) + .rejects.toThrow('conditional-resource does not exist'); + + // Deploy the stack with resources + await cdkDeploy('conditional-resource'); + + // Then again WITHOUT resources (this should destroy the stack) + await cdkDeploy('conditional-resource', { modEnv: { NO_RESOURCE: 'TRUE' } }); + + await expect(cloudFormation('describeStacks', { StackName: fullStackName('conditional-resource') })) + .rejects.toThrow('conditional-resource does not exist'); +}); + +test('IAM diff', async () => { + const output = await cdk(['diff', fullStackName('iam-test')]); + + // Roughly check for a table like this: + // + // ┌───┬─────────────────┬────────┬────────────────┬────────────────────────────┬───────────┐ + // │ │ Resource │ Effect │ Action │ Principal │ Condition │ + // ├───┼─────────────────┼────────┼────────────────┼────────────────────────────┼───────────┤ + // │ + │ ${SomeRole.Arn} │ Allow │ sts:AssumeRole │ Service:ec2.amazon.aws.com │ │ + // └───┴─────────────────┴────────┴────────────────┴────────────────────────────┴───────────┘ + + expect(output).toContain('${SomeRole.Arn}'); + expect(output).toContain('sts:AssumeRole'); + expect(output).toContain('ec2.amazon.aws.com'); +}); + +test('fast deploy', async () => { + // 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 cdkDeploy('with-nested-stack', { captureStderr: false }); + const changeSet1 = await getLatestChangeSet(); + + // Deploy the same stack again, there should be no new change set created + await 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 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 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 cloudFormation('describeStacks', { StackName: stackArn }); + if (!response.Stacks?.[0]) { throw new Error('Did not get a ChangeSet at all'); } + log(`Found Change Set ${response.Stacks?.[0].ChangeSetId}`); + return response.Stacks?.[0]; + } +}); + +test('failed deploy does not hang', async () => { + // this will hang if we introduce https://github.com/aws/aws-cdk/issues/6403 again. + await expect(cdkDeploy('failed')).rejects.toThrow('exited with error'); +}); diff --git a/packages/aws-cdk/test/integ/cli/test-cdk-bootstrap-no-execute.sh b/packages/aws-cdk/test/integ/cli/test-cdk-bootstrap-no-execute.sh deleted file mode 100755 index 9434c38e461d8..0000000000000 --- a/packages/aws-cdk/test/integ/cli/test-cdk-bootstrap-no-execute.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash -set -euo pipefail -scriptdir=$(cd $(dirname $0) && pwd) -source ${scriptdir}/common.bash -# ---------------------------------------------------------- - -setup - -bootstrap_stack_name="toolkit-stack-1-${RANDOM}" - -# deploy with --no-execute (leaves stack in review) -cdk bootstrap --toolkit-stack-name ${bootstrap_stack_name} --no-execute - -response_json=$(mktemp).json -aws cloudformation describe-stacks --stack-name ${bootstrap_stack_name} > ${response_json} - -stack_status=$(node -e "console.log(require('${response_json}').Stacks[0].StackStatus)") -if [ ! "${stack_status}" == "REVIEW_IN_PROGRESS" ]; then - fail "Expected stack to be in status REVIEW_IN_PROGRESS but got ${stack_status}" -fi - -# destroy -aws cloudformation delete-stack --stack-name ${bootstrap_stack_name} - -echo "✅ success" diff --git a/packages/aws-cdk/test/integ/cli/test-cdk-context.sh b/packages/aws-cdk/test/integ/cli/test-cdk-context.sh deleted file mode 100755 index 4670bf409ff10..0000000000000 --- a/packages/aws-cdk/test/integ/cli/test-cdk-context.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash -set -euo pipefail -scriptdir=$(cd $(dirname $0) && pwd) -source ${scriptdir}/common.bash -# ---------------------------------------------------------- - -cat > cdk.context.json <&1 | grep "this is the context value" > /dev/null - -# Test that deleting the contextkey works -cdk context --reset contextkey -cdk context 2>&1 | grep "this is the context value" > /dev/null && { echo "Should not contain key"; exit 1; } || true - -# Test that forced delete of the context key does not error -cdk context -f --reset contextkey - -rm -f cdk.context.json - -echo "✅ success" diff --git a/packages/aws-cdk/test/integ/cli/test-cdk-deploy-all.sh b/packages/aws-cdk/test/integ/cli/test-cdk-deploy-all.sh deleted file mode 100755 index 9fe3109d26d96..0000000000000 --- a/packages/aws-cdk/test/integ/cli/test-cdk-deploy-all.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash -set -euo pipefail -scriptdir=$(cd $(dirname $0) && pwd) -source ${scriptdir}/common.bash -# ---------------------------------------------------------- - -setup - -stack_arns=$(cdk deploy ${STACK_NAME_PREFIX}-test-\*) -echo "Stack deployed successfully" - -# verify that we only deployed a single stack (there's a single ARN in the output) -lines="$(echo "${stack_arns}" | wc -l)" -if [ "${lines}" -ne 2 ]; then - echo "-- output -----------" - echo "${stack_arns}" - echo "---------------------" - fail "cdk deploy returned ${lines} arns and we expected 2" -fi - -cdk destroy -f ${STACK_NAME_PREFIX}-test-\* - -echo "✅ success" diff --git a/packages/aws-cdk/test/integ/cli/test-cdk-deploy-nested-stack-with-parameters.sh b/packages/aws-cdk/test/integ/cli/test-cdk-deploy-nested-stack-with-parameters.sh deleted file mode 100755 index b2389899ee71b..0000000000000 --- a/packages/aws-cdk/test/integ/cli/test-cdk-deploy-nested-stack-with-parameters.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash -set -euo pipefail -scriptdir=$(cd $(dirname $0) && pwd) -source ${scriptdir}/common.bash -# ---------------------------------------------------------- - -setup - -# 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. -stack_arn=$(cdk deploy -v ${STACK_NAME_PREFIX}-with-nested-stack-using-parameters --parameters "MyTopicParam=${STACK_NAME_PREFIX}ThereIsNoSpoon") -echo "Stack deployed successfully" - -# verify that we only deployed a single stack (there's a single ARN in the output) -assert_lines "${stack_arn}" 1 - -# verify the number of resources in the stack -response_json=$(mktemp).json -aws cloudformation describe-stack-resources --stack-name ${stack_arn} > ${response_json} -resource_count=$(node -e "console.log(require('${response_json}').StackResources.length)") -if [ "${resource_count}" -ne 1 ]; then - fail "stack has ${resource_count} resources, and we expected two" -fi - -# destroy -cdk destroy -f ${STACK_NAME_PREFIX}-with-nested-stack-using-parameters - -echo "✅ success" diff --git a/packages/aws-cdk/test/integ/cli/test-cdk-deploy-no-execute.sh b/packages/aws-cdk/test/integ/cli/test-cdk-deploy-no-execute.sh deleted file mode 100755 index f5c4620c2aa1c..0000000000000 --- a/packages/aws-cdk/test/integ/cli/test-cdk-deploy-no-execute.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash -set -euo pipefail -scriptdir=$(cd $(dirname $0) && pwd) -source ${scriptdir}/common.bash -# ---------------------------------------------------------- - -setup - -stack_arn=$(cdk deploy -v ${STACK_NAME_PREFIX}-test-2 --no-execute) -echo "Stack deployed successfully" - -# verify that we only deployed a single stack (there's a single ARN in the output) -assert_lines "${stack_arn}" 1 - -# verify the number of resources in the stack -response_json=$(mktemp).json -aws cloudformation describe-stacks --stack-name ${stack_arn} > ${response_json} - -stack_status=$(node -e "console.log(require('${response_json}').Stacks[0].StackStatus)") -if [ ! "${stack_status}" == "REVIEW_IN_PROGRESS" ]; then - fail "Expected stack to be in status REVIEW_IN_PROGRESS but got ${stack_status}" -fi - -# destroy -cdk destroy -f ${STACK_NAME_PREFIX}-test-2 - -echo "✅ success" diff --git a/packages/aws-cdk/test/integ/cli/test-cdk-deploy-no-tty.sh b/packages/aws-cdk/test/integ/cli/test-cdk-deploy-no-tty.sh deleted file mode 100755 index 0ddad00be3b57..0000000000000 --- a/packages/aws-cdk/test/integ/cli/test-cdk-deploy-no-tty.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash -set -euo pipefail -scriptdir=$(cd $(dirname $0) && pwd) -source ${scriptdir}/common.bash -# ---------------------------------------------------------- - -setup - -# 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 -if cdk deploy ${STACK_NAME_PREFIX}-iam-test < /dev/null; then - fail "test failed. we expect 'cdk deploy' to fail if there are security-related changes and no tty" -fi - -echo "✅ success" diff --git a/packages/aws-cdk/test/integ/cli/test-cdk-deploy-wildcard-with-outputs.sh b/packages/aws-cdk/test/integ/cli/test-cdk-deploy-wildcard-with-outputs.sh deleted file mode 100755 index b77a489955e97..0000000000000 --- a/packages/aws-cdk/test/integ/cli/test-cdk-deploy-wildcard-with-outputs.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash -set -euo pipefail -scriptdir=$(cd $(dirname $0) && pwd) -source ${scriptdir}/common.bash -# ---------------------------------------------------------- - -setup - -# create directory in /tmp -outputs_folder=${integ_test_dir}/outputs -mkdir -p ${outputs_folder} - -# set up outputs -outputs_file=${integ_test_dir}/outputs/outputs.json -expected_file=${integ_test_dir}/outputs/expected.json -touch $expected_file - -# add prefixes as stacks are keyed on their stack name in the outputs file -expected_outputs=${scriptdir}/cdk-deploy-wildcard-with-outputs-expected.template -sed "s|%STACK_NAME_PREFIX%|$STACK_NAME_PREFIX|g" "$expected_outputs" > "$expected_file" - -# deploy all outputs stacks -cdk deploy ${STACK_NAME_PREFIX}-outputs-test-\* --outputs-file ${outputs_file} -echo "Stacks deployed successfully" - -# verify generated outputs file -generated_outputs_file="$(cat ${outputs_file})" -expected_outputs_file="$(cat ${expected_file})" -if [[ "${generated_outputs_file}" != "${expected_outputs_file}" ]]; then - fail "unexpected outputs. Expected: ${expected_outputs_file} Actual: ${generated_outputs_file}" -fi - -# destroy -rm -rf ${outputs_folder} -cdk destroy -f ${STACK_NAME_PREFIX}-outputs-test-1 -cdk destroy -f ${STACK_NAME_PREFIX}-outputs-test-2 - -echo "✅ success" diff --git a/packages/aws-cdk/test/integ/cli/test-cdk-deploy-wildcard-with-parameters.sh b/packages/aws-cdk/test/integ/cli/test-cdk-deploy-wildcard-with-parameters.sh deleted file mode 100755 index fcfaac90b870e..0000000000000 --- a/packages/aws-cdk/test/integ/cli/test-cdk-deploy-wildcard-with-parameters.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash -set -euo pipefail -scriptdir=$(cd $(dirname $0) && pwd) -source ${scriptdir}/common.bash -# ---------------------------------------------------------- - -setup - -# STACK_NAME_PREFIX is used in OtherTopicNameParam to allow multiple instances -# of this test to run in parallel, othewise they will attempt to create the same SNS topic. -stack_arns=$(cdk deploy ${STACK_NAME_PREFIX}-param-test-\* --parameters "${STACK_NAME_PREFIX}-param-test-1:TopicNameParam=${STACK_NAME_PREFIX}bazinga" --parameters "${STACK_NAME_PREFIX}-param-test-2:OtherTopicNameParam=${STACK_NAME_PREFIX}ThatsMySpot" --parameters "${STACK_NAME_PREFIX}-param-test-3:DisplayNameParam=${STACK_NAME_PREFIX}HeyThere" --parameters "${STACK_NAME_PREFIX}-param-test-3:OtherDisplayNameParam=${STACK_NAME_PREFIX}AnotherOne") -echo "Stack deployed successfully" - -# verify that we only deployed a single stack (there's a single ARN in the output) -lines="$(echo "${stack_arns}" | wc -l)" -if [ "${lines}" -ne 3 ]; then - echo "-- output -----------" - echo "${stack_arns}" - echo "---------------------" - fail "cdk deploy returned ${lines} arns and we expected 2" -fi - -cdk destroy -f ${STACK_NAME_PREFIX}-param-test-\* - -echo "✅ success" diff --git a/packages/aws-cdk/test/integ/cli/test-cdk-deploy-with-notification-arns.sh b/packages/aws-cdk/test/integ/cli/test-cdk-deploy-with-notification-arns.sh deleted file mode 100755 index 8e9be952ef2dc..0000000000000 --- a/packages/aws-cdk/test/integ/cli/test-cdk-deploy-with-notification-arns.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash -set -euo pipefail -scriptdir=$(cd $(dirname $0) && pwd) -source ${scriptdir}/common.bash -# ---------------------------------------------------------- -sns_topic_name=${STACK_NAME_PREFIX}-test-topic - -response_json=$(mktemp).json -aws sns create-topic --name ${sns_topic_name} > ${response_json} -sns_arn=$(node -e "console.log(require('${response_json}').TopicArn)") - -setup - -stack_arn=$(cdk deploy ${STACK_NAME_PREFIX}-test-2 --notification-arns ${sns_arn}) -echo "Stack deployed successfully" - -# verify that the stack we deployed has our notification ARN -aws cloudformation describe-stacks --stack-name ${stack_arn} > ${response_json} - -notification_count=$(node -e "console.log(require('${response_json}').Stacks[0].NotificationARNs.length)") -if [[ "${notification_count}" -ne 1 ]]; then - fail "stack has ${notification_count} SNS notification ARNs, and we expected one" -fi - -notification_arn=$(node -e "console.log(require('${response_json}').Stacks[0].NotificationARNs[0])") -if [[ "${notification_arn}" != "${sns_arn}" ]]; then - fail "stack has ${notification_arn} SNS notification ARN, and we expected ${sns_arn}" -fi - -# destroy stack and delete SNS topic -cdk destroy -f ${STACK_NAME_PREFIX}-test-2 -aws sns delete-topic --topic-arn ${sns_arn} - -echo "✅ success" diff --git a/packages/aws-cdk/test/integ/cli/test-cdk-deploy-with-outputs.sh b/packages/aws-cdk/test/integ/cli/test-cdk-deploy-with-outputs.sh deleted file mode 100755 index cd135f06c28f3..0000000000000 --- a/packages/aws-cdk/test/integ/cli/test-cdk-deploy-with-outputs.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash -set -euo pipefail -scriptdir=$(cd $(dirname $0) && pwd) -source ${scriptdir}/common.bash -# ---------------------------------------------------------- - -setup - -# create directory in /tmp -outputs_folder=${integ_test_dir}/outputs -mkdir -p ${outputs_folder} - -# set up outputs -outputs_file=${outputs_folder}/outputs.json -expected_file=${outputs_folder}/expected.json -touch $expected_file - -# add prefixes as stacks are keyed on their stack name in the outputs file -expected_outputs=${scriptdir}/cdk-deploy-with-outputs-expected.template -sed "s|%STACK_NAME_PREFIX%|$STACK_NAME_PREFIX|g" "$expected_outputs" > "$expected_file" - -cdk deploy ${STACK_NAME_PREFIX}-outputs-test-1 --outputs-file ${outputs_file} -echo "Stack deployed successfully" - -# verify generated outputs file -generated_outputs_file="$(cat ${outputs_file})" -expected_outputs_file="$(cat ${expected_file})" -if [[ "${generated_outputs_file}" != "${expected_outputs_file}" ]]; then - fail "unexpected outputs. Expected: ${expected_outputs_file} Actual: ${generated_outputs_file}" -fi - -# destroy -rm -rf ${outputs_folder} -cdk destroy -f ${STACK_NAME_PREFIX}-outputs-test-1 - -echo "✅ success" diff --git a/packages/aws-cdk/test/integ/cli/test-cdk-deploy-with-parameters-multi.sh b/packages/aws-cdk/test/integ/cli/test-cdk-deploy-with-parameters-multi.sh deleted file mode 100755 index 456cf825641ec..0000000000000 --- a/packages/aws-cdk/test/integ/cli/test-cdk-deploy-with-parameters-multi.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/bash -set -euo pipefail -scriptdir=$(cd $(dirname $0) && pwd) -source ${scriptdir}/common.bash -# ---------------------------------------------------------- - -setup - -paramVal1="${STACK_NAME_PREFIX}bazinga" -paramVal2="${STACK_NAME_PREFIX}=jagshemash" - -stack_arn=$(cdk deploy -v ${STACK_NAME_PREFIX}-param-test-3 --parameters "DisplayNameParam=${paramVal1}" --parameters "OtherDisplayNameParam=${paramVal2}") -echo "Stack deployed successfully" - -# verify that we only deployed a single stack (there's a single ARN in the output) -assert_lines "${stack_arn}" 1 - -# retrieve stack parameters -response_json=$(mktemp).json -aws cloudformation describe-stacks --stack-name ${stack_arn} > ${response_json} -parameter_count=$(node -e "console.log(require('${response_json}').Stacks[0].Parameters.length)") - -# verify stack parameter count -if [ "${parameter_count}" -ne 2 ]; then - fail "stack has ${parameter_count} parameters, and we expected two" -fi - -# verify stack parameters -for (( i=0; i<$parameter_count; i++ )); do - passedParameterVal=$(node -e "console.log(require('${response_json}').Stacks[0].Parameters[$i].ParameterValue)") - if ! [[ "${passedParameterVal}" =~ ^($paramVal1|$paramVal2)$ ]]; then - fail "Unexpected parameter: '${passedParameterVal}'. Expected parameter values: '${paramVal1}' or '${paramVal2}'" - fi -done; - -# destroy -cdk destroy -f ${STACK_NAME_PREFIX}-param-test-3 - - -echo "✅ success" diff --git a/packages/aws-cdk/test/integ/cli/test-cdk-deploy-with-parameters.sh b/packages/aws-cdk/test/integ/cli/test-cdk-deploy-with-parameters.sh deleted file mode 100755 index a0abe0ddaee9a..0000000000000 --- a/packages/aws-cdk/test/integ/cli/test-cdk-deploy-with-parameters.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash -set -euo pipefail -scriptdir=$(cd $(dirname $0) && pwd) -source ${scriptdir}/common.bash -# ---------------------------------------------------------- - -setup - -# STACK_NAME_PREFIX is used in TopicNameParam to allow multiple instances -# of this test to run in parallel, othewise they will attempt to create the same SNS topic. -stack_arn=$(cdk deploy -v ${STACK_NAME_PREFIX}-param-test-1 --parameters "TopicNameParam=${STACK_NAME_PREFIX}bazinga") -echo "Stack deployed successfully" - -# verify that we only deployed a single stack (there's a single ARN in the output) -assert_lines "${stack_arn}" 1 - -# verify the number of resources in the stack -response_json=$(mktemp).json -aws cloudformation describe-stack-resources --stack-name ${stack_arn} > ${response_json} -resource_count=$(node -e "console.log(require('${response_json}').StackResources.length)") -if [ "${resource_count}" -ne 1 ]; then - fail "stack has ${resource_count} resources, and we expected one" -fi - -# destroy -cdk destroy -f ${STACK_NAME_PREFIX}-param-test-1 - -echo "✅ success" diff --git a/packages/aws-cdk/test/integ/cli/test-cdk-deploy-with-role.sh b/packages/aws-cdk/test/integ/cli/test-cdk-deploy-with-role.sh deleted file mode 100755 index 892a4239aaf1a..0000000000000 --- a/packages/aws-cdk/test/integ/cli/test-cdk-deploy-with-role.sh +++ /dev/null @@ -1,79 +0,0 @@ -#!/bin/bash -set -euo pipefail -scriptdir=$(cd $(dirname $0) && pwd) -source ${scriptdir}/common.bash -# ---------------------------------------------------------- - -role_name=${STACK_NAME_PREFIX}-test-role -delete_role() { - for policy_name in $(aws iam list-role-policies --role-name $role_name --output text --query PolicyNames); do - aws iam delete-role-policy --role-name $role_name --policy-name $policy_name - done - aws iam delete-role --role-name $role_name -} - -delete_role || echo 'Role does not exist yet' -current_identity=$(aws sts get-caller-identity | grep Arn | sed 's/.*: //g' | sed 's/"//g') -role_arn=$(aws iam create-role \ - --output text --query Role.Arn \ - --role-name $role_name \ - --assume-role-policy-document file://<(echo "{ - \"Version\": \"2012-10-17\", - \"Statement\": [{ - \"Action\": \"sts:AssumeRole\", - \"Principal\": { \"Service\": \"cloudformation.amazonaws.com\" }, - \"Effect\": \"Allow\" - }, { - \"Action\": \"sts:AssumeRole\", - \"Principal\": { \"AWS\": \"$current_identity\"}, - \"Effect\": \"Allow\" - }] - }")) -trap delete_role EXIT -aws iam put-role-policy \ - --role-name $role_name \ - --policy-name DefaultPolicy \ - --policy-document file://<(echo '{ - "Version": "2012-10-17", - "Statement": [{ - "Action": "*", - "Resource": "*", - "Effect": "Allow" - }] - }') - -# 5 minutes -attempts=60 -for i in $(seq 1 $attempts); do - if aws sts assume-role --role-arn ${role_arn} --role-session-name testing >/dev/null; then - echo "Successfully assumed newly created role" - break; - elif [ $i -lt $attempts ]; then - echo "Sleeping 5 seconds to improve chances of the role having propagated" - sleep 5 - else - echo "Failed to assume role after $attempts attempts, exiting." - exit 1 - fi -done - -setup - -stack_arn=$(cdk --role-arn $role_arn deploy ${STACK_NAME_PREFIX}-test-2) -echo "Stack deployed successfully" - -# verify that we only deployed a single stack (there's a single ARN in the output) -assert_lines "${stack_arn}" 1 - -# verify the number of resources in the stack -response_json=$(mktemp).json -aws cloudformation describe-stack-resources --stack-name ${stack_arn} > ${response_json} -resource_count=$(node -e "console.log(require('${response_json}').StackResources.length)") -if [ "${resource_count}" -ne 2 ]; then - fail "stack has ${resource_count} resources, and we expected two" -fi - -# destroy -cdk destroy --role-arn $role_arn -f ${STACK_NAME_PREFIX}-test-2 - -echo "✅ success" diff --git a/packages/aws-cdk/test/integ/cli/test-cdk-deploy.sh b/packages/aws-cdk/test/integ/cli/test-cdk-deploy.sh deleted file mode 100755 index 4f056691309c6..0000000000000 --- a/packages/aws-cdk/test/integ/cli/test-cdk-deploy.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash -set -euo pipefail -scriptdir=$(cd $(dirname $0) && pwd) -source ${scriptdir}/common.bash -# ---------------------------------------------------------- - -setup - -stack_arn=$(cdk deploy -v ${STACK_NAME_PREFIX}-test-2) -echo "Stack deployed successfully" - -# verify that we only deployed a single stack (there's a single ARN in the output) -assert_lines "${stack_arn}" 1 - -# verify the number of resources in the stack -response_json=$(mktemp).json -aws cloudformation describe-stack-resources --stack-name ${stack_arn} > ${response_json} -resource_count=$(node -e "console.log(require('${response_json}').StackResources.length)") -if [ "${resource_count}" -ne 2 ]; then - fail "stack has ${resource_count} resources, and we expected two" -fi - -# destroy -cdk destroy -f ${STACK_NAME_PREFIX}-test-2 - -echo "✅ success" diff --git a/packages/aws-cdk/test/integ/cli/test-cdk-diff.sh b/packages/aws-cdk/test/integ/cli/test-cdk-diff.sh deleted file mode 100755 index 7b489ec70ce71..0000000000000 --- a/packages/aws-cdk/test/integ/cli/test-cdk-diff.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash -set -euo pipefail -scriptdir=$(cd $(dirname $0) && pwd) -source ${scriptdir}/common.bash -# ---------------------------------------------------------- - -setup - -cdk diff ${STACK_NAME_PREFIX}-test-1 2>&1 | grep "AWS::SNS::Topic" -cdk diff ${STACK_NAME_PREFIX}-test-2 2>&1 | grep "AWS::SNS::Topic" - -failed=0 -cdk diff --fail ${STACK_NAME_PREFIX}-test-1 2>&1 || failed=1 - -if [ $failed -ne 1 ]; then - fail 'cdk diff with --fail does not fail' -fi - -echo "✅ success" diff --git a/packages/aws-cdk/test/integ/cli/test-cdk-docker.sh b/packages/aws-cdk/test/integ/cli/test-cdk-docker.sh deleted file mode 100755 index b0df2bfc2abb1..0000000000000 --- a/packages/aws-cdk/test/integ/cli/test-cdk-docker.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash -set -euo pipefail -scriptdir=$(cd $(dirname $0) && pwd) -source ${scriptdir}/common.bash -# ---------------------------------------------------------- - -setup - -stack="${STACK_NAME_PREFIX}-docker" - -stack_arn=$(cdk deploy -v ${stack} --require-approval=never) -echo "Stack deployed successfully" - -# # destroy -cdk destroy -f ${stack} - -echo "✅ success" diff --git a/packages/aws-cdk/test/integ/cli/test-cdk-failed-deploy-doesnt-hang.sh b/packages/aws-cdk/test/integ/cli/test-cdk-failed-deploy-doesnt-hang.sh deleted file mode 100755 index eb93a371b5a51..0000000000000 --- a/packages/aws-cdk/test/integ/cli/test-cdk-failed-deploy-doesnt-hang.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash -set -euo pipefail -scriptdir=$(cd $(dirname $0) && pwd) -source ${scriptdir}/common.bash -# ---------------------------------------------------------- - -setup - -set +e -# this will hang if we introduce https://github.com/aws/aws-cdk/issues/6403 again. -cdk deploy -v ${STACK_NAME_PREFIX}-failed -deploy_status=$? -set -e - -# destroy -cdk destroy -f ${STACK_NAME_PREFIX}-failed - -if [ "${deploy_status}" -eq 0 ]; then - fail "stack deployment should have failed" -fi - -echo "✅ success" diff --git a/packages/aws-cdk/test/integ/cli/test-cdk-fast-deploy.sh b/packages/aws-cdk/test/integ/cli/test-cdk-fast-deploy.sh deleted file mode 100755 index 6649588d56949..0000000000000 --- a/packages/aws-cdk/test/integ/cli/test-cdk-fast-deploy.sh +++ /dev/null @@ -1,61 +0,0 @@ -#!/bin/bash -set -euo pipefail -scriptdir=$(cd $(dirname $0) && pwd) -source ${scriptdir}/common.bash -# ---------------------------------------------------------- -# this test verifies that deployment is skipped when the template did not -# change, and that `--force` can be used to override this behavior. - -setup - -# 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. -stack_name="${STACK_NAME_PREFIX}-with-nested-stack" - -get_last_changeset() { - aws cloudformation describe-stacks --stack-name ${stack_name} --query 'Stacks[0].ChangeSetId' -} - -# deploy once -echo "============================================================" -echo " deploying stack" -echo "============================================================" -cdk deploy -v ${stack_name} -changeset1=$(get_last_changeset) -echo "changeset1=${changeset1}" - -echo "============================================================" -echo " deploying the same stack again (no change)" -echo "============================================================" -cdk deploy -v ${stack_name} -changeset2=$(get_last_changeset) -if [ "${changeset2}" != "${changeset1}" ]; then - echo "TEST FAILED: expected the 'cdk deploy' will skip deployment because the app did not change" - exit 1 -fi - -echo "============================================================" -echo " deploying the same stack again (no change, --force)" -echo "============================================================" -cdk deploy --force -v ${stack_name} -changeset3=$(get_last_changeset) -if [ "${changeset3}" == "${changeset1}" ]; then - echo "TEST FAILED: expected --force to create a new changeset" - exit 1 -fi - -echo "============================================================" -echo " deploying the same stack again with different tags" -echo "============================================================" -cdk deploy -v ${stack_name} --tags key=value -changeset4=$(get_last_changeset) -if [ "${changeset4}" == "${changeset1}" ]; then - echo "TEST FAILED: expected tags to create a new changeset" - exit 1 -fi - -# destroy -cdk destroy -f ${stack_name} - -echo "✅ success" diff --git a/packages/aws-cdk/test/integ/cli/test-cdk-iam-diff.sh b/packages/aws-cdk/test/integ/cli/test-cdk-iam-diff.sh deleted file mode 100755 index 1b3749a8b170d..0000000000000 --- a/packages/aws-cdk/test/integ/cli/test-cdk-iam-diff.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash -set -euo pipefail -scriptdir=$(cd $(dirname $0) && pwd) -source ${scriptdir}/common.bash -# ---------------------------------------------------------- - -setup - -function nonfailing_diff() { - cdk diff $1 2>&1 | strip_color_codes -} - -assert "nonfailing_diff ${STACK_NAME_PREFIX}-iam-test" < /dev/null 2>&1 || deployed=0 - -if [ $deployed -ne 0 ]; then - fail 'Stack has been deployed' -fi - -# Deploy the stack with resources -cdk deploy ${STACK_NAME_PREFIX}-conditional-resource - -# Now, deploy the stack without resources -NO_RESOURCE="TRUE" cdk deploy ${STACK_NAME_PREFIX}-conditional-resource - -# Verify that the stack has been destroyed -destroyed=0 -aws cloudformation describe-stacks --stack-name ${STACK_NAME_PREFIX}-conditional-resource > /dev/null 2>&1 || destroyed=1 - -if [ $destroyed -ne 1 ]; then - fail 'Stack has not been destroyed' -fi - diff --git a/packages/aws-cdk/test/integ/cli/test-cdk-order.sh b/packages/aws-cdk/test/integ/cli/test-cdk-order.sh deleted file mode 100755 index deb82ca41fea9..0000000000000 --- a/packages/aws-cdk/test/integ/cli/test-cdk-order.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash -set -euo pipefail -scriptdir=$(cd $(dirname $0) && pwd) -source ${scriptdir}/common.bash -# ---------------------------------------------------------- - -setup - -# Deploy the consuming stack which will include the producing stack -cdk deploy ${STACK_NAME_PREFIX}-order-consuming - -# Destroy the providing stack which will include the consuming stack -cdk destroy -f ${STACK_NAME_PREFIX}-order-providing - -echo "✅ success" diff --git a/packages/aws-cdk/test/integ/cli/test-cdk-ssm-parameter-provider-error.sh b/packages/aws-cdk/test/integ/cli/test-cdk-ssm-parameter-provider-error.sh deleted file mode 100755 index 224e930658e0a..0000000000000 --- a/packages/aws-cdk/test/integ/cli/test-cdk-ssm-parameter-provider-error.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash -set -euo pipefail -scriptdir=$(cd $(dirname $0) && pwd) -source ${scriptdir}/common.bash -# ---------------------------------------------------------- - -setup - -parameterName=/does/not/exist - -function cdk_synth() { - (cdk synth $@ 2>&1 || true) | strip_color_codes -} - -assert "cdk_synth ${STACK_NAME_PREFIX}-missing-ssm-parameter -c test:ssm-parameter-name=${parameterName}" <&1 || destroyed=0 - -if [ $destroyed -eq 1 ]; then - fail 'cdk destroy succeeded on a stack with termination protection enabled' -fi - -# disable termination protection and destroy stack -aws cloudformation update-termination-protection --no-enable-termination-protection --stack-name ${stack} -cdk destroy -f ${stack} - -echo "✅ success" diff --git a/packages/aws-cdk/test/integ/cli/test-cdk-version.sh b/packages/aws-cdk/test/integ/cli/test-cdk-version.sh deleted file mode 100755 index 3784e4a523fe0..0000000000000 --- a/packages/aws-cdk/test/integ/cli/test-cdk-version.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash -set -euo pipefail -scriptdir=$(cd $(dirname $0) && pwd) -source ${scriptdir}/common.bash -# ---------------------------------------------------------- - -setup - -cdk version > v1.txt -cdk --version > v2.txt - -assert_diff "version" v1.txt v2.txt - -echo "✅ success" diff --git a/packages/aws-cdk/test/integ/cli/test-jest.sh b/packages/aws-cdk/test/integ/cli/test-jest.sh new file mode 100755 index 0000000000000..c4873d28c7d3e --- /dev/null +++ b/packages/aws-cdk/test/integ/cli/test-jest.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# A number of tests have been written in TS/Jest, instead of bash. +# This script runs them. + +set -euo pipefail +scriptdir=$(cd $(dirname $0) && pwd) + +cd $scriptdir + +# Install these dependencies that the tests (written in Jest) need. +# Only if we're not running from the repo, because if we are the +# dependencies have already been installed by the containing 'aws-cdk' package's +# package.json. +if ! npx --no-install jest --version; then + echo 'Looks like we need to install jest first. Hold on.' >& 2 + npm install --prefix . jest aws-sdk +fi + +npx jest --runInBand --testPathPattern 'test/integ/cli' --testMatch '**/*.integtest.js' --verbose "$@" diff --git a/packages/aws-cdk/test/integ/cli/test-vpc-lookup.sh b/packages/aws-cdk/test/integ/cli/test-vpc-lookup.sh deleted file mode 100755 index afc3e7157dbad..0000000000000 --- a/packages/aws-cdk/test/integ/cli/test-vpc-lookup.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash -set -euo pipefail -scriptdir=$(cd $(dirname $0) && pwd) -source ${scriptdir}/common.bash -# ---------------------------------------------------------- - -setup - -echo "Making sure we are clean before starting." -ENABLE_VPC_TESTING="DEFINE" cdk destroy -f ${STACK_NAME_PREFIX}-define-vpc - -echo "Setting up: creating a VPC with known tags" -ENABLE_VPC_TESTING="DEFINE" cdk deploy ${STACK_NAME_PREFIX}-define-vpc -echo "Setup complete!" - -# verify we can synth the importing stack now -echo "Verifying we can now import that VPC" -ENABLE_VPC_TESTING="IMPORT" cdk synth -v ${STACK_NAME_PREFIX}-import-vpc - -# destroy -echo "Cleaning up..." -ENABLE_VPC_TESTING="DEFINE" cdk destroy -f ${STACK_NAME_PREFIX}-define-vpc - -echo "✅ success" \ No newline at end of file