Skip to content

Commit a16908f

Browse files
committed
buildx: history load .dockerbuild
Signed-off-by: CrazyMax <[email protected]>
1 parent c14688a commit a16908f

11 files changed

+261
-2
lines changed

__tests__/buildx/history.test.ts

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* Copyright 2024 actions-toolkit authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import {afterEach, beforeEach, describe, expect, jest, test} from '@jest/globals';
18+
import path from 'path';
19+
import * as rimraf from 'rimraf';
20+
21+
import {History} from '../../src/buildx/history';
22+
23+
const fixturesDir = path.join(__dirname, '..', 'fixtures');
24+
25+
// prettier-ignore
26+
const tmpDir = path.join(process.env.TEMP || '/tmp', 'docker-jest');
27+
28+
beforeEach(() => {
29+
jest.clearAllMocks();
30+
});
31+
32+
afterEach(function () {
33+
rimraf.sync(tmpDir);
34+
});
35+
36+
describe('load', () => {
37+
// prettier-ignore
38+
test.each([
39+
['crazy-max~docker-alpine-s6~II9A63.dockerbuild'],
40+
['docker~build-push-action~2778G2.dockerbuild'],
41+
['docker~login-action~T0XYYW.dockerbuild'],
42+
['docker~test-docker-action~dfile-error~DEBCS4.dockerbuild'],
43+
['docker~test-docker-action~go-error~BGI5SX.dockerbuild'],
44+
['moby~buildkit~LWDOW6.dockerbuild'],
45+
])('loading %p', async (filename) => {
46+
const res = await History.load({
47+
file: path.join(fixturesDir, 'oci-archive', filename)
48+
});
49+
// console.log(JSON.stringify(res, null, 2));
50+
expect(res).toBeDefined();
51+
});
52+
});
Binary file not shown.
Binary file not shown.

src/buildx/history.ts

+86-2
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,18 @@ import {Context} from '../context';
2626
import {Docker} from '../docker/docker';
2727
import {Exec} from '../exec';
2828
import {GitHub} from '../github';
29-
30-
import {ExportRecordOpts, ExportRecordResponse, Summaries} from '../types/buildx/history';
29+
import {OCI} from '../oci/oci';
30+
31+
import {ExportRecordOpts, ExportRecordResponse, LoadRecordOpts, Summaries} from '../types/buildx/history';
32+
import {Index} from '../types/oci';
33+
import {MEDIATYPE_IMAGE_INDEX_V1, MEDIATYPE_IMAGE_MANIFEST_V1} from '../types/oci/mediatype';
34+
import {Archive} from '../types/oci/oci';
35+
import {BuildRecord} from '../types/buildx/buildx';
36+
import {Descriptor} from '../types/oci/descriptor';
37+
import {MEDIATYPE_PAYLOAD as MEDIATYPE_INTOTO_PAYLOAD, MEDIATYPE_PREDICATE} from '../types/intoto/intoto';
38+
import {ProvenancePredicate} from '../types/intoto/slsa_provenance/v0.2/provenance';
39+
import {ANNOTATION_REF_KEY, MEDIATYPE_HISTORY_RECORD_V0, MEDIATYPE_SOLVE_STATUS_V0} from '../types/buildkit/buildkit';
40+
import {SolveStatus} from '../types/buildkit/client';
3141

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

55+
public static async load(opts: LoadRecordOpts): Promise<Record<string, BuildRecord>> {
56+
const ociArchive = await OCI.loadArchive({
57+
file: opts.file
58+
});
59+
return History.readRecords(ociArchive.root.index, ociArchive);
60+
}
61+
62+
private static readRecords(index: Index, archive: Archive): Record<string, BuildRecord> {
63+
const res: Record<string, BuildRecord> = {};
64+
index.manifests.forEach(desc => {
65+
switch (desc.mediaType) {
66+
case MEDIATYPE_IMAGE_MANIFEST_V1: {
67+
const record = History.readRecord(desc, archive);
68+
res[record.Ref] = record;
69+
break;
70+
}
71+
case MEDIATYPE_IMAGE_INDEX_V1: {
72+
if (!Object.prototype.hasOwnProperty.call(archive.indexes, desc.digest)) {
73+
throw new Error(`Missing index: ${desc.digest}`);
74+
}
75+
const records = History.readRecords(archive.indexes[desc.digest], archive);
76+
for (const ref in records) {
77+
if (!Object.prototype.hasOwnProperty.call(records, ref)) {
78+
continue;
79+
}
80+
res[ref] = records[ref];
81+
}
82+
break;
83+
}
84+
}
85+
});
86+
return res;
87+
}
88+
89+
private static readRecord(desc: Descriptor, archive: Archive): BuildRecord {
90+
if (!Object.prototype.hasOwnProperty.call(archive.manifests, desc.digest)) {
91+
throw new Error(`Missing manifest: ${desc.digest}`);
92+
}
93+
const manifest = archive.manifests[desc.digest];
94+
if (manifest.config.mediaType !== MEDIATYPE_HISTORY_RECORD_V0) {
95+
throw new Error(`Unexpected config media type: ${manifest.config.mediaType}`);
96+
}
97+
if (!Object.prototype.hasOwnProperty.call(archive.blobs, manifest.config.digest)) {
98+
throw new Error(`Missing config blob: ${manifest.config.digest}`);
99+
}
100+
const record = <BuildRecord>JSON.parse(archive.blobs[manifest.config.digest]);
101+
if (manifest.annotations && ANNOTATION_REF_KEY in manifest.annotations) {
102+
if (record.Ref !== manifest.annotations[ANNOTATION_REF_KEY]) {
103+
throw new Error(`Mismatched ref ${desc.digest}: ${record.Ref} != ${manifest.annotations[ANNOTATION_REF_KEY]}`);
104+
}
105+
}
106+
manifest.layers.forEach(layer => {
107+
switch (layer.mediaType) {
108+
case MEDIATYPE_SOLVE_STATUS_V0: {
109+
if (!Object.prototype.hasOwnProperty.call(archive.blobs, layer.digest)) {
110+
throw new Error(`Missing blob: ${layer.digest}`);
111+
}
112+
record.solveStatus = <SolveStatus>JSON.parse(archive.blobs[layer.digest]);
113+
break;
114+
}
115+
case MEDIATYPE_INTOTO_PAYLOAD: {
116+
if (!Object.prototype.hasOwnProperty.call(archive.blobs, layer.digest)) {
117+
throw new Error(`Missing blob: ${layer.digest}`);
118+
}
119+
if (layer.annotations && MEDIATYPE_PREDICATE in layer.annotations && layer.annotations[MEDIATYPE_PREDICATE].startsWith('https://slsa.dev/provenance/')) {
120+
record.provenance = <ProvenancePredicate>JSON.parse(archive.blobs[layer.digest]);
121+
}
122+
break;
123+
}
124+
}
125+
});
126+
return record;
127+
}
128+
45129
public async export(opts: ExportRecordOpts): Promise<ExportRecordResponse> {
46130
if (os.platform() === 'win32') {
47131
throw new Error('Exporting a build record is currently not supported on Windows');

src/types/buildkit/buildkit.ts

+6
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,11 @@
1414
* limitations under the License.
1515
*/
1616

17+
export const ANNOTATION_REF_KEY = 'vnd.buildkit.history.reference';
18+
19+
export const MEDIATYPE_SOLVE_STATUS_V0 = 'application/vnd.buildkit.solvestatus.v0';
20+
21+
export const MEDIATYPE_HISTORY_RECORD_V0 = 'application/vnd.buildkit.historyrecord.v0';
22+
1723
// https://github.com/moby/buildkit/blob/v0.14.0/solver/llbsolver/history.go#L672
1824
export const MEDIATYPE_STATUS_V0 = 'application/vnd.buildkit.status.v0';

src/types/buildx/buildx.ts

+24
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
* limitations under the License.
1515
*/
1616

17+
import {SolveStatus} from '../buildkit/client';
18+
import {BuildHistoryRecord} from '../buildkit/control';
19+
import {ProvenancePredicate} from '../intoto/slsa_provenance/v0.2/provenance';
20+
1721
export interface Cert {
1822
cacert?: string;
1923
cert?: string;
@@ -44,3 +48,23 @@ export interface LocalState {
4448
DockerfilePath: string;
4549
GroupRef?: string;
4650
}
51+
52+
export interface StateGroup {
53+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
54+
Definition: any;
55+
Targets: Array<string>;
56+
Inputs: Array<string>;
57+
Refs?: Array<string>;
58+
}
59+
60+
// https://github.com/docker/desktop-build/blob/b609016485f6d37cb22cdfb616c6222c85c30683/tools/export-build/main.go#L48-L54
61+
export interface ExportedRecord extends BuildHistoryRecord {
62+
localState: LocalState;
63+
stateGroup: StateGroup;
64+
DefaultPlatform: string;
65+
}
66+
67+
export interface BuildRecord extends ExportedRecord {
68+
solveStatus?: SolveStatus;
69+
provenance?: ProvenancePredicate;
70+
}

src/types/buildx/history.ts

+4
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,7 @@ export interface RecordSummary {
4242
frontendAttrs: Record<string, string>;
4343
error?: string;
4444
}
45+
46+
export interface LoadRecordOpts {
47+
file: string;
48+
}

src/types/intoto/intoto.ts

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Copyright 2024 actions-toolkit authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
// https://github.com/in-toto/in-toto-golang/blob/dd6278764ab1dae7301609c7510129888e2fd569/in_toto/envelope.go#L17
18+
export const MEDIATYPE_PAYLOAD = 'application/vnd.in-toto+json';
19+
20+
export const MEDIATYPE_PREDICATE = 'in-toto.io/predicate-type';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* Copyright 2024 actions-toolkit authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
// https://github.com/in-toto/in-toto-golang/blob/master/in_toto/slsa_provenance/v0.2/provenance.go
18+
19+
export const PREDICATE_SLSA_PROVENANCE = 'https://slsa.dev/provenance/v0.2';
20+
21+
export interface ProvenancePredicate {
22+
builder: ProvenanceBuilder;
23+
buildType: string;
24+
invocation?: ProvenanceInvocation;
25+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
26+
buildConfig?: any;
27+
metadata: ProvenanceMetadata;
28+
materials?: Material[];
29+
}
30+
31+
export interface ProvenanceBuilder {
32+
id: string;
33+
}
34+
35+
export interface ProvenanceInvocation {
36+
configSource?: ConfigSource;
37+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
38+
parameters?: any;
39+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
40+
environment?: any;
41+
}
42+
43+
export interface DigestSet {
44+
[key: string]: string;
45+
}
46+
47+
export interface ConfigSource {
48+
uri?: string;
49+
digest?: DigestSet;
50+
entryPoint?: string;
51+
}
52+
53+
export interface Completeness {
54+
parameters?: boolean;
55+
environment?: boolean;
56+
materials?: boolean;
57+
}
58+
59+
export interface ProvenanceMetadata {
60+
buildInvocationId?: string;
61+
buildStartedOn?: string;
62+
completeness?: Completeness;
63+
reproducible?: boolean;
64+
}
65+
66+
export interface Material {
67+
uri: string;
68+
digest: DigestSet;
69+
}

0 commit comments

Comments
 (0)