diff --git a/__tests__/buildx/bake.test.ts b/__tests__/buildx/bake.test.ts index c421412..1b7cc85 100644 --- a/__tests__/buildx/bake.test.ts +++ b/__tests__/buildx/bake.test.ts @@ -14,21 +14,67 @@ * limitations under the License. */ -import {beforeEach, describe, expect, jest, test} from '@jest/globals'; +import {afterEach, beforeEach, describe, expect, it, jest, test} from '@jest/globals'; import * as fs from 'fs'; import * as path from 'path'; +import * as rimraf from 'rimraf'; import {Bake} from '../../src/buildx/bake'; +import {Context} from '../../src/context'; import {ExecOptions} from '@actions/exec'; -import {BakeDefinition} from '../../src/types/bake'; +import {BakeDefinition, BakeMetadata} from '../../src/types/bake'; const fixturesDir = path.join(__dirname, '..', 'fixtures'); +// prettier-ignore +const tmpDir = path.join(process.env.TEMP || '/tmp', 'buildx-inputs-jest'); +const tmpName = path.join(tmpDir, '.tmpname-jest'); +const metadata: BakeMetadata = { + app: { + 'buildx.build.ref': 'default/default/7frbdw1fmfozgtqavghowsepk' + }, + db: { + 'buildx.build.ref': 'default/default/onic7g2axylf56rxetob7qruy' + } +}; + +jest.spyOn(Context, 'tmpDir').mockImplementation((): string => { + if (!fs.existsSync(tmpDir)) { + fs.mkdirSync(tmpDir, {recursive: true}); + } + return tmpDir; +}); + +jest.spyOn(Context, 'tmpName').mockImplementation((): string => { + return tmpName; +}); beforeEach(() => { jest.clearAllMocks(); }); +afterEach(() => { + rimraf.sync(tmpDir); +}); + +describe('resolveMetadata', () => { + it('matches', async () => { + const metadataFile = Bake.getMetadataFilePath(); + await fs.writeFileSync(metadataFile, JSON.stringify(metadata)); + const expected = Bake.resolveMetadata(); + expect(expected).toEqual(metadata as BakeMetadata); + }); +}); + +describe('resolveRefs', () => { + it('matches', async () => { + const metadataFile = Bake.getMetadataFilePath(); + await fs.writeFileSync(metadataFile, JSON.stringify(metadata)); + const expected = Bake.resolveRefs(); + expect(expected).toEqual(['default/default/7frbdw1fmfozgtqavghowsepk', 'default/default/onic7g2axylf56rxetob7qruy']); + }); +}); + describe('getDefinition', () => { // prettier-ignore test.each([ diff --git a/__tests__/buildx/inputs.test.ts b/__tests__/buildx/build.test.ts similarity index 81% rename from __tests__/buildx/inputs.test.ts rename to __tests__/buildx/build.test.ts index 15bd9d6..e258b27 100644 --- a/__tests__/buildx/inputs.test.ts +++ b/__tests__/buildx/build.test.ts @@ -20,16 +20,19 @@ import * as path from 'path'; import * as rimraf from 'rimraf'; import {Context} from '../../src/context'; -import {Inputs} from '../../src/buildx/inputs'; +import {Build} from '../../src/buildx/build'; + +import {BuildMetadata} from '../../src/types/build'; const fixturesDir = path.join(__dirname, '..', 'fixtures'); // prettier-ignore const tmpDir = path.join(process.env.TEMP || '/tmp', 'buildx-inputs-jest'); const tmpName = path.join(tmpDir, '.tmpname-jest'); -const metadata = `{ - "containerimage.config.digest": "sha256:059b68a595b22564a1cbc167af369349fdc2ecc1f7bc092c2235cbf601a795fd", - "containerimage.digest": "sha256:b09b9482c72371486bb2c1d2c2a2633ed1d0b8389e12c8d52b9e052725c0c83c" -}`; +const metadata: BuildMetadata = { + 'buildx.build.ref': 'default/default/n6ibcp9b2pw108rrz7ywdznvo', + 'containerimage.config.digest': 'sha256:059b68a595b22564a1cbc167f369349fdc2ecc1f7bc092c2235cbf601a795fd', + 'containerimage.digest': 'sha256:b09b9482c72371486bb2c1d2c2a2633ed1d0b8389e12c8d52b9e052725c0c83c' +}; jest.spyOn(Context, 'tmpDir').mockImplementation((): string => { if (!fs.existsSync(tmpDir)) { @@ -50,30 +53,39 @@ afterEach(() => { rimraf.sync(tmpDir); }); -describe('resolveBuildImageID', () => { +describe('resolveImageID', () => { it('matches', async () => { const imageID = 'sha256:bfb45ab72e46908183546477a08f8867fc40cebadd00af54b071b097aed127a9'; - const imageIDFile = Inputs.getBuildImageIDFilePath(); + const imageIDFile = Build.getImageIDFilePath(); await fs.writeFileSync(imageIDFile, imageID); - const expected = Inputs.resolveBuildImageID(); + const expected = Build.resolveImageID(); expect(expected).toEqual(imageID); }); }); -describe('resolveBuildMetadata', () => { +describe('resolveMetadata', () => { it('matches', async () => { - const metadataFile = Inputs.getBuildMetadataFilePath(); - await fs.writeFileSync(metadataFile, metadata); - const expected = Inputs.resolveBuildMetadata(); + const metadataFile = Build.getMetadataFilePath(); + await fs.writeFileSync(metadataFile, JSON.stringify(metadata)); + const expected = Build.resolveMetadata(); expect(expected).toEqual(metadata); }); }); +describe('resolveRef', () => { + it('matches', async () => { + const metadataFile = Build.getMetadataFilePath(); + await fs.writeFileSync(metadataFile, JSON.stringify(metadata)); + const expected = Build.resolveRef(); + expect(expected).toEqual('default/default/n6ibcp9b2pw108rrz7ywdznvo'); + }); +}); + describe('resolveDigest', () => { it('matches', async () => { - const metadataFile = Inputs.getBuildMetadataFilePath(); - await fs.writeFileSync(metadataFile, metadata); - const expected = Inputs.resolveDigest(); + const metadataFile = Build.getMetadataFilePath(); + await fs.writeFileSync(metadataFile, JSON.stringify(metadata)); + const expected = Build.resolveDigest(); expect(expected).toEqual('sha256:b09b9482c72371486bb2c1d2c2a2633ed1d0b8389e12c8d52b9e052725c0c83c'); }); }); @@ -120,7 +132,7 @@ describe('getProvenanceInput', () => { ], ])('given input %p', async (input: string, expected: string) => { await setInput('provenance', input); - expect(Inputs.getProvenanceInput('provenance')).toEqual(expected); + expect(Build.getProvenanceInput('provenance')).toEqual(expected); }); }); @@ -148,11 +160,11 @@ describe('resolveProvenanceAttrs', () => { 'builder-id=https://github.com/docker/actions-toolkit/actions/runs/123' ], ])('given %p', async (input: string, expected: string) => { - expect(Inputs.resolveProvenanceAttrs(input)).toEqual(expected); + expect(Build.resolveProvenanceAttrs(input)).toEqual(expected); }); }); -describe('resolveBuildSecret', () => { +describe('resolveSecret', () => { test.each([ ['A_SECRET=abcdef0123456789', false, 'A_SECRET', 'abcdef0123456789', null], ['GIT_AUTH_TOKEN=abcdefghijklmno=0123456789', false, 'GIT_AUTH_TOKEN', 'abcdefghijklmno=0123456789', null], @@ -166,9 +178,9 @@ describe('resolveBuildSecret', () => { try { let secret: string; if (file) { - secret = Inputs.resolveBuildSecretFile(kvp); + secret = Build.resolveSecretFile(kvp); } else { - secret = Inputs.resolveBuildSecretString(kvp); + secret = Build.resolveSecretString(kvp); } expect(secret).toEqual(`id=${exKey},src=${tmpName}`); expect(fs.readFileSync(tmpName, 'utf-8')).toEqual(exValue); @@ -185,7 +197,7 @@ describe('resolveBuildSecret', () => { ['FOO=bar=baz', 'FOO', 'bar=baz', null] ])('given %p key and %p env', async (kvp: string, exKey: string, exValue: string, error: Error | null) => { try { - const secret = Inputs.resolveBuildSecretEnv(kvp); + const secret = Build.resolveSecretEnv(kvp); expect(secret).toEqual(`id=${exKey},env=${exValue}`); } catch (e) { // eslint-disable-next-line jest/no-conditional-expect @@ -206,7 +218,7 @@ describe('hasLocalExporter', () => { [['" type= local" , dest=./release-out'], true], [['.'], true] ])('given %p returns %p', async (exporters: Array, expected: boolean) => { - expect(Inputs.hasLocalExporter(exporters)).toEqual(expected); + expect(Build.hasLocalExporter(exporters)).toEqual(expected); }); }); @@ -222,7 +234,7 @@ describe('hasTarExporter', () => { [['" type= local" , dest=./release-out'], false], [['.'], false] ])('given %p returns %p', async (exporters: Array, expected: boolean) => { - expect(Inputs.hasTarExporter(exporters)).toEqual(expected); + expect(Build.hasTarExporter(exporters)).toEqual(expected); }); }); @@ -240,7 +252,7 @@ describe('hasDockerExporter', () => { [['type=docker'], true, true], [['.'], true, true], ])('given %p returns %p', async (exporters: Array, expected: boolean, load: boolean | undefined) => { - expect(Inputs.hasDockerExporter(exporters, load)).toEqual(expected); + expect(Build.hasDockerExporter(exporters, load)).toEqual(expected); }); }); @@ -251,7 +263,7 @@ describe('hasAttestationType', () => { ['type=sbom,true', 'sbom', true], ['type=foo,bar', 'provenance', false], ])('given %p for %p returns %p', async (attrs: string, name: string, expected: boolean) => { - expect(Inputs.hasAttestationType(name, attrs)).toEqual(expected); + expect(Build.hasAttestationType(name, attrs)).toEqual(expected); }); }); @@ -275,7 +287,7 @@ describe('resolveAttestationAttrs', () => { '' ], ])('given %p', async (input: string, expected: string) => { - expect(Inputs.resolveAttestationAttrs(input)).toEqual(expected); + expect(Build.resolveAttestationAttrs(input)).toEqual(expected); }); }); @@ -285,7 +297,7 @@ describe('hasGitAuthTokenSecret', () => { [['A_SECRET=abcdef0123456789'], false], [['GIT_AUTH_TOKEN=abcdefghijklmno=0123456789'], true], ])('given %p secret', async (kvp: Array, expected: boolean) => { - expect(Inputs.hasGitAuthTokenSecret(kvp)).toBe(expected); + expect(Build.hasGitAuthTokenSecret(kvp)).toBe(expected); }); }); diff --git a/src/buildx/bake.ts b/src/buildx/bake.ts index 4835a07..3b86fe1 100644 --- a/src/buildx/bake.ts +++ b/src/buildx/bake.ts @@ -14,13 +14,17 @@ * limitations under the License. */ +import fs from 'fs'; +import path from 'path'; + +import {Build} from './build'; import {Buildx} from './buildx'; +import {Context} from '../context'; import {Exec} from '../exec'; -import {Inputs} from './inputs'; import {Util} from '../util'; import {ExecOptions} from '@actions/exec'; -import {BakeDefinition} from '../types/bake'; +import {BakeDefinition, BakeMetadata} from '../types/bake'; export interface BakeOpts { buildx?: Buildx; @@ -47,6 +51,36 @@ export class Bake { this.buildx = opts?.buildx || new Buildx(); } + public static getMetadataFilePath(): string { + return path.join(Context.tmpDir(), 'metadata-file'); + } + + public static resolveMetadata(): BakeMetadata | undefined { + const metadataFile = Bake.getMetadataFilePath(); + if (!fs.existsSync(metadataFile)) { + return undefined; + } + const content = fs.readFileSync(metadataFile, {encoding: 'utf-8'}).trim(); + if (content === 'null') { + return undefined; + } + return JSON.parse(content); + } + + public static resolveRefs(): Array | undefined { + const metadata = Bake.resolveMetadata(); + if (!metadata) { + return undefined; + } + const refs = new Array(); + for (const key in metadata) { + if ('buildx.build.ref' in metadata[key]) { + refs.push(metadata[key]['buildx.build.ref']); + } + } + return refs; + } + public async getDefinition(cmdOpts: BakeCmdOpts, execOptions?: ExecOptions): Promise { execOptions = execOptions || {ignoreReturnCode: true}; execOptions.ignoreReturnCode = true; @@ -119,15 +153,15 @@ export class Bake { } public static hasLocalExporter(def: BakeDefinition): boolean { - return Inputs.hasExporterType('local', Bake.exporters(def)); + return Build.hasExporterType('local', Bake.exporters(def)); } public static hasTarExporter(def: BakeDefinition): boolean { - return Inputs.hasExporterType('tar', Bake.exporters(def)); + return Build.hasExporterType('tar', Bake.exporters(def)); } public static hasDockerExporter(def: BakeDefinition, load?: boolean): boolean { - return load || Inputs.hasExporterType('docker', Bake.exporters(def)); + return load || Build.hasExporterType('docker', Bake.exporters(def)); } private static exporters(def: BakeDefinition): Array { diff --git a/src/buildx/inputs.ts b/src/buildx/build.ts similarity index 73% rename from src/buildx/inputs.ts rename to src/buildx/build.ts index 491eb8d..4352a7a 100644 --- a/src/buildx/inputs.ts +++ b/src/buildx/build.ts @@ -23,37 +23,27 @@ import {Context} from '../context'; import {GitHub} from '../github'; import {Util} from '../util'; -const parseKvp = (kvp: string): [string, string] => { - const delimiterIndex = kvp.indexOf('='); - const key = kvp.substring(0, delimiterIndex); - const value = kvp.substring(delimiterIndex + 1); +import {BuildMetadata} from '../types/build'; - if (key.length == 0 || value.length == 0) { - throw new Error(`${kvp} is not a valid secret`); - } - - return [key, value]; -}; - -export class Inputs { - public static getBuildImageIDFilePath(): string { +export class Build { + public static getImageIDFilePath(): string { return path.join(Context.tmpDir(), 'iidfile'); } - public static getBuildMetadataFilePath(): string { + public static getMetadataFilePath(): string { return path.join(Context.tmpDir(), 'metadata-file'); } - public static resolveBuildImageID(): string | undefined { - const iidFile = Inputs.getBuildImageIDFilePath(); + public static resolveImageID(): string | undefined { + const iidFile = Build.getImageIDFilePath(); if (!fs.existsSync(iidFile)) { return undefined; } return fs.readFileSync(iidFile, {encoding: 'utf-8'}).trim(); } - public static resolveBuildMetadata(): string | undefined { - const metadataFile = Inputs.getBuildMetadataFilePath(); + public static resolveMetadata(): BuildMetadata | undefined { + const metadataFile = Build.getMetadataFilePath(); if (!fs.existsSync(metadataFile)) { return undefined; } @@ -61,38 +51,48 @@ export class Inputs { if (content === 'null') { return undefined; } - return content; + return JSON.parse(content); } - public static resolveDigest(): string | undefined { - const metadata = Inputs.resolveBuildMetadata(); - if (metadata === undefined) { + public static resolveRef(): string | undefined { + const metadata = Build.resolveMetadata(); + if (!metadata) { return undefined; } - const metadataJSON = JSON.parse(metadata); - if (metadataJSON['containerimage.digest']) { - return metadataJSON['containerimage.digest']; + if ('buildx.build.ref' in metadata) { + return metadata['buildx.build.ref']; } return undefined; } - public static resolveBuildSecretString(kvp: string): string { - const [key, file] = Inputs.resolveBuildSecret(kvp, false); + public static resolveDigest(): string | undefined { + const metadata = Build.resolveMetadata(); + if (!metadata) { + return undefined; + } + if ('containerimage.digest' in metadata) { + return metadata['containerimage.digest']; + } + return undefined; + } + + public static resolveSecretString(kvp: string): string { + const [key, file] = Build.resolveSecret(kvp, false); return `id=${key},src=${file}`; } - public static resolveBuildSecretFile(kvp: string): string { - const [key, file] = Inputs.resolveBuildSecret(kvp, true); + public static resolveSecretFile(kvp: string): string { + const [key, file] = Build.resolveSecret(kvp, true); return `id=${key},src=${file}`; } - public static resolveBuildSecretEnv(kvp: string): string { - const [key, value] = parseKvp(kvp); + public static resolveSecretEnv(kvp: string): string { + const [key, value] = Build.parseSecretKvp(kvp); return `id=${key},env=${value}`; } - public static resolveBuildSecret(kvp: string, file: boolean): [string, string] { - const [key, _value] = parseKvp(kvp); + public static resolveSecret(kvp: string, file: boolean): [string, string] { + const [key, _value] = Build.parseSecretKvp(kvp); let value = _value; if (file) { if (!fs.existsSync(value)) { @@ -115,7 +115,7 @@ export class Inputs { return core.getBooleanInput(name) ? `builder-id=${GitHub.workflowRunURL}` : 'false'; } catch (err) { // not a valid boolean, so we assume it's a string - return Inputs.resolveProvenanceAttrs(input); + return Build.resolveProvenanceAttrs(input); } } @@ -143,15 +143,15 @@ export class Inputs { } public static hasLocalExporter(exporters: string[]): boolean { - return Inputs.hasExporterType('local', exporters); + return Build.hasExporterType('local', exporters); } public static hasTarExporter(exporters: string[]): boolean { - return Inputs.hasExporterType('tar', exporters); + return Build.hasExporterType('tar', exporters); } public static hasDockerExporter(exporters: string[], load?: boolean): boolean { - return load || Inputs.hasExporterType('docker', exporters); + return load || Build.hasExporterType('docker', exporters); } public static hasExporterType(name: string, exporters: string[]): boolean { @@ -223,4 +223,14 @@ export class Inputs { } return false; } + + private static parseSecretKvp(kvp: string): [string, string] { + const delimiterIndex = kvp.indexOf('='); + const key = kvp.substring(0, delimiterIndex); + const value = kvp.substring(delimiterIndex + 1); + if (key.length == 0 || value.length == 0) { + throw new Error(`${kvp} is not a valid secret`); + } + return [key, value]; + } } diff --git a/src/types/bake.ts b/src/types/bake.ts index e257dc5..b45b329 100644 --- a/src/types/bake.ts +++ b/src/types/bake.ts @@ -19,6 +19,10 @@ export interface BakeDefinition { target: Record; } +export interface BakeMetadata { + [target: string]: Record; +} + export interface Group { targets: Array; } diff --git a/src/types/build.ts b/src/types/build.ts new file mode 100644 index 0000000..4466220 --- /dev/null +++ b/src/types/build.ts @@ -0,0 +1,19 @@ +/** + * 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. + */ + +export type BuildMetadata = { + [key: string]: string; +};