sigs.k8s.io/cluster-api@v1.6.3/internal/controllers/topology/cluster/structuredmerge/serversidepathhelper_test.go (about)

     1  /*
     2  Copyright 2022 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package structuredmerge
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  	"net"
    24  	"os"
    25  	"path/filepath"
    26  	"strconv"
    27  	"testing"
    28  	"time"
    29  
    30  	. "github.com/onsi/gomega"
    31  	admissionv1 "k8s.io/api/admissionregistration/v1"
    32  	corev1 "k8s.io/api/core/v1"
    33  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    34  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    35  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    36  	"k8s.io/apimachinery/pkg/runtime"
    37  	"k8s.io/utils/pointer"
    38  	"sigs.k8s.io/controller-runtime/pkg/client"
    39  	"sigs.k8s.io/controller-runtime/pkg/webhook"
    40  	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
    41  
    42  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    43  	bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1"
    44  	"sigs.k8s.io/cluster-api/internal/test/builder"
    45  	"sigs.k8s.io/cluster-api/internal/util/ssa"
    46  	"sigs.k8s.io/cluster-api/util/patch"
    47  )
    48  
    49  // NOTE: This test ensures the ServerSideApply works as expected when the object is co-authored by other controllers.
    50  func TestServerSideApply(t *testing.T) {
    51  	g := NewWithT(t)
    52  
    53  	// Write the config file to access the test env for debugging.
    54  	// g.Expect(os.WriteFile("test.conf", kubeconfig.FromEnvTestConfig(env.Config, &clusterv1.Cluster{
    55  	// 	ObjectMeta: metav1.ObjectMeta{Name: "test"},
    56  	// }), 0777)).To(Succeed())
    57  
    58  	// Create a namespace for running the test
    59  	ns, err := env.CreateNamespace(ctx, "ssa")
    60  	g.Expect(err).ToNot(HaveOccurred())
    61  
    62  	// Build the test object to work with.
    63  	obj := builder.TestInfrastructureCluster(ns.Name, "obj1").WithSpecFields(map[string]interface{}{
    64  		"spec.controlPlaneEndpoint.host": "1.2.3.4",
    65  		"spec.controlPlaneEndpoint.port": int64(1234),
    66  		"spec.foo":                       "", // this field is then explicitly ignored by the patch helper
    67  	}).Build()
    68  	g.Expect(unstructured.SetNestedField(obj.Object, "", "status", "foo")).To(Succeed()) // this field is then ignored by the patch helper (not allowed path).
    69  
    70  	t.Run("Server side apply detect changes on object creation (unstructured)", func(t *testing.T) {
    71  		g := NewWithT(t)
    72  
    73  		var original *unstructured.Unstructured
    74  		modified := obj.DeepCopy()
    75  
    76  		p0, err := NewServerSidePatchHelper(ctx, original, modified, env.GetClient(), ssa.NewCache())
    77  		g.Expect(err).ToNot(HaveOccurred())
    78  		g.Expect(p0.HasChanges()).To(BeTrue())
    79  		g.Expect(p0.HasSpecChanges()).To(BeTrue())
    80  	})
    81  	t.Run("Server side apply detect changes on object creation (typed)", func(t *testing.T) {
    82  		g := NewWithT(t)
    83  
    84  		var original *clusterv1.MachineDeployment
    85  		modified := obj.DeepCopy()
    86  
    87  		p0, err := NewServerSidePatchHelper(ctx, original, modified, env.GetClient(), ssa.NewCache())
    88  		g.Expect(err).ToNot(HaveOccurred())
    89  		g.Expect(p0.HasChanges()).To(BeTrue())
    90  		g.Expect(p0.HasSpecChanges()).To(BeTrue())
    91  	})
    92  	t.Run("When creating an object using server side apply, it should track managed fields for the topology controller", func(t *testing.T) {
    93  		g := NewWithT(t)
    94  
    95  		// Create a patch helper with original == nil and modified == obj, ensure this is detected as operation that triggers changes.
    96  		p0, err := NewServerSidePatchHelper(ctx, nil, obj.DeepCopy(), env.GetClient(), ssa.NewCache(), IgnorePaths{{"spec", "foo"}})
    97  		g.Expect(err).ToNot(HaveOccurred())
    98  		g.Expect(p0.HasChanges()).To(BeTrue())
    99  		g.Expect(p0.HasSpecChanges()).To(BeTrue())
   100  
   101  		// Create the object using server side apply
   102  		g.Expect(p0.Patch(ctx)).To(Succeed())
   103  
   104  		// Check the object and verify managed field are properly set.
   105  		got := obj.DeepCopy()
   106  		g.Expect(env.GetAPIReader().Get(ctx, client.ObjectKeyFromObject(got), got)).To(Succeed())
   107  		fieldV1 := getTopologyManagedFields(got)
   108  		g.Expect(fieldV1).ToNot(BeEmpty())
   109  		g.Expect(fieldV1).To(HaveKey("f:spec"))      // topology controller should express opinions on spec.
   110  		g.Expect(fieldV1).ToNot(HaveKey("f:status")) // topology controller should not express opinions on status/not allowed paths.
   111  
   112  		specFieldV1 := fieldV1["f:spec"].(map[string]interface{})
   113  		g.Expect(specFieldV1).ToNot(BeEmpty())
   114  		g.Expect(specFieldV1).To(HaveKey("f:controlPlaneEndpoint")) // topology controller should express opinions on spec.controlPlaneEndpoint.
   115  		g.Expect(specFieldV1).ToNot(HaveKey("f:foo"))               // topology controller should not express opinions on ignore paths.
   116  
   117  		controlPlaneEndpointFieldV1 := specFieldV1["f:controlPlaneEndpoint"].(map[string]interface{})
   118  		g.Expect(controlPlaneEndpointFieldV1).ToNot(BeEmpty())
   119  		g.Expect(controlPlaneEndpointFieldV1).To(HaveKey("f:host")) // topology controller should express opinions on spec.controlPlaneEndpoint.host.
   120  		g.Expect(controlPlaneEndpointFieldV1).To(HaveKey("f:port")) // topology controller should express opinions on spec.controlPlaneEndpoint.port.
   121  	})
   122  	t.Run("Server side apply patch helper detects no changes", func(t *testing.T) {
   123  		g := NewWithT(t)
   124  
   125  		// Get the current object (assumes tests to be run in sequence).
   126  		original := obj.DeepCopy()
   127  		g.Expect(env.GetAPIReader().Get(ctx, client.ObjectKeyFromObject(original), original)).To(Succeed())
   128  
   129  		// Create a patch helper for a modified object with no changes.
   130  		modified := obj.DeepCopy()
   131  		p0, err := NewServerSidePatchHelper(ctx, original, modified, env.GetClient(), ssa.NewCache(), IgnorePaths{{"spec", "foo"}})
   132  		g.Expect(err).ToNot(HaveOccurred())
   133  		g.Expect(p0.HasChanges()).To(BeFalse())
   134  		g.Expect(p0.HasSpecChanges()).To(BeFalse())
   135  	})
   136  
   137  	t.Run("Server side apply patch helper discard changes in not allowed fields, e.g. status", func(t *testing.T) {
   138  		g := NewWithT(t)
   139  
   140  		// Get the current object (assumes tests to be run in sequence).
   141  		original := obj.DeepCopy()
   142  		g.Expect(env.GetAPIReader().Get(ctx, client.ObjectKeyFromObject(original), original)).To(Succeed())
   143  
   144  		// Create a patch helper for a modified object with changes only in status.
   145  		modified := obj.DeepCopy()
   146  		g.Expect(unstructured.SetNestedField(modified.Object, "changed", "status", "foo")).To(Succeed())
   147  
   148  		p0, err := NewServerSidePatchHelper(ctx, original, modified, env.GetClient(), ssa.NewCache(), IgnorePaths{{"spec", "foo"}})
   149  		g.Expect(err).ToNot(HaveOccurred())
   150  		g.Expect(p0.HasChanges()).To(BeFalse())
   151  		g.Expect(p0.HasSpecChanges()).To(BeFalse())
   152  	})
   153  
   154  	t.Run("Server side apply patch helper detect changes", func(t *testing.T) {
   155  		g := NewWithT(t)
   156  
   157  		// Get the current object (assumes tests to be run in sequence).
   158  		original := obj.DeepCopy()
   159  		g.Expect(env.GetAPIReader().Get(ctx, client.ObjectKeyFromObject(original), original)).To(Succeed())
   160  
   161  		// Create a patch helper for a modified object with changes in spec.
   162  		modified := obj.DeepCopy()
   163  		g.Expect(unstructured.SetNestedField(modified.Object, "changed", "spec", "bar")).To(Succeed())
   164  
   165  		p0, err := NewServerSidePatchHelper(ctx, original, modified, env.GetClient(), ssa.NewCache(), IgnorePaths{{"spec", "foo"}})
   166  		g.Expect(err).ToNot(HaveOccurred())
   167  		g.Expect(p0.HasChanges()).To(BeTrue())
   168  		g.Expect(p0.HasSpecChanges()).To(BeTrue())
   169  	})
   170  
   171  	t.Run("Server side apply patch helper detect changes impacting only metadata.labels", func(t *testing.T) {
   172  		g := NewWithT(t)
   173  
   174  		// Get the current object (assumes tests to be run in sequence).
   175  		original := obj.DeepCopy()
   176  		g.Expect(env.GetAPIReader().Get(ctx, client.ObjectKeyFromObject(original), original)).To(Succeed())
   177  
   178  		// Create a patch helper for a modified object with changes only in metadata.
   179  		modified := obj.DeepCopy()
   180  		modified.SetLabels(map[string]string{"foo": "changed"})
   181  
   182  		p0, err := NewServerSidePatchHelper(ctx, original, modified, env.GetClient(), ssa.NewCache(), IgnorePaths{{"spec", "foo"}})
   183  		g.Expect(err).ToNot(HaveOccurred())
   184  		g.Expect(p0.HasChanges()).To(BeTrue())
   185  		g.Expect(p0.HasSpecChanges()).To(BeFalse())
   186  	})
   187  
   188  	t.Run("Server side apply patch helper detect changes impacting only metadata.annotations", func(t *testing.T) {
   189  		g := NewWithT(t)
   190  
   191  		// Get the current object (assumes tests to be run in sequence).
   192  		original := obj.DeepCopy()
   193  		g.Expect(env.GetAPIReader().Get(ctx, client.ObjectKeyFromObject(original), original)).To(Succeed())
   194  
   195  		// Create a patch helper for a modified object with changes only in metadata.
   196  		modified := obj.DeepCopy()
   197  		modified.SetAnnotations(map[string]string{"foo": "changed"})
   198  
   199  		p0, err := NewServerSidePatchHelper(ctx, original, modified, env.GetClient(), ssa.NewCache(), IgnorePaths{{"spec", "foo"}})
   200  		g.Expect(err).ToNot(HaveOccurred())
   201  		g.Expect(p0.HasChanges()).To(BeTrue())
   202  		g.Expect(p0.HasSpecChanges()).To(BeFalse())
   203  	})
   204  
   205  	t.Run("Server side apply patch helper detect changes impacting only metadata.ownerReferences", func(t *testing.T) {
   206  		g := NewWithT(t)
   207  
   208  		// Get the current object (assumes tests to be run in sequence).
   209  		original := obj.DeepCopy()
   210  		g.Expect(env.GetAPIReader().Get(ctx, client.ObjectKeyFromObject(original), original)).To(Succeed())
   211  
   212  		// Create a patch helper for a modified object with changes only in metadata.
   213  		modified := obj.DeepCopy()
   214  		modified.SetOwnerReferences([]metav1.OwnerReference{
   215  			{
   216  				APIVersion: "foo/v1alpha1",
   217  				Kind:       "foo",
   218  				Name:       "foo",
   219  				UID:        "foo",
   220  			},
   221  		})
   222  
   223  		p0, err := NewServerSidePatchHelper(ctx, original, modified, env.GetClient(), ssa.NewCache(), IgnorePaths{{"spec", "foo"}})
   224  		g.Expect(err).ToNot(HaveOccurred())
   225  		g.Expect(p0.HasChanges()).To(BeTrue())
   226  		g.Expect(p0.HasSpecChanges()).To(BeFalse())
   227  	})
   228  
   229  	t.Run("Server side apply patch helper discard changes in ignore paths", func(t *testing.T) {
   230  		g := NewWithT(t)
   231  
   232  		// Get the current object (assumes tests to be run in sequence).
   233  		original := obj.DeepCopy()
   234  		g.Expect(env.GetAPIReader().Get(ctx, client.ObjectKeyFromObject(original), original)).To(Succeed())
   235  
   236  		// Create a patch helper for a modified object with changes only in an ignoredField.
   237  		modified := obj.DeepCopy()
   238  		g.Expect(unstructured.SetNestedField(modified.Object, "changed", "spec", "foo")).To(Succeed())
   239  
   240  		p0, err := NewServerSidePatchHelper(ctx, original, modified, env.GetClient(), ssa.NewCache(), IgnorePaths{{"spec", "foo"}})
   241  		g.Expect(err).ToNot(HaveOccurred())
   242  		g.Expect(p0.HasChanges()).To(BeFalse())
   243  		g.Expect(p0.HasSpecChanges()).To(BeFalse())
   244  	})
   245  
   246  	t.Run("Another controller applies changes", func(t *testing.T) {
   247  		g := NewWithT(t)
   248  
   249  		// Get the current object (assumes tests to be run in sequence).
   250  		obj := obj.DeepCopy()
   251  		g.Expect(env.GetAPIReader().Get(ctx, client.ObjectKeyFromObject(obj), obj)).To(Succeed())
   252  
   253  		// Store object before another controller applies changes.
   254  		original := obj.DeepCopy()
   255  
   256  		// Create a patch helper like we do/recommend doing in the controllers and use it to apply some changes.
   257  		p, err := patch.NewHelper(obj, env.Client)
   258  		g.Expect(err).ToNot(HaveOccurred())
   259  
   260  		g.Expect(unstructured.SetNestedField(obj.Object, "changed", "spec", "foo")).To(Succeed())   // Controller sets a well known field ignored in the topology controller
   261  		g.Expect(unstructured.SetNestedField(obj.Object, "changed", "spec", "bar")).To(Succeed())   // Controller sets an infra specific field the topology controller is not aware of
   262  		g.Expect(unstructured.SetNestedField(obj.Object, "changed", "status", "foo")).To(Succeed()) // Controller sets something in status
   263  		g.Expect(unstructured.SetNestedField(obj.Object, true, "status", "ready")).To(Succeed())    // Required field
   264  
   265  		g.Expect(p.Patch(ctx, obj)).To(Succeed())
   266  
   267  		// Verify that the topology controller detects no changes after another controller changed fields.
   268  		// Note: We verify here that the ServerSidePatchHelper ignores changes in managed fields of other controllers.
   269  		//       There's also a change in .spec.bar that is intentionally not ignored by the controller, we have to ignore
   270  		//       it here to be able to verify that managed field changes are ignored. This is the same situation as when
   271  		//       other controllers update .status (that is ignored) and the ServerSidePatchHelper then ignores the corresponding
   272  		//       managed field changes.
   273  		p0, err := NewServerSidePatchHelper(ctx, original, original, env.GetClient(), ssa.NewCache(), IgnorePaths{{"spec", "foo"}, {"spec", "bar"}})
   274  		g.Expect(err).ToNot(HaveOccurred())
   275  		g.Expect(p0.HasChanges()).To(BeFalse())
   276  		g.Expect(p0.HasSpecChanges()).To(BeFalse())
   277  	})
   278  
   279  	t.Run("Topology controller reconcile again with no changes on topology managed fields", func(t *testing.T) {
   280  		g := NewWithT(t)
   281  
   282  		// Get the current object (assumes tests to be run in sequence).
   283  		original := obj.DeepCopy()
   284  		g.Expect(env.GetAPIReader().Get(ctx, client.ObjectKeyFromObject(original), original)).To(Succeed())
   285  
   286  		// Create a patch helper for a modified object with no changes to what previously applied by th topology manager.
   287  		modified := obj.DeepCopy()
   288  
   289  		p0, err := NewServerSidePatchHelper(ctx, original, modified, env.GetClient(), ssa.NewCache(), IgnorePaths{{"spec", "foo"}})
   290  		g.Expect(err).ToNot(HaveOccurred())
   291  		g.Expect(p0.HasChanges()).To(BeFalse())
   292  		g.Expect(p0.HasSpecChanges()).To(BeFalse())
   293  
   294  		// Change the object using server side apply
   295  		g.Expect(p0.Patch(ctx)).To(Succeed())
   296  
   297  		// Check the object and verify fields set by the other controller are preserved.
   298  		got := obj.DeepCopy()
   299  		g.Expect(env.GetAPIReader().Get(ctx, client.ObjectKeyFromObject(got), got)).To(Succeed())
   300  
   301  		// Check if resourceVersion stayed the same
   302  		g.Expect(got.GetResourceVersion()).To(Equal(original.GetResourceVersion()))
   303  
   304  		v1, _, _ := unstructured.NestedString(got.Object, "spec", "foo")
   305  		g.Expect(v1).To(Equal("changed"))
   306  		v2, _, _ := unstructured.NestedString(got.Object, "spec", "bar")
   307  		g.Expect(v2).To(Equal("changed"))
   308  		v3, _, _ := unstructured.NestedString(got.Object, "status", "foo")
   309  		g.Expect(v3).To(Equal("changed"))
   310  		v4, _, _ := unstructured.NestedBool(got.Object, "status", "ready")
   311  		g.Expect(v4).To(BeTrue())
   312  
   313  		fieldV1 := getTopologyManagedFields(got)
   314  		g.Expect(fieldV1).ToNot(BeEmpty())
   315  		g.Expect(fieldV1).To(HaveKey("f:spec"))      // topology controller should express opinions on spec.
   316  		g.Expect(fieldV1).ToNot(HaveKey("f:status")) // topology controller should not express opinions on status/not allowed paths.
   317  
   318  		specFieldV1 := fieldV1["f:spec"].(map[string]interface{})
   319  		g.Expect(specFieldV1).ToNot(BeEmpty())
   320  		g.Expect(specFieldV1).To(HaveKey("f:controlPlaneEndpoint")) // topology controller should express opinions on spec.controlPlaneEndpoint.
   321  		g.Expect(specFieldV1).ToNot(HaveKey("f:foo"))               // topology controller should not express opinions on ignore paths.
   322  		g.Expect(specFieldV1).ToNot(HaveKey("f:bar"))               // topology controller should not express opinions on fields managed by other controllers.
   323  	})
   324  
   325  	t.Run("Topology controller reconcile again with some changes on topology managed fields", func(t *testing.T) {
   326  		g := NewWithT(t)
   327  
   328  		// Get the current object (assumes tests to be run in sequence).
   329  		original := obj.DeepCopy()
   330  		g.Expect(env.GetAPIReader().Get(ctx, client.ObjectKeyFromObject(original), original)).To(Succeed())
   331  
   332  		// Create a patch helper for a modified object with some changes to what previously applied by th topology manager.
   333  		modified := obj.DeepCopy()
   334  		g.Expect(unstructured.SetNestedField(modified.Object, "changed", "spec", "controlPlaneEndpoint", "host")).To(Succeed())
   335  
   336  		p0, err := NewServerSidePatchHelper(ctx, original, modified, env.GetClient(), ssa.NewCache(), IgnorePaths{{"spec", "foo"}})
   337  		g.Expect(err).ToNot(HaveOccurred())
   338  		g.Expect(p0.HasChanges()).To(BeTrue())
   339  		g.Expect(p0.HasSpecChanges()).To(BeTrue())
   340  
   341  		// Create the object using server side apply
   342  		g.Expect(p0.Patch(ctx)).To(Succeed())
   343  
   344  		// Check the object and verify the change is applied as well as the fields set by the other controller are still preserved.
   345  		got := obj.DeepCopy()
   346  		g.Expect(env.GetAPIReader().Get(ctx, client.ObjectKeyFromObject(got), got)).To(Succeed())
   347  
   348  		// Check if resourceVersion did change
   349  		g.Expect(got.GetResourceVersion()).ToNot(Equal(original.GetResourceVersion()))
   350  
   351  		v0, _, _ := unstructured.NestedString(got.Object, "spec", "controlPlaneEndpoint", "host")
   352  		g.Expect(v0).To(Equal("changed"))
   353  		v1, _, _ := unstructured.NestedString(got.Object, "spec", "foo")
   354  		g.Expect(v1).To(Equal("changed"))
   355  		v2, _, _ := unstructured.NestedString(got.Object, "spec", "bar")
   356  		g.Expect(v2).To(Equal("changed"))
   357  		v3, _, _ := unstructured.NestedString(got.Object, "status", "foo")
   358  		g.Expect(v3).To(Equal("changed"))
   359  		v4, _, _ := unstructured.NestedBool(got.Object, "status", "ready")
   360  		g.Expect(v4).To(BeTrue())
   361  	})
   362  	t.Run("Topology controller reconcile again with an opinion on a field managed by another controller (co-ownership)", func(t *testing.T) {
   363  		g := NewWithT(t)
   364  
   365  		// Get the current object (assumes tests to be run in sequence).
   366  		original := obj.DeepCopy()
   367  		g.Expect(env.GetAPIReader().Get(ctx, client.ObjectKeyFromObject(original), original)).To(Succeed())
   368  
   369  		// Create a patch helper for a modified object with some changes to what previously applied by th topology manager.
   370  		modified := obj.DeepCopy()
   371  		g.Expect(unstructured.SetNestedField(modified.Object, "changed", "spec", "controlPlaneEndpoint", "host")).To(Succeed())
   372  		g.Expect(unstructured.SetNestedField(modified.Object, "changed", "spec", "bar")).To(Succeed())
   373  
   374  		p0, err := NewServerSidePatchHelper(ctx, original, modified, env.GetClient(), ssa.NewCache(), IgnorePaths{{"spec", "foo"}})
   375  		g.Expect(err).ToNot(HaveOccurred())
   376  		g.Expect(p0.HasChanges()).To(BeTrue())
   377  		g.Expect(p0.HasSpecChanges()).To(BeFalse())
   378  
   379  		// Create the object using server side apply
   380  		g.Expect(p0.Patch(ctx)).To(Succeed())
   381  
   382  		// Check the object and verify the change is applied as well as managed field updated accordingly.
   383  		got := obj.DeepCopy()
   384  		g.Expect(env.GetAPIReader().Get(ctx, client.ObjectKeyFromObject(got), got)).To(Succeed())
   385  
   386  		// Check if resourceVersion did change
   387  		g.Expect(got.GetResourceVersion()).ToNot(Equal(original.GetResourceVersion()))
   388  
   389  		v2, _, _ := unstructured.NestedString(got.Object, "spec", "bar")
   390  		g.Expect(v2).To(Equal("changed"))
   391  
   392  		fieldV1 := getTopologyManagedFields(got)
   393  		g.Expect(fieldV1).ToNot(BeEmpty())
   394  		g.Expect(fieldV1).To(HaveKey("f:spec")) // topology controller should express opinions on spec.
   395  
   396  		specFieldV1 := fieldV1["f:spec"].(map[string]interface{})
   397  		g.Expect(specFieldV1).ToNot(BeEmpty())
   398  		g.Expect(specFieldV1).To(HaveKey("f:controlPlaneEndpoint")) // topology controller should express opinions on spec.controlPlaneEndpoint.
   399  		g.Expect(specFieldV1).ToNot(HaveKey("f:foo"))               // topology controller should not express opinions on ignore paths.
   400  		g.Expect(specFieldV1).To(HaveKey("f:bar"))                  // topology controller now has an opinion on a field previously managed by other controllers (force ownership).
   401  	})
   402  	t.Run("Topology controller reconcile again with an opinion on a field managed by another controller (force ownership)", func(t *testing.T) {
   403  		g := NewWithT(t)
   404  
   405  		// Get the current object (assumes tests to be run in sequence).
   406  		original := obj.DeepCopy()
   407  		g.Expect(env.GetAPIReader().Get(ctx, client.ObjectKeyFromObject(original), original)).To(Succeed())
   408  
   409  		// Create a patch helper for a modified object with some changes to what previously applied by th topology manager.
   410  		modified := obj.DeepCopy()
   411  		g.Expect(unstructured.SetNestedField(modified.Object, "changed", "spec", "controlPlaneEndpoint", "host")).To(Succeed())
   412  		g.Expect(unstructured.SetNestedField(modified.Object, "changed-by-topology-controller", "spec", "bar")).To(Succeed())
   413  
   414  		p0, err := NewServerSidePatchHelper(ctx, original, modified, env.GetClient(), ssa.NewCache(), IgnorePaths{{"spec", "foo"}})
   415  		g.Expect(err).ToNot(HaveOccurred())
   416  		g.Expect(p0.HasChanges()).To(BeTrue())
   417  		g.Expect(p0.HasSpecChanges()).To(BeTrue())
   418  
   419  		// Create the object using server side apply
   420  		g.Expect(p0.Patch(ctx)).To(Succeed())
   421  
   422  		// Check the object and verify the change is applied as well as managed field updated accordingly.
   423  		got := obj.DeepCopy()
   424  		g.Expect(env.GetAPIReader().Get(ctx, client.ObjectKeyFromObject(got), got)).To(Succeed())
   425  
   426  		// Check if resourceVersion did change
   427  		g.Expect(got.GetResourceVersion()).ToNot(Equal(original.GetResourceVersion()))
   428  
   429  		v2, _, _ := unstructured.NestedString(got.Object, "spec", "bar")
   430  		g.Expect(v2).To(Equal("changed-by-topology-controller"))
   431  
   432  		fieldV1 := getTopologyManagedFields(got)
   433  		g.Expect(fieldV1).ToNot(BeEmpty())
   434  		g.Expect(fieldV1).To(HaveKey("f:spec")) // topology controller should express opinions on spec.
   435  
   436  		specFieldV1 := fieldV1["f:spec"].(map[string]interface{})
   437  		g.Expect(specFieldV1).ToNot(BeEmpty())
   438  		g.Expect(specFieldV1).To(HaveKey("f:controlPlaneEndpoint")) // topology controller should express opinions on spec.controlPlaneEndpoint.
   439  		g.Expect(specFieldV1).ToNot(HaveKey("f:foo"))               // topology controller should not express opinions on ignore paths.
   440  		g.Expect(specFieldV1).To(HaveKey("f:bar"))                  // topology controller now has an opinion on a field previously managed by other controllers (force ownership).
   441  	})
   442  	t.Run("No-op on unstructured object having empty map[string]interface in spec", func(t *testing.T) {
   443  		g := NewWithT(t)
   444  
   445  		obj2 := builder.TestInfrastructureCluster(ns.Name, "obj2").
   446  			WithSpecFields(map[string]interface{}{
   447  				"spec.fooMap":  map[string]interface{}{},
   448  				"spec.fooList": []interface{}{},
   449  			}).
   450  			Build()
   451  
   452  		// create new object having an empty map[string]interface in spec and a copy of it for further testing
   453  		original := obj2.DeepCopy()
   454  		modified := obj2.DeepCopy()
   455  
   456  		// Create the object using server side apply
   457  		g.Expect(env.PatchAndWait(ctx, original, client.FieldOwner(TopologyManagerName))).To(Succeed())
   458  		// Get created object to have managed fields
   459  		g.Expect(env.GetAPIReader().Get(ctx, client.ObjectKeyFromObject(original), original)).To(Succeed())
   460  
   461  		// Create a patch helper for a modified object with which has no changes.
   462  		p0, err := NewServerSidePatchHelper(ctx, original, modified, env.GetClient(), ssa.NewCache())
   463  		g.Expect(err).ToNot(HaveOccurred())
   464  		g.Expect(p0.HasChanges()).To(BeFalse())
   465  		g.Expect(p0.HasSpecChanges()).To(BeFalse())
   466  	})
   467  	t.Run("Error on object which has another uid due to immutability", func(t *testing.T) {
   468  		g := NewWithT(t)
   469  
   470  		// Get the current object (assumes tests to be run in sequence).
   471  		original := obj.DeepCopy()
   472  		g.Expect(env.GetAPIReader().Get(ctx, client.ObjectKeyFromObject(original), original)).To(Succeed())
   473  
   474  		// Create a patch helper for a modified object with some changes to what previously applied by th topology manager.
   475  		modified := obj.DeepCopy()
   476  		g.Expect(unstructured.SetNestedField(modified.Object, "changed", "spec", "controlPlaneEndpoint", "host")).To(Succeed())
   477  		g.Expect(unstructured.SetNestedField(modified.Object, "changed-by-topology-controller", "spec", "bar")).To(Succeed())
   478  
   479  		// Set an other uid to original
   480  		original.SetUID("a-wrong-one")
   481  		modified.SetUID("")
   482  
   483  		// Create a patch helper which should fail because original's real UID changed.
   484  		_, err := NewServerSidePatchHelper(ctx, original, modified, env.GetClient(), ssa.NewCache())
   485  		g.Expect(err).To(HaveOccurred())
   486  	})
   487  	t.Run("Error on object which does not exist (anymore) but was expected to get updated", func(t *testing.T) {
   488  		original := builder.TestInfrastructureCluster(ns.Name, "obj3").WithSpecFields(map[string]interface{}{
   489  			"spec.controlPlaneEndpoint.host": "1.2.3.4",
   490  			"spec.controlPlaneEndpoint.port": int64(1234),
   491  			"spec.foo":                       "", // this field is then explicitly ignored by the patch helper
   492  		}).Build()
   493  
   494  		modified := original.DeepCopy()
   495  		g.Expect(unstructured.SetNestedField(modified.Object, "changed", "spec", "controlPlaneEndpoint", "host")).To(Succeed())
   496  
   497  		// Set a not existing uid to the not existing original object
   498  		original.SetUID("does-not-exist")
   499  
   500  		// Create a patch helper which should fail because original does not exist.
   501  		_, err := NewServerSidePatchHelper(ctx, original, modified, env.GetClient(), ssa.NewCache())
   502  		g.Expect(err).To(HaveOccurred())
   503  	})
   504  }
   505  
   506  // getTopologyManagedFields returns metadata.managedFields entry tracking
   507  // server side apply operations for the topology controller.
   508  func getTopologyManagedFields(original client.Object) map[string]interface{} {
   509  	r := map[string]interface{}{}
   510  
   511  	for _, m := range original.GetManagedFields() {
   512  		if m.Operation == metav1.ManagedFieldsOperationApply &&
   513  			m.Manager == TopologyManagerName &&
   514  			m.APIVersion == original.GetObjectKind().GroupVersionKind().GroupVersion().String() {
   515  			// NOTE: API server ensures this is a valid json.
   516  			err := json.Unmarshal(m.FieldsV1.Raw, &r)
   517  			if err != nil {
   518  				continue
   519  			}
   520  			break
   521  		}
   522  	}
   523  	return r
   524  }
   525  
   526  // NOTE: This test ensures that ServerSideApply works as expected when new defaulting logic is introduced by a Cluster API update.
   527  func TestServerSideApplyWithDefaulting(t *testing.T) {
   528  	g := NewWithT(t)
   529  
   530  	// Create a namespace for running the test
   531  	ns, err := env.CreateNamespace(ctx, "ssa-defaulting")
   532  	g.Expect(err).ToNot(HaveOccurred())
   533  
   534  	// Setup webhook with the manager.
   535  	// Note: The webhooks is not active yet, as the MutatingWebhookConfiguration will be deployed later.
   536  	defaulter, mutatingWebhookConfiguration, err := setupWebhookWithManager(ns)
   537  	g.Expect(err).ToNot(HaveOccurred())
   538  
   539  	// Calculate KubeadmConfigTemplate.
   540  	kct := &bootstrapv1.KubeadmConfigTemplate{
   541  		ObjectMeta: metav1.ObjectMeta{
   542  			Name:      "kct",
   543  			Namespace: ns.Name,
   544  		},
   545  		Spec: bootstrapv1.KubeadmConfigTemplateSpec{
   546  			Template: bootstrapv1.KubeadmConfigTemplateResource{
   547  				Spec: bootstrapv1.KubeadmConfigSpec{
   548  					JoinConfiguration: &bootstrapv1.JoinConfiguration{
   549  						NodeRegistration: bootstrapv1.NodeRegistrationOptions{
   550  							KubeletExtraArgs: map[string]string{
   551  								"eviction-hard": "nodefs.available<0%,nodefs.inodesFree<0%,imagefs.available<0%",
   552  							},
   553  						},
   554  					},
   555  				},
   556  			},
   557  		},
   558  	}
   559  
   560  	// The test does the following.
   561  	// 1. Create KubeadmConfigTemplate
   562  	// 2. Activate the new defaulting logic via the webhook
   563  	//   * This simulates the deployment of a new Cluster API version with new defaulting
   564  	// 3. Simulate defaulting on original and/or modified
   565  	//   * defaultOriginal will add a label to the KubeadmConfigTemplate which will trigger defaulting
   566  	//     * original is the KubeadmConfigTemplate referenced in a MachineDeployment of the Cluster topology
   567  	//   * defaultModified will simulate that defaulting was run on the KubeadmConfigTemplate referenced in the ClusterClass
   568  	//     * modified is the desired state calculated based on the KubeadmConfigTemplate referenced in the ClusterClass
   569  	//   * We are testing through all permutations as we don't want to assume on which objects defaulting was run.
   570  	// 4. Check patch helper results
   571  
   572  	// We have the following test cases:
   573  	// | original   | modified    | expect behavior                                           |
   574  	// |            |             | no-op                                                     |
   575  	// | defaulted  |             | no-op                                                     |
   576  	// |            | defaulted   | no spec changes, only take ownership of defaulted fields  |
   577  	// | defaulted  | defaulted   | no spec changes, only take ownership of defaulted fields  |
   578  	tests := []struct {
   579  		name                 string
   580  		defaultOriginal      bool
   581  		defaultModified      bool
   582  		expectChanges        bool
   583  		expectSpecChanges    bool
   584  		expectFieldOwnership bool
   585  	}{
   586  		{
   587  			name:            "no-op if neither is defaulted",
   588  			defaultOriginal: false,
   589  			defaultModified: false,
   590  			// Dry run results:
   591  			// * original: field will be defaulted by the webhook, capi-topology doesn't get ownership.
   592  			// * modified: field will be defaulted by the webhook, capi-topology doesn't get ownership.
   593  			expectChanges:        false,
   594  			expectSpecChanges:    false,
   595  			expectFieldOwnership: false,
   596  		},
   597  		{
   598  			name:            "no-op if original is defaulted",
   599  			defaultOriginal: true,
   600  			defaultModified: false,
   601  			// Dry run results:
   602  			// * original: no defaulting in dry run, as field has already been defaulted before, capi-topology doesn't get ownership.
   603  			// * modified: field will be defaulted by the webhook, capi-topology doesn't get ownership.
   604  			expectChanges:        false,
   605  			expectSpecChanges:    false,
   606  			expectFieldOwnership: false,
   607  		},
   608  		{
   609  			name:            "no spec changes, only take ownership of defaulted fields if modified is defaulted",
   610  			defaultOriginal: false,
   611  			defaultModified: true,
   612  			// Dry run results:
   613  			// * original: field will be defaulted by the webhook, capi-topology doesn't get ownership.
   614  			// * original: no defaulting in dry run, as field has already been defaulted before, capi-topology does get ownership as we explicitly set the field.
   615  			// => capi-topology takes ownership during Patch
   616  			expectChanges:        true,
   617  			expectSpecChanges:    false,
   618  			expectFieldOwnership: true,
   619  		},
   620  		{
   621  			name:            "no spec changes, only take ownership of defaulted fields if both are defaulted",
   622  			defaultOriginal: true,
   623  			defaultModified: true,
   624  			// Dry run results:
   625  			// * original: no defaulting in dry run, as field has already been defaulted before, capi-topology doesn't get ownership.
   626  			// * original: no defaulting in dry run, as field has already been defaulted before, capi-topology does get ownership as we explicitly set the field.
   627  			// => capi-topology takes ownership during Patch
   628  			expectChanges:        true,
   629  			expectSpecChanges:    false,
   630  			expectFieldOwnership: true,
   631  		},
   632  	}
   633  
   634  	for _, tt := range tests {
   635  		t.Run(tt.name, func(t *testing.T) {
   636  			g := NewWithT(t)
   637  			// Note: This is necessary because otherwise we could not create the webhook config
   638  			// in multiple test runs, because after the first test run it has a resourceVersion set.
   639  			mutatingWebhookConfiguration := mutatingWebhookConfiguration.DeepCopy()
   640  
   641  			// Create a cache to cache SSA requests.
   642  			ssaCache := ssa.NewCache()
   643  
   644  			// Create the initial KubeadmConfigTemplate (with the old defaulting logic).
   645  			p0, err := NewServerSidePatchHelper(ctx, nil, kct.DeepCopy(), env.GetClient(), ssaCache)
   646  			g.Expect(err).ToNot(HaveOccurred())
   647  			g.Expect(p0.HasChanges()).To(BeTrue())
   648  			g.Expect(p0.HasSpecChanges()).To(BeTrue())
   649  			g.Expect(p0.Patch(ctx)).To(Succeed())
   650  			defer func() {
   651  				g.Expect(env.CleanupAndWait(ctx, kct.DeepCopy())).To(Succeed())
   652  			}()
   653  
   654  			// Enable the new defaulting logic (i.e. simulate the Cluster API update).
   655  			// The webhook will default the users field to `[{Name: "default-user"}]`.
   656  			g.Expect(env.Create(ctx, mutatingWebhookConfiguration)).To(Succeed())
   657  			defer func() {
   658  				g.Expect(env.CleanupAndWait(ctx, mutatingWebhookConfiguration)).To(Succeed())
   659  			}()
   660  
   661  			// Run defaulting on the KubeadmConfigTemplate (triggered by an "external controller")
   662  			// Note: We have to retry this with eventually as it seems to take a bit of time until
   663  			// the webhook is active.
   664  			if tt.defaultOriginal {
   665  				g.Eventually(ctx, func(g Gomega) {
   666  					patchKCT := &bootstrapv1.KubeadmConfigTemplate{}
   667  					g.Expect(env.Get(ctx, client.ObjectKeyFromObject(kct), patchKCT)).To(Succeed())
   668  
   669  					if patchKCT.Labels == nil {
   670  						patchKCT.Labels = map[string]string{}
   671  					}
   672  					patchKCT.Labels["trigger"] = "update"
   673  
   674  					g.Expect(env.Patch(ctx, patchKCT, client.MergeFrom(kct))).To(Succeed())
   675  
   676  					// Ensure patchKCT was defaulted.
   677  					g.Expect(env.Get(ctx, client.ObjectKeyFromObject(kct), patchKCT)).To(Succeed())
   678  					g.Expect(patchKCT.Spec.Template.Spec.Users).To(BeComparableTo([]bootstrapv1.User{{Name: "default-user"}}))
   679  				}, 5*time.Second).Should(Succeed())
   680  			}
   681  			// Get original for the update.
   682  			original := kct.DeepCopy()
   683  			g.Expect(env.Get(ctx, client.ObjectKeyFromObject(original), original)).To(Succeed())
   684  
   685  			// Calculate modified for the update.
   686  			modified := kct.DeepCopy()
   687  			// Run defaulting on modified
   688  			// Note: We just default the modified / desired locally as we are not simulating
   689  			// an entire ClusterClass. Defaulting on the template of the ClusterClass would
   690  			// lead to the modified object having the defaults.
   691  			if tt.defaultModified {
   692  				defaultKubeadmConfigTemplate(modified)
   693  			}
   694  
   695  			// Apply modified.
   696  			p0, err = NewServerSidePatchHelper(ctx, original, modified, env.GetClient(), ssaCache)
   697  			g.Expect(err).ToNot(HaveOccurred())
   698  			g.Expect(p0.HasChanges()).To(Equal(tt.expectChanges))
   699  			g.Expect(p0.HasSpecChanges()).To(Equal(tt.expectSpecChanges))
   700  			g.Expect(p0.Patch(ctx)).To(Succeed())
   701  
   702  			// Verify field ownership
   703  			// Note: It might take a bit for the cache to be up-to-date.
   704  			g.Eventually(func(g Gomega) {
   705  				got := original.DeepCopy()
   706  				g.Expect(env.Get(ctx, client.ObjectKeyFromObject(got), got)).To(Succeed())
   707  
   708  				// topology controller should express opinions on spec.template.spec.
   709  				fieldV1 := getTopologyManagedFields(got)
   710  				g.Expect(fieldV1).ToNot(BeEmpty())
   711  				g.Expect(fieldV1).To(HaveKey("f:spec"))
   712  				specFieldV1 := fieldV1["f:spec"].(map[string]interface{})
   713  				g.Expect(specFieldV1).ToNot(BeEmpty())
   714  				g.Expect(specFieldV1).To(HaveKey("f:template"))
   715  				specTemplateFieldV1 := specFieldV1["f:template"].(map[string]interface{})
   716  				g.Expect(specTemplateFieldV1).ToNot(BeEmpty())
   717  				g.Expect(specTemplateFieldV1).To(HaveKey("f:spec"))
   718  
   719  				specTemplateSpecFieldV1 := specTemplateFieldV1["f:spec"].(map[string]interface{})
   720  				if tt.expectFieldOwnership {
   721  					// topology controller should express opinions on spec.template.spec.users.
   722  					g.Expect(specTemplateSpecFieldV1).To(HaveKey("f:users"))
   723  				} else {
   724  					// topology controller should not express opinions on spec.template.spec.users.
   725  					g.Expect(specTemplateSpecFieldV1).ToNot(HaveKey("f:users"))
   726  				}
   727  			}, 2*time.Second).Should(Succeed())
   728  
   729  			if p0.HasChanges() {
   730  				// If there were changes the request should not be cached.
   731  				// Which means on the next call we should not hit the cache and thus
   732  				// send a request to the server.
   733  				// We verify this by checking the webhook call counter.
   734  
   735  				// Get original.
   736  				original = kct.DeepCopy()
   737  				g.Expect(env.Get(ctx, client.ObjectKeyFromObject(original), original)).To(Succeed())
   738  
   739  				countBefore := defaulter.Counter
   740  
   741  				// Apply modified again.
   742  				p0, err = NewServerSidePatchHelper(ctx, original, modified, env.GetClient(), ssaCache)
   743  				g.Expect(err).ToNot(HaveOccurred())
   744  
   745  				// Expect no changes.
   746  				g.Expect(p0.HasChanges()).To(BeFalse())
   747  				g.Expect(p0.HasSpecChanges()).To(BeFalse())
   748  				g.Expect(p0.Patch(ctx)).To(Succeed())
   749  
   750  				// Expect webhook to be called.
   751  				g.Expect(defaulter.Counter).To(Equal(countBefore+2),
   752  					"request should not have been cached and thus we expect the webhook to be called twice (once for original and once for modified)")
   753  
   754  				// Note: Now the request is also cached, which we verify below.
   755  			}
   756  
   757  			// If there were no changes the request is now cached.
   758  			// Which means on the next call we should only hit the cache and thus
   759  			// don't send a request to the server.
   760  			// We verify this by checking the webhook call counter.
   761  
   762  			// Get original.
   763  			original = kct.DeepCopy()
   764  			g.Expect(env.Get(ctx, client.ObjectKeyFromObject(original), original)).To(Succeed())
   765  
   766  			countBefore := defaulter.Counter
   767  
   768  			// Apply modified again.
   769  			p0, err = NewServerSidePatchHelper(ctx, original, modified, env.GetClient(), ssaCache)
   770  			g.Expect(err).ToNot(HaveOccurred())
   771  
   772  			// Expect no changes.
   773  			g.Expect(p0.HasChanges()).To(BeFalse())
   774  			g.Expect(p0.HasSpecChanges()).To(BeFalse())
   775  			g.Expect(p0.Patch(ctx)).To(Succeed())
   776  
   777  			// Expect webhook to not be called.
   778  			g.Expect(defaulter.Counter).To(Equal(countBefore),
   779  				"request should have been cached and thus the webhook not called")
   780  		})
   781  	}
   782  }
   783  
   784  // setupWebhookWithManager configures the envtest manager / webhook server to serve the webhook.
   785  // It also calculates and returns the corresponding MutatingWebhookConfiguration.
   786  // Note: To activate the webhook, the MutatingWebhookConfiguration has to be deployed.
   787  func setupWebhookWithManager(ns *corev1.Namespace) (*KubeadmConfigTemplateTestDefaulter, *admissionv1.MutatingWebhookConfiguration, error) {
   788  	webhookServer := env.Manager.GetWebhookServer().(*webhook.DefaultServer)
   789  
   790  	// Calculate webhook host and path.
   791  	// Note: This is done the same way as in our envtest package.
   792  	webhookPath := fmt.Sprintf("/%s/ssa-defaulting-webhook", ns.Name)
   793  	webhookHost := "127.0.0.1"
   794  	if host := os.Getenv("CAPI_WEBHOOK_HOSTNAME"); host != "" {
   795  		webhookHost = host
   796  	}
   797  
   798  	// Serve KubeadmConfigTemplateTestDefaulter on the webhook server.
   799  	// Note: This should only ever be called once with the same path, otherwise we get a panic.
   800  	defaulter := &KubeadmConfigTemplateTestDefaulter{}
   801  	webhookServer.Register(webhookPath,
   802  		admission.WithCustomDefaulter(env.Manager.GetScheme(), &bootstrapv1.KubeadmConfigTemplate{}, defaulter))
   803  
   804  	// Calculate the MutatingWebhookConfiguration
   805  	caBundle, err := os.ReadFile(filepath.Join(webhookServer.Options.CertDir, webhookServer.Options.CertName))
   806  	if err != nil {
   807  		return nil, nil, err
   808  	}
   809  
   810  	sideEffectNone := admissionv1.SideEffectClassNone
   811  	webhookConfig := &admissionv1.MutatingWebhookConfiguration{
   812  		ObjectMeta: metav1.ObjectMeta{
   813  			Name: ns.Name + "-webhook-config",
   814  		},
   815  		Webhooks: []admissionv1.MutatingWebhook{
   816  			{
   817  				Name: ns.Name + ".kubeadmconfigtemplate.bootstrap.cluster.x-k8s.io",
   818  				ClientConfig: admissionv1.WebhookClientConfig{
   819  					URL:      pointer.String(fmt.Sprintf("https://%s%s", net.JoinHostPort(webhookHost, strconv.Itoa(webhookServer.Options.Port)), webhookPath)),
   820  					CABundle: caBundle,
   821  				},
   822  				Rules: []admissionv1.RuleWithOperations{
   823  					{
   824  						Operations: []admissionv1.OperationType{
   825  							admissionv1.Create,
   826  							admissionv1.Update,
   827  						},
   828  						Rule: admissionv1.Rule{
   829  							APIGroups:   []string{bootstrapv1.GroupVersion.Group},
   830  							APIVersions: []string{bootstrapv1.GroupVersion.Version},
   831  							Resources:   []string{"kubeadmconfigtemplates"},
   832  						},
   833  					},
   834  				},
   835  				NamespaceSelector: &metav1.LabelSelector{
   836  					MatchLabels: map[string]string{
   837  						corev1.LabelMetadataName: ns.Name,
   838  					},
   839  				},
   840  				AdmissionReviewVersions: []string{"v1"},
   841  				SideEffects:             &sideEffectNone,
   842  			},
   843  		},
   844  	}
   845  	return defaulter, webhookConfig, nil
   846  }
   847  
   848  var _ webhook.CustomDefaulter = &KubeadmConfigTemplateTestDefaulter{}
   849  
   850  type KubeadmConfigTemplateTestDefaulter struct {
   851  	Counter int
   852  }
   853  
   854  func (d *KubeadmConfigTemplateTestDefaulter) Default(_ context.Context, obj runtime.Object) error {
   855  	kct, ok := obj.(*bootstrapv1.KubeadmConfigTemplate)
   856  	if !ok {
   857  		return apierrors.NewBadRequest(fmt.Sprintf("expected a Cluster but got a %T", obj))
   858  	}
   859  
   860  	d.Counter++
   861  
   862  	defaultKubeadmConfigTemplate(kct)
   863  	return nil
   864  }
   865  
   866  func defaultKubeadmConfigTemplate(kct *bootstrapv1.KubeadmConfigTemplate) {
   867  	if len(kct.Spec.Template.Spec.Users) == 0 {
   868  		kct.Spec.Template.Spec.Users = []bootstrapv1.User{
   869  			{
   870  				Name: "default-user",
   871  			},
   872  		}
   873  	}
   874  }