github.com/vtuson/helm@v2.8.2+incompatible/pkg/kube/client_test.go (about)

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