Repository navigation
feat(flux): gate releases on operator status kstatus cannot read - #3264
myasnikovdaniil wants to merge 5 commits into
Conversation
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueFinishing Touches
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. |
Verification update — CEL status values confirmed against live CRs (read-only)The convention-based status fields/values these gates key on are confirmed against live custom resources on a cluster running these apps:
Still pending (their CRDs weren't present on the verification cluster): Scope note: this confirms the CELs reference the real status fields/values the operators actually emit. The end-to-end "HR stays not-Ready until the CR is healthy" behavior still needs a deploy of this PR's charts to a dev cluster before un-drafting. |
IvanHunters
left a comment
There was a problem hiding this comment.
Verdict
NOT LGTM
feat(flux): kstatus health gates on CR-wrapping umbrella HelmReleases. Two MAJOR issues: the failed predicates for CNPG Cluster and EtcdCluster are dead code, so genuinely-broken dependencies are classified inProgress forever instead of failed. (Noted: this PR is a draft with an open self-checklist — flagging early.)
Findings
[MAJOR] packages/apps/harbor/templates/harbor.yaml:112 and packages/extra/monitoring/templates/helmrelease.yaml:91 — the failed CEL predicate for the CNPG Cluster is dead code. The condition e.reason != 'ClusterIsNotReady' is unreachable: in the pinned CloudNativePG 1.27.1 (SetClusterReadyCondition, transactions.go:34-56), Ready=False for ANY non-healthy phase always carries reason: ClusterIsNotReady — there is no second reason; the differentiated cause goes into status.phase, not reason. So a genuinely-broken Postgres (harbor-db, alerta-db, grafana-db) is permanently classified inProgress, never failed; the only backstop is timeout + remediation.retries: -1 (infinite silent retry). The same bug is already in production in seaweedfs-db.yaml:33-34; this PR triples the blast radius. Match on status.phase (or drop the reason discriminator) instead.
[MAJOR] packages/apps/tenant/templates/etcd.yaml:50 — the failed predicate for EtcdCluster (status.brokenMembers > 0) is dead for the default PVC-backed storage. Confirmed even by the vendored CRD (etcdclusters.yaml:1859-1860: "While the auto-replacement predicate is a stub it is always 0") and upstream etcd-operator v0.5.2 (isBroken returns false except for StorageMediumMemory). A tenant etcd losing quorum stays inProgress forever, even though the operator already writes a legible Degraded=True, reason=QuorumLost in an adjacent field. Gate on that condition instead.
[MINOR] PR body is missing the required ```release-note block (.github/PULL_REQUEST_TEMPLATE.md).
Caveats
- CI is inconclusive on the new logic: the single E2E run failed on an unrelated dependency (grafana-operator dashboards sidecar) and never reached the code this PR touches — so the new health-check logic was neither confirmed nor refuted by CI.
- The other four new/extended predicates (VM*
updateStatus, Grafanastage/stageStatus,TenantGatewayReady) were checked against upstream and are correct.
efeae9e to
16e119d
Compare
Aleksei Sviridkin (lexfrei)
left a comment
There was a problem hiding this comment.
NOT LGTM. The new Grafana readiness hook wedges the monitoring install, and CI reproduces it on this head.
Business context: an umbrella HelmRelease wrapping a custom resource flips Ready as soon as Helm applies the CR, so dependants start before the backing workload is usable. This gates those releases on the CR's own readiness.
Both major findings from the earlier review are fixed. I checked them against the pinned operators, not against the diff.
The CNPG failed predicate is gone, and harbor and monitoring now declare current only. The EtcdCluster gate no longer keys on brokenMembers; it matches Available=False with reason BootstrapFailed or DeadlineExceeded, pinned to metadata.generation. In the operator at the pinned appVersion v0.5.5 both reasons are written, and Progressing=False with reason Reconciled lands in the same status update as Available=True, so current is reachable. Worth knowing that EtcdClusterStatus has no root-level observedGeneration, so the per-condition generation clauses are the only staleness guard the evaluator gets here. Dropping them would silently remove it.
Data safety on the gated Postgres releases holds. Harbor, the tenant etcd child and monitoring all set RetryOnFailure on install and upgrade, so a gate that never goes green times out and retries in place instead of falling through to uninstall remediation, which is the path that deletes the CNPG Cluster and its PVCs.
Blockers
B1: the Grafana readiness hook never completes and blocks the install
packages/system/monitoring/templates/grafana/oidc-users-job.yaml. The Job now renders in every mode, not only when spec.oidc.users is non-empty, and it holds the release until it can read the Grafana CR and its Deployment. In the run on this head it never read either one.
The run for 16e119dba fails at the install step. Every other branch failing in that window fails at the test step instead, so this is not the fleet-wide red. The cluster report has the chain: monitoring-system reports Helm install failed ... failed post-install: timeout waiting for: [Job/tenant-root/monitoring-system-oidc-users status: 'InProgress'], then monitoring reports timeout waiting for: [HelmRelease/tenant-root/monitoring-system status: 'InProgress'], then tenant-root reports timeout waiting for: [HelmRelease/tenant-root/monitoring status: 'InProgress'].
The Job log is 114 identical lines of Cannot read Grafana or its Deployment; retrying named API GETs from 19:04:17, ending in Grafana did not reach the desired healthy rollout within 10m; aborting at 19:14:21. It never got past the two kubectl get --raw calls, so it never evaluated readiness once.
Grafana was already healthy before the Job started. grafana-deployment hit readyReplicas: 2, availableReplicas: 2, updatedReplicas: 2, observedGeneration: 1 at 19:02:51, carrying monitoring.cozystack.io/grafana-spec-hash: 7652101d... matching the CR. The hash propagation works: the operator copies the annotation to the Deployment and the ReplicaSet. Everything the gate checks was true 90 seconds before the Job started.
The rendered Job has no GF_ADMIN_USER, OIDC_MODE or DESIRED_USERS_JSON env, so $manageUsers was false, and the old if and (ne $mode "None") (gt (len $users) 0) guard would have emitted nothing. This configuration gained a blocking post-install hook it did not have before.
I could not work out why the reads failed, and B2 is why. RBAC is shaped right and was created at 19:00:55, the ServiceAccount exists, the pod carries policy.cozystack.io/allow-to-apiserver and the matching policy is valid. I considered that label newly switching on egress enforcement and ruled it out: allow-internal-communication and allow-to-dns both use an empty endpointSelector, so every pod in the namespace was already under egress policy and already had DNS. Treating it as unrelated.
Since this is a post-install hook timeout under RetryOnFailure, it retries indefinitely rather than surfacing, and every dependant waits behind it.
B2: the hook throws away the error from every API read
Same file. Both kubectl get --raw calls send stderr to /dev/null, and so does the jq after them. A permission denial, an unreachable API server, a missing object and a Grafana that is genuinely still rolling out all produce the same one-line message.
Ten minutes of one repeated line, no status code, no API error, nothing separating "not ready yet" from "cannot reach the API at all". That is the exact distinction this gate exists to make. It is also why B1 cannot be root-caused from artifacts the run already collected. Keep stderr and log it, at least on the final attempt before aborting, so the abort message carries the real failure.
B3: the PR description credits an authoring tool
The body ends with a Generated with [Claude Code] line. Contributor guidance forbids naming an AI model or its vendor in an authorship line in a PR description. The commits get this right: all five carry a bare Assisted-By: LLM trailer and a sign-off. Drop the line from the body.
Non-blocking
- The description drifted. The table still says monitoring extends the gate to
Grafanaonstage/stageStatus, but the rendered HelmRelease has six expressions and none is aGrafana; readiness moved to the rollout hook. The body also never mentions the health-check test module, the readiness RBAC, or the new make target, which is most of the diff. - The release-note block the PR template asks for is still missing, as the earlier review said.
- One clause of the etcd
currentexpression is unpinned. Changingstatus.readyMembers == spec.replicasto<=leaves the suite green, because no fixture pairsAvailable=True/QuorumHealthyandProgressing=False/Reconciledwith partial membership.quorum-available-but-one-member-unhealthyhas noProgressingcondition, so a different clause holds it. The rest is pinned properly: renaming theReconciledreason reddenshealthyandsingle-member-healthy, which is how I checked. - The
VMAgentexpression matches nothing intenant-rootorcozy-monitoring, sincetemplates/vm/vmagent.yamlis guarded not to render there. An expression matching no object in the release is ignored without complaint, so the gate covers one resource fewer in those namespaces than the list reads. A comment would help if that is deliberate.
Rendering the real charts and running the shipped CEL evaluator against status fixtures is the right shape for this, and I confirmed by mutation that both it and the chart tests fail when the rendered values change.
16e119d to
29169b0
Compare
|
Aleksei Sviridkin (@lexfrei) thanks, pushed the fixes and rebased on main.
Non-blocking 3 has a fixture with |
An applied custom resource is not a usable service: Harbor's CNPG Cluster and a tenant's EtcdCluster take minutes to bootstrap, while kstatus reads neither until the operator writes the condition it looks for. The nested releases went Ready on apply, so anything depending on them started against a database or datastore that was not serving. The etcd gate fails early only on the two states the operator stops at, and has no inProgress expression because Flux evaluates that first. CNPG writes one Ready=False reason for every nonhealthy phase, so its gate declares no failure and waits for the timeout. The expressions are executed against status fixtures with the CEL evaluator helm-controller v1.5.0 ships, in a module of its own so its versions follow the controller rather than the API server. Assisted-By: LLM Signed-off-by: Myasnikov Daniil <[email protected]>
…CNPG status The release already waited on its log, trace and auth stores, but went Ready while the metrics cluster, vmagent, vmalert, Alertmanager and the Grafana and Alerta databases were still converging or had failed, so a tenant saw a Ready monitoring stack that could not store or alert. The VictoriaMetrics kinds share the updateStatus contract the existing gates already read. A gate that never goes green must not end in uninstall remediation: that deletes the CNPG Clusters, and with them the database PVCs they own, so both actions retry in place. Assisted-By: LLM Signed-off-by: Myasnikov Daniil <[email protected]>
Grafana operator v5.13.0 reports its Deployment stage done once it has written the Deployment, before any Pod is ready, and kstatus has no Ready condition to read on the Grafana CR. The release went Ready with Grafana still rolling out, and during a failed upgrade an old Pod kept answering health checks for a spec that was no longer the desired one. The chart hashes the rendered Grafana spec into the Pod template, and the post-install/post-upgrade hook now runs in every mode and waits until the Deployment the current Grafana owns carries that hash, is fully rolled out and answers health. Admin credentials and admin API calls stay limited to the mode that manages users. The hook reads only the named Grafana and Deployment through its own ServiceAccount. Assisted-By: LLM Signed-off-by: Myasnikov Daniil <[email protected]>
…rants access BucketClaim and BucketAccess report readiness as booleans with no conditions, which kstatus reads as Current on apply, and the system chart reads the credentials Secret through lookup and renders without it. A bucket went Ready before COSI had created it or written any user's credentials. Assisted-By: LLM Signed-off-by: Myasnikov Daniil <[email protected]>
The e2e guide said only two hand-written HelmRelease templates under packages/extra/ set a wait strategy. The tenant gateway release already set one, and harbor and the tenant etcd child now do too. What a test author needs is that every one of them sets poller, which a count does not say and goes stale whenever a gate is added. Assisted-By: LLM Signed-off-by: Myasnikov Daniil <[email protected]>
29169b0 to
91b4d17
Compare
What this PR does
This PR adds readiness gates to releases whose operators write no status kstatus can read. Under helm-controller v1.5.0 release waits only on what kstatus reads, and for custom resource it reads only
Readycondition, so harbor, tenant etcd, monitoring and bucket releases went Ready right after apply, before database, datastore, metrics stack, Grafana or bucket was usable, and everything depending on them started too early. Part of #2642.Gates:
-system- CNPGClusterbyReadyconditionetcdchild -EtcdClusteris current with positive replicas,readyMembers == spec.replicas,Available=TrueandProgressing=Falsewith reasonReconciled, and failed onAvailable=FalsewithBootstrapFailedorDeadlineExceeded. Every condition is pinned tometadata.generationbecause status has no rootobservedGeneration. NoinProgressexpression, flux evaluates it beforefailed-system-VMCluster,VMAgent,VMAlertandVMAlertmanagernext to existingVLCluster,VTCluster,VTSingleandVMAuthgates onstatus.updateStatus, plus CNPGClusterbyReadyBucketClaimbystatus.bucketReadyand everyBucketAccessbystatus.accessGranted. It is set on ApplicationDefinition, so cozystack-api writes it into new releases and ApplicationDefinition reconciler patches existing onesCNPG uses one
Ready=Falsereason for bootstrap and for persistent errors, and COSI v1alpha1 status has no error field, so both gates have onlycurrentand broken database or bucket waits for release timeout. Uninstall remediation would delete CNPG Clusters together with their PVCs, so harbor, tenant etcd and monitoring retry install and upgrade in place withRetryOnFailure.grafana-operator writes nothing kstatus can read, its stages only confirm that Deployment was written. Existing post-install/post-upgrade Job now renders in every mode and waits until Deployment owned by current Grafana carries hash of rendered Grafana spec, is fully rolled out and answers health. Users work and admin credentials only when
oidc.usersis set in OIDC mode. Job reads API with its own ServiceAccount that can only getgrafanaandgrafana-deployment, keeps API errors in log and does not pass--request-timeoutto kubectl (any nonzero value makes kubectl skip in-cluster credentials, #4288).Tenant
gatewaychild is not touched, main already readsTenantGatewaywith always-current expression because listener certificates keep itReady=Falsewhile pending.Known limits. Bucket gate makes bucket Ready truthful, but
-systemrelease still renders user credentials Secret throughlookupat install time and skips it for good if COSI did not write it yet. Field paths and status values are taken from CRDs and operator source at versions pinned in tree, not checked on live stand yet.Tests
helm-unittest pins rendered fields.
test/healthchecksis separate Go module, it renders real charts and reads bucket ApplicationDefinition, runs every expression through CEL evaluator shipped with helm-controller v1.5.0 against status fixtures, and runs rendered Grafana Job script under dash with fakekubectlandcurl.make go-module-tests(part ofmake unit-tests) picks it up.Screenshots
Not a UI change.
Downstream repositories
Release note