sigs.k8s.io/cluster-api@v1.7.1/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/ptr" 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(*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 // Wait until the initial KubeadmConfigTemplate is visible in the local cache. Otherwise the test fails below. 655 g.Eventually(ctx, func(g Gomega) { 656 g.Expect(env.Get(ctx, client.ObjectKeyFromObject(kct), &bootstrapv1.KubeadmConfigTemplate{})).To(Succeed()) 657 }, 5*time.Second).Should(Succeed()) 658 659 // Enable the new defaulting logic (i.e. simulate the Cluster API update). 660 // The webhook will default the users field to `[{Name: "default-user"}]`. 661 g.Expect(env.Create(ctx, mutatingWebhookConfiguration)).To(Succeed()) 662 defer func() { 663 g.Expect(env.CleanupAndWait(ctx, mutatingWebhookConfiguration)).To(Succeed()) 664 }() 665 666 // Run defaulting on the KubeadmConfigTemplate (triggered by an "external controller") 667 // Note: We have to retry this with eventually as it seems to take a bit of time until 668 // the webhook is active. 669 if tt.defaultOriginal { 670 g.Eventually(ctx, func(g Gomega) { 671 patchKCT := &bootstrapv1.KubeadmConfigTemplate{} 672 g.Expect(env.Get(ctx, client.ObjectKeyFromObject(kct), patchKCT)).To(Succeed()) 673 674 if patchKCT.Labels == nil { 675 patchKCT.Labels = map[string]string{} 676 } 677 patchKCT.Labels["trigger"] = "update" 678 679 g.Expect(env.Patch(ctx, patchKCT, client.MergeFrom(kct))).To(Succeed()) 680 681 // Ensure patchKCT was defaulted. 682 g.Expect(env.Get(ctx, client.ObjectKeyFromObject(kct), patchKCT)).To(Succeed()) 683 g.Expect(patchKCT.Spec.Template.Spec.Users).To(BeComparableTo([]bootstrapv1.User{{Name: "default-user"}})) 684 }, 5*time.Second).Should(Succeed()) 685 } 686 // Get original for the update. 687 original := kct.DeepCopy() 688 g.Expect(env.Get(ctx, client.ObjectKeyFromObject(original), original)).To(Succeed()) 689 690 // Calculate modified for the update. 691 modified := kct.DeepCopy() 692 // Run defaulting on modified 693 // Note: We just default the modified / desired locally as we are not simulating 694 // an entire ClusterClass. Defaulting on the template of the ClusterClass would 695 // lead to the modified object having the defaults. 696 if tt.defaultModified { 697 defaultKubeadmConfigTemplate(modified) 698 } 699 700 // Apply modified. 701 p0, err = NewServerSidePatchHelper(ctx, original, modified, env.GetClient(), ssaCache) 702 g.Expect(err).ToNot(HaveOccurred()) 703 g.Expect(p0.HasChanges()).To(Equal(tt.expectChanges)) 704 g.Expect(p0.HasSpecChanges()).To(Equal(tt.expectSpecChanges)) 705 g.Expect(p0.Patch(ctx)).To(Succeed()) 706 707 // Verify field ownership 708 // Note: It might take a bit for the cache to be up-to-date. 709 g.Eventually(func(g Gomega) { 710 got := original.DeepCopy() 711 g.Expect(env.Get(ctx, client.ObjectKeyFromObject(got), got)).To(Succeed()) 712 713 // topology controller should express opinions on spec.template.spec. 714 fieldV1 := getTopologyManagedFields(got) 715 g.Expect(fieldV1).ToNot(BeEmpty()) 716 g.Expect(fieldV1).To(HaveKey("f:spec")) 717 specFieldV1 := fieldV1["f:spec"].(map[string]interface{}) 718 g.Expect(specFieldV1).ToNot(BeEmpty()) 719 g.Expect(specFieldV1).To(HaveKey("f:template")) 720 specTemplateFieldV1 := specFieldV1["f:template"].(map[string]interface{}) 721 g.Expect(specTemplateFieldV1).ToNot(BeEmpty()) 722 g.Expect(specTemplateFieldV1).To(HaveKey("f:spec")) 723 724 specTemplateSpecFieldV1 := specTemplateFieldV1["f:spec"].(map[string]interface{}) 725 if tt.expectFieldOwnership { 726 // topology controller should express opinions on spec.template.spec.users. 727 g.Expect(specTemplateSpecFieldV1).To(HaveKey("f:users")) 728 } else { 729 // topology controller should not express opinions on spec.template.spec.users. 730 g.Expect(specTemplateSpecFieldV1).ToNot(HaveKey("f:users")) 731 } 732 }, 2*time.Second).Should(Succeed()) 733 734 if p0.HasChanges() { 735 // If there were changes the request should not be cached. 736 // Which means on the next call we should not hit the cache and thus 737 // send a request to the server. 738 // We verify this by checking the webhook call counter. 739 740 // Get original. 741 original = kct.DeepCopy() 742 g.Expect(env.Get(ctx, client.ObjectKeyFromObject(original), original)).To(Succeed()) 743 744 countBefore := defaulter.Counter 745 746 // Apply modified again. 747 p0, err = NewServerSidePatchHelper(ctx, original, modified, env.GetClient(), ssaCache) 748 g.Expect(err).ToNot(HaveOccurred()) 749 750 // Expect no changes. 751 g.Expect(p0.HasChanges()).To(BeFalse()) 752 g.Expect(p0.HasSpecChanges()).To(BeFalse()) 753 g.Expect(p0.Patch(ctx)).To(Succeed()) 754 755 // Expect webhook to be called. 756 g.Expect(defaulter.Counter).To(Equal(countBefore+2), 757 "request should not have been cached and thus we expect the webhook to be called twice (once for original and once for modified)") 758 759 // Note: Now the request is also cached, which we verify below. 760 } 761 762 // If there were no changes the request is now cached. 763 // Which means on the next call we should only hit the cache and thus 764 // don't send a request to the server. 765 // We verify this by checking the webhook call counter. 766 767 // Get original. 768 original = kct.DeepCopy() 769 g.Expect(env.Get(ctx, client.ObjectKeyFromObject(original), original)).To(Succeed()) 770 771 countBefore := defaulter.Counter 772 773 // Apply modified again. 774 p0, err = NewServerSidePatchHelper(ctx, original, modified, env.GetClient(), ssaCache) 775 g.Expect(err).ToNot(HaveOccurred()) 776 777 // Expect no changes. 778 g.Expect(p0.HasChanges()).To(BeFalse()) 779 g.Expect(p0.HasSpecChanges()).To(BeFalse()) 780 g.Expect(p0.Patch(ctx)).To(Succeed()) 781 782 // Expect webhook to not be called. 783 g.Expect(defaulter.Counter).To(Equal(countBefore), 784 "request should have been cached and thus the webhook not called") 785 }) 786 } 787 } 788 789 // setupWebhookWithManager configures the envtest manager / webhook server to serve the webhook. 790 // It also calculates and returns the corresponding MutatingWebhookConfiguration. 791 // Note: To activate the webhook, the MutatingWebhookConfiguration has to be deployed. 792 func setupWebhookWithManager(ns *corev1.Namespace) (*KubeadmConfigTemplateTestDefaulter, *admissionv1.MutatingWebhookConfiguration, error) { 793 webhookServer := env.Manager.GetWebhookServer().(*webhook.DefaultServer) 794 795 // Calculate webhook host and path. 796 // Note: This is done the same way as in our envtest package. 797 webhookPath := fmt.Sprintf("/%s/ssa-defaulting-webhook", ns.Name) 798 webhookHost := "127.0.0.1" 799 if host := os.Getenv("CAPI_WEBHOOK_HOSTNAME"); host != "" { 800 webhookHost = host 801 } 802 803 // Serve KubeadmConfigTemplateTestDefaulter on the webhook server. 804 // Note: This should only ever be called once with the same path, otherwise we get a panic. 805 defaulter := &KubeadmConfigTemplateTestDefaulter{} 806 webhookServer.Register(webhookPath, 807 admission.WithCustomDefaulter(env.Manager.GetScheme(), &bootstrapv1.KubeadmConfigTemplate{}, defaulter)) 808 809 // Calculate the MutatingWebhookConfiguration 810 caBundle, err := os.ReadFile(filepath.Join(webhookServer.Options.CertDir, webhookServer.Options.CertName)) 811 if err != nil { 812 return nil, nil, err 813 } 814 815 sideEffectNone := admissionv1.SideEffectClassNone 816 webhookConfig := &admissionv1.MutatingWebhookConfiguration{ 817 ObjectMeta: metav1.ObjectMeta{ 818 Name: ns.Name + "-webhook-config", 819 }, 820 Webhooks: []admissionv1.MutatingWebhook{ 821 { 822 Name: ns.Name + ".kubeadmconfigtemplate.bootstrap.cluster.x-k8s.io", 823 ClientConfig: admissionv1.WebhookClientConfig{ 824 URL: ptr.To(fmt.Sprintf("https://%s%s", net.JoinHostPort(webhookHost, strconv.Itoa(webhookServer.Options.Port)), webhookPath)), 825 CABundle: caBundle, 826 }, 827 Rules: []admissionv1.RuleWithOperations{ 828 { 829 Operations: []admissionv1.OperationType{ 830 admissionv1.Create, 831 admissionv1.Update, 832 }, 833 Rule: admissionv1.Rule{ 834 APIGroups: []string{bootstrapv1.GroupVersion.Group}, 835 APIVersions: []string{bootstrapv1.GroupVersion.Version}, 836 Resources: []string{"kubeadmconfigtemplates"}, 837 }, 838 }, 839 }, 840 NamespaceSelector: &metav1.LabelSelector{ 841 MatchLabels: map[string]string{ 842 corev1.LabelMetadataName: ns.Name, 843 }, 844 }, 845 AdmissionReviewVersions: []string{"v1"}, 846 SideEffects: &sideEffectNone, 847 }, 848 }, 849 } 850 return defaulter, webhookConfig, nil 851 } 852 853 var _ webhook.CustomDefaulter = &KubeadmConfigTemplateTestDefaulter{} 854 855 type KubeadmConfigTemplateTestDefaulter struct { 856 Counter int 857 } 858 859 func (d *KubeadmConfigTemplateTestDefaulter) Default(_ context.Context, obj runtime.Object) error { 860 kct, ok := obj.(*bootstrapv1.KubeadmConfigTemplate) 861 if !ok { 862 return apierrors.NewBadRequest(fmt.Sprintf("expected a Cluster but got a %T", obj)) 863 } 864 865 d.Counter++ 866 867 defaultKubeadmConfigTemplate(kct) 868 return nil 869 } 870 871 func defaultKubeadmConfigTemplate(kct *bootstrapv1.KubeadmConfigTemplate) { 872 if len(kct.Spec.Template.Spec.Users) == 0 { 873 kct.Spec.Template.Spec.Users = []bootstrapv1.User{ 874 { 875 Name: "default-user", 876 }, 877 } 878 } 879 }