Lesson 09 of 9 · Modules
Capstone: a fleet report tool
Build fleetreport: a tested, packaged CLI that checks every Kubernetes cluster in your kubeconfig in parallel (versions, node health, failing pods, certificate-style warnings) and outputs a table, JSON or CSV.
The brief
Platform teams often answer the same questions every week: which clusters run which version? Any nodes NotReady? Any pods crash-looping? Build fleetreport, a tool that answers them for every cluster in a kubeconfig, in parallel, and tolerates clusters that can't be reached.
You're the school inspector with 20 schools to visit before lunch. Instead of visiting one at a time, you send eight helpers at once, each with the same checklist. If a school's gate is locked, the helper writes "couldn't get in" and moves on, so one locked gate doesn't ruin the whole morning.
Requirements
- Read contexts from the kubeconfig (all, or a
--contextslist). - For each cluster, with a per-cluster timeout: server version, node count, NotReady nodes, and pods in
CrashLoopBackOff/ImagePullBackOff/Pendingfor longer than 10 minutes. - Check clusters in parallel with a bounded pool (
--workers, default 5). - Output as
table(default),jsonorcsv. - Exit codes: 0 all healthy, 1 findings, 2 usage error, 3 at least one cluster unreachable.
- Packaged with an entry point; pytest tests with the Kubernetes API mocked.
Skeleton
# src/fleetreport/core.py
from __future__ import annotations
from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass, field
from kubernetes import client, config
BAD_REASONS = {"CrashLoopBackOff", "ImagePullBackOff", "ErrImagePull"}
@dataclass
class ClusterResult:
context: str
version: str = "-"
nodes: int = 0
not_ready: list[str] = field(default_factory=list)
bad_pods: list[str] = field(default_factory=list)
error: str | None = None
@property
def healthy(self) -> bool:
return self.error is None and not self.not_ready and not self.bad_pods
def check_cluster(context: str, timeout: int) -> ClusterResult:
res = ClusterResult(context)
try:
api_client = config.new_client_from_config(context=context)
v1 = client.CoreV1Api(api_client)
res.version = client.VersionApi(api_client).get_code(_request_timeout=timeout).git_version
nodes = v1.list_node(_request_timeout=timeout).items
res.nodes = len(nodes)
for n in nodes:
ready = next((c.status for c in n.status.conditions if c.type == "Ready"), "Unknown")
if ready != "True":
res.not_ready.append(n.metadata.name)
for p in v1.list_pod_for_all_namespaces(_request_timeout=timeout).items:
for cs in p.status.container_statuses or []:
reason = cs.state.waiting.reason if cs.state and cs.state.waiting else None
if reason in BAD_REASONS:
res.bad_pods.append(f"{p.metadata.namespace}/{p.metadata.name} ({reason})")
# TODO: pods Pending for more than 10 minutes (compare p.status.start_time or creation time)
except Exception as exc: # a cluster failing must not stop the fleet report
res.error = f"{type(exc).__name__}: {exc}"[:200]
return res
def check_fleet(contexts: list[str], workers: int, timeout: int) -> list[ClusterResult]:
results = []
with ThreadPoolExecutor(max_workers=workers) as pool:
futures = {pool.submit(check_cluster, c, timeout): c for c in contexts}
for fut in as_completed(futures):
results.append(fut.result())
return sorted(results, key=lambda r: r.context)
# src/fleetreport/cli.py (outline)
# - argparse: --contexts, --workers, --timeout, -o/--output {table,json,csv}, -v
# - contexts: config.list_kube_config_contexts()[0] gives all contexts (names under ["name"])
# - print the report to stdout (table / json.dumps([asdict(r) ...]) / csv)
# - exit 3 if any r.error, else 1 if any not healthy, else 0
Notes:
config.new_client_from_config(context=…)builds an independent client per context, which is safe to use from separate threads._request_timeoutkeeps one slow API server from blocking a worker forever.- The broad
exceptis deliberate here, at the boundary of one cluster: it's converted into a visibleerrorin the report, never swallowed.
Testing without clusters
Mock check_cluster for the fleet logic, and mock the API objects for the per-cluster logic:
# tests/test_fleet.py
from fleetreport import core
def test_unreachable_cluster_is_reported(monkeypatch):
def fake_check(context, timeout):
if context == "broken":
return core.ClusterResult(context, error="ConnectionError: refused")
return core.ClusterResult(context, version="v1.31.2", nodes=3)
monkeypatch.setattr(core, "check_cluster", fake_check)
results = core.check_fleet(["prod", "broken"], workers=2, timeout=5)
assert [r.context for r in results] == ["broken", "prod"]
assert results[0].error and not results[0].healthy
assert results[1].healthy
Sample output
$ fleetreport
CONTEXT VERSION NODES NOT-READY BAD-PODS STATUS
kind-canary v1.31.0 3 0 0 OK
kind-dev v1.30.4 3 1 2 FINDINGS
edge-site-042 - - - - UNREACHABLE (ConnectTimeoutError)
$ echo $?
3
Review checklist
- Unreachable clusters are reported, not fatal, and the exit code reflects them.
- Per-cluster timeouts are enforced; a hung cluster doesn't hang the tool.
-o jsonoutput is valid JSON on stdout only; logs go to stderr.- Tests cover healthy, findings and unreachable cases without real clusters.
pip install -e .provides thefleetreportcommand;ruffandpytestpass in CI.- The tool only needs read-only RBAC (
get/liston nodes and pods).
Going deeper: where this goes next
- Run it as a CronJob in a management cluster and publish results as metrics (Prometheus Pushgateway or a small exporter), so dashboards and alerts replace the weekly manual check.
- Add certificate-expiry and deprecated-API checks (see Kubernetes Administration, lessons 09 and 25), and compare versions against your supported-version policy.
- When the tool starts making changes (cordon, restart), add dry-run by default, confirmation, and an audit log.
You've finished Python for Infrastructure
You can build real tools: safe subprocess use, APIs, Kubernetes and AWS automation, tested and packaged, running in parallel across a fleet. Next up in the foundations path: Kubernetes Administration.
This site is a public version of my personal engineering knowledge hub. It intentionally excludes confidential company information and internal operational details.