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