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: support secrets in render task definition #152

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ To insert the image URI `amazon/amazon-ecs-sample:latest` as the image for the `
container-name: web
image: amazon/amazon-ecs-sample:latest
environment-variables: "LOG_LEVEL=info"
secrets: "SECRET_KEY=arn:aws:ssm:region:0123456789:parameter/secret"

- name: Deploy to Amazon ECS service
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
Expand All @@ -50,6 +51,9 @@ input of the second:
environment-variables: |
LOG_LEVEL=info
ENVIRONMENT=prod
secrets: |
SECRET_KEY=arn:aws:ssm:region:0123456789:parameter/secret
SECOND_SECRET_KEY=arn:aws:secretsmanager:us-east-1:0123456789:secret:secretName

- name: Modify Amazon ECS task definition with second container
id: render-app-container
Expand Down
4 changes: 4 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ 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
secrets:
description: 'Secrets to add to the container. Each secret is of the form KEY=valueFrom, where valueFrom is a secret arn. You can specify multiple secrets with multi-line YAML strings.'
required: false

outputs:
task-definition:
description: 'The path to the rendered task definition file'
Expand Down
41 changes: 40 additions & 1 deletion dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1194,7 +1194,7 @@ async function run() {
const imageURI = core.getInput('image', { required: true });

const environmentVariables = core.getInput('environment-variables', { required: false });

const secrets = core.getInput('secrets', { required: false })
// Parse the task definition
const taskDefPath = path.isAbsolute(taskDefinitionFile) ?
taskDefinitionFile :
Expand Down Expand Up @@ -1251,9 +1251,48 @@ async function run() {
containerDef.environment.push(variable);
}
})
if (secrets) {
// If environment array is missing, create it
if (!Array.isArray(containerDef.secrets)) {
containerDef.secrets = [];
}

// Get pairs by splitting on newlines
secrets.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 secret '${trimmedLine}'. Secret pairs must be of the form NAME=valueFrom,
where valueFrom is an arn from parameter store or secrets manager. See AWS documentation for more information:
https://docs.aws.amazon.com/AmazonECS/latest/developerguide/specifying-sensitive-data.html.`);
}
// Build object
const secret = {
name: trimmedLine.substring(0, separatorIdx),
valueFrom: trimmedLine.substring(separatorIdx + 1),
};

// Search container definition environment for one matching name
const secretDef = containerDef.secrets.find((s) => s.name == secret.name);
if (secretDef) {
// If found, update
secretDef.valueFrom = secret.valueFrom;
} else {
// Else, create
containerDef.secrets.push(secret);
}
})
}
}



// Write out a new task definition file
var updatedTaskDefFile = tmp.fileSync({
tmpdir: process.env.RUNNER_TEMP,
Expand Down
41 changes: 40 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ async function run() {
const imageURI = core.getInput('image', { required: true });

const environmentVariables = core.getInput('environment-variables', { required: false });

const secrets = core.getInput('secrets', { required: false })
// Parse the task definition
const taskDefPath = path.isAbsolute(taskDefinitionFile) ?
taskDefinitionFile :
Expand Down Expand Up @@ -68,9 +68,48 @@ async function run() {
containerDef.environment.push(variable);
}
})
if (secrets) {
// If environment array is missing, create it
if (!Array.isArray(containerDef.secrets)) {
containerDef.secrets = [];
}

// Get pairs by splitting on newlines
secrets.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 secret '${trimmedLine}'. Secret pairs must be of the form NAME=valueFrom,
where valueFrom is an arn from parameter store or secrets manager. See AWS documentation for more information:
https://docs.aws.amazon.com/AmazonECS/latest/developerguide/specifying-sensitive-data.html.`);
}
// Build object
const secret = {
name: trimmedLine.substring(0, separatorIdx),
valueFrom: trimmedLine.substring(separatorIdx + 1),
};

// Search container definition environment for one matching name
const secretDef = containerDef.secrets.find((s) => s.name == secret.name);
if (secretDef) {
// If found, update
secretDef.valueFrom = secret.valueFrom;
} else {
// Else, create
containerDef.secrets.push(secret);
}
})
}
}



// Write out a new task definition file
var updatedTaskDefFile = tmp.fileSync({
tmpdir: process.env.RUNNER_TEMP,
Expand Down
55 changes: 51 additions & 4 deletions index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ describe('Render task definition', () => {
.mockReturnValueOnce('task-definition.json') // task-definition
.mockReturnValueOnce('web') // container-name
.mockReturnValueOnce('nginx:latest') // image
.mockReturnValueOnce('FOO=bar\nHELLO=world'); // environment-variables
.mockReturnValueOnce('FOO=bar\nHELLO=world') // environment-variables
.mockReturnValueOnce('SSM_SECRET=arn:aws:ssm:region:0123456789:parameter/secret\nSM_SECRET=arn:aws:secretsmanager:us-east-1:0123456789:secret:secretName')

process.env = Object.assign(process.env, { GITHUB_WORKSPACE: __dirname });
process.env = Object.assign(process.env, { RUNNER_TEMP: '/home/runner/work/_temp' });
Expand All @@ -43,6 +44,17 @@ describe('Render task definition', () => {
name: "DONT-TOUCH",
value: "me"
}
],
secrets: [
{
name: "EXISTING_SECRET",
valueFrom: "arn:aws:ssm:region:0123456789:parameter/existingSecret"
},
{
name: "SSM_SECRET",
valueFrom: "arn:aws:ssm:region:0123456789:parameter/oldSsmSecret"
}

]
},
{
Expand Down Expand Up @@ -82,6 +94,21 @@ describe('Render task definition', () => {
name: "HELLO",
value: "world"
}
],
secrets: [
{
name: "EXISTING_SECRET",
valueFrom: "arn:aws:ssm:region:0123456789:parameter/existingSecret"
},
{
name: "SSM_SECRET",
valueFrom: 'arn:aws:ssm:region:0123456789:parameter/secret'
},

{
name: "SM_SECRET",
valueFrom: 'arn:aws:secretsmanager:us-east-1:0123456789:secret:secretName'
}
]
},
{
Expand All @@ -94,13 +121,14 @@ describe('Render task definition', () => {
expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition', 'new-task-def-file-name');
});

test('renders a task definition at an absolute path, and with initial environment empty', async () => {
test('renders a task definition at an absolute path, and with initial environment and secrets empty', async () => {
core.getInput = jest
.fn()
.mockReturnValueOnce('/hello/task-definition.json') // task-definition
.mockReturnValueOnce('web') // container-name
.mockReturnValueOnce('nginx:latest') // image
.mockReturnValueOnce('EXAMPLE=here'); // environment-variables
.mockReturnValueOnce('EXAMPLE=here')
.mockReturnValueOnce('SECRET=arn:aws:ssm:region:0123456789:parameter/secret'); // environment-variables
jest.mock('/hello/task-definition.json', () => ({
family: 'task-def-family',
containerDefinitions: [
Expand Down Expand Up @@ -132,6 +160,12 @@ describe('Render task definition', () => {
name: "EXAMPLE",
value: "here"
}
],
secrets: [
{
name: "SECRET",
valueFrom: 'arn:aws:ssm:region:0123456789:parameter/secret'
}
]
}
]
Expand All @@ -140,7 +174,20 @@ describe('Render task definition', () => {
expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition', 'new-task-def-file-name');
});

test('error returned for missing task definition file', async () => {
test('error returned for malformatted secret string', async () => {
core.getInput = jest
.fn()
.mockReturnValueOnce('task-definition.json')
.mockReturnValueOnce('web')
.mockReturnValueOnce('nginx:latest')
.mockReturnValueOnce('EXAMPLE=here')
.mockReturnValueOnce('SECRET');
await run();

expect(core.setFailed).toBeCalledWith(expect.stringContaining(`Cannot parse the secret 'SECRET'`));
});

test('error returned for invalid secret format', async () => {
fs.existsSync.mockReturnValue(false);
core.getInput = jest
.fn()
Expand Down
Loading