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