github.com/cloudposse/helm@v2.2.3+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 var actions []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 = append(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 := []string{ 205 "/namespaces/default/pods/starfish:GET", 206 "/namespaces/default/pods/starfish:PATCH", 207 "/namespaces/default/pods/otter:GET", 208 "/namespaces/default/pods/otter:GET", 209 "/namespaces/default/pods/dolphin:GET", 210 "/namespaces/default/pods:POST", 211 } 212 if len(expectedActions) != len(actions) { 213 t.Errorf("unexpected number of requests, expected %d, got %d", len(expectedActions), len(actions)) 214 return 215 } 216 for k, v := range expectedActions { 217 if actions[k] != v { 218 t.Errorf("expected %s request got %s", v, actions[k]) 219 } 220 } 221 222 if reaper.name != "squid" { 223 t.Errorf("unexpected reaper: %#v", reaper) 224 } 225 226 } 227 228 func TestBuild(t *testing.T) { 229 tests := []struct { 230 name string 231 namespace string 232 reader io.Reader 233 count int 234 swaggerFile string 235 err bool 236 errMessage string 237 }{ 238 { 239 name: "Valid input", 240 namespace: "test", 241 reader: strings.NewReader(guestbookManifest), 242 count: 6, 243 }, { 244 name: "Invalid schema", 245 namespace: "test", 246 reader: strings.NewReader(testInvalidServiceManifest), 247 swaggerFile: "../../vendor/k8s.io/kubernetes/api/swagger-spec/" + testapi.Default.GroupVersion().Version + ".json", 248 err: true, 249 errMessage: `error validating "": error validating data: expected type int, for field spec.ports[0].port, got string`, 250 }, 251 } 252 253 for _, tt := range tests { 254 f, tf, _, _ := cmdtesting.NewAPIFactory() 255 c := &Client{Factory: f} 256 if tt.swaggerFile != "" { 257 data, err := ioutil.ReadFile(tt.swaggerFile) 258 if err != nil { 259 t.Fatalf("could not read swagger spec: %s", err) 260 } 261 validator, err := validation.NewSwaggerSchemaFromBytes(data, nil) 262 if err != nil { 263 t.Fatalf("could not load swagger spec: %s", err) 264 } 265 tf.Validator = validator 266 } 267 268 // Test for an invalid manifest 269 infos, err := c.Build(tt.namespace, tt.reader) 270 if err != nil && err.Error() != tt.errMessage { 271 t.Errorf("%q. expected error message: %v, got %v", tt.name, tt.errMessage, err) 272 } else if err != nil && !tt.err { 273 t.Errorf("%q. Got error message when no error should have occurred: %v, got %v", tt.name, tt.errMessage, err) 274 } 275 276 if len(infos) != tt.count { 277 t.Errorf("%q. expected %d result objects, got %d", tt.name, tt.count, len(infos)) 278 } 279 } 280 } 281 282 func TestPerform(t *testing.T) { 283 tests := []struct { 284 name string 285 namespace string 286 reader io.Reader 287 count int 288 swaggerFile string 289 err bool 290 errMessage string 291 }{ 292 { 293 name: "Valid input", 294 namespace: "test", 295 reader: strings.NewReader(guestbookManifest), 296 count: 6, 297 }, { 298 name: "Empty manifests", 299 namespace: "test", 300 reader: strings.NewReader(""), 301 err: true, 302 errMessage: "no objects visited", 303 }, 304 } 305 306 for _, tt := range tests { 307 results := []*resource.Info{} 308 309 fn := func(info *resource.Info) error { 310 results = append(results, info) 311 312 if info.Namespace != tt.namespace { 313 t.Errorf("%q. expected namespace to be '%s', got %s", tt.name, tt.namespace, info.Namespace) 314 } 315 return nil 316 } 317 318 f, tf, _, _ := cmdtesting.NewAPIFactory() 319 c := &Client{Factory: f} 320 if tt.swaggerFile != "" { 321 data, err := ioutil.ReadFile(tt.swaggerFile) 322 if err != nil { 323 t.Fatalf("could not read swagger spec: %s", err) 324 } 325 validator, err := validation.NewSwaggerSchemaFromBytes(data, nil) 326 if err != nil { 327 t.Fatalf("could not load swagger spec: %s", err) 328 } 329 tf.Validator = validator 330 } 331 332 infos, err := c.Build(tt.namespace, tt.reader) 333 if err != nil && err.Error() != tt.errMessage { 334 t.Errorf("%q. Error while building manifests: %v", tt.name, err) 335 } 336 337 err = perform(c, tt.namespace, infos, fn) 338 if (err != nil) != tt.err { 339 t.Errorf("%q. expected error: %v, got %v", tt.name, tt.err, err) 340 } 341 if err != nil && err.Error() != tt.errMessage { 342 t.Errorf("%q. expected error message: %v, got %v", tt.name, tt.errMessage, err) 343 } 344 345 if len(results) != tt.count { 346 t.Errorf("%q. expected %d result objects, got %d", tt.name, tt.count, len(results)) 347 } 348 } 349 } 350 351 func TestWaitAndGetCompletedPodPhase(t *testing.T) { 352 tests := []struct { 353 podPhase api.PodPhase 354 expectedPhase api.PodPhase 355 err bool 356 errMessage string 357 }{ 358 { 359 podPhase: api.PodPending, 360 expectedPhase: api.PodUnknown, 361 err: true, 362 errMessage: "timed out waiting for the condition", 363 }, { 364 podPhase: api.PodRunning, 365 expectedPhase: api.PodUnknown, 366 err: true, 367 errMessage: "timed out waiting for the condition", 368 }, { 369 podPhase: api.PodSucceeded, 370 expectedPhase: api.PodSucceeded, 371 }, { 372 podPhase: api.PodFailed, 373 expectedPhase: api.PodFailed, 374 }, 375 } 376 377 for _, tt := range tests { 378 f, tf, codec, ns := cmdtesting.NewAPIFactory() 379 actions := make(map[string]string) 380 381 var testPodList api.PodList 382 testPodList.Items = append(testPodList.Items, newPodWithStatus("bestpod", api.PodStatus{Phase: tt.podPhase}, "test")) 383 384 tf.Client = &fake.RESTClient{ 385 NegotiatedSerializer: ns, 386 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { 387 p, m := req.URL.Path, req.Method 388 actions[p] = m 389 switch { 390 case p == "/namespaces/test/pods/bestpod" && m == "GET": 391 return newResponse(200, &testPodList.Items[0]) 392 case p == "/watch/namespaces/test/pods/bestpod" && m == "GET": 393 event := watch.Event{Type: watch.Added, Object: &testPodList.Items[0]} 394 return newEventResponse(200, &event) 395 default: 396 t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) 397 return nil, nil 398 } 399 }), 400 } 401 402 c := &Client{Factory: f} 403 404 phase, err := c.WaitAndGetCompletedPodPhase("test", objBody(codec, &testPodList), 1*time.Second) 405 if (err != nil) != tt.err { 406 t.Fatalf("Expected error but there was none.") 407 } 408 if err != nil && err.Error() != tt.errMessage { 409 t.Fatalf("Expected error %s, got %s", tt.errMessage, err.Error()) 410 } 411 if phase != tt.expectedPhase { 412 t.Fatalf("Expected pod phase %s, got %s", tt.expectedPhase, phase) 413 } 414 } 415 } 416 417 func TestReal(t *testing.T) { 418 t.Skip("This is a live test, comment this line to run") 419 c := New(nil) 420 if err := c.Create("test", strings.NewReader(guestbookManifest), 300, false); err != nil { 421 t.Fatal(err) 422 } 423 424 testSvcEndpointManifest := testServiceManifest + "\n---\n" + testEndpointManifest 425 c = New(nil) 426 if err := c.Create("test-delete", strings.NewReader(testSvcEndpointManifest), 300, false); err != nil { 427 t.Fatal(err) 428 } 429 430 if err := c.Delete("test-delete", strings.NewReader(testEndpointManifest)); err != nil { 431 t.Fatal(err) 432 } 433 434 // ensures that delete does not fail if a resource is not found 435 if err := c.Delete("test-delete", strings.NewReader(testSvcEndpointManifest)); err != nil { 436 t.Fatal(err) 437 } 438 } 439 440 const testServiceManifest = ` 441 kind: Service 442 apiVersion: v1 443 metadata: 444 name: my-service 445 spec: 446 selector: 447 app: myapp 448 ports: 449 - port: 80 450 protocol: TCP 451 targetPort: 9376 452 ` 453 454 const testInvalidServiceManifest = ` 455 kind: Service 456 apiVersion: v1 457 spec: 458 ports: 459 - port: "80" 460 ` 461 462 const testEndpointManifest = ` 463 kind: Endpoints 464 apiVersion: v1 465 metadata: 466 name: my-service 467 subsets: 468 - addresses: 469 - ip: "1.2.3.4" 470 ports: 471 - port: 9376 472 ` 473 474 const guestbookManifest = ` 475 apiVersion: v1 476 kind: Service 477 metadata: 478 name: redis-master 479 labels: 480 app: redis 481 tier: backend 482 role: master 483 spec: 484 ports: 485 - port: 6379 486 targetPort: 6379 487 selector: 488 app: redis 489 tier: backend 490 role: master 491 --- 492 apiVersion: extensions/v1beta1 493 kind: Deployment 494 metadata: 495 name: redis-master 496 spec: 497 replicas: 1 498 template: 499 metadata: 500 labels: 501 app: redis 502 role: master 503 tier: backend 504 spec: 505 containers: 506 - name: master 507 image: gcr.io/google_containers/redis:e2e # or just image: redis 508 resources: 509 requests: 510 cpu: 100m 511 memory: 100Mi 512 ports: 513 - containerPort: 6379 514 --- 515 apiVersion: v1 516 kind: Service 517 metadata: 518 name: redis-slave 519 labels: 520 app: redis 521 tier: backend 522 role: slave 523 spec: 524 ports: 525 # the port that this service should serve on 526 - port: 6379 527 selector: 528 app: redis 529 tier: backend 530 role: slave 531 --- 532 apiVersion: extensions/v1beta1 533 kind: Deployment 534 metadata: 535 name: redis-slave 536 spec: 537 replicas: 2 538 template: 539 metadata: 540 labels: 541 app: redis 542 role: slave 543 tier: backend 544 spec: 545 containers: 546 - name: slave 547 image: gcr.io/google_samples/gb-redisslave:v1 548 resources: 549 requests: 550 cpu: 100m 551 memory: 100Mi 552 env: 553 - name: GET_HOSTS_FROM 554 value: dns 555 ports: 556 - containerPort: 6379 557 --- 558 apiVersion: v1 559 kind: Service 560 metadata: 561 name: frontend 562 labels: 563 app: guestbook 564 tier: frontend 565 spec: 566 ports: 567 - port: 80 568 selector: 569 app: guestbook 570 tier: frontend 571 --- 572 apiVersion: extensions/v1beta1 573 kind: Deployment 574 metadata: 575 name: frontend 576 spec: 577 replicas: 3 578 template: 579 metadata: 580 labels: 581 app: guestbook 582 tier: frontend 583 spec: 584 containers: 585 - name: php-redis 586 image: gcr.io/google-samples/gb-frontend:v4 587 resources: 588 requests: 589 cpu: 100m 590 memory: 100Mi 591 env: 592 - name: GET_HOSTS_FROM 593 value: dns 594 ports: 595 - containerPort: 80 596 `