@pleri/olam-cli 0.1.152 → 0.1.157
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/commands/bootstrap.d.ts +2 -1
- package/dist/commands/bootstrap.d.ts.map +1 -1
- package/dist/commands/bootstrap.js +8 -10
- package/dist/commands/bootstrap.js.map +1 -1
- package/dist/commands/doctor.d.ts +46 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +146 -8
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/flywheel/check-persona-skeleton.d.ts +7 -0
- package/dist/commands/flywheel/check-persona-skeleton.d.ts.map +1 -0
- package/dist/commands/flywheel/check-persona-skeleton.js +14 -0
- package/dist/commands/flywheel/check-persona-skeleton.js.map +1 -0
- package/dist/commands/flywheel/diversity-check.d.ts +7 -0
- package/dist/commands/flywheel/diversity-check.d.ts.map +1 -0
- package/dist/commands/flywheel/diversity-check.js +14 -0
- package/dist/commands/flywheel/diversity-check.js.map +1 -0
- package/dist/commands/flywheel/emit-breadcrumb.d.ts +20 -0
- package/dist/commands/flywheel/emit-breadcrumb.d.ts.map +1 -0
- package/dist/commands/flywheel/emit-breadcrumb.js +137 -0
- package/dist/commands/flywheel/emit-breadcrumb.js.map +1 -0
- package/dist/commands/flywheel/index.d.ts +27 -0
- package/dist/commands/flywheel/index.d.ts.map +1 -0
- package/dist/commands/flywheel/index.js +48 -0
- package/dist/commands/flywheel/index.js.map +1 -0
- package/dist/commands/flywheel/install-shims.d.ts +8 -0
- package/dist/commands/flywheel/install-shims.d.ts.map +1 -0
- package/dist/commands/flywheel/install-shims.js +15 -0
- package/dist/commands/flywheel/install-shims.js.map +1 -0
- package/dist/commands/flywheel/k10-measure.d.ts +7 -0
- package/dist/commands/flywheel/k10-measure.d.ts.map +1 -0
- package/dist/commands/flywheel/k10-measure.js +14 -0
- package/dist/commands/flywheel/k10-measure.js.map +1 -0
- package/dist/commands/flywheel/k5-score.d.ts +14 -0
- package/dist/commands/flywheel/k5-score.d.ts.map +1 -0
- package/dist/commands/flywheel/k5-score.js +59 -0
- package/dist/commands/flywheel/k5-score.js.map +1 -0
- package/dist/commands/flywheel/k5-validate.d.ts +15 -0
- package/dist/commands/flywheel/k5-validate.d.ts.map +1 -0
- package/dist/commands/flywheel/k5-validate.js +185 -0
- package/dist/commands/flywheel/k5-validate.js.map +1 -0
- package/dist/commands/flywheel/ping.d.ts +21 -0
- package/dist/commands/flywheel/ping.d.ts.map +1 -0
- package/dist/commands/flywheel/ping.js +79 -0
- package/dist/commands/flywheel/ping.js.map +1 -0
- package/dist/commands/flywheel/sanitize-persona-output.d.ts +7 -0
- package/dist/commands/flywheel/sanitize-persona-output.d.ts.map +1 -0
- package/dist/commands/flywheel/sanitize-persona-output.js +14 -0
- package/dist/commands/flywheel/sanitize-persona-output.js.map +1 -0
- package/dist/commands/hermes-kg-hook.d.ts +36 -0
- package/dist/commands/hermes-kg-hook.d.ts.map +1 -0
- package/dist/commands/hermes-kg-hook.js +80 -0
- package/dist/commands/hermes-kg-hook.js.map +1 -0
- package/dist/commands/hermes.d.ts +46 -0
- package/dist/commands/hermes.d.ts.map +1 -0
- package/dist/commands/hermes.js +320 -0
- package/dist/commands/hermes.js.map +1 -0
- package/dist/commands/host-cp.d.ts.map +1 -1
- package/dist/commands/host-cp.js +17 -0
- package/dist/commands/host-cp.js.map +1 -1
- package/dist/commands/kg-install-hook.d.ts +7 -1
- package/dist/commands/kg-install-hook.d.ts.map +1 -1
- package/dist/commands/kg-install-hook.js +122 -6
- package/dist/commands/kg-install-hook.js.map +1 -1
- package/dist/commands/memory/_paths.d.ts +13 -3
- package/dist/commands/memory/_paths.d.ts.map +1 -1
- package/dist/commands/memory/_paths.js +25 -22
- package/dist/commands/memory/_paths.js.map +1 -1
- package/dist/commands/memory/logs.d.ts +8 -4
- package/dist/commands/memory/logs.d.ts.map +1 -1
- package/dist/commands/memory/logs.js +18 -13
- package/dist/commands/memory/logs.js.map +1 -1
- package/dist/commands/memory/mode.d.ts.map +1 -1
- package/dist/commands/memory/mode.js +7 -3
- package/dist/commands/memory/mode.js.map +1 -1
- package/dist/commands/memory/start.d.ts +16 -14
- package/dist/commands/memory/start.d.ts.map +1 -1
- package/dist/commands/memory/start.js +55 -189
- package/dist/commands/memory/start.js.map +1 -1
- package/dist/commands/memory/status.d.ts +10 -8
- package/dist/commands/memory/status.d.ts.map +1 -1
- package/dist/commands/memory/status.js +35 -38
- package/dist/commands/memory/status.js.map +1 -1
- package/dist/commands/memory/stop.d.ts +5 -4
- package/dist/commands/memory/stop.d.ts.map +1 -1
- package/dist/commands/memory/stop.js +26 -55
- package/dist/commands/memory/stop.js.map +1 -1
- package/dist/commands/memory-service-container.d.ts +78 -0
- package/dist/commands/memory-service-container.d.ts.map +1 -0
- package/dist/commands/memory-service-container.js +187 -0
- package/dist/commands/memory-service-container.js.map +1 -0
- package/dist/commands/services.d.ts +16 -1
- package/dist/commands/services.d.ts.map +1 -1
- package/dist/commands/services.js +97 -38
- package/dist/commands/services.js.map +1 -1
- package/dist/commands/substrate.d.ts +19 -1
- package/dist/commands/substrate.d.ts.map +1 -1
- package/dist/commands/substrate.js +19 -11
- package/dist/commands/substrate.js.map +1 -1
- package/dist/commands/upgrade.d.ts.map +1 -1
- package/dist/commands/upgrade.js +11 -0
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/image-digests.json +7 -7
- package/dist/index.js +3662 -2044
- package/dist/index.js.map +1 -1
- package/dist/lib/auth-refresh-kubernetes.d.ts +3 -0
- package/dist/lib/auth-refresh-kubernetes.d.ts.map +1 -1
- package/dist/lib/auth-refresh-kubernetes.js +6 -17
- package/dist/lib/auth-refresh-kubernetes.js.map +1 -1
- package/dist/lib/health-probes.d.ts +20 -0
- package/dist/lib/health-probes.d.ts.map +1 -1
- package/dist/lib/health-probes.js +55 -0
- package/dist/lib/health-probes.js.map +1 -1
- package/dist/lib/k8s-bootstrap.d.ts +120 -0
- package/dist/lib/k8s-bootstrap.d.ts.map +1 -0
- package/dist/lib/k8s-bootstrap.js +193 -0
- package/dist/lib/k8s-bootstrap.js.map +1 -0
- package/dist/lib/k8s-secret-render.d.ts +139 -0
- package/dist/lib/k8s-secret-render.d.ts.map +1 -0
- package/dist/lib/k8s-secret-render.js +281 -0
- package/dist/lib/k8s-secret-render.js.map +1 -0
- package/dist/lib/kubectl-context.d.ts +38 -0
- package/dist/lib/kubectl-context.d.ts.map +1 -0
- package/dist/lib/kubectl-context.js +43 -0
- package/dist/lib/kubectl-context.js.map +1 -0
- package/dist/lib/memory-host-process-migration.d.ts +56 -0
- package/dist/lib/memory-host-process-migration.d.ts.map +1 -0
- package/dist/lib/memory-host-process-migration.js +156 -0
- package/dist/lib/memory-host-process-migration.js.map +1 -0
- package/dist/lib/upgrade-kubernetes.d.ts +42 -0
- package/dist/lib/upgrade-kubernetes.d.ts.map +1 -1
- package/dist/lib/upgrade-kubernetes.js +258 -24
- package/dist/lib/upgrade-kubernetes.js.map +1 -1
- package/dist/mcp-server.js +56 -22
- package/hermes-bundle/kg-first.sh +100 -0
- package/hermes-bundle/version.json +4 -0
- package/host-cp/k8s/manifests/50-deployment.yaml +54 -27
- package/host-cp/k8s/manifests/auth-service/30-configmap.yaml +5 -0
- package/host-cp/k8s/manifests/auth-service/50-deployment.yaml +5 -1
- package/host-cp/k8s/manifests/kg-service/30-configmap.yaml +5 -0
- package/host-cp/k8s/manifests/kg-service/50-deployment.yaml +5 -1
- package/host-cp/k8s/manifests/mcp-auth-service/30-configmap.yaml +4 -0
- package/host-cp/k8s/manifests/mcp-auth-service/50-deployment.yaml +5 -1
- package/host-cp/k8s/manifests/memory-service/30-configmap.yaml +4 -0
- package/host-cp/k8s/manifests/memory-service/50-deployment.yaml +5 -1
- package/package.json +3 -4
- package/memory-service-bundle/scripts/ensure-iii-engine.mjs +0 -179
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory-host-process-migration.ts — idempotent cleanup of the Phase A
|
|
3
|
+
* host-process model's residual state on disk.
|
|
4
|
+
*
|
|
5
|
+
* Phase A spawned `agentmemory` as a host child process and recorded its PID
|
|
6
|
+
* at `~/.olam/memory.pid` + stdout/stderr at `~/.olam/memory-service.log`.
|
|
7
|
+
* Phase B repoints `olam memory start` at the Docker substrate (this skill's
|
|
8
|
+
* `MemoryServiceContainerController`); the legacy pidfile + log become orphan
|
|
9
|
+
* artefacts the moment the new substrate runs.
|
|
10
|
+
*
|
|
11
|
+
* This helper is called from `runMemoryStart` / `runMemoryStop` (B1 / B2) BEFORE
|
|
12
|
+
* the substrate handoff so operators upgrading from the host-process model
|
|
13
|
+
* never have to manually clean up.
|
|
14
|
+
*
|
|
15
|
+
* PID-reuse defence (T2 mitigation). A stale pidfile can record a PID that
|
|
16
|
+
* the OS has since recycled to an unrelated process. SIGTERMing the wrong PID
|
|
17
|
+
* is a foot-gun. Defence:
|
|
18
|
+
* 1. If pidfile is absent → no-op.
|
|
19
|
+
* 2. If recorded PID is dead (kill(0) throws ESRCH) → unlink pidfile only.
|
|
20
|
+
* 3. If recorded PID is alive BUT `ps -p <pid> -o comm=` doesn't return
|
|
21
|
+
* `node` (the legacy host-process was always a node child) → unlink
|
|
22
|
+
* pidfile WITHOUT signalling. Logs a warning so support can diagnose.
|
|
23
|
+
* 4. Only when PID is alive AND comm=node → SIGTERM, wait up to 5s,
|
|
24
|
+
* SIGKILL on timeout, then unlink.
|
|
25
|
+
*
|
|
26
|
+
* Plan reference: docs/plans/memory-service-as-docker-peripheral/phase-b-tasks.md B5
|
|
27
|
+
*/
|
|
28
|
+
import { existsSync, readFileSync, unlinkSync } from 'node:fs';
|
|
29
|
+
import { spawnSync } from 'node:child_process';
|
|
30
|
+
import { MEMORY_PID_PATH, MEMORY_LOG_PATH } from '../commands/memory/_paths.js';
|
|
31
|
+
const KILL_TIMEOUT_MS = 5_000;
|
|
32
|
+
/**
|
|
33
|
+
* Detect + clean up legacy host-process state. Idempotent — safe to call on
|
|
34
|
+
* every `olam memory start` / `olam memory stop` invocation; no-op when no
|
|
35
|
+
* legacy artefacts present.
|
|
36
|
+
*
|
|
37
|
+
* Side effects (only when artefacts present):
|
|
38
|
+
* - Unlink `~/.olam/memory.pid` if it exists.
|
|
39
|
+
* - Send SIGTERM (then SIGKILL on timeout) to the recorded PID iff it's
|
|
40
|
+
* alive AND its comm name is `node` (the legacy spawn shape).
|
|
41
|
+
* - Leave `~/.olam/memory-service.log` in place by default — operators
|
|
42
|
+
* may want to inspect it post-migration. Pass `{ removeLog: true }` to
|
|
43
|
+
* remove it explicitly (B2's stop path uses this).
|
|
44
|
+
*/
|
|
45
|
+
export function migrateFromHostProcess(opts = {}) {
|
|
46
|
+
const pidPath = opts.pidPath ?? MEMORY_PID_PATH;
|
|
47
|
+
const logPath = opts.logPath ?? MEMORY_LOG_PATH;
|
|
48
|
+
const pidfileExists = existsSync(pidPath);
|
|
49
|
+
const logExists = existsSync(logPath);
|
|
50
|
+
if (!pidfileExists && !logExists) {
|
|
51
|
+
return { cleaned: false, summary: 'no legacy host-process state to clean' };
|
|
52
|
+
}
|
|
53
|
+
const events = [];
|
|
54
|
+
if (pidfileExists) {
|
|
55
|
+
const pid = readPidFromFile(pidPath);
|
|
56
|
+
if (pid === null) {
|
|
57
|
+
// Pidfile exists but is unparseable — just unlink.
|
|
58
|
+
unlinkSync(pidPath);
|
|
59
|
+
events.push('unlinked unparseable ~/.olam/memory.pid');
|
|
60
|
+
}
|
|
61
|
+
else if (!isProcessAlive(pid)) {
|
|
62
|
+
// Recorded PID is dead — clean unlink with no signalling.
|
|
63
|
+
unlinkSync(pidPath);
|
|
64
|
+
events.push(`unlinked stale ~/.olam/memory.pid (pid ${pid} not alive)`);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
// PID is alive — check comm name before signalling.
|
|
68
|
+
const comm = processCommName(pid);
|
|
69
|
+
if (comm !== 'node') {
|
|
70
|
+
// PID-reuse case: alive but not our process. Unlink WITHOUT signalling.
|
|
71
|
+
unlinkSync(pidPath);
|
|
72
|
+
events.push(`unlinked ~/.olam/memory.pid without signal (pid ${pid} comm=${comm ?? 'unknown'}, not node — assumed PID-reuse, defensive skip)`);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
// Our process — terminate gracefully.
|
|
76
|
+
const terminated = terminateProcess(pid);
|
|
77
|
+
unlinkSync(pidPath);
|
|
78
|
+
events.push(terminated
|
|
79
|
+
? `terminated legacy host-process pid ${pid} (SIGTERM) + unlinked pidfile`
|
|
80
|
+
: `terminated legacy host-process pid ${pid} (SIGKILL after timeout) + unlinked pidfile`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (logExists && opts.removeLog) {
|
|
85
|
+
unlinkSync(logPath);
|
|
86
|
+
events.push('removed legacy ~/.olam/memory-service.log');
|
|
87
|
+
}
|
|
88
|
+
else if (logExists) {
|
|
89
|
+
events.push('left ~/.olam/memory-service.log in place (inspectable; pass {removeLog:true} to remove)');
|
|
90
|
+
}
|
|
91
|
+
return { cleaned: true, summary: events.join('; ') };
|
|
92
|
+
}
|
|
93
|
+
function readPidFromFile(pidPath) {
|
|
94
|
+
try {
|
|
95
|
+
const raw = readFileSync(pidPath, 'utf8').trim();
|
|
96
|
+
const pid = parseInt(raw, 10);
|
|
97
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
98
|
+
return null;
|
|
99
|
+
return pid;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function isProcessAlive(pid) {
|
|
106
|
+
try {
|
|
107
|
+
// signal 0 doesn't deliver; just tests existence + permission
|
|
108
|
+
process.kill(pid, 0);
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Read the comm name of a process via `ps -p <pid> -o comm=`. Returns null
|
|
117
|
+
* on any error (process gone, ps not available, etc.) — the caller treats
|
|
118
|
+
* null as "don't signal," which is the safe default.
|
|
119
|
+
*/
|
|
120
|
+
function processCommName(pid) {
|
|
121
|
+
const r = spawnSync('ps', ['-p', String(pid), '-o', 'comm='], { encoding: 'utf-8' });
|
|
122
|
+
if (r.status !== 0)
|
|
123
|
+
return null;
|
|
124
|
+
const comm = r.stdout.trim();
|
|
125
|
+
// `ps` may return a full path (e.g. `/usr/local/bin/node`); take basename.
|
|
126
|
+
return comm.split('/').pop() ?? null;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* SIGTERM the PID; wait up to 5s for it to exit; SIGKILL on timeout.
|
|
130
|
+
* Returns true if SIGTERM was sufficient, false if SIGKILL was needed.
|
|
131
|
+
*/
|
|
132
|
+
function terminateProcess(pid) {
|
|
133
|
+
try {
|
|
134
|
+
process.kill(pid, 'SIGTERM');
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// Already dead between the comm-check and now; nothing to do.
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
const deadline = Date.now() + KILL_TIMEOUT_MS;
|
|
141
|
+
while (Date.now() < deadline) {
|
|
142
|
+
if (!isProcessAlive(pid))
|
|
143
|
+
return true;
|
|
144
|
+
// Busy-spin sleep — node:fs is synchronous; no async helper here.
|
|
145
|
+
spawnSync('sleep', ['0.1']);
|
|
146
|
+
}
|
|
147
|
+
// Still alive after timeout — escalate.
|
|
148
|
+
try {
|
|
149
|
+
process.kill(pid, 'SIGKILL');
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// race: died between probe and SIGKILL
|
|
153
|
+
}
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=memory-host-process-migration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory-host-process-migration.js","sourceRoot":"","sources":["../../src/lib/memory-host-process-migration.ts"],"names":[],"mappings":"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"}
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
* D27 — audit log entry (phase2.flag_removed) emitted per upgrade run
|
|
24
24
|
*
|
|
25
25
|
* Step order (Phase D — kubernetes substrate, Phase 2 GA):
|
|
26
|
+
* 0.4 R3-C create/update ghcr-pull imagePullSecret in olam namespace (when GH_TOKEN available)
|
|
27
|
+
* 0.5 D4 k3d node docker socket bind-mount preflight (Decision #11 backward-compat surface)
|
|
26
28
|
* 0 probeKubernetesApiReachable — 5s timeout kubectl cluster-info
|
|
27
29
|
* 1 D10 context-allowlist + OLAM_K8S_CONTEXT_ACK strict-equality byte-for-byte
|
|
28
30
|
* 2 D12 Secret pre-check (olam-host-cp-secret; base64-decode; key-name check)
|
|
@@ -44,6 +46,7 @@ import { kubectlWrap } from './kubectl-wrap.js';
|
|
|
44
46
|
import { spawnPortForward, spawnAllPeripheralPortForwards, probePortForwardLiveness, type PortForwardDeps, type PeripheralPortForwardDeps } from './port-forward.js';
|
|
45
47
|
import { emitUpgradeComplete, type EmitOpts } from './instrumentation.js';
|
|
46
48
|
import { runManifestRefresh, seedManifestsFromBundle, type SeedManifestsDeps } from './manifest-refresh.js';
|
|
49
|
+
import { ensureK8sBootstrap, type BootstrapDeps as K8sBootstrapDeps } from './k8s-bootstrap.js';
|
|
47
50
|
export declare const OLAM_K8S_MANIFESTS_DIR: string;
|
|
48
51
|
export declare const K8S_NAMESPACE = "olam";
|
|
49
52
|
export declare const HOST_CP_SECRET_NAME = "olam-host-cp-secret";
|
|
@@ -99,10 +102,49 @@ export interface UpgradeKubernetesDeps {
|
|
|
99
102
|
* Tests inject a mock to avoid real kubectl invocations.
|
|
100
103
|
*/
|
|
101
104
|
readonly checkDockerSocketImpl?: (context: string) => Promise<boolean>;
|
|
105
|
+
/**
|
|
106
|
+
* B3: override config path passed to resolveKubectlContext (tests use a
|
|
107
|
+
* tmpdir fixture). Defaults to the real ~/.olam/config.json.
|
|
108
|
+
*/
|
|
109
|
+
readonly configPath?: string;
|
|
110
|
+
/**
|
|
111
|
+
* B4: override the k8s-bootstrap step (tests inject a mock so kubectl
|
|
112
|
+
* isn't called). When omitted defaults to the real `ensureK8sBootstrap`.
|
|
113
|
+
*/
|
|
114
|
+
readonly k8sBootstrapImpl?: typeof ensureK8sBootstrap;
|
|
115
|
+
/** B4: pass-through deps for `ensureK8sBootstrap`. */
|
|
116
|
+
readonly k8sBootstrapDeps?: K8sBootstrapDeps;
|
|
117
|
+
/**
|
|
118
|
+
* B3/step 0.4: override the GH token value for tests.
|
|
119
|
+
* When `ghTokenOverrideActive` is true AND `ghTokenOverride` is undefined,
|
|
120
|
+
* the step treats GH_TOKEN as absent (skipped path). When `ghTokenOverride`
|
|
121
|
+
* is a string, that string is used as the token regardless of env/config.
|
|
122
|
+
* When `ghTokenOverrideActive` is falsy (default), real env/config is used.
|
|
123
|
+
*/
|
|
124
|
+
readonly ghTokenOverride?: string;
|
|
125
|
+
readonly ghTokenOverrideActive?: boolean;
|
|
126
|
+
/**
|
|
127
|
+
* Phase D D4: override for k3d node mount detection (step 0.5 preflight).
|
|
128
|
+
* Returns 'new-form' when /host-colima/ bind is present (correct),
|
|
129
|
+
* 'old-form' when /var/run/docker.sock direct file-bind is present (broken on colima),
|
|
130
|
+
* or 'none' when neither is detectable (non-k3d or bare k3s cluster — warn only).
|
|
131
|
+
* Tests inject a mock to avoid real `docker inspect` invocations.
|
|
132
|
+
*/
|
|
133
|
+
readonly checkK3dNodeMountsImpl?: () => Promise<'new-form' | 'old-form' | 'none'>;
|
|
102
134
|
}
|
|
103
135
|
export interface UpgradeKubernetesOpts {
|
|
104
136
|
readonly forceRefreshManifests?: boolean;
|
|
105
137
|
readonly acceptSecurityRegression?: boolean;
|
|
138
|
+
/** B4/B5: regenerate all rendered Secret values (overwrites k8s-secrets-state.json). */
|
|
139
|
+
readonly rotateSecrets?: boolean;
|
|
140
|
+
/**
|
|
141
|
+
* B4: run the namespace + RBAC + secret bootstrap step BEFORE step 1.
|
|
142
|
+
* Default is `false` for direct lib callers (preserves existing test
|
|
143
|
+
* ordering invariants); the CLI command in `upgrade.ts` passes `true` so
|
|
144
|
+
* npm-only operators get the bootstrap automatically. Operators who
|
|
145
|
+
* manage secrets out-of-band (GitOps) can disable via `--skip-k8s-bootstrap`.
|
|
146
|
+
*/
|
|
147
|
+
readonly runK8sBootstrap?: boolean;
|
|
106
148
|
}
|
|
107
149
|
export interface UpgradeKubernetesResult {
|
|
108
150
|
readonly exitCode: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upgrade-kubernetes.d.ts","sourceRoot":"","sources":["../../src/lib/upgrade-kubernetes.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"upgrade-kubernetes.d.ts","sourceRoot":"","sources":["../../src/lib/upgrade-kubernetes.ts"],"names":[],"mappings":"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"}
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
* D27 — audit log entry (phase2.flag_removed) emitted per upgrade run
|
|
24
24
|
*
|
|
25
25
|
* Step order (Phase D — kubernetes substrate, Phase 2 GA):
|
|
26
|
+
* 0.4 R3-C create/update ghcr-pull imagePullSecret in olam namespace (when GH_TOKEN available)
|
|
27
|
+
* 0.5 D4 k3d node docker socket bind-mount preflight (Decision #11 backward-compat surface)
|
|
26
28
|
* 0 probeKubernetesApiReachable — 5s timeout kubectl cluster-info
|
|
27
29
|
* 1 D10 context-allowlist + OLAM_K8S_CONTEXT_ACK strict-equality byte-for-byte
|
|
28
30
|
* 2 D12 Secret pre-check (olam-host-cp-secret; base64-decode; key-name check)
|
|
@@ -53,6 +55,8 @@ import { emitUpgradeComplete } from './instrumentation.js';
|
|
|
53
55
|
import { runManifestRefresh, seedManifestsFromBundle } from './manifest-refresh.js';
|
|
54
56
|
import { OLAM_HOME, OLAM_STATE_DIR } from './config.js';
|
|
55
57
|
import { PERIPHERALS } from './peripheral-registry.js';
|
|
58
|
+
import { resolveKubectlContext } from './kubectl-context.js';
|
|
59
|
+
import { ensureK8sBootstrap } from './k8s-bootstrap.js';
|
|
56
60
|
export const OLAM_K8S_MANIFESTS_DIR = path.join(OLAM_HOME, 'k8s', 'manifests');
|
|
57
61
|
export const K8S_NAMESPACE = 'olam';
|
|
58
62
|
export const HOST_CP_SECRET_NAME = 'olam-host-cp-secret';
|
|
@@ -80,10 +84,20 @@ const PERIPHERAL_SECRETS = [
|
|
|
80
84
|
const K8S_DNS_SUFFIX = 'olam.svc.cluster.local';
|
|
81
85
|
/**
|
|
82
86
|
* Build the K8s in-cluster DNS URL for a peripheral service.
|
|
83
|
-
* Form: http
|
|
87
|
+
* Form: http://olam-<k8sServiceName>.olam.svc.cluster.local:<port>
|
|
88
|
+
*
|
|
89
|
+
* All olam peripheral k8s Services are named `olam-<short-name>` in the
|
|
90
|
+
* manifests (e.g. `olam-auth-service` not `auth-service`). The PERIPHERALS
|
|
91
|
+
* registry stores the short name in `k8sServiceName` for use by port-forward
|
|
92
|
+
* (which already works). Here we always prepend `olam-` because the DNS
|
|
93
|
+
* hostname MUST match the Service metadata.name in 60-service.yaml.
|
|
94
|
+
*
|
|
95
|
+
* C3 (R3-F): fix — previous form `http://auth-service.olam.svc.cluster.local`
|
|
96
|
+
* resolved to nothing; correct form is `http://olam-auth-service.olam.svc.cluster.local`.
|
|
84
97
|
*/
|
|
85
98
|
function buildK8sDnsUrl(k8sServiceName, port) {
|
|
86
|
-
|
|
99
|
+
const prefixed = k8sServiceName.startsWith('olam-') ? k8sServiceName : `olam-${k8sServiceName}`;
|
|
100
|
+
return `http://${prefixed}.${K8S_DNS_SUFFIX}:${port}`;
|
|
87
101
|
}
|
|
88
102
|
/**
|
|
89
103
|
* Append a JSONL audit entry to the substrate audit log (D18).
|
|
@@ -392,6 +406,140 @@ async function verifyHealthHeader(deps) {
|
|
|
392
406
|
return { ok: false, engineHeader: null };
|
|
393
407
|
}
|
|
394
408
|
}
|
|
409
|
+
/**
|
|
410
|
+
* Step 0.4 — R3-C: create/update ghcr-pull imagePullSecret (Decision R3-#3).
|
|
411
|
+
*
|
|
412
|
+
* Creates a kubernetes.io/dockerconfigjson Secret named `ghcr-pull` in the
|
|
413
|
+
* `olam` namespace so all 5 Deployment specs (host-cp + 4 peripherals) can
|
|
414
|
+
* pull private images from ghcr.io/pleri/* without anonymous rate limits.
|
|
415
|
+
*
|
|
416
|
+
* Token resolution order:
|
|
417
|
+
* 1. deps.ghTokenOverride (test injection — ghTokenOverrideActive must be true)
|
|
418
|
+
* 2. host.gh_token in ~/.olam/config.json (operator config, not yet wired here —
|
|
419
|
+
* future: read via resolveKubectlContext deps; skipped for Phase B scope)
|
|
420
|
+
* 3. GH_TOKEN environment variable
|
|
421
|
+
*
|
|
422
|
+
* Returns { skipped: true } when no token is available (not a hard failure —
|
|
423
|
+
* operators without a GH token fall back to anonymous pulls which may rate-limit).
|
|
424
|
+
*
|
|
425
|
+
* Idempotent: `kubectl apply -f -` (stdin) is a no-op when the Secret already
|
|
426
|
+
* exists with the same content (kubectl reports "configured" or "unchanged").
|
|
427
|
+
*/
|
|
428
|
+
async function createGhcrPullSecret(context, deps, stderr) {
|
|
429
|
+
// Resolve GH token from override (test injection) or environment.
|
|
430
|
+
let ghToken;
|
|
431
|
+
if (deps.ghTokenOverrideActive === true) {
|
|
432
|
+
ghToken = deps.ghTokenOverride;
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
ghToken = process.env['GH_TOKEN'];
|
|
436
|
+
}
|
|
437
|
+
if (!ghToken) {
|
|
438
|
+
stderr.write(`${pc.yellow('[warn]')} GH_TOKEN not found — skipping ghcr-pull Secret creation.\n` +
|
|
439
|
+
` Image pulls from ghcr.io/pleri/* will use anonymous access (may rate-limit).\n` +
|
|
440
|
+
` Set GH_TOKEN env var or add host.gh_token to ~/.olam/config.json to enable.\n`);
|
|
441
|
+
return { skipped: true, reason: 'no GH_TOKEN in env or config' };
|
|
442
|
+
}
|
|
443
|
+
const dockerConfigJson = JSON.stringify({
|
|
444
|
+
auths: {
|
|
445
|
+
'ghcr.io': {
|
|
446
|
+
auth: Buffer.from(`pleri:${ghToken}`).toString('base64'),
|
|
447
|
+
},
|
|
448
|
+
},
|
|
449
|
+
});
|
|
450
|
+
const secretManifest = {
|
|
451
|
+
apiVersion: 'v1',
|
|
452
|
+
kind: 'Secret',
|
|
453
|
+
type: 'kubernetes.io/dockerconfigjson',
|
|
454
|
+
metadata: { name: 'ghcr-pull', namespace: K8S_NAMESPACE },
|
|
455
|
+
data: {
|
|
456
|
+
'.dockerconfigjson': Buffer.from(dockerConfigJson).toString('base64'),
|
|
457
|
+
},
|
|
458
|
+
};
|
|
459
|
+
const secretYaml = JSON.stringify(secretManifest); // kubectl apply accepts JSON as YAML
|
|
460
|
+
const wrap = deps.kubectlWrapImpl ?? kubectlWrap;
|
|
461
|
+
const result = await wrap(['--context', context, 'apply', '-f', '-'], { timeout: 30_000, stdin: secretYaml });
|
|
462
|
+
if (!result.ok) {
|
|
463
|
+
return { skipped: false, reason: `kubectl apply ghcr-pull failed: ${result.stderr.split('\n')[0] ?? ''}` };
|
|
464
|
+
}
|
|
465
|
+
return { skipped: false };
|
|
466
|
+
}
|
|
467
|
+
/** k3d node container name olam uses by convention. */
|
|
468
|
+
const K3D_NODE_CONTAINER = 'k3d-olam-host-server-0';
|
|
469
|
+
/**
|
|
470
|
+
* Default implementation: run `docker inspect k3d-olam-host-server-0` and
|
|
471
|
+
* scan the Mounts array to determine which bind form is in use.
|
|
472
|
+
*
|
|
473
|
+
* Returns:
|
|
474
|
+
* 'new-form' — /host-colima/ bind present (correct form per Phase B B1)
|
|
475
|
+
* 'old-form' — /var/run/docker.sock direct file-bind present (broken on colima, R3-A)
|
|
476
|
+
* 'none' — container not found or neither bind detectable (non-k3d / bare k3s)
|
|
477
|
+
*/
|
|
478
|
+
async function defaultCheckK3dNodeMounts() {
|
|
479
|
+
const { execFile } = await import('node:child_process');
|
|
480
|
+
const { promisify } = await import('node:util');
|
|
481
|
+
const execFileAsync = promisify(execFile);
|
|
482
|
+
try {
|
|
483
|
+
const { stdout } = await execFileAsync('docker', ['inspect', K3D_NODE_CONTAINER, '--format', '{{json .HostConfig.Binds}}'], { timeout: 8_000 });
|
|
484
|
+
const binds = JSON.parse(stdout.trim());
|
|
485
|
+
if (!Array.isArray(binds))
|
|
486
|
+
return 'none';
|
|
487
|
+
// New form: bind contains /host-colima/ (directory bind from Phase B B1).
|
|
488
|
+
if (binds.some((b) => b.includes('/host-colima/')))
|
|
489
|
+
return 'new-form';
|
|
490
|
+
// Old form: bind contains /var/run/docker.sock as a file source (R3-A broken form).
|
|
491
|
+
if (binds.some((b) => b.startsWith('/var/run/docker.sock:')))
|
|
492
|
+
return 'old-form';
|
|
493
|
+
return 'none';
|
|
494
|
+
}
|
|
495
|
+
catch {
|
|
496
|
+
// Container not found, docker unavailable, or JSON parse error — treat as non-k3d.
|
|
497
|
+
return 'none';
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Step 0.5 — Phase D D4: k3d node docker socket bind-mount preflight (Decision #11).
|
|
502
|
+
*
|
|
503
|
+
* Detects whether the k3d-olam-host cluster was created with the correct
|
|
504
|
+
* `--volume "$HOME/.colima/default/:/host-colima/@server:*"` bind (new form,
|
|
505
|
+
* Phase B B1) or the old `--volume /var/run/docker.sock:/var/run/docker.sock`
|
|
506
|
+
* direct file-bind (broken on colima, R3-A).
|
|
507
|
+
*
|
|
508
|
+
* - 'new-form' → proceed (correct)
|
|
509
|
+
* - 'old-form' → hard error with actionable Decision #11 recreate command
|
|
510
|
+
* - 'none' → WARN only (non-k3d or bare k3s without socket bind; may work)
|
|
511
|
+
*
|
|
512
|
+
* Runs ONLY on kubernetes substrate. The check is host-side (`docker inspect`)
|
|
513
|
+
* so it can fire BEFORE any kubectl apply (pre-step 0b runs AFTER cluster is
|
|
514
|
+
* deployed; this runs BEFORE step 0 so fresh-install operators catch the
|
|
515
|
+
* broken bind before wasting a full upgrade attempt).
|
|
516
|
+
*
|
|
517
|
+
* Returns null on pass/warn (upgrade should continue);
|
|
518
|
+
* returns error message string on hard failure (upgrade should abort).
|
|
519
|
+
*/
|
|
520
|
+
async function preflightK3dNodeMounts(deps, stderr) {
|
|
521
|
+
const check = deps.checkK3dNodeMountsImpl ?? defaultCheckK3dNodeMounts;
|
|
522
|
+
const form = await check();
|
|
523
|
+
if (form === 'new-form') {
|
|
524
|
+
// Correct bind — proceed silently.
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
if (form === 'old-form') {
|
|
528
|
+
// Decision #11 backward-compat error: operator must recreate the cluster.
|
|
529
|
+
return (`Your k3d cluster was created with the old --volume form which fails on colima.\n` +
|
|
530
|
+
` Recreate the cluster with:\n` +
|
|
531
|
+
` k3d cluster delete olam-host\n` +
|
|
532
|
+
` k3d cluster create olam-host --volume "$HOME/.colima/default/:/host-colima/@server:*"\n` +
|
|
533
|
+
` Then re-run olam upgrade.`);
|
|
534
|
+
}
|
|
535
|
+
// 'none': k3d node container not found or non-k3d cluster (bare k3s, minikube, etc.).
|
|
536
|
+
// Emit WARN only — we cannot detect the bind on non-k3d setups, so do not block.
|
|
537
|
+
stderr.write(`${pc.yellow('[warn]')} step 0.5: k3d node container "${K3D_NODE_CONTAINER}" not found or bind form undetectable.\n` +
|
|
538
|
+
` This is expected on non-k3d clusters (bare k3s, minikube). Continuing.\n` +
|
|
539
|
+
` On colima+k3d: ensure the cluster was created with:\n` +
|
|
540
|
+
` k3d cluster create olam-host --volume "$HOME/.colima/default/:/host-colima/@server:*"\n`);
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
395
543
|
/**
|
|
396
544
|
* Main entrypoint for the kubernetes upgrade path.
|
|
397
545
|
*
|
|
@@ -418,6 +566,68 @@ export async function runUpgradeKubernetes(opts = {}, deps = {}) {
|
|
|
418
566
|
}
|
|
419
567
|
// skipped: dir exists or bundle absent (dev context) — no output, continue.
|
|
420
568
|
}
|
|
569
|
+
// ── Step 0.4: R3-C — create/update ghcr-pull imagePullSecret ────────
|
|
570
|
+
// Runs BEFORE probeKubernetesApiReachable because the Secret creation itself
|
|
571
|
+
// requires API access. Placed after manifest seed so manifests (which include
|
|
572
|
+
// imagePullSecrets: [{name: ghcr-pull}]) are on disk before step 3 applies them.
|
|
573
|
+
//
|
|
574
|
+
// Context resolution: step 0.4 runs before the B3 context-resolution block, so
|
|
575
|
+
// we resolve a preliminary context here. The canonical resolution happens at
|
|
576
|
+
// pre-step 0a-prelude and is used for all subsequent steps. This duplicate
|
|
577
|
+
// resolution is acceptable — step 0.4 is additive and does not replace B3.
|
|
578
|
+
{
|
|
579
|
+
const preliminaryResolved = resolveKubectlContext(deps.configPath);
|
|
580
|
+
if (preliminaryResolved.error === undefined) {
|
|
581
|
+
const preliminaryContext = preliminaryResolved.context;
|
|
582
|
+
const step04Spinner = ora('Creating ghcr-pull imagePullSecret (R3-C)').start();
|
|
583
|
+
const pullSecretResult = await createGhcrPullSecret(preliminaryContext, deps, stderr);
|
|
584
|
+
if (pullSecretResult.skipped) {
|
|
585
|
+
step04Spinner.warn(`ghcr-pull Secret skipped: ${pullSecretResult.reason ?? 'no token'}`);
|
|
586
|
+
}
|
|
587
|
+
else if (pullSecretResult.reason) {
|
|
588
|
+
// kubectl apply failed — warn but do not abort. Rollout will fail on 401
|
|
589
|
+
// image pull if the secret is needed, which surfaces a clear error.
|
|
590
|
+
const warnMsg = `ghcr-pull Secret apply failed (continuing): ${pullSecretResult.reason}`;
|
|
591
|
+
step04Spinner.warn(warnMsg);
|
|
592
|
+
stderr.write(`${pc.yellow('[warn]')} ${warnMsg}\n`);
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
step04Spinner.succeed('ghcr-pull imagePullSecret created/updated');
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
// If context resolution fails here, step 0a-prelude will catch and abort below.
|
|
599
|
+
}
|
|
600
|
+
// ── Step 0.5: D4 — k3d node docker socket bind-mount preflight (Decision #11) ──
|
|
601
|
+
// Runs BEFORE API reachability (step 0) so operators on a mis-configured k3d
|
|
602
|
+
// cluster get an actionable error before wasting a full upgrade attempt.
|
|
603
|
+
// Only meaningful on k3d clusters where the node container is named
|
|
604
|
+
// k3d-olam-host-server-0. Non-k3d clusters emit WARN and proceed.
|
|
605
|
+
{
|
|
606
|
+
const step05Error = await preflightK3dNodeMounts(deps, stderr);
|
|
607
|
+
if (step05Error !== null) {
|
|
608
|
+
stderr.write(`${pc.red('error:')} ${step05Error}\n`);
|
|
609
|
+
return { exitCode: 1, summary: 'k3d node bind-mount preflight failed (Decision #11)' };
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
// ── Pre-step 0a-prelude: B3 — resolve kubectl context ONCE (config + env) ──
|
|
613
|
+
// The context is needed by ALL kubectl-invoking pre-steps and steps below.
|
|
614
|
+
// Resolution policy lives in `kubectl-context.ts` (single source of truth):
|
|
615
|
+
// 1. host.kubectl_context_pinned from ~/.olam/config.json (preferred)
|
|
616
|
+
// 2. OLAM_K8S_CONTEXT_ACK env var (deprecated, emits warning)
|
|
617
|
+
// 3. neither → error with actionable remediation
|
|
618
|
+
//
|
|
619
|
+
// Pre-B3 bug: step 0 read OLAM_K8S_CONTEXT_ACK directly and fell back to
|
|
620
|
+
// 'default' when unset, so operators who had only pinned the config got
|
|
621
|
+
// cryptic `cluster-info failed` failures pointing at the wrong remediation.
|
|
622
|
+
const resolved = resolveKubectlContext(deps.configPath);
|
|
623
|
+
if (resolved.error !== undefined) {
|
|
624
|
+
stderr.write(`${pc.red('error:')} ${resolved.error}\n`);
|
|
625
|
+
return { exitCode: 1, summary: 'kubectl context not configured' };
|
|
626
|
+
}
|
|
627
|
+
const pinnedContext = resolved.context;
|
|
628
|
+
if (resolved.deprecationWarning !== undefined) {
|
|
629
|
+
stderr.write(`${pc.yellow('[warn]')} ${resolved.deprecationWarning}\n`);
|
|
630
|
+
}
|
|
421
631
|
// ── Pre-step 0a: D6 — managed-k8s context refusal (Decision #18) ──────
|
|
422
632
|
// Detects managed-k8s cluster server URL patterns and refuses BEFORE any
|
|
423
633
|
// kubectl apply. Fail-open: unrecognized cluster URLs proceed normally.
|
|
@@ -440,11 +650,10 @@ export async function runUpgradeKubernetes(opts = {}, deps = {}) {
|
|
|
440
650
|
// Note: only meaningful when host-cp is already deployed. On a fresh install
|
|
441
651
|
// the pod doesn't exist yet — kubectl exec will fail gracefully (WARN only).
|
|
442
652
|
{
|
|
443
|
-
const
|
|
444
|
-
const socketAccessible = await checkDockerSocketAccessible(preflightContext, deps);
|
|
653
|
+
const socketAccessible = await checkDockerSocketAccessible(pinnedContext, deps);
|
|
445
654
|
if (!socketAccessible) {
|
|
446
655
|
// Issue #713: emit a cluster-type-aware remedy (colima vs k3d).
|
|
447
|
-
const clusterType = defaultDetectK8sClusterType(
|
|
656
|
+
const clusterType = defaultDetectK8sClusterType(pinnedContext);
|
|
448
657
|
const remedyText = buildDockerSocketRemedy(clusterType)
|
|
449
658
|
.split('\n')
|
|
450
659
|
.map((line) => ` ${line}`)
|
|
@@ -460,33 +669,58 @@ export async function runUpgradeKubernetes(opts = {}, deps = {}) {
|
|
|
460
669
|
}
|
|
461
670
|
// ── Step 0: D22 — probe Kubernetes API reachability (5s) ─────────
|
|
462
671
|
const step0Spinner = ora('Probing Kubernetes API reachability').start();
|
|
463
|
-
const
|
|
464
|
-
// We need a context for the probe — use the env var if set, else try default.
|
|
465
|
-
// The full context validation happens at step 1.
|
|
466
|
-
const probeContext = context.length > 0 ? context : 'default';
|
|
467
|
-
const reachable = await probeKubernetesApiReachable(probeContext, deps);
|
|
672
|
+
const reachable = await probeKubernetesApiReachable(pinnedContext, deps);
|
|
468
673
|
if (!reachable) {
|
|
469
674
|
step0Spinner.fail('Kubernetes API not reachable');
|
|
470
|
-
stderr.write(`${pc.red('error:')} kubectl cluster-info failed (5s timeout).\n` +
|
|
471
|
-
` Ensure
|
|
472
|
-
`
|
|
675
|
+
stderr.write(`${pc.red('error:')} kubectl cluster-info failed (5s timeout) for context ${pc.bold(pinnedContext)}.\n` +
|
|
676
|
+
` Ensure the cluster is reachable for this context, or update host.kubectl_context_pinned\n` +
|
|
677
|
+
` in ~/.olam/config.json to a working context.\n`);
|
|
473
678
|
return { exitCode: 1, summary: 'kubernetes api not reachable' };
|
|
474
679
|
}
|
|
475
680
|
step0Spinner.succeed('Kubernetes API reachable');
|
|
476
|
-
// ── Step
|
|
681
|
+
// ── Step 0.5: B4 — ensureK8sBootstrap ────────────────────────────
|
|
682
|
+
// Apply namespace + RBAC + ConfigMap + PVC + Secret set BEFORE the
|
|
683
|
+
// existing 8-step flow's step 2 Secret pre-check. Without this an npm-only
|
|
684
|
+
// operator running `olam upgrade --substrate=kubernetes` against a fresh
|
|
685
|
+
// cluster hit "namespaces \"olam\" not found" / "Secret olam-host-cp-secret
|
|
686
|
+
// not found". Idempotent: reapply on subsequent runs reuses tokens from
|
|
687
|
+
// ~/.olam/k8s-secrets-state.json so worlds with cached values don't break.
|
|
688
|
+
// --rotate-secrets opts in to fresh value generation.
|
|
689
|
+
if (opts.runK8sBootstrap === true) {
|
|
690
|
+
const step05Spinner = ora('Bootstrapping olam namespace + RBAC + secrets (B4)').start();
|
|
691
|
+
const bootstrapImpl = deps.k8sBootstrapImpl ?? ensureK8sBootstrap;
|
|
692
|
+
// Thread the same kubectlWrap that the rest of upgrade-kubernetes uses so
|
|
693
|
+
// existing tests that inject `kubectlWrapImpl` don't reach the real kubectl
|
|
694
|
+
// binary via the bootstrap module's default.
|
|
695
|
+
const bootstrap = await bootstrapImpl({
|
|
696
|
+
context: pinnedContext,
|
|
697
|
+
namespace: K8S_NAMESPACE,
|
|
698
|
+
rotateSecrets: opts.rotateSecrets === true,
|
|
699
|
+
}, {
|
|
700
|
+
kubectlWrapImpl: deps.kubectlWrapImpl,
|
|
701
|
+
...(deps.k8sBootstrapDeps ?? {}),
|
|
702
|
+
});
|
|
703
|
+
if (bootstrap.exitCode !== 0) {
|
|
704
|
+
step05Spinner.fail('k8s bootstrap failed');
|
|
705
|
+
stderr.write(`${pc.red('error:')} ${bootstrap.error ?? 'unknown bootstrap error'}\n`);
|
|
706
|
+
return { exitCode: 1, summary: 'k8s bootstrap failed' };
|
|
707
|
+
}
|
|
708
|
+
const appliedCount = bootstrap.result.applied.length;
|
|
709
|
+
const skippedCount = bootstrap.result.skipped.length;
|
|
710
|
+
const note = skippedCount > 0 ? ` (${skippedCount} skipped — see warnings above)` : '';
|
|
711
|
+
step05Spinner.succeed(`k8s bootstrap: ${appliedCount} applied${note}`);
|
|
712
|
+
}
|
|
713
|
+
// ── Step 1: D10 — context-allowlist validation ───────────────────
|
|
714
|
+
// The context was already resolved (config-first, env-fallback) before
|
|
715
|
+
// pre-step 0a. Step 1 keeps the D10 allowlist gate + the audit WARN so
|
|
716
|
+
// observability of which context is being used is preserved.
|
|
477
717
|
const step1Spinner = ora('Verifying kubectl context (D10)').start();
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
step1Spinner.fail('Context ACK missing or disallowed');
|
|
482
|
-
stderr.write(`${pc.red('error:')} OLAM_K8S_CONTEXT_ACK is not set or empty.\n` +
|
|
483
|
-
` Set it to your kubectl context name (byte-for-byte match):\n` +
|
|
484
|
-
` export OLAM_K8S_CONTEXT_ACK=<your-context-name>\n` +
|
|
718
|
+
if (!isContextAllowed(pinnedContext)) {
|
|
719
|
+
step1Spinner.fail('Context disallowed');
|
|
720
|
+
stderr.write(`${pc.red('error:')} kubectl context ${pc.bold(pinnedContext)} is not allowed by the D10 allowlist.\n` +
|
|
485
721
|
` See GATES.md G-001 for the context-allowlist requirement.\n`);
|
|
486
|
-
return { exitCode: 1, summary: 'context
|
|
722
|
+
return { exitCode: 1, summary: 'context disallowed' };
|
|
487
723
|
}
|
|
488
|
-
// Capture the pinned context name (byte-for-byte, as provided).
|
|
489
|
-
const pinnedContext = ackValue;
|
|
490
724
|
// Audit WARN on bypass (D10: operator explicitly acknowledged a potentially unsafe context).
|
|
491
725
|
process.stderr.write(`${pc.yellow('[WARN]')} OLAM_K8S_UNSAFE_CONTEXT_ACK ctx=${pinnedContext}\n`);
|
|
492
726
|
step1Spinner.succeed(`Context pinned: ${pc.bold(pinnedContext)}`);
|