From 14573f984866926aca60b36d90623b2aa4afb641 Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Mon, 1 Oct 2018 16:41:26 -0700 Subject: [PATCH] feat(aws-codedeploy): Added ability to tag EC2 and on-premise instances in Deployment Groups. --- packages/@aws-cdk/aws-codedeploy/README.md | 21 ++++ .../aws-codedeploy/lib/deployment-group.ts | 118 ++++++++++++++++++ .../test/test.deployment-group.ts | 107 ++++++++++++++++ 3 files changed, 246 insertions(+) diff --git a/packages/@aws-cdk/aws-codedeploy/README.md b/packages/@aws-cdk/aws-codedeploy/README.md index 601829edab23b..79f3bf2621b31 100644 --- a/packages/@aws-cdk/aws-codedeploy/README.md +++ b/packages/@aws-cdk/aws-codedeploy/README.md @@ -32,6 +32,27 @@ const deploymentGroup = new codedeploy.ServerDeploymentGroup(this, 'CodeDeployDe // adds User Data that installs the CodeDeploy agent on your auto-scaling groups hosts // default: true installAgent: true, + // adds EC2 instances matching tags + ec2InstanceTags: new codedeploy.InstanceTagSet( + { + // any instance with tags satisfying + // key1=v1 or key1=v2 or key2 (any value) or value v3 (any key) + // will match this group + 'key1': ['v1', 'v2'], + 'key2': [], + '': ['v3'], + }, + ), + // adds on-premise instances matching tags + onPremiseInstanceTags: new codedeploy.InstanceTagSet( + // only instances with tags (key1=v1 or key1=v2) AND key2=v3 will match this set + { + 'key1': ['v1', 'v2'], + }, + { + 'key2': ['v3'], + }, + ), }); ``` diff --git a/packages/@aws-cdk/aws-codedeploy/lib/deployment-group.ts b/packages/@aws-cdk/aws-codedeploy/lib/deployment-group.ts index 4d488197d80ff..d48f8a6d5c00c 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/deployment-group.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/deployment-group.ts @@ -98,6 +98,39 @@ class ImportedServerDeploymentGroupRef extends ServerDeploymentGroupRef { } } +/** + * Represents a group of instance tags. + * An instance will match a group if it has a tag matching + * any of the group's tags by name and any of the provided values - + * in other words, tag groups follow 'or' semantics. + * If the value for a given key is an empty array, + * an instance will match when it has a tag with the given key, + * regardless of the value. + * If the key is an empty string, any tag, + * regardless of its key, with any of the given values, will match. + */ +export interface InstanceTagGroup { + [key: string]: string[], +} + +/** + * Represents a set of instance tag groups. + * An instance will match a set if it matches all of the groups in the set - + * in other words, sets follow 'and' semantics. + * You can have a maximum of 3 tag groups inside a set. + */ +export class InstanceTagSet { + public readonly instanceTagGroups: InstanceTagGroup[]; + + constructor(...instanceTagGroups: InstanceTagGroup[]) { + if (instanceTagGroups.length > 3) { + throw new Error('An instance tag set can have a maximum of 3 instance tag groups, ' + + `but ${instanceTagGroups.length} were provided`); + } + this.instanceTagGroups = instanceTagGroups; + } +} + /** * Construction properties for {@link ServerDeploymentGroup}. */ @@ -143,6 +176,20 @@ export interface ServerDeploymentGroupProps { * @see https://docs.aws.amazon.com/codedeploy/latest/userguide/codedeploy-agent-operations-install.html */ installAgent?: boolean; + + /** + * All EC2 instances matching the given set of tags when a deployment occurs will be added to this Deployment Group. + * + * @default no additional EC2 instances will be added to the Deployment Group + */ + ec2InstanceTags?: InstanceTagSet; + + /** + * All on-premise instances matching the given set of tags when a deployment occurs will be added to this Deployment Group. + * + * @default no additional on-premise instances will be added to the Deployment Group + */ + onPremiseInstanceTags?: InstanceTagSet; } /** @@ -188,6 +235,8 @@ export class ServerDeploymentGroup extends ServerDeploymentGroupRef { this._autoScalingGroups.length === 0 ? undefined : this._autoScalingGroups.map(asg => asg.autoScalingGroupName())), + ec2TagSet: this.ec2TagSet(props.ec2InstanceTags), + onPremisesTagSet: this.onPremiseTagSet(props.onPremiseInstanceTags), }); this.deploymentGroupName = resource.deploymentGroupName; @@ -244,6 +293,75 @@ export class ServerDeploymentGroup extends ServerDeploymentGroupRef { break; } } + + private ec2TagSet(tagSet?: InstanceTagSet): + cloudformation.DeploymentGroupResource.EC2TagSetProperty | undefined { + if (!tagSet || tagSet.instanceTagGroups.length === 0) { + return undefined; + } + + return { + ec2TagSetList: tagSet.instanceTagGroups.map(tagGroup => { + return { + ec2TagGroup: this.tagGroup2TagsArray(tagGroup) as + cloudformation.DeploymentGroupResource.EC2TagFilterProperty[], + }; + }), + }; + } + + private onPremiseTagSet(tagSet?: InstanceTagSet): + cloudformation.DeploymentGroupResource.OnPremisesTagSetProperty | undefined { + if (!tagSet || tagSet.instanceTagGroups.length === 0) { + return undefined; + } + + return { + onPremisesTagSetList: tagSet.instanceTagGroups.map(tagGroup => { + return { + onPremisesTagGroup: this.tagGroup2TagsArray(tagGroup) as + cloudformation.DeploymentGroupResource.TagFilterProperty[], + }; + }), + }; + } + + private tagGroup2TagsArray(tagGroup: InstanceTagGroup): any[] { + const tagsInGroup = []; + for (const tagKey in tagGroup) { + if (tagGroup.hasOwnProperty(tagKey)) { + const tagValues = tagGroup[tagKey]; + if (tagKey.length > 0) { + if (tagValues.length > 0) { + for (const tagValue of tagValues) { + tagsInGroup.push({ + key: tagKey, + value: tagValue, + type: 'KEY_AND_VALUE', + }); + } + } else { + tagsInGroup.push({ + key: tagKey, + type: 'KEY_ONLY', + }); + } + } else { + if (tagValues.length > 0) { + for (const tagValue of tagValues) { + tagsInGroup.push({ + value: tagValue, + type: 'VALUE_ONLY', + }); + } + } else { + throw new Error('Cannot specify both an empty key and no values for an instance tag filter'); + } + } + } + } + return tagsInGroup; + } } function deploymentGroupName2Arn(applicationName: string, deploymentGroupName: string): string { diff --git a/packages/@aws-cdk/aws-codedeploy/test/test.deployment-group.ts b/packages/@aws-cdk/aws-codedeploy/test/test.deployment-group.ts index bd20dfb1bf707..4542cf27a9aed 100644 --- a/packages/@aws-cdk/aws-codedeploy/test/test.deployment-group.ts +++ b/packages/@aws-cdk/aws-codedeploy/test/test.deployment-group.ts @@ -88,5 +88,112 @@ export = { test.done(); }, + + 'can be created with a single EC2 instance tag set with a single or no value'(test: Test) { + const stack = new cdk.Stack(); + + new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup', { + ec2InstanceTags: new codedeploy.InstanceTagSet( + { + 'some-key': ['some-value'], + 'other-key': [], + }, + ), + }); + + expect(stack).to(haveResource('AWS::CodeDeploy::DeploymentGroup', { + "Ec2TagSet": { + "Ec2TagSetList": [ + { + "Ec2TagGroup": [ + { + "Key": "some-key", + "Value": "some-value", + "Type": "KEY_AND_VALUE", + }, + { + "Key": "other-key", + "Type": "KEY_ONLY", + }, + ], + }, + ], + }, + })); + + test.done(); + }, + + 'can be created with two on-premise instance tag sets with multiple values or without a key'(test: Test) { + const stack = new cdk.Stack(); + + new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup', { + onPremiseInstanceTags: new codedeploy.InstanceTagSet( + { + 'some-key': ['some-value', 'another-value'], + }, + { + '': ['keyless-value'], + }, + ), + }); + + expect(stack).to(haveResource('AWS::CodeDeploy::DeploymentGroup', { + "OnPremisesTagSet": { + "OnPremisesTagSetList": [ + { + "OnPremisesTagGroup": [ + { + "Key": "some-key", + "Value": "some-value", + "Type": "KEY_AND_VALUE", + }, + { + "Key": "some-key", + "Value": "another-value", + "Type": "KEY_AND_VALUE", + }, + ], + }, + { + "OnPremisesTagGroup": [ + { + "Value": "keyless-value", + "Type": "VALUE_ONLY", + }, + ], + }, + ], + }, + })); + + test.done(); + }, + + 'cannot be created with an instance tag set containing a keyless, valueless filter'(test: Test) { + const stack = new cdk.Stack(); + + test.throws(() => { + new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup', { + onPremiseInstanceTags: new codedeploy.InstanceTagSet({ + '': [], + }), + }); + }); + + test.done(); + }, + + 'cannot be created with an instance tag set containing 4 instance tag groups'(test: Test) { + const stack = new cdk.Stack(); + + test.throws(() => { + new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup', { + onPremiseInstanceTags: new codedeploy.InstanceTagSet({}, {}, {}, {}), + }); + }, /3/); + + test.done(); + }, }, };