From 4e12eabf92b725e8549da676c08ee8b3fe250641 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Mon, 5 Oct 2026 13:58:27 +0200 Subject: [PATCH] use registry hosts for scoped credentials Parse registry URLs so scoped ECR credentials use the same config directory for login, logout, and Buildx lookup. Preserve explicit default ports. Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- __tests__/context.test.ts | 22 +++++++++++++++++++++- __tests__/docker.test.ts | 32 +++++++++++++++++++++++++++++++- src/context.ts | 5 ++++- 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/__tests__/context.test.ts b/__tests__/context.test.ts index e9b5207..6c03020 100644 --- a/__tests__/context.test.ts +++ b/__tests__/context.test.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import {Buildx} from '@docker/actions-toolkit/lib/buildx/buildx.js'; -import {getAuthList, getInputs} from '../src/context.js'; +import {getAuthList, getInputs, scopeToConfigDir} from '../src/context.js'; afterEach(() => { vi.restoreAllMocks(); @@ -188,3 +188,23 @@ test.each([ }); expect(auth.ecr).toBe(expected); }); + +// prettier-ignore +test.each([ + ['ghcr.io', 'ghcr.io'], + ['ghcr.io/', 'ghcr.io'], + ['https://ghcr.io', 'ghcr.io'], + ['https://ghcr.io/', 'ghcr.io'], + ['https://ghcr.io/v2/', 'ghcr.io'], + ['http://localhost:5000', 'localhost:5000'], + ['http://localhost:80', 'localhost:80'], + ['https://ghcr.io:443', 'ghcr.io:443'], + ['https://012345678910.dkr.ecr.eu-west-3.amazonaws.com', '012345678910.dkr.ecr.eu-west-3.amazonaws.com'], + ['https://docker.io', 'registry-1.docker.io'] +])('scopeToConfigDir uses the registry host for %s', (registry, host) => { + expect(scopeToConfigDir(registry, 'myorg/myimage@push')).toBe(path.join(Buildx.configDir, 'config', host, 'myorg', 'myimage') + '@push'); +}); + +test('scopeToConfigDir rejects a URL whose host escapes the config directory', () => { + expect(() => scopeToConfigDir('https://../', '@push')).toThrow(/Invalid registry/); +}); diff --git a/__tests__/docker.test.ts b/__tests__/docker.test.ts index 4ad2d90..2a41d4b 100644 --- a/__tests__/docker.test.ts +++ b/__tests__/docker.test.ts @@ -1,8 +1,12 @@ import {afterEach, expect, test, vi} from 'vitest'; +import * as path from 'path'; +import {Buildx} from '@docker/actions-toolkit/lib/buildx/buildx.js'; import {Docker} from '@docker/actions-toolkit/lib/docker/docker.js'; -import {login, loginStandard, logout} from '../src/docker.js'; +import * as aws from '../src/aws.js'; +import {getAuthList} from '../src/context.js'; +import {login, loginECR, loginStandard, logout} from '../src/docker.js'; import * as dockerhub from '../src/dockerhub.js'; afterEach(() => { @@ -92,3 +96,29 @@ test('logout calls exec', async () => { ignoreReturnCode: true }); }); + +test('loginECR and logout use the config dir of the registry host', async () => { + const execSpy = vi.spyOn(Docker, 'getExecOutput').mockResolvedValue({ + exitCode: 0, + stdout: '', + stderr: '' + }); + const registry = '012345678910.dkr.ecr.eu-west-3.amazonaws.com'; + vi.spyOn(aws, 'getRegistriesData').mockResolvedValue([ + { + registry: `https://${registry}`, + username: 'AWS', + password: 'world' + } + ]); + + const scope = 'myapp@push'; + const [auth] = getAuthList({registry, username: 'CAFEBABE', password: 'DEADBEEF', scope, ecr: 'auto', logout: true, registryAuth: ''}); + await loginECR(auth.registry, auth.username, auth.password, auth.scope); + await logout(auth.registry, auth.configDir); + + const expectedConfigDir = path.join(Buildx.configDir, 'config', registry, 'myapp@push'); + expect(execSpy).toHaveBeenCalledTimes(2); + expect(execSpy.mock.calls[0][1]?.env?.DOCKER_CONFIG).toBe(expectedConfigDir); + expect(execSpy.mock.calls[1][1]?.env?.DOCKER_CONFIG).toBe(expectedConfigDir); +}); diff --git a/src/context.ts b/src/context.ts index f1f0d8a..56362a9 100644 --- a/src/context.ts +++ b/src/context.ts @@ -77,8 +77,11 @@ export function scopeToConfigDir(registry: string, scope?: string): string { if (scopeDisabled() || !scope || scope === '') { return ''; } + const schemeEnd = registry.indexOf('://'); + // A neutral scheme preserves explicit ports, including HTTP(S) default ports. + const registryHost = schemeEnd === -1 ? registry : new URL(`registry://${registry.slice(schemeEnd + 3)}`).host; const configRoot = path.resolve(Buildx.configDir, 'config'); - const registryDir = path.resolve(configRoot, registry === 'docker.io' ? 'registry-1.docker.io' : registry); + const registryDir = path.resolve(configRoot, registryHost === 'docker.io' ? 'registry-1.docker.io' : registryHost); if (!isChildPath(configRoot, registryDir)) { throw new Error(`Invalid registry '${registry}': resolved config path escapes the Buildx config directory`); }