From fe035b8fcefa06010c56b43796ba6f8dbf7a33d9 Mon Sep 17 00:00:00 2001 From: Brad Stein Date: Thu, 29 Jan 2026 09:23:13 -0300 Subject: [PATCH] snapshot: add node version facts --- atlasbot/snapshot/builder.py | 54 ++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/atlasbot/snapshot/builder.py b/atlasbot/snapshot/builder.py index b274900..a1aea51 100644 --- a/atlasbot/snapshot/builder.py +++ b/atlasbot/snapshot/builder.py @@ -77,6 +77,7 @@ def build_summary(snapshot: dict[str, Any] | None) -> dict[str, Any]: summary.update(_build_pressure(snapshot)) summary.update(_build_hardware(nodes_detail)) summary.update(_build_hardware_by_node(nodes_detail)) + summary.update(_build_node_facts(nodes_detail)) summary.update(_build_node_ages(nodes_detail)) summary.update(_build_node_taints(nodes_detail)) summary.update(_build_capacity(metrics)) @@ -167,6 +168,41 @@ def _build_node_ages(nodes_detail: list[dict[str, Any]]) -> dict[str, Any]: return {"node_ages": ages[:5]} if ages else {} +def _count_values(nodes_detail: list[dict[str, Any]], key: str) -> dict[str, int]: + counts: dict[str, int] = {} + for node in nodes_detail or []: + if not isinstance(node, dict): + continue + value = node.get(key) + if isinstance(value, str) and value: + counts[value] = counts.get(value, 0) + 1 + return counts + + +def _build_node_facts(nodes_detail: list[dict[str, Any]]) -> dict[str, Any]: + if not nodes_detail: + return {} + role_counts: dict[str, int] = {} + for node in nodes_detail: + if not isinstance(node, dict): + continue + if node.get("is_worker"): + role_counts["worker"] = role_counts.get("worker", 0) + 1 + roles = node.get("roles") + if isinstance(roles, list): + for role in roles: + if isinstance(role, str) and role: + role_counts[role] = role_counts.get(role, 0) + 1 + return { + "node_arch_counts": _count_values(nodes_detail, "arch"), + "node_os_counts": _count_values(nodes_detail, "os"), + "node_kubelet_versions": _count_values(nodes_detail, "kubelet"), + "node_kernel_versions": _count_values(nodes_detail, "kernel"), + "node_runtime_versions": _count_values(nodes_detail, "container_runtime"), + "node_role_counts": role_counts, + } + + def _build_node_taints(nodes_detail: list[dict[str, Any]]) -> dict[str, Any]: taints: dict[str, list[str]] = {} for node in nodes_detail or []: @@ -466,6 +502,23 @@ def _append_node_taints(lines: list[str], summary: dict[str, Any]) -> None: lines.append("node_taints: " + "; ".join(sorted(parts))) +def _append_node_facts(lines: list[str], summary: dict[str, Any]) -> None: + def top_counts(label: str, counts: dict[str, int], limit: int = 4) -> None: + if not counts: + return + top = sorted(counts.items(), key=lambda item: (-item[1], item[0]))[:limit] + rendered = "; ".join([f"{name}={count}" for name, count in top]) + if rendered: + lines.append(f"{label}: {rendered}") + + top_counts("node_arch", summary.get("node_arch_counts") or {}) + top_counts("node_os", summary.get("node_os_counts") or {}) + top_counts("node_kubelet_versions", summary.get("node_kubelet_versions") or {}) + top_counts("node_kernel_versions", summary.get("node_kernel_versions") or {}) + top_counts("node_runtime_versions", summary.get("node_runtime_versions") or {}) + top_counts("node_roles", summary.get("node_role_counts") or {}) + + def _append_pressure(lines: list[str], summary: dict[str, Any]) -> None: pressure = summary.get("pressure_nodes") if not isinstance(pressure, dict) or not pressure: @@ -1142,6 +1195,7 @@ def summary_text(snapshot: dict[str, Any] | None) -> str: _append_nodes(lines, summary) _append_pressure(lines, summary) _append_hardware(lines, summary) + _append_node_facts(lines, summary) _append_node_ages(lines, summary) _append_node_taints(lines, summary) _append_capacity(lines, summary)