From 8c2401723e6776f4a189e4aad3684238c783e55f Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Wed, 27 Nov 2024 15:29:51 +0100 Subject: [PATCH] bake: support composable attributes Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- __tests__/.fixtures/bake-01-overrides.json | 4 +- __tests__/.fixtures/bake-01-validate.json | 12 +- __tests__/.fixtures/bake-03-default.json | 38 ++++ __tests__/.fixtures/bake-03.hcl | 28 +++ .../bake-buildx-0.10.4-binaries-cross.json | 5 +- __tests__/buildx/bake.test.ts | 213 ++++++++---------- src/buildx/bake.ts | 188 +++++++++++++++- src/types/buildx/bake.ts | 31 ++- 8 files changed, 376 insertions(+), 143 deletions(-) create mode 100644 __tests__/.fixtures/bake-03-default.json create mode 100644 __tests__/.fixtures/bake-03.hcl diff --git a/__tests__/.fixtures/bake-01-overrides.json b/__tests__/.fixtures/bake-01-overrides.json index 0de12001..a0ef0cc6 100644 --- a/__tests__/.fixtures/bake-01-overrides.json +++ b/__tests__/.fixtures/bake-01-overrides.json @@ -22,7 +22,9 @@ "linux/amd64" ], "output": [ - "type=docker" + { + "type": "docker" + } ] } } diff --git a/__tests__/.fixtures/bake-01-validate.json b/__tests__/.fixtures/bake-01-validate.json index dcb927c4..d6806d1c 100644 --- a/__tests__/.fixtures/bake-01-validate.json +++ b/__tests__/.fixtures/bake-01-validate.json @@ -22,7 +22,9 @@ "GO_VERSION": "1.20" }, "output": [ - "type=cacheonly" + { + "type": "cacheonly" + } ] }, "validate-docs": { @@ -36,7 +38,9 @@ }, "target": "validate", "output": [ - "type=cacheonly" + { + "type": "cacheonly" + } ] }, "validate-vendor": { @@ -48,7 +52,9 @@ }, "target": "validate", "output": [ - "type=cacheonly" + { + "type": "cacheonly" + } ] } } diff --git a/__tests__/.fixtures/bake-03-default.json b/__tests__/.fixtures/bake-03-default.json new file mode 100644 index 00000000..00de99f2 --- /dev/null +++ b/__tests__/.fixtures/bake-03-default.json @@ -0,0 +1,38 @@ +{ + "target": { + "default": { + "context": ".", + "dockerfile": "Dockerfile", + "cache-from": [ + { + "scope": "build", + "type": "gha" + }, + { + "ref": "user/repo:cache", + "type": "registry" + } + ], + "cache-to": [ + { + "mode": "max", + "scope": "build", + "type": "gha" + }, + { + "type": "inline" + } + ], + "output": [ + { + "dest": "./release-out", + "type": "local" + }, + { + "ref": "user/app", + "type": "registry" + } + ] + } + } +} diff --git a/__tests__/.fixtures/bake-03.hcl b/__tests__/.fixtures/bake-03.hcl new file mode 100644 index 00000000..20140d59 --- /dev/null +++ b/__tests__/.fixtures/bake-03.hcl @@ -0,0 +1,28 @@ +// Copyright 2024 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. + +target "default" { + cache-from = [ + "type=gha,scope=build", + "user/repo:cache", + ] + cache-to = [ + "type=gha,scope=build,mode=max", + "type=inline" + ] + output = [ + "./release-out", + "type=registry,ref=user/app" + ] +} diff --git a/__tests__/.fixtures/bake-buildx-0.10.4-binaries-cross.json b/__tests__/.fixtures/bake-buildx-0.10.4-binaries-cross.json index 8ade295e..fd4b1486 100644 --- a/__tests__/.fixtures/bake-buildx-0.10.4-binaries-cross.json +++ b/__tests__/.fixtures/bake-buildx-0.10.4-binaries-cross.json @@ -29,7 +29,10 @@ "windows/arm64" ], "output": [ - "./bin/build" + { + "dest": "./bin/build", + "type": "local" + } ] } } diff --git a/__tests__/buildx/bake.test.ts b/__tests__/buildx/bake.test.ts index 1bb0ff10..022b6346 100644 --- a/__tests__/buildx/bake.test.ts +++ b/__tests__/buildx/bake.test.ts @@ -94,7 +94,14 @@ describe('getDefinition', () => { ['*.output=type=docker', '*.platform=linux/amd64'], undefined, path.join(fixturesDir, 'bake-01-overrides.json') - ] + ], + [ + [path.join(fixturesDir, 'bake-03.hcl')], + [], + [], + undefined, + path.join(fixturesDir, 'bake-03-default.json') + ], ])('given %p', async (files: string[], targets: string[], overrides: string[], execOptions: ExecOptions | undefined, out: string) => { const bake = new Bake(); const expectedDef = JSON.parse(fs.readFileSync(out, {encoding: 'utf-8'}).trim()) @@ -103,7 +110,7 @@ describe('getDefinition', () => { targets: targets, overrides: overrides }, execOptions)).toEqual(expectedDef); - }); + }, 30 * 60 * 1000); }); describe('hasLocalExporter', () => { @@ -114,7 +121,9 @@ describe('hasLocalExporter', () => { "target": { "build": { "output": [ - "type=docker" + { + "type": "docker" + } ] }, } @@ -136,7 +145,10 @@ describe('hasLocalExporter', () => { "target": { "local": { "output": [ - "type=local,dest=./release-out" + { + "type": "local", + "dest": "./release-out" + } ] }, } @@ -148,43 +160,25 @@ describe('hasLocalExporter', () => { "target": { "tar": { "output": [ - "type=tar,dest=/tmp/image.tar" + { + "type": "tar", + "dest": "/tmp/image.tar" + } ] }, } } as unknown as BakeDefinition, false ], - [ - { - "target": { - "tar": { - "output": [ - '"type=tar","dest=/tmp/image.tar"', - ] - }, - } - } as unknown as BakeDefinition, - false - ], - [ - { - "target": { - "local": { - "output": [ - '" type= local" , dest=./release-out', - ] - }, - } - } as unknown as BakeDefinition, - true - ], [ { "target": { "local": { "output": [ - ".", + { + "type": "local", + "dest": "." + } ] }, } @@ -204,7 +198,10 @@ describe('hasTarExporter', () => { "target": { "reg": { "output": [ - "type=registry,ref=user/app" + { + "type": "registry", + "ref": "user/app" + } ] }, } @@ -216,7 +213,9 @@ describe('hasTarExporter', () => { "target": { "build": { "output": [ - "type=docker" + { + "type": "docker" + } ] }, } @@ -228,7 +227,10 @@ describe('hasTarExporter', () => { "target": { "local": { "output": [ - "type=local,dest=./release-out" + { + "type": "local", + "dest": "./release-out" + } ] }, } @@ -240,7 +242,10 @@ describe('hasTarExporter', () => { "target": { "tar": { "output": [ - "type=tar,dest=/tmp/image.tar" + { + "type": "tar", + "dest": "/tmp/image.tar" + } ] }, } @@ -252,44 +257,28 @@ describe('hasTarExporter', () => { "target": { "multi": { "output": [ - "type=docker", - "type=tar,dest=/tmp/image.tar" + { + "type": "docker" + }, + { + "type": "tar", + "dest": "/tmp/image.tar" + } ] }, } } as unknown as BakeDefinition, true ], - [ - { - "target": { - "tar": { - "output": [ - '"type=tar","dest=/tmp/image.tar"', - ] - }, - } - } as unknown as BakeDefinition, - true - ], - [ - { - "target": { - "local": { - "output": [ - '" type= local" , dest=./release-out', - ] - }, - } - } as unknown as BakeDefinition, - false - ], [ { "target": { "local": { "output": [ - ".", + { + "type": "local", + "dest": "." + } ] }, } @@ -309,7 +298,10 @@ describe('hasDockerExporter', () => { "target": { "reg": { "output": [ - "type=registry,ref=user/app" + { + "type": "registry", + "ref": "user/app" + } ] }, } @@ -322,7 +314,9 @@ describe('hasDockerExporter', () => { "target": { "build": { "output": [ - "type=docker" + { + "type": "docker" + } ] }, } @@ -335,8 +329,13 @@ describe('hasDockerExporter', () => { "target": { "multi": { "output": [ - "type=docker", - "type=tar,dest=/tmp/image.tar" + { + "type": "docker" + }, + { + "type": "tar", + "dest": "/tmp/image.tar" + } ] }, } @@ -349,20 +348,10 @@ describe('hasDockerExporter', () => { "target": { "local": { "output": [ - '" type= local" , dest=./release-out' - ] - }, - } - } as unknown as BakeDefinition, - false, - undefined - ], - [ - { - "target": { - "local": { - "output": [ - "type=local,dest=./release-out" + { + "type": "local", + "dest": "./release-out" + } ] }, } @@ -375,7 +364,10 @@ describe('hasDockerExporter', () => { "target": { "tar": { "output": [ - "type=tar,dest=/tmp/image.tar" + { + "type": "tar", + "dest": "/tmp/image.tar" + } ] }, } @@ -388,8 +380,13 @@ describe('hasDockerExporter', () => { "target": { "multi": { "output": [ - "type=docker", - "type=tar,dest=/tmp/image.tar" + { + "type": "docker" + }, + { + "type": "tar", + "dest": "/tmp/image.tar" + } ] }, } @@ -397,51 +394,14 @@ describe('hasDockerExporter', () => { true, undefined ], - [ - { - "target": { - "tar": { - "output": [ - '"type=tar","dest=/tmp/image.tar"' - ] - }, - } - } as unknown as BakeDefinition, - false, - undefined - ], - [ - { - "target": { - "tar": { - "output": [ - '"type=tar","dest=/tmp/image.tar"' - ] - }, - } - } as unknown as BakeDefinition, - false, - undefined - ], - [ - { - "target": { - "local": { - "output": [ - '" type= local" , dest=./release-out' - ] - }, - } - } as unknown as BakeDefinition, - false, - undefined - ], [ { "target": { "build": { "output": [ - "type=docker" + { + "type": "docker" + } ] }, } @@ -454,7 +414,9 @@ describe('hasDockerExporter', () => { "target": { "build": { "output": [ - "type=docker" + { + "type": "docker" + } ] }, } @@ -467,7 +429,10 @@ describe('hasDockerExporter', () => { "target": { "build": { "output": [ - "." + { + "type": "local", + "dest": "." + } ] }, } diff --git a/src/buildx/bake.ts b/src/buildx/bake.ts index 590d3510..34a9ae45 100644 --- a/src/buildx/bake.ts +++ b/src/buildx/bake.ts @@ -16,15 +16,15 @@ import fs from 'fs'; import path from 'path'; +import {parse} from 'csv-parse/sync'; -import {Build} from './build'; import {Buildx} from './buildx'; import {Context} from '../context'; import {Exec} from '../exec'; import {Util} from '../util'; import {ExecOptions} from '@actions/exec'; -import {BakeDefinition} from '../types/buildx/bake'; +import {BakeDefinition, CacheOptionsEntry, ExportEntry, SecretEntry, SSHEntry} from '../types/buildx/bake'; import {BuildMetadata} from '../types/buildx/build'; import {VertexWarning} from '../types/buildkit/client'; @@ -178,27 +178,197 @@ export class Bake { } public static parseDefinition(dt: string): BakeDefinition { - return JSON.parse(dt); + const definition = JSON.parse(dt); + + // convert to composable attributes: https://github.com/docker/buildx/pull/2758 + for (const name in definition.target) { + const target = definition.target[name]; + if (target['cache-from'] && Array.isArray(target['cache-from'])) { + target['cache-from'] = target['cache-from'].map((item: string | CacheOptionsEntry): CacheOptionsEntry => { + return Bake.parseCacheOptionsEntry(item); + }); + } + if (target['cache-to'] && Array.isArray(target['cache-to'])) { + target['cache-to'] = target['cache-to'].map((item: string | CacheOptionsEntry): CacheOptionsEntry => { + return Bake.parseCacheOptionsEntry(item); + }); + } + if (target['output'] && Array.isArray(target['output'])) { + target['output'] = target['output'].map((item: string | ExportEntry): ExportEntry => { + return Bake.parseExportEntry(item); + }); + } + if (target['secret'] && Array.isArray(target['secret'])) { + target['secret'] = target['secret'].map((item: string | SecretEntry): SecretEntry => { + return Bake.parseSecretEntry(item); + }); + } + if (target['ssh'] && Array.isArray(target['ssh'])) { + target['ssh'] = target['ssh'].map((item: string | SSHEntry): SSHEntry => { + return Bake.parseSSHEntry(item); + }); + } + } + + return definition; + } + + private static parseCacheOptionsEntry(item: CacheOptionsEntry | string): CacheOptionsEntry { + if (typeof item !== 'string') { + return item; + } + + const cacheOptionsEntry: CacheOptionsEntry = {type: ''}; + const fields = parse(item, { + relaxColumnCount: true, + skipEmptyLines: true + })[0]; + + if (fields.length === 1 && !fields[0].includes('=')) { + cacheOptionsEntry.type = 'registry'; + cacheOptionsEntry.ref = fields[0]; + return cacheOptionsEntry; + } + + for (const field of fields) { + const [key, value] = field + .toString() + .split(/(?<=^[^=]+?)=/) + .map((item: string) => item.trim()); + switch (key) { + case 'type': + cacheOptionsEntry.type = value; + break; + default: + cacheOptionsEntry[key] = value; + } + } + + return cacheOptionsEntry; + } + + private static parseExportEntry(item: ExportEntry | string): ExportEntry { + if (typeof item !== 'string') { + return item; + } + + const exportEntry: ExportEntry = {type: ''}; + const fields = parse(item, { + relaxColumnCount: true, + skipEmptyLines: true + })[0]; + + if (fields.length === 1 && fields[0] === item && !item.startsWith('type=')) { + if (item !== '-') { + exportEntry.type = 'local'; + exportEntry.dest = item; + return exportEntry; + } + exportEntry.type = 'tar'; + exportEntry.dest = item; + return exportEntry; + } + + for (const field of fields) { + const [key, value] = field + .toString() + .split(/(?<=^[^=]+?)=/) + .map((item: string) => item.trim()); + switch (key) { + case 'type': + exportEntry.type = value; + break; + default: + exportEntry[key] = value; + } + } + + return exportEntry; + } + + private static parseSecretEntry(item: SecretEntry | string): SecretEntry { + if (typeof item !== 'string') { + return item; + } + + const secretEntry: SecretEntry = {}; + const fields = parse(item, { + relaxColumnCount: true, + skipEmptyLines: true + })[0]; + + let typ = ''; + for (const field of fields) { + const [key, value] = field + .toString() + .split(/(?<=^[^=]+?)=/) + .map((item: string) => item.trim()); + switch (key) { + case 'type': + typ = value; + break; + case 'id': + secretEntry.id = value; + break; + case 'source': + case 'src': + secretEntry.src = value; + break; + case 'env': + break; + } + } + if (typ === 'env' && !secretEntry.env) { + secretEntry.env = secretEntry.src; + secretEntry.src = undefined; + } + return secretEntry; + } + + private static parseSSHEntry(item: SSHEntry | string): SSHEntry { + if (typeof item !== 'string') { + return item; + } + + const sshEntry: SSHEntry = {}; + const [key, value] = item.split('=', 2); + sshEntry.id = key; + if (value) { + sshEntry.paths = value.split(','); + } + + return sshEntry; } public static hasLocalExporter(def: BakeDefinition): boolean { - return Build.hasExporterType('local', Bake.exporters(def)); + return Bake.hasExporterType('local', Bake.exporters(def)); } public static hasTarExporter(def: BakeDefinition): boolean { - return Build.hasExporterType('tar', Bake.exporters(def)); + return Bake.hasExporterType('tar', Bake.exporters(def)); } public static hasDockerExporter(def: BakeDefinition, load?: boolean): boolean { - return load || Build.hasExporterType('docker', Bake.exporters(def)); + return load || Bake.hasExporterType('docker', Bake.exporters(def)); + } + + public static hasExporterType(name: string, exporters: Array): boolean { + for (const exporter of exporters) { + if (exporter.type == name) { + return true; + } + } + return false; } - private static exporters(def: BakeDefinition): Array { - const exporters = new Array(); + private static exporters(def: BakeDefinition): Array { + const exporters = new Array(); for (const key in def.target) { const target = def.target[key]; if (target.output) { - exporters.push(...target.output); + for (const output of target.output) { + exporters.push(Bake.parseExportEntry(output)); + } } } return exporters; diff --git a/src/types/buildx/bake.ts b/src/types/buildx/bake.ts index 8230715c..dcd8aa70 100644 --- a/src/types/buildx/bake.ts +++ b/src/types/buildx/bake.ts @@ -28,8 +28,8 @@ export interface Target { description?: string; args?: Record; attest?: Array; - 'cache-from'?: Array; - 'cache-to'?: Array; + 'cache-from'?: Array | Array; + 'cache-to'?: Array | Array; call?: string; context: string; contexts?: Record; @@ -39,13 +39,34 @@ export interface Target { labels?: Record; 'no-cache'?: boolean; 'no-cache-filter'?: Array; - output?: Array; + output?: Array | Array; platforms?: Array; pull?: boolean; - secret?: Array; + secret?: Array | Array; 'shm-size'?: string; - ssh?: Array; + ssh?: Array | Array; tags?: Array; target?: string; ulimits?: Array; } + +export interface CacheOptionsEntry { + type: string; + [key: string]: string; +} + +export interface ExportEntry { + type: string; + [key: string]: string; +} + +export interface SecretEntry { + id?: string; + src?: string; + env?: string; +} + +export interface SSHEntry { + id?: string; + paths?: Array; +}