diff --git a/packages/@aws-cdk/aws-codedeploy/README.md b/packages/@aws-cdk/aws-codedeploy/README.md index 93f9d31630ee6..647191d5bb76f 100644 --- a/packages/@aws-cdk/aws-codedeploy/README.md +++ b/packages/@aws-cdk/aws-codedeploy/README.md @@ -43,6 +43,37 @@ const deploymentGroup = codedeploy.ServerDeploymentGroupRef.import(this, 'Existi }); ``` +### 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: new codedeploy.DeploymentConfigName('MyExistingDeploymentConfiguration'), +}); +``` + ### Use in CodePipeline This module also contains an Action that allows you to use CodeDeploy with AWS CodePipeline. 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..98a8262f4988e --- /dev/null +++ b/packages/@aws-cdk/aws-codedeploy/lib/deployment-config.ts @@ -0,0 +1,150 @@ +import cdk = require('@aws-cdk/cdk'); +import { cloudformation } from './codedeploy.generated'; + +export class DeploymentConfigName extends cdk.CloudFormationToken {} + +export class DeploymentConfigArn extends cdk.Arn {} + +export interface IServerDeploymentConfig { + readonly deploymentConfigName: DeploymentConfigName; + readonly deploymentConfigArn: DeploymentConfigArn; +} + +export interface ServerDeploymentConfigRefProps { + /** + * The physical, human-readable name of the custom CodeDeploy EC2/on-premise Deployment Configuration + * that we are referencing. + */ + deploymentConfigName: DeploymentConfigName; +} + +/** + * 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: DeploymentConfigName; + public abstract readonly deploymentConfigArn: DeploymentConfigArn; + + public export(): ServerDeploymentConfigRefProps { + return { + deploymentConfigName: new cdk.Output(this, 'DeploymentConfigName', { + value: this.deploymentConfigName, + }).makeImportValue(), + }; + } +} + +class ImportedServerDeploymentConfigRef extends ServerDeploymentConfigRef { + public readonly deploymentConfigName: DeploymentConfigName; + public readonly deploymentConfigArn: DeploymentConfigArn; + + 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: DeploymentConfigName; + public readonly deploymentConfigArn: DeploymentConfigArn; + + constructor(deploymentConfigName: string) { + this.deploymentConfigName = new 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: DeploymentConfigName; + public readonly deploymentConfigArn: DeploymentConfigArn; + + 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; + 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: DeploymentConfigName): + DeploymentConfigArn { + return cdk.Arn.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 e4cb8404cd3dc..c5ed30d290dea 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 { ApplicationName, cloudformation, DeploymentGroupName } from './codedeploy.generated'; +import { IServerDeploymentConfig, ServerDeploymentConfig } from "./deployment-config"; export class DeploymentGroupArn extends cdk.Arn {} @@ -23,6 +24,13 @@ export interface ServerDeploymentGroupRefProps { * that we are referencing. */ deploymentGroupName: DeploymentGroupName; + + /** + * The Deployment Configuration this Deployment Group uses. + * + * @default ServerDeploymentConfig#OneAtATime + */ + deploymentConfig?: IServerDeploymentConfig; } /** @@ -52,6 +60,12 @@ export abstract class ServerDeploymentGroupRef extends cdk.Construct { public abstract readonly application: ServerApplicationRef; public abstract readonly deploymentGroupName: DeploymentGroupName; public abstract readonly deploymentGroupArn: DeploymentGroupArn; + public readonly deploymentConfig: IServerDeploymentConfig; + + constructor(parent: cdk.Construct, id: string, deploymentConfig?: IServerDeploymentConfig) { + super(parent, id); + this.deploymentConfig = deploymentConfig || ServerDeploymentConfig.OneAtATime; + } public export(): ServerDeploymentGroupRefProps { return { @@ -69,7 +83,7 @@ class ImportedServerDeploymentGroupRef extends ServerDeploymentGroupRef { public readonly deploymentGroupArn: DeploymentGroupArn; constructor(parent: cdk.Construct, id: string, props: ServerDeploymentGroupRefProps) { - super(parent, id); + super(parent, id, props.deploymentConfig); this.application = props.application; this.deploymentGroupName = props.deploymentGroupName; @@ -100,6 +114,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; } /** @@ -112,7 +133,7 @@ export class ServerDeploymentGroup extends ServerDeploymentGroupRef { public readonly deploymentGroupName: DeploymentGroupName; constructor(parent: cdk.Construct, id: string, props?: ServerDeploymentGroupProps) { - super(parent, id); + super(parent, id, props && props.deploymentConfig); this.application = (props && props.application) || new ServerApplication(this, 'Application'); @@ -125,6 +146,8 @@ export class ServerDeploymentGroup extends ServerDeploymentGroupRef { applicationName: this.application.applicationName, deploymentGroupName: props && props.deploymentGroupName, serviceRoleArn: this.role.roleArn, + deploymentConfigName: props && props.deploymentConfig && + props.deploymentConfig.deploymentConfigName, }); this.deploymentGroupName = resource.ref; 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/package.json b/packages/@aws-cdk/aws-codedeploy/package.json index 6b4b32e3fd33f..5b94501ac2ed1 100644 --- a/packages/@aws-cdk/aws-codedeploy/package.json +++ b/packages/@aws-cdk/aws-codedeploy/package.json @@ -39,6 +39,10 @@ "cdk-build": { "cloudformation": "AWS::CodeDeploy" }, + "nyc": { + "lines": 50, + "branches": 40 + }, "keywords": [ "aws", "cdk", 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..138eae6ffebe2 --- /dev/null +++ b/packages/@aws-cdk/aws-codedeploy/test/test.deployment-config.ts @@ -0,0 +1,68 @@ +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(); + }, + }, +}; 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', {