github.com/darkowlzz/helm@v2.5.1-0.20171213183701-6707fe0468d4+incompatible/pkg/kube/client_test.go (about) 1 /* 2 Copyright 2016 The Kubernetes Authors All rights reserved. 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 kube 18 19 import ( 20 "bytes" 21 "encoding/json" 22 "fmt" 23 "io" 24 "io/ioutil" 25 "net/http" 26 "strings" 27 "testing" 28 "time" 29 30 "k8s.io/apimachinery/pkg/api/meta" 31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 "k8s.io/apimachinery/pkg/runtime" 33 "k8s.io/apimachinery/pkg/watch" 34 "k8s.io/client-go/dynamic" 35 "k8s.io/client-go/rest/fake" 36 "k8s.io/kubernetes/pkg/api" 37 "k8s.io/kubernetes/pkg/api/testapi" 38 "k8s.io/kubernetes/pkg/kubectl" 39 cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing" 40 cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" 41 "k8s.io/kubernetes/pkg/kubectl/resource" 42 "k8s.io/kubernetes/pkg/kubectl/validation" 43 "k8s.io/kubernetes/pkg/printers" 44 watchjson "k8s.io/kubernetes/pkg/watch/json" 45 ) 46 47 func objBody(codec runtime.Codec, obj runtime.Object) io.ReadCloser { 48 return ioutil.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(codec, obj)))) 49 } 50 51 func newPod(name string) api.Pod { 52 return newPodWithStatus(name, api.PodStatus{}, "") 53 } 54 55 func newPodWithStatus(name string, status api.PodStatus, namespace string) api.Pod { 56 ns := api.NamespaceDefault 57 if namespace != "" { 58 ns = namespace 59 } 60 return api.Pod{ 61 ObjectMeta: metav1.ObjectMeta{ 62 Name: name, 63 Namespace: ns, 64 SelfLink: "/api/v1/namespaces/default/pods/" + name, 65 }, 66 Spec: api.PodSpec{ 67 Containers: []api.Container{{ 68 Name: "app:v4", 69 Image: "abc/app:v4", 70 Ports: []api.ContainerPort{{Name: "http", ContainerPort: 80}}, 71 }}, 72 }, 73 Status: status, 74 } 75 } 76 77 func newPodList(names ...string) api.PodList { 78 var list api.PodList 79 for _, name := range names { 80 list.Items = append(list.Items, newPod(name)) 81 } 82 return list 83 } 84 85 func notFoundBody() *metav1.Status { 86 return &metav1.Status{ 87 Code: http.StatusNotFound, 88 Status: metav1.StatusFailure, 89 Reason: metav1.StatusReasonNotFound, 90 Message: " \"\" not found", 91 Details: &metav1.StatusDetails{}, 92 } 93 } 94 95 func newResponse(code int, obj runtime.Object) (*http.Response, error) { 96 header := http.Header{} 97 header.Set("Content-Type", runtime.ContentTypeJSON) 98 body := ioutil.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(testapi.Default.Codec(), obj)))) 99 return &http.Response{StatusCode: code, Header: header, Body: body}, nil 100 } 101 102 type fakeReaper struct { 103 name string 104 } 105 106 func (r *fakeReaper) Stop(namespace, name string, timeout time.Duration, gracePeriod *metav1.DeleteOptions) error { 107 r.name = name 108 return nil 109 } 110 111 type fakeReaperFactory struct { 112 cmdutil.Factory 113 reaper kubectl.Reaper 114 } 115 116 func (f *fakeReaperFactory) Reaper(mapping *meta.RESTMapping) (kubectl.Reaper, error) { 117 return f.reaper, nil 118 } 119 120 func newEventResponse(code int, e *watch.Event) (*http.Response, error) { 121 dispatchedEvent, err := encodeAndMarshalEvent(e) 122 if err != nil { 123 return nil, err 124 } 125 126 header := http.Header{} 127 header.Set("Content-Type", runtime.ContentTypeJSON) 128 body := ioutil.NopCloser(bytes.NewReader(dispatchedEvent)) 129 return &http.Response{StatusCode: code, Header: header, Body: body}, nil 130 } 131 132 func encodeAndMarshalEvent(e *watch.Event) ([]byte, error) { 133 encodedEvent, err := watchjson.Object(testapi.Default.Codec(), e) 134 if err != nil { 135 return nil, err 136 } 137 138 return json.Marshal(encodedEvent) 139 } 140 141 func newTestClient(f cmdutil.Factory) *Client { 142 c := New(nil) 143 c.Factory = f 144 return c 145 } 146 147 func TestUpdate(t *testing.T) { 148 listA := newPodList("starfish", "otter", "squid") 149 listB := newPodList("starfish", "otter", "dolphin") 150 listC := newPodList("starfish", "otter", "dolphin") 151 listB.Items[0].Spec.Containers[0].Ports = []api.ContainerPort{{Name: "https", ContainerPort: 443}} 152 listC.Items[0].Spec.Containers[0].Ports = []api.ContainerPort{{Name: "https", ContainerPort: 443}} 153 154 var actions []string 155 156 f, tf, codec, _ := cmdtesting.NewAPIFactory() 157 tf.UnstructuredClient = &fake.RESTClient{ 158 APIRegistry: api.Registry, 159 NegotiatedSerializer: dynamic.ContentConfig().NegotiatedSerializer, 160 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { 161 p, m := req.URL.Path, req.Method 162 actions = append(actions, p+":"+m) 163 t.Logf("got request %s %s", p, m) 164 switch { 165 case p == "/namespaces/default/pods/starfish" && m == "GET": 166 return newResponse(200, &listA.Items[0]) 167 case p == "/namespaces/default/pods/otter" && m == "GET": 168 return newResponse(200, &listA.Items[1]) 169 case p == "/namespaces/default/pods/dolphin" && m == "GET": 170 return newResponse(404, notFoundBody()) 171 case p == "/namespaces/default/pods/starfish" && m == "PATCH": 172 data, err := ioutil.ReadAll(req.Body) 173 if err != nil { 174 t.Fatalf("could not dump request: %s", err) 175 } 176 req.Body.Close() 177 expected := `{"spec":{"$setElementOrder/containers":[{"name":"app:v4"}],"containers":[{"$setElementOrder/ports":[{"containerPort":443}],"name":"app:v4","ports":[{"containerPort":443,"name":"https"},{"$patch":"delete","containerPort":80}]}]}}` 178 if string(data) != expected { 179 t.Errorf("expected patch\n%s\ngot\n%s", expected, string(data)) 180 } 181 return newResponse(200, &listB.Items[0]) 182 case p == "/namespaces/default/pods" && m == "POST": 183 return newResponse(200, &listB.Items[1]) 184 case p == "/namespaces/default/pods/squid" && m == "DELETE": 185 return newResponse(200, &listB.Items[1]) 186 default: 187 t.Fatalf("unexpected request: %s %s", req.Method, req.URL.Path) 188 return nil, nil 189 } 190 }), 191 } 192 193 reaper := &fakeReaper{} 194 rf := &fakeReaperFactory{Factory: f, reaper: reaper} 195 c := newTestClient(rf) 196 if err := c.Update(api.NamespaceDefault, objBody(codec, &listA), objBody(codec, &listB), false, false, 0, false); err != nil { 197 t.Fatal(err) 198 } 199 // TODO: Find a way to test methods that use Client Set 200 // Test with a wait 201 // if err := c.Update("test", objBody(codec, &listB), objBody(codec, &listC), false, 300, true); err != nil { 202 // t.Fatal(err) 203 // } 204 // Test with a wait should fail 205 // TODO: A way to make this not based off of an extremely short timeout? 206 // if err := c.Update("test", objBody(codec, &listC), objBody(codec, &listA), false, 2, true); err != nil { 207 // t.Fatal(err) 208 // } 209 expectedActions := []string{ 210 "/namespaces/default/pods/starfish:GET", 211 "/namespaces/default/pods/starfish:PATCH", 212 "/namespaces/default/pods/otter:GET", 213 "/namespaces/default/pods/otter:GET", 214 "/namespaces/default/pods/dolphin:GET", 215 "/namespaces/default/pods:POST", 216 } 217 if len(expectedActions) != len(actions) { 218 t.Errorf("unexpected number of requests, expected %d, got %d", len(expectedActions), len(actions)) 219 return 220 } 221 for k, v := range expectedActions { 222 if actions[k] != v { 223 t.Errorf("expected %s request got %s", v, actions[k]) 224 } 225 } 226 227 if reaper.name != "squid" { 228 t.Errorf("unexpected reaper: %#v", reaper) 229 } 230 231 } 232 233 func TestBuild(t *testing.T) { 234 tests := []struct { 235 name string 236 namespace string 237 reader io.Reader 238 count int 239 swaggerFile string 240 err bool 241 errMessage string 242 }{ 243 { 244 name: "Valid input", 245 namespace: "test", 246 reader: strings.NewReader(guestbookManifest), 247 count: 6, 248 }, { 249 name: "Invalid schema", 250 namespace: "test", 251 reader: strings.NewReader(testInvalidServiceManifest), 252 swaggerFile: "../../vendor/k8s.io/kubernetes/api/swagger-spec/" + testapi.Default.GroupVersion().Version + ".json", 253 err: true, 254 errMessage: `error validating "": error validating data: expected type int, for field spec.ports[0].port, got string`, 255 }, 256 } 257 258 for _, tt := range tests { 259 f, tf, _, _ := cmdtesting.NewAPIFactory() 260 c := newTestClient(f) 261 if tt.swaggerFile != "" { 262 data, err := ioutil.ReadFile(tt.swaggerFile) 263 if err != nil { 264 t.Fatalf("could not read swagger spec: %s", err) 265 } 266 validator, err := validation.NewSwaggerSchemaFromBytes(data, nil) 267 if err != nil { 268 t.Fatalf("could not load swagger spec: %s", err) 269 } 270 tf.Validator = validator 271 } 272 273 // Test for an invalid manifest 274 infos, err := c.Build(tt.namespace, tt.reader) 275 if err != nil && err.Error() != tt.errMessage { 276 t.Errorf("%q. expected error message: %v, got %v", tt.name, tt.errMessage, err) 277 } else if err != nil && !tt.err { 278 t.Errorf("%q. Got error message when no error should have occurred: %v, got %v", tt.name, tt.errMessage, err) 279 } 280 281 if len(infos) != tt.count { 282 t.Errorf("%q. expected %d result objects, got %d", tt.name, tt.count, len(infos)) 283 } 284 } 285 } 286 287 type testPrinter struct { 288 Objects []runtime.Object 289 Err error 290 printers.ResourcePrinter 291 } 292 293 func (t *testPrinter) PrintObj(obj runtime.Object, out io.Writer) error { 294 t.Objects = append(t.Objects, obj) 295 fmt.Fprintf(out, "%#v", obj) 296 return t.Err 297 } 298 299 func (t *testPrinter) HandledResources() []string { 300 return []string{} 301 } 302 303 func (t *testPrinter) AfterPrint(io.Writer, string) error { 304 return t.Err 305 } 306 307 func TestGet(t *testing.T) { 308 list := newPodList("starfish", "otter") 309 f, tf, _, _ := cmdtesting.NewAPIFactory() 310 tf.Printer = &testPrinter{} 311 tf.UnstructuredClient = &fake.RESTClient{ 312 APIRegistry: api.Registry, 313 NegotiatedSerializer: dynamic.ContentConfig().NegotiatedSerializer, 314 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { 315 p, m := req.URL.Path, req.Method 316 //actions = append(actions, p+":"+m) 317 t.Logf("got request %s %s", p, m) 318 switch { 319 case p == "/namespaces/default/pods/starfish" && m == "GET": 320 return newResponse(404, notFoundBody()) 321 case p == "/namespaces/default/pods/otter" && m == "GET": 322 return newResponse(200, &list.Items[1]) 323 default: 324 t.Fatalf("unexpected request: %s %s", req.Method, req.URL.Path) 325 return nil, nil 326 } 327 }), 328 } 329 c := newTestClient(f) 330 331 // Test Success 332 data := strings.NewReader("kind: Pod\napiVersion: v1\nmetadata:\n name: otter") 333 o, err := c.Get("default", data) 334 if err != nil { 335 t.Errorf("Expected missing results, got %q", err) 336 } 337 if !strings.Contains(o, "==> v1/Pod") && !strings.Contains(o, "otter") { 338 t.Errorf("Expected v1/Pod otter, got %s", o) 339 } 340 341 // Test failure 342 data = strings.NewReader("kind: Pod\napiVersion: v1\nmetadata:\n name: starfish") 343 o, err = c.Get("default", data) 344 if err != nil { 345 t.Errorf("Expected missing results, got %q", err) 346 } 347 if !strings.Contains(o, "MISSING") && !strings.Contains(o, "pods\t\tstarfish") { 348 t.Errorf("Expected missing starfish, got %s", o) 349 } 350 } 351 352 func TestPerform(t *testing.T) { 353 tests := []struct { 354 name string 355 namespace string 356 reader io.Reader 357 count int 358 swaggerFile string 359 err bool 360 errMessage string 361 }{ 362 { 363 name: "Valid input", 364 namespace: "test", 365 reader: strings.NewReader(guestbookManifest), 366 count: 6, 367 }, { 368 name: "Empty manifests", 369 namespace: "test", 370 reader: strings.NewReader(""), 371 err: true, 372 errMessage: "no objects visited", 373 }, 374 } 375 376 for _, tt := range tests { 377 results := []*resource.Info{} 378 379 fn := func(info *resource.Info) error { 380 results = append(results, info) 381 382 if info.Namespace != tt.namespace { 383 t.Errorf("%q. expected namespace to be '%s', got %s", tt.name, tt.namespace, info.Namespace) 384 } 385 return nil 386 } 387 388 f, tf, _, _ := cmdtesting.NewAPIFactory() 389 c := newTestClient(f) 390 if tt.swaggerFile != "" { 391 data, err := ioutil.ReadFile(tt.swaggerFile) 392 if err != nil { 393 t.Fatalf("could not read swagger spec: %s", err) 394 } 395 validator, err := validation.NewSwaggerSchemaFromBytes(data, nil) 396 if err != nil { 397 t.Fatalf("could not load swagger spec: %s", err) 398 } 399 tf.Validator = validator 400 } 401 402 infos, err := c.Build(tt.namespace, tt.reader) 403 if err != nil && err.Error() != tt.errMessage { 404 t.Errorf("%q. Error while building manifests: %v", tt.name, err) 405 } 406 407 err = perform(infos, fn) 408 if (err != nil) != tt.err { 409 t.Errorf("%q. expected error: %v, got %v", tt.name, tt.err, err) 410 } 411 if err != nil && err.Error() != tt.errMessage { 412 t.Errorf("%q. expected error message: %v, got %v", tt.name, tt.errMessage, err) 413 } 414 415 if len(results) != tt.count { 416 t.Errorf("%q. expected %d result objects, got %d", tt.name, tt.count, len(results)) 417 } 418 } 419 } 420 421 func TestWaitAndGetCompletedPodPhase(t *testing.T) { 422 tests := []struct { 423 podPhase api.PodPhase 424 expectedPhase api.PodPhase 425 err bool 426 errMessage string 427 }{ 428 { 429 podPhase: api.PodPending, 430 expectedPhase: api.PodUnknown, 431 err: true, 432 errMessage: "watch closed before Until timeout", 433 }, { 434 podPhase: api.PodRunning, 435 expectedPhase: api.PodUnknown, 436 err: true, 437 errMessage: "watch closed before Until timeout", 438 }, { 439 podPhase: api.PodSucceeded, 440 expectedPhase: api.PodSucceeded, 441 }, { 442 podPhase: api.PodFailed, 443 expectedPhase: api.PodFailed, 444 }, 445 } 446 447 for _, tt := range tests { 448 f, tf, codec, ns := cmdtesting.NewAPIFactory() 449 actions := make(map[string]string) 450 451 var testPodList api.PodList 452 testPodList.Items = append(testPodList.Items, newPodWithStatus("bestpod", api.PodStatus{Phase: tt.podPhase}, "test")) 453 454 tf.Client = &fake.RESTClient{ 455 APIRegistry: api.Registry, 456 NegotiatedSerializer: ns, 457 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { 458 p, m := req.URL.Path, req.Method 459 actions[p] = m 460 switch { 461 case p == "/namespaces/test/pods/bestpod" && m == "GET": 462 return newResponse(200, &testPodList.Items[0]) 463 case p == "/namespaces/test/pods" && m == "GET": 464 event := watch.Event{Type: watch.Added, Object: &testPodList.Items[0]} 465 return newEventResponse(200, &event) 466 default: 467 t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) 468 return nil, nil 469 } 470 }), 471 } 472 473 c := newTestClient(f) 474 475 phase, err := c.WaitAndGetCompletedPodPhase("test", objBody(codec, &testPodList), 1*time.Second) 476 if (err != nil) != tt.err { 477 t.Fatalf("Expected error but there was none.") 478 } 479 if err != nil && err.Error() != tt.errMessage { 480 t.Fatalf("Expected error %s, got %s", tt.errMessage, err.Error()) 481 } 482 if phase != tt.expectedPhase { 483 t.Fatalf("Expected pod phase %s, got %s", tt.expectedPhase, phase) 484 } 485 } 486 } 487 488 func TestReal(t *testing.T) { 489 t.Skip("This is a live test, comment this line to run") 490 c := New(nil) 491 if err := c.Create("test", strings.NewReader(guestbookManifest), 300, false); err != nil { 492 t.Fatal(err) 493 } 494 495 testSvcEndpointManifest := testServiceManifest + "\n---\n" + testEndpointManifest 496 c = New(nil) 497 if err := c.Create("test-delete", strings.NewReader(testSvcEndpointManifest), 300, false); err != nil { 498 t.Fatal(err) 499 } 500 501 if err := c.Delete("test-delete", strings.NewReader(testEndpointManifest)); err != nil { 502 t.Fatal(err) 503 } 504 505 // ensures that delete does not fail if a resource is not found 506 if err := c.Delete("test-delete", strings.NewReader(testSvcEndpointManifest)); err != nil { 507 t.Fatal(err) 508 } 509 } 510 511 const testServiceManifest = ` 512 kind: Service 513 apiVersion: v1 514 metadata: 515 name: my-service 516 spec: 517 selector: 518 app: myapp 519 ports: 520 - port: 80 521 protocol: TCP 522 targetPort: 9376 523 ` 524 525 const testInvalidServiceManifest = ` 526 kind: Service 527 apiVersion: v1 528 spec: 529 ports: 530 - port: "80" 531 ` 532 533 const testEndpointManifest = ` 534 kind: Endpoints 535 apiVersion: v1 536 metadata: 537 name: my-service 538 subsets: 539 - addresses: 540 - ip: "1.2.3.4" 541 ports: 542 - port: 9376 543 ` 544 545 const guestbookManifest = ` 546 apiVersion: v1 547 kind: Service 548 metadata: 549 name: redis-master 550 labels: 551 app: redis 552 tier: backend 553 role: master 554 spec: 555 ports: 556 - port: 6379 557 targetPort: 6379 558 selector: 559 app: redis 560 tier: backend 561 role: master 562 --- 563 apiVersion: extensions/v1beta1 564 kind: Deployment 565 metadata: 566 name: redis-master 567 spec: 568 replicas: 1 569 template: 570 metadata: 571 labels: 572 app: redis 573 role: master 574 tier: backend 575 spec: 576 containers: 577 - name: master 578 image: gcr.io/google_containers/redis:e2e # or just image: redis 579 resources: 580 requests: 581 cpu: 100m 582 memory: 100Mi 583 ports: 584 - containerPort: 6379 585 --- 586 apiVersion: v1 587 kind: Service 588 metadata: 589 name: redis-slave 590 labels: 591 app: redis 592 tier: backend 593 role: slave 594 spec: 595 ports: 596 # the port that this service should serve on 597 - port: 6379 598 selector: 599 app: redis 600 tier: backend 601 role: slave 602 --- 603 apiVersion: extensions/v1beta1 604 kind: Deployment 605 metadata: 606 name: redis-slave 607 spec: 608 replicas: 2 609 template: 610 metadata: 611 labels: 612 app: redis 613 role: slave 614 tier: backend 615 spec: 616 containers: 617 - name: slave 618 image: gcr.io/google_samples/gb-redisslave:v1 619 resources: 620 requests: 621 cpu: 100m 622 memory: 100Mi 623 env: 624 - name: GET_HOSTS_FROM 625 value: dns 626 ports: 627 - containerPort: 6379 628 --- 629 apiVersion: v1 630 kind: Service 631 metadata: 632 name: frontend 633 labels: 634 app: guestbook 635 tier: frontend 636 spec: 637 ports: 638 - port: 80 639 selector: 640 app: guestbook 641 tier: frontend 642 --- 643 apiVersion: extensions/v1beta1 644 kind: Deployment 645 metadata: 646 name: frontend 647 spec: 648 replicas: 3 649 template: 650 metadata: 651 labels: 652 app: guestbook 653 tier: frontend 654 spec: 655 containers: 656 - name: php-redis 657 image: gcr.io/google-samples/gb-frontend:v4 658 resources: 659 requests: 660 cpu: 100m 661 memory: 100Mi 662 env: 663 - name: GET_HOSTS_FROM 664 value: dns 665 ports: 666 - containerPort: 80 667 `