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

     1  /*
     2  Copyright The Helm Authors.
     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  	"sort"
    25  	"strings"
    26  	"testing"
    27  	"time"
    28  
    29  	"k8s.io/api/core/v1"
    30  	apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
    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/cli-runtime/pkg/resource"
    35  	"k8s.io/client-go/kubernetes/scheme"
    36  	"k8s.io/client-go/rest/fake"
    37  	cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing"
    38  	kubectlscheme "k8s.io/kubernetes/pkg/kubectl/scheme"
    39  )
    40  
    41  func init() {
    42  	err := apiextv1beta1.AddToScheme(scheme.Scheme)
    43  	if err != nil {
    44  		panic(err)
    45  	}
    46  
    47  	// Tiller use the scheme from go-client, but the cmdtesting
    48  	// package used here is hardcoded to use the scheme from
    49  	// kubectl. So for testing, we need to add the CustomResourceDefinition
    50  	// type to both schemes.
    51  	err = apiextv1beta1.AddToScheme(kubectlscheme.Scheme)
    52  	if err != nil {
    53  		panic(err)
    54  	}
    55  }
    56  
    57  var (
    58  	unstructuredSerializer = resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer
    59  )
    60  
    61  func getCodec() runtime.Codec {
    62  	return scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
    63  }
    64  
    65  func objBody(obj runtime.Object) io.ReadCloser {
    66  	return ioutil.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(getCodec(), obj))))
    67  }
    68  
    69  func newPod(name string) v1.Pod {
    70  	return newPodWithStatus(name, v1.PodStatus{}, "")
    71  }
    72  
    73  func newPodWithStatus(name string, status v1.PodStatus, namespace string) v1.Pod {
    74  	ns := v1.NamespaceDefault
    75  	if namespace != "" {
    76  		ns = namespace
    77  	}
    78  	return v1.Pod{
    79  		ObjectMeta: metav1.ObjectMeta{
    80  			Name:      name,
    81  			Namespace: ns,
    82  			SelfLink:  "/api/v1/namespaces/default/pods/" + name,
    83  		},
    84  		Spec: v1.PodSpec{
    85  			Containers: []v1.Container{{
    86  				Name:  "app:v4",
    87  				Image: "abc/app:v4",
    88  				Ports: []v1.ContainerPort{{Name: "http", ContainerPort: 80}},
    89  			}},
    90  		},
    91  		Status: status,
    92  	}
    93  }
    94  
    95  func newPodList(names ...string) v1.PodList {
    96  	var list v1.PodList
    97  	for _, name := range names {
    98  		list.Items = append(list.Items, newPod(name))
    99  	}
   100  	return list
   101  }
   102  
   103  func newService(name string) v1.Service {
   104  	ns := v1.NamespaceDefault
   105  	return v1.Service{
   106  		ObjectMeta: metav1.ObjectMeta{
   107  			Name:      name,
   108  			Namespace: ns,
   109  			SelfLink:  "/api/v1/namespaces/default/services/" + name,
   110  		},
   111  		Spec: v1.ServiceSpec{},
   112  	}
   113  }
   114  
   115  func notFoundBody() *metav1.Status {
   116  	return &metav1.Status{
   117  		Code:    http.StatusNotFound,
   118  		Status:  metav1.StatusFailure,
   119  		Reason:  metav1.StatusReasonNotFound,
   120  		Message: " \"\" not found",
   121  		Details: &metav1.StatusDetails{},
   122  	}
   123  }
   124  
   125  func newResponse(code int, obj runtime.Object) (*http.Response, error) {
   126  	header := http.Header{}
   127  	header.Set("Content-Type", runtime.ContentTypeJSON)
   128  	body := ioutil.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(getCodec(), obj))))
   129  	return &http.Response{StatusCode: code, Header: header, Body: body}, nil
   130  }
   131  
   132  type testClient struct {
   133  	*Client
   134  	*cmdtesting.TestFactory
   135  }
   136  
   137  func newTestClient() *testClient {
   138  	tf := cmdtesting.NewTestFactory()
   139  	c := &Client{
   140  		Factory: tf,
   141  		Log:     nopLogger,
   142  	}
   143  	return &testClient{
   144  		Client:      c,
   145  		TestFactory: tf,
   146  	}
   147  }
   148  
   149  func TestUpdate(t *testing.T) {
   150  	listA := newPodList("starfish", "otter", "squid")
   151  	listB := newPodList("starfish", "otter", "dolphin")
   152  	listC := newPodList("starfish", "otter", "dolphin")
   153  	listB.Items[0].Spec.Containers[0].Ports = []v1.ContainerPort{{Name: "https", ContainerPort: 443}}
   154  	listC.Items[0].Spec.Containers[0].Ports = []v1.ContainerPort{{Name: "https", ContainerPort: 443}}
   155  
   156  	var actions []string
   157  
   158  	tf := cmdtesting.NewTestFactory()
   159  	defer tf.Cleanup()
   160  
   161  	tf.UnstructuredClient = &fake.RESTClient{
   162  		NegotiatedSerializer: unstructuredSerializer,
   163  		Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
   164  			p, m := req.URL.Path, req.Method
   165  			actions = append(actions, p+":"+m)
   166  			t.Logf("got request %s %s", p, m)
   167  			switch {
   168  			case p == "/namespaces/default/pods/starfish" && m == "GET":
   169  				return newResponse(200, &listA.Items[0])
   170  			case p == "/namespaces/default/pods/otter" && m == "GET":
   171  				return newResponse(200, &listA.Items[1])
   172  			case p == "/namespaces/default/pods/dolphin" && m == "GET":
   173  				return newResponse(404, notFoundBody())
   174  			case p == "/namespaces/default/pods/starfish" && m == "PATCH":
   175  				data, err := ioutil.ReadAll(req.Body)
   176  				if err != nil {
   177  					t.Fatalf("could not dump request: %s", err)
   178  				}
   179  				req.Body.Close()
   180  				expected := `{"spec":{"$setElementOrder/containers":[{"name":"app:v4"}],"containers":[{"$setElementOrder/ports":[{"containerPort":443}],"name":"app:v4","ports":[{"containerPort":443,"name":"https"},{"$patch":"delete","containerPort":80}]}]}}`
   181  				if string(data) != expected {
   182  					t.Errorf("expected patch\n%s\ngot\n%s", expected, string(data))
   183  				}
   184  				return newResponse(200, &listB.Items[0])
   185  			case p == "/namespaces/default/pods" && m == "POST":
   186  				return newResponse(200, &listB.Items[1])
   187  			case p == "/namespaces/default/pods/squid" && m == "DELETE":
   188  				return newResponse(200, &listB.Items[1])
   189  			case p == "/namespaces/default/pods/squid" && m == "GET":
   190  				return newResponse(200, &listA.Items[2])
   191  			default:
   192  				t.Fatalf("unexpected request: %s %s", req.Method, req.URL.Path)
   193  				return nil, nil
   194  			}
   195  		}),
   196  	}
   197  
   198  	c := &Client{
   199  		Factory: tf,
   200  		Log:     nopLogger,
   201  	}
   202  
   203  	if err := c.Update(v1.NamespaceDefault, objBody(&listA), objBody(&listB), false, false, 0, false); err != nil {
   204  		t.Fatal(err)
   205  	}
   206  	// TODO: Find a way to test methods that use Client Set
   207  	// Test with a wait
   208  	// if err := c.Update("test", objBody(&listB), objBody(&listC), false, 300, true); err != nil {
   209  	// 	t.Fatal(err)
   210  	// }
   211  	// Test with a wait should fail
   212  	// TODO: A way to make this not based off of an extremely short timeout?
   213  	// if err := c.Update("test", objBody(&listC), objBody(&listA), false, 2, true); err != nil {
   214  	// 	t.Fatal(err)
   215  	// }
   216  	expectedActions := []string{
   217  		"/namespaces/default/pods/starfish:GET",
   218  		"/namespaces/default/pods/starfish:PATCH",
   219  		"/namespaces/default/pods/otter:GET",
   220  		"/namespaces/default/pods/otter:GET",
   221  		"/namespaces/default/pods/dolphin:GET",
   222  		"/namespaces/default/pods:POST",
   223  		"/namespaces/default/pods/squid:GET",
   224  		"/namespaces/default/pods/squid:DELETE",
   225  	}
   226  	if len(expectedActions) != len(actions) {
   227  		t.Errorf("unexpected number of requests, expected %d, got %d", len(expectedActions), len(actions))
   228  		return
   229  	}
   230  	for k, v := range expectedActions {
   231  		if actions[k] != v {
   232  			t.Errorf("expected %s request got %s", v, actions[k])
   233  		}
   234  	}
   235  
   236  	// Test resource policy is respected
   237  	actions = nil
   238  	listA.Items[2].ObjectMeta.Annotations = map[string]string{ResourcePolicyAnno: "keep"}
   239  	if err := c.Update(v1.NamespaceDefault, objBody(&listA), objBody(&listB), false, false, 0, false); err != nil {
   240  		t.Fatal(err)
   241  	}
   242  	for _, v := range actions {
   243  		if v == "/namespaces/default/pods/squid:DELETE" {
   244  			t.Errorf("should not have deleted squid - it has helm.sh/resource-policy=keep")
   245  		}
   246  	}
   247  }
   248  
   249  func TestUpdateNonManagedResourceError(t *testing.T) {
   250  	actual := newPodList("starfish")
   251  	current := newPodList()
   252  	target := newPodList("starfish")
   253  
   254  	tf := cmdtesting.NewTestFactory()
   255  	defer tf.Cleanup()
   256  
   257  	tf.UnstructuredClient = &fake.RESTClient{
   258  		NegotiatedSerializer: unstructuredSerializer,
   259  		Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
   260  			p, m := req.URL.Path, req.Method
   261  			t.Logf("got request %s %s", p, m)
   262  			switch {
   263  			case p == "/namespaces/default/pods/starfish" && m == "GET":
   264  				return newResponse(200, &actual.Items[0])
   265  			default:
   266  				t.Fatalf("unexpected request: %s %s", req.Method, req.URL.Path)
   267  				return nil, nil
   268  			}
   269  		}),
   270  	}
   271  
   272  	c := &Client{
   273  		Factory: tf,
   274  		Log:     nopLogger,
   275  	}
   276  
   277  	if err := c.Update(v1.NamespaceDefault, objBody(&current), objBody(&target), false, false, 0, false); err != nil {
   278  		if err.Error() != "kind Pod with the name \"starfish\" already exists in the cluster and wasn't defined in the previous release. Before upgrading, please either delete the resource from the cluster or remove it from the chart" {
   279  			t.Fatal(err)
   280  		}
   281  	} else {
   282  		t.Fatalf("error expected")
   283  	}
   284  }
   285  
   286  func TestDeleteWithTimeout(t *testing.T) {
   287  	testCases := map[string]struct {
   288  		deleteTimeout int64
   289  		deleteAfter   time.Duration
   290  		success       bool
   291  	}{
   292  		"resource is deleted within timeout period": {
   293  			int64((2 * time.Minute).Seconds()),
   294  			10 * time.Second,
   295  			true,
   296  		},
   297  		"resource is not deleted within the timeout period": {
   298  			int64((10 * time.Second).Seconds()),
   299  			20 * time.Second,
   300  			false,
   301  		},
   302  	}
   303  
   304  	for tn, tc := range testCases {
   305  		t.Run(tn, func(t *testing.T) {
   306  			c := newTestClient()
   307  			defer c.Cleanup()
   308  
   309  			service := newService("my-service")
   310  			startTime := time.Now()
   311  			c.TestFactory.UnstructuredClient = &fake.RESTClient{
   312  				GroupVersion:         schema.GroupVersion{Version: "v1"},
   313  				NegotiatedSerializer: unstructuredSerializer,
   314  				Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
   315  					currentTime := time.Now()
   316  					if startTime.Add(tc.deleteAfter).Before(currentTime) {
   317  						return newResponse(404, notFoundBody())
   318  					}
   319  					return newResponse(200, &service)
   320  				}),
   321  			}
   322  
   323  			err := c.DeleteWithTimeout(metav1.NamespaceDefault, strings.NewReader(testServiceManifest), tc.deleteTimeout, true)
   324  			if err != nil && tc.success {
   325  				t.Errorf("expected no error, but got %v", err)
   326  			}
   327  			if err == nil && !tc.success {
   328  				t.Errorf("expected error, but didn't get one")
   329  			}
   330  		})
   331  	}
   332  }
   333  
   334  func TestBuild(t *testing.T) {
   335  	tests := []struct {
   336  		name      string
   337  		namespace string
   338  		reader    io.Reader
   339  		count     int
   340  		err       bool
   341  	}{
   342  		{
   343  			name:      "Valid input",
   344  			namespace: "test",
   345  			reader:    strings.NewReader(guestbookManifest),
   346  			count:     6,
   347  		}, {
   348  			name:      "Invalid schema",
   349  			namespace: "test",
   350  			reader:    strings.NewReader(testInvalidServiceManifest),
   351  			err:       true,
   352  		},
   353  	}
   354  
   355  	c := newTestClient()
   356  	for _, tt := range tests {
   357  		t.Run(tt.name, func(t *testing.T) {
   358  			c.Cleanup()
   359  
   360  			// Test for an invalid manifest
   361  			infos, err := c.Build(tt.namespace, tt.reader)
   362  			if err != nil && !tt.err {
   363  				t.Errorf("Got error message when no error should have occurred: %v", err)
   364  			} else if err != nil && strings.Contains(err.Error(), "--validate=false") {
   365  				t.Error("error message was not scrubbed")
   366  			}
   367  
   368  			if len(infos) != tt.count {
   369  				t.Errorf("expected %d result objects, got %d", tt.count, len(infos))
   370  			}
   371  		})
   372  	}
   373  }
   374  
   375  func TestGet(t *testing.T) {
   376  	list := newPodList("starfish", "otter")
   377  	c := newTestClient()
   378  	defer c.Cleanup()
   379  	c.TestFactory.UnstructuredClient = &fake.RESTClient{
   380  		GroupVersion:         schema.GroupVersion{Version: "v1"},
   381  		NegotiatedSerializer: unstructuredSerializer,
   382  		Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
   383  			p, m := req.URL.Path, req.Method
   384  			t.Logf("got request %s %s", p, m)
   385  			switch {
   386  			case p == "/namespaces/default/pods/starfish" && m == "GET":
   387  				return newResponse(404, notFoundBody())
   388  			case p == "/namespaces/default/pods/otter" && m == "GET":
   389  				return newResponse(200, &list.Items[1])
   390  			default:
   391  				t.Fatalf("unexpected request: %s %s", req.Method, req.URL.Path)
   392  				return nil, nil
   393  			}
   394  		}),
   395  	}
   396  
   397  	// Test Success
   398  	data := strings.NewReader("kind: Pod\napiVersion: v1\nmetadata:\n  name: otter")
   399  	o, err := c.Get("default", data)
   400  	if err != nil {
   401  		t.Errorf("Expected missing results, got %q", err)
   402  	}
   403  	if !strings.Contains(o, "==> v1/Pod") && !strings.Contains(o, "otter") {
   404  		t.Errorf("Expected v1/Pod otter, got %s", o)
   405  	}
   406  
   407  	// Test failure
   408  	data = strings.NewReader("kind: Pod\napiVersion: v1\nmetadata:\n  name: starfish")
   409  	o, err = c.Get("default", data)
   410  	if err != nil {
   411  		t.Errorf("Expected missing results, got %q", err)
   412  	}
   413  	if !strings.Contains(o, "MISSING") && !strings.Contains(o, "pods\t\tstarfish") {
   414  		t.Errorf("Expected missing starfish, got %s", o)
   415  	}
   416  }
   417  
   418  func TestResourceTypeSortOrder(t *testing.T) {
   419  	pod := newPod("my-pod")
   420  	service := newService("my-service")
   421  	c := newTestClient()
   422  	defer c.Cleanup()
   423  	c.TestFactory.UnstructuredClient = &fake.RESTClient{
   424  		GroupVersion:         schema.GroupVersion{Version: "v1"},
   425  		NegotiatedSerializer: unstructuredSerializer,
   426  		Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
   427  			p, m := req.URL.Path, req.Method
   428  			t.Logf("got request %s %s", p, m)
   429  			switch {
   430  			case p == "/namespaces/default/pods/my-pod" && m == "GET":
   431  				return newResponse(200, &pod)
   432  			case p == "/namespaces/default/services/my-service" && m == "GET":
   433  				return newResponse(200, &service)
   434  			default:
   435  				t.Fatalf("unexpected request: %s %s", req.Method, req.URL.Path)
   436  				return nil, nil
   437  			}
   438  		}),
   439  	}
   440  
   441  	// Test sorting order
   442  	data := strings.NewReader(testResourceTypeSortOrder)
   443  	o, err := c.Get("default", data)
   444  	if err != nil {
   445  		t.Errorf("Expected missing results, got %q", err)
   446  	}
   447  	podIndex := strings.Index(o, "my-pod")
   448  	serviceIndex := strings.Index(o, "my-service")
   449  	if podIndex == -1 {
   450  		t.Errorf("Expected v1/Pod my-pod, got %s", o)
   451  	}
   452  	if serviceIndex == -1 {
   453  		t.Errorf("Expected v1/Service my-service, got %s", o)
   454  	}
   455  	if !sort.IntsAreSorted([]int{podIndex, serviceIndex}) {
   456  		t.Errorf("Expected order: [v1/Pod v1/Service], got %s", o)
   457  	}
   458  }
   459  
   460  func TestResourceSortOrder(t *testing.T) {
   461  	list := newPodList("albacore", "coral", "beluga")
   462  	c := newTestClient()
   463  	defer c.Cleanup()
   464  	c.TestFactory.UnstructuredClient = &fake.RESTClient{
   465  		GroupVersion:         schema.GroupVersion{Version: "v1"},
   466  		NegotiatedSerializer: unstructuredSerializer,
   467  		Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
   468  			p, m := req.URL.Path, req.Method
   469  			t.Logf("got request %s %s", p, m)
   470  			switch {
   471  			case p == "/namespaces/default/pods/albacore" && m == "GET":
   472  				return newResponse(200, &list.Items[0])
   473  			case p == "/namespaces/default/pods/coral" && m == "GET":
   474  				return newResponse(200, &list.Items[1])
   475  			case p == "/namespaces/default/pods/beluga" && m == "GET":
   476  				return newResponse(200, &list.Items[2])
   477  			default:
   478  				t.Fatalf("unexpected request: %s %s", req.Method, req.URL.Path)
   479  				return nil, nil
   480  			}
   481  		}),
   482  	}
   483  
   484  	// Test sorting order
   485  	data := strings.NewReader(testResourceSortOrder)
   486  	o, err := c.Get("default", data)
   487  	if err != nil {
   488  		t.Errorf("Expected missing results, got %q", err)
   489  	}
   490  	albacoreIndex := strings.Index(o, "albacore")
   491  	belugaIndex := strings.Index(o, "beluga")
   492  	coralIndex := strings.Index(o, "coral")
   493  	if albacoreIndex == -1 {
   494  		t.Errorf("Expected v1/Pod albacore, got %s", o)
   495  	}
   496  	if belugaIndex == -1 {
   497  		t.Errorf("Expected v1/Pod beluga, got %s", o)
   498  	}
   499  	if coralIndex == -1 {
   500  		t.Errorf("Expected v1/Pod coral, got %s", o)
   501  	}
   502  	if !sort.IntsAreSorted([]int{albacoreIndex, belugaIndex, coralIndex}) {
   503  		t.Errorf("Expected order: [albacore beluga coral], got %s", o)
   504  	}
   505  }
   506  
   507  func TestWaitUntilCRDEstablished(t *testing.T) {
   508  	testCases := map[string]struct {
   509  		conditions            []apiextv1beta1.CustomResourceDefinitionCondition
   510  		returnConditionsAfter int
   511  		success               bool
   512  	}{
   513  		"crd reaches established state after 2 requests": {
   514  			conditions: []apiextv1beta1.CustomResourceDefinitionCondition{
   515  				{
   516  					Type:   apiextv1beta1.Established,
   517  					Status: apiextv1beta1.ConditionTrue,
   518  				},
   519  			},
   520  			returnConditionsAfter: 2,
   521  			success:               true,
   522  		},
   523  		"crd does not reach established state before timeout": {
   524  			conditions:            []apiextv1beta1.CustomResourceDefinitionCondition{},
   525  			returnConditionsAfter: 100,
   526  			success:               false,
   527  		},
   528  		"crd name is not accepted": {
   529  			conditions: []apiextv1beta1.CustomResourceDefinitionCondition{
   530  				{
   531  					Type:   apiextv1beta1.NamesAccepted,
   532  					Status: apiextv1beta1.ConditionFalse,
   533  				},
   534  			},
   535  			returnConditionsAfter: 1,
   536  			success:               false,
   537  		},
   538  	}
   539  
   540  	for tn, tc := range testCases {
   541  		func(name string) {
   542  			c := newTestClient()
   543  			defer c.Cleanup()
   544  
   545  			crdWithoutConditions := newCrdWithStatus("name", apiextv1beta1.CustomResourceDefinitionStatus{})
   546  			crdWithConditions := newCrdWithStatus("name", apiextv1beta1.CustomResourceDefinitionStatus{
   547  				Conditions: tc.conditions,
   548  			})
   549  
   550  			requestCount := 0
   551  			c.TestFactory.UnstructuredClient = &fake.RESTClient{
   552  				GroupVersion:         schema.GroupVersion{Version: "v1"},
   553  				NegotiatedSerializer: unstructuredSerializer,
   554  				Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
   555  					var crd apiextv1beta1.CustomResourceDefinition
   556  					if requestCount < tc.returnConditionsAfter {
   557  						crd = crdWithoutConditions
   558  					} else {
   559  						crd = crdWithConditions
   560  					}
   561  					requestCount++
   562  					return newResponse(200, &crd)
   563  				}),
   564  			}
   565  
   566  			err := c.WaitUntilCRDEstablished(strings.NewReader(crdManifest), 5*time.Second)
   567  			if err != nil && tc.success {
   568  				t.Errorf("%s: expected no error, but got %v", name, err)
   569  			}
   570  			if err == nil && !tc.success {
   571  				t.Errorf("%s: expected error, but didn't get one", name)
   572  			}
   573  		}(tn)
   574  	}
   575  }
   576  
   577  func newCrdWithStatus(name string, status apiextv1beta1.CustomResourceDefinitionStatus) apiextv1beta1.CustomResourceDefinition {
   578  	crd := apiextv1beta1.CustomResourceDefinition{
   579  		ObjectMeta: metav1.ObjectMeta{
   580  			Name:      name,
   581  			Namespace: metav1.NamespaceDefault,
   582  		},
   583  		Spec:   apiextv1beta1.CustomResourceDefinitionSpec{},
   584  		Status: status,
   585  	}
   586  	return crd
   587  }
   588  
   589  func TestPerform(t *testing.T) {
   590  	tests := []struct {
   591  		name       string
   592  		namespace  string
   593  		reader     io.Reader
   594  		count      int
   595  		err        bool
   596  		errMessage string
   597  	}{
   598  		{
   599  			name:      "Valid input",
   600  			namespace: "test",
   601  			reader:    strings.NewReader(guestbookManifest),
   602  			count:     6,
   603  		}, {
   604  			name:       "Empty manifests",
   605  			namespace:  "test",
   606  			reader:     strings.NewReader(""),
   607  			err:        true,
   608  			errMessage: "no objects visited",
   609  		},
   610  	}
   611  
   612  	for _, tt := range tests {
   613  		t.Run(tt.name, func(t *testing.T) {
   614  			results := []*resource.Info{}
   615  
   616  			fn := func(info *resource.Info) error {
   617  				results = append(results, info)
   618  
   619  				if info.Namespace != tt.namespace {
   620  					t.Errorf("expected namespace to be '%s', got %s", tt.namespace, info.Namespace)
   621  				}
   622  				return nil
   623  			}
   624  
   625  			c := newTestClient()
   626  			defer c.Cleanup()
   627  			infos, err := c.Build(tt.namespace, tt.reader)
   628  			if err != nil && err.Error() != tt.errMessage {
   629  				t.Errorf("Error while building manifests: %v", err)
   630  			}
   631  
   632  			err = perform(infos, fn)
   633  			if (err != nil) != tt.err {
   634  				t.Errorf("expected error: %v, got %v", tt.err, err)
   635  			}
   636  			if err != nil && err.Error() != tt.errMessage {
   637  				t.Errorf("expected error message: %v, got %v", tt.errMessage, err)
   638  			}
   639  
   640  			if len(results) != tt.count {
   641  				t.Errorf("expected %d result objects, got %d", tt.count, len(results))
   642  			}
   643  		})
   644  	}
   645  }
   646  
   647  func TestReal(t *testing.T) {
   648  	t.Skip("This is a live test, comment this line to run")
   649  	c := New(nil)
   650  	if err := c.Create("test", strings.NewReader(guestbookManifest), 300, false); err != nil {
   651  		t.Fatal(err)
   652  	}
   653  
   654  	testSvcEndpointManifest := testServiceManifest + "\n---\n" + testEndpointManifest
   655  	c = New(nil)
   656  	if err := c.Create("test-delete", strings.NewReader(testSvcEndpointManifest), 300, false); err != nil {
   657  		t.Fatal(err)
   658  	}
   659  
   660  	if err := c.Delete("test-delete", strings.NewReader(testEndpointManifest)); err != nil {
   661  		t.Fatal(err)
   662  	}
   663  
   664  	// ensures that delete does not fail if a resource is not found
   665  	if err := c.Delete("test-delete", strings.NewReader(testSvcEndpointManifest)); err != nil {
   666  		t.Fatal(err)
   667  	}
   668  }
   669  
   670  const testResourceTypeSortOrder = `
   671  kind: Service
   672  apiVersion: v1
   673  metadata:
   674    name: my-service
   675  ---
   676  kind: Pod
   677  apiVersion: v1
   678  metadata:
   679    name: my-pod
   680  `
   681  
   682  const testResourceSortOrder = `
   683  kind: Pod
   684  apiVersion: v1
   685  metadata:
   686    name: albacore
   687  ---
   688  kind: Pod
   689  apiVersion: v1
   690  metadata:
   691    name: coral
   692  ---
   693  kind: Pod
   694  apiVersion: v1
   695  metadata:
   696    name: beluga
   697  `
   698  
   699  const testServiceManifest = `
   700  kind: Service
   701  apiVersion: v1
   702  metadata:
   703    name: my-service
   704  spec:
   705    selector:
   706      app: myapp
   707    ports:
   708      - port: 80
   709        protocol: TCP
   710        targetPort: 9376
   711  `
   712  
   713  const testInvalidServiceManifest = `
   714  kind: Service
   715  apiVersion: v1
   716  spec:
   717    ports:
   718      - port: "80"
   719  `
   720  
   721  const testEndpointManifest = `
   722  kind: Endpoints
   723  apiVersion: v1
   724  metadata:
   725    name: my-service
   726  subsets:
   727    - addresses:
   728        - ip: "1.2.3.4"
   729      ports:
   730        - port: 9376
   731  `
   732  
   733  const guestbookManifest = `
   734  apiVersion: v1
   735  kind: Service
   736  metadata:
   737    name: redis-master
   738    labels:
   739      app: redis
   740      tier: backend
   741      role: master
   742  spec:
   743    ports:
   744    - port: 6379
   745      targetPort: 6379
   746    selector:
   747      app: redis
   748      tier: backend
   749      role: master
   750  ---
   751  apiVersion: apps/v1
   752  kind: Deployment
   753  metadata:
   754    name: redis-master
   755  spec:
   756    replicas: 1
   757    template:
   758      metadata:
   759        labels:
   760          app: redis
   761          role: master
   762          tier: backend
   763      spec:
   764        containers:
   765        - name: master
   766          image: k8s.gcr.io/redis:e2e  # or just image: redis
   767          resources:
   768            requests:
   769              cpu: 100m
   770              memory: 100Mi
   771          ports:
   772          - containerPort: 6379
   773  ---
   774  apiVersion: v1
   775  kind: Service
   776  metadata:
   777    name: redis-slave
   778    labels:
   779      app: redis
   780      tier: backend
   781      role: slave
   782  spec:
   783    ports:
   784      # the port that this service should serve on
   785    - port: 6379
   786    selector:
   787      app: redis
   788      tier: backend
   789      role: slave
   790  ---
   791  apiVersion: apps/v1
   792  kind: Deployment
   793  metadata:
   794    name: redis-slave
   795  spec:
   796    replicas: 2
   797    template:
   798      metadata:
   799        labels:
   800          app: redis
   801          role: slave
   802          tier: backend
   803      spec:
   804        containers:
   805        - name: slave
   806          image: gcr.io/google_samples/gb-redisslave:v1
   807          resources:
   808            requests:
   809              cpu: 100m
   810              memory: 100Mi
   811          env:
   812          - name: GET_HOSTS_FROM
   813            value: dns
   814          ports:
   815          - containerPort: 6379
   816  ---
   817  apiVersion: v1
   818  kind: Service
   819  metadata:
   820    name: frontend
   821    labels:
   822      app: guestbook
   823      tier: frontend
   824  spec:
   825    ports:
   826    - port: 80
   827    selector:
   828      app: guestbook
   829      tier: frontend
   830  ---
   831  apiVersion: apps/v1
   832  kind: Deployment
   833  metadata:
   834    name: frontend
   835  spec:
   836    replicas: 3
   837    template:
   838      metadata:
   839        labels:
   840          app: guestbook
   841          tier: frontend
   842      spec:
   843        containers:
   844        - name: php-redis
   845          image: gcr.io/google-samples/gb-frontend:v4
   846          resources:
   847            requests:
   848              cpu: 100m
   849              memory: 100Mi
   850          env:
   851          - name: GET_HOSTS_FROM
   852            value: dns
   853          ports:
   854          - containerPort: 80
   855  `
   856  
   857  const crdManifest = `
   858  apiVersion: apiextensions.k8s.io/v1beta1
   859  kind: CustomResourceDefinition
   860  metadata:
   861    creationTimestamp: null
   862    labels:
   863      controller-tools.k8s.io: "1.0"
   864    name: applications.app.k8s.io
   865  spec:
   866    group: app.k8s.io
   867    names:
   868      kind: Application
   869      plural: applications
   870    scope: Namespaced
   871    validation:
   872      openAPIV3Schema:
   873        properties:
   874          apiVersion:
   875            description: 'Description'
   876            type: string
   877          kind:
   878            description: 'Kind'
   879            type: string
   880          metadata:
   881            type: object
   882          spec:
   883            type: object
   884          status:
   885            type: object
   886    version: v1beta1
   887  status:
   888    acceptedNames:
   889      kind: ""
   890      plural: ""
   891    conditions: []
   892    storedVersions: []
   893  `