Skip to content

Commit 94fe59f

Browse files
romankydybetsKollaAdithya
authored andcommitted
feat: Add desired tasks (#505)
* add desired count * add desired count * add desired count * fix test for desired count * small change * update dist/index.js * address feedback @iamhopaul123 * update dist/index.js --------- Co-authored-by: Adithya Kolla <[email protected]>
1 parent ee7a5be commit 94fe59f

File tree

5 files changed

+422
-24
lines changed

5 files changed

+422
-24
lines changed

action.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ inputs:
77
task-definition:
88
description: 'The path to the ECS task definition file to register'
99
required: true
10+
desired-count:
11+
description: 'The number of instantiations of the task to place and keep running in your service.'
12+
required: false
1013
service:
1114
description: 'The name of the ECS service to deploy to. The action will only register the task definition if no service is given.'
1215
required: false
@@ -34,6 +37,21 @@ inputs:
3437
force-new-deployment:
3538
description: 'Whether to force a new deployment of the service. Valid value is "true". Will default to not force a new deployment.'
3639
required: false
40+
41+
init-task-command:
42+
description: 'A command to be run as a task before deploy.'
43+
required: false
44+
init-task-name:
45+
description: 'Container name for init run task.'
46+
default: 'app'
47+
required: false
48+
init-task-network-configuration:
49+
description: 'Network configuration as json string required for network mode `awsvpc`.'
50+
required: false
51+
started-by:
52+
description: "The value of the task started-by"
53+
required: false
54+
3755
outputs:
3856
task-definition-arn:
3957
description: 'The ARN of the registered ECS task definition'

dist/index.js

Lines changed: 123 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,123 @@ const IGNORED_TASK_DEFINITION_ATTRIBUTES = [
2626
'registeredBy'
2727
];
2828

29+
async function runInitTask(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, initTaskCommand) {
30+
core.info(`Starting init task "${initTaskCommand}"`)
31+
32+
const agent = 'amazon-ecs-run-task-for-github-actions'
33+
const startedBy = core.getInput('started-by', { required: false }) || agent;
34+
const networkConfiguration = JSON.parse(core.getInput('init-task-network-configuration', { required : false }));
35+
const containerName = core.getInput('init-task-name', { required : false })
36+
37+
const runTaskResponse = await ecs.runTask({
38+
startedBy: startedBy,
39+
cluster: clusterName,
40+
taskDefinition: taskDefArn,
41+
enableExecuteCommand: true,
42+
overrides: {
43+
containerOverrides: [
44+
{
45+
name: containerName,
46+
command: initTaskCommand.split(' ')
47+
}
48+
]
49+
},
50+
launchType: 'FARGATE',
51+
networkConfiguration: networkConfiguration
52+
}).promise();
53+
54+
core.debug(`Run task response ${JSON.stringify(runTaskResponse)}`)
55+
56+
const taskArns = runTaskResponse.tasks.map(task => task.taskArn);
57+
core.setOutput('init-task-arn', taskArns);
58+
59+
taskArns.map(taskArn =>{
60+
let taskId = taskArn.split('/').pop();
61+
62+
core.info(`Init task started. Watch the task logs in https://${aws.config.region}.console.aws.amazon.com/cloudwatch/home?region=${aws.config.region}#logsV2:log-groups/log-group/ecs$252F${service}/log-events/ecs$252Fapp$252F${taskId}`)
63+
});
64+
65+
if (runTaskResponse.failures && runTaskResponse.failures.length > 0) {
66+
const failure = runTaskResponse.failures[0];
67+
throw new Error(`${failure.arn} is ${failure.reason}`);
68+
}
69+
70+
// Wait for task to end
71+
if (waitForService && waitForService.toLowerCase() === 'true') {
72+
core.debug(`Waiting for the service to become stable. Will wait for ${waitForMinutes} minutes`);
73+
await waitForTasksStopped(ecs, clusterName, taskArns, waitForMinutes)
74+
await tasksExitCode(ecs, clusterName, taskArns)
75+
} else {
76+
core.debug('Not waiting for the service to become stable');
77+
}
78+
}
79+
80+
async function waitForTasksStopped(ecs, clusterName, taskArns, waitForMinutes) {
81+
if (waitForMinutes > MAX_WAIT_MINUTES) {
82+
waitForMinutes = MAX_WAIT_MINUTES;
83+
}
84+
85+
const maxAttempts = (waitForMinutes * 60) / WAIT_DEFAULT_DELAY_SEC;
86+
87+
core.info('Waiting for tasks to stop');
88+
89+
const waitTaskResponse = await ecs.waitFor('tasksStopped', {
90+
cluster: clusterName,
91+
tasks: taskArns,
92+
$waiter: {
93+
delay: WAIT_DEFAULT_DELAY_SEC,
94+
maxAttempts: maxAttempts
95+
}
96+
}).promise();
97+
98+
core.debug(`Run task response ${JSON.stringify(waitTaskResponse)}`)
99+
100+
core.info(`All tasks have stopped. Watch progress in the Amazon ECS console: https://console.aws.amazon.com/ecs/home?region=${aws.config.region}#/clusters/${clusterName}/tasks`);
101+
}
102+
103+
async function tasksExitCode(ecs, clusterName, taskArns) {
104+
const describeResponse = await ecs.describeTasks({
105+
cluster: clusterName,
106+
tasks: taskArns
107+
}).promise();
108+
109+
const containers = [].concat(...describeResponse.tasks.map(task => task.containers))
110+
const exitCodes = containers.map(container => container.exitCode)
111+
const reasons = containers.map(container => container.reason)
112+
113+
const failuresIdx = [];
114+
115+
exitCodes.filter((exitCode, index) => {
116+
if (exitCode !== 0) {
117+
failuresIdx.push(index)
118+
}
119+
})
120+
121+
const failures = reasons.filter((_, index) => failuresIdx.indexOf(index) !== -1)
122+
123+
if (failures.length > 0) {
124+
console.log(`failed to with exit code${failures}`)
125+
core.setFailed(failures.join("\n"));
126+
throw new Error(`Run task failed: ${JSON.stringify(failures)}`);
127+
} else {
128+
core.info(`All tasks have exited successfully.`);
129+
}
130+
}
131+
29132
// Deploy to a service that uses the 'ECS' deployment controller
30-
async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment) {
133+
async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment, desiredCount) {
31134
core.debug('Updating the service');
32-
await ecs.updateService({
135+
let params = {
33136
cluster: clusterName,
34137
service: service,
35138
taskDefinition: taskDefArn,
36139
forceNewDeployment: forceNewDeployment
37-
}).promise();
140+
};
141+
// Add the desiredCount property only if it is defined and a number.
142+
if (!isNaN(desiredCount) && desiredCount !== undefined) {
143+
params.desiredCount = desiredCount;
144+
}
145+
await ecs.updateService(params).promise();
38146

39147
const consoleHostname = aws.config.region.startsWith('cn') ? 'console.amazonaws.cn' : 'console.aws.amazon.com';
40148

@@ -268,13 +376,17 @@ async function run() {
268376
const cluster = core.getInput('cluster', { required: false });
269377
const waitForService = core.getInput('wait-for-service-stability', { required: false });
270378
let waitForMinutes = parseInt(core.getInput('wait-for-minutes', { required: false })) || 30;
379+
const initTaskCommand = core.getInput('init-task-command');
271380
if (waitForMinutes > MAX_WAIT_MINUTES) {
272381
waitForMinutes = MAX_WAIT_MINUTES;
273382
}
274383

275384
const forceNewDeployInput = core.getInput('force-new-deployment', { required: false }) || 'false';
276385
const forceNewDeployment = forceNewDeployInput.toLowerCase() === 'true';
277386

387+
const desiredCount = parseInt((core.getInput('desired-count', {required: false})));
388+
389+
278390
// Register the task definition
279391
core.debug('Registering the task definition');
280392
const taskDefPath = path.isAbsolute(taskDefinitionFile) ?
@@ -314,9 +426,15 @@ async function run() {
314426
throw new Error(`Service is ${serviceResponse.status}`);
315427
}
316428

429+
if (initTaskCommand) {
430+
await runInitTask(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, initTaskCommand);
431+
} else {
432+
core.debug('InitTaskCommand was not specified, no init run task.');
433+
}
434+
317435
if (!serviceResponse.deploymentController || !serviceResponse.deploymentController.type || serviceResponse.deploymentController.type === 'ECS') {
318436
// Service uses the 'ECS' deployment controller, so we can call UpdateService
319-
await updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment);
437+
await updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment, desiredCount);
320438
} else if (serviceResponse.deploymentController.type === 'CODE_DEPLOY') {
321439
// Service uses CodeDeploy, so we should start a CodeDeploy deployment
322440
await createCodeDeployDeployment(codedeploy, clusterName, service, taskDefArn, waitForService, waitForMinutes);
@@ -328,6 +446,7 @@ async function run() {
328446
}
329447
}
330448
catch (error) {
449+
console.log(error);
331450
core.setFailed(error.message);
332451
core.debug(error.stack);
333452
}

index.js

Lines changed: 123 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,123 @@ const IGNORED_TASK_DEFINITION_ATTRIBUTES = [
2020
'registeredBy'
2121
];
2222

23+
async function runInitTask(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, initTaskCommand) {
24+
core.info(`Starting init task "${initTaskCommand}"`)
25+
26+
const agent = 'amazon-ecs-run-task-for-github-actions'
27+
const startedBy = core.getInput('started-by', { required: false }) || agent;
28+
const networkConfiguration = JSON.parse(core.getInput('init-task-network-configuration', { required : false }));
29+
const containerName = core.getInput('init-task-name', { required : false })
30+
31+
const runTaskResponse = await ecs.runTask({
32+
startedBy: startedBy,
33+
cluster: clusterName,
34+
taskDefinition: taskDefArn,
35+
enableExecuteCommand: true,
36+
overrides: {
37+
containerOverrides: [
38+
{
39+
name: containerName,
40+
command: initTaskCommand.split(' ')
41+
}
42+
]
43+
},
44+
launchType: 'FARGATE',
45+
networkConfiguration: networkConfiguration
46+
}).promise();
47+
48+
core.debug(`Run task response ${JSON.stringify(runTaskResponse)}`)
49+
50+
const taskArns = runTaskResponse.tasks.map(task => task.taskArn);
51+
core.setOutput('init-task-arn', taskArns);
52+
53+
taskArns.map(taskArn =>{
54+
let taskId = taskArn.split('/').pop();
55+
56+
core.info(`Init task started. Watch the task logs in https://${aws.config.region}.console.aws.amazon.com/cloudwatch/home?region=${aws.config.region}#logsV2:log-groups/log-group/ecs$252F${service}/log-events/ecs$252Fapp$252F${taskId}`)
57+
});
58+
59+
if (runTaskResponse.failures && runTaskResponse.failures.length > 0) {
60+
const failure = runTaskResponse.failures[0];
61+
throw new Error(`${failure.arn} is ${failure.reason}`);
62+
}
63+
64+
// Wait for task to end
65+
if (waitForService && waitForService.toLowerCase() === 'true') {
66+
core.debug(`Waiting for the service to become stable. Will wait for ${waitForMinutes} minutes`);
67+
await waitForTasksStopped(ecs, clusterName, taskArns, waitForMinutes)
68+
await tasksExitCode(ecs, clusterName, taskArns)
69+
} else {
70+
core.debug('Not waiting for the service to become stable');
71+
}
72+
}
73+
74+
async function waitForTasksStopped(ecs, clusterName, taskArns, waitForMinutes) {
75+
if (waitForMinutes > MAX_WAIT_MINUTES) {
76+
waitForMinutes = MAX_WAIT_MINUTES;
77+
}
78+
79+
const maxAttempts = (waitForMinutes * 60) / WAIT_DEFAULT_DELAY_SEC;
80+
81+
core.info('Waiting for tasks to stop');
82+
83+
const waitTaskResponse = await ecs.waitFor('tasksStopped', {
84+
cluster: clusterName,
85+
tasks: taskArns,
86+
$waiter: {
87+
delay: WAIT_DEFAULT_DELAY_SEC,
88+
maxAttempts: maxAttempts
89+
}
90+
}).promise();
91+
92+
core.debug(`Run task response ${JSON.stringify(waitTaskResponse)}`)
93+
94+
core.info(`All tasks have stopped. Watch progress in the Amazon ECS console: https://console.aws.amazon.com/ecs/home?region=${aws.config.region}#/clusters/${clusterName}/tasks`);
95+
}
96+
97+
async function tasksExitCode(ecs, clusterName, taskArns) {
98+
const describeResponse = await ecs.describeTasks({
99+
cluster: clusterName,
100+
tasks: taskArns
101+
}).promise();
102+
103+
const containers = [].concat(...describeResponse.tasks.map(task => task.containers))
104+
const exitCodes = containers.map(container => container.exitCode)
105+
const reasons = containers.map(container => container.reason)
106+
107+
const failuresIdx = [];
108+
109+
exitCodes.filter((exitCode, index) => {
110+
if (exitCode !== 0) {
111+
failuresIdx.push(index)
112+
}
113+
})
114+
115+
const failures = reasons.filter((_, index) => failuresIdx.indexOf(index) !== -1)
116+
117+
if (failures.length > 0) {
118+
console.log(`failed to with exit code${failures}`)
119+
core.setFailed(failures.join("\n"));
120+
throw new Error(`Run task failed: ${JSON.stringify(failures)}`);
121+
} else {
122+
core.info(`All tasks have exited successfully.`);
123+
}
124+
}
125+
23126
// Deploy to a service that uses the 'ECS' deployment controller
24-
async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment) {
127+
async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment, desiredCount) {
25128
core.debug('Updating the service');
26-
await ecs.updateService({
129+
let params = {
27130
cluster: clusterName,
28131
service: service,
29132
taskDefinition: taskDefArn,
30133
forceNewDeployment: forceNewDeployment
31-
}).promise();
134+
};
135+
// Add the desiredCount property only if it is defined and a number.
136+
if (!isNaN(desiredCount) && desiredCount !== undefined) {
137+
params.desiredCount = desiredCount;
138+
}
139+
await ecs.updateService(params).promise();
32140

33141
const consoleHostname = aws.config.region.startsWith('cn') ? 'console.amazonaws.cn' : 'console.aws.amazon.com';
34142

@@ -262,13 +370,17 @@ async function run() {
262370
const cluster = core.getInput('cluster', { required: false });
263371
const waitForService = core.getInput('wait-for-service-stability', { required: false });
264372
let waitForMinutes = parseInt(core.getInput('wait-for-minutes', { required: false })) || 30;
373+
const initTaskCommand = core.getInput('init-task-command');
265374
if (waitForMinutes > MAX_WAIT_MINUTES) {
266375
waitForMinutes = MAX_WAIT_MINUTES;
267376
}
268377

269378
const forceNewDeployInput = core.getInput('force-new-deployment', { required: false }) || 'false';
270379
const forceNewDeployment = forceNewDeployInput.toLowerCase() === 'true';
271380

381+
const desiredCount = parseInt((core.getInput('desired-count', {required: false})));
382+
383+
272384
// Register the task definition
273385
core.debug('Registering the task definition');
274386
const taskDefPath = path.isAbsolute(taskDefinitionFile) ?
@@ -308,9 +420,15 @@ async function run() {
308420
throw new Error(`Service is ${serviceResponse.status}`);
309421
}
310422

423+
if (initTaskCommand) {
424+
await runInitTask(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, initTaskCommand);
425+
} else {
426+
core.debug('InitTaskCommand was not specified, no init run task.');
427+
}
428+
311429
if (!serviceResponse.deploymentController || !serviceResponse.deploymentController.type || serviceResponse.deploymentController.type === 'ECS') {
312430
// Service uses the 'ECS' deployment controller, so we can call UpdateService
313-
await updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment);
431+
await updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment, desiredCount);
314432
} else if (serviceResponse.deploymentController.type === 'CODE_DEPLOY') {
315433
// Service uses CodeDeploy, so we should start a CodeDeploy deployment
316434
await createCodeDeployDeployment(codedeploy, clusterName, service, taskDefArn, waitForService, waitForMinutes);
@@ -322,6 +440,7 @@ async function run() {
322440
}
323441
}
324442
catch (error) {
443+
console.log(error);
325444
core.setFailed(error.message);
326445
core.debug(error.stack);
327446
}

0 commit comments

Comments
 (0)