github.com/y-taka-23/helm@v2.8.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/runtime/schema" 34 "k8s.io/apimachinery/pkg/watch" 35 "k8s.io/client-go/dynamic" 36 "k8s.io/client-go/rest/fake" 37 "k8s.io/kubernetes/pkg/api/testapi" 38 "k8s.io/kubernetes/pkg/apis/core" 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) core.Pod { 52 return newPodWithStatus(name, core.PodStatus{}, "") 53 } 54 55 func newPodWithStatus(name string, status core.PodStatus, namespace string) core.Pod { 56 ns := core.NamespaceDefault 57 if namespace != "" { 58 ns = namespace 59 } 60 return core.Pod{ 61 ObjectMeta: metav1.ObjectMeta{ 62 Name: name, 63 Namespace: ns, 64 SelfLink: "/api/v1/namespaces/default/pods/" + name, 65 }, 66 Spec: core.PodSpec{ 67 Containers: []core.Container{{ 68 Name: "app:v4", 69 Image: "abc/app:v4", 70 Ports: []core.ContainerPort{{Name: "http", ContainerPort: 80}}, 71 }}, 72 }, 73 Status: status, 74 } 75 } 76 77 func newPodList(names ...string) core.PodList { 78 var list core.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 = []core.ContainerPort{{Name: "https", ContainerPort: 443}} 152 listC.Items[0].Spec.Containers[0].Ports = []core.ContainerPort{{Name: "https", ContainerPort: 443}} 153 154 var actions []string 155 156 f, tf, codec, _ := cmdtesting.NewAPIFactory() 157 tf.UnstructuredClient = &fake.RESTClient{ 158 GroupVersion: schema.GroupVersion{Version: "v1"}, 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(core.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 err bool 240 }{ 241 { 242 name: "Valid input", 243 namespace: "test", 244 reader: strings.NewReader(guestbookManifest), 245 count: 6, 246 }, { 247 name: "Invalid schema", 248 namespace: "test", 249 reader: strings.NewReader(testInvalidServiceManifest), 250 err: true, 251 }, 252 } 253 254 for _, tt := range tests { 255 f, _, _, _ := cmdtesting.NewAPIFactory() 256 c := newTestClient(f) 257 258 // Test for an invalid manifest 259 infos, err := c.Build(tt.namespace, tt.reader) 260 if err != nil && !tt.err { 261 t.Errorf("%q. Got error message when no error should have occurred: %v", tt.name, err) 262 } else if err != nil && strings.Contains(err.Error(), "--validate=false") { 263 t.Errorf("%q. error message was not scrubbed", tt.name) 264 } 265 266 if len(infos) != tt.count { 267 t.Errorf("%q. expected %d result objects, got %d", tt.name, tt.count, len(infos)) 268 } 269 } 270 } 271 272 type testPrinter struct { 273 Objects []runtime.Object 274 Err error 275 printers.ResourcePrinter 276 } 277 278 func (t *testPrinter) PrintObj(obj runtime.Object, out io.Writer) error { 279 t.Objects = append(t.Objects, obj) 280 fmt.Fprintf(out, "%#v", obj) 281 return t.Err 282 } 283 284 func (t *testPrinter) HandledResources() []string { 285 return []string{} 286 } 287 288 func (t *testPrinter) AfterPrint(io.Writer, string) error { 289 return t.Err 290 } 291 292 func TestGet(t *testing.T) { 293 list := newPodList("starfish", "otter") 294 f, tf, _, _ := cmdtesting.NewAPIFactory() 295 tf.Printer = &testPrinter{} 296 tf.UnstructuredClient = &fake.RESTClient{ 297 GroupVersion: schema.GroupVersion{Version: "v1"}, 298 NegotiatedSerializer: dynamic.ContentConfig().NegotiatedSerializer, 299 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { 300 p, m := req.URL.Path, req.Method 301 //actions = append(actions, p+":"+m) 302 t.Logf("got request %s %s", p, m) 303 switch { 304 case p == "/namespaces/default/pods/starfish" && m == "GET": 305 return newResponse(404, notFoundBody()) 306 case p == "/namespaces/default/pods/otter" && m == "GET": 307 return newResponse(200, &list.Items[1]) 308 default: 309 t.Fatalf("unexpected request: %s %s", req.Method, req.URL.Path) 310 return nil, nil 311 } 312 }), 313 } 314 c := newTestClient(f) 315 316 // Test Success 317 data := strings.NewReader("kind: Pod\napiVersion: v1\nmetadata:\n name: otter") 318 o, err := c.Get("default", data) 319 if err != nil { 320 t.Errorf("Expected missing results, got %q", err) 321 } 322 if !strings.Contains(o, "==> v1/Pod") && !strings.Contains(o, "otter") { 323 t.Errorf("Expected v1/Pod otter, got %s", o) 324 } 325 326 // Test failure 327 data = strings.NewReader("kind: Pod\napiVersion: v1\nmetadata:\n name: starfish") 328 o, err = c.Get("default", data) 329 if err != nil { 330 t.Errorf("Expected missing results, got %q", err) 331 } 332 if !strings.Contains(o, "MISSING") && !strings.Contains(o, "pods\t\tstarfish") { 333 t.Errorf("Expected missing starfish, got %s", o) 334 } 335 } 336 337 func TestPerform(t *testing.T) { 338 tests := []struct { 339 name string 340 namespace string 341 reader io.Reader 342 count int 343 err bool 344 errMessage string 345 }{ 346 { 347 name: "Valid input", 348 namespace: "test", 349 reader: strings.NewReader(guestbookManifest), 350 count: 6, 351 }, { 352 name: "Empty manifests", 353 namespace: "test", 354 reader: strings.NewReader(""), 355 err: true, 356 errMessage: "no objects visited", 357 }, 358 } 359 360 for _, tt := range tests { 361 results := []*resource.Info{} 362 363 fn := func(info *resource.Info) error { 364 results = append(results, info) 365 366 if info.Namespace != tt.namespace { 367 t.Errorf("%q. expected namespace to be '%s', got %s", tt.name, tt.namespace, info.Namespace) 368 } 369 return nil 370 } 371 372 f, _, _, _ := cmdtesting.NewAPIFactory() 373 c := newTestClient(f) 374 infos, err := c.Build(tt.namespace, tt.reader) 375 if err != nil && err.Error() != tt.errMessage { 376 t.Errorf("%q. Error while building manifests: %v", tt.name, err) 377 } 378 379 err = perform(infos, fn) 380 if (err != nil) != tt.err { 381 t.Errorf("%q. expected error: %v, got %v", tt.name, tt.err, err) 382 } 383 if err != nil && err.Error() != tt.errMessage { 384 t.Errorf("%q. expected error message: %v, got %v", tt.name, tt.errMessage, err) 385 } 386 387 if len(results) != tt.count { 388 t.Errorf("%q. expected %d result objects, got %d", tt.name, tt.count, len(results)) 389 } 390 } 391 } 392 393 func TestWaitAndGetCompletedPodPhase(t *testing.T) { 394 tests := []struct { 395 podPhase core.PodPhase 396 expectedPhase core.PodPhase 397 err bool 398 errMessage string 399 }{ 400 { 401 podPhase: core.PodPending, 402 expectedPhase: core.PodUnknown, 403 err: true, 404 errMessage: "watch closed before Until timeout", 405 }, { 406 podPhase: core.PodRunning, 407 expectedPhase: core.PodUnknown, 408 err: true, 409 errMessage: "watch closed before Until timeout", 410 }, { 411 podPhase: core.PodSucceeded, 412 expectedPhase: core.PodSucceeded, 413 }, { 414 podPhase: core.PodFailed, 415 expectedPhase: core.PodFailed, 416 }, 417 } 418 419 for _, tt := range tests { 420 f, tf, codec, ns := cmdtesting.NewAPIFactory() 421 actions := make(map[string]string) 422 423 var testPodList core.PodList 424 testPodList.Items = append(testPodList.Items, newPodWithStatus("bestpod", core.PodStatus{Phase: tt.podPhase}, "test")) 425 426 tf.Client = &fake.RESTClient{ 427 NegotiatedSerializer: ns, 428 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { 429 p, m := req.URL.Path, req.Method 430 actions[p] = m 431 switch { 432 case p == "/namespaces/test/pods/bestpod" && m == "GET": 433 return newResponse(200, &testPodList.Items[0]) 434 case p == "/namespaces/test/pods" && m == "GET": 435 event := watch.Event{Type: watch.Added, Object: &testPodList.Items[0]} 436 return newEventResponse(200, &event) 437 default: 438 t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) 439 return nil, nil 440 } 441 }), 442 } 443 444 c := newTestClient(f) 445 446 phase, err := c.WaitAndGetCompletedPodPhase("test", objBody(codec, &testPodList), 1*time.Second) 447 if (err != nil) != tt.err { 448 t.Fatalf("Expected error but there was none.") 449 } 450 if err != nil && err.Error() != tt.errMessage { 451 t.Fatalf("Expected error %s, got %s", tt.errMessage, err.Error()) 452 } 453 if phase != tt.expectedPhase { 454 t.Fatalf("Expected pod phase %s, got %s", tt.expectedPhase, phase) 455 } 456 } 457 } 458 459 func TestReal(t *testing.T) { 460 t.Skip("This is a live test, comment this line to run") 461 c := New(nil) 462 if err := c.Create("test", strings.NewReader(guestbookManifest), 300, false); err != nil { 463 t.Fatal(err) 464 } 465 466 testSvcEndpointManifest := testServiceManifest + "\n---\n" + testEndpointManifest 467 c = New(nil) 468 if err := c.Create("test-delete", strings.NewReader(testSvcEndpointManifest), 300, false); err != nil { 469 t.Fatal(err) 470 } 471 472 if err := c.Delete("test-delete", strings.NewReader(testEndpointManifest)); err != nil { 473 t.Fatal(err) 474 } 475 476 // ensures that delete does not fail if a resource is not found 477 if err := c.Delete("test-delete", strings.NewReader(testSvcEndpointManifest)); err != nil { 478 t.Fatal(err) 479 } 480 } 481 482 const testServiceManifest = ` 483 kind: Service 484 apiVersion: v1 485 metadata: 486 name: my-service 487 spec: 488 selector: 489 app: myapp 490 ports: 491 - port: 80 492 protocol: TCP 493 targetPort: 9376 494 ` 495 496 const testInvalidServiceManifest = ` 497 kind: Service 498 apiVersion: v1 499 spec: 500 ports: 501 - port: "80" 502 ` 503 504 const testEndpointManifest = ` 505 kind: Endpoints 506 apiVersion: v1 507 metadata: 508 name: my-service 509 subsets: 510 - addresses: 511 - ip: "1.2.3.4" 512 ports: 513 - port: 9376 514 ` 515 516 const guestbookManifest = ` 517 apiVersion: v1 518 kind: Service 519 metadata: 520 name: redis-master 521 labels: 522 app: redis 523 tier: backend 524 role: master 525 spec: 526 ports: 527 - port: 6379 528 targetPort: 6379 529 selector: 530 app: redis 531 tier: backend 532 role: master 533 --- 534 apiVersion: extensions/v1beta1 535 kind: Deployment 536 metadata: 537 name: redis-master 538 spec: 539 replicas: 1 540 template: 541 metadata: 542 labels: 543 app: redis 544 role: master 545 tier: backend 546 spec: 547 containers: 548 - name: master 549 image: k8s.gcr.io/redis:e2e # or just image: redis 550 resources: 551 requests: 552 cpu: 100m 553 memory: 100Mi 554 ports: 555 - containerPort: 6379 556 --- 557 apiVersion: v1 558 kind: Service 559 metadata: 560 name: redis-slave 561 labels: 562 app: redis 563 tier: backend 564 role: slave 565 spec: 566 ports: 567 # the port that this service should serve on 568 - port: 6379 569 selector: 570 app: redis 571 tier: backend 572 role: slave 573 --- 574 apiVersion: extensions/v1beta1 575 kind: Deployment 576 metadata: 577 name: redis-slave 578 spec: 579 replicas: 2 580 template: 581 metadata: 582 labels: 583 app: redis 584 role: slave 585 tier: backend 586 spec: 587 containers: 588 - name: slave 589 image: gcr.io/google_samples/gb-redisslave:v1 590 resources: 591 requests: 592 cpu: 100m 593 memory: 100Mi 594 env: 595 - name: GET_HOSTS_FROM 596 value: dns 597 ports: 598 - containerPort: 6379 599 --- 600 apiVersion: v1 601 kind: Service 602 metadata: 603 name: frontend 604 labels: 605 app: guestbook 606 tier: frontend 607 spec: 608 ports: 609 - port: 80 610 selector: 611 app: guestbook 612 tier: frontend 613 --- 614 apiVersion: extensions/v1beta1 615 kind: Deployment 616 metadata: 617 name: frontend 618 spec: 619 replicas: 3 620 template: 621 metadata: 622 labels: 623 app: guestbook 624 tier: frontend 625 spec: 626 containers: 627 - name: php-redis 628 image: gcr.io/google-samples/gb-frontend:v4 629 resources: 630 requests: 631 cpu: 100m 632 memory: 100Mi 633 env: 634 - name: GET_HOSTS_FROM 635 value: dns 636 ports: 637 - containerPort: 80 638 `