From e6b67ad22912796662a348f3f5e1d3777ab8d1bf Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Thu, 20 Sep 2018 16:19:41 -0700 Subject: [PATCH] feat(aws-codedeploy): Deployment Configuration Construct. (#653) Part of the work on a AWS Construct Library for CodeDeploy. --- packages/@aws-cdk/aws-codedeploy/README.md | 35 +++- .../aws-codedeploy/lib/deployment-config.ts | 158 ++++++++++++++++++ .../aws-codedeploy/lib/deployment-group.ts | 41 ++++- packages/@aws-cdk/aws-codedeploy/lib/index.ts | 1 + .../aws-codedeploy/test/test.codedeploy.ts | 8 - .../test/test.deployment-config.ts | 80 +++++++++ .../test/test.deployment-group.ts | 43 +++++ .../integ.pipeline-code-deploy.expected.json | 12 ++ .../test/integ.pipeline-code-deploy.ts | 5 + 9 files changed, 365 insertions(+), 18 deletions(-) create mode 100644 packages/@aws-cdk/aws-codedeploy/lib/deployment-config.ts delete mode 100644 packages/@aws-cdk/aws-codedeploy/test/test.codedeploy.ts create mode 100644 packages/@aws-cdk/aws-codedeploy/test/test.deployment-config.ts create mode 100644 packages/@aws-cdk/aws-codedeploy/test/test.deployment-group.ts diff --git a/packages/@aws-cdk/aws-codedeploy/README.md b/packages/@aws-cdk/aws-codedeploy/README.md index 93f9d31630ee6..e58b1b5d77c4b 100644 --- a/packages/@aws-cdk/aws-codedeploy/README.md +++ b/packages/@aws-cdk/aws-codedeploy/README.md @@ -16,7 +16,7 @@ To import an already existing Application: ```ts const application = codedeploy.ServerApplicationRef.import(this, 'ExistingCodeDeployApplication', { - applicationName: new codedeploy.ApplicationName('MyExistingApplication'), + applicationName: 'MyExistingApplication', }); ``` @@ -39,7 +39,38 @@ To import an already existing Deployment Group: ```ts const deploymentGroup = codedeploy.ServerDeploymentGroupRef.import(this, 'ExistingCodeDeployDeploymentGroup', { application, - deploymentGroupName: new codedeploy.DeploymentGroupName('MyExistingDeploymentGroup'), + deploymentGroupName: 'MyExistingDeploymentGroup', +}); +``` + +### Deployment Configurations + +You can also pass a Deployment Configuration when creating the Deployment Group: + +```ts +const deploymentGroup = new codedeploy.ServerDeploymentGroup(this, 'CodeDeployDeploymentGroup', { + deploymentConfig: codedeploy.ServerDeploymentConfig.AllAtOnce, +}); +``` + +The default Deployment Configuration is `ServerDeploymentConfig.OneAtATime`. + +You can also create a custom Deployment Configuration: + +```ts +const deploymentConfig = new codedeploy.ServerDeploymentConfig(this, 'DeploymentConfiguration', { + deploymentConfigName: 'MyDeploymentConfiguration', // optional property + // one of these is required, but both cannot be specified at the same time + minHealthyHostCount: 2, + minHealthyHostPercentage: 75, +}); +``` + +Or import an existing one: + +```ts +const deploymentConfig = codedeploy.ServerDeploymentConfigRef.import(this, 'ExistingDeploymentConfiguration', { + deploymentConfigName: 'MyExistingDeploymentConfiguration', }); ``` diff --git a/packages/@aws-cdk/aws-codedeploy/lib/deployment-config.ts b/packages/@aws-cdk/aws-codedeploy/lib/deployment-config.ts new file mode 100644 index 0000000000000..dbc44f1ff2aef --- /dev/null +++ b/packages/@aws-cdk/aws-codedeploy/lib/deployment-config.ts @@ -0,0 +1,158 @@ +import cdk = require('@aws-cdk/cdk'); +import { cloudformation } from './codedeploy.generated'; + +/** + * The Deployment Configuration of an EC2/on-premise Deployment Group. + * The default, pre-defined Configurations are available as constants on the {@link ServerDeploymentConfig} class + * (`ServerDeploymentConfig.HalfAtATime`, `ServerDeploymentConfig.AllAtOnce`, etc.). + * To create a custom Deployment Configuration, + * instantiate the {@link ServerDeploymentConfig} Construct. + */ +export interface IServerDeploymentConfig { + readonly deploymentConfigName: string; + readonly deploymentConfigArn: string; +} + +/** + * Properties of a reference to a CodeDeploy EC2/on-premise Deployment Configuration. + * + * @see ServerDeploymentConfigRef#import + * @see ServerDeploymentConfigRef#export + */ +export interface ServerDeploymentConfigRefProps { + /** + * The physical, human-readable name of the custom CodeDeploy EC2/on-premise Deployment Configuration + * that we are referencing. + */ + deploymentConfigName: string; +} + +/** + * Reference to a custom Deployment Configuration for an EC2/on-premise Deployment Group. + */ +export abstract class ServerDeploymentConfigRef extends cdk.Construct implements IServerDeploymentConfig { + /** + * Import a custom Deployment Configuration for an EC2/on-premise Deployment Group defined either outside the CDK, + * or in a different CDK Stack and exported using the {@link #export} method. + * + * @param parent the parent Construct for this new Construct + * @param id the logical ID of this new Construct + * @param props the properties of the referenced custom Deployment Configuration + * @returns a Construct representing a reference to an existing custom Deployment Configuration + */ + public static import(parent: cdk.Construct, id: string, props: ServerDeploymentConfigRefProps): + ServerDeploymentConfigRef { + return new ImportedServerDeploymentConfigRef(parent, id, props); + } + + public abstract readonly deploymentConfigName: string; + public abstract readonly deploymentConfigArn: string; + + public export(): ServerDeploymentConfigRefProps { + return { + deploymentConfigName: new cdk.Output(this, 'DeploymentConfigName', { + value: this.deploymentConfigName, + }).makeImportValue().toString(), + }; + } +} + +class ImportedServerDeploymentConfigRef extends ServerDeploymentConfigRef { + public readonly deploymentConfigName: string; + public readonly deploymentConfigArn: string; + + constructor(parent: cdk.Construct, id: string, props: ServerDeploymentConfigRefProps) { + super(parent, id); + + this.deploymentConfigName = props.deploymentConfigName; + this.deploymentConfigArn = arnForDeploymentConfigName(this.deploymentConfigName); + } +} + +class DefaultServerDeploymentConfig implements IServerDeploymentConfig { + public readonly deploymentConfigName: string; + public readonly deploymentConfigArn: string; + + constructor(deploymentConfigName: string) { + this.deploymentConfigName = deploymentConfigName; + this.deploymentConfigArn = arnForDeploymentConfigName(this.deploymentConfigName); + } +} + +/** + * Construction properties of {@link ServerDeploymentConfig}. + */ +export interface ServerDeploymentConfigProps { + /** + * The physical, human-readable name of the Deployment Configuration. + * + * @default a name will be auto-generated + */ + deploymentConfigName?: string; + + /** + * The minimum healhty hosts threshold expressed as an absolute number. + * If you've specified this value, + * you can't specify {@link #minHealthyHostPercentage}, + * however one of this or {@link #minHealthyHostPercentage} is required. + */ + minHealthyHostCount?: number; + + /** + * The minmum healhty hosts threshold expressed as a percentage of the fleet. + * If you've specified this value, + * you can't specify {@link #minHealthyHostCount}, + * however one of this or {@link #minHealthyHostCount} is required. + */ + minHealthyHostPercentage?: number; +} + +/** + * A custom Deployment Configuration for an EC2/on-premise Deployment Group. + */ +export class ServerDeploymentConfig extends ServerDeploymentConfigRef { + public static readonly OneAtATime: IServerDeploymentConfig = new DefaultServerDeploymentConfig('CodeDeployDefault.OneAtATime'); + public static readonly HalfAtATime: IServerDeploymentConfig = new DefaultServerDeploymentConfig('CodeDeployDefault.HalfAtATime'); + public static readonly AllAtOnce: IServerDeploymentConfig = new DefaultServerDeploymentConfig('CodeDeployDefault.AllAtOnce'); + + public readonly deploymentConfigName: string; + public readonly deploymentConfigArn: string; + + constructor(parent: cdk.Construct, id: string, props: ServerDeploymentConfigProps) { + super(parent, id); + + const resource = new cloudformation.DeploymentConfigResource(this, 'Resource', { + deploymentConfigName: props.deploymentConfigName, + minimumHealthyHosts: this.minimumHealthyHosts(props), + }); + + this.deploymentConfigName = resource.ref.toString(); + this.deploymentConfigArn = arnForDeploymentConfigName(this.deploymentConfigName); + } + + private minimumHealthyHosts(props: ServerDeploymentConfigProps): + cloudformation.DeploymentConfigResource.MinimumHealthyHostsProperty { + if (props.minHealthyHostCount === undefined && props.minHealthyHostPercentage === undefined) { + throw new Error('At least one of minHealthyHostCount or minHealthyHostPercentage must be specified when creating ' + + 'a custom Server DeploymentConfig'); + } + if (props.minHealthyHostCount !== undefined && props.minHealthyHostPercentage !== undefined) { + throw new Error('Both minHealthyHostCount and minHealthyHostPercentage cannot be specified when creating ' + + 'a custom Server DeploymentConfig'); + } + + return { + type: props.minHealthyHostCount !== undefined ? 'HOST_COUNT' : 'FLEET_PERCENT', + value: props.minHealthyHostCount !== undefined ? props.minHealthyHostCount : props.minHealthyHostPercentage!, + }; + } +} + +function arnForDeploymentConfigName(name: string): string { + return cdk.ArnUtils.fromComponents({ + service: 'codedeploy', + resource: 'deploymentconfig', + resourceName: name, + sep: ':', + }); +} diff --git a/packages/@aws-cdk/aws-codedeploy/lib/deployment-group.ts b/packages/@aws-cdk/aws-codedeploy/lib/deployment-group.ts index 7e8f0bcdaeed0..dc2a48b1f6ddb 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/deployment-group.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/deployment-group.ts @@ -2,6 +2,7 @@ import cdk = require("@aws-cdk/cdk"); import iam = require("../../aws-iam/lib/role"); import { ServerApplication, ServerApplicationRef } from "./application"; import { cloudformation } from './codedeploy.generated'; +import { IServerDeploymentConfig, ServerDeploymentConfig } from "./deployment-config"; /** * Properties of a reference to a CodeDeploy EC2/on-premise Deployment Group. @@ -21,6 +22,13 @@ export interface ServerDeploymentGroupRefProps { * that we are referencing. */ deploymentGroupName: string; + + /** + * The Deployment Configuration this Deployment Group uses. + * + * @default ServerDeploymentConfig#OneAtATime + */ + deploymentConfig?: IServerDeploymentConfig; } /** @@ -50,11 +58,19 @@ export abstract class ServerDeploymentGroupRef extends cdk.Construct { public abstract readonly application: ServerApplicationRef; public abstract readonly deploymentGroupName: string; public abstract readonly deploymentGroupArn: string; + public readonly deploymentConfig: IServerDeploymentConfig; + + constructor(parent: cdk.Construct, id: string, deploymentConfig?: IServerDeploymentConfig) { + super(parent, id); + this.deploymentConfig = deploymentConfig || ServerDeploymentConfig.OneAtATime; + } public export(): ServerDeploymentGroupRefProps { return { application: this.application, - deploymentGroupName: new cdk.Output(this, 'DeploymentGroupName', { value: this.deploymentGroupName }).makeImportValue().toString() + deploymentGroupName: new cdk.Output(this, 'DeploymentGroupName', { + value: this.deploymentGroupName + }).makeImportValue().toString(), }; } } @@ -65,7 +81,7 @@ class ImportedServerDeploymentGroupRef extends ServerDeploymentGroupRef { public readonly deploymentGroupArn: string; constructor(parent: cdk.Construct, id: string, props: ServerDeploymentGroupRefProps) { - super(parent, id); + super(parent, id, props.deploymentConfig); this.application = props.application; this.deploymentGroupName = props.deploymentGroupName; @@ -96,6 +112,13 @@ export interface ServerDeploymentGroupProps { * @default an auto-generated name will be used */ deploymentGroupName?: string; + + /** + * The EC2/on-premise Deployment Configuration to use for this Deployment Group. + * + * @default ServerDeploymentConfig#OneAtATime + */ + deploymentConfig?: IServerDeploymentConfig; } /** @@ -107,23 +130,25 @@ export class ServerDeploymentGroup extends ServerDeploymentGroupRef { public readonly deploymentGroupArn: string; public readonly deploymentGroupName: string; - constructor(parent: cdk.Construct, id: string, props?: ServerDeploymentGroupProps) { - super(parent, id); + constructor(parent: cdk.Construct, id: string, props: ServerDeploymentGroupProps = {}) { + super(parent, id, props && props.deploymentConfig); - this.application = (props && props.application) || new ServerApplication(this, 'Application'); + this.application = props.application || new ServerApplication(this, 'Application'); - this.role = (props && props.role) || new iam.Role(this, 'Role', { + this.role = props.role || new iam.Role(this, 'Role', { assumedBy: new cdk.ServicePrincipal('codedeploy.amazonaws.com'), managedPolicyArns: ['arn:aws:iam::aws:policy/service-role/AWSCodeDeployRole'], }); const resource = new cloudformation.DeploymentGroupResource(this, 'Resource', { applicationName: this.application.applicationName, - deploymentGroupName: props && props.deploymentGroupName, + deploymentGroupName: props.deploymentGroupName, serviceRoleArn: this.role.roleArn, + deploymentConfigName: props.deploymentConfig && + props.deploymentConfig.deploymentConfigName, }); - this.deploymentGroupName = resource.ref; + this.deploymentGroupName = resource.deploymentGroupName; this.deploymentGroupArn = deploymentGroupName2Arn(this.application.applicationName, this.deploymentGroupName); } diff --git a/packages/@aws-cdk/aws-codedeploy/lib/index.ts b/packages/@aws-cdk/aws-codedeploy/lib/index.ts index 862add5a074ec..2b4163f14be4f 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/index.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/index.ts @@ -1,4 +1,5 @@ export * from './application'; +export * from './deployment-config'; export * from './deployment-group'; export * from './pipeline-action'; diff --git a/packages/@aws-cdk/aws-codedeploy/test/test.codedeploy.ts b/packages/@aws-cdk/aws-codedeploy/test/test.codedeploy.ts deleted file mode 100644 index db4c843199541..0000000000000 --- a/packages/@aws-cdk/aws-codedeploy/test/test.codedeploy.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Test, testCase } from 'nodeunit'; - -exports = testCase({ - notTested(test: Test) { - test.ok(true, 'No tests are specified for this package.'); - test.done(); - } -}); diff --git a/packages/@aws-cdk/aws-codedeploy/test/test.deployment-config.ts b/packages/@aws-cdk/aws-codedeploy/test/test.deployment-config.ts new file mode 100644 index 0000000000000..485ba8a3cdca2 --- /dev/null +++ b/packages/@aws-cdk/aws-codedeploy/test/test.deployment-config.ts @@ -0,0 +1,80 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import codedeploy = require('../lib'); + +// tslint:disable:object-literal-key-quotes + +export = { + 'CodeDeploy DeploymentConfig': { + "cannot be created without specifying minHealthyHostCount or minHealthyHostPercentage"(test: Test) { + const stack = new cdk.Stack(); + + test.throws(() => { + new codedeploy.ServerDeploymentConfig(stack, 'DeploymentConfig', { + }); + }, /minHealthyHost/i); + + test.done(); + }, + + "cannot be created specifying both minHealthyHostCount and minHealthyHostPercentage"(test: Test) { + const stack = new cdk.Stack(); + + test.throws(() => { + new codedeploy.ServerDeploymentConfig(stack, 'DeploymentConfig', { + minHealthyHostCount: 1, + minHealthyHostPercentage: 1, + }); + }, /minHealthyHost/i); + + test.done(); + }, + + "can be created by specifying only minHealthyHostCount"(test: Test) { + const stack = new cdk.Stack(); + + new codedeploy.ServerDeploymentConfig(stack, 'DeploymentConfig', { + minHealthyHostCount: 1, + }); + + expect(stack).to(haveResource('AWS::CodeDeploy::DeploymentConfig', { + "MinimumHealthyHosts": { + "Type": "HOST_COUNT", + "Value": 1, + }, + })); + + test.done(); + }, + + "can be created by specifying only minHealthyHostPercentage"(test: Test) { + const stack = new cdk.Stack(); + + new codedeploy.ServerDeploymentConfig(stack, 'DeploymentConfig', { + minHealthyHostPercentage: 75, + }); + + expect(stack).to(haveResource('AWS::CodeDeploy::DeploymentConfig', { + "MinimumHealthyHosts": { + "Type": "FLEET_PERCENT", + "Value": 75, + }, + })); + + test.done(); + }, + + 'can be imported'(test: Test) { + const stack = new cdk.Stack(); + + const deploymentConfig = codedeploy.ServerDeploymentConfigRef.import(stack, 'MyDC', { + deploymentConfigName: 'MyDC', + }); + + test.notEqual(deploymentConfig, undefined); + + test.done(); + }, + }, +}; diff --git a/packages/@aws-cdk/aws-codedeploy/test/test.deployment-group.ts b/packages/@aws-cdk/aws-codedeploy/test/test.deployment-group.ts new file mode 100644 index 0000000000000..9e6af5add179a --- /dev/null +++ b/packages/@aws-cdk/aws-codedeploy/test/test.deployment-group.ts @@ -0,0 +1,43 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import codedeploy = require('../lib'); + +// tslint:disable:object-literal-key-quotes + +export = { + 'CodeDeploy Deployment Group': { + "can be created by explicitly passing an Application"(test: Test) { + const stack = new cdk.Stack(); + + const application = new codedeploy.ServerApplication(stack, 'MyApp'); + new codedeploy.ServerDeploymentGroup(stack, 'MyDG', { + application, + }); + + expect(stack).to(haveResource('AWS::CodeDeploy::DeploymentGroup', { + "ApplicationName": { + "Ref": "MyApp3CE31C26" + }, + })); + + test.done(); + }, + + 'can be imported'(test: Test) { + const stack = new cdk.Stack(); + + const application = codedeploy.ServerApplicationRef.import(stack, 'MyApp', { + applicationName: 'MyApp', + }); + const deploymentGroup = codedeploy.ServerDeploymentGroupRef.import(stack, 'MyDG', { + application, + deploymentGroupName: 'MyDG', + }); + + test.notEqual(deploymentGroup, undefined); + + test.done(); + } + }, +}; diff --git a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-deploy.expected.json b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-deploy.expected.json index 3fb62e98b05eb..c1eb90b667bbb 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-deploy.expected.json +++ b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-deploy.expected.json @@ -7,6 +7,15 @@ "ComputePlatform": "Server" } }, + "CustomDeployConfig52EEBC13": { + "Type": "AWS::CodeDeploy::DeploymentConfig", + "Properties": { + "MinimumHealthyHosts": { + "Type": "HOST_COUNT", + "Value": 0 + } + } + }, "CodeDeployGroupRole1D304F7A": { "Type": "AWS::IAM::Role", "Properties": { @@ -39,6 +48,9 @@ "Arn" ] }, + "DeploymentConfigName": { + "Ref": "CustomDeployConfig52EEBC13" + }, "DeploymentGroupName": "IntegTestDeploymentGroup" } }, diff --git a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-deploy.ts b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-deploy.ts index 3df11ab1beb14..d622ec79a20d1 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-deploy.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/integ.pipeline-code-deploy.ts @@ -11,9 +11,14 @@ const application = new codedeploy.ServerApplication(stack, 'CodeDeployApplicati applicationName: 'IntegTestDeployApp', }); +const deploymentConfig = new codedeploy.ServerDeploymentConfig(stack, 'CustomDeployConfig', { + minHealthyHostCount: 0, +}); + new codedeploy.ServerDeploymentGroup(stack, 'CodeDeployGroup', { application, deploymentGroupName: 'IntegTestDeploymentGroup', + deploymentConfig, }); const bucket = new s3.Bucket(stack, 'CodeDeployPipelineIntegTest', {