Virt-Operator Deployment Reconciliation#
virt-operator determines whether a KubeVirt installation needs to be created or updated by computing a deployment ID — a SHA-1 hash over all meaningful configuration properties — and comparing it against the state stored on the KubeVirt CR. Every reconcile loop uses this ID to decide whether to trigger a new install-strategy job, update component deployments/DaemonSets, or declare the system converged.
KubeVirtDeploymentConfig and the Deployment ID#
The central data structure is KubeVirtDeploymentConfig, which holds:
Registry,ImagePrefix,KubeVirtVersion,Namespace— top-level deployment coordinatesComponentImages— an embedded struct with per-component image references (operator, API, controller, handler, launcher, export proxy/server, sidecar shim, etc.)AdditionalProperties— amap[string]stringfor values sourced fromKubeVirtSpec(pull policy, pull secrets, product metadata, feature-gate flags, etc.)PassthroughEnvVars— extra env vars forwarded from the operator pod
The ID is computed in generateInstallStrategyID(), which hashes the output of getStringFromFields() using SHA-1 (used as a non-cryptographic fingerprint, not for security). After construction, the ID is written to KubeVirt.Status.TargetDeploymentID via SetTargetDeploymentConfig().
How the ID Drives Reconciliation#
Three mechanisms consume the deployment ID:
-
Upgrade detection —
isUpdating()inpkg/virt-operator/kubevirt.goreturnstruewhenObservedDeploymentID != TargetDeploymentID. This comparison gates the entire update flow. -
Install-strategy-identifier annotation — every managed Kubernetes object (Deployments, DaemonSets, Pods) gets the ID injected as the
v1.InstallStrategyIdentifierAnnotationannotation viainjectOperatorMetadata(). Reconciliation skips objects whose annotation already matches the target ID . -
Pod up-to-date check —
PodIsUpToDate()returnstrueonly if the pod'sInstallStrategyIdentifierAnnotation,InstallStrategyVersionAnnotation, andInstallStrategyRegistryAnnotationall match the current target. Stale pods (mismatched ID) are treated as needing replacement.
Reflection-Based String Serialization#
getStringFromFields() delegates to fieldsToString(), which walks the struct via reflect.Value, appending each field's name and value:
| Kind | Behavior |
|---|---|
reflect.String | Appends field.String() |
reflect.Struct | Recursively calls fieldsToString() on the nested struct |
reflect.Map | Sorts keys, then appends each key+value pair |
| Other | Panics (defensive; prevents silent hash omissions) |
Key detail: AdditionalProperties keys are sorted before serialization to ensure map iteration order doesn't produce different IDs for the same configuration. An empty ImagePrefixKey entry is also stripped before hashing to normalize "" and absent keys to the same ID .
Bug: reflect.Value.String() on Struct Fields (Fixed in PR #16791)#
Before PR #16791, getStringFromFields() had no reflect.Struct branch. For any non-map field, it called v.Field(i).String() directly. In Go, calling .String() on a reflect.Value whose kind is reflect.Struct does not return the struct's contents — it returns a constant type-descriptor string like <util.ComponentImages Value> .
This became a critical bug after commit e80193d263 moved the individual image fields from flat fields on KubeVirtDeploymentConfig into the embedded ComponentImages struct. All 11 image fields (VirtOperatorImage, VirtApiImage, VirtControllerImage, VirtHandlerImage, VirtLauncherImage, VirtExportProxyImage, VirtExportServerImage, VirtSynchronizationControllerImage, GsImage, PrHelperImage, SidecarShimImage) were silently omitted from the hash. Changing any VIRT_*_IMAGE env var on the operator did not produce a new deployment ID, so the operator reused stale install-strategy ConfigMaps and syncDeployment() detected no diff, skipping the rollout entirely .
The fix added the reflect.Struct case in fieldsToString() with a recursive call, and added a default: panic(...) to catch future omissions .
Related Bug: CustomizeComponents and the Deployment ID (PR #18133)#
A similar problem exists with spec.customizeComponents: getKVMapFromSpec() calls v.Field(i).String() on each KubeVirtSpec field. The CustomizeComponents field is a struct, so it too produces a constant type-descriptor. Changing spec.customizeComponents alone does not alter the deployment ID, meaning PodIsUpToDate() will not flag existing pods as stale and the canary rollout phases are bypassed entirely .
PR #18133 worked around this in the canary upgrade test by switching the test trigger to spec.productVersion — a plain string field that does flow through AdditionalProperties and does mutate the hash. This is a test-level workaround; the production gap (CustomizeComponents changes not triggering a rollout) remains a known issue flagged for follow-up .
Key Files#
| File | Purpose |
|---|---|
pkg/virt-operator/util/config.go | KubeVirtDeploymentConfig, ID generation, fieldsToString() |
pkg/virt-operator/kubevirt.go | isUpdating(), syncInstallation() — reconcile entry point |
pkg/virt-operator/util/readycheck.go | PodIsUpToDate(), DaemonSetIsReady(), DeploymentIsReady() |
pkg/virt-operator/resource/apply/reconcile.go | injectOperatorMetadata(), objectMatchesVersion() |