k8s.io/client-go@v0.22.2/dynamic/client_test.go (about) 1 /* 2 Copyright 2016 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 dynamic 18 19 import ( 20 "bytes" 21 "context" 22 "fmt" 23 "io/ioutil" 24 "net/http" 25 "net/http/httptest" 26 "reflect" 27 "testing" 28 29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 31 "k8s.io/apimachinery/pkg/runtime" 32 "k8s.io/apimachinery/pkg/runtime/schema" 33 "k8s.io/apimachinery/pkg/runtime/serializer/streaming" 34 "k8s.io/apimachinery/pkg/types" 35 "k8s.io/apimachinery/pkg/watch" 36 restclient "k8s.io/client-go/rest" 37 restclientwatch "k8s.io/client-go/rest/watch" 38 ) 39 40 func getJSON(version, kind, name string) []byte { 41 return []byte(fmt.Sprintf(`{"apiVersion": %q, "kind": %q, "metadata": {"name": %q}}`, version, kind, name)) 42 } 43 44 func getListJSON(version, kind string, items ...[]byte) []byte { 45 json := fmt.Sprintf(`{"apiVersion": %q, "kind": %q, "items": [%s]}`, 46 version, kind, bytes.Join(items, []byte(","))) 47 return []byte(json) 48 } 49 50 func getObject(version, kind, name string) *unstructured.Unstructured { 51 return &unstructured.Unstructured{ 52 Object: map[string]interface{}{ 53 "apiVersion": version, 54 "kind": kind, 55 "metadata": map[string]interface{}{ 56 "name": name, 57 }, 58 }, 59 } 60 } 61 62 func getClientServer(h func(http.ResponseWriter, *http.Request)) (Interface, *httptest.Server, error) { 63 srv := httptest.NewServer(http.HandlerFunc(h)) 64 cl, err := NewForConfig(&restclient.Config{ 65 Host: srv.URL, 66 }) 67 if err != nil { 68 srv.Close() 69 return nil, nil, err 70 } 71 return cl, srv, nil 72 } 73 74 func TestList(t *testing.T) { 75 tcs := []struct { 76 name string 77 namespace string 78 path string 79 resp []byte 80 want *unstructured.UnstructuredList 81 }{ 82 { 83 name: "normal_list", 84 path: "/apis/gtest/vtest/rtest", 85 resp: getListJSON("vTest", "rTestList", 86 getJSON("vTest", "rTest", "item1"), 87 getJSON("vTest", "rTest", "item2")), 88 want: &unstructured.UnstructuredList{ 89 Object: map[string]interface{}{ 90 "apiVersion": "vTest", 91 "kind": "rTestList", 92 }, 93 Items: []unstructured.Unstructured{ 94 *getObject("vTest", "rTest", "item1"), 95 *getObject("vTest", "rTest", "item2"), 96 }, 97 }, 98 }, 99 { 100 name: "namespaced_list", 101 namespace: "nstest", 102 path: "/apis/gtest/vtest/namespaces/nstest/rtest", 103 resp: getListJSON("vTest", "rTestList", 104 getJSON("vTest", "rTest", "item1"), 105 getJSON("vTest", "rTest", "item2")), 106 want: &unstructured.UnstructuredList{ 107 Object: map[string]interface{}{ 108 "apiVersion": "vTest", 109 "kind": "rTestList", 110 }, 111 Items: []unstructured.Unstructured{ 112 *getObject("vTest", "rTest", "item1"), 113 *getObject("vTest", "rTest", "item2"), 114 }, 115 }, 116 }, 117 } 118 for _, tc := range tcs { 119 resource := schema.GroupVersionResource{Group: "gtest", Version: "vtest", Resource: "rtest"} 120 cl, srv, err := getClientServer(func(w http.ResponseWriter, r *http.Request) { 121 if r.Method != "GET" { 122 t.Errorf("List(%q) got HTTP method %s. wanted GET", tc.name, r.Method) 123 } 124 125 if r.URL.Path != tc.path { 126 t.Errorf("List(%q) got path %s. wanted %s", tc.name, r.URL.Path, tc.path) 127 } 128 129 w.Header().Set("Content-Type", runtime.ContentTypeJSON) 130 w.Write(tc.resp) 131 }) 132 if err != nil { 133 t.Errorf("unexpected error when creating client: %v", err) 134 continue 135 } 136 defer srv.Close() 137 138 got, err := cl.Resource(resource).Namespace(tc.namespace).List(context.TODO(), metav1.ListOptions{}) 139 if err != nil { 140 t.Errorf("unexpected error when listing %q: %v", tc.name, err) 141 continue 142 } 143 144 if !reflect.DeepEqual(got, tc.want) { 145 t.Errorf("List(%q) want: %v\ngot: %v", tc.name, tc.want, got) 146 } 147 } 148 } 149 150 func TestGet(t *testing.T) { 151 tcs := []struct { 152 resource string 153 subresource []string 154 namespace string 155 name string 156 path string 157 resp []byte 158 want *unstructured.Unstructured 159 }{ 160 { 161 resource: "rtest", 162 name: "normal_get", 163 path: "/apis/gtest/vtest/rtest/normal_get", 164 resp: getJSON("vTest", "rTest", "normal_get"), 165 want: getObject("vTest", "rTest", "normal_get"), 166 }, 167 { 168 resource: "rtest", 169 namespace: "nstest", 170 name: "namespaced_get", 171 path: "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_get", 172 resp: getJSON("vTest", "rTest", "namespaced_get"), 173 want: getObject("vTest", "rTest", "namespaced_get"), 174 }, 175 { 176 resource: "rtest", 177 subresource: []string{"srtest"}, 178 name: "normal_subresource_get", 179 path: "/apis/gtest/vtest/rtest/normal_subresource_get/srtest", 180 resp: getJSON("vTest", "srTest", "normal_subresource_get"), 181 want: getObject("vTest", "srTest", "normal_subresource_get"), 182 }, 183 { 184 resource: "rtest", 185 subresource: []string{"srtest"}, 186 namespace: "nstest", 187 name: "namespaced_subresource_get", 188 path: "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_subresource_get/srtest", 189 resp: getJSON("vTest", "srTest", "namespaced_subresource_get"), 190 want: getObject("vTest", "srTest", "namespaced_subresource_get"), 191 }, 192 } 193 for _, tc := range tcs { 194 resource := schema.GroupVersionResource{Group: "gtest", Version: "vtest", Resource: tc.resource} 195 cl, srv, err := getClientServer(func(w http.ResponseWriter, r *http.Request) { 196 if r.Method != "GET" { 197 t.Errorf("Get(%q) got HTTP method %s. wanted GET", tc.name, r.Method) 198 } 199 200 if r.URL.Path != tc.path { 201 t.Errorf("Get(%q) got path %s. wanted %s", tc.name, r.URL.Path, tc.path) 202 } 203 204 w.Header().Set("Content-Type", runtime.ContentTypeJSON) 205 w.Write(tc.resp) 206 }) 207 if err != nil { 208 t.Errorf("unexpected error when creating client: %v", err) 209 continue 210 } 211 defer srv.Close() 212 213 got, err := cl.Resource(resource).Namespace(tc.namespace).Get(context.TODO(), tc.name, metav1.GetOptions{}, tc.subresource...) 214 if err != nil { 215 t.Errorf("unexpected error when getting %q: %v", tc.name, err) 216 continue 217 } 218 219 if !reflect.DeepEqual(got, tc.want) { 220 t.Errorf("Get(%q) want: %v\ngot: %v", tc.name, tc.want, got) 221 } 222 } 223 } 224 225 func TestDelete(t *testing.T) { 226 background := metav1.DeletePropagationBackground 227 uid := types.UID("uid") 228 229 statusOK := &metav1.Status{ 230 TypeMeta: metav1.TypeMeta{Kind: "Status"}, 231 Status: metav1.StatusSuccess, 232 } 233 tcs := []struct { 234 subresource []string 235 namespace string 236 name string 237 path string 238 deleteOptions metav1.DeleteOptions 239 }{ 240 { 241 name: "normal_delete", 242 path: "/apis/gtest/vtest/rtest/normal_delete", 243 }, 244 { 245 namespace: "nstest", 246 name: "namespaced_delete", 247 path: "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_delete", 248 }, 249 { 250 subresource: []string{"srtest"}, 251 name: "normal_delete", 252 path: "/apis/gtest/vtest/rtest/normal_delete/srtest", 253 }, 254 { 255 subresource: []string{"srtest"}, 256 namespace: "nstest", 257 name: "namespaced_delete", 258 path: "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_delete/srtest", 259 }, 260 { 261 namespace: "nstest", 262 name: "namespaced_delete_with_options", 263 path: "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_delete_with_options", 264 deleteOptions: metav1.DeleteOptions{Preconditions: &metav1.Preconditions{UID: &uid}, PropagationPolicy: &background}, 265 }, 266 } 267 for _, tc := range tcs { 268 resource := schema.GroupVersionResource{Group: "gtest", Version: "vtest", Resource: "rtest"} 269 cl, srv, err := getClientServer(func(w http.ResponseWriter, r *http.Request) { 270 if r.Method != "DELETE" { 271 t.Errorf("Delete(%q) got HTTP method %s. wanted DELETE", tc.name, r.Method) 272 } 273 274 if r.URL.Path != tc.path { 275 t.Errorf("Delete(%q) got path %s. wanted %s", tc.name, r.URL.Path, tc.path) 276 } 277 278 w.Header().Set("Content-Type", runtime.ContentTypeJSON) 279 unstructured.UnstructuredJSONScheme.Encode(statusOK, w) 280 }) 281 if err != nil { 282 t.Errorf("unexpected error when creating client: %v", err) 283 continue 284 } 285 defer srv.Close() 286 287 err = cl.Resource(resource).Namespace(tc.namespace).Delete(context.TODO(), tc.name, tc.deleteOptions, tc.subresource...) 288 if err != nil { 289 t.Errorf("unexpected error when deleting %q: %v", tc.name, err) 290 continue 291 } 292 } 293 } 294 295 func TestDeleteCollection(t *testing.T) { 296 statusOK := &metav1.Status{ 297 TypeMeta: metav1.TypeMeta{Kind: "Status"}, 298 Status: metav1.StatusSuccess, 299 } 300 tcs := []struct { 301 namespace string 302 name string 303 path string 304 }{ 305 { 306 name: "normal_delete_collection", 307 path: "/apis/gtest/vtest/rtest", 308 }, 309 { 310 namespace: "nstest", 311 name: "namespaced_delete_collection", 312 path: "/apis/gtest/vtest/namespaces/nstest/rtest", 313 }, 314 } 315 for _, tc := range tcs { 316 resource := schema.GroupVersionResource{Group: "gtest", Version: "vtest", Resource: "rtest"} 317 cl, srv, err := getClientServer(func(w http.ResponseWriter, r *http.Request) { 318 if r.Method != "DELETE" { 319 t.Errorf("DeleteCollection(%q) got HTTP method %s. wanted DELETE", tc.name, r.Method) 320 } 321 322 if r.URL.Path != tc.path { 323 t.Errorf("DeleteCollection(%q) got path %s. wanted %s", tc.name, r.URL.Path, tc.path) 324 } 325 326 w.Header().Set("Content-Type", runtime.ContentTypeJSON) 327 unstructured.UnstructuredJSONScheme.Encode(statusOK, w) 328 }) 329 if err != nil { 330 t.Errorf("unexpected error when creating client: %v", err) 331 continue 332 } 333 defer srv.Close() 334 335 err = cl.Resource(resource).Namespace(tc.namespace).DeleteCollection(context.TODO(), metav1.DeleteOptions{}, metav1.ListOptions{}) 336 if err != nil { 337 t.Errorf("unexpected error when deleting collection %q: %v", tc.name, err) 338 continue 339 } 340 } 341 } 342 343 func TestCreate(t *testing.T) { 344 tcs := []struct { 345 resource string 346 subresource []string 347 name string 348 namespace string 349 obj *unstructured.Unstructured 350 path string 351 }{ 352 { 353 resource: "rtest", 354 name: "normal_create", 355 path: "/apis/gtest/vtest/rtest", 356 obj: getObject("gtest/vTest", "rTest", "normal_create"), 357 }, 358 { 359 resource: "rtest", 360 name: "namespaced_create", 361 namespace: "nstest", 362 path: "/apis/gtest/vtest/namespaces/nstest/rtest", 363 obj: getObject("gtest/vTest", "rTest", "namespaced_create"), 364 }, 365 { 366 resource: "rtest", 367 subresource: []string{"srtest"}, 368 name: "normal_subresource_create", 369 path: "/apis/gtest/vtest/rtest/normal_subresource_create/srtest", 370 obj: getObject("vTest", "srTest", "normal_subresource_create"), 371 }, 372 { 373 resource: "rtest/", 374 subresource: []string{"srtest"}, 375 name: "namespaced_subresource_create", 376 namespace: "nstest", 377 path: "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_subresource_create/srtest", 378 obj: getObject("vTest", "srTest", "namespaced_subresource_create"), 379 }, 380 } 381 for _, tc := range tcs { 382 resource := schema.GroupVersionResource{Group: "gtest", Version: "vtest", Resource: tc.resource} 383 cl, srv, err := getClientServer(func(w http.ResponseWriter, r *http.Request) { 384 if r.Method != "POST" { 385 t.Errorf("Create(%q) got HTTP method %s. wanted POST", tc.name, r.Method) 386 } 387 388 if r.URL.Path != tc.path { 389 t.Errorf("Create(%q) got path %s. wanted %s", tc.name, r.URL.Path, tc.path) 390 } 391 392 w.Header().Set("Content-Type", runtime.ContentTypeJSON) 393 data, err := ioutil.ReadAll(r.Body) 394 if err != nil { 395 t.Errorf("Create(%q) unexpected error reading body: %v", tc.name, err) 396 w.WriteHeader(http.StatusInternalServerError) 397 return 398 } 399 400 w.Write(data) 401 }) 402 if err != nil { 403 t.Errorf("unexpected error when creating client: %v", err) 404 continue 405 } 406 defer srv.Close() 407 408 got, err := cl.Resource(resource).Namespace(tc.namespace).Create(context.TODO(), tc.obj, metav1.CreateOptions{}, tc.subresource...) 409 if err != nil { 410 t.Errorf("unexpected error when creating %q: %v", tc.name, err) 411 continue 412 } 413 414 if !reflect.DeepEqual(got, tc.obj) { 415 t.Errorf("Create(%q) want: %v\ngot: %v", tc.name, tc.obj, got) 416 } 417 } 418 } 419 420 func TestUpdate(t *testing.T) { 421 tcs := []struct { 422 resource string 423 subresource []string 424 name string 425 namespace string 426 obj *unstructured.Unstructured 427 path string 428 }{ 429 { 430 resource: "rtest", 431 name: "normal_update", 432 path: "/apis/gtest/vtest/rtest/normal_update", 433 obj: getObject("gtest/vTest", "rTest", "normal_update"), 434 }, 435 { 436 resource: "rtest", 437 name: "namespaced_update", 438 namespace: "nstest", 439 path: "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_update", 440 obj: getObject("gtest/vTest", "rTest", "namespaced_update"), 441 }, 442 { 443 resource: "rtest", 444 subresource: []string{"srtest"}, 445 name: "normal_subresource_update", 446 path: "/apis/gtest/vtest/rtest/normal_update/srtest", 447 obj: getObject("gtest/vTest", "srTest", "normal_update"), 448 }, 449 { 450 resource: "rtest", 451 subresource: []string{"srtest"}, 452 name: "namespaced_subresource_update", 453 namespace: "nstest", 454 path: "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_update/srtest", 455 obj: getObject("gtest/vTest", "srTest", "namespaced_update"), 456 }, 457 } 458 for _, tc := range tcs { 459 resource := schema.GroupVersionResource{Group: "gtest", Version: "vtest", Resource: tc.resource} 460 cl, srv, err := getClientServer(func(w http.ResponseWriter, r *http.Request) { 461 if r.Method != "PUT" { 462 t.Errorf("Update(%q) got HTTP method %s. wanted PUT", tc.name, r.Method) 463 } 464 465 if r.URL.Path != tc.path { 466 t.Errorf("Update(%q) got path %s. wanted %s", tc.name, r.URL.Path, tc.path) 467 } 468 469 w.Header().Set("Content-Type", runtime.ContentTypeJSON) 470 data, err := ioutil.ReadAll(r.Body) 471 if err != nil { 472 t.Errorf("Update(%q) unexpected error reading body: %v", tc.name, err) 473 w.WriteHeader(http.StatusInternalServerError) 474 return 475 } 476 477 w.Write(data) 478 }) 479 if err != nil { 480 t.Errorf("unexpected error when creating client: %v", err) 481 continue 482 } 483 defer srv.Close() 484 485 got, err := cl.Resource(resource).Namespace(tc.namespace).Update(context.TODO(), tc.obj, metav1.UpdateOptions{}, tc.subresource...) 486 if err != nil { 487 t.Errorf("unexpected error when updating %q: %v", tc.name, err) 488 continue 489 } 490 491 if !reflect.DeepEqual(got, tc.obj) { 492 t.Errorf("Update(%q) want: %v\ngot: %v", tc.name, tc.obj, got) 493 } 494 } 495 } 496 497 func TestWatch(t *testing.T) { 498 tcs := []struct { 499 name string 500 namespace string 501 events []watch.Event 502 path string 503 query string 504 }{ 505 { 506 name: "normal_watch", 507 path: "/apis/gtest/vtest/rtest", 508 query: "watch=true", 509 events: []watch.Event{ 510 {Type: watch.Added, Object: getObject("gtest/vTest", "rTest", "normal_watch")}, 511 {Type: watch.Modified, Object: getObject("gtest/vTest", "rTest", "normal_watch")}, 512 {Type: watch.Deleted, Object: getObject("gtest/vTest", "rTest", "normal_watch")}, 513 }, 514 }, 515 { 516 name: "namespaced_watch", 517 namespace: "nstest", 518 path: "/apis/gtest/vtest/namespaces/nstest/rtest", 519 query: "watch=true", 520 events: []watch.Event{ 521 {Type: watch.Added, Object: getObject("gtest/vTest", "rTest", "namespaced_watch")}, 522 {Type: watch.Modified, Object: getObject("gtest/vTest", "rTest", "namespaced_watch")}, 523 {Type: watch.Deleted, Object: getObject("gtest/vTest", "rTest", "namespaced_watch")}, 524 }, 525 }, 526 } 527 for _, tc := range tcs { 528 resource := schema.GroupVersionResource{Group: "gtest", Version: "vtest", Resource: "rtest"} 529 cl, srv, err := getClientServer(func(w http.ResponseWriter, r *http.Request) { 530 if r.Method != "GET" { 531 t.Errorf("Watch(%q) got HTTP method %s. wanted GET", tc.name, r.Method) 532 } 533 534 if r.URL.Path != tc.path { 535 t.Errorf("Watch(%q) got path %s. wanted %s", tc.name, r.URL.Path, tc.path) 536 } 537 if r.URL.RawQuery != tc.query { 538 t.Errorf("Watch(%q) got query %s. wanted %s", tc.name, r.URL.RawQuery, tc.query) 539 } 540 541 w.Header().Set("Content-Type", "application/json") 542 543 enc := restclientwatch.NewEncoder(streaming.NewEncoder(w, unstructured.UnstructuredJSONScheme), unstructured.UnstructuredJSONScheme) 544 for _, e := range tc.events { 545 enc.Encode(&e) 546 } 547 }) 548 if err != nil { 549 t.Errorf("unexpected error when creating client: %v", err) 550 continue 551 } 552 defer srv.Close() 553 554 watcher, err := cl.Resource(resource).Namespace(tc.namespace).Watch(context.TODO(), metav1.ListOptions{}) 555 if err != nil { 556 t.Errorf("unexpected error when watching %q: %v", tc.name, err) 557 continue 558 } 559 560 for _, want := range tc.events { 561 got := <-watcher.ResultChan() 562 if !reflect.DeepEqual(got, want) { 563 t.Errorf("Watch(%q) want: %v\ngot: %v", tc.name, want, got) 564 } 565 } 566 } 567 } 568 569 func TestPatch(t *testing.T) { 570 tcs := []struct { 571 resource string 572 subresource []string 573 name string 574 namespace string 575 patch []byte 576 want *unstructured.Unstructured 577 path string 578 }{ 579 { 580 resource: "rtest", 581 name: "normal_patch", 582 path: "/apis/gtest/vtest/rtest/normal_patch", 583 patch: getJSON("gtest/vTest", "rTest", "normal_patch"), 584 want: getObject("gtest/vTest", "rTest", "normal_patch"), 585 }, 586 { 587 resource: "rtest", 588 name: "namespaced_patch", 589 namespace: "nstest", 590 path: "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_patch", 591 patch: getJSON("gtest/vTest", "rTest", "namespaced_patch"), 592 want: getObject("gtest/vTest", "rTest", "namespaced_patch"), 593 }, 594 { 595 resource: "rtest", 596 subresource: []string{"srtest"}, 597 name: "normal_subresource_patch", 598 path: "/apis/gtest/vtest/rtest/normal_subresource_patch/srtest", 599 patch: getJSON("gtest/vTest", "srTest", "normal_subresource_patch"), 600 want: getObject("gtest/vTest", "srTest", "normal_subresource_patch"), 601 }, 602 { 603 resource: "rtest", 604 subresource: []string{"srtest"}, 605 name: "namespaced_subresource_patch", 606 namespace: "nstest", 607 path: "/apis/gtest/vtest/namespaces/nstest/rtest/namespaced_subresource_patch/srtest", 608 patch: getJSON("gtest/vTest", "srTest", "namespaced_subresource_patch"), 609 want: getObject("gtest/vTest", "srTest", "namespaced_subresource_patch"), 610 }, 611 } 612 for _, tc := range tcs { 613 resource := schema.GroupVersionResource{Group: "gtest", Version: "vtest", Resource: tc.resource} 614 cl, srv, err := getClientServer(func(w http.ResponseWriter, r *http.Request) { 615 if r.Method != "PATCH" { 616 t.Errorf("Patch(%q) got HTTP method %s. wanted PATCH", tc.name, r.Method) 617 } 618 619 if r.URL.Path != tc.path { 620 t.Errorf("Patch(%q) got path %s. wanted %s", tc.name, r.URL.Path, tc.path) 621 } 622 623 content := r.Header.Get("Content-Type") 624 if content != string(types.StrategicMergePatchType) { 625 t.Errorf("Patch(%q) got Content-Type %s. wanted %s", tc.name, content, types.StrategicMergePatchType) 626 } 627 628 data, err := ioutil.ReadAll(r.Body) 629 if err != nil { 630 t.Errorf("Patch(%q) unexpected error reading body: %v", tc.name, err) 631 w.WriteHeader(http.StatusInternalServerError) 632 return 633 } 634 635 w.Header().Set("Content-Type", "application/json") 636 w.Write(data) 637 }) 638 if err != nil { 639 t.Errorf("unexpected error when creating client: %v", err) 640 continue 641 } 642 defer srv.Close() 643 644 got, err := cl.Resource(resource).Namespace(tc.namespace).Patch(context.TODO(), tc.name, types.StrategicMergePatchType, tc.patch, metav1.PatchOptions{}, tc.subresource...) 645 if err != nil { 646 t.Errorf("unexpected error when patching %q: %v", tc.name, err) 647 continue 648 } 649 650 if !reflect.DeepEqual(got, tc.want) { 651 t.Errorf("Patch(%q) want: %v\ngot: %v", tc.name, tc.want, got) 652 } 653 } 654 }