github.com/caicloud/helm@v2.5.0+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/api/validation" 39 "k8s.io/kubernetes/pkg/kubectl" 40 cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing" 41 cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" 42 "k8s.io/kubernetes/pkg/kubectl/resource" 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/squid" && m == "GET": 170 return newResponse(200, &listA.Items[2]) 171 case p == "/namespaces/default/pods/dolphin" && m == "GET": 172 return newResponse(404, notFoundBody()) 173 case p == "/namespaces/default/pods/starfish" && m == "PATCH": 174 data, err := ioutil.ReadAll(req.Body) 175 if err != nil { 176 t.Fatalf("could not dump request: %s", err) 177 } 178 req.Body.Close() 179 expected := `{"spec":{"containers":[{"name":"app:v4","ports":[{"containerPort":443,"name":"https"},{"$patch":"delete","containerPort":80}]}]}}` 180 if string(data) != expected { 181 t.Errorf("expected patch\n%s\ngot\n%s", expected, string(data)) 182 } 183 return newResponse(200, &listB.Items[0]) 184 case p == "/namespaces/default/pods" && m == "POST": 185 return newResponse(200, &listB.Items[1]) 186 case p == "/namespaces/default/pods/squid" && m == "DELETE": 187 return newResponse(200, &listB.Items[1]) 188 default: 189 t.Fatalf("unexpected request: %s %s", req.Method, req.URL.Path) 190 return nil, nil 191 } 192 }), 193 } 194 195 reaper := &fakeReaper{} 196 rf := &fakeReaperFactory{Factory: f, reaper: reaper} 197 c := newTestClient(rf) 198 if err := c.Update(api.NamespaceDefault, objBody(codec, &listA), objBody(codec, &listB), false, false, 0, false); err != nil { 199 t.Fatal(err) 200 } 201 // TODO: Find a way to test methods that use Client Set 202 // Test with a wait 203 // if err := c.Update("test", objBody(codec, &listB), objBody(codec, &listC), false, 300, true); err != nil { 204 // t.Fatal(err) 205 // } 206 // Test with a wait should fail 207 // TODO: A way to make this not based off of an extremely short timeout? 208 // if err := c.Update("test", objBody(codec, &listC), objBody(codec, &listA), false, 2, true); err != nil { 209 // t.Fatal(err) 210 // } 211 expectedActions := []string{ 212 "/namespaces/default/pods/starfish:GET", 213 "/namespaces/default/pods/starfish:GET", 214 "/namespaces/default/pods/starfish:PATCH", 215 "/namespaces/default/pods/otter:GET", 216 "/namespaces/default/pods/otter:GET", 217 "/namespaces/default/pods/otter:GET", 218 "/namespaces/default/pods/dolphin:GET", 219 "/namespaces/default/pods:POST", 220 "/namespaces/default/pods/squid:GET", 221 } 222 if len(expectedActions) != len(actions) { 223 t.Errorf("unexpected number of requests, expected %d, got %d", len(expectedActions), len(actions)) 224 return 225 } 226 for k, v := range expectedActions { 227 if actions[k] != v { 228 t.Errorf("expected %s request got %s", v, actions[k]) 229 } 230 } 231 232 if reaper.name != "squid" { 233 t.Errorf("unexpected reaper: %#v", reaper) 234 } 235 236 } 237 238 func TestBuild(t *testing.T) { 239 tests := []struct { 240 name string 241 namespace string 242 reader io.Reader 243 count int 244 swaggerFile string 245 err bool 246 errMessage string 247 }{ 248 { 249 name: "Valid input", 250 namespace: "test", 251 reader: strings.NewReader(guestbookManifest), 252 count: 6, 253 }, { 254 name: "Invalid schema", 255 namespace: "test", 256 reader: strings.NewReader(testInvalidServiceManifest), 257 swaggerFile: "../../vendor/k8s.io/kubernetes/api/swagger-spec/" + testapi.Default.GroupVersion().Version + ".json", 258 err: true, 259 errMessage: `error validating "": error validating data: expected type int, for field spec.ports[0].port, got string`, 260 }, 261 } 262 263 for _, tt := range tests { 264 f, tf, _, _ := cmdtesting.NewAPIFactory() 265 c := newTestClient(f) 266 if tt.swaggerFile != "" { 267 data, err := ioutil.ReadFile(tt.swaggerFile) 268 if err != nil { 269 t.Fatalf("could not read swagger spec: %s", err) 270 } 271 validator, err := validation.NewSwaggerSchemaFromBytes(data, nil) 272 if err != nil { 273 t.Fatalf("could not load swagger spec: %s", err) 274 } 275 tf.Validator = validator 276 } 277 278 // Test for an invalid manifest 279 infos, err := c.Build(tt.namespace, tt.reader) 280 if err != nil && err.Error() != tt.errMessage { 281 t.Errorf("%q. expected error message: %v, got %v", tt.name, tt.errMessage, err) 282 } else if err != nil && !tt.err { 283 t.Errorf("%q. Got error message when no error should have occurred: %v, got %v", tt.name, tt.errMessage, err) 284 } 285 286 if len(infos) != tt.count { 287 t.Errorf("%q. expected %d result objects, got %d", tt.name, tt.count, len(infos)) 288 } 289 } 290 } 291 292 type testPrinter struct { 293 Objects []runtime.Object 294 Err error 295 printers.ResourcePrinter 296 } 297 298 func (t *testPrinter) PrintObj(obj runtime.Object, out io.Writer) error { 299 t.Objects = append(t.Objects, obj) 300 fmt.Fprintf(out, "%#v", obj) 301 return t.Err 302 } 303 304 func (t *testPrinter) HandledResources() []string { 305 return []string{} 306 } 307 308 func (t *testPrinter) AfterPrint(io.Writer, string) error { 309 return t.Err 310 } 311 312 func TestGet(t *testing.T) { 313 list := newPodList("starfish", "otter") 314 f, tf, _, _ := cmdtesting.NewAPIFactory() 315 tf.Printer = &testPrinter{} 316 tf.UnstructuredClient = &fake.RESTClient{ 317 APIRegistry: api.Registry, 318 NegotiatedSerializer: dynamic.ContentConfig().NegotiatedSerializer, 319 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { 320 p, m := req.URL.Path, req.Method 321 //actions = append(actions, p+":"+m) 322 t.Logf("got request %s %s", p, m) 323 switch { 324 case p == "/namespaces/default/pods/starfish" && m == "GET": 325 return newResponse(404, notFoundBody()) 326 case p == "/namespaces/default/pods/otter" && m == "GET": 327 return newResponse(200, &list.Items[1]) 328 default: 329 t.Fatalf("unexpected request: %s %s", req.Method, req.URL.Path) 330 return nil, nil 331 } 332 }), 333 } 334 c := newTestClient(f) 335 336 // Test Success 337 data := strings.NewReader("kind: Pod\napiVersion: v1\nmetadata:\n name: otter") 338 o, err := c.Get("default", data) 339 if err != nil { 340 t.Errorf("Expected missing results, got %q", err) 341 } 342 if !strings.Contains(o, "==> v1/Pod") && !strings.Contains(o, "otter") { 343 t.Errorf("Expected v1/Pod otter, got %s", o) 344 } 345 346 // Test failure 347 data = strings.NewReader("kind: Pod\napiVersion: v1\nmetadata:\n name: starfish") 348 o, err = c.Get("default", data) 349 if err != nil { 350 t.Errorf("Expected missing results, got %q", err) 351 } 352 if !strings.Contains(o, "MISSING") && !strings.Contains(o, "pods\t\tstarfish") { 353 t.Errorf("Expected missing starfish, got %s", o) 354 } 355 } 356 357 func TestPerform(t *testing.T) { 358 tests := []struct { 359 name string 360 namespace string 361 reader io.Reader 362 count int 363 swaggerFile string 364 err bool 365 errMessage string 366 }{ 367 { 368 name: "Valid input", 369 namespace: "test", 370 reader: strings.NewReader(guestbookManifest), 371 count: 6, 372 }, { 373 name: "Empty manifests", 374 namespace: "test", 375 reader: strings.NewReader(""), 376 err: true, 377 errMessage: "no objects visited", 378 }, 379 } 380 381 for _, tt := range tests { 382 results := []*resource.Info{} 383 384 fn := func(info *resource.Info) error { 385 results = append(results, info) 386 387 if info.Namespace != tt.namespace { 388 t.Errorf("%q. expected namespace to be '%s', got %s", tt.name, tt.namespace, info.Namespace) 389 } 390 return nil 391 } 392 393 f, tf, _, _ := cmdtesting.NewAPIFactory() 394 c := newTestClient(f) 395 if tt.swaggerFile != "" { 396 data, err := ioutil.ReadFile(tt.swaggerFile) 397 if err != nil { 398 t.Fatalf("could not read swagger spec: %s", err) 399 } 400 validator, err := validation.NewSwaggerSchemaFromBytes(data, nil) 401 if err != nil { 402 t.Fatalf("could not load swagger spec: %s", err) 403 } 404 tf.Validator = validator 405 } 406 407 infos, err := c.Build(tt.namespace, tt.reader) 408 if err != nil && err.Error() != tt.errMessage { 409 t.Errorf("%q. Error while building manifests: %v", tt.name, err) 410 } 411 412 err = perform(infos, fn) 413 if (err != nil) != tt.err { 414 t.Errorf("%q. expected error: %v, got %v", tt.name, tt.err, err) 415 } 416 if err != nil && err.Error() != tt.errMessage { 417 t.Errorf("%q. expected error message: %v, got %v", tt.name, tt.errMessage, err) 418 } 419 420 if len(results) != tt.count { 421 t.Errorf("%q. expected %d result objects, got %d", tt.name, tt.count, len(results)) 422 } 423 } 424 } 425 426 func TestWaitAndGetCompletedPodPhase(t *testing.T) { 427 tests := []struct { 428 podPhase api.PodPhase 429 expectedPhase api.PodPhase 430 err bool 431 errMessage string 432 }{ 433 { 434 podPhase: api.PodPending, 435 expectedPhase: api.PodUnknown, 436 err: true, 437 errMessage: "watch closed before Until timeout", 438 }, { 439 podPhase: api.PodRunning, 440 expectedPhase: api.PodUnknown, 441 err: true, 442 errMessage: "watch closed before Until timeout", 443 }, { 444 podPhase: api.PodSucceeded, 445 expectedPhase: api.PodSucceeded, 446 }, { 447 podPhase: api.PodFailed, 448 expectedPhase: api.PodFailed, 449 }, 450 } 451 452 for _, tt := range tests { 453 f, tf, codec, ns := cmdtesting.NewAPIFactory() 454 actions := make(map[string]string) 455 456 var testPodList api.PodList 457 testPodList.Items = append(testPodList.Items, newPodWithStatus("bestpod", api.PodStatus{Phase: tt.podPhase}, "test")) 458 459 tf.Client = &fake.RESTClient{ 460 APIRegistry: api.Registry, 461 NegotiatedSerializer: ns, 462 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { 463 p, m := req.URL.Path, req.Method 464 actions[p] = m 465 switch { 466 case p == "/namespaces/test/pods/bestpod" && m == "GET": 467 return newResponse(200, &testPodList.Items[0]) 468 case p == "/namespaces/test/pods" && m == "GET": 469 event := watch.Event{Type: watch.Added, Object: &testPodList.Items[0]} 470 return newEventResponse(200, &event) 471 default: 472 t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) 473 return nil, nil 474 } 475 }), 476 } 477 478 c := newTestClient(f) 479 480 phase, err := c.WaitAndGetCompletedPodPhase("test", objBody(codec, &testPodList), 1*time.Second) 481 if (err != nil) != tt.err { 482 t.Fatalf("Expected error but there was none.") 483 } 484 if err != nil && err.Error() != tt.errMessage { 485 t.Fatalf("Expected error %s, got %s", tt.errMessage, err.Error()) 486 } 487 if phase != tt.expectedPhase { 488 t.Fatalf("Expected pod phase %s, got %s", tt.expectedPhase, phase) 489 } 490 } 491 } 492 493 func TestReal(t *testing.T) { 494 t.Skip("This is a live test, comment this line to run") 495 c := New(nil) 496 if err := c.Create("test", strings.NewReader(guestbookManifest), 300, false); err != nil { 497 t.Fatal(err) 498 } 499 500 testSvcEndpointManifest := testServiceManifest + "\n---\n" + testEndpointManifest 501 c = New(nil) 502 if err := c.Create("test-delete", strings.NewReader(testSvcEndpointManifest), 300, false); err != nil { 503 t.Fatal(err) 504 } 505 506 if err := c.Delete("test-delete", strings.NewReader(testEndpointManifest)); err != nil { 507 t.Fatal(err) 508 } 509 510 // ensures that delete does not fail if a resource is not found 511 if err := c.Delete("test-delete", strings.NewReader(testSvcEndpointManifest)); err != nil { 512 t.Fatal(err) 513 } 514 } 515 516 const testServiceManifest = ` 517 kind: Service 518 apiVersion: v1 519 metadata: 520 name: my-service 521 spec: 522 selector: 523 app: myapp 524 ports: 525 - port: 80 526 protocol: TCP 527 targetPort: 9376 528 ` 529 530 const testInvalidServiceManifest = ` 531 kind: Service 532 apiVersion: v1 533 spec: 534 ports: 535 - port: "80" 536 ` 537 538 const testEndpointManifest = ` 539 kind: Endpoints 540 apiVersion: v1 541 metadata: 542 name: my-service 543 subsets: 544 - addresses: 545 - ip: "1.2.3.4" 546 ports: 547 - port: 9376 548 ` 549 550 const guestbookManifest = ` 551 apiVersion: v1 552 kind: Service 553 metadata: 554 name: redis-master 555 labels: 556 app: redis 557 tier: backend 558 role: master 559 spec: 560 ports: 561 - port: 6379 562 targetPort: 6379 563 selector: 564 app: redis 565 tier: backend 566 role: master 567 --- 568 apiVersion: extensions/v1beta1 569 kind: Deployment 570 metadata: 571 name: redis-master 572 spec: 573 replicas: 1 574 template: 575 metadata: 576 labels: 577 app: redis 578 role: master 579 tier: backend 580 spec: 581 containers: 582 - name: master 583 image: gcr.io/google_containers/redis:e2e # or just image: redis 584 resources: 585 requests: 586 cpu: 100m 587 memory: 100Mi 588 ports: 589 - containerPort: 6379 590 --- 591 apiVersion: v1 592 kind: Service 593 metadata: 594 name: redis-slave 595 labels: 596 app: redis 597 tier: backend 598 role: slave 599 spec: 600 ports: 601 # the port that this service should serve on 602 - port: 6379 603 selector: 604 app: redis 605 tier: backend 606 role: slave 607 --- 608 apiVersion: extensions/v1beta1 609 kind: Deployment 610 metadata: 611 name: redis-slave 612 spec: 613 replicas: 2 614 template: 615 metadata: 616 labels: 617 app: redis 618 role: slave 619 tier: backend 620 spec: 621 containers: 622 - name: slave 623 image: gcr.io/google_samples/gb-redisslave:v1 624 resources: 625 requests: 626 cpu: 100m 627 memory: 100Mi 628 env: 629 - name: GET_HOSTS_FROM 630 value: dns 631 ports: 632 - containerPort: 6379 633 --- 634 apiVersion: v1 635 kind: Service 636 metadata: 637 name: frontend 638 labels: 639 app: guestbook 640 tier: frontend 641 spec: 642 ports: 643 - port: 80 644 selector: 645 app: guestbook 646 tier: frontend 647 --- 648 apiVersion: extensions/v1beta1 649 kind: Deployment 650 metadata: 651 name: frontend 652 spec: 653 replicas: 3 654 template: 655 metadata: 656 labels: 657 app: guestbook 658 tier: frontend 659 spec: 660 containers: 661 - name: php-redis 662 image: gcr.io/google-samples/gb-frontend:v4 663 resources: 664 requests: 665 cpu: 100m 666 memory: 100Mi 667 env: 668 - name: GET_HOSTS_FROM 669 value: dns 670 ports: 671 - containerPort: 80 672 `