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