Skip to content

Commit 4cb9755

Browse files
Simon-Pierre GingrasRomainMuller
Simon-Pierre Gingras
authored andcommitted
fix(cli): correctly handle tags when deploying multiple stacks (#3455)
Fixes a bug where stacks being declared with different tags end up with identical tags when deployed to CloudFormation, due to the first tagged stack's tags overriding the value of `options.tags`. This change makes it so that the `options.tags` value is not overridden, and the actual tag list to be used is determined in the same way for each stack that will be deployed. Fixes #3471
1 parent 1280071 commit 4cb9755

File tree

2 files changed

+131
-3
lines changed

2 files changed

+131
-3
lines changed

Diff for: packages/aws-cdk/lib/cdk-toolkit.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,9 @@ export class CdkToolkit {
112112
print('%s: deploying...', colors.bold(stack.name));
113113
}
114114

115-
if (!options.tags || options.tags.length === 0) {
116-
options.tags = this.appStacks.getTagsFromStackMetadata(stack);
115+
let tags = options.tags;
116+
if (!tags || tags.length === 0) {
117+
tags = this.appStacks.getTagsFromStackMetadata(stack);
117118
}
118119

119120
try {
@@ -124,7 +125,7 @@ export class CdkToolkit {
124125
ci: options.ci,
125126
toolkitStackName: options.toolkitStackName,
126127
reuseAssets: options.reuseAssets,
127-
tags: options.tags
128+
tags
128129
});
129130

130131
const message = result.noOp

Diff for: packages/aws-cdk/test/test.cdk-toolkit.ts

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import cxapi = require('@aws-cdk/cx-api');
2+
import nodeunit = require('nodeunit');
3+
import { AppStacks, Tag } from '../lib/api/cxapp/stacks';
4+
import { DeployStackResult } from '../lib/api/deploy-stack';
5+
import { DeployStackOptions, IDeploymentTarget, Template } from '../lib/api/deployment-target';
6+
import { CdkToolkit } from '../lib/cdk-toolkit';
7+
8+
export = nodeunit.testCase({
9+
deploy: {
10+
'makes correct CloudFormation calls': {
11+
'without options'(test: nodeunit.Test) {
12+
// GIVEN
13+
const toolkit = new CdkToolkit({
14+
appStacks: new TestAppStacks(test),
15+
provisioner: new TestProvisioner(test, {
16+
'Test-Stack-A': { Foo: 'Bar' },
17+
'Test-Stack-B': { Baz: 'Zinga!' },
18+
}),
19+
});
20+
21+
// WHEN
22+
toolkit.deploy({ stackNames: ['Test-Stack-A', 'Test-Stack-B'] });
23+
24+
// THEN
25+
test.done();
26+
},
27+
},
28+
},
29+
});
30+
31+
class MockStack {
32+
constructor(
33+
public readonly name: string,
34+
public readonly originalName: string = name,
35+
public readonly template: any = { Resources: { TempalteName: name } },
36+
public readonly templateFile: string = `fake/stack/${name}.json`,
37+
public readonly assets: cxapi.AssetMetadataEntry[] = [],
38+
public readonly parameters: { [id: string]: string } = {},
39+
public readonly environment: cxapi.Environment = { name: 'MockEnv', account: '123456789012', region: 'bermuda-triangle-1' },
40+
) {}
41+
}
42+
43+
class TestAppStacks extends AppStacks {
44+
public static readonly MOCK_STACK_A = new MockStack('Test-Stack-A');
45+
public static readonly MOCK_STACK_B = new MockStack('Test-Stack-B');
46+
47+
constructor(private readonly test: nodeunit.Test) {
48+
super(undefined as any);
49+
}
50+
51+
public getTagsFromStackMetadata(stack: cxapi.CloudFormationStackArtifact): Tag[] {
52+
switch (stack.name) {
53+
case TestAppStacks.MOCK_STACK_A.name:
54+
return [{ Key: 'Foo', Value: 'Bar' }];
55+
case TestAppStacks.MOCK_STACK_B.name:
56+
return [{ Key: 'Baz', Value: 'Zinga!' }];
57+
default:
58+
throw new Error(`Not an expected mock stack: ${stack.name}`);
59+
}
60+
}
61+
62+
public selectStacks(selectors: string[]): Promise<cxapi.CloudFormationStackArtifact[]> {
63+
this.test.deepEqual(selectors, ['Test-Stack-A', 'Test-Stack-B']);
64+
return Promise.resolve([
65+
// Cheating the type system here (intentionally, so we have to stub less!)
66+
TestAppStacks.MOCK_STACK_A as cxapi.CloudFormationStackArtifact,
67+
TestAppStacks.MOCK_STACK_B as cxapi.CloudFormationStackArtifact,
68+
]);
69+
}
70+
71+
public processMetadata(stacks: cxapi.CloudFormationStackArtifact[]): void {
72+
stacks.forEach(stack =>
73+
this.test.ok(stack === TestAppStacks.MOCK_STACK_A || stack === TestAppStacks.MOCK_STACK_B,
74+
`Not an expected mock stack: ${stack.name}`));
75+
}
76+
77+
public listStacks(): never {
78+
throw new Error('Not Implemented');
79+
}
80+
81+
public synthesizeStack(): never {
82+
throw new Error('Not Implemented');
83+
}
84+
85+
public synthesizeStacks(): never {
86+
throw new Error('Not Implemented');
87+
}
88+
}
89+
90+
class TestProvisioner implements IDeploymentTarget {
91+
private readonly expectedTags: { [sytackName: string]: Tag[] } = {};
92+
93+
constructor(
94+
private readonly test: nodeunit.Test,
95+
expectedTags: { [sytackName: string]: { [kay: string]: string } } = {},
96+
) {
97+
for (const [stackName, tags] of Object.entries(expectedTags)) {
98+
this.expectedTags[stackName] =
99+
Object.entries(tags).map(([Key, Value]) => ({ Key, Value }))
100+
.sort((l, r) => l.Key.localeCompare(r.Key));
101+
}
102+
}
103+
104+
public deployStack(options: DeployStackOptions): Promise<DeployStackResult> {
105+
this.test.ok(
106+
options.stack.name === TestAppStacks.MOCK_STACK_A.name || options.stack.name === TestAppStacks.MOCK_STACK_B.name,
107+
`Not an expected mock stack: ${options.stack.name}`
108+
);
109+
this.test.deepEqual(options.tags, this.expectedTags[options.stack.name]);
110+
return Promise.resolve({
111+
stackArn: `arn:aws:cloudformation:::stack/${options.stack.name}/MockedOut`,
112+
noOp: false,
113+
outputs: { StackName: options.stack.name },
114+
});
115+
}
116+
117+
public readCurrentTemplate(stack: cxapi.CloudFormationStackArtifact): Promise<Template> {
118+
switch (stack.name) {
119+
case TestAppStacks.MOCK_STACK_A.name:
120+
return Promise.resolve({});
121+
case TestAppStacks.MOCK_STACK_B.name:
122+
return Promise.resolve({});
123+
default:
124+
return Promise.reject(`Not an expected mock stack: ${stack.name}`);
125+
}
126+
}
127+
}

0 commit comments

Comments
 (0)