k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/test/integration/apiserver/apiserver_test.go (about) 1 /* 2 Copyright 2017 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package apiserver 18 19 import ( 20 "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 /* TODO (pohly): return ktesting.TContext */, *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.ControlPlane.Extra.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 // We don't use the typed Go client to send this request to be able to verify the response status code. 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 // untyped 232 "/", 233 // health 234 "/healthz", 235 // openapi 236 "/openapi/v2", 237 // discovery 238 "/api", 239 "/api/v1", 240 "/apis", 241 "/apis/apps", 242 "/apis/apps/v1", 243 // apis 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 // Tests that the apiserver returns HSTS headers as expected. 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 // untyped 278 "/", 279 // health 280 "/healthz", 281 // openapi 282 "/openapi/v2", 283 // discovery 284 "/api", 285 "/api/v1", 286 "/apis", 287 "/apis/apps", 288 "/apis/apps/v1", 289 // apis 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 // Tests that the apiserver returns 202 status code as expected. 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 // 1. Create the resource without any finalizer and then delete it without setting DeleteOptions. 328 // Verify that server returns 200 in this case. 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 // 2. Create the resource with a finalizer so that the resource is not immediately deleted and then delete it without setting DeleteOptions. 336 // Verify that the apiserver still returns 200 since DeleteOptions.OrphanDependents is not set. 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 // 3. Create the resource and then delete it with DeleteOptions.OrphanDependents=false. 346 // Verify that the server still returns 200 since the resource is immediately deleted. 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 // 4. Create the resource with a finalizer so that the resource is not immediately deleted and then delete it with DeleteOptions.OrphanDependents=false. 355 // Verify that the server returns 202 in this case. 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 // TestListOptions ensures that list works as expected for valid and invalid combinations of limit, continue, 372 // resourceVersion and resourceVersionMatch. 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 // We compact this first resource version below 403 } 404 // delete the first 5, and then compact them 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 // compact some of the revision history in etcd so we can test "too old" resource versions 415 rawClient, kvClient, err := integration.GetEtcdClients(*storageTransport) 416 if err != nil { 417 t.Fatal(err) 418 } 419 // kvClient is a wrapper around rawClient and to avoid leaking goroutines we need to 420 // close the client (which we can do by closing rawClient). 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 // test all combinations of these, for both watch cache enabled and disabled: 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 // check for expected validation errors 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 // Should not be allowed for any resource version, but tightening the validation would be a breaking change 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 // Check for too old errors 548 isExact := opts.ResourceVersionMatch == metav1.ResourceVersionMatchExact 549 // Legacy corner cases that can be avoided by using an explicit resourceVersionMatch value 550 // see https://kubernetes.io/docs/reference/using-api/api-concepts/#the-resourceversion-parameter 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 // test successful responses 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 // Cacher.GetList defines this for logic to decide if the watch cache is skipped. We need to know it to know if 571 // the limit is respected when testing here. 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 { // watch cache does not respect limit and is not used for continue 578 if count != 10 { 579 t.Errorf("Expected list size to be 10 but got %d", count) // limit is ignored if watch cache is hit 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 // poll until the watch cache has the full list in memory 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 // because the dynamic client only has a json serializer, the client processing of the error cannot 1311 // turn the response into something meaningful, so we verify that fallback handling works correctly 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 // because the dynamic client only has a json serializer, the client processing of the error cannot 1369 // turn the response into something meaningful, so we verify that fallback handling works correctly 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 // TODO: this should be a more specific error 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 // TODO: this should be a more specific error 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 // TODO: this should be a more specific error 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 // TODO: this should be a more specific error 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 // no object was created in the preamble to the test, so get recent data 2270 rv = "0" 2271 } else { 2272 // we created an object, and need to list+watch from some time before the creation to see it 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 // Now update both configmaps so that filtering watch can observe them. 2409 // This is needed to validate whether events caching done by apiserver 2410 // distinguished objects by type. 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 // create test namespace 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 // List different objects 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 // create test namespace 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 // some fake owner references 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 // TODO: currently a patch request that duplicates owner references can still 2899 // wipe out managed fields. Note that this happens to built-in resources but 2900 // not custom resources. In future we should either dedup before writing manage 2901 // fields, or stop deduping and reject the request. 2902 // assertManagedFields(t, dependent) 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 // cleanup 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 }