@outerlayer/cli 0.33.0-next.20261021171323 → 0.33.0-next.20261021194303
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 +5 -1
- package/dist/{agent-setup-NSUX64RU.js → agent-setup-P5PWEGZM.js} +5 -5
- package/dist/{build-HU24C46Z.js → build-SK5BYBER.js} +4 -4
- package/dist/build-info.json +1 -1
- package/dist/{builder-VFREWHZA.js → builder-X4PIGYS6.js} +5 -5
- package/dist/{check-3TQJDGKM.js → check-DPVNDAQQ.js} +32 -32
- package/dist/chunk-2PGI6RDE.js +90 -0
- package/dist/{chunk-ZZSQYOY5.js → chunk-3VGDZ23W.js} +1 -1
- package/dist/{chunk-7LP3F2QH.js → chunk-4OKOLFPH.js} +3 -3
- package/dist/{chunk-NJS7JJYN.js → chunk-52C3SSNF.js} +2 -2
- package/dist/{chunk-IWYS4ERC.js → chunk-533QGXGZ.js} +1 -1
- package/dist/{chunk-54GBOMDW.js → chunk-543ZX5TM.js} +3 -3
- package/dist/{chunk-OX7CHZTJ.js → chunk-5ZUT3IK3.js} +1 -1
- package/dist/{chunk-ZBN4JGTJ.js → chunk-A3H27SYX.js} +1 -1
- package/dist/{chunk-P7BEZAEU.js → chunk-ALSE7YKX.js} +11 -3
- package/dist/{chunk-G7QBSH3X.js → chunk-BBK62GXM.js} +1256 -589
- package/dist/{chunk-NYKZC6HU.js → chunk-C4N5QRQJ.js} +3 -3
- package/dist/{chunk-CI6PRMWZ.js → chunk-CG5TWQ23.js} +5 -5
- package/dist/{chunk-TZQIEUDE.js → chunk-CNRMP6EN.js} +1 -1
- package/dist/{chunk-LAS3DOTW.js → chunk-ENV3AIWQ.js} +42 -18
- package/dist/{chunk-KP6R7CV3.js → chunk-ESP5VZBD.js} +4 -1
- package/dist/{chunk-RQGHHEM7.js → chunk-HBBCQ5NE.js} +2 -2
- package/dist/{chunk-T5TBGOT6.js → chunk-IPJ6HR7E.js} +1 -1
- package/dist/{chunk-APSYTJ6I.js → chunk-JT3YO5MH.js} +1 -1
- package/dist/{chunk-ZZWWKBLL.js → chunk-KUOY5UIR.js} +4 -4
- package/dist/{chunk-X7UUIHGH.js → chunk-LH3KULPL.js} +1 -1
- package/dist/{chunk-LK2S5XZH.js → chunk-LHYO4RDP.js} +9 -1
- package/dist/{chunk-LSMF3EET.js → chunk-LPIUIRHA.js} +10 -10
- package/dist/{chunk-BXA6JLHQ.js → chunk-LTZVMKQS.js} +5 -5
- package/dist/{chunk-WHSYPR3U.js → chunk-M5ZXOQYS.js} +1 -1
- package/dist/{chunk-VXYPD33W.js → chunk-NNNUME4Y.js} +5 -5
- package/dist/{chunk-KQON2T4A.js → chunk-OVSUSOHL.js} +23 -7
- package/dist/{chunk-NHXJU7CM.js → chunk-OXSFOLWE.js} +1 -1
- package/dist/{chunk-77F7NGAZ.js → chunk-TA6TFYAN.js} +3 -3
- package/dist/{chunk-EKMMX5A5.js → chunk-TAZJDD2U.js} +4 -4
- package/dist/{chunk-TI5Y242B.js → chunk-TGXYNEI7.js} +57 -89
- package/dist/{chunk-BSHUTMXV.js → chunk-TPGHCH2H.js} +45 -19
- package/dist/{chunk-AR6UB2BP.js → chunk-UVZ43SW3.js} +3 -3
- package/dist/{chunk-HPOW2VCB.js → chunk-VFW6I4GG.js} +2 -2
- package/dist/{chunk-F3EF56LQ.js → chunk-VQ2JGZSC.js} +1 -1
- package/dist/{chunk-D73OLAYE.js → chunk-WRX2MCDV.js} +1 -1
- package/dist/{chunk-LQFDTQC2.js → chunk-XQTT3222.js} +3 -3
- package/dist/{chunk-SOTJ2V3A.js → chunk-XRZGT54P.js} +6 -6
- package/dist/{chunk-GKNF4D62.js → chunk-XT5SOJRC.js} +48 -25
- package/dist/{chunk-H6Y5D6J7.js → chunk-YZIC5JXC.js} +1 -1
- package/dist/{chunk-JKXTG3BV.js → chunk-ZPW6PUNK.js} +3 -3
- package/dist/{claude-launcher-RNMUZZ3Y.js → claude-launcher-Q7YAPLY2.js} +1 -1
- package/dist/{cli-YQ64SSV3.js → cli-OGX6JNIN.js} +98 -98
- package/dist/{command-RFVCDSJV.js → command-UIAQUHHU.js} +22 -21
- package/dist/{config-3JRFDYZL.js → config-CUJ3Q4L2.js} +1 -1
- package/dist/{connect-cmd-HVHPNVAJ.js → connect-cmd-OLESV5O3.js} +5 -5
- package/dist/{container-start-DP3N72CK.js → container-start-MGY5VFHW.js} +22 -21
- package/dist/{control-plane-launch-DHMD5WBU.js → control-plane-launch-7PXJSMVM.js} +4 -4
- package/dist/{dist-MKY2BSYN.js → dist-DY2ZNEBS.js} +1 -1
- package/dist/{docker-6G6IF4YO.js → docker-XRUAUQIG.js} +5 -5
- package/dist/{doctor-ZE4LA7EZ.js → doctor-ZMKO3F7I.js} +31 -28
- package/dist/{emit-artifact-cmd-YRRXCZAI.js → emit-artifact-cmd-RI7HA37S.js} +10 -10
- package/dist/{emit-criteria-cmd-PSQORVHS.js → emit-criteria-cmd-T5HZZ7XJ.js} +6 -6
- package/dist/{emit-demo-cmd-RJPSISCC.js → emit-demo-cmd-MWRFZHSX.js} +13 -93
- package/dist/{emit-finding-cmd-6FVW3GGK.js → emit-finding-cmd-RN7LSGJ7.js} +7 -7
- package/dist/{emit-result-cmd-PFQ6HSVN.js → emit-result-cmd-72STGWMP.js} +9 -9
- package/dist/{emit-step-cmd-U32Y4PZJ.js → emit-step-cmd-POFAEM7G.js} +7 -7
- package/dist/{findings-cmd-AMYNXO3S.js → findings-cmd-TMURLT3A.js} +7 -7
- package/dist/{fleet-readiness-7ROJ54ZF.js → fleet-readiness-N6Y3F7R6.js} +4 -4
- package/dist/{governing-source-DYJQCHKU.js → governing-source-2R7IS7LZ.js} +9 -9
- package/dist/{hook-fast-WQQYEXO5.js → hook-fast-GO7UTNN5.js} +1 -1
- package/dist/{host-key-KAYMYRO3.js → host-key-UEWE4PXB.js} +2 -2
- package/dist/{image-tag-NM2YT2J2.js → image-tag-V2LXTKK7.js} +2 -2
- package/dist/index.js +4 -4
- package/dist/{init-QDUIHSTK.js → init-E3VWTJFQ.js} +21 -20
- package/dist/{init-CHWFDK23.js → init-YMU4QFRV.js} +21 -20
- package/dist/{init-cmd-STCU6RGY.js → init-cmd-WBPE3X7T.js} +10 -10
- package/dist/{init-update-OIFOUIAN.js → init-update-MONZSG7L.js} +8 -8
- package/dist/{install-OIRIEOS7.js → install-3DBYJITP.js} +3 -3
- package/dist/{install-cmd-IYWVR6WX.js → install-cmd-EELSKBSZ.js} +4 -4
- package/dist/{logs-C3RJ3ZTC.js → logs-ET75ZUCN.js} +1 -1
- package/dist/{loop-NUW7SSZY.js → loop-N3IUN4F3.js} +32 -32
- package/dist/{machine-BJ7TT3B7.js → machine-J7QFINBX.js} +17 -16
- package/dist/{mcp-serve-cmd-PB4UOTGL.js → mcp-serve-cmd-WYUP263T.js} +3 -3
- package/dist/paths-BN7X2YPM.js +9 -0
- package/dist/{pending-RKVRYOTF.js → pending-ROAIK5WO.js} +2 -2
- package/dist/{pidfile-BMFAUQAI.js → pidfile-52TGLGQH.js} +2 -2
- package/dist/{record-26AH6WDB.js → record-BSY4BKJ5.js} +2 -2
- package/dist/{runner-env-66RAFIDJ.js → runner-env-H6XLACTG.js} +4 -4
- package/dist/{runner-identity-FL67WQIA.js → runner-identity-MSMI2PD4.js} +5 -5
- package/dist/{runner-key-YHAJFEGN.js → runner-key-QTNYQ2I7.js} +21 -20
- package/dist/{session-sender-PHQ6PJVD.js → session-sender-USH6NOZL.js} +10 -10
- package/dist/{spec-cmd-SAMHN5FU.js → spec-cmd-ABGR2ZDH.js} +7 -7
- package/dist/{status-QWLX2CCV.js → status-FHEVPRWN.js} +22 -22
- package/dist/{sync-cmd-O6UIF4QJ.js → sync-cmd-GC2TBSUO.js} +9 -9
- package/dist/{watch-5HMOSQ4Z.js → watch-F3LCBTNO.js} +9 -9
- package/dist/{work-claim-cmd-6W7BW4O6.js → work-claim-cmd-X7KNYGU7.js} +10 -10
- package/dist/{work-cmd-3G26E3NL.js → work-cmd-AI3ZPCP7.js} +7 -7
- package/dist/{work-comment-cmd-S6EGHGHL.js → work-comment-cmd-6UXS342S.js} +7 -7
- package/dist/{work-logs-cmd-RQILRNHS.js → work-logs-cmd-TVKWMDOW.js} +7 -7
- package/dist/{work-open-pr-cmd-5CDP6KOF.js → work-open-pr-cmd-WZ7W2HZ7.js} +8 -8
- package/dist/{work-pr-cmd-D3PJJQOX.js → work-pr-cmd-KTNNR46X.js} +7 -7
- package/dist/{work-stop-cmd-AGZWXGRP.js → work-stop-cmd-2BUDSXUU.js} +7 -7
- package/package.json +1 -1
- package/dist/chunk-YYYRBYWG.js +0 -32
- package/dist/paths-OK2EZS7W.js +0 -9
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
3
|
+
import { sha256OfFile, downloadPinned, VoiceDownloadError } from './chunk-2PGI6RDE.js';
|
|
4
|
+
import { runExec, execCapture, waitForAgent } from './chunk-QQGZ5THT.js';
|
|
5
|
+
import { GUEST_CID, linkVsockSockets } from './chunk-HIGJTX5U.js';
|
|
6
|
+
import { describeCallFailure, keepStagedContext, controlPlaneTokenFailure, refusalCode, resolveGoverningSource, removeStagedContext, stageGovernedContext, pinnedSourceOf, commandWithShim, shimScript, WORKFLOW_PERMISSION_REASON } from './chunk-LTZVMKQS.js';
|
|
7
|
+
import { runnerImageVersion } from './chunk-533QGXGZ.js';
|
|
7
8
|
import { hostAllowed } from './chunk-UXYLWVTQ.js';
|
|
8
|
-
import { readDefaultBranchHead, cloneRepository, buildRepositoryImage } from './chunk-
|
|
9
|
+
import { readDefaultBranchHead, cloneRepository, buildRepositoryImage, prepareImageBuild, KEY_PREFIX, FETCHED_DIR, toolsDockerfile, TAG_HOST, metadataFromLabels, writeRecord, FETCHED_FFMPEG, FETCHED_GH, FETCHED_TINI, FETCHED_VOICE_DIR } from './chunk-TPGHCH2H.js';
|
|
9
10
|
import { startTimeOf, isSameProcess } from './chunk-JBJTC3UU.js';
|
|
10
11
|
import { currentCliBuild } from './chunk-OGFGZQA3.js';
|
|
11
|
-
import { createDockerHost, builderSpec, runnerLabelValue, RUNNER_LABEL, BUILD_LABEL, prepareRunArgs, containerRunArgs, HASH_ARGV, parseProgramHash, JUDGE_PROGRAM, containerExecArgs, childEnvironment, JUDGE_DIR, MAX_ITEMS_BYTES, JUDGE_ITEMS, MAX_REPORT_BYTES, JUDGE_REPORT, spawnRun } from './chunk-
|
|
12
|
+
import { createDockerHost, builderSpec, runnerLabelValue, RUNNER_LABEL, BUILD_LABEL, prepareRunArgs, containerRunArgs, HASH_ARGV, parseProgramHash, JUDGE_PROGRAM, containerExecArgs, childEnvironment, JUDGE_DIR, MAX_ITEMS_BYTES, JUDGE_ITEMS, MAX_REPORT_BYTES, JUDGE_REPORT, spawnRun } from './chunk-KUOY5UIR.js';
|
|
12
13
|
import { readDu } from './chunk-YJLHUKYS.js';
|
|
13
|
-
import { RecipeError, DOCKER_FEATURE } from './chunk-3Z3MPR55.js';
|
|
14
|
-
import { FIRECRACKER_VERSION, hostHelperPinned, firecrackerArch, MICROVM_ARCHES, CLAUDE_CODE_VERSION, GUEST_HELPER_SHA256, GUEST_ASSETS_BASE_URL, GUEST_KERNEL_SHA256, JAILER_BINARY_SHA256, FIRECRACKER_BINARY_SHA256, FIRECRACKER_SHA256, GUEST_ASSETS_VERSION, DEFAULT_BASE_IMAGE } from './chunk-
|
|
15
|
-
import { WorkCommandError } from './chunk-
|
|
16
|
-
import { setCustomScrubbers, setLocalIdentity, setRepoRoot, parseTranscript, scrubDeep, scrubText } from './chunk-
|
|
17
|
-
import {
|
|
14
|
+
import { RecipeError, DOCKER_FEATURE, writeSanitizedRecipe, classifyProperties } from './chunk-3Z3MPR55.js';
|
|
15
|
+
import { FIRECRACKER_VERSION, hostHelperPinned, firecrackerArch, MICROVM_ARCHES, CLAUDE_CODE_VERSION, GUEST_HELPER_SHA256, GUEST_ASSETS_BASE_URL, GUEST_KERNEL_SHA256, JAILER_BINARY_SHA256, FIRECRACKER_BINARY_SHA256, FIRECRACKER_SHA256, GUEST_ASSETS_VERSION, BUILDER_DISK_VERSION, builderDiskPinned, FFMPEG_STATIC, FFMPEG_BUILD_TAG, GH_SHA256, GH_VERSION, TINI_SHA256, TINI_VERSION, VOICE_PACK_FILES, BUILDER_DISK_SHA256, BUILDER_DISK_BASE_URL, DEFAULT_BASE_IMAGE } from './chunk-LHYO4RDP.js';
|
|
16
|
+
import { WorkCommandError } from './chunk-XRZGT54P.js';
|
|
17
|
+
import { setCustomScrubbers, setLocalIdentity, setRepoRoot, parseTranscript, scrubDeep, scrubText } from './chunk-XT5SOJRC.js';
|
|
18
|
+
import { rootfsPathFor, diskTemplatesDir, RUNNER_FILE_MODE, buildsDir, buildDir, vmAssetsDir, RUNNER_DIR_MODE, deliveryDir, ghConfigDir, runnerDirOfBuild, spoolDir, mirrorDir, SHIM_DIR, runnerDir, vsockPath, builderCacheDiskPath } from './chunk-ESP5VZBD.js';
|
|
18
19
|
import { repositoriesOf, primaryOf, RELAY_PORT, repositoryPath, CHECKOUT_DIR, lifecycleWorkdir, claudeCredential, buildEnvironment, startBuildServices, checkEnvironment, AGENT_CREDENTIAL_MISSING, checkoutPath, JOB_MOUNT } from './chunk-HDSDUTX4.js';
|
|
19
20
|
import { sharedSkillNames, skillConflictMessage } from './chunk-FGF52V4V.js';
|
|
20
21
|
import { versionAtLeast, MINIMUM_CLAUDE_VERSION } from './chunk-HCVLP7XA.js';
|
|
@@ -22,14 +23,14 @@ import { orderSessions, extractFacts, sessionsToTimeline } from './chunk-JUXATN4
|
|
|
22
23
|
import { downconvertSession } from './chunk-3WW2TL7V.js';
|
|
23
24
|
import { realFdStorePort } from './chunk-XD4ZCMA4.js';
|
|
24
25
|
import { RELAY_USER, RELAY_ID, UNKNOWN_TRANSPORT_REASON, DOCKER_DESKTOP_REASON, MAX_MALFORMED_SESSIONS, MALFORMED_REASON, JOIN_WAIT_MS, MAX_OPEN_STREAMS, STREAM_RESET_RATE, STREAM_RESET_BURST, STREAM_WINDOW, SESSION_WINDOW, targetOfPath } from './chunk-MESOOLF4.js';
|
|
25
|
-
import { readFileSync, mkdirSync,
|
|
26
|
-
import { join,
|
|
26
|
+
import { statfsSync, readFileSync, existsSync, mkdirSync, chmodSync, truncateSync, rmSync, statSync, readdirSync, mkdtempSync, writeFileSync, lstatSync, copyFileSync, accessSync, constants, rmdirSync, renameSync, unlinkSync, linkSync, openSync, closeSync, cpSync, writeSync, appendFileSync, createWriteStream, createReadStream } from 'fs';
|
|
27
|
+
import { dirname, join, relative, resolve, basename } from 'path';
|
|
27
28
|
import { isIP, createConnection, connect, createServer as createServer$1 } from 'net';
|
|
28
29
|
import { spawnSync, spawn } from 'child_process';
|
|
29
|
-
import { availableParallelism, networkInterfaces } from 'os';
|
|
30
30
|
import { randomBytes, createHash } from 'crypto';
|
|
31
|
+
import { availableParallelism, networkInterfaces, tmpdir } from 'os';
|
|
32
|
+
import { Duplex, PassThrough, Transform } from 'stream';
|
|
31
33
|
import http2 from 'http2';
|
|
32
|
-
import { Duplex, Transform } from 'stream';
|
|
33
34
|
import { createServer, request } from 'http';
|
|
34
35
|
import { request as request$1 } from 'https';
|
|
35
36
|
import { pipeline, finished } from 'stream/promises';
|
|
@@ -249,6 +250,978 @@ function jailedAlive(unit, cgroupRoot = "/sys/fs/cgroup") {
|
|
|
249
250
|
function realHelperPort(env = process.env) {
|
|
250
251
|
return { probe: () => probeHelper(env[HELPER_ENV]), alive: (unit) => jailedAlive(unit) };
|
|
251
252
|
}
|
|
253
|
+
function freeBytes(path, statfs = statfsSync) {
|
|
254
|
+
let at = path;
|
|
255
|
+
for (; ; ) {
|
|
256
|
+
try {
|
|
257
|
+
const stats = statfs(at);
|
|
258
|
+
return Number(stats.bavail) * Number(stats.bsize);
|
|
259
|
+
} catch (err) {
|
|
260
|
+
if (err.code !== "ENOENT") return void 0;
|
|
261
|
+
const parent = dirname(at);
|
|
262
|
+
if (parent === at) return void 0;
|
|
263
|
+
at = parent;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
var UNITS = ["B", "KB", "MB", "GB", "TB"];
|
|
268
|
+
function formatBytes(bytes) {
|
|
269
|
+
let value = Math.max(0, bytes);
|
|
270
|
+
let unit = 0;
|
|
271
|
+
while (value >= 1024 && unit < UNITS.length - 1) {
|
|
272
|
+
value /= 1024;
|
|
273
|
+
unit++;
|
|
274
|
+
}
|
|
275
|
+
return unit === 0 ? `${Math.round(value)} B` : `${value.toFixed(1)} ${UNITS[unit]}`;
|
|
276
|
+
}
|
|
277
|
+
var STOP_GRACE_SECONDS = 15;
|
|
278
|
+
function parseImageConfig(json) {
|
|
279
|
+
const config = JSON.parse(json);
|
|
280
|
+
const env = {};
|
|
281
|
+
for (const entry of config.Env ?? []) {
|
|
282
|
+
const eq = entry.indexOf("=");
|
|
283
|
+
if (eq > 0) env[entry.slice(0, eq)] = entry.slice(eq + 1);
|
|
284
|
+
}
|
|
285
|
+
return { user: config.User ?? "", env };
|
|
286
|
+
}
|
|
287
|
+
function createContainerHost(deps = {}) {
|
|
288
|
+
const run = deps.run ?? spawnRun;
|
|
289
|
+
return {
|
|
290
|
+
async version() {
|
|
291
|
+
const result = await run("docker", ["version", "--format", "{{.Server.Version}}"]);
|
|
292
|
+
return result.status === 0 && result.stdout.trim() !== "" ? result.stdout.trim() : null;
|
|
293
|
+
},
|
|
294
|
+
async operatingSystem() {
|
|
295
|
+
const result = await run("docker", ["info", "--format", "{{.OperatingSystem}}"]);
|
|
296
|
+
return result.status === 0 && result.stdout.trim() !== "" ? result.stdout.trim() : null;
|
|
297
|
+
},
|
|
298
|
+
async imageConfig(imageId) {
|
|
299
|
+
const result = await run("docker", ["image", "inspect", "--format", "{{json .Config}}", imageId]);
|
|
300
|
+
return result.status === 0 ? parseImageConfig(result.stdout) : { user: "", env: {} };
|
|
301
|
+
},
|
|
302
|
+
createVolume(name, labels) {
|
|
303
|
+
const flags = Object.entries(labels).flatMap(([key, value]) => ["--label", `${key}=${value}`]);
|
|
304
|
+
return run("docker", ["volume", "create", ...flags, name]);
|
|
305
|
+
},
|
|
306
|
+
removeVolume(name) {
|
|
307
|
+
return run("docker", ["volume", "rm", "--force", name]);
|
|
308
|
+
},
|
|
309
|
+
run(args, opts) {
|
|
310
|
+
return run("docker", ["run", ...args], opts);
|
|
311
|
+
},
|
|
312
|
+
exec(input, opts) {
|
|
313
|
+
const env = {};
|
|
314
|
+
for (const [name, value] of Object.entries(input.env.passed)) if (value !== void 0) env[name] = value;
|
|
315
|
+
return run("docker", ["exec", ...containerExecArgs(input)], { ...opts, env, ...input.stdin === void 0 ? {} : { input: input.stdin } });
|
|
316
|
+
},
|
|
317
|
+
execDetached(input) {
|
|
318
|
+
const env = {};
|
|
319
|
+
for (const [name, value] of Object.entries(input.env.passed)) if (value !== void 0) env[name] = value;
|
|
320
|
+
return run("docker", ["exec", ...containerExecArgs({ ...input, detached: true })], { env });
|
|
321
|
+
},
|
|
322
|
+
async state(container) {
|
|
323
|
+
const result = await run("docker", ["inspect", "--format", "{{.State.Running}}", container]);
|
|
324
|
+
if (result.status !== 0) return /no such (object|container)/i.test(result.stderr) ? "missing" : "unknown";
|
|
325
|
+
return result.stdout.trim() === "true" ? "running" : "stopped";
|
|
326
|
+
},
|
|
327
|
+
execStream(input) {
|
|
328
|
+
const child = (deps.spawn ?? spawn)("docker", ["exec", ...containerExecArgs({ ...input, interactive: true })], {
|
|
329
|
+
env: childEnvironment(process.env),
|
|
330
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
331
|
+
});
|
|
332
|
+
let stderr = "";
|
|
333
|
+
child.stderr?.on("data", (chunk) => {
|
|
334
|
+
stderr = (stderr + chunk.toString()).slice(-400);
|
|
335
|
+
});
|
|
336
|
+
child.stderr?.on("error", () => void 0);
|
|
337
|
+
const exited = new Promise((resolve2) => {
|
|
338
|
+
child.once("error", () => resolve2(-1));
|
|
339
|
+
child.once("close", (code) => resolve2(code ?? -1));
|
|
340
|
+
});
|
|
341
|
+
child.stdin.on("error", () => void 0);
|
|
342
|
+
return { stdin: child.stdin, stdout: child.stdout, exited, kill: () => void child.kill("SIGKILL"), stderrTail: () => stderr };
|
|
343
|
+
},
|
|
344
|
+
stop(container) {
|
|
345
|
+
return run("docker", ["stop", "--time", String(STOP_GRACE_SECONDS), container]);
|
|
346
|
+
},
|
|
347
|
+
start(container) {
|
|
348
|
+
return run("docker", ["start", container]);
|
|
349
|
+
},
|
|
350
|
+
remove(container) {
|
|
351
|
+
return run("docker", ["rm", "--force", "--volumes", container]);
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
function converterScript(workDir = "/tmp/rootfs") {
|
|
356
|
+
return [
|
|
357
|
+
"set -eu",
|
|
358
|
+
`rootfs=${workDir}`,
|
|
359
|
+
'mkdir -p "$rootfs"',
|
|
360
|
+
'tar -xf - -C "$rootfs"',
|
|
361
|
+
'used_kb=$(du -sk "$rootfs" | cut -f1)',
|
|
362
|
+
'files=$(find "$rootfs" | wc -l)',
|
|
363
|
+
// The file is made roomy and sparse, so that populating it cannot run out of space, and is shrunk to the smallest size that holds the files afterwards.
|
|
364
|
+
"total_kb=$(( used_kb * 2 + 262144 ))",
|
|
365
|
+
'truncate -s "${total_kb}K" "/out/$OUT_NAME"',
|
|
366
|
+
'mke2fs -q -F -t ext4 -m 0 -N $(( files + files / 4 + 4096 )) -d "$rootfs" "/out/$OUT_NAME"',
|
|
367
|
+
// e2fsck exits 1 when it repaired something; resize2fs -M needs the check to have run.
|
|
368
|
+
'e2fsck -f -y "/out/$OUT_NAME" > /dev/null || [ $? -lt 4 ]',
|
|
369
|
+
'resize2fs -M "/out/$OUT_NAME" > /dev/null 2>&1',
|
|
370
|
+
'chown "$HOST_UID:$HOST_GID" "/out/$OUT_NAME"',
|
|
371
|
+
// In the builder microVM the extracted file system sits on the cache disk, which the next builder reuses.
|
|
372
|
+
...workDir === "/tmp/rootfs" ? [] : ['rm -rf "$rootfs"']
|
|
373
|
+
].join("\n");
|
|
374
|
+
}
|
|
375
|
+
function templateScript() {
|
|
376
|
+
return [
|
|
377
|
+
"set -eu",
|
|
378
|
+
'truncate -s "$BYTES" "/out/$OUT_NAME"',
|
|
379
|
+
'mke2fs -q -F -t ext4 -m 0 -E lazy_itable_init=1,lazy_journal_init=1,nodiscard "/out/$OUT_NAME"',
|
|
380
|
+
'chown "$HOST_UID:$HOST_GID" "/out/$OUT_NAME"'
|
|
381
|
+
].join("\n");
|
|
382
|
+
}
|
|
383
|
+
var ids = () => ({ uid: String(process.getuid?.() ?? 0), gid: String(process.getgid?.() ?? 0) });
|
|
384
|
+
function converterArgs(dest, memoryBytes, extra, script) {
|
|
385
|
+
const { uid, gid } = ids();
|
|
386
|
+
return [
|
|
387
|
+
"run",
|
|
388
|
+
"--rm",
|
|
389
|
+
"-i",
|
|
390
|
+
"--network",
|
|
391
|
+
"none",
|
|
392
|
+
"--memory",
|
|
393
|
+
String(memoryBytes),
|
|
394
|
+
"--memory-swap",
|
|
395
|
+
String(memoryBytes),
|
|
396
|
+
"--mount",
|
|
397
|
+
`type=bind,src=${dirname(dest)},dst=/out`,
|
|
398
|
+
"-e",
|
|
399
|
+
`OUT_NAME=${basename(dest)}`,
|
|
400
|
+
"-e",
|
|
401
|
+
`HOST_UID=${uid}`,
|
|
402
|
+
"-e",
|
|
403
|
+
`HOST_GID=${gid}`,
|
|
404
|
+
...extra,
|
|
405
|
+
// The image that runs `mke2fs` as root is the default base image: Ubuntu holds e2fsprogs.
|
|
406
|
+
DEFAULT_BASE_IMAGE,
|
|
407
|
+
"sh",
|
|
408
|
+
"-c",
|
|
409
|
+
script
|
|
410
|
+
];
|
|
411
|
+
}
|
|
412
|
+
var firstLine = (text) => text.trim().split("\n")[0] ?? "";
|
|
413
|
+
function exportInto(container, converter) {
|
|
414
|
+
return new Promise((resolve2, reject) => {
|
|
415
|
+
const exporter = spawn("docker", ["export", container], { stdio: ["ignore", "pipe", "pipe"], env: childEnvironment(process.env) });
|
|
416
|
+
const writer = spawn("docker", converter, { stdio: ["pipe", "ignore", "pipe"], env: childEnvironment(process.env) });
|
|
417
|
+
const errors = [];
|
|
418
|
+
exporter.stderr.on("data", (chunk) => errors.push(`docker export: ${chunk.toString("utf8")}`));
|
|
419
|
+
writer.stderr.on("data", (chunk) => errors.push(`converter: ${chunk.toString("utf8")}`));
|
|
420
|
+
writer.stdin.on("error", () => void 0);
|
|
421
|
+
exporter.stdout.on("error", () => void 0);
|
|
422
|
+
exporter.stdout.pipe(writer.stdin);
|
|
423
|
+
exporter.once("error", reject);
|
|
424
|
+
writer.once("error", reject);
|
|
425
|
+
let exits = 0;
|
|
426
|
+
let failure2;
|
|
427
|
+
const done = (name, code) => {
|
|
428
|
+
if (code !== 0 && failure2 === void 0) failure2 = `${name} exited ${code}`;
|
|
429
|
+
exits += 1;
|
|
430
|
+
if (exits < 2) return;
|
|
431
|
+
if (failure2 === void 0) resolve2();
|
|
432
|
+
else reject(new Error(`${failure2}: ${firstLine(errors.join("\n"))}`));
|
|
433
|
+
};
|
|
434
|
+
exporter.once("exit", (code) => done("docker export", code));
|
|
435
|
+
writer.once("exit", (code) => done("the converter", code));
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
function createRootfsHost() {
|
|
439
|
+
const env = () => childEnvironment(process.env);
|
|
440
|
+
return {
|
|
441
|
+
async convert({ imageId, dest, memoryBytes }) {
|
|
442
|
+
const created = await spawnRun("docker", ["create", imageId, "none"], { env: env() });
|
|
443
|
+
if (created.status !== 0) throw new Error(`docker create ${imageId} failed: ${firstLine(created.stderr)}`);
|
|
444
|
+
const container = created.stdout.trim();
|
|
445
|
+
try {
|
|
446
|
+
await exportInto(container, converterArgs(dest, memoryBytes, [], converterScript()));
|
|
447
|
+
} finally {
|
|
448
|
+
await spawnRun("docker", ["rm", "--force", "--volumes", container], { env: env() });
|
|
449
|
+
}
|
|
450
|
+
},
|
|
451
|
+
async makeTemplate({ bytes, dest, memoryBytes }) {
|
|
452
|
+
const made = await spawnRun("docker", converterArgs(dest, memoryBytes, ["-e", `BYTES=${bytes}`], templateScript()), { env: env() });
|
|
453
|
+
if (made.status !== 0) throw new Error(`making the empty build disk failed: ${firstLine(made.stderr)}`);
|
|
454
|
+
},
|
|
455
|
+
async imageConfig(imageId) {
|
|
456
|
+
const inspected = await spawnRun("docker", ["image", "inspect", "--format", "{{json .Config}}", imageId], { env: env() });
|
|
457
|
+
if (inspected.status !== 0) throw new Error(`docker image inspect ${imageId} failed: ${firstLine(inspected.stderr)}`);
|
|
458
|
+
return parseImageConfig(inspected.stdout);
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
function sweepStalePartials(dir) {
|
|
463
|
+
let names;
|
|
464
|
+
try {
|
|
465
|
+
names = readdirSync(dir);
|
|
466
|
+
} catch {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
for (const name of names) {
|
|
470
|
+
const pid = /\.partial-(\d+)-[0-9a-f]+$/.exec(name)?.[1];
|
|
471
|
+
if (pid === void 0 || Number(pid) === process.pid) continue;
|
|
472
|
+
try {
|
|
473
|
+
process.kill(Number(pid), 0);
|
|
474
|
+
} catch (err) {
|
|
475
|
+
if (err.code === "ESRCH") rmSync(join(dir, name), { force: true });
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
async function madeAtomically(path, make) {
|
|
480
|
+
mkdirSync(dirname(path), { recursive: true, mode: RUNNER_DIR_MODE });
|
|
481
|
+
const temp = `${path}.partial-${process.pid}-${randomBytes(3).toString("hex")}`;
|
|
482
|
+
sweepStalePartials(dirname(path));
|
|
483
|
+
try {
|
|
484
|
+
await make(temp);
|
|
485
|
+
renameSync(temp, path);
|
|
486
|
+
} catch (err) {
|
|
487
|
+
rmSync(temp, { force: true });
|
|
488
|
+
throw err;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
async function ensureRootfs(input) {
|
|
492
|
+
const path = rootfsPathFor(input.runnerDir, input.imageId);
|
|
493
|
+
if (existsSync(path)) return path;
|
|
494
|
+
const { uid, gid } = ids();
|
|
495
|
+
await madeAtomically(path, (temp) => input.host.convert({ imageId: input.imageId, dest: temp, uid: Number(uid), gid: Number(gid), memoryBytes: input.memoryBytes }));
|
|
496
|
+
return path;
|
|
497
|
+
}
|
|
498
|
+
async function createBuildDisk(input) {
|
|
499
|
+
const template = join(diskTemplatesDir(input.runnerDir), `${input.bytes}.ext4`);
|
|
500
|
+
if (!existsSync(template)) {
|
|
501
|
+
const { uid, gid } = ids();
|
|
502
|
+
await madeAtomically(template, (temp) => input.host.makeTemplate({ bytes: input.bytes, dest: temp, uid: Number(uid), gid: Number(gid), memoryBytes: input.memoryBytes }));
|
|
503
|
+
}
|
|
504
|
+
mkdirSync(dirname(input.dest), { recursive: true });
|
|
505
|
+
if (input.host.copyTemplate) input.host.copyTemplate(template, input.dest);
|
|
506
|
+
else {
|
|
507
|
+
const copied = spawnSync("cp", ["--sparse=always", template, input.dest], { encoding: "utf8" });
|
|
508
|
+
if (copied.status !== 0) throw new Error(`copying the empty build disk failed: ${firstLine(copied.stderr)}`);
|
|
509
|
+
}
|
|
510
|
+
chmodSync(input.dest, RUNNER_FILE_MODE);
|
|
511
|
+
truncateSync(input.dest, input.bytes);
|
|
512
|
+
}
|
|
513
|
+
function deleteBuildDisk(path) {
|
|
514
|
+
rmSync(path, { force: true });
|
|
515
|
+
}
|
|
516
|
+
var CONSOLE_CUT_NOTE = "[outerlayer runner: cut the console at 8 MiB; the microVM keeps running]\n";
|
|
517
|
+
function pumpConsole(fd, streams) {
|
|
518
|
+
const cap = 8 * 1024 ** 2;
|
|
519
|
+
let written = 0;
|
|
520
|
+
let lastByte = 10;
|
|
521
|
+
let cut = false;
|
|
522
|
+
const append = (data) => {
|
|
523
|
+
try {
|
|
524
|
+
writeSync(fd, data);
|
|
525
|
+
} catch {
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
const onData = (chunk) => {
|
|
529
|
+
if (cut) return;
|
|
530
|
+
const room = cap - written;
|
|
531
|
+
if (chunk.length <= room) {
|
|
532
|
+
append(chunk);
|
|
533
|
+
written += chunk.length;
|
|
534
|
+
lastByte = chunk[chunk.length - 1] ?? lastByte;
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
const head = chunk.subarray(0, room);
|
|
538
|
+
append(head);
|
|
539
|
+
if (head.length > 0) lastByte = head[head.length - 1];
|
|
540
|
+
append(Buffer.from(`${lastByte === 10 ? "" : "\n"}${CONSOLE_CUT_NOTE}`));
|
|
541
|
+
written = cap;
|
|
542
|
+
cut = true;
|
|
543
|
+
};
|
|
544
|
+
let open = streams.length;
|
|
545
|
+
for (const stream of streams) {
|
|
546
|
+
stream.on("data", onData);
|
|
547
|
+
stream.on("error", () => void 0);
|
|
548
|
+
stream.once("close", () => {
|
|
549
|
+
open -= 1;
|
|
550
|
+
if (open === 0) closeSync(fd);
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// src/runner/microvm/firecracker.ts
|
|
556
|
+
var clearBit = (bit) => `0b${"x".repeat(31 - bit)}0${"x".repeat(bit)}`;
|
|
557
|
+
var CPU_TEMPLATE = {
|
|
558
|
+
cpuid_modifiers: [
|
|
559
|
+
{ leaf: "0x1", subleaf: "0x0", flags: 0, modifiers: [{ register: "ecx", bitmap: clearBit(5) }] },
|
|
560
|
+
{ leaf: "0x80000001", subleaf: "0x0", flags: 0, modifiers: [{ register: "ecx", bitmap: clearBit(2) }] }
|
|
561
|
+
]
|
|
562
|
+
};
|
|
563
|
+
var MIB = 1024 ** 2;
|
|
564
|
+
var INIT = "/opt/outerlayer/bin/outerlayer-guest";
|
|
565
|
+
function machineConfig(limits2, paths) {
|
|
566
|
+
const bootArgs = [
|
|
567
|
+
"console=ttyS0",
|
|
568
|
+
"reboot=k",
|
|
569
|
+
"panic=1",
|
|
570
|
+
"pci=off",
|
|
571
|
+
"i8042.noaux",
|
|
572
|
+
"i8042.nomux",
|
|
573
|
+
"i8042.nopnp",
|
|
574
|
+
"i8042.dumbkbd",
|
|
575
|
+
// The pinned guest kernel builds pressure stall information default-disabled; the guest usage record reads it.
|
|
576
|
+
"psi=1",
|
|
577
|
+
"ro",
|
|
578
|
+
"root=/dev/vda",
|
|
579
|
+
"rootfstype=ext4",
|
|
580
|
+
`init=${INIT}`,
|
|
581
|
+
...paths.relayPorts.length === 0 ? [] : [`outerlayer.relay=${paths.relayPorts.join(",")}`],
|
|
582
|
+
...paths.leasePort === void 0 ? [] : [`outerlayer.lease=${paths.leasePort}`],
|
|
583
|
+
...paths.docker === true ? ["outerlayer.docker=1"] : [],
|
|
584
|
+
// The kernel passes an argument of this form on to the init process as an environment variable.
|
|
585
|
+
...paths.guestInit === void 0 ? [] : [`OUTERLAYER_GUEST_VM_INIT=${paths.guestInit}`]
|
|
586
|
+
];
|
|
587
|
+
return {
|
|
588
|
+
"boot-source": { kernel_image_path: paths.kernel, boot_args: bootArgs.join(" ") },
|
|
589
|
+
drives: [
|
|
590
|
+
{ drive_id: "rootfs", path_on_host: paths.rootfs, is_root_device: true, is_read_only: true },
|
|
591
|
+
{ drive_id: "build", path_on_host: paths.buildDisk, is_root_device: false, is_read_only: false }
|
|
592
|
+
],
|
|
593
|
+
"machine-config": { vcpu_count: limits2.cpus, mem_size_mib: Math.max(1, Math.floor(limits2.memoryBytes / MIB)), smt: false },
|
|
594
|
+
vsock: { guest_cid: GUEST_CID, uds_path: paths.vsock },
|
|
595
|
+
"cpu-config": CPU_TEMPLATE
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
function firecrackerCommand(input) {
|
|
599
|
+
const args = ["--no-api", "--config-file", input.configPath];
|
|
600
|
+
const binary = resolve(input.binary);
|
|
601
|
+
if (input.cgroup === void 0) return { command: binary, args };
|
|
602
|
+
return { command: "/bin/sh", args: ["-c", 'echo $$ > "$1/cgroup.procs" && shift && exec "$@"', "sh", input.cgroup, binary, ...args] };
|
|
603
|
+
}
|
|
604
|
+
var STOP_WAIT_MS = 1e4;
|
|
605
|
+
var POLL_MS = 20;
|
|
606
|
+
function isZombie(pid) {
|
|
607
|
+
try {
|
|
608
|
+
const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
609
|
+
return stat.slice(stat.lastIndexOf(")") + 2, stat.lastIndexOf(")") + 3) === "Z";
|
|
610
|
+
} catch {
|
|
611
|
+
return false;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
function createFirecrackerHost(options = {}) {
|
|
615
|
+
const launch = options.launch ?? firecrackerCommand;
|
|
616
|
+
const spawnChild = options.spawnChild ?? spawn;
|
|
617
|
+
const exits = /* @__PURE__ */ new Map();
|
|
618
|
+
const alive = (vm) => isSameProcess(vm.pid, vm.start) && !isZombie(vm.pid);
|
|
619
|
+
return {
|
|
620
|
+
async spawn(input) {
|
|
621
|
+
const { command, args } = launch(input);
|
|
622
|
+
mkdirSync(dirname(input.consolePath), { recursive: true });
|
|
623
|
+
const consoleFd = openSync(input.consolePath, "a", 384);
|
|
624
|
+
let pumping = false;
|
|
625
|
+
try {
|
|
626
|
+
const child = spawnChild(command, args, { detached: true, stdio: ["ignore", "pipe", "pipe"], env: {} });
|
|
627
|
+
pumpConsole(consoleFd, [child.stdout, child.stderr]);
|
|
628
|
+
pumping = true;
|
|
629
|
+
for (const stream of [child.stdout, child.stderr]) stream.unref?.();
|
|
630
|
+
const failed = new Promise((_resolve, reject) => child.once("error", reject));
|
|
631
|
+
const pid = child.pid;
|
|
632
|
+
if (pid === void 0) return await failed;
|
|
633
|
+
exits.set(pid, new Promise((resolve2) => child.once("exit", () => resolve2())));
|
|
634
|
+
child.unref();
|
|
635
|
+
const start = startTimeOf(pid);
|
|
636
|
+
if (start === null) throw new Error(`${command} exited as soon as it started; its console is at ${input.consolePath}`);
|
|
637
|
+
return { pid, start };
|
|
638
|
+
} finally {
|
|
639
|
+
if (!pumping) closeSync(consoleFd);
|
|
640
|
+
}
|
|
641
|
+
},
|
|
642
|
+
async stop(vm) {
|
|
643
|
+
if (!alive(vm)) return;
|
|
644
|
+
try {
|
|
645
|
+
process.kill(vm.pid, "SIGKILL");
|
|
646
|
+
} catch {
|
|
647
|
+
}
|
|
648
|
+
const exited = exits.get(vm.pid);
|
|
649
|
+
const deadline = Date.now() + STOP_WAIT_MS;
|
|
650
|
+
if (exited !== void 0) await Promise.race([exited, new Promise((resolve2) => setTimeout(resolve2, STOP_WAIT_MS))]);
|
|
651
|
+
while (alive(vm) && Date.now() < deadline) await new Promise((resolve2) => setTimeout(resolve2, POLL_MS));
|
|
652
|
+
exits.delete(vm.pid);
|
|
653
|
+
},
|
|
654
|
+
alive
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
function unitNameFor(buildId) {
|
|
658
|
+
return `outerlayer-build-${buildId}`;
|
|
659
|
+
}
|
|
660
|
+
function linkIn(buildDir2, name, source) {
|
|
661
|
+
const target = join(buildDir2, name);
|
|
662
|
+
rmSync(target, { force: true });
|
|
663
|
+
try {
|
|
664
|
+
linkSync(source, target);
|
|
665
|
+
} catch (err) {
|
|
666
|
+
if (err.code === "EXDEV") {
|
|
667
|
+
throw new Error(`${source} and ${buildDir2} are on different file systems, so the build's files cannot be linked into its jail`);
|
|
668
|
+
}
|
|
669
|
+
throw err;
|
|
670
|
+
}
|
|
671
|
+
chmodSync(source, 292);
|
|
672
|
+
}
|
|
673
|
+
async function startJailed(input) {
|
|
674
|
+
linkIn(input.buildDir, "kernel", input.kernel);
|
|
675
|
+
linkIn(input.buildDir, "rootfs.ext4", input.rootfs);
|
|
676
|
+
for (const name of input.sockets) {
|
|
677
|
+
try {
|
|
678
|
+
chmodSync(join(input.buildDir, name), 438);
|
|
679
|
+
} catch (err) {
|
|
680
|
+
if (err.code !== "ENOENT") throw err;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return input.client.start({ build: input.buildId, kind: input.kind, ...input.timeLimitMinutes === void 0 ? {} : { timeLimitMinutes: input.timeLimitMinutes } });
|
|
684
|
+
}
|
|
685
|
+
async function startJailedInto(state, input, save) {
|
|
686
|
+
state.jailed = { unit: unitNameFor(state.buildId), chroot: "" };
|
|
687
|
+
save();
|
|
688
|
+
try {
|
|
689
|
+
const started = await startJailed({ ...input, buildId: state.buildId, buildDir: state.buildDir });
|
|
690
|
+
state.jailed = { unit: started.unit, chroot: started.chroot };
|
|
691
|
+
state.vsockPath = started.vsock;
|
|
692
|
+
} catch (err) {
|
|
693
|
+
if (err instanceof HelperRefusal) {
|
|
694
|
+
delete state.jailed;
|
|
695
|
+
save();
|
|
696
|
+
}
|
|
697
|
+
throw err;
|
|
698
|
+
}
|
|
699
|
+
save();
|
|
700
|
+
}
|
|
701
|
+
async function stopJailed(helper, state) {
|
|
702
|
+
const probe = await helper.probe();
|
|
703
|
+
if (!probe.ok) throw new Error(`the build's unit ${state.jailed?.unit ?? unitNameFor(state.buildId)} could not be stopped, because there is no helper to ask: ${probe.detail}`);
|
|
704
|
+
try {
|
|
705
|
+
await probe.client.stop(state.buildId);
|
|
706
|
+
} catch (err) {
|
|
707
|
+
throw new Error(`the build's unit ${state.jailed?.unit ?? unitNameFor(state.buildId)} was not stopped: ${describeCallFailure(err)}`);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
function makeBuildsDirPrivate(buildDir2) {
|
|
711
|
+
const builds = dirname(buildDir2);
|
|
712
|
+
const stat = statSync(builds);
|
|
713
|
+
if (stat.uid === process.getuid?.() && (stat.mode & 18) !== 0) chmodSync(builds, 448);
|
|
714
|
+
}
|
|
715
|
+
var MANAGED_UNJAILED_REASON = "the root helper does not answer, and a managed runner starts no build without the jailer. The build did not start; restore the helper with `outerlayer runner install --service --replace`";
|
|
716
|
+
|
|
717
|
+
// src/runner/microvm/image-builder.ts
|
|
718
|
+
var ROOT_DISK_ID_PREFIX = "rootdisk:";
|
|
719
|
+
var BUILDER_INIT = "/opt/outerlayer/builder-init";
|
|
720
|
+
var TUNNEL_PORT = RELAY_PORT;
|
|
721
|
+
var ENGINE_PROXY = `http://127.0.0.1:${TUNNEL_PORT}`;
|
|
722
|
+
var BRIDGE_ADDRESS = "172.17.0.1";
|
|
723
|
+
var STEP_PROXY = `http://${BRIDGE_ADDRESS}:${TUNNEL_PORT}`;
|
|
724
|
+
var NO_PROXY = "localhost,127.0.0.1,::1";
|
|
725
|
+
var TUNNEL_SOCKET = "tunnel.sock";
|
|
726
|
+
var RECORD_FILE = "image-builder.json";
|
|
727
|
+
var BOOT_ATTEMPTS = 120;
|
|
728
|
+
var BOOT_POLL_MS = 500;
|
|
729
|
+
var ROOT_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
|
730
|
+
var KEEP_RECORDS = 3;
|
|
731
|
+
var LOG_TAIL_LINES = 30;
|
|
732
|
+
var MAX_PENDING_BYTES = 64 * 1024;
|
|
733
|
+
var MAX_LOG_BYTES = 64 * 1024 * 1024;
|
|
734
|
+
var RESULT_PREFIX = "OL-RESULT ";
|
|
735
|
+
var REFUSALS_NAMED = 5;
|
|
736
|
+
var GUEST_OUTPUT = "/out/rootfs.ext4";
|
|
737
|
+
var DOCKER_WAIT_SECONDS = 90;
|
|
738
|
+
var BUILD_CACHE_KEEP = "2GB";
|
|
739
|
+
function builderMachineConfig(limits2, paths) {
|
|
740
|
+
return machineConfig(limits2, { ...paths, guestInit: BUILDER_INIT });
|
|
741
|
+
}
|
|
742
|
+
function daemonConfig() {
|
|
743
|
+
return { "data-root": "/cache/docker", bip: `${BRIDGE_ADDRESS}/16`, proxies: { "http-proxy": ENGINE_PROXY, "https-proxy": ENGINE_PROXY, "no-proxy": NO_PROXY } };
|
|
744
|
+
}
|
|
745
|
+
function clientConfig() {
|
|
746
|
+
return { proxies: { default: { httpProxy: STEP_PROXY, httpsProxy: STEP_PROXY, noProxy: NO_PROXY } } };
|
|
747
|
+
}
|
|
748
|
+
function plain(name, value, allowed) {
|
|
749
|
+
if (!allowed.test(value)) throw new Error(`${name} holds a character the builder's script cannot carry safely: ${JSON.stringify(value)}`);
|
|
750
|
+
return value;
|
|
751
|
+
}
|
|
752
|
+
function builderGuestScript(parts) {
|
|
753
|
+
const recipeTag = plain("the recipe tag", parts.recipeTag, /^[A-Za-z0-9_.:@/-]+$/);
|
|
754
|
+
const buildTag = plain("the image tag", parts.buildTag, /^[A-Za-z0-9_.:@/-]+$/);
|
|
755
|
+
const build = parts.recipe.kind === "default" ? `docker build --progress plain --tag ${recipeTag} --file /work/recipe/Dockerfile /work/recipe` : `devcontainer build --workspace-folder /work/repo --config /work/repo/${plain("the recipe path", parts.recipe.config, /^[A-Za-z0-9_./-]+$/)} --image-name ${recipeTag} --log-format text`;
|
|
756
|
+
return [
|
|
757
|
+
"set -eu",
|
|
758
|
+
`export PATH=${ROOT_PATH}`,
|
|
759
|
+
// The devcontainer CLI, BuildKit and the engine read the environment; Node's fetch reads it only when asked.
|
|
760
|
+
`export HTTP_PROXY=${ENGINE_PROXY}`,
|
|
761
|
+
`export HTTPS_PROXY=${ENGINE_PROXY}`,
|
|
762
|
+
`export http_proxy=${ENGINE_PROXY}`,
|
|
763
|
+
`export https_proxy=${ENGINE_PROXY}`,
|
|
764
|
+
`export NO_PROXY=${NO_PROXY}`,
|
|
765
|
+
`export no_proxy=${NO_PROXY}`,
|
|
766
|
+
"export NODE_USE_ENV_PROXY=1",
|
|
767
|
+
"export DOCKER_CONFIG=/run/builder/docker DOCKER_BUILDKIT=1",
|
|
768
|
+
"mkdir -p /run/builder/docker /cache/docker",
|
|
769
|
+
// Whatever way the script ends, the cache keeps the recipe image and a bounded build cache, so a repository's cache disk cannot fill with the images of old recipes.
|
|
770
|
+
"trim() {",
|
|
771
|
+
` docker image ls --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -vxF -e '<none>:<none>' -e ${recipeTag} | xargs -r docker rmi > /dev/null 2>&1 || true`,
|
|
772
|
+
" docker image prune --force > /dev/null 2>&1 || true",
|
|
773
|
+
` docker builder prune --force --keep-storage ${BUILD_CACHE_KEEP} > /dev/null 2>&1 || true`,
|
|
774
|
+
"}",
|
|
775
|
+
"trap trim EXIT",
|
|
776
|
+
"cat > /run/builder/daemon.json <<'OL_DAEMON_JSON'",
|
|
777
|
+
JSON.stringify(daemonConfig(), null, 2),
|
|
778
|
+
"OL_DAEMON_JSON",
|
|
779
|
+
"cat > /run/builder/docker/config.json <<'OL_CLIENT_JSON'",
|
|
780
|
+
JSON.stringify(clientConfig(), null, 2),
|
|
781
|
+
"OL_CLIENT_JSON",
|
|
782
|
+
"cat > /run/builder/convert.sh <<'OL_CONVERT'",
|
|
783
|
+
converterScript("/cache/rootfs"),
|
|
784
|
+
"OL_CONVERT",
|
|
785
|
+
// The forwarder binds the bridge address before the engine makes the bridge.
|
|
786
|
+
"echo 1 > /proc/sys/net/ipv4/ip_nonlocal_bind",
|
|
787
|
+
"mount -t cgroup2 cgroup2 /sys/fs/cgroup 2>/dev/null || true",
|
|
788
|
+
`node /work/tools/dist/index.js vm-forward --listen ${BRIDGE_ADDRESS}:${TUNNEL_PORT} --to 127.0.0.1:${TUNNEL_PORT} > /run/builder/forward.log 2>&1 &`,
|
|
789
|
+
"dockerd --config-file /run/builder/daemon.json > /run/builder/dockerd.log 2>&1 &",
|
|
790
|
+
"waited=0",
|
|
791
|
+
"until docker info > /dev/null 2>&1; do",
|
|
792
|
+
" waited=$((waited + 1))",
|
|
793
|
+
` if [ "$waited" -gt ${DOCKER_WAIT_SECONDS} ]; then echo "the Docker engine in the builder did not start:"; tail -n 20 /run/builder/dockerd.log; exit 1; fi`,
|
|
794
|
+
" sleep 1",
|
|
795
|
+
"done",
|
|
796
|
+
build,
|
|
797
|
+
`docker build --progress plain --tag ${buildTag} --file /work/tools/Dockerfile /work/tools`,
|
|
798
|
+
`docker image inspect --format '{{json .Config}}' ${buildTag} > /run/builder/config.json`,
|
|
799
|
+
// The conversion puts the extracted file system and the root disk beside the layers. The inputs and Docker's copies
|
|
800
|
+
// of the build contexts are not needed for it, and the tool files in them are large, so they go first.
|
|
801
|
+
"rm -rf /work/repo /work/recipe /work/tools/fetched",
|
|
802
|
+
"docker builder prune --force --filter type=source.local > /dev/null 2>&1 || true",
|
|
803
|
+
`container=$(docker create ${buildTag} none)`,
|
|
804
|
+
'docker export "$container" | OUT_NAME=rootfs.ext4 HOST_UID=0 HOST_GID=0 sh /run/builder/convert.sh',
|
|
805
|
+
'docker rm --force --volumes "$container" > /dev/null',
|
|
806
|
+
`printf '${RESULT_PREFIX}{"config":%s,"bytes":%s}\\n' "$(cat /run/builder/config.json)" "$(stat -c %s ${GUEST_OUTPUT})"`,
|
|
807
|
+
""
|
|
808
|
+
].join("\n");
|
|
809
|
+
}
|
|
810
|
+
function tarArchive(dir, exclude) {
|
|
811
|
+
const child = spawn("tar", ["-C", dir, ...exclude.map((name) => `--exclude=${name}`), "-cf", "-", "."], { stdio: ["ignore", "pipe", "pipe"] });
|
|
812
|
+
const errors = [];
|
|
813
|
+
child.stderr.on("data", (chunk) => errors.push(chunk));
|
|
814
|
+
const out = new PassThrough();
|
|
815
|
+
child.stdout.pipe(out, { end: false });
|
|
816
|
+
child.once("error", (err) => out.destroy(err));
|
|
817
|
+
out.once("close", () => child.kill());
|
|
818
|
+
child.once("close", (code) => {
|
|
819
|
+
if (code === 0) out.end();
|
|
820
|
+
else out.destroy(new Error(`tar could not pack ${dir}: ${Buffer.concat(errors).toString("utf8").trim().split("\n")[0] ?? `exit ${code}`}`));
|
|
821
|
+
});
|
|
822
|
+
return out;
|
|
823
|
+
}
|
|
824
|
+
function builderInput(parts, archive = tarArchive) {
|
|
825
|
+
return [
|
|
826
|
+
{ target: "/work/repo", stream: archive(parts.checkoutDir, [".git"]) },
|
|
827
|
+
{ target: "/work/tools", stream: archive(parts.toolsDir, []) },
|
|
828
|
+
...parts.toolFilesDir === void 0 ? [] : [{ target: `/work/tools/${FETCHED_DIR}`, stream: archive(parts.toolFilesDir, []) }],
|
|
829
|
+
...parts.recipeDir === void 0 ? [] : [{ target: "/work/recipe", stream: archive(parts.recipeDir, []) }]
|
|
830
|
+
];
|
|
831
|
+
}
|
|
832
|
+
function parseCacheSpace(stdout) {
|
|
833
|
+
const fields = stdout.trim().split("\n").at(-1)?.trim().split(/\s+/) ?? [];
|
|
834
|
+
if (fields.length < 6 || fields[5] !== "/cache") return void 0;
|
|
835
|
+
const total = Number(fields[1]) * 1024;
|
|
836
|
+
const free = Number(fields[3]) * 1024;
|
|
837
|
+
return Number.isFinite(total) && Number.isFinite(free) && total > 0 ? { free, total } : void 0;
|
|
838
|
+
}
|
|
839
|
+
var StepFailed = class extends Error {
|
|
840
|
+
};
|
|
841
|
+
function readStored(prepared) {
|
|
842
|
+
const path = join(prepared.recordDir, "rootfs.ext4");
|
|
843
|
+
const recordPath = join(prepared.recordDir, "rootdisk.json");
|
|
844
|
+
if (!existsSync(path) || !existsSync(recordPath)) return void 0;
|
|
845
|
+
try {
|
|
846
|
+
const stored = JSON.parse(readFileSync(recordPath, "utf8"));
|
|
847
|
+
if (typeof stored.user !== "string" || typeof stored.env !== "object" || stored.env === null || typeof stored.report !== "object" || stored.report === null) return void 0;
|
|
848
|
+
if (statSync(path).size !== stored.bytes) return void 0;
|
|
849
|
+
return {
|
|
850
|
+
reused: true,
|
|
851
|
+
recipeBuilt: false,
|
|
852
|
+
toolsBuilt: false,
|
|
853
|
+
checkedCommit: prepared.checkout.commit,
|
|
854
|
+
recordDir: prepared.recordDir,
|
|
855
|
+
report: stored.report,
|
|
856
|
+
removed: [],
|
|
857
|
+
rootfs: { path, env: stored.env, user: stored.user }
|
|
858
|
+
};
|
|
859
|
+
} catch {
|
|
860
|
+
return void 0;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
function writeBuilderRecord(dir, record) {
|
|
864
|
+
writeFileSync(join(dir, RECORD_FILE), `${JSON.stringify(record)}
|
|
865
|
+
`, { mode: 384 });
|
|
866
|
+
}
|
|
867
|
+
function refusalReason(refusals, logPath) {
|
|
868
|
+
const named = refusals.slice(0, REFUSALS_NAMED).map((r) => r.range === "not-allowed" ? `${r.host} (not in runner.build.allowHosts)` : `${r.host} (${r.range})`);
|
|
869
|
+
const more = refusals.length > REFUSALS_NAMED ? ` and ${refusals.length - REFUSALS_NAMED} more` : "";
|
|
870
|
+
return `the image build called ${named.join(", ")}${more}, which the tunnel refuses. An image build reaches only the public hosts in runner.build.allowHosts, and no private address. The build log is at ${logPath}`;
|
|
871
|
+
}
|
|
872
|
+
function pruneRecords(repoRecords, current) {
|
|
873
|
+
const recordNames = readdirSync(repoRecords).filter((name) => /^[0-9a-f]{12}-[0-9a-f]{12}$/.test(name) && join(repoRecords, name) !== current);
|
|
874
|
+
const aged = recordNames.map((name) => ({ name, at: statSync(join(repoRecords, name)).mtimeMs })).sort((a, b) => b.at - a.at);
|
|
875
|
+
for (const { name } of aged.slice(KEEP_RECORDS - 1)) rmSync(join(repoRecords, name), { recursive: true, force: true });
|
|
876
|
+
}
|
|
877
|
+
async function buildRootDiskInMicroVm(opts, deps) {
|
|
878
|
+
const prepared = await (deps.prepare ?? prepareImageBuild)(opts);
|
|
879
|
+
try {
|
|
880
|
+
const stored = readStored(prepared);
|
|
881
|
+
if (stored !== void 0) return stored;
|
|
882
|
+
return await buildInBuilder(opts, deps, prepared);
|
|
883
|
+
} finally {
|
|
884
|
+
prepared.release();
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
async function buildInBuilder(opts, deps, prepared) {
|
|
888
|
+
const { ports } = deps;
|
|
889
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms)));
|
|
890
|
+
const archive = deps.archive ?? tarArchive;
|
|
891
|
+
const now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
892
|
+
const { repository, owner, name, checkout, location, recipe, repoRecords, recordDir, logPath } = prepared;
|
|
893
|
+
const rKey = prepared.recipeKey.slice(0, KEY_PREFIX);
|
|
894
|
+
const tKey = prepared.toolsKey.slice(0, KEY_PREFIX);
|
|
895
|
+
const recipeTag = `${TAG_HOST}/recipe/${repository}:${rKey}`;
|
|
896
|
+
const buildTag = `${TAG_HOST}/build/${repository}:${rKey}-${tKey}`;
|
|
897
|
+
const runner = runnerDir(opts.configPath);
|
|
898
|
+
const builderDisk = await ports.builderDisk(runner);
|
|
899
|
+
const toolFilesDir = await ports.toolFiles(runner);
|
|
900
|
+
const probe = await ports.helper.probe();
|
|
901
|
+
const helper = probe.ok ? probe.client : void 0;
|
|
902
|
+
if (helper === void 0 && opts.managed === true) throw new RecipeError("image_builder_unavailable", MANAGED_UNJAILED_REASON);
|
|
903
|
+
const id = randomBytes(6).toString("hex");
|
|
904
|
+
const dir = buildDir(opts.configPath, id);
|
|
905
|
+
const state = { buildId: id, buildDir: dir, vsockPath: vsockPath(dir) };
|
|
906
|
+
const record = { owner, name, id };
|
|
907
|
+
const cachePath = builderCacheDiskPath(opts.configPath, owner, name);
|
|
908
|
+
const diskPath = join(dir, "build.ext4");
|
|
909
|
+
const partial = join(recordDir, "rootfs.ext4.partial");
|
|
910
|
+
const scratchRoot = opts.tmpDir ?? tmpdir();
|
|
911
|
+
let scratch;
|
|
912
|
+
let tunnel;
|
|
913
|
+
let sanitized;
|
|
914
|
+
let timedOut = false;
|
|
915
|
+
const controller = new AbortController();
|
|
916
|
+
const timer = opts.deadlineMs === void 0 ? void 0 : setTimeout(() => {
|
|
917
|
+
timedOut = true;
|
|
918
|
+
controller.abort();
|
|
919
|
+
}, Math.max(0, opts.deadlineMs - Date.now()));
|
|
920
|
+
const tail = [];
|
|
921
|
+
let result;
|
|
922
|
+
let pending = "";
|
|
923
|
+
let logged = 0;
|
|
924
|
+
let hostFailure;
|
|
925
|
+
const failOnHost = (what, err) => {
|
|
926
|
+
hostFailure ??= `${what}: ${describeCallFailure(err)}`;
|
|
927
|
+
controller.abort();
|
|
928
|
+
};
|
|
929
|
+
const onOutput = (chunk) => {
|
|
930
|
+
if (hostFailure !== void 0) return;
|
|
931
|
+
if (logged < MAX_LOG_BYTES) {
|
|
932
|
+
const kept = chunk.subarray(0, MAX_LOG_BYTES - logged);
|
|
933
|
+
try {
|
|
934
|
+
appendFileSync(logPath, kept);
|
|
935
|
+
if (logged + kept.length >= MAX_LOG_BYTES) appendFileSync(logPath, "\n[the build log is cut here: it reached its size limit]\n");
|
|
936
|
+
} catch (err) {
|
|
937
|
+
failOnHost(`the build log ${logPath} could not be written`, err);
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
logged += kept.length;
|
|
941
|
+
}
|
|
942
|
+
pending += chunk.toString("utf8");
|
|
943
|
+
const lines = pending.split("\n");
|
|
944
|
+
pending = lines.pop() ?? "";
|
|
945
|
+
if (pending.length > MAX_PENDING_BYTES) {
|
|
946
|
+
lines.push(pending);
|
|
947
|
+
pending = "";
|
|
948
|
+
}
|
|
949
|
+
for (const line of lines) {
|
|
950
|
+
if (line.startsWith(RESULT_PREFIX)) result = line.slice(RESULT_PREFIX.length);
|
|
951
|
+
else {
|
|
952
|
+
tail.push(line);
|
|
953
|
+
if (tail.length > LOG_TAIL_LINES) tail.shift();
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
};
|
|
957
|
+
const exec = (argv) => ({ argv, user: "root", cwd: "/", env: { PATH: ROOT_PATH } });
|
|
958
|
+
const check = (status, what) => {
|
|
959
|
+
if (hostFailure !== void 0) throw new StepFailed(`${what} stopped: ${hostFailure}`);
|
|
960
|
+
if (controller.signal.aborted) throw new StepFailed(`${what} was stopped at the time limit`);
|
|
961
|
+
if (status !== 0) throw new StepFailed(`${what} failed (exit ${status})`);
|
|
962
|
+
};
|
|
963
|
+
const cacheSpace = async () => {
|
|
964
|
+
try {
|
|
965
|
+
const df = await ports.exec(state.vsockPath, exec(["df", "-P", "-k", "/cache"]), { signal: controller.signal });
|
|
966
|
+
return df.status === 0 ? parseCacheSpace(df.stdout) : void 0;
|
|
967
|
+
} catch {
|
|
968
|
+
return void 0;
|
|
969
|
+
}
|
|
970
|
+
};
|
|
971
|
+
const spaceNote = async () => {
|
|
972
|
+
const space = await cacheSpace();
|
|
973
|
+
return space === void 0 ? "" : `; the builder's cache disk had ${formatBytes(space.free)} of ${formatBytes(space.total)} free (runner.build.disk)`;
|
|
974
|
+
};
|
|
975
|
+
let inputs = [];
|
|
976
|
+
let built;
|
|
977
|
+
let failure2;
|
|
978
|
+
try {
|
|
979
|
+
scratch = mkdtempSync(join(scratchRoot, "ol-builder-"));
|
|
980
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
981
|
+
chmodSync(dir, 448);
|
|
982
|
+
makeBuildsDirPrivate(dir);
|
|
983
|
+
writeBuilderRecord(dir, record);
|
|
984
|
+
mkdirSync(dirname(logPath), { recursive: true, mode: RUNNER_DIR_MODE });
|
|
985
|
+
opts.log?.(`building the image in a builder microVM at ${opts.memory ?? "the default memory"}`);
|
|
986
|
+
mkdirSync(dirname(cachePath), { recursive: true, mode: RUNNER_DIR_MODE });
|
|
987
|
+
if (existsSync(cachePath) && statSync(cachePath).size !== opts.limits.diskBytes) rmSync(cachePath, { force: true });
|
|
988
|
+
if (existsSync(cachePath)) renameSync(cachePath, diskPath);
|
|
989
|
+
else await createBuildDisk({ host: ports.rootfs, runnerDir: runner, bytes: opts.limits.diskBytes, dest: diskPath, memoryBytes: opts.limits.memoryBytes });
|
|
990
|
+
tunnel = await deps.startTunnel(join(dir, TUNNEL_SOCKET), {
|
|
991
|
+
...deps.tunnelOptions,
|
|
992
|
+
...opts.allowHosts === void 0 ? {} : { allowHosts: opts.allowHosts },
|
|
993
|
+
recordPath: join(dir, "tunnel.json")
|
|
994
|
+
});
|
|
995
|
+
const configPath = join(dir, "vm.json");
|
|
996
|
+
if (helper === void 0) linkVsockSockets({ buildDir: dir, vsockPath: state.vsockPath, links: [{ port: TUNNEL_PORT, socket: TUNNEL_SOCKET }] });
|
|
997
|
+
writeFileSync(
|
|
998
|
+
configPath,
|
|
999
|
+
JSON.stringify(builderMachineConfig(opts.limits, { kernel: opts.kernel, rootfs: builderDisk, buildDisk: diskPath, vsock: state.vsockPath, relayPorts: [TUNNEL_PORT] }), null, 2),
|
|
1000
|
+
{ mode: 384 }
|
|
1001
|
+
);
|
|
1002
|
+
if (helper !== void 0) {
|
|
1003
|
+
record.jailed = true;
|
|
1004
|
+
writeBuilderRecord(dir, record);
|
|
1005
|
+
await startJailedInto(state, { client: helper, kind: "build", kernel: opts.kernel, rootfs: builderDisk, sockets: [TUNNEL_SOCKET], timeLimitMinutes: opts.timeLimitMinutes }, () => void 0);
|
|
1006
|
+
} else {
|
|
1007
|
+
state.cgroup = ports.cgroup.create(id, opts.limits.memoryBytes);
|
|
1008
|
+
if (state.cgroup !== void 0) record.cgroup = state.cgroup;
|
|
1009
|
+
state.firecracker = await ports.firecracker.spawn({ binary: opts.firecracker, configPath, consolePath: join(dir, "console.log"), cgroup: state.cgroup });
|
|
1010
|
+
record.firecracker = state.firecracker;
|
|
1011
|
+
writeBuilderRecord(dir, record);
|
|
1012
|
+
}
|
|
1013
|
+
if (!await waitForAgent(state.vsockPath, { attempts: BOOT_ATTEMPTS, delayMs: BOOT_POLL_MS, sleep, onFailure: () => void 0 })) {
|
|
1014
|
+
throw new StepFailed(`the builder microVM did not start: its exec agent did not answer${state.jailed === void 0 ? ` (console in ${join(dir, "console.log")})` : `; its console output is in the journal of the unit ${state.jailed.unit}`}`);
|
|
1015
|
+
}
|
|
1016
|
+
const space = await cacheSpace();
|
|
1017
|
+
check(0, "checking the builder's cache disk");
|
|
1018
|
+
if (space !== void 0 && space.free * 2 < space.total) {
|
|
1019
|
+
check((await ports.exec(state.vsockPath, exec(["rm", "-rf", "/cache/docker"]), { signal: controller.signal })).status, "emptying the builder's layer cache");
|
|
1020
|
+
opts.log?.(`emptied the builder's layer cache: ${formatBytes(space.free)} of ${formatBytes(space.total)} was free`);
|
|
1021
|
+
}
|
|
1022
|
+
let recipeParts = { kind: "default" };
|
|
1023
|
+
const recipeDir = join(scratch, "recipe");
|
|
1024
|
+
mkdirSync(recipeDir);
|
|
1025
|
+
if (location.kind === "default") writeFileSync(join(recipeDir, "Dockerfile"), prepared.defaultText);
|
|
1026
|
+
else {
|
|
1027
|
+
sanitized = writeSanitizedRecipe(checkout.dir, location, recipe);
|
|
1028
|
+
recipeParts = { kind: "file", config: relative(checkout.dir, sanitized) };
|
|
1029
|
+
}
|
|
1030
|
+
const toolsDir = join(scratch, "tools");
|
|
1031
|
+
mkdirSync(toolsDir);
|
|
1032
|
+
cpSync(opts.cli.distDir, join(toolsDir, "dist"), { recursive: true });
|
|
1033
|
+
cpSync(join(opts.cli.distDir, "..", "package.json"), join(toolsDir, "package.json"));
|
|
1034
|
+
cpSync(join(opts.cli.distDir, "..", "skill-pack"), join(toolsDir, "skill-pack"), { recursive: true });
|
|
1035
|
+
mkdirSync(join(toolsDir, "guest"));
|
|
1036
|
+
cpSync(opts.guestHelper.path, join(toolsDir, "guest", "outerlayer-guest"));
|
|
1037
|
+
mkdirSync(join(toolsDir, FETCHED_DIR));
|
|
1038
|
+
writeFileSync(
|
|
1039
|
+
join(toolsDir, "Dockerfile"),
|
|
1040
|
+
toolsDockerfile({ recipeImage: recipeTag, nodeImage: prepared.nodeImage, claudeCodeVersion: prepared.claudeCodeVersion, buildId: opts.cli.buildId, guestHelper: true, fetched: true })
|
|
1041
|
+
);
|
|
1042
|
+
inputs = builderInput({ checkoutDir: checkout.dir, toolsDir, toolFilesDir, recipeDir }, archive);
|
|
1043
|
+
for (const { target, stream } of inputs) {
|
|
1044
|
+
let status;
|
|
1045
|
+
try {
|
|
1046
|
+
status = (await ports.exec(state.vsockPath, exec(["tar", "-x", "--no-same-owner", "-C", target]), { stdin: stream, signal: controller.signal })).status;
|
|
1047
|
+
} catch (err) {
|
|
1048
|
+
if (hostFailure !== void 0 || controller.signal.aborted) throw err;
|
|
1049
|
+
throw new StepFailed(`unpacking ${target} in the builder failed: ${err instanceof Error ? err.message : String(err)}${await spaceNote()}`);
|
|
1050
|
+
}
|
|
1051
|
+
if (status !== 0 && hostFailure === void 0 && !controller.signal.aborted) throw new StepFailed(`unpacking ${target} in the builder failed (exit ${status})${await spaceNote()}`);
|
|
1052
|
+
check(status, `unpacking ${target} in the builder`);
|
|
1053
|
+
}
|
|
1054
|
+
const script = builderGuestScript({ recipe: recipeParts, recipeTag, buildTag });
|
|
1055
|
+
const ran = await ports.execStream(state.vsockPath, exec(["/bin/sh", "-c", script]), { signal: controller.signal, onStdout: onOutput, onStderr: onOutput });
|
|
1056
|
+
check(ran.status, "the image build in the builder microVM");
|
|
1057
|
+
if (result === void 0) throw new StepFailed("the image build in the builder microVM ended without describing the image");
|
|
1058
|
+
const described = JSON.parse(result);
|
|
1059
|
+
const imageConfig = parseImageConfig(JSON.stringify(described.config));
|
|
1060
|
+
const labels = described.config.Labels ?? {};
|
|
1061
|
+
mkdirSync(recordDir, { recursive: true, mode: RUNNER_DIR_MODE });
|
|
1062
|
+
const fd = openSync(partial, "w", 384);
|
|
1063
|
+
let received = 0;
|
|
1064
|
+
try {
|
|
1065
|
+
const copied = await ports.execStream(state.vsockPath, exec(["cat", GUEST_OUTPUT]), {
|
|
1066
|
+
signal: controller.signal,
|
|
1067
|
+
onStdout: (chunk) => {
|
|
1068
|
+
if (hostFailure !== void 0) return;
|
|
1069
|
+
if (received + chunk.length > opts.limits.diskBytes) return failOnHost("the root disk is larger than build.disk", new Error(`more than ${opts.limits.diskBytes} bytes`));
|
|
1070
|
+
try {
|
|
1071
|
+
writeSync(fd, chunk);
|
|
1072
|
+
} catch (err) {
|
|
1073
|
+
return failOnHost(`the root disk ${partial} could not be written`, err);
|
|
1074
|
+
}
|
|
1075
|
+
received += chunk.length;
|
|
1076
|
+
},
|
|
1077
|
+
onStderr: onOutput
|
|
1078
|
+
});
|
|
1079
|
+
check(copied.status, "reading the root disk out of the builder microVM");
|
|
1080
|
+
} finally {
|
|
1081
|
+
closeSync(fd);
|
|
1082
|
+
}
|
|
1083
|
+
if (received !== described.bytes) throw new StepFailed(`the root disk arrived with ${received} bytes, and the builder made ${described.bytes}`);
|
|
1084
|
+
const report = {
|
|
1085
|
+
repository,
|
|
1086
|
+
sourceCommit: checkout.commit,
|
|
1087
|
+
defaultBranch: checkout.defaultBranch,
|
|
1088
|
+
recipe: location.kind === "default" ? "default" : location.path,
|
|
1089
|
+
recipeKey: prepared.recipeKey,
|
|
1090
|
+
toolsKey: prepared.toolsKey,
|
|
1091
|
+
imageId: `${ROOT_DISK_ID_PREFIX}${rKey}-${tKey}`,
|
|
1092
|
+
tag: buildTag,
|
|
1093
|
+
...classifyProperties(recipe, metadataFromLabels(labels))
|
|
1094
|
+
};
|
|
1095
|
+
const path = join(recordDir, "rootfs.ext4");
|
|
1096
|
+
chmodSync(partial, 292);
|
|
1097
|
+
renameSync(partial, path);
|
|
1098
|
+
const stored = { env: imageConfig.env, user: imageConfig.user, bytes: received, report };
|
|
1099
|
+
writeFileSync(join(recordDir, "rootdisk.json"), `${JSON.stringify(stored, null, 2)}
|
|
1100
|
+
`, { mode: 384 });
|
|
1101
|
+
writeRecord(recordDir, report, now().toISOString());
|
|
1102
|
+
pruneRecords(repoRecords, recordDir);
|
|
1103
|
+
opts.log?.("root disk built in a builder microVM");
|
|
1104
|
+
built = { reused: false, recipeBuilt: true, toolsBuilt: true, checkedCommit: checkout.commit, recordDir, report, removed: [], rootfs: { path, env: imageConfig.env, user: imageConfig.user } };
|
|
1105
|
+
} catch (err) {
|
|
1106
|
+
failure2 = await explain(err);
|
|
1107
|
+
} finally {
|
|
1108
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
1109
|
+
for (const { stream } of inputs) stream.destroy();
|
|
1110
|
+
const stopped = await stopBuilder();
|
|
1111
|
+
await tunnel?.close().catch(() => void 0);
|
|
1112
|
+
if (sanitized !== void 0) rmSync(sanitized, { force: true });
|
|
1113
|
+
if (scratch !== void 0) rmSync(scratch, { recursive: true, force: true });
|
|
1114
|
+
rmSync(partial, { force: true });
|
|
1115
|
+
if (stopped) {
|
|
1116
|
+
if (existsSync(diskPath)) renameSync(diskPath, cachePath);
|
|
1117
|
+
rmSync(dir, { recursive: true, force: true });
|
|
1118
|
+
} else {
|
|
1119
|
+
opts.log?.(`the builder microVM ${id} was not stopped, so its directory ${dir} stays for the runner's next start to remove`);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
if (failure2 !== void 0) throw failure2;
|
|
1123
|
+
return built;
|
|
1124
|
+
async function explain(err) {
|
|
1125
|
+
if (err instanceof RecipeError) return err;
|
|
1126
|
+
if (hostFailure !== void 0) return new RecipeError("image_builder_failed", `the image build stopped: ${hostFailure}`, logPath);
|
|
1127
|
+
if (timedOut) {
|
|
1128
|
+
const limit = opts.timeLimitMinutes === void 0 ? "runner.timeLimitMinutes" : `runner.timeLimitMinutes (${opts.timeLimitMinutes} minutes)`;
|
|
1129
|
+
return new RecipeError("image_builder_failed", `the image build ran past ${limit} and the builder microVM was stopped. The build log is at ${logPath}`, logPath);
|
|
1130
|
+
}
|
|
1131
|
+
try {
|
|
1132
|
+
if (state.cgroup !== void 0 && ports.cgroup.oomKills(state.cgroup) > 0 || helper !== void 0 && (await helper.state(id)).oomKills > 0) {
|
|
1133
|
+
return new RecipeError("image_build_out_of_memory", outOfMemoryReason(opts.memory), logPath);
|
|
1134
|
+
}
|
|
1135
|
+
} catch {
|
|
1136
|
+
}
|
|
1137
|
+
const refusals = tunnel?.record().refusals ?? [];
|
|
1138
|
+
if (err instanceof StepFailed && refusals.length > 0) return new RecipeError("image_builder_failed", refusalReason(refusals, logPath), logPath);
|
|
1139
|
+
const last = tail.filter((line) => line.trim() !== "").at(-1);
|
|
1140
|
+
const why = err instanceof StepFailed ? err.message : `the builder microVM failed: ${describeCallFailure(err)}`;
|
|
1141
|
+
return new RecipeError("image_builder_failed", `${why}${last === void 0 ? "" : `; the last line was: ${last}`}. The build log is at ${logPath}`, logPath);
|
|
1142
|
+
}
|
|
1143
|
+
async function stopBuilder() {
|
|
1144
|
+
let ok = true;
|
|
1145
|
+
if (state.jailed !== void 0) {
|
|
1146
|
+
try {
|
|
1147
|
+
await stopJailed(ports.helper, state);
|
|
1148
|
+
} catch (err) {
|
|
1149
|
+
ok = false;
|
|
1150
|
+
opts.log?.(describeCallFailure(err));
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
if (state.firecracker !== void 0) {
|
|
1154
|
+
try {
|
|
1155
|
+
await ports.firecracker.stop(state.firecracker);
|
|
1156
|
+
if (ports.firecracker.alive(state.firecracker)) ok = false;
|
|
1157
|
+
} catch (err) {
|
|
1158
|
+
ok = false;
|
|
1159
|
+
opts.log?.(`the builder microVM (pid ${state.firecracker.pid}) was not stopped: ${describeCallFailure(err)}`);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
if (ok && state.cgroup !== void 0) {
|
|
1163
|
+
try {
|
|
1164
|
+
ports.cgroup.remove(state.cgroup);
|
|
1165
|
+
} catch (err) {
|
|
1166
|
+
opts.log?.(`the builder's cgroup ${state.cgroup} was not removed: ${describeCallFailure(err)}`);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
return ok;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
async function sweepImageBuilders(configPath, ports, onError) {
|
|
1173
|
+
const root = buildsDir(configPath);
|
|
1174
|
+
let names;
|
|
1175
|
+
try {
|
|
1176
|
+
names = readdirSync(root);
|
|
1177
|
+
} catch {
|
|
1178
|
+
return [];
|
|
1179
|
+
}
|
|
1180
|
+
const swept = [];
|
|
1181
|
+
for (const id of names) {
|
|
1182
|
+
const dir = buildDir(configPath, id);
|
|
1183
|
+
let record;
|
|
1184
|
+
try {
|
|
1185
|
+
record = JSON.parse(readFileSync(join(dir, RECORD_FILE), "utf8"));
|
|
1186
|
+
} catch {
|
|
1187
|
+
continue;
|
|
1188
|
+
}
|
|
1189
|
+
let stopped = true;
|
|
1190
|
+
if (record.jailed === true) {
|
|
1191
|
+
const probe = await ports.helper.probe();
|
|
1192
|
+
if (!probe.ok) {
|
|
1193
|
+
onError?.(`the builder microVM ${id} could not be stopped, because there is no helper to ask: ${probe.detail}`);
|
|
1194
|
+
continue;
|
|
1195
|
+
}
|
|
1196
|
+
try {
|
|
1197
|
+
if ((await probe.client.list()).includes(id)) await probe.client.stop(id);
|
|
1198
|
+
} catch (err) {
|
|
1199
|
+
onError?.(`the builder microVM ${id} was not stopped: ${describeCallFailure(err)}`);
|
|
1200
|
+
stopped = false;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
if (record.firecracker !== void 0) {
|
|
1204
|
+
try {
|
|
1205
|
+
if (ports.firecracker.alive(record.firecracker)) await ports.firecracker.stop(record.firecracker);
|
|
1206
|
+
if (ports.firecracker.alive(record.firecracker)) stopped = false;
|
|
1207
|
+
} catch (err) {
|
|
1208
|
+
onError?.(`the builder microVM ${id} (pid ${record.firecracker.pid}) was not stopped: ${describeCallFailure(err)}`);
|
|
1209
|
+
stopped = false;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
if (!stopped) continue;
|
|
1213
|
+
if (record.cgroup !== void 0) {
|
|
1214
|
+
try {
|
|
1215
|
+
ports.cgroup.remove(record.cgroup);
|
|
1216
|
+
} catch (err) {
|
|
1217
|
+
onError?.(`the builder's cgroup ${record.cgroup} was not removed: ${describeCallFailure(err)}`);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
rmSync(dir, { recursive: true, force: true });
|
|
1221
|
+
swept.push(id);
|
|
1222
|
+
}
|
|
1223
|
+
return swept;
|
|
1224
|
+
}
|
|
252
1225
|
var BranchPrepareError = class extends Error {
|
|
253
1226
|
};
|
|
254
1227
|
var OWN_PREFIX = "outerlayer/";
|
|
@@ -270,13 +1243,13 @@ function localGit(cwd, args) {
|
|
|
270
1243
|
}
|
|
271
1244
|
return Promise.resolve({ status: result.status, stdout: result.stdout.trim(), stderr: result.stderr.trim() });
|
|
272
1245
|
}
|
|
273
|
-
function
|
|
1246
|
+
function firstLine2(stderr) {
|
|
274
1247
|
return (stderr.split("\n")[0] ?? "").replace(/\/\/[^/@\s]+@/g, "//");
|
|
275
1248
|
}
|
|
276
1249
|
async function mustGit(git2, cwd, step, args) {
|
|
277
1250
|
const result = await git2(cwd, args);
|
|
278
1251
|
if (result.status !== 0) {
|
|
279
|
-
const detail =
|
|
1252
|
+
const detail = firstLine2(result.stderr);
|
|
280
1253
|
throw new BranchPrepareError(`${step} failed${detail === "" ? "" : `: ${detail}`}`);
|
|
281
1254
|
}
|
|
282
1255
|
return result.stdout;
|
|
@@ -321,7 +1294,7 @@ async function isMerged(git2, workdir, branch, defaultBranch) {
|
|
|
321
1294
|
const result = await git2(workdir, ["merge-base", "--is-ancestor", `origin/${branch}`, `origin/${defaultBranch}`]);
|
|
322
1295
|
if (result.status === 0) return true;
|
|
323
1296
|
if (result.status === 1) return false;
|
|
324
|
-
const detail =
|
|
1297
|
+
const detail = firstLine2(result.stderr);
|
|
325
1298
|
throw new BranchPrepareError(`comparing ${branch} with ${defaultBranch} failed${detail === "" ? "" : `: ${detail}`}`);
|
|
326
1299
|
}
|
|
327
1300
|
|
|
@@ -389,592 +1362,286 @@ function usableCpus(deps = {}) {
|
|
|
389
1362
|
}
|
|
390
1363
|
return Array.from({ length: Math.max(1, (deps.parallelism ?? availableParallelism)()) }, (_unused, core) => core);
|
|
391
1364
|
}
|
|
392
|
-
function createCpuAllocator(cores) {
|
|
393
|
-
const usable = [...new Set(cores)].sort((a, b) => a - b);
|
|
394
|
-
const held = /* @__PURE__ */ new Map();
|
|
395
|
-
const holders = (core) => {
|
|
396
|
-
let count = 0;
|
|
397
|
-
for (const set of held.values()) if (set.includes(core)) count++;
|
|
398
|
-
return count;
|
|
399
|
-
};
|
|
400
|
-
return {
|
|
401
|
-
acquire(buildId, cpus) {
|
|
402
|
-
const existing = held.get(buildId);
|
|
403
|
-
if (existing !== void 0) return existing;
|
|
404
|
-
const wanted = Math.min(Math.ceil(cpus), usable.length);
|
|
405
|
-
const ranked = usable.map((core) => ({ core, count: holders(core) })).sort((a, b) => a.count - b.count);
|
|
406
|
-
const chosen = ranked.slice(0, wanted).map(({ core }) => core).sort((a, b) => a - b);
|
|
407
|
-
held.set(buildId, chosen);
|
|
408
|
-
return chosen;
|
|
409
|
-
},
|
|
410
|
-
hold(buildId, set) {
|
|
411
|
-
held.set(buildId, [...set]);
|
|
412
|
-
},
|
|
413
|
-
release(buildId) {
|
|
414
|
-
held.delete(buildId);
|
|
415
|
-
}
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// src/runner/container/lease-channel.ts
|
|
420
|
-
var LEASE_SECONDS = 900;
|
|
421
|
-
function leaseExpiresAtMs(job) {
|
|
422
|
-
const base = Date.parse(job.lastRenewAt ?? job.startedAt);
|
|
423
|
-
return (Number.isFinite(base) ? base : Date.now()) + LEASE_SECONDS * 1e3;
|
|
424
|
-
}
|
|
425
|
-
function leaseTimeLeftMs(job, nowMs) {
|
|
426
|
-
return Math.max(0, leaseExpiresAtMs(job) - nowMs);
|
|
427
|
-
}
|
|
428
|
-
function createLeaseChannel(valueMs) {
|
|
429
|
-
const open = /* @__PURE__ */ new Set();
|
|
430
|
-
let stopped = false;
|
|
431
|
-
const write = (stream) => {
|
|
432
|
-
if (!stopped && !stream.destroyed && stream.writable) stream.write(`${Math.floor(valueMs())}
|
|
433
|
-
`);
|
|
434
|
-
};
|
|
435
|
-
return {
|
|
436
|
-
accept(stream) {
|
|
437
|
-
stream.on("error", () => void 0);
|
|
438
|
-
open.add(stream);
|
|
439
|
-
stream.once("close", () => open.delete(stream));
|
|
440
|
-
queueMicrotask(() => write(stream));
|
|
441
|
-
},
|
|
442
|
-
publish() {
|
|
443
|
-
for (const stream of open) write(stream);
|
|
444
|
-
},
|
|
445
|
-
stop() {
|
|
446
|
-
stopped = true;
|
|
447
|
-
},
|
|
448
|
-
close() {
|
|
449
|
-
for (const stream of [...open]) stream.destroy();
|
|
450
|
-
open.clear();
|
|
451
|
-
}
|
|
452
|
-
};
|
|
453
|
-
}
|
|
454
|
-
var AssetError = class extends Error {
|
|
455
|
-
constructor(file, message) {
|
|
456
|
-
super(message);
|
|
457
|
-
this.file = file;
|
|
458
|
-
}
|
|
459
|
-
};
|
|
460
|
-
var FIRECRACKER_RELEASES = "https://github.com/firecracker-microvm/firecracker/releases/download";
|
|
461
|
-
var DOWNLOAD_TIMEOUT_MS = 10 * 6e4;
|
|
462
|
-
function pinnedAssets(arch) {
|
|
463
|
-
const version = FIRECRACKER_VERSION;
|
|
464
|
-
return {
|
|
465
|
-
firecracker: {
|
|
466
|
-
url: `${FIRECRACKER_RELEASES}/v${version}/firecracker-v${version}-${arch}.tgz`,
|
|
467
|
-
sha256: FIRECRACKER_SHA256[arch],
|
|
468
|
-
binarySha256: FIRECRACKER_BINARY_SHA256[arch],
|
|
469
|
-
jailerSha256: JAILER_BINARY_SHA256[arch]
|
|
470
|
-
},
|
|
471
|
-
kernel: { url: `${GUEST_ASSETS_BASE_URL}/vmlinux-${arch}`, sha256: GUEST_KERNEL_SHA256[arch] },
|
|
472
|
-
helper: { url: `${GUEST_ASSETS_BASE_URL}/outerlayer-guest-${arch}`, sha256: GUEST_HELPER_SHA256[arch] }
|
|
473
|
-
};
|
|
474
|
-
}
|
|
475
|
-
var sha256Of = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
476
|
-
function pinProblem(file, expected, actual, where) {
|
|
477
|
-
const release = file.startsWith("vmlinux-") || file.startsWith("outerlayer-guest-") ? ` (guest assets release ${GUEST_ASSETS_VERSION})` : "";
|
|
478
|
-
return new AssetError(file, `${file} ${where} has sha256 ${actual}, not the ${expected} this release pins${release}, so no microVM build starts`);
|
|
479
|
-
}
|
|
480
|
-
function writeAtomic(path, bytes, mode) {
|
|
481
|
-
const temp = `${path}.${process.pid}.tmp`;
|
|
482
|
-
writeFileSync(temp, bytes, { mode });
|
|
483
|
-
renameSync(temp, path);
|
|
484
|
-
}
|
|
485
|
-
async function download(file, url, fetchImpl) {
|
|
486
|
-
let response;
|
|
487
|
-
try {
|
|
488
|
-
response = await fetchImpl(url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) });
|
|
489
|
-
} catch (err) {
|
|
490
|
-
throw new AssetError(file, `${file} could not be downloaded from ${url}: ${err instanceof Error ? err.message : String(err)}`);
|
|
491
|
-
}
|
|
492
|
-
if (!response.ok) throw new AssetError(file, `${file} could not be downloaded from ${url}: the server answered ${response.status}`);
|
|
493
|
-
return Buffer.from(await response.arrayBuffer());
|
|
494
|
-
}
|
|
495
|
-
function keptPath(dir, sha256, name) {
|
|
496
|
-
mkdirSync(join(dir, sha256), { recursive: true, mode: RUNNER_DIR_MODE });
|
|
497
|
-
return join(dir, sha256, name);
|
|
498
|
-
}
|
|
499
|
-
async function ensureFile(dir, file, pin, fetchImpl, mode) {
|
|
500
|
-
const path = keptPath(dir, pin.sha256, file);
|
|
501
|
-
if (existsSync(path)) {
|
|
502
|
-
const actual2 = sha256Of(readFileSync(path));
|
|
503
|
-
if (actual2 !== pin.sha256) throw pinProblem(file, pin.sha256, actual2, `at ${path}`);
|
|
504
|
-
return path;
|
|
505
|
-
}
|
|
506
|
-
const bytes = await download(file, pin.url, fetchImpl);
|
|
507
|
-
const actual = sha256Of(bytes);
|
|
508
|
-
if (actual !== pin.sha256) throw pinProblem(file, pin.sha256, actual, `downloaded from ${pin.url}`);
|
|
509
|
-
writeAtomic(path, bytes, mode);
|
|
510
|
-
return path;
|
|
511
|
-
}
|
|
512
|
-
function extractWithTar(arch, program = "firecracker") {
|
|
513
|
-
const name = `${program}-v${FIRECRACKER_VERSION}-${arch}`;
|
|
514
|
-
return async (tarball, destDir) => {
|
|
515
|
-
const result = spawnSync("tar", ["-xzf", tarball, "-C", destDir, "--strip-components=1", `release-v${FIRECRACKER_VERSION}-${arch}/${name}`], { encoding: "utf8" });
|
|
516
|
-
if (result.status !== 0) throw new Error(`tar could not unpack ${tarball}: ${result.stderr.trim()}`);
|
|
517
|
-
return join(destDir, name);
|
|
518
|
-
};
|
|
519
|
-
}
|
|
520
|
-
async function ensureFirecracker(input) {
|
|
521
|
-
const { dir, arch, pins, fetchImpl } = input;
|
|
522
|
-
const firecracker = keptPath(dir, pins.firecracker.binarySha256, "firecracker");
|
|
523
|
-
if (existsSync(firecracker)) {
|
|
524
|
-
const actual = sha256Of(readFileSync(firecracker));
|
|
525
|
-
if (actual !== pins.firecracker.binarySha256) throw pinProblem("firecracker", pins.firecracker.binarySha256, actual, `at ${firecracker}`);
|
|
526
|
-
return firecracker;
|
|
527
|
-
}
|
|
528
|
-
const tarballName = `firecracker-${arch}.tgz`;
|
|
529
|
-
const tarball = await ensureFile(dir, tarballName, pins.firecracker, fetchImpl, RUNNER_FILE_MODE);
|
|
530
|
-
const scratch = mkdtempSync(join(dir, ".extract-"));
|
|
531
|
-
try {
|
|
532
|
-
const extracted = await (input.extract ?? extractWithTar(arch))(tarball, scratch);
|
|
533
|
-
const bytes = readFileSync(extracted);
|
|
534
|
-
const actual = sha256Of(bytes);
|
|
535
|
-
if (actual !== pins.firecracker.binarySha256) throw pinProblem("firecracker", pins.firecracker.binarySha256, actual, `unpacked from ${tarballName}`);
|
|
536
|
-
writeAtomic(firecracker, bytes, 493);
|
|
537
|
-
} finally {
|
|
538
|
-
rmSync(scratch, { recursive: true, force: true });
|
|
539
|
-
}
|
|
540
|
-
return firecracker;
|
|
541
|
-
}
|
|
542
|
-
async function ensureFirecrackerAndJailer(options) {
|
|
543
|
-
const arch = options.arch ?? firecrackerArch(process.arch);
|
|
544
|
-
if (!MICROVM_ARCHES.includes(arch)) throw new AssetError("firecracker", `no Firecracker assets are pinned for ${arch}, so microVM builds run on ${MICROVM_ARCHES.join(" or ")} hosts only`);
|
|
545
|
-
const pins = options.pins ?? pinnedAssets(arch);
|
|
546
|
-
const dir = vmAssetsDir(options.runnerDir);
|
|
547
|
-
mkdirSync(dir, { recursive: true, mode: RUNNER_DIR_MODE });
|
|
548
|
-
const firecracker = await ensureFirecracker({ dir, arch, pins, fetchImpl: options.fetchImpl ?? fetch, extract: options.extractFirecracker });
|
|
549
|
-
return { firecracker, jailer: await ensureJailer(options) };
|
|
550
|
-
}
|
|
551
|
-
async function ensureAssets(options) {
|
|
552
|
-
const arch = options.arch ?? firecrackerArch(process.arch);
|
|
553
|
-
if (!MICROVM_ARCHES.includes(arch)) throw new AssetError("firecracker", `no Firecracker or guest assets are pinned for ${arch}, so microVM builds run on ${MICROVM_ARCHES.join(" or ")} hosts only`);
|
|
554
|
-
const pins = options.pins ?? pinnedAssets(arch);
|
|
555
|
-
const fetchImpl = options.fetchImpl ?? fetch;
|
|
556
|
-
const dir = vmAssetsDir(options.runnerDir);
|
|
557
|
-
mkdirSync(dir, { recursive: true, mode: RUNNER_DIR_MODE });
|
|
558
|
-
const firecracker = await ensureFirecracker({ dir, arch, pins, fetchImpl, extract: options.extractFirecracker });
|
|
559
|
-
const kernel = await ensureFile(dir, `vmlinux-${arch}`, pins.kernel, fetchImpl, RUNNER_FILE_MODE);
|
|
560
|
-
const helper = await ensureFile(dir, `outerlayer-guest-${arch}`, pins.helper, fetchImpl, 493);
|
|
561
|
-
return { firecracker, kernel, helper, helperSha256: pins.helper.sha256 };
|
|
562
|
-
}
|
|
563
|
-
async function ensureJailer(options) {
|
|
564
|
-
const arch = options.arch ?? firecrackerArch(process.arch);
|
|
565
|
-
if (!MICROVM_ARCHES.includes(arch)) throw new AssetError("jailer", `no Firecracker assets are pinned for ${arch}, so microVM builds run on ${MICROVM_ARCHES.join(" or ")} hosts only`);
|
|
566
|
-
const pins = options.pins ?? pinnedAssets(arch);
|
|
567
|
-
const expected = pins.firecracker.jailerSha256;
|
|
568
|
-
if (expected === void 0) throw new AssetError("jailer", "this release pins no hash for the jailer, so it cannot be had");
|
|
569
|
-
const fetchImpl = options.fetchImpl ?? fetch;
|
|
570
|
-
const dir = vmAssetsDir(options.runnerDir);
|
|
571
|
-
mkdirSync(dir, { recursive: true, mode: RUNNER_DIR_MODE });
|
|
572
|
-
const jailer = keptPath(dir, expected, "jailer");
|
|
573
|
-
if (existsSync(jailer)) {
|
|
574
|
-
const actual = sha256Of(readFileSync(jailer));
|
|
575
|
-
if (actual !== expected) throw pinProblem("jailer", expected, actual, `at ${jailer}`);
|
|
576
|
-
return jailer;
|
|
577
|
-
}
|
|
578
|
-
const tarballName = `firecracker-${arch}.tgz`;
|
|
579
|
-
const tarball = await ensureFile(dir, tarballName, pins.firecracker, fetchImpl, RUNNER_FILE_MODE);
|
|
580
|
-
const scratch = mkdtempSync(join(dir, ".extract-"));
|
|
581
|
-
try {
|
|
582
|
-
const extracted = await (options.extractJailer ?? extractWithTar(arch, "jailer"))(tarball, scratch);
|
|
583
|
-
const bytes = readFileSync(extracted);
|
|
584
|
-
const actual = sha256Of(bytes);
|
|
585
|
-
if (actual !== expected) throw pinProblem("jailer", expected, actual, `unpacked from ${tarballName}`);
|
|
586
|
-
writeAtomic(jailer, bytes, 493);
|
|
587
|
-
} finally {
|
|
588
|
-
rmSync(scratch, { recursive: true, force: true });
|
|
589
|
-
}
|
|
590
|
-
return jailer;
|
|
591
|
-
}
|
|
592
|
-
var STOP_GRACE_SECONDS = 15;
|
|
593
|
-
function parseImageConfig(json) {
|
|
594
|
-
const config = JSON.parse(json);
|
|
595
|
-
const env = {};
|
|
596
|
-
for (const entry of config.Env ?? []) {
|
|
597
|
-
const eq = entry.indexOf("=");
|
|
598
|
-
if (eq > 0) env[entry.slice(0, eq)] = entry.slice(eq + 1);
|
|
599
|
-
}
|
|
600
|
-
return { user: config.User ?? "", env };
|
|
601
|
-
}
|
|
602
|
-
function createContainerHost(deps = {}) {
|
|
603
|
-
const run = deps.run ?? spawnRun;
|
|
1365
|
+
function createCpuAllocator(cores) {
|
|
1366
|
+
const usable = [...new Set(cores)].sort((a, b) => a - b);
|
|
1367
|
+
const held = /* @__PURE__ */ new Map();
|
|
1368
|
+
const holders = (core) => {
|
|
1369
|
+
let count = 0;
|
|
1370
|
+
for (const set of held.values()) if (set.includes(core)) count++;
|
|
1371
|
+
return count;
|
|
1372
|
+
};
|
|
604
1373
|
return {
|
|
605
|
-
|
|
606
|
-
const
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
const
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
async imageConfig(imageId) {
|
|
614
|
-
const result = await run("docker", ["image", "inspect", "--format", "{{json .Config}}", imageId]);
|
|
615
|
-
return result.status === 0 ? parseImageConfig(result.stdout) : { user: "", env: {} };
|
|
616
|
-
},
|
|
617
|
-
createVolume(name, labels) {
|
|
618
|
-
const flags = Object.entries(labels).flatMap(([key, value]) => ["--label", `${key}=${value}`]);
|
|
619
|
-
return run("docker", ["volume", "create", ...flags, name]);
|
|
620
|
-
},
|
|
621
|
-
removeVolume(name) {
|
|
622
|
-
return run("docker", ["volume", "rm", "--force", name]);
|
|
623
|
-
},
|
|
624
|
-
run(args, opts) {
|
|
625
|
-
return run("docker", ["run", ...args], opts);
|
|
626
|
-
},
|
|
627
|
-
exec(input, opts) {
|
|
628
|
-
const env = {};
|
|
629
|
-
for (const [name, value] of Object.entries(input.env.passed)) if (value !== void 0) env[name] = value;
|
|
630
|
-
return run("docker", ["exec", ...containerExecArgs(input)], { ...opts, env, ...input.stdin === void 0 ? {} : { input: input.stdin } });
|
|
631
|
-
},
|
|
632
|
-
execDetached(input) {
|
|
633
|
-
const env = {};
|
|
634
|
-
for (const [name, value] of Object.entries(input.env.passed)) if (value !== void 0) env[name] = value;
|
|
635
|
-
return run("docker", ["exec", ...containerExecArgs({ ...input, detached: true })], { env });
|
|
636
|
-
},
|
|
637
|
-
async state(container) {
|
|
638
|
-
const result = await run("docker", ["inspect", "--format", "{{.State.Running}}", container]);
|
|
639
|
-
if (result.status !== 0) return /no such (object|container)/i.test(result.stderr) ? "missing" : "unknown";
|
|
640
|
-
return result.stdout.trim() === "true" ? "running" : "stopped";
|
|
641
|
-
},
|
|
642
|
-
execStream(input) {
|
|
643
|
-
const child = (deps.spawn ?? spawn)("docker", ["exec", ...containerExecArgs({ ...input, interactive: true })], {
|
|
644
|
-
env: childEnvironment(process.env),
|
|
645
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
646
|
-
});
|
|
647
|
-
let stderr = "";
|
|
648
|
-
child.stderr?.on("data", (chunk) => {
|
|
649
|
-
stderr = (stderr + chunk.toString()).slice(-400);
|
|
650
|
-
});
|
|
651
|
-
child.stderr?.on("error", () => void 0);
|
|
652
|
-
const exited = new Promise((resolve2) => {
|
|
653
|
-
child.once("error", () => resolve2(-1));
|
|
654
|
-
child.once("close", (code) => resolve2(code ?? -1));
|
|
655
|
-
});
|
|
656
|
-
child.stdin.on("error", () => void 0);
|
|
657
|
-
return { stdin: child.stdin, stdout: child.stdout, exited, kill: () => void child.kill("SIGKILL"), stderrTail: () => stderr };
|
|
658
|
-
},
|
|
659
|
-
stop(container) {
|
|
660
|
-
return run("docker", ["stop", "--time", String(STOP_GRACE_SECONDS), container]);
|
|
1374
|
+
acquire(buildId, cpus) {
|
|
1375
|
+
const existing = held.get(buildId);
|
|
1376
|
+
if (existing !== void 0) return existing;
|
|
1377
|
+
const wanted = Math.min(Math.ceil(cpus), usable.length);
|
|
1378
|
+
const ranked = usable.map((core) => ({ core, count: holders(core) })).sort((a, b) => a.count - b.count);
|
|
1379
|
+
const chosen = ranked.slice(0, wanted).map(({ core }) => core).sort((a, b) => a - b);
|
|
1380
|
+
held.set(buildId, chosen);
|
|
1381
|
+
return chosen;
|
|
661
1382
|
},
|
|
662
|
-
|
|
663
|
-
|
|
1383
|
+
hold(buildId, set) {
|
|
1384
|
+
held.set(buildId, [...set]);
|
|
664
1385
|
},
|
|
665
|
-
|
|
666
|
-
|
|
1386
|
+
release(buildId) {
|
|
1387
|
+
held.delete(buildId);
|
|
667
1388
|
}
|
|
668
1389
|
};
|
|
669
1390
|
}
|
|
670
1391
|
|
|
671
|
-
// src/runner/
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
'mkdir -p "$rootfs"',
|
|
677
|
-
'tar -xf - -C "$rootfs"',
|
|
678
|
-
'used_kb=$(du -sk "$rootfs" | cut -f1)',
|
|
679
|
-
'files=$(find "$rootfs" | wc -l)',
|
|
680
|
-
// Room for the file system's own metadata, and for an inode per file with some to spare.
|
|
681
|
-
"total_kb=$(( used_kb + used_kb / 4 + 65536 ))",
|
|
682
|
-
'truncate -s "${total_kb}K" "/out/$OUT_NAME"',
|
|
683
|
-
'mke2fs -q -F -t ext4 -m 0 -N $(( files + files / 4 + 4096 )) -d "$rootfs" "/out/$OUT_NAME"',
|
|
684
|
-
'chown "$HOST_UID:$HOST_GID" "/out/$OUT_NAME"'
|
|
685
|
-
].join("\n");
|
|
686
|
-
}
|
|
687
|
-
function templateScript() {
|
|
688
|
-
return [
|
|
689
|
-
"set -eu",
|
|
690
|
-
'truncate -s "$BYTES" "/out/$OUT_NAME"',
|
|
691
|
-
'mke2fs -q -F -t ext4 -m 0 -E lazy_itable_init=1,lazy_journal_init=1,nodiscard "/out/$OUT_NAME"',
|
|
692
|
-
'chown "$HOST_UID:$HOST_GID" "/out/$OUT_NAME"'
|
|
693
|
-
].join("\n");
|
|
694
|
-
}
|
|
695
|
-
var ids = () => ({ uid: String(process.getuid?.() ?? 0), gid: String(process.getgid?.() ?? 0) });
|
|
696
|
-
function converterArgs(dest, memoryBytes, extra, script) {
|
|
697
|
-
const { uid, gid } = ids();
|
|
698
|
-
return [
|
|
699
|
-
"run",
|
|
700
|
-
"--rm",
|
|
701
|
-
"-i",
|
|
702
|
-
"--network",
|
|
703
|
-
"none",
|
|
704
|
-
"--memory",
|
|
705
|
-
String(memoryBytes),
|
|
706
|
-
"--memory-swap",
|
|
707
|
-
String(memoryBytes),
|
|
708
|
-
"--mount",
|
|
709
|
-
`type=bind,src=${dirname(dest)},dst=/out`,
|
|
710
|
-
"-e",
|
|
711
|
-
`OUT_NAME=${basename(dest)}`,
|
|
712
|
-
"-e",
|
|
713
|
-
`HOST_UID=${uid}`,
|
|
714
|
-
"-e",
|
|
715
|
-
`HOST_GID=${gid}`,
|
|
716
|
-
...extra,
|
|
717
|
-
// The image that runs `mke2fs` as root is the default base image: Ubuntu holds e2fsprogs.
|
|
718
|
-
DEFAULT_BASE_IMAGE,
|
|
719
|
-
"sh",
|
|
720
|
-
"-c",
|
|
721
|
-
script
|
|
722
|
-
];
|
|
1392
|
+
// src/runner/container/lease-channel.ts
|
|
1393
|
+
var LEASE_SECONDS = 900;
|
|
1394
|
+
function leaseExpiresAtMs(job) {
|
|
1395
|
+
const base = Date.parse(job.lastRenewAt ?? job.startedAt);
|
|
1396
|
+
return (Number.isFinite(base) ? base : Date.now()) + LEASE_SECONDS * 1e3;
|
|
723
1397
|
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
return new Promise((resolve2, reject) => {
|
|
727
|
-
const exporter = spawn("docker", ["export", container], { stdio: ["ignore", "pipe", "pipe"], env: childEnvironment(process.env) });
|
|
728
|
-
const writer = spawn("docker", converter, { stdio: ["pipe", "ignore", "pipe"], env: childEnvironment(process.env) });
|
|
729
|
-
const errors = [];
|
|
730
|
-
exporter.stderr.on("data", (chunk) => errors.push(`docker export: ${chunk.toString("utf8")}`));
|
|
731
|
-
writer.stderr.on("data", (chunk) => errors.push(`converter: ${chunk.toString("utf8")}`));
|
|
732
|
-
writer.stdin.on("error", () => void 0);
|
|
733
|
-
exporter.stdout.on("error", () => void 0);
|
|
734
|
-
exporter.stdout.pipe(writer.stdin);
|
|
735
|
-
exporter.once("error", reject);
|
|
736
|
-
writer.once("error", reject);
|
|
737
|
-
let exits = 0;
|
|
738
|
-
let failure2;
|
|
739
|
-
const done = (name, code) => {
|
|
740
|
-
if (code !== 0 && failure2 === void 0) failure2 = `${name} exited ${code}`;
|
|
741
|
-
exits += 1;
|
|
742
|
-
if (exits < 2) return;
|
|
743
|
-
if (failure2 === void 0) resolve2();
|
|
744
|
-
else reject(new Error(`${failure2}: ${firstLine2(errors.join("\n"))}`));
|
|
745
|
-
};
|
|
746
|
-
exporter.once("exit", (code) => done("docker export", code));
|
|
747
|
-
writer.once("exit", (code) => done("the converter", code));
|
|
748
|
-
});
|
|
1398
|
+
function leaseTimeLeftMs(job, nowMs) {
|
|
1399
|
+
return Math.max(0, leaseExpiresAtMs(job) - nowMs);
|
|
749
1400
|
}
|
|
750
|
-
function
|
|
751
|
-
const
|
|
1401
|
+
function createLeaseChannel(valueMs) {
|
|
1402
|
+
const open = /* @__PURE__ */ new Set();
|
|
1403
|
+
let stopped = false;
|
|
1404
|
+
const write = (stream) => {
|
|
1405
|
+
if (!stopped && !stream.destroyed && stream.writable) stream.write(`${Math.floor(valueMs())}
|
|
1406
|
+
`);
|
|
1407
|
+
};
|
|
752
1408
|
return {
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
await exportInto(container, converterArgs(dest, memoryBytes, [], converterScript()));
|
|
759
|
-
} finally {
|
|
760
|
-
await spawnRun("docker", ["rm", "--force", "--volumes", container], { env: env() });
|
|
761
|
-
}
|
|
1409
|
+
accept(stream) {
|
|
1410
|
+
stream.on("error", () => void 0);
|
|
1411
|
+
open.add(stream);
|
|
1412
|
+
stream.once("close", () => open.delete(stream));
|
|
1413
|
+
queueMicrotask(() => write(stream));
|
|
762
1414
|
},
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
if (made.status !== 0) throw new Error(`making the empty build disk failed: ${firstLine2(made.stderr)}`);
|
|
1415
|
+
publish() {
|
|
1416
|
+
for (const stream of open) write(stream);
|
|
766
1417
|
},
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
1418
|
+
stop() {
|
|
1419
|
+
stopped = true;
|
|
1420
|
+
},
|
|
1421
|
+
close() {
|
|
1422
|
+
for (const stream of [...open]) stream.destroy();
|
|
1423
|
+
open.clear();
|
|
771
1424
|
}
|
|
772
1425
|
};
|
|
773
1426
|
}
|
|
774
|
-
|
|
775
|
-
|
|
1427
|
+
var AssetError = class extends Error {
|
|
1428
|
+
constructor(file, message) {
|
|
1429
|
+
super(message);
|
|
1430
|
+
this.file = file;
|
|
1431
|
+
}
|
|
1432
|
+
};
|
|
1433
|
+
var FIRECRACKER_RELEASES = "https://github.com/firecracker-microvm/firecracker/releases/download";
|
|
1434
|
+
var DOWNLOAD_TIMEOUT_MS = 10 * 6e4;
|
|
1435
|
+
function pinnedAssets(arch) {
|
|
1436
|
+
const version = FIRECRACKER_VERSION;
|
|
1437
|
+
return {
|
|
1438
|
+
firecracker: {
|
|
1439
|
+
url: `${FIRECRACKER_RELEASES}/v${version}/firecracker-v${version}-${arch}.tgz`,
|
|
1440
|
+
sha256: FIRECRACKER_SHA256[arch],
|
|
1441
|
+
binarySha256: FIRECRACKER_BINARY_SHA256[arch],
|
|
1442
|
+
jailerSha256: JAILER_BINARY_SHA256[arch]
|
|
1443
|
+
},
|
|
1444
|
+
kernel: { url: `${GUEST_ASSETS_BASE_URL}/vmlinux-${arch}`, sha256: GUEST_KERNEL_SHA256[arch] },
|
|
1445
|
+
helper: { url: `${GUEST_ASSETS_BASE_URL}/outerlayer-guest-${arch}`, sha256: GUEST_HELPER_SHA256[arch] }
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
function builderDiskPin(arch) {
|
|
1449
|
+
return { url: `${BUILDER_DISK_BASE_URL}/builder-rootfs-${arch}.ext4`, sha256: BUILDER_DISK_SHA256[arch] };
|
|
1450
|
+
}
|
|
1451
|
+
var sha256Of = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
1452
|
+
function pinProblem(file, expected, actual, where) {
|
|
1453
|
+
const release = file.startsWith("vmlinux-") || file.startsWith("outerlayer-guest-") ? ` (guest assets release ${GUEST_ASSETS_VERSION})` : file.startsWith("builder-rootfs-") ? ` (builder root disk release ${BUILDER_DISK_VERSION})` : "";
|
|
1454
|
+
return new AssetError(file, `${file} ${where} has sha256 ${actual}, not the ${expected} this release pins${release}, so no microVM build starts`);
|
|
1455
|
+
}
|
|
1456
|
+
function writeAtomic(path, bytes, mode) {
|
|
1457
|
+
const temp = `${path}.${process.pid}.tmp`;
|
|
1458
|
+
writeFileSync(temp, bytes, { mode });
|
|
1459
|
+
renameSync(temp, path);
|
|
1460
|
+
}
|
|
1461
|
+
async function download(file, url, fetchImpl) {
|
|
1462
|
+
let response;
|
|
776
1463
|
try {
|
|
777
|
-
|
|
778
|
-
} catch {
|
|
779
|
-
|
|
1464
|
+
response = await fetchImpl(url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) });
|
|
1465
|
+
} catch (err) {
|
|
1466
|
+
throw new AssetError(file, `${file} could not be downloaded from ${url}: ${err instanceof Error ? err.message : String(err)}`);
|
|
780
1467
|
}
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
1468
|
+
if (!response.ok) throw new AssetError(file, `${file} could not be downloaded from ${url}: the server answered ${response.status}`);
|
|
1469
|
+
return Buffer.from(await response.arrayBuffer());
|
|
1470
|
+
}
|
|
1471
|
+
function keptPath(dir, sha256, name) {
|
|
1472
|
+
mkdirSync(join(dir, sha256), { recursive: true, mode: RUNNER_DIR_MODE });
|
|
1473
|
+
return join(dir, sha256, name);
|
|
1474
|
+
}
|
|
1475
|
+
async function ensureFile(dir, file, pin, fetchImpl, mode) {
|
|
1476
|
+
const path = keptPath(dir, pin.sha256, file);
|
|
1477
|
+
if (existsSync(path)) {
|
|
1478
|
+
const actual2 = sha256Of(readFileSync(path));
|
|
1479
|
+
if (actual2 !== pin.sha256) throw pinProblem(file, pin.sha256, actual2, `at ${path}`);
|
|
1480
|
+
return path;
|
|
789
1481
|
}
|
|
1482
|
+
const bytes = await download(file, pin.url, fetchImpl);
|
|
1483
|
+
const actual = sha256Of(bytes);
|
|
1484
|
+
if (actual !== pin.sha256) throw pinProblem(file, pin.sha256, actual, `downloaded from ${pin.url}`);
|
|
1485
|
+
writeAtomic(path, bytes, mode);
|
|
1486
|
+
return path;
|
|
790
1487
|
}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
1488
|
+
function extractWithTar(arch, program = "firecracker") {
|
|
1489
|
+
const name = `${program}-v${FIRECRACKER_VERSION}-${arch}`;
|
|
1490
|
+
return async (tarball, destDir) => {
|
|
1491
|
+
const result = spawnSync("tar", ["-xzf", tarball, "-C", destDir, "--strip-components=1", `release-v${FIRECRACKER_VERSION}-${arch}/${name}`], { encoding: "utf8" });
|
|
1492
|
+
if (result.status !== 0) throw new Error(`tar could not unpack ${tarball}: ${result.stderr.trim()}`);
|
|
1493
|
+
return join(destDir, name);
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
async function ensureFirecracker(input) {
|
|
1497
|
+
const { dir, arch, pins, fetchImpl } = input;
|
|
1498
|
+
const firecracker = keptPath(dir, pins.firecracker.binarySha256, "firecracker");
|
|
1499
|
+
if (existsSync(firecracker)) {
|
|
1500
|
+
const actual = sha256Of(readFileSync(firecracker));
|
|
1501
|
+
if (actual !== pins.firecracker.binarySha256) throw pinProblem("firecracker", pins.firecracker.binarySha256, actual, `at ${firecracker}`);
|
|
1502
|
+
return firecracker;
|
|
1503
|
+
}
|
|
1504
|
+
const tarballName = `firecracker-${arch}.tgz`;
|
|
1505
|
+
const tarball = await ensureFile(dir, tarballName, pins.firecracker, fetchImpl, RUNNER_FILE_MODE);
|
|
1506
|
+
const scratch = mkdtempSync(join(dir, ".extract-"));
|
|
795
1507
|
try {
|
|
796
|
-
await
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
1508
|
+
const extracted = await (input.extract ?? extractWithTar(arch))(tarball, scratch);
|
|
1509
|
+
const bytes = readFileSync(extracted);
|
|
1510
|
+
const actual = sha256Of(bytes);
|
|
1511
|
+
if (actual !== pins.firecracker.binarySha256) throw pinProblem("firecracker", pins.firecracker.binarySha256, actual, `unpacked from ${tarballName}`);
|
|
1512
|
+
writeAtomic(firecracker, bytes, 493);
|
|
1513
|
+
} finally {
|
|
1514
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
801
1515
|
}
|
|
1516
|
+
return firecracker;
|
|
802
1517
|
}
|
|
803
|
-
async function
|
|
804
|
-
const
|
|
805
|
-
if (
|
|
806
|
-
const
|
|
807
|
-
|
|
808
|
-
|
|
1518
|
+
async function ensureFirecrackerAndJailer(options) {
|
|
1519
|
+
const arch = options.arch ?? firecrackerArch(process.arch);
|
|
1520
|
+
if (!MICROVM_ARCHES.includes(arch)) throw new AssetError("firecracker", `no Firecracker assets are pinned for ${arch}, so microVM builds run on ${MICROVM_ARCHES.join(" or ")} hosts only`);
|
|
1521
|
+
const pins = options.pins ?? pinnedAssets(arch);
|
|
1522
|
+
const dir = vmAssetsDir(options.runnerDir);
|
|
1523
|
+
mkdirSync(dir, { recursive: true, mode: RUNNER_DIR_MODE });
|
|
1524
|
+
const firecracker = await ensureFirecracker({ dir, arch, pins, fetchImpl: options.fetchImpl ?? fetch, extract: options.extractFirecracker });
|
|
1525
|
+
return { firecracker, jailer: await ensureJailer(options) };
|
|
809
1526
|
}
|
|
810
|
-
async function
|
|
811
|
-
const
|
|
812
|
-
if (!
|
|
813
|
-
|
|
814
|
-
|
|
1527
|
+
async function ensureAssets(options) {
|
|
1528
|
+
const arch = options.arch ?? firecrackerArch(process.arch);
|
|
1529
|
+
if (!MICROVM_ARCHES.includes(arch)) throw new AssetError("firecracker", `no Firecracker or guest assets are pinned for ${arch}, so microVM builds run on ${MICROVM_ARCHES.join(" or ")} hosts only`);
|
|
1530
|
+
const pins = options.pins ?? pinnedAssets(arch);
|
|
1531
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1532
|
+
const dir = vmAssetsDir(options.runnerDir);
|
|
1533
|
+
mkdirSync(dir, { recursive: true, mode: RUNNER_DIR_MODE });
|
|
1534
|
+
const firecracker = await ensureFirecracker({ dir, arch, pins, fetchImpl, extract: options.extractFirecracker });
|
|
1535
|
+
const kernel = await ensureFile(dir, `vmlinux-${arch}`, pins.kernel, fetchImpl, RUNNER_FILE_MODE);
|
|
1536
|
+
const helper = await ensureFile(dir, `outerlayer-guest-${arch}`, pins.helper, fetchImpl, 493);
|
|
1537
|
+
return { firecracker, kernel, helper, helperSha256: pins.helper.sha256 };
|
|
1538
|
+
}
|
|
1539
|
+
async function ensureJailer(options) {
|
|
1540
|
+
const arch = options.arch ?? firecrackerArch(process.arch);
|
|
1541
|
+
if (!MICROVM_ARCHES.includes(arch)) throw new AssetError("jailer", `no Firecracker assets are pinned for ${arch}, so microVM builds run on ${MICROVM_ARCHES.join(" or ")} hosts only`);
|
|
1542
|
+
const pins = options.pins ?? pinnedAssets(arch);
|
|
1543
|
+
const expected = pins.firecracker.jailerSha256;
|
|
1544
|
+
if (expected === void 0) throw new AssetError("jailer", "this release pins no hash for the jailer, so it cannot be had");
|
|
1545
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1546
|
+
const dir = vmAssetsDir(options.runnerDir);
|
|
1547
|
+
mkdirSync(dir, { recursive: true, mode: RUNNER_DIR_MODE });
|
|
1548
|
+
const jailer = keptPath(dir, expected, "jailer");
|
|
1549
|
+
if (existsSync(jailer)) {
|
|
1550
|
+
const actual = sha256Of(readFileSync(jailer));
|
|
1551
|
+
if (actual !== expected) throw pinProblem("jailer", expected, actual, `at ${jailer}`);
|
|
1552
|
+
return jailer;
|
|
815
1553
|
}
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
1554
|
+
const tarballName = `firecracker-${arch}.tgz`;
|
|
1555
|
+
const tarball = await ensureFile(dir, tarballName, pins.firecracker, fetchImpl, RUNNER_FILE_MODE);
|
|
1556
|
+
const scratch = mkdtempSync(join(dir, ".extract-"));
|
|
1557
|
+
try {
|
|
1558
|
+
const extracted = await (options.extractJailer ?? extractWithTar(arch, "jailer"))(tarball, scratch);
|
|
1559
|
+
const bytes = readFileSync(extracted);
|
|
1560
|
+
const actual = sha256Of(bytes);
|
|
1561
|
+
if (actual !== expected) throw pinProblem("jailer", expected, actual, `unpacked from ${tarballName}`);
|
|
1562
|
+
writeAtomic(jailer, bytes, 493);
|
|
1563
|
+
} finally {
|
|
1564
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
821
1565
|
}
|
|
822
|
-
|
|
823
|
-
truncateSync(input.dest, input.bytes);
|
|
824
|
-
}
|
|
825
|
-
function deleteBuildDisk(path) {
|
|
826
|
-
rmSync(path, { force: true });
|
|
1566
|
+
return jailer;
|
|
827
1567
|
}
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
try {
|
|
836
|
-
writeSync(fd, data);
|
|
837
|
-
} catch {
|
|
838
|
-
}
|
|
839
|
-
};
|
|
840
|
-
const onData = (chunk) => {
|
|
841
|
-
if (cut) return;
|
|
842
|
-
const room = cap - written;
|
|
843
|
-
if (chunk.length <= room) {
|
|
844
|
-
append(chunk);
|
|
845
|
-
written += chunk.length;
|
|
846
|
-
lastByte = chunk[chunk.length - 1] ?? lastByte;
|
|
847
|
-
return;
|
|
848
|
-
}
|
|
849
|
-
const head = chunk.subarray(0, room);
|
|
850
|
-
append(head);
|
|
851
|
-
if (head.length > 0) lastByte = head[head.length - 1];
|
|
852
|
-
append(Buffer.from(`${lastByte === 10 ? "" : "\n"}${CONSOLE_CUT_NOTE}`));
|
|
853
|
-
written = cap;
|
|
854
|
-
cut = true;
|
|
855
|
-
};
|
|
856
|
-
let open = streams.length;
|
|
857
|
-
for (const stream of streams) {
|
|
858
|
-
stream.on("data", onData);
|
|
859
|
-
stream.on("error", () => void 0);
|
|
860
|
-
stream.once("close", () => {
|
|
861
|
-
open -= 1;
|
|
862
|
-
if (open === 0) closeSync(fd);
|
|
863
|
-
});
|
|
1568
|
+
async function ensureBuilderDisk(options) {
|
|
1569
|
+
const arch = options.arch ?? firecrackerArch(process.arch);
|
|
1570
|
+
if (!MICROVM_ARCHES.includes(arch)) throw new AssetError("builder-rootfs", `no builder root disk is pinned for ${arch}, so microVM image builds run on ${MICROVM_ARCHES.join(" or ")} hosts only`);
|
|
1571
|
+
const file = `builder-rootfs-${arch}.ext4`;
|
|
1572
|
+
const pin = options.pin ?? builderDiskPin(arch);
|
|
1573
|
+
if (options.pin === void 0 && !builderDiskPinned(arch)) {
|
|
1574
|
+
throw new AssetError(file, `${file} is not published for this release of the CLI, so the image cannot be built in a builder microVM. Update the CLI, or set runner.build.imageBuild to host`);
|
|
864
1575
|
}
|
|
1576
|
+
const dir = vmAssetsDir(options.runnerDir);
|
|
1577
|
+
mkdirSync(dir, { recursive: true, mode: RUNNER_DIR_MODE });
|
|
1578
|
+
return ensureFile(dir, file, pin, options.fetchImpl ?? fetch, RUNNER_FILE_MODE);
|
|
865
1579
|
}
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
cpuid_modifiers: [
|
|
871
|
-
{ leaf: "0x1", subleaf: "0x0", flags: 0, modifiers: [{ register: "ecx", bitmap: clearBit(5) }] },
|
|
872
|
-
{ leaf: "0x80000001", subleaf: "0x0", flags: 0, modifiers: [{ register: "ecx", bitmap: clearBit(2) }] }
|
|
873
|
-
]
|
|
874
|
-
};
|
|
875
|
-
var MIB = 1024 ** 2;
|
|
876
|
-
var INIT = "/opt/outerlayer/bin/outerlayer-guest";
|
|
877
|
-
function machineConfig(limits2, paths) {
|
|
878
|
-
const bootArgs = [
|
|
879
|
-
"console=ttyS0",
|
|
880
|
-
"reboot=k",
|
|
881
|
-
"panic=1",
|
|
882
|
-
"pci=off",
|
|
883
|
-
"i8042.noaux",
|
|
884
|
-
"i8042.nomux",
|
|
885
|
-
"i8042.nopnp",
|
|
886
|
-
"i8042.dumbkbd",
|
|
887
|
-
// The pinned guest kernel builds pressure stall information default-disabled; the guest usage record reads it.
|
|
888
|
-
"psi=1",
|
|
889
|
-
"ro",
|
|
890
|
-
"root=/dev/vda",
|
|
891
|
-
"rootfstype=ext4",
|
|
892
|
-
`init=${INIT}`,
|
|
893
|
-
...paths.relayPorts.length === 0 ? [] : [`outerlayer.relay=${paths.relayPorts.join(",")}`],
|
|
894
|
-
...paths.leasePort === void 0 ? [] : [`outerlayer.lease=${paths.leasePort}`],
|
|
895
|
-
...paths.docker === true ? ["outerlayer.docker=1"] : []
|
|
896
|
-
];
|
|
897
|
-
return {
|
|
898
|
-
"boot-source": { kernel_image_path: paths.kernel, boot_args: bootArgs.join(" ") },
|
|
899
|
-
drives: [
|
|
900
|
-
{ drive_id: "rootfs", path_on_host: paths.rootfs, is_root_device: true, is_read_only: true },
|
|
901
|
-
{ drive_id: "build", path_on_host: paths.buildDisk, is_root_device: false, is_read_only: false }
|
|
902
|
-
],
|
|
903
|
-
"machine-config": { vcpu_count: limits2.cpus, mem_size_mib: Math.max(1, Math.floor(limits2.memoryBytes / MIB)), smt: false },
|
|
904
|
-
vsock: { guest_cid: GUEST_CID, uds_path: paths.vsock },
|
|
905
|
-
"cpu-config": CPU_TEMPLATE
|
|
906
|
-
};
|
|
907
|
-
}
|
|
908
|
-
function firecrackerCommand(input) {
|
|
909
|
-
const args = ["--no-api", "--config-file", input.configPath];
|
|
910
|
-
const binary = resolve(input.binary);
|
|
911
|
-
if (input.cgroup === void 0) return { command: binary, args };
|
|
912
|
-
return { command: "/bin/sh", args: ["-c", 'echo $$ > "$1/cgroup.procs" && shift && exec "$@"', "sh", input.cgroup, binary, ...args] };
|
|
1580
|
+
function dockerArch(nodeArch) {
|
|
1581
|
+
if (nodeArch === "x64") return "amd64";
|
|
1582
|
+
if (nodeArch === "arm64") return "arm64";
|
|
1583
|
+
throw new AssetError("tool files", `no tool files are pinned for ${nodeArch}`);
|
|
913
1584
|
}
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
return false;
|
|
922
|
-
}
|
|
1585
|
+
function toolFilePins(arch) {
|
|
1586
|
+
return [
|
|
1587
|
+
...VOICE_PACK_FILES.map((file) => ({ path: `${FETCHED_VOICE_DIR}/${file.name}`, url: file.url, sha256: file.sha256 })),
|
|
1588
|
+
{ path: FETCHED_FFMPEG, url: `https://github.com/BtbN/FFmpeg-Builds/releases/download/${FFMPEG_BUILD_TAG}/${FFMPEG_STATIC[arch].file}`, sha256: FFMPEG_STATIC[arch].sha256 },
|
|
1589
|
+
{ path: FETCHED_GH, url: `https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_${arch}.tar.gz`, sha256: GH_SHA256[arch] },
|
|
1590
|
+
{ path: FETCHED_TINI, url: `https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini-static-${arch}`, sha256: TINI_SHA256[arch] }
|
|
1591
|
+
];
|
|
923
1592
|
}
|
|
924
|
-
function
|
|
925
|
-
const
|
|
926
|
-
const
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
return { pid, start };
|
|
948
|
-
} finally {
|
|
949
|
-
if (!pumping) closeSync(consoleFd);
|
|
950
|
-
}
|
|
951
|
-
},
|
|
952
|
-
async stop(vm) {
|
|
953
|
-
if (!alive(vm)) return;
|
|
954
|
-
try {
|
|
955
|
-
process.kill(vm.pid, "SIGKILL");
|
|
956
|
-
} catch {
|
|
1593
|
+
function pinsDir(runnerDir2, pins) {
|
|
1594
|
+
const key = createHash("sha256").update(pins.map((pin) => `${pin.path} ${pin.sha256}`).join("\n")).digest("hex").slice(0, 16);
|
|
1595
|
+
const root = join(vmAssetsDir(runnerDir2), "tool-files");
|
|
1596
|
+
return { root, dir: join(root, key) };
|
|
1597
|
+
}
|
|
1598
|
+
var inFlight = /* @__PURE__ */ new Map();
|
|
1599
|
+
async function ensureToolFiles(options) {
|
|
1600
|
+
const pins = options.pins ?? toolFilePins(options.arch ?? dockerArch(process.arch));
|
|
1601
|
+
const { root, dir } = pinsDir(options.runnerDir, pins);
|
|
1602
|
+
const running = inFlight.get(dir);
|
|
1603
|
+
if (running !== void 0) return running;
|
|
1604
|
+
const fetching = fetchAll(root, dir, pins, options.fetchImpl ?? fetch).finally(() => inFlight.delete(dir));
|
|
1605
|
+
inFlight.set(dir, fetching);
|
|
1606
|
+
return fetching;
|
|
1607
|
+
}
|
|
1608
|
+
async function fetchAll(root, dir, pins, fetchImpl) {
|
|
1609
|
+
mkdirSync(dir, { recursive: true, mode: RUNNER_DIR_MODE });
|
|
1610
|
+
for (const pin of pins) {
|
|
1611
|
+
const path = join(dir, pin.path);
|
|
1612
|
+
if (existsSync(path)) {
|
|
1613
|
+
const actual = await sha256OfFile(path);
|
|
1614
|
+
if (actual !== pin.sha256) {
|
|
1615
|
+
throw new AssetError(pin.path, `the tool file ${path} has sha256 ${actual}, not the ${pin.sha256} this release pins, so no image build starts. Delete it to download it again`);
|
|
957
1616
|
}
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
}
|
|
1617
|
+
continue;
|
|
1618
|
+
}
|
|
1619
|
+
try {
|
|
1620
|
+
await downloadPinned({ url: pin.url, dest: path, sha256: pin.sha256, name: pin.path, fetchImpl, stopped: "no image build started" });
|
|
1621
|
+
} catch (err) {
|
|
1622
|
+
throw new AssetError(pin.path, `the tool file ${pin.path} could not be had: ${err instanceof VoiceDownloadError ? err.message : String(err)}`);
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
for (const name of readdirSync(root)) {
|
|
1626
|
+
if (join(root, name) !== dir) rmSync(join(root, name), { recursive: true, force: true });
|
|
1627
|
+
}
|
|
1628
|
+
return dir;
|
|
966
1629
|
}
|
|
967
1630
|
|
|
968
1631
|
// src/runner/microvm/host.ts
|
|
969
1632
|
function createMicroVmPorts(over = {}) {
|
|
970
1633
|
return {
|
|
971
|
-
assets: over.assets ?? ((
|
|
1634
|
+
assets: over.assets ?? ((runnerDir2) => ensureAssets({ runnerDir: runnerDir2 })),
|
|
972
1635
|
rootfs: over.rootfs ?? createRootfsHost(),
|
|
973
1636
|
firecracker: over.firecracker ?? createFirecrackerHost(),
|
|
974
1637
|
cgroup: over.cgroup ?? realCgroupPort(),
|
|
975
1638
|
helper: over.helper ?? realHelperPort(),
|
|
976
1639
|
fdStore: over.fdStore ?? realFdStorePort(),
|
|
977
|
-
exec: over.exec ?? execCapture
|
|
1640
|
+
exec: over.exec ?? execCapture,
|
|
1641
|
+
execStream: over.execStream ?? runExec,
|
|
1642
|
+
builderDisk: over.builderDisk ?? ((runnerDir2) => ensureBuilderDisk({ runnerDir: runnerDir2 })),
|
|
1643
|
+
builderDiskPinned: over.builderDiskPinned ?? (() => builderDiskPinned()),
|
|
1644
|
+
toolFiles: over.toolFiles ?? ((runnerDir2) => ensureToolFiles({ runnerDir: runnerDir2 }))
|
|
978
1645
|
};
|
|
979
1646
|
}
|
|
980
1647
|
var aborted = () => ({ ok: false, reason: "", aborted: true });
|
|
@@ -1947,7 +2614,7 @@ function upstreamHeaders(incoming, authorization) {
|
|
|
1947
2614
|
function isWorkflowRefusal(text) {
|
|
1948
2615
|
return WORKFLOW_REFUSAL.test(text);
|
|
1949
2616
|
}
|
|
1950
|
-
function
|
|
2617
|
+
function plain2(res, status, text) {
|
|
1951
2618
|
res.writeHead(status, { "content-type": "text/plain", "content-length": Buffer.byteLength(text), connection: "close" });
|
|
1952
2619
|
res.end(text);
|
|
1953
2620
|
}
|
|
@@ -2038,7 +2705,7 @@ function createGitRemote(options) {
|
|
|
2038
2705
|
upstream.on("error", (err) => {
|
|
2039
2706
|
log(`could not reach GitHub for ${route.kind}: ${err.message}`);
|
|
2040
2707
|
if (res.headersSent) res.destroy();
|
|
2041
|
-
else
|
|
2708
|
+
else plain2(res, 502, "the runner could not reach GitHub\n");
|
|
2042
2709
|
resolve2();
|
|
2043
2710
|
});
|
|
2044
2711
|
res.once("close", () => upstream.destroy());
|
|
@@ -2050,7 +2717,7 @@ function createGitRemote(options) {
|
|
|
2050
2717
|
return await options.tokens.token(kind, repository);
|
|
2051
2718
|
} catch (err) {
|
|
2052
2719
|
log(`could not get a ${kind} token: ${refusalCode(err)}`);
|
|
2053
|
-
|
|
2720
|
+
plain2(res, 502, `the runner could not get a ${kind} token: ${refusalCode(err)}
|
|
2054
2721
|
`);
|
|
2055
2722
|
return void 0;
|
|
2056
2723
|
}
|
|
@@ -2062,7 +2729,7 @@ function createGitRemote(options) {
|
|
|
2062
2729
|
const refuse = async (reason) => {
|
|
2063
2730
|
log(`push refused: ${reason}`);
|
|
2064
2731
|
await drain(req);
|
|
2065
|
-
|
|
2732
|
+
plain2(res, 400, `the runner refused this push: ${reason}
|
|
2066
2733
|
`);
|
|
2067
2734
|
};
|
|
2068
2735
|
if (encoding !== void 0 && encoding !== "gzip" && encoding !== "identity") {
|
|
@@ -2146,19 +2813,19 @@ function createGitRemote(options) {
|
|
|
2146
2813
|
const route = classifyRequest(req.method ?? "", req.url ?? "");
|
|
2147
2814
|
if (route === void 0) {
|
|
2148
2815
|
req.resume();
|
|
2149
|
-
return
|
|
2816
|
+
return plain2(res, 404, "not found\n");
|
|
2150
2817
|
}
|
|
2151
2818
|
if (options.readOnly === true && (route.kind === "receive-pack" || route.kind === "info-refs" && route.service === "git-receive-pack")) {
|
|
2152
2819
|
log(`push refused: this remote is read-only`);
|
|
2153
2820
|
req.resume();
|
|
2154
|
-
return
|
|
2821
|
+
return plain2(res, 403, "this remote is read-only: a check cannot push\n");
|
|
2155
2822
|
}
|
|
2156
2823
|
const target = claimed.find((entry) => isOwnRepository(entry.repository, route.owner, route.name));
|
|
2157
2824
|
const pushing = route.kind === "receive-pack" || route.kind === "info-refs" && route.service === "git-receive-pack";
|
|
2158
2825
|
if (target === void 0 && pushing) {
|
|
2159
2826
|
log(`push refused: ${route.owner}/${route.name} is not a repository of this build's claim`);
|
|
2160
2827
|
req.resume();
|
|
2161
|
-
return
|
|
2828
|
+
return plain2(res, 403, `the runner refused this push: ${route.owner}/${route.name} is not a repository of this build's claim
|
|
2162
2829
|
`);
|
|
2163
2830
|
}
|
|
2164
2831
|
if (target !== void 0 && route.kind === "receive-pack") return push(req, res, route, target);
|
|
@@ -2166,9 +2833,9 @@ function createGitRemote(options) {
|
|
|
2166
2833
|
let body;
|
|
2167
2834
|
if (route.kind === "lfs-batch") {
|
|
2168
2835
|
body = await readBody(req, maxLfsBodyBytes);
|
|
2169
|
-
if (body === void 0) return
|
|
2836
|
+
if (body === void 0) return plain2(res, 413, "the request is too large\n");
|
|
2170
2837
|
kind = isLfsUpload(body) ? "push" : "read";
|
|
2171
|
-
if (options.readOnly === true && kind === "push") return
|
|
2838
|
+
if (options.readOnly === true && kind === "push") return plain2(res, 403, "this remote is read-only: a check cannot upload\n");
|
|
2172
2839
|
}
|
|
2173
2840
|
let authorization;
|
|
2174
2841
|
if (target !== void 0) {
|
|
@@ -2197,7 +2864,7 @@ function createGitRemote(options) {
|
|
|
2197
2864
|
} catch (err) {
|
|
2198
2865
|
log(`could not serve a request: ${err instanceof Error ? err.message : String(err)}`);
|
|
2199
2866
|
if (res.headersSent) res.destroy();
|
|
2200
|
-
else
|
|
2867
|
+
else plain2(res, 502, "the runner could not serve this request\n");
|
|
2201
2868
|
}
|
|
2202
2869
|
};
|
|
2203
2870
|
}
|
|
@@ -2692,6 +3359,8 @@ function withTunnelLog(opts, log) {
|
|
|
2692
3359
|
}
|
|
2693
3360
|
function createContainerRuntime(opts = {}) {
|
|
2694
3361
|
const { cli, remoteFor } = opts;
|
|
3362
|
+
const vm = createMicroVmPorts(opts.vm);
|
|
3363
|
+
const sleep = opts.sleep ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms)));
|
|
2695
3364
|
return {
|
|
2696
3365
|
host: opts.host ?? createContainerHost(),
|
|
2697
3366
|
cpus: opts.cpus ?? createCpuAllocator(usableCpus()),
|
|
@@ -2712,9 +3381,13 @@ function createContainerRuntime(opts = {}) {
|
|
|
2712
3381
|
},
|
|
2713
3382
|
{ host: opts.imageHost ?? createDockerHost({ builder: builderSpec(build.configPath, build.buildMemory, opts.hostEnv ?? process.env) }) }
|
|
2714
3383
|
)),
|
|
3384
|
+
buildRootDisk: opts.buildRootDisk ?? ((build) => buildRootDiskInMicroVm(
|
|
3385
|
+
{ ...build, remote: remoteFor?.(build.repository), cli: cli ?? currentCliBuild() },
|
|
3386
|
+
{ ports: vm, startTunnel: opts.startTunnel ?? startTunnel, tunnelOptions: opts.tunnelOptions, sleep }
|
|
3387
|
+
)),
|
|
2715
3388
|
startTunnel: opts.startTunnel ?? startTunnel,
|
|
2716
3389
|
tunnelOptions: opts.tunnelOptions,
|
|
2717
|
-
sleep
|
|
3390
|
+
sleep,
|
|
2718
3391
|
hostEnv: opts.hostEnv ?? process.env,
|
|
2719
3392
|
platform: opts.platform ?? process.platform,
|
|
2720
3393
|
credentials: opts.credentials ?? /* @__PURE__ */ new Map(),
|
|
@@ -2724,7 +3397,7 @@ function createContainerRuntime(opts = {}) {
|
|
|
2724
3397
|
clone: opts.clone ?? ((input) => cloneRepository(input.remote, input.dest)),
|
|
2725
3398
|
destinationOptions: opts.destinationOptions,
|
|
2726
3399
|
claudeUpstream: opts.claudeUpstream,
|
|
2727
|
-
vm
|
|
3400
|
+
vm,
|
|
2728
3401
|
cliBuild: () => cli ?? currentCliBuild()
|
|
2729
3402
|
};
|
|
2730
3403
|
}
|
|
@@ -2852,27 +3525,21 @@ async function obtainImage(input) {
|
|
|
2852
3525
|
};
|
|
2853
3526
|
}
|
|
2854
3527
|
try {
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
built: await runtime.
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
buildMemory: config.build?.memory ?? "8g",
|
|
2861
|
-
readToken,
|
|
2862
|
-
pinned: input.pinned,
|
|
2863
|
-
guestHelper: input.guestHelper,
|
|
2864
|
-
log: (line) => log.line(`${tag} ${line}`)
|
|
2865
|
-
})
|
|
2866
|
-
};
|
|
3528
|
+
const common = { repository, configPath: config.configPath, readToken, pinned: input.pinned, log: (line) => log.line(`${tag} ${line}`) };
|
|
3529
|
+
if (input.inBuilder !== void 0) {
|
|
3530
|
+
return { ok: true, built: await runtime.buildRootDisk({ ...input.inBuilder, ...common, guestHelper: input.guestHelper, deadlineMs: input.deadlineMs }) };
|
|
3531
|
+
}
|
|
3532
|
+
return { ok: true, built: await runtime.buildImage({ ...common, buildMemory: config.build?.memory ?? "8g", guestHelper: input.guestHelper }) };
|
|
2867
3533
|
} catch (err) {
|
|
2868
3534
|
if (err instanceof RecipeError && err.code === "build_in_progress" && (input.now?.() ?? /* @__PURE__ */ new Date()).getTime() < input.deadlineMs) {
|
|
2869
3535
|
log.line(`${tag} waiting for another build of ${repository}'s image`);
|
|
2870
3536
|
await runtime.sleep(BUSY_IMAGE_WAIT_MS);
|
|
2871
3537
|
continue;
|
|
2872
3538
|
}
|
|
3539
|
+
if (err instanceof AssetError) return { ok: false, reason: err.message };
|
|
2873
3540
|
if (err instanceof RecipeError) {
|
|
2874
3541
|
log.error(`${tag} image build for ${repository} failed: ${err.code}: ${err.message}`);
|
|
2875
|
-
const says = err.code === "image_build_out_of_memory" || err.code === "image_builder_unavailable";
|
|
3542
|
+
const says = err.code === "image_build_out_of_memory" || err.code === "image_builder_unavailable" || err.code === "image_builder_failed";
|
|
2876
3543
|
return { ok: false, reason: says ? err.message : err.code };
|
|
2877
3544
|
}
|
|
2878
3545
|
return { ok: false, reason: `the image build failed: ${describeCallFailure(err)}` };
|
|
@@ -3551,4 +4218,4 @@ async function startCheckContainer(input) {
|
|
|
3551
4218
|
};
|
|
3552
4219
|
}
|
|
3553
4220
|
|
|
3554
|
-
export { ADDED_USER, AgentUserError, AssetError, DeliveryFailure, EVAL_INPUT_NAME, GH_CONFIG_YML, HELPER_ENV,
|
|
4221
|
+
export { ADDED_USER, AgentUserError, AssetError, DeliveryFailure, EVAL_INPUT_NAME, GH_CONFIG_YML, HELPER_ENV, LEASE_SECONDS, MANAGED_UNJAILED_REASON, MIN_ENGINE_MAJOR, ROOT_DISK_ID_PREFIX, cleanupContainer, containerCommandExec, containerJudgeEnvironment, containerSenderCommand, copyJudgeTree, copyTranscripts, copyTranscriptsFromContainer, createBuildDisk, createContainerRuntime, createLeaseChannel, deleteBuildDisk, deliverContainerHooks, ensureFirecrackerAndJailer, ensureRootfs, ensureTunnel, filesAreCopied, formatBytes, freeBytes, ghHostsYml, gitRemoteFor, helperFirecrackerNote, helperSocketPath, isLiteralAddress, keepGhConfigCurrent, leaseExpiresAtMs, leaseTimeLeftMs, lifecycleFailureReason, lifecycleSteps, machineConfig, makeBuildsDirPrivate, measureDisk, obtainImage, outOfMemoryReason, parseCpuList, prepareBranch, probeHelper, provisionContainer, resolveAgentUser, runBuildSteps, senderAliveScript, senderSignalScript, setUpRunnerCgroup, shellQuote, signalInContainer, startCheckContainer, startJailedInto, startServicesFor, stopContainer, stopJailed, sweepImageBuilders, syncInContainer, withTunnelLog, writeEvalInput };
|