From 74f883a069d52594ed7213574f6abbb13ab433e4 Mon Sep 17 00:00:00 2001 From: CrazyMax Date: Wed, 11 Jan 2023 12:10:34 +0100 Subject: [PATCH] check BuildKit compatibility before setting default provenance opts Signed-off-by: CrazyMax --- .github/workflows/ci.yml | 37 ++++++++++-- src/buildx.ts | 123 ++++++++++++++++++++++++++++++++++++++- src/context.ts | 16 ++--- src/main.ts | 2 +- 4 files changed, 165 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b8de236..d2c90f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -491,7 +491,39 @@ jobs: cache-from: type=gha,scope=nocachefilter cache-to: type=gha,scope=nocachefilter,mode=max - attests: + attests-compat: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - buildx: latest + buildkit: moby/buildkit:buildx-stable-1 + - buildx: latest + buildkit: moby/buildkit:v0.10.6 + - buildx: v0.9.1 + buildkit: moby/buildkit:buildx-stable-1 + steps: + - + name: Checkout + uses: actions/checkout@v3 + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + with: + version: ${{ matrix.buildx }} + driver-opts: | + network=host + image=${{ matrix.buildkit }} + - + name: Build + uses: ./ + with: + context: ./test/go + file: ./test/go/Dockerfile + outputs: type=cacheonly + + sbom: runs-on: ubuntu-latest strategy: fail-fast: false @@ -506,9 +538,6 @@ jobs: image: registry:2 ports: - 5000:5000 - env: - BUILDX_VERSION: v0.10.0-rc2 # TODO: remove when Buildx v0.10.0 is released - BUILDKIT_IMAGE: moby/buildkit:v0.11.0-rc3 # TODO: remove when BuildKit v0.11.0 is released steps: - name: Checkout diff --git a/src/buildx.ts b/src/buildx.ts index 4835ce6..4f503a5 100644 --- a/src/buildx.ts +++ b/src/buildx.ts @@ -3,9 +3,24 @@ import fs from 'fs'; import path from 'path'; import * as semver from 'semver'; import * as exec from '@actions/exec'; - import * as context from './context'; +export type Builder = { + name?: string; + driver?: string; + nodes: Node[]; +}; + +export type Node = { + name?: string; + endpoint?: string; + 'driver-opts'?: Array; + status?: string; + 'buildkitd-flags'?: string; + buildkit?: string; + platforms?: string; +}; + export async function getImageIDFile(): Promise { return path.join(context.tmpDir(), 'iidfile').split(path.sep).join(path.posix.sep); } @@ -126,6 +141,112 @@ export async function isAvailable(standalone?: boolean): Promise { }); } +export async function satisfiesBuildKitVersion(builderName: string, range: string, standalone?: boolean): Promise { + const builderInspect = await inspect(builderName, standalone); + for (const node of builderInspect.nodes) { + if (!node.buildkit) { + return false; + } + // BuildKit version reported by moby is in the format of `v0.11.0-moby` + if (builderInspect.driver == 'docker' && !node.buildkit.endsWith('-moby')) { + return false; + } + const version = node.buildkit.replace(/-moby$/, ''); + if (!semver.satisfies(version, range)) { + return false; + } + } + return true; +} + +async function inspect(name: string, standalone?: boolean): Promise { + const cmd = getCommand(['inspect', name], standalone); + 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()); + } + return parseInspect(res.stdout); + }); +} + +async function parseInspect(data: string): Promise { + const builder: Builder = { + nodes: [] + }; + let node: Node = {}; + for (const line of data.trim().split(`\n`)) { + const [key, ...rest] = line.split(':'); + const value = rest.map(v => v.trim()).join(':'); + if (key.length == 0 || value.length == 0) { + continue; + } + switch (key.toLowerCase()) { + case 'name': { + if (builder.name == undefined) { + builder.name = value; + } else { + if (Object.keys(node).length > 0) { + builder.nodes.push(node); + node = {}; + } + node.name = value; + } + break; + } + case 'driver': { + builder.driver = value; + break; + } + case 'endpoint': { + node.endpoint = value; + break; + } + case 'driver options': { + node['driver-opts'] = (value.match(/(\w+)="([^"]*)"/g) || []).map(v => v.replace(/^(.*)="(.*)"$/g, '$1=$2')); + break; + } + case 'status': { + node.status = value; + break; + } + case 'flags': { + node['buildkitd-flags'] = value; + break; + } + case 'buildkit': { + node.buildkit = value; + break; + } + case 'platforms': { + let platforms: Array = []; + // if a preferred platform is being set then use only these + // https://docs.docker.com/engine/reference/commandline/buildx_inspect/#get-information-about-a-builder-instance + if (value.includes('*')) { + for (const platform of value.split(', ')) { + if (platform.includes('*')) { + platforms.push(platform.replace('*', '')); + } + } + } else { + // otherwise set all platforms available + platforms = value.split(', '); + } + node.platforms = platforms.join(','); + break; + } + } + } + if (Object.keys(node).length > 0) { + builder.nodes.push(node); + } + return builder; +} + export async function getVersion(standalone?: boolean): Promise { const cmd = getCommand(['version'], standalone); return await exec diff --git a/src/context.ts b/src/context.ts index 0405a02..9f9576a 100644 --- a/src/context.ts +++ b/src/context.ts @@ -103,17 +103,17 @@ export async function getInputs(defaultContext: string): Promise { }; } -export async function getArgs(inputs: Inputs, defaultContext: string, buildxVersion: string): Promise> { +export async function getArgs(inputs: Inputs, defaultContext: string, buildxVersion: string, standalone?: boolean): Promise> { const context = handlebars.compile(inputs.context)({defaultContext}); // prettier-ignore return [ - ...await getBuildArgs(inputs, defaultContext, context, buildxVersion), + ...await getBuildArgs(inputs, defaultContext, context, buildxVersion, standalone), ...await getCommonArgs(inputs, buildxVersion), context ]; } -async function getBuildArgs(inputs: Inputs, defaultContext: string, context: string, buildxVersion: string): Promise> { +async function getBuildArgs(inputs: Inputs, defaultContext: string, context: string, buildxVersion: string, standalone?: boolean): Promise> { const args: Array = ['build']; await asyncForEach(inputs.addHosts, async addHost => { args.push('--add-host', addHost); @@ -164,10 +164,12 @@ async function getBuildArgs(inputs: Inputs, defaultContext: string, context: str if (buildx.satisfies(buildxVersion, '>=0.10.0')) { if (inputs.provenance) { args.push('--provenance', inputs.provenance); - } else if (fromPayload('repository.private') !== false) { - args.push('--provenance', `mode=min,inline-only=true`); - } else { - args.push('--provenance', `mode=max,builder-id=${process.env.GITHUB_SERVER_URL || 'https://github.com'}/${github.context.repo.owner}/${github.context.repo.repo}/actions/runs/${github.context.runId}`); + } else if (await buildx.satisfiesBuildKitVersion(inputs.builder, '>=0.11.0', standalone)) { + if (fromPayload('repository.private') !== false) { + args.push('--provenance', `mode=min,inline-only=true`); + } else { + args.push('--provenance', `mode=max,builder-id=${process.env.GITHUB_SERVER_URL || 'https://github.com'}/${github.context.repo.owner}/${github.context.repo.repo}/actions/runs/${github.context.runId}`); + } } if (inputs.sbom) { args.push('--sbom', inputs.sbom); diff --git a/src/main.ts b/src/main.ts index 8d164fd..60b4c76 100644 --- a/src/main.ts +++ b/src/main.ts @@ -41,7 +41,7 @@ async function run(): Promise { }); }); - const args: string[] = await context.getArgs(inputs, defContext, buildxVersion); + const args: string[] = await context.getArgs(inputs, defContext, buildxVersion, standalone); const buildCmd = buildx.getCommand(args, standalone); await exec .getExecOutput(buildCmd.command, buildCmd.args, {