🤖 Generated by the Agentic Engineer
Evidence: 17 of 20 Repository managed resources sat Synced: False for weeks with no signal
(parent #2820). Today kubectl get managed -A spans 109 resources across 7 API groups, of which
7 are at reason=ReconcileError and 9 unifi resources are at reason=ReconcilePaused by design.
None of that state is queryable as a metric: the cluster has no ServiceMonitor/PodMonitor CRDs
(prometheus-operator went with the kube-prometheus-stack), and Coroot's bundled cluster-agent
exposes only image under clusterAgent.kubeStateMetrics — no CustomResourceState config — so
nothing currently emits Crossplane condition state.
Audience/problem: nothing can observe, chart, or alert on a managed resource that stops
reconciling, because the underlying signal does not exist in the metrics store. This blocks every
option weighed on the parent, not just one of them.
Expected behaviour: each Crossplane managed resource's Synced condition is exposed as a
Prometheus metric in Coroot's bundled Prometheus, carrying at minimum the resource's
kind/name/namespace and its condition reason — the reason label is what makes the deliberate
ReconcilePaused state separable from a genuine ReconcileError, and the parent's measurements show
a filter without it selects exactly the wrong set.
Proposed: upstream kube-state-metrics with a CustomResourceState ConfigMap, scraped by the
cluster-agent via the pod annotations it supports (coroot.com/scrape-metrics: "true" +
coroot.com/metrics-port; nothing in k8s/ uses these yet). A pinned upstream image keeps this
declarative and Renovate-trackable and avoids standing up an image build — platform's Go module is
yaml.v3 only, so a bespoke exporter would be a project rather than an increment. Bind its
ServiceAccount to the existing aggregated crossplane-view ClusterRole, verified on the parent to
grant no core secrets; authoring a new ClusterRole is unnecessary and would trip the EKS
authorization-surface fingerprint gate.
Known cost to weigh during implementation: CustomResourceState is configured per-GVK, so covering
7 API groups means 7 entries, and a newly-installed provider needs one added. Note that trade-off in
the PR against the alternative of a small first-party exporter.
Acceptance criteria:
Two criteria above are decided rather than outstanding, and are ticked for that reason. The
record and the artifact disagreed until this amendment, and on the ClusterRole item that
disagreement pointed at the less safe option — see the 2026-08-06 comment below for the full
measurement.
Rough size: M. Delivers the signal only — alerting and Slack routing are #2987.
Part of #2820
Evidence: 17 of 20
Repositorymanaged resources satSynced: Falsefor weeks with no signal(parent #2820). Today
kubectl get managed -Aspans 109 resources across 7 API groups, of which7 are at
reason=ReconcileErrorand 9 unifi resources are atreason=ReconcilePausedby design.None of that state is queryable as a metric: the cluster has no
ServiceMonitor/PodMonitorCRDs(prometheus-operator went with the kube-prometheus-stack), and Coroot's bundled cluster-agent
exposes only
imageunderclusterAgent.kubeStateMetrics— noCustomResourceStateconfig — sonothing currently emits Crossplane condition state.
Audience/problem: nothing can observe, chart, or alert on a managed resource that stops
reconciling, because the underlying signal does not exist in the metrics store. This blocks every
option weighed on the parent, not just one of them.
Expected behaviour: each Crossplane managed resource's
Syncedcondition is exposed as aPrometheus metric in Coroot's bundled Prometheus, carrying at minimum the resource's
kind/name/namespace and its condition
reason— thereasonlabel is what makes the deliberateReconcilePausedstate separable from a genuineReconcileError, and the parent's measurements showa filter without it selects exactly the wrong set.
Proposed: upstream kube-state-metrics with a
CustomResourceStateConfigMap, scraped by thecluster-agent via the pod annotations it supports (
coroot.com/scrape-metrics: "true"+coroot.com/metrics-port; nothing ink8s/uses these yet). A pinned upstream image keeps thisdeclarative and Renovate-trackable and avoids standing up an image build —
platform's Go module isyaml.v3only, so a bespoke exporter would be a project rather than an increment. Bind itsServiceAccount to the existing aggregated
crossplane-viewClusterRole, verified on the parent togrant no core
secrets; authoring a new ClusterRole is unnecessary and would trip the EKSauthorization-surface fingerprint gate.
Known cost to weigh during implementation:
CustomResourceStateis configured per-GVK, so covering7 API groups means 7 entries, and a newly-installed provider needs one added. Note that trade-off in
the PR against the alternative of a small first-party exporter.
Acceptance criteria:
Syncedcondition state for Crossplane managed resources is present in Coroot's bundledPrometheus and queryable in the UI.
reason, and a query excludingReconcilePausedreturnsthe genuinely-failing set and not the paused one.
up{job="crossplane-sync-exporter"}is delivered too, so "no condition series" is notambiguous between a healthy fleet and a dead exporter.
Repositoryresources atreason=ReconcileErrorappear.purpose-built
crossplane-sync-exporterClusterRole grantingget/list/watchon exactlyrepo.github.m.upbound.io/repositories. The alternative this criterion originally asked for —binding the aggregated
crossplane-view— carries 49 rules, so the deviation is anarrowing, not a widening. The
🔐 Validate EKS Authorizationgate that the criterionpredicted would trip passed on the merged head. Do not "fix" this by rebinding to
crossplane-view: that would widen read access from one kind to the whole aggregate.secrets— held; the role above grants none.Covers managed resources generically across API groups, not onlyRepository.Scope amended 2026-08-10 → Extend the Crossplane sync-state exporter past Repository — 90 of 110 managed resources are unobserved #3052.
CustomResourceStateMetricstakes an exact GVK with nowildcard, so generic coverage is a separate piece of work: 110 managed resources across 7
groups and 14 kinds exist, of which this exporter observes 20.
Rough size: M. Delivers the signal only — alerting and Slack routing are #2987.
Part of #2820