Merge pull request #311 from pikasTech/fix/hwlab-1119-d601-pk01-migration

fix: 收敛 D601 HWLAB PK01 migration 路径
This commit is contained in:
Lyon
2026-06-13 07:44:39 +08:00
committed by GitHub
3 changed files with 88 additions and 3 deletions
+6 -1
View File
@@ -121,6 +121,11 @@ postgres:
user: sub2api
address: 10.0.8.0/22
method: scram-sha-256
- type: hostssl
database: hwlab_d601_v03
user: hwlab_d601_v03_app
address: 10.0.8.0/22
method: scram-sha-256
- type: hostssl
database: sub2api
user: sub2api
@@ -349,7 +354,7 @@ exports:
sourceSecretRef: platform-db/hwlab-d601-v03-db.env
render:
envKey: DATABASE_URL
format: postgresql://$(HWLAB_D601_V03_DB_USER):$(HWLAB_D601_V03_DB_PASSWORD)@$(PGHOST):5432/$(HWLAB_D601_V03_DB_NAME)?sslmode=require
format: postgresql://$(HWLAB_D601_V03_DB_USER):$(HWLAB_D601_V03_DB_PASSWORD)@$(PGHOST):5432/$(HWLAB_D601_V03_DB_NAME)?sslmode=require&uselibpqcompat=true
variables:
PGHOST: 82.156.23.220
writeToSecretSource:
+2
View File
@@ -24,6 +24,8 @@ CI/CD、GitOps、rollout、artifact 发布、PR 合并后的 runtime lane 滚动
`hwlab nodes control-plane infra tools-image status|build|logs --node D601 --lane v03` 是 D601 tools image 的受控入口。Dockerfile 必须由 `config/hwlab-node-control-plane.yaml``tekton.toolsImage.dockerfileInline` 声明,输入镜像必须列在 `publicBaseImages`,构建参数和网络模式也来自 YAMLconfirmed build 只在 D601 后台异步构建并推送到 node-local registry,返回 status/logs 轮询命令。`hwlab nodes control-plane infra argo status|apply|logs --node D601 --lane v03` 是 D601 Argo CD 的声明式安装入口。Argo 版本、官方 manifest URL、镜像 rewrite/preload、field manager、imagePullPolicy、CRD 列表、期望 Deployment/StatefulSet 以及生成的 AppProject/Application 都必须来自同一个 YAML`argo apply --confirm` 只执行可重复 server-side apply 和后台轮询,不把原生 `kubectl apply`、手工 Argo CLI 或临时 manifest 作为正式安装路径。
`hwlab nodes control-plane runtime-migration --node <node-id> --lane vNN [--dry-run|--allow-live-db-read --dry-run|--confirm]` 是 node-scoped runtime lane 的受控 schema migration 入口。它只通过目标 runtime namespace 当前 `deployment/hwlab-cloud-api -c hwlab-cloud-api` 内 repo-owned `cmd/hwlab-cloud-api/migrate.ts` 执行,输出 report path、source commit 和有界 stdout/stderr 摘要;不读取或打印 Secret 值、不手写 `psql`、不把 pod 内临时命令沉淀成正式流程。D601 v03 这类由 UniDesk YAML 声明的外置 PK01 PostgreSQL 切换,DB/Secret/bridge 仍以 UniDesk YAML 和 `platform-db postgres ...``hwlab nodes control-plane apply|trigger-current` 为 source truthruntime migration 只负责在已发布 runtime 上补齐应用 schema。
## Command Model
- `help` 输出命令索引,适合作为交互式入口。
+80 -2
View File
@@ -117,6 +117,8 @@ export function hwlabNodeHelp(): Record<string, unknown> {
"bun scripts/cli.ts hwlab nodes control-plane refresh --node G14 --lane v03 --confirm",
"bun scripts/cli.ts hwlab nodes control-plane sync --node D601 --lane v03 --confirm",
"bun scripts/cli.ts hwlab nodes control-plane trigger-current --node G14 --lane v03 --confirm",
"bun scripts/cli.ts hwlab nodes control-plane runtime-migration --node D601 --lane v03 --dry-run",
"bun scripts/cli.ts hwlab nodes control-plane runtime-migration --node D601 --lane v03 --confirm",
"bun scripts/cli.ts hwlab nodes control-plane allow-endpoint-bridge --node G14 --lane v03 --confirm",
"bun scripts/cli.ts hwlab nodes git-mirror status --node G14 --lane v03",
"bun scripts/cli.ts hwlab nodes secret status --node G14 --lane v03 --name hwlab-v03-openfga",
@@ -142,7 +144,7 @@ async function runNodeDelegatedDomain(config: Config, domain: DelegatedNodeDomai
}
if (domain === "control-plane" && scoped.node !== defaultSpec.nodeId) {
if (scoped.action === "status") return nodeRuntimeControlPlaneStatus(scoped);
if (scoped.action === "apply" || scoped.action === "trigger-current" || scoped.action === "refresh" || scoped.action === "sync") {
if (scoped.action === "apply" || scoped.action === "trigger-current" || scoped.action === "refresh" || scoped.action === "sync" || scoped.action === "runtime-migration") {
if (scoped.confirm && !scoped.dryRun && !scoped.wait) return startNodeDelegatedJob(scoped);
return nodeRuntimeControlPlaneRun(scoped);
}
@@ -183,6 +185,7 @@ function parseNodeScopedDelegatedOptions(domain: DelegatedNodeDomain, args: stri
dryRun: boolean;
wait: boolean;
rerun: boolean;
allowLiveDbRead: boolean;
timeoutSeconds: number;
originalArgs: string[];
spec: HwlabRuntimeLaneSpec;
@@ -206,6 +209,7 @@ function parseNodeScopedDelegatedOptions(domain: DelegatedNodeDomain, args: stri
dryRun,
wait: args.includes("--wait"),
rerun: args.includes("--rerun"),
allowLiveDbRead: args.includes("--allow-live-db-read"),
timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", 1800, 3600),
originalArgs: [...args],
spec,
@@ -343,7 +347,7 @@ function nodeRuntimeUnsupportedAction(scoped: ReturnType<typeof parseNodeScopedD
lane: scoped.lane,
mutation: false,
degradedReason: "unsupported-node-scoped-runtime-action",
message: "node-scoped runtime currently supports plan/status/apply/refresh/sync/trigger-current",
message: "node-scoped runtime currently supports plan/status/apply/refresh/sync/trigger-current/runtime-migration",
expected: nodeRuntimeExpected(scoped.spec),
};
}
@@ -558,9 +562,83 @@ function nodeRuntimeControlPlaneRun(scoped: ReturnType<typeof parseNodeScopedDel
if (scoped.action === "sync") return nodeRuntimeSync(scoped);
if (scoped.action === "apply") return nodeRuntimeApply(scoped);
if (scoped.action === "trigger-current") return nodeRuntimeTriggerCurrent(scoped);
if (scoped.action === "runtime-migration") return nodeRuntimeMigration(scoped);
return nodeRuntimeUnsupportedAction(scoped);
}
function nodeRuntimeMigration(scoped: ReturnType<typeof parseNodeScopedDelegatedOptions>): Record<string, unknown> {
const spec = scoped.spec;
if (scoped.allowLiveDbRead && scoped.confirm) throw new Error("control-plane runtime-migration accepts --allow-live-db-read only with dry-run/source-check mode, not --confirm");
if (!scoped.confirm && !scoped.dryRun) throw new Error("control-plane runtime-migration requires --dry-run or --confirm");
const head = resolveNodeRuntimeLaneHead(spec);
const sourceCommit = head.sourceCommit;
if (sourceCommit === null) {
return {
ok: false,
command: `hwlab nodes control-plane runtime-migration --node ${scoped.node} --lane ${scoped.lane}`,
node: scoped.node,
lane: scoped.lane,
phase: "source-head",
mutation: false,
degradedReason: "node-runtime-source-head-unresolved",
headProbe: compactRuntimeCommand(head.result),
};
}
const reportPath = `/tmp/hwlab-${scoped.node.toLowerCase()}-${scoped.lane}-runtime-migration-${shortSha(sourceCommit)}.json`;
const migrationArgs = scoped.dryRun
? [
...(scoped.allowLiveDbRead ? ["--dry-run", "--allow-live-db-read", "--confirm-dev"] : ["--check"]),
"--report",
reportPath,
]
: [
"--apply",
"--confirm-dev",
"--confirmed-non-production",
"--report",
reportPath,
];
const result = runNodeK3sArgs(spec, [
"kubectl",
"exec",
"-n",
spec.runtimeNamespace,
"deployment/hwlab-cloud-api",
"-c",
"hwlab-cloud-api",
"--",
"sh",
"-lc",
`cd /workspace/hwlab-boot/repo && exec bun cmd/hwlab-cloud-api/migrate.ts ${migrationArgs.map(shellQuote).join(" ")}`,
], scoped.timeoutSeconds);
const ok = isCommandSuccess(result);
return {
ok,
command: `hwlab nodes control-plane runtime-migration --node ${scoped.node} --lane ${scoped.lane}`,
node: scoped.node,
lane: scoped.lane,
mode: scoped.dryRun ? scoped.allowLiveDbRead ? "live-read-dry-run" : "source-check" : "confirmed-apply",
sourceCommit,
runtimeNamespace: spec.runtimeNamespace,
target: "deployment/hwlab-cloud-api -c hwlab-cloud-api",
workingDirectory: "/workspace/hwlab-boot/repo",
migrationCommand: ["bun", "cmd/hwlab-cloud-api/migrate.ts", ...migrationArgs],
reportPath,
mutation: !scoped.dryRun && ok,
result: compactRuntimeCommand(result),
degradedReason: ok ? undefined : "node-runtime-migration-failed",
next: scoped.dryRun
? {
liveReadDryRun: `bun scripts/cli.ts hwlab nodes control-plane runtime-migration --node ${scoped.node} --lane ${scoped.lane} --allow-live-db-read --dry-run`,
apply: `bun scripts/cli.ts hwlab nodes control-plane runtime-migration --node ${scoped.node} --lane ${scoped.lane} --confirm`,
}
: {
health: `curl -fsS --max-time 20 ${spec.publicApiUrl}/health/live`,
status: `bun scripts/cli.ts hwlab nodes control-plane status --node ${scoped.node} --lane ${scoped.lane}`,
},
};
}
function nodeRuntimeApply(scoped: ReturnType<typeof parseNodeScopedDelegatedOptions>): Record<string, unknown> {
const spec = scoped.spec;
const head = resolveNodeRuntimeLaneHead(spec);