Repository: kubevirt/kubevirt
Pull request #14682: instancetype: Only return revision of currently referenced object
Author: login: lyarwood
Labels: name: size/L
name: release-note-none
description: Denotes a PR that doesn't merit a release note.
name: lgtm
description: Indicates that a PR is ready to be merged.
name: approved
description: Indicates a PR has been approved by an approver from all required OWNERS files.
name: dco-signoff: yes
description: Indicates the PR's author has DCO signed all their commits.
name: area/instancetype
name: sig/compute
Created: 2025-05-09T18:07:13Z | Merged: 2025-05-14T06:43:59Z

<!--  Thanks for sending a pull request!  Here are some tips for you:
1. Consider creating this PR as draft: https://github.com/kubevirt/kubevirt/blob/main/CONTRIBUTING.md#consider-opening-your-pull-request-as-draft
2. Follow the instructions for writing a release note from k8s: https://git.k8s.io/community/contributors/guide/release-notes.md
-->

### What this PR does
We currently check that a given VirtualMachine meets any referenced
preference requirements within the admission webhook.

With the recent move of references to captured ControllerRevisions of
instance types and preferences into the status of the VirtualMachine we
no longer populate `spec.{instancetype,preference}.revisionName`. This
data is now held under
`status.{instancetypeRef,preferenceRef}.controllerRevisionRef` and is
maintained by the core VirtualMachine sync loop.

This causes an issue when checking any referenced preference
requirements within the admission webhook as these references will still
point to ControllerRevisions of different objects when switching between
instance types or preferences during a live update etc.

This change fixes this by ensuring the ControllerRevisions referenced by
the status of the VirtualMachine contain the instance type or preference
currently referenced from the spec of the VirtualMachine.

<!-- (optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*: -->
Fixes #14595

### Why we need it and why it was done in this way
The following tradeoffs were made:

The following alternatives were considered:

Links to places where the discussion took place: <!-- optional: slack, other GH issue, mailinglist, ... -->

### Special notes for your reviewer

Numerous follow ups are left for later PRs here to allow this bugfix to be backportable. These include:

* Moving preference resource requirement checks to the core VM sync loop
* Merging the seperate `revisionFinder` implementations for instance types and preferences into a single implementation like the `controllerRevisionFinder`
* Adding support to the instance type builder for `WithMemoryRequirement()`
* Removing duplication from `spec_test.go`
* Linting `tests/hotplug`

### Checklist

This checklist is not enforcing, but it's a reminder of items that could be relevant to every PR.
Approvers are expected to review this list.

- [ ] Design: A [design document](https://github.com/kubevirt/community/tree/main/design-proposals) was considered and is present (link) or not required
- [ ] PR: The PR description is expressive enough and will help future contributors
- [ ] Code: [Write code that humans can understand](https://en.wikiquote.org/wiki/Martin_Fowler#code-for-humans) and [Keep it simple](https://en.wikipedia.org/wiki/KISS_principle)
- [ ] Refactor: You have [left the code cleaner than you found it (Boy Scout Rule)](https://learning.oreilly.com/library/view/97-things-every/9780596809515/ch08.html)
- [ ] Upgrade: Impact of this change on upgrade flows was considered and addressed if required
- [ ] Testing: New code requires [new unit tests](https://github.com/kubevirt/kubevirt/blob/main/docs/reviewer-guide.md#when-is-a-pr-good-enough). New features and bug fixes require at least on e2e test
- [ ] Documentation: A [user-guide update](https://github.com/kubevirt/user-guide/) was considered and is present (link) or not required. You want a user-guide update if it's a user facing feature / API change.
- [ ] Community: Announcement to [kubevirt-dev](https://groups.google.com/g/kubevirt-dev/) was considered

### Release note
<!--  Write your release note:
1. Enter your extended release note in the below block. If the PR requires additional action from users switching to the new release, include the string "action required".
2. If no release note is required, just write "NONE".
-->
```release-note
NONE
```



Linked issue #14595: preference requirements not being checked with initial update of VirtualMachine
/area instancetype

**What happened**:

Preferences can define resource requirements for a given workload, for example with Fedora:

```
$ ./kubevirtci/cluster-up/kubectl.sh get virtualmachineclusterpreference/fedora -o json | jq .spec.requirements
selecting podman as container runtime
{
  "cpu": {
    "guest": 1
  },
  "memory": {
    "guest": "2Gi"
  }
}
```

The requirements are currently correctly checked on creation:

```
$ virtctl create vm \
  --instancetype u1.micro \
  --preference fedora \
  --volume-containerdisk name:fedora,src:quay.io/containerdisks/fedora:latest \
  --name fedora | ./kubevirtci/cluster-up/kubectl.sh apply -f -
selecting podman as container runtime
The request is invalid: spec.instancetype: failure checking preference requirements: insufficient Memory resources of 1Gi provided by instance type, preference requires 2Gi
```

However on the first update these checks are skipped before apparently being preformed on follow up updates:

```
$ virtctl create vm \
  --instancetype u1.small \
  --preference fedora \
  --volume-containerdisk name:fedora,src:quay.io/containerdisks/fedora:latest \
  --name fedora | ./kubevirtci/cluster-up/kubectl.sh apply -f -
selecting podman as container runtime
virtualmachine.kubevirt.io/fedora created

$ ./kubevirtci/cluster-up/kubectl.sh patch vm/fedora --type=merge -p '{"spec":{"instancetype":{"name": "u1.micro"}}}'
selecting podman as container runtime
virtualmachine.kubevirt.io/fedora patched

$ ./kubevirtci/cluster-up/kubectl.sh patch vm/fedora --type=merge -p '{"spec":{"instancetype":{"name": "u1.small"}}}'
selecting podman as container runtime
The request is invalid: spec.instancetype: failure checking preference requirements: insufficient Memory resources of 1Gi provided by instance type, preference requires 2Gi

```

These checks should be migrated to the core sync loop of the VirtualMachine anyway to support eventually consistency but for now we need to understand how this is happening.


**What you expected to happen**:
A clear and concise description of what you expected to happen.

**How to reproduce it (as minimally and precisely as possible)**:
Steps to reproduce the behavior.

**Additional context**:
Add any other context about the problem here.

**Environment**:
- KubeVirt version (use `virtctl version`): N/A
- Kubernetes version (use `kubectl version`): N/A
- VM or VMI specifications: N/A
- Cloud provider or hardware configuration: N/A
- OS (e.g. from /etc/os-release): N/A
- Kernel (e.g. `uname -a`): N/A
- Install tools: N/A
- Others: N/A

Linked issue comments: user: login: dosubot[bot]
created_at: 2025-04-28T11:52:19Z
body: <!-- Answer -->
I found a similar closed issue that might be relevant to your situation: [VirtualMachine/VirtualMachineInstance defaults not applied before preference resource requirements checks are completed](https://github.com/kubevirt/kubevirt/issues/10047). This issue was resolved by a pull request titled [instancetype: Apply defaults before checking preference requirements](https://github.com/kubevirt/kubevirt/pull/10054) on July 12, 2023 <sup>[[1]](https://github.com/kubevirt/kubevirt/issues/10047)</sup>.

In your case, it seems that the preference requirements checks are being skipped during the initial update of a VirtualMachine. This behavior might be related to how user-defined values are handled in the VirtualMachineInstance specification. Specifically, the logic ensures that user-defined values for certain settings are not overwritten by preference specifications, which could lead to the checks being skipped initially <sup>[[2]](https://github.com/kubevirt/kubevirt/blob/main/pkg/instancetype/preference/apply/firmware_test.go)</sup>.

To address this, you might want to investigate how the preference requirements checks are integrated into the core sync loop of the VirtualMachine. The `Sync` method in the `controller` struct is responsible for ensuring that any referenced resources exist and for handling instancetype and preference specifications. This method could be a key area to explore for understanding and potentially resolving the issue <sup>[[3]](https://github.com/kubevirt/kubevirt/blob/main/pkg/instancetype/controller/vm/controller.go)</sup>.


<!-- Dosu Comment Footer -->
*To reply, just mention [@dosu](https://go.dosu.dev/dosubot?utm_source=github&utm_medium=bot-comment&utm_campaign=github-comment-footer-20260415&utm_content=reply-with-mention&utm_term=kubevirt%2Fkubevirt).*

---

Docs are dead. Just use [Dosu](https://dosu.dev?utm_source=github&utm_medium=bot-comment&utm_campaign=github-comment-footer-20260415&utm_content=static-docs-tagline&utm_term=kubevirt%2Fkubevirt).

[![Leave Feedback](https://img.shields.io/badge/Leave%20Feedback-555555?style=flat)](https://app.dosu.dev/response-feedback?utm_source=github&utm_medium=bot-comment&utm_campaign=github-comment-footer-20260415&utm_content=static-docs-feedback&utm_term=kubevirt%2Fkubevirt&message_id=e216a90c-89e6-432b-8a40-066b92f92bef) [![Ask Dosu about kubevirt](https://img.shields.io/badge/Ask%20Dosu%20about%20kubevirt-2f7b3f?style=flat&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iODYiIGhlaWdodD0iODkiIHZpZXdCb3g9IjAgMCA4NiA4OSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNNS4yOTIzNiAxMi43OTI4TDE3Ljc1OTMgNi42ODE4OFY3Mi41NjY3TDUuMjkyMzYgODQuMDYxOFYxMi43OTI4WiIgZmlsbD0iI0I0QkI5MSIvPjxwYXRoIGQ9Ik0xOC4yNTc1IDczLjExOTZMNTkuMTMyOSA3Mi43NDhMNTEuNzAxMSA4Mi40MDk1TDI5LjAzMzggODYuMjkxTDYuMjM5NjIgODUuMTU1NEwxOC4yNTc1IDczLjExOTZaIiBmaWxsPSIjNzc4NTYxIi8%2BPHBhdGggZD0iTTE3LjQ5MTYgMy43MzYzM0wzLjU4NTU3IDEyLjcwOTlWODMuNTc5MkMzLjU4NTU3IDg0Ljc1NDIgNC45ODU2MyA4NS4zNjUyIDUuODQ3MDUgODQuNTY2TDE5LjYyOTYgNzEuNzgwMSIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLXdpZHRoPSI2LjQyODQ0IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz48bWFzayBpZD0iZG9zdS1kLWN1dG91dCIgZmlsbD0id2hpdGUiPjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNDAuNzA0IDAuNTE4MDY2SDE3LjA0MzlWNzYuMjIyMUg0MC43MDRINDIuNTgwNUg0Ny44MDEzQzY4LjcwNjQgNzYuMjIyMSA4NS42NTMzIDU5LjI3NTIgODUuNjUzMyAzOC4zNzAxQzg1LjY1MzMgMTcuNDY1IDY4LjcwNjMgMC41MTgwNjYgNDcuODAxMyAwLjUxODA2Nkg0Mi41ODA1SDQwLjcwNFoiLz48L21hc2s%2BPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik00MC43MDQgMC41MTgwNjZIMTcuMDQzOVY3Ni4yMjIxSDQwLjcwNEg0Mi41ODA1SDQ3LjgwMTNDNjguNzA2NCA3Ni4yMjIxIDg1LjY1MzMgNTkuMjc1MiA4NS42NTMzIDM4LjM3MDFDODUuNjUzMyAxNy40NjUgNjguNzA2MyAwLjUxODA2NiA0Ny44MDEzIDAuNTE4MDY2SDQyLjU4MDVINDAuNzA0WiIgZmlsbD0iI0YzRjZGMSIvPjxwYXRoIGQ9Ik0xNy4wNDM5IDAuNTE4MDY2Vi02LjU3OTE5SDkuOTQ2NjlWMC41MTgwNjZIMTcuMDQzOVpNMTcuMDQzOSA3Ni4yMjIxSDkuOTQ2NjlWODMuMzE5NEgxNy4wNDM5Vjc2LjIyMjFaTTE3LjA0MzkgNy42MTUzMkg0MC43MDRWLTYuNTc5MTlIMTcuMDQzOVY3LjYxNTMyWk0yNC4xNDEyIDc2LjIyMjFWMC41MTgwNjZIOS45NDY2OVY3Ni4yMjIxSDI0LjE0MTJaTTQwLjcwNCA2OS4xMjQ5SDE3LjA0MzlWODMuMzE5NEg0MC43MDRWNjkuMTI0OVpNNDIuNTgwNSA2OS4xMjQ5SDQwLjcwNFY4My4zMTk0SDQyLjU4MDVWNjkuMTI0OVpNNDcuODAxMyA2OS4xMjQ5SDQyLjU4MDVWODMuMzE5NEg0Ny44MDEzVjY5LjEyNDlaTTc4LjU1NiAzOC4zNzAxQzc4LjU1NiA1NS4zNTU1IDY0Ljc4NjcgNjkuMTI0OSA0Ny44MDEzIDY5LjEyNDlWODMuMzE5NEM3Mi42MjYxIDgzLjMxOTQgOTIuNzUwNSA2My4xOTQ5IDkyLjc1MDUgMzguMzcwMUg3OC41NTZaTTQ3LjgwMTMgNy42MTUzMkM2NC43ODY2IDcuNjE1MzIgNzguNTU2IDIxLjM4NDcgNzguNTU2IDM4LjM3MDFIOTIuNzUwNUM5Mi43NTA1IDEzLjU0NTMgNzIuNjI2IC02LjU3OTE5IDQ3LjgwMTMgLTYuNTc5MTlWNy42MTUzMlpNNDIuNTgwNSA3LjYxNTMySDQ3LjgwMTNWLTYuNTc5MTlINDIuNTgwNVY3LjYxNTMyWk00MC43MDQgNy42MTUzMkg0Mi41ODA1Vi02LjU3OTE5SDQwLjcwNFY3LjYxNTMyWiIgZmlsbD0iYmxhY2siIG1hc2s9InVybCgjZG9zdS1kLWN1dG91dCkiLz48cGF0aCBkPSJNNjguOTIxNSAzNi4wMTM1QzY4LjkyMTUgMzYuMDEzNSA2NS43MzY5IDQ5LjQ3MzggNTEuNDIzMSA0OS40NzM4QzM3LjEwOTMgNDkuNDczOCAzMi41Nzg3IDM3LjM1OTYgMzIuNTc4NyAzNi4wMTM1IiBzdHJva2U9ImJsYWNrIiBzdHJva2Utd2lkdGg9IjcuNjkxNjEiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxwYXRoIGQ9Ik0wLjM0ODYzMyA4NS40OTQ2QzAuMzQ4NjMzIDg1LjQ5NDYgMjkuNDg1NiA4NS44MzA5IDM0LjgwOSA4NS42OThDNDQuODMzNyA4NS40NDc3IDUxLjI4NzIgODQuNDAyIDU3LjUyNjkgNzguOTcyNEM2Mi44MTI5IDc0LjM3MjcgNzUuMTM0MiA1OS42ODM2IDc1LjEzNDIgNTkuNjgzNiIgc3Ryb2tlPSJibGFjayIgc3Ryb2tlLXdpZHRoPSI2LjE2NDgyIi8%2BPC9zdmc%2B)](https://github.dosu.com/kubevirt/kubevirt?utm_source=github&utm_medium=bot-comment&utm_campaign=github-comment-footer-20260415&utm_content=static-docs-ask-repo&utm_term=kubevirt%2Fkubevirt) [![Share Dosu with your team](https://img.shields.io/badge/Share%20Dosu%20with%20your%20team-1f6feb?style=flat)](https://app.dosu.dev/signup?referrer=openSource&source=github-footer&utm_source=github&utm_medium=bot-comment&utm_campaign=github-comment-footer-20260415&utm_content=static-docs-share-team&utm_term=kubevirt%2Fkubevirt)
author_association: NONE

Diff:
diff --git a/pkg/instancetype/find/revision.go b/pkg/instancetype/find/revision.go
index f8487506e1ec..f456763ac2a7 100644
--- a/pkg/instancetype/find/revision.go
+++ b/pkg/instancetype/find/revision.go
@@ -24,6 +24,7 @@ import (
 	"k8s.io/client-go/tools/cache"
 
 	virtv1 "kubevirt.io/api/core/v1"
+	instancetypeapi "kubevirt.io/api/instancetype"
 	"kubevirt.io/client-go/kubecli"
 )
 
@@ -47,10 +48,17 @@ func (f *revisionFinder) Find(vm *virtv1.VirtualMachine) (*appsv1.ControllerRevi
 	}
 	ref := vm.Status.InstancetypeRef
 	if ref != nil && ref.ControllerRevisionRef != nil && ref.ControllerRevisionRef.Name != "" {
-		return f.controllerRevisionFinder.Find(types.NamespacedName{
+		cr, err := f.controllerRevisionFinder.Find(types.NamespacedName{
 			Namespace: vm.Namespace,
 			Name:      ref.ControllerRevisionRef.Name,
 		})
+		if err != nil {
+			return nil, err
+		}
+		// Only return the found CR if it is for the referenced instance type
+		if label, ok := cr.Labels[instancetypeapi.ControllerRevisionObjectNameLabel]; ok && label == vm.Spec.Instancetype.Name {
+			return cr, nil
+		}
 	}
 	return nil, nil
 }
diff --git a/pkg/instancetype/find/spec_test.go b/pkg/instancetype/find/spec_test.go
index e9176a64c361..3791b6c343f9 100644
--- a/pkg/instancetype/find/spec_test.go
+++ b/pkg/instancetype/find/spec_test.go
@@ -36,6 +36,7 @@ import (
 var _ = Describe("Instance Type SpecFinder", func() {
 	const (
 		nonExistingResourceName = "non-existing-resource"
+		storedName              = "stored"
 	)
 
 	type instancetypeSpecFinder interface {
@@ -295,6 +296,36 @@ var _ = Describe("Instance Type SpecFinder", func() {
 			Expect(err).ToNot(HaveOccurred())
 			Expect(foundInstancetypeSpec).To(HaveValue(Equal(clusterInstancetype.Spec)))
 		})
+
+		It("find returns only referenced object - bug #14595", func() {
+			// Make a slightly altered copy of the object already present in the client and store it in a CR
+			stored := clusterInstancetype.DeepCopy()
+			stored.ObjectMeta.Name = storedName
+			stored.Spec.CPU.Guest = uint32(99)
+
+			controllerRevision, err := revision.CreateControllerRevision(vm, stored)
+			Expect(err).ToNot(HaveOccurred())
+
+			_, err = virtClient.AppsV1().ControllerRevisions(vm.Namespace).Create(
+				context.Background(), controllerRevision, metav1.CreateOptions{})
+			Expect(err).ToNot(HaveOccurred())
+
+			// Assert that the spec points to the original clusterInstancetype
+			Expect(vm.Spec.Instancetype.Name).To(Equal(clusterInstancetype.Name))
+
+			// Reference this stored version from the VM status
+			vm.Status.InstancetypeRef = &v1.InstancetypeStatusRef{
+				Name: stored.Name,
+				Kind: stored.Kind,
+				ControllerRevisionRef: &v1.ControllerRevisionRef{
+					Name: controllerRevision.Name,
+				},
+			}
+
+			foundInstancetypeSpec, err := finder.Find(vm)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(foundInstancetypeSpec).To(HaveValue(Equal(clusterInstancetype.Spec)))
+		})
 	})
 
 	Context("Using namespaced Instancetype", func() {
@@ -479,5 +510,35 @@ var _ = Describe("Instance Type SpecFinder", func() {
 			Expect(err).ToNot(HaveOccurred())
 			Expect(foundInstancetypeSpec).To(HaveValue(Equal(fakeInstancetype.Spec)))
 		})
+
+		It("find returns only referenced object - bug #14595", func() {
+			// Make a slightly altered copy of the object already present in the client and store it in a CR
+			stored := fakeInstancetype.DeepCopy()
+			stored.ObjectMeta.Name = storedName
+			stored.Spec.CPU.Guest = uint32(99)
+
+			controllerRevision, err := revision.CreateControllerRevision(vm, stored)
+			Expect(err).ToNot(HaveOccurred())
+
+			_, err = virtClient.AppsV1().ControllerRevisions(vm.Namespace).Create(
+				context.Background(), controllerRevision, metav1.CreateOptions{})
+			Expect(err).ToNot(HaveOccurred())
+
+			// Assert that the spec points to the original clusterInstancetype
+			Expect(vm.Spec.Instancetype.Name).To(Equal(fakeInstancetype.Name))
+
+			// Reference this stored version from the VM status
+			vm.Status.InstancetypeRef = &v1.InstancetypeStatusRef{
+				Name: stored.Name,
+				Kind: stored.Kind,
+				ControllerRevisionRef: &v1.ControllerRevisionRef{
+					Name: controllerRevision.Name,
+				},
+			}
+
+			foundInstancetypeSpec, err := finder.Find(vm)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(foundInstancetypeSpec).To(HaveValue(Equal(fakeInstancetype.Spec)))
+		})
 	})
 })
diff --git a/pkg/instancetype/preference/find/BUILD.bazel b/pkg/instancetype/preference/find/BUILD.bazel
index 66ba38b7c6dd..4b92270b3601 100644
--- a/pkg/instancetype/preference/find/BUILD.bazel
+++ b/pkg/instancetype/preference/find/BUILD.bazel
@@ -34,6 +34,7 @@ go_test(
         ":go_default_library",
         "//pkg/instancetype/revision:go_default_library",
         "//pkg/libvmi:go_default_library",
+        "//pkg/pointer:go_default_library",
         "//pkg/testutils:go_default_library",
         "//staging/src/kubevirt.io/api/core/v1:go_default_library",
         "//staging/src/kubevirt.io/api/instancetype:go_default_library",
diff --git a/pkg/instancetype/preference/find/revision.go b/pkg/instancetype/preference/find/revision.go
index 36f108008929..5cc772d071d5 100644
--- a/pkg/instancetype/preference/find/revision.go
+++ b/pkg/instancetype/preference/find/revision.go
@@ -24,7 +24,7 @@ import (
 	"k8s.io/client-go/tools/cache"
 
 	virtv1 "kubevirt.io/api/core/v1"
-
+	instancetypeapi "kubevirt.io/api/instancetype"
 	"kubevirt.io/client-go/kubecli"
 
 	"kubevirt.io/kubevirt/pkg/instancetype/find"
@@ -54,10 +54,17 @@ func (f *revisionFinder) FindPreference(vm *virtv1.VirtualMachine) (*appsv1.Cont
 	}
 	ref := vm.Status.PreferenceRef
 	if ref != nil && ref.ControllerRevisionRef != nil && ref.ControllerRevisionRef.Name != "" {
-		return f.controllerRevisionFinder.Find(types.NamespacedName{
+		cr, err := f.controllerRevisionFinder.Find(types.NamespacedName{
 			Namespace: vm.Namespace,
 			Name:      ref.ControllerRevisionRef.Name,
 		})
+		if err != nil {
+			return nil, err
+		}
+		// Only return the found CR if it is for the referenced instance type
+		if label, ok := cr.Labels[instancetypeapi.ControllerRevisionObjectNameLabel]; ok && label == vm.Spec.Preference.Name {
+			return cr, nil
+		}
 	}
 	return nil, nil
 }
diff --git a/pkg/instancetype/preference/find/spec_test.go b/pkg/instancetype/preference/find/spec_test.go
index f5a7aa45ba43..0672212448df 100644
--- a/pkg/instancetype/preference/find/spec_test.go
+++ b/pkg/instancetype/preference/find/spec_test.go
@@ -25,12 +25,14 @@ import (
 	"kubevirt.io/kubevirt/pkg/instancetype/preference/find"
 	"kubevirt.io/kubevirt/pkg/instancetype/revision"
 	"kubevirt.io/kubevirt/pkg/libvmi"
+	"kubevirt.io/kubevirt/pkg/pointer"
 	"kubevirt.io/kubevirt/pkg/testutils"
 )
 
 var _ = Describe("Preference SpecFinder", func() {
 	const (
 		nonExistingResourceName = "non-existing-resource"
+		storedName              = "stored"
 	)
 
 	type preferenceSpecFinder interface {
@@ -234,6 +236,36 @@ var _ = Describe("Preference SpecFinder", func() {
 			_, err := finder.FindPreference(vm)
 			Expect(err).To(MatchError(errors.IsNotFound, "IsNotFound"))
 		})
+
+		It("find returns only referenced object - bug #14595", func() {
+			// Make a slightly altered copy of the object already present in the client and store it in a CR
+			stored := clusterPreference.DeepCopy()
+			stored.ObjectMeta.Name = storedName
+			stored.Spec.CPU.PreferredCPUTopology = pointer.P(v1beta1.Threads)
+
+			controllerRevision, err := revision.CreateControllerRevision(vm, stored)
+			Expect(err).ToNot(HaveOccurred())
+
+			_, err = virtClient.AppsV1().ControllerRevisions(vm.Namespace).Create(
+				context.Background(), controllerRevision, metav1.CreateOptions{})
+			Expect(err).ToNot(HaveOccurred())
+
+			// Assert that the spec points to the original clusterInstancetype
+			Expect(vm.Spec.Preference.Name).To(Equal(clusterPreference.Name))
+
+			// Reference this stored version from the VM status
+			vm.Status.PreferenceRef = &v1.InstancetypeStatusRef{
+				Name: stored.Name,
+				Kind: stored.Kind,
+				ControllerRevisionRef: &v1.ControllerRevisionRef{
+					Name: controllerRevision.Name,
+				},
+			}
+
+			foundPreferenceSpec, err := finder.FindPreference(vm)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(foundPreferenceSpec).To(HaveValue(Equal(clusterPreference.Spec)))
+		})
 	})
 
 	Context("Using namespaced Preference", func() {
@@ -372,5 +404,35 @@ var _ = Describe("Preference SpecFinder", func() {
 			_, err := finder.FindPreference(vm)
 			Expect(err).To(MatchError(errors.IsNotFound, "IsNotFound"))
 		})
+
+		It("find returns only referenced object - bug #14595", func() {
+			// Make a slightly altered copy of the object already present in the client and store it in a CR
+			stored := preference.DeepCopy()
+			stored.ObjectMeta.Name = storedName
+			stored.Spec.CPU.PreferredCPUTopology = pointer.P(v1beta1.Threads)
+
+			controllerRevision, err := revision.CreateControllerRevision(vm, stored)
+			Expect(err).ToNot(HaveOccurred())
+
+			_, err = virtClient.AppsV1().ControllerRevisions(vm.Namespace).Create(
+				context.Background(), controllerRevision, metav1.CreateOptions{})
+			Expect(err).ToNot(HaveOccurred())
+
+			// Assert that the spec points to the original clusterInstancetype
+			Expect(vm.Spec.Preference.Name).To(Equal(preference.Name))
+
+			// Reference this stored version from the VM status
+			vm.Status.PreferenceRef = &v1.InstancetypeStatusRef{
+				Name: stored.Name,
+				Kind: stored.Kind,
+				ControllerRevisionRef: &v1.ControllerRevisionRef{
+					Name: controllerRevision.Name,
+				},
+			}
+
+			foundPreferenceSpec, err := finder.FindPreference(vm)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(foundPreferenceSpec).To(HaveValue(Equal(preference.Spec)))
+		})
 	})
 })
diff --git a/tests/hotplug/BUILD.bazel b/tests/hotplug/BUILD.bazel
index f4736af1c44f..78512786a899 100644
--- a/tests/hotplug/BUILD.bazel
+++ b/tests/hotplug/BUILD.bazel
@@ -28,6 +28,7 @@ go_library(
         "//tests/framework/kubevirt:go_default_library",
         "//tests/framework/matcher:go_default_library",
         "//tests/libdomain:go_default_library",
+        "//tests/libinstancetype/builder:go_default_library",
         "//tests/libkubevirt:go_default_library",
         "//tests/libkubevirt/config:go_default_library",
         "//tests/libmigration:go_default_library",
diff --git a/tests/hotplug/instancetype.go b/tests/hotplug/instancetype.go
index b7bfaa0a9117..0fbc6bb7752a 100644
--- a/tests/hotplug/instancetype.go
+++ b/tests/hotplug/instancetype.go
@@ -6,6 +6,7 @@ import (
 
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
+	gomegatypes "github.com/onsi/gomega/types"
 
 	k8sv1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/resource"
@@ -23,6 +24,7 @@ import (
 	"kubevirt.io/kubevirt/tests/decorators"
 	"kubevirt.io/kubevirt/tests/framework/kubevirt"
 	. "kubevirt.io/kubevirt/tests/framework/matcher"
+	"kubevirt.io/kubevirt/tests/libinstancetype/builder"
 	"kubevirt.io/kubevirt/tests/libkubevirt"
 	"kubevirt.io/kubevirt/tests/libkubevirt/config"
 	"kubevirt.io/kubevirt/tests/libmigration"
@@ -182,4 +184,109 @@ var _ = Describe("[sig-compute]Instance Type and Preference Hotplug", decorators
 		Entry("with maxGuest and maxSockets defined", true),
 		Entry("without maxGuest and maxSockets defined", false),
 	)
+
+	DescribeTable("should reject live update when preference requirements are no longer met - bug #14595",
+		func(
+			createVM func(*virtv1.VirtualMachineInstance, string) (*virtv1.VirtualMachine, error),
+			updateVM func(*virtv1.VirtualMachine) (*virtv1.VirtualMachine, error),
+			errMatcher gomegatypes.GomegaMatcher,
+		) {
+			vmi := libvmifact.NewAlpine(
+				libvmi.WithResourceMemory("1Gi"),
+				libvmi.WithNamespace(testsuite.GetTestNamespace(nil)),
+			)
+
+			preference := builder.NewPreference()
+			preference.Spec.Requirements = &instancetypev1beta1.PreferenceRequirements{
+				Memory: &instancetypev1beta1.MemoryPreferenceRequirement{
+					Guest: resource.MustParse("1Gi"),
+				},
+			}
+			preference, err := virtClient.VirtualMachinePreference(vmi.Namespace).Create(
+				context.Background(), preference, metav1.CreateOptions{})
+			Expect(err).ToNot(HaveOccurred())
+
+			vm, err := createVM(vmi, preference.Name)
+			Expect(err).ToNot(HaveOccurred())
+
+			_, err = updateVM(vm)
+			Expect(err).To(MatchError(errMatcher))
+		},
+		Entry("by VirtualMachine resource requests",
+			func(vmi *virtv1.VirtualMachineInstance, preferenceName string) (*virtv1.VirtualMachine, error) {
+				return virtClient.VirtualMachine(vmi.Namespace).Create(
+					context.Background(),
+					libvmi.NewVirtualMachine(vmi, libvmi.WithPreference(preferenceName)),
+					metav1.CreateOptions{},
+				)
+			},
+			func(vm *virtv1.VirtualMachine) (*virtv1.VirtualMachine, error) {
+				newMemory := vm.Spec.Template.Spec.Domain.Resources.Requests.Memory().DeepCopy()
+				newMemory.Sub(resource.MustParse("512Mi"))
+
+				const memoryPath = "/spec/template/spec/domain/resources/requests/memory"
+				patches := patch.New(
+					patch.WithTest(memoryPath, vm.Spec.Template.Spec.Domain.Resources.Requests.Memory().String()),
+					patch.WithReplace(memoryPath, newMemory.String()),
+				)
+				patchData, err := patches.GeneratePayload()
+				Expect(err).NotTo(HaveOccurred())
+				return virtClient.VirtualMachine(vm.Namespace).Patch(context.Background(), vm.Name, types.JSONPatchType, patchData, metav1.PatchOptions{})
+			},
+			ContainSubstring("failure checking preference requirements: insufficient Memory resources of 512Mi provided by VirtualMachine, preference requires 1Gi"),
+		),
+		Entry("by instance type resource requests",
+			func(vmi *virtv1.VirtualMachineInstance, preferenceName string) (*virtv1.VirtualMachine, error) {
+				origInstancetype, err := virtClient.VirtualMachineInstancetype(vmi.Namespace).Create(
+					context.Background(),
+					builder.NewInstancetypeFromVMI(vmi),
+					metav1.CreateOptions{},
+				)
+				Expect(err).ToNot(HaveOccurred())
+
+				vm, err := virtClient.VirtualMachine(vmi.Namespace).Create(
+					context.Background(),
+					libvmi.NewVirtualMachine(
+						vmi,
+						libvmi.WithInstancetype(origInstancetype.Name),
+						libvmi.WithPreference(preferenceName),
+					),
+					metav1.CreateOptions{})
+				Expect(err).ToNot(HaveOccurred())
+
+				// bug #14595 was caused by lookup code using the stashed controller
+				// revision copy of a different object during live update so wait until
+				// these are created before asserting that we fail to live update
+				By("Waiting for VirtualMachineInstancetype and VirtualMachinePreference ControllerRevision to be referenced from the VirtualMachine")
+				Eventually(ThisVM(vm)).WithTimeout(1 * time.Minute).WithPolling(2 * time.Second).Should(HaveControllerRevisionRefs())
+				return vm, nil
+			},
+			func(vm *virtv1.VirtualMachine) (*virtv1.VirtualMachine, error) {
+				origInstancetype, err := virtClient.VirtualMachineInstancetype(vm.Namespace).Get(
+					context.Background(), vm.Spec.Instancetype.Name, metav1.GetOptions{})
+				Expect(err).ToNot(HaveOccurred())
+
+				newMemory := origInstancetype.Spec.Memory.Guest.DeepCopy()
+				newMemory.Sub(resource.MustParse("512Mi"))
+				newInstancetype, err := virtClient.VirtualMachineInstancetype(vm.Namespace).Create(
+					context.Background(),
+					builder.NewInstancetype(
+						builder.WithCPUs(1),
+						builder.WithMemory(newMemory.String()),
+					),
+					metav1.CreateOptions{},
+				)
+				Expect(err).ToNot(HaveOccurred())
+
+				patches := patch.New(
+					patch.WithTest("/spec/instancetype/name", origInstancetype.Name),
+					patch.WithReplace("/spec/instancetype/name", newInstancetype.Name),
+				)
+				patchData, err := patches.GeneratePayload()
+				Expect(err).NotTo(HaveOccurred())
+				return virtClient.VirtualMachine(vm.Namespace).Patch(context.Background(), vm.Name, types.JSONPatchType, patchData, metav1.PatchOptions{})
+			},
+			ContainSubstring("failure checking preference requirements: insufficient Memory resources of 512Mi provided by instance type, preference requires 1Gi"),
+		),
+	)
 })


Reviews:
user: login: sourcery-ai[bot]
body: Hey @lyarwood - I've reviewed your changes - here's some feedback:

- If this PR is ready for detailed review, consider removing 'WIP' from the title.
- Populating the 'Before this PR'/'After this PR' and 'Why we need it' sections in the PR description would provide valuable context for reviewers.

<details>
<summary>Here's what I looked at during the review</summary>

- 🟡 **General issues**: 1 issue found
- 🟢 **Security**: all looks good
- 🟢 **Testing**: all looks good
- 🟢 **Complexity**: all looks good
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-09T18:09:33Z
user: login: 0xFelix
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T10:24:39Z
user: login: lyarwood
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T10:30:58Z
user: login: lyarwood
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T10:33:06Z
user: login: lyarwood
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T10:35:25Z
user: login: lyarwood
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T10:37:09Z
user: login: 0xFelix
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T10:53:16Z
user: login: 0xFelix
body: /approve
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T10:54:34Z

Review comments:
diff_hunk: @@ -47,10 +48,16 @@ func (f *revisionFinder) Find(vm *virtv1.VirtualMachine) (*appsv1.ControllerRevi
 	}
 	ref := vm.Status.InstancetypeRef
 	if ref != nil && ref.ControllerRevisionRef != nil && ref.ControllerRevisionRef.Name != "" {
-		return f.controllerRevisionFinder.Find(types.NamespacedName{
+		cr, err := f.controllerRevisionFinder.Find(types.NamespacedName{
 			Namespace: vm.Namespace,
 			Name:      ref.ControllerRevisionRef.Name,
 		})
+		if err != nil {
+			return nil, err
+		}
+		if cr.Labels[instancetypeapi.ControllerRevisionObjectNameLabel] == vm.Spec.Instancetype.Name {
path: pkg/instancetype/find/revision.go
user: login: sourcery-ai[bot]
body: **suggestion (bug_risk):** Consider whether accessing the label directly is fully safe.

Since Go map lookups return the zero value for missing keys, explicitly verify the label exists before comparing to avoid treating an absent label as a mismatch.

```suggestion
		if label, ok := cr.Labels[instancetypeapi.ControllerRevisionObjectNameLabel]; ok && label == vm.Spec.Instancetype.Name {
```
created_at: 2025-05-09T18:09:33Z
author_association: NONE
diff_hunk: @@ -182,4 +184,111 @@ var _ = Describe("[sig-compute]Instance Type and Preference Hotplug", decorators
 		Entry("with maxGuest and maxSockets defined", true),
 		Entry("without maxGuest and maxSockets defined", false),
 	)
+
+	DescribeTable("should reject live update when preference requirements are no longer met - bug #14595",
+		func(
+			createVM func(*virtv1.VirtualMachineInstance, string) (*virtv1.VirtualMachine, error),
+			updateVM func(*virtv1.VirtualMachine) (*virtv1.VirtualMachine, error),
+			errMatcher gomegatypes.GomegaMatcher,
+		) {
+			vmi := libvmifact.NewAlpine(
+				libnet.WithMasqueradeNetworking(),
path: tests/hotplug/instancetype.go
user: login: 0xFelix
body: nit: Do we need masquerade?
created_at: 2025-05-13T10:19:51Z
author_association: MEMBER
diff_hunk: @@ -182,4 +184,111 @@ var _ = Describe("[sig-compute]Instance Type and Preference Hotplug", decorators
 		Entry("with maxGuest and maxSockets defined", true),
 		Entry("without maxGuest and maxSockets defined", false),
 	)
+
+	DescribeTable("should reject live update when preference requirements are no longer met - bug #14595",
+		func(
+			createVM func(*virtv1.VirtualMachineInstance, string) (*virtv1.VirtualMachine, error),
+			updateVM func(*virtv1.VirtualMachine) (*virtv1.VirtualMachine, error),
+			errMatcher gomegatypes.GomegaMatcher,
+		) {
+			vmi := libvmifact.NewAlpine(
+				libnet.WithMasqueradeNetworking(),
+				libvmi.WithResourceMemory("1Gi"),
+			)
+			vmi.Namespace = testsuite.GetTestNamespace(vmi)
path: tests/hotplug/instancetype.go
user: login: 0xFelix
body: There should be `libvmi.WithNamespace`
created_at: 2025-05-13T10:20:09Z
author_association: MEMBER
diff_hunk: @@ -182,4 +184,111 @@ var _ = Describe("[sig-compute]Instance Type and Preference Hotplug", decorators
 		Entry("with maxGuest and maxSockets defined", true),
 		Entry("without maxGuest and maxSockets defined", false),
 	)
+
+	DescribeTable("should reject live update when preference requirements are no longer met - bug #14595",
+		func(
+			createVM func(*virtv1.VirtualMachineInstance, string) (*virtv1.VirtualMachine, error),
+			updateVM func(*virtv1.VirtualMachine) (*virtv1.VirtualMachine, error),
+			errMatcher gomegatypes.GomegaMatcher,
+		) {
+			vmi := libvmifact.NewAlpine(
+				libnet.WithMasqueradeNetworking(),
+				libvmi.WithResourceMemory("1Gi"),
+			)
+			vmi.Namespace = testsuite.GetTestNamespace(vmi)
+
+			preference := builder.NewPreference()
+			preference.Spec.Requirements = &instancetypev1beta1.PreferenceRequirements{
+				Memory: &instancetypev1beta1.MemoryPreferenceRequirement{
+					Guest: resource.MustParse("1Gi"),
+				},
+			}
+			preference, err := virtClient.VirtualMachinePreference(vmi.Namespace).Create(
+				context.Background(), preference, metav1.CreateOptions{})
+			Expect(err).ToNot(HaveOccurred())
+
+			vm, err := createVM(vmi, preference.Name)
+			Expect(err).ToNot(HaveOccurred())
+
+			_, err = updateVM(vm)
+			Expect(err).To(HaveOccurred())
+			Expect(err.Error()).To(errMatcher)
path: tests/hotplug/instancetype.go
user: login: 0xFelix
body: ```suggestion
			Expect(err).To(MatchError(errMatcher), "unexpected error")
```
created_at: 2025-05-13T10:21:50Z
author_association: MEMBER
diff_hunk: @@ -182,4 +184,111 @@ var _ = Describe("[sig-compute]Instance Type and Preference Hotplug", decorators
 		Entry("with maxGuest and maxSockets defined", true),
 		Entry("without maxGuest and maxSockets defined", false),
 	)
+
+	DescribeTable("should reject live update when preference requirements are no longer met - bug #14595",
+		func(
+			createVM func(*virtv1.VirtualMachineInstance, string) (*virtv1.VirtualMachine, error),
+			updateVM func(*virtv1.VirtualMachine) (*virtv1.VirtualMachine, error),
+			errMatcher gomegatypes.GomegaMatcher,
+		) {
+			vmi := libvmifact.NewAlpine(
+				libnet.WithMasqueradeNetworking(),
+				libvmi.WithResourceMemory("1Gi"),
+			)
+			vmi.Namespace = testsuite.GetTestNamespace(vmi)
+
+			preference := builder.NewPreference()
+			preference.Spec.Requirements = &instancetypev1beta1.PreferenceRequirements{
+				Memory: &instancetypev1beta1.MemoryPreferenceRequirement{
+					Guest: resource.MustParse("1Gi"),
+				},
+			}
+			preference, err := virtClient.VirtualMachinePreference(vmi.Namespace).Create(
+				context.Background(), preference, metav1.CreateOptions{})
+			Expect(err).ToNot(HaveOccurred())
+
+			vm, err := createVM(vmi, preference.Name)
+			Expect(err).ToNot(HaveOccurred())
+
+			_, err = updateVM(vm)
+			Expect(err).To(HaveOccurred())
+			Expect(err.Error()).To(errMatcher)
+		},
+		Entry("by VirtualMachine resource requests",
+			func(vmi *virtv1.VirtualMachineInstance, preferenceName string) (*virtv1.VirtualMachine, error) {
+				return virtClient.VirtualMachine(vmi.Namespace).Create(
+					context.Background(),
+					libvmi.NewVirtualMachine(vmi, libvmi.WithPreference(preferenceName)),
+					metav1.CreateOptions{},
+				)
+			},
+			func(vm *virtv1.VirtualMachine) (*virtv1.VirtualMachine, error) {
+				newMemory := vm.Spec.Template.Spec.Domain.Resources.Requests.Memory().DeepCopy()
+				newMemory.Sub(resource.MustParse("512Mi"))
+
+				const memoryPath = "/spec/template/spec/domain/resources/requests/memory"
+				patches := patch.New(
+					patch.WithTest(memoryPath, vm.Spec.Template.Spec.Domain.Resources.Requests.Memory().String()),
+					patch.WithReplace(memoryPath, newMemory.String()),
+				)
+				patchData, err := patches.GeneratePayload()
+				Expect(err).NotTo(HaveOccurred())
+				return virtClient.VirtualMachine(vm.Namespace).Patch(context.Background(), vm.Name, types.JSONPatchType, patchData, metav1.PatchOptions{})
+			},
+			ContainSubstring("failure checking preference requirements: insufficient Memory resources of 512Mi provided by VirtualMachine, preference requires 1Gi"),
+		),
+		Entry("by instance type resource requests",
+			func(vmi *virtv1.VirtualMachineInstance, preferenceName string) (*virtv1.VirtualMachine, error) {
+				origInstancetype, err := virtClient.VirtualMachineInstancetype(vmi.Namespace).Create(
+					context.Background(),
+					builder.NewInstancetypeFromVMI(vmi),
+					metav1.CreateOptions{},
+				)
+				Expect(err).ToNot(HaveOccurred())
+
+				vm, err := virtClient.VirtualMachine(vmi.Namespace).Create(
+					context.Background(),
+					libvmi.NewVirtualMachine(
+						vmi,
path: tests/hotplug/instancetype.go
user: login: 0xFelix
body: Will there be no conflict with the VMI's `libvmi.WithResourceMemory("1Gi")`?
created_at: 2025-05-13T10:23:02Z
author_association: MEMBER
diff_hunk: @@ -182,4 +184,111 @@ var _ = Describe("[sig-compute]Instance Type and Preference Hotplug", decorators
 		Entry("with maxGuest and maxSockets defined", true),
 		Entry("without maxGuest and maxSockets defined", false),
 	)
+
+	DescribeTable("should reject live update when preference requirements are no longer met - bug #14595",
+		func(
+			createVM func(*virtv1.VirtualMachineInstance, string) (*virtv1.VirtualMachine, error),
+			updateVM func(*virtv1.VirtualMachine) (*virtv1.VirtualMachine, error),
+			errMatcher gomegatypes.GomegaMatcher,
+		) {
+			vmi := libvmifact.NewAlpine(
+				libnet.WithMasqueradeNetworking(),
+				libvmi.WithResourceMemory("1Gi"),
+			)
+			vmi.Namespace = testsuite.GetTestNamespace(vmi)
+
+			preference := builder.NewPreference()
+			preference.Spec.Requirements = &instancetypev1beta1.PreferenceRequirements{
+				Memory: &instancetypev1beta1.MemoryPreferenceRequirement{
+					Guest: resource.MustParse("1Gi"),
+				},
+			}
+			preference, err := virtClient.VirtualMachinePreference(vmi.Namespace).Create(
+				context.Background(), preference, metav1.CreateOptions{})
+			Expect(err).ToNot(HaveOccurred())
+
+			vm, err := createVM(vmi, preference.Name)
+			Expect(err).ToNot(HaveOccurred())
+
+			_, err = updateVM(vm)
+			Expect(err).To(HaveOccurred())
+			Expect(err.Error()).To(errMatcher)
+		},
+		Entry("by VirtualMachine resource requests",
+			func(vmi *virtv1.VirtualMachineInstance, preferenceName string) (*virtv1.VirtualMachine, error) {
+				return virtClient.VirtualMachine(vmi.Namespace).Create(
+					context.Background(),
+					libvmi.NewVirtualMachine(vmi, libvmi.WithPreference(preferenceName)),
+					metav1.CreateOptions{},
+				)
+			},
+			func(vm *virtv1.VirtualMachine) (*virtv1.VirtualMachine, error) {
+				newMemory := vm.Spec.Template.Spec.Domain.Resources.Requests.Memory().DeepCopy()
+				newMemory.Sub(resource.MustParse("512Mi"))
+
+				const memoryPath = "/spec/template/spec/domain/resources/requests/memory"
+				patches := patch.New(
+					patch.WithTest(memoryPath, vm.Spec.Template.Spec.Domain.Resources.Requests.Memory().String()),
+					patch.WithReplace(memoryPath, newMemory.String()),
+				)
+				patchData, err := patches.GeneratePayload()
+				Expect(err).NotTo(HaveOccurred())
+				return virtClient.VirtualMachine(vm.Namespace).Patch(context.Background(), vm.Name, types.JSONPatchType, patchData, metav1.PatchOptions{})
+			},
+			ContainSubstring("failure checking preference requirements: insufficient Memory resources of 512Mi provided by VirtualMachine, preference requires 1Gi"),
+		),
+		Entry("by instance type resource requests",
+			func(vmi *virtv1.VirtualMachineInstance, preferenceName string) (*virtv1.VirtualMachine, error) {
+				origInstancetype, err := virtClient.VirtualMachineInstancetype(vmi.Namespace).Create(
+					context.Background(),
+					builder.NewInstancetypeFromVMI(vmi),
+					metav1.CreateOptions{},
+				)
+				Expect(err).ToNot(HaveOccurred())
+
+				vm, err := virtClient.VirtualMachine(vmi.Namespace).Create(
+					context.Background(),
+					libvmi.NewVirtualMachine(
+						vmi,
+						libvmi.WithInstancetype(origInstancetype.Name),
+						libvmi.WithPreference(preferenceName),
+					),
+					metav1.CreateOptions{})
+				Expect(err).ToNot(HaveOccurred())
+
+				// bug #14595 was caused by lookup code using the stashed controller
+				// revision copy of a different object during live update so wait until
+				// these are created before asserting that we fail to live update
+				By("Waiting for VirtualMachineInstancetype and VirtualMachinePreference ControllerRevision to be referenced from the VirtualMachine")
+				Eventually(ThisVM(vm)).WithTimeout(1 * time.Minute).WithPolling(2 * time.Second).Should(HaveControllerRevisionRefs())
+				return vm, nil
+			},
+			func(vm *virtv1.VirtualMachine) (*virtv1.VirtualMachine, error) {
+				origInstancetype, err := virtClient.VirtualMachineInstancetype(vm.Namespace).Get(
+					context.Background(), vm.Spec.Instancetype.Name, metav1.GetOptions{})
+				Expect(err).ToNot(HaveOccurred())
+
+				newMemory := origInstancetype.Spec.Memory.Guest.DeepCopy()
+				newMemory.Sub(resource.MustParse("512Mi"))
+				newInstancetype, err := virtClient.VirtualMachineInstancetype(vm.Namespace).Create(
+					context.Background(),
+					builder.NewInstancetype(
+						builder.WithCPUs(1),
+						builder.WithMemory(newMemory.String()),
+					),
+					metav1.CreateOptions{},
+				)
+				Expect(err).ToNot(HaveOccurred())
+
+				patches := patch.New(
+					patch.WithTest("/spec/instancetype/name", origInstancetype.Name),
+					patch.WithReplace("/spec/instancetype/name", newInstancetype.Name),
+				)
+				patchData, err := patches.GeneratePayload()
+				Expect(err).NotTo(HaveOccurred())
+				return virtClient.VirtualMachine(vm.Namespace).Patch(context.Background(), vm.Name, types.JSONPatchType, patchData, metav1.PatchOptions{})
path: tests/hotplug/instancetype.go
user: login: 0xFelix
body: Do we need to wait for updated CR refs to avoid flakes?
created_at: 2025-05-13T10:24:34Z
author_association: MEMBER
diff_hunk: @@ -182,4 +184,111 @@ var _ = Describe("[sig-compute]Instance Type and Preference Hotplug", decorators
 		Entry("with maxGuest and maxSockets defined", true),
 		Entry("without maxGuest and maxSockets defined", false),
 	)
+
+	DescribeTable("should reject live update when preference requirements are no longer met - bug #14595",
+		func(
+			createVM func(*virtv1.VirtualMachineInstance, string) (*virtv1.VirtualMachine, error),
+			updateVM func(*virtv1.VirtualMachine) (*virtv1.VirtualMachine, error),
+			errMatcher gomegatypes.GomegaMatcher,
+		) {
+			vmi := libvmifact.NewAlpine(
+				libnet.WithMasqueradeNetworking(),
path: tests/hotplug/instancetype.go
user: login: lyarwood
body: for this specific test no, I'll remove.
created_at: 2025-05-13T10:30:57Z
author_association: MEMBER
diff_hunk: @@ -182,4 +184,111 @@ var _ = Describe("[sig-compute]Instance Type and Preference Hotplug", decorators
 		Entry("with maxGuest and maxSockets defined", true),
 		Entry("without maxGuest and maxSockets defined", false),
 	)
+
+	DescribeTable("should reject live update when preference requirements are no longer met - bug #14595",
+		func(
+			createVM func(*virtv1.VirtualMachineInstance, string) (*virtv1.VirtualMachine, error),
+			updateVM func(*virtv1.VirtualMachine) (*virtv1.VirtualMachine, error),
+			errMatcher gomegatypes.GomegaMatcher,
+		) {
+			vmi := libvmifact.NewAlpine(
+				libnet.WithMasqueradeNetworking(),
+				libvmi.WithResourceMemory("1Gi"),
+			)
+			vmi.Namespace = testsuite.GetTestNamespace(vmi)
path: tests/hotplug/instancetype.go
user: login: lyarwood
body: Yeah I didn't realise that `GetTestNamespace(nil)` was valid here, I'll fold it in.
created_at: 2025-05-13T10:33:06Z
author_association: MEMBER
diff_hunk: @@ -182,4 +184,111 @@ var _ = Describe("[sig-compute]Instance Type and Preference Hotplug", decorators
 		Entry("with maxGuest and maxSockets defined", true),
 		Entry("without maxGuest and maxSockets defined", false),
 	)
+
+	DescribeTable("should reject live update when preference requirements are no longer met - bug #14595",
+		func(
+			createVM func(*virtv1.VirtualMachineInstance, string) (*virtv1.VirtualMachine, error),
+			updateVM func(*virtv1.VirtualMachine) (*virtv1.VirtualMachine, error),
+			errMatcher gomegatypes.GomegaMatcher,
+		) {
+			vmi := libvmifact.NewAlpine(
+				libnet.WithMasqueradeNetworking(),
+				libvmi.WithResourceMemory("1Gi"),
+			)
+			vmi.Namespace = testsuite.GetTestNamespace(vmi)
+
+			preference := builder.NewPreference()
+			preference.Spec.Requirements = &instancetypev1beta1.PreferenceRequirements{
+				Memory: &instancetypev1beta1.MemoryPreferenceRequirement{
+					Guest: resource.MustParse("1Gi"),
+				},
+			}
+			preference, err := virtClient.VirtualMachinePreference(vmi.Namespace).Create(
+				context.Background(), preference, metav1.CreateOptions{})
+			Expect(err).ToNot(HaveOccurred())
+
+			vm, err := createVM(vmi, preference.Name)
+			Expect(err).ToNot(HaveOccurred())
+
+			_, err = updateVM(vm)
+			Expect(err).To(HaveOccurred())
+			Expect(err.Error()).To(errMatcher)
+		},
+		Entry("by VirtualMachine resource requests",
+			func(vmi *virtv1.VirtualMachineInstance, preferenceName string) (*virtv1.VirtualMachine, error) {
+				return virtClient.VirtualMachine(vmi.Namespace).Create(
+					context.Background(),
+					libvmi.NewVirtualMachine(vmi, libvmi.WithPreference(preferenceName)),
+					metav1.CreateOptions{},
+				)
+			},
+			func(vm *virtv1.VirtualMachine) (*virtv1.VirtualMachine, error) {
+				newMemory := vm.Spec.Template.Spec.Domain.Resources.Requests.Memory().DeepCopy()
+				newMemory.Sub(resource.MustParse("512Mi"))
+
+				const memoryPath = "/spec/template/spec/domain/resources/requests/memory"
+				patches := patch.New(
+					patch.WithTest(memoryPath, vm.Spec.Template.Spec.Domain.Resources.Requests.Memory().String()),
+					patch.WithReplace(memoryPath, newMemory.String()),
+				)
+				patchData, err := patches.GeneratePayload()
+				Expect(err).NotTo(HaveOccurred())
+				return virtClient.VirtualMachine(vm.Namespace).Patch(context.Background(), vm.Name, types.JSONPatchType, patchData, metav1.PatchOptions{})
+			},
+			ContainSubstring("failure checking preference requirements: insufficient Memory resources of 512Mi provided by VirtualMachine, preference requires 1Gi"),
+		),
+		Entry("by instance type resource requests",
+			func(vmi *virtv1.VirtualMachineInstance, preferenceName string) (*virtv1.VirtualMachine, error) {
+				origInstancetype, err := virtClient.VirtualMachineInstancetype(vmi.Namespace).Create(
+					context.Background(),
+					builder.NewInstancetypeFromVMI(vmi),
+					metav1.CreateOptions{},
+				)
+				Expect(err).ToNot(HaveOccurred())
+
+				vm, err := virtClient.VirtualMachine(vmi.Namespace).Create(
+					context.Background(),
+					libvmi.NewVirtualMachine(
+						vmi,
path: tests/hotplug/instancetype.go
user: login: lyarwood
body: Yeah, `WithInstancetype` calls `resourcesRemovedFromVMI` to handle this:

https://github.com/kubevirt/kubevirt/blob/21ce93e095e3f183d939422dfe3cb661ae7525d4/pkg/libvmi/vm.go#L137-L145

https://github.com/kubevirt/kubevirt/blob/21ce93e095e3f183d939422dfe3cb661ae7525d4/pkg/libvmi/vm.go#L101-L105


created_at: 2025-05-13T10:35:25Z
author_association: MEMBER
diff_hunk: @@ -182,4 +184,111 @@ var _ = Describe("[sig-compute]Instance Type and Preference Hotplug", decorators
 		Entry("with maxGuest and maxSockets defined", true),
 		Entry("without maxGuest and maxSockets defined", false),
 	)
+
+	DescribeTable("should reject live update when preference requirements are no longer met - bug #14595",
+		func(
+			createVM func(*virtv1.VirtualMachineInstance, string) (*virtv1.VirtualMachine, error),
+			updateVM func(*virtv1.VirtualMachine) (*virtv1.VirtualMachine, error),
+			errMatcher gomegatypes.GomegaMatcher,
+		) {
+			vmi := libvmifact.NewAlpine(
+				libnet.WithMasqueradeNetworking(),
+				libvmi.WithResourceMemory("1Gi"),
+			)
+			vmi.Namespace = testsuite.GetTestNamespace(vmi)
+
+			preference := builder.NewPreference()
+			preference.Spec.Requirements = &instancetypev1beta1.PreferenceRequirements{
+				Memory: &instancetypev1beta1.MemoryPreferenceRequirement{
+					Guest: resource.MustParse("1Gi"),
+				},
+			}
+			preference, err := virtClient.VirtualMachinePreference(vmi.Namespace).Create(
+				context.Background(), preference, metav1.CreateOptions{})
+			Expect(err).ToNot(HaveOccurred())
+
+			vm, err := createVM(vmi, preference.Name)
+			Expect(err).ToNot(HaveOccurred())
+
+			_, err = updateVM(vm)
+			Expect(err).To(HaveOccurred())
+			Expect(err.Error()).To(errMatcher)
+		},
+		Entry("by VirtualMachine resource requests",
+			func(vmi *virtv1.VirtualMachineInstance, preferenceName string) (*virtv1.VirtualMachine, error) {
+				return virtClient.VirtualMachine(vmi.Namespace).Create(
+					context.Background(),
+					libvmi.NewVirtualMachine(vmi, libvmi.WithPreference(preferenceName)),
+					metav1.CreateOptions{},
+				)
+			},
+			func(vm *virtv1.VirtualMachine) (*virtv1.VirtualMachine, error) {
+				newMemory := vm.Spec.Template.Spec.Domain.Resources.Requests.Memory().DeepCopy()
+				newMemory.Sub(resource.MustParse("512Mi"))
+
+				const memoryPath = "/spec/template/spec/domain/resources/requests/memory"
+				patches := patch.New(
+					patch.WithTest(memoryPath, vm.Spec.Template.Spec.Domain.Resources.Requests.Memory().String()),
+					patch.WithReplace(memoryPath, newMemory.String()),
+				)
+				patchData, err := patches.GeneratePayload()
+				Expect(err).NotTo(HaveOccurred())
+				return virtClient.VirtualMachine(vm.Namespace).Patch(context.Background(), vm.Name, types.JSONPatchType, patchData, metav1.PatchOptions{})
+			},
+			ContainSubstring("failure checking preference requirements: insufficient Memory resources of 512Mi provided by VirtualMachine, preference requires 1Gi"),
+		),
+		Entry("by instance type resource requests",
+			func(vmi *virtv1.VirtualMachineInstance, preferenceName string) (*virtv1.VirtualMachine, error) {
+				origInstancetype, err := virtClient.VirtualMachineInstancetype(vmi.Namespace).Create(
+					context.Background(),
+					builder.NewInstancetypeFromVMI(vmi),
+					metav1.CreateOptions{},
+				)
+				Expect(err).ToNot(HaveOccurred())
+
+				vm, err := virtClient.VirtualMachine(vmi.Namespace).Create(
+					context.Background(),
+					libvmi.NewVirtualMachine(
+						vmi,
+						libvmi.WithInstancetype(origInstancetype.Name),
+						libvmi.WithPreference(preferenceName),
+					),
+					metav1.CreateOptions{})
+				Expect(err).ToNot(HaveOccurred())
+
+				// bug #14595 was caused by lookup code using the stashed controller
+				// revision copy of a different object during live update so wait until
+				// these are created before asserting that we fail to live update
+				By("Waiting for VirtualMachineInstancetype and VirtualMachinePreference ControllerRevision to be referenced from the VirtualMachine")
+				Eventually(ThisVM(vm)).WithTimeout(1 * time.Minute).WithPolling(2 * time.Second).Should(HaveControllerRevisionRefs())
+				return vm, nil
+			},
+			func(vm *virtv1.VirtualMachine) (*virtv1.VirtualMachine, error) {
+				origInstancetype, err := virtClient.VirtualMachineInstancetype(vm.Namespace).Get(
+					context.Background(), vm.Spec.Instancetype.Name, metav1.GetOptions{})
+				Expect(err).ToNot(HaveOccurred())
+
+				newMemory := origInstancetype.Spec.Memory.Guest.DeepCopy()
+				newMemory.Sub(resource.MustParse("512Mi"))
+				newInstancetype, err := virtClient.VirtualMachineInstancetype(vm.Namespace).Create(
+					context.Background(),
+					builder.NewInstancetype(
+						builder.WithCPUs(1),
+						builder.WithMemory(newMemory.String()),
+					),
+					metav1.CreateOptions{},
+				)
+				Expect(err).ToNot(HaveOccurred())
+
+				patches := patch.New(
+					patch.WithTest("/spec/instancetype/name", origInstancetype.Name),
+					patch.WithReplace("/spec/instancetype/name", newInstancetype.Name),
+				)
+				patchData, err := patches.GeneratePayload()
+				Expect(err).NotTo(HaveOccurred())
+				return virtClient.VirtualMachine(vm.Namespace).Patch(context.Background(), vm.Name, types.JSONPatchType, patchData, metav1.PatchOptions{})
path: tests/hotplug/instancetype.go
user: login: lyarwood
body: No, this patch will always fail in the webhook now so the updated VirtualMachine is never seen by the controller or sync loop.
created_at: 2025-05-13T10:37:09Z
author_association: MEMBER
diff_hunk: @@ -182,4 +184,111 @@ var _ = Describe("[sig-compute]Instance Type and Preference Hotplug", decorators
 		Entry("with maxGuest and maxSockets defined", true),
 		Entry("without maxGuest and maxSockets defined", false),
 	)
+
+	DescribeTable("should reject live update when preference requirements are no longer met - bug #14595",
+		func(
+			createVM func(*virtv1.VirtualMachineInstance, string) (*virtv1.VirtualMachine, error),
+			updateVM func(*virtv1.VirtualMachine) (*virtv1.VirtualMachine, error),
+			errMatcher gomegatypes.GomegaMatcher,
+		) {
+			vmi := libvmifact.NewAlpine(
+				libnet.WithMasqueradeNetworking(),
+				libvmi.WithResourceMemory("1Gi"),
+			)
+			vmi.Namespace = testsuite.GetTestNamespace(vmi)
+
+			preference := builder.NewPreference()
+			preference.Spec.Requirements = &instancetypev1beta1.PreferenceRequirements{
+				Memory: &instancetypev1beta1.MemoryPreferenceRequirement{
+					Guest: resource.MustParse("1Gi"),
+				},
+			}
+			preference, err := virtClient.VirtualMachinePreference(vmi.Namespace).Create(
+				context.Background(), preference, metav1.CreateOptions{})
+			Expect(err).ToNot(HaveOccurred())
+
+			vm, err := createVM(vmi, preference.Name)
+			Expect(err).ToNot(HaveOccurred())
+
+			_, err = updateVM(vm)
+			Expect(err).To(HaveOccurred())
+			Expect(err.Error()).To(errMatcher)
+		},
+		Entry("by VirtualMachine resource requests",
+			func(vmi *virtv1.VirtualMachineInstance, preferenceName string) (*virtv1.VirtualMachine, error) {
+				return virtClient.VirtualMachine(vmi.Namespace).Create(
+					context.Background(),
+					libvmi.NewVirtualMachine(vmi, libvmi.WithPreference(preferenceName)),
+					metav1.CreateOptions{},
+				)
+			},
+			func(vm *virtv1.VirtualMachine) (*virtv1.VirtualMachine, error) {
+				newMemory := vm.Spec.Template.Spec.Domain.Resources.Requests.Memory().DeepCopy()
+				newMemory.Sub(resource.MustParse("512Mi"))
+
+				const memoryPath = "/spec/template/spec/domain/resources/requests/memory"
+				patches := patch.New(
+					patch.WithTest(memoryPath, vm.Spec.Template.Spec.Domain.Resources.Requests.Memory().String()),
+					patch.WithReplace(memoryPath, newMemory.String()),
+				)
+				patchData, err := patches.GeneratePayload()
+				Expect(err).NotTo(HaveOccurred())
+				return virtClient.VirtualMachine(vm.Namespace).Patch(context.Background(), vm.Name, types.JSONPatchType, patchData, metav1.PatchOptions{})
+			},
+			ContainSubstring("failure checking preference requirements: insufficient Memory resources of 512Mi provided by VirtualMachine, preference requires 1Gi"),
+		),
+		Entry("by instance type resource requests",
+			func(vmi *virtv1.VirtualMachineInstance, preferenceName string) (*virtv1.VirtualMachine, error) {
+				origInstancetype, err := virtClient.VirtualMachineInstancetype(vmi.Namespace).Create(
+					context.Background(),
+					builder.NewInstancetypeFromVMI(vmi),
+					metav1.CreateOptions{},
+				)
+				Expect(err).ToNot(HaveOccurred())
+
+				vm, err := virtClient.VirtualMachine(vmi.Namespace).Create(
+					context.Background(),
+					libvmi.NewVirtualMachine(
+						vmi,
path: tests/hotplug/instancetype.go
user: login: 0xFelix
body: Interesting to know, I would have not expected `libvmi` to contain such logic.
created_at: 2025-05-13T10:53:16Z
author_association: MEMBER

PR comments:
user: login: kubevirt-bot
created_at: 2025-05-13T10:54:44Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubevirt/kubevirt/pull/14682#pullrequestreview-2836247985" title="Approved">0xFelix</a>*

The full list of commands accepted by this bot can be found [here](https://prow.ci.kubevirt.io/command-help?repo=kubevirt%2Fkubevirt).

The pull request process is described [here](https://github.com/kubevirt/community/blob/main/docs/add-merge-automation-to-your-repository.md#example-workflow)

<details >
Needs approval from an approver in each of these files:

- ~~[pkg/instancetype/OWNERS](https://github.com/kubevirt/kubevirt/blob/main/pkg/instancetype/OWNERS)~~ [0xFelix]
- ~~[tests/OWNERS](https://github.com/kubevirt/kubevirt/blob/main/tests/OWNERS)~~ [0xFelix]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
author_association: CONTRIBUTOR
user: login: akrejcir
created_at: 2025-05-13T11:51:04Z
body: /lgtm
author_association: CONTRIBUTOR
user: login: kubevirt-commenter-bot
created_at: 2025-05-13T11:51:11Z
body: Required labels detected, running phase 2 presubmits:
/test pull-kubevirt-e2e-windows2016
/test pull-kubevirt-e2e-kind-1.30-vgpu
/test pull-kubevirt-e2e-kind-sriov
/test pull-kubevirt-e2e-k8s-1.32-ipv6-sig-network
/test pull-kubevirt-e2e-k8s-1.30-sig-network
/test pull-kubevirt-e2e-k8s-1.30-sig-storage
/test pull-kubevirt-e2e-k8s-1.30-sig-compute
/test pull-kubevirt-e2e-k8s-1.30-sig-operator
/test pull-kubevirt-e2e-k8s-1.31-sig-network
/test pull-kubevirt-e2e-k8s-1.31-sig-storage
/test pull-kubevirt-e2e-k8s-1.31-sig-compute
/test pull-kubevirt-e2e-k8s-1.31-sig-operator

author_association: NONE
user: login: lyarwood
created_at: 2025-05-13T12:31:31Z
body: /cherry-pick release-1.5
author_association: MEMBER
user: login: kubevirt-bot
created_at: 2025-05-13T12:31:34Z
body: @lyarwood: once the present PR merges, I will cherry-pick it on top of `release-1.5` in a new PR and assign it to you.

<details>

In response to [this](https://github.com/kubevirt/kubevirt/pull/14682#issuecomment-2876326655):

>/cherry-pick release-1.5


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>
author_association: CONTRIBUTOR
user: login: kubevirt-commenter-bot
created_at: 2025-05-14T03:39:01Z
body: /retest-required
This bot automatically retries required jobs that failed/flaked on approved PRs.
Silence the bot with an `/lgtm cancel` or `/hold` comment for consistent failures.
author_association: NONE
user: login: kubevirt-bot
created_at: 2025-05-14T06:44:40Z
body: @lyarwood: #14682 failed to apply on top of branch "release-1.5":
```
Applying: instancetype: Regression unit tests for bug #14595
Applying: instancetype: Only return revision of currently referenced object
Applying: instancetype: Add functional coverage for bug #14595
Using index info to reconstruct a base tree...
M	tests/hotplug/BUILD.bazel
Falling back to patching base and 3-way merge...
Auto-merging tests/hotplug/BUILD.bazel
CONFLICT (content): Merge conflict in tests/hotplug/BUILD.bazel
error: Failed to merge in the changes.
hint: Use 'git am --show-current-patch=diff' to see the failed patch
hint: When you have resolved this problem, run "git am --continue".
hint: If you prefer to skip this patch, run "git am --skip" instead.
hint: To restore the original branch and stop patching, run "git am --abort".
hint: Disable this message with "git config advice.mergeConflict false"
Patch failed at 0003 instancetype: Add functional coverage for bug #14595

```

<details>

In response to [this](https://github.com/kubevirt/kubevirt/pull/14682#issuecomment-2876326655):

>/cherry-pick release-1.5


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>
author_association: CONTRIBUTOR