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