diff --git a/README.md b/README.md index 5d57d586..3d9f2fd4 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,19 @@ To turn on [Amazon ECS-managed tags](https://docs.aws.amazon.com/AmazonECS/lates enable-ecs-managed-tags: true ``` +You can propagate your custom tags from your existing service using `propagate-tags`: + +```yaml + - name: Deploy Amazon ECS task definition + uses: aws-actions/amazon-ecs-deploy-task-definition@v2 + with: + task-definition: task-definition.json + service: my-service + cluster: my-cluster + wait-for-service-stability: true + propagate-tags: SERVICE +``` + ## Credentials and Region This action relies on the [default behavior of the AWS SDK for Javascript](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/setting-credentials-node.html) to determine AWS credentials and region. @@ -288,6 +301,27 @@ In the following example, the service would not be updated until the ad-hoc task Overrides and VPC networking options are available as well. See [action.yml](action.yml) for more details. The `FARGATE` launch type requires `awsvpc` network mode in your task definition and you must specify a network configuration. +### Tags + +To tag your tasks: + +* to turn on Amazon ECS-managed tags (`aws:ecs:clusterName`), use `enable-ecs-managed-tags` +* for custom tags, use `run-task-tags` + +```yaml + - name: Deploy to Amazon ECS + uses: aws-actions/amazon-ecs-deploy-task-definition@v2 + with: + task-definition: task-definition.json + service: my-service + cluster: my-cluster + wait-for-service-stability: true + run-task: true + enable-ecs-managed-tags: true + run-task-tags: '[{"key": "project", "value": "myproject"}]' + wait-for-task-stopped: true +``` + ## Troubleshooting 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. diff --git a/action.yml b/action.yml index 9e9444ef..c3bc7b27 100644 --- a/action.yml +++ b/action.yml @@ -61,12 +61,18 @@ inputs: run-task-started-by: description: "A name to use for the startedBy tag when running a task outside of a service. Will default to 'GitHub-Actions'." required: false + run-task-tags: + description: 'A JSON array of tags' + required: false wait-for-task-stopped: description: 'Whether to wait for the task to stop when running it outside of a service. Will default to not wait.' required: false enable-ecs-managed-tags: description: "Determines whether to turn on Amazon ECS managed tags 'aws:ecs:serviceName' and 'aws:ecs:clusterName' for the tasks in the service." required: false + propagate-tags: + description: "Determines to propagate the tags from the 'SERVICE' to the task. Will default to 'NONE'" + required: false outputs: task-definition-arn: description: 'The ARN of the registered ECS task definition' diff --git a/dist/index.js b/dist/index.js index ecae24e1..b6a1dbd1 100644 --- a/dist/index.js +++ b/dist/index.js @@ -37,6 +37,7 @@ async function runTask(ecs, clusterName, taskDefArn, waitForMinutes, enableECSMa const securityGroupIds = core.getInput('run-task-security-groups', { required: false }) || ''; const containerOverrides = JSON.parse(core.getInput('run-task-container-overrides', { required: false }) || '[]'); const assignPublicIP = core.getInput('run-task-assign-public-IP', { required: false }) || 'DISABLED'; + const tags = JSON.parse(core.getInput('run-task-tags', { required: false }) || '[]'); let awsvpcConfiguration = {} @@ -61,7 +62,8 @@ async function runTask(ecs, clusterName, taskDefArn, waitForMinutes, enableECSMa }, launchType: launchType, networkConfiguration: Object.keys(awsvpcConfiguration).length === 0 ? null : { awsvpcConfiguration: awsvpcConfiguration }, - enableECSManagedTags: enableECSManagedTags + enableECSManagedTags: enableECSManagedTags, + tags: tags }); core.debug(`Run task response ${JSON.stringify(runTaskResponse)}`) @@ -135,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) { +async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags) { core.debug('Updating the service'); let params = { @@ -143,7 +145,8 @@ async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForSe service: service, taskDefinition: taskDefArn, forceNewDeployment: forceNewDeployment, - enableECSManagedTags: enableECSManagedTags + enableECSManagedTags: enableECSManagedTags, + propagateTags: propagateTags }; // Add the desiredCount property only if it is defined and a number. @@ -401,7 +404,8 @@ async function run() { const desiredCount = parseInt((core.getInput('desired-count', {required: false}))); const enableECSManagedTagsInput = core.getInput('enable-ecs-managed-tags', { required: false }) || 'false'; const enableECSManagedTags = enableECSManagedTagsInput.toLowerCase() === 'true'; - + const propagateTags = core.getInput('propagate-tags', { required: false }) || 'NONE'; + // Register the task definition core.debug('Registering the task definition'); const taskDefPath = path.isAbsolute(taskDefinitionFile) ? @@ -452,7 +456,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); + await updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags); } else if (serviceResponse.deploymentController.type === 'CODE_DEPLOY') { // Service uses CodeDeploy, so we should start a CodeDeploy deployment diff --git a/index.js b/index.js index 6168cc70..8e9f90d9 100644 --- a/index.js +++ b/index.js @@ -31,6 +31,7 @@ async function runTask(ecs, clusterName, taskDefArn, waitForMinutes, enableECSMa const securityGroupIds = core.getInput('run-task-security-groups', { required: false }) || ''; const containerOverrides = JSON.parse(core.getInput('run-task-container-overrides', { required: false }) || '[]'); const assignPublicIP = core.getInput('run-task-assign-public-IP', { required: false }) || 'DISABLED'; + const tags = JSON.parse(core.getInput('run-task-tags', { required: false }) || '[]'); let awsvpcConfiguration = {} @@ -55,7 +56,8 @@ async function runTask(ecs, clusterName, taskDefArn, waitForMinutes, enableECSMa }, launchType: launchType, networkConfiguration: Object.keys(awsvpcConfiguration).length === 0 ? null : { awsvpcConfiguration: awsvpcConfiguration }, - enableECSManagedTags: enableECSManagedTags + enableECSManagedTags: enableECSManagedTags, + tags: tags }); core.debug(`Run task response ${JSON.stringify(runTaskResponse)}`) @@ -129,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) { +async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags) { core.debug('Updating the service'); let params = { @@ -137,7 +139,8 @@ async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForSe service: service, taskDefinition: taskDefArn, forceNewDeployment: forceNewDeployment, - enableECSManagedTags: enableECSManagedTags + enableECSManagedTags: enableECSManagedTags, + propagateTags: propagateTags }; // Add the desiredCount property only if it is defined and a number. @@ -395,7 +398,8 @@ async function run() { const desiredCount = parseInt((core.getInput('desired-count', {required: false}))); const enableECSManagedTagsInput = core.getInput('enable-ecs-managed-tags', { required: false }) || 'false'; const enableECSManagedTags = enableECSManagedTagsInput.toLowerCase() === 'true'; - + const propagateTags = core.getInput('propagate-tags', { required: false }) || 'NONE'; + // Register the task definition core.debug('Registering the task definition'); const taskDefPath = path.isAbsolute(taskDefinitionFile) ? @@ -446,7 +450,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); + await updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags); } else if (serviceResponse.deploymentController.type === 'CODE_DEPLOY') { // Service uses CodeDeploy, so we should start a CodeDeploy deployment diff --git a/index.test.js b/index.test.js index 2b610cd7..2cae5c1b 100644 --- a/index.test.js +++ b/index.test.js @@ -183,7 +183,8 @@ describe('Deploy to ECS', () => { service: 'service-456', taskDefinition: 'task:def:arn', forceNewDeployment: false, - enableECSManagedTags: false + enableECSManagedTags: false, + propagateTags: 'NONE' }); expect(waitUntilServicesStable).toHaveBeenCalledTimes(0); expect(core.info).toBeCalledWith("Deployment started. Watch this deployment's progress in the Amazon ECS console: https://fake-region.console.aws.amazon.com/ecs/v2/clusters/cluster-789/services/service-456/events?region=fake-region"); @@ -215,7 +216,8 @@ describe('Deploy to ECS', () => { service: 'service-456', taskDefinition: 'task:def:arn', forceNewDeployment: false, - enableECSManagedTags: false + enableECSManagedTags: false, + propagateTags: 'NONE' }); expect(waitUntilServicesStable).toHaveBeenCalledTimes(0); expect(core.info).toBeCalledWith("Deployment started. Watch this deployment's progress in the Amazon ECS console: https://fake-region.console.aws.amazon.com/ecs/v2/clusters/cluster-789/services/service-456/events?region=fake-region"); @@ -711,6 +713,7 @@ describe('Deploy to ECS', () => { .mockReturnValueOnce('') // run-task .mockReturnValueOnce('') // desired count .mockReturnValueOnce('') // enable-ecs-managed-tags + .mockReturnValueOnce('') // propagate-task .mockReturnValueOnce('/hello/appspec.json') // codedeploy-appspec .mockReturnValueOnce('MyApplication') // codedeploy-application .mockReturnValueOnce('MyDeploymentGroup'); // codedeploy-deployment-group @@ -948,7 +951,8 @@ describe('Deploy to ECS', () => { service: 'service-456', taskDefinition: 'task:def:arn', forceNewDeployment: false, - enableECSManagedTags: false + enableECSManagedTags: false, + propagateTags: 'NONE' }); expect(waitUntilServicesStable).toHaveBeenNthCalledWith( 1, @@ -988,7 +992,8 @@ describe('Deploy to ECS', () => { service: 'service-456', taskDefinition: 'task:def:arn', forceNewDeployment: false, - enableECSManagedTags: false + enableECSManagedTags: false, + propagateTags: 'NONE' }); expect(waitUntilServicesStable).toHaveBeenNthCalledWith( 1, @@ -1028,7 +1033,8 @@ describe('Deploy to ECS', () => { service: 'service-456', taskDefinition: 'task:def:arn', forceNewDeployment: false, - enableECSManagedTags: false + enableECSManagedTags: false, + propagateTags: 'NONE' }); expect(waitUntilServicesStable).toHaveBeenNthCalledWith( 1, @@ -1070,7 +1076,8 @@ describe('Deploy to ECS', () => { service: 'service-456', taskDefinition: 'task:def:arn', forceNewDeployment: true, - enableECSManagedTags: false + enableECSManagedTags: false, + propagateTags: 'NONE' }); }); @@ -1095,7 +1102,8 @@ describe('Deploy to ECS', () => { service: 'service-456', taskDefinition: 'task:def:arn', forceNewDeployment: false, - enableECSManagedTags: false + enableECSManagedTags: false, + propagateTags: 'NONE' }); }); @@ -1122,6 +1130,7 @@ describe('Deploy to ECS', () => { .mockReturnValueOnce('') // wait-for-service-stability .mockReturnValueOnce('') // wait-for-minutes .mockReturnValueOnce('') // enable-ecs-managed-tags + .mockReturnValueOnce('') // propagate-tags .mockReturnValueOnce('') // force-new-deployment .mockReturnValueOnce('') // desired-count .mockReturnValueOnce('true'); // run-task @@ -1139,7 +1148,8 @@ describe('Deploy to ECS', () => { taskDefinition: 'task:def:arn', overrides: {"containerOverrides": []}, networkConfiguration: null, - enableECSManagedTags: false + enableECSManagedTags: false, + tags: [] }); expect(core.setOutput).toHaveBeenNthCalledWith(2, 'run-task-arn', ["arn:aws:ecs:fake-region:account_id:task/arn"]); @@ -1155,14 +1165,17 @@ describe('Deploy to ECS', () => { .mockReturnValueOnce('') // wait-for-minutes .mockReturnValueOnce('') // force-new-deployment .mockReturnValueOnce('') // desired-count - .mockReturnValueOnce('true') // enable-ecs-managed-tags + .mockReturnValueOnce('false') // enable-ecs-managed-tags + .mockReturnValueOnce('') // propagate-tags .mockReturnValueOnce('true') // run-task .mockReturnValueOnce('false') // wait-for-task-stopped .mockReturnValueOnce('someJoe') // run-task-started-by .mockReturnValueOnce('EC2') // run-task-launch-type .mockReturnValueOnce('a,b') // run-task-subnet-ids .mockReturnValueOnce('c,d') // run-task-security-group-ids - .mockReturnValueOnce(JSON.stringify([{ name: 'someapp', command: 'somecmd' }])); // run-task-container-overrides + .mockReturnValueOnce(JSON.stringify([{ name: 'someapp', command: 'somecmd' }])) // run-task-container-overrides + .mockReturnValueOnce('') // run-task-assign-public-IP + .mockReturnValueOnce('[{"key": "project", "value": "myproject"}]'); // run-task-tags await run(); expect(core.setFailed).toHaveBeenCalledTimes(0); @@ -1176,7 +1189,8 @@ describe('Deploy to ECS', () => { taskDefinition: 'task:def:arn', overrides: { containerOverrides: [{ name: 'someapp', command: 'somecmd' }] }, networkConfiguration: { awsvpcConfiguration: { subnets: ['a', 'b'], securityGroups: ['c', 'd'], assignPublicIp: "DISABLED" } }, - enableECSManagedTags: true + enableECSManagedTags: false, + tags: [{"key": "project", "value": "myproject"}] }); expect(core.setOutput).toHaveBeenNthCalledWith(2, 'run-task-arn', ["arn:aws:ecs:fake-region:account_id:task/arn"]); }); @@ -1192,6 +1206,7 @@ describe('Deploy to ECS', () => { .mockReturnValueOnce('') // force-new-deployment .mockReturnValueOnce('') // desired-count .mockReturnValueOnce('') // enable-ecs-managed-tags + .mockReturnValueOnce('') // propagate-tags .mockReturnValueOnce('true') // run-task .mockReturnValueOnce('false') // wait-for-task-stopped .mockReturnValueOnce('someJoe') // run-task-started-by @@ -1214,7 +1229,8 @@ describe('Deploy to ECS', () => { service: 'service-456', taskDefinition: 'task:def:arn', forceNewDeployment: false, - enableECSManagedTags: false + enableECSManagedTags: false, + propagateTags: 'NONE', }); expect(mockRunTask).toHaveBeenCalledWith({ startedBy: 'someJoe', @@ -1223,7 +1239,8 @@ describe('Deploy to ECS', () => { launchType: 'EC2', overrides: { containerOverrides: [{ name: 'someapp', command: 'somecmd' }] }, networkConfiguration: { awsvpcConfiguration: { subnets: ['a', 'b'], securityGroups: ['c', 'd'], assignPublicIp: "DISABLED" } }, - enableECSManagedTags: false + enableECSManagedTags: false, + tags: [] }); expect(core.setOutput).toHaveBeenNthCalledWith(2, 'run-task-arn', ["arn:aws:ecs:fake-region:account_id:task/arn"]); }); @@ -1239,6 +1256,7 @@ describe('Deploy to ECS', () => { .mockReturnValueOnce('') // force-new-deployment .mockReturnValueOnce('') // desired-count .mockReturnValueOnce('') // enable-ecs-managed-tags + .mockReturnValueOnce('') // propagate-tags .mockReturnValueOnce('true') // run-task .mockReturnValueOnce('true'); // wait-for-task-stopped @@ -1263,6 +1281,7 @@ describe('Deploy to ECS', () => { .mockReturnValueOnce('') // enable-ecs-managed-tags .mockReturnValueOnce('') // force-new-deployment .mockReturnValueOnce('') // desired-count + .mockReturnValueOnce('') // propagate-tags .mockReturnValueOnce('true') // run-task .mockReturnValueOnce('true') // wait-for-task-stopped .mockReturnValueOnce('someJoe') // run-task-started-by @@ -1280,7 +1299,8 @@ describe('Deploy to ECS', () => { launchType: 'EC2', overrides: { containerOverrides: [] }, networkConfiguration: null, - enableECSManagedTags: false + enableECSManagedTags: false, + tags: [] }); }); @@ -1294,6 +1314,8 @@ describe('Deploy to ECS', () => { .mockReturnValueOnce('') // wait-for-minutes .mockReturnValueOnce('') // force-new-deployment .mockReturnValueOnce('') // desired-count + .mockReturnValueOnce('') // enable-ecs-managed-tags + .mockReturnValueOnce('') // propagate-tags .mockReturnValueOnce('true') // run-task .mockReturnValueOnce('true'); // wait-for-task-stopped @@ -1336,6 +1358,7 @@ describe('Deploy to ECS', () => { .mockReturnValueOnce('') // force-new-deployment .mockReturnValueOnce('') // desired-count .mockReturnValueOnce('') // enable-ecs-managed-tags + .mockReturnValueOnce('') // propagate-tags .mockReturnValueOnce('true') // run-task .mockReturnValueOnce('false'); // wait-for-task-stopped @@ -1446,4 +1469,36 @@ describe('Deploy to ECS', () => { expect(core.setFailed).toHaveBeenNthCalledWith(1, 'Failed to register task definition in ECS: Could not parse'); expect(core.setFailed).toHaveBeenNthCalledWith(2, 'Could not parse'); }); + + test('propagate service tags from service and enable ecs managed tags', async () => { + core.getInput = jest + .fn() + .mockReturnValueOnce('task-definition.json') // task-definition + .mockReturnValueOnce('service-456') // service + .mockReturnValueOnce('cluster-789') // cluster + .mockReturnValueOnce('false') // wait-for-service-stability + .mockReturnValueOnce('') // wait-for-minutes + .mockReturnValueOnce('') // force-new-deployment + .mockReturnValueOnce('') // desired-count + .mockReturnValueOnce('true') // enable-ecs-managed-tags + .mockReturnValueOnce('SERVICE'); // propagate-tags + + 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', + forceNewDeployment: false, + enableECSManagedTags: true, + propagateTags: 'SERVICE' + }); + }); }); \ No newline at end of file