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  `