From 437b1870cc8884dc531f29e9f33b9b0d3124d45d Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Tue, 28 Oct 2025 11:29:48 +0100 Subject: [PATCH] buildx(imagetools): return attestations digests Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- __tests__/.fixtures/imagetools-03.json | 5 + __tests__/.fixtures/imagetools-04.json | 141 ++++++++++++++++++++++++ __tests__/.fixtures/imagetools-05.json | 80 ++++++++++++++ __tests__/buildx/imagetools.test.itg.ts | 39 +++++++ src/buildx/imagetools.ts | 36 ++++++ src/types/buildx/imagetools.ts | 28 +++++ 6 files changed, 329 insertions(+) create mode 100644 __tests__/.fixtures/imagetools-03.json create mode 100644 __tests__/.fixtures/imagetools-04.json create mode 100644 __tests__/.fixtures/imagetools-05.json create mode 100644 src/types/buildx/imagetools.ts diff --git a/__tests__/.fixtures/imagetools-03.json b/__tests__/.fixtures/imagetools-03.json new file mode 100644 index 0000000..13dd268 --- /dev/null +++ b/__tests__/.fixtures/imagetools-03.json @@ -0,0 +1,5 @@ +{ + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:dccc69dd895968c4f21aa9e43e715f25f0cedfce4b17f1014c88c307928e22fc", + "size": 1599 +} diff --git a/__tests__/.fixtures/imagetools-04.json b/__tests__/.fixtures/imagetools-04.json new file mode 100644 index 0000000..4c9c5ae --- /dev/null +++ b/__tests__/.fixtures/imagetools-04.json @@ -0,0 +1,141 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "digest": "sha256:79cc6476ab1a3371c9afd8b44e7c55610057c43e18d9b39b68e2b0c2475cc1b6", + "size": 4654, + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:dccc69dd895968c4f21aa9e43e715f25f0cedfce4b17f1014c88c307928e22fc", + "size": 1599, + "platform": { + "architecture": "amd64", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:1b6bce668653f08e2d0f9f7c9b646675b2cbce94ce8abdf4eb0eabaef4353045", + "size": 1599, + "platform": { + "architecture": "arm", + "os": "linux", + "variant": "v7" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:8f251fda6057e9dffc54f7874b249920f15f1813e9b1406a0cebeca5e4ab1ad9", + "size": 1599, + "platform": { + "architecture": "arm64", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:d306cbc2d506547f136c8e0ea040b929743f298fb2813d9030efdb9d9eee4d51", + "size": 1599, + "platform": { + "architecture": "s390x", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:9d195ff2dc9ef347bb52ebb1c2a6e6587d4bd87019d2ea11df3e7046a3d19708", + "size": 1599, + "platform": { + "architecture": "ppc64le", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:72410c2c4529fca9339ebbcc8db2a1d5cb4d72d72c669f50b6d45d8a0f79fc22", + "size": 1599, + "platform": { + "architecture": "riscv64", + "os": "linux" + } + }, + { + "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" + } + }, + { + "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" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:241b7159129d53923c89708bcc052b3398086a826519896be2f025545916e43e", + "size": 1113, + "annotations": { + "vnd.docker.reference.digest": "sha256:8f251fda6057e9dffc54f7874b249920f15f1813e9b1406a0cebeca5e4ab1ad9", + "vnd.docker.reference.type": "attestation-manifest" + }, + "platform": { + "architecture": "unknown", + "os": "unknown" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:97f4a222a7992dba6dc1a43991d0cca1fcffdc25593033c6a3a7ff14c8651cbf", + "size": 1113, + "annotations": { + "vnd.docker.reference.digest": "sha256:d306cbc2d506547f136c8e0ea040b929743f298fb2813d9030efdb9d9eee4d51", + "vnd.docker.reference.type": "attestation-manifest" + }, + "platform": { + "architecture": "unknown", + "os": "unknown" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:aa933713d8094b2708120e889acb6f7153dee4e0f3298ccd3e37a584cd0c260d", + "size": 1113, + "annotations": { + "vnd.docker.reference.digest": "sha256:9d195ff2dc9ef347bb52ebb1c2a6e6587d4bd87019d2ea11df3e7046a3d19708", + "vnd.docker.reference.type": "attestation-manifest" + }, + "platform": { + "architecture": "unknown", + "os": "unknown" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:d95ca72d4f2a6bc416d4b2f3003b2af9d5f4dea99acec6ad3ab0c2082000a98c", + "size": 1113, + "annotations": { + "vnd.docker.reference.digest": "sha256:72410c2c4529fca9339ebbcc8db2a1d5cb4d72d72c669f50b6d45d8a0f79fc22", + "vnd.docker.reference.type": "attestation-manifest" + }, + "platform": { + "architecture": "unknown", + "os": "unknown" + } + } + ] +} diff --git a/__tests__/.fixtures/imagetools-05.json b/__tests__/.fixtures/imagetools-05.json new file mode 100644 index 0000000..6ff1a22 --- /dev/null +++ b/__tests__/.fixtures/imagetools-05.json @@ -0,0 +1,80 @@ +[ + { + "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" + } + }, + { + "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" + } + }, + { + "mediaType":"application/vnd.oci.image.manifest.v1+json", + "digest":"sha256:241b7159129d53923c89708bcc052b3398086a826519896be2f025545916e43e", + "size":1113, + "annotations":{ + "vnd.docker.reference.digest":"sha256:8f251fda6057e9dffc54f7874b249920f15f1813e9b1406a0cebeca5e4ab1ad9", + "vnd.docker.reference.type":"attestation-manifest" + }, + "platform":{ + "architecture":"unknown", + "os":"unknown" + } + }, + { + "mediaType":"application/vnd.oci.image.manifest.v1+json", + "digest":"sha256:97f4a222a7992dba6dc1a43991d0cca1fcffdc25593033c6a3a7ff14c8651cbf", + "size":1113, + "annotations":{ + "vnd.docker.reference.digest":"sha256:d306cbc2d506547f136c8e0ea040b929743f298fb2813d9030efdb9d9eee4d51", + "vnd.docker.reference.type":"attestation-manifest" + }, + "platform":{ + "architecture":"unknown", + "os":"unknown" + } + }, + { + "mediaType":"application/vnd.oci.image.manifest.v1+json", + "digest":"sha256:aa933713d8094b2708120e889acb6f7153dee4e0f3298ccd3e37a584cd0c260d", + "size":1113, + "annotations":{ + "vnd.docker.reference.digest":"sha256:9d195ff2dc9ef347bb52ebb1c2a6e6587d4bd87019d2ea11df3e7046a3d19708", + "vnd.docker.reference.type":"attestation-manifest" + }, + "platform":{ + "architecture":"unknown", + "os":"unknown" + } + }, + { + "mediaType":"application/vnd.oci.image.manifest.v1+json", + "digest":"sha256:d95ca72d4f2a6bc416d4b2f3003b2af9d5f4dea99acec6ad3ab0c2082000a98c", + "size":1113, + "annotations":{ + "vnd.docker.reference.digest":"sha256:72410c2c4529fca9339ebbcc8db2a1d5cb4d72d72c669f50b6d45d8a0f79fc22", + "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 556a9c3..b8ecc33 100644 --- a/__tests__/buildx/imagetools.test.itg.ts +++ b/__tests__/buildx/imagetools.test.itg.ts @@ -19,7 +19,10 @@ import * as fs from 'fs'; import * as path from 'path'; import {ImageTools} from '../../src/buildx/imagetools'; + +import {Manifest as ImageToolsManifest} from '../../src/types/buildx/imagetools'; import {Image} from '../../src/types/oci/config'; +import {Descriptor} from '../../src/types/oci/descriptor'; const fixturesDir = path.join(__dirname, '..', '.fixtures'); @@ -37,3 +40,39 @@ maybe('inspectImage', () => { expect(image).toEqual(expectedImage); }); }); + +maybe('inspectManifest', () => { + it('inspect descriptor', async () => { + const manifest = await new ImageTools().inspectManifest('moby/buildkit:latest@sha256:dccc69dd895968c4f21aa9e43e715f25f0cedfce4b17f1014c88c307928e22fc'); + const expectedManifest = 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 expectedManifest = JSON.parse(fs.readFileSync(path.join(fixturesDir, 'imagetools-04.json'), {encoding: 'utf-8'}).trim()); + expect(manifest).toEqual(expectedManifest); + }); +}); + +maybe('attestationDescriptors', () => { + it('returns buildkit attestations descriptors', async () => { + const attestations = await new ImageTools().attestationDescriptors('moby/buildkit:latest@sha256:79cc6476ab1a3371c9afd8b44e7c55610057c43e18d9b39b68e2b0c2475cc1b6'); + const expectedAttestations = >JSON.parse(fs.readFileSync(path.join(fixturesDir, 'imagetools-05.json'), {encoding: 'utf-8'}).trim()); + expect(attestations).toEqual(expectedAttestations); + }); +}); + +maybe('attestationDigests', () => { + it('returns buildkit attestations digests', async () => { + const digests = await new ImageTools().attestationDigests('moby/buildkit:latest@sha256:79cc6476ab1a3371c9afd8b44e7c55610057c43e18d9b39b68e2b0c2475cc1b6'); + // prettier-ignore + expect(digests).toEqual([ + 'sha256:2ba4ad6eae1efcafee73a971953093c7c32b6938f2f9fd4998c8bf4d0fbe76f2', + 'sha256:0709528fae1747ce17638ad2978ee7936b38a294136eaadaf692e415f64b1e03', + 'sha256:241b7159129d53923c89708bcc052b3398086a826519896be2f025545916e43e', + 'sha256:97f4a222a7992dba6dc1a43991d0cca1fcffdc25593033c6a3a7ff14c8651cbf', + 'sha256:aa933713d8094b2708120e889acb6f7153dee4e0f3298ccd3e37a584cd0c260d', + 'sha256:d95ca72d4f2a6bc416d4b2f3003b2af9d5f4dea99acec6ad3ab0c2082000a98c' + ]); + }); +}); diff --git a/src/buildx/imagetools.ts b/src/buildx/imagetools.ts index 3763960..6ea0f3c 100644 --- a/src/buildx/imagetools.ts +++ b/src/buildx/imagetools.ts @@ -17,7 +17,10 @@ import {Buildx} from './buildx'; 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 {Digest} from '../types/oci/digest'; export interface ImageToolsOpts { buildx?: Buildx; @@ -58,4 +61,37 @@ export class ImageTools { throw new Error('Unexpected output format'); }); } + + public async inspectManifest(name: string): Promise { + const cmd = await this.getInspectCommand([name, '--format', '{{json .Manifest}}']); + return await Exec.getExecOutput(cmd.command, cmd.args, { + ignoreReturnCode: true, + silent: true + }).then(res => { + if (res.stderr.length > 0 && res.exitCode != 0) { + throw new Error(res.stderr.trim()); + } + const parsedOutput = JSON.parse(res.stdout); + if (typeof parsedOutput === 'object' && !Array.isArray(parsedOutput) && parsedOutput !== null) { + if (Object.prototype.hasOwnProperty.call(parsedOutput, 'manifests')) { + return parsedOutput; + } else { + return parsedOutput; + } + } + throw new Error('Unexpected output format'); + }); + } + + public async attestationDescriptors(name: string): 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'); + } + throw new Error(`No attestation descriptors found for ${name}`); + } + + public async attestationDigests(name: string): Promise> { + return (await this.attestationDescriptors(name)).map(attestation => attestation.digest); + } } diff --git a/src/types/buildx/imagetools.ts b/src/types/buildx/imagetools.ts new file mode 100644 index 0000000..4594c7f --- /dev/null +++ b/src/types/buildx/imagetools.ts @@ -0,0 +1,28 @@ +/** + * Copyright 2025 actions-toolkit authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Versioned} from '../oci/versioned'; +import {Descriptor} from '../oci/descriptor'; +import {Digest} from '../oci/digest'; + +// https://github.com/docker/buildx/blob/62857022a08552bee5cad0c3044a9a3b185f0b32/util/imagetools/printers.go#L109-L123 +export interface Manifest extends Versioned { + mediaType?: string; + digest: Digest; + size: number; + manifests?: Descriptor[]; + annotations?: Record; +}