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  `