...

Source file src/k8s.io/cli-runtime/pkg/resource/helper_test.go

Documentation: k8s.io/cli-runtime/pkg/resource

     1  /*
     2  Copyright 2014 The Kubernetes 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 resource
    18  
    19  import (
    20  	"bytes"
    21  	"errors"
    22  	"fmt"
    23  	"io"
    24  	"net/http"
    25  	"reflect"
    26  	"strings"
    27  	"testing"
    28  
    29  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    30  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    31  	"k8s.io/apimachinery/pkg/labels"
    32  	"k8s.io/apimachinery/pkg/runtime"
    33  	"k8s.io/client-go/rest/fake"
    34  
    35  	// TODO we need to remove this linkage and create our own scheme
    36  	corev1 "k8s.io/api/core/v1"
    37  	"k8s.io/client-go/kubernetes/scheme"
    38  )
    39  
    40  func objBody(obj runtime.Object) io.ReadCloser {
    41  	return io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(corev1Codec, obj))))
    42  }
    43  
    44  func header() http.Header {
    45  	header := http.Header{}
    46  	header.Set("Content-Type", runtime.ContentTypeJSON)
    47  	return header
    48  }
    49  
    50  // splitPath returns the segments for a URL path.
    51  func splitPath(path string) []string {
    52  	path = strings.Trim(path, "/")
    53  	if path == "" {
    54  		return []string{}
    55  	}
    56  	return strings.Split(path, "/")
    57  }
    58  
    59  // V1DeepEqualSafePodSpec returns a PodSpec which is ready to be used with apiequality.Semantic.DeepEqual
    60  func V1DeepEqualSafePodSpec() corev1.PodSpec {
    61  	grace := int64(30)
    62  	return corev1.PodSpec{
    63  		RestartPolicy:                 corev1.RestartPolicyAlways,
    64  		DNSPolicy:                     corev1.DNSClusterFirst,
    65  		TerminationGracePeriodSeconds: &grace,
    66  		SecurityContext:               &corev1.PodSecurityContext{},
    67  	}
    68  }
    69  
    70  func V1DeepEqualSafePodStatus() corev1.PodStatus {
    71  	return corev1.PodStatus{
    72  		Conditions: []corev1.PodCondition{
    73  			{
    74  				Status: corev1.ConditionTrue,
    75  				Type:   corev1.PodReady,
    76  			},
    77  		},
    78  	}
    79  }
    80  
    81  func TestHelperDelete(t *testing.T) {
    82  	tests := []struct {
    83  		name    string
    84  		Err     bool
    85  		Req     func(*http.Request) bool
    86  		Resp    *http.Response
    87  		HttpErr error
    88  	}{
    89  		{
    90  			name:    "test1",
    91  			HttpErr: errors.New("failure"),
    92  			Err:     true,
    93  		},
    94  		{
    95  			name: "test2",
    96  			Resp: &http.Response{
    97  				StatusCode: http.StatusNotFound,
    98  				Header:     header(),
    99  				Body:       objBody(&metav1.Status{Status: metav1.StatusFailure}),
   100  			},
   101  			Err: true,
   102  		},
   103  		{
   104  			name: "test3pkg/kubectl/genericclioptions/resource/helper_test.go",
   105  			Resp: &http.Response{
   106  				StatusCode: http.StatusOK,
   107  				Header:     header(),
   108  				Body:       objBody(&metav1.Status{Status: metav1.StatusSuccess}),
   109  			},
   110  			Req: func(req *http.Request) bool {
   111  				if req.Method != "DELETE" {
   112  					t.Errorf("unexpected method: %#v", req)
   113  					return false
   114  				}
   115  				parts := splitPath(req.URL.Path)
   116  				if len(parts) < 3 {
   117  					t.Errorf("expected URL path to have 3 parts: %s", req.URL.Path)
   118  					return false
   119  				}
   120  				if parts[1] != "bar" {
   121  					t.Errorf("url doesn't contain namespace: %#v", req)
   122  					return false
   123  				}
   124  				if parts[2] != "foo" {
   125  					t.Errorf("url doesn't contain name: %#v", req)
   126  					return false
   127  				}
   128  				return true
   129  			},
   130  		},
   131  	}
   132  	for _, tt := range tests {
   133  		t.Run(tt.name, func(t *testing.T) {
   134  			client := &fake.RESTClient{
   135  				NegotiatedSerializer: scheme.Codecs.WithoutConversion(),
   136  				Resp:                 tt.Resp,
   137  				Err:                  tt.HttpErr,
   138  			}
   139  			modifier := &Helper{
   140  				RESTClient:      client,
   141  				NamespaceScoped: true,
   142  			}
   143  			_, err := modifier.Delete("bar", "foo")
   144  			if (err != nil) != tt.Err {
   145  				t.Errorf("unexpected error: %t %v", tt.Err, err)
   146  			}
   147  			if err != nil {
   148  				return
   149  			}
   150  			if tt.Req != nil && !tt.Req(client.Req) {
   151  				t.Errorf("unexpected request: %#v", client.Req)
   152  			}
   153  		})
   154  	}
   155  }
   156  
   157  func TestHelperCreate(t *testing.T) {
   158  	expectPost := func(req *http.Request) bool {
   159  		if req.Method != "POST" {
   160  			t.Errorf("unexpected method: %#v", req)
   161  			return false
   162  		}
   163  		parts := splitPath(req.URL.Path)
   164  		if parts[1] != "bar" {
   165  			t.Errorf("url doesn't contain namespace: %#v", req)
   166  			return false
   167  		}
   168  		return true
   169  	}
   170  
   171  	tests := []struct {
   172  		name    string
   173  		Resp    *http.Response
   174  		HttpErr error
   175  		Modify  bool
   176  		Object  runtime.Object
   177  
   178  		ExpectObject runtime.Object
   179  		Err          bool
   180  		Req          func(*http.Request) bool
   181  	}{
   182  		{
   183  			name:    "test1",
   184  			HttpErr: errors.New("failure"),
   185  			Err:     true,
   186  		},
   187  		{
   188  			name: "test1",
   189  			Resp: &http.Response{
   190  				StatusCode: http.StatusNotFound,
   191  				Header:     header(),
   192  				Body:       objBody(&metav1.Status{Status: metav1.StatusFailure}),
   193  			},
   194  			Err: true,
   195  		},
   196  		{
   197  			name: "test1",
   198  			Resp: &http.Response{
   199  				StatusCode: http.StatusOK,
   200  				Header:     header(),
   201  				Body:       objBody(&metav1.Status{Status: metav1.StatusSuccess}),
   202  			},
   203  			Object:       &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
   204  			ExpectObject: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
   205  			Req:          expectPost,
   206  		},
   207  		{
   208  			name:         "test1",
   209  			Modify:       false,
   210  			Object:       &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "10"}},
   211  			ExpectObject: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "10"}},
   212  			Resp:         &http.Response{StatusCode: http.StatusOK, Header: header(), Body: objBody(&metav1.Status{Status: metav1.StatusSuccess})},
   213  			Req:          expectPost,
   214  		},
   215  		{
   216  			name:   "test1",
   217  			Modify: true,
   218  			Object: &corev1.Pod{
   219  				ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "10"},
   220  				Spec:       V1DeepEqualSafePodSpec(),
   221  			},
   222  			ExpectObject: &corev1.Pod{
   223  				ObjectMeta: metav1.ObjectMeta{Name: "foo"},
   224  				Spec:       V1DeepEqualSafePodSpec(),
   225  			},
   226  			Resp: &http.Response{StatusCode: http.StatusOK, Header: header(), Body: objBody(&metav1.Status{Status: metav1.StatusSuccess})},
   227  			Req:  expectPost,
   228  		},
   229  	}
   230  	for i, tt := range tests {
   231  		t.Run(tt.name, func(t *testing.T) {
   232  			client := &fake.RESTClient{
   233  				GroupVersion:         corev1GV,
   234  				NegotiatedSerializer: scheme.Codecs,
   235  				Resp:                 tt.Resp,
   236  				Err:                  tt.HttpErr,
   237  			}
   238  			modifier := &Helper{
   239  				RESTClient:      client,
   240  				NamespaceScoped: true,
   241  			}
   242  			_, err := modifier.Create("bar", tt.Modify, tt.Object)
   243  			if (err != nil) != tt.Err {
   244  				t.Errorf("%d: unexpected error: %t %v", i, tt.Err, err)
   245  			}
   246  			if err != nil {
   247  				return
   248  			}
   249  			if tt.Req != nil && !tt.Req(client.Req) {
   250  				t.Errorf("%d: unexpected request: %#v", i, client.Req)
   251  			}
   252  			body, err := io.ReadAll(client.Req.Body)
   253  			if err != nil {
   254  				t.Fatalf("%d: unexpected error: %#v", i, err)
   255  			}
   256  			t.Logf("got body: %s", string(body))
   257  			expect := []byte{}
   258  			if tt.ExpectObject != nil {
   259  				expect = []byte(runtime.EncodeOrDie(corev1Codec, tt.ExpectObject))
   260  			}
   261  			if !reflect.DeepEqual(expect, body) {
   262  				t.Errorf("%d: unexpected body: %s (expected %s)", i, string(body), string(expect))
   263  			}
   264  		})
   265  	}
   266  }
   267  
   268  func TestHelperGet(t *testing.T) {
   269  	tests := []struct {
   270  		name        string
   271  		subresource string
   272  		Err         bool
   273  		Req         func(*http.Request) bool
   274  		Resp        *http.Response
   275  		HttpErr     error
   276  	}{
   277  		{
   278  			name:    "test1",
   279  			HttpErr: errors.New("failure"),
   280  			Err:     true,
   281  		},
   282  		{
   283  			name: "test1",
   284  			Resp: &http.Response{
   285  				StatusCode: http.StatusNotFound,
   286  				Header:     header(),
   287  				Body:       objBody(&metav1.Status{Status: metav1.StatusFailure}),
   288  			},
   289  			Err: true,
   290  		},
   291  		{
   292  			name: "test1",
   293  			Resp: &http.Response{
   294  				StatusCode: http.StatusOK,
   295  				Header:     header(),
   296  				Body:       objBody(&corev1.Pod{TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Pod"}, ObjectMeta: metav1.ObjectMeta{Name: "foo"}}),
   297  			},
   298  			Req: func(req *http.Request) bool {
   299  				if req.Method != "GET" {
   300  					t.Errorf("unexpected method: %#v", req)
   301  					return false
   302  				}
   303  				parts := splitPath(req.URL.Path)
   304  				if parts[1] != "bar" {
   305  					t.Errorf("url doesn't contain namespace: %#v", req)
   306  					return false
   307  				}
   308  				if parts[2] != "foo" {
   309  					t.Errorf("url doesn't contain name: %#v", req)
   310  					return false
   311  				}
   312  				return true
   313  			},
   314  		},
   315  		{
   316  			name:        "test with subresource",
   317  			subresource: "status",
   318  			Resp: &http.Response{
   319  				StatusCode: http.StatusOK,
   320  				Header:     header(),
   321  				Body:       objBody(&corev1.Pod{TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Pod"}, ObjectMeta: metav1.ObjectMeta{Name: "foo"}}),
   322  			},
   323  			Req: func(req *http.Request) bool {
   324  				if req.Method != "GET" {
   325  					t.Errorf("unexpected method: %#v", req)
   326  					return false
   327  				}
   328  				parts := splitPath(req.URL.Path)
   329  				if parts[1] != "bar" {
   330  					t.Errorf("url doesn't contain namespace: %#v", req)
   331  					return false
   332  				}
   333  				if parts[2] != "foo" {
   334  					t.Errorf("url doesn't contain name: %#v", req)
   335  					return false
   336  				}
   337  				if parts[3] != "status" {
   338  					t.Errorf("url doesn't contain subresource: %#v", req)
   339  					return false
   340  				}
   341  				return true
   342  			},
   343  		},
   344  	}
   345  	for i, tt := range tests {
   346  		t.Run(tt.name, func(t *testing.T) {
   347  			client := &fake.RESTClient{
   348  				GroupVersion:         corev1GV,
   349  				NegotiatedSerializer: scheme.Codecs.WithoutConversion(),
   350  				Resp:                 tt.Resp,
   351  				Err:                  tt.HttpErr,
   352  			}
   353  			modifier := &Helper{
   354  				RESTClient:      client,
   355  				NamespaceScoped: true,
   356  				Subresource:     tt.subresource,
   357  			}
   358  			obj, err := modifier.Get("bar", "foo")
   359  
   360  			if (err != nil) != tt.Err {
   361  				t.Errorf("unexpected error: %d %t %v", i, tt.Err, err)
   362  			}
   363  			if err != nil {
   364  				return
   365  			}
   366  			if obj.(*corev1.Pod).Name != "foo" {
   367  				t.Errorf("unexpected object: %#v", obj)
   368  			}
   369  			if tt.Req != nil && !tt.Req(client.Req) {
   370  				t.Errorf("unexpected request: %#v", client.Req)
   371  			}
   372  		})
   373  	}
   374  }
   375  
   376  func TestHelperList(t *testing.T) {
   377  	tests := []struct {
   378  		name    string
   379  		Err     bool
   380  		Req     func(*http.Request) bool
   381  		Resp    *http.Response
   382  		HttpErr error
   383  	}{
   384  		{
   385  			name:    "test1",
   386  			HttpErr: errors.New("failure"),
   387  			Err:     true,
   388  		},
   389  		{
   390  			name: "test2",
   391  			Resp: &http.Response{
   392  				StatusCode: http.StatusNotFound,
   393  				Header:     header(),
   394  				Body:       objBody(&metav1.Status{Status: metav1.StatusFailure}),
   395  			},
   396  			Err: true,
   397  		},
   398  		{
   399  			name: "test3",
   400  			Resp: &http.Response{
   401  				StatusCode: http.StatusOK,
   402  				Header:     header(),
   403  				Body: objBody(&corev1.PodList{
   404  					Items: []corev1.Pod{{
   405  						ObjectMeta: metav1.ObjectMeta{Name: "foo"},
   406  					},
   407  					},
   408  				}),
   409  			},
   410  			Req: func(req *http.Request) bool {
   411  				if req.Method != "GET" {
   412  					t.Errorf("unexpected method: %#v", req)
   413  					return false
   414  				}
   415  				if req.URL.Path != "/namespaces/bar" {
   416  					t.Errorf("url doesn't contain name: %#v", req.URL)
   417  					return false
   418  				}
   419  				if req.URL.Query().Get(metav1.LabelSelectorQueryParam(corev1GV.String())) != labels.SelectorFromSet(labels.Set{"foo": "baz"}).String() {
   420  					t.Errorf("url doesn't contain query parameters: %#v", req.URL)
   421  					return false
   422  				}
   423  				return true
   424  			},
   425  		},
   426  		{
   427  			name: "test with",
   428  			Resp: &http.Response{
   429  				StatusCode: http.StatusOK,
   430  				Header:     header(),
   431  				Body: objBody(&corev1.PodList{
   432  					Items: []corev1.Pod{{
   433  						ObjectMeta: metav1.ObjectMeta{Name: "foo"},
   434  					},
   435  					},
   436  				}),
   437  			},
   438  			Req: func(req *http.Request) bool {
   439  				if req.Method != "GET" {
   440  					t.Errorf("unexpected method: %#v", req)
   441  					return false
   442  				}
   443  				if req.URL.Path != "/namespaces/bar" {
   444  					t.Errorf("url doesn't contain name: %#v", req.URL)
   445  					return false
   446  				}
   447  				if req.URL.Query().Get(metav1.LabelSelectorQueryParam(corev1GV.String())) != labels.SelectorFromSet(labels.Set{"foo": "baz"}).String() {
   448  					t.Errorf("url doesn't contain query parameters: %#v", req.URL)
   449  					return false
   450  				}
   451  				return true
   452  			},
   453  		},
   454  	}
   455  	for _, tt := range tests {
   456  		t.Run(tt.name, func(t *testing.T) {
   457  			client := &fake.RESTClient{
   458  				GroupVersion:         corev1GV,
   459  				NegotiatedSerializer: scheme.Codecs.WithoutConversion(),
   460  				Resp:                 tt.Resp,
   461  				Err:                  tt.HttpErr,
   462  			}
   463  			modifier := &Helper{
   464  				RESTClient:      client,
   465  				NamespaceScoped: true,
   466  			}
   467  			obj, err := modifier.List("bar", corev1GV.String(), &metav1.ListOptions{LabelSelector: "foo=baz"})
   468  			if (err != nil) != tt.Err {
   469  				t.Errorf("unexpected error: %t %v", tt.Err, err)
   470  			}
   471  			if err != nil {
   472  				return
   473  			}
   474  			if obj.(*corev1.PodList).Items[0].Name != "foo" {
   475  				t.Errorf("unexpected object: %#v", obj)
   476  			}
   477  			if tt.Req != nil && !tt.Req(client.Req) {
   478  				t.Errorf("unexpected request: %#v", client.Req)
   479  			}
   480  		})
   481  	}
   482  }
   483  
   484  func TestHelperListSelectorCombination(t *testing.T) {
   485  	tests := []struct {
   486  		Name          string
   487  		Err           bool
   488  		ErrMsg        string
   489  		FieldSelector string
   490  		LabelSelector string
   491  	}{
   492  		{
   493  			Name: "No selector",
   494  			Err:  false,
   495  		},
   496  		{
   497  			Name:          "Only Label Selector",
   498  			Err:           false,
   499  			LabelSelector: "foo=baz",
   500  		},
   501  		{
   502  			Name:          "Only Field Selector",
   503  			Err:           false,
   504  			FieldSelector: "xyz=zyx",
   505  		},
   506  		{
   507  			Name:          "Both Label and Field Selector",
   508  			Err:           false,
   509  			LabelSelector: "foo=baz",
   510  			FieldSelector: "xyz=zyx",
   511  		},
   512  	}
   513  
   514  	resp := &http.Response{
   515  		StatusCode: http.StatusOK,
   516  		Header:     header(),
   517  		Body: objBody(&corev1.PodList{
   518  			Items: []corev1.Pod{{
   519  				ObjectMeta: metav1.ObjectMeta{Name: "foo"},
   520  			},
   521  			},
   522  		}),
   523  	}
   524  	client := &fake.RESTClient{
   525  		NegotiatedSerializer: scheme.Codecs,
   526  		Resp:                 resp,
   527  		Err:                  nil,
   528  	}
   529  	modifier := &Helper{
   530  		RESTClient:      client,
   531  		NamespaceScoped: true,
   532  	}
   533  
   534  	for _, tt := range tests {
   535  		t.Run(tt.Name, func(t *testing.T) {
   536  			_, err := modifier.List("bar",
   537  				corev1GV.String(),
   538  				&metav1.ListOptions{LabelSelector: tt.LabelSelector, FieldSelector: tt.FieldSelector})
   539  			if tt.Err {
   540  				if err == nil {
   541  					t.Errorf("%q expected error: %q", tt.Name, tt.ErrMsg)
   542  				}
   543  				if err != nil && err.Error() != tt.ErrMsg {
   544  					t.Errorf("%q expected error: %q", tt.Name, tt.ErrMsg)
   545  				}
   546  			}
   547  		})
   548  	}
   549  }
   550  
   551  func TestHelperReplace(t *testing.T) {
   552  	expectPut := func(path string, req *http.Request) bool {
   553  		if req.Method != "PUT" {
   554  			t.Errorf("unexpected method: %#v", req)
   555  			return false
   556  		}
   557  		if req.URL.Path != path {
   558  			t.Errorf("unexpected url: %v", req.URL)
   559  			return false
   560  		}
   561  		return true
   562  	}
   563  
   564  	tests := []struct {
   565  		Name            string
   566  		Resp            *http.Response
   567  		HTTPClient      *http.Client
   568  		HttpErr         error
   569  		Overwrite       bool
   570  		Object          runtime.Object
   571  		Namespace       string
   572  		NamespaceScoped bool
   573  		Subresource     string
   574  
   575  		ExpectPath   string
   576  		ExpectObject runtime.Object
   577  		Err          bool
   578  		Req          func(string, *http.Request) bool
   579  	}{
   580  		{
   581  			Name:            "test1",
   582  			Namespace:       "bar",
   583  			NamespaceScoped: true,
   584  			HttpErr:         errors.New("failure"),
   585  			Err:             true,
   586  		},
   587  		{
   588  			Name:            "test2",
   589  			Namespace:       "bar",
   590  			NamespaceScoped: true,
   591  			Object:          &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
   592  			Resp: &http.Response{
   593  				StatusCode: http.StatusNotFound,
   594  				Header:     header(),
   595  				Body:       objBody(&metav1.Status{Status: metav1.StatusFailure}),
   596  			},
   597  			Err: true,
   598  		},
   599  		{
   600  			Name:            "test3",
   601  			Namespace:       "bar",
   602  			NamespaceScoped: true,
   603  			Object:          &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
   604  			ExpectPath:      "/namespaces/bar/foo",
   605  			ExpectObject:    &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
   606  			Resp: &http.Response{
   607  				StatusCode: http.StatusOK,
   608  				Header:     header(),
   609  				Body:       objBody(&metav1.Status{Status: metav1.StatusSuccess}),
   610  			},
   611  			Req: expectPut,
   612  		},
   613  		// namespace scoped resource
   614  		{
   615  			Name:            "test4",
   616  			Namespace:       "bar",
   617  			NamespaceScoped: true,
   618  			Object: &corev1.Pod{
   619  				ObjectMeta: metav1.ObjectMeta{Name: "foo"},
   620  				Spec:       V1DeepEqualSafePodSpec(),
   621  			},
   622  			ExpectPath: "/namespaces/bar/foo",
   623  			ExpectObject: &corev1.Pod{
   624  				ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "10"},
   625  				Spec:       V1DeepEqualSafePodSpec(),
   626  			},
   627  			Overwrite: true,
   628  			HTTPClient: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
   629  				if req.Method == "PUT" {
   630  					return &http.Response{StatusCode: http.StatusOK, Header: header(), Body: objBody(&metav1.Status{Status: metav1.StatusSuccess})}, nil
   631  				}
   632  				return &http.Response{StatusCode: http.StatusOK, Header: header(), Body: objBody(&corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "10"}})}, nil
   633  			}),
   634  			Req: expectPut,
   635  		},
   636  		// cluster scoped resource
   637  		{
   638  			Name: "test5",
   639  			Object: &corev1.Node{
   640  				ObjectMeta: metav1.ObjectMeta{Name: "foo"},
   641  			},
   642  			ExpectObject: &corev1.Node{
   643  				ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "10"},
   644  			},
   645  			Overwrite:  true,
   646  			ExpectPath: "/foo",
   647  			HTTPClient: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
   648  				if req.Method == "PUT" {
   649  					return &http.Response{StatusCode: http.StatusOK, Header: header(), Body: objBody(&metav1.Status{Status: metav1.StatusSuccess})}, nil
   650  				}
   651  				return &http.Response{StatusCode: http.StatusOK, Header: header(), Body: objBody(&corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "10"}})}, nil
   652  			}),
   653  			Req: expectPut,
   654  		},
   655  		{
   656  			Name:            "test6",
   657  			Namespace:       "bar",
   658  			NamespaceScoped: true,
   659  			Object:          &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "10"}},
   660  			ExpectPath:      "/namespaces/bar/foo",
   661  			ExpectObject:    &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "10"}},
   662  			Resp:            &http.Response{StatusCode: http.StatusOK, Header: header(), Body: objBody(&metav1.Status{Status: metav1.StatusSuccess})},
   663  			Req:             expectPut,
   664  		},
   665  		{
   666  			Name:            "test7 - with status subresource",
   667  			Namespace:       "bar",
   668  			NamespaceScoped: true,
   669  			Subresource:     "status",
   670  			Object: &corev1.Pod{
   671  				ObjectMeta: metav1.ObjectMeta{Name: "foo"},
   672  				Status:     V1DeepEqualSafePodStatus(),
   673  			},
   674  			ExpectPath: "/namespaces/bar/foo/status",
   675  			ExpectObject: &corev1.Pod{
   676  				ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "10"},
   677  				Status:     V1DeepEqualSafePodStatus(),
   678  			},
   679  			Overwrite: true,
   680  			HTTPClient: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
   681  				if req.Method == "PUT" {
   682  					return &http.Response{StatusCode: http.StatusOK, Header: header(), Body: objBody(&metav1.Status{Status: metav1.StatusSuccess})}, nil
   683  				}
   684  				return &http.Response{StatusCode: http.StatusOK, Header: header(), Body: objBody(&corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo", ResourceVersion: "10"}})}, nil
   685  			}),
   686  			Req: expectPut,
   687  		},
   688  	}
   689  	for _, tt := range tests {
   690  		t.Run(tt.Name, func(t *testing.T) {
   691  			client := &fake.RESTClient{
   692  				GroupVersion:         corev1GV,
   693  				NegotiatedSerializer: scheme.Codecs.WithoutConversion(),
   694  				Client:               tt.HTTPClient,
   695  				Resp:                 tt.Resp,
   696  				Err:                  tt.HttpErr,
   697  			}
   698  			modifier := &Helper{
   699  				RESTClient:      client,
   700  				NamespaceScoped: tt.NamespaceScoped,
   701  				Subresource:     tt.Subresource,
   702  			}
   703  			_, err := modifier.Replace(tt.Namespace, "foo", tt.Overwrite, tt.Object)
   704  			if (err != nil) != tt.Err {
   705  				t.Fatalf("unexpected error: %t %v", tt.Err, err)
   706  			}
   707  			if err != nil {
   708  				return
   709  			}
   710  			if tt.Req != nil && (client.Req == nil || !tt.Req(tt.ExpectPath, client.Req)) {
   711  				t.Fatalf("unexpected request: %#v", client.Req)
   712  			}
   713  			body, err := io.ReadAll(client.Req.Body)
   714  			if err != nil {
   715  				t.Fatalf("unexpected error: %#v", err)
   716  			}
   717  			expect := []byte{}
   718  			if tt.ExpectObject != nil {
   719  				expect = []byte(runtime.EncodeOrDie(corev1Codec, tt.ExpectObject))
   720  			}
   721  			if !reflect.DeepEqual(expect, body) {
   722  				t.Fatalf("unexpected body: %s", string(body))
   723  			}
   724  		})
   725  	}
   726  }
   727  
   728  func TestEnhanceListError(t *testing.T) {
   729  	podGVR := corev1.SchemeGroupVersion.WithResource(corev1.ResourcePods.String())
   730  	podSubject := podGVR.String()
   731  	tests := []struct {
   732  		name string
   733  		err  error
   734  		opts metav1.ListOptions
   735  		subj string
   736  
   737  		expectedErr     string
   738  		expectStatusErr bool
   739  	}{
   740  		{
   741  			name:            "leaves resource expired error as is",
   742  			err:             apierrors.NewResourceExpired("resourceversion too old"),
   743  			opts:            metav1.ListOptions{},
   744  			subj:            podSubject,
   745  			expectedErr:     "resourceversion too old",
   746  			expectStatusErr: true,
   747  		}, {
   748  			name:            "leaves unrecognized error as is",
   749  			err:             errors.New("something went wrong"),
   750  			opts:            metav1.ListOptions{},
   751  			subj:            podSubject,
   752  			expectedErr:     "something went wrong",
   753  			expectStatusErr: false,
   754  		}, {
   755  			name:            "bad request StatusError without selectors",
   756  			err:             apierrors.NewBadRequest("request is invalid"),
   757  			opts:            metav1.ListOptions{},
   758  			subj:            podSubject,
   759  			expectedErr:     "Unable to list \"/v1, Resource=pods\": request is invalid",
   760  			expectStatusErr: true,
   761  		}, {
   762  			name: "bad request StatusError with selectors",
   763  			err:  apierrors.NewBadRequest("request is invalid"),
   764  			opts: metav1.ListOptions{
   765  				LabelSelector: "a=b",
   766  				FieldSelector: ".spec.nodeName=foo",
   767  			},
   768  			subj:            podSubject,
   769  			expectedErr:     "Unable to find \"/v1, Resource=pods\" that match label selector \"a=b\", field selector \".spec.nodeName=foo\": request is invalid",
   770  			expectStatusErr: true,
   771  		}, {
   772  			name:            "not found without selectors",
   773  			err:             apierrors.NewNotFound(podGVR.GroupResource(), "foo"),
   774  			opts:            metav1.ListOptions{},
   775  			subj:            podSubject,
   776  			expectedErr:     "Unable to list \"/v1, Resource=pods\": pods \"foo\" not found",
   777  			expectStatusErr: true,
   778  		}, {
   779  			name: "not found StatusError with selectors",
   780  			err:  apierrors.NewNotFound(podGVR.GroupResource(), "foo"),
   781  			opts: metav1.ListOptions{
   782  				LabelSelector: "a=b",
   783  				FieldSelector: ".spec.nodeName=foo",
   784  			},
   785  			subj:            podSubject,
   786  			expectedErr:     "Unable to find \"/v1, Resource=pods\" that match label selector \"a=b\", field selector \".spec.nodeName=foo\": pods \"foo\" not found",
   787  			expectStatusErr: true,
   788  		}, {
   789  			name: "non StatusError without selectors",
   790  			err: fmt.Errorf("extra info: %w", apierrors.NewNotFound(podGVR.GroupResource(),
   791  				"foo")),
   792  			opts:            metav1.ListOptions{},
   793  			subj:            podSubject,
   794  			expectedErr:     "Unable to list \"/v1, Resource=pods\": extra info: pods \"foo\" not found",
   795  			expectStatusErr: false,
   796  		}, {
   797  			name: "non StatusError with selectors",
   798  			err:  fmt.Errorf("extra info: %w", apierrors.NewNotFound(podGVR.GroupResource(), "foo")),
   799  			opts: metav1.ListOptions{
   800  				LabelSelector: "a=b",
   801  				FieldSelector: ".spec.nodeName=foo",
   802  			},
   803  			subj: podSubject,
   804  			expectedErr: "Unable to find \"/v1, " +
   805  				"Resource=pods\" that match label selector \"a=b\", " +
   806  				"field selector \".spec.nodeName=foo\": extra info: pods \"foo\" not found",
   807  			expectStatusErr: false,
   808  		},
   809  	}
   810  	for _, tt := range tests {
   811  		t.Run(tt.name, func(t *testing.T) {
   812  			err := EnhanceListError(tt.err, tt.opts, tt.subj)
   813  			if err == nil {
   814  				t.Errorf("EnhanceListError did not return an error")
   815  			}
   816  			if err.Error() != tt.expectedErr {
   817  				t.Errorf("EnhanceListError() error = %q, expectedErr %q", err, tt.expectedErr)
   818  			}
   819  			if tt.expectStatusErr {
   820  				if _, ok := err.(*apierrors.StatusError); !ok {
   821  					t.Errorf("EnhanceListError incorrectly returned a non-StatusError: %v", err)
   822  				}
   823  			}
   824  		})
   825  	}
   826  }
   827  
   828  func TestFollowContinue(t *testing.T) {
   829  	var continueTokens []string
   830  	tests := []struct {
   831  		name        string
   832  		initialOpts *metav1.ListOptions
   833  		tokensSeen  []string
   834  		listFunc    func(metav1.ListOptions) (runtime.Object, error)
   835  
   836  		expectedTokens []string
   837  		wantErr        string
   838  	}{
   839  		{
   840  			name:        "updates list options with continue token until list finished",
   841  			initialOpts: &metav1.ListOptions{},
   842  			listFunc: func(options metav1.ListOptions) (runtime.Object, error) {
   843  				continueTokens = append(continueTokens, options.Continue)
   844  				obj := corev1.PodList{}
   845  				switch options.Continue {
   846  				case "":
   847  					metadataAccessor.SetContinue(&obj, "abc")
   848  				case "abc":
   849  					metadataAccessor.SetContinue(&obj, "def")
   850  				case "def":
   851  					metadataAccessor.SetKind(&obj, "ListComplete")
   852  				}
   853  				return &obj, nil
   854  			},
   855  			expectedTokens: []string{"", "abc", "def"},
   856  		},
   857  		{
   858  			name:        "stops looping if listFunc returns an error",
   859  			initialOpts: &metav1.ListOptions{},
   860  			listFunc: func(options metav1.ListOptions) (runtime.Object, error) {
   861  				continueTokens = append(continueTokens, options.Continue)
   862  				obj := corev1.PodList{}
   863  				switch options.Continue {
   864  				case "":
   865  					metadataAccessor.SetContinue(&obj, "abc")
   866  				case "abc":
   867  					return nil, fmt.Errorf("err from list func")
   868  				case "def":
   869  					metadataAccessor.SetKind(&obj, "ListComplete")
   870  				}
   871  				return &obj, nil
   872  			},
   873  			expectedTokens: []string{"", "abc"},
   874  			wantErr:        "err from list func",
   875  		},
   876  	}
   877  	for _, tt := range tests {
   878  		continueTokens = []string{}
   879  		t.Run(tt.name, func(t *testing.T) {
   880  			err := FollowContinue(tt.initialOpts, tt.listFunc)
   881  			if tt.wantErr != "" {
   882  				if err == nil {
   883  					t.Fatalf("FollowContinue was expected to return an error and did not")
   884  				} else if err.Error() != tt.wantErr {
   885  					t.Fatalf("wanted error %q, got %q", tt.wantErr, err.Error())
   886  				}
   887  			} else {
   888  				if err != nil {
   889  					t.Errorf("FollowContinue failed: %v", tt.wantErr)
   890  				}
   891  				if !reflect.DeepEqual(continueTokens, tt.expectedTokens) {
   892  					t.Errorf("got token list %q, wanted %q", continueTokens, tt.expectedTokens)
   893  				}
   894  			}
   895  		})
   896  	}
   897  }
   898  

View as plain text