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