@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/cli.js +865 -220
- package/dist/index.d.ts +31 -25
- package/dist/index.js +816 -193
- package/package.json +1 -1
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/
|
|
7269
|
-
|
|
7270
|
-
|
|
7271
|
-
"
|
|
7272
|
-
|
|
7273
|
-
|
|
7274
|
-
|
|
7275
|
-
|
|
7276
|
-
"
|
|
7277
|
-
|
|
7278
|
-
|
|
7279
|
-
|
|
7280
|
-
|
|
7281
|
-
|
|
7282
|
-
|
|
7283
|
-
|
|
7284
|
-
|
|
7285
|
-
|
|
7286
|
-
|
|
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
|
|
7289
|
-
const
|
|
7290
|
-
|
|
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
|
-
|
|
7293
|
-
|
|
7294
|
-
|
|
7295
|
-
|
|
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
|
-
|
|
7298
|
-
|
|
7299
|
-
|
|
7300
|
-
|
|
7301
|
-
|
|
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
|
-
|
|
7304
|
-
|
|
7305
|
-
|
|
7306
|
-
|
|
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
|
-
|
|
7309
|
-
|
|
7310
|
-
|
|
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/
|
|
7314
|
-
|
|
7315
|
-
|
|
7316
|
-
|
|
7317
|
-
|
|
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
|
|
7321
|
-
|
|
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
|
|
7324
|
-
return
|
|
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
|
|
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: {
|
|
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
|
-
|
|
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
|
|
8129
|
+
const credentials = await loadOrCreateRegistryCredentials(ctx.fs, ctx.random);
|
|
7639
8130
|
const raw = await ctx.fs.readFile(REGISTRIES_FILE);
|
|
7640
|
-
|
|
7641
|
-
const
|
|
7642
|
-
|
|
7643
|
-
|
|
7644
|
-
|
|
7645
|
-
|
|
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
|
|
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)))
|
|
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 =
|
|
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
|
|
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
|
-
|
|
8567
|
-
|
|
8568
|
-
warnings
|
|
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
|
-
{
|
|
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
|
|
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
|
-
|
|
8840
|
-
|
|
8841
|
-
|
|
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 ${
|
|
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", [
|
|
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 ${
|
|
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,
|
|
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", () =>
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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,
|