...

Source file src/helm.sh/helm/v3/pkg/kube/client_test.go

Documentation: helm.sh/helm/v3/pkg/kube

     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  	"net/http"
    23  	"strings"
    24  	"testing"
    25  
    26  	v1 "k8s.io/api/core/v1"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	"k8s.io/apimachinery/pkg/runtime"
    29  	"k8s.io/cli-runtime/pkg/resource"
    30  	"k8s.io/client-go/kubernetes/scheme"
    31  	"k8s.io/client-go/rest/fake"
    32  	cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
    33  )
    34  
    35  var unstructuredSerializer = resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer
    36  var codec = scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
    37  
    38  func objBody(obj runtime.Object) io.ReadCloser {
    39  	return io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(codec, obj))))
    40  }
    41  
    42  func newPod(name string) v1.Pod {
    43  	return newPodWithStatus(name, v1.PodStatus{}, "")
    44  }
    45  
    46  func newPodWithStatus(name string, status v1.PodStatus, namespace string) v1.Pod {
    47  	ns := v1.NamespaceDefault
    48  	if namespace != "" {
    49  		ns = namespace
    50  	}
    51  	return v1.Pod{
    52  		ObjectMeta: metav1.ObjectMeta{
    53  			Name:      name,
    54  			Namespace: ns,
    55  			SelfLink:  "/api/v1/namespaces/default/pods/" + name,
    56  		},
    57  		Spec: v1.PodSpec{
    58  			Containers: []v1.Container{{
    59  				Name:  "app:v4",
    60  				Image: "abc/app:v4",
    61  				Ports: []v1.ContainerPort{{Name: "http", ContainerPort: 80}},
    62  			}},
    63  		},
    64  		Status: status,
    65  	}
    66  }
    67  
    68  func newPodList(names ...string) v1.PodList {
    69  	var list v1.PodList
    70  	for _, name := range names {
    71  		list.Items = append(list.Items, newPod(name))
    72  	}
    73  	return list
    74  }
    75  
    76  func notFoundBody() *metav1.Status {
    77  	return &metav1.Status{
    78  		Code:    http.StatusNotFound,
    79  		Status:  metav1.StatusFailure,
    80  		Reason:  metav1.StatusReasonNotFound,
    81  		Message: " \"\" not found",
    82  		Details: &metav1.StatusDetails{},
    83  	}
    84  }
    85  
    86  func newResponse(code int, obj runtime.Object) (*http.Response, error) {
    87  	header := http.Header{}
    88  	header.Set("Content-Type", runtime.ContentTypeJSON)
    89  	body := io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(codec, obj))))
    90  	return &http.Response{StatusCode: code, Header: header, Body: body}, nil
    91  }
    92  
    93  func newTestClient(t *testing.T) *Client {
    94  	testFactory := cmdtesting.NewTestFactory()
    95  	t.Cleanup(testFactory.Cleanup)
    96  
    97  	return &Client{
    98  		Factory: testFactory.WithNamespace("default"),
    99  		Log:     nopLogger,
   100  	}
   101  }
   102  
   103  func TestUpdate(t *testing.T) {
   104  	listA := newPodList("starfish", "otter", "squid")
   105  	listB := newPodList("starfish", "otter", "dolphin")
   106  	listC := newPodList("starfish", "otter", "dolphin")
   107  	listB.Items[0].Spec.Containers[0].Ports = []v1.ContainerPort{{Name: "https", ContainerPort: 443}}
   108  	listC.Items[0].Spec.Containers[0].Ports = []v1.ContainerPort{{Name: "https", ContainerPort: 443}}
   109  
   110  	var actions []string
   111  
   112  	c := newTestClient(t)
   113  	c.Factory.(*cmdtesting.TestFactory).UnstructuredClient = &fake.RESTClient{
   114  		NegotiatedSerializer: unstructuredSerializer,
   115  		Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
   116  			p, m := req.URL.Path, req.Method
   117  			actions = append(actions, p+":"+m)
   118  			t.Logf("got request %s %s", p, m)
   119  			switch {
   120  			case p == "/namespaces/default/pods/starfish" && m == "GET":
   121  				return newResponse(200, &listA.Items[0])
   122  			case p == "/namespaces/default/pods/otter" && m == "GET":
   123  				return newResponse(200, &listA.Items[1])
   124  			case p == "/namespaces/default/pods/otter" && m == "PATCH":
   125  				data, err := io.ReadAll(req.Body)
   126  				if err != nil {
   127  					t.Fatalf("could not dump request: %s", err)
   128  				}
   129  				req.Body.Close()
   130  				expected := `{}`
   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/dolphin" && m == "GET":
   136  				return newResponse(404, notFoundBody())
   137  			case p == "/namespaces/default/pods/starfish" && m == "PATCH":
   138  				data, err := io.ReadAll(req.Body)
   139  				if err != nil {
   140  					t.Fatalf("could not dump request: %s", err)
   141  				}
   142  				req.Body.Close()
   143  				expected := `{"spec":{"$setElementOrder/containers":[{"name":"app:v4"}],"containers":[{"$setElementOrder/ports":[{"containerPort":443}],"name":"app:v4","ports":[{"containerPort":443,"name":"https"},{"$patch":"delete","containerPort":80}]}]}}`
   144  				if string(data) != expected {
   145  					t.Errorf("expected patch\n%s\ngot\n%s", expected, string(data))
   146  				}
   147  				return newResponse(200, &listB.Items[0])
   148  			case p == "/namespaces/default/pods" && m == "POST":
   149  				return newResponse(200, &listB.Items[1])
   150  			case p == "/namespaces/default/pods/squid" && m == "DELETE":
   151  				return newResponse(200, &listB.Items[1])
   152  			case p == "/namespaces/default/pods/squid" && m == "GET":
   153  				return newResponse(200, &listB.Items[2])
   154  			default:
   155  				t.Fatalf("unexpected request: %s %s", req.Method, req.URL.Path)
   156  				return nil, nil
   157  			}
   158  		}),
   159  	}
   160  	first, err := c.Build(objBody(&listA), false)
   161  	if err != nil {
   162  		t.Fatal(err)
   163  	}
   164  	second, err := c.Build(objBody(&listB), false)
   165  	if err != nil {
   166  		t.Fatal(err)
   167  	}
   168  
   169  	result, err := c.Update(first, second, false)
   170  	if err != nil {
   171  		t.Fatal(err)
   172  	}
   173  
   174  	if len(result.Created) != 1 {
   175  		t.Errorf("expected 1 resource created, got %d", len(result.Created))
   176  	}
   177  	if len(result.Updated) != 2 {
   178  		t.Errorf("expected 2 resource updated, got %d", len(result.Updated))
   179  	}
   180  	if len(result.Deleted) != 1 {
   181  		t.Errorf("expected 1 resource deleted, got %d", len(result.Deleted))
   182  	}
   183  
   184  	// TODO: Find a way to test methods that use Client Set
   185  	// Test with a wait
   186  	// if err := c.Update("test", objBody(codec, &listB), objBody(codec, &listC), false, 300, true); err != nil {
   187  	// 	t.Fatal(err)
   188  	// }
   189  	// Test with a wait should fail
   190  	// TODO: A way to make this not based off of an extremely short timeout?
   191  	// if err := c.Update("test", objBody(codec, &listC), objBody(codec, &listA), false, 2, true); err != nil {
   192  	// 	t.Fatal(err)
   193  	// }
   194  	expectedActions := []string{
   195  		"/namespaces/default/pods/starfish:GET",
   196  		"/namespaces/default/pods/starfish:GET",
   197  		"/namespaces/default/pods/starfish:PATCH",
   198  		"/namespaces/default/pods/otter:GET",
   199  		"/namespaces/default/pods/otter:GET",
   200  		"/namespaces/default/pods/otter:GET",
   201  		"/namespaces/default/pods/dolphin:GET",
   202  		"/namespaces/default/pods:POST",
   203  		"/namespaces/default/pods/squid:GET",
   204  		"/namespaces/default/pods/squid:DELETE",
   205  	}
   206  	if len(expectedActions) != len(actions) {
   207  		t.Fatalf("unexpected number of requests, expected %d, got %d", len(expectedActions), len(actions))
   208  	}
   209  	for k, v := range expectedActions {
   210  		if actions[k] != v {
   211  			t.Errorf("expected %s request got %s", v, actions[k])
   212  		}
   213  	}
   214  }
   215  
   216  func TestBuild(t *testing.T) {
   217  	tests := []struct {
   218  		name      string
   219  		namespace string
   220  		reader    io.Reader
   221  		count     int
   222  		err       bool
   223  	}{
   224  		{
   225  			name:      "Valid input",
   226  			namespace: "test",
   227  			reader:    strings.NewReader(guestbookManifest),
   228  			count:     6,
   229  		}, {
   230  			name:      "Valid input, deploying resources into different namespaces",
   231  			namespace: "test",
   232  			reader:    strings.NewReader(namespacedGuestbookManifest),
   233  			count:     1,
   234  		},
   235  	}
   236  
   237  	c := newTestClient(t)
   238  	for _, tt := range tests {
   239  		t.Run(tt.name, func(t *testing.T) {
   240  			// Test for an invalid manifest
   241  			infos, err := c.Build(tt.reader, false)
   242  			if err != nil && !tt.err {
   243  				t.Errorf("Got error message when no error should have occurred: %v", err)
   244  			} else if err != nil && strings.Contains(err.Error(), "--validate=false") {
   245  				t.Error("error message was not scrubbed")
   246  			}
   247  
   248  			if len(infos) != tt.count {
   249  				t.Errorf("expected %d result objects, got %d", tt.count, len(infos))
   250  			}
   251  		})
   252  	}
   253  }
   254  
   255  func TestBuildTable(t *testing.T) {
   256  	tests := []struct {
   257  		name      string
   258  		namespace string
   259  		reader    io.Reader
   260  		count     int
   261  		err       bool
   262  	}{
   263  		{
   264  			name:      "Valid input",
   265  			namespace: "test",
   266  			reader:    strings.NewReader(guestbookManifest),
   267  			count:     6,
   268  		}, {
   269  			name:      "Valid input, deploying resources into different namespaces",
   270  			namespace: "test",
   271  			reader:    strings.NewReader(namespacedGuestbookManifest),
   272  			count:     1,
   273  		},
   274  	}
   275  
   276  	c := newTestClient(t)
   277  	for _, tt := range tests {
   278  		t.Run(tt.name, func(t *testing.T) {
   279  			// Test for an invalid manifest
   280  			infos, err := c.BuildTable(tt.reader, false)
   281  			if err != nil && !tt.err {
   282  				t.Errorf("Got error message when no error should have occurred: %v", err)
   283  			} else if err != nil && strings.Contains(err.Error(), "--validate=false") {
   284  				t.Error("error message was not scrubbed")
   285  			}
   286  
   287  			if len(infos) != tt.count {
   288  				t.Errorf("expected %d result objects, got %d", tt.count, len(infos))
   289  			}
   290  		})
   291  	}
   292  }
   293  
   294  func TestPerform(t *testing.T) {
   295  	tests := []struct {
   296  		name       string
   297  		reader     io.Reader
   298  		count      int
   299  		err        bool
   300  		errMessage string
   301  	}{
   302  		{
   303  			name:   "Valid input",
   304  			reader: strings.NewReader(guestbookManifest),
   305  			count:  6,
   306  		}, {
   307  			name:       "Empty manifests",
   308  			reader:     strings.NewReader(""),
   309  			err:        true,
   310  			errMessage: "no objects visited",
   311  		},
   312  	}
   313  
   314  	for _, tt := range tests {
   315  		t.Run(tt.name, func(t *testing.T) {
   316  			results := []*resource.Info{}
   317  
   318  			fn := func(info *resource.Info) error {
   319  				results = append(results, info)
   320  				return nil
   321  			}
   322  
   323  			c := newTestClient(t)
   324  			infos, err := c.Build(tt.reader, false)
   325  			if err != nil && err.Error() != tt.errMessage {
   326  				t.Errorf("Error while building manifests: %v", err)
   327  			}
   328  
   329  			err = perform(infos, fn)
   330  			if (err != nil) != tt.err {
   331  				t.Errorf("expected error: %v, got %v", tt.err, err)
   332  			}
   333  			if err != nil && err.Error() != tt.errMessage {
   334  				t.Errorf("expected error message: %v, got %v", tt.errMessage, err)
   335  			}
   336  
   337  			if len(results) != tt.count {
   338  				t.Errorf("expected %d result objects, got %d", tt.count, len(results))
   339  			}
   340  		})
   341  	}
   342  }
   343  
   344  func TestReal(t *testing.T) {
   345  	t.Skip("This is a live test, comment this line to run")
   346  	c := New(nil)
   347  	resources, err := c.Build(strings.NewReader(guestbookManifest), false)
   348  	if err != nil {
   349  		t.Fatal(err)
   350  	}
   351  	if _, err := c.Create(resources); err != nil {
   352  		t.Fatal(err)
   353  	}
   354  
   355  	testSvcEndpointManifest := testServiceManifest + "\n---\n" + testEndpointManifest
   356  	c = New(nil)
   357  	resources, err = c.Build(strings.NewReader(testSvcEndpointManifest), false)
   358  	if err != nil {
   359  		t.Fatal(err)
   360  	}
   361  	if _, err := c.Create(resources); err != nil {
   362  		t.Fatal(err)
   363  	}
   364  
   365  	resources, err = c.Build(strings.NewReader(testEndpointManifest), false)
   366  	if err != nil {
   367  		t.Fatal(err)
   368  	}
   369  
   370  	if _, errs := c.Delete(resources); errs != nil {
   371  		t.Fatal(errs)
   372  	}
   373  
   374  	resources, err = c.Build(strings.NewReader(testSvcEndpointManifest), false)
   375  	if err != nil {
   376  		t.Fatal(err)
   377  	}
   378  	// ensures that delete does not fail if a resource is not found
   379  	if _, errs := c.Delete(resources); errs != nil {
   380  		t.Fatal(errs)
   381  	}
   382  }
   383  
   384  const testServiceManifest = `
   385  kind: Service
   386  apiVersion: v1
   387  metadata:
   388    name: my-service
   389  spec:
   390    selector:
   391      app: myapp
   392    ports:
   393      - port: 80
   394        protocol: TCP
   395        targetPort: 9376
   396  `
   397  
   398  const testEndpointManifest = `
   399  kind: Endpoints
   400  apiVersion: v1
   401  metadata:
   402    name: my-service
   403  subsets:
   404    - addresses:
   405        - ip: "1.2.3.4"
   406      ports:
   407        - port: 9376
   408  `
   409  
   410  const guestbookManifest = `
   411  apiVersion: v1
   412  kind: Service
   413  metadata:
   414    name: redis-master
   415    labels:
   416      app: redis
   417      tier: backend
   418      role: master
   419  spec:
   420    ports:
   421    - port: 6379
   422      targetPort: 6379
   423    selector:
   424      app: redis
   425      tier: backend
   426      role: master
   427  ---
   428  apiVersion: extensions/v1beta1
   429  kind: Deployment
   430  metadata:
   431    name: redis-master
   432  spec:
   433    replicas: 1
   434    template:
   435      metadata:
   436        labels:
   437          app: redis
   438          role: master
   439          tier: backend
   440      spec:
   441        containers:
   442        - name: master
   443          image: registry.k8s.io/redis:e2e  # or just image: redis
   444          resources:
   445            requests:
   446              cpu: 100m
   447              memory: 100Mi
   448          ports:
   449          - containerPort: 6379
   450  ---
   451  apiVersion: v1
   452  kind: Service
   453  metadata:
   454    name: redis-slave
   455    labels:
   456      app: redis
   457      tier: backend
   458      role: slave
   459  spec:
   460    ports:
   461      # the port that this service should serve on
   462    - port: 6379
   463    selector:
   464      app: redis
   465      tier: backend
   466      role: slave
   467  ---
   468  apiVersion: extensions/v1beta1
   469  kind: Deployment
   470  metadata:
   471    name: redis-slave
   472  spec:
   473    replicas: 2
   474    template:
   475      metadata:
   476        labels:
   477          app: redis
   478          role: slave
   479          tier: backend
   480      spec:
   481        containers:
   482        - name: slave
   483          image: gcr.io/google_samples/gb-redisslave:v1
   484          resources:
   485            requests:
   486              cpu: 100m
   487              memory: 100Mi
   488          env:
   489          - name: GET_HOSTS_FROM
   490            value: dns
   491          ports:
   492          - containerPort: 6379
   493  ---
   494  apiVersion: v1
   495  kind: Service
   496  metadata:
   497    name: frontend
   498    labels:
   499      app: guestbook
   500      tier: frontend
   501  spec:
   502    ports:
   503    - port: 80
   504    selector:
   505      app: guestbook
   506      tier: frontend
   507  ---
   508  apiVersion: extensions/v1beta1
   509  kind: Deployment
   510  metadata:
   511    name: frontend
   512  spec:
   513    replicas: 3
   514    template:
   515      metadata:
   516        labels:
   517          app: guestbook
   518          tier: frontend
   519      spec:
   520        containers:
   521        - name: php-redis
   522          image: gcr.io/google-samples/gb-frontend:v4
   523          resources:
   524            requests:
   525              cpu: 100m
   526              memory: 100Mi
   527          env:
   528          - name: GET_HOSTS_FROM
   529            value: dns
   530          ports:
   531          - containerPort: 80
   532  `
   533  
   534  const namespacedGuestbookManifest = `
   535  apiVersion: extensions/v1beta1
   536  kind: Deployment
   537  metadata:
   538    name: frontend
   539    namespace: guestbook
   540  spec:
   541    replicas: 3
   542    template:
   543      metadata:
   544        labels:
   545          app: guestbook
   546          tier: frontend
   547      spec:
   548        containers:
   549        - name: php-redis
   550          image: gcr.io/google-samples/gb-frontend:v4
   551          resources:
   552            requests:
   553              cpu: 100m
   554              memory: 100Mi
   555          env:
   556          - name: GET_HOSTS_FROM
   557            value: dns
   558          ports:
   559          - containerPort: 80
   560  `
   561  

View as plain text