@rallycry/conveyor-k3 0.1.13 → 0.1.14

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
@@ -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(),
@@ -6266,6 +6275,8 @@ var ANTHROPIC_CATALOG = [
6266
6275
  anthropicEntry(DEFAULT_HAIKU_MODEL, "Haiku 4.5", 1, 5, { supportsEffort: false }),
6267
6276
  anthropicEntry(FABLE_MODEL, "Fable 5.1", 10, 50)
6268
6277
  ];
6278
+ var PROJECT_ARCHIVE_RETENTION_DAYS = 30;
6279
+ var RETENTION_MS = PROJECT_ARCHIVE_RETENTION_DAYS * 24 * 60 * 60 * 1e3;
6269
6280
  var CLAUDESPACE_WORKLOAD_LABEL = "rc-workload";
6270
6281
  var CLAUDESPACE_WORKLOAD_VALUE = "claudespace";
6271
6282
  var CONVEYOR_POD_SELECTOR = `${CLAUDESPACE_WORKLOAD_LABEL}=${CLAUDESPACE_WORKLOAD_VALUE}`;
@@ -6292,6 +6303,8 @@ var PERSONAL_LOCAL_REGISTRY_NAME = "conveyor-registry";
6292
6303
  var PERSONAL_LOCAL_REGISTRY_PORT = 5e3;
6293
6304
  var PERSONAL_LOCAL_REGISTRY_HOST = `${PERSONAL_LOCAL_REGISTRY_NAME}.${PERSONAL_LOCAL_REGISTRY_NAMESPACE}.svc.cluster.local:${PERSONAL_LOCAL_REGISTRY_PORT}`;
6294
6305
  var PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT = `http://127.0.0.1:${PERSONAL_LOCAL_REGISTRY_PORT}`;
6306
+ var PERSONAL_LOCAL_REGISTRY_PUSH_SECRET = "conveyor-registry-push";
6307
+ var PERSONAL_LOCAL_REGISTRY_PUSH_SECRET_KEY = "config.json";
6295
6308
  var BAKE_COMPONENT_LABEL = "claudespace.rallycry.com/component";
6296
6309
  var BAKE_COMPONENT_PREBAKE = "prebake";
6297
6310
  var BAKE_COMPONENT_STATE_BAKE = "state-bake";
@@ -6957,14 +6970,22 @@ async function readImageInventory(exec) {
6957
6970
  }
6958
6971
  return { images, referencedIds };
6959
6972
  }
6973
+ var CRICTL_RMI_TIMEOUT = "120s";
6974
+ var RMI_EXEC_TIMEOUT_MS = 15e4;
6975
+ var DEADLINE_EXCEEDED = "context deadline exceeded";
6960
6976
  async function pruneStrandedImages(exec, inventory, wantedImages) {
6961
6977
  const candidates = selectStrandedImages(inventory, wantedImages);
6962
- const result = { removed: 0, bytesReclaimed: 0, failures: [] };
6978
+ const result = { removed: 0, bytesReclaimed: 0, failures: [], pending: [] };
6963
6979
  for (const candidate of candidates) {
6964
- const removal = await exec("crictl", ["rmi", candidate.id]);
6980
+ const removal = await exec("crictl", ["--timeout", CRICTL_RMI_TIMEOUT, "rmi", candidate.id], {
6981
+ timeoutMs: RMI_EXEC_TIMEOUT_MS
6982
+ });
6965
6983
  if (removal.exitCode === 0) {
6966
6984
  result.removed += 1;
6967
6985
  result.bytesReclaimed += candidate.sizeBytes;
6986
+ } else if (`${removal.stderr}
6987
+ ${removal.stdout}`.includes(DEADLINE_EXCEEDED)) {
6988
+ result.pending.push(candidate.id);
6968
6989
  } else {
6969
6990
  result.failures.push(`${candidate.id}: ${removal.stderr.trim().slice(0, 120)}`);
6970
6991
  }
@@ -7074,6 +7095,8 @@ var STATE_DIR = "/var/lib/conveyor-k3";
7074
7095
  var STATE_FILE = `${STATE_DIR}/state.json`;
7075
7096
  var LOCAL_REGISTRY_DATA_DIR = `${STATE_DIR}/registry`;
7076
7097
  var LOCAL_REGISTRY_MIRROR_RESTART_FILE = `${STATE_DIR}/registry-mirror-restart`;
7098
+ var LOCAL_REGISTRY_CREDENTIALS_FILE = `${STATE_DIR}/registry-credentials.json`;
7099
+ var LOCAL_REGISTRY_AUTH_STATE_FILE = `${STATE_DIR}/registry-auth.json`;
7077
7100
  var RANCHER_DIR = "/etc/rancher/k3s";
7078
7101
  var K3S_OWNER_CONFIG = `${RANCHER_DIR}/config.yaml`;
7079
7102
  var K3S_CONFIG_DROPIN_DIR = `${RANCHER_DIR}/config.yaml.d`;
@@ -7123,6 +7146,16 @@ function upsertRegistryAuth(file, host, auth) {
7123
7146
  next.configs = configs;
7124
7147
  return next;
7125
7148
  }
7149
+ function removeRegistryAuth(file, host) {
7150
+ const existing = file.configs?.[host];
7151
+ if (!existing?.auth) return file;
7152
+ const { auth: _auth, ...rest } = existing;
7153
+ const { [host]: _removed, ...others } = file.configs ?? {};
7154
+ const configs = Object.keys(rest).length > 0 ? { ...others, [host]: rest } : others;
7155
+ const next = { ...file, configs };
7156
+ if (Object.keys(configs).length === 0) delete next.configs;
7157
+ return next;
7158
+ }
7126
7159
  function mirrorEndpoints(file, host) {
7127
7160
  const entry = file.mirrors?.[host];
7128
7161
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) return [];
@@ -7179,6 +7212,85 @@ function managesK3s(desired, state) {
7179
7212
  return desired.isolation === void 0 || state.k3sInstalledByConveyor === true;
7180
7213
  }
7181
7214
 
7215
+ // src/lib/manifest-guard.ts
7216
+ import { isAlias, isMap, isScalar, parseAllDocuments } from "yaml";
7217
+ var ALLOWED_MANIFEST_KINDS = [
7218
+ "Namespace",
7219
+ "Role",
7220
+ "RoleBinding",
7221
+ "ClusterRole",
7222
+ "ClusterRoleBinding",
7223
+ "PriorityClass"
7224
+ ];
7225
+ var ALLOWED = new Set(ALLOWED_MANIFEST_KINDS);
7226
+ var MISSING_KIND = "(missing kind)";
7227
+ var UNPARSEABLE = "(unparseable)";
7228
+ var LIST_SHAPED = "(list-shaped document)";
7229
+ function isListShaped(map) {
7230
+ return map.items.some(({ key }) => {
7231
+ if (isAlias(key)) return true;
7232
+ const name = isScalar(key) ? key.value : key;
7233
+ return typeof name === "string" && (name === "<<" || name.toLowerCase() === "items");
7234
+ });
7235
+ }
7236
+ function findDisallowedKinds(manifest) {
7237
+ let documents;
7238
+ try {
7239
+ documents = parseAllDocuments(manifest);
7240
+ } catch {
7241
+ return [UNPARSEABLE];
7242
+ }
7243
+ const disallowed = [];
7244
+ const record = (kind) => {
7245
+ if (!disallowed.includes(kind)) disallowed.push(kind);
7246
+ };
7247
+ for (const doc of documents) {
7248
+ if (doc.errors.length > 0) {
7249
+ record(UNPARSEABLE);
7250
+ continue;
7251
+ }
7252
+ if (doc.contents === null || doc.contents === void 0) continue;
7253
+ if (isScalar(doc.contents) && doc.contents.value === null) continue;
7254
+ if (!isMap(doc.contents)) {
7255
+ record(MISSING_KIND);
7256
+ continue;
7257
+ }
7258
+ if (isListShaped(doc.contents)) {
7259
+ record(LIST_SHAPED);
7260
+ continue;
7261
+ }
7262
+ const kind = doc.get("kind");
7263
+ if (typeof kind !== "string" || kind.length === 0) {
7264
+ record(MISSING_KIND);
7265
+ continue;
7266
+ }
7267
+ if (!ALLOWED.has(kind)) record(kind);
7268
+ }
7269
+ return disallowed;
7270
+ }
7271
+ function withoutKind(manifest, kind) {
7272
+ return parseAllDocuments(manifest).filter((doc) => !(isMap(doc.contents) && doc.get("kind") === kind)).map((doc) => doc.toString()).join("---\n");
7273
+ }
7274
+ function declaresPriorityClass(manifest, name, value) {
7275
+ return parseAllDocuments(manifest).some(
7276
+ (doc) => isMap(doc.contents) && doc.get("kind") === "PriorityClass" && doc.getIn(["metadata", "name"]) === name && doc.get("value") === value
7277
+ );
7278
+ }
7279
+
7280
+ // src/daemon/cluster-ops.ts
7281
+ async function kubectlApply(exec, manifest) {
7282
+ await runOrThrow(exec, "k3s", ["kubectl", "apply", "-f", "-"], {
7283
+ stdin: manifest,
7284
+ timeoutMs: 6e4
7285
+ });
7286
+ }
7287
+ function mayDisrupt(runningConveyorPods) {
7288
+ return runningConveyorPods === 0;
7289
+ }
7290
+ function busyReason(runningConveyorPods) {
7291
+ return runningConveyorPods === null ? "the number of running Conveyor pods could not be read" : `${runningConveyorPods} Conveyor pod(s) are running`;
7292
+ }
7293
+
7182
7294
  // src/lib/k3s-config.ts
7183
7295
  var IMAGE_GC_HIGH = "image-gc-high-threshold";
7184
7296
  var IMAGE_GC_LOW = "image-gc-low-threshold";
@@ -7263,76 +7375,345 @@ function renderK3sDropIn(kubelet, ownerConfig, options = {}) {
7263
7375
  }
7264
7376
  return { content: [...lines2, ""].join("\n"), skipped };
7265
7377
  }
7266
- var renderKubeletDropIn = renderK3sDropIn;
7267
7378
 
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];
7379
+ // src/daemon/pod-cpu-quota.ts
7380
+ var POD_SLICE = "kubepods.slice";
7381
+ function cpuMillicores(quantity) {
7382
+ const checked = assertQuantity(quantity, "reserveCpu");
7383
+ const match = /^(\d+(?:\.\d+)?)(m?)$/.exec(checked);
7384
+ if (!match?.[1])
7385
+ throw new Error(`Invalid reserveCpu: ${JSON.stringify(checked)} is not a CPU amount`);
7386
+ const amount = Number.parseFloat(match[1]);
7387
+ return Math.round(match[2] === "m" ? amount : amount * 1e3);
7388
+ }
7389
+ function podCpuQuotaPercent(cores, reservedMillicores) {
7390
+ const left = cores * 1e3 - reservedMillicores;
7391
+ if (left < 10) return null;
7392
+ return Math.floor(left / 10);
7393
+ }
7394
+ function setQuota(exec, value) {
7395
+ return exec("systemctl", ["set-property", POD_SLICE, `CPUQuota=${value}`]);
7396
+ }
7397
+ async function liftPodCpuQuota(exec) {
7398
+ const shown = await exec("systemctl", ["show", POD_SLICE, "-p", "CPUQuotaPerSecUSec", "--value"]);
7399
+ if (shown.exitCode !== 0 || ["", "infinity"].includes(shown.stdout.trim())) return null;
7400
+ const cleared = await setQuota(exec, "");
7401
+ return cleared.exitCode === 0 ? null : cleared.stderr.trim().slice(0, 200);
7402
+ }
7403
+ async function reconcilePodCpuQuota(exec, desired, warnings) {
7404
+ const reserve = desired.kubelet.systemReserved?.cpu;
7405
+ if (!reserve) {
7406
+ const failure = await liftPodCpuQuota(exec);
7407
+ if (failure) warnings.push(`Could not lift the CPU cap on ${POD_SLICE}: ${failure}`);
7408
+ return void 0;
7287
7409
  }
7288
- const disallowed = [];
7289
- const record = (kind) => {
7290
- if (!disallowed.includes(kind)) disallowed.push(kind);
7410
+ const reserved = cpuMillicores(reserve);
7411
+ const state = await exec("systemctl", ["show", POD_SLICE, "-p", "ActiveState", "--value"]);
7412
+ if (state.exitCode !== 0 || state.stdout.trim() !== "active") {
7413
+ warnings.push(
7414
+ `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.`
7415
+ );
7416
+ return false;
7417
+ }
7418
+ const nproc = await exec("nproc", []);
7419
+ const cores = Number.parseInt(nproc.stdout.trim(), 10);
7420
+ if (nproc.exitCode !== 0 || !Number.isInteger(cores) || cores <= 0) {
7421
+ warnings.push(
7422
+ "Could not count this machine's CPUs, so the CPU you keep is weighted, not capped."
7423
+ );
7424
+ return false;
7425
+ }
7426
+ const percent = podCpuQuotaPercent(cores, reserved);
7427
+ if (percent === null) {
7428
+ warnings.push(
7429
+ `The CPU you keep (${reserve}) leaves no CPU for builds on this ${cores}-CPU machine. Lower it in your Conveyor profile.`
7430
+ );
7431
+ return false;
7432
+ }
7433
+ const set = await setQuota(exec, `${percent}%`);
7434
+ if (set.exitCode !== 0) {
7435
+ warnings.push(
7436
+ `Could not cap pod CPU at ${percent / 100} CPUs: ${set.stderr.trim().slice(0, 200)}`
7437
+ );
7438
+ return false;
7439
+ }
7440
+ return true;
7441
+ }
7442
+
7443
+ // src/daemon/kubelet-upkeep.ts
7444
+ function ownerRestartWarning(change) {
7445
+ return `${change} Conveyor did not install this server's k3s, so it never restarts it: run \`sudo systemctl restart k3s\` when no build runs.`;
7446
+ }
7447
+ async function reconcileKubelet(ctx, desired, result, managed) {
7448
+ const ownerConfig = await ctx.fs.readFile(K3S_OWNER_CONFIG);
7449
+ const rendered = renderK3sDropIn(desired.kubelet, ownerConfig, { disableComponents: managed });
7450
+ result.skippedKubeletFlags = rendered.skipped;
7451
+ result.effectiveImageGcHighThreshold = rendered.skipped.includes("image-gc-high-threshold") ? ownerImageGcHighThreshold(ownerConfig) : desired.kubelet.imageGcHighThreshold;
7452
+ for (const flag of rendered.skipped) {
7453
+ result.warnings.push(
7454
+ `Left \`${flag}\` to your own /etc/rancher/k3s/config.yaml \u2014 Conveyor does not override a setting you chose.`
7455
+ );
7456
+ }
7457
+ const current = await ctx.fs.readFile(K3S_CONFIG_DROPIN);
7458
+ if (rendered.content === null) {
7459
+ if (current !== null) await ctx.fs.removeFile(K3S_CONFIG_DROPIN);
7460
+ return;
7461
+ }
7462
+ if (current === rendered.content) return;
7463
+ await ctx.fs.ensureDir(K3S_CONFIG_DROPIN.slice(0, K3S_CONFIG_DROPIN.lastIndexOf("/")));
7464
+ await ctx.fs.writeFile(K3S_CONFIG_DROPIN, rendered.content, 420);
7465
+ if (!managed) {
7466
+ result.warnings.push(ownerRestartWarning("Kubelet settings changed."));
7467
+ return;
7468
+ }
7469
+ if (!mayDisrupt(ctx.runningConveyorPods)) {
7470
+ result.warnings.push(
7471
+ `Kubelet settings changed but ${busyReason(ctx.runningConveyorPods)} \u2014 the restart is deferred until the machine is idle.`
7472
+ );
7473
+ return;
7474
+ }
7475
+ const restart = await ctx.exec("systemctl", ["restart", "k3s"], { timeoutMs: 12e4 });
7476
+ if (restart.exitCode === 0) {
7477
+ result.restartedK3s = true;
7478
+ } else {
7479
+ result.warnings.push(
7480
+ `Could not restart k3s to apply new kubelet settings: ${restart.stderr.trim().slice(0, 200)}`
7481
+ );
7482
+ }
7483
+ }
7484
+
7485
+ // src/daemon/local-registry-upkeep.ts
7486
+ import { createHash as createHash3 } from "crypto";
7487
+
7488
+ // src/lib/local-registry.ts
7489
+ import { createHash as createHash2 } from "crypto";
7490
+
7491
+ // src/lib/registry-credentials.ts
7492
+ import { createHash, randomBytes as randomBytes2 } from "crypto";
7493
+
7494
+ // src/lib/bcrypt.ts
7495
+ var P_WORDS = 18;
7496
+ var S_WORDS = 1024;
7497
+ var MAGIC = "OrpheanBeholderScryDoubt";
7498
+ var ALPHABET = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
7499
+ var MAX_KEY_BYTES = 72;
7500
+ var BCRYPT_SALT_BYTES = 16;
7501
+ function scaledAtanInverse(x, one) {
7502
+ const xSquared = x * x;
7503
+ let power = one / x;
7504
+ let sum = 0n;
7505
+ for (let n = 0n; power !== 0n; n++) {
7506
+ const term = power / (2n * n + 1n);
7507
+ sum = n % 2n === 0n ? sum + term : sum - term;
7508
+ power /= xSquared;
7509
+ }
7510
+ return sum;
7511
+ }
7512
+ function piFractionWords(count) {
7513
+ const guard = 64n;
7514
+ const bits = BigInt(count * 32) + guard;
7515
+ const one = 1n << bits;
7516
+ const pi = 16n * scaledAtanInverse(5n, one) - 4n * scaledAtanInverse(239n, one);
7517
+ const fraction = pi - 3n * one >> guard;
7518
+ const words = new Uint32Array(count);
7519
+ for (let index = 0; index < count; index++) {
7520
+ words[index] = Number(fraction >> BigInt(32 * (count - 1 - index)) & 0xffffffffn);
7521
+ }
7522
+ return words;
7523
+ }
7524
+ var initialState = null;
7525
+ function blowfishInitialState() {
7526
+ if (!initialState) {
7527
+ const words = piFractionWords(P_WORDS + S_WORDS);
7528
+ initialState = { p: words.slice(0, P_WORDS), s: words.slice(P_WORDS) };
7529
+ }
7530
+ return initialState;
7531
+ }
7532
+ function word(array, index) {
7533
+ return array[index] ?? 0;
7534
+ }
7535
+ function round(s, x) {
7536
+ const mixed = word(s, x >>> 24) + word(s, 256 + (x >>> 16 & 255)) ^ word(s, 512 + (x >>> 8 & 255));
7537
+ return mixed + word(s, 768 + (x & 255)) | 0;
7538
+ }
7539
+ function encipher(state, data, offset) {
7540
+ const { p, s } = state;
7541
+ let left = word(data, offset) ^ word(p, 0);
7542
+ let right = word(data, offset + 1);
7543
+ for (let index = 1; index <= 16; index += 2) {
7544
+ right ^= round(s, left) ^ word(p, index);
7545
+ left ^= round(s, right) ^ word(p, index + 1);
7546
+ }
7547
+ data[offset] = right ^ word(p, 17);
7548
+ data[offset + 1] = left;
7549
+ }
7550
+ function wordStream(bytes) {
7551
+ let position = 0;
7552
+ return () => {
7553
+ let value = 0;
7554
+ for (let index = 0; index < 4; index++) {
7555
+ value = value << 8 | (bytes[position] ?? 0);
7556
+ position = (position + 1) % bytes.length;
7557
+ }
7558
+ return value >>> 0;
7291
7559
  };
7292
- for (const doc of documents) {
7293
- if (doc.errors.length > 0) {
7294
- record(UNPARSEABLE);
7295
- continue;
7560
+ }
7561
+ function expandKey(state, key, salt) {
7562
+ const nextKeyWord = wordStream(key);
7563
+ for (let index = 0; index < P_WORDS; index++) {
7564
+ state.p[index] = word(state.p, index) ^ nextKeyWord();
7565
+ }
7566
+ const nextSaltWord = salt ? wordStream(salt) : () => 0;
7567
+ const block = new Uint32Array(2);
7568
+ const fill = (target, length) => {
7569
+ for (let index = 0; index < length; index += 2) {
7570
+ block[0] = word(block, 0) ^ nextSaltWord();
7571
+ block[1] = word(block, 1) ^ nextSaltWord();
7572
+ encipher(state, block, 0);
7573
+ target[index] = word(block, 0);
7574
+ target[index + 1] = word(block, 1);
7296
7575
  }
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;
7576
+ };
7577
+ fill(state.p, P_WORDS);
7578
+ fill(state.s, S_WORDS);
7579
+ }
7580
+ function encodeBase64(bytes, length) {
7581
+ let out = "";
7582
+ let offset = 0;
7583
+ while (offset < length) {
7584
+ let first = bytes[offset++] ?? 0;
7585
+ out += ALPHABET[first >> 2];
7586
+ first = (first & 3) << 4;
7587
+ if (offset >= length) {
7588
+ out += ALPHABET[first];
7589
+ break;
7302
7590
  }
7303
- const kind = doc.get("kind");
7304
- if (typeof kind !== "string" || kind.length === 0) {
7305
- record(MISSING_KIND);
7306
- continue;
7591
+ let second = bytes[offset++] ?? 0;
7592
+ out += ALPHABET[first | second >> 4];
7593
+ first = (second & 15) << 2;
7594
+ if (offset >= length) {
7595
+ out += ALPHABET[first];
7596
+ break;
7307
7597
  }
7308
- if (!ALLOWED.has(kind)) record(kind);
7309
- }
7310
- return disallowed;
7598
+ second = bytes[offset++] ?? 0;
7599
+ out += ALPHABET[first | second >> 6];
7600
+ out += ALPHABET[second & 63];
7601
+ }
7602
+ return out;
7603
+ }
7604
+ function bcryptHash(password, cost, salt) {
7605
+ if (!Number.isInteger(cost) || cost < 4 || cost > 31) {
7606
+ throw new Error(`bcrypt cost must be an integer from 4 to 31, not ${cost}`);
7607
+ }
7608
+ if (salt.length !== BCRYPT_SALT_BYTES) {
7609
+ throw new Error(`bcrypt needs a ${BCRYPT_SALT_BYTES}-byte salt`);
7610
+ }
7611
+ const passwordBytes = new TextEncoder().encode(password).slice(0, MAX_KEY_BYTES);
7612
+ const key = new Uint8Array(passwordBytes.length + 1);
7613
+ key.set(passwordBytes);
7614
+ const base = blowfishInitialState();
7615
+ const state = { p: base.p.slice(), s: base.s.slice() };
7616
+ expandKey(state, key, salt);
7617
+ for (let rounds = 2 ** cost; rounds > 0; rounds--) {
7618
+ expandKey(state, key, null);
7619
+ expandKey(state, salt, null);
7620
+ }
7621
+ const magic = new TextEncoder().encode(MAGIC);
7622
+ const nextMagicWord = wordStream(magic);
7623
+ const block = Uint32Array.from({ length: 6 }, () => nextMagicWord());
7624
+ for (let pass = 0; pass < 64; pass++) {
7625
+ for (let offset = 0; offset < 6; offset += 2) encipher(state, block, offset);
7626
+ }
7627
+ const digest = new Uint8Array(24);
7628
+ block.forEach((value, index) => {
7629
+ digest[index * 4] = value >>> 24;
7630
+ digest[index * 4 + 1] = value >>> 16 & 255;
7631
+ digest[index * 4 + 2] = value >>> 8 & 255;
7632
+ digest[index * 4 + 3] = value & 255;
7633
+ });
7634
+ const costText = String(cost).padStart(2, "0");
7635
+ return `$2y$${costText}$${encodeBase64(salt, BCRYPT_SALT_BYTES)}${encodeBase64(digest, 23)}`;
7311
7636
  }
7312
7637
 
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
- });
7638
+ // src/lib/registry-credentials.ts
7639
+ var LOCAL_REGISTRY_USERNAME = "conveyor";
7640
+ var BCRYPT_COST = 5;
7641
+ var USERNAME = /^[a-z][a-z0-9-]{0,31}$/;
7642
+ var PASSWORD = /^[A-Za-z0-9_-]{32,128}$/;
7643
+ function generateRegistryCredentials(random = randomBytes2) {
7644
+ const username = LOCAL_REGISTRY_USERNAME;
7645
+ const password = Buffer.from(random(32)).toString("base64url");
7646
+ const hash = bcryptHash(password, BCRYPT_COST, random(BCRYPT_SALT_BYTES));
7647
+ return { username, password, htpasswd: `${username}:${hash}` };
7648
+ }
7649
+ function parseRegistryCredentials(raw) {
7650
+ if (!raw) return null;
7651
+ try {
7652
+ const value = JSON.parse(raw);
7653
+ const { username, password, htpasswd } = value ?? {};
7654
+ if (typeof username !== "string" || !USERNAME.test(username)) return null;
7655
+ if (typeof password !== "string" || !PASSWORD.test(password)) return null;
7656
+ if (typeof htpasswd !== "string" || !htpasswd.startsWith(`${username}:$2`)) return null;
7657
+ if (htpasswd.includes("\n")) return null;
7658
+ return { username, password, htpasswd };
7659
+ } catch {
7660
+ return null;
7661
+ }
7319
7662
  }
7320
- function mayDisrupt(runningConveyorPods) {
7321
- return runningConveyorPods === 0;
7663
+ async function loadOrCreateRegistryCredentials(fs, random) {
7664
+ const stored = parseRegistryCredentials(await fs.readFile(LOCAL_REGISTRY_CREDENTIALS_FILE));
7665
+ if (stored) return stored;
7666
+ const created = generateRegistryCredentials(random);
7667
+ await fs.writeFile(LOCAL_REGISTRY_CREDENTIALS_FILE, `${JSON.stringify(created)}
7668
+ `, 384);
7669
+ return created;
7322
7670
  }
7323
- function busyReason(runningConveyorPods) {
7324
- return runningConveyorPods === null ? "the number of running Conveyor pods could not be read" : `${runningConveyorPods} Conveyor pod(s) are running`;
7671
+ function credentialFingerprint(credentials) {
7672
+ return createHash("sha256").update(`${credentials.username}:${credentials.password}`).digest("hex").slice(0, 16);
7673
+ }
7674
+ function basicAuthorization(credentials) {
7675
+ return `Basic ${Buffer.from(`${credentials.username}:${credentials.password}`).toString("base64")}`;
7676
+ }
7677
+ function registryDockerConfig(credentials, host) {
7678
+ const auth = Buffer.from(`${credentials.username}:${credentials.password}`).toString("base64");
7679
+ return JSON.stringify({ auths: { [host]: { auth } } });
7680
+ }
7681
+ function parseRegistryAuthState(raw) {
7682
+ if (!raw) return null;
7683
+ try {
7684
+ const value = JSON.parse(raw);
7685
+ if (typeof value?.credential !== "string") return null;
7686
+ const writtenAt = typeof value.writtenAt === "string" && !Number.isNaN(Date.parse(value.writtenAt)) ? value.writtenAt : null;
7687
+ return { credential: value.credential, writtenAt, enforced: value.enforced === true };
7688
+ } catch {
7689
+ return null;
7690
+ }
7691
+ }
7692
+ async function registryAuthEnforced(readFile3) {
7693
+ return parseRegistryAuthState(await readFile3(LOCAL_REGISTRY_AUTH_STATE_FILE))?.enforced === true;
7694
+ }
7695
+ function withRegistryCredentials(fetchImpl, readFile3) {
7696
+ let credentials = null;
7697
+ return async (url, init) => {
7698
+ credentials ??= readFile3(LOCAL_REGISTRY_CREDENTIALS_FILE).then(parseRegistryCredentials);
7699
+ const stored = await credentials;
7700
+ if (!stored) return fetchImpl(url, init);
7701
+ const headers = new Headers(init?.headers);
7702
+ headers.set("Authorization", basicAuthorization(stored));
7703
+ return fetchImpl(url, { ...init, headers });
7704
+ };
7325
7705
  }
7326
-
7327
- // src/daemon/local-registry-upkeep.ts
7328
- import { createHash } from "crypto";
7329
7706
 
7330
7707
  // src/lib/local-registry.ts
7331
7708
  var LOCAL_REGISTRY_IMAGE = "docker.io/library/registry:3.1.2@sha256:c87f33837722a100572e95d7dc4bf539fc42cf68202b13c3bc03c0ff54c3a649";
7332
7709
  var LOCAL_REGISTRY_CONFIG = "/etc/distribution/config.yml";
7333
7710
  var LOCAL_REGISTRY_GC_JOB = "conveyor-registry-gc";
7711
+ var LOCAL_REGISTRY_HTPASSWD_SECRET = "conveyor-registry-htpasswd";
7712
+ var HTPASSWD_KEY = "htpasswd";
7713
+ var AUTH_MOUNT = "/etc/distribution/auth";
7334
7714
  var LOCAL_REGISTRY_SELECTOR = { "app.kubernetes.io/name": PERSONAL_LOCAL_REGISTRY_NAME };
7335
7715
  var MANAGED_BY = { "app.kubernetes.io/managed-by": "conveyor-k3" };
7716
+ var AUTH_CHECKSUM_ANNOTATION = "conveyor.rallycry.com/registry-auth";
7336
7717
  var DATA_MOUNT = "/var/lib/registry";
7337
7718
  var REGISTRY_ENV = [
7338
7719
  { name: "REGISTRY_STORAGE_DELETE_ENABLED", value: "true" },
@@ -7347,6 +7728,23 @@ var VOLUME_MOUNTS = [
7347
7728
  { name: "data", mountPath: DATA_MOUNT },
7348
7729
  { name: "tmp", mountPath: "/tmp" }
7349
7730
  ];
7731
+ var AUTH_ENV = [
7732
+ { name: "REGISTRY_AUTH", value: "htpasswd" },
7733
+ { name: "REGISTRY_AUTH_HTPASSWD_REALM", value: "conveyor" },
7734
+ { name: "REGISTRY_AUTH_HTPASSWD_PATH", value: `${AUTH_MOUNT}/${HTPASSWD_KEY}` }
7735
+ ];
7736
+ var AUTH_VOLUME = {
7737
+ name: "auth",
7738
+ secret: {
7739
+ secretName: LOCAL_REGISTRY_HTPASSWD_SECRET,
7740
+ items: [{ key: HTPASSWD_KEY, path: HTPASSWD_KEY }],
7741
+ defaultMode: 256
7742
+ }
7743
+ };
7744
+ var AUTH_VOLUME_MOUNT = { name: "auth", mountPath: AUTH_MOUNT, readOnly: true };
7745
+ function htpasswdChecksum(htpasswd) {
7746
+ return createHash2("sha256").update(htpasswd).digest("hex").slice(0, 16);
7747
+ }
7350
7748
  var VOLUMES = [
7351
7749
  { name: "data", hostPath: { path: LOCAL_REGISTRY_DATA_DIR, type: "Directory" } },
7352
7750
  { name: "tmp", emptyDir: { sizeLimit: "64Mi" } }
@@ -7383,7 +7781,34 @@ function registryNetworkPolicy(metadata) {
7383
7781
  }
7384
7782
  };
7385
7783
  }
7386
- function renderLocalRegistryManifests() {
7784
+ function renderLocalRegistrySecrets(credentials) {
7785
+ const encode = (value) => Buffer.from(value).toString("base64");
7786
+ const secret = (namespace, name, key, value) => ({
7787
+ apiVersion: "v1",
7788
+ kind: "Secret",
7789
+ metadata: { name, namespace, labels: { ...MANAGED_BY } },
7790
+ type: "Opaque",
7791
+ data: { [key]: encode(value) }
7792
+ });
7793
+ const items = [
7794
+ secret(
7795
+ PERSONAL_LOCAL_REGISTRY_NAMESPACE,
7796
+ LOCAL_REGISTRY_HTPASSWD_SECRET,
7797
+ HTPASSWD_KEY,
7798
+ `${credentials.htpasswd}
7799
+ `
7800
+ ),
7801
+ secret(
7802
+ PERSONAL_BAKE_NAMESPACE,
7803
+ PERSONAL_LOCAL_REGISTRY_PUSH_SECRET,
7804
+ PERSONAL_LOCAL_REGISTRY_PUSH_SECRET_KEY,
7805
+ registryDockerConfig(credentials, PERSONAL_LOCAL_REGISTRY_HOST)
7806
+ )
7807
+ ];
7808
+ return `${JSON.stringify({ apiVersion: "v1", kind: "List", items }, null, 2)}
7809
+ `;
7810
+ }
7811
+ function renderLocalRegistryManifests(auth = null) {
7387
7812
  const metadata = {
7388
7813
  name: PERSONAL_LOCAL_REGISTRY_NAME,
7389
7814
  namespace: PERSONAL_LOCAL_REGISTRY_NAMESPACE,
@@ -7399,15 +7824,19 @@ function renderLocalRegistryManifests() {
7399
7824
  strategy: { type: "Recreate" },
7400
7825
  selector: { matchLabels: LOCAL_REGISTRY_SELECTOR },
7401
7826
  template: {
7402
- metadata: { labels: metadata.labels },
7827
+ metadata: {
7828
+ labels: metadata.labels,
7829
+ ...auth ? { annotations: { [AUTH_CHECKSUM_ANNOTATION]: htpasswdChecksum(auth.htpasswd) } } : {}
7830
+ },
7403
7831
  spec: {
7404
7832
  ...POD_BASE,
7833
+ ...auth ? { volumes: [...VOLUMES, AUTH_VOLUME] } : {},
7405
7834
  containers: [
7406
7835
  {
7407
7836
  name: "registry",
7408
7837
  image: LOCAL_REGISTRY_IMAGE,
7409
7838
  imagePullPolicy: "IfNotPresent",
7410
- env: REGISTRY_ENV,
7839
+ env: auth ? [...REGISTRY_ENV, ...AUTH_ENV] : REGISTRY_ENV,
7411
7840
  ports: [
7412
7841
  {
7413
7842
  name: "registry",
@@ -7419,8 +7848,10 @@ function renderLocalRegistryManifests() {
7419
7848
  protocol: "TCP"
7420
7849
  }
7421
7850
  ],
7851
+ // A TCP check: once the credential is enforced, `/v2/` answers
7852
+ // 401 to a probe that does not carry it.
7422
7853
  readinessProbe: {
7423
- httpGet: { path: "/v2/", port: PERSONAL_LOCAL_REGISTRY_PORT },
7854
+ tcpSocket: { port: PERSONAL_LOCAL_REGISTRY_PORT },
7424
7855
  periodSeconds: 10
7425
7856
  },
7426
7857
  resources: {
@@ -7428,7 +7859,7 @@ function renderLocalRegistryManifests() {
7428
7859
  limits: { memory: "512Mi" }
7429
7860
  },
7430
7861
  securityContext: CONTAINER_SECURITY,
7431
- volumeMounts: VOLUME_MOUNTS
7862
+ volumeMounts: auth ? [...VOLUME_MOUNTS, AUTH_VOLUME_MOUNT] : VOLUME_MOUNTS
7432
7863
  }
7433
7864
  ]
7434
7865
  }
@@ -7519,11 +7950,18 @@ async function registryAnswers(fetchImpl = fetch) {
7519
7950
  }
7520
7951
  async function localRegistryReady(ports) {
7521
7952
  const [answers, mirrored] = await Promise.all([
7522
- registryAnswers(ports.fetch),
7953
+ registryAnswers(withRegistryCredentials(ports.fetch ?? fetch, ports.readFile)),
7523
7954
  mirrorLoaded(ports.readFile)
7524
7955
  ]);
7525
7956
  return answers && mirrored;
7526
7957
  }
7958
+ async function localRegistryStatus(ports) {
7959
+ const [ready, authEnforced] = await Promise.all([
7960
+ localRegistryReady(ports),
7961
+ registryAuthEnforced(ports.readFile)
7962
+ ]);
7963
+ return { ready, authEnforced };
7964
+ }
7527
7965
 
7528
7966
  // src/daemon/registry-gc-lock.ts
7529
7967
  var REGISTRY_GC_MARK = "conveyor.rallycry.com/registry-gc";
@@ -7614,6 +8052,59 @@ async function deleteSweepJob(exec) {
7614
8052
  return result.exitCode === 0;
7615
8053
  }
7616
8054
 
8055
+ // src/daemon/registry-auth.ts
8056
+ var LOCAL_REGISTRY_AUTH_HOSTS = [
8057
+ PERSONAL_LOCAL_REGISTRY_HOST,
8058
+ new URL(PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT).host
8059
+ ];
8060
+ function registryAuthConfigured(file, credentials) {
8061
+ return LOCAL_REGISTRY_AUTH_HOSTS.every((host) => {
8062
+ const auth = file.configs?.[host]?.auth;
8063
+ return auth?.username === credentials.username && auth.password === credentials.password && auth.auth === void 0 && auth.identitytoken === void 0;
8064
+ });
8065
+ }
8066
+ function withLocalRegistryAuth(file, credentials) {
8067
+ return LOCAL_REGISTRY_AUTH_HOSTS.reduce(
8068
+ (next, host) => upsertRegistryAuth(next, host, {
8069
+ username: credentials.username,
8070
+ password: credentials.password
8071
+ }),
8072
+ file
8073
+ );
8074
+ }
8075
+ function monotonicNowUs() {
8076
+ return Number(process.hrtime.bigint() / 1000n);
8077
+ }
8078
+ async function k3sStartedAtMs(exec, nowMs, monotonicNow = monotonicNowUs) {
8079
+ const result = await exec(
8080
+ "systemctl",
8081
+ ["show", "k3s", "--property=ActiveEnterTimestampMonotonic", "--value"],
8082
+ { timeoutMs: 1e4 }
8083
+ ).catch(() => null);
8084
+ if (!result || result.exitCode !== 0) return null;
8085
+ const startedUs = Number.parseInt(result.stdout.trim(), 10);
8086
+ if (!Number.isFinite(startedUs) || startedUs <= 0) return null;
8087
+ return nowMs - (monotonicNow() - startedUs) / 1e3;
8088
+ }
8089
+ async function settleRegistryAuth(ctx, credentials, written) {
8090
+ const now = (ctx.now ?? (() => /* @__PURE__ */ new Date()))();
8091
+ const raw = await ctx.fs.readFile(LOCAL_REGISTRY_AUTH_STATE_FILE);
8092
+ const credential = credentialFingerprint(credentials);
8093
+ const stored = parseRegistryAuthState(raw);
8094
+ const state = stored?.credential === credential ? stored : { credential, writtenAt: null, enforced: false };
8095
+ if (!state.enforced && (written || state.writtenAt === null)) {
8096
+ state.writtenAt = now.toISOString();
8097
+ }
8098
+ if (!state.enforced && state.writtenAt !== null) {
8099
+ const startedAt = await k3sStartedAtMs(ctx.exec, now.getTime(), ctx.monotonicNowUs);
8100
+ if (startedAt !== null && startedAt > Date.parse(state.writtenAt)) state.enforced = true;
8101
+ }
8102
+ const next = `${JSON.stringify(state)}
8103
+ `;
8104
+ if (next !== raw) await ctx.fs.writeFile(LOCAL_REGISTRY_AUTH_STATE_FILE, next, 384);
8105
+ return state.enforced;
8106
+ }
8107
+
7617
8108
  // src/daemon/local-registry-upkeep.ts
7618
8109
  function nowDate() {
7619
8110
  return /* @__PURE__ */ new Date();
@@ -7622,33 +8113,47 @@ function mirrorConfigured(raw) {
7622
8113
  const endpoints = mirrorEndpoints(parseRegistries(raw), PERSONAL_LOCAL_REGISTRY_HOST);
7623
8114
  return endpoints.length === 1 && endpoints[0] === PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT;
7624
8115
  }
7625
- async function applyUnlessCollecting(ctx) {
8116
+ async function applyUnlessCollecting(ctx, auth) {
7626
8117
  const deployment = await readRegistryDeployment(ctx.exec);
7627
8118
  if (!deployment) throw new Error("the registry's Deployment could not be read");
7628
8119
  if (gcMarkIsLive(deployment.mark, (ctx.now ?? nowDate)().getTime(), ctx.isAlive)) return false;
7629
8120
  if (deployment.mark !== null && !await deleteSweepJob(ctx.exec)) {
7630
8121
  throw new Error("a stopped cleanup's Job could not be deleted, so the registry stays stopped");
7631
8122
  }
7632
- await kubectlApply(ctx.exec, renderLocalRegistryManifests());
8123
+ await kubectlApply(ctx.exec, renderLocalRegistryManifests(auth));
7633
8124
  if (deployment.mark !== null) await clearGcMark(ctx.exec);
7634
8125
  return true;
7635
8126
  }
7636
8127
  async function deployLocalRegistry(ctx) {
7637
8128
  await ctx.fs.ensureDir(LOCAL_REGISTRY_DATA_DIR, 448);
7638
- const applied = await applyUnlessCollecting(ctx);
8129
+ const credentials = await loadOrCreateRegistryCredentials(ctx.fs, ctx.random);
7639
8130
  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 };
8131
+ let file = parseRegistries(raw);
8132
+ const mirrorWritten = !mirrorConfigured(raw);
8133
+ const authWritten = !registryAuthConfigured(file, credentials);
8134
+ let backupPath = null;
8135
+ if (mirrorWritten) {
8136
+ file = upsertRegistryMirror(file, PERSONAL_LOCAL_REGISTRY_HOST, [
8137
+ PERSONAL_LOCAL_REGISTRY_NODE_ENDPOINT
8138
+ ]);
8139
+ }
8140
+ if (authWritten) file = withLocalRegistryAuth(file, credentials);
8141
+ if (mirrorWritten || authWritten) {
8142
+ backupPath = await writeRegistriesWithBackup(ctx.fs, raw, file, (ctx.now ?? nowDate)());
8143
+ }
8144
+ const authEnforced = await settleRegistryAuth(ctx, credentials, authWritten);
8145
+ await kubectlApply(ctx.exec, renderLocalRegistrySecrets(credentials));
8146
+ const applied = await applyUnlessCollecting(ctx, authEnforced ? credentials : null);
8147
+ return { applied, mirrorWritten, authWritten, backupPath, authEnforced };
7646
8148
  }
7647
8149
  async function readMirrorState(fs) {
7648
8150
  const raw = await fs.readFile(REGISTRIES_FILE);
7649
- const fingerprint = createHash("sha256").update(raw ?? "").digest("hex");
8151
+ const authPending = !await registryAuthEnforced((path) => fs.readFile(path));
8152
+ const fingerprint = createHash3("sha256").update(raw ?? "").digest("hex");
7650
8153
  if (!mirrorConfigured(raw)) return { state: "absent", fingerprint };
7651
- if (await mirrorLoaded((path) => fs.readFile(path))) return { state: "loaded", fingerprint };
8154
+ if (!authPending && await mirrorLoaded((path) => fs.readFile(path))) {
8155
+ return { state: "loaded", fingerprint };
8156
+ }
7652
8157
  const tried = (await fs.readFile(LOCAL_REGISTRY_MIRROR_RESTART_FILE))?.trim() === fingerprint;
7653
8158
  return { state: tried ? "tried" : "owed", fingerprint };
7654
8159
  }
@@ -7660,14 +8165,14 @@ async function loadRegistryMirror(ctx) {
7660
8165
  if (state === "tried") {
7661
8166
  return {
7662
8167
  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}.`
8168
+ 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
8169
  };
7665
8170
  }
7666
8171
  if (state !== "owed") return { restarted: false, warning: null };
7667
8172
  if (!mayDisrupt(ctx.runningConveyorPods)) {
7668
8173
  return {
7669
8174
  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.`
8175
+ 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
8176
  };
7672
8177
  }
7673
8178
  const restart = await ctx.exec("systemctl", ["restart", "k3s"], { timeoutMs: 12e4 });
@@ -7849,7 +8354,18 @@ async function stdoutOf(exec, bin, args) {
7849
8354
  const run = await exec(bin, args, { timeoutMs: 1e4 }).catch(() => null);
7850
8355
  return run && run.exitCode === 0 ? run.stdout : null;
7851
8356
  }
8357
+ async function dropInDirExists(exec) {
8358
+ const run = await exec("test", ["-d", K3S_CONFIG_DROPIN_DIR], { timeoutMs: 1e4 }).catch(
8359
+ () => null
8360
+ );
8361
+ if (run === null) return null;
8362
+ if (run.exitCode === 0) return true;
8363
+ return run.exitCode === 1 && run.stderr.trim() === "" ? false : null;
8364
+ }
7852
8365
  async function readK3sConfigs(ctx) {
8366
+ const dirExists = await dropInDirExists(ctx.exec);
8367
+ if (dirExists === null) return null;
8368
+ if (!dirExists) return [await ctx.fs.readFile(K3S_OWNER_CONFIG)];
7853
8369
  const listed = await stdoutOf(ctx.exec, "find", [
7854
8370
  "-L",
7855
8371
  K3S_CONFIG_DROPIN_DIR,
@@ -8106,6 +8622,55 @@ async function reconcileIsolation(ctx, desired, result) {
8106
8622
  result.isolation = { applied };
8107
8623
  }
8108
8624
 
8625
+ // src/lib/fs-port.ts
8626
+ import { chmod, mkdir, readFile, rm, writeFile } from "fs/promises";
8627
+ var nodeFileSystem = {
8628
+ async readFile(path) {
8629
+ try {
8630
+ return await readFile(path, "utf8");
8631
+ } catch {
8632
+ return null;
8633
+ }
8634
+ },
8635
+ async writeFile(path, content, mode) {
8636
+ await mkdir(path.slice(0, path.lastIndexOf("/")), { recursive: true });
8637
+ await writeFile(path, content, { encoding: "utf8", ...mode === void 0 ? {} : { mode } });
8638
+ if (mode !== void 0) await chmod(path, mode);
8639
+ },
8640
+ async removeFile(path) {
8641
+ await rm(path, { force: true });
8642
+ },
8643
+ async ensureDir(path, mode) {
8644
+ await mkdir(path, { recursive: true });
8645
+ if (mode !== void 0) await chmod(path, mode);
8646
+ }
8647
+ };
8648
+ function createMemoryFileSystem(seed = {}) {
8649
+ const files = new Map(Object.entries(seed));
8650
+ const modes = /* @__PURE__ */ new Map();
8651
+ const removed = [];
8652
+ return {
8653
+ files,
8654
+ modes,
8655
+ removed,
8656
+ readFile: (path) => Promise.resolve(files.get(path) ?? null),
8657
+ writeFile: (path, content, mode) => {
8658
+ files.set(path, content);
8659
+ if (mode !== void 0) modes.set(path, mode);
8660
+ return Promise.resolve();
8661
+ },
8662
+ removeFile: (path) => {
8663
+ files.delete(path);
8664
+ removed.push(path);
8665
+ return Promise.resolve();
8666
+ },
8667
+ ensureDir: (path, mode) => {
8668
+ if (mode !== void 0) modes.set(path, mode);
8669
+ return Promise.resolve();
8670
+ }
8671
+ };
8672
+ }
8673
+
8109
8674
  // src/daemon/registry-tags.ts
8110
8675
  var REPO_NAME = /^[a-z0-9]+(?:[._-][a-z0-9]+)*(?:\/[a-z0-9]+(?:[._-][a-z0-9]+)*)*$/;
8111
8676
  var TAG_NAME = /^[A-Za-z0-9_][A-Za-z0-9._-]{0,127}$/;
@@ -8391,7 +8956,10 @@ async function collectRegistryGarbage(ports, options) {
8391
8956
  blobsCollected: false
8392
8957
  });
8393
8958
  const now = ports.now ?? Date.now;
8394
- const fetchImpl = ports.fetch ?? fetch;
8959
+ const fetchImpl = withRegistryCredentials(
8960
+ ports.fetch ?? fetch,
8961
+ ports.readFile ?? ((path) => nodeFileSystem.readFile(path))
8962
+ );
8395
8963
  const retain = await readRetainList(ports);
8396
8964
  if (typeof retain === "string") return skip(retain);
8397
8965
  const blocked = await registryBlocker(ports, now());
@@ -8557,17 +9125,26 @@ var GRANTED_COMMAND_NAMES = LOCAL_BAKE_ROOT_COMMANDS.map((command) => command.na
8557
9125
 
8558
9126
  // src/daemon/telemetry.ts
8559
9127
  var LOCAL_REGISTRY_WARN_AFTER_MS = 10 * 6e4;
9128
+ 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
9129
  async function reportLocalRegistry(ports, watch, now = Date.now()) {
8561
9130
  if (!watch) return { fields: {}, warnings: [] };
8562
- const ready = await localRegistryReady({ readFile: ports.readFile, fetch: ports.fetch });
9131
+ const { ready, authEnforced } = await localRegistryStatus({
9132
+ readFile: ports.readFile,
9133
+ fetch: ports.fetch
9134
+ });
8563
9135
  const paused = (watch.pausedUntil ?? 0) > now;
8564
9136
  watch.notReadySince = ready || paused ? null : watch.notReadySince ?? now;
8565
9137
  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 ? [
9138
+ const warnings = [];
9139
+ if (stuckMs >= LOCAL_REGISTRY_WARN_AFTER_MS) {
9140
+ warnings.push(
8569
9141
  `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
- ] : []
9142
+ );
9143
+ }
9144
+ if (!authEnforced) warnings.push(LOCAL_REGISTRY_OPEN_WARNING);
9145
+ return {
9146
+ fields: { localRegistry: { host: PERSONAL_LOCAL_REGISTRY_HOST, ready, authEnforced } },
9147
+ warnings
8571
9148
  };
8572
9149
  }
8573
9150
  var VM_CAPACITY_FILE = "/etc/conveyor-vm/capacity.json";
@@ -8690,6 +9267,7 @@ async function collectHeartbeat(ports, state, inventory) {
8690
9267
  ...localImages ? { localImages } : {},
8691
9268
  ...registry.fields,
8692
9269
  ...state.isolation ? { isolation: state.isolation } : {},
9270
+ ...state.cpuQuotaApplied === void 0 ? {} : { cpuQuotaApplied: state.cpuQuotaApplied },
8693
9271
  localWarnings: withinHeartbeatBounds([
8694
9272
  ...state.localWarnings,
8695
9273
  ...gcWarning ? [gcWarning] : [],
@@ -8740,7 +9318,15 @@ async function collect(ports, now) {
8740
9318
  if (watch) watch.pausedUntil = now.getTime() + REGISTRY_GC_MARK_TTL_MS;
8741
9319
  try {
8742
9320
  const outcome = await collectRegistryGarbage(
8743
- { exec, log, requestDesiredState, fetch: fetch2, sleep: sleep2, isAlive },
9321
+ {
9322
+ exec,
9323
+ log,
9324
+ requestDesiredState,
9325
+ fetch: fetch2,
9326
+ readFile: (path) => ports.fs.readFile(path),
9327
+ sleep: sleep2,
9328
+ isAlive
9329
+ },
8744
9330
  { dryRun: false }
8745
9331
  );
8746
9332
  if (outcome.skipped) return outcome.skipped;
@@ -8786,48 +9372,7 @@ async function runRegistryUpkeep(ports, lastGcAt, maintenanceHour) {
8786
9372
  }
8787
9373
 
8788
9374
  // 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");
9375
+ async function livePriorityClassValue(ctx, name) {
8831
9376
  const read = await ctx.exec("k3s", [
8832
9377
  "kubectl",
8833
9378
  "get",
@@ -8836,30 +9381,59 @@ async function reconcilePriorityClass(ctx, desired, result) {
8836
9381
  "-o",
8837
9382
  "jsonpath={.value}"
8838
9383
  ]);
8839
- if (read.exitCode !== 0) return;
8840
- const existingValue = Number.parseInt(read.stdout.trim(), 10);
8841
- if (existingValue === desired.priorityClass.value) return;
9384
+ const value = Number.parseInt(read.stdout.trim(), 10);
9385
+ return read.exitCode === 0 && Number.isFinite(value) ? value : null;
9386
+ }
9387
+ async function reconcilePriorityClass(ctx, desired, result) {
9388
+ const name = assertK8sName(desired.priorityClass.name, "priorityClass.name");
9389
+ const wanted = desired.priorityClass.value;
9390
+ const existingValue = await livePriorityClassValue(ctx, name);
9391
+ if (existingValue === null || existingValue === wanted) return "ready";
9392
+ if (!declaresPriorityClass(desired.rbacManifest, name, wanted)) {
9393
+ result.warnings.push(
9394
+ `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.`
9395
+ );
9396
+ return "kept";
9397
+ }
8842
9398
  if (!mayDisrupt(ctx.runningConveyorPods)) {
8843
9399
  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)}.`
9400
+ `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
9401
  );
8846
- return;
9402
+ return "kept";
8847
9403
  }
8848
- const removed = await ctx.exec("k3s", ["kubectl", "delete", "priorityclass", name]);
9404
+ const removed = await ctx.exec("k3s", [
9405
+ "kubectl",
9406
+ "delete",
9407
+ "priorityclass",
9408
+ name,
9409
+ "--ignore-not-found"
9410
+ ]);
8849
9411
  if (removed.exitCode !== 0) {
8850
9412
  result.warnings.push(
8851
- `Could not replace the \`${name}\` PriorityClass (value ${existingValue}, needs ${desired.priorityClass.value}): ${removed.stderr.trim().slice(0, 160)}`
9413
+ `Could not replace the \`${name}\` PriorityClass (value ${existingValue}, needs ${wanted}): ${removed.stderr.trim().slice(0, 160)}`
8852
9414
  );
8853
- return;
9415
+ return "kept";
8854
9416
  }
9417
+ return "removed";
9418
+ }
9419
+ async function applyRbac(ctx, desired, result) {
9420
+ const outcome = await reconcilePriorityClass(ctx, desired, result);
9421
+ const manifest = outcome === "kept" ? withoutKind(desired.rbacManifest, "PriorityClass") : desired.rbacManifest;
8855
9422
  try {
8856
- await kubectlApply(ctx.exec, desired.rbacManifest);
8857
- result.recreatedPriorityClass = true;
9423
+ await kubectlApply(ctx.exec, manifest);
8858
9424
  } catch (error) {
9425
+ if (outcome !== "removed") throw error;
9426
+ const value = await livePriorityClassValue(ctx, desired.priorityClass.name).catch(() => null);
9427
+ if (value === desired.priorityClass.value) {
9428
+ result.recreatedPriorityClass = true;
9429
+ throw error;
9430
+ }
8859
9431
  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\`.`
9432
+ `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
9433
  );
9434
+ return;
8862
9435
  }
9436
+ if (outcome === "removed") result.recreatedPriorityClass = true;
8863
9437
  }
8864
9438
  async function step(result, label, run) {
8865
9439
  try {
@@ -8885,12 +9459,7 @@ async function reconcile(ctx, desired) {
8885
9459
  `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
9460
  );
8887
9461
  } 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
- );
9462
+ await step(result, "Could not apply RBAC", () => applyRbac(ctx, desired, result));
8894
9463
  }
8895
9464
  await step(
8896
9465
  result,
@@ -8922,11 +9491,23 @@ async function reconcile(ctx, desired) {
8922
9491
  "Could not apply kubelet settings",
8923
9492
  () => reconcileKubelet(ctx, desired, result, managed)
8924
9493
  );
9494
+ if (desired.isolation !== void 0) {
9495
+ await step(result, "Could not cap pod CPU", async () => {
9496
+ result.cpuQuotaApplied = await reconcilePodCpuQuota(ctx.exec, desired, result.warnings);
9497
+ });
9498
+ if (desired.kubelet.systemReserved?.cpu && result.cpuQuotaApplied === void 0) {
9499
+ result.cpuQuotaApplied = false;
9500
+ }
9501
+ }
8925
9502
  if (registry && !result.restartedK3s) {
8926
9503
  await step(result, "Could not load the local image registry mirror", async () => {
8927
9504
  if (!managed) {
8928
9505
  if (await registryMirrorPending(ctx.fs))
8929
- result.warnings.push(ownerRestartWarning("The local image registry's mirror is new."));
9506
+ result.warnings.push(
9507
+ ownerRestartWarning(
9508
+ "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."
9509
+ )
9510
+ );
8930
9511
  return;
8931
9512
  }
8932
9513
  const loaded = await loadRegistryMirror(ctx);
@@ -8989,6 +9570,8 @@ var ClusterDaemon = class {
8989
9570
  * against k3s's own settings before reporting it.
8990
9571
  */
8991
9572
  isolation = void 0;
9573
+ /** Whether the pod slice's CPU cap is in force, from the last reconcile. */
9574
+ cpuQuotaApplied = void 0;
8992
9575
  timers = [];
8993
9576
  /** When the last store walk started, so one that failed is not retried on every reconcile. */
8994
9577
  storeWalkStartedAt = 0;
@@ -9071,6 +9654,7 @@ var ClusterDaemon = class {
9071
9654
  );
9072
9655
  this.warnings = result.warnings;
9073
9656
  this.isolation = result.isolation;
9657
+ this.cpuQuotaApplied = result.cpuQuotaApplied;
9074
9658
  this.scheduleIsolationRetry();
9075
9659
  for (const warning of result.warnings) this.ports.log(warning);
9076
9660
  if (result.prunedNamespaces.length > 0) {
@@ -9227,6 +9811,11 @@ var ClusterDaemon = class {
9227
9811
  );
9228
9812
  }
9229
9813
  for (const failure of result.failures) this.ports.log(`Image prune failed \u2014 ${failure}`);
9814
+ if (result.pending.length > 0) {
9815
+ this.ports.log(
9816
+ `Image removal still in progress for ${result.pending.length} image(s); the next prune re-checks them.`
9817
+ );
9818
+ }
9230
9819
  } catch (error) {
9231
9820
  this.ports.log(
9232
9821
  `Skipping image prune: ${error instanceof Error ? error.message : String(error)}`
@@ -9265,6 +9854,7 @@ var ClusterDaemon = class {
9265
9854
  localImageHashes: this.localImageHashes,
9266
9855
  ...registryOn ? { localRegistry: this.registryWatch } : {},
9267
9856
  ...isolation.isolation ? { isolation: isolation.isolation } : {},
9857
+ cpuQuotaApplied: this.cpuQuotaApplied,
9268
9858
  k3sStoreBytes: this.measuredStoreBytes
9269
9859
  },
9270
9860
  inventory
@@ -9399,55 +9989,6 @@ function renderAllUnits(options) {
9399
9989
  };
9400
9990
  }
9401
9991
 
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
9992
  // src/verbs.ts
9452
9993
  var VERBS = [
9453
9994
  "vm",
@@ -9613,17 +10154,36 @@ async function removeLocalRegistry(ports) {
9613
10154
  `Could not remove the local image registry from the cluster: ${removed.stderr.trim().slice(0, 200)}`
9614
10155
  );
9615
10156
  }
10157
+ for (const [namespace, name] of [
10158
+ [PERSONAL_LOCAL_REGISTRY_NAMESPACE, LOCAL_REGISTRY_HTPASSWD_SECRET],
10159
+ [PERSONAL_BAKE_NAMESPACE, PERSONAL_LOCAL_REGISTRY_PUSH_SECRET]
10160
+ ]) {
10161
+ const secret = await ports.exec(
10162
+ "k3s",
10163
+ ["kubectl", "-n", namespace, "delete", "secret", name, "--ignore-not-found"],
10164
+ { timeoutMs: 6e4 }
10165
+ );
10166
+ if (secret.exitCode !== 0) {
10167
+ ports.log(
10168
+ `Could not remove the local image registry's Secret ${namespace}/${name}: ${secret.stderr.trim().slice(0, 200)}`
10169
+ );
10170
+ }
10171
+ }
9616
10172
  const raw = await ports.fs.readFile(REGISTRIES_FILE);
9617
10173
  const parsed = parseRegistries(raw);
9618
- if (mirrorEndpoints(parsed, PERSONAL_LOCAL_REGISTRY_HOST).length === 0) return;
10174
+ const next = LOCAL_REGISTRY_AUTH_HOSTS.reduce(
10175
+ removeRegistryAuth,
10176
+ removeRegistryMirror(parsed, PERSONAL_LOCAL_REGISTRY_HOST)
10177
+ );
10178
+ if (serializeRegistries(next) === serializeRegistries(parsed)) return;
9619
10179
  const backup = await writeRegistriesWithBackup(
9620
10180
  ports.fs,
9621
10181
  raw,
9622
- removeRegistryMirror(parsed, PERSONAL_LOCAL_REGISTRY_HOST),
10182
+ next,
9623
10183
  (ports.now ?? (() => /* @__PURE__ */ new Date()))()
9624
10184
  );
9625
10185
  ports.log(
9626
- `Removed the local image registry's mirror from ${REGISTRIES_FILE} (backup: ${backup}).`
10186
+ `Removed the local image registry's mirror and credential from ${REGISTRIES_FILE} (backup: ${backup}).`
9627
10187
  );
9628
10188
  }
9629
10189
  async function removeIsolation(ports) {
@@ -9680,6 +10240,10 @@ async function uninstall(ports, options = {}) {
9680
10240
  }
9681
10241
  await removeLocalRegistry(ports);
9682
10242
  await removeIsolation(ports);
10243
+ const lifted = await liftPodCpuQuota(ports.exec).catch(
10244
+ (error) => error instanceof Error ? error.message : String(error)
10245
+ );
10246
+ if (lifted) ports.log(`Could not lift Conveyor's CPU cap on kubepods.slice: ${lifted}`);
9683
10247
  for (const path of OWNED_FILES) {
9684
10248
  await ports.fs.removeFile(path);
9685
10249
  result.removedFiles.push(path);
@@ -9732,6 +10296,45 @@ async function uninstall(ports, options = {}) {
9732
10296
  return result;
9733
10297
  }
9734
10298
 
10299
+ // src/lib/bins.ts
10300
+ var INSTALLER_PATHS = {
10301
+ bun: "/root/.bun/bin/bun",
10302
+ cli: "/root/.bun/bin/conveyor-k3",
10303
+ cloudflared: "/usr/local/bin/cloudflared"
10304
+ };
10305
+ var CLI_PATH_LINK = "/usr/local/bin/conveyor-k3";
10306
+ async function linkCliOnPath(exec, target = INSTALLER_PATHS.cli) {
10307
+ if (target === CLI_PATH_LINK) return { linked: false };
10308
+ const present = await exec("test", ["-x", target]).catch(() => null);
10309
+ if (present?.exitCode !== 0) return { linked: false };
10310
+ const link = await exec("ln", ["-sfn", target, CLI_PATH_LINK]).catch(() => null);
10311
+ if (link?.exitCode === 0) return { linked: true };
10312
+ return {
10313
+ linked: false,
10314
+ problem: `Could not link ${CLI_PATH_LINK} to ${target}: ${link?.stderr.trim().slice(0, 200) || "ln failed"}`
10315
+ };
10316
+ }
10317
+
10318
+ // src/lib/cloudflared.ts
10319
+ var PACKAGE_MANAGER_REFUSAL = "installed by a package manager";
10320
+ async function resolveRealPath(exec, bin) {
10321
+ const result = await exec("readlink", ["-f", bin]).catch(() => null);
10322
+ const resolved = result?.exitCode === 0 ? result.stdout.trim() : "";
10323
+ return resolved.startsWith("/") ? resolved : bin;
10324
+ }
10325
+ async function cloudflaredUpdater(exec, bin) {
10326
+ const real = await resolveRealPath(exec, bin);
10327
+ const owner = await exec("dpkg", ["-S", real]).catch(() => null);
10328
+ return owner?.exitCode === 0 ? "package-manager" : "conveyor";
10329
+ }
10330
+ function isPackageManagerRefusal(output) {
10331
+ return `${output.stdout}
10332
+ ${output.stderr}`.toLowerCase().includes(PACKAGE_MANAGER_REFUSAL);
10333
+ }
10334
+ function describeCloudflaredUpdater(updater) {
10335
+ return updater === "package-manager" ? "cloudflared is updated by your package manager, not by Conveyor" : "cloudflared is updated by Conveyor's daily update";
10336
+ }
10337
+
9735
10338
  // src/commands/update.ts
9736
10339
  var CLOUDFLARED_UPDATED_EXIT = 11;
9737
10340
  function readExecStart(unit) {
@@ -9819,7 +10422,22 @@ async function updatePackage(ports, result, targetVersion) {
9819
10422
  await ports.exec("systemctl", ["restart", DAEMON_UNIT]);
9820
10423
  }
9821
10424
  async function updateCloudflared(ports, result) {
10425
+ const packaged = () => {
10426
+ result.cloudflaredUpdater = "package-manager";
10427
+ result.notes.push(
10428
+ `cloudflared update skipped: ${describeCloudflaredUpdater("package-manager")}`
10429
+ );
10430
+ };
10431
+ if (await cloudflaredUpdater(ports.exec, ports.bins.cloudflared) === "package-manager") {
10432
+ packaged();
10433
+ return;
10434
+ }
9822
10435
  const update = await ports.exec(ports.bins.cloudflared, ["update"], { timeoutMs: 5 * 6e4 });
10436
+ if (isPackageManagerRefusal(update)) {
10437
+ packaged();
10438
+ return;
10439
+ }
10440
+ result.cloudflaredUpdater = "conveyor";
9823
10441
  if (update.exitCode !== CLOUDFLARED_UPDATED_EXIT) return;
9824
10442
  result.cloudflaredUpdated = true;
9825
10443
  ports.log("cloudflared was updated; restarting the tunnel.");
@@ -9829,10 +10447,13 @@ async function runUpdate(ports, decision, channel, targetVersion) {
9829
10447
  const result = {
9830
10448
  packageUpdated: false,
9831
10449
  cloudflaredUpdated: false,
10450
+ cloudflaredUpdater: null,
9832
10451
  k3sUpgraded: false,
9833
10452
  notes: []
9834
10453
  };
9835
10454
  await updatePackage(ports, result, targetVersion);
10455
+ const linked = await linkCliOnPath(ports.exec);
10456
+ if (linked.problem) result.notes.push(linked.problem);
9836
10457
  await updateCloudflared(ports, result);
9837
10458
  if (!decision.upgrade) {
9838
10459
  result.notes.push(`k3s upgrade skipped: ${decision.reason}`);
@@ -9865,6 +10486,9 @@ async function runUpdate(ports, decision, channel, targetVersion) {
9865
10486
  return result;
9866
10487
  }
9867
10488
 
10489
+ // src/lib/env.ts
10490
+ import { readFileSync as readFileSync3 } from "fs";
10491
+
9868
10492
  // src/commands/setup.ts
9869
10493
  var K3S_MIN_MAJOR_MINOR = [1, 29];
9870
10494
  function parseK3sVersion(output) {
@@ -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,