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