diff --git a/packages/@aws-cdk/aws-codedeploy/README.md b/packages/@aws-cdk/aws-codedeploy/README.md index 0e0ec2c5798ce..d832cc267c814 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 2fa51c92cac50..e6c1eac4876f1 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/deployment-group.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/deployment-group.ts @@ -99,6 +99,41 @@ 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 key 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 type 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 { + private 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; + } + + public get instanceTagGroups(): InstanceTagGroup[] { + return this._instanceTagGroups.slice(); + } +} + /** * Construction properties for {@link ServerDeploymentGroup}. */ @@ -153,6 +188,20 @@ export interface ServerDeploymentGroupProps { * @default the Deployment Group will not have a load balancer defined */ loadBalancer?: codedeploylb.ILoadBalancer; + + /* + * 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; } /** @@ -204,6 +253,8 @@ export class ServerDeploymentGroup extends ServerDeploymentGroupRef { : { deploymentOption: 'WITH_TRAFFIC_CONTROL', }, + ec2TagSet: this.ec2TagSet(props.ec2InstanceTags), + onPremisesTagSet: this.onPremiseTagSet(props.onPremiseInstanceTags), }); this.deploymentGroupName = resource.deploymentGroupName; @@ -284,6 +335,75 @@ export class ServerDeploymentGroup extends ServerDeploymentGroupRef { }; } } + + 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 48114508654a6..a1d72b54e6834 100644 --- a/packages/@aws-cdk/aws-codedeploy/test/test.deployment-group.ts +++ b/packages/@aws-cdk/aws-codedeploy/test/test.deployment-group.ts @@ -157,5 +157,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(); + }, }, };