VM Restore Admission Validation#
Overview#
VMRestoreAdmitter is the validating webhook for VirtualMachineRestore objects. It lives in pkg/storage/admitters/vmrestore.go and runs on CREATE and UPDATE operations. On create, it validates the target VM, checks for in-progress restores to the same target, and validates any spec.patches supplied on the restore object.
Validation Checks#
The Admit method dispatches to several validators on CREATE:
| Validator | What it checks |
|---|---|
validateTargetVM | Target VM exists (or snapshot is found), and cross-VM restore isn't attempted when backend storage (TPM/EFI) is present |
validatePatches | Each patch in spec.patches operates only on /spec/, /metadata/labels/, or /metadata/annotations/ paths |
validateVolumeOverrides | Every volumeRestoreOverrides entry has a volumeName and at least one override field |
validateVolumeRestorePolicy | spec.volumeRestorePolicy is one of the recognized enum values |
validateVolumeOwnershipPolicy | spec.volumeOwnershipPolicy is one of the recognized enum values |
| In-progress check | No other restore to the same target is already in progress (via informer cache) |
On UPDATE, the spec is treated as immutable .
The validatePatches Bug#
What the code does (incorrectly)#
validatePatches parses each patch string using string splitting, not JSON parsing:
- Strips leading/trailing
{}from the patch string, then splits on,to get key-value pairs. - Splits each pair on
:and asserts exactly two parts. - Checks whether the part after
"path"starts with"/spec/","/metadata/labels/", or"/metadata/annotations/".
This approach is documented in the code's own inline comment .
Why it fails for MAC address patches#
The clone controller's addMacAddressPatches generates RFC 6902 replace patches for each network interface:
{"op":"replace","path":"/spec/template/spec/domain/devices/interfaces/0/macAddress","value":"be:ad:00:00:be:04"}
When the admitter splits on ,, the "value":"be:ad:00:00:be:04" fragment becomes one token. When it then splits that token on :, it gets six parts instead of two, triggering the error :
patch format is not valid - one ":" expected in a single key-value json patch: "value": "be:ad:00:00:be:04"
The same failure affects any patch whose value contains a colon or comma β firmware serial strings, node selectors, tolerations, or any JSON object/array value .
Mismatch with the actual patch consumer#
The actual patch application in pkg/storage/snapshot/restore.go uses jsonpatch.DecodePatch from gopkg.in/evanphx/json-patch.v4, which is a spec-correct RFC 6902 parser. The webhook validator is therefore strictly more restrictive than the runtime β patches that reach the restore controller would succeed, but they are blocked at admission .
Secondary issue: missing "path" key is silently accepted#
Because the validator only emits a cause when it encounters a token whose key is "path", a patch that has no "path" key at all passes validation without error β a logic inversion .
Clone Controller Integration#
The clone controller creates VirtualMachineRestore objects via generateRestore in pkg/virt-controller/watch/clone/util.go, passing in patches generated by generatePatches. MAC address patches are serialized to JSON strings via json.Marshal in generateStringPatchOperations. These correctly-formed JSON strings then fail the admitter's naive string-split validation.
Fix Direction#
Replace the string-splitting logic in validatePatches with proper JSON unmarshaling: unmarshal each patch string into a struct with Op, Path, and Value fields, then validate the decoded Path field. This aligns the webhook with how patchVM actually consumes the patches. The issue is tracked at kubevirt/kubevirt#18617 .
Key Files#
| File | Role |
|---|---|
pkg/storage/admitters/vmrestore.go | VMRestore admission webhook β contains the buggy validatePatches |
pkg/virt-controller/watch/clone/vm-target.go | Clone controller β generates MAC address and other patches |
pkg/virt-controller/watch/clone/util.go | Clone utilities β assembles the VirtualMachineRestore object |
pkg/storage/snapshot/restore.go | Restore controller β applies patches via jsonpatch.DecodePatch |
pkg/apimachinery/patch/ | Patch builder used by clone controller |