From 39158d804743082ae8b9af26b7e02bdb2d833fe9 Mon Sep 17 00:00:00 2001 From: CrazyMax Date: Mon, 13 Mar 2023 00:16:24 +0100 Subject: [PATCH] bake class to parse definitions and handle exporters type Signed-off-by: CrazyMax --- __tests__/buildx/bake.test.ts | 459 ++++++++++++++++++++++++++++++++++ __tests__/fixtures/bake.hcl | 172 +++++++++++++ src/buildx/bake.ts | 74 ++++++ src/types/bake.ts | 45 ++++ 4 files changed, 750 insertions(+) create mode 100644 __tests__/buildx/bake.test.ts create mode 100644 __tests__/fixtures/bake.hcl create mode 100644 src/buildx/bake.ts create mode 100644 src/types/bake.ts diff --git a/__tests__/buildx/bake.test.ts b/__tests__/buildx/bake.test.ts new file mode 100644 index 0000000..7135c23 --- /dev/null +++ b/__tests__/buildx/bake.test.ts @@ -0,0 +1,459 @@ +/** + * Copyright 2023 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 {beforeEach, describe, expect, jest, test} from '@jest/globals'; +import * as path from 'path'; + +import {Bake} from '../../src/buildx/bake'; +import {BakeDefinition} from '../../src/types/bake'; + +const fixturesDir = path.join(__dirname, '..', 'fixtures'); + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe('parseDefinitions', () => { + // prettier-ignore + test.each([ + [ + [path.join(fixturesDir, 'bake.hcl')], + ['validate'], + { + "group": { + "default": { + "targets": [ + "validate" + ] + }, + "validate": { + "targets": [ + "lint", + "validate-vendor", + "validate-docs" + ] + } + }, + "target": { + "lint": { + "context": ".", + "dockerfile": "./hack/dockerfiles/lint.Dockerfile", + "args": { + "BUILDKIT_CONTEXT_KEEP_GIT_DIR": "1", + "GO_VERSION": "1.20" + }, + "output": [ + "type=cacheonly" + ] + }, + "validate-docs": { + "context": ".", + "dockerfile": "./hack/dockerfiles/docs.Dockerfile", + "args": { + "BUILDKIT_CONTEXT_KEEP_GIT_DIR": "1", + "BUILDX_EXPERIMENTAL": "1", + "FORMATS": "md", + "GO_VERSION": "1.20" + }, + "target": "validate", + "output": [ + "type=cacheonly" + ] + }, + "validate-vendor": { + "context": ".", + "dockerfile": "./hack/dockerfiles/vendor.Dockerfile", + "args": { + "BUILDKIT_CONTEXT_KEEP_GIT_DIR": "1", + "GO_VERSION": "1.20" + }, + "target": "validate", + "output": [ + "type=cacheonly" + ] + } + } + } + ] + ])('given %p', async (files, targets, expected: BakeDefinition) => { + const bake = new Bake(); + expect(await bake.parseDefinitions(files, targets)).toEqual(expected); + }); +}); + +describe('hasLocalExporter', () => { + // prettier-ignore + test.each([ + [ + { + "target": { + "build": { + "output": [ + "type=docker" + ] + }, + } + }, + false + ], + [ + { + "target": { + "local": { + "output": [ + "type=local,dest=./release-out" + ] + }, + } + }, + true + ], + [ + { + "target": { + "tar": { + "output": [ + "type=tar,dest=/tmp/image.tar" + ] + }, + } + }, + false + ], + [ + { + "target": { + "tar": { + "output": [ + '"type=tar","dest=/tmp/image.tar"', + ] + }, + } + }, + false + ], + [ + { + "target": { + "local": { + "output": [ + '" type= local" , dest=./release-out', + ] + }, + } + }, + true + ], + [ + { + "target": { + "local": { + "output": [ + ".", + ] + }, + } + }, + true + ] + ])('given %o returns %p', async (def: BakeDefinition, expected: boolean) => { + expect(Bake.hasLocalExporter(def)).toEqual(expected); + }); +}); + +describe('hasTarExporter', () => { + // prettier-ignore + test.each([ + [ + { + "target": { + "reg": { + "output": [ + "type=registry,ref=user/app" + ] + }, + } + }, + false + ], + [ + { + "target": { + "build": { + "output": [ + "type=docker" + ] + }, + } + }, + false + ], + [ + { + "target": { + "local": { + "output": [ + "type=local,dest=./release-out" + ] + }, + } + }, + false + ], + [ + { + "target": { + "tar": { + "output": [ + "type=tar,dest=/tmp/image.tar" + ] + }, + } + }, + true + ], + [ + { + "target": { + "multi": { + "output": [ + "type=docker", + "type=tar,dest=/tmp/image.tar" + ] + }, + } + }, + true + ], + [ + { + "target": { + "tar": { + "output": [ + '"type=tar","dest=/tmp/image.tar"', + ] + }, + } + }, + true + ], + [ + { + "target": { + "local": { + "output": [ + '" type= local" , dest=./release-out', + ] + }, + } + }, + false + ], + [ + { + "target": { + "local": { + "output": [ + ".", + ] + }, + } + }, + false + ], + ])('given %o returns %p', async (def: BakeDefinition, expected: boolean) => { + expect(Bake.hasTarExporter(def)).toEqual(expected); + }); +}); + +describe('hasDockerExporter', () => { + // prettier-ignore + test.each([ + [ + { + "target": { + "reg": { + "output": [ + "type=registry,ref=user/app" + ] + }, + } + }, + false, + undefined + ], + [ + { + "target": { + "build": { + "output": [ + "type=docker" + ] + }, + } + }, + true, + undefined + ], + [ + { + "target": { + "multi": { + "output": [ + "type=docker", + "type=tar,dest=/tmp/image.tar" + ] + }, + } + }, + true, + undefined + ], + [ + { + "target": { + "local": { + "output": [ + '" type= local" , dest=./release-out' + ] + }, + } + }, + false, + undefined + ], + [ + { + "target": { + "local": { + "output": [ + "type=local,dest=./release-out" + ] + }, + } + }, + false, + undefined + ], + [ + { + "target": { + "tar": { + "output": [ + "type=tar,dest=/tmp/image.tar" + ] + }, + } + }, + false, + undefined + ], + [ + { + "target": { + "multi": { + "output": [ + "type=docker", + "type=tar,dest=/tmp/image.tar" + ] + }, + } + }, + true, + undefined + ], + [ + { + "target": { + "tar": { + "output": [ + '"type=tar","dest=/tmp/image.tar"' + ] + }, + } + }, + false, + undefined + ], + [ + { + "target": { + "tar": { + "output": [ + '"type=tar","dest=/tmp/image.tar"' + ] + }, + } + }, + false, + undefined + ], + [ + { + "target": { + "local": { + "output": [ + '" type= local" , dest=./release-out' + ] + }, + } + }, + false, + undefined + ], + [ + { + "target": { + "build": { + "output": [ + "type=docker" + ] + }, + } + }, + true, + false + ], + [ + { + "target": { + "build": { + "output": [ + "type=docker" + ] + }, + } + }, + true, + true + ], + [ + { + "target": { + "build": { + "output": [ + "." + ] + }, + } + }, + true, + true + ], + ])('given %o and load:%p returns %p', async (def: BakeDefinition, expected: boolean, load: boolean) => { + expect(Bake.hasDockerExporter(def, load)).toEqual(expected); + }); +}); diff --git a/__tests__/fixtures/bake.hcl b/__tests__/fixtures/bake.hcl new file mode 100644 index 0000000..47dce15 --- /dev/null +++ b/__tests__/fixtures/bake.hcl @@ -0,0 +1,172 @@ +// Copyright 2023 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. + +variable "GO_VERSION" { + default = "1.20" +} +variable "DOCS_FORMATS" { + default = "md" +} +variable "DESTDIR" { + default = "./bin" +} + +# Special target: https://github.com/docker/metadata-action#bake-definition +target "meta-helper" { + tags = ["docker/buildx-bin:local"] +} + +target "_common" { + args = { + GO_VERSION = GO_VERSION + BUILDKIT_CONTEXT_KEEP_GIT_DIR = 1 + } +} + +group "default" { + targets = ["binaries"] +} + +group "validate" { + targets = ["lint", "validate-vendor", "validate-docs"] +} + +target "lint" { + inherits = ["_common"] + dockerfile = "./hack/dockerfiles/lint.Dockerfile" + output = ["type=cacheonly"] +} + +target "validate-vendor" { + inherits = ["_common"] + dockerfile = "./hack/dockerfiles/vendor.Dockerfile" + target = "validate" + output = ["type=cacheonly"] +} + +target "validate-docs" { + inherits = ["_common"] + args = { + FORMATS = DOCS_FORMATS + BUILDX_EXPERIMENTAL = 1 // enables experimental cmds/flags for docs generation + } + dockerfile = "./hack/dockerfiles/docs.Dockerfile" + target = "validate" + output = ["type=cacheonly"] +} + +target "validate-authors" { + inherits = ["_common"] + dockerfile = "./hack/dockerfiles/authors.Dockerfile" + target = "validate" + output = ["type=cacheonly"] +} + +target "validate-generated-files" { + inherits = ["_common"] + dockerfile = "./hack/dockerfiles/generated-files.Dockerfile" + target = "validate" + output = ["type=cacheonly"] +} + +target "update-vendor" { + inherits = ["_common"] + dockerfile = "./hack/dockerfiles/vendor.Dockerfile" + target = "update" + output = ["."] +} + +target "update-docs" { + inherits = ["_common"] + args = { + FORMATS = DOCS_FORMATS + BUILDX_EXPERIMENTAL = 1 // enables experimental cmds/flags for docs generation + } + dockerfile = "./hack/dockerfiles/docs.Dockerfile" + target = "update" + output = ["./docs/reference"] +} + +target "update-authors" { + inherits = ["_common"] + dockerfile = "./hack/dockerfiles/authors.Dockerfile" + target = "update" + output = ["."] +} + +target "update-generated-files" { + inherits = ["_common"] + dockerfile = "./hack/dockerfiles/generated-files.Dockerfile" + target = "update" + output = ["."] +} + +target "mod-outdated" { + inherits = ["_common"] + dockerfile = "./hack/dockerfiles/vendor.Dockerfile" + target = "outdated" + no-cache-filter = ["outdated"] + output = ["type=cacheonly"] +} + +target "test" { + inherits = ["_common"] + target = "test-coverage" + output = ["${DESTDIR}/coverage"] +} + +target "binaries" { + inherits = ["_common"] + target = "binaries" + output = ["${DESTDIR}/build"] + platforms = ["local"] +} + +target "binaries-cross" { + inherits = ["binaries"] + platforms = [ + "darwin/amd64", + "darwin/arm64", + "linux/amd64", + "linux/arm/v6", + "linux/arm/v7", + "linux/arm64", + "linux/ppc64le", + "linux/riscv64", + "linux/s390x", + "windows/amd64", + "windows/arm64" + ] +} + +target "release" { + inherits = ["binaries-cross"] + target = "release" + output = ["${DESTDIR}/release"] +} + +target "image" { + inherits = ["meta-helper", "binaries"] + output = ["type=image"] +} + +target "image-cross" { + inherits = ["meta-helper", "binaries-cross"] + output = ["type=image"] +} + +target "image-local" { + inherits = ["image"] + output = ["type=docker"] +} diff --git a/src/buildx/bake.ts b/src/buildx/bake.ts new file mode 100644 index 0000000..7609a72 --- /dev/null +++ b/src/buildx/bake.ts @@ -0,0 +1,74 @@ +/** + * Copyright 2023 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 {Buildx} from './buildx'; +import {Exec} from '../exec'; +import {Inputs} from './inputs'; + +import {BakeDefinition} from '../types/bake'; + +export interface BakeOpts { + buildx?: Buildx; +} + +export class Bake { + private readonly buildx: Buildx; + + constructor(opts?: BakeOpts) { + this.buildx = opts?.buildx || new Buildx(); + } + + public async parseDefinitions(files: Array, targets: Array): Promise { + const args = ['bake']; + if (files) { + for (const file of files) { + args.push('--file', file); + } + } + + const printCmd = await this.buildx.getCommand([...args, '--print', ...targets]); + return await Exec.getExecOutput(printCmd.command, printCmd.args, { + ignoreReturnCode: true, + silent: true + }).then(res => { + if (res.stderr.length > 0 && res.exitCode != 0) { + throw new Error(res.stderr); + } + return JSON.parse(res.stdout.trim()); + }); + } + + public static hasLocalExporter(def: BakeDefinition): boolean { + return Inputs.hasExporterType('local', Bake.exporters(def)); + } + + public static hasTarExporter(def: BakeDefinition): boolean { + return Inputs.hasExporterType('tar', Bake.exporters(def)); + } + + public static hasDockerExporter(def: BakeDefinition, load?: boolean): boolean { + return load || Inputs.hasExporterType('docker', Bake.exporters(def)); + } + + private static exporters(def: BakeDefinition): Array { + const exporters = new Array(); + for (const key in def.target) { + const target = def.target[key]; + exporters.push(...target.output); + } + return exporters; + } +} diff --git a/src/types/bake.ts b/src/types/bake.ts new file mode 100644 index 0000000..57c09f9 --- /dev/null +++ b/src/types/bake.ts @@ -0,0 +1,45 @@ +/** + * Copyright 2023 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. + */ + +export interface BakeDefinition { + group: Record; + target: Record; +} + +export interface Group { + targets: Array; +} + +export interface Target { + args: Record; + attest: Array; + 'cache-from': Array; + 'cache-to': Array; + context: string; + contexts: Record; + dockerfile: string; + 'dockerfile-inline': string; + labels: Record; + 'no-cache': boolean; + 'no-cache-filter': Array; + output: Array; + platforms: Array; + pull: boolean; + secret: Array; + ssh: Array; + tags: Array; + target: string; +}