@kici-dev/orchestrator 0.1.3 → 0.1.6
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/agent/dispatcher.d.ts +26 -0
- package/dist/agent/ownership-tracker.d.ts +45 -3
- package/dist/app.d.ts +9 -3
- package/dist/audit/access-log.d.ts +7 -0
- package/dist/cli/api-client.d.ts +14 -0
- package/dist/cli/commands/cluster-name.d.ts +18 -0
- package/dist/cli/commands/db.d.ts +9 -6
- package/dist/cli/commands/org-settings.d.ts +2 -1
- package/dist/cli/commands/shared/secret-input.d.ts +46 -0
- package/dist/cli/commands/variable.d.ts +18 -0
- package/dist/cli/service/index.d.ts +1 -0
- package/dist/cli/service/launchd.d.ts +19 -0
- package/dist/cli/service/privilege.d.ts +21 -0
- package/dist/cli/wizard/orchestrator-wizard.d.ts +4 -2
- package/dist/cli.js +1074 -225
- package/dist/cluster/coordinator.d.ts +3 -3
- package/dist/cluster/join-token.d.ts +21 -13
- package/dist/cluster/peer-client.d.ts +9 -0
- package/dist/config/cluster-id.d.ts +23 -0
- package/dist/config/cluster-name.d.ts +39 -0
- package/dist/config.d.ts +8 -0
- package/dist/dashboard/handler.d.ts +54 -2
- package/dist/db/migrations/019_generic_sources_change_notify.d.ts +25 -0
- package/dist/db/migrations/020_org_settings_dashboard_write_policy.d.ts +17 -0
- package/dist/db/migrations/021_check_run_tracking.d.ts +26 -0
- package/dist/db/migrations/022_scaler_manager_state.d.ts +29 -0
- package/dist/db/migrations/023_dispatch_queue_recovery_deadline.d.ts +26 -0
- package/dist/db/types.d.ts +137 -0
- package/dist/events/event-store.d.ts +9 -2
- package/dist/events/trust-store.d.ts +7 -0
- package/dist/events/types.d.ts +33 -0
- package/dist/metrics/scheduled-jobs.d.ts +26 -5
- package/dist/orchestrator-core.d.ts +16 -2
- package/dist/pipeline/processor.d.ts +2 -2
- package/dist/pipeline/rerun.d.ts +2 -2
- package/dist/pipeline/test-pipeline.d.ts +2 -2
- package/dist/policy/dashboard-write-policy.d.ts +115 -0
- package/dist/queue/job-queue.d.ts +54 -1
- package/dist/reporting/{commit-status.d.ts → check-run-reporter.d.ts} +94 -16
- package/dist/reporting/check-run-tracking-store.d.ts +133 -0
- package/dist/routes/admin-access-log.d.ts +1 -0
- package/dist/routes/admin-backends.d.ts +9 -1
- package/dist/routes/admin-cluster-name.d.ts +50 -0
- package/dist/routes/admin-db.d.ts +9 -1
- package/dist/routes/admin-environments.d.ts +10 -6
- package/dist/routes/admin-event-dlq.d.ts +1 -0
- package/dist/routes/admin-event-log.d.ts +1 -0
- package/dist/routes/admin-events.d.ts +19 -0
- package/dist/routes/admin-maintenance.d.ts +9 -1
- package/dist/routes/admin-org-settings.d.ts +10 -1
- package/dist/routes/admin-queue-execution.d.ts +1 -0
- package/dist/routes/admin-registrations.d.ts +1 -0
- package/dist/routes/admin-runs.d.ts +1 -0
- package/dist/routes/admin-scheduled-jobs.d.ts +1 -0
- package/dist/routes/admin-sources.d.ts +24 -1
- package/dist/routes/admin.d.ts +22 -0
- package/dist/scaler/manager.d.ts +38 -0
- package/dist/scaler/scaler-state-store.d.ts +102 -0
- package/dist/secrets/routing-key-scope.d.ts +43 -0
- package/dist/server.js +10098 -6961
- package/dist/sources/build-platform-sources.d.ts +26 -0
- package/dist/sources/source-manager.d.ts +14 -0
- package/dist/stale-detector/stale-run-detector.d.ts +17 -5
- package/dist/standalone.js +7525 -5172
- package/dist/webhook/generic-sources-listener.d.ts +89 -0
- package/dist/webhook/register-source-bundle.d.ts +57 -0
- package/dist/ws/dashboard-backends-handler.d.ts +14 -0
- package/dist/ws/dashboard-env-handler.d.ts +7 -0
- package/dist/ws/dashboard-global-workflows-handler.d.ts +6 -0
- package/dist/ws/dashboard-registrations-handler.d.ts +7 -0
- package/dist/ws/platform-client.d.ts +57 -2
- package/package.json +3 -3
- package/sbom.spdx.json +40 -35
package/dist/cli.js
CHANGED
|
@@ -5,13 +5,16 @@ __cjs_dirname(__cjs_fileURLToPath(import.meta.url));
|
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
import { BaseColdStore, ChunkLru, chunkObjectKey, clearDispatchQueueDirect, computeChunkId, computeMigrationsHash, createDb, createDbRole, createEnvironmentTemplateDirect, createLogger, createPool, createReadOnlyDbUser, decodeChunk, dropAndCreateDatabase, emitKiciEventDirect, encodeKeySegment, ensureDatabase, formatBytes, formatUptime, isSchemaCurrent, listEnvironmentsDirect, listExecutionRunsDirect, listQueueDirect, listRegistrationsDirect, maskDatabaseUrl, parseDatabaseUrl, parseManifest, prunePeerCredentialsDirect, purgeScopedSecretsDirect, purgeSecretBackendsDirect, purgeStaleExecutionDirect, purgeStaleSourcesDirect, registerWorkflowManualDirect, resetRaftStateDirect, seedEnvironmentBindingDirect, seedEnvironmentDirect, setEnvironmentPolicyDirect, setEnvironmentSecretDirect, sha256, showEnvironmentDirect, showExecutionRunDirect, showQueueEntryDirect, showRegistrationDirect, storeMigrationContentHash, tablePrefix, toErrorMessage } from "@kici-dev/shared";
|
|
8
|
+
import * as fs$1 from "node:fs";
|
|
9
|
+
import fs, { createWriteStream, promises, readFileSync } from "node:fs";
|
|
10
|
+
import crypto, { createDecipheriv, createHash, hkdfSync, randomBytes, randomUUID } from "node:crypto";
|
|
8
11
|
import { createInterface } from "node:readline";
|
|
9
12
|
import { readFile, writeFile } from "node:fs/promises";
|
|
10
13
|
import { stringify } from "yaml";
|
|
11
|
-
import crypto, { createDecipheriv, hkdfSync, randomBytes, randomUUID } from "node:crypto";
|
|
12
14
|
import { z } from "zod";
|
|
13
15
|
import "node:os";
|
|
14
16
|
import os from "node:os";
|
|
17
|
+
import { getDatabaseCollationDrift, refreshDatabaseCollationVersion, reindexDatabaseConcurrently } from "@kici-dev/shared/db-collation";
|
|
15
18
|
import { sql } from "kysely";
|
|
16
19
|
import { Migrator } from "kysely/migration";
|
|
17
20
|
import "jsonpath-plus";
|
|
@@ -19,13 +22,13 @@ import { execFile, execFileSync, execSync, spawn } from "node:child_process";
|
|
|
19
22
|
import { promisify } from "node:util";
|
|
20
23
|
import * as path$1 from "node:path";
|
|
21
24
|
import path from "node:path";
|
|
22
|
-
import * as fs$1 from "node:fs";
|
|
23
|
-
import fs, { createWriteStream, readFileSync } from "node:fs";
|
|
24
25
|
import archiver from "archiver";
|
|
25
26
|
import JSZip from "jszip";
|
|
26
27
|
import { confirm, input, password, select } from "@inquirer/prompts";
|
|
27
28
|
import { pipeline } from "node:stream/promises";
|
|
28
29
|
import "pg";
|
|
30
|
+
import { DASHBOARD_WRITE_OPERATIONS, DASHBOARD_WRITE_OPERATIONS_BY_NAME, DashboardWriteCategory, DashboardWriteSensitivity } from "@kici-dev/engine/protocol/dashboard-write-operations";
|
|
31
|
+
import { CLUSTER_NAME_FORMAT_MESSAGE } from "@kici-dev/engine/protocol/cluster-name";
|
|
29
32
|
import { TERMINAL_JOB_STATES, TERMINAL_RUN_STATES, WS_MAX_PAYLOAD_BYTES, accessLogWarmSqlCase, getAccessLogColdDays, getSecretAuditLogColdDays, minAccessLogWarmDays, minSecretAuditLogWarmDays, secretAuditLogWarmSqlCase } from "@kici-dev/engine";
|
|
30
33
|
var __defProp = Object.defineProperty;
|
|
31
34
|
var __esmMin = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
@@ -92,6 +95,12 @@ var AdminApiClient = class {
|
|
|
92
95
|
return this.request("PATCH", path, body);
|
|
93
96
|
}
|
|
94
97
|
/**
|
|
98
|
+
* Public PUT request returning parsed JSON.
|
|
99
|
+
*/
|
|
100
|
+
async put(path, body) {
|
|
101
|
+
return this.request("PUT", path, body);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
95
104
|
* Public DELETE request returning parsed JSON.
|
|
96
105
|
*/
|
|
97
106
|
async delete(path) {
|
|
@@ -125,6 +134,21 @@ var AdminApiClient = class {
|
|
|
125
134
|
async deleteSecret(orgId, scope, key) {
|
|
126
135
|
return this.request("DELETE", `/api/v1/admin/secrets/${encodeURIComponent(orgId)}/${encodeURIComponent(scope)}/${encodeURIComponent(key)}`);
|
|
127
136
|
}
|
|
137
|
+
async listVariables(orgId, environment) {
|
|
138
|
+
const params = new URLSearchParams({ orgId });
|
|
139
|
+
return this.request("GET", `/api/v1/admin/environments/${encodeURIComponent(environment)}/variables?${params}`);
|
|
140
|
+
}
|
|
141
|
+
async setVariable(orgId, environment, key, value, locked) {
|
|
142
|
+
const params = new URLSearchParams({ orgId });
|
|
143
|
+
return this.request("PUT", `/api/v1/admin/environments/${encodeURIComponent(environment)}/variables/${encodeURIComponent(key)}?${params}`, {
|
|
144
|
+
value,
|
|
145
|
+
locked
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
async deleteVariable(orgId, environment, key) {
|
|
149
|
+
const params = new URLSearchParams({ orgId });
|
|
150
|
+
return this.request("DELETE", `/api/v1/admin/environments/${encodeURIComponent(environment)}/variables/${encodeURIComponent(key)}?${params}`);
|
|
151
|
+
}
|
|
128
152
|
async rotateKey() {
|
|
129
153
|
return this.request("POST", "/api/v1/admin/rotate-key");
|
|
130
154
|
}
|
|
@@ -384,6 +408,140 @@ var AdminApiClient = class {
|
|
|
384
408
|
}
|
|
385
409
|
};
|
|
386
410
|
//#endregion
|
|
411
|
+
//#region src/cli/commands/shared/secret-input.ts
|
|
412
|
+
/**
|
|
413
|
+
* Shared input-mode helper for `kici-admin secret set` and
|
|
414
|
+
* `kici-admin variable set`.
|
|
415
|
+
*
|
|
416
|
+
* Resolves a value from exactly one of:
|
|
417
|
+
* --value <plaintext> — argv literal (warned; goes into shell history)
|
|
418
|
+
* --prompt — interactive no-echo prompt (TTY only)
|
|
419
|
+
* --from-stdin — pipe (full stdin until EOF)
|
|
420
|
+
* --from-file <path> — file contents (--trim default-on)
|
|
421
|
+
* --from-env <VAR> — env var contents
|
|
422
|
+
*
|
|
423
|
+
* Default-mode resolution when no flag is provided:
|
|
424
|
+
* TTY -> --prompt
|
|
425
|
+
* non-TTY -> --from-stdin
|
|
426
|
+
*
|
|
427
|
+
* Optional checks:
|
|
428
|
+
* --confirm-fingerprint <hex> — SHA-256 fingerprint guard against
|
|
429
|
+
* paste corruption.
|
|
430
|
+
*
|
|
431
|
+
* The helper does NOT print to stdout (it returns the value). It DOES
|
|
432
|
+
* write to stderr for the `--value` warning, the prompt label, and any
|
|
433
|
+
* "empty value" warning. Stdout is reserved for the caller's confirmation
|
|
434
|
+
* output so the caller's `--dry-run` summary stays clean.
|
|
435
|
+
*/
|
|
436
|
+
const INPUT_MODE_FLAGS = [
|
|
437
|
+
"value",
|
|
438
|
+
"fromStdin",
|
|
439
|
+
"fromFile",
|
|
440
|
+
"fromEnv",
|
|
441
|
+
"prompt"
|
|
442
|
+
];
|
|
443
|
+
const FINGERPRINT_RE = /^[0-9a-f]{64}$/i;
|
|
444
|
+
async function resolveSecretInput(opts, stderr = process.stderr, stdin = process.stdin) {
|
|
445
|
+
const explicit = INPUT_MODE_FLAGS.filter((flag) => {
|
|
446
|
+
const v = opts[flag];
|
|
447
|
+
return v !== void 0 && v !== false;
|
|
448
|
+
});
|
|
449
|
+
if (explicit.length > 1) throw new Error(`Ambiguous input mode: pick exactly one of --value, --from-stdin, --from-file, --from-env, --prompt (got: ${explicit.map((f) => "--" + f.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase())).join(", ")}).`);
|
|
450
|
+
let result;
|
|
451
|
+
if (opts.value !== void 0) {
|
|
452
|
+
stderr.write("warning: --value puts the secret in shell history. Prefer --prompt, --from-stdin, --from-env, or --from-file.\n");
|
|
453
|
+
result = {
|
|
454
|
+
value: opts.value,
|
|
455
|
+
source: "value"
|
|
456
|
+
};
|
|
457
|
+
} else if (opts.fromEnv !== void 0) {
|
|
458
|
+
const envValue = process.env[opts.fromEnv];
|
|
459
|
+
if (envValue === void 0) throw new Error(`--from-env ${opts.fromEnv}: environment variable is not set.`);
|
|
460
|
+
result = {
|
|
461
|
+
value: envValue,
|
|
462
|
+
source: "env"
|
|
463
|
+
};
|
|
464
|
+
} else if (opts.fromFile !== void 0) {
|
|
465
|
+
const raw = await promises.readFile(opts.fromFile, "utf8");
|
|
466
|
+
result = {
|
|
467
|
+
value: opts.trim === false ? raw : raw.replace(/\r?\n$/, ""),
|
|
468
|
+
source: "file"
|
|
469
|
+
};
|
|
470
|
+
} else if (opts.prompt) {
|
|
471
|
+
if (!stdin.isTTY) throw new Error("--prompt requires a TTY; use --from-stdin to read piped input.");
|
|
472
|
+
result = {
|
|
473
|
+
value: await promptSecret$1(stderr, stdin),
|
|
474
|
+
source: "prompt"
|
|
475
|
+
};
|
|
476
|
+
} else if (opts.fromStdin) {
|
|
477
|
+
if (stdin.isTTY) throw new Error("--from-stdin requires piped input (no TTY); pipe the value in or use --prompt to enter interactively.");
|
|
478
|
+
result = {
|
|
479
|
+
value: await readAllStdin(stdin),
|
|
480
|
+
source: "stdin"
|
|
481
|
+
};
|
|
482
|
+
} else if (stdin.isTTY) result = {
|
|
483
|
+
value: await promptSecret$1(stderr, stdin),
|
|
484
|
+
source: "prompt"
|
|
485
|
+
};
|
|
486
|
+
else result = {
|
|
487
|
+
value: await readAllStdin(stdin),
|
|
488
|
+
source: "stdin"
|
|
489
|
+
};
|
|
490
|
+
if (result.value.length === 0) stderr.write("warning: value is empty.\n");
|
|
491
|
+
if (opts.confirmFingerprint !== void 0) {
|
|
492
|
+
if (!FINGERPRINT_RE.test(opts.confirmFingerprint)) throw new Error("--confirm-fingerprint must be a SHA-256 hex string (64 lowercase hex characters).");
|
|
493
|
+
const actual = createHash("sha256").update(result.value, "utf8").digest("hex");
|
|
494
|
+
if (actual !== opts.confirmFingerprint.toLowerCase()) throw new Error(`--confirm-fingerprint mismatch: expected ${opts.confirmFingerprint.toLowerCase()}, got ${actual}. Value not written.`);
|
|
495
|
+
}
|
|
496
|
+
return result;
|
|
497
|
+
}
|
|
498
|
+
async function readAllStdin(stdin) {
|
|
499
|
+
return new Promise((resolve, reject) => {
|
|
500
|
+
const chunks = [];
|
|
501
|
+
stdin.on("data", (chunk) => {
|
|
502
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk);
|
|
503
|
+
});
|
|
504
|
+
stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "")));
|
|
505
|
+
stdin.on("error", reject);
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
async function promptSecret$1(stderr, stdin) {
|
|
509
|
+
const { createInterface } = await import("node:readline");
|
|
510
|
+
const rl = createInterface({
|
|
511
|
+
input: stdin,
|
|
512
|
+
output: stderr
|
|
513
|
+
});
|
|
514
|
+
if (stdin.setRawMode) stdin.setRawMode(true);
|
|
515
|
+
return new Promise((resolve) => {
|
|
516
|
+
stderr.write("Enter value: ");
|
|
517
|
+
let value = "";
|
|
518
|
+
function onData(chunk) {
|
|
519
|
+
const str = chunk.toString("utf8");
|
|
520
|
+
for (const char of str) if (char === "\n" || char === "\r") {
|
|
521
|
+
stdin.removeListener("data", onData);
|
|
522
|
+
if (stdin.setRawMode) stdin.setRawMode(false);
|
|
523
|
+
stderr.write("\n");
|
|
524
|
+
rl.close();
|
|
525
|
+
resolve(value);
|
|
526
|
+
return;
|
|
527
|
+
} else if (char === "" || char === "\b") value = value.slice(0, -1);
|
|
528
|
+
else if (char === "") {
|
|
529
|
+
stderr.write("\n");
|
|
530
|
+
process.exit(1);
|
|
531
|
+
} else value += char;
|
|
532
|
+
}
|
|
533
|
+
stdin.on("data", onData);
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Compute the canonical SHA-256 fingerprint for a value. Helper for
|
|
538
|
+
* callers (tests, --dry-run output) that want to display the same hex
|
|
539
|
+
* the user would pass to `--confirm-fingerprint`.
|
|
540
|
+
*/
|
|
541
|
+
function fingerprintValue(value) {
|
|
542
|
+
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
543
|
+
}
|
|
544
|
+
//#endregion
|
|
387
545
|
//#region src/cli/commands/secret.ts
|
|
388
546
|
function resolveDirectDbUrl$9(explicit) {
|
|
389
547
|
return explicit ?? process.env.KICI_DATABASE_URL ?? null;
|
|
@@ -416,7 +574,7 @@ function registerSecretCommands(program, getClient) {
|
|
|
416
574
|
process.exit(1);
|
|
417
575
|
}
|
|
418
576
|
});
|
|
419
|
-
sec.command("set [orgId] [scope] [key]").description("Set a secret value. Positional form: \"set <orgId> <scope> <key>\". Sugar form (environment scope): \"set --org <id> --environment <env> --key <k>\". Value
|
|
577
|
+
sec.command("set [orgId] [scope] [key]").description("Set a secret value. Positional form: \"set <orgId> <scope> <key>\". Sugar form (environment scope): \"set --org <id> --environment <env> --key <k>\". Value comes from one of: --prompt (default on TTY), --from-stdin (default on pipe), --from-file <path>, --from-env <VAR>, --value <plaintext> (discouraged).").option("--value <value>", "Secret value via argv (visible in shell history; prefer --prompt)").option("--org <orgId>", "Org ID (use with --environment + --key; mutually exclusive with positional form)").option("--environment <name>", "Environment scope — sugar for positional <scope>. Requires --org and --key.").option("--key <key>", "Secret key name (use with --org + --environment)").option("--prompt", "Interactive no-echo prompt (requires TTY)").option("--from-stdin", "Read value from piped stdin until EOF").option("--from-file <path>", "Read value from a file (trailing newline trimmed)").option("--from-env <var>", "Read value from a named environment variable").option("--no-trim", "When reading --from-file, keep the trailing newline (default: trim once)").option("--confirm-fingerprint <sha256hex>", "Refuse the write unless SHA-256(value) matches this 64-hex string").option("--dry-run", "Parse + validate the value, print fingerprint + length, do not write").option("--database-url <url>", "Direct-DB mode: write encrypted_value verbatim to scoped_secrets (offline; skips HTTP + encryption)").action(async (posOrgId, posScope, posKey, opts) => {
|
|
420
578
|
try {
|
|
421
579
|
const hasPositional = Boolean(posOrgId || posScope || posKey);
|
|
422
580
|
const hasSugar = Boolean(opts.org || opts.environment || opts.key);
|
|
@@ -437,9 +595,11 @@ function registerSecretCommands(program, getClient) {
|
|
|
437
595
|
scope = posScope;
|
|
438
596
|
key = posKey;
|
|
439
597
|
}
|
|
440
|
-
|
|
441
|
-
if (opts.
|
|
442
|
-
|
|
598
|
+
const { value, source } = await resolveSecretInput(opts);
|
|
599
|
+
if (opts.dryRun) {
|
|
600
|
+
console.log(`[dry-run] would set secret '${key}' in scope '${scope}' for org ${orgId} (${value.length} chars, source=${source}, sha256=${fingerprintValue(value)})`);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
443
603
|
const dbUrl = resolveDirectDbUrl$9(opts.databaseUrl);
|
|
444
604
|
if (dbUrl) {
|
|
445
605
|
await setEnvironmentSecretDirect(dbUrl, {
|
|
@@ -461,7 +621,7 @@ function registerSecretCommands(program, getClient) {
|
|
|
461
621
|
sec.command("delete <orgId> <scope> <key>").description("Delete a secret").option("--yes", "Skip confirmation prompt").action(async (orgId, scope, key, opts) => {
|
|
462
622
|
try {
|
|
463
623
|
if (!opts.yes) {
|
|
464
|
-
if (!await confirm$
|
|
624
|
+
if (!await confirm$4(`Are you sure you want to delete secret '${key}' from scope '${scope}'?`)) {
|
|
465
625
|
console.log("Aborted.");
|
|
466
626
|
return;
|
|
467
627
|
}
|
|
@@ -474,63 +634,7 @@ function registerSecretCommands(program, getClient) {
|
|
|
474
634
|
}
|
|
475
635
|
});
|
|
476
636
|
}
|
|
477
|
-
|
|
478
|
-
* Read all of piped stdin as a string, trimmed. Throws if stdin is a TTY
|
|
479
|
-
* (no pipe attached). Used by `--from-stdin` in scripted callers (E2E,
|
|
480
|
-
* deploy scripts) that want deterministic piping semantics.
|
|
481
|
-
*/
|
|
482
|
-
async function readPipedStdin() {
|
|
483
|
-
if (process.stdin.isTTY) throw new Error("--from-stdin requires piped input (no TTY); pipe the value in or omit the flag to prompt.");
|
|
484
|
-
return new Promise((resolve, reject) => {
|
|
485
|
-
const chunks = [];
|
|
486
|
-
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
487
|
-
process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString().trim()));
|
|
488
|
-
process.stdin.on("error", reject);
|
|
489
|
-
});
|
|
490
|
-
}
|
|
491
|
-
/**
|
|
492
|
-
* Read a secret value from stdin (pipe) or interactive prompt.
|
|
493
|
-
*
|
|
494
|
-
* - If stdin is piped, reads the entire input as the value.
|
|
495
|
-
* - If stdin is a TTY, prompts for the value with hidden input.
|
|
496
|
-
*/
|
|
497
|
-
async function readSecretValue() {
|
|
498
|
-
if (!process.stdin.isTTY) return new Promise((resolve, reject) => {
|
|
499
|
-
const chunks = [];
|
|
500
|
-
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
501
|
-
process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString().trim()));
|
|
502
|
-
process.stdin.on("error", reject);
|
|
503
|
-
});
|
|
504
|
-
const { createInterface } = await import("node:readline");
|
|
505
|
-
const rl = createInterface({
|
|
506
|
-
input: process.stdin,
|
|
507
|
-
output: process.stderr
|
|
508
|
-
});
|
|
509
|
-
if (process.stdin.setRawMode) process.stdin.setRawMode(true);
|
|
510
|
-
return new Promise((resolve) => {
|
|
511
|
-
process.stderr.write("Enter secret value: ");
|
|
512
|
-
let value = "";
|
|
513
|
-
process.stdin.on("data", function onData(chunk) {
|
|
514
|
-
const str = chunk.toString();
|
|
515
|
-
for (const char of str) if (char === "\n" || char === "\r") {
|
|
516
|
-
process.stdin.removeListener("data", onData);
|
|
517
|
-
if (process.stdin.setRawMode) process.stdin.setRawMode(false);
|
|
518
|
-
process.stderr.write("\n");
|
|
519
|
-
rl.close();
|
|
520
|
-
resolve(value);
|
|
521
|
-
return;
|
|
522
|
-
} else if (char === "" || char === "\b") value = value.slice(0, -1);
|
|
523
|
-
else if (char === "") {
|
|
524
|
-
process.stderr.write("\n");
|
|
525
|
-
process.exit(1);
|
|
526
|
-
} else value += char;
|
|
527
|
-
});
|
|
528
|
-
});
|
|
529
|
-
}
|
|
530
|
-
/**
|
|
531
|
-
* Simple confirmation prompt.
|
|
532
|
-
*/
|
|
533
|
-
async function confirm$3(message) {
|
|
637
|
+
async function confirm$4(message) {
|
|
534
638
|
const { createInterface } = await import("node:readline");
|
|
535
639
|
const rl = createInterface({
|
|
536
640
|
input: process.stdin,
|
|
@@ -610,7 +714,7 @@ function formatTokenTable(tokens) {
|
|
|
610
714
|
}
|
|
611
715
|
function registerTokenCommands(program, getClient) {
|
|
612
716
|
const tok = program.command("token").description("Manage admin API tokens");
|
|
613
|
-
tok.command("create <label>").description("Create a new admin API token").requiredOption("--role <role>", "Token role (owner, admin, auditor)").option("--routing-key <key>", "
|
|
717
|
+
tok.command("create <label>").description("Create a new admin API token").requiredOption("--role <role>", "Token role (owner, admin, auditor)").option("--routing-key <key>", "Restrict the token to a single source routing key (e.g. \"github:42\"). The token can only act on requests targeting that routing key. Without this, the token has full orchestrator access.").action(async (label, opts) => {
|
|
614
718
|
try {
|
|
615
719
|
const result = await getClient().createToken({
|
|
616
720
|
label,
|
|
@@ -1412,8 +1516,8 @@ var init_client = __esmMin((() => {
|
|
|
1412
1516
|
//#region src/db/migrations/001_initial.ts
|
|
1413
1517
|
init_client();
|
|
1414
1518
|
var _001_initial_exports = /* @__PURE__ */ __exportAll({
|
|
1415
|
-
down: () => down$
|
|
1416
|
-
up: () => up$
|
|
1519
|
+
down: () => down$22,
|
|
1520
|
+
up: () => up$22
|
|
1417
1521
|
});
|
|
1418
1522
|
/**
|
|
1419
1523
|
* Squashed initial migration -- creates the complete Orchestrator database schema.
|
|
@@ -2105,7 +2209,7 @@ const DDL_STATEMENTS = [
|
|
|
2105
2209
|
`ALTER TABLE ONLY public.held_runs
|
|
2106
2210
|
ADD CONSTRAINT held_runs_environment_id_fkey FOREIGN KEY (environment_id) REFERENCES public.environments(id);`
|
|
2107
2211
|
];
|
|
2108
|
-
async function up$
|
|
2212
|
+
async function up$22(db) {
|
|
2109
2213
|
for (const stmt of DDL_STATEMENTS) await sql.raw(stmt).execute(db);
|
|
2110
2214
|
await sql`
|
|
2111
2215
|
INSERT INTO cluster_meta (key, value)
|
|
@@ -2127,7 +2231,7 @@ async function up$17(db) {
|
|
|
2127
2231
|
* Rollback drops everything created above. Uses CASCADE on table drops to cut
|
|
2128
2232
|
* through the FK graph without relying on exact topological order.
|
|
2129
2233
|
*/
|
|
2130
|
-
async function down$
|
|
2234
|
+
async function down$22(db) {
|
|
2131
2235
|
for (const [trig, tbl] of [["source_secrets_change_trigger", "scoped_secrets"], ["sources_change_trigger", "sources"]]) await sql.raw(`DROP TRIGGER IF EXISTS ${trig} ON public.${tbl}`).execute(db);
|
|
2132
2236
|
for (const table of [
|
|
2133
2237
|
"workflow_registrations",
|
|
@@ -2174,8 +2278,8 @@ async function down$17(db) {
|
|
|
2174
2278
|
//#endregion
|
|
2175
2279
|
//#region src/db/migrations/002_config_versions_key_version.ts
|
|
2176
2280
|
var _002_config_versions_key_version_exports = /* @__PURE__ */ __exportAll({
|
|
2177
|
-
down: () => down$
|
|
2178
|
-
up: () => up$
|
|
2281
|
+
down: () => down$21,
|
|
2282
|
+
up: () => up$21
|
|
2179
2283
|
});
|
|
2180
2284
|
/**
|
|
2181
2285
|
* Add key_version column to config_versions so that sensitive-field encryption
|
|
@@ -2189,13 +2293,13 @@ var _002_config_versions_key_version_exports = /* @__PURE__ */ __exportAll({
|
|
|
2189
2293
|
* No index is needed: rotation does a full-table scan; reads are by
|
|
2190
2294
|
* `version` primary key and never filter on `key_version`.
|
|
2191
2295
|
*/
|
|
2192
|
-
async function up$
|
|
2296
|
+
async function up$21(db) {
|
|
2193
2297
|
await sql`
|
|
2194
2298
|
ALTER TABLE public.config_versions
|
|
2195
2299
|
ADD COLUMN key_version integer NOT NULL DEFAULT 1
|
|
2196
2300
|
`.execute(db);
|
|
2197
2301
|
}
|
|
2198
|
-
async function down$
|
|
2302
|
+
async function down$21(db) {
|
|
2199
2303
|
await sql`
|
|
2200
2304
|
ALTER TABLE public.config_versions
|
|
2201
2305
|
DROP COLUMN key_version
|
|
@@ -2204,8 +2308,8 @@ async function down$16(db) {
|
|
|
2204
2308
|
//#endregion
|
|
2205
2309
|
//#region src/db/migrations/003_access_log.ts
|
|
2206
2310
|
var _003_access_log_exports = /* @__PURE__ */ __exportAll({
|
|
2207
|
-
down: () => down$
|
|
2208
|
-
up: () => up$
|
|
2311
|
+
down: () => down$20,
|
|
2312
|
+
up: () => up$20
|
|
2209
2313
|
});
|
|
2210
2314
|
/**
|
|
2211
2315
|
* Access log: one row per read or orchestrator-admin mutation attributable
|
|
@@ -2229,7 +2333,7 @@ var _003_access_log_exports = /* @__PURE__ */ __exportAll({
|
|
|
2229
2333
|
* Retention is TTL-based via expires_at; packages/orchestrator/src/queue/
|
|
2230
2334
|
* cleanup.ts picks up the prune pass.
|
|
2231
2335
|
*/
|
|
2232
|
-
async function up$
|
|
2336
|
+
async function up$20(db) {
|
|
2233
2337
|
await sql`
|
|
2234
2338
|
CREATE TABLE public.access_log (
|
|
2235
2339
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
@@ -2267,28 +2371,28 @@ async function up$15(db) {
|
|
|
2267
2371
|
ON public.access_log (actor_type, actor_id, created_at DESC)
|
|
2268
2372
|
`.execute(db);
|
|
2269
2373
|
}
|
|
2270
|
-
async function down$
|
|
2374
|
+
async function down$20(db) {
|
|
2271
2375
|
await sql`DROP TABLE IF EXISTS public.access_log`.execute(db);
|
|
2272
2376
|
}
|
|
2273
2377
|
//#endregion
|
|
2274
2378
|
//#region src/db/migrations/004_rename_bundle_to_source.ts
|
|
2275
2379
|
var _004_rename_bundle_to_source_exports = /* @__PURE__ */ __exportAll({
|
|
2276
|
-
down: () => down$
|
|
2277
|
-
up: () => up$
|
|
2380
|
+
down: () => down$19,
|
|
2381
|
+
up: () => up$19
|
|
2278
2382
|
});
|
|
2279
|
-
async function up$
|
|
2383
|
+
async function up$19(db) {
|
|
2280
2384
|
await db.schema.alterTable("dispatch_queue").renameColumn("bundle_url", "source_tar_url").execute();
|
|
2281
2385
|
await db.schema.alterTable("dispatch_queue").renameColumn("bundle_hash", "source_tar_hash").execute();
|
|
2282
2386
|
}
|
|
2283
|
-
async function down$
|
|
2387
|
+
async function down$19(db) {
|
|
2284
2388
|
await db.schema.alterTable("dispatch_queue").renameColumn("source_tar_url", "bundle_url").execute();
|
|
2285
2389
|
await db.schema.alterTable("dispatch_queue").renameColumn("source_tar_hash", "bundle_hash").execute();
|
|
2286
2390
|
}
|
|
2287
2391
|
//#endregion
|
|
2288
2392
|
//#region src/db/migrations/005_cold_store_chunk_counter.ts
|
|
2289
2393
|
var _005_cold_store_chunk_counter_exports = /* @__PURE__ */ __exportAll({
|
|
2290
|
-
down: () => down$
|
|
2291
|
-
up: () => up$
|
|
2394
|
+
down: () => down$18,
|
|
2395
|
+
up: () => up$18
|
|
2292
2396
|
});
|
|
2293
2397
|
/**
|
|
2294
2398
|
* Cold-store chunk counter table.
|
|
@@ -2305,7 +2409,7 @@ var _005_cold_store_chunk_counter_exports = /* @__PURE__ */ __exportAll({
|
|
|
2305
2409
|
*
|
|
2306
2410
|
* sections 5 and 8.
|
|
2307
2411
|
*/
|
|
2308
|
-
async function up$
|
|
2412
|
+
async function up$18(db) {
|
|
2309
2413
|
await sql`
|
|
2310
2414
|
CREATE TABLE public.cold_store_chunk_counts (
|
|
2311
2415
|
db TEXT NOT NULL,
|
|
@@ -2323,14 +2427,14 @@ async function up$13(db) {
|
|
|
2323
2427
|
ON public.cold_store_chunk_counts (db, table_name)
|
|
2324
2428
|
`.execute(db);
|
|
2325
2429
|
}
|
|
2326
|
-
async function down$
|
|
2430
|
+
async function down$18(db) {
|
|
2327
2431
|
await sql`DROP TABLE IF EXISTS public.cold_store_chunk_counts`.execute(db);
|
|
2328
2432
|
}
|
|
2329
2433
|
//#endregion
|
|
2330
2434
|
//#region src/db/migrations/006_runs_jobs_steps_archived_at.ts
|
|
2331
2435
|
var _006_runs_jobs_steps_archived_at_exports = /* @__PURE__ */ __exportAll({
|
|
2332
|
-
down: () => down$
|
|
2333
|
-
up: () => up$
|
|
2436
|
+
down: () => down$17,
|
|
2437
|
+
up: () => up$17
|
|
2334
2438
|
});
|
|
2335
2439
|
/**
|
|
2336
2440
|
* `execution_runs` / `execution_jobs` / `execution_steps` cold-store
|
|
@@ -2366,7 +2470,7 @@ var _006_runs_jobs_steps_archived_at_exports = /* @__PURE__ */ __exportAll({
|
|
|
2366
2470
|
* - `idx_execution_jobs_routing_key_created (routing_key, created_at)`
|
|
2367
2471
|
* - `idx_execution_steps_routing_key_created (routing_key, created_at)`
|
|
2368
2472
|
*/
|
|
2369
|
-
async function up$
|
|
2473
|
+
async function up$17(db) {
|
|
2370
2474
|
await sql`
|
|
2371
2475
|
ALTER TABLE public.execution_runs
|
|
2372
2476
|
ADD COLUMN archived_at TIMESTAMPTZ NULL,
|
|
@@ -2411,7 +2515,7 @@ async function up$12(db) {
|
|
|
2411
2515
|
ON public.execution_steps (routing_key, created_at)
|
|
2412
2516
|
`.execute(db);
|
|
2413
2517
|
}
|
|
2414
|
-
async function down$
|
|
2518
|
+
async function down$17(db) {
|
|
2415
2519
|
await sql`DROP INDEX IF EXISTS public.idx_execution_steps_routing_key_created`.execute(db);
|
|
2416
2520
|
await sql`
|
|
2417
2521
|
ALTER TABLE public.execution_steps
|
|
@@ -2436,8 +2540,8 @@ async function down$12(db) {
|
|
|
2436
2540
|
//#endregion
|
|
2437
2541
|
//#region src/db/migrations/007_audit_logs_archived_at.ts
|
|
2438
2542
|
var _007_audit_logs_archived_at_exports = /* @__PURE__ */ __exportAll({
|
|
2439
|
-
down: () => down$
|
|
2440
|
-
up: () => up$
|
|
2543
|
+
down: () => down$16,
|
|
2544
|
+
up: () => up$16
|
|
2441
2545
|
});
|
|
2442
2546
|
/**
|
|
2443
2547
|
* `secret_audit_log` and `access_log` cold-store schema additions, plus
|
|
@@ -2477,7 +2581,7 @@ var _007_audit_logs_archived_at_exports = /* @__PURE__ */ __exportAll({
|
|
|
2477
2581
|
* `down()` would not have meaningful retention bounds. Acceptable for
|
|
2478
2582
|
* staging.
|
|
2479
2583
|
*/
|
|
2480
|
-
async function up$
|
|
2584
|
+
async function up$16(db) {
|
|
2481
2585
|
await sql`
|
|
2482
2586
|
ALTER TABLE public.secret_audit_log
|
|
2483
2587
|
ADD COLUMN archived_at TIMESTAMPTZ NULL,
|
|
@@ -2498,7 +2602,7 @@ async function up$11(db) {
|
|
|
2498
2602
|
DROP COLUMN IF EXISTS expires_at
|
|
2499
2603
|
`.execute(db);
|
|
2500
2604
|
}
|
|
2501
|
-
async function down$
|
|
2605
|
+
async function down$16(db) {
|
|
2502
2606
|
await sql`
|
|
2503
2607
|
ALTER TABLE public.access_log
|
|
2504
2608
|
ADD COLUMN expires_at TIMESTAMPTZ NOT NULL DEFAULT (now() + INTERVAL '90 days')
|
|
@@ -2526,8 +2630,8 @@ async function down$11(db) {
|
|
|
2526
2630
|
//#endregion
|
|
2527
2631
|
//#region src/db/migrations/008_event_log_archived_at.ts
|
|
2528
2632
|
var _008_event_log_archived_at_exports = /* @__PURE__ */ __exportAll({
|
|
2529
|
-
down: () => down$
|
|
2530
|
-
up: () => up$
|
|
2633
|
+
down: () => down$15,
|
|
2634
|
+
up: () => up$15
|
|
2531
2635
|
});
|
|
2532
2636
|
/**
|
|
2533
2637
|
* `event_log` cold-store schema additions plus removal of the
|
|
@@ -2565,7 +2669,7 @@ var _008_event_log_archived_at_exports = /* @__PURE__ */ __exportAll({
|
|
|
2565
2669
|
* — best effort; rows inserted between `up()` and a hypothetical
|
|
2566
2670
|
* `down()` would not have meaningful retention bounds.
|
|
2567
2671
|
*/
|
|
2568
|
-
async function up$
|
|
2672
|
+
async function up$15(db) {
|
|
2569
2673
|
await sql`
|
|
2570
2674
|
ALTER TABLE public.event_log
|
|
2571
2675
|
ADD COLUMN archived_at TIMESTAMPTZ NULL,
|
|
@@ -2581,7 +2685,7 @@ async function up$10(db) {
|
|
|
2581
2685
|
DROP COLUMN IF EXISTS expires_at
|
|
2582
2686
|
`.execute(db);
|
|
2583
2687
|
}
|
|
2584
|
-
async function down$
|
|
2688
|
+
async function down$15(db) {
|
|
2585
2689
|
await sql`
|
|
2586
2690
|
ALTER TABLE public.event_log
|
|
2587
2691
|
ADD COLUMN expires_at TIMESTAMPTZ NOT NULL DEFAULT (now() + INTERVAL '30 days')
|
|
@@ -2600,8 +2704,8 @@ async function down$10(db) {
|
|
|
2600
2704
|
//#endregion
|
|
2601
2705
|
//#region src/db/migrations/009_access_log_trigram.ts
|
|
2602
2706
|
var _009_access_log_trigram_exports = /* @__PURE__ */ __exportAll({
|
|
2603
|
-
down: () => down$
|
|
2604
|
-
up: () => up$
|
|
2707
|
+
down: () => down$14,
|
|
2708
|
+
up: () => up$14
|
|
2605
2709
|
});
|
|
2606
2710
|
/**
|
|
2607
2711
|
* Trigram (pg_trgm) index on access_log.error_message for the federated
|
|
@@ -2614,7 +2718,7 @@ var _009_access_log_trigram_exports = /* @__PURE__ */ __exportAll({
|
|
|
2614
2718
|
* EXISTS` are both safe to re-run. No CONCURRENTLY because Kysely runs
|
|
2615
2719
|
* migrations inside a transaction; the lock is brief on a sampled table.
|
|
2616
2720
|
*/
|
|
2617
|
-
async function up$
|
|
2721
|
+
async function up$14(db) {
|
|
2618
2722
|
await sql`CREATE EXTENSION IF NOT EXISTS pg_trgm`.execute(db);
|
|
2619
2723
|
await sql`
|
|
2620
2724
|
CREATE INDEX IF NOT EXISTS access_log_error_message_trgm_idx
|
|
@@ -2623,14 +2727,14 @@ async function up$9(db) {
|
|
|
2623
2727
|
WHERE error_message IS NOT NULL
|
|
2624
2728
|
`.execute(db);
|
|
2625
2729
|
}
|
|
2626
|
-
async function down$
|
|
2730
|
+
async function down$14(db) {
|
|
2627
2731
|
await sql`DROP INDEX IF EXISTS public.access_log_error_message_trgm_idx`.execute(db);
|
|
2628
2732
|
}
|
|
2629
2733
|
//#endregion
|
|
2630
2734
|
//#region src/db/migrations/010_cold_store_chunks.ts
|
|
2631
2735
|
var _010_cold_store_chunks_exports = /* @__PURE__ */ __exportAll({
|
|
2632
|
-
down: () => down$
|
|
2633
|
-
up: () => up$
|
|
2736
|
+
down: () => down$13,
|
|
2737
|
+
up: () => up$13
|
|
2634
2738
|
});
|
|
2635
2739
|
/**
|
|
2636
2740
|
* Cold-store chunk index — Phase 2 (cold-store purge).
|
|
@@ -2662,7 +2766,7 @@ var _010_cold_store_chunks_exports = /* @__PURE__ */ __exportAll({
|
|
|
2662
2766
|
* forever. Adapters that don't opt into per-bucket archival via
|
|
2663
2767
|
* `coldTtlDays` don't insert here either.
|
|
2664
2768
|
*/
|
|
2665
|
-
async function up$
|
|
2769
|
+
async function up$13(db) {
|
|
2666
2770
|
await sql`
|
|
2667
2771
|
CREATE TABLE public.cold_store_chunks (
|
|
2668
2772
|
db TEXT NOT NULL,
|
|
@@ -2689,14 +2793,14 @@ async function up$8(db) {
|
|
|
2689
2793
|
ON public.cold_store_chunks (db, table_name, tenant_id, archived_at DESC)
|
|
2690
2794
|
`.execute(db);
|
|
2691
2795
|
}
|
|
2692
|
-
async function down$
|
|
2796
|
+
async function down$13(db) {
|
|
2693
2797
|
await sql`DROP TABLE IF EXISTS public.cold_store_chunks`.execute(db);
|
|
2694
2798
|
}
|
|
2695
2799
|
//#endregion
|
|
2696
2800
|
//#region src/db/migrations/011_drop_source_secrets_notify.ts
|
|
2697
2801
|
var _011_drop_source_secrets_notify_exports = /* @__PURE__ */ __exportAll({
|
|
2698
|
-
down: () => down$
|
|
2699
|
-
up: () => up$
|
|
2802
|
+
down: () => down$12,
|
|
2803
|
+
up: () => up$12
|
|
2700
2804
|
});
|
|
2701
2805
|
/**
|
|
2702
2806
|
* Drop the `source_secrets_change_trigger` and the
|
|
@@ -2715,11 +2819,11 @@ var _011_drop_source_secrets_notify_exports = /* @__PURE__ */ __exportAll({
|
|
|
2715
2819
|
* in `001_initial.ts`. They wake up no consumer until the `WebhookSecretManager`
|
|
2716
2820
|
* is restored, so this migration is safe to roll back.
|
|
2717
2821
|
*/
|
|
2718
|
-
async function up$
|
|
2822
|
+
async function up$12(db) {
|
|
2719
2823
|
await sql`DROP TRIGGER IF EXISTS source_secrets_change_trigger ON public.scoped_secrets`.execute(db);
|
|
2720
2824
|
await sql`DROP FUNCTION IF EXISTS public.notify_source_secrets_change() CASCADE`.execute(db);
|
|
2721
2825
|
}
|
|
2722
|
-
async function down$
|
|
2826
|
+
async function down$12(db) {
|
|
2723
2827
|
await sql`
|
|
2724
2828
|
CREATE OR REPLACE FUNCTION public.notify_source_secrets_change() RETURNS trigger
|
|
2725
2829
|
LANGUAGE plpgsql
|
|
@@ -2758,8 +2862,8 @@ async function down$7(db) {
|
|
|
2758
2862
|
//#endregion
|
|
2759
2863
|
//#region src/db/migrations/012_peer_credentials_active_uniq.ts
|
|
2760
2864
|
var _012_peer_credentials_active_uniq_exports = /* @__PURE__ */ __exportAll({
|
|
2761
|
-
down: () => down$
|
|
2762
|
-
up: () => up$
|
|
2865
|
+
down: () => down$11,
|
|
2866
|
+
up: () => up$11
|
|
2763
2867
|
});
|
|
2764
2868
|
/**
|
|
2765
2869
|
* Add a partial unique index on `peer_credentials (instance_id) WHERE
|
|
@@ -2785,7 +2889,7 @@ var _012_peer_credentials_active_uniq_exports = /* @__PURE__ */ __exportAll({
|
|
|
2785
2889
|
* `down()` only drops the index; it does NOT undo the dedupe (there's no
|
|
2786
2890
|
* safe way to recreate revoked rows, and the dedupe is monotonic).
|
|
2787
2891
|
*/
|
|
2788
|
-
async function up$
|
|
2892
|
+
async function up$11(db) {
|
|
2789
2893
|
await sql`
|
|
2790
2894
|
UPDATE public.peer_credentials
|
|
2791
2895
|
SET revoked_at = NOW()
|
|
@@ -2803,14 +2907,14 @@ async function up$6(db) {
|
|
|
2803
2907
|
WHERE revoked_at IS NULL
|
|
2804
2908
|
`.execute(db);
|
|
2805
2909
|
}
|
|
2806
|
-
async function down$
|
|
2910
|
+
async function down$11(db) {
|
|
2807
2911
|
await sql`DROP INDEX IF EXISTS public.peer_credentials_active_uniq`.execute(db);
|
|
2808
2912
|
}
|
|
2809
2913
|
//#endregion
|
|
2810
2914
|
//#region src/db/migrations/013_execution_log_bytes.ts
|
|
2811
2915
|
var _013_execution_log_bytes_exports = /* @__PURE__ */ __exportAll({
|
|
2812
|
-
down: () => down$
|
|
2813
|
-
up: () => up$
|
|
2916
|
+
down: () => down$10,
|
|
2917
|
+
up: () => up$10
|
|
2814
2918
|
});
|
|
2815
2919
|
/**
|
|
2816
2920
|
* Add `log_bytes BIGINT NOT NULL DEFAULT 0` columns to `execution_runs` and
|
|
@@ -2829,7 +2933,7 @@ var _013_execution_log_bytes_exports = /* @__PURE__ */ __exportAll({
|
|
|
2829
2933
|
*
|
|
2830
2934
|
* Idempotent (`ADD COLUMN IF NOT EXISTS`).
|
|
2831
2935
|
*/
|
|
2832
|
-
async function up$
|
|
2936
|
+
async function up$10(db) {
|
|
2833
2937
|
await sql`
|
|
2834
2938
|
ALTER TABLE public.execution_runs
|
|
2835
2939
|
ADD COLUMN IF NOT EXISTS log_bytes BIGINT NOT NULL DEFAULT 0
|
|
@@ -2839,15 +2943,15 @@ async function up$5(db) {
|
|
|
2839
2943
|
ADD COLUMN IF NOT EXISTS log_bytes BIGINT NOT NULL DEFAULT 0
|
|
2840
2944
|
`.execute(db);
|
|
2841
2945
|
}
|
|
2842
|
-
async function down$
|
|
2946
|
+
async function down$10(db) {
|
|
2843
2947
|
await sql`ALTER TABLE public.execution_runs DROP COLUMN IF EXISTS log_bytes`.execute(db);
|
|
2844
2948
|
await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS log_bytes`.execute(db);
|
|
2845
2949
|
}
|
|
2846
2950
|
//#endregion
|
|
2847
2951
|
//#region src/db/migrations/014_kici_events_lease_retry.ts
|
|
2848
2952
|
var _014_kici_events_lease_retry_exports = /* @__PURE__ */ __exportAll({
|
|
2849
|
-
down: () => down$
|
|
2850
|
-
up: () => up$
|
|
2953
|
+
down: () => down$9,
|
|
2954
|
+
up: () => up$9
|
|
2851
2955
|
});
|
|
2852
2956
|
/**
|
|
2853
2957
|
* Add lease + retry + DLQ columns to `kici_events` so the EventRouter can
|
|
@@ -2881,7 +2985,7 @@ var _014_kici_events_lease_retry_exports = /* @__PURE__ */ __exportAll({
|
|
|
2881
2985
|
*
|
|
2882
2986
|
* Idempotent (`ADD COLUMN IF NOT EXISTS` + `CREATE INDEX IF NOT EXISTS`).
|
|
2883
2987
|
*/
|
|
2884
|
-
async function up$
|
|
2988
|
+
async function up$9(db) {
|
|
2885
2989
|
await sql`
|
|
2886
2990
|
ALTER TABLE public.kici_events
|
|
2887
2991
|
ADD COLUMN IF NOT EXISTS claimed_at TIMESTAMPTZ,
|
|
@@ -2912,7 +3016,7 @@ async function up$4(db) {
|
|
|
2912
3016
|
WHERE dlq_at IS NOT NULL
|
|
2913
3017
|
`.execute(db);
|
|
2914
3018
|
}
|
|
2915
|
-
async function down$
|
|
3019
|
+
async function down$9(db) {
|
|
2916
3020
|
await sql`DROP INDEX IF EXISTS public.idx_kici_events_dlq`.execute(db);
|
|
2917
3021
|
await sql`DROP INDEX IF EXISTS public.idx_kici_events_lease_expired`.execute(db);
|
|
2918
3022
|
await sql`DROP INDEX IF EXISTS public.idx_kici_events_retry_due`.execute(db);
|
|
@@ -2930,8 +3034,8 @@ async function down$4(db) {
|
|
|
2930
3034
|
//#endregion
|
|
2931
3035
|
//#region src/db/migrations/015_org_settings_customer_scoped.ts
|
|
2932
3036
|
var _015_org_settings_customer_scoped_exports = /* @__PURE__ */ __exportAll({
|
|
2933
|
-
down: () => down$
|
|
2934
|
-
up: () => up$
|
|
3037
|
+
down: () => down$8,
|
|
3038
|
+
up: () => up$8
|
|
2935
3039
|
});
|
|
2936
3040
|
/**
|
|
2937
3041
|
* Org-scope `org_settings` and qualify each glob entry by source.
|
|
@@ -2959,7 +3063,7 @@ var _015_org_settings_customer_scoped_exports = /* @__PURE__ */ __exportAll({
|
|
|
2959
3063
|
* Idempotent: a re-run on an already-migrated DB sees `customer_id` exists
|
|
2960
3064
|
* and the list columns are already jsonb, so it is a no-op.
|
|
2961
3065
|
*/
|
|
2962
|
-
async function up$
|
|
3066
|
+
async function up$8(db) {
|
|
2963
3067
|
if ((await sql`
|
|
2964
3068
|
SELECT EXISTS (
|
|
2965
3069
|
SELECT 1 FROM information_schema.columns
|
|
@@ -3084,7 +3188,7 @@ async function up$3(db) {
|
|
|
3084
3188
|
await sql`DROP TABLE _org_settings_merged`.execute(db);
|
|
3085
3189
|
await sql`DROP TABLE _org_settings_stage`.execute(db);
|
|
3086
3190
|
}
|
|
3087
|
-
async function down$
|
|
3191
|
+
async function down$8(db) {
|
|
3088
3192
|
if (!(await sql`
|
|
3089
3193
|
SELECT EXISTS (
|
|
3090
3194
|
SELECT 1 FROM information_schema.columns
|
|
@@ -3109,8 +3213,8 @@ async function down$3(db) {
|
|
|
3109
3213
|
//#endregion
|
|
3110
3214
|
//#region src/db/migrations/016_org_settings_allow_http_npm.ts
|
|
3111
3215
|
var _016_org_settings_allow_http_npm_exports = /* @__PURE__ */ __exportAll({
|
|
3112
|
-
down: () => down$
|
|
3113
|
-
up: () => up$
|
|
3216
|
+
down: () => down$7,
|
|
3217
|
+
up: () => up$7
|
|
3114
3218
|
});
|
|
3115
3219
|
/**
|
|
3116
3220
|
* Add `org_settings.allow_http_npm_registries boolean NOT NULL DEFAULT false`.
|
|
@@ -3123,7 +3227,7 @@ var _016_org_settings_allow_http_npm_exports = /* @__PURE__ */ __exportAll({
|
|
|
3123
3227
|
*
|
|
3124
3228
|
* Idempotent: a re-run on a DB that already has the column is a no-op.
|
|
3125
3229
|
*/
|
|
3126
|
-
async function up$
|
|
3230
|
+
async function up$7(db) {
|
|
3127
3231
|
if ((await sql`
|
|
3128
3232
|
SELECT EXISTS (
|
|
3129
3233
|
SELECT 1 FROM information_schema.columns
|
|
@@ -3137,7 +3241,7 @@ async function up$2(db) {
|
|
|
3137
3241
|
ADD COLUMN allow_http_npm_registries boolean NOT NULL DEFAULT false
|
|
3138
3242
|
`.execute(db);
|
|
3139
3243
|
}
|
|
3140
|
-
async function down$
|
|
3244
|
+
async function down$7(db) {
|
|
3141
3245
|
await sql`
|
|
3142
3246
|
ALTER TABLE public.org_settings DROP COLUMN IF EXISTS allow_http_npm_registries
|
|
3143
3247
|
`.execute(db);
|
|
@@ -3145,8 +3249,8 @@ async function down$2(db) {
|
|
|
3145
3249
|
//#endregion
|
|
3146
3250
|
//#region src/db/migrations/017_org_id_widen.ts
|
|
3147
3251
|
var _017_org_id_widen_exports = /* @__PURE__ */ __exportAll({
|
|
3148
|
-
down: () => down$
|
|
3149
|
-
up: () => up$
|
|
3252
|
+
down: () => down$6,
|
|
3253
|
+
up: () => up$6
|
|
3150
3254
|
});
|
|
3151
3255
|
/**
|
|
3152
3256
|
* Widen every orchestrator-side `org_id varchar(12)` column to
|
|
@@ -3186,17 +3290,17 @@ const ORG_ID_TABLES$1 = [
|
|
|
3186
3290
|
"held_runs",
|
|
3187
3291
|
"scoped_secrets"
|
|
3188
3292
|
];
|
|
3189
|
-
async function up$
|
|
3293
|
+
async function up$6(db) {
|
|
3190
3294
|
for (const table of ORG_ID_TABLES$1) await sql.raw(`ALTER TABLE public.${table} ALTER COLUMN org_id TYPE varchar(16)`).execute(db);
|
|
3191
3295
|
}
|
|
3192
|
-
async function down$
|
|
3296
|
+
async function down$6(db) {
|
|
3193
3297
|
for (const table of ORG_ID_TABLES$1) await sql.raw(`ALTER TABLE public.${table} ALTER COLUMN org_id TYPE varchar(12)`).execute(db);
|
|
3194
3298
|
}
|
|
3195
3299
|
//#endregion
|
|
3196
3300
|
//#region src/db/migrations/018_org_id_prefix_backfill.ts
|
|
3197
3301
|
var _018_org_id_prefix_backfill_exports = /* @__PURE__ */ __exportAll({
|
|
3198
|
-
down: () => down,
|
|
3199
|
-
up: () => up
|
|
3302
|
+
down: () => down$5,
|
|
3303
|
+
up: () => up$5
|
|
3200
3304
|
});
|
|
3201
3305
|
/**
|
|
3202
3306
|
* Prefix every orchestrator-side tenant string with `org_` to align
|
|
@@ -3213,41 +3317,341 @@ var _018_org_id_prefix_backfill_exports = /* @__PURE__ */ __exportAll({
|
|
|
3213
3317
|
* `environment_bindings`, `environment_source_overrides`,
|
|
3214
3318
|
* `environment_variables`).
|
|
3215
3319
|
*
|
|
3216
|
-
* This migration walks both column families — `org_id` and
|
|
3217
|
-
* `customer_id` — and applies the same `'org_' || value` prefix to
|
|
3218
|
-
* every row whose value is not the platform-admin sentinel
|
|
3219
|
-
* `kici-admin`. After it runs, the orchestrator's `WHERE org_id = $1`
|
|
3220
|
-
* / `WHERE customer_id = $1` filters match the prefixed canonical IDs
|
|
3221
|
-
* that Platform now sends.
|
|
3320
|
+
* This migration walks both column families — `org_id` and
|
|
3321
|
+
* `customer_id` — and applies the same `'org_' || value` prefix to
|
|
3322
|
+
* every row whose value is not the platform-admin sentinel
|
|
3323
|
+
* `kici-admin`. After it runs, the orchestrator's `WHERE org_id = $1`
|
|
3324
|
+
* / `WHERE customer_id = $1` filters match the prefixed canonical IDs
|
|
3325
|
+
* that Platform now sends.
|
|
3326
|
+
*
|
|
3327
|
+
* Idempotency: the `WHERE NOT LIKE 'org\\_%'` guard makes the
|
|
3328
|
+
* migration a no-op on rows already prefixed (e.g. a re-run after a
|
|
3329
|
+
* fresh deploy populated some rows already), so applying it twice is
|
|
3330
|
+
* safe.
|
|
3331
|
+
*/
|
|
3332
|
+
const ORG_ID_TABLES = [
|
|
3333
|
+
"access_log",
|
|
3334
|
+
"environment_bindings",
|
|
3335
|
+
"environment_source_overrides",
|
|
3336
|
+
"environment_variables",
|
|
3337
|
+
"environments",
|
|
3338
|
+
"event_log",
|
|
3339
|
+
"held_runs",
|
|
3340
|
+
"scoped_secrets"
|
|
3341
|
+
];
|
|
3342
|
+
const CUSTOMER_ID_TABLES = [
|
|
3343
|
+
"sources",
|
|
3344
|
+
"generic_webhook_sources",
|
|
3345
|
+
"workflow_registrations",
|
|
3346
|
+
"org_settings"
|
|
3347
|
+
];
|
|
3348
|
+
async function up$5(db) {
|
|
3349
|
+
for (const table of ORG_ID_TABLES) await sql.raw(`UPDATE public.${table} SET org_id = 'org_' || org_id WHERE org_id <> 'kici-admin' AND org_id NOT LIKE 'org\\_%' ESCAPE '\\'`).execute(db);
|
|
3350
|
+
for (const table of CUSTOMER_ID_TABLES) await sql.raw(`UPDATE public.${table} SET customer_id = 'org_' || customer_id WHERE customer_id <> 'kici-admin' AND customer_id NOT LIKE 'org\\_%' ESCAPE '\\'`).execute(db);
|
|
3351
|
+
}
|
|
3352
|
+
async function down$5(db) {
|
|
3353
|
+
for (const table of CUSTOMER_ID_TABLES) await sql.raw(`UPDATE public.${table} SET customer_id = substring(customer_id from 5) WHERE customer_id LIKE 'org\\_%' ESCAPE '\\'`).execute(db);
|
|
3354
|
+
for (const table of ORG_ID_TABLES) await sql.raw(`UPDATE public.${table} SET org_id = substring(org_id from 5) WHERE org_id LIKE 'org\\_%' ESCAPE '\\'`).execute(db);
|
|
3355
|
+
}
|
|
3356
|
+
//#endregion
|
|
3357
|
+
//#region src/db/migrations/019_generic_sources_change_notify.ts
|
|
3358
|
+
var _019_generic_sources_change_notify_exports = /* @__PURE__ */ __exportAll({
|
|
3359
|
+
down: () => down$4,
|
|
3360
|
+
up: () => up$4
|
|
3361
|
+
});
|
|
3362
|
+
/**
|
|
3363
|
+
* Add a Postgres trigger on `generic_webhook_sources` that emits
|
|
3364
|
+
* `pg_notify('generic_sources_change', routing_key)` on every INSERT,
|
|
3365
|
+
* UPDATE, and DELETE. Soft-deletes (UPDATE that sets `deleted_at`) ride
|
|
3366
|
+
* the same UPDATE path — the listener treats a row with `deleted_at IS
|
|
3367
|
+
* NOT NULL` (or a row that no longer exists for that routing_key) as a
|
|
3368
|
+
* de-registration signal.
|
|
3369
|
+
*
|
|
3370
|
+
* The consumer is `GenericSourcesChangeListener`
|
|
3371
|
+
* (`packages/orchestrator/src/webhook/generic-sources-listener.ts`),
|
|
3372
|
+
* which runs in every orchestrator peer, LISTENs on the channel, and
|
|
3373
|
+
* mutates the local `ProviderRegistry` in place
|
|
3374
|
+
* (`registerProviderBundleForSource` for INSERT/UPDATE; `unregister` for
|
|
3375
|
+
* DELETE / soft-delete). Without this round-trip, peers other than the
|
|
3376
|
+
* one that handled `kici-admin source add generic` would 404 incoming
|
|
3377
|
+
* webhooks until restart.
|
|
3378
|
+
*
|
|
3379
|
+
* Mirrors the existing GitHub-app `sources` trigger
|
|
3380
|
+
* (`notify_sources_change()` + `sources_change_trigger`, defined in
|
|
3381
|
+
* `001_initial.ts`).
|
|
3382
|
+
*/
|
|
3383
|
+
async function up$4(db) {
|
|
3384
|
+
await sql`
|
|
3385
|
+
CREATE FUNCTION public.notify_generic_sources_change() RETURNS trigger
|
|
3386
|
+
LANGUAGE plpgsql
|
|
3387
|
+
AS $$
|
|
3388
|
+
BEGIN
|
|
3389
|
+
IF TG_OP = 'DELETE' THEN
|
|
3390
|
+
PERFORM pg_notify('generic_sources_change', OLD.routing_key);
|
|
3391
|
+
ELSE
|
|
3392
|
+
PERFORM pg_notify('generic_sources_change', NEW.routing_key);
|
|
3393
|
+
END IF;
|
|
3394
|
+
RETURN NULL;
|
|
3395
|
+
END;
|
|
3396
|
+
$$
|
|
3397
|
+
`.execute(db);
|
|
3398
|
+
await sql`
|
|
3399
|
+
CREATE TRIGGER generic_sources_change_trigger
|
|
3400
|
+
AFTER INSERT OR UPDATE OR DELETE ON public.generic_webhook_sources
|
|
3401
|
+
FOR EACH ROW EXECUTE FUNCTION public.notify_generic_sources_change()
|
|
3402
|
+
`.execute(db);
|
|
3403
|
+
}
|
|
3404
|
+
async function down$4(db) {
|
|
3405
|
+
await sql`DROP TRIGGER IF EXISTS generic_sources_change_trigger ON public.generic_webhook_sources`.execute(db);
|
|
3406
|
+
await sql`DROP FUNCTION IF EXISTS public.notify_generic_sources_change()`.execute(db);
|
|
3407
|
+
}
|
|
3408
|
+
//#endregion
|
|
3409
|
+
//#region src/db/migrations/020_org_settings_dashboard_write_policy.ts
|
|
3410
|
+
var _020_org_settings_dashboard_write_policy_exports = /* @__PURE__ */ __exportAll({
|
|
3411
|
+
down: () => down$3,
|
|
3412
|
+
up: () => up$3
|
|
3413
|
+
});
|
|
3414
|
+
/**
|
|
3415
|
+
* Add `org_settings.dashboard_write_policy jsonb NOT NULL DEFAULT '{}'`.
|
|
3416
|
+
*
|
|
3417
|
+
* Stores the per-customer policy that decides which Platform-routed
|
|
3418
|
+
* dashboard.* write operations the orchestrator accepts. An empty
|
|
3419
|
+
* JSONB object means everything is enabled (permissive default). To
|
|
3420
|
+
* disable an operation, set its key to `false`, e.g.
|
|
3421
|
+
* `{"secrets.set": false, "variables.set": false}`. Unknown / missing
|
|
3422
|
+
* keys are treated as `true` by the resolver in
|
|
3423
|
+
* `@kici-dev/engine/protocol/dashboard-write-operations`.
|
|
3424
|
+
*
|
|
3425
|
+
* Idempotent: a re-run on a DB that already has the column is a no-op.
|
|
3426
|
+
*/
|
|
3427
|
+
async function up$3(db) {
|
|
3428
|
+
if ((await sql`
|
|
3429
|
+
SELECT EXISTS (
|
|
3430
|
+
SELECT 1 FROM information_schema.columns
|
|
3431
|
+
WHERE table_schema = 'public'
|
|
3432
|
+
AND table_name = 'org_settings'
|
|
3433
|
+
AND column_name = 'dashboard_write_policy'
|
|
3434
|
+
) AS exists
|
|
3435
|
+
`.execute(db)).rows[0]?.exists) return;
|
|
3436
|
+
await sql`
|
|
3437
|
+
ALTER TABLE public.org_settings
|
|
3438
|
+
ADD COLUMN dashboard_write_policy jsonb NOT NULL DEFAULT '{}'::jsonb
|
|
3439
|
+
`.execute(db);
|
|
3440
|
+
}
|
|
3441
|
+
async function down$3(db) {
|
|
3442
|
+
await sql`
|
|
3443
|
+
ALTER TABLE public.org_settings DROP COLUMN IF EXISTS dashboard_write_policy
|
|
3444
|
+
`.execute(db);
|
|
3445
|
+
}
|
|
3446
|
+
//#endregion
|
|
3447
|
+
//#region src/db/migrations/021_check_run_tracking.ts
|
|
3448
|
+
var _021_check_run_tracking_exports = /* @__PURE__ */ __exportAll({
|
|
3449
|
+
down: () => down$2,
|
|
3450
|
+
up: () => up$2
|
|
3451
|
+
});
|
|
3452
|
+
/**
|
|
3453
|
+
* Add `check_run_tracking` table for HA-safe check-run state persistence.
|
|
3454
|
+
*
|
|
3455
|
+
* The `CommitStatusReporter` previously held all of its check-run bookkeeping
|
|
3456
|
+
* in per-process `Map`s. Under HA (Raft leader switch / coord crash) those
|
|
3457
|
+
* maps reset to empty on the replacement coord, leaving GitHub check runs
|
|
3458
|
+
* stuck in `queued` forever.
|
|
3459
|
+
*
|
|
3460
|
+
* Single composite-keyed table per (provider, owner, repo, sha, check_name):
|
|
3461
|
+
*
|
|
3462
|
+
* - `check_run_id` replaces in-memory `checkRunIds`.
|
|
3463
|
+
* - `build_creation_state` replaces `pendingBuildCreations` Promise map.
|
|
3464
|
+
* - `step_progress_json` replaces `stepProgress` array map.
|
|
3465
|
+
* - `in_progress_sent_at` replaces `inProgressSent` boolean map.
|
|
3466
|
+
* - `run_id` indexed; replaces `runIdToKeys` reverse map.
|
|
3467
|
+
*
|
|
3468
|
+
* Progress-timer debounce state (the `progressTimers` Map) is NOT persisted —
|
|
3469
|
+
* timers are recreated on demand when an update arrives and the row's
|
|
3470
|
+
* `updated_at` is older than the debounce window.
|
|
3471
|
+
*
|
|
3472
|
+
* Idempotent: a re-run on a DB that already has the table is a no-op.
|
|
3473
|
+
*/
|
|
3474
|
+
async function up$2(db) {
|
|
3475
|
+
if ((await sql`
|
|
3476
|
+
SELECT EXISTS (
|
|
3477
|
+
SELECT 1 FROM information_schema.tables
|
|
3478
|
+
WHERE table_schema = 'public'
|
|
3479
|
+
AND table_name = 'check_run_tracking'
|
|
3480
|
+
) AS exists
|
|
3481
|
+
`.execute(db)).rows[0]?.exists) return;
|
|
3482
|
+
await sql`
|
|
3483
|
+
CREATE TABLE public.check_run_tracking (
|
|
3484
|
+
provider TEXT NOT NULL,
|
|
3485
|
+
owner TEXT NOT NULL,
|
|
3486
|
+
repo TEXT NOT NULL,
|
|
3487
|
+
sha TEXT NOT NULL,
|
|
3488
|
+
check_name TEXT NOT NULL,
|
|
3489
|
+
check_run_id BIGINT,
|
|
3490
|
+
build_creation_state TEXT,
|
|
3491
|
+
step_progress_json JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
3492
|
+
in_progress_sent_at TIMESTAMPTZ,
|
|
3493
|
+
run_id TEXT,
|
|
3494
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
3495
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
3496
|
+
PRIMARY KEY (provider, owner, repo, sha, check_name)
|
|
3497
|
+
)
|
|
3498
|
+
`.execute(db);
|
|
3499
|
+
await sql`
|
|
3500
|
+
CREATE INDEX idx_check_run_tracking_run_id
|
|
3501
|
+
ON public.check_run_tracking (run_id)
|
|
3502
|
+
WHERE run_id IS NOT NULL
|
|
3503
|
+
`.execute(db);
|
|
3504
|
+
}
|
|
3505
|
+
async function down$2(db) {
|
|
3506
|
+
await sql`DROP TABLE IF EXISTS public.check_run_tracking`.execute(db);
|
|
3507
|
+
}
|
|
3508
|
+
//#endregion
|
|
3509
|
+
//#region src/db/migrations/022_scaler_manager_state.ts
|
|
3510
|
+
var _022_scaler_manager_state_exports = /* @__PURE__ */ __exportAll({
|
|
3511
|
+
down: () => down$1,
|
|
3512
|
+
up: () => up$1
|
|
3513
|
+
});
|
|
3514
|
+
/**
|
|
3515
|
+
* Add three tables persisting `ScalerManager` per-coord state:
|
|
3516
|
+
*
|
|
3517
|
+
* - `scaler_spawning_agents`: agents spawned via Docker/Podman/Firecracker
|
|
3518
|
+
* that have not yet registered via WS. Carries the `bound_job_id` so a
|
|
3519
|
+
* replacement coord still issues the eager-dispatch hop on register.
|
|
3520
|
+
*
|
|
3521
|
+
* - `scaler_agent_jobs`: agentId → (runId, jobId) correlation for
|
|
3522
|
+
* scaler-lifecycle event routing. Inserted on `correlateAgentToJob`,
|
|
3523
|
+
* deleted on disconnect / job completion.
|
|
3524
|
+
*
|
|
3525
|
+
* - `scaler_reservations`: outstanding resource reservations keyed by
|
|
3526
|
+
* agentId. Per-scaler / global usage counters are derived state —
|
|
3527
|
+
* recomputed on coord boot as `SUM(...) GROUP BY scaler_name` so the
|
|
3528
|
+
* cap-check critical section stays correct.
|
|
3529
|
+
*
|
|
3530
|
+
* Without these tables, a coord crash mid-spawn orphans the agent (eager
|
|
3531
|
+
* dispatch lost), strands the reservation (resource leak until backend
|
|
3532
|
+
* GC eventually disconnects the WS, minutes later), and loses every
|
|
3533
|
+
* scaler-lifecycle event emitted before correlation (execution-tracker
|
|
3534
|
+
* sees a hole in the run timeline).
|
|
3535
|
+
*
|
|
3536
|
+
* Idempotent: a re-run on a DB that already has any of these tables
|
|
3537
|
+
* leaves the existing one alone.
|
|
3538
|
+
*/
|
|
3539
|
+
async function up$1(db) {
|
|
3540
|
+
const tableExists = async (name) => {
|
|
3541
|
+
return (await sql`
|
|
3542
|
+
SELECT EXISTS (
|
|
3543
|
+
SELECT 1 FROM information_schema.tables
|
|
3544
|
+
WHERE table_schema = 'public' AND table_name = ${name}
|
|
3545
|
+
) AS exists
|
|
3546
|
+
`.execute(db)).rows[0]?.exists ?? false;
|
|
3547
|
+
};
|
|
3548
|
+
if (!await tableExists("scaler_spawning_agents")) {
|
|
3549
|
+
await sql`
|
|
3550
|
+
CREATE TABLE public.scaler_spawning_agents (
|
|
3551
|
+
agent_id TEXT PRIMARY KEY,
|
|
3552
|
+
scaler_name TEXT NOT NULL,
|
|
3553
|
+
label_set JSONB NOT NULL,
|
|
3554
|
+
run_id TEXT,
|
|
3555
|
+
job_id TEXT,
|
|
3556
|
+
bound_job_id TEXT,
|
|
3557
|
+
spawned_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
3558
|
+
)
|
|
3559
|
+
`.execute(db);
|
|
3560
|
+
await sql`
|
|
3561
|
+
CREATE INDEX idx_scaler_spawning_agents_spawned_at
|
|
3562
|
+
ON public.scaler_spawning_agents (spawned_at)
|
|
3563
|
+
`.execute(db);
|
|
3564
|
+
}
|
|
3565
|
+
if (!await tableExists("scaler_agent_jobs")) await sql`
|
|
3566
|
+
CREATE TABLE public.scaler_agent_jobs (
|
|
3567
|
+
agent_id TEXT PRIMARY KEY,
|
|
3568
|
+
run_id TEXT NOT NULL,
|
|
3569
|
+
job_id TEXT NOT NULL,
|
|
3570
|
+
correlated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
3571
|
+
)
|
|
3572
|
+
`.execute(db);
|
|
3573
|
+
if (!await tableExists("scaler_reservations")) {
|
|
3574
|
+
await sql`
|
|
3575
|
+
CREATE TABLE public.scaler_reservations (
|
|
3576
|
+
agent_id TEXT PRIMARY KEY,
|
|
3577
|
+
scaler_name TEXT NOT NULL,
|
|
3578
|
+
cpu_units DOUBLE PRECISION NOT NULL,
|
|
3579
|
+
mem_bytes BIGINT NOT NULL,
|
|
3580
|
+
reserved_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
3581
|
+
)
|
|
3582
|
+
`.execute(db);
|
|
3583
|
+
await sql`
|
|
3584
|
+
CREATE INDEX idx_scaler_reservations_scaler_name
|
|
3585
|
+
ON public.scaler_reservations (scaler_name)
|
|
3586
|
+
`.execute(db);
|
|
3587
|
+
}
|
|
3588
|
+
}
|
|
3589
|
+
async function down$1(db) {
|
|
3590
|
+
await sql`DROP TABLE IF EXISTS public.scaler_reservations`.execute(db);
|
|
3591
|
+
await sql`DROP TABLE IF EXISTS public.scaler_agent_jobs`.execute(db);
|
|
3592
|
+
await sql`DROP TABLE IF EXISTS public.scaler_spawning_agents`.execute(db);
|
|
3593
|
+
}
|
|
3594
|
+
//#endregion
|
|
3595
|
+
//#region src/db/migrations/023_dispatch_queue_recovery_deadline.ts
|
|
3596
|
+
var _023_dispatch_queue_recovery_deadline_exports = /* @__PURE__ */ __exportAll({
|
|
3597
|
+
down: () => down,
|
|
3598
|
+
up: () => up
|
|
3599
|
+
});
|
|
3600
|
+
/**
|
|
3601
|
+
* Add `dispatch_queue.recovery_deadline TIMESTAMPTZ` and
|
|
3602
|
+
* `dispatch_queue.recovery_agent_id TEXT` columns persisting per-job
|
|
3603
|
+
* recovery state for HA-safe agent-disconnect handling.
|
|
3604
|
+
*
|
|
3605
|
+
* `Dispatcher.recoveringJobs` previously held the timer handle, agent
|
|
3606
|
+
* ID, and deadline entirely in process memory. A coord crash between
|
|
3607
|
+
* starting the timer and it firing dropped the timer; the replacement
|
|
3608
|
+
* coord saw the row as `status='recovering'` but had no record of when
|
|
3609
|
+
* the deadline expired or which agent owned the job. Jobs lingered
|
|
3610
|
+
* forever.
|
|
3222
3611
|
*
|
|
3223
|
-
*
|
|
3224
|
-
*
|
|
3225
|
-
*
|
|
3226
|
-
*
|
|
3612
|
+
* The new columns make the deadline durable. A leader-gated sweep
|
|
3613
|
+
* (`Dispatcher.sweepExpiredRecoveries`) scans
|
|
3614
|
+
* `WHERE status='recovering' AND recovery_deadline < now()` and marks
|
|
3615
|
+
* each row failed; on coord boot, `Dispatcher.recoverState()`
|
|
3616
|
+
* hydrates the in-memory Map by re-creating timers from the persisted
|
|
3617
|
+
* deadlines.
|
|
3618
|
+
*
|
|
3619
|
+
* Idempotent: re-running on a DB that already has either column is a
|
|
3620
|
+
* no-op.
|
|
3227
3621
|
*/
|
|
3228
|
-
const ORG_ID_TABLES = [
|
|
3229
|
-
"access_log",
|
|
3230
|
-
"environment_bindings",
|
|
3231
|
-
"environment_source_overrides",
|
|
3232
|
-
"environment_variables",
|
|
3233
|
-
"environments",
|
|
3234
|
-
"event_log",
|
|
3235
|
-
"held_runs",
|
|
3236
|
-
"scoped_secrets"
|
|
3237
|
-
];
|
|
3238
|
-
const CUSTOMER_ID_TABLES = [
|
|
3239
|
-
"sources",
|
|
3240
|
-
"generic_webhook_sources",
|
|
3241
|
-
"workflow_registrations",
|
|
3242
|
-
"org_settings"
|
|
3243
|
-
];
|
|
3244
3622
|
async function up(db) {
|
|
3245
|
-
|
|
3246
|
-
|
|
3623
|
+
const colExists = async (name) => {
|
|
3624
|
+
return (await sql`
|
|
3625
|
+
SELECT EXISTS (
|
|
3626
|
+
SELECT 1 FROM information_schema.columns
|
|
3627
|
+
WHERE table_schema = 'public'
|
|
3628
|
+
AND table_name = 'dispatch_queue'
|
|
3629
|
+
AND column_name = ${name}
|
|
3630
|
+
) AS exists
|
|
3631
|
+
`.execute(db)).rows[0]?.exists ?? false;
|
|
3632
|
+
};
|
|
3633
|
+
if (!await colExists("recovery_deadline")) await sql`
|
|
3634
|
+
ALTER TABLE public.dispatch_queue
|
|
3635
|
+
ADD COLUMN recovery_deadline TIMESTAMPTZ
|
|
3636
|
+
`.execute(db);
|
|
3637
|
+
if (!await colExists("recovery_agent_id")) await sql`
|
|
3638
|
+
ALTER TABLE public.dispatch_queue
|
|
3639
|
+
ADD COLUMN recovery_agent_id TEXT
|
|
3640
|
+
`.execute(db);
|
|
3641
|
+
await sql`
|
|
3642
|
+
CREATE INDEX IF NOT EXISTS idx_dispatch_queue_recovery_deadline
|
|
3643
|
+
ON public.dispatch_queue (recovery_deadline)
|
|
3644
|
+
WHERE recovery_deadline IS NOT NULL
|
|
3645
|
+
`.execute(db);
|
|
3247
3646
|
}
|
|
3248
3647
|
async function down(db) {
|
|
3249
|
-
|
|
3250
|
-
|
|
3648
|
+
await sql`DROP INDEX IF EXISTS public.idx_dispatch_queue_recovery_deadline`.execute(db);
|
|
3649
|
+
await sql`
|
|
3650
|
+
ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS recovery_agent_id
|
|
3651
|
+
`.execute(db);
|
|
3652
|
+
await sql`
|
|
3653
|
+
ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS recovery_deadline
|
|
3654
|
+
`.execute(db);
|
|
3251
3655
|
}
|
|
3252
3656
|
//#endregion
|
|
3253
3657
|
//#region src/db/migration-provider.ts
|
|
@@ -3271,7 +3675,12 @@ function createMigrationProvider() {
|
|
|
3271
3675
|
"015_org_settings_customer_scoped": _015_org_settings_customer_scoped_exports,
|
|
3272
3676
|
"016_org_settings_allow_http_npm": _016_org_settings_allow_http_npm_exports,
|
|
3273
3677
|
"017_org_id_widen": _017_org_id_widen_exports,
|
|
3274
|
-
"018_org_id_prefix_backfill": _018_org_id_prefix_backfill_exports
|
|
3678
|
+
"018_org_id_prefix_backfill": _018_org_id_prefix_backfill_exports,
|
|
3679
|
+
"019_generic_sources_change_notify": _019_generic_sources_change_notify_exports,
|
|
3680
|
+
"020_org_settings_dashboard_write_policy": _020_org_settings_dashboard_write_policy_exports,
|
|
3681
|
+
"021_check_run_tracking": _021_check_run_tracking_exports,
|
|
3682
|
+
"022_scaler_manager_state": _022_scaler_manager_state_exports,
|
|
3683
|
+
"023_dispatch_queue_recovery_deadline": _023_dispatch_queue_recovery_deadline_exports
|
|
3275
3684
|
};
|
|
3276
3685
|
} };
|
|
3277
3686
|
}
|
|
@@ -3321,12 +3730,15 @@ async function runMigrations(opts) {
|
|
|
3321
3730
|
/**
|
|
3322
3731
|
* Database management commands for kici-admin.
|
|
3323
3732
|
*
|
|
3324
|
-
* db migrate
|
|
3325
|
-
* db fresh
|
|
3326
|
-
* db ensure <name>
|
|
3327
|
-
* db create-role
|
|
3328
|
-
* db create-readonly-user
|
|
3329
|
-
* db check-schema
|
|
3733
|
+
* db migrate Run pending migrations (HTTP — orchestrator must be up)
|
|
3734
|
+
* db fresh DROP + CREATE + migrate + store content hash (direct DB)
|
|
3735
|
+
* db ensure <name> CREATE DATABASE IF NOT EXISTS (direct DB)
|
|
3736
|
+
* db create-role CREATE ROLE LOGIN [CREATEDB] (direct DB)
|
|
3737
|
+
* db create-readonly-user Read-only role + GRANT SELECT (direct DB)
|
|
3738
|
+
* db check-schema Compare bundled migrations vs live schema (direct DB)
|
|
3739
|
+
* db collation-check Check pg_database.datcollversion vs running libc (direct DB)
|
|
3740
|
+
* db reindex REINDEX DATABASE CONCURRENTLY (direct DB)
|
|
3741
|
+
* db refresh-collation-version ALTER DATABASE REFRESH COLLATION VERSION (direct DB)
|
|
3330
3742
|
*
|
|
3331
3743
|
* The direct-DB subcommands cannot go through HTTP because the target database
|
|
3332
3744
|
* may not exist yet or is about to be dropped. They follow the same pattern
|
|
@@ -3410,15 +3822,19 @@ function registerDbCommands(program, getClient) {
|
|
|
3410
3822
|
process.exit(1);
|
|
3411
3823
|
}
|
|
3412
3824
|
});
|
|
3413
|
-
db.command("ensure <name>").description("CREATE DATABASE IF NOT EXISTS (idempotent)").option("--database-url <url>", "Admin DB URL (else KICI_DATABASE_URL / DATABASE_URL)").action(async (name, opts) => {
|
|
3825
|
+
db.command("ensure <name>").description("CREATE DATABASE IF NOT EXISTS (idempotent)").option("--database-url <url>", "Admin DB URL (else KICI_DATABASE_URL / DATABASE_URL)").option("--owner <role>", "DB owner role (default: URL user). Pass when the admin connection is privileged but the new DB should be owned by a separate non-privileged role.").option("--revoke-connect-public", "After ensure, REVOKE CONNECT ON DATABASE \"<name>\" FROM PUBLIC (recommended on shared clusters).").action(async (name, opts) => {
|
|
3414
3826
|
try {
|
|
3415
3827
|
const baseUrl = resolveDatabaseUrl$1(opts.databaseUrl);
|
|
3416
3828
|
const url = new URL(baseUrl);
|
|
3417
3829
|
url.pathname = `/${name}`;
|
|
3418
3830
|
const targetUrl = url.toString();
|
|
3419
3831
|
logInvocation(`ensure ${name}`, targetUrl);
|
|
3420
|
-
const outcome = await ensureDatabase(targetUrl
|
|
3421
|
-
|
|
3832
|
+
const outcome = await ensureDatabase(targetUrl, {
|
|
3833
|
+
owner: opts.owner,
|
|
3834
|
+
revokeConnectFromPublic: !!opts.revokeConnectPublic
|
|
3835
|
+
});
|
|
3836
|
+
const suffix = (opts.owner ? ` (owner=${opts.owner})` : "") + (opts.revokeConnectPublic ? " [revoked CONNECT from PUBLIC]" : "");
|
|
3837
|
+
console.log(`db ensure: ${name} — ${outcome}${suffix}`);
|
|
3422
3838
|
} catch (err) {
|
|
3423
3839
|
console.error(`Error: ${toErrorMessage(err)}`);
|
|
3424
3840
|
process.exit(1);
|
|
@@ -3471,6 +3887,70 @@ function registerDbCommands(program, getClient) {
|
|
|
3471
3887
|
process.exit(1);
|
|
3472
3888
|
}
|
|
3473
3889
|
});
|
|
3890
|
+
db.command("collation-check").description("Compare pg_database.datcollversion against the running libc collation version. Exit 2 on drift.").option("--database-url <url>", "Target DB URL (else KICI_DATABASE_URL / DATABASE_URL)").option("--json", "Emit JSON instead of a human-readable line", false).action(async (opts) => {
|
|
3891
|
+
try {
|
|
3892
|
+
const url = resolveDatabaseUrl$1(opts.databaseUrl);
|
|
3893
|
+
const { dbName } = parseDatabaseUrl(url);
|
|
3894
|
+
const pool = createPool(url);
|
|
3895
|
+
try {
|
|
3896
|
+
const drift = await getDatabaseCollationDrift(pool, dbName);
|
|
3897
|
+
if (drift) {
|
|
3898
|
+
if (opts.json) process.stdout.write(JSON.stringify({
|
|
3899
|
+
clean: false,
|
|
3900
|
+
database: dbName,
|
|
3901
|
+
stamped: drift.stamped,
|
|
3902
|
+
actual: drift.actual
|
|
3903
|
+
}) + "\n");
|
|
3904
|
+
else console.log(`db collation-check: ${dbName} — drift: stamped=${drift.stamped} actual=${drift.actual}`);
|
|
3905
|
+
process.exit(2);
|
|
3906
|
+
}
|
|
3907
|
+
if (opts.json) process.stdout.write(JSON.stringify({
|
|
3908
|
+
clean: true,
|
|
3909
|
+
database: dbName
|
|
3910
|
+
}) + "\n");
|
|
3911
|
+
else console.log(`db collation-check: ${dbName} — clean`);
|
|
3912
|
+
} finally {
|
|
3913
|
+
await pool.end().catch(() => void 0);
|
|
3914
|
+
}
|
|
3915
|
+
} catch (err) {
|
|
3916
|
+
console.error(`Error: ${toErrorMessage(err)}`);
|
|
3917
|
+
process.exit(1);
|
|
3918
|
+
}
|
|
3919
|
+
});
|
|
3920
|
+
db.command("reindex").description("REINDEX DATABASE CONCURRENTLY <db>. Rebuilds every index under the running libc collation rules. Non-blocking but takes minutes + ~2× temp disk.").option("--database-url <url>", "Target DB URL (else KICI_DATABASE_URL / DATABASE_URL)").requiredOption("--confirm", "Explicit confirmation (destructive — long-running)").requiredOption("--reason <text>", "Reason (recorded in stderr banner)").action(async (opts) => {
|
|
3921
|
+
try {
|
|
3922
|
+
const url = resolveDatabaseUrl$1(opts.databaseUrl);
|
|
3923
|
+
const { dbName } = parseDatabaseUrl(url);
|
|
3924
|
+
logInvocation(`reindex ${dbName} (${opts.reason})`, url);
|
|
3925
|
+
const pool = createPool(url);
|
|
3926
|
+
try {
|
|
3927
|
+
await reindexDatabaseConcurrently(pool, dbName);
|
|
3928
|
+
console.log(`db reindex: ${dbName} — REINDEX DATABASE CONCURRENTLY completed`);
|
|
3929
|
+
} finally {
|
|
3930
|
+
await pool.end().catch(() => void 0);
|
|
3931
|
+
}
|
|
3932
|
+
} catch (err) {
|
|
3933
|
+
console.error(`Error: ${toErrorMessage(err)}`);
|
|
3934
|
+
process.exit(1);
|
|
3935
|
+
}
|
|
3936
|
+
});
|
|
3937
|
+
db.command("refresh-collation-version").description("ALTER DATABASE <db> REFRESH COLLATION VERSION. Metadata-only bump; pair with db reindex after a libc-base image rebuild.").option("--database-url <url>", "Target DB URL (else KICI_DATABASE_URL / DATABASE_URL)").requiredOption("--reason <text>", "Reason (recorded in stderr banner)").action(async (opts) => {
|
|
3938
|
+
try {
|
|
3939
|
+
const url = resolveDatabaseUrl$1(opts.databaseUrl);
|
|
3940
|
+
const { dbName } = parseDatabaseUrl(url);
|
|
3941
|
+
logInvocation(`refresh-collation-version ${dbName} (${opts.reason})`, url);
|
|
3942
|
+
const pool = createPool(url);
|
|
3943
|
+
try {
|
|
3944
|
+
await refreshDatabaseCollationVersion(pool, dbName);
|
|
3945
|
+
console.log(`db refresh-collation-version: ${dbName} — ALTER DATABASE REFRESH COLLATION VERSION completed`);
|
|
3946
|
+
} finally {
|
|
3947
|
+
await pool.end().catch(() => void 0);
|
|
3948
|
+
}
|
|
3949
|
+
} catch (err) {
|
|
3950
|
+
console.error(`Error: ${toErrorMessage(err)}`);
|
|
3951
|
+
process.exit(1);
|
|
3952
|
+
}
|
|
3953
|
+
});
|
|
3474
3954
|
}
|
|
3475
3955
|
//#endregion
|
|
3476
3956
|
//#region src/providers/universal-git/config.ts
|
|
@@ -3734,6 +4214,26 @@ createLogger({ prefix: "universal-git:clone-token" });
|
|
|
3734
4214
|
* --stdin Read from stdin
|
|
3735
4215
|
*/
|
|
3736
4216
|
/**
|
|
4217
|
+
* Short reason shown in `(unavailable — <reason>)` when no webhook URL could be
|
|
4218
|
+
* resolved for an added source.
|
|
4219
|
+
*/
|
|
4220
|
+
function webhookNoteReason(note) {
|
|
4221
|
+
switch (note) {
|
|
4222
|
+
case "github-ingress-platform-only": return "this orchestrator runs in independent mode";
|
|
4223
|
+
case "platform-no-public-url": return "the Platform has no public webhook URL configured";
|
|
4224
|
+
case "platform-unavailable": return "could not reach the Platform";
|
|
4225
|
+
default: return "webhook URL could not be determined";
|
|
4226
|
+
}
|
|
4227
|
+
}
|
|
4228
|
+
/** Actionable next-step hint paired with {@link webhookNoteReason}. */
|
|
4229
|
+
function webhookNoteHint(note) {
|
|
4230
|
+
switch (note) {
|
|
4231
|
+
case "github-ingress-platform-only": return "GitHub App ingress is served by the KiCI Platform; find the URL in the dashboard under Sources.";
|
|
4232
|
+
case "platform-unavailable": return "Retry once the orchestrator reconnects, or find the URL in the dashboard under Sources.";
|
|
4233
|
+
default: return "Find the webhook URL in the dashboard under Sources.";
|
|
4234
|
+
}
|
|
4235
|
+
}
|
|
4236
|
+
/**
|
|
3737
4237
|
* Resolve a secret value from various input modes.
|
|
3738
4238
|
* Returns undefined if no input mode is specified.
|
|
3739
4239
|
*/
|
|
@@ -3877,7 +4377,16 @@ function registerSourceCommands(program, getClient) {
|
|
|
3877
4377
|
webhookSecret: opts.webhookSecret
|
|
3878
4378
|
});
|
|
3879
4379
|
if (opts.json) console.log(JSON.stringify(result, null, 2));
|
|
3880
|
-
else
|
|
4380
|
+
else {
|
|
4381
|
+
console.log(`Source added: ${result.routingKey} (${result.name})`);
|
|
4382
|
+
if (result.webhookUrl) {
|
|
4383
|
+
console.log(`Webhook URL: ${result.webhookUrl}`);
|
|
4384
|
+
console.log(` ↳ Paste this into your GitHub App's "Webhook URL" field.`);
|
|
4385
|
+
} else {
|
|
4386
|
+
console.log(`Webhook URL: (unavailable — ${webhookNoteReason(result.webhookNote)})`);
|
|
4387
|
+
console.log(` ↳ ${webhookNoteHint(result.webhookNote)}`);
|
|
4388
|
+
}
|
|
4389
|
+
}
|
|
3881
4390
|
} catch (err) {
|
|
3882
4391
|
console.error(`Error: ${toErrorMessage(err)}`);
|
|
3883
4392
|
process.exit(1);
|
|
@@ -4844,7 +5353,7 @@ function registerBackendCommands(program, getClient) {
|
|
|
4844
5353
|
try {
|
|
4845
5354
|
scopeInfo = ` has ${(await getClient().getBackend(name)).scopeCount ?? 0} discovered scopes -- they will become unavailable.`;
|
|
4846
5355
|
} catch {}
|
|
4847
|
-
if (!await confirm$
|
|
5356
|
+
if (!await confirm$3(`Backend "${name}"${scopeInfo} Continue? [y/N] `)) {
|
|
4848
5357
|
console.log("Aborted.");
|
|
4849
5358
|
return;
|
|
4850
5359
|
}
|
|
@@ -5034,7 +5543,7 @@ function printBackendSummary(b) {
|
|
|
5034
5543
|
/**
|
|
5035
5544
|
* Simple confirmation prompt.
|
|
5036
5545
|
*/
|
|
5037
|
-
async function confirm$
|
|
5546
|
+
async function confirm$3(message) {
|
|
5038
5547
|
const rl = createInterface({
|
|
5039
5548
|
input: process.stdin,
|
|
5040
5549
|
output: process.stderr
|
|
@@ -5594,6 +6103,18 @@ function getCacheDir() {
|
|
|
5594
6103
|
}
|
|
5595
6104
|
var init_platform_detect = __esmMin((() => {}));
|
|
5596
6105
|
//#endregion
|
|
6106
|
+
//#region src/cli/service/privilege.ts
|
|
6107
|
+
init_platform_detect();
|
|
6108
|
+
function resolveUserLevel(opts) {
|
|
6109
|
+
if (opts.system && opts.userLevel) throw new Error("`--system` and `--user-level` are mutually exclusive. Pick one (or neither for auto-detect).");
|
|
6110
|
+
if (opts.system) {
|
|
6111
|
+
if (!isRoot()) throw new Error("`--system` requires root privileges. Re-run under sudo, e.g. `sudo kici-admin orchestrator install --system ...`.");
|
|
6112
|
+
return false;
|
|
6113
|
+
}
|
|
6114
|
+
if (opts.userLevel) return true;
|
|
6115
|
+
return !isRoot();
|
|
6116
|
+
}
|
|
6117
|
+
//#endregion
|
|
5597
6118
|
//#region src/cli/service/systemd.ts
|
|
5598
6119
|
/**
|
|
5599
6120
|
* systemd service manager implementation.
|
|
@@ -5885,26 +6406,79 @@ var init_launchd = __esmMin((() => {
|
|
|
5885
6406
|
lines.push("");
|
|
5886
6407
|
return lines.join("\n");
|
|
5887
6408
|
}
|
|
6409
|
+
/**
|
|
6410
|
+
* Resolve the launchctl domain string for a service config.
|
|
6411
|
+
*
|
|
6412
|
+
* - User-level (LaunchAgent at `~/Library/LaunchAgents/…`) → `gui/<uid>`
|
|
6413
|
+
* - System-level (LaunchDaemon at `/Library/LaunchDaemons/…`) → `system`
|
|
6414
|
+
*
|
|
6415
|
+
* The legacy `launchctl load` / `unload` verbs implicitly inferred the
|
|
6416
|
+
* domain from the plist's filesystem location, but they fail silently on
|
|
6417
|
+
* headless macOS hosts where no GUI session exists (the `gui/<uid>` domain
|
|
6418
|
+
* is not available without a console login). The modern `bootstrap` /
|
|
6419
|
+
* `bootout` / `kickstart` / `kill` verbs take the domain explicitly, so a
|
|
6420
|
+
* system-level LaunchDaemon loads regardless of whether anyone is logged
|
|
6421
|
+
* in at the console — exactly what we need for headless deploy targets.
|
|
6422
|
+
*/
|
|
6423
|
+
domain(config) {
|
|
6424
|
+
return config.isUserLevel ? `gui/${os.userInfo().uid}` : "system";
|
|
6425
|
+
}
|
|
6426
|
+
/** `<domain>/<label>` — the target string for kickstart/kill/print/enable. */
|
|
6427
|
+
domainTarget(config) {
|
|
6428
|
+
return `${this.domain(config)}/${this.label(config)}`;
|
|
6429
|
+
}
|
|
6430
|
+
/** Is the service currently loaded in its target domain? */
|
|
6431
|
+
isLoaded(config) {
|
|
6432
|
+
try {
|
|
6433
|
+
execFileSync("launchctl", ["print", this.domainTarget(config)], { stdio: "ignore" });
|
|
6434
|
+
return true;
|
|
6435
|
+
} catch {
|
|
6436
|
+
return false;
|
|
6437
|
+
}
|
|
6438
|
+
}
|
|
5888
6439
|
async install(config) {
|
|
5889
6440
|
const plistFile = this.plistPath(config);
|
|
5890
6441
|
const plistContent = this.generatePlist(config);
|
|
5891
6442
|
fs.mkdirSync(path.dirname(plistFile), { recursive: true });
|
|
5892
|
-
|
|
6443
|
+
const logDirectory = this.logDir(config);
|
|
6444
|
+
fs.mkdirSync(logDirectory, { recursive: true });
|
|
6445
|
+
if (!config.isUserLevel && config.user) execFileSync("chown", [
|
|
6446
|
+
"-R",
|
|
6447
|
+
`${config.user}:staff`,
|
|
6448
|
+
logDirectory
|
|
6449
|
+
], { stdio: "inherit" });
|
|
5893
6450
|
fs.writeFileSync(plistFile, plistContent, "utf-8");
|
|
5894
|
-
|
|
6451
|
+
if (this.isLoaded(config)) try {
|
|
6452
|
+
execFileSync("launchctl", ["bootout", this.domainTarget(config)], { stdio: "inherit" });
|
|
6453
|
+
} catch {}
|
|
6454
|
+
execFileSync("launchctl", [
|
|
6455
|
+
"bootstrap",
|
|
6456
|
+
this.domain(config),
|
|
6457
|
+
plistFile
|
|
6458
|
+
], { stdio: "inherit" });
|
|
5895
6459
|
}
|
|
5896
6460
|
async uninstall(config) {
|
|
5897
6461
|
const plistFile = this.plistPath(config);
|
|
5898
6462
|
try {
|
|
5899
|
-
execFileSync("launchctl", ["
|
|
6463
|
+
execFileSync("launchctl", ["bootout", this.domainTarget(config)], { stdio: "inherit" });
|
|
6464
|
+
} catch {}
|
|
6465
|
+
try {
|
|
6466
|
+
fs.unlinkSync(plistFile);
|
|
5900
6467
|
} catch {}
|
|
5901
|
-
fs.unlinkSync(plistFile);
|
|
5902
6468
|
}
|
|
5903
6469
|
async start(config) {
|
|
5904
|
-
execFileSync("launchctl", [
|
|
6470
|
+
execFileSync("launchctl", [
|
|
6471
|
+
"kickstart",
|
|
6472
|
+
"-k",
|
|
6473
|
+
this.domainTarget(config)
|
|
6474
|
+
], { stdio: "inherit" });
|
|
5905
6475
|
}
|
|
5906
6476
|
async stop(config) {
|
|
5907
|
-
execFileSync("launchctl", [
|
|
6477
|
+
execFileSync("launchctl", [
|
|
6478
|
+
"kill",
|
|
6479
|
+
"TERM",
|
|
6480
|
+
this.domainTarget(config)
|
|
6481
|
+
], { stdio: "inherit" });
|
|
5908
6482
|
}
|
|
5909
6483
|
async restart(config) {
|
|
5910
6484
|
await this.stop(config);
|
|
@@ -6397,6 +6971,11 @@ function getRestartMode(config) {
|
|
|
6397
6971
|
* Uses a manual YAML builder to avoid adding a YAML library dependency.
|
|
6398
6972
|
* The generated file includes the service definition with env_file,
|
|
6399
6973
|
* restart policy, and volume mounts.
|
|
6974
|
+
*
|
|
6975
|
+
* The `image:` line points at `quay.io/kici-dev/<service-name>:latest`.
|
|
6976
|
+
* Source of truth for the publish targets: packages/ci/src/publish-images.ts.
|
|
6977
|
+
* See .claude/rules/container-publish-registry.md — Quay is the sole
|
|
6978
|
+
* registry for public KiCI images we publish.
|
|
6400
6979
|
*/
|
|
6401
6980
|
function generateComposeYaml(config) {
|
|
6402
6981
|
const restartMode = getRestartMode(config);
|
|
@@ -6406,7 +6985,7 @@ function generateComposeYaml(config) {
|
|
|
6406
6985
|
"",
|
|
6407
6986
|
"services:",
|
|
6408
6987
|
` ${config.name}:`,
|
|
6409
|
-
` image:
|
|
6988
|
+
` image: quay.io/kici-dev/${config.name}:latest`,
|
|
6410
6989
|
` container_name: ${config.name}`,
|
|
6411
6990
|
` restart: ${restartMode}`,
|
|
6412
6991
|
" env_file:",
|
|
@@ -6604,8 +7183,9 @@ var orchestrator_wizard_exports = /* @__PURE__ */ __exportAll({ runOrchestratorW
|
|
|
6604
7183
|
* 2. Database URL
|
|
6605
7184
|
* 3. Port
|
|
6606
7185
|
* 4. Secrets encryption key
|
|
6607
|
-
* 5.
|
|
6608
|
-
* 6.
|
|
7186
|
+
* 5. Bootstrap admin token (for kici-admin authentication)
|
|
7187
|
+
* 6. Platform URL + token (if platform/hybrid mode)
|
|
7188
|
+
* 7. Webhook secret (if hybrid/independent mode)
|
|
6609
7189
|
*/
|
|
6610
7190
|
async function runOrchestratorWizard() {
|
|
6611
7191
|
console.log("");
|
|
@@ -6639,6 +7219,13 @@ async function runOrchestratorWizard() {
|
|
|
6639
7219
|
let secretsKey;
|
|
6640
7220
|
if (useGenerated) secretsKey = generatedKey;
|
|
6641
7221
|
else secretsKey = await promptSecret("Enter custom secrets encryption key (64 hex chars):");
|
|
7222
|
+
console.log("");
|
|
7223
|
+
const generatedAdminToken = randomBytes(32).toString("hex");
|
|
7224
|
+
console.log(`Generated bootstrap admin token: ${generatedAdminToken}`);
|
|
7225
|
+
const useGeneratedAdminToken = await promptConfirm("Use this token?");
|
|
7226
|
+
let bootstrapAdminToken;
|
|
7227
|
+
if (useGeneratedAdminToken) bootstrapAdminToken = generatedAdminToken;
|
|
7228
|
+
else bootstrapAdminToken = await promptSecret("Enter custom bootstrap admin token:");
|
|
6642
7229
|
let platformUrl;
|
|
6643
7230
|
let platformToken;
|
|
6644
7231
|
if (mode === "platform" || mode === "hybrid") {
|
|
@@ -6678,6 +7265,7 @@ async function runOrchestratorWizard() {
|
|
|
6678
7265
|
console.log(` Mode: ${mode}`);
|
|
6679
7266
|
console.log(` Database: ${databaseUrl.replace(/:[^@]*@/, ":***@")}`);
|
|
6680
7267
|
console.log(` Port: ${port}`);
|
|
7268
|
+
console.log(` Admin token: ${bootstrapAdminToken.slice(0, 8)}…`);
|
|
6681
7269
|
console.log(` Platform: ${platformUrl ?? "N/A"}`);
|
|
6682
7270
|
if (source) console.log(` Source: ${source.provider}:${source.appId} (${source.name})`);
|
|
6683
7271
|
console.log("");
|
|
@@ -6686,6 +7274,7 @@ async function runOrchestratorWizard() {
|
|
|
6686
7274
|
databaseUrl,
|
|
6687
7275
|
port,
|
|
6688
7276
|
secretsKey,
|
|
7277
|
+
bootstrapAdminToken,
|
|
6689
7278
|
platformUrl,
|
|
6690
7279
|
platformToken,
|
|
6691
7280
|
source
|
|
@@ -6724,18 +7313,18 @@ function startDevPostgres(containerName) {
|
|
|
6724
7313
|
if (err instanceof Error && err.message.includes("already exists")) throw err;
|
|
6725
7314
|
}
|
|
6726
7315
|
console.log(`Starting dev PostgreSQL container "${containerName}" on port ${port}...`);
|
|
6727
|
-
execSync(`${runtime} run -d --name ${containerName} -p ${port}:5432 -e POSTGRES_PASSWORD=${password} -e POSTGRES_DB=kici postgres:18-
|
|
7316
|
+
execSync(`${runtime} run -d --name ${containerName} -p ${port}:5432 -e POSTGRES_PASSWORD=${password} -e POSTGRES_DB=kici postgres:18-trixie`, { stdio: "inherit" });
|
|
6728
7317
|
return `postgresql://postgres:${password}@localhost:${port}/kici`;
|
|
6729
7318
|
}
|
|
6730
7319
|
function registerOrchestratorInstall(orchestrator) {
|
|
6731
|
-
orchestrator.command("install").description("Install the orchestrator as a system service").option("--platform <type>", "Service platform (systemd, launchd, windows, compose)").option("--env-file <path>", "Path to existing env/config file to use").option("--binary <path>", "Path to orchestrator binary (default: current executable)").option("--dev", "Dev mode: spin up PostgreSQL container on port 15432").option("--wizard", "Interactive wizard for guided setup").option("--name <name>", "Service name", "kici-orchestrator").action(async (opts) => {
|
|
7320
|
+
orchestrator.command("install").description("Install the orchestrator as a system service").option("--platform <type>", "Service platform (systemd, launchd, windows, compose)").option("--env-file <path>", "Path to existing env/config file to use").option("--binary <path>", "Path to orchestrator binary (default: current executable)").option("--dev", "Dev mode: spin up PostgreSQL container on port 15432").option("--wizard", "Interactive wizard for guided setup").option("--name <name>", "Service name", "kici-orchestrator").option("--system", "Install as system-level service (requires root)").option("--user-level", "Install as user-level service (no root required)").option("--user <name>", "Run the service as the named user (system-level launchd only; sets UserName in plist so the daemon drops privileges)").action(async (opts) => {
|
|
6732
7321
|
try {
|
|
6733
7322
|
if (opts.wizard && opts.envFile) {
|
|
6734
7323
|
console.error("Error: Cannot use --wizard with --env-file");
|
|
6735
7324
|
process.exit(1);
|
|
6736
7325
|
}
|
|
6737
7326
|
const platform = detectPlatform(opts.platform);
|
|
6738
|
-
const userLevel =
|
|
7327
|
+
const userLevel = resolveUserLevel(opts);
|
|
6739
7328
|
const serviceName = opts.name;
|
|
6740
7329
|
console.log(`Platform: ${platform}`);
|
|
6741
7330
|
console.log(`Privilege: ${userLevel ? "user" : "system"}`);
|
|
@@ -6755,9 +7344,10 @@ function registerOrchestratorInstall(orchestrator) {
|
|
|
6755
7344
|
const wizardConfig = await runOrchestratorWizard();
|
|
6756
7345
|
let envContent = "# KiCI orchestrator configuration (generated by setup wizard)\n";
|
|
6757
7346
|
envContent += `KICI_MODE=${wizardConfig.mode}\n`;
|
|
6758
|
-
envContent += `
|
|
7347
|
+
envContent += `KICI_DATABASE_URL=${wizardConfig.databaseUrl}\n`;
|
|
6759
7348
|
envContent += `KICI_PORT=${wizardConfig.port}\n`;
|
|
6760
|
-
envContent += `
|
|
7349
|
+
envContent += `KICI_SECRET_KEY=${wizardConfig.secretsKey}\n`;
|
|
7350
|
+
envContent += `KICI_BOOTSTRAP_ADMIN_TOKEN=${wizardConfig.bootstrapAdminToken}\n`;
|
|
6761
7351
|
if (wizardConfig.platformUrl) envContent += `KICI_PLATFORM_URL=${wizardConfig.platformUrl}\n`;
|
|
6762
7352
|
if (wizardConfig.platformToken) envContent += `KICI_PLATFORM_TOKEN=${wizardConfig.platformToken}\n`;
|
|
6763
7353
|
fs.writeFileSync(envFilePath, envContent, "utf-8");
|
|
@@ -6772,12 +7362,12 @@ function registerOrchestratorInstall(orchestrator) {
|
|
|
6772
7362
|
console.log(`Copied env file to ${envFilePath}`);
|
|
6773
7363
|
} else if (!fs.existsSync(envFilePath)) {
|
|
6774
7364
|
let envContent = `# KiCI orchestrator configuration\n# See docs for all available options\n`;
|
|
6775
|
-
if (devDbUrl) envContent += `
|
|
7365
|
+
if (devDbUrl) envContent += `KICI_DATABASE_URL=${devDbUrl}\n`;
|
|
6776
7366
|
fs.writeFileSync(envFilePath, envContent, "utf-8");
|
|
6777
7367
|
console.log(`Created env file at ${envFilePath}`);
|
|
6778
7368
|
} else if (devDbUrl) {
|
|
6779
|
-
fs.appendFileSync(envFilePath, `\
|
|
6780
|
-
console.log(`Appended
|
|
7369
|
+
fs.appendFileSync(envFilePath, `\nKICI_DATABASE_URL=${devDbUrl}\n`);
|
|
7370
|
+
console.log(`Appended KICI_DATABASE_URL to ${envFilePath}`);
|
|
6781
7371
|
}
|
|
6782
7372
|
const executablePath = opts.binary ? path.resolve(opts.binary) : process.argv[0];
|
|
6783
7373
|
if (userLevel) try {
|
|
@@ -6796,6 +7386,7 @@ function registerOrchestratorInstall(orchestrator) {
|
|
|
6796
7386
|
envFilePath,
|
|
6797
7387
|
workingDirectory: configDir,
|
|
6798
7388
|
isUserLevel: userLevel,
|
|
7389
|
+
user: opts.user,
|
|
6799
7390
|
restartPolicy: DEFAULT_RESTART_POLICY
|
|
6800
7391
|
};
|
|
6801
7392
|
await (await createServiceManager(platform)).install(config);
|
|
@@ -6814,10 +7405,10 @@ function registerOrchestratorInstall(orchestrator) {
|
|
|
6814
7405
|
//#endregion
|
|
6815
7406
|
//#region src/cli/commands/orchestrator-service/uninstall.ts
|
|
6816
7407
|
function registerOrchestratorUninstall(orchestrator) {
|
|
6817
|
-
orchestrator.command("uninstall").description("Remove the orchestrator service registration").option("--platform <type>", "Service platform (systemd, launchd, windows, compose)").option("--name <name>", "Service name", "kici-orchestrator").action(async (opts) => {
|
|
7408
|
+
orchestrator.command("uninstall").description("Remove the orchestrator service registration").option("--platform <type>", "Service platform (systemd, launchd, windows, compose)").option("--name <name>", "Service name", "kici-orchestrator").option("--system", "Operate against the system-level service (requires root)").option("--user-level", "Operate against the user-level service").action(async (opts) => {
|
|
6818
7409
|
try {
|
|
6819
7410
|
const platform = detectPlatform(opts.platform);
|
|
6820
|
-
const userLevel =
|
|
7411
|
+
const userLevel = resolveUserLevel(opts);
|
|
6821
7412
|
const serviceName = opts.name;
|
|
6822
7413
|
const configDir = getConfigDir(serviceName, userLevel);
|
|
6823
7414
|
const logDir = getLogDir(serviceName, userLevel);
|
|
@@ -6857,10 +7448,10 @@ function registerOrchestratorUninstall(orchestrator) {
|
|
|
6857
7448
|
//#endregion
|
|
6858
7449
|
//#region src/cli/commands/orchestrator-service/start.ts
|
|
6859
7450
|
function registerOrchestratorStart(orchestrator) {
|
|
6860
|
-
orchestrator.command("start").description("Start the orchestrator service").option("--platform <type>", "Service platform (systemd, launchd, windows, compose)").option("--name <name>", "Service name", "kici-orchestrator").action(async (opts) => {
|
|
7451
|
+
orchestrator.command("start").description("Start the orchestrator service").option("--platform <type>", "Service platform (systemd, launchd, windows, compose)").option("--name <name>", "Service name", "kici-orchestrator").option("--system", "Operate against the system-level service (requires root)").option("--user-level", "Operate against the user-level service").action(async (opts) => {
|
|
6861
7452
|
try {
|
|
6862
7453
|
const platform = detectPlatform(opts.platform);
|
|
6863
|
-
const userLevel =
|
|
7454
|
+
const userLevel = resolveUserLevel(opts);
|
|
6864
7455
|
const serviceName = opts.name;
|
|
6865
7456
|
const configDir = getConfigDir(serviceName, userLevel);
|
|
6866
7457
|
const config = {
|
|
@@ -6890,10 +7481,10 @@ function registerOrchestratorStart(orchestrator) {
|
|
|
6890
7481
|
//#endregion
|
|
6891
7482
|
//#region src/cli/commands/orchestrator-service/stop.ts
|
|
6892
7483
|
function registerOrchestratorStop(orchestrator) {
|
|
6893
|
-
orchestrator.command("stop").description("Stop the orchestrator service").option("--platform <type>", "Service platform (systemd, launchd, windows, compose)").option("--name <name>", "Service name", "kici-orchestrator").action(async (opts) => {
|
|
7484
|
+
orchestrator.command("stop").description("Stop the orchestrator service").option("--platform <type>", "Service platform (systemd, launchd, windows, compose)").option("--name <name>", "Service name", "kici-orchestrator").option("--system", "Operate against the system-level service (requires root)").option("--user-level", "Operate against the user-level service").action(async (opts) => {
|
|
6894
7485
|
try {
|
|
6895
7486
|
const platform = detectPlatform(opts.platform);
|
|
6896
|
-
const userLevel =
|
|
7487
|
+
const userLevel = resolveUserLevel(opts);
|
|
6897
7488
|
const serviceName = opts.name;
|
|
6898
7489
|
const configDir = getConfigDir(serviceName, userLevel);
|
|
6899
7490
|
const config = {
|
|
@@ -6917,10 +7508,10 @@ function registerOrchestratorStop(orchestrator) {
|
|
|
6917
7508
|
//#endregion
|
|
6918
7509
|
//#region src/cli/commands/orchestrator-service/restart.ts
|
|
6919
7510
|
function registerOrchestratorRestart(orchestrator) {
|
|
6920
|
-
orchestrator.command("restart").description("Restart the orchestrator service").option("--platform <type>", "Service platform (systemd, launchd, windows, compose)").option("--name <name>", "Service name", "kici-orchestrator").action(async (opts) => {
|
|
7511
|
+
orchestrator.command("restart").description("Restart the orchestrator service").option("--platform <type>", "Service platform (systemd, launchd, windows, compose)").option("--name <name>", "Service name", "kici-orchestrator").option("--system", "Operate against the system-level service (requires root)").option("--user-level", "Operate against the user-level service").action(async (opts) => {
|
|
6921
7512
|
try {
|
|
6922
7513
|
const platform = detectPlatform(opts.platform);
|
|
6923
|
-
const userLevel =
|
|
7514
|
+
const userLevel = resolveUserLevel(opts);
|
|
6924
7515
|
const serviceName = opts.name;
|
|
6925
7516
|
const configDir = getConfigDir(serviceName, userLevel);
|
|
6926
7517
|
const config = {
|
|
@@ -7106,7 +7697,7 @@ function registerLogsCommand(parent) {
|
|
|
7106
7697
|
* - Optional cleanup of old versions
|
|
7107
7698
|
*/
|
|
7108
7699
|
/** Prompt the user for confirmation (returns true if yes). */
|
|
7109
|
-
async function confirm$
|
|
7700
|
+
async function confirm$2(message) {
|
|
7110
7701
|
const rl = createInterface({
|
|
7111
7702
|
input: process.stdin,
|
|
7112
7703
|
output: process.stdout
|
|
@@ -7322,7 +7913,7 @@ async function performVersionedUpgrade(component, opts) {
|
|
|
7322
7913
|
console.log(` Install path: ${versionedDirPath}`);
|
|
7323
7914
|
console.log(" The service will be stopped during upgrade.");
|
|
7324
7915
|
console.log("");
|
|
7325
|
-
if (!await confirm$
|
|
7916
|
+
if (!await confirm$2("Proceed with upgrade?")) {
|
|
7326
7917
|
console.log("Upgrade cancelled.");
|
|
7327
7918
|
return;
|
|
7328
7919
|
}
|
|
@@ -7404,7 +7995,7 @@ async function handleRollback(component, platform, installBase, config, manager,
|
|
|
7404
7995
|
console.log(` Current version: ${currentVersion}`);
|
|
7405
7996
|
console.log(` Rollback to: ${previousVersion}`);
|
|
7406
7997
|
console.log("");
|
|
7407
|
-
if (!await confirm$
|
|
7998
|
+
if (!await confirm$2("Proceed with rollback?")) {
|
|
7408
7999
|
console.log("Rollback cancelled.");
|
|
7409
8000
|
return;
|
|
7410
8001
|
}
|
|
@@ -7968,30 +8559,39 @@ var JoinTokenManager = class {
|
|
|
7968
8559
|
return token;
|
|
7969
8560
|
}
|
|
7970
8561
|
/**
|
|
7971
|
-
*
|
|
7972
|
-
*
|
|
8562
|
+
* Atomically validate and consume a token in one DB round-trip.
|
|
8563
|
+
*
|
|
8564
|
+
* Single UPDATE with `WHERE token_hash = ? AND consumed_at IS NULL AND
|
|
8565
|
+
* expires_at > NOW()` — only one caller can win the claim across a
|
|
8566
|
+
* shared-DB multi-coordinator mesh. The winner gets `{ routing, keys }`;
|
|
8567
|
+
* every other concurrent caller gets `TOKEN_ALREADY_USED_MESSAGE` and is
|
|
8568
|
+
* expected to fall into the idempotent recovery branch in
|
|
8569
|
+
* `peer-handler.ts` (which serialises credential issuance via the
|
|
8570
|
+
* `peer_credentials_active_uniq` partial unique index).
|
|
8571
|
+
*
|
|
8572
|
+
* On a 0-row claim, a follow-up SELECT disambiguates not-found / expired
|
|
8573
|
+
* / already-used so callers can branch on the specific reason — the
|
|
8574
|
+
* recovery path in peer-handler.ts keys on the "already used" string
|
|
8575
|
+
* specifically. The follow-up only fires on the unhappy path.
|
|
7973
8576
|
*/
|
|
7974
|
-
async
|
|
8577
|
+
async validateAndConsumeToken(token, consumedBy) {
|
|
7975
8578
|
const parsed = parseToken(token);
|
|
7976
8579
|
const keys = deriveKeys(Buffer.from(parsed.secretHex, "hex"));
|
|
8580
|
+
const updateResult = await this.deps.db.updateTable("join_tokens").set({
|
|
8581
|
+
consumed_at: /* @__PURE__ */ new Date(),
|
|
8582
|
+
consumed_by: consumedBy
|
|
8583
|
+
}).where("token_hash", "=", keys.validationHash).where("consumed_at", "is", null).where("expires_at", ">", /* @__PURE__ */ new Date()).executeTakeFirst();
|
|
8584
|
+
if (Number(updateResult?.numUpdatedRows ?? 0n) > 0) {
|
|
8585
|
+
joinTokenLogger.info("Consumed join token", { consumedBy });
|
|
8586
|
+
return {
|
|
8587
|
+
routing: parsed.routing,
|
|
8588
|
+
keys
|
|
8589
|
+
};
|
|
8590
|
+
}
|
|
7977
8591
|
const row = await this.deps.db.selectFrom("join_tokens").selectAll().where("token_hash", "=", keys.validationHash).executeTakeFirst();
|
|
7978
8592
|
if (!row) throw new Error("Invalid join token");
|
|
7979
8593
|
if (row.consumed_at) throw new Error(TOKEN_ALREADY_USED_MESSAGE);
|
|
7980
|
-
|
|
7981
|
-
return {
|
|
7982
|
-
routing: parsed.routing,
|
|
7983
|
-
keys
|
|
7984
|
-
};
|
|
7985
|
-
}
|
|
7986
|
-
/**
|
|
7987
|
-
* Mark token as consumed (one-time use).
|
|
7988
|
-
*/
|
|
7989
|
-
async consumeToken(validationHash, consumedBy) {
|
|
7990
|
-
await this.deps.db.updateTable("join_tokens").set({
|
|
7991
|
-
consumed_at: /* @__PURE__ */ new Date(),
|
|
7992
|
-
consumed_by: consumedBy
|
|
7993
|
-
}).where("token_hash", "=", validationHash).execute();
|
|
7994
|
-
joinTokenLogger.info("Consumed join token", { consumedBy });
|
|
8594
|
+
throw new Error("Join token has expired. Generate a new token with: kici admin create-join-token");
|
|
7995
8595
|
}
|
|
7996
8596
|
};
|
|
7997
8597
|
/**
|
|
@@ -8347,6 +8947,7 @@ function registerOrgSettingsCommands(program, getClient) {
|
|
|
8347
8947
|
registerListMutators(gw, getClient, "allow", "allowedRepos");
|
|
8348
8948
|
registerListMutators(gw, getClient, "deny", "deniedRepos");
|
|
8349
8949
|
registerListMutators(gw, getClient, "elevate", "elevatedRepos");
|
|
8950
|
+
registerDashboardWritesCommands(orgSettings, getClient);
|
|
8350
8951
|
orgSettings.command("allow-http-npm <value>").description("Permit plain http:// npm registry URLs in workflow registries:. Default false; loopback / *.local are always allowed regardless.").option("--customer-id <id>", "Customer / org id (alias: --org)").option("--org <id>", "Alias for --customer-id").option("--format <format>", "Output format: json|table", "table").action(async (value, opts) => {
|
|
8351
8952
|
const allow = value === "true" ? true : value === "false" ? false : void 0;
|
|
8352
8953
|
if (allow === void 0) {
|
|
@@ -8432,6 +9033,173 @@ function label(prefix) {
|
|
|
8432
9033
|
if (prefix === "deny") return "source-repo deny-list";
|
|
8433
9034
|
return "elevated-access list";
|
|
8434
9035
|
}
|
|
9036
|
+
function formatDashboardWrites(response, format, filter) {
|
|
9037
|
+
if (format === "json") return JSON.stringify(response, null, 2);
|
|
9038
|
+
const lines = [];
|
|
9039
|
+
lines.push(`Customer/org id: ${response.customerId}`);
|
|
9040
|
+
lines.push("");
|
|
9041
|
+
const byCategory = /* @__PURE__ */ new Map();
|
|
9042
|
+
for (const descriptor of DASHBOARD_WRITE_OPERATIONS) {
|
|
9043
|
+
if (filter?.category && descriptor.category !== filter.category) continue;
|
|
9044
|
+
if (filter?.sensitivity && descriptor.sensitivity !== filter.sensitivity) continue;
|
|
9045
|
+
const list = byCategory.get(descriptor.category) ?? [];
|
|
9046
|
+
list.push(descriptor);
|
|
9047
|
+
byCategory.set(descriptor.category, list);
|
|
9048
|
+
}
|
|
9049
|
+
for (const [category, descriptors] of byCategory) {
|
|
9050
|
+
lines.push(`${category.toUpperCase()}`);
|
|
9051
|
+
for (const descriptor of descriptors) {
|
|
9052
|
+
const state = response.effective[descriptor.name] ?? true ? "enabled " : "disabled";
|
|
9053
|
+
lines.push(` ${state} ${descriptor.name.padEnd(40)} (${descriptor.cliEquivalent})`);
|
|
9054
|
+
}
|
|
9055
|
+
lines.push("");
|
|
9056
|
+
}
|
|
9057
|
+
return lines.join("\n").trimEnd();
|
|
9058
|
+
}
|
|
9059
|
+
function parseOpFlag(value, previous) {
|
|
9060
|
+
const eq = value.indexOf("=");
|
|
9061
|
+
if (eq < 1 || eq === value.length - 1) {
|
|
9062
|
+
console.error(`Error: --op expects <operation>=<true|false>, got: ${value}`);
|
|
9063
|
+
process.exit(1);
|
|
9064
|
+
}
|
|
9065
|
+
const op = value.slice(0, eq);
|
|
9066
|
+
const bool = value.slice(eq + 1).toLowerCase();
|
|
9067
|
+
if (bool !== "true" && bool !== "false") {
|
|
9068
|
+
console.error(`Error: --op value must be "true" or "false", got: ${bool}`);
|
|
9069
|
+
process.exit(1);
|
|
9070
|
+
}
|
|
9071
|
+
if (!DASHBOARD_WRITE_OPERATIONS_BY_NAME.has(op)) {
|
|
9072
|
+
console.error(`Error: unknown operation "${op}". Run "kici-admin org-settings dashboard-writes show" to list valid operations.`);
|
|
9073
|
+
process.exit(1);
|
|
9074
|
+
}
|
|
9075
|
+
previous.push([op, bool === "true"]);
|
|
9076
|
+
return previous;
|
|
9077
|
+
}
|
|
9078
|
+
function registerDashboardWritesCommands(orgSettings, getClient) {
|
|
9079
|
+
const dw = orgSettings.command("dashboard-writes").description("Manage per-orch dashboard write policy (which Platform-routed dashboard.* writes the orch accepts)");
|
|
9080
|
+
dw.command("show").description("Print current dashboard-write policy. Empty = all enabled.").option("--customer-id <id>", "Customer / org id (alias: --org)").option("--org <id>", "Alias for --customer-id").option("--category <name>", "Filter to one category (Secrets|Variables|Environments|Bindings|\"Held runs\"|DLQ|Registrations|Topology)").option("--sensitivity <name>", "Filter to one sensitivity bucket (plaintext|authority|dispatch)").option("--format <format>", "Output format: json|table", "table").action(async (opts) => {
|
|
9081
|
+
const customerId = resolveCustomerId(opts);
|
|
9082
|
+
try {
|
|
9083
|
+
const response = await getClient().get(`/api/v1/admin/org-settings/dashboard-writes?customerId=${encodeURIComponent(customerId)}`);
|
|
9084
|
+
const filter = parseFilters(opts);
|
|
9085
|
+
console.log(formatDashboardWrites(response, opts.format, filter));
|
|
9086
|
+
} catch (err) {
|
|
9087
|
+
console.error(`Error: ${toErrorMessage(err)}`);
|
|
9088
|
+
process.exit(1);
|
|
9089
|
+
}
|
|
9090
|
+
});
|
|
9091
|
+
dw.command("set").description("Set one or more operations. Use --op <name>=<true|false> per operation. Sugar: --category or --sensitivity + --enabled <bool> expands to the matching operations.").option("--customer-id <id>", "Customer / org id (alias: --org)").option("--org <id>", "Alias for --customer-id").option("--op <op=bool>", "Single operation flip; repeatable (e.g. --op secrets.set=false --op variables.set=false)", parseOpFlag, []).option("--category <name>", "Apply --enabled to every operation in this category").option("--sensitivity <name>", "Apply --enabled to every operation in this sensitivity bucket").option("--enabled <bool>", "Pair with --category or --sensitivity to flip the whole group").option("--format <format>", "Output format: json|table", "table").action(async (opts) => {
|
|
9092
|
+
const customerId = resolveCustomerId(opts);
|
|
9093
|
+
try {
|
|
9094
|
+
const updates = collectUpdates(opts);
|
|
9095
|
+
if (Object.keys(updates).length === 0) {
|
|
9096
|
+
console.error("Error: no operations specified. Pass --op <name>=<bool> or --category/--sensitivity + --enabled.");
|
|
9097
|
+
process.exit(1);
|
|
9098
|
+
}
|
|
9099
|
+
printPlannedChange(updates, await getClient().get(`/api/v1/admin/org-settings/dashboard-writes?customerId=${encodeURIComponent(customerId)}`));
|
|
9100
|
+
const response = await getClient().patch(`/api/v1/admin/org-settings/dashboard-writes`, {
|
|
9101
|
+
customerId,
|
|
9102
|
+
updates
|
|
9103
|
+
});
|
|
9104
|
+
console.log(formatDashboardWrites(response, opts.format));
|
|
9105
|
+
} catch (err) {
|
|
9106
|
+
console.error(`Error: ${toErrorMessage(err)}`);
|
|
9107
|
+
process.exit(1);
|
|
9108
|
+
}
|
|
9109
|
+
});
|
|
9110
|
+
dw.command("reset").description("Reset all operations to enabled (permissive default).").option("--customer-id <id>", "Customer / org id (alias: --org)").option("--org <id>", "Alias for --customer-id").option("--format <format>", "Output format: json|table", "table").action(async (opts) => {
|
|
9111
|
+
const customerId = resolveCustomerId(opts);
|
|
9112
|
+
try {
|
|
9113
|
+
const response = await getClient().patch(`/api/v1/admin/org-settings/dashboard-writes`, {
|
|
9114
|
+
customerId,
|
|
9115
|
+
reset: true
|
|
9116
|
+
});
|
|
9117
|
+
console.log(formatDashboardWrites(response, opts.format));
|
|
9118
|
+
} catch (err) {
|
|
9119
|
+
console.error(`Error: ${toErrorMessage(err)}`);
|
|
9120
|
+
process.exit(1);
|
|
9121
|
+
}
|
|
9122
|
+
});
|
|
9123
|
+
}
|
|
9124
|
+
function parseFilters(opts) {
|
|
9125
|
+
if (!opts.category && !opts.sensitivity) return void 0;
|
|
9126
|
+
const filter = {};
|
|
9127
|
+
if (opts.category) {
|
|
9128
|
+
const parsed = DashboardWriteCategory.safeParse(opts.category);
|
|
9129
|
+
if (!parsed.success) throw new Error(`Unknown --category: ${opts.category}`);
|
|
9130
|
+
filter.category = parsed.data;
|
|
9131
|
+
}
|
|
9132
|
+
if (opts.sensitivity) {
|
|
9133
|
+
const parsed = DashboardWriteSensitivity.safeParse(opts.sensitivity);
|
|
9134
|
+
if (!parsed.success) throw new Error(`Unknown --sensitivity: ${opts.sensitivity}`);
|
|
9135
|
+
filter.sensitivity = parsed.data;
|
|
9136
|
+
}
|
|
9137
|
+
return filter;
|
|
9138
|
+
}
|
|
9139
|
+
function collectUpdates(opts) {
|
|
9140
|
+
const updates = {};
|
|
9141
|
+
for (const [op, value] of opts.op) updates[op] = value;
|
|
9142
|
+
if (Boolean(opts.category || opts.sensitivity)) {
|
|
9143
|
+
if (opts.enabled === void 0) throw new Error("--category / --sensitivity require --enabled <true|false>");
|
|
9144
|
+
const enabled = opts.enabled.toLowerCase();
|
|
9145
|
+
if (enabled !== "true" && enabled !== "false") throw new Error("--enabled must be \"true\" or \"false\"");
|
|
9146
|
+
const value = enabled === "true";
|
|
9147
|
+
const cat = opts.category ? DashboardWriteCategory.parse(opts.category) : void 0;
|
|
9148
|
+
const sens = opts.sensitivity ? DashboardWriteSensitivity.parse(opts.sensitivity) : void 0;
|
|
9149
|
+
for (const descriptor of DASHBOARD_WRITE_OPERATIONS) {
|
|
9150
|
+
if (cat && descriptor.category !== cat) continue;
|
|
9151
|
+
if (sens && descriptor.sensitivity !== sens) continue;
|
|
9152
|
+
updates[descriptor.name] = value;
|
|
9153
|
+
}
|
|
9154
|
+
}
|
|
9155
|
+
return updates;
|
|
9156
|
+
}
|
|
9157
|
+
function printPlannedChange(updates, before) {
|
|
9158
|
+
const lines = ["Planned changes:"];
|
|
9159
|
+
let any = false;
|
|
9160
|
+
for (const [op, next] of Object.entries(updates)) {
|
|
9161
|
+
const prior = before.effective[op] ?? true;
|
|
9162
|
+
if (prior === next) continue;
|
|
9163
|
+
any = true;
|
|
9164
|
+
lines.push(` ${op}: ${prior ? "enabled" : "disabled"} -> ${next ? "enabled" : "disabled"}`);
|
|
9165
|
+
}
|
|
9166
|
+
if (!any) lines.push(" (no effective change)");
|
|
9167
|
+
console.error(lines.join("\n"));
|
|
9168
|
+
}
|
|
9169
|
+
//#endregion
|
|
9170
|
+
//#region src/cli/commands/cluster-name.ts
|
|
9171
|
+
function registerClusterNameCommands(program, getClient) {
|
|
9172
|
+
const cn = program.command("cluster-name").description("Manage this orchestrator's cluster name (Platform-visible identifier)");
|
|
9173
|
+
cn.command("get").description("Print the current cluster name.").option("--format <format>", "Output format: json|table", "table").action(async (opts) => {
|
|
9174
|
+
try {
|
|
9175
|
+
const response = await getClient().get("/api/v1/admin/cluster-name");
|
|
9176
|
+
if (opts.format === "json") {
|
|
9177
|
+
console.log(JSON.stringify(response, null, 2));
|
|
9178
|
+
return;
|
|
9179
|
+
}
|
|
9180
|
+
console.log(`Cluster name: ${response.clusterName}`);
|
|
9181
|
+
console.log(`Looks auto-generated: ${response.looksAutoGenerated ? "yes" : "no"}`);
|
|
9182
|
+
} catch (err) {
|
|
9183
|
+
console.error(`Error: ${toErrorMessage(err)}`);
|
|
9184
|
+
process.exit(1);
|
|
9185
|
+
}
|
|
9186
|
+
});
|
|
9187
|
+
cn.command("set <name>").description(`Rename the cluster. ${CLUSTER_NAME_FORMAT_MESSAGE}.`).option("--format <format>", "Output format: json|table", "table").action(async (name, opts) => {
|
|
9188
|
+
try {
|
|
9189
|
+
const response = await getClient().put("/api/v1/admin/cluster-name", { name });
|
|
9190
|
+
if (opts.format === "json") {
|
|
9191
|
+
console.log(JSON.stringify(response, null, 2));
|
|
9192
|
+
return;
|
|
9193
|
+
}
|
|
9194
|
+
if (response.prior !== null && response.prior !== response.clusterName) console.log(`Renamed: ${response.prior} -> ${response.clusterName}`);
|
|
9195
|
+
else console.log(`Cluster name is already ${response.clusterName} (no change).`);
|
|
9196
|
+
if (response.reconnectRequired) console.log("Reconnect to Platform to publish the new name. Restart the orchestrator or run `kici-admin orchestrator-service restart`.");
|
|
9197
|
+
} catch (err) {
|
|
9198
|
+
console.error(`Error: ${toErrorMessage(err)}`);
|
|
9199
|
+
process.exit(1);
|
|
9200
|
+
}
|
|
9201
|
+
});
|
|
9202
|
+
}
|
|
8435
9203
|
//#endregion
|
|
8436
9204
|
//#region src/cli/commands/maintenance.ts
|
|
8437
9205
|
/**
|
|
@@ -8742,6 +9510,85 @@ function registerEnvironmentCommands(program, getClient) {
|
|
|
8742
9510
|
});
|
|
8743
9511
|
}
|
|
8744
9512
|
//#endregion
|
|
9513
|
+
//#region src/cli/commands/variable.ts
|
|
9514
|
+
function registerVariableCommands(program, getClient) {
|
|
9515
|
+
const vr = program.command("variable").description("Manage environment variables");
|
|
9516
|
+
vr.command("list <orgId> <environment>").description("List org-level variables in an environment").option("--values", "Print variable values inline (default: keys + locked flag only)").action(async (orgId, environment, opts) => {
|
|
9517
|
+
try {
|
|
9518
|
+
const { variables } = await getClient().listVariables(orgId, environment);
|
|
9519
|
+
if (variables.length === 0) {
|
|
9520
|
+
console.log("No variables in this environment.");
|
|
9521
|
+
return;
|
|
9522
|
+
}
|
|
9523
|
+
for (const v of variables) {
|
|
9524
|
+
const lockTag = v.locked ? " [locked]" : "";
|
|
9525
|
+
if (opts.values) console.log(` - ${v.key}=${v.value}${lockTag}`);
|
|
9526
|
+
else console.log(` - ${v.key}${lockTag}`);
|
|
9527
|
+
}
|
|
9528
|
+
} catch (err) {
|
|
9529
|
+
console.error(`Error: ${toErrorMessage(err)}`);
|
|
9530
|
+
process.exit(1);
|
|
9531
|
+
}
|
|
9532
|
+
});
|
|
9533
|
+
vr.command("get <orgId> <environment> <key>").description("Print the value of a single variable").action(async (orgId, environment, key) => {
|
|
9534
|
+
try {
|
|
9535
|
+
const { variables } = await getClient().listVariables(orgId, environment);
|
|
9536
|
+
const match = variables.find((v) => v.key === key);
|
|
9537
|
+
if (!match) {
|
|
9538
|
+
console.error(`Variable '${key}' not found in environment '${environment}'.`);
|
|
9539
|
+
process.exit(1);
|
|
9540
|
+
}
|
|
9541
|
+
console.log(match.value);
|
|
9542
|
+
} catch (err) {
|
|
9543
|
+
console.error(`Error: ${toErrorMessage(err)}`);
|
|
9544
|
+
process.exit(1);
|
|
9545
|
+
}
|
|
9546
|
+
});
|
|
9547
|
+
vr.command("set <orgId> <environment> <key>").description("Set an environment variable. Value comes from one of: --prompt (default on TTY), --from-stdin (default on pipe), --from-file <path>, --from-env <VAR>, --value <plaintext> (discouraged).").option("--value <value>", "Variable value via argv (visible in shell history)").option("--prompt", "Interactive no-echo prompt (requires TTY)").option("--from-stdin", "Read value from piped stdin until EOF").option("--from-file <path>", "Read value from a file (trailing newline trimmed)").option("--from-env <var>", "Read value from a named environment variable").option("--no-trim", "When reading --from-file, keep the trailing newline (default: trim once)").option("--locked", "Mark the variable as locked (source overrides cannot replace it)").option("--confirm-fingerprint <sha256hex>", "Refuse the write unless SHA-256(value) matches this 64-hex string").option("--dry-run", "Parse + validate the value, print fingerprint + length, do not write").action(async (orgId, environment, key, opts) => {
|
|
9548
|
+
try {
|
|
9549
|
+
const { value, source } = await resolveSecretInput(opts);
|
|
9550
|
+
if (opts.dryRun) {
|
|
9551
|
+
console.log(`[dry-run] would set variable '${key}' in environment '${environment}' for org ${orgId} (${value.length} chars, source=${source}, locked=${Boolean(opts.locked)}, sha256=${fingerprintValue(value)})`);
|
|
9552
|
+
return;
|
|
9553
|
+
}
|
|
9554
|
+
await getClient().setVariable(orgId, environment, key, value, opts.locked);
|
|
9555
|
+
const lockTag = opts.locked ? " [locked]" : "";
|
|
9556
|
+
console.log(`Variable '${key}' set in environment '${environment}' for org ${orgId}${lockTag}.`);
|
|
9557
|
+
} catch (err) {
|
|
9558
|
+
console.error(`Error: ${toErrorMessage(err)}`);
|
|
9559
|
+
process.exit(1);
|
|
9560
|
+
}
|
|
9561
|
+
});
|
|
9562
|
+
vr.command("delete <orgId> <environment> <key>").description("Delete an environment variable").option("--yes", "Skip confirmation prompt").action(async (orgId, environment, key, opts) => {
|
|
9563
|
+
try {
|
|
9564
|
+
if (!opts.yes) {
|
|
9565
|
+
if (!await confirm$1(`Are you sure you want to delete variable '${key}' from environment '${environment}'?`)) {
|
|
9566
|
+
console.log("Aborted.");
|
|
9567
|
+
return;
|
|
9568
|
+
}
|
|
9569
|
+
}
|
|
9570
|
+
await getClient().deleteVariable(orgId, environment, key);
|
|
9571
|
+
console.log(`Variable '${key}' deleted from environment '${environment}' for org ${orgId}.`);
|
|
9572
|
+
} catch (err) {
|
|
9573
|
+
console.error(`Error: ${toErrorMessage(err)}`);
|
|
9574
|
+
process.exit(1);
|
|
9575
|
+
}
|
|
9576
|
+
});
|
|
9577
|
+
}
|
|
9578
|
+
async function confirm$1(message) {
|
|
9579
|
+
const { createInterface } = await import("node:readline");
|
|
9580
|
+
const rl = createInterface({
|
|
9581
|
+
input: process.stdin,
|
|
9582
|
+
output: process.stderr
|
|
9583
|
+
});
|
|
9584
|
+
return new Promise((resolve) => {
|
|
9585
|
+
rl.question(`${message} [y/N] `, (answer) => {
|
|
9586
|
+
rl.close();
|
|
9587
|
+
resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
|
9588
|
+
});
|
|
9589
|
+
});
|
|
9590
|
+
}
|
|
9591
|
+
//#endregion
|
|
8745
9592
|
//#region src/cli/commands/queue.ts
|
|
8746
9593
|
function resolveDirectDbUrl$3(explicit) {
|
|
8747
9594
|
return explicit ?? process.env.KICI_DATABASE_URL ?? null;
|
|
@@ -11419,8 +12266,10 @@ registerOrchestratorServiceCommands(program);
|
|
|
11419
12266
|
registerAgentServiceCommands(program);
|
|
11420
12267
|
registerPeerCommands(program, getClient);
|
|
11421
12268
|
registerOrgSettingsCommands(program, getClient);
|
|
12269
|
+
registerClusterNameCommands(program, getClient);
|
|
11422
12270
|
registerMaintenanceCommands(program, getClient);
|
|
11423
12271
|
registerEnvironmentCommands(program, getClient);
|
|
12272
|
+
registerVariableCommands(program, getClient);
|
|
11424
12273
|
registerQueueCommands(program, getClient);
|
|
11425
12274
|
registerExecutionCommands(program, getClient);
|
|
11426
12275
|
registerRegistrationCommands(program, getClient);
|