Skip to content

Commit d0e19d5

Browse files
IsmaelMartinezElad Ben-Israel
authored and
Elad Ben-Israel
committedJun 3, 2019
feat(core+cli): support tagging of stacks (#2185)
Adding tags parameter option to cdk deploy command to allow tagging full stacks and their associated resources. Now it will be possible to: ``` const app = new App(); const stack1 = new Stack(app, 'stack1', { tags: { foo: 'bar' } }); const stack2 = new Stacl(app, 'stack2'); stack1.node.apply(new Tag('fii', 'bug')); stack2.node.apply(new Tag('boo', 'bug')); ``` That will produce * stack1 with tags `foo bar` and `fii bug` * stack2 with tags `boo bug` It is possible also to override constructor tags with the stack.node.apply. So doing: ``` stack1.node.apply(new Tag('foo', 'newBar'); ``` stack1 will have tags `foo newBar` and `fii bug` Last, but not least, it is also possible to pass it via arguments (using yargs) as in the following example: ``` cdk deploy --tags foo=bar --tags myTag=myValue ``` That will produce a stack with tags `foo bar`and `myTag myValue` **Important** That will ignore tags provided by the constructor and/or aspects. Fixes #932
1 parent 0b1bbf7 commit d0e19d5

File tree

14 files changed

+209
-47
lines changed

14 files changed

+209
-47
lines changed
 

‎packages/@aws-cdk/cdk/lib/cfn-resource.ts

+3-15
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ import cxapi = require('@aws-cdk/cx-api');
22
import { CfnCondition } from './cfn-condition';
33
import { Construct, IConstruct } from './construct';
44
import { CreationPolicy, DeletionPolicy, UpdatePolicy } from './resource-policy';
5-
import { TagManager } from './tag-manager';
65
import { capitalizePropertyNames, ignoreEmpty, PostResolveToken } from './util';
76
// import required to be here, otherwise causes a cycle when running the generated JavaScript
87
// tslint:disable-next-line:ordered-imports
98
import { CfnRefElement } from './cfn-element';
109
import { CfnReference } from './cfn-reference';
10+
import { TagManager } from './tag-manager';
1111

1212
export interface CfnResourceProps {
1313
/**
@@ -23,12 +23,6 @@ export interface CfnResourceProps {
2323
readonly properties?: any;
2424
}
2525

26-
export interface ITaggable {
27-
/**
28-
* TagManager to set, remove and format tags
29-
*/
30-
readonly tags: TagManager;
31-
}
3226
/**
3327
* Represents a CloudFormation resource.
3428
*/
@@ -56,13 +50,6 @@ export class CfnResource extends CfnRefElement {
5650
return (construct as any).resourceType !== undefined;
5751
}
5852

59-
/**
60-
* Check whether the given construct is Taggable
61-
*/
62-
public static isTaggable(construct: any): construct is ITaggable {
63-
return (construct as any).tags !== undefined;
64-
}
65-
6653
/**
6754
* Options for this resource, such as condition, update policy etc.
6855
*/
@@ -212,7 +199,7 @@ export class CfnResource extends CfnRefElement {
212199
public _toCloudFormation(): object {
213200
try {
214201
// merge property overrides onto properties and then render (and validate).
215-
const tags = CfnResource.isTaggable(this) ? this.tags.renderTags() : undefined;
202+
const tags = TagManager.isTaggable(this) ? this.tags.renderTags() : undefined;
216203
const properties = deepMerge(
217204
this.properties || {},
218205
{ tags },
@@ -281,6 +268,7 @@ export enum TagType {
281268
Standard = 'StandardTag',
282269
AutoScalingGroup = 'AutoScalingGroupTag',
283270
Map = 'StringToStringMap',
271+
KeyValue = 'KeyValue',
284272
NotTaggable = 'NotTaggable',
285273
}
286274

‎packages/@aws-cdk/cdk/lib/construct.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -719,4 +719,5 @@ export interface OutgoingReference {
719719
}
720720

721721
// Import this _after_ everything else to help node work the classes out in the correct order...
722-
import { Reference } from './reference';
722+
723+
import { Reference } from './reference';

‎packages/@aws-cdk/cdk/lib/stack.ts

+23-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { Construct, IConstruct, PATH_SEP } from './construct';
77
import { Environment } from './environment';
88
import { HashedAddressingScheme, IAddressingScheme, LogicalIDs } from './logical-id';
99
import { makeUniqueId } from './uniqueid';
10-
1110
export interface StackProps {
1211
/**
1312
* The AWS environment (account/region) where this stack will be deployed.
@@ -41,14 +40,22 @@ export interface StackProps {
4140
* @default true
4241
*/
4342
readonly autoDeploy?: boolean;
43+
44+
/**
45+
* Stack tags that will be applied to all the taggable resources and the stack itself.
46+
*
47+
* @default {}
48+
*/
49+
readonly tags?: { [key: string]: string };
4450
}
4551

4652
const STACK_SYMBOL = Symbol.for('@aws-cdk/cdk.Stack');
4753

4854
/**
4955
* A root construct which represents a single CloudFormation stack.
5056
*/
51-
export class Stack extends Construct {
57+
export class Stack extends Construct implements ITaggable {
58+
5259
/**
5360
* Adds a metadata annotation "aws:cdk:physical-name" to the construct if physicalName
5461
* is non-null. This can be used later by tools and aspects to determine if resources
@@ -73,6 +80,11 @@ export class Stack extends Construct {
7380

7481
private static readonly VALID_STACK_NAME_REGEX = /^[A-Za-z][A-Za-z0-9-]*$/;
7582

83+
/**
84+
* Tags to be applied to the stack.
85+
*/
86+
public readonly tags: TagManager;
87+
7688
/**
7789
* Lists all missing contextual information.
7890
* This is returned when the stack is synthesized under the 'missing' attribute
@@ -150,6 +162,7 @@ export class Stack extends Construct {
150162
this.logicalIds = new LogicalIDs(props && props.namingScheme ? props.namingScheme : new HashedAddressingScheme());
151163
this.name = props.stackName !== undefined ? props.stackName : this.calculateStackName();
152164
this.autoDeploy = props && props.autoDeploy === false ? false : true;
165+
this.tags = new TagManager(TagType.KeyValue, "aws:cdk:stack", props.tags);
153166

154167
if (!Stack.VALID_STACK_NAME_REGEX.test(this.name)) {
155168
throw new Error(`Stack name must match the regular expression: ${Stack.VALID_STACK_NAME_REGEX.toString()}, got '${name}'`);
@@ -490,6 +503,10 @@ export class Stack extends Construct {
490503
}
491504
}
492505
}
506+
507+
if (this.tags.hasTags()) {
508+
this.node.addMetadata(cxapi.STACK_TAGS_METADATA_KEY, this.tags.renderTags());
509+
}
493510
}
494511

495512
protected synthesize(builder: cxapi.CloudAssemblyBuilder): void {
@@ -552,13 +569,15 @@ export class Stack extends Construct {
552569
visit(this);
553570

554571
const app = this.parentApp();
572+
555573
if (app && app.node.metadata.length > 0) {
556574
output[PATH_SEP] = app.node.metadata;
557575
}
558576

559577
return output;
560578

561579
function visit(node: IConstruct) {
580+
562581
if (node.node.metadata.length > 0) {
563582
// Make the path absolute
564583
output[PATH_SEP + node.node.path] = node.node.metadata.map(md => node.node.resolve(md) as cxapi.MetadataEntry);
@@ -664,8 +683,9 @@ function cfnElements(node: IConstruct, into: CfnElement[] = []): CfnElement[] {
664683
import { ArnComponents, arnFromComponents, parseArn } from './arn';
665684
import { CfnElement } from './cfn-element';
666685
import { CfnReference } from './cfn-reference';
667-
import { CfnResource } from './cfn-resource';
686+
import { CfnResource, TagType } from './cfn-resource';
668687
import { Aws, ScopedAws } from './pseudo';
688+
import { ITaggable, TagManager } from './tag-manager';
669689

670690
/**
671691
* Find all resources in a set of constructs

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

+4-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
// import cxapi = require('@aws-cdk/cx-api');
12
import { IAspect } from './aspect';
2-
import { CfnResource, ITaggable } from './cfn-resource';
33
import { IConstruct } from './construct';
4+
import { ITaggable, TagManager } from './tag-manager';
45

56
/**
67
* Properties for a tag
@@ -71,12 +72,8 @@ export abstract class TagBase implements IAspect {
7172
}
7273

7374
public visit(construct: IConstruct): void {
74-
if (!CfnResource.isCfnResource(construct)) {
75-
return;
76-
}
77-
const resource = construct as CfnResource;
78-
if (CfnResource.isTaggable(resource)) {
79-
this.applyTag(resource);
75+
if (TagManager.isTaggable(construct)) {
76+
this.applyTag(construct);
8077
}
8178
}
8279

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

+60
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ interface CfnAsgTag {
1818
propagateAtLaunch: boolean;
1919
}
2020

21+
interface StackTag {
22+
Key: string;
23+
Value: string;
24+
}
2125
/**
2226
* Interface for converter between CloudFormation and internal tag representations
2327
*/
@@ -142,6 +146,36 @@ class MapFormatter implements ITagFormatter {
142146
}
143147
}
144148

149+
/**
150+
* StackTags are of the format { Key: key, Value: value }
151+
*/
152+
class KeyValueFormatter implements ITagFormatter {
153+
public parseTags(keyValueTags: any, priority: number): Tag[] {
154+
const tags: Tag[] = [];
155+
for (const key in keyValueTags) {
156+
if (keyValueTags.hasOwnProperty(key)) {
157+
const value = keyValueTags[key];
158+
tags.push({
159+
key,
160+
value,
161+
priority
162+
});
163+
}
164+
}
165+
return tags;
166+
}
167+
public formatTags(unformattedTags: Tag[]): any {
168+
const tags: StackTag[] = [];
169+
unformattedTags.forEach(tag => {
170+
tags.push({
171+
Key: tag.key,
172+
Value: tag.value
173+
});
174+
});
175+
return tags;
176+
}
177+
}
178+
145179
class NoFormat implements ITagFormatter {
146180
public parseTags(_cfnPropertyTags: any): Tag[] {
147181
return [];
@@ -155,13 +189,32 @@ const TAG_FORMATTERS: {[key: string]: ITagFormatter} = {
155189
[TagType.AutoScalingGroup]: new AsgFormatter(),
156190
[TagType.Standard]: new StandardFormatter(),
157191
[TagType.Map]: new MapFormatter(),
192+
[TagType.KeyValue]: new KeyValueFormatter(),
158193
[TagType.NotTaggable]: new NoFormat(),
159194
};
160195

196+
/**
197+
* Interface to implement tags.
198+
*/
199+
export interface ITaggable {
200+
/**
201+
* TagManager to set, remove and format tags
202+
*/
203+
readonly tags: TagManager;
204+
}
205+
161206
/**
162207
* TagManager facilitates a common implementation of tagging for Constructs.
163208
*/
164209
export class TagManager {
210+
211+
/**
212+
* Check whether the given construct is Taggable
213+
*/
214+
public static isTaggable(construct: any): construct is ITaggable {
215+
return (construct as any).tags !== undefined;
216+
}
217+
165218
private readonly tags = new Map<string, Tag>();
166219
private readonly priorities = new Map<string, number>();
167220
private readonly tagFormatter: ITagFormatter;
@@ -217,6 +270,13 @@ export class TagManager {
217270
return true;
218271
}
219272

273+
/**
274+
* Returns true if there are any tags defined
275+
*/
276+
public hasTags(): boolean {
277+
return this.tags.size > 0;
278+
}
279+
220280
private _setTag(...tags: Tag[]) {
221281
for (const tag of tags) {
222282
if (tag.priority >= (this.priorities.get(tag.key) || 0)) {

‎packages/@aws-cdk/cdk/test/test.tag-manager.ts

+20-2
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,24 @@ export = {
4141
'when there are no tags': {
4242
'#renderTags() returns undefined'(test: Test) {
4343
const mgr = new TagManager(TagType.Standard, 'AWS::Resource::Type');
44-
test.deepEqual(mgr.renderTags(), undefined );
44+
test.deepEqual(mgr.renderTags(), undefined);
4545
test.done();
4646
},
47+
'#hasTags() returns false'(test: Test) {
48+
const mgr = new TagManager(TagType.Standard, 'AWS::Resource::Type');
49+
test.equal(mgr.hasTags(), false);
50+
test.done();
51+
}
4752
},
48-
'#renderTags() handles standard, map, and ASG tag formats'(test: Test) {
53+
'#renderTags() handles standard, map, keyValue, and ASG tag formats'(test: Test) {
4954
const tagged: TagManager[] = [];
5055
const standard = new TagManager(TagType.Standard, 'AWS::Resource::Type');
5156
const asg = new TagManager(TagType.AutoScalingGroup, 'AWS::Resource::Type');
57+
const keyValue = new TagManager(TagType.KeyValue, 'AWS::Resource::Type');
5258
const mapper = new TagManager(TagType.Map, 'AWS::Resource::Type');
5359
tagged.push(standard);
5460
tagged.push(asg);
61+
tagged.push(keyValue);
5562
tagged.push(mapper);
5663
for (const res of tagged) {
5764
res.setTag('foo', 'bar');
@@ -65,12 +72,23 @@ export = {
6572
{key: 'foo', value: 'bar', propagateAtLaunch: true},
6673
{key: 'asg', value: 'only', propagateAtLaunch: false},
6774
]);
75+
test.deepEqual(keyValue.renderTags(), [
76+
{ Key: 'foo', Value : 'bar' },
77+
{ Key: 'asg', Value : 'only' }
78+
]);
6879
test.deepEqual(mapper.renderTags(), {
6980
foo: 'bar',
7081
asg: 'only',
7182
});
7283
test.done();
7384
},
85+
'when there are tags it hasTags returns true'(test: Test) {
86+
const mgr = new TagManager(TagType.Standard, 'AWS::Resource::Type');
87+
mgr.setTag('key', 'myVal', 2);
88+
mgr.setTag('key', 'newVal', 1);
89+
test.equal(mgr.hasTags(), true);
90+
test.done();
91+
},
7492
'tags with higher or equal priority always take precedence'(test: Test) {
7593
const mgr = new TagManager(TagType.Standard, 'AWS::Resource::Type');
7694
mgr.setTag('key', 'myVal', 2);

‎packages/@aws-cdk/cx-api/lib/cxapi.ts

+2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ export const DISABLE_VERSION_REPORTING = 'aws:cdk:disable-version-reporting';
3131
export const DISABLE_ASSET_STAGING_CONTEXT = 'aws:cdk:disable-asset-staging';
3232

3333
/**
34+
* If this context key is set, the CDK will stage assets under the specified
35+
* directory. Otherwise, assets will not be staged.
3436
* Omits stack traces from construct metadata entries.
3537
*/
3638
export const DISABLE_METADATA_STACK_TRACE = 'aws:cdk:disable-stack-trace';

‎packages/@aws-cdk/cx-api/lib/metadata.ts

+5
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ export const ERROR_METADATA_KEY = 'aws:cdk:error';
1919
*/
2020
export const PATH_METADATA_KEY = 'aws:cdk:path';
2121

22+
/**
23+
* Tag metadata key.
24+
*/
25+
export const STACK_TAGS_METADATA_KEY = 'aws:cdk:stack-tags';
26+
2227
export enum SynthesisMessageLevel {
2328
INFO = 'info',
2429
WARNING = 'warning',

‎packages/aws-cdk/bin/cdk.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ async function parseCommandLineArguments() {
5858
.option('exclusively', { type: 'boolean', alias: 'e', desc: 'only deploy requested stacks, don\'t include dependencies' })
5959
.option('require-approval', { type: 'string', choices: [RequireApproval.Never, RequireApproval.AnyChange, RequireApproval.Broadening], desc: 'what security-sensitive changes need manual approval' }))
6060
.option('ci', { type: 'boolean', desc: 'Force CI detection. Use --no-ci to disable CI autodetection.', default: process.env.CI !== undefined })
61+
.option('tags', { type: 'array', alias: 't', desc: 'tags to add to the stack (KEY=VALUE)', nargs: 1, requiresArg: true })
6162
.command('destroy [STACKS..]', 'Destroy the stack(s) named STACKS', yargs => yargs
6263
.option('exclusively', { type: 'boolean', alias: 'x', desc: 'only deploy requested stacks, don\'t include dependees' })
6364
.option('force', { type: 'boolean', alias: 'f', desc: 'Do not ask for confirmation before destroying the stacks' }))
@@ -99,7 +100,6 @@ async function initCommandLine() {
99100
proxyAddress: argv.proxy,
100101
ec2creds: argv.ec2creds,
101102
});
102-
103103
const configuration = new Configuration(argv);
104104
await configuration.load();
105105

@@ -198,7 +198,8 @@ async function initCommandLine() {
198198
roleArn: args.roleArn,
199199
requireApproval: configuration.settings.get(['requireApproval']),
200200
ci: args.ci,
201-
reuseAssets: args['build-exclude']
201+
reuseAssets: args['build-exclude'],
202+
tags: configuration.settings.get(['tags'])
202203
});
203204

204205
case 'destroy':

0 commit comments

Comments
 (0)