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