Skip to content

feat(flux): gate releases on operator status kstatus cannot read - #3264

Draft
myasnikovdaniil wants to merge 5 commits into
mainfrom
worktree-kstatus-umbrellas
Draft

myasnikovdaniil wants to merge 5 commits into
mainfrom
worktree-kstatus-umbrellas

Conversation

@myasnikovdaniil

@myasnikovdaniil myasnikovdaniil commented Jul 9, 2026 •

Copy link
Copy Markdown
Contributor

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 Ready condition, 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:

  • harbor -system - CNPG Cluster by Ready condition
  • tenant etcd child - EtcdCluster is current with positive replicas, readyMembers == spec.replicas, Available=True and Progressing=False with reason Reconciled, and failed on Available=False with BootstrapFailed or DeadlineExceeded. Every condition is pinned to metadata.generation because status has no root observedGeneration. No inProgress expression, flux evaluates it before failed
  • monitoring -system - VMCluster, VMAgent, VMAlert and VMAlertmanager next to existing VLCluster, VTCluster, VTSingle and VMAuth gates on status.updateStatus, plus CNPG Cluster by Ready
  • bucket - BucketClaim by status.bucketReady and every BucketAccess by status.accessGranted. It is set on ApplicationDefinition, so cozystack-api writes it into new releases and ApplicationDefinition reconciler patches existing ones

CNPG uses one Ready=False reason for bootstrap and for persistent errors, and COSI v1alpha1 status has no error field, so both gates have only current and 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 with RetryOnFailure.

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.users is set in OIDC mode. Job reads API with its own ServiceAccount that can only get grafana and grafana-deployment, keeps API errors in log and does not pass --request-timeout to kubectl (any nonzero value makes kubectl skip in-cluster credentials, #4288).

Tenant gateway child is not touched, main already reads TenantGateway with always-current expression because listener certificates keep it Ready=False while pending.

Known limits. Bucket gate makes bucket Ready truthful, but -system release still renders user credentials Secret through lookup at 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/healthchecks is 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 fake kubectl and curl. make go-module-tests (part of make unit-tests) picks it up.

Screenshots

Not a UI change.

Downstream repositories

Release note

feat(flux): harbor, tenant etcd, monitoring and bucket releases report Ready only once resources created by their operators are usable, monitoring release also waits for Grafana rollout

@coderabbitai

coderabbitai Bot commented Jul 9, 2026 •

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
Finishing Touches

🧪 Generate unit tests (beta)

  • Create PR with unit tests
  • Commit unit tests in branch worktree-kstatus-umbrellas
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@github-actions github-actions Bot added area/platform Issues or PRs related to platform infrastructure (bundle, flux, talos, installer) kind/feature Categorizes issue or PR as related to a new feature size/L This PR changes 100-499 lines, ignoring generated files labels Jul 9, 2026
@myasnikovdaniil

Copy link
Copy Markdown
Contributor Author

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:

  • CNPG Cluster (harbor DB, monitoring DB): Ready=True / reason ClusterIsReady when healthy; Ready=False / reason ClusterIsNotReady during bootstrap — and also for a genuinely-stuck cluster (e.g. out of disk), which the CEL intentionally treats as in-progress with the HR timeout as the backstop (matching the seaweedfs reference). ✓
  • monitoring VMCluster/VMAgent/VMAlert/VMAlertmanager/VLCluster: status.updateStatus == 'operational' when healthy. ✓
  • monitoring Grafana: status.stage == 'complete' && status.stageStatus == 'success'. ✓

Still pending (their CRDs weren't present on the verification cluster): tenant/etcd EtcdCluster (readyMembers gate) and tenant/gateway TenantGateway (Ready condition) — both field-verified from their CRDs, awaiting a cluster that runs them.

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 IvanHunters left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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, Grafana stage/stageStatus, TenantGateway Ready) were checked against upstream and are correct.

@myasnikovdaniil
myasnikovdaniil force-pushed the worktree-kstatus-umbrellas branch from efeae9e to 16e119d Compare September 10, 2026 18:12
@github-actions github-actions Bot added size/XXL This PR changes 1000+ lines, ignoring generated files and removed size/L This PR changes 100-499 lines, ignoring generated files labels Sep 10, 2026

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.

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

  1. The description drifted. The table still says monitoring extends the gate to Grafana on stage/stageStatus, but the rendered HelmRelease has six expressions and none is a Grafana; 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.
  2. The release-note block the PR template asks for is still missing, as the earlier review said.
  3. One clause of the etcd current expression is unpinned. Changing status.readyMembers == spec.replicas to <= leaves the suite green, because no fixture pairs Available=True/QuorumHealthy and Progressing=False/Reconciled with partial membership. quorum-available-but-one-member-unhealthy has no Progressing condition, so a different clause holds it. The rest is pinned properly: renaming the Reconciled reason reddens healthy and single-member-healthy, which is how I checked.
  4. The VMAgent expression matches nothing in tenant-root or cozy-monitoring, since templates/vm/vmagent.yaml is 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.

@myasnikovdaniil
myasnikovdaniil force-pushed the worktree-kstatus-umbrellas branch from 16e119d to 29169b0 Compare September 30, 2026 08:03
@myasnikovdaniil

Copy link
Copy Markdown
Contributor Author

Aleksei Sviridkin (@lexfrei) thanks, pushed the fixes and rebased on main.

  1. B1: the reads fail because of --request-timeout=5s. Any nonzero value makes kubectl skip the in-cluster credentials and go to localhost:8080, same defect as bucket: the ACME cleanup hook passes --request-timeout, so it never reaches the apiserver and reports success anyway #4288 in bucket. The flag is removed and calls are bounded with timeout 10. The hook still renders in every mode, it is the only place where the Grafana rollout is checked. I did not run it on a stand yet, E2E on this head will show.
  2. B2: stderr is kept and logged on every change of state, the abort message ends with the last one. In the kubectl image without a cluster it now prints connection to the server localhost:8080 was refused.
  3. B3: removed. The description is rewritten too (Grafana hook, health-check tests, RBAC, make target, release-note), that covers non-blocking 1 and 2.

Non-blocking 3 has a fixture with Available=True, Reconciled and 2 of 3 members, <= turns it red. 4 is a comment in the template.

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]>
@myasnikovdaniil
myasnikovdaniil force-pushed the worktree-kstatus-umbrellas branch from 29169b0 to 91b4d17 Compare November 13, 2026 19:15
@myasnikovdaniil myasnikovdaniil changed the title feat(flux): kstatus health gates on CR-wrapping umbrella HelmReleases feat(flux): gate releases on operator status kstatus cannot read Nov 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/platform Issues or PRs related to platform infrastructure (bundle, flux, talos, installer) kind/feature Categorizes issue or PR as related to a new feature size/XXL This PR changes 1000+ lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants