Skip to content

Commit

Permalink
Merge branch 'aws-actions-master' into upstream-master
Browse files Browse the repository at this point in the history
  • Loading branch information
yuekui committed Nov 27, 2023
2 parents d43675a + dcd8861 commit 56eac65
Show file tree
Hide file tree
Showing 4 changed files with 271 additions and 38 deletions.
15 changes: 6 additions & 9 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,14 @@ 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-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.'
log-configuration-log-driver:
description: "Create/Override logDriver inside logConfiguration"
required: false
log-group:
description: 'The log group to set for the task'
log-configuration-options:
description: "Create/Override options inside logConfiguration. Each variable is of the form key=value, you can specify multiple variables with multi-line YAML strings."
required: false
service-family:
description: 'The service family container'
required: false
env-list:
description: 'A list of variables to pull from the environment'
docker-labels:
description: "Create/Override options inside dockerLabels. Each variable is key=value, you can specify multiple variables with multi-line YAML."
required: false
outputs:
task-definition:
Expand Down
89 changes: 87 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ async function run() {

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

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

const logConfigurationLogDriver = core.getInput("log-configuration-log-driver", { required: false });
const logConfigurationOptions = core.getInput("log-configuration-options", { required: false });
const dockerLabels = core.getInput('docker-labels', { required: false });

// Parse the task definition
const taskDefPath = path.isAbsolute(taskDefinitionFile) ?
taskDefinitionFile :
Expand All @@ -33,7 +39,7 @@ async function run() {
if (!Array.isArray(taskDefContents.containerDefinitions)) {
throw new Error('Invalid task definition format: containerDefinitions section is not present or is not an array');
}
const containerDef = taskDefContents.containerDefinitions.find(function(element) {
const containerDef = taskDefContents.containerDefinitions.find(function (element) {
return element.name == containerName;
});
if (!containerDef) {
Expand Down Expand Up @@ -114,6 +120,85 @@ async function run() {
}


if (environmentVariables) {

// If environment array is missing, create it
if (!Array.isArray(containerDef.environment)) {
containerDef.environment = [];
}

// Get pairs by splitting on newlines
environmentVariables.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 variable '${trimmedLine}'. Environment variable pairs must be of the form NAME=value.`);
}
// Build object
const variable = {
name: trimmedLine.substring(0, separatorIdx),
value: trimmedLine.substring(separatorIdx + 1),
};

// Search container definition environment for one matching name
const variableDef = containerDef.environment.find((e) => e.name == variable.name);
if (variableDef) {
// If found, update
variableDef.value = variable.value;
} else {
// Else, create
containerDef.environment.push(variable);
}
})
}

if (logConfigurationLogDriver) {
if (!containerDef.logConfiguration) { containerDef.logConfiguration = {} }
const validDrivers = ["json-file", "syslog", "journald", "logentries", "gelf", "fluentd", "awslogs", "splunk", "awsfirelens"];
if (!validDrivers.includes(logConfigurationLogDriver)) {
throw new Error(`'${logConfigurationLogDriver}' is invalid logConfigurationLogDriver. valid options are ${validDrivers}. More details: https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_LogConfiguration.html`)
}
containerDef.logConfiguration.logDriver = logConfigurationLogDriver
}

if (logConfigurationOptions) {
if (!containerDef.logConfiguration) { containerDef.logConfiguration = {} }
if (!containerDef.logConfiguration.options) { containerDef.logConfiguration.options = {} }
logConfigurationOptions.split("\n").forEach(function (option) {
option = option.trim();
if (option && option.length) { // not a blank line
if (option.indexOf("=") == -1) {
throw new Error(`Can't parse logConfiguration option ${option}. Must be in key=value format, one per line`);
}
const [key, value] = option.split("=");
containerDef.logConfiguration.options[key] = value
}
})
}

if (dockerLabels) {
// If dockerLabels object is missing, create it
if (!containerDef.dockerLabels) { containerDef.dockerLabels = {} }

// Get pairs by splitting on newlines
dockerLabels.split('\n').forEach(function (label) {
// Trim whitespace
label = label.trim();
if (label && label.length) {
if (label.indexOf("=") == -1 ) {
throw new Error(`Can't parse logConfiguration option ${label}. Must be in key=value format, one per line`);
}
const [key, value] = label.split("=");
containerDef.dockerLabels[key] = value;
}
})
}

// Write out a new task definition file
var updatedTaskDefFile = tmp.fileSync({
tmpdir: process.env.RUNNER_TEMP,
Expand All @@ -135,5 +220,5 @@ module.exports = run;

/* istanbul ignore next */
if (require.main === module) {
run();
run();
}
195 changes: 173 additions & 22 deletions index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,17 @@ const fs = require('fs');

jest.mock('@actions/core');
jest.mock('tmp');
jest.mock('fs');
jest.mock('fs', () => ({
promises: {
access: jest.fn()
},
constants: {
O_CREATE: jest.fn()
},
rmdirSync: jest.fn(),
existsSync: jest.fn(),
writeFileSync: jest.fn()
}));

describe('Render task definition', () => {

Expand Down Expand Up @@ -75,7 +85,7 @@ describe('Render task definition', () => {
postfix: '.json',
keep: true,
discardDescriptor: true
});
});
expect(fs.writeFileSync).toHaveBeenNthCalledWith(1, 'new-task-def-file-name',
JSON.stringify({
family: 'service-family',
Expand All @@ -88,25 +98,6 @@ describe('Render task definition', () => {
"awslogs-group": "log-group",
},
},
environment: [
{
name: "FOO",
value: "bar"
},
{
name: "DONT-TOUCH",
value: "me"
},
{
name: "HELLO",
value: "world"
}
],
logConfiguration: {
options: {
"awslogs-group": "log-group",
},
},
environment: [
{
name: "FOO",
Expand Down Expand Up @@ -172,7 +163,7 @@ describe('Render task definition', () => {
postfix: '.json',
keep: true,
discardDescriptor: true
});
});
expect(fs.writeFileSync).toHaveBeenNthCalledWith(1, 'new-task-def-file-name',
JSON.stringify({
family: 'service-family',
Expand All @@ -198,6 +189,67 @@ describe('Render task definition', () => {
expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition', 'new-task-def-file-name');
});

test('renders logConfiguration on the task definition', async () => {
core.getInput = jest
.fn()
.mockReturnValueOnce('task-definition.json')
.mockReturnValueOnce('web')
.mockReturnValueOnce('nginx:latest')
.mockReturnValueOnce('FOO=bar\nHELLO=world')
.mockReturnValueOnce('awslogs')
.mockReturnValueOnce(`awslogs-create-group=true\nawslogs-group=/ecs/web\nawslogs-region=us-east-1\nawslogs-stream-prefix=ecs`);

await run()

expect(tmp.fileSync).toHaveBeenNthCalledWith(1, {
tmpdir: '/home/runner/work/_temp',
prefix: 'task-definition-',
postfix: '.json',
keep: true,
discardDescriptor: true
});


expect(fs.writeFileSync).toHaveBeenNthCalledWith(1, 'new-task-def-file-name',
JSON.stringify({
family: 'task-def-family',
containerDefinitions: [
{
name: "web",
image: "nginx:latest",
environment: [
{
name: "FOO",
value: "bar"
},
{
name: "DONT-TOUCH",
value: "me"
},
{
name: "HELLO",
value: "world"
}
],
logConfiguration: {
logDriver: "awslogs",
options: {
"awslogs-create-group": "true",
"awslogs-group": "/ecs/web",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "ecs"
}
}
},
{
name: "sidecar",
image: "hello"
}
]
}, null, 2)
);
});

test('error returned for missing task definition file', async () => {
fs.existsSync.mockReturnValue(false);
core.getInput = jest
Expand All @@ -214,6 +266,105 @@ describe('Render task definition', () => {
expect(core.setFailed).toBeCalledWith('Task definition file does not exist: does-not-exist-task-definition.json');
});

test('renders a task definition with docker labels', async () => {
core.getInput = jest
.fn()
.mockReturnValueOnce('task-definition.json')
.mockReturnValueOnce('web')
.mockReturnValueOnce('nginx:latest')
.mockReturnValueOnce('EXAMPLE=here')
.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');

await run();

expect(tmp.fileSync).toHaveBeenNthCalledWith(1, {
tmpdir: '/home/runner/work/_temp',
prefix: 'task-definition-',
postfix: '.json',
keep: true,
discardDescriptor: true
});

expect(fs.writeFileSync).toHaveBeenNthCalledWith(1, 'new-task-def-file-name',
JSON.stringify({
family: 'task-def-family',
containerDefinitions: [
{
name: "web",
image: "nginx:latest",
environment: [
{
name: "FOO",
value: "bar"
},
{
name: "DONT-TOUCH",
value: "me"
},
{
name: "HELLO",
value: "world"
},
{
name: "EXAMPLE",
value: "here"
}
],
logConfiguration: {
logDriver: "awslogs",
options: {
"awslogs-create-group": "true",
"awslogs-group": "/ecs/web",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "ecs"
}
},
dockerLabels : {
"key1":"value1",
"key2":"value2"
}
},
{
name: "sidecar",
image: "hello"
}
]
}, null, 2)
);
});

test('renders a task definition at an absolute path with bad docker labels', async () => {
core.getInput = jest
.fn()
.mockReturnValueOnce('/hello/task-definition.json')
.mockReturnValueOnce('web')
.mockReturnValueOnce('nginx:latest')
.mockReturnValueOnce('EXAMPLE=here')
.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');

jest.mock('/hello/task-definition.json', () => ({
family: 'task-def-family',
containerDefinitions: [
{
name: "web",
image: "some-other-image",
dockerLabels : {
"key1":"value1",
"key2":"value2"
}
}
]
}), { virtual: true });

await run();

expect(core.setFailed).toBeCalledWith('Can\'t parse logConfiguration option key2. Must be in key=value format, one per line');
});

test('error returned for non-JSON task definition contents', async () => {
jest.mock('./non-json-task-definition.json', () => ("hello"), { virtual: true });

Expand Down
Loading

0 comments on commit 56eac65

Please sign in to comment.