@kici-dev/orchestrator 0.1.2 → 0.1.5
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/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 +1056 -236
- 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 +6 -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 +7 -0
- package/dist/policy/dashboard-write-policy.d.ts +115 -0
- package/dist/queue/job-queue.d.ts +54 -1
- package/dist/reporting/check-run-tracking-store.d.ts +133 -0
- package/dist/reporting/commit-status.d.ts +87 -9
- 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 +9 -1
- package/dist/routes/admin.d.ts +8 -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 +9846 -6884
- package/dist/stale-detector/stale-run-detector.d.ts +14 -2
- package/dist/standalone.js +6120 -3838
- package/dist/webhook/generic-sources-listener.d.ts +74 -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 +30 -1
- 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,68 +3290,368 @@ 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
|
}
|
|
3296
|
+
async function down$6(db) {
|
|
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);
|
|
3298
|
+
}
|
|
3299
|
+
//#endregion
|
|
3300
|
+
//#region src/db/migrations/018_org_id_prefix_backfill.ts
|
|
3301
|
+
var _018_org_id_prefix_backfill_exports = /* @__PURE__ */ __exportAll({
|
|
3302
|
+
down: () => down$5,
|
|
3303
|
+
up: () => up$5
|
|
3304
|
+
});
|
|
3305
|
+
/**
|
|
3306
|
+
* Prefix every orchestrator-side tenant string with `org_` to align
|
|
3307
|
+
* with Platform's prefixed canonical org ID format (`org_<12-char>`).
|
|
3308
|
+
*
|
|
3309
|
+
* Migration 017 widened the column types but deliberately left rows
|
|
3310
|
+
* un-backfilled, reasoning that pre-migration runtime data could be
|
|
3311
|
+
* orphaned and regenerated on the next deploy. That works for
|
|
3312
|
+
* historical artefacts (execution_runs, event_log, access_log) but
|
|
3313
|
+
* NOT for tenant-state tables the orchestrator needs to recognise the
|
|
3314
|
+
* staging org at routing time (`sources`, `generic_webhook_sources`,
|
|
3315
|
+
* `workflow_registrations`, `org_settings`) and the env-config tables
|
|
3316
|
+
* referenced from runtime requests (`environments`,
|
|
3317
|
+
* `environment_bindings`, `environment_source_overrides`,
|
|
3318
|
+
* `environment_variables`).
|
|
3319
|
+
*
|
|
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
|
+
}
|
|
3192
3589
|
async function down$1(db) {
|
|
3193
|
-
|
|
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);
|
|
3194
3593
|
}
|
|
3195
3594
|
//#endregion
|
|
3196
|
-
//#region src/db/migrations/
|
|
3197
|
-
var
|
|
3595
|
+
//#region src/db/migrations/023_dispatch_queue_recovery_deadline.ts
|
|
3596
|
+
var _023_dispatch_queue_recovery_deadline_exports = /* @__PURE__ */ __exportAll({
|
|
3198
3597
|
down: () => down,
|
|
3199
3598
|
up: () => up
|
|
3200
3599
|
});
|
|
3201
3600
|
/**
|
|
3202
|
-
*
|
|
3203
|
-
*
|
|
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.
|
|
3204
3604
|
*
|
|
3205
|
-
*
|
|
3206
|
-
*
|
|
3207
|
-
*
|
|
3208
|
-
*
|
|
3209
|
-
*
|
|
3210
|
-
*
|
|
3211
|
-
* `workflow_registrations`, `org_settings`) and the env-config tables
|
|
3212
|
-
* referenced from runtime requests (`environments`,
|
|
3213
|
-
* `environment_bindings`, `environment_source_overrides`,
|
|
3214
|
-
* `environment_variables`).
|
|
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.
|
|
3215
3611
|
*
|
|
3216
|
-
*
|
|
3217
|
-
* `
|
|
3218
|
-
*
|
|
3219
|
-
*
|
|
3220
|
-
*
|
|
3221
|
-
*
|
|
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.
|
|
3222
3618
|
*
|
|
3223
|
-
*
|
|
3224
|
-
*
|
|
3225
|
-
* fresh deploy populated some rows already), so applying it twice is
|
|
3226
|
-
* safe.
|
|
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
|
|
@@ -4844,7 +5324,7 @@ function registerBackendCommands(program, getClient) {
|
|
|
4844
5324
|
try {
|
|
4845
5325
|
scopeInfo = ` has ${(await getClient().getBackend(name)).scopeCount ?? 0} discovered scopes -- they will become unavailable.`;
|
|
4846
5326
|
} catch {}
|
|
4847
|
-
if (!await confirm$
|
|
5327
|
+
if (!await confirm$3(`Backend "${name}"${scopeInfo} Continue? [y/N] `)) {
|
|
4848
5328
|
console.log("Aborted.");
|
|
4849
5329
|
return;
|
|
4850
5330
|
}
|
|
@@ -5034,7 +5514,7 @@ function printBackendSummary(b) {
|
|
|
5034
5514
|
/**
|
|
5035
5515
|
* Simple confirmation prompt.
|
|
5036
5516
|
*/
|
|
5037
|
-
async function confirm$
|
|
5517
|
+
async function confirm$3(message) {
|
|
5038
5518
|
const rl = createInterface({
|
|
5039
5519
|
input: process.stdin,
|
|
5040
5520
|
output: process.stderr
|
|
@@ -5594,6 +6074,18 @@ function getCacheDir() {
|
|
|
5594
6074
|
}
|
|
5595
6075
|
var init_platform_detect = __esmMin((() => {}));
|
|
5596
6076
|
//#endregion
|
|
6077
|
+
//#region src/cli/service/privilege.ts
|
|
6078
|
+
init_platform_detect();
|
|
6079
|
+
function resolveUserLevel(opts) {
|
|
6080
|
+
if (opts.system && opts.userLevel) throw new Error("`--system` and `--user-level` are mutually exclusive. Pick one (or neither for auto-detect).");
|
|
6081
|
+
if (opts.system) {
|
|
6082
|
+
if (!isRoot()) throw new Error("`--system` requires root privileges. Re-run under sudo, e.g. `sudo kici-admin orchestrator install --system ...`.");
|
|
6083
|
+
return false;
|
|
6084
|
+
}
|
|
6085
|
+
if (opts.userLevel) return true;
|
|
6086
|
+
return !isRoot();
|
|
6087
|
+
}
|
|
6088
|
+
//#endregion
|
|
5597
6089
|
//#region src/cli/service/systemd.ts
|
|
5598
6090
|
/**
|
|
5599
6091
|
* systemd service manager implementation.
|
|
@@ -5885,26 +6377,79 @@ var init_launchd = __esmMin((() => {
|
|
|
5885
6377
|
lines.push("");
|
|
5886
6378
|
return lines.join("\n");
|
|
5887
6379
|
}
|
|
6380
|
+
/**
|
|
6381
|
+
* Resolve the launchctl domain string for a service config.
|
|
6382
|
+
*
|
|
6383
|
+
* - User-level (LaunchAgent at `~/Library/LaunchAgents/…`) → `gui/<uid>`
|
|
6384
|
+
* - System-level (LaunchDaemon at `/Library/LaunchDaemons/…`) → `system`
|
|
6385
|
+
*
|
|
6386
|
+
* The legacy `launchctl load` / `unload` verbs implicitly inferred the
|
|
6387
|
+
* domain from the plist's filesystem location, but they fail silently on
|
|
6388
|
+
* headless macOS hosts where no GUI session exists (the `gui/<uid>` domain
|
|
6389
|
+
* is not available without a console login). The modern `bootstrap` /
|
|
6390
|
+
* `bootout` / `kickstart` / `kill` verbs take the domain explicitly, so a
|
|
6391
|
+
* system-level LaunchDaemon loads regardless of whether anyone is logged
|
|
6392
|
+
* in at the console — exactly what we need for headless deploy targets.
|
|
6393
|
+
*/
|
|
6394
|
+
domain(config) {
|
|
6395
|
+
return config.isUserLevel ? `gui/${os.userInfo().uid}` : "system";
|
|
6396
|
+
}
|
|
6397
|
+
/** `<domain>/<label>` — the target string for kickstart/kill/print/enable. */
|
|
6398
|
+
domainTarget(config) {
|
|
6399
|
+
return `${this.domain(config)}/${this.label(config)}`;
|
|
6400
|
+
}
|
|
6401
|
+
/** Is the service currently loaded in its target domain? */
|
|
6402
|
+
isLoaded(config) {
|
|
6403
|
+
try {
|
|
6404
|
+
execFileSync("launchctl", ["print", this.domainTarget(config)], { stdio: "ignore" });
|
|
6405
|
+
return true;
|
|
6406
|
+
} catch {
|
|
6407
|
+
return false;
|
|
6408
|
+
}
|
|
6409
|
+
}
|
|
5888
6410
|
async install(config) {
|
|
5889
6411
|
const plistFile = this.plistPath(config);
|
|
5890
6412
|
const plistContent = this.generatePlist(config);
|
|
5891
6413
|
fs.mkdirSync(path.dirname(plistFile), { recursive: true });
|
|
5892
|
-
|
|
6414
|
+
const logDirectory = this.logDir(config);
|
|
6415
|
+
fs.mkdirSync(logDirectory, { recursive: true });
|
|
6416
|
+
if (!config.isUserLevel && config.user) execFileSync("chown", [
|
|
6417
|
+
"-R",
|
|
6418
|
+
`${config.user}:staff`,
|
|
6419
|
+
logDirectory
|
|
6420
|
+
], { stdio: "inherit" });
|
|
5893
6421
|
fs.writeFileSync(plistFile, plistContent, "utf-8");
|
|
5894
|
-
|
|
6422
|
+
if (this.isLoaded(config)) try {
|
|
6423
|
+
execFileSync("launchctl", ["bootout", this.domainTarget(config)], { stdio: "inherit" });
|
|
6424
|
+
} catch {}
|
|
6425
|
+
execFileSync("launchctl", [
|
|
6426
|
+
"bootstrap",
|
|
6427
|
+
this.domain(config),
|
|
6428
|
+
plistFile
|
|
6429
|
+
], { stdio: "inherit" });
|
|
5895
6430
|
}
|
|
5896
6431
|
async uninstall(config) {
|
|
5897
6432
|
const plistFile = this.plistPath(config);
|
|
5898
6433
|
try {
|
|
5899
|
-
execFileSync("launchctl", ["
|
|
6434
|
+
execFileSync("launchctl", ["bootout", this.domainTarget(config)], { stdio: "inherit" });
|
|
6435
|
+
} catch {}
|
|
6436
|
+
try {
|
|
6437
|
+
fs.unlinkSync(plistFile);
|
|
5900
6438
|
} catch {}
|
|
5901
|
-
fs.unlinkSync(plistFile);
|
|
5902
6439
|
}
|
|
5903
6440
|
async start(config) {
|
|
5904
|
-
execFileSync("launchctl", [
|
|
6441
|
+
execFileSync("launchctl", [
|
|
6442
|
+
"kickstart",
|
|
6443
|
+
"-k",
|
|
6444
|
+
this.domainTarget(config)
|
|
6445
|
+
], { stdio: "inherit" });
|
|
5905
6446
|
}
|
|
5906
6447
|
async stop(config) {
|
|
5907
|
-
execFileSync("launchctl", [
|
|
6448
|
+
execFileSync("launchctl", [
|
|
6449
|
+
"kill",
|
|
6450
|
+
"TERM",
|
|
6451
|
+
this.domainTarget(config)
|
|
6452
|
+
], { stdio: "inherit" });
|
|
5908
6453
|
}
|
|
5909
6454
|
async restart(config) {
|
|
5910
6455
|
await this.stop(config);
|
|
@@ -6397,6 +6942,11 @@ function getRestartMode(config) {
|
|
|
6397
6942
|
* Uses a manual YAML builder to avoid adding a YAML library dependency.
|
|
6398
6943
|
* The generated file includes the service definition with env_file,
|
|
6399
6944
|
* restart policy, and volume mounts.
|
|
6945
|
+
*
|
|
6946
|
+
* The `image:` line points at `quay.io/kici-dev/<service-name>:latest`.
|
|
6947
|
+
* Source of truth for the publish targets: packages/ci/src/publish-images.ts.
|
|
6948
|
+
* See .claude/rules/container-publish-registry.md — Quay is the sole
|
|
6949
|
+
* registry for public KiCI images we publish.
|
|
6400
6950
|
*/
|
|
6401
6951
|
function generateComposeYaml(config) {
|
|
6402
6952
|
const restartMode = getRestartMode(config);
|
|
@@ -6406,7 +6956,7 @@ function generateComposeYaml(config) {
|
|
|
6406
6956
|
"",
|
|
6407
6957
|
"services:",
|
|
6408
6958
|
` ${config.name}:`,
|
|
6409
|
-
` image:
|
|
6959
|
+
` image: quay.io/kici-dev/${config.name}:latest`,
|
|
6410
6960
|
` container_name: ${config.name}`,
|
|
6411
6961
|
` restart: ${restartMode}`,
|
|
6412
6962
|
" env_file:",
|
|
@@ -6604,8 +7154,9 @@ var orchestrator_wizard_exports = /* @__PURE__ */ __exportAll({ runOrchestratorW
|
|
|
6604
7154
|
* 2. Database URL
|
|
6605
7155
|
* 3. Port
|
|
6606
7156
|
* 4. Secrets encryption key
|
|
6607
|
-
* 5.
|
|
6608
|
-
* 6.
|
|
7157
|
+
* 5. Bootstrap admin token (for kici-admin authentication)
|
|
7158
|
+
* 6. Platform URL + token (if platform/hybrid mode)
|
|
7159
|
+
* 7. Webhook secret (if hybrid/independent mode)
|
|
6609
7160
|
*/
|
|
6610
7161
|
async function runOrchestratorWizard() {
|
|
6611
7162
|
console.log("");
|
|
@@ -6639,6 +7190,13 @@ async function runOrchestratorWizard() {
|
|
|
6639
7190
|
let secretsKey;
|
|
6640
7191
|
if (useGenerated) secretsKey = generatedKey;
|
|
6641
7192
|
else secretsKey = await promptSecret("Enter custom secrets encryption key (64 hex chars):");
|
|
7193
|
+
console.log("");
|
|
7194
|
+
const generatedAdminToken = randomBytes(32).toString("hex");
|
|
7195
|
+
console.log(`Generated bootstrap admin token: ${generatedAdminToken}`);
|
|
7196
|
+
const useGeneratedAdminToken = await promptConfirm("Use this token?");
|
|
7197
|
+
let bootstrapAdminToken;
|
|
7198
|
+
if (useGeneratedAdminToken) bootstrapAdminToken = generatedAdminToken;
|
|
7199
|
+
else bootstrapAdminToken = await promptSecret("Enter custom bootstrap admin token:");
|
|
6642
7200
|
let platformUrl;
|
|
6643
7201
|
let platformToken;
|
|
6644
7202
|
if (mode === "platform" || mode === "hybrid") {
|
|
@@ -6678,6 +7236,7 @@ async function runOrchestratorWizard() {
|
|
|
6678
7236
|
console.log(` Mode: ${mode}`);
|
|
6679
7237
|
console.log(` Database: ${databaseUrl.replace(/:[^@]*@/, ":***@")}`);
|
|
6680
7238
|
console.log(` Port: ${port}`);
|
|
7239
|
+
console.log(` Admin token: ${bootstrapAdminToken.slice(0, 8)}…`);
|
|
6681
7240
|
console.log(` Platform: ${platformUrl ?? "N/A"}`);
|
|
6682
7241
|
if (source) console.log(` Source: ${source.provider}:${source.appId} (${source.name})`);
|
|
6683
7242
|
console.log("");
|
|
@@ -6686,6 +7245,7 @@ async function runOrchestratorWizard() {
|
|
|
6686
7245
|
databaseUrl,
|
|
6687
7246
|
port,
|
|
6688
7247
|
secretsKey,
|
|
7248
|
+
bootstrapAdminToken,
|
|
6689
7249
|
platformUrl,
|
|
6690
7250
|
platformToken,
|
|
6691
7251
|
source
|
|
@@ -6724,18 +7284,18 @@ function startDevPostgres(containerName) {
|
|
|
6724
7284
|
if (err instanceof Error && err.message.includes("already exists")) throw err;
|
|
6725
7285
|
}
|
|
6726
7286
|
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-
|
|
7287
|
+
execSync(`${runtime} run -d --name ${containerName} -p ${port}:5432 -e POSTGRES_PASSWORD=${password} -e POSTGRES_DB=kici postgres:18-trixie`, { stdio: "inherit" });
|
|
6728
7288
|
return `postgresql://postgres:${password}@localhost:${port}/kici`;
|
|
6729
7289
|
}
|
|
6730
7290
|
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) => {
|
|
7291
|
+
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
7292
|
try {
|
|
6733
7293
|
if (opts.wizard && opts.envFile) {
|
|
6734
7294
|
console.error("Error: Cannot use --wizard with --env-file");
|
|
6735
7295
|
process.exit(1);
|
|
6736
7296
|
}
|
|
6737
7297
|
const platform = detectPlatform(opts.platform);
|
|
6738
|
-
const userLevel =
|
|
7298
|
+
const userLevel = resolveUserLevel(opts);
|
|
6739
7299
|
const serviceName = opts.name;
|
|
6740
7300
|
console.log(`Platform: ${platform}`);
|
|
6741
7301
|
console.log(`Privilege: ${userLevel ? "user" : "system"}`);
|
|
@@ -6755,9 +7315,10 @@ function registerOrchestratorInstall(orchestrator) {
|
|
|
6755
7315
|
const wizardConfig = await runOrchestratorWizard();
|
|
6756
7316
|
let envContent = "# KiCI orchestrator configuration (generated by setup wizard)\n";
|
|
6757
7317
|
envContent += `KICI_MODE=${wizardConfig.mode}\n`;
|
|
6758
|
-
envContent += `
|
|
7318
|
+
envContent += `KICI_DATABASE_URL=${wizardConfig.databaseUrl}\n`;
|
|
6759
7319
|
envContent += `KICI_PORT=${wizardConfig.port}\n`;
|
|
6760
|
-
envContent += `
|
|
7320
|
+
envContent += `KICI_SECRET_KEY=${wizardConfig.secretsKey}\n`;
|
|
7321
|
+
envContent += `KICI_BOOTSTRAP_ADMIN_TOKEN=${wizardConfig.bootstrapAdminToken}\n`;
|
|
6761
7322
|
if (wizardConfig.platformUrl) envContent += `KICI_PLATFORM_URL=${wizardConfig.platformUrl}\n`;
|
|
6762
7323
|
if (wizardConfig.platformToken) envContent += `KICI_PLATFORM_TOKEN=${wizardConfig.platformToken}\n`;
|
|
6763
7324
|
fs.writeFileSync(envFilePath, envContent, "utf-8");
|
|
@@ -6772,12 +7333,12 @@ function registerOrchestratorInstall(orchestrator) {
|
|
|
6772
7333
|
console.log(`Copied env file to ${envFilePath}`);
|
|
6773
7334
|
} else if (!fs.existsSync(envFilePath)) {
|
|
6774
7335
|
let envContent = `# KiCI orchestrator configuration\n# See docs for all available options\n`;
|
|
6775
|
-
if (devDbUrl) envContent += `
|
|
7336
|
+
if (devDbUrl) envContent += `KICI_DATABASE_URL=${devDbUrl}\n`;
|
|
6776
7337
|
fs.writeFileSync(envFilePath, envContent, "utf-8");
|
|
6777
7338
|
console.log(`Created env file at ${envFilePath}`);
|
|
6778
7339
|
} else if (devDbUrl) {
|
|
6779
|
-
fs.appendFileSync(envFilePath, `\
|
|
6780
|
-
console.log(`Appended
|
|
7340
|
+
fs.appendFileSync(envFilePath, `\nKICI_DATABASE_URL=${devDbUrl}\n`);
|
|
7341
|
+
console.log(`Appended KICI_DATABASE_URL to ${envFilePath}`);
|
|
6781
7342
|
}
|
|
6782
7343
|
const executablePath = opts.binary ? path.resolve(opts.binary) : process.argv[0];
|
|
6783
7344
|
if (userLevel) try {
|
|
@@ -6796,6 +7357,7 @@ function registerOrchestratorInstall(orchestrator) {
|
|
|
6796
7357
|
envFilePath,
|
|
6797
7358
|
workingDirectory: configDir,
|
|
6798
7359
|
isUserLevel: userLevel,
|
|
7360
|
+
user: opts.user,
|
|
6799
7361
|
restartPolicy: DEFAULT_RESTART_POLICY
|
|
6800
7362
|
};
|
|
6801
7363
|
await (await createServiceManager(platform)).install(config);
|
|
@@ -6814,10 +7376,10 @@ function registerOrchestratorInstall(orchestrator) {
|
|
|
6814
7376
|
//#endregion
|
|
6815
7377
|
//#region src/cli/commands/orchestrator-service/uninstall.ts
|
|
6816
7378
|
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) => {
|
|
7379
|
+
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
7380
|
try {
|
|
6819
7381
|
const platform = detectPlatform(opts.platform);
|
|
6820
|
-
const userLevel =
|
|
7382
|
+
const userLevel = resolveUserLevel(opts);
|
|
6821
7383
|
const serviceName = opts.name;
|
|
6822
7384
|
const configDir = getConfigDir(serviceName, userLevel);
|
|
6823
7385
|
const logDir = getLogDir(serviceName, userLevel);
|
|
@@ -6857,10 +7419,10 @@ function registerOrchestratorUninstall(orchestrator) {
|
|
|
6857
7419
|
//#endregion
|
|
6858
7420
|
//#region src/cli/commands/orchestrator-service/start.ts
|
|
6859
7421
|
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) => {
|
|
7422
|
+
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
7423
|
try {
|
|
6862
7424
|
const platform = detectPlatform(opts.platform);
|
|
6863
|
-
const userLevel =
|
|
7425
|
+
const userLevel = resolveUserLevel(opts);
|
|
6864
7426
|
const serviceName = opts.name;
|
|
6865
7427
|
const configDir = getConfigDir(serviceName, userLevel);
|
|
6866
7428
|
const config = {
|
|
@@ -6890,10 +7452,10 @@ function registerOrchestratorStart(orchestrator) {
|
|
|
6890
7452
|
//#endregion
|
|
6891
7453
|
//#region src/cli/commands/orchestrator-service/stop.ts
|
|
6892
7454
|
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) => {
|
|
7455
|
+
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
7456
|
try {
|
|
6895
7457
|
const platform = detectPlatform(opts.platform);
|
|
6896
|
-
const userLevel =
|
|
7458
|
+
const userLevel = resolveUserLevel(opts);
|
|
6897
7459
|
const serviceName = opts.name;
|
|
6898
7460
|
const configDir = getConfigDir(serviceName, userLevel);
|
|
6899
7461
|
const config = {
|
|
@@ -6917,10 +7479,10 @@ function registerOrchestratorStop(orchestrator) {
|
|
|
6917
7479
|
//#endregion
|
|
6918
7480
|
//#region src/cli/commands/orchestrator-service/restart.ts
|
|
6919
7481
|
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) => {
|
|
7482
|
+
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
7483
|
try {
|
|
6922
7484
|
const platform = detectPlatform(opts.platform);
|
|
6923
|
-
const userLevel =
|
|
7485
|
+
const userLevel = resolveUserLevel(opts);
|
|
6924
7486
|
const serviceName = opts.name;
|
|
6925
7487
|
const configDir = getConfigDir(serviceName, userLevel);
|
|
6926
7488
|
const config = {
|
|
@@ -7106,7 +7668,7 @@ function registerLogsCommand(parent) {
|
|
|
7106
7668
|
* - Optional cleanup of old versions
|
|
7107
7669
|
*/
|
|
7108
7670
|
/** Prompt the user for confirmation (returns true if yes). */
|
|
7109
|
-
async function confirm$
|
|
7671
|
+
async function confirm$2(message) {
|
|
7110
7672
|
const rl = createInterface({
|
|
7111
7673
|
input: process.stdin,
|
|
7112
7674
|
output: process.stdout
|
|
@@ -7322,7 +7884,7 @@ async function performVersionedUpgrade(component, opts) {
|
|
|
7322
7884
|
console.log(` Install path: ${versionedDirPath}`);
|
|
7323
7885
|
console.log(" The service will be stopped during upgrade.");
|
|
7324
7886
|
console.log("");
|
|
7325
|
-
if (!await confirm$
|
|
7887
|
+
if (!await confirm$2("Proceed with upgrade?")) {
|
|
7326
7888
|
console.log("Upgrade cancelled.");
|
|
7327
7889
|
return;
|
|
7328
7890
|
}
|
|
@@ -7404,7 +7966,7 @@ async function handleRollback(component, platform, installBase, config, manager,
|
|
|
7404
7966
|
console.log(` Current version: ${currentVersion}`);
|
|
7405
7967
|
console.log(` Rollback to: ${previousVersion}`);
|
|
7406
7968
|
console.log("");
|
|
7407
|
-
if (!await confirm$
|
|
7969
|
+
if (!await confirm$2("Proceed with rollback?")) {
|
|
7408
7970
|
console.log("Rollback cancelled.");
|
|
7409
7971
|
return;
|
|
7410
7972
|
}
|
|
@@ -7968,30 +8530,39 @@ var JoinTokenManager = class {
|
|
|
7968
8530
|
return token;
|
|
7969
8531
|
}
|
|
7970
8532
|
/**
|
|
7971
|
-
*
|
|
7972
|
-
*
|
|
8533
|
+
* Atomically validate and consume a token in one DB round-trip.
|
|
8534
|
+
*
|
|
8535
|
+
* Single UPDATE with `WHERE token_hash = ? AND consumed_at IS NULL AND
|
|
8536
|
+
* expires_at > NOW()` — only one caller can win the claim across a
|
|
8537
|
+
* shared-DB multi-coordinator mesh. The winner gets `{ routing, keys }`;
|
|
8538
|
+
* every other concurrent caller gets `TOKEN_ALREADY_USED_MESSAGE` and is
|
|
8539
|
+
* expected to fall into the idempotent recovery branch in
|
|
8540
|
+
* `peer-handler.ts` (which serialises credential issuance via the
|
|
8541
|
+
* `peer_credentials_active_uniq` partial unique index).
|
|
8542
|
+
*
|
|
8543
|
+
* On a 0-row claim, a follow-up SELECT disambiguates not-found / expired
|
|
8544
|
+
* / already-used so callers can branch on the specific reason — the
|
|
8545
|
+
* recovery path in peer-handler.ts keys on the "already used" string
|
|
8546
|
+
* specifically. The follow-up only fires on the unhappy path.
|
|
7973
8547
|
*/
|
|
7974
|
-
async
|
|
8548
|
+
async validateAndConsumeToken(token, consumedBy) {
|
|
7975
8549
|
const parsed = parseToken(token);
|
|
7976
8550
|
const keys = deriveKeys(Buffer.from(parsed.secretHex, "hex"));
|
|
8551
|
+
const updateResult = await this.deps.db.updateTable("join_tokens").set({
|
|
8552
|
+
consumed_at: /* @__PURE__ */ new Date(),
|
|
8553
|
+
consumed_by: consumedBy
|
|
8554
|
+
}).where("token_hash", "=", keys.validationHash).where("consumed_at", "is", null).where("expires_at", ">", /* @__PURE__ */ new Date()).executeTakeFirst();
|
|
8555
|
+
if (Number(updateResult?.numUpdatedRows ?? 0n) > 0) {
|
|
8556
|
+
joinTokenLogger.info("Consumed join token", { consumedBy });
|
|
8557
|
+
return {
|
|
8558
|
+
routing: parsed.routing,
|
|
8559
|
+
keys
|
|
8560
|
+
};
|
|
8561
|
+
}
|
|
7977
8562
|
const row = await this.deps.db.selectFrom("join_tokens").selectAll().where("token_hash", "=", keys.validationHash).executeTakeFirst();
|
|
7978
8563
|
if (!row) throw new Error("Invalid join token");
|
|
7979
8564
|
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 });
|
|
8565
|
+
throw new Error("Join token has expired. Generate a new token with: kici admin create-join-token");
|
|
7995
8566
|
}
|
|
7996
8567
|
};
|
|
7997
8568
|
/**
|
|
@@ -8347,6 +8918,7 @@ function registerOrgSettingsCommands(program, getClient) {
|
|
|
8347
8918
|
registerListMutators(gw, getClient, "allow", "allowedRepos");
|
|
8348
8919
|
registerListMutators(gw, getClient, "deny", "deniedRepos");
|
|
8349
8920
|
registerListMutators(gw, getClient, "elevate", "elevatedRepos");
|
|
8921
|
+
registerDashboardWritesCommands(orgSettings, getClient);
|
|
8350
8922
|
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
8923
|
const allow = value === "true" ? true : value === "false" ? false : void 0;
|
|
8352
8924
|
if (allow === void 0) {
|
|
@@ -8432,6 +9004,173 @@ function label(prefix) {
|
|
|
8432
9004
|
if (prefix === "deny") return "source-repo deny-list";
|
|
8433
9005
|
return "elevated-access list";
|
|
8434
9006
|
}
|
|
9007
|
+
function formatDashboardWrites(response, format, filter) {
|
|
9008
|
+
if (format === "json") return JSON.stringify(response, null, 2);
|
|
9009
|
+
const lines = [];
|
|
9010
|
+
lines.push(`Customer/org id: ${response.customerId}`);
|
|
9011
|
+
lines.push("");
|
|
9012
|
+
const byCategory = /* @__PURE__ */ new Map();
|
|
9013
|
+
for (const descriptor of DASHBOARD_WRITE_OPERATIONS) {
|
|
9014
|
+
if (filter?.category && descriptor.category !== filter.category) continue;
|
|
9015
|
+
if (filter?.sensitivity && descriptor.sensitivity !== filter.sensitivity) continue;
|
|
9016
|
+
const list = byCategory.get(descriptor.category) ?? [];
|
|
9017
|
+
list.push(descriptor);
|
|
9018
|
+
byCategory.set(descriptor.category, list);
|
|
9019
|
+
}
|
|
9020
|
+
for (const [category, descriptors] of byCategory) {
|
|
9021
|
+
lines.push(`${category.toUpperCase()}`);
|
|
9022
|
+
for (const descriptor of descriptors) {
|
|
9023
|
+
const state = response.effective[descriptor.name] ?? true ? "enabled " : "disabled";
|
|
9024
|
+
lines.push(` ${state} ${descriptor.name.padEnd(40)} (${descriptor.cliEquivalent})`);
|
|
9025
|
+
}
|
|
9026
|
+
lines.push("");
|
|
9027
|
+
}
|
|
9028
|
+
return lines.join("\n").trimEnd();
|
|
9029
|
+
}
|
|
9030
|
+
function parseOpFlag(value, previous) {
|
|
9031
|
+
const eq = value.indexOf("=");
|
|
9032
|
+
if (eq < 1 || eq === value.length - 1) {
|
|
9033
|
+
console.error(`Error: --op expects <operation>=<true|false>, got: ${value}`);
|
|
9034
|
+
process.exit(1);
|
|
9035
|
+
}
|
|
9036
|
+
const op = value.slice(0, eq);
|
|
9037
|
+
const bool = value.slice(eq + 1).toLowerCase();
|
|
9038
|
+
if (bool !== "true" && bool !== "false") {
|
|
9039
|
+
console.error(`Error: --op value must be "true" or "false", got: ${bool}`);
|
|
9040
|
+
process.exit(1);
|
|
9041
|
+
}
|
|
9042
|
+
if (!DASHBOARD_WRITE_OPERATIONS_BY_NAME.has(op)) {
|
|
9043
|
+
console.error(`Error: unknown operation "${op}". Run "kici-admin org-settings dashboard-writes show" to list valid operations.`);
|
|
9044
|
+
process.exit(1);
|
|
9045
|
+
}
|
|
9046
|
+
previous.push([op, bool === "true"]);
|
|
9047
|
+
return previous;
|
|
9048
|
+
}
|
|
9049
|
+
function registerDashboardWritesCommands(orgSettings, getClient) {
|
|
9050
|
+
const dw = orgSettings.command("dashboard-writes").description("Manage per-orch dashboard write policy (which Platform-routed dashboard.* writes the orch accepts)");
|
|
9051
|
+
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) => {
|
|
9052
|
+
const customerId = resolveCustomerId(opts);
|
|
9053
|
+
try {
|
|
9054
|
+
const response = await getClient().get(`/api/v1/admin/org-settings/dashboard-writes?customerId=${encodeURIComponent(customerId)}`);
|
|
9055
|
+
const filter = parseFilters(opts);
|
|
9056
|
+
console.log(formatDashboardWrites(response, opts.format, filter));
|
|
9057
|
+
} catch (err) {
|
|
9058
|
+
console.error(`Error: ${toErrorMessage(err)}`);
|
|
9059
|
+
process.exit(1);
|
|
9060
|
+
}
|
|
9061
|
+
});
|
|
9062
|
+
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) => {
|
|
9063
|
+
const customerId = resolveCustomerId(opts);
|
|
9064
|
+
try {
|
|
9065
|
+
const updates = collectUpdates(opts);
|
|
9066
|
+
if (Object.keys(updates).length === 0) {
|
|
9067
|
+
console.error("Error: no operations specified. Pass --op <name>=<bool> or --category/--sensitivity + --enabled.");
|
|
9068
|
+
process.exit(1);
|
|
9069
|
+
}
|
|
9070
|
+
printPlannedChange(updates, await getClient().get(`/api/v1/admin/org-settings/dashboard-writes?customerId=${encodeURIComponent(customerId)}`));
|
|
9071
|
+
const response = await getClient().patch(`/api/v1/admin/org-settings/dashboard-writes`, {
|
|
9072
|
+
customerId,
|
|
9073
|
+
updates
|
|
9074
|
+
});
|
|
9075
|
+
console.log(formatDashboardWrites(response, opts.format));
|
|
9076
|
+
} catch (err) {
|
|
9077
|
+
console.error(`Error: ${toErrorMessage(err)}`);
|
|
9078
|
+
process.exit(1);
|
|
9079
|
+
}
|
|
9080
|
+
});
|
|
9081
|
+
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) => {
|
|
9082
|
+
const customerId = resolveCustomerId(opts);
|
|
9083
|
+
try {
|
|
9084
|
+
const response = await getClient().patch(`/api/v1/admin/org-settings/dashboard-writes`, {
|
|
9085
|
+
customerId,
|
|
9086
|
+
reset: true
|
|
9087
|
+
});
|
|
9088
|
+
console.log(formatDashboardWrites(response, opts.format));
|
|
9089
|
+
} catch (err) {
|
|
9090
|
+
console.error(`Error: ${toErrorMessage(err)}`);
|
|
9091
|
+
process.exit(1);
|
|
9092
|
+
}
|
|
9093
|
+
});
|
|
9094
|
+
}
|
|
9095
|
+
function parseFilters(opts) {
|
|
9096
|
+
if (!opts.category && !opts.sensitivity) return void 0;
|
|
9097
|
+
const filter = {};
|
|
9098
|
+
if (opts.category) {
|
|
9099
|
+
const parsed = DashboardWriteCategory.safeParse(opts.category);
|
|
9100
|
+
if (!parsed.success) throw new Error(`Unknown --category: ${opts.category}`);
|
|
9101
|
+
filter.category = parsed.data;
|
|
9102
|
+
}
|
|
9103
|
+
if (opts.sensitivity) {
|
|
9104
|
+
const parsed = DashboardWriteSensitivity.safeParse(opts.sensitivity);
|
|
9105
|
+
if (!parsed.success) throw new Error(`Unknown --sensitivity: ${opts.sensitivity}`);
|
|
9106
|
+
filter.sensitivity = parsed.data;
|
|
9107
|
+
}
|
|
9108
|
+
return filter;
|
|
9109
|
+
}
|
|
9110
|
+
function collectUpdates(opts) {
|
|
9111
|
+
const updates = {};
|
|
9112
|
+
for (const [op, value] of opts.op) updates[op] = value;
|
|
9113
|
+
if (Boolean(opts.category || opts.sensitivity)) {
|
|
9114
|
+
if (opts.enabled === void 0) throw new Error("--category / --sensitivity require --enabled <true|false>");
|
|
9115
|
+
const enabled = opts.enabled.toLowerCase();
|
|
9116
|
+
if (enabled !== "true" && enabled !== "false") throw new Error("--enabled must be \"true\" or \"false\"");
|
|
9117
|
+
const value = enabled === "true";
|
|
9118
|
+
const cat = opts.category ? DashboardWriteCategory.parse(opts.category) : void 0;
|
|
9119
|
+
const sens = opts.sensitivity ? DashboardWriteSensitivity.parse(opts.sensitivity) : void 0;
|
|
9120
|
+
for (const descriptor of DASHBOARD_WRITE_OPERATIONS) {
|
|
9121
|
+
if (cat && descriptor.category !== cat) continue;
|
|
9122
|
+
if (sens && descriptor.sensitivity !== sens) continue;
|
|
9123
|
+
updates[descriptor.name] = value;
|
|
9124
|
+
}
|
|
9125
|
+
}
|
|
9126
|
+
return updates;
|
|
9127
|
+
}
|
|
9128
|
+
function printPlannedChange(updates, before) {
|
|
9129
|
+
const lines = ["Planned changes:"];
|
|
9130
|
+
let any = false;
|
|
9131
|
+
for (const [op, next] of Object.entries(updates)) {
|
|
9132
|
+
const prior = before.effective[op] ?? true;
|
|
9133
|
+
if (prior === next) continue;
|
|
9134
|
+
any = true;
|
|
9135
|
+
lines.push(` ${op}: ${prior ? "enabled" : "disabled"} -> ${next ? "enabled" : "disabled"}`);
|
|
9136
|
+
}
|
|
9137
|
+
if (!any) lines.push(" (no effective change)");
|
|
9138
|
+
console.error(lines.join("\n"));
|
|
9139
|
+
}
|
|
9140
|
+
//#endregion
|
|
9141
|
+
//#region src/cli/commands/cluster-name.ts
|
|
9142
|
+
function registerClusterNameCommands(program, getClient) {
|
|
9143
|
+
const cn = program.command("cluster-name").description("Manage this orchestrator's cluster name (Platform-visible identifier)");
|
|
9144
|
+
cn.command("get").description("Print the current cluster name.").option("--format <format>", "Output format: json|table", "table").action(async (opts) => {
|
|
9145
|
+
try {
|
|
9146
|
+
const response = await getClient().get("/api/v1/admin/cluster-name");
|
|
9147
|
+
if (opts.format === "json") {
|
|
9148
|
+
console.log(JSON.stringify(response, null, 2));
|
|
9149
|
+
return;
|
|
9150
|
+
}
|
|
9151
|
+
console.log(`Cluster name: ${response.clusterName}`);
|
|
9152
|
+
console.log(`Looks auto-generated: ${response.looksAutoGenerated ? "yes" : "no"}`);
|
|
9153
|
+
} catch (err) {
|
|
9154
|
+
console.error(`Error: ${toErrorMessage(err)}`);
|
|
9155
|
+
process.exit(1);
|
|
9156
|
+
}
|
|
9157
|
+
});
|
|
9158
|
+
cn.command("set <name>").description(`Rename the cluster. ${CLUSTER_NAME_FORMAT_MESSAGE}.`).option("--format <format>", "Output format: json|table", "table").action(async (name, opts) => {
|
|
9159
|
+
try {
|
|
9160
|
+
const response = await getClient().put("/api/v1/admin/cluster-name", { name });
|
|
9161
|
+
if (opts.format === "json") {
|
|
9162
|
+
console.log(JSON.stringify(response, null, 2));
|
|
9163
|
+
return;
|
|
9164
|
+
}
|
|
9165
|
+
if (response.prior !== null && response.prior !== response.clusterName) console.log(`Renamed: ${response.prior} -> ${response.clusterName}`);
|
|
9166
|
+
else console.log(`Cluster name is already ${response.clusterName} (no change).`);
|
|
9167
|
+
if (response.reconnectRequired) console.log("Reconnect to Platform to publish the new name. Restart the orchestrator or run `kici-admin orchestrator-service restart`.");
|
|
9168
|
+
} catch (err) {
|
|
9169
|
+
console.error(`Error: ${toErrorMessage(err)}`);
|
|
9170
|
+
process.exit(1);
|
|
9171
|
+
}
|
|
9172
|
+
});
|
|
9173
|
+
}
|
|
8435
9174
|
//#endregion
|
|
8436
9175
|
//#region src/cli/commands/maintenance.ts
|
|
8437
9176
|
/**
|
|
@@ -8742,6 +9481,85 @@ function registerEnvironmentCommands(program, getClient) {
|
|
|
8742
9481
|
});
|
|
8743
9482
|
}
|
|
8744
9483
|
//#endregion
|
|
9484
|
+
//#region src/cli/commands/variable.ts
|
|
9485
|
+
function registerVariableCommands(program, getClient) {
|
|
9486
|
+
const vr = program.command("variable").description("Manage environment variables");
|
|
9487
|
+
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) => {
|
|
9488
|
+
try {
|
|
9489
|
+
const { variables } = await getClient().listVariables(orgId, environment);
|
|
9490
|
+
if (variables.length === 0) {
|
|
9491
|
+
console.log("No variables in this environment.");
|
|
9492
|
+
return;
|
|
9493
|
+
}
|
|
9494
|
+
for (const v of variables) {
|
|
9495
|
+
const lockTag = v.locked ? " [locked]" : "";
|
|
9496
|
+
if (opts.values) console.log(` - ${v.key}=${v.value}${lockTag}`);
|
|
9497
|
+
else console.log(` - ${v.key}${lockTag}`);
|
|
9498
|
+
}
|
|
9499
|
+
} catch (err) {
|
|
9500
|
+
console.error(`Error: ${toErrorMessage(err)}`);
|
|
9501
|
+
process.exit(1);
|
|
9502
|
+
}
|
|
9503
|
+
});
|
|
9504
|
+
vr.command("get <orgId> <environment> <key>").description("Print the value of a single variable").action(async (orgId, environment, key) => {
|
|
9505
|
+
try {
|
|
9506
|
+
const { variables } = await getClient().listVariables(orgId, environment);
|
|
9507
|
+
const match = variables.find((v) => v.key === key);
|
|
9508
|
+
if (!match) {
|
|
9509
|
+
console.error(`Variable '${key}' not found in environment '${environment}'.`);
|
|
9510
|
+
process.exit(1);
|
|
9511
|
+
}
|
|
9512
|
+
console.log(match.value);
|
|
9513
|
+
} catch (err) {
|
|
9514
|
+
console.error(`Error: ${toErrorMessage(err)}`);
|
|
9515
|
+
process.exit(1);
|
|
9516
|
+
}
|
|
9517
|
+
});
|
|
9518
|
+
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) => {
|
|
9519
|
+
try {
|
|
9520
|
+
const { value, source } = await resolveSecretInput(opts);
|
|
9521
|
+
if (opts.dryRun) {
|
|
9522
|
+
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)})`);
|
|
9523
|
+
return;
|
|
9524
|
+
}
|
|
9525
|
+
await getClient().setVariable(orgId, environment, key, value, opts.locked);
|
|
9526
|
+
const lockTag = opts.locked ? " [locked]" : "";
|
|
9527
|
+
console.log(`Variable '${key}' set in environment '${environment}' for org ${orgId}${lockTag}.`);
|
|
9528
|
+
} catch (err) {
|
|
9529
|
+
console.error(`Error: ${toErrorMessage(err)}`);
|
|
9530
|
+
process.exit(1);
|
|
9531
|
+
}
|
|
9532
|
+
});
|
|
9533
|
+
vr.command("delete <orgId> <environment> <key>").description("Delete an environment variable").option("--yes", "Skip confirmation prompt").action(async (orgId, environment, key, opts) => {
|
|
9534
|
+
try {
|
|
9535
|
+
if (!opts.yes) {
|
|
9536
|
+
if (!await confirm$1(`Are you sure you want to delete variable '${key}' from environment '${environment}'?`)) {
|
|
9537
|
+
console.log("Aborted.");
|
|
9538
|
+
return;
|
|
9539
|
+
}
|
|
9540
|
+
}
|
|
9541
|
+
await getClient().deleteVariable(orgId, environment, key);
|
|
9542
|
+
console.log(`Variable '${key}' deleted from environment '${environment}' for org ${orgId}.`);
|
|
9543
|
+
} catch (err) {
|
|
9544
|
+
console.error(`Error: ${toErrorMessage(err)}`);
|
|
9545
|
+
process.exit(1);
|
|
9546
|
+
}
|
|
9547
|
+
});
|
|
9548
|
+
}
|
|
9549
|
+
async function confirm$1(message) {
|
|
9550
|
+
const { createInterface } = await import("node:readline");
|
|
9551
|
+
const rl = createInterface({
|
|
9552
|
+
input: process.stdin,
|
|
9553
|
+
output: process.stderr
|
|
9554
|
+
});
|
|
9555
|
+
return new Promise((resolve) => {
|
|
9556
|
+
rl.question(`${message} [y/N] `, (answer) => {
|
|
9557
|
+
rl.close();
|
|
9558
|
+
resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
|
9559
|
+
});
|
|
9560
|
+
});
|
|
9561
|
+
}
|
|
9562
|
+
//#endregion
|
|
8745
9563
|
//#region src/cli/commands/queue.ts
|
|
8746
9564
|
function resolveDirectDbUrl$3(explicit) {
|
|
8747
9565
|
return explicit ?? process.env.KICI_DATABASE_URL ?? null;
|
|
@@ -11419,8 +12237,10 @@ registerOrchestratorServiceCommands(program);
|
|
|
11419
12237
|
registerAgentServiceCommands(program);
|
|
11420
12238
|
registerPeerCommands(program, getClient);
|
|
11421
12239
|
registerOrgSettingsCommands(program, getClient);
|
|
12240
|
+
registerClusterNameCommands(program, getClient);
|
|
11422
12241
|
registerMaintenanceCommands(program, getClient);
|
|
11423
12242
|
registerEnvironmentCommands(program, getClient);
|
|
12243
|
+
registerVariableCommands(program, getClient);
|
|
11424
12244
|
registerQueueCommands(program, getClient);
|
|
11425
12245
|
registerExecutionCommands(program, getClient);
|
|
11426
12246
|
registerRegistrationCommands(program, getClient);
|