fix(asset_scanner): Pod→Deployment via ReplicaSet 橋樑 (relationship 漏掉修復)
All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 9m31s
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:
@@ -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 []:
|
||||
|
||||
Reference in New Issue
Block a user