Repository navigation
fix(vm-instance): disable helm readiness wait on the VMInstance release - #3735
Conversation
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository: cozystack/cozystack/.coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (4)
🚧 Files skipped from review as they are similar to previous changes (3)
Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review. 📝 WalkthroughWalkthroughThe change adds shared release annotation handling, reconciles Helm ChangesVM instance Helm readiness
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant ApplicationDefinition
participant ApplicationDefinitionHelmReconciler
participant HelmRelease
ApplicationDefinition->>ApplicationDefinitionHelmReconciler: provide helm-install-disable-wait annotation
ApplicationDefinitionHelmReconciler->>HelmRelease: reconcile Install.DisableWait and Upgrade.DisableWait
ApplicationDefinitionHelmReconciler->>HelmRelease: persist changes when values differ
Merge Risk: ⚪ Minimal · up to The change is mergeable after normal checks; no actionable readiness-control risk is established. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Same content as fork branch feat/vm-instance-cozyplane-static-ip (commit 5187e11), applied on the v1.6.1 bundle: values field, template block, test suite, and the schema, README and RD regenerated with cozyvalues-gen v1.6.0 + hack/update-crd.sh. The RD keeps the helm-install-disable-wait annotation (cozystack#3735) and the hotpluggable schema (cozystack#3978).
Same content as fork branch feat/vm-instance-cozyplane-static-ip (commit 4ddc400), applied on the v1.6.1 bundle: values field, template block, test suite, and the schema, README and RD regenerated with cozyvalues-gen v1.6.0 + hack/update-crd.sh. The RD keeps the helm-install-disable-wait annotation (cozystack#3735), the hotpluggable schema (cozystack#3978) and its kubevirt chartRef.
2728eb1 to
ca251ec
Compare
The generated HelmRelease is health-assessed by helm-controller's kstatus poller, which returns InProgress for any object whose status.observedGeneration trails metadata.generation: a built-in pre-check in fluxcd/pkg/runtime/cel that runs before, and cannot be overridden by, healthCheckExprs. KubeVirt legitimately keeps a VirtualMachine's observedGeneration behind its generation while a template change is pending a restart, the VM is stopped, or a live-updatable change never converges (kubevirt/kubevirt#18700), so a healthy Running VM is read as permanently InProgress and the release never reaches Ready. Releases still carrying the pre-1.5.0 remediation.retries:-1 + rollback spec then revert the previous manifest, dropping just-applied changes such as opened ports. Disable the readiness wait so the release settles once the VirtualMachine is applied; VM health is observed on the VirtualMachine itself. This restores the pre-1.5.0 behaviour under which the VM was never a helm readiness gate. A helm-unittest suite guards that the annotation stays in place. Signed-off-by: IvanHunters <[email protected]>
ca251ec to
f39b557
Compare
Aleksei Sviridkin (lexfrei)
left a comment
There was a problem hiding this comment.
IvanHunters NOT LGTM, for one missing test. The premise holds at the versions this repo ships and disabling the wait is the right fix, but the new branch that skips an invalid annotation can be deleted without any test going red.
Business context: since helm-controller v1.5.0 a VMInstance release waits on a KubeVirt VirtualMachine that kstatus often cannot see as ready, so healthy VMs loop upgrade and timeout. This PR turns the wait off for that kind and carries the setting to releases that already exist.
Premise at the pinned versions
The repo ships helm-controller v1.5.0, which pins Helm v4.1.1, fluxcd/cli-utils v0.37.1-flux.1 and fluxcd/pkg/runtime v0.100.2, plus KubeVirt v1.9.0. With neither DisableWait nor a waitStrategy set, getWaitStrategy in helm-controller falls through to the kstatus poller. KubeVirt v1.9.0 writes status.observedGeneration only from syncGenerationInfo, which runs only while a VMI exists. The generation it copies is bumped only when the template matches the controller revision (vm.go:1438-1465). So a stopped VM, a restart-pending change or a live update all leave observedGeneration behind, and the hang is real.
One correction on the mechanism, which is also in the YAML comment and in #3734. vm-instance sets no healthCheckExprs, so the CEL StatusEvaluator never runs for its VirtualMachine. What fires is kstatus's own checkGeneration in cli-utils (pkg/kstatus/status/generic.go), which has the same rule. Past that check, Compute falls back to checkReadyCondition, so a halted VM with Ready=False is InProgress even when its generation is in sync. This does not change the conclusion. If anything it makes disable-wait a better fit than healthCheckExprs.
What loses its readiness signal
The HelmRelease Ready, and the Application Ready that mirrors it (rest.go keeps WorkloadsReady out of Ready on purpose). On main it meant the VirtualMachine is Ready. After this PR it means the manifests are applied and the update hook Job finished, since hooks are still waited on. That matches the behaviour before v1.5.0, when Helm 3's checker treated any custom resource as ready.
VM health stays visible in WorkloadsReady. The chart's WorkloadMonitor selects the virt-launcher pod by app.kubernetes.io/instance, and for external VMs it also wants LoadBalancer ingress on the Service. I found nothing that waits on a VMInstance HelmRelease: no dependsOn points at one, the Velero strategy and the VM import controller never read its Ready condition, and the vminstance chainsaw suite checks the VMI IP and VM Ready on their own after the HelmRelease assert, so it still covers the boot.
Blocker: the invalid-annotation branch has no test
internal/controller/applicationdefinition_helmreconciler.go:179. The reconciler leaves DisableWait alone when the annotation does not parse. I changed if err != nil { to if err != nil && false { and every test in ./internal/controller still passed. In that state a value like "yes" parses to false and sets DisableWait=false on every release of the kind, which is what the comment says the branch prevents. A test that starts from a release with DisableWait=true, sets the annotation to "yes" and asserts the release is unchanged would close it.
Non-blocking
- The body says
waitStrategy: legacy"buys nothing here, as those are ready the moment they are applied". That is not true forexternal: true: Helm's legacy checker keeps a LoadBalancer Service not ready until it has ingress (pkg/kube/ready.go:260-270at v4.1.1), so legacy would still gate external VMs on IP allocation. Disable-wait is still the better choice, because the kstatus Service check only looks at clusterIP and WorkloadsReady does watch ingress. The body becomes the merge commit, so please fix that sentence and the CEL attribution above. - Both controllers touched here run in cozystack-controller (
cmd/cozystack-controller/main.go:255,263), not in cozystack-operator, which is a separate binary. The newReleaseAnnotationPrefixcomment says cozystack-operator rolls the api, the last two commit subjects usefix(operator), and the body calls these "operator-side gaps". - The label selector that keeps the carry-over on its own kind has no test. Dropping
apps.cozystack.io/application.kindfrom it leaves the suite green. That gap is older than this PR, but the reconciler now writes the wait too, so one release of another kind in the new tests would pin it. - The 20-line comment on the annotation repeats the PR body and the header of
readiness_gate_test.yaml. Two lines on why (observedGeneration trails generation, so kstatus never reports the VM Current) would be enough there. - cozystack-api reads the annotation only at start-up, and the carry-over reacts only to ApplicationDefinition events. An Application written through an api pod that has not restarted yet goes back to DisableWait=false until the definition is reconciled again. The window is short.
Tests: helm unittest on vm-instance-rd passes 2/2, and go test ./internal/controller ./pkg/config passes 54 top-level tests. I mutated the annotation value, the hash field, the prefix filter, the block creation, the upgrade assignment and the in-sync check, and each one turned the matching test red. Only the invalid-annotation branch and the kind selector survived.
CI: the E2E run on this head (35774391142) is still running and I have not seen its suites. The previous run on the earlier head with the same three commits (35196009993) failed in kafka (context deadline) and opensearch (StatefulSet currentRevision != updateRevision), while vminstance and vmdisk passed. Neither failure touches this diff: those definitions carry no disable-wait annotation and their releases already have DisableWait=false, so the carry-over does nothing to them.
The ApplicationDefinition config hash was built from name and spec alone, so a definition whose whole change is a release.cozystack.io/* annotation left the hash untouched and cozystack-api kept running with the configuration it read at its last start-up. Those annotations are only ever read there, once, in buildResourceFromCRD, which makes the config-hash rollout the single mechanism that delivers a change in one: without it the annotation is inert until the api pod restarts for an unrelated reason, and an inert annotation is indistinguishable from one that was never delivered. Hash the release.cozystack.io/* annotations alongside the spec. Only that prefix, because kubectl and Helm rewrite annotations of their own on every apply and hashing those would roll the api Deployment on every reconcile. Assisted-by: LLM Signed-off-by: IvanHunters <[email protected]>
A HelmRelease created before its ApplicationDefinition carried release.cozystack.io/helm-install-disable-wait keeps waiting on readiness, because cozystack-api only reads that annotation while building a release and an existing release is not rebuilt until its Application is written through the apps API again. Where the wait is what broke, that is every existing instance of the kind at once, each looping upgrade and timeout, and rewriting them by hand is the operator's only way out. The definition already drives chartRef, valuesFrom and labels on releases that exist, so drive the wait from there too, in both directions: the API rebuilds the whole spec on every write and emits DisableWait=false once the annotation is gone, so a release left disabled would drift back anyway. An absent install or upgrade block behaves as DisableWait=false already and is only materialised when the definition asks to disable the wait, which keeps a release nothing asks to change from being rewritten. Assisted-by: LLM Signed-off-by: IvanHunters <[email protected]>
…of the wait carry-over The reconciler skips a disable-wait annotation it cannot parse, and it selects releases by application kind. Removing either left the suite green; each now has a test that fails without it. Assisted-by: LLM Signed-off-by: IvanHunters <[email protected]>
…echanism The annotation comment repeated the PR description and the test header; two lines on why are enough. The hang comes from the kstatus generation check in cli-utils, not the CEL status evaluator, since vm-instance sets no healthCheckExprs. The config-hash rollout runs in cozystack-controller, not cozystack-operator. Assisted-by: LLM Signed-off-by: IvanHunters <[email protected]>
|
Thanks for the mutation pass, that is what found both gaps. The invalid-annotation branch now has a test: a release at DisableWait=true with the annotation set to On the non-blocking points:
|
3cb624f to
70ea242
Compare
Aleksei Sviridkin (lexfrei)
left a comment
There was a problem hiding this comment.
IvanHunters LGTM. The branch that skips an unparseable annotation is pinned now. I changed if err != nil { to if err != nil && false { and TestAppDefHelm_DisableWaitInvalidAnnotationLeavesReleaseAlone went red, with nothing else failing. Dropping the kind label from the selector turns TestAppDefHelm_DisableWaitScopedToKind red, so the kind scope is covered as well.
The base did not move, and the rework is the two tests plus comment, body and subject fixes. The body now puts the hang on kstatus checkGeneration and says why waitStrategy: legacy would still hold an external VM on its LoadBalancer. go test ./internal/controller ./pkg/config passes 56 top-level tests and helm unittest on vm-instance-rd passes 2/2. CI on this head is green, E2E included.
One optional leftover. The first commit's message still says the generation check is "a built-in pre-check in fluxcd/pkg/runtime/cel" that healthCheckExprs cannot override. Since vm-instance sets no healthCheckExprs, the check that fires is kstatus checkGeneration, as the body says now. This repo merges with merge commits, so that sentence stays in history unless you reword it.
|
Could this be backported to We hit #3734 on a 1.6.3 to 1.6.4 upgrade today. Every other release converged, but the VMInstance release failed with Restarting the VM cleared it, and the next retry succeeded. Without this fix on 1.6, though, every future 1.6.x patch that touches the vm-instance chart needs one reboot per customer VM before its release goes Ready. That's hard to schedule on a platform running customer workloads. The PR has no backport label, and v1.6.4 doesn't include it (the live VMInstance HelmRelease has no |
|
Is a backport to release-1.6 planned? v1.6.4 still has the wait on. Field data from a v1.6.x cluster: one vm-instance HelmRelease with Even with a backport, existing releases keep their current wait settings until they are written again through the apps API (#4409). |
What this PR does
A VMInstance is served as a Flux HelmRelease that renders a KubeVirt VirtualMachine. Since v1.5.0 (helm-controller v1.4.3 to v1.5.0) that release is health-assessed by the kstatus poller. vm-instance sets no
healthCheckExprs, so the VirtualMachine goes through kstatus's own rules influxcd/cli-utils(pkg/kstatus/status):checkGenerationreturns InProgress for any object whosestatus.observedGenerationdiffers frommetadata.generation, and past that check aReady=Falsecondition is InProgress too, so a halted VM is never Current even with its generation in sync. KubeVirt keeps a VirtualMachine'sobservedGenerationbehind itsgenerationwhenever aspec.templatechange is pending a restart, the VM is stopped, or a live-updatable change never converges (kubevirt/kubevirt#18700). So a healthy, Running VM is read as permanently InProgress, the upgrade waits its full timeout, and fails withtimeout waiting for: [VirtualMachine/... status: 'InProgress']. On releases still carrying the pre-1.5.0remediation.retries: -1+ rollback spec, the timeout then reverts the previous manifest, dropping just-applied changes such as opened ports.This sets
release.cozystack.io/helm-install-disable-wait: "true"on the vm-instance ApplicationDefinition, so cozystack-api generates the HelmRelease with Install/Upgrade DisableWait. The release settles once the VirtualMachine is applied, and VM health is read off the VirtualMachine itself (status.printableStatus/Ready) and off the WorkloadMonitor the chart already renders, which is whatWorkloadsReadyon the Application reports. This restores the pre-1.5.0 behaviour, under which the VM was never a helm readiness gate. The same mechanism is already used bykubernetes-rdandkubernetes-nodes-rd.On its own that annotation does not reach a running cluster, so two gaps in cozystack-controller are closed with it.
The first is the rollout.
applicationdefinition-controllerhashed each definition's name and spec only, so a change confined to arelease.cozystack.io/*annotation left the hash unmoved and cozystack-api kept serving the configuration it read at its last start-up, which is the only place those annotations are ever read. The hash now covers them too. Only that prefix, because kubectl and Helm rewrite annotations of their own on every apply and hashing those would roll the api Deployment on every reconcile.The second is the releases that already exist.
applicationdefinition-helm-reconcilerbrings chartRef, valuesFrom and labels on existing HelmReleases in line with their definition; it now does the same for the readiness wait. Without it the annotation reaches new instances only, which on an affected cluster is the wrong half: #3734 reports 134 of 136 releases already looping upgrade and timeout, and re-applying every VMInstance by hand was the only way out. The setting is driven in both directions, since cozystack-api rebuilds the whole spec on every write and emitsDisableWait=falseonce the annotation is gone. An absent install or upgrade block already behaves asDisableWait=falseand is materialised only when the definition asks to disable the wait, so a release with nothing to change is not rewritten.Two alternatives were considered. A per-kind
healthCheckExprskeyed onstatus.printableStatuscannot work: setting it moves the VirtualMachine from kstatus to the CEL status evaluator (fluxcd/pkg/runtime/cel), which runs the sameobservedGenerationcheck ahead of the expressions, and anyhealthCheckExprsadded to this kind later is a no-op while the wait is disabled. The typedspec.release.waitStrategy: legacyis the narrower option, since the legacy checker never waits on a custom resource, but it still gates the release on the chart's Service, and withexternal: truethat is a LoadBalancer the legacy checker holds not ready until it has ingress. So an external VM would still wait on IP allocation, whichWorkloadsReadyalready watches. Disable-wait avoids that and is the mechanism this repository already uses for the same class of problem.Tests: a helm-unittest suite (
tests/readiness_gate_test.yaml) guards that the annotation stays on the rendered ApplicationDefinition, and Go unit tests cover both cozystack-controller paths: the hash moves on a release annotation and stands still on an unrelated one, and the wait lands on an existing release, is withdrawn when the annotation goes, does not materialise empty install/upgrade blocks, does not rewrite a release already in sync, leaves a release alone when the annotation does not parse, and does not reach a release of another kind.Note on existing clusters: no manual step. The definition changes on upgrade, the config-hash rolls cozystack-api, and the helm reconciler brings existing VMInstance releases in line. Releases that predate v1.5.0 also carry a
remediation.retries: -1+ rollback spec, which is left alone: with the wait disabled the upgrade no longer times out, so that remediation no longer fires, and any write through the apps API drops it.Closes #3734.
Downstream repositories
Release note
Summary by CodeRabbit