Skip to content

Commit

Permalink
feat: add option to specify number of minutes to wait for deployment …
Browse files Browse the repository at this point in the history
…to complete

Fixes #33
  • Loading branch information
clareliguori committed Mar 2, 2020
1 parent 6f86f5b commit 753c9b3
Show file tree
Hide file tree
Showing 3 changed files with 243 additions and 21 deletions.
3 changes: 3 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ inputs:
wait-for-service-stability:
description: 'Whether to wait for the ECS service to reach stable state after deploying the new task definition. Valid value is "true". Will default to not waiting.'
required: false
wait-for-minutes:
description: 'How long to wait for the ECS service to reach stable state, in minutes (default: 30 minutes, max: 6 hours). For CodeDeploy deployments, any wait time configured in the CodeDeploy deployment group will be added to this value.'
required: false
codedeploy-appspec:
description: "The path to the AWS CodeDeploy AppSpec file, if the ECS service uses the CODE_DEPLOY deployment controller. Will default to 'appspec.yaml'."
required: false
Expand Down
40 changes: 22 additions & 18 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@ const yaml = require('yaml');
const fs = require('fs');
const crypto = require('crypto');

const CODE_DEPLOY_WAIT_BUFFER_MINUTES = 10;
const CODE_DEPLOY_MAX_WAIT_MINUTES = 360; // 6 hours
const CODE_DEPLOY_MIN_WAIT_MINUTES = 30;
const CODE_DEPLOY_WAIT_DEFAULT_DELAY_SEC = 15;
const MAX_WAIT_MINUTES = 360; // 6 hours
const WAIT_DEFAULT_DELAY_SEC = 15;

// Attributes that are returned by DescribeTaskDefinition, but are not valid RegisterTaskDefinition inputs
const IGNORED_TASK_DEFINITION_ATTRIBUTES = [
Expand All @@ -20,7 +18,7 @@ const IGNORED_TASK_DEFINITION_ATTRIBUTES = [
];

// Deploy to a service that uses the 'ECS' deployment controller
async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForService) {
async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes) {
core.debug('Updating the service');
await ecs.updateService({
cluster: clusterName,
Expand All @@ -30,10 +28,15 @@ async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForSe

// Wait for service stability
if (waitForService && waitForService.toLowerCase() === 'true') {
core.debug('Waiting for the service to become stable');
core.debug(`Waiting for the service to become stable. Will wait for ${waitForMinutes} minutes`);
const maxAttempts = (waitForMinutes * 60) / WAIT_DEFAULT_DELAY_SEC;
await ecs.waitFor('servicesStable', {
services: [service],
cluster: clusterName
cluster: clusterName,
$waiter: {
delay: WAIT_DEFAULT_DELAY_SEC,
maxAttempts: maxAttempts
}
}).promise();
} else {
core.debug('Not waiting for the service to become stable');
Expand Down Expand Up @@ -88,7 +91,7 @@ function removeIgnoredAttributes(taskDef) {
}

// Deploy to a service that uses the 'CODE_DEPLOY' deployment controller
async function createCodeDeployDeployment(codedeploy, clusterName, service, taskDefArn, waitForService) {
async function createCodeDeployDeployment(codedeploy, clusterName, service, taskDefArn, waitForService, waitForMinutes) {
core.debug('Updating AppSpec file with new task definition ARN');

let codeDeployAppSpecFile = core.getInput('codedeploy-appspec', { required : false });
Expand Down Expand Up @@ -145,20 +148,17 @@ async function createCodeDeployDeployment(codedeploy, clusterName, service, task
// Determine wait time
const deployReadyWaitMin = deploymentGroupDetails.blueGreenDeploymentConfiguration.deploymentReadyOption.waitTimeInMinutes;
const terminationWaitMin = deploymentGroupDetails.blueGreenDeploymentConfiguration.terminateBlueInstancesOnDeploymentSuccess.terminationWaitTimeInMinutes;
let totalWaitMin = deployReadyWaitMin + terminationWaitMin + CODE_DEPLOY_WAIT_BUFFER_MINUTES;
if (totalWaitMin > CODE_DEPLOY_MAX_WAIT_MINUTES) {
totalWaitMin = CODE_DEPLOY_MAX_WAIT_MINUTES;
let totalWaitMin = deployReadyWaitMin + terminationWaitMin + waitForMinutes;
if (totalWaitMin > MAX_WAIT_MINUTES) {
totalWaitMin = MAX_WAIT_MINUTES;
}
if (totalWaitMin < CODE_DEPLOY_MIN_WAIT_MINUTES) {
totalWaitMin = CODE_DEPLOY_MIN_WAIT_MINUTES;
}
const maxAttempts = (totalWaitMin * 60) / CODE_DEPLOY_WAIT_DEFAULT_DELAY_SEC;
const maxAttempts = (totalWaitMin * 60) / WAIT_DEFAULT_DELAY_SEC;

core.debug(`Waiting for the deployment to complete. Will wait for ${totalWaitMin} minutes`);
await codedeploy.waitFor('deploymentSuccessful', {
deploymentId: createDeployResponse.deploymentId,
$waiter: {
delay: CODE_DEPLOY_WAIT_DEFAULT_DELAY_SEC,
delay: WAIT_DEFAULT_DELAY_SEC,
maxAttempts: maxAttempts
}
}).promise();
Expand All @@ -181,6 +181,10 @@ async function run() {
const service = core.getInput('service', { required: false });
const cluster = core.getInput('cluster', { required: false });
const waitForService = core.getInput('wait-for-service-stability', { required: false });
let waitForMinutes = parseInt(core.getInput('wait-for-minutes', { required: false })) || 30;
if (waitForMinutes > MAX_WAIT_MINUTES) {
waitForMinutes = MAX_WAIT_MINUTES;
}

// Register the task definition
core.debug('Registering the task definition');
Expand Down Expand Up @@ -215,10 +219,10 @@ async function run() {

if (!serviceResponse.deploymentController) {
// Service uses the 'ECS' deployment controller, so we can call UpdateService
await updateEcsService(ecs, clusterName, service, taskDefArn, waitForService);
await updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes);
} else if (serviceResponse.deploymentController.type == 'CODE_DEPLOY') {
// Service uses CodeDeploy, so we should start a CodeDeploy deployment
await createCodeDeployDeployment(codedeploy, clusterName, service, taskDefArn, waitForService);
await createCodeDeployDeployment(codedeploy, clusterName, service, taskDefArn, waitForService, waitForMinutes);
} else {
throw new Error(`Unsupported deployment controller: ${serviceResponse.deploymentController.type}`);
}
Expand Down
221 changes: 218 additions & 3 deletions index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ describe('Deploy to ECS', () => {
expect(mockEcsRegisterTaskDef).toHaveBeenNthCalledWith(1, { family: 'task-def-family'});
});

test('registers the task definition contents and creates a CodeDeploy deployment', async () => {
test('registers the task definition contents and creates a CodeDeploy deployment, waits for 30 minutes + deployment group wait time', async () => {
core.getInput = jest
.fn()
.mockReturnValueOnce('task-definition.json') // task-definition
Expand Down Expand Up @@ -247,7 +247,151 @@ describe('Deploy to ECS', () => {
deploymentId: 'deployment-1',
$waiter: {
delay: 15,
maxAttempts: 400
maxAttempts: (30 + 90) * 4
}
});

expect(mockEcsUpdateService).toHaveBeenCalledTimes(0);
expect(mockEcsWaiter).toHaveBeenCalledTimes(0);
});

test('registers the task definition contents and creates a CodeDeploy deployment, waits for 1 hour + deployment group\'s wait time', async () => {
core.getInput = jest
.fn()
.mockReturnValueOnce('task-definition.json') // task-definition
.mockReturnValueOnce('service-456') // service
.mockReturnValueOnce('cluster-789') // cluster
.mockReturnValueOnce('TRUE') // wait-for-service-stability
.mockReturnValueOnce('60'); // wait-for-minutes

mockEcsDescribeServices.mockImplementation(() => {
return {
promise() {
return Promise.resolve({
failures: [],
services: [{
status: 'ACTIVE',
deploymentController: {
type: 'CODE_DEPLOY'
}
}]
});
}
};
});

await run();
expect(core.setFailed).toHaveBeenCalledTimes(0);

expect(mockEcsRegisterTaskDef).toHaveBeenNthCalledWith(1, { family: 'task-def-family'});
expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition-arn', 'task:def:arn');
expect(mockEcsDescribeServices).toHaveBeenNthCalledWith(1, {
cluster: 'cluster-789',
services: ['service-456']
});

expect(mockCodeDeployCreateDeployment).toHaveBeenNthCalledWith(1, {
applicationName: 'AppECS-cluster-789-service-456',
deploymentGroupName: 'DgpECS-cluster-789-service-456',
revision: {
revisionType: 'AppSpecContent',
appSpecContent: {
content: JSON.stringify({
Resources: [{
TargetService: {
Type: 'AWS::ECS::Service',
Properties: {
TaskDefinition: 'task:def:arn',
LoadBalancerInfo: {
ContainerName: "web",
ContainerPort: 80
}
}
}
}]
}),
sha256: '0911d1e99f48b492e238d1284d8ddb805382d33e1d1fc74ffadf37d8b7e6d096'
}
}
});

expect(mockCodeDeployWaiter).toHaveBeenNthCalledWith(1, 'deploymentSuccessful', {
deploymentId: 'deployment-1',
$waiter: {
delay: 15,
maxAttempts: (60 + 90) * 4
}
});

expect(mockEcsUpdateService).toHaveBeenCalledTimes(0);
expect(mockEcsWaiter).toHaveBeenCalledTimes(0);
});

test('registers the task definition contents and creates a CodeDeploy deployment, waits for max 6 hours', async () => {
core.getInput = jest
.fn()
.mockReturnValueOnce('task-definition.json') // task-definition
.mockReturnValueOnce('service-456') // service
.mockReturnValueOnce('cluster-789') // cluster
.mockReturnValueOnce('TRUE') // wait-for-service-stability
.mockReturnValueOnce('1000'); // wait-for-minutes

mockEcsDescribeServices.mockImplementation(() => {
return {
promise() {
return Promise.resolve({
failures: [],
services: [{
status: 'ACTIVE',
deploymentController: {
type: 'CODE_DEPLOY'
}
}]
});
}
};
});

await run();
expect(core.setFailed).toHaveBeenCalledTimes(0);

expect(mockEcsRegisterTaskDef).toHaveBeenNthCalledWith(1, { family: 'task-def-family'});
expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition-arn', 'task:def:arn');
expect(mockEcsDescribeServices).toHaveBeenNthCalledWith(1, {
cluster: 'cluster-789',
services: ['service-456']
});

expect(mockCodeDeployCreateDeployment).toHaveBeenNthCalledWith(1, {
applicationName: 'AppECS-cluster-789-service-456',
deploymentGroupName: 'DgpECS-cluster-789-service-456',
revision: {
revisionType: 'AppSpecContent',
appSpecContent: {
content: JSON.stringify({
Resources: [{
TargetService: {
Type: 'AWS::ECS::Service',
Properties: {
TaskDefinition: 'task:def:arn',
LoadBalancerInfo: {
ContainerName: "web",
ContainerPort: 80
}
}
}
}]
}),
sha256: '0911d1e99f48b492e238d1284d8ddb805382d33e1d1fc74ffadf37d8b7e6d096'
}
}
});

expect(mockCodeDeployWaiter).toHaveBeenNthCalledWith(1, 'deploymentSuccessful', {
deploymentId: 'deployment-1',
$waiter: {
delay: 15,
maxAttempts: 6 * 60 * 4
}
});

Expand All @@ -262,6 +406,7 @@ describe('Deploy to ECS', () => {
.mockReturnValueOnce('service-456') // service
.mockReturnValueOnce('cluster-789') // cluster
.mockReturnValueOnce('false') // wait-for-service-stability
.mockReturnValueOnce('') // wait-for-minutes
.mockReturnValueOnce('/hello/appspec.json') // codedeploy-appspec
.mockReturnValueOnce('MyApplication') // codedeploy-application
.mockReturnValueOnce('MyDeploymentGroup'); // codedeploy-deployment-group
Expand Down Expand Up @@ -418,7 +563,77 @@ describe('Deploy to ECS', () => {
});
expect(mockEcsWaiter).toHaveBeenNthCalledWith(1, 'servicesStable', {
services: ['service-456'],
cluster: 'cluster-789'
cluster: 'cluster-789',
"$waiter": {
"delay": 15,
"maxAttempts": 120,
},
});
});

test('waits for the service to be stable for specified minutes', async () => {
core.getInput = jest
.fn()
.mockReturnValueOnce('task-definition.json') // task-definition
.mockReturnValueOnce('service-456') // service
.mockReturnValueOnce('cluster-789') // cluster
.mockReturnValueOnce('TRUE') // wait-for-service-stability
.mockReturnValue('60'); // wait-for-minutes

await run();
expect(core.setFailed).toHaveBeenCalledTimes(0);

expect(mockEcsRegisterTaskDef).toHaveBeenNthCalledWith(1, { family: 'task-def-family'});
expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition-arn', 'task:def:arn');
expect(mockEcsDescribeServices).toHaveBeenNthCalledWith(1, {
cluster: 'cluster-789',
services: ['service-456']
});
expect(mockEcsUpdateService).toHaveBeenNthCalledWith(1, {
cluster: 'cluster-789',
service: 'service-456',
taskDefinition: 'task:def:arn'
});
expect(mockEcsWaiter).toHaveBeenNthCalledWith(1, 'servicesStable', {
services: ['service-456'],
cluster: 'cluster-789',
"$waiter": {
"delay": 15,
"maxAttempts": 240,
},
});
});

test('waits for the service to be stable for max 6 hours', async () => {
core.getInput = jest
.fn()
.mockReturnValueOnce('task-definition.json') // task-definition
.mockReturnValueOnce('service-456') // service
.mockReturnValueOnce('cluster-789') // cluster
.mockReturnValueOnce('TRUE') // wait-for-service-stability
.mockReturnValue('1000'); // wait-for-minutes

await run();
expect(core.setFailed).toHaveBeenCalledTimes(0);

expect(mockEcsRegisterTaskDef).toHaveBeenNthCalledWith(1, { family: 'task-def-family'});
expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition-arn', 'task:def:arn');
expect(mockEcsDescribeServices).toHaveBeenNthCalledWith(1, {
cluster: 'cluster-789',
services: ['service-456']
});
expect(mockEcsUpdateService).toHaveBeenNthCalledWith(1, {
cluster: 'cluster-789',
service: 'service-456',
taskDefinition: 'task:def:arn'
});
expect(mockEcsWaiter).toHaveBeenNthCalledWith(1, 'servicesStable', {
services: ['service-456'],
cluster: 'cluster-789',
"$waiter": {
"delay": 15,
"maxAttempts": 6 * 60 * 4,
},
});
});

Expand Down

0 comments on commit 753c9b3

Please sign in to comment.