Skip to content

Commit

Permalink
buildx: metadata and refs resolution for build and bake
Browse files Browse the repository at this point in the history
Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
  • Loading branch information
crazy-max committed Apr 25, 2024
1 parent e03b189 commit 97b8071
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 39 deletions.
50 changes: 48 additions & 2 deletions __tests__/buildx/bake.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand Down
46 changes: 29 additions & 17 deletions __tests__/buildx/build.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -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');
});
Expand Down Expand Up @@ -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],
Expand All @@ -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);
Expand All @@ -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
Expand Down
38 changes: 36 additions & 2 deletions src/buildx/bake.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {Build} from './build';
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;
Expand All @@ -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 <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> {
execOptions = execOptions || {ignoreReturnCode: true};
execOptions.ignoreReturnCode = true;
Expand Down
48 changes: 30 additions & 18 deletions src/buildx/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,63 +23,75 @@ 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;
}
const content = fs.readFileSync(metadataFile, {encoding: 'utf-8'}).trim();
if (content === 'null') {
return undefined;
}
return content;
return <BuildMetadata>JSON.parse(content);
}

public static resolveRef(): string | undefined {
const metadata = Build.resolveMetadata();
if (!metadata) {
return undefined;
}
if ('buildx.build.ref' in metadata) {
return metadata['buildx.build.ref'];
}
return undefined;
}

public static resolveDigest(): string | undefined {
const metadata = Build.resolveBuildMetadata();
if (metadata === undefined) {
const metadata = Build.resolveMetadata();
if (!metadata) {
return undefined;
}
const metadataJSON = JSON.parse(metadata);
if (metadataJSON['containerimage.digest']) {
return metadataJSON['containerimage.digest'];
if ('containerimage.digest' in metadata) {
return metadata['containerimage.digest'];
}
return undefined;
}

public static resolveBuildSecretString(kvp: string): string {
const [key, file] = Build.resolveBuildSecret(kvp, false);
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) {
Expand Down
4 changes: 4 additions & 0 deletions src/types/bake.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ export interface BakeDefinition {
target: Record<string, Target>;
}

export interface BakeMetadata {
[target: string]: Record<string, string>;
}

export interface Group {
targets: Array<string>;
}
Expand Down
19 changes: 19 additions & 0 deletions src/types/build.ts
Original file line number Diff line number Diff line change
@@ -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;
};

0 comments on commit 97b8071

Please sign in to comment.