From e677773e397654d517bc1c08e048f2a99f33f101 Mon Sep 17 00:00:00 2001 From: OG T Date: Sun, 19 Apr 2026 17:26:57 +0800 Subject: [PATCH] =?UTF-8?q?fix(asset=5Fscanner):=20Pod=E2=86=92Deployment?= =?UTF-8?q?=20via=20ReplicaSet=20=E6=A9=8B=E6=A8=91=20(relationship=20?= =?UTF-8?q?=E6=BC=8F=E6=8E=89=E4=BF=AE=E5=BE=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- apps/api/src/jobs/asset_scanner_job.py | 46 ++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/apps/api/src/jobs/asset_scanner_job.py b/apps/api/src/jobs/asset_scanner_job.py index 2aa88ffc..be1d8d5f 100644 --- a/apps/api/src/jobs/asset_scanner_job.py +++ b/apps/api/src/jobs/asset_scanner_job.py @@ -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 []: