From 97b80719d2c6a70831affc530e8cff4432512552 Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Wed, 24 Apr 2024 17:33:01 +0200 Subject: [PATCH] buildx: metadata and refs resolution for build and bake Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- __tests__/buildx/bake.test.ts | 50 ++++++++++++++++++++++++++++++++-- __tests__/buildx/build.test.ts | 46 +++++++++++++++++++------------ src/buildx/bake.ts | 40 +++++++++++++++++++++++++-- src/buildx/build.ts | 50 +++++++++++++++++++++------------- src/types/bake.ts | 4 +++ src/types/build.ts | 19 +++++++++++++ 6 files changed, 168 insertions(+), 41 deletions(-) create mode 100644 src/types/build.ts 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/build.test.ts b/__tests__/buildx/build.test.ts index f36d88c..e258b27 100644 --- a/__tests__/buildx/build.test.ts +++ b/__tests__/buildx/build.test.ts @@ -22,14 +22,17 @@ import * as rimraf from 'rimraf'; import {Context} from '../../src/context'; 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,29 +53,38 @@ afterEach(() => { rimraf.sync(tmpDir); }); -describe('resolveBuildImageID', () => { +describe('resolveImageID', () => { it('matches', async () => { const imageID = 'sha256:bfb45ab72e46908183546477a08f8867fc40cebadd00af54b071b097aed127a9'; - const imageIDFile = Build.getBuildImageIDFilePath(); + const imageIDFile = Build.getImageIDFilePath(); await fs.writeFileSync(imageIDFile, imageID); - const expected = Build.resolveBuildImageID(); + const expected = Build.resolveImageID(); expect(expected).toEqual(imageID); }); }); -describe('resolveBuildMetadata', () => { +describe('resolveMetadata', () => { it('matches', async () => { - const metadataFile = Build.getBuildMetadataFilePath(); - await fs.writeFileSync(metadataFile, metadata); - const expected = Build.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 = Build.getBuildMetadataFilePath(); - await fs.writeFileSync(metadataFile, metadata); + const metadataFile = Build.getMetadataFilePath(); + await fs.writeFileSync(metadataFile, JSON.stringify(metadata)); const expected = Build.resolveDigest(); expect(expected).toEqual('sha256:b09b9482c72371486bb2c1d2c2a2633ed1d0b8389e12c8d52b9e052725c0c83c'); }); @@ -152,7 +164,7 @@ describe('resolveProvenanceAttrs', () => { }); }); -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 = Build.resolveBuildSecretFile(kvp); + secret = Build.resolveSecretFile(kvp); } else { - secret = Build.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 = Build.resolveBuildSecretEnv(kvp); + const secret = Build.resolveSecretEnv(kvp); expect(secret).toEqual(`id=${exKey},env=${exValue}`); } catch (e) { // eslint-disable-next-line jest/no-conditional-expect diff --git a/src/buildx/bake.ts b/src/buildx/bake.ts index 8cff1a1..3b86fe1 100644 --- a/src/buildx/bake.ts +++ b/src/buildx/bake.ts @@ -14,13 +14,17 @@ * limitations under the License. */ -import {Buildx} from './buildx'; -import {Exec} from '../exec'; +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 {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; diff --git a/src/buildx/build.ts b/src/buildx/build.ts index 3a81e67..4352a7a 100644 --- a/src/buildx/build.ts +++ b/src/buildx/build.ts @@ -23,25 +23,27 @@ import {Context} from '../context'; import {GitHub} from '../github'; import {Util} from '../util'; +import {BuildMetadata} from '../types/build'; + export class Build { - public static getBuildImageIDFilePath(): string { + 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 = Build.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 = Build.getBuildMetadataFilePath(); + public static resolveMetadata(): BuildMetadata | undefined { + const metadataFile = Build.getMetadataFilePath(); if (!fs.existsSync(metadataFile)) { return undefined; } @@ -49,37 +51,47 @@ export class Build { if (content === 'null') { return undefined; } - return content; + return JSON.parse(content); } - public static resolveDigest(): string | undefined { - const metadata = Build.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] = Build.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] = Build.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 { + 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] { + public static resolveSecret(kvp: string, file: boolean): [string, string] { const [key, _value] = Build.parseSecretKvp(kvp); let value = _value; if (file) { 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; +};