fix: 披露 NC01 留存策略安装漂移

This commit is contained in:
Codex
2026-07-16 11:08:36 +02:00
parent c0418d6920
commit 08241061a8
6 changed files with 89 additions and 4 deletions
+2
View File
@@ -151,6 +151,8 @@ bun scripts/cli.ts gc remote <providerId> status --job-id <id>
- 活跃、过新、未知分组、时间不可解析、扫描截断和 Table 形状漂移全部保护;
- 只删除 YAML 指定的根对象,通过 ownerRef 级联,不直接删除下游 TaskRun、Pod、Secret 或 Event
- 自动回收由 `gc remote <providerId> policy plan|install --confirm|status` 管理,周期和单批上限由 YAML 控制。
- `policy status` 必须一次披露期望与安装态的配置/runner 指纹、关键留存字段和 `inSync`
- owning YAML 变化后只通过 `policy install --confirm` 收敛安装态,禁止直接修改 host JSON、runner 或 systemd unit。
## WebProbe 低内存人工处置
+1 -1
View File
@@ -245,7 +245,7 @@ gc:
keepLatestPerGroup: 3
manualMaxDeletePerRun: 500
automaticMaxDeletePerRun: 100
previewLimit: 10
previewLimit: 5
scanLimit: 5000
requestTimeoutSeconds: 45
deleteBatchSize: 50
@@ -28,6 +28,9 @@
#### R1.2.4 [completed]
完成 [UniDesk #1710](https://github.com/pikasTech/unidesk/issues/1710) 的 Kafka 周边内存调优:保持纯 Kafka 实时/回放 SSE 架构不变,把 broker、Strimzi Cluster Operator、Topic/User Entity Operator 的 JVM 与 Kubernetes 资源预算,以及临时 consumer group 生命周期收敛到 `config/platform-infra/kafka.yaml`;经受控渲染、正常 PR 自动交付后验证 Kafka topic/client/producer/consumer 正常并对比物理内存,完成任务后将详细报告写入[任务报告](./details/web-observe-runtime-reliability/R1.2.4_Task_Report.md)。
#### R1.2.5 [in_progress]
完成 [UniDesk #1710](https://github.com/pikasTech/unidesk/issues/1710) 的 NC01 CI 历史对象自动留存对齐与 k3s 内存降载:修复 YAML 期望与 host timer 安装态不可见、不同步问题,使 `gc remote <node> policy status` 一次披露配置/runner 指纹、关键留存字段和漂移结论;通过 YAML-first 自动留存回收终态 PipelineRun owner 树,验证对象数量、k3s 内存与真实 `MemAvailable`,明确不触碰 Webterm,完成任务后将详细报告写入[任务报告](./details/web-observe-runtime-reliability/R1.2.5_Task_Report.md)。
### R1.3
测试与验收:https://github.com/pikasTech/unidesk/issues/1842;覆盖生命周期、stale/protected 分类和失败路径,提交独立 PR;合并后只等待自动 CI/CD 发布链,使用正式入口验证 observer/Chrome 退出、内存压力缓解且健康运行面不受扰动,完成任务后将详细报告写入[任务报告](./details/web-observe-runtime-reliability/R1.3_Task_Report.md)。
+5
View File
@@ -151,6 +151,11 @@ PK01 pikanode temp retention 只允许清理 `/home/ubuntu/pikanode/html/temp`
- 不直接删除 TaskRun、Pod、Secret 或 Event
- 手动入口提交异步 job 后必须用 `status --job-id` 查询终态;
- 自动 systemd timer 复用同一筛选器,并把每轮结果写入 YAML 指定的状态文件。
- 策略安装态核对固定使用 `gc remote NC01 policy status`
- `policySync` 必须同时披露期望与安装态的配置/runner SHA-256、字节数和关键留存字段;
- `inSync=false` 时只通过 `gc remote NC01 policy install --confirm` 从 owning YAML 重装,禁止直接修改 host JSON 或 Python runner
- owning YAML 的周期、保留窗口或阶段配置发生变化后,必须重新安装策略,并以 `policySync.inSync=true` 作为配置收敛证据。
- `retention plan` 的默认分组与候选预览数量由同一 YAML 的 `previewLimit` 控制,默认输出必须保留摘要且不触发 stdout 截断;完整明细只在显式 `--full` 时披露。
## JD01 远端策略
@@ -476,9 +476,9 @@ def run_kubernetes_object_retention_policy(config):
"id": "kubernetes-object-retention",
"ok": result.get("ok") is True,
"skipped": result.get("mutation") is not True,
"configId": result.get("configId"),
"namespace": result.get("namespace"),
"resource": result.get("resource"),
"configId": result.get("configId") or retention.get("configId"),
"namespace": result.get("namespace") or retention.get("namespace"),
"resource": result.get("resource") or retention.get("resource"),
"candidateCount": ((result.get("plan") or {}).get("summary") or {}).get("eligibleCount"),
"requestedDeleteCount": result.get("requestedDeleteCount"),
"deletedCount": result.get("deletedCount"),
+75
View File
@@ -1,6 +1,7 @@
import base64
import calendar
import fcntl
import hashlib
import json
import os
import re
@@ -1964,6 +1965,7 @@ def remote_policy_status_payload(observed_at):
last = None
if last is not None and not bool(OPTIONS.get("full")):
last = compact_policy_last_run(last)
policy_sync = remote_policy_sync_payload(rendered)
return {
"ok": True,
"action": "gc remote policy status",
@@ -1976,6 +1978,7 @@ def remote_policy_status_payload(observed_at):
"timer": bounded(timer),
"service": bounded(service),
},
"policySync": policy_sync,
"lastRun": last,
"next": {
"plan": "bun scripts/cli.ts gc remote %s policy plan" % PROVIDER_ID,
@@ -1984,6 +1987,78 @@ def remote_policy_status_payload(observed_at):
},
}
def sha256_summary(data):
return {
"bytes": len(data),
"sha256": hashlib.sha256(data).hexdigest(),
}
def file_sha256_summary(path):
try:
with open(path, "rb") as handle:
data = handle.read()
return {"present": True, **sha256_summary(data)}
except FileNotFoundError:
return {"present": False, "bytes": None, "sha256": None, "error": "not-found"}
except Exception as exc:
return {"present": False, "bytes": None, "sha256": None, "error": str(exc)}
def retention_policy_summary(config):
retention = config.get("kubernetesObjectRetention") if isinstance(config, dict) else None
if not isinstance(retention, dict):
return None
return {
key: retention.get(key)
for key in [
"enabled",
"configId",
"namespace",
"resource",
"minAgeHours",
"keepLatestPerGroup",
"automaticMaxDeletePerRun",
]
}
def remote_policy_sync_payload(rendered):
desired_config_data = (json.dumps(rendered["policyConfig"], sort_keys=True, separators=(",", ":")) + "\n").encode("utf-8")
desired_script_data = rendered["script"].encode("utf-8")
desired_config = sha256_summary(desired_config_data)
desired_script = sha256_summary(desired_script_data)
installed_config = file_sha256_summary(rendered["configPath"])
installed_script = file_sha256_summary(rendered["scriptPath"])
installed_policy = None
installed_config_error = None
try:
with open(rendered["configPath"], "r", encoding="utf-8") as handle:
installed_policy = json.load(handle)
except Exception as exc:
installed_config_error = str(exc)
config_in_sync = installed_config.get("sha256") == desired_config.get("sha256")
runner_in_sync = installed_script.get("sha256") == desired_script.get("sha256")
drift = []
if not config_in_sync:
drift.append("policy-config-drift")
if not runner_in_sync:
drift.append("policy-runner-drift")
if installed_config_error is not None:
drift.append("installed-config-unreadable")
return {
"inSync": config_in_sync and runner_in_sync and installed_config_error is None,
"drift": drift,
"desired": {
"config": desired_config,
"runner": desired_script,
"retention": retention_policy_summary(rendered["policyConfig"]),
},
"installed": {
"config": installed_config,
"runner": installed_script,
"retention": retention_policy_summary(installed_policy),
"configError": installed_config_error,
},
}
def compact_policy_last_run(last):
results = []
for item in last.get("results") or []: