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  }