Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions __tests__/docker/docker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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
Expand Down
21 changes: 15 additions & 6 deletions src/docker/docker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,16 @@ export class Docker {
};
}

public static async pull(image: string, cache?: boolean): Promise<void> {
public static async pull(image: string, cache?: boolean, endpoint?: string): Promise<void> {
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, '--');
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -224,11 +233,11 @@ export class Docker {
}
}

private static async pullWithRetry(image: string): Promise<void> {
private static async pullWithRetry(image: string, args: string[]): Promise<void> {
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) {
Expand Down