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 }