Skip to content

Commit 18c0371

Browse files
committed
buildx: history load .dockerbuild
Signed-off-by: CrazyMax <[email protected]>
1 parent 6fb52d2 commit 18c0371

File tree

12 files changed

+571
-2
lines changed

12 files changed

+571
-2
lines changed

__tests__/buildx/history.test.ts

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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~login-action~T0XYYW.dockerbuild'],
41+
['moby~buildkit~LWDOW6.dockerbuild'],
42+
])('loading %p', async (filename) => {
43+
const res = await History.load({
44+
file: path.join(fixturesDir, 'oci-archive', filename)
45+
});
46+
// console.log(JSON.stringify(res, null, 2));
47+
expect(res).toBeDefined();
48+
});
49+
});
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

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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+
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+
23+
// https://github.com/moby/buildkit/blob/593aad1ea909b978d2c54fef2a138c6d3a9107e6/solver/llbsolver/history.go#L672
24+
export const MEDIATYPE_STATUS_V0 = 'application/vnd.buildkit.status.v0';

src/types/buildkit/client.ts

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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 {Digest} from '../oci/digest';
18+
import {ProgressGroup, Range, SourceInfo} from './ops';
19+
20+
// https://github.com/moby/buildkit/blob/593aad1ea909b978d2c54fef2a138c6d3a9107e6/client/graph.go#L10-L19
21+
export interface Vertex {
22+
digest?: Digest;
23+
inputs?: Array<Digest>;
24+
name?: string;
25+
started?: Date;
26+
completed?: Date;
27+
cached?: boolean;
28+
error?: string;
29+
progressGroup?: ProgressGroup;
30+
}
31+
32+
// https://github.com/moby/buildkit/blob/593aad1ea909b978d2c54fef2a138c6d3a9107e6/client/graph.go#L21-L30
33+
export interface VertexStatus {
34+
id: string;
35+
vertex?: Digest;
36+
name?: string;
37+
total?: number;
38+
current: number;
39+
timestamp?: Date;
40+
started?: Date;
41+
completed?: Date;
42+
}
43+
44+
// https://github.com/moby/buildkit/blob/593aad1ea909b978d2c54fef2a138c6d3a9107e6/client/graph.go#L32-L37
45+
export interface VertexLog {
46+
vertex?: Digest;
47+
stream?: number;
48+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
49+
data: any;
50+
timestamp: Date;
51+
}
52+
53+
// https://github.com/moby/buildkit/blob/593aad1ea909b978d2c54fef2a138c6d3a9107e6/client/graph.go#L39-L48
54+
export interface VertexWarning {
55+
vertex?: Digest;
56+
level?: number;
57+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
58+
short?: any;
59+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
60+
detail?: Array<any>;
61+
url?: string;
62+
63+
sourceInfo?: SourceInfo;
64+
range?: Range[];
65+
}
66+
67+
// https://github.com/moby/buildkit/blob/593aad1ea909b978d2c54fef2a138c6d3a9107e6/client/graph.go#L50-L55
68+
export interface SolveStatus {
69+
vertexes?: Vertex[];
70+
statuses?: VertexStatus[];
71+
logs?: VertexLog[];
72+
warnings?: VertexWarning[];
73+
}
74+
75+
// https://github.com/moby/buildkit/blob/593aad1ea909b978d2c54fef2a138c6d3a9107e6/client/graph.go#L57-L60
76+
export interface SolveResponse {
77+
exporterResponse: Record<string, string>;
78+
}

src/types/buildkit/control.ts

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
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 {Descriptor} from '../oci/descriptor';
18+
import {Digest} from '../oci/digest';
19+
import {ProgressGroup, Range, SourceInfo} from './ops';
20+
import {RpcStatus} from './rpc';
21+
22+
// https://github.com/moby/buildkit/blob/593aad1ea909b978d2c54fef2a138c6d3a9107e6/api/services/control/control.pb.go#L1504-L1525
23+
export interface BuildHistoryRecord {
24+
Ref: string;
25+
Frontend: string;
26+
FrontendAttrs: Record<string, string>;
27+
Exporters: Exporter[];
28+
error?: RpcStatus;
29+
CreatedAt?: Date;
30+
CompletedAt?: Date;
31+
logs?: Descriptor;
32+
ExporterResponse: Record<string, string>;
33+
Result?: BuildResultInfo;
34+
Results: Record<string, BuildResultInfo>;
35+
Generation: number;
36+
trace?: Descriptor;
37+
pinned: boolean;
38+
numCachedSteps: number;
39+
numTotalSteps: number;
40+
numCompletedSteps: number;
41+
}
42+
43+
// https://github.com/moby/buildkit/blob/593aad1ea909b978d2c54fef2a138c6d3a9107e6/api/services/control/control.pb.go#L1909-L1917
44+
export interface Exporter {
45+
Type: string;
46+
Attrs: Record<string, string>;
47+
}
48+
49+
// https://github.com/moby/buildkit/blob/593aad1ea909b978d2c54fef2a138c6d3a9107e6/api/services/control/control.pb.go#L1845-L1852
50+
export interface BuildResultInfo {
51+
ResultDeprecated?: Descriptor;
52+
Attestations?: Descriptor[];
53+
Results?: Record<number, Descriptor>;
54+
}
55+
56+
// https://github.com/moby/buildkit/blob/593aad1ea909b978d2c54fef2a138c6d3a9107e6/api/services/control/control.pb.go#L751-L759
57+
export interface StatusResponse {
58+
vertexes?: Vertex[];
59+
statuses?: VertexStatus[];
60+
logs?: VertexLog[];
61+
warnings?: VertexWarning[];
62+
}
63+
64+
// https://github.com/moby/buildkit/blob/593aad1ea909b978d2c54fef2a138c6d3a9107e6/api/services/control/control.pb.go#L822-L834
65+
export interface Vertex {
66+
digest: Digest;
67+
inputs: Digest[];
68+
name?: string;
69+
cached?: boolean;
70+
started?: Date;
71+
completed?: Date;
72+
error?: string;
73+
progressGroup?: ProgressGroup;
74+
}
75+
76+
// https://github.com/moby/buildkit/blob/593aad1ea909b978d2c54fef2a138c6d3a9107e6/api/services/control/control.pb.go#L911-L923
77+
export interface VertexStatus {
78+
ID?: string;
79+
vertex: Digest;
80+
name?: string;
81+
current?: number;
82+
total?: number;
83+
timestamp: Date;
84+
started?: Date;
85+
completed?: Date;
86+
}
87+
88+
// https://github.com/moby/buildkit/blob/593aad1ea909b978d2c54fef2a138c6d3a9107e6/api/services/control/control.pb.go#L1007-L1015
89+
export interface VertexLog {
90+
vertex: Digest;
91+
timestamp: Date;
92+
stream?: number;
93+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
94+
msg?: any;
95+
}
96+
97+
// https://github.com/moby/buildkit/blob/593aad1ea909b978d2c54fef2a138c6d3a9107e6/api/services/control/control.pb.go#L1071-L1082
98+
export interface VertexWarning {
99+
vertex: Digest;
100+
level?: number;
101+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
102+
short?: any;
103+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
104+
detail?: any[];
105+
url?: string;
106+
info?: SourceInfo;
107+
ranges?: Range[];
108+
}

0 commit comments

Comments
 (0)