diff --git a/packages/@aws-cdk/aws-autoscaling/test/integ.asg-w-loadbalancer.expected.json b/packages/@aws-cdk/aws-autoscaling/test/integ.asg-w-loadbalancer.expected.json index 65b6fa4d5ddb6..848931edf69db 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/integ.asg-w-loadbalancer.expected.json +++ b/packages/@aws-cdk/aws-autoscaling/test/integ.asg-w-loadbalancer.expected.json @@ -7,7 +7,12 @@ "EnableDnsHostnames": true, "EnableDnsSupport": true, "InstanceTenancy": "default", - "Tags": [] + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-integ/VPC" + } + ] } }, "VPCPublicSubnet1SubnetB4246D30": { @@ -18,7 +23,13 @@ "Ref": "VPCB9E5F0B4" }, "AvailabilityZone": "test-region-1a", - "MapPublicIpOnLaunch": true + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-integ/VPC/PublicSubnet1" + } + ] } }, "VPCPublicSubnet1RouteTableFEE4B781": { @@ -26,7 +37,13 @@ "Properties": { "VpcId": { "Ref": "VPCB9E5F0B4" - } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-integ/VPC/PublicSubnet1" + } + ] } }, "VPCPublicSubnet1RouteTableAssociatioin249B4093": { @@ -57,7 +74,13 @@ }, "SubnetId": { "Ref": "VPCPublicSubnet1SubnetB4246D30" - } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-integ/VPC/PublicSubnet1" + } + ] } }, "VPCPublicSubnet1DefaultRoute91CEF279": { @@ -80,7 +103,13 @@ "Ref": "VPCB9E5F0B4" }, "AvailabilityZone": "test-region-1b", - "MapPublicIpOnLaunch": true + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-integ/VPC/PublicSubnet2" + } + ] } }, "VPCPublicSubnet2RouteTable6F1A15F1": { @@ -88,7 +117,13 @@ "Properties": { "VpcId": { "Ref": "VPCB9E5F0B4" - } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-integ/VPC/PublicSubnet2" + } + ] } }, "VPCPublicSubnet2RouteTableAssociatioin766225D7": { @@ -119,7 +154,13 @@ }, "SubnetId": { "Ref": "VPCPublicSubnet2Subnet74179F39" - } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-integ/VPC/PublicSubnet2" + } + ] } }, "VPCPublicSubnet2DefaultRouteB7481BBA": { @@ -142,7 +183,13 @@ "Ref": "VPCB9E5F0B4" }, "AvailabilityZone": "test-region-1c", - "MapPublicIpOnLaunch": true + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-integ/VPC/PublicSubnet3" + } + ] } }, "VPCPublicSubnet3RouteTable98AE0E14": { @@ -150,7 +197,13 @@ "Properties": { "VpcId": { "Ref": "VPCB9E5F0B4" - } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-integ/VPC/PublicSubnet3" + } + ] } }, "VPCPublicSubnet3RouteTableAssociatioinF4E24B3B": { @@ -181,7 +234,13 @@ }, "SubnetId": { "Ref": "VPCPublicSubnet3Subnet631C5E25" - } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-integ/VPC/PublicSubnet3" + } + ] } }, "VPCPublicSubnet3DefaultRouteA0D29D46": { @@ -204,7 +263,13 @@ "Ref": "VPCB9E5F0B4" }, "AvailabilityZone": "test-region-1a", - "MapPublicIpOnLaunch": false + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-integ/VPC/PrivateSubnet1" + } + ] } }, "VPCPrivateSubnet1RouteTableBE8A6027": { @@ -212,7 +277,13 @@ "Properties": { "VpcId": { "Ref": "VPCB9E5F0B4" - } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-integ/VPC/PrivateSubnet1" + } + ] } }, "VPCPrivateSubnet1RouteTableAssociatioin77F7CA18": { @@ -246,7 +317,13 @@ "Ref": "VPCB9E5F0B4" }, "AvailabilityZone": "test-region-1b", - "MapPublicIpOnLaunch": false + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-integ/VPC/PrivateSubnet2" + } + ] } }, "VPCPrivateSubnet2RouteTable0A19E10E": { @@ -254,7 +331,13 @@ "Properties": { "VpcId": { "Ref": "VPCB9E5F0B4" - } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-integ/VPC/PrivateSubnet2" + } + ] } }, "VPCPrivateSubnet2RouteTableAssociatioinC31995B4": { @@ -288,7 +371,13 @@ "Ref": "VPCB9E5F0B4" }, "AvailabilityZone": "test-region-1c", - "MapPublicIpOnLaunch": false + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-integ/VPC/PrivateSubnet3" + } + ] } }, "VPCPrivateSubnet3RouteTable192186F8": { @@ -296,7 +385,13 @@ "Properties": { "VpcId": { "Ref": "VPCB9E5F0B4" - } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-integ/VPC/PrivateSubnet3" + } + ] } }, "VPCPrivateSubnet3RouteTableAssociatioin3B0B6B38": { @@ -323,7 +418,15 @@ } }, "VPCIGWB7E252D3": { - "Type": "AWS::EC2::InternetGateway" + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-integ/VPC" + } + ] + } }, "VPCVPCGW99B986DC": { "Type": "AWS::EC2::VPCGatewayAttachment", diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc.ts b/packages/@aws-cdk/aws-ec2/lib/vpc.ts index 2bca6d6a97251..8b8b160dbbbb2 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc.ts @@ -3,6 +3,12 @@ import { Obj } from '@aws-cdk/util'; import { cloudformation, SubnetId, VPCId } from './ec2.generated'; import { NetworkBuilder } from './network-util'; import { VpcNetworkRef, VpcSubnetRef } from './vpc-ref'; + +/** + * Name tag constant + */ +const NAME_TAG: string = 'Name'; + /** * VpcNetworkProps allows you to specify configuration options for a VPC */ @@ -42,7 +48,7 @@ export interface VpcNetworkProps { /** * The AWS resource tags to associate with the VPC. */ - tags?: cdk.Tag[]; + tags?: cdk.Tags; /** * Define the maximum number of AZs to use in this region @@ -181,6 +187,11 @@ export interface SubnetConfiguration { * availability zone. */ name: string; + + /** + * The AWS resource tags to associate with the resource. + */ + tags?: cdk.Tags; } /** @@ -203,7 +214,7 @@ export interface SubnetConfiguration { * * } */ -export class VpcNetwork extends VpcNetworkRef { +export class VpcNetwork extends VpcNetworkRef implements cdk.ITaggable { /** * The default CIDR range used when creating VPCs. @@ -248,6 +259,11 @@ export class VpcNetwork extends VpcNetworkRef { */ public readonly isolatedSubnets: VpcSubnetRef[] = []; + /** + * Manage tags for this construct and children + */ + public readonly tags: cdk.TagManager; + /** * Maximum Number of NAT Gateways used to control cost * @@ -296,13 +312,15 @@ export class VpcNetwork extends VpcNetworkRef { throw new Error('To use DNS Hostnames, DNS Support must be enabled, however, it was explicitly disabled.'); } + this.tags = new cdk.TagManager(this, props.tags); + this.tags.setTag(NAME_TAG, this.path, { overwrite: false }); + const cidrBlock = ifUndefined(props.cidr, VpcNetwork.DEFAULT_CIDR_RANGE); this.networkBuilder = new NetworkBuilder(cidrBlock); const enableDnsHostnames = props.enableDnsHostnames == null ? true : props.enableDnsHostnames; const enableDnsSupport = props.enableDnsSupport == null ? true : props.enableDnsSupport; const instanceTenancy = props.defaultInstanceTenancy || 'default'; - const tags = props.tags || []; // Define a VPC using the provided CIDR range this.resource = new cloudformation.VPCResource(this, 'Resource', { @@ -310,7 +328,7 @@ export class VpcNetwork extends VpcNetworkRef { enableDnsHostnames, enableDnsSupport, instanceTenancy, - tags + tags: this.tags, }); this.availabilityZones = new cdk.AvailabilityZoneProvider(this).availabilityZones; @@ -336,7 +354,9 @@ export class VpcNetwork extends VpcNetworkRef { // Create an Internet Gateway and attach it if necessary if (allowOutbound) { - const igw = new cloudformation.InternetGatewayResource(this, 'IGW'); + const igw = new cloudformation.InternetGatewayResource(this, 'IGW', { + tags: new cdk.TagManager(this), + }); const att = new cloudformation.VPCGatewayAttachmentResource(this, 'VPCGW', { internetGatewayId: igw.ref, vpcId: this.resource.ref @@ -395,11 +415,12 @@ export class VpcNetwork extends VpcNetworkRef { private createSubnetResources(subnetConfig: SubnetConfiguration, cidrMask: number) { this.availabilityZones.forEach((zone, index) => { const name: string = `${subnetConfig.name}Subnet${index + 1}`; - const subnetProps = { + const subnetProps: VpcSubnetProps = { availabilityZone: zone, vpcId: this.vpcId, cidrBlock: this.networkBuilder.addSubnet(cidrMask), mapPublicIpOnLaunch: (subnetConfig.subnetType === SubnetType.Public), + tags: subnetConfig.tags, }; switch (subnetConfig.subnetType) { @@ -452,12 +473,18 @@ 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 VpcSubnetRef { +export class VpcSubnet extends VpcSubnetRef implements cdk.ITaggable { + /** * The Availability Zone the subnet is located in */ @@ -468,6 +495,11 @@ export class VpcSubnet extends VpcSubnetRef { */ public readonly subnetId: SubnetId; + /** + * Manage tags for Construct and propagate to children + */ + public readonly tags: cdk.TagManager; + /** * The routeTableId attached to this subnet. */ @@ -475,16 +507,21 @@ export class VpcSubnet extends VpcSubnetRef { constructor(parent: cdk.Construct, name: string, props: VpcSubnetProps) { super(parent, name); + this.tags = new cdk.TagManager(this, props.tags); + this.tags.setTag(NAME_TAG, this.path, {overwrite: false}); + this.availabilityZone = props.availabilityZone; const subnet = new cloudformation.SubnetResource(this, 'Subnet', { vpcId: props.vpcId, cidrBlock: props.cidrBlock, availabilityZone: props.availabilityZone, mapPublicIpOnLaunch: props.mapPublicIpOnLaunch, + tags: this.tags, }); this.subnetId = subnet.ref; const table = new cloudformation.RouteTableResource(this, 'RouteTable', { vpcId: props.vpcId, + tags: new cdk.TagManager(this), }); this.routeTableId = table.ref; @@ -540,7 +577,8 @@ export class VpcPublicSubnet extends VpcSubnet { subnetId: this.subnetId, allocationId: new cloudformation.EIPResource(this, `EIP`, { domain: 'vpc' - }).eipAllocationId + }).eipAllocationId, + tags: new cdk.TagManager(this), }); return ngw.ref; } diff --git a/packages/@aws-cdk/aws-ec2/test/integ.vpc.expected.json b/packages/@aws-cdk/aws-ec2/test/integ.vpc.expected.json index 4bc476853450f..511cafc47ace0 100644 --- a/packages/@aws-cdk/aws-ec2/test/integ.vpc.expected.json +++ b/packages/@aws-cdk/aws-ec2/test/integ.vpc.expected.json @@ -7,7 +7,12 @@ "EnableDnsHostnames": true, "EnableDnsSupport": true, "InstanceTenancy": "default", - "Tags": [] + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc/MyVpc" + } + ] } }, "MyVpcPublicSubnet1SubnetF6608456": { @@ -18,7 +23,13 @@ "Ref": "MyVpcF9F0CA6F" }, "AvailabilityZone": "test-region-1a", - "MapPublicIpOnLaunch": true + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc/MyVpc/PublicSubnet1" + } + ] } }, "MyVpcPublicSubnet1RouteTableC46AB2F4": { @@ -26,7 +37,13 @@ "Properties": { "VpcId": { "Ref": "MyVpcF9F0CA6F" - } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc/MyVpc/PublicSubnet1" + } + ] } }, "MyVpcPublicSubnet1RouteTableAssociatioin3562612E": { @@ -57,7 +74,13 @@ }, "SubnetId": { "Ref": "MyVpcPublicSubnet1SubnetF6608456" - } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc/MyVpc/PublicSubnet1" + } + ] } }, "MyVpcPublicSubnet1DefaultRoute95FDF9EB": { @@ -80,7 +103,13 @@ "Ref": "MyVpcF9F0CA6F" }, "AvailabilityZone": "test-region-1b", - "MapPublicIpOnLaunch": true + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc/MyVpc/PublicSubnet2" + } + ] } }, "MyVpcPublicSubnet2RouteTable1DF17386": { @@ -88,7 +117,13 @@ "Properties": { "VpcId": { "Ref": "MyVpcF9F0CA6F" - } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc/MyVpc/PublicSubnet2" + } + ] } }, "MyVpcPublicSubnet2RouteTableAssociatioin8E74FB35": { @@ -119,7 +154,13 @@ }, "SubnetId": { "Ref": "MyVpcPublicSubnet2Subnet492B6BFB" - } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc/MyVpc/PublicSubnet2" + } + ] } }, "MyVpcPublicSubnet2DefaultRoute052936F6": { @@ -142,7 +183,13 @@ "Ref": "MyVpcF9F0CA6F" }, "AvailabilityZone": "test-region-1c", - "MapPublicIpOnLaunch": true + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc/MyVpc/PublicSubnet3" + } + ] } }, "MyVpcPublicSubnet3RouteTable15028F08": { @@ -150,7 +197,13 @@ "Properties": { "VpcId": { "Ref": "MyVpcF9F0CA6F" - } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc/MyVpc/PublicSubnet3" + } + ] } }, "MyVpcPublicSubnet3RouteTableAssociatioinA3FD1B71": { @@ -181,7 +234,13 @@ }, "SubnetId": { "Ref": "MyVpcPublicSubnet3Subnet57EEE236" - } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc/MyVpc/PublicSubnet3" + } + ] } }, "MyVpcPublicSubnet3DefaultRoute3A83AB36": { @@ -204,7 +263,13 @@ "Ref": "MyVpcF9F0CA6F" }, "AvailabilityZone": "test-region-1a", - "MapPublicIpOnLaunch": false + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc/MyVpc/PrivateSubnet1" + } + ] } }, "MyVpcPrivateSubnet1RouteTable8819E6E2": { @@ -212,7 +277,13 @@ "Properties": { "VpcId": { "Ref": "MyVpcF9F0CA6F" - } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc/MyVpc/PrivateSubnet1" + } + ] } }, "MyVpcPrivateSubnet1RouteTableAssociatioin90CF6BAB": { @@ -246,7 +317,13 @@ "Ref": "MyVpcF9F0CA6F" }, "AvailabilityZone": "test-region-1b", - "MapPublicIpOnLaunch": false + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc/MyVpc/PrivateSubnet2" + } + ] } }, "MyVpcPrivateSubnet2RouteTableCEDCEECE": { @@ -254,7 +331,13 @@ "Properties": { "VpcId": { "Ref": "MyVpcF9F0CA6F" - } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc/MyVpc/PrivateSubnet2" + } + ] } }, "MyVpcPrivateSubnet2RouteTableAssociatioin803693C0": { @@ -288,7 +371,13 @@ "Ref": "MyVpcF9F0CA6F" }, "AvailabilityZone": "test-region-1c", - "MapPublicIpOnLaunch": false + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc/MyVpc/PrivateSubnet3" + } + ] } }, "MyVpcPrivateSubnet3RouteTableB790927C": { @@ -296,7 +385,13 @@ "Properties": { "VpcId": { "Ref": "MyVpcF9F0CA6F" - } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc/MyVpc/PrivateSubnet3" + } + ] } }, "MyVpcPrivateSubnet3RouteTableAssociatioinFB4A6FE6": { @@ -323,7 +418,15 @@ } }, "MyVpcIGW5C4A4F63": { - "Type": "AWS::EC2::InternetGateway" + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc/MyVpc" + } + ] + } }, "MyVpcVPCGW488ACE0D": { "Type": "AWS::EC2::VPCGatewayAttachment", diff --git a/packages/@aws-cdk/aws-ec2/test/test.vpc.ts b/packages/@aws-cdk/aws-ec2/test/test.vpc.ts index 64204e1873016..14604eb238e7d 100644 --- a/packages/@aws-cdk/aws-ec2/test/test.vpc.ts +++ b/packages/@aws-cdk/aws-ec2/test/test.vpc.ts @@ -1,10 +1,9 @@ -import { countResources, expect, haveResource } from '@aws-cdk/assert'; -import { AvailabilityZoneProvider, resolve, Stack } from '@aws-cdk/cdk'; +import { countResources, expect, haveResource, isSuperObject } from '@aws-cdk/assert'; +import { AvailabilityZoneProvider, resolve, Stack, Tags } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; import { DefaultInstanceTenancy, SubnetType, VpcNetwork } from '../lib'; export = { - "When creating a VPC": { "with the default CIDR range": { @@ -16,40 +15,50 @@ export = { }, "it uses the correct network range"(test: Test) { - const stack = getTestStack(); + const stack = getTestStack(); new VpcNetwork(stack, 'TheVPC'); expect(stack).to(haveResource('AWS::EC2::VPC', { CidrBlock: VpcNetwork.DEFAULT_CIDR_RANGE, EnableDnsHostnames: true, EnableDnsSupport: true, InstanceTenancy: DefaultInstanceTenancy.Default, - Tags: [] })); test.done(); - } + }, + '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'} ]))); + test.done(); + }, + }, "with all of the properties set, it successfully sets the correct VPC properties"(test: Test) { const stack = getTestStack(); - const tag = { - key: 'testKey', - value: 'testValue' + 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: [tag] + 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, - Tags: [{ Key: tag.key, Value: tag.value }] })); + expect(stack).to(haveResource('AWS::EC2::VPC', hasTags(cfnTags))); test.done(); }, @@ -148,7 +157,7 @@ export = { } test.done(); }, - "with custom subents and natGateways = 2 there should be only to NATGW"(test: Test) { + "with custom subents and natGateways = 2 there should be only two NATGW"(test: Test) { const stack = getTestStack(); new VpcNetwork(stack, 'TheVPC', { cidr: '10.0.0.0/21', @@ -257,7 +266,54 @@ export = { } }, + 'When tagging': { + 'VPC propagated tags will be on subnet, IGW, routetables, NATGW'(test: Test) { + const stack = getTestStack(); + const tags = { + VpcType: 'Good', + }; + const noPropTags = { + BusinessUnit: 'Marketing', + }; + const allTags: Tags = {...tags, ...noPropTags}; + const vpc = new VpcNetwork(stack, 'TheVPC', { tags: allTags }); + // overwrite to set propagate + vpc.tags.setTag('BusinessUnit', 'Marketing', {propagate: false}); + expect(stack).to(haveResource("AWS::EC2::VPC", hasTags(toCfnTags(allTags)))); + const taggables = ['Subnet', 'InternetGateway', 'NatGateway', 'RouteTable']; + const propTags = toCfnTags(tags); + const noProp = toCfnTags(noPropTags); + for (const resource of taggables) { + expect(stack).to(haveResource(`AWS::EC2::${resource}`, hasTags(propTags))); + expect(stack).notTo(haveResource(`AWS::EC2::${resource}`, hasTags(noProp))); + } + test.done(); + }, + 'Subnet Name will propagate to route tables and NATGW'(test: Test) { + const stack = getTestStack(); + const vpc = new VpcNetwork(stack, 'TheVPC'); + for (const subnet of vpc.publicSubnets) { + const tag = {Key: 'Name', Value: subnet.path}; + expect(stack).to(haveResource('AWS::EC2::NatGateway', hasTags([tag]))); + expect(stack).to(haveResource('AWS::EC2::RouteTable', hasTags([tag]))); + } + for (const subnet of vpc.privateSubnets) { + const tag = {Key: 'Name', Value: subnet.path}; + expect(stack).to(haveResource('AWS::EC2::RouteTable', hasTags([tag]))); + } + test.done(); + }, + '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'}; + expect(stack).notTo(haveResource('AWS::EC2::VPC', hasTags([tag]))); + vpc.tags.setTag(tag.Key, tag.Value); + expect(stack).to(haveResource('AWS::EC2::VPC', hasTags([tag]))); + test.done(); + }, + }, 'export/import'(test: Test) { // GIVEN const stack1 = getTestStack(); @@ -280,3 +336,33 @@ export = { function getTestStack(): Stack { return new Stack(undefined, 'TestStack', { env: { account: '123456789012', region: 'us-east-1' } }); } + +function toCfnTags(tags: Tags): Array<{Key: string, Value: string}> { + return Object.keys(tags).map( key => { + return {Key: key, Value: tags[key]}; + }); +} + +function hasTags(expectedTags: Array<{Key: string, Value: string}>): (props: any) => boolean { + return (props: any) => { + try { + const tags = props.Tags; + const actualTags = tags.filter( (tag: {Key: string, Value: string}) => { + for (const expectedTag of expectedTags) { + if (isSuperObject(expectedTag, tag)) { + return true; + } else { + continue; + } + } + // no values in array so expecting empty + return false; + }); + return actualTags.length === expectedTags.length; + } catch (e) { + // tslint:disable-next-line:no-console + console.error('Invalid Tags array in ', props); + throw e; + } + }; +} diff --git a/packages/@aws-cdk/aws-rds/test/integ.cluster.expected.json b/packages/@aws-cdk/aws-rds/test/integ.cluster.expected.json index 4bd5e110dfb43..d7c0ecd373561 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.cluster.expected.json +++ b/packages/@aws-cdk/aws-rds/test/integ.cluster.expected.json @@ -7,7 +7,12 @@ "EnableDnsHostnames": true, "EnableDnsSupport": true, "InstanceTenancy": "default", - "Tags": [] + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-integ/VPC" + } + ] } }, "VPCPublicSubnet1SubnetB4246D30": { @@ -18,7 +23,13 @@ "Ref": "VPCB9E5F0B4" }, "AvailabilityZone": "test-region-1a", - "MapPublicIpOnLaunch": true + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-integ/VPC/PublicSubnet1" + } + ] } }, "VPCPublicSubnet1RouteTableFEE4B781": { @@ -26,7 +37,13 @@ "Properties": { "VpcId": { "Ref": "VPCB9E5F0B4" - } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-integ/VPC/PublicSubnet1" + } + ] } }, "VPCPublicSubnet1RouteTableAssociatioin249B4093": { @@ -57,7 +74,13 @@ }, "SubnetId": { "Ref": "VPCPublicSubnet1SubnetB4246D30" - } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-integ/VPC/PublicSubnet1" + } + ] } }, "VPCPublicSubnet1DefaultRoute91CEF279": { @@ -80,7 +103,13 @@ "Ref": "VPCB9E5F0B4" }, "AvailabilityZone": "test-region-1b", - "MapPublicIpOnLaunch": true + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-integ/VPC/PublicSubnet2" + } + ] } }, "VPCPublicSubnet2RouteTable6F1A15F1": { @@ -88,7 +117,13 @@ "Properties": { "VpcId": { "Ref": "VPCB9E5F0B4" - } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-integ/VPC/PublicSubnet2" + } + ] } }, "VPCPublicSubnet2RouteTableAssociatioin766225D7": { @@ -119,7 +154,13 @@ }, "SubnetId": { "Ref": "VPCPublicSubnet2Subnet74179F39" - } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-integ/VPC/PublicSubnet2" + } + ] } }, "VPCPublicSubnet2DefaultRouteB7481BBA": { @@ -142,7 +183,13 @@ "Ref": "VPCB9E5F0B4" }, "AvailabilityZone": "test-region-1a", - "MapPublicIpOnLaunch": false + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-integ/VPC/PrivateSubnet1" + } + ] } }, "VPCPrivateSubnet1RouteTableBE8A6027": { @@ -150,7 +197,13 @@ "Properties": { "VpcId": { "Ref": "VPCB9E5F0B4" - } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-integ/VPC/PrivateSubnet1" + } + ] } }, "VPCPrivateSubnet1RouteTableAssociatioin77F7CA18": { @@ -184,7 +237,13 @@ "Ref": "VPCB9E5F0B4" }, "AvailabilityZone": "test-region-1b", - "MapPublicIpOnLaunch": false + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-integ/VPC/PrivateSubnet2" + } + ] } }, "VPCPrivateSubnet2RouteTable0A19E10E": { @@ -192,7 +251,13 @@ "Properties": { "VpcId": { "Ref": "VPCB9E5F0B4" - } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-integ/VPC/PrivateSubnet2" + } + ] } }, "VPCPrivateSubnet2RouteTableAssociatioinC31995B4": { @@ -219,7 +284,15 @@ } }, "VPCIGWB7E252D3": { - "Type": "AWS::EC2::InternetGateway" + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-integ/VPC" + } + ] + } }, "VPCVPCGW99B986DC": { "Type": "AWS::EC2::VPCGatewayAttachment", diff --git a/packages/@aws-cdk/aws-route53/test/integ.route53.expected.json b/packages/@aws-cdk/aws-route53/test/integ.route53.expected.json index 6383b4c17ebf5..4da2fb5686c5f 100644 --- a/packages/@aws-cdk/aws-route53/test/integ.route53.expected.json +++ b/packages/@aws-cdk/aws-route53/test/integ.route53.expected.json @@ -7,7 +7,12 @@ "EnableDnsHostnames": true, "EnableDnsSupport": true, "InstanceTenancy": "default", - "Tags": [] + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-route53-integ/VPC" + } + ] } }, "VPCPublicSubnet1SubnetB4246D30": { @@ -18,7 +23,13 @@ "Ref": "VPCB9E5F0B4" }, "AvailabilityZone": "test-region-1a", - "MapPublicIpOnLaunch": true + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-route53-integ/VPC/PublicSubnet1" + } + ] } }, "VPCPublicSubnet1RouteTableFEE4B781": { @@ -26,7 +37,13 @@ "Properties": { "VpcId": { "Ref": "VPCB9E5F0B4" - } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-route53-integ/VPC/PublicSubnet1" + } + ] } }, "VPCPublicSubnet1RouteTableAssociatioin249B4093": { @@ -57,7 +74,13 @@ }, "SubnetId": { "Ref": "VPCPublicSubnet1SubnetB4246D30" - } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-route53-integ/VPC/PublicSubnet1" + } + ] } }, "VPCPublicSubnet1DefaultRoute91CEF279": { @@ -80,7 +103,13 @@ "Ref": "VPCB9E5F0B4" }, "AvailabilityZone": "test-region-1b", - "MapPublicIpOnLaunch": true + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-route53-integ/VPC/PublicSubnet2" + } + ] } }, "VPCPublicSubnet2RouteTable6F1A15F1": { @@ -88,7 +117,13 @@ "Properties": { "VpcId": { "Ref": "VPCB9E5F0B4" - } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-route53-integ/VPC/PublicSubnet2" + } + ] } }, "VPCPublicSubnet2RouteTableAssociatioin766225D7": { @@ -119,7 +154,13 @@ }, "SubnetId": { "Ref": "VPCPublicSubnet2Subnet74179F39" - } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-route53-integ/VPC/PublicSubnet2" + } + ] } }, "VPCPublicSubnet2DefaultRouteB7481BBA": { @@ -142,7 +183,13 @@ "Ref": "VPCB9E5F0B4" }, "AvailabilityZone": "test-region-1c", - "MapPublicIpOnLaunch": true + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-route53-integ/VPC/PublicSubnet3" + } + ] } }, "VPCPublicSubnet3RouteTable98AE0E14": { @@ -150,7 +197,13 @@ "Properties": { "VpcId": { "Ref": "VPCB9E5F0B4" - } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-route53-integ/VPC/PublicSubnet3" + } + ] } }, "VPCPublicSubnet3RouteTableAssociatioinF4E24B3B": { @@ -181,7 +234,13 @@ }, "SubnetId": { "Ref": "VPCPublicSubnet3Subnet631C5E25" - } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-route53-integ/VPC/PublicSubnet3" + } + ] } }, "VPCPublicSubnet3DefaultRouteA0D29D46": { @@ -204,7 +263,13 @@ "Ref": "VPCB9E5F0B4" }, "AvailabilityZone": "test-region-1a", - "MapPublicIpOnLaunch": false + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-route53-integ/VPC/PrivateSubnet1" + } + ] } }, "VPCPrivateSubnet1RouteTableBE8A6027": { @@ -212,7 +277,13 @@ "Properties": { "VpcId": { "Ref": "VPCB9E5F0B4" - } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-route53-integ/VPC/PrivateSubnet1" + } + ] } }, "VPCPrivateSubnet1RouteTableAssociatioin77F7CA18": { @@ -246,7 +317,13 @@ "Ref": "VPCB9E5F0B4" }, "AvailabilityZone": "test-region-1b", - "MapPublicIpOnLaunch": false + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-route53-integ/VPC/PrivateSubnet2" + } + ] } }, "VPCPrivateSubnet2RouteTable0A19E10E": { @@ -254,7 +331,13 @@ "Properties": { "VpcId": { "Ref": "VPCB9E5F0B4" - } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-route53-integ/VPC/PrivateSubnet2" + } + ] } }, "VPCPrivateSubnet2RouteTableAssociatioinC31995B4": { @@ -288,7 +371,13 @@ "Ref": "VPCB9E5F0B4" }, "AvailabilityZone": "test-region-1c", - "MapPublicIpOnLaunch": false + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-route53-integ/VPC/PrivateSubnet3" + } + ] } }, "VPCPrivateSubnet3RouteTable192186F8": { @@ -296,7 +385,13 @@ "Properties": { "VpcId": { "Ref": "VPCB9E5F0B4" - } + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-route53-integ/VPC/PrivateSubnet3" + } + ] } }, "VPCPrivateSubnet3RouteTableAssociatioin3B0B6B38": { @@ -323,7 +418,15 @@ } }, "VPCIGWB7E252D3": { - "Type": "AWS::EC2::InternetGateway" + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-route53-integ/VPC" + } + ] + } }, "VPCVPCGW99B986DC": { "Type": "AWS::EC2::VPCGatewayAttachment", diff --git a/packages/@aws-cdk/cdk/lib/core/construct.ts b/packages/@aws-cdk/cdk/lib/core/construct.ts index 16fd813a89130..97e08df2cc486 100644 --- a/packages/@aws-cdk/cdk/lib/core/construct.ts +++ b/packages/@aws-cdk/cdk/lib/core/construct.ts @@ -276,7 +276,7 @@ export class Construct { * @param to The construct to return the path components relative to, or * the entire list of ancestors (including root) if omitted. */ - protected ancestors(upTo?: Construct): Construct[] { + public ancestors(upTo?: Construct): Construct[] { const ret = new Array(); let curr: Construct | undefined = this; diff --git a/packages/@aws-cdk/cdk/lib/core/tag-manager.ts b/packages/@aws-cdk/cdk/lib/core/tag-manager.ts new file mode 100644 index 0000000000000..d73cd472513f2 --- /dev/null +++ b/packages/@aws-cdk/cdk/lib/core/tag-manager.ts @@ -0,0 +1,218 @@ +import { Construct } from './construct'; +import { Token } from './tokens'; + +/** + * ITaggable indicates a entity manages tags via the `tags` property + */ +export interface ITaggable { + readonly tags: TagManager, +} + +/** + * 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}; +} + +/** + * 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; +} + +/** + * Properties for removing tags + */ +export interface RemoveProps { + /** + * If true prevent this tag form being set via propagation + * + * @default true + */ + blockPropagate?: 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 { + + /** + * 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 + }; + + /* + * 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 parent: Construct, initialTags: Tags = {}) { + super(); + 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(): any { + 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; + } + }); + return filteredTags; + } + + function propagatedTags(tagProviders: Construct[]): 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); + } + } + return parentTags; + } + + const propOverwrite = filterTags(this._tags, {sticky: false}); + const nonOverwrite = filterTags(this._tags, {sticky: true}); + const ancestors = this.parent.ancestors(); + ancestors.push(this.parent); + const tags = {...propOverwrite, ...propagatedTags(ancestors), ...nonOverwrite}; + for (const key of this.blockedTags) { delete tags[key]; } + + return Object.keys(tags).map( key => ({key, value: tags[key]})); + } + + /** + * 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 + */ + public removeTag(key: string, props: RemoveProps = {blockPropagate: true}): void { + if (props.blockPropagate) { + this.blockedTags.push(key); + } + delete this._tags[key]; + } + + /** + * Retrieve all propagated tags from all ancestors + * + * This retrieves tags from parents but not local tags + */ +} diff --git a/packages/@aws-cdk/cdk/lib/index.ts b/packages/@aws-cdk/cdk/lib/index.ts index 4a1167345295f..0f78d369d02d9 100644 --- a/packages/@aws-cdk/cdk/lib/index.ts +++ b/packages/@aws-cdk/cdk/lib/index.ts @@ -1,5 +1,6 @@ export * from './core/construct'; export * from './core/tokens'; +export * from './core/tag-manager'; export * from './core/jsx'; export * from './cloudformation/cloudformation-json'; diff --git a/packages/@aws-cdk/cdk/test/core/test.tag-manager.ts b/packages/@aws-cdk/cdk/test/core/test.tag-manager.ts new file mode 100644 index 0000000000000..03d32660a6ee7 --- /dev/null +++ b/packages/@aws-cdk/cdk/test/core/test.tag-manager.ts @@ -0,0 +1,181 @@ +import { Test } from 'nodeunit'; +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; + constructor(parent: Construct, name: string) { + super(parent, name); + this.tags = new TagManager(parent); + } +} +class Child extends Construct { + constructor(parent: Construct, name: string) { + super(parent, name); + } +} + +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(construct.tags.resolve(), tagArray); + } + + test.deepEqual(ctagger2.tags.resolve().length, 0); + 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(construct.tags.resolve().length, 0); + } + test.deepEqual(ctagger.tags.resolve()[0].key, 'Name'); + test.deepEqual(ctagger.tags.resolve()[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 = ctagger.tags.resolve(); + 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 = ctagger.tags.resolve(); + const child = ctagger1.tags.resolve(); + 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(ctagger2.tags.resolve(), [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(ctagger.tags.resolve(), [tag]); + ctagger.tags.removeTag(tag.key); + test.deepEqual(ctagger.tags.resolve(), []); + ctagger.tags.setTag(tag.key, tag.value, {propagate: true}); + test.deepEqual(ctagger.tags.resolve(), [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(construct.tags.resolve().length, 0); + } + 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 = ctagger.tags.resolve(); + test.deepEqual(result, [{key: 'Env', value: 'Dev'}]); + test.deepEqual(ctagger1.tags.resolve(), []); + 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 = ctagger.tags.resolve(); + const childTags = ctagChild.tags.resolve(); + test.deepEqual(parentTags, [tag]); + test.deepEqual(childTags, [tag2]); + 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 cAll.resolve()) { + const expectedTag = allTags.filter( (t) => (t.key === tag.key)); + test.deepEqual(expectedTag[0].value, tag.value); + } + for (const tag of cProp.resolve()) { + const expectedTag = tagsProp.filter( (t) => (t.key === tag.key)); + test.deepEqual(expectedTag[0].value, tag.value); + } + test.done(); + }, + }, +};