@hyperbrowser/sdk 0.93.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +190 -0
- package/dist/client.d.ts +2 -20
- package/dist/client.js +5 -17
- package/dist/error.d.ts +27 -0
- package/dist/error.js +32 -0
- package/dist/image-builds.d.ts +6 -0
- package/dist/image-builds.js +12 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +12 -1
- package/dist/retry.d.ts +14 -0
- package/dist/retry.js +49 -0
- package/dist/sandbox/base.d.ts +27 -0
- package/dist/sandbox/base.js +298 -99
- package/dist/sandbox/files.d.ts +24 -25
- package/dist/sandbox/files.js +146 -37
- package/dist/sandbox/image-build/artifacts.d.ts +16 -0
- package/dist/sandbox/image-build/artifacts.js +16 -0
- package/dist/sandbox/image-build/blake2b.d.ts +2 -0
- package/dist/sandbox/image-build/blake2b.js +89 -0
- package/dist/sandbox/image-build/common.d.ts +19 -0
- package/dist/sandbox/image-build/common.js +109 -0
- package/dist/sandbox/image-build/context.d.ts +43 -0
- package/dist/sandbox/image-build/context.js +599 -0
- package/dist/sandbox/image-build/docker-image.d.ts +34 -0
- package/dist/sandbox/image-build/docker-image.js +472 -0
- package/dist/sandbox/image-build/dockerfile-analysis.d.ts +17 -0
- package/dist/sandbox/image-build/dockerfile-analysis.js +409 -0
- package/dist/sandbox/image-build/dockerignore.d.ts +18 -0
- package/dist/sandbox/image-build/dockerignore.js +275 -0
- package/dist/sandbox/image-build/gzip.d.ts +11 -0
- package/dist/sandbox/image-build/gzip.js +91 -0
- package/dist/sandbox/image-build/image-init.d.ts +5 -0
- package/dist/sandbox/image-build/image-init.js +97 -0
- package/dist/sandbox/image-build/index.d.ts +13 -0
- package/dist/sandbox/image-build/index.js +36 -0
- package/dist/sandbox/image-build/local-docker.d.ts +11 -0
- package/dist/sandbox/image-build/local-docker.js +72 -0
- package/dist/sandbox/image-build/resolution.d.ts +27 -0
- package/dist/sandbox/image-build/resolution.js +137 -0
- package/dist/sandbox/image-build/tar.d.ts +41 -0
- package/dist/sandbox/image-build/tar.js +378 -0
- package/dist/sandbox/image-build/upload.d.ts +10 -0
- package/dist/sandbox/image-build/upload.js +143 -0
- package/dist/sandbox/index.d.ts +2 -1
- package/dist/sandbox/index.js +4 -1
- package/dist/sandbox/process-output.d.ts +36 -0
- package/dist/sandbox/process-output.js +117 -0
- package/dist/sandbox/process.d.ts +25 -1
- package/dist/sandbox/process.js +197 -29
- package/dist/sandbox/terminal.js +9 -5
- package/dist/sandbox/ws.d.ts +4 -1
- package/dist/sandbox/ws.js +33 -7
- package/dist/services/base.d.ts +2 -0
- package/dist/services/base.js +46 -30
- package/dist/services/normalize.d.ts +3 -0
- package/dist/services/normalize.js +21 -0
- package/dist/services/sandboxes.d.ts +40 -3
- package/dist/services/sandboxes.js +569 -53
- package/dist/services/volumes.js +29 -14
- package/dist/types/constants.d.ts +1 -1
- package/dist/types/index.d.ts +2 -1
- package/dist/types/sandbox.d.ts +246 -30
- package/dist/types/volume.d.ts +7 -7
- package/package.json +24 -5
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/** Local Docker image inspection and layer-manifest packaging for prebuilt imports. */
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
20
|
+
if (mod && mod.__esModule) return mod;
|
|
21
|
+
var result = {};
|
|
22
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
23
|
+
__setModuleDefault(result, mod);
|
|
24
|
+
return result;
|
|
25
|
+
};
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.packageDockerImageManifest = exports.prepareDockerImageManifestSource = exports.dockerImageDigest = exports.runDockerCommand = exports.DockerCommandError = void 0;
|
|
28
|
+
const stream_1 = require("stream");
|
|
29
|
+
const promises_1 = require("stream/promises");
|
|
30
|
+
const child_process_1 = require("child_process");
|
|
31
|
+
const crypto_1 = require("crypto");
|
|
32
|
+
const fs_1 = require("fs");
|
|
33
|
+
const events_1 = require("events");
|
|
34
|
+
const os_1 = require("os");
|
|
35
|
+
const path = __importStar(require("path"));
|
|
36
|
+
const util_1 = require("util");
|
|
37
|
+
const artifacts_1 = require("./artifacts");
|
|
38
|
+
const common_1 = require("./common");
|
|
39
|
+
const context_1 = require("./context");
|
|
40
|
+
const image_init_1 = require("./image-init");
|
|
41
|
+
const tar_1 = require("./tar");
|
|
42
|
+
const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
|
|
43
|
+
const MAX_DOCKER_SAVE_ENTRIES = 4096;
|
|
44
|
+
const MAX_DOCKER_SAVE_ARCHIVE_BYTES = 5 * 1024 * 1024 * 1024;
|
|
45
|
+
const MAX_DOCKER_SAVE_METADATA_BYTES = 16 * 1024 * 1024;
|
|
46
|
+
const MAX_DOCKER_IMAGE_LAYERS = 512;
|
|
47
|
+
class DockerCommandError extends Error {
|
|
48
|
+
constructor(message) {
|
|
49
|
+
super(message);
|
|
50
|
+
this.name = "DockerCommandError";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
exports.DockerCommandError = DockerCommandError;
|
|
54
|
+
const describeCommandFailure = (args, error) => {
|
|
55
|
+
if (error.code === "ENOENT") {
|
|
56
|
+
return new DockerCommandError("docker CLI is required for local Docker image operations");
|
|
57
|
+
}
|
|
58
|
+
const stderr = String(error.stderr ?? "").trim();
|
|
59
|
+
const command = ["docker", ...args].join(" ");
|
|
60
|
+
if (stderr) {
|
|
61
|
+
return new DockerCommandError(`${command}: ${stderr}`);
|
|
62
|
+
}
|
|
63
|
+
return new DockerCommandError(`${command} failed with code ${String(error.code ?? "unknown")}`);
|
|
64
|
+
};
|
|
65
|
+
const runDockerCommand = async (args) => {
|
|
66
|
+
try {
|
|
67
|
+
const { stdout } = await execFileAsync("docker", args, {
|
|
68
|
+
maxBuffer: MAX_DOCKER_SAVE_METADATA_BYTES,
|
|
69
|
+
encoding: "utf8",
|
|
70
|
+
});
|
|
71
|
+
return stdout;
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
throw describeCommandFailure(args, error);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
exports.runDockerCommand = runDockerCommand;
|
|
78
|
+
const normalizeSha256Digest = (value) => {
|
|
79
|
+
const digest = String(value ?? "")
|
|
80
|
+
.trim()
|
|
81
|
+
.toLowerCase();
|
|
82
|
+
if (!digest.startsWith("sha256:") || !common_1.SHA256_HEX_PATTERN.test(digest.slice(7))) {
|
|
83
|
+
throw new Error("docker inspect returned an invalid linux/amd64 image digest");
|
|
84
|
+
}
|
|
85
|
+
return digest;
|
|
86
|
+
};
|
|
87
|
+
const unsupportedDockerImagePlatformError = (dockerImage, actualPlatform, expectedPlatform) => new Error([
|
|
88
|
+
"docker image platform is not supported for Hyperbrowser image builds: " +
|
|
89
|
+
`${dockerImage} is ${actualPlatform} (expected ${expectedPlatform}).`,
|
|
90
|
+
`Please rebuild the image for ${expectedPlatform} and try again:`,
|
|
91
|
+
" cd <docker-build-context-root>",
|
|
92
|
+
` docker buildx build --platform ${expectedPlatform} -t ${dockerImage} ` +
|
|
93
|
+
"-f <path/to/Dockerfile> --load .",
|
|
94
|
+
].join("\n"));
|
|
95
|
+
const inspectDockerImage = async (dockerImage, platform) => {
|
|
96
|
+
const output = (await (0, exports.runDockerCommand)([
|
|
97
|
+
"image",
|
|
98
|
+
"inspect",
|
|
99
|
+
`--platform=${platform}`,
|
|
100
|
+
"--format",
|
|
101
|
+
"{{json .}}",
|
|
102
|
+
dockerImage,
|
|
103
|
+
])).trim();
|
|
104
|
+
let inspection;
|
|
105
|
+
try {
|
|
106
|
+
inspection = JSON.parse(output);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
throw new Error("decode Docker image inspection");
|
|
110
|
+
}
|
|
111
|
+
if (!(0, common_1.isRecord)(inspection)) {
|
|
112
|
+
throw new Error("docker inspect returned an invalid image inspection");
|
|
113
|
+
}
|
|
114
|
+
const actualPlatform = `${String(inspection.Os ?? "")}/${String(inspection.Architecture ?? "")}`.toLowerCase();
|
|
115
|
+
if (actualPlatform !== platform.trim().toLowerCase()) {
|
|
116
|
+
throw unsupportedDockerImagePlatformError(dockerImage, actualPlatform, platform);
|
|
117
|
+
}
|
|
118
|
+
return inspection;
|
|
119
|
+
};
|
|
120
|
+
const inspectDockerContainerConfig = async (containerId) => {
|
|
121
|
+
const output = (await (0, exports.runDockerCommand)(["container", "inspect", "--format", "{{json .Config}}", containerId])).trim();
|
|
122
|
+
if (!output || output === "null") {
|
|
123
|
+
throw new Error("docker inspect returned empty container config");
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
return (0, common_1.requireRecord)(JSON.parse(output), "Docker container config");
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
if (error instanceof SyntaxError) {
|
|
130
|
+
throw new Error("decode Docker container config");
|
|
131
|
+
}
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
const removeDockerContainer = async (containerId) => {
|
|
136
|
+
try {
|
|
137
|
+
await (0, exports.runDockerCommand)(["rm", "-f", containerId]);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
if (!(error instanceof DockerCommandError)) {
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
/** Inspect platform identity with Docker API 1.49+, without temporary resources. */
|
|
146
|
+
const dockerImageDigest = async (dockerImage, options = {}) => {
|
|
147
|
+
const platform = options.platform ?? artifacts_1.IMAGE_BUILD_SOURCE_PLATFORM;
|
|
148
|
+
let inspection;
|
|
149
|
+
try {
|
|
150
|
+
inspection = await inspectDockerImage(dockerImage, platform);
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
if (error instanceof DockerCommandError) {
|
|
154
|
+
const message = error.message;
|
|
155
|
+
if (message.includes('"--platform" requires API version') ||
|
|
156
|
+
message.includes("unknown flag: --platform")) {
|
|
157
|
+
throw new Error("Local Docker image imports require a Docker CLI and Engine supporting API 1.49 " +
|
|
158
|
+
"or newer (Docker 28.1+). Upgrade Docker or remove an older DOCKER_API_VERSION override.");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
return normalizeSha256Digest(inspection.Id);
|
|
164
|
+
};
|
|
165
|
+
exports.dockerImageDigest = dockerImageDigest;
|
|
166
|
+
const manifestSource = (imageDigest, config, cleanup) => ({
|
|
167
|
+
imageDigest,
|
|
168
|
+
config,
|
|
169
|
+
imageConfigUser: String(config.User ?? "").trim(),
|
|
170
|
+
imageInit: (0, image_init_1.deriveAutoImageInit)(config),
|
|
171
|
+
cleanup,
|
|
172
|
+
});
|
|
173
|
+
const prepareDockerImageManifestSource = async (dockerImage, options = {}) => {
|
|
174
|
+
const platform = options.platform ?? artifacts_1.IMAGE_BUILD_SOURCE_PLATFORM;
|
|
175
|
+
try {
|
|
176
|
+
const inspection = await inspectDockerImage(dockerImage, platform);
|
|
177
|
+
return manifestSource(normalizeSha256Digest(inspection.Id), (0, common_1.requireRecord)(inspection.Config, "Docker image config"), async () => undefined);
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
if (!(error instanceof DockerCommandError)) {
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const containerId = (await (0, exports.runDockerCommand)(["create", `--platform=${platform}`, dockerImage])).trim();
|
|
185
|
+
if (!containerId) {
|
|
186
|
+
throw new Error("docker create returned empty container ID");
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
const config = await inspectDockerContainerConfig(containerId);
|
|
190
|
+
let digest;
|
|
191
|
+
try {
|
|
192
|
+
digest = await (0, exports.runDockerCommand)([
|
|
193
|
+
"image",
|
|
194
|
+
"inspect",
|
|
195
|
+
`--platform=${platform}`,
|
|
196
|
+
"--format",
|
|
197
|
+
"{{.Id}}",
|
|
198
|
+
dockerImage,
|
|
199
|
+
]);
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
if (!(error instanceof DockerCommandError)) {
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
digest = await (0, exports.runDockerCommand)([
|
|
206
|
+
"container",
|
|
207
|
+
"inspect",
|
|
208
|
+
"--format",
|
|
209
|
+
"{{.Image}}",
|
|
210
|
+
containerId,
|
|
211
|
+
]);
|
|
212
|
+
}
|
|
213
|
+
return manifestSource(normalizeSha256Digest(digest), config, () => removeDockerContainer(containerId));
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
await removeDockerContainer(containerId);
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
exports.prepareDockerImageManifestSource = prepareDockerImageManifestSource;
|
|
221
|
+
const normalizeDockerSaveEntryName = (raw) => {
|
|
222
|
+
if (!raw ||
|
|
223
|
+
raw.includes("\\") ||
|
|
224
|
+
raw.includes("\x00") ||
|
|
225
|
+
raw.includes("\r") ||
|
|
226
|
+
raw.includes("\n") ||
|
|
227
|
+
raw.startsWith("/")) {
|
|
228
|
+
throw new Error("docker image save contains an unsafe entry path");
|
|
229
|
+
}
|
|
230
|
+
const normalized = (0, common_1.posixNormpath)(raw);
|
|
231
|
+
if (normalized === "." ||
|
|
232
|
+
normalized === ".." ||
|
|
233
|
+
normalized.startsWith("../") ||
|
|
234
|
+
normalized !== raw) {
|
|
235
|
+
throw new Error("docker image save contains an unsafe entry path");
|
|
236
|
+
}
|
|
237
|
+
return normalized;
|
|
238
|
+
};
|
|
239
|
+
const storeStreamedEntry = async (source, destination, expectedSize, name) => {
|
|
240
|
+
const hasher = (0, crypto_1.createHash)("sha256");
|
|
241
|
+
const output = (0, fs_1.createWriteStream)(destination, { flags: "wx" });
|
|
242
|
+
let written = 0;
|
|
243
|
+
async function* chunks() {
|
|
244
|
+
for await (const chunk of source) {
|
|
245
|
+
hasher.update(chunk);
|
|
246
|
+
written += chunk.length;
|
|
247
|
+
yield chunk;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
await (0, promises_1.pipeline)(stream_1.Readable.from(chunks(), { objectMode: false }), output);
|
|
251
|
+
if (written !== expectedSize) {
|
|
252
|
+
throw new Error(`docker image save entry "${name}" has truncated content`);
|
|
253
|
+
}
|
|
254
|
+
return { path: destination, sha256Hex: hasher.digest("hex"), sizeBytes: written };
|
|
255
|
+
};
|
|
256
|
+
const parseJsonObject = (data, label) => {
|
|
257
|
+
let parsed;
|
|
258
|
+
try {
|
|
259
|
+
parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(data));
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
throw new Error(`${label} is not valid JSON`);
|
|
263
|
+
}
|
|
264
|
+
return (0, common_1.requireRecord)(parsed, label);
|
|
265
|
+
};
|
|
266
|
+
const resolveDockerSaveManifest = (entries) => {
|
|
267
|
+
const manifestEntry = entries.get("manifest.json");
|
|
268
|
+
if (manifestEntry === undefined ||
|
|
269
|
+
manifestEntry.sizeBytes <= 0 ||
|
|
270
|
+
manifestEntry.sizeBytes > MAX_DOCKER_SAVE_METADATA_BYTES) {
|
|
271
|
+
throw new Error("docker image save manifest.json is missing or too large");
|
|
272
|
+
}
|
|
273
|
+
let manifest;
|
|
274
|
+
try {
|
|
275
|
+
manifest = JSON.parse((0, fs_1.readFileSync)(manifestEntry.path, "utf8"));
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
throw new Error("docker image save manifest.json is invalid");
|
|
279
|
+
}
|
|
280
|
+
if (!Array.isArray(manifest) || manifest.length !== 1) {
|
|
281
|
+
throw new Error("docker image save must contain exactly one manifest entry");
|
|
282
|
+
}
|
|
283
|
+
const item = (0, common_1.requireRecord)(manifest[0], "Docker save manifest entry");
|
|
284
|
+
const configName = normalizeDockerSaveEntryName(String(item.Config ?? ""));
|
|
285
|
+
const configEntry = entries.get(configName);
|
|
286
|
+
if (configEntry === undefined ||
|
|
287
|
+
configEntry.sizeBytes <= 0 ||
|
|
288
|
+
configEntry.sizeBytes > MAX_DOCKER_SAVE_METADATA_BYTES) {
|
|
289
|
+
throw new Error("docker image save config is missing or too large");
|
|
290
|
+
}
|
|
291
|
+
const rawLayers = item.Layers;
|
|
292
|
+
if (!Array.isArray(rawLayers) || rawLayers.length > MAX_DOCKER_IMAGE_LAYERS) {
|
|
293
|
+
throw new Error("docker image save has too many layers");
|
|
294
|
+
}
|
|
295
|
+
const layers = [];
|
|
296
|
+
for (const rawLayer of rawLayers) {
|
|
297
|
+
const layerName = normalizeDockerSaveEntryName(String(rawLayer));
|
|
298
|
+
const layer = entries.get(layerName);
|
|
299
|
+
if (layer === undefined || layer.sizeBytes <= 0) {
|
|
300
|
+
throw new Error(`docker image save layer "${layerName}" is missing`);
|
|
301
|
+
}
|
|
302
|
+
layers.push(layer);
|
|
303
|
+
}
|
|
304
|
+
return [configEntry, layers];
|
|
305
|
+
};
|
|
306
|
+
const resolveOciImageDescriptor = (entries, imageDigest, config, layers) => {
|
|
307
|
+
const digestHex = imageDigest.slice("sha256:".length);
|
|
308
|
+
const entry = entries.get(`blobs/sha256/${digestHex}`);
|
|
309
|
+
if (entry === undefined ||
|
|
310
|
+
entry.sha256Hex !== digestHex ||
|
|
311
|
+
entry.sizeBytes <= 0 ||
|
|
312
|
+
entry.sizeBytes > MAX_DOCKER_SAVE_METADATA_BYTES) {
|
|
313
|
+
throw new Error("docker image save is missing its inspected OCI image manifest");
|
|
314
|
+
}
|
|
315
|
+
const data = (0, fs_1.readFileSync)(entry.path);
|
|
316
|
+
const descriptor = parseJsonObject(data, "OCI image manifest");
|
|
317
|
+
if (descriptor.schemaVersion !== 2) {
|
|
318
|
+
throw new Error("inspected Docker image descriptor is not a valid OCI image manifest");
|
|
319
|
+
}
|
|
320
|
+
const descriptorConfig = (0, common_1.requireRecord)(descriptor.config, "OCI config");
|
|
321
|
+
if (descriptorConfig.digest !== `sha256:${config.sha256}` ||
|
|
322
|
+
descriptorConfig.size !== config.sizeBytes) {
|
|
323
|
+
throw new Error("OCI image manifest config does not match Docker save config");
|
|
324
|
+
}
|
|
325
|
+
const descriptorLayers = descriptor.layers;
|
|
326
|
+
if (!Array.isArray(descriptorLayers) || descriptorLayers.length !== layers.length) {
|
|
327
|
+
throw new Error("OCI image manifest layer count does not match Docker save manifest");
|
|
328
|
+
}
|
|
329
|
+
descriptorLayers.forEach((rawLayer, index) => {
|
|
330
|
+
const item = (0, common_1.requireRecord)(rawLayer, `OCI layer ${index}`);
|
|
331
|
+
const layer = layers[index];
|
|
332
|
+
if (item.digest !== `sha256:${layer.sha256}` || item.size !== layer.sizeBytes) {
|
|
333
|
+
throw new Error(`OCI image manifest layer ${index} does not match Docker save manifest`);
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
return {
|
|
337
|
+
sha256: entry.sha256Hex,
|
|
338
|
+
sizeBytes: entry.sizeBytes,
|
|
339
|
+
dataBase64: data.toString("base64"),
|
|
340
|
+
};
|
|
341
|
+
};
|
|
342
|
+
const terminateProcess = async (child) => {
|
|
343
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
child.kill("SIGTERM");
|
|
347
|
+
const timer = setTimeout(() => child.kill("SIGKILL"), 5000);
|
|
348
|
+
await (0, events_1.once)(child, "close");
|
|
349
|
+
clearTimeout(timer);
|
|
350
|
+
};
|
|
351
|
+
/** Stream `docker image save` into verified layer artifacts plus a manifest. */
|
|
352
|
+
const packageDockerImageManifest = async (dockerImage, imageDigest, config, options = {}) => {
|
|
353
|
+
const platform = options.platform ?? artifacts_1.IMAGE_BUILD_SOURCE_PLATFORM;
|
|
354
|
+
const normalizedDigest = normalizeSha256Digest(imageDigest);
|
|
355
|
+
const workspace = (0, fs_1.mkdtempSync)(path.join(options.tempDir ?? (0, os_1.tmpdir)(), "hb-docker-image-layers-"));
|
|
356
|
+
let child = null;
|
|
357
|
+
try {
|
|
358
|
+
const args = ["image", "save", `--platform=${platform}`, dockerImage];
|
|
359
|
+
child = (0, child_process_1.spawn)("docker", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
360
|
+
const process = child;
|
|
361
|
+
const stdout = process.stdout;
|
|
362
|
+
if (!stdout || !process.stderr) {
|
|
363
|
+
throw new Error("docker image save did not provide stdout");
|
|
364
|
+
}
|
|
365
|
+
let stderr = "";
|
|
366
|
+
process.stderr.setEncoding("utf8");
|
|
367
|
+
process.stderr.on("data", (chunk) => {
|
|
368
|
+
stderr += chunk;
|
|
369
|
+
});
|
|
370
|
+
const exit = new Promise((resolve, reject) => {
|
|
371
|
+
process.on("error", (error) => reject(describeCommandFailure(args, error)));
|
|
372
|
+
process.on("close", (code) => resolve(code));
|
|
373
|
+
});
|
|
374
|
+
exit.catch(() => undefined);
|
|
375
|
+
const entries = new Map();
|
|
376
|
+
let totalBytes = 0;
|
|
377
|
+
let index = 0;
|
|
378
|
+
const consume = async () => {
|
|
379
|
+
for await (const member of (0, tar_1.readTarEntries)(stdout)) {
|
|
380
|
+
if (index >= MAX_DOCKER_SAVE_ENTRIES) {
|
|
381
|
+
throw new Error(`docker image save contains more than ${MAX_DOCKER_SAVE_ENTRIES} entries`);
|
|
382
|
+
}
|
|
383
|
+
const entryIndex = index;
|
|
384
|
+
index += 1;
|
|
385
|
+
if (!member.isFile) {
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
const name = normalizeDockerSaveEntryName(member.name);
|
|
389
|
+
if (entries.has(name)) {
|
|
390
|
+
throw new Error(`docker image save contains duplicate entry "${name}"`);
|
|
391
|
+
}
|
|
392
|
+
totalBytes += member.size;
|
|
393
|
+
if (member.size < 0 || totalBytes > MAX_DOCKER_SAVE_ARCHIVE_BYTES) {
|
|
394
|
+
throw new Error("docker image save archive exceeds the size limit");
|
|
395
|
+
}
|
|
396
|
+
const destination = path.join(workspace, `entry-${String(entryIndex).padStart(4, "0")}`);
|
|
397
|
+
entries.set(name, await storeStreamedEntry(member.content(), destination, member.size, name));
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
try {
|
|
401
|
+
await consume();
|
|
402
|
+
}
|
|
403
|
+
catch (error) {
|
|
404
|
+
await terminateProcess(process);
|
|
405
|
+
throw error;
|
|
406
|
+
}
|
|
407
|
+
const returnCode = await exit;
|
|
408
|
+
if (returnCode !== 0) {
|
|
409
|
+
const message = stderr.trim();
|
|
410
|
+
throw new Error(message
|
|
411
|
+
? `docker image save ${dockerImage} failed: ${message}`
|
|
412
|
+
: `docker image save ${dockerImage} failed with code ${returnCode}`);
|
|
413
|
+
}
|
|
414
|
+
child = null;
|
|
415
|
+
const [configEntry, layerEntries] = resolveDockerSaveManifest(entries);
|
|
416
|
+
const configBytes = (0, fs_1.readFileSync)(configEntry.path);
|
|
417
|
+
parseJsonObject(configBytes, "Docker image config");
|
|
418
|
+
const configDescriptor = {
|
|
419
|
+
sha256: configEntry.sha256Hex,
|
|
420
|
+
sizeBytes: configEntry.sizeBytes,
|
|
421
|
+
dataBase64: configBytes.toString("base64"),
|
|
422
|
+
};
|
|
423
|
+
const layerDescriptors = [];
|
|
424
|
+
const layers = {};
|
|
425
|
+
for (const layer of layerEntries) {
|
|
426
|
+
layerDescriptors.push({ sha256: layer.sha256Hex, sizeBytes: layer.sizeBytes });
|
|
427
|
+
const existing = layers[layer.sha256Hex];
|
|
428
|
+
if (existing !== undefined && existing.sizeBytes !== layer.sizeBytes) {
|
|
429
|
+
throw new Error(`Docker layer ${layer.sha256Hex} has conflicting sizes`);
|
|
430
|
+
}
|
|
431
|
+
if (existing === undefined) {
|
|
432
|
+
layers[layer.sha256Hex] = {
|
|
433
|
+
path: layer.path,
|
|
434
|
+
sha256Hex: layer.sha256Hex,
|
|
435
|
+
sizeBytes: layer.sizeBytes,
|
|
436
|
+
inputFormat: artifacts_1.DOCKER_IMAGE_MANIFEST_INPUT_FORMAT,
|
|
437
|
+
sourcePlatform: platform,
|
|
438
|
+
imageConfigUser: "",
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
const imageDescriptor = normalizedDigest !== `sha256:${configDescriptor.sha256}`
|
|
443
|
+
? resolveOciImageDescriptor(entries, normalizedDigest, configDescriptor, layerDescriptors)
|
|
444
|
+
: undefined;
|
|
445
|
+
const manifest = {
|
|
446
|
+
version: 1,
|
|
447
|
+
imageDigest: normalizedDigest,
|
|
448
|
+
...(imageDescriptor ? { descriptor: imageDescriptor } : {}),
|
|
449
|
+
config: configDescriptor,
|
|
450
|
+
layers: layerDescriptors,
|
|
451
|
+
};
|
|
452
|
+
const artifact = (0, context_1.writeManifestArtifact)(workspace, "docker-image-manifest.json", (0, context_1.canonicalManifestJson)(manifest), artifacts_1.DOCKER_IMAGE_MANIFEST_INPUT_FORMAT, {
|
|
453
|
+
imageConfigUser: String(config.User ?? "").trim(),
|
|
454
|
+
imageInit: (0, image_init_1.deriveAutoImageInit)(config),
|
|
455
|
+
});
|
|
456
|
+
return {
|
|
457
|
+
artifact,
|
|
458
|
+
manifest,
|
|
459
|
+
layers,
|
|
460
|
+
workspace,
|
|
461
|
+
cleanup: () => (0, artifacts_1.removeWorkspace)(workspace),
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
catch (error) {
|
|
465
|
+
if (child !== null) {
|
|
466
|
+
await terminateProcess(child);
|
|
467
|
+
}
|
|
468
|
+
(0, artifacts_1.removeWorkspace)(workspace);
|
|
469
|
+
throw error;
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
exports.packageDockerImageManifest = packageDockerImageManifest;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conservative Dockerfile analysis for remote build context selection.
|
|
3
|
+
*
|
|
4
|
+
* This module intentionally does not try to execute or fully reproduce the
|
|
5
|
+
* BuildKit Dockerfile frontend. It recognizes the context-consuming syntax the
|
|
6
|
+
* SDK can analyze safely and requests a full context for anything ambiguous.
|
|
7
|
+
* That keeps sparse uploads an optimization rather than a correctness
|
|
8
|
+
* requirement.
|
|
9
|
+
*/
|
|
10
|
+
export type DockerfileSourceAnalysis = {
|
|
11
|
+
groups: string[][];
|
|
12
|
+
fallbackReason: string;
|
|
13
|
+
};
|
|
14
|
+
/** POSIX `shlex.split` word splitting. */
|
|
15
|
+
export declare const shlexSplit: (value: string) => string[];
|
|
16
|
+
/** Return local source groups or a reason to upload the full context. */
|
|
17
|
+
export declare const analyzeDockerfileSources: (data: Buffer) => DockerfileSourceAnalysis;
|