Repository navigation
[API proposal] Typed healthCheckExprs / waitStrategy on ApplicationDefinition and ComponentInstall (kstatus readiness, #2642) #3263
Description
Activity
- addedkind/api-changeCategorizes issue or PR as related to adding, removing, or otherwise changing an APICategorizes issue or PR as related to adding, removing, or otherwise changing an APIarea/platformIssues or PRs related to platform infrastructure (bundle, flux, talos, installer)Issues or PRs related to platform infrastructure (bundle, flux, talos, installer)
on Jul 9, 2026 API-owner review. The surface is sound and I'm good with the direction. Answers to your three questions, then a few non-blocking refinements.
Q1 — depend on
helmv2.CustomHealthCheckdirectly? Yes.ApplicationDefinitionReleasealready embeds*helmv2.CrossNamespaceSourceReferenceforchartRef, so the coupling to the helm-controllerapisubmodule version is a precedent we've already accepted. Mirroring a local struct would duplicate a type we don't own and invite drift; reusing it verbatim is the DRY choice and keeps exactly one CEL shape to learn. Keep the direct dependency.Q2 —
spec.releasevs a newspec.readiness? Keep it inspec.release. That block already is "the config that shapes the generated HelmRelease" (chartRef/labels/prefix), and both new fields map 1:1 ontoHelmReleaseSpec. A separatespec.readinesswould split HR-shaping config across two homes and wrongly imply per-instance/dynamic scope — this is per-kind template config, loaded once into the apiserver'sReleaseConfigat startup like the rest ofspec.release. On theComponentInstallside, keeping the two fields flat (next toupgradeCRDs, which already maps toUpgrade.CRDs) is the matching local convention — good.Q3 — bump
helm-controller/apito v1.5.x ahead of the rest of Flux? Fine, and it's fixing a skew rather than getting ahead: the runtime CRD is already v1.5.0 (shipped by #2602) while the Go types are pinned at v1.4.3. I confirmedWaitStrategy/CustomHealthCheck/HealthCheckExprsdo not exist in the v1.4.3apisubmodule (whileCrossNamespaceSourceReferencedoes). Theapisubmodule is intentionally thin and independently versioned, so bumping just it is low-risk. Do it as an isolated step-0 commit and glance over thego.mod/go.sumtransitive diff; just confirm a matchinggithub.com/fluxcd/helm-controller/apitag exists at v1.5.x.Refinements (none blocking):
-
waitStrategyshape. UpstreamHelmReleaseSpec.WaitStrategyis an object{name: poller|legacy}, not a scalar — the seaweedfs template already writeswaitStrategy: {name: poller}. YourWaitStrategy stringis nicer DX, but it contradicts the "reuse the upstream type verbatim, one shape to learn" rationale you use forhealthCheckExprs. I'm fine with keeping the scalar enum — just document explicitly in the field doc that it maps to.nameand is a deliberate simplification. -
Couple the default.
healthCheckExprsare only evaluated under thepollerstrategy. Today that works becausepolleris the v1.5.0 runtime default, but it makes evaluation silently dependent on that global default never flipping (theUseHelm3Defaultsgate the epic forbids). Suggest: whenhealthCheckExprsis non-empty andwaitStrategyis unset, have both builders default the HR topoller. That makes an app setting onlyhealthCheckExprsself-contained and removes the seaweedfs "must also set poller" footgun. -
disableWaitscope. [Epic] Adopt the kstatus readiness model platform-wide (Flux 2.8 / helm-controller v1.5.0) #2642 listsdisableWaitas a possible sibling field, but this proposal drops it. NotewaitStrategy: legacyis not equivalent todisableWait— legacy still waits (Helm-v3 wait on core kinds), disableWait skips waiting entirely. If dropping it is a conscious YAGNI cut (the kubernetes-package deadlock was resolved child-side, Option B, so no operatordisableWaitis needed now), I'm fine with it — just call it out so we add it later only when a concrete leaf needs a true "don't gate at all" escape hatch. -
Serialization sanity. On the apiserver path the fields thread through
config.ReleaseConfig. That struct carriesyaml:tags but is only ever built in-memory andjson.Marshaled (start.go), andhelmv2.CustomHealthCheckis json-tagged, so this is fine — just don't introduce agopkg.in/yaml.v3round-trip ofReleaseConfig, which wouldn't honor the json tags.make generatedeepcopy for[]helmv2.CustomHealthCheckwill delegate to helmv2's deepcopy the same waychartRefalready does. -
Failure mode to document. OpenAPI validates only the struct shape, not that the CEL compiles or that the referenced
apiVersion/kindCRD is installed. A bad expr or a not-yet-installed CRD (you already flag vm-instance/vm-disk as runtime-installed / UNVERIFIED) makes the HR hang totimeout. Worth a one-liner in the field doc that the HRtimeoutis the backstop.
Backward compatibility is a non-issue: both fields are optional/omitempty, additive to two CRDs, and coexist with the existing
release.cozystack.io/*annotations. Good to proceed once the above are settled.-
- addedtriage/needs-triageIndicates an issue needs triage by a maintainerIndicates an issue needs triage by a maintainer
on Aug 12, 2026 myasnikovdaniil commented
on Aug 14, 2026 ContributorAuthorMore actions
Summary
Add typed, first-class fields so a Cozystack package/app can declare the Flux helm-controller v1.5.0 kstatus health expression for the custom resource it wraps. This is the operator/apiserver half of #2642 (the template half — inner child HRs like
seaweedfs-db— needs no API change). Requesting API-owner sign-off on the surface below before implementation.Why an API change is needed
Under helm-controller v1.5.0 (shipped by #2602),
HelmRelease.Readymeans "every resource in the release manifest is healthy" per kstatus. For an HR whose manifest is a custom resource (CNPGCluster, StrimziKafka, KubeVirtVirtualMachine, …), kstatus can't infer readiness — it needs aspec.healthCheckExprsCEL block, evaluated underspec.waitStrategy.name: poller. Today two HR builders emit those wrapping HRs and have no way to set these fields:pkg/registry/apps/application/rest.go::convertApplicationToHelmReleasebuilds the HR for every catalog app (postgres, kafka, mongodb, …).internal/operator/package_reconciler.go::buildHelmReleaseSpecbuilds the HR for platform packages/singletons (KubeVirt, CDI, Linstor).The template path already works (raw YAML, e.g.
packages/extra/seaweedfs/templates/seaweedfs-db.yaml). This proposal covers only the two Go-built paths.Hard prerequisite
go.modcurrently pinsgithub.com/fluxcd/helm-controller/api v1.4.3. TheWaitStrategyandCustomHealthCheckGo types (andHelmReleaseSpec.HealthCheckExprs) do not exist in v1.4.3 — only the runtime CRD is v1.5.0. Step 0 of implementation isgo get github.com/fluxcd/helm-controller/[email protected](+ transitive review). No new field can compile until then.Proposed API surface (decided: typed field, not annotation)
Reuse the upstream
helmv2.CustomHealthChecktype so there is exactly one shape to learn and validate.api/v1alpha1/applicationdefinitions_types.go— extendApplicationDefinitionRelease:api/v1alpha1/packagesource_types.go— the identical pair onComponentInstall(platform singletons: KubeVirt/CDI/Linstor).Threading:
config.ReleaseConfigfrom the newspec.releasefields (same startup loader that already readsrelease.cozystack.io/*annotations) → setSpec.WaitStrategy/Spec.HealthCheckExprsinconvertApplicationToHelmRelease.buildHelmReleaseSpecfromComponentInstall.make generate(deepcopy + openapi) + regenerate the ApplicationDefinition CRD and cozyrds blobs. (Watch the cozyvalues-gen reorder trap — hand-edit generated blobs, don't blind-commitmake generate.)Why typed over annotation
The existing precedent (
release.cozystack.io/helm-install-disable-wait,-timeout) is a scalar annotation.healthCheckExprsis a structured list of multi-line CEL — JSON-in-an-annotation would be unreadable and unvalidated. A typed field gets openapi validation, the enum onwaitStrategy, and reuses the upstream type verbatim.Backward compatibility
omitempty/ optional → zero behavior change for any app/package that doesn't set them.waitStrategy: polleris the v1.5.0 runtime default already, so an app that only addshealthCheckExprsbehaves identically to today until its CR reports status.Adoption (post-merge, one PR per app, verified on a dev cluster)
The full CRD→CEL catalog (evidence-cited to each vendored CRD) is maintained separately as the readiness audit and is the appendix to this work. Leaves and their signals: postgres/CNPG, kafka/Strimzi, mongodb/Percona (string state), mariadb, clickhouse (+keeper, distinct API group
clickhouse-keeper.altinity.com), rabbitmq, foundationdb (health struct), opensearch (string phase), vm-instance/vm-disk (KubeVirt/CDI — CRDs runtime-installed, values UNVERIFIED), etcd (EtcdClusterreadyMembers). Platform singletons KubeVirt/CDI/Linstor viaComponentInstall.Explicitly excluded: redis (
RedisFailoverhas no status subresource — helm-controller can't gate on it; stays on WorkloadMonitor), valkey (does not exist in the repo), the 19 in-tenant-clusterkubernetesaddon HRs (parent isdisable-waitby design), and all config-only CRs.Questions for API owners
helmv2.CustomHealthCheckdirectly incozystack.io/v1alpha1types (couples our API to the helm-controller Go module version)? Alternative: mirror a minimal local struct.ApplicationDefinition.spec.release(proposed) vs a newspec.readinessblock?helm-controller/apito v1.5.x in the sharedgo.modahead of the rest of the Flux Go deps?