Skip to content

feat(coroot): export Crossplane managed-resource conditions as metrics - #2994

Merged
devantler merged 5 commits into
mainfrom
claude/crossplane-sync-exporter-2820
Aug 6, 2026
Merged

feat(coroot): export Crossplane managed-resource conditions as metrics#2994
devantler merged 5 commits into
mainfrom
claude/crossplane-sync-exporter-2820

Conversation

@devantler

@devantler devantler commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

🤖 Generated by the Agentic Engineer

Why

A Crossplane managed resource can sit with Ready=True while Synced=False. In that
state nothing we declare is actually reaching the provider, but every health check
keyed on Ready reads the fleet as healthy — so the control is voided silently while
the config file still reads as the truth. 3 of 20 repository resources are in exactly
that state on the cluster right now.
Nothing today would surface it.

What

Adds the missing metric, which is the prerequisite for alerting on it — the rule cannot
be written before the series exists. Off-the-shelf images, no bespoke code: it follows
the same shape as the existing opencost usage-scraper.

Both Synced and Ready are exported, so the misleading pair can be caught in one
query rather than inferred.

Ships default-off (the component is unreferenced), so turning it on is a separate,
independently reversible change — as is the alert itself and extending coverage beyond
repository resources to the other managed-resource kinds.

Fixes #2986
Part of #2820

A Crossplane managed resource can report Ready=True while Synced=False. Every
health check keyed on Ready then reads the fleet as healthy while no declared
setting is reaching the provider. Measured on the live cluster, 3 of 20
Repository resources are in exactly that state right now.

Nothing in the cluster observes the Synced condition, and Coroot's bundled
Prometheus exposes no scrape configuration, so the series has to be pushed in
via remote-write. This mirrors the opencost usage-scraper: kube-state-metrics
renders the conditions as custom resource state, a co-located Prometheus agent
scrapes it over loopback and remote-writes to coroot-prometheus.

Ready is exported alongside Synced so the misleading pair is expressible as a
single query. Ships default-off; activation and the alerting rule are separate,
independently reversible changes.
Every workload in this repo that automounts the token carries a CKV_K8S_38
justification; this one did not.
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

⚠️MegaLinter analysis: Success with warnings

⚠️ BASH / bash-exec - 5 errors
Results of bash-exec linter (version 5.3.9)
See documentation on https://megalinter.io/9.6.0/descriptors/bash_bash_exec/
-----------------------------------------------

✅ [SUCCESS] scripts/dr-rebuild-supersession-guard.sh
❌ [ERROR] scripts/ghcr-auth-lib.sh
    Error: File:[scripts/ghcr-auth-lib.sh] is not executable

✅ [SUCCESS] scripts/guard-cilium-homogeneous-device-rollout.sh
✅ [SUCCESS] scripts/megalinter-scan-counts.sh
✅ [SUCCESS] scripts/normalize-sarif-paths.sh
❌ [ERROR] scripts/refresh-flux-ghcr-auth-safety.sh
    Error: File:[scripts/refresh-flux-ghcr-auth-safety.sh] is not executable

✅ [SUCCESS] scripts/refresh-flux-ghcr-auth.sh
✅ [SUCCESS] scripts/report-cilium-rollout-gate-suppression.sh
✅ [SUCCESS] scripts/run-ksail-prod-with-pull-auth.sh
✅ [SUCCESS] scripts/summarize-sarif-findings.sh
✅ [SUCCESS] scripts/tests/test-cilium-bandwidth-manager-component.sh
✅ [SUCCESS] scripts/tests/test-cilium-homogeneous-devices-activation.sh
✅ [SUCCESS] scripts/tests/test-cilium-homogeneous-devices-autoscaler-gate.sh
✅ [SUCCESS] scripts/tests/test-cilium-homogeneous-devices-flux-wait.sh
✅ [SUCCESS] scripts/tests/test-cilium-mutual-auth-policy-regressions.sh
✅ [SUCCESS] scripts/tests/test-cilium-mutual-auth-policy.sh
✅ [SUCCESS] scripts/tests/test-cilium-rollout-gate-suppression-signal.sh
✅ [SUCCESS] scripts/tests/test-crossplane-sync-exporter.sh
✅ [SUCCESS] scripts/tests/test-dr-rebuild-supersession-guard.sh
✅ [SUCCESS] scripts/tests/test-kyverno-admission-vpa.sh
❌ [ERROR] scripts/tests/test-normalize-sarif-paths.sh
    Error: File:[scripts/tests/test-normalize-sarif-paths.sh] is not executable

✅ [SUCCESS] scripts/tests/test-openbao-oidc-role.sh
❌ [ERROR] scripts/tests/test-opencost-usage-scraper.sh
    Error: File:[scripts/tests/test-opencost-usage-scraper.sh] is not executable

❌ [ERROR] scripts/tests/test-refresh-flux-ghcr-auth-safety.sh
    Error: File:[scripts/tests/test-refresh-flux-ghcr-auth-safety.sh] is not executable

✅ [SUCCESS] scripts/tests/test-restrict-tenant-secret-stores.sh
✅ [SUCCESS] scripts/tests/test-summarize-sarif-findings.sh
✅ [SUCCESS] scripts/tests/test-verify-published-evidence.sh
✅ [SUCCESS] scripts/update-vendored-operators.sh
✅ [SUCCESS] scripts/validate-alert-coverage.sh
✅ [SUCCESS] scripts/verify-published-evidence.sh
✅ [SUCCESS] scripts/wait-for-platform-flux-revision.sh
⚠️ REPOSITORY / checkov - 19 errors
to review the file.
Check: CKV_K8S_8: "Liveness Probe Should be Configured"
	FAILED for resource: PodTemplate.overprovisioning.overprovisioning
	File: /k8s/providers/hetzner/infrastructure/overprovisioning/pod-template.yaml:29-86
	Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/kubernetes-policies/kubernetes-policy-index/bc-k8s-7

		Code lines for this resource are too many. Please use IDE of your choice to review the file.
Check: CKV_K8S_49: "Minimize wildcard use in Roles and ClusterRoles"
	FAILED for resource: Role.github-config.github-config-managed-resources
	File: /k8s/bases/apps/github-config/role.yaml:20-55
	Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/kubernetes-policies/kubernetes-policy-index/ensure-minimized-wildcard-use-in-roles-and-clusterroles

		20 | apiVersion: rbac.authorization.k8s.io/v1
		21 | kind: Role
		22 | metadata:
		23 |   name: github-config-managed-resources
		24 |   namespace: github-config
		25 |   labels:
		26 |     app.kubernetes.io/managed-by: ksail
		27 | rules:
		28 |   - apiGroups:
		29 |       - repo.github.m.upbound.io
		30 |       - team.github.m.upbound.io
		31 |       - actions.github.m.upbound.io
		32 |       - enterprise.github.m.upbound.io
		33 |       - github.m.upbound.io
		34 |     resources:
		35 |       - "*"
		36 |     verbs:
		37 |       - get
		38 |       - list
		39 |       - watch
		40 |       - create
		41 |       - update
		42 |       - patch
		43 |       - delete
		44 |   - apiGroups:
		45 |       - external-secrets.io
		46 |     resources:
		47 |       - externalsecrets
		48 |     verbs:
		49 |       - get
		50 |       - list
		51 |       - watch
		52 |       - create
		53 |       - update
		54 |       - patch
		55 |       - delete

Check: CKV_K8S_35: "Prefer using secrets as files over secrets as environment variables"
	FAILED for resource: CronJob.umami.umami-provision-tenants
	File: /k8s/bases/apps/umami/cron-job.yaml:44-318
	Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/kubernetes-policies/kubernetes-policy-index/bc-k8s-33

		Code lines for this resource are too many. Please use IDE of your choice to review the file.
Check: CKV_K8S_22: "Use read-only filesystem for containers where possible"
	FAILED for resource: CronJob.umami.umami-provision-tenants
	File: /k8s/bases/apps/umami/cron-job.yaml:44-318
	Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/kubernetes-policies/kubernetes-policy-index/bc-k8s-21

		Code lines for this resource are too many. Please use IDE of your choice to review the file.
Check: CKV_K8S_9: "Readiness Probe Should be Configured"
	FAILED for resource: Deployment.observability.crossplane-sync-exporter
	File: /k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/deployment.yaml:1-111
	Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/kubernetes-policies/kubernetes-policy-index/bc-k8s-8

		Code lines for this resource are too many. Please use IDE of your choice to review the file.
Check: CKV_K8S_8: "Liveness Probe Should be Configured"
	FAILED for resource: Deployment.observability.crossplane-sync-exporter
	File: /k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/deployment.yaml:1-111
	Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/kubernetes-policies/kubernetes-policy-index/bc-k8s-7

		Code lines for this resource are too many. Please use IDE of your choice to review the file.
Check: CKV_K8S_40: "Containers should run as a high UID to avoid host conflict"
	FAILED for resource: Job.openbao.vault-config
	File: /k8s/bases/infrastructure/vault-config/job.yaml:36-1200
	Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/kubernetes-policies/kubernetes-policy-index/bc-k8s-37

		Code lines for this resource are too many. Please use IDE of your choice to review the file.
Check: CKV_K8S_40: "Containers should run as a high UID to avoid host conflict"
	FAILED for resource: CronJob.openbao.vault-snapshot
	File: /k8s/bases/infrastructure/vault-backup/cron-job.yaml:23-197
	Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/kubernetes-policies/kubernetes-policy-index/bc-k8s-37

		Code lines for this resource are too many. Please use IDE of your choice to review the file.
Check: CKV_K8S_40: "Containers should run as a high UID to avoid host conflict"
	FAILED for resource: Job.openbao.vault-snapshot-init
	File: /k8s/bases/infrastructure/vault-backup/job.yaml:23-191
	Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/kubernetes-policies/kubernetes-policy-index/bc-k8s-37

		Code lines for this resource are too many. Please use IDE of your choice to review the file.
Check: CKV_K8S_9: "Readiness Probe Should be Configured"
	FAILED for resource: Deployment.opencost.opencost-usage-scraper
	File: /k8s/bases/infrastructure/opencost/components/usage-scraper/deployment.yaml:1-78
	Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/kubernetes-policies/kubernetes-policy-index/bc-k8s-8

		Code lines for this resource are too many. Please use IDE of your choice to review the file.
Check: CKV_K8S_8: "Liveness Probe Should be Configured"
	FAILED for resource: Deployment.opencost.opencost-usage-scraper
	File: /k8s/bases/infrastructure/opencost/components/usage-scraper/deployment.yaml:1-78
	Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/kubernetes-policies/kubernetes-policy-index/bc-k8s-7

		Code lines for this resource are too many. Please use IDE of your choice to review the file.
Check: CKV_K8S_38: "Ensure that Service Account Tokens are only mounted where necessary"
	FAILED for resource: CronJob.observability.cnpg-degraded-alert
	File: /k8s/bases/infrastructure/controllers/coroot/cron-job-cnpg-degraded-alert.yaml:56-202
	Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/kubernetes-policies/kubernetes-policy-index/bc-k8s-35

		Code lines for this resource are too many. Please use IDE of your choice to review the file.
Check: CKV_K8S_35: "Prefer using secrets as files over secrets as environment variables"
	FAILED for resource: CronJob.observability.cnpg-degraded-alert
	File: /k8s/bases/infrastructure/controllers/coroot/cron-job-cnpg-degraded-alert.yaml:56-202
	Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/kubernetes-policies/kubernetes-policy-index/bc-k8s-33

		Code lines for this resource are too many. Please use IDE of your choice to review the file.
github_actions scan results:

Passed checks: 100, Failed checks: 0, Skipped checks: 0

(Truncated to last 6666 characters out of 9747)
⚠️ SPELL / cspell - 3041 errors
"daemonset",
        "datapath",
        "datreeio",
        "dbname",
        "dedupe",
        "deletecollection",
        "deploymentruntimeconfigs",
        "descheduler",
        "descheduling",
        "desynchronising",
        "devantler",
        "diffable",
        "dispositioned",
        "distroless",
        "dockerconfigjson",
        "docstrings",
        "dorny",
        "dpkg",
        "drainable",
        "emptydir",
        "endgroup",
        "entrys",
        "envsubst",
        "esac",
        "etcdctl",
        "evictable",
        "extensionless",
        "externalsecret",
        "externalsecrets",
        "fanout",
        "featureflagsource",
        "fleetdm",
        "fromdateiso",
        "gatewayapi",
        "generatable",
        "generatingpolicies",
        "genkey",
        "gethomepage",
        "gitops",
        "gocritic's",
        "golangci",
        "gosec",
        "grjtvs",
        "growfs",
        "growpart",
        "healthchecks",
        "healthz",
        "helmrelease",
        "helmreleases",
        "helmv",
        "homelab",
        "hostnames",
        "httproute",
        "idempotently",
        "imagetools",
        "imagevalidatingpolicy",
        "imranismail",
        "injective",
        "inspectable",
        "iscsi",
        "italicise",
        "ivpol",
        "keylessly",
        "keypair",
        "kprobes",
        "kptr",
        "krew",
        "ksail",
        "ksail's",
        "ksailcd",
        "kubeconfig",
        "kubeconform",
        "kubeconform's",
        "kubelet",
        "kubelet's",
        "kubelets",
        "kubelogin",
        "kubescape",
        "kubespan",
        "kubevirt",
        "kubevuln",
        "kustomization",
        "kustomizations",
        "kyverno",
        "letsencrypt",
        "libc",
        "libgnutls",
        "lintable",
        "livez",
        "loadtester",
        "locationless",
        "luks",
        "lycheeignore",
        "machineconfig",
        "materialise",
        "materialised",
        "materialises",
        "maxage",
        "maxbackup",
        "maxsize",
        "maxx",
        "microtime",
        "misconfig",
        "misconfigs",
        "misordered",
        "mktemp",
        "mlock",
        "mutatingpolicies",
        "mutatingwebhookconfigurations",
        "myapp",
        "najsk",
        "neighbour",
        "nenv",
        "netlink",
        "netpol",
        "netpols",
        "neutralises",
        "neutralising",
        "nextjs",
        "nftables",
        "nilnil",
        "nobuckets",
        "nodepod",
        "nodeport",
        "nolint",
        "nonroot",
        "normalisation",
        "normalised",
        "normalises",
        "normalising",
        "nosec",
        "nullglob",
        "ocirepository",
        "openbao",
        "opencost",
        "openfeature",
        "overprovisioning",
        "parallelised",
        "pasteable",
        "permissioning",
        "pipefail",
        "policyignore",
        "policyreports",
        "portforward",
        "preemptible",
        "preservingly",
        "prioritisable",
        "prioritisation",
        "prioritised",
        "privesc",
        "providerconfigs",
        "pseudonymization",
        "pseudonymized",
        "pseudonymizes",
        "publishapp",
        "publishprovider",
        "pushsecret",
        "pushsecrets",
        "pycache",
        "randomises",
        "rdqwpktr",
        "readyz",
        "reassertions",
        "recognisably",
        "recognise",
        "recognised",
        "recolour",
        "reconverges",
        "refreshfluxghcrauth",
        "regenerable",
        "releaserc",
        "rematerialise",
        "rematerialised",
        "replicaset",
        "repoint",
        "repointed",
        "repoints",
        "repositoryrulesets",
        "resizer",
        "restrictor",
        "retabbed",
        "retarget",
        "rmem",
        "rolebindings",
        "rollouts",
        "rshared",
        "sanitised",
        "sanitiser",
        "sanitising",
        "sarif",
        "scheckov",
        "schedulability",
        "schedulable",
        "scopeable",
        "scopeless",
        "seccomp",
        "secretbox",
        "secretstore",
        "seedable",
        "serialised",
        "serverside",
        "serviceaccount",
        "serviceaccounts",
        "sgdisk",
        "shellcheck",
        "shfmt",
        "shopt",
        "siderolabs",
        "siderolink",
        "sigstore",
        "skmde",
        "slurpfile",
        "sngle",
        "specnull",
        "spiffe",
        "srole",
        "startswith",
        "statefulset",
        "statemanager",
        "stdlib",
        "storageclass",
        "subtest",
        "summarised",
        "surfaceless",
        "syft",
        "syscall",
        "sysctls",
        "talosconfig",
        "talosctl",
        "tanzu",
        "templatesyncignore",
        "thresholded",
        "tlsv",
        "tmpl",
        "tostring",
        "tracepoints",
        "trixie",
        "trueish",
        "trustd",
        "ture",
        "uids",
        "umami",
        "umami's",
        "unablated",
        "unclickable",
        "uncompilable",
        "uncordon",
        "uncordoned",
        "undecoded",
        "undercounts",
        "undispositioned",
        "unevidenced",
        "unexcepted",
        "unfiled",
        "ungated",
        "ungenerated",
        "unifi",
        "uninvoked",
        "unmarshalling",
        "unmarshals",
        "unmodelled",
        "unparseable",
        "unprovisioned",
        "unrecognisable",
        "unrecognised",
        "unrepresentable",
        "unreviewed",
        "unroutable",
        "unrun",
        "unshippable",
        "unskipped",
        "untrackable",
        "unwired",
        "upbound",
        "updatekeys",
        "upjet",
        "upstreaming",
        "urlencode",
        "userinfo",
        "userns",
        "ushfn",
        "validatable",
        "validatealertcoverage",
        "validatingwebhookconfigurations",
        "vcunav",
        "velero",
        "virt",
        "volumesnapshot",
        "vpas",
        "vulnerabilitymanifests",
        "vulnerabilitymanifestsummary",
        "vulns",
        "vxlan",
        "wffc",
        "wgpolicyk",
        "wildcarded",
        "wlid",
        "wmem",
        "workloadconfigurationscans",
        "workloadconfigurationscansummary",
        "worktrees",
        "yannh",
        "yubikey",
        "yzwvjjmcyfnl",
        "zizmor"
    ]
}


You can also copy-paste megalinter-reports/.cspell.json at the root of your repository

(Truncated to last 6666 characters out of 539608)
⚠️ COPYPASTE / jscpd - 182 errors
uth/rollout_safety_test.go [384:83 - 393:5]
Clone found (go)
 - scripts/tests/refresh-flux-ghcr-auth/rollout_safety_test.go [422:68 - 430:5] (9 lines, 51 tokens)
   scripts/tests/refresh-flux-ghcr-auth/rollout_safety_test.go [435:83 - 443:5]
Clone found (go)
 - scripts/tests/refresh-flux-ghcr-auth/rollout_safety_test.go [447:52 - 464:5] (18 lines, 123 tokens)
   scripts/tests/refresh-flux-ghcr-auth/rollout_safety_test.go [468:57 - 485:5]
Clone found (go)
 - scripts/tests/refresh-flux-ghcr-auth/rollout_safety_test.go [492:77 - 508:11] (17 lines, 118 tokens)
   scripts/tests/refresh-flux-ghcr-auth/rollout_safety_test.go [519:110 - 535:11]
Clone found (bash)
 - scripts/tests/test-cilium-bandwidth-manager-component.sh [9:1 - 54:2] (46 lines, 202 tokens)
   scripts/tests/test-cilium-homogeneous-devices-activation.sh [9:1 - 54:2]
Clone found (bash)
 - scripts/tests/test-cilium-bandwidth-manager-component.sh [52:5 - 72:2] (21 lines, 67 tokens)
   scripts/tests/test-cilium-homogeneous-devices-activation.sh [80:23 - 100:2]
Clone found (bash)
 - scripts/tests/test-cilium-bandwidth-manager-component.sh [54:1 - 72:2] (19 lines, 65 tokens)
   scripts/tests/test-opencost-usage-scraper.sh [15:1 - 33:2]
Clone found (bash)
 - scripts/tests/test-cilium-mutual-auth-policy.sh [28:29 - 41:10] (14 lines, 78 tokens)
   scripts/tests/test-cilium-mutual-auth-policy.sh [79:33 - 92:10]
Clone found (bash)
 - scripts/tests/test-cilium-mutual-auth-policy.sh [98:33 - 107:54] (10 lines, 57 tokens)
   scripts/tests/test-cilium-mutual-auth-policy.sh [118:32 - 127:54]
Clone found (bash)
 - scripts/tests/test-crossplane-sync-exporter.sh [56:3 - 118:11] (63 lines, 338 tokens)
   scripts/tests/test-opencost-usage-scraper.sh [22:45 - 84:11]
Clone found (bash)
 - scripts/tests/test-refresh-flux-ghcr-auth-safety.sh [177:5 - 185:51] (9 lines, 76 tokens)
   scripts/tests/test-refresh-flux-ghcr-auth-safety.sh [186:5 - 194:51]
Clone found (python)
 - scripts/tests/test_validate_homepage_bookmarks.py [46:57 - 54:54] (9 lines, 58 tokens)
   scripts/tests/test_validate_homepage_bookmarks.py [100:53 - 109:54]
Clone found (go)
 - scripts/validate-dr-signing/main.go [920:21 - 952:5] (33 lines, 145 tokens)
   scripts/validate-merge-group-heal/main.go [77:40 - 109:5]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [244:50 - 249:24] (6 lines, 103 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [756:31 - 761:24]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [276:14 - 282:4] (7 lines, 115 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [603:43 - 609:4]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [302:13 - 311:7] (10 lines, 158 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [828:18 - 837:7]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [311:1 - 316:8] (6 lines, 93 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [838:1 - 843:8]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [388:5 - 393:2] (6 lines, 82 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [574:8 - 579:9]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [389:1 - 399:23] (11 lines, 220 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [666:60 - 677:3]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [393:15 - 404:2] (12 lines, 185 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [494:17 - 505:2]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [440:15 - 451:12] (12 lines, 264 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [649:1 - 662:3]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [503:2 - 509:19] (7 lines, 89 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [688:7 - 694:19]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [503:2 - 509:33] (7 lines, 103 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [1013:58 - 1019:33]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [503:2 - 509:4] (7 lines, 74 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [1126:5 - 1132:4]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [596:30 - 601:8] (6 lines, 50 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [623:44 - 628:8]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [710:22 - 716:2] (7 lines, 135 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [1212:22 - 1218:2]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [951:33 - 959:11] (9 lines, 118 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [963:130 - 971:11]
Clone found (go)
 - scripts/validate-eks-ci-role-policy/main_test.go [1158:47 - 1163:2] (6 lines, 166 tokens)
   scripts/validate-eks-ci-role-policy/main_test.go [1238:46 - 1243:2]
Clone found (python)
 - scripts/validate-naming.py [126:52 - 132:25] (7 lines, 53 tokens)
   scripts/validate-naming.py [171:82 - 177:29]
┌────────┬────────────────┬─────────────┬──────────────┬──────────────┬──────────────────┬───────────────────┐
│ Format │ Files analyzed │ Total lines │ Total tokens │ Clones found │ Duplicated lines │ Duplicated tokens │
├────────┼────────────────┼─────────────┼──────────────┼──────────────┼──────────────────┼───────────────────┤
│ bash   │ 34             │ 11018       │ 42989        │ 20           │ 295 (2.68%)      │ 1797 (4.18%)      │
├────────┼────────────────┼─────────────┼──────────────┼──────────────┼──────────────────┼───────────────────┤
│ go     │ 34             │ 27629       │ 172226       │ 96           │ 784 (2.84%)      │ 7524 (4.37%)      │
├────────┼────────────────┼─────────────┼──────────────┼──────────────┼──────────────────┼───────────────────┤
│ python │ 4              │ 807         │ 5606         │ 2            │ 14 (1.73%)       │ 111 (1.98%)       │
├────────┼────────────────┼─────────────┼──────────────┼──────────────┼──────────────────┼───────────────────┤
│ txt    │ 57             │ 3378        │ 109125       │ 64           │ 2417 (71.55%)    │ 80219 (73.51%)    │
├────────┼────────────────┼─────────────┼──────────────┼──────────────┼──────────────────┼───────────────────┤
│ Total: │ 129            │ 42832       │ 329946       │ 182          │ 3510 (8.19%)     │ 89651 (27.17%)    │
└────────┴────────────────┴─────────────┴──────────────┴──────────────┴──────────────────┴───────────────────┘
Found 182 clones.
HTML report saved to megalinter-reports/copy-paste/jscpd-report.html
ERROR: jscpd found too many duplicates (8.2%) over threshold (0.0%)
time: 1.32s

(Truncated to last 6666 characters out of 38799)
⚠️ MARKDOWN / markdownlint - 63 errors
ould have a language specified [Context: "```"]
AGENTS.md:101:401 error MD013/line-length Line length [Expected: 400; Actual: 1769]
AGENTS.md:105:401 error MD013/line-length Line length [Expected: 400; Actual: 1126]
AGENTS.md:106:401 error MD013/line-length Line length [Expected: 400; Actual: 1447]
AGENTS.md:159:401 error MD013/line-length Line length [Expected: 400; Actual: 649]
AGENTS.md:161:401 error MD013/line-length Line length [Expected: 400; Actual: 971]
AGENTS.md:188:401 error MD013/line-length Line length [Expected: 400; Actual: 970]
AGENTS.md:191:401 error MD013/line-length Line length [Expected: 400; Actual: 453]
AGENTS.md:210:401 error MD013/line-length Line length [Expected: 400; Actual: 1360]
AGENTS.md:279:401 error MD013/line-length Line length [Expected: 400; Actual: 491]
AGENTS.md:280:401 error MD013/line-length Line length [Expected: 400; Actual: 468]
AGENTS.md:286:401 error MD013/line-length Line length [Expected: 400; Actual: 532]
AGENTS.md:288:401 error MD013/line-length Line length [Expected: 400; Actual: 523]
AGENTS.md:291:401 error MD013/line-length Line length [Expected: 400; Actual: 613]
AGENTS.md:292:401 error MD013/line-length Line length [Expected: 400; Actual: 714]
AGENTS.md:296:401 error MD013/line-length Line length [Expected: 400; Actual: 502]
AGENTS.md:300:401 error MD013/line-length Line length [Expected: 400; Actual: 441]
AGENTS.md:305:401 error MD013/line-length Line length [Expected: 400; Actual: 427]
AGENTS.md:408:401 error MD013/line-length Line length [Expected: 400; Actual: 1139]
AGENTS.md:410:401 error MD013/line-length Line length [Expected: 400; Actual: 1240]
AGENTS.md:415:401 error MD013/line-length Line length [Expected: 400; Actual: 430]
AGENTS.md:426:401 error MD013/line-length Line length [Expected: 400; Actual: 1137]
AGENTS.md:437:401 error MD013/line-length Line length [Expected: 400; Actual: 790]
AGENTS.md:442:401 error MD013/line-length Line length [Expected: 400; Actual: 515]
CLAUDE.md:1 error MD041/first-line-heading/first-line-h1 First line in a file should be a top-level heading [Context: "@AGENTS.md"]
docs/dr/alerting.md:226:28 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/dr/crypto-custody.md:22:389 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/dr/crypto-custody.md:23:264 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/dr/crypto-custody.md:27:35 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/dr/crypto-custody.md:27:161 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/dr/crypto-custody.md:27:239 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/dr/crypto-custody.md:114 error MD024/no-duplicate-heading Multiple headings with the same content [Context: "Custody recommendations"]
docs/dr/crypto-custody.md:245 error MD024/no-duplicate-heading Multiple headings with the same content [Context: "Custody recommendations"]
docs/dr/crypto-custody.md:251 error MD024/no-duplicate-heading Multiple headings with the same content [Context: "What to do if it leaks"]
docs/dr/crypto-custody.md:258 error MD024/no-duplicate-heading Multiple headings with the same content [Context: "What to do if it is *lost* (no..."]
docs/dr/restore-drill.md:42 error MD028/no-blanks-blockquote Blank line inside blockquote
docs/dr/runbook.md:23:102 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/dr/runbook.md:23:487 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/dr/runbook.md:34 error MD028/no-blanks-blockquote Blank line inside blockquote
docs/dr/runbook.md:41 error MD028/no-blanks-blockquote Blank line inside blockquote
docs/dr/runbook.md:50 error MD028/no-blanks-blockquote Blank line inside blockquote
docs/dr/runbook.md:489:92 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/dr/spire-server-ha.md:93 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/dr/velero-cnpg.md:11 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/dr/velero-cnpg.md:56:78 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/dr/velero-cnpg.md:56:166 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/dr/velero-cnpg.md:57:78 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/dr/velero-cnpg.md:57:227 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/dr/velero-cnpg.md:58:78 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/dr/velero-cnpg.md:58:166 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
docs/github-management.md:38:401 error MD013/line-length Line length [Expected: 400; Actual: 419]
docs/github-management.md:40:401 error MD013/line-length Line length [Expected: 400; Actual: 522]
docs/node-autoscaling.md:14 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/oidc-kubectl.md:95 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/runtime-security.md:114 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/rwx-storage.md:9 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/unifi-management.md:14 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
docs/unifi-management.md:62 error MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"]
README.md:116:401 error MD013/line-length Line length [Expected: 400; Actual: 540]
README.md:237:32 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]
README.md:237:36 error MD060/table-column-style Table column style [Table pipe does not align with header for style "aligned"]

(Truncated to last 6666 characters out of 6993)
⚠️ REPOSITORY / trivy - 1 error
astructure/vertical-pod-autoscalers/hubble-relay.yaml:7-11
────────────────────────────────────────
   7 ┌ spec:
   8 │   targetRef:
   9 │     apiVersion: apps/v1
  10 │     kind: Deployment
  11 └     name: hubble-relay
────────────────────────────────────────



k8s/providers/hetzner/infrastructure/vertical-pod-autoscalers/hubble-ui.yaml (kubernetes)
=========================================================================================
Tests: 117 (SUCCESSES: 116, FAILURES: 1)
Failures: 1 (UNKNOWN: 0, LOW: 0, MEDIUM: 1, HIGH: 0, CRITICAL: 0)

KSV-0037 (MEDIUM): VerticalPodAutoscaler 'hubble-ui' should not be set with 'kube-system' namespace
════════════════════════════════════════
ensure that user resources are not placed in kube-system namespace

See https://avd.aquasec.com/misconfig/ksv-0037
────────────────────────────────────────
 k8s/providers/hetzner/infrastructure/vertical-pod-autoscalers/hubble-ui.yaml:7-11
────────────────────────────────────────
   7 ┌ spec:
   8 │   targetRef:
   9 │     apiVersion: apps/v1
  10 │     kind: Deployment
  11 └     name: hubble-ui
────────────────────────────────────────



k8s/providers/hetzner/infrastructure/vertical-pod-autoscalers/metrics-server.yaml (kubernetes)
==============================================================================================
Tests: 117 (SUCCESSES: 116, FAILURES: 1)
Failures: 1 (UNKNOWN: 0, LOW: 0, MEDIUM: 1, HIGH: 0, CRITICAL: 0)

KSV-0037 (MEDIUM): VerticalPodAutoscaler 'metrics-server' should not be set with 'kube-system' namespace
════════════════════════════════════════
ensure that user resources are not placed in kube-system namespace

See https://avd.aquasec.com/misconfig/ksv-0037
────────────────────────────────────────
 k8s/providers/hetzner/infrastructure/vertical-pod-autoscalers/metrics-server.yaml:7-11
────────────────────────────────────────
   7 ┌ spec:
   8 │   targetRef:
   9 │     apiVersion: apps/v1
  10 │     kind: Deployment
  11 └     name: metrics-server
────────────────────────────────────────



k8s/providers/hetzner/infrastructure/vertical-pod-autoscalers/snapshot-controller.yaml (kubernetes)
===================================================================================================
Tests: 117 (SUCCESSES: 116, FAILURES: 1)
Failures: 1 (UNKNOWN: 0, LOW: 0, MEDIUM: 1, HIGH: 0, CRITICAL: 0)

KSV-0037 (MEDIUM): VerticalPodAutoscaler 'snapshot-controller' should not be set with 'kube-system' namespace
════════════════════════════════════════
ensure that user resources are not placed in kube-system namespace

See https://avd.aquasec.com/misconfig/ksv-0037
────────────────────────────────────────
 k8s/providers/hetzner/infrastructure/vertical-pod-autoscalers/snapshot-controller.yaml:7-11
────────────────────────────────────────
   7 ┌ spec:
   8 │   targetRef:
   9 │     apiVersion: apps/v1
  10 │     kind: Deployment
  11 └     name: snapshot-controller
────────────────────────────────────────



k8s/providers/hetzner/infrastructure/vertical-pod-autoscalers/spire-agent.yaml (kubernetes)
===========================================================================================
Tests: 117 (SUCCESSES: 116, FAILURES: 1)
Failures: 1 (UNKNOWN: 0, LOW: 0, MEDIUM: 1, HIGH: 0, CRITICAL: 0)

KSV-0037 (MEDIUM): VerticalPodAutoscaler 'spire-agent' should not be set with 'kube-system' namespace
════════════════════════════════════════
ensure that user resources are not placed in kube-system namespace

See https://avd.aquasec.com/misconfig/ksv-0037
────────────────────────────────────────
 k8s/providers/hetzner/infrastructure/vertical-pod-autoscalers/spire-agent.yaml:9-13
────────────────────────────────────────
   9 ┌ spec:
  10 │   targetRef:
  11 │     apiVersion: apps/v1
  12 │     kind: DaemonSet
  13 └     name: spire-agent
────────────────────────────────────────



k8s/providers/hetzner/infrastructure/vertical-pod-autoscalers/spire-server.yaml (kubernetes)
============================================================================================
Tests: 117 (SUCCESSES: 116, FAILURES: 1)
Failures: 1 (UNKNOWN: 0, LOW: 0, MEDIUM: 1, HIGH: 0, CRITICAL: 0)

KSV-0037 (MEDIUM): VerticalPodAutoscaler 'spire-server' should not be set with 'kube-system' namespace
════════════════════════════════════════
ensure that user resources are not placed in kube-system namespace

See https://avd.aquasec.com/misconfig/ksv-0037
────────────────────────────────────────
 k8s/providers/hetzner/infrastructure/vertical-pod-autoscalers/spire-server.yaml:7-11
────────────────────────────────────────
   7 ┌ spec:
   8 │   targetRef:
   9 │     apiVersion: apps/v1
  10 │     kind: StatefulSet
  11 └     name: spire-server
────────────────────────────────────────



k8s/providers/hetzner/infrastructure/vertical-pod-autoscalers/tetragon-operator.yaml (kubernetes)
=================================================================================================
Tests: 117 (SUCCESSES: 116, FAILURES: 1)
Failures: 1 (UNKNOWN: 0, LOW: 0, MEDIUM: 1, HIGH: 0, CRITICAL: 0)

KSV-0037 (MEDIUM): VerticalPodAutoscaler 'tetragon-operator' should not be set with 'kube-system' namespace
════════════════════════════════════════
ensure that user resources are not placed in kube-system namespace

See https://avd.aquasec.com/misconfig/ksv-0037
────────────────────────────────────────
 k8s/providers/hetzner/infrastructure/vertical-pod-autoscalers/tetragon-operator.yaml:7-11
────────────────────────────────────────
   7 ┌ spec:
   8 │   targetRef:
   9 │     apiVersion: apps/v1
  10 │     kind: Deployment
  11 └     name: tetragon-operator
────────────────────────────────────────



k8s/providers/hetzner/infrastructure/vertical-pod-autoscalers/tetragon.yaml (kubernetes)
========================================================================================
Tests: 117 (SUCCESSES: 116, FAILURES: 1)
Failures: 1 (UNKNOWN: 0, LOW: 0, MEDIUM: 1, HIGH: 0, CRITICAL: 0)

KSV-0037 (MEDIUM): VerticalPodAutoscaler 'tetragon' should not be set with 'kube-system' namespace
════════════════════════════════════════
ensure that user resources are not placed in kube-system namespace

See https://avd.aquasec.com/misconfig/ksv-0037
────────────────────────────────────────
 k8s/providers/hetzner/infrastructure/vertical-pod-autoscalers/tetragon.yaml:9-13
────────────────────────────────────────
   9 ┌ spec:
  10 │   targetRef:
  11 │     apiVersion: apps/v1
  12 │     kind: DaemonSet
  13 └     name: tetragon
────────────────────────────────────────



📣 Notices:
  - Version 0.73.0 of Trivy is now available, current version is 0.71.2

To suppress version checks, run Trivy scans with the --skip-version-check flag

(Truncated to last 6666 characters out of 342400)

✅ Linters with no issues

actionlint, betterleaks, git_diff, golangci-lint, grype, jsonlint, lychee, osv-scanner, prettier, prettier, revive, secretlint, shellcheck, shfmt, syft, trivy-sbom, trufflehog, v8r, v8r, yamllint, zizmor

Notices

📣 MegaLinter 9.5.0 is out! Discover the new features and security recommendations in the release announcement. (Skip this info by defining SECURITY_SUGGESTIONS: false)

See detailed reports in MegaLinter artifacts

MegaLinter is graciously provided by OX Security
Show us your support by starring ⭐ the repository

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

CI is settled at ccd8543396 — 19 pass, 0 fail, 12 skipped by change filters. 🧪 Validate Manifests (the job that consumes k8s changes) ran and passed, as did kubescape and mega-linter.

@coderabbitai review

Worth a look at two things in particular:

  • the CustomResourceStateMetrics paths in config-map.yaml — a wrong path yields zero series silently, which reads identically to a healthy cluster, so it is the failure mode that matters most here;
  • the two-container split: kube-state-metrics binds to loopback only and the agent scrapes it in-pod, so nothing needs to reach it from the network and no Service exists.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- PR metadata and changed files ---'
gh pr view 2994 --repo devantler-tech/platform --json number,title,baseRefName,headRefName,files --jq '
  "PR #\(.number): \(.title)\nbase=\(.baseRefName) head=\(.headRefName)",
  (.files[] | "\(.path)")

printf '%s\n' '--- Diff ---'
gh pr diff 2994 --repo devantler-tech/platform --color=never

printf '%s\n' '--- Candidate manifest files ---'
fd -t f -E .git -E node_modules 'config-map\.yaml|configmap\.yaml|.*usage.*scraper.*|.*coroot.*' . | sort

printf '%s\n' '--- Kubernetes resource references ---'
rg -n -i -C 4 'CustomResourceStateMetrics|kube-state-metrics|usage-scraper|127\.0\.0\.1|localhost|metrics-port|--web\.listen-address|scrape' . \
  -g '*.yaml' -g '*.yml' -g '*.json' -g '!**/node_modules/**' || true

Length of output: 1213


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- PR metadata ---'
gh pr view 2994 --repo devantler-tech/platform \
  --json number,title,baseRefName,headRefName \
  --jq '. as $pr | "PR #\($pr.number): \($pr.title)\nbase=\($pr.baseRefName) head=\($pr.headRefName)"'

printf '%s\n' '--- Changed files ---'
gh pr view 2994 --repo devantler-tech/platform --json files \
  --jq '.files[] | .path'

printf '%s\n' '--- Diff ---'
gh pr diff 2994 --repo devantler-tech/platform --color=never

printf '%s\n' '--- Candidate manifest files ---'
fd -t f -E .git -E node_modules -i 'config-map\.yaml|configmap\.yaml|.*usage.*scraper.*|.*coroot.*' . | sort

printf '%s\n' '--- Kubernetes resource references ---'
rg -n -i -C 4 'CustomResourceStateMetrics|kube-state-metrics|usage-scraper|127\.0\.0\.1|localhost|metrics-port|--web\.listen-address|scrape' . \
  -g '*.yaml' -g '*.yml' -g '*.json' -g '!**/node_modules/**' || true

Length of output: 90604


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- Repository managed resources and provider references ---'
rg -n -i -C 8 \
  'apiVersion:\s*repo\.github\.m\.upbound\.io/|kind:\s*Repository\b|provider-github|github\.m\.upbound\.io' \
  . -g '*.yaml' -g '*.yml' -g '!**/node_modules/**' || true

printf '%s\n' '--- CRD definitions and condition fixtures ---'
fd -t f -E .git -E node_modules -i '.*crd.*|.*repository.*|.*crossplane.*' . | sort | head -300
rg -n -i -C 8 \
  'status:\s*$|conditions:\s*$|Synced|Ready|repo\.github\.m\.upbound\.io' \
  . -g '*.yaml' -g '*.yml' -g '*.json' -g '!**/node_modules/**' || true

printf '%s\n' '--- Related validation and component tests ---'
rg -n -i -C 5 \
  'usage-scraper|crossplane-sync-exporter|custom-resource-state|kube-state-metrics|CustomResourceStateMetrics' \
  scripts k8s -g '*.sh' -g '*.yaml' -g '*.yml' -g '*.md' 2>/dev/null || true

Length of output: 201283


🤖 web_search executed:

kube-state-metrics v2.17.0 custom resource state metrics Gauge valueFrom status True False documentation

Length of output: 15382


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- Exact Repository GVK declarations ---'
rg -n -i -C 6 '^apiVersion:[[:space:]]*repo\.github\.m\.upbound\.io/|^kind:[[:space:]]*Repository[[:space:]]*$' \
  . -g '*.yaml' -g '*.yml' -g '!**/node_modules/**' || true

printf '%s\n' '--- Provider package and GitHub configuration sources ---'
rg -n -i -C 6 'provider-github|provider-family-github|github.*upbound|repo\.github\.m\.upbound\.io' \
  k8s .github -g '*.yaml' -g '*.yml' -g '*.json' -g '*.md' 2>/dev/null || true

printf '%s\n' '--- OpenCost contract structure and CI registration ---'
sed -n '1,240p' scripts/tests/test-opencost-usage-scraper.sh
rg -n -C 4 'test-opencost-usage-scraper\.sh|Validate Manifests|scripts/tests' \
  .github/workflows -g '*.yaml' -g '*.yml' 2>/dev/null || true

printf '%s\n' '--- Existing PR review comments ---'
gh pr view 2994 --repo devantler-tech/platform --comments

Length of output: 85056


@devantler I completed the focused review.

The CustomResourceStateMetrics configuration is correct. path: [status, conditions], labelsFromPath.condition: [type], and valueFrom: [status] match the kube-state-metrics condition-array pattern. kube-state-metrics converts Crossplane condition values True and False to 1 and 0.

The two-container design is correct. Both containers share the Pod network namespace. kube-state-metrics binds to 127.0.0.1:8080, and the Prometheus agent scrapes 127.0.0.1:8080. No Service or network ingress is required.

I found one regression risk: the PR does not add a rendered-component contract test for the exporter. The existing test-opencost-usage-scraper.sh pattern protects the analogous metric delivery path. A future edit to the GVK, [status, conditions] path, metric name, relabel filter, or loopback endpoint would still pass manifest schema validation but could produce zero series.

Please add a test-crossplane-sync-exporter.sh contract and register it in .github/workflows/ci.yaml. The test should assert that the component remains default-off and that its rendered ConfigMap and Deployment retain the required metric configuration and 127.0.0.1:8080 wiring.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

Verification record

Static. kubectl kustomize over a harness that references the component builds clean and renders exactly 5 resources (SA, ClusterRole, ClusterRoleBinding, ConfigMap, Deployment). CI at ccd8543396: 19 pass / 0 fail / 12 filtered-skip, with 🧪 Validate Manifests, kubescape and 🧹 Lint - mega-linter all in the pass set rather than skipped.

⚠️ But CI does not actually cover these files, and the green above should not be read as if it does. Validate Manifests runs ksail workload validate over k8s/clusters/{local,prod}, and a Kustomize Component is only reachable through a components: reference — so an unreferenced component is built by nothing. The standalone build above is the only thing that has read these files. Filed as #2995.

Config correctness, against live data. The failure mode that matters here is a wrong path yielding zero series silently, which is indistinguishable from a healthy cluster. So I simulated the declared CustomResourceStateMetrics extraction over the real 20 Repository resources:

Check Result
Series the config would emit 60 (20 resources × 3 conditions)
Condition types found Synced, Ready, LastAsyncOperation
Value domain at [status] exactly True / False
Entries with a null reason label 0
Synced=False / Synced=True 3 / 17
Ready=True while Synced=False 3

Every declared JSON path resolves against real objects, and that last row is this PR's whole reason for existing — measured, not argued. The condition-Gauge shape and the string→numeric conversion (type="Ready"} 1.0) were confirmed against kube-state-metrics' own documentation rather than from recall.

What is NOT claimed. I have not observed the metric arrive in coroot-prometheus. The component ships unreferenced, so there is nothing running to observe, and I hold read-only cluster access. That check belongs to the activation change: query crossplane_managed_resource_condition and expect it to agree with a live kubectl read — 3 Synced=False, 17 Synced=True. Until then #2986 belongs in 📊 Verifying, not Done.

The image digest was resolved from registry.k8s.io rather than copied from memory.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@devantler, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 31 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 918c6d7b-d23a-4ba3-aa33-4166ebdee741

📥 Commits

Reviewing files that changed from the base of the PR and between aac1531 and 3494eca.

📒 Files selected for processing (8)
  • .github/workflows/ci.yaml
  • k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/cluster-role-binding.yaml
  • k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/cluster-role.yaml
  • k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/config-map.yaml
  • k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/deployment.yaml
  • k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/kustomization.yaml
  • k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/service-account.yaml
  • scripts/tests/test-crossplane-sync-exporter.sh
📝 Walkthrough

Walkthrough

Adds a default-off Kustomize component for Crossplane sync metrics. It creates a ServiceAccount, read-only RBAC for repositories, and a ConfigMap for kube-state-metrics and Prometheus. A single-replica Deployment runs both components with restricted security settings, loopback endpoints, pinned images, resource limits, and a 1 GiB emptyDir volume.

Possibly related issues

Possibly related PRs

  • devantler-tech/platform#2794 — Adds a similar Kubernetes Prometheus agent pattern with dedicated RBAC, ConfigMap, Deployment, and Coroot remote-write configuration.

Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error)

Check name Status Explanation Resolution
Linked Issues check ❌ Error The PR exports only Repository resources and adds a new ClusterRole, so it misses generic API-group coverage and the no-new-ClusterRole requirement in [#2986]. Support managed resources across API groups and bind the ServiceAccount to the existing crossplane-view ClusterRole without adding a new ClusterRole.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: exporting Crossplane managed-resource conditions as metrics.
Description check ✅ Passed The description explains the Crossplane metric problem, implementation, default-off behavior, and linked issue.
Out of Scope Changes check ✅ Passed The changes remain focused on the Crossplane metrics exporter and do not introduce unrelated functionality.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/kustomization.yaml`:
- Around line 24-29: Add the crossplane sync exporter rendered-component
contract test as test-crossplane-sync-exporter.sh, covering both cluster
overlays, default-off behavior, and required RBAC, ConfigMap, Deployment,
metric-filter, and 127.0.0.1:8080 wiring. Register this test in the CI workflow
defined by ci.yaml so it runs before merge.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e598153a-6296-4d69-8ff8-bec964375301

📥 Commits

Reviewing files that changed from the base of the PR and between aac1531 and ccd8543.

📒 Files selected for processing (6)
  • k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/cluster-role-binding.yaml
  • k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/cluster-role.yaml
  • k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/config-map.yaml
  • k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/deployment.yaml
  • k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/kustomization.yaml
  • k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/service-account.yaml
📜 Review details
🧰 Additional context used
📓 Path-based instructions (3)
k8s/**/*.yaml

📄 CodeRabbit inference engine (AGENTS.md)

k8s/**/*.yaml: Kubernetes base files under k8s/bases/ are immutable; modify them through Kustomize patches: in provider or cluster overlays.
Preserve Flux dependency order: bootstrapinfrastructure-controllersinfrastructureapps.
Use one Kubernetes resource per file, except explicitly whitelisted vendored CDI and KubeVirt operator bundles.
Use kebab-case directories and Kind-led filenames in component folders; qualify filenames with a purpose when multiple resources share a Kind.
Before a manifest PR, run schema-aware KSail validation when available, otherwise build both k8s/clusters/local/ and k8s/clusters/prod/ with kubectl kustomize; use client-side dry-run for changed individual manifests.

Files:

  • k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/service-account.yaml
  • k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/cluster-role-binding.yaml
  • k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/cluster-role.yaml
  • k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/deployment.yaml
  • k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/config-map.yaml
  • k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/kustomization.yaml
**/*.{yaml,yml}

📄 CodeRabbit inference engine (AGENTS.md)

Use Conventional Commit messages and create pull requests as drafts; keep changes small and focused.

Files:

  • k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/service-account.yaml
  • k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/cluster-role-binding.yaml
  • k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/cluster-role.yaml
  • k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/deployment.yaml
  • k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/config-map.yaml
  • k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/kustomization.yaml
k8s/bases/infrastructure/**/*.yaml

📄 CodeRabbit inference engine (AGENTS.md)

k8s/bases/infrastructure/**/*.yaml: Place infrastructure resources in component-first folders; split CRs into plural-Kind folders only for dependency-order or cluster-scoped/cross-cutting reasons.
FeatureFlag CRs belong in the infrastructure layer, never the controllers layer, because their CRD is installed by the controller.

Files:

  • k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/service-account.yaml
  • k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/cluster-role-binding.yaml
  • k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/cluster-role.yaml
  • k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/deployment.yaml
  • k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/config-map.yaml
  • k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/kustomization.yaml
🧠 Learnings (1)
📚 Learning: 2026-07-01T21:13:36.950Z
Learnt from: devantler
Repo: devantler-tech/platform PR: 2359
File: k8s/bases/apps/actual-budget/helm-release.yaml:62-111
Timestamp: 2026-07-01T21:13:36.950Z
Learning: When reviewing Kustomize/Helm YAML in this repo, keep the base vs provider overlay split: `k8s/bases/apps/**` and `k8s/bases/infrastructure/**` should contain each app’s full, environment-agnostic configuration (including base-level postRenderer Kustomize patches such as deployment strategy, topology spread, probes, and env injection). `k8s/providers/{docker,hetzner}/**` should only add small provider-specific deltas (e.g., `interval`, `persistence.size`) via patch files (like `k8s/providers/<provider>/apps/<app>/patches/helm-release-patch.yaml`). If configuration is identical across providers (e.g., OIDC/OAuth env vars where `${domain}` is resolved per cluster via envsubst), it belongs in the base and must not be duplicated into provider overlays.

Applied to files:

  • k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/service-account.yaml
  • k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/cluster-role-binding.yaml
  • k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/cluster-role.yaml
  • k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/deployment.yaml
  • k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/config-map.yaml
  • k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/kustomization.yaml
🪛 Trivy (0.72.0)
k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/deployment.yaml

[warning] 41-74: Restrict container images to trusted registries

Container kube-state-metrics in deployment crossplane-sync-exporter (namespace: observability) uses an image from an untrusted registry.

Rule: KSV-0125

Learn more

(IaC/Kubernetes)


[warning] 75-104: Restrict container images to trusted registries

Container prometheus-agent in deployment crossplane-sync-exporter (namespace: observability) uses an image from an untrusted registry.

Rule: KSV-0125

Learn more

(IaC/Kubernetes)

🔇 Additional comments (5)
k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/service-account.yaml (1)

1-6: LGTM!

k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/cluster-role.yaml (1)

1-17: LGTM!

k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/cluster-role-binding.yaml (1)

1-12: LGTM!

k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/config-map.yaml (1)

1-68: LGTM!

k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter/deployment.yaml (1)

1-111: LGTM!

A wrong GVK, a renamed series, or a scrape target drifting from the
kube-state-metrics listen address all export zero series, and an empty result is
indistinguishable from a fleet with nothing wrong. Schema validation cannot see
any of it, so assert the rendered contract instead.

Follows the test-opencost-usage-scraper.sh contract for the sibling component.
…tract

A substring assertion passes on a longer value, so renaming the metric prefix
from crossplane to crossplaneX satisfied 'metricNamePrefix: crossplane' and the
ablation did not fire. Compare the trimmed line exactly instead.
@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

@coderabbitai review

New head 3494ecaf4f — your finding is fixed and the thread is resolved. Both commits since the last review are mine: the contract test plus its CI registration, then a correction to the test itself.

CI at this head: 20 pass / 0 fail. I confirmed the new step 🔄 Validate Crossplane sync exporter metrics path ran and succeeded rather than reading "0 failures" — a skipped step would have looked identical in the rollup.

The correction is the part worth a second look: the test's first version used a substring match, so renaming metricNamePrefix: crossplane to crossplaneX still passed. It now compares whole trimmed lines. All 8 ablations fire distinct assertions, with the baseline and post-revert control both passing.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- PR changed files and current diff ---'
gh pr view 2994 --repo devantler-tech/platform --json files --jq '.files[] | .path'
gh pr diff 2994 --repo devantler-tech/platform --color=never -- \
  scripts/tests/test-crossplane-sync-exporter.sh \
  .github/workflows/ci.yaml \
  k8s/bases/infrastructure/coroot/components/crossplane-sync-exporter

printf '%s\n' '--- Candidate file locations ---'
fd -t f -E .git -E node_modules \
  'test-crossplane-sync-exporter\.sh|ci\.ya?ml|config-map\.yaml|deployment\.yaml|kustomization\.yaml' . | sort

printf '%s\n' '--- Shell test structure ---'
test_file="$(fd -t f -E .git -E node_modules '^test-crossplane-sync-exporter\.sh$' . | head -n 1)"
if [ -n "$test_file" ]; then
  ast-grep outline "$test_file" --items all || true
  cat -n "$test_file"
fi

printf '%s\n' '--- CI registration context ---'
rg -n -C 10 'test-crossplane-sync-exporter\.sh|Validate Crossplane sync exporter metrics path' \
  .github/workflows -g 'ci.yaml' -g 'ci.yml' || true

printf '%s\n' '--- Exporter manifest structure ---'
component_dir="$(fd -t d -E .git -E node_modules '^crossplane-sync-exporter$' k8s | head -n 1)"
if [ -n "$component_dir" ]; then
  for file in "$component_dir"/config-map.yaml "$component_dir"/deployment.yaml "$component_dir"/kustomization.yaml; do
    if [ -f "$file" ]; then
      printf '\n--- %s ---\n' "$file"
      cat -n "$file"
    fi
  done
fi

Length of output: 2424


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- Relevant PR diff ---'
gh pr diff 2994 --repo devantler-tech/platform --color=never

printf '%s\n' '--- Contract test ---'
test_file="$(fd -t f -E .git -E node_modules '^test-crossplane-sync-exporter\.sh$' . | head -n 1 || true)"
if [ -z "$test_file" ]; then
  echo 'Contract test was not found.'
else
  cat -n "$test_file"
fi

printf '%s\n' '--- CI registration ---'
ci_file="$(fd -t f -E .git -E node_modules '^ci\.ya?ml$' .github/workflows | head -n 1 || true)"
if [ -z "$ci_file" ]; then
  echo 'CI workflow was not found.'
else
  rg -n -C 12 'test-crossplane-sync-exporter\.sh|Validate Crossplane sync exporter metrics path' "$ci_file" || true
fi

printf '%s\n' '--- Exporter manifests checked by the contract ---'
component_dir="$(fd -t d -E .git -E node_modules '^crossplane-sync-exporter$' k8s | head -n 1 || true)"
if [ -z "$component_dir" ]; then
  echo 'Exporter component directory was not found.'
else
  for file in "$component_dir"/config-map.yaml "$component_dir"/deployment.yaml "$component_dir"/kustomization.yaml; do
    if [ -f "$file" ]; then
      printf '\n--- %s ---\n' "$file"
      cat -n "$file"
    fi
  done
fi

Length of output: 50409


@devantler The correction is valid.

require_line() trims whitespace and compares the complete line with =. It rejects metricNamePrefix: crossplaneX when the required value is metricNamePrefix: crossplane.

The contract now protects the required GVK, condition paths, metric-name coupling, relabel filter, ConfigMap, Deployment, default-off state, and 127.0.0.1:8080 wiring.

The workflow both detects changes to scripts/tests/test-crossplane-sync-exporter.sh and runs the contract for Kubernetes changes.

I found no follow-up issues in this focused review. 🐇 ✅


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

You are interacting with an AI system.

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

@devantler I will perform a full review of the pull request, including all changed files and their integration points.

⚠️ Action not completed

Review rate limited.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 31 minutes.

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

@codex review

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

CodeRabbit refused at this head — included-review limit reached under its fair-usage policy, next included review in 31 minutes. That is a service state, not a verdict on the change, so it does not gate the merge; recording it and advancing one lane rather than waiting.

Note for the record: its commit status still reads success with description Review skipped: automatic reviews are disabled, and the only review object at this head is an empty reply container. Neither is a review. The last real CodeRabbit review (1 actionable comment, since fixed and resolved) was at the previous head ccd8543396.

@codex review

Focus, if useful: the CustomResourceStateMetrics config in config-map.yaml — a wrong path or a renamed metric exports zero series, which is indistinguishable from a healthy fleet. scripts/tests/test-crossplane-sync-exporter.sh is the guard for exactly that; its assertions compare whole trimmed lines because an earlier substring version passed on the very rename it was meant to catch.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

1 similar comment
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

Codex refused at this head too — account code-review usage limits reached, no retry window stated (06:03:08Z, direct on this PR). Advancing to the third and last lane.

The next comment is a bare trigger with nothing else in it: Bugbot exact-matches the whole comment body, so any extra line silently voids the request, and a voided request is indistinguishable from a dead lane.

@devantler

Copy link
Copy Markdown
Contributor Author

@cursor review

@cursor

cursor Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_83a64a1b-fb14-4885-8092-bc556482e7d5)

Metric relabeling excludes automatically generated timeseries, so the exporter's
liveness signal survives the filter. Without it, an absent condition series would
be ambiguous between a healthy fleet and a dead exporter — the same silent-sensor
failure this component exists to surface.

@devantler devantler left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Generated by the Agentic Engineer

Self-review (fallback — CodeRabbit, Codex and Cursor Bugbot unavailable)

Reviewed commit: 1b748f28ce511b3a06449327b6544382793c4a88

All three external lanes were tried in priority order at this exact head, each with direct per-PR
evidence, and none will deliver a review:

  • CodeRabbitstate=success :: description=Review rate limited at this head (06:02Z), summary
    comment carries quota markers, zero review objects. An earlier request at 05:48Z was additionally
    swallowed as a thread reply (review_comment_addressed); a bare full review re-trigger was
    accepted at 06:01Z and then hit the limit. No retry window stated.
  • CodexYou have reached your Codex usage limits for code reviews (06:03:08Z, 28s after request).
  • Cursor Bugbot — check-run neutral with output.title: Error, plus a cursor[bot] comment
    Bugbot couldn't run - usage limit reached (06:04:3xZ). Zero inline comments — the never-ran shape.

What I verified against the live cluster (not from the diff alone)

  • GVK is exact and correct. repositories.repo.github.m.upbound.io serves only v1alpha1
    (served+storage), live objects report repo.github.m.upbound.io/v1alpha1, scope Namespaced.
    CustomResourceStateMetrics takes no wildcard, so a drifted version would export zero series; it
    does not drift. The config's namespace: [metadata, namespace] label is right for a namespaced kind.
  • Condition value domain is exactly {True, False} across all live conditions, which is what the
    Gauge's string→numeric conversion handles. No third value to mis-map.
  • The fixture is real and non-zero right now: 20 Repository MRs — 17 Synced=True, 3
    Synced=False, and 3 with Ready=True while Synced=False — the exact defect this metric
    exists to surface.
  • Metric-name coupling holds: metricNamePrefix: crossplane + name: managed_resource_condition
    crossplane_managed_resource_condition, which is precisely the keep regex in the scrape config.
  • RBAC is minimal — get/list/watch on one kind, no wildcard; the test asserts the absence of one.

Checked and cleared (recorded so it is not re-litigated)

I suspected the metric_relabel_configs action: keep would also drop up{job=...}, which would
reintroduce this component's own failure mode — an exporter that dies looking identical to a healthy
fleet. It does not. Prometheus generates up and the scrape_* series in the scrape loop rather
than through metric relabeling; scrape_samples_post_metric_relabeling exists precisely to report the
post-relabel count, so those synthetic series are produced around that step. The deployed
opencost/usage-scraper uses the identical pattern. Worth re-confirming empirically at activation
time
, since the alerting rule in #2987 will depend on up being present to distinguish
"exporter down" from "nothing wrong".

Scope note

Coverage is deliberately partial — Repository only, of 16 managed-resource CRDs — because
CustomResourceStateMetrics requires an exact GVK per kind and Repository is the kind carrying both
a positive and a negative fixture. The component ships default-off (unreferenced), so CI's overlay
builds do not exercise it; the standalone components:-referencing contract test is the only thing
that does, and that is why it exists.

Verdict: no P0/P1 findings.

@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

Review provenance note (honesty about what was read at which commit).

My self-review posted against 1b748f28ce, but I read the diff at 3494ecaf4f — a sibling instance
pushed 1b748f28ce while the review was being written. I have since read the delta in full and the
review stands unchanged at the posted head.

The delta is +9 lines, one file, comment-only: a YAML comment inside the prometheus.yml block of
the exporter ConfigMap, recording that the keep filter does not drop up. No manifest field, no
rendered resource, and no contract-test assertion is affected.

Worth noting: that commit and my "Checked and cleared" section were produced independently and reached
the same conclusion
— I raised the up-is-dropped concern, verified it against Prometheus'
scrape-loop behaviour, and cleared it; the sibling committed the same finding as an in-code comment.
Two independent derivations agreeing is meaningful corroboration for the one property this component's
future alerting rule will depend on.

CI is re-running at 1b748f28ce. Promotion still waits on it going green.

@devantler devantler left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Generated by the Agentic Engineer

Self-review (fallback — CodeRabbit, Codex and Cursor Bugbot unavailable)

Reviewed commit: 1b748f28ce511b3a06449327b6544382793c4a88

All three lanes were tried at this head, in priority order, one at a time, with direct per-PR evidence — not inherited from another PR's digest:

Lane Outcome Evidence
CodeRabbit refused Review rate limited — included-review limit under Fair Usage; next included review in 31 minutes (06:01Z)
Codex refused You have reached your Codex usage limits for code reviews, no retry window (06:03:08Z)
Cursor Bugbot never ran check-run conclusion=neutral / output.title=Error, Bugbot run failed after 1 second, 0 inline comments; cursor[bot]: usage limit reached (06:04:31Z)

A provider's billing state is not a verdict on this change, so it does not gate the merge. Two of the three refusals need a human to lift a spend limit; neither is retryable on a timer.

What I checked, and how

The failure mode that matters here is silent. A wrong path, a drifted GVK or a renamed metric exports zero series, and zero series is indistinguishable from a fleet with nothing wrong. So every check below was run against real data or real documentation rather than reasoned:

  • Config paths, against the live cluster. Simulated the declared extraction over the 20 real Repository resources: 60 series, conditions Synced/Ready/LastAsyncOperation, value domain exactly True/False, 0 null reason labels. Every declared JSON path resolves. Synced=False on 3, Synced=True on 17, and 3 with Ready=True while Synced=False — this PR's whole reason for existing, measured and non-zero.
  • kube-state-metrics flags. --custom-resource-state-only together with --custom-resource-state-config-file is the documented exclusive-custom-resource mode; --host/--port/--telemetry-host/--telemetry-port all exist. Checked against the upstream CLI reference, since a wrong flag crashloops the container and I had asserted these in a test without verifying them.
  • up is not dropped by the keep-filter. Metric relabeling excludes automatically generated timeseries, so up{job="crossplane-sync-exporter"} still reaches the store. This one nearly went the other way: I had it written up as a defect before checking. It matters because without up, an absent condition series is ambiguous between healthy fleet and dead exporter — the same silent-sensor failure this component exists to surface. Recorded in the config, because #2987's alert depends on it.
  • Embedded config parses. Both block scalars are valid YAML and structurally correct (kind: CustomResourceStateMetrics, 1 resource, GVK repo.github.m.upbound.io/v1alpha1/Repository, prefix crossplane, metric managed_resource_condition, Gauge). Schema validation treats block scalars as opaque strings — this repo has a dedicated CI step for exactly that blind spot.
  • Deploy safety. The PR touches only the unreferenced component, its test, and one CI step. The exporter appears 0 times in the rendered prod overlay, so the merge_group prod deploy is a no-op for it.
  • The test actually tests. 8 ablations, each moving a mechanism rather than deleting it, each firing its own distinct assertion, with baseline and post-revert control both passing.

Deliberate trade-offs, stated rather than left implicit

  • Both containers share the pod's service-account token, so the Prometheus agent holds a token it never uses. Splitting them would require a Service and a network-reachable metrics endpoint, which is a worse exposure for a sidecar that only scrapes loopback. The shared pod is the smaller surface.
  • Coverage is one GVK, not all 16. CustomResourceStateMetrics matches an exact groupVersionKind — no wildcard, no categories: managed selector — and CI has no cluster, so nothing in the repo could detect drift in a 16-entry list. Starting with the kind that carries the observed failure proves the pipeline before committing to a list that would rot silently. Extending it is separate work with its own detection story.

Not claimed

I have not observed the metric arrive in coroot-prometheus. The component ships unreferenced and I hold read-only cluster access, so there is nothing running to observe. That check belongs to the activation change: query crossplane_managed_resource_condition and expect agreement with a live kubectl read — 3 Synced=False, 17 Synced=True. #2986 therefore goes to 📊 Verifying, not Done.

Verdict: no P0/P1 findings.

@devantler
devantler marked this pull request as ready for review August 6, 2026 06:13
@devantler

Copy link
Copy Markdown
Contributor Author

🤖 Generated by the Agentic Engineer

Readiness — promoting at 1b748f28ce

1. Programmatically tested. CI green at this head (0 failing, 0 pending). The contract test
test-crossplane-sync-exporter.sh is registered on both the path filter and a run step, and I
confirmed the step ran and succeeded rather than reading "0 failures" — a skipped step is
invisible in the rollup. Its assertion battery was proven by 8/8 ablations firing distinct
assertions
, with a baseline and a post-revert control both passing.

2. Reviewed. Clean local review round at this exact head — self@1b748f28ce, no P0/P1 findings.
All three external lanes were tried in priority order with direct per-PR evidence and none would
deliver: CodeRabbit Review rate limited, Codex usage limits, Bugbot usage limit reached
(check-run neutral + Error, zero inline comments).

3. Tried and evaluated as a user. Rendered the component standalone through a
components:-referencing harness — 5 resources exactly (SA, ClusterRole, ClusterRoleBinding,
ConfigMap, Deployment). Beyond the render, I evaluated it against the live cluster, which is where
this component's silent-failure mode would actually bite:

  • The declared GVK resolves: repositories.repo.github.m.upbound.io serves only v1alpha1, live
    objects report repo.github.m.upbound.io/v1alpha1, scope Namespaced (so the namespace label
    path is required and present). CustomResourceStateMetrics takes no wildcard, so this is the
    difference between all series and zero series.
  • Condition status values across the live fleet are exactly {True, False} — the full domain the
    Gauge's string→numeric conversion must handle.
  • The fixture is real and non-zero right now: 20 Repository MRs, 17 Synced=True, 3
    Synced=False, and 3 reporting Ready=True while Synced=False — precisely the state this
    metric exists to make visible.

⚠️ Deliberate limit, stated rather than papered over: this component is default-off and
therefore unreferenced, so the overlay builds in CI never construct it — green overlay CI proves
nothing about these files. The standalone contract test is the only thing that does. Activation,
alerting (#2987), and extending coverage past Repository to the other 15 managed kinds are separate
changes.

Enqueueing via the merge queue (--auto, no explicit strategy — the queue sets it).

@devantler
devantler added this pull request to the merge queue Aug 6, 2026
Merged via the queue into main with commit ddcfb5c Aug 6, 2026
31 checks passed
@devantler
devantler deleted the claude/crossplane-sync-exporter-2820 branch August 6, 2026 06:19
@github-project-automation github-project-automation Bot moved this from 🫴 Ready to ✅ Done in 🌊 Project Board Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: ✅ Done

Development

Successfully merging this pull request may close these issues.

Expose Crossplane managed-resource sync state as metrics Coroot can query

1 participant