kubepreflight

module
v1.2.1 Latest Latest
Warning

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

Go to latest
Published: Jul 25, 2026 License: Apache-2.0

README

KubePreflight logo

KubePreflight

Go Kubernetes EKS Interface Audience License

KubePreflight is a Kubernetes upgrade-readiness CLI and Console for SRE and platform teams. It scans a cluster before a Kubernetes or EKS upgrade and reports exactly what will break — removed/deprecated APIs, fail-closed admission webhooks, PDB drain blockers, unhealthy workloads, node/kubelet skew, and EKS add-on or upgrade-insight risk — evidence-backed, prioritized, and read-only.

New here? Test KubePreflight in 5 minutes.

KubePreflight v1.0.0 — scan, findings, report, in one real run. Built from real, sanitized SEC-TRUST-002 evidence; see demo/v1-launch for the full pipeline and provenance.

Who this is for

  • SRE teams planning and gating a Kubernetes or EKS version bump
  • Platform engineering teams who own upgrade policy across many clusters
  • DevOps engineers who need a go/no-go signal before opening a change window
  • Kubernetes/EKS cluster owners who want evidence, not guesses, about what an upgrade will break
  • CI/CD and GitOps teams who want an exit-code gate they can drop into a pipeline

Problem

Kubernetes and EKS upgrades fail or get delayed because teams discover removed APIs, PDB drain blockers, fail-closed admission webhooks, unhealthy workloads, node/kubelet skew, or EKS add-on incompatibilities too late — usually mid-upgrade, inside the change window. Existing tools each cover one slice of this (deprecated APIs, or cluster hygiene, or native EKS Upgrade Insights); none correlate evidence across manifests, live cluster state, and AWS APIs into one prioritized readiness report.

What it does

KubePreflight answers one question before you touch the change window: will this upgrade break production? It correlates deprecated-API usage, admission webhooks, PodDisruptionBudgets, EKS add-ons, node/kubelet skew, and AWS provider signals into a single go/no-go readiness report with sequenced remediation.

KubePreflight is CLI-first: the read-only CLI is the readiness engine, and the optional local Console reads findings.json for review and evidence exploration. Hosted SaaS/fleet mode remains deferred until pilot validation.

Terminal running kubectl get nodes against a real, disposable EKS cluster, then kubepreflight scan, printing a live BLOCKED verdict with real blockers and warnings.

A real read-only scan against a real, disposable EKS cluster — not a replay. Cluster created, scanned, and destroyed in one session; see demo/live-eks to reproduce it yourself, including the teardown.

Install

The fastest way to get started is a prebuilt binary from the latest release — no Go toolchain, no build step.

Linux (amd64)
VERSION=v1.0.0

curl -LO "https://github.com/imneeteeshyadav98/kubepreflight/releases/download/${VERSION}/kubepreflight_${VERSION}_linux_amd64.tar.gz"
curl -LO "https://github.com/imneeteeshyadav98/kubepreflight/releases/download/${VERSION}/kubepreflight_${VERSION}_checksums.txt"

grep "kubepreflight_${VERSION}_linux_amd64.tar.gz" "kubepreflight_${VERSION}_checksums.txt" | sha256sum -c -

tar -xzf "kubepreflight_${VERSION}_linux_amd64.tar.gz"
sudo install -m 0755 "kubepreflight_${VERSION}_linux_amd64/kubepreflight" /usr/local/bin/kubepreflight

kubepreflight --help

Each archive extracts into its own directory (kubepreflight_<version>_<os>_<arch>/, containing the binary alongside README.md/LICENSE) rather than a bare binary — the path above accounts for that.

Linux (arm64)

Same steps as above, substituting kubepreflight_${VERSION}_linux_arm64.tar.gz and the matching extracted directory name.

macOS (Apple Silicon)
VERSION=v1.0.0

curl -LO "https://github.com/imneeteeshyadav98/kubepreflight/releases/download/${VERSION}/kubepreflight_${VERSION}_darwin_arm64.tar.gz"
curl -LO "https://github.com/imneeteeshyadav98/kubepreflight/releases/download/${VERSION}/kubepreflight_${VERSION}_checksums.txt"

grep "kubepreflight_${VERSION}_darwin_arm64.tar.gz" "kubepreflight_${VERSION}_checksums.txt" | shasum -a 256 -c -

tar -xzf "kubepreflight_${VERSION}_darwin_arm64.tar.gz"
sudo install -m 0755 "kubepreflight_${VERSION}_darwin_arm64/kubepreflight" /usr/local/bin/kubepreflight

kubepreflight --help
macOS (Intel)

Same steps as above, substituting kubepreflight_${VERSION}_darwin_amd64.tar.gz and the matching extracted directory name.

Gatekeeper note: if macOS refuses to run the binary ("cannot be opened because the developer cannot be verified"), clear the quarantine attribute rather than disabling Gatekeeper system-wide:

xattr -d com.apple.quarantine /usr/local/bin/kubepreflight
Windows (amd64)
$VERSION = "v1.0.0"
Invoke-WebRequest -Uri "https://github.com/imneeteeshyadav98/kubepreflight/releases/download/$VERSION/kubepreflight_${VERSION}_windows_amd64.zip" -OutFile "kubepreflight.zip"
Expand-Archive -Path "kubepreflight.zip" -DestinationPath "."
.\kubepreflight_${VERSION}_windows_amd64\kubepreflight.exe --help

The zip extracts into its own folder (kubepreflight_<version>_windows_amd64\, alongside README.md/LICENSE) rather than a bare .exe. Move kubepreflight_<version>_windows_amd64\kubepreflight.exe into a directory already on your PATH (or add that folder to PATH) to run it as kubepreflight from any shell.

Verify a download

Every release publishes kubepreflight_<version>_checksums.txt (SHA-256, GNU coreutils format) and an SPDX SBOM, kubepreflight_<version>_sbom.spdx.json. If you downloaded every asset into one folder, verify all of them at once:

sha256sum -c kubepreflight_v1.0.0_checksums.txt      # Linux
shasum -a 256 -c kubepreflight_v1.0.0_checksums.txt  # macOS

Windows PowerShell has no built-in batch-verify equivalent to -c; compute a single file's hash and compare it by eye against the matching line in kubepreflight_v1.0.0_checksums.txt:

Get-FileHash .\kubepreflight_v1.0.0_windows_amd64.zip -Algorithm SHA256
Docker
docker pull ghcr.io/imneeteeshyadav98/kubepreflight:1.0.0
docker run --rm ghcr.io/imneeteeshyadav98/kubepreflight:1.0.0 --help

The image is distroless and runs as a fixed non-root user, so a plain bind mount for output usually fails with a permission error unless you match the container's user to your own — this is why every real example below passes --user:

# Manifest-only scan: no cluster, no kubeconfig, only a mounted directory
docker run --rm --user "$(id -u):$(id -g)" \
  -v "$(pwd)/k8s:/work/k8s:ro" \
  -v "$(pwd)/out:/work/out" \
  ghcr.io/imneeteeshyadav98/kubepreflight:1.0.0 \
  scan --manifests-only --manifests /work/k8s --target-version 1.36 \
  --output-dir /work/out --serve-report never

For a live-cluster scan, also mount a kubeconfig read-only and point --kubeconfig at its in-container path — docker-compose.yml in this repo does exactly that (mounts ~/.kube read-only, matches your host UID/GID via user:, writes findings.json to ./out):

docker compose up

docker-compose.yml sets network_mode: host, needed on Linux because kind (and most local clusters) bind their API server to 127.0.0.1 on the host, unreachable from inside a container without host networking — this is Linux-only. Docker Desktop on macOS/Windows runs containers inside a VM where host networking doesn't give the same access to a locally-running kind cluster; on those platforms run KubePreflight natively against a local kind cluster instead. A real EKS/GKE/AKS cluster has a routable endpoint, not 127.0.0.1, so this caveat only applies to local kind-style clusters.

A real EKS cluster brings its own distroless caveat, though: the kubeconfig aws eks update-kubeconfig/eksctl generate uses an exec: user entry that shells out to the aws CLI to mint a token, and this image has no aws CLI (or shell) to run it — mounting that kubeconfig in as-is fails with an executable aws not found-style error the moment the container tries to authenticate. It doesn't affect the manifest-only example above, which never touches cluster credentials, and it's a property of the minimal release image, not a bug — it changes how credentials get resolved before the container runs, not what a scan/plan/compare/rollback finds. Simplest fix: run the host binary for the live-EKS step instead of the container, the same way docs/ci-integration.md resolves it for the GitHub Action. If the container is a hard requirement, mint a short-lived static token on the host (aws eks get-token --cluster-name <name> --output json) and write it into the kubeconfig's user: block in place of the exec: entry before mounting it in — that token is only good for about 15 minutes, so it suits a single invocation, not a long-running one.

The distroless image also has no helm binary, so render charts on the host (helm template) and mount the rendered YAML — see Quick start below.

Building from source instead of using a release binary or the published image:

git clone https://github.com/imneeteeshyadav98/kubepreflight.git
cd kubepreflight && go build -o kubepreflight ./cmd/kubepreflight

Or, without cloning first:

go install github.com/imneeteeshyadav98/kubepreflight/cmd/kubepreflight@latest

go install builds directly from source and does not run this project's release-workflow -ldflags, so kubepreflight version on a go install-built binary reports dev/unknown/unknown rather than a real version/commit/build date — the release archives, Docker image, and a locally cloned+tagged build all carry real provenance; a plain go install build does not. See docs/published-install-matrix.md for the full installation verification matrix.

Current capabilities

Capability What it means
Read-only cluster scan Cluster-only by default, or with --provider=eks for AWS enrichment
Multi-format reports JSON, Markdown, and self-contained HTML, plus a compact terminal summary
Embedded Console Local React app embedded in the binary — no Node, no browser account — split-pane findings workspace with filters and evidence/remediation detail
Local report server Serves report.html, findings.json, and the Console together on 127.0.0.1
Evidence-backed findings Every finding carries raw evidence, a confidence tier, and a specific remediation — never just a rule name
AWS/EKS enrichment EKS Upgrade Insights, add-on compatibility, and subnet/VPC checks with --provider=eks — degrades gracefully without it
Compatibility catalog Versioned, validated add-on compatibility catalog covering EKS managed add-ons and live workload add-ons (metrics-server, ingress-nginx, AWS Load Balancer Controller, cert-manager, external-dns) — deterministic Blocker/Warning/no-finding verdicts instead of a perpetual "unverifiable" warning — see Compatibility Catalog
API version catalog Versioned, validated Kubernetes API deprecation/removal catalog is the sole source of truth API-001/API-002 decide from — full parity with every API removal KubePreflight has ever tracked, with source/reference/verification-date provenance on every entry — see API Version Catalog
Unsupported target-version rejection A --target-version (or plan --to-version) outside the range this build's API catalog has actually been verified against fails fast with a clear error and exit code 1 — before any collector runs, before any report or action-plan file is written. scan and plan share one centralized check, so KubePreflight never silently guesses about a target it hasn't reviewed data for
EKS rollback readiness assessment kubepreflight rollback plan/rollback assess combine AWS eligibility evidence, AWS rollback/upgrade insights, and existing scan findings into a read-only eligibility/readiness/recommendation assessment — never executes rollback or mutates cluster resources — see Rollback Readiness
Validated on real EKS Run end-to-end against a real, throwaway EKS cluster, both clean and seeded worst-case — see Validated on real EKS
Upgrade Priority (P1–P4) Every finding is assigned a priority — what to fix first — independent of Severity and Confidence — see Priority (P1–P4)
Multi-hop upgrade planner kubepreflight plan sequences a hop-by-hop readiness view, plus an optional action-plan checklist — see Multi-hop upgrade planner
Upgrade Readiness scorecard A 0–100 readiness score plus a Passed/Warning/Failed rollup across all 9 rule-family categories (API Compatibility, Extension APIs, Admission Webhooks, Disruption Safety, Node Readiness, Add-ons, CoreDNS, Workload Health, EKS Upgrade Insights) — the numeric score is always kept separate from the hard blocker verdict, in every format including the Console, where each category's rule IDs are clickable chips that jump straight to the filtered finding
No-upgrade-required framing When the cluster's current version and --target-version are the same release, KubePreflight never says "Upgrade Ready" — it says "No upgrade required" instead, and every renderer (terminal, Markdown, HTML, Console) relabels the scorecard as "Cluster Health" with a "Remediation needed" column in place of "Upgrade continue", and the Next Actions list as "Recommended Maintenance". Current-state and manifest-safety findings are still fully evaluated and reported — only the upgrade-transition framing is skipped
Downgrade rejection A --target-version (or plan --to-version) below the detected current version fails fast with a clear error and exit code 1 — before any collector runs, before any report or action-plan file is written. KubePreflight assesses upgrade readiness, not downgrades; scan and plan share one centralized version-comparison helper so this is never a guess when the current version is unknown
Short cluster identifiers An EKS cluster ARN (60+ characters, e.g. what aws eks update-kubeconfig names a kubeconfig context by default) is shown as just its cluster name everywhere — terminal, Markdown, HTML, and the Console — with the full ARN available via a tooltip and a one-click copy button, never silently dropped. Any identifier that isn't ARN-shaped is displayed completely unchanged
Manifest-only scanning --manifests-only skips kubeconfig loading and all cluster/AWS collection entirely — no cluster credentials needed to catch removed/deprecated APIs in raw or Helm-rendered YAML — see Quick start
Bounded collector concurrency --collector-concurrency (default 4) runs Kubernetes collector requests in flight at once instead of strictly one-at-a-time, cutting collection wall-clock time on large or high-latency clusters — 1 reproduces fully sequential collection exactly, and every level produces identical results, backed by an explicit conservative client-side QPS/Burst limit
GitHub Action Composite action wrapping the same read-only scan, with a Step Summary scorecard, workflow-artifact reports, and a configurable Blocker/Warning exit policy — see GitHub Action
Scan comparison kubepreflight compare diffs two findings.json scans by fingerprint — new/resolved/changed/unchanged findings, verdict movement, and readiness-score delta — turning repeated scans into a remediation-progress tracker, not just a one-time snapshot — see Scan comparison

Context-aware upgrade gating

The same cluster condition can have a different impact depending on the planned operation.

KubePreflight context-aware upgrade gating demo

A PDB with zero allowed disruptions may be a warning for a control-plane-only upgrade but a blocker for a worker rollout that requires pod eviction.

The example below is from a real scan against a local kind cluster seeded with the original MVP failure modes (see demo/) — run it yourself and you'll get this exact shape of output.

KubePreflight scan — cluster: kind-kubepreflight-demo  target: 1.34  provider: cluster-only
Result: BLOCKED

Upgrade Readiness: BLOCKED — Score: 16/100 — Upgrade Continue: No
  API Compatibility: Failed (5 blocker(s), 0 warning(s))
  Extension APIs: Passed (0 blocker(s), 0 warning(s))
  Admission Webhooks: Failed (1 blocker(s), 1 warning(s))
  Disruption Safety: Failed (3 blocker(s), 0 warning(s))
  Node Readiness: Failed (2 blocker(s), 1 warning(s))
  Add-ons: Passed (0 blocker(s), 0 warning(s))
  CoreDNS: Warning (0 blocker(s), 1 warning(s))
  Workload Health: Passed (0 blocker(s), 0 warning(s))
  EKS Upgrade Insights: Passed (0 blocker(s), 0 warning(s))

API Compatibility: Failed — Upgrade Continue: No — Score Impact: -60
  Removed API objects: 5 across 3 API families
  Deprecated API objects: 0 across 0 API families
  Critical impact: Yes

Blockers (11)
  [P2/API-001] PodDisruptionBudget "demo/shared-app-pdb-a" (apiVersion policy/v1beta1) still exists
  at a version removed in Kubernetes 1.25 — target version 1.34 will no longer serve this API...
    Priority P2 (do not attempt other remediation until this is fixed): Resource or behavior may
    fail after the target Kubernetes upgrade.
  (also fires for shared-app-pdb-b, singleton-app-pdb, and PodSecurityPolicy demo-restricted)

  [P3/NODE-001] Node "kubepreflight-demo-control-plane": kubelet version v1.24.15 is outside the
  supported skew window for target version 1.34 — 10 minor versions behind, exceeds n-3 policy

  [P3/PDB-001] PodDisruptionBudget demo/singleton-app-pdb: disruptionsAllowed=0 (minAvailable: 1,
  currentHealthy=1, desiredHealthy=1, expectedPods=1) — healthy matching pods cannot currently be
  voluntarily evicted, so a node drain or node upgrade can stall or fail
  (also fires for shared-app-pdb-b)

  [P3/PDB-002] PodDisruptionBudgets demo/shared-app-pdb-a and demo/shared-app-pdb-b select an
  overlapping set of pods (2 overlapping pods) — the Eviction API rejects eviction when multiple
  PDBs match the same pod...

  [P4/WH-002] ValidatingWebhookConfiguration "demo-catchall-guard": fail-closed, zero ready
  endpoints — matching API writes will be rejected

Warnings (3)
  [P4/COREDNS-001] CoreDNS Corefile is missing the `ready` plugin...
  [P4/WH-001] ValidatingWebhookConfiguration "demo-catchall-guard": catch-all scope...

Info (23)
  23 findings that need no action right now — kube-apiserver-managed FlowSchema/
  PriorityLevelConfiguration defaults and controller-managed EndpointSlices.

Next Actions (9)
  1. [P2/Blocker] PodSecurityPolicy/demo-restricted (API-001)
  ...
  7. [P4/Blocker] ValidatingWebhookConfiguration/demo-catchall-guard (WH-001, WH-002)
     ...    Also see WH-001: Inspect the webhook's current rules and selectors: ...

Summary: 11 blocker(s), 3 warning(s), 23 info(s)
Reports written: findings.json · report.md · report.html

WH-001 and WH-002 fired on the same webhook (broad scope + a dead backend), but Next Actions merges them into one item instead of two separate, potentially contradictory instructions — the Blockers section above still lists both separately, since that's correlation evidence worth keeping.

A few things worth knowing about the counts above, all covered in full in Known limitations: the 23 Info findings are kube-apiserver-owned FlowSchema/PriorityLevelConfiguration defaults and controller-managed EndpointSlice objects — neither is something a person migrates by hand, so they're visible for inventory but don't gate the exit code. One EndpointSlice (default/kubernetes) still reports as a real Blocker — a disclosed, narrow exception where KubePreflight can't yet confirm it's safe to downgrade. Live Event objects are excluded from scanning entirely (not shown above at all) for the same reason.

Demo output isn't committed

demo/sample-output/ used to be a captured, committed example — it went stale repeatedly as the product evolved, and a real scan's finding count moves with the product (new checks, catalog entries, and rule behavior all change it), so a frozen snapshot couldn't stay accurate. Run the block above yourself (see demo/README.md) for real, current output. Nothing in this repo's own tests depends on a committed capture staying accurate either — web/tests/browser_smoke.py drives the Console against a small fixture generated fresh from internal/report on every run (see CI / dev verification), not demo output.


What it checks

31 checks today:

ID Check Data source Severity Confidence
API-001 Removed APIs vs target version Live objects + raw/rendered manifests Blocker STATIC_CERTAIN
API-002 Deprecated APIs still served at target version Live objects + raw/rendered manifests Warning STATIC_CERTAIN
WH-001 Broad/catch-all fail-closed webhooks ValidatingWebhookConfiguration Warning STATIC_CERTAIN
WH-002 Webhook backend unavailable or misconfigured: missing/invalid clientConfig, referenced Service missing, invalid port, or zero ready endpoints Webhook config + Service + EndpointSlice Blocker for failurePolicy: Fail; Warning for failurePolicy: Ignore (writes aren't rejected, but admission control silently doesn't apply) STATIC_CERTAIN for static config checks, OBSERVED for the live endpoint-health check
WH-004 Webhook TLS/CA safety: empty/invalid/unparseable caBundle, expired or soon-to-expire CA certificate, certificate not marked as a CA, insecure (non-https://) webhook URL Webhook clientConfig (caBundle, url) Blocker for failurePolicy: Fail; Warning for failurePolicy: Ignore on the "currently broken" cases (empty/invalid caBundle, expired cert, insecure URL); always Warning for forward-looking signals (cert expiring soon, cert not marked as CA) STATIC_CERTAIN
WH-005 Webhook unsafe scope and timeout: timeoutSeconds near the 30s API ceiling, operations: ["*"] (includes CONNECT), self-interception (rules cover the webhook config resources themselves), or coverage of cluster-critical resources (nodes, namespaces, persistentvolumes) Webhook rules, selectors, timeoutSeconds Warning regardless of failurePolicy -- WH-005 flags scope/configuration risk, not a confirmed admission failure; the message text distinguishes failurePolicy: Fail (a more direct dependency) from failurePolicy: Ignore (a configuration risk worth reviewing) STATIC_CERTAIN
PDB-001 Fresh disruptionsAllowed=0 with selected pods PodDisruptionBudget status Blocker OBSERVED
PDB-002 Overlapping PDBs (incl. CoreDNS duplicate-PDB case) PDB selectors vs live pods Blocker OBSERVED
DRAIN-001 Deployment/StatefulSet with effectively one replica: zero available capacity during eviction, regardless of PDB presence Deployment/StatefulSet spec + status + PDB relationship (evidence only) Warning STATIC_CERTAIN
DRAIN-002 Deployment/StatefulSet pod using hostPath or a PersistentVolumeClaim bound to a node-pinned PersistentVolume (Local source, or exact single-hostname nodeAffinity) Pod template volumes + PVC/PV binding + Node readiness Warning; Blocker only when the pinned node is currently NotReady and the workload is a singleton OBSERVED
DRAIN-003 Hard scheduling constraint that a replacement pod may not survive: node affinity/selector satisfied by <= 1 node, hostname-topology anti-affinity with no spare node, topologySpreadConstraints (DoNotSchedule) collapsed to one domain, or a hostPort with no free alternate node Pod template constraints + live Node labels/taints + live Pod placement Warning OBSERVED
DRAIN-004 Estimated node-capacity shortage: if one node is removed, its non-DaemonSet pods' resource requests (plus any pending pods) exceed the other nodes' estimated spare CPU/memory Node allocatable + live Pod resource requests Warning (never Blocker -- no reliable signal that a cluster autoscaler is absent) INFERRED
DRAIN-005 StatefulSet/DaemonSet with fewer Ready replicas/pods than desired right now, before any drain adds further disruption StatefulSet/DaemonSet status Warning; Blocker only when zero replicas/pods are Ready (a proven, current fact); CriticalInfra escalation for well-known critical node agents (CNI, kube-proxy, CoreDNS, etc.) OBSERVED
ADDON-001 Catalog-known add-on incompatible with target version (VPC CNI, kube-proxy, CoreDNS, EBS/EFS CSI, AWS Load Balancer Controller, metrics-server, ingress-nginx, cert-manager, external-dns) EKS add-on inventory / live workload image + compatibility catalog operational impacts Contextual: Blocker only when catalog operational impacts intersect the selected --upgrade-context; Warning/allow for audit-only; Warning/operator decision for unrelated, unknown, optional, or unspecified contexts PROVIDER_REPORTED/OBSERVED
ADDON-002 High-impact add-on compatibility unknown or upgrade recommended EKS add-on inventory / live workload image + compatibility catalog Warning PROVIDER_REPORTED/OBSERVED
EKS-NG-001 EKS managed node group health issues eks:ListNodegroups/DescribeNodegroup Warning PROVIDER_REPORTED
EKS-NG-002 EKS managed node group limited update headroom eks:ListNodegroups/DescribeNodegroup Warning PROVIDER_REPORTED
EKS-NG-003 EKS managed node group launch template/custom AMI review eks:ListNodegroups/DescribeNodegroup Info PROVIDER_REPORTED
EKS-NG-004 EKS managed node group version context eks:ListNodegroups/DescribeNodegroup Info PROVIDER_REPORTED
EKS-INSIGHT-001 EKS Upgrade Insight reports ERROR eks:ListInsights/DescribeInsight Warning PROVIDER_REPORTED
EKS-INSIGHT-002 EKS Upgrade Insight reports WARNING eks:ListInsights/DescribeInsight Warning PROVIDER_REPORTED
EKS-INSIGHT-003 EKS Upgrade Insight status UNKNOWN eks:ListInsights/DescribeInsight Info PROVIDER_REPORTED
NODE-001 kubelet skew outside supported policy Node status Blocker STATIC_CERTAIN
NODE-002 Control-plane subnet IP headroom ec2:DescribeSubnets Contextual: Blocker for control-plane-only/full-platform-upgrade; Warning otherwise STATIC_CERTAIN
NODE-003 Deprecated node-role.kubernetes.io/master scheduling label Live and manifest workload pod templates Warning STATIC_CERTAIN
NET-002 Cluster's security group or VPC no longer exists ec2:DescribeSecurityGroups/DescribeVpcs Contextual: Blocker for control-plane-only/full-platform-upgrade; Warning otherwise STATIC_CERTAIN
COREDNS-001 Corefile missing ready plugin ConfigMap (single allowlisted Get) Warning STATIC_CERTAIN
CRD-001 Legacy or unavailable CRD stored versions need migration CustomResourceDefinition status Warning; Blocker if a stored version is no longer served STATIC_CERTAIN
CRD-002 CRD conversion webhook is broken: no ready endpoints, referenced Service missing, incomplete webhook config, or an empty/unsupported conversionReviewVersions list CRD + Service + EndpointSlice Blocker OBSERVED for the live endpoint-health check, STATIC_CERTAIN for static config checks
APISERVICE-001 Aggregated APIService is unavailable APIService status Blocker OBSERVED
WORKLOAD-001 Pod already unhealthy before the upgrade (ImagePullBackOff, CrashLoopBackOff, etc.) Live pod status Warning OBSERVED

ADDON-001 uses compatibility-catalog operationalImpacts to decide whether a confirmed add-on incompatibility intersects the selected operation: full-platform upgrades block for concrete non-optional impacts; control-plane and worker-rollout scans block only for matching impact classes; workload-restart and unspecified require operator decision; audit-only remains visible but non-blocking. NODE-002 and NET-002 are EKS control-plane provider preconditions: they block control-plane-only and full-platform-upgrade contexts, but audit-only keeps them as warnings and unspecified, worker-rollout, or workload-restart require an operator decision instead of assuming a control-plane operation. NET-002 was added after AWS upgrade troubleshooting guidance surfaced SecurityGroupNotFound/VpcIdNotFound as common hard failures alongside IP exhaustion. EKS-NG checks cover EKS managed node groups returned by the EKS ListNodegroups API only; self-managed node groups are not listed by that AWS API. EKS Upgrade Insights are AWS-native signals surfaced as warning/info findings plus inventory; ERROR is not treated as a blocker yet. CRD storage/conversion and aggregated APIService availability extend that same principle to Kubernetes extension APIs.

Every finding carries a confidence tier so a clean local scan is never silently contradicted by a stale provider signal. EKS Upgrade Insight evidence is marked PROVIDER_REPORTED and includes freshness/staleness context rather than replacing KubePreflight's local checks.

Provider support

Provider Status
cluster-only (no --provider) Current — Kubernetes-plane checks run against any cluster
eks Current — validated against a real EKS cluster (see Validated on real EKS)
aks Planned — CLI flags recognized and validated today; enrichment checks not implemented yet
gke Planned — CLI flags recognized and validated today; enrichment checks not implemented yet

See docs/provider-roadmap.md for what each provider's enrichment checks do (or will do), and which checks are already portable across all of them.

Quick start

Manifest-only scan (no cluster access)
kubepreflight scan \
  --manifests-only \
  --manifests ./k8s \
  --target-version 1.36

--manifests-only skips kubeconfig loading and all cluster/AWS collection entirely — nothing about the runner needs cluster access at all. It only runs the two checks that read manifest data (API-001/API-002); every other category will read "Passed" without actually having been checked, since it was never evaluated. Good for pull requests, GitOps repositories, and any CI job that shouldn't need cluster credentials just to catch a removed/deprecated API before it merges.

Helm charts aren't scanned directly — render first, then point --manifests at either the output directory or a single rendered file:

helm template my-app ./chart > rendered.yaml
kubepreflight scan --manifests-only --manifests rendered.yaml --target-version 1.36
Scan your current kubeconfig context
kubectl config current-context
kubectl get nodes

kubepreflight scan --target-version 1.36

With no --kubeconfig flag, KubePreflight uses the same KUBECONFIG/home loading rules as kubectl — whatever kubectl config current-context just showed you is what gets scanned.

EKS scan
aws eks update-kubeconfig \
  --name production-eks \
  --region ap-south-1

AWS_REGION=ap-south-1 kubepreflight scan \
  --provider eks \
  --cluster-name production-eks \
  --target-version 1.36

--cluster-name is required whenever --provider eks is set; there's no separate --region flag — AWS enrichment always resolves its region through the standard AWS SDK credential/region chain (AWS_REGION above, or a shared config/credentials file, or an IAM role). If AWS enrichment can't complete — missing credentials, insufficient IAM permissions — the Kubernetes-plane findings are never discarded, but the overall result is marked INCOMPLETE (exit code 3) rather than silently reported as clean.

Live cluster plus manifests
kubepreflight scan \
  --target-version 1.36 \
  --manifests ./k8s

Correlates both planes in one report: API-001/API-002 catch removed APIs in the manifests directory before they're ever applied, on top of everything the live cluster connection already checks (PDBs, webhooks, node skew, and more). Manifest scanning is additive to a live scan here — for a scan that needs no cluster connection at all, use --manifests-only above instead.

GitHub Action

KubePreflight also ships as a GitHub Action — the same read-only scan command, wired into your CI as a job pass/fail decision with a readiness scorecard in the Step Summary and findings.json/report.html uploaded as workflow artifacts. Full reference, the EKS kubeconfig caveat, and every input/output: docs/ci-integration.md.

- uses: imneeteeshyadav98/kubepreflight@v1.0.0
  with:
    target-version: '1.36'
    manifests: './deploy'
    manifests-only: 'true'

Exit policy: a BLOCKED verdict (Blocker findings) always fails the job; INCOMPLETE and infrastructure failures (no report could be produced at all) always fail too, since partial evidence must never read as safe; Warning-only findings pass unless you set fail-on-warning: 'true'.

A sibling composite action, imneeteeshyadav98/kubepreflight/compare, gates a PR on regressions rather than a single scan's absolute state — point it at a baseline and a current findings.json (e.g. a scan of the PR's base ref and head ref) and it evaluates a configurable pass/fail/neutral gate: new Blocker findings, a warning policy, verdict regression, and readiness- score movement, plus a Step Summary diff and inline annotations on new blockers. See docs/ci-integration.md.

Usage

# Cluster-only scan (no AWS setup required)
./kubepreflight scan --target-version 1.36 --serve-report always

# With AWS/EKS enrichment (EKS-INSIGHT, ADDON, EKS-NG, NODE/NET) — opt-in
AWS_PROFILE=<profile> AWS_REGION=<region> ./kubepreflight scan \
  --provider eks \
  --cluster-name <cluster-name> \
  --target-version 1.36 \
  --serve-report always

# Scan raw manifests alongside the required live-cluster scan
./kubepreflight scan \
  --provider eks \
  --cluster-name <cluster-name> \
  --target-version 1.36 \
  --manifests ./k8s \
  --serve-report always

# Limit namespaced findings; cluster-scoped and AWS findings remain included
kubepreflight scan --target-version 1.36 --namespace-allowlist payments,platform

# CI/script mode: canonical JSON file, no local server, no blocking,
# and no human-readable text on stdout — see the note below.
kubepreflight scan \
  --target-version 1.36 \
  --output json \
  --terminal-output silent \
  --findings-out findings.json \
  --serve-report never

jq '.summary' findings.json

# Keep a run's artifacts together
kubepreflight scan --target-version 1.36 --output all --output-dir ./preflight-output

--output json controls which report file gets written (findings.json, always written regardless of --output) — it does not by itself change what prints to stdout. Stdout is controlled separately by --terminal-output (full by default): unless you also pass --terminal-output silent, stdout still gets the full human-readable report even in JSON mode, which will break a naive kubepreflight scan --output json | jq . pipeline. Add --terminal-output silent in CI if stdout must stay machine-safe, and read the JSON from the file it's written to.

AWS enrichment degrades gracefully: missing credentials or IAM permissions do not discard Kubernetes findings, but the report is marked INCOMPLETE and exits 3 so CI cannot mistake missing provider evidence for readiness. --cluster-name is required when --provider=eks is explicitly set. The same rule applies to every requested evidence plane: Kubernetes, AWS, manifest, and Helm failures preserve whatever sibling data was collected, record the unavailable area under coverage, and prevent a false CLEAN/ready verdict.

--findings-out always writes the canonical JSON report, including when --output=md or --output=html; --output selects additional human-readable artifacts. Manifest scanning is additive to a live cluster scan by default — combine --manifests/--helm-chart with a normal scan to check both planes at once. For a scan that needs no cluster connection at all, pass --manifests-only (see Quick start): it skips kubeconfig loading and all cluster/AWS collection, and only runs the two checks that read manifest data (API-001/API-002) — every rule that needs live cluster/AWS state is skipped rather than run unsafely against absent data.

By default, a scan attached to an interactive terminal writes findings.json, report.md, and report.html, then serves the report and local Console on a random 127.0.0.1 port until you press Ctrl+C. Redirected stdout, CI environments, and explicit --output=json runs do not start or wait on the server. Use --serve-report=never for scripts, --serve-report=always to override non-interactive detection, --listen to choose the local address, and --open-report to ask the OS to open the report URL. Browser-open failure never invalidates the scan.

Once the local server is starting, stdout switches to a compact summary (cluster/target/provider/result/counts + the report and Console URLs) instead of the full per-finding listing — report.html and the Console already show every finding's evidence and remediation, so repeating it on stdout is redundant. Use --terminal-output full to keep the old detailed output even while serving, or --terminal-output silent to print nothing but the URLs (and fatal errors). Runs that aren't serving a local report keep today's full terminal output by default, so scripts and CI output are unaffected unless you pass --terminal-output explicitly.

When --namespace-allowlist is set, findings with known namespaced resources are included only when every namespaced reference belongs to the allowlist. Cluster-scoped Kubernetes and AWS findings remain visible. Namespace-less namespaced manifests are excluded because their apply-time namespace cannot be inferred safely; the active allowlist is recorded in every report format.

--collector-timeout (default 30s, both scan and plan) bounds each individual Kubernetes, AWS, and Helm-chart-render collector request — a hung or unreachable API server, AWS endpoint, or helm template process can never block the scan forever. This is a per-call, not per-scan, budget: a timed-out call is recorded exactly like any other collection failure (marking that plane's coverage partial and the overall result INCOMPLETE, never a false CLEAN), and the next call still gets its own fresh window. Against a completely unreachable cluster or AWS endpoint, total worst-case wall-clock time is roughly (number of calls) × --collector-timeout — confirmed against a real black-holed server address for Kubernetes (~50 sequential calls at a 3s timeout took ~3 minutes end to end, correctly finishing INCOMPLETE with exit code 3, never hanging indefinitely) and against a genuinely hung helm process for the manifest collector. Pass a smaller value if you want faster failure against a suspected-unreachable cluster or endpoint. Ctrl+C/SIGTERM during collection cancels in-flight calls immediately rather than waiting out their own timeout.

Coverage errors are intentionally concise and classified, for example pods [timeout,timedOut,retryable,partialDataPreserved]: context deadline exceeded or list-insights [permission-denied,permissionDenied,partialDataPreserved]: .... Those labels are diagnostic only; they do not change detector severity, priority, fingerprints, readiness scoring, or the exit-code contract.

--collector-concurrency (default 4, both scan and plan, range 1-16) bounds how many Kubernetes collector requests run in flight at once, cutting that (number of calls) × --collector-timeout worst-case figure down to roughly (number of calls / concurrency) × --collector-timeout. 1 preserves fully sequential collection byte-for-byte (same Snapshot content, same errors) — every value is bounded by an explicit, conservative client-side QPS/Burst limit (20 QPS / 40 burst, up from client-go's own unset-default of 5/10) so raising concurrency increases parallelism, not uncontrolled request pressure against the API server. Concurrency only ever changes when results arrive, never what they are: every Snapshot field is either written by exactly one call or merged under a shared lock, and the one place multiple calls append into the same slice (deprecated-API usage across GVRs) is proven identical after sorting across concurrency 1/2/4/8 in internal/collectors/k8s/collector_concurrency_external_test.go. This is a Kubernetes-collection-only setting — AWS and Helm-chart-render collection stay sequential, unaffected by this flag.

Version directionscan --target-version and plan --to-version are compared against the detected current Kubernetes version through one shared, centralized helper (findings.CompareMinorVersions), so scan and plan can never classify the same current/target pair two different ways:

  • target > current — a normal upgrade-readiness scan or plan.
  • target = current — "No upgrade required" framing (see above); every finding is still evaluated, nothing is skipped.
  • target < current — rejected immediately with exit code 1, before any collector runs and before any report, findings.json, or upgrade action plan is written: downgrade is not supported: current Kubernetes version is X, target version is Y. Choose a target version greater than X. KubePreflight assesses upgrade readiness, not downgrades — this is a distinct concern from an EKS-style version rollback, which kubepreflight rollback plan/rollback assess assess separately and explicitly (read-only; see Rollback Readiness).

This comparison is only ever enforced when the current version is confidently known (a real cluster connection succeeded); an unreachable cluster or --manifests-only run never guesses a downgrade that isn't provably true, matching the same "don't guess" principle the same-version framing already applies.

Exit codes (for CI)
Code Meaning
0 Clean — no blockers or warnings
1 Warnings or operator decisions only
2 Blockers found
3 Assessment incomplete because requested evidence could not be collected. This outranks blockers/warnings in the top-level result: any partial requested coverage means the report exists but cannot be treated as upgrade-ready. Findings from successful collectors remain visible.
4 Infrastructure failure — no trustworthy report was produced, or a requested persistent report/artifact could not be created or fully written (bad kubeconfig, cannot build a Kubernetes client, the collector failed outright, the output directory could not be created, or a --output/action-plan/rollback-report file could not be written). Distinct from 3: 3 means a report exists but some evidence is missing; 4 means either no report was produced at all, or a requested output artifact was not durably delivered. A CI gate checking exit code <= 1 for "safe to proceed" must not treat 4 as safe.

Multi-format output (--output all) and partial write failures: scan, plan, and rollback plan/rollback assess can write more than one report format in a single run. If an earlier format (e.g. findings.json) is written successfully and a later one (e.g. report.html) then fails, the already-written file(s) are left in place — KubePreflight does not delete or roll back partial output — and the command still returns exit code 4. A partial artifact set must never be treated as a complete, trustworthy report; do not script around 4 by assuming a prior run's files are the full expected set.

Upgrade context

scan and the exact first hop of plan accept --upgrade-context so KubePreflight can distinguish audit-only findings from risks that actually block the planned operation:

kubepreflight scan --target-version 1.36 --upgrade-context worker-rollout
kubepreflight plan --to-version 1.36 --upgrade-context full-platform-upgrade

Allowed values are unspecified, audit-only, control-plane-only, worker-rollout, full-platform-upgrade, and workload-restart. The default is unspecified, which keeps ambiguous drain/workload-restart risks, add-on compatibility risks, and EKS control-plane provider preconditions visible as operator decisions without inventing a hard blocker. Context-sensitive findings carry impactScopes and upgradeGate; blocker counts and exit code 2 are based on upgradeGate: "block", while upgradeGate: "operator_decision" exits 1 and prevents a clean ready verdict until reviewed.

Compatibility note for automation: pipelines that do not specify an upgrade context may observe changed exit behavior for contextual findings. A finding that previously caused exit code 2 may now produce operator-review behavior and exit code 1 under the default unspecified context. CI/CD users should set the intended operation explicitly, for example --upgrade-context worker-rollout or the GitHub Action input upgrade-context: worker-rollout.

Output

A scan run writes to the current directory by default. Use --output-dir to place all generated artifacts together; --findings-out can override the canonical JSON filename/path.

  • findings.json — canonical machine-readable report, always written
  • report.md — Markdown, written with --output=md or all
  • report.html — single-file, self-contained HTML report, written with --output=html/all or whenever a local server starts

When a local report server starts (see above), it prints:

Open report:
  http://127.0.0.1:<port>/report.html

Open Console:
  http://127.0.0.1:<port>/console/?findings=/findings.json#summary

The Console URL's ?findings= query param is pre-filled with the just-completed scan's results, so opening it loads the dashboard immediately.

Permissions

KubePreflight is read-only by design. It never requests secrets access.

  • Kubernetes RBAC: get/list/watch on nodes, pods, poddisruptionbudgets, validating/mutatingwebhookconfigurations, services, endpointslices, customresourcedefinitions, deployments, daemonsets, plus a single allowlisted get on the kube-system/coredns ConfigMap (not a blanket ConfigMap list, enforced via a separate namespace-scoped Role with resourceNames). Copy-pasteable manifest: deploy/clusterrole.yaml — every rule in it is cross-checked against what the collector actually calls, verified against a real API server with kubectl auth can-i.
  • AWS IAM: eks:DescribeCluster, eks:ListInsights, eks:DescribeInsight, eks:ListAddons, eks:DescribeAddon, eks:DescribeAddonVersions, eks:ListNodegroups, eks:DescribeNodegroup, ec2:DescribeSubnets, ec2:DescribeSecurityGroups, ec2:DescribeVpcs. All read-only; KubePreflight does not call eks:StartInsightsRefresh. Copy-pasteable policy: deploy/iam-policy.json.

Safety

  • No cluster writes. Every Kubernetes and AWS call KubePreflight makes is a read (get/list/watch/Describe*/List*). It never creates, patches, or deletes anything in your cluster or AWS account.
  • No auto-remediation. Findings include remediation guidance and copy-pasteable commands; nothing is ever applied automatically.
  • No SaaS upload required. findings.json/report.html/the Console are local files served from 127.0.0.1 by default. Binding a non-loopback address requires the explicit --allow-remote-report acknowledgement because the local server has no authentication.
  • AWS credentials stay local. KubePreflight uses whatever the AWS SDK's standard credential chain resolves (env vars, shared config/credentials file, IAM role) — it never reads, logs, or transmits credentials elsewhere.
  • Seeded demo manifests are for test/demo clusters only. pdb-lab.yaml and broken-webhook.yaml deliberately create a fail-closed webhook and disruption-blocking PDBs to exercise the worst-case checks — never apply them to a production cluster.
  • Generated evidence contains real infrastructure identifiers by default. findings.json/report.html/report.md (and the equivalent plan/rollback assess/compare outputs) embed the real AWS account ID (inside the cluster ARN) and real EC2-style internal node hostnames verbatim — useful for actually remediating your own cluster, but not something to publish or attach to a public issue as-is. Add --redact-sensitive-identifiers to scan/plan/rollback plan/ rollback assess/compare before sharing generated evidence outside your organization; it replaces ARNs and node hostnames with fixed placeholders in every output format (including the terminal) without changing any finding, score, verdict, comparison result, or exit code — see internal/redact. compare redacts its own output independently of whether --baseline/--current were themselves produced with the flag, so comparing two unredacted scans and asking only the comparison to be shareable still works correctly.

Architecture

cmd/kubepreflight/          CLI entrypoint (Cobra)
internal/collectors/k8s/    Kubernetes API collector (client-go + dynamic client, read-only)
internal/collectors/aws/    EKS/EC2 collector (aws-sdk-go-v2, read-only, gracefully degrades)
internal/apicatalog/        Deprecated/removed Kubernetes API ruleset and versioned catalog foundation
internal/rules/             Rule interface, Registry, and all 23 check implementations
internal/findings/          Finding schema, confidence tiers, fingerprinting
internal/plan/              Multi-hop upgrade planner: version discovery, hop generation, per-rule projection policy
internal/report/            Terminal / JSON / Markdown / HTML renderers (shared dedup logic)
internal/reportserver/      Local-only post-scan HTTP report serving (report.html, findings.json, embedded Console)
web/                        React Console (Vite + TypeScript), built once and embedded into the Go binary via go:embed
testdata/                   Fixture clusters for deterministic rule testing
demo/                       kind demo cluster manifests (no committed sample output — see demo/README.md)
deploy/                     ClusterRole, IAM policy (Terraform module planned, not shipped)

Confidence tiers

Tier Meaning
STATIC_CERTAIN Provable directly from live objects or provider data treated as ground truth
PROVIDER_REPORTED Relayed from AWS's own judgment (e.g. EKS Insights), with caveats
OBSERVED Confirmed from time-sensitive live state such as endpoint/PDB/APIService status
INFERRED Risk pattern flagged without direct proof

Priority (P1–P4)

Every finding carries four independent axes, and it's easy to conflate them:

  • Severity — Blocker/Warning/Info — communicates operator urgency.
  • Confidence — the tier above — how certain the evidence is.
  • Priority — P1 through P4 — what to fix first, and whether that fix has to happen before you touch anything else.
  • Upgrade gateblock, operator_decision, or allow — drives the blocker count, go/no-go Result, and exit code.

Two Blocker-severity findings can carry very different priority: a global admission-webhook outage needs attention right now (P1), while an incompatible add-on whose catalog impacts intersect the selected operation is P2 because that operation may fail without the add-on update.

Priority Meaning
P1 Global API write blocker — may block kubectl apply/patch/scale, Helm upgrades, or controller reconciliation cluster-wide, including the commands needed to fix every other finding. Always wins regardless of which rule caught it.
P2 Removed API, or a cluster-critical infrastructure component (CNI, DNS, kube-system) affected by an otherwise-lower-priority condition — a resource or behavior that will fail once the target upgrade actually happens.
P3 Node-drain risk — something that can cause a node drain to stall or fail during maintenance or a managed node group upgrade.
P4 Unhealthy workload, add-on, or node-readiness risk — the upgrade shouldn't begin while these are unhealthy, but they aren't an active blocker on their own.

Every finding also carries three fields that explain the priority, not just state it (real fields, from a live scan's findings.json):

{
  "ruleId": "PDB-001",
  "severity": "Blocker",
  "priority": "P3",
  "priorityReason": "Node drain may fail during maintenance or a managed node group upgrade.",
  "affectedScope": "workload",
  "impactScopes": ["node_drain", "worker_rollout"],
  "upgradeGate": "operator_decision",
  "canUpgradeContinue": false
}
  • priorityReason — why this priority, in one sentence.
  • affectedScopeglobal, cluster, node, workload, or addon: what class of resource the fix actually touches.
  • impactScopes — which operation can be affected, such as node_drain, worker_rollout, api_write, or workload_restart.
  • upgradeGate — whether this finding blocks the current operation, is allowed, or needs an operator decision because context is missing.
  • canUpgradeContinuefalse for upgradeGate: "block", operator_decision, and P1; true otherwise. The terminal output spells this out too: "do not attempt other remediation until this is fixed" vs. "can continue upgrade planning".

Two conditions escalate past a rule's normal priority, regardless of which rule caught them:

  • Global blocker — a fail-closed admission webhook with catch-all scope and no healthy backend escalates straight to P1, because its outage can break the kubectl/Helm commands needed to fix everything else.
  • Critical infrastructure — the same condition on an ordinary application workload vs. a kube-system or CNI/DNS/kube-proxy/ autoscaler component can escalate priority, since the blast radius is the whole cluster rather than one workload. NODE-003 remains a Warning/P3 by default until KubePreflight has contextual evidence that replacement nodes will not retain the legacy label.

Findings are sorted Priority-first everywhere a human reads them — terminal, Markdown, HTML, the Console's Findings and Evidence tabs, and Next Actions grouping — so the first thing listed is always the most urgent thing to fix, not just the first thing the scanner happened to find. report.html and the Console both show a one-line P1–P4 legend near the first finding. findings.json's findings array itself is not re-sorted — it stays in rule-registration order as the canonical machine-readable record; a jq pipeline that cares about priority order should sort on the priority field itself (P1 < P2 < P3 < P4 lexically, so a plain string sort already works).

Next Actions vs. Blockers/Warnings — why findings aren't just deduped

A resource hit by multiple rules (e.g. a webhook firing both WH-001 and WH-002) still gets two separate entries in the Blockers/Warnings sections — that's correlation evidence, and collapsing it would hide why something is risky. The Next Actions section is different: it groups by resource and picks the higher-severity finding's remediation as the one instruction to follow, with any other finding's distinct guidance appended as a one-line pointer — so you get one clear step per resource, not several that might read as contradictory.

KubePreflight Console (local viewer)

Interactive scans open a local embedded Console automatically. A React app (web/) is built once at release time and embedded into the kubepreflight binary via go:embedend users never install Node or run a separate server. kubepreflight scan starts a local, 127.0.0.1-only HTTP server (see Output above) that serves report.html, findings.json, and the Console together.

Summary → findings → next actions, on the same disposable-cluster evidence as the scan/comparison GIFs above.

The Console URL's ?findings= query param is pre-filled with the just-completed scan's results, so opening it loads the dashboard immediately — no blank import screen, no manual file picker. It derives the readiness dashboard, filters by severity/confidence/namespace/search, and shows evidence plus structured safe/emergency/break-glass remediation and verification commands in a detail drawer per finding. Large findings and comparison lists render in bounded pages while counts and filters still cover the complete loaded report, so a scan with thousands of findings stays usable without changing the findings.json contract. The Summary tab's Upgrade Readiness scorecard renders each category's rule IDs as clickable chips — clicking one switches to the Findings tab pre-filtered to that exact rule. The Compare tab uploads a second, earlier findings.json as a baseline and diffs it against the currently-loaded scan entirely in the browser — new/resolved/changed/unchanged findings, verdict movement, and readiness-score delta, using the same fingerprint-matching contract as kubepreflight compare (see Scan comparison). It has no backend, authentication, database, telemetry, or cluster connector; imported files stay in the browser. report.html remains the static, shareable CAB/export artifact — the Console is for interactive investigation.

Use --serve-report never for CI, scripts, or any run where nothing should block on a local server (this is also the default when stdout isn't a terminal or CI is set — see "Usage" above).

See web/README.md for how the Console is built and tested. This is intentionally not a multi-tenant SaaS surface; hosted fleet features remain gated on discovery and pilot signal. The staged product boundary is documented in docs/product-shape.md.

Multi-hop upgrade planner

Real EKS upgrades from an old cluster (e.g. 1.29) to a much newer target (e.g. 1.36) happen one minor version at a time — a single scan against the final target is misleading, since live-cluster-state findings (node/kubelet skew, PDB overlap, webhook health) won't necessarily still be true several hops from now. kubepreflight plan provides a sequenced, hop-by-hop readiness view:

./kubepreflight plan \
  --from-version 1.29 \
  --to-version 1.36 \
  --manifests ./k8s

Honestly, not optimistically:

  • The immediate next hop is a real, exact scan — identical to what scan would produce for that target version.
  • Further hops only project what's honestly predictable (a manifest's deprecated API doesn't change hop to hop; AWS's own EKS Upgrade Insights/add-on compatibility API is authoritative for whatever version it's asked about) and label everything else — node skew, PDB overlap, webhook health — as checks requiring a rescan once that hop is actually reached, with the exact scan command to run at that point.
  • Writes upgrade-plan.json alongside the immediate hop's normal findings.json/report.html.
  • Plan-generated report.html includes the upgrade path and readiness verdict. The Console automatically loads upgrade-plan.json, adds an Upgrade Planner tab, distinguishes current-live from projected findings, and shows exact rescan commands for future-hop coverage requirements.
Upgrade action plan (change-ticket checklist)

plan can also emit a phased, operator-facing checklist derived from the immediate hop's findings — built for pasting into a change ticket, not for re-parsing programmatically:

./kubepreflight plan \
  --from-version 1.32 --to-version 1.36 \
  --action-plan-out action-plan.json \
  --action-plan-md action-plan.md

action-plan.md groups actions into four phases — Critical Blockers, Upgrade Preparation, Upgrade, and Validation — with Phase 3 gated on every required Phase 1 action being resolved. Each action lists its source rule IDs, success criteria, and copy-pasteable inspect commands — for example, a real run against a cluster with a stuck PDB and a deprecated node-role label produces:

### Phase 1 - Critical Blockers

Fix findings that can prevent a safe upgrade or make upgrade remediation fail.

**Gate:** Phase 3 is blocked until every required action in this phase is resolved.

- [ ] **Resolve disruption budget and unhealthy workload risks** (`required`, required)
  - Why: Required because matching findings were detected in the current assessment.
  - Source rules: `PDB-001`
  - Success: Workloads protected by PodDisruptionBudgets can tolerate at least one voluntary disruption.
  - Command: `kubectl get pdb --all-namespaces`
- [ ] **Replace deprecated master node label selectors** (`recommended`, optional)
  - Source rules: `NODE-003`
  - Command: `kubectl get nodes --show-labels | grep -E 'node-role.kubernetes.io/(master|control-plane)'`

Most actions are always required once their source rule fires at all. A couple — currently the WORKLOAD-001 and NODE-003 checklist items — are recommended instead when only a Warning-severity finding matches and no Blocker does, since those two checks are explicitly designed to not be hard upgrade blockers on their own (see Priority (P1–P4)'s canUpgradeContinue). action-plan.json carries the same structure machine-readably (schemaVersion: kubepreflight.io/upgrade-action-plan/v1) if you want to drive a ticketing integration instead of pasting Markdown by hand.

Scan comparison

A single scan is a snapshot; kubepreflight compare turns two of them into a remediation-progress view — what got fixed, what's new, what changed, and whether the readiness score and verdict actually moved. The Console's Compare tab does the same thing entirely client-side — upload a baseline findings.json against whatever scan is already loaded, no CLI needed (see KubePreflight Console).

Terminal showing seven real blockers on a disposable EKS cluster, real remediation commands fixing four of them, then kubepreflight compare printing readiness score moving 18 to 56 with a passing gate decision.

Real blockers → real remediation → real comparison. Same live-cluster session as the scan above — see demo/live-eks.

kubepreflight compare \
  --baseline previous-findings.json \
  --current current-findings.json \
  --json-out comparison.json \
  --markdown-out comparison.md

Findings are matched by fingerprint only — never by message text, remediation wording, or array position, any of which can change between kubepreflight versions without the underlying issue changing. A finding is:

Baseline Current
New absent present
Resolved present absent
Changed present, tracked fields differ present, tracked fields differ
Unchanged present, no tracked difference present, no tracked difference

Tracked fields are severity, priority, confidence, canUpgradeContinue, and affectedScope — a wording tweak to a finding's message/remediation text is deliberately not tracked, so a kubepreflight version bump alone never shows up as a pile of "changed" findings.

--baseline/--current accept any findings.json, including one from an older kubepreflight version: missing priority/canUpgradeContinue are backfilled with the current policy, and a missing Upgrade Readiness scorecard is rebuilt from the document's own findings — the same way scan derives them today. Coverage (e.g. an INCOMPLETE baseline) is never altered — partial evidence is never upgraded into a clean result just because a newer field happened to be absent.

Both scans should target the same --target-version — a finding's fingerprint is scoped to the target version it was scanned against, so comparing scans from two different target versions makes even a genuinely unchanged finding look like a resolved+new pair. compare detects this and emits an explicit warning in the output rather than silently misleading you.

comparison.json follows its own schema (schemaVersion: kubepreflight.io/scan-comparison/v1) with a summary (verdict movement, readiness-score delta, and per-bucket counts including newBlockers/resolvedBlockers) plus the full new/resolved/changed/ unchanged finding lists — see internal/comparison/model.go for the exact shape. Exit codes follow the same convention as scan/plan: 0 on success, 1 for invalid flags or an unparseable input document, 4 for a filesystem/runtime failure (e.g. --baseline doesn't exist).

Validated on real EKS

For the full evidence-backed story, see docs/case-studies/eks-1.31-to-1.32.md: a real EKS 1.31 to 1.32 run with clean baseline evidence, seeded risks, remediation comparison, control-plane upgrade evidence, rollback assessment, screenshots, and a CI comparison gate.

Sanitized real-EKS case-study evidence → comparison decision → embedded Console and actionable reports. This walkthrough replays the public, sanitized EKS 1.31 → 1.32 case-study artifacts by running kubepreflight compare against them — it is not a live cluster scan. No cluster access required to reproduce — see demo/eks-case-study.

This isn't just tested against fixtures — it's been run against a real, throwaway EKS cluster (EKS 1.35, us-east-1) end to end:

  • A clean cluster scan (--provider eks) returned Result: CLEAN with AWS enrichment: true.
  • The same cluster, seeded with worst-case resources (overlapping zero-disruption PDBs, a fail-closed catch-all webhook, a manifest with a removed API), returned Result: BLOCKED and correctly fired API-001, PDB-001, PDB-002, WH-001, and WH-002.
  • Finding counts matched exactly across findings.json, report.html, and the Console in both runs.
  • The test cluster and all seeded resources were deleted afterward, with aws eks list-clusters confirming no orphaned resources remained.

To validate against a throwaway real EKS cluster yourself, see demo/eks/README.md. This creates billable AWS resources — use a sandbox account and delete the cluster immediately after testing.

This same case study is also the backbone of the public website: Explore the public website and real EKS case study (website repository).

Not included yet

  • SaaS/hosted backend
  • SARIF output
  • Auto-remediation (and never planned as a default — see Safety)
Known limitations

Accepted false-positive controls are governed in docs/false-positive-governance.md. Every suppression or severity downgrade must carry evidence, positive and negative tests, spoofing regression coverage, and documented scope.

  • API-001 excludes live Event objects entirely (not even as Info) — they're emitted by whatever client-go version the calling controller links, self-expire in about an hour, and there's no single object to fix.
  • FlowSchema/PriorityLevelConfiguration objects kube-apiserver itself owns (marked with its own apf.kubernetes.io/autoupdate-spec annotation, confirmed against a live cluster) report as Info, not Blocker, with remediation text that says there's usually nothing to do — a user-created FlowSchema/PriorityLevelConfiguration without that annotation is unaffected and still reports as a normal Blocker.
  • EndpointSlice objects the built-in EndpointSlice controller created (marked with the real endpointslice.kubernetes.io/managed-by label) report as Info the same way. One narrow, disclosed exception remains: an EndpointSlice with neither that label nor a Service owner reference — observed on the default/kubernetes Service on some clusters — still reports as a normal Blocker, since there's no confirmed signal to distinguish that case from a genuine migration task. Tracked as an open follow-up, not silently ignored.

Release history

  • v0.1.0 (initial release) — CLI, the first locked-MVP checks, terminal/JSON/Markdown/HTML reports, graceful AWS degradation, and a kind demo walkthrough
  • v0.2.x — full-width Console/report, multi-hop planner, upgrade action plan, upgrade-risk priorities (P1–P4), expanded live-cluster/manifest/EKS checks, and real EKS validation
  • v0.3.0 — API Compatibility scorecard
  • v0.4.x — consolidated Upgrade Readiness scorecard, --manifests-only, GitHub Action support, multi-platform release artifacts, checksums, SPDX SBOM, and GHCR image
  • v0.5.0kubepreflight compare for upgrade-progress diffing, with a matching Console comparison experience
  • v0.6.x — expanded admission-webhook safety, including TLS/CA, expiry, unsafe scope, timeout, wildcard, and fail-closed coverage checks
  • v0.7.0 — expanded add-on compatibility coverage for CoreDNS, CSI drivers, metrics-server, ingress, cert-manager, and external-dns
  • v0.8.x — drain readiness covering singleton workloads, node-local storage, scheduling constraints, capacity shortage, StatefulSet health, and DaemonSet rollout health
  • v0.9.x — scale and resilience: bounded collector concurrency, cancellation and timeout handling, partial-collector-failure hardening, large-report performance, and correct same-version/downgrade framing
  • v0.10.0 — versioned compatibility catalog for EKS-managed and workload add-ons, with maintenance checks
  • v0.11.x — authoritative Kubernetes API lifecycle catalog, unsupported-target rejection, and Console responsive findings fixes
  • v0.12.0 — EKS rollback eligibility, operational readiness, recommendations, reports, and Console surfaces, validated against real EKS
  • v0.13.0 — comparison regression-gate policy and published compare GitHub Action with summaries, annotations, artifacts, and end-to-end tests
  • v0.14.0 — reproducible real EKS 1.31→1.32 upgrade and rollback case study, including a real Blocker bug fix and sanitized evidence
  • v0.15.0 — native sensitive-identifier redaction across scan, plan, compare, and rollback report output consumed by the Console
  • v0.16.x — release and supply-chain trust: build provenance, GHCR alias consistency, exhaustive leak gates, published-artifact verification, CodeQL, Scorecard, and Trivy scanning
  • v0.17.x — published installation matrix covering Linux, macOS, Windows, GHCR aliases, go install, and GitHub Actions
  • v1.0.0 — real released-artifact validation against disposable EKS; scan, plan, compare, and rollback assessment parity between binary and container; false-positive governance; stable compatibility contract; scale hardening; and a zero-violation Console accessibility audit
  • v1.1.0 — context-aware upgrade gating, a downgraded (warning, not blocker) deprecated-master-label finding, and a five-minute KubePreflight testing guide
  • v1.2.0 (current) — trusted rollback evidence and decision hardening: findings must be a valid, correctly-targeted, same-cluster, sufficiently fresh document before rollback checks trust them, and a confirmed provider eligibility blocker now always preserves its do_not_proceed/exit-2 contract even when that operational evidence is missing or incomplete

Roadmap

  • Next — SARIF output and deeper AKS/GKE provider coverage
  • Later — opt-in network probes, CloudWatch telemetry, and Slack/Jira integrations
  • Demand-gated — hosted collaboration or backend services only after repeated user demand

CI / dev verification

go test ./...
go vet ./...
scripts/check-v1-compatibility-contract.sh
scripts/check-exemption-governance.sh
scripts/check-compatibility-catalog.sh
npm --prefix web test
npm --prefix web run build
scripts/check-console-dist.sh
docker build -t kubepreflight:local .

CI runs this verification matrix on pushes and pull requests. scripts/check-v1-compatibility-contract.sh validates the documented v1 compatibility contract for CLI commands, flags, exit codes, schema identifiers, rule IDs, priorities, fingerprints, and conservative incomplete-evidence behavior. scripts/check-console-dist.sh rebuilds the Console and diffs it against the committed web/dist — it fails if a web/src change was committed without also committing the rebuilt, embedded Console assets. scripts/check-exemption-governance.sh validates false-positive exemption metadata, documentation anchors, referenced tests, audit inventory, and production callsite references. scripts/check-compatibility-catalog.sh validates the embedded add-on compatibility catalog (schema, required coverage, deterministic lookup) and prints its matrix — see Compatibility Catalog.

Manually generating a report against a real cluster

go test/npm test don't catch real layout or click/scroll behavior — Vitest's jsdom environment can't compute CSS grid/box layout, and Go's HTML tests only check for output substrings. To visually verify a report.html/Console change, build the binary and run a real scan against a connected cluster:

cd ~/kubepreflight
rm -rf bin && mkdir -p bin
go build -o bin/kubepreflight ./cmd/kubepreflight
./bin/kubepreflight --help

# Confirm the target cluster is reachable
kubectl config current-context
kubectl get nodes

# Or, for a local kind cluster:
kind get kubeconfig --name kp-smoke > /tmp/kp-smoke.kubeconfig
kubectl --kubeconfig /tmp/kp-smoke.kubeconfig get nodes

# Generate the report into its own directory
mkdir -p /tmp/kp-report && cd /tmp/kp-report
~/kubepreflight/bin/kubepreflight scan \
  --kubeconfig /tmp/kp-smoke.kubeconfig \
  --target-version 1.36 \
  --findings-out findings.json \
  --output all \
  --serve-report never || true

ls -lah   # findings.json, report.md, report.html

python3 -m http.server 8080
# then open http://127.0.0.1:8080/report.html

|| true matters here: a scan that finds blockers exits 2 (see Exit codes) — that's the scan working correctly, not a tooling failure, and the report is written either way. --output all --serve-report never writes findings.json/report.md/ report.html to disk without blocking on the CLI's own server, which is what lets python3 -m http.server serve those same files afterward. If you'd rather use the CLI's built-in server instead (auto-opens the report, prints the report/Console URLs, no separate python3 step needed), drop --output all --serve-report never — see Usage above.

Contributing

Read-only checks only. No auto-remediation, no write actions, no telemetry phone-home in the OSS core. New checks should include a fixture test (see internal/rules/*_test.go for the pattern: positive fixture, negative fixture, Registry wiring).

First-time testers can share actionable feedback with the First External Test Report issue template.

License

Apache 2.0. See LICENSE.

Author

Built by @imneeteeshyadav98.

Directories

Path Synopsis
cmd
apicatalogcheck command
Command apicatalogcheck validates the embedded versioned API catalog (internal/apicatalog/versioned_catalog.json) and reports its coverage and staleness in a stable, human-readable form.
Command apicatalogcheck validates the embedded versioned API catalog (internal/apicatalog/versioned_catalog.json) and reports its coverage and staleness in a stable, human-readable form.
compatcatalogcheck command
Command compatcatalogcheck validates the embedded compatibility catalog (internal/compatcatalog/catalog.json) and reports its coverage and staleness in a stable, human-readable form.
Command compatcatalogcheck validates the embedded compatibility catalog (internal/compatcatalog/catalog.json) and reports its coverage and staleness in a stable, human-readable form.
consoledevserver command
Command consoledevserver starts the real local report server (internal/reportserver, the same code path `kubepreflight scan` uses) against an existing report output directory, so the Console browser smoke test (web/tests/browser_smoke.py) can exercise the actual embedded-Console mount at /console/ instead of a stand-in static file server.
Command consoledevserver starts the real local report server (internal/reportserver, the same code path `kubepreflight scan` uses) against an existing report output directory, so the Console browser smoke test (web/tests/browser_smoke.py) can exercise the actual embedded-Console mount at /console/ instead of a stand-in static file server.
exemptioncheck command
Command exemptioncheck validates the false-positive governance registry, audit inventory, documentation anchors, referenced regression tests, and known production callsites.
Command exemptioncheck validates the false-positive governance registry, audit inventory, documentation anchors, referenced regression tests, and known production callsites.
kubepreflight command
Command kubepreflight is the CLI entrypoint.
Command kubepreflight is the CLI entrypoint.
v1compatcheck command
Command v1compatcheck validates KubePreflight's documented v1 compatibility contract against the current implementation.
Command v1compatcheck validates KubePreflight's documented v1 compatibility contract against the current implementation.
internal
apicatalog
Package apicatalog holds the deprecated/removed Kubernetes API ruleset used by API-001.
Package apicatalog holds the deprecated/removed Kubernetes API ruleset used by API-001.
buildinfo
Package buildinfo holds the release identity injected into the binary at build time via -ldflags (see .github/workflows/release.yml).
Package buildinfo holds the release identity injected into the binary at build time via -ldflags (see .github/workflows/release.yml).
cli
Package cli wires the Cobra command tree for the kubepreflight binary.
Package cli wires the Cobra command tree for the kubepreflight binary.
collectors/aws
Package aws collects a read-only snapshot of EKS/EC2 provider state used by the AWS-enrichment checks (EKS-INSIGHT, ADDON-001, NODE-002).
Package aws collects a read-only snapshot of EKS/EC2 provider state used by the AWS-enrichment checks (EKS-INSIGHT, ADDON-001, NODE-002).
collectors/k8s
Package k8s collects a read-only snapshot of cluster state used by the rules engine.
Package k8s collects a read-only snapshot of cluster state used by the rules engine.
collectors/manifest
Package manifest collects deprecated-API usage from static Kubernetes manifests: raw YAML directories and rendered Helm charts.
Package manifest collects deprecated-API usage from static Kubernetes manifests: raw YAML directories and rendered Helm charts.
comparison
Package comparison diffs two findings.json documents (a baseline and a current scan of the same cluster/target) into new/resolved/changed/ unchanged findings plus verdict and readiness-score movement.
Package comparison diffs two findings.json documents (a baseline and a current scan of the same cluster/target) into new/resolved/changed/ unchanged findings plus verdict and readiness-score movement.
findings
Package findings defines the KubePreflight finding schema: severities, confidence tiers, structured resource references, and fingerprints.
Package findings defines the KubePreflight finding schema: severities, confidence tiers, structured resource references, and fingerprints.
gate
Package gate turns a comparison.Comparison into a deterministic CI pass/fail/neutral decision under a configurable policy.
Package gate turns a comparison.Comparison into a deterministic CI pass/fail/neutral decision under a configurable policy.
plan
Package plan implements the multi-hop upgrade planner: given a cluster's current Kubernetes version and a further-out target, it generates the sequence of one-minor-version hops between them and assembles a PlanReport describing what's known for certain (the immediate next hop, scanned exactly like `scan` does) versus what's honestly only a prediction or requires a rescan once that hop is actually reached (see classify.go).
Package plan implements the multi-hop upgrade planner: given a cluster's current Kubernetes version and a further-out target, it generates the sequence of one-minor-version hops between them and assembles a PlanReport describing what's known for certain (the immediate next hop, scanned exactly like `scan` does) versus what's honestly only a prediction or requires a rescan once that hop is actually reached (see classify.go).
providers
Package providers identifies the cloud/cluster providers KubePreflight's CLI knows about (today: eks, aks, gke, or cluster-only when omitted).
Package providers identifies the cloud/cluster providers KubePreflight's CLI knows about (today: eks, aks, gke, or cluster-only when omitted).
providers/aks
Package aks identifies the AKS provider for CLI wiring.
Package aks identifies the AKS provider for CLI wiring.
providers/eks
Package eks identifies the EKS provider for CLI wiring.
Package eks identifies the EKS provider for CLI wiring.
providers/gke
Package gke identifies the GKE provider for CLI wiring.
Package gke identifies the GKE provider for CLI wiring.
redact
Package redact removes AWS account IDs/ARNs and EC2-style internal node hostnames from an already-fully-built report or assessment, for users who intend to share generated evidence (findings.json, report.html, rollback-assessment.json, upgrade-plan.json) outside their organization.
Package redact removes AWS account IDs/ARNs and EC2-style internal node hostnames from an already-fully-built report or assessment, for users who intend to share generated evidence (findings.json, report.html, rollback-assessment.json, upgrade-plan.json) outside their organization.
report
Package report renders a findings.Report to an output format.
Package report renders a findings.Report to an output format.
reportserver
Package reportserver serves generated KubePreflight reports on a local-only HTTP listener.
Package reportserver serves generated KubePreflight reports on a local-only HTTP listener.
rollback
Package rollback defines the assessment document used by the EKS rollback-readiness workflow.
Package rollback defines the assessment document used by the EKS rollback-readiness workflow.
rules
Package rules defines the check interface and registry every deterministic check (API-001, WH-001, etc.) registers against.
Package rules defines the check interface and registry every deterministic check (API-001, WH-001, etc.) registers against.
testutil
Package testutil provides fixture-loading helpers shared by tests across packages.
Package testutil provides fixture-loading helpers shared by tests across packages.
Package web embeds the built Console (see web/README.md for the build step) so the CLI can serve an interactive viewer without a Node.js runtime dependency for end users.
Package web embeds the built Console (see web/README.md for the build step) so the CLI can serve an interactive viewer without a Node.js runtime dependency for end users.

Jump to

Keyboard shortcuts

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