diff --git a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts index 584240c470701..397758747fc8d 100644 --- a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts +++ b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts @@ -185,7 +185,8 @@ export class AutoScalingGroup extends cdk.Construct implements cdk.ITaggable, el }); this.connections = new ec2.Connections({ securityGroup: this.securityGroup }); this.securityGroups.push(this.securityGroup); - this.tags = new TagManager(this, {initialTags: props.tags}); + this.tags = new cdk.TagManager(this, {initialTags: props.tags, + tagFormatter: new cdk.AutoScalingGroupTagFormatter()}); this.tags.setTag(NAME_TAG, this.path, { overwrite: false }); this.role = new iam.Role(this, 'InstanceRole', { @@ -489,16 +490,6 @@ function renderRollingUpdateConfig(config: RollingUpdateConfiguration = {}): cdk }; } -class TagManager extends cdk.TagManager { - protected tagFormatResolve(tagGroups: cdk.TagGroups): any { - const tags = {...tagGroups.nonStickyTags, ...tagGroups.ancestorTags, ...tagGroups.stickyTags}; - return Object.keys(tags).map( (key) => { - const propagateAtLaunch = !!tagGroups.propagateTags[key] || !!tagGroups.ancestorTags[key]; - return {key, value: tags[key], propagateAtLaunch}; - }); - } -} - /** * Render a number of seconds to a PTnX string. */ diff --git a/packages/@aws-cdk/aws-ec2/lib/security-group.ts b/packages/@aws-cdk/aws-ec2/lib/security-group.ts index 49b187bc107bd..254c0a47ac004 100644 --- a/packages/@aws-cdk/aws-ec2/lib/security-group.ts +++ b/packages/@aws-cdk/aws-ec2/lib/security-group.ts @@ -176,7 +176,6 @@ export class SecurityGroup extends SecurityGroupRef implements ITaggable { securityGroupIngress: new Token(() => this.directIngressRules), securityGroupEgress: new Token(() => this.directEgressRules), vpcId: props.vpc.vpcId, - tags: this.tags, }); this.securityGroupId = this.securityGroup.securityGroupId; diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc.ts b/packages/@aws-cdk/aws-ec2/lib/vpc.ts index 41bc9b97bcfdc..27453c5305fab 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc.ts @@ -290,6 +290,7 @@ export class VpcNetwork extends VpcNetworkRef implements cdk.ITaggable { instanceTenancy, tags: this.tags, }); + // this.resource.tags = this.tags; this.availabilityZones = new cdk.AvailabilityZoneProvider(this).availabilityZones; this.availabilityZones.sort(); @@ -309,9 +310,7 @@ export class VpcNetwork extends VpcNetworkRef implements cdk.ITaggable { // Create an Internet Gateway and attach it if necessary if (allowOutbound) { - const igw = new cloudformation.InternetGatewayResource(this, 'IGW', { - tags: new cdk.TagManager(this), - }); + const igw = new cloudformation.InternetGatewayResource(this, 'IGW'); const att = new cloudformation.VPCGatewayAttachmentResource(this, 'VPCGW', { internetGatewayId: igw.ref, vpcId: this.resource.ref @@ -494,12 +493,10 @@ export class VpcSubnet extends VpcSubnetRef implements cdk.ITaggable { cidrBlock: props.cidrBlock, availabilityZone: props.availabilityZone, mapPublicIpOnLaunch: props.mapPublicIpOnLaunch, - tags: this.tags, }); this.subnetId = subnet.subnetId; const table = new cloudformation.RouteTableResource(this, 'RouteTable', { vpcId: props.vpcId, - tags: new cdk.TagManager(this), }); this.routeTableId = table.ref; @@ -556,7 +553,6 @@ export class VpcPublicSubnet extends VpcSubnet { allocationId: new cloudformation.EIPResource(this, `EIP`, { domain: 'vpc' }).eipAllocationId, - tags: new cdk.TagManager(this), }); return ngw.natGatewayId; } diff --git a/packages/@aws-cdk/aws-ec2/test/test.vpc.ts b/packages/@aws-cdk/aws-ec2/test/test.vpc.ts index 403563c2875f5..1ffcf5a34537a 100644 --- a/packages/@aws-cdk/aws-ec2/test/test.vpc.ts +++ b/packages/@aws-cdk/aws-ec2/test/test.vpc.ts @@ -28,8 +28,10 @@ export = { 'the Name tag is defaulted to path'(test: Test) { const stack = getTestStack(); new VpcNetwork(stack, 'TheVPC'); - expect(stack).to(haveResource('AWS::EC2::VPC', - hasTags( [ {Key: 'Name', Value: 'TheVPC'} ]))); + expect(stack).to( + haveResource('AWS::EC2::VPC', + hasTags( [ {Key: 'Name', Value: 'TheVPC'} ])) + ); test.done(); }, @@ -124,25 +126,25 @@ export = { new VpcNetwork(stack, 'TheVPC', { cidr: '10.0.0.0/21', subnetConfiguration: [ - { - cidrMask: 24, - name: 'ingress', - subnetType: SubnetType.Public, - tags: { - type: 'Public', - init: 'No', + { + cidrMask: 24, + name: 'ingress', + subnetType: SubnetType.Public, + tags: { + type: 'Public', + init: 'No', + }, }, - }, - { - cidrMask: 24, - name: 'application', - subnetType: SubnetType.Private, - }, - { - cidrMask: 28, - name: 'rds', - subnetType: SubnetType.Isolated, - } + { + cidrMask: 24, + name: 'application', + subnetType: SubnetType.Private, + }, + { + cidrMask: 28, + name: 'rds', + subnetType: SubnetType.Isolated, + } ], maxAZs: 3 }); @@ -151,12 +153,12 @@ export = { expect(stack).to(countResources("AWS::EC2::Subnet", 9)); for (let i = 0; i < 6; i++) { expect(stack).to(haveResource("AWS::EC2::Subnet", { - CidrBlock: `10.0.${i}.0/24` + CidrBlock: `10.0.${i}.0/24` })); } for (let i = 0; i < 3; i++) { expect(stack).to(haveResource("AWS::EC2::Subnet", { - CidrBlock: `10.0.6.${i * 16}/28` + CidrBlock: `10.0.6.${i * 16}/28` })); } expect(stack).to(haveResource("AWS::EC2::Subnet", hasTags( diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts b/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts index e53d11112fbd8..420b58f8ce4e1 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts @@ -1,4 +1,5 @@ import { Construct } from '../core/construct'; +import { ITagFormatter, ITaggable, TagGroups, TagManager } from '../core/tag-manager'; import { capitalizePropertyNames, ignoreEmpty } from '../core/util'; import { CloudFormationToken } from './cloudformation-token'; import { Condition } from './condition'; @@ -229,6 +230,129 @@ export class Resource extends Referenceable { } } +export enum TagType { + Standard = "StandardTag", + AutoScalingGroup = "AutoScalingGroupTag", + Map = "StringToStringMap", +} + +/** + * Represents a CloudFormation resource which supports Tags + * + * The resource exposes `tags` property as the `TagManager` for this resource. The + * developer can set and remove tags from this location in the construct tree using + * the tags object. For example: + * + * ``` + * const myResource = new MyTaggableResource(parent, id, props: {}); + * myResource.setTag('Mykey, 'MyValue'); + * // you can also configure behavior with `TagProps` + * myResource.setTag('MyKey', 'MyValue', { propagate: false }); + * ``` + */ +export class TaggableResource extends Resource implements ITaggable { + /** + * TagManager to manage the propagation and assignment of tags + */ + public readonly tags: TagManager; + + protected readonly tagType: TagType = TagType.Standard; + + constructor(parent: Construct, id: string, props: ResourceProps) { + super(parent, id, props); + if (props.properties && props.properties.tags instanceof TagManager) { + this.tags = props.properties.tags; + } else { + this.tags = new TagManager(this); + if (!!props.properties && props.properties.tags) { + this.addCloudFormationTags(props.properties.tags); + } + } + } + + /** + * Emits CloudFormation for this resource. + * + * This method calls super after resolving the cloudformation tags. + */ + public toCloudFormation(): object { + // typescript initializes child second so this can't be dont in the constructor + this.tags.tagFormatter = this.tagFormatterForType(); + this.properties.tags = this.tags; + return super.toCloudFormation(); + } + + private tagFormatterForType(): ITagFormatter { + switch (this.tagType) { + case TagType.Standard: { + return new StandardTagFormatter(); + } + case TagType.AutoScalingGroup: { + return new AutoScalingGroupTagFormatter(); + } + case TagType.Map: { + return new MapTagFormatter(); + } + } + } + + /** + * Add any of the supported CloudFormatiom Tag Types to be managed + * + * @param tags: The tag(s) to add + */ + private addCloudFormationTags(tags: any) { + if (tags === undefined) { return; } + if (tags.map) { + for (const tag of tags) { + const propagate = tag.propagateAtLaunch !== false; + this.tags.setTag(tag.key, tag.value, {propagate}); + } + } else { + for (const key of Object.keys(tags)) { + this.tags.setTag(key, tags[key]); + } + } + } +} + +/** + * Handles converting TagManager tags to AutoScalingGroupTags + */ +export class AutoScalingGroupTagFormatter implements ITagFormatter { + public toCloudFormationTags(tagGroups: TagGroups): any { + const tags = {...tagGroups.nonStickyTags, ...tagGroups.ancestorTags, ...tagGroups.stickyTags}; + return Object.keys(tags).map( (key) => { + const propagateAtLaunch = !!tagGroups.propagateTags[key] || !!tagGroups.ancestorTags[key]; + return {key, value: tags[key], propagateAtLaunch}; + }); + } +} + +/** + * Handles converting TagManager tags to a map of string to string tags + */ +export class MapTagFormatter implements ITagFormatter { + public toCloudFormationTags(tagGroups: TagGroups): any { + const tags = {...tagGroups.nonStickyTags, ...tagGroups.ancestorTags, ...tagGroups.stickyTags}; + return Object.keys(tags).length === 0 ? undefined : tags; + } +} + +/** + * Handles converting TagManager tags to a standard array of key value pairs + */ +export class StandardTagFormatter implements ITagFormatter { + public toCloudFormationTags(tagGroups: TagGroups): any { + const tags = {...tagGroups.nonStickyTags, ...tagGroups.ancestorTags, ...tagGroups.stickyTags}; + const cfnTags = Object.keys(tags).map( key => ({key, value: tags[key]})); + if (cfnTags.length === 0) { + return undefined; + } + return cfnTags; + } +} + export interface ResourceOptions { /** * A condition to associate with this resource. This means that only if the condition evaluates to 'true' when the stack diff --git a/packages/@aws-cdk/cdk/lib/core/tag-manager.ts b/packages/@aws-cdk/cdk/lib/core/tag-manager.ts index 3b5ebf053320d..089d35ab8c434 100644 --- a/packages/@aws-cdk/cdk/lib/core/tag-manager.ts +++ b/packages/@aws-cdk/cdk/lib/core/tag-manager.ts @@ -1,3 +1,4 @@ +import { StandardTagFormatter } from '../cloudformation/resource'; import { Construct } from './construct'; import { Token } from './tokens'; @@ -85,6 +86,18 @@ export interface RemoveProps { blockPropagate?: boolean; } +/** + * This interface controls the final type of the tags + * + * This method is invoked inside `toCloudFormation` and developers should + * implement a class for this interface to extend formatting of tags for + * specialized CloudFormation Resources (e.g. AutoScalingGroups, Serverless and + * DAX) + */ +export interface ITagFormatter { + toCloudFormationTags(tagGroups: TagGroups): any; +} + /** * Properties for Tag Manager */ @@ -93,6 +106,11 @@ export interface TagManagerProps { * Initial tags to set on the tag manager using TAG_DEFAULTS */ initialTags?: Tags; + + /** + * Controls the toCloudFormation tag format + */ + tagFormatter?: ITagFormatter; } /** @@ -145,6 +163,13 @@ export class TagManager extends Token { overwrite: true }; + /** + * The format to resolve tags with for CloudFormation output + * + * @default `StandardTagFormatter` + */ + public tagFormatter: ITagFormatter; + /* * Internally tags will have properties set */ @@ -157,8 +182,8 @@ export class TagManager extends Token { constructor(private readonly parent: Construct, props: TagManagerProps = {}) { super(); - const initialTags = props.initialTags || {}; + this.tagFormatter = props.tagFormatter || new StandardTagFormatter(); for (const key of Object.keys(initialTags)) { const tag = { value: initialTags[key], @@ -169,9 +194,50 @@ export class TagManager extends Token { } /** - * Converts the `tags` to a Token for use in lazy evaluation + * Creates the CloudFormation representation of the tags + * + * This invokes the `tagFormatter.toCloudFormationTags` function */ public resolve(): any { + return this.tagFormatter.toCloudFormationTags(this.toTagGroups()); + } + /** + * Adds the specified tag to the array of tags + * + * @param key The key value of the tag + * @param value The value value of the tag + * @param props A `TagProps` object for the tag @default `TagManager.DEFAULT_TAG_PROPS` + */ + public setTag(key: string, value: string, tagProps: TagProps = {}): void { + const props = {...TagManager.DEFAULT_TAG_PROPS, ...tagProps}; + if (!props.overwrite) { + this._tags[key] = this._tags[key] || {value, props}; + } else { + this._tags[key] = {value, props}; + } + const index = this.blockedTags.indexOf(key); + if (index > -1) { + this.blockedTags.splice(index, 1); + } + } + + /** + * Removes the specified tag from the array if it exists + * + * @param key The key of the tag to remove + * @param props The `RemoveProps` for the tag + */ + public removeTag(key: string, props: RemoveProps = {blockPropagate: true}): void { + if (props.blockPropagate) { + this.blockedTags.push(key); + } + delete this._tags[key]; + } + + /** + * Converts the `tags` to a `TagGroups` object + */ + private toTagGroups(): TagGroups { // need this for scoping const blockedTags = this.blockedTags; function filterTags(_tags: FullTags, filter: TagProps = {}): Tags { @@ -214,60 +280,11 @@ export class TagManager extends Token { const ancestors = this.parent.ancestors(); const ancestorTags = propagatedTags(ancestors); const propagateTags = filterTags(this._tags, {propagate: true}); - return this.tagFormatResolve( { + return { ancestorTags, nonStickyTags, stickyTags, propagateTags, - }); - } - - /** - * Adds the specified tag to the array of tags - * - * @param key The key value of the tag - * @param value The value value of the tag - * @param props A `TagProps` object for the tag @default `TagManager.DEFAULT_TAG_PROPS` - */ - public setTag(key: string, value: string, tagProps: TagProps = {}): void { - const props = {...TagManager.DEFAULT_TAG_PROPS, ...tagProps}; - if (!props.overwrite) { - this._tags[key] = this._tags[key] || {value, props}; - } else { - this._tags[key] = {value, props}; - } - const index = this.blockedTags.indexOf(key); - if (index > -1) { - this.blockedTags.splice(index, 1); - } - } - - /** - * Removes the specified tag from the array if it exists - * - * @param key The key of the tag to remove - * @param props The `RemoveProps` for the tag - */ - public removeTag(key: string, props: RemoveProps = {blockPropagate: true}): void { - if (props.blockPropagate) { - this.blockedTags.push(key); - } - delete this._tags[key]; - } - - /** - * Handles returning the tags in the desired format - * - * This function can be overridden to support another tag format. This was - * specifically designed to enable AutoScalingGroup Tags that have an - * additional CloudFormation key for `PropagateAtLaunch` - */ - protected tagFormatResolve(tagGroups: TagGroups): any { - const tags = {...tagGroups.nonStickyTags, ...tagGroups.ancestorTags, ...tagGroups.stickyTags}; - for (const key of this.blockedTags) { delete tags[key]; } - if (Object.keys(tags).length === 0) { - return undefined; - } - return Object.keys(tags).map( key => ({key, value: tags[key]})); + }; } } diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.resource.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.resource.ts index 7c9fa4a60c722..d7ef1e14520e4 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.resource.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.resource.ts @@ -1,7 +1,14 @@ import { Test } from 'nodeunit'; import { applyRemovalPolicy, Condition, Construct, DeletionPolicy, - FnEquals, FnNot, HashedAddressingScheme, IDependable, - RemovalPolicy, resolve, Resource, Root, Stack } from '../../lib'; + FnEquals, FnNot, HashedAddressingScheme, IDependable, + RemovalPolicy, resolve, Resource, Root, Stack, TaggableResource } from '../../lib'; + +// class AsgTaggable extends TaggableResource { +// protected readonly tagType: TagType = TagType.AutoScalingGroup; +// } +// class MapTaggable extends TaggableResource { +// protected readonly tagType: TagType = TagType.Map; +// } export = { 'all resources derive from Resource, which derives from Entity'(test: Test) { @@ -332,12 +339,12 @@ export = { MyC3C2R2F213BD26: { Type: 'T2' }, MyC3C2R38CE6F9F7: { Type: 'T3' }, MyResource: - { Type: 'R', - DependsOn: + { Type: 'R', + DependsOn: [ 'MyC1R1FB2A562F', - 'MyC1R2AE2B5066', - 'MyC2R3809EEAD6', - 'MyC3C2R38CE6F9F7' ] } } }); + 'MyC1R2AE2B5066', + 'MyC2R3809EEAD6', + 'MyC3C2R38CE6F9F7' ] } } }); test.done(); }, @@ -349,6 +356,43 @@ export = { test.done(); }, + 'TaggableResource': { + 'TaggableResource with tags from an L1 constructor are initial tags '(test: Test) { + const stack = new Stack(); + const tagResource = new TaggableResource(stack, 'TaggableGuy', { + type: 'SomeTaggableResource', + properties: { + tags: [ + {key: 'tagKey', value: 'tagValue'}, + {key: 'tagKey1', value: 'tagValue1'}, + ], + }, + }); + tagResource.tags.setTag('tagKey', 'newValue'); + test.deepEqual( + tagResource.toCloudFormation(), + { + Resources: + { TaggableGuy: { + Properties: { + tags: [ + {key: 'tagKey', value: 'newValue'}, + {key: 'tagKey1', value: 'tagValue1'}, + ] + }, + Type: 'SomeTaggableResource', + DependsOn: undefined, + CreationPolicy: undefined, + UpdatePolicy: undefined, + DeletionPolicy: undefined, + Metadata: undefined, + Condition: undefined, + } + } + }); + test.done(); + }, + }, 'overrides': { 'addOverride(p, v) allows assigning arbitrary values to synthesized resource definitions'(test: Test) { // GIVEN @@ -363,9 +407,9 @@ export = { // THEN test.deepEqual(stack.toCloudFormation(), { Resources: { MyResource: - { Type: 'YouCanEvenOverrideTheType', - Use: { Dot: { Notation: 'To create subtrees' } }, - Metadata: { Key: 12 } } } }); + { Type: 'YouCanEvenOverrideTheType', + Use: { Dot: { Notation: 'To create subtrees' } }, + Metadata: { Key: 12 } } } }); test.done(); }, @@ -392,8 +436,8 @@ export = { // THEN test.deepEqual(stack.toCloudFormation(), { Resources: { MyResource: - { Type: 'AWS::Resource::Type', - Properties: { Hello: { World: { Value1: 'Hello', Value2: null } } } } } }); + { Type: 'AWS::Resource::Type', + Properties: { Hello: { World: { Value1: 'Hello', Value2: null } } } } } }); test.done(); }, @@ -420,8 +464,8 @@ export = { // THEN test.deepEqual(stack.toCloudFormation(), { Resources: { MyResource: - { Type: 'AWS::Resource::Type', - Properties: { Hello: { World: { Value1: 'Hello' } } } } } }); + { Type: 'AWS::Resource::Type', + Properties: { Hello: { World: { Value1: 'Hello' } } } } } }); test.done(); }, @@ -439,8 +483,8 @@ export = { // THEN test.deepEqual(stack.toCloudFormation(), { Resources: { MyResource: - { Type: 'AWS::Resource::Type', - Properties: { Tree: { Exists: 42 } } } } }); + { Type: 'AWS::Resource::Type', + Properties: { Tree: { Exists: 42 } } } } }); test.done(); }, @@ -469,8 +513,8 @@ export = { // THEN test.deepEqual(stack.toCloudFormation(), { Resources: { MyResource: - { Type: 'AWS::Resource::Type', - Properties: { Hello: { World: { Value1: 'Hello' } } } } } }); + { Type: 'AWS::Resource::Type', + Properties: { Hello: { World: { Value1: 'Hello' } } } } } }); test.done(); }, @@ -495,12 +539,12 @@ export = { // THEN test.deepEqual(stack.toCloudFormation(), { Resources: { MyResource: - { Type: 'AWS::Resource::Type', - Properties: + { Type: 'AWS::Resource::Type', + Properties: { Hello: { World: { Foo: { Bar: 42 } } }, - Override1: { - Override2: { Heyy: [ 1] } - } } } } }); + Override1: { + Override2: { Heyy: [ 1] } + } } } } }); test.done(); }, @@ -518,8 +562,8 @@ export = { // THEN test.deepEqual(stack.toCloudFormation(), { Resources: { MyResource: - { Type: 'AWS::Resource::Type', - Properties: { Hello: { World: { Hey: 'Jude' } } } } } }); + { Type: 'AWS::Resource::Type', + Properties: { Hello: { World: { Hey: 'Jude' } } } } } }); test.done(); }, @@ -536,8 +580,8 @@ export = { test.deepEqual(stack.toCloudFormation(), { Resources: { MyResource: - { Type: 'MyResourceType', - Properties: { PROP1: 'foo', PROP2: 'bar' } } } }); + { Type: 'MyResourceType', + Properties: { PROP1: 'foo', PROP2: 'bar' } } } }); test.done(); }, @@ -550,8 +594,8 @@ export = { test.deepEqual(stack.toCloudFormation(), { Resources: { MyResource: - { Type: 'MyResourceType', - Properties: { PROP3: 'zoo' } } } }); + { Type: 'MyResourceType', + Properties: { PROP3: 'zoo' } } } }); test.done(); }, @@ -565,10 +609,10 @@ export = { test.deepEqual(stack.toCloudFormation(), { Resources: { MyResource: - { Type: 'MyResourceType', - Properties: { PROP2: 'hey', PROP3: 'zoo' } } } }); + { Type: 'MyResourceType', + Properties: { PROP2: 'hey', PROP3: 'zoo' } } } }); test.done(); - } + }, } } }; diff --git a/packages/@aws-cdk/cdk/test/core/test.tag-manager.ts b/packages/@aws-cdk/cdk/test/core/test.tag-manager.ts index b34e57341772e..0199e130cb815 100644 --- a/packages/@aws-cdk/cdk/test/core/test.tag-manager.ts +++ b/packages/@aws-cdk/cdk/test/core/test.tag-manager.ts @@ -3,7 +3,7 @@ import { Construct, Root } from '../../lib/core/construct'; import { ITaggable, TagManager } from '../../lib/core/tag-manager'; class ChildTagger extends Construct implements ITaggable { - public readonly tags: TagManager; + public tags: TagManager; constructor(parent: Construct, name: string) { super(parent, name); this.tags = new TagManager(parent); @@ -53,8 +53,9 @@ export = { for (const construct of [ctagger1, ctagger2]) { test.deepEqual(construct.tags.resolve(), undefined); } - test.deepEqual(ctagger.tags.resolve()[0].key, 'Name'); - test.deepEqual(ctagger.tags.resolve()[0].value, 'TheCakeIsALie'); + const cfnTags = ctagger.tags.resolve() || []; + test.deepEqual(cfnTags[0].key, 'Name'); + test.deepEqual(cfnTags[0].value, 'TheCakeIsALie'); test.done(); }, 'setTag with overwrite false does not overwrite a tag'(test: Test) { @@ -145,7 +146,7 @@ export = { test.deepEqual(childTags, [tag2]); test.done(); }, - 'resolve() returns all tags'(test: Test) { + 'toCloudFormation() returns all tags'(test: Test) { const root = new Root(); const ctagger = new ChildTagger(root, 'one'); const ctagChild = new ChildTagger(ctagger, 'one'); @@ -167,11 +168,11 @@ export = { const cAll = ctagger.tags; const cProp = ctagChild.tags; - for (const tag of cAll.resolve()) { + for (const tag of cAll.resolve() || []) { const expectedTag = allTags.filter( (t) => (t.key === tag.key)); test.deepEqual(expectedTag[0].value, tag.value); } - for (const tag of cProp.resolve()) { + for (const tag of cProp.resolve() || []) { const expectedTag = tagsProp.filter( (t) => (t.key === tag.key)); test.deepEqual(expectedTag[0].value, tag.value); } diff --git a/packages/@aws-cdk/cfnspec/lib/schema/property.ts b/packages/@aws-cdk/cfnspec/lib/schema/property.ts index b9679d9ac98b3..d2d0c31654ba0 100644 --- a/packages/@aws-cdk/cfnspec/lib/schema/property.ts +++ b/packages/@aws-cdk/cfnspec/lib/schema/property.ts @@ -5,6 +5,7 @@ export type ScalarProperty = PrimitiveProperty | ComplexProperty | UnionProperty export type CollectionProperty = ListProperty | MapProperty | UnionProperty; export type ListProperty = PrimitiveListProperty | ComplexListProperty; export type MapProperty = PrimitiveMapProperty | ComplexMapProperty; +export type TagProperty = TagPropertyStandard | TagPropertyAutoScalingGroup | TagPropertyJson | TagPropertyStringMap; export interface PropertyBase extends Documented { /** Indicates whether the property is required. */ @@ -72,6 +73,22 @@ export interface ComplexMapProperty extends MapPropertyBase { ItemType: string; } +export interface TagPropertyStandard extends PropertyBase { + ItemType: 'Tag'; +} + +export interface TagPropertyAutoScalingGroup extends PropertyBase { + ItemType: 'TagProperty'; +} + +export interface TagPropertyJson extends PropertyBase { + PrimitiveType: PrimitiveType.Json; +} + +export interface TagPropertyStringMap extends PropertyBase { + PrimitiveItemType: 'String'; +} + /** * A property type that can be one of several types. Currently used only in SAM. */ @@ -154,3 +171,34 @@ export function isUnionProperty(prop: Property): prop is UnionProperty { const castProp = prop as UnionProperty; return !!(castProp.ItemTypes || castProp.PrimitiveTypes || castProp.Types); } + +/** + * This function validates that the property **can** be a Tag Property + * + * The property is only a Tag if the name of this property is Tags, which is + * validated using `ResourceType.isTaggable(resource)`. + */ +export function isTagProperty(prop: Property): prop is TagProperty { + return ( + isTagPropertyStandard(prop) || + isTagPropertyAutoScalingGroup(prop) || + isTagPropertyJson(prop) || + isTagPropertyStringMap(prop) + ); +} + +export function isTagPropertyStandard(prop: Property): prop is TagPropertyStandard { + return (prop as TagPropertyStandard).ItemType === 'Tag'; +} + +export function isTagPropertyAutoScalingGroup(prop: Property): prop is TagPropertyAutoScalingGroup { + return (prop as TagPropertyAutoScalingGroup).ItemType === 'TagProperty'; +} + +export function isTagPropertyJson(prop: Property): prop is TagPropertyJson { + return (prop as TagPropertyJson).PrimitiveType === PrimitiveType.Json; +} + +export function isTagPropertyStringMap(prop: Property): prop is TagPropertyStringMap { + return (prop as TagPropertyStringMap).PrimitiveItemType === 'String'; +} diff --git a/packages/@aws-cdk/cfnspec/lib/schema/resource-type.ts b/packages/@aws-cdk/cfnspec/lib/schema/resource-type.ts index 7517fcdbe1bf8..91313a762aaba 100644 --- a/packages/@aws-cdk/cfnspec/lib/schema/resource-type.ts +++ b/packages/@aws-cdk/cfnspec/lib/schema/resource-type.ts @@ -1,5 +1,5 @@ import { Documented, PrimitiveType } from './base-types'; -import { Property } from './property'; +import { isTagProperty, Property, TagProperty } from './property'; export interface ResourceType extends Documented { /** @@ -21,6 +21,13 @@ export interface ResourceType extends Documented { RefKind?: string; } +export interface TaggableResource extends ResourceType { + Properties: { + Tags: TagProperty; + [name: string]: Property; + } +} + export type Attribute = PrimitiveAttribute | ListAttribute; export interface PrimitiveAttribute { @@ -39,6 +46,20 @@ export interface ComplexListAttribute { ItemType: string; } +/** + * Determine if the resource supports tags + * + * This function combined with isTagProperty determines if the `cdk.TagManager` + * and `cdk.TaggableResource` can process these tags. If not, standard code + * generation of properties will be used. + */ +export function isTaggableResource(spec: ResourceType): spec is TaggableResource { + if (spec.Properties && spec.Properties.Tags) { + return isTagProperty(spec.Properties.Tags); + } + return false; +} + export function isPrimitiveAttribute(spec: Attribute): spec is PrimitiveAttribute { return !!(spec as PrimitiveAttribute).PrimitiveType; } diff --git a/tools/cfn2ts/lib/codegen.ts b/tools/cfn2ts/lib/codegen.ts index e47762e7a4e90..1bd2d0319bea4 100644 --- a/tools/cfn2ts/lib/codegen.ts +++ b/tools/cfn2ts/lib/codegen.ts @@ -8,6 +8,9 @@ import { itemTypeNames, PropertyAttributeName, scalarTypeNames, SpecName } from const CORE = genspec.CORE_NAMESPACE; const RESOURCE_BASE_CLASS = `${CORE}.Resource`; // base class for all resources const CONSTRUCT_CLASS = `${CORE}.Construct`; +const TAGGABLE_RESOURCE_BASE_CLASS = `${CORE}.TaggableResource`; +const TAG_TYPE = 'TagType'; +const NOT_TAGGABLE = 'NOT_TAGGABLE'; interface Dictionary { [key: string]: T; } @@ -183,7 +186,9 @@ export default class CodeGenerator { if (propsType) { this.code.line(); } - this.openClass(resourceName, spec.Documentation, RESOURCE_BASE_CLASS); + + const baseClass: string = schema.isTaggableResource(spec) ? TAGGABLE_RESOURCE_BASE_CLASS : RESOURCE_BASE_CLASS; + this.openClass(resourceName, spec.Documentation, baseClass); // // Static inspectors. @@ -238,6 +243,12 @@ export default class CodeGenerator { } } + // set the TagType to help TagManager format tags later + if (schema.isTaggableResource(spec)) { + const tagEnum = tagType(spec.Properties!.Tags); + this.code.line(`protected readonly tagType = ${tagEnum};`); + } + // // Constructor // @@ -252,7 +263,17 @@ export default class CodeGenerator { const optionalProps = spec.Properties && !Object.values(spec.Properties).some(p => p.Required); const propsArgument = propsType ? `, properties${optionalProps ? '?' : ''}: ${propsType.className}` : ''; this.code.openBlock(`constructor(parent: ${CONSTRUCT_CLASS}, name: string${propsArgument})`); - this.code.line(`super(parent, name, { type: ${resourceName.className}.resourceTypeName${propsType ? ', properties' : ''} });`); + + // + // Configure properties for super call + // + const superProps: string[] = []; + superProps.push(`type: ${resourceName.className}.resourceTypeName`); + if (propsType) { + superProps.push('properties'); + } + + this.code.line(`super(parent, name, { ${superProps.join(', ')} });`); // verify all required properties if (spec.Properties) { for (const propName of Object.keys(spec.Properties)) { @@ -334,9 +355,10 @@ export default class CodeGenerator { * * Generated as a top-level function outside any namespace so we can hide it from library consumers. */ - private emitCloudFormationMapper(typeName: genspec.CodeName, - propSpecs: { [name: string]: schema.Property }, - nameConversionTable: Dictionary) { + private emitCloudFormationMapper( + typeName: genspec.CodeName, + propSpecs: { [name: string]: schema.Property }, + nameConversionTable: Dictionary) { const mapperName = genspec.cfnMapperName(typeName); this.code.line('/**'); @@ -405,9 +427,10 @@ export default class CodeGenerator { * * Generated as a top-level function outside any namespace so we can hide it from library consumers. */ - private emitValidator(typeName: genspec.CodeName, - propSpecs: { [name: string]: schema.Property }, - nameConversionTable: Dictionary) { + private emitValidator( + typeName: genspec.CodeName, + propSpecs: { [name: string]: schema.Property }, + nameConversionTable: Dictionary) { const validatorName = genspec.validatorName(typeName); this.code.line('/**'); @@ -490,7 +513,15 @@ export default class CodeGenerator { const javascriptPropertyName = genspec.cloudFormationToScriptName(propName); this.docLink(spec.Documentation, additionalDocs); - this.code.line(`${javascriptPropertyName}${question}: ${this.findNativeType(context, spec)};`); + const nativeTypes = this.findNativeType(context, spec); + if (propName === 'Tags' && schema.isTagProperty(spec)) { + // allow tags to use special TagManagerToken + nativeTypes.push(genspec.TAG_MANAGER_NAME.fqn); + } else { + // any other type can use the alternative of a Token + nativeTypes.push(genspec.TOKEN_NAME.fqn); + } + this.code.line(`${javascriptPropertyName}${question}: ${nativeTypes.join(' | ')};`); return javascriptPropertyName; } @@ -543,26 +574,27 @@ export default class CodeGenerator { /** * Return the native type expression for the given propSpec */ - private findNativeType(resource: genspec.CodeName, propSpec: schema.Property): string { + private findNativeType(resource: genspec.CodeName, propSpec: schema.Property): string[] { const alternatives: string[] = []; if (schema.isCollectionProperty(propSpec)) { // render the union of all item types const itemTypes = genspec.specTypesToCodeTypes(resource.specName!, itemTypeNames(propSpec)); - // Always accept a token in place of any list element - itemTypes.push(genspec.TOKEN_NAME); + if (itemTypes.indexOf(genspec.TAG_NAME) === -1) { + // Always accept a token in place of any list element that is not a Tag + itemTypes.push(genspec.TOKEN_NAME); + } const union = this.renderTypeUnion(resource, itemTypes); if (schema.isMapProperty(propSpec)) { alternatives.push(`{ [key: string]: (${union}) }`); } else { // To make TSLint happy, we have to either emit: SingleType[] or Array - if (union.indexOf('|') !== -1) { alternatives.push(`Array<${union}>`); } else { - alternatives.push(`(${union})[]`); + alternatives.push(`${union}[]`); } } } @@ -574,11 +606,7 @@ export default class CodeGenerator { const types = genspec.specTypesToCodeTypes(resource.specName!, typeNames); alternatives.push(this.renderTypeUnion(resource, types)); } - - // Always - alternatives.push(genspec.TOKEN_NAME.fqn); - - return alternatives.join(' | '); + return alternatives; } private renderTypeUnion(context: genspec.CodeName, types: genspec.CodeName[]) { @@ -628,3 +656,16 @@ function mapperNames(types: genspec.CodeName[]): string { function quoteCode(code: string): string { return '``' + code + '``'; } + +function tagType(prop: schema.TagProperty): string { + if (schema.isTagPropertyStandard(prop)) { + return `${CORE}.${TAG_TYPE}.Standard`; + } + if (schema.isTagPropertyAutoScalingGroup(prop)) { + return `${CORE}.${TAG_TYPE}.AutoScalingGroup`; + } + if (schema.isTagPropertyJson(prop) || schema.isTagPropertyStringMap(prop)) { + return `${CORE}.${TAG_TYPE}.Map`; + } + return NOT_TAGGABLE; +} diff --git a/tools/cfn2ts/lib/genspec.ts b/tools/cfn2ts/lib/genspec.ts index 79828f35d869f..65d6151dd8777 100644 --- a/tools/cfn2ts/lib/genspec.ts +++ b/tools/cfn2ts/lib/genspec.ts @@ -119,6 +119,7 @@ export class AttributeTypeDeclaration { export const TAG_NAME = new CodeName('', CORE_NAMESPACE, 'Tag'); export const TOKEN_NAME = new CodeName('', CORE_NAMESPACE, 'Token'); +export const TAG_MANAGER_NAME = new CodeName('', CORE_NAMESPACE, 'TagManager'); /** * Resource attribute