@rallycry/conveyor-k3 0.1.11 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +6117 -1521
- package/dist/index.d.ts +311 -84
- package/dist/index.js +2382 -475
- package/dist/vm-qemu.js +256 -91
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -21,8 +21,61 @@ function buildConveyorSocketOptions(auth) {
|
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
// src/daemon/control-endpoint.ts
|
|
25
|
+
import { lookup as dnsLookup } from "dns";
|
|
26
|
+
import { existsSync, readFileSync } from "fs";
|
|
27
|
+
import { Agent } from "https";
|
|
28
|
+
var CONTROL_FILE = "/etc/conveyor-vm/control.json";
|
|
29
|
+
var CONTROL_ALIAS_IP = /^192\.168\.5\.24[0-7]$/;
|
|
30
|
+
function readControlFile(path) {
|
|
31
|
+
try {
|
|
32
|
+
return readFileSync(path, "utf8");
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function controlAlias(apiUrl, text) {
|
|
38
|
+
if (!text) return void 0;
|
|
39
|
+
let hostname;
|
|
40
|
+
let aliases;
|
|
41
|
+
try {
|
|
42
|
+
hostname = new URL(apiUrl).hostname.toLowerCase();
|
|
43
|
+
aliases = JSON.parse(text);
|
|
44
|
+
} catch {
|
|
45
|
+
return void 0;
|
|
46
|
+
}
|
|
47
|
+
if (typeof aliases !== "object" || aliases === null || Array.isArray(aliases)) return void 0;
|
|
48
|
+
const alias = aliases[hostname];
|
|
49
|
+
return typeof alias === "string" && CONTROL_ALIAS_IP.test(alias) ? { hostname, alias } : void 0;
|
|
50
|
+
}
|
|
51
|
+
function controlPlaneRoute(apiUrl, readFile3 = readControlFile, fileExists = existsSync) {
|
|
52
|
+
if (!fileExists(CONTROL_FILE)) return void 0;
|
|
53
|
+
let skipAlias = false;
|
|
54
|
+
let usedAlias = false;
|
|
55
|
+
const lookup = (hostname, options, callback) => {
|
|
56
|
+
const target = skipAlias ? void 0 : controlAlias(apiUrl, readFile3(CONTROL_FILE));
|
|
57
|
+
usedAlias = target !== void 0 && hostname.toLowerCase() === target.hostname;
|
|
58
|
+
if (!target || !usedAlias) {
|
|
59
|
+
dnsLookup(hostname, options, callback);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (options.all) {
|
|
63
|
+
const addresses = [{ address: target.alias, family: 4 }];
|
|
64
|
+
callback(null, addresses);
|
|
65
|
+
} else callback(null, target.alias, 4);
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
agent: new Agent({ lookup }),
|
|
69
|
+
// After a failure through the alias, try DNS; after a failure through DNS,
|
|
70
|
+
// try the alias again. A lasting fault in either route costs one attempt.
|
|
71
|
+
failed: () => {
|
|
72
|
+
skipAlias = usedAlias;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
24
77
|
// src/daemon/device-proof.ts
|
|
25
|
-
import { lstatSync, readFileSync } from "fs";
|
|
78
|
+
import { lstatSync, readFileSync as readFileSync2 } from "fs";
|
|
26
79
|
import { createPrivateKey, randomBytes, sign } from "crypto";
|
|
27
80
|
var DEVICE_KEY_PATH = "/etc/conveyor/device-key.pem";
|
|
28
81
|
function privateDeviceKey(path, ownerUid) {
|
|
@@ -37,7 +90,7 @@ function privateDeviceKey(path, ownerUid) {
|
|
|
37
90
|
throw new Error("Conveyor device key must be a regular file.");
|
|
38
91
|
if ((stat.mode & 511) !== 384 || stat.uid !== ownerUid)
|
|
39
92
|
throw new Error("Conveyor device key must be owned by root with mode 0600.");
|
|
40
|
-
const pem =
|
|
93
|
+
const pem = readFileSync2(path, "utf8");
|
|
41
94
|
if (Buffer.byteLength(pem) > 16 * 1024) throw new Error("Conveyor device key is too large.");
|
|
42
95
|
const key = createPrivateKey(pem);
|
|
43
96
|
if (key.asymmetricKeyType !== "ed25519")
|
|
@@ -48,7 +101,7 @@ function createClusterProof(apiUrl, clusterToken, key) {
|
|
|
48
101
|
const origin = new URL(apiUrl).origin;
|
|
49
102
|
const timestamp = Date.now();
|
|
50
103
|
const nonce = randomBytes(32).toString("base64url");
|
|
51
|
-
const
|
|
104
|
+
const message2 = `conveyor-device-v1
|
|
52
105
|
${origin}
|
|
53
106
|
${clusterToken}
|
|
54
107
|
${timestamp}
|
|
@@ -56,7 +109,7 @@ ${nonce}`;
|
|
|
56
109
|
return {
|
|
57
110
|
timestamp,
|
|
58
111
|
nonce,
|
|
59
|
-
signature: sign(null, Buffer.from(
|
|
112
|
+
signature: sign(null, Buffer.from(message2, "utf8"), key).toString("base64url")
|
|
60
113
|
};
|
|
61
114
|
}
|
|
62
115
|
function deviceClusterAuth(apiUrl, clusterToken, keyPath = DEVICE_KEY_PATH, ownerUid = 0) {
|
|
@@ -434,104 +487,104 @@ ZodError.create = (issues) => {
|
|
|
434
487
|
|
|
435
488
|
// ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/locales/en.js
|
|
436
489
|
var errorMap = (issue, _ctx) => {
|
|
437
|
-
let
|
|
490
|
+
let message2;
|
|
438
491
|
switch (issue.code) {
|
|
439
492
|
case ZodIssueCode.invalid_type:
|
|
440
493
|
if (issue.received === ZodParsedType.undefined) {
|
|
441
|
-
|
|
494
|
+
message2 = "Required";
|
|
442
495
|
} else {
|
|
443
|
-
|
|
496
|
+
message2 = `Expected ${issue.expected}, received ${issue.received}`;
|
|
444
497
|
}
|
|
445
498
|
break;
|
|
446
499
|
case ZodIssueCode.invalid_literal:
|
|
447
|
-
|
|
500
|
+
message2 = `Invalid literal value, expected ${JSON.stringify(issue.expected, util.jsonStringifyReplacer)}`;
|
|
448
501
|
break;
|
|
449
502
|
case ZodIssueCode.unrecognized_keys:
|
|
450
|
-
|
|
503
|
+
message2 = `Unrecognized key(s) in object: ${util.joinValues(issue.keys, ", ")}`;
|
|
451
504
|
break;
|
|
452
505
|
case ZodIssueCode.invalid_union:
|
|
453
|
-
|
|
506
|
+
message2 = `Invalid input`;
|
|
454
507
|
break;
|
|
455
508
|
case ZodIssueCode.invalid_union_discriminator:
|
|
456
|
-
|
|
509
|
+
message2 = `Invalid discriminator value. Expected ${util.joinValues(issue.options)}`;
|
|
457
510
|
break;
|
|
458
511
|
case ZodIssueCode.invalid_enum_value:
|
|
459
|
-
|
|
512
|
+
message2 = `Invalid enum value. Expected ${util.joinValues(issue.options)}, received '${issue.received}'`;
|
|
460
513
|
break;
|
|
461
514
|
case ZodIssueCode.invalid_arguments:
|
|
462
|
-
|
|
515
|
+
message2 = `Invalid function arguments`;
|
|
463
516
|
break;
|
|
464
517
|
case ZodIssueCode.invalid_return_type:
|
|
465
|
-
|
|
518
|
+
message2 = `Invalid function return type`;
|
|
466
519
|
break;
|
|
467
520
|
case ZodIssueCode.invalid_date:
|
|
468
|
-
|
|
521
|
+
message2 = `Invalid date`;
|
|
469
522
|
break;
|
|
470
523
|
case ZodIssueCode.invalid_string:
|
|
471
524
|
if (typeof issue.validation === "object") {
|
|
472
525
|
if ("includes" in issue.validation) {
|
|
473
|
-
|
|
526
|
+
message2 = `Invalid input: must include "${issue.validation.includes}"`;
|
|
474
527
|
if (typeof issue.validation.position === "number") {
|
|
475
|
-
|
|
528
|
+
message2 = `${message2} at one or more positions greater than or equal to ${issue.validation.position}`;
|
|
476
529
|
}
|
|
477
530
|
} else if ("startsWith" in issue.validation) {
|
|
478
|
-
|
|
531
|
+
message2 = `Invalid input: must start with "${issue.validation.startsWith}"`;
|
|
479
532
|
} else if ("endsWith" in issue.validation) {
|
|
480
|
-
|
|
533
|
+
message2 = `Invalid input: must end with "${issue.validation.endsWith}"`;
|
|
481
534
|
} else {
|
|
482
535
|
util.assertNever(issue.validation);
|
|
483
536
|
}
|
|
484
537
|
} else if (issue.validation !== "regex") {
|
|
485
|
-
|
|
538
|
+
message2 = `Invalid ${issue.validation}`;
|
|
486
539
|
} else {
|
|
487
|
-
|
|
540
|
+
message2 = "Invalid";
|
|
488
541
|
}
|
|
489
542
|
break;
|
|
490
543
|
case ZodIssueCode.too_small:
|
|
491
544
|
if (issue.type === "array")
|
|
492
|
-
|
|
545
|
+
message2 = `Array must contain ${issue.exact ? "exactly" : issue.inclusive ? `at least` : `more than`} ${issue.minimum} element(s)`;
|
|
493
546
|
else if (issue.type === "string")
|
|
494
|
-
|
|
547
|
+
message2 = `String must contain ${issue.exact ? "exactly" : issue.inclusive ? `at least` : `over`} ${issue.minimum} character(s)`;
|
|
495
548
|
else if (issue.type === "number")
|
|
496
|
-
|
|
549
|
+
message2 = `Number must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${issue.minimum}`;
|
|
497
550
|
else if (issue.type === "bigint")
|
|
498
|
-
|
|
551
|
+
message2 = `Number must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${issue.minimum}`;
|
|
499
552
|
else if (issue.type === "date")
|
|
500
|
-
|
|
553
|
+
message2 = `Date must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${new Date(Number(issue.minimum))}`;
|
|
501
554
|
else
|
|
502
|
-
|
|
555
|
+
message2 = "Invalid input";
|
|
503
556
|
break;
|
|
504
557
|
case ZodIssueCode.too_big:
|
|
505
558
|
if (issue.type === "array")
|
|
506
|
-
|
|
559
|
+
message2 = `Array must contain ${issue.exact ? `exactly` : issue.inclusive ? `at most` : `less than`} ${issue.maximum} element(s)`;
|
|
507
560
|
else if (issue.type === "string")
|
|
508
|
-
|
|
561
|
+
message2 = `String must contain ${issue.exact ? `exactly` : issue.inclusive ? `at most` : `under`} ${issue.maximum} character(s)`;
|
|
509
562
|
else if (issue.type === "number")
|
|
510
|
-
|
|
563
|
+
message2 = `Number must be ${issue.exact ? `exactly` : issue.inclusive ? `less than or equal to` : `less than`} ${issue.maximum}`;
|
|
511
564
|
else if (issue.type === "bigint")
|
|
512
|
-
|
|
565
|
+
message2 = `BigInt must be ${issue.exact ? `exactly` : issue.inclusive ? `less than or equal to` : `less than`} ${issue.maximum}`;
|
|
513
566
|
else if (issue.type === "date")
|
|
514
|
-
|
|
567
|
+
message2 = `Date must be ${issue.exact ? `exactly` : issue.inclusive ? `smaller than or equal to` : `smaller than`} ${new Date(Number(issue.maximum))}`;
|
|
515
568
|
else
|
|
516
|
-
|
|
569
|
+
message2 = "Invalid input";
|
|
517
570
|
break;
|
|
518
571
|
case ZodIssueCode.custom:
|
|
519
|
-
|
|
572
|
+
message2 = `Invalid input`;
|
|
520
573
|
break;
|
|
521
574
|
case ZodIssueCode.invalid_intersection_types:
|
|
522
|
-
|
|
575
|
+
message2 = `Intersection results could not be merged`;
|
|
523
576
|
break;
|
|
524
577
|
case ZodIssueCode.not_multiple_of:
|
|
525
|
-
|
|
578
|
+
message2 = `Number must be a multiple of ${issue.multipleOf}`;
|
|
526
579
|
break;
|
|
527
580
|
case ZodIssueCode.not_finite:
|
|
528
|
-
|
|
581
|
+
message2 = "Number must be finite";
|
|
529
582
|
break;
|
|
530
583
|
default:
|
|
531
|
-
|
|
584
|
+
message2 = _ctx.defaultError;
|
|
532
585
|
util.assertNever(issue);
|
|
533
586
|
}
|
|
534
|
-
return { message };
|
|
587
|
+
return { message: message2 };
|
|
535
588
|
};
|
|
536
589
|
var en_default = errorMap;
|
|
537
590
|
|
|
@@ -657,8 +710,8 @@ var isAsync = (x) => typeof Promise !== "undefined" && x instanceof Promise;
|
|
|
657
710
|
// ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/errorUtil.js
|
|
658
711
|
var errorUtil;
|
|
659
712
|
(function(errorUtil2) {
|
|
660
|
-
errorUtil2.errToObj = (
|
|
661
|
-
errorUtil2.toString = (
|
|
713
|
+
errorUtil2.errToObj = (message2) => typeof message2 === "string" ? { message: message2 } : message2 || {};
|
|
714
|
+
errorUtil2.toString = (message2) => typeof message2 === "string" ? message2 : message2?.message;
|
|
662
715
|
})(errorUtil || (errorUtil = {}));
|
|
663
716
|
|
|
664
717
|
// ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/types.js
|
|
@@ -710,16 +763,16 @@ function processCreateParams(params) {
|
|
|
710
763
|
if (errorMap2)
|
|
711
764
|
return { errorMap: errorMap2, description };
|
|
712
765
|
const customMap = (iss, ctx) => {
|
|
713
|
-
const { message } = params;
|
|
766
|
+
const { message: message2 } = params;
|
|
714
767
|
if (iss.code === "invalid_enum_value") {
|
|
715
|
-
return { message:
|
|
768
|
+
return { message: message2 ?? ctx.defaultError };
|
|
716
769
|
}
|
|
717
770
|
if (typeof ctx.data === "undefined") {
|
|
718
|
-
return { message:
|
|
771
|
+
return { message: message2 ?? required_error ?? ctx.defaultError };
|
|
719
772
|
}
|
|
720
773
|
if (iss.code !== "invalid_type")
|
|
721
774
|
return { message: ctx.defaultError };
|
|
722
|
-
return { message:
|
|
775
|
+
return { message: message2 ?? invalid_type_error ?? ctx.defaultError };
|
|
723
776
|
};
|
|
724
777
|
return { errorMap: customMap, description };
|
|
725
778
|
}
|
|
@@ -845,14 +898,14 @@ var ZodType = class {
|
|
|
845
898
|
const result = await (isAsync(maybeAsyncResult) ? maybeAsyncResult : Promise.resolve(maybeAsyncResult));
|
|
846
899
|
return handleResult(ctx, result);
|
|
847
900
|
}
|
|
848
|
-
refine(check,
|
|
901
|
+
refine(check, message2) {
|
|
849
902
|
const getIssueProperties = (val) => {
|
|
850
|
-
if (typeof
|
|
851
|
-
return { message };
|
|
852
|
-
} else if (typeof
|
|
853
|
-
return
|
|
903
|
+
if (typeof message2 === "string" || typeof message2 === "undefined") {
|
|
904
|
+
return { message: message2 };
|
|
905
|
+
} else if (typeof message2 === "function") {
|
|
906
|
+
return message2(val);
|
|
854
907
|
} else {
|
|
855
|
-
return
|
|
908
|
+
return message2;
|
|
856
909
|
}
|
|
857
910
|
};
|
|
858
911
|
return this._refinement((val, ctx) => {
|
|
@@ -1388,11 +1441,11 @@ var ZodString = class _ZodString extends ZodType {
|
|
|
1388
1441
|
}
|
|
1389
1442
|
return { status: status.value, value: input.data };
|
|
1390
1443
|
}
|
|
1391
|
-
_regex(regex, validation,
|
|
1444
|
+
_regex(regex, validation, message2) {
|
|
1392
1445
|
return this.refinement((data) => regex.test(data), {
|
|
1393
1446
|
validation,
|
|
1394
1447
|
code: ZodIssueCode.invalid_string,
|
|
1395
|
-
...errorUtil.errToObj(
|
|
1448
|
+
...errorUtil.errToObj(message2)
|
|
1396
1449
|
});
|
|
1397
1450
|
}
|
|
1398
1451
|
_addCheck(check) {
|
|
@@ -1401,37 +1454,37 @@ var ZodString = class _ZodString extends ZodType {
|
|
|
1401
1454
|
checks: [...this._def.checks, check]
|
|
1402
1455
|
});
|
|
1403
1456
|
}
|
|
1404
|
-
email(
|
|
1405
|
-
return this._addCheck({ kind: "email", ...errorUtil.errToObj(
|
|
1457
|
+
email(message2) {
|
|
1458
|
+
return this._addCheck({ kind: "email", ...errorUtil.errToObj(message2) });
|
|
1406
1459
|
}
|
|
1407
|
-
url(
|
|
1408
|
-
return this._addCheck({ kind: "url", ...errorUtil.errToObj(
|
|
1460
|
+
url(message2) {
|
|
1461
|
+
return this._addCheck({ kind: "url", ...errorUtil.errToObj(message2) });
|
|
1409
1462
|
}
|
|
1410
|
-
emoji(
|
|
1411
|
-
return this._addCheck({ kind: "emoji", ...errorUtil.errToObj(
|
|
1463
|
+
emoji(message2) {
|
|
1464
|
+
return this._addCheck({ kind: "emoji", ...errorUtil.errToObj(message2) });
|
|
1412
1465
|
}
|
|
1413
|
-
uuid(
|
|
1414
|
-
return this._addCheck({ kind: "uuid", ...errorUtil.errToObj(
|
|
1466
|
+
uuid(message2) {
|
|
1467
|
+
return this._addCheck({ kind: "uuid", ...errorUtil.errToObj(message2) });
|
|
1415
1468
|
}
|
|
1416
|
-
nanoid(
|
|
1417
|
-
return this._addCheck({ kind: "nanoid", ...errorUtil.errToObj(
|
|
1469
|
+
nanoid(message2) {
|
|
1470
|
+
return this._addCheck({ kind: "nanoid", ...errorUtil.errToObj(message2) });
|
|
1418
1471
|
}
|
|
1419
|
-
cuid(
|
|
1420
|
-
return this._addCheck({ kind: "cuid", ...errorUtil.errToObj(
|
|
1472
|
+
cuid(message2) {
|
|
1473
|
+
return this._addCheck({ kind: "cuid", ...errorUtil.errToObj(message2) });
|
|
1421
1474
|
}
|
|
1422
|
-
cuid2(
|
|
1423
|
-
return this._addCheck({ kind: "cuid2", ...errorUtil.errToObj(
|
|
1475
|
+
cuid2(message2) {
|
|
1476
|
+
return this._addCheck({ kind: "cuid2", ...errorUtil.errToObj(message2) });
|
|
1424
1477
|
}
|
|
1425
|
-
ulid(
|
|
1426
|
-
return this._addCheck({ kind: "ulid", ...errorUtil.errToObj(
|
|
1478
|
+
ulid(message2) {
|
|
1479
|
+
return this._addCheck({ kind: "ulid", ...errorUtil.errToObj(message2) });
|
|
1427
1480
|
}
|
|
1428
|
-
base64(
|
|
1429
|
-
return this._addCheck({ kind: "base64", ...errorUtil.errToObj(
|
|
1481
|
+
base64(message2) {
|
|
1482
|
+
return this._addCheck({ kind: "base64", ...errorUtil.errToObj(message2) });
|
|
1430
1483
|
}
|
|
1431
|
-
base64url(
|
|
1484
|
+
base64url(message2) {
|
|
1432
1485
|
return this._addCheck({
|
|
1433
1486
|
kind: "base64url",
|
|
1434
|
-
...errorUtil.errToObj(
|
|
1487
|
+
...errorUtil.errToObj(message2)
|
|
1435
1488
|
});
|
|
1436
1489
|
}
|
|
1437
1490
|
jwt(options) {
|
|
@@ -1461,8 +1514,8 @@ var ZodString = class _ZodString extends ZodType {
|
|
|
1461
1514
|
...errorUtil.errToObj(options?.message)
|
|
1462
1515
|
});
|
|
1463
1516
|
}
|
|
1464
|
-
date(
|
|
1465
|
-
return this._addCheck({ kind: "date", message });
|
|
1517
|
+
date(message2) {
|
|
1518
|
+
return this._addCheck({ kind: "date", message: message2 });
|
|
1466
1519
|
}
|
|
1467
1520
|
time(options) {
|
|
1468
1521
|
if (typeof options === "string") {
|
|
@@ -1478,14 +1531,14 @@ var ZodString = class _ZodString extends ZodType {
|
|
|
1478
1531
|
...errorUtil.errToObj(options?.message)
|
|
1479
1532
|
});
|
|
1480
1533
|
}
|
|
1481
|
-
duration(
|
|
1482
|
-
return this._addCheck({ kind: "duration", ...errorUtil.errToObj(
|
|
1534
|
+
duration(message2) {
|
|
1535
|
+
return this._addCheck({ kind: "duration", ...errorUtil.errToObj(message2) });
|
|
1483
1536
|
}
|
|
1484
|
-
regex(regex,
|
|
1537
|
+
regex(regex, message2) {
|
|
1485
1538
|
return this._addCheck({
|
|
1486
1539
|
kind: "regex",
|
|
1487
1540
|
regex,
|
|
1488
|
-
...errorUtil.errToObj(
|
|
1541
|
+
...errorUtil.errToObj(message2)
|
|
1489
1542
|
});
|
|
1490
1543
|
}
|
|
1491
1544
|
includes(value, options) {
|
|
@@ -1496,46 +1549,46 @@ var ZodString = class _ZodString extends ZodType {
|
|
|
1496
1549
|
...errorUtil.errToObj(options?.message)
|
|
1497
1550
|
});
|
|
1498
1551
|
}
|
|
1499
|
-
startsWith(value,
|
|
1552
|
+
startsWith(value, message2) {
|
|
1500
1553
|
return this._addCheck({
|
|
1501
1554
|
kind: "startsWith",
|
|
1502
1555
|
value,
|
|
1503
|
-
...errorUtil.errToObj(
|
|
1556
|
+
...errorUtil.errToObj(message2)
|
|
1504
1557
|
});
|
|
1505
1558
|
}
|
|
1506
|
-
endsWith(value,
|
|
1559
|
+
endsWith(value, message2) {
|
|
1507
1560
|
return this._addCheck({
|
|
1508
1561
|
kind: "endsWith",
|
|
1509
1562
|
value,
|
|
1510
|
-
...errorUtil.errToObj(
|
|
1563
|
+
...errorUtil.errToObj(message2)
|
|
1511
1564
|
});
|
|
1512
1565
|
}
|
|
1513
|
-
min(minLength,
|
|
1566
|
+
min(minLength, message2) {
|
|
1514
1567
|
return this._addCheck({
|
|
1515
1568
|
kind: "min",
|
|
1516
1569
|
value: minLength,
|
|
1517
|
-
...errorUtil.errToObj(
|
|
1570
|
+
...errorUtil.errToObj(message2)
|
|
1518
1571
|
});
|
|
1519
1572
|
}
|
|
1520
|
-
max(maxLength,
|
|
1573
|
+
max(maxLength, message2) {
|
|
1521
1574
|
return this._addCheck({
|
|
1522
1575
|
kind: "max",
|
|
1523
1576
|
value: maxLength,
|
|
1524
|
-
...errorUtil.errToObj(
|
|
1577
|
+
...errorUtil.errToObj(message2)
|
|
1525
1578
|
});
|
|
1526
1579
|
}
|
|
1527
|
-
length(len,
|
|
1580
|
+
length(len, message2) {
|
|
1528
1581
|
return this._addCheck({
|
|
1529
1582
|
kind: "length",
|
|
1530
1583
|
value: len,
|
|
1531
|
-
...errorUtil.errToObj(
|
|
1584
|
+
...errorUtil.errToObj(message2)
|
|
1532
1585
|
});
|
|
1533
1586
|
}
|
|
1534
1587
|
/**
|
|
1535
1588
|
* Equivalent to `.min(1)`
|
|
1536
1589
|
*/
|
|
1537
|
-
nonempty(
|
|
1538
|
-
return this.min(1, errorUtil.errToObj(
|
|
1590
|
+
nonempty(message2) {
|
|
1591
|
+
return this.min(1, errorUtil.errToObj(message2));
|
|
1539
1592
|
}
|
|
1540
1593
|
trim() {
|
|
1541
1594
|
return new _ZodString({
|
|
@@ -1728,19 +1781,19 @@ var ZodNumber = class _ZodNumber extends ZodType {
|
|
|
1728
1781
|
}
|
|
1729
1782
|
return { status: status.value, value: input.data };
|
|
1730
1783
|
}
|
|
1731
|
-
gte(value,
|
|
1732
|
-
return this.setLimit("min", value, true, errorUtil.toString(
|
|
1784
|
+
gte(value, message2) {
|
|
1785
|
+
return this.setLimit("min", value, true, errorUtil.toString(message2));
|
|
1733
1786
|
}
|
|
1734
|
-
gt(value,
|
|
1735
|
-
return this.setLimit("min", value, false, errorUtil.toString(
|
|
1787
|
+
gt(value, message2) {
|
|
1788
|
+
return this.setLimit("min", value, false, errorUtil.toString(message2));
|
|
1736
1789
|
}
|
|
1737
|
-
lte(value,
|
|
1738
|
-
return this.setLimit("max", value, true, errorUtil.toString(
|
|
1790
|
+
lte(value, message2) {
|
|
1791
|
+
return this.setLimit("max", value, true, errorUtil.toString(message2));
|
|
1739
1792
|
}
|
|
1740
|
-
lt(value,
|
|
1741
|
-
return this.setLimit("max", value, false, errorUtil.toString(
|
|
1793
|
+
lt(value, message2) {
|
|
1794
|
+
return this.setLimit("max", value, false, errorUtil.toString(message2));
|
|
1742
1795
|
}
|
|
1743
|
-
setLimit(kind, value, inclusive,
|
|
1796
|
+
setLimit(kind, value, inclusive, message2) {
|
|
1744
1797
|
return new _ZodNumber({
|
|
1745
1798
|
...this._def,
|
|
1746
1799
|
checks: [
|
|
@@ -1749,7 +1802,7 @@ var ZodNumber = class _ZodNumber extends ZodType {
|
|
|
1749
1802
|
kind,
|
|
1750
1803
|
value,
|
|
1751
1804
|
inclusive,
|
|
1752
|
-
message: errorUtil.toString(
|
|
1805
|
+
message: errorUtil.toString(message2)
|
|
1753
1806
|
}
|
|
1754
1807
|
]
|
|
1755
1808
|
});
|
|
@@ -1760,68 +1813,68 @@ var ZodNumber = class _ZodNumber extends ZodType {
|
|
|
1760
1813
|
checks: [...this._def.checks, check]
|
|
1761
1814
|
});
|
|
1762
1815
|
}
|
|
1763
|
-
int(
|
|
1816
|
+
int(message2) {
|
|
1764
1817
|
return this._addCheck({
|
|
1765
1818
|
kind: "int",
|
|
1766
|
-
message: errorUtil.toString(
|
|
1819
|
+
message: errorUtil.toString(message2)
|
|
1767
1820
|
});
|
|
1768
1821
|
}
|
|
1769
|
-
positive(
|
|
1822
|
+
positive(message2) {
|
|
1770
1823
|
return this._addCheck({
|
|
1771
1824
|
kind: "min",
|
|
1772
1825
|
value: 0,
|
|
1773
1826
|
inclusive: false,
|
|
1774
|
-
message: errorUtil.toString(
|
|
1827
|
+
message: errorUtil.toString(message2)
|
|
1775
1828
|
});
|
|
1776
1829
|
}
|
|
1777
|
-
negative(
|
|
1830
|
+
negative(message2) {
|
|
1778
1831
|
return this._addCheck({
|
|
1779
1832
|
kind: "max",
|
|
1780
1833
|
value: 0,
|
|
1781
1834
|
inclusive: false,
|
|
1782
|
-
message: errorUtil.toString(
|
|
1835
|
+
message: errorUtil.toString(message2)
|
|
1783
1836
|
});
|
|
1784
1837
|
}
|
|
1785
|
-
nonpositive(
|
|
1838
|
+
nonpositive(message2) {
|
|
1786
1839
|
return this._addCheck({
|
|
1787
1840
|
kind: "max",
|
|
1788
1841
|
value: 0,
|
|
1789
1842
|
inclusive: true,
|
|
1790
|
-
message: errorUtil.toString(
|
|
1843
|
+
message: errorUtil.toString(message2)
|
|
1791
1844
|
});
|
|
1792
1845
|
}
|
|
1793
|
-
nonnegative(
|
|
1846
|
+
nonnegative(message2) {
|
|
1794
1847
|
return this._addCheck({
|
|
1795
1848
|
kind: "min",
|
|
1796
1849
|
value: 0,
|
|
1797
1850
|
inclusive: true,
|
|
1798
|
-
message: errorUtil.toString(
|
|
1851
|
+
message: errorUtil.toString(message2)
|
|
1799
1852
|
});
|
|
1800
1853
|
}
|
|
1801
|
-
multipleOf(value,
|
|
1854
|
+
multipleOf(value, message2) {
|
|
1802
1855
|
return this._addCheck({
|
|
1803
1856
|
kind: "multipleOf",
|
|
1804
1857
|
value,
|
|
1805
|
-
message: errorUtil.toString(
|
|
1858
|
+
message: errorUtil.toString(message2)
|
|
1806
1859
|
});
|
|
1807
1860
|
}
|
|
1808
|
-
finite(
|
|
1861
|
+
finite(message2) {
|
|
1809
1862
|
return this._addCheck({
|
|
1810
1863
|
kind: "finite",
|
|
1811
|
-
message: errorUtil.toString(
|
|
1864
|
+
message: errorUtil.toString(message2)
|
|
1812
1865
|
});
|
|
1813
1866
|
}
|
|
1814
|
-
safe(
|
|
1867
|
+
safe(message2) {
|
|
1815
1868
|
return this._addCheck({
|
|
1816
1869
|
kind: "min",
|
|
1817
1870
|
inclusive: true,
|
|
1818
1871
|
value: Number.MIN_SAFE_INTEGER,
|
|
1819
|
-
message: errorUtil.toString(
|
|
1872
|
+
message: errorUtil.toString(message2)
|
|
1820
1873
|
})._addCheck({
|
|
1821
1874
|
kind: "max",
|
|
1822
1875
|
inclusive: true,
|
|
1823
1876
|
value: Number.MAX_SAFE_INTEGER,
|
|
1824
|
-
message: errorUtil.toString(
|
|
1877
|
+
message: errorUtil.toString(message2)
|
|
1825
1878
|
});
|
|
1826
1879
|
}
|
|
1827
1880
|
get minValue() {
|
|
@@ -1944,19 +1997,19 @@ var ZodBigInt = class _ZodBigInt extends ZodType {
|
|
|
1944
1997
|
});
|
|
1945
1998
|
return INVALID;
|
|
1946
1999
|
}
|
|
1947
|
-
gte(value,
|
|
1948
|
-
return this.setLimit("min", value, true, errorUtil.toString(
|
|
2000
|
+
gte(value, message2) {
|
|
2001
|
+
return this.setLimit("min", value, true, errorUtil.toString(message2));
|
|
1949
2002
|
}
|
|
1950
|
-
gt(value,
|
|
1951
|
-
return this.setLimit("min", value, false, errorUtil.toString(
|
|
2003
|
+
gt(value, message2) {
|
|
2004
|
+
return this.setLimit("min", value, false, errorUtil.toString(message2));
|
|
1952
2005
|
}
|
|
1953
|
-
lte(value,
|
|
1954
|
-
return this.setLimit("max", value, true, errorUtil.toString(
|
|
2006
|
+
lte(value, message2) {
|
|
2007
|
+
return this.setLimit("max", value, true, errorUtil.toString(message2));
|
|
1955
2008
|
}
|
|
1956
|
-
lt(value,
|
|
1957
|
-
return this.setLimit("max", value, false, errorUtil.toString(
|
|
2009
|
+
lt(value, message2) {
|
|
2010
|
+
return this.setLimit("max", value, false, errorUtil.toString(message2));
|
|
1958
2011
|
}
|
|
1959
|
-
setLimit(kind, value, inclusive,
|
|
2012
|
+
setLimit(kind, value, inclusive, message2) {
|
|
1960
2013
|
return new _ZodBigInt({
|
|
1961
2014
|
...this._def,
|
|
1962
2015
|
checks: [
|
|
@@ -1965,7 +2018,7 @@ var ZodBigInt = class _ZodBigInt extends ZodType {
|
|
|
1965
2018
|
kind,
|
|
1966
2019
|
value,
|
|
1967
2020
|
inclusive,
|
|
1968
|
-
message: errorUtil.toString(
|
|
2021
|
+
message: errorUtil.toString(message2)
|
|
1969
2022
|
}
|
|
1970
2023
|
]
|
|
1971
2024
|
});
|
|
@@ -1976,43 +2029,43 @@ var ZodBigInt = class _ZodBigInt extends ZodType {
|
|
|
1976
2029
|
checks: [...this._def.checks, check]
|
|
1977
2030
|
});
|
|
1978
2031
|
}
|
|
1979
|
-
positive(
|
|
2032
|
+
positive(message2) {
|
|
1980
2033
|
return this._addCheck({
|
|
1981
2034
|
kind: "min",
|
|
1982
2035
|
value: BigInt(0),
|
|
1983
2036
|
inclusive: false,
|
|
1984
|
-
message: errorUtil.toString(
|
|
2037
|
+
message: errorUtil.toString(message2)
|
|
1985
2038
|
});
|
|
1986
2039
|
}
|
|
1987
|
-
negative(
|
|
2040
|
+
negative(message2) {
|
|
1988
2041
|
return this._addCheck({
|
|
1989
2042
|
kind: "max",
|
|
1990
2043
|
value: BigInt(0),
|
|
1991
2044
|
inclusive: false,
|
|
1992
|
-
message: errorUtil.toString(
|
|
2045
|
+
message: errorUtil.toString(message2)
|
|
1993
2046
|
});
|
|
1994
2047
|
}
|
|
1995
|
-
nonpositive(
|
|
2048
|
+
nonpositive(message2) {
|
|
1996
2049
|
return this._addCheck({
|
|
1997
2050
|
kind: "max",
|
|
1998
2051
|
value: BigInt(0),
|
|
1999
2052
|
inclusive: true,
|
|
2000
|
-
message: errorUtil.toString(
|
|
2053
|
+
message: errorUtil.toString(message2)
|
|
2001
2054
|
});
|
|
2002
2055
|
}
|
|
2003
|
-
nonnegative(
|
|
2056
|
+
nonnegative(message2) {
|
|
2004
2057
|
return this._addCheck({
|
|
2005
2058
|
kind: "min",
|
|
2006
2059
|
value: BigInt(0),
|
|
2007
2060
|
inclusive: true,
|
|
2008
|
-
message: errorUtil.toString(
|
|
2061
|
+
message: errorUtil.toString(message2)
|
|
2009
2062
|
});
|
|
2010
2063
|
}
|
|
2011
|
-
multipleOf(value,
|
|
2064
|
+
multipleOf(value, message2) {
|
|
2012
2065
|
return this._addCheck({
|
|
2013
2066
|
kind: "multipleOf",
|
|
2014
2067
|
value,
|
|
2015
|
-
message: errorUtil.toString(
|
|
2068
|
+
message: errorUtil.toString(message2)
|
|
2016
2069
|
});
|
|
2017
2070
|
}
|
|
2018
2071
|
get minValue() {
|
|
@@ -2135,18 +2188,18 @@ var ZodDate = class _ZodDate extends ZodType {
|
|
|
2135
2188
|
checks: [...this._def.checks, check]
|
|
2136
2189
|
});
|
|
2137
2190
|
}
|
|
2138
|
-
min(minDate,
|
|
2191
|
+
min(minDate, message2) {
|
|
2139
2192
|
return this._addCheck({
|
|
2140
2193
|
kind: "min",
|
|
2141
2194
|
value: minDate.getTime(),
|
|
2142
|
-
message: errorUtil.toString(
|
|
2195
|
+
message: errorUtil.toString(message2)
|
|
2143
2196
|
});
|
|
2144
2197
|
}
|
|
2145
|
-
max(maxDate,
|
|
2198
|
+
max(maxDate, message2) {
|
|
2146
2199
|
return this._addCheck({
|
|
2147
2200
|
kind: "max",
|
|
2148
2201
|
value: maxDate.getTime(),
|
|
2149
|
-
message: errorUtil.toString(
|
|
2202
|
+
message: errorUtil.toString(message2)
|
|
2150
2203
|
});
|
|
2151
2204
|
}
|
|
2152
2205
|
get minDate() {
|
|
@@ -2378,26 +2431,26 @@ var ZodArray = class _ZodArray extends ZodType {
|
|
|
2378
2431
|
get element() {
|
|
2379
2432
|
return this._def.type;
|
|
2380
2433
|
}
|
|
2381
|
-
min(minLength,
|
|
2434
|
+
min(minLength, message2) {
|
|
2382
2435
|
return new _ZodArray({
|
|
2383
2436
|
...this._def,
|
|
2384
|
-
minLength: { value: minLength, message: errorUtil.toString(
|
|
2437
|
+
minLength: { value: minLength, message: errorUtil.toString(message2) }
|
|
2385
2438
|
});
|
|
2386
2439
|
}
|
|
2387
|
-
max(maxLength,
|
|
2440
|
+
max(maxLength, message2) {
|
|
2388
2441
|
return new _ZodArray({
|
|
2389
2442
|
...this._def,
|
|
2390
|
-
maxLength: { value: maxLength, message: errorUtil.toString(
|
|
2443
|
+
maxLength: { value: maxLength, message: errorUtil.toString(message2) }
|
|
2391
2444
|
});
|
|
2392
2445
|
}
|
|
2393
|
-
length(len,
|
|
2446
|
+
length(len, message2) {
|
|
2394
2447
|
return new _ZodArray({
|
|
2395
2448
|
...this._def,
|
|
2396
|
-
exactLength: { value: len, message: errorUtil.toString(
|
|
2449
|
+
exactLength: { value: len, message: errorUtil.toString(message2) }
|
|
2397
2450
|
});
|
|
2398
2451
|
}
|
|
2399
|
-
nonempty(
|
|
2400
|
-
return this.min(1,
|
|
2452
|
+
nonempty(message2) {
|
|
2453
|
+
return this.min(1, message2);
|
|
2401
2454
|
}
|
|
2402
2455
|
};
|
|
2403
2456
|
ZodArray.create = (schema, params) => {
|
|
@@ -2540,17 +2593,17 @@ var ZodObject = class _ZodObject extends ZodType {
|
|
|
2540
2593
|
get shape() {
|
|
2541
2594
|
return this._def.shape();
|
|
2542
2595
|
}
|
|
2543
|
-
strict(
|
|
2596
|
+
strict(message2) {
|
|
2544
2597
|
errorUtil.errToObj;
|
|
2545
2598
|
return new _ZodObject({
|
|
2546
2599
|
...this._def,
|
|
2547
2600
|
unknownKeys: "strict",
|
|
2548
|
-
...
|
|
2601
|
+
...message2 !== void 0 ? {
|
|
2549
2602
|
errorMap: (issue, ctx) => {
|
|
2550
2603
|
const defaultError = this._def.errorMap?.(issue, ctx).message ?? ctx.defaultError;
|
|
2551
2604
|
if (issue.code === "unrecognized_keys")
|
|
2552
2605
|
return {
|
|
2553
|
-
message: errorUtil.errToObj(
|
|
2606
|
+
message: errorUtil.errToObj(message2).message ?? defaultError
|
|
2554
2607
|
};
|
|
2555
2608
|
return {
|
|
2556
2609
|
message: defaultError
|
|
@@ -3306,23 +3359,23 @@ var ZodSet = class _ZodSet extends ZodType {
|
|
|
3306
3359
|
return finalizeSet(elements);
|
|
3307
3360
|
}
|
|
3308
3361
|
}
|
|
3309
|
-
min(minSize,
|
|
3362
|
+
min(minSize, message2) {
|
|
3310
3363
|
return new _ZodSet({
|
|
3311
3364
|
...this._def,
|
|
3312
|
-
minSize: { value: minSize, message: errorUtil.toString(
|
|
3365
|
+
minSize: { value: minSize, message: errorUtil.toString(message2) }
|
|
3313
3366
|
});
|
|
3314
3367
|
}
|
|
3315
|
-
max(maxSize,
|
|
3368
|
+
max(maxSize, message2) {
|
|
3316
3369
|
return new _ZodSet({
|
|
3317
3370
|
...this._def,
|
|
3318
|
-
maxSize: { value: maxSize, message: errorUtil.toString(
|
|
3371
|
+
maxSize: { value: maxSize, message: errorUtil.toString(message2) }
|
|
3319
3372
|
});
|
|
3320
3373
|
}
|
|
3321
|
-
size(size,
|
|
3322
|
-
return this.min(size,
|
|
3374
|
+
size(size, message2) {
|
|
3375
|
+
return this.min(size, message2).max(size, message2);
|
|
3323
3376
|
}
|
|
3324
|
-
nonempty(
|
|
3325
|
-
return this.min(1,
|
|
3377
|
+
nonempty(message2) {
|
|
3378
|
+
return this.min(1, message2);
|
|
3326
3379
|
}
|
|
3327
3380
|
};
|
|
3328
3381
|
ZodSet.create = (valueType, params) => {
|
|
@@ -4109,7 +4162,7 @@ var coerce = {
|
|
|
4109
4162
|
};
|
|
4110
4163
|
var NEVER = INVALID;
|
|
4111
4164
|
|
|
4112
|
-
// ../shared/dist/chunk-
|
|
4165
|
+
// ../shared/dist/chunk-VSUBG7GR.js
|
|
4113
4166
|
var DEFAULT_SONNET_MODEL = "claude-sonnet-5-5";
|
|
4114
4167
|
var DEFAULT_OPUS_MODEL = "claude-opus-5-5";
|
|
4115
4168
|
var DEFAULT_HAIKU_MODEL = "claude-haiku-4-5-20251001";
|
|
@@ -4600,6 +4653,11 @@ var DeleteTaskFileRequestSchema = external_exports.object({
|
|
|
4600
4653
|
sessionId: external_exports.string(),
|
|
4601
4654
|
fileId: external_exports.string()
|
|
4602
4655
|
});
|
|
4656
|
+
var SetTaskFileShowcaseRequestSchema = external_exports.object({
|
|
4657
|
+
sessionId: external_exports.string(),
|
|
4658
|
+
fileId: external_exports.string(),
|
|
4659
|
+
showcase: external_exports.boolean()
|
|
4660
|
+
});
|
|
4603
4661
|
var GetTaskRequestSchema = external_exports.object({
|
|
4604
4662
|
sessionId: external_exports.string(),
|
|
4605
4663
|
taskSlugOrId: external_exports.string()
|
|
@@ -4648,7 +4706,11 @@ var ConfirmFileUploadRequestSchema = external_exports.object({
|
|
|
4648
4706
|
fileId: external_exports.string(),
|
|
4649
4707
|
title: external_exports.string().max(500).optional(),
|
|
4650
4708
|
/** Glossary tag names (or ids) this file is an example of. */
|
|
4651
|
-
tags: external_exports.array(external_exports.string().min(1).max(MAX_FILE_TAG_LENGTH)).max(MAX_FILE_TAGS).optional()
|
|
4709
|
+
tags: external_exports.array(external_exports.string().min(1).max(MAX_FILE_TAG_LENGTH)).max(MAX_FILE_TAGS).optional(),
|
|
4710
|
+
/** Mark the file as the card's showcase (the deliverable). */
|
|
4711
|
+
showcase: external_exports.boolean().optional(),
|
|
4712
|
+
/** Id of an older file on the same card this upload replaces. */
|
|
4713
|
+
supersedes: external_exports.string().min(1).max(64).optional()
|
|
4652
4714
|
});
|
|
4653
4715
|
var UpdateTaskStatusRequestSchema = external_exports.object({
|
|
4654
4716
|
sessionId: external_exports.string(),
|
|
@@ -4718,7 +4780,8 @@ var DiscoveredPortSchema = external_exports.object({
|
|
|
4718
4780
|
port: external_exports.number().int().min(1).max(65535),
|
|
4719
4781
|
label: external_exports.string().min(1).max(64).optional(),
|
|
4720
4782
|
protocol: external_exports.enum(["http", "tcp"]).optional(),
|
|
4721
|
-
detectedAt: external_exports.string()
|
|
4783
|
+
detectedAt: external_exports.string(),
|
|
4784
|
+
hidden: external_exports.boolean().optional()
|
|
4722
4785
|
});
|
|
4723
4786
|
var ReportDiscoveredPortsRequestSchema = external_exports.object({
|
|
4724
4787
|
sessionId: external_exports.string(),
|
|
@@ -4865,7 +4928,11 @@ var SubmitCodeReviewResultRequestSchema = external_exports.object({
|
|
|
4865
4928
|
var CycleCodingAgentKeyRequestSchema = external_exports.object({
|
|
4866
4929
|
sessionId: external_exports.string(),
|
|
4867
4930
|
rateLimitType: external_exports.string(),
|
|
4868
|
-
resetsAt: external_exports.string().optional()
|
|
4931
|
+
resetsAt: external_exports.string().optional(),
|
|
4932
|
+
// The key the pod is actually running (CONVEYOR_CODING_AGENT_KEY_ID). Wins
|
|
4933
|
+
// over the session's attribution stamp after owner validation; optional so
|
|
4934
|
+
// older agents keep the session fallback.
|
|
4935
|
+
codingAgentKeyId: external_exports.string().optional()
|
|
4869
4936
|
});
|
|
4870
4937
|
var PostChildChatMessageRequestSchema = external_exports.object({
|
|
4871
4938
|
sessionId: external_exports.string(),
|
|
@@ -5183,6 +5250,9 @@ var CreateProjectTaskRequestSchema = external_exports.object({
|
|
|
5183
5250
|
status: external_exports.string().optional(),
|
|
5184
5251
|
// Assign to a sub-project board. Validated to belong to `projectId` in the handler.
|
|
5185
5252
|
subProjectId: external_exports.string().nullable().optional(),
|
|
5253
|
+
// Priority value. Resolved to the project's configured Priority row in the
|
|
5254
|
+
// handler, which rejects an unconfigured value before the card is written.
|
|
5255
|
+
priorityValue: external_exports.number().int().positive().optional(),
|
|
5186
5256
|
requestingUserId: external_exports.string().optional()
|
|
5187
5257
|
});
|
|
5188
5258
|
var SetProjectTaskParentRequestSchema = external_exports.object({
|
|
@@ -5212,6 +5282,9 @@ var UpdateProjectTaskRequestSchema = external_exports.object({
|
|
|
5212
5282
|
// Story-point value, or null to clear. Resolved to the project's configured
|
|
5213
5283
|
// StoryPoint row in the handler, which rejects an unconfigured value.
|
|
5214
5284
|
storyPointValue: external_exports.number().int().positive().nullable().optional(),
|
|
5285
|
+
// Priority value, or null to clear. Resolved to the project's configured
|
|
5286
|
+
// Priority row in the handler, which rejects an unconfigured value.
|
|
5287
|
+
priorityValue: external_exports.number().int().positive().nullable().optional(),
|
|
5215
5288
|
assignedUserId: external_exports.string().nullish(),
|
|
5216
5289
|
// Move to a different sub-project board, or null to move to the parent board.
|
|
5217
5290
|
// Validated to belong to `projectId` in the handler.
|
|
@@ -5228,9 +5301,9 @@ var UpdateProjectTaskRequestSchema = external_exports.object({
|
|
|
5228
5301
|
onHold: external_exports.boolean().optional(),
|
|
5229
5302
|
requestingUserId: external_exports.string().optional()
|
|
5230
5303
|
}).strict().refine(
|
|
5231
|
-
(v) => v.title !== void 0 || v.description !== void 0 || v.plan !== void 0 || v.status !== void 0 || v.risk !== void 0 || v.storyPointValue !== void 0 || v.assignedUserId !== void 0 || v.subProjectId !== void 0 || v.githubBranch !== void 0 || v.onHold !== void 0,
|
|
5304
|
+
(v) => v.title !== void 0 || v.description !== void 0 || v.plan !== void 0 || v.status !== void 0 || v.risk !== void 0 || v.storyPointValue !== void 0 || v.priorityValue !== void 0 || v.assignedUserId !== void 0 || v.subProjectId !== void 0 || v.githubBranch !== void 0 || v.onHold !== void 0,
|
|
5232
5305
|
{
|
|
5233
|
-
message: "update_task requires at least one field to change (title, description, plan, status, risk, storyPointValue, assignedUserId, subProjectId, githubBranch, or onHold)"
|
|
5306
|
+
message: "update_task requires at least one field to change (title, description, plan, status, risk, storyPointValue, priorityValue, assignedUserId, subProjectId, githubBranch, or onHold)"
|
|
5234
5307
|
}
|
|
5235
5308
|
);
|
|
5236
5309
|
var TransitionProjectTaskStatusRequestSchema = external_exports.object({
|
|
@@ -5273,11 +5346,12 @@ var QueryProjectGcpLogsRequestSchema = external_exports.object({
|
|
|
5273
5346
|
sqlInstances: external_exports.array(external_exports.string().min(1).max(200)).max(25).optional(),
|
|
5274
5347
|
allServices: external_exports.boolean().optional(),
|
|
5275
5348
|
search: external_exports.string().max(256).optional(),
|
|
5276
|
-
filter: external_exports.string().max(
|
|
5349
|
+
filter: external_exports.string().max(2e3).optional(),
|
|
5277
5350
|
startTime: external_exports.string().optional(),
|
|
5278
5351
|
endTime: external_exports.string().optional(),
|
|
5279
5352
|
limit: external_exports.number().int().min(1).max(200).optional().default(50),
|
|
5280
|
-
pageToken: external_exports.string().max(4096).optional()
|
|
5353
|
+
pageToken: external_exports.string().max(4096).optional(),
|
|
5354
|
+
full: external_exports.boolean().optional()
|
|
5281
5355
|
});
|
|
5282
5356
|
var QueryProjectGrafanaLogsRequestSchema = external_exports.object({
|
|
5283
5357
|
projectId: external_exports.string(),
|
|
@@ -5478,43 +5552,6 @@ var GetProjectTaskDependenciesRequestSchema = external_exports.object({
|
|
|
5478
5552
|
projectId: external_exports.string(),
|
|
5479
5553
|
taskId: external_exports.string()
|
|
5480
5554
|
});
|
|
5481
|
-
var ListProjectTaskFilesRequestSchema = external_exports.object({
|
|
5482
|
-
projectId: external_exports.string(),
|
|
5483
|
-
taskId: external_exports.string()
|
|
5484
|
-
});
|
|
5485
|
-
var GetProjectAttachmentRequestSchema = external_exports.object({
|
|
5486
|
-
projectId: external_exports.string(),
|
|
5487
|
-
taskId: external_exports.string(),
|
|
5488
|
-
fileId: external_exports.string(),
|
|
5489
|
-
/** Byte offset into text content (paging large logs/JSON). Default 0. */
|
|
5490
|
-
offset: external_exports.number().int().nonnegative().optional(),
|
|
5491
|
-
/** Max bytes of text content to return from `offset`. Server default applies. */
|
|
5492
|
-
maxBytes: external_exports.number().int().positive().optional()
|
|
5493
|
-
});
|
|
5494
|
-
var DeleteProjectTaskFileRequestSchema = external_exports.object({
|
|
5495
|
-
projectId: external_exports.string(),
|
|
5496
|
-
taskId: external_exports.string(),
|
|
5497
|
-
fileId: external_exports.string(),
|
|
5498
|
-
requestingUserId: external_exports.string().optional()
|
|
5499
|
-
});
|
|
5500
|
-
var RequestProjectFileUploadRequestSchema = external_exports.object({
|
|
5501
|
-
projectId: external_exports.string(),
|
|
5502
|
-
taskId: external_exports.string(),
|
|
5503
|
-
fileName: external_exports.string().min(1).max(255),
|
|
5504
|
-
mimeType: external_exports.string().min(1).max(128),
|
|
5505
|
-
fileSize: external_exports.number().int().positive().max(MAX_FILE_SIZE_BYTES),
|
|
5506
|
-
requestingUserId: external_exports.string().optional()
|
|
5507
|
-
});
|
|
5508
|
-
var ConfirmProjectFileUploadRequestSchema = external_exports.object({
|
|
5509
|
-
projectId: external_exports.string(),
|
|
5510
|
-
taskId: external_exports.string(),
|
|
5511
|
-
fileId: external_exports.string(),
|
|
5512
|
-
/** When set, the attachment is also posted to the task chat with this text. */
|
|
5513
|
-
comment: external_exports.string().max(2e3).optional(),
|
|
5514
|
-
/** Glossary tag names (or ids) this file is an example of. */
|
|
5515
|
-
tags: external_exports.array(external_exports.string().min(1).max(MAX_FILE_TAG_LENGTH)).max(MAX_FILE_TAGS).optional(),
|
|
5516
|
-
requestingUserId: external_exports.string().optional()
|
|
5517
|
-
});
|
|
5518
5555
|
var CreateProjectPullRequestRequestSchema = external_exports.object({
|
|
5519
5556
|
projectId: external_exports.string(),
|
|
5520
5557
|
taskId: external_exports.string(),
|
|
@@ -5621,6 +5658,54 @@ var GetProjectAnalyticsSummaryRequestSchema = external_exports.object({
|
|
|
5621
5658
|
rangeDays: external_exports.number().int().min(1).max(GET_ANALYTICS_SUMMARY_MAX_RANGE_DAYS).optional(),
|
|
5622
5659
|
campaign: external_exports.string().max(200).optional()
|
|
5623
5660
|
});
|
|
5661
|
+
var ListProjectTaskFilesRequestSchema = external_exports.object({
|
|
5662
|
+
projectId: external_exports.string(),
|
|
5663
|
+
taskId: external_exports.string()
|
|
5664
|
+
});
|
|
5665
|
+
var GetProjectAttachmentRequestSchema = external_exports.object({
|
|
5666
|
+
projectId: external_exports.string(),
|
|
5667
|
+
taskId: external_exports.string(),
|
|
5668
|
+
fileId: external_exports.string(),
|
|
5669
|
+
/** Byte offset into text content (paging large logs/JSON). Default 0. */
|
|
5670
|
+
offset: external_exports.number().int().nonnegative().optional(),
|
|
5671
|
+
/** Max bytes of text content to return from `offset`. Server default applies. */
|
|
5672
|
+
maxBytes: external_exports.number().int().positive().optional()
|
|
5673
|
+
});
|
|
5674
|
+
var DeleteProjectTaskFileRequestSchema = external_exports.object({
|
|
5675
|
+
projectId: external_exports.string(),
|
|
5676
|
+
taskId: external_exports.string(),
|
|
5677
|
+
fileId: external_exports.string(),
|
|
5678
|
+
requestingUserId: external_exports.string().optional()
|
|
5679
|
+
});
|
|
5680
|
+
var SetProjectTaskFileShowcaseRequestSchema = external_exports.object({
|
|
5681
|
+
projectId: external_exports.string(),
|
|
5682
|
+
taskId: external_exports.string(),
|
|
5683
|
+
fileId: external_exports.string(),
|
|
5684
|
+
showcase: external_exports.boolean(),
|
|
5685
|
+
requestingUserId: external_exports.string().optional()
|
|
5686
|
+
});
|
|
5687
|
+
var RequestProjectFileUploadRequestSchema = external_exports.object({
|
|
5688
|
+
projectId: external_exports.string(),
|
|
5689
|
+
taskId: external_exports.string(),
|
|
5690
|
+
fileName: external_exports.string().min(1).max(255),
|
|
5691
|
+
mimeType: external_exports.string().min(1).max(128),
|
|
5692
|
+
fileSize: external_exports.number().int().positive().max(MAX_FILE_SIZE_BYTES),
|
|
5693
|
+
requestingUserId: external_exports.string().optional()
|
|
5694
|
+
});
|
|
5695
|
+
var ConfirmProjectFileUploadRequestSchema = external_exports.object({
|
|
5696
|
+
projectId: external_exports.string(),
|
|
5697
|
+
taskId: external_exports.string(),
|
|
5698
|
+
fileId: external_exports.string(),
|
|
5699
|
+
/** When set, the attachment is also posted to the task chat with this text. */
|
|
5700
|
+
comment: external_exports.string().max(2e3).optional(),
|
|
5701
|
+
/** Glossary tag names (or ids) this file is an example of. */
|
|
5702
|
+
tags: external_exports.array(external_exports.string().min(1).max(MAX_FILE_TAG_LENGTH)).max(MAX_FILE_TAGS).optional(),
|
|
5703
|
+
/** The card's showcase (its deliverable); always posts to chat, even with no `comment`. */
|
|
5704
|
+
showcase: external_exports.boolean().optional(),
|
|
5705
|
+
/** Id of an older file on the same card this upload replaces. */
|
|
5706
|
+
supersedes: external_exports.string().min(1).max(64).optional(),
|
|
5707
|
+
requestingUserId: external_exports.string().optional()
|
|
5708
|
+
});
|
|
5624
5709
|
var RequestWorkspaceRecycleRequestSchema = external_exports.object({
|
|
5625
5710
|
sessionId: external_exports.string(),
|
|
5626
5711
|
reason: external_exports.string().max(2e3)
|
|
@@ -6189,6 +6274,7 @@ function isArtifactRegistryHost(host) {
|
|
|
6189
6274
|
return bare.endsWith(".pkg.dev") || bare === "gcr.io" || bare.endsWith(".gcr.io");
|
|
6190
6275
|
}
|
|
6191
6276
|
var CONTAINERD_K8S_NAMESPACE = "k8s.io";
|
|
6277
|
+
var CONVEYOR_CONTENT_HASH_LABEL = "com.rallycry.conveyor.content-hash";
|
|
6192
6278
|
var LOCAL_BAKE_ROOT_COMMANDS = [
|
|
6193
6279
|
{
|
|
6194
6280
|
name: "import",
|
|
@@ -6201,6 +6287,16 @@ var LOCAL_BAKE_ROOT_COMMANDS = [
|
|
|
6201
6287
|
takesImageArgs: true
|
|
6202
6288
|
}
|
|
6203
6289
|
];
|
|
6290
|
+
var PERSONAL_LOCAL_REGISTRY_NAMESPACE = "conveyor-system";
|
|
6291
|
+
var PERSONAL_LOCAL_REGISTRY_NAME = "conveyor-registry";
|
|
6292
|
+
var PERSONAL_LOCAL_REGISTRY_PORT = 5e3;
|
|
6293
|
+
var PERSONAL_LOCAL_REGISTRY_HOST = `${PERSONAL_LOCAL_REGISTRY_NAME}.${PERSONAL_LOCAL_REGISTRY_NAMESPACE}.svc.cluster.local:${PERSONAL_LOCAL_REGISTRY_PORT}`;
|
|
6294
|
+
var PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT = `http://127.0.0.1:${PERSONAL_LOCAL_REGISTRY_PORT}`;
|
|
6295
|
+
var BAKE_COMPONENT_LABEL = "claudespace.rallycry.com/component";
|
|
6296
|
+
var BAKE_COMPONENT_PREBAKE = "prebake";
|
|
6297
|
+
var BAKE_COMPONENT_STATE_BAKE = "state-bake";
|
|
6298
|
+
var BAKE_COMPONENT_VALUES = [BAKE_COMPONENT_PREBAKE, BAKE_COMPONENT_STATE_BAKE];
|
|
6299
|
+
var PERSONAL_BAKE_NAMESPACE = "conveyor-bakes";
|
|
6204
6300
|
var MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
6205
6301
|
var POSTGRES_ENV = {
|
|
6206
6302
|
POSTGRES_HOST_AUTH_METHOD: "trust",
|
|
@@ -6519,6 +6615,8 @@ var REPORT_CARD_TYPES = surfaceTypes("report");
|
|
|
6519
6615
|
var DECISION_CARD_TYPES = surfaceTypes("decision");
|
|
6520
6616
|
|
|
6521
6617
|
// src/daemon/connection.ts
|
|
6618
|
+
var REDIAL_FIRST_MS = 5e3;
|
|
6619
|
+
var REDIAL_MAX_MS = 5 * 6e4;
|
|
6522
6620
|
function unwrap(ack, what) {
|
|
6523
6621
|
if (!ack?.ok || ack.data === void 0) {
|
|
6524
6622
|
throw new Error(`${what} failed: ${ack?.error ?? "no response"}`);
|
|
@@ -6528,6 +6626,8 @@ function unwrap(ack, what) {
|
|
|
6528
6626
|
var ClusterConnection = class {
|
|
6529
6627
|
socket = null;
|
|
6530
6628
|
config;
|
|
6629
|
+
redialDelayMs = REDIAL_FIRST_MS;
|
|
6630
|
+
redialTimer = null;
|
|
6531
6631
|
/** Called when Conveyor says the desired state changed. */
|
|
6532
6632
|
onSyncRequested = null;
|
|
6533
6633
|
/** Called when Conveyor asks the machine to remove itself. */
|
|
@@ -6552,9 +6652,18 @@ var ClusterConnection = class {
|
|
|
6552
6652
|
reject(error instanceof Error ? error : new Error(String(error)));
|
|
6553
6653
|
return;
|
|
6554
6654
|
}
|
|
6555
|
-
const
|
|
6655
|
+
const route = controlPlaneRoute(
|
|
6656
|
+
this.config.apiUrl,
|
|
6657
|
+
this.config.readControlFile,
|
|
6658
|
+
this.config.controlFileExists
|
|
6659
|
+
);
|
|
6660
|
+
const socket = io(this.config.apiUrl, {
|
|
6661
|
+
...buildConveyorSocketOptions(auth),
|
|
6662
|
+
...route ? { transportOptions: { websocket: { agent: route.agent } } } : {}
|
|
6663
|
+
});
|
|
6556
6664
|
this.socket = socket;
|
|
6557
6665
|
socket.on("connect", () => {
|
|
6666
|
+
this.redialDelayMs = REDIAL_FIRST_MS;
|
|
6558
6667
|
if (!settled) {
|
|
6559
6668
|
settled = true;
|
|
6560
6669
|
resolve();
|
|
@@ -6568,10 +6677,14 @@ var ClusterConnection = class {
|
|
|
6568
6677
|
settled = true;
|
|
6569
6678
|
reject(err);
|
|
6570
6679
|
}
|
|
6680
|
+
if (!isAuthFailure) route?.failed();
|
|
6571
6681
|
});
|
|
6572
6682
|
socket.on("disconnect", (reason) => {
|
|
6573
6683
|
process.stderr.write(`[conveyor-k3] disconnected: ${reason}
|
|
6574
6684
|
`);
|
|
6685
|
+
if (reason === "io server disconnect" && this.config.reconnectWhenReplaced) {
|
|
6686
|
+
this.redial(socket);
|
|
6687
|
+
}
|
|
6575
6688
|
});
|
|
6576
6689
|
socket.on(PERSONAL_CLUSTER_EVENTS.syncRequested, () => {
|
|
6577
6690
|
this.onSyncRequested?.();
|
|
@@ -6633,7 +6746,20 @@ var ClusterConnection = class {
|
|
|
6633
6746
|
} catch {
|
|
6634
6747
|
}
|
|
6635
6748
|
}
|
|
6749
|
+
/** Dial again after the server closed the socket, waiting longer each time. */
|
|
6750
|
+
redial(socket) {
|
|
6751
|
+
if (this.redialTimer) return;
|
|
6752
|
+
const delay = this.redialDelayMs;
|
|
6753
|
+
this.redialDelayMs = Math.min(delay * 2, REDIAL_MAX_MS);
|
|
6754
|
+
this.redialTimer = setTimeout(() => {
|
|
6755
|
+
this.redialTimer = null;
|
|
6756
|
+
if (this.socket === socket && !socket.connected) socket.connect();
|
|
6757
|
+
}, delay);
|
|
6758
|
+
this.redialTimer.unref?.();
|
|
6759
|
+
}
|
|
6636
6760
|
disconnect() {
|
|
6761
|
+
if (this.redialTimer) clearTimeout(this.redialTimer);
|
|
6762
|
+
this.redialTimer = null;
|
|
6637
6763
|
this.socket?.disconnect();
|
|
6638
6764
|
this.socket = null;
|
|
6639
6765
|
}
|
|
@@ -6939,31 +7065,6 @@ function countPinnedWanted(inventory, wantedImages) {
|
|
|
6939
7065
|
|
|
6940
7066
|
// src/lib/registries.ts
|
|
6941
7067
|
import { parse, stringify } from "yaml";
|
|
6942
|
-
function parseRegistries(raw) {
|
|
6943
|
-
if (!raw || raw.trim().length === 0) return {};
|
|
6944
|
-
const parsed = parse(raw);
|
|
6945
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
6946
|
-
return parsed;
|
|
6947
|
-
}
|
|
6948
|
-
function configuredAuthHosts(file) {
|
|
6949
|
-
return Object.entries(file.configs ?? {}).filter(([, entry]) => Boolean(entry?.auth)).map(([host]) => host).sort();
|
|
6950
|
-
}
|
|
6951
|
-
function missingAuthHosts(file, requiredHosts) {
|
|
6952
|
-
const configured = new Set(configuredAuthHosts(file));
|
|
6953
|
-
return requiredHosts.filter((host) => !configured.has(host)).sort();
|
|
6954
|
-
}
|
|
6955
|
-
function upsertRegistryAuth(file, host, auth) {
|
|
6956
|
-
assertRegistryHost(host);
|
|
6957
|
-
const next = { ...file };
|
|
6958
|
-
const configs = { ...next.configs ?? {} };
|
|
6959
|
-
const existing = configs[host] ?? {};
|
|
6960
|
-
configs[host] = { ...existing, auth };
|
|
6961
|
-
next.configs = configs;
|
|
6962
|
-
return next;
|
|
6963
|
-
}
|
|
6964
|
-
function serializeRegistries(file) {
|
|
6965
|
-
return stringify(file);
|
|
6966
|
-
}
|
|
6967
7068
|
|
|
6968
7069
|
// src/lib/paths.ts
|
|
6969
7070
|
var ETC_CONVEYOR = "/etc/conveyor";
|
|
@@ -6971,11 +7072,16 @@ var ENV_FILE = `${ETC_CONVEYOR}/k3.env`;
|
|
|
6971
7072
|
var TUNNEL_ENV_FILE = `${ETC_CONVEYOR}/k3-tunnel.env`;
|
|
6972
7073
|
var STATE_DIR = "/var/lib/conveyor-k3";
|
|
6973
7074
|
var STATE_FILE = `${STATE_DIR}/state.json`;
|
|
7075
|
+
var LOCAL_REGISTRY_DATA_DIR = `${STATE_DIR}/registry`;
|
|
7076
|
+
var LOCAL_REGISTRY_MIRROR_RESTART_FILE = `${STATE_DIR}/registry-mirror-restart`;
|
|
6974
7077
|
var RANCHER_DIR = "/etc/rancher/k3s";
|
|
6975
7078
|
var K3S_OWNER_CONFIG = `${RANCHER_DIR}/config.yaml`;
|
|
6976
7079
|
var K3S_CONFIG_DROPIN_DIR = `${RANCHER_DIR}/config.yaml.d`;
|
|
6977
7080
|
var K3S_CONFIG_DROPIN = `${K3S_CONFIG_DROPIN_DIR}/50-conveyor-k3.yaml`;
|
|
6978
7081
|
var REGISTRIES_FILE = `${RANCHER_DIR}/registries.yaml`;
|
|
7082
|
+
var K3S_CERTS_D = "/var/lib/rancher/k3s/agent/etc/containerd/certs.d";
|
|
7083
|
+
var K3S_CONTAINERD_DIR = "/var/lib/rancher/k3s/agent/containerd";
|
|
7084
|
+
var JOURNALD_DROPIN = "/etc/systemd/journald.conf.d/50-conveyor-k3.conf";
|
|
6979
7085
|
var CLOUDFLARED_DIR = "/etc/cloudflared";
|
|
6980
7086
|
var CA_POOL_PATH = `${CLOUDFLARED_DIR}/conveyor-k3-server-ca.crt`;
|
|
6981
7087
|
var SYSTEMD_DIR = "/etc/systemd/system";
|
|
@@ -6989,10 +7095,64 @@ var OWNED_FILES = [
|
|
|
6989
7095
|
TUNNEL_ENV_FILE,
|
|
6990
7096
|
K3S_CONFIG_DROPIN,
|
|
6991
7097
|
CA_POOL_PATH,
|
|
7098
|
+
JOURNALD_DROPIN,
|
|
6992
7099
|
...ALL_UNITS.map((unit) => `${SYSTEMD_DIR}/${unit}`)
|
|
6993
7100
|
];
|
|
6994
7101
|
var RUNNERS_SUDOERS_FILE = "/etc/sudoers.d/conveyor-k3-runners";
|
|
6995
7102
|
|
|
7103
|
+
// src/lib/registries.ts
|
|
7104
|
+
function parseRegistries(raw) {
|
|
7105
|
+
if (!raw || raw.trim().length === 0) return {};
|
|
7106
|
+
const parsed = parse(raw);
|
|
7107
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
7108
|
+
return parsed;
|
|
7109
|
+
}
|
|
7110
|
+
function configuredAuthHosts(file) {
|
|
7111
|
+
return Object.entries(file.configs ?? {}).filter(([, entry]) => Boolean(entry?.auth)).map(([host]) => host).sort();
|
|
7112
|
+
}
|
|
7113
|
+
function missingAuthHosts(file, requiredHosts) {
|
|
7114
|
+
const configured = new Set(configuredAuthHosts(file));
|
|
7115
|
+
return requiredHosts.filter((host) => !configured.has(host)).sort();
|
|
7116
|
+
}
|
|
7117
|
+
function upsertRegistryAuth(file, host, auth) {
|
|
7118
|
+
assertRegistryHost(host);
|
|
7119
|
+
const next = { ...file };
|
|
7120
|
+
const configs = { ...next.configs ?? {} };
|
|
7121
|
+
const existing = configs[host] ?? {};
|
|
7122
|
+
configs[host] = { ...existing, auth };
|
|
7123
|
+
next.configs = configs;
|
|
7124
|
+
return next;
|
|
7125
|
+
}
|
|
7126
|
+
function mirrorEndpoints(file, host) {
|
|
7127
|
+
const entry = file.mirrors?.[host];
|
|
7128
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return [];
|
|
7129
|
+
const endpoint = entry.endpoint;
|
|
7130
|
+
return Array.isArray(endpoint) ? endpoint.filter((value) => typeof value === "string") : [];
|
|
7131
|
+
}
|
|
7132
|
+
function upsertRegistryMirror(file, host, endpoints) {
|
|
7133
|
+
assertRegistryHost(host);
|
|
7134
|
+
return { ...file, mirrors: { ...file.mirrors ?? {}, [host]: { endpoint: [...endpoints] } } };
|
|
7135
|
+
}
|
|
7136
|
+
function removeRegistryMirror(file, host) {
|
|
7137
|
+
if (!file.mirrors || !Object.hasOwn(file.mirrors, host)) return file;
|
|
7138
|
+
const { [host]: _removed, ...mirrors } = file.mirrors;
|
|
7139
|
+
const next = { ...file, mirrors };
|
|
7140
|
+
if (Object.keys(mirrors).length === 0) delete next.mirrors;
|
|
7141
|
+
return next;
|
|
7142
|
+
}
|
|
7143
|
+
function serializeRegistries(file) {
|
|
7144
|
+
return stringify(file);
|
|
7145
|
+
}
|
|
7146
|
+
async function writeRegistriesWithBackup(fs, raw, next, now) {
|
|
7147
|
+
let backupPath = null;
|
|
7148
|
+
if (raw !== null) {
|
|
7149
|
+
backupPath = `${REGISTRIES_FILE}.bak-${now.toISOString().replace(/[:.]/g, "-")}`;
|
|
7150
|
+
await fs.writeFile(backupPath, raw, 384);
|
|
7151
|
+
}
|
|
7152
|
+
await fs.writeFile(REGISTRIES_FILE, serializeRegistries(next), 384);
|
|
7153
|
+
return backupPath;
|
|
7154
|
+
}
|
|
7155
|
+
|
|
6996
7156
|
// src/lib/state.ts
|
|
6997
7157
|
var EMPTY_STATE = {
|
|
6998
7158
|
appliedDesiredStateVersion: 0,
|
|
@@ -7015,6 +7175,9 @@ async function writeState(fs, state) {
|
|
|
7015
7175
|
await fs.writeFile(STATE_FILE, `${JSON.stringify(state, null, 2)}
|
|
7016
7176
|
`, 384);
|
|
7017
7177
|
}
|
|
7178
|
+
function managesK3s(desired, state) {
|
|
7179
|
+
return desired.isolation === void 0 || state.k3sInstalledByConveyor === true;
|
|
7180
|
+
}
|
|
7018
7181
|
|
|
7019
7182
|
// src/lib/k3s-config.ts
|
|
7020
7183
|
var IMAGE_GC_HIGH = "image-gc-high-threshold";
|
|
@@ -7028,6 +7191,15 @@ var K3S_INSTALL_EXEC = [
|
|
|
7028
7191
|
`--${WRITE_KUBECONFIG_MODE}`,
|
|
7029
7192
|
KUBECONFIG_MODE
|
|
7030
7193
|
].join(" ");
|
|
7194
|
+
function bareToken(token) {
|
|
7195
|
+
let bare = token.trim();
|
|
7196
|
+
if (bare.endsWith("\\")) bare = bare.slice(0, -1).trim();
|
|
7197
|
+
const quote = bare[0];
|
|
7198
|
+
if (bare.length >= 2 && (quote === "'" || quote === '"') && bare.endsWith(quote)) {
|
|
7199
|
+
bare = bare.slice(1, -1);
|
|
7200
|
+
}
|
|
7201
|
+
return bare;
|
|
7202
|
+
}
|
|
7031
7203
|
function ownerConfiguredKubeletFlags(ownerConfig) {
|
|
7032
7204
|
const found = /* @__PURE__ */ new Set();
|
|
7033
7205
|
if (!ownerConfig) return found;
|
|
@@ -7053,7 +7225,7 @@ function ownerImageGcHighThreshold(ownerConfig) {
|
|
|
7053
7225
|
const value = Number.parseInt(match[2] ?? "", 10);
|
|
7054
7226
|
return Number.isInteger(value) && value >= 0 && value <= 100 ? value : null;
|
|
7055
7227
|
}
|
|
7056
|
-
function renderK3sDropIn(kubelet, ownerConfig) {
|
|
7228
|
+
function renderK3sDropIn(kubelet, ownerConfig, options = {}) {
|
|
7057
7229
|
const ownerFlags = ownerConfiguredKubeletFlags(ownerConfig);
|
|
7058
7230
|
const ownerKeys = ownerConfiguredKeys(ownerConfig);
|
|
7059
7231
|
const args = [];
|
|
@@ -7072,22 +7244,24 @@ function renderK3sDropIn(kubelet, ownerConfig) {
|
|
|
7072
7244
|
const memory = assertQuantity(kubelet.systemReserved.memory, "reserveMemory");
|
|
7073
7245
|
add(SYSTEM_RESERVED, `cpu=${cpu},memory=${memory}`);
|
|
7074
7246
|
}
|
|
7075
|
-
const
|
|
7247
|
+
const lines2 = [
|
|
7076
7248
|
"# Managed by Conveyor (@rallycry/conveyor-k3). Do not edit.",
|
|
7077
7249
|
"# Your own /etc/rancher/k3s/config.yaml is never modified; k3s merges both.",
|
|
7078
7250
|
"# `kubelet-arg+` and `disable+` append rather than replacing a list you set."
|
|
7079
7251
|
];
|
|
7080
|
-
|
|
7081
|
-
|
|
7252
|
+
if (options.disableComponents !== false) {
|
|
7253
|
+
lines2.push("disable+:");
|
|
7254
|
+
for (const component of K3S_DISABLED_COMPONENTS) lines2.push(` - ${component}`);
|
|
7255
|
+
}
|
|
7082
7256
|
if (ownerKeys.has(WRITE_KUBECONFIG_MODE)) {
|
|
7083
7257
|
skipped.push(WRITE_KUBECONFIG_MODE);
|
|
7084
7258
|
} else {
|
|
7085
|
-
|
|
7259
|
+
lines2.push(`${WRITE_KUBECONFIG_MODE}: "${KUBECONFIG_MODE}"`);
|
|
7086
7260
|
}
|
|
7087
7261
|
if (args.length > 0) {
|
|
7088
|
-
|
|
7262
|
+
lines2.push("kubelet-arg+:", ...args);
|
|
7089
7263
|
}
|
|
7090
|
-
return { content: [...
|
|
7264
|
+
return { content: [...lines2, ""].join("\n"), skipped };
|
|
7091
7265
|
}
|
|
7092
7266
|
var renderKubeletDropIn = renderK3sDropIn;
|
|
7093
7267
|
|
|
@@ -7136,86 +7310,668 @@ function findDisallowedKinds(manifest) {
|
|
|
7136
7310
|
return disallowed;
|
|
7137
7311
|
}
|
|
7138
7312
|
|
|
7139
|
-
// src/daemon/
|
|
7313
|
+
// src/daemon/cluster-ops.ts
|
|
7314
|
+
async function kubectlApply(exec, manifest) {
|
|
7315
|
+
await runOrThrow(exec, "k3s", ["kubectl", "apply", "-f", "-"], {
|
|
7316
|
+
stdin: manifest,
|
|
7317
|
+
timeoutMs: 6e4
|
|
7318
|
+
});
|
|
7319
|
+
}
|
|
7140
7320
|
function mayDisrupt(runningConveyorPods) {
|
|
7141
7321
|
return runningConveyorPods === 0;
|
|
7142
7322
|
}
|
|
7143
7323
|
function busyReason(runningConveyorPods) {
|
|
7144
7324
|
return runningConveyorPods === null ? "the number of running Conveyor pods could not be read" : `${runningConveyorPods} Conveyor pod(s) are running`;
|
|
7145
7325
|
}
|
|
7146
|
-
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
7326
|
+
|
|
7327
|
+
// src/daemon/local-registry-upkeep.ts
|
|
7328
|
+
import { createHash } from "crypto";
|
|
7329
|
+
|
|
7330
|
+
// src/lib/local-registry.ts
|
|
7331
|
+
var LOCAL_REGISTRY_IMAGE = "docker.io/library/registry:3.1.2@sha256:c87f33837722a100572e95d7dc4bf539fc42cf68202b13c3bc03c0ff54c3a649";
|
|
7332
|
+
var LOCAL_REGISTRY_CONFIG = "/etc/distribution/config.yml";
|
|
7333
|
+
var LOCAL_REGISTRY_GC_JOB = "conveyor-registry-gc";
|
|
7334
|
+
var LOCAL_REGISTRY_SELECTOR = { "app.kubernetes.io/name": PERSONAL_LOCAL_REGISTRY_NAME };
|
|
7335
|
+
var MANAGED_BY = { "app.kubernetes.io/managed-by": "conveyor-k3" };
|
|
7336
|
+
var DATA_MOUNT = "/var/lib/registry";
|
|
7337
|
+
var REGISTRY_ENV = [
|
|
7338
|
+
{ name: "REGISTRY_STORAGE_DELETE_ENABLED", value: "true" },
|
|
7339
|
+
{ name: "OTEL_TRACES_EXPORTER", value: "none" }
|
|
7340
|
+
];
|
|
7341
|
+
var CONTAINER_SECURITY = {
|
|
7342
|
+
allowPrivilegeEscalation: false,
|
|
7343
|
+
readOnlyRootFilesystem: true,
|
|
7344
|
+
capabilities: { drop: ["ALL"] }
|
|
7345
|
+
};
|
|
7346
|
+
var VOLUME_MOUNTS = [
|
|
7347
|
+
{ name: "data", mountPath: DATA_MOUNT },
|
|
7348
|
+
{ name: "tmp", mountPath: "/tmp" }
|
|
7349
|
+
];
|
|
7350
|
+
var VOLUMES = [
|
|
7351
|
+
{ name: "data", hostPath: { path: LOCAL_REGISTRY_DATA_DIR, type: "Directory" } },
|
|
7352
|
+
{ name: "tmp", emptyDir: { sizeLimit: "64Mi" } }
|
|
7353
|
+
];
|
|
7354
|
+
var POD_BASE = {
|
|
7355
|
+
automountServiceAccountToken: false,
|
|
7356
|
+
enableServiceLinks: false,
|
|
7357
|
+
securityContext: { seccompProfile: { type: "RuntimeDefault" } },
|
|
7358
|
+
volumes: VOLUMES
|
|
7359
|
+
};
|
|
7360
|
+
function registryNetworkPolicy(metadata) {
|
|
7361
|
+
return {
|
|
7362
|
+
apiVersion: "networking.k8s.io/v1",
|
|
7363
|
+
kind: "NetworkPolicy",
|
|
7364
|
+
metadata,
|
|
7365
|
+
spec: {
|
|
7366
|
+
podSelector: { matchLabels: LOCAL_REGISTRY_SELECTOR },
|
|
7367
|
+
policyTypes: ["Ingress"],
|
|
7368
|
+
ingress: [
|
|
7369
|
+
{
|
|
7370
|
+
from: [
|
|
7371
|
+
{
|
|
7372
|
+
namespaceSelector: {},
|
|
7373
|
+
podSelector: {
|
|
7374
|
+
matchExpressions: [
|
|
7375
|
+
{ key: BAKE_COMPONENT_LABEL, operator: "In", values: [...BAKE_COMPONENT_VALUES] }
|
|
7376
|
+
]
|
|
7377
|
+
}
|
|
7378
|
+
}
|
|
7379
|
+
],
|
|
7380
|
+
ports: [{ protocol: "TCP", port: PERSONAL_LOCAL_REGISTRY_PORT }]
|
|
7381
|
+
}
|
|
7382
|
+
]
|
|
7383
|
+
}
|
|
7384
|
+
};
|
|
7151
7385
|
}
|
|
7152
|
-
|
|
7153
|
-
const
|
|
7154
|
-
|
|
7155
|
-
|
|
7156
|
-
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
|
|
7160
|
-
|
|
7161
|
-
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
7168
|
-
|
|
7169
|
-
|
|
7170
|
-
|
|
7171
|
-
|
|
7172
|
-
|
|
7173
|
-
|
|
7174
|
-
|
|
7386
|
+
function renderLocalRegistryManifests() {
|
|
7387
|
+
const metadata = {
|
|
7388
|
+
name: PERSONAL_LOCAL_REGISTRY_NAME,
|
|
7389
|
+
namespace: PERSONAL_LOCAL_REGISTRY_NAMESPACE,
|
|
7390
|
+
labels: { ...LOCAL_REGISTRY_SELECTOR, ...MANAGED_BY }
|
|
7391
|
+
};
|
|
7392
|
+
const deployment = {
|
|
7393
|
+
apiVersion: "apps/v1",
|
|
7394
|
+
kind: "Deployment",
|
|
7395
|
+
metadata,
|
|
7396
|
+
spec: {
|
|
7397
|
+
replicas: 1,
|
|
7398
|
+
// One pod owns the data directory at a time.
|
|
7399
|
+
strategy: { type: "Recreate" },
|
|
7400
|
+
selector: { matchLabels: LOCAL_REGISTRY_SELECTOR },
|
|
7401
|
+
template: {
|
|
7402
|
+
metadata: { labels: metadata.labels },
|
|
7403
|
+
spec: {
|
|
7404
|
+
...POD_BASE,
|
|
7405
|
+
containers: [
|
|
7406
|
+
{
|
|
7407
|
+
name: "registry",
|
|
7408
|
+
image: LOCAL_REGISTRY_IMAGE,
|
|
7409
|
+
imagePullPolicy: "IfNotPresent",
|
|
7410
|
+
env: REGISTRY_ENV,
|
|
7411
|
+
ports: [
|
|
7412
|
+
{
|
|
7413
|
+
name: "registry",
|
|
7414
|
+
containerPort: PERSONAL_LOCAL_REGISTRY_PORT,
|
|
7415
|
+
// Loopback only: the node's containerd is the one client
|
|
7416
|
+
// that comes in through this port.
|
|
7417
|
+
hostPort: PERSONAL_LOCAL_REGISTRY_PORT,
|
|
7418
|
+
hostIP: "127.0.0.1",
|
|
7419
|
+
protocol: "TCP"
|
|
7420
|
+
}
|
|
7421
|
+
],
|
|
7422
|
+
readinessProbe: {
|
|
7423
|
+
httpGet: { path: "/v2/", port: PERSONAL_LOCAL_REGISTRY_PORT },
|
|
7424
|
+
periodSeconds: 10
|
|
7425
|
+
},
|
|
7426
|
+
resources: {
|
|
7427
|
+
requests: { cpu: "50m", memory: "64Mi" },
|
|
7428
|
+
limits: { memory: "512Mi" }
|
|
7429
|
+
},
|
|
7430
|
+
securityContext: CONTAINER_SECURITY,
|
|
7431
|
+
volumeMounts: VOLUME_MOUNTS
|
|
7432
|
+
}
|
|
7433
|
+
]
|
|
7434
|
+
}
|
|
7435
|
+
}
|
|
7436
|
+
}
|
|
7437
|
+
};
|
|
7438
|
+
const service = {
|
|
7439
|
+
apiVersion: "v1",
|
|
7440
|
+
kind: "Service",
|
|
7441
|
+
metadata,
|
|
7442
|
+
spec: {
|
|
7443
|
+
type: "ClusterIP",
|
|
7444
|
+
selector: LOCAL_REGISTRY_SELECTOR,
|
|
7445
|
+
ports: [
|
|
7446
|
+
{
|
|
7447
|
+
name: "registry",
|
|
7448
|
+
port: PERSONAL_LOCAL_REGISTRY_PORT,
|
|
7449
|
+
targetPort: PERSONAL_LOCAL_REGISTRY_PORT,
|
|
7450
|
+
protocol: "TCP"
|
|
7451
|
+
}
|
|
7452
|
+
]
|
|
7453
|
+
}
|
|
7454
|
+
};
|
|
7455
|
+
const items = [deployment, service, registryNetworkPolicy(metadata)];
|
|
7456
|
+
return `${JSON.stringify({ apiVersion: "v1", kind: "List", items }, null, 2)}
|
|
7457
|
+
`;
|
|
7458
|
+
}
|
|
7459
|
+
function renderRegistryGcJob() {
|
|
7460
|
+
const labels = { "app.kubernetes.io/name": LOCAL_REGISTRY_GC_JOB, ...MANAGED_BY };
|
|
7461
|
+
const job = {
|
|
7462
|
+
apiVersion: "batch/v1",
|
|
7463
|
+
kind: "Job",
|
|
7464
|
+
metadata: { name: LOCAL_REGISTRY_GC_JOB, namespace: PERSONAL_LOCAL_REGISTRY_NAMESPACE, labels },
|
|
7465
|
+
spec: {
|
|
7466
|
+
backoffLimit: 0,
|
|
7467
|
+
activeDeadlineSeconds: 600,
|
|
7468
|
+
// A Job left by a daemon that died mid-sweep goes on its own.
|
|
7469
|
+
ttlSecondsAfterFinished: 600,
|
|
7470
|
+
template: {
|
|
7471
|
+
metadata: { labels },
|
|
7472
|
+
spec: {
|
|
7473
|
+
...POD_BASE,
|
|
7474
|
+
restartPolicy: "Never",
|
|
7475
|
+
containers: [
|
|
7476
|
+
{
|
|
7477
|
+
name: "garbage-collect",
|
|
7478
|
+
image: LOCAL_REGISTRY_IMAGE,
|
|
7479
|
+
imagePullPolicy: "IfNotPresent",
|
|
7480
|
+
args: ["garbage-collect", LOCAL_REGISTRY_CONFIG],
|
|
7481
|
+
env: REGISTRY_ENV,
|
|
7482
|
+
resources: {
|
|
7483
|
+
requests: { cpu: "100m", memory: "128Mi" },
|
|
7484
|
+
limits: { memory: "1Gi" }
|
|
7485
|
+
},
|
|
7486
|
+
securityContext: CONTAINER_SECURITY,
|
|
7487
|
+
volumeMounts: VOLUME_MOUNTS
|
|
7488
|
+
}
|
|
7489
|
+
]
|
|
7490
|
+
}
|
|
7491
|
+
}
|
|
7492
|
+
}
|
|
7493
|
+
};
|
|
7494
|
+
return `${JSON.stringify(job, null, 2)}
|
|
7495
|
+
`;
|
|
7496
|
+
}
|
|
7497
|
+
function mirrorHostsTomlPaths(host) {
|
|
7498
|
+
const colon = host.lastIndexOf(":");
|
|
7499
|
+
const escaped = colon > 0 ? `${host.slice(0, colon)}_${host.slice(colon + 1)}_` : host;
|
|
7500
|
+
return [.../* @__PURE__ */ new Set([escaped, host])].map((dir) => `${K3S_CERTS_D}/${dir}/hosts.toml`);
|
|
7501
|
+
}
|
|
7502
|
+
async function mirrorLoaded(readFile3) {
|
|
7503
|
+
for (const path of mirrorHostsTomlPaths(PERSONAL_LOCAL_REGISTRY_HOST)) {
|
|
7504
|
+
const content = await readFile3(path);
|
|
7505
|
+
if (content?.includes(PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT)) return true;
|
|
7175
7506
|
}
|
|
7176
|
-
|
|
7177
|
-
|
|
7178
|
-
|
|
7179
|
-
|
|
7180
|
-
|
|
7181
|
-
|
|
7182
|
-
);
|
|
7507
|
+
return false;
|
|
7508
|
+
}
|
|
7509
|
+
async function registryAnswers(fetchImpl = fetch) {
|
|
7510
|
+
try {
|
|
7511
|
+
const response = await fetchImpl(`${PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT}/v2/`, {
|
|
7512
|
+
signal: AbortSignal.timeout(2e3)
|
|
7513
|
+
});
|
|
7514
|
+
await response.body?.cancel();
|
|
7515
|
+
return response.status === 200;
|
|
7516
|
+
} catch {
|
|
7517
|
+
return false;
|
|
7183
7518
|
}
|
|
7184
7519
|
}
|
|
7185
|
-
async function
|
|
7186
|
-
const
|
|
7187
|
-
|
|
7188
|
-
|
|
7520
|
+
async function localRegistryReady(ports) {
|
|
7521
|
+
const [answers, mirrored] = await Promise.all([
|
|
7522
|
+
registryAnswers(ports.fetch),
|
|
7523
|
+
mirrorLoaded(ports.readFile)
|
|
7524
|
+
]);
|
|
7525
|
+
return answers && mirrored;
|
|
7526
|
+
}
|
|
7527
|
+
|
|
7528
|
+
// src/daemon/registry-gc-lock.ts
|
|
7529
|
+
var REGISTRY_GC_MARK = "conveyor.rallycry.com/registry-gc";
|
|
7530
|
+
var REGISTRY_GC_MARK_TTL_MS = 20 * 6e4;
|
|
7531
|
+
function inRegistryNamespace(exec, args, timeoutMs = 3e4) {
|
|
7532
|
+
return exec("k3s", ["kubectl", "-n", PERSONAL_LOCAL_REGISTRY_NAMESPACE, ...args], { timeoutMs });
|
|
7533
|
+
}
|
|
7534
|
+
async function readRegistryDeployment(exec) {
|
|
7535
|
+
const result = await inRegistryNamespace(exec, [
|
|
7189
7536
|
"get",
|
|
7190
|
-
"
|
|
7191
|
-
|
|
7537
|
+
"deployment",
|
|
7538
|
+
PERSONAL_LOCAL_REGISTRY_NAME,
|
|
7539
|
+
"--ignore-not-found",
|
|
7192
7540
|
"-o",
|
|
7193
|
-
"
|
|
7541
|
+
"json"
|
|
7194
7542
|
]);
|
|
7195
|
-
if (
|
|
7196
|
-
|
|
7197
|
-
|
|
7198
|
-
|
|
7199
|
-
|
|
7200
|
-
|
|
7201
|
-
|
|
7202
|
-
|
|
7203
|
-
|
|
7204
|
-
|
|
7205
|
-
|
|
7206
|
-
|
|
7207
|
-
|
|
7208
|
-
);
|
|
7209
|
-
return;
|
|
7543
|
+
if (result.exitCode !== 0) return null;
|
|
7544
|
+
if (result.stdout.trim().length === 0) return { exists: false, replicas: null, mark: null };
|
|
7545
|
+
try {
|
|
7546
|
+
const deployment = JSON.parse(result.stdout);
|
|
7547
|
+
const replicas = deployment.spec?.replicas;
|
|
7548
|
+
const mark = deployment.metadata?.annotations?.[REGISTRY_GC_MARK];
|
|
7549
|
+
return {
|
|
7550
|
+
exists: true,
|
|
7551
|
+
replicas: typeof replicas === "number" ? replicas : null,
|
|
7552
|
+
mark: typeof mark === "string" ? mark : null
|
|
7553
|
+
};
|
|
7554
|
+
} catch {
|
|
7555
|
+
return null;
|
|
7210
7556
|
}
|
|
7557
|
+
}
|
|
7558
|
+
function processAlive(pid) {
|
|
7211
7559
|
try {
|
|
7212
|
-
|
|
7213
|
-
|
|
7560
|
+
process.kill(pid, 0);
|
|
7561
|
+
return true;
|
|
7214
7562
|
} catch (error) {
|
|
7215
|
-
|
|
7216
|
-
|
|
7217
|
-
|
|
7563
|
+
return error.code === "EPERM";
|
|
7564
|
+
}
|
|
7565
|
+
}
|
|
7566
|
+
function newGcMark(now) {
|
|
7567
|
+
return `${process.pid}@${new Date(now).toISOString()}`;
|
|
7568
|
+
}
|
|
7569
|
+
function gcMarkIsLive(mark, now, alive = processAlive) {
|
|
7570
|
+
const match = mark ? /^(\d+)@(.+)$/.exec(mark) : null;
|
|
7571
|
+
if (!match?.[1] || !match[2]) return false;
|
|
7572
|
+
const started = Date.parse(match[2]);
|
|
7573
|
+
return !Number.isNaN(started) && now - started < REGISTRY_GC_MARK_TTL_MS && alive(Number(match[1]));
|
|
7574
|
+
}
|
|
7575
|
+
async function setGcMark(exec, mark) {
|
|
7576
|
+
await runOrThrow(
|
|
7577
|
+
exec,
|
|
7578
|
+
"k3s",
|
|
7579
|
+
[
|
|
7580
|
+
"kubectl",
|
|
7581
|
+
"-n",
|
|
7582
|
+
PERSONAL_LOCAL_REGISTRY_NAMESPACE,
|
|
7583
|
+
"annotate",
|
|
7584
|
+
"deployment",
|
|
7585
|
+
PERSONAL_LOCAL_REGISTRY_NAME,
|
|
7586
|
+
`${REGISTRY_GC_MARK}=${mark}`,
|
|
7587
|
+
"--overwrite"
|
|
7588
|
+
],
|
|
7589
|
+
{ timeoutMs: 3e4 }
|
|
7590
|
+
);
|
|
7591
|
+
}
|
|
7592
|
+
async function clearGcMark(exec) {
|
|
7593
|
+
const result = await inRegistryNamespace(exec, [
|
|
7594
|
+
"annotate",
|
|
7595
|
+
"deployment",
|
|
7596
|
+
PERSONAL_LOCAL_REGISTRY_NAME,
|
|
7597
|
+
`${REGISTRY_GC_MARK}-`
|
|
7598
|
+
]);
|
|
7599
|
+
return result.exitCode === 0;
|
|
7600
|
+
}
|
|
7601
|
+
async function deleteSweepJob(exec) {
|
|
7602
|
+
const result = await inRegistryNamespace(
|
|
7603
|
+
exec,
|
|
7604
|
+
[
|
|
7605
|
+
"delete",
|
|
7606
|
+
"job",
|
|
7607
|
+
LOCAL_REGISTRY_GC_JOB,
|
|
7608
|
+
"--ignore-not-found",
|
|
7609
|
+
"--cascade=foreground",
|
|
7610
|
+
"--wait=true"
|
|
7611
|
+
],
|
|
7612
|
+
12e4
|
|
7613
|
+
);
|
|
7614
|
+
return result.exitCode === 0;
|
|
7615
|
+
}
|
|
7616
|
+
|
|
7617
|
+
// src/daemon/local-registry-upkeep.ts
|
|
7618
|
+
function nowDate() {
|
|
7619
|
+
return /* @__PURE__ */ new Date();
|
|
7620
|
+
}
|
|
7621
|
+
function mirrorConfigured(raw) {
|
|
7622
|
+
const endpoints = mirrorEndpoints(parseRegistries(raw), PERSONAL_LOCAL_REGISTRY_HOST);
|
|
7623
|
+
return endpoints.length === 1 && endpoints[0] === PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT;
|
|
7624
|
+
}
|
|
7625
|
+
async function applyUnlessCollecting(ctx) {
|
|
7626
|
+
const deployment = await readRegistryDeployment(ctx.exec);
|
|
7627
|
+
if (!deployment) throw new Error("the registry's Deployment could not be read");
|
|
7628
|
+
if (gcMarkIsLive(deployment.mark, (ctx.now ?? nowDate)().getTime(), ctx.isAlive)) return false;
|
|
7629
|
+
if (deployment.mark !== null && !await deleteSweepJob(ctx.exec)) {
|
|
7630
|
+
throw new Error("a stopped cleanup's Job could not be deleted, so the registry stays stopped");
|
|
7631
|
+
}
|
|
7632
|
+
await kubectlApply(ctx.exec, renderLocalRegistryManifests());
|
|
7633
|
+
if (deployment.mark !== null) await clearGcMark(ctx.exec);
|
|
7634
|
+
return true;
|
|
7635
|
+
}
|
|
7636
|
+
async function deployLocalRegistry(ctx) {
|
|
7637
|
+
await ctx.fs.ensureDir(LOCAL_REGISTRY_DATA_DIR, 448);
|
|
7638
|
+
const applied = await applyUnlessCollecting(ctx);
|
|
7639
|
+
const raw = await ctx.fs.readFile(REGISTRIES_FILE);
|
|
7640
|
+
if (mirrorConfigured(raw)) return { applied, mirrorWritten: false, backupPath: null };
|
|
7641
|
+
const next = upsertRegistryMirror(parseRegistries(raw), PERSONAL_LOCAL_REGISTRY_HOST, [
|
|
7642
|
+
PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT
|
|
7643
|
+
]);
|
|
7644
|
+
const backupPath = await writeRegistriesWithBackup(ctx.fs, raw, next, (ctx.now ?? nowDate)());
|
|
7645
|
+
return { applied, mirrorWritten: true, backupPath };
|
|
7646
|
+
}
|
|
7647
|
+
async function readMirrorState(fs) {
|
|
7648
|
+
const raw = await fs.readFile(REGISTRIES_FILE);
|
|
7649
|
+
const fingerprint = createHash("sha256").update(raw ?? "").digest("hex");
|
|
7650
|
+
if (!mirrorConfigured(raw)) return { state: "absent", fingerprint };
|
|
7651
|
+
if (await mirrorLoaded((path) => fs.readFile(path))) return { state: "loaded", fingerprint };
|
|
7652
|
+
const tried = (await fs.readFile(LOCAL_REGISTRY_MIRROR_RESTART_FILE))?.trim() === fingerprint;
|
|
7653
|
+
return { state: tried ? "tried" : "owed", fingerprint };
|
|
7654
|
+
}
|
|
7655
|
+
async function registryMirrorPending(fs) {
|
|
7656
|
+
return (await readMirrorState(fs)).state === "owed";
|
|
7657
|
+
}
|
|
7658
|
+
async function loadRegistryMirror(ctx) {
|
|
7659
|
+
const { state, fingerprint } = await readMirrorState(ctx.fs);
|
|
7660
|
+
if (state === "tried") {
|
|
7661
|
+
return {
|
|
7662
|
+
restarted: false,
|
|
7663
|
+
warning: `k3s restarted but did not load the local image registry's mirror, so pods cannot pull from that registry. Conveyor restarts k3s for it again only after /etc/rancher/k3s/registries.yaml changes. Check that k3s reads that file and keeps its data in the default directory: the mirror should appear under ${K3S_CERTS_D}.`
|
|
7664
|
+
};
|
|
7665
|
+
}
|
|
7666
|
+
if (state !== "owed") return { restarted: false, warning: null };
|
|
7667
|
+
if (!mayDisrupt(ctx.runningConveyorPods)) {
|
|
7668
|
+
return {
|
|
7669
|
+
restarted: false,
|
|
7670
|
+
warning: `The local image registry needs a k3s restart to load its mirror, but ${busyReason(ctx.runningConveyorPods)} \u2014 the restart is deferred until the machine is idle.`
|
|
7671
|
+
};
|
|
7672
|
+
}
|
|
7673
|
+
const restart = await ctx.exec("systemctl", ["restart", "k3s"], { timeoutMs: 12e4 });
|
|
7674
|
+
if (restart.exitCode !== 0) {
|
|
7675
|
+
return {
|
|
7676
|
+
restarted: false,
|
|
7677
|
+
warning: `Could not restart k3s to load the local image registry mirror: ${restart.stderr.trim().slice(0, 200)}`
|
|
7678
|
+
};
|
|
7218
7679
|
}
|
|
7680
|
+
await ctx.fs.writeFile(LOCAL_REGISTRY_MIRROR_RESTART_FILE, `${fingerprint}
|
|
7681
|
+
`, 384);
|
|
7682
|
+
return { restarted: true, warning: null };
|
|
7683
|
+
}
|
|
7684
|
+
|
|
7685
|
+
// src/lib/isolation.ts
|
|
7686
|
+
var EGRESS_POLICY_NAME = "conveyor-k3-egress";
|
|
7687
|
+
var MANAGED_BY2 = { "app.kubernetes.io/managed-by": "conveyor-k3" };
|
|
7688
|
+
var NON_ROUTABLE_IPV4 = [
|
|
7689
|
+
"0.0.0.0/8",
|
|
7690
|
+
"10.0.0.0/8",
|
|
7691
|
+
"100.64.0.0/10",
|
|
7692
|
+
"127.0.0.0/8",
|
|
7693
|
+
"169.254.0.0/16",
|
|
7694
|
+
"172.16.0.0/12",
|
|
7695
|
+
"192.0.0.0/24",
|
|
7696
|
+
"192.0.2.0/24",
|
|
7697
|
+
"192.88.99.0/24",
|
|
7698
|
+
"192.168.0.0/16",
|
|
7699
|
+
"198.18.0.0/15",
|
|
7700
|
+
"198.51.100.0/24",
|
|
7701
|
+
"203.0.113.0/24",
|
|
7702
|
+
"224.0.0.0/4",
|
|
7703
|
+
"240.0.0.0/4"
|
|
7704
|
+
];
|
|
7705
|
+
var KUBE_DNS = { namespace: "kube-system", labels: { "k8s-app": "kube-dns" } };
|
|
7706
|
+
var PSA_PREFIX = "pod-security.kubernetes.io";
|
|
7707
|
+
function psaLabels(level) {
|
|
7708
|
+
const advisory = level === "baseline" ? "restricted" : level;
|
|
7709
|
+
return {
|
|
7710
|
+
[`${PSA_PREFIX}/enforce`]: level,
|
|
7711
|
+
[`${PSA_PREFIX}/warn`]: advisory,
|
|
7712
|
+
[`${PSA_PREFIX}/audit`]: advisory
|
|
7713
|
+
};
|
|
7714
|
+
}
|
|
7715
|
+
var PSA_LABEL_KEYS = Object.keys(psaLabels("baseline"));
|
|
7716
|
+
function ipv4Value(address) {
|
|
7717
|
+
const parts = address.split(".");
|
|
7718
|
+
if (parts.length !== 4) return null;
|
|
7719
|
+
let value = 0;
|
|
7720
|
+
for (const part of parts) {
|
|
7721
|
+
if (!/^\d{1,3}$/.test(part)) return null;
|
|
7722
|
+
const octet = Number(part);
|
|
7723
|
+
if (octet > 255) return null;
|
|
7724
|
+
value = value * 256 + octet;
|
|
7725
|
+
}
|
|
7726
|
+
return value;
|
|
7727
|
+
}
|
|
7728
|
+
function inNonRoutableRange(address) {
|
|
7729
|
+
const value = ipv4Value(address);
|
|
7730
|
+
if (value === null) return false;
|
|
7731
|
+
return NON_ROUTABLE_IPV4.some((cidr) => {
|
|
7732
|
+
const [base = "", bits = "32"] = cidr.split("/");
|
|
7733
|
+
const start = ipv4Value(base) ?? 0;
|
|
7734
|
+
return value >= start && value < start + 2 ** (32 - Number(bits));
|
|
7735
|
+
});
|
|
7736
|
+
}
|
|
7737
|
+
function hostAddressExceptions(addresses) {
|
|
7738
|
+
const kept = /* @__PURE__ */ new Set();
|
|
7739
|
+
for (const raw of addresses) {
|
|
7740
|
+
const address = raw.trim();
|
|
7741
|
+
if (ipv4Value(address) === null || inNonRoutableRange(address)) continue;
|
|
7742
|
+
kept.add(`${address}/32`);
|
|
7743
|
+
}
|
|
7744
|
+
return [...kept].sort();
|
|
7745
|
+
}
|
|
7746
|
+
function renderEgressNetworkPolicy(namespace, hostAddresses) {
|
|
7747
|
+
const policy = {
|
|
7748
|
+
apiVersion: "networking.k8s.io/v1",
|
|
7749
|
+
kind: "NetworkPolicy",
|
|
7750
|
+
metadata: {
|
|
7751
|
+
name: EGRESS_POLICY_NAME,
|
|
7752
|
+
namespace: assertK8sName(namespace, "namespace"),
|
|
7753
|
+
labels: MANAGED_BY2
|
|
7754
|
+
},
|
|
7755
|
+
spec: {
|
|
7756
|
+
podSelector: {},
|
|
7757
|
+
policyTypes: ["Egress"],
|
|
7758
|
+
egress: [
|
|
7759
|
+
{
|
|
7760
|
+
to: [
|
|
7761
|
+
{
|
|
7762
|
+
namespaceSelector: {
|
|
7763
|
+
matchLabels: { "kubernetes.io/metadata.name": KUBE_DNS.namespace }
|
|
7764
|
+
},
|
|
7765
|
+
podSelector: { matchLabels: KUBE_DNS.labels }
|
|
7766
|
+
}
|
|
7767
|
+
],
|
|
7768
|
+
ports: [
|
|
7769
|
+
{ protocol: "UDP", port: 53 },
|
|
7770
|
+
{ protocol: "TCP", port: 53 }
|
|
7771
|
+
]
|
|
7772
|
+
},
|
|
7773
|
+
{
|
|
7774
|
+
// kube-router enforces after the Service's DNAT, so the registry is
|
|
7775
|
+
// reached by its pods' labels, never by the service range.
|
|
7776
|
+
to: [
|
|
7777
|
+
{
|
|
7778
|
+
namespaceSelector: {
|
|
7779
|
+
matchLabels: { "kubernetes.io/metadata.name": PERSONAL_LOCAL_REGISTRY_NAMESPACE }
|
|
7780
|
+
},
|
|
7781
|
+
podSelector: { matchLabels: LOCAL_REGISTRY_SELECTOR }
|
|
7782
|
+
}
|
|
7783
|
+
],
|
|
7784
|
+
ports: [{ protocol: "TCP", port: PERSONAL_LOCAL_REGISTRY_PORT }]
|
|
7785
|
+
},
|
|
7786
|
+
{
|
|
7787
|
+
to: [
|
|
7788
|
+
{
|
|
7789
|
+
ipBlock: {
|
|
7790
|
+
cidr: "0.0.0.0/0",
|
|
7791
|
+
except: [...NON_ROUTABLE_IPV4, ...hostAddressExceptions(hostAddresses)]
|
|
7792
|
+
}
|
|
7793
|
+
}
|
|
7794
|
+
]
|
|
7795
|
+
}
|
|
7796
|
+
]
|
|
7797
|
+
}
|
|
7798
|
+
};
|
|
7799
|
+
return `${JSON.stringify(policy, null, 2)}
|
|
7800
|
+
`;
|
|
7801
|
+
}
|
|
7802
|
+
var DISABLE_KEY = /(?:^|[\s{,])(["']?)disable-network-policy\1\s*:([^,}]*)/g;
|
|
7803
|
+
var DISABLE_FLAGS = /* @__PURE__ */ new Set(["--disable-network-policy", "-disable-network-policy"]);
|
|
7804
|
+
var FALSE_VALUES = /* @__PURE__ */ new Set(["false", "no", "off", "n", "f", "0"]);
|
|
7805
|
+
function setsTrue(raw) {
|
|
7806
|
+
const value = bareToken(raw).trim().toLowerCase();
|
|
7807
|
+
return !FALSE_VALUES.has(value);
|
|
7808
|
+
}
|
|
7809
|
+
function uncommented(line) {
|
|
7810
|
+
return /^\s*#/.test(line) ? "" : line.replace(/\s#.*$/, "");
|
|
7811
|
+
}
|
|
7812
|
+
function configSetsKey(text) {
|
|
7813
|
+
return text.split("\n").map(uncommented).some((line) => [...line.matchAll(DISABLE_KEY)].some((match) => setsTrue(match[2] ?? "")));
|
|
7814
|
+
}
|
|
7815
|
+
function unitLines(unitText) {
|
|
7816
|
+
const lines2 = [];
|
|
7817
|
+
let pending = "";
|
|
7818
|
+
for (const raw of unitText.split("\n")) {
|
|
7819
|
+
const line = raw.trimEnd();
|
|
7820
|
+
if (/^\s*[#;]/.test(line)) continue;
|
|
7821
|
+
if (line.endsWith("\\")) {
|
|
7822
|
+
pending += `${line.slice(0, -1)} `;
|
|
7823
|
+
continue;
|
|
7824
|
+
}
|
|
7825
|
+
lines2.push(pending + line);
|
|
7826
|
+
pending = "";
|
|
7827
|
+
}
|
|
7828
|
+
return pending ? [...lines2, pending] : lines2;
|
|
7829
|
+
}
|
|
7830
|
+
function flagSetIn(line) {
|
|
7831
|
+
return line.split(/\s+/).some((raw) => {
|
|
7832
|
+
const token = bareToken(raw).replace(/^["']+|["']+$/g, "");
|
|
7833
|
+
const [name = "", ...value] = token.split("=");
|
|
7834
|
+
return DISABLE_FLAGS.has(name) && (value.length === 0 || setsTrue(value.join("=")));
|
|
7835
|
+
});
|
|
7836
|
+
}
|
|
7837
|
+
function networkPolicyControllerDisabled(sources) {
|
|
7838
|
+
if (sources.configs.some((text) => text !== null && configSetsKey(text))) return true;
|
|
7839
|
+
return unitLines(sources.unitText).some(flagSetIn);
|
|
7840
|
+
}
|
|
7841
|
+
|
|
7842
|
+
// src/daemon/isolation-probes.ts
|
|
7843
|
+
var CONTROLLER_DISABLED_WARNING = "k3s runs with its network policy controller disabled, so the egress policy Conveyor applies here is not enforced. Remove disable-network-policy from the k3s config or unit and restart k3s. Until then this server is not isolated; Conveyor checks again within 10 minutes.";
|
|
7844
|
+
var CONTROLLER_UNKNOWN_WARNING = "Could not read the k3s unit or its config files to check that its network policy controller is on, so this server does not count as isolated. Check `systemctl cat k3s.service` and /etc/rancher/k3s; Conveyor checks again within 10 minutes.";
|
|
7845
|
+
var NODE_ADDRESSES_JSONPATH = '{range .items[*]}{range .status.addresses[?(@.type=="InternalIP")]}{.address}{"\\n"}{end}{range .status.addresses[?(@.type=="ExternalIP")]}{.address}{"\\n"}{end}{end}';
|
|
7846
|
+
var API_ENDPOINTS_JSONPATH = "{.items[*].endpoints[*].addresses[*]}";
|
|
7847
|
+
var IPV4_ON_INTERFACE = /\binet (\d{1,3}(?:\.\d{1,3}){3})(?:\/| peer )/g;
|
|
7848
|
+
async function stdoutOf(exec, bin, args) {
|
|
7849
|
+
const run = await exec(bin, args, { timeoutMs: 1e4 }).catch(() => null);
|
|
7850
|
+
return run && run.exitCode === 0 ? run.stdout : null;
|
|
7851
|
+
}
|
|
7852
|
+
async function readK3sConfigs(ctx) {
|
|
7853
|
+
const listed = await stdoutOf(ctx.exec, "find", [
|
|
7854
|
+
"-L",
|
|
7855
|
+
K3S_CONFIG_DROPIN_DIR,
|
|
7856
|
+
"-maxdepth",
|
|
7857
|
+
"1",
|
|
7858
|
+
"-type",
|
|
7859
|
+
"f",
|
|
7860
|
+
"(",
|
|
7861
|
+
"-iname",
|
|
7862
|
+
"*.yaml",
|
|
7863
|
+
"-o",
|
|
7864
|
+
"-iname",
|
|
7865
|
+
"*.yml",
|
|
7866
|
+
")"
|
|
7867
|
+
]);
|
|
7868
|
+
if (listed === null) return null;
|
|
7869
|
+
const paths = listed.split("\n").map((path) => path.trim()).filter(Boolean).sort();
|
|
7870
|
+
const dropIns = await Promise.all(paths.map((path) => ctx.fs.readFile(path)));
|
|
7871
|
+
if (dropIns.some((text) => text === null)) return null;
|
|
7872
|
+
return [await ctx.fs.readFile(K3S_OWNER_CONFIG), ...dropIns];
|
|
7873
|
+
}
|
|
7874
|
+
async function controllerProblem(ctx) {
|
|
7875
|
+
const unitText = await stdoutOf(ctx.exec, "systemctl", ["cat", "k3s.service"]);
|
|
7876
|
+
const configs = unitText === null ? null : await readK3sConfigs(ctx);
|
|
7877
|
+
if (unitText === null || configs === null) return CONTROLLER_UNKNOWN_WARNING;
|
|
7878
|
+
return networkPolicyControllerDisabled({ configs, unitText }) ? CONTROLLER_DISABLED_WARNING : null;
|
|
7879
|
+
}
|
|
7880
|
+
async function readHostAddresses(exec) {
|
|
7881
|
+
const nodes = await stdoutOf(exec, "k3s", [
|
|
7882
|
+
"kubectl",
|
|
7883
|
+
"get",
|
|
7884
|
+
"nodes",
|
|
7885
|
+
"-o",
|
|
7886
|
+
`jsonpath=${NODE_ADDRESSES_JSONPATH}`
|
|
7887
|
+
]);
|
|
7888
|
+
const interfaces = await stdoutOf(exec, "ip", ["-o", "-4", "addr", "show"]);
|
|
7889
|
+
const api = await stdoutOf(exec, "k3s", [
|
|
7890
|
+
"kubectl",
|
|
7891
|
+
"get",
|
|
7892
|
+
"endpointslices",
|
|
7893
|
+
"-n",
|
|
7894
|
+
"default",
|
|
7895
|
+
"-l",
|
|
7896
|
+
"kubernetes.io/service-name=kubernetes",
|
|
7897
|
+
"-o",
|
|
7898
|
+
`jsonpath=${API_ENDPOINTS_JSONPATH}`
|
|
7899
|
+
]);
|
|
7900
|
+
if (nodes === null || interfaces === null || api === null) return null;
|
|
7901
|
+
const onInterfaces = [...interfaces.matchAll(IPV4_ON_INTERFACE)].map((match) => match[1] ?? "");
|
|
7902
|
+
return [...nodes.split(/\s+/), ...onInterfaces, ...api.split(/\s+/)].filter(Boolean);
|
|
7903
|
+
}
|
|
7904
|
+
async function recheckIsolation(ctx, reconciled) {
|
|
7905
|
+
if (reconciled?.applied !== true) return { isolation: reconciled, warnings: [] };
|
|
7906
|
+
const problem = await controllerProblem(ctx);
|
|
7907
|
+
return problem ? { isolation: { applied: false }, warnings: [problem] } : { isolation: reconciled, warnings: [] };
|
|
7908
|
+
}
|
|
7909
|
+
|
|
7910
|
+
// src/daemon/prune-namespaces.ts
|
|
7911
|
+
var POOL_NAMESPACE_PREFIX = "claudespace-";
|
|
7912
|
+
var WORKSPACE_MANAGER_ROLE = "conveyor-workspace-manager";
|
|
7913
|
+
var MAX_NAMESPACE_DELETES_PER_RUN = 5;
|
|
7914
|
+
var KUBECTL_TIMEOUT = "--request-timeout=10s";
|
|
7915
|
+
var EXEC_OPTIONS = { timeoutMs: 15e3 };
|
|
7916
|
+
function kubectl(exec, args) {
|
|
7917
|
+
return exec("k3s", ["kubectl", ...args, KUBECTL_TIMEOUT], EXEC_OPTIONS).catch(() => null);
|
|
7918
|
+
}
|
|
7919
|
+
function candidateNamespaces(listing, wanted) {
|
|
7920
|
+
const names = [];
|
|
7921
|
+
for (const line of listing.split("\n")) {
|
|
7922
|
+
const [name, phase] = line.trim().split(/\s+/);
|
|
7923
|
+
if (!name?.startsWith(POOL_NAMESPACE_PREFIX) || wanted.has(name)) continue;
|
|
7924
|
+
if (phase === "Terminating") continue;
|
|
7925
|
+
try {
|
|
7926
|
+
names.push(assertK8sName(name, "namespace"));
|
|
7927
|
+
} catch {
|
|
7928
|
+
}
|
|
7929
|
+
}
|
|
7930
|
+
return names;
|
|
7931
|
+
}
|
|
7932
|
+
async function isEmptyConveyorNamespace(exec, namespace) {
|
|
7933
|
+
const role = await kubectl(exec, [
|
|
7934
|
+
"get",
|
|
7935
|
+
"role",
|
|
7936
|
+
WORKSPACE_MANAGER_ROLE,
|
|
7937
|
+
"-n",
|
|
7938
|
+
namespace,
|
|
7939
|
+
"-o",
|
|
7940
|
+
"name"
|
|
7941
|
+
]);
|
|
7942
|
+
if (role?.exitCode !== 0) return false;
|
|
7943
|
+
const pods = await kubectl(exec, ["get", "pods", "-n", namespace, "-o", "name"]);
|
|
7944
|
+
return pods?.exitCode === 0 && pods.stdout.trim() === "";
|
|
7945
|
+
}
|
|
7946
|
+
async function pruneStaleNamespaces(ctx, desired, result) {
|
|
7947
|
+
if (desired.namespaces.length === 0) return;
|
|
7948
|
+
const listed = await kubectl(ctx.exec, [
|
|
7949
|
+
"get",
|
|
7950
|
+
"namespaces",
|
|
7951
|
+
"-o",
|
|
7952
|
+
'jsonpath={range .items[*]}{.metadata.name}{" "}{.status.phase}{"\\n"}{end}'
|
|
7953
|
+
]);
|
|
7954
|
+
if (listed?.exitCode !== 0) return;
|
|
7955
|
+
const wanted = new Set(desired.namespaces.map((namespace) => namespace.name));
|
|
7956
|
+
let deletes = 0;
|
|
7957
|
+
for (const namespace of candidateNamespaces(listed.stdout, wanted)) {
|
|
7958
|
+
if (deletes >= MAX_NAMESPACE_DELETES_PER_RUN) return;
|
|
7959
|
+
if (!await isEmptyConveyorNamespace(ctx.exec, namespace)) continue;
|
|
7960
|
+
deletes += 1;
|
|
7961
|
+
const removed = await kubectl(ctx.exec, ["delete", "namespace", namespace, "--wait=false"]);
|
|
7962
|
+
if (removed?.exitCode === 0) {
|
|
7963
|
+
result.prunedNamespaces.push(namespace);
|
|
7964
|
+
} else {
|
|
7965
|
+
result.warnings.push(
|
|
7966
|
+
`Could not remove the unused namespace ${namespace}: ${(removed?.stderr ?? "no answer from k3s").trim().slice(0, 160)}`
|
|
7967
|
+
);
|
|
7968
|
+
}
|
|
7969
|
+
}
|
|
7970
|
+
}
|
|
7971
|
+
|
|
7972
|
+
// src/daemon/namespace-upkeep.ts
|
|
7973
|
+
function message(error) {
|
|
7974
|
+
return error instanceof Error ? error.message : String(error);
|
|
7219
7975
|
}
|
|
7220
7976
|
function renderLimitRange(namespace, limits) {
|
|
7221
7977
|
const ns = assertK8sName(namespace, "namespace");
|
|
@@ -7249,56 +8005,522 @@ async function reconcileNamespaces(ctx, desired, result) {
|
|
|
7249
8005
|
await kubectlApply(ctx.exec, renderLimitRange(namespace.name, namespace.limitRange));
|
|
7250
8006
|
} catch (error) {
|
|
7251
8007
|
result.warnings.push(
|
|
7252
|
-
`Could not apply resource defaults to ${namespace.name} (${namespace.projectName}): ${
|
|
8008
|
+
`Could not apply resource defaults to ${namespace.name} (${namespace.projectName}): ${message(error)}`
|
|
7253
8009
|
);
|
|
7254
8010
|
}
|
|
7255
8011
|
}
|
|
7256
8012
|
}
|
|
7257
|
-
async function
|
|
8013
|
+
async function ensureBakeNamespace(ctx, desired) {
|
|
8014
|
+
const found = await ctx.exec(
|
|
8015
|
+
"k3s",
|
|
8016
|
+
["kubectl", "get", "namespace", PERSONAL_BAKE_NAMESPACE],
|
|
8017
|
+
KUBECTL_TIMEOUT2
|
|
8018
|
+
);
|
|
8019
|
+
if (found.exitCode !== 0) {
|
|
8020
|
+
await runOrThrow(
|
|
8021
|
+
ctx.exec,
|
|
8022
|
+
"k3s",
|
|
8023
|
+
["kubectl", "create", "namespace", PERSONAL_BAKE_NAMESPACE],
|
|
8024
|
+
KUBECTL_TIMEOUT2
|
|
8025
|
+
);
|
|
8026
|
+
}
|
|
8027
|
+
const limitRange = desired.namespaces[0]?.limitRange;
|
|
8028
|
+
if (limitRange) {
|
|
8029
|
+
await kubectlApply(ctx.exec, renderLimitRange(PERSONAL_BAKE_NAMESPACE, limitRange));
|
|
8030
|
+
}
|
|
8031
|
+
}
|
|
8032
|
+
function knownStance(isolation) {
|
|
8033
|
+
const { podSecurity, egress, ...rest } = { ...isolation };
|
|
8034
|
+
const extra = Object.keys(rest);
|
|
8035
|
+
if (podSecurity === "baseline" && egress === "no-private" && extra.length === 0) return null;
|
|
8036
|
+
const named = `Pod Security "${String(podSecurity).slice(0, 40)}", egress "${String(egress).slice(0, 40)}"`;
|
|
8037
|
+
return extra.length === 0 ? named : `${named}, and ${extra.slice(0, 3).join(", ").slice(0, 80)}`;
|
|
8038
|
+
}
|
|
8039
|
+
var KUBECTL_TIMEOUT2 = { timeoutMs: 3e4 };
|
|
8040
|
+
var RECHECK = "Conveyor checks again within 10 minutes.";
|
|
8041
|
+
async function isolateNamespace(ctx, namespace, isolation, fail) {
|
|
8042
|
+
const ns = assertK8sName(namespace, "namespace");
|
|
8043
|
+
const labels = Object.entries(psaLabels(isolation.podSecurity)).map(
|
|
8044
|
+
([key, value]) => `${key}=${value}`
|
|
8045
|
+
);
|
|
8046
|
+
const labelled = await ctx.exec(
|
|
8047
|
+
"k3s",
|
|
8048
|
+
["kubectl", "label", "namespace", ns, "--overwrite", ...labels],
|
|
8049
|
+
KUBECTL_TIMEOUT2
|
|
8050
|
+
);
|
|
8051
|
+
if (labelled.exitCode !== 0) {
|
|
8052
|
+
fail(
|
|
8053
|
+
`Could not apply Pod Security labels to ${ns}: ${labelled.stderr.trim().slice(0, 200)} ${RECHECK}`
|
|
8054
|
+
);
|
|
8055
|
+
}
|
|
8056
|
+
if (isolation.hostAddresses === null) return;
|
|
7258
8057
|
try {
|
|
7259
|
-
await
|
|
8058
|
+
await kubectlApply(ctx.exec, renderEgressNetworkPolicy(ns, isolation.hostAddresses));
|
|
7260
8059
|
} catch (error) {
|
|
7261
|
-
|
|
8060
|
+
fail(`Could not apply the egress policy to ${ns}: ${message(error).slice(0, 200)} ${RECHECK}`);
|
|
7262
8061
|
}
|
|
7263
8062
|
}
|
|
7264
|
-
async function
|
|
7265
|
-
|
|
7266
|
-
|
|
7267
|
-
|
|
7268
|
-
restartedK3s: false,
|
|
7269
|
-
skippedKubeletFlags: [],
|
|
7270
|
-
effectiveImageGcHighThreshold: null,
|
|
7271
|
-
recreatedPriorityClass: false
|
|
7272
|
-
};
|
|
7273
|
-
const disallowedKinds = findDisallowedKinds(desired.rbacManifest);
|
|
7274
|
-
if (disallowedKinds.length > 0) {
|
|
8063
|
+
async function reconcileIsolation(ctx, desired, result) {
|
|
8064
|
+
try {
|
|
8065
|
+
await ensureBakeNamespace(ctx, desired);
|
|
8066
|
+
} catch (error) {
|
|
7275
8067
|
result.warnings.push(
|
|
7276
|
-
`
|
|
8068
|
+
`Could not prepare the bake namespace ${PERSONAL_BAKE_NAMESPACE}: ${message(error)}`
|
|
7277
8069
|
);
|
|
7278
|
-
}
|
|
7279
|
-
|
|
7280
|
-
|
|
7281
|
-
|
|
7282
|
-
|
|
7283
|
-
|
|
8070
|
+
}
|
|
8071
|
+
const isolation = desired.isolation;
|
|
8072
|
+
if (!isolation) {
|
|
8073
|
+
result.isolation = void 0;
|
|
8074
|
+
return;
|
|
8075
|
+
}
|
|
8076
|
+
const unknown = knownStance(isolation);
|
|
8077
|
+
if (unknown) {
|
|
8078
|
+
result.warnings.push(
|
|
8079
|
+
`Conveyor asks for pod isolation this version of conveyor-k3 does not know (${unknown}), so nothing was changed and this server does not count as isolated. Run \`sudo conveyor-k3 update\`.`
|
|
7284
8080
|
);
|
|
8081
|
+
result.isolation = { applied: false };
|
|
8082
|
+
return;
|
|
7285
8083
|
}
|
|
7286
|
-
|
|
7287
|
-
|
|
7288
|
-
|
|
7289
|
-
()
|
|
8084
|
+
let applied = true;
|
|
8085
|
+
const fail = (warning) => {
|
|
8086
|
+
applied = false;
|
|
8087
|
+
result.warnings.push(warning);
|
|
8088
|
+
};
|
|
8089
|
+
const problem = await controllerProblem(ctx);
|
|
8090
|
+
if (problem) fail(problem);
|
|
8091
|
+
const hostAddresses = await readHostAddresses(ctx.exec);
|
|
8092
|
+
if (hostAddresses === null) {
|
|
8093
|
+
fail(
|
|
8094
|
+
`Could not read every address this server answers on, so the egress policies in place were left as they are and this server does not count as isolated. ${RECHECK}`
|
|
8095
|
+
);
|
|
8096
|
+
}
|
|
8097
|
+
for (const namespace of desired.namespaces) {
|
|
8098
|
+
await isolateNamespace(ctx, namespace.name, { podSecurity: "baseline", hostAddresses }, fail);
|
|
8099
|
+
}
|
|
8100
|
+
await isolateNamespace(
|
|
8101
|
+
ctx,
|
|
8102
|
+
PERSONAL_BAKE_NAMESPACE,
|
|
8103
|
+
{ podSecurity: "privileged", hostAddresses },
|
|
8104
|
+
fail
|
|
7290
8105
|
);
|
|
7291
|
-
|
|
7292
|
-
|
|
7293
|
-
|
|
7294
|
-
|
|
8106
|
+
result.isolation = { applied };
|
|
8107
|
+
}
|
|
8108
|
+
|
|
8109
|
+
// src/daemon/registry-tags.ts
|
|
8110
|
+
var REPO_NAME = /^[a-z0-9]+(?:[._-][a-z0-9]+)*(?:\/[a-z0-9]+(?:[._-][a-z0-9]+)*)*$/;
|
|
8111
|
+
var TAG_NAME = /^[A-Za-z0-9_][A-Za-z0-9._-]{0,127}$/;
|
|
8112
|
+
var LEG_TAG = /^current-([0-9a-f]{16})-(?:amd64|arm64)$/;
|
|
8113
|
+
var DIGEST = /^sha256:[0-9a-f]{64}$/;
|
|
8114
|
+
var PAGE_SIZE = 100;
|
|
8115
|
+
var MAX_REPOSITORIES = 1e3;
|
|
8116
|
+
var MAX_TAGS = 5e3;
|
|
8117
|
+
var REQUEST_TIMEOUT_MS = 15e3;
|
|
8118
|
+
var MANIFEST_ACCEPT = [
|
|
8119
|
+
"application/vnd.oci.image.index.v1+json",
|
|
8120
|
+
"application/vnd.docker.distribution.manifest.list.v2+json",
|
|
8121
|
+
"application/vnd.oci.image.manifest.v1+json",
|
|
8122
|
+
"application/vnd.docker.distribution.manifest.v2+json"
|
|
8123
|
+
].join(", ");
|
|
8124
|
+
function stringList(value) {
|
|
8125
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
8126
|
+
}
|
|
8127
|
+
async function getJson(fetchImpl, path) {
|
|
8128
|
+
const response = await fetchImpl(`${PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT}${path}`, {
|
|
8129
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
8130
|
+
});
|
|
8131
|
+
if (!response.ok) {
|
|
8132
|
+
await response.body?.cancel();
|
|
8133
|
+
throw new Error(`The local registry answered ${response.status} to GET ${path}`);
|
|
8134
|
+
}
|
|
8135
|
+
return { body: await response.json(), hasNext: response.headers.has("link") };
|
|
8136
|
+
}
|
|
8137
|
+
async function listPaged(fetchImpl, path, key, limit) {
|
|
8138
|
+
const names = [];
|
|
8139
|
+
let last;
|
|
8140
|
+
while (names.length < limit) {
|
|
8141
|
+
const query = `n=${PAGE_SIZE}${last ? `&last=${encodeURIComponent(last)}` : ""}`;
|
|
8142
|
+
const { body, hasNext } = await getJson(fetchImpl, `${path}?${query}`);
|
|
8143
|
+
const page = stringList(body?.[key]);
|
|
8144
|
+
names.push(...page);
|
|
8145
|
+
if (!hasNext || page.length === 0) return { names, complete: true };
|
|
8146
|
+
last = page.at(-1);
|
|
8147
|
+
}
|
|
8148
|
+
return { names: names.slice(0, limit), complete: false };
|
|
8149
|
+
}
|
|
8150
|
+
async function readManifest(fetchImpl, repo, reference, method) {
|
|
8151
|
+
const response = await fetchImpl(
|
|
8152
|
+
`${PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT}/v2/${repo}/manifests/${reference}`,
|
|
8153
|
+
{
|
|
8154
|
+
method,
|
|
8155
|
+
headers: { Accept: MANIFEST_ACCEPT },
|
|
8156
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
8157
|
+
}
|
|
7295
8158
|
);
|
|
7296
|
-
|
|
8159
|
+
const digest = response.headers.get("docker-content-digest");
|
|
8160
|
+
if (!response.ok || !digest || !DIGEST.test(digest)) {
|
|
8161
|
+
await response.body?.cancel();
|
|
8162
|
+
return null;
|
|
8163
|
+
}
|
|
8164
|
+
if (method === "HEAD") return { digest, children: [] };
|
|
8165
|
+
const body = await response.json();
|
|
8166
|
+
const children = (body?.manifests ?? []).map((entry) => entry.digest).filter((child) => typeof child === "string" && DIGEST.test(child));
|
|
8167
|
+
return { digest, children };
|
|
8168
|
+
}
|
|
8169
|
+
function isSupersededLeg(tag, retain) {
|
|
8170
|
+
const match = LEG_TAG.exec(tag);
|
|
8171
|
+
return match?.[1] !== void 0 && !retain.has(match[1]);
|
|
8172
|
+
}
|
|
8173
|
+
async function planRepository(fetchImpl, repo, retain) {
|
|
8174
|
+
const listed = await listPaged(fetchImpl, `/v2/${repo}/tags/list`, "tags", MAX_TAGS);
|
|
8175
|
+
if (!listed.complete) return { skip: `it has more than ${MAX_TAGS} tags` };
|
|
8176
|
+
const tags = listed.names.filter((tag) => TAG_NAME.test(tag));
|
|
8177
|
+
const legs = tags.filter((tag) => isSupersededLeg(tag, retain));
|
|
8178
|
+
const kept = tags.filter((tag) => !isSupersededLeg(tag, retain));
|
|
8179
|
+
if (legs.length === 0) return { deletions: [], kept: kept.length };
|
|
8180
|
+
const keep = /* @__PURE__ */ new Set();
|
|
8181
|
+
for (const tag of kept) {
|
|
8182
|
+
const manifest = await readManifest(fetchImpl, repo, tag, "GET");
|
|
8183
|
+
if (!manifest) return { skip: `the tag ${tag} it keeps could not be read` };
|
|
8184
|
+
keep.add(manifest.digest);
|
|
8185
|
+
for (const child of manifest.children) keep.add(child);
|
|
8186
|
+
}
|
|
8187
|
+
const byDigest = /* @__PURE__ */ new Map();
|
|
8188
|
+
for (const tag of legs) {
|
|
8189
|
+
const manifest = await readManifest(fetchImpl, repo, tag, "HEAD");
|
|
8190
|
+
if (!manifest || keep.has(manifest.digest)) continue;
|
|
8191
|
+
byDigest.set(manifest.digest, [...byDigest.get(manifest.digest) ?? [], tag]);
|
|
8192
|
+
}
|
|
8193
|
+
const deletions = [...byDigest].map(([digest, names]) => ({ repo, digest, tags: names }));
|
|
8194
|
+
return { deletions, kept: kept.length };
|
|
8195
|
+
}
|
|
8196
|
+
async function planTagCleanup(fetchImpl, retain) {
|
|
8197
|
+
const catalog = await listPaged(fetchImpl, "/v2/_catalog", "repositories", MAX_REPOSITORIES);
|
|
8198
|
+
const repos = catalog.names.filter(
|
|
8199
|
+
(repo) => repo.startsWith("personal-") && REPO_NAME.test(repo)
|
|
8200
|
+
);
|
|
8201
|
+
const plan = { deletions: [], keptTags: 0, skippedRepos: [] };
|
|
8202
|
+
for (const repo of repos) {
|
|
8203
|
+
const planned = await planRepository(fetchImpl, repo, retain).catch((error) => ({
|
|
8204
|
+
skip: `its tags could not be read (${error instanceof Error ? error.message : String(error)})`
|
|
8205
|
+
}));
|
|
8206
|
+
if ("skip" in planned) {
|
|
8207
|
+
plan.skippedRepos.push({ repo, reason: planned.skip });
|
|
8208
|
+
continue;
|
|
8209
|
+
}
|
|
8210
|
+
plan.deletions.push(...planned.deletions);
|
|
8211
|
+
plan.keptTags += planned.kept;
|
|
8212
|
+
}
|
|
8213
|
+
return plan;
|
|
8214
|
+
}
|
|
8215
|
+
async function deleteManifest(fetchImpl, deletion) {
|
|
8216
|
+
const response = await fetchImpl(
|
|
8217
|
+
`${PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT}/v2/${deletion.repo}/manifests/${deletion.digest}`,
|
|
8218
|
+
{ method: "DELETE", signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) }
|
|
8219
|
+
);
|
|
8220
|
+
await response.body?.cancel();
|
|
8221
|
+
return response.status === 202 || response.status === 404;
|
|
8222
|
+
}
|
|
8223
|
+
|
|
8224
|
+
// src/daemon/registry-gc.ts
|
|
8225
|
+
var HOUR_MS = 60 * 6e4;
|
|
8226
|
+
var REGISTRY_GC_INTERVAL_MS = 20 * HOUR_MS;
|
|
8227
|
+
var REGISTRY_GC_SIZE_BYTES = 20 * 1024 ** 3;
|
|
8228
|
+
var REGISTRY_GC_SIZE_INTERVAL_MS = 6 * HOUR_MS;
|
|
8229
|
+
var BAKE_OBJECT_STALE_MS = 6 * HOUR_MS;
|
|
8230
|
+
function lines(text) {
|
|
8231
|
+
return text.split("\n").filter((line) => line.trim().length > 0);
|
|
8232
|
+
}
|
|
8233
|
+
var inNamespace = inRegistryNamespace;
|
|
8234
|
+
function defaultSleep(ms) {
|
|
8235
|
+
return new Promise((resolve) => {
|
|
8236
|
+
setTimeout(resolve, ms);
|
|
8237
|
+
});
|
|
8238
|
+
}
|
|
8239
|
+
async function bakeActivity(exec, now) {
|
|
8240
|
+
const pods = await exec(
|
|
8241
|
+
"k3s",
|
|
8242
|
+
[
|
|
8243
|
+
"kubectl",
|
|
8244
|
+
"get",
|
|
8245
|
+
"pods",
|
|
8246
|
+
"--all-namespaces",
|
|
8247
|
+
"-l",
|
|
8248
|
+
CONVEYOR_POD_SELECTOR,
|
|
8249
|
+
// A pod still pulling its image needs the registry as much as a running one.
|
|
8250
|
+
"--field-selector=status.phase!=Succeeded,status.phase!=Failed",
|
|
8251
|
+
"-o",
|
|
8252
|
+
"name"
|
|
8253
|
+
],
|
|
8254
|
+
{ timeoutMs: 3e4 }
|
|
8255
|
+
);
|
|
8256
|
+
if (pods.exitCode !== 0) return "the Conveyor pods could not be listed";
|
|
8257
|
+
const podCount = lines(pods.stdout).length;
|
|
8258
|
+
if (podCount > 0) return `${podCount} Conveyor pod(s) are on the machine`;
|
|
8259
|
+
const bakes = await exec(
|
|
8260
|
+
"k3s",
|
|
8261
|
+
[
|
|
8262
|
+
"kubectl",
|
|
8263
|
+
"get",
|
|
8264
|
+
"jobs,secrets,configmaps",
|
|
8265
|
+
"--all-namespaces",
|
|
8266
|
+
"-l",
|
|
8267
|
+
`${BAKE_COMPONENT_LABEL} in (${BAKE_COMPONENT_VALUES.join(",")})`,
|
|
8268
|
+
"-o",
|
|
8269
|
+
'jsonpath={range .items[*]}{.metadata.creationTimestamp}{"\\n"}{end}'
|
|
8270
|
+
],
|
|
8271
|
+
{ timeoutMs: 3e4 }
|
|
8272
|
+
);
|
|
8273
|
+
if (bakes.exitCode !== 0) return "the bake objects could not be listed";
|
|
8274
|
+
const inFlight = lines(bakes.stdout).some((stamp) => {
|
|
8275
|
+
const created = Date.parse(stamp.trim());
|
|
8276
|
+
return Number.isNaN(created) || now - created < BAKE_OBJECT_STALE_MS;
|
|
8277
|
+
});
|
|
8278
|
+
return inFlight ? "a bake is in flight" : null;
|
|
8279
|
+
}
|
|
8280
|
+
async function pollUntil(check, timeoutMs, intervalMs, sleep2) {
|
|
8281
|
+
for (let waited = 0; waited < timeoutMs; waited += intervalMs) {
|
|
8282
|
+
if (await check()) return true;
|
|
8283
|
+
await sleep2(intervalMs);
|
|
8284
|
+
}
|
|
8285
|
+
return await check();
|
|
8286
|
+
}
|
|
8287
|
+
async function registryPodsGone(exec) {
|
|
8288
|
+
const result = await inNamespace(exec, [
|
|
8289
|
+
"get",
|
|
8290
|
+
"pods",
|
|
8291
|
+
"-l",
|
|
8292
|
+
`app.kubernetes.io/name=${PERSONAL_LOCAL_REGISTRY_NAME}`,
|
|
8293
|
+
"-o",
|
|
8294
|
+
"name"
|
|
8295
|
+
]);
|
|
8296
|
+
return result.exitCode === 0 && lines(result.stdout).length === 0;
|
|
8297
|
+
}
|
|
8298
|
+
async function gcJobState(exec) {
|
|
8299
|
+
const result = await inNamespace(exec, [
|
|
8300
|
+
"get",
|
|
8301
|
+
"job",
|
|
8302
|
+
LOCAL_REGISTRY_GC_JOB,
|
|
8303
|
+
"-o",
|
|
8304
|
+
"jsonpath={.status.succeeded},{.status.failed}"
|
|
8305
|
+
]);
|
|
8306
|
+
if (result.exitCode !== 0) return null;
|
|
8307
|
+
const [succeeded, failed] = result.stdout.trim().split(",");
|
|
8308
|
+
if (Number(succeeded) > 0) return "succeeded";
|
|
8309
|
+
return Number(failed) > 0 ? "failed" : null;
|
|
8310
|
+
}
|
|
8311
|
+
async function waitForGcJob(exec, sleep2) {
|
|
8312
|
+
for (let waited = 0; waited <= 11 * 6e4; waited += 5e3) {
|
|
8313
|
+
const state = await gcJobState(exec);
|
|
8314
|
+
if (state) return state;
|
|
8315
|
+
await sleep2(5e3);
|
|
8316
|
+
}
|
|
8317
|
+
return null;
|
|
8318
|
+
}
|
|
8319
|
+
async function releaseRegistry(ports, mark) {
|
|
8320
|
+
const { exec } = ports;
|
|
8321
|
+
const now = (ports.now ?? Date.now)();
|
|
8322
|
+
const current = await readRegistryDeployment(exec);
|
|
8323
|
+
if (current?.mark !== mark && gcMarkIsLive(current?.mark ?? null, now, ports.isAlive)) return;
|
|
8324
|
+
if (!await deleteSweepJob(exec)) {
|
|
8325
|
+
ports.log(
|
|
8326
|
+
"Left the local image registry stopped: its cleanup Job could not be deleted. The next hourly pass tries again."
|
|
8327
|
+
);
|
|
8328
|
+
return;
|
|
8329
|
+
}
|
|
8330
|
+
await clearGcMark(exec);
|
|
8331
|
+
const restored = await inNamespace(exec, [
|
|
8332
|
+
"scale",
|
|
8333
|
+
"deployment",
|
|
8334
|
+
PERSONAL_LOCAL_REGISTRY_NAME,
|
|
8335
|
+
"--replicas=1"
|
|
8336
|
+
]);
|
|
8337
|
+
if (restored.exitCode !== 0) {
|
|
8338
|
+
ports.log(
|
|
8339
|
+
`Could not scale the local image registry back up; the next hourly pass restores it: ${restored.stderr.trim().slice(0, 200)}`
|
|
8340
|
+
);
|
|
8341
|
+
}
|
|
8342
|
+
}
|
|
8343
|
+
async function collectBlobs(ports) {
|
|
8344
|
+
const { exec } = ports;
|
|
8345
|
+
const sleep2 = ports.sleep ?? defaultSleep;
|
|
8346
|
+
if (!await deleteSweepJob(exec)) throw new Error("a previous cleanup Job could not be deleted");
|
|
8347
|
+
const mark = newGcMark((ports.now ?? Date.now)());
|
|
8348
|
+
await setGcMark(exec, mark);
|
|
8349
|
+
try {
|
|
8350
|
+
const scaled = await inNamespace(exec, [
|
|
8351
|
+
"scale",
|
|
8352
|
+
"deployment",
|
|
8353
|
+
PERSONAL_LOCAL_REGISTRY_NAME,
|
|
8354
|
+
"--replicas=0"
|
|
8355
|
+
]);
|
|
8356
|
+
if (scaled.exitCode !== 0) throw new Error("the registry could not be scaled to zero");
|
|
8357
|
+
if (!await pollUntil(() => registryPodsGone(exec), 12e4, 2e3, sleep2)) {
|
|
8358
|
+
throw new Error("the registry pod did not stop within 2 minutes");
|
|
8359
|
+
}
|
|
8360
|
+
const held = await readRegistryDeployment(exec);
|
|
8361
|
+
if (held?.replicas !== 0 || held.mark !== mark) {
|
|
8362
|
+
throw new Error(
|
|
8363
|
+
"the registry was started again by another process, so the sweep did not run"
|
|
8364
|
+
);
|
|
8365
|
+
}
|
|
8366
|
+
await kubectlApply(exec, renderRegistryGcJob());
|
|
8367
|
+
const state = await waitForGcJob(exec, sleep2);
|
|
8368
|
+
if (state !== "succeeded") throw new Error(`garbage-collect ${state ?? "did not finish"}`);
|
|
8369
|
+
} finally {
|
|
8370
|
+
await releaseRegistry(ports, mark);
|
|
8371
|
+
}
|
|
8372
|
+
}
|
|
8373
|
+
async function readRetainList(ports) {
|
|
8374
|
+
const desired = await ports.requestDesiredState().catch(() => null);
|
|
8375
|
+
if (!desired) return "the settings could not be fetched from Conveyor";
|
|
8376
|
+
if (desired.localRegistry?.enabled !== true) return "the local image registry is off";
|
|
8377
|
+
return desired.localRegistry.retainHashes ?? "Conveyor did not send the list of bakes to keep";
|
|
8378
|
+
}
|
|
8379
|
+
async function registryBlocker(ports, now) {
|
|
8380
|
+
const deployment = await readRegistryDeployment(ports.exec);
|
|
8381
|
+
if (!deployment) return "the local image registry could not be read";
|
|
8382
|
+
if (!deployment.exists) return "the local image registry is not deployed yet";
|
|
8383
|
+
if (gcMarkIsLive(deployment.mark, now, ports.isAlive))
|
|
8384
|
+
return "another registry cleanup is running";
|
|
8385
|
+
return await bakeActivity(ports.exec, now);
|
|
8386
|
+
}
|
|
8387
|
+
async function collectRegistryGarbage(ports, options) {
|
|
8388
|
+
const skip = (reason) => ({
|
|
8389
|
+
skipped: reason,
|
|
8390
|
+
deletions: [],
|
|
8391
|
+
blobsCollected: false
|
|
8392
|
+
});
|
|
8393
|
+
const now = ports.now ?? Date.now;
|
|
8394
|
+
const fetchImpl = ports.fetch ?? fetch;
|
|
8395
|
+
const retain = await readRetainList(ports);
|
|
8396
|
+
if (typeof retain === "string") return skip(retain);
|
|
8397
|
+
const blocked = await registryBlocker(ports, now());
|
|
8398
|
+
if (blocked) return skip(blocked);
|
|
8399
|
+
const plan = await planTagCleanup(fetchImpl, new Set(retain));
|
|
8400
|
+
for (const skipped of plan.skippedRepos) {
|
|
8401
|
+
ports.log(`Left ${skipped.repo} alone: ${skipped.reason}.`);
|
|
8402
|
+
}
|
|
8403
|
+
if (options.dryRun) return { deletions: plan.deletions, blobsCollected: false };
|
|
8404
|
+
const late2 = await bakeActivity(ports.exec, now());
|
|
8405
|
+
if (late2) return skip(late2);
|
|
8406
|
+
const deletions = [];
|
|
8407
|
+
for (const deletion of plan.deletions) {
|
|
8408
|
+
if (await deleteManifest(fetchImpl, deletion)) deletions.push(deletion);
|
|
8409
|
+
else ports.log(`Could not delete ${deletion.repo}@${deletion.digest}.`);
|
|
8410
|
+
}
|
|
8411
|
+
await collectBlobs(ports);
|
|
8412
|
+
return { deletions, blobsCollected: true };
|
|
8413
|
+
}
|
|
8414
|
+
async function registryDataBytes(exec) {
|
|
8415
|
+
const result = await exec("du", ["-s", "--block-size=1", LOCAL_REGISTRY_DATA_DIR], {
|
|
8416
|
+
timeoutMs: 12e4
|
|
8417
|
+
});
|
|
8418
|
+
if (result.exitCode !== 0) return null;
|
|
8419
|
+
const bytes = Number.parseInt(result.stdout.trim().split(/\s+/)[0] ?? "", 10);
|
|
8420
|
+
return Number.isFinite(bytes) ? bytes : null;
|
|
8421
|
+
}
|
|
8422
|
+
async function registryGcDue(exec, lastGcAt, maintenanceHour, now = /* @__PURE__ */ new Date()) {
|
|
8423
|
+
const last = lastGcAt ? Date.parse(lastGcAt) : Number.NaN;
|
|
8424
|
+
const since = Number.isNaN(last) ? Number.POSITIVE_INFINITY : now.getTime() - last;
|
|
8425
|
+
if (now.getHours() === maintenanceHour && since >= REGISTRY_GC_INTERVAL_MS) return true;
|
|
8426
|
+
if (since < REGISTRY_GC_SIZE_INTERVAL_MS) return false;
|
|
8427
|
+
const bytes = await registryDataBytes(exec);
|
|
8428
|
+
return bytes !== null && bytes >= REGISTRY_GC_SIZE_BYTES;
|
|
7297
8429
|
}
|
|
7298
8430
|
|
|
7299
8431
|
// src/daemon/telemetry.ts
|
|
7300
8432
|
import { statfs } from "fs/promises";
|
|
7301
8433
|
|
|
8434
|
+
// src/vm/capacity.ts
|
|
8435
|
+
var GIB = 1024 ** 3;
|
|
8436
|
+
var MIB = 1024 ** 2;
|
|
8437
|
+
var CONTROL_RELAY_SLOTS = 6;
|
|
8438
|
+
var CAPACITY_BOUNDS = {
|
|
8439
|
+
relaySlots: { min: 16, max: 192, flag: "relay-slots" },
|
|
8440
|
+
relayMaxGiB: { min: 1, max: 16, flag: "relay-max-gib" },
|
|
8441
|
+
diskMiBps: { min: 25, max: 2e3, flag: "disk-mbps" },
|
|
8442
|
+
diskIops: { min: 500, max: 5e4, flag: "disk-iops" }
|
|
8443
|
+
};
|
|
8444
|
+
var LEGACY_CAPACITY_LIMITS = Object.freeze({
|
|
8445
|
+
schema: 1,
|
|
8446
|
+
profile: "default",
|
|
8447
|
+
relaySlots: 16,
|
|
8448
|
+
reservedRelaySlots: 0,
|
|
8449
|
+
relayMaxBytes: GIB,
|
|
8450
|
+
diskBytesPerSecond: 100 * MIB,
|
|
8451
|
+
diskIops: 2e3
|
|
8452
|
+
});
|
|
8453
|
+
var KEYS = Object.keys(CAPACITY_BOUNDS);
|
|
8454
|
+
var PROFILES = /* @__PURE__ */ new Set(["default", "server", "custom"]);
|
|
8455
|
+
function parseCapacityLimits(value) {
|
|
8456
|
+
if (value === void 0) return { ...LEGACY_CAPACITY_LIMITS };
|
|
8457
|
+
const limits = typeof value === "object" && value !== null ? value : {};
|
|
8458
|
+
const within = (key, min, max) => Number.isSafeInteger(limits[key]) && limits[key] >= min && limits[key] <= max;
|
|
8459
|
+
const bounds = CAPACITY_BOUNDS;
|
|
8460
|
+
if (limits.schema !== 1 || !PROFILES.has(limits.profile) || !within("relaySlots", bounds.relaySlots.min, bounds.relaySlots.max) || limits.reservedRelaySlots !== 0 && limits.reservedRelaySlots !== CONTROL_RELAY_SLOTS || !within("relayMaxBytes", bounds.relayMaxGiB.min * GIB, bounds.relayMaxGiB.max * GIB) || !within("diskBytesPerSecond", bounds.diskMiBps.min * MIB, bounds.diskMiBps.max * MIB) || !within("diskIops", bounds.diskIops.min, bounds.diskIops.max))
|
|
8461
|
+
throw new Error("The VM capacity limits in the host network policy are invalid.");
|
|
8462
|
+
return {
|
|
8463
|
+
schema: 1,
|
|
8464
|
+
profile: limits.profile,
|
|
8465
|
+
relaySlots: limits.relaySlots,
|
|
8466
|
+
reservedRelaySlots: limits.reservedRelaySlots,
|
|
8467
|
+
relayMaxBytes: limits.relayMaxBytes,
|
|
8468
|
+
diskBytesPerSecond: limits.diskBytesPerSecond,
|
|
8469
|
+
diskIops: limits.diskIops
|
|
8470
|
+
};
|
|
8471
|
+
}
|
|
8472
|
+
|
|
8473
|
+
// src/daemon/local-images.ts
|
|
8474
|
+
var MAX_LOCAL_IMAGES = 64;
|
|
8475
|
+
var MAX_INSPECTIONS_PER_BEAT = 4;
|
|
8476
|
+
var CONTENT_HASH = /^[0-9a-f]{16}$/;
|
|
8477
|
+
function isImageRef(ref) {
|
|
8478
|
+
try {
|
|
8479
|
+
assertImageRef(ref);
|
|
8480
|
+
return true;
|
|
8481
|
+
} catch {
|
|
8482
|
+
return false;
|
|
8483
|
+
}
|
|
8484
|
+
}
|
|
8485
|
+
async function inspectContentHash(exec, imageId) {
|
|
8486
|
+
const result = await exec("crictl", ["inspecti", "-o", "json", imageId], {
|
|
8487
|
+
timeoutMs: 5e3,
|
|
8488
|
+
maxOutputBytes: 256 * 1024
|
|
8489
|
+
}).catch(() => null);
|
|
8490
|
+
if (!result || result.exitCode !== 0) return void 0;
|
|
8491
|
+
try {
|
|
8492
|
+
const label = JSON.parse(result.stdout).info?.imageSpec?.config?.Labels?.[CONVEYOR_CONTENT_HASH_LABEL];
|
|
8493
|
+
return typeof label === "string" && CONTENT_HASH.test(label) ? label : null;
|
|
8494
|
+
} catch {
|
|
8495
|
+
return void 0;
|
|
8496
|
+
}
|
|
8497
|
+
}
|
|
8498
|
+
async function reportLocalImages(exec, inventory, wantedImages, cache) {
|
|
8499
|
+
if (!inventory) return void 0;
|
|
8500
|
+
const wanted = new Set(wantedImages);
|
|
8501
|
+
const held = inventory.images.flatMap(
|
|
8502
|
+
(image) => (image.repoTags ?? []).filter((ref) => ref.startsWith("localhost/") && isImageRef(ref)).map((ref) => ({ ref, imageId: image.id }))
|
|
8503
|
+
);
|
|
8504
|
+
held.sort(
|
|
8505
|
+
(a, b) => Number(wanted.has(b.ref)) - Number(wanted.has(a.ref)) || (a.ref < b.ref ? -1 : a.ref > b.ref ? 1 : 0)
|
|
8506
|
+
);
|
|
8507
|
+
const reported = held.slice(0, MAX_LOCAL_IMAGES);
|
|
8508
|
+
const live = new Set(reported.map((entry) => entry.imageId));
|
|
8509
|
+
for (const id of [...cache.keys()]) if (!live.has(id)) cache.delete(id);
|
|
8510
|
+
const images = [];
|
|
8511
|
+
let inspections = 0;
|
|
8512
|
+
for (const entry of reported) {
|
|
8513
|
+
let contentHash = cache.get(entry.imageId);
|
|
8514
|
+
if (contentHash === void 0 && inspections < MAX_INSPECTIONS_PER_BEAT) {
|
|
8515
|
+
inspections += 1;
|
|
8516
|
+
contentHash = await inspectContentHash(exec, entry.imageId);
|
|
8517
|
+
if (contentHash !== void 0) cache.set(entry.imageId, contentHash);
|
|
8518
|
+
}
|
|
8519
|
+
images.push({ ...entry, contentHash: contentHash ?? null });
|
|
8520
|
+
}
|
|
8521
|
+
return images;
|
|
8522
|
+
}
|
|
8523
|
+
|
|
7302
8524
|
// src/commands/runners.ts
|
|
7303
8525
|
var STAGING_FILE = `${RUNNERS_SUDOERS_FILE}.staged`;
|
|
7304
8526
|
async function detectRunnerUsers(exec) {
|
|
@@ -7334,6 +8556,21 @@ function ungrantedRunnersWarning(users) {
|
|
|
7334
8556
|
var GRANTED_COMMAND_NAMES = LOCAL_BAKE_ROOT_COMMANDS.map((command) => command.name);
|
|
7335
8557
|
|
|
7336
8558
|
// src/daemon/telemetry.ts
|
|
8559
|
+
var LOCAL_REGISTRY_WARN_AFTER_MS = 10 * 6e4;
|
|
8560
|
+
async function reportLocalRegistry(ports, watch, now = Date.now()) {
|
|
8561
|
+
if (!watch) return { fields: {}, warnings: [] };
|
|
8562
|
+
const ready = await localRegistryReady({ readFile: ports.readFile, fetch: ports.fetch });
|
|
8563
|
+
const paused = (watch.pausedUntil ?? 0) > now;
|
|
8564
|
+
watch.notReadySince = ready || paused ? null : watch.notReadySince ?? now;
|
|
8565
|
+
const stuckMs = watch.notReadySince === null ? 0 : now - watch.notReadySince;
|
|
8566
|
+
return {
|
|
8567
|
+
fields: { localRegistry: { host: PERSONAL_LOCAL_REGISTRY_HOST, ready } },
|
|
8568
|
+
warnings: stuckMs >= LOCAL_REGISTRY_WARN_AFTER_MS ? [
|
|
8569
|
+
`The local image registry has not been ready for ${Math.floor(stuckMs / 6e4)} minutes, so shadow bakes cannot use it yet. Check its pod with \`k3s kubectl -n conveyor-system get pods -l app.kubernetes.io/name=conveyor-registry\`. If the pod is running, k3s has not loaded the registry's mirror yet: it restarts k3s for that only while no Conveyor pod runs.`
|
|
8570
|
+
] : []
|
|
8571
|
+
};
|
|
8572
|
+
}
|
|
8573
|
+
var VM_CAPACITY_FILE = "/etc/conveyor-vm/capacity.json";
|
|
7337
8574
|
async function versionOf(exec, bin, args) {
|
|
7338
8575
|
const result = await exec(bin, args, { timeoutMs: 1e4 }).catch(() => null);
|
|
7339
8576
|
if (!result || result.exitCode !== 0) return null;
|
|
@@ -7366,16 +8603,26 @@ async function readNodeAllocatable(exec) {
|
|
|
7366
8603
|
const [cpu, memory] = result.stdout.trim().split(/\s+/);
|
|
7367
8604
|
return { allocatableCpu: cpu ?? null, allocatableMemory: memory ?? null };
|
|
7368
8605
|
}
|
|
7369
|
-
async function
|
|
8606
|
+
async function measureK3sStore(exec) {
|
|
8607
|
+
const result = await exec("du", ["-sx", "-B1", K3S_CONTAINERD_DIR], {
|
|
8608
|
+
timeoutMs: 6e4,
|
|
8609
|
+
maxOutputBytes: 1024 * 1024
|
|
8610
|
+
}).catch(() => null);
|
|
8611
|
+
const match = result?.stdout.match(/^\s*(\d+)\s/);
|
|
8612
|
+
if (!match?.[1]) return null;
|
|
8613
|
+
const bytes = Number(match[1]);
|
|
8614
|
+
return Number.isSafeInteger(bytes) ? bytes : null;
|
|
8615
|
+
}
|
|
8616
|
+
async function readDisk(k3sStoreBytes) {
|
|
7370
8617
|
try {
|
|
7371
8618
|
const stats = await statfs(STATE_DIR);
|
|
7372
8619
|
return {
|
|
7373
8620
|
totalBytes: Number(stats.blocks) * Number(stats.bsize),
|
|
7374
8621
|
freeBytes: Number(stats.bavail) * Number(stats.bsize),
|
|
7375
|
-
k3sStoreBytes
|
|
8622
|
+
k3sStoreBytes
|
|
7376
8623
|
};
|
|
7377
8624
|
} catch {
|
|
7378
|
-
return { totalBytes: 0, freeBytes: 0, k3sStoreBytes
|
|
8625
|
+
return { totalBytes: 0, freeBytes: 0, k3sStoreBytes };
|
|
7379
8626
|
}
|
|
7380
8627
|
}
|
|
7381
8628
|
function summarizeImages(inventory, wantedImages) {
|
|
@@ -7398,14 +8645,33 @@ function imageGcPressureWarning(disk, highThreshold) {
|
|
|
7398
8645
|
if (usedPercent < highThreshold) return null;
|
|
7399
8646
|
return `Kubelet image garbage collection is active: the disk is ${usedPercent}% full and the image-gc-high-threshold is ${highThreshold}%. Images that no container is running are removed within 5 minutes, a freshly baked one included. Free disk space, or raise image-gc-high-threshold.`;
|
|
7400
8647
|
}
|
|
8648
|
+
async function readVmCapacity(ports) {
|
|
8649
|
+
const text = await ports.readFile(VM_CAPACITY_FILE);
|
|
8650
|
+
if (text === null) return void 0;
|
|
8651
|
+
try {
|
|
8652
|
+
const { schema: _schema, ...capacity } = parseCapacityLimits(JSON.parse(text));
|
|
8653
|
+
return capacity;
|
|
8654
|
+
} catch {
|
|
8655
|
+
return void 0;
|
|
8656
|
+
}
|
|
8657
|
+
}
|
|
7401
8658
|
async function collectHeartbeat(ports, state, inventory) {
|
|
7402
8659
|
const registries = parseRegistries(await ports.readFile(REGISTRIES_FILE));
|
|
7403
|
-
const disk = await readDisk();
|
|
8660
|
+
const disk = await readDisk(state.k3sStoreBytes ?? null);
|
|
7404
8661
|
const gcWarning = imageGcPressureWarning(disk, state.imageGcHighThreshold);
|
|
8662
|
+
const vmCapacity = await readVmCapacity(ports);
|
|
8663
|
+
const localImages = await reportLocalImages(
|
|
8664
|
+
ports.exec,
|
|
8665
|
+
inventory,
|
|
8666
|
+
state.wantedImages,
|
|
8667
|
+
state.localImageHashes ?? /* @__PURE__ */ new Map()
|
|
8668
|
+
);
|
|
8669
|
+
const registry = await reportLocalRegistry(ports, state.localRegistry);
|
|
7405
8670
|
const runnerUsers = await detectRunnerUsers(ports.exec);
|
|
7406
8671
|
const ungranted = ungrantedRunnerUsers(runnerUsers, await ports.readFile(RUNNERS_SUDOERS_FILE));
|
|
7407
8672
|
return {
|
|
7408
8673
|
publicWebProxy: (await ports.readFile("/etc/conveyor-vm/public-web"))?.trim() === "enabled",
|
|
8674
|
+
...vmCapacity ? { vmCapacity } : {},
|
|
7409
8675
|
cliVersion: state.cliVersion,
|
|
7410
8676
|
k3sVersion: await versionOf(ports.exec, "k3s", ["--version"]),
|
|
7411
8677
|
cloudflaredVersion: await versionOf(ports.exec, "cloudflared", ["--version"]),
|
|
@@ -7421,42 +8687,262 @@ async function collectHeartbeat(ports, state, inventory) {
|
|
|
7421
8687
|
lastWarmAt: state.lastWarmAt,
|
|
7422
8688
|
lastUpdateAt: state.lastUpdateAt,
|
|
7423
8689
|
...runnerUsers.length > 0 ? { runnerUsers } : {},
|
|
7424
|
-
|
|
8690
|
+
...localImages ? { localImages } : {},
|
|
8691
|
+
...registry.fields,
|
|
8692
|
+
...state.isolation ? { isolation: state.isolation } : {},
|
|
8693
|
+
localWarnings: withinHeartbeatBounds([
|
|
7425
8694
|
...state.localWarnings,
|
|
7426
8695
|
...gcWarning ? [gcWarning] : [],
|
|
7427
|
-
...ungranted.length > 0 ? [ungrantedRunnersWarning(ungranted)] : []
|
|
7428
|
-
|
|
8696
|
+
...ungranted.length > 0 ? [ungrantedRunnersWarning(ungranted)] : [],
|
|
8697
|
+
...registry.warnings
|
|
8698
|
+
])
|
|
8699
|
+
};
|
|
8700
|
+
}
|
|
8701
|
+
var HEARTBEAT_LIST_MAX = 64;
|
|
8702
|
+
var HEARTBEAT_STRING_MAX = 512;
|
|
8703
|
+
function withinHeartbeatBounds(warnings) {
|
|
8704
|
+
return warnings.slice(0, HEARTBEAT_LIST_MAX).map(
|
|
8705
|
+
(warning) => warning.length > HEARTBEAT_STRING_MAX ? `${warning.slice(0, HEARTBEAT_STRING_MAX - 1)}\u2026` : warning
|
|
8706
|
+
);
|
|
8707
|
+
}
|
|
8708
|
+
async function readPodIdentities(exec) {
|
|
8709
|
+
const result = await exec(
|
|
8710
|
+
"k3s",
|
|
8711
|
+
[
|
|
8712
|
+
"kubectl",
|
|
8713
|
+
"get",
|
|
8714
|
+
"pods",
|
|
8715
|
+
"-A",
|
|
8716
|
+
"-l",
|
|
8717
|
+
CONVEYOR_POD_SELECTOR,
|
|
8718
|
+
"--request-timeout=5s",
|
|
8719
|
+
"--no-headers",
|
|
8720
|
+
"-o",
|
|
8721
|
+
"custom-columns=NS:.metadata.namespace,NAME:.metadata.name,UID:.metadata.uid"
|
|
8722
|
+
],
|
|
8723
|
+
{ timeoutMs: 7e3, maxOutputBytes: 65536 }
|
|
8724
|
+
);
|
|
8725
|
+
if (result.exitCode !== 0) return [];
|
|
8726
|
+
const rows = result.stdout.trim().split("\n").filter(Boolean);
|
|
8727
|
+
if (rows.length > 100) return [];
|
|
8728
|
+
return rows.flatMap((row) => {
|
|
8729
|
+
const [namespace, name, uid, extra] = row.trim().split(/\s+/);
|
|
8730
|
+
return !extra && [namespace, name, uid].every((s) => s && /^[a-zA-Z0-9][a-zA-Z0-9.-]{0,252}$/.test(s)) ? [{ namespace, name, uid }] : [];
|
|
8731
|
+
});
|
|
8732
|
+
}
|
|
8733
|
+
|
|
8734
|
+
// src/daemon/registry-upkeep-clock.ts
|
|
8735
|
+
function errorText(error) {
|
|
8736
|
+
return error instanceof Error ? error.message : String(error);
|
|
8737
|
+
}
|
|
8738
|
+
async function collect(ports, now) {
|
|
8739
|
+
const { exec, log, requestDesiredState, fetch: fetch2, sleep: sleep2, isAlive, watch } = ports;
|
|
8740
|
+
if (watch) watch.pausedUntil = now.getTime() + REGISTRY_GC_MARK_TTL_MS;
|
|
8741
|
+
try {
|
|
8742
|
+
const outcome = await collectRegistryGarbage(
|
|
8743
|
+
{ exec, log, requestDesiredState, fetch: fetch2, sleep: sleep2, isAlive },
|
|
8744
|
+
{ dryRun: false }
|
|
8745
|
+
);
|
|
8746
|
+
if (outcome.skipped) return outcome.skipped;
|
|
8747
|
+
log(
|
|
8748
|
+
`Registry garbage collection removed ${outcome.deletions.length} superseded image manifest(s) and their unreferenced layers.`
|
|
8749
|
+
);
|
|
8750
|
+
return null;
|
|
8751
|
+
} finally {
|
|
8752
|
+
if (watch) {
|
|
8753
|
+
watch.pausedUntil = null;
|
|
8754
|
+
watch.notReadySince = null;
|
|
8755
|
+
}
|
|
8756
|
+
}
|
|
8757
|
+
}
|
|
8758
|
+
async function runRegistryUpkeep(ports, lastGcAt, maintenanceHour) {
|
|
8759
|
+
const now = (ports.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
8760
|
+
try {
|
|
8761
|
+
await deployLocalRegistry({
|
|
8762
|
+
exec: ports.exec,
|
|
8763
|
+
fs: ports.fs,
|
|
8764
|
+
runningConveyorPods: null,
|
|
8765
|
+
now: ports.now,
|
|
8766
|
+
isAlive: ports.isAlive
|
|
8767
|
+
});
|
|
8768
|
+
if (await registryMirrorPending(ports.fs) && mayDisrupt(await countRunningConveyorPods(ports.exec))) {
|
|
8769
|
+
await ports.reconcile();
|
|
8770
|
+
}
|
|
8771
|
+
if (!await registryGcDue(ports.exec, lastGcAt, maintenanceHour, now)) return lastGcAt;
|
|
8772
|
+
} catch (error) {
|
|
8773
|
+
ports.log(`Local image registry upkeep failed: ${errorText(error)}`);
|
|
8774
|
+
return lastGcAt;
|
|
8775
|
+
}
|
|
8776
|
+
try {
|
|
8777
|
+
const skipped = await collect(ports, now);
|
|
8778
|
+
if (skipped) {
|
|
8779
|
+
ports.log(`Registry garbage collection skipped: ${skipped}.`);
|
|
8780
|
+
return lastGcAt;
|
|
8781
|
+
}
|
|
8782
|
+
} catch (error) {
|
|
8783
|
+
ports.log(`Registry garbage collection failed: ${errorText(error)}`);
|
|
8784
|
+
}
|
|
8785
|
+
return now.toISOString();
|
|
8786
|
+
}
|
|
8787
|
+
|
|
8788
|
+
// src/daemon/reconcile.ts
|
|
8789
|
+
function ownerRestartWarning(change) {
|
|
8790
|
+
return `${change} Conveyor did not install this server's k3s, so it never restarts it: run \`sudo systemctl restart k3s\` when no build runs.`;
|
|
8791
|
+
}
|
|
8792
|
+
async function reconcileKubelet(ctx, desired, result, managed) {
|
|
8793
|
+
const ownerConfig = await ctx.fs.readFile(K3S_OWNER_CONFIG);
|
|
8794
|
+
const rendered = renderK3sDropIn(desired.kubelet, ownerConfig, { disableComponents: managed });
|
|
8795
|
+
result.skippedKubeletFlags = rendered.skipped;
|
|
8796
|
+
result.effectiveImageGcHighThreshold = rendered.skipped.includes("image-gc-high-threshold") ? ownerImageGcHighThreshold(ownerConfig) : desired.kubelet.imageGcHighThreshold;
|
|
8797
|
+
for (const flag of rendered.skipped) {
|
|
8798
|
+
result.warnings.push(
|
|
8799
|
+
`Left \`${flag}\` to your own /etc/rancher/k3s/config.yaml \u2014 Conveyor does not override a setting you chose.`
|
|
8800
|
+
);
|
|
8801
|
+
}
|
|
8802
|
+
const current = await ctx.fs.readFile(K3S_CONFIG_DROPIN);
|
|
8803
|
+
if (rendered.content === null) {
|
|
8804
|
+
if (current !== null) await ctx.fs.removeFile(K3S_CONFIG_DROPIN);
|
|
8805
|
+
return;
|
|
8806
|
+
}
|
|
8807
|
+
if (current === rendered.content) return;
|
|
8808
|
+
await ctx.fs.ensureDir(K3S_CONFIG_DROPIN.slice(0, K3S_CONFIG_DROPIN.lastIndexOf("/")));
|
|
8809
|
+
await ctx.fs.writeFile(K3S_CONFIG_DROPIN, rendered.content, 420);
|
|
8810
|
+
if (!managed) {
|
|
8811
|
+
result.warnings.push(ownerRestartWarning("Kubelet settings changed."));
|
|
8812
|
+
return;
|
|
8813
|
+
}
|
|
8814
|
+
if (!mayDisrupt(ctx.runningConveyorPods)) {
|
|
8815
|
+
result.warnings.push(
|
|
8816
|
+
`Kubelet settings changed but ${busyReason(ctx.runningConveyorPods)} \u2014 the restart is deferred until the machine is idle.`
|
|
8817
|
+
);
|
|
8818
|
+
return;
|
|
8819
|
+
}
|
|
8820
|
+
const restart = await ctx.exec("systemctl", ["restart", "k3s"], { timeoutMs: 12e4 });
|
|
8821
|
+
if (restart.exitCode === 0) {
|
|
8822
|
+
result.restartedK3s = true;
|
|
8823
|
+
} else {
|
|
8824
|
+
result.warnings.push(
|
|
8825
|
+
`Could not restart k3s to apply new kubelet settings: ${restart.stderr.trim().slice(0, 200)}`
|
|
8826
|
+
);
|
|
8827
|
+
}
|
|
8828
|
+
}
|
|
8829
|
+
async function reconcilePriorityClass(ctx, desired, result) {
|
|
8830
|
+
const name = assertK8sName(desired.priorityClass.name, "priorityClass.name");
|
|
8831
|
+
const read = await ctx.exec("k3s", [
|
|
8832
|
+
"kubectl",
|
|
8833
|
+
"get",
|
|
8834
|
+
"priorityclass",
|
|
8835
|
+
name,
|
|
8836
|
+
"-o",
|
|
8837
|
+
"jsonpath={.value}"
|
|
8838
|
+
]);
|
|
8839
|
+
if (read.exitCode !== 0) return;
|
|
8840
|
+
const existingValue = Number.parseInt(read.stdout.trim(), 10);
|
|
8841
|
+
if (existingValue === desired.priorityClass.value) return;
|
|
8842
|
+
if (!mayDisrupt(ctx.runningConveyorPods)) {
|
|
8843
|
+
result.warnings.push(
|
|
8844
|
+
`The \`${name}\` PriorityClass has value ${existingValue}, but Conveyor needs ${desired.priorityClass.value}. The value is immutable, so it must be recreated \u2014 deferred because ${busyReason(ctx.runningConveyorPods)}.`
|
|
8845
|
+
);
|
|
8846
|
+
return;
|
|
8847
|
+
}
|
|
8848
|
+
const removed = await ctx.exec("k3s", ["kubectl", "delete", "priorityclass", name]);
|
|
8849
|
+
if (removed.exitCode !== 0) {
|
|
8850
|
+
result.warnings.push(
|
|
8851
|
+
`Could not replace the \`${name}\` PriorityClass (value ${existingValue}, needs ${desired.priorityClass.value}): ${removed.stderr.trim().slice(0, 160)}`
|
|
8852
|
+
);
|
|
8853
|
+
return;
|
|
8854
|
+
}
|
|
8855
|
+
try {
|
|
8856
|
+
await kubectlApply(ctx.exec, desired.rbacManifest);
|
|
8857
|
+
result.recreatedPriorityClass = true;
|
|
8858
|
+
} catch (error) {
|
|
8859
|
+
result.warnings.push(
|
|
8860
|
+
`The \`${name}\` PriorityClass was removed to change its value but could not be recreated: ${error instanceof Error ? error.message : String(error)}. No build can start on this machine until it exists \u2014 run \`conveyor-k3 sync\`.`
|
|
8861
|
+
);
|
|
8862
|
+
}
|
|
8863
|
+
}
|
|
8864
|
+
async function step(result, label, run) {
|
|
8865
|
+
try {
|
|
8866
|
+
await run();
|
|
8867
|
+
} catch (error) {
|
|
8868
|
+
result.warnings.push(`${label}: ${error instanceof Error ? error.message : String(error)}`);
|
|
8869
|
+
}
|
|
8870
|
+
}
|
|
8871
|
+
async function reconcile(ctx, desired) {
|
|
8872
|
+
const result = {
|
|
8873
|
+
appliedVersion: desired.version,
|
|
8874
|
+
warnings: [],
|
|
8875
|
+
restartedK3s: false,
|
|
8876
|
+
skippedKubeletFlags: [],
|
|
8877
|
+
effectiveImageGcHighThreshold: null,
|
|
8878
|
+
recreatedPriorityClass: false,
|
|
8879
|
+
prunedNamespaces: []
|
|
7429
8880
|
};
|
|
7430
|
-
|
|
7431
|
-
|
|
7432
|
-
|
|
7433
|
-
|
|
7434
|
-
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
|
|
7441
|
-
|
|
7442
|
-
|
|
7443
|
-
|
|
7444
|
-
|
|
7445
|
-
|
|
7446
|
-
|
|
8881
|
+
const disallowedKinds = findDisallowedKinds(desired.rbacManifest);
|
|
8882
|
+
const manifestAccepted = disallowedKinds.length === 0;
|
|
8883
|
+
if (disallowedKinds.length > 0) {
|
|
8884
|
+
result.warnings.push(
|
|
8885
|
+
`Conveyor sent a cluster manifest containing disallowed kind(s) ${disallowedKinds.join(", ")} \u2014 refused. RBAC and PriorityClass steps skipped; this should never happen, report it.`
|
|
8886
|
+
);
|
|
8887
|
+
} else {
|
|
8888
|
+
await step(result, "Could not apply RBAC", () => kubectlApply(ctx.exec, desired.rbacManifest));
|
|
8889
|
+
await step(
|
|
8890
|
+
result,
|
|
8891
|
+
"Could not reconcile the PriorityClass",
|
|
8892
|
+
() => reconcilePriorityClass(ctx, desired, result)
|
|
8893
|
+
);
|
|
8894
|
+
}
|
|
8895
|
+
await step(
|
|
8896
|
+
result,
|
|
8897
|
+
"Could not apply namespace defaults",
|
|
8898
|
+
() => reconcileNamespaces(ctx, desired, result)
|
|
7447
8899
|
);
|
|
7448
|
-
|
|
7449
|
-
|
|
7450
|
-
|
|
7451
|
-
|
|
7452
|
-
|
|
7453
|
-
|
|
7454
|
-
|
|
8900
|
+
await step(
|
|
8901
|
+
result,
|
|
8902
|
+
"Could not apply pod isolation",
|
|
8903
|
+
() => reconcileIsolation(ctx, desired, result)
|
|
8904
|
+
);
|
|
8905
|
+
if (desired.isolation && result.isolation === void 0) result.isolation = { applied: false };
|
|
8906
|
+
if (manifestAccepted) {
|
|
8907
|
+
await step(
|
|
8908
|
+
result,
|
|
8909
|
+
"Could not remove unused namespaces",
|
|
8910
|
+
() => pruneStaleNamespaces(ctx, desired, result)
|
|
8911
|
+
);
|
|
8912
|
+
}
|
|
8913
|
+
const registry = desired.localRegistry?.enabled === true;
|
|
8914
|
+
const managed = managesK3s(desired, await readState(ctx.fs).catch(() => ({})));
|
|
8915
|
+
if (registry) {
|
|
8916
|
+
await step(result, "Could not reconcile the local image registry", async () => {
|
|
8917
|
+
await deployLocalRegistry(ctx);
|
|
8918
|
+
});
|
|
8919
|
+
}
|
|
8920
|
+
await step(
|
|
8921
|
+
result,
|
|
8922
|
+
"Could not apply kubelet settings",
|
|
8923
|
+
() => reconcileKubelet(ctx, desired, result, managed)
|
|
8924
|
+
);
|
|
8925
|
+
if (registry && !result.restartedK3s) {
|
|
8926
|
+
await step(result, "Could not load the local image registry mirror", async () => {
|
|
8927
|
+
if (!managed) {
|
|
8928
|
+
if (await registryMirrorPending(ctx.fs))
|
|
8929
|
+
result.warnings.push(ownerRestartWarning("The local image registry's mirror is new."));
|
|
8930
|
+
return;
|
|
8931
|
+
}
|
|
8932
|
+
const loaded = await loadRegistryMirror(ctx);
|
|
8933
|
+
result.restartedK3s = loaded.restarted;
|
|
8934
|
+
if (loaded.warning) result.warnings.push(loaded.warning);
|
|
8935
|
+
});
|
|
8936
|
+
}
|
|
8937
|
+
return result;
|
|
7455
8938
|
}
|
|
7456
8939
|
|
|
7457
8940
|
// src/daemon/runner.ts
|
|
7458
|
-
var
|
|
7459
|
-
var DAY_MS = 24 *
|
|
8941
|
+
var HOUR_MS2 = 60 * 6e4;
|
|
8942
|
+
var DAY_MS = 24 * HOUR_MS2;
|
|
8943
|
+
var STORE_MEASURE_INTERVAL_MS = 55 * 6e4;
|
|
8944
|
+
var ISOLATION_RETRY_FIRST_MS = 6e4;
|
|
8945
|
+
var ISOLATION_RETRY_MAX_MS = 10 * 6e4;
|
|
7460
8946
|
var DISK_PRESSURE_FREE_FRACTION = 0.2;
|
|
7461
8947
|
var STRANDED_PRUNE_BYTES = 20 * 1024 ** 3;
|
|
7462
8948
|
function flattenWanted(desired) {
|
|
@@ -7493,10 +8979,26 @@ var ClusterDaemon = class {
|
|
|
7493
8979
|
* the next reconcile silently clear them.
|
|
7494
8980
|
*/
|
|
7495
8981
|
warmWarnings = [];
|
|
8982
|
+
/** Content hash labels by image id, so each image is inspected once. */
|
|
8983
|
+
localImageHashes = /* @__PURE__ */ new Map();
|
|
8984
|
+
/** How long the in-cluster registry has been not ready, across beats. */
|
|
8985
|
+
registryWatch = { notReadySince: null };
|
|
8986
|
+
/**
|
|
8987
|
+
* Whether pod isolation is in force, from the last reconcile; undefined
|
|
8988
|
+
* while desired state does not ask for it. Every heartbeat checks it again
|
|
8989
|
+
* against k3s's own settings before reporting it.
|
|
8990
|
+
*/
|
|
8991
|
+
isolation = void 0;
|
|
7496
8992
|
timers = [];
|
|
8993
|
+
/** When the last store walk started, so one that failed is not retried on every reconcile. */
|
|
8994
|
+
storeWalkStartedAt = 0;
|
|
7497
8995
|
/** Serializes reconcile against itself — a sync nudge during a reconcile
|
|
7498
8996
|
* must queue, not interleave two kubectl applies of the same manifests. */
|
|
7499
8997
|
reconciling = null;
|
|
8998
|
+
/** The run queued behind the running one, until it starts. At most one waits. */
|
|
8999
|
+
queuedRun = null;
|
|
9000
|
+
/** The next reconcile while pod isolation is not applied; `dueAt` 0 = none. */
|
|
9001
|
+
isolationRetry = { delayMs: ISOLATION_RETRY_FIRST_MS, dueAt: 0 };
|
|
7500
9002
|
constructor(ports) {
|
|
7501
9003
|
this.ports = ports;
|
|
7502
9004
|
}
|
|
@@ -7513,7 +9015,7 @@ var ClusterDaemon = class {
|
|
|
7513
9015
|
}, PERSONAL_CLUSTER_HEARTBEAT_INTERVAL_MS),
|
|
7514
9016
|
setInterval(() => {
|
|
7515
9017
|
void this.warmAndPruneIfDue();
|
|
7516
|
-
},
|
|
9018
|
+
}, HOUR_MS2),
|
|
7517
9019
|
setInterval(() => {
|
|
7518
9020
|
void this.syncAndReconcile();
|
|
7519
9021
|
}, DAY_MS)
|
|
@@ -7532,10 +9034,27 @@ var ClusterDaemon = class {
|
|
|
7532
9034
|
}
|
|
7533
9035
|
/** Fetch the desired state and converge to it. Queued, never concurrent. */
|
|
7534
9036
|
syncAndReconcile() {
|
|
7535
|
-
|
|
9037
|
+
if (this.queuedRun) return this.queuedRun;
|
|
9038
|
+
const run = (this.reconciling ?? Promise.resolve()).then(() => {
|
|
9039
|
+
this.queuedRun = null;
|
|
9040
|
+
return this.runReconcile();
|
|
9041
|
+
});
|
|
9042
|
+
this.queuedRun = run;
|
|
7536
9043
|
this.reconciling = run.catch(() => void 0);
|
|
7537
9044
|
return run;
|
|
7538
9045
|
}
|
|
9046
|
+
/** Reconcile again later while pod isolation is not applied; stop once it is. */
|
|
9047
|
+
scheduleIsolationRetry() {
|
|
9048
|
+
if (this.isolation?.applied !== false) {
|
|
9049
|
+
this.isolationRetry = { delayMs: ISOLATION_RETRY_FIRST_MS, dueAt: 0 };
|
|
9050
|
+
return;
|
|
9051
|
+
}
|
|
9052
|
+
const { delayMs } = this.isolationRetry;
|
|
9053
|
+
this.isolationRetry = {
|
|
9054
|
+
delayMs: Math.min(delayMs * 2, ISOLATION_RETRY_MAX_MS),
|
|
9055
|
+
dueAt: Date.now() + delayMs
|
|
9056
|
+
};
|
|
9057
|
+
}
|
|
7539
9058
|
async runReconcile() {
|
|
7540
9059
|
try {
|
|
7541
9060
|
this.desired = await this.ports.connection.requestDesiredState();
|
|
@@ -7551,13 +9070,21 @@ var ClusterDaemon = class {
|
|
|
7551
9070
|
this.desired
|
|
7552
9071
|
);
|
|
7553
9072
|
this.warnings = result.warnings;
|
|
9073
|
+
this.isolation = result.isolation;
|
|
9074
|
+
this.scheduleIsolationRetry();
|
|
7554
9075
|
for (const warning of result.warnings) this.ports.log(warning);
|
|
9076
|
+
if (result.prunedNamespaces.length > 0) {
|
|
9077
|
+
this.ports.log(
|
|
9078
|
+
`Removed ${result.prunedNamespaces.length} empty namespace(s) of projects this machine no longer serves: ${result.prunedNamespaces.join(", ")}.`
|
|
9079
|
+
);
|
|
9080
|
+
}
|
|
7555
9081
|
this.effectiveImageGcHigh = result.effectiveImageGcHighThreshold;
|
|
7556
9082
|
const state = this.state ?? await readState(this.ports.fs);
|
|
7557
9083
|
this.state = { ...state, appliedDesiredStateVersion: result.appliedVersion };
|
|
7558
9084
|
await writeState(this.ports.fs, this.state);
|
|
7559
9085
|
await this.warm();
|
|
7560
9086
|
await this.pruneIfDue();
|
|
9087
|
+
await this.measureStoreIfDue();
|
|
7561
9088
|
}
|
|
7562
9089
|
/**
|
|
7563
9090
|
* The hourly clock: warm, re-pin, and reclaim if the pile justifies it.
|
|
@@ -7570,6 +9097,45 @@ var ClusterDaemon = class {
|
|
|
7570
9097
|
async warmAndPruneIfDue() {
|
|
7571
9098
|
await this.warm();
|
|
7572
9099
|
await this.pruneIfDue();
|
|
9100
|
+
await this.measureStoreIfDue();
|
|
9101
|
+
if (this.desired?.localRegistry?.enabled !== true) return;
|
|
9102
|
+
const lastRegistryGcAt = await runRegistryUpkeep(
|
|
9103
|
+
{
|
|
9104
|
+
exec: this.ports.exec,
|
|
9105
|
+
fs: this.ports.fs,
|
|
9106
|
+
log: this.ports.log,
|
|
9107
|
+
requestDesiredState: () => this.ports.connection.requestDesiredState(),
|
|
9108
|
+
reconcile: () => this.syncAndReconcile(),
|
|
9109
|
+
watch: this.registryWatch,
|
|
9110
|
+
fetch: this.ports.fetch
|
|
9111
|
+
},
|
|
9112
|
+
this.state?.lastRegistryGcAt ?? null,
|
|
9113
|
+
this.desired.maintenance.hour
|
|
9114
|
+
);
|
|
9115
|
+
if (lastRegistryGcAt === (this.state?.lastRegistryGcAt ?? null)) return;
|
|
9116
|
+
const state = this.state ?? await readState(this.ports.fs);
|
|
9117
|
+
this.state = { ...state, lastRegistryGcAt };
|
|
9118
|
+
await writeState(this.ports.fs, this.state);
|
|
9119
|
+
}
|
|
9120
|
+
/**
|
|
9121
|
+
* Measure k3s's containerd store when the last measurement is old enough,
|
|
9122
|
+
* and keep the result for the heartbeat. After a prune, so the number it
|
|
9123
|
+
* reports is what the prune left. A failed measurement keeps the last one.
|
|
9124
|
+
*/
|
|
9125
|
+
async measureStoreIfDue() {
|
|
9126
|
+
const measured = this.state?.k3sStoreMeasuredAt ? Date.parse(this.state.k3sStoreMeasuredAt) : 0;
|
|
9127
|
+
const last = Math.max(measured, this.storeWalkStartedAt);
|
|
9128
|
+
if (Date.now() - last < STORE_MEASURE_INTERVAL_MS) return;
|
|
9129
|
+
this.storeWalkStartedAt = Date.now();
|
|
9130
|
+
const bytes = await measureK3sStore(this.ports.exec);
|
|
9131
|
+
if (bytes === null) return;
|
|
9132
|
+
const state = this.state ?? await readState(this.ports.fs);
|
|
9133
|
+
this.state = { ...state, k3sStoreBytes: bytes, k3sStoreMeasuredAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
9134
|
+
await writeState(this.ports.fs, this.state);
|
|
9135
|
+
}
|
|
9136
|
+
/** The store size the heartbeat reports: the last measurement, null before one. */
|
|
9137
|
+
get measuredStoreBytes() {
|
|
9138
|
+
return this.state?.k3sStoreBytes ?? null;
|
|
7573
9139
|
}
|
|
7574
9140
|
/** Pre-pull the wanted refs. Silent on success; that is the normal case. */
|
|
7575
9141
|
async warm() {
|
|
@@ -7674,8 +9240,18 @@ var ClusterDaemon = class {
|
|
|
7674
9240
|
async buildHeartbeat() {
|
|
7675
9241
|
const { wantedImages, authRequiredHosts } = this.desired ? flattenWanted(this.desired) : { wantedImages: [], authRequiredHosts: [] };
|
|
7676
9242
|
const inventory = await readImageInventory(this.ports.exec).catch(() => null);
|
|
9243
|
+
const registryOn = this.desired?.localRegistry?.enabled === true;
|
|
9244
|
+
if (!registryOn) this.registryWatch.notReadySince = null;
|
|
9245
|
+
const isolation = await recheckIsolation(
|
|
9246
|
+
{ exec: this.ports.exec, fs: this.ports.fs },
|
|
9247
|
+
this.isolation
|
|
9248
|
+
);
|
|
7677
9249
|
return await collectHeartbeat(
|
|
7678
|
-
{
|
|
9250
|
+
{
|
|
9251
|
+
exec: this.ports.exec,
|
|
9252
|
+
readFile: (path) => this.ports.fs.readFile(path),
|
|
9253
|
+
fetch: this.ports.fetch
|
|
9254
|
+
},
|
|
7679
9255
|
{
|
|
7680
9256
|
cliVersion: this.ports.cliVersion,
|
|
7681
9257
|
appliedDesiredStateVersion: this.state?.appliedDesiredStateVersion ?? 0,
|
|
@@ -7684,8 +9260,12 @@ var ClusterDaemon = class {
|
|
|
7684
9260
|
lastPruneAt: this.state?.lastPruneAt ?? null,
|
|
7685
9261
|
lastWarmAt: this.state?.lastWarmAt ?? null,
|
|
7686
9262
|
lastUpdateAt: this.state?.lastUpdateAt ?? null,
|
|
7687
|
-
localWarnings: [...this.warnings, ...this.warmWarnings],
|
|
7688
|
-
imageGcHighThreshold: this.effectiveImageGcHigh
|
|
9263
|
+
localWarnings: [...this.warnings, ...this.warmWarnings, ...isolation.warnings],
|
|
9264
|
+
imageGcHighThreshold: this.effectiveImageGcHigh,
|
|
9265
|
+
localImageHashes: this.localImageHashes,
|
|
9266
|
+
...registryOn ? { localRegistry: this.registryWatch } : {},
|
|
9267
|
+
...isolation.isolation ? { isolation: isolation.isolation } : {},
|
|
9268
|
+
k3sStoreBytes: this.measuredStoreBytes
|
|
7689
9269
|
},
|
|
7690
9270
|
inventory
|
|
7691
9271
|
);
|
|
@@ -7709,6 +9289,10 @@ var ClusterDaemon = class {
|
|
|
7709
9289
|
return this.beating;
|
|
7710
9290
|
}
|
|
7711
9291
|
async runBeat() {
|
|
9292
|
+
if (this.isolationRetry.dueAt > 0 && Date.now() >= this.isolationRetry.dueAt) {
|
|
9293
|
+
this.isolationRetry.dueAt = 0;
|
|
9294
|
+
void this.syncAndReconcile();
|
|
9295
|
+
}
|
|
7712
9296
|
try {
|
|
7713
9297
|
const workloads = await this.ports.connection.sendHeartbeat({
|
|
7714
9298
|
...await this.buildHeartbeat(),
|
|
@@ -7833,8 +9417,9 @@ var nodeFileSystem = {
|
|
|
7833
9417
|
async removeFile(path) {
|
|
7834
9418
|
await rm(path, { force: true });
|
|
7835
9419
|
},
|
|
7836
|
-
async ensureDir(path) {
|
|
9420
|
+
async ensureDir(path, mode) {
|
|
7837
9421
|
await mkdir(path, { recursive: true });
|
|
9422
|
+
if (mode !== void 0) await chmod(path, mode);
|
|
7838
9423
|
}
|
|
7839
9424
|
};
|
|
7840
9425
|
function createMemoryFileSystem(seed = {}) {
|
|
@@ -7856,13 +9441,17 @@ function createMemoryFileSystem(seed = {}) {
|
|
|
7856
9441
|
removed.push(path);
|
|
7857
9442
|
return Promise.resolve();
|
|
7858
9443
|
},
|
|
7859
|
-
ensureDir: () =>
|
|
9444
|
+
ensureDir: (path, mode) => {
|
|
9445
|
+
if (mode !== void 0) modes.set(path, mode);
|
|
9446
|
+
return Promise.resolve();
|
|
9447
|
+
}
|
|
7860
9448
|
};
|
|
7861
9449
|
}
|
|
7862
9450
|
|
|
7863
9451
|
// src/verbs.ts
|
|
7864
9452
|
var VERBS = [
|
|
7865
9453
|
"vm",
|
|
9454
|
+
"host",
|
|
7866
9455
|
"setup",
|
|
7867
9456
|
"start",
|
|
7868
9457
|
"stop",
|
|
@@ -7895,14 +9484,22 @@ function parseArgs(argv) {
|
|
|
7895
9484
|
}
|
|
7896
9485
|
return { verb, positional, flags };
|
|
7897
9486
|
}
|
|
7898
|
-
var HELP = `conveyor-k3 \u2014
|
|
9487
|
+
var HELP = `conveyor-k3 \u2014 Personal Compute for Conveyor
|
|
7899
9488
|
|
|
7900
9489
|
Usage: conveyor-k3 <command> [options]
|
|
7901
9490
|
|
|
7902
|
-
|
|
7903
|
-
|
|
7904
|
-
|
|
7905
|
-
|
|
9491
|
+
One product, two runtimes. Pick the one that fits the machine:
|
|
9492
|
+
|
|
9493
|
+
On your computer or server:
|
|
9494
|
+
vm Personal computer runtime: an isolated VM on macOS or
|
|
9495
|
+
Linux. Run conveyor-k3 vm for its commands.
|
|
9496
|
+
host Dedicated server runtime: k3s and the daemon on this
|
|
9497
|
+
Linux machine, with no VM. Root only. Run
|
|
9498
|
+
conveyor-k3 host for its commands.
|
|
9499
|
+
|
|
9500
|
+
Inside a linked machine (the VM guest, or the server itself):
|
|
9501
|
+
setup VM guest only: install and enrol this machine. Safe
|
|
9502
|
+
to re-run. On a server, use conveyor-k3 host setup.
|
|
7906
9503
|
start Start the tunnel and the daemon.
|
|
7907
9504
|
stop Stop both, and tell Conveyor at once so your next
|
|
7908
9505
|
build goes straight to project compute.
|
|
@@ -7917,16 +9514,24 @@ Commands:
|
|
|
7917
9514
|
--username <user>
|
|
7918
9515
|
--password-stdin read the password from stdin
|
|
7919
9516
|
--no-restart skip the required k3s restart
|
|
9517
|
+
registry gc Delete superseded shadow-bake images from the image
|
|
9518
|
+
registry inside this cluster. Runs daily on its own,
|
|
9519
|
+
and only while no Conveyor pod or bake is running.
|
|
9520
|
+
--dry-run list what it would delete
|
|
7920
9521
|
runners list Which GitHub Actions runners this machine hosts, and
|
|
7921
9522
|
whether each may import a locally baked image.
|
|
7922
9523
|
runners grant <user>\u2026 Let those runner users import a locally baked image
|
|
7923
9524
|
into this node and pin it. Writes one file under
|
|
7924
9525
|
/etc/sudoers.d, checked with visudo first. Needed only
|
|
7925
9526
|
when a project has Bake Locally turned on.
|
|
7926
|
-
uninstall
|
|
7927
|
-
|
|
7928
|
-
|
|
7929
|
-
|
|
9527
|
+
uninstall VM guest only: remove Conveyor's units, environment,
|
|
9528
|
+
state, and image registry. Your k3s config, your own
|
|
9529
|
+
registries.yaml entries, your workloads, and the
|
|
9530
|
+
registry's data are left alone. On a server, use
|
|
9531
|
+
conveyor-k3 host uninstall: it also removes the
|
|
9532
|
+
device key.
|
|
9533
|
+
--purge-k3s --yes also remove k3s itself and the
|
|
9534
|
+
registry's data
|
|
7930
9535
|
daemon Run in the foreground (what the systemd unit calls).
|
|
7931
9536
|
|
|
7932
9537
|
Options:
|
|
@@ -7945,17 +9550,12 @@ async function registryAdd(ports, options) {
|
|
|
7945
9550
|
);
|
|
7946
9551
|
}
|
|
7947
9552
|
const raw = await ports.fs.readFile(REGISTRIES_FILE);
|
|
7948
|
-
const parsed = parseRegistries(raw);
|
|
7949
|
-
let backupPath = null;
|
|
7950
|
-
if (raw !== null) {
|
|
7951
|
-
backupPath = `${REGISTRIES_FILE}.bak-${ports.now().toISOString().replace(/[:.]/g, "-")}`;
|
|
7952
|
-
await ports.fs.writeFile(backupPath, raw, 384);
|
|
7953
|
-
}
|
|
7954
9553
|
const auth = { username: options.username, password: options.password };
|
|
7955
|
-
await
|
|
7956
|
-
|
|
7957
|
-
|
|
7958
|
-
|
|
9554
|
+
const backupPath = await writeRegistriesWithBackup(
|
|
9555
|
+
ports.fs,
|
|
9556
|
+
raw,
|
|
9557
|
+
upsertRegistryAuth(parseRegistries(raw), host, auth),
|
|
9558
|
+
ports.now()
|
|
7959
9559
|
);
|
|
7960
9560
|
if (options.noRestart) {
|
|
7961
9561
|
ports.log(`Added credentials for ${host}. Restart k3s to pick them up: systemctl restart k3s`);
|
|
@@ -7994,24 +9594,115 @@ function renderRegistryAuthSummary(missing) {
|
|
|
7994
9594
|
}
|
|
7995
9595
|
|
|
7996
9596
|
// src/commands/uninstall.ts
|
|
9597
|
+
async function removeLocalRegistry(ports) {
|
|
9598
|
+
const removed = await ports.exec(
|
|
9599
|
+
"k3s",
|
|
9600
|
+
[
|
|
9601
|
+
"kubectl",
|
|
9602
|
+
"-n",
|
|
9603
|
+
PERSONAL_LOCAL_REGISTRY_NAMESPACE,
|
|
9604
|
+
"delete",
|
|
9605
|
+
"deployment,service,networkpolicy",
|
|
9606
|
+
PERSONAL_LOCAL_REGISTRY_NAME,
|
|
9607
|
+
"--ignore-not-found"
|
|
9608
|
+
],
|
|
9609
|
+
{ timeoutMs: 12e4 }
|
|
9610
|
+
);
|
|
9611
|
+
if (removed.exitCode !== 0) {
|
|
9612
|
+
ports.log(
|
|
9613
|
+
`Could not remove the local image registry from the cluster: ${removed.stderr.trim().slice(0, 200)}`
|
|
9614
|
+
);
|
|
9615
|
+
}
|
|
9616
|
+
const raw = await ports.fs.readFile(REGISTRIES_FILE);
|
|
9617
|
+
const parsed = parseRegistries(raw);
|
|
9618
|
+
if (mirrorEndpoints(parsed, PERSONAL_LOCAL_REGISTRY_HOST).length === 0) return;
|
|
9619
|
+
const backup = await writeRegistriesWithBackup(
|
|
9620
|
+
ports.fs,
|
|
9621
|
+
raw,
|
|
9622
|
+
removeRegistryMirror(parsed, PERSONAL_LOCAL_REGISTRY_HOST),
|
|
9623
|
+
(ports.now ?? (() => /* @__PURE__ */ new Date()))()
|
|
9624
|
+
);
|
|
9625
|
+
ports.log(
|
|
9626
|
+
`Removed the local image registry's mirror from ${REGISTRIES_FILE} (backup: ${backup}).`
|
|
9627
|
+
);
|
|
9628
|
+
}
|
|
9629
|
+
async function removeIsolation(ports) {
|
|
9630
|
+
const [key, value] = Object.entries(MANAGED_BY2)[0] ?? [];
|
|
9631
|
+
const removed = await ports.exec(
|
|
9632
|
+
"k3s",
|
|
9633
|
+
["kubectl", "delete", "networkpolicy", "-A", "-l", `${key}=${value}`, "--ignore-not-found"],
|
|
9634
|
+
{ timeoutMs: 12e4 }
|
|
9635
|
+
);
|
|
9636
|
+
if (removed.exitCode !== 0) {
|
|
9637
|
+
ports.log(
|
|
9638
|
+
`Could not remove Conveyor's network policies: ${removed.stderr.trim().slice(0, 200)}`
|
|
9639
|
+
);
|
|
9640
|
+
}
|
|
9641
|
+
const listed = await ports.exec("k3s", [
|
|
9642
|
+
"kubectl",
|
|
9643
|
+
"get",
|
|
9644
|
+
"namespaces",
|
|
9645
|
+
"-o",
|
|
9646
|
+
"jsonpath={.items[*].metadata.name}"
|
|
9647
|
+
]);
|
|
9648
|
+
if (listed.exitCode !== 0) {
|
|
9649
|
+
ports.log(
|
|
9650
|
+
`Could not list namespaces to remove Conveyor's Pod Security labels: ${listed.stderr.trim().slice(0, 200)}`
|
|
9651
|
+
);
|
|
9652
|
+
return;
|
|
9653
|
+
}
|
|
9654
|
+
const namespaces = listed.stdout.split(/\s+/).filter((name) => name.startsWith("claudespace-") || name === PERSONAL_BAKE_NAMESPACE);
|
|
9655
|
+
for (const namespace of namespaces) {
|
|
9656
|
+
const unlabelled = await ports.exec("k3s", [
|
|
9657
|
+
"kubectl",
|
|
9658
|
+
"label",
|
|
9659
|
+
"namespace",
|
|
9660
|
+
namespace,
|
|
9661
|
+
...PSA_LABEL_KEYS.map((labelKey) => `${labelKey}-`)
|
|
9662
|
+
]);
|
|
9663
|
+
if (unlabelled.exitCode !== 0) {
|
|
9664
|
+
ports.log(
|
|
9665
|
+
`Could not remove Conveyor's Pod Security labels from ${namespace}: ${unlabelled.stderr.trim().slice(0, 200)}`
|
|
9666
|
+
);
|
|
9667
|
+
}
|
|
9668
|
+
}
|
|
9669
|
+
}
|
|
7997
9670
|
async function uninstall(ports, options = {}) {
|
|
7998
9671
|
const result = { stoppedUnits: [], removedFiles: [], purgedK3s: false };
|
|
9672
|
+
if (options.purgeK3s && !options.confirmed) {
|
|
9673
|
+
throw new Error(
|
|
9674
|
+
"--purge-k3s removes k3s, everything running on it, and the local image registry's data. Re-run with --yes if that is what you want."
|
|
9675
|
+
);
|
|
9676
|
+
}
|
|
7999
9677
|
for (const unit of ALL_UNITS) {
|
|
8000
9678
|
await ports.exec("systemctl", ["disable", "--now", unit]);
|
|
8001
9679
|
result.stoppedUnits.push(unit);
|
|
8002
9680
|
}
|
|
9681
|
+
await removeLocalRegistry(ports);
|
|
9682
|
+
await removeIsolation(ports);
|
|
8003
9683
|
for (const path of OWNED_FILES) {
|
|
8004
9684
|
await ports.fs.removeFile(path);
|
|
8005
9685
|
result.removedFiles.push(path);
|
|
8006
9686
|
}
|
|
8007
|
-
await ports.exec("
|
|
9687
|
+
await ports.exec("find", [
|
|
9688
|
+
STATE_DIR,
|
|
9689
|
+
"-mindepth",
|
|
9690
|
+
"1",
|
|
9691
|
+
"-maxdepth",
|
|
9692
|
+
"1",
|
|
9693
|
+
"!",
|
|
9694
|
+
"-path",
|
|
9695
|
+
LOCAL_REGISTRY_DATA_DIR,
|
|
9696
|
+
"-exec",
|
|
9697
|
+
"rm",
|
|
9698
|
+
"-rf",
|
|
9699
|
+
"--",
|
|
9700
|
+
"{}",
|
|
9701
|
+
"+"
|
|
9702
|
+
]);
|
|
8008
9703
|
await ports.exec("systemctl", ["daemon-reload"]);
|
|
9704
|
+
await ports.exec("systemctl", ["restart", "systemd-journald"]).catch(() => void 0);
|
|
8009
9705
|
if (options.purgeK3s) {
|
|
8010
|
-
if (!options.confirmed) {
|
|
8011
|
-
throw new Error(
|
|
8012
|
-
"--purge-k3s removes k3s and everything running on it. Re-run with --yes if that is what you want."
|
|
8013
|
-
);
|
|
8014
|
-
}
|
|
8015
9706
|
const purge = await ports.exec("/usr/local/bin/k3s-uninstall.sh", [], {
|
|
8016
9707
|
timeoutMs: 10 * 6e4
|
|
8017
9708
|
});
|
|
@@ -8021,10 +9712,18 @@ async function uninstall(ports, options = {}) {
|
|
|
8021
9712
|
"k3s-uninstall.sh did not complete. k3s is still installed; remove it by hand if you want it gone."
|
|
8022
9713
|
);
|
|
8023
9714
|
}
|
|
9715
|
+
await ports.exec("rm", ["-rf", STATE_DIR]);
|
|
8024
9716
|
}
|
|
8025
|
-
ports.log("Removed Conveyor's units, environment files, and
|
|
8026
|
-
ports.log(
|
|
9717
|
+
ports.log("Removed Conveyor's units, environment files, state, and local image registry.");
|
|
9718
|
+
ports.log(
|
|
9719
|
+
"Your k3s config, your own registries.yaml entries, and your workloads were not touched."
|
|
9720
|
+
);
|
|
8027
9721
|
if (options.purgeK3s && result.purgedK3s) ports.log("k3s was removed.");
|
|
9722
|
+
if (!options.purgeK3s) {
|
|
9723
|
+
ports.log(
|
|
9724
|
+
`The local image registry's data is still at ${LOCAL_REGISTRY_DATA_DIR}. Delete that directory to reclaim its disk space.`
|
|
9725
|
+
);
|
|
9726
|
+
}
|
|
8028
9727
|
if (!options.serverInitiated) {
|
|
8029
9728
|
ports.log(
|
|
8030
9729
|
"Now click Disconnect in your Conveyor profile so the tunnel and DNS record are removed too."
|
|
@@ -8036,23 +9735,17 @@ async function uninstall(ports, options = {}) {
|
|
|
8036
9735
|
// src/commands/update.ts
|
|
8037
9736
|
var CLOUDFLARED_UPDATED_EXIT = 11;
|
|
8038
9737
|
function readExecStart(unit) {
|
|
8039
|
-
const
|
|
8040
|
-
const start =
|
|
9738
|
+
const lines2 = unit.split("\n");
|
|
9739
|
+
const start = lines2.findIndex((line) => line.trimStart().startsWith("ExecStart="));
|
|
8041
9740
|
if (start < 0) return "";
|
|
8042
9741
|
const collected = [];
|
|
8043
|
-
for (let index = start; index <
|
|
8044
|
-
const line =
|
|
9742
|
+
for (let index = start; index < lines2.length; index += 1) {
|
|
9743
|
+
const line = lines2[index] ?? "";
|
|
8045
9744
|
collected.push(line);
|
|
8046
9745
|
if (!line.trimEnd().endsWith("\\")) break;
|
|
8047
9746
|
}
|
|
8048
9747
|
return collected.join(" ");
|
|
8049
9748
|
}
|
|
8050
|
-
function bareToken(token) {
|
|
8051
|
-
let bare = token.trim();
|
|
8052
|
-
if (bare.endsWith("\\")) bare = bare.slice(0, -1).trim();
|
|
8053
|
-
if (bare.length >= 2 && bare.startsWith("'") && bare.endsWith("'")) bare = bare.slice(1, -1);
|
|
8054
|
-
return bare;
|
|
8055
|
-
}
|
|
8056
9749
|
function parseForeignExecFlags(execStart) {
|
|
8057
9750
|
const ours = new Set(K3S_INSTALL_EXEC.split(/\s+/).filter((token) => token.startsWith("--")));
|
|
8058
9751
|
const foreign = [];
|
|
@@ -8072,6 +9765,12 @@ async function foreignK3sExecFlags(exec) {
|
|
|
8072
9765
|
return parseForeignExecFlags(readExecStart(unit.stdout));
|
|
8073
9766
|
}
|
|
8074
9767
|
function decideK3sUpgrade(input) {
|
|
9768
|
+
if (input.managesK3s === false) {
|
|
9769
|
+
return {
|
|
9770
|
+
upgrade: false,
|
|
9771
|
+
reason: "Conveyor did not install this server's k3s, so its owner upgrades it"
|
|
9772
|
+
};
|
|
9773
|
+
}
|
|
8075
9774
|
if (!input.autoUpgradeK3s) {
|
|
8076
9775
|
return { upgrade: false, reason: "automatic k3s upgrades are turned off for this machine" };
|
|
8077
9776
|
}
|
|
@@ -8183,12 +9882,75 @@ function meetsK3sMinimum(version) {
|
|
|
8183
9882
|
// src/vm/network.ts
|
|
8184
9883
|
import { networkInterfaces } from "os";
|
|
8185
9884
|
import { resolve4 as systemResolve4, resolveSrv as systemResolveSrv } from "dns/promises";
|
|
9885
|
+
|
|
9886
|
+
// src/vm/endpoints.ts
|
|
9887
|
+
var CONTROL_PLANE_HOSTS = ["api.conveyor.rallycryapp.com", "conveyor-api.rallycryapp.com"];
|
|
9888
|
+
var DEFAULT_ENDPOINTS = [
|
|
9889
|
+
"archive.ubuntu.com",
|
|
9890
|
+
"security.ubuntu.com",
|
|
9891
|
+
"ports.ubuntu.com",
|
|
9892
|
+
"registry.npmjs.org",
|
|
9893
|
+
// Font Awesome Pro tarballs through Conveyor's Artifact Registry cache.
|
|
9894
|
+
"us-central1-npm.pkg.dev",
|
|
9895
|
+
// Tiptap Pro packages, which every claudespace bake installs.
|
|
9896
|
+
"registry.tiptap.dev",
|
|
9897
|
+
// The Python venv a pod builds at runtime.
|
|
9898
|
+
"pypi.org",
|
|
9899
|
+
"files.pythonhosted.org",
|
|
9900
|
+
"bun.sh",
|
|
9901
|
+
"get.k3s.io",
|
|
9902
|
+
"update.k3s.io",
|
|
9903
|
+
"github.com",
|
|
9904
|
+
"api.github.com",
|
|
9905
|
+
"raw.githubusercontent.com",
|
|
9906
|
+
"release-assets.githubusercontent.com",
|
|
9907
|
+
"objects.githubusercontent.com",
|
|
9908
|
+
"ghcr.io",
|
|
9909
|
+
"pkg-containers.githubusercontent.com",
|
|
9910
|
+
// The crane image that shadow bakes' state-bake and publish Jobs run. gcr.io
|
|
9911
|
+
// serves its blobs from the same host (/artifacts-downloads), on Google's
|
|
9912
|
+
// shared frontend addresses like us-central1-npm.pkg.dev.
|
|
9913
|
+
"gcr.io",
|
|
9914
|
+
// The first shadow bake on a VM builds the toolchain base inside it; base
|
|
9915
|
+
// reuse never crosses machines. These are the hosts that build downloads
|
|
9916
|
+
// from. Several sit on shared CDN addresses.
|
|
9917
|
+
// Plain HTTP too: the first apt-get runs before ca-certificates exists.
|
|
9918
|
+
"deb.debian.org",
|
|
9919
|
+
"www.postgresql.org",
|
|
9920
|
+
"apt.postgresql.org",
|
|
9921
|
+
"cli.github.com",
|
|
9922
|
+
"johnvansickle.com",
|
|
9923
|
+
"astral.sh",
|
|
9924
|
+
// uv downloads CPython from here.
|
|
9925
|
+
"releases.astral.sh",
|
|
9926
|
+
"opencode.ai",
|
|
9927
|
+
"cdn.playwright.dev",
|
|
9928
|
+
"playwright.download.prss.microsoft.com",
|
|
9929
|
+
"registry-1.docker.io",
|
|
9930
|
+
"auth.docker.io",
|
|
9931
|
+
"production.cloudflare.docker.com",
|
|
9932
|
+
"production.cloudfront.docker.com",
|
|
9933
|
+
"api.cloudflare.com",
|
|
9934
|
+
"conveyor.rallycryapp.com",
|
|
9935
|
+
"conveyor-api.rallycryapp.com",
|
|
9936
|
+
"api.conveyor.rallycryapp.com",
|
|
9937
|
+
"api.anthropic.com",
|
|
9938
|
+
"api.openai.com",
|
|
9939
|
+
"chatgpt.com"
|
|
9940
|
+
].map((host) => ({
|
|
9941
|
+
host,
|
|
9942
|
+
ports: host.endsWith(".ubuntu.com") || host === "deb.debian.org" ? [80, 443] : [443]
|
|
9943
|
+
}));
|
|
9944
|
+
|
|
9945
|
+
// src/vm/network.ts
|
|
8186
9946
|
var WORKER_SUBNET = "192.168.5.0/24";
|
|
8187
9947
|
var WORKER_IP = "192.168.5.15";
|
|
8188
9948
|
var MAX_NETWORK_ENDPOINTS = 100;
|
|
8189
9949
|
var MAX_PORTS_PER_ENDPOINT = 16;
|
|
8190
9950
|
var MAX_NETWORK_FORWARDS = 100;
|
|
8191
9951
|
var MAX_CLOUDFLARE_EDGE_ADDRESSES = 4;
|
|
9952
|
+
var MAX_CONTROL_ALIASES = 8;
|
|
9953
|
+
var ARGOTUNNEL = /^(?:us-)?region[12]\.v2\.argotunnel\.com$/;
|
|
8192
9954
|
var DOMAIN = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/i;
|
|
8193
9955
|
function parseIPv4(address) {
|
|
8194
9956
|
const parts = address.split(".");
|
|
@@ -8255,6 +10017,36 @@ function validatePorts(ports, host) {
|
|
|
8255
10017
|
function syntheticIp(index) {
|
|
8256
10018
|
return `192.168.5.${100 + index}`;
|
|
8257
10019
|
}
|
|
10020
|
+
function pinAddresses(name, answers, localAddresses) {
|
|
10021
|
+
if (!Array.isArray(answers) || answers.length === 0) {
|
|
10022
|
+
throw new Error(`No IPv4 address resolved for ${name}`);
|
|
10023
|
+
}
|
|
10024
|
+
const uniqueAnswers = [...new Set(answers)];
|
|
10025
|
+
const unsafe = uniqueAnswers.find(
|
|
10026
|
+
(address) => !isGloballyRoutableIPv4(address) || localAddresses.has(address)
|
|
10027
|
+
);
|
|
10028
|
+
if (unsafe) {
|
|
10029
|
+
throw new Error(`Refusing unsafe IPv4 answer for ${name}: ${unsafe}`);
|
|
10030
|
+
}
|
|
10031
|
+
return ARGOTUNNEL.test(name) ? uniqueAnswers.slice(0, MAX_CLOUDFLARE_EDGE_ADDRESSES) : uniqueAnswers.slice(0, 1);
|
|
10032
|
+
}
|
|
10033
|
+
function forwardCount(entries) {
|
|
10034
|
+
return entries.reduce((sum, entry) => sum + entry.ports.length, 0);
|
|
10035
|
+
}
|
|
10036
|
+
function controlAliases(hosts) {
|
|
10037
|
+
return CONTROL_PLANE_HOSTS.slice(0, MAX_CONTROL_ALIASES).flatMap((name, index) => {
|
|
10038
|
+
const host = hosts.find((entry) => entry.name === name && entry.ports.includes(443));
|
|
10039
|
+
return host ? [
|
|
10040
|
+
{
|
|
10041
|
+
name,
|
|
10042
|
+
guestIp: `192.168.5.${240 + index}`,
|
|
10043
|
+
address: host.address,
|
|
10044
|
+
ports: [443],
|
|
10045
|
+
reserve: "control"
|
|
10046
|
+
}
|
|
10047
|
+
] : [];
|
|
10048
|
+
});
|
|
10049
|
+
}
|
|
8258
10050
|
async function resolveNetwork(endpoints, lookup = systemResolve4) {
|
|
8259
10051
|
if (!Array.isArray(endpoints) || endpoints.length > MAX_NETWORK_ENDPOINTS) {
|
|
8260
10052
|
throw new Error(`Too many network endpoints (maximum ${MAX_NETWORK_ENDPOINTS})`);
|
|
@@ -8278,28 +10070,27 @@ async function resolveNetwork(endpoints, lookup = systemResolve4) {
|
|
|
8278
10070
|
const localAddresses = localIPv4Addresses();
|
|
8279
10071
|
const hosts = [];
|
|
8280
10072
|
for (const [name, ports] of merged) {
|
|
8281
|
-
const
|
|
8282
|
-
|
|
8283
|
-
|
|
8284
|
-
|
|
8285
|
-
|
|
8286
|
-
|
|
8287
|
-
|
|
8288
|
-
|
|
8289
|
-
|
|
8290
|
-
throw new Error(`Refusing unsafe IPv4 answer for ${name}: ${unsafe}`);
|
|
8291
|
-
}
|
|
8292
|
-
const addresses = /^(?:us-)?region[12]\.v2\.argotunnel\.com$/.test(name) ? uniqueAnswers.slice(0, MAX_CLOUDFLARE_EDGE_ADDRESSES) : uniqueAnswers.slice(0, 1);
|
|
8293
|
-
for (const address of addresses) {
|
|
8294
|
-
hosts.push({ name, guestIp: syntheticIp(hosts.length), address, ports });
|
|
10073
|
+
const tunnel = ARGOTUNNEL.test(name);
|
|
10074
|
+
for (const address of pinAddresses(name, await lookup(name), localAddresses)) {
|
|
10075
|
+
hosts.push({
|
|
10076
|
+
name,
|
|
10077
|
+
guestIp: syntheticIp(hosts.length),
|
|
10078
|
+
address,
|
|
10079
|
+
ports,
|
|
10080
|
+
...tunnel ? { reserve: "control" } : {}
|
|
10081
|
+
});
|
|
8295
10082
|
}
|
|
8296
10083
|
}
|
|
8297
|
-
|
|
10084
|
+
const control = controlAliases(hosts);
|
|
10085
|
+
if (forwardCount(hosts) + forwardCount(control) > MAX_NETWORK_FORWARDS) {
|
|
10086
|
+
throw new Error(`Too many network forwarding rules (maximum ${MAX_NETWORK_FORWARDS})`);
|
|
10087
|
+
}
|
|
10088
|
+
return { hosts, srv: cloudflaredSrvRecords(hosts), ...control.length > 0 ? { control } : {} };
|
|
8298
10089
|
}
|
|
8299
10090
|
function cloudflaredSrvRecords(hosts) {
|
|
8300
10091
|
const targets = [
|
|
8301
10092
|
...new Map(
|
|
8302
|
-
hosts.filter((host) =>
|
|
10093
|
+
hosts.filter((host) => ARGOTUNNEL.test(host.name)).map((host) => [host.name, host])
|
|
8303
10094
|
).values()
|
|
8304
10095
|
];
|
|
8305
10096
|
if (targets.length === 0) return [];
|
|
@@ -8508,6 +10299,28 @@ while True:
|
|
|
8508
10299
|
udp.sendto(response, peer)
|
|
8509
10300
|
`;
|
|
8510
10301
|
|
|
10302
|
+
// src/vm/instance-name.ts
|
|
10303
|
+
var VM_NAME = "conveyor";
|
|
10304
|
+
var DEFAULT_INSTANCE = "default";
|
|
10305
|
+
var NAMED_INSTANCE = /^(?=.{1,12}$)[a-z](?:-?[a-z0-9])*$/;
|
|
10306
|
+
function isNamedInstance(value) {
|
|
10307
|
+
return typeof value === "string" && NAMED_INSTANCE.test(value) && value !== DEFAULT_INSTANCE && value !== VM_NAME;
|
|
10308
|
+
}
|
|
10309
|
+
function parseInstanceName(value) {
|
|
10310
|
+
if (value === void 0 || value === DEFAULT_INSTANCE) return DEFAULT_INSTANCE;
|
|
10311
|
+
if (value === true) throw new Error("Use --instance=<name>.");
|
|
10312
|
+
if (value === VM_NAME)
|
|
10313
|
+
throw new Error(`"${VM_NAME}" is reserved for the default VM. Omit --instance to use it.`);
|
|
10314
|
+
if (!isNamedInstance(value))
|
|
10315
|
+
throw new Error(
|
|
10316
|
+
"Use an instance name of 1-12 lowercase letters, digits, or single hyphens, starting with a letter."
|
|
10317
|
+
);
|
|
10318
|
+
return value;
|
|
10319
|
+
}
|
|
10320
|
+
function vmNameFor(instance) {
|
|
10321
|
+
return instance === DEFAULT_INSTANCE ? VM_NAME : instance;
|
|
10322
|
+
}
|
|
10323
|
+
|
|
8511
10324
|
// src/vm/public-web.ts
|
|
8512
10325
|
import { resolve4 } from "dns/promises";
|
|
8513
10326
|
import { networkInterfaces as networkInterfaces2 } from "os";
|
|
@@ -8515,6 +10328,65 @@ var PUBLIC_WEB_IP = "192.168.5.250";
|
|
|
8515
10328
|
var PUBLIC_WEB_PORT = 3128;
|
|
8516
10329
|
var PUBLIC_WEB_URL = `http://${PUBLIC_WEB_IP}:${PUBLIC_WEB_PORT}`;
|
|
8517
10330
|
|
|
10331
|
+
// src/vm/qemu-forwards.ts
|
|
10332
|
+
var ENDPOINT_IP = /^192\.168\.5\.(?:1\d\d)$/;
|
|
10333
|
+
var CONTROL_ALIAS_IP2 = /^192\.168\.5\.24[0-7]$/;
|
|
10334
|
+
var PINNED_IPV4 = /^(?:[1-9]?\d|1\d\d|2[0-4]\d|25[0-5])(?:\.(?:[1-9]?\d|1\d\d|2[0-4]\d|25[0-5])){3}$/;
|
|
10335
|
+
function assertPort(port) {
|
|
10336
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
10337
|
+
throw new Error("Invalid guest forwarding port");
|
|
10338
|
+
}
|
|
10339
|
+
function validateForwards(forwards) {
|
|
10340
|
+
const seen = /* @__PURE__ */ new Set();
|
|
10341
|
+
for (const forward of forwards) {
|
|
10342
|
+
assertPort(forward.port);
|
|
10343
|
+
const key = `${forward.guestIp}:${forward.port}`;
|
|
10344
|
+
if (seen.has(key)) throw new Error("Duplicate QEMU guest forwarding rule");
|
|
10345
|
+
seen.add(key);
|
|
10346
|
+
if (!isGloballyRoutableIPv4(forward.address) || !PINNED_IPV4.test(forward.address))
|
|
10347
|
+
throw new Error("Invalid pinned forwarding address");
|
|
10348
|
+
}
|
|
10349
|
+
}
|
|
10350
|
+
function forwardsOf(entries, guestIps, control) {
|
|
10351
|
+
return entries.flatMap((host) => {
|
|
10352
|
+
if (!guestIps.test(host.guestIp) || control && host.reserve !== "control")
|
|
10353
|
+
throw new Error("Invalid pinned forwarding address");
|
|
10354
|
+
return host.ports.map((port) => ({
|
|
10355
|
+
guestIp: host.guestIp,
|
|
10356
|
+
address: host.address,
|
|
10357
|
+
port,
|
|
10358
|
+
...host.reserve === "control" ? { reserve: "control" } : {}
|
|
10359
|
+
}));
|
|
10360
|
+
});
|
|
10361
|
+
}
|
|
10362
|
+
function qemuForwards(network) {
|
|
10363
|
+
const forwards = [
|
|
10364
|
+
...forwardsOf(network.hosts, ENDPOINT_IP, false),
|
|
10365
|
+
...forwardsOf(network.control ?? [], CONTROL_ALIAS_IP2, true)
|
|
10366
|
+
];
|
|
10367
|
+
if (forwards.length > MAX_NETWORK_FORWARDS) throw new Error("Too many QEMU forwarding rules");
|
|
10368
|
+
validateForwards(forwards);
|
|
10369
|
+
return forwards;
|
|
10370
|
+
}
|
|
10371
|
+
function restrictedUsernet(network, forwards, ncPath) {
|
|
10372
|
+
const guestForwards = forwards.map(
|
|
10373
|
+
(forward) => `guestfwd=tcp:${forward.guestIp}:${forward.port}-cmd:${ncPath} -4 -n -w 60 ${forward.address} ${forward.port}${forward.reserve === "control" ? " control" : ""}`
|
|
10374
|
+
);
|
|
10375
|
+
return [
|
|
10376
|
+
"user",
|
|
10377
|
+
"id=net0",
|
|
10378
|
+
`net=${WORKER_SUBNET}`,
|
|
10379
|
+
`dhcpstart=${WORKER_IP}`,
|
|
10380
|
+
"restrict=on",
|
|
10381
|
+
"ipv6=off",
|
|
10382
|
+
`hostfwd=tcp:127.0.0.1:${network.sshPort}-:22`,
|
|
10383
|
+
...guestForwards,
|
|
10384
|
+
...network.publicWeb === true ? [
|
|
10385
|
+
`guestfwd=tcp:${PUBLIC_WEB_IP}:${PUBLIC_WEB_PORT}-cmd:${ncPath} -4 -n -w 60 public-web ${PUBLIC_WEB_PORT}`
|
|
10386
|
+
] : []
|
|
10387
|
+
].join(",");
|
|
10388
|
+
}
|
|
10389
|
+
|
|
8518
10390
|
// src/vm/qemu-policy.ts
|
|
8519
10391
|
var ALLOWED_PROBES = new Set(
|
|
8520
10392
|
[
|
|
@@ -8568,9 +10440,7 @@ var REQUIRED_CONTROLS = [
|
|
|
8568
10440
|
"drive:cidata"
|
|
8569
10441
|
];
|
|
8570
10442
|
var DISALLOWED_CONTROLS = /* @__PURE__ */ new Set(["-sandbox", "-no-user-config", "-nodefaults"]);
|
|
8571
|
-
|
|
8572
|
-
var BOOT_DISK_IOPS = "2000";
|
|
8573
|
-
function assertPort(port, field) {
|
|
10443
|
+
function assertPort2(port, field) {
|
|
8574
10444
|
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`Invalid ${field}`);
|
|
8575
10445
|
}
|
|
8576
10446
|
function isProbe(args) {
|
|
@@ -8691,10 +10561,11 @@ function validateDrive(value, context) {
|
|
|
8691
10561
|
}
|
|
8692
10562
|
assertFields(fields, { file: /.+/, if: "none", discard: "on", id: "boot-disk" }, "boot drive");
|
|
8693
10563
|
assertInstancePath(path, "disk", context);
|
|
8694
|
-
|
|
8695
|
-
|
|
8696
|
-
|
|
10564
|
+
const disk = context.disk ?? {
|
|
10565
|
+
bps: LEGACY_CAPACITY_LIMITS.diskBytesPerSecond,
|
|
10566
|
+
iops: LEGACY_CAPACITY_LIMITS.diskIops
|
|
8697
10567
|
};
|
|
10568
|
+
return { kind: "boot", value: `${value},bps=${disk.bps},iops=${disk.iops}` };
|
|
8698
10569
|
}
|
|
8699
10570
|
if (fields.get("id") === "cdrom0") {
|
|
8700
10571
|
assertFields(
|
|
@@ -8739,38 +10610,6 @@ function parseOriginalUsernet(value, sshPort) {
|
|
|
8739
10610
|
"Lima QEMU user network"
|
|
8740
10611
|
);
|
|
8741
10612
|
}
|
|
8742
|
-
function restrictedUsernet(network, forwards, ncPath) {
|
|
8743
|
-
const guestForwards = forwards.map(
|
|
8744
|
-
(forward) => `guestfwd=tcp:${forward.guestIp}:${forward.port}-cmd:${ncPath} -4 -n -w 60 ${forward.address} ${forward.port}`
|
|
8745
|
-
);
|
|
8746
|
-
return [
|
|
8747
|
-
"user",
|
|
8748
|
-
"id=net0",
|
|
8749
|
-
`net=${WORKER_SUBNET}`,
|
|
8750
|
-
`dhcpstart=${WORKER_IP}`,
|
|
8751
|
-
"restrict=on",
|
|
8752
|
-
"ipv6=off",
|
|
8753
|
-
`hostfwd=tcp:127.0.0.1:${network.sshPort}-:22`,
|
|
8754
|
-
...guestForwards,
|
|
8755
|
-
...network.publicWeb === true ? [
|
|
8756
|
-
`guestfwd=tcp:${PUBLIC_WEB_IP}:${PUBLIC_WEB_PORT}-cmd:${ncPath} -4 -n -w 60 public-web ${PUBLIC_WEB_PORT}`
|
|
8757
|
-
] : []
|
|
8758
|
-
].join(",");
|
|
8759
|
-
}
|
|
8760
|
-
function validateForwards(forwards) {
|
|
8761
|
-
const seen = /* @__PURE__ */ new Set();
|
|
8762
|
-
for (const forward of forwards) {
|
|
8763
|
-
assertPort(forward.port, "guest forwarding port");
|
|
8764
|
-
const key = `${forward.guestIp}:${forward.port}`;
|
|
8765
|
-
if (seen.has(key)) throw new Error("Duplicate QEMU guest forwarding rule");
|
|
8766
|
-
seen.add(key);
|
|
8767
|
-
if (!/^192\.168\.5\.(?:1\d\d)$/.test(forward.guestIp) || !isGloballyRoutableIPv4(forward.address) || !/^(?:[1-9]?\d|1\d\d|2[0-4]\d|25[0-5])(?:\.(?:[1-9]?\d|1\d\d|2[0-4]\d|25[0-5])){3}$/.test(
|
|
8768
|
-
forward.address
|
|
8769
|
-
)) {
|
|
8770
|
-
throw new Error("Invalid pinned forwarding address");
|
|
8771
|
-
}
|
|
8772
|
-
}
|
|
8773
|
-
}
|
|
8774
10613
|
function fixedControl(control, expected) {
|
|
8775
10614
|
return (value) => {
|
|
8776
10615
|
if (value !== expected) throw new Error(`Unexpected QEMU ${control}`);
|
|
@@ -8803,7 +10642,7 @@ var SIMPLE_VALIDATORS = {
|
|
|
8803
10642
|
"-parallel": fixedControl("-parallel", "none"),
|
|
8804
10643
|
"-serial": fixedControl("serial", "chardev:char-serial"),
|
|
8805
10644
|
"-qmp": fixedControl("qmp", "chardev:char-qmp"),
|
|
8806
|
-
"-name": fixedControl("name",
|
|
10645
|
+
"-name": (value, { vmName = VM_NAME }) => fixedControl("name", `lima-${vmName}`)(value, {}),
|
|
8807
10646
|
"-pidfile": (value, context) => {
|
|
8808
10647
|
assertInstancePath(value, "qemu.pid", context);
|
|
8809
10648
|
return { value, control: "pidfile" };
|
|
@@ -8874,15 +10713,16 @@ function assertCompleteProfile(state) {
|
|
|
8874
10713
|
throw new Error("QEMU PCI serial device and chardev must appear together");
|
|
8875
10714
|
}
|
|
8876
10715
|
}
|
|
8877
|
-
function rewriteQemuArgs(args, network, ncPath = "/usr/bin/nc",
|
|
10716
|
+
function rewriteQemuArgs(args, network, ncPath = "/usr/bin/nc", hostContext = {}) {
|
|
8878
10717
|
if (isProbe(args)) return [...args];
|
|
8879
|
-
|
|
10718
|
+
assertPort2(network.sshPort, "SSH port");
|
|
8880
10719
|
if (!TRUSTED_NC_PATH.test(ncPath)) throw new Error("Invalid netcat path");
|
|
8881
|
-
const forwards = network
|
|
8882
|
-
|
|
8883
|
-
|
|
8884
|
-
|
|
8885
|
-
|
|
10720
|
+
const forwards = qemuForwards(network);
|
|
10721
|
+
const limits = parseCapacityLimits(network.limits);
|
|
10722
|
+
const context = {
|
|
10723
|
+
...hostContext,
|
|
10724
|
+
disk: { bps: limits.diskBytesPerSecond, iops: limits.diskIops }
|
|
10725
|
+
};
|
|
8886
10726
|
const output = [];
|
|
8887
10727
|
const state = {
|
|
8888
10728
|
controls: /* @__PURE__ */ new Set(),
|
|
@@ -8922,15 +10762,79 @@ function rewriteQemuArgs(args, network, ncPath = "/usr/bin/nc", context = {}) {
|
|
|
8922
10762
|
}
|
|
8923
10763
|
|
|
8924
10764
|
// src/vm/memory-budget.ts
|
|
8925
|
-
|
|
10765
|
+
var RELAY_RSS_ALLOWANCE_MIB = 128;
|
|
10766
|
+
var BASE_RELAY_SLOTS = 16;
|
|
10767
|
+
function relayAllowanceGiB(relaySlots = BASE_RELAY_SLOTS) {
|
|
10768
|
+
return Math.max(0, relaySlots - BASE_RELAY_SLOTS) * RELAY_RSS_ALLOWANCE_MIB / 1024;
|
|
10769
|
+
}
|
|
10770
|
+
function maximumGuestMemoryGiBWithin(budgetGiB, relaySlots = BASE_RELAY_SLOTS) {
|
|
10771
|
+
const guestBudget = budgetGiB - relayAllowanceGiB(relaySlots);
|
|
10772
|
+
if (!Number.isFinite(guestBudget) || guestBudget <= 0) return 0;
|
|
10773
|
+
return Math.max(0, Math.floor(Math.min(guestBudget - 1, guestBudget / 1.25)));
|
|
10774
|
+
}
|
|
10775
|
+
function maximumGuestMemoryGiB(hostBytes, relaySlots = BASE_RELAY_SLOTS) {
|
|
8926
10776
|
if (!Number.isFinite(hostBytes) || hostBytes <= 0) return 0;
|
|
8927
|
-
|
|
8928
|
-
|
|
10777
|
+
return maximumGuestMemoryGiBWithin(hostBytes / 1024 ** 3 * 0.75, relaySlots);
|
|
10778
|
+
}
|
|
10779
|
+
|
|
10780
|
+
// src/vm/host-state.ts
|
|
10781
|
+
import { access, chmod as chmod2, lstat, mkdir as mkdir2, readFile as readFile2, rename, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
10782
|
+
import { homedir, uptime } from "os";
|
|
10783
|
+
import { join } from "path";
|
|
10784
|
+
import { setTimeout as sleep } from "timers/promises";
|
|
10785
|
+
function vmProfile() {
|
|
10786
|
+
const profile = process.env.CONVEYOR_VM_TEST_PROFILE;
|
|
10787
|
+
if (profile !== void 0 && !/^acceptance-[a-z0-9-]{1,32}$/.test(profile)) {
|
|
10788
|
+
throw new Error("Test VM profiles must match acceptance-[a-z0-9-]{1,32}.");
|
|
10789
|
+
}
|
|
10790
|
+
return profile;
|
|
10791
|
+
}
|
|
10792
|
+
function instancesBase() {
|
|
10793
|
+
const profile = vmProfile();
|
|
10794
|
+
return join(
|
|
10795
|
+
homedir(),
|
|
10796
|
+
".local",
|
|
10797
|
+
"share",
|
|
10798
|
+
profile ? `conveyor-k3-${profile}-vms` : "conveyor-k3-vms"
|
|
10799
|
+
);
|
|
10800
|
+
}
|
|
10801
|
+
function hostPaths(instance = DEFAULT_INSTANCE) {
|
|
10802
|
+
const name = parseInstanceName(instance);
|
|
10803
|
+
if (name !== DEFAULT_INSTANCE) return pathsForRoot(join(instancesBase(), name), name);
|
|
10804
|
+
const profile = vmProfile();
|
|
10805
|
+
return pathsForRoot(
|
|
10806
|
+
join(homedir(), ".local", "share", profile ? `conveyor-k3-${profile}` : "conveyor-k3-vm")
|
|
10807
|
+
);
|
|
10808
|
+
}
|
|
10809
|
+
function pathsForRoot(root, instance = DEFAULT_INSTANCE) {
|
|
10810
|
+
const vmName = vmNameFor(parseInstanceName(instance));
|
|
10811
|
+
const instanceDir = join(root, "lima", vmName);
|
|
10812
|
+
return {
|
|
10813
|
+
root,
|
|
10814
|
+
limaHome: join(root, "lima"),
|
|
10815
|
+
state: join(root, "state.json"),
|
|
10816
|
+
network: join(root, "network.json"),
|
|
10817
|
+
template: join(root, "conveyor.yaml"),
|
|
10818
|
+
wrapper: join(root, "qemu-wrapper"),
|
|
10819
|
+
wrapperJs: join(root, "vm-qemu.js"),
|
|
10820
|
+
applied: join(root, "applied-network.json"),
|
|
10821
|
+
guard: join(root, "guard-state.json"),
|
|
10822
|
+
startRequest: join(root, "start-request.json"),
|
|
10823
|
+
policyTransaction: join(root, "policy-transaction.json"),
|
|
10824
|
+
lock: join(root, "operation.lock"),
|
|
10825
|
+
/** Written by an owner stop, so a supervising service does not restart the VM. */
|
|
10826
|
+
stopIntent: join(root, "stop-intent.json"),
|
|
10827
|
+
instance,
|
|
10828
|
+
vmName,
|
|
10829
|
+
instanceDir,
|
|
10830
|
+
limaConfig: join(instanceDir, "lima.yaml")
|
|
10831
|
+
};
|
|
8929
10832
|
}
|
|
8930
10833
|
export {
|
|
8931
10834
|
ClusterConnection,
|
|
8932
10835
|
ClusterDaemon,
|
|
8933
10836
|
CommandError,
|
|
10837
|
+
DEFAULT_INSTANCE,
|
|
8934
10838
|
HELP,
|
|
8935
10839
|
K3S_DISABLED_COMPONENTS,
|
|
8936
10840
|
K3S_INSTALL_EXEC,
|
|
@@ -8941,15 +10845,18 @@ export {
|
|
|
8941
10845
|
countRunningConveyorPods,
|
|
8942
10846
|
createMemoryFileSystem,
|
|
8943
10847
|
decideK3sUpgrade,
|
|
10848
|
+
hostPaths,
|
|
8944
10849
|
isGloballyRoutableIPv4,
|
|
8945
10850
|
isVerb,
|
|
8946
10851
|
maximumGuestMemoryGiB,
|
|
10852
|
+
maximumGuestMemoryGiBWithin,
|
|
8947
10853
|
meetsK3sMinimum,
|
|
8948
10854
|
missingAuthHosts,
|
|
8949
10855
|
nodeFileSystem,
|
|
8950
10856
|
ownerConfiguredKeys,
|
|
8951
10857
|
ownerConfiguredKubeletFlags,
|
|
8952
10858
|
parseArgs,
|
|
10859
|
+
parseInstanceName,
|
|
8953
10860
|
parseK3sVersion,
|
|
8954
10861
|
parseRegistries,
|
|
8955
10862
|
pruneStrandedImages,
|