sigs.k8s.io/cluster-api@v1.7.1/internal/util/ssa/patch_test.go (about) 1 /* 2 Copyright 2023 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 ssa 18 19 import ( 20 "testing" 21 "time" 22 23 . "github.com/onsi/gomega" 24 corev1 "k8s.io/api/core/v1" 25 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 27 "k8s.io/utils/ptr" 28 "sigs.k8s.io/controller-runtime/pkg/client" 29 30 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 31 "sigs.k8s.io/cluster-api/internal/test/builder" 32 ) 33 34 func TestPatch(t *testing.T) { 35 g := NewWithT(t) 36 37 // Create a namespace for running the test 38 ns, err := env.CreateNamespace(ctx, "ssa") 39 g.Expect(err).ToNot(HaveOccurred()) 40 41 t.Run("Test patch with unstructured", func(*testing.T) { 42 // Build the test object to work with. 43 initialObject := builder.TestInfrastructureCluster(ns.Name, "obj1").WithSpecFields(map[string]interface{}{ 44 "spec.controlPlaneEndpoint.host": "1.2.3.4", 45 "spec.controlPlaneEndpoint.port": int64(1234), 46 "spec.foo": "bar", 47 }).Build() 48 49 fieldManager := "test-manager" 50 ssaCache := NewCache() 51 52 // 1. Create the object 53 createObject := initialObject.DeepCopy() 54 g.Expect(Patch(ctx, env.GetClient(), fieldManager, createObject)).To(Succeed()) 55 56 // 2. Update the object and verify that the request was not cached as the object was changed. 57 // Get the original object. 58 originalObject := initialObject.DeepCopy() 59 g.Expect(env.GetAPIReader().Get(ctx, client.ObjectKeyFromObject(originalObject), originalObject)).To(Succeed()) 60 // Modify the object 61 modifiedObject := initialObject.DeepCopy() 62 g.Expect(unstructured.SetNestedField(modifiedObject.Object, "baz", "spec", "foo")).To(Succeed()) 63 // Compute request identifier, so we can later verify that the update call was not cached. 64 modifiedUnstructured, err := prepareModified(env.Scheme(), modifiedObject) 65 g.Expect(err).ToNot(HaveOccurred()) 66 requestIdentifier, err := ComputeRequestIdentifier(env.GetScheme(), originalObject, modifiedUnstructured) 67 g.Expect(err).ToNot(HaveOccurred()) 68 // Update the object 69 g.Expect(Patch(ctx, env.GetClient(), fieldManager, modifiedObject, WithCachingProxy{Cache: ssaCache, Original: originalObject})).To(Succeed()) 70 // Verify that request was not cached (as it changed the object) 71 g.Expect(ssaCache.Has(requestIdentifier)).To(BeFalse()) 72 73 // 3. Repeat the same update and verify that the request was cached as the object was not changed. 74 // Get the original object. 75 originalObject = initialObject.DeepCopy() 76 g.Expect(env.GetAPIReader().Get(ctx, client.ObjectKeyFromObject(originalObject), originalObject)).To(Succeed()) 77 // Modify the object 78 modifiedObject = initialObject.DeepCopy() 79 g.Expect(unstructured.SetNestedField(modifiedObject.Object, "baz", "spec", "foo")).To(Succeed()) 80 // Compute request identifier, so we can later verify that the update call was cached. 81 modifiedUnstructured, err = prepareModified(env.Scheme(), modifiedObject) 82 g.Expect(err).ToNot(HaveOccurred()) 83 requestIdentifier, err = ComputeRequestIdentifier(env.GetScheme(), originalObject, modifiedUnstructured) 84 g.Expect(err).ToNot(HaveOccurred()) 85 // Update the object 86 g.Expect(Patch(ctx, env.GetClient(), fieldManager, modifiedObject, WithCachingProxy{Cache: ssaCache, Original: originalObject})).To(Succeed()) 87 // Verify that request was cached (as it did not change the object) 88 g.Expect(ssaCache.Has(requestIdentifier)).To(BeTrue()) 89 }) 90 91 t.Run("Test patch with Machine", func(*testing.T) { 92 // Build the test object to work with. 93 initialObject := &clusterv1.Machine{ 94 TypeMeta: metav1.TypeMeta{ 95 APIVersion: clusterv1.GroupVersion.String(), 96 Kind: "Machine", 97 }, 98 ObjectMeta: metav1.ObjectMeta{ 99 Name: "machine-1", 100 Namespace: ns.Name, 101 Labels: map[string]string{ 102 "label": "labelValue", 103 }, 104 Annotations: map[string]string{ 105 "annotation": "annotationValue", 106 }, 107 }, 108 Spec: clusterv1.MachineSpec{ 109 ClusterName: "cluster-1", 110 Version: ptr.To("v1.25.0"), 111 NodeDrainTimeout: &metav1.Duration{Duration: 10 * time.Second}, 112 Bootstrap: clusterv1.Bootstrap{ 113 DataSecretName: ptr.To("data-secret"), 114 }, 115 InfrastructureRef: corev1.ObjectReference{ 116 // The namespace needs to get set here. Otherwise the defaulting webhook always sets this field again 117 // which would lead to an resourceVersion bump at the 3rd step and to a flaky test. 118 Namespace: ns.Name, 119 }, 120 }, 121 } 122 fieldManager := "test-manager" 123 ssaCache := NewCache() 124 125 // 1. Create the object 126 createObject := initialObject.DeepCopy() 127 g.Expect(Patch(ctx, env.GetClient(), fieldManager, createObject)).To(Succeed()) 128 // Verify that gvk is still set 129 g.Expect(createObject.GroupVersionKind()).To(Equal(initialObject.GroupVersionKind())) 130 // Note: We have to patch the status here to explicitly set these two status fields. 131 // If we don't do it the Machine defaulting webhook will try to set the two fields to false. 132 // For an unknown reason this will happen with the 2nd update call (3.) below and not before. 133 // This means that this call would unexpectedly not cache the object because the resourceVersion 134 // is changed because the fields are set. 135 // It's unclear why those status fields are not already set during create (1.) or the first update (2.) 136 // (the webhook is returning patches for the two fields in those requests as well). 137 // To further investigate this behavior it would be necessary to debug the kube-apiserver. 138 // Fortunately, in reality this is not an issue as the fields will be set sooner or later and then 139 // the requests are cached. 140 createObjectWithStatus := createObject.DeepCopy() 141 createObjectWithStatus.Status.BootstrapReady = false 142 createObjectWithStatus.Status.InfrastructureReady = false 143 g.Expect(env.Status().Patch(ctx, createObjectWithStatus, client.MergeFrom(createObject))).To(Succeed()) 144 145 // 2. Update the object and verify that the request was not cached as the object was changed. 146 // Get the original object. 147 originalObject := initialObject.DeepCopy() 148 g.Expect(env.GetAPIReader().Get(ctx, client.ObjectKeyFromObject(originalObject), originalObject)).To(Succeed()) 149 // Modify the object 150 modifiedObject := initialObject.DeepCopy() 151 modifiedObject.Spec.NodeDrainTimeout = &metav1.Duration{Duration: 5 * time.Second} 152 // Compute request identifier, so we can later verify that the update call was not cached. 153 modifiedUnstructured, err := prepareModified(env.Scheme(), modifiedObject) 154 g.Expect(err).ToNot(HaveOccurred()) 155 requestIdentifier, err := ComputeRequestIdentifier(env.GetScheme(), originalObject, modifiedUnstructured) 156 g.Expect(err).ToNot(HaveOccurred()) 157 // Update the object 158 g.Expect(Patch(ctx, env.GetClient(), fieldManager, modifiedObject, WithCachingProxy{Cache: ssaCache, Original: originalObject})).To(Succeed()) 159 // Verify that gvk is still set 160 g.Expect(modifiedObject.GroupVersionKind()).To(Equal(initialObject.GroupVersionKind())) 161 // Verify that request was not cached (as it changed the object) 162 g.Expect(ssaCache.Has(requestIdentifier)).To(BeFalse()) 163 164 // 3. Repeat the same update and verify that the request was cached as the object was not changed. 165 // Get the original object. 166 originalObject = initialObject.DeepCopy() 167 g.Expect(env.GetAPIReader().Get(ctx, client.ObjectKeyFromObject(originalObject), originalObject)).To(Succeed()) 168 // Modify the object 169 modifiedObject = initialObject.DeepCopy() 170 modifiedObject.Spec.NodeDrainTimeout = &metav1.Duration{Duration: 5 * time.Second} 171 // Compute request identifier, so we can later verify that the update call was cached. 172 modifiedUnstructured, err = prepareModified(env.Scheme(), modifiedObject) 173 g.Expect(err).ToNot(HaveOccurred()) 174 requestIdentifier, err = ComputeRequestIdentifier(env.GetScheme(), originalObject, modifiedUnstructured) 175 g.Expect(err).ToNot(HaveOccurred()) 176 // Update the object 177 g.Expect(Patch(ctx, env.GetClient(), fieldManager, modifiedObject, WithCachingProxy{Cache: ssaCache, Original: originalObject})).To(Succeed()) 178 // Verify that request was cached (as it did not change the object) 179 g.Expect(ssaCache.Has(requestIdentifier)).To(BeTrue()) 180 // Verify that gvk is still set 181 g.Expect(modifiedObject.GroupVersionKind()).To(Equal(initialObject.GroupVersionKind())) 182 }) 183 }