@vercel/container 22.0.2 → 23.0.0
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/dist/buildah-store-command.js +418 -0
- package/dist/index.js +1107 -520
- package/dist/schedules-command-dispatcher +0 -0
- package/package.json +6 -5
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// src/buildah-store-cache/command/operations.ts
|
|
4
|
+
var import_node_crypto = require("crypto");
|
|
5
|
+
var import_node_fs = require("fs");
|
|
6
|
+
var import_promises3 = require("fs/promises");
|
|
7
|
+
var import_node_path3 = require("path");
|
|
8
|
+
|
|
9
|
+
// src/util.ts
|
|
10
|
+
var import_build_utils = require("@vercel/build-utils");
|
|
11
|
+
var import_node_child_process = require("child_process");
|
|
12
|
+
var import_promises = require("fs/promises");
|
|
13
|
+
var DEBUG = Boolean((0, import_build_utils.getPlatformEnv)("BUILDER_DEBUG"));
|
|
14
|
+
function run(cmd, args, opts = {}) {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
const child = (0, import_node_child_process.spawn)(cmd, args, {
|
|
17
|
+
cwd: opts.cwd,
|
|
18
|
+
env: opts.env,
|
|
19
|
+
stdio: [opts.input !== void 0 ? "pipe" : "ignore", "pipe", "pipe"]
|
|
20
|
+
});
|
|
21
|
+
let stdout = "";
|
|
22
|
+
let stderr = "";
|
|
23
|
+
child.stdout?.on("data", (chunk) => {
|
|
24
|
+
const text = chunk.toString();
|
|
25
|
+
stdout += text;
|
|
26
|
+
if (!opts.quiet) {
|
|
27
|
+
process.stderr.write(text);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
child.stderr?.on("data", (chunk) => {
|
|
31
|
+
const text = chunk.toString();
|
|
32
|
+
stderr += text;
|
|
33
|
+
if (!opts.quiet) {
|
|
34
|
+
process.stderr.write(text);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
child.on("error", (err) => {
|
|
38
|
+
if (err.code === "ENOENT") {
|
|
39
|
+
reject(
|
|
40
|
+
new Error(
|
|
41
|
+
`Command not found: \`${cmd}\`. Ensure \`${cmd}\` is installed and on your PATH.`
|
|
42
|
+
)
|
|
43
|
+
);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
reject(err);
|
|
47
|
+
});
|
|
48
|
+
child.on("close", (code) => {
|
|
49
|
+
if (code === 0) {
|
|
50
|
+
resolve({ stdout, stderr });
|
|
51
|
+
} else {
|
|
52
|
+
const detail = stderr.trim().split("\n").slice(-5).join("\n");
|
|
53
|
+
const error = new Error(
|
|
54
|
+
`\`${cmd} ${args.join(" ")}\` exited with code ${code}` + (detail ? `
|
|
55
|
+
${detail}` : "")
|
|
56
|
+
);
|
|
57
|
+
error.exitCode = code ?? void 0;
|
|
58
|
+
reject(error);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
if (opts.input !== void 0) {
|
|
62
|
+
child.stdin?.end(opts.input);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
async function lstatIfPresent(path) {
|
|
67
|
+
try {
|
|
68
|
+
return await (0, import_promises.lstat)(path);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (error.code === "ENOENT")
|
|
71
|
+
return void 0;
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/buildah-store-cache/lock.ts
|
|
77
|
+
var FileLock = class {
|
|
78
|
+
constructor(path) {
|
|
79
|
+
this.path = path;
|
|
80
|
+
}
|
|
81
|
+
shared(command) {
|
|
82
|
+
return this.wrap("--shared", command);
|
|
83
|
+
}
|
|
84
|
+
exclusive(command) {
|
|
85
|
+
return this.wrap("--exclusive", command);
|
|
86
|
+
}
|
|
87
|
+
wrap(mode, command) {
|
|
88
|
+
return ["flock", mode, "--close", this.path, ...command];
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// src/buildah-store-cache/state.ts
|
|
93
|
+
var import_promises2 = require("fs/promises");
|
|
94
|
+
var import_node_path = require("path");
|
|
95
|
+
var CLEANUP_CONTAINER_RE = /^vercel-cnb-[a-zA-Z0-9_.-]+$/;
|
|
96
|
+
async function listStateRecords(directory, suffix) {
|
|
97
|
+
return (await (0, import_promises2.readdir)(directory)).filter((name) => !name.startsWith(".") && name.endsWith(suffix)).sort().map((name) => (0, import_node_path.join)(directory, name));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/buildah-store-cache/command/index.ts
|
|
101
|
+
var import_node_path2 = require("path");
|
|
102
|
+
var STORE_COMMAND_SCRIPT = (0, import_node_path2.join)(__dirname, "buildah-store-command.js");
|
|
103
|
+
function storeCommand(request, script = STORE_COMMAND_SCRIPT) {
|
|
104
|
+
return [process.execPath, script, JSON.stringify(request)];
|
|
105
|
+
}
|
|
106
|
+
async function runStoreCommand(request, lock) {
|
|
107
|
+
const [command, ...args] = lock.exclusive(storeCommand(request));
|
|
108
|
+
const { stdout } = await run(command, args, { quiet: true });
|
|
109
|
+
return JSON.parse(stdout);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/buildah-store-cache/command/operations.ts
|
|
113
|
+
var STAGED_BLOB_INFO_CACHE = ".vercel-blob-info-cache.sqlite";
|
|
114
|
+
var RETAINED_REPOSITORY = "localhost/vercel-buildah-store";
|
|
115
|
+
var StoreOperations = class {
|
|
116
|
+
async restore(options) {
|
|
117
|
+
if (await lstatIfPresent(options.marker)) {
|
|
118
|
+
await requireRegularFile(options.marker);
|
|
119
|
+
return { status: "skipped" };
|
|
120
|
+
}
|
|
121
|
+
await (0, import_promises3.mkdir)((0, import_node_path3.dirname)(options.marker), { recursive: true });
|
|
122
|
+
await (0, import_promises3.writeFile)(options.marker, "", { flag: "wx", mode: 384 });
|
|
123
|
+
if (!await lstatIfPresent(options.archive)) {
|
|
124
|
+
return { status: "miss", reason: "no_compatible_snapshot" };
|
|
125
|
+
}
|
|
126
|
+
return runStoreCommand(
|
|
127
|
+
{
|
|
128
|
+
action: "extract",
|
|
129
|
+
options: {
|
|
130
|
+
graphRoot: options.graphRoot,
|
|
131
|
+
archive: options.archive,
|
|
132
|
+
blobInfoCache: options.blobInfoCache
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
new FileLock(options.storeLock)
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
async extract(options) {
|
|
139
|
+
const store = await lstatIfPresent(options.graphRoot);
|
|
140
|
+
if (store) {
|
|
141
|
+
await requireDirectory(options.graphRoot);
|
|
142
|
+
if ((await (0, import_promises3.readdir)(options.graphRoot)).length > 0) {
|
|
143
|
+
return { status: "warm" };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const archive = await requireRegularFile(options.archive);
|
|
147
|
+
await (0, import_promises3.mkdir)((0, import_node_path3.dirname)(options.graphRoot), { recursive: true });
|
|
148
|
+
const staging = await (0, import_promises3.mkdtemp)(`${options.graphRoot}.restore.`);
|
|
149
|
+
try {
|
|
150
|
+
await run(
|
|
151
|
+
"tar",
|
|
152
|
+
[
|
|
153
|
+
"--extract",
|
|
154
|
+
"--zstd",
|
|
155
|
+
`--file=${options.archive}`,
|
|
156
|
+
`--directory=${staging}`,
|
|
157
|
+
"--numeric-owner",
|
|
158
|
+
"--same-owner",
|
|
159
|
+
"--same-permissions",
|
|
160
|
+
"--xattrs",
|
|
161
|
+
"--xattrs-include=*"
|
|
162
|
+
],
|
|
163
|
+
{ quiet: true }
|
|
164
|
+
);
|
|
165
|
+
if (store)
|
|
166
|
+
await (0, import_promises3.rmdir)(options.graphRoot);
|
|
167
|
+
await run("mv", ["-T", "--", staging, options.graphRoot], {
|
|
168
|
+
quiet: true
|
|
169
|
+
});
|
|
170
|
+
await this.restoreBlobInfoCache(
|
|
171
|
+
(0, import_node_path3.join)(options.graphRoot, STAGED_BLOB_INFO_CACHE),
|
|
172
|
+
options.blobInfoCache
|
|
173
|
+
);
|
|
174
|
+
return { status: "hit", bytes: archive.size };
|
|
175
|
+
} finally {
|
|
176
|
+
await (0, import_promises3.rm)(staging, { recursive: true, force: true });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async save(options) {
|
|
180
|
+
const result = (0, import_node_path3.join)(options.stateDirectory, "save-result");
|
|
181
|
+
if (await lstatIfPresent(result)) {
|
|
182
|
+
await requireRegularFile(result);
|
|
183
|
+
const previous = (await (0, import_promises3.readFile)(result, "utf8")).replace(/\n+$/, "") || "failed";
|
|
184
|
+
if (previous === "saved" || previous === "unused" || previous === "failed") {
|
|
185
|
+
return { status: "skipped", previous };
|
|
186
|
+
}
|
|
187
|
+
throw new Error("invalid Buildah store save result");
|
|
188
|
+
}
|
|
189
|
+
await (0, import_promises3.mkdir)(options.stateDirectory, { recursive: true });
|
|
190
|
+
await (0, import_promises3.writeFile)(result, "", { flag: "wx", mode: 384 });
|
|
191
|
+
const records = await listStateRecords(options.stateDirectory, ".images");
|
|
192
|
+
if (records.length === 0) {
|
|
193
|
+
await (0, import_promises3.writeFile)(result, "unused\n");
|
|
194
|
+
return { status: "unused" };
|
|
195
|
+
}
|
|
196
|
+
await this.finishPendingCleanup(options);
|
|
197
|
+
await requireDirectory(options.graphRoot);
|
|
198
|
+
await this.pruneImages(records, options.buildahArgs);
|
|
199
|
+
await this.stageBlobInfoCache(
|
|
200
|
+
options.blobInfoCache,
|
|
201
|
+
(0, import_node_path3.join)(options.graphRoot, STAGED_BLOB_INFO_CACHE)
|
|
202
|
+
);
|
|
203
|
+
await this.archiveStore(options);
|
|
204
|
+
await (0, import_promises3.writeFile)(result, "saved\n");
|
|
205
|
+
return { status: "saved" };
|
|
206
|
+
}
|
|
207
|
+
async cleanup(options) {
|
|
208
|
+
if (!await lstatIfPresent(options.record))
|
|
209
|
+
return;
|
|
210
|
+
await requireRegularFile(options.record);
|
|
211
|
+
await run("buildah", options.buildahArgs, { quiet: true });
|
|
212
|
+
await (0, import_promises3.unlink)(options.record);
|
|
213
|
+
}
|
|
214
|
+
async finishPendingCleanup(options) {
|
|
215
|
+
for (const record of await listStateRecords(
|
|
216
|
+
options.stateDirectory,
|
|
217
|
+
".cleanup"
|
|
218
|
+
)) {
|
|
219
|
+
await requireRegularFile(record);
|
|
220
|
+
const container = (await (0, import_promises3.readFile)(record, "utf8")).replace(/\n+$/, "");
|
|
221
|
+
if (!CLEANUP_CONTAINER_RE.test(container)) {
|
|
222
|
+
throw new Error("invalid Buildah cleanup record");
|
|
223
|
+
}
|
|
224
|
+
await run("buildah", [...options.buildahArgs, "rm", container], {
|
|
225
|
+
quiet: true
|
|
226
|
+
});
|
|
227
|
+
await (0, import_promises3.unlink)(record);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
async pruneImages(records, buildahArgs) {
|
|
231
|
+
const containers = await run(
|
|
232
|
+
"buildah",
|
|
233
|
+
[...buildahArgs, "containers", "--quiet"],
|
|
234
|
+
{
|
|
235
|
+
quiet: true
|
|
236
|
+
}
|
|
237
|
+
);
|
|
238
|
+
if (containers.stdout.replace(/\n+$/, "") !== "") {
|
|
239
|
+
throw new Error("Buildah containers are still active");
|
|
240
|
+
}
|
|
241
|
+
const listing = await run(
|
|
242
|
+
"buildah",
|
|
243
|
+
[...buildahArgs, "images", "--all", "--no-trunc", "--json"],
|
|
244
|
+
{ quiet: true }
|
|
245
|
+
);
|
|
246
|
+
const images = JSON.parse(listing.stdout);
|
|
247
|
+
const names = /* @__PURE__ */ new Map();
|
|
248
|
+
for (const image of images) {
|
|
249
|
+
const id = image.id.replace(/^sha256:/, "");
|
|
250
|
+
if (!/^[a-f0-9]{64}$/.test(id)) {
|
|
251
|
+
throw new Error(`invalid Buildah image ID: ${id}`);
|
|
252
|
+
}
|
|
253
|
+
names.set(id, image.names ?? []);
|
|
254
|
+
}
|
|
255
|
+
const keep = /* @__PURE__ */ new Set();
|
|
256
|
+
for (const record of records) {
|
|
257
|
+
await requireRegularFile(record);
|
|
258
|
+
const references = (await (0, import_promises3.readFile)(record, "utf8")).split("\n");
|
|
259
|
+
if (references.at(-1) === "")
|
|
260
|
+
references.pop();
|
|
261
|
+
if (references.length === 0) {
|
|
262
|
+
throw new Error("Buildah image usage is incomplete");
|
|
263
|
+
}
|
|
264
|
+
for (const line of references) {
|
|
265
|
+
const reference = line.trim();
|
|
266
|
+
if (!/^[a-f0-9]{12,64}$/.test(reference)) {
|
|
267
|
+
throw new Error("Buildah image usage is incomplete");
|
|
268
|
+
}
|
|
269
|
+
for (const id of names.keys()) {
|
|
270
|
+
if (id.startsWith(reference))
|
|
271
|
+
keep.add(id);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (keep.size === 0)
|
|
276
|
+
throw new Error("no Buildah images retained");
|
|
277
|
+
const untag = [];
|
|
278
|
+
const obsolete = [];
|
|
279
|
+
for (const [id, references] of names) {
|
|
280
|
+
if (keep.has(id)) {
|
|
281
|
+
const tag = `${RETAINED_REPOSITORY}:${id}`;
|
|
282
|
+
await run("buildah", [...buildahArgs, "tag", id, tag], { quiet: true });
|
|
283
|
+
for (const name of references) {
|
|
284
|
+
if (!name.includes("@") && name !== tag)
|
|
285
|
+
untag.push(name);
|
|
286
|
+
}
|
|
287
|
+
} else {
|
|
288
|
+
obsolete.push(id);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (untag.length > 0) {
|
|
292
|
+
await run("buildah", [...buildahArgs, "rmi", ...untag], { quiet: true });
|
|
293
|
+
}
|
|
294
|
+
if (obsolete.length > 0) {
|
|
295
|
+
await run("buildah", [...buildahArgs, "rmi", "--force", ...obsolete], {
|
|
296
|
+
quiet: true
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
async archiveStore(options) {
|
|
301
|
+
const temporary = `${options.archive}.${(0, import_node_crypto.randomUUID)()}`;
|
|
302
|
+
await (0, import_promises3.writeFile)(temporary, "", { flag: "wx", mode: 384 });
|
|
303
|
+
try {
|
|
304
|
+
await run(
|
|
305
|
+
"tar",
|
|
306
|
+
[
|
|
307
|
+
"--create",
|
|
308
|
+
`--use-compress-program=zstd -q -1 -T${options.threads}`,
|
|
309
|
+
`--file=${temporary}`,
|
|
310
|
+
`--directory=${options.graphRoot}`,
|
|
311
|
+
"--one-file-system",
|
|
312
|
+
"--sparse",
|
|
313
|
+
"--numeric-owner",
|
|
314
|
+
"--xattrs",
|
|
315
|
+
"--xattrs-include=*",
|
|
316
|
+
"--anchored",
|
|
317
|
+
"--no-wildcards-match-slash",
|
|
318
|
+
"--exclude=./overlay/*/merged",
|
|
319
|
+
"--exclude=./overlay/*/work/*",
|
|
320
|
+
"."
|
|
321
|
+
],
|
|
322
|
+
{ quiet: true }
|
|
323
|
+
);
|
|
324
|
+
await run("mv", ["-fT", "--", temporary, options.archive], {
|
|
325
|
+
quiet: true
|
|
326
|
+
});
|
|
327
|
+
} finally {
|
|
328
|
+
await (0, import_promises3.rm)(temporary, { force: true });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
async restoreBlobInfoCache(staged, destination) {
|
|
332
|
+
try {
|
|
333
|
+
if (!(await lstatIfPresent(staged))?.isFile())
|
|
334
|
+
return;
|
|
335
|
+
if (await lstatIfPresent(destination))
|
|
336
|
+
return;
|
|
337
|
+
} catch {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
try {
|
|
341
|
+
await (0, import_promises3.mkdir)((0, import_node_path3.dirname)(destination), { recursive: true });
|
|
342
|
+
await (0, import_promises3.copyFile)(staged, destination, import_node_fs.constants.COPYFILE_EXCL);
|
|
343
|
+
} catch (error) {
|
|
344
|
+
if (error.code !== "EEXIST") {
|
|
345
|
+
await (0, import_promises3.rm)(destination, { force: true }).catch(() => {
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
async stageBlobInfoCache(source, staged) {
|
|
351
|
+
try {
|
|
352
|
+
await (0, import_promises3.rm)(staged, { force: true });
|
|
353
|
+
} catch {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
try {
|
|
357
|
+
if (!(await lstatIfPresent(source))?.isFile())
|
|
358
|
+
return;
|
|
359
|
+
if (await (0, import_promises3.stat)(`${source}-journal`).catch(() => void 0))
|
|
360
|
+
return;
|
|
361
|
+
await (0, import_promises3.copyFile)(source, staged);
|
|
362
|
+
} catch {
|
|
363
|
+
await (0, import_promises3.rm)(staged, { force: true }).catch(() => {
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
async function requireRegularFile(path) {
|
|
369
|
+
const stats = await lstatIfPresent(path);
|
|
370
|
+
if (stats?.isSymbolicLink()) {
|
|
371
|
+
throw new Error(`file must not be a symlink: ${path}`);
|
|
372
|
+
}
|
|
373
|
+
if (!stats?.isFile())
|
|
374
|
+
throw new Error(`expected a regular file: ${path}`);
|
|
375
|
+
return stats;
|
|
376
|
+
}
|
|
377
|
+
async function requireDirectory(path) {
|
|
378
|
+
const stats = await lstatIfPresent(path);
|
|
379
|
+
if (stats?.isSymbolicLink()) {
|
|
380
|
+
throw new Error(`directory must not be a symlink: ${path}`);
|
|
381
|
+
}
|
|
382
|
+
if (!stats?.isDirectory())
|
|
383
|
+
throw new Error(`expected a directory: ${path}`);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// src/buildah-store-cache/command/main.ts
|
|
387
|
+
async function main() {
|
|
388
|
+
const request = JSON.parse(process.argv[2]);
|
|
389
|
+
const operations = new StoreOperations();
|
|
390
|
+
let result;
|
|
391
|
+
switch (request.action) {
|
|
392
|
+
case "restore":
|
|
393
|
+
result = await operations.restore(request.options);
|
|
394
|
+
break;
|
|
395
|
+
case "extract":
|
|
396
|
+
result = await operations.extract(request.options);
|
|
397
|
+
break;
|
|
398
|
+
case "save":
|
|
399
|
+
result = await operations.save(request.options);
|
|
400
|
+
break;
|
|
401
|
+
case "cleanup":
|
|
402
|
+
await operations.cleanup(request.options);
|
|
403
|
+
result = null;
|
|
404
|
+
break;
|
|
405
|
+
default:
|
|
406
|
+
throw new Error("unknown Buildah store command");
|
|
407
|
+
}
|
|
408
|
+
process.stdout.write(`${JSON.stringify(result)}
|
|
409
|
+
`);
|
|
410
|
+
}
|
|
411
|
+
void main().catch((error) => {
|
|
412
|
+
process.stderr.write(
|
|
413
|
+
`${error instanceof Error ? error.message : String(error)}
|
|
414
|
+
`
|
|
415
|
+
);
|
|
416
|
+
const exitCode = error?.exitCode;
|
|
417
|
+
process.exitCode = typeof exitCode === "number" && Number.isInteger(exitCode) && exitCode >= 1 && exitCode <= 255 ? exitCode : 1;
|
|
418
|
+
});
|