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