Skip to content

Commit

Permalink
feature: Support multi reagion login for AWS ECR
Browse files Browse the repository at this point in the history
This adds support for allowing AWS ECR logins via multiple regions.

Functionality is quite similar to the existing support for multi-AWS
accounts. This adds in a new valid environment variable `AWS_REGIONS`
that can additionally be used to run the login against multiple regions.

Main changes are in `aws.ts` where `getRegion` is replaced by
`getRegions` which will construct and return a list rather than a
string. Since `getRegistriesData` already returns `regDatas` in a list
due to its support for multi-aws-accounts, all I really needed to add
was a `for-loop` wrapper to iterate on regions around the existing loop
that iterates on account IDs.
  • Loading branch information
hhrygim committed Feb 28, 2025
1 parent 327cd5a commit 07451cc
Show file tree
Hide file tree
Showing 5 changed files with 213 additions and 62 deletions.
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,30 @@ jobs:

> Only available with [AWS CLI version 1](https://docs.aws.amazon.com/cli/latest/reference/ecr/get-login.html)

You can use the environment variable `AWS_REGIONS` to set multiple regions account ids in `AWS_ACCOUNT_IDS`.

```yaml
name: ci
on:
push:
branches: main
jobs:
login:
runs-on: ubuntu-latest
steps:
-
name: Login to ECR
uses: docker/login-action@v3
with:
registry: <aws-account-number>.dkr.ecr.<region>.amazonaws.com
username: ${{ vars.AWS_ACCESS_KEY_ID }}
password: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
env:
AWS_REGIONS: us-west-2,us-east-1,eu-central-1
```

You can also use the [Configure AWS Credentials](https://github.com/aws-actions/configure-aws-credentials)
action in combination with this action:

Expand Down
167 changes: 136 additions & 31 deletions __tests__/aws.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,21 @@ describe('isPubECR', () => {
});
});

describe('getRegion', () => {
describe('getRegions', () => {
test.each([
['012345678901.dkr.ecr.eu-west-3.amazonaws.com', 'eu-west-3'],
['876820548815.dkr.ecr.cn-north-1.amazonaws.com.cn', 'cn-north-1'],
['390948362332.dkr.ecr.cn-northwest-1.amazonaws.com.cn', 'cn-northwest-1'],
['public.ecr.aws', 'us-east-1']
])('given registry %p', async (registry, expected) => {
expect(aws.getRegion(registry)).toEqual(expected);
['012345678901.dkr.ecr.eu-west-3.amazonaws.com', undefined, ['eu-west-3']],
['876820548815.dkr.ecr.cn-north-1.amazonaws.com.cn', undefined, ['cn-north-1']],
['390948362332.dkr.ecr.cn-northwest-1.amazonaws.com.cn', undefined, ['cn-northwest-1']],
['public.ecr.aws', undefined, ['us-east-1']],
['012345678901.dkr.ecr.eu-west-3.amazonaws.com', 'us-west-1,us-east-1', ['eu-west-3', 'us-west-1', 'us-east-1']],
['012345678901.dkr.ecr.eu-west-3.amazonaws.com', 'us-west-1,eu-west-3,us-east-1', ['eu-west-3', 'us-west-1', 'us-east-1']],
['', 'us-west-1,us-east-1', ['us-west-1', 'us-east-1']],
['', 'us-west-1,us-east-1,us-east-1', ['us-west-1', 'us-east-1']]
])('given registry %p', async (registry, regionsEnv, expected) => {
if (regionsEnv) {
process.env.AWS_REGIONS = regionsEnv;
}
expect(aws.getRegions(registry)).toEqual(expected);
});
});

Expand Down Expand Up @@ -76,12 +83,13 @@ describe('getRegistriesData', () => {
beforeEach(() => {
jest.clearAllMocks();
delete process.env.AWS_ACCOUNT_IDS;
delete process.env.AWS_REGIONS;
});
// prettier-ignore
test.each([
[
'012345678901.dkr.ecr.aws-region-1.amazonaws.com',
'dkr.ecr.aws-region-1.amazonaws.com', undefined,
'dkr.ecr.aws-region-1.amazonaws.com', undefined, undefined,
[
{
registry: '012345678901.dkr.ecr.aws-region-1.amazonaws.com',
Expand All @@ -94,6 +102,7 @@ describe('getRegistriesData', () => {
'012345678901.dkr.ecr.eu-west-3.amazonaws.com',
'dkr.ecr.eu-west-3.amazonaws.com',
'012345678910,023456789012',
undefined,
[
{
registry: '012345678901.dkr.ecr.eu-west-3.amazonaws.com',
Expand All @@ -116,41 +125,137 @@ describe('getRegistriesData', () => {
'public.ecr.aws',
undefined,
undefined,
undefined,
[
{
registry: 'public.ecr.aws',
username: 'AWS',
password: 'world'
}
]
],
[
'012345678901.dkr.ecr.eu-west-3.amazonaws.com',
undefined,
undefined,
'us-west-1,us-east-3',
[
{
registry: '012345678901.dkr.ecr.eu-west-3.amazonaws.com',
username: '012345678901',
password: 'world'
},
{
registry: '012345678901.dkr.ecr.us-west-1.amazonaws.com',
username: '012345678901',
password: 'world'
},
{
registry: '012345678901.dkr.ecr.us-east-3.amazonaws.com',
username: '012345678901',
password: 'world'
}
],
],
[
'012345678901.dkr.ecr.eu-west-3.amazonaws.com',
undefined,
'023456789012',
'us-west-1,us-east-3',
[
{
registry: '012345678901.dkr.ecr.eu-west-3.amazonaws.com',
username: '012345678901',
password: 'world'
},
{
registry: '023456789012.dkr.ecr.eu-west-3.amazonaws.com',
username: '023456789012',
password: 'world'
},
{
registry: '012345678901.dkr.ecr.us-west-1.amazonaws.com',
username: '012345678901',
password: 'world'
},
{
registry: '023456789012.dkr.ecr.us-west-1.amazonaws.com',
username: '023456789012',
password: 'world'
},
{
registry: '012345678901.dkr.ecr.us-east-3.amazonaws.com',
username: '012345678901',
password: 'world'
},
{
registry: '023456789012.dkr.ecr.us-east-3.amazonaws.com',
username: '023456789012',
password: 'world'
}
]
],
[
'',
undefined,
'012345678901,023456789012',
'us-west-1,us-east-3',
[
{
registry: '012345678901.dkr.ecr.us-west-1.amazonaws.com',
username: '012345678901',
password: 'world'
},
{
registry: '023456789012.dkr.ecr.us-west-1.amazonaws.com',
username: '023456789012',
password: 'world'
},
{
registry: '012345678901.dkr.ecr.us-east-3.amazonaws.com',
username: '012345678901',
password: 'world'
},
{
registry: '023456789012.dkr.ecr.us-east-3.amazonaws.com',
username: '023456789012',
password: 'world'
}
]
]
])('given registry %p', async (registry, fqdn, accountIDsEnv, expected: aws.RegistryData[]) => {
])('given registry %p', async (registry, fqdn, accountIDsEnv, regionsEnv, expected: aws.RegistryData[]) => {
if (accountIDsEnv) {
process.env.AWS_ACCOUNT_IDS = accountIDsEnv;
}
const accountIDs = aws.getAccountIDs(registry);
const authData: AuthorizationData[] = [];
if (accountIDs.length == 0) {
mockEcrPublicGetAuthToken.mockImplementation(() => {
return Promise.resolve({
authorizationData: {
authorizationToken: Buffer.from(`AWS:world`).toString('base64'),
}
});
});
} else {
aws.getAccountIDs(registry).forEach(accountID => {
authData.push({
authorizationToken: Buffer.from(`${accountID}:world`).toString('base64'),
proxyEndpoint: `${accountID}.${fqdn}`
});
});
mockEcrGetAuthToken.mockImplementation(() => {
return Promise.resolve({
authorizationData: authData
});
});

if (regionsEnv) {
process.env.AWS_REGIONS = regionsEnv;
}

const accountIDs = aws.getAccountIDs(registry);
const regions = aws.getRegions(registry);
const authDataByRegion: AuthorizationData[][] = [];

if (accountIDs.length == 0) {
mockEcrPublicGetAuthToken.mockImplementation(() => ({
authorizationData: {
authorizationToken: Buffer.from(`AWS:world`).toString('base64'),
}
}));
} else {
regions.forEach(region => {
const regionAuthData = accountIDs.map(accountID => ({
authorizationToken: Buffer.from(`${accountID}:world`).toString('base64'),
proxyEndpoint: `${accountID}.dkr.ecr.${region}.amazonaws.com`
}));
authDataByRegion.push(regionAuthData);
});

mockEcrGetAuthToken.mockImplementation(() => {
const regionAuthData = authDataByRegion.shift();
return { authorizationData: regionAuthData };
});
}
const regData = await aws.getRegistriesData(registry);
expect(regData).toEqual(expected);
});
Expand Down
2 changes: 1 addition & 1 deletion dist/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

80 changes: 51 additions & 29 deletions src/aws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,39 @@ export const isPubECR = (registry: string): boolean => {
return registry === 'public.ecr.aws';
};

export const getRegion = (registry: string): string => {
export const getRegions = (registry: string): string[] => {
if (isPubECR(registry)) {
return process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || 'us-east-1';
return [process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || 'us-east-1'];
}

const matches = registry.match(ecrRegistryRegex);
if (!matches) {
return '';
if (process.env.AWS_REGIONS) {
const regions: Array<string> = [...process.env.AWS_REGIONS.split(',')];
return regions.filter((item, index) => regions.indexOf(item) === index);
}
return [];
}

const regions: Array<string> = [matches[3]];
if (process.env.AWS_REGIONS) {
regions.push(...process.env.AWS_REGIONS.split(','));
}
return matches[3];

return regions.filter((item, index) => regions.indexOf(item) === index);
};

export const getAccountIDs = (registry: string): string[] => {
if (isPubECR(registry)) {
return [];
}

const matches = registry.match(ecrRegistryRegex);
if (!matches) {
if (process.env.AWS_ACCOUNT_IDS) {
const accountIDs: Array<string> = [...process.env.AWS_ACCOUNT_IDS.split(',')];
return accountIDs.filter((item, index) => accountIDs.indexOf(item) === index);
}
return [];
}
const accountIDs: Array<string> = [matches[2]];
Expand All @@ -48,7 +64,7 @@ export interface RegistryData {
}

export const getRegistriesData = async (registry: string, username?: string, password?: string): Promise<RegistryData[]> => {
const region = getRegion(registry);
const regions = getRegions(registry);
const accountIDs = getAccountIDs(registry);

const authTokenRequest = {};
Expand Down Expand Up @@ -80,11 +96,11 @@ export const getRegistriesData = async (registry: string, username?: string, pas
: undefined;

if (isPubECR(registry)) {
core.info(`AWS Public ECR detected with ${region} region`);
core.info(`AWS Public ECR detected with region ${regions[0]}`);
const ecrPublic = new ECRPUBLIC({
customUserAgent: 'docker-login-action',
credentials,
region: region,
region: regions[0],
requestHandler: new NodeHttpHandler({
httpAgent: httpProxyAgent,
httpsAgent: httpsProxyAgent
Expand All @@ -106,31 +122,37 @@ export const getRegistriesData = async (registry: string, username?: string, pas
}
];
} else {
core.info(`AWS ECR detected with ${region} region`);
const ecr = new ECR({
customUserAgent: 'docker-login-action',
credentials,
region: region,
requestHandler: new NodeHttpHandler({
httpAgent: httpProxyAgent,
httpsAgent: httpsProxyAgent
})
});
const authTokenResponse = await ecr.getAuthorizationToken(authTokenRequest);
if (!Array.isArray(authTokenResponse.authorizationData) || !authTokenResponse.authorizationData.length) {
throw new Error('Could not retrieve an authorization token from AWS ECR');
if (regions.length > 1) {
core.info(`AWS ECR detected with regions ${regions}`);
} else {
core.info(`AWS ECR detected with region ${regions[0]}`);
}
const regDatas: RegistryData[] = [];
for (const authData of authTokenResponse.authorizationData) {
const authToken = Buffer.from(authData.authorizationToken || '', 'base64').toString('utf-8');
const creds = authToken.split(':', 2);
core.setSecret(creds[0]); // redacted in workflow logs
core.setSecret(creds[1]); // redacted in workflow logs
regDatas.push({
registry: authData.proxyEndpoint || '',
username: creds[0],
password: creds[1]
for (const region of regions) {
const ecr = new ECR({
customUserAgent: 'docker-login-action',
credentials,
region: region,
requestHandler: new NodeHttpHandler({
httpAgent: httpProxyAgent,
httpsAgent: httpsProxyAgent
})
});
const authTokenResponse = await ecr.getAuthorizationToken(authTokenRequest);
if (!Array.isArray(authTokenResponse.authorizationData) || !authTokenResponse.authorizationData.length) {
throw new Error('Could not retrieve an authorization token from AWS ECR');
}
for (const authData of authTokenResponse.authorizationData) {
const authToken = Buffer.from(authData.authorizationToken || '', 'base64').toString('utf-8');
const creds = authToken.split(':', 2);
core.setSecret(creds[0]); // redacted in workflow logs
core.setSecret(creds[1]); // redacted in workflow logs
regDatas.push({
registry: authData.proxyEndpoint || '',
username: creds[0],
password: creds[1]
});
}
}
return regDatas;
}
Expand Down

0 comments on commit 07451cc

Please sign in to comment.