1
16
17 package apiserver
18
19 import (
20 "context"
21 "encoding/json"
22 "fmt"
23 "reflect"
24 "strings"
25 "testing"
26
27 v1 "k8s.io/api/core/v1"
28 apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
29 "k8s.io/apimachinery/pkg/api/meta"
30 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
31 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
32 "k8s.io/apimachinery/pkg/runtime/schema"
33 "k8s.io/client-go/dynamic"
34 "k8s.io/client-go/kubernetes"
35 apiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
36
37 "k8s.io/kubernetes/test/integration/etcd"
38 "k8s.io/kubernetes/test/integration/framework"
39 "k8s.io/kubernetes/test/utils/image"
40 )
41
42
43 const resetFieldsNamespace = "reset-fields-namespace"
44
45
46
47
48 var resetFieldsStatusData = map[schema.GroupVersionResource]string{
49 gvr("", "v1", "persistentvolumes"): `{"status": {"message": "hello2"}}`,
50 gvr("", "v1", "resourcequotas"): `{"status": {"used": {"cpu": "25M"}}}`,
51 gvr("", "v1", "services"): `{"status": {"loadBalancer": {"ingress": [{"ip": "127.0.0.2", "ipMode": "VIP"}]}}}`,
52 gvr("extensions", "v1beta1", "ingresses"): `{"status": {"loadBalancer": {"ingress": [{"ip": "127.0.0.2"}]}}}`,
53 gvr("networking.k8s.io", "v1beta1", "ingresses"): `{"status": {"loadBalancer": {"ingress": [{"ip": "127.0.0.2"}]}}}`,
54 gvr("networking.k8s.io", "v1", "ingresses"): `{"status": {"loadBalancer": {"ingress": [{"ip": "127.0.0.2"}]}}}`,
55 gvr("autoscaling", "v1", "horizontalpodautoscalers"): `{"status": {"currentReplicas": 25}}`,
56 gvr("autoscaling", "v2", "horizontalpodautoscalers"): `{"status": {"currentReplicas": 25}}`,
57 gvr("batch", "v1", "cronjobs"): `{"status": {"lastScheduleTime": "2020-01-01T00:00:00Z"}}`,
58 gvr("batch", "v1beta1", "cronjobs"): `{"status": {"lastScheduleTime": "2020-01-01T00:00:00Z"}}`,
59 gvr("storage.k8s.io", "v1", "volumeattachments"): `{"status": {"attached": false}}`,
60 gvr("policy", "v1", "poddisruptionbudgets"): `{"status": {"currentHealthy": 25}}`,
61 gvr("policy", "v1beta1", "poddisruptionbudgets"): `{"status": {"currentHealthy": 25}}`,
62 gvr("resource.k8s.io", "v1alpha2", "podschedulingcontexts"): `{"status": {"resourceClaims": [{"name": "my-claim", "unsuitableNodes": ["node2"]}]}}`,
63 gvr("resource.k8s.io", "v1alpha2", "resourceclaims"): `{"status": {"driverName": "other.example.com"}}`,
64 gvr("internal.apiserver.k8s.io", "v1alpha1", "storageversions"): `{"status": {"commonEncodingVersion":"v1","storageVersions":[{"apiServerID":"1","decodableVersions":["v1","v2"],"encodingVersion":"v1"}],"conditions":[{"type":"AllEncodingVersionsEqual","status":"False","lastTransitionTime":"2020-01-01T00:00:00Z","reason":"allEncodingVersionsEqual","message":"all encoding versions are set to v1"}]}}`,
65
66 gvr("admissionregistration.k8s.io", "v1alpha1", "validatingadmissionpolicies"): `{"status": {"conditions":[{"type":"Accepted","status":"True","lastTransitionTime":"2020-01-01T00:00:00Z","reason":"RuleApplied","message":"Rule was applied"}]}}`,
67 gvr("admissionregistration.k8s.io", "v1beta1", "validatingadmissionpolicies"): `{"status": {"conditions":[{"type":"Accepted","status":"True","lastTransitionTime":"2020-01-01T00:00:00Z","reason":"RuleApplied","message":"Rule was applied"}]}}`,
68 gvr("admissionregistration.k8s.io", "v1", "validatingadmissionpolicies"): `{"status": {"conditions":[{"type":"Accepted","status":"True","lastTransitionTime":"2020-01-01T00:00:00Z","reason":"RuleApplied","message":"Rule was applied"}]}}`,
69 gvr("networking.k8s.io", "v1alpha1", "servicecidrs"): `{"status": {"conditions":[{"type":"Accepted","status":"True","lastTransitionTime":"2020-01-01T00:00:00Z","reason":"RuleApplied","message":"Rule was applied"}]}}`,
70 }
71
72
73 const resetFieldsStatusDefault = `{"status": {"conditions": [{"type": "MyStatus", "status":"False"}]}}`
74
75 var resetFieldsSkippedResources = map[string]struct{}{}
76
77
78
79 var noConflicts = map[string]struct{}{
80
81
82 "certificatesigningrequests": {},
83
84
85 "storageversions": {},
86
87
88 "servicecidrs": {},
89
90
91 "namespaces": {},
92
93
94
95 "podschedulingcontexts": {},
96 }
97
98 var image2 = image.GetE2EImage(image.Etcd)
99
100
101
102
103
104
105
106 var resetFieldsSpecData = map[schema.GroupVersionResource]string{
107 gvr("", "v1", "resourcequotas"): `{"spec": {"hard": {"cpu": "25M"}}}`,
108 gvr("", "v1", "namespaces"): `{"spec": {"finalizers": ["kubernetes2"]}}`,
109 gvr("", "v1", "nodes"): `{"spec": {"unschedulable": false}}`,
110 gvr("", "v1", "persistentvolumes"): `{"spec": {"capacity": {"storage": "23M"}}}`,
111 gvr("", "v1", "persistentvolumeclaims"): `{"spec": {"resources": {"limits": {"storage": "21M"}}}}`,
112 gvr("", "v1", "pods"): `{"metadata": {"deletionTimestamp": "2020-01-01T00:00:00Z", "ownerReferences":[]}, "spec": {"containers": [{"image": "` + image2 + `", "name": "container7"}]}}`,
113 gvr("", "v1", "replicationcontrollers"): `{"spec": {"selector": {"new": "stuff2"}}}`,
114 gvr("", "v1", "resourcequotas"): `{"spec": {"hard": {"cpu": "25M"}}}`,
115 gvr("", "v1", "services"): `{"spec": {"type": "ClusterIP"}}`,
116 gvr("apps", "v1", "daemonsets"): `{"spec": {"template": {"spec": {"containers": [{"image": "` + image2 + `", "name": "container6"}]}}}}`,
117 gvr("apps", "v1", "deployments"): `{"metadata": {"labels": {"a":"c"}}, "spec": {"template": {"spec": {"containers": [{"image": "` + image2 + `", "name": "container6"}]}}}}`,
118 gvr("apps", "v1", "replicasets"): `{"spec": {"template": {"spec": {"containers": [{"image": "` + image2 + `", "name": "container4"}]}}}}`,
119 gvr("apps", "v1", "statefulsets"): `{"spec": {"selector": {"matchLabels": {"a2": "b2"}}}}`,
120 gvr("autoscaling", "v1", "horizontalpodautoscalers"): `{"spec": {"maxReplicas": 23}}`,
121 gvr("autoscaling", "v2", "horizontalpodautoscalers"): `{"spec": {"maxReplicas": 23}}`,
122 gvr("autoscaling", "v2beta1", "horizontalpodautoscalers"): `{"spec": {"maxReplicas": 23}}`,
123 gvr("autoscaling", "v2beta2", "horizontalpodautoscalers"): `{"spec": {"maxReplicas": 23}}`,
124 gvr("batch", "v1", "jobs"): `{"spec": {"template": {"spec": {"containers": [{"image": "` + image2 + `", "name": "container1"}]}}}}`,
125 gvr("batch", "v1", "cronjobs"): `{"spec": {"jobTemplate": {"spec": {"template": {"spec": {"containers": [{"image": "` + image2 + `", "name": "container0"}]}}}}}}`,
126 gvr("batch", "v1beta1", "cronjobs"): `{"spec": {"jobTemplate": {"spec": {"template": {"spec": {"containers": [{"image": "` + image2 + `", "name": "container0"}]}}}}}}`,
127 gvr("certificates.k8s.io", "v1", "certificatesigningrequests"): `{}`,
128 gvr("certificates.k8s.io", "v1beta1", "certificatesigningrequests"): `{}`,
129 gvr("flowcontrol.apiserver.k8s.io", "v1alpha1", "flowschemas"): `{"metadata": {"labels":{"a":"c"}}, "spec": {"priorityLevelConfiguration": {"name": "name2"}}}`,
130 gvr("flowcontrol.apiserver.k8s.io", "v1beta1", "flowschemas"): `{"metadata": {"labels":{"a":"c"}}, "spec": {"priorityLevelConfiguration": {"name": "name2"}}}`,
131 gvr("flowcontrol.apiserver.k8s.io", "v1beta2", "flowschemas"): `{"metadata": {"labels":{"a":"c"}}, "spec": {"priorityLevelConfiguration": {"name": "name2"}}}`,
132 gvr("flowcontrol.apiserver.k8s.io", "v1beta3", "flowschemas"): `{"metadata": {"labels":{"a":"c"}}, "spec": {"priorityLevelConfiguration": {"name": "name2"}}}`,
133 gvr("flowcontrol.apiserver.k8s.io", "v1", "flowschemas"): `{"metadata": {"labels":{"a":"c"}}, "spec": {"priorityLevelConfiguration": {"name": "name2"}}}`,
134 gvr("flowcontrol.apiserver.k8s.io", "v1alpha1", "prioritylevelconfigurations"): `{"metadata": {"labels":{"a":"c"}}, "spec": {"limited": {"assuredConcurrencyShares": 23}}}`,
135 gvr("flowcontrol.apiserver.k8s.io", "v1beta1", "prioritylevelconfigurations"): `{"metadata": {"labels":{"a":"c"}}, "spec": {"limited": {"assuredConcurrencyShares": 23}}}`,
136 gvr("flowcontrol.apiserver.k8s.io", "v1beta2", "prioritylevelconfigurations"): `{"metadata": {"labels":{"a":"c"}}, "spec": {"limited": {"assuredConcurrencyShares": 23}}}`,
137 gvr("flowcontrol.apiserver.k8s.io", "v1beta3", "prioritylevelconfigurations"): `{"metadata": {"labels":{"a":"c"}}, "spec": {"limited": {"nominalConcurrencyShares": 23}}}`,
138 gvr("flowcontrol.apiserver.k8s.io", "v1", "prioritylevelconfigurations"): `{"metadata": {"labels":{"a":"c"}}, "spec": {"limited": {"nominalConcurrencyShares": 23}}}`,
139 gvr("extensions", "v1beta1", "ingresses"): `{"spec": {"backend": {"serviceName": "service2"}}}`,
140 gvr("networking.k8s.io", "v1beta1", "ingresses"): `{"spec": {"backend": {"serviceName": "service2"}}}`,
141 gvr("networking.k8s.io", "v1", "ingresses"): `{"spec": {"defaultBackend": {"service": {"name": "service2"}}}}`,
142 gvr("networking.k8s.io", "v1alpha1", "servicecidrs"): `{}`,
143 gvr("policy", "v1", "poddisruptionbudgets"): `{"spec": {"selector": {"matchLabels": {"anokkey2": "anokvalue"}}}}`,
144 gvr("policy", "v1beta1", "poddisruptionbudgets"): `{"spec": {"selector": {"matchLabels": {"anokkey2": "anokvalue"}}}}`,
145 gvr("storage.k8s.io", "v1alpha1", "volumeattachments"): `{"metadata": {"name": "va3"}, "spec": {"nodeName": "localhost2"}}`,
146 gvr("storage.k8s.io", "v1", "volumeattachments"): `{"metadata": {"name": "va3"}, "spec": {"nodeName": "localhost2"}}`,
147 gvr("apiextensions.k8s.io", "v1", "customresourcedefinitions"): `{"metadata": {"labels":{"a":"c"}}, "spec": {"group": "webconsole22.operator.openshift.io"}}`,
148 gvr("apiextensions.k8s.io", "v1beta1", "customresourcedefinitions"): `{"metadata": {"labels":{"a":"c"}}, "spec": {"group": "webconsole22.operator.openshift.io"}}`,
149 gvr("awesome.bears.com", "v1", "pandas"): `{"spec": {"replicas": 102}}`,
150 gvr("awesome.bears.com", "v3", "pandas"): `{"spec": {"replicas": 302}}`,
151 gvr("apiregistration.k8s.io", "v1beta1", "apiservices"): `{"metadata": {"labels": {"a":"c"}}, "spec": {"group": "foo2.com"}}`,
152 gvr("apiregistration.k8s.io", "v1", "apiservices"): `{"metadata": {"labels": {"a":"c"}}, "spec": {"group": "foo2.com"}}`,
153 gvr("resource.k8s.io", "v1alpha2", "podschedulingcontexts"): `{"spec": {"selectedNode": "node2name"}}`,
154 gvr("resource.k8s.io", "v1alpha2", "resourceclasses"): `{"driverName": "other.example.com"}`,
155 gvr("resource.k8s.io", "v1alpha2", "resourceclaims"): `{"spec": {"resourceClassName": "class2name"}}`,
156 gvr("resource.k8s.io", "v1alpha2", "resourceclaimtemplates"): `{"spec": {"spec": {"resourceClassName": "class2name"}}}`,
157 gvr("internal.apiserver.k8s.io", "v1alpha1", "storageversions"): `{}`,
158 gvr("admissionregistration.k8s.io", "v1alpha1", "validatingadmissionpolicies"): `{"metadata": {"labels": {"a":"c"}}, "spec": {"paramKind": {"apiVersion": "apps/v1", "kind": "Deployment"}}}`,
159 gvr("admissionregistration.k8s.io", "v1beta1", "validatingadmissionpolicies"): `{"metadata": {"labels": {"a":"c"}}, "spec": {"paramKind": {"apiVersion": "apps/v1", "kind": "Deployment"}}}`,
160 gvr("admissionregistration.k8s.io", "v1", "validatingadmissionpolicies"): `{"metadata": {"labels": {"a":"c"}}, "spec": {"paramKind": {"apiVersion": "apps/v1", "kind": "Deployment"}}}`,
161 }
162
163
164
165
166
167
168 func TestApplyResetFields(t *testing.T) {
169 server, err := apiservertesting.StartTestServer(t, apiservertesting.NewDefaultTestServerOptions(), []string{"--disable-admission-plugins", "ServiceAccount,TaintNodesByCondition"}, framework.SharedEtcd())
170 if err != nil {
171 t.Fatal(err)
172 }
173 defer server.TearDownFn()
174
175 client, err := kubernetes.NewForConfig(server.ClientConfig)
176 if err != nil {
177 t.Fatal(err)
178 }
179 dynamicClient, err := dynamic.NewForConfig(server.ClientConfig)
180 if err != nil {
181 t.Fatal(err)
182 }
183
184
185 etcd.CreateTestCRDs(t, apiextensionsclientset.NewForConfigOrDie(server.ClientConfig), false, etcd.GetCustomResourceDefinitionData()...)
186
187 if _, err := client.CoreV1().Namespaces().Create(context.TODO(), &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: resetFieldsNamespace}}, metav1.CreateOptions{}); err != nil {
188 t.Fatal(err)
189 }
190
191 createData := etcd.GetEtcdStorageDataForNamespace(resetFieldsNamespace)
192
193 _, resourceLists, err := client.Discovery().ServerGroupsAndResources()
194 if err != nil {
195 t.Fatalf("Failed to get ServerGroupsAndResources with error: %+v", err)
196 }
197
198 for _, resourceList := range resourceLists {
199 for _, resource := range resourceList.APIResources {
200 if !strings.HasSuffix(resource.Name, "/status") {
201 continue
202 }
203 mapping, err := createMapping(resourceList.GroupVersion, resource)
204 if err != nil {
205 t.Fatal(err)
206 }
207 t.Run(mapping.Resource.String(), func(t *testing.T) {
208 if _, ok := resetFieldsSkippedResources[mapping.Resource.Resource]; ok {
209 t.Skip()
210 }
211
212 namespace := resetFieldsNamespace
213 if mapping.Scope == meta.RESTScopeRoot {
214 namespace = ""
215 }
216
217
218 status, ok := statusData[mapping.Resource]
219 if !ok {
220 status = statusDefault
221 }
222
223 resource, ok := createData[mapping.Resource]
224 if !ok {
225 t.Fatalf("no test data for %s. Please add a test for your new type to etcd.GetEtcdStorageData() or getResetFieldsEtcdStorageData()", mapping.Resource)
226 }
227
228 obj1 := unstructured.Unstructured{}
229 if err := json.Unmarshal([]byte(resource.Stub), &obj1.Object); err != nil {
230 t.Fatal(err)
231 }
232 if err := json.Unmarshal([]byte(status), &obj1.Object); err != nil {
233 t.Fatal(err)
234 }
235
236 name := obj1.GetName()
237 obj1.SetAPIVersion(mapping.GroupVersionKind.GroupVersion().String())
238 obj1.SetKind(mapping.GroupVersionKind.Kind)
239 obj1.SetName(name)
240
241
242 _, err = dynamicClient.
243 Resource(mapping.Resource).
244 Namespace(namespace).
245 Apply(context.TODO(), name, &obj1, metav1.ApplyOptions{FieldManager: "fieldmanager1"})
246 if err != nil {
247 t.Fatalf("Failed to apply obj1: %v", err)
248 }
249
250
251 obj2 := &unstructured.Unstructured{}
252 obj1.DeepCopyInto(obj2)
253 if err := json.Unmarshal([]byte(resetFieldsSpecData[mapping.Resource]), &obj2.Object); err != nil {
254 t.Fatal(err)
255 }
256 status2, ok := resetFieldsStatusData[mapping.Resource]
257 if !ok {
258 status2 = resetFieldsStatusDefault
259 }
260 if err := json.Unmarshal([]byte(status2), &obj2.Object); err != nil {
261 t.Fatal(err)
262 }
263
264 if reflect.DeepEqual(obj1, obj2) {
265 t.Fatalf("obj1 and obj2 should not be equal %v", obj2)
266 }
267
268
269
270
271 _, err = dynamicClient.
272 Resource(mapping.Resource).
273 Namespace(namespace).
274 ApplyStatus(context.TODO(), name, obj2, metav1.ApplyOptions{FieldManager: "fieldmanager2"})
275 if err != nil {
276 t.Fatalf("Failed to apply obj2: %v", err)
277 }
278
279
280
281 if _, ok = noConflicts[mapping.Resource.Resource]; !ok {
282 var objRet *unstructured.Unstructured
283
284
285
286 objRet, err = dynamicClient.
287 Resource(mapping.Resource).
288 Namespace(namespace).
289 Apply(context.TODO(), name, obj2, metav1.ApplyOptions{FieldManager: "fieldmanager2"})
290 err = expectConflict(objRet, err, dynamicClient, mapping.Resource, namespace, name)
291 if err != nil {
292 t.Fatalf("Did not get expected conflict in spec of %s %s/%s: %v", mapping.Resource, namespace, name, err)
293 }
294
295
296
297 objRet, err = dynamicClient.
298 Resource(mapping.Resource).
299 Namespace(namespace).
300 ApplyStatus(context.TODO(), name, &obj1, metav1.ApplyOptions{FieldManager: "fieldmanager1"})
301 err = expectConflict(objRet, err, dynamicClient, mapping.Resource, namespace, name)
302 if err != nil {
303 t.Fatalf("Did not get expected conflict in status of %s %s/%s: %v", mapping.Resource, namespace, name, err)
304 }
305 }
306
307
308 rsc := dynamicClient.Resource(mapping.Resource).Namespace(namespace)
309 if err := rsc.Delete(context.TODO(), name, *metav1.NewDeleteOptions(0)); err != nil {
310 t.Fatalf("deleting final object failed: %v", err)
311 }
312 })
313 }
314 }
315 }
316
317 func expectConflict(objRet *unstructured.Unstructured, err error, dynamicClient dynamic.Interface, resource schema.GroupVersionResource, namespace, name string) error {
318 if err != nil && strings.Contains(err.Error(), "conflict") {
319 return nil
320 }
321 which := "returned"
322
323 if objRet == nil {
324 which = "subsequently fetched"
325 var err2 error
326 objRet, err2 = dynamicClient.
327 Resource(resource).
328 Namespace(namespace).
329 Get(context.TODO(), name, metav1.GetOptions{})
330 if err2 != nil {
331 return fmt.Errorf("instead got error %w, and failed to Get object: %v", err, err2)
332 }
333 }
334 marshBytes, marshErr := json.Marshal(objRet)
335 var gotten string
336 if marshErr == nil {
337 gotten = string(marshBytes)
338 } else {
339 gotten = fmt.Sprintf("<failed to json.Marshall(%#+v): %v>", objRet, marshErr)
340 }
341 return fmt.Errorf("instead got error %w; %s object is %s", err, which, gotten)
342 }
343
View as plain text