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