Repository: kubernetes-sigs/cluster-api-provider-kubevirt (go)

Issue:
ClusterClass incompatibility caused by KubevirtMachineTemplateSpec immutability.
**What steps did you take and what happened:**
Actually, at our company, we are using the CAPI ClusterClass feature to manage our clusters lifecycle.
We saw that the capi-controller-manager tried to update KubevirtMachineTemplate when we update a field (like the memory of cpu cores) but we're facing this error :
```
"Reconciler error" err="error reconciling the Cluster topology: failed to reconcile MachineDeployment/worker-djv5f: failed to create patch helper for KubevirtMachineTemplate/worker-infra-fdjc7: server side apply dry-run failed for modified object: admission webhook \"validation.kubevirtmachinetemplate.infrastructure.cluster.x-k8s.io\" denied the request: KubevirtMachineTemplateSpec is immutable
```

Did you know why KubevirtMachineTemplate resources are immutable, is this for security, lifecycle of any other purpose ? 


**What did you expect to happen:**
The KubevirtMachineTemplate should be updated and a new machine set will be triggered by the cluster-api to rollout worker nodes. 

/kind bug
[One or more /area label. See https://github.com/kubernetes-sigs/cluster-api-provider-kubevirt/labels?q=area for the list of labels]


Pull request:
feat: check if the request is a dry-run to match CAPI topology design
Fix #264 

Patch:
diff --git a/go.mod b/go.mod
index 05de296..e17cf76 100644
--- a/go.mod
+++ b/go.mod
@@ -18,8 +18,9 @@ require (
 	k8s.io/component-base v0.28.3
 	k8s.io/klog/v2 v2.100.1
 	k8s.io/kubectl v0.28.3
-	k8s.io/utils v0.0.0-20230505201702-9f6742963106
+	k8s.io/utils v0.0.0-20240102154912-e7106e64919e
 	kubevirt.io/api v1.0.0
+	kubevirt.io/containerized-data-importer-api v1.57.0
 	sigs.k8s.io/cluster-api v1.5.2
 	sigs.k8s.io/controller-runtime v0.16.2
 	sigs.k8s.io/kind v0.20.0
@@ -102,7 +103,6 @@ require (
 	k8s.io/apiextensions-apiserver v0.28.0 // indirect
 	k8s.io/cli-runtime v0.28.3 // indirect
 	k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect
-	kubevirt.io/containerized-data-importer-api v1.57.0 // indirect
 	kubevirt.io/controller-lifecycle-operator-sdk/api v0.0.0-20220329064328-f3cc58c6ed90 // indirect
 	sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
 	sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 // indirect
diff --git a/go.sum b/go.sum
index adb4b85..c7c420c 100644
--- a/go.sum
+++ b/go.sum
@@ -496,8 +496,8 @@ k8s.io/kubectl v0.28.3 h1:H1Peu1O3EbN9zHkJCcvhiJ4NUj6lb88sGPO5wrWIM6k=
 k8s.io/kubectl v0.28.3/go.mod h1:RDAudrth/2wQ3Sg46fbKKl4/g+XImzvbsSRZdP2RiyE=
 k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
 k8s.io/utils v0.0.0-20211116205334-6203023598ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
-k8s.io/utils v0.0.0-20230505201702-9f6742963106 h1:EObNQ3TW2D+WptiYXlApGNLVy0zm/JIBVY9i+M4wpAU=
-k8s.io/utils v0.0.0-20230505201702-9f6742963106/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+k8s.io/utils v0.0.0-20240102154912-e7106e64919e h1:eQ/4ljkx21sObifjzXwlPKpdGLrCfRziVtos3ofG/sQ=
+k8s.io/utils v0.0.0-20240102154912-e7106e64919e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
 kubevirt.io/api v1.0.0 h1:RBdXP5CDhE0v5qL2OUQdrYyRrHe/F68Z91GWqBDF6nw=
 kubevirt.io/api v1.0.0/go.mod h1:CJ4vZsaWhVN3jNbyc9y3lIZhw8nUHbWjap0xHABQiqc=
 kubevirt.io/containerized-data-importer-api v1.57.0 h1:IpRCUyDS0x7BaVa5q5MCzuWRAfvXT54GpEnNJke5hSE=
diff --git a/pkg/webhookhandler/validator.go b/pkg/webhookhandler/validator.go
index 41b3b02..4f5bc67 100644
--- a/pkg/webhookhandler/validator.go
+++ b/pkg/webhookhandler/validator.go
@@ -67,6 +67,11 @@ func (wh *kubevirtMachineTemplateHandler) Handle(_ context.Context, req admissio
 
 	case admissionv1.Update:
 		oldKVTmplt := &v1alpha1.KubevirtMachineTemplate{}
+		// Server Side Apply implementation in ClusterClass and managed topologies requires to dry-run changes on templates.
+		// cf: https://cluster-api.sigs.k8s.io/developer/providers/migrations/v1.1-to-v1.2?search=#required-api-changes-for-providers	
+		if *req.DryRun {
+			return admission.Allowed("")
+		}
 		if err := wh.decoder.DecodeRaw(req.Object, kvTmplt); err != nil {
 			return admission.Errored(http.StatusBadRequest, err)
 		}


Test patch:
diff --git a/e2e/go.mod b/e2e/go.mod
index 95a90ce..d388553 100644
--- a/e2e/go.mod
+++ b/e2e/go.mod
@@ -10,7 +10,7 @@ require (
 	k8s.io/api v0.28.3
 	k8s.io/apimachinery v0.28.3
 	k8s.io/client-go v12.0.0+incompatible
-	k8s.io/utils v0.0.0-20230505201702-9f6742963106
+	k8s.io/utils v0.0.0-20240102154912-e7106e64919e
 	kubevirt.io/api v1.0.0
 	kubevirt.io/client-go v0.0.0-00010101000000-000000000000
 	sigs.k8s.io/cluster-api v1.5.2
diff --git a/e2e/go.sum b/e2e/go.sum
index ab18490..f8f7374 100644
--- a/e2e/go.sum
+++ b/e2e/go.sum
@@ -1683,8 +1683,8 @@ k8s.io/utils v0.0.0-20191114200735-6ca3b61696b6/go.mod h1:sZAwmy6armz5eXlNoLmJcl
 k8s.io/utils v0.0.0-20200414100711-2df71ebbae66/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
 k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
 k8s.io/utils v0.0.0-20221107191617-1a15be271d1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
-k8s.io/utils v0.0.0-20230505201702-9f6742963106 h1:EObNQ3TW2D+WptiYXlApGNLVy0zm/JIBVY9i+M4wpAU=
-k8s.io/utils v0.0.0-20230505201702-9f6742963106/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+k8s.io/utils v0.0.0-20240102154912-e7106e64919e h1:eQ/4ljkx21sObifjzXwlPKpdGLrCfRziVtos3ofG/sQ=
+k8s.io/utils v0.0.0-20240102154912-e7106e64919e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
 kubevirt.io/api v1.0.0 h1:RBdXP5CDhE0v5qL2OUQdrYyRrHe/F68Z91GWqBDF6nw=
 kubevirt.io/api v1.0.0/go.mod h1:CJ4vZsaWhVN3jNbyc9y3lIZhw8nUHbWjap0xHABQiqc=
 kubevirt.io/client-go v1.0.0 h1:MMn41j/lFd+lJ7gWn7yuIZYW/aT9fI3bUimAuxAQ+Xk=
diff --git a/pkg/webhookhandler/validator_test.go b/pkg/webhookhandler/validator_test.go
index c3f7708..5365d71 100644
--- a/pkg/webhookhandler/validator_test.go
+++ b/pkg/webhookhandler/validator_test.go
@@ -18,9 +18,10 @@ package webhookhandler
 import (
 	"context"
 	"errors"
-	"k8s.io/apimachinery/pkg/runtime/serializer"
 	"net/http"
 
+	"k8s.io/apimachinery/pkg/runtime/serializer"
+
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
 	admissionv1 "k8s.io/api/admission/v1"
@@ -28,6 +29,7 @@ import (
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/client-go/kubernetes/scheme"
 	"k8s.io/utils/pointer"
+	"k8s.io/utils/ptr"
 	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
 
 	"sigs.k8s.io/cluster-api-provider-kubevirt/api/v1alpha1"
@@ -180,6 +182,7 @@ func newRequest(operation admissionv1.Operation, oldObj, newObj *v1alpha1.Kubevi
 			Object: oldObj,
 		}
 	case admissionv1.Update:
+		req.DryRun = ptr.To(false)
 		req.Object = runtime.RawExtension{
 			Raw:    []byte(runtime.EncodeOrDie(encoder, newObj)),
 			Object: oldObj,