github.com/cloudposse/helm@v2.2.3+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  	var actions []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 = append(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 := []string{
   205  		"/namespaces/default/pods/starfish:GET",
   206  		"/namespaces/default/pods/starfish:PATCH",
   207  		"/namespaces/default/pods/otter:GET",
   208  		"/namespaces/default/pods/otter:GET",
   209  		"/namespaces/default/pods/dolphin:GET",
   210  		"/namespaces/default/pods:POST",
   211  	}
   212  	if len(expectedActions) != len(actions) {
   213  		t.Errorf("unexpected number of requests, expected %d, got %d", len(expectedActions), len(actions))
   214  		return
   215  	}
   216  	for k, v := range expectedActions {
   217  		if actions[k] != v {
   218  			t.Errorf("expected %s request got %s", v, actions[k])
   219  		}
   220  	}
   221  
   222  	if reaper.name != "squid" {
   223  		t.Errorf("unexpected reaper: %#v", reaper)
   224  	}
   225  
   226  }
   227  
   228  func TestBuild(t *testing.T) {
   229  	tests := []struct {
   230  		name        string
   231  		namespace   string
   232  		reader      io.Reader
   233  		count       int
   234  		swaggerFile string
   235  		err         bool
   236  		errMessage  string
   237  	}{
   238  		{
   239  			name:      "Valid input",
   240  			namespace: "test",
   241  			reader:    strings.NewReader(guestbookManifest),
   242  			count:     6,
   243  		}, {
   244  			name:        "Invalid schema",
   245  			namespace:   "test",
   246  			reader:      strings.NewReader(testInvalidServiceManifest),
   247  			swaggerFile: "../../vendor/k8s.io/kubernetes/api/swagger-spec/" + testapi.Default.GroupVersion().Version + ".json",
   248  			err:         true,
   249  			errMessage:  `error validating "": error validating data: expected type int, for field spec.ports[0].port, got string`,
   250  		},
   251  	}
   252  
   253  	for _, tt := range tests {
   254  		f, tf, _, _ := cmdtesting.NewAPIFactory()
   255  		c := &Client{Factory: f}
   256  		if tt.swaggerFile != "" {
   257  			data, err := ioutil.ReadFile(tt.swaggerFile)
   258  			if err != nil {
   259  				t.Fatalf("could not read swagger spec: %s", err)
   260  			}
   261  			validator, err := validation.NewSwaggerSchemaFromBytes(data, nil)
   262  			if err != nil {
   263  				t.Fatalf("could not load swagger spec: %s", err)
   264  			}
   265  			tf.Validator = validator
   266  		}
   267  
   268  		// Test for an invalid manifest
   269  		infos, err := c.Build(tt.namespace, tt.reader)
   270  		if err != nil && err.Error() != tt.errMessage {
   271  			t.Errorf("%q. expected error message: %v, got %v", tt.name, tt.errMessage, err)
   272  		} else if err != nil && !tt.err {
   273  			t.Errorf("%q. Got error message when no error should have occurred: %v, got %v", tt.name, tt.errMessage, err)
   274  		}
   275  
   276  		if len(infos) != tt.count {
   277  			t.Errorf("%q. expected %d result objects, got %d", tt.name, tt.count, len(infos))
   278  		}
   279  	}
   280  }
   281  
   282  func TestPerform(t *testing.T) {
   283  	tests := []struct {
   284  		name        string
   285  		namespace   string
   286  		reader      io.Reader
   287  		count       int
   288  		swaggerFile string
   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  		results := []*resource.Info{}
   308  
   309  		fn := func(info *resource.Info) error {
   310  			results = append(results, info)
   311  
   312  			if info.Namespace != tt.namespace {
   313  				t.Errorf("%q. expected namespace to be '%s', got %s", tt.name, tt.namespace, info.Namespace)
   314  			}
   315  			return nil
   316  		}
   317  
   318  		f, tf, _, _ := cmdtesting.NewAPIFactory()
   319  		c := &Client{Factory: f}
   320  		if tt.swaggerFile != "" {
   321  			data, err := ioutil.ReadFile(tt.swaggerFile)
   322  			if err != nil {
   323  				t.Fatalf("could not read swagger spec: %s", err)
   324  			}
   325  			validator, err := validation.NewSwaggerSchemaFromBytes(data, nil)
   326  			if err != nil {
   327  				t.Fatalf("could not load swagger spec: %s", err)
   328  			}
   329  			tf.Validator = validator
   330  		}
   331  
   332  		infos, err := c.Build(tt.namespace, tt.reader)
   333  		if err != nil && err.Error() != tt.errMessage {
   334  			t.Errorf("%q. Error while building manifests: %v", tt.name, err)
   335  		}
   336  
   337  		err = perform(c, tt.namespace, infos, fn)
   338  		if (err != nil) != tt.err {
   339  			t.Errorf("%q. expected error: %v, got %v", tt.name, tt.err, err)
   340  		}
   341  		if err != nil && err.Error() != tt.errMessage {
   342  			t.Errorf("%q. expected error message: %v, got %v", tt.name, tt.errMessage, err)
   343  		}
   344  
   345  		if len(results) != tt.count {
   346  			t.Errorf("%q. expected %d result objects, got %d", tt.name, tt.count, len(results))
   347  		}
   348  	}
   349  }
   350  
   351  func TestWaitAndGetCompletedPodPhase(t *testing.T) {
   352  	tests := []struct {
   353  		podPhase      api.PodPhase
   354  		expectedPhase api.PodPhase
   355  		err           bool
   356  		errMessage    string
   357  	}{
   358  		{
   359  			podPhase:      api.PodPending,
   360  			expectedPhase: api.PodUnknown,
   361  			err:           true,
   362  			errMessage:    "timed out waiting for the condition",
   363  		}, {
   364  			podPhase:      api.PodRunning,
   365  			expectedPhase: api.PodUnknown,
   366  			err:           true,
   367  			errMessage:    "timed out waiting for the condition",
   368  		}, {
   369  			podPhase:      api.PodSucceeded,
   370  			expectedPhase: api.PodSucceeded,
   371  		}, {
   372  			podPhase:      api.PodFailed,
   373  			expectedPhase: api.PodFailed,
   374  		},
   375  	}
   376  
   377  	for _, tt := range tests {
   378  		f, tf, codec, ns := cmdtesting.NewAPIFactory()
   379  		actions := make(map[string]string)
   380  
   381  		var testPodList api.PodList
   382  		testPodList.Items = append(testPodList.Items, newPodWithStatus("bestpod", api.PodStatus{Phase: tt.podPhase}, "test"))
   383  
   384  		tf.Client = &fake.RESTClient{
   385  			NegotiatedSerializer: ns,
   386  			Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
   387  				p, m := req.URL.Path, req.Method
   388  				actions[p] = m
   389  				switch {
   390  				case p == "/namespaces/test/pods/bestpod" && m == "GET":
   391  					return newResponse(200, &testPodList.Items[0])
   392  				case p == "/watch/namespaces/test/pods/bestpod" && m == "GET":
   393  					event := watch.Event{Type: watch.Added, Object: &testPodList.Items[0]}
   394  					return newEventResponse(200, &event)
   395  				default:
   396  					t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
   397  					return nil, nil
   398  				}
   399  			}),
   400  		}
   401  
   402  		c := &Client{Factory: f}
   403  
   404  		phase, err := c.WaitAndGetCompletedPodPhase("test", objBody(codec, &testPodList), 1*time.Second)
   405  		if (err != nil) != tt.err {
   406  			t.Fatalf("Expected error but there was none.")
   407  		}
   408  		if err != nil && err.Error() != tt.errMessage {
   409  			t.Fatalf("Expected error %s, got %s", tt.errMessage, err.Error())
   410  		}
   411  		if phase != tt.expectedPhase {
   412  			t.Fatalf("Expected pod phase %s, got %s", tt.expectedPhase, phase)
   413  		}
   414  	}
   415  }
   416  
   417  func TestReal(t *testing.T) {
   418  	t.Skip("This is a live test, comment this line to run")
   419  	c := New(nil)
   420  	if err := c.Create("test", strings.NewReader(guestbookManifest), 300, false); err != nil {
   421  		t.Fatal(err)
   422  	}
   423  
   424  	testSvcEndpointManifest := testServiceManifest + "\n---\n" + testEndpointManifest
   425  	c = New(nil)
   426  	if err := c.Create("test-delete", strings.NewReader(testSvcEndpointManifest), 300, false); err != nil {
   427  		t.Fatal(err)
   428  	}
   429  
   430  	if err := c.Delete("test-delete", strings.NewReader(testEndpointManifest)); err != nil {
   431  		t.Fatal(err)
   432  	}
   433  
   434  	// ensures that delete does not fail if a resource is not found
   435  	if err := c.Delete("test-delete", strings.NewReader(testSvcEndpointManifest)); err != nil {
   436  		t.Fatal(err)
   437  	}
   438  }
   439  
   440  const testServiceManifest = `
   441  kind: Service
   442  apiVersion: v1
   443  metadata:
   444    name: my-service
   445  spec:
   446    selector:
   447      app: myapp
   448    ports:
   449      - port: 80
   450        protocol: TCP
   451        targetPort: 9376
   452  `
   453  
   454  const testInvalidServiceManifest = `
   455  kind: Service
   456  apiVersion: v1
   457  spec:
   458    ports:
   459      - port: "80"
   460  `
   461  
   462  const testEndpointManifest = `
   463  kind: Endpoints
   464  apiVersion: v1
   465  metadata:
   466    name: my-service
   467  subsets:
   468    - addresses:
   469        - ip: "1.2.3.4"
   470      ports:
   471        - port: 9376
   472  `
   473  
   474  const guestbookManifest = `
   475  apiVersion: v1
   476  kind: Service
   477  metadata:
   478    name: redis-master
   479    labels:
   480      app: redis
   481      tier: backend
   482      role: master
   483  spec:
   484    ports:
   485    - port: 6379
   486      targetPort: 6379
   487    selector:
   488      app: redis
   489      tier: backend
   490      role: master
   491  ---
   492  apiVersion: extensions/v1beta1
   493  kind: Deployment
   494  metadata:
   495    name: redis-master
   496  spec:
   497    replicas: 1
   498    template:
   499      metadata:
   500        labels:
   501          app: redis
   502          role: master
   503          tier: backend
   504      spec:
   505        containers:
   506        - name: master
   507          image: gcr.io/google_containers/redis:e2e  # or just image: redis
   508          resources:
   509            requests:
   510              cpu: 100m
   511              memory: 100Mi
   512          ports:
   513          - containerPort: 6379
   514  ---
   515  apiVersion: v1
   516  kind: Service
   517  metadata:
   518    name: redis-slave
   519    labels:
   520      app: redis
   521      tier: backend
   522      role: slave
   523  spec:
   524    ports:
   525      # the port that this service should serve on
   526    - port: 6379
   527    selector:
   528      app: redis
   529      tier: backend
   530      role: slave
   531  ---
   532  apiVersion: extensions/v1beta1
   533  kind: Deployment
   534  metadata:
   535    name: redis-slave
   536  spec:
   537    replicas: 2
   538    template:
   539      metadata:
   540        labels:
   541          app: redis
   542          role: slave
   543          tier: backend
   544      spec:
   545        containers:
   546        - name: slave
   547          image: gcr.io/google_samples/gb-redisslave:v1
   548          resources:
   549            requests:
   550              cpu: 100m
   551              memory: 100Mi
   552          env:
   553          - name: GET_HOSTS_FROM
   554            value: dns
   555          ports:
   556          - containerPort: 6379
   557  ---
   558  apiVersion: v1
   559  kind: Service
   560  metadata:
   561    name: frontend
   562    labels:
   563      app: guestbook
   564      tier: frontend
   565  spec:
   566    ports:
   567    - port: 80
   568    selector:
   569      app: guestbook
   570      tier: frontend
   571  ---
   572  apiVersion: extensions/v1beta1
   573  kind: Deployment
   574  metadata:
   575    name: frontend
   576  spec:
   577    replicas: 3
   578    template:
   579      metadata:
   580        labels:
   581          app: guestbook
   582          tier: frontend
   583      spec:
   584        containers:
   585        - name: php-redis
   586          image: gcr.io/google-samples/gb-frontend:v4
   587          resources:
   588            requests:
   589              cpu: 100m
   590              memory: 100Mi
   591          env:
   592          - name: GET_HOSTS_FROM
   593            value: dns
   594          ports:
   595          - containerPort: 80
   596  `