Skip to content

Commit a98cecd

Browse files
committed
feature: Support multi reagion login for AWS ECR
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. Signed-off-by: Helen Lim <hlim2@atlassian.com>
1 parent 327cd5a commit a98cecd

File tree

5 files changed

+213
-62
lines changed

5 files changed

+213
-62
lines changed

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,30 @@ jobs:
352352

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

355+
You can use the environment variable `AWS_REGIONS` to set multiple regions account ids in `AWS_ACCOUNT_IDS`.
356+
357+
```yaml
358+
name: ci
359+
360+
on:
361+
push:
362+
branches: main
363+
364+
jobs:
365+
login:
366+
runs-on: ubuntu-latest
367+
steps:
368+
-
369+
name: Login to ECR
370+
uses: docker/login-action@v3
371+
with:
372+
registry: <aws-account-number>.dkr.ecr.<region>.amazonaws.com
373+
username: ${{ vars.AWS_ACCESS_KEY_ID }}
374+
password: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
375+
env:
376+
AWS_REGIONS: us-west-2,us-east-1,eu-central-1
377+
```
378+
355379
You can also use the [Configure AWS Credentials](https://github.com/aws-actions/configure-aws-credentials)
356380
action in combination with this action:
357381

__tests__/aws.test.ts

Lines changed: 136 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,21 @@ describe('isPubECR', () => {
2929
});
3030
});
3131

32-
describe('getRegion', () => {
32+
describe('getRegions', () => {
3333
test.each([
34-
['012345678901.dkr.ecr.eu-west-3.amazonaws.com', 'eu-west-3'],
35-
['876820548815.dkr.ecr.cn-north-1.amazonaws.com.cn', 'cn-north-1'],
36-
['390948362332.dkr.ecr.cn-northwest-1.amazonaws.com.cn', 'cn-northwest-1'],
37-
['public.ecr.aws', 'us-east-1']
38-
])('given registry %p', async (registry, expected) => {
39-
expect(aws.getRegion(registry)).toEqual(expected);
34+
['012345678901.dkr.ecr.eu-west-3.amazonaws.com', undefined, ['eu-west-3']],
35+
['876820548815.dkr.ecr.cn-north-1.amazonaws.com.cn', undefined, ['cn-north-1']],
36+
['390948362332.dkr.ecr.cn-northwest-1.amazonaws.com.cn', undefined, ['cn-northwest-1']],
37+
['public.ecr.aws', undefined, ['us-east-1']],
38+
['012345678901.dkr.ecr.eu-west-3.amazonaws.com', 'us-west-1,us-east-1', ['eu-west-3', 'us-west-1', 'us-east-1']],
39+
['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']],
40+
['', 'us-west-1,us-east-1', ['us-west-1', 'us-east-1']],
41+
['', 'us-west-1,us-east-1,us-east-1', ['us-west-1', 'us-east-1']]
42+
])('given registry %p', async (registry, regionsEnv, expected) => {
43+
if (regionsEnv) {
44+
process.env.AWS_REGIONS = regionsEnv;
45+
}
46+
expect(aws.getRegions(registry)).toEqual(expected);
4047
});
4148
});
4249

@@ -76,12 +83,13 @@ describe('getRegistriesData', () => {
7683
beforeEach(() => {
7784
jest.clearAllMocks();
7885
delete process.env.AWS_ACCOUNT_IDS;
86+
delete process.env.AWS_REGIONS;
7987
});
8088
// prettier-ignore
8189
test.each([
8290
[
8391
'012345678901.dkr.ecr.aws-region-1.amazonaws.com',
84-
'dkr.ecr.aws-region-1.amazonaws.com', undefined,
92+
'dkr.ecr.aws-region-1.amazonaws.com', undefined, undefined,
8593
[
8694
{
8795
registry: '012345678901.dkr.ecr.aws-region-1.amazonaws.com',
@@ -94,6 +102,7 @@ describe('getRegistriesData', () => {
94102
'012345678901.dkr.ecr.eu-west-3.amazonaws.com',
95103
'dkr.ecr.eu-west-3.amazonaws.com',
96104
'012345678910,023456789012',
105+
undefined,
97106
[
98107
{
99108
registry: '012345678901.dkr.ecr.eu-west-3.amazonaws.com',
@@ -116,41 +125,137 @@ describe('getRegistriesData', () => {
116125
'public.ecr.aws',
117126
undefined,
118127
undefined,
128+
undefined,
119129
[
120130
{
121131
registry: 'public.ecr.aws',
122132
username: 'AWS',
123133
password: 'world'
124134
}
125135
]
136+
],
137+
[
138+
'012345678901.dkr.ecr.eu-west-3.amazonaws.com',
139+
undefined,
140+
undefined,
141+
'us-west-1,us-east-3',
142+
[
143+
{
144+
registry: '012345678901.dkr.ecr.eu-west-3.amazonaws.com',
145+
username: '012345678901',
146+
password: 'world'
147+
},
148+
{
149+
registry: '012345678901.dkr.ecr.us-west-1.amazonaws.com',
150+
username: '012345678901',
151+
password: 'world'
152+
},
153+
{
154+
registry: '012345678901.dkr.ecr.us-east-3.amazonaws.com',
155+
username: '012345678901',
156+
password: 'world'
157+
}
158+
],
159+
],
160+
[
161+
'012345678901.dkr.ecr.eu-west-3.amazonaws.com',
162+
undefined,
163+
'023456789012',
164+
'us-west-1,us-east-3',
165+
[
166+
{
167+
registry: '012345678901.dkr.ecr.eu-west-3.amazonaws.com',
168+
username: '012345678901',
169+
password: 'world'
170+
},
171+
{
172+
registry: '023456789012.dkr.ecr.eu-west-3.amazonaws.com',
173+
username: '023456789012',
174+
password: 'world'
175+
},
176+
{
177+
registry: '012345678901.dkr.ecr.us-west-1.amazonaws.com',
178+
username: '012345678901',
179+
password: 'world'
180+
},
181+
{
182+
registry: '023456789012.dkr.ecr.us-west-1.amazonaws.com',
183+
username: '023456789012',
184+
password: 'world'
185+
},
186+
{
187+
registry: '012345678901.dkr.ecr.us-east-3.amazonaws.com',
188+
username: '012345678901',
189+
password: 'world'
190+
},
191+
{
192+
registry: '023456789012.dkr.ecr.us-east-3.amazonaws.com',
193+
username: '023456789012',
194+
password: 'world'
195+
}
196+
]
197+
],
198+
[
199+
'',
200+
undefined,
201+
'012345678901,023456789012',
202+
'us-west-1,us-east-3',
203+
[
204+
{
205+
registry: '012345678901.dkr.ecr.us-west-1.amazonaws.com',
206+
username: '012345678901',
207+
password: 'world'
208+
},
209+
{
210+
registry: '023456789012.dkr.ecr.us-west-1.amazonaws.com',
211+
username: '023456789012',
212+
password: 'world'
213+
},
214+
{
215+
registry: '012345678901.dkr.ecr.us-east-3.amazonaws.com',
216+
username: '012345678901',
217+
password: 'world'
218+
},
219+
{
220+
registry: '023456789012.dkr.ecr.us-east-3.amazonaws.com',
221+
username: '023456789012',
222+
password: 'world'
223+
}
224+
]
126225
]
127-
])('given registry %p', async (registry, fqdn, accountIDsEnv, expected: aws.RegistryData[]) => {
226+
])('given registry %p', async (registry, fqdn, accountIDsEnv, regionsEnv, expected: aws.RegistryData[]) => {
128227
if (accountIDsEnv) {
129228
process.env.AWS_ACCOUNT_IDS = accountIDsEnv;
130229
}
131-
const accountIDs = aws.getAccountIDs(registry);
132-
const authData: AuthorizationData[] = [];
133-
if (accountIDs.length == 0) {
134-
mockEcrPublicGetAuthToken.mockImplementation(() => {
135-
return Promise.resolve({
136-
authorizationData: {
137-
authorizationToken: Buffer.from(`AWS:world`).toString('base64'),
138-
}
139-
});
140-
});
141-
} else {
142-
aws.getAccountIDs(registry).forEach(accountID => {
143-
authData.push({
144-
authorizationToken: Buffer.from(`${accountID}:world`).toString('base64'),
145-
proxyEndpoint: `${accountID}.${fqdn}`
146-
});
147-
});
148-
mockEcrGetAuthToken.mockImplementation(() => {
149-
return Promise.resolve({
150-
authorizationData: authData
151-
});
152-
});
230+
231+
if (regionsEnv) {
232+
process.env.AWS_REGIONS = regionsEnv;
153233
}
234+
235+
const accountIDs = aws.getAccountIDs(registry);
236+
const regions = aws.getRegions(registry);
237+
const authDataByRegion: AuthorizationData[][] = [];
238+
239+
if (accountIDs.length == 0) {
240+
mockEcrPublicGetAuthToken.mockImplementation(() => ({
241+
authorizationData: {
242+
authorizationToken: Buffer.from(`AWS:world`).toString('base64'),
243+
}
244+
}));
245+
} else {
246+
regions.forEach(region => {
247+
const regionAuthData = accountIDs.map(accountID => ({
248+
authorizationToken: Buffer.from(`${accountID}:world`).toString('base64'),
249+
proxyEndpoint: `${accountID}.dkr.ecr.${region}.amazonaws.com`
250+
}));
251+
authDataByRegion.push(regionAuthData);
252+
});
253+
254+
mockEcrGetAuthToken.mockImplementation(() => {
255+
const regionAuthData = authDataByRegion.shift();
256+
return { authorizationData: regionAuthData };
257+
});
258+
}
154259
const regData = await aws.getRegistriesData(registry);
155260
expect(regData).toEqual(expected);
156261
});

dist/index.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/aws.ts

Lines changed: 51 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,39 @@ export const isPubECR = (registry: string): boolean => {
1515
return registry === 'public.ecr.aws';
1616
};
1717

18-
export const getRegion = (registry: string): string => {
18+
export const getRegions = (registry: string): string[] => {
1919
if (isPubECR(registry)) {
20-
return process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || 'us-east-1';
20+
return [process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || 'us-east-1'];
2121
}
22+
2223
const matches = registry.match(ecrRegistryRegex);
2324
if (!matches) {
24-
return '';
25+
if (process.env.AWS_REGIONS) {
26+
const regions: Array<string> = [...process.env.AWS_REGIONS.split(',')];
27+
return regions.filter((item, index) => regions.indexOf(item) === index);
28+
}
29+
return [];
30+
}
31+
32+
const regions: Array<string> = [matches[3]];
33+
if (process.env.AWS_REGIONS) {
34+
regions.push(...process.env.AWS_REGIONS.split(','));
2535
}
26-
return matches[3];
36+
37+
return regions.filter((item, index) => regions.indexOf(item) === index);
2738
};
2839

2940
export const getAccountIDs = (registry: string): string[] => {
3041
if (isPubECR(registry)) {
3142
return [];
3243
}
44+
3345
const matches = registry.match(ecrRegistryRegex);
3446
if (!matches) {
47+
if (process.env.AWS_ACCOUNT_IDS) {
48+
const accountIDs: Array<string> = [...process.env.AWS_ACCOUNT_IDS.split(',')];
49+
return accountIDs.filter((item, index) => accountIDs.indexOf(item) === index);
50+
}
3551
return [];
3652
}
3753
const accountIDs: Array<string> = [matches[2]];
@@ -48,7 +64,7 @@ export interface RegistryData {
4864
}
4965

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

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

8298
if (isPubECR(registry)) {
83-
core.info(`AWS Public ECR detected with ${region} region`);
99+
core.info(`AWS Public ECR detected with region ${regions[0]}`);
84100
const ecrPublic = new ECRPUBLIC({
85101
customUserAgent: 'docker-login-action',
86102
credentials,
87-
region: region,
103+
region: regions[0],
88104
requestHandler: new NodeHttpHandler({
89105
httpAgent: httpProxyAgent,
90106
httpsAgent: httpsProxyAgent
@@ -106,31 +122,37 @@ export const getRegistriesData = async (registry: string, username?: string, pas
106122
}
107123
];
108124
} else {
109-
core.info(`AWS ECR detected with ${region} region`);
110-
const ecr = new ECR({
111-
customUserAgent: 'docker-login-action',
112-
credentials,
113-
region: region,
114-
requestHandler: new NodeHttpHandler({
115-
httpAgent: httpProxyAgent,
116-
httpsAgent: httpsProxyAgent
117-
})
118-
});
119-
const authTokenResponse = await ecr.getAuthorizationToken(authTokenRequest);
120-
if (!Array.isArray(authTokenResponse.authorizationData) || !authTokenResponse.authorizationData.length) {
121-
throw new Error('Could not retrieve an authorization token from AWS ECR');
125+
if (regions.length > 1) {
126+
core.info(`AWS ECR detected with regions ${regions}`);
127+
} else {
128+
core.info(`AWS ECR detected with region ${regions[0]}`);
122129
}
123130
const regDatas: RegistryData[] = [];
124-
for (const authData of authTokenResponse.authorizationData) {
125-
const authToken = Buffer.from(authData.authorizationToken || '', 'base64').toString('utf-8');
126-
const creds = authToken.split(':', 2);
127-
core.setSecret(creds[0]); // redacted in workflow logs
128-
core.setSecret(creds[1]); // redacted in workflow logs
129-
regDatas.push({
130-
registry: authData.proxyEndpoint || '',
131-
username: creds[0],
132-
password: creds[1]
131+
for (const region of regions) {
132+
const ecr = new ECR({
133+
customUserAgent: 'docker-login-action',
134+
credentials,
135+
region: region,
136+
requestHandler: new NodeHttpHandler({
137+
httpAgent: httpProxyAgent,
138+
httpsAgent: httpsProxyAgent
139+
})
133140
});
141+
const authTokenResponse = await ecr.getAuthorizationToken(authTokenRequest);
142+
if (!Array.isArray(authTokenResponse.authorizationData) || !authTokenResponse.authorizationData.length) {
143+
throw new Error('Could not retrieve an authorization token from AWS ECR');
144+
}
145+
for (const authData of authTokenResponse.authorizationData) {
146+
const authToken = Buffer.from(authData.authorizationToken || '', 'base64').toString('utf-8');
147+
const creds = authToken.split(':', 2);
148+
core.setSecret(creds[0]); // redacted in workflow logs
149+
core.setSecret(creds[1]); // redacted in workflow logs
150+
regDatas.push({
151+
registry: authData.proxyEndpoint || '',
152+
username: creds[0],
153+
password: creds[1]
154+
});
155+
}
134156
}
135157
return regDatas;
136158
}

0 commit comments

Comments
 (0)