diff --git a/.gitignore b/.gitignore index 6441c7528..559d96d2d 100644 --- a/.gitignore +++ b/.gitignore @@ -70,3 +70,4 @@ cdk.context.json *.tsbuildinfo tsconfig.json tsconfig.eslint.json +.idea diff --git a/lerna.json b/lerna.json index 4dd73b49d..0c0015760 100644 --- a/lerna.json +++ b/lerna.json @@ -12,4 +12,4 @@ "**/__fixtures__/**", "**/__tests__/**" ] -} +} \ No newline at end of file diff --git a/packages/cdk-blue-green-container-deployment/lambda-file-sizes.json b/packages/cdk-blue-green-container-deployment/lambda-file-sizes.json index 292a9dc16..97052cc01 100644 --- a/packages/cdk-blue-green-container-deployment/lambda-file-sizes.json +++ b/packages/cdk-blue-green-container-deployment/lambda-file-sizes.json @@ -1 +1 @@ -[{"timestamp":1643739695237,"files":[{"filename":"ecs-deployment-group/index.js","previous":148006,"size":147762,"diff":-244},{"filename":"ecs-service/index.js","previous":147514,"size":147469,"diff":-45}]},{"timestamp":1643739156615,"files":[{"filename":"ecs-deployment-group/index.js","previous":147762,"size":148006,"diff":244},{"filename":"ecs-service/index.js","previous":147469,"size":147514,"diff":45}]},{"timestamp":1643738715672,"files":[{"filename":"ecs-deployment-group/index.js","previous":148006,"size":147762,"diff":-244},{"filename":"ecs-service/index.js","previous":147514,"size":147469,"diff":-45}]},{"timestamp":1643738518282,"files":[{"filename":"ecs-deployment-group/index.js","previous":0,"size":148006,"diff":148006},{"filename":"ecs-service/index.js","previous":0,"size":147514,"diff":147514}]},{"timestamp":1643732386377,"files":[{"filename":"ecs-deployment-group/index.js","previous":147762,"size":0,"diff":-147762},{"filename":"ecs-service/index.js","previous":147469,"size":0,"diff":-147469}]},{"timestamp":1642109009944,"files":[{"filename":"ecs-deployment-group/index.js","previous":145598,"size":147762,"diff":2164},{"filename":"ecs-service/index.js","previous":145304,"size":147469,"diff":2165}]},{"timestamp":1639256487176,"files":[{"filename":"ecs-deployment-group/index.js","previous":145308,"size":145598,"diff":290},{"filename":"ecs-service/index.js","previous":145014,"size":145304,"diff":290}]},{"timestamp":1637794842477,"files":[{"filename":"ecs-deployment-group/index.js","previous":145307,"size":145308,"diff":1},{"filename":"ecs-service/index.js","previous":145016,"size":145014,"diff":-2}]},{"timestamp":1637781788109,"files":[{"filename":"ecs-deployment-group/index.js","previous":144989,"size":145307,"diff":318},{"filename":"ecs-service/index.js","previous":144998,"size":145016,"diff":18}]},{"timestamp":1637708323927,"files":[{"filename":"ecs-deployment-group/index.js","previous":0,"size":144989,"diff":144989},{"filename":"ecs-service/index.js","previous":0,"size":144998,"diff":144998}]}] +[{"timestamp":1690558790308,"files":[{"filename":"ecs-deployment-group/index.js","previous":147762,"size":147762,"diff":0},{"filename":"ecs-service/index.js","previous":147510,"size":147638,"diff":128}]},{"timestamp":1690499998459,"files":[{"filename":"ecs-deployment-group/index.js","previous":147762,"size":147762,"diff":0},{"filename":"ecs-service/index.js","previous":147497,"size":147510,"diff":13}]},{"timestamp":1690491995429,"files":[{"filename":"ecs-deployment-group/index.js","previous":147762,"size":147762,"diff":0},{"filename":"ecs-service/index.js","previous":147512,"size":147497,"diff":-15}]},{"timestamp":1690479814306,"files":[{"filename":"ecs-deployment-group/index.js","previous":147762,"size":147762,"diff":0},{"filename":"ecs-service/index.js","previous":147497,"size":147512,"diff":15}]},{"timestamp":1690475921041,"files":[{"filename":"ecs-deployment-group/index.js","previous":147762,"size":147762,"diff":0},{"filename":"ecs-service/index.js","previous":147496,"size":147497,"diff":1}]},{"timestamp":1689884928007,"files":[{"filename":"ecs-deployment-group/index.js","previous":147762,"size":147762,"diff":0},{"filename":"ecs-service/index.js","previous":147469,"size":147496,"diff":27}]},{"timestamp":1643739695237,"files":[{"filename":"ecs-deployment-group/index.js","previous":148006,"size":147762,"diff":-244},{"filename":"ecs-service/index.js","previous":147514,"size":147469,"diff":-45}]},{"timestamp":1643739156615,"files":[{"filename":"ecs-deployment-group/index.js","previous":147762,"size":148006,"diff":244},{"filename":"ecs-service/index.js","previous":147469,"size":147514,"diff":45}]},{"timestamp":1643738715672,"files":[{"filename":"ecs-deployment-group/index.js","previous":148006,"size":147762,"diff":-244},{"filename":"ecs-service/index.js","previous":147514,"size":147469,"diff":-45}]},{"timestamp":1643738518282,"files":[{"filename":"ecs-deployment-group/index.js","previous":0,"size":148006,"diff":148006},{"filename":"ecs-service/index.js","previous":0,"size":147514,"diff":147514}]},{"timestamp":1643732386377,"files":[{"filename":"ecs-deployment-group/index.js","previous":147762,"size":0,"diff":-147762},{"filename":"ecs-service/index.js","previous":147469,"size":0,"diff":-147469}]},{"timestamp":1642109009944,"files":[{"filename":"ecs-deployment-group/index.js","previous":145598,"size":147762,"diff":2164},{"filename":"ecs-service/index.js","previous":145304,"size":147469,"diff":2165}]},{"timestamp":1639256487176,"files":[{"filename":"ecs-deployment-group/index.js","previous":145308,"size":145598,"diff":290},{"filename":"ecs-service/index.js","previous":145014,"size":145304,"diff":290}]},{"timestamp":1637794842477,"files":[{"filename":"ecs-deployment-group/index.js","previous":145307,"size":145308,"diff":1},{"filename":"ecs-service/index.js","previous":145016,"size":145014,"diff":-2}]},{"timestamp":1637781788109,"files":[{"filename":"ecs-deployment-group/index.js","previous":144989,"size":145307,"diff":318},{"filename":"ecs-service/index.js","previous":144998,"size":145016,"diff":18}]},{"timestamp":1637708323927,"files":[{"filename":"ecs-deployment-group/index.js","previous":0,"size":144989,"diff":144989},{"filename":"ecs-service/index.js","previous":0,"size":144998,"diff":144998}]}] diff --git a/packages/cdk-blue-green-container-deployment/src/__tests__/__snapshots__/dummy-task-definition.test.ts.snap b/packages/cdk-blue-green-container-deployment/src/__tests__/__snapshots__/dummy-task-definition.test.ts.snap index 6cf8ee37e..f8430b123 100644 --- a/packages/cdk-blue-green-container-deployment/src/__tests__/__snapshots__/dummy-task-definition.test.ts.snap +++ b/packages/cdk-blue-green-container-deployment/src/__tests__/__snapshots__/dummy-task-definition.test.ts.snap @@ -103,6 +103,13 @@ Object { "Arn", ], }, + "\\",\\"taskRoleArn\\":\\"", + Object { + "Fn::GetAtt": Array [ + "DummyTaskDefinitionExecutionRole715DBD43", + "Arn", + ], + }, "\\",\\"networkMode\\":\\"awsvpc\\",\\"cpu\\":\\"256\\",\\"memory\\":\\"512\\",\\"containerDefinitions\\":[{\\"name\\":\\"sample-website\\",\\"image\\":\\"image\\",\\"portMappings\\":[{\\"hostPort\\":80,\\"protocol\\":\\"tcp\\",\\"containerPort\\":80}]}]},\\"physicalResourceId\\":{\\"responsePath\\":\\"taskDefinition.taskDefinitionArn\\"}}", ], ], @@ -126,6 +133,13 @@ Object { "Arn", ], }, + "\\",\\"taskRoleArn\\":\\"", + Object { + "Fn::GetAtt": Array [ + "DummyTaskDefinitionExecutionRole715DBD43", + "Arn", + ], + }, "\\",\\"networkMode\\":\\"awsvpc\\",\\"cpu\\":\\"256\\",\\"memory\\":\\"512\\",\\"containerDefinitions\\":[{\\"name\\":\\"sample-website\\",\\"image\\":\\"image\\",\\"portMappings\\":[{\\"hostPort\\":80,\\"protocol\\":\\"tcp\\",\\"containerPort\\":80}]}]},\\"physicalResourceId\\":{\\"responsePath\\":\\"taskDefinition.taskDefinitionArn\\"}}", ], ], diff --git a/packages/cdk-blue-green-container-deployment/src/__tests__/ecs-service.test.ts b/packages/cdk-blue-green-container-deployment/src/__tests__/ecs-service.test.ts index cbcef06a1..f9e12a92c 100644 --- a/packages/cdk-blue-green-container-deployment/src/__tests__/ecs-service.test.ts +++ b/packages/cdk-blue-green-container-deployment/src/__tests__/ecs-service.test.ts @@ -91,4 +91,54 @@ describe('EcsService', () => { ); }); }); + + describe('with execute command', () => { + const stack = new cdk.Stack(app, 'MyStackWithExecute'); + const cluster = new ecs.Cluster(stack, 'Cluster'); + const prodTargetGroup = new elb.ApplicationTargetGroup(stack, 'ProdTargetGroup', { vpc: cluster.vpc }); + const testTargetGroup = new elb.ApplicationTargetGroup(stack, 'TestTargetGroup', { vpc: cluster.vpc }); + const taskDefinition = new DummyTaskDefinition(stack, 'DummyTaskDefinition', { image: 'nginx' }); + const enableExecuteCommand = true + + new EcsService(stack, 'Service', { + cluster, + serviceName: 'My Service', + prodTargetGroup, + testTargetGroup, + taskDefinition, + enableExecuteCommand + }); + + test('Creates a BlueGreenService custom resource', () => { + expectCDK(stack).to( + haveResource('Custom::BlueGreenService', { + EnableExecuteCommand: true, + }), + ); + }); + }); + + describe('execute command defaults to false', () => { + const stack = new cdk.Stack(app, 'MyStackWithout'); + const cluster = new ecs.Cluster(stack, 'Cluster'); + const prodTargetGroup = new elb.ApplicationTargetGroup(stack, 'ProdTargetGroup', { vpc: cluster.vpc }); + const testTargetGroup = new elb.ApplicationTargetGroup(stack, 'TestTargetGroup', { vpc: cluster.vpc }); + const taskDefinition = new DummyTaskDefinition(stack, 'DummyTaskDefinition', { image: 'nginx' }); + + new EcsService(stack, 'Service', { + cluster, + serviceName: 'My Service', + prodTargetGroup, + testTargetGroup, + taskDefinition + }); + + test('Creates a BlueGreenService custom resource', () => { + expectCDK(stack).to( + haveResource('Custom::BlueGreenService', { + EnableExecuteCommand: false, + }), + ); + }); + }); }); diff --git a/packages/cdk-blue-green-container-deployment/src/__tests__/lambdas/__fixtures__/default-ecs-service-resource-properties.ts b/packages/cdk-blue-green-container-deployment/src/__tests__/lambdas/__fixtures__/default-ecs-service-resource-properties.ts index d9cc0911f..21c43508c 100644 --- a/packages/cdk-blue-green-container-deployment/src/__tests__/lambdas/__fixtures__/default-ecs-service-resource-properties.ts +++ b/packages/cdk-blue-green-container-deployment/src/__tests__/lambdas/__fixtures__/default-ecs-service-resource-properties.ts @@ -16,4 +16,5 @@ export const defaultEcsServiceResourceProperties = { DeploymentConfiguration: {}, PropogateTags: 'SERVICE', Tags: [], + EnableExecuteCommand: false }; diff --git a/packages/cdk-blue-green-container-deployment/src/__tests__/lambdas/ecs-service/index.test.ts b/packages/cdk-blue-green-container-deployment/src/__tests__/lambdas/ecs-service/index.test.ts index a127ae7c5..128157f18 100644 --- a/packages/cdk-blue-green-container-deployment/src/__tests__/lambdas/ecs-service/index.test.ts +++ b/packages/cdk-blue-green-container-deployment/src/__tests__/lambdas/ecs-service/index.test.ts @@ -81,6 +81,48 @@ describe('createHandler', () => { }, }); }); + + test('sends enable execute true command with create request', async () => { + const response = await handleCreate( + { + ...defaultEvent, + RequestType: 'Create', + ResourceProperties: { + ...defaultEcsServiceResourceProperties, + EnableExecuteCommand: true, + }, + }, + defaultContext, + defaultLogger, + ); + + expect(mockCreateRequest).toHaveBeenCalledWith( + expect.objectContaining({ + enableExecuteCommand: true, + }), + ); + }); + + test('sends enable execute false command with create request', async () => { + const response = await handleCreate( + { + ...defaultEvent, + RequestType: 'Create', + ResourceProperties: { + ...defaultEcsServiceResourceProperties, + EnableExecuteCommand: false, + }, + }, + defaultContext, + defaultLogger, + ); + + expect(mockCreateRequest).toHaveBeenCalledWith( + expect.objectContaining({ + enableExecuteCommand: false, + }), + ); + }); }); describe('updateHandler', () => { @@ -138,6 +180,31 @@ describe('updateHandler', () => { ); }); + test('update enable execute command', async () => { + await handleUpdate( + { + ...defaultEvent, + RequestType: 'Update', + PhysicalResourceId: 'foo', + ResourceProperties: { + ...defaultEcsServiceResourceProperties, + EnableExecuteCommand: true, + }, + OldResourceProperties: { + ...defaultEcsServiceResourceProperties, + }, + }, + defaultContext, + defaultLogger, + ); + + expect(mockUpdateRequest).toHaveBeenCalledWith( + expect.objectContaining({ + enableExecuteCommand: true + }), + ); + }); + test('does not delete keys if no old keys are deleted', async () => { await handleUpdate( { diff --git a/packages/cdk-blue-green-container-deployment/src/dummy-task-definition.ts b/packages/cdk-blue-green-container-deployment/src/dummy-task-definition.ts index b78fc4fca..6239f33b1 100644 --- a/packages/cdk-blue-green-container-deployment/src/dummy-task-definition.ts +++ b/packages/cdk-blue-green-container-deployment/src/dummy-task-definition.ts @@ -1,6 +1,13 @@ import { ITaggable, TagManager, TagType, Lazy } from 'aws-cdk-lib'; import { NetworkMode } from 'aws-cdk-lib/aws-ecs'; -import { Role, ServicePrincipal, ManagedPolicy, PolicyStatement, Effect, IRole } from 'aws-cdk-lib/aws-iam'; +import { + Role, + ServicePrincipal, + ManagedPolicy, + PolicyStatement, + Effect, + IRole, +} from 'aws-cdk-lib/aws-iam'; import { AwsCustomResource, AwsCustomResourcePolicy, @@ -81,6 +88,7 @@ export class DummyTaskDefinition extends Construct implements IDummyTaskDefiniti requiresCompatibilities: ['FARGATE'], family: this.family, executionRoleArn: this.executionRole.roleArn, + taskRoleArn: this.executionRole.roleArn, networkMode: NetworkMode.AWS_VPC, cpu: '256', memory: '512', diff --git a/packages/cdk-blue-green-container-deployment/src/ecs-service.ts b/packages/cdk-blue-green-container-deployment/src/ecs-service.ts index e5bae9c5a..322d359e9 100644 --- a/packages/cdk-blue-green-container-deployment/src/ecs-service.ts +++ b/packages/cdk-blue-green-container-deployment/src/ecs-service.ts @@ -25,6 +25,7 @@ export interface EcsServiceProps { readonly prodTargetGroup: ITargetGroup; readonly testTargetGroup: ITargetGroup; readonly taskDefinition: DummyTaskDefinition; + readonly enableExecuteCommand?: boolean; /** * The period of time, in seconds, that the Amazon ECS service scheduler ignores unhealthy @@ -85,6 +86,7 @@ export class EcsService extends Construct implements IConnectable, IEcsService, testTargetGroup, taskDefinition, healthCheckGracePeriod = Duration.seconds(60), + enableExecuteCommand = false, } = props; this.tags = new TagManager(TagType.KEY_VALUE, 'TagManager'); @@ -142,6 +144,7 @@ export class EcsService extends Construct implements IConnectable, IEcsService, ContainerPort: containerPort, SchedulingStrategy: SchedulingStrategy.REPLICA, HealthCheckGracePeriodSeconds: healthCheckGracePeriod.toSeconds(), + EnableExecuteCommand: enableExecuteCommand, PropagateTags: props.propagateTags, DeploymentConfiguration: { maximumPercent: props.maxHealthyPercent ?? 200, diff --git a/packages/cdk-blue-green-container-deployment/src/lambdas/ecs-service/index.ts b/packages/cdk-blue-green-container-deployment/src/lambdas/ecs-service/index.ts index 3202d470c..939bdce25 100644 --- a/packages/cdk-blue-green-container-deployment/src/lambdas/ecs-service/index.ts +++ b/packages/cdk-blue-green-container-deployment/src/lambdas/ecs-service/index.ts @@ -31,10 +31,20 @@ export interface BlueGreenServiceProps { deploymentConfiguration: ECS.DeploymentConfiguration; propagateTags: 'SERVICE' | 'TASK_DEFINITION' | string; tags: Tag[]; + enableExecuteCommand: boolean; } const ecs = new ECS(); +// this method is required to fix a Cloudformation bug which causes properties to be converted to strings. +// Can be removed once resolved: https://github.com/aws-cloudformation/cloudformation-coverage-roadmap/issues/1037 +function booleanize(candidate: string | boolean) : boolean { + if(typeof candidate === "boolean") { + return candidate; + } + return candidate === "true"; +} + const getProperties = (props: CloudFormationCustomResourceEvent['ResourceProperties']): BlueGreenServiceProps => ({ cluster: props.Cluster, serviceName: props.ServiceName, @@ -52,6 +62,7 @@ const getProperties = (props: CloudFormationCustomResourceEvent['ResourcePropert deploymentConfiguration: props.DeploymentConfiguration, propagateTags: props.PropagateTags, tags: props.Tags ?? [], + enableExecuteCommand: booleanize(props.EnableExecuteCommand), }); export const handleCreate: OnCreateHandler = async (event): Promise => { @@ -72,6 +83,7 @@ export const handleCreate: OnCreateHandler = async (event): Promise { return { key: t.Key, value: t.Value }; }), + enableExecuteCommand }) .promise(); @@ -127,7 +140,7 @@ export const handleCreate: OnCreateHandler = async (event): Promise => { - const { cluster, serviceName, desiredCount, deploymentConfiguration, healthCheckGracePeriodSeconds, tags } = getProperties( + const { cluster, serviceName, desiredCount, deploymentConfiguration, healthCheckGracePeriodSeconds, tags, enableExecuteCommand } = getProperties( event.ResourceProperties, ); @@ -138,6 +151,7 @@ export const handleUpdate: OnUpdateHandler = async (event): Promise