Skip to content

Commit 4df9a4f

Browse files
authored
fix(cfn-include): allow CFN Functions in Tags (#19923)
Our `TagManger` does not allow expressing Tags that included top-level CloudFormation functions, like `Fn::If`. While that's not a big issue when writing CDK code directly, it makes it impossible to include some CloudFormation templates that use that pattern. Introduce the concept of "dynamic tags" to the `TagManager` that allows preserving these, and return them alongside the "regular" Tags when rendering. Fixes #16889 ---- ### All Submissions: * [x] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/master/CONTRIBUTING.md) ### Adding new Unconventional Dependencies: * [ ] This PR adds new unconventional dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/master/CONTRIBUTING.md/#adding-new-unconventional-dependencies) ### New Features * [ ] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/master/INTEGRATION_TESTS.md)? * [ ] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)? *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent a58a803 commit 4df9a4f

File tree

3 files changed

+112
-42
lines changed

3 files changed

+112
-42
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"Conditions": {
3+
"ValcacheServerEnabled": true
4+
},
5+
"Resources": {
6+
"TxAutoScalingGroup": {
7+
"Type": "AWS::AutoScaling::AutoScalingGroup",
8+
"Properties": {
9+
"MinSize": "1",
10+
"MaxSize": "3",
11+
"Tags": [
12+
{
13+
"Fn::If": [
14+
"ValcacheServerEnabled",
15+
{
16+
"Key": "datomic:cache-group",
17+
"Value": "SystemName",
18+
"PropagateAtLaunch": true
19+
},
20+
{
21+
"Ref": "AWS::NoValue"
22+
}
23+
]
24+
}
25+
]
26+
}
27+
}
28+
}
29+
}

packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts

+8
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,14 @@ describe('CDK Include', () => {
228228
);
229229
});
230230

231+
test('can ingest a template using Fn::If in Tags, and output it unchanged', () => {
232+
includeTestTemplate(stack, 'if-in-tags.json');
233+
234+
Template.fromStack(stack).templateMatches(
235+
loadTestFileToJsObject('if-in-tags.json'),
236+
);
237+
});
238+
231239
test('can ingest a UserData script, and output it unchanged', () => {
232240
includeTestTemplate(stack, 'user-data.json');
233241

packages/@aws-cdk/core/lib/tag-manager.ts

+75-42
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,24 @@ interface StackTag {
2424
Key: string;
2525
Value: string;
2626
}
27+
28+
/**
29+
* The results of parsing Tags.
30+
*/
31+
interface ParseTagsResult {
32+
/**
33+
* The "simple" (meaning, not including complex CloudFormation functions)
34+
* tags that were found.
35+
*/
36+
readonly tags: Tag[];
37+
38+
/**
39+
* The collection of "dynamic" (meaning, including complex CloudFormation functions)
40+
* tags that were found.
41+
*/
42+
readonly dynamicTags: any;
43+
}
44+
2745
/**
2846
* Interface for converter between CloudFormation and internal tag representations
2947
*/
@@ -38,31 +56,33 @@ interface ITagFormatter {
3856
*
3957
* Use the given priority.
4058
*/
41-
parseTags(cfnPropertyTags: any, priority: number): Tag[];
59+
parseTags(cfnPropertyTags: any, priority: number): ParseTagsResult;
4260
}
4361

4462
/**
4563
* Standard tags are a list of { key, value } objects
4664
*/
4765
class StandardFormatter implements ITagFormatter {
48-
public parseTags(cfnPropertyTags: any, priority: number): Tag[] {
66+
public parseTags(cfnPropertyTags: any, priority: number): ParseTagsResult {
4967
if (!Array.isArray(cfnPropertyTags)) {
5068
throw new Error(`Invalid tag input expected array of {key, value} have ${JSON.stringify(cfnPropertyTags)}`);
5169
}
5270

5371
const tags: Tag[] = [];
72+
const dynamicTags: any = [];
5473
for (const tag of cfnPropertyTags) {
5574
if (tag.key === undefined || tag.value === undefined) {
56-
throw new Error(`Invalid tag input expected {key, value} have ${JSON.stringify(tag)}`);
75+
dynamicTags.push(tag);
76+
} else {
77+
// using interp to ensure Token is now string
78+
tags.push({
79+
key: `${tag.key}`,
80+
value: `${tag.value}`,
81+
priority,
82+
});
5783
}
58-
// using interp to ensure Token is now string
59-
tags.push({
60-
key: `${tag.key}`,
61-
value: `${tag.value}`,
62-
priority,
63-
});
6484
}
65-
return tags;
85+
return { tags, dynamicTags };
6686
}
6787

6888
public formatTags(tags: Tag[]): any {
@@ -73,36 +93,38 @@ class StandardFormatter implements ITagFormatter {
7393
value: tag.value,
7494
});
7595
}
76-
return cfnTags.length === 0 ? undefined : cfnTags;
96+
return cfnTags;
7797
}
7898
}
7999

80100
/**
81101
* ASG tags are a list of { key, value, propagateAtLaunch } objects
82102
*/
83103
class AsgFormatter implements ITagFormatter {
84-
public parseTags(cfnPropertyTags: any, priority: number): Tag[] {
85-
const tags: Tag[] = [];
104+
public parseTags(cfnPropertyTags: any, priority: number): ParseTagsResult {
86105
if (!Array.isArray(cfnPropertyTags)) {
87106
throw new Error(`Invalid tag input expected array of {key, value, propagateAtLaunch} have ${JSON.stringify(cfnPropertyTags)}`);
88107
}
89108

109+
const tags: Tag[] = [];
110+
const dynamicTags: any = [];
90111
for (const tag of cfnPropertyTags) {
91112
if (tag.key === undefined ||
92-
tag.value === undefined ||
93-
tag.propagateAtLaunch === undefined) {
94-
throw new Error(`Invalid tag input expected {key, value, propagateAtLaunch} have ${JSON.stringify(tag)}`);
113+
tag.value === undefined ||
114+
tag.propagateAtLaunch === undefined) {
115+
dynamicTags.push(tag);
116+
} else {
117+
// using interp to ensure Token is now string
118+
tags.push({
119+
key: `${tag.key}`,
120+
value: `${tag.value}`,
121+
priority,
122+
applyToLaunchedInstances: !!tag.propagateAtLaunch,
123+
});
95124
}
96-
// using interp to ensure Token is now string
97-
tags.push({
98-
key: `${tag.key}`,
99-
value: `${tag.value}`,
100-
priority,
101-
applyToLaunchedInstances: !!tag.propagateAtLaunch,
102-
});
103125
}
104126

105-
return tags;
127+
return { tags, dynamicTags };
106128
}
107129

108130
public formatTags(tags: Tag[]): any {
@@ -114,20 +136,20 @@ class AsgFormatter implements ITagFormatter {
114136
propagateAtLaunch: tag.applyToLaunchedInstances !== false,
115137
});
116138
}
117-
return cfnTags.length === 0 ? undefined : cfnTags;
139+
return cfnTags;
118140
}
119141
}
120142

121143
/**
122144
* Some CloudFormation constructs use a { key: value } map for tags
123145
*/
124146
class MapFormatter implements ITagFormatter {
125-
public parseTags(cfnPropertyTags: any, priority: number): Tag[] {
126-
const tags: Tag[] = [];
147+
public parseTags(cfnPropertyTags: any, priority: number): ParseTagsResult {
127148
if (Array.isArray(cfnPropertyTags) || typeof(cfnPropertyTags) !== 'object') {
128149
throw new Error(`Invalid tag input expected map of {key: value} have ${JSON.stringify(cfnPropertyTags)}`);
129150
}
130151

152+
const tags: Tag[] = [];
131153
for (const [key, value] of Object.entries(cfnPropertyTags)) {
132154
tags.push({
133155
key,
@@ -136,23 +158,23 @@ class MapFormatter implements ITagFormatter {
136158
});
137159
}
138160

139-
return tags;
161+
return { tags, dynamicTags: undefined };
140162
}
141163

142164
public formatTags(tags: Tag[]): any {
143-
const cfnTags: {[key: string]: string} = {};
165+
const cfnTags: { [key: string]: string } = {};
144166
for (const tag of tags) {
145167
cfnTags[`${tag.key}`] = `${tag.value}`;
146168
}
147-
return Object.keys(cfnTags).length === 0 ? undefined : cfnTags;
169+
return cfnTags;
148170
}
149171
}
150172

151173
/**
152174
* StackTags are of the format { Key: key, Value: value }
153175
*/
154176
class KeyValueFormatter implements ITagFormatter {
155-
public parseTags(keyValueTags: any, priority: number): Tag[] {
177+
public parseTags(keyValueTags: any, priority: number): ParseTagsResult {
156178
const tags: Tag[] = [];
157179
for (const key in keyValueTags) {
158180
if (keyValueTags.hasOwnProperty(key)) {
@@ -164,8 +186,9 @@ class KeyValueFormatter implements ITagFormatter {
164186
});
165187
}
166188
}
167-
return tags;
189+
return { tags, dynamicTags: undefined };
168190
}
191+
169192
public formatTags(unformattedTags: Tag[]): any {
170193
const tags: StackTag[] = [];
171194
unformattedTags.forEach(tag => {
@@ -174,20 +197,20 @@ class KeyValueFormatter implements ITagFormatter {
174197
Value: tag.value,
175198
});
176199
});
177-
return tags.length > 0 ? tags : undefined;
200+
return tags;
178201
}
179202
}
180203

181204
class NoFormat implements ITagFormatter {
182-
public parseTags(_cfnPropertyTags: any): Tag[] {
183-
return [];
205+
public parseTags(_cfnPropertyTags: any): ParseTagsResult {
206+
return { tags: [], dynamicTags: undefined };
184207
}
208+
185209
public formatTags(_tags: Tag[]): any {
186210
return undefined;
187211
}
188212
}
189213

190-
191214
let _tagFormattersCache: {[key: string]: ITagFormatter} | undefined;
192215

193216
/**
@@ -203,7 +226,7 @@ function TAG_FORMATTERS(): {[key: string]: ITagFormatter} {
203226
[TagType.KEY_VALUE]: new KeyValueFormatter(),
204227
[TagType.NOT_TAGGABLE]: new NoFormat(),
205228
});
206-
};
229+
}
207230

208231
/**
209232
* Interface to implement tags.
@@ -258,7 +281,6 @@ export interface TagManagerOptions {
258281
*
259282
*/
260283
export class TagManager {
261-
262284
/**
263285
* Check whether the given construct is Taggable
264286
*/
@@ -283,6 +305,7 @@ export class TagManager {
283305
public readonly renderedTags: IResolvable;
284306

285307
private readonly tags = new Map<string, Tag>();
308+
private readonly dynamicTags: any;
286309
private readonly priorities = new Map<string, number>();
287310
private readonly tagFormatter: ITagFormatter;
288311
private readonly resourceTypeName: string;
@@ -292,7 +315,9 @@ export class TagManager {
292315
this.resourceTypeName = resourceTypeName;
293316
this.tagFormatter = TAG_FORMATTERS()[tagType];
294317
if (tagStructure !== undefined) {
295-
this._setTag(...this.tagFormatter.parseTags(tagStructure, this.initialTagPriority));
318+
const parseTagsResult = this.tagFormatter.parseTags(tagStructure, this.initialTagPriority);
319+
this.dynamicTags = parseTagsResult.dynamicTags;
320+
this._setTag(...parseTagsResult.tags);
296321
}
297322
this.tagPropertyName = options.tagPropertyName || 'tags';
298323

@@ -331,7 +356,14 @@ export class TagManager {
331356
* tags at synthesis time.
332357
*/
333358
public renderTags(): any {
334-
return this.tagFormatter.formatTags(this.sortedTags);
359+
const formattedTags = this.tagFormatter.formatTags(this.sortedTags);
360+
if (Array.isArray(formattedTags) || Array.isArray(this.dynamicTags)) {
361+
const ret = [...formattedTags ?? [], ...this.dynamicTags ?? []];
362+
return ret.length > 0 ? ret : undefined;
363+
} else {
364+
const ret = { ...formattedTags ?? {}, ...this.dynamicTags ?? {} };
365+
return Object.keys(ret).length > 0 ? ret : undefined;
366+
}
335367
}
336368

337369
/**
@@ -378,7 +410,8 @@ export class TagManager {
378410
}
379411
}
380412

381-
private get sortedTags() {
382-
return Array.from(this.tags.values()).sort((a, b) => a.key.localeCompare(b.key));
413+
private get sortedTags(): Tag[] {
414+
return Array.from(this.tags.values())
415+
.sort((a, b) => a.key.localeCompare(b.key));
383416
}
384417
}

0 commit comments

Comments
 (0)