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 dbd85436c8000..0b091505e01f4 100644 --- a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts +++ b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts @@ -137,11 +137,6 @@ export interface AutoScalingGroupProps { */ resourceSignalTimeoutSec?: number; - /** - * The AWS resource tags to associate with the ASG. - */ - tags?: cdk.Tags; - /** * Default scaling cooldown for this AutoScalingGroup * @@ -161,7 +156,7 @@ export interface AutoScalingGroupProps { * * The ASG spans all availability zones. */ -export class AutoScalingGroup extends cdk.Construct implements IAutoScalingGroup, cdk.ITaggable, elb.ILoadBalancerTarget, ec2.IConnectable, +export class AutoScalingGroup extends cdk.Construct implements IAutoScalingGroup, elb.ILoadBalancerTarget, ec2.IConnectable, elbv2.IApplicationLoadBalancerTarget, elbv2.INetworkLoadBalancerTarget { /** * The type of OS instances of this fleet are running. @@ -178,11 +173,6 @@ export class AutoScalingGroup extends cdk.Construct implements IAutoScalingGroup */ public readonly role: iam.Role; - /** - * Manage tags for this construct and children - */ - public readonly tags: cdk.TagManager; - /** * Name of the AutoScalingGroup */ @@ -209,8 +199,7 @@ export class AutoScalingGroup extends cdk.Construct implements IAutoScalingGroup }); this.connections = new ec2.Connections({ securityGroups: [this.securityGroup] }); this.securityGroups.push(this.securityGroup); - this.tags = new TagManager(this, {initialTags: props.tags}); - this.tags.setTag(NAME_TAG, this.node.path, { overwrite: false }); + this.apply(new cdk.Tag(NAME_TAG, this.node.path, { applyToLaunchInstances: true })); this.role = new iam.Role(this, 'InstanceRole', { assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com') @@ -255,7 +244,6 @@ export class AutoScalingGroup extends cdk.Construct implements IAutoScalingGroup launchConfigurationName: launchConfig.ref, loadBalancerNames: new cdk.Token(() => this.loadBalancerNames.length > 0 ? this.loadBalancerNames : undefined), targetGroupArns: new cdk.Token(() => this.targetGroupArns.length > 0 ? this.targetGroupArns : undefined), - tags: this.tags, }; if (props.notificationsTopic) { @@ -616,16 +604,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. */ @@ -748,4 +726,4 @@ export interface MetricTargetTrackingProps extends BaseTargetTrackingProps { * Value to keep the metric around */ targetValue: number; -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts b/packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts index a4dbb1bf20aed..710446ae53f88 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts @@ -9,7 +9,7 @@ import autoscaling = require('../lib'); export = { 'default fleet'(test: Test) { - const stack = new cdk.Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' }}); + const stack = getTestStack(); const vpc = mockVpc(stack); new autoscaling.AutoScalingGroup(stack, 'MyFleet', { @@ -18,6 +18,8 @@ export = { vpc }); + stack.testInvokeAspects(); + expect(stack).toMatch({ "Resources": { "MyFleetInstanceSecurityGroup774E8234": { @@ -365,7 +367,8 @@ export = { }, 'can set tags'(test: Test) { // GIVEN - const stack = new cdk.Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' }}); + const stack = getTestStack(); + // new cdk.Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' }}); const vpc = mockVpc(stack); // WHEN @@ -378,27 +381,28 @@ export = { minSuccessfulInstancesPercent: 50, pauseTimeSec: 345 }, - tags: {superfood: 'acai'}, }); - asg.tags.setTag('notsuper', 'caramel', {propagate: false}); + asg.apply( new cdk.Tag('superfood', 'acai')); + asg.apply( new cdk.Tag('notsuper', 'caramel', { applyToLaunchInstances: false })); + stack.testInvokeAspects(); // THEN expect(stack).to(haveResource("AWS::AutoScaling::AutoScalingGroup", { Tags: [ { - Key: 'superfood', - Value: 'acai', + Key: 'Name', PropagateAtLaunch: true, + Value: 'MyFleet', }, { - Key: 'Name', - Value: 'MyFleet', + Key: 'superfood', PropagateAtLaunch: true, + Value: 'acai', }, { Key: 'notsuper', - Value: 'caramel', PropagateAtLaunch: false, + Value: 'caramel', }, ] })); @@ -421,3 +425,12 @@ function mockSecurityGroup(stack: cdk.Stack) { securityGroupId: 'most-secure', }); } + +class TestStack extends cdk.Stack { + public testInvokeAspects(): void { + this.invokeAspects(); + } +} +function getTestStack(): TestStack { + return new TestStack(undefined, 'TestStack', { env: { account: '1234', region: 'us-east-1' } }); +} diff --git a/packages/@aws-cdk/aws-autoscaling/test/test.scheduled-action.ts b/packages/@aws-cdk/aws-autoscaling/test/test.scheduled-action.ts index 8d7433e33a866..770c57cc29c6a 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/test.scheduled-action.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/test.scheduled-action.ts @@ -47,7 +47,7 @@ export = { 'autoscaling group has recommended updatepolicy for scheduled actions'(test: Test) { // GIVEN - const stack = new cdk.Stack(); + const stack = getTestStack(); const asg = makeAutoScalingGroup(stack); // WHEN @@ -56,6 +56,7 @@ export = { minCapacity: 10, }); + stack.testInvokeAspects(); // THEN expect(stack).toMatch({ Resources: { @@ -111,4 +112,13 @@ function makeAutoScalingGroup(scope: cdk.Construct) { machineImage: new ec2.AmazonLinuxImage(), updateType: autoscaling.UpdateType.RollingUpdate, }); -} \ No newline at end of file +} + +class TestStack extends cdk.Stack { + public testInvokeAspects(): void { + this.invokeAspects(); + } +} +function getTestStack(): TestStack { + return new TestStack(undefined, 'TestStack', { env: { account: '1234', region: 'us-east-1' } }); +} diff --git a/packages/@aws-cdk/aws-ec2/lib/security-group.ts b/packages/@aws-cdk/aws-ec2/lib/security-group.ts index 0a22448141753..66371dd9a722d 100644 --- a/packages/@aws-cdk/aws-ec2/lib/security-group.ts +++ b/packages/@aws-cdk/aws-ec2/lib/security-group.ts @@ -1,4 +1,4 @@ -import { Construct, IConstruct, ITaggable, Output, TagManager, Tags, Token } from '@aws-cdk/cdk'; +import { Construct, IConstruct, Output, Token } from '@aws-cdk/cdk'; import { Connections, IConnectable } from './connections'; import { CfnSecurityGroup, CfnSecurityGroupEgress, CfnSecurityGroupIngress } from './ec2.generated'; import { IPortRange, ISecurityGroupRule } from './security-group-rule'; @@ -105,11 +105,6 @@ export interface SecurityGroupProps { */ description?: string; - /** - * The AWS resource tags to associate with the security group. - */ - tags?: Tags; - /** * The VPC in which to create the security group. */ @@ -134,7 +129,7 @@ export interface SecurityGroupProps { * inline ingress and egress rule (which saves on the total number of resources inside * the template). */ -export class SecurityGroup extends SecurityGroupBase implements ITaggable { +export class SecurityGroup extends SecurityGroupBase { /** * Import an existing SecurityGroup */ @@ -157,11 +152,6 @@ export class SecurityGroup extends SecurityGroupBase implements ITaggable { */ public readonly securityGroupId: string; - /** - * Manage tags for this construct and children - */ - public readonly tags: TagManager; - private readonly securityGroup: CfnSecurityGroup; private readonly directIngressRules: CfnSecurityGroup.IngressProperty[] = []; private readonly directEgressRules: CfnSecurityGroup.EgressProperty[] = []; @@ -171,7 +161,6 @@ export class SecurityGroup extends SecurityGroupBase implements ITaggable { constructor(scope: Construct, id: string, props: SecurityGroupProps) { super(scope, id); - this.tags = new TagManager(this, { initialTags: props.tags}); const groupDescription = props.description || this.node.path; this.allowAllOutbound = props.allowAllOutbound !== false; @@ -182,7 +171,6 @@ export class SecurityGroup extends SecurityGroupBase 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 875d22ba24483..30613085e09f2 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc.ts @@ -47,11 +47,6 @@ export interface VpcNetworkProps { */ defaultInstanceTenancy?: DefaultInstanceTenancy; - /** - * The AWS resource tags to associate with the VPC. - */ - tags?: cdk.Tags; - /** * Define the maximum number of AZs to use in this region * @@ -158,11 +153,6 @@ export interface SubnetConfiguration { * availability zone. */ name: string; - - /** - * The AWS resource tags to associate with the resource. - */ - tags?: cdk.Tags; } /** @@ -185,7 +175,7 @@ export interface SubnetConfiguration { * * } */ -export class VpcNetwork extends VpcNetworkBase implements cdk.ITaggable { +export class VpcNetwork extends VpcNetworkBase { /** * @returns The IPv4 CidrBlock as returned by the VPC */ @@ -255,11 +245,6 @@ export class VpcNetwork extends VpcNetworkBase implements cdk.ITaggable { */ public readonly availabilityZones: string[]; - /** - * Manage tags for this construct and children - */ - public readonly tags: cdk.TagManager; - /** * The VPC resource */ @@ -294,9 +279,6 @@ export class VpcNetwork extends VpcNetworkBase implements cdk.ITaggable { throw new Error('To use DNS Hostnames, DNS Support must be enabled, however, it was explicitly disabled.'); } - this.tags = new cdk.TagManager(this, { initialTags: props.tags}); - this.tags.setTag(NAME_TAG, this.node.path, { overwrite: false }); - const cidrBlock = ifUndefined(props.cidr, VpcNetwork.DEFAULT_CIDR_RANGE); this.networkBuilder = new NetworkBuilder(cidrBlock); @@ -310,9 +292,10 @@ export class VpcNetwork extends VpcNetworkBase implements cdk.ITaggable { enableDnsHostnames, enableDnsSupport, instanceTenancy, - tags: this.tags, }); + this.apply(new cdk.Tag(NAME_TAG, this.node.path)); + this.availabilityZones = new cdk.AvailabilityZoneProvider(this).availabilityZones; this.availabilityZones.sort(); if (props.maxAZs != null) { @@ -332,7 +315,6 @@ export class VpcNetwork extends VpcNetworkBase implements cdk.ITaggable { // Create an Internet Gateway and attach it if necessary if (allowOutbound) { const igw = new CfnInternetGateway(this, 'IGW', { - tags: new cdk.TagManager(this), }); this.internetDependencies.push(igw); const att = new CfnVPCGatewayAttachment(this, 'VPCGW', { @@ -440,7 +422,6 @@ export class VpcNetwork extends VpcNetworkBase implements cdk.ITaggable { vpcId: this.vpcId, cidrBlock: this.networkBuilder.addSubnet(cidrMask), mapPublicIpOnLaunch: (subnetConfig.subnetType === SubnetType.Public), - tags: subnetConfig.tags, }; let subnet: VpcSubnet; @@ -457,7 +438,6 @@ export class VpcNetwork extends VpcNetworkBase implements cdk.ITaggable { break; case SubnetType.Isolated: const isolatedSubnet = new VpcPrivateSubnet(this, name, subnetProps); - isolatedSubnet.tags.setTag(SUBNETTYPE_TAG, subnetTypeTagValue(subnetConfig.subnetType)); this.isolatedSubnets.push(isolatedSubnet); subnet = isolatedSubnet; break; @@ -466,8 +446,9 @@ export class VpcNetwork extends VpcNetworkBase implements cdk.ITaggable { } // These values will be used to recover the config upon provider import - subnet.tags.setTag(SUBNETNAME_TAG, subnetConfig.name, { propagate: false }); - subnet.tags.setTag(SUBNETTYPE_TAG, subnetTypeTagValue(subnetConfig.subnetType), { propagate: false }); + const include = [CfnSubnet.resourceTypeName]; + subnet.apply(new cdk.Tag(SUBNETNAME_TAG, subnetConfig.name, {include})); + subnet.apply(new cdk.Tag(SUBNETTYPE_TAG, subnetTypeTagValue(subnetConfig.subnetType), {include})); }); } } @@ -509,17 +490,12 @@ export interface VpcSubnetProps { * Defaults to true in Subnet.Public, false in Subnet.Private or Subnet.Isolated. */ mapPublicIpOnLaunch?: boolean; - - /** - * The AWS resource tags to associate with the Subnet - */ - tags?: cdk.Tags; } /** * Represents a new VPC subnet resource */ -export class VpcSubnet extends cdk.Construct implements IVpcSubnet, cdk.ITaggable, cdk.IDependable { +export class VpcSubnet extends cdk.Construct implements IVpcSubnet, cdk.IDependable { public static import(scope: cdk.Construct, id: string, props: VpcSubnetImportProps): IVpcSubnet { return new ImportedVpcSubnet(scope, id, props); } @@ -534,11 +510,6 @@ export class VpcSubnet extends cdk.Construct implements IVpcSubnet, cdk.ITaggabl */ public readonly subnetId: string; - /** - * Manage tags for Construct and propagate to children - */ - public readonly tags: cdk.TagManager; - /** * Parts of this VPC subnet */ @@ -551,8 +522,7 @@ export class VpcSubnet extends cdk.Construct implements IVpcSubnet, cdk.ITaggabl constructor(scope: cdk.Construct, id: string, props: VpcSubnetProps) { super(scope, id); - this.tags = new cdk.TagManager(this, {initialTags: props.tags}); - this.tags.setTag(NAME_TAG, this.node.path, {overwrite: false}); + this.apply(new cdk.Tag(NAME_TAG, this.node.path)); this.availabilityZone = props.availabilityZone; const subnet = new CfnSubnet(this, 'Subnet', { @@ -560,12 +530,10 @@ export class VpcSubnet extends cdk.Construct implements IVpcSubnet, cdk.ITaggabl cidrBlock: props.cidrBlock, availabilityZone: props.availabilityZone, mapPublicIpOnLaunch: props.mapPublicIpOnLaunch, - tags: this.tags, }); this.subnetId = subnet.subnetId; const table = new CfnRouteTable(this, 'RouteTable', { vpcId: props.vpcId, - tags: new cdk.TagManager(this), }); this.routeTableId = table.ref; @@ -639,7 +607,6 @@ export class VpcPublicSubnet extends VpcSubnet { allocationId: new CfnEIP(this, `EIP`, { domain: 'vpc' }).eipAllocationId, - tags: new cdk.TagManager(this), }); return ngw.natGatewayId; } @@ -709,4 +676,4 @@ class ImportedVpcSubnet extends cdk.Construct implements IVpcSubnet { public export() { return this.props; } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ec2/test/test.vpc.ts b/packages/@aws-cdk/aws-ec2/test/test.vpc.ts index afa4c91f45177..78041f057754b 100644 --- a/packages/@aws-cdk/aws-ec2/test/test.vpc.ts +++ b/packages/@aws-cdk/aws-ec2/test/test.vpc.ts @@ -1,7 +1,7 @@ import { countResources, expect, haveResource, haveResourceLike, isSuperObject } from '@aws-cdk/assert'; -import { AvailabilityZoneProvider, Construct, Stack, Tags } from '@aws-cdk/cdk'; +import { AvailabilityZoneProvider, Construct, Stack, Tag } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; -import { DefaultInstanceTenancy, IVpcNetwork, SubnetType, VpcNetwork } from '../lib'; +import { CfnVPC, DefaultInstanceTenancy, IVpcNetwork, SubnetType, VpcNetwork } from '../lib'; export = { "When creating a VPC": { @@ -28,8 +28,15 @@ 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'} ]))); + stack.testInvokeAspects(); + expect(stack).to( + haveResource('AWS::EC2::VPC', + hasTags( [ {Key: 'Name', Value: 'TheVPC'} ])) + ); + expect(stack).to( + haveResource('AWS::EC2::InternetGateway', + hasTags( [ {Key: 'Name', Value: 'TheVPC'} ])) + ); test.done(); }, @@ -37,28 +44,19 @@ export = { "with all of the properties set, it successfully sets the correct VPC properties"(test: Test) { const stack = getTestStack(); - const tags = { - first: 'foo', - second: 'bar', - third: 'barz', - - }; new VpcNetwork(stack, 'TheVPC', { cidr: "192.168.0.0/16", enableDnsHostnames: false, enableDnsSupport: false, defaultInstanceTenancy: DefaultInstanceTenancy.Dedicated, - tags, }); - const cfnTags = toCfnTags(tags); expect(stack).to(haveResource('AWS::EC2::VPC', { CidrBlock: '192.168.0.0/16', EnableDnsHostnames: false, EnableDnsSupport: false, InstanceTenancy: DefaultInstanceTenancy.Dedicated, })); - expect(stack).to(haveResource('AWS::EC2::VPC', hasTags(cfnTags))); test.done(); }, @@ -124,25 +122,21 @@ 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, }, - }, - { - 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,20 +145,14 @@ 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( - [ - { Key: 'type', Value: 'Public'}, - { Key: 'init', Value: 'No'}, - ], - ))); test.done(); }, "with custom subents and natGateways = 2 there should be only two NATGW"(test: Test) { @@ -287,16 +275,13 @@ export = { subnetName: 'egress' }, }); + stack.testInvokeAspects(); expect(stack).to(countResources("AWS::EC2::NatGateway", 3)); for (let i = 1; i < 4; i++) { - expect(stack).to(haveResource("AWS::EC2::NatGateway", { - Tags: [ - { - Key: 'Name', - Value: `VPC/egressSubnet${i}`, - } - ] - })); + expect(stack).to(haveResource('AWS::EC2::Subnet', hasTags([{ + Key: 'Name', + Value: `VPC/egressSubnet${i}`, + }]))); } test.done(); }, @@ -343,11 +328,13 @@ export = { const noPropTags = { BusinessUnit: 'Marketing', }; - const allTags: Tags = {...tags, ...noPropTags}; + const allTags = {...tags, ...noPropTags}; - const vpc = new VpcNetwork(stack, 'TheVPC', { tags: allTags }); + const vpc = new VpcNetwork(stack, 'TheVPC'); // overwrite to set propagate - vpc.tags.setTag('BusinessUnit', 'Marketing', {propagate: false}); + vpc.apply(new Tag('BusinessUnit', 'Marketing', {include: [CfnVPC.resourceTypeName]})); + vpc.apply(new Tag('VpcType', 'Good')); + stack.testInvokeAspects(); expect(stack).to(haveResource("AWS::EC2::VPC", hasTags(toCfnTags(allTags)))); const taggables = ['Subnet', 'InternetGateway', 'NatGateway', 'RouteTable']; const propTags = toCfnTags(tags); @@ -361,6 +348,7 @@ export = { 'Subnet Name will propagate to route tables and NATGW'(test: Test) { const stack = getTestStack(); const vpc = new VpcNetwork(stack, 'TheVPC'); + stack.testInvokeAspects(); for (const subnet of vpc.publicSubnets) { const tag = {Key: 'Name', Value: subnet.node.path}; expect(stack).to(haveResource('AWS::EC2::NatGateway', hasTags([tag]))); @@ -374,10 +362,13 @@ export = { }, 'Tags can be added after the Vpc is created with `vpc.tags.setTag(...)`'(test: Test) { const stack = getTestStack(); + const vpc = new VpcNetwork(stack, 'TheVPC'); const tag = {Key: 'Late', Value: 'Adder'}; + stack.testInvokeAspects(); expect(stack).notTo(haveResource('AWS::EC2::VPC', hasTags([tag]))); - vpc.tags.setTag(tag.Key, tag.Value); + vpc.apply(new Tag(tag.Key, tag.Value)); + stack.testInvokeAspects(); expect(stack).to(haveResource('AWS::EC2::VPC', hasTags([tag]))); test.done(); }, @@ -526,8 +517,14 @@ export = { }, }; -function getTestStack(): Stack { - return new Stack(undefined, 'TestStack', { env: { account: '123456789012', region: 'us-east-1' } }); +class TestStack extends Stack { + public testInvokeAspects(): void { + this.invokeAspects(); + } +} + +function getTestStack(): TestStack { + return new TestStack(undefined, 'TestStack', { env: { account: '123456789012', region: 'us-east-1' } }); } /** @@ -544,7 +541,7 @@ function doImportExportTest(constructFn: (scope: Construct) => VpcNetwork): IVpc return VpcNetwork.import(stack2, 'VPC2', vpc1.export()); } -function toCfnTags(tags: Tags): Array<{Key: string, Value: string}> { +function toCfnTags(tags: any): Array<{Key: string, Value: string}> { return Object.keys(tags).map( key => { return {Key: key, Value: tags[key]}; }); @@ -567,8 +564,11 @@ function hasTags(expectedTags: Array<{Key: string, Value: string}>): (props: any }); return actualTags.length === expectedTags.length; } catch (e) { - // tslint:disable-next-line:no-console - console.error('Invalid Tags array in ', props); + // tslint:disable:no-console + console.error('Tags are incorrect'); + console.error('found tags ', props.Tags); + console.error('expected tags ', expectedTags); + // tslint:enable:no-console throw e; } }; diff --git a/packages/@aws-cdk/aws-kms/lib/key.ts b/packages/@aws-cdk/aws-kms/lib/key.ts index 7b259bfb8fc25..20c8f4506a77b 100644 --- a/packages/@aws-cdk/aws-kms/lib/key.ts +++ b/packages/@aws-cdk/aws-kms/lib/key.ts @@ -1,5 +1,5 @@ import { PolicyDocument, PolicyStatement } from '@aws-cdk/aws-iam'; -import { Construct, DeletionPolicy, IConstruct, Output, TagManager, Tags } from '@aws-cdk/cdk'; +import { Construct, DeletionPolicy, IConstruct, Output } from '@aws-cdk/cdk'; import { EncryptionKeyAlias } from './alias'; import { CfnKey } from './kms.generated'; @@ -106,11 +106,6 @@ export interface EncryptionKeyProps { * administer the key will be created. */ policy?: PolicyDocument; - - /** - * The AWS resource tags to associate with the KMS key. - */ - tags?: Tags; } /** @@ -139,11 +134,6 @@ export class EncryptionKey extends EncryptionKeyBase { return new ImportedEncryptionKey(scope, id, props); } - /** - * Manage tags for this construct and children - */ - public readonly tags: TagManager; - public readonly keyArn: string; protected readonly policy?: PolicyDocument; @@ -157,14 +147,11 @@ export class EncryptionKey extends EncryptionKeyBase { this.allowAccountToAdmin(); } - this.tags = new TagManager(this, { initialTags: props.tags }); - const resource = new CfnKey(this, 'Resource', { description: props.description, enableKeyRotation: props.enableKeyRotation, enabled: props.enabled, keyPolicy: this.policy, - tags: this.tags }); this.keyArn = resource.keyArn; diff --git a/packages/@aws-cdk/aws-kms/test/test.key.ts b/packages/@aws-cdk/aws-kms/test/test.key.ts index 155347aa7a225..7ba07a37e171e 100644 --- a/packages/@aws-cdk/aws-kms/test/test.key.ts +++ b/packages/@aws-cdk/aws-kms/test/test.key.ts @@ -1,6 +1,6 @@ import { exactlyMatchTemplate, expect } from '@aws-cdk/assert'; import { PolicyDocument, PolicyStatement } from '@aws-cdk/aws-iam'; -import { App, Stack } from '@aws-cdk/cdk'; +import { App, Stack, Tag } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; import { EncryptionKey } from '../lib'; @@ -138,97 +138,99 @@ export = { }, 'key with some options'(test: Test) { + // const app = getTestApp(); const app = new App(); - const stack = new Stack(app, 'Test'); + // const stack = new Stack(app, 'Test'); + const stack = new TestStack(app, 'Test'); const key = new EncryptionKey(stack, 'MyKey', { enableKeyRotation: true, enabled: false, - tags: { - tag1: 'value1', - tag2: 'value2', - tag3: '' - } }); const p = new PolicyStatement().addAllResources().addAction('kms:encrypt'); p.addAwsPrincipal('arn'); key.addToResourcePolicy(p); + key.apply(new Tag('tag1', 'value1')); + key.apply(new Tag('tag2', 'value2')); + key.apply(new Tag('tag3', '')); + // app.testInvokeAspects(); + stack.testInvokeAspects(); expect(app.synthesizeStack(stack.name)).to(exactlyMatchTemplate({ Resources: { MyKey6AB29FA6: { - Type: "AWS::KMS::Key", - Properties: { - Enabled: false, - EnableKeyRotation: true, - KeyPolicy: { - Statement: [ - { - Action: [ - "kms:Create*", - "kms:Describe*", - "kms:Enable*", - "kms:List*", - "kms:Put*", - "kms:Update*", - "kms:Revoke*", - "kms:Disable*", - "kms:Get*", - "kms:Delete*", - "kms:ScheduleKeyDeletion", - "kms:CancelKeyDeletion" - ], - Effect: "Allow", - Principal: { - AWS: { - "Fn::Join": [ - "", - [ - "arn:", + Type: "AWS::KMS::Key", + Properties: { + KeyPolicy: { + Statement: [ { - Ref: "AWS::Partition" + Action: [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion" + ], + Effect: "Allow", + Principal: { + AWS: { + "Fn::Join": [ + "", + [ + "arn:", + { + Ref: "AWS::Partition" + }, + ":iam::", + { + Ref: "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + Resource: '*' }, - ":iam::", { - Ref: "AWS::AccountId" - }, - ":root" - ] - ] - } - }, - Resource: '*' - }, - { - Action: "kms:encrypt", - Effect: "Allow", - Principal: { - AWS: "arn" + Action: "kms:encrypt", + Effect: "Allow", + Principal: { + AWS: "arn" + }, + Resource: "*" + } + ], + Version: "2012-10-17" }, - Resource: "*" - } - ], - Version: "2012-10-17" + Enabled: false, + EnableKeyRotation: true, + Tags: [ + { + Key: "tag1", + Value: "value1" + }, + { + Key: "tag2", + Value: "value2" + }, + { + Key: "tag3", + Value: "" + } + ] }, - Tags: [ - { - Key: "tag1", - Value: "value1" - }, - { - Key: "tag2", - Value: "value2" - }, - { - Key: "tag3", - Value: "" - } - ] - }, - DeletionPolicy: "Retain" + DeletionPolicy: "Retain" } } - })); + })); test.done(); }, @@ -247,67 +249,67 @@ export = { test.deepEqual(app.synthesizeStack(stack.name).template, { Resources: { - MyKey6AB29FA6: { - Type: "AWS::KMS::Key", - Properties: { - EnableKeyRotation: true, - Enabled: false, - KeyPolicy: { - Statement: [ - { - Action: [ - "kms:Create*", - "kms:Describe*", - "kms:Enable*", - "kms:List*", - "kms:Put*", - "kms:Update*", - "kms:Revoke*", - "kms:Disable*", - "kms:Get*", - "kms:Delete*", - "kms:ScheduleKeyDeletion", - "kms:CancelKeyDeletion" - ], - Effect: "Allow", - Principal: { - AWS: { - "Fn::Join": [ - "", - [ - "arn:", - { - Ref: "AWS::Partition" - }, - ":iam::", + MyKey6AB29FA6: { + Type: "AWS::KMS::Key", + Properties: { + EnableKeyRotation: true, + Enabled: false, + KeyPolicy: { + Statement: [ { - Ref: "AWS::AccountId" - }, - ":root" - ] + Action: [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion" + ], + Effect: "Allow", + Principal: { + AWS: { + "Fn::Join": [ + "", + [ + "arn:", + { + Ref: "AWS::Partition" + }, + ":iam::", + { + Ref: "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + Resource: "*" + } + ], + Version: "2012-10-17" + } + }, + DeletionPolicy: "Retain" + }, + MyKeyAlias1B45D9DA: { + Type: "AWS::KMS::Alias", + Properties: { + AliasName: "alias/xoo", + TargetKeyId: { + "Fn::GetAtt": [ + "MyKey6AB29FA6", + "Arn" ] } - }, - Resource: "*" } - ], - Version: "2012-10-17" } - }, - DeletionPolicy: "Retain" - }, - MyKeyAlias1B45D9DA: { - Type: "AWS::KMS::Alias", - Properties: { - AliasName: "alias/xoo", - TargetKeyId: { - "Fn::GetAtt": [ - "MyKey6AB29FA6", - "Arn" - ] - } - } - } } }); @@ -362,16 +364,16 @@ export = { expect(stack2).toMatch({ Resources: { - MyKeyImportedAliasB1C5269F: { - Type: "AWS::KMS::Alias", - Properties: { - AliasName: "alias/hello", - TargetKeyId: { - "Fn::ImportValue": "MyKeyKeyArn317F1332" - } + MyKeyImportedAliasB1C5269F: { + Type: "AWS::KMS::Alias", + Properties: { + AliasName: "alias/hello", + TargetKeyId: { + "Fn::ImportValue": "MyKeyKeyArn317F1332" + } + } } } - } }); test.done(); @@ -403,3 +405,9 @@ export = { } } }; + +class TestStack extends Stack { + public testInvokeAspects(): void { + this.invokeAspects(); + } +} diff --git a/packages/@aws-cdk/cdk/lib/app.ts b/packages/@aws-cdk/cdk/lib/app.ts index dc38817aafba8..2d985ab006c2d 100644 --- a/packages/@aws-cdk/cdk/lib/app.ts +++ b/packages/@aws-cdk/cdk/lib/app.ts @@ -41,6 +41,9 @@ export class App extends Root { return; } + // invoke all aspects before rendering + this.invokeAspects(); + const result: cxapi.SynthesizeResponse = { version: cxapi.PROTO_RESPONSE_VERSION, stacks: this.synthesizeStacks(Object.keys(this.stacks)), @@ -232,4 +235,4 @@ function getJsiiAgentVersion() { function noEmptyArray(xs: T[]): T[] | undefined { return xs.length > 0 ? xs : undefined; -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/cdk/lib/aspects/aspect.ts b/packages/@aws-cdk/cdk/lib/aspects/aspect.ts new file mode 100644 index 0000000000000..b54e87303da9b --- /dev/null +++ b/packages/@aws-cdk/cdk/lib/aspects/aspect.ts @@ -0,0 +1,38 @@ +import { IConstruct } from '../core/construct'; + +/** + * Represents an Aspect + */ +export interface IAspect { + /** + * The type of Aspect + */ + readonly type: string; + + /** + * All aspects can visit by IConstructs + */ + visit(node: IConstruct): void; +} + +/** + * TODO: description + */ +export abstract class Aspect implements IAspect { + public abstract readonly type: string; + private readonly visitedBy: {[id: string]: boolean} = {}; + + public visit(construct: IConstruct): void { + if (this.visitedBy[construct.node.uniqueId] === true) { + return; + } + this.visitedBy[construct.node.uniqueId] = true; + this.visitAction(construct); + for (const child of construct.node.children) { + // recurse through all children + this.visit(child); + } + } + + protected abstract visitAction(construct: IConstruct): void; +} diff --git a/packages/@aws-cdk/cdk/lib/aspects/tag-aspect.ts b/packages/@aws-cdk/cdk/lib/aspects/tag-aspect.ts new file mode 100644 index 0000000000000..fdbc572251266 --- /dev/null +++ b/packages/@aws-cdk/cdk/lib/aspects/tag-aspect.ts @@ -0,0 +1,86 @@ +import { ITaggable, Resource } from '../cloudformation/resource'; +import { IConstruct } from '../core/construct'; +import { Aspect } from './aspect'; + +export interface TagBaseProps extends TagProperties { + // TODO docs + value?: string; +} + +export interface TagProperties { + // TODO docs + applyToLaunchInstances?: boolean; + include?: string[]; + exclude?: string[]; +} + +export abstract class TagBase extends Aspect { + + public static isResource(resource: any): resource is Resource { + return resource.resourceType !== undefined; + } + + public readonly type: string = 'taggable'; + + public readonly key: string; + public readonly value?: string; + + private readonly include: string[]; + private readonly exclude: string[]; + + constructor(key: string, props: TagBaseProps = {}) { + super(); + this.key = key; + + this.value = props.value; + this.include = props.include || []; + this.exclude = props.exclude || []; + } + + protected visitAction(construct: IConstruct): void { + if (!TagBase.isResource(construct)) { + return; + } + const resource = construct as Resource; + if (Resource.isTaggable(resource)) { + if (this.exclude.length !== 0 && this.exclude.indexOf(resource.resourceType) !== -1) { + return; + } + if (this.include.length !== 0 && this.include.indexOf(resource.resourceType) === -1) { + return; + } + this.applyTag(resource); + } + } + + protected abstract applyTag(resource: ITaggable): void; +} + +export class Tag extends TagBase { + + private readonly applyToLaunchInstances: boolean; + + constructor(key: string, value: string, props: TagProperties = {}) { + super(key, {value, ...props}); + this.applyToLaunchInstances = props.applyToLaunchInstances !== false; + if (this.value === undefined) { + throw new Error('Tag must have a value'); + } + } + + protected applyTag(resource: ITaggable) { + resource.tags.setTag(this.key, this.value!, {applyToLaunchInstances: this.applyToLaunchInstances}); + } +} + +export class RemoveTag extends TagBase { + + constructor(key: string, props: TagProperties = {}) { + super(key, props); + } + + protected applyTag(resource: ITaggable): void { + resource.tags.removeTag(this.key); + return; + } +} diff --git a/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts b/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts index a2a4243dde2ac..390b1dcd5175e 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/resource.ts @@ -1,5 +1,6 @@ import cxapi = require('@aws-cdk/cx-api'); import { Construct } from '../core/construct'; +import { TagManager } from '../core/tag-manager'; import { capitalizePropertyNames, ignoreEmpty } from '../core/util'; import { CfnReference } from './cfn-tokens'; import { Condition } from './condition'; @@ -18,6 +19,12 @@ export interface ResourceProps { properties?: any; } +export interface ITaggable { + /** + * TagManager to set, remove and format tags + */ + readonly tags: TagManager; +} /** * Represents a CloudFormation resource. */ @@ -38,6 +45,13 @@ export class Resource extends Referenceable { }; } + /** + * determines if the reosurce is taggable + */ + public static isTaggable(resource: any): resource is ITaggable { + return resource.tags !== undefined; + } + /** * Options for this resource, such as condition, update policy etc. */ @@ -48,6 +62,13 @@ export class Resource extends Referenceable { */ public readonly resourceType: string; + /** + * AWS resource properties. + * + * This object is rendered via a call to "renderProperties(this.properties)". + */ + public readonly properties: any; + /** * AWS resource property overrides. * @@ -60,13 +81,6 @@ export class Resource extends Referenceable { */ protected readonly untypedPropertyOverrides: any = { }; - /** - * AWS resource properties. - * - * This object is rendered via a call to "renderProperties(this.properties)". - */ - protected readonly properties: any; - /** * An object to be merged on top of the entire resource definition. */ @@ -180,6 +194,10 @@ export class Resource extends Referenceable { */ public toCloudFormation(): object { try { + if (Resource.isTaggable(this)) { + const tags = this.tags.renderTags(); + this.properties.tags = tags === undefined ? this.properties.tags : tags; + } // merge property overrides onto properties and then render (and validate). const properties = this.renderProperties(deepMerge(this.properties || { }, this.untypedPropertyOverrides)); @@ -239,6 +257,13 @@ export class Resource extends Referenceable { } } +export enum TagType { + Standard = 'StandardTag', + AutoScalingGroup = 'AutoScalingGroupTag', + Map = 'StringToStringMap', + NotTaggable = 'NotTaggable', +} + 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/cloudformation/tag.ts b/packages/@aws-cdk/cdk/lib/cloudformation/tag.ts index 26b13b0b41d28..a05aa282335b9 100644 --- a/packages/@aws-cdk/cdk/lib/cloudformation/tag.ts +++ b/packages/@aws-cdk/cdk/lib/cloudformation/tag.ts @@ -1,7 +1,7 @@ /** * @link http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-resource-tags.html */ -export interface Tag { +export interface CfnTag { /** * @link http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-resource-tags.html#cfn-resource-tags-key */ diff --git a/packages/@aws-cdk/cdk/lib/core/construct.ts b/packages/@aws-cdk/cdk/lib/core/construct.ts index 9551618c24b46..f7a7887866cbe 100644 --- a/packages/@aws-cdk/cdk/lib/core/construct.ts +++ b/packages/@aws-cdk/cdk/lib/core/construct.ts @@ -1,4 +1,5 @@ import cxapi = require('@aws-cdk/cx-api'); +import { IAspect } from '../aspects/aspect'; import { CloudFormationJSON } from '../cloudformation/cloudformation-json'; import { makeUniqueId } from '../util/uniqueid'; import { Token, unresolved } from './tokens'; @@ -31,6 +32,11 @@ export class ConstructNode { */ public readonly id: string; + /** + * An array of aspects applied to this node + */ + public readonly aspects: IAspect[] = []; + /** * List of children and their names */ @@ -504,6 +510,14 @@ export class Construct implements IConstruct { return this.node.typename + (path.length > 0 ? ` [${path}]` : ''); } + /** + * Applies the aspect to this Constructs node + */ + public apply(aspect: IAspect): void { + this.node.aspects.push(aspect); + return; + } + /** * Validate the current construct. * @@ -529,6 +543,18 @@ export class Construct implements IConstruct { protected prepare(): void { // Intentionally left blank } + + /** + * Triggers each aspect to invoke visit + */ + protected invokeAspects(): void { + for (const aspect of this.node.aspects) { + aspect.visit(this); + } + for (const child of this.node.children) { + (child as Construct).invokeAspects(); + } + } } /** diff --git a/packages/@aws-cdk/cdk/lib/core/tag-manager.ts b/packages/@aws-cdk/cdk/lib/core/tag-manager.ts index 7f5f6c57d3814..a0d01b668d341 100644 --- a/packages/@aws-cdk/cdk/lib/core/tag-manager.ts +++ b/packages/@aws-cdk/cdk/lib/core/tag-manager.ts @@ -1,273 +1,85 @@ -import { Construct, IConstruct } from './construct'; -import { ResolveContext, Token } from './tokens'; - -/** - * ITaggable indicates a entity manages tags via the `tags` property - */ -export interface ITaggable { - readonly tags: TagManager, -} +import { TagType } from '../cloudformation/resource'; /** * Properties Tags is a dictionary of tags as strings */ -export type Tags = { [key: string]: string }; - -/** - * An object of tags with value and properties - * - * This is used internally but not exported - */ -interface FullTags { - [key: string]: {value: string, props?: TagProps}; -} +type Tags = { [key: string]: {value: string, props: TagProps }}; /** * Properties for a tag */ export interface TagProps { /** - * If true all child taggable `Constructs` will receive this tag - * - * @default true - */ - propagate?: boolean; - - /** - * If set propagated tags from parents will not overwrite the tag - * - * @default true - */ - sticky?: boolean; - - /** - * If set this tag will overwrite existing tags - * - * @default true - */ - overwrite?: boolean; -} - -/** - * This is the interface for arguments to `tagFormatResolve` to enable extensions - */ -export interface TagGroups { - /** - * Tags that overwrite ancestor tags - */ - stickyTags: Tags; - - /** - * Tags that are overwritten by ancestor tags - */ - nonStickyTags: Tags; - - /** - * Tags with propagate true not from an ancestor - */ - propagateTags: Tags; - - /** - * Tags that are propagated from ancestors - */ - ancestorTags: Tags; -} - -/** - * Properties for removing tags - */ -export interface RemoveProps { - /** - * If true prevent this tag form being set via propagation - * - * @default true - */ - blockPropagate?: boolean; -} - -/** - * Properties for Tag Manager - */ -export interface TagManagerProps { - /** - * Initial tags to set on the tag manager using TAG_DEFAULTS + * Handles AutoScalingGroup PropagateAtLaunch property */ - initialTags?: Tags; + applyToLaunchInstances: boolean; } /** * TagManager facilitates a common implementation of tagging for Constructs. - * - * Each construct that wants to support tags should implement the `ITaggable` - * interface and properly pass tags to the `Resources` (Cloudformation) elements - * the `Construct` creates. The `TagManager` extends `Token` the object can be - * passed directly to `Resources` that support tag properties. - * - * There are a few standard use cases the `TagManager` supports for managing - * tags across the resources in your stack. - * - * Propagation: If you tag a resource and it has children, by default those tags - * will be propagated to the children. This is controlled by - * `TagProps.propagate`. - * - * Default a tag unless an ancestor has a value: There are situations where a - * construct author might want to set a tag value, but choose to take a parents - * value. For example, you might default `{Key: "Compliance", Value: "None"}`, - * but if a parent has `{Key: "Compliance", Value: "PCI"}` allow that parent to - * override your tag. This is can be done by setting `TagProps.sticky` to false. - * The default behavior is that child tags have precedence and `TagProps.sticky` - * defaults to true to reflect this. - * - * Overwrite: Construct authors have the need to set a tag, but only if one was - * not provided by the consumer. The most common example is the `Name` tag. - * Overwrite is for this purpose and is controlled by `TagProps.overwrite`. The - * default is `true`. - * - * Removing Tags: Tags can be removed from the local manager via `removeTag`. If - * a parent also has a tag with the same name then it can be propagated to the - * child (after removal). The user can control this `RemoveProps.blockPropagate`. By default - * this is `true` and prevents a parent tag from propagating to the child after - * the `removeTag` is invoked. However, if user wants the parent tag to - * propagate, if it is provided by a parent this can be set to `false`. */ -export class TagManager extends Token { +export class TagManager { - /** - * Checks if the object implements the `ITaggable` interface - */ - public static isTaggable(taggable: ITaggable | any): taggable is ITaggable { - return ((taggable as ITaggable).tags !== undefined); - } - - private static readonly DEFAULT_TAG_PROPS: TagProps = { - propagate: true, - sticky: true, - overwrite: true - }; + private readonly tags: Tags = {}; - /* - * Internally tags will have properties set - */ - private readonly _tags: FullTags = {}; - - /* - * Tags that will be removed during `tags` method - */ - private readonly blockedTags: string[] = []; - - constructor(private readonly scope: Construct, props: TagManagerProps = {}) { - super(); - - const initialTags = props.initialTags || {}; - for (const key of Object.keys(initialTags)) { - const tag = { - value: initialTags[key], - props: TagManager.DEFAULT_TAG_PROPS, - }; - this._tags[key] = tag; - } - } - - /** - * Converts the `tags` to a Token for use in lazy evaluation - */ - public resolve(_context: ResolveContext): any { - // need this for scoping - const blockedTags = this.blockedTags; - function filterTags(_tags: FullTags, filter: TagProps = {}): Tags { - const filteredTags: Tags = {}; - Object.keys(_tags).map( key => { - let filterResult = true; - const props: TagProps = _tags[key].props || {}; - if (filter.propagate !== undefined) { - filterResult = filterResult && (filter.propagate === props.propagate); - } - if (filter.sticky !== undefined) { - filterResult = filterResult && - (filter.sticky === props.sticky); - } - if (filter.overwrite !== undefined) { - filterResult = filterResult && (filter.overwrite === props.overwrite); - } - if (filterResult) { - filteredTags[key] = _tags[key].value; - } - }); - for (const key of blockedTags) { delete filteredTags[key]; } - return filteredTags; - } - - function propagatedTags(tagProviders: IConstruct[]): Tags { - const parentTags: Tags = {}; - for (const ancestor of tagProviders) { - if (TagManager.isTaggable(ancestor)) { - const tagsFrom = filterTags(ancestor.tags._tags, {propagate: true}); - Object.assign(parentTags, tagsFrom); - } - } - for (const key of blockedTags) { delete parentTags[key]; } - return parentTags; - } - - const nonStickyTags = filterTags(this._tags, {sticky: false}); - const stickyTags = filterTags(this._tags, {sticky: true}); - const ancestors = this.scope.node.ancestors(); - const ancestorTags = propagatedTags(ancestors); - const propagateTags = filterTags(this._tags, {propagate: true}); - return this.tagFormatResolve( { - ancestorTags, - nonStickyTags, - stickyTags, - propagateTags, - }); - } + constructor(private readonly tagType: TagType) { } /** * 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` + * @param props A `TagProps` defaulted to applyToLaunchInstances true */ - 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); - } + public setTag(key: string, value: string, props: TagProps = {applyToLaunchInstances: true}): void { + this.tags[key] = { value, props }; } /** * 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]; + public removeTag(key: string): void { + // tslint:disable-next-line:no-console + console.log(`removing ${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; + * Renders tags into the proper format based on TagType + */ + public renderTags(): any { + const keys = Object.keys(this.tags); + switch (this.tagType) { + case TagType.Standard: { + const tags: Array<{key: string, value: string}> = []; + for (const key of keys) { + tags.push({key, value: this.tags[key].value}); + } + return tags.length === 0 ? undefined : tags; + } + case TagType.AutoScalingGroup: { + const tags: Array<{key: string, value: string, propagateAtLaunch: boolean}> = []; + for (const key of keys) { + tags.push({ + key, + value: this.tags[key].value, + propagateAtLaunch: this.tags[key].props.applyToLaunchInstances} + ); + } + return tags.length === 0 ? undefined : tags; + } + case TagType.Map: { + const tags: {[key: string]: string} = {}; + for (const key of keys) { + tags[key] = this.tags[key].value; + } + return Object.keys(tags).length === 0 ? undefined : tags; + } + case TagType.NotTaggable: { + return undefined; + } } - return Object.keys(tags).map( key => ({key, value: tags[key]})); } } diff --git a/packages/@aws-cdk/cdk/lib/index.ts b/packages/@aws-cdk/cdk/lib/index.ts index 72e4e00592b2d..c542da89697e6 100644 --- a/packages/@aws-cdk/cdk/lib/index.ts +++ b/packages/@aws-cdk/cdk/lib/index.ts @@ -1,3 +1,6 @@ +export * from './aspects/aspect'; +export * from './aspects/tag-aspect'; + export * from './core/construct'; export * from './core/tokens'; export * from './core/tag-manager'; diff --git a/packages/@aws-cdk/cdk/lib/runtime.ts b/packages/@aws-cdk/cdk/lib/runtime.ts index e8fb8ba893064..00d55a2cdcfbe 100644 --- a/packages/@aws-cdk/cdk/lib/runtime.ts +++ b/packages/@aws-cdk/cdk/lib/runtime.ts @@ -48,7 +48,7 @@ function pad(x: number) { /** * Turn a tag object into the proper CloudFormation representation */ -export function tagToCloudFormation(x: any): any { +export function cfnTagToCloudFormation(x: any): any { return { Key: x.key, Value: x.value @@ -249,7 +249,7 @@ export function validateObject(x: any): ValidationResult { return VALIDATION_SUCCESS; } -export function validateTag(x: any): ValidationResult { +export function validateCfnTag(x: any): ValidationResult { if (!canInspect(x)) { return VALIDATION_SUCCESS; } if (x.key == null || x.value == null) { diff --git a/packages/@aws-cdk/cdk/test/aspects/test.tag-aspect.ts b/packages/@aws-cdk/cdk/test/aspects/test.tag-aspect.ts new file mode 100644 index 0000000000000..e4b2909c6faf4 --- /dev/null +++ b/packages/@aws-cdk/cdk/test/aspects/test.tag-aspect.ts @@ -0,0 +1,180 @@ +import { Test } from 'nodeunit'; +import { RemoveTag, Resource, Stack, Tag, TagManager, TagType } from '../../lib'; + +class TaggableResource extends Resource { + public readonly tags = new TagManager(TagType.Standard); +} + +class AsgTaggableResource extends Resource { + public readonly tags = new TagManager(TagType.AutoScalingGroup); +} + +class MapTaggableResource extends Resource { + public readonly tags = new TagManager(TagType.Map); +} + +class TestRoot extends Stack { + constructor() { + super(undefined, 'TestStack', { env: { account: '123456789012', region: 'us-east-1' } }); + } + public testInvokeAspects() { + this.invokeAspects(); + } +} + +export = { + 'Tag visit all children of the applied node'(test: Test) { + const root = new TestRoot(); + const res = new TaggableResource(root, 'FakeResource', { + type: 'AWS::Fake::Thing', + }); + const res2 = new TaggableResource(res, 'FakeResource', { + type: 'AWS::Fake::Thing', + }); + const asg = new AsgTaggableResource(res, 'AsgFakeResource', { + type: 'AWS::Fake::Thing', + }); + + const map = new MapTaggableResource(res, 'MapFakeResource', { + type: 'AWS::Fake::Thing', + }); + res.apply(new Tag('foo', 'bar')); + test.deepEqual(res.node.aspects.length, 1); + root.testInvokeAspects(); + test.deepEqual(res.tags.renderTags(), [{key: 'foo', value: 'bar'}]); + test.deepEqual(res2.tags.renderTags(), [{key: 'foo', value: 'bar'}]); + test.deepEqual(map.tags.renderTags(), {foo: 'bar'}); + test.deepEqual(asg.tags.renderTags(), [{key: 'foo', value: 'bar', propagateAtLaunch: true}]); + test.done(); + }, + 'The last aspect applied takes precedence'(test: Test) { + const root = new TestRoot(); + const res = new TaggableResource(root, 'FakeResource', { + type: 'AWS::Fake::Thing', + }); + const res2 = new TaggableResource(res, 'FakeResource', { + type: 'AWS::Fake::Thing', + }); + res.apply(new Tag('foo', 'bar')); + res.apply(new Tag('foo', 'foobar')); + res.apply(new Tag('foo', 'baz')); + res2.apply(new Tag('foo', 'good')); + root.testInvokeAspects(); + test.deepEqual(res.tags.renderTags(), [{key: 'foo', value: 'baz'}]); + test.deepEqual(res2.tags.renderTags(), [{key: 'foo', value: 'good'}]); + test.done(); + }, + 'RemoveTag will remove a tag if it exists'(test: Test) { + const root = new TestRoot(); + const res = new TaggableResource(root, 'FakeResource', { + type: 'AWS::Fake::Thing', + }); + const res2 = new TaggableResource(res, 'FakeResource', { + type: 'AWS::Fake::Thing', + }); + const asg = new AsgTaggableResource(res, 'AsgFakeResource', { + type: 'AWS::Fake::Thing', + }); + + const map = new MapTaggableResource(res, 'MapFakeResource', { + type: 'AWS::Fake::Thing', + }); + root.apply(new Tag('root', 'was here')); + res.apply(new Tag('first', 'there is only 1')); + res.apply(new RemoveTag('root')); + res.apply(new RemoveTag('doesnotexist')); + root.testInvokeAspects(); + + test.deepEqual(res.tags.renderTags(), [{key: 'first', value: 'there is only 1'}]); + test.deepEqual(map.tags.renderTags(), {first: 'there is only 1'}); + test.deepEqual(asg.tags.renderTags(), [{key: 'first', value: 'there is only 1', propagateAtLaunch: true}]); + test.deepEqual(res2.tags.renderTags(), [{key: 'first', value: 'there is only 1'}]); + test.done(); + }, + 'the #visit function is idempotent'(test: Test) { + const root = new TestRoot(); + const res = new TaggableResource(root, 'FakeResource', { + type: 'AWS::Fake::Thing', + }); + + res.apply(new Tag('foo', 'bar')); + root.testInvokeAspects(); + test.deepEqual(res.tags.renderTags(), [{key: 'foo', value: 'bar'}]); + root.testInvokeAspects(); + test.deepEqual(res.tags.renderTags(), [{key: 'foo', value: 'bar'}]); + root.testInvokeAspects(); + test.deepEqual(res.tags.renderTags(), [{key: 'foo', value: 'bar'}]); + test.done(); + }, + 'include restricts tag application to resources types in the list'(test: Test) { + const root = new TestRoot(); + const res = new TaggableResource(root, 'FakeResource', { + type: 'AWS::Fake::Thing', + }); + const res2 = new TaggableResource(res, 'FakeResource', { + type: 'AWS::Fake::Thing', + }); + const asg = new AsgTaggableResource(res, 'AsgFakeResource', { + type: 'AWS::Fake::Asg', + }); + + const map = new MapTaggableResource(res, 'MapFakeResource', { + type: 'AWS::Fake::Map', + }); + res.apply(new Tag('foo', 'bar', {include: ['AWS::Fake::Asg']})); + root.testInvokeAspects(); + test.deepEqual(res.tags.renderTags(), undefined); + test.deepEqual(map.tags.renderTags(), undefined); + test.deepEqual(res2.tags.renderTags(), undefined); + test.deepEqual(asg.tags.renderTags(), [{key: 'foo', value: 'bar', propagateAtLaunch: true}]); + test.deepEqual(map.properties.tags, undefined); + test.done(); + }, + 'exclude prevents tag application to resource types in the list'(test: Test) { + const root = new TestRoot(); + const res = new TaggableResource(root, 'FakeResource', { + type: 'AWS::Fake::Thing', + }); + const res2 = new TaggableResource(res, 'FakeResource', { + type: 'AWS::Fake::Thing', + }); + const asg = new AsgTaggableResource(res, 'AsgFakeResource', { + type: 'AWS::Fake::Asg', + }); + + const map = new MapTaggableResource(res, 'MapFakeResource', { + type: 'AWS::Fake::Map', + }); + res.apply(new Tag('foo', 'bar', {exclude: ['AWS::Fake::Asg']})); + root.testInvokeAspects(); + test.deepEqual(res.tags.renderTags(), [{key: 'foo', value: 'bar'}]); + test.deepEqual(res2.tags.renderTags(), [{key: 'foo', value: 'bar'}]); + test.deepEqual(asg.tags.renderTags(), undefined); + test.deepEqual(map.tags.renderTags(), {foo: 'bar'}); + test.done(); + }, + 'Aspects are mutually exclusive with tags created by L1 Constructor'(test: Test) { + const root = new TestRoot(); + const aspectBranch = new TaggableResource(root, 'FakeBranchA', { + type: 'AWS::Fake::Thing', + properties: { + tags: [ + {key: 'cfn', value: 'is cool'}, + ], + }, + }); + const cfnBranch = new TaggableResource(root, 'FakeBranchB', { + type: 'AWS::Fake::Thing', + properties: { + tags: [ + {key: 'cfn', value: 'is cool'}, + ], + }, + }); + aspectBranch.apply(new Tag('aspects', 'rule')); + root.testInvokeAspects(); + test.deepEqual(aspectBranch.tags.renderTags(), [{key: 'aspects', value: 'rule'}]); + test.deepEqual(cfnBranch.properties.tags, [{key: 'cfn', value: 'is cool'}]); + test.done(); + }, +}; diff --git a/packages/@aws-cdk/cdk/test/cloudformation/test.resource.ts b/packages/@aws-cdk/cdk/test/cloudformation/test.resource.ts index 3b0ee595aa35a..6897e7996edfc 100644 --- a/packages/@aws-cdk/cdk/test/cloudformation/test.resource.ts +++ b/packages/@aws-cdk/cdk/test/cloudformation/test.resource.ts @@ -346,12 +346,12 @@ export = { MyC3C2R2F213BD26: { Type: 'T2' }, MyC3C2R38CE6F9F7: { Type: 'T3' }, MyResource: - { Type: 'R', - DependsOn: + { Type: 'R', + DependsOn: [ 'MyC1R1FB2A562F', - 'MyC1R2AE2B5066', - 'MyC2R3809EEAD6', - 'MyC3C2R38CE6F9F7' ] } } }); + 'MyC1R2AE2B5066', + 'MyC2R3809EEAD6', + 'MyC3C2R38CE6F9F7' ] } } }); test.done(); }, @@ -377,9 +377,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(); }, @@ -406,8 +406,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(); }, @@ -434,8 +434,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(); }, @@ -453,8 +453,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(); }, @@ -483,8 +483,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(); }, @@ -509,12 +509,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(); }, @@ -532,8 +532,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(); }, @@ -550,8 +550,8 @@ export = { test.deepEqual(stack.toCloudFormation(), { Resources: { MyResource: - { Type: 'MyResourceType', - Properties: { PROP1: 'foo', PROP2: 'bar' } } } }); + { Type: 'MyResourceType', + Properties: { PROP1: 'foo', PROP2: 'bar' } } } }); test.done(); }, @@ -564,8 +564,8 @@ export = { test.deepEqual(stack.toCloudFormation(), { Resources: { MyResource: - { Type: 'MyResourceType', - Properties: { PROP3: 'zoo' } } } }); + { Type: 'MyResourceType', + Properties: { PROP3: 'zoo' } } } }); test.done(); }, @@ -579,10 +579,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 71af1326f6b7b..8997c8a6e2641 100644 --- a/packages/@aws-cdk/cdk/test/core/test.tag-manager.ts +++ b/packages/@aws-cdk/cdk/test/core/test.tag-manager.ts @@ -1,180 +1,38 @@ import { Test } from 'nodeunit'; -import { Construct, ITaggable, Root, TagManager } from '../../lib'; - -class ChildTagger extends Construct implements ITaggable { - public readonly tags: TagManager; - constructor(scope: Construct, id: string) { - super(scope, id); - this.tags = new TagManager(scope); - } -} - -class Child extends Construct { - constructor(scope: Construct, id: string) { - super(scope, id); - } -} +import { TagType } from '../../lib/cloudformation/resource'; +import { TagManager } from '../../lib/core/tag-manager'; + +// class TaggableResource extends Resource { +// public readonly tagType = TagType.Standard; +// } +// +// class AsgTaggableResource extends Resource { +// public readonly tagType = TagType.AutoScalingGroup; +// } +// +// class MapTaggableResource extends Resource { +// public readonly tagType = TagType.Map; +// } export = { - 'TagManger handles tags for a Contruct Tree': { - 'setTag by default propagates to children'(test: Test) { - const root = new Root(); - const ctagger = new ChildTagger(root, 'one'); - const ctagger1 = new ChildTagger(ctagger, 'two'); - const ctagger2 = new ChildTagger(root, 'three'); - - // not taggable at all - new Child(ctagger, 'notag'); - - const tag = {key: 'Name', value: 'TheCakeIsALie'}; - ctagger.tags.setTag(tag.key, tag.value); - - const tagArray = [tag]; - for (const construct of [ctagger, ctagger1]) { - test.deepEqual(root.node.resolve(construct.tags), tagArray); - } - - test.deepEqual(root.node.resolve(ctagger2.tags), undefined); - test.done(); - }, - 'setTag with propagate false tags do not propagate'(test: Test) { - const root = new Root(); - const ctagger = new ChildTagger(root, 'one'); - const ctagger1 = new ChildTagger(ctagger, 'two'); - const ctagger2 = new ChildTagger(root, 'three'); - - // not taggable at all - new Child(ctagger, 'notag'); - - const tag = {key: 'Name', value: 'TheCakeIsALie'}; - ctagger.tags.setTag(tag.key, tag.value, {propagate: false}); - - for (const construct of [ctagger1, ctagger2]) { - test.deepEqual(root.node.resolve(construct.tags), undefined); - } - test.deepEqual(root.node.resolve(ctagger.tags)[0].key, 'Name'); - test.deepEqual(root.node.resolve(ctagger.tags)[0].value, 'TheCakeIsALie'); - test.done(); - }, - 'setTag with overwrite false does not overwrite a tag'(test: Test) { - const root = new Root(); - const ctagger = new ChildTagger(root, 'one'); - ctagger.tags.setTag('Env', 'Dev'); - ctagger.tags.setTag('Env', 'Prod', {overwrite: false}); - const result = root.node.resolve(ctagger.tags); - test.deepEqual(result, [{key: 'Env', value: 'Dev'}]); - test.done(); - }, - 'setTag with sticky false enables propagations to overwrite child tags'(test: Test) { - const root = new Root(); - const ctagger = new ChildTagger(root, 'one'); - const ctagger1 = new ChildTagger(ctagger, 'two'); - ctagger.tags.setTag('Parent', 'Is always right'); - ctagger1.tags.setTag('Parent', 'Is wrong', {sticky: false}); - const parent = root.node.resolve(ctagger.tags); - const child = root.node.resolve(ctagger1.tags); - test.deepEqual(parent, child); - test.done(); - - }, - 'tags propagate from all parents'(test: Test) { - const root = new Root(); - const ctagger = new ChildTagger(root, 'one'); - new ChildTagger(ctagger, 'two'); - const cNoTag = new Child(ctagger, 'three'); - const ctagger2 = new ChildTagger(cNoTag, 'four'); - const tag = {key: 'Name', value: 'TheCakeIsALie'}; - ctagger.tags.setTag(tag.key, tag.value, {propagate: true}); - test.deepEqual(root.node.resolve(ctagger2.tags), [tag]); - test.done(); - }, - 'a tag can be removed and added back'(test: Test) { - const root = new Root(); - const ctagger = new ChildTagger(root, 'one'); - const tag = {key: 'Name', value: 'TheCakeIsALie'}; - ctagger.tags.setTag(tag.key, tag.value, {propagate: true}); - test.deepEqual(root.node.resolve(ctagger.tags), [tag]); - ctagger.tags.removeTag(tag.key); - test.deepEqual(root.node.resolve(ctagger.tags), undefined); - ctagger.tags.setTag(tag.key, tag.value, {propagate: true}); - test.deepEqual(root.node.resolve(ctagger.tags), [tag]); - test.done(); - }, - 'removeTag removes a tag by key'(test: Test) { - const root = new Root(); - const ctagger = new ChildTagger(root, 'one'); - const ctagger1 = new ChildTagger(ctagger, 'two'); - const ctagger2 = new ChildTagger(root, 'three'); - - // not taggable at all - new Child(ctagger, 'notag'); - - const tag = {key: 'Name', value: 'TheCakeIsALie'}; - ctagger.tags.setTag(tag.key, tag.value); - ctagger.tags.removeTag('Name'); - - for (const construct of [ctagger, ctagger1, ctagger2]) { - test.deepEqual(root.node.resolve(construct.tags), undefined); - } - test.done(); - }, - 'removeTag with blockPropagate removes any propagated tags'(test: Test) { - const root = new Root(); - const ctagger = new ChildTagger(root, 'one'); - const ctagger1 = new ChildTagger(ctagger, 'two'); - ctagger.tags.setTag('Env', 'Dev'); - ctagger1.tags.removeTag('Env', {blockPropagate: true}); - const result = root.node.resolve(ctagger.tags); - test.deepEqual(result, [{key: 'Env', value: 'Dev'}]); - test.deepEqual(root.node.resolve(ctagger1.tags), undefined); - test.done(); - }, - 'children can override parent propagated tags'(test: Test) { - const root = new Root(); - const ctagger = new ChildTagger(root, 'one'); - const ctagChild = new ChildTagger(ctagger, 'one'); - const tag = {key: 'BestBeach', value: 'StoneSteps'}; - const tag2 = {key: 'BestBeach', value: 'k-38'}; - ctagger.tags.setTag(tag2.key, tag2.value); - ctagger.tags.setTag(tag.key, tag.value); - ctagChild.tags.setTag(tag2.key, tag2.value); - const parentTags = root.node.resolve(ctagger.tags); - const childTags = root.node.resolve(ctagChild.tags); - test.deepEqual(parentTags, [tag]); - test.deepEqual(childTags, [tag2]); + '#setTag() supports setting a tag regardless of Type'(test: Test) { + const notTaggable = new TagManager(TagType.NotTaggable); + notTaggable.setTag('key', 'value'); + test.deepEqual(notTaggable.renderTags(), undefined); + test.done(); + }, + 'when there are no tags': { + '#renderTags() returns undefined'(test: Test) { + const mgr = new TagManager(TagType.Standard); + test.deepEqual(mgr.renderTags(), undefined ); test.done(); }, - 'resolve() returns all tags'(test: Test) { - const root = new Root(); - const ctagger = new ChildTagger(root, 'one'); - const ctagChild = new ChildTagger(ctagger, 'one'); - const tagsNoProp = [ - {key: 'NorthCountySpot', value: 'Tabletops'}, - {key: 'Crowded', value: 'Trestles'}, - ]; - const tagsProp = [ - {key: 'BestBeach', value: 'StoneSteps'}, - {key: 'BestWaves', value: 'Blacks'}, - ]; - for (const tag of tagsNoProp) { - ctagger.tags.setTag(tag.key, tag.value, {propagate: false}); - } - for (const tag of tagsProp) { - ctagger.tags.setTag(tag.key, tag.value); - } - const allTags = tagsNoProp.concat(tagsProp); - const cAll = ctagger.tags; - const cProp = ctagChild.tags; - - for (const tag of root.node.resolve(cAll)) { - const expectedTag = allTags.filter( (t) => (t.key === tag.key)); - test.deepEqual(expectedTag[0].value, tag.value); - } - for (const tag of root.node.resolve(cProp)) { - const expectedTag = tagsProp.filter( (t) => (t.key === tag.key)); - test.deepEqual(expectedTag[0].value, tag.value); - } + '#renderTags() returns undefined with set and remove'(test: Test) { + const mgr = new TagManager(TagType.Standard); + mgr.setTag('foo', 'bar'); + mgr.removeTag('foo'); + test.deepEqual(mgr.renderTags(), undefined ); test.done(); }, - }, + } }; diff --git a/packages/@aws-cdk/cfnspec/lib/schema/property.ts b/packages/@aws-cdk/cfnspec/lib/schema/property.ts index 16dbad3a016e5..bcb87287904db 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. */ @@ -79,6 +80,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. */ @@ -196,4 +213,35 @@ export enum PropertyScrutinyType { export function isPropertyScrutinyType(str: string): str is PropertyScrutinyType { return (PropertyScrutinyType as any)[str] !== undefined; -} \ No newline at end of file +} + +/** + * 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 1b00979d72cd6..7e561c4590f46 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 { /** @@ -28,6 +28,13 @@ export interface ResourceType extends Documented { ScrutinyType?: ResourceScrutinyType; } +export interface TaggableResource extends ResourceType { + Properties: { + Tags: TagProperty; + [name: string]: Property; + } +} + export type Attribute = PrimitiveAttribute | ListAttribute; export interface PrimitiveAttribute { @@ -46,6 +53,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; } @@ -117,4 +138,4 @@ export enum ResourceScrutinyType { export function isResourceScrutinyType(str: string): str is ResourceScrutinyType { return (ResourceScrutinyType as any)[str] !== undefined; -} \ No newline at end of file +} diff --git a/tools/cfn2ts/lib/codegen.ts b/tools/cfn2ts/lib/codegen.ts index 17be51bc8ddce..dfdccc10674d2 100644 --- a/tools/cfn2ts/lib/codegen.ts +++ b/tools/cfn2ts/lib/codegen.ts @@ -8,6 +8,8 @@ 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 TAG_TYPE = `${CORE}.TagType`; +const TAG_MANAGER = `${CORE}.TagManager`; interface Dictionary { [key: string]: T; } @@ -231,6 +233,20 @@ export default class CodeGenerator { attributes.push(refAttribute); } } + // set the TagType to help format tags later + const tagEnum = tagType(spec); + if (tagEnum !== `${TAG_TYPE}.NotTaggable`) { + this.code.line(); + this.code.line('/**'); + this.code.line(' * The ``TagManager`` handles setting, removing and formatting tags'); + this.code.line(' *'); + this.code.line(' * Tags should be managed either passing them as properties during'); + this.code.line(' * initiation or by calling methods on this object. If both techniques are'); + this.code.line(' * used only the tags from the TagManager will be used. ``TagAspects``'); + this.code.line(' * will use the manager.'); + this.code.line(' */'); + this.code.line(`public readonly tags = new ${TAG_MANAGER}(${tagEnum});`); + } // // Constructor @@ -633,4 +649,20 @@ function tokenizableType(alternatives: string[]) { // TODO: number return false; -} \ No newline at end of file +} + +function tagType(resource: schema.ResourceType): string { + if (schema.isTaggableResource(resource)) { + const prop = resource.Properties.Tags; + if (schema.isTagPropertyStandard(prop)) { + return `${TAG_TYPE}.Standard`; + } + if (schema.isTagPropertyAutoScalingGroup(prop)) { + return `${TAG_TYPE}.AutoScalingGroup`; + } + if (schema.isTagPropertyJson(prop) || schema.isTagPropertyStringMap(prop)) { + return `${TAG_TYPE}.Map`; + } + } + return `${TAG_TYPE}.NotTaggable`; +} diff --git a/tools/cfn2ts/lib/genspec.ts b/tools/cfn2ts/lib/genspec.ts index 1d65543141817..add31b6df554d 100644 --- a/tools/cfn2ts/lib/genspec.ts +++ b/tools/cfn2ts/lib/genspec.ts @@ -87,7 +87,7 @@ export class CodeName { } } -export const TAG_NAME = new CodeName('', CORE_NAMESPACE, 'Tag'); +export const TAG_NAME = new CodeName('', CORE_NAMESPACE, 'CfnTag'); export const TOKEN_NAME = new CodeName('', CORE_NAMESPACE, 'Token'); /**