@rallycry/conveyor-k3 0.1.10 → 0.1.12
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 +12935 -8559
- package/dist/index.d.ts +267 -85
- package/dist/index.js +1853 -364
- package/dist/vm-qemu.js +408 -89
- 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")
|
|
@@ -4109,12 +4162,12 @@ var coerce = {
|
|
|
4109
4162
|
};
|
|
4110
4163
|
var NEVER = INVALID;
|
|
4111
4164
|
|
|
4112
|
-
// ../shared/dist/chunk-
|
|
4113
|
-
var DEFAULT_SONNET_MODEL = "claude-sonnet-5";
|
|
4165
|
+
// ../shared/dist/chunk-BH4SLNGU.js
|
|
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";
|
|
4116
4169
|
var FABLE_MODEL = "claude-fable-5-1";
|
|
4117
|
-
var PREVIOUS_SONNET_MODEL = "claude-sonnet-
|
|
4170
|
+
var PREVIOUS_SONNET_MODEL = "claude-sonnet-5";
|
|
4118
4171
|
var PREVIOUS_OPUS_MODEL = "claude-opus-5";
|
|
4119
4172
|
function formatModelId(provider, model) {
|
|
4120
4173
|
return `${provider}/${model}`;
|
|
@@ -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(),
|
|
@@ -5183,6 +5245,9 @@ var CreateProjectTaskRequestSchema = external_exports.object({
|
|
|
5183
5245
|
status: external_exports.string().optional(),
|
|
5184
5246
|
// Assign to a sub-project board. Validated to belong to `projectId` in the handler.
|
|
5185
5247
|
subProjectId: external_exports.string().nullable().optional(),
|
|
5248
|
+
// Priority value. Resolved to the project's configured Priority row in the
|
|
5249
|
+
// handler, which rejects an unconfigured value before the card is written.
|
|
5250
|
+
priorityValue: external_exports.number().int().positive().optional(),
|
|
5186
5251
|
requestingUserId: external_exports.string().optional()
|
|
5187
5252
|
});
|
|
5188
5253
|
var SetProjectTaskParentRequestSchema = external_exports.object({
|
|
@@ -5212,6 +5277,9 @@ var UpdateProjectTaskRequestSchema = external_exports.object({
|
|
|
5212
5277
|
// Story-point value, or null to clear. Resolved to the project's configured
|
|
5213
5278
|
// StoryPoint row in the handler, which rejects an unconfigured value.
|
|
5214
5279
|
storyPointValue: external_exports.number().int().positive().nullable().optional(),
|
|
5280
|
+
// Priority value, or null to clear. Resolved to the project's configured
|
|
5281
|
+
// Priority row in the handler, which rejects an unconfigured value.
|
|
5282
|
+
priorityValue: external_exports.number().int().positive().nullable().optional(),
|
|
5215
5283
|
assignedUserId: external_exports.string().nullish(),
|
|
5216
5284
|
// Move to a different sub-project board, or null to move to the parent board.
|
|
5217
5285
|
// Validated to belong to `projectId` in the handler.
|
|
@@ -5228,9 +5296,9 @@ var UpdateProjectTaskRequestSchema = external_exports.object({
|
|
|
5228
5296
|
onHold: external_exports.boolean().optional(),
|
|
5229
5297
|
requestingUserId: external_exports.string().optional()
|
|
5230
5298
|
}).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,
|
|
5299
|
+
(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
5300
|
{
|
|
5233
|
-
message: "update_task requires at least one field to change (title, description, plan, status, risk, storyPointValue, assignedUserId, subProjectId, githubBranch, or onHold)"
|
|
5301
|
+
message: "update_task requires at least one field to change (title, description, plan, status, risk, storyPointValue, priorityValue, assignedUserId, subProjectId, githubBranch, or onHold)"
|
|
5234
5302
|
}
|
|
5235
5303
|
);
|
|
5236
5304
|
var TransitionProjectTaskStatusRequestSchema = external_exports.object({
|
|
@@ -5478,43 +5546,6 @@ var GetProjectTaskDependenciesRequestSchema = external_exports.object({
|
|
|
5478
5546
|
projectId: external_exports.string(),
|
|
5479
5547
|
taskId: external_exports.string()
|
|
5480
5548
|
});
|
|
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
5549
|
var CreateProjectPullRequestRequestSchema = external_exports.object({
|
|
5519
5550
|
projectId: external_exports.string(),
|
|
5520
5551
|
taskId: external_exports.string(),
|
|
@@ -5621,6 +5652,54 @@ var GetProjectAnalyticsSummaryRequestSchema = external_exports.object({
|
|
|
5621
5652
|
rangeDays: external_exports.number().int().min(1).max(GET_ANALYTICS_SUMMARY_MAX_RANGE_DAYS).optional(),
|
|
5622
5653
|
campaign: external_exports.string().max(200).optional()
|
|
5623
5654
|
});
|
|
5655
|
+
var ListProjectTaskFilesRequestSchema = external_exports.object({
|
|
5656
|
+
projectId: external_exports.string(),
|
|
5657
|
+
taskId: external_exports.string()
|
|
5658
|
+
});
|
|
5659
|
+
var GetProjectAttachmentRequestSchema = external_exports.object({
|
|
5660
|
+
projectId: external_exports.string(),
|
|
5661
|
+
taskId: external_exports.string(),
|
|
5662
|
+
fileId: external_exports.string(),
|
|
5663
|
+
/** Byte offset into text content (paging large logs/JSON). Default 0. */
|
|
5664
|
+
offset: external_exports.number().int().nonnegative().optional(),
|
|
5665
|
+
/** Max bytes of text content to return from `offset`. Server default applies. */
|
|
5666
|
+
maxBytes: external_exports.number().int().positive().optional()
|
|
5667
|
+
});
|
|
5668
|
+
var DeleteProjectTaskFileRequestSchema = external_exports.object({
|
|
5669
|
+
projectId: external_exports.string(),
|
|
5670
|
+
taskId: external_exports.string(),
|
|
5671
|
+
fileId: external_exports.string(),
|
|
5672
|
+
requestingUserId: external_exports.string().optional()
|
|
5673
|
+
});
|
|
5674
|
+
var SetProjectTaskFileShowcaseRequestSchema = external_exports.object({
|
|
5675
|
+
projectId: external_exports.string(),
|
|
5676
|
+
taskId: external_exports.string(),
|
|
5677
|
+
fileId: external_exports.string(),
|
|
5678
|
+
showcase: external_exports.boolean(),
|
|
5679
|
+
requestingUserId: external_exports.string().optional()
|
|
5680
|
+
});
|
|
5681
|
+
var RequestProjectFileUploadRequestSchema = external_exports.object({
|
|
5682
|
+
projectId: external_exports.string(),
|
|
5683
|
+
taskId: external_exports.string(),
|
|
5684
|
+
fileName: external_exports.string().min(1).max(255),
|
|
5685
|
+
mimeType: external_exports.string().min(1).max(128),
|
|
5686
|
+
fileSize: external_exports.number().int().positive().max(MAX_FILE_SIZE_BYTES),
|
|
5687
|
+
requestingUserId: external_exports.string().optional()
|
|
5688
|
+
});
|
|
5689
|
+
var ConfirmProjectFileUploadRequestSchema = external_exports.object({
|
|
5690
|
+
projectId: external_exports.string(),
|
|
5691
|
+
taskId: external_exports.string(),
|
|
5692
|
+
fileId: external_exports.string(),
|
|
5693
|
+
/** When set, the attachment is also posted to the task chat with this text. */
|
|
5694
|
+
comment: external_exports.string().max(2e3).optional(),
|
|
5695
|
+
/** Glossary tag names (or ids) this file is an example of. */
|
|
5696
|
+
tags: external_exports.array(external_exports.string().min(1).max(MAX_FILE_TAG_LENGTH)).max(MAX_FILE_TAGS).optional(),
|
|
5697
|
+
/** The card's showcase (its deliverable); always posts to chat, even with no `comment`. */
|
|
5698
|
+
showcase: external_exports.boolean().optional(),
|
|
5699
|
+
/** Id of an older file on the same card this upload replaces. */
|
|
5700
|
+
supersedes: external_exports.string().min(1).max(64).optional(),
|
|
5701
|
+
requestingUserId: external_exports.string().optional()
|
|
5702
|
+
});
|
|
5624
5703
|
var RequestWorkspaceRecycleRequestSchema = external_exports.object({
|
|
5625
5704
|
sessionId: external_exports.string(),
|
|
5626
5705
|
reason: external_exports.string().max(2e3)
|
|
@@ -6175,8 +6254,8 @@ function anthropicEntry(model, label, inputPerMillion, outputPerMillion, opts =
|
|
|
6175
6254
|
var ANTHROPIC_CATALOG = [
|
|
6176
6255
|
anthropicEntry(DEFAULT_OPUS_MODEL, "Opus 5.5 Latest", 4, 20),
|
|
6177
6256
|
anthropicEntry(PREVIOUS_OPUS_MODEL, "Opus 5", 5, 25),
|
|
6178
|
-
anthropicEntry(DEFAULT_SONNET_MODEL, "Sonnet 5 Latest",
|
|
6179
|
-
anthropicEntry(PREVIOUS_SONNET_MODEL, "Sonnet
|
|
6257
|
+
anthropicEntry(DEFAULT_SONNET_MODEL, "Sonnet 5.5 Latest", 2, 10),
|
|
6258
|
+
anthropicEntry(PREVIOUS_SONNET_MODEL, "Sonnet 5", 2, 10),
|
|
6180
6259
|
// The Haiku line (4.5 and older) predates the tuning surface and 400s on it.
|
|
6181
6260
|
anthropicEntry(DEFAULT_HAIKU_MODEL, "Haiku 4.5", 1, 5, { supportsEffort: false }),
|
|
6182
6261
|
anthropicEntry(FABLE_MODEL, "Fable 5.1", 10, 50)
|
|
@@ -6189,6 +6268,7 @@ function isArtifactRegistryHost(host) {
|
|
|
6189
6268
|
return bare.endsWith(".pkg.dev") || bare === "gcr.io" || bare.endsWith(".gcr.io");
|
|
6190
6269
|
}
|
|
6191
6270
|
var CONTAINERD_K8S_NAMESPACE = "k8s.io";
|
|
6271
|
+
var CONVEYOR_CONTENT_HASH_LABEL = "com.rallycry.conveyor.content-hash";
|
|
6192
6272
|
var LOCAL_BAKE_ROOT_COMMANDS = [
|
|
6193
6273
|
{
|
|
6194
6274
|
name: "import",
|
|
@@ -6201,6 +6281,15 @@ var LOCAL_BAKE_ROOT_COMMANDS = [
|
|
|
6201
6281
|
takesImageArgs: true
|
|
6202
6282
|
}
|
|
6203
6283
|
];
|
|
6284
|
+
var PERSONAL_LOCAL_REGISTRY_NAMESPACE = "conveyor-system";
|
|
6285
|
+
var PERSONAL_LOCAL_REGISTRY_NAME = "conveyor-registry";
|
|
6286
|
+
var PERSONAL_LOCAL_REGISTRY_PORT = 5e3;
|
|
6287
|
+
var PERSONAL_LOCAL_REGISTRY_HOST = `${PERSONAL_LOCAL_REGISTRY_NAME}.${PERSONAL_LOCAL_REGISTRY_NAMESPACE}.svc.cluster.local:${PERSONAL_LOCAL_REGISTRY_PORT}`;
|
|
6288
|
+
var PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT = `http://127.0.0.1:${PERSONAL_LOCAL_REGISTRY_PORT}`;
|
|
6289
|
+
var BAKE_COMPONENT_LABEL = "claudespace.rallycry.com/component";
|
|
6290
|
+
var BAKE_COMPONENT_PREBAKE = "prebake";
|
|
6291
|
+
var BAKE_COMPONENT_STATE_BAKE = "state-bake";
|
|
6292
|
+
var BAKE_COMPONENT_VALUES = [BAKE_COMPONENT_PREBAKE, BAKE_COMPONENT_STATE_BAKE];
|
|
6204
6293
|
var MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
6205
6294
|
var POSTGRES_ENV = {
|
|
6206
6295
|
POSTGRES_HOST_AUTH_METHOD: "trust",
|
|
@@ -6552,7 +6641,15 @@ var ClusterConnection = class {
|
|
|
6552
6641
|
reject(error instanceof Error ? error : new Error(String(error)));
|
|
6553
6642
|
return;
|
|
6554
6643
|
}
|
|
6555
|
-
const
|
|
6644
|
+
const route = controlPlaneRoute(
|
|
6645
|
+
this.config.apiUrl,
|
|
6646
|
+
this.config.readControlFile,
|
|
6647
|
+
this.config.controlFileExists
|
|
6648
|
+
);
|
|
6649
|
+
const socket = io(this.config.apiUrl, {
|
|
6650
|
+
...buildConveyorSocketOptions(auth),
|
|
6651
|
+
...route ? { transportOptions: { websocket: { agent: route.agent } } } : {}
|
|
6652
|
+
});
|
|
6556
6653
|
this.socket = socket;
|
|
6557
6654
|
socket.on("connect", () => {
|
|
6558
6655
|
if (!settled) {
|
|
@@ -6568,6 +6665,7 @@ var ClusterConnection = class {
|
|
|
6568
6665
|
settled = true;
|
|
6569
6666
|
reject(err);
|
|
6570
6667
|
}
|
|
6668
|
+
if (!isAuthFailure) route?.failed();
|
|
6571
6669
|
});
|
|
6572
6670
|
socket.on("disconnect", (reason) => {
|
|
6573
6671
|
process.stderr.write(`[conveyor-k3] disconnected: ${reason}
|
|
@@ -6611,9 +6709,16 @@ var ClusterConnection = class {
|
|
|
6611
6709
|
const ack = await this.require().timeout(3e4).emitWithAck(PERSONAL_CLUSTER_EVENTS.requestDesiredState, {});
|
|
6612
6710
|
return unwrap(ack, "Fetching settings");
|
|
6613
6711
|
}
|
|
6614
|
-
/**
|
|
6615
|
-
sendHeartbeat(payload) {
|
|
6616
|
-
this.socket?.
|
|
6712
|
+
/** Old servers still receive liveness even when they do not acknowledge inspection data. */
|
|
6713
|
+
async sendHeartbeat(payload) {
|
|
6714
|
+
if (!this.socket?.connected) return void 0;
|
|
6715
|
+
try {
|
|
6716
|
+
const ack = await this.socket.timeout(8e3).emitWithAck(PERSONAL_CLUSTER_EVENTS.heartbeat, payload);
|
|
6717
|
+
if (!this.socket?.connected) return void 0;
|
|
6718
|
+
return unwrap(ack, "Reading pod assignments");
|
|
6719
|
+
} catch {
|
|
6720
|
+
return void 0;
|
|
6721
|
+
}
|
|
6617
6722
|
}
|
|
6618
6723
|
/**
|
|
6619
6724
|
* Tell Conveyor this machine is stopping, so placement skips it at once
|
|
@@ -6932,31 +7037,6 @@ function countPinnedWanted(inventory, wantedImages) {
|
|
|
6932
7037
|
|
|
6933
7038
|
// src/lib/registries.ts
|
|
6934
7039
|
import { parse, stringify } from "yaml";
|
|
6935
|
-
function parseRegistries(raw) {
|
|
6936
|
-
if (!raw || raw.trim().length === 0) return {};
|
|
6937
|
-
const parsed = parse(raw);
|
|
6938
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
6939
|
-
return parsed;
|
|
6940
|
-
}
|
|
6941
|
-
function configuredAuthHosts(file) {
|
|
6942
|
-
return Object.entries(file.configs ?? {}).filter(([, entry]) => Boolean(entry?.auth)).map(([host]) => host).sort();
|
|
6943
|
-
}
|
|
6944
|
-
function missingAuthHosts(file, requiredHosts) {
|
|
6945
|
-
const configured = new Set(configuredAuthHosts(file));
|
|
6946
|
-
return requiredHosts.filter((host) => !configured.has(host)).sort();
|
|
6947
|
-
}
|
|
6948
|
-
function upsertRegistryAuth(file, host, auth) {
|
|
6949
|
-
assertRegistryHost(host);
|
|
6950
|
-
const next = { ...file };
|
|
6951
|
-
const configs = { ...next.configs ?? {} };
|
|
6952
|
-
const existing = configs[host] ?? {};
|
|
6953
|
-
configs[host] = { ...existing, auth };
|
|
6954
|
-
next.configs = configs;
|
|
6955
|
-
return next;
|
|
6956
|
-
}
|
|
6957
|
-
function serializeRegistries(file) {
|
|
6958
|
-
return stringify(file);
|
|
6959
|
-
}
|
|
6960
7040
|
|
|
6961
7041
|
// src/lib/paths.ts
|
|
6962
7042
|
var ETC_CONVEYOR = "/etc/conveyor";
|
|
@@ -6964,11 +7044,16 @@ var ENV_FILE = `${ETC_CONVEYOR}/k3.env`;
|
|
|
6964
7044
|
var TUNNEL_ENV_FILE = `${ETC_CONVEYOR}/k3-tunnel.env`;
|
|
6965
7045
|
var STATE_DIR = "/var/lib/conveyor-k3";
|
|
6966
7046
|
var STATE_FILE = `${STATE_DIR}/state.json`;
|
|
7047
|
+
var LOCAL_REGISTRY_DATA_DIR = `${STATE_DIR}/registry`;
|
|
7048
|
+
var LOCAL_REGISTRY_MIRROR_RESTART_FILE = `${STATE_DIR}/registry-mirror-restart`;
|
|
6967
7049
|
var RANCHER_DIR = "/etc/rancher/k3s";
|
|
6968
7050
|
var K3S_OWNER_CONFIG = `${RANCHER_DIR}/config.yaml`;
|
|
6969
7051
|
var K3S_CONFIG_DROPIN_DIR = `${RANCHER_DIR}/config.yaml.d`;
|
|
6970
7052
|
var K3S_CONFIG_DROPIN = `${K3S_CONFIG_DROPIN_DIR}/50-conveyor-k3.yaml`;
|
|
6971
7053
|
var REGISTRIES_FILE = `${RANCHER_DIR}/registries.yaml`;
|
|
7054
|
+
var K3S_CERTS_D = "/var/lib/rancher/k3s/agent/etc/containerd/certs.d";
|
|
7055
|
+
var K3S_CONTAINERD_DIR = "/var/lib/rancher/k3s/agent/containerd";
|
|
7056
|
+
var JOURNALD_DROPIN = "/etc/systemd/journald.conf.d/50-conveyor-k3.conf";
|
|
6972
7057
|
var CLOUDFLARED_DIR = "/etc/cloudflared";
|
|
6973
7058
|
var CA_POOL_PATH = `${CLOUDFLARED_DIR}/conveyor-k3-server-ca.crt`;
|
|
6974
7059
|
var SYSTEMD_DIR = "/etc/systemd/system";
|
|
@@ -6982,10 +7067,64 @@ var OWNED_FILES = [
|
|
|
6982
7067
|
TUNNEL_ENV_FILE,
|
|
6983
7068
|
K3S_CONFIG_DROPIN,
|
|
6984
7069
|
CA_POOL_PATH,
|
|
7070
|
+
JOURNALD_DROPIN,
|
|
6985
7071
|
...ALL_UNITS.map((unit) => `${SYSTEMD_DIR}/${unit}`)
|
|
6986
7072
|
];
|
|
6987
7073
|
var RUNNERS_SUDOERS_FILE = "/etc/sudoers.d/conveyor-k3-runners";
|
|
6988
7074
|
|
|
7075
|
+
// src/lib/registries.ts
|
|
7076
|
+
function parseRegistries(raw) {
|
|
7077
|
+
if (!raw || raw.trim().length === 0) return {};
|
|
7078
|
+
const parsed = parse(raw);
|
|
7079
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
7080
|
+
return parsed;
|
|
7081
|
+
}
|
|
7082
|
+
function configuredAuthHosts(file) {
|
|
7083
|
+
return Object.entries(file.configs ?? {}).filter(([, entry]) => Boolean(entry?.auth)).map(([host]) => host).sort();
|
|
7084
|
+
}
|
|
7085
|
+
function missingAuthHosts(file, requiredHosts) {
|
|
7086
|
+
const configured = new Set(configuredAuthHosts(file));
|
|
7087
|
+
return requiredHosts.filter((host) => !configured.has(host)).sort();
|
|
7088
|
+
}
|
|
7089
|
+
function upsertRegistryAuth(file, host, auth) {
|
|
7090
|
+
assertRegistryHost(host);
|
|
7091
|
+
const next = { ...file };
|
|
7092
|
+
const configs = { ...next.configs ?? {} };
|
|
7093
|
+
const existing = configs[host] ?? {};
|
|
7094
|
+
configs[host] = { ...existing, auth };
|
|
7095
|
+
next.configs = configs;
|
|
7096
|
+
return next;
|
|
7097
|
+
}
|
|
7098
|
+
function mirrorEndpoints(file, host) {
|
|
7099
|
+
const entry = file.mirrors?.[host];
|
|
7100
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return [];
|
|
7101
|
+
const endpoint = entry.endpoint;
|
|
7102
|
+
return Array.isArray(endpoint) ? endpoint.filter((value) => typeof value === "string") : [];
|
|
7103
|
+
}
|
|
7104
|
+
function upsertRegistryMirror(file, host, endpoints) {
|
|
7105
|
+
assertRegistryHost(host);
|
|
7106
|
+
return { ...file, mirrors: { ...file.mirrors ?? {}, [host]: { endpoint: [...endpoints] } } };
|
|
7107
|
+
}
|
|
7108
|
+
function removeRegistryMirror(file, host) {
|
|
7109
|
+
if (!file.mirrors || !Object.hasOwn(file.mirrors, host)) return file;
|
|
7110
|
+
const { [host]: _removed, ...mirrors } = file.mirrors;
|
|
7111
|
+
const next = { ...file, mirrors };
|
|
7112
|
+
if (Object.keys(mirrors).length === 0) delete next.mirrors;
|
|
7113
|
+
return next;
|
|
7114
|
+
}
|
|
7115
|
+
function serializeRegistries(file) {
|
|
7116
|
+
return stringify(file);
|
|
7117
|
+
}
|
|
7118
|
+
async function writeRegistriesWithBackup(fs, raw, next, now) {
|
|
7119
|
+
let backupPath = null;
|
|
7120
|
+
if (raw !== null) {
|
|
7121
|
+
backupPath = `${REGISTRIES_FILE}.bak-${now.toISOString().replace(/[:.]/g, "-")}`;
|
|
7122
|
+
await fs.writeFile(backupPath, raw, 384);
|
|
7123
|
+
}
|
|
7124
|
+
await fs.writeFile(REGISTRIES_FILE, serializeRegistries(next), 384);
|
|
7125
|
+
return backupPath;
|
|
7126
|
+
}
|
|
7127
|
+
|
|
6989
7128
|
// src/lib/state.ts
|
|
6990
7129
|
var EMPTY_STATE = {
|
|
6991
7130
|
appliedDesiredStateVersion: 0,
|
|
@@ -7065,22 +7204,22 @@ function renderK3sDropIn(kubelet, ownerConfig) {
|
|
|
7065
7204
|
const memory = assertQuantity(kubelet.systemReserved.memory, "reserveMemory");
|
|
7066
7205
|
add(SYSTEM_RESERVED, `cpu=${cpu},memory=${memory}`);
|
|
7067
7206
|
}
|
|
7068
|
-
const
|
|
7207
|
+
const lines2 = [
|
|
7069
7208
|
"# Managed by Conveyor (@rallycry/conveyor-k3). Do not edit.",
|
|
7070
7209
|
"# Your own /etc/rancher/k3s/config.yaml is never modified; k3s merges both.",
|
|
7071
7210
|
"# `kubelet-arg+` and `disable+` append rather than replacing a list you set."
|
|
7072
7211
|
];
|
|
7073
|
-
|
|
7074
|
-
for (const component of K3S_DISABLED_COMPONENTS)
|
|
7212
|
+
lines2.push("disable+:");
|
|
7213
|
+
for (const component of K3S_DISABLED_COMPONENTS) lines2.push(` - ${component}`);
|
|
7075
7214
|
if (ownerKeys.has(WRITE_KUBECONFIG_MODE)) {
|
|
7076
7215
|
skipped.push(WRITE_KUBECONFIG_MODE);
|
|
7077
7216
|
} else {
|
|
7078
|
-
|
|
7217
|
+
lines2.push(`${WRITE_KUBECONFIG_MODE}: "${KUBECONFIG_MODE}"`);
|
|
7079
7218
|
}
|
|
7080
7219
|
if (args.length > 0) {
|
|
7081
|
-
|
|
7220
|
+
lines2.push("kubelet-arg+:", ...args);
|
|
7082
7221
|
}
|
|
7083
|
-
return { content: [...
|
|
7222
|
+
return { content: [...lines2, ""].join("\n"), skipped };
|
|
7084
7223
|
}
|
|
7085
7224
|
var renderKubeletDropIn = renderK3sDropIn;
|
|
7086
7225
|
|
|
@@ -7129,169 +7268,855 @@ function findDisallowedKinds(manifest) {
|
|
|
7129
7268
|
return disallowed;
|
|
7130
7269
|
}
|
|
7131
7270
|
|
|
7132
|
-
// src/daemon/
|
|
7271
|
+
// src/daemon/cluster-ops.ts
|
|
7272
|
+
async function kubectlApply(exec, manifest) {
|
|
7273
|
+
await runOrThrow(exec, "k3s", ["kubectl", "apply", "-f", "-"], {
|
|
7274
|
+
stdin: manifest,
|
|
7275
|
+
timeoutMs: 6e4
|
|
7276
|
+
});
|
|
7277
|
+
}
|
|
7133
7278
|
function mayDisrupt(runningConveyorPods) {
|
|
7134
7279
|
return runningConveyorPods === 0;
|
|
7135
7280
|
}
|
|
7136
7281
|
function busyReason(runningConveyorPods) {
|
|
7137
7282
|
return runningConveyorPods === null ? "the number of running Conveyor pods could not be read" : `${runningConveyorPods} Conveyor pod(s) are running`;
|
|
7138
7283
|
}
|
|
7139
|
-
|
|
7140
|
-
|
|
7141
|
-
|
|
7142
|
-
|
|
7143
|
-
|
|
7284
|
+
|
|
7285
|
+
// src/daemon/local-registry-upkeep.ts
|
|
7286
|
+
import { createHash } from "crypto";
|
|
7287
|
+
|
|
7288
|
+
// src/lib/local-registry.ts
|
|
7289
|
+
var LOCAL_REGISTRY_IMAGE = "docker.io/library/registry:3.1.2@sha256:c87f33837722a100572e95d7dc4bf539fc42cf68202b13c3bc03c0ff54c3a649";
|
|
7290
|
+
var LOCAL_REGISTRY_CONFIG = "/etc/distribution/config.yml";
|
|
7291
|
+
var LOCAL_REGISTRY_GC_JOB = "conveyor-registry-gc";
|
|
7292
|
+
var LOCAL_REGISTRY_SELECTOR = { "app.kubernetes.io/name": PERSONAL_LOCAL_REGISTRY_NAME };
|
|
7293
|
+
var MANAGED_BY = { "app.kubernetes.io/managed-by": "conveyor-k3" };
|
|
7294
|
+
var DATA_MOUNT = "/var/lib/registry";
|
|
7295
|
+
var REGISTRY_ENV = [
|
|
7296
|
+
{ name: "REGISTRY_STORAGE_DELETE_ENABLED", value: "true" },
|
|
7297
|
+
{ name: "OTEL_TRACES_EXPORTER", value: "none" }
|
|
7298
|
+
];
|
|
7299
|
+
var CONTAINER_SECURITY = {
|
|
7300
|
+
allowPrivilegeEscalation: false,
|
|
7301
|
+
readOnlyRootFilesystem: true,
|
|
7302
|
+
capabilities: { drop: ["ALL"] }
|
|
7303
|
+
};
|
|
7304
|
+
var VOLUME_MOUNTS = [
|
|
7305
|
+
{ name: "data", mountPath: DATA_MOUNT },
|
|
7306
|
+
{ name: "tmp", mountPath: "/tmp" }
|
|
7307
|
+
];
|
|
7308
|
+
var VOLUMES = [
|
|
7309
|
+
{ name: "data", hostPath: { path: LOCAL_REGISTRY_DATA_DIR, type: "Directory" } },
|
|
7310
|
+
{ name: "tmp", emptyDir: { sizeLimit: "64Mi" } }
|
|
7311
|
+
];
|
|
7312
|
+
var POD_BASE = {
|
|
7313
|
+
automountServiceAccountToken: false,
|
|
7314
|
+
enableServiceLinks: false,
|
|
7315
|
+
securityContext: { seccompProfile: { type: "RuntimeDefault" } },
|
|
7316
|
+
volumes: VOLUMES
|
|
7317
|
+
};
|
|
7318
|
+
function registryNetworkPolicy(metadata) {
|
|
7319
|
+
return {
|
|
7320
|
+
apiVersion: "networking.k8s.io/v1",
|
|
7321
|
+
kind: "NetworkPolicy",
|
|
7322
|
+
metadata,
|
|
7323
|
+
spec: {
|
|
7324
|
+
podSelector: { matchLabels: LOCAL_REGISTRY_SELECTOR },
|
|
7325
|
+
policyTypes: ["Ingress"],
|
|
7326
|
+
ingress: [
|
|
7327
|
+
{
|
|
7328
|
+
from: [
|
|
7329
|
+
{
|
|
7330
|
+
namespaceSelector: {},
|
|
7331
|
+
podSelector: {
|
|
7332
|
+
matchExpressions: [
|
|
7333
|
+
{ key: BAKE_COMPONENT_LABEL, operator: "In", values: [...BAKE_COMPONENT_VALUES] }
|
|
7334
|
+
]
|
|
7335
|
+
}
|
|
7336
|
+
}
|
|
7337
|
+
],
|
|
7338
|
+
ports: [{ protocol: "TCP", port: PERSONAL_LOCAL_REGISTRY_PORT }]
|
|
7339
|
+
}
|
|
7340
|
+
]
|
|
7341
|
+
}
|
|
7342
|
+
};
|
|
7144
7343
|
}
|
|
7145
|
-
|
|
7146
|
-
const
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
|
|
7155
|
-
|
|
7156
|
-
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
|
|
7160
|
-
|
|
7161
|
-
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
7344
|
+
function renderLocalRegistryManifests() {
|
|
7345
|
+
const metadata = {
|
|
7346
|
+
name: PERSONAL_LOCAL_REGISTRY_NAME,
|
|
7347
|
+
namespace: PERSONAL_LOCAL_REGISTRY_NAMESPACE,
|
|
7348
|
+
labels: { ...LOCAL_REGISTRY_SELECTOR, ...MANAGED_BY }
|
|
7349
|
+
};
|
|
7350
|
+
const deployment = {
|
|
7351
|
+
apiVersion: "apps/v1",
|
|
7352
|
+
kind: "Deployment",
|
|
7353
|
+
metadata,
|
|
7354
|
+
spec: {
|
|
7355
|
+
replicas: 1,
|
|
7356
|
+
// One pod owns the data directory at a time.
|
|
7357
|
+
strategy: { type: "Recreate" },
|
|
7358
|
+
selector: { matchLabels: LOCAL_REGISTRY_SELECTOR },
|
|
7359
|
+
template: {
|
|
7360
|
+
metadata: { labels: metadata.labels },
|
|
7361
|
+
spec: {
|
|
7362
|
+
...POD_BASE,
|
|
7363
|
+
containers: [
|
|
7364
|
+
{
|
|
7365
|
+
name: "registry",
|
|
7366
|
+
image: LOCAL_REGISTRY_IMAGE,
|
|
7367
|
+
imagePullPolicy: "IfNotPresent",
|
|
7368
|
+
env: REGISTRY_ENV,
|
|
7369
|
+
ports: [
|
|
7370
|
+
{
|
|
7371
|
+
name: "registry",
|
|
7372
|
+
containerPort: PERSONAL_LOCAL_REGISTRY_PORT,
|
|
7373
|
+
// Loopback only: the node's containerd is the one client
|
|
7374
|
+
// that comes in through this port.
|
|
7375
|
+
hostPort: PERSONAL_LOCAL_REGISTRY_PORT,
|
|
7376
|
+
hostIP: "127.0.0.1",
|
|
7377
|
+
protocol: "TCP"
|
|
7378
|
+
}
|
|
7379
|
+
],
|
|
7380
|
+
readinessProbe: {
|
|
7381
|
+
httpGet: { path: "/v2/", port: PERSONAL_LOCAL_REGISTRY_PORT },
|
|
7382
|
+
periodSeconds: 10
|
|
7383
|
+
},
|
|
7384
|
+
resources: {
|
|
7385
|
+
requests: { cpu: "50m", memory: "64Mi" },
|
|
7386
|
+
limits: { memory: "512Mi" }
|
|
7387
|
+
},
|
|
7388
|
+
securityContext: CONTAINER_SECURITY,
|
|
7389
|
+
volumeMounts: VOLUME_MOUNTS
|
|
7390
|
+
}
|
|
7391
|
+
]
|
|
7392
|
+
}
|
|
7393
|
+
}
|
|
7394
|
+
}
|
|
7395
|
+
};
|
|
7396
|
+
const service = {
|
|
7397
|
+
apiVersion: "v1",
|
|
7398
|
+
kind: "Service",
|
|
7399
|
+
metadata,
|
|
7400
|
+
spec: {
|
|
7401
|
+
type: "ClusterIP",
|
|
7402
|
+
selector: LOCAL_REGISTRY_SELECTOR,
|
|
7403
|
+
ports: [
|
|
7404
|
+
{
|
|
7405
|
+
name: "registry",
|
|
7406
|
+
port: PERSONAL_LOCAL_REGISTRY_PORT,
|
|
7407
|
+
targetPort: PERSONAL_LOCAL_REGISTRY_PORT,
|
|
7408
|
+
protocol: "TCP"
|
|
7409
|
+
}
|
|
7410
|
+
]
|
|
7411
|
+
}
|
|
7412
|
+
};
|
|
7413
|
+
const items = [deployment, service, registryNetworkPolicy(metadata)];
|
|
7414
|
+
return `${JSON.stringify({ apiVersion: "v1", kind: "List", items }, null, 2)}
|
|
7415
|
+
`;
|
|
7416
|
+
}
|
|
7417
|
+
function renderRegistryGcJob() {
|
|
7418
|
+
const labels = { "app.kubernetes.io/name": LOCAL_REGISTRY_GC_JOB, ...MANAGED_BY };
|
|
7419
|
+
const job = {
|
|
7420
|
+
apiVersion: "batch/v1",
|
|
7421
|
+
kind: "Job",
|
|
7422
|
+
metadata: { name: LOCAL_REGISTRY_GC_JOB, namespace: PERSONAL_LOCAL_REGISTRY_NAMESPACE, labels },
|
|
7423
|
+
spec: {
|
|
7424
|
+
backoffLimit: 0,
|
|
7425
|
+
activeDeadlineSeconds: 600,
|
|
7426
|
+
// A Job left by a daemon that died mid-sweep goes on its own.
|
|
7427
|
+
ttlSecondsAfterFinished: 600,
|
|
7428
|
+
template: {
|
|
7429
|
+
metadata: { labels },
|
|
7430
|
+
spec: {
|
|
7431
|
+
...POD_BASE,
|
|
7432
|
+
restartPolicy: "Never",
|
|
7433
|
+
containers: [
|
|
7434
|
+
{
|
|
7435
|
+
name: "garbage-collect",
|
|
7436
|
+
image: LOCAL_REGISTRY_IMAGE,
|
|
7437
|
+
imagePullPolicy: "IfNotPresent",
|
|
7438
|
+
args: ["garbage-collect", LOCAL_REGISTRY_CONFIG],
|
|
7439
|
+
env: REGISTRY_ENV,
|
|
7440
|
+
resources: {
|
|
7441
|
+
requests: { cpu: "100m", memory: "128Mi" },
|
|
7442
|
+
limits: { memory: "1Gi" }
|
|
7443
|
+
},
|
|
7444
|
+
securityContext: CONTAINER_SECURITY,
|
|
7445
|
+
volumeMounts: VOLUME_MOUNTS
|
|
7446
|
+
}
|
|
7447
|
+
]
|
|
7448
|
+
}
|
|
7449
|
+
}
|
|
7450
|
+
}
|
|
7451
|
+
};
|
|
7452
|
+
return `${JSON.stringify(job, null, 2)}
|
|
7453
|
+
`;
|
|
7454
|
+
}
|
|
7455
|
+
function mirrorHostsTomlPaths(host) {
|
|
7456
|
+
const colon = host.lastIndexOf(":");
|
|
7457
|
+
const escaped = colon > 0 ? `${host.slice(0, colon)}_${host.slice(colon + 1)}_` : host;
|
|
7458
|
+
return [.../* @__PURE__ */ new Set([escaped, host])].map((dir) => `${K3S_CERTS_D}/${dir}/hosts.toml`);
|
|
7459
|
+
}
|
|
7460
|
+
async function mirrorLoaded(readFile3) {
|
|
7461
|
+
for (const path of mirrorHostsTomlPaths(PERSONAL_LOCAL_REGISTRY_HOST)) {
|
|
7462
|
+
const content = await readFile3(path);
|
|
7463
|
+
if (content?.includes(PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT)) return true;
|
|
7168
7464
|
}
|
|
7169
|
-
|
|
7170
|
-
|
|
7171
|
-
|
|
7172
|
-
|
|
7173
|
-
|
|
7174
|
-
|
|
7175
|
-
);
|
|
7465
|
+
return false;
|
|
7466
|
+
}
|
|
7467
|
+
async function registryAnswers(fetchImpl = fetch) {
|
|
7468
|
+
try {
|
|
7469
|
+
const response = await fetchImpl(`${PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT}/v2/`, {
|
|
7470
|
+
signal: AbortSignal.timeout(2e3)
|
|
7471
|
+
});
|
|
7472
|
+
await response.body?.cancel();
|
|
7473
|
+
return response.status === 200;
|
|
7474
|
+
} catch {
|
|
7475
|
+
return false;
|
|
7176
7476
|
}
|
|
7177
7477
|
}
|
|
7178
|
-
async function
|
|
7179
|
-
const
|
|
7180
|
-
|
|
7181
|
-
|
|
7478
|
+
async function localRegistryReady(ports) {
|
|
7479
|
+
const [answers, mirrored] = await Promise.all([
|
|
7480
|
+
registryAnswers(ports.fetch),
|
|
7481
|
+
mirrorLoaded(ports.readFile)
|
|
7482
|
+
]);
|
|
7483
|
+
return answers && mirrored;
|
|
7484
|
+
}
|
|
7485
|
+
|
|
7486
|
+
// src/daemon/registry-gc-lock.ts
|
|
7487
|
+
var REGISTRY_GC_MARK = "conveyor.rallycry.com/registry-gc";
|
|
7488
|
+
var REGISTRY_GC_MARK_TTL_MS = 20 * 6e4;
|
|
7489
|
+
function inRegistryNamespace(exec, args, timeoutMs = 3e4) {
|
|
7490
|
+
return exec("k3s", ["kubectl", "-n", PERSONAL_LOCAL_REGISTRY_NAMESPACE, ...args], { timeoutMs });
|
|
7491
|
+
}
|
|
7492
|
+
async function readRegistryDeployment(exec) {
|
|
7493
|
+
const result = await inRegistryNamespace(exec, [
|
|
7182
7494
|
"get",
|
|
7183
|
-
"
|
|
7184
|
-
|
|
7495
|
+
"deployment",
|
|
7496
|
+
PERSONAL_LOCAL_REGISTRY_NAME,
|
|
7497
|
+
"--ignore-not-found",
|
|
7185
7498
|
"-o",
|
|
7186
|
-
"
|
|
7499
|
+
"json"
|
|
7187
7500
|
]);
|
|
7188
|
-
if (
|
|
7189
|
-
|
|
7190
|
-
|
|
7191
|
-
|
|
7192
|
-
|
|
7193
|
-
|
|
7194
|
-
|
|
7195
|
-
|
|
7196
|
-
|
|
7197
|
-
|
|
7198
|
-
|
|
7199
|
-
|
|
7200
|
-
|
|
7201
|
-
);
|
|
7202
|
-
return;
|
|
7501
|
+
if (result.exitCode !== 0) return null;
|
|
7502
|
+
if (result.stdout.trim().length === 0) return { exists: false, replicas: null, mark: null };
|
|
7503
|
+
try {
|
|
7504
|
+
const deployment = JSON.parse(result.stdout);
|
|
7505
|
+
const replicas = deployment.spec?.replicas;
|
|
7506
|
+
const mark = deployment.metadata?.annotations?.[REGISTRY_GC_MARK];
|
|
7507
|
+
return {
|
|
7508
|
+
exists: true,
|
|
7509
|
+
replicas: typeof replicas === "number" ? replicas : null,
|
|
7510
|
+
mark: typeof mark === "string" ? mark : null
|
|
7511
|
+
};
|
|
7512
|
+
} catch {
|
|
7513
|
+
return null;
|
|
7203
7514
|
}
|
|
7515
|
+
}
|
|
7516
|
+
function processAlive(pid) {
|
|
7204
7517
|
try {
|
|
7205
|
-
|
|
7206
|
-
|
|
7518
|
+
process.kill(pid, 0);
|
|
7519
|
+
return true;
|
|
7207
7520
|
} catch (error) {
|
|
7208
|
-
|
|
7209
|
-
|
|
7210
|
-
|
|
7211
|
-
|
|
7521
|
+
return error.code === "EPERM";
|
|
7522
|
+
}
|
|
7523
|
+
}
|
|
7524
|
+
function newGcMark(now) {
|
|
7525
|
+
return `${process.pid}@${new Date(now).toISOString()}`;
|
|
7526
|
+
}
|
|
7527
|
+
function gcMarkIsLive(mark, now, alive = processAlive) {
|
|
7528
|
+
const match = mark ? /^(\d+)@(.+)$/.exec(mark) : null;
|
|
7529
|
+
if (!match?.[1] || !match[2]) return false;
|
|
7530
|
+
const started = Date.parse(match[2]);
|
|
7531
|
+
return !Number.isNaN(started) && now - started < REGISTRY_GC_MARK_TTL_MS && alive(Number(match[1]));
|
|
7532
|
+
}
|
|
7533
|
+
async function setGcMark(exec, mark) {
|
|
7534
|
+
await runOrThrow(
|
|
7535
|
+
exec,
|
|
7536
|
+
"k3s",
|
|
7537
|
+
[
|
|
7538
|
+
"kubectl",
|
|
7539
|
+
"-n",
|
|
7540
|
+
PERSONAL_LOCAL_REGISTRY_NAMESPACE,
|
|
7541
|
+
"annotate",
|
|
7542
|
+
"deployment",
|
|
7543
|
+
PERSONAL_LOCAL_REGISTRY_NAME,
|
|
7544
|
+
`${REGISTRY_GC_MARK}=${mark}`,
|
|
7545
|
+
"--overwrite"
|
|
7546
|
+
],
|
|
7547
|
+
{ timeoutMs: 3e4 }
|
|
7548
|
+
);
|
|
7212
7549
|
}
|
|
7213
|
-
function
|
|
7214
|
-
const
|
|
7215
|
-
|
|
7216
|
-
|
|
7217
|
-
|
|
7218
|
-
|
|
7219
|
-
|
|
7220
|
-
|
|
7221
|
-
|
|
7222
|
-
|
|
7223
|
-
|
|
7224
|
-
|
|
7225
|
-
|
|
7226
|
-
|
|
7227
|
-
|
|
7228
|
-
|
|
7229
|
-
|
|
7230
|
-
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
""
|
|
7237
|
-
].join("\n");
|
|
7550
|
+
async function clearGcMark(exec) {
|
|
7551
|
+
const result = await inRegistryNamespace(exec, [
|
|
7552
|
+
"annotate",
|
|
7553
|
+
"deployment",
|
|
7554
|
+
PERSONAL_LOCAL_REGISTRY_NAME,
|
|
7555
|
+
`${REGISTRY_GC_MARK}-`
|
|
7556
|
+
]);
|
|
7557
|
+
return result.exitCode === 0;
|
|
7558
|
+
}
|
|
7559
|
+
async function deleteSweepJob(exec) {
|
|
7560
|
+
const result = await inRegistryNamespace(
|
|
7561
|
+
exec,
|
|
7562
|
+
[
|
|
7563
|
+
"delete",
|
|
7564
|
+
"job",
|
|
7565
|
+
LOCAL_REGISTRY_GC_JOB,
|
|
7566
|
+
"--ignore-not-found",
|
|
7567
|
+
"--cascade=foreground",
|
|
7568
|
+
"--wait=true"
|
|
7569
|
+
],
|
|
7570
|
+
12e4
|
|
7571
|
+
);
|
|
7572
|
+
return result.exitCode === 0;
|
|
7238
7573
|
}
|
|
7239
|
-
|
|
7240
|
-
|
|
7574
|
+
|
|
7575
|
+
// src/daemon/local-registry-upkeep.ts
|
|
7576
|
+
function nowDate() {
|
|
7577
|
+
return /* @__PURE__ */ new Date();
|
|
7578
|
+
}
|
|
7579
|
+
function mirrorConfigured(raw) {
|
|
7580
|
+
const endpoints = mirrorEndpoints(parseRegistries(raw), PERSONAL_LOCAL_REGISTRY_HOST);
|
|
7581
|
+
return endpoints.length === 1 && endpoints[0] === PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT;
|
|
7582
|
+
}
|
|
7583
|
+
async function applyUnlessCollecting(ctx) {
|
|
7584
|
+
const deployment = await readRegistryDeployment(ctx.exec);
|
|
7585
|
+
if (!deployment) throw new Error("the registry's Deployment could not be read");
|
|
7586
|
+
if (gcMarkIsLive(deployment.mark, (ctx.now ?? nowDate)().getTime(), ctx.isAlive)) return false;
|
|
7587
|
+
if (deployment.mark !== null && !await deleteSweepJob(ctx.exec)) {
|
|
7588
|
+
throw new Error("a stopped cleanup's Job could not be deleted, so the registry stays stopped");
|
|
7589
|
+
}
|
|
7590
|
+
await kubectlApply(ctx.exec, renderLocalRegistryManifests());
|
|
7591
|
+
if (deployment.mark !== null) await clearGcMark(ctx.exec);
|
|
7592
|
+
return true;
|
|
7593
|
+
}
|
|
7594
|
+
async function deployLocalRegistry(ctx) {
|
|
7595
|
+
await ctx.fs.ensureDir(LOCAL_REGISTRY_DATA_DIR, 448);
|
|
7596
|
+
const applied = await applyUnlessCollecting(ctx);
|
|
7597
|
+
const raw = await ctx.fs.readFile(REGISTRIES_FILE);
|
|
7598
|
+
if (mirrorConfigured(raw)) return { applied, mirrorWritten: false, backupPath: null };
|
|
7599
|
+
const next = upsertRegistryMirror(parseRegistries(raw), PERSONAL_LOCAL_REGISTRY_HOST, [
|
|
7600
|
+
PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT
|
|
7601
|
+
]);
|
|
7602
|
+
const backupPath = await writeRegistriesWithBackup(ctx.fs, raw, next, (ctx.now ?? nowDate)());
|
|
7603
|
+
return { applied, mirrorWritten: true, backupPath };
|
|
7604
|
+
}
|
|
7605
|
+
async function readMirrorState(fs) {
|
|
7606
|
+
const raw = await fs.readFile(REGISTRIES_FILE);
|
|
7607
|
+
const fingerprint = createHash("sha256").update(raw ?? "").digest("hex");
|
|
7608
|
+
if (!mirrorConfigured(raw)) return { state: "absent", fingerprint };
|
|
7609
|
+
if (await mirrorLoaded((path) => fs.readFile(path))) return { state: "loaded", fingerprint };
|
|
7610
|
+
const tried = (await fs.readFile(LOCAL_REGISTRY_MIRROR_RESTART_FILE))?.trim() === fingerprint;
|
|
7611
|
+
return { state: tried ? "tried" : "owed", fingerprint };
|
|
7612
|
+
}
|
|
7613
|
+
async function registryMirrorPending(fs) {
|
|
7614
|
+
return (await readMirrorState(fs)).state === "owed";
|
|
7615
|
+
}
|
|
7616
|
+
async function loadRegistryMirror(ctx) {
|
|
7617
|
+
const { state, fingerprint } = await readMirrorState(ctx.fs);
|
|
7618
|
+
if (state === "tried") {
|
|
7619
|
+
return {
|
|
7620
|
+
restarted: false,
|
|
7621
|
+
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}.`
|
|
7622
|
+
};
|
|
7623
|
+
}
|
|
7624
|
+
if (state !== "owed") return { restarted: false, warning: null };
|
|
7625
|
+
if (!mayDisrupt(ctx.runningConveyorPods)) {
|
|
7626
|
+
return {
|
|
7627
|
+
restarted: false,
|
|
7628
|
+
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.`
|
|
7629
|
+
};
|
|
7630
|
+
}
|
|
7631
|
+
const restart = await ctx.exec("systemctl", ["restart", "k3s"], { timeoutMs: 12e4 });
|
|
7632
|
+
if (restart.exitCode !== 0) {
|
|
7633
|
+
return {
|
|
7634
|
+
restarted: false,
|
|
7635
|
+
warning: `Could not restart k3s to load the local image registry mirror: ${restart.stderr.trim().slice(0, 200)}`
|
|
7636
|
+
};
|
|
7637
|
+
}
|
|
7638
|
+
await ctx.fs.writeFile(LOCAL_REGISTRY_MIRROR_RESTART_FILE, `${fingerprint}
|
|
7639
|
+
`, 384);
|
|
7640
|
+
return { restarted: true, warning: null };
|
|
7641
|
+
}
|
|
7642
|
+
|
|
7643
|
+
// src/daemon/prune-namespaces.ts
|
|
7644
|
+
var POOL_NAMESPACE_PREFIX = "claudespace-";
|
|
7645
|
+
var WORKSPACE_MANAGER_ROLE = "conveyor-workspace-manager";
|
|
7646
|
+
var MAX_NAMESPACE_DELETES_PER_RUN = 5;
|
|
7647
|
+
var KUBECTL_TIMEOUT = "--request-timeout=10s";
|
|
7648
|
+
var EXEC_OPTIONS = { timeoutMs: 15e3 };
|
|
7649
|
+
function kubectl(exec, args) {
|
|
7650
|
+
return exec("k3s", ["kubectl", ...args, KUBECTL_TIMEOUT], EXEC_OPTIONS).catch(() => null);
|
|
7651
|
+
}
|
|
7652
|
+
function candidateNamespaces(listing, wanted) {
|
|
7653
|
+
const names = [];
|
|
7654
|
+
for (const line of listing.split("\n")) {
|
|
7655
|
+
const [name, phase] = line.trim().split(/\s+/);
|
|
7656
|
+
if (!name?.startsWith(POOL_NAMESPACE_PREFIX) || wanted.has(name)) continue;
|
|
7657
|
+
if (phase === "Terminating") continue;
|
|
7241
7658
|
try {
|
|
7242
|
-
|
|
7243
|
-
} catch
|
|
7659
|
+
names.push(assertK8sName(name, "namespace"));
|
|
7660
|
+
} catch {
|
|
7661
|
+
}
|
|
7662
|
+
}
|
|
7663
|
+
return names;
|
|
7664
|
+
}
|
|
7665
|
+
async function isEmptyConveyorNamespace(exec, namespace) {
|
|
7666
|
+
const role = await kubectl(exec, [
|
|
7667
|
+
"get",
|
|
7668
|
+
"role",
|
|
7669
|
+
WORKSPACE_MANAGER_ROLE,
|
|
7670
|
+
"-n",
|
|
7671
|
+
namespace,
|
|
7672
|
+
"-o",
|
|
7673
|
+
"name"
|
|
7674
|
+
]);
|
|
7675
|
+
if (role?.exitCode !== 0) return false;
|
|
7676
|
+
const pods = await kubectl(exec, ["get", "pods", "-n", namespace, "-o", "name"]);
|
|
7677
|
+
return pods?.exitCode === 0 && pods.stdout.trim() === "";
|
|
7678
|
+
}
|
|
7679
|
+
async function pruneStaleNamespaces(ctx, desired, result) {
|
|
7680
|
+
if (desired.namespaces.length === 0) return;
|
|
7681
|
+
const listed = await kubectl(ctx.exec, [
|
|
7682
|
+
"get",
|
|
7683
|
+
"namespaces",
|
|
7684
|
+
"-o",
|
|
7685
|
+
'jsonpath={range .items[*]}{.metadata.name}{" "}{.status.phase}{"\\n"}{end}'
|
|
7686
|
+
]);
|
|
7687
|
+
if (listed?.exitCode !== 0) return;
|
|
7688
|
+
const wanted = new Set(desired.namespaces.map((namespace) => namespace.name));
|
|
7689
|
+
let deletes = 0;
|
|
7690
|
+
for (const namespace of candidateNamespaces(listed.stdout, wanted)) {
|
|
7691
|
+
if (deletes >= MAX_NAMESPACE_DELETES_PER_RUN) return;
|
|
7692
|
+
if (!await isEmptyConveyorNamespace(ctx.exec, namespace)) continue;
|
|
7693
|
+
deletes += 1;
|
|
7694
|
+
const removed = await kubectl(ctx.exec, ["delete", "namespace", namespace, "--wait=false"]);
|
|
7695
|
+
if (removed?.exitCode === 0) {
|
|
7696
|
+
result.prunedNamespaces.push(namespace);
|
|
7697
|
+
} else {
|
|
7244
7698
|
result.warnings.push(
|
|
7245
|
-
`Could not
|
|
7699
|
+
`Could not remove the unused namespace ${namespace}: ${(removed?.stderr ?? "no answer from k3s").trim().slice(0, 160)}`
|
|
7246
7700
|
);
|
|
7247
7701
|
}
|
|
7248
7702
|
}
|
|
7249
7703
|
}
|
|
7250
|
-
|
|
7251
|
-
|
|
7252
|
-
|
|
7253
|
-
|
|
7254
|
-
|
|
7704
|
+
|
|
7705
|
+
// src/daemon/registry-tags.ts
|
|
7706
|
+
var REPO_NAME = /^[a-z0-9]+(?:[._-][a-z0-9]+)*(?:\/[a-z0-9]+(?:[._-][a-z0-9]+)*)*$/;
|
|
7707
|
+
var TAG_NAME = /^[A-Za-z0-9_][A-Za-z0-9._-]{0,127}$/;
|
|
7708
|
+
var LEG_TAG = /^current-([0-9a-f]{16})-(?:amd64|arm64)$/;
|
|
7709
|
+
var DIGEST = /^sha256:[0-9a-f]{64}$/;
|
|
7710
|
+
var PAGE_SIZE = 100;
|
|
7711
|
+
var MAX_REPOSITORIES = 1e3;
|
|
7712
|
+
var MAX_TAGS = 5e3;
|
|
7713
|
+
var REQUEST_TIMEOUT_MS = 15e3;
|
|
7714
|
+
var MANIFEST_ACCEPT = [
|
|
7715
|
+
"application/vnd.oci.image.index.v1+json",
|
|
7716
|
+
"application/vnd.docker.distribution.manifest.list.v2+json",
|
|
7717
|
+
"application/vnd.oci.image.manifest.v1+json",
|
|
7718
|
+
"application/vnd.docker.distribution.manifest.v2+json"
|
|
7719
|
+
].join(", ");
|
|
7720
|
+
function stringList(value) {
|
|
7721
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
7722
|
+
}
|
|
7723
|
+
async function getJson(fetchImpl, path) {
|
|
7724
|
+
const response = await fetchImpl(`${PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT}${path}`, {
|
|
7725
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
7726
|
+
});
|
|
7727
|
+
if (!response.ok) {
|
|
7728
|
+
await response.body?.cancel();
|
|
7729
|
+
throw new Error(`The local registry answered ${response.status} to GET ${path}`);
|
|
7730
|
+
}
|
|
7731
|
+
return { body: await response.json(), hasNext: response.headers.has("link") };
|
|
7732
|
+
}
|
|
7733
|
+
async function listPaged(fetchImpl, path, key, limit) {
|
|
7734
|
+
const names = [];
|
|
7735
|
+
let last;
|
|
7736
|
+
while (names.length < limit) {
|
|
7737
|
+
const query = `n=${PAGE_SIZE}${last ? `&last=${encodeURIComponent(last)}` : ""}`;
|
|
7738
|
+
const { body, hasNext } = await getJson(fetchImpl, `${path}?${query}`);
|
|
7739
|
+
const page = stringList(body?.[key]);
|
|
7740
|
+
names.push(...page);
|
|
7741
|
+
if (!hasNext || page.length === 0) return { names, complete: true };
|
|
7742
|
+
last = page.at(-1);
|
|
7743
|
+
}
|
|
7744
|
+
return { names: names.slice(0, limit), complete: false };
|
|
7745
|
+
}
|
|
7746
|
+
async function readManifest(fetchImpl, repo, reference, method) {
|
|
7747
|
+
const response = await fetchImpl(
|
|
7748
|
+
`${PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT}/v2/${repo}/manifests/${reference}`,
|
|
7749
|
+
{
|
|
7750
|
+
method,
|
|
7751
|
+
headers: { Accept: MANIFEST_ACCEPT },
|
|
7752
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
7753
|
+
}
|
|
7754
|
+
);
|
|
7755
|
+
const digest = response.headers.get("docker-content-digest");
|
|
7756
|
+
if (!response.ok || !digest || !DIGEST.test(digest)) {
|
|
7757
|
+
await response.body?.cancel();
|
|
7758
|
+
return null;
|
|
7759
|
+
}
|
|
7760
|
+
if (method === "HEAD") return { digest, children: [] };
|
|
7761
|
+
const body = await response.json();
|
|
7762
|
+
const children = (body?.manifests ?? []).map((entry) => entry.digest).filter((child) => typeof child === "string" && DIGEST.test(child));
|
|
7763
|
+
return { digest, children };
|
|
7764
|
+
}
|
|
7765
|
+
function isSupersededLeg(tag, retain) {
|
|
7766
|
+
const match = LEG_TAG.exec(tag);
|
|
7767
|
+
return match?.[1] !== void 0 && !retain.has(match[1]);
|
|
7768
|
+
}
|
|
7769
|
+
async function planRepository(fetchImpl, repo, retain) {
|
|
7770
|
+
const listed = await listPaged(fetchImpl, `/v2/${repo}/tags/list`, "tags", MAX_TAGS);
|
|
7771
|
+
if (!listed.complete) return { skip: `it has more than ${MAX_TAGS} tags` };
|
|
7772
|
+
const tags = listed.names.filter((tag) => TAG_NAME.test(tag));
|
|
7773
|
+
const legs = tags.filter((tag) => isSupersededLeg(tag, retain));
|
|
7774
|
+
const kept = tags.filter((tag) => !isSupersededLeg(tag, retain));
|
|
7775
|
+
if (legs.length === 0) return { deletions: [], kept: kept.length };
|
|
7776
|
+
const keep = /* @__PURE__ */ new Set();
|
|
7777
|
+
for (const tag of kept) {
|
|
7778
|
+
const manifest = await readManifest(fetchImpl, repo, tag, "GET");
|
|
7779
|
+
if (!manifest) return { skip: `the tag ${tag} it keeps could not be read` };
|
|
7780
|
+
keep.add(manifest.digest);
|
|
7781
|
+
for (const child of manifest.children) keep.add(child);
|
|
7782
|
+
}
|
|
7783
|
+
const byDigest = /* @__PURE__ */ new Map();
|
|
7784
|
+
for (const tag of legs) {
|
|
7785
|
+
const manifest = await readManifest(fetchImpl, repo, tag, "HEAD");
|
|
7786
|
+
if (!manifest || keep.has(manifest.digest)) continue;
|
|
7787
|
+
byDigest.set(manifest.digest, [...byDigest.get(manifest.digest) ?? [], tag]);
|
|
7788
|
+
}
|
|
7789
|
+
const deletions = [...byDigest].map(([digest, names]) => ({ repo, digest, tags: names }));
|
|
7790
|
+
return { deletions, kept: kept.length };
|
|
7791
|
+
}
|
|
7792
|
+
async function planTagCleanup(fetchImpl, retain) {
|
|
7793
|
+
const catalog = await listPaged(fetchImpl, "/v2/_catalog", "repositories", MAX_REPOSITORIES);
|
|
7794
|
+
const repos = catalog.names.filter(
|
|
7795
|
+
(repo) => repo.startsWith("personal-") && REPO_NAME.test(repo)
|
|
7796
|
+
);
|
|
7797
|
+
const plan = { deletions: [], keptTags: 0, skippedRepos: [] };
|
|
7798
|
+
for (const repo of repos) {
|
|
7799
|
+
const planned = await planRepository(fetchImpl, repo, retain).catch((error) => ({
|
|
7800
|
+
skip: `its tags could not be read (${error instanceof Error ? error.message : String(error)})`
|
|
7801
|
+
}));
|
|
7802
|
+
if ("skip" in planned) {
|
|
7803
|
+
plan.skippedRepos.push({ repo, reason: planned.skip });
|
|
7804
|
+
continue;
|
|
7805
|
+
}
|
|
7806
|
+
plan.deletions.push(...planned.deletions);
|
|
7807
|
+
plan.keptTags += planned.kept;
|
|
7255
7808
|
}
|
|
7809
|
+
return plan;
|
|
7256
7810
|
}
|
|
7257
|
-
async function
|
|
7258
|
-
const
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
|
|
7262
|
-
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
|
|
7267
|
-
|
|
7268
|
-
|
|
7269
|
-
|
|
7811
|
+
async function deleteManifest(fetchImpl, deletion) {
|
|
7812
|
+
const response = await fetchImpl(
|
|
7813
|
+
`${PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT}/v2/${deletion.repo}/manifests/${deletion.digest}`,
|
|
7814
|
+
{ method: "DELETE", signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) }
|
|
7815
|
+
);
|
|
7816
|
+
await response.body?.cancel();
|
|
7817
|
+
return response.status === 202 || response.status === 404;
|
|
7818
|
+
}
|
|
7819
|
+
|
|
7820
|
+
// src/daemon/registry-gc.ts
|
|
7821
|
+
var HOUR_MS = 60 * 6e4;
|
|
7822
|
+
var REGISTRY_GC_INTERVAL_MS = 20 * HOUR_MS;
|
|
7823
|
+
var REGISTRY_GC_SIZE_BYTES = 20 * 1024 ** 3;
|
|
7824
|
+
var REGISTRY_GC_SIZE_INTERVAL_MS = 6 * HOUR_MS;
|
|
7825
|
+
var BAKE_OBJECT_STALE_MS = 6 * HOUR_MS;
|
|
7826
|
+
function lines(text) {
|
|
7827
|
+
return text.split("\n").filter((line) => line.trim().length > 0);
|
|
7828
|
+
}
|
|
7829
|
+
var inNamespace = inRegistryNamespace;
|
|
7830
|
+
function defaultSleep(ms) {
|
|
7831
|
+
return new Promise((resolve) => {
|
|
7832
|
+
setTimeout(resolve, ms);
|
|
7833
|
+
});
|
|
7834
|
+
}
|
|
7835
|
+
async function bakeActivity(exec, now) {
|
|
7836
|
+
const pods = await exec(
|
|
7837
|
+
"k3s",
|
|
7838
|
+
[
|
|
7839
|
+
"kubectl",
|
|
7840
|
+
"get",
|
|
7841
|
+
"pods",
|
|
7842
|
+
"--all-namespaces",
|
|
7843
|
+
"-l",
|
|
7844
|
+
CONVEYOR_POD_SELECTOR,
|
|
7845
|
+
// A pod still pulling its image needs the registry as much as a running one.
|
|
7846
|
+
"--field-selector=status.phase!=Succeeded,status.phase!=Failed",
|
|
7847
|
+
"-o",
|
|
7848
|
+
"name"
|
|
7849
|
+
],
|
|
7850
|
+
{ timeoutMs: 3e4 }
|
|
7851
|
+
);
|
|
7852
|
+
if (pods.exitCode !== 0) return "the Conveyor pods could not be listed";
|
|
7853
|
+
const podCount = lines(pods.stdout).length;
|
|
7854
|
+
if (podCount > 0) return `${podCount} Conveyor pod(s) are on the machine`;
|
|
7855
|
+
const bakes = await exec(
|
|
7856
|
+
"k3s",
|
|
7857
|
+
[
|
|
7858
|
+
"kubectl",
|
|
7859
|
+
"get",
|
|
7860
|
+
"jobs,secrets,configmaps",
|
|
7861
|
+
"--all-namespaces",
|
|
7862
|
+
"-l",
|
|
7863
|
+
`${BAKE_COMPONENT_LABEL} in (${BAKE_COMPONENT_VALUES.join(",")})`,
|
|
7864
|
+
"-o",
|
|
7865
|
+
'jsonpath={range .items[*]}{.metadata.creationTimestamp}{"\\n"}{end}'
|
|
7866
|
+
],
|
|
7867
|
+
{ timeoutMs: 3e4 }
|
|
7868
|
+
);
|
|
7869
|
+
if (bakes.exitCode !== 0) return "the bake objects could not be listed";
|
|
7870
|
+
const inFlight = lines(bakes.stdout).some((stamp) => {
|
|
7871
|
+
const created = Date.parse(stamp.trim());
|
|
7872
|
+
return Number.isNaN(created) || now - created < BAKE_OBJECT_STALE_MS;
|
|
7873
|
+
});
|
|
7874
|
+
return inFlight ? "a bake is in flight" : null;
|
|
7875
|
+
}
|
|
7876
|
+
async function pollUntil(check, timeoutMs, intervalMs, sleep2) {
|
|
7877
|
+
for (let waited = 0; waited < timeoutMs; waited += intervalMs) {
|
|
7878
|
+
if (await check()) return true;
|
|
7879
|
+
await sleep2(intervalMs);
|
|
7880
|
+
}
|
|
7881
|
+
return await check();
|
|
7882
|
+
}
|
|
7883
|
+
async function registryPodsGone(exec) {
|
|
7884
|
+
const result = await inNamespace(exec, [
|
|
7885
|
+
"get",
|
|
7886
|
+
"pods",
|
|
7887
|
+
"-l",
|
|
7888
|
+
`app.kubernetes.io/name=${PERSONAL_LOCAL_REGISTRY_NAME}`,
|
|
7889
|
+
"-o",
|
|
7890
|
+
"name"
|
|
7891
|
+
]);
|
|
7892
|
+
return result.exitCode === 0 && lines(result.stdout).length === 0;
|
|
7893
|
+
}
|
|
7894
|
+
async function gcJobState(exec) {
|
|
7895
|
+
const result = await inNamespace(exec, [
|
|
7896
|
+
"get",
|
|
7897
|
+
"job",
|
|
7898
|
+
LOCAL_REGISTRY_GC_JOB,
|
|
7899
|
+
"-o",
|
|
7900
|
+
"jsonpath={.status.succeeded},{.status.failed}"
|
|
7901
|
+
]);
|
|
7902
|
+
if (result.exitCode !== 0) return null;
|
|
7903
|
+
const [succeeded, failed] = result.stdout.trim().split(",");
|
|
7904
|
+
if (Number(succeeded) > 0) return "succeeded";
|
|
7905
|
+
return Number(failed) > 0 ? "failed" : null;
|
|
7906
|
+
}
|
|
7907
|
+
async function waitForGcJob(exec, sleep2) {
|
|
7908
|
+
for (let waited = 0; waited <= 11 * 6e4; waited += 5e3) {
|
|
7909
|
+
const state = await gcJobState(exec);
|
|
7910
|
+
if (state) return state;
|
|
7911
|
+
await sleep2(5e3);
|
|
7912
|
+
}
|
|
7913
|
+
return null;
|
|
7914
|
+
}
|
|
7915
|
+
async function releaseRegistry(ports, mark) {
|
|
7916
|
+
const { exec } = ports;
|
|
7917
|
+
const now = (ports.now ?? Date.now)();
|
|
7918
|
+
const current = await readRegistryDeployment(exec);
|
|
7919
|
+
if (current?.mark !== mark && gcMarkIsLive(current?.mark ?? null, now, ports.isAlive)) return;
|
|
7920
|
+
if (!await deleteSweepJob(exec)) {
|
|
7921
|
+
ports.log(
|
|
7922
|
+
"Left the local image registry stopped: its cleanup Job could not be deleted. The next hourly pass tries again."
|
|
7270
7923
|
);
|
|
7271
|
-
|
|
7272
|
-
|
|
7273
|
-
|
|
7274
|
-
|
|
7275
|
-
|
|
7276
|
-
|
|
7924
|
+
return;
|
|
7925
|
+
}
|
|
7926
|
+
await clearGcMark(exec);
|
|
7927
|
+
const restored = await inNamespace(exec, [
|
|
7928
|
+
"scale",
|
|
7929
|
+
"deployment",
|
|
7930
|
+
PERSONAL_LOCAL_REGISTRY_NAME,
|
|
7931
|
+
"--replicas=1"
|
|
7932
|
+
]);
|
|
7933
|
+
if (restored.exitCode !== 0) {
|
|
7934
|
+
ports.log(
|
|
7935
|
+
`Could not scale the local image registry back up; the next hourly pass restores it: ${restored.stderr.trim().slice(0, 200)}`
|
|
7277
7936
|
);
|
|
7278
7937
|
}
|
|
7279
|
-
|
|
7280
|
-
|
|
7281
|
-
|
|
7282
|
-
|
|
7283
|
-
);
|
|
7284
|
-
|
|
7285
|
-
|
|
7286
|
-
|
|
7287
|
-
|
|
7288
|
-
|
|
7289
|
-
|
|
7938
|
+
}
|
|
7939
|
+
async function collectBlobs(ports) {
|
|
7940
|
+
const { exec } = ports;
|
|
7941
|
+
const sleep2 = ports.sleep ?? defaultSleep;
|
|
7942
|
+
if (!await deleteSweepJob(exec)) throw new Error("a previous cleanup Job could not be deleted");
|
|
7943
|
+
const mark = newGcMark((ports.now ?? Date.now)());
|
|
7944
|
+
await setGcMark(exec, mark);
|
|
7945
|
+
try {
|
|
7946
|
+
const scaled = await inNamespace(exec, [
|
|
7947
|
+
"scale",
|
|
7948
|
+
"deployment",
|
|
7949
|
+
PERSONAL_LOCAL_REGISTRY_NAME,
|
|
7950
|
+
"--replicas=0"
|
|
7951
|
+
]);
|
|
7952
|
+
if (scaled.exitCode !== 0) throw new Error("the registry could not be scaled to zero");
|
|
7953
|
+
if (!await pollUntil(() => registryPodsGone(exec), 12e4, 2e3, sleep2)) {
|
|
7954
|
+
throw new Error("the registry pod did not stop within 2 minutes");
|
|
7955
|
+
}
|
|
7956
|
+
const held = await readRegistryDeployment(exec);
|
|
7957
|
+
if (held?.replicas !== 0 || held.mark !== mark) {
|
|
7958
|
+
throw new Error(
|
|
7959
|
+
"the registry was started again by another process, so the sweep did not run"
|
|
7960
|
+
);
|
|
7961
|
+
}
|
|
7962
|
+
await kubectlApply(exec, renderRegistryGcJob());
|
|
7963
|
+
const state = await waitForGcJob(exec, sleep2);
|
|
7964
|
+
if (state !== "succeeded") throw new Error(`garbage-collect ${state ?? "did not finish"}`);
|
|
7965
|
+
} finally {
|
|
7966
|
+
await releaseRegistry(ports, mark);
|
|
7967
|
+
}
|
|
7968
|
+
}
|
|
7969
|
+
async function readRetainList(ports) {
|
|
7970
|
+
const desired = await ports.requestDesiredState().catch(() => null);
|
|
7971
|
+
if (!desired) return "the settings could not be fetched from Conveyor";
|
|
7972
|
+
if (desired.localRegistry?.enabled !== true) return "the local image registry is off";
|
|
7973
|
+
return desired.localRegistry.retainHashes ?? "Conveyor did not send the list of bakes to keep";
|
|
7974
|
+
}
|
|
7975
|
+
async function registryBlocker(ports, now) {
|
|
7976
|
+
const deployment = await readRegistryDeployment(ports.exec);
|
|
7977
|
+
if (!deployment) return "the local image registry could not be read";
|
|
7978
|
+
if (!deployment.exists) return "the local image registry is not deployed yet";
|
|
7979
|
+
if (gcMarkIsLive(deployment.mark, now, ports.isAlive))
|
|
7980
|
+
return "another registry cleanup is running";
|
|
7981
|
+
return await bakeActivity(ports.exec, now);
|
|
7982
|
+
}
|
|
7983
|
+
async function collectRegistryGarbage(ports, options) {
|
|
7984
|
+
const skip = (reason) => ({
|
|
7985
|
+
skipped: reason,
|
|
7986
|
+
deletions: [],
|
|
7987
|
+
blobsCollected: false
|
|
7988
|
+
});
|
|
7989
|
+
const now = ports.now ?? Date.now;
|
|
7990
|
+
const fetchImpl = ports.fetch ?? fetch;
|
|
7991
|
+
const retain = await readRetainList(ports);
|
|
7992
|
+
if (typeof retain === "string") return skip(retain);
|
|
7993
|
+
const blocked = await registryBlocker(ports, now());
|
|
7994
|
+
if (blocked) return skip(blocked);
|
|
7995
|
+
const plan = await planTagCleanup(fetchImpl, new Set(retain));
|
|
7996
|
+
for (const skipped of plan.skippedRepos) {
|
|
7997
|
+
ports.log(`Left ${skipped.repo} alone: ${skipped.reason}.`);
|
|
7998
|
+
}
|
|
7999
|
+
if (options.dryRun) return { deletions: plan.deletions, blobsCollected: false };
|
|
8000
|
+
const late2 = await bakeActivity(ports.exec, now());
|
|
8001
|
+
if (late2) return skip(late2);
|
|
8002
|
+
const deletions = [];
|
|
8003
|
+
for (const deletion of plan.deletions) {
|
|
8004
|
+
if (await deleteManifest(fetchImpl, deletion)) deletions.push(deletion);
|
|
8005
|
+
else ports.log(`Could not delete ${deletion.repo}@${deletion.digest}.`);
|
|
8006
|
+
}
|
|
8007
|
+
await collectBlobs(ports);
|
|
8008
|
+
return { deletions, blobsCollected: true };
|
|
8009
|
+
}
|
|
8010
|
+
async function registryDataBytes(exec) {
|
|
8011
|
+
const result = await exec("du", ["-s", "--block-size=1", LOCAL_REGISTRY_DATA_DIR], {
|
|
8012
|
+
timeoutMs: 12e4
|
|
8013
|
+
});
|
|
8014
|
+
if (result.exitCode !== 0) return null;
|
|
8015
|
+
const bytes = Number.parseInt(result.stdout.trim().split(/\s+/)[0] ?? "", 10);
|
|
8016
|
+
return Number.isFinite(bytes) ? bytes : null;
|
|
8017
|
+
}
|
|
8018
|
+
async function registryGcDue(exec, lastGcAt, maintenanceHour, now = /* @__PURE__ */ new Date()) {
|
|
8019
|
+
const last = lastGcAt ? Date.parse(lastGcAt) : Number.NaN;
|
|
8020
|
+
const since = Number.isNaN(last) ? Number.POSITIVE_INFINITY : now.getTime() - last;
|
|
8021
|
+
if (now.getHours() === maintenanceHour && since >= REGISTRY_GC_INTERVAL_MS) return true;
|
|
8022
|
+
if (since < REGISTRY_GC_SIZE_INTERVAL_MS) return false;
|
|
8023
|
+
const bytes = await registryDataBytes(exec);
|
|
8024
|
+
return bytes !== null && bytes >= REGISTRY_GC_SIZE_BYTES;
|
|
7290
8025
|
}
|
|
7291
8026
|
|
|
7292
8027
|
// src/daemon/telemetry.ts
|
|
7293
8028
|
import { statfs } from "fs/promises";
|
|
7294
8029
|
|
|
8030
|
+
// src/vm/capacity.ts
|
|
8031
|
+
var GIB = 1024 ** 3;
|
|
8032
|
+
var MIB = 1024 ** 2;
|
|
8033
|
+
var CONTROL_RELAY_SLOTS = 6;
|
|
8034
|
+
var CAPACITY_BOUNDS = {
|
|
8035
|
+
relaySlots: { min: 16, max: 192, flag: "relay-slots" },
|
|
8036
|
+
relayMaxGiB: { min: 1, max: 16, flag: "relay-max-gib" },
|
|
8037
|
+
diskMiBps: { min: 25, max: 2e3, flag: "disk-mbps" },
|
|
8038
|
+
diskIops: { min: 500, max: 5e4, flag: "disk-iops" }
|
|
8039
|
+
};
|
|
8040
|
+
var LEGACY_CAPACITY_LIMITS = Object.freeze({
|
|
8041
|
+
schema: 1,
|
|
8042
|
+
profile: "default",
|
|
8043
|
+
relaySlots: 16,
|
|
8044
|
+
reservedRelaySlots: 0,
|
|
8045
|
+
relayMaxBytes: GIB,
|
|
8046
|
+
diskBytesPerSecond: 100 * MIB,
|
|
8047
|
+
diskIops: 2e3
|
|
8048
|
+
});
|
|
8049
|
+
var KEYS = Object.keys(CAPACITY_BOUNDS);
|
|
8050
|
+
var PROFILES = /* @__PURE__ */ new Set(["default", "server", "custom"]);
|
|
8051
|
+
function parseCapacityLimits(value) {
|
|
8052
|
+
if (value === void 0) return { ...LEGACY_CAPACITY_LIMITS };
|
|
8053
|
+
const limits = typeof value === "object" && value !== null ? value : {};
|
|
8054
|
+
const within = (key, min, max) => Number.isSafeInteger(limits[key]) && limits[key] >= min && limits[key] <= max;
|
|
8055
|
+
const bounds = CAPACITY_BOUNDS;
|
|
8056
|
+
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))
|
|
8057
|
+
throw new Error("The VM capacity limits in the host network policy are invalid.");
|
|
8058
|
+
return {
|
|
8059
|
+
schema: 1,
|
|
8060
|
+
profile: limits.profile,
|
|
8061
|
+
relaySlots: limits.relaySlots,
|
|
8062
|
+
reservedRelaySlots: limits.reservedRelaySlots,
|
|
8063
|
+
relayMaxBytes: limits.relayMaxBytes,
|
|
8064
|
+
diskBytesPerSecond: limits.diskBytesPerSecond,
|
|
8065
|
+
diskIops: limits.diskIops
|
|
8066
|
+
};
|
|
8067
|
+
}
|
|
8068
|
+
|
|
8069
|
+
// src/daemon/local-images.ts
|
|
8070
|
+
var MAX_LOCAL_IMAGES = 64;
|
|
8071
|
+
var MAX_INSPECTIONS_PER_BEAT = 4;
|
|
8072
|
+
var CONTENT_HASH = /^[0-9a-f]{16}$/;
|
|
8073
|
+
function isImageRef(ref) {
|
|
8074
|
+
try {
|
|
8075
|
+
assertImageRef(ref);
|
|
8076
|
+
return true;
|
|
8077
|
+
} catch {
|
|
8078
|
+
return false;
|
|
8079
|
+
}
|
|
8080
|
+
}
|
|
8081
|
+
async function inspectContentHash(exec, imageId) {
|
|
8082
|
+
const result = await exec("crictl", ["inspecti", "-o", "json", imageId], {
|
|
8083
|
+
timeoutMs: 5e3,
|
|
8084
|
+
maxOutputBytes: 256 * 1024
|
|
8085
|
+
}).catch(() => null);
|
|
8086
|
+
if (!result || result.exitCode !== 0) return void 0;
|
|
8087
|
+
try {
|
|
8088
|
+
const label = JSON.parse(result.stdout).info?.imageSpec?.config?.Labels?.[CONVEYOR_CONTENT_HASH_LABEL];
|
|
8089
|
+
return typeof label === "string" && CONTENT_HASH.test(label) ? label : null;
|
|
8090
|
+
} catch {
|
|
8091
|
+
return void 0;
|
|
8092
|
+
}
|
|
8093
|
+
}
|
|
8094
|
+
async function reportLocalImages(exec, inventory, wantedImages, cache) {
|
|
8095
|
+
if (!inventory) return void 0;
|
|
8096
|
+
const wanted = new Set(wantedImages);
|
|
8097
|
+
const held = inventory.images.flatMap(
|
|
8098
|
+
(image) => (image.repoTags ?? []).filter((ref) => ref.startsWith("localhost/") && isImageRef(ref)).map((ref) => ({ ref, imageId: image.id }))
|
|
8099
|
+
);
|
|
8100
|
+
held.sort(
|
|
8101
|
+
(a, b) => Number(wanted.has(b.ref)) - Number(wanted.has(a.ref)) || (a.ref < b.ref ? -1 : a.ref > b.ref ? 1 : 0)
|
|
8102
|
+
);
|
|
8103
|
+
const reported = held.slice(0, MAX_LOCAL_IMAGES);
|
|
8104
|
+
const live = new Set(reported.map((entry) => entry.imageId));
|
|
8105
|
+
for (const id of [...cache.keys()]) if (!live.has(id)) cache.delete(id);
|
|
8106
|
+
const images = [];
|
|
8107
|
+
let inspections = 0;
|
|
8108
|
+
for (const entry of reported) {
|
|
8109
|
+
let contentHash = cache.get(entry.imageId);
|
|
8110
|
+
if (contentHash === void 0 && inspections < MAX_INSPECTIONS_PER_BEAT) {
|
|
8111
|
+
inspections += 1;
|
|
8112
|
+
contentHash = await inspectContentHash(exec, entry.imageId);
|
|
8113
|
+
if (contentHash !== void 0) cache.set(entry.imageId, contentHash);
|
|
8114
|
+
}
|
|
8115
|
+
images.push({ ...entry, contentHash: contentHash ?? null });
|
|
8116
|
+
}
|
|
8117
|
+
return images;
|
|
8118
|
+
}
|
|
8119
|
+
|
|
7295
8120
|
// src/commands/runners.ts
|
|
7296
8121
|
var STAGING_FILE = `${RUNNERS_SUDOERS_FILE}.staged`;
|
|
7297
8122
|
async function detectRunnerUsers(exec) {
|
|
@@ -7327,6 +8152,21 @@ function ungrantedRunnersWarning(users) {
|
|
|
7327
8152
|
var GRANTED_COMMAND_NAMES = LOCAL_BAKE_ROOT_COMMANDS.map((command) => command.name);
|
|
7328
8153
|
|
|
7329
8154
|
// src/daemon/telemetry.ts
|
|
8155
|
+
var LOCAL_REGISTRY_WARN_AFTER_MS = 10 * 6e4;
|
|
8156
|
+
async function reportLocalRegistry(ports, watch, now = Date.now()) {
|
|
8157
|
+
if (!watch) return { fields: {}, warnings: [] };
|
|
8158
|
+
const ready = await localRegistryReady({ readFile: ports.readFile, fetch: ports.fetch });
|
|
8159
|
+
const paused = (watch.pausedUntil ?? 0) > now;
|
|
8160
|
+
watch.notReadySince = ready || paused ? null : watch.notReadySince ?? now;
|
|
8161
|
+
const stuckMs = watch.notReadySince === null ? 0 : now - watch.notReadySince;
|
|
8162
|
+
return {
|
|
8163
|
+
fields: { localRegistry: { host: PERSONAL_LOCAL_REGISTRY_HOST, ready } },
|
|
8164
|
+
warnings: stuckMs >= LOCAL_REGISTRY_WARN_AFTER_MS ? [
|
|
8165
|
+
`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.`
|
|
8166
|
+
] : []
|
|
8167
|
+
};
|
|
8168
|
+
}
|
|
8169
|
+
var VM_CAPACITY_FILE = "/etc/conveyor-vm/capacity.json";
|
|
7330
8170
|
async function versionOf(exec, bin, args) {
|
|
7331
8171
|
const result = await exec(bin, args, { timeoutMs: 1e4 }).catch(() => null);
|
|
7332
8172
|
if (!result || result.exitCode !== 0) return null;
|
|
@@ -7359,71 +8199,354 @@ async function readNodeAllocatable(exec) {
|
|
|
7359
8199
|
const [cpu, memory] = result.stdout.trim().split(/\s+/);
|
|
7360
8200
|
return { allocatableCpu: cpu ?? null, allocatableMemory: memory ?? null };
|
|
7361
8201
|
}
|
|
7362
|
-
async function
|
|
8202
|
+
async function measureK3sStore(exec) {
|
|
8203
|
+
const result = await exec("du", ["-sx", "-B1", K3S_CONTAINERD_DIR], {
|
|
8204
|
+
timeoutMs: 6e4,
|
|
8205
|
+
maxOutputBytes: 1024 * 1024
|
|
8206
|
+
}).catch(() => null);
|
|
8207
|
+
const match = result?.stdout.match(/^\s*(\d+)\s/);
|
|
8208
|
+
if (!match?.[1]) return null;
|
|
8209
|
+
const bytes = Number(match[1]);
|
|
8210
|
+
return Number.isSafeInteger(bytes) ? bytes : null;
|
|
8211
|
+
}
|
|
8212
|
+
async function readDisk(k3sStoreBytes) {
|
|
8213
|
+
try {
|
|
8214
|
+
const stats = await statfs(STATE_DIR);
|
|
8215
|
+
return {
|
|
8216
|
+
totalBytes: Number(stats.blocks) * Number(stats.bsize),
|
|
8217
|
+
freeBytes: Number(stats.bavail) * Number(stats.bsize),
|
|
8218
|
+
k3sStoreBytes
|
|
8219
|
+
};
|
|
8220
|
+
} catch {
|
|
8221
|
+
return { totalBytes: 0, freeBytes: 0, k3sStoreBytes };
|
|
8222
|
+
}
|
|
8223
|
+
}
|
|
8224
|
+
function summarizeImages(inventory, wantedImages) {
|
|
8225
|
+
if (!inventory) return { count: 0, bytes: 0, strandedCount: 0, pinnedCount: 0 };
|
|
8226
|
+
const bytes = inventory.images.reduce(
|
|
8227
|
+
(total, image) => total + (Number.parseInt(image.size ?? "0", 10) || 0),
|
|
8228
|
+
0
|
|
8229
|
+
);
|
|
8230
|
+
return {
|
|
8231
|
+
count: inventory.images.length,
|
|
8232
|
+
bytes,
|
|
8233
|
+
strandedCount: selectStrandedImages(inventory, wantedImages).length,
|
|
8234
|
+
pinnedCount: countPinnedWanted(inventory, wantedImages)
|
|
8235
|
+
};
|
|
8236
|
+
}
|
|
8237
|
+
function imageGcPressureWarning(disk, highThreshold) {
|
|
8238
|
+
if (highThreshold === null || highThreshold === void 0) return null;
|
|
8239
|
+
if (disk.totalBytes <= 0) return null;
|
|
8240
|
+
const usedPercent = Math.round((disk.totalBytes - disk.freeBytes) / disk.totalBytes * 100);
|
|
8241
|
+
if (usedPercent < highThreshold) return null;
|
|
8242
|
+
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.`;
|
|
8243
|
+
}
|
|
8244
|
+
async function readVmCapacity(ports) {
|
|
8245
|
+
const text = await ports.readFile(VM_CAPACITY_FILE);
|
|
8246
|
+
if (text === null) return void 0;
|
|
8247
|
+
try {
|
|
8248
|
+
const { schema: _schema, ...capacity } = parseCapacityLimits(JSON.parse(text));
|
|
8249
|
+
return capacity;
|
|
8250
|
+
} catch {
|
|
8251
|
+
return void 0;
|
|
8252
|
+
}
|
|
8253
|
+
}
|
|
8254
|
+
async function collectHeartbeat(ports, state, inventory) {
|
|
8255
|
+
const registries = parseRegistries(await ports.readFile(REGISTRIES_FILE));
|
|
8256
|
+
const disk = await readDisk(state.k3sStoreBytes ?? null);
|
|
8257
|
+
const gcWarning = imageGcPressureWarning(disk, state.imageGcHighThreshold);
|
|
8258
|
+
const vmCapacity = await readVmCapacity(ports);
|
|
8259
|
+
const localImages = await reportLocalImages(
|
|
8260
|
+
ports.exec,
|
|
8261
|
+
inventory,
|
|
8262
|
+
state.wantedImages,
|
|
8263
|
+
state.localImageHashes ?? /* @__PURE__ */ new Map()
|
|
8264
|
+
);
|
|
8265
|
+
const registry = await reportLocalRegistry(ports, state.localRegistry);
|
|
8266
|
+
const runnerUsers = await detectRunnerUsers(ports.exec);
|
|
8267
|
+
const ungranted = ungrantedRunnerUsers(runnerUsers, await ports.readFile(RUNNERS_SUDOERS_FILE));
|
|
8268
|
+
return {
|
|
8269
|
+
publicWebProxy: (await ports.readFile("/etc/conveyor-vm/public-web"))?.trim() === "enabled",
|
|
8270
|
+
...vmCapacity ? { vmCapacity } : {},
|
|
8271
|
+
cliVersion: state.cliVersion,
|
|
8272
|
+
k3sVersion: await versionOf(ports.exec, "k3s", ["--version"]),
|
|
8273
|
+
cloudflaredVersion: await versionOf(ports.exec, "cloudflared", ["--version"]),
|
|
8274
|
+
appliedDesiredStateVersion: state.appliedDesiredStateVersion,
|
|
8275
|
+
node: await readNodeAllocatable(ports.exec),
|
|
8276
|
+
disk,
|
|
8277
|
+
images: summarizeImages(inventory, state.wantedImages),
|
|
8278
|
+
registryAuthMissing: missingAuthHosts(registries, state.authRequiredHosts),
|
|
8279
|
+
// Display only. A null count is reported as 0 here; the guards that must
|
|
8280
|
+
// not act on an unknown count read the nullable value directly.
|
|
8281
|
+
runningConveyorPods: await countRunningConveyorPods(ports.exec) ?? 0,
|
|
8282
|
+
lastPruneAt: state.lastPruneAt,
|
|
8283
|
+
lastWarmAt: state.lastWarmAt,
|
|
8284
|
+
lastUpdateAt: state.lastUpdateAt,
|
|
8285
|
+
...runnerUsers.length > 0 ? { runnerUsers } : {},
|
|
8286
|
+
...localImages ? { localImages } : {},
|
|
8287
|
+
...registry.fields,
|
|
8288
|
+
localWarnings: [
|
|
8289
|
+
...state.localWarnings,
|
|
8290
|
+
...gcWarning ? [gcWarning] : [],
|
|
8291
|
+
...ungranted.length > 0 ? [ungrantedRunnersWarning(ungranted)] : [],
|
|
8292
|
+
...registry.warnings
|
|
8293
|
+
]
|
|
8294
|
+
};
|
|
8295
|
+
}
|
|
8296
|
+
async function readPodIdentities(exec) {
|
|
8297
|
+
const result = await exec(
|
|
8298
|
+
"k3s",
|
|
8299
|
+
[
|
|
8300
|
+
"kubectl",
|
|
8301
|
+
"get",
|
|
8302
|
+
"pods",
|
|
8303
|
+
"-A",
|
|
8304
|
+
"-l",
|
|
8305
|
+
CONVEYOR_POD_SELECTOR,
|
|
8306
|
+
"--request-timeout=5s",
|
|
8307
|
+
"--no-headers",
|
|
8308
|
+
"-o",
|
|
8309
|
+
"custom-columns=NS:.metadata.namespace,NAME:.metadata.name,UID:.metadata.uid"
|
|
8310
|
+
],
|
|
8311
|
+
{ timeoutMs: 7e3, maxOutputBytes: 65536 }
|
|
8312
|
+
);
|
|
8313
|
+
if (result.exitCode !== 0) return [];
|
|
8314
|
+
const rows = result.stdout.trim().split("\n").filter(Boolean);
|
|
8315
|
+
if (rows.length > 100) return [];
|
|
8316
|
+
return rows.flatMap((row) => {
|
|
8317
|
+
const [namespace, name, uid, extra] = row.trim().split(/\s+/);
|
|
8318
|
+
return !extra && [namespace, name, uid].every((s) => s && /^[a-zA-Z0-9][a-zA-Z0-9.-]{0,252}$/.test(s)) ? [{ namespace, name, uid }] : [];
|
|
8319
|
+
});
|
|
8320
|
+
}
|
|
8321
|
+
|
|
8322
|
+
// src/daemon/registry-upkeep-clock.ts
|
|
8323
|
+
function errorText(error) {
|
|
8324
|
+
return error instanceof Error ? error.message : String(error);
|
|
8325
|
+
}
|
|
8326
|
+
async function collect(ports, now) {
|
|
8327
|
+
const { exec, log, requestDesiredState, fetch: fetch2, sleep: sleep2, isAlive, watch } = ports;
|
|
8328
|
+
if (watch) watch.pausedUntil = now.getTime() + REGISTRY_GC_MARK_TTL_MS;
|
|
8329
|
+
try {
|
|
8330
|
+
const outcome = await collectRegistryGarbage(
|
|
8331
|
+
{ exec, log, requestDesiredState, fetch: fetch2, sleep: sleep2, isAlive },
|
|
8332
|
+
{ dryRun: false }
|
|
8333
|
+
);
|
|
8334
|
+
if (outcome.skipped) return outcome.skipped;
|
|
8335
|
+
log(
|
|
8336
|
+
`Registry garbage collection removed ${outcome.deletions.length} superseded image manifest(s) and their unreferenced layers.`
|
|
8337
|
+
);
|
|
8338
|
+
return null;
|
|
8339
|
+
} finally {
|
|
8340
|
+
if (watch) {
|
|
8341
|
+
watch.pausedUntil = null;
|
|
8342
|
+
watch.notReadySince = null;
|
|
8343
|
+
}
|
|
8344
|
+
}
|
|
8345
|
+
}
|
|
8346
|
+
async function runRegistryUpkeep(ports, lastGcAt, maintenanceHour) {
|
|
8347
|
+
const now = (ports.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
8348
|
+
try {
|
|
8349
|
+
await deployLocalRegistry({
|
|
8350
|
+
exec: ports.exec,
|
|
8351
|
+
fs: ports.fs,
|
|
8352
|
+
runningConveyorPods: null,
|
|
8353
|
+
now: ports.now,
|
|
8354
|
+
isAlive: ports.isAlive
|
|
8355
|
+
});
|
|
8356
|
+
if (await registryMirrorPending(ports.fs) && mayDisrupt(await countRunningConveyorPods(ports.exec))) {
|
|
8357
|
+
await ports.reconcile();
|
|
8358
|
+
}
|
|
8359
|
+
if (!await registryGcDue(ports.exec, lastGcAt, maintenanceHour, now)) return lastGcAt;
|
|
8360
|
+
} catch (error) {
|
|
8361
|
+
ports.log(`Local image registry upkeep failed: ${errorText(error)}`);
|
|
8362
|
+
return lastGcAt;
|
|
8363
|
+
}
|
|
8364
|
+
try {
|
|
8365
|
+
const skipped = await collect(ports, now);
|
|
8366
|
+
if (skipped) {
|
|
8367
|
+
ports.log(`Registry garbage collection skipped: ${skipped}.`);
|
|
8368
|
+
return lastGcAt;
|
|
8369
|
+
}
|
|
8370
|
+
} catch (error) {
|
|
8371
|
+
ports.log(`Registry garbage collection failed: ${errorText(error)}`);
|
|
8372
|
+
}
|
|
8373
|
+
return now.toISOString();
|
|
8374
|
+
}
|
|
8375
|
+
|
|
8376
|
+
// src/daemon/reconcile.ts
|
|
8377
|
+
async function reconcileKubelet(ctx, desired, result) {
|
|
8378
|
+
const ownerConfig = await ctx.fs.readFile(K3S_OWNER_CONFIG);
|
|
8379
|
+
const rendered = renderK3sDropIn(desired.kubelet, ownerConfig);
|
|
8380
|
+
result.skippedKubeletFlags = rendered.skipped;
|
|
8381
|
+
result.effectiveImageGcHighThreshold = rendered.skipped.includes("image-gc-high-threshold") ? ownerImageGcHighThreshold(ownerConfig) : desired.kubelet.imageGcHighThreshold;
|
|
8382
|
+
for (const flag of rendered.skipped) {
|
|
8383
|
+
result.warnings.push(
|
|
8384
|
+
`Left \`${flag}\` to your own /etc/rancher/k3s/config.yaml \u2014 Conveyor does not override a setting you chose.`
|
|
8385
|
+
);
|
|
8386
|
+
}
|
|
8387
|
+
const current = await ctx.fs.readFile(K3S_CONFIG_DROPIN);
|
|
8388
|
+
if (rendered.content === null) {
|
|
8389
|
+
if (current !== null) await ctx.fs.removeFile(K3S_CONFIG_DROPIN);
|
|
8390
|
+
return;
|
|
8391
|
+
}
|
|
8392
|
+
if (current === rendered.content) return;
|
|
8393
|
+
await ctx.fs.ensureDir(K3S_CONFIG_DROPIN.slice(0, K3S_CONFIG_DROPIN.lastIndexOf("/")));
|
|
8394
|
+
await ctx.fs.writeFile(K3S_CONFIG_DROPIN, rendered.content, 420);
|
|
8395
|
+
if (!mayDisrupt(ctx.runningConveyorPods)) {
|
|
8396
|
+
result.warnings.push(
|
|
8397
|
+
`Kubelet settings changed but ${busyReason(ctx.runningConveyorPods)} \u2014 the restart is deferred until the machine is idle.`
|
|
8398
|
+
);
|
|
8399
|
+
return;
|
|
8400
|
+
}
|
|
8401
|
+
const restart = await ctx.exec("systemctl", ["restart", "k3s"], { timeoutMs: 12e4 });
|
|
8402
|
+
if (restart.exitCode === 0) {
|
|
8403
|
+
result.restartedK3s = true;
|
|
8404
|
+
} else {
|
|
8405
|
+
result.warnings.push(
|
|
8406
|
+
`Could not restart k3s to apply new kubelet settings: ${restart.stderr.trim().slice(0, 200)}`
|
|
8407
|
+
);
|
|
8408
|
+
}
|
|
8409
|
+
}
|
|
8410
|
+
async function reconcilePriorityClass(ctx, desired, result) {
|
|
8411
|
+
const name = assertK8sName(desired.priorityClass.name, "priorityClass.name");
|
|
8412
|
+
const read = await ctx.exec("k3s", [
|
|
8413
|
+
"kubectl",
|
|
8414
|
+
"get",
|
|
8415
|
+
"priorityclass",
|
|
8416
|
+
name,
|
|
8417
|
+
"-o",
|
|
8418
|
+
"jsonpath={.value}"
|
|
8419
|
+
]);
|
|
8420
|
+
if (read.exitCode !== 0) return;
|
|
8421
|
+
const existingValue = Number.parseInt(read.stdout.trim(), 10);
|
|
8422
|
+
if (existingValue === desired.priorityClass.value) return;
|
|
8423
|
+
if (!mayDisrupt(ctx.runningConveyorPods)) {
|
|
8424
|
+
result.warnings.push(
|
|
8425
|
+
`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)}.`
|
|
8426
|
+
);
|
|
8427
|
+
return;
|
|
8428
|
+
}
|
|
8429
|
+
const removed = await ctx.exec("k3s", ["kubectl", "delete", "priorityclass", name]);
|
|
8430
|
+
if (removed.exitCode !== 0) {
|
|
8431
|
+
result.warnings.push(
|
|
8432
|
+
`Could not replace the \`${name}\` PriorityClass (value ${existingValue}, needs ${desired.priorityClass.value}): ${removed.stderr.trim().slice(0, 160)}`
|
|
8433
|
+
);
|
|
8434
|
+
return;
|
|
8435
|
+
}
|
|
8436
|
+
try {
|
|
8437
|
+
await kubectlApply(ctx.exec, desired.rbacManifest);
|
|
8438
|
+
result.recreatedPriorityClass = true;
|
|
8439
|
+
} catch (error) {
|
|
8440
|
+
result.warnings.push(
|
|
8441
|
+
`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\`.`
|
|
8442
|
+
);
|
|
8443
|
+
}
|
|
8444
|
+
}
|
|
8445
|
+
function renderLimitRange(namespace, limits) {
|
|
8446
|
+
const ns = assertK8sName(namespace, "namespace");
|
|
8447
|
+
const values = [
|
|
8448
|
+
assertQuantity(limits.defaultRequest.cpu, "defaultRequest.cpu"),
|
|
8449
|
+
assertQuantity(limits.defaultRequest.memory, "defaultRequest.memory"),
|
|
8450
|
+
assertQuantity(limits.default.cpu, "default.cpu"),
|
|
8451
|
+
assertQuantity(limits.default.memory, "default.memory")
|
|
8452
|
+
];
|
|
8453
|
+
return [
|
|
8454
|
+
"apiVersion: v1",
|
|
8455
|
+
"kind: LimitRange",
|
|
8456
|
+
"metadata:",
|
|
8457
|
+
" name: conveyor-k3-defaults",
|
|
8458
|
+
` namespace: ${ns}`,
|
|
8459
|
+
"spec:",
|
|
8460
|
+
" limits:",
|
|
8461
|
+
" - type: Container",
|
|
8462
|
+
" defaultRequest:",
|
|
8463
|
+
` cpu: "${values[0]}"`,
|
|
8464
|
+
` memory: "${values[1]}"`,
|
|
8465
|
+
" default:",
|
|
8466
|
+
` cpu: "${values[2]}"`,
|
|
8467
|
+
` memory: "${values[3]}"`,
|
|
8468
|
+
""
|
|
8469
|
+
].join("\n");
|
|
8470
|
+
}
|
|
8471
|
+
async function reconcileNamespaces(ctx, desired, result) {
|
|
8472
|
+
for (const namespace of desired.namespaces) {
|
|
8473
|
+
try {
|
|
8474
|
+
await kubectlApply(ctx.exec, renderLimitRange(namespace.name, namespace.limitRange));
|
|
8475
|
+
} catch (error) {
|
|
8476
|
+
result.warnings.push(
|
|
8477
|
+
`Could not apply resource defaults to ${namespace.name} (${namespace.projectName}): ${error instanceof Error ? error.message : String(error)}`
|
|
8478
|
+
);
|
|
8479
|
+
}
|
|
8480
|
+
}
|
|
8481
|
+
}
|
|
8482
|
+
async function step(result, label, run) {
|
|
7363
8483
|
try {
|
|
7364
|
-
|
|
7365
|
-
|
|
7366
|
-
|
|
7367
|
-
freeBytes: Number(stats.bavail) * Number(stats.bsize),
|
|
7368
|
-
k3sStoreBytes: 0
|
|
7369
|
-
};
|
|
7370
|
-
} catch {
|
|
7371
|
-
return { totalBytes: 0, freeBytes: 0, k3sStoreBytes: 0 };
|
|
8484
|
+
await run();
|
|
8485
|
+
} catch (error) {
|
|
8486
|
+
result.warnings.push(`${label}: ${error instanceof Error ? error.message : String(error)}`);
|
|
7372
8487
|
}
|
|
7373
8488
|
}
|
|
7374
|
-
function
|
|
7375
|
-
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
strandedCount: selectStrandedImages(inventory, wantedImages).length,
|
|
7384
|
-
pinnedCount: countPinnedWanted(inventory, wantedImages)
|
|
7385
|
-
};
|
|
7386
|
-
}
|
|
7387
|
-
function imageGcPressureWarning(disk, highThreshold) {
|
|
7388
|
-
if (highThreshold === null || highThreshold === void 0) return null;
|
|
7389
|
-
if (disk.totalBytes <= 0) return null;
|
|
7390
|
-
const usedPercent = Math.round((disk.totalBytes - disk.freeBytes) / disk.totalBytes * 100);
|
|
7391
|
-
if (usedPercent < highThreshold) return null;
|
|
7392
|
-
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.`;
|
|
7393
|
-
}
|
|
7394
|
-
async function collectHeartbeat(ports, state, inventory) {
|
|
7395
|
-
const registries = parseRegistries(await ports.readFile(REGISTRIES_FILE));
|
|
7396
|
-
const disk = await readDisk();
|
|
7397
|
-
const gcWarning = imageGcPressureWarning(disk, state.imageGcHighThreshold);
|
|
7398
|
-
const runnerUsers = await detectRunnerUsers(ports.exec);
|
|
7399
|
-
const ungranted = ungrantedRunnerUsers(runnerUsers, await ports.readFile(RUNNERS_SUDOERS_FILE));
|
|
7400
|
-
return {
|
|
7401
|
-
cliVersion: state.cliVersion,
|
|
7402
|
-
k3sVersion: await versionOf(ports.exec, "k3s", ["--version"]),
|
|
7403
|
-
cloudflaredVersion: await versionOf(ports.exec, "cloudflared", ["--version"]),
|
|
7404
|
-
appliedDesiredStateVersion: state.appliedDesiredStateVersion,
|
|
7405
|
-
node: await readNodeAllocatable(ports.exec),
|
|
7406
|
-
disk,
|
|
7407
|
-
images: summarizeImages(inventory, state.wantedImages),
|
|
7408
|
-
registryAuthMissing: missingAuthHosts(registries, state.authRequiredHosts),
|
|
7409
|
-
// Display only. A null count is reported as 0 here; the guards that must
|
|
7410
|
-
// not act on an unknown count read the nullable value directly.
|
|
7411
|
-
runningConveyorPods: await countRunningConveyorPods(ports.exec) ?? 0,
|
|
7412
|
-
lastPruneAt: state.lastPruneAt,
|
|
7413
|
-
lastWarmAt: state.lastWarmAt,
|
|
7414
|
-
lastUpdateAt: state.lastUpdateAt,
|
|
7415
|
-
...runnerUsers.length > 0 ? { runnerUsers } : {},
|
|
7416
|
-
localWarnings: [
|
|
7417
|
-
...state.localWarnings,
|
|
7418
|
-
...gcWarning ? [gcWarning] : [],
|
|
7419
|
-
...ungranted.length > 0 ? [ungrantedRunnersWarning(ungranted)] : []
|
|
7420
|
-
]
|
|
8489
|
+
async function reconcile(ctx, desired) {
|
|
8490
|
+
const result = {
|
|
8491
|
+
appliedVersion: desired.version,
|
|
8492
|
+
warnings: [],
|
|
8493
|
+
restartedK3s: false,
|
|
8494
|
+
skippedKubeletFlags: [],
|
|
8495
|
+
effectiveImageGcHighThreshold: null,
|
|
8496
|
+
recreatedPriorityClass: false,
|
|
8497
|
+
prunedNamespaces: []
|
|
7421
8498
|
};
|
|
8499
|
+
const disallowedKinds = findDisallowedKinds(desired.rbacManifest);
|
|
8500
|
+
const manifestAccepted = disallowedKinds.length === 0;
|
|
8501
|
+
if (disallowedKinds.length > 0) {
|
|
8502
|
+
result.warnings.push(
|
|
8503
|
+
`Conveyor sent a cluster manifest containing disallowed kind(s) ${disallowedKinds.join(", ")} \u2014 refused. RBAC and PriorityClass steps skipped; this should never happen, report it.`
|
|
8504
|
+
);
|
|
8505
|
+
} else {
|
|
8506
|
+
await step(result, "Could not apply RBAC", () => kubectlApply(ctx.exec, desired.rbacManifest));
|
|
8507
|
+
await step(
|
|
8508
|
+
result,
|
|
8509
|
+
"Could not reconcile the PriorityClass",
|
|
8510
|
+
() => reconcilePriorityClass(ctx, desired, result)
|
|
8511
|
+
);
|
|
8512
|
+
}
|
|
8513
|
+
await step(
|
|
8514
|
+
result,
|
|
8515
|
+
"Could not apply namespace defaults",
|
|
8516
|
+
() => reconcileNamespaces(ctx, desired, result)
|
|
8517
|
+
);
|
|
8518
|
+
if (manifestAccepted) {
|
|
8519
|
+
await step(
|
|
8520
|
+
result,
|
|
8521
|
+
"Could not remove unused namespaces",
|
|
8522
|
+
() => pruneStaleNamespaces(ctx, desired, result)
|
|
8523
|
+
);
|
|
8524
|
+
}
|
|
8525
|
+
const registry = desired.localRegistry?.enabled === true;
|
|
8526
|
+
if (registry) {
|
|
8527
|
+
await step(result, "Could not reconcile the local image registry", async () => {
|
|
8528
|
+
await deployLocalRegistry(ctx);
|
|
8529
|
+
});
|
|
8530
|
+
}
|
|
8531
|
+
await step(
|
|
8532
|
+
result,
|
|
8533
|
+
"Could not apply kubelet settings",
|
|
8534
|
+
() => reconcileKubelet(ctx, desired, result)
|
|
8535
|
+
);
|
|
8536
|
+
if (registry && !result.restartedK3s) {
|
|
8537
|
+
await step(result, "Could not load the local image registry mirror", async () => {
|
|
8538
|
+
const loaded = await loadRegistryMirror(ctx);
|
|
8539
|
+
result.restartedK3s = loaded.restarted;
|
|
8540
|
+
if (loaded.warning) result.warnings.push(loaded.warning);
|
|
8541
|
+
});
|
|
8542
|
+
}
|
|
8543
|
+
return result;
|
|
7422
8544
|
}
|
|
7423
8545
|
|
|
7424
8546
|
// src/daemon/runner.ts
|
|
7425
|
-
var
|
|
7426
|
-
var DAY_MS = 24 *
|
|
8547
|
+
var HOUR_MS2 = 60 * 6e4;
|
|
8548
|
+
var DAY_MS = 24 * HOUR_MS2;
|
|
8549
|
+
var STORE_MEASURE_INTERVAL_MS = 55 * 6e4;
|
|
7427
8550
|
var DISK_PRESSURE_FREE_FRACTION = 0.2;
|
|
7428
8551
|
var STRANDED_PRUNE_BYTES = 20 * 1024 ** 3;
|
|
7429
8552
|
function flattenWanted(desired) {
|
|
@@ -7460,7 +8583,13 @@ var ClusterDaemon = class {
|
|
|
7460
8583
|
* the next reconcile silently clear them.
|
|
7461
8584
|
*/
|
|
7462
8585
|
warmWarnings = [];
|
|
8586
|
+
/** Content hash labels by image id, so each image is inspected once. */
|
|
8587
|
+
localImageHashes = /* @__PURE__ */ new Map();
|
|
8588
|
+
/** How long the in-cluster registry has been not ready, across beats. */
|
|
8589
|
+
registryWatch = { notReadySince: null };
|
|
7463
8590
|
timers = [];
|
|
8591
|
+
/** When the last store walk started, so one that failed is not retried on every reconcile. */
|
|
8592
|
+
storeWalkStartedAt = 0;
|
|
7464
8593
|
/** Serializes reconcile against itself — a sync nudge during a reconcile
|
|
7465
8594
|
* must queue, not interleave two kubectl applies of the same manifests. */
|
|
7466
8595
|
reconciling = null;
|
|
@@ -7480,7 +8609,7 @@ var ClusterDaemon = class {
|
|
|
7480
8609
|
}, PERSONAL_CLUSTER_HEARTBEAT_INTERVAL_MS),
|
|
7481
8610
|
setInterval(() => {
|
|
7482
8611
|
void this.warmAndPruneIfDue();
|
|
7483
|
-
},
|
|
8612
|
+
}, HOUR_MS2),
|
|
7484
8613
|
setInterval(() => {
|
|
7485
8614
|
void this.syncAndReconcile();
|
|
7486
8615
|
}, DAY_MS)
|
|
@@ -7489,10 +8618,13 @@ var ClusterDaemon = class {
|
|
|
7489
8618
|
await this.beat();
|
|
7490
8619
|
}
|
|
7491
8620
|
async stop() {
|
|
8621
|
+
this.stopped = true;
|
|
7492
8622
|
for (const timer of this.timers) clearInterval(timer);
|
|
7493
8623
|
this.timers = [];
|
|
7494
8624
|
await this.ports.connection.sayGoodbye();
|
|
7495
8625
|
this.ports.connection.disconnect();
|
|
8626
|
+
await this.beating;
|
|
8627
|
+
await this.persistWorkloads(void 0);
|
|
7496
8628
|
}
|
|
7497
8629
|
/** Fetch the desired state and converge to it. Queued, never concurrent. */
|
|
7498
8630
|
syncAndReconcile() {
|
|
@@ -7516,12 +8648,18 @@ var ClusterDaemon = class {
|
|
|
7516
8648
|
);
|
|
7517
8649
|
this.warnings = result.warnings;
|
|
7518
8650
|
for (const warning of result.warnings) this.ports.log(warning);
|
|
8651
|
+
if (result.prunedNamespaces.length > 0) {
|
|
8652
|
+
this.ports.log(
|
|
8653
|
+
`Removed ${result.prunedNamespaces.length} empty namespace(s) of projects this machine no longer serves: ${result.prunedNamespaces.join(", ")}.`
|
|
8654
|
+
);
|
|
8655
|
+
}
|
|
7519
8656
|
this.effectiveImageGcHigh = result.effectiveImageGcHighThreshold;
|
|
7520
8657
|
const state = this.state ?? await readState(this.ports.fs);
|
|
7521
8658
|
this.state = { ...state, appliedDesiredStateVersion: result.appliedVersion };
|
|
7522
8659
|
await writeState(this.ports.fs, this.state);
|
|
7523
8660
|
await this.warm();
|
|
7524
8661
|
await this.pruneIfDue();
|
|
8662
|
+
await this.measureStoreIfDue();
|
|
7525
8663
|
}
|
|
7526
8664
|
/**
|
|
7527
8665
|
* The hourly clock: warm, re-pin, and reclaim if the pile justifies it.
|
|
@@ -7534,6 +8672,45 @@ var ClusterDaemon = class {
|
|
|
7534
8672
|
async warmAndPruneIfDue() {
|
|
7535
8673
|
await this.warm();
|
|
7536
8674
|
await this.pruneIfDue();
|
|
8675
|
+
await this.measureStoreIfDue();
|
|
8676
|
+
if (this.desired?.localRegistry?.enabled !== true) return;
|
|
8677
|
+
const lastRegistryGcAt = await runRegistryUpkeep(
|
|
8678
|
+
{
|
|
8679
|
+
exec: this.ports.exec,
|
|
8680
|
+
fs: this.ports.fs,
|
|
8681
|
+
log: this.ports.log,
|
|
8682
|
+
requestDesiredState: () => this.ports.connection.requestDesiredState(),
|
|
8683
|
+
reconcile: () => this.syncAndReconcile(),
|
|
8684
|
+
watch: this.registryWatch,
|
|
8685
|
+
fetch: this.ports.fetch
|
|
8686
|
+
},
|
|
8687
|
+
this.state?.lastRegistryGcAt ?? null,
|
|
8688
|
+
this.desired.maintenance.hour
|
|
8689
|
+
);
|
|
8690
|
+
if (lastRegistryGcAt === (this.state?.lastRegistryGcAt ?? null)) return;
|
|
8691
|
+
const state = this.state ?? await readState(this.ports.fs);
|
|
8692
|
+
this.state = { ...state, lastRegistryGcAt };
|
|
8693
|
+
await writeState(this.ports.fs, this.state);
|
|
8694
|
+
}
|
|
8695
|
+
/**
|
|
8696
|
+
* Measure k3s's containerd store when the last measurement is old enough,
|
|
8697
|
+
* and keep the result for the heartbeat. After a prune, so the number it
|
|
8698
|
+
* reports is what the prune left. A failed measurement keeps the last one.
|
|
8699
|
+
*/
|
|
8700
|
+
async measureStoreIfDue() {
|
|
8701
|
+
const measured = this.state?.k3sStoreMeasuredAt ? Date.parse(this.state.k3sStoreMeasuredAt) : 0;
|
|
8702
|
+
const last = Math.max(measured, this.storeWalkStartedAt);
|
|
8703
|
+
if (Date.now() - last < STORE_MEASURE_INTERVAL_MS) return;
|
|
8704
|
+
this.storeWalkStartedAt = Date.now();
|
|
8705
|
+
const bytes = await measureK3sStore(this.ports.exec);
|
|
8706
|
+
if (bytes === null) return;
|
|
8707
|
+
const state = this.state ?? await readState(this.ports.fs);
|
|
8708
|
+
this.state = { ...state, k3sStoreBytes: bytes, k3sStoreMeasuredAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
8709
|
+
await writeState(this.ports.fs, this.state);
|
|
8710
|
+
}
|
|
8711
|
+
/** The store size the heartbeat reports: the last measurement, null before one. */
|
|
8712
|
+
get measuredStoreBytes() {
|
|
8713
|
+
return this.state?.k3sStoreBytes ?? null;
|
|
7537
8714
|
}
|
|
7538
8715
|
/** Pre-pull the wanted refs. Silent on success; that is the normal case. */
|
|
7539
8716
|
async warm() {
|
|
@@ -7638,8 +8815,14 @@ var ClusterDaemon = class {
|
|
|
7638
8815
|
async buildHeartbeat() {
|
|
7639
8816
|
const { wantedImages, authRequiredHosts } = this.desired ? flattenWanted(this.desired) : { wantedImages: [], authRequiredHosts: [] };
|
|
7640
8817
|
const inventory = await readImageInventory(this.ports.exec).catch(() => null);
|
|
8818
|
+
const registryOn = this.desired?.localRegistry?.enabled === true;
|
|
8819
|
+
if (!registryOn) this.registryWatch.notReadySince = null;
|
|
7641
8820
|
return await collectHeartbeat(
|
|
7642
|
-
{
|
|
8821
|
+
{
|
|
8822
|
+
exec: this.ports.exec,
|
|
8823
|
+
readFile: (path) => this.ports.fs.readFile(path),
|
|
8824
|
+
fetch: this.ports.fetch
|
|
8825
|
+
},
|
|
7643
8826
|
{
|
|
7644
8827
|
cliVersion: this.ports.cliVersion,
|
|
7645
8828
|
appliedDesiredStateVersion: this.state?.appliedDesiredStateVersion ?? 0,
|
|
@@ -7649,15 +8832,41 @@ var ClusterDaemon = class {
|
|
|
7649
8832
|
lastWarmAt: this.state?.lastWarmAt ?? null,
|
|
7650
8833
|
lastUpdateAt: this.state?.lastUpdateAt ?? null,
|
|
7651
8834
|
localWarnings: [...this.warnings, ...this.warmWarnings],
|
|
7652
|
-
imageGcHighThreshold: this.effectiveImageGcHigh
|
|
8835
|
+
imageGcHighThreshold: this.effectiveImageGcHigh,
|
|
8836
|
+
localImageHashes: this.localImageHashes,
|
|
8837
|
+
...registryOn ? { localRegistry: this.registryWatch } : {},
|
|
8838
|
+
k3sStoreBytes: this.measuredStoreBytes
|
|
7653
8839
|
},
|
|
7654
8840
|
inventory
|
|
7655
8841
|
);
|
|
7656
8842
|
}
|
|
7657
|
-
async
|
|
8843
|
+
async persistWorkloads(data) {
|
|
8844
|
+
try {
|
|
8845
|
+
await this.ports.writeWorkloads?.(data);
|
|
8846
|
+
} catch (error) {
|
|
8847
|
+
this.ports.log(
|
|
8848
|
+
`Pod context cache unavailable: ${error instanceof Error ? error.message : String(error)}`
|
|
8849
|
+
);
|
|
8850
|
+
}
|
|
8851
|
+
}
|
|
8852
|
+
beating;
|
|
8853
|
+
stopped = false;
|
|
8854
|
+
beat() {
|
|
8855
|
+
if (this.stopped) return Promise.resolve();
|
|
8856
|
+
this.beating ??= this.runBeat().finally(() => {
|
|
8857
|
+
this.beating = void 0;
|
|
8858
|
+
});
|
|
8859
|
+
return this.beating;
|
|
8860
|
+
}
|
|
8861
|
+
async runBeat() {
|
|
7658
8862
|
try {
|
|
7659
|
-
this.ports.connection.sendHeartbeat(
|
|
8863
|
+
const workloads = await this.ports.connection.sendHeartbeat({
|
|
8864
|
+
...await this.buildHeartbeat(),
|
|
8865
|
+
observedPods: await readPodIdentities(this.ports.exec).catch(() => [])
|
|
8866
|
+
});
|
|
8867
|
+
if (!this.stopped) await this.persistWorkloads(workloads);
|
|
7660
8868
|
} catch (error) {
|
|
8869
|
+
await this.persistWorkloads(void 0);
|
|
7661
8870
|
this.ports.log(`Heartbeat failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
7662
8871
|
}
|
|
7663
8872
|
}
|
|
@@ -7774,8 +8983,9 @@ var nodeFileSystem = {
|
|
|
7774
8983
|
async removeFile(path) {
|
|
7775
8984
|
await rm(path, { force: true });
|
|
7776
8985
|
},
|
|
7777
|
-
async ensureDir(path) {
|
|
8986
|
+
async ensureDir(path, mode) {
|
|
7778
8987
|
await mkdir(path, { recursive: true });
|
|
8988
|
+
if (mode !== void 0) await chmod(path, mode);
|
|
7779
8989
|
}
|
|
7780
8990
|
};
|
|
7781
8991
|
function createMemoryFileSystem(seed = {}) {
|
|
@@ -7797,7 +9007,10 @@ function createMemoryFileSystem(seed = {}) {
|
|
|
7797
9007
|
removed.push(path);
|
|
7798
9008
|
return Promise.resolve();
|
|
7799
9009
|
},
|
|
7800
|
-
ensureDir: () =>
|
|
9010
|
+
ensureDir: (path, mode) => {
|
|
9011
|
+
if (mode !== void 0) modes.set(path, mode);
|
|
9012
|
+
return Promise.resolve();
|
|
9013
|
+
}
|
|
7801
9014
|
};
|
|
7802
9015
|
}
|
|
7803
9016
|
|
|
@@ -7858,16 +9071,22 @@ Commands:
|
|
|
7858
9071
|
--username <user>
|
|
7859
9072
|
--password-stdin read the password from stdin
|
|
7860
9073
|
--no-restart skip the required k3s restart
|
|
9074
|
+
registry gc Delete superseded shadow-bake images from the image
|
|
9075
|
+
registry inside this cluster. Runs daily on its own,
|
|
9076
|
+
and only while no Conveyor pod or bake is running.
|
|
9077
|
+
--dry-run list what it would delete
|
|
7861
9078
|
runners list Which GitHub Actions runners this machine hosts, and
|
|
7862
9079
|
whether each may import a locally baked image.
|
|
7863
9080
|
runners grant <user>\u2026 Let those runner users import a locally baked image
|
|
7864
9081
|
into this node and pin it. Writes one file under
|
|
7865
9082
|
/etc/sudoers.d, checked with visudo first. Needed only
|
|
7866
9083
|
when a project has Bake Locally turned on.
|
|
7867
|
-
uninstall Remove Conveyor's units, environment, and
|
|
7868
|
-
k3s config, registries.yaml
|
|
7869
|
-
|
|
7870
|
-
|
|
9084
|
+
uninstall Remove Conveyor's units, environment, state, and image
|
|
9085
|
+
registry. Your k3s config, your own registries.yaml
|
|
9086
|
+
entries, your workloads, and the registry's data are
|
|
9087
|
+
left alone.
|
|
9088
|
+
--purge-k3s --yes also remove k3s itself and the
|
|
9089
|
+
registry's data
|
|
7871
9090
|
daemon Run in the foreground (what the systemd unit calls).
|
|
7872
9091
|
|
|
7873
9092
|
Options:
|
|
@@ -7886,17 +9105,12 @@ async function registryAdd(ports, options) {
|
|
|
7886
9105
|
);
|
|
7887
9106
|
}
|
|
7888
9107
|
const raw = await ports.fs.readFile(REGISTRIES_FILE);
|
|
7889
|
-
const parsed = parseRegistries(raw);
|
|
7890
|
-
let backupPath = null;
|
|
7891
|
-
if (raw !== null) {
|
|
7892
|
-
backupPath = `${REGISTRIES_FILE}.bak-${ports.now().toISOString().replace(/[:.]/g, "-")}`;
|
|
7893
|
-
await ports.fs.writeFile(backupPath, raw, 384);
|
|
7894
|
-
}
|
|
7895
9108
|
const auth = { username: options.username, password: options.password };
|
|
7896
|
-
await
|
|
7897
|
-
|
|
7898
|
-
|
|
7899
|
-
|
|
9109
|
+
const backupPath = await writeRegistriesWithBackup(
|
|
9110
|
+
ports.fs,
|
|
9111
|
+
raw,
|
|
9112
|
+
upsertRegistryAuth(parseRegistries(raw), host, auth),
|
|
9113
|
+
ports.now()
|
|
7900
9114
|
);
|
|
7901
9115
|
if (options.noRestart) {
|
|
7902
9116
|
ports.log(`Added credentials for ${host}. Restart k3s to pick them up: systemctl restart k3s`);
|
|
@@ -7935,24 +9149,73 @@ function renderRegistryAuthSummary(missing) {
|
|
|
7935
9149
|
}
|
|
7936
9150
|
|
|
7937
9151
|
// src/commands/uninstall.ts
|
|
9152
|
+
async function removeLocalRegistry(ports) {
|
|
9153
|
+
const removed = await ports.exec(
|
|
9154
|
+
"k3s",
|
|
9155
|
+
[
|
|
9156
|
+
"kubectl",
|
|
9157
|
+
"-n",
|
|
9158
|
+
PERSONAL_LOCAL_REGISTRY_NAMESPACE,
|
|
9159
|
+
"delete",
|
|
9160
|
+
"deployment,service,networkpolicy",
|
|
9161
|
+
PERSONAL_LOCAL_REGISTRY_NAME,
|
|
9162
|
+
"--ignore-not-found"
|
|
9163
|
+
],
|
|
9164
|
+
{ timeoutMs: 12e4 }
|
|
9165
|
+
);
|
|
9166
|
+
if (removed.exitCode !== 0) {
|
|
9167
|
+
ports.log(
|
|
9168
|
+
`Could not remove the local image registry from the cluster: ${removed.stderr.trim().slice(0, 200)}`
|
|
9169
|
+
);
|
|
9170
|
+
}
|
|
9171
|
+
const raw = await ports.fs.readFile(REGISTRIES_FILE);
|
|
9172
|
+
const parsed = parseRegistries(raw);
|
|
9173
|
+
if (mirrorEndpoints(parsed, PERSONAL_LOCAL_REGISTRY_HOST).length === 0) return;
|
|
9174
|
+
const backup = await writeRegistriesWithBackup(
|
|
9175
|
+
ports.fs,
|
|
9176
|
+
raw,
|
|
9177
|
+
removeRegistryMirror(parsed, PERSONAL_LOCAL_REGISTRY_HOST),
|
|
9178
|
+
(ports.now ?? (() => /* @__PURE__ */ new Date()))()
|
|
9179
|
+
);
|
|
9180
|
+
ports.log(
|
|
9181
|
+
`Removed the local image registry's mirror from ${REGISTRIES_FILE} (backup: ${backup}).`
|
|
9182
|
+
);
|
|
9183
|
+
}
|
|
7938
9184
|
async function uninstall(ports, options = {}) {
|
|
7939
9185
|
const result = { stoppedUnits: [], removedFiles: [], purgedK3s: false };
|
|
9186
|
+
if (options.purgeK3s && !options.confirmed) {
|
|
9187
|
+
throw new Error(
|
|
9188
|
+
"--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."
|
|
9189
|
+
);
|
|
9190
|
+
}
|
|
7940
9191
|
for (const unit of ALL_UNITS) {
|
|
7941
9192
|
await ports.exec("systemctl", ["disable", "--now", unit]);
|
|
7942
9193
|
result.stoppedUnits.push(unit);
|
|
7943
9194
|
}
|
|
9195
|
+
await removeLocalRegistry(ports);
|
|
7944
9196
|
for (const path of OWNED_FILES) {
|
|
7945
9197
|
await ports.fs.removeFile(path);
|
|
7946
9198
|
result.removedFiles.push(path);
|
|
7947
9199
|
}
|
|
7948
|
-
await ports.exec("
|
|
9200
|
+
await ports.exec("find", [
|
|
9201
|
+
STATE_DIR,
|
|
9202
|
+
"-mindepth",
|
|
9203
|
+
"1",
|
|
9204
|
+
"-maxdepth",
|
|
9205
|
+
"1",
|
|
9206
|
+
"!",
|
|
9207
|
+
"-path",
|
|
9208
|
+
LOCAL_REGISTRY_DATA_DIR,
|
|
9209
|
+
"-exec",
|
|
9210
|
+
"rm",
|
|
9211
|
+
"-rf",
|
|
9212
|
+
"--",
|
|
9213
|
+
"{}",
|
|
9214
|
+
"+"
|
|
9215
|
+
]);
|
|
7949
9216
|
await ports.exec("systemctl", ["daemon-reload"]);
|
|
9217
|
+
await ports.exec("systemctl", ["restart", "systemd-journald"]).catch(() => void 0);
|
|
7950
9218
|
if (options.purgeK3s) {
|
|
7951
|
-
if (!options.confirmed) {
|
|
7952
|
-
throw new Error(
|
|
7953
|
-
"--purge-k3s removes k3s and everything running on it. Re-run with --yes if that is what you want."
|
|
7954
|
-
);
|
|
7955
|
-
}
|
|
7956
9219
|
const purge = await ports.exec("/usr/local/bin/k3s-uninstall.sh", [], {
|
|
7957
9220
|
timeoutMs: 10 * 6e4
|
|
7958
9221
|
});
|
|
@@ -7962,10 +9225,18 @@ async function uninstall(ports, options = {}) {
|
|
|
7962
9225
|
"k3s-uninstall.sh did not complete. k3s is still installed; remove it by hand if you want it gone."
|
|
7963
9226
|
);
|
|
7964
9227
|
}
|
|
9228
|
+
await ports.exec("rm", ["-rf", STATE_DIR]);
|
|
7965
9229
|
}
|
|
7966
|
-
ports.log("Removed Conveyor's units, environment files, and
|
|
7967
|
-
ports.log(
|
|
9230
|
+
ports.log("Removed Conveyor's units, environment files, state, and local image registry.");
|
|
9231
|
+
ports.log(
|
|
9232
|
+
"Your k3s config, your own registries.yaml entries, and your workloads were not touched."
|
|
9233
|
+
);
|
|
7968
9234
|
if (options.purgeK3s && result.purgedK3s) ports.log("k3s was removed.");
|
|
9235
|
+
if (!options.purgeK3s) {
|
|
9236
|
+
ports.log(
|
|
9237
|
+
`The local image registry's data is still at ${LOCAL_REGISTRY_DATA_DIR}. Delete that directory to reclaim its disk space.`
|
|
9238
|
+
);
|
|
9239
|
+
}
|
|
7969
9240
|
if (!options.serverInitiated) {
|
|
7970
9241
|
ports.log(
|
|
7971
9242
|
"Now click Disconnect in your Conveyor profile so the tunnel and DNS record are removed too."
|
|
@@ -7977,12 +9248,12 @@ async function uninstall(ports, options = {}) {
|
|
|
7977
9248
|
// src/commands/update.ts
|
|
7978
9249
|
var CLOUDFLARED_UPDATED_EXIT = 11;
|
|
7979
9250
|
function readExecStart(unit) {
|
|
7980
|
-
const
|
|
7981
|
-
const start =
|
|
9251
|
+
const lines2 = unit.split("\n");
|
|
9252
|
+
const start = lines2.findIndex((line) => line.trimStart().startsWith("ExecStart="));
|
|
7982
9253
|
if (start < 0) return "";
|
|
7983
9254
|
const collected = [];
|
|
7984
|
-
for (let index = start; index <
|
|
7985
|
-
const line =
|
|
9255
|
+
for (let index = start; index < lines2.length; index += 1) {
|
|
9256
|
+
const line = lines2[index] ?? "";
|
|
7986
9257
|
collected.push(line);
|
|
7987
9258
|
if (!line.trimEnd().endsWith("\\")) break;
|
|
7988
9259
|
}
|
|
@@ -8124,12 +9395,75 @@ function meetsK3sMinimum(version) {
|
|
|
8124
9395
|
// src/vm/network.ts
|
|
8125
9396
|
import { networkInterfaces } from "os";
|
|
8126
9397
|
import { resolve4 as systemResolve4, resolveSrv as systemResolveSrv } from "dns/promises";
|
|
9398
|
+
|
|
9399
|
+
// src/vm/endpoints.ts
|
|
9400
|
+
var CONTROL_PLANE_HOSTS = ["api.conveyor.rallycryapp.com", "conveyor-api.rallycryapp.com"];
|
|
9401
|
+
var DEFAULT_ENDPOINTS = [
|
|
9402
|
+
"archive.ubuntu.com",
|
|
9403
|
+
"security.ubuntu.com",
|
|
9404
|
+
"ports.ubuntu.com",
|
|
9405
|
+
"registry.npmjs.org",
|
|
9406
|
+
// Font Awesome Pro tarballs through Conveyor's Artifact Registry cache.
|
|
9407
|
+
"us-central1-npm.pkg.dev",
|
|
9408
|
+
// Tiptap Pro packages, which every claudespace bake installs.
|
|
9409
|
+
"registry.tiptap.dev",
|
|
9410
|
+
// The Python venv a pod builds at runtime.
|
|
9411
|
+
"pypi.org",
|
|
9412
|
+
"files.pythonhosted.org",
|
|
9413
|
+
"bun.sh",
|
|
9414
|
+
"get.k3s.io",
|
|
9415
|
+
"update.k3s.io",
|
|
9416
|
+
"github.com",
|
|
9417
|
+
"api.github.com",
|
|
9418
|
+
"raw.githubusercontent.com",
|
|
9419
|
+
"release-assets.githubusercontent.com",
|
|
9420
|
+
"objects.githubusercontent.com",
|
|
9421
|
+
"ghcr.io",
|
|
9422
|
+
"pkg-containers.githubusercontent.com",
|
|
9423
|
+
// The crane image that shadow bakes' state-bake and publish Jobs run. gcr.io
|
|
9424
|
+
// serves its blobs from the same host (/artifacts-downloads), on Google's
|
|
9425
|
+
// shared frontend addresses like us-central1-npm.pkg.dev.
|
|
9426
|
+
"gcr.io",
|
|
9427
|
+
// The first shadow bake on a VM builds the toolchain base inside it; base
|
|
9428
|
+
// reuse never crosses machines. These are the hosts that build downloads
|
|
9429
|
+
// from. Several sit on shared CDN addresses.
|
|
9430
|
+
// Plain HTTP too: the first apt-get runs before ca-certificates exists.
|
|
9431
|
+
"deb.debian.org",
|
|
9432
|
+
"www.postgresql.org",
|
|
9433
|
+
"apt.postgresql.org",
|
|
9434
|
+
"cli.github.com",
|
|
9435
|
+
"johnvansickle.com",
|
|
9436
|
+
"astral.sh",
|
|
9437
|
+
// uv downloads CPython from here.
|
|
9438
|
+
"releases.astral.sh",
|
|
9439
|
+
"opencode.ai",
|
|
9440
|
+
"cdn.playwright.dev",
|
|
9441
|
+
"playwright.download.prss.microsoft.com",
|
|
9442
|
+
"registry-1.docker.io",
|
|
9443
|
+
"auth.docker.io",
|
|
9444
|
+
"production.cloudflare.docker.com",
|
|
9445
|
+
"production.cloudfront.docker.com",
|
|
9446
|
+
"api.cloudflare.com",
|
|
9447
|
+
"conveyor.rallycryapp.com",
|
|
9448
|
+
"conveyor-api.rallycryapp.com",
|
|
9449
|
+
"api.conveyor.rallycryapp.com",
|
|
9450
|
+
"api.anthropic.com",
|
|
9451
|
+
"api.openai.com",
|
|
9452
|
+
"chatgpt.com"
|
|
9453
|
+
].map((host) => ({
|
|
9454
|
+
host,
|
|
9455
|
+
ports: host.endsWith(".ubuntu.com") || host === "deb.debian.org" ? [80, 443] : [443]
|
|
9456
|
+
}));
|
|
9457
|
+
|
|
9458
|
+
// src/vm/network.ts
|
|
8127
9459
|
var WORKER_SUBNET = "192.168.5.0/24";
|
|
8128
9460
|
var WORKER_IP = "192.168.5.15";
|
|
8129
9461
|
var MAX_NETWORK_ENDPOINTS = 100;
|
|
8130
9462
|
var MAX_PORTS_PER_ENDPOINT = 16;
|
|
8131
9463
|
var MAX_NETWORK_FORWARDS = 100;
|
|
8132
9464
|
var MAX_CLOUDFLARE_EDGE_ADDRESSES = 4;
|
|
9465
|
+
var MAX_CONTROL_ALIASES = 8;
|
|
9466
|
+
var ARGOTUNNEL = /^(?:us-)?region[12]\.v2\.argotunnel\.com$/;
|
|
8133
9467
|
var DOMAIN = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/i;
|
|
8134
9468
|
function parseIPv4(address) {
|
|
8135
9469
|
const parts = address.split(".");
|
|
@@ -8196,6 +9530,36 @@ function validatePorts(ports, host) {
|
|
|
8196
9530
|
function syntheticIp(index) {
|
|
8197
9531
|
return `192.168.5.${100 + index}`;
|
|
8198
9532
|
}
|
|
9533
|
+
function pinAddresses(name, answers, localAddresses) {
|
|
9534
|
+
if (!Array.isArray(answers) || answers.length === 0) {
|
|
9535
|
+
throw new Error(`No IPv4 address resolved for ${name}`);
|
|
9536
|
+
}
|
|
9537
|
+
const uniqueAnswers = [...new Set(answers)];
|
|
9538
|
+
const unsafe = uniqueAnswers.find(
|
|
9539
|
+
(address) => !isGloballyRoutableIPv4(address) || localAddresses.has(address)
|
|
9540
|
+
);
|
|
9541
|
+
if (unsafe) {
|
|
9542
|
+
throw new Error(`Refusing unsafe IPv4 answer for ${name}: ${unsafe}`);
|
|
9543
|
+
}
|
|
9544
|
+
return ARGOTUNNEL.test(name) ? uniqueAnswers.slice(0, MAX_CLOUDFLARE_EDGE_ADDRESSES) : uniqueAnswers.slice(0, 1);
|
|
9545
|
+
}
|
|
9546
|
+
function forwardCount(entries) {
|
|
9547
|
+
return entries.reduce((sum, entry) => sum + entry.ports.length, 0);
|
|
9548
|
+
}
|
|
9549
|
+
function controlAliases(hosts) {
|
|
9550
|
+
return CONTROL_PLANE_HOSTS.slice(0, MAX_CONTROL_ALIASES).flatMap((name, index) => {
|
|
9551
|
+
const host = hosts.find((entry) => entry.name === name && entry.ports.includes(443));
|
|
9552
|
+
return host ? [
|
|
9553
|
+
{
|
|
9554
|
+
name,
|
|
9555
|
+
guestIp: `192.168.5.${240 + index}`,
|
|
9556
|
+
address: host.address,
|
|
9557
|
+
ports: [443],
|
|
9558
|
+
reserve: "control"
|
|
9559
|
+
}
|
|
9560
|
+
] : [];
|
|
9561
|
+
});
|
|
9562
|
+
}
|
|
8199
9563
|
async function resolveNetwork(endpoints, lookup = systemResolve4) {
|
|
8200
9564
|
if (!Array.isArray(endpoints) || endpoints.length > MAX_NETWORK_ENDPOINTS) {
|
|
8201
9565
|
throw new Error(`Too many network endpoints (maximum ${MAX_NETWORK_ENDPOINTS})`);
|
|
@@ -8219,28 +9583,27 @@ async function resolveNetwork(endpoints, lookup = systemResolve4) {
|
|
|
8219
9583
|
const localAddresses = localIPv4Addresses();
|
|
8220
9584
|
const hosts = [];
|
|
8221
9585
|
for (const [name, ports] of merged) {
|
|
8222
|
-
const
|
|
8223
|
-
|
|
8224
|
-
|
|
8225
|
-
|
|
8226
|
-
|
|
8227
|
-
|
|
8228
|
-
|
|
8229
|
-
|
|
8230
|
-
|
|
8231
|
-
throw new Error(`Refusing unsafe IPv4 answer for ${name}: ${unsafe}`);
|
|
8232
|
-
}
|
|
8233
|
-
const addresses = /^(?:us-)?region[12]\.v2\.argotunnel\.com$/.test(name) ? uniqueAnswers.slice(0, MAX_CLOUDFLARE_EDGE_ADDRESSES) : uniqueAnswers.slice(0, 1);
|
|
8234
|
-
for (const address of addresses) {
|
|
8235
|
-
hosts.push({ name, guestIp: syntheticIp(hosts.length), address, ports });
|
|
9586
|
+
const tunnel = ARGOTUNNEL.test(name);
|
|
9587
|
+
for (const address of pinAddresses(name, await lookup(name), localAddresses)) {
|
|
9588
|
+
hosts.push({
|
|
9589
|
+
name,
|
|
9590
|
+
guestIp: syntheticIp(hosts.length),
|
|
9591
|
+
address,
|
|
9592
|
+
ports,
|
|
9593
|
+
...tunnel ? { reserve: "control" } : {}
|
|
9594
|
+
});
|
|
8236
9595
|
}
|
|
8237
9596
|
}
|
|
8238
|
-
|
|
9597
|
+
const control = controlAliases(hosts);
|
|
9598
|
+
if (forwardCount(hosts) + forwardCount(control) > MAX_NETWORK_FORWARDS) {
|
|
9599
|
+
throw new Error(`Too many network forwarding rules (maximum ${MAX_NETWORK_FORWARDS})`);
|
|
9600
|
+
}
|
|
9601
|
+
return { hosts, srv: cloudflaredSrvRecords(hosts), ...control.length > 0 ? { control } : {} };
|
|
8239
9602
|
}
|
|
8240
9603
|
function cloudflaredSrvRecords(hosts) {
|
|
8241
9604
|
const targets = [
|
|
8242
9605
|
...new Map(
|
|
8243
|
-
hosts.filter((host) =>
|
|
9606
|
+
hosts.filter((host) => ARGOTUNNEL.test(host.name)).map((host) => [host.name, host])
|
|
8244
9607
|
).values()
|
|
8245
9608
|
];
|
|
8246
9609
|
if (targets.length === 0) return [];
|
|
@@ -8449,6 +9812,94 @@ while True:
|
|
|
8449
9812
|
udp.sendto(response, peer)
|
|
8450
9813
|
`;
|
|
8451
9814
|
|
|
9815
|
+
// src/vm/instance-name.ts
|
|
9816
|
+
var VM_NAME = "conveyor";
|
|
9817
|
+
var DEFAULT_INSTANCE = "default";
|
|
9818
|
+
var NAMED_INSTANCE = /^(?=.{1,12}$)[a-z](?:-?[a-z0-9])*$/;
|
|
9819
|
+
function isNamedInstance(value) {
|
|
9820
|
+
return typeof value === "string" && NAMED_INSTANCE.test(value) && value !== DEFAULT_INSTANCE && value !== VM_NAME;
|
|
9821
|
+
}
|
|
9822
|
+
function parseInstanceName(value) {
|
|
9823
|
+
if (value === void 0 || value === DEFAULT_INSTANCE) return DEFAULT_INSTANCE;
|
|
9824
|
+
if (value === true) throw new Error("Use --instance=<name>.");
|
|
9825
|
+
if (value === VM_NAME)
|
|
9826
|
+
throw new Error(`"${VM_NAME}" is reserved for the default VM. Omit --instance to use it.`);
|
|
9827
|
+
if (!isNamedInstance(value))
|
|
9828
|
+
throw new Error(
|
|
9829
|
+
"Use an instance name of 1-12 lowercase letters, digits, or single hyphens, starting with a letter."
|
|
9830
|
+
);
|
|
9831
|
+
return value;
|
|
9832
|
+
}
|
|
9833
|
+
function vmNameFor(instance) {
|
|
9834
|
+
return instance === DEFAULT_INSTANCE ? VM_NAME : instance;
|
|
9835
|
+
}
|
|
9836
|
+
|
|
9837
|
+
// src/vm/public-web.ts
|
|
9838
|
+
import { resolve4 } from "dns/promises";
|
|
9839
|
+
import { networkInterfaces as networkInterfaces2 } from "os";
|
|
9840
|
+
var PUBLIC_WEB_IP = "192.168.5.250";
|
|
9841
|
+
var PUBLIC_WEB_PORT = 3128;
|
|
9842
|
+
var PUBLIC_WEB_URL = `http://${PUBLIC_WEB_IP}:${PUBLIC_WEB_PORT}`;
|
|
9843
|
+
|
|
9844
|
+
// src/vm/qemu-forwards.ts
|
|
9845
|
+
var ENDPOINT_IP = /^192\.168\.5\.(?:1\d\d)$/;
|
|
9846
|
+
var CONTROL_ALIAS_IP2 = /^192\.168\.5\.24[0-7]$/;
|
|
9847
|
+
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}$/;
|
|
9848
|
+
function assertPort(port) {
|
|
9849
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
9850
|
+
throw new Error("Invalid guest forwarding port");
|
|
9851
|
+
}
|
|
9852
|
+
function validateForwards(forwards) {
|
|
9853
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9854
|
+
for (const forward of forwards) {
|
|
9855
|
+
assertPort(forward.port);
|
|
9856
|
+
const key = `${forward.guestIp}:${forward.port}`;
|
|
9857
|
+
if (seen.has(key)) throw new Error("Duplicate QEMU guest forwarding rule");
|
|
9858
|
+
seen.add(key);
|
|
9859
|
+
if (!isGloballyRoutableIPv4(forward.address) || !PINNED_IPV4.test(forward.address))
|
|
9860
|
+
throw new Error("Invalid pinned forwarding address");
|
|
9861
|
+
}
|
|
9862
|
+
}
|
|
9863
|
+
function forwardsOf(entries, guestIps, control) {
|
|
9864
|
+
return entries.flatMap((host) => {
|
|
9865
|
+
if (!guestIps.test(host.guestIp) || control && host.reserve !== "control")
|
|
9866
|
+
throw new Error("Invalid pinned forwarding address");
|
|
9867
|
+
return host.ports.map((port) => ({
|
|
9868
|
+
guestIp: host.guestIp,
|
|
9869
|
+
address: host.address,
|
|
9870
|
+
port,
|
|
9871
|
+
...host.reserve === "control" ? { reserve: "control" } : {}
|
|
9872
|
+
}));
|
|
9873
|
+
});
|
|
9874
|
+
}
|
|
9875
|
+
function qemuForwards(network) {
|
|
9876
|
+
const forwards = [
|
|
9877
|
+
...forwardsOf(network.hosts, ENDPOINT_IP, false),
|
|
9878
|
+
...forwardsOf(network.control ?? [], CONTROL_ALIAS_IP2, true)
|
|
9879
|
+
];
|
|
9880
|
+
if (forwards.length > MAX_NETWORK_FORWARDS) throw new Error("Too many QEMU forwarding rules");
|
|
9881
|
+
validateForwards(forwards);
|
|
9882
|
+
return forwards;
|
|
9883
|
+
}
|
|
9884
|
+
function restrictedUsernet(network, forwards, ncPath) {
|
|
9885
|
+
const guestForwards = forwards.map(
|
|
9886
|
+
(forward) => `guestfwd=tcp:${forward.guestIp}:${forward.port}-cmd:${ncPath} -4 -n -w 60 ${forward.address} ${forward.port}${forward.reserve === "control" ? " control" : ""}`
|
|
9887
|
+
);
|
|
9888
|
+
return [
|
|
9889
|
+
"user",
|
|
9890
|
+
"id=net0",
|
|
9891
|
+
`net=${WORKER_SUBNET}`,
|
|
9892
|
+
`dhcpstart=${WORKER_IP}`,
|
|
9893
|
+
"restrict=on",
|
|
9894
|
+
"ipv6=off",
|
|
9895
|
+
`hostfwd=tcp:127.0.0.1:${network.sshPort}-:22`,
|
|
9896
|
+
...guestForwards,
|
|
9897
|
+
...network.publicWeb === true ? [
|
|
9898
|
+
`guestfwd=tcp:${PUBLIC_WEB_IP}:${PUBLIC_WEB_PORT}-cmd:${ncPath} -4 -n -w 60 public-web ${PUBLIC_WEB_PORT}`
|
|
9899
|
+
] : []
|
|
9900
|
+
].join(",");
|
|
9901
|
+
}
|
|
9902
|
+
|
|
8452
9903
|
// src/vm/qemu-policy.ts
|
|
8453
9904
|
var ALLOWED_PROBES = new Set(
|
|
8454
9905
|
[
|
|
@@ -8502,9 +9953,7 @@ var REQUIRED_CONTROLS = [
|
|
|
8502
9953
|
"drive:cidata"
|
|
8503
9954
|
];
|
|
8504
9955
|
var DISALLOWED_CONTROLS = /* @__PURE__ */ new Set(["-sandbox", "-no-user-config", "-nodefaults"]);
|
|
8505
|
-
|
|
8506
|
-
var BOOT_DISK_IOPS = "2000";
|
|
8507
|
-
function assertPort(port, field) {
|
|
9956
|
+
function assertPort2(port, field) {
|
|
8508
9957
|
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`Invalid ${field}`);
|
|
8509
9958
|
}
|
|
8510
9959
|
function isProbe(args) {
|
|
@@ -8625,10 +10074,11 @@ function validateDrive(value, context) {
|
|
|
8625
10074
|
}
|
|
8626
10075
|
assertFields(fields, { file: /.+/, if: "none", discard: "on", id: "boot-disk" }, "boot drive");
|
|
8627
10076
|
assertInstancePath(path, "disk", context);
|
|
8628
|
-
|
|
8629
|
-
|
|
8630
|
-
|
|
10077
|
+
const disk = context.disk ?? {
|
|
10078
|
+
bps: LEGACY_CAPACITY_LIMITS.diskBytesPerSecond,
|
|
10079
|
+
iops: LEGACY_CAPACITY_LIMITS.diskIops
|
|
8631
10080
|
};
|
|
10081
|
+
return { kind: "boot", value: `${value},bps=${disk.bps},iops=${disk.iops}` };
|
|
8632
10082
|
}
|
|
8633
10083
|
if (fields.get("id") === "cdrom0") {
|
|
8634
10084
|
assertFields(
|
|
@@ -8673,35 +10123,6 @@ function parseOriginalUsernet(value, sshPort) {
|
|
|
8673
10123
|
"Lima QEMU user network"
|
|
8674
10124
|
);
|
|
8675
10125
|
}
|
|
8676
|
-
function restrictedUsernet(network, forwards, ncPath) {
|
|
8677
|
-
const guestForwards = forwards.map(
|
|
8678
|
-
(forward) => `guestfwd=tcp:${forward.guestIp}:${forward.port}-cmd:${ncPath} -4 -n -w 60 ${forward.address} ${forward.port}`
|
|
8679
|
-
);
|
|
8680
|
-
return [
|
|
8681
|
-
"user",
|
|
8682
|
-
"id=net0",
|
|
8683
|
-
`net=${WORKER_SUBNET}`,
|
|
8684
|
-
`dhcpstart=${WORKER_IP}`,
|
|
8685
|
-
"restrict=on",
|
|
8686
|
-
"ipv6=off",
|
|
8687
|
-
`hostfwd=tcp:127.0.0.1:${network.sshPort}-:22`,
|
|
8688
|
-
...guestForwards
|
|
8689
|
-
].join(",");
|
|
8690
|
-
}
|
|
8691
|
-
function validateForwards(forwards) {
|
|
8692
|
-
const seen = /* @__PURE__ */ new Set();
|
|
8693
|
-
for (const forward of forwards) {
|
|
8694
|
-
assertPort(forward.port, "guest forwarding port");
|
|
8695
|
-
const key = `${forward.guestIp}:${forward.port}`;
|
|
8696
|
-
if (seen.has(key)) throw new Error("Duplicate QEMU guest forwarding rule");
|
|
8697
|
-
seen.add(key);
|
|
8698
|
-
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(
|
|
8699
|
-
forward.address
|
|
8700
|
-
)) {
|
|
8701
|
-
throw new Error("Invalid pinned forwarding address");
|
|
8702
|
-
}
|
|
8703
|
-
}
|
|
8704
|
-
}
|
|
8705
10126
|
function fixedControl(control, expected) {
|
|
8706
10127
|
return (value) => {
|
|
8707
10128
|
if (value !== expected) throw new Error(`Unexpected QEMU ${control}`);
|
|
@@ -8734,7 +10155,7 @@ var SIMPLE_VALIDATORS = {
|
|
|
8734
10155
|
"-parallel": fixedControl("-parallel", "none"),
|
|
8735
10156
|
"-serial": fixedControl("serial", "chardev:char-serial"),
|
|
8736
10157
|
"-qmp": fixedControl("qmp", "chardev:char-qmp"),
|
|
8737
|
-
"-name": fixedControl("name",
|
|
10158
|
+
"-name": (value, { vmName = VM_NAME }) => fixedControl("name", `lima-${vmName}`)(value, {}),
|
|
8738
10159
|
"-pidfile": (value, context) => {
|
|
8739
10160
|
assertInstancePath(value, "qemu.pid", context);
|
|
8740
10161
|
return { value, control: "pidfile" };
|
|
@@ -8805,15 +10226,16 @@ function assertCompleteProfile(state) {
|
|
|
8805
10226
|
throw new Error("QEMU PCI serial device and chardev must appear together");
|
|
8806
10227
|
}
|
|
8807
10228
|
}
|
|
8808
|
-
function rewriteQemuArgs(args, network, ncPath = "/usr/bin/nc",
|
|
10229
|
+
function rewriteQemuArgs(args, network, ncPath = "/usr/bin/nc", hostContext = {}) {
|
|
8809
10230
|
if (isProbe(args)) return [...args];
|
|
8810
|
-
|
|
10231
|
+
assertPort2(network.sshPort, "SSH port");
|
|
8811
10232
|
if (!TRUSTED_NC_PATH.test(ncPath)) throw new Error("Invalid netcat path");
|
|
8812
|
-
const forwards = network
|
|
8813
|
-
|
|
8814
|
-
|
|
8815
|
-
|
|
8816
|
-
|
|
10233
|
+
const forwards = qemuForwards(network);
|
|
10234
|
+
const limits = parseCapacityLimits(network.limits);
|
|
10235
|
+
const context = {
|
|
10236
|
+
...hostContext,
|
|
10237
|
+
disk: { bps: limits.diskBytesPerSecond, iops: limits.diskIops }
|
|
10238
|
+
};
|
|
8817
10239
|
const output = [];
|
|
8818
10240
|
const state = {
|
|
8819
10241
|
controls: /* @__PURE__ */ new Set(),
|
|
@@ -8853,15 +10275,79 @@ function rewriteQemuArgs(args, network, ncPath = "/usr/bin/nc", context = {}) {
|
|
|
8853
10275
|
}
|
|
8854
10276
|
|
|
8855
10277
|
// src/vm/memory-budget.ts
|
|
8856
|
-
|
|
10278
|
+
var RELAY_RSS_ALLOWANCE_MIB = 128;
|
|
10279
|
+
var BASE_RELAY_SLOTS = 16;
|
|
10280
|
+
function relayAllowanceGiB(relaySlots = BASE_RELAY_SLOTS) {
|
|
10281
|
+
return Math.max(0, relaySlots - BASE_RELAY_SLOTS) * RELAY_RSS_ALLOWANCE_MIB / 1024;
|
|
10282
|
+
}
|
|
10283
|
+
function maximumGuestMemoryGiBWithin(budgetGiB, relaySlots = BASE_RELAY_SLOTS) {
|
|
10284
|
+
const guestBudget = budgetGiB - relayAllowanceGiB(relaySlots);
|
|
10285
|
+
if (!Number.isFinite(guestBudget) || guestBudget <= 0) return 0;
|
|
10286
|
+
return Math.max(0, Math.floor(Math.min(guestBudget - 1, guestBudget / 1.25)));
|
|
10287
|
+
}
|
|
10288
|
+
function maximumGuestMemoryGiB(hostBytes, relaySlots = BASE_RELAY_SLOTS) {
|
|
8857
10289
|
if (!Number.isFinite(hostBytes) || hostBytes <= 0) return 0;
|
|
8858
|
-
|
|
8859
|
-
|
|
10290
|
+
return maximumGuestMemoryGiBWithin(hostBytes / 1024 ** 3 * 0.75, relaySlots);
|
|
10291
|
+
}
|
|
10292
|
+
|
|
10293
|
+
// src/vm/host-state.ts
|
|
10294
|
+
import { access, chmod as chmod2, lstat, mkdir as mkdir2, readFile as readFile2, rename, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
10295
|
+
import { homedir, uptime } from "os";
|
|
10296
|
+
import { join } from "path";
|
|
10297
|
+
import { setTimeout as sleep } from "timers/promises";
|
|
10298
|
+
function vmProfile() {
|
|
10299
|
+
const profile = process.env.CONVEYOR_VM_TEST_PROFILE;
|
|
10300
|
+
if (profile !== void 0 && !/^acceptance-[a-z0-9-]{1,32}$/.test(profile)) {
|
|
10301
|
+
throw new Error("Test VM profiles must match acceptance-[a-z0-9-]{1,32}.");
|
|
10302
|
+
}
|
|
10303
|
+
return profile;
|
|
10304
|
+
}
|
|
10305
|
+
function instancesBase() {
|
|
10306
|
+
const profile = vmProfile();
|
|
10307
|
+
return join(
|
|
10308
|
+
homedir(),
|
|
10309
|
+
".local",
|
|
10310
|
+
"share",
|
|
10311
|
+
profile ? `conveyor-k3-${profile}-vms` : "conveyor-k3-vms"
|
|
10312
|
+
);
|
|
10313
|
+
}
|
|
10314
|
+
function hostPaths(instance = DEFAULT_INSTANCE) {
|
|
10315
|
+
const name = parseInstanceName(instance);
|
|
10316
|
+
if (name !== DEFAULT_INSTANCE) return pathsForRoot(join(instancesBase(), name), name);
|
|
10317
|
+
const profile = vmProfile();
|
|
10318
|
+
return pathsForRoot(
|
|
10319
|
+
join(homedir(), ".local", "share", profile ? `conveyor-k3-${profile}` : "conveyor-k3-vm")
|
|
10320
|
+
);
|
|
10321
|
+
}
|
|
10322
|
+
function pathsForRoot(root, instance = DEFAULT_INSTANCE) {
|
|
10323
|
+
const vmName = vmNameFor(parseInstanceName(instance));
|
|
10324
|
+
const instanceDir = join(root, "lima", vmName);
|
|
10325
|
+
return {
|
|
10326
|
+
root,
|
|
10327
|
+
limaHome: join(root, "lima"),
|
|
10328
|
+
state: join(root, "state.json"),
|
|
10329
|
+
network: join(root, "network.json"),
|
|
10330
|
+
template: join(root, "conveyor.yaml"),
|
|
10331
|
+
wrapper: join(root, "qemu-wrapper"),
|
|
10332
|
+
wrapperJs: join(root, "vm-qemu.js"),
|
|
10333
|
+
applied: join(root, "applied-network.json"),
|
|
10334
|
+
guard: join(root, "guard-state.json"),
|
|
10335
|
+
startRequest: join(root, "start-request.json"),
|
|
10336
|
+
policyTransaction: join(root, "policy-transaction.json"),
|
|
10337
|
+
lock: join(root, "operation.lock"),
|
|
10338
|
+
/** Written by an owner stop, so a supervising service does not restart the VM. */
|
|
10339
|
+
stopIntent: join(root, "stop-intent.json"),
|
|
10340
|
+
instance,
|
|
10341
|
+
vmName,
|
|
10342
|
+
instanceDir,
|
|
10343
|
+
limaConfig: join(instanceDir, "lima.yaml")
|
|
10344
|
+
};
|
|
8860
10345
|
}
|
|
8861
10346
|
export {
|
|
8862
10347
|
ClusterConnection,
|
|
8863
10348
|
ClusterDaemon,
|
|
8864
10349
|
CommandError,
|
|
10350
|
+
DEFAULT_INSTANCE,
|
|
8865
10351
|
HELP,
|
|
8866
10352
|
K3S_DISABLED_COMPONENTS,
|
|
8867
10353
|
K3S_INSTALL_EXEC,
|
|
@@ -8872,15 +10358,18 @@ export {
|
|
|
8872
10358
|
countRunningConveyorPods,
|
|
8873
10359
|
createMemoryFileSystem,
|
|
8874
10360
|
decideK3sUpgrade,
|
|
10361
|
+
hostPaths,
|
|
8875
10362
|
isGloballyRoutableIPv4,
|
|
8876
10363
|
isVerb,
|
|
8877
10364
|
maximumGuestMemoryGiB,
|
|
10365
|
+
maximumGuestMemoryGiBWithin,
|
|
8878
10366
|
meetsK3sMinimum,
|
|
8879
10367
|
missingAuthHosts,
|
|
8880
10368
|
nodeFileSystem,
|
|
8881
10369
|
ownerConfiguredKeys,
|
|
8882
10370
|
ownerConfiguredKubeletFlags,
|
|
8883
10371
|
parseArgs,
|
|
10372
|
+
parseInstanceName,
|
|
8884
10373
|
parseK3sVersion,
|
|
8885
10374
|
parseRegistries,
|
|
8886
10375
|
pruneStrandedImages,
|