diff --git a/docs/reference/cli.md b/docs/reference/cli.md index e7efb487..2768cb1b 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -55,7 +55,7 @@ CI/CD、GitOps、rollout、artifact 发布、PR 合并后的 DEV/PROD 滚动、P 创建 PipelineRun 前会读取 `devops-infra` mirror refs,若 `localV02` 未等于当前 source commit,则自动执行一次受控 manual `git-mirror sync` Job 并复核 ref,复核失败时停止触发,避免 Tekton `prepare-source` 已知失败;services 参数只包含 v02 runtime service matrix,`hwlab-cli` 是固定 repo 短连接源码工具,不进入 PipelineRun service build。 `--dry-run` 只报告是否会 pre-sync,不创建 Job;confirmed trigger 默认创建 `.state/jobs/` 异步 job 并立刻返回 `job.id`、`statusCommand`、stdout/stderr 路径,避免 git mirror pre-sync 或 PipelineRun 创建期间长时间阻塞;`--wait` 路径也必须向 stderr 输出 `hwlab.v02.trigger.progress` JSON 事件,覆盖 `control-plane-refresh`、`git-mirror-pre-sync` 和 `create-pipelinerun`,避免异步 job 长时间只有启动命令而无法判断卡点;默认 JSON 必须对 `manifest_b64`、长脚本和远端 stdout/stderr 做有界摘要,保留长度与 hash,最终 trigger 结果只返回阶段摘要和关键 tail,完整内容通过 job stdout/stderr 文件渐进披露;只有现场同步调试才显式加 `--wait`;旧 `rerun-current` 只作为输入别名保留。PipelineRun `Completed`、Argo `Synced/Healthy` 和 `webAssets.ok=true` 只证明 G14 runtime 已更新;交付收口还必须用 `hwlab g14 git-mirror status` 查看 `cache.summary.pendingFlush`,若为 true,继续执行受控 `hwlab g14 git-mirror flush --confirm` 并用 job status 轮询到 `pendingFlush=false`。 - `hwlab g14 control-plane runtime-migration --lane v02 [--dry-run|--allow-live-db-read --dry-run|--confirm]` 只通过 `hwlab-v02` namespace 当前 `deployment/hwlab-cloud-api -c hwlab-cloud-api` 内 repo-owned migration CLI 执行;不读取或打印 Secret 值、不触碰 PROD、不绕到手工 `psql`。 -- `hwlab g14 secret status|ensure --lane v02 --name hwlab-v02-openfga [--dry-run|--confirm]` 是 HWLAB v0.2 runtime OpenFGA SecretRef bootstrap 的标准入口,确保 OpenFGA preshared token、datastore URI、Postgres password 和对应数据库/角色存在;`status` 只返回 key 是否存在、解码后字节数和 DB 对象存在性,永远不读取、不打印、不回传 secret 明文。`hwlab g14 secret delete --lane v02 --name [--dry-run|--confirm]` 只用于删除确认已不被 workload 引用的 v0.2 废弃 Secret,默认 dry-run,拒绝删除 OpenFGA/Postgres 等必需 Secret;共享 device-pod API key 已退出当前授权路径,不再提供 ensure/bootstrap 入口。 +- `hwlab g14 secret status|ensure --lane v02 --name hwlab-v02-openfga|hwlab-v02-master-server-admin-api-key [--dry-run|--confirm]` 是 HWLAB v0.2 runtime SecretRef bootstrap 的标准入口:OpenFGA preset 确保 preshared token、datastore URI、Postgres password 和对应数据库/角色存在;master server admin API key preset 确保本机 `/root/.config/hwlab-v02/master-server-admin-api-key.env` 以 0600 保存 `HWLAB_API_KEY`,并同步到 `hwlab-v02-master-server-admin-api-key/api-key`。`status` 只返回 key 是否存在、解码后字节数、key prefix 和 DB 对象存在性,永远不读取、不打印、不回传 secret 明文。`hwlab g14 secret delete --lane v02 --name [--dry-run|--confirm]` 只用于删除确认已不被 workload 引用的 v0.2 废弃 Secret,默认 dry-run,拒绝删除 OpenFGA/Postgres/master admin API key 等必需 Secret;共享 device-pod API key 已退出当前授权路径,不再提供 ensure/bootstrap 入口。 - `hwlab g14 control-plane cleanup-runs --lane v02|g14|all [--min-age-minutes N] [--limit N] [--dry-run|--confirm]` 是完成态 PipelineRun 工作区 retention 入口;真实清理只删除已完成 PipelineRun,让 Tekton/local-path 回收临时 PVC,不触碰 registry storage、业务 PVC、Secret、runtime workload 或 GitOps desired state。 - `hwlab g14 control-plane cleanup-released-pvs --lane all [--limit N] [--dry-run|--confirm]` 是 local-path 未自动回收后的补充 retention 入口;只列并删除 `Released`、`local-path`、`Delete`、`claimNamespace=hwlab-ci` 且 claim 名称形如 Tekton 临时 `pvc-*` 的 PV。 - `hwlab g14 git-mirror status|apply|sync|flush [--dry-run|--confirm]` 是 `devops-infra` git mirror/relay 的受控维护入口:`apply` 渲染并 server-side apply `devops-infra/git-mirror.yaml`,同时删除遗留 `git-mirror-hwlab-sync` CronJob;`sync` 创建一次性 manual Job,把 GitHub allowlist refs 拉入本地 mirror;`flush` 创建一次性 manual Job,把本地 `v0.2-gitops` 快进推回 GitHub。 diff --git a/scripts/hwlab-g14-contract-test.ts b/scripts/hwlab-g14-contract-test.ts index 9b308e6f..7a587abf 100644 --- a/scripts/hwlab-g14-contract-test.ts +++ b/scripts/hwlab-g14-contract-test.ts @@ -53,8 +53,10 @@ assertCondition( ); assertCondition( hwlabHelpUsage.some((line) => line.includes("secret status --lane v02 --name hwlab-v02-openfga")) - && hwlabHelpUsage.some((line) => line.includes("secret ensure --lane v02 --name hwlab-v02-openfga --confirm")), - "v0.2 secret help must expose the controlled OpenFGA SecretRef bootstrap path", + && hwlabHelpUsage.some((line) => line.includes("secret ensure --lane v02 --name hwlab-v02-openfga --confirm")) + && hwlabHelpUsage.some((line) => line.includes("secret status --lane v02 --name hwlab-v02-master-server-admin-api-key")) + && hwlabHelpUsage.some((line) => line.includes("secret ensure --lane v02 --name hwlab-v02-master-server-admin-api-key --confirm")), + "v0.2 secret help must expose controlled OpenFGA and master-server admin API key SecretRef bootstrap paths", hwlabHelpUsage, ); assertCondition( diff --git a/scripts/src/command.ts b/scripts/src/command.ts index 92f5b8c0..dd052d78 100644 --- a/scripts/src/command.ts +++ b/scripts/src/command.ts @@ -11,11 +11,12 @@ export interface CommandResult { timedOut: boolean; } -export function runCommand(command: string[], cwd: string, options: { timeoutMs?: number; env?: NodeJS.ProcessEnv } = {}): CommandResult { +export function runCommand(command: string[], cwd: string, options: { timeoutMs?: number; env?: NodeJS.ProcessEnv; input?: string } = {}): CommandResult { const result = spawnSync(command[0], command.slice(1), { cwd, encoding: "utf8", env: options.env, + input: options.input, maxBuffer: 1024 * 1024 * 8, timeout: options.timeoutMs, }); diff --git a/scripts/src/hwlab-g14.ts b/scripts/src/hwlab-g14.ts index ca3361f0..b66e3f02 100644 --- a/scripts/src/hwlab-g14.ts +++ b/scripts/src/hwlab-g14.ts @@ -1,6 +1,6 @@ -import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs"; +import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; -import { createHash } from "node:crypto"; +import { createHash, randomBytes } from "node:crypto"; import { repoRoot, rootPath, type Config } from "./config"; import { runCommand } from "./command"; import { readJob, startJob } from "./jobs"; @@ -36,6 +36,9 @@ const V02_OPENFGA_DATASTORE_URI_SECRET_KEY = "datastore-uri"; const V02_OPENFGA_POSTGRES_PASSWORD_SECRET_KEY = "postgres-password"; const V02_OPENFGA_DB_NAME = "hwlab_openfga"; const V02_OPENFGA_DB_USER = "hwlab_openfga"; +const V02_MASTER_ADMIN_API_KEY_SECRET = "hwlab-v02-master-server-admin-api-key"; +const V02_MASTER_ADMIN_API_KEY_SECRET_KEY = "api-key"; +const V02_MASTER_ADMIN_API_KEY_LOCAL_ENV = "/root/.config/hwlab-v02/master-server-admin-api-key.env"; const V02_REGISTRY_PREFIX = "127.0.0.1:5000/hwlab"; const V02_BASE_IMAGE = "127.0.0.1:5000/hwlab/hwlab-node20-base:20-bookworm-slim"; const GIT_MIRROR_NAMESPACE = "devops-infra"; @@ -190,8 +193,8 @@ interface G14SecretOptions { dryRun: boolean; confirm: boolean; name: string; - key?: typeof V02_OPENFGA_AUTHN_SECRET_KEY | typeof V02_OPENFGA_DATASTORE_URI_SECRET_KEY | typeof V02_OPENFGA_POSTGRES_PASSWORD_SECRET_KEY; - preset: "openfga" | "generic-delete"; + key?: string; + preset: "openfga" | "master-server-admin-api-key" | "generic-delete"; timeoutSeconds: number; } @@ -503,7 +506,7 @@ function parseObservabilityOptions(args: string[]): G14ObservabilityOptions { function parseSecretOptions(args: string[]): G14SecretOptions { const [actionRaw] = args; if (actionRaw !== "status" && actionRaw !== "ensure" && actionRaw !== "delete") { - throw new Error("secret usage: status|ensure --lane v02 --name hwlab-v02-openfga [--dry-run|--confirm] | delete --lane v02 --name [--dry-run|--confirm]"); + throw new Error("secret usage: status|ensure --lane v02 --name hwlab-v02-openfga|hwlab-v02-master-server-admin-api-key [--dry-run|--confirm] | delete --lane v02 --name [--dry-run|--confirm]"); } const lane = optionValue(args, "--lane") ?? "v02"; if (lane !== "v02") throw new Error("secret currently supports --lane v02"); @@ -515,7 +518,7 @@ function parseSecretOptions(args: string[]): G14SecretOptions { if (actionRaw === "delete") { if (key !== undefined) throw new Error("secret delete does not accept --key; it deletes a whole obsolete Secret object"); if (!/^hwlab-v02-[a-z0-9-]+$/u.test(name)) throw new Error("secret delete requires a hwlab-v02-* Secret name"); - if (name === V02_OPENFGA_SECRET || name === "hwlab-v02-postgres") throw new Error(`secret delete refuses required v0.2 Secret ${name}`); + if (name === V02_OPENFGA_SECRET || name === V02_MASTER_ADMIN_API_KEY_SECRET || name === "hwlab-v02-postgres") throw new Error(`secret delete refuses required v0.2 Secret ${name}`); return { action: actionRaw, lane, @@ -526,8 +529,21 @@ function parseSecretOptions(args: string[]): G14SecretOptions { timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", 120, 600), }; } + if (name === V02_MASTER_ADMIN_API_KEY_SECRET) { + if (key !== undefined && key !== V02_MASTER_ADMIN_API_KEY_SECRET_KEY) throw new Error(`secret ${V02_MASTER_ADMIN_API_KEY_SECRET} supports only key ${V02_MASTER_ADMIN_API_KEY_SECRET_KEY}`); + return { + action: actionRaw, + lane, + confirm, + dryRun: actionRaw === "status" ? true : explicitDryRun || !confirm, + name, + key: key ?? V02_MASTER_ADMIN_API_KEY_SECRET_KEY, + preset: "master-server-admin-api-key", + timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", 120, 600), + }; + } if (name !== V02_OPENFGA_SECRET) { - throw new Error(`secret status/ensure currently supports only --name ${V02_OPENFGA_SECRET}; use secret delete for obsolete Secret objects`); + throw new Error(`secret status/ensure currently supports only --name ${V02_OPENFGA_SECRET} or ${V02_MASTER_ADMIN_API_KEY_SECRET}; use secret delete for obsolete Secret objects`); } if (key !== undefined && key !== V02_OPENFGA_AUTHN_SECRET_KEY && key !== V02_OPENFGA_DATASTORE_URI_SECRET_KEY && key !== V02_OPENFGA_POSTGRES_PASSWORD_SECRET_KEY) { throw new Error(`secret ${V02_OPENFGA_SECRET} supports keys ${V02_OPENFGA_AUTHN_SECRET_KEY}, ${V02_OPENFGA_DATASTORE_URI_SECRET_KEY}, and ${V02_OPENFGA_POSTGRES_PASSWORD_SECRET_KEY}`); @@ -577,6 +593,26 @@ function commandJson(command: string[], timeoutMs = 60_000): CommandJsonResult { }; } +function commandJsonWithInput(command: string[], input: string, timeoutMs = 60_000): CommandJsonResult { + const result = runCommand(command, repoRoot, { timeoutMs, input }); + let parsed: unknown | null = null; + if (result.stdout.trim().length > 0) { + try { + parsed = JSON.parse(result.stdout) as unknown; + } catch { + parsed = null; + } + } + return { + ok: result.exitCode === 0, + command, + exitCode: result.exitCode, + stdout: result.stdout, + stderr: result.stderr, + parsed, + }; +} + function cliJson(args: string[], timeoutMs = 60_000): CommandJsonResult { return commandJson(["bun", "scripts/cli.ts", ...args], timeoutMs); } @@ -636,8 +672,59 @@ function textHash(value: string): string { return createHash("sha256").update(value).digest("hex").slice(0, 12); } +function generateHwlabApiKey(): string { + return `hwl_live_${randomBytes(32).toString("base64url")}`; +} + +function hwlabApiKeyPrefix(value: string): string { + const text = value.trim(); + if (!text.startsWith("hwl_live_")) return ""; + const remainder = text.slice("hwl_live_".length); + const dot = remainder.indexOf("."); + const segment = dot === -1 ? remainder : remainder.slice(0, dot); + return `hwl_live_${segment}`.slice(0, 24); +} + +function localMasterAdminApiKeyStatus(): Record { + if (!existsSync(V02_MASTER_ADMIN_API_KEY_LOCAL_ENV)) { + return { exists: false, path: V02_MASTER_ADMIN_API_KEY_LOCAL_ENV, mode: null, valueBytes: 0, keyPrefix: null }; + } + const stat = statSync(V02_MASTER_ADMIN_API_KEY_LOCAL_ENV); + const content = readFileSync(V02_MASTER_ADMIN_API_KEY_LOCAL_ENV, "utf8"); + const match = content.match(/^HWLAB_API_KEY=(.+)$/mu); + const value = match?.[1]?.trim() ?? ""; + return { + exists: true, + path: V02_MASTER_ADMIN_API_KEY_LOCAL_ENV, + mode: `0${(stat.mode & 0o777).toString(8)}`, + valueBytes: Buffer.byteLength(value, "utf8"), + keyPrefix: value ? hwlabApiKeyPrefix(value) : null, + validPrefix: value.startsWith("hwl_live_"), + }; +} + +function readOrCreateLocalMasterAdminApiKey(dryRun: boolean): { key: string | null; created: boolean; status: Record } { + const existing = localMasterAdminApiKeyStatus(); + if (existing.exists === true) { + const content = readFileSync(V02_MASTER_ADMIN_API_KEY_LOCAL_ENV, "utf8"); + const match = content.match(/^HWLAB_API_KEY=(.+)$/mu); + const key = match?.[1]?.trim() ?? ""; + if (!key.startsWith("hwl_live_")) throw new Error(`${V02_MASTER_ADMIN_API_KEY_LOCAL_ENV} exists but does not contain a hwl_live_ HWLAB_API_KEY`); + if (!dryRun && existing.mode !== "0600") chmodSync(V02_MASTER_ADMIN_API_KEY_LOCAL_ENV, 0o600); + return { key, created: false, status: localMasterAdminApiKeyStatus() }; + } + if (dryRun) return { key: null, created: false, status: existing }; + const key = generateHwlabApiKey(); + mkdirSync(dirname(V02_MASTER_ADMIN_API_KEY_LOCAL_ENV), { recursive: true, mode: 0o700 }); + writeFileSync(V02_MASTER_ADMIN_API_KEY_LOCAL_ENV, `# HWLAB v0.2 master server admin API key; do not commit or print.\nHWLAB_API_KEY=${key}\n`, { mode: 0o600 }); + return { key, created: true, status: localMasterAdminApiKeyStatus() }; +} + function redactLargePayloads(value: string): string { return value + .replace(/hwl_live_[A-Za-z0-9._:-]{12,}/gu, (payload: string) => { + return `hwl_live_`; + }) .replace(/manifest_b64='([^']{128,})'/gu, (_match, payload: string) => { return `manifest_b64=''`; }) @@ -781,6 +868,10 @@ function g14K3s(args: string[], timeoutMs = 60_000): CommandJsonResult { return cliJson(["ssh", `${G14_PROVIDER}:k3s`, ...args], timeoutMs); } +function g14K3sInlineScriptWithInput(script: string, input: string, timeoutMs = 60_000): CommandJsonResult { + return commandJsonWithInput(["bun", "scripts/cli.ts", "ssh", `${G14_PROVIDER}:k3s`, "script", "--", script], input, timeoutMs); +} + function v02CicdRepoEnsureScript(): string { return [ `cicd_repo=${shellQuote(V02_CICD_REPO)}`, @@ -3346,6 +3437,7 @@ function runV02ControlPlane(options: G14ControlPlaneOptions): Record/dev/null 2>&1 && printf yes || printf no; }", + "secret_b64_key() { kubectl -n \"$namespace\" get secret \"$name\" -o \"go-template={{ index .data \\\"$1\\\" }}\" 2>/dev/null || true; }", + "decoded_length() { if [ -n \"$1\" ]; then printf '%s' \"$1\" | base64 -d 2>/dev/null | wc -c | tr -d ' '; else printf '0'; fi; }", + "decoded_prefix() { if [ -n \"$1\" ]; then value=$(printf '%s' \"$1\" | base64 -d 2>/dev/null || true); printf '%s' \"$value\" | cut -c1-24; value=; fi; }", + "before_exists=$(secret_exists_flag)", + "before_api_key_b64=$(secret_b64_key \"$api_key_name\")", + "before_api_key_present=$([ -n \"$before_api_key_b64\" ] && printf yes || printf no)", + "before_api_key_bytes=$(decoded_length \"$before_api_key_b64\")", + "before_api_key_prefix=$(decoded_prefix \"$before_api_key_b64\")", + "action=observed", + "mutation=false", + "apply_exit=", + "if [ \"$action_request\" = ensure ]; then", + " missing_secret=false", + " [ \"$before_api_key_present\" = yes ] && [ \"$before_api_key_bytes\" -gt 0 ] || missing_secret=true", + " if [ \"$dry_run\" = true ]; then", + " if [ \"$missing_secret\" = true ]; then action=would-ensure; else action=kept; fi", + " else", + " api_key=$(cat)", + " case \"$api_key\" in hwl_live_*) ;; *) action=api-key-invalid; apply_exit=43 ;; esac", + " if [ -z \"$apply_exit\" ]; then", + " kubectl -n \"$namespace\" create secret generic \"$name\" --from-literal=\"$api_key_name=$api_key\" --dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager=\"$field_manager\" -f -", + " apply_exit=$?", + " if [ \"$apply_exit\" -eq 0 ]; then action=ensured; mutation=true; else action=apply-failed; fi", + " fi", + " api_key=", + " fi", + "fi", + "after_exists=$(secret_exists_flag)", + "after_api_key_b64=$(secret_b64_key \"$api_key_name\")", + "after_api_key_present=$([ -n \"$after_api_key_b64\" ] && printf yes || printf no)", + "after_api_key_bytes=$(decoded_length \"$after_api_key_b64\")", + "after_api_key_prefix=$(decoded_prefix \"$after_api_key_b64\")", + "printf 'namespace\\t%s\\n' \"$namespace\"", + "printf 'secret\\t%s\\n' \"$name\"", + "printf 'key\\t%s\\n' \"$api_key_name\"", + "printf 'preset\\t%s\\n' \"$preset\"", + "printf 'action\\t%s\\n' \"$action\"", + "printf 'dryRun\\t%s\\n' \"$dry_run\"", + "printf 'mutation\\t%s\\n' \"$mutation\"", + "printf 'beforeExists\\t%s\\n' \"$before_exists\"", + "printf 'beforeApiKeyPresent\\t%s\\n' \"$before_api_key_present\"", + "printf 'beforeApiKeyBytes\\t%s\\n' \"$before_api_key_bytes\"", + "printf 'beforeApiKeyPrefix\\t%s\\n' \"$before_api_key_prefix\"", + "printf 'afterExists\\t%s\\n' \"$after_exists\"", + "printf 'afterApiKeyPresent\\t%s\\n' \"$after_api_key_present\"", + "printf 'afterApiKeyBytes\\t%s\\n' \"$after_api_key_bytes\"", + "printf 'afterApiKeyPrefix\\t%s\\n' \"$after_api_key_prefix\"", + "printf 'applyExitCode\\t%s\\n' \"$apply_exit\"", + "if [ -n \"$apply_exit\" ] && [ \"$apply_exit\" != 0 ]; then exit \"$apply_exit\"; fi", + ].join("\n"); +} + function v02SecretStatusFromText(text: string, commandOk: boolean, exitCode: number | null, stderr: string): Record { const fields = keyValueLinesFromText(text); if (fields.preset === "generic-delete") { @@ -3568,6 +3723,39 @@ function v02SecretStatusFromText(text: string, commandOk: boolean, exitCode: num : `${fields.secret || "secret"} still exists`, }; } + if (fields.preset === "master-server-admin-api-key") { + const afterBytes = numericField(fields.afterApiKeyBytes); + const healthy = + fields.afterExists === "yes" && + fields.afterApiKeyPresent === "yes" && + typeof afterBytes === "number" && afterBytes > 0 && + String(fields.afterApiKeyPrefix ?? "").startsWith("hwl_live_"); + return { + ok: commandOk && healthy, + namespace: fields.namespace || V02_RUNTIME_NAMESPACE, + secret: fields.secret || V02_MASTER_ADMIN_API_KEY_SECRET, + key: fields.key || V02_MASTER_ADMIN_API_KEY_SECRET_KEY, + preset: "master-server-admin-api-key", + action: fields.action || null, + dryRun: fields.dryRun === "true", + mutation: fields.mutation === "true", + before: { + exists: fields.beforeExists === "yes", + apiKey: { keyPresent: fields.beforeApiKeyPresent === "yes", valueBytes: numericField(fields.beforeApiKeyBytes), keyPrefix: fields.beforeApiKeyPrefix || null }, + }, + after: { + exists: fields.afterExists === "yes", + apiKey: { keyPresent: fields.afterApiKeyPresent === "yes", valueBytes: afterBytes, keyPrefix: fields.afterApiKeyPrefix || null }, + }, + applyExitCode: numericField(fields.applyExitCode), + exitCode, + stderr: commandOk ? "" : stderr.trim().slice(0, 2000), + valuesRedacted: true, + summary: healthy + ? `${fields.secret || V02_MASTER_ADMIN_API_KEY_SECRET}/${fields.key || V02_MASTER_ADMIN_API_KEY_SECRET_KEY} exists` + : `${fields.secret || V02_MASTER_ADMIN_API_KEY_SECRET}/${fields.key || V02_MASTER_ADMIN_API_KEY_SECRET_KEY} missing`, + }; + } if (fields.preset === "openfga") { const afterAuthnBytes = numericField(fields.afterAuthnBytes); const afterUriBytes = numericField(fields.afterDatastoreUriBytes); @@ -3643,7 +3831,12 @@ function v02SecretStatusFromText(text: string, commandOk: boolean, exitCode: num function runG14Secret(options: G14SecretOptions): Record { const script = v02SecretScript(options); - const result = g14K3s(["script", "--", script], options.timeoutSeconds * 1000); + const localAdminApiKey = options.preset === "master-server-admin-api-key" + ? readOrCreateLocalMasterAdminApiKey(options.action !== "ensure" || options.dryRun) + : null; + const result = options.preset === "master-server-admin-api-key" + ? g14K3sInlineScriptWithInput(script, localAdminApiKey?.key ?? "", options.timeoutSeconds * 1000 + 2000) + : g14K3s(["script", "--", script], options.timeoutSeconds * 1000); const status = v02SecretStatusFromText(statusText(result), isCommandSuccess(result), result.exitCode, result.stderr); const dryRunOk = options.action === "ensure" && options.dryRun && isCommandSuccess(result); const deleteDryRunOk = options.action === "delete" && options.dryRun && isCommandSuccess(result); @@ -3658,6 +3851,9 @@ function runG14Secret(options: G14SecretOptions): Record { preset: options.preset, mode: options.action === "status" ? "status" : options.dryRun ? "dry-run" : options.action === "delete" ? "confirmed-delete" : "confirmed-ensure", status, + localMasterServer: localAdminApiKey + ? { created: localAdminApiKey.created, envFile: localAdminApiKey.status, valuesRedacted: true } + : undefined, mutation: status.mutation === true, result: compactCommandResult(result), valuesRedacted: true, @@ -7458,6 +7654,8 @@ export function hwlabG14Help(): Record { "bun scripts/cli.ts hwlab g14 secret status --lane v02 --name hwlab-v02-openfga", "bun scripts/cli.ts hwlab g14 secret ensure --lane v02 --name hwlab-v02-openfga --dry-run", "bun scripts/cli.ts hwlab g14 secret ensure --lane v02 --name hwlab-v02-openfga --confirm", + "bun scripts/cli.ts hwlab g14 secret status --lane v02 --name hwlab-v02-master-server-admin-api-key", + "bun scripts/cli.ts hwlab g14 secret ensure --lane v02 --name hwlab-v02-master-server-admin-api-key --confirm", "bun scripts/cli.ts hwlab g14 secret delete --lane v02 --name --dry-run", "bun scripts/cli.ts hwlab g14 secret delete --lane v02 --name --confirm", "bun scripts/cli.ts hwlab g14 git-mirror status",