github.com/umeshredd/helm@v3.0.0-alpha.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 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 } 198 199 c := newTestClient() 200 for _, tt := range tests { 201 t.Run(tt.name, func(t *testing.T) { 202 // Test for an invalid manifest 203 infos, err := c.Build(tt.reader) 204 if err != nil && !tt.err { 205 t.Errorf("Got error message when no error should have occurred: %v", err) 206 } else if err != nil && strings.Contains(err.Error(), "--validate=false") { 207 t.Error("error message was not scrubbed") 208 } 209 210 if len(infos) != tt.count { 211 t.Errorf("expected %d result objects, got %d", tt.count, len(infos)) 212 } 213 }) 214 } 215 } 216 217 func TestPerform(t *testing.T) { 218 tests := []struct { 219 name string 220 reader io.Reader 221 count int 222 err bool 223 errMessage string 224 }{ 225 { 226 name: "Valid input", 227 reader: strings.NewReader(guestbookManifest), 228 count: 6, 229 }, { 230 name: "Empty manifests", 231 reader: strings.NewReader(""), 232 err: true, 233 errMessage: "no objects visited", 234 }, 235 } 236 237 for _, tt := range tests { 238 t.Run(tt.name, func(t *testing.T) { 239 results := []*resource.Info{} 240 241 fn := func(info *resource.Info) error { 242 results = append(results, info) 243 return nil 244 } 245 246 c := newTestClient() 247 infos, err := c.Build(tt.reader) 248 if err != nil && err.Error() != tt.errMessage { 249 t.Errorf("Error while building manifests: %v", err) 250 } 251 252 err = perform(infos, fn) 253 if (err != nil) != tt.err { 254 t.Errorf("expected error: %v, got %v", tt.err, err) 255 } 256 if err != nil && err.Error() != tt.errMessage { 257 t.Errorf("expected error message: %v, got %v", tt.errMessage, err) 258 } 259 260 if len(results) != tt.count { 261 t.Errorf("expected %d result objects, got %d", tt.count, len(results)) 262 } 263 }) 264 } 265 } 266 267 func TestReal(t *testing.T) { 268 t.Skip("This is a live test, comment this line to run") 269 c := New(nil) 270 if err := c.Create(strings.NewReader(guestbookManifest)); err != nil { 271 t.Fatal(err) 272 } 273 274 testSvcEndpointManifest := testServiceManifest + "\n---\n" + testEndpointManifest 275 c = New(nil) 276 if err := c.Create(strings.NewReader(testSvcEndpointManifest)); err != nil { 277 t.Fatal(err) 278 } 279 280 if err := c.Delete(strings.NewReader(testEndpointManifest)); err != nil { 281 t.Fatal(err) 282 } 283 284 // ensures that delete does not fail if a resource is not found 285 if err := c.Delete(strings.NewReader(testSvcEndpointManifest)); err != nil { 286 t.Fatal(err) 287 } 288 } 289 290 const testServiceManifest = ` 291 kind: Service 292 apiVersion: v1 293 metadata: 294 name: my-service 295 spec: 296 selector: 297 app: myapp 298 ports: 299 - port: 80 300 protocol: TCP 301 targetPort: 9376 302 ` 303 304 const testInvalidServiceManifest = ` 305 kind: Service 306 apiVersion: v1 307 spec: 308 ports: 309 - port: "80" 310 ` 311 312 const testEndpointManifest = ` 313 kind: Endpoints 314 apiVersion: v1 315 metadata: 316 name: my-service 317 subsets: 318 - addresses: 319 - ip: "1.2.3.4" 320 ports: 321 - port: 9376 322 ` 323 324 const guestbookManifest = ` 325 apiVersion: v1 326 kind: Service 327 metadata: 328 name: redis-master 329 labels: 330 app: redis 331 tier: backend 332 role: master 333 spec: 334 ports: 335 - port: 6379 336 targetPort: 6379 337 selector: 338 app: redis 339 tier: backend 340 role: master 341 --- 342 apiVersion: extensions/v1beta1 343 kind: Deployment 344 metadata: 345 name: redis-master 346 spec: 347 replicas: 1 348 template: 349 metadata: 350 labels: 351 app: redis 352 role: master 353 tier: backend 354 spec: 355 containers: 356 - name: master 357 image: k8s.gcr.io/redis:e2e # or just image: redis 358 resources: 359 requests: 360 cpu: 100m 361 memory: 100Mi 362 ports: 363 - containerPort: 6379 364 --- 365 apiVersion: v1 366 kind: Service 367 metadata: 368 name: redis-slave 369 labels: 370 app: redis 371 tier: backend 372 role: slave 373 spec: 374 ports: 375 # the port that this service should serve on 376 - port: 6379 377 selector: 378 app: redis 379 tier: backend 380 role: slave 381 --- 382 apiVersion: extensions/v1beta1 383 kind: Deployment 384 metadata: 385 name: redis-slave 386 spec: 387 replicas: 2 388 template: 389 metadata: 390 labels: 391 app: redis 392 role: slave 393 tier: backend 394 spec: 395 containers: 396 - name: slave 397 image: gcr.io/google_samples/gb-redisslave:v1 398 resources: 399 requests: 400 cpu: 100m 401 memory: 100Mi 402 env: 403 - name: GET_HOSTS_FROM 404 value: dns 405 ports: 406 - containerPort: 6379 407 --- 408 apiVersion: v1 409 kind: Service 410 metadata: 411 name: frontend 412 labels: 413 app: guestbook 414 tier: frontend 415 spec: 416 ports: 417 - port: 80 418 selector: 419 app: guestbook 420 tier: frontend 421 --- 422 apiVersion: extensions/v1beta1 423 kind: Deployment 424 metadata: 425 name: frontend 426 spec: 427 replicas: 3 428 template: 429 metadata: 430 labels: 431 app: guestbook 432 tier: frontend 433 spec: 434 containers: 435 - name: php-redis 436 image: gcr.io/google-samples/gb-frontend:v4 437 resources: 438 requests: 439 cpu: 100m 440 memory: 100Mi 441 env: 442 - name: GET_HOSTS_FROM 443 value: dns 444 ports: 445 - containerPort: 80 446 `