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