diff --git a/__tests__/.fixtures/imagetools-06.json b/__tests__/.fixtures/imagetools-06.json new file mode 100644 index 00000000..2596cf83 --- /dev/null +++ b/__tests__/.fixtures/imagetools-06.json @@ -0,0 +1,15 @@ +[ + { + "mediaType":"application/vnd.oci.image.manifest.v1+json", + "digest":"sha256:2ba4ad6eae1efcafee73a971953093c7c32b6938f2f9fd4998c8bf4d0fbe76f2", + "size":1113, + "annotations":{ + "vnd.docker.reference.digest":"sha256:dccc69dd895968c4f21aa9e43e715f25f0cedfce4b17f1014c88c307928e22fc", + "vnd.docker.reference.type":"attestation-manifest" + }, + "platform":{ + "architecture":"unknown", + "os":"unknown" + } + } +] diff --git a/__tests__/.fixtures/imagetools-07.json b/__tests__/.fixtures/imagetools-07.json new file mode 100644 index 00000000..3ed0fce4 --- /dev/null +++ b/__tests__/.fixtures/imagetools-07.json @@ -0,0 +1,15 @@ +[ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:0709528fae1747ce17638ad2978ee7936b38a294136eaadaf692e415f64b1e03", + "size": 1113, + "annotations": { + "vnd.docker.reference.digest": "sha256:1b6bce668653f08e2d0f9f7c9b646675b2cbce94ce8abdf4eb0eabaef4353045", + "vnd.docker.reference.type": "attestation-manifest" + }, + "platform": { + "architecture": "unknown", + "os": "unknown" + } + } +] diff --git a/__tests__/buildx/imagetools.test.itg.ts b/__tests__/buildx/imagetools.test.itg.ts index b8ecc33d..da796f7b 100644 --- a/__tests__/buildx/imagetools.test.itg.ts +++ b/__tests__/buildx/imagetools.test.itg.ts @@ -60,6 +60,16 @@ maybe('attestationDescriptors', () => { const expectedAttestations = >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 expectedAttestations = >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 expectedAttestations = >JSON.parse(fs.readFileSync(path.join(fixturesDir, 'imagetools-07.json'), {encoding: 'utf-8'}).trim()); + expect(attestations).toEqual(expectedAttestations); + }); }); maybe('attestationDigests', () => { @@ -75,4 +85,12 @@ maybe('attestationDigests', () => { 'sha256:d95ca72d4f2a6bc416d4b2f3003b2af9d5f4dea99acec6ad3ab0c2082000a98c' ]); }); + it('returns buildkit attestations digests for linux/amd64', async () => { + const digests = await new ImageTools().attestationDigests('moby/buildkit:latest@sha256:79cc6476ab1a3371c9afd8b44e7c55610057c43e18d9b39b68e2b0c2475cc1b6', {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'}); + expect(digests).toEqual(['sha256:0709528fae1747ce17638ad2978ee7936b38a294136eaadaf692e415f64b1e03']); + }); }); diff --git a/src/buildx/imagetools.ts b/src/buildx/imagetools.ts index 6ea0f3c2..dfaac461 100644 --- a/src/buildx/imagetools.ts +++ b/src/buildx/imagetools.ts @@ -19,7 +19,7 @@ import {Exec} from '../exec'; import {Manifest as ImageToolsManifest} from '../types/buildx/imagetools'; import {Image} from '../types/oci/config'; -import {Descriptor} from '../types/oci/descriptor'; +import {Descriptor, Platform} from '../types/oci/descriptor'; import {Digest} from '../types/oci/digest'; export interface ImageToolsOpts { @@ -83,15 +83,39 @@ export class ImageTools { }); } - public async attestationDescriptors(name: string): Promise> { + public async attestationDescriptors(name: string, platform?: Platform): Promise> { const manifest = await this.inspectManifest(name); - if (typeof manifest === 'object' && manifest !== null && 'manifests' in manifest && Array.isArray(manifest.manifests)) { - return manifest.manifests.filter(m => m.annotations && m.annotations['vnd.docker.reference.type'] === 'attestation-manifest'); + + if (typeof manifest !== 'object' || manifest === null || !('manifests' in manifest) || !Array.isArray(manifest.manifests)) { + throw new Error(`No descriptor found for ${name}`); + } + + const attestations = manifest.manifests.filter(m => m.annotations?.['vnd.docker.reference.type'] === 'attestation-manifest'); + if (!platform) { + return attestations; + } + + const manifestByDigest = new Map(); + for (const m of manifest.manifests) { + if (m.digest) { + manifestByDigest.set(m.digest, m); + } } - throw new Error(`No attestation descriptors found for ${name}`); + + return attestations.filter(attestation => { + const refDigest = attestation.annotations?.['vnd.docker.reference.digest']; + if (!refDigest) { + return false; + } + const referencedManifest = manifestByDigest.get(refDigest); + if (!referencedManifest) { + return false; + } + return referencedManifest.platform?.os === platform.os && referencedManifest.platform?.architecture === platform.architecture && (referencedManifest.platform?.variant ?? '') === (platform.variant ?? ''); + }); } - public async attestationDigests(name: string): Promise> { - return (await this.attestationDescriptors(name)).map(attestation => attestation.digest); + public async attestationDigests(name: string, platform?: Platform): Promise> { + return (await this.attestationDescriptors(name, platform)).map(attestation => attestation.digest); } }