Skip to content

Commit 7567f92

Browse files
committed
Add scope input to set scopes for the authentication token
Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
1 parent 0567fa5 commit 7567f92

File tree

8 files changed

+264
-78
lines changed

8 files changed

+264
-78
lines changed

.github/workflows/ci.yml

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ jobs:
207207
with:
208208
registry: public.ecr.aws
209209

210-
github-container:
210+
ghcr:
211211
runs-on: ${{ matrix.os }}
212212
strategy:
213213
fail-fast: false
@@ -356,3 +356,125 @@ jobs:
356356
echo "::error::Should have failed"
357357
exit 1
358358
fi
359+
360+
scope-dockerhub:
361+
runs-on: ${{ matrix.os }}
362+
strategy:
363+
fail-fast: false
364+
matrix:
365+
os:
366+
- ubuntu-latest
367+
- windows-latest
368+
steps:
369+
-
370+
name: Checkout
371+
uses: actions/checkout@v6
372+
-
373+
name: Login to Docker Hub
374+
uses: ./
375+
with:
376+
username: ${{ secrets.DOCKERHUB_USERNAME }}
377+
password: ${{ secrets.DOCKERHUB_TOKEN }}
378+
scope: '@push'
379+
-
380+
name: Print config.json files
381+
shell: bash
382+
run: |
383+
shopt -s globstar nullglob
384+
for file in ~/.docker/**/config.json; do
385+
echo "## ${file}"
386+
jq '(.auths[]?.auth) |= "REDACTED"' "$file"
387+
echo ""
388+
done
389+
390+
scope-dockerhub-repo:
391+
runs-on: ${{ matrix.os }}
392+
strategy:
393+
fail-fast: false
394+
matrix:
395+
os:
396+
- ubuntu-latest
397+
- windows-latest
398+
steps:
399+
-
400+
name: Checkout
401+
uses: actions/checkout@v6
402+
-
403+
name: Login to Docker Hub
404+
uses: ./
405+
with:
406+
username: ${{ secrets.DOCKERHUB_USERNAME }}
407+
password: ${{ secrets.DOCKERHUB_TOKEN }}
408+
scope: 'docker/buildx-bin@push'
409+
-
410+
name: Print config.json files
411+
shell: bash
412+
run: |
413+
shopt -s globstar nullglob
414+
for file in ~/.docker/**/config.json; do
415+
echo "## ${file}"
416+
jq '(.auths[]?.auth) |= "REDACTED"' "$file"
417+
echo ""
418+
done
419+
420+
scope-ghcr:
421+
runs-on: ${{ matrix.os }}
422+
strategy:
423+
fail-fast: false
424+
matrix:
425+
os:
426+
- ubuntu-latest
427+
- windows-latest
428+
steps:
429+
-
430+
name: Checkout
431+
uses: actions/checkout@v6
432+
-
433+
name: Login to GitHub Container Registry
434+
uses: ./
435+
with:
436+
registry: ghcr.io
437+
username: ${{ github.actor }}
438+
password: ${{ secrets.GITHUB_TOKEN }}
439+
scope: '@push'
440+
-
441+
name: Print config.json files
442+
shell: bash
443+
run: |
444+
shopt -s globstar nullglob
445+
for file in ~/.docker/**/config.json; do
446+
echo "## ${file}"
447+
jq '(.auths[]?.auth) |= "REDACTED"' "$file"
448+
echo ""
449+
done
450+
451+
scope-ghcr-repo:
452+
runs-on: ${{ matrix.os }}
453+
strategy:
454+
fail-fast: false
455+
matrix:
456+
os:
457+
- ubuntu-latest
458+
- windows-latest
459+
steps:
460+
-
461+
name: Checkout
462+
uses: actions/checkout@v6
463+
-
464+
name: Login to GitHub Container Registry
465+
uses: ./
466+
with:
467+
registry: ghcr.io
468+
username: ${{ github.actor }}
469+
password: ${{ secrets.GITHUB_TOKEN }}
470+
scope: 'docker/login-action@push'
471+
-
472+
name: Print config.json files
473+
shell: bash
474+
run: |
475+
shopt -s globstar nullglob
476+
for file in ~/.docker/**/config.json; do
477+
echo "## ${file}"
478+
jq '(.auths[]?.auth) |= "REDACTED"' "$file"
479+
echo ""
480+
done

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -527,8 +527,8 @@ jobs:
527527
```
528528

529529
You can also use the `registry-auth` input for raw authentication to
530-
registries, defined as YAML objects. Each object can contain `registry`,
531-
`username`, `password` and `ecr` keys similar to current inputs:
530+
registries, defined as YAML objects. Each object have the same attributes as
531+
current inputs (except `logout`):
532532

533533
> [!WARNING]
534534
> We don't recommend using this method, it's better to use the action multiple
@@ -568,13 +568,13 @@ The following inputs can be used as `step.with` keys:
568568
| `registry` | String | `docker.io` | Server address of Docker registry. If not set then will default to Docker Hub |
569569
| `username` | String | | Username for authenticating to the Docker registry |
570570
| `password` | String | | Password or personal access token for authenticating the Docker registry |
571+
| `scope` | String | | Scope for the authentication token |
571572
| `ecr` | String | `auto` | Specifies whether the given registry is ECR (`auto`, `true` or `false`) |
572573
| `logout` | Bool | `true` | Log out from the Docker registry at the end of a job |
573574
| `registry-auth` | YAML | | Raw authentication to registries, defined as YAML objects |
574575

575576
> [!NOTE]
576-
> The `registry-auth` input is mutually exclusive with `registry`, `username`,
577-
> `password` and `ecr` inputs.
577+
> The `registry-auth` input cannot be used with other inputs except `logout`.
578578

579579
## Contributing
580580

__tests__/docker.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ test('logout calls exec', async () => {
5050

5151
const registry = 'https://ghcr.io';
5252

53-
await logout(registry);
53+
await logout(registry, '');
5454

5555
expect(execSpy).toHaveBeenCalledTimes(1);
5656
const callfunc = execSpy.mock.calls[0];

action.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ inputs:
1919
ecr:
2020
description: 'Specifies whether the given registry is ECR (auto, true or false)'
2121
required: false
22+
scope:
23+
description: 'Scope for the authentication token'
24+
required: false
2225
logout:
2326
description: 'Log out from the Docker registry at the end of a job'
2427
default: 'true'

src/context.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,90 @@
1+
import path from 'path';
12
import * as core from '@actions/core';
3+
import * as yaml from 'js-yaml';
4+
5+
import {Buildx} from '@docker/actions-toolkit/lib/buildx/buildx';
6+
import {Util} from '@docker/actions-toolkit/lib/util';
27

38
export interface Inputs {
49
registry: string;
510
username: string;
611
password: string;
12+
scope: string;
713
ecr: string;
814
logout: boolean;
915
registryAuth: string;
1016
}
1117

18+
export interface Auth {
19+
registry: string;
20+
username: string;
21+
password: string;
22+
scope: string;
23+
ecr: string;
24+
configDir: string;
25+
}
26+
1227
export function getInputs(): Inputs {
1328
return {
1429
registry: core.getInput('registry'),
1530
username: core.getInput('username'),
1631
password: core.getInput('password'),
32+
scope: core.getInput('scope'),
1733
ecr: core.getInput('ecr'),
1834
logout: core.getBooleanInput('logout'),
1935
registryAuth: core.getInput('registry-auth')
2036
};
2137
}
38+
39+
export function getAuthList(inputs: Inputs): Array<Auth> {
40+
if (inputs.registryAuth && (inputs.registry || inputs.username || inputs.password || inputs.scope || inputs.ecr)) {
41+
throw new Error('Cannot use registry-auth with other inputs');
42+
}
43+
let auths: Array<Auth> = [];
44+
if (!inputs.registryAuth) {
45+
auths.push({
46+
registry: inputs.registry || 'docker.io',
47+
username: inputs.username,
48+
password: inputs.password,
49+
scope: inputs.scope,
50+
ecr: inputs.ecr || 'auto',
51+
configDir: scopeToConfigDir(inputs.registry, inputs.scope)
52+
});
53+
} else {
54+
auths = (yaml.load(inputs.registryAuth) as Array<Auth>).map(auth => {
55+
core.setSecret(auth.password); // redacted in workflow logs
56+
return {
57+
registry: auth.registry || 'docker.io',
58+
username: auth.username,
59+
password: auth.password,
60+
scope: auth.scope,
61+
ecr: auth.ecr || 'auto',
62+
configDir: scopeToConfigDir(auth.registry || 'docker.io', auth.scope)
63+
};
64+
});
65+
}
66+
if (auths.length == 0) {
67+
throw new Error('No registry to login');
68+
}
69+
return auths;
70+
}
71+
72+
export function scopeToConfigDir(registry: string, scope?: string): string {
73+
if (scopeDisabled() || !scope || scope === '') {
74+
return '';
75+
}
76+
let configDir = path.join(Buildx.configDir, 'config', registry === 'docker.io' ? 'registry-1.docker.io' : registry);
77+
if (scope.startsWith('@')) {
78+
configDir += scope;
79+
} else {
80+
configDir = path.join(configDir, scope);
81+
}
82+
return configDir;
83+
}
84+
85+
function scopeDisabled(): boolean {
86+
if (process.env.DOCKER_LOGIN_SCOPE_DISABLED) {
87+
return Util.parseBool(process.env.DOCKER_LOGIN_SCOPE_DISABLED);
88+
}
89+
return false;
90+
}

src/docker.ts

Lines changed: 46 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,39 @@
1-
import * as aws from './aws';
21
import * as core from '@actions/core';
32

3+
import * as aws from './aws';
4+
import * as context from './context';
5+
46
import {Docker} from '@docker/actions-toolkit/lib/docker/docker';
57

6-
export async function login(registry: string, username: string, password: string, ecr: string): Promise<void> {
7-
if (/true/i.test(ecr) || (ecr == 'auto' && aws.isECR(registry))) {
8-
await loginECR(registry, username, password);
8+
export async function login(auth: context.Auth): Promise<void> {
9+
if (/true/i.test(auth.ecr) || (auth.ecr == 'auto' && aws.isECR(auth.registry))) {
10+
await loginECR(auth.registry, auth.username, auth.password, auth.scope);
911
} else {
10-
await loginStandard(registry, username, password);
12+
await loginStandard(auth.registry, auth.username, auth.password, auth.scope);
1113
}
1214
}
1315

14-
export async function logout(registry: string): Promise<void> {
16+
export async function logout(registry: string, configDir: string): Promise<void> {
17+
let envs: {[key: string]: string} | undefined;
18+
if (configDir !== '') {
19+
envs = Object.assign({}, process.env, {
20+
DOCKER_CONFIG: configDir
21+
}) as {
22+
[key: string]: string;
23+
};
24+
core.info(`Alternative config dir: ${configDir}`);
25+
}
1526
await Docker.getExecOutput(['logout', registry], {
16-
ignoreReturnCode: true
27+
ignoreReturnCode: true,
28+
env: envs
1729
}).then(res => {
1830
if (res.stderr.length > 0 && res.exitCode != 0) {
1931
core.warning(res.stderr.trim());
2032
}
2133
});
2234
}
2335

24-
export async function loginStandard(registry: string, username: string, password: string): Promise<void> {
36+
export async function loginStandard(registry: string, username: string, password: string, scope?: string): Promise<void> {
2537
if (!username && !password) {
2638
throw new Error('Username and password required');
2739
}
@@ -31,38 +43,39 @@ export async function loginStandard(registry: string, username: string, password
3143
if (!password) {
3244
throw new Error('Password required');
3345
}
46+
await loginExec(registry, username, password, scope);
47+
}
3448

35-
const loginArgs: Array<string> = ['login', '--password-stdin'];
36-
loginArgs.push('--username', username);
37-
loginArgs.push(registry);
49+
export async function loginECR(registry: string, username: string, password: string, scope?: string): Promise<void> {
50+
core.info(`Retrieving registries data through AWS SDK...`);
51+
const regDatas = await aws.getRegistriesData(registry, username, password);
52+
for (const regData of regDatas) {
53+
await loginExec(regData.registry, regData.username, regData.password, scope);
54+
}
55+
}
3856

39-
core.info(`Logging into ${registry}...`);
40-
await Docker.getExecOutput(loginArgs, {
57+
async function loginExec(registry: string, username: string, password: string, scope?: string): Promise<void> {
58+
let envs: {[key: string]: string} | undefined;
59+
const configDir = context.scopeToConfigDir(registry, scope);
60+
if (configDir !== '') {
61+
envs = Object.assign({}, process.env, {
62+
DOCKER_CONFIG: configDir
63+
}) as {
64+
[key: string]: string;
65+
};
66+
core.info(`Logging into ${registry} (scope ${scope})...`);
67+
} else {
68+
core.info(`Logging into ${registry}...`);
69+
}
70+
await Docker.getExecOutput(['login', '--password-stdin', '--username', username, registry], {
4171
ignoreReturnCode: true,
4272
silent: true,
43-
input: Buffer.from(password)
73+
input: Buffer.from(password),
74+
env: envs
4475
}).then(res => {
4576
if (res.stderr.length > 0 && res.exitCode != 0) {
4677
throw new Error(res.stderr.trim());
4778
}
48-
core.info(`Login Succeeded!`);
79+
core.info('Login Succeeded!');
4980
});
5081
}
51-
52-
export async function loginECR(registry: string, username: string, password: string): Promise<void> {
53-
core.info(`Retrieving registries data through AWS SDK...`);
54-
const regDatas = await aws.getRegistriesData(registry, username, password);
55-
for (const regData of regDatas) {
56-
core.info(`Logging into ${regData.registry}...`);
57-
await Docker.getExecOutput(['login', '--password-stdin', '--username', regData.username, regData.registry], {
58-
ignoreReturnCode: true,
59-
silent: true,
60-
input: Buffer.from(regData.password)
61-
}).then(res => {
62-
if (res.stderr.length > 0 && res.exitCode != 0) {
63-
throw new Error(res.stderr.trim());
64-
}
65-
core.info('Login Succeeded!');
66-
});
67-
}
68-
}

0 commit comments

Comments
 (0)