buildx(imagetools): make manifest retries configurable

Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
This commit is contained in:
CrazyMax
2026-03-13 13:18:20 +01:00
parent 4d926d8b7b
commit 21e2b75b0b
6 changed files with 213 additions and 43 deletions

View File

@@ -30,12 +30,12 @@ const maybe = !process.env.GITHUB_ACTIONS || (process.env.GITHUB_ACTIONS === 'tr
maybe('inspectImage', () => {
it('inspect single platform', async () => {
const image = await new ImageTools().inspectImage('moby/buildkit:latest@sha256:5769c54b98840147b74128f38fb0b0a049e24b11a75bd81664131edd2854593f');
const image = await new ImageTools().inspectImage({name: 'moby/buildkit:latest@sha256:5769c54b98840147b74128f38fb0b0a049e24b11a75bd81664131edd2854593f'});
const expectedImage = <Image>JSON.parse(fs.readFileSync(path.join(fixturesDir, 'imagetools-01.json'), {encoding: 'utf-8'}).trim());
expect(image).toEqual(expectedImage);
});
it('inspect multi platform', async () => {
const image = await new ImageTools().inspectImage('moby/buildkit:latest@sha256:86c0ad9d1137c186e9d455912167df20e530bdf7f7c19de802e892bb8ca16552');
const image = await new ImageTools().inspectImage({name: 'moby/buildkit:latest@sha256:86c0ad9d1137c186e9d455912167df20e530bdf7f7c19de802e892bb8ca16552'});
const expectedImage = <Record<string, Image>>JSON.parse(fs.readFileSync(path.join(fixturesDir, 'imagetools-02.json'), {encoding: 'utf-8'}).trim());
expect(image).toEqual(expectedImage);
});
@@ -43,12 +43,12 @@ maybe('inspectImage', () => {
maybe('inspectManifest', () => {
it('inspect descriptor', async () => {
const manifest = await new ImageTools().inspectManifest('moby/buildkit:latest@sha256:dccc69dd895968c4f21aa9e43e715f25f0cedfce4b17f1014c88c307928e22fc');
const manifest = await new ImageTools().inspectManifest({name: 'moby/buildkit:latest@sha256:dccc69dd895968c4f21aa9e43e715f25f0cedfce4b17f1014c88c307928e22fc'});
const expectedManifest = <Descriptor>JSON.parse(fs.readFileSync(path.join(fixturesDir, 'imagetools-03.json'), {encoding: 'utf-8'}).trim());
expect(manifest).toEqual(expectedManifest);
});
it('inspect index', async () => {
const manifest = await new ImageTools().inspectManifest('moby/buildkit:latest@sha256:79cc6476ab1a3371c9afd8b44e7c55610057c43e18d9b39b68e2b0c2475cc1b6');
const manifest = await new ImageTools().inspectManifest({name: 'moby/buildkit:latest@sha256:79cc6476ab1a3371c9afd8b44e7c55610057c43e18d9b39b68e2b0c2475cc1b6'});
const expectedManifest = <ImageToolsManifest>JSON.parse(fs.readFileSync(path.join(fixturesDir, 'imagetools-04.json'), {encoding: 'utf-8'}).trim());
expect(manifest).toEqual(expectedManifest);
});
@@ -56,17 +56,17 @@ maybe('inspectManifest', () => {
maybe('attestationDescriptors', () => {
it('returns buildkit attestations descriptors', async () => {
const attestations = await new ImageTools().attestationDescriptors('moby/buildkit:latest@sha256:79cc6476ab1a3371c9afd8b44e7c55610057c43e18d9b39b68e2b0c2475cc1b6');
const attestations = await new ImageTools().attestationDescriptors({name: 'moby/buildkit:latest@sha256:79cc6476ab1a3371c9afd8b44e7c55610057c43e18d9b39b68e2b0c2475cc1b6'});
const expectedAttestations = <Array<Descriptor>>JSON.parse(fs.readFileSync(path.join(fixturesDir, 'imagetools-05.json'), {encoding: 'utf-8'}).trim());
expect(attestations).toEqual(expectedAttestations);
});
it('returns buildkit attestations descriptors for linux/amd64', async () => {
const attestations = await new ImageTools().attestationDescriptors('moby/buildkit:latest@sha256:79cc6476ab1a3371c9afd8b44e7c55610057c43e18d9b39b68e2b0c2475cc1b6', {os: 'linux', architecture: 'amd64'});
const attestations = await new ImageTools().attestationDescriptors({name: 'moby/buildkit:latest@sha256:79cc6476ab1a3371c9afd8b44e7c55610057c43e18d9b39b68e2b0c2475cc1b6', platform: {os: 'linux', architecture: 'amd64'}});
const expectedAttestations = <Array<Descriptor>>JSON.parse(fs.readFileSync(path.join(fixturesDir, 'imagetools-06.json'), {encoding: 'utf-8'}).trim());
expect(attestations).toEqual(expectedAttestations);
});
it('returns buildkit attestations descriptors for linux/arm/v7', async () => {
const attestations = await new ImageTools().attestationDescriptors('moby/buildkit:latest@sha256:79cc6476ab1a3371c9afd8b44e7c55610057c43e18d9b39b68e2b0c2475cc1b6', {os: 'linux', architecture: 'arm', variant: 'v7'});
const attestations = await new ImageTools().attestationDescriptors({name: 'moby/buildkit:latest@sha256:79cc6476ab1a3371c9afd8b44e7c55610057c43e18d9b39b68e2b0c2475cc1b6', platform: {os: 'linux', architecture: 'arm', variant: 'v7'}});
const expectedAttestations = <Array<Descriptor>>JSON.parse(fs.readFileSync(path.join(fixturesDir, 'imagetools-07.json'), {encoding: 'utf-8'}).trim());
expect(attestations).toEqual(expectedAttestations);
});
@@ -74,7 +74,7 @@ maybe('attestationDescriptors', () => {
maybe('attestationDigests', () => {
it('returns buildkit attestations digests', async () => {
const digests = await new ImageTools().attestationDigests('moby/buildkit:latest@sha256:79cc6476ab1a3371c9afd8b44e7c55610057c43e18d9b39b68e2b0c2475cc1b6');
const digests = await new ImageTools().attestationDigests({name: 'moby/buildkit:latest@sha256:79cc6476ab1a3371c9afd8b44e7c55610057c43e18d9b39b68e2b0c2475cc1b6'});
// prettier-ignore
expect(digests).toEqual([
'sha256:2ba4ad6eae1efcafee73a971953093c7c32b6938f2f9fd4998c8bf4d0fbe76f2',
@@ -86,11 +86,11 @@ maybe('attestationDigests', () => {
]);
});
it('returns buildkit attestations digests for linux/amd64', async () => {
const digests = await new ImageTools().attestationDigests('moby/buildkit:latest@sha256:79cc6476ab1a3371c9afd8b44e7c55610057c43e18d9b39b68e2b0c2475cc1b6', {os: 'linux', architecture: 'amd64'});
const digests = await new ImageTools().attestationDigests({name: 'moby/buildkit:latest@sha256:79cc6476ab1a3371c9afd8b44e7c55610057c43e18d9b39b68e2b0c2475cc1b6', platform: {os: 'linux', architecture: 'amd64'}});
expect(digests).toEqual(['sha256:2ba4ad6eae1efcafee73a971953093c7c32b6938f2f9fd4998c8bf4d0fbe76f2']);
});
it('returns buildkit attestations digests for linux/arm/v7', async () => {
const digests = await new ImageTools().attestationDigests('moby/buildkit:latest@sha256:79cc6476ab1a3371c9afd8b44e7c55610057c43e18d9b39b68e2b0c2475cc1b6', {os: 'linux', architecture: 'arm', variant: 'v7'});
const digests = await new ImageTools().attestationDigests({name: 'moby/buildkit:latest@sha256:79cc6476ab1a3371c9afd8b44e7c55610057c43e18d9b39b68e2b0c2475cc1b6', platform: {os: 'linux', architecture: 'arm', variant: 'v7'}});
expect(digests).toEqual(['sha256:0709528fae1747ce17638ad2978ee7936b38a294136eaadaf692e415f64b1e03']);
});
});

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
import {afterEach, describe, expect, it, vi} from 'vitest';
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
import fs from 'fs';
import os from 'os';
import path from 'path';
@@ -38,10 +38,133 @@ vi.spyOn(Context, 'tmpName').mockImplementation((): string => {
});
afterEach(() => {
vi.useRealTimers();
vi.clearAllMocks();
rimraf.sync(tmpDir);
});
beforeEach(() => {
vi.useRealTimers();
fs.mkdirSync(tmpDir, {recursive: true});
});
describe('inspectManifest', () => {
it('retries transient manifest unknown errors when requested', async () => {
vi.useFakeTimers();
const getCommand = vi.fn().mockResolvedValue({
command: 'docker',
args: ['buildx', 'imagetools', 'inspect']
});
const buildx = {getCommand} as unknown as Buildx;
const execSpy = vi
.spyOn(Exec, 'getExecOutput')
.mockResolvedValueOnce({
exitCode: 1,
stdout: '',
stderr: 'ERROR: MANIFEST_UNKNOWN: manifest unknown'
})
.mockResolvedValueOnce({
exitCode: 0,
stdout: JSON.stringify({
schemaVersion: 2,
mediaType: 'application/vnd.oci.image.index.v1+json',
manifests: []
}),
stderr: ''
});
const inspectPromise = new ImageTools({buildx}).inspectManifest({
name: 'docker.io/library/alpine:latest',
retryOnManifestUnknown: true,
retryLimit: 2
});
await vi.runAllTimersAsync();
expect(await inspectPromise).toEqual({
schemaVersion: 2,
mediaType: 'application/vnd.oci.image.index.v1+json',
manifests: []
});
expect(getCommand).toHaveBeenCalledWith(['imagetools', 'inspect', 'docker.io/library/alpine:latest', '--format', '{{json .Manifest}}']);
expect(execSpy).toHaveBeenCalledTimes(2);
});
it('does not retry non-manifest errors', async () => {
const getCommand = vi.fn().mockResolvedValue({
command: 'docker',
args: ['buildx', 'imagetools', 'inspect']
});
const buildx = {getCommand} as unknown as Buildx;
const execSpy = vi.spyOn(Exec, 'getExecOutput').mockResolvedValue({
exitCode: 1,
stdout: '',
stderr: 'ERROR: unauthorized'
});
const result = await new ImageTools({buildx})
.inspectManifest({
name: 'docker.io/library/alpine:latest',
retryOnManifestUnknown: true
})
.then(
value => ({value, error: undefined}),
error => ({value: undefined, error: error as Error})
);
expect(result.value).toBeUndefined();
expect(result.error).toBeInstanceOf(Error);
expect(result.error?.message).toContain('ERROR: unauthorized');
expect(execSpy).toHaveBeenCalledTimes(1);
});
});
describe('inspectImage', () => {
it('retries transient manifest unknown errors when requested', async () => {
vi.useFakeTimers();
const getCommand = vi.fn().mockResolvedValue({
command: 'docker',
args: ['buildx', 'imagetools', 'inspect']
});
const buildx = {getCommand} as unknown as Buildx;
const execSpy = vi
.spyOn(Exec, 'getExecOutput')
.mockResolvedValueOnce({
exitCode: 1,
stdout: '',
stderr: 'ERROR: MANIFEST_UNKNOWN: manifest unknown'
})
.mockResolvedValueOnce({
exitCode: 0,
stdout: JSON.stringify({
config: {
digest: 'sha256:test'
}
}),
stderr: ''
});
const inspectPromise = new ImageTools({buildx}).inspectImage({
name: 'docker.io/library/alpine:latest',
retryOnManifestUnknown: true,
retryLimit: 2
});
await vi.runAllTimersAsync();
expect(await inspectPromise).toEqual({
config: {
digest: 'sha256:test'
}
});
expect(getCommand).toHaveBeenCalledWith(['imagetools', 'inspect', 'docker.io/library/alpine:latest', '--format', '{{json .Image}}']);
expect(execSpy).toHaveBeenCalledTimes(2);
});
});
describe('create', () => {
it('parses metadata and supports cwd sources', async () => {
const getCommand = vi.fn().mockResolvedValue({