k8s.io/kubernetes@v1.29.3/test/integration/apiserver/patch_test.go (about) 1 /* 2 Copyright 2017 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 apiserver 18 19 import ( 20 "fmt" 21 "sync" 22 "sync/atomic" 23 "testing" 24 25 "github.com/google/uuid" 26 "github.com/stretchr/testify/require" 27 28 admissionregistrationv1 "k8s.io/api/admissionregistration/v1" 29 corev1 "k8s.io/api/core/v1" 30 apierrors "k8s.io/apimachinery/pkg/api/errors" 31 "k8s.io/apimachinery/pkg/api/meta" 32 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 33 "k8s.io/apimachinery/pkg/types" 34 "k8s.io/kubernetes/test/integration/framework" 35 ) 36 37 // Tests that the apiserver retries patches 38 func TestPatchConflicts(t *testing.T) { 39 ctx, clientSet, _, tearDownFn := setup(t) 40 defer tearDownFn() 41 42 ns := framework.CreateNamespaceOrDie(clientSet, "status-code", t) 43 defer framework.DeleteNamespaceOrDie(clientSet, ns, t) 44 45 numOfConcurrentPatches := 100 46 47 UIDs := make([]types.UID, numOfConcurrentPatches) 48 ownerRefs := []metav1.OwnerReference{} 49 for i := 0; i < numOfConcurrentPatches; i++ { 50 uid := types.UID(uuid.New().String()) 51 ownerName := fmt.Sprintf("owner-%d", i) 52 UIDs[i] = uid 53 ownerRefs = append(ownerRefs, metav1.OwnerReference{ 54 APIVersion: "example.com/v1", 55 Kind: "Foo", 56 Name: ownerName, 57 UID: uid, 58 }) 59 } 60 secret := &corev1.Secret{ 61 ObjectMeta: metav1.ObjectMeta{ 62 Name: "test", 63 OwnerReferences: ownerRefs, 64 }, 65 } 66 67 // Create the object we're going to conflict on 68 _, err := clientSet.CoreV1().Secrets(ns.Name).Create(ctx, secret, metav1.CreateOptions{}) 69 if err != nil { 70 t.Fatal(err) 71 } 72 client := clientSet.CoreV1().RESTClient() 73 74 successes := int32(0) 75 76 // Run a lot of simultaneous patch operations to exercise internal API server retry of application of patches that do not specify resourceVersion. 77 // They should all succeed. 78 wg := sync.WaitGroup{} 79 for i := 0; i < numOfConcurrentPatches; i++ { 80 wg.Add(1) 81 go func(i int) { 82 defer wg.Done() 83 labelName := fmt.Sprintf("label-%d", i) 84 value := uuid.New().String() 85 86 obj, err := client.Patch(types.StrategicMergePatchType). 87 Namespace(ns.Name). 88 Resource("secrets"). 89 Name("test"). 90 Body([]byte(fmt.Sprintf(`{"metadata":{"labels":{"%s":"%s"}, "ownerReferences":[{"$patch":"delete","uid":"%s"}]}}`, labelName, value, UIDs[i]))). 91 Do(ctx). 92 Get() 93 94 if apierrors.IsConflict(err) { 95 t.Logf("tolerated conflict error patching %s: %v", "secrets", err) 96 return 97 } 98 if err != nil { 99 t.Errorf("error patching %s: %v", "secrets", err) 100 return 101 } 102 103 accessor, err := meta.Accessor(obj) 104 if err != nil { 105 t.Errorf("error getting object from %s: %v", "secrets", err) 106 return 107 } 108 // make sure the label we wanted was effective 109 if accessor.GetLabels()[labelName] != value { 110 t.Errorf("patch of %s was ineffective, expected %s=%s, got labels %#v", "secrets", labelName, value, accessor.GetLabels()) 111 return 112 } 113 // make sure the patch directive didn't get lost, and that an entry in the ownerReference list was deleted. 114 found := findOwnerRefByUID(accessor.GetOwnerReferences(), UIDs[i]) 115 if found { 116 t.Errorf("patch of %s with $patch directive was ineffective, didn't delete the entry in the ownerReference slice: %#v", "secrets", UIDs[i]) 117 } 118 119 atomic.AddInt32(&successes, 1) 120 }(i) 121 } 122 wg.Wait() 123 124 if successes < int32(numOfConcurrentPatches) { 125 t.Errorf("Expected at least %d successful patches for %s, got %d", numOfConcurrentPatches, "secrets", successes) 126 } else { 127 t.Logf("Got %d successful patches for %s", successes, "secrets") 128 } 129 130 } 131 132 func findOwnerRefByUID(ownerRefs []metav1.OwnerReference, uid types.UID) bool { 133 for _, of := range ownerRefs { 134 if of.UID == uid { 135 return true 136 } 137 } 138 return false 139 } 140 141 // Shows that a strategic merge patch with a nested patch which is merged 142 // with an empty slice is handled property 143 // https://github.com/kubernetes/kubernetes/issues/117470 144 func TestNestedStrategicMergePatchWithEmpty(t *testing.T) { 145 ctx, clientSet, _, tearDownFn := setup(t) 146 defer tearDownFn() 147 148 url := "https://foo.com" 149 se := admissionregistrationv1.SideEffectClassNone 150 151 _, err := clientSet. 152 AdmissionregistrationV1(). 153 ValidatingWebhookConfigurations(). 154 Create( 155 ctx, 156 &admissionregistrationv1.ValidatingWebhookConfiguration{ 157 ObjectMeta: metav1.ObjectMeta{ 158 Name: "base-validation", 159 }, 160 Webhooks: []admissionregistrationv1.ValidatingWebhook{ 161 { 162 AdmissionReviewVersions: []string{"v1"}, 163 ClientConfig: admissionregistrationv1.WebhookClientConfig{URL: &url}, 164 Name: "foo.bar.com", 165 SideEffects: &se, 166 }, 167 }, 168 }, 169 metav1.CreateOptions{ 170 FieldManager: "kubectl-client-side-apply", 171 FieldValidation: metav1.FieldValidationStrict, 172 }, 173 ) 174 require.NoError(t, err) 175 176 _, err = clientSet. 177 AdmissionregistrationV1(). 178 ValidatingWebhookConfigurations(). 179 Patch( 180 ctx, 181 "base-validation", 182 types.StrategicMergePatchType, 183 []byte(` 184 { 185 "webhooks": null 186 } 187 `), 188 metav1.PatchOptions{ 189 FieldManager: "kubectl-edit", 190 FieldValidation: metav1.FieldValidationStrict, 191 }, 192 ) 193 require.NoError(t, err) 194 195 // Try to apply a patch to the webhook 196 _, err = clientSet. 197 AdmissionregistrationV1(). 198 ValidatingWebhookConfigurations(). 199 Patch( 200 ctx, 201 "base-validation", 202 types.StrategicMergePatchType, 203 []byte(`{"$setElementOrder/webhooks":[{"name":"new.foo.com"}],"metadata":{"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"admissionregistration.k8s.io/v1\",\"kind\":\"ValidatingWebhookConfiguration\",\"metadata\":{\"annotations\":{},\"name\":\"base-validation\"},\"webhooks\":[{\"admissionReviewVersions\":[\"v1\"],\"clientConfig\":{\"url\":\"https://foo.com\"},\"name\":\"new.foo.com\",\"sideEffects\":\"None\"}]}\n"}},"webhooks":[{"admissionReviewVersions":["v1"],"clientConfig":{"url":"https://foo.com"},"name":"new.foo.com","sideEffects":"None"},{"$patch":"delete","name":"foo.bar.com"}]}`), 204 metav1.PatchOptions{ 205 FieldManager: "kubectl-client-side-apply", 206 FieldValidation: metav1.FieldValidationStrict, 207 }, 208 ) 209 require.NoError(t, err) 210 }