1
16
17 package apiserver
18
19 import (
20 "bytes"
21 "context"
22 "encoding/json"
23 "fmt"
24 "io"
25 "net"
26 "net/http"
27 "path"
28 "reflect"
29 "strconv"
30 "strings"
31 "sync"
32 "testing"
33 "time"
34
35 apps "k8s.io/api/apps/v1"
36 v1 "k8s.io/api/core/v1"
37 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
38 apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
39 "k8s.io/apiextensions-apiserver/test/integration/fixtures"
40 apiequality "k8s.io/apimachinery/pkg/api/equality"
41 apierrors "k8s.io/apimachinery/pkg/api/errors"
42 "k8s.io/apimachinery/pkg/api/meta"
43 metainternalversionscheme "k8s.io/apimachinery/pkg/apis/meta/internalversion/scheme"
44 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
45 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
46 metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
47 "k8s.io/apimachinery/pkg/fields"
48 "k8s.io/apimachinery/pkg/runtime"
49 "k8s.io/apimachinery/pkg/runtime/schema"
50 "k8s.io/apimachinery/pkg/runtime/serializer/protobuf"
51 "k8s.io/apimachinery/pkg/runtime/serializer/streaming"
52 "k8s.io/apimachinery/pkg/types"
53 "k8s.io/apimachinery/pkg/util/uuid"
54 "k8s.io/apimachinery/pkg/util/wait"
55 "k8s.io/apimachinery/pkg/watch"
56 "k8s.io/apiserver/pkg/endpoints/handlers"
57 "k8s.io/apiserver/pkg/storage/storagebackend"
58 "k8s.io/client-go/dynamic"
59 clientset "k8s.io/client-go/kubernetes"
60 appsv1 "k8s.io/client-go/kubernetes/typed/apps/v1"
61 "k8s.io/client-go/metadata"
62 restclient "k8s.io/client-go/rest"
63 "k8s.io/client-go/tools/pager"
64 "k8s.io/klog/v2"
65 "k8s.io/kubernetes/cmd/kube-apiserver/app/options"
66 kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
67 "k8s.io/kubernetes/pkg/controlplane"
68 "k8s.io/kubernetes/test/integration"
69 "k8s.io/kubernetes/test/integration/etcd"
70 "k8s.io/kubernetes/test/integration/framework"
71 "k8s.io/kubernetes/test/utils/ktesting"
72 )
73
74 func setup(t *testing.T, groupVersions ...schema.GroupVersion) (context.Context, clientset.Interface, *restclient.Config, framework.TearDownFunc) {
75 return setupWithResources(t, groupVersions, nil)
76 }
77
78 func setupWithResources(t *testing.T, groupVersions []schema.GroupVersion, resources []schema.GroupVersionResource) (context.Context, clientset.Interface , *restclient.Config, framework.TearDownFunc) {
79 tCtx := ktesting.Init(t)
80
81 client, config, teardown := framework.StartTestServer(tCtx, t, framework.TestServerSetup{
82 ModifyServerConfig: func(config *controlplane.Config) {
83 if len(groupVersions) > 0 || len(resources) > 0 {
84 resourceConfig := controlplane.DefaultAPIResourceConfigSource()
85 resourceConfig.EnableVersions(groupVersions...)
86 resourceConfig.EnableResources(resources...)
87 config.ExtraConfig.APIResourceConfigSource = resourceConfig
88 }
89 },
90 })
91
92 newTeardown := func() {
93 tCtx.Cancel("tearing down apiserver")
94 teardown()
95 }
96
97 return tCtx, client, config, newTeardown
98 }
99
100 func verifyStatusCode(t *testing.T, transport http.RoundTripper, verb, URL, body string, expectedStatusCode int) {
101
102 bodyBytes := bytes.NewReader([]byte(body))
103 req, err := http.NewRequest(verb, URL, bodyBytes)
104 if err != nil {
105 t.Fatalf("unexpected error: %v in sending req with verb: %s, URL: %s and body: %s", err, verb, URL, body)
106 }
107 klog.Infof("Sending request: %v", req)
108 resp, err := transport.RoundTrip(req)
109 if err != nil {
110 t.Fatalf("unexpected error: %v in req: %v", err, req)
111 }
112 defer resp.Body.Close()
113 b, _ := io.ReadAll(resp.Body)
114 if resp.StatusCode != expectedStatusCode {
115 t.Errorf("Expected status %v, but got %v", expectedStatusCode, resp.StatusCode)
116 t.Errorf("Body: %v", string(b))
117 }
118 }
119
120 func newRS(namespace string) *apps.ReplicaSet {
121 return &apps.ReplicaSet{
122 TypeMeta: metav1.TypeMeta{
123 Kind: "ReplicaSet",
124 APIVersion: "apps/v1",
125 },
126 ObjectMeta: metav1.ObjectMeta{
127 Namespace: namespace,
128 GenerateName: "apiserver-test",
129 },
130 Spec: apps.ReplicaSetSpec{
131 Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"name": "test"}},
132 Template: v1.PodTemplateSpec{
133 ObjectMeta: metav1.ObjectMeta{
134 Labels: map[string]string{"name": "test"},
135 },
136 Spec: v1.PodSpec{
137 Containers: []v1.Container{
138 {
139 Name: "fake-name",
140 Image: "fakeimage",
141 },
142 },
143 },
144 },
145 },
146 }
147 }
148
149 var cascDel = `
150 {
151 "kind": "DeleteOptions",
152 "apiVersion": "v1",
153 "orphanDependents": false
154 }
155 `
156
157 func Test4xxStatusCodeInvalidPatch(t *testing.T) {
158 ctx, client, _, tearDownFn := setup(t)
159 defer tearDownFn()
160
161 obj := []byte(`{
162 "apiVersion": "apps/v1",
163 "kind": "Deployment",
164 "metadata": {
165 "name": "deployment",
166 "labels": {"app": "nginx"}
167 },
168 "spec": {
169 "selector": {
170 "matchLabels": {
171 "app": "nginx"
172 }
173 },
174 "template": {
175 "metadata": {
176 "labels": {
177 "app": "nginx"
178 }
179 },
180 "spec": {
181 "containers": [{
182 "name": "nginx",
183 "image": "nginx:latest"
184 }]
185 }
186 }
187 }
188 }`)
189
190 resp, err := client.CoreV1().RESTClient().Post().
191 AbsPath("/apis/apps/v1").
192 Namespace("default").
193 Resource("deployments").
194 Body(obj).Do(ctx).Get()
195 if err != nil {
196 t.Fatalf("Failed to create object: %v: %v", err, resp)
197 }
198 result := client.CoreV1().RESTClient().Patch(types.MergePatchType).
199 AbsPath("/apis/apps/v1").
200 Namespace("default").
201 Resource("deployments").
202 Name("deployment").
203 Body([]byte(`{"metadata":{"annotations":{"foo":["bar"]}}}`)).Do(ctx)
204 var statusCode int
205 result.StatusCode(&statusCode)
206 if statusCode != 422 {
207 t.Fatalf("Expected status code to be 422, got %v (%#v)", statusCode, result)
208 }
209 result = client.CoreV1().RESTClient().Patch(types.StrategicMergePatchType).
210 AbsPath("/apis/apps/v1").
211 Namespace("default").
212 Resource("deployments").
213 Name("deployment").
214 Body([]byte(`{"metadata":{"annotations":{"foo":["bar"]}}}`)).Do(ctx)
215 result.StatusCode(&statusCode)
216 if statusCode != 422 {
217 t.Fatalf("Expected status code to be 422, got %v (%#v)", statusCode, result)
218 }
219 }
220
221 func TestCacheControl(t *testing.T) {
222 server := kubeapiservertesting.StartTestServerOrDie(t, nil, nil, framework.SharedEtcd())
223 defer server.TearDownFn()
224
225 rt, err := restclient.TransportFor(server.ClientConfig)
226 if err != nil {
227 t.Fatal(err)
228 }
229
230 paths := []string{
231
232 "/",
233
234 "/healthz",
235
236 "/openapi/v2",
237
238 "/api",
239 "/api/v1",
240 "/apis",
241 "/apis/apps",
242 "/apis/apps/v1",
243
244 "/api/v1/namespaces",
245 "/apis/apps/v1/deployments",
246 }
247 for _, path := range paths {
248 t.Run(path, func(t *testing.T) {
249 req, err := http.NewRequest("GET", server.ClientConfig.Host+path, nil)
250 if err != nil {
251 t.Fatal(err)
252 }
253 resp, err := rt.RoundTrip(req)
254 if err != nil {
255 t.Fatal(err)
256 }
257 defer resp.Body.Close()
258 cc := resp.Header.Get("Cache-Control")
259 if !strings.Contains(cc, "private") {
260 t.Errorf("expected private cache-control, got %q", cc)
261 }
262 })
263 }
264 }
265
266
267 func TestHSTS(t *testing.T) {
268 server := kubeapiservertesting.StartTestServerOrDie(t, nil, []string{"--strict-transport-security-directives=max-age=31536000,includeSubDomains"}, framework.SharedEtcd())
269 defer server.TearDownFn()
270
271 rt, err := restclient.TransportFor(server.ClientConfig)
272 if err != nil {
273 t.Fatal(err)
274 }
275
276 paths := []string{
277
278 "/",
279
280 "/healthz",
281
282 "/openapi/v2",
283
284 "/api",
285 "/api/v1",
286 "/apis",
287 "/apis/apps",
288 "/apis/apps/v1",
289
290 "/api/v1/namespaces",
291 "/apis/apps/v1/deployments",
292 }
293 for _, path := range paths {
294 t.Run(path, func(t *testing.T) {
295 req, err := http.NewRequest("GET", server.ClientConfig.Host+path, nil)
296 if err != nil {
297 t.Fatal(err)
298 }
299 resp, err := rt.RoundTrip(req)
300 if err != nil {
301 t.Fatal(err)
302 }
303 defer resp.Body.Close()
304 cc := resp.Header.Get("Strict-Transport-Security")
305 if !strings.Contains(cc, "max-age=31536000; includeSubDomains") {
306 t.Errorf("expected max-age=31536000; includeSubDomains, got %q", cc)
307 }
308 })
309 }
310 }
311
312
313 func Test202StatusCode(t *testing.T) {
314 ctx, clientSet, kubeConfig, tearDownFn := setup(t)
315 defer tearDownFn()
316
317 transport, err := restclient.TransportFor(kubeConfig)
318 if err != nil {
319 t.Fatal(err)
320 }
321
322 ns := framework.CreateNamespaceOrDie(clientSet, "status-code", t)
323 defer framework.DeleteNamespaceOrDie(clientSet, ns, t)
324
325 rsClient := clientSet.AppsV1().ReplicaSets(ns.Name)
326
327
328
329 rs, err := rsClient.Create(ctx, newRS(ns.Name), metav1.CreateOptions{})
330 if err != nil {
331 t.Fatalf("Failed to create rs: %v", err)
332 }
333 verifyStatusCode(t, transport, "DELETE", kubeConfig.Host+path.Join("/apis/apps/v1/namespaces", ns.Name, "replicasets", rs.Name), "", 200)
334
335
336
337 rs = newRS(ns.Name)
338 rs.ObjectMeta.Finalizers = []string{"kube.io/dummy-finalizer"}
339 rs, err = rsClient.Create(ctx, rs, metav1.CreateOptions{})
340 if err != nil {
341 t.Fatalf("Failed to create rs: %v", err)
342 }
343 verifyStatusCode(t, transport, "DELETE", kubeConfig.Host+path.Join("/apis/apps/v1/namespaces", ns.Name, "replicasets", rs.Name), "", 200)
344
345
346
347 rs = newRS(ns.Name)
348 rs, err = rsClient.Create(ctx, rs, metav1.CreateOptions{})
349 if err != nil {
350 t.Fatalf("Failed to create rs: %v", err)
351 }
352 verifyStatusCode(t, transport, "DELETE", kubeConfig.Host+path.Join("/apis/apps/v1/namespaces", ns.Name, "replicasets", rs.Name), cascDel, 200)
353
354
355
356 rs = newRS(ns.Name)
357 rs.ObjectMeta.Finalizers = []string{"kube.io/dummy-finalizer"}
358 rs, err = rsClient.Create(ctx, rs, metav1.CreateOptions{})
359 if err != nil {
360 t.Fatalf("Failed to create rs: %v", err)
361 }
362 verifyStatusCode(t, transport, "DELETE", kubeConfig.Host+path.Join("/apis/apps/v1/namespaces", ns.Name, "replicasets", rs.Name), cascDel, 202)
363 }
364
365 var (
366 invalidContinueToken = "invalidContinueToken"
367 invalidResourceVersion = "invalid"
368 invalidResourceVersionMatch = metav1.ResourceVersionMatch("InvalidMatch")
369 )
370
371
372
373 func TestListOptions(t *testing.T) {
374
375 for _, watchCacheEnabled := range []bool{true, false} {
376 t.Run(fmt.Sprintf("watchCacheEnabled=%t", watchCacheEnabled), func(t *testing.T) {
377 tCtx := ktesting.Init(t)
378
379 var storageTransport *storagebackend.TransportConfig
380 clientSet, _, tearDownFn := framework.StartTestServer(tCtx, t, framework.TestServerSetup{
381 ModifyServerRunOptions: func(opts *options.ServerRunOptions) {
382 opts.Etcd.EnableWatchCache = watchCacheEnabled
383 storageTransport = &opts.Etcd.StorageConfig.Transport
384 },
385 })
386 defer tearDownFn()
387
388 ns := framework.CreateNamespaceOrDie(clientSet, "list-options", t)
389 defer framework.DeleteNamespaceOrDie(clientSet, ns, t)
390
391 rsClient := clientSet.AppsV1().ReplicaSets(ns.Name)
392
393 var compactedRv, oldestUncompactedRv string
394 for i := 0; i < 15; i++ {
395 rs := newRS(ns.Name)
396 rs.Name = fmt.Sprintf("test-%d", i)
397 created, err := rsClient.Create(tCtx, rs, metav1.CreateOptions{})
398 if err != nil {
399 t.Fatal(err)
400 }
401 if i == 0 {
402 compactedRv = created.ResourceVersion
403 }
404
405 if i < 5 {
406 var zero int64
407 if err := rsClient.Delete(tCtx, rs.Name, metav1.DeleteOptions{GracePeriodSeconds: &zero}); err != nil {
408 t.Fatal(err)
409 }
410 oldestUncompactedRv = created.ResourceVersion
411 }
412 }
413
414
415 rawClient, kvClient, err := integration.GetEtcdClients(*storageTransport)
416 if err != nil {
417 t.Fatal(err)
418 }
419
420
421 defer rawClient.Close()
422
423 revision, err := strconv.Atoi(oldestUncompactedRv)
424 if err != nil {
425 t.Fatal(err)
426 }
427 _, err = kvClient.Compact(tCtx, int64(revision))
428 if err != nil {
429 t.Fatal(err)
430 }
431
432 listObj, err := rsClient.List(tCtx, metav1.ListOptions{
433 Limit: 6,
434 })
435 if err != nil {
436 t.Fatalf("unexpected error: %v", err)
437 }
438 validContinueToken := listObj.Continue
439
440
441 limits := []int64{0, 6}
442 continueTokens := []string{"", validContinueToken, invalidContinueToken}
443 rvs := []string{"", "0", compactedRv, invalidResourceVersion}
444 rvMatches := []metav1.ResourceVersionMatch{
445 "",
446 metav1.ResourceVersionMatchNotOlderThan,
447 metav1.ResourceVersionMatchExact,
448 invalidResourceVersionMatch,
449 }
450
451 for _, limit := range limits {
452 for _, continueToken := range continueTokens {
453 for _, rv := range rvs {
454 for _, rvMatch := range rvMatches {
455 rvName := ""
456 switch rv {
457 case "":
458 rvName = "empty"
459 case "0":
460 rvName = "0"
461 case compactedRv:
462 rvName = "compacted"
463 case invalidResourceVersion:
464 rvName = "invalid"
465 default:
466 rvName = "unknown"
467 }
468
469 continueName := ""
470 switch continueToken {
471 case "":
472 continueName = "empty"
473 case validContinueToken:
474 continueName = "valid"
475 case invalidContinueToken:
476 continueName = "invalid"
477 default:
478 continueName = "unknown"
479 }
480
481 name := fmt.Sprintf("limit=%d continue=%s rv=%s rvMatch=%s", limit, continueName, rvName, rvMatch)
482 t.Run(name, func(t *testing.T) {
483 opts := metav1.ListOptions{
484 ResourceVersion: rv,
485 ResourceVersionMatch: rvMatch,
486 Continue: continueToken,
487 Limit: limit,
488 }
489 testListOptionsCase(t, rsClient, watchCacheEnabled, opts, compactedRv)
490 })
491 }
492 }
493 }
494 }
495 })
496 }
497 }
498
499 func testListOptionsCase(t *testing.T, rsClient appsv1.ReplicaSetInterface, watchCacheEnabled bool, opts metav1.ListOptions, compactedRv string) {
500 listObj, err := rsClient.List(context.Background(), opts)
501
502
503 if opts.ResourceVersion == "" && opts.ResourceVersionMatch != "" {
504 if err == nil || !strings.Contains(err.Error(), "resourceVersionMatch is forbidden unless resourceVersion is provided") {
505 t.Fatalf("expected forbidden error, but got: %v", err)
506 }
507 return
508 }
509 if opts.Continue != "" && opts.ResourceVersionMatch != "" {
510 if err == nil || !strings.Contains(err.Error(), "resourceVersionMatch is forbidden when continue is provided") {
511 t.Fatalf("expected forbidden error, but got: %v", err)
512 }
513 return
514 }
515 if opts.ResourceVersionMatch == invalidResourceVersionMatch {
516 if err == nil || !strings.Contains(err.Error(), "supported values") {
517 t.Fatalf("expected not supported error, but got: %v", err)
518 }
519 return
520 }
521 if opts.ResourceVersionMatch == metav1.ResourceVersionMatchExact && opts.ResourceVersion == "0" {
522 if err == nil || !strings.Contains(err.Error(), "resourceVersionMatch \"exact\" is forbidden for resourceVersion \"0\"") {
523 t.Fatalf("expected forbidden error, but got: %v", err)
524 }
525 return
526 }
527 if opts.Continue == invalidContinueToken {
528 if err == nil || !strings.Contains(err.Error(), "continue key is not valid") {
529 t.Fatalf("expected continue key not valid error, but got: %v", err)
530 }
531 return
532 }
533
534 if opts.Continue != "" && !(opts.ResourceVersion == "" || opts.ResourceVersion == "0") {
535 if err == nil || !strings.Contains(err.Error(), "specifying resource version is not allowed when using continue") {
536 t.Fatalf("expected not allowed error, but got: %v", err)
537 }
538 return
539 }
540 if opts.ResourceVersion == invalidResourceVersion {
541 if err == nil || !strings.Contains(err.Error(), "Invalid value") {
542 t.Fatalf("expecting invalid value error, but got: %v", err)
543 }
544 return
545 }
546
547
548 isExact := opts.ResourceVersionMatch == metav1.ResourceVersionMatchExact
549
550
551 isLegacyExact := opts.Limit > 0 && opts.ResourceVersionMatch == ""
552
553 if opts.ResourceVersion == compactedRv && (isExact || isLegacyExact) {
554 if err == nil || !strings.Contains(err.Error(), "The resourceVersion for the provided list is too old") {
555 t.Fatalf("expected too old error, but got: %v", err)
556 }
557 return
558 }
559
560
561 if err != nil {
562 t.Fatalf("unexpected error: %v", err)
563 }
564 items, err := meta.ExtractList(listObj)
565 if err != nil {
566 t.Fatalf("Failed to extract list from %v", listObj)
567 }
568 count := int64(len(items))
569
570
571
572 hasContinuation := len(opts.Continue) > 0
573 hasLimit := opts.Limit > 0 && opts.ResourceVersion != "0"
574 skipWatchCache := opts.ResourceVersion == "" || hasContinuation || hasLimit || isExact
575 usingWatchCache := watchCacheEnabled && !skipWatchCache
576
577 if usingWatchCache {
578 if count != 10 {
579 t.Errorf("Expected list size to be 10 but got %d", count)
580 }
581 return
582 }
583
584 if opts.Continue != "" {
585 if count != 4 {
586 t.Errorf("Expected list size of 4 but got %d", count)
587 }
588 return
589 }
590 if opts.Limit > 0 {
591 if count != opts.Limit {
592 t.Errorf("Expected list size to be limited to %d but got %d", opts.Limit, count)
593 }
594 return
595 }
596 if count != 10 {
597 t.Errorf("Expected list size to be 10 but got %d", count)
598 }
599 }
600
601 func TestListResourceVersion0(t *testing.T) {
602 var testcases = []struct {
603 name string
604 watchCacheEnabled bool
605 }{
606 {
607 name: "watchCacheOn",
608 watchCacheEnabled: true,
609 },
610 {
611 name: "watchCacheOff",
612 watchCacheEnabled: false,
613 },
614 }
615
616 for _, tc := range testcases {
617 t.Run(tc.name, func(t *testing.T) {
618 tCtx := ktesting.Init(t)
619
620 clientSet, _, tearDownFn := framework.StartTestServer(tCtx, t, framework.TestServerSetup{
621 ModifyServerRunOptions: func(opts *options.ServerRunOptions) {
622 opts.Etcd.EnableWatchCache = tc.watchCacheEnabled
623 },
624 })
625 defer tearDownFn()
626
627 ns := framework.CreateNamespaceOrDie(clientSet, "list-paging", t)
628 defer framework.DeleteNamespaceOrDie(clientSet, ns, t)
629
630 rsClient := clientSet.AppsV1().ReplicaSets(ns.Name)
631
632 for i := 0; i < 10; i++ {
633 rs := newRS(ns.Name)
634 rs.Name = fmt.Sprintf("test-%d", i)
635 if _, err := rsClient.Create(tCtx, rs, metav1.CreateOptions{}); err != nil {
636 t.Fatal(err)
637 }
638 }
639
640 if tc.watchCacheEnabled {
641
642 err := wait.PollImmediate(time.Second, wait.ForeverTestTimeout, func() (bool, error) {
643 list, err := clientSet.AppsV1().ReplicaSets(ns.Name).List(tCtx, metav1.ListOptions{ResourceVersion: "0"})
644 if err != nil {
645 return false, err
646 }
647 return len(list.Items) == 10, nil
648 })
649 if err != nil {
650 t.Fatalf("error waiting for watch cache to observe the full list: %v", err)
651 }
652 }
653
654 pagerFn := func(opts metav1.ListOptions) (runtime.Object, error) {
655 return rsClient.List(tCtx, opts)
656 }
657
658 p := pager.New(pager.SimplePageFunc(pagerFn))
659 p.PageSize = 3
660 listObj, _, err := p.List(tCtx, metav1.ListOptions{ResourceVersion: "0"})
661 if err != nil {
662 t.Fatalf("Unexpected list error: %v", err)
663 }
664 items, err := meta.ExtractList(listObj)
665 if err != nil {
666 t.Fatalf("Failed to extract list from %v", listObj)
667 }
668 if len(items) != 10 {
669 t.Errorf("Expected list size of 10 but got %d", len(items))
670 }
671 })
672 }
673 }
674
675 func TestAPIListChunking(t *testing.T) {
676 ctx, clientSet, _, tearDownFn := setup(t)
677 defer tearDownFn()
678
679 ns := framework.CreateNamespaceOrDie(clientSet, "list-paging", t)
680 defer framework.DeleteNamespaceOrDie(clientSet, ns, t)
681
682 rsClient := clientSet.AppsV1().ReplicaSets(ns.Name)
683
684 for i := 0; i < 4; i++ {
685 rs := newRS(ns.Name)
686 rs.Name = fmt.Sprintf("test-%d", i)
687 if _, err := rsClient.Create(ctx, rs, metav1.CreateOptions{}); err != nil {
688 t.Fatal(err)
689 }
690 }
691
692 calls := 0
693 firstRV := ""
694 p := &pager.ListPager{
695 PageSize: 1,
696 PageFn: pager.SimplePageFunc(func(opts metav1.ListOptions) (runtime.Object, error) {
697 calls++
698 list, err := rsClient.List(ctx, opts)
699 if err != nil {
700 return nil, err
701 }
702 if calls == 1 {
703 firstRV = list.ResourceVersion
704 }
705 if calls == 2 {
706 rs := newRS(ns.Name)
707 rs.Name = "test-5"
708 if _, err := rsClient.Create(ctx, rs, metav1.CreateOptions{}); err != nil {
709 t.Fatal(err)
710 }
711 }
712 return list, err
713 }),
714 }
715 listObj, _, err := p.List(ctx, metav1.ListOptions{})
716 if err != nil {
717 t.Fatal(err)
718 }
719 if calls != 4 {
720 t.Errorf("unexpected list invocations: %d", calls)
721 }
722 list := listObj.(metav1.ListInterface)
723 if len(list.GetContinue()) != 0 {
724 t.Errorf("unexpected continue: %s", list.GetContinue())
725 }
726 if list.GetResourceVersion() != firstRV {
727 t.Errorf("unexpected resource version: %s instead of %s", list.GetResourceVersion(), firstRV)
728 }
729 var names []string
730 if err := meta.EachListItem(listObj, func(obj runtime.Object) error {
731 rs := obj.(*apps.ReplicaSet)
732 names = append(names, rs.Name)
733 return nil
734 }); err != nil {
735 t.Fatal(err)
736 }
737 if !reflect.DeepEqual(names, []string{"test-0", "test-1", "test-2", "test-3"}) {
738 t.Errorf("unexpected items: %#v", list)
739 }
740 }
741
742 func TestAPIListChunkingWithLabelSelector(t *testing.T) {
743 ctx, clientSet, _, tearDownFn := setup(t)
744 defer tearDownFn()
745
746 ns := framework.CreateNamespaceOrDie(clientSet, "list-paging-with-label-selector", t)
747 defer framework.DeleteNamespaceOrDie(clientSet, ns, t)
748
749 rsClient := clientSet.AppsV1().ReplicaSets(ns.Name)
750
751 for i := 0; i < 10; i++ {
752 rs := newRS(ns.Name)
753 rs.Name = fmt.Sprintf("test-%d", i)
754 odd := i%2 != 0
755 rs.Labels = map[string]string{"odd-index": strconv.FormatBool(odd)}
756 if _, err := rsClient.Create(ctx, rs, metav1.CreateOptions{}); err != nil {
757 t.Fatal(err)
758 }
759 }
760
761 calls := 0
762 firstRV := ""
763 p := &pager.ListPager{
764 PageSize: 1,
765 PageFn: pager.SimplePageFunc(func(opts metav1.ListOptions) (runtime.Object, error) {
766 calls++
767 list, err := rsClient.List(ctx, opts)
768 if err != nil {
769 return nil, err
770 }
771 if calls == 1 {
772 firstRV = list.ResourceVersion
773 }
774 return list, err
775 }),
776 }
777 listObj, _, err := p.List(ctx, metav1.ListOptions{LabelSelector: "odd-index=true", Limit: 3})
778 if err != nil {
779 t.Fatal(err)
780 }
781 if calls != 2 {
782 t.Errorf("unexpected list invocations: %d", calls)
783 }
784 list := listObj.(metav1.ListInterface)
785 if len(list.GetContinue()) != 0 {
786 t.Errorf("unexpected continue: %s", list.GetContinue())
787 }
788 if list.GetResourceVersion() != firstRV {
789 t.Errorf("unexpected resource version: %s instead of %s", list.GetResourceVersion(), firstRV)
790 }
791 var names []string
792 if err := meta.EachListItem(listObj, func(obj runtime.Object) error {
793 rs := obj.(*apps.ReplicaSet)
794 names = append(names, rs.Name)
795 return nil
796 }); err != nil {
797 t.Fatal(err)
798 }
799 if !reflect.DeepEqual(names, []string{"test-1", "test-3", "test-5", "test-7", "test-9"}) {
800 t.Errorf("unexpected items: %#v", list)
801 }
802 }
803
804 func makeSecret(name string) *v1.Secret {
805 return &v1.Secret{
806 ObjectMeta: metav1.ObjectMeta{
807 Name: name,
808 },
809 Data: map[string][]byte{
810 "key": []byte("value"),
811 },
812 }
813 }
814
815 func TestNameInFieldSelector(t *testing.T) {
816 ctx, clientSet, _, tearDownFn := setup(t)
817 defer tearDownFn()
818
819 numNamespaces := 3
820 for i := 0; i < 3; i++ {
821 ns := framework.CreateNamespaceOrDie(clientSet, fmt.Sprintf("ns%d", i), t)
822 defer framework.DeleteNamespaceOrDie(clientSet, ns, t)
823
824 _, err := clientSet.CoreV1().Secrets(ns.Name).Create(ctx, makeSecret("foo"), metav1.CreateOptions{})
825 if err != nil {
826 t.Errorf("Couldn't create secret: %v", err)
827 }
828 _, err = clientSet.CoreV1().Secrets(ns.Name).Create(ctx, makeSecret("bar"), metav1.CreateOptions{})
829 if err != nil {
830 t.Errorf("Couldn't create secret: %v", err)
831 }
832 }
833
834 testcases := []struct {
835 namespace string
836 selector string
837 expectedSecrets int
838 }{
839 {
840 namespace: "",
841 selector: "metadata.name=foo",
842 expectedSecrets: numNamespaces,
843 },
844 {
845 namespace: "",
846 selector: "metadata.name=foo,metadata.name=bar",
847 expectedSecrets: 0,
848 },
849 {
850 namespace: "",
851 selector: "metadata.name=foo,metadata.namespace=ns1",
852 expectedSecrets: 1,
853 },
854 {
855 namespace: "ns1",
856 selector: "metadata.name=foo,metadata.namespace=ns1",
857 expectedSecrets: 1,
858 },
859 {
860 namespace: "ns1",
861 selector: "metadata.name=foo,metadata.namespace=ns2",
862 expectedSecrets: 0,
863 },
864 {
865 namespace: "ns1",
866 selector: "metadata.name=foo,metadata.namespace=",
867 expectedSecrets: 0,
868 },
869 }
870
871 for _, tc := range testcases {
872 opts := metav1.ListOptions{
873 FieldSelector: tc.selector,
874 }
875 secrets, err := clientSet.CoreV1().Secrets(tc.namespace).List(ctx, opts)
876 if err != nil {
877 t.Errorf("%s: Unexpected error: %v", tc.selector, err)
878 }
879 if len(secrets.Items) != tc.expectedSecrets {
880 t.Errorf("%s: Unexpected number of secrets: %d, expected: %d", tc.selector, len(secrets.Items), tc.expectedSecrets)
881 }
882 }
883 }
884
885 type callWrapper struct {
886 nested http.RoundTripper
887 req *http.Request
888 resp *http.Response
889 err error
890 }
891
892 func (w *callWrapper) RoundTrip(req *http.Request) (*http.Response, error) {
893 w.req = req
894 resp, err := w.nested.RoundTrip(req)
895 w.resp = resp
896 w.err = err
897 return resp, err
898 }
899
900 func TestMetadataClient(t *testing.T) {
901 tearDown, config, _, err := fixtures.StartDefaultServer(t)
902 if err != nil {
903 t.Fatal(err)
904 }
905 defer tearDown()
906
907 ctx, clientset, kubeConfig, tearDownFn := setup(t)
908 defer tearDownFn()
909
910 apiExtensionClient, err := apiextensionsclient.NewForConfig(config)
911 if err != nil {
912 t.Fatal(err)
913 }
914
915 dynamicClient, err := dynamic.NewForConfig(config)
916 if err != nil {
917 t.Fatal(err)
918 }
919
920 fooCRD := &apiextensionsv1.CustomResourceDefinition{
921 ObjectMeta: metav1.ObjectMeta{
922 Name: "foos.cr.bar.com",
923 },
924 Spec: apiextensionsv1.CustomResourceDefinitionSpec{
925 Group: "cr.bar.com",
926 Scope: apiextensionsv1.NamespaceScoped,
927 Names: apiextensionsv1.CustomResourceDefinitionNames{
928 Plural: "foos",
929 Kind: "Foo",
930 },
931 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
932 {
933 Name: "v1",
934 Served: true,
935 Storage: true,
936 Schema: fixtures.AllowAllSchema(),
937 Subresources: &apiextensionsv1.CustomResourceSubresources{
938 Status: &apiextensionsv1.CustomResourceSubresourceStatus{},
939 },
940 },
941 },
942 },
943 }
944 fooCRD, err = fixtures.CreateNewV1CustomResourceDefinition(fooCRD, apiExtensionClient, dynamicClient)
945 if err != nil {
946 t.Fatal(err)
947 }
948 crdGVR := schema.GroupVersionResource{Group: fooCRD.Spec.Group, Version: fooCRD.Spec.Versions[0].Name, Resource: "foos"}
949
950 testcases := []struct {
951 name string
952 want func(*testing.T)
953 }{
954 {
955 name: "list, get, patch, and delete via metadata client",
956 want: func(t *testing.T) {
957 ns := "metadata-builtin"
958 namespace := framework.CreateNamespaceOrDie(clientset, ns, t)
959 defer framework.DeleteNamespaceOrDie(clientset, namespace, t)
960
961 svc, err := clientset.CoreV1().Services(ns).Create(ctx, &v1.Service{ObjectMeta: metav1.ObjectMeta{Name: "test-1", Annotations: map[string]string{"foo": "bar"}}, Spec: v1.ServiceSpec{Ports: []v1.ServicePort{{Port: 1000}}}}, metav1.CreateOptions{})
962 if err != nil {
963 t.Fatalf("unable to create service: %v", err)
964 }
965
966 cfg := metadata.ConfigFor(kubeConfig)
967 wrapper := &callWrapper{}
968 cfg.Wrap(func(rt http.RoundTripper) http.RoundTripper {
969 wrapper.nested = rt
970 return wrapper
971 })
972
973 client := metadata.NewForConfigOrDie(cfg).Resource(v1.SchemeGroupVersion.WithResource("services"))
974 items, err := client.Namespace(ns).List(ctx, metav1.ListOptions{})
975 if err != nil {
976 t.Fatal(err)
977 }
978 if items.ResourceVersion == "" {
979 t.Fatalf("unexpected items: %#v", items)
980 }
981 if len(items.Items) != 1 {
982 t.Fatalf("unexpected list: %#v", items)
983 }
984 if item := items.Items[0]; item.Name != "test-1" || item.UID != svc.UID || item.Annotations["foo"] != "bar" {
985 t.Fatalf("unexpected object: %#v", item)
986 }
987
988 if wrapper.resp == nil || wrapper.resp.Header.Get("Content-Type") != "application/vnd.kubernetes.protobuf" {
989 t.Fatalf("unexpected response: %#v", wrapper.resp)
990 }
991 wrapper.resp = nil
992
993 item, err := client.Namespace(ns).Get(ctx, "test-1", metav1.GetOptions{})
994 if err != nil {
995 t.Fatal(err)
996 }
997 if item.ResourceVersion == "" || item.UID != svc.UID || item.Annotations["foo"] != "bar" {
998 t.Fatalf("unexpected object: %#v", item)
999 }
1000 if wrapper.resp == nil || wrapper.resp.Header.Get("Content-Type") != "application/vnd.kubernetes.protobuf" {
1001 t.Fatalf("unexpected response: %#v", wrapper.resp)
1002 }
1003
1004 item, err = client.Namespace(ns).Patch(ctx, "test-1", types.MergePatchType, []byte(`{"metadata":{"annotations":{"foo":"baz"}}}`), metav1.PatchOptions{})
1005 if err != nil {
1006 t.Fatal(err)
1007 }
1008 if item.Annotations["foo"] != "baz" {
1009 t.Fatalf("unexpected object: %#v", item)
1010 }
1011
1012 if err := client.Namespace(ns).Delete(ctx, "test-1", metav1.DeleteOptions{Preconditions: &metav1.Preconditions{UID: &item.UID}}); err != nil {
1013 t.Fatal(err)
1014 }
1015
1016 if _, err := client.Namespace(ns).Get(ctx, "test-1", metav1.GetOptions{}); !apierrors.IsNotFound(err) {
1017 t.Fatal(err)
1018 }
1019 },
1020 },
1021 {
1022 name: "list, get, patch, and delete via metadata client on a CRD",
1023 want: func(t *testing.T) {
1024 ns := "metadata-crd"
1025 crclient := dynamicClient.Resource(crdGVR).Namespace(ns)
1026 cr, err := crclient.Create(ctx, &unstructured.Unstructured{
1027 Object: map[string]interface{}{
1028 "apiVersion": "cr.bar.com/v1",
1029 "kind": "Foo",
1030 "spec": map[string]interface{}{"field": 1},
1031 "metadata": map[string]interface{}{
1032 "name": "test-1",
1033 "annotations": map[string]interface{}{
1034 "foo": "bar",
1035 },
1036 },
1037 },
1038 }, metav1.CreateOptions{})
1039 if err != nil {
1040 t.Fatalf("unable to create cr: %v", err)
1041 }
1042
1043 cfg := metadata.ConfigFor(config)
1044 wrapper := &callWrapper{}
1045 cfg.Wrap(func(rt http.RoundTripper) http.RoundTripper {
1046 wrapper.nested = rt
1047 return wrapper
1048 })
1049
1050 client := metadata.NewForConfigOrDie(cfg).Resource(crdGVR)
1051 items, err := client.Namespace(ns).List(ctx, metav1.ListOptions{})
1052 if err != nil {
1053 t.Fatal(err)
1054 }
1055 if items.ResourceVersion == "" {
1056 t.Fatalf("unexpected items: %#v", items)
1057 }
1058 if len(items.Items) != 1 {
1059 t.Fatalf("unexpected list: %#v", items)
1060 }
1061 if item := items.Items[0]; item.Name != "test-1" || item.UID != cr.GetUID() || item.Annotations["foo"] != "bar" {
1062 t.Fatalf("unexpected object: %#v", item)
1063 }
1064
1065 if wrapper.resp == nil || wrapper.resp.Header.Get("Content-Type") != "application/vnd.kubernetes.protobuf" {
1066 t.Fatalf("unexpected response: %#v", wrapper.resp)
1067 }
1068 wrapper.resp = nil
1069
1070 item, err := client.Namespace(ns).Get(ctx, "test-1", metav1.GetOptions{})
1071 if err != nil {
1072 t.Fatal(err)
1073 }
1074 if item.ResourceVersion == "" || item.UID != cr.GetUID() || item.Annotations["foo"] != "bar" {
1075 t.Fatalf("unexpected object: %#v", item)
1076 }
1077 if wrapper.resp == nil || wrapper.resp.Header.Get("Content-Type") != "application/vnd.kubernetes.protobuf" {
1078 t.Fatalf("unexpected response: %#v", wrapper.resp)
1079 }
1080
1081 item, err = client.Namespace(ns).Patch(ctx, "test-1", types.MergePatchType, []byte(`{"metadata":{"annotations":{"foo":"baz"}}}`), metav1.PatchOptions{})
1082 if err != nil {
1083 t.Fatal(err)
1084 }
1085 if item.Annotations["foo"] != "baz" {
1086 t.Fatalf("unexpected object: %#v", item)
1087 }
1088
1089 if err := client.Namespace(ns).Delete(ctx, "test-1", metav1.DeleteOptions{Preconditions: &metav1.Preconditions{UID: &item.UID}}); err != nil {
1090 t.Fatal(err)
1091 }
1092 if _, err := client.Namespace(ns).Get(ctx, "test-1", metav1.GetOptions{}); !apierrors.IsNotFound(err) {
1093 t.Fatal(err)
1094 }
1095 },
1096 },
1097 {
1098 name: "watch via metadata client",
1099 want: func(t *testing.T) {
1100 ns := "metadata-watch"
1101 namespace := framework.CreateNamespaceOrDie(clientset, ns, t)
1102 defer framework.DeleteNamespaceOrDie(clientset, namespace, t)
1103
1104 svc, err := clientset.CoreV1().Services(ns).Create(ctx, &v1.Service{ObjectMeta: metav1.ObjectMeta{Name: "test-2", Annotations: map[string]string{"foo": "bar"}}, Spec: v1.ServiceSpec{Ports: []v1.ServicePort{{Port: 1000}}}}, metav1.CreateOptions{})
1105 if err != nil {
1106 t.Fatalf("unable to create service: %v", err)
1107 }
1108 if _, err := clientset.CoreV1().Services(ns).Patch(ctx, "test-2", types.MergePatchType, []byte(`{"metadata":{"annotations":{"test":"1"}}}`), metav1.PatchOptions{}); err != nil {
1109 t.Fatalf("unable to patch cr: %v", err)
1110 }
1111
1112 cfg := metadata.ConfigFor(kubeConfig)
1113 wrapper := &callWrapper{}
1114 cfg.Wrap(func(rt http.RoundTripper) http.RoundTripper {
1115 wrapper.nested = rt
1116 return wrapper
1117 })
1118
1119 client := metadata.NewForConfigOrDie(cfg).Resource(v1.SchemeGroupVersion.WithResource("services"))
1120 w, err := client.Namespace(ns).Watch(ctx, metav1.ListOptions{ResourceVersion: svc.ResourceVersion, Watch: true})
1121 if err != nil {
1122 t.Fatal(err)
1123 }
1124 defer w.Stop()
1125 var r watch.Event
1126 select {
1127 case evt, ok := <-w.ResultChan():
1128 if !ok {
1129 t.Fatal("watch closed")
1130 }
1131 r = evt
1132 case <-time.After(5 * time.Second):
1133 t.Fatal("no watch event in 5 seconds, bug")
1134 }
1135 if r.Type != watch.Modified {
1136 t.Fatalf("unexpected watch: %#v", r)
1137 }
1138 item, ok := r.Object.(*metav1.PartialObjectMetadata)
1139 if !ok {
1140 t.Fatalf("unexpected object: %T", item)
1141 }
1142 if item.ResourceVersion == "" || item.Name != "test-2" || item.UID != svc.UID || item.Annotations["test"] != "1" {
1143 t.Fatalf("unexpected object: %#v", item)
1144 }
1145
1146 if wrapper.resp == nil || wrapper.resp.Header.Get("Content-Type") != "application/vnd.kubernetes.protobuf;stream=watch" {
1147 t.Fatalf("unexpected response: %#v", wrapper.resp)
1148 }
1149 },
1150 },
1151
1152 {
1153 name: "watch via metadata client on a CRD",
1154 want: func(t *testing.T) {
1155 ns := "metadata-watch-crd"
1156 crclient := dynamicClient.Resource(crdGVR).Namespace(ns)
1157 cr, err := crclient.Create(ctx, &unstructured.Unstructured{
1158 Object: map[string]interface{}{
1159 "apiVersion": "cr.bar.com/v1",
1160 "kind": "Foo",
1161 "spec": map[string]interface{}{"field": 1},
1162 "metadata": map[string]interface{}{
1163 "name": "test-2",
1164 "annotations": map[string]interface{}{
1165 "foo": "bar",
1166 },
1167 },
1168 },
1169 }, metav1.CreateOptions{})
1170 if err != nil {
1171 t.Fatalf("unable to create cr: %v", err)
1172 }
1173
1174 cfg := metadata.ConfigFor(config)
1175 client := metadata.NewForConfigOrDie(cfg).Resource(crdGVR)
1176
1177 patched, err := client.Namespace(ns).Patch(ctx, "test-2", types.MergePatchType, []byte(`{"metadata":{"annotations":{"test":"1"}}}`), metav1.PatchOptions{})
1178 if err != nil {
1179 t.Fatal(err)
1180 }
1181 if patched.GetResourceVersion() == cr.GetResourceVersion() {
1182 t.Fatalf("Patch did not modify object: %#v", patched)
1183 }
1184
1185 wrapper := &callWrapper{}
1186 cfg.Wrap(func(rt http.RoundTripper) http.RoundTripper {
1187 wrapper.nested = rt
1188 return wrapper
1189 })
1190 client = metadata.NewForConfigOrDie(cfg).Resource(crdGVR)
1191
1192 w, err := client.Namespace(ns).Watch(ctx, metav1.ListOptions{ResourceVersion: cr.GetResourceVersion(), Watch: true})
1193 if err != nil {
1194 t.Fatal(err)
1195 }
1196 defer w.Stop()
1197 var r watch.Event
1198 select {
1199 case evt, ok := <-w.ResultChan():
1200 if !ok {
1201 t.Fatal("watch closed")
1202 }
1203 r = evt
1204 case <-time.After(5 * time.Second):
1205 t.Fatal("no watch event in 5 seconds, bug")
1206 }
1207 if r.Type != watch.Modified {
1208 t.Fatalf("unexpected watch: %#v", r)
1209 }
1210 item, ok := r.Object.(*metav1.PartialObjectMetadata)
1211 if !ok {
1212 t.Fatalf("unexpected object: %T", item)
1213 }
1214 if item.ResourceVersion == "" || item.Name != "test-2" || item.UID != cr.GetUID() || item.Annotations["test"] != "1" {
1215 t.Fatalf("unexpected object: %#v", item)
1216 }
1217
1218 if wrapper.resp == nil || wrapper.resp.Header.Get("Content-Type") != "application/vnd.kubernetes.protobuf;stream=watch" {
1219 t.Fatalf("unexpected response: %#v", wrapper.resp)
1220 }
1221 },
1222 },
1223 }
1224
1225 for i := range testcases {
1226 tc := testcases[i]
1227 t.Run(tc.name, func(t *testing.T) {
1228 tc.want(t)
1229 })
1230 }
1231 }
1232
1233 func TestAPICRDProtobuf(t *testing.T) {
1234 testNamespace := "test-api-crd-protobuf"
1235 tearDown, config, _, err := fixtures.StartDefaultServer(t)
1236 if err != nil {
1237 t.Fatal(err)
1238 }
1239 defer tearDown()
1240
1241 ctx, _, kubeConfig, tearDownFn := setup(t)
1242 defer tearDownFn()
1243
1244 apiExtensionClient, err := apiextensionsclient.NewForConfig(config)
1245 if err != nil {
1246 t.Fatal(err)
1247 }
1248
1249 dynamicClient, err := dynamic.NewForConfig(config)
1250 if err != nil {
1251 t.Fatal(err)
1252 }
1253
1254 fooCRD := &apiextensionsv1.CustomResourceDefinition{
1255 ObjectMeta: metav1.ObjectMeta{
1256 Name: "foos.cr.bar.com",
1257 },
1258 Spec: apiextensionsv1.CustomResourceDefinitionSpec{
1259 Group: "cr.bar.com",
1260 Scope: apiextensionsv1.NamespaceScoped,
1261 Names: apiextensionsv1.CustomResourceDefinitionNames{
1262 Plural: "foos",
1263 Kind: "Foo",
1264 },
1265 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
1266 {
1267 Name: "v1",
1268 Served: true,
1269 Storage: true,
1270 Schema: fixtures.AllowAllSchema(),
1271 Subresources: &apiextensionsv1.CustomResourceSubresources{Status: &apiextensionsv1.CustomResourceSubresourceStatus{}},
1272 },
1273 },
1274 },
1275 }
1276 fooCRD, err = fixtures.CreateNewV1CustomResourceDefinition(fooCRD, apiExtensionClient, dynamicClient)
1277 if err != nil {
1278 t.Fatal(err)
1279 }
1280 crdGVR := schema.GroupVersionResource{Group: fooCRD.Spec.Group, Version: fooCRD.Spec.Versions[0].Name, Resource: "foos"}
1281 crclient := dynamicClient.Resource(crdGVR).Namespace(testNamespace)
1282
1283 testcases := []struct {
1284 name string
1285 accept string
1286 subresource string
1287 object func(*testing.T) (metav1.Object, string, string)
1288 wantErr func(*testing.T, error)
1289 wantBody func(*testing.T, io.Reader)
1290 }{
1291 {
1292 name: "server returns 406 when asking for protobuf for CRDs, which dynamic client does not support",
1293 accept: "application/vnd.kubernetes.protobuf",
1294 object: func(t *testing.T) (metav1.Object, string, string) {
1295 cr, err := crclient.Create(ctx, &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "cr.bar.com/v1", "kind": "Foo", "metadata": map[string]interface{}{"name": "test-1"}}}, metav1.CreateOptions{})
1296 if err != nil {
1297 t.Fatalf("unable to create cr: %v", err)
1298 }
1299 if _, err := crclient.Patch(ctx, "test-1", types.MergePatchType, []byte(`{"metadata":{"annotations":{"test":"1"}}}`), metav1.PatchOptions{}); err != nil {
1300 t.Fatalf("unable to patch cr: %v", err)
1301 }
1302 return cr, crdGVR.Group, "foos"
1303 },
1304 wantErr: func(t *testing.T, err error) {
1305 if !apierrors.IsNotAcceptable(err) {
1306 t.Fatal(err)
1307 }
1308 status := err.(apierrors.APIStatus).Status()
1309 data, _ := json.MarshalIndent(status, "", " ")
1310
1311
1312 if !apierrors.IsUnexpectedServerError(err) {
1313 t.Fatal(string(data))
1314 }
1315 if status.Message != "the server was unable to respond with a content type that the client supports (get foos.cr.bar.com test-1)" {
1316 t.Fatal(string(data))
1317 }
1318 },
1319 },
1320 {
1321 name: "server returns JSON when asking for protobuf and json for CRDs",
1322 accept: "application/vnd.kubernetes.protobuf,application/json",
1323 object: func(t *testing.T) (metav1.Object, string, string) {
1324 cr, err := crclient.Create(ctx, &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "cr.bar.com/v1", "kind": "Foo", "spec": map[string]interface{}{"field": 1}, "metadata": map[string]interface{}{"name": "test-2"}}}, metav1.CreateOptions{})
1325 if err != nil {
1326 t.Fatalf("unable to create cr: %v", err)
1327 }
1328 if _, err := crclient.Patch(ctx, "test-2", types.MergePatchType, []byte(`{"metadata":{"annotations":{"test":"1"}}}`), metav1.PatchOptions{}); err != nil {
1329 t.Fatalf("unable to patch cr: %v", err)
1330 }
1331 return cr, crdGVR.Group, "foos"
1332 },
1333 wantBody: func(t *testing.T, w io.Reader) {
1334 obj := &unstructured.Unstructured{}
1335 if err := json.NewDecoder(w).Decode(obj); err != nil {
1336 t.Fatal(err)
1337 }
1338 v, ok, err := unstructured.NestedInt64(obj.UnstructuredContent(), "spec", "field")
1339 if !ok || err != nil {
1340 data, _ := json.MarshalIndent(obj.UnstructuredContent(), "", " ")
1341 t.Fatalf("err=%v ok=%t json=%s", err, ok, string(data))
1342 }
1343 if v != 1 {
1344 t.Fatalf("unexpected body: %#v", obj.UnstructuredContent())
1345 }
1346 },
1347 },
1348 {
1349 name: "server returns 406 when asking for protobuf for CRDs status, which dynamic client does not support",
1350 accept: "application/vnd.kubernetes.protobuf",
1351 subresource: "status",
1352 object: func(t *testing.T) (metav1.Object, string, string) {
1353 cr, err := crclient.Create(ctx, &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "cr.bar.com/v1", "kind": "Foo", "metadata": map[string]interface{}{"name": "test-3"}}}, metav1.CreateOptions{})
1354 if err != nil {
1355 t.Fatalf("unable to create cr: %v", err)
1356 }
1357 if _, err := crclient.Patch(ctx, "test-3", types.MergePatchType, []byte(`{"metadata":{"annotations":{"test":"3"}}}`), metav1.PatchOptions{}); err != nil {
1358 t.Fatalf("unable to patch cr: %v", err)
1359 }
1360 return cr, crdGVR.Group, "foos"
1361 },
1362 wantErr: func(t *testing.T, err error) {
1363 if !apierrors.IsNotAcceptable(err) {
1364 t.Fatal(err)
1365 }
1366 status := err.(apierrors.APIStatus).Status()
1367 data, _ := json.MarshalIndent(status, "", " ")
1368
1369
1370 if !apierrors.IsUnexpectedServerError(err) {
1371 t.Fatal(string(data))
1372 }
1373 if status.Message != "the server was unable to respond with a content type that the client supports (get foos.cr.bar.com test-3)" {
1374 t.Fatal(string(data))
1375 }
1376 },
1377 },
1378 {
1379 name: "server returns JSON when asking for protobuf and json for CRDs status",
1380 accept: "application/vnd.kubernetes.protobuf,application/json",
1381 subresource: "status",
1382 object: func(t *testing.T) (metav1.Object, string, string) {
1383 cr, err := crclient.Create(ctx, &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "cr.bar.com/v1", "kind": "Foo", "spec": map[string]interface{}{"field": 1}, "metadata": map[string]interface{}{"name": "test-4"}}}, metav1.CreateOptions{})
1384 if err != nil {
1385 t.Fatalf("unable to create cr: %v", err)
1386 }
1387 if _, err := crclient.Patch(ctx, "test-4", types.MergePatchType, []byte(`{"metadata":{"annotations":{"test":"4"}}}`), metav1.PatchOptions{}); err != nil {
1388 t.Fatalf("unable to patch cr: %v", err)
1389 }
1390 return cr, crdGVR.Group, "foos"
1391 },
1392 wantBody: func(t *testing.T, w io.Reader) {
1393 obj := &unstructured.Unstructured{}
1394 if err := json.NewDecoder(w).Decode(obj); err != nil {
1395 t.Fatal(err)
1396 }
1397 v, ok, err := unstructured.NestedInt64(obj.UnstructuredContent(), "spec", "field")
1398 if !ok || err != nil {
1399 data, _ := json.MarshalIndent(obj.UnstructuredContent(), "", " ")
1400 t.Fatalf("err=%v ok=%t json=%s", err, ok, string(data))
1401 }
1402 if v != 1 {
1403 t.Fatalf("unexpected body: %#v", obj.UnstructuredContent())
1404 }
1405 },
1406 },
1407 }
1408
1409 for i := range testcases {
1410 tc := testcases[i]
1411 t.Run(tc.name, func(t *testing.T) {
1412 obj, group, resource := tc.object(t)
1413
1414 cfg := dynamic.ConfigFor(config)
1415 if len(group) == 0 {
1416 cfg = dynamic.ConfigFor(kubeConfig)
1417 cfg.APIPath = "/api"
1418 } else {
1419 cfg.APIPath = "/apis"
1420 }
1421 cfg.GroupVersion = &schema.GroupVersion{Group: group, Version: "v1"}
1422 client, err := restclient.RESTClientFor(cfg)
1423 if err != nil {
1424 t.Fatal(err)
1425 }
1426
1427 w, err := client.Get().
1428 Resource(resource).NamespaceIfScoped(obj.GetNamespace(), len(obj.GetNamespace()) > 0).Name(obj.GetName()).SubResource(tc.subresource).
1429 SetHeader("Accept", tc.accept).
1430 Stream(ctx)
1431 if (tc.wantErr != nil) != (err != nil) {
1432 t.Fatalf("unexpected error: %v", err)
1433 }
1434 if tc.wantErr != nil {
1435 tc.wantErr(t, err)
1436 return
1437 }
1438 if err != nil {
1439 t.Fatal(err)
1440 }
1441 defer w.Close()
1442 tc.wantBody(t, w)
1443 })
1444 }
1445 }
1446
1447 func TestGetSubresourcesAsTables(t *testing.T) {
1448 testNamespace := "test-transform"
1449 tearDown, config, _, err := fixtures.StartDefaultServer(t)
1450 if err != nil {
1451 t.Fatal(err)
1452 }
1453 defer tearDown()
1454
1455 ctx, clientset, kubeConfig, tearDownFn := setup(t)
1456 defer tearDownFn()
1457
1458 ns := framework.CreateNamespaceOrDie(clientset, testNamespace, t)
1459 defer framework.DeleteNamespaceOrDie(clientset, ns, t)
1460
1461 apiExtensionClient, err := apiextensionsclient.NewForConfig(config)
1462 if err != nil {
1463 t.Fatal(err)
1464 }
1465
1466 dynamicClient, err := dynamic.NewForConfig(config)
1467 if err != nil {
1468 t.Fatal(err)
1469 }
1470
1471 fooWithSubresourceCRD := &apiextensionsv1.CustomResourceDefinition{
1472 ObjectMeta: metav1.ObjectMeta{
1473 Name: "foosubs.cr.bar.com",
1474 },
1475 Spec: apiextensionsv1.CustomResourceDefinitionSpec{
1476 Group: "cr.bar.com",
1477 Scope: apiextensionsv1.NamespaceScoped,
1478 Names: apiextensionsv1.CustomResourceDefinitionNames{
1479 Plural: "foosubs",
1480 Kind: "FooSub",
1481 },
1482 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
1483 {
1484 Name: "v1",
1485 Served: true,
1486 Storage: true,
1487 Schema: &apiextensionsv1.CustomResourceValidation{
1488 OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
1489 Type: "object",
1490 Properties: map[string]apiextensionsv1.JSONSchemaProps{
1491 "spec": {
1492 Type: "object",
1493 Properties: map[string]apiextensionsv1.JSONSchemaProps{
1494 "replicas": {
1495 Type: "integer",
1496 },
1497 },
1498 },
1499 "status": {
1500 Type: "object",
1501 Properties: map[string]apiextensionsv1.JSONSchemaProps{
1502 "replicas": {
1503 Type: "integer",
1504 },
1505 },
1506 },
1507 },
1508 },
1509 },
1510 Subresources: &apiextensionsv1.CustomResourceSubresources{
1511 Status: &apiextensionsv1.CustomResourceSubresourceStatus{},
1512 Scale: &apiextensionsv1.CustomResourceSubresourceScale{
1513 SpecReplicasPath: ".spec.replicas",
1514 StatusReplicasPath: ".status.replicas",
1515 },
1516 },
1517 },
1518 },
1519 },
1520 }
1521
1522 fooWithSubresourceCRD, err = fixtures.CreateNewV1CustomResourceDefinition(fooWithSubresourceCRD, apiExtensionClient, dynamicClient)
1523 if err != nil {
1524 t.Fatal(err)
1525 }
1526 subresourcesCrdGVR := schema.GroupVersionResource{Group: fooWithSubresourceCRD.Spec.Group, Version: fooWithSubresourceCRD.Spec.Versions[0].Name, Resource: "foosubs"}
1527 subresourcesCrclient := dynamicClient.Resource(subresourcesCrdGVR).Namespace(testNamespace)
1528
1529 testcases := []struct {
1530 name string
1531 accept string
1532 object func(*testing.T) (metav1.Object, string, string)
1533 subresource string
1534 }{
1535 {
1536 name: "v1 verify status subresource returns a table for CRDs",
1537 accept: "application/json;as=Table;g=meta.k8s.io;v=v1",
1538 object: func(t *testing.T) (metav1.Object, string, string) {
1539 cr, err := subresourcesCrclient.Create(ctx, &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "cr.bar.com/v1", "kind": "FooSub", "metadata": map[string]interface{}{"name": "test-1"}, "spec": map[string]interface{}{"replicas": 2}}}, metav1.CreateOptions{})
1540 if err != nil {
1541 t.Fatalf("unable to create cr: %v", err)
1542 }
1543 return cr, subresourcesCrdGVR.Group, "foosubs"
1544 },
1545 subresource: "status",
1546 },
1547 {
1548 name: "v1 verify scale subresource returns a table for CRDs",
1549 accept: "application/json;as=Table;g=meta.k8s.io;v=v1",
1550 object: func(t *testing.T) (metav1.Object, string, string) {
1551 cr, err := subresourcesCrclient.Create(ctx, &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "cr.bar.com/v1", "kind": "FooSub", "metadata": map[string]interface{}{"name": "test-2"}, "spec": map[string]interface{}{"replicas": 2}}}, metav1.CreateOptions{})
1552 if err != nil {
1553 t.Fatalf("unable to create cr: %v", err)
1554 }
1555 return cr, subresourcesCrdGVR.Group, "foosubs"
1556 },
1557 subresource: "scale",
1558 },
1559 {
1560 name: "verify status subresource returns a table for replicationcontrollers",
1561 accept: "application/json;as=Table;g=meta.k8s.io;v=v1",
1562 object: func(t *testing.T) (metav1.Object, string, string) {
1563 rc := &v1.ReplicationController{
1564 ObjectMeta: metav1.ObjectMeta{
1565 Name: "replicationcontroller-1",
1566 },
1567 Spec: v1.ReplicationControllerSpec{
1568 Replicas: int32Ptr(2),
1569 Selector: map[string]string{
1570 "label": "test-label",
1571 },
1572 Template: &v1.PodTemplateSpec{
1573 ObjectMeta: metav1.ObjectMeta{
1574 Labels: map[string]string{
1575 "label": "test-label",
1576 },
1577 },
1578 Spec: v1.PodSpec{
1579 Containers: []v1.Container{
1580 {Name: "test-name", Image: "nonexistant-image"},
1581 },
1582 },
1583 },
1584 },
1585 }
1586 rc, err := clientset.CoreV1().ReplicationControllers(testNamespace).Create(ctx, rc, metav1.CreateOptions{})
1587 if err != nil {
1588 t.Fatalf("unable to create replicationcontroller: %v", err)
1589 }
1590 return rc, "", "replicationcontrollers"
1591 },
1592 subresource: "status",
1593 },
1594 {
1595 name: "verify scale subresource returns a table for replicationcontrollers",
1596 accept: "application/json;as=Table;g=meta.k8s.io;v=v1",
1597 object: func(t *testing.T) (metav1.Object, string, string) {
1598 rc := &v1.ReplicationController{
1599 ObjectMeta: metav1.ObjectMeta{
1600 Name: "replicationcontroller-2",
1601 },
1602 Spec: v1.ReplicationControllerSpec{
1603 Replicas: int32Ptr(2),
1604 Selector: map[string]string{
1605 "label": "test-label",
1606 },
1607 Template: &v1.PodTemplateSpec{
1608 ObjectMeta: metav1.ObjectMeta{
1609 Labels: map[string]string{
1610 "label": "test-label",
1611 },
1612 },
1613 Spec: v1.PodSpec{
1614 Containers: []v1.Container{
1615 {Name: "test-name", Image: "nonexistant-image"},
1616 },
1617 },
1618 },
1619 },
1620 }
1621 rc, err := clientset.CoreV1().ReplicationControllers(testNamespace).Create(ctx, rc, metav1.CreateOptions{})
1622 if err != nil {
1623 t.Fatalf("unable to create replicationcontroller: %v", err)
1624 }
1625 return rc, "", "replicationcontrollers"
1626 },
1627 subresource: "scale",
1628 },
1629 }
1630
1631 for i := range testcases {
1632 tc := testcases[i]
1633 t.Run(tc.name, func(t *testing.T) {
1634 obj, group, resource := tc.object(t)
1635
1636 cfg := dynamic.ConfigFor(config)
1637 if len(group) == 0 {
1638 cfg = dynamic.ConfigFor(kubeConfig)
1639 cfg.APIPath = "/api"
1640 } else {
1641 cfg.APIPath = "/apis"
1642 }
1643 cfg.GroupVersion = &schema.GroupVersion{Group: group, Version: "v1"}
1644
1645 client, err := restclient.RESTClientFor(cfg)
1646 if err != nil {
1647 t.Fatal(err)
1648 }
1649
1650 res := client.Get().
1651 Resource(resource).NamespaceIfScoped(obj.GetNamespace(), len(obj.GetNamespace()) > 0).
1652 SetHeader("Accept", tc.accept).
1653 Name(obj.GetName()).
1654 SubResource(tc.subresource).
1655 Do(ctx)
1656
1657 resObj, err := res.Get()
1658 if err != nil {
1659 t.Fatalf("failed to retrieve object from response: %v", err)
1660 }
1661 actualKind := resObj.GetObjectKind().GroupVersionKind().Kind
1662 if actualKind != "Table" {
1663 t.Fatalf("Expected Kind 'Table', got '%v'", actualKind)
1664 }
1665 })
1666 }
1667 }
1668
1669 func TestTransform(t *testing.T) {
1670 testNamespace := "test-transform"
1671 tearDown, config, _, err := fixtures.StartDefaultServer(t)
1672 if err != nil {
1673 t.Fatal(err)
1674 }
1675 defer tearDown()
1676
1677 ctx, clientset, kubeConfig, tearDownFn := setup(t)
1678 defer tearDownFn()
1679
1680 ns := framework.CreateNamespaceOrDie(clientset, testNamespace, t)
1681 defer framework.DeleteNamespaceOrDie(clientset, ns, t)
1682
1683 apiExtensionClient, err := apiextensionsclient.NewForConfig(config)
1684 if err != nil {
1685 t.Fatal(err)
1686 }
1687
1688 dynamicClient, err := dynamic.NewForConfig(config)
1689 if err != nil {
1690 t.Fatal(err)
1691 }
1692
1693 fooCRD := &apiextensionsv1.CustomResourceDefinition{
1694 ObjectMeta: metav1.ObjectMeta{
1695 Name: "foos.cr.bar.com",
1696 },
1697 Spec: apiextensionsv1.CustomResourceDefinitionSpec{
1698 Group: "cr.bar.com",
1699 Scope: apiextensionsv1.NamespaceScoped,
1700 Names: apiextensionsv1.CustomResourceDefinitionNames{
1701 Plural: "foos",
1702 Kind: "Foo",
1703 },
1704 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
1705 {
1706 Name: "v1",
1707 Served: true,
1708 Storage: true,
1709 Schema: fixtures.AllowAllSchema(),
1710 },
1711 },
1712 },
1713 }
1714 fooCRD, err = fixtures.CreateNewV1CustomResourceDefinition(fooCRD, apiExtensionClient, dynamicClient)
1715 if err != nil {
1716 t.Fatal(err)
1717 }
1718 crdGVR := schema.GroupVersionResource{Group: fooCRD.Spec.Group, Version: fooCRD.Spec.Versions[0].Name, Resource: "foos"}
1719 crclient := dynamicClient.Resource(crdGVR).Namespace(testNamespace)
1720
1721 previousList, err := crclient.List(ctx, metav1.ListOptions{})
1722 if err != nil {
1723 t.Fatalf("failed to list CRs before test: %v", err)
1724 }
1725 previousRV := previousList.GetResourceVersion()
1726
1727 testcases := []struct {
1728 name string
1729 accept string
1730 includeObject metav1.IncludeObjectPolicy
1731 object func(*testing.T) (metav1.Object, string, string)
1732 wantErr func(*testing.T, error)
1733 wantBody func(*testing.T, io.Reader)
1734 }{
1735 {
1736 name: "v1beta1 verify columns on cluster scoped resources",
1737 accept: "application/json;as=Table;g=meta.k8s.io;v=v1beta1",
1738 object: func(t *testing.T) (metav1.Object, string, string) {
1739 return &metav1.ObjectMeta{Name: "default", Namespace: ""}, "", "namespaces"
1740 },
1741 wantBody: func(t *testing.T, w io.Reader) {
1742 expectTableWatchEvents(t, 1, 3, metav1.IncludeMetadata, json.NewDecoder(w))
1743 },
1744 },
1745 {
1746 name: "v1beta1 verify columns on CRDs in json",
1747 accept: "application/json;as=Table;g=meta.k8s.io;v=v1beta1",
1748 object: func(t *testing.T) (metav1.Object, string, string) {
1749 cr, err := crclient.Create(ctx, &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "cr.bar.com/v1", "kind": "Foo", "metadata": map[string]interface{}{"name": "test-1"}}}, metav1.CreateOptions{})
1750 if err != nil {
1751 t.Fatalf("unable to create cr: %v", err)
1752 }
1753 if _, err := crclient.Patch(ctx, "test-1", types.MergePatchType, []byte(`{"metadata":{"annotations":{"test":"1"}}}`), metav1.PatchOptions{}); err != nil {
1754 t.Fatalf("unable to patch cr: %v", err)
1755 }
1756 return cr, crdGVR.Group, "foos"
1757 },
1758 wantBody: func(t *testing.T, w io.Reader) {
1759 expectTableWatchEvents(t, 2, 2, metav1.IncludeMetadata, json.NewDecoder(w))
1760 },
1761 },
1762 {
1763 name: "v1beta1 verify columns on CRDs in json;stream=watch",
1764 accept: "application/json;stream=watch;as=Table;g=meta.k8s.io;v=v1beta1",
1765 object: func(t *testing.T) (metav1.Object, string, string) {
1766 cr, err := crclient.Create(ctx, &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "cr.bar.com/v1", "kind": "Foo", "metadata": map[string]interface{}{"name": "test-2"}}}, metav1.CreateOptions{})
1767 if err != nil {
1768 t.Fatalf("unable to create cr: %v", err)
1769 }
1770 if _, err := crclient.Patch(ctx, "test-2", types.MergePatchType, []byte(`{"metadata":{"annotations":{"test":"1"}}}`), metav1.PatchOptions{}); err != nil {
1771 t.Fatalf("unable to patch cr: %v", err)
1772 }
1773 return cr, crdGVR.Group, "foos"
1774 },
1775 wantBody: func(t *testing.T, w io.Reader) {
1776 expectTableWatchEvents(t, 2, 2, metav1.IncludeMetadata, json.NewDecoder(w))
1777 },
1778 },
1779 {
1780 name: "v1beta1 verify columns on CRDs in yaml",
1781 accept: "application/yaml;as=Table;g=meta.k8s.io;v=v1beta1",
1782 object: func(t *testing.T) (metav1.Object, string, string) {
1783 cr, err := crclient.Create(ctx, &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "cr.bar.com/v1", "kind": "Foo", "metadata": map[string]interface{}{"name": "test-3"}}}, metav1.CreateOptions{})
1784 if err != nil {
1785 t.Fatalf("unable to create cr: %v", err)
1786 }
1787 if _, err := crclient.Patch(ctx, "test-3", types.MergePatchType, []byte(`{"metadata":{"annotations":{"test":"1"}}}`), metav1.PatchOptions{}); err != nil {
1788 t.Fatalf("unable to patch cr: %v", err)
1789 }
1790 return cr, crdGVR.Group, "foos"
1791 },
1792 wantErr: func(t *testing.T, err error) {
1793 if !apierrors.IsNotAcceptable(err) {
1794 t.Fatal(err)
1795 }
1796
1797 if err.Error() != "only the following media types are accepted: application/json;stream=watch, application/vnd.kubernetes.protobuf;stream=watch" {
1798 t.Fatal(err)
1799 }
1800 },
1801 },
1802 {
1803 name: "v1beta1 verify columns on services",
1804 accept: "application/json;as=Table;g=meta.k8s.io;v=v1beta1",
1805 object: func(t *testing.T) (metav1.Object, string, string) {
1806 svc, err := clientset.CoreV1().Services(testNamespace).Create(ctx, &v1.Service{ObjectMeta: metav1.ObjectMeta{Name: "test-1"}, Spec: v1.ServiceSpec{Ports: []v1.ServicePort{{Port: 1000}}}}, metav1.CreateOptions{})
1807 if err != nil {
1808 t.Fatalf("unable to create service: %v", err)
1809 }
1810 if _, err := clientset.CoreV1().Services(testNamespace).Patch(ctx, svc.Name, types.MergePatchType, []byte(`{"metadata":{"annotations":{"test":"1"}}}`), metav1.PatchOptions{}); err != nil {
1811 t.Fatalf("unable to update service: %v", err)
1812 }
1813 return svc, "", "services"
1814 },
1815 wantBody: func(t *testing.T, w io.Reader) {
1816 expectTableWatchEvents(t, 2, 7, metav1.IncludeMetadata, json.NewDecoder(w))
1817 },
1818 },
1819 {
1820 name: "v1beta1 verify columns on services with no object",
1821 accept: "application/json;as=Table;g=meta.k8s.io;v=v1beta1",
1822 includeObject: metav1.IncludeNone,
1823 object: func(t *testing.T) (metav1.Object, string, string) {
1824 obj, err := clientset.CoreV1().Services(testNamespace).Create(ctx, &v1.Service{ObjectMeta: metav1.ObjectMeta{Name: "test-2"}, Spec: v1.ServiceSpec{Ports: []v1.ServicePort{{Port: 1000}}}}, metav1.CreateOptions{})
1825 if err != nil {
1826 t.Fatalf("unable to create object: %v", err)
1827 }
1828 if _, err := clientset.CoreV1().Services(testNamespace).Patch(ctx, obj.Name, types.MergePatchType, []byte(`{"metadata":{"annotations":{"test":"1"}}}`), metav1.PatchOptions{}); err != nil {
1829 t.Fatalf("unable to update object: %v", err)
1830 }
1831 return obj, "", "services"
1832 },
1833 wantBody: func(t *testing.T, w io.Reader) {
1834 expectTableWatchEvents(t, 2, 7, metav1.IncludeNone, json.NewDecoder(w))
1835 },
1836 },
1837 {
1838 name: "v1beta1 verify columns on services with full object",
1839 accept: "application/json;as=Table;g=meta.k8s.io;v=v1beta1",
1840 includeObject: metav1.IncludeObject,
1841 object: func(t *testing.T) (metav1.Object, string, string) {
1842 obj, err := clientset.CoreV1().Services(testNamespace).Create(ctx, &v1.Service{ObjectMeta: metav1.ObjectMeta{Name: "test-3"}, Spec: v1.ServiceSpec{Ports: []v1.ServicePort{{Port: 1000}}}}, metav1.CreateOptions{})
1843 if err != nil {
1844 t.Fatalf("unable to create object: %v", err)
1845 }
1846 if _, err := clientset.CoreV1().Services(testNamespace).Patch(ctx, obj.Name, types.MergePatchType, []byte(`{"metadata":{"annotations":{"test":"1"}}}`), metav1.PatchOptions{}); err != nil {
1847 t.Fatalf("unable to update object: %v", err)
1848 }
1849 return obj, "", "services"
1850 },
1851 wantBody: func(t *testing.T, w io.Reader) {
1852 objects := expectTableWatchEvents(t, 2, 7, metav1.IncludeObject, json.NewDecoder(w))
1853 var svc v1.Service
1854 if err := json.Unmarshal(objects[1], &svc); err != nil {
1855 t.Fatal(err)
1856 }
1857 if svc.Annotations["test"] != "1" || svc.Spec.Ports[0].Port != 1000 {
1858 t.Fatalf("unexpected object: %#v", svc)
1859 }
1860 },
1861 },
1862 {
1863 name: "v1beta1 verify partial metadata object on config maps",
1864 accept: "application/json;as=PartialObjectMetadata;g=meta.k8s.io;v=v1beta1",
1865 object: func(t *testing.T) (metav1.Object, string, string) {
1866 obj, err := clientset.CoreV1().ConfigMaps(testNamespace).Create(ctx, &v1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "test-1", Annotations: map[string]string{"test": "0"}}}, metav1.CreateOptions{})
1867 if err != nil {
1868 t.Fatalf("unable to create object: %v", err)
1869 }
1870 if _, err := clientset.CoreV1().ConfigMaps(testNamespace).Patch(ctx, obj.Name, types.MergePatchType, []byte(`{"metadata":{"annotations":{"test":"1"}}}`), metav1.PatchOptions{}); err != nil {
1871 t.Fatalf("unable to update object: %v", err)
1872 }
1873 return obj, "", "configmaps"
1874 },
1875 wantBody: func(t *testing.T, w io.Reader) {
1876 expectPartialObjectMetaEvents(t, json.NewDecoder(w), "0", "1")
1877 },
1878 },
1879 {
1880 name: "v1beta1 verify partial metadata object on config maps in protobuf",
1881 accept: "application/vnd.kubernetes.protobuf;as=PartialObjectMetadata;g=meta.k8s.io;v=v1beta1",
1882 object: func(t *testing.T) (metav1.Object, string, string) {
1883 obj, err := clientset.CoreV1().ConfigMaps(testNamespace).Create(ctx, &v1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "test-2", Annotations: map[string]string{"test": "0"}}}, metav1.CreateOptions{})
1884 if err != nil {
1885 t.Fatalf("unable to create object: %v", err)
1886 }
1887 if _, err := clientset.CoreV1().ConfigMaps(testNamespace).Patch(ctx, obj.Name, types.MergePatchType, []byte(`{"metadata":{"annotations":{"test":"1"}}}`), metav1.PatchOptions{}); err != nil {
1888 t.Fatalf("unable to update object: %v", err)
1889 }
1890 return obj, "", "configmaps"
1891 },
1892 wantBody: func(t *testing.T, w io.Reader) {
1893 expectPartialObjectMetaEventsProtobuf(t, w, "0", "1")
1894 },
1895 },
1896 {
1897 name: "v1beta1 verify partial metadata object on CRDs in protobuf",
1898 accept: "application/vnd.kubernetes.protobuf;as=PartialObjectMetadata;g=meta.k8s.io;v=v1beta1",
1899 object: func(t *testing.T) (metav1.Object, string, string) {
1900 cr, err := crclient.Create(ctx, &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "cr.bar.com/v1", "kind": "Foo", "metadata": map[string]interface{}{"name": "test-4", "annotations": map[string]string{"test": "0"}}}}, metav1.CreateOptions{})
1901 if err != nil {
1902 t.Fatalf("unable to create cr: %v", err)
1903 }
1904 if _, err := crclient.Patch(ctx, "test-4", types.MergePatchType, []byte(`{"metadata":{"annotations":{"test":"1"}}}`), metav1.PatchOptions{}); err != nil {
1905 t.Fatalf("unable to patch cr: %v", err)
1906 }
1907 return cr, crdGVR.Group, "foos"
1908 },
1909 wantBody: func(t *testing.T, w io.Reader) {
1910 expectPartialObjectMetaEventsProtobuf(t, w, "0", "1")
1911 },
1912 },
1913 {
1914 name: "v1beta1 verify error on unsupported mimetype protobuf for table conversion",
1915 accept: "application/vnd.kubernetes.protobuf;as=Table;g=meta.k8s.io;v=v1beta1",
1916 object: func(t *testing.T) (metav1.Object, string, string) {
1917 return &metav1.ObjectMeta{Name: "kubernetes", Namespace: "default"}, "", "services"
1918 },
1919 wantErr: func(t *testing.T, err error) {
1920 if !apierrors.IsNotAcceptable(err) {
1921 t.Fatal(err)
1922 }
1923
1924 if err.Error() != "only the following media types are accepted: application/json, application/yaml, application/vnd.kubernetes.protobuf" {
1925 t.Fatal(err)
1926 }
1927 },
1928 },
1929 {
1930 name: "verify error on invalid mimetype - bad version",
1931 accept: "application/json;as=PartialObjectMetadata;g=meta.k8s.io;v=v1alpha1",
1932 object: func(t *testing.T) (metav1.Object, string, string) {
1933 return &metav1.ObjectMeta{Name: "kubernetes", Namespace: "default"}, "", "services"
1934 },
1935 wantErr: func(t *testing.T, err error) {
1936 if !apierrors.IsNotAcceptable(err) {
1937 t.Fatal(err)
1938 }
1939 },
1940 },
1941 {
1942 name: "v1beta1 verify error on invalid mimetype - bad group",
1943 accept: "application/json;as=PartialObjectMetadata;g=k8s.io;v=v1beta1",
1944 object: func(t *testing.T) (metav1.Object, string, string) {
1945 return &metav1.ObjectMeta{Name: "kubernetes", Namespace: "default"}, "", "services"
1946 },
1947 wantErr: func(t *testing.T, err error) {
1948 if !apierrors.IsNotAcceptable(err) {
1949 t.Fatal(err)
1950 }
1951 },
1952 },
1953 {
1954 name: "v1beta1 verify error on invalid mimetype - bad kind",
1955 accept: "application/json;as=PartialObject;g=meta.k8s.io;v=v1beta1",
1956 object: func(t *testing.T) (metav1.Object, string, string) {
1957 return &metav1.ObjectMeta{Name: "kubernetes", Namespace: "default"}, "", "services"
1958 },
1959 wantErr: func(t *testing.T, err error) {
1960 if !apierrors.IsNotAcceptable(err) {
1961 t.Fatal(err)
1962 }
1963 },
1964 },
1965 {
1966 name: "v1beta1 verify error on invalid mimetype - missing kind",
1967 accept: "application/json;g=meta.k8s.io;v=v1beta1",
1968 object: func(t *testing.T) (metav1.Object, string, string) {
1969 return &metav1.ObjectMeta{Name: "kubernetes", Namespace: "default"}, "", "services"
1970 },
1971 wantErr: func(t *testing.T, err error) {
1972 if !apierrors.IsNotAcceptable(err) {
1973 t.Fatal(err)
1974 }
1975 },
1976 },
1977 {
1978 name: "v1beta1 verify error on invalid transform parameter",
1979 accept: "application/json;as=Table;g=meta.k8s.io;v=v1beta1",
1980 includeObject: metav1.IncludeObjectPolicy("unrecognized"),
1981 object: func(t *testing.T) (metav1.Object, string, string) {
1982 return &metav1.ObjectMeta{Name: "kubernetes", Namespace: "default"}, "", "services"
1983 },
1984 wantErr: func(t *testing.T, err error) {
1985 if !apierrors.IsBadRequest(err) || !strings.Contains(err.Error(), `Invalid value: "unrecognized": must be 'Metadata', 'Object', 'None', or empty`) {
1986 t.Fatal(err)
1987 }
1988 },
1989 },
1990
1991 {
1992 name: "v1 verify columns on cluster scoped resources",
1993 accept: "application/json;as=Table;g=meta.k8s.io;v=v1",
1994 object: func(t *testing.T) (metav1.Object, string, string) {
1995 return &metav1.ObjectMeta{Name: "default", Namespace: ""}, "", "namespaces"
1996 },
1997 wantBody: func(t *testing.T, w io.Reader) {
1998 expectTableV1WatchEvents(t, 1, 3, metav1.IncludeMetadata, json.NewDecoder(w))
1999 },
2000 },
2001 {
2002 name: "v1 verify columns on CRDs in json",
2003 accept: "application/json;as=Table;g=meta.k8s.io;v=v1",
2004 object: func(t *testing.T) (metav1.Object, string, string) {
2005 cr, err := crclient.Create(ctx, &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "cr.bar.com/v1", "kind": "Foo", "metadata": map[string]interface{}{"name": "test-5"}}}, metav1.CreateOptions{})
2006 if err != nil {
2007 t.Fatalf("unable to create cr: %v", err)
2008 }
2009 if _, err := crclient.Patch(ctx, "test-5", types.MergePatchType, []byte(`{"metadata":{"annotations":{"test":"1"}}}`), metav1.PatchOptions{}); err != nil {
2010 t.Fatalf("unable to patch cr: %v", err)
2011 }
2012 return cr, crdGVR.Group, "foos"
2013 },
2014 wantBody: func(t *testing.T, w io.Reader) {
2015 expectTableV1WatchEvents(t, 2, 2, metav1.IncludeMetadata, json.NewDecoder(w))
2016 },
2017 },
2018 {
2019 name: "v1 verify columns on CRDs in json;stream=watch",
2020 accept: "application/json;stream=watch;as=Table;g=meta.k8s.io;v=v1",
2021 object: func(t *testing.T) (metav1.Object, string, string) {
2022 cr, err := crclient.Create(ctx, &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "cr.bar.com/v1", "kind": "Foo", "metadata": map[string]interface{}{"name": "test-6"}}}, metav1.CreateOptions{})
2023 if err != nil {
2024 t.Fatalf("unable to create cr: %v", err)
2025 }
2026 if _, err := crclient.Patch(ctx, "test-6", types.MergePatchType, []byte(`{"metadata":{"annotations":{"test":"1"}}}`), metav1.PatchOptions{}); err != nil {
2027 t.Fatalf("unable to patch cr: %v", err)
2028 }
2029 return cr, crdGVR.Group, "foos"
2030 },
2031 wantBody: func(t *testing.T, w io.Reader) {
2032 expectTableV1WatchEvents(t, 2, 2, metav1.IncludeMetadata, json.NewDecoder(w))
2033 },
2034 },
2035 {
2036 name: "v1 verify columns on CRDs in yaml",
2037 accept: "application/yaml;as=Table;g=meta.k8s.io;v=v1",
2038 object: func(t *testing.T) (metav1.Object, string, string) {
2039 cr, err := crclient.Create(ctx, &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "cr.bar.com/v1", "kind": "Foo", "metadata": map[string]interface{}{"name": "test-7"}}}, metav1.CreateOptions{})
2040 if err != nil {
2041 t.Fatalf("unable to create cr: %v", err)
2042 }
2043 if _, err := crclient.Patch(ctx, "test-7", types.MergePatchType, []byte(`{"metadata":{"annotations":{"test":"1"}}}`), metav1.PatchOptions{}); err != nil {
2044 t.Fatalf("unable to patch cr: %v", err)
2045 }
2046 return cr, crdGVR.Group, "foos"
2047 },
2048 wantErr: func(t *testing.T, err error) {
2049 if !apierrors.IsNotAcceptable(err) {
2050 t.Fatal(err)
2051 }
2052
2053 if err.Error() != "only the following media types are accepted: application/json;stream=watch, application/vnd.kubernetes.protobuf;stream=watch" {
2054 t.Fatal(err)
2055 }
2056 },
2057 },
2058 {
2059 name: "v1 verify columns on services",
2060 accept: "application/json;as=Table;g=meta.k8s.io;v=v1",
2061 object: func(t *testing.T) (metav1.Object, string, string) {
2062 svc, err := clientset.CoreV1().Services(testNamespace).Create(ctx, &v1.Service{ObjectMeta: metav1.ObjectMeta{Name: "test-5"}, Spec: v1.ServiceSpec{Ports: []v1.ServicePort{{Port: 1000}}}}, metav1.CreateOptions{})
2063 if err != nil {
2064 t.Fatalf("unable to create service: %v", err)
2065 }
2066 if _, err := clientset.CoreV1().Services(testNamespace).Patch(ctx, svc.Name, types.MergePatchType, []byte(`{"metadata":{"annotations":{"test":"1"}}}`), metav1.PatchOptions{}); err != nil {
2067 t.Fatalf("unable to update service: %v", err)
2068 }
2069 return svc, "", "services"
2070 },
2071 wantBody: func(t *testing.T, w io.Reader) {
2072 expectTableV1WatchEvents(t, 2, 7, metav1.IncludeMetadata, json.NewDecoder(w))
2073 },
2074 },
2075 {
2076 name: "v1 verify columns on services with no object",
2077 accept: "application/json;as=Table;g=meta.k8s.io;v=v1",
2078 includeObject: metav1.IncludeNone,
2079 object: func(t *testing.T) (metav1.Object, string, string) {
2080 obj, err := clientset.CoreV1().Services(testNamespace).Create(ctx, &v1.Service{ObjectMeta: metav1.ObjectMeta{Name: "test-6"}, Spec: v1.ServiceSpec{Ports: []v1.ServicePort{{Port: 1000}}}}, metav1.CreateOptions{})
2081 if err != nil {
2082 t.Fatalf("unable to create object: %v", err)
2083 }
2084 if _, err := clientset.CoreV1().Services(testNamespace).Patch(ctx, obj.Name, types.MergePatchType, []byte(`{"metadata":{"annotations":{"test":"1"}}}`), metav1.PatchOptions{}); err != nil {
2085 t.Fatalf("unable to update object: %v", err)
2086 }
2087 return obj, "", "services"
2088 },
2089 wantBody: func(t *testing.T, w io.Reader) {
2090 expectTableV1WatchEvents(t, 2, 7, metav1.IncludeNone, json.NewDecoder(w))
2091 },
2092 },
2093 {
2094 name: "v1 verify columns on services with full object",
2095 accept: "application/json;as=Table;g=meta.k8s.io;v=v1",
2096 includeObject: metav1.IncludeObject,
2097 object: func(t *testing.T) (metav1.Object, string, string) {
2098 obj, err := clientset.CoreV1().Services(testNamespace).Create(ctx, &v1.Service{ObjectMeta: metav1.ObjectMeta{Name: "test-7"}, Spec: v1.ServiceSpec{Ports: []v1.ServicePort{{Port: 1000}}}}, metav1.CreateOptions{})
2099 if err != nil {
2100 t.Fatalf("unable to create object: %v", err)
2101 }
2102 if _, err := clientset.CoreV1().Services(testNamespace).Patch(ctx, obj.Name, types.MergePatchType, []byte(`{"metadata":{"annotations":{"test":"1"}}}`), metav1.PatchOptions{}); err != nil {
2103 t.Fatalf("unable to update object: %v", err)
2104 }
2105 return obj, "", "services"
2106 },
2107 wantBody: func(t *testing.T, w io.Reader) {
2108 objects := expectTableV1WatchEvents(t, 2, 7, metav1.IncludeObject, json.NewDecoder(w))
2109 var svc v1.Service
2110 if err := json.Unmarshal(objects[1], &svc); err != nil {
2111 t.Fatal(err)
2112 }
2113 if svc.Annotations["test"] != "1" || svc.Spec.Ports[0].Port != 1000 {
2114 t.Fatalf("unexpected object: %#v", svc)
2115 }
2116 },
2117 },
2118 {
2119 name: "v1 verify partial metadata object on config maps",
2120 accept: "application/json;as=PartialObjectMetadata;g=meta.k8s.io;v=v1",
2121 object: func(t *testing.T) (metav1.Object, string, string) {
2122 obj, err := clientset.CoreV1().ConfigMaps(testNamespace).Create(ctx, &v1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "test-3", Annotations: map[string]string{"test": "0"}}}, metav1.CreateOptions{})
2123 if err != nil {
2124 t.Fatalf("unable to create object: %v", err)
2125 }
2126 if _, err := clientset.CoreV1().ConfigMaps(testNamespace).Patch(ctx, obj.Name, types.MergePatchType, []byte(`{"metadata":{"annotations":{"test":"1"}}}`), metav1.PatchOptions{}); err != nil {
2127 t.Fatalf("unable to update object: %v", err)
2128 }
2129 return obj, "", "configmaps"
2130 },
2131 wantBody: func(t *testing.T, w io.Reader) {
2132 expectPartialObjectMetaV1Events(t, json.NewDecoder(w), "0", "1")
2133 },
2134 },
2135 {
2136 name: "v1 verify partial metadata object on config maps in protobuf",
2137 accept: "application/vnd.kubernetes.protobuf;as=PartialObjectMetadata;g=meta.k8s.io;v=v1",
2138 object: func(t *testing.T) (metav1.Object, string, string) {
2139 obj, err := clientset.CoreV1().ConfigMaps(testNamespace).Create(ctx, &v1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "test-4", Annotations: map[string]string{"test": "0"}}}, metav1.CreateOptions{})
2140 if err != nil {
2141 t.Fatalf("unable to create object: %v", err)
2142 }
2143 if _, err := clientset.CoreV1().ConfigMaps(testNamespace).Patch(ctx, obj.Name, types.MergePatchType, []byte(`{"metadata":{"annotations":{"test":"1"}}}`), metav1.PatchOptions{}); err != nil {
2144 t.Fatalf("unable to update object: %v", err)
2145 }
2146 return obj, "", "configmaps"
2147 },
2148 wantBody: func(t *testing.T, w io.Reader) {
2149 expectPartialObjectMetaV1EventsProtobuf(t, w, "0", "1")
2150 },
2151 },
2152 {
2153 name: "v1 verify partial metadata object on CRDs in protobuf",
2154 accept: "application/vnd.kubernetes.protobuf;as=PartialObjectMetadata;g=meta.k8s.io;v=v1",
2155 object: func(t *testing.T) (metav1.Object, string, string) {
2156 cr, err := crclient.Create(ctx, &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "cr.bar.com/v1", "kind": "Foo", "metadata": map[string]interface{}{"name": "test-8", "annotations": map[string]string{"test": "0"}}}}, metav1.CreateOptions{})
2157 if err != nil {
2158 t.Fatalf("unable to create cr: %v", err)
2159 }
2160 if _, err := crclient.Patch(ctx, cr.GetName(), types.MergePatchType, []byte(`{"metadata":{"annotations":{"test":"1"}}}`), metav1.PatchOptions{}); err != nil {
2161 t.Fatalf("unable to patch cr: %v", err)
2162 }
2163 return cr, crdGVR.Group, "foos"
2164 },
2165 wantBody: func(t *testing.T, w io.Reader) {
2166 expectPartialObjectMetaV1EventsProtobuf(t, w, "0", "1")
2167 },
2168 },
2169 {
2170 name: "v1 verify error on unsupported mimetype protobuf for table conversion",
2171 accept: "application/vnd.kubernetes.protobuf;as=Table;g=meta.k8s.io;v=v1",
2172 object: func(t *testing.T) (metav1.Object, string, string) {
2173 return &metav1.ObjectMeta{Name: "kubernetes", Namespace: "default"}, "", "services"
2174 },
2175 wantErr: func(t *testing.T, err error) {
2176 if !apierrors.IsNotAcceptable(err) {
2177 t.Fatal(err)
2178 }
2179
2180 if err.Error() != "only the following media types are accepted: application/json, application/yaml, application/vnd.kubernetes.protobuf" {
2181 t.Fatal(err)
2182 }
2183 },
2184 },
2185 {
2186 name: "v1 verify error on invalid mimetype - bad group",
2187 accept: "application/json;as=PartialObjectMetadata;g=k8s.io;v=v1",
2188 object: func(t *testing.T) (metav1.Object, string, string) {
2189 return &metav1.ObjectMeta{Name: "kubernetes", Namespace: "default"}, "", "services"
2190 },
2191 wantErr: func(t *testing.T, err error) {
2192 if !apierrors.IsNotAcceptable(err) {
2193 t.Fatal(err)
2194 }
2195 },
2196 },
2197 {
2198 name: "v1 verify error on invalid mimetype - bad kind",
2199 accept: "application/json;as=PartialObject;g=meta.k8s.io;v=v1",
2200 object: func(t *testing.T) (metav1.Object, string, string) {
2201 return &metav1.ObjectMeta{Name: "kubernetes", Namespace: "default"}, "", "services"
2202 },
2203 wantErr: func(t *testing.T, err error) {
2204 if !apierrors.IsNotAcceptable(err) {
2205 t.Fatal(err)
2206 }
2207 },
2208 },
2209 {
2210 name: "v1 verify error on invalid mimetype - only meta kinds accepted",
2211 accept: "application/json;as=Service;g=;v=v1",
2212 object: func(t *testing.T) (metav1.Object, string, string) {
2213 return &metav1.ObjectMeta{Name: "kubernetes", Namespace: "default"}, "", "services"
2214 },
2215 wantErr: func(t *testing.T, err error) {
2216 if !apierrors.IsNotAcceptable(err) {
2217 t.Fatal(err)
2218 }
2219 },
2220 },
2221 {
2222 name: "v1 verify error on invalid mimetype - missing kind",
2223 accept: "application/json;g=meta.k8s.io;v=v1",
2224 object: func(t *testing.T) (metav1.Object, string, string) {
2225 return &metav1.ObjectMeta{Name: "kubernetes", Namespace: "default"}, "", "services"
2226 },
2227 wantErr: func(t *testing.T, err error) {
2228 if !apierrors.IsNotAcceptable(err) {
2229 t.Fatal(err)
2230 }
2231 },
2232 },
2233 {
2234 name: "v1 verify error on invalid transform parameter",
2235 accept: "application/json;as=Table;g=meta.k8s.io;v=v1",
2236 includeObject: metav1.IncludeObjectPolicy("unrecognized"),
2237 object: func(t *testing.T) (metav1.Object, string, string) {
2238 return &metav1.ObjectMeta{Name: "kubernetes", Namespace: "default"}, "", "services"
2239 },
2240 wantErr: func(t *testing.T, err error) {
2241 if !apierrors.IsBadRequest(err) || !strings.Contains(err.Error(), `Invalid value: "unrecognized": must be 'Metadata', 'Object', 'None', or empty`) {
2242 t.Fatal(err)
2243 }
2244 },
2245 },
2246 }
2247
2248 for i := range testcases {
2249 tc := testcases[i]
2250 t.Run(tc.name, func(t *testing.T) {
2251 obj, group, resource := tc.object(t)
2252
2253 cfg := dynamic.ConfigFor(config)
2254 if len(group) == 0 {
2255 cfg = dynamic.ConfigFor(kubeConfig)
2256 cfg.APIPath = "/api"
2257 } else {
2258 cfg.APIPath = "/apis"
2259 }
2260 cfg.GroupVersion = &schema.GroupVersion{Group: group, Version: "v1"}
2261
2262 client, err := restclient.RESTClientFor(cfg)
2263 if err != nil {
2264 t.Fatal(err)
2265 }
2266
2267 var rv string
2268 if obj.GetResourceVersion() == "" || obj.GetResourceVersion() == "0" {
2269
2270 rv = "0"
2271 } else {
2272
2273 rv = previousRV
2274 }
2275
2276 timeoutCtx, timeoutCancel := context.WithTimeout(ctx, wait.ForeverTestTimeout)
2277 t.Cleanup(func() {
2278 timeoutCancel()
2279 })
2280 w, err := client.Get().
2281 Resource(resource).NamespaceIfScoped(obj.GetNamespace(), len(obj.GetNamespace()) > 0).
2282 SetHeader("Accept", tc.accept).
2283 VersionedParams(&metav1.ListOptions{
2284 ResourceVersion: rv,
2285 Watch: true,
2286 FieldSelector: fields.OneTermEqualSelector("metadata.name", obj.GetName()).String(),
2287 }, metav1.ParameterCodec).
2288 Param("includeObject", string(tc.includeObject)).
2289 Stream(timeoutCtx)
2290 if (tc.wantErr != nil) != (err != nil) {
2291 t.Fatalf("unexpected error: %v", err)
2292 }
2293 if tc.wantErr != nil {
2294 tc.wantErr(t, err)
2295 return
2296 }
2297 if err != nil {
2298 t.Fatal(err)
2299 }
2300 defer w.Close()
2301 tc.wantBody(t, w)
2302 })
2303 }
2304 }
2305
2306 func TestWatchTransformCaching(t *testing.T) {
2307 ctx, clientSet, _, tearDownFn := setup(t)
2308 defer tearDownFn()
2309
2310 ns := framework.CreateNamespaceOrDie(clientSet, "watch-transform", t)
2311 defer framework.DeleteNamespaceOrDie(clientSet, ns, t)
2312
2313 list, err := clientSet.CoreV1().ConfigMaps(ns.Name).List(ctx, metav1.ListOptions{})
2314 if err != nil {
2315 t.Fatalf("Failed to list objects: %v", err)
2316 }
2317
2318 timeout := 30 * time.Second
2319 listOptions := &metav1.ListOptions{
2320 ResourceVersion: list.ResourceVersion,
2321 Watch: true,
2322 }
2323
2324 wMeta, err := clientSet.CoreV1().RESTClient().Get().
2325 AbsPath("/api/v1/namespaces/watch-transform/configmaps").
2326 SetHeader("Accept", "application/vnd.kubernetes.protobuf;as=PartialObjectMetadata;g=meta.k8s.io;v=v1beta1").
2327 VersionedParams(listOptions, metav1.ParameterCodec).
2328 Timeout(timeout).
2329 Stream(ctx)
2330 if err != nil {
2331 t.Fatalf("Failed to start meta watch: %v", err)
2332 }
2333 defer wMeta.Close()
2334
2335 wTableIncludeMeta, err := clientSet.CoreV1().RESTClient().Get().
2336 AbsPath("/api/v1/namespaces/watch-transform/configmaps").
2337 SetHeader("Accept", "application/json;as=Table;g=meta.k8s.io;v=v1beta1").
2338 VersionedParams(listOptions, metav1.ParameterCodec).
2339 Param("includeObject", string(metav1.IncludeMetadata)).
2340 Timeout(timeout).
2341 Stream(ctx)
2342 if err != nil {
2343 t.Fatalf("Failed to start table meta watch: %v", err)
2344 }
2345 defer wTableIncludeMeta.Close()
2346
2347 wTableIncludeObject, err := clientSet.CoreV1().RESTClient().Get().
2348 AbsPath("/api/v1/namespaces/watch-transform/configmaps").
2349 SetHeader("Accept", "application/json;as=Table;g=meta.k8s.io;v=v1beta1").
2350 VersionedParams(listOptions, metav1.ParameterCodec).
2351 Param("includeObject", string(metav1.IncludeObject)).
2352 Timeout(timeout).
2353 Stream(ctx)
2354 if err != nil {
2355 t.Fatalf("Failed to start table object watch: %v", err)
2356 }
2357 defer wTableIncludeObject.Close()
2358
2359 wTableIncludeObjectFiltered, err := clientSet.CoreV1().RESTClient().Get().
2360 AbsPath("/api/v1/namespaces/watch-transform/configmaps").
2361 SetHeader("Accept", "application/json;as=Table;g=meta.k8s.io;v=v1beta1").
2362 VersionedParams(listOptions, metav1.ParameterCodec).
2363 Param("includeObject", string(metav1.IncludeObject)).
2364 Param("labelSelector", "foo=bar").
2365 Timeout(timeout).
2366 Stream(ctx)
2367 if err != nil {
2368 t.Fatalf("Failed to start table object watch: %v", err)
2369 }
2370 defer wTableIncludeObjectFiltered.Close()
2371
2372 configMap, err := clientSet.CoreV1().ConfigMaps("watch-transform").Create(ctx, &v1.ConfigMap{
2373 ObjectMeta: metav1.ObjectMeta{Name: "test1"},
2374 Data: map[string]string{
2375 "foo": "bar",
2376 },
2377 }, metav1.CreateOptions{})
2378 if err != nil {
2379 t.Fatalf("Failed to create a configMap: %v", err)
2380 }
2381
2382 listOptionsDelayed := &metav1.ListOptions{
2383 ResourceVersion: configMap.ResourceVersion,
2384 Watch: true,
2385 }
2386 wTableIncludeObjectDelayed, err := clientSet.CoreV1().RESTClient().Get().
2387 AbsPath("/api/v1/namespaces/watch-transform/configmaps").
2388 SetHeader("Accept", "application/json;as=Table;g=meta.k8s.io;v=v1beta1").
2389 VersionedParams(listOptionsDelayed, metav1.ParameterCodec).
2390 Param("includeObject", string(metav1.IncludeObject)).
2391 Timeout(timeout).
2392 Stream(ctx)
2393 if err != nil {
2394 t.Fatalf("Failed to start table object watch: %v", err)
2395 }
2396 defer wTableIncludeObjectDelayed.Close()
2397
2398 configMap2, err := clientSet.CoreV1().ConfigMaps("watch-transform").Create(ctx, &v1.ConfigMap{
2399 ObjectMeta: metav1.ObjectMeta{Name: "test2"},
2400 Data: map[string]string{
2401 "foo": "bar",
2402 },
2403 }, metav1.CreateOptions{})
2404 if err != nil {
2405 t.Fatalf("Failed to create a second configMap: %v", err)
2406 }
2407
2408
2409
2410
2411
2412 configMapUpdated, err := clientSet.CoreV1().ConfigMaps("watch-transform").Update(ctx, &v1.ConfigMap{
2413 ObjectMeta: metav1.ObjectMeta{Name: "test1", Labels: map[string]string{"foo": "bar"}},
2414 Data: map[string]string{
2415 "foo": "baz",
2416 },
2417 }, metav1.UpdateOptions{})
2418 if err != nil {
2419 t.Fatalf("Failed to update a configMap: %v", err)
2420 }
2421
2422 configMap2Updated, err := clientSet.CoreV1().ConfigMaps("watch-transform").Update(ctx, &v1.ConfigMap{
2423 ObjectMeta: metav1.ObjectMeta{Name: "test2", Labels: map[string]string{"foo": "bar"}},
2424 Data: map[string]string{
2425 "foo": "baz",
2426 },
2427 }, metav1.UpdateOptions{})
2428 if err != nil {
2429 t.Fatalf("Failed to update a second configMap: %v", err)
2430 }
2431
2432 metaChecks := []partialObjectMetadataCheck{
2433 func(res *metav1beta1.PartialObjectMetadata) {
2434 if !apiequality.Semantic.DeepEqual(configMap.ObjectMeta, res.ObjectMeta) {
2435 t.Errorf("expected object: %#v, got: %#v", configMap.ObjectMeta, res.ObjectMeta)
2436 }
2437 },
2438 func(res *metav1beta1.PartialObjectMetadata) {
2439 if !apiequality.Semantic.DeepEqual(configMap2.ObjectMeta, res.ObjectMeta) {
2440 t.Errorf("expected object: %#v, got: %#v", configMap2.ObjectMeta, res.ObjectMeta)
2441 }
2442 },
2443 func(res *metav1beta1.PartialObjectMetadata) {
2444 if !apiequality.Semantic.DeepEqual(configMapUpdated.ObjectMeta, res.ObjectMeta) {
2445 t.Errorf("expected object: %#v, got: %#v", configMapUpdated.ObjectMeta, res.ObjectMeta)
2446 }
2447 },
2448 func(res *metav1beta1.PartialObjectMetadata) {
2449 if !apiequality.Semantic.DeepEqual(configMap2Updated.ObjectMeta, res.ObjectMeta) {
2450 t.Errorf("expected object: %#v, got: %#v", configMap2Updated.ObjectMeta, res.ObjectMeta)
2451 }
2452 },
2453 }
2454 expectPartialObjectMetaEventsProtobufChecks(t, wMeta, metaChecks)
2455
2456 tableMetaCheck := func(expected *v1.ConfigMap, got []byte) {
2457 var obj metav1.PartialObjectMetadata
2458 if err := json.Unmarshal(got, &obj); err != nil {
2459 t.Fatal(err)
2460 }
2461 if !apiequality.Semantic.DeepEqual(expected.ObjectMeta, obj.ObjectMeta) {
2462 t.Errorf("expected object: %#v, got: %#v", expected, obj)
2463 }
2464 }
2465
2466 objectMetas := expectTableWatchEvents(t, 4, 3, metav1.IncludeMetadata, json.NewDecoder(wTableIncludeMeta))
2467 tableMetaCheck(configMap, objectMetas[0])
2468 tableMetaCheck(configMap2, objectMetas[1])
2469 tableMetaCheck(configMapUpdated, objectMetas[2])
2470 tableMetaCheck(configMap2Updated, objectMetas[3])
2471
2472 tableObjectCheck := func(expectedType watch.EventType, expectedObj *v1.ConfigMap, got streamedEvent) {
2473 var obj *v1.ConfigMap
2474 if err := json.Unmarshal(got.rawObject, &obj); err != nil {
2475 t.Fatal(err)
2476 }
2477 if expectedType != watch.EventType(got.eventType) {
2478 t.Errorf("expected type: %#v, got: %#v", expectedType, got.eventType)
2479 }
2480 obj.TypeMeta = metav1.TypeMeta{}
2481 if !apiequality.Semantic.DeepEqual(expectedObj, obj) {
2482 t.Errorf("expected object: %#v, got: %#v", expectedObj, obj)
2483 }
2484 }
2485
2486 objects := expectTableWatchEventsWithTypes(t, 4, 3, metav1.IncludeObject, json.NewDecoder(wTableIncludeObject))
2487 tableObjectCheck(watch.Added, configMap, objects[0])
2488 tableObjectCheck(watch.Added, configMap2, objects[1])
2489 tableObjectCheck(watch.Modified, configMapUpdated, objects[2])
2490 tableObjectCheck(watch.Modified, configMap2Updated, objects[3])
2491
2492 filteredObjects := expectTableWatchEventsWithTypes(t, 2, 3, metav1.IncludeObject, json.NewDecoder(wTableIncludeObjectFiltered))
2493 tableObjectCheck(watch.Added, configMapUpdated, filteredObjects[0])
2494 tableObjectCheck(watch.Added, configMap2Updated, filteredObjects[1])
2495
2496 delayedObjects := expectTableWatchEventsWithTypes(t, 3, 3, metav1.IncludeObject, json.NewDecoder(wTableIncludeObjectDelayed))
2497 tableObjectCheck(watch.Added, configMap2, delayedObjects[0])
2498 tableObjectCheck(watch.Modified, configMapUpdated, delayedObjects[1])
2499 tableObjectCheck(watch.Modified, configMap2Updated, delayedObjects[2])
2500 }
2501
2502 func expectTableWatchEvents(t *testing.T, count, columns int, policy metav1.IncludeObjectPolicy, d *json.Decoder) [][]byte {
2503 events := expectTableWatchEventsWithTypes(t, count, columns, policy, d)
2504 var objects [][]byte
2505 for _, event := range events {
2506 objects = append(objects, event.rawObject)
2507 }
2508 return objects
2509 }
2510
2511 type streamedEvent struct {
2512 eventType string
2513 rawObject []byte
2514 }
2515
2516 func expectTableWatchEventsWithTypes(t *testing.T, count, columns int, policy metav1.IncludeObjectPolicy, d *json.Decoder) []streamedEvent {
2517 t.Helper()
2518
2519 var events []streamedEvent
2520
2521 for i := 0; i < count; i++ {
2522 var evt metav1.WatchEvent
2523 if err := d.Decode(&evt); err != nil {
2524 t.Fatal(err)
2525 }
2526
2527 var table metav1beta1.Table
2528 if err := json.Unmarshal(evt.Object.Raw, &table); err != nil {
2529 t.Fatal(err)
2530 }
2531 if i == 0 {
2532 if len(table.ColumnDefinitions) != columns {
2533 t.Fatalf("Got unexpected columns on first watch event: %d vs %#v", columns, table.ColumnDefinitions)
2534 }
2535 } else {
2536 if len(table.ColumnDefinitions) != 0 {
2537 t.Fatalf("Expected no columns on second watch event: %#v", table.ColumnDefinitions)
2538 }
2539 }
2540 if len(table.Rows) != 1 {
2541 t.Fatalf("Invalid rows: %#v", table.Rows)
2542 }
2543 row := table.Rows[0]
2544 if len(row.Cells) != columns {
2545 t.Fatalf("Invalid row width: %#v", row.Cells)
2546 }
2547 switch policy {
2548 case metav1.IncludeMetadata:
2549 var meta metav1beta1.PartialObjectMetadata
2550 if err := json.Unmarshal(row.Object.Raw, &meta); err != nil {
2551 t.Fatalf("expected partial object: %v", err)
2552 }
2553 partialObj := metav1.TypeMeta{Kind: "PartialObjectMetadata", APIVersion: "meta.k8s.io/v1beta1"}
2554 if meta.TypeMeta != partialObj {
2555 t.Fatalf("expected partial object: %#v", meta)
2556 }
2557 events = append(events, streamedEvent{eventType: evt.Type, rawObject: row.Object.Raw})
2558 case metav1.IncludeNone:
2559 if len(row.Object.Raw) != 0 {
2560 t.Fatalf("Expected no object: %s", string(row.Object.Raw))
2561 }
2562 case metav1.IncludeObject:
2563 if len(row.Object.Raw) == 0 {
2564 t.Fatalf("Expected object: %s", string(row.Object.Raw))
2565 }
2566 events = append(events, streamedEvent{eventType: evt.Type, rawObject: row.Object.Raw})
2567 }
2568 }
2569 return events
2570 }
2571
2572 func expectPartialObjectMetaEvents(t *testing.T, d *json.Decoder, values ...string) {
2573 t.Helper()
2574
2575 for i, value := range values {
2576 var evt metav1.WatchEvent
2577 if err := d.Decode(&evt); err != nil {
2578 t.Fatal(err)
2579 }
2580 var meta metav1beta1.PartialObjectMetadata
2581 if err := json.Unmarshal(evt.Object.Raw, &meta); err != nil {
2582 t.Fatal(err)
2583 }
2584 typeMeta := metav1.TypeMeta{Kind: "PartialObjectMetadata", APIVersion: "meta.k8s.io/v1beta1"}
2585 if meta.TypeMeta != typeMeta {
2586 t.Fatalf("expected partial object: %#v", meta)
2587 }
2588 if meta.Annotations["test"] != value {
2589 t.Fatalf("expected event %d to have value %q instead of %q", i+1, value, meta.Annotations["test"])
2590 }
2591 }
2592 }
2593
2594 type partialObjectMetadataCheck func(*metav1beta1.PartialObjectMetadata)
2595
2596 func expectPartialObjectMetaEventsProtobuf(t *testing.T, r io.Reader, values ...string) {
2597 checks := []partialObjectMetadataCheck{}
2598 for i, value := range values {
2599 i, value := i, value
2600 checks = append(checks, func(meta *metav1beta1.PartialObjectMetadata) {
2601 if meta.Annotations["test"] != value {
2602 t.Fatalf("expected event %d to have value %q instead of %q", i+1, value, meta.Annotations["test"])
2603 }
2604 })
2605 }
2606 expectPartialObjectMetaEventsProtobufChecks(t, r, checks)
2607 }
2608
2609 func expectPartialObjectMetaEventsProtobufChecks(t *testing.T, r io.Reader, checks []partialObjectMetadataCheck) {
2610 scheme := runtime.NewScheme()
2611 metav1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"})
2612 rs := protobuf.NewRawSerializer(scheme, scheme)
2613 d := streaming.NewDecoder(
2614 protobuf.LengthDelimitedFramer.NewFrameReader(io.NopCloser(r)),
2615 rs,
2616 )
2617 ds := metainternalversionscheme.Codecs.UniversalDeserializer()
2618
2619 for _, check := range checks {
2620 var evt metav1.WatchEvent
2621 if _, _, err := d.Decode(nil, &evt); err != nil {
2622 t.Fatal(err)
2623 }
2624 obj, gvk, err := ds.Decode(evt.Object.Raw, nil, nil)
2625 if err != nil {
2626 t.Fatal(err)
2627 }
2628 meta, ok := obj.(*metav1beta1.PartialObjectMetadata)
2629 if !ok {
2630 t.Fatalf("unexpected watch object %T", obj)
2631 }
2632 expected := &schema.GroupVersionKind{Kind: "PartialObjectMetadata", Version: "v1beta1", Group: "meta.k8s.io"}
2633 if !reflect.DeepEqual(expected, gvk) {
2634 t.Fatalf("expected partial object: %#v", meta)
2635 }
2636 check(meta)
2637 }
2638 }
2639
2640 func expectTableV1WatchEvents(t *testing.T, count, columns int, policy metav1.IncludeObjectPolicy, d *json.Decoder) [][]byte {
2641 t.Helper()
2642
2643 var objects [][]byte
2644
2645 for i := 0; i < count; i++ {
2646 var evt metav1.WatchEvent
2647 if err := d.Decode(&evt); err != nil {
2648 t.Fatal(err)
2649 }
2650 var table metav1.Table
2651 if err := json.Unmarshal(evt.Object.Raw, &table); err != nil {
2652 t.Fatal(err)
2653 }
2654 if i == 0 {
2655 if len(table.ColumnDefinitions) != columns {
2656 t.Fatalf("Got unexpected columns on first watch event: %d vs %#v", columns, table.ColumnDefinitions)
2657 }
2658 } else {
2659 if len(table.ColumnDefinitions) != 0 {
2660 t.Fatalf("Expected no columns on second watch event: %#v", table.ColumnDefinitions)
2661 }
2662 }
2663 if len(table.Rows) != 1 {
2664 t.Fatalf("Invalid rows: %#v", table.Rows)
2665 }
2666 row := table.Rows[0]
2667 if len(row.Cells) != columns {
2668 t.Fatalf("Invalid row width: %#v", row.Cells)
2669 }
2670 switch policy {
2671 case metav1.IncludeMetadata:
2672 var meta metav1.PartialObjectMetadata
2673 if err := json.Unmarshal(row.Object.Raw, &meta); err != nil {
2674 t.Fatalf("expected partial object: %v", err)
2675 }
2676 partialObj := metav1.TypeMeta{Kind: "PartialObjectMetadata", APIVersion: "meta.k8s.io/v1"}
2677 if meta.TypeMeta != partialObj {
2678 t.Fatalf("expected partial object: %#v", meta)
2679 }
2680 case metav1.IncludeNone:
2681 if len(row.Object.Raw) != 0 {
2682 t.Fatalf("Expected no object: %s", string(row.Object.Raw))
2683 }
2684 case metav1.IncludeObject:
2685 if len(row.Object.Raw) == 0 {
2686 t.Fatalf("Expected object: %s", string(row.Object.Raw))
2687 }
2688 objects = append(objects, row.Object.Raw)
2689 }
2690 }
2691 return objects
2692 }
2693
2694 func expectPartialObjectMetaV1Events(t *testing.T, d *json.Decoder, values ...string) {
2695 t.Helper()
2696
2697 for i, value := range values {
2698 var evt metav1.WatchEvent
2699 if err := d.Decode(&evt); err != nil {
2700 t.Fatal(err)
2701 }
2702 var meta metav1.PartialObjectMetadata
2703 if err := json.Unmarshal(evt.Object.Raw, &meta); err != nil {
2704 t.Fatal(err)
2705 }
2706 typeMeta := metav1.TypeMeta{Kind: "PartialObjectMetadata", APIVersion: "meta.k8s.io/v1"}
2707 if meta.TypeMeta != typeMeta {
2708 t.Fatalf("expected partial object: %#v", meta)
2709 }
2710 if meta.Annotations["test"] != value {
2711 t.Fatalf("expected event %d to have value %q instead of %q", i+1, value, meta.Annotations["test"])
2712 }
2713 }
2714 }
2715
2716 func expectPartialObjectMetaV1EventsProtobuf(t *testing.T, r io.Reader, values ...string) {
2717 scheme := runtime.NewScheme()
2718 metav1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"})
2719 rs := protobuf.NewRawSerializer(scheme, scheme)
2720 d := streaming.NewDecoder(
2721 protobuf.LengthDelimitedFramer.NewFrameReader(io.NopCloser(r)),
2722 rs,
2723 )
2724 ds := metainternalversionscheme.Codecs.UniversalDeserializer()
2725
2726 for i, value := range values {
2727 var evt metav1.WatchEvent
2728 if _, _, err := d.Decode(nil, &evt); err != nil {
2729 t.Fatal(err)
2730 }
2731 obj, gvk, err := ds.Decode(evt.Object.Raw, nil, nil)
2732 if err != nil {
2733 t.Fatal(err)
2734 }
2735 meta, ok := obj.(*metav1.PartialObjectMetadata)
2736 if !ok {
2737 t.Fatalf("unexpected watch object %T", obj)
2738 }
2739 expected := &schema.GroupVersionKind{Kind: "PartialObjectMetadata", Version: "v1", Group: "meta.k8s.io"}
2740 if !reflect.DeepEqual(expected, gvk) {
2741 t.Fatalf("expected partial object: %#v", meta)
2742 }
2743 if meta.Annotations["test"] != value {
2744 t.Fatalf("expected event %d to have value %q instead of %q", i+1, value, meta.Annotations["test"])
2745 }
2746 }
2747 }
2748
2749 func TestClientsetShareTransport(t *testing.T) {
2750 var counter int
2751 var mu sync.Mutex
2752 server := kubeapiservertesting.StartTestServerOrDie(t, nil, nil, framework.SharedEtcd())
2753 defer server.TearDownFn()
2754
2755 dialFn := func(ctx context.Context, network, address string) (net.Conn, error) {
2756 mu.Lock()
2757 counter++
2758 mu.Unlock()
2759 return (&net.Dialer{}).DialContext(ctx, network, address)
2760 }
2761 server.ClientConfig.Dial = dialFn
2762 client := clientset.NewForConfigOrDie(server.ClientConfig)
2763
2764 _, err := client.CoreV1().Namespaces().Create(context.TODO(), &v1.Namespace{
2765 ObjectMeta: metav1.ObjectMeta{
2766 Name: "test-creation",
2767 },
2768 }, metav1.CreateOptions{})
2769 if err != nil {
2770 t.Fatalf("failed to create test ns: %v", err)
2771 }
2772
2773 result := client.CoreV1().RESTClient().Get().AbsPath("/healthz").Do(context.TODO())
2774 _, err = result.Raw()
2775 if err != nil {
2776 t.Fatal(err)
2777 }
2778 _, _, err = client.Discovery().ServerGroupsAndResources()
2779 if err != nil {
2780 t.Fatal(err)
2781 }
2782 n, err := client.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{})
2783 if err != nil {
2784 t.Fatal(err)
2785 }
2786 t.Logf("Listed %d namespaces on the cluster", len(n.Items))
2787 p, err := client.CoreV1().Pods("").List(context.TODO(), metav1.ListOptions{})
2788 if err != nil {
2789 t.Fatal(err)
2790 }
2791 t.Logf("Listed %d pods on the cluster", len(p.Items))
2792 e, err := client.DiscoveryV1().EndpointSlices("").List(context.TODO(), metav1.ListOptions{})
2793 if err != nil {
2794 t.Fatal(err)
2795 }
2796 t.Logf("Listed %d endpoint slices on the cluster", len(e.Items))
2797 d, err := client.AppsV1().Deployments("").List(context.TODO(), metav1.ListOptions{})
2798 if err != nil {
2799 t.Fatal(err)
2800 }
2801 t.Logf("Listed %d deployments on the cluster", len(d.Items))
2802
2803 if counter != 1 {
2804 t.Fatalf("expected only one connection, created %d connections", counter)
2805 }
2806 }
2807
2808 func TestDedupOwnerReferences(t *testing.T) {
2809 server := kubeapiservertesting.StartTestServerOrDie(t, nil, nil, framework.SharedEtcd())
2810 defer server.TearDownFn()
2811 etcd.CreateTestCRDs(t, apiextensionsclient.NewForConfigOrDie(server.ClientConfig), false, etcd.GetCustomResourceDefinitionData()[0])
2812
2813 b := &bytes.Buffer{}
2814 warningWriter := restclient.NewWarningWriter(b, restclient.WarningWriterOptions{})
2815 server.ClientConfig.WarningHandler = warningWriter
2816 client := clientset.NewForConfigOrDie(server.ClientConfig)
2817 dynamicClient := dynamic.NewForConfigOrDie(server.ClientConfig)
2818
2819 ns := "test-dedup-owner-references"
2820
2821 _, err := client.CoreV1().Namespaces().Create(context.TODO(), &v1.Namespace{
2822 ObjectMeta: metav1.ObjectMeta{
2823 Name: ns,
2824 },
2825 }, metav1.CreateOptions{})
2826 if err != nil {
2827 t.Fatalf("failed to create test ns: %v", err)
2828 }
2829
2830
2831 fakeRefA := metav1.OwnerReference{
2832 APIVersion: "v1",
2833 Kind: "ConfigMap",
2834 Name: "fake-configmap",
2835 UID: uuid.NewUUID(),
2836 }
2837 fakeRefB := metav1.OwnerReference{
2838 APIVersion: "v1",
2839 Kind: "Node",
2840 Name: "fake-node",
2841 UID: uuid.NewUUID(),
2842 }
2843 fakeRefC := metav1.OwnerReference{
2844 APIVersion: "cr.bar.com/v1",
2845 Kind: "Foo",
2846 Name: "fake-foo",
2847 UID: uuid.NewUUID(),
2848 }
2849
2850 tcs := []struct {
2851 gvr schema.GroupVersionResource
2852 kind string
2853 }{
2854 {
2855 gvr: schema.GroupVersionResource{
2856 Group: "",
2857 Version: "v1",
2858 Resource: "configmaps",
2859 },
2860 kind: "ConfigMap",
2861 },
2862 {
2863 gvr: schema.GroupVersionResource{
2864 Group: "cr.bar.com",
2865 Version: "v1",
2866 Resource: "foos",
2867 },
2868 kind: "Foo",
2869 },
2870 }
2871
2872 for i, tc := range tcs {
2873 t.Run(tc.gvr.String(), func(t *testing.T) {
2874 previousWarningCount := i * 3
2875 c := &dependentClient{
2876 t: t,
2877 client: dynamicClient.Resource(tc.gvr).Namespace(ns),
2878 gvr: tc.gvr,
2879 kind: tc.kind,
2880 }
2881 klog.Infof("creating dependent with duplicate owner references")
2882 dependent := c.createDependentWithOwners([]metav1.OwnerReference{fakeRefA, fakeRefA})
2883 assertManagedFields(t, dependent)
2884 expectedWarning := fmt.Sprintf(handlers.DuplicateOwnerReferencesWarningFormat, fakeRefA.UID)
2885 assertOwnerReferences(t, dependent, []metav1.OwnerReference{fakeRefA})
2886 assertWarningCount(t, warningWriter, previousWarningCount+1)
2887 assertWarningMessage(t, b, expectedWarning)
2888
2889 klog.Infof("updating dependent with duplicate owner references")
2890 dependent = c.updateDependentWithOwners(dependent, []metav1.OwnerReference{fakeRefA, fakeRefA})
2891 assertManagedFields(t, dependent)
2892 assertOwnerReferences(t, dependent, []metav1.OwnerReference{fakeRefA})
2893 assertWarningCount(t, warningWriter, previousWarningCount+2)
2894 assertWarningMessage(t, b, expectedWarning)
2895
2896 klog.Infof("patching dependent with duplicate owner reference")
2897 dependent = c.patchDependentWithOwner(dependent, fakeRefA)
2898
2899
2900
2901
2902
2903 expectedPatchWarning := fmt.Sprintf(handlers.DuplicateOwnerReferencesAfterMutatingAdmissionWarningFormat, fakeRefA.UID)
2904 assertOwnerReferences(t, dependent, []metav1.OwnerReference{fakeRefA})
2905 assertWarningCount(t, warningWriter, previousWarningCount+3)
2906 assertWarningMessage(t, b, expectedPatchWarning)
2907
2908 klog.Infof("updating dependent with different owner references")
2909 dependent = c.updateDependentWithOwners(dependent, []metav1.OwnerReference{fakeRefA, fakeRefB})
2910 assertOwnerReferences(t, dependent, []metav1.OwnerReference{fakeRefA, fakeRefB})
2911 assertWarningCount(t, warningWriter, previousWarningCount+3)
2912 assertWarningMessage(t, b, "")
2913
2914 klog.Infof("patching dependent with different owner references")
2915 dependent = c.patchDependentWithOwner(dependent, fakeRefC)
2916 assertOwnerReferences(t, dependent, []metav1.OwnerReference{fakeRefA, fakeRefB, fakeRefC})
2917 assertWarningCount(t, warningWriter, previousWarningCount+3)
2918 assertWarningMessage(t, b, "")
2919
2920 klog.Infof("deleting dependent")
2921 c.deleteDependent()
2922 assertWarningCount(t, warningWriter, previousWarningCount+3)
2923 assertWarningMessage(t, b, "")
2924 })
2925 }
2926
2927 if err := client.CoreV1().Namespaces().Delete(context.TODO(), ns, metav1.DeleteOptions{}); err != nil {
2928 t.Fatalf("failed to delete test ns: %v", err)
2929 }
2930 }
2931
2932 type dependentClient struct {
2933 t *testing.T
2934 client dynamic.ResourceInterface
2935 gvr schema.GroupVersionResource
2936 kind string
2937 }
2938
2939 func (c *dependentClient) createDependentWithOwners(refs []metav1.OwnerReference) *unstructured.Unstructured {
2940 obj := &unstructured.Unstructured{}
2941 obj.SetName("dependent")
2942 obj.SetOwnerReferences(refs)
2943 obj.SetKind(c.kind)
2944 obj.SetAPIVersion(fmt.Sprintf("%s/%s", c.gvr.Group, c.gvr.Version))
2945 obj, err := c.client.Create(context.TODO(), obj, metav1.CreateOptions{})
2946 if err != nil {
2947 c.t.Fatalf("failed to create dependent with owner references %v: %v", refs, err)
2948 }
2949 return obj
2950 }
2951
2952 func (c *dependentClient) updateDependentWithOwners(obj *unstructured.Unstructured, refs []metav1.OwnerReference) *unstructured.Unstructured {
2953 obj.SetOwnerReferences(refs)
2954 obj, err := c.client.Update(context.TODO(), obj, metav1.UpdateOptions{})
2955 if err != nil {
2956 c.t.Fatalf("failed to update dependent with owner references %v: %v", refs, err)
2957 }
2958 return obj
2959 }
2960
2961 func (c *dependentClient) patchDependentWithOwner(obj *unstructured.Unstructured, ref metav1.OwnerReference) *unstructured.Unstructured {
2962 patch := []byte(fmt.Sprintf(`[{"op":"add","path":"/metadata/ownerReferences/-","value":{"apiVersion":"%v", "kind": "%v", "name": "%v", "uid": "%v"}}]`, ref.APIVersion, ref.Kind, ref.Name, ref.UID))
2963 obj, err := c.client.Patch(context.TODO(), obj.GetName(), types.JSONPatchType, patch, metav1.PatchOptions{})
2964 if err != nil {
2965 c.t.Fatalf("failed to append owner reference to dependent with owner reference %v, patch %v: %v",
2966 ref, patch, err)
2967 }
2968 return obj
2969 }
2970
2971 func (c *dependentClient) deleteDependent() {
2972 if err := c.client.Delete(context.TODO(), "dependent", metav1.DeleteOptions{}); err != nil {
2973 c.t.Fatalf("failed to delete dependent: %v", err)
2974 }
2975 }
2976
2977 type warningCounter interface {
2978 WarningCount() int
2979 }
2980
2981 func assertOwnerReferences(t *testing.T, obj *unstructured.Unstructured, refs []metav1.OwnerReference) {
2982 if !reflect.DeepEqual(obj.GetOwnerReferences(), refs) {
2983 t.Errorf("unexpected owner references, expected: %v, got: %v", refs, obj.GetOwnerReferences())
2984 }
2985 }
2986
2987 func assertWarningCount(t *testing.T, counter warningCounter, expected int) {
2988 if counter.WarningCount() != expected {
2989 t.Errorf("unexpected warning count, expected: %v, got: %v", expected, counter.WarningCount())
2990 }
2991 }
2992
2993 func assertWarningMessage(t *testing.T, b *bytes.Buffer, expected string) {
2994 defer b.Reset()
2995 actual := b.String()
2996 if len(expected) == 0 && len(actual) != 0 {
2997 t.Errorf("unexpected warning message, expected no warning, got: %v", actual)
2998 }
2999 if len(expected) == 0 {
3000 return
3001 }
3002 if !strings.Contains(actual, expected) {
3003 t.Errorf("unexpected warning message, expected: %v, got: %v", expected, actual)
3004 }
3005 }
3006
3007 func assertManagedFields(t *testing.T, obj *unstructured.Unstructured) {
3008 if len(obj.GetManagedFields()) == 0 {
3009 t.Errorf("unexpected empty managed fields in object: %v", obj)
3010 }
3011 }
3012
3013 func int32Ptr(i int32) *int32 {
3014 return &i
3015 }
3016
View as plain text