diff --git a/README.md b/README.md index d955608d..eacf68e8 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,17 @@ To insert the image URI `amazon/amazon-ecs-sample:latest` as the image for the ` uses: aws-actions/amazon-ecs-render-task-definition@v1 with: task-definition: task-definition.json + family: "core-service" + cpu: "1024" + memory: "2048" + executionRoleArn: "arn:aws:iam::xxxxxxxxxxxx:role/x" + taskRoleArn: "arn:aws:iam::xxxxxxxxxxxx:role/x" container-name: web + awslogs-group: "ecs/web" + awslogs-region: "us-west-2" image: amazon/amazon-ecs-sample:latest environment-variables: "LOG_LEVEL=info" + environment-secrets: "SECRET_VAR=arn:aws:secretsmanager:us-west-x:xxxxxxxxxxxx:secret:prod/pkey" - name: Deploy to Amazon ECS service uses: aws-actions/amazon-ecs-deploy-task-definition@v1 diff --git a/action.yml b/action.yml index e1a613c9..5e9bb5ce 100644 --- a/action.yml +++ b/action.yml @@ -16,6 +16,30 @@ inputs: environment-variables: description: 'Variables to add to the container. Each variable is of the form KEY=value, you can specify multiple variables with multi-line YAML strings.' required: false + environment-secrets: + description: 'Secrets to add to the container. Each secret is of the form KEY=value, you can specify multiple variables with multi-line YAML strings.' + required: false + family: + description: 'task-def family' + required: false + cpu: + description: 'CPU' + required: false + memory: + description: 'Memory' + required: false + executionRoleArn: + description: 'executionRoleArn' + required: false + taskRoleArn: + description: 'taskRoleArn' + required: false + awslogs-group: + description: 'awslogs-group' + required: false + awslogs-region: + description: 'awslogs-region' + required: false outputs: task-definition: description: 'The path to the rendered task definition file' diff --git a/dist/index.js b/dist/index.js index ddd1020b..051ef98f 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1190,10 +1190,20 @@ async function run() { try { // Get inputs const taskDefinitionFile = core.getInput('task-definition', { required: true }); - const containerName = core.getInput('container-name', { required: true }); + + const family = core.getInput('family', { required: false }); + const cpu = core.getInput('cpu', { required: false }); + const memory = core.getInput('memory', { required: false }); + const executionRoleArn = core.getInput('executionRoleArn', { required: false }); + const taskRoleArn = core.getInput('taskRoleArn', { required: false }); + + const containerName = core.getInput('container-name', { required: false }); const imageURI = core.getInput('image', { required: true }); + const awslogsGroup = core.getInput('awslogs-group', { required: false }); + const awslogsRegion = core.getInput('awslogs-region', { required: false }); const environmentVariables = core.getInput('environment-variables', { required: false }); + const environmentSecrets = core.getInput('environment-secrets', { required: false }); // Parse the task definition const taskDefPath = path.isAbsolute(taskDefinitionFile) ? @@ -1216,6 +1226,26 @@ async function run() { } containerDef.image = imageURI; + if (family) { + taskDefContents.family = family; + } + + if (cpu) { + taskDefContents.cpu = cpu; + } + + if (memory) { + taskDefContents.memory = memory; + } + + if (executionRoleArn) { + taskDefContents.executionRoleArn = executionRoleArn; + } + + if (taskRoleArn) { + taskDefContents.taskRoleArn = taskRoleArn; + } + if (environmentVariables) { // If environment array is missing, create it @@ -1253,6 +1283,47 @@ async function run() { }) } + if (environmentSecrets) { + + // If environment array is missing, create it + if (!Array.isArray(containerDef.secrets)) { + containerDef.secrets = []; + } + + // Get pairs by splitting on newlines + environmentSecrets.split('\n').forEach(function (line) { + // Trim whitespace + const trimmedLine = line.trim(); + // Skip if empty + if (trimmedLine.length === 0) { return; } + // Split on = + const separatorIdx = trimmedLine.indexOf("="); + // If there's nowhere to split + if (separatorIdx === -1) { + throw new Error(`Cannot parse the environment secret '${trimmedLine}'. Environment secret pairs must be of the form KEY=value.`); + } + // Build object + const secret = { + name: trimmedLine.substring(0, separatorIdx), + valueFrom: trimmedLine.substring(separatorIdx + 1), + }; + + // Search container definition environment for one matching name + const variableDef = containerDef.secrets.find((e) => e.name == secret.name); + if (variableDef) { + // If found, update + variableDef.valueFrom = secret.valueFrom; + } else { + // Else, create + containerDef.secrets.push(secret); + } + }) + } + + if (awslogsGroup && awslogsRegion && containerDef.logConfiguration && containerDef.logConfiguration.options) { + containerDef.logConfiguration.options["awslogs-group"] = awslogsGroup; + containerDef.logConfiguration.options["awslogs-region"] = awslogsRegion; + } // Write out a new task definition file var updatedTaskDefFile = tmp.fileSync({ diff --git a/index.js b/index.js index 99bb717d..b316ea16 100644 --- a/index.js +++ b/index.js @@ -7,10 +7,20 @@ async function run() { try { // Get inputs const taskDefinitionFile = core.getInput('task-definition', { required: true }); - const containerName = core.getInput('container-name', { required: true }); + + const family = core.getInput('family', { required: false }); + const cpu = core.getInput('cpu', { required: false }); + const memory = core.getInput('memory', { required: false }); + const executionRoleArn = core.getInput('executionRoleArn', { required: false }); + const taskRoleArn = core.getInput('taskRoleArn', { required: false }); + + const containerName = core.getInput('container-name', { required: false }); const imageURI = core.getInput('image', { required: true }); + const awslogsGroup = core.getInput('awslogs-group', { required: false }); + const awslogsRegion = core.getInput('awslogs-region', { required: false }); const environmentVariables = core.getInput('environment-variables', { required: false }); + const environmentSecrets = core.getInput('environment-secrets', { required: false }); // Parse the task definition const taskDefPath = path.isAbsolute(taskDefinitionFile) ? @@ -33,6 +43,26 @@ async function run() { } containerDef.image = imageURI; + if (family) { + taskDefContents.family = family; + } + + if (cpu) { + taskDefContents.cpu = cpu; + } + + if (memory) { + taskDefContents.memory = memory; + } + + if (executionRoleArn) { + taskDefContents.executionRoleArn = executionRoleArn; + } + + if (taskRoleArn) { + taskDefContents.taskRoleArn = taskRoleArn; + } + if (environmentVariables) { // If environment array is missing, create it @@ -70,6 +100,47 @@ async function run() { }) } + if (environmentSecrets) { + + // If environment array is missing, create it + if (!Array.isArray(containerDef.secrets)) { + containerDef.secrets = []; + } + + // Get pairs by splitting on newlines + environmentSecrets.split('\n').forEach(function (line) { + // Trim whitespace + const trimmedLine = line.trim(); + // Skip if empty + if (trimmedLine.length === 0) { return; } + // Split on = + const separatorIdx = trimmedLine.indexOf("="); + // If there's nowhere to split + if (separatorIdx === -1) { + throw new Error(`Cannot parse the environment secret '${trimmedLine}'. Environment secret pairs must be of the form KEY=value.`); + } + // Build object + const secret = { + name: trimmedLine.substring(0, separatorIdx), + valueFrom: trimmedLine.substring(separatorIdx + 1), + }; + + // Search container definition environment for one matching name + const variableDef = containerDef.secrets.find((e) => e.name == secret.name); + if (variableDef) { + // If found, update + variableDef.valueFrom = secret.valueFrom; + } else { + // Else, create + containerDef.secrets.push(secret); + } + }) + } + + if (awslogsGroup && awslogsRegion && containerDef.logConfiguration && containerDef.logConfiguration.options) { + containerDef.logConfiguration.options["awslogs-group"] = awslogsGroup; + containerDef.logConfiguration.options["awslogs-region"] = awslogsRegion; + } // Write out a new task definition file var updatedTaskDefFile = tmp.fileSync({ diff --git a/index.test.js b/index.test.js index a1f89ffe..0185871a 100644 --- a/index.test.js +++ b/index.test.js @@ -15,9 +15,17 @@ describe('Render task definition', () => { core.getInput = jest .fn() .mockReturnValueOnce('task-definition.json') // task-definition + .mockReturnValueOnce('task-def-family-modified') // family + .mockReturnValueOnce('2048') // cpu + .mockReturnValueOnce('4096') // memory + .mockReturnValueOnce('arn:aws:iam::xxxxxxxxxxxx:role/new') // executionRoleArn + .mockReturnValueOnce('arn:aws:iam::xxxxxxxxxxxx:role/new') // taskRoleArn .mockReturnValueOnce('web') // container-name .mockReturnValueOnce('nginx:latest') // image - .mockReturnValueOnce('FOO=bar\nHELLO=world'); // environment-variables + .mockReturnValueOnce('/ecs/new') // awslogs-group + .mockReturnValueOnce('us-west-1') // awslogs-region + .mockReturnValueOnce('FOO=bar\nHELLO=world') // environment-variables + .mockReturnValueOnce('FOO=bar\nHELLO=world'); // environment-secrets process.env = Object.assign(process.env, { GITHUB_WORKSPACE: __dirname }); process.env = Object.assign(process.env, { RUNNER_TEMP: '/home/runner/work/_temp' }); @@ -30,10 +38,23 @@ describe('Render task definition', () => { jest.mock('./task-definition.json', () => ({ family: 'task-def-family', + cpu: "1024", + memory: "2048", + executionRoleArn: "arn:aws:iam::xxxxxxxxxxxx:role/old", + taskRoleArn: "arn:aws:iam::xxxxxxxxxxxx:role/old", containerDefinitions: [ { name: "web", image: "some-other-image", + logConfiguration: { + logDriver: "awslogs", + options: { + "awslogs-group": "/ecs/old", + "awslogs-region": "us-west-2", + "awslogs-stream-prefix": "ecs", + "awslogs-create-group": "true" + } + }, environment: [ { name: "FOO", @@ -43,6 +64,16 @@ describe('Render task definition', () => { name: "DONT-TOUCH", value: "me" } + ], + secrets: [ + { + name: "FOO", + valueFrom: "not bar" + }, + { + name: "DONT-TOUCH", + valueFrom: "me" + } ] }, { @@ -64,11 +95,24 @@ describe('Render task definition', () => { }); expect(fs.writeFileSync).toHaveBeenNthCalledWith(1, 'new-task-def-file-name', JSON.stringify({ - family: 'task-def-family', + family: 'task-def-family-modified', + cpu: "2048", + memory: "4096", + executionRoleArn: "arn:aws:iam::xxxxxxxxxxxx:role/new", + taskRoleArn: "arn:aws:iam::xxxxxxxxxxxx:role/new", containerDefinitions: [ { name: "web", image: "nginx:latest", + logConfiguration: { + logDriver: "awslogs", + options: { + "awslogs-group": "/ecs/new", + "awslogs-region": "us-west-1", + "awslogs-stream-prefix": "ecs", + "awslogs-create-group": "true" + } + }, environment: [ { name: "FOO", @@ -82,6 +126,20 @@ describe('Render task definition', () => { name: "HELLO", value: "world" } + ], + secrets: [ + { + name: "FOO", + valueFrom: "bar" + }, + { + name: "DONT-TOUCH", + valueFrom: "me" + }, + { + name: "HELLO", + valueFrom: "world" + } ] }, { @@ -98,9 +156,17 @@ describe('Render task definition', () => { core.getInput = jest .fn() .mockReturnValueOnce('/hello/task-definition.json') // task-definition + .mockReturnValueOnce('task-def-family') // family + .mockReturnValueOnce('2048') // cpu + .mockReturnValueOnce('4096') // memory + .mockReturnValueOnce('arn:aws:iam::xxxxxxxxxxxx:role/new') // executionRoleArn + .mockReturnValueOnce('arn:aws:iam::xxxxxxxxxxxx:role/new') // taskRoleArn .mockReturnValueOnce('web') // container-name .mockReturnValueOnce('nginx:latest') // image - .mockReturnValueOnce('EXAMPLE=here'); // environment-variables + .mockReturnValueOnce('/ecs/new') // awslogs-group + .mockReturnValueOnce('us-west-1') // awslogs-region + .mockReturnValueOnce('EXAMPLE=here') // environment-variables + .mockReturnValueOnce('EXAMPLE=here'); // environment-secrets jest.mock('/hello/task-definition.json', () => ({ family: 'task-def-family', containerDefinitions: [ @@ -132,9 +198,19 @@ describe('Render task definition', () => { name: "EXAMPLE", value: "here" } + ], + secrets: [ + { + name: "EXAMPLE", + valueFrom: "here" + } ] } - ] + ], + cpu: "2048", + memory: "4096", + executionRoleArn: "arn:aws:iam::xxxxxxxxxxxx:role/new", + taskRoleArn: "arn:aws:iam::xxxxxxxxxxxx:role/new", }, null, 2) ); expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition', 'new-task-def-file-name');