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