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