Skip to content
This repository has been archived by the owner on Mar 21, 2024. It is now read-only.

Commit

Permalink
add support to provide secrets
Browse files Browse the repository at this point in the history
taken from aws-actions#152

Signed-off-by: Florian Greinus <[email protected]>
  • Loading branch information
fgreinus committed Jan 24, 2024
1 parent 612ac18 commit 28c9289
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 4 deletions.
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
docker-labels: |
SERVICE=service
VERSION=version
Expand Down
3 changes: 3 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ 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
log-configuration-log-driver:
description: "Create/Override logDriver inside logConfiguration"
required: false
Expand Down
40 changes: 40 additions & 0 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,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 })

const logConfigurationLogDriver = core.getInput("log-configuration-log-driver", { required: false });
const logConfigurationOptions = core.getInput("log-configuration-options", { required: false });
Expand Down Expand Up @@ -80,6 +81,45 @@ async function run() {
})
}

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);
}
})
}

if (logConfigurationLogDriver) {
if (!containerDef.logConfiguration) { containerDef.logConfiguration = {} }
const validDrivers = ["json-file", "syslog", "journald", "logentries", "gelf", "fluentd", "awslogs", "splunk", "awsfirelens"];
Expand Down
40 changes: 40 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +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 })

const logConfigurationLogDriver = core.getInput("log-configuration-log-driver", { required: false });
const logConfigurationOptions = core.getInput("log-configuration-options", { required: false });
Expand Down Expand Up @@ -74,6 +75,45 @@ async function run() {
})
}

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);
}
})
}

if (logConfigurationLogDriver) {
if (!containerDef.logConfiguration) { containerDef.logConfiguration = {} }
const validDrivers = ["json-file", "syslog", "journald", "logentries", "gelf", "fluentd", "awslogs", "splunk", "awsfirelens"];
Expand Down
66 changes: 62 additions & 4 deletions index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,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('EXISTING_SECRET=arn:aws:ssm:region:0123456789:parameter/existingSecret');

process.env = Object.assign(process.env, { GITHUB_WORKSPACE: __dirname });
process.env = Object.assign(process.env, { RUNNER_TEMP: '/home/runner/work/_temp' });
Expand All @@ -53,6 +54,12 @@ describe('Render task definition', () => {
name: "DONT-TOUCH",
value: "me"
}
],
secrets: [
{
name: "EXISTING_SECRET",
valueFrom: "arn:aws:ssm:region:0123456789:parameter/existingSecret"
}
]
},
{
Expand Down Expand Up @@ -92,6 +99,12 @@ describe('Render task definition', () => {
name: "HELLO",
value: "world"
}
],
secrets: [
{
name: "EXISTING_SECRET",
valueFrom: "arn:aws:ssm:region:0123456789:parameter/existingSecret"
},
]
},
{
Expand All @@ -110,7 +123,8 @@ describe('Render task definition', () => {
.mockReturnValueOnce('/hello/task-definition.json') // task-definition
.mockReturnValueOnce('web') // container-name
.mockReturnValueOnce('nginx:latest') // image
.mockReturnValueOnce('EXAMPLE=here'); // environment-variables
.mockReturnValueOnce('EXAMPLE=here') // environment-variables
.mockReturnValueOnce('SECRET=arn:aws:ssm:region:0123456789:parameter/secret');
jest.mock('/hello/task-definition.json', () => ({
family: 'task-def-family',
containerDefinitions: [
Expand Down Expand Up @@ -142,6 +156,12 @@ describe('Render task definition', () => {
name: "EXAMPLE",
value: "here"
}
],
secrets: [
{
name: "SECRET",
valueFrom: 'arn:aws:ssm:region:0123456789:parameter/secret'
}
]
}
]
Expand All @@ -157,6 +177,7 @@ describe('Render task definition', () => {
.mockReturnValueOnce('web')
.mockReturnValueOnce('nginx:latest')
.mockReturnValueOnce('FOO=bar\nHELLO=world')
.mockReturnValueOnce('SECRET=a')
.mockReturnValueOnce('awslogs')
.mockReturnValueOnce(`awslogs-create-group=true\nawslogs-group=/ecs/web\nawslogs-region=us-east-1\nawslogs-stream-prefix=ecs`);

Expand All @@ -170,7 +191,6 @@ describe('Render task definition', () => {
discardDescriptor: true
});


expect(fs.writeFileSync).toHaveBeenNthCalledWith(1, 'new-task-def-file-name',
JSON.stringify({
family: 'task-def-family',
Expand All @@ -192,6 +212,16 @@ describe('Render task definition', () => {
value: "world"
}
],
secrets: [
{
name: "EXISTING_SECRET",
valueFrom: "arn:aws:ssm:region:0123456789:parameter/existingSecret"
},
{
name: "SECRET",
valueFrom: 'a'
}
],
logConfiguration: {
logDriver: "awslogs",
options: {
Expand Down Expand Up @@ -231,6 +261,7 @@ describe('Render task definition', () => {
.mockReturnValueOnce('web')
.mockReturnValueOnce('nginx:latest')
.mockReturnValueOnce('EXAMPLE=here')
.mockReturnValueOnce('SECRET=a')
.mockReturnValueOnce('awslogs')
.mockReturnValueOnce('awslogs-create-group=true\nawslogs-group=/ecs/web\nawslogs-region=us-east-1\nawslogs-stream-prefix=ecs')
.mockReturnValueOnce('key1=value1\nkey2=value2');
Expand Down Expand Up @@ -270,6 +301,16 @@ describe('Render task definition', () => {
value: "here"
}
],
secrets: [
{
"name": "EXISTING_SECRET",
"valueFrom": "arn:aws:ssm:region:0123456789:parameter/existingSecret"
},
{
"name": "SECRET",
"valueFrom": "a",
}
],
logConfiguration: {
logDriver: "awslogs",
options: {
Expand Down Expand Up @@ -300,6 +341,7 @@ describe('Render task definition', () => {
.mockReturnValueOnce('web')
.mockReturnValueOnce('nginx:latest')
.mockReturnValueOnce('EXAMPLE=here')
.mockReturnValueOnce('SECRET=a')
.mockReturnValueOnce('awslogs')
.mockReturnValueOnce('awslogs-create-group=true\nawslogs-group=/ecs/web\nawslogs-region=us-east-1\nawslogs-stream-prefix=ecs')
.mockReturnValueOnce('key1=update_value1\nkey2\nkey3=value3');
Expand All @@ -313,7 +355,10 @@ describe('Render task definition', () => {
dockerLabels : {
"key1":"value1",
"key2":"value2"
}
},
secrets: {
"SECRET": "a",
},
}
]
}), { virtual: true });
Expand Down Expand Up @@ -375,4 +420,17 @@ describe('Render task definition', () => {

expect(core.setFailed).toBeCalledWith('Invalid task definition: Could not find container definition with matching name');
});

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'`));
});
});

0 comments on commit 28c9289

Please sign in to comment.