Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adds code for showing service event info when using ecs deploy controller #454

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -326,6 +326,8 @@ To tag your tasks:

This action emits debug logs to help troubleshoot deployment failures. To see the debug logs, create a secret named `ACTIONS_STEP_DEBUG` with value `true` in your repository.

The input `show-service-events` helps you to check logs from the service deployment events without going to the AWS console. This is just for the `ECS deployment controller`. Is desirable to configure [deployment circuit breaker](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/deployment-circuit-breaker.html) to get a 'FAILED' rolloutState.

## License Summary

This code is made available under the MIT license.
6 changes: 6 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
@@ -16,6 +16,12 @@ inputs:
cluster:
description: "The name of the ECS service's cluster. Will default to the 'default' cluster."
required: false
show-service-events:
description: "Whether to see or not the service deployment events when deployment rolloutState is 'FAILED'. Useful to see errors when a deployment fails."
required: false
show-service-events-frequency:
description: "The frequency for showing a log line of the service events (default: 15 seconds)."
required: false
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
94 changes: 88 additions & 6 deletions dist/index.js
Original file line number Diff line number Diff line change
@@ -137,7 +137,7 @@ async function tasksExitCode(ecs, clusterName, taskArns) {
}

// Deploy to a service that uses the 'ECS' deployment controller
async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags) {
async function updateEcsService(ecs, clusterName, service, taskDefArn, showServiceEvents, showServiceEventsFrequency, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags) {
core.debug('Updating the service');

let params = {
@@ -155,10 +155,65 @@ async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForSe
}
await ecs.updateService(params);

const region = await ecs.config.region();
const consoleHostname = region.startsWith('cn') ? 'console.amazonaws.cn' : 'console.aws.amazon.com';
// Create a while loop to print the events of the service if deployment rollout state is failed
// or if there are failed tasks but rollout state is still in progress
if (showServiceEvents && showServiceEvents.toLowerCase() === 'true') {
core.debug(`Deployment started. The option show-service-events is set to true. Showing logs each ${showServiceEventsFrequency} seconds.`);
const initialState = 'IN_PROGRESS';
let describeResponse = await ecs.describeServices({
services: [service],
cluster: clusterName
});
const deployTime = describeResponse.services[0].events[0].createdAt
let newEvents = [];

while (initialState == 'IN_PROGRESS') {
const showServiceEventsFrequencyMilisec = (showServiceEventsFrequency * 1000)
await delay(showServiceEventsFrequencyMilisec);
let describeResponse = await ecs.describeServices({
services: [service],
cluster: clusterName
});

core.info(`Deployment started. Watch this deployment's progress in the Amazon ECS console: https://${region}.${consoleHostname}/ecs/v2/clusters/${clusterName}/services/${service}/events?region=${region}`);
let serviceResponse = describeResponse.services[0];
let rolloutState = serviceResponse.deployments[0].rolloutState;
let rolloutStateReason = serviceResponse.deployments[0].rolloutStateReason;
let failedTasksCount = serviceResponse.deployments[0].failedTasks;
let indexEventContainDeployDate = getPosition(deployTime.toString(), serviceResponse.events);
newEvents = serviceResponse.events.slice(0, indexEventContainDeployDate);

if (rolloutState == 'COMPLETED') {
printEvents(newEvents.reverse());
break;
} else if (rolloutState == 'FAILED') {
printEvents(newEvents.reverse());
throw new Error(`Rollout state is ${rolloutState}. Reason: ${rolloutStateReason}.`);
} else if (rolloutState == 'IN_PROGRESS' && failedTasksCount > 0) {
printEvents(newEvents.reverse());
let tasksList = await ecs.listTasks({
serviceName: service,
cluster: clusterName,
desiredStatus: 'STOPPED'
});
let describeTaskResponse = await ecs.describeTasks({
tasks: [tasksList.taskArns[0]],
cluster: clusterName,
});
let stopCode = describeTaskResponse.tasks[0].stopCode;
let stoppedReason = describeTaskResponse.tasks[0].stoppedReason;
let containerLastStatus = describeTaskResponse.tasks[0].containers[0].lastStatus;
core.info(`Task status: ${stopCode}. The reason is: ${stoppedReason}.`);
if (containerLastStatus == 'STOPPED') {
core.info(`Container status: ${containerLastStatus}. The reason is: ${describeTaskResponse.tasks[0].containers[0].reason}.`);
}
throw new Error(`There are failed tasks. This means the deployment didn't go well. Please check the logs of task, service or container for more information.`);
}
}
} else {
const region = await ecs.config.region();
const consoleHostname = region.startsWith('cn') ? 'console.amazonaws.cn' : 'console.aws.amazon.com';
core.info(`Deployment started. Watch this deployment's progress in the Amazon ECS console: https://${region}.${consoleHostname}/ecs/v2/clusters/${clusterName}/services/${service}/events?region=${region}`);
}

// Wait for service stability
if (waitForService && waitForService.toLowerCase() === 'true') {
@@ -176,6 +231,29 @@ async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForSe
}
}

// Function to print the events of the service
function printEvents(events) {
core.debug('Showing the service events:')
for (let i = 0; i < events.length; i++) {
core.info(events[i].createdAt.toString().split('(')[0]);
core.info(events[i].message);
}
}

// Function to get the position of an element in an array
function getPosition(elementToFind, arrayElements) {
for (let i = 0; i < arrayElements.length; i += 1) {
if (arrayElements[i].createdAt.toString().includes(elementToFind)) {
return i;
}
}
}

// Fuction to create a delay
function delay(time) {
return new Promise(resolve => setTimeout(resolve, time));
}

// Find value in a CodeDeploy AppSpec file with a case-insensitive key
function findAppSpecValue(obj, keyName) {
return obj[findAppSpecKey(obj, keyName)];
@@ -392,7 +470,8 @@ async function run() {
const taskDefinitionFile = core.getInput('task-definition', { required: true });
const service = core.getInput('service', { required: false });
const cluster = core.getInput('cluster', { required: false });
const waitForService = core.getInput('wait-for-service-stability', { 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) {
@@ -406,6 +485,9 @@ async function run() {
const enableECSManagedTags = enableECSManagedTagsInput.toLowerCase() === 'true';
const propagateTags = core.getInput('propagate-tags', { required: false }) || 'NONE';

const showServiceEvents = core.getInput('show-service-events', { required: false });
let showServiceEventsFrequency = core.getInput('show-service-events-frequency', { required: false }) || 15;

// Register the task definition
core.debug('Registering the task definition');
const taskDefPath = path.isAbsolute(taskDefinitionFile) ?
@@ -456,7 +538,7 @@ async function run() {
if (!serviceResponse.deploymentController || !serviceResponse.deploymentController.type || serviceResponse.deploymentController.type === 'ECS') {
// Service uses the 'ECS' deployment controller, so we can call UpdateService
core.debug('Updating service...');
await updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags);
await updateEcsService(ecs, clusterName, service, taskDefArn, showServiceEvents, showServiceEventsFrequency, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags);

} else if (serviceResponse.deploymentController.type === 'CODE_DEPLOY') {
// Service uses CodeDeploy, so we should start a CodeDeploy deployment
94 changes: 88 additions & 6 deletions index.js
Original file line number Diff line number Diff line change
@@ -131,7 +131,7 @@ async function tasksExitCode(ecs, clusterName, taskArns) {
}

// Deploy to a service that uses the 'ECS' deployment controller
async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags) {
async function updateEcsService(ecs, clusterName, service, taskDefArn, showServiceEvents, showServiceEventsFrequency, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags) {
core.debug('Updating the service');

let params = {
@@ -149,10 +149,65 @@ async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForSe
}
await ecs.updateService(params);

const region = await ecs.config.region();
const consoleHostname = region.startsWith('cn') ? 'console.amazonaws.cn' : 'console.aws.amazon.com';
// Create a while loop to print the events of the service if deployment rollout state is failed
// or if there are failed tasks but rollout state is still in progress
if (showServiceEvents && showServiceEvents.toLowerCase() === 'true') {
core.debug(`Deployment started. The option show-service-events is set to true. Showing logs each ${showServiceEventsFrequency} seconds.`);
const initialState = 'IN_PROGRESS';
let describeResponse = await ecs.describeServices({
services: [service],
cluster: clusterName
});
const deployTime = describeResponse.services[0].events[0].createdAt
let newEvents = [];

core.info(`Deployment started. Watch this deployment's progress in the Amazon ECS console: https://${region}.${consoleHostname}/ecs/v2/clusters/${clusterName}/services/${service}/events?region=${region}`);
while (initialState == 'IN_PROGRESS') {
const showServiceEventsFrequencyMilisec = (showServiceEventsFrequency * 1000)
await delay(showServiceEventsFrequencyMilisec);
let describeResponse = await ecs.describeServices({
services: [service],
cluster: clusterName
});

let serviceResponse = describeResponse.services[0];
let rolloutState = serviceResponse.deployments[0].rolloutState;
let rolloutStateReason = serviceResponse.deployments[0].rolloutStateReason;
let failedTasksCount = serviceResponse.deployments[0].failedTasks;
let indexEventContainDeployDate = getPosition(deployTime.toString(), serviceResponse.events);
newEvents = serviceResponse.events.slice(0, indexEventContainDeployDate);

if (rolloutState == 'COMPLETED') {
printEvents(newEvents.reverse());
break;
} else if (rolloutState == 'FAILED') {
printEvents(newEvents.reverse());
throw new Error(`Rollout state is ${rolloutState}. Reason: ${rolloutStateReason}.`);
} else if (rolloutState == 'IN_PROGRESS' && failedTasksCount > 0) {
printEvents(newEvents.reverse());
let tasksList = await ecs.listTasks({
serviceName: service,
cluster: clusterName,
desiredStatus: 'STOPPED'
});
let describeTaskResponse = await ecs.describeTasks({
tasks: [tasksList.taskArns[0]],
cluster: clusterName,
});
let stopCode = describeTaskResponse.tasks[0].stopCode;
let stoppedReason = describeTaskResponse.tasks[0].stoppedReason;
let containerLastStatus = describeTaskResponse.tasks[0].containers[0].lastStatus;
core.info(`Task status: ${stopCode}. The reason is: ${stoppedReason}.`);
if (containerLastStatus == 'STOPPED') {
core.info(`Container status: ${containerLastStatus}. The reason is: ${describeTaskResponse.tasks[0].containers[0].reason}.`);
}
throw new Error(`There are failed tasks. This means the deployment didn't go well. Please check the logs of task, service or container for more information.`);
}
}
} else {
const region = await ecs.config.region();
const consoleHostname = region.startsWith('cn') ? 'console.amazonaws.cn' : 'console.aws.amazon.com';
core.info(`Deployment started. Watch this deployment's progress in the Amazon ECS console: https://${region}.${consoleHostname}/ecs/v2/clusters/${clusterName}/services/${service}/events?region=${region}`);
}

// Wait for service stability
if (waitForService && waitForService.toLowerCase() === 'true') {
@@ -170,6 +225,29 @@ async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForSe
}
}

// Function to print the events of the service
function printEvents(events) {
core.debug('Showing the service events:')
for (let i = 0; i < events.length; i++) {
core.info(events[i].createdAt.toString().split('(')[0]);
core.info(events[i].message);
}
}

// Function to get the position of an element in an array
function getPosition(elementToFind, arrayElements) {
for (let i = 0; i < arrayElements.length; i += 1) {
if (arrayElements[i].createdAt.toString().includes(elementToFind)) {
return i;
}
}
}

// Fuction to create a delay
function delay(time) {
return new Promise(resolve => setTimeout(resolve, time));
}

// Find value in a CodeDeploy AppSpec file with a case-insensitive key
function findAppSpecValue(obj, keyName) {
return obj[findAppSpecKey(obj, keyName)];
@@ -386,7 +464,8 @@ async function run() {
const taskDefinitionFile = core.getInput('task-definition', { required: true });
const service = core.getInput('service', { required: false });
const cluster = core.getInput('cluster', { required: false });
const waitForService = core.getInput('wait-for-service-stability', { 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) {
@@ -400,6 +479,9 @@ async function run() {
const enableECSManagedTags = enableECSManagedTagsInput.toLowerCase() === 'true';
const propagateTags = core.getInput('propagate-tags', { required: false }) || 'NONE';

const showServiceEvents = core.getInput('show-service-events', { required: false });
let showServiceEventsFrequency = core.getInput('show-service-events-frequency', { required: false }) || 15;

// Register the task definition
core.debug('Registering the task definition');
const taskDefPath = path.isAbsolute(taskDefinitionFile) ?
@@ -450,7 +532,7 @@ async function run() {
if (!serviceResponse.deploymentController || !serviceResponse.deploymentController.type || serviceResponse.deploymentController.type === 'ECS') {
// Service uses the 'ECS' deployment controller, so we can call UpdateService
core.debug('Updating service...');
await updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags);
await updateEcsService(ecs, clusterName, service, taskDefArn, showServiceEvents, showServiceEventsFrequency, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags);

} else if (serviceResponse.deploymentController.type === 'CODE_DEPLOY') {
// Service uses CodeDeploy, so we should start a CodeDeploy deployment
Loading