Skip to content

Commit

Permalink
feat(aws-codedeploy): add instance tag filter support for server Depl…
Browse files Browse the repository at this point in the history
…oyment Groups. (#824)
  • Loading branch information
skinny85 committed Oct 12, 2018
1 parent 2af8309 commit e6e8c51
Show file tree
Hide file tree
Showing 3 changed files with 248 additions and 0 deletions.
21 changes: 21 additions & 0 deletions packages/@aws-cdk/aws-codedeploy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
},
),
});
```

Expand Down
120 changes: 120 additions & 0 deletions packages/@aws-cdk/aws-codedeploy/lib/deployment-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
*/
Expand Down Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
107 changes: 107 additions & 0 deletions packages/@aws-cdk/aws-codedeploy/test/test.deployment-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
},
},
};

0 comments on commit e6e8c51

Please sign in to comment.