Repository navigation
[Epic] Adopt the kstatus readiness model platform-wide (Flux 2.8 / helm-controller v1.5.0) #2642
Description
Epic. Flux 2.8 (helm-controller v1.5.0) flips HelmRelease health assessment to the kstatus model platform-wide. The version bump landed in #2602 (merged), so this epic tracks the consequential work: an API change to
ComponentInstall/ApplicationDefinition(healthCheckExprs,waitStrategy) plus per-package adoption across the three readiness classes below.
Status as of 2026-08-13
The plumbing is finished and adoption has barely begun. Task 1 landed in #2602 via Option B, and the typed API plus its threading through both HR builders landed in #3273, but no package on origin/main sets waitStrategy or healthCheckExprs through that typed API, so it is a live and entirely unconsumed surface. Only two HelmRelease templates carry a kstatus gate anywhere in the tree, packages/extra/seaweedfs/templates/seaweedfs-db.yaml and packages/extra/monitoring/templates/helmrelease.yaml, and both got there through unrelated PRs rather than through this epic. Phase 1 of the template work (#3264) is a stalled draft: CHANGES_REQUESTED since 2026-07-20 over dead failed predicates, with five unticked dev-cluster verification boxes and an E2E run that never reached the changed code. Two corrections to the original plan are recorded in Task 2 and Task 5 below. The "add healthCheckExprs to all 12 umbrellas" framing is wrong for umbrellas that wrap only native workloads, and packages/apps/kubernetes plus packages/apps/kubernetes-nodes are structurally excluded from the model as long as they carry release.cozystack.io/helm-install-disable-wait.
Context
PR #2602 (feat(flux): upgrade to v2.8.0 — tenant chart v0.48.0 + management-cluster embed, web UI opt-in) upgrades Flux from v2.7.3 → v2.8.0 across both planes:
- tenant clusters via
flux-operator/flux-instancecharts v0.33.0 → v0.48.0 (now at 0.50.0, still trackingdistribution.version: 2.8.xinpackages/system/fluxcd/values.yaml). - management cluster via the embedded
internal/fluxinstall/manifests/fluxcd.yamlregenerated from theflux-aiotimoni bundle (helm-controller:v1.5.0,internal/fluxinstall/manifests/fluxcd.yaml:8097).
In particular helm-controller goes v1.4.3 → v1.5.0. Beyond the version bump, v1.5.0 flips two defaults to align with Helm v4 (verbatim from the v1.5.0 CHANGELOG):
The controller now uses Helm v4, and, with this change, new default behaviors are being introduced (breaking changes) to keep Flux and Helm aligned:
Apply method is now defaulting to server-side apply for new HelmReleases.
Health checks now default to using kstatus for assessing readiness and failures of applied resources.
waitStrategy.name now defaults to poller (kstatus polling of every applied resource, including managed HelmReleases), and can be reverted to legacy (Helm v3 wait, core kinds only) via the UseHelm3Defaults controller feature gate. Cozystack does not enable that gate.
The kstatus default makes HelmRelease.Ready mean "every applied resource is healthy" instead of "helm install returned". That is correct for the end-to-end semantics we want between packages, spec.dependsOn becomes a real readiness gate instead of an ordering hint, but it interacts badly with existing umbrella patterns and is currently unreachable for leaf packages.
Symptoms
After applying #2602 on dev10, tenant-root/kubernetes-test HR enters Reconciling and its child HRs report DependencyNotReady. Cycle shape: parent waits on each child via kstatus → each child has dependsOn: [parent] → deadlock. Same shape (different chains) already fixed in 60aecd5f (vpa vpa-for-vpa nested HR) and unrelated yaml-merge bug in a23b5d27 (seaweedfs dependsOn collision). Retrospective write-up in docs/release.md:565-571.
Tasks
1. Unblock the Flux 2.8 bump (Class 3, single package), DONE
packages/apps/kubernetes is the only package in the monorepo whose child HRs carry dependsOn: [{Release.Name}] (guarded by a lookup). Pick one:
- Option A, operator-side knob. Extend
internal/operator/package_reconciler.go::buildHelmReleaseSpecto setspec.waitStrategy.name: legacy(orspec.install.disableWait: true+spec.upgrade.disableWait: true) on the parent HR forcozystack.kubernetes-application. Not taken, superseded by Option B. - Option B, child-side, remove the guard. Delete the
{{- if lookup … Release.Name }}block from the 19 child HR templates inpackages/apps/kubernetes/templates/helmreleases/. Landed in feat(flux): upgrade to v2.8.0 + chart fixes for strict SSA & kstatus (folds #2612) #2602 (f6c737983). No child HR references the bare parent any more, all remainingdependsOnentries point at siblings, andpackages/apps/kubernetes/templates/helmreleases/ouroboros.yaml:93keeps the comment explaining why no render-time guard is wired.
2. Class 2 umbrellas adopt healthCheckExprs (template edits only)
Correction (#3264 readiness audit, unmerged, needs sign-off). The original "add healthCheckExprs to every umbrella" framing is wrong. kstatus already assesses Deployments, StatefulSets and child HelmReleases natively, and a built-in workload has no empty-status race to close, so an expression on such an umbrella is a no-op. A gate is required only where the HR's manifest is a custom resource. Rows below are annotated with that verdict, and the 8 marked no-op are candidates for closing rather than pending work.
-
packages/apps/openbao(inner HR → OpenBao CR), no-op per feat(flux): kstatus health gates on CR-wrapping umbrella HelmReleases #3264 audit -
packages/apps/qdrant, no-op per feat(flux): kstatus health gates on CR-wrapping umbrella HelmReleases #3264 audit -
packages/apps/bucket, no-op per feat(flux): kstatus health gates on CR-wrapping umbrella HelmReleases #3264 audit -
packages/apps/nats, no-op per feat(flux): kstatus health gates on CR-wrapping umbrella HelmReleases #3264 audit -
packages/apps/harbor, needed (CNPGCluster), drafted in feat(flux): kstatus health gates on CR-wrapping umbrella HelmReleases #3264, blocked on a deadfailedpredicate. The bundledRedisFailoverhas no status subresource and cannot be gated. -
packages/apps/tenant, the package now has 7 HR children, not the 5 originally listed (computeplane,etcd,gateway,info,ingress,monitoring,seaweedfs). Needed foretcd(EtcdCluster) andgateway(TenantGateway), both drafted in feat(flux): kstatus health gates on CR-wrapping umbrella HelmReleases #3264, no-op for the other five. -
packages/extra/external-dns, no-op per feat(flux): kstatus health gates on CR-wrapping umbrella HelmReleases #3264 audit -
packages/extra/ingress, no-op per feat(flux): kstatus health gates on CR-wrapping umbrella HelmReleases #3264 audit -
packages/extra/monitoring, landed in fix(monitoring): surface missing VictoriaLogs and gate dead-end vlinsert ExternalName #3215 (f9afc7309):packages/extra/monitoring/templates/helmrelease.yaml:30setswaitStrategy.name: poller,:51gates onVLCluster.status.updateStatus, field pins inpackages/extra/monitoring/tests/helmrelease_test.yaml:22-43. feat(flux): kstatus health gates on CR-wrapping umbrella HelmReleases #3264 would extend it toVMCluster/VMAgent/VMAlert/VMAlertmanager,Grafanaand the backing CNPGCluster. -
packages/extra/seaweedfs, landed in refactor(seaweedfs): split seaweedfs-system into seaweedfs-db + seaweedfs-system #2601 (29c6afc84):packages/extra/seaweedfs/templates/seaweedfs-db.yaml:24-25setswaitStrategy.name: poller,:30-35gates on the CNPGCluster. The sibling-systemchild (packages/extra/seaweedfs/templates/seaweedfs.yaml:195) is intentionally ungated (native workloads). See Task 5 for the contestedfailedpredicate. -
packages/system/bootbox, no-op per feat(flux): kstatus health gates on CR-wrapping umbrella HelmReleases #3264 audit -
packages/system/cozystack-basics, no-op per feat(flux): kstatus health gates on CR-wrapping umbrella HelmReleases #3264 audit
3. Class 1 leaves, operator support for per-package knobs
These packages render the underlying CR directly, the wrapping HR is built in Go. Adopting Model B requires plumbing new fields through the operator and through the apiserver. The epic originally named only the operator, but every catalog app's HR is built by pkg/registry/apps/application/rest.go::convertApplicationToHelmRelease.
- Extend
cozystack.io/v1alpha1.ComponentInstall(orPackage) inapi/v1alpha1/withhealthCheckExprs, optionallywaitStrategyanddisableWait. Landed in feat(api): typed waitStrategy/healthCheckExprs for kstatus readiness (#2642) #3273:api/v1alpha1/packagesource_types.go:154,164onComponentInstallandapi/v1alpha1/applicationdefinitions_types.go:97,111onApplicationDefinitionRelease, both typed askustomize.CustomHealthCheckand gated by apoller;legacyenum. Prerequisitego.modbump ofhelm-controller/apiv1.4.3 → v1.5.1 landed in the same PR (83f0d3bc9,go.mod:11).disableWaitwas not added, the pre-existingrelease.cozystack.io/helm-install-disable-waitannotation (pkg/config/config.go:59) already covers it. - Thread them into
internal/operator/package_reconciler.go::buildHelmReleaseSpec. Landed in feat(api): typed waitStrategy/healthCheckExprs for kstatus readiness (#2642) #3273 atinternal/operator/package_reconciler.go:100-106, plus the apiserver builder atpkg/registry/apps/application/rest.go:1645-1646fed frompkg/cmd/server/start.go:274-275. Shared coupling helperconfig.ResolveWaitStrategy(pkg/config/config.go:220-228) defaults topollerwhenever expressions are set without an explicit strategy. Tests:internal/operator/package_reconciler_test.go:164,pkg/registry/apps/application/rest_helmrelease_spec_test.go:140,pkg/config/waitstrategy_test.go:30. - Adopt per-package via values. Zero packages consume the typed API today. No
ApplicationDefinitioninpackages/system/*-rd/cozyrds/and noPackageSourceinpackages/core/platform/sources/sets either field. Candidate leaves and their natural readiness signals:-
packages/apps/postgres→ CNPGCluster.status.phase == 'Cluster in healthy state' -
packages/apps/kafka→ StrimziKafkaReady condition -
packages/apps/mongodb→ PerconaPerconaServerMongoDB.status.state -
packages/apps/mariadb→ mariadb-operatorMariaDBReady -
packages/apps/clickhouse→ClickHouseInstallation.status.status -
packages/apps/redis,rabbitmq,foundationdb,opensearch(valkeydropped, novalkey-rdpackage exists on main) -
packages/apps/vm-instance,vm-disk→ KubeVirtVirtualMachine.status.printableStatus
-
4. Out of scope (explicit)
- Do not enable
--feature-gates=UseHelm3Defaults=trueon helm-controller. The global flip would undo every Model B gain from tasks 2 and 3 and force the project back onto Helm v3 wait semantics. Verified holding:UseHelm3Defaultsappears on main only inside CRD description text ininternal/fluxinstall/manifests/fluxcd.yaml, and the only feature gates actually set there areExternalArtifact=true(:8014,:8079).
5. Follow-ups discovered after the epic was written
- The merged CNPG
failedpredicate inpackages/extra/seaweedfs/templates/seaweedfs-db.yaml:34may be dead code, tracked in its own issue, Verify whether the seaweedfs-db CNPG failed health-check predicate is reachable #3807. TheEtcdClusterstatus.brokenMembers > 0predicate flagged by the same review stays inside feat(flux): kstatus health gates on CR-wrapping umbrella HelmReleases #3264's scope, since that predicate is not merged. - Decide what to do about
helm-install-disable-wait.packages/apps/kubernetesandpackages/apps/kubernetes-nodescarryrelease.cozystack.io/helm-install-disable-wait: "true"(packages/system/kubernetes-rd/cozyrds/kubernetes.yaml:28,packages/system/kubernetes-nodes-rd/cozyrds/kubernetes-nodes.yaml:16) to break a real chicken-and-egg with the talos-reconcile Job. Upstream evaluateshealthCheckExprsonly when the Helm action has wait enabled, so any gate on those two kinds is a silent no-op (documented atapi/v1alpha1/applicationdefinitions_types.go:105-109). Either accept them as permanently outside the kstatus model or find a wait-compatible way to break the cycle. - Document the readiness contract for package authors.
git grep -il kstatus -- docs/hits onlydocs/changelogs/v1.5.0.mdand the retrospective atdocs/release.md:565-571. There is no guidance on when a gate is required, how to write the three predicates, or thatdisableWaitdefeats them. - [API proposal] Typed healthCheckExprs / waitStrategy on ApplicationDefinition and ComponentInstall (kstatus readiness, #2642) #3263 was closed as implemented by feat(api): typed waitStrategy/healthCheckExprs for kstatus readiness (#2642) #3273. The API-proposal issue asked for typed
healthCheckExprs/waitStrategyonApplicationDefinitionandComponentInstall. That surface and its threading landed in #3273, so anyone arriving from a link to [API proposal] Typed healthCheckExprs / waitStrategy on ApplicationDefinition and ComponentInstall (kstatus readiness, #2642) #3263 should read feat(api): typed waitStrategy/healthCheckExprs for kstatus readiness (#2642) #3273 instead.
References
- PR feat(flux): upgrade to v2.8.0 + chart fixes for strict SSA & kstatus (folds #2612) #2602: feat(flux): upgrade to v2.8.0 + chart fixes for strict SSA & kstatus (folds #2612) #2602
- PR refactor(seaweedfs): split seaweedfs-system into seaweedfs-db + seaweedfs-system #2601 (
seaweedfs-dbreference gate): refactor(seaweedfs): split seaweedfs-system into seaweedfs-db + seaweedfs-system #2601 - PR fix(monitoring): surface missing VictoriaLogs and gate dead-end vlinsert ExternalName #3215 (
monitoring-systemreference gate): fix(monitoring): surface missing VictoriaLogs and gate dead-end vlinsert ExternalName #3215 - PR feat(api): typed waitStrategy/healthCheckExprs for kstatus readiness (#2642) #3273 (typed API + threading): feat(api): typed waitStrategy/healthCheckExprs for kstatus readiness (#2642) #3273
- PR feat(flux): kstatus health gates on CR-wrapping umbrella HelmReleases #3264 (Phase 1 template gates, blocked draft): feat(flux): kstatus health gates on CR-wrapping umbrella HelmReleases #3264
- Issue [API proposal] Typed healthCheckExprs / waitStrategy on ApplicationDefinition and ComponentInstall (kstatus readiness, #2642) #3263 (API proposal, implemented by feat(api): typed waitStrategy/healthCheckExprs for kstatus readiness (#2642) #3273): [API proposal] Typed healthCheckExprs / waitStrategy on ApplicationDefinition and ComponentInstall (kstatus readiness, #2642) #3263
- helm-controller v1.5.0 CHANGELOG: https://github.com/fluxcd/helm-controller/blob/v1.5.0/CHANGELOG.md
- HelmRelease API spec v2: https://github.com/fluxcd/helm-controller/blob/v1.5.0/docs/spec/v2/helmreleases.md
- Flux 2.8.0 release notes: https://github.com/fluxcd/flux2/releases/tag/v2.8.0
- CEL health-checks cheatsheet: https://fluxcd.io/flux/cheatsheets/cel-healthchecks/
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsTodo
Activity
fix(kubernetes): drop lookup-guarded parent-HR dependsOn on tenant ad…
fix(kubernetes): drop lookup-guarded parent-HR dependsOn on tenant ad…
fix(kubernetes): drop lookup-guarded parent-HR dependsOn on tenant ad…
fix(kubernetes): drop lookup-guarded parent-HR dependsOn on tenant ad…
myasnikovdaniil commentedon Jun 2, 2026
Status update (as of main @ f0f1431)
Base PR #2602 (Flux v2.7.3 → v2.8.0) is merged. Per-package migration status below.
✅ Task 1 — Unblock the Flux 2.8 bump (kubernetes package) — DONE
Resolved via Option B (child-side, remove the guard) in
f6c737983(
fix(kubernetes): drop lookup-guarded parent-HR dependsOn on tenant addon child HRs).dependsOn: [{Release.Name}]against the parent anymore.dependsOnentries point to sibling children (e.g.cilium → -gateway-api-crds).lookup/Release.Nameguard is gone;ouroboros.yamlretains only an explanatorycomment on why no render-time guard is wired.
Option A (operator-side knob) was not taken.
healthCheckExprs— ~1 of 12packages/extra/seaweedfs— hashealthCheckExprs+waitStrategy: poller(targets its backing CNPG
Cluster; landed incidentally via29c6afc84)packages/apps/openbaopackages/apps/qdrantpackages/apps/bucketpackages/apps/natspackages/apps/harborpackages/apps/tenant(seaweedfs, ingress, etcd, monitoring, info)packages/extra/external-dnspackages/extra/ingresspackages/extra/monitoringpackages/system/bootboxpackages/system/cozystack-basics❌ Task 3 — Class 1 leaves, operator support — NOT STARTED
ComponentInstall/Packageinapi/v1alpha1/— nohealthCheckExprs/waitStrategy/disableWaitfields.buildHelmReleaseSpecininternal/operator/package_reconciler.go— no readiness knobs threaded.foundationdb, opensearch, valkey, vm-instance, vm-disk) adopted anything.
✅ Task 4 — Out of scope guard respected
--feature-gates=UseHelm3Defaults=trueis not enabled anywhere (only appears in CRDdescription text in the embedded
fluxcd.yaml).Remaining work: Task 2 (11/12 umbrellas) and all of Task 3 (operator plumbing + every leaf).
[-]Flux 2.8 readiness model migration: per-package work[/-][+][Epic] Adopt the kstatus readiness model platform-wide (Flux 2.8 / helm-controller v1.5.0)[/+]build(deps): bump helm-controller/api to v1.5.1 for kstatus types
feat(api): typed waitStrategy/healthCheckExprs for kstatus readiness
feat(api): typed waitStrategy/healthCheckExprs for kstatus readiness (#…
myasnikovdaniil commentedon Aug 14, 2026
Re-audited this epic against
origin/mainat09dc7f47fand rewrote the body to match the tree. Six of the 25 checkboxes are now checked: Task 1 Option B landed in #2602 (f6c737983),extra/seaweedfsin #2601 (29c6afc84),extra/monitoringin #3215 (f9afc7309), and both Task 3 plumbing items in #3273, which also bumpedhelm-controller/apito v1.5.1 and threaded the apiserver HR builder the epic never named. The typed API is a live but unconsumed surface: noApplicationDefinitionunderpackages/system/*-rd/cozyrds/and noPackageSourceunderpackages/core/platform/sources/setswaitStrategyorhealthCheckExprs, so every Task 3 leaf is still at zero. Task 2 needed a correction. Per the readiness audit in #3264, 8 of the 12 umbrellas wrap only native workloads or child HRs that kstatus already assesses, so an expression there is a no-op, and the rows are annotated rather than deleted because that audit lives in an unmerged draft. #3264 itself is blocked rather than idle: CHANGES_REQUESTED since 2026-07-20 overfailedpredicates that never fire, one of which is already merged inseaweedfs-db.yaml:34and now has its own issue, while the unmergedEtcdClusterone stays in #3264's scope. A new Task 5 records the follow-ups the original body predates, including thatpackages/apps/kubernetesandkubernetes-nodesare structurally excluded from the model while they carryrelease.cozystack.io/helm-install-disable-wait. #3263 is closed as implemented by #3273, and thevalkeyleaf is dropped from Task 3 because novalkey-rdpackage exists on main.