Appearance
从一个真实 Pending Pod 到受控 Patch 和 Ready
先把两次运行区分开:第一次从新建夹具开始验证证据链;第二次从已经恢复的 Deployment 重新注入同一故障,检验旧 Ready 副本会不会骗过验证节点。保留 deployment generation、Pod UID 和任务 ID,就能把这两轮的记录分开。
sequenceDiagram Watcher->>Graph: 按 UID 和 generation 建任务 Graph->>MCP: 查询 Pod 和同 UID 事件 MCP-->>Graph: FailedScheduling 与 selector Graph-->>Human: 暂停并展示证据 Human->>Graph: 批准 Graph->>API: 按版本条件提交修改 API-->>Graph: 新副本满足验收条件
目标不是让 Agent 把任意 Pending 都修好,而是证明一个窄动作足够可靠:只移除 opspilot-lab/pending-lab 上精确匹配的教学错误 selector。以下仅针对该夹具,不能套在有真实业务的 Deployment 上。首次按环境章节加载镜像及 RBAC。
先按部署章启动专用集群并加载 CPU 镜像。以下命令从项目根目录执行,仅操作实验 namespace:
sh
export KUBECONFIG="$PWD/tmp/handbook.kubeconfig"
kubectl apply -f opspilot/cluster/lab.yaml -f opspilot/cluster/rbac.json
kubectl -n opspilot-lab get pod -l app=pending-lab
.venv/bin/python opspilot/cluster/run.py start预期看到 Pending,以及 proposal、selector_mismatch、pods/events/deployment 三类来源。把输出 id 记作 RUN。若刚创建、事件尚未出现,可能得到 insufficient_evidence;先查 Pod conditions,再新开诊断,不能直接跳过证据门槛。可选本地模型:设置 PLANNER=llm、LLM_BASE_URL、LLM_MODEL;使用支持 tools 的模型,托管 API 还需 LLM_API_KEY。
sh
.venv/bin/python opspilot/cluster/run.py show "$RUN"
.venv/bin/python opspilot/cluster/run.py approve "$RUN"
kubectl -n opspilot-lab get deploy,pod每条命令都是新进程。批准前图在 interrupt 边界暂停,SqliteSaver 用 thread_id 恢复快照。拒绝用 reject;恢复执行错误用 retry,它不会跳过审批。批准后构造 JSON Patch,同时 test UID、resourceVersion 和精确故障字段,在同一次 API 操作里移除 selector、写操作 ID。重复回复丢失时检查同 UID 的注解确认已执行;名字相同但 UID 不同要重新诊断。若部署对象同时被修改,patch test 失败,不盲目覆盖新意图。
verify 回读 observedGeneration、updated/available/total replicas 和 UID。1 个旧 Ready Pod 不足以证明新修复就绪,升级时旧新 Pod 可能共存。实际本地记录中专用 Pending 示例修复后新 Pod 为 Running、Ready 1/1。再次实验用 lab.yaml 恢复注入条件,并重新检查实际 selector;手动移除字段后 kubectl apply 未必重写上次提交过的相同值,可显式 patch 注入。
做第二轮时,显式重新注入并关联证据
默认的 rolling update 可能保留旧 Ready 副本。查询所有带标签的 Pod 并找未调度的那个,不能只取列表第一项;先等新 Pod 的 FailedScheduling 事件,再触发任务。
sh
export KUBECONFIG="$PWD/tmp/handbook.kubeconfig" PLANNER=rules
kubectl -n opspilot-lab patch deployment pending-lab --type=merge -p '{"spec":{"template":{"spec":{"nodeSelector":{"lab.invalid/pool":"absent"}}}}}'
kubectl -n opspilot-lab get pod -l app=pending-lab -o wide
kubectl -n opspilot-lab get events --field-selector reason=FailedScheduling
mkdir -p tmp
export OPS_TRACE_FILE="$PWD/tmp/pending-trace.jsonl"
.venv/bin/python opspilot/cluster/watch.py --cycles 1
# 将上一行输出的任务 UUID 赋给 RUN;若它显示 healthy,等事件后再执行
.venv/bin/python opspilot/cluster/run.py show "$RUN"
.venv/bin/python opspilot/cluster/run.py approve "$RUN"
kubectl -n opspilot-lab rollout status deployment/pending-lab
kubectl -n opspilot-lab get pods -l app=pending-labwatch 以 Deployment UID 与 generation 构造任务 ID,因此同一 revision 再次轮询不会重复开任务。CLI start 则显式开启新任务。一次练习选一种入口,不要对同一故障开启多个并发审批。拒绝分支可以用 reject 代替 approve;用 CLI 拒绝后故障仍在,重新 start 并审批方可恢复。
本轮真实轨迹(rules,CPU Kind)
按序观测 pods → events → deployment;状态 proposal,citations 包含三项;decision=true,修复返回 replay=false,verify 最终 resolved。校验的是目标 UID 和新 generation 的 updated/available/total 副本均为 1,不能拿旧 Ready Pod 冒充修复。完整六条事件可在本轮轨迹查看。若权限不足或出现陈旧计划,保留现场查 RBAC/UID;不要直接去除其他标签。用专属部署演示后正常批准会移除错误 selector。
完整源码:opspilot/cluster/run.py · 第 99–111 行
python
def repair(s):
if not s["approved"] or not sufficient(s["observations"]):raise ValueError("approval and evidence required")
d=s["observations"]["deployment"];result=call("release_selector",uid=d["uid"],generation=d["generation"],operation=s["id"])
return {"trace":s["trace"]+[result],"status":"verifying"}
def verify(s):
for i in range(30):
d=call("evidence",source="deployment");st=d["status"]
if (d["uid"]==s["observations"]["deployment"]["uid"] and not d["selector"] and st.get("observedGeneration")==d["generation"] and st.get("updatedReplicas")==1 and st.get("availableReplicas")==1 and st.get("replicas")==1):
memory("save",{"id":s["id"],"category":"selector_mismatch","source":"opspilot-lab/pending-lab UID "+d["uid"]})
return {"verified":d,"status":"resolved","trace":s["trace"]+[{"verified_at":time.time(),"polls":i+1}]}
time.sleep(1)
return {"status":"verification_failed","verified":d}
def graph(conn):本轮还单独验证了 watch 入口:对同一个 Pending revision 连续扫描两次,得到同一个任务 UUID;批准这一个任务后重新达到 resolved。