Repository navigation
vm-instance releases never converge: kstatus observedGeneration pre-check gates healthy KubeVirt VMs #3734
Description
Summary
From v1.5.0 the vm-instance HelmRelease is health-assessed by helm-controller's kstatus poller. Its CEL status evaluator (fluxcd/pkg/runtime/cel, status_evaluator.go) returns InProgress for any object whose status.observedGeneration trails metadata.generation, before evaluating anything else. KubeVirt legitimately keeps a VirtualMachine's observedGeneration behind its generation (a spec.template change pending a restart, a stopped VM, or the open live-update bug kubevirt/kubevirt#18700), so a healthy Running VM is read as permanently InProgress and the release never reaches Ready.
Impact
- vm-instance HelmReleases stuck not-Ready with
timeout waiting for: [VirtualMachine/... status: 'InProgress']. - On releases still carrying the pre-1.5.0
remediation.retries: -1+ rollback spec, the timeout triggers a rollback that reverts the previous manifest, dropping just-applied changes (e.g. opened ports). Reported in production on a 1.2.4 to 1.6.0 upgrade (134/136 releases affected, 139/171 VMs with trailing observedGeneration).
Why healthCheckExprs does not solve it
The observedGeneration pre-check runs before the user CEL expressions in the evaluator and cannot be overridden, so a printableStatus/Ready-based healthCheckExprs is a no-op for this failure.
Mitigation (this repo)
Disable the readiness wait for the VMInstance release, restoring the pre-1.5.0 behaviour where the VM was never a helm readiness gate.
Mitigation PR: #3735
Residual / follow-ups
- Existing HelmReleases keep the old rollback spec until their Application is re-written through the apps API; document/automate regeneration.
- Upstream: vm.status.observedGeneration does not converge after live-updatable VM spec changes, breaking the generation sync signal kubevirt/kubevirt#18700 (observedGeneration convergence). The flux pre-check that shadows
healthCheckExprsis arguably a helm-controller limitation worth raising upstream.
Activity
lfinmauritius commentedon Aug 28, 2026
Independent confirmation from a Cozystack v1.6.1 lab (Talos v1.13.6, KubeVirt v1.8.4, 3 combined control-plane/worker nodes, 3 VMInstances), 2026-08-28.
Symptom, measured. Every VMInstance HelmRelease was in the loop
upgrade → kstatus wait → 10 min timeout → failed → retry(75 / 100 / 137 helm revisions on our three machines). Because helm-controller serialises upgrades per release, any change made while an upgrade is waiting (e.g. a disk attach through the apps API) only gets applied once the running one times out: 3–15 min per gesture, while the declarative volume hotplug itself completes in ~30 s once the VirtualMachine is patched.Root cause, confirmed on the code.
status.observedGenerationcomes from the VMI annotationkubevirt.io/vm-generation, whichconditionallyBumpGenerationAnnotationOnVmi(pkg/virt-controller/watch/vm/vm.go, v1.8.4) only bumps when the whole currentspec.templateequals the ControllerRevision the VMI was started from. Any template change not followed by a restart therefore leavesobservedGeneration < generation, and the kstatus pre-check influxcd/pkg/runtime/celreturns InProgress before anyhealthCheckExprsis evaluated. We also verified that template labels (e.g.helm.sh/chart: <digest>) are not the trigger: KubeVirt neither sets RestartRequired for them (syncRestartRequiredcomparesTemplate.Speconly) nor syncs them to the VMI.Mitigation from #3735, applied and measured. With
release.cozystack.io/helm-install-disable-wait: "true"on the vm-instance ApplicationDefinition (v1.6.1 cozystack-api already honours it, same mechanism as kubernetes-rd), after regenerating the HelmReleases: attach of a ready disk to a running VM — HelmReleasedeployed10 s after the API call, VMI volumeReadyat 41 s (was 7 min 20 s); detach — volume removed at 49 s (was 5 min); no failed revisions since; RestartRequired never set; VM health read from the VirtualMachine as expected.Two operational notes for the PR description:
ApplicationDefinitionReconcilercomputescozystack.io/config-hashoverName+Speconly — an annotation-only change to the ApplicationDefinition does not roll cozystack-api. On an existing cluster the annotation had no effect until a manualkubectl -n cozy-system rollout restart deployment/cozystack-api(we waited 6 min to be sure). Either hashing metadata.annotations too, or documenting the restart, would avoid a silent no-op.Happy to share the exact timelines if useful.
ghostrider0470 commentedon Sep 18, 2026
What changes the template on an upgrade that changes nothing: the chart pins
the kube-ovn address into it, from a
lookupthat has nothing to find atinstall time.
packages/apps/vm-instance/templates/vm.yamllooks up the VM's kube-ovnIPobject and, when it exists, writes the address and MAC into
spec.template.metadata.annotations:The
IPobject is created when the VM's pod starts, so the install renderswithout these annotations and the first upgrade after boot adds them. That
makes the first upgrade of every VM a
spec.templatechange, whatever theupgrade was for, which is the case the comment above describes. On v1.6.2 the
template diff of such an upgrade was only these two lines:
and the VirtualMachine then sat at
generation 2, observedGeneration 1, desiredGeneration 2for the whole 10-minute timeout while Running andReady=True.So #3735 fixes the wait, and this is what triggers it. Rendering the same
template on install and upgrade would stop a routine upgrade (a platform chart
bump, a resize, a metadata-only write through the apps API) from being a
template change at all. That could mean leaving the pin out of the template, or
setting it on the VM at a point where install and upgrade agree. Which is right
depends on why the address is pinned: presumably so it survives the pod being
recreated, which is worth keeping.
One thing that makes it fire more often than it would: cozystack-api applies
spec defaults on read but not on create, so the first write of any kind to a
VMInstance, even an annotation, upgrades its release. Reported as #4342.x-itm commentedon Sep 19, 2026
The
lookupmakes the rendered VM spec depend on live cluster state, so it creates a second problem beyond the kstatus gate: an otherwise no-op upgrade can become aspec.templatemutation after the IP object appears. Disabling the Helm readiness wait addresses the immediate release deadlock, but it would also be worth removing or stabilising this lookup-driven template change. In particular, avoid rendering empty IP/MAC annotations and comparehelm get manifestbefore/after the first boot to identify every generated template diff. CEL health expressions cannot solve this while the observedGeneration pre-check runs first.fix(vm-instance): disable helm readiness wait on the VMInstance relea…