Add-on registry¶
The add-on registry is upgradescope's public dataset of add-on end-of-life
dates, release lines and Kubernetes compatibility: one YAML file per add-on
in registry/data/.
It is importable as a Go package on its own (registry), compiled into the
binary, and browsable from a running server at GET /api/v1/registry and in
the dashboard.
How an add-on is found¶
- Container images. Every pod's images are matched by repository path,
as a suffix on whole path segments, so mirrors and pull-through caches
match too; the version is read from the tag. Images of provider builds
(
gke.gcr.io/,gcr.io/gke-release/,mcr.microsoft.com/) follow the provider's support policy and only match entries written for them. Where one repository publishes two vendors' builds that only the tag tells apart, an entry claims its tags with a tag pattern:rancher/nginx-ingress-controlleris RKE2's build with a-hardenedNtag and Ingress NGINX with any other: RKE1's-rancherNbuilds, which RKE2's first releases also shipped (nginx-0.30.0-rancher1up to v1.20.11,nginx-0.46.0-rancher1in v1.21.2, both long out of support). An image pinned by digest alone has no tag to tell them apart: it is RKE2's build when its pod's labels or a Helm release in its namespace namerke2-ingress-nginx, and Ingress NGINX otherwise. An image that names no version (pinned by digest, or:latest) takes the version of its pod'sapp.kubernetes.io/versionlabel when the pod's labels name the same add-on by the rule under Pod labels below: ahelm templaterender or a kustomizeimages: digest:keeps the label though the image has no tag. A tag that names a version is never overridden by a label, a label naming another add-on gives nothing, and where pods sharing the image carry different labels the oldest wins. A component image's version is its product line, which no label is guessed in place of. - Component images. A product whose parts carry their own versions is
matched by those images too, each part's release line mapped to the
product line that ships it: Flux's
source-controllerv1.5 andhelm-controllerv1.2 are Flux 2.5. A part's line the entry does not map yet (a Flux release newer than the registry) reads as Flux with no version, anaddon-no-datainfo, and so do Flux v2's pre-GA 0.x releases, deliberately: endoflife.date lists no line for them. - Helm releases. A release whose chart name is in an entry's matchers is
that add-on, at the release's
appVersion(never the chart version). In the release's namespace, theappVersionwins over image tags and labels on the same release line (major.minor): a pod a patch behind the release, or one without a version tag, is the release. An image or label version on another line is a second install in that namespace, judged at its own version: an istioctl canary revision runningistio/pilot:1.28.10beside anistiodrelease at 1.31.1 inistio-systemgets the 1.28 line's findings, and so does an image tag overridden in the release's values onto another line than the chart'sappVersion. - Pod labels, for images no matcher knows (a rebuilt or renamed
controller image). A pod whose
app.kubernetes.io/name,helm.sh/chartchart name orapp.kubernetes.io/part-ofis an entry's ID or chart matcher is that add-on; the version isapp.kubernetes.io/versionwhen the name label (or, without one, the chart label) named it. Pods running a provider build are never claimed through their labels. The same label version fills in an image the add-on's own matcher claims but that gives no version (see Container images). - IngressClass. An
IngressClasswhosespec.controllerisk8s.io/ingress-nginxis Ingress NGINX, without a version, when nothing else found Ingress NGINX, its RKE2 or AKS builds, or Traefik (whose Kubernetes Ingress NGINX provider serves that class). It is enough for a blocker, because Ingress NGINX is retired as a whole. - Node container runtimes.
node.status.nodeInfo.containerRuntimeVersion(containerd://1.7.27). Only containerd has an entry: other runtimes (cri-o://,docker://) are reported as anaddon-no-data/<runtime>info finding naming their nodes, because their end of life and Kubernetes compatibility are not assessed.
Rendered manifests (scan --files, and /gate, which with ?cluster=
merges the add-ons it finds into the cluster's) are matched the same way,
from the images and labels of Pod, Deployment, DaemonSet, StatefulSet,
ReplicaSet, Job and CronJob pod templates and from IngressClasses. Images
injected at admission time are not in them.
Image repositories that match no entry are listed as unrecognizedImages
(at most 200) in the inventory and the report, in JSON, the table, Markdown
and the dashboard's cluster view (not in SARIF), and never become
findings: an add-on the registry does not know is not judged. An add-on
running one of them may still have been found by its labels or Helm
release.
How it is judged¶
At the installed version, independent of the target unless noted:
- the product is retired as a whole (
support.status: eol, Ingress NGINX), or its installed release line has ended →eol-addonblocker; - the installed version is older than the oldest release line the entry
tracks, and that line has ended (cert-manager 1.5, whose oldest tracked
line is 1.10; Istio 1.5, oldest 1.7) →
eol-addonblocker keyedeol-addon/<id>/below-<oldest line>, citing that line and the product's lifecycle pages. endoflife.date stops at some old line, and anything older than an ended line has ended too; - the end of life is within 90 days →
eol-approachingwarning; - split support, where support ends for everyone on one date and lasts to
a later one only under a condition upgradescope cannot see (RKE2's
Ingress NGINX: SUSE builds for community users ended in March 2026,
Prime LTS subscribers are supported through November 2027) → an
eol-approachingwarning that states the condition between the two dates, and aneol-addonblocker after the later one; - the release line's, or a compatibility row's, Kubernetes range excludes
the target →
chart-incompatblocker; - a node runtime's ended release line, or a runtime older than the oldest tracked line → warning only, naming the nodes (the runtime comes with the node image, not with Kubernetes);
- no lifecycle data for the installed version (a version between two
tracked lines, one newer than the newest tracked line, which means the
registry is behind upstream, or a product without release lines), or no
version readable →
addon-no-datainfo, never a blocker.
The info's detail says what was missing for the evidence the install was
found by, and never a source the detector did not consult. For an image it
says no version was read from it: the inventory cannot tell a tag that
names no version (a digest, :latest) with no usable pod label from a
component image whose release line the registry does not map yet, and the
sentence covers both. An inventory from an agent that predates the
pod-label rule (v0.1.x, v0.2.0-rc.1 and rc.2, which stamp no
collectorSchema) never took a pod's version label for such an image, so
the detail names only the tag or component reason and says the agent did
not read the label, and to upgrade it. A Helm release or GitOps chart
without an app version is worded the same way, with one difference for a
Helm release found by such an agent: a v0.1.x agent reported the chart
version where the app version belongs, which the server sets aside, so the
release may well record an appVersion, and the detail says only that no
app version was read (the release records none, or the agent did not
collect it), never that the release records none.
Each install (one per namespace, two where a Helm release and images on
another release line share one, or one per node for a runtime) is judged
on its own, grouped by release line: with Istio 1.28 in one team's
namespace and 1.29 in another's, the ended 1.28 line is a finding keyed
eol-addon/istio/1.28 that names only the namespaces and teams running
1.28, so a newer install neither hides an older one nor shares its blame. A product retired as a
whole is one finding naming every install. A node runtime's
chart-incompat blocker always lists the nodes that cannot run the target
(at most 10 named, the rest counted), since nodes have no namespace to
name them by.
Evidence is tenant-controlled¶
Every source above except node runtimes and IngressClasses is written by
whoever can write to the namespace: Helm release Secrets and ConfigMaps,
pod images and pod labels. A tenant with secrets: create in its own
namespace can forge a Helm release, and one that creates pods chooses
their images and labels, so findings are only as trustworthy as namespace
write access. A forged release of chart ingress-nginx raises the
Ingress NGINX blocker for that namespace, and with it the cluster's
verdict. Hiding is narrower. A release's appVersion stands only for
pods on its own release line in its namespace (the Helm releases
bullet above), so a forged release that claims a newer version does not
hide an older image of the add-on on another line: that image is judged
at its own version. What it can still hide, in its own namespace, is a
patch-level difference on the same line, a pod whose image has no
version tag, and an add-on whose image no matcher recognizes.
The security model
says what this means for a multi-tenant cluster.
Within one namespace, pods without a Helm release behind them (or off its
release line) are one install at their oldest version: a namespace running
Istio 1.28 and 1.29 images and no release is judged on the 1.28 line, and
the 1.29 line, being newer, is not judged separately there. Data-plane pods
that lag the control plane by a release line in the release's namespace
(an ingress gateway still on 1.28 beside istiod 1.31) are judged on
their own line too: that is the version running.
What is in it¶
34 add-ons today:
Add-on (id) |
Release lines | Kubernetes ranges | Synced from endoflife.date |
|---|---|---|---|
argo-cd |
yes | — | yes |
calico |
yes | minimum per release line (compat rows) | yes |
cert-manager |
yes | compat rows, minimum and maximum | yes |
cilium |
yes | minimum for 1.13 and 1.14 (compat rows) | yes |
containerd (node runtime) |
yes | compat rows | yes |
etcd |
yes | — | yes |
fluent-bit |
yes | — | yes |
flux |
yes | — | yes |
gatekeeper |
yes | — | yes |
istio |
yes | per release line, and compat rows from istio.io (1.13 to 1.31) | yes |
karpenter |
yes | maximum per release line (compat rows) | yes |
keda |
yes | — (a tested-with table, which eol-sync drops) |
yes |
kyverno |
yes | — (a tested-with matrix, which eol-sync drops) |
yes |
longhorn |
yes | compat rows (minimum, 1.5 and later) | yes |
traefik |
yes | — | yes |
crossplane |
yes (from its own release table) | — | — |
ingress-nginx |
retired as a whole (March 2026) | — | — |
kubernetes-dashboard |
retired as a whole (archived 2026-01-21) | — | — |
promtail |
retired as a whole (2026-03-02) | — | — |
grafana-agent |
retired as a whole (2025-11-01) | — | — |
weave-net |
retired as a whole (archived 2024-06-20) | — | — |
external-dns |
— | compat rows | — |
linkerd |
— | compat rows, minimum and maximum (2.10 to 2.20) | — |
aws-load-balancer-controller |
— | compat rows (minimum; 2.0 to 2.3 also a maximum) | — |
aws-efs-csi-driver |
— | compat rows (minimum; 0.1.0 also a maximum) | — |
rke2-ingress-nginx |
community builds ended 2026-03-31; Prime LTS only until 2027-11-30 | compat rows | — |
aks-app-routing-nginx |
product end of life 2026-11-30 | — | — |
coredns |
— | — (no support range published) | — |
cluster-autoscaler |
— | — (a tested-with pairing, not a bound) | — |
aws-ebs-csi-driver |
— | — ("all Kubernetes versions supported", not a bound) | — |
kube-state-metrics |
— | — | — |
metrics-server |
— | compat rows (0.3.x maximum, minimum of 0.6+) | — |
prometheus-operator |
— | compat rows (minimum) | — |
velero |
— | compat row (minimum) | — |
So: 15 entries carry release-line EOL data kept in sync with
endoflife.date, one more (Crossplane, which
endoflife.date does not track) carries release-line end dates read from its
own release table, five are retired as a whole
(Ingress NGINX, Kubernetes Dashboard, Promtail, Grafana Agent and Weave
Net, each a blocker at any version), one more carries an end-of-life date
for the product as a whole (the AKS application routing NGINX build), one
carries an end date for everyone and a later one under a condition (RKE2's
Ingress NGINX build, supported until 2027-11-30 only with a SUSE Rancher
Prime LTS subscription),
15 carry Kubernetes compatibility ranges, and for the 4 entries with
none of these (CoreDNS, kube-state-metrics, Cluster Autoscaler and the AWS
EBS CSI driver), a detected install is reported as addon-no-data (info)
rather than judged. ExternalDNS, Metrics Server, Velero, Prometheus
Operator, Linkerd, the AWS Load Balancer Controller and the AWS EFS CSI
driver have compatibility ranges but no end-of-life data, which is
reported the same way unless the range excludes the target. The
registry is small on purpose: every row needs a source.
Which Kubernetes ranges are encoded¶
A range is encoded only as far as the upstream page states it. Each row
cites the page it was read from, with the date read in the entry's
comments, and the ranges of an entry that endoflife.date syncs live in its
compat rows, which tools/eol-sync does not rewrite (see
CONTRIBUTING).
- A maximum (the line is unsupported above it) is encoded where upstream says so: cert-manager's supported range per release line, Metrics Server 0.3.x (Kubernetes 1.8 to 1.21), Istio's supported Kubernetes versions per release (istio.io, 1.13 to 1.31, kept as compat rows beside the ranges endoflife.date syncs into its cycles, and tested to agree with them), Linkerd's minimum and maximum per release (2.10 to 2.20), the AWS Load Balancer Controller 2.0 to 2.3 (up to 1.21), the AWS EFS CSI driver 0.1.0 (1.11 to 1.13), and Karpenter, whose matrix lists the oldest Karpenter that runs each Kubernetes minor, so a line's maximum is the newest minor it can run. At Kubernetes 1.37 that makes cert-manager 1.21, Karpenter up to 1.14, Metrics Server 0.3.x and Linkerd up to 2.20 blockers, because no release of theirs the registry knows supports 1.37 yet, and Istio 1.30 and older too; Istio 1.31 supports 1.37.
- A minimum is encoded where upstream says older Kubernetes is unsupported: Calico 3.25 to 3.32 ("will not work" below 1.16 or 1.21), Cilium 1.13 and 1.14, Metrics Server 0.6 and later, Velero 1.14 to 1.18, Prometheus Operator, the AWS Load Balancer Controller 2.4 and later, the AWS EFS CSI driver 0.2 to 2.1, and Longhorn 1.5 and later (1.13 needs Kubernetes 1.34). A forward-upgrade scan cannot trip a minimum, since the target is newer than the cluster; a target below one can.
- A tested-with list is not a range. Argo CD, Cilium 1.15 and later,
Calico's tested versions (so Calico has no maximum), the client-go
version kube-state-metrics is built with, KEDA's and Kyverno's
compatibility tables, Ingress NGINX's supported-versions table, Cluster
Autoscaler's per-minor pairing ("tested on GCP") and Longhorn's tested
Kubernetes lists state what upstream runs its tests on, and say nothing,
or say "may work", about Kubernetes versions outside it, so none of them
is a bound: encoding the lowest or highest tested version would call a
working install incompatible. endoflife.date copies KEDA's and Kyverno's
tables into its cycles as ranges; their entries carry the
# eol-sync: ignore-k8s-rangedirective, sotools/eol-synckeeps the end dates and drops the ranges. - No matrix: Flux, Traefik, Gatekeeper and Crossplane publish only a policy relative to upstream Kubernetes's own support window, the AWS EBS CSI driver says it is compatible with every Kubernetes version the project or EKS supports, the AKS application routing add-on follows the cluster's AKS version, and CoreDNS publishes only the version kubeadm installs. Their entries say so in a comment.
An image is matched by the repository path an entry declares. A path of two
or more segments is a suffix on whole path segments: ingress-nginx/controller is
registry.k8s.io/ingress-nginx/controller and
myregistry.example.com/mirror/ingress-nginx/controller, never a bare
controller repository. A one-segment path is that repository exactly, not a
suffix. An entry can opt one distinctive name into matching under any
registry host or prefix by writing it "*/etcd"; the validator refuses that
for generic names such as controller, and only etcd uses it, so etcd is
recognized at registry.k8s.io/etcd, bitnamilegacy/etcd and behind a kubeadm
imageRepository or a Harbor proxy cache (myregistry.corp/k8s/etcd). The
vendor builds of Ingress NGINX have their own
entries (rke2-ingress-nginx, aks-app-routing-nginx) and the Bitnami
rebuild is in ingress-nginx, as is RKE1's build (-rancherN tags of
rancher/nginx-ingress-controller; RKE1 itself has been end of life since
2025-07-31), which RKE2's releases of 2020-2021 up to v1.20.11 and v1.21.2
shipped too. No image is claimed by two entries: a tag-qualified matcher
takes its tags ahead of another entry's path-only matcher of that
repository, and two tag-qualified matchers on one repository are refused.
Flux is found by its v2 controllers (fluxcd/source-controller,
kustomize-controller, helm-controller, notification-controller,
image-reflector-controller, image-automation-controller,
source-watcher), however it was installed, by the flux2 chart, and
as Flux v1 by fluxcd/flux and weaveworks/flux. Kubernetes Dashboard is
found by its v1.x images too (kubernetes-dashboard-amd64 and the -arm,
-arm64, -ppc64le and -s390x variants, under k8s.gcr.io or
gcr.io/google_containers). Those v1 names are one path segment, matched
exactly: a mirror that adds a prefix
(myregistry.corp/k8s/kubernetes-dashboard-amd64) is not recognized, and
the image is listed as unrecognized; add the mirror's path to a
--registry-dir copy of the entry.
As an example, CoreDNS is found at every one of these (coredns.yaml):
| Where | Image path | Tag |
|---|---|---|
Docker Hub, registry.k8s.io, a mirror |
coredns/coredns |
v1.11.3 |
| EKS add-on, EKS Distro (public ECR) | eks/coredns, eks-distro/coredns/coredns |
v1.11.1-eksbuild.4, v1.13.2-eks-1-36-1 |
| AKS | mcr.microsoft.com/oss/kubernetes/coredns, mcr.microsoft.com/oss/v2/kubernetes/coredns |
v1.9.4, v1.9.4-hotfix.20240704, v1.9.4-7 |
| RKE2, k3s | rancher/mirrored-coredns-coredns |
1.9.4 |
Metrics Server and kube-state-metrics are found the same way, and also at
GKE's gcr.io/gke-release/<name> (alias gke.gcr.io/<name>, tag
v0.8.0-gke.13) and at Bitnami's rebuilds (bitnami/ and bitnamilegacy/,
tag 0.8.0-debian-12-r4). Each path was read from the registry that serves
it, and its tag's build suffix never reaches the version. Where a managed
distribution's build follows the provider's own support policy and the
registry has no entry for that policy, the image stays unrecognized rather
than be judged by upstream's release-line dates: AKS's Cilium and Calico
(mcr.microsoft.com/oss/cilium/*, oss/calico/*), GKE's Cilium and Calico,
and Rancher's hardened-* rebuilds are listed under unrecognizedImages.
Rancher's mirrored-* images are verbatim copies and match like the upstream
image.
Add-ons no entry covers yet (the Amazon VPC CNI and the EKS Pod Identity Agent, for example) are not judged; the report lists their images as unrecognized.
Cover your own add-ons¶
--registry-dir <file-or-directory> on scan, agent and serve loads
more registry entries from YAML in the schema of
registry/CONTRIBUTING.md:
one <id>.yaml file, or every *.yaml file of a directory. Each entry
goes through the same validation as an embedded one, citations included,
and a file that fails it stops the command at start, naming the file. An
entry whose id is an embedded entry's replaces it entirely (to correct a
date for your fleet or add a mirror's image path); any other id adds an
add-on, and may not claim an image or chart an embedded entry already claims
(the command stops at start naming both entries; replace the embedded entry
instead). The extra entries are part of the knowledge base version, so a
report says which registry judged it.
serve takes the flag too, and needs it when agents push add-ons the extra
entries cover: it judges stored inventories, and what /gate finds in
posted manifests, against its own registry, so an add-on it has no entry
for is not judged, whatever the agent detected. Give the server and the
agents the same entries. GET /api/v1/registry and the dashboard's
registry view list the embedded registry only.
In the Helm chart, agent.extraRegistry maps <id>.yaml file names to
entries and mounts them from a ConfigMap as --registry-dir. The server
has no such option: pass --registry-dir through server.extraArgs and
mount the entries with server.extraVolumes and
server.extraVolumeMounts.
Citations are enforced¶
registry.Validate fails an entry with any status but unknown, any
release line and any compatibility row that has no citation URL, so an EOL
claim cannot land without a source. Entries with an endoflife_product
slug have their release lines generated by tools/eol-sync from the
endoflife.date API; CI runs eol-sync -check on pull requests that touch
registry/, and the weekly refresh opens a pull request when upstream data
moves. Like the rest of the knowledge base, a registry change reaches users
only in a release.
Add an add-on¶
registry/CONTRIBUTING.md
has the schema, how matchers and versions work, and the checks a pull
request must pass. The data's license is in
registry/DATA-LICENSE.md.