diff --git a/__tests__/docker/docker.test.ts b/__tests__/docker/docker.test.ts index 0de37ba6..68cd9ce4 100644 --- a/__tests__/docker/docker.test.ts +++ b/__tests__/docker/docker.test.ts @@ -25,6 +25,7 @@ import * as rimraf from 'rimraf'; import {mockHomedir} from '../.helpers/os.js'; import {Docker} from '../../src/docker/docker.js'; +import {Cache} from '../../src/cache.js'; import {ConfigFile} from '../../src/types/docker/docker.js'; @@ -198,6 +199,62 @@ describe('pull', () => { vi.useRealTimers(); }); + it.each([ + {endpoint: undefined, args: []}, + {endpoint: '', args: []}, + {endpoint: 'remote', args: ['--context', 'remote']}, + {endpoint: 'default', args: ['--context', 'default']}, + {endpoint: 'tcp://socket-proxy:2375', args: ['--host', 'tcp://socket-proxy:2375']}, + {endpoint: 'ssh://user@remote', args: ['--host', 'ssh://user@remote']}, + {endpoint: 'unix:///var/run/docker.sock', args: ['--host', 'unix:///var/run/docker.sock']} + ])('uses endpoint $endpoint for cached images and retries', async ({endpoint, args}) => { + vi.useFakeTimers(); + const originalEnv = {...process.env}; + const cachePath = path.join(tmpDir, 'image.tar'); + vi.spyOn(Cache.prototype, 'find').mockResolvedValue(cachePath); + const saveSpy = vi.spyOn(Cache.prototype, 'save').mockResolvedValue(cachePath); + let pulls = 0; + const execSpy = vi.spyOn(Docker, 'getExecOutput').mockImplementation(async command => { + if (command?.[0] === 'context') { + return args[0] === '--context' ? execOutput(0, JSON.stringify([{Name: endpoint}]), '') : execOutput(1, '', 'context not found'); + } + if (command?.includes('pull') && pulls++ === 0) { + return execOutput(1, '', '503 Service Unavailable'); + } + return execOutput(0, '', ''); + }); + + const pull = Docker.pull('moby/buildkit:buildx-stable-1', true, endpoint); + await vi.runAllTimersAsync(); + await pull; + + expect(execSpy.mock.calls.map(call => call[0])).toEqual([ + ...(endpoint ? [['context', 'inspect', '--format=json', endpoint]] : []), + [...args, 'load', '-i', cachePath], + [...args, 'pull', 'moby/buildkit:buildx-stable-1'], + [...args, 'pull', 'moby/buildkit:buildx-stable-1'], + [...args, 'save', '-o', expect.any(String), 'moby/buildkit:buildx-stable-1'] + ]); + expect(saveSpy).toHaveBeenCalledExactlyOnceWith(execSpy.mock.calls.at(-1)?.[0]?.at(-2)); + expect(process.env).toEqual(originalEnv); + }); + + it('pulls from the endpoint without caching', async () => { + const findSpy = vi.spyOn(Cache.prototype, 'find'); + const execSpy = vi + .spyOn(Docker, 'getExecOutput') + .mockResolvedValueOnce(execOutput(1, '', 'context not found')) + .mockResolvedValue(execOutput(0, '', '')); + + await Docker.pull('moby/buildkit:buildx-stable-1', false, 'tcp://socket-proxy:2375'); + + expect(execSpy.mock.calls.map(call => call[0])).toEqual([ + ['context', 'inspect', '--format=json', 'tcp://socket-proxy:2375'], + ['--host', 'tcp://socket-proxy:2375', 'pull', 'moby/buildkit:buildx-stable-1'] + ]); + expect(findSpy).not.toHaveBeenCalled(); + }); + it('retries transient registry errors', async () => { vi.useFakeTimers(); const execSpy = vi diff --git a/src/docker/docker.ts b/src/docker/docker.ts index c7c6a26b..c28ddd0d 100644 --- a/src/docker/docker.ts +++ b/src/docker/docker.ts @@ -170,7 +170,16 @@ export class Docker { }; } - public static async pull(image: string, cache?: boolean): Promise { + public static async pull(image: string, cache?: boolean, endpoint?: string): Promise { + const args: string[] = []; + if (endpoint) { + try { + await Docker.contextInspect(endpoint); + args.push('--context', endpoint); + } catch { + args.push('--host', endpoint); + } + } const parsedImage = Docker.parseRepoTag(image); const repoSanitized = parsedImage.repository.replace(/[^a-zA-Z0-9.]+/g, '--'); const tagSanitized = parsedImage.tag.replace(/[^a-zA-Z0-9.]+/g, '--'); @@ -187,7 +196,7 @@ export class Docker { cacheFoundPath = await imageCache.find(); if (cacheFoundPath) { core.info(`Image found from cache in ${cacheFoundPath}`); - await Docker.getExecOutput(['load', '-i', cacheFoundPath], { + await Docker.getExecOutput([...args, 'load', '-i', cacheFoundPath], { ignoreReturnCode: true }).then(res => { if (res.stderr.length > 0 && res.exitCode != 0) { @@ -199,7 +208,7 @@ export class Docker { let pulled = true; try { - await Docker.pullWithRetry(image); + await Docker.pullWithRetry(image, args); } catch (e) { pulled = false; if (cacheFoundPath) { @@ -211,7 +220,7 @@ export class Docker { if (cache && pulled) { const imageTarPath = path.join(Context.tmpDir(), `${Util.hash(image)}.tar`); - await Docker.getExecOutput(['save', '-o', imageTarPath, image], { + await Docker.getExecOutput([...args, 'save', '-o', imageTarPath, image], { ignoreReturnCode: true }).then(async res => { if (res.stderr.length > 0 && res.exitCode != 0) { @@ -224,11 +233,11 @@ export class Docker { } } - private static async pullWithRetry(image: string): Promise { + private static async pullWithRetry(image: string, args: string[]): Promise { const retries = 5; await retry( async bail => { - const res = await Docker.getExecOutput(['pull', image], { + const res = await Docker.getExecOutput([...args, 'pull', image], { ignoreReturnCode: true }); if (res.stderr.length > 0 && res.exitCode != 0) {