buildx: metadata and refs resolution for build and bake

Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
This commit is contained in:
CrazyMax
2024-04-24 17:33:01 +02:00
parent e03b1899b0
commit 97b80719d2
6 changed files with 168 additions and 41 deletions

View File

@@ -14,21 +14,67 @@
* limitations under the License. * 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 fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import * as rimraf from 'rimraf';
import {Bake} from '../../src/buildx/bake'; import {Bake} from '../../src/buildx/bake';
import {Context} from '../../src/context';
import {ExecOptions} from '@actions/exec'; import {ExecOptions} from '@actions/exec';
import {BakeDefinition} from '../../src/types/bake'; import {BakeDefinition, BakeMetadata} from '../../src/types/bake';
const fixturesDir = path.join(__dirname, '..', 'fixtures'); 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(() => { beforeEach(() => {
jest.clearAllMocks(); 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', () => { describe('getDefinition', () => {
// prettier-ignore // prettier-ignore
test.each([ test.each([

View File

@@ -22,14 +22,17 @@ import * as rimraf from 'rimraf';
import {Context} from '../../src/context'; import {Context} from '../../src/context';
import {Build} from '../../src/buildx/build'; import {Build} from '../../src/buildx/build';
import {BuildMetadata} from '../../src/types/build';
const fixturesDir = path.join(__dirname, '..', 'fixtures'); const fixturesDir = path.join(__dirname, '..', 'fixtures');
// prettier-ignore // prettier-ignore
const tmpDir = path.join(process.env.TEMP || '/tmp', 'buildx-inputs-jest'); const tmpDir = path.join(process.env.TEMP || '/tmp', 'buildx-inputs-jest');
const tmpName = path.join(tmpDir, '.tmpname-jest'); const tmpName = path.join(tmpDir, '.tmpname-jest');
const metadata = `{ const metadata: BuildMetadata = {
"containerimage.config.digest": "sha256:059b68a595b22564a1cbc167af369349fdc2ecc1f7bc092c2235cbf601a795fd", 'buildx.build.ref': 'default/default/n6ibcp9b2pw108rrz7ywdznvo',
"containerimage.digest": "sha256:b09b9482c72371486bb2c1d2c2a2633ed1d0b8389e12c8d52b9e052725c0c83c" 'containerimage.config.digest': 'sha256:059b68a595b22564a1cbc167f369349fdc2ecc1f7bc092c2235cbf601a795fd',
}`; 'containerimage.digest': 'sha256:b09b9482c72371486bb2c1d2c2a2633ed1d0b8389e12c8d52b9e052725c0c83c'
};
jest.spyOn(Context, 'tmpDir').mockImplementation((): string => { jest.spyOn(Context, 'tmpDir').mockImplementation((): string => {
if (!fs.existsSync(tmpDir)) { if (!fs.existsSync(tmpDir)) {
@@ -50,29 +53,38 @@ afterEach(() => {
rimraf.sync(tmpDir); rimraf.sync(tmpDir);
}); });
describe('resolveBuildImageID', () => { describe('resolveImageID', () => {
it('matches', async () => { it('matches', async () => {
const imageID = 'sha256:bfb45ab72e46908183546477a08f8867fc40cebadd00af54b071b097aed127a9'; const imageID = 'sha256:bfb45ab72e46908183546477a08f8867fc40cebadd00af54b071b097aed127a9';
const imageIDFile = Build.getBuildImageIDFilePath(); const imageIDFile = Build.getImageIDFilePath();
await fs.writeFileSync(imageIDFile, imageID); await fs.writeFileSync(imageIDFile, imageID);
const expected = Build.resolveBuildImageID(); const expected = Build.resolveImageID();
expect(expected).toEqual(imageID); expect(expected).toEqual(imageID);
}); });
}); });
describe('resolveBuildMetadata', () => { describe('resolveMetadata', () => {
it('matches', async () => { it('matches', async () => {
const metadataFile = Build.getBuildMetadataFilePath(); const metadataFile = Build.getMetadataFilePath();
await fs.writeFileSync(metadataFile, metadata); await fs.writeFileSync(metadataFile, JSON.stringify(metadata));
const expected = Build.resolveBuildMetadata(); const expected = Build.resolveMetadata();
expect(expected).toEqual(metadata); 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', () => { describe('resolveDigest', () => {
it('matches', async () => { it('matches', async () => {
const metadataFile = Build.getBuildMetadataFilePath(); const metadataFile = Build.getMetadataFilePath();
await fs.writeFileSync(metadataFile, metadata); await fs.writeFileSync(metadataFile, JSON.stringify(metadata));
const expected = Build.resolveDigest(); const expected = Build.resolveDigest();
expect(expected).toEqual('sha256:b09b9482c72371486bb2c1d2c2a2633ed1d0b8389e12c8d52b9e052725c0c83c'); expect(expected).toEqual('sha256:b09b9482c72371486bb2c1d2c2a2633ed1d0b8389e12c8d52b9e052725c0c83c');
}); });
@@ -152,7 +164,7 @@ describe('resolveProvenanceAttrs', () => {
}); });
}); });
describe('resolveBuildSecret', () => { describe('resolveSecret', () => {
test.each([ test.each([
['A_SECRET=abcdef0123456789', false, 'A_SECRET', 'abcdef0123456789', null], ['A_SECRET=abcdef0123456789', false, 'A_SECRET', 'abcdef0123456789', null],
['GIT_AUTH_TOKEN=abcdefghijklmno=0123456789', false, 'GIT_AUTH_TOKEN', 'abcdefghijklmno=0123456789', null], ['GIT_AUTH_TOKEN=abcdefghijklmno=0123456789', false, 'GIT_AUTH_TOKEN', 'abcdefghijklmno=0123456789', null],
@@ -166,9 +178,9 @@ describe('resolveBuildSecret', () => {
try { try {
let secret: string; let secret: string;
if (file) { if (file) {
secret = Build.resolveBuildSecretFile(kvp); secret = Build.resolveSecretFile(kvp);
} else { } else {
secret = Build.resolveBuildSecretString(kvp); secret = Build.resolveSecretString(kvp);
} }
expect(secret).toEqual(`id=${exKey},src=${tmpName}`); expect(secret).toEqual(`id=${exKey},src=${tmpName}`);
expect(fs.readFileSync(tmpName, 'utf-8')).toEqual(exValue); expect(fs.readFileSync(tmpName, 'utf-8')).toEqual(exValue);
@@ -185,7 +197,7 @@ describe('resolveBuildSecret', () => {
['FOO=bar=baz', 'FOO', 'bar=baz', null] ['FOO=bar=baz', 'FOO', 'bar=baz', null]
])('given %p key and %p env', async (kvp: string, exKey: string, exValue: string, error: Error | null) => { ])('given %p key and %p env', async (kvp: string, exKey: string, exValue: string, error: Error | null) => {
try { try {
const secret = Build.resolveBuildSecretEnv(kvp); const secret = Build.resolveSecretEnv(kvp);
expect(secret).toEqual(`id=${exKey},env=${exValue}`); expect(secret).toEqual(`id=${exKey},env=${exValue}`);
} catch (e) { } catch (e) {
// eslint-disable-next-line jest/no-conditional-expect // eslint-disable-next-line jest/no-conditional-expect

View File

@@ -14,13 +14,17 @@
* limitations under the License. * limitations under the License.
*/ */
import {Buildx} from './buildx'; import fs from 'fs';
import {Exec} from '../exec'; import path from 'path';
import {Build} from './build'; import {Build} from './build';
import {Buildx} from './buildx';
import {Context} from '../context';
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/bake'; import {BakeDefinition, BakeMetadata} from '../types/bake';
export interface BakeOpts { export interface BakeOpts {
buildx?: Buildx; buildx?: Buildx;
@@ -47,6 +51,36 @@ export class Bake {
this.buildx = opts?.buildx || new Buildx(); 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 <BakeMetadata>JSON.parse(content);
}
public static resolveRefs(): Array<string> | undefined {
const metadata = Bake.resolveMetadata();
if (!metadata) {
return undefined;
}
const refs = new Array<string>();
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<BakeDefinition> { public async getDefinition(cmdOpts: BakeCmdOpts, execOptions?: ExecOptions): Promise<BakeDefinition> {
execOptions = execOptions || {ignoreReturnCode: true}; execOptions = execOptions || {ignoreReturnCode: true};
execOptions.ignoreReturnCode = true; execOptions.ignoreReturnCode = true;

View File

@@ -23,25 +23,27 @@ import {Context} from '../context';
import {GitHub} from '../github'; import {GitHub} from '../github';
import {Util} from '../util'; import {Util} from '../util';
import {BuildMetadata} from '../types/build';
export class Build { export class Build {
public static getBuildImageIDFilePath(): string { public static getImageIDFilePath(): string {
return path.join(Context.tmpDir(), 'iidfile'); return path.join(Context.tmpDir(), 'iidfile');
} }
public static getBuildMetadataFilePath(): string { public static getMetadataFilePath(): string {
return path.join(Context.tmpDir(), 'metadata-file'); return path.join(Context.tmpDir(), 'metadata-file');
} }
public static resolveBuildImageID(): string | undefined { public static resolveImageID(): string | undefined {
const iidFile = Build.getBuildImageIDFilePath(); const iidFile = Build.getImageIDFilePath();
if (!fs.existsSync(iidFile)) { if (!fs.existsSync(iidFile)) {
return undefined; return undefined;
} }
return fs.readFileSync(iidFile, {encoding: 'utf-8'}).trim(); return fs.readFileSync(iidFile, {encoding: 'utf-8'}).trim();
} }
public static resolveBuildMetadata(): string | undefined { public static resolveMetadata(): BuildMetadata | undefined {
const metadataFile = Build.getBuildMetadataFilePath(); const metadataFile = Build.getMetadataFilePath();
if (!fs.existsSync(metadataFile)) { if (!fs.existsSync(metadataFile)) {
return undefined; return undefined;
} }
@@ -49,37 +51,47 @@ export class Build {
if (content === 'null') { if (content === 'null') {
return undefined; return undefined;
} }
return content; return <BuildMetadata>JSON.parse(content);
} }
public static resolveDigest(): string | undefined { public static resolveRef(): string | undefined {
const metadata = Build.resolveBuildMetadata(); const metadata = Build.resolveMetadata();
if (metadata === undefined) { if (!metadata) {
return undefined; return undefined;
} }
const metadataJSON = JSON.parse(metadata); if ('buildx.build.ref' in metadata) {
if (metadataJSON['containerimage.digest']) { return metadata['buildx.build.ref'];
return metadataJSON['containerimage.digest'];
} }
return undefined; return undefined;
} }
public static resolveBuildSecretString(kvp: string): string { public static resolveDigest(): string | undefined {
const [key, file] = Build.resolveBuildSecret(kvp, false); 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}`; return `id=${key},src=${file}`;
} }
public static resolveBuildSecretFile(kvp: string): string { public static resolveSecretFile(kvp: string): string {
const [key, file] = Build.resolveBuildSecret(kvp, true); const [key, file] = Build.resolveSecret(kvp, true);
return `id=${key},src=${file}`; return `id=${key},src=${file}`;
} }
public static resolveBuildSecretEnv(kvp: string): string { public static resolveSecretEnv(kvp: string): string {
const [key, value] = Build.parseSecretKvp(kvp); const [key, value] = Build.parseSecretKvp(kvp);
return `id=${key},env=${value}`; 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); const [key, _value] = Build.parseSecretKvp(kvp);
let value = _value; let value = _value;
if (file) { if (file) {

View File

@@ -19,6 +19,10 @@ export interface BakeDefinition {
target: Record<string, Target>; target: Record<string, Target>;
} }
export interface BakeMetadata {
[target: string]: Record<string, string>;
}
export interface Group { export interface Group {
targets: Array<string>; targets: Array<string>;
} }

19
src/types/build.ts Normal file
View File

@@ -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;
};