Verdict and score¶
Every report, from scan, the agent or the server, carries two answers:
a verdict for gating and a score for trends. Both come from the same
pure function, engine.Evaluate(inventory, knowledge base, target, date).
The verdict¶
| Verdict | When |
|---|---|
blocked |
At least one finding is a blocker. |
unknown |
No blocker, but a required check was not assessed, so a blocker may have been missed. |
ready |
No blocker, and every required check was assessed. |
ready in every output is verdict == "ready"; it is kept for clients of
v0.1, when it meant "no blockers". A failed required check never leaves a
report reading ready.
A check the inventory does not report at all (its capabilities map has
no entry for it, as in a hand-built, third-party or empty inventory) was
not assessed, and for a required check below that is a required gap too:
api-usage always, versions and addons in a cluster inventory. The
server judges such a push unknown at best; every collector since v0.1.0
reports them.
Required checks:
api-usage, always: which removed or deprecated APIs objects use. A partial api-usage (some resources could not be listed) is required only when what it skipped includes an API removed at or before the target.kb-coverage, always: the target must be at or below the knowledge base's horizon. A newer target cannot be judged: the knowledge base does not know what that release removes.target, for live clusters: the target must be an upgrade. A target at or below the minor the oldest kube-apiserver already runs (a downgrade or the same minor) is reported as a requiredtargetgap, so the verdict isunknown, neverready: every check judges a newer minor. A target below 1.16, the oldest minor the knowledge base covers (a typo such as1.4for1.40, which is what YAML makes of an unquoted number), is not a verdict: it is refused as an input.versions, for live clusters: the server version (skew needs it). A partialversionsis required too when it names a component whose version upstream would have told and was not read (a component image with a digest orlatestfor a tag, or any unread kube-apiserver, kube-controller-manager or kube-scheduler pod): that component may be the one past the skew policy. A kube-proxy pod on a vendor image of another name (Oracle OKE's) is an optional gap. A partialversionsthat skippednodes(no Node was listed, so kubelet skew and node runtimes were not assessed) is required too. See Version skew.addons, for live clusters: add-on detection from images, labels, IngressClasses and charts. Without the cluster-wide pod list the capability is partial and still required: the Helm releases and IngressClasses that were read are matched (an EOL chart-installed ingress-nginx still blocks), but an add-on installed any other way goes undetected, so a clean result isunknown. A role that cannot list IngressClasses alone leaves an optional partial gap. The agent lists the pods outsidekube-systemonly every few ticks and detects add-ons from the last full pass in between (--pod-pass-every,--pod-pass-max-age); that is no gap, since the pass was complete, and the report'saddOnEvidenceAgeSecondssays how old it was. See Scale and cost.volumes, for live clusters, when it is partial and skipped the PersistentVolume or StorageClass list (refused, or failed on any page): required when the knowledge base has a plugin removed at or before the target (classificationremoved, notcsi-migration) that the unread list could have shown, a PersistentVolume source field for the PersistentVolume list and an in-tree provisioner for the StorageClass list. The earliest such removal isscaleIOat 1.22, so in practice the gap is required at every target from 1.22: a refused list can hide anrbdPersistentVolume, which blocks 1.31. CSI-migration plugins only ever warn, so they never make it required. A partialvolumesthat skipped only pods is optional, asaddonsis then a required gap already, and one an older collector does not report at all is optional. See Volume plugins.
Not required, because a blocker cannot hide behind them, or because managed
platforms routinely deny them: deprecated-calls (the apiserver's
/metrics, whether it is denied or does not answer in time), helm
(Secrets are often forbidden; the objects themselves are still checked by
api-usage, and add-ons by their images and labels), crds (CRD
versions are the add-ons', not the Kubernetes target's; see
CRD versions), and volumes
outside the case above. In files mode api-usage and kb-coverage are
required and add-ons are assessed from the manifests without being
required (an image injected at admission is not in them); volumes is
never required; versions, deprecated-calls and helm are reported
with the reason files mode.
What a verdict does not cover¶
ready means that no blocker was found among what the engine judges, and
that every required check ran. The engine judges the apiVersion and kind
of each object against the API lifecycle data, and the in-tree volume
plugins that pods, PersistentVolumes and StorageClasses name (Volume plugins).
It does not assess other field-level removals inside an API that is
still served: an object that uses the seccomp alpha annotations,
Service.spec.externalIPs or beta.kubernetes.io/os scans as ready with a
score of 100. The table and Markdown reports end with a line saying so
("Field-level removals other than in-tree volume plugins are not checked by
this version."). This is a disclosure, not a
check: it changes no score, verdict or exit code, and the JSON report does
not carry it.
Severity, by category¶
Severity depends on the category. Only some categories depend on the target.
| Category | Blocker | Warning | Info |
|---|---|---|---|
removed-api |
An object (or a Helm release's stored manifest) uses an API removed at or before the target; or, in --files mode and the gate (the manifests are proposed state), a manifest uses an API version the target does not serve yet, titled "not served until X" (applying it fails the same way). |
The API is removed in the minor after the target. A removal after the knowledge base's horizon is titled "(projected)" in either severity. | — |
deprecated-api |
— | A Helm release's stored manifest uses a deprecated API that is not removed by the target, even when the live scan also flags the object (as info). | An object uses an API that is deprecated (or will be, after the target) and not removed by the minor after the target. |
deprecated-api-in-use |
The apiserver saw requests to an API removed at or before the target, and no object finding covers it (or every object of the one that did was suppressed by annotation or an object-scoped rule). The removal release is the knowledge base's when it knows the API, else the metric's removed_release label. |
Removed in the minor after the target. | No removal release known to the knowledge base or reported. |
eol-addon |
The add-on, or its installed release line, is past end of life. Whatever the target. | A node container runtime past end of life (it ships with the node image, not with Kubernetes). | — |
eol-approaching |
— | End of life within 90 days. | — |
chart-incompat |
The installed release line's (or a registry compat range's) Kubernetes range excludes the target; a Helm chart's kubeVersion excludes the target. |
— | A chart kubeVersion that does not parse. |
version-skew |
Kubelets or kube-proxy that would fall outside the policy once the control plane is at the target; a controller-manager or scheduler newer than the apiserver (whatever the target). | Violations today: kubelets or kube-proxy too far behind, kubelets or kube-proxy newer than the apiserver, HA apiserver spread, controller-manager or scheduler too far behind. | Unparseable kubelet versions; a target more than one minor ahead, which takes several upgrades (version-skew/upgrade-path names each step). |
kb-stale |
— | The cluster or the target is newer than the knowledge base's horizon. | — |
addon-no-data |
— | — | A detected add-on whose version has no lifecycle data. |
unknown-api |
— | — | An object of a built-in API group (core, or any group the generator's scheme registers (k8s.io/api plus the apiextensions and apiregistration schemes), such as imagepolicy.k8s.io) at a version or kind the knowledge base does not know, such as the typo apps/v1beta9: whether the target serves it was not assessed. API groups of CRDs produce nothing. |
support-lifecycle |
A cluster on EKS, GKE or AKS whose Kubernetes minor is past the provider's standard support (in extended support, or out of support). Whatever the target. | Standard support for the minor ends within 90 days. | — |
crd-version |
Custom resources at a version their CRD does not serve (served: false, or no longer listed): the apiserver rejects them. Whatever the target. |
Custom resources written through a version the CRD marks deprecated: true; a status.storedVersions entry the CRD no longer serves. |
A deprecated CRD version nothing was found using. |
volume-plugin |
A pod, pod template or PersistentVolume names an in-tree volume plugin removed with no migration path at or before the target (glusterfs, cephfs, rbd; gitRepo, disabled from 1.33), or a StorageClass names its in-tree provisioner (kubernetes.io/rbd): pods naming it, or mounting a claim bound to such a PersistentVolume, do not start, and the class's new claims are not provisioned. |
Such a plugin removed in the minor after the target; a plugin served only through CSI migration at the target (awsElasticBlockStore, gcePersistentDisk, ...), naming the CSI driver that must be installed. Never a blocker, since a cluster with the driver is fine. |
A plugin deprecated only (flexVolume), or whose removal or CSI-only minor is later. See in-tree volume plugins. |
Version skew has the skew rules; the
add-on registry the EOL data, and
managed-provider support the support-lifecycle finding
and its cost line.
The score¶
score = max(0, 100 − min(75, 25 × blockers) − min(20, 5 × warnings))
Info findings are never scored, and suppressed findings are excluded. The caps keep one noisy category from zeroing the score, and keep a cluster with many warnings distinguishable from one with a blocker. The score is for trends and comparison: gate on the verdict, not on a score threshold. Per-team scores apply the same formula to each team's findings.
Each team also has a verdict, on the report's rules: blocked by a
blocker of its own or one no team is attributed (it cannot be ruled out as
the team's, and it blocks the cluster's upgrade either way), otherwise
unknown when a required check was not assessed (it may have hidden a
blocker of any team), otherwise ready. A team's ready is true only when
its verdict is ready, so no team reads ready on an unknown cluster.
Another team's blocker leaves it ready; gate on the cluster's verdict,
not on one team's. A team with no findings is not listed, and there is no
per-namespace score.
The formula is part of the public contract (compatibility policy): changing it is a breaking change.
Deterministic, for a given date¶
The same inventory, knowledge base, target and date give the same
report, byte for byte (the golden tests in internal/engine pin it). The
date matters because end-of-life is calendar-based: an add-on whose end of
life is 2026-11-30 turns from a warning (eol-approaching) into a blocker
(eol-addon) on that day, with nothing in the cluster changing. End-of-life
dates are compared at 00:00 UTC on the date. Two other inputs move without
any change in the cluster: the knowledge base (with each release) and the
default target (when the cluster is upgraded).
Consequences:
- The agent re-evaluates every tick, so a passing EOL date reaches the
ClusterReadinesswithin one interval. - The server re-evaluates stored snapshots hourly and just after each UTC
midnight; until it has, reads mark a stored verdict
outdated. - A CI gate against a server (
POST /api/v1/gate?cluster=) or with a floating action version can change its answer on an unchanged pull request. Pin the version for a reproducible gate.
The gate¶
scan --fail-on blocker|warning|never (default blocker) exits 2 when a
finding at or above the threshold remains after suppression and is not
unchanged against a --baseline, or when the verdict is unknown,
unless --allow-incomplete. A target that is not an upgrade of the cluster
exits 2 even with --allow-incomplete; only --fail-on never always exits 0,
that case included. Operational errors, and a --target below the knowledge
base, exit 1. The server's gate endpoint and the GitHub Action use the same rule.
Details: Suppressions and baselines.