fix(asset_scanner): Pod→Deployment via ReplicaSet 橋樑 (relationship 漏掉修復)
All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 9m31s

Review 盲點: 實測 asset_relationship 52 筆,但都是 Pod→StatefulSet + Pod→ConfigMap,
完全沒有 Pod→Deployment!

真因:
  K8s 中 Pod.ownerReferences[0].kind = 'ReplicaSet' (99% 案例)
  Deployment 管 ReplicaSet 管 Pod (兩層 owner chain)
  原 code 只 match kind in (deployment/statefulset/daemonset) → 跳過 ReplicaSet
  → Pod→Deployment 關係全部漏掉

修復 v3.1:
  0. 新增 collect replicasets pass (僅作為 bridge,不寫 asset_inventory)
     建 rs_to_deployment map: {ns/rs_name: deployment_name}
  2. Pod ownerRef.kind='ReplicaSet' → 反查 rs_to_deployment → 建 Pod→Deployment

預期效果:
  - asset_relationship 從 52 → 150+ (所有 Deployment-managed Pod 都有 relationship)
  - OpenClaw blast_radius 計算 Deployment 影響的 Pod 數 = 正確

不寫 ReplicaSet 為 asset (他是 ephemeral 中介,滾動更新會大量產生,污染 inventory)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
OG T
2026-04-19 17:26:57 +08:00
parent c8b263db06
commit e677773e39

View File

@@ -345,16 +345,35 @@ async def _collect_all_k8s_assets() -> tuple[list[dict[str, Any]], list[dict[str
掃多種 K8s 資源類型 + 提取 relationship.
Relationships:
- Pod ─ depends_on ─> Deployment/StatefulSet/DaemonSet (via ownerReferences)
- Pod ─ depends_on ─> Deployment (via ReplicaSet 橋接 owner chain)
- Pod ─ depends_on ─> StatefulSet/DaemonSet (ownerReferences 直連)
- Service ─ routes_to ─> Pod (via spec.selector 匹配 Pod.labels)
- Pod ─ depends_on ─> ConfigMap (via spec.volumes[].configMap.name)
2026-04-19 ogt + Claude Opus 4.7 v3 bug fix:
Pod.ownerReferences[0].kind 99% 是 ReplicaSet (Deployment 管 ReplicaSet 管 Pod),
原 code 跳過 ReplicaSet → Pod→Deployment 全部漏掉.
修: 先掃 ReplicaSet 建 rs_to_deployment map,Pod 用 rs_name 反查 Deployment.
回傳 (assets, relationships) tuple.
relationships: [{'from_key': ..., 'to_key': ..., 'relationship_type': ...}, ...]
"""
assets: list[dict[str, Any]] = []
relationships: list[dict[str, str]] = []
# 0. ReplicaSets — 僅作為 Pod→Deployment 橋樑,不寫入 asset_inventory
rs_to_deployment: dict[str, str] = {} # "ns/rs_name" -> "deployment_name"
try:
payload = await _fetch_kubectl_json("replicasets")
for item in payload.get("items", []) or []:
meta = item.get("metadata", {}) or {}
rs_ns = meta.get("namespace") or "default"
rs_name = meta.get("name") or ""
for ref in meta.get("ownerReferences", []) or []:
if ref.get("kind", "").lower() == "deployment":
rs_to_deployment[f"{rs_ns}/{rs_name}"] = ref.get("name", "")
except Exception as e:
logger.warning("collect_replicasets_failed", error=str(e))
# 1. Nodes (不帶 ns)
try:
payload = await _fetch_kubectl_json("nodes", all_namespaces=False)
@@ -364,7 +383,7 @@ async def _collect_all_k8s_assets() -> tuple[list[dict[str, Any]], list[dict[str
logger.warning("collect_nodes_failed", error=str(e))
# 2. Pods — 主體 + 從 ownerReferences 建 relationship
pod_by_key: dict[str, dict[str, Any]] = {} # asset_key -> pod item (for Service selector match)
pod_by_key: dict[str, dict[str, Any]] = {}
try:
payload = await _fetch_kubectl_json("pods")
for item in payload.get("items", []) or []:
@@ -372,7 +391,6 @@ async def _collect_all_k8s_assets() -> tuple[list[dict[str, Any]], list[dict[str
assets.append(a)
pod_by_key[a["asset_key"]] = item
# OwnerReference → relationship (Pod ReplicaSet → Deployment 通常是兩層)
meta = item.get("metadata", {}) or {}
ns = meta.get("namespace") or "default"
for ref in meta.get("ownerReferences", []) or []:
@@ -380,13 +398,29 @@ async def _collect_all_k8s_assets() -> tuple[list[dict[str, Any]], list[dict[str
owner_name = ref.get("name", "")
if not owner_name:
continue
# 跳過 ReplicaSet (中介層),直接找到真正的 Deployment 在後續步驟補
if owner_kind in ("deployment", "statefulset", "daemonset"):
# StatefulSet/DaemonSet 直接 owner Pod,直接建 relationship
if owner_kind in ("statefulset", "daemonset"):
relationships.append({
"from_key": a["asset_key"],
"to_key": f"k8s/{owner_kind}/{ns}/{owner_name}",
"relationship_type": "depends_on",
})
# ReplicaSet 中介: 用 rs_to_deployment map 反查 Deployment
elif owner_kind == "replicaset":
deploy_name = rs_to_deployment.get(f"{ns}/{owner_name}")
if deploy_name:
relationships.append({
"from_key": a["asset_key"],
"to_key": f"k8s/deployment/{ns}/{deploy_name}",
"relationship_type": "depends_on",
})
# 極少數直接是 Deployment owner (舊版 K8s)
elif owner_kind == "deployment":
relationships.append({
"from_key": a["asset_key"],
"to_key": f"k8s/deployment/{ns}/{owner_name}",
"relationship_type": "depends_on",
})
# Pod volumes → ConfigMap relationship
for v in (item.get("spec", {}) or {}).get("volumes", []) or []: