Skip to content

Commit bfb14b6

Browse files
moofish32rix0rrr
authored andcommitted
fix(cdk): merge cloudFormation tags with aspect tags (#1762)
This modifies the behavior of TagManager to enable the merging of tags provided during Cfn* properties with tag aspects. If a collision occurs the aspects tag precedence. Fixes #1725
1 parent bda12f2 commit bfb14b6

File tree

7 files changed

+417
-173
lines changed

7 files changed

+417
-173
lines changed

packages/@aws-cdk/cdk/README.md

+9-1
Original file line numberDiff line numberDiff line change
@@ -117,11 +117,19 @@ has a few features that are covered later to explain how this works.
117117

118118
### API
119119

120-
In order to enable additional controls a Tags can specifically include or
120+
In order to enable additional controls a Tag can specifically include or
121121
exclude a CloudFormation Resource Type, propagate tags for an autoscaling group,
122122
and use priority to override the default precedence. See the `TagProps`
123123
interface for more details.
124124

125+
Tags can be configured by using the properties for the AWS CloudFormation layer
126+
resources or by using the tag aspects described here. The aspects will always
127+
take precedence over the AWS CloudFormation layer in the event of a name
128+
collision. The tags will be merged otherwise. For the aspect based tags, the
129+
tags applied closest to the resource will take precedence, given an equal
130+
priority. A higher priority tag will always take precedence over a lower
131+
priority tag.
132+
125133
#### applyToLaunchedInstances
126134

127135
This property is a boolean that defaults to `true`. When `true` and the aspect

packages/@aws-cdk/cdk/lib/aspects/tag-aspect.ts

+66-7
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,58 @@
11
import { ITaggable, Resource } from '../cloudformation/resource';
22
import { IConstruct } from '../core/construct';
3-
import { TagProps } from '../core/tag-manager';
43
import { IAspect } from './aspect';
54

5+
/**
6+
* Properties for a tag
7+
*/
8+
export interface TagProps {
9+
/**
10+
* Whether the tag should be applied to instances in an AutoScalingGroup
11+
*
12+
* @default true
13+
*/
14+
applyToLaunchedInstances?: boolean;
15+
16+
/**
17+
* An array of Resource Types that will not receive this tag
18+
*
19+
* An empty array will allow this tag to be applied to all resources. A
20+
* non-empty array will apply this tag only if the Resource type is not in
21+
* this array.
22+
* @default []
23+
*/
24+
excludeResourceTypes?: string[];
25+
26+
/**
27+
* An array of Resource Types that will receive this tag
28+
*
29+
* An empty array will match any Resource. A non-empty array will apply this
30+
* tag only to Resource types that are included in this array.
31+
* @default []
32+
*/
33+
includeResourceTypes?: string[];
34+
35+
/**
36+
* Priority of the tag operation
37+
*
38+
* Higher or equal priority tags will take precedence.
39+
*
40+
* Setting priority will enable the user to control tags when they need to not
41+
* follow the default precedence pattern of last applied and closest to the
42+
* construct in the tree.
43+
*
44+
* @default
45+
*
46+
* Default priorities:
47+
*
48+
* - 100 for {@link SetTag}
49+
* - 200 for {@link RemoveTag}
50+
* - 50 for tags added directly to CloudFormation resources
51+
*
52+
*/
53+
priority?: number;
54+
}
55+
656
/**
757
* The common functionality for Tag and Remove Tag Aspects
858
*/
@@ -43,18 +93,25 @@ export class Tag extends TagBase {
4393
*/
4494
public readonly value: string;
4595

96+
private readonly defaultPriority = 100;
97+
4698
constructor(key: string, value: string, props: TagProps = {}) {
4799
super(key, props);
48-
this.props.applyToLaunchedInstances = props.applyToLaunchedInstances !== false;
49-
this.props.priority = props.priority === undefined ? 0 : props.priority;
50100
if (value === undefined) {
51101
throw new Error('Tag must have a value');
52102
}
53103
this.value = value;
54104
}
55105

56106
protected applyTag(resource: ITaggable) {
57-
resource.tags.setTag(this.key, this.value!, this.props);
107+
if (resource.tags.applyTagAspectHere(this.props.includeResourceTypes, this.props.excludeResourceTypes)) {
108+
resource.tags.setTag(
109+
this.key,
110+
this.value,
111+
this.props.priority !== undefined ? this.props.priority : this.defaultPriority,
112+
this.props.applyToLaunchedInstances !== false
113+
);
114+
}
58115
}
59116
}
60117

@@ -63,13 +120,15 @@ export class Tag extends TagBase {
63120
*/
64121
export class RemoveTag extends TagBase {
65122

123+
private readonly defaultPriority = 200;
124+
66125
constructor(key: string, props: TagProps = {}) {
67126
super(key, props);
68-
this.props.priority = props.priority === undefined ? 1 : props.priority;
69127
}
70128

71129
protected applyTag(resource: ITaggable): void {
72-
resource.tags.removeTag(this.key, this.props);
73-
return;
130+
if (resource.tags.applyTagAspectHere(this.props.includeResourceTypes, this.props.excludeResourceTypes)) {
131+
resource.tags.removeTag(this.key, this.props.priority !== undefined ? this.props.priority : this.defaultPriority);
132+
}
74133
}
75134
}

packages/@aws-cdk/cdk/lib/cloudformation/resource.ts

+30-28
Original file line numberDiff line numberDiff line change
@@ -206,12 +206,13 @@ export class Resource extends Referenceable {
206206
*/
207207
public toCloudFormation(): object {
208208
try {
209-
if (Resource.isTaggable(this)) {
210-
const tags = this.tags.renderTags();
211-
this.properties.tags = tags === undefined ? this.properties.tags : tags;
212-
}
213209
// merge property overrides onto properties and then render (and validate).
214-
const properties = this.renderProperties(deepMerge(this.properties || { }, this.untypedPropertyOverrides));
210+
const tags = Resource.isTaggable(this) ? this.tags.renderTags() : undefined;
211+
const properties = this.renderProperties(deepMerge(
212+
this.properties || {},
213+
{ tags },
214+
this.untypedPropertyOverrides
215+
));
215216

216217
return {
217218
Resources: {
@@ -254,7 +255,6 @@ export class Resource extends Referenceable {
254255
protected renderProperties(properties: any): { [key: string]: any } {
255256
return properties;
256257
}
257-
258258
}
259259

260260
export enum TagType {
@@ -312,33 +312,35 @@ export interface ResourceOptions {
312312
* Merges `source` into `target`, overriding any existing values.
313313
* `null`s will cause a value to be deleted.
314314
*/
315-
export function deepMerge(target: any, source: any) {
316-
if (typeof(source) !== 'object' || typeof(target) !== 'object') {
317-
throw new Error(`Invalid usage. Both source (${JSON.stringify(source)}) and target (${JSON.stringify(target)}) must be objects`);
318-
}
315+
export function deepMerge(target: any, ...sources: any[]) {
316+
for (const source of sources) {
317+
if (typeof(source) !== 'object' || typeof(target) !== 'object') {
318+
throw new Error(`Invalid usage. Both source (${JSON.stringify(source)}) and target (${JSON.stringify(target)}) must be objects`);
319+
}
319320

320-
for (const key of Object.keys(source)) {
321-
const value = source[key];
322-
if (typeof(value) === 'object' && value != null && !Array.isArray(value)) {
323-
// if the value at the target is not an object, override it with an
324-
// object so we can continue the recursion
325-
if (typeof(target[key]) !== 'object') {
326-
target[key] = { };
327-
}
321+
for (const key of Object.keys(source)) {
322+
const value = source[key];
323+
if (typeof(value) === 'object' && value != null && !Array.isArray(value)) {
324+
// if the value at the target is not an object, override it with an
325+
// object so we can continue the recursion
326+
if (typeof(target[key]) !== 'object') {
327+
target[key] = { };
328+
}
328329

329-
deepMerge(target[key], value);
330+
deepMerge(target[key], value);
330331

331-
// if the result of the merge is an empty object, it's because the
332-
// eventual value we assigned is `undefined`, and there are no
333-
// sibling concrete values alongside, so we can delete this tree.
334-
const output = target[key];
335-
if (typeof(output) === 'object' && Object.keys(output).length === 0) {
332+
// if the result of the merge is an empty object, it's because the
333+
// eventual value we assigned is `undefined`, and there are no
334+
// sibling concrete values alongside, so we can delete this tree.
335+
const output = target[key];
336+
if (typeof(output) === 'object' && Object.keys(output).length === 0) {
337+
delete target[key];
338+
}
339+
} else if (value === undefined) {
336340
delete target[key];
341+
} else {
342+
target[key] = value;
337343
}
338-
} else if (value === undefined) {
339-
delete target[key];
340-
} else {
341-
target[key] = value;
342344
}
343345
}
344346

0 commit comments

Comments
 (0)