upgradescope

module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 11, 2026 License: Apache-2.0

README

upgradescope

Continuous Kubernetes upgrade-readiness for everyone — the standalone, Apache-2.0 alternative to commercial "operational safety" platforms.

Status: v0.1 — all four phases shipped. Point-in-time scan, continuous agent + ClusterReadiness CRD, self-hosted server (SQLite/Postgres) with embedded web dashboard, fleet rollups, per-team scores, CI gate + GitHub Action, auditor exports, community registry pipeline. Design: docs/superpowers/specs/2026-06-10-upgradescope-design.md.

demo

Install

go install github.com/abd-ulbasit/upgradescope/cmd/upgradescope@latest

Quickstart

# Scan the current kubeconfig context for readiness against Kubernetes 1.36
upgradescope scan --target 1.36

# Machine-readable report
upgradescope scan --target 1.36 --output json

# CI annotation: SARIF output + gate (exit 2 if any blocker, exit 1 on error)
upgradescope scan --target 1.36 --output sarif --fail-on blocker > scan.sarif

# Offline / CI: scan rendered manifests instead of a live cluster
upgradescope scan --target 1.36 --files ./rendered-manifests

Exit codes: 0 ready (or below --fail-on threshold), 1 scan error, 2 gate failed.

The readiness score

The score is deterministic and explainable — same inventory, same knowledge base, same number:

score = max(0, 100 − min(75, 25 × blockers) − min(20, 5 × warnings))
ready = (blockers == 0)        # what --fail-on blocker gates on

Blockers are findings that break at the target version (a removed API in use, an EOL add-on); warnings break one version later or are approaching EOL; info findings are listed but never scored. Caps keep one noisy category from zeroing the score.

Continuous mode (P2)

Run the agent in-cluster via the Helm chart — it keeps a ClusterReadiness resource up to date and (optionally) pushes to a self-hosted server:

helm install upgradescope ./deploy/chart -n upgradescope --create-namespace \
  --set server.enabled=true        # single-cluster all-in-one

kubectl get ucr                    # ClusterReadiness: TARGET / SCORE / READY
curl -s $SERVER/api/v1/clusters    # fleet API: clusters, findings, history, what-if (?target=1.38)
  • Agent is read-only (plus status on its own CRD); works with no server at all.
  • Version-skew checks cover kubelet, HA apiserver spread, controller-manager/scheduler, and kube-proxy; kubectl skew is out of scope by design — client versions only appear in apiserver audit logs, which upgradescope never reads.
  • Server: single binary + SQLite (Postgres in P3), Slack/webhook alerts on new blockers only.
  • Auth: bearer tokens (--ingest-token, optional --read-token).

Integration tests are env-gated: make demo-up && make it; full agent e2e: make agent-e2e (kind + Helm + Docker/Colima required).

Fleet mode (P3)

With several clusters pushing to one server, the read API rolls them up:

# Cluster × target score matrix (latest stored evaluations; no recompute)
curl -s "$SERVER/api/v1/fleet?targets=1.37,1.38"

# Per-team rollup across the fleet: worst score, total blockers, affected clusters
curl -s "$SERVER/api/v1/fleet/teams?target=1.38"

# Per-team scores for one cluster (also embedded as `teams` in /report)
curl -s "$SERVER/api/v1/clusters/1/teams?target=1.38"

Team attribution comes from namespace labels (--team-label, default team), optionally overridden server-side with serve --team-map teams.yaml:

# first matching glob wins; namespaces matching nothing keep their label
- pattern: "payments-*"
  team: payments
- pattern: "kube-*"
  team: platform

The CLI table output grows a TEAMS section whenever at least one finding is attributed to a named team.

Dashboard (P4)

serve embeds a web dashboard at /: the fleet score matrix, per-cluster drill-down (findings with category/team filters, score trend, team table), and a browser for the add-on registry this binary evaluates against (GET /api/v1/registry).

upgradescope dashboard

make web && make build      # build the SPA, stage it for go:embed, build the binary
./bin/upgradescope serve --ingest-token $TOKEN
# open http://localhost:8080/
  • No runtime JS dependencies beyond react/react-dom; charts are hand-rolled SVG.
  • If the server runs with --read-token, set the token in the dashboard header (stored in browser localStorage, sent as a bearer header).
  • Without make web the binary still builds and serves the API; go build -tags nodashboard skips the embed entirely.
  • Dashboard dev loop: cd web && npm run dev (Vite proxies /api to :8080).

CI gate

Two ways to block a PR that adds a removed API:

1. CLI (no server needed):

helm template ./chart > rendered.yaml   # or kustomize build
upgradescope scan --files . --target 1.38 --output sarif --fail-on blocker > results.sarif

2. Server gate endpoint — evaluates manifests inside a known cluster's context (its add-ons, version skew, and namespace→team labels are merged in; the manifests replace the cluster's API usage):

curl -sf -X POST "$SERVER/api/v1/gate?target=1.38&cluster=prod-eu-1&format=sarif" \
  -H "Authorization: Bearer $READ_TOKEN" \
  -H "Content-Type: application/x-yaml" \
  --data-binary @rendered.yaml > results.sarif

Omit cluster= to evaluate the manifests standalone; format=json (default) returns the full report.

GitHub Action (composite, in action/):

jobs:
  upgrade-gate:
    runs-on: ubuntu-latest
    permissions:
      security-events: write   # for SARIF upload
    steps:
      - uses: actions/checkout@v4
      - run: helm template ./chart --output-dir rendered
      - uses: abd-ulbasit/upgradescope/action@main
        id: gate
        with:
          path: rendered
          target: "1.38"
          fail-on: blocker
      - uses: github/codeql-action/upload-sarif@v3
        if: always()           # annotate the PR even when the gate fails
        with:
          sarif_file: ${{ steps.gate.outputs.sarif-file }}

Auditor export

One self-contained artifact per cluster per target — no JS, no CDN, prints cleanly:

# findings as CSV (one row per finding, citations included)
curl -s "$SERVER/api/v1/clusters/1/export?target=1.38&format=csv" -o report.csv

# single-file HTML report: score badge, findings by severity, score-history sparkline
curl -s "$SERVER/api/v1/clusters/1/export?target=1.38&format=html" -o report.html

Exports always reflect the latest stored evaluation (what the system actually recorded, with its evaluatedAt), never an on-the-fly recompute.

Ingest tokens

  • Dev / single cluster: one shared serve --ingest-token <secret> for all agents.
  • Fleet: per-cluster tokens — upgradescope tokens create <cluster> --db ... prints a token (once) valid only for that cluster name (403 on mismatch); revoke with upgradescope tokens revoke <cluster>. The shared --ingest-token keeps working as a back-compat/dev path.
  • Postgres backend: serve --db-url postgres://... (mutually exclusive with --db).

The problem

Upgrading a Kubernetes cluster safely requires answering, continuously, questions that today are answered by one-shot CLIs or expensive commercial tools:

  • Which workloads still use APIs that are deprecated or removed in my target version?
  • Which of my add-ons (controllers, CRDs, charts) are end-of-life or unmaintained — e.g. teams that missed that Ingress NGINX hit EOL in March 2026 and now fail compliance scans?
  • Is my version skew (kubelets vs control plane vs clients) within policy?
  • Are my Helm releases compatible with the target Kubernetes version?
  • Can I prove all of the above to a compliance auditor, per team, over time?

Open-source answers (pluto, kubent) are point-in-time CLI scans that depend on where manifests live and require manual wiring into CI. The continuous, in-cluster, fleet-aware version of this is commercial-only.

What upgradescope is

A self-hosted service + in-cluster agent that continuously watches what actually runs in your clusters, evaluates it against a curated knowledge base (API deprecations/removals per Kubernetes version, add-on EOL data, chart compatibility), and produces:

  • a readiness score per cluster per target version, broken down by team/namespace,
  • a live findings feed (what breaks at 1.37? what's EOL today?),
  • compliance-friendly reports and a CI gate webhook ("block this PR — it adds a removed API").

Architecture

One binary, three subcommands, one pure evaluation core embedded everywhere:

                    knowledge base (versioned with the code)
                    ├── internal/kb/data/apilifecycle.json   ← gen-kb ← k8s.io/api source
                    └── registry/data/*.yaml (cited)         ← eol-sync ← endoflife.date API
                                      │
                                      ▼
   CLI / CI                engine.Evaluate(inventory, kb, target) → report
   ───────────             (pure, deterministic, golden-file tested)
   upgradescope scan ──────────┐      ▲      ┌────────────────────────────────┐
   (kubeconfig or --files,     │      │      │ in-cluster (Helm chart)        │
    table/json/sarif,          │      └──────│ upgradescope agent             │
    exit codes for gating)     │             │  · read-only collectors       ─┼─► apiserver,
                               │             │  · ClusterReadiness CRD status │   metrics, Helm
                               ▼             └───────────────┬────────────────┘
                        ┌─────────────────────────┐          │ push (bearer token,
                        │ upgradescope serve      │◄─────────┘  per-cluster)
                        │  SQLite / Postgres      │
                        │  /api/v1: fleet matrix, │
                        │  reports, history, gate,│
                        │  CSV/HTML export        │
                        └─────────────────────────┘

The agent degrades gracefully: each collector (objects, apiserver metrics, Helm, nodes) fails independently and the report says what it could not see.

Managed clusters (EKS/GKE/AKS)

Zero config — point scan or the agent at a managed cluster and it works. What changes because the provider owns the control plane:

  • Deprecated-calls metric degrades — providers often block the apiserver /metrics endpoint regardless of your RBAC. The capability is then reported as unavailable with the reason apiserver /metrics forbidden (managed control planes often block this; …). You lose the active caller signal (who is still hitting a deprecated API); the static signal (which deprecated/removed objects exist) is unaffected.
  • Control-plane pods are absent from kube-system on managed offerings. This costs nothing: version skew is always computed from the API server version and node kubelet versions, never from control-plane pods.

Everything else — object inventory, API lifecycle findings, add-on EOL, Helm chart compatibility, scoring, CRD status, server push — works identically on managed and self-managed clusters.

Measured numbers

Measured 2026-06-11 on an Apple-silicon MacBook (Docker via Colima), against the kind demo cluster (single node, Kubernetes v1.35, demo workloads + EOL ingress-nginx installed):

What Measured
upgradescope scan --target 1.36 against the live demo cluster 0.22–0.30 s wall (3 runs)
Release binary, darwin/arm64 (-ldflags "-s -w") 52 MB (75 MB unstripped)
Docker image (multi-stage, distroless static, nonroot) 47 MB
API lifecycle dataset 160 entries from k8s.io/api v0.36.1
Add-on registry 18 add-ons, every claim cited

The knowledge base stays fresh by itself

  • tools/gen-kb regenerates the API lifecycle dataset from upstream k8s.io/api source (the same generated lifecycle methods the apiserver uses) — never hand-copied tables.
  • tools/eol-sync syncs registry entries that declare an endoflife_product slug against the live endoflife.date API; CI fails on drift (make eol-check).
  • A weekly GitHub Actions cron (kb-refresh.yml) bumps k8s.io/api, reruns both tools, and opens a reviewable PR — no silent dataset changes.

Want to add an add-on? See registry/CONTRIBUTING.md.

How it compares

pluto kubent upgradescope
Deprecated/removed API detection manifests in repos live cluster, one-shot live cluster + manifests
Detects clients still calling deprecated APIs ✅ (apiserver metrics)
Add-on EOL detection (e.g. ingress-nginx) ✅ (curated, cited registry)
Version-skew checks
Helm chart ↔ K8s compatibility
Readiness score + CI gate ✅ (SARIF, exit codes)
Continuous, in-cluster ✅ (agent + CRD + server)
Fleet rollups, team scores, auditor export

Every EOL claim in the knowledge base carries an upstream citation URL — auditable, not a black box. The API lifecycle dataset is generated from upstream k8s.io/api source, not hand-copied.

License

Apache-2.0.

Directories

Path Synopsis
cmd
upgradescope command
internal
agent
Package agent runs the in-cluster continuous loop: collect → evaluate per target → ClusterReadiness CRD status (always) → push snapshot to the server on content change.
Package agent runs the in-cluster continuous loop: collect → evaluate per target → ClusterReadiness CRD status (always) → push snapshot to the server on content change.
cli
collect
Package collect builds an inventory.Inventory from a live cluster (or, in files mode, from rendered manifests).
Package collect builds an inventory.Inventory from a live cluster (or, in files mode, from rendered manifests).
crd
Package crd owns the ClusterReadiness custom resource: its embedded CRD manifest, plain JSON-tagged Go types (no codegen), and apply/status logic via the dynamic client.
Package crd owns the ClusterReadiness custom resource: its embedded CRD manifest, plain JSON-tagged Go types (no codegen), and apply/status logic via the dynamic client.
kb
Package kb loads and indexes the three upgrade-readiness datasets: generated API lifecycle data, the add-on EOL registry, and the hardcoded Kubernetes version-skew policy.
Package kb loads and indexes the three upgrade-readiness datasets: generated API lifecycle data, the add-on EOL registry, and the hardcoded Kubernetes version-skew policy.
sarif
Package sarif renders engine reports as minimal SARIF 2.1.0 for CI annotation.
Package sarif renders engine reports as minimal SARIF 2.1.0 for CI annotation.
server
Package server is the upgradescope continuous-mode server: snapshot ingest, persisted evaluations, a read API, and on-demand what-if re-evaluation.
Package server is the upgradescope continuous-mode server: snapshot ingest, persisted evaluations, a read API, and on-demand what-if re-evaluation.
server/notify
Package notify defines the notification seam fired on finding deltas (new blocker, became-ready, add-on entering the EOL window) — never on every snapshot.
Package notify defines the notification seam fired on finding deltas (new blocker, became-ready, add-on entering the EOL window) — never on every snapshot.
server/store
Package store persists clusters, snapshots and evaluations for the upgradescope server.
Package store persists clusters, snapshots and evaluations for the upgradescope server.
server/store/storetest
Package storetest pins the behavioral contract of store.Store.
Package storetest pins the behavioral contract of store.Store.
registry/load.go
registry/load.go

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL