@rallycry/conveyor-k3 0.1.13 → 0.1.15

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/index.js CHANGED
@@ -4162,7 +4162,7 @@ var coerce = {
4162
4162
  };
4163
4163
  var NEVER = INVALID;
4164
4164
 
4165
- // ../shared/dist/chunk-VSUBG7GR.js
4165
+ // ../shared/dist/chunk-SFSQ7NKP.js
4166
4166
  var DEFAULT_SONNET_MODEL = "claude-sonnet-5-5";
4167
4167
  var DEFAULT_OPUS_MODEL = "claude-opus-5-5";
4168
4168
  var DEFAULT_HAIKU_MODEL = "claude-haiku-4-5-20251001";
@@ -4356,6 +4356,15 @@ var ALLOWED_FILE_ACCEPT = [
4356
4356
  var EMBED_THRESHOLD_IMAGES = 5 * 1024 * 1024;
4357
4357
  var EMBED_THRESHOLD_TEXT = 2 * 1024 * 1024;
4358
4358
  var IDLE_HEARTBEAT_MS = 90 * 1e3;
4359
+ var siteAssetExtension = external_exports.string().regex(/^[a-z0-9]{1,10}$/);
4360
+ var RequestSiteAssetUploadRequestSchema = external_exports.object({
4361
+ projectId: external_exports.string(),
4362
+ siteId: external_exports.string().min(1).max(100),
4363
+ sha256: external_exports.string().regex(/^[a-f0-9]{64}$/),
4364
+ ext: siteAssetExtension,
4365
+ sizeBytes: external_exports.number().int().positive()
4366
+ });
4367
+ var ListSiteAssetTargetsRequestSchema = external_exports.object({ projectId: external_exports.string() });
4359
4368
  var TurnEndToolCallSchema = external_exports.object({
4360
4369
  tool: external_exports.string(),
4361
4370
  input: external_exports.string().optional(),
@@ -4647,7 +4656,9 @@ var GetTaskFilesRequestSchema = external_exports.object({
4647
4656
  var GetTaskFileRequestSchema = external_exports.object({
4648
4657
  sessionId: external_exports.string(),
4649
4658
  fileId: external_exports.string(),
4650
- taskId: external_exports.string().optional()
4659
+ taskId: external_exports.string().optional(),
4660
+ /** Ask for a short-lived `agentDownloadUrl` when the bytes are not inlined. */
4661
+ agentDownload: external_exports.boolean().optional()
4651
4662
  });
4652
4663
  var DeleteTaskFileRequestSchema = external_exports.object({
4653
4664
  sessionId: external_exports.string(),
@@ -4787,10 +4798,6 @@ var ReportDiscoveredPortsRequestSchema = external_exports.object({
4787
4798
  sessionId: external_exports.string(),
4788
4799
  ports: external_exports.array(DiscoveredPortSchema).max(64)
4789
4800
  });
4790
- var ReportBootMilestoneRequestSchema = external_exports.object({
4791
- sessionId: external_exports.string(),
4792
- key: external_exports.string().max(64)
4793
- });
4794
4801
  var CreateSubtaskRequestSchema = external_exports.object({
4795
4802
  sessionId: external_exports.string(),
4796
4803
  title: external_exports.string().min(1),
@@ -6266,6 +6273,8 @@ var ANTHROPIC_CATALOG = [
6266
6273
  anthropicEntry(DEFAULT_HAIKU_MODEL, "Haiku 4.5", 1, 5, { supportsEffort: false }),
6267
6274
  anthropicEntry(FABLE_MODEL, "Fable 5.1", 10, 50)
6268
6275
  ];
6276
+ var PROJECT_ARCHIVE_RETENTION_DAYS = 30;
6277
+ var RETENTION_MS = PROJECT_ARCHIVE_RETENTION_DAYS * 24 * 60 * 60 * 1e3;
6269
6278
  var CLAUDESPACE_WORKLOAD_LABEL = "rc-workload";
6270
6279
  var CLAUDESPACE_WORKLOAD_VALUE = "claudespace";
6271
6280
  var CONVEYOR_POD_SELECTOR = `${CLAUDESPACE_WORKLOAD_LABEL}=${CLAUDESPACE_WORKLOAD_VALUE}`;
@@ -6292,6 +6301,8 @@ var PERSONAL_LOCAL_REGISTRY_NAME = "conveyor-registry";
6292
6301
  var PERSONAL_LOCAL_REGISTRY_PORT = 5e3;
6293
6302
  var PERSONAL_LOCAL_REGISTRY_HOST = `${PERSONAL_LOCAL_REGISTRY_NAME}.${PERSONAL_LOCAL_REGISTRY_NAMESPACE}.svc.cluster.local:${PERSONAL_LOCAL_REGISTRY_PORT}`;
6294
6303
  var PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT = `http://127.0.0.1:${PERSONAL_LOCAL_REGISTRY_PORT}`;
6304
+ var PERSONAL_LOCAL_REGISTRY_PUSH_SECRET = "conveyor-registry-push";
6305
+ var PERSONAL_LOCAL_REGISTRY_PUSH_SECRET_KEY = "config.json";
6295
6306
  var BAKE_COMPONENT_LABEL = "claudespace.rallycry.com/component";
6296
6307
  var BAKE_COMPONENT_PREBAKE = "prebake";
6297
6308
  var BAKE_COMPONENT_STATE_BAKE = "state-bake";
@@ -6598,6 +6609,24 @@ var PRESTIGE_TIERS = ACHIEVEMENT_RARITIES.map((rarity, index) => ({
6598
6609
  minLevel: (index + 1) * LEVELS_PER_BAND
6599
6610
  }));
6600
6611
  var TOP_PRESTIGE_BAND = PRESTIGE_TIERS.length;
6612
+ var VM_HOST_MEMORY_SHARE = 0.75;
6613
+ var RELAY_RSS_ALLOWANCE_MIB = 128;
6614
+ var BASE_RELAY_SLOTS = 16;
6615
+ function relayAllowanceGiB(relaySlots = BASE_RELAY_SLOTS) {
6616
+ return Math.max(0, relaySlots - BASE_RELAY_SLOTS) * RELAY_RSS_ALLOWANCE_MIB / 1024;
6617
+ }
6618
+ function maximumGuestMemoryGiBWithin(budgetGiB, relaySlots = BASE_RELAY_SLOTS) {
6619
+ const guestBudget = budgetGiB - relayAllowanceGiB(relaySlots);
6620
+ if (!Number.isFinite(guestBudget) || guestBudget <= 0) return 0;
6621
+ return Math.max(0, Math.floor(Math.min(guestBudget - 1, guestBudget / 1.25)));
6622
+ }
6623
+ function maximumGuestMemoryGiB(hostBytes, relaySlots = BASE_RELAY_SLOTS) {
6624
+ if (!Number.isFinite(hostBytes) || hostBytes <= 0) return 0;
6625
+ return maximumGuestMemoryGiBWithin(vmMemoryBudgetGiB(hostBytes / 1024 ** 3), relaySlots);
6626
+ }
6627
+ function vmMemoryBudgetGiB(hostMemoryGiB) {
6628
+ return hostMemoryGiB * VM_HOST_MEMORY_SHARE;
6629
+ }
6601
6630
  var CARD_TYPE_SURFACE = {
6602
6631
  task: "board",
6603
6632
  chat: "board",
@@ -6957,14 +6986,22 @@ async function readImageInventory(exec) {
6957
6986
  }
6958
6987
  return { images, referencedIds };
6959
6988
  }
6989
+ var CRICTL_RMI_TIMEOUT = "120s";
6990
+ var RMI_EXEC_TIMEOUT_MS = 15e4;
6991
+ var DEADLINE_EXCEEDED = "context deadline exceeded";
6960
6992
  async function pruneStrandedImages(exec, inventory, wantedImages) {
6961
6993
  const candidates = selectStrandedImages(inventory, wantedImages);
6962
- const result = { removed: 0, bytesReclaimed: 0, failures: [] };
6994
+ const result = { removed: 0, bytesReclaimed: 0, failures: [], pending: [] };
6963
6995
  for (const candidate of candidates) {
6964
- const removal = await exec("crictl", ["rmi", candidate.id]);
6996
+ const removal = await exec("crictl", ["--timeout", CRICTL_RMI_TIMEOUT, "rmi", candidate.id], {
6997
+ timeoutMs: RMI_EXEC_TIMEOUT_MS
6998
+ });
6965
6999
  if (removal.exitCode === 0) {
6966
7000
  result.removed += 1;
6967
7001
  result.bytesReclaimed += candidate.sizeBytes;
7002
+ } else if (`${removal.stderr}
7003
+ ${removal.stdout}`.includes(DEADLINE_EXCEEDED)) {
7004
+ result.pending.push(candidate.id);
6968
7005
  } else {
6969
7006
  result.failures.push(`${candidate.id}: ${removal.stderr.trim().slice(0, 120)}`);
6970
7007
  }
@@ -7074,6 +7111,8 @@ var STATE_DIR = "/var/lib/conveyor-k3";
7074
7111
  var STATE_FILE = `${STATE_DIR}/state.json`;
7075
7112
  var LOCAL_REGISTRY_DATA_DIR = `${STATE_DIR}/registry`;
7076
7113
  var LOCAL_REGISTRY_MIRROR_RESTART_FILE = `${STATE_DIR}/registry-mirror-restart`;
7114
+ var LOCAL_REGISTRY_CREDENTIALS_FILE = `${STATE_DIR}/registry-credentials.json`;
7115
+ var LOCAL_REGISTRY_AUTH_STATE_FILE = `${STATE_DIR}/registry-auth.json`;
7077
7116
  var RANCHER_DIR = "/etc/rancher/k3s";
7078
7117
  var K3S_OWNER_CONFIG = `${RANCHER_DIR}/config.yaml`;
7079
7118
  var K3S_CONFIG_DROPIN_DIR = `${RANCHER_DIR}/config.yaml.d`;
@@ -7123,6 +7162,16 @@ function upsertRegistryAuth(file, host, auth) {
7123
7162
  next.configs = configs;
7124
7163
  return next;
7125
7164
  }
7165
+ function removeRegistryAuth(file, host) {
7166
+ const existing = file.configs?.[host];
7167
+ if (!existing?.auth) return file;
7168
+ const { auth: _auth, ...rest } = existing;
7169
+ const { [host]: _removed, ...others } = file.configs ?? {};
7170
+ const configs = Object.keys(rest).length > 0 ? { ...others, [host]: rest } : others;
7171
+ const next = { ...file, configs };
7172
+ if (Object.keys(configs).length === 0) delete next.configs;
7173
+ return next;
7174
+ }
7126
7175
  function mirrorEndpoints(file, host) {
7127
7176
  const entry = file.mirrors?.[host];
7128
7177
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) return [];
@@ -7179,6 +7228,85 @@ function managesK3s(desired, state) {
7179
7228
  return desired.isolation === void 0 || state.k3sInstalledByConveyor === true;
7180
7229
  }
7181
7230
 
7231
+ // src/lib/manifest-guard.ts
7232
+ import { isAlias, isMap, isScalar, parseAllDocuments } from "yaml";
7233
+ var ALLOWED_MANIFEST_KINDS = [
7234
+ "Namespace",
7235
+ "Role",
7236
+ "RoleBinding",
7237
+ "ClusterRole",
7238
+ "ClusterRoleBinding",
7239
+ "PriorityClass"
7240
+ ];
7241
+ var ALLOWED = new Set(ALLOWED_MANIFEST_KINDS);
7242
+ var MISSING_KIND = "(missing kind)";
7243
+ var UNPARSEABLE = "(unparseable)";
7244
+ var LIST_SHAPED = "(list-shaped document)";
7245
+ function isListShaped(map) {
7246
+ return map.items.some(({ key }) => {
7247
+ if (isAlias(key)) return true;
7248
+ const name = isScalar(key) ? key.value : key;
7249
+ return typeof name === "string" && (name === "<<" || name.toLowerCase() === "items");
7250
+ });
7251
+ }
7252
+ function findDisallowedKinds(manifest) {
7253
+ let documents;
7254
+ try {
7255
+ documents = parseAllDocuments(manifest);
7256
+ } catch {
7257
+ return [UNPARSEABLE];
7258
+ }
7259
+ const disallowed = [];
7260
+ const record = (kind) => {
7261
+ if (!disallowed.includes(kind)) disallowed.push(kind);
7262
+ };
7263
+ for (const doc of documents) {
7264
+ if (doc.errors.length > 0) {
7265
+ record(UNPARSEABLE);
7266
+ continue;
7267
+ }
7268
+ if (doc.contents === null || doc.contents === void 0) continue;
7269
+ if (isScalar(doc.contents) && doc.contents.value === null) continue;
7270
+ if (!isMap(doc.contents)) {
7271
+ record(MISSING_KIND);
7272
+ continue;
7273
+ }
7274
+ if (isListShaped(doc.contents)) {
7275
+ record(LIST_SHAPED);
7276
+ continue;
7277
+ }
7278
+ const kind = doc.get("kind");
7279
+ if (typeof kind !== "string" || kind.length === 0) {
7280
+ record(MISSING_KIND);
7281
+ continue;
7282
+ }
7283
+ if (!ALLOWED.has(kind)) record(kind);
7284
+ }
7285
+ return disallowed;
7286
+ }
7287
+ function withoutKind(manifest, kind) {
7288
+ return parseAllDocuments(manifest).filter((doc) => !(isMap(doc.contents) && doc.get("kind") === kind)).map((doc) => doc.toString()).join("---\n");
7289
+ }
7290
+ function declaresPriorityClass(manifest, name, value) {
7291
+ return parseAllDocuments(manifest).some(
7292
+ (doc) => isMap(doc.contents) && doc.get("kind") === "PriorityClass" && doc.getIn(["metadata", "name"]) === name && doc.get("value") === value
7293
+ );
7294
+ }
7295
+
7296
+ // src/daemon/cluster-ops.ts
7297
+ async function kubectlApply(exec, manifest) {
7298
+ await runOrThrow(exec, "k3s", ["kubectl", "apply", "-f", "-"], {
7299
+ stdin: manifest,
7300
+ timeoutMs: 6e4
7301
+ });
7302
+ }
7303
+ function mayDisrupt(runningConveyorPods) {
7304
+ return runningConveyorPods === 0;
7305
+ }
7306
+ function busyReason(runningConveyorPods) {
7307
+ return runningConveyorPods === null ? "the number of running Conveyor pods could not be read" : `${runningConveyorPods} Conveyor pod(s) are running`;
7308
+ }
7309
+
7182
7310
  // src/lib/k3s-config.ts
7183
7311
  var IMAGE_GC_HIGH = "image-gc-high-threshold";
7184
7312
  var IMAGE_GC_LOW = "image-gc-low-threshold";
@@ -7263,76 +7391,345 @@ function renderK3sDropIn(kubelet, ownerConfig, options = {}) {
7263
7391
  }
7264
7392
  return { content: [...lines2, ""].join("\n"), skipped };
7265
7393
  }
7266
- var renderKubeletDropIn = renderK3sDropIn;
7267
7394
 
7268
- // src/lib/manifest-guard.ts
7269
- import { isMap, isScalar, parseAllDocuments } from "yaml";
7270
- var ALLOWED_MANIFEST_KINDS = [
7271
- "Namespace",
7272
- "Role",
7273
- "RoleBinding",
7274
- "ClusterRole",
7275
- "ClusterRoleBinding",
7276
- "PriorityClass"
7277
- ];
7278
- var ALLOWED = new Set(ALLOWED_MANIFEST_KINDS);
7279
- var MISSING_KIND = "(missing kind)";
7280
- var UNPARSEABLE = "(unparseable)";
7281
- function findDisallowedKinds(manifest) {
7282
- let documents;
7283
- try {
7284
- documents = parseAllDocuments(manifest);
7285
- } catch {
7286
- return [UNPARSEABLE];
7395
+ // src/daemon/pod-cpu-quota.ts
7396
+ var POD_SLICE = "kubepods.slice";
7397
+ function cpuMillicores(quantity) {
7398
+ const checked = assertQuantity(quantity, "reserveCpu");
7399
+ const match = /^(\d+(?:\.\d+)?)(m?)$/.exec(checked);
7400
+ if (!match?.[1])
7401
+ throw new Error(`Invalid reserveCpu: ${JSON.stringify(checked)} is not a CPU amount`);
7402
+ const amount = Number.parseFloat(match[1]);
7403
+ return Math.round(match[2] === "m" ? amount : amount * 1e3);
7404
+ }
7405
+ function podCpuQuotaPercent(cores, reservedMillicores) {
7406
+ const left = cores * 1e3 - reservedMillicores;
7407
+ if (left < 10) return null;
7408
+ return Math.floor(left / 10);
7409
+ }
7410
+ function setQuota(exec, value) {
7411
+ return exec("systemctl", ["set-property", POD_SLICE, `CPUQuota=${value}`]);
7412
+ }
7413
+ async function liftPodCpuQuota(exec) {
7414
+ const shown = await exec("systemctl", ["show", POD_SLICE, "-p", "CPUQuotaPerSecUSec", "--value"]);
7415
+ if (shown.exitCode !== 0 || ["", "infinity"].includes(shown.stdout.trim())) return null;
7416
+ const cleared = await setQuota(exec, "");
7417
+ return cleared.exitCode === 0 ? null : cleared.stderr.trim().slice(0, 200);
7418
+ }
7419
+ async function reconcilePodCpuQuota(exec, desired, warnings) {
7420
+ const reserve = desired.kubelet.systemReserved?.cpu;
7421
+ if (!reserve) {
7422
+ const failure = await liftPodCpuQuota(exec);
7423
+ if (failure) warnings.push(`Could not lift the CPU cap on ${POD_SLICE}: ${failure}`);
7424
+ return void 0;
7287
7425
  }
7288
- const disallowed = [];
7289
- const record = (kind) => {
7290
- if (!disallowed.includes(kind)) disallowed.push(kind);
7426
+ const reserved = cpuMillicores(reserve);
7427
+ const state = await exec("systemctl", ["show", POD_SLICE, "-p", "ActiveState", "--value"]);
7428
+ if (state.exitCode !== 0 || state.stdout.trim() !== "active") {
7429
+ warnings.push(
7430
+ `Pods do not run under ${POD_SLICE} (k3s is not using the systemd cgroup driver), so the CPU you keep for this machine is weighted, not capped.`
7431
+ );
7432
+ return false;
7433
+ }
7434
+ const nproc = await exec("nproc", []);
7435
+ const cores = Number.parseInt(nproc.stdout.trim(), 10);
7436
+ if (nproc.exitCode !== 0 || !Number.isInteger(cores) || cores <= 0) {
7437
+ warnings.push(
7438
+ "Could not count this machine's CPUs, so the CPU you keep is weighted, not capped."
7439
+ );
7440
+ return false;
7441
+ }
7442
+ const percent = podCpuQuotaPercent(cores, reserved);
7443
+ if (percent === null) {
7444
+ warnings.push(
7445
+ `The CPU you keep (${reserve}) leaves no CPU for builds on this ${cores}-CPU machine. Lower it in your Conveyor profile.`
7446
+ );
7447
+ return false;
7448
+ }
7449
+ const set = await setQuota(exec, `${percent}%`);
7450
+ if (set.exitCode !== 0) {
7451
+ warnings.push(
7452
+ `Could not cap pod CPU at ${percent / 100} CPUs: ${set.stderr.trim().slice(0, 200)}`
7453
+ );
7454
+ return false;
7455
+ }
7456
+ return true;
7457
+ }
7458
+
7459
+ // src/daemon/kubelet-upkeep.ts
7460
+ function ownerRestartWarning(change) {
7461
+ return `${change} Conveyor did not install this server's k3s, so it never restarts it: run \`sudo systemctl restart k3s\` when no build runs.`;
7462
+ }
7463
+ async function reconcileKubelet(ctx, desired, result, managed) {
7464
+ const ownerConfig = await ctx.fs.readFile(K3S_OWNER_CONFIG);
7465
+ const rendered = renderK3sDropIn(desired.kubelet, ownerConfig, { disableComponents: managed });
7466
+ result.skippedKubeletFlags = rendered.skipped;
7467
+ result.effectiveImageGcHighThreshold = rendered.skipped.includes("image-gc-high-threshold") ? ownerImageGcHighThreshold(ownerConfig) : desired.kubelet.imageGcHighThreshold;
7468
+ for (const flag of rendered.skipped) {
7469
+ result.warnings.push(
7470
+ `Left \`${flag}\` to your own /etc/rancher/k3s/config.yaml \u2014 Conveyor does not override a setting you chose.`
7471
+ );
7472
+ }
7473
+ const current = await ctx.fs.readFile(K3S_CONFIG_DROPIN);
7474
+ if (rendered.content === null) {
7475
+ if (current !== null) await ctx.fs.removeFile(K3S_CONFIG_DROPIN);
7476
+ return;
7477
+ }
7478
+ if (current === rendered.content) return;
7479
+ await ctx.fs.ensureDir(K3S_CONFIG_DROPIN.slice(0, K3S_CONFIG_DROPIN.lastIndexOf("/")));
7480
+ await ctx.fs.writeFile(K3S_CONFIG_DROPIN, rendered.content, 420);
7481
+ if (!managed) {
7482
+ result.warnings.push(ownerRestartWarning("Kubelet settings changed."));
7483
+ return;
7484
+ }
7485
+ if (!mayDisrupt(ctx.runningConveyorPods)) {
7486
+ result.warnings.push(
7487
+ `Kubelet settings changed but ${busyReason(ctx.runningConveyorPods)} \u2014 the restart is deferred until the machine is idle.`
7488
+ );
7489
+ return;
7490
+ }
7491
+ const restart = await ctx.exec("systemctl", ["restart", "k3s"], { timeoutMs: 12e4 });
7492
+ if (restart.exitCode === 0) {
7493
+ result.restartedK3s = true;
7494
+ } else {
7495
+ result.warnings.push(
7496
+ `Could not restart k3s to apply new kubelet settings: ${restart.stderr.trim().slice(0, 200)}`
7497
+ );
7498
+ }
7499
+ }
7500
+
7501
+ // src/daemon/local-registry-upkeep.ts
7502
+ import { createHash as createHash3 } from "crypto";
7503
+
7504
+ // src/lib/local-registry.ts
7505
+ import { createHash as createHash2 } from "crypto";
7506
+
7507
+ // src/lib/registry-credentials.ts
7508
+ import { createHash, randomBytes as randomBytes2 } from "crypto";
7509
+
7510
+ // src/lib/bcrypt.ts
7511
+ var P_WORDS = 18;
7512
+ var S_WORDS = 1024;
7513
+ var MAGIC = "OrpheanBeholderScryDoubt";
7514
+ var ALPHABET = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
7515
+ var MAX_KEY_BYTES = 72;
7516
+ var BCRYPT_SALT_BYTES = 16;
7517
+ function scaledAtanInverse(x, one) {
7518
+ const xSquared = x * x;
7519
+ let power = one / x;
7520
+ let sum = 0n;
7521
+ for (let n = 0n; power !== 0n; n++) {
7522
+ const term = power / (2n * n + 1n);
7523
+ sum = n % 2n === 0n ? sum + term : sum - term;
7524
+ power /= xSquared;
7525
+ }
7526
+ return sum;
7527
+ }
7528
+ function piFractionWords(count) {
7529
+ const guard = 64n;
7530
+ const bits = BigInt(count * 32) + guard;
7531
+ const one = 1n << bits;
7532
+ const pi = 16n * scaledAtanInverse(5n, one) - 4n * scaledAtanInverse(239n, one);
7533
+ const fraction = pi - 3n * one >> guard;
7534
+ const words = new Uint32Array(count);
7535
+ for (let index = 0; index < count; index++) {
7536
+ words[index] = Number(fraction >> BigInt(32 * (count - 1 - index)) & 0xffffffffn);
7537
+ }
7538
+ return words;
7539
+ }
7540
+ var initialState = null;
7541
+ function blowfishInitialState() {
7542
+ if (!initialState) {
7543
+ const words = piFractionWords(P_WORDS + S_WORDS);
7544
+ initialState = { p: words.slice(0, P_WORDS), s: words.slice(P_WORDS) };
7545
+ }
7546
+ return initialState;
7547
+ }
7548
+ function word(array, index) {
7549
+ return array[index] ?? 0;
7550
+ }
7551
+ function round(s, x) {
7552
+ const mixed = word(s, x >>> 24) + word(s, 256 + (x >>> 16 & 255)) ^ word(s, 512 + (x >>> 8 & 255));
7553
+ return mixed + word(s, 768 + (x & 255)) | 0;
7554
+ }
7555
+ function encipher(state, data, offset) {
7556
+ const { p, s } = state;
7557
+ let left = word(data, offset) ^ word(p, 0);
7558
+ let right = word(data, offset + 1);
7559
+ for (let index = 1; index <= 16; index += 2) {
7560
+ right ^= round(s, left) ^ word(p, index);
7561
+ left ^= round(s, right) ^ word(p, index + 1);
7562
+ }
7563
+ data[offset] = right ^ word(p, 17);
7564
+ data[offset + 1] = left;
7565
+ }
7566
+ function wordStream(bytes) {
7567
+ let position = 0;
7568
+ return () => {
7569
+ let value = 0;
7570
+ for (let index = 0; index < 4; index++) {
7571
+ value = value << 8 | (bytes[position] ?? 0);
7572
+ position = (position + 1) % bytes.length;
7573
+ }
7574
+ return value >>> 0;
7291
7575
  };
7292
- for (const doc of documents) {
7293
- if (doc.errors.length > 0) {
7294
- record(UNPARSEABLE);
7295
- continue;
7576
+ }
7577
+ function expandKey(state, key, salt) {
7578
+ const nextKeyWord = wordStream(key);
7579
+ for (let index = 0; index < P_WORDS; index++) {
7580
+ state.p[index] = word(state.p, index) ^ nextKeyWord();
7581
+ }
7582
+ const nextSaltWord = salt ? wordStream(salt) : () => 0;
7583
+ const block = new Uint32Array(2);
7584
+ const fill = (target, length) => {
7585
+ for (let index = 0; index < length; index += 2) {
7586
+ block[0] = word(block, 0) ^ nextSaltWord();
7587
+ block[1] = word(block, 1) ^ nextSaltWord();
7588
+ encipher(state, block, 0);
7589
+ target[index] = word(block, 0);
7590
+ target[index + 1] = word(block, 1);
7296
7591
  }
7297
- if (doc.contents === null || doc.contents === void 0) continue;
7298
- if (isScalar(doc.contents) && doc.contents.value === null) continue;
7299
- if (!isMap(doc.contents)) {
7300
- record(MISSING_KIND);
7301
- continue;
7592
+ };
7593
+ fill(state.p, P_WORDS);
7594
+ fill(state.s, S_WORDS);
7595
+ }
7596
+ function encodeBase64(bytes, length) {
7597
+ let out = "";
7598
+ let offset = 0;
7599
+ while (offset < length) {
7600
+ let first = bytes[offset++] ?? 0;
7601
+ out += ALPHABET[first >> 2];
7602
+ first = (first & 3) << 4;
7603
+ if (offset >= length) {
7604
+ out += ALPHABET[first];
7605
+ break;
7302
7606
  }
7303
- const kind = doc.get("kind");
7304
- if (typeof kind !== "string" || kind.length === 0) {
7305
- record(MISSING_KIND);
7306
- continue;
7607
+ let second = bytes[offset++] ?? 0;
7608
+ out += ALPHABET[first | second >> 4];
7609
+ first = (second & 15) << 2;
7610
+ if (offset >= length) {
7611
+ out += ALPHABET[first];
7612
+ break;
7307
7613
  }
7308
- if (!ALLOWED.has(kind)) record(kind);
7309
- }
7310
- return disallowed;
7614
+ second = bytes[offset++] ?? 0;
7615
+ out += ALPHABET[first | second >> 6];
7616
+ out += ALPHABET[second & 63];
7617
+ }
7618
+ return out;
7619
+ }
7620
+ function bcryptHash(password, cost, salt) {
7621
+ if (!Number.isInteger(cost) || cost < 4 || cost > 31) {
7622
+ throw new Error(`bcrypt cost must be an integer from 4 to 31, not ${cost}`);
7623
+ }
7624
+ if (salt.length !== BCRYPT_SALT_BYTES) {
7625
+ throw new Error(`bcrypt needs a ${BCRYPT_SALT_BYTES}-byte salt`);
7626
+ }
7627
+ const passwordBytes = new TextEncoder().encode(password).slice(0, MAX_KEY_BYTES);
7628
+ const key = new Uint8Array(passwordBytes.length + 1);
7629
+ key.set(passwordBytes);
7630
+ const base = blowfishInitialState();
7631
+ const state = { p: base.p.slice(), s: base.s.slice() };
7632
+ expandKey(state, key, salt);
7633
+ for (let rounds = 2 ** cost; rounds > 0; rounds--) {
7634
+ expandKey(state, key, null);
7635
+ expandKey(state, salt, null);
7636
+ }
7637
+ const magic = new TextEncoder().encode(MAGIC);
7638
+ const nextMagicWord = wordStream(magic);
7639
+ const block = Uint32Array.from({ length: 6 }, () => nextMagicWord());
7640
+ for (let pass = 0; pass < 64; pass++) {
7641
+ for (let offset = 0; offset < 6; offset += 2) encipher(state, block, offset);
7642
+ }
7643
+ const digest = new Uint8Array(24);
7644
+ block.forEach((value, index) => {
7645
+ digest[index * 4] = value >>> 24;
7646
+ digest[index * 4 + 1] = value >>> 16 & 255;
7647
+ digest[index * 4 + 2] = value >>> 8 & 255;
7648
+ digest[index * 4 + 3] = value & 255;
7649
+ });
7650
+ const costText = String(cost).padStart(2, "0");
7651
+ return `$2y$${costText}$${encodeBase64(salt, BCRYPT_SALT_BYTES)}${encodeBase64(digest, 23)}`;
7311
7652
  }
7312
7653
 
7313
- // src/daemon/cluster-ops.ts
7314
- async function kubectlApply(exec, manifest) {
7315
- await runOrThrow(exec, "k3s", ["kubectl", "apply", "-f", "-"], {
7316
- stdin: manifest,
7317
- timeoutMs: 6e4
7318
- });
7654
+ // src/lib/registry-credentials.ts
7655
+ var LOCAL_REGISTRY_USERNAME = "conveyor";
7656
+ var BCRYPT_COST = 5;
7657
+ var USERNAME = /^[a-z][a-z0-9-]{0,31}$/;
7658
+ var PASSWORD = /^[A-Za-z0-9_-]{32,128}$/;
7659
+ function generateRegistryCredentials(random = randomBytes2) {
7660
+ const username = LOCAL_REGISTRY_USERNAME;
7661
+ const password = Buffer.from(random(32)).toString("base64url");
7662
+ const hash = bcryptHash(password, BCRYPT_COST, random(BCRYPT_SALT_BYTES));
7663
+ return { username, password, htpasswd: `${username}:${hash}` };
7664
+ }
7665
+ function parseRegistryCredentials(raw) {
7666
+ if (!raw) return null;
7667
+ try {
7668
+ const value = JSON.parse(raw);
7669
+ const { username, password, htpasswd } = value ?? {};
7670
+ if (typeof username !== "string" || !USERNAME.test(username)) return null;
7671
+ if (typeof password !== "string" || !PASSWORD.test(password)) return null;
7672
+ if (typeof htpasswd !== "string" || !htpasswd.startsWith(`${username}:$2`)) return null;
7673
+ if (htpasswd.includes("\n")) return null;
7674
+ return { username, password, htpasswd };
7675
+ } catch {
7676
+ return null;
7677
+ }
7319
7678
  }
7320
- function mayDisrupt(runningConveyorPods) {
7321
- return runningConveyorPods === 0;
7679
+ async function loadOrCreateRegistryCredentials(fs, random) {
7680
+ const stored = parseRegistryCredentials(await fs.readFile(LOCAL_REGISTRY_CREDENTIALS_FILE));
7681
+ if (stored) return stored;
7682
+ const created = generateRegistryCredentials(random);
7683
+ await fs.writeFile(LOCAL_REGISTRY_CREDENTIALS_FILE, `${JSON.stringify(created)}
7684
+ `, 384);
7685
+ return created;
7322
7686
  }
7323
- function busyReason(runningConveyorPods) {
7324
- return runningConveyorPods === null ? "the number of running Conveyor pods could not be read" : `${runningConveyorPods} Conveyor pod(s) are running`;
7687
+ function credentialFingerprint(credentials) {
7688
+ return createHash("sha256").update(`${credentials.username}:${credentials.password}`).digest("hex").slice(0, 16);
7689
+ }
7690
+ function basicAuthorization(credentials) {
7691
+ return `Basic ${Buffer.from(`${credentials.username}:${credentials.password}`).toString("base64")}`;
7692
+ }
7693
+ function registryDockerConfig(credentials, host) {
7694
+ const auth = Buffer.from(`${credentials.username}:${credentials.password}`).toString("base64");
7695
+ return JSON.stringify({ auths: { [host]: { auth } } });
7696
+ }
7697
+ function parseRegistryAuthState(raw) {
7698
+ if (!raw) return null;
7699
+ try {
7700
+ const value = JSON.parse(raw);
7701
+ if (typeof value?.credential !== "string") return null;
7702
+ const writtenAt = typeof value.writtenAt === "string" && !Number.isNaN(Date.parse(value.writtenAt)) ? value.writtenAt : null;
7703
+ return { credential: value.credential, writtenAt, enforced: value.enforced === true };
7704
+ } catch {
7705
+ return null;
7706
+ }
7707
+ }
7708
+ async function registryAuthEnforced(readFile3) {
7709
+ return parseRegistryAuthState(await readFile3(LOCAL_REGISTRY_AUTH_STATE_FILE))?.enforced === true;
7710
+ }
7711
+ function withRegistryCredentials(fetchImpl, readFile3) {
7712
+ let credentials = null;
7713
+ return async (url, init) => {
7714
+ credentials ??= readFile3(LOCAL_REGISTRY_CREDENTIALS_FILE).then(parseRegistryCredentials);
7715
+ const stored = await credentials;
7716
+ if (!stored) return fetchImpl(url, init);
7717
+ const headers = new Headers(init?.headers);
7718
+ headers.set("Authorization", basicAuthorization(stored));
7719
+ return fetchImpl(url, { ...init, headers });
7720
+ };
7325
7721
  }
7326
-
7327
- // src/daemon/local-registry-upkeep.ts
7328
- import { createHash } from "crypto";
7329
7722
 
7330
7723
  // src/lib/local-registry.ts
7331
7724
  var LOCAL_REGISTRY_IMAGE = "docker.io/library/registry:3.1.2@sha256:c87f33837722a100572e95d7dc4bf539fc42cf68202b13c3bc03c0ff54c3a649";
7332
7725
  var LOCAL_REGISTRY_CONFIG = "/etc/distribution/config.yml";
7333
7726
  var LOCAL_REGISTRY_GC_JOB = "conveyor-registry-gc";
7727
+ var LOCAL_REGISTRY_HTPASSWD_SECRET = "conveyor-registry-htpasswd";
7728
+ var HTPASSWD_KEY = "htpasswd";
7729
+ var AUTH_MOUNT = "/etc/distribution/auth";
7334
7730
  var LOCAL_REGISTRY_SELECTOR = { "app.kubernetes.io/name": PERSONAL_LOCAL_REGISTRY_NAME };
7335
7731
  var MANAGED_BY = { "app.kubernetes.io/managed-by": "conveyor-k3" };
7732
+ var AUTH_CHECKSUM_ANNOTATION = "conveyor.rallycry.com/registry-auth";
7336
7733
  var DATA_MOUNT = "/var/lib/registry";
7337
7734
  var REGISTRY_ENV = [
7338
7735
  { name: "REGISTRY_STORAGE_DELETE_ENABLED", value: "true" },
@@ -7347,6 +7744,23 @@ var VOLUME_MOUNTS = [
7347
7744
  { name: "data", mountPath: DATA_MOUNT },
7348
7745
  { name: "tmp", mountPath: "/tmp" }
7349
7746
  ];
7747
+ var AUTH_ENV = [
7748
+ { name: "REGISTRY_AUTH", value: "htpasswd" },
7749
+ { name: "REGISTRY_AUTH_HTPASSWD_REALM", value: "conveyor" },
7750
+ { name: "REGISTRY_AUTH_HTPASSWD_PATH", value: `${AUTH_MOUNT}/${HTPASSWD_KEY}` }
7751
+ ];
7752
+ var AUTH_VOLUME = {
7753
+ name: "auth",
7754
+ secret: {
7755
+ secretName: LOCAL_REGISTRY_HTPASSWD_SECRET,
7756
+ items: [{ key: HTPASSWD_KEY, path: HTPASSWD_KEY }],
7757
+ defaultMode: 256
7758
+ }
7759
+ };
7760
+ var AUTH_VOLUME_MOUNT = { name: "auth", mountPath: AUTH_MOUNT, readOnly: true };
7761
+ function htpasswdChecksum(htpasswd) {
7762
+ return createHash2("sha256").update(htpasswd).digest("hex").slice(0, 16);
7763
+ }
7350
7764
  var VOLUMES = [
7351
7765
  { name: "data", hostPath: { path: LOCAL_REGISTRY_DATA_DIR, type: "Directory" } },
7352
7766
  { name: "tmp", emptyDir: { sizeLimit: "64Mi" } }
@@ -7383,7 +7797,34 @@ function registryNetworkPolicy(metadata) {
7383
7797
  }
7384
7798
  };
7385
7799
  }
7386
- function renderLocalRegistryManifests() {
7800
+ function renderLocalRegistrySecrets(credentials) {
7801
+ const encode = (value) => Buffer.from(value).toString("base64");
7802
+ const secret = (namespace, name, key, value) => ({
7803
+ apiVersion: "v1",
7804
+ kind: "Secret",
7805
+ metadata: { name, namespace, labels: { ...MANAGED_BY } },
7806
+ type: "Opaque",
7807
+ data: { [key]: encode(value) }
7808
+ });
7809
+ const items = [
7810
+ secret(
7811
+ PERSONAL_LOCAL_REGISTRY_NAMESPACE,
7812
+ LOCAL_REGISTRY_HTPASSWD_SECRET,
7813
+ HTPASSWD_KEY,
7814
+ `${credentials.htpasswd}
7815
+ `
7816
+ ),
7817
+ secret(
7818
+ PERSONAL_BAKE_NAMESPACE,
7819
+ PERSONAL_LOCAL_REGISTRY_PUSH_SECRET,
7820
+ PERSONAL_LOCAL_REGISTRY_PUSH_SECRET_KEY,
7821
+ registryDockerConfig(credentials, PERSONAL_LOCAL_REGISTRY_HOST)
7822
+ )
7823
+ ];
7824
+ return `${JSON.stringify({ apiVersion: "v1", kind: "List", items }, null, 2)}
7825
+ `;
7826
+ }
7827
+ function renderLocalRegistryManifests(auth = null) {
7387
7828
  const metadata = {
7388
7829
  name: PERSONAL_LOCAL_REGISTRY_NAME,
7389
7830
  namespace: PERSONAL_LOCAL_REGISTRY_NAMESPACE,
@@ -7399,15 +7840,19 @@ function renderLocalRegistryManifests() {
7399
7840
  strategy: { type: "Recreate" },
7400
7841
  selector: { matchLabels: LOCAL_REGISTRY_SELECTOR },
7401
7842
  template: {
7402
- metadata: { labels: metadata.labels },
7843
+ metadata: {
7844
+ labels: metadata.labels,
7845
+ ...auth ? { annotations: { [AUTH_CHECKSUM_ANNOTATION]: htpasswdChecksum(auth.htpasswd) } } : {}
7846
+ },
7403
7847
  spec: {
7404
7848
  ...POD_BASE,
7849
+ ...auth ? { volumes: [...VOLUMES, AUTH_VOLUME] } : {},
7405
7850
  containers: [
7406
7851
  {
7407
7852
  name: "registry",
7408
7853
  image: LOCAL_REGISTRY_IMAGE,
7409
7854
  imagePullPolicy: "IfNotPresent",
7410
- env: REGISTRY_ENV,
7855
+ env: auth ? [...REGISTRY_ENV, ...AUTH_ENV] : REGISTRY_ENV,
7411
7856
  ports: [
7412
7857
  {
7413
7858
  name: "registry",
@@ -7419,8 +7864,10 @@ function renderLocalRegistryManifests() {
7419
7864
  protocol: "TCP"
7420
7865
  }
7421
7866
  ],
7867
+ // A TCP check: once the credential is enforced, `/v2/` answers
7868
+ // 401 to a probe that does not carry it.
7422
7869
  readinessProbe: {
7423
- httpGet: { path: "/v2/", port: PERSONAL_LOCAL_REGISTRY_PORT },
7870
+ tcpSocket: { port: PERSONAL_LOCAL_REGISTRY_PORT },
7424
7871
  periodSeconds: 10
7425
7872
  },
7426
7873
  resources: {
@@ -7428,7 +7875,7 @@ function renderLocalRegistryManifests() {
7428
7875
  limits: { memory: "512Mi" }
7429
7876
  },
7430
7877
  securityContext: CONTAINER_SECURITY,
7431
- volumeMounts: VOLUME_MOUNTS
7878
+ volumeMounts: auth ? [...VOLUME_MOUNTS, AUTH_VOLUME_MOUNT] : VOLUME_MOUNTS
7432
7879
  }
7433
7880
  ]
7434
7881
  }
@@ -7519,11 +7966,18 @@ async function registryAnswers(fetchImpl = fetch) {
7519
7966
  }
7520
7967
  async function localRegistryReady(ports) {
7521
7968
  const [answers, mirrored] = await Promise.all([
7522
- registryAnswers(ports.fetch),
7969
+ registryAnswers(withRegistryCredentials(ports.fetch ?? fetch, ports.readFile)),
7523
7970
  mirrorLoaded(ports.readFile)
7524
7971
  ]);
7525
7972
  return answers && mirrored;
7526
7973
  }
7974
+ async function localRegistryStatus(ports) {
7975
+ const [ready, authEnforced] = await Promise.all([
7976
+ localRegistryReady(ports),
7977
+ registryAuthEnforced(ports.readFile)
7978
+ ]);
7979
+ return { ready, authEnforced };
7980
+ }
7527
7981
 
7528
7982
  // src/daemon/registry-gc-lock.ts
7529
7983
  var REGISTRY_GC_MARK = "conveyor.rallycry.com/registry-gc";
@@ -7614,6 +8068,59 @@ async function deleteSweepJob(exec) {
7614
8068
  return result.exitCode === 0;
7615
8069
  }
7616
8070
 
8071
+ // src/daemon/registry-auth.ts
8072
+ var LOCAL_REGISTRY_AUTH_HOSTS = [
8073
+ PERSONAL_LOCAL_REGISTRY_HOST,
8074
+ new URL(PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT).host
8075
+ ];
8076
+ function registryAuthConfigured(file, credentials) {
8077
+ return LOCAL_REGISTRY_AUTH_HOSTS.every((host) => {
8078
+ const auth = file.configs?.[host]?.auth;
8079
+ return auth?.username === credentials.username && auth.password === credentials.password && auth.auth === void 0 && auth.identitytoken === void 0;
8080
+ });
8081
+ }
8082
+ function withLocalRegistryAuth(file, credentials) {
8083
+ return LOCAL_REGISTRY_AUTH_HOSTS.reduce(
8084
+ (next, host) => upsertRegistryAuth(next, host, {
8085
+ username: credentials.username,
8086
+ password: credentials.password
8087
+ }),
8088
+ file
8089
+ );
8090
+ }
8091
+ function monotonicNowUs() {
8092
+ return Number(process.hrtime.bigint() / 1000n);
8093
+ }
8094
+ async function k3sStartedAtMs(exec, nowMs, monotonicNow = monotonicNowUs) {
8095
+ const result = await exec(
8096
+ "systemctl",
8097
+ ["show", "k3s", "--property=ActiveEnterTimestampMonotonic", "--value"],
8098
+ { timeoutMs: 1e4 }
8099
+ ).catch(() => null);
8100
+ if (!result || result.exitCode !== 0) return null;
8101
+ const startedUs = Number.parseInt(result.stdout.trim(), 10);
8102
+ if (!Number.isFinite(startedUs) || startedUs <= 0) return null;
8103
+ return nowMs - (monotonicNow() - startedUs) / 1e3;
8104
+ }
8105
+ async function settleRegistryAuth(ctx, credentials, written) {
8106
+ const now = (ctx.now ?? (() => /* @__PURE__ */ new Date()))();
8107
+ const raw = await ctx.fs.readFile(LOCAL_REGISTRY_AUTH_STATE_FILE);
8108
+ const credential = credentialFingerprint(credentials);
8109
+ const stored = parseRegistryAuthState(raw);
8110
+ const state = stored?.credential === credential ? stored : { credential, writtenAt: null, enforced: false };
8111
+ if (!state.enforced && (written || state.writtenAt === null)) {
8112
+ state.writtenAt = now.toISOString();
8113
+ }
8114
+ if (!state.enforced && state.writtenAt !== null) {
8115
+ const startedAt = await k3sStartedAtMs(ctx.exec, now.getTime(), ctx.monotonicNowUs);
8116
+ if (startedAt !== null && startedAt > Date.parse(state.writtenAt)) state.enforced = true;
8117
+ }
8118
+ const next = `${JSON.stringify(state)}
8119
+ `;
8120
+ if (next !== raw) await ctx.fs.writeFile(LOCAL_REGISTRY_AUTH_STATE_FILE, next, 384);
8121
+ return state.enforced;
8122
+ }
8123
+
7617
8124
  // src/daemon/local-registry-upkeep.ts
7618
8125
  function nowDate() {
7619
8126
  return /* @__PURE__ */ new Date();
@@ -7622,33 +8129,47 @@ function mirrorConfigured(raw) {
7622
8129
  const endpoints = mirrorEndpoints(parseRegistries(raw), PERSONAL_LOCAL_REGISTRY_HOST);
7623
8130
  return endpoints.length === 1 && endpoints[0] === PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT;
7624
8131
  }
7625
- async function applyUnlessCollecting(ctx) {
8132
+ async function applyUnlessCollecting(ctx, auth) {
7626
8133
  const deployment = await readRegistryDeployment(ctx.exec);
7627
8134
  if (!deployment) throw new Error("the registry's Deployment could not be read");
7628
8135
  if (gcMarkIsLive(deployment.mark, (ctx.now ?? nowDate)().getTime(), ctx.isAlive)) return false;
7629
8136
  if (deployment.mark !== null && !await deleteSweepJob(ctx.exec)) {
7630
8137
  throw new Error("a stopped cleanup's Job could not be deleted, so the registry stays stopped");
7631
8138
  }
7632
- await kubectlApply(ctx.exec, renderLocalRegistryManifests());
8139
+ await kubectlApply(ctx.exec, renderLocalRegistryManifests(auth));
7633
8140
  if (deployment.mark !== null) await clearGcMark(ctx.exec);
7634
8141
  return true;
7635
8142
  }
7636
8143
  async function deployLocalRegistry(ctx) {
7637
8144
  await ctx.fs.ensureDir(LOCAL_REGISTRY_DATA_DIR, 448);
7638
- const applied = await applyUnlessCollecting(ctx);
8145
+ const credentials = await loadOrCreateRegistryCredentials(ctx.fs, ctx.random);
7639
8146
  const raw = await ctx.fs.readFile(REGISTRIES_FILE);
7640
- if (mirrorConfigured(raw)) return { applied, mirrorWritten: false, backupPath: null };
7641
- const next = upsertRegistryMirror(parseRegistries(raw), PERSONAL_LOCAL_REGISTRY_HOST, [
7642
- PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT
7643
- ]);
7644
- const backupPath = await writeRegistriesWithBackup(ctx.fs, raw, next, (ctx.now ?? nowDate)());
7645
- return { applied, mirrorWritten: true, backupPath };
8147
+ let file = parseRegistries(raw);
8148
+ const mirrorWritten = !mirrorConfigured(raw);
8149
+ const authWritten = !registryAuthConfigured(file, credentials);
8150
+ let backupPath = null;
8151
+ if (mirrorWritten) {
8152
+ file = upsertRegistryMirror(file, PERSONAL_LOCAL_REGISTRY_HOST, [
8153
+ PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT
8154
+ ]);
8155
+ }
8156
+ if (authWritten) file = withLocalRegistryAuth(file, credentials);
8157
+ if (mirrorWritten || authWritten) {
8158
+ backupPath = await writeRegistriesWithBackup(ctx.fs, raw, file, (ctx.now ?? nowDate)());
8159
+ }
8160
+ const authEnforced = await settleRegistryAuth(ctx, credentials, authWritten);
8161
+ await kubectlApply(ctx.exec, renderLocalRegistrySecrets(credentials));
8162
+ const applied = await applyUnlessCollecting(ctx, authEnforced ? credentials : null);
8163
+ return { applied, mirrorWritten, authWritten, backupPath, authEnforced };
7646
8164
  }
7647
8165
  async function readMirrorState(fs) {
7648
8166
  const raw = await fs.readFile(REGISTRIES_FILE);
7649
- const fingerprint = createHash("sha256").update(raw ?? "").digest("hex");
8167
+ const authPending = !await registryAuthEnforced((path) => fs.readFile(path));
8168
+ const fingerprint = createHash3("sha256").update(raw ?? "").digest("hex");
7650
8169
  if (!mirrorConfigured(raw)) return { state: "absent", fingerprint };
7651
- if (await mirrorLoaded((path) => fs.readFile(path))) return { state: "loaded", fingerprint };
8170
+ if (!authPending && await mirrorLoaded((path) => fs.readFile(path))) {
8171
+ return { state: "loaded", fingerprint };
8172
+ }
7652
8173
  const tried = (await fs.readFile(LOCAL_REGISTRY_MIRROR_RESTART_FILE))?.trim() === fingerprint;
7653
8174
  return { state: tried ? "tried" : "owed", fingerprint };
7654
8175
  }
@@ -7660,14 +8181,14 @@ async function loadRegistryMirror(ctx) {
7660
8181
  if (state === "tried") {
7661
8182
  return {
7662
8183
  restarted: false,
7663
- warning: `k3s restarted but did not load the local image registry's mirror, so pods cannot pull from that registry. Conveyor restarts k3s for it again only after /etc/rancher/k3s/registries.yaml changes. Check that k3s reads that file and keeps its data in the default directory: the mirror should appear under ${K3S_CERTS_D}.`
8184
+ warning: `k3s restarted but did not load the local image registry's mirror or credential, so pods cannot pull from that registry or the registry stays open to every account on this machine. Conveyor restarts k3s for it again only after /etc/rancher/k3s/registries.yaml changes. Check that k3s reads that file and keeps its data in the default directory: the mirror should appear under ${K3S_CERTS_D}.`
7664
8185
  };
7665
8186
  }
7666
8187
  if (state !== "owed") return { restarted: false, warning: null };
7667
8188
  if (!mayDisrupt(ctx.runningConveyorPods)) {
7668
8189
  return {
7669
8190
  restarted: false,
7670
- warning: `The local image registry needs a k3s restart to load its mirror, but ${busyReason(ctx.runningConveyorPods)} \u2014 the restart is deferred until the machine is idle.`
8191
+ warning: `The local image registry needs a k3s restart to load its mirror or credential, but ${busyReason(ctx.runningConveyorPods)} \u2014 the restart is deferred until the machine is idle.`
7671
8192
  };
7672
8193
  }
7673
8194
  const restart = await ctx.exec("systemctl", ["restart", "k3s"], { timeoutMs: 12e4 });
@@ -7849,7 +8370,18 @@ async function stdoutOf(exec, bin, args) {
7849
8370
  const run = await exec(bin, args, { timeoutMs: 1e4 }).catch(() => null);
7850
8371
  return run && run.exitCode === 0 ? run.stdout : null;
7851
8372
  }
8373
+ async function dropInDirExists(exec) {
8374
+ const run = await exec("test", ["-d", K3S_CONFIG_DROPIN_DIR], { timeoutMs: 1e4 }).catch(
8375
+ () => null
8376
+ );
8377
+ if (run === null) return null;
8378
+ if (run.exitCode === 0) return true;
8379
+ return run.exitCode === 1 && run.stderr.trim() === "" ? false : null;
8380
+ }
7852
8381
  async function readK3sConfigs(ctx) {
8382
+ const dirExists = await dropInDirExists(ctx.exec);
8383
+ if (dirExists === null) return null;
8384
+ if (!dirExists) return [await ctx.fs.readFile(K3S_OWNER_CONFIG)];
7853
8385
  const listed = await stdoutOf(ctx.exec, "find", [
7854
8386
  "-L",
7855
8387
  K3S_CONFIG_DROPIN_DIR,
@@ -8106,6 +8638,55 @@ async function reconcileIsolation(ctx, desired, result) {
8106
8638
  result.isolation = { applied };
8107
8639
  }
8108
8640
 
8641
+ // src/lib/fs-port.ts
8642
+ import { chmod, mkdir, readFile, rm, writeFile } from "fs/promises";
8643
+ var nodeFileSystem = {
8644
+ async readFile(path) {
8645
+ try {
8646
+ return await readFile(path, "utf8");
8647
+ } catch {
8648
+ return null;
8649
+ }
8650
+ },
8651
+ async writeFile(path, content, mode) {
8652
+ await mkdir(path.slice(0, path.lastIndexOf("/")), { recursive: true });
8653
+ await writeFile(path, content, { encoding: "utf8", ...mode === void 0 ? {} : { mode } });
8654
+ if (mode !== void 0) await chmod(path, mode);
8655
+ },
8656
+ async removeFile(path) {
8657
+ await rm(path, { force: true });
8658
+ },
8659
+ async ensureDir(path, mode) {
8660
+ await mkdir(path, { recursive: true });
8661
+ if (mode !== void 0) await chmod(path, mode);
8662
+ }
8663
+ };
8664
+ function createMemoryFileSystem(seed = {}) {
8665
+ const files = new Map(Object.entries(seed));
8666
+ const modes = /* @__PURE__ */ new Map();
8667
+ const removed = [];
8668
+ return {
8669
+ files,
8670
+ modes,
8671
+ removed,
8672
+ readFile: (path) => Promise.resolve(files.get(path) ?? null),
8673
+ writeFile: (path, content, mode) => {
8674
+ files.set(path, content);
8675
+ if (mode !== void 0) modes.set(path, mode);
8676
+ return Promise.resolve();
8677
+ },
8678
+ removeFile: (path) => {
8679
+ files.delete(path);
8680
+ removed.push(path);
8681
+ return Promise.resolve();
8682
+ },
8683
+ ensureDir: (path, mode) => {
8684
+ if (mode !== void 0) modes.set(path, mode);
8685
+ return Promise.resolve();
8686
+ }
8687
+ };
8688
+ }
8689
+
8109
8690
  // src/daemon/registry-tags.ts
8110
8691
  var REPO_NAME = /^[a-z0-9]+(?:[._-][a-z0-9]+)*(?:\/[a-z0-9]+(?:[._-][a-z0-9]+)*)*$/;
8111
8692
  var TAG_NAME = /^[A-Za-z0-9_][A-Za-z0-9._-]{0,127}$/;
@@ -8391,7 +8972,10 @@ async function collectRegistryGarbage(ports, options) {
8391
8972
  blobsCollected: false
8392
8973
  });
8393
8974
  const now = ports.now ?? Date.now;
8394
- const fetchImpl = ports.fetch ?? fetch;
8975
+ const fetchImpl = withRegistryCredentials(
8976
+ ports.fetch ?? fetch,
8977
+ ports.readFile ?? ((path) => nodeFileSystem.readFile(path))
8978
+ );
8395
8979
  const retain = await readRetainList(ports);
8396
8980
  if (typeof retain === "string") return skip(retain);
8397
8981
  const blocked = await registryBlocker(ports, now());
@@ -8499,7 +9083,7 @@ async function reportLocalImages(exec, inventory, wantedImages, cache) {
8499
9083
  if (!inventory) return void 0;
8500
9084
  const wanted = new Set(wantedImages);
8501
9085
  const held = inventory.images.flatMap(
8502
- (image) => (image.repoTags ?? []).filter((ref) => ref.startsWith("localhost/") && isImageRef(ref)).map((ref) => ({ ref, imageId: image.id }))
9086
+ (image) => (image.repoTags ?? []).filter((ref) => (ref.startsWith("localhost/") || wanted.has(ref)) && isImageRef(ref)).map((ref) => ({ ref, imageId: image.id }))
8503
9087
  );
8504
9088
  held.sort(
8505
9089
  (a, b) => Number(wanted.has(b.ref)) - Number(wanted.has(a.ref)) || (a.ref < b.ref ? -1 : a.ref > b.ref ? 1 : 0)
@@ -8557,17 +9141,26 @@ var GRANTED_COMMAND_NAMES = LOCAL_BAKE_ROOT_COMMANDS.map((command) => command.na
8557
9141
 
8558
9142
  // src/daemon/telemetry.ts
8559
9143
  var LOCAL_REGISTRY_WARN_AFTER_MS = 10 * 6e4;
9144
+ var LOCAL_REGISTRY_OPEN_WARNING = "The local image registry is open to every account on this machine: any of them can read and replace the images your workspaces start from. It requires its credential once k3s restarts and loads it.";
8560
9145
  async function reportLocalRegistry(ports, watch, now = Date.now()) {
8561
9146
  if (!watch) return { fields: {}, warnings: [] };
8562
- const ready = await localRegistryReady({ readFile: ports.readFile, fetch: ports.fetch });
9147
+ const { ready, authEnforced } = await localRegistryStatus({
9148
+ readFile: ports.readFile,
9149
+ fetch: ports.fetch
9150
+ });
8563
9151
  const paused = (watch.pausedUntil ?? 0) > now;
8564
9152
  watch.notReadySince = ready || paused ? null : watch.notReadySince ?? now;
8565
9153
  const stuckMs = watch.notReadySince === null ? 0 : now - watch.notReadySince;
8566
- return {
8567
- fields: { localRegistry: { host: PERSONAL_LOCAL_REGISTRY_HOST, ready } },
8568
- warnings: stuckMs >= LOCAL_REGISTRY_WARN_AFTER_MS ? [
9154
+ const warnings = [];
9155
+ if (stuckMs >= LOCAL_REGISTRY_WARN_AFTER_MS) {
9156
+ warnings.push(
8569
9157
  `The local image registry has not been ready for ${Math.floor(stuckMs / 6e4)} minutes, so shadow bakes cannot use it yet. Check its pod with \`k3s kubectl -n conveyor-system get pods -l app.kubernetes.io/name=conveyor-registry\`. If the pod is running, k3s has not loaded the registry's mirror yet: it restarts k3s for that only while no Conveyor pod runs.`
8570
- ] : []
9158
+ );
9159
+ }
9160
+ if (!authEnforced) warnings.push(LOCAL_REGISTRY_OPEN_WARNING);
9161
+ return {
9162
+ fields: { localRegistry: { host: PERSONAL_LOCAL_REGISTRY_HOST, ready, authEnforced } },
9163
+ warnings
8571
9164
  };
8572
9165
  }
8573
9166
  var VM_CAPACITY_FILE = "/etc/conveyor-vm/capacity.json";
@@ -8690,6 +9283,7 @@ async function collectHeartbeat(ports, state, inventory) {
8690
9283
  ...localImages ? { localImages } : {},
8691
9284
  ...registry.fields,
8692
9285
  ...state.isolation ? { isolation: state.isolation } : {},
9286
+ ...state.cpuQuotaApplied === void 0 ? {} : { cpuQuotaApplied: state.cpuQuotaApplied },
8693
9287
  localWarnings: withinHeartbeatBounds([
8694
9288
  ...state.localWarnings,
8695
9289
  ...gcWarning ? [gcWarning] : [],
@@ -8740,7 +9334,15 @@ async function collect(ports, now) {
8740
9334
  if (watch) watch.pausedUntil = now.getTime() + REGISTRY_GC_MARK_TTL_MS;
8741
9335
  try {
8742
9336
  const outcome = await collectRegistryGarbage(
8743
- { exec, log, requestDesiredState, fetch: fetch2, sleep: sleep2, isAlive },
9337
+ {
9338
+ exec,
9339
+ log,
9340
+ requestDesiredState,
9341
+ fetch: fetch2,
9342
+ readFile: (path) => ports.fs.readFile(path),
9343
+ sleep: sleep2,
9344
+ isAlive
9345
+ },
8744
9346
  { dryRun: false }
8745
9347
  );
8746
9348
  if (outcome.skipped) return outcome.skipped;
@@ -8786,48 +9388,7 @@ async function runRegistryUpkeep(ports, lastGcAt, maintenanceHour) {
8786
9388
  }
8787
9389
 
8788
9390
  // src/daemon/reconcile.ts
8789
- function ownerRestartWarning(change) {
8790
- return `${change} Conveyor did not install this server's k3s, so it never restarts it: run \`sudo systemctl restart k3s\` when no build runs.`;
8791
- }
8792
- async function reconcileKubelet(ctx, desired, result, managed) {
8793
- const ownerConfig = await ctx.fs.readFile(K3S_OWNER_CONFIG);
8794
- const rendered = renderK3sDropIn(desired.kubelet, ownerConfig, { disableComponents: managed });
8795
- result.skippedKubeletFlags = rendered.skipped;
8796
- result.effectiveImageGcHighThreshold = rendered.skipped.includes("image-gc-high-threshold") ? ownerImageGcHighThreshold(ownerConfig) : desired.kubelet.imageGcHighThreshold;
8797
- for (const flag of rendered.skipped) {
8798
- result.warnings.push(
8799
- `Left \`${flag}\` to your own /etc/rancher/k3s/config.yaml \u2014 Conveyor does not override a setting you chose.`
8800
- );
8801
- }
8802
- const current = await ctx.fs.readFile(K3S_CONFIG_DROPIN);
8803
- if (rendered.content === null) {
8804
- if (current !== null) await ctx.fs.removeFile(K3S_CONFIG_DROPIN);
8805
- return;
8806
- }
8807
- if (current === rendered.content) return;
8808
- await ctx.fs.ensureDir(K3S_CONFIG_DROPIN.slice(0, K3S_CONFIG_DROPIN.lastIndexOf("/")));
8809
- await ctx.fs.writeFile(K3S_CONFIG_DROPIN, rendered.content, 420);
8810
- if (!managed) {
8811
- result.warnings.push(ownerRestartWarning("Kubelet settings changed."));
8812
- return;
8813
- }
8814
- if (!mayDisrupt(ctx.runningConveyorPods)) {
8815
- result.warnings.push(
8816
- `Kubelet settings changed but ${busyReason(ctx.runningConveyorPods)} \u2014 the restart is deferred until the machine is idle.`
8817
- );
8818
- return;
8819
- }
8820
- const restart = await ctx.exec("systemctl", ["restart", "k3s"], { timeoutMs: 12e4 });
8821
- if (restart.exitCode === 0) {
8822
- result.restartedK3s = true;
8823
- } else {
8824
- result.warnings.push(
8825
- `Could not restart k3s to apply new kubelet settings: ${restart.stderr.trim().slice(0, 200)}`
8826
- );
8827
- }
8828
- }
8829
- async function reconcilePriorityClass(ctx, desired, result) {
8830
- const name = assertK8sName(desired.priorityClass.name, "priorityClass.name");
9391
+ async function livePriorityClassValue(ctx, name) {
8831
9392
  const read = await ctx.exec("k3s", [
8832
9393
  "kubectl",
8833
9394
  "get",
@@ -8836,30 +9397,59 @@ async function reconcilePriorityClass(ctx, desired, result) {
8836
9397
  "-o",
8837
9398
  "jsonpath={.value}"
8838
9399
  ]);
8839
- if (read.exitCode !== 0) return;
8840
- const existingValue = Number.parseInt(read.stdout.trim(), 10);
8841
- if (existingValue === desired.priorityClass.value) return;
9400
+ const value = Number.parseInt(read.stdout.trim(), 10);
9401
+ return read.exitCode === 0 && Number.isFinite(value) ? value : null;
9402
+ }
9403
+ async function reconcilePriorityClass(ctx, desired, result) {
9404
+ const name = assertK8sName(desired.priorityClass.name, "priorityClass.name");
9405
+ const wanted = desired.priorityClass.value;
9406
+ const existingValue = await livePriorityClassValue(ctx, name);
9407
+ if (existingValue === null || existingValue === wanted) return "ready";
9408
+ if (!declaresPriorityClass(desired.rbacManifest, name, wanted)) {
9409
+ result.warnings.push(
9410
+ `The \`${name}\` PriorityClass has value ${existingValue}, but Conveyor's manifest does not declare it at ${wanted}, so it is left as it is. This should never happen; report it.`
9411
+ );
9412
+ return "kept";
9413
+ }
8842
9414
  if (!mayDisrupt(ctx.runningConveyorPods)) {
8843
9415
  result.warnings.push(
8844
- `The \`${name}\` PriorityClass has value ${existingValue}, but Conveyor needs ${desired.priorityClass.value}. The value is immutable, so it must be recreated \u2014 deferred because ${busyReason(ctx.runningConveyorPods)}.`
9416
+ `The \`${name}\` PriorityClass has value ${existingValue}, but Conveyor needs ${wanted}. The value is immutable, so it must be recreated \u2014 deferred because ${busyReason(ctx.runningConveyorPods)}.`
8845
9417
  );
8846
- return;
9418
+ return "kept";
8847
9419
  }
8848
- const removed = await ctx.exec("k3s", ["kubectl", "delete", "priorityclass", name]);
9420
+ const removed = await ctx.exec("k3s", [
9421
+ "kubectl",
9422
+ "delete",
9423
+ "priorityclass",
9424
+ name,
9425
+ "--ignore-not-found"
9426
+ ]);
8849
9427
  if (removed.exitCode !== 0) {
8850
9428
  result.warnings.push(
8851
- `Could not replace the \`${name}\` PriorityClass (value ${existingValue}, needs ${desired.priorityClass.value}): ${removed.stderr.trim().slice(0, 160)}`
9429
+ `Could not replace the \`${name}\` PriorityClass (value ${existingValue}, needs ${wanted}): ${removed.stderr.trim().slice(0, 160)}`
8852
9430
  );
8853
- return;
9431
+ return "kept";
8854
9432
  }
9433
+ return "removed";
9434
+ }
9435
+ async function applyRbac(ctx, desired, result) {
9436
+ const outcome = await reconcilePriorityClass(ctx, desired, result);
9437
+ const manifest = outcome === "kept" ? withoutKind(desired.rbacManifest, "PriorityClass") : desired.rbacManifest;
8855
9438
  try {
8856
- await kubectlApply(ctx.exec, desired.rbacManifest);
8857
- result.recreatedPriorityClass = true;
9439
+ await kubectlApply(ctx.exec, manifest);
8858
9440
  } catch (error) {
9441
+ if (outcome !== "removed") throw error;
9442
+ const value = await livePriorityClassValue(ctx, desired.priorityClass.name).catch(() => null);
9443
+ if (value === desired.priorityClass.value) {
9444
+ result.recreatedPriorityClass = true;
9445
+ throw error;
9446
+ }
8859
9447
  result.warnings.push(
8860
- `The \`${name}\` PriorityClass was removed to change its value but could not be recreated: ${error instanceof Error ? error.message : String(error)}. No build can start on this machine until it exists \u2014 run \`conveyor-k3 sync\`.`
9448
+ `The \`${desired.priorityClass.name}\` PriorityClass was removed to change its value but could not be recreated: ${error instanceof Error ? error.message : String(error)}. No build can start on this machine until it exists \u2014 run \`conveyor-k3 sync\`.`
8861
9449
  );
9450
+ return;
8862
9451
  }
9452
+ if (outcome === "removed") result.recreatedPriorityClass = true;
8863
9453
  }
8864
9454
  async function step(result, label, run) {
8865
9455
  try {
@@ -8885,12 +9475,7 @@ async function reconcile(ctx, desired) {
8885
9475
  `Conveyor sent a cluster manifest containing disallowed kind(s) ${disallowedKinds.join(", ")} \u2014 refused. RBAC and PriorityClass steps skipped; this should never happen, report it.`
8886
9476
  );
8887
9477
  } else {
8888
- await step(result, "Could not apply RBAC", () => kubectlApply(ctx.exec, desired.rbacManifest));
8889
- await step(
8890
- result,
8891
- "Could not reconcile the PriorityClass",
8892
- () => reconcilePriorityClass(ctx, desired, result)
8893
- );
9478
+ await step(result, "Could not apply RBAC", () => applyRbac(ctx, desired, result));
8894
9479
  }
8895
9480
  await step(
8896
9481
  result,
@@ -8922,11 +9507,23 @@ async function reconcile(ctx, desired) {
8922
9507
  "Could not apply kubelet settings",
8923
9508
  () => reconcileKubelet(ctx, desired, result, managed)
8924
9509
  );
9510
+ if (desired.isolation !== void 0) {
9511
+ await step(result, "Could not cap pod CPU", async () => {
9512
+ result.cpuQuotaApplied = await reconcilePodCpuQuota(ctx.exec, desired, result.warnings);
9513
+ });
9514
+ if (desired.kubelet.systemReserved?.cpu && result.cpuQuotaApplied === void 0) {
9515
+ result.cpuQuotaApplied = false;
9516
+ }
9517
+ }
8925
9518
  if (registry && !result.restartedK3s) {
8926
9519
  await step(result, "Could not load the local image registry mirror", async () => {
8927
9520
  if (!managed) {
8928
9521
  if (await registryMirrorPending(ctx.fs))
8929
- result.warnings.push(ownerRestartWarning("The local image registry's mirror is new."));
9522
+ result.warnings.push(
9523
+ ownerRestartWarning(
9524
+ "The local image registry's mirror or credential is new, and the registry stays open to every account on this machine until k3s loads it."
9525
+ )
9526
+ );
8930
9527
  return;
8931
9528
  }
8932
9529
  const loaded = await loadRegistryMirror(ctx);
@@ -8989,6 +9586,8 @@ var ClusterDaemon = class {
8989
9586
  * against k3s's own settings before reporting it.
8990
9587
  */
8991
9588
  isolation = void 0;
9589
+ /** Whether the pod slice's CPU cap is in force, from the last reconcile. */
9590
+ cpuQuotaApplied = void 0;
8992
9591
  timers = [];
8993
9592
  /** When the last store walk started, so one that failed is not retried on every reconcile. */
8994
9593
  storeWalkStartedAt = 0;
@@ -9071,6 +9670,7 @@ var ClusterDaemon = class {
9071
9670
  );
9072
9671
  this.warnings = result.warnings;
9073
9672
  this.isolation = result.isolation;
9673
+ this.cpuQuotaApplied = result.cpuQuotaApplied;
9074
9674
  this.scheduleIsolationRetry();
9075
9675
  for (const warning of result.warnings) this.ports.log(warning);
9076
9676
  if (result.prunedNamespaces.length > 0) {
@@ -9227,6 +9827,11 @@ var ClusterDaemon = class {
9227
9827
  );
9228
9828
  }
9229
9829
  for (const failure of result.failures) this.ports.log(`Image prune failed \u2014 ${failure}`);
9830
+ if (result.pending.length > 0) {
9831
+ this.ports.log(
9832
+ `Image removal still in progress for ${result.pending.length} image(s); the next prune re-checks them.`
9833
+ );
9834
+ }
9230
9835
  } catch (error) {
9231
9836
  this.ports.log(
9232
9837
  `Skipping image prune: ${error instanceof Error ? error.message : String(error)}`
@@ -9265,6 +9870,7 @@ var ClusterDaemon = class {
9265
9870
  localImageHashes: this.localImageHashes,
9266
9871
  ...registryOn ? { localRegistry: this.registryWatch } : {},
9267
9872
  ...isolation.isolation ? { isolation: isolation.isolation } : {},
9873
+ cpuQuotaApplied: this.cpuQuotaApplied,
9268
9874
  k3sStoreBytes: this.measuredStoreBytes
9269
9875
  },
9270
9876
  inventory
@@ -9399,55 +10005,6 @@ function renderAllUnits(options) {
9399
10005
  };
9400
10006
  }
9401
10007
 
9402
- // src/lib/fs-port.ts
9403
- import { chmod, mkdir, readFile, rm, writeFile } from "fs/promises";
9404
- var nodeFileSystem = {
9405
- async readFile(path) {
9406
- try {
9407
- return await readFile(path, "utf8");
9408
- } catch {
9409
- return null;
9410
- }
9411
- },
9412
- async writeFile(path, content, mode) {
9413
- await mkdir(path.slice(0, path.lastIndexOf("/")), { recursive: true });
9414
- await writeFile(path, content, "utf8");
9415
- if (mode !== void 0) await chmod(path, mode);
9416
- },
9417
- async removeFile(path) {
9418
- await rm(path, { force: true });
9419
- },
9420
- async ensureDir(path, mode) {
9421
- await mkdir(path, { recursive: true });
9422
- if (mode !== void 0) await chmod(path, mode);
9423
- }
9424
- };
9425
- function createMemoryFileSystem(seed = {}) {
9426
- const files = new Map(Object.entries(seed));
9427
- const modes = /* @__PURE__ */ new Map();
9428
- const removed = [];
9429
- return {
9430
- files,
9431
- modes,
9432
- removed,
9433
- readFile: (path) => Promise.resolve(files.get(path) ?? null),
9434
- writeFile: (path, content, mode) => {
9435
- files.set(path, content);
9436
- if (mode !== void 0) modes.set(path, mode);
9437
- return Promise.resolve();
9438
- },
9439
- removeFile: (path) => {
9440
- files.delete(path);
9441
- removed.push(path);
9442
- return Promise.resolve();
9443
- },
9444
- ensureDir: (path, mode) => {
9445
- if (mode !== void 0) modes.set(path, mode);
9446
- return Promise.resolve();
9447
- }
9448
- };
9449
- }
9450
-
9451
10008
  // src/verbs.ts
9452
10009
  var VERBS = [
9453
10010
  "vm",
@@ -9613,17 +10170,36 @@ async function removeLocalRegistry(ports) {
9613
10170
  `Could not remove the local image registry from the cluster: ${removed.stderr.trim().slice(0, 200)}`
9614
10171
  );
9615
10172
  }
10173
+ for (const [namespace, name] of [
10174
+ [PERSONAL_LOCAL_REGISTRY_NAMESPACE, LOCAL_REGISTRY_HTPASSWD_SECRET],
10175
+ [PERSONAL_BAKE_NAMESPACE, PERSONAL_LOCAL_REGISTRY_PUSH_SECRET]
10176
+ ]) {
10177
+ const secret = await ports.exec(
10178
+ "k3s",
10179
+ ["kubectl", "-n", namespace, "delete", "secret", name, "--ignore-not-found"],
10180
+ { timeoutMs: 6e4 }
10181
+ );
10182
+ if (secret.exitCode !== 0) {
10183
+ ports.log(
10184
+ `Could not remove the local image registry's Secret ${namespace}/${name}: ${secret.stderr.trim().slice(0, 200)}`
10185
+ );
10186
+ }
10187
+ }
9616
10188
  const raw = await ports.fs.readFile(REGISTRIES_FILE);
9617
10189
  const parsed = parseRegistries(raw);
9618
- if (mirrorEndpoints(parsed, PERSONAL_LOCAL_REGISTRY_HOST).length === 0) return;
10190
+ const next = LOCAL_REGISTRY_AUTH_HOSTS.reduce(
10191
+ removeRegistryAuth,
10192
+ removeRegistryMirror(parsed, PERSONAL_LOCAL_REGISTRY_HOST)
10193
+ );
10194
+ if (serializeRegistries(next) === serializeRegistries(parsed)) return;
9619
10195
  const backup = await writeRegistriesWithBackup(
9620
10196
  ports.fs,
9621
10197
  raw,
9622
- removeRegistryMirror(parsed, PERSONAL_LOCAL_REGISTRY_HOST),
10198
+ next,
9623
10199
  (ports.now ?? (() => /* @__PURE__ */ new Date()))()
9624
10200
  );
9625
10201
  ports.log(
9626
- `Removed the local image registry's mirror from ${REGISTRIES_FILE} (backup: ${backup}).`
10202
+ `Removed the local image registry's mirror and credential from ${REGISTRIES_FILE} (backup: ${backup}).`
9627
10203
  );
9628
10204
  }
9629
10205
  async function removeIsolation(ports) {
@@ -9680,6 +10256,10 @@ async function uninstall(ports, options = {}) {
9680
10256
  }
9681
10257
  await removeLocalRegistry(ports);
9682
10258
  await removeIsolation(ports);
10259
+ const lifted = await liftPodCpuQuota(ports.exec).catch(
10260
+ (error) => error instanceof Error ? error.message : String(error)
10261
+ );
10262
+ if (lifted) ports.log(`Could not lift Conveyor's CPU cap on kubepods.slice: ${lifted}`);
9683
10263
  for (const path of OWNED_FILES) {
9684
10264
  await ports.fs.removeFile(path);
9685
10265
  result.removedFiles.push(path);
@@ -9732,6 +10312,45 @@ async function uninstall(ports, options = {}) {
9732
10312
  return result;
9733
10313
  }
9734
10314
 
10315
+ // src/lib/bins.ts
10316
+ var INSTALLER_PATHS = {
10317
+ bun: "/root/.bun/bin/bun",
10318
+ cli: "/root/.bun/bin/conveyor-k3",
10319
+ cloudflared: "/usr/local/bin/cloudflared"
10320
+ };
10321
+ var CLI_PATH_LINK = "/usr/local/bin/conveyor-k3";
10322
+ async function linkCliOnPath(exec, target = INSTALLER_PATHS.cli) {
10323
+ if (target === CLI_PATH_LINK) return { linked: false };
10324
+ const present = await exec("test", ["-x", target]).catch(() => null);
10325
+ if (present?.exitCode !== 0) return { linked: false };
10326
+ const link = await exec("ln", ["-sfn", target, CLI_PATH_LINK]).catch(() => null);
10327
+ if (link?.exitCode === 0) return { linked: true };
10328
+ return {
10329
+ linked: false,
10330
+ problem: `Could not link ${CLI_PATH_LINK} to ${target}: ${link?.stderr.trim().slice(0, 200) || "ln failed"}`
10331
+ };
10332
+ }
10333
+
10334
+ // src/lib/cloudflared.ts
10335
+ var PACKAGE_MANAGER_REFUSAL = "installed by a package manager";
10336
+ async function resolveRealPath(exec, bin) {
10337
+ const result = await exec("readlink", ["-f", bin]).catch(() => null);
10338
+ const resolved = result?.exitCode === 0 ? result.stdout.trim() : "";
10339
+ return resolved.startsWith("/") ? resolved : bin;
10340
+ }
10341
+ async function cloudflaredUpdater(exec, bin) {
10342
+ const real = await resolveRealPath(exec, bin);
10343
+ const owner = await exec("dpkg", ["-S", real]).catch(() => null);
10344
+ return owner?.exitCode === 0 ? "package-manager" : "conveyor";
10345
+ }
10346
+ function isPackageManagerRefusal(output) {
10347
+ return `${output.stdout}
10348
+ ${output.stderr}`.toLowerCase().includes(PACKAGE_MANAGER_REFUSAL);
10349
+ }
10350
+ function describeCloudflaredUpdater(updater) {
10351
+ return updater === "package-manager" ? "cloudflared is updated by your package manager, not by Conveyor" : "cloudflared is updated by Conveyor's daily update";
10352
+ }
10353
+
9735
10354
  // src/commands/update.ts
9736
10355
  var CLOUDFLARED_UPDATED_EXIT = 11;
9737
10356
  function readExecStart(unit) {
@@ -9819,7 +10438,22 @@ async function updatePackage(ports, result, targetVersion) {
9819
10438
  await ports.exec("systemctl", ["restart", DAEMON_UNIT]);
9820
10439
  }
9821
10440
  async function updateCloudflared(ports, result) {
10441
+ const packaged = () => {
10442
+ result.cloudflaredUpdater = "package-manager";
10443
+ result.notes.push(
10444
+ `cloudflared update skipped: ${describeCloudflaredUpdater("package-manager")}`
10445
+ );
10446
+ };
10447
+ if (await cloudflaredUpdater(ports.exec, ports.bins.cloudflared) === "package-manager") {
10448
+ packaged();
10449
+ return;
10450
+ }
9822
10451
  const update = await ports.exec(ports.bins.cloudflared, ["update"], { timeoutMs: 5 * 6e4 });
10452
+ if (isPackageManagerRefusal(update)) {
10453
+ packaged();
10454
+ return;
10455
+ }
10456
+ result.cloudflaredUpdater = "conveyor";
9823
10457
  if (update.exitCode !== CLOUDFLARED_UPDATED_EXIT) return;
9824
10458
  result.cloudflaredUpdated = true;
9825
10459
  ports.log("cloudflared was updated; restarting the tunnel.");
@@ -9829,10 +10463,13 @@ async function runUpdate(ports, decision, channel, targetVersion) {
9829
10463
  const result = {
9830
10464
  packageUpdated: false,
9831
10465
  cloudflaredUpdated: false,
10466
+ cloudflaredUpdater: null,
9832
10467
  k3sUpgraded: false,
9833
10468
  notes: []
9834
10469
  };
9835
10470
  await updatePackage(ports, result, targetVersion);
10471
+ const linked = await linkCliOnPath(ports.exec);
10472
+ if (linked.problem) result.notes.push(linked.problem);
9836
10473
  await updateCloudflared(ports, result);
9837
10474
  if (!decision.upgrade) {
9838
10475
  result.notes.push(`k3s upgrade skipped: ${decision.reason}`);
@@ -9865,6 +10502,9 @@ async function runUpdate(ports, decision, channel, targetVersion) {
9865
10502
  return result;
9866
10503
  }
9867
10504
 
10505
+ // src/lib/env.ts
10506
+ import { readFileSync as readFileSync3 } from "fs";
10507
+
9868
10508
  // src/commands/setup.ts
9869
10509
  var K3S_MIN_MAJOR_MINOR = [1, 29];
9870
10510
  function parseK3sVersion(output) {
@@ -10761,22 +11401,6 @@ function rewriteQemuArgs(args, network, ncPath = "/usr/bin/nc", hostContext = {}
10761
11401
  return output;
10762
11402
  }
10763
11403
 
10764
- // src/vm/memory-budget.ts
10765
- var RELAY_RSS_ALLOWANCE_MIB = 128;
10766
- var BASE_RELAY_SLOTS = 16;
10767
- function relayAllowanceGiB(relaySlots = BASE_RELAY_SLOTS) {
10768
- return Math.max(0, relaySlots - BASE_RELAY_SLOTS) * RELAY_RSS_ALLOWANCE_MIB / 1024;
10769
- }
10770
- function maximumGuestMemoryGiBWithin(budgetGiB, relaySlots = BASE_RELAY_SLOTS) {
10771
- const guestBudget = budgetGiB - relayAllowanceGiB(relaySlots);
10772
- if (!Number.isFinite(guestBudget) || guestBudget <= 0) return 0;
10773
- return Math.max(0, Math.floor(Math.min(guestBudget - 1, guestBudget / 1.25)));
10774
- }
10775
- function maximumGuestMemoryGiB(hostBytes, relaySlots = BASE_RELAY_SLOTS) {
10776
- if (!Number.isFinite(hostBytes) || hostBytes <= 0) return 0;
10777
- return maximumGuestMemoryGiBWithin(hostBytes / 1024 ** 3 * 0.75, relaySlots);
10778
- }
10779
-
10780
11404
  // src/vm/host-state.ts
10781
11405
  import { access, chmod as chmod2, lstat, mkdir as mkdir2, readFile as readFile2, rename, rm as rm2, writeFile as writeFile2 } from "fs/promises";
10782
11406
  import { homedir, uptime } from "os";
@@ -10868,7 +11492,6 @@ export {
10868
11492
  renderAllUnits,
10869
11493
  renderGuestDnsProvision,
10870
11494
  renderK3sDropIn,
10871
- renderKubeletDropIn,
10872
11495
  renderRegistryAuthSummary,
10873
11496
  resolveCloudflaredSrvRecords,
10874
11497
  resolveNetwork,