Navigation Menu

Skip to content

Commit

Permalink
Initial action code (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
clareliguori committed Nov 1, 2019
1 parent c90f042 commit 3e7c6d4
Show file tree
Hide file tree
Showing 8 changed files with 5,849 additions and 1 deletion.
18 changes: 18 additions & 0 deletions .eslintrc.json
@@ -0,0 +1,18 @@
{
"env": {
"commonjs": true,
"es6": true,
"node": true,
"jest": true
},
"extends": "eslint:recommended",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018
},
"rules": {
}
}
66 changes: 66 additions & 0 deletions .gitignore
@@ -0,0 +1,66 @@
# comment this out distribution branches
node_modules/

# Editors
.vscode

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Other Dependency directories
jspm_packages/

# TypeScript v1 declaration files
typings/

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env

# next.js build output
.next
92 changes: 91 additions & 1 deletion README.md
@@ -1,6 +1,96 @@
## Amazon ECR "Login" Action for GitHub Actions

Logs into Amazon ECR with the local Docker client.
Logs in the local Docker client to one or more Amazon ECR registries.

## Usage

```yaml
- name: Login to Amazon ECR
id: login-ecr
uses: aws/amazon-ecr-login-for-github-actions@release

- name: Build, tag, and push image to Amazon ECR
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
ECR_REPOSITORY: my-ecr-repo
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
```

## Credentials and Region

This action relies on the [default behavior of the AWS SDK for Javascript](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/setting-credentials-node.html) to determine AWS credentials and region. Use the `aws/configure-aws-credentials-for-github-actions` action to configure the GitHub Actions environment with environment variables containing AWS credentials and your desired region.

We recommend following [Amazon IAM best practices](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html) for the AWS credentials used in GitHub Actions workflows, including:
* Do not store credentials in your repository's code. You may use [GitHub Actions secrets](https://help.github.com/en/github/automating-your-workflow-with-github-actions/virtual-environments-for-github-actions#creating-and-using-secrets-encrypted-variables) to store credentials and redact credentials from GitHub Actions workflow logs.
* [Create an individual IAM user](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#create-iam-users) with an access key for use in GitHub Actions workflows, preferably one per repository. Do not use the AWS account root user access key.
* [Grant least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege) to the credentials used in GitHub Actions workflows. Grant only the permissions required to perform the actions in your GitHub Actions workflows. See the Permissions section below for the permissions required by this action.
* [Rotate the credentials](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#rotate-credentials) used in GitHub Actions workflows regularly.
* [Monitor the activity](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#keep-a-log) of the credentials used in GitHub Actions workflows.

## Permissions

This action requires the following minimum set of permissions:

```
{
"Version":"2012-10-17",
"Statement":[
{
"Sid":"GetAuthorizationToken",
"Effect":"Allow",
"Action":[
"ecr:GetAuthorizationToken"
],
"Resource":"*"
}
]
}
```

Docker commands in your GitHub Actions workflow, like `docker pull` and `docker push`, may require additional permissions attached to the credentials used by this action.
The following minimum permissions are required for pulling an image from an ECR repository:

```
{
"Version":"2012-10-17",
"Statement":[
{
"Sid":"AllowPull",
"Effect":"Allow",
"Action":[
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:BatchCheckLayerAvailability"
],
"Resource":"arn:aws:ecr:us-east-1:123456789012:repository/my-repo"
}
]
}
```

The following minimum permissions are required for pushing an image to an ECR repository:

```
{
"Version":"2012-10-17",
"Statement":[
{
"Sid":"AllowPush",
"Effect":"Allow",
"Action":[
"ecr:PutImage",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload"
],
"Resource":"arn:aws:ecr:us-east-1:123456789012:repository/my-repo"
}
]
}
```

## License Summary

Expand Down
12 changes: 12 additions & 0 deletions action.yml
@@ -0,0 +1,12 @@
name: 'Amazon ECR "Login" Action for GitHub Actions'
description: 'Logs in the local Docker client to one or more ECR registries'
inputs:
registry-ids:
description: 'A comma-delimited list of AWS account IDs that are associated with the ECR registries. If you do not specify a registry, the default ECR registry is assumed.'
required: false
outputs:
registry:
description: 'The URI of the default ECR registry i.e. aws_account_id.dkr.ecr.region.amazonaws.com, if none were provided as inputs'
runs:
using: 'node12'
main: 'dist/index.js'
63 changes: 63 additions & 0 deletions index.js
@@ -0,0 +1,63 @@
const core = require('@actions/core');
const exec = require('@actions/exec');
const aws = require('aws-sdk');

async function run() {
try {
const registries = core.getInput('registries', { required: false });

// Get the ECR authorization token
const ecr = new aws.ECR();
const authTokenRequest = {};
if (registries) {
authTokenRequest.registryIds = registries.split(',')
}
const authTokenResponse = await ecr.getAuthorizationToken(authTokenRequest).promise();
if (!Array.isArray(authTokenResponse.authorizationData) || !authTokenResponse.authorizationData.length) {
throw new Error('Could not retrieve an authorization token from Amazon ECR');
}

for (const authData of authTokenResponse.authorizationData) {
const authToken = Buffer.from(authData.authorizationToken, 'base64').toString('utf-8');
const creds = authToken.split(':', 2);
const proxyEndpoint = authData.proxyEndpoint;

if (!registries) {
// output the default registry if none were provided
const registryId = proxyEndpoint.replace(/^https?:\/\//,'');
core.setOutput('registry', registryId);
}

// Execute the docker login command
let doLoginStdout = '';
let doLoginStderr = '';
const exitCode = await exec.exec('docker login', ['-u', creds[0], '-p', creds[1], proxyEndpoint], {
silent: true,
ignoreReturnCode: true,
listeners: {
stdout: (data) => {
doLoginStdout += data.toString();
},
stderr: (data) => {
doLoginStderr += data.toString();
}
}
});

if (exitCode != 0) {
core.debug(doLoginStdout);
throw new Error('Could not login: ' + doLoginStderr);
}
}
}
catch (error) {
core.setFailed(error.message);
}
}

module.exports = run;

/* istanbul ignore next */
if (require.main === module) {
run();
}
105 changes: 105 additions & 0 deletions index.test.js
@@ -0,0 +1,105 @@
const run = require('.');
const core = require('@actions/core');
const exec = require('@actions/exec');

jest.mock('@actions/core');
jest.mock('@actions/exec');

const mockEcrGetAuthToken = jest.fn();
jest.mock('aws-sdk', () => {
return {
ECR: jest.fn(() => ({
getAuthorizationToken: mockEcrGetAuthToken
}))
};
});

describe('Login to ECR', () => {

beforeEach(() => {
jest.clearAllMocks();

mockEcrGetAuthToken.mockImplementation((params) => {
return {
promise() {
return Promise.resolve({
authorizationData: [
{
authorizationToken: Buffer.from('hello:world').toString('base64'),
proxyEndpoint: 'https://123456789012.dkr.ecr.aws-region-1.amazonaws.com'
}
]
});
}
};
});

exec.exec.mockReturnValue(0);
});

test('gets auth token from ECR and logins the Docker client for the default registry', async () => {
await run();
expect(mockEcrGetAuthToken).toHaveBeenCalled();
expect(core.setOutput).toHaveBeenNthCalledWith(1, 'registry', '123456789012.dkr.ecr.aws-region-1.amazonaws.com');
expect(exec.exec).toHaveBeenNthCalledWith(1,
'docker login',
['-u', 'hello', '-p', 'world', 'https://123456789012.dkr.ecr.aws-region-1.amazonaws.com'],
expect.anything());
});

test('gets auth token from ECR and logins the Docker client for each provided registry', async () => {
core.getInput = jest.fn().mockReturnValueOnce('123456789012,111111111111');
mockEcrGetAuthToken.mockImplementation((params) => {
return {
promise() {
return Promise.resolve({
authorizationData: [
{
authorizationToken: Buffer.from('hello:world').toString('base64'),
proxyEndpoint: 'https://123456789012.dkr.ecr.aws-region-1.amazonaws.com'
},
{
authorizationToken: Buffer.from('foo:bar').toString('base64'),
proxyEndpoint: 'https://111111111111.dkr.ecr.aws-region-1.amazonaws.com'
}
]
});
}
};
});

await run();

expect(mockEcrGetAuthToken).toHaveBeenCalledWith({
registryIds: ['123456789012','111111111111']
});
expect(core.setOutput).toHaveBeenCalledTimes(0);
expect(exec.exec).toHaveBeenCalledTimes(2);
expect(exec.exec).toHaveBeenNthCalledWith(1,
'docker login',
['-u', 'hello', '-p', 'world', 'https://123456789012.dkr.ecr.aws-region-1.amazonaws.com'],
expect.anything());
expect(exec.exec).toHaveBeenNthCalledWith(2,
'docker login',
['-u', 'foo', '-p', 'bar', 'https://111111111111.dkr.ecr.aws-region-1.amazonaws.com'],
expect.anything());
});

test('error is caught by core.setFailed for failed docker login', async () => {
exec.exec.mockReturnValue(1);

await run();

expect(core.setFailed).toBeCalled();
});

test('error is caught by core.setFailed for ECR call', async () => {
mockEcrGetAuthToken.mockImplementation(() => {
throw new Error();
});

await run();

expect(core.setFailed).toBeCalled();
});
});

0 comments on commit 3e7c6d4

Please sign in to comment.