Skip to content

Commit

Permalink
buildx: history load .dockerbuild
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 Jun 27, 2024
1 parent 7bd4fed commit e46fa74
Show file tree
Hide file tree
Showing 9 changed files with 171 additions and 1 deletion.
52 changes: 52 additions & 0 deletions __tests__/buildx/history.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* 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.
*/

import {afterEach, beforeEach, describe, expect, jest, test} from '@jest/globals';
import path from 'path';
import * as rimraf from 'rimraf';

import {History} from '../../src/buildx/history';

const fixturesDir = path.join(__dirname, '..', 'fixtures');

// prettier-ignore
const tmpDir = path.join(process.env.TEMP || '/tmp', 'docker-jest');

beforeEach(() => {
jest.clearAllMocks();
});

afterEach(function () {
rimraf.sync(tmpDir);
});

describe('load', () => {
// prettier-ignore
test.each([
['crazy-max~docker-alpine-s6~II9A63.dockerbuild'],
['docker~build-push-action~2778G2.dockerbuild'],
['docker~login-action~T0XYYW.dockerbuild'],
['docker~test-docker-action~dfile-error~DEBCS4.dockerbuild'],
['docker~test-docker-action~go-error~BGI5SX.dockerbuild'],
['moby~buildkit~LWDOW6.dockerbuild'],
])('loading %p', async (filename) => {
const res = await History.load({
file: path.join(fixturesDir, 'oci-archive', filename)
});
// console.log(JSON.stringify(res, null, 2));
expect(res).toBeDefined();
});
});
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
86 changes: 85 additions & 1 deletion src/buildx/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,18 @@ import {Context} from '../context';
import {Docker} from '../docker/docker';
import {Exec} from '../exec';
import {GitHub} from '../github';
import {OCI} from '../oci/oci';

import {ExportRecordOpts, ExportRecordResponse, Summaries} from '../types/buildx/history';
import {ExportRecordOpts, ExportRecordResponse, LoadRecordOpts, Summaries} from '../types/buildx/history';
import {Index} from '../types/oci';
import {MEDIATYPE_IMAGE_INDEX_V1, MEDIATYPE_IMAGE_MANIFEST_V1} from '../types/oci/mediatype';
import {Archive} from '../types/oci/oci';
import {BuildRecord} from '../types/buildx/buildx';
import {Descriptor} from '../types/oci/descriptor';
import {MEDIATYPE_PAYLOAD as MEDIATYPE_INTOTO_PAYLOAD, MEDIATYPE_PREDICATE} from '../types/intoto/intoto';
import {ProvenancePredicate} from '../types/intoto/slsa_provenance/v0.2/provenance';
import {ANNOTATION_REF_KEY, MEDIATYPE_HISTORY_RECORD_V0, MEDIATYPE_SOLVE_STATUS_V0} from '../types/buildkit/buildkit';
import {SolveStatus} from '../types/buildkit/client';

export interface HistoryOpts {
buildx?: Buildx;
Expand All @@ -42,6 +52,80 @@ export class History {
this.buildx = opts?.buildx || new Buildx();
}

public static async load(opts: LoadRecordOpts): Promise<Record<string, BuildRecord>> {
const ociArchive = await OCI.loadArchive({
file: opts.file
});
return History.readRecords(ociArchive.root.index, ociArchive);
}

private static readRecords(index: Index, archive: Archive): Record<string, BuildRecord> {
const res: Record<string, BuildRecord> = {};
index.manifests.forEach(desc => {
switch (desc.mediaType) {
case MEDIATYPE_IMAGE_MANIFEST_V1: {
const record = History.readRecord(desc, archive);
res[record.Ref] = record;
break;
}
case MEDIATYPE_IMAGE_INDEX_V1: {
if (!Object.prototype.hasOwnProperty.call(archive.indexes, desc.digest)) {
throw new Error(`Missing index: ${desc.digest}`);
}
const records = History.readRecords(archive.indexes[desc.digest], archive);
for (const ref in records) {
if (!Object.prototype.hasOwnProperty.call(records, ref)) {
continue;
}
res[ref] = records[ref];
}
break;
}
}
});
return res;
}

private static readRecord(desc: Descriptor, archive: Archive): BuildRecord {
if (!Object.prototype.hasOwnProperty.call(archive.manifests, desc.digest)) {
throw new Error(`Missing manifest: ${desc.digest}`);
}
const manifest = archive.manifests[desc.digest];
if (manifest.config.mediaType !== MEDIATYPE_HISTORY_RECORD_V0) {
throw new Error(`Unexpected config media type: ${manifest.config.mediaType}`);
}
if (!Object.prototype.hasOwnProperty.call(archive.blobs, manifest.config.digest)) {
throw new Error(`Missing config blob: ${manifest.config.digest}`);
}
const record = <BuildRecord>JSON.parse(archive.blobs[manifest.config.digest]);
if (manifest.annotations && ANNOTATION_REF_KEY in manifest.annotations) {
if (record.Ref !== manifest.annotations[ANNOTATION_REF_KEY]) {
throw new Error(`Mismatched ref ${desc.digest}: ${record.Ref} != ${manifest.annotations[ANNOTATION_REF_KEY]}`);
}
}
manifest.layers.forEach(layer => {
switch (layer.mediaType) {
case MEDIATYPE_SOLVE_STATUS_V0: {
if (!Object.prototype.hasOwnProperty.call(archive.blobs, layer.digest)) {
throw new Error(`Missing blob: ${layer.digest}`);
}
record.solveStatus = <SolveStatus>JSON.parse(archive.blobs[layer.digest]);
break;
}
case MEDIATYPE_INTOTO_PAYLOAD: {
if (!Object.prototype.hasOwnProperty.call(archive.blobs, layer.digest)) {
throw new Error(`Missing blob: ${layer.digest}`);
}
if (layer.annotations && MEDIATYPE_PREDICATE in layer.annotations && layer.annotations[MEDIATYPE_PREDICATE].startsWith('https://slsa.dev/provenance/')) {
record.provenance = <ProvenancePredicate>JSON.parse(archive.blobs[layer.digest]);
}
break;
}
}
});
return record;
}

public async export(opts: ExportRecordOpts): Promise<ExportRecordResponse> {
if (os.platform() === 'win32') {
throw new Error('Exporting a build record is currently not supported on Windows');
Expand Down
6 changes: 6 additions & 0 deletions src/types/buildkit/buildkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,11 @@
* limitations under the License.
*/

export const ANNOTATION_REF_KEY = 'vnd.buildkit.history.reference';

export const MEDIATYPE_SOLVE_STATUS_V0 = 'application/vnd.buildkit.solvestatus.v0';

export const MEDIATYPE_HISTORY_RECORD_V0 = 'application/vnd.buildkit.historyrecord.v0';

// https://github.com/moby/buildkit/blob/v0.14.0/solver/llbsolver/history.go#L672
export const MEDIATYPE_STATUS_V0 = 'application/vnd.buildkit.status.v0';
24 changes: 24 additions & 0 deletions src/types/buildx/buildx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
* limitations under the License.
*/

import {SolveStatus} from '../buildkit/client';
import {BuildHistoryRecord} from '../buildkit/control';
import {ProvenancePredicate} from '../intoto/slsa_provenance/v0.2/provenance';

export interface Cert {
cacert?: string;
cert?: string;
Expand Down Expand Up @@ -44,3 +48,23 @@ export interface LocalState {
DockerfilePath: string;
GroupRef?: string;
}

export interface StateGroup {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Definition: any;
Targets: Array<string>;
Inputs: Array<string>;
Refs?: Array<string>;
}

// https://github.com/docker/desktop-build/blob/b609016485f6d37cb22cdfb616c6222c85c30683/tools/export-build/main.go#L48-L54
export interface ExportedRecord extends BuildHistoryRecord {
localState: LocalState;
stateGroup: StateGroup;
DefaultPlatform: string;
}

export interface BuildRecord extends ExportedRecord {
solveStatus?: SolveStatus;
provenance?: ProvenancePredicate;
}
4 changes: 4 additions & 0 deletions src/types/buildx/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,7 @@ export interface RecordSummary {
frontendAttrs: Record<string, string>;
error?: string;
}

export interface LoadRecordOpts {
file: string;
}

0 comments on commit e46fa74

Please sign in to comment.