bake: support composable attributes

Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
This commit is contained in:
CrazyMax
2024-11-27 15:29:51 +01:00
parent 2e6acb85f0
commit 068b0dcb39
8 changed files with 376 additions and 143 deletions

View File

@@ -22,7 +22,9 @@
"linux/amd64" "linux/amd64"
], ],
"output": [ "output": [
"type=docker" {
"type": "docker"
}
] ]
} }
} }

View File

@@ -22,7 +22,9 @@
"GO_VERSION": "1.20" "GO_VERSION": "1.20"
}, },
"output": [ "output": [
"type=cacheonly" {
"type": "cacheonly"
}
] ]
}, },
"validate-docs": { "validate-docs": {
@@ -36,7 +38,9 @@
}, },
"target": "validate", "target": "validate",
"output": [ "output": [
"type=cacheonly" {
"type": "cacheonly"
}
] ]
}, },
"validate-vendor": { "validate-vendor": {
@@ -48,7 +52,9 @@
}, },
"target": "validate", "target": "validate",
"output": [ "output": [
"type=cacheonly" {
"type": "cacheonly"
}
] ]
} }
} }

View File

@@ -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"
}
]
}
}
}

View File

@@ -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"
]
}

View File

@@ -29,7 +29,10 @@
"windows/arm64" "windows/arm64"
], ],
"output": [ "output": [
"./bin/build" {
"dest": "./bin/build",
"type": "local"
}
] ]
} }
} }

View File

@@ -94,7 +94,14 @@ describe('getDefinition', () => {
['*.output=type=docker', '*.platform=linux/amd64'], ['*.output=type=docker', '*.platform=linux/amd64'],
undefined, undefined,
path.join(fixturesDir, 'bake-01-overrides.json') 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) => { ])('given %p', async (files: string[], targets: string[], overrides: string[], execOptions: ExecOptions | undefined, out: string) => {
const bake = new Bake(); const bake = new Bake();
const expectedDef = <BakeDefinition>JSON.parse(fs.readFileSync(out, {encoding: 'utf-8'}).trim()) const expectedDef = <BakeDefinition>JSON.parse(fs.readFileSync(out, {encoding: 'utf-8'}).trim())
@@ -103,7 +110,7 @@ describe('getDefinition', () => {
targets: targets, targets: targets,
overrides: overrides overrides: overrides
}, execOptions)).toEqual(expectedDef); }, execOptions)).toEqual(expectedDef);
}); }, 30 * 60 * 1000);
}); });
describe('hasLocalExporter', () => { describe('hasLocalExporter', () => {
@@ -114,7 +121,9 @@ describe('hasLocalExporter', () => {
"target": { "target": {
"build": { "build": {
"output": [ "output": [
"type=docker" {
"type": "docker"
}
] ]
}, },
} }
@@ -136,7 +145,10 @@ describe('hasLocalExporter', () => {
"target": { "target": {
"local": { "local": {
"output": [ "output": [
"type=local,dest=./release-out" {
"type": "local",
"dest": "./release-out"
}
] ]
}, },
} }
@@ -148,19 +160,10 @@ describe('hasLocalExporter', () => {
"target": { "target": {
"tar": { "tar": {
"output": [ "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"',
] ]
}, },
} }
@@ -172,19 +175,10 @@ describe('hasLocalExporter', () => {
"target": { "target": {
"local": { "local": {
"output": [ "output": [
'" type= local" , dest=./release-out', {
] "type": "local",
}, "dest": "."
} }
} as unknown as BakeDefinition,
true
],
[
{
"target": {
"local": {
"output": [
".",
] ]
}, },
} }
@@ -204,7 +198,10 @@ describe('hasTarExporter', () => {
"target": { "target": {
"reg": { "reg": {
"output": [ "output": [
"type=registry,ref=user/app" {
"type": "registry",
"ref": "user/app"
}
] ]
}, },
} }
@@ -216,7 +213,9 @@ describe('hasTarExporter', () => {
"target": { "target": {
"build": { "build": {
"output": [ "output": [
"type=docker" {
"type": "docker"
}
] ]
}, },
} }
@@ -228,7 +227,10 @@ describe('hasTarExporter', () => {
"target": { "target": {
"local": { "local": {
"output": [ "output": [
"type=local,dest=./release-out" {
"type": "local",
"dest": "./release-out"
}
] ]
}, },
} }
@@ -240,7 +242,10 @@ describe('hasTarExporter', () => {
"target": { "target": {
"tar": { "tar": {
"output": [ "output": [
"type=tar,dest=/tmp/image.tar" {
"type": "tar",
"dest": "/tmp/image.tar"
}
] ]
}, },
} }
@@ -252,20 +257,13 @@ describe('hasTarExporter', () => {
"target": { "target": {
"multi": { "multi": {
"output": [ "output": [
"type=docker", {
"type=tar,dest=/tmp/image.tar" "type": "docker"
] },
}, {
} "type": "tar",
} as unknown as BakeDefinition, "dest": "/tmp/image.tar"
true }
],
[
{
"target": {
"tar": {
"output": [
'"type=tar","dest=/tmp/image.tar"',
] ]
}, },
} }
@@ -277,19 +275,10 @@ describe('hasTarExporter', () => {
"target": { "target": {
"local": { "local": {
"output": [ "output": [
'" type= local" , dest=./release-out', {
] "type": "local",
}, "dest": "."
} }
} as unknown as BakeDefinition,
false
],
[
{
"target": {
"local": {
"output": [
".",
] ]
}, },
} }
@@ -309,7 +298,10 @@ describe('hasDockerExporter', () => {
"target": { "target": {
"reg": { "reg": {
"output": [ "output": [
"type=registry,ref=user/app" {
"type": "registry",
"ref": "user/app"
}
] ]
}, },
} }
@@ -322,7 +314,9 @@ describe('hasDockerExporter', () => {
"target": { "target": {
"build": { "build": {
"output": [ "output": [
"type=docker" {
"type": "docker"
}
] ]
}, },
} }
@@ -335,8 +329,13 @@ describe('hasDockerExporter', () => {
"target": { "target": {
"multi": { "multi": {
"output": [ "output": [
"type=docker", {
"type=tar,dest=/tmp/image.tar" "type": "docker"
},
{
"type": "tar",
"dest": "/tmp/image.tar"
}
] ]
}, },
} }
@@ -349,20 +348,10 @@ describe('hasDockerExporter', () => {
"target": { "target": {
"local": { "local": {
"output": [ "output": [
'" type= local" , dest=./release-out' {
] "type": "local",
}, "dest": "./release-out"
} }
} as unknown as BakeDefinition,
false,
undefined
],
[
{
"target": {
"local": {
"output": [
"type=local,dest=./release-out"
] ]
}, },
} }
@@ -375,7 +364,10 @@ describe('hasDockerExporter', () => {
"target": { "target": {
"tar": { "tar": {
"output": [ "output": [
"type=tar,dest=/tmp/image.tar" {
"type": "tar",
"dest": "/tmp/image.tar"
}
] ]
}, },
} }
@@ -388,8 +380,13 @@ describe('hasDockerExporter', () => {
"target": { "target": {
"multi": { "multi": {
"output": [ "output": [
"type=docker", {
"type=tar,dest=/tmp/image.tar" "type": "docker"
},
{
"type": "tar",
"dest": "/tmp/image.tar"
}
] ]
}, },
} }
@@ -397,51 +394,14 @@ describe('hasDockerExporter', () => {
true, true,
undefined 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": { "target": {
"build": { "build": {
"output": [ "output": [
"type=docker" {
"type": "docker"
}
] ]
}, },
} }
@@ -454,7 +414,9 @@ describe('hasDockerExporter', () => {
"target": { "target": {
"build": { "build": {
"output": [ "output": [
"type=docker" {
"type": "docker"
}
] ]
}, },
} }
@@ -467,7 +429,10 @@ describe('hasDockerExporter', () => {
"target": { "target": {
"build": { "build": {
"output": [ "output": [
"." {
"type": "local",
"dest": "."
}
] ]
}, },
} }

View File

@@ -16,15 +16,15 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import {parse} from 'csv-parse/sync';
import {Build} from './build';
import {Buildx} from './buildx'; import {Buildx} from './buildx';
import {Context} from '../context'; import {Context} from '../context';
import {Exec} from '../exec'; import {Exec} from '../exec';
import {Util} from '../util'; import {Util} from '../util';
import {ExecOptions} from '@actions/exec'; import {ExecOptions} from '@actions/exec';
import {BakeDefinition} from '../types/buildx/bake'; import {BakeDefinition, CacheEntry, ExportEntry, SecretEntry, SSHEntry} from '../types/buildx/bake';
import {BuildMetadata} from '../types/buildx/build'; import {BuildMetadata} from '../types/buildx/build';
import {VertexWarning} from '../types/buildkit/client'; import {VertexWarning} from '../types/buildkit/client';
@@ -178,27 +178,197 @@ export class Bake {
} }
public static parseDefinition(dt: string): BakeDefinition { public static parseDefinition(dt: string): BakeDefinition {
return <BakeDefinition>JSON.parse(dt); const definition = <BakeDefinition>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 | CacheEntry): CacheEntry => {
return Bake.parseCacheEntry(item);
});
}
if (target['cache-to'] && Array.isArray(target['cache-to'])) {
target['cache-to'] = target['cache-to'].map((item: string | CacheEntry): CacheEntry => {
return Bake.parseCacheEntry(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 parseCacheEntry(item: CacheEntry | string): CacheEntry {
if (typeof item !== 'string') {
return item;
}
const cacheEntry: CacheEntry = {type: ''};
const fields = parse(item, {
relaxColumnCount: true,
skipEmptyLines: true
})[0];
if (fields.length === 1 && !fields[0].includes('=')) {
cacheEntry.type = 'registry';
cacheEntry.ref = fields[0];
return cacheEntry;
}
for (const field of fields) {
const [key, value] = field
.toString()
.split(/(?<=^[^=]+?)=/)
.map((item: string) => item.trim());
switch (key) {
case 'type':
cacheEntry.type = value;
break;
default:
cacheEntry[key] = value;
}
}
return cacheEntry;
}
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 { 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 { 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 { public static hasDockerExporter(def: BakeDefinition, load?: boolean): boolean {
return load || Build.hasExporterType('docker', Bake.exporters(def)); return load || Bake.hasExporterType('docker', Bake.exporters(def));
} }
private static exporters(def: BakeDefinition): Array<string> { public static hasExporterType(name: string, exporters: Array<ExportEntry>): boolean {
const exporters = new Array<string>(); for (const exporter of exporters) {
if (exporter.type == name) {
return true;
}
}
return false;
}
private static exporters(def: BakeDefinition): Array<ExportEntry> {
const exporters = new Array<ExportEntry>();
for (const key in def.target) { for (const key in def.target) {
const target = def.target[key]; const target = def.target[key];
if (target.output) { if (target.output) {
exporters.push(...target.output); for (const output of target.output) {
exporters.push(Bake.parseExportEntry(output));
}
} }
} }
return exporters; return exporters;

View File

@@ -28,8 +28,8 @@ export interface Target {
description?: string; description?: string;
args?: Record<string, string>; args?: Record<string, string>;
attest?: Array<string>; attest?: Array<string>;
'cache-from'?: Array<string>; 'cache-from'?: Array<CacheEntry> | Array<string>;
'cache-to'?: Array<string>; 'cache-to'?: Array<CacheEntry> | Array<string>;
call?: string; call?: string;
context: string; context: string;
contexts?: Record<string, string>; contexts?: Record<string, string>;
@@ -39,13 +39,34 @@ export interface Target {
labels?: Record<string, string>; labels?: Record<string, string>;
'no-cache'?: boolean; 'no-cache'?: boolean;
'no-cache-filter'?: Array<string>; 'no-cache-filter'?: Array<string>;
output?: Array<string>; output?: Array<ExportEntry> | Array<string>;
platforms?: Array<string>; platforms?: Array<string>;
pull?: boolean; pull?: boolean;
secret?: Array<string>; secret?: Array<SecretEntry> | Array<string>;
'shm-size'?: string; 'shm-size'?: string;
ssh?: Array<string>; ssh?: Array<SSHEntry> | Array<string>;
tags?: Array<string>; tags?: Array<string>;
target?: string; target?: string;
ulimits?: Array<string>; ulimits?: Array<string>;
} }
export interface CacheEntry {
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<string>;
}