1
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
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
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
77
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
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
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
142
143
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
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 }
211
View as plain text