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  `