Troubleshooting¶
The verdict is unknown and the gate fails¶
unknown means no blocker was found, but a required check could not run,
so one may have been missed. The report's NOT ASSESSED section (JSON
notAssessed, the CR's status.notAssessed) names the gap and why:
| Gap | Usual cause | What to do |
|---|---|---|
kb-coverage (required) |
The target is newer than the knowledge base's horizon (upgradescope version prints it). |
Target the horizon or lower, upgrade to a release with a newer knowledge base, or accept the gap with --allow-incomplete. |
api-usage (required) |
The scan could not list resources (RBAC, an unavailable API group), or a partial list skipped an API removed by the target. | Fix the access the reason names. |
versions (required) |
/version or nodes could not be read, or the server version did not parse. |
Fix the access; a version string that does not parse is a bug, please report it. |
versions (partial, required) |
A component pod in kube-system whose version upstream would have told was not read: the reason names the first pod and its image, an upstream-named image under a digest or a tag that is not a version (kube-proxy@sha256:…, kube-scheduler:latest), or a kube-apiserver, kube-controller-manager or kube-scheduler pod on a vendor image. |
Pin the image to a version tag (kube-proxy:v1.33.4, …:v1.33.4@sha256:… reads too) so its skew is judged, or accept the gap with --allow-incomplete. A versions (partial) gap without required (a vendor kube-proxy image such as Oracle OKE's) leaves the verdict alone. |
addons (required) |
Pods could not be listed, and there was nothing else to match add-ons from: no Helm release was found and IngressClasses could not be read. | Grant list on pods. |
addons (partial, required) |
Pods could not be listed, but a Helm release or the IngressClasses could be read: add-ons were matched from what was read (the reason says which), so an EOL chart-installed add-on still blocks while one installed any other way is not seen. | Grant list on pods, or accept the gap with --allow-incomplete. |
--allow-incomplete (CLI), or allow-incomplete: true in the GitHub
Action, gates on findings alone; see CI gate.
deprecated-calls: apiserver /metrics forbidden¶
Managed control planes (EKS, GKE, AKS) often deny /metrics regardless of
RBAC. The check is optional: the verdict does not depend on it, and stored
objects are still checked. Nothing to fix;
see Managed clusters.
The same holds when /metrics does not answer: the request is given up
after --request-timeout (default 30s), the reason (get /metrics: …
naming a timeout, in one of the forms below) says so, and the verdict can
still be ready, judged on stored objects without the request signal.
A check not assessed with a timeout or a step deadline¶
A live scan has 5 minutes. Each API request is given up after
--request-timeout (default 30s; 0 turns the per-request limit off), and
each of the five collector steps runs under its own share of the time left,
so a slow or stalled API server degrades only the step it stalled: that
capability is not assessed, and the other steps still run.
When a single request timed out, the reason names a timeout:
Client.Timeout exceeded, request canceled or
context deadline exceeded, alone or together. Which form appears varies
from run to run, because client-go's per-request context and the HTTP
client's own timeout race each other. When the step's share of the time ran
out, the reason ends with
(step deadline: gave up after …, this step's share of the scan's time),
typically after context deadline exceeded. When the step is a required one
(versions, addons, api-usage) the verdict is unknown. Check the API
server's health and the network path to it, or raise --request-timeout
when single requests are slow but complete (a very large list page, a big
/metrics).
helm not assessed: Secrets forbidden¶
The agent was installed with rbac.helmSecrets=false, or your own identity
cannot list Secrets. Helm chart findings are missing from the report;
add-ons are still found from images. The trade-off is in
Security model and RBAC.
helm partial: Argo CD or Flux present, no Helm releases¶
The reason reads "no Helm releases read, but Argo CD (or Flux) is present".
The cluster has no Helm release Secrets or ConfigMaps but shows the tool, so
chart kubeVersion and stored-manifest checks were not assessed for the
charts it deploys; they are not clean. Argo CD's helm template leaves no
release to read, so with rbac.gitops.argocd set every chart read from an
Application raises the same gap ("N Argo CD chart(s) read from Applications
leave no Helm release"), even on a cluster that has other Helm releases.
Add-ons are still found from images, and, with
rbac.gitops.argocd or rbac.gitops.flux set, from the charts the tool's
resources name. A reason that says "chart sources not read" with a
forbidden error means the tool is installed and the agent's role lacks that
grant. See
GitOps with Argo CD and Flux.
Exit code 1¶
An operational error, never a verdict: the cluster could not be read at all
(check --context and --kubeconfig), --files found no Kubernetes
objects (no Kubernetes manifests found under ...), or a flag, config file
or baseline is invalid. The message says which.
A removed-API finding I did not expect¶
Live findings name the field manager that wrote the object through the
deprecated version (Written by: ...). Re-apply the object through the
current version with that tool and the finding clears on the next scan. If
the manager has moved to the new version but an old entry remains (a Helm
upgrade that changed nothing but the apiVersion), see the known limits in
Deprecated-API detection.
To accept a finding for now, with a reason and an expiry, use an
ignore rule.
The agent¶
ClusterReadiness CRD is not installed. The chart installs the CRD fromcrds/on first install only; it is missing when the chart was installed with--skip-crds, the CRD was deleted, or the chart was upgraded from v0.1.x or a release candidate across the API group move. The error carries the fix:kubectl apply -fof the CRD manifest at the agent's release (https://raw.githubusercontent.com/abd-ulbasit/upgradescope/v<version>/deploy/chart/crds/clusterreadinesses.upgradescope.basit.engineer.yaml, orcrds/fromhelm pull --untar). Withagent.manageCRD=truethe agent stops at startup with this error; withfalse, every tick fails with it.could not bring the ClusterReadiness CRD up to date(startup, WARN) andcrdErroron tick lines. The CRD exists but the agent could not check or upgrade its schema: a transient apiserver fault, an apiserver that did not answer within the startup check's 30 seconds, or a role withoutpatchon the CRD. Every tick tries again until it succeeds; meanwhilestatus.notAssessedleads with a note, because an older schema makes the apiserver drop the status fields it lacks (conditions, for Argo CD andkubectl wait). A role that may not patch the CRD should run withagent.manageCRD=falseand the CRD applied by hand.- The agent exits at once with
invalid --server-url,invalid --server-token,invalid --cluster-nameorinvalid --force-sync-every. These settings could never work, so they are refused before the agent touches the cluster: the server URL must be anhttp://orhttps://URL with a host (fleet.example.comalone is not); the token must have no whitespace or control character inside it (surrounding whitespace, such as the newline of a Secret made with--from-file, is trimmed from every source);--cluster-namemay not be set to""(leave it out to use the cluster UID); and--force-sync-everymust be positive (0 no longer means the 1h default). A--force-sync-everyat or below--intervalis not refused: it means an unchanged inventory is pushed on every tick, and the agent logs a warning with the period in effect, nine tenths of the interval, the shortest gap the tick jitter leaves between two ticks. - The agent exits at once with
invalid --pod-pass-everyorinvalid --pod-pass-max-age.--pod-pass-everyis a number of ticks, at least 1 (1 reads the pods outsidekube-systemon every tick, as before #228), and--pod-pass-max-agea positive duration; 0 does not mean the default (3 and 1h). The chart'sagent.podPassEveryandagent.podPassMaxAgeare refused athelm installfor the same values. - The agent logs
pod-pass-max-age is too short for pod-pass-every, and some or all ticks list every pod. A--pod-pass-max-agethat is not above 1.1 times--intervaltimes (--pod-pass-everyminus one), themustExceedfield of the warning, is not refused, but it cannot work as set. Ticks are spaced by the interval with a jitter of 10% either way (0.9 to 1.1 times the interval), and the sleep starts after the tick ends, so the spacing also holds the tick's run time. A pass is reused only while it is younger than the maximum age, and the last reuse after a pass (the one--pod-pass-everyminus one ticks later) is about that many spacings old, up to 1.1 times the interval plus the run time each. At or below the interval the pass is too old by the next tick, so only a tick that the jitter brings early reuses it; between the interval and the floor the first reuses work and the last ones depend on the draw. The request savings of the pod pass setting are then partly or wholly not there. Raise--pod-pass-max-ageabove themustExceedvalue, and leave room on top of it for the ticks' run time, which the floor cannot know and which is a few seconds each at 2,001 nodes. Exceeding the floor is needed and, because of that run time, not quite enough: at--interval 10m --pod-pass-every 3the floor is 22 minutes, and 21 minutes fails on the second reuse whenever the two spacings and their run time come to 21 minutes or more; 25 minutes works unless the ticks take more than a minute and a half each (two spacings of up to 11 minutes and two run times must stay under 25). At a 30-minute interval the default hour is under the floor of 66 minutes, so the last of the two reuses is lost on the ticks the jitter spaces widely. Instead of raising the maximum age, lower--pod-pass-every(agent.podPassEvery) until its floor,--intervaltimes 1.1 times (--pod-pass-everyminus one), fits under the maximum age: the warning'spodPassEveryThatFitsfield is the largest value whose floor alone is under it. That field leaves out the ticks' run time, which comes on top, so on slow ticks you may need one less (the warning text says so too). At the 30-minute interval and the default hour that is 2 (floor 33 minutes). The price is a full pass every other tick instead of every third (a cycle of 2 averages 25.5 requests at the 2,001-node fill, computed, against 23.7 for 3), and an add-on is then behind for at most one tick (up to about 33 minutes) instead of two. Or set--pod-pass-every=1(agent.podPassEvery=1) if reading every pod on every tick is what you want, which also ends the warning. - An add-on I just installed or upgraded is missing, or still shows its
old version. The agent lists the pods outside
kube-systemonly every--pod-pass-everyticks (3 by default, so about every 30 minutes at the default interval), or sooner when the last full pass is--pod-pass-max-ageold (1h), and detects add-ons from that pass's images and labels in between. An add-on changed right after a pass is reported as it was for at most--pod-pass-everyminus one ticks, and a pass that is--pod-pass-max-ageold or more is not reused (the age is measured at the tick that reuses it, and that tick's report stays up until the next one).status.addOnEvidenceAgeSeconds(alsoaddOnEvidenceAgeSecondsin the report and theupgradescope_addon_evidence_age_secondsgauge) says how old the evidence of the last tick was: absent or 0 means every pod was read. Wait for the next full pass, or setagent.podPassEvery=1to read the pods on every tick, at the request count Scale and cost gives for it. Thekube-systempods, Helm releases, Argo CD and Flux charts and IngressClasses are read on every tick, and a change in the Helm releases or chart references that name an add-on (a new release or revision, a new chart reference) makes the next tick list every pod. So an add-on inkube-system, or installed or upgraded through Helm or a GitOps chart reference, is not behind; one installed another way, or whose image was changed in place (kubectl set image), is, for the ticks above.scanreads every pod. - The pod never becomes Ready. Readiness waits for a successful tick.
kubectl logsshows one line per tick, withtick failedand the error. A tick fails when no target can be evaluated (nospec.targetsand an unreadable server version), or the status cannot be written. health listener: address already in usewhen running the agent outside a pod: something holds port 8081. Pass--health-addr 127.0.0.1:<port>, or--health-addr ""to turn it off.LASTEVALUATEDis old (more than about two intervals): the agent is not ticking. Check the pod; theUpgradescopeAgentNotTickingalert fires on this (Prometheus and Grafana).- The ClusterReadiness has an
upgradescope.basit.engineer/status-errorannotation. The agent could not write the status, at the time and for the reason the annotation gives, so the verdict shown is the last one it could write: stale. The usual cause is a role withoutupdateonclusterreadinesses/status(a hand-written role,rbac.create=false); it is also set when the agent could not read the object's spec or set itsspec.targets, since a status for targets it did not read is not written. The next successful write removes the annotation. If the role also lackspatchon the object itself, there is no annotation, and the signals are/readyz, the agent's logs (tick failed) and the alert above.
The server¶
refusing to serve the read API and /api/v1/gate without a read token.servebound a non-loopback address with no read credential. Set--read-token, mint a read token (upgradescope tokens create --read --teams '*'), trust an authenticating proxy's team header, listen on loopback, or pass--allow-anonymous-readwhen something in front of the server authenticates (Read access). To keep the API closed even after the database is lost,--require-read-credentialstarts without a credential and answers401until one is minted.401from an open server, for a token that "worked before". An open read API now refuses a bearer it does not know instead of reading the whole fleet for it. Send noAuthorizationheader, or a token the server holds (tokens list --read).421fromservefor a name that worked. An open read API answers an anonymous request only forlocalhost, a loopback address, the address it arrived on and the--allowed-hostnames (the Host check).- A scoped read token gets 404 for a cluster that exists, or 403 on
/metrics. The cluster has no namespace of the token's teams in its latest evaluated snapshot, and/metricstakes a fleet-wide credential (what a team-scoped read sees). - A push gets 409:
cluster name ... is registered to clusterId .... Another cluster already uses this--cluster-name, or the cluster was rebuilt. Give the agent a distinct name, or delete the old record withupgradescope clusters delete <name> --server-url ...(admin token); the message spells out both. - A push gets 401 or 403. 401: the token is missing or unknown. 403: a per-cluster token pushing as another cluster name.
- A push or
/gategets 413... is too large to evaluate .... The body is within its byte cap but holds more JSON values or YAML nodes than one request may decode (YAML aliases count at what they expand to). Split the manifest stream, or a large List, into several requests; for a push, check what the agent collects (Memory and request limits). - A push or
/gategets 408 or 503. 408: the body did not arrive within the server's read timeout (60s); 503 withRetry-After: other requests hold the shared body budget or the decode slot, or, for/gate, responses still waiting for their clients leave no room for this answer. The agent retries both; a CI caller should retry too. - A cluster's page, report, findings, teams, history or export, or the
fleet teams rollup, gets 503. Reads that load a stored snapshot run
one at a time, and this one waited more than 30s for its turn (other
reads, usually what-if reports of a large cluster, held it), or the
responses still waiting for slow clients leave no room for this one.
Retry after
Retry-Afterseconds (Memory and request limits). - The cluster list, the fleet matrix or a
/metricsscrape gets 503. Reads of the whole fleet run two at a time, and this one waited more than 30s for its turn (5s for a scrape), or the responses still waiting for slow clients leave no room for this one. The dashboard and Prometheus try again at their next poll or scrape; if it persists, look for clients that ask for large reports or fleet reads and do not read them (the server'supgradescope_http_requests_totalby route and code shows the 503s). /gategets 422a UTF-16 byte order mark. Re-encode the manifests as UTF-8 (iconv -f UTF-16 -t UTF-8)./gategets 413the answer to this stream could take up to. The answer would list more objects, with longer names, namespaces and?path=, than--max-gate-bytesallows (with?cluster=, the cluster's own findings count too). Split the stream into several requests, or raise--max-gate-bytes, which raises the body cap too./fleetgets 422targets lists more than 16 distinct minors. Ask for at most 16 targets at a time.- The agent warns
pushing snapshots to ... over plain http. Its--server-urlishttp://to a host that is not loopback. Serve HTTPS (--tls-cert-file, the chart'sserver.tls, or an Ingress). - A cluster shows as stale. Its agent has not pushed within
serve --stale-after(2h). Check the agent's logs forpush=failed. - The dashboard says
dashboard not built. The binary was built with-tags nodashboard, or from a tree without the committed dashboard. The API still works. Release binaries, images andgo installbuilds include the dashboard. - Ingest token changes on every sync under Argo CD. Renderers without
cluster access cannot look up the chart's generated token. Set
server.ingestTokenorserver.existingSecret(GitOps).
Dashboard blank or assets 404 behind a proxy¶
The page loads empty, or its script and stylesheet answer 404, when the
dashboard is served through a reverse proxy or Ingress at a path prefix such
as https://ops.example.com/upgradescope/. Check, in order
(The dashboard):
- The URL has no trailing slash. Open
/upgradescope/, not/upgradescope: the page's asset and API URLs are relative, so without the slash the browser asks the parent path for them. - The proxy does not strip the prefix.
servesees/and/assets/..., never/upgradescope/assets/.... In nginx the trailing slash onproxy_pass http://server:8080/;strips it; with the ingress-nginx annotations userewrite-target: /$2with the path/upgradescope(/|$)(.*). - A pasted nested link lands at the wrong place. The server redirects a
nested path such as
/cluster/3with a relativeLocation(../#/cluster/3) that the browser resolves under the prefix. Make sure the proxy passesLocationthrough unchanged and does not turn it into an absolute URL. - The page answers
421. The Host the proxy forwards is not an--allowed-host(The Host check).
Still stuck¶
Open an issue with upgradescope version, the command you ran, and the
NOT ASSESSED section of the report:
new issue.