From 7395878b511edbf0531c93806fec62bbf40457b0 Mon Sep 17 00:00:00 2001 From: Gabe Rojas-Westall Date: Fri, 6 Nov 2020 13:04:37 -0600 Subject: [PATCH 1/9] task def merging --- index.js | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/index.js b/index.js index ec9b78c8..c50b3840 100644 --- a/index.js +++ b/index.js @@ -2,6 +2,18 @@ const path = require('path'); const core = require('@actions/core'); const tmp = require('tmp'); const fs = require('fs'); +const mergeWith = require('lodash.mergeWith'); + +// Customizer for lodash mergeWith +// allows arrays in the original task definition to contain +// values as opposed to only in the mergeFiles (otherwise +// they are overridden) +// https://lodash.com/docs/4.17.15#mergeWith +function customizer(objValue, srcValue) { + if (Array.isArray(objValue)) { + return objValue.concat(srcValue); + } +} async function run() { try { @@ -9,6 +21,7 @@ async function run() { const taskDefinitionFile = core.getInput('task-definition', { required: true }); const containerName = core.getInput('container-name', { required: true }); const imageURI = core.getInput('image', { required: true }); + const mergeFile = core.getInput('merge', { required: false }); // Parse the task definition const taskDefPath = path.isAbsolute(taskDefinitionFile) ? @@ -31,6 +44,32 @@ async function run() { } containerDef.image = imageURI; + // Check for mergeFile + if (mergeFile) { + // Parse the merge file + const mergeFilePath = path.isAbsolute(mergeFile) ? + mergeFile : + path.join(process.env.GITHUB_WORKSPACE, mergeFile); + if (!fs.existsSync(mergeFilePath)) { + throw new Error(`Merge file does not exist: ${mergeFile}`); + } + const mergeContents = require(mergeFilePath); + + // Merge the merge file + if (!Array.isArray(mergeContents.containerDefinitions)) { + throw new Error('Invalid merge fragment definition: containerDefinitions section is not present or is not an array'); + } + const mergeDef = mergeContents.containerDefinitions.find(function(element) { + return element.name == containerName; + }); + if (!mergeDef) { + throw new Error('Invalid merge fragment definition: Could not find container definition with matching name'); + } + + // mergeWith contents + mergeWith(containerDef, mergeDef, customizer); + } + // Write out a new task definition file var updatedTaskDefFile = tmp.fileSync({ tmpdir: process.env.RUNNER_TEMP, From fb78f146ec71a20f60d746d93bd1c74aa410df4b Mon Sep 17 00:00:00 2001 From: Gabe Rojas-Westall Date: Fri, 6 Nov 2020 13:04:59 -0600 Subject: [PATCH 2/9] mergeWith dependency --- package-lock.json | 5 +++++ package.json | 1 + 2 files changed, 6 insertions(+) diff --git a/package-lock.json b/package-lock.json index 3bf23519..56d338cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4546,6 +4546,11 @@ "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", "dev": true }, + "lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==" + }, "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", diff --git a/package.json b/package.json index 27173492..6483d36e 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "homepage": "https://github.com/aws-actions/amazon-ecs-render-task-definition#readme", "dependencies": { "@actions/core": "^1.2.6", + "lodash.mergewith": "^4.6.2", "tmp": "^0.2.1" }, "devDependencies": { From 0ab86bc15f94f344a08ee1b6ba8c69475fc8b94c Mon Sep 17 00:00:00 2001 From: Gabe Rojas-Westall Date: Fri, 6 Nov 2020 13:05:18 -0600 Subject: [PATCH 3/9] merge tests --- index.test.js | 146 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) diff --git a/index.test.js b/index.test.js index 379d5706..06406f51 100644 --- a/index.test.js +++ b/index.test.js @@ -108,6 +108,124 @@ describe('Render task definition', () => { expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition', 'new-task-def-file-name'); }); + test('renders a task definition with a merge file', async () => { + core.getInput = jest + .fn() + .mockReturnValueOnce('task-definition.json') // task-definition + .mockReturnValueOnce('web') // container-name + .mockReturnValueOnce('nginx:latest') // image + .mockReturnValueOnce('merge.json'); // merge + + jest.mock('./merge.json', () => ({ + containerDefinitions: [ + { + name: "web", + environment: [ + { + name: "log_level", + value: "info" + } + ] + } + ] + }), {virtual: true}); + + await run(); + + expect(fs.writeFileSync).toHaveBeenNthCalledWith(1, 'new-task-def-file-name', + JSON.stringify({ + family: 'task-def-family', + containerDefinitions: [ + { + name: "web", + image: "nginx:latest", + environment: [ + { + name: "log_level", + value: "info" + } + ] + }, + { + name: "sidecar", + image: "hello" + } + ] + }, null, 2) + ); + }); + + test('renders a task definition with a merge file to merge an array value', async () => { + core.getInput = jest + .fn() + .mockReturnValueOnce('task-definition.json') // task-definition + .mockReturnValueOnce('web') // container-name + .mockReturnValueOnce('nginx:latest') // image + .mockReturnValueOnce('merge-to-existing.json'); // merge + + jest.mock('./merge-to-existing.json', () => ({ + containerDefinitions: [ + { + name: "web", + environment: [ + { + name: "env", + value: "prod" + } + ] + } + ] + }), {virtual: true}); + + jest.mock('./task-definition.json', () => ({ + family: 'task-def-family', + containerDefinitions: [ + { + name: "web", + image: "some-other-image", + environment: [ + { + name: "log_level", + value: "info" + } + ] + }, + { + name: "sidecar", + image: "hello" + } + ] + }), { virtual: true }); + + await run(); + + expect(fs.writeFileSync).toHaveBeenNthCalledWith(1, 'new-task-def-file-name', + JSON.stringify({ + family: 'task-def-family', + containerDefinitions: [ + { + name: "web", + image: "nginx:latest", + environment: [ + { + name: "log_level", + value: "info" + }, + { + name: "env", + value: "prod" + } + ] + }, + { + name: "sidecar", + image: "hello" + } + ] + }, null, 2) + ); + }); + test('error returned for missing task definition file', async () => { fs.existsSync.mockReturnValue(false); core.getInput = jest @@ -173,4 +291,32 @@ describe('Render task definition', () => { expect(core.setFailed).toBeCalledWith('Invalid task definition: Could not find container definition with matching name'); }); + + test('error returned for invalid merge file', async () => { + fs.existsSync.mockReturnValue(false); + core.getInput = jest + .fn() + .mockReturnValueOnce('task-definition.json') + .mockReturnValueOnce('web') + .mockReturnValueOnce('nginx:latest') + .mockReturnValueOnce('invalid-merge-file.json'); + + fs.existsSync.mockReturnValueOnce(JSON.stringify({ + family: 'task-def-family', + containerDefinitions: [ + { + name: "web", + image: "nginx:latest" + }, + { + name: "sidecar", + image: "hello" + } + ] + })); + + await run(); + + expect(core.setFailed).toBeCalledWith('Merge file does not exist: invalid-merge-file.json'); + }); }); From 973f34d789cfd5da030409fc3eb10044ceb76214 Mon Sep 17 00:00:00 2001 From: Gabe Rojas-Westall Date: Fri, 6 Nov 2020 13:08:46 -0600 Subject: [PATCH 4/9] update action.yml --- action.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/action.yml b/action.yml index 5f6ae11a..c190b955 100644 --- a/action.yml +++ b/action.yml @@ -13,6 +13,9 @@ inputs: image: description: 'The URI of the container image to insert into the ECS task definition' required: true + merge: + description: 'The path to a task definition JSON fragment file to merge with the task defintion' + required: false outputs: task-definition: description: 'The path to the rendered task definition file' From 6b64c34568aa6060d7f5b91bb7a909c877db6abf Mon Sep 17 00:00:00 2001 From: Gabe Rojas-Westall Date: Fri, 6 Nov 2020 14:09:31 -0600 Subject: [PATCH 5/9] make image optional --- action.yml | 2 +- index.js | 11 +++++++--- index.test.js | 61 +++++++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 66 insertions(+), 8 deletions(-) diff --git a/action.yml b/action.yml index c190b955..f91f2e6d 100644 --- a/action.yml +++ b/action.yml @@ -12,7 +12,7 @@ inputs: required: true image: description: 'The URI of the container image to insert into the ECS task definition' - required: true + required: false merge: description: 'The path to a task definition JSON fragment file to merge with the task defintion' required: false diff --git a/index.js b/index.js index c50b3840..0c0c984d 100644 --- a/index.js +++ b/index.js @@ -20,7 +20,7 @@ async function run() { // Get inputs const taskDefinitionFile = core.getInput('task-definition', { required: true }); const containerName = core.getInput('container-name', { required: true }); - const imageURI = core.getInput('image', { required: true }); + const imageURI = core.getInput('image', { required: false }); const mergeFile = core.getInput('merge', { required: false }); // Parse the task definition @@ -32,7 +32,7 @@ async function run() { } const taskDefContents = require(taskDefPath); - // Insert the image URI + // Get containerDef with name `containerName` if (!Array.isArray(taskDefContents.containerDefinitions)) { throw new Error('Invalid task definition format: containerDefinitions section is not present or is not an array'); } @@ -42,7 +42,12 @@ async function run() { if (!containerDef) { throw new Error('Invalid task definition: Could not find container definition with matching name'); } - containerDef.image = imageURI; + + // Check for imageURI + if(imageURI) { + // Insert the image URI + containerDef.image = imageURI; + } // Check for mergeFile if (mergeFile) { diff --git a/index.test.js b/index.test.js index 06406f51..461ca3ad 100644 --- a/index.test.js +++ b/index.test.js @@ -108,13 +108,66 @@ describe('Render task definition', () => { expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition', 'new-task-def-file-name'); }); + test('renders a task definition without an image specificied', async () => { + core.getInput = jest + .fn() + .mockReturnValueOnce('task-definition-no-image.json') + .mockReturnValueOnce('web') + .mockReturnValueOnce(undefined) + .mockReturnValueOnce('merge-no-image.json'); + + jest.mock('./task-definition-no-image.json', () => ({ + family: 'task-def-family', + containerDefinitions: [ + { + name: "web", + image: "nginx:latest" + } + ] + }), { virtual: true }); + + jest.mock('./merge-no-image.json', () => ({ + containerDefinitions: [ + { + name: "web", + environment: [ + { + name: "log_level", + value: "info" + } + ] + } + ] + }), {virtual: true}); + + await run(); + + expect(fs.writeFileSync).toHaveBeenNthCalledWith(1, 'new-task-def-file-name', + JSON.stringify({ + family: 'task-def-family', + containerDefinitions: [ + { + name: "web", + image: "nginx:latest", + environment: [ + { + name: "log_level", + value: "info" + } + ] + } + ] + }, null, 2) + ); + }); + test('renders a task definition with a merge file', async () => { core.getInput = jest .fn() - .mockReturnValueOnce('task-definition.json') // task-definition - .mockReturnValueOnce('web') // container-name - .mockReturnValueOnce('nginx:latest') // image - .mockReturnValueOnce('merge.json'); // merge + .mockReturnValueOnce('task-definition.json') // task-definition + .mockReturnValueOnce('web') // container-name + .mockReturnValueOnce('nginx:latest') // image + .mockReturnValueOnce('merge.json'); // merge jest.mock('./merge.json', () => ({ containerDefinitions: [ From 284310571d174153133a2d88fe60830e93dc8841 Mon Sep 17 00:00:00 2001 From: Gabe Rojas-Westall Date: Fri, 6 Nov 2020 14:09:55 -0600 Subject: [PATCH 6/9] update README --- README.md | 90 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/README.md b/README.md index 1cf738a8..128f554f 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,96 @@ input of the second: cluster: my-cluster ``` +If containers in your task definition require different values depending on environment, you can specify `merge` file that contains a JSON fragment to merge with the `task-definition`: + +_task-def.json_ + +```json + { + "family": "task-def-family", + "containerDefinitions": [ + { + "name": "web", + "image": "some-image" + } + ] + } +``` + +_staging-vars.json_ + +```json + { + "containerDefinitions": [ + { + "name": "web", + "environment": [ + { + "name": "log_level", + "value": "debug" + } + ] + } + ] + } +``` + +_prod-vars.json_ + +```json + { + "containerDefinitions": [ + { + "name": "web", + "environment": [ + { + "name": "log_level", + "value": "info" + } + ] + } + ] + } +``` + +```yaml + - name: Add image to Amazon ECS task definition + id: render-image-in-task-def + uses: aws-actions/amazon-ecs-render-task-definition@v1 + with: + task-definition: task-def.json + container-name: web + image: amazon/amazon-ecs-sample:latest + + - name: Render Amazon ECS task definition for staging + id: render-staging-task-def + uses: aws-actions/amazon-ecs-render-task-definition@v1 + with: + task-definition: ${{ steps.render-image-in-task-def.outputs.task-definition }} + merge: staging-vars.json + + - name: Render Amazon ECS task definition for prod + id: render-prod-task-def + uses: aws-actions/amazon-ecs-render-task-definition@v1 + with: + task-definition: ${{ steps.render-image-in-task-def.outputs.task-definition }} + merge: prod-vars.json + + - name: Deploy to Staging + uses: aws-actions/amazon-ecs-deploy-task-definition@v1 + with: + task-definition: ${{ steps.render-staging-task-def.outputs.task-definition }} + service: my-staging-service + cluster: my-staging-cluster + + - name: Deploy to Prod + uses: aws-actions/amazon-ecs-deploy-task-definition@v1 + with: + task-definition: ${{ steps.render-prod-task-def.outputs.task-definition }} + service: my-prod-service + cluster: my-prod-cluster +``` + See [action.yml](action.yml) for the full documentation for this action's inputs and outputs. ## License Summary From 77ec5e7dbc85623dee61edc02baf0571acd98af2 Mon Sep 17 00:00:00 2001 From: Gabe Rojas-Westall Date: Fri, 6 Nov 2020 14:23:50 -0600 Subject: [PATCH 7/9] update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 128f554f..cdfbf0f9 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ input of the second: cluster: my-cluster ``` -If containers in your task definition require different values depending on environment, you can specify `merge` file that contains a JSON fragment to merge with the `task-definition`: +If containers in your task definition require different values depending on environment, you can specify a `merge` file that contains a JSON fragment to merge with the `task-definition`. `merge` task defintion JSON fragments can be used to modify any key/value pair in `task-definition`. If merging an array value, arrays from the `task-defition` and `merge` fragment will be concatenated. _task-def.json_ From 7a6b7e8050807b033ed3859dca46ac9771ffc433 Mon Sep 17 00:00:00 2001 From: Gabe Rojas-Westall Date: Fri, 6 Nov 2020 14:35:41 -0600 Subject: [PATCH 8/9] update README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index cdfbf0f9..31bc3098 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,8 @@ input of the second: If containers in your task definition require different values depending on environment, you can specify a `merge` file that contains a JSON fragment to merge with the `task-definition`. `merge` task defintion JSON fragments can be used to modify any key/value pair in `task-definition`. If merging an array value, arrays from the `task-defition` and `merge` fragment will be concatenated. +`containerDefintions` and `name` within each container definition are required. + _task-def.json_ ```json From 07f05d6b97684e408381b5b0db9fe808116521f8 Mon Sep 17 00:00:00 2001 From: Gabe Rojas-Westall Date: Fri, 6 Nov 2020 14:58:07 -0600 Subject: [PATCH 9/9] mergeWith -> mergewith --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 0c0c984d..eca2e240 100644 --- a/index.js +++ b/index.js @@ -2,7 +2,7 @@ const path = require('path'); const core = require('@actions/core'); const tmp = require('tmp'); const fs = require('fs'); -const mergeWith = require('lodash.mergeWith'); +const mergeWith = require('lodash.mergewith'); // Customizer for lodash mergeWith // allows arrays in the original task definition to contain