Python for Infrastructure Automation›09 · Capstone: a fleet report tool

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.

Practitioner
Key wordscapstonemulti-clusterThreadPoolExecutortimeoutspartial failurereport formats

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

  1. Read contexts from the kubeconfig (all, or a --contexts list).
  2. For each cluster, with a per-cluster timeout: server version, node count, NotReady nodes, and pods in CrashLoopBackOff/ImagePullBackOff/Pending for longer than 10 minutes.
  3. Check clusters in parallel with a bounded pool (--workers, default 5).
  4. Output as table (default), json or csv.
  5. Exit codes: 0 all healthy, 1 findings, 2 usage error, 3 at least one cluster unreachable.
  6. 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_timeout keeps one slow API server from blocking a worker forever.
  • The broad except is deliberate here, at the boundary of one cluster: it's converted into a visible error in 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 json output is valid JSON on stdout only; logs go to stderr.
  • Tests cover healthy, findings and unreachable cases without real clusters.
  • pip install -e . provides the fleetreport command; ruff and pytest pass in CI.
  • The tool only needs read-only RBAC (get/list on 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.