k8s.io/client-go@v0.31.1/dynamic/fake/simple_test.go (about)

     1  /*
     2  Copyright 2018 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 fake
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"testing"
    23  
    24  	"github.com/google/go-cmp/cmp"
    25  	"k8s.io/apimachinery/pkg/api/equality"
    26  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    28  	"k8s.io/apimachinery/pkg/runtime"
    29  	"k8s.io/apimachinery/pkg/runtime/schema"
    30  	"k8s.io/apimachinery/pkg/types"
    31  )
    32  
    33  const (
    34  	testGroup      = "testgroup"
    35  	testVersion    = "testversion"
    36  	testResource   = "testkinds"
    37  	testNamespace  = "testns"
    38  	testName       = "testname"
    39  	testKind       = "TestKind"
    40  	testAPIVersion = "testgroup/testversion"
    41  )
    42  
    43  func newUnstructured(apiVersion, kind, namespace, name string) *unstructured.Unstructured {
    44  	return &unstructured.Unstructured{
    45  		Object: map[string]interface{}{
    46  			"apiVersion": apiVersion,
    47  			"kind":       kind,
    48  			"metadata": map[string]interface{}{
    49  				"namespace": namespace,
    50  				"name":      name,
    51  			},
    52  		},
    53  	}
    54  }
    55  
    56  func newUnstructuredWithSpec(spec map[string]interface{}) *unstructured.Unstructured {
    57  	u := newUnstructured(testAPIVersion, testKind, testNamespace, testName)
    58  	u.Object["spec"] = spec
    59  	return u
    60  }
    61  
    62  func TestGet(t *testing.T) {
    63  	scheme := runtime.NewScheme()
    64  
    65  	client := NewSimpleDynamicClient(scheme, newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"))
    66  	get, err := client.Resource(schema.GroupVersionResource{Group: "group", Version: "version", Resource: "thekinds"}).Namespace("ns-foo").Get(context.TODO(), "name-foo", metav1.GetOptions{})
    67  	if err != nil {
    68  		t.Fatal(err)
    69  	}
    70  
    71  	expected := &unstructured.Unstructured{
    72  		Object: map[string]interface{}{
    73  			"apiVersion": "group/version",
    74  			"kind":       "TheKind",
    75  			"metadata": map[string]interface{}{
    76  				"name":      "name-foo",
    77  				"namespace": "ns-foo",
    78  			},
    79  		},
    80  	}
    81  	if !equality.Semantic.DeepEqual(get, expected) {
    82  		t.Fatal(cmp.Diff(expected, get))
    83  	}
    84  }
    85  
    86  func TestListDecoding(t *testing.T) {
    87  	// this the duplication of logic from the real List API.  This will prove that our dynamic client actually returns the gvk
    88  	uncastObj, err := runtime.Decode(unstructured.UnstructuredJSONScheme, []byte(`{"apiVersion": "group/version", "kind": "TheKindList", "items":[]}`))
    89  	if err != nil {
    90  		t.Fatal(err)
    91  	}
    92  	list := uncastObj.(*unstructured.UnstructuredList)
    93  	expectedList := &unstructured.UnstructuredList{
    94  		Object: map[string]interface{}{
    95  			"apiVersion": "group/version",
    96  			"kind":       "TheKindList",
    97  		},
    98  		Items: []unstructured.Unstructured{},
    99  	}
   100  	if !equality.Semantic.DeepEqual(list, expectedList) {
   101  		t.Fatal(cmp.Diff(expectedList, list))
   102  	}
   103  }
   104  
   105  func TestGetDecoding(t *testing.T) {
   106  	// this the duplication of logic from the real Get API.  This will prove that our dynamic client actually returns the gvk
   107  	uncastObj, err := runtime.Decode(unstructured.UnstructuredJSONScheme, []byte(`{"apiVersion": "group/version", "kind": "TheKind"}`))
   108  	if err != nil {
   109  		t.Fatal(err)
   110  	}
   111  	get := uncastObj.(*unstructured.Unstructured)
   112  	expectedObj := &unstructured.Unstructured{
   113  		Object: map[string]interface{}{
   114  			"apiVersion": "group/version",
   115  			"kind":       "TheKind",
   116  		},
   117  	}
   118  	if !equality.Semantic.DeepEqual(get, expectedObj) {
   119  		t.Fatal(cmp.Diff(expectedObj, get))
   120  	}
   121  }
   122  
   123  func TestList(t *testing.T) {
   124  	scheme := runtime.NewScheme()
   125  
   126  	client := NewSimpleDynamicClientWithCustomListKinds(scheme,
   127  		map[schema.GroupVersionResource]string{
   128  			{Group: "group", Version: "version", Resource: "thekinds"}: "TheKindList",
   129  		},
   130  		newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
   131  		newUnstructured("group2/version", "TheKind", "ns-foo", "name2-foo"),
   132  		newUnstructured("group/version", "TheKind", "ns-foo", "name-bar"),
   133  		newUnstructured("group/version", "TheKind", "ns-foo", "name-baz"),
   134  		newUnstructured("group2/version", "TheKind", "ns-foo", "name2-baz"),
   135  	)
   136  	listFirst, err := client.Resource(schema.GroupVersionResource{Group: "group", Version: "version", Resource: "thekinds"}).List(context.TODO(), metav1.ListOptions{})
   137  	if err != nil {
   138  		t.Fatal(err)
   139  	}
   140  
   141  	expected := []unstructured.Unstructured{
   142  		*newUnstructured("group/version", "TheKind", "ns-foo", "name-bar"),
   143  		*newUnstructured("group/version", "TheKind", "ns-foo", "name-baz"),
   144  		*newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
   145  	}
   146  	if !equality.Semantic.DeepEqual(listFirst.Items, expected) {
   147  		t.Fatal(cmp.Diff(expected, listFirst.Items))
   148  	}
   149  }
   150  
   151  func Test_ListKind(t *testing.T) {
   152  	scheme := runtime.NewScheme()
   153  
   154  	client := NewSimpleDynamicClientWithCustomListKinds(scheme,
   155  		map[schema.GroupVersionResource]string{
   156  			{Group: "group", Version: "version", Resource: "thekinds"}: "TheKindList",
   157  		},
   158  		&unstructured.UnstructuredList{
   159  			Object: map[string]interface{}{
   160  				"apiVersion": "group/version",
   161  				"kind":       "TheKindList",
   162  			},
   163  			Items: []unstructured.Unstructured{
   164  				*newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
   165  				*newUnstructured("group/version", "TheKind", "ns-foo", "name-bar"),
   166  				*newUnstructured("group/version", "TheKind", "ns-foo", "name-baz"),
   167  			},
   168  		},
   169  	)
   170  	listFirst, err := client.Resource(schema.GroupVersionResource{Group: "group", Version: "version", Resource: "thekinds"}).List(context.TODO(), metav1.ListOptions{})
   171  	if err != nil {
   172  		t.Fatal(err)
   173  	}
   174  
   175  	expectedList := &unstructured.UnstructuredList{
   176  		Object: map[string]interface{}{
   177  			"apiVersion": "group/version",
   178  			"kind":       "TheKindList",
   179  			"metadata": map[string]interface{}{
   180  				"continue":        "",
   181  				"resourceVersion": "",
   182  			},
   183  		},
   184  		Items: []unstructured.Unstructured{
   185  			*newUnstructured("group/version", "TheKind", "ns-foo", "name-bar"),
   186  			*newUnstructured("group/version", "TheKind", "ns-foo", "name-baz"),
   187  			*newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
   188  		},
   189  	}
   190  	if !equality.Semantic.DeepEqual(listFirst, expectedList) {
   191  		t.Fatal(cmp.Diff(expectedList, listFirst))
   192  	}
   193  }
   194  
   195  type patchTestCase struct {
   196  	name                  string
   197  	object                runtime.Object
   198  	patchType             types.PatchType
   199  	patchBytes            []byte
   200  	wantErrMsg            string
   201  	expectedPatchedObject runtime.Object
   202  }
   203  
   204  func (tc *patchTestCase) runner(t *testing.T) {
   205  	client := NewSimpleDynamicClient(runtime.NewScheme(), tc.object)
   206  	resourceInterface := client.Resource(schema.GroupVersionResource{Group: testGroup, Version: testVersion, Resource: testResource}).Namespace(testNamespace)
   207  
   208  	got, recErr := resourceInterface.Patch(context.TODO(), testName, tc.patchType, tc.patchBytes, metav1.PatchOptions{})
   209  
   210  	if err := tc.verifyErr(recErr); err != nil {
   211  		t.Error(err)
   212  	}
   213  
   214  	if err := tc.verifyResult(got); err != nil {
   215  		t.Error(err)
   216  	}
   217  
   218  }
   219  
   220  // verifyErr verifies that the given error returned from Patch is the error
   221  // expected by the test case.
   222  func (tc *patchTestCase) verifyErr(err error) error {
   223  	if tc.wantErrMsg != "" && err == nil {
   224  		return fmt.Errorf("want error, got nil")
   225  	}
   226  
   227  	if tc.wantErrMsg == "" && err != nil {
   228  		return fmt.Errorf("want no error, got %v", err)
   229  	}
   230  
   231  	if err != nil {
   232  		if want, got := tc.wantErrMsg, err.Error(); want != got {
   233  			return fmt.Errorf("incorrect error: want: %q got: %q", want, got)
   234  		}
   235  	}
   236  	return nil
   237  }
   238  
   239  func (tc *patchTestCase) verifyResult(result *unstructured.Unstructured) error {
   240  	if tc.expectedPatchedObject == nil && result == nil {
   241  		return nil
   242  	}
   243  	if !equality.Semantic.DeepEqual(result, tc.expectedPatchedObject) {
   244  		return fmt.Errorf("unexpected diff in received object: %s", cmp.Diff(tc.expectedPatchedObject, result))
   245  	}
   246  	return nil
   247  }
   248  
   249  func TestPatch(t *testing.T) {
   250  	testCases := []patchTestCase{
   251  		{
   252  			name:       "jsonpatch fails with merge type",
   253  			object:     newUnstructuredWithSpec(map[string]interface{}{"foo": "bar"}),
   254  			patchType:  types.StrategicMergePatchType,
   255  			patchBytes: []byte(`[]`),
   256  			wantErrMsg: "invalid JSON document",
   257  		}, {
   258  			name:      "jsonpatch works with empty patch",
   259  			object:    newUnstructuredWithSpec(map[string]interface{}{"foo": "bar"}),
   260  			patchType: types.JSONPatchType,
   261  			// No-op
   262  			patchBytes:            []byte(`[]`),
   263  			expectedPatchedObject: newUnstructuredWithSpec(map[string]interface{}{"foo": "bar"}),
   264  		}, {
   265  			name:      "jsonpatch works with simple change patch",
   266  			object:    newUnstructuredWithSpec(map[string]interface{}{"foo": "bar"}),
   267  			patchType: types.JSONPatchType,
   268  			// change spec.foo from bar to foobar
   269  			patchBytes:            []byte(`[{"op": "replace", "path": "/spec/foo", "value": "foobar"}]`),
   270  			expectedPatchedObject: newUnstructuredWithSpec(map[string]interface{}{"foo": "foobar"}),
   271  		}, {
   272  			name:      "jsonpatch works with simple addition",
   273  			object:    newUnstructuredWithSpec(map[string]interface{}{"foo": "bar"}),
   274  			patchType: types.JSONPatchType,
   275  			// add spec.newvalue = dummy
   276  			patchBytes:            []byte(`[{"op": "add", "path": "/spec/newvalue", "value": "dummy"}]`),
   277  			expectedPatchedObject: newUnstructuredWithSpec(map[string]interface{}{"foo": "bar", "newvalue": "dummy"}),
   278  		}, {
   279  			name:      "jsonpatch works with simple deletion",
   280  			object:    newUnstructuredWithSpec(map[string]interface{}{"foo": "bar", "toremove": "shouldnotbehere"}),
   281  			patchType: types.JSONPatchType,
   282  			// remove spec.newvalue = dummy
   283  			patchBytes:            []byte(`[{"op": "remove", "path": "/spec/toremove"}]`),
   284  			expectedPatchedObject: newUnstructuredWithSpec(map[string]interface{}{"foo": "bar"}),
   285  		}, {
   286  			name:      "strategic merge patch fails with JSONPatch",
   287  			object:    newUnstructuredWithSpec(map[string]interface{}{"foo": "bar"}),
   288  			patchType: types.StrategicMergePatchType,
   289  			// add spec.newvalue = dummy
   290  			patchBytes: []byte(`[{"op": "add", "path": "/spec/newvalue", "value": "dummy"}]`),
   291  			wantErrMsg: "invalid JSON document",
   292  		}, {
   293  			name:                  "merge patch works with simple replacement",
   294  			object:                newUnstructuredWithSpec(map[string]interface{}{"foo": "bar"}),
   295  			patchType:             types.MergePatchType,
   296  			patchBytes:            []byte(`{ "spec": { "foo": "baz" } }`),
   297  			expectedPatchedObject: newUnstructuredWithSpec(map[string]interface{}{"foo": "baz"}),
   298  		},
   299  		// TODO: Add tests for strategic merge using v1.Pod for example to ensure the test cases
   300  		// demonstrate expected use cases.
   301  	}
   302  
   303  	for _, tc := range testCases {
   304  		t.Run(tc.name, tc.runner)
   305  	}
   306  }
   307  
   308  // This test ensures list works when the fake dynamic client is seeded with a typed scheme and
   309  // unstructured type fixtures
   310  func TestListWithUnstructuredObjectsAndTypedScheme(t *testing.T) {
   311  	gvr := schema.GroupVersionResource{Group: testGroup, Version: testVersion, Resource: testResource}
   312  	gvk := gvr.GroupVersion().WithKind(testKind)
   313  
   314  	listGVK := gvk
   315  	listGVK.Kind += "List"
   316  
   317  	u := unstructured.Unstructured{}
   318  	u.SetGroupVersionKind(gvk)
   319  	u.SetName("name")
   320  	u.SetNamespace("namespace")
   321  
   322  	typedScheme := runtime.NewScheme()
   323  	typedScheme.AddKnownTypeWithName(gvk, &mockResource{})
   324  	typedScheme.AddKnownTypeWithName(listGVK, &mockResourceList{})
   325  
   326  	client := NewSimpleDynamicClient(typedScheme, &u)
   327  	list, err := client.Resource(gvr).Namespace("namespace").List(context.Background(), metav1.ListOptions{})
   328  
   329  	if err != nil {
   330  		t.Error("error listing", err)
   331  	}
   332  
   333  	expectedList := &unstructured.UnstructuredList{}
   334  	expectedList.SetGroupVersionKind(listGVK)
   335  	expectedList.SetResourceVersion("") // by product of the fake setting resource version
   336  	expectedList.SetContinue("")
   337  	expectedList.Items = append(expectedList.Items, u)
   338  
   339  	if diff := cmp.Diff(expectedList, list); diff != "" {
   340  		t.Fatal("unexpected diff (-want, +got): ", diff)
   341  	}
   342  }
   343  
   344  func TestListWithNoFixturesAndTypedScheme(t *testing.T) {
   345  	gvr := schema.GroupVersionResource{Group: testGroup, Version: testVersion, Resource: testResource}
   346  	gvk := gvr.GroupVersion().WithKind(testKind)
   347  
   348  	listGVK := gvk
   349  	listGVK.Kind += "List"
   350  
   351  	typedScheme := runtime.NewScheme()
   352  	typedScheme.AddKnownTypeWithName(gvk, &mockResource{})
   353  	typedScheme.AddKnownTypeWithName(listGVK, &mockResourceList{})
   354  
   355  	client := NewSimpleDynamicClient(typedScheme)
   356  	list, err := client.Resource(gvr).Namespace("namespace").List(context.Background(), metav1.ListOptions{})
   357  
   358  	if err != nil {
   359  		t.Error("error listing", err)
   360  	}
   361  
   362  	expectedList := &unstructured.UnstructuredList{}
   363  	expectedList.SetGroupVersionKind(listGVK)
   364  	expectedList.SetResourceVersion("") // by product of the fake setting resource version
   365  	expectedList.SetContinue("")
   366  
   367  	if diff := cmp.Diff(expectedList, list); diff != "" {
   368  		t.Fatal("unexpected diff (-want, +got): ", diff)
   369  	}
   370  }
   371  
   372  // This test ensures list works when the dynamic client is seeded with an empty scheme and
   373  // unstructured typed fixtures
   374  func TestListWithNoScheme(t *testing.T) {
   375  	gvr := schema.GroupVersionResource{Group: testGroup, Version: testVersion, Resource: testResource}
   376  	gvk := gvr.GroupVersion().WithKind(testKind)
   377  
   378  	listGVK := gvk
   379  	listGVK.Kind += "List"
   380  
   381  	u := unstructured.Unstructured{}
   382  	u.SetGroupVersionKind(gvk)
   383  	u.SetName("name")
   384  	u.SetNamespace("namespace")
   385  
   386  	emptyScheme := runtime.NewScheme()
   387  
   388  	client := NewSimpleDynamicClient(emptyScheme, &u)
   389  	list, err := client.Resource(gvr).Namespace("namespace").List(context.Background(), metav1.ListOptions{})
   390  
   391  	if err != nil {
   392  		t.Error("error listing", err)
   393  	}
   394  
   395  	expectedList := &unstructured.UnstructuredList{}
   396  	expectedList.SetGroupVersionKind(listGVK)
   397  	expectedList.SetResourceVersion("") // by product of the fake setting resource version
   398  	expectedList.SetContinue("")
   399  	expectedList.Items = append(expectedList.Items, u)
   400  
   401  	if diff := cmp.Diff(expectedList, list); diff != "" {
   402  		t.Fatal("unexpected diff (-want, +got): ", diff)
   403  	}
   404  }
   405  
   406  // This test ensures list works when the dynamic client is seeded with an empty scheme and
   407  // unstructured typed fixtures
   408  func TestListWithTypedFixtures(t *testing.T) {
   409  	gvr := schema.GroupVersionResource{Group: testGroup, Version: testVersion, Resource: testResource}
   410  	gvk := gvr.GroupVersion().WithKind(testKind)
   411  
   412  	listGVK := gvk
   413  	listGVK.Kind += "List"
   414  
   415  	r := mockResource{}
   416  	r.SetGroupVersionKind(gvk)
   417  	r.SetName("name")
   418  	r.SetNamespace("namespace")
   419  
   420  	u := unstructured.Unstructured{}
   421  	u.SetGroupVersionKind(r.GetObjectKind().GroupVersionKind())
   422  	u.SetName(r.GetName())
   423  	u.SetNamespace(r.GetNamespace())
   424  	// Needed see: https://github.com/kubernetes/kubernetes/issues/67610
   425  	unstructured.SetNestedField(u.Object, nil, "metadata", "creationTimestamp")
   426  
   427  	typedScheme := runtime.NewScheme()
   428  	typedScheme.AddKnownTypeWithName(gvk, &mockResource{})
   429  	typedScheme.AddKnownTypeWithName(listGVK, &mockResourceList{})
   430  
   431  	client := NewSimpleDynamicClient(typedScheme, &r)
   432  	list, err := client.Resource(gvr).Namespace("namespace").List(context.Background(), metav1.ListOptions{})
   433  
   434  	if err != nil {
   435  		t.Error("error listing", err)
   436  	}
   437  
   438  	expectedList := &unstructured.UnstructuredList{}
   439  	expectedList.SetGroupVersionKind(listGVK)
   440  	expectedList.SetResourceVersion("") // by product of the fake setting resource version
   441  	expectedList.SetContinue("")
   442  	expectedList.Items = []unstructured.Unstructured{u}
   443  
   444  	if diff := cmp.Diff(expectedList, list); diff != "" {
   445  		t.Fatal("unexpected diff (-want, +got): ", diff)
   446  	}
   447  }
   448  
   449  type (
   450  	mockResource struct {
   451  		metav1.TypeMeta   `json:",inline"`
   452  		metav1.ObjectMeta `json:"metadata"`
   453  	}
   454  	mockResourceList struct {
   455  		metav1.TypeMeta `json:",inline"`
   456  		metav1.ListMeta `json:"metadata"`
   457  
   458  		Items []mockResource
   459  	}
   460  )
   461  
   462  func (l *mockResourceList) DeepCopyObject() runtime.Object {
   463  	o := *l
   464  	return &o
   465  }
   466  
   467  func (r *mockResource) DeepCopyObject() runtime.Object {
   468  	o := *r
   469  	return &o
   470  }
   471  
   472  var _ runtime.Object = (*mockResource)(nil)
   473  var _ runtime.Object = (*mockResourceList)(nil)