diff --git a/.agents/skills/unidesk-ymalops/SKILL.md b/.agents/skills/unidesk-ymalops/SKILL.md index fd400110..8aaac9be 100644 --- a/.agents/skills/unidesk-ymalops/SKILL.md +++ b/.agents/skills/unidesk-ymalops/SKILL.md @@ -98,6 +98,10 @@ description: UniDesk YAML-first 运维正规化技能。用户提到 ymal-first/ - control-plane drop-in 只走 `hwlab nodes control-plane infra k3s-config` 窄域: - 渲染结果一致且节点容量已收敛时返回 `mutation=false`; - 只在必要时写入 drop-in、reload 和重启 k3s; + - 确需重启 server 时,先停止服务并备份 k3s 派生 + `server/cred/passwd`;禁止用该文件与 datastore 的 + mtime/freshness 预判阻断变更,备份只输出 + `blocking=false` warning; - WireGuard 内容一致时不重启,API/容量读取暂时失败时保持 unknown 并停止 mutation; - worker agent 使用 YAML 派生摘要决定是否重装,禁止每次 apply 先卸载再覆盖; - 禁止借用全量 infra apply 顺带覆盖 registry、CI/CD 或业务运行面。 diff --git a/docs/reference/platform-infra.md b/docs/reference/platform-infra.md index 7dd3da81..ec5beb06 100644 --- a/docs/reference/platform-infra.md +++ b/docs/reference/platform-infra.md @@ -33,6 +33,12 @@ - control-plane 节点配置只组合 `hwlab nodes control-plane infra k3s-config plan|status|apply`: - 该窄域只比较并安装 owning YAML 渲染的 k3s systemd drop-in; - 只在 drop-in 或节点 pod 容量需要收敛时重启 k3s; + - 确需重启 k3s server 时,受控入口先停止服务并备份 + `/var/lib/rancher/k3s/server/cred/passwd` 派生文件,再启动服务; + - 禁止用 `passwd` 与 `state.db` 的文件 mtime、datastore freshness + 预判或其他凭据新旧检查阻断 YAML-first 变更;备份动作必须以 + `blocking=false` warning 披露,只有真实 stop/start、Node Ready + 或期望状态收敛失败才可使 apply 失败; - 同一入口等待节点 Ready; - 禁止调用 HWLAB 全量 `infra apply`,也不得触碰 Kubernetes manifests、registry、Tekton、Argo、git-mirror 或业务 rollout。 - 运行边界: diff --git a/project-management/PJ2026-01/specs/PJ2026-01060308-cicd-yaml-first-target-governance.md b/project-management/PJ2026-01/specs/PJ2026-01060308-cicd-yaml-first-target-governance.md index 527f12bd..0dfbcea2 100644 --- a/project-management/PJ2026-01/specs/PJ2026-01060308-cicd-yaml-first-target-governance.md +++ b/project-management/PJ2026-01/specs/PJ2026-01060308-cicd-yaml-first-target-governance.md @@ -251,6 +251,18 @@ PipelineRun source step 不得以 mutable `refs/heads/` 作为本轮源 status/closeout 必须同时展示 selected snapshot、PipelineRun snapshot、latest mirror head、GitOps revision 和 runtime provenance。branch 在发布过程中前进只能标记为 `latest-drift` 或 `superseded`,不得把 selected commit 的成功发布误判为本轮失败。 +### 6.8 OPS-TARGET-REQ-008 k3s server 重启门禁非阻塞 + +| 编号 | 短名 | 主责模块 | 关联模块 | +| --- | --- | --- | --- | +| OPS-TARGET-REQ-008 | k3s重启 | PJ2026-01060308 YAML目标治理 | [平台运维](PJ2026-0106-platform-ops.md)、[控制面模块化](PJ2026-01060307-control-plane-modularity.md) | + +k3s server 受控重启不得以派生凭据文件与 datastore 的 mtime 或 +freshness 预判阻断 YAML-first 变更。入口应在停止服务后备份派生 +`server/cred/passwd`,并以 `blocking=false` warning 披露;只有真实启停、 +节点就绪或期望状态收敛失败才可使 apply 失败。不得重新引入凭据新旧 +preflight、CI gate 或人工确认门禁。 + ## 7. 过程控制 本规格的执行 issue 为 [#911](https://github.com/pikasTech/unidesk/issues/911)。源码文件头部应标注 `SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets`;自动生成文件、纯配置、锁文件和无法承载注释头的二进制产物可例外。 diff --git a/scripts/src/hwlab-node-control-plane-runtime.ts b/scripts/src/hwlab-node-control-plane-runtime.ts index 64560a2d..1d62a679 100644 --- a/scripts/src/hwlab-node-control-plane-runtime.ts +++ b/scripts/src/hwlab-node-control-plane-runtime.ts @@ -246,6 +246,8 @@ fi mutation=false daemon_reload_rc=0 restart_rc=0 +credential_backup_path= +credential_backup_rc=0 if [ "$apply" = true ] && [ "$restart_required" = true ]; then mkdir -p "$(dirname "$dropin")" if [ "$changed" = true ]; then @@ -255,12 +257,15 @@ if [ "$apply" = true ] && [ "$restart_required" = true ]; then fi systemctl daemon-reload || daemon_reload_rc=$? if [ "$daemon_reload_rc" = 0 ]; then - state_db=/var/lib/rancher/k3s/server/db/state.db derived_passwd=/var/lib/rancher/k3s/server/cred/passwd - if [ -f "$state_db" ] && [ -f "$derived_passwd" ] && [ "$derived_passwd" -nt "$state_db" ]; then - mv "$derived_passwd" "$derived_passwd.unidesk-pre-restart-$(date -u +%Y%m%dT%H%M%SZ)" + systemctl stop "$service" || restart_rc=$? + if [ "$restart_rc" = 0 ] && [ -f "$derived_passwd" ]; then + credential_backup_path="$derived_passwd.unidesk-pre-restart-$(date -u +%Y%m%dT%H%M%SZ)-$$" + mv "$derived_passwd" "$credential_backup_path" || credential_backup_rc=$? + fi + if [ "$restart_rc" = 0 ]; then + systemctl start "$service" || restart_rc=$? fi - systemctl restart "$service" || restart_rc=$? fi mutation=true fi @@ -290,15 +295,25 @@ if [ -f "$dropin" ]; then after_sha=$(sha256sum "$dropin" | awk '{print "sha256: after_capacity=$(kubectl get node "$node" -o 'jsonpath={.status.capacity.pods}' 2>/dev/null || true) after_allocatable=$(kubectl get node "$node" -o 'jsonpath={.status.allocatable.pods}' 2>/dev/null || true) service_active=$(systemctl is-active "$service" 2>/dev/null || true) -python3 - "$apply" "$mutation" "$changed" "$restart_required" "$daemon_reload_rc" "$restart_rc" "$before_sha" "$after_sha" "$expected_sha" "$service_active" "$ready" "$before_capacity" "$before_allocatable" "$after_capacity" "$after_allocatable" "$desired_max_pods" <<'PY' +python3 - "$apply" "$mutation" "$changed" "$restart_required" "$daemon_reload_rc" "$restart_rc" "$credential_backup_path" "$credential_backup_rc" "$before_sha" "$after_sha" "$expected_sha" "$service_active" "$ready" "$before_capacity" "$before_allocatable" "$after_capacity" "$after_allocatable" "$desired_max_pods" <<'PY' import json, sys apply, mutation, changed, restart_required = [value == "true" for value in sys.argv[1:5]] daemon_reload_rc, restart_rc = int(sys.argv[5]), int(sys.argv[6]) -before_sha, after_sha, expected_sha = sys.argv[7:10] -service_active, ready = sys.argv[10], sys.argv[11] == "true" -before_capacity, before_allocatable, after_capacity, after_allocatable, desired = sys.argv[12:17] +credential_backup_path, credential_backup_rc = sys.argv[7], int(sys.argv[8]) +before_sha, after_sha, expected_sha = sys.argv[9:12] +service_active, ready = sys.argv[12], sys.argv[13] == "true" +before_capacity, before_allocatable, after_capacity, after_allocatable, desired = sys.argv[14:19] def number(value): return int(value) if value.isdigit() else None +warnings = [] +if credential_backup_path: + warnings.append({ + "code": "k3s-derived-credential-backed-up", + "blocking": False, + "message": "已在受控重启前备份 k3s 派生 passwd,避免 datastore freshness 门禁阻断变更", + "backupPath": credential_backup_path, + "backupExitCode": credential_backup_rc, + }) ok = ( after_sha == expected_sha and service_active == "active" @@ -316,6 +331,7 @@ print(json.dumps({ "restartRequired": restart_required, "daemonReloadExitCode": daemon_reload_rc, "restartExitCode": restart_rc, + "warnings": warnings, "beforeDropInSha256": before_sha or None, "dropInSha256": after_sha or None, "expectedDropInSha256": expected_sha,