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