From 58a9c8dfb3ef85fa755e899a698f074eeb186697 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 5 Jun 2026 06:17:59 +0000 Subject: [PATCH] fix: remove hwlab v02 device pod key bootstrap --- docs/reference/cli.md | 2 +- scripts/src/hwlab-g14.ts | 190 +++++++++++++++++++-------------------- 2 files changed, 95 insertions(+), 97 deletions(-) diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 10103586..e7efb487 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-device-pod-api-key --key api-key [--dry-run|--confirm]` 是 HWLAB v0.2 runtime SecretRef bootstrap 的标准入口,用于确保 `deploy/deploy.json` 中 `HWLAB_DEVICE_POD_API_KEY=secretRef:hwlab-v02-device-pod-api-key/api-key` 对应的 Kubernetes Secret 存在。`status` 只返回 secret/key 是否存在和解码后的字节数;`ensure --dry-run` 只报告会创建还是保持;`ensure --confirm` 在 G14 k3s 侧生成随机值并 server-side apply Secret。该命令永远不读取、不打印、不回传 secret 明文,也不提供手工值注入、fallback session token 或临时 lease 路径。 +- `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 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/src/hwlab-g14.ts b/scripts/src/hwlab-g14.ts index f86e8131..1532b8c4 100644 --- a/scripts/src/hwlab-g14.ts +++ b/scripts/src/hwlab-g14.ts @@ -30,8 +30,6 @@ const V02_GITOPS_BRANCH = "v0.2-gitops"; const V02_CATALOG_PATH = "deploy/artifact-catalog.v02.json"; const V02_RUNTIME_PATH = "deploy/gitops/g14/runtime-v02"; const V02_RUNTIME_NAMESPACE = "hwlab-v02"; -const V02_DEVICE_POD_API_KEY_SECRET = "hwlab-v02-device-pod-api-key"; -const V02_DEVICE_POD_API_KEY_SECRET_KEY = "api-key"; const V02_OPENFGA_SECRET = "hwlab-v02-openfga"; const V02_OPENFGA_AUTHN_SECRET_KEY = "authn-preshared-key"; const V02_OPENFGA_DATASTORE_URI_SECRET_KEY = "datastore-uri"; @@ -189,13 +187,13 @@ interface G14ObservabilityOptions { } interface G14SecretOptions { - action: "status" | "ensure"; + action: "status" | "ensure" | "delete"; lane: "v02"; dryRun: boolean; confirm: boolean; - name: typeof V02_DEVICE_POD_API_KEY_SECRET | typeof V02_OPENFGA_SECRET; - key?: typeof V02_DEVICE_POD_API_KEY_SECRET_KEY | typeof V02_OPENFGA_AUTHN_SECRET_KEY | typeof V02_OPENFGA_DATASTORE_URI_SECRET_KEY | typeof V02_OPENFGA_POSTGRES_PASSWORD_SECRET_KEY; - preset: "device-pod-api-key" | "openfga"; + 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"; timeoutSeconds: number; } @@ -506,34 +504,44 @@ function parseObservabilityOptions(args: string[]): G14ObservabilityOptions { function parseSecretOptions(args: string[]): G14SecretOptions { const [actionRaw] = args; - if (actionRaw !== "status" && actionRaw !== "ensure") { - throw new Error("secret usage: status|ensure --lane v02 --name hwlab-v02-device-pod-api-key --key api-key | --name hwlab-v02-openfga [--dry-run|--confirm]"); + 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]"); } const lane = optionValue(args, "--lane") ?? "v02"; if (lane !== "v02") throw new Error("secret currently supports --lane v02"); - const name = optionValue(args, "--name") ?? V02_DEVICE_POD_API_KEY_SECRET; - if (name !== V02_DEVICE_POD_API_KEY_SECRET && name !== V02_OPENFGA_SECRET) { - throw new Error(`secret currently supports --name ${V02_DEVICE_POD_API_KEY_SECRET} or ${V02_OPENFGA_SECRET}`); - } + const name = optionValue(args, "--name") ?? V02_OPENFGA_SECRET; const key = optionValue(args, "--key"); - const preset = name === V02_OPENFGA_SECRET ? "openfga" : "device-pod-api-key"; - if (preset === "device-pod-api-key") { - const effectiveKey = key ?? V02_DEVICE_POD_API_KEY_SECRET_KEY; - if (effectiveKey !== V02_DEVICE_POD_API_KEY_SECRET_KEY) throw new Error(`secret ${V02_DEVICE_POD_API_KEY_SECRET} supports --key ${V02_DEVICE_POD_API_KEY_SECRET_KEY}`); - } else 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}`); - } const confirm = args.includes("--confirm"); const explicitDryRun = args.includes("--dry-run"); if (confirm && explicitDryRun) throw new Error("secret accepts only one of --confirm or --dry-run"); + 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}`); + return { + action: actionRaw, + lane, + confirm, + dryRun: explicitDryRun || !confirm, + name, + preset: "generic-delete", + 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`); + } + 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}`); + } return { action: actionRaw, lane, confirm, dryRun: actionRaw === "status" ? true : explicitDryRun || !confirm, name, - key: preset === "device-pod-api-key" ? V02_DEVICE_POD_API_KEY_SECRET_KEY : key, - preset, + key, + preset: "openfga", timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", 120, 600), }; } @@ -3310,66 +3318,46 @@ function runV02ControlPlane(options: G14ControlPlaneOptions): Record/dev/null 2>&1 && printf yes || printf no; }", - "secret_b64() { kubectl -n \"$namespace\" get secret \"$name\" -o \"go-template={{ index .data \\\"$key\\\" }}\" 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; }", "before_exists=$(secret_exists_flag)", - "before_b64=$(secret_b64)", - "before_key_present=$([ -n \"$before_b64\" ] && printf yes || printf no)", - "before_value_bytes=$(decoded_length \"$before_b64\")", + "delete_exit=", "action=observed", "mutation=false", - "apply_exit=", - "if [ \"$action_request\" = ensure ]; then", - " if [ \"$dry_run\" = true ]; then", - " if [ \"$before_key_present\" = yes ] && [ \"$before_value_bytes\" -gt 0 ]; then action=kept; else action=would-create; fi", - " elif [ \"$before_key_present\" = yes ] && [ \"$before_value_bytes\" -gt 0 ]; then", - " action=kept", + "if [ \"$dry_run\" = true ]; then", + " if [ \"$before_exists\" = yes ]; then action=would-delete; else action=already-absent; fi", + "else", + " kubectl -n \"$namespace\" delete secret \"$name\" --ignore-not-found=true >/tmp/hwlab-secret-delete.out 2>/tmp/hwlab-secret-delete.err", + " delete_exit=$?", + " if [ \"$delete_exit\" -eq 0 ]; then", + " if [ \"$before_exists\" = yes ]; then action=deleted; mutation=true; else action=already-absent; fi", " else", - " if ! command -v openssl >/dev/null 2>&1; then", - " action=openssl-missing", - " apply_exit=127", - " else", - " generated_key=$(openssl rand -base64 48)", - " kubectl -n \"$namespace\" create secret generic \"$name\" --from-literal=\"$key=$generated_key\" --dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager=\"$field_manager\" -f -", - " apply_exit=$?", - " generated_key=", - " if [ \"$apply_exit\" -eq 0 ]; then action=ensured; mutation=true; else action=apply-failed; fi", - " fi", + " action=delete-failed", " fi", "fi", - emitAfterStatus, - "if [ -n \"$apply_exit\" ] && [ \"$apply_exit\" != 0 ]; then exit \"$apply_exit\"; fi", + "after_exists=$(secret_exists_flag)", + "printf 'namespace\\t%s\\n' \"$namespace\"", + "printf 'secret\\t%s\\n' \"$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 'afterExists\\t%s\\n' \"$after_exists\"", + "printf 'deleteExitCode\\t%s\\n' \"$delete_exit\"", + "if [ -n \"$delete_exit\" ] && [ \"$delete_exit\" != 0 ]; then exit \"$delete_exit\"; fi", ].join("\n"); } @@ -3528,6 +3516,31 @@ function v02OpenFgaSecretScript(options: G14SecretOptions): string { function v02SecretStatusFromText(text: string, commandOk: boolean, exitCode: number | null, stderr: string): Record { const fields = keyValueLinesFromText(text); + if (fields.preset === "generic-delete") { + const absent = fields.afterExists !== "yes"; + return { + ok: commandOk && absent, + namespace: fields.namespace || V02_RUNTIME_NAMESPACE, + secret: fields.secret || null, + preset: "generic-delete", + action: fields.action || null, + dryRun: fields.dryRun === "true", + mutation: fields.mutation === "true", + before: { + exists: fields.beforeExists === "yes", + }, + after: { + exists: fields.afterExists === "yes", + }, + deleteExitCode: numericField(fields.deleteExitCode), + exitCode, + stderr: commandOk ? "" : stderr.trim().slice(0, 2000), + valuesRedacted: true, + summary: absent + ? `${fields.secret || "secret"} absent` + : `${fields.secret || "secret"} still exists`, + }; + } if (fields.preset === "openfga") { const afterAuthnBytes = numericField(fields.afterAuthnBytes); const afterUriBytes = numericField(fields.afterDatastoreUriBytes); @@ -3586,35 +3599,18 @@ function v02SecretStatusFromText(text: string, commandOk: boolean, exitCode: num : `${fields.secret || V02_OPENFGA_SECRET} keys or Postgres database missing`, }; } - const afterExists = fields.afterExists === "yes"; - const afterKeyPresent = fields.afterKeyPresent === "yes"; - const afterValueBytes = numericField(fields.afterValueBytes); - const healthy = afterExists && afterKeyPresent && typeof afterValueBytes === "number" && afterValueBytes > 0; return { - ok: commandOk && healthy, + ok: false, namespace: fields.namespace || V02_RUNTIME_NAMESPACE, - secret: fields.secret || V02_DEVICE_POD_API_KEY_SECRET, - key: fields.key || V02_DEVICE_POD_API_KEY_SECRET_KEY, + secret: fields.secret || null, + preset: fields.preset || null, action: fields.action || null, dryRun: fields.dryRun === "true", mutation: fields.mutation === "true", - before: { - exists: fields.beforeExists === "yes", - keyPresent: fields.beforeKeyPresent === "yes", - valueBytes: numericField(fields.beforeValueBytes), - }, - after: { - exists: afterExists, - keyPresent: afterKeyPresent, - valueBytes: afterValueBytes, - }, - applyExitCode: numericField(fields.applyExitCode), exitCode, - stderr: commandOk ? "" : stderr.trim().slice(0, 2000), + stderr: stderr.trim().slice(0, 2000), valuesRedacted: true, - summary: healthy - ? `${fields.secret || V02_DEVICE_POD_API_KEY_SECRET}/${fields.key || V02_DEVICE_POD_API_KEY_SECRET_KEY} exists` - : `${fields.secret || V02_DEVICE_POD_API_KEY_SECRET}/${fields.key || V02_DEVICE_POD_API_KEY_SECRET_KEY} missing or empty`, + summary: "unrecognized secret status output", }; } @@ -3623,7 +3619,8 @@ function runG14Secret(options: G14SecretOptions): Record { const result = 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 ok = dryRunOk ? true : status.ok === true; + const deleteDryRunOk = options.action === "delete" && options.dryRun && isCommandSuccess(result); + const ok = dryRunOk || deleteDryRunOk ? true : status.ok === true; return { ok, command: `hwlab g14 secret ${options.action} --lane v02`, @@ -3632,14 +3629,16 @@ function runG14Secret(options: G14SecretOptions): Record { secret: options.name, key: options.key ?? null, preset: options.preset, - mode: options.action === "status" ? "status" : options.dryRun ? "dry-run" : "confirmed-ensure", + mode: options.action === "status" ? "status" : options.dryRun ? "dry-run" : options.action === "delete" ? "confirmed-delete" : "confirmed-ensure", status, mutation: status.mutation === true, result: compactCommandResult(result), valuesRedacted: true, next: ok && options.action === "status" ? undefined - : { ensure: `bun scripts/cli.ts hwlab g14 secret ensure --lane v02 --name ${options.name}${options.key ? ` --key ${options.key}` : ""} --confirm` }, + : options.action === "delete" + ? undefined + : { ensure: `bun scripts/cli.ts hwlab g14 secret ensure --lane v02 --name ${options.name}${options.key ? ` --key ${options.key}` : ""} --confirm` }, }; } @@ -7427,12 +7426,11 @@ export function hwlabG14Help(): Record { "bun scripts/cli.ts hwlab g14 control-plane runtime-migration --lane v02 --dry-run", "bun scripts/cli.ts hwlab g14 control-plane runtime-migration --lane v02 --allow-live-db-read --dry-run", "bun scripts/cli.ts hwlab g14 control-plane runtime-migration --lane v02 --confirm", - "bun scripts/cli.ts hwlab g14 secret status --lane v02 --name hwlab-v02-device-pod-api-key --key api-key", - "bun scripts/cli.ts hwlab g14 secret ensure --lane v02 --name hwlab-v02-device-pod-api-key --key api-key --dry-run", - "bun scripts/cli.ts hwlab g14 secret ensure --lane v02 --name hwlab-v02-device-pod-api-key --key api-key --confirm", "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 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", "bun scripts/cli.ts hwlab g14 git-mirror apply --confirm", "bun scripts/cli.ts hwlab g14 git-mirror sync --confirm",