README
¶
KubePreflight
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.
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.

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 direction — scan --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, whichkubepreflight rollback plan/rollback assessassess 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 writtenreport.md— Markdown, written with--output=mdorallreport.html— single-file, self-contained HTML report, written with--output=html/allor 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/watchon nodes, pods, poddisruptionbudgets, validating/mutatingwebhookconfigurations, services, endpointslices, customresourcedefinitions, deployments, daemonsets, plus a single allowlistedgeton thekube-system/corednsConfigMap (not a blanket ConfigMap list, enforced via a separate namespace-scopedRolewithresourceNames). 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 withkubectl 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 calleks: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 from127.0.0.1by default. Binding a non-loopback address requires the explicit--allow-remote-reportacknowledgement 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.yamlandbroken-webhook.yamldeliberately 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 equivalentplan/rollback assess/compareoutputs) 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-identifierstoscan/plan/rollback plan/rollback assess/comparebefore 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 — seeinternal/redact.compareredacts its own output independently of whether--baseline/--currentwere 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 gate —
block,operator_decision, orallow— drives the blocker count, go/no-goResult, 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.affectedScope—global,cluster,node,workload, oraddon: what class of resource the fix actually touches.impactScopes— which operation can be affected, such asnode_drain,worker_rollout,api_write, orworkload_restart.upgradeGate— whether this finding blocks the current operation, is allowed, or needs an operator decision because context is missing.canUpgradeContinue—falseforupgradeGate: "block",operator_decision, and P1;trueotherwise. 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-systemor CNI/DNS/kube-proxy/ autoscaler component can escalate priority, since the blast radius is the whole cluster rather than one workload.NODE-003remains 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:embed — end 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
scanwould 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
scancommand to run at that point. - Writes
upgrade-plan.jsonalongside the immediate hop's normalfindings.json/report.html. - Plan-generated
report.htmlincludes the upgrade path and readiness verdict. The Console automatically loadsupgrade-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).
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) returnedResult: CLEANwithAWS 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: BLOCKEDand correctly firedAPI-001,PDB-001,PDB-002,WH-001, andWH-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-clustersconfirming 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-001excludes liveEventobjects 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/PriorityLevelConfigurationobjects kube-apiserver itself owns (marked with its ownapf.kubernetes.io/autoupdate-specannotation, 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.EndpointSliceobjects the built-in EndpointSlice controller created (marked with the realendpointslice.kubernetes.io/managed-bylabel) report as Info the same way. One narrow, disclosed exception remains: anEndpointSlicewith neither that label nor aServiceowner reference — observed on thedefault/kubernetesService 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.0 —
kubepreflight comparefor 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.
- More DevOps/SRE tooling and runbooks: devops-sre-playbook
- Writing on SRE, platform engineering, and Kubernetes: devopsofworld.com
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. |