Skip to content

Commit 5511076

Browse files
Elad Ben-IsraelRomainMuller
Elad Ben-Israel
authored andcommitted
fix(toolkit): multi-stack apps cannot be synthesized or deployed (#911)
Due to a recent cx protocol change (#868), some toolkit commands stopped respecting the "selected" stacks (the ones specified in the command line). "cdk synth" would always return the first stack, and "cdk deploy" would always deploy all stacks. Since we have test coverage gaps in the toolkit (#294), we did not discover this before we released. This change includes an initial set of integration tests for the toolkit. At the moment they should be manually executed when toolkit changes are made, but we will execute them in a pipeline. Fixes #910
1 parent c23da91 commit 5511076

12 files changed

+253
-28
lines changed

packages/aws-cdk/bin/cdk.ts

+24-28
Original file line numberDiff line numberDiff line change
@@ -270,8 +270,8 @@ async function initCommandLine() {
270270
if (environmentGlobs.length === 0) {
271271
environmentGlobs = [ '**' ]; // default to ALL
272272
}
273-
const stackInfos = await selectStacks();
274-
const availableEnvironments = distinct(stackInfos.map(stack => stack.environment)
273+
const stacks = await selectStacks();
274+
const availableEnvironments = distinct(stacks.map(stack => stack.environment)
275275
.filter(env => env !== undefined) as cxapi.Environment[]);
276276
const environments = availableEnvironments.filter(env => environmentGlobs.find(glob => minimatch(env!.name, glob)));
277277
if (environments.length === 0) {
@@ -324,31 +324,28 @@ async function initCommandLine() {
324324
doInteractive: boolean,
325325
outputDir: string|undefined,
326326
json: boolean): Promise<void> {
327-
const stackIds = await selectStacks(...stackNames);
328-
renames.validateSelectedStacks(stackIds);
327+
const stacks = await selectStacks(...stackNames);
328+
renames.validateSelectedStacks(stacks);
329329

330330
if (doInteractive) {
331-
if (stackIds.length !== 1) {
332-
throw new Error(`When using interactive synthesis, must select exactly one stack. Got: ${listStackNames(stackIds)}`);
331+
if (stacks.length !== 1) {
332+
throw new Error(`When using interactive synthesis, must select exactly one stack. Got: ${listStackNames(stacks)}`);
333333
}
334-
return await interactive(stackIds[0], argv.verbose, (stack) => synthesizeStack(stack));
334+
return await interactive(stacks[0], argv.verbose, (stack) => synthesizeStack(stack));
335335
}
336336

337-
if (stackIds.length > 1 && outputDir == null) {
337+
if (stacks.length > 1 && outputDir == null) {
338338
// tslint:disable-next-line:max-line-length
339-
throw new Error(`Multiple stacks selected (${listStackNames(stackIds)}), but output is directed to stdout. Either select one stack, or use --output to send templates to a directory.`);
339+
throw new Error(`Multiple stacks selected (${listStackNames(stacks)}), but output is directed to stdout. Either select one stack, or use --output to send templates to a directory.`);
340340
}
341341

342-
const response = await synthesizeStacks();
343-
const synthesizedStacks = response.stacks;
344-
345342
if (outputDir == null) {
346-
return synthesizedStacks[0].template; // Will be printed in main()
343+
return stacks[0].template; // Will be printed in main()
347344
}
348345

349346
fs.mkdirpSync(outputDir);
350347

351-
for (const stack of synthesizedStacks) {
348+
for (const stack of stacks) {
352349
const finalName = renames.finalName(stack.name);
353350
const fileName = `${outputDir}/${finalName}.template.${json ? 'json' : 'yaml'}`;
354351
highlight(fileName);
@@ -579,13 +576,11 @@ async function initCommandLine() {
579576
}
580577

581578
async function cliDeploy(stackNames: string[], toolkitStackName: string) {
582-
const stackIds = await selectStacks(...stackNames);
583-
renames.validateSelectedStacks(stackIds);
579+
const stacks = await selectStacks(...stackNames);
580+
renames.validateSelectedStacks(stacks);
584581

585-
const response = await synthesizeStacks();
586-
587-
for (const stack of response.stacks) {
588-
if (stackIds.length !== 1) { highlight(stack.name); }
582+
for (const stack of stacks) {
583+
if (stacks.length !== 1) { highlight(stack.name); }
589584
if (!stack.environment) {
590585
// tslint:disable-next-line:max-line-length
591586
throw new Error(`Stack ${stack.name} does not define an environment, and AWS credentials could not be obtained from standard locations or no region was configured.`);
@@ -603,6 +598,7 @@ async function initCommandLine() {
603598
const result = await deployStack(stack, aws, toolkitInfo, deployName);
604599
const message = result.noOp ? ` ✅ Stack was already up-to-date, it has ARN ${colors.blue(result.stackArn)}`
605600
: ` ✅ Deployment of stack %s completed successfully, it has ARN ${colors.blue(result.stackArn)}`;
601+
data(result.stackArn);
606602
success(message, colors.blue(stack.name));
607603
for (const name of Object.keys(result.outputs)) {
608604
const value = result.outputs[name];
@@ -616,18 +612,18 @@ async function initCommandLine() {
616612
}
617613

618614
async function cliDestroy(stackNames: string[], force: boolean) {
619-
const stackIds = await selectStacks(...stackNames);
620-
renames.validateSelectedStacks(stackIds);
615+
const stacks = await selectStacks(...stackNames);
616+
renames.validateSelectedStacks(stacks);
621617

622618
if (!force) {
623619
// tslint:disable-next-line:max-line-length
624-
const confirmed = await util.promisify(promptly.confirm)(`Are you sure you want to delete: ${colors.blue(stackIds.map(s => s.name).join(', '))} (y/n)?`);
620+
const confirmed = await util.promisify(promptly.confirm)(`Are you sure you want to delete: ${colors.blue(stacks.map(s => s.name).join(', '))} (y/n)?`);
625621
if (!confirmed) {
626622
return;
627623
}
628624
}
629625

630-
for (const stack of stackIds) {
626+
for (const stack of stacks) {
631627
const deployName = renames.finalName(stack.name);
632628

633629
success(' ⏳ Starting destruction of stack %s...', colors.blue(deployName));
@@ -687,14 +683,14 @@ async function initCommandLine() {
687683
* Match a single stack from the list of available stacks
688684
*/
689685
async function findStack(name: string): Promise<string> {
690-
const stackIds = await selectStacks(name);
686+
const stacks = await selectStacks(name);
691687

692688
// Could have been a glob so check that we evaluated to exactly one
693-
if (stackIds.length > 1) {
694-
throw new Error(`This command requires exactly one stack and we matched more than one: ${stackIds.map(x => x.name)}`);
689+
if (stacks.length > 1) {
690+
throw new Error(`This command requires exactly one stack and we matched more than one: ${stacks.map(x => x.name)}`);
695691
}
696692

697-
return stackIds[0].name;
693+
return stacks[0].name;
698694
}
699695

700696
function logDefaults() {
+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# CDK toolkit integreation tests
2+
3+
To run, just execute `./test.sh`. The test uses the default AWS credentials.
4+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
!*.js
+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
const cdk = require('@aws-cdk/cdk');
2+
const sns = require('@aws-cdk/aws-sns');
3+
4+
class MyStack extends cdk.Stack {
5+
constructor(parent, id) {
6+
super(parent, id);
7+
new sns.Topic(this, 'topic');
8+
}
9+
}
10+
11+
class YourStack extends cdk.Stack {
12+
constructor(parent, id) {
13+
super(parent, id);
14+
new sns.Topic(this, 'topic1');
15+
new sns.Topic(this, 'topic2');
16+
}
17+
}
18+
19+
const app = new cdk.App();
20+
21+
new MyStack(app, 'cdk-toolkit-integration-test-1');
22+
new YourStack(app, 'cdk-toolkit-integration-test-2');
23+
24+
app.run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"app": "node app.js",
3+
"versionReporting": false
4+
}
+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
function cleanup_stack() {
2+
local stack_arn=$1
3+
echo "| ensuring ${stack_arn} is cleaned up"
4+
if aws cloudformation describe-stacks --stack-name ${stack_arn} 2> /dev/null; then
5+
aws cloudformation delete-stack --stack-name ${stack_arn}
6+
fi
7+
}
8+
9+
function cleanup() {
10+
cleanup_stack cdk-toolkit-integration-test-1
11+
cleanup_stack cdk-toolkit-integration-test-2
12+
}
13+
14+
function setup() {
15+
cleanup
16+
cd app
17+
18+
npm i --no-save @aws-cdk/cdk @aws-cdk/aws-sns
19+
}
20+
21+
function fail() {
22+
echo "$@"
23+
exit 1
24+
}
25+
26+
function assert_diff() {
27+
local test=$1
28+
local actual=$2
29+
local expected=$3
30+
31+
diff ${actual} ${expected} || {
32+
echo
33+
echo "+-----------"
34+
echo "| expected:"
35+
cat ${expected}
36+
echo "|--"
37+
echo
38+
echo "+-----------"
39+
echo "| actual:"
40+
cat ${actual}
41+
echo "|--"
42+
echo
43+
fail "assertion failed. ${test}"
44+
}
45+
}
46+
47+
function assert() {
48+
local command="$1"
49+
50+
local expected=$(mktemp)
51+
local actual=$(mktemp)
52+
53+
echo "| running ${command}"
54+
55+
$command > ${actual} || {
56+
fail "command ${command} non-zero exit code"
57+
}
58+
59+
cat > ${expected}
60+
61+
assert_diff "command: ${command}" "${actual}" "${expected}"
62+
}
63+
64+
function assert_lines() {
65+
local data="$1"
66+
local expected="$2"
67+
echo "| assert that last command returned ${expected} line(s)"
68+
69+
local lines="$(echo "${data}" | wc -l)"
70+
if [ "${lines}" -ne "${expected}" ]; then
71+
fail "response has ${lines} lines and we expected ${expected} lines to be returned"
72+
fi
73+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
scriptdir=$(cd $(dirname $0) && pwd)
4+
source ${scriptdir}/common.bash
5+
# ----------------------------------------------------------
6+
7+
setup
8+
9+
stack_arns=$(cdk deploy)
10+
echo "Stack deployed successfully"
11+
12+
# verify that we only deployed a single stack (there's a single ARN in the output)
13+
lines="$(echo "${stack_arns}" | wc -l)"
14+
if [ "${lines}" -ne 2 ]; then
15+
fail "cdk deploy returned ${lines} arns and we expected 2"
16+
fi
17+
18+
cdk destroy -f cdk-toolkit-integration-test-1
19+
cdk destroy -f cdk-toolkit-integration-test-2
20+
21+
echo "✅ success"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
scriptdir=$(cd $(dirname $0) && pwd)
4+
source ${scriptdir}/common.bash
5+
# ----------------------------------------------------------
6+
7+
setup
8+
9+
stack_arn=$(cdk deploy cdk-toolkit-integration-test-2)
10+
echo "Stack deployed successfully"
11+
12+
# verify that we only deployed a single stack (there's a single ARN in the output)
13+
assert_lines "${stack_arn}" 1
14+
15+
# verify the number of resources in the stack
16+
response_json=$(mktemp).json
17+
aws cloudformation describe-stack-resources --stack-name ${stack_arn} > ${response_json}
18+
resource_count=$(node -e "console.log(require('${response_json}').StackResources.length)")
19+
if [ "${resource_count}" -ne 2 ]; then
20+
fail "stack has ${resource_count} resources, and we expected two"
21+
fi
22+
23+
# destroy
24+
cdk destroy -f cdk-toolkit-integration-test-2
25+
26+
echo "✅ success"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
scriptdir=$(cd $(dirname $0) && pwd)
4+
source ${scriptdir}/common.bash
5+
# ----------------------------------------------------------
6+
7+
setup
8+
9+
function cdk_diff() {
10+
cdk diff $1 2>&1 || true
11+
}
12+
13+
assert_lines "$(cdk_diff cdk-toolkit-integration-test-1)" 1
14+
assert_lines "$(cdk_diff cdk-toolkit-integration-test-2)" 2
15+
16+
echo "✅ success"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
scriptdir=$(cd $(dirname $0) && pwd)
4+
source ${scriptdir}/common.bash
5+
# ----------------------------------------------------------
6+
7+
setup
8+
9+
assert "cdk ls" <<HERE
10+
cdk-toolkit-integration-test-1
11+
cdk-toolkit-integration-test-2
12+
HERE
13+
14+
echo "✅ success"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
scriptdir=$(cd $(dirname $0) && pwd)
4+
source ${scriptdir}/common.bash
5+
# ----------------------------------------------------------
6+
7+
setup
8+
9+
assert "cdk synth cdk-toolkit-integration-test-1" <<HERE
10+
Resources:
11+
topic69831491:
12+
Type: 'AWS::SNS::Topic'
13+
14+
HERE
15+
16+
assert "cdk synth cdk-toolkit-integration-test-2" <<HERE
17+
Resources:
18+
topic152D84A37:
19+
Type: 'AWS::SNS::Topic'
20+
topic2A4FB547F:
21+
Type: 'AWS::SNS::Topic'
22+
23+
HERE
24+
25+
echo "✅ success"

packages/aws-cdk/integ-tests/test.sh

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
scriptdir=$(cd $(dirname $0) && pwd)
4+
5+
toolkit_bin="${scriptdir}/../bin"
6+
7+
if [ ! -x ${toolkit_bin}/cdk ]; then
8+
echo "Unable to find 'cdk' under ${toolkit_bin}"
9+
exit 1
10+
fi
11+
12+
# make sure "this" toolkit is in the path
13+
export PATH=${toolkit_bin}:$PATH
14+
15+
cd ${scriptdir}
16+
for test in test-*.sh; do
17+
echo "============================================================================================"
18+
echo "${test}"
19+
echo "============================================================================================"
20+
/bin/bash ${test}
21+
done

0 commit comments

Comments
 (0)