Skip to content

[API proposal] Typed healthCheckExprs / waitStrategy on ApplicationDefinition and ComponentInstall (kstatus readiness, #2642) #3263

Description

@myasnikovdaniil

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.Ready means "every resource in the release manifest is healthy" per kstatus. For an HR whose manifest is a custom resource (CNPG Cluster, Strimzi Kafka, KubeVirt VirtualMachine, …), kstatus can't infer readiness — it needs a spec.healthCheckExprs CEL block, evaluated under spec.waitStrategy.name: poller. Today two HR builders emit those wrapping HRs and have no way to set these fields:

  • apiserver — pkg/registry/apps/application/rest.go::convertApplicationToHelmRelease builds the HR for every catalog app (postgres, kafka, mongodb, …).
  • operator — internal/operator/package_reconciler.go::buildHelmReleaseSpec builds 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.mod currently pins github.com/fluxcd/helm-controller/api v1.4.3. The WaitStrategy and CustomHealthCheck Go types (and HelmReleaseSpec.HealthCheckExprs) do not exist in v1.4.3 — only the runtime CRD is v1.5.0. Step 0 of implementation is go 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.CustomHealthCheck type so there is exactly one shape to learn and validate.

api/v1alpha1/applicationdefinitions_types.go — extend ApplicationDefinitionRelease:

type ApplicationDefinitionRelease struct {
    ChartRef *helmv2.CrossNamespaceSourceReference `json:"chartRef"`
    Labels   map[string]string                     `json:"labels,omitempty"`
    Prefix   string                                `json:"prefix"`

    // WaitStrategy sets HelmReleaseSpec.WaitStrategy (poller|legacy).
    // +optional
    // +kubebuilder:validation:Enum=poller;legacy
    WaitStrategy string `json:"waitStrategy,omitempty"`

    // HealthCheckExprs sets HelmReleaseSpec.HealthCheckExprs — CEL health
    // expressions for the custom resource(s) this app renders.
    // +optional
    HealthCheckExprs []helmv2.CustomHealthCheck `json:"healthCheckExprs,omitempty"`
}

api/v1alpha1/packagesource_types.go — the identical pair on ComponentInstall (platform singletons: KubeVirt/CDI/Linstor).

Threading:

  • apiserver: populate config.ReleaseConfig from the new spec.release fields (same startup loader that already reads release.cozystack.io/* annotations) → set Spec.WaitStrategy / Spec.HealthCheckExprs in convertApplicationToHelmRelease.
  • operator: set the same two fields in buildHelmReleaseSpec from ComponentInstall.
  • Root make generate (deepcopy + openapi) + regenerate the ApplicationDefinition CRD and cozyrds blobs. (Watch the cozyvalues-gen reorder trap — hand-edit generated blobs, don't blind-commit make generate.)

Why typed over annotation

The existing precedent (release.cozystack.io/helm-install-disable-wait, -timeout) is a scalar annotation. healthCheckExprs is a structured list of multi-line CEL — JSON-in-an-annotation would be unreadable and unvalidated. A typed field gets openapi validation, the enum on waitStrategy, and reuses the upstream type verbatim.

Backward compatibility

  • Both fields omitempty / optional → zero behavior change for any app/package that doesn't set them.
  • No change to the annotation precedent; annotations and typed fields can coexist.
  • Setting waitStrategy: poller is the v1.5.0 runtime default already, so an app that only adds healthCheckExprs behaves 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 (EtcdCluster readyMembers). Platform singletons KubeVirt/CDI/Linstor via ComponentInstall.

Explicitly excluded: redis (RedisFailover has no status subresource — helm-controller can't gate on it; stays on WorkloadMonitor), valkey (does not exist in the repo), the 19 in-tenant-cluster kubernetes addon HRs (parent is disable-wait by design), and all config-only CRs.

Questions for API owners

  1. OK to depend on helmv2.CustomHealthCheck directly in cozystack.io/v1alpha1 types (couples our API to the helm-controller Go module version)? Alternative: mirror a minimal local struct.
  2. Field home on the apiserver side: ApplicationDefinition.spec.release (proposed) vs a new spec.readiness block?
  3. Any concern bumping helm-controller/api to v1.5.x in the shared go.mod ahead of the rest of the Flux Go deps?

Activity

  1. added
    kind/api-changeCategorizes issue or PR as related to adding, removing, or otherwise changing an API
    area/platformIssues or PRs related to platform infrastructure (bundle, flux, talos, installer)
    on Jul 9, 2026
  2. kvaps commented on Jul 9, 2026

    @kvaps
    Member

    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.CustomHealthCheck directly? Yes. ApplicationDefinitionRelease already embeds *helmv2.CrossNamespaceSourceReference for chartRef, so the coupling to the helm-controller api submodule 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.release vs a new spec.readiness? Keep it in spec.release. That block already is "the config that shapes the generated HelmRelease" (chartRef/labels/prefix), and both new fields map 1:1 onto HelmReleaseSpec. A separate spec.readiness would 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's ReleaseConfig at startup like the rest of spec.release. On the ComponentInstall side, keeping the two fields flat (next to upgradeCRDs, which already maps to Upgrade.CRDs) is the matching local convention — good.

    Q3 — bump helm-controller/api to 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 confirmed WaitStrategy / CustomHealthCheck / HealthCheckExprs do not exist in the v1.4.3 api submodule (while CrossNamespaceSourceReference does). The api submodule is intentionally thin and independently versioned, so bumping just it is low-risk. Do it as an isolated step-0 commit and glance over the go.mod/go.sum transitive diff; just confirm a matching github.com/fluxcd/helm-controller/api tag exists at v1.5.x.

    Refinements (none blocking):

    1. waitStrategy shape. Upstream HelmReleaseSpec.WaitStrategy is an object {name: poller|legacy}, not a scalar — the seaweedfs template already writes waitStrategy: {name: poller}. Your WaitStrategy string is nicer DX, but it contradicts the "reuse the upstream type verbatim, one shape to learn" rationale you use for healthCheckExprs. I'm fine with keeping the scalar enum — just document explicitly in the field doc that it maps to .name and is a deliberate simplification.

    2. Couple the default. healthCheckExprs are only evaluated under the poller strategy. Today that works because poller is the v1.5.0 runtime default, but it makes evaluation silently dependent on that global default never flipping (the UseHelm3Defaults gate the epic forbids). Suggest: when healthCheckExprs is non-empty and waitStrategy is unset, have both builders default the HR to poller. That makes an app setting only healthCheckExprs self-contained and removes the seaweedfs "must also set poller" footgun.

    3. disableWait scope. [Epic] Adopt the kstatus readiness model platform-wide (Flux 2.8 / helm-controller v1.5.0) #2642 lists disableWait as a possible sibling field, but this proposal drops it. Note waitStrategy: legacy is not equivalent to disableWait — 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 operator disableWait is 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.

    4. Serialization sanity. On the apiserver path the fields thread through config.ReleaseConfig. That struct carries yaml: tags but is only ever built in-memory and json.Marshaled (start.go), and helmv2.CustomHealthCheck is json-tagged, so this is fine — just don't introduce a gopkg.in/yaml.v3 round-trip of ReleaseConfig, which wouldn't honor the json tags. make generate deepcopy for []helmv2.CustomHealthCheck will delegate to helmv2's deepcopy the same way chartRef already does.

    5. Failure mode to document. OpenAPI validates only the struct shape, not that the CEL compiles or that the referenced apiVersion/kind CRD 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 to timeout. Worth a one-liner in the field doc that the HR timeout is 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.

  3. myasnikovdaniil commented on Aug 14, 2026

    @myasnikovdaniil
    ContributorAuthor

    Implemented by #3273, which landed the typed waitStrategy and healthCheckExprs on both ComponentInstall and ApplicationDefinitionRelease and threaded them through the operator and the apiserver HR builders. Closing. Tracked in #2642 Task 3.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/platformIssues or PRs related to platform infrastructure (bundle, flux, talos, installer)kind/api-changeCategorizes issue or PR as related to adding, removing, or otherwise changing an APItriage/needs-triageIndicates an issue needs triage by a maintainer

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions