Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cdk): adding tags parameter option to cdk deploy command #2185

Merged
merged 29 commits into from
Jun 3, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
98226c1
feat(cdk): adding tags parameter option to cdk deploy command to allo…
IsmaelMartinez Apr 5, 2019
26c8921
feat(cdk) removing duplicated code and adding missing semicolons
IsmaelMartinez Apr 5, 2019
0ee589a
feat(cdk): #2185 Reading the stack metadata to find tags if no tags o…
IsmaelMartinez Apr 8, 2019
6444ba4
Removing print from getTagsFromStackMetadata
IsmaelMartinez Apr 8, 2019
14c08df
feat(cdk): #2185 Changes from PR comments
IsmaelMartinez Apr 11, 2019
71883f2
Changes from PR comments. Fixing build
IsmaelMartinez Apr 11, 2019
9defac8
Correcting copy/paste error and changing the name to TAGS_METADATA_KE…
IsmaelMartinez Apr 11, 2019
e046be6
Correcting copy/paste error and changing the name to TAGS_METADATA_KE…
IsmaelMartinez Apr 11, 2019
caaeff5
WIP with tag aspects
IsmaelMartinez Apr 22, 2019
17addb8
Merge branch 'master' into ismaelmartinez/feat-add-tags-option-cdk-de…
IsmaelMartinez Apr 22, 2019
c504b6f
Merge remote-tracking branch 'upstream/master' into ismaelmartinez/fe…
IsmaelMartinez Apr 24, 2019
ca252b0
WIP
IsmaelMartinez Apr 24, 2019
5e43a64
Adding the tags via properties and the stack.node.apply. WIP
IsmaelMartinez Apr 26, 2019
53f0158
Adding the tags via properties and the stack.node.apply. WIP
IsmaelMartinez Apr 26, 2019
408d7fa
Merge branch 'master' into ismaelmartinez/feat-add-tags-option-cdk-de…
IsmaelMartinez Apr 26, 2019
8eed2c8
changes from comments
IsmaelMartinez May 2, 2019
90fc71f
Only adding the metadata when there are tags
IsmaelMartinez May 3, 2019
d0cc809
Adding some tests for the KeyValue and hasTags in tag-manager
IsmaelMartinez May 3, 2019
bc1bb26
Moving the isTaggable to the Contructor and tidying up after code rev…
IsmaelMartinez May 6, 2019
c1e3e22
Merge remote-tracking branch 'upstream/master' into ismaelmartinez/fe…
IsmaelMartinez May 30, 2019
bbeebd2
Changes to address most of the code review comments
IsmaelMartinez May 31, 2019
196b2ca
Reverting the move of the check for if the resource/stack is taggable…
IsmaelMartinez May 31, 2019
08eb2b3
Fixing the build (alphabeticall order of imports)
IsmaelMartinez May 31, 2019
f0a219e
Moving isTaggable from the constructor to the TagManager
IsmaelMartinez Jun 3, 2019
0e3c101
renaming TAGS_METADATA_KEY to STACK_TAGS_METADATA_KEY and moving it t…
IsmaelMartinez Jun 3, 2019
4d97f7c
Missing fixes from code review changes
IsmaelMartinez Jun 3, 2019
26d1870
Fixing build
IsmaelMartinez Jun 3, 2019
d04c9c1
Fixing an issue with the tags not been populated if pass via the cons…
IsmaelMartinez Jun 3, 2019
bf518e3
Adding docs
IsmaelMartinez Jun 3, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 3 additions & 15 deletions packages/@aws-cdk/cdk/lib/cfn-resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import cxapi = require('@aws-cdk/cx-api');
import { CfnCondition } from './cfn-condition';
import { Construct, IConstruct } from './construct';
import { CreationPolicy, DeletionPolicy, UpdatePolicy } from './resource-policy';
import { TagManager } from './tag-manager';
import { capitalizePropertyNames, ignoreEmpty, PostResolveToken } from './util';
// import required to be here, otherwise causes a cycle when running the generated JavaScript
// tslint:disable-next-line:ordered-imports
import { CfnRefElement } from './cfn-element';
import { CfnReference } from './cfn-reference';
import { TagManager } from './tag-manager';

export interface CfnResourceProps {
/**
Expand All @@ -23,12 +23,6 @@ export interface CfnResourceProps {
readonly properties?: any;
}

export interface ITaggable {
/**
* TagManager to set, remove and format tags
*/
readonly tags: TagManager;
}
/**
* Represents a CloudFormation resource.
*/
Expand Down Expand Up @@ -56,13 +50,6 @@ export class CfnResource extends CfnRefElement {
return (construct as any).resourceType !== undefined;
}

/**
* Check whether the given construct is Taggable
*/
public static isTaggable(construct: any): construct is ITaggable {
return (construct as any).tags !== undefined;
}

/**
* Options for this resource, such as condition, update policy etc.
*/
Expand Down Expand Up @@ -212,7 +199,7 @@ export class CfnResource extends CfnRefElement {
public _toCloudFormation(): object {
try {
// merge property overrides onto properties and then render (and validate).
const tags = CfnResource.isTaggable(this) ? this.tags.renderTags() : undefined;
const tags = TagManager.isTaggable(this) ? this.tags.renderTags() : undefined;
const properties = deepMerge(
this.properties || {},
{ tags },
Expand Down Expand Up @@ -279,6 +266,7 @@ export enum TagType {
Standard = 'StandardTag',
AutoScalingGroup = 'AutoScalingGroupTag',
Map = 'StringToStringMap',
KeyValue = 'KeyValue',
IsmaelMartinez marked this conversation as resolved.
Show resolved Hide resolved
NotTaggable = 'NotTaggable',
}

Expand Down
3 changes: 2 additions & 1 deletion packages/@aws-cdk/cdk/lib/construct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -704,4 +704,5 @@ export interface OutgoingReference {
}

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

import { Reference } from './reference';
26 changes: 23 additions & 3 deletions packages/@aws-cdk/cdk/lib/stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { Construct, IConstruct, PATH_SEP } from './construct';
import { Environment } from './environment';
import { HashedAddressingScheme, IAddressingScheme, LogicalIDs } from './logical-id';
import { makeUniqueId } from './uniqueid';

export interface StackProps {
/**
* The AWS environment (account/region) where this stack will be deployed.
Expand Down Expand Up @@ -41,14 +40,22 @@ export interface StackProps {
* @default true
*/
readonly autoDeploy?: boolean;

/**
* Stack tags that will be applied to all the taggable resources and the stack itself.
*
* @default {}
*/
readonly tags?: { [key: string]: string };
IsmaelMartinez marked this conversation as resolved.
Show resolved Hide resolved
}

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

/**
* A root construct which represents a single CloudFormation stack.
*/
export class Stack extends Construct {
export class Stack extends Construct implements ITaggable {

/**
* Adds a metadata annotation "aws:cdk:physical-name" to the construct if physicalName
* is non-null. This can be used later by tools and aspects to determine if resources
Expand All @@ -73,6 +80,11 @@ export class Stack extends Construct {

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

/**
* Tags to be applied to the stack.
*/
public readonly tags: TagManager;
IsmaelMartinez marked this conversation as resolved.
Show resolved Hide resolved

/**
* Lists all missing contextual information.
* This is returned when the stack is synthesized under the 'missing' attribute
Expand Down Expand Up @@ -150,6 +162,7 @@ export class Stack extends Construct {
this.logicalIds = new LogicalIDs(props && props.namingScheme ? props.namingScheme : new HashedAddressingScheme());
this.name = props.stackName !== undefined ? props.stackName : this.calculateStackName();
this.autoDeploy = props && props.autoDeploy === false ? false : true;
this.tags = new TagManager(TagType.KeyValue, "aws:cdk:stack", props.tags);

if (!Stack.VALID_STACK_NAME_REGEX.test(this.name)) {
throw new Error(`Stack name must match the regular expression: ${Stack.VALID_STACK_NAME_REGEX.toString()}, got '${name}'`);
Expand Down Expand Up @@ -490,6 +503,10 @@ export class Stack extends Construct {
}
}
}

if (this.tags.hasTags()) {
this.node.addMetadata(cxapi.STACK_TAGS_METADATA_KEY, this.tags.renderTags());
}
}

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

const app = this.parentApp();

if (app && app.node.metadata.length > 0) {
output[PATH_SEP] = app.node.metadata;
}

return output;

function visit(node: IConstruct) {

if (node.node.metadata.length > 0) {
// Make the path absolute
output[PATH_SEP + node.node.path] = node.node.metadata.map(md => node.node.resolve(md) as cxapi.MetadataEntry);
Expand Down Expand Up @@ -664,8 +683,9 @@ function cfnElements(node: IConstruct, into: CfnElement[] = []): CfnElement[] {
import { ArnComponents, arnFromComponents, parseArn } from './arn';
import { CfnElement } from './cfn-element';
import { CfnReference } from './cfn-reference';
import { CfnResource } from './cfn-resource';
import { CfnResource, TagType } from './cfn-resource';
import { Aws, ScopedAws } from './pseudo';
import { ITaggable, TagManager } from './tag-manager';

/**
* Find all resources in a set of constructs
Expand Down
11 changes: 4 additions & 7 deletions packages/@aws-cdk/cdk/lib/tag-aspect.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// import cxapi = require('@aws-cdk/cx-api');
import { IAspect } from './aspect';
import { CfnResource, ITaggable } from './cfn-resource';
import { IConstruct } from './construct';
import { ITaggable, TagManager } from './tag-manager';

/**
* Properties for a tag
Expand Down Expand Up @@ -71,12 +72,8 @@ export abstract class TagBase implements IAspect {
}

public visit(construct: IConstruct): void {
if (!CfnResource.isCfnResource(construct)) {
return;
}
const resource = construct as CfnResource;
if (CfnResource.isTaggable(resource)) {
this.applyTag(resource);
if (TagManager.isTaggable(construct)) {
this.applyTag(construct);
}
}

Expand Down
60 changes: 60 additions & 0 deletions packages/@aws-cdk/cdk/lib/tag-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ interface CfnAsgTag {
propagateAtLaunch: boolean;
}

interface StackTag {
Key: string;
Value: string;
}
/**
* Interface for converter between CloudFormation and internal tag representations
*/
Expand Down Expand Up @@ -142,6 +146,36 @@ class MapFormatter implements ITagFormatter {
}
}

/**
* StackTags are of the format { Key: key, Value: value }
*/
class KeyValueFormatter implements ITagFormatter {
IsmaelMartinez marked this conversation as resolved.
Show resolved Hide resolved
public parseTags(keyValueTags: any, priority: number): Tag[] {
const tags: Tag[] = [];
for (const key in keyValueTags) {
if (keyValueTags.hasOwnProperty(key)) {
const value = keyValueTags[key];
tags.push({
key,
value,
priority
});
}
}
return tags;
}
public formatTags(unformattedTags: Tag[]): any {
const tags: StackTag[] = [];
unformattedTags.forEach(tag => {
tags.push({
Key: tag.key,
Value: tag.value
});
});
return tags;
}
}

class NoFormat implements ITagFormatter {
public parseTags(_cfnPropertyTags: any): Tag[] {
return [];
Expand All @@ -155,13 +189,32 @@ const TAG_FORMATTERS: {[key: string]: ITagFormatter} = {
[TagType.AutoScalingGroup]: new AsgFormatter(),
[TagType.Standard]: new StandardFormatter(),
[TagType.Map]: new MapFormatter(),
[TagType.KeyValue]: new KeyValueFormatter(),
[TagType.NotTaggable]: new NoFormat(),
};

/**
* Interface to implement tags.
*/
export interface ITaggable {
IsmaelMartinez marked this conversation as resolved.
Show resolved Hide resolved
/**
* TagManager to set, remove and format tags
*/
readonly tags: TagManager;
}

/**
* TagManager facilitates a common implementation of tagging for Constructs.
*/
export class TagManager {

/**
* Check whether the given construct is Taggable
*/
public static isTaggable(construct: any): construct is ITaggable {
return (construct as any).tags !== undefined;
}

private readonly tags = new Map<string, Tag>();
private readonly priorities = new Map<string, number>();
private readonly tagFormatter: ITagFormatter;
Expand Down Expand Up @@ -217,6 +270,13 @@ export class TagManager {
return true;
}

/**
* Returns true if there are any tags defined
*/
public hasTags(): boolean {
IsmaelMartinez marked this conversation as resolved.
Show resolved Hide resolved
return this.tags.size > 0;
}

private _setTag(...tags: Tag[]) {
for (const tag of tags) {
if (tag.priority >= (this.priorities.get(tag.key) || 0)) {
Expand Down
22 changes: 20 additions & 2 deletions packages/@aws-cdk/cdk/test/test.tag-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,24 @@ export = {
'when there are no tags': {
'#renderTags() returns undefined'(test: Test) {
const mgr = new TagManager(TagType.Standard, 'AWS::Resource::Type');
test.deepEqual(mgr.renderTags(), undefined );
test.deepEqual(mgr.renderTags(), undefined);
test.done();
},
'#hasTags() returns false'(test: Test) {
const mgr = new TagManager(TagType.Standard, 'AWS::Resource::Type');
test.equal(mgr.hasTags(), false);
test.done();
}
},
'#renderTags() handles standard, map, and ASG tag formats'(test: Test) {
'#renderTags() handles standard, map, keyValue, and ASG tag formats'(test: Test) {
const tagged: TagManager[] = [];
const standard = new TagManager(TagType.Standard, 'AWS::Resource::Type');
const asg = new TagManager(TagType.AutoScalingGroup, 'AWS::Resource::Type');
const keyValue = new TagManager(TagType.KeyValue, 'AWS::Resource::Type');
const mapper = new TagManager(TagType.Map, 'AWS::Resource::Type');
tagged.push(standard);
tagged.push(asg);
tagged.push(keyValue);
tagged.push(mapper);
for (const res of tagged) {
res.setTag('foo', 'bar');
Expand All @@ -65,12 +72,23 @@ export = {
{key: 'foo', value: 'bar', propagateAtLaunch: true},
{key: 'asg', value: 'only', propagateAtLaunch: false},
]);
test.deepEqual(keyValue.renderTags(), [
{ Key: 'foo', Value : 'bar' },
{ Key: 'asg', Value : 'only' }
]);
test.deepEqual(mapper.renderTags(), {
foo: 'bar',
asg: 'only',
});
test.done();
},
'when there are tags it hasTags returns true'(test: Test) {
const mgr = new TagManager(TagType.Standard, 'AWS::Resource::Type');
mgr.setTag('key', 'myVal', 2);
mgr.setTag('key', 'newVal', 1);
test.equal(mgr.hasTags(), true);
test.done();
},
'tags with higher or equal priority always take precedence'(test: Test) {
const mgr = new TagManager(TagType.Standard, 'AWS::Resource::Type');
mgr.setTag('key', 'myVal', 2);
Expand Down
2 changes: 2 additions & 0 deletions packages/@aws-cdk/cx-api/lib/cxapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export const DISABLE_VERSION_REPORTING = 'aws:cdk:disable-version-reporting';
export const DISABLE_ASSET_STAGING_CONTEXT = 'aws:cdk:disable-asset-staging';

/**
* If this context key is set, the CDK will stage assets under the specified
* directory. Otherwise, assets will not be staged.
* Omits stack traces from construct metadata entries.
*/
export const DISABLE_METADATA_STACK_TRACE = 'aws:cdk:disable-stack-trace';
5 changes: 5 additions & 0 deletions packages/@aws-cdk/cx-api/lib/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ export const ERROR_METADATA_KEY = 'aws:cdk:error';
*/
export const PATH_METADATA_KEY = 'aws:cdk:path';

/**
* Tag metadata key.
*/
export const STACK_TAGS_METADATA_KEY = 'aws:cdk:stack-tags';

export enum SynthesisMessageLevel {
INFO = 'info',
WARNING = 'warning',
Expand Down
5 changes: 3 additions & 2 deletions packages/aws-cdk/bin/cdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ async function parseCommandLineArguments() {
.option('exclusively', { type: 'boolean', alias: 'e', desc: 'only deploy requested stacks, don\'t include dependencies' })
.option('require-approval', { type: 'string', choices: [RequireApproval.Never, RequireApproval.AnyChange, RequireApproval.Broadening], desc: 'what security-sensitive changes need manual approval' }))
.option('ci', { type: 'boolean', desc: 'Force CI detection. Use --no-ci to disable CI autodetection.', default: process.env.CI !== undefined })
.option('tags', { type: 'array', alias: 't', desc: 'tags to add to the stack (KEY=VALUE)', nargs: 1, requiresArg: true })
.command('destroy [STACKS..]', 'Destroy the stack(s) named STACKS', yargs => yargs
.option('exclusively', { type: 'boolean', alias: 'x', desc: 'only deploy requested stacks, don\'t include dependees' })
.option('force', { type: 'boolean', alias: 'f', desc: 'Do not ask for confirmation before destroying the stacks' }))
Expand Down Expand Up @@ -99,7 +100,6 @@ async function initCommandLine() {
proxyAddress: argv.proxy,
ec2creds: argv.ec2creds,
});

const configuration = new Configuration(argv);
await configuration.load();

Expand Down Expand Up @@ -198,7 +198,8 @@ async function initCommandLine() {
roleArn: args.roleArn,
requireApproval: configuration.settings.get(['requireApproval']),
ci: args.ci,
reuseAssets: args['build-exclude']
reuseAssets: args['build-exclude'],
tags: configuration.settings.get(['tags'])
});

case 'destroy':
Expand Down
Loading