k8s.io/client-go@v0.22.2/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  	"k8s.io/apimachinery/pkg/util/diff"
    32  )
    33  
    34  const (
    35  	testGroup      = "testgroup"
    36  	testVersion    = "testversion"
    37  	testResource   = "testkinds"
    38  	testNamespace  = "testns"
    39  	testName       = "testname"
    40  	testKind       = "TestKind"
    41  	testAPIVersion = "testgroup/testversion"
    42  )
    43  
    44  func newUnstructured(apiVersion, kind, namespace, name string) *unstructured.Unstructured {
    45  	return &unstructured.Unstructured{
    46  		Object: map[string]interface{}{
    47  			"apiVersion": apiVersion,
    48  			"kind":       kind,
    49  			"metadata": map[string]interface{}{
    50  				"namespace": namespace,
    51  				"name":      name,
    52  			},
    53  		},
    54  	}
    55  }
    56  
    57  func newUnstructuredWithSpec(spec map[string]interface{}) *unstructured.Unstructured {
    58  	u := newUnstructured(testAPIVersion, testKind, testNamespace, testName)
    59  	u.Object["spec"] = spec
    60  	return u
    61  }
    62  
    63  func TestGet(t *testing.T) {
    64  	scheme := runtime.NewScheme()
    65  
    66  	client := NewSimpleDynamicClient(scheme, newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"))
    67  	get, err := client.Resource(schema.GroupVersionResource{Group: "group", Version: "version", Resource: "thekinds"}).Namespace("ns-foo").Get(context.TODO(), "name-foo", metav1.GetOptions{})
    68  	if err != nil {
    69  		t.Fatal(err)
    70  	}
    71  
    72  	expected := &unstructured.Unstructured{
    73  		Object: map[string]interface{}{
    74  			"apiVersion": "group/version",
    75  			"kind":       "TheKind",
    76  			"metadata": map[string]interface{}{
    77  				"name":      "name-foo",
    78  				"namespace": "ns-foo",
    79  			},
    80  		},
    81  	}
    82  	if !equality.Semantic.DeepEqual(get, expected) {
    83  		t.Fatal(diff.ObjectGoPrintDiff(expected, get))
    84  	}
    85  }
    86  
    87  func TestListDecoding(t *testing.T) {
    88  	// this the duplication of logic from the real List API.  This will prove that our dynamic client actually returns the gvk
    89  	uncastObj, err := runtime.Decode(unstructured.UnstructuredJSONScheme, []byte(`{"apiVersion": "group/version", "kind": "TheKindList", "items":[]}`))
    90  	if err != nil {
    91  		t.Fatal(err)
    92  	}
    93  	list := uncastObj.(*unstructured.UnstructuredList)
    94  	expectedList := &unstructured.UnstructuredList{
    95  		Object: map[string]interface{}{
    96  			"apiVersion": "group/version",
    97  			"kind":       "TheKindList",
    98  		},
    99  		Items: []unstructured.Unstructured{},
   100  	}
   101  	if !equality.Semantic.DeepEqual(list, expectedList) {
   102  		t.Fatal(diff.ObjectGoPrintDiff(expectedList, list))
   103  	}
   104  }
   105  
   106  func TestGetDecoding(t *testing.T) {
   107  	// this the duplication of logic from the real Get API.  This will prove that our dynamic client actually returns the gvk
   108  	uncastObj, err := runtime.Decode(unstructured.UnstructuredJSONScheme, []byte(`{"apiVersion": "group/version", "kind": "TheKind"}`))
   109  	if err != nil {
   110  		t.Fatal(err)
   111  	}
   112  	get := uncastObj.(*unstructured.Unstructured)
   113  	expectedObj := &unstructured.Unstructured{
   114  		Object: map[string]interface{}{
   115  			"apiVersion": "group/version",
   116  			"kind":       "TheKind",
   117  		},
   118  	}
   119  	if !equality.Semantic.DeepEqual(get, expectedObj) {
   120  		t.Fatal(diff.ObjectGoPrintDiff(expectedObj, get))
   121  	}
   122  }
   123  
   124  func TestList(t *testing.T) {
   125  	scheme := runtime.NewScheme()
   126  
   127  	client := NewSimpleDynamicClientWithCustomListKinds(scheme,
   128  		map[schema.GroupVersionResource]string{
   129  			{Group: "group", Version: "version", Resource: "thekinds"}: "TheKindList",
   130  		},
   131  		newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
   132  		newUnstructured("group2/version", "TheKind", "ns-foo", "name2-foo"),
   133  		newUnstructured("group/version", "TheKind", "ns-foo", "name-bar"),
   134  		newUnstructured("group/version", "TheKind", "ns-foo", "name-baz"),
   135  		newUnstructured("group2/version", "TheKind", "ns-foo", "name2-baz"),
   136  	)
   137  	listFirst, err := client.Resource(schema.GroupVersionResource{Group: "group", Version: "version", Resource: "thekinds"}).List(context.TODO(), metav1.ListOptions{})
   138  	if err != nil {
   139  		t.Fatal(err)
   140  	}
   141  
   142  	expected := []unstructured.Unstructured{
   143  		*newUnstructured("group/version", "TheKind", "ns-foo", "name-bar"),
   144  		*newUnstructured("group/version", "TheKind", "ns-foo", "name-baz"),
   145  		*newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
   146  	}
   147  	if !equality.Semantic.DeepEqual(listFirst.Items, expected) {
   148  		t.Fatal(diff.ObjectGoPrintDiff(expected, listFirst.Items))
   149  	}
   150  }
   151  
   152  func Test_ListKind(t *testing.T) {
   153  	scheme := runtime.NewScheme()
   154  
   155  	client := NewSimpleDynamicClientWithCustomListKinds(scheme,
   156  		map[schema.GroupVersionResource]string{
   157  			{Group: "group", Version: "version", Resource: "thekinds"}: "TheKindList",
   158  		},
   159  		&unstructured.UnstructuredList{
   160  			Object: map[string]interface{}{
   161  				"apiVersion": "group/version",
   162  				"kind":       "TheKindList",
   163  			},
   164  			Items: []unstructured.Unstructured{
   165  				*newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"),
   166  				*newUnstructured("group/version", "TheKind", "ns-foo", "name-bar"),
   167  				*newUnstructured("group/version", "TheKind", "ns-foo", "name-baz"),
   168  			},
   169  		},
   170  	)
   171  	listFirst, err := client.Resource(schema.GroupVersionResource{Group: "group", Version: "version", Resource: "thekinds"}).List(context.TODO(), metav1.ListOptions{})
   172  	if err != nil {
   173  		t.Fatal(err)
   174  	}
   175  
   176  	expectedList := &unstructured.UnstructuredList{
   177  		Object: map[string]interface{}{
   178  			"apiVersion": "group/version",
   179  			"kind":       "TheKindList",
   180  			"metadata": map[string]interface{}{
   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(diff.ObjectGoPrintDiff(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", diff.ObjectGoPrintDiff(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.Items = append(expectedList.Items, u)
   337  
   338  	if diff := cmp.Diff(expectedList, list); diff != "" {
   339  		t.Fatal("unexpected diff (-want, +got): ", diff)
   340  	}
   341  }
   342  
   343  func TestListWithNoFixturesAndTypedScheme(t *testing.T) {
   344  	gvr := schema.GroupVersionResource{Group: testGroup, Version: testVersion, Resource: testResource}
   345  	gvk := gvr.GroupVersion().WithKind(testKind)
   346  
   347  	listGVK := gvk
   348  	listGVK.Kind += "List"
   349  
   350  	typedScheme := runtime.NewScheme()
   351  	typedScheme.AddKnownTypeWithName(gvk, &mockResource{})
   352  	typedScheme.AddKnownTypeWithName(listGVK, &mockResourceList{})
   353  
   354  	client := NewSimpleDynamicClient(typedScheme)
   355  	list, err := client.Resource(gvr).Namespace("namespace").List(context.Background(), metav1.ListOptions{})
   356  
   357  	if err != nil {
   358  		t.Error("error listing", err)
   359  	}
   360  
   361  	expectedList := &unstructured.UnstructuredList{}
   362  	expectedList.SetGroupVersionKind(listGVK)
   363  	expectedList.SetResourceVersion("") // by product of the fake setting resource version
   364  
   365  	if diff := cmp.Diff(expectedList, list); diff != "" {
   366  		t.Fatal("unexpected diff (-want, +got): ", diff)
   367  	}
   368  }
   369  
   370  // This test ensures list works when the dynamic client is seeded with an empty scheme and
   371  // unstructured typed fixtures
   372  func TestListWithNoScheme(t *testing.T) {
   373  	gvr := schema.GroupVersionResource{Group: testGroup, Version: testVersion, Resource: testResource}
   374  	gvk := gvr.GroupVersion().WithKind(testKind)
   375  
   376  	listGVK := gvk
   377  	listGVK.Kind += "List"
   378  
   379  	u := unstructured.Unstructured{}
   380  	u.SetGroupVersionKind(gvk)
   381  	u.SetName("name")
   382  	u.SetNamespace("namespace")
   383  
   384  	emptyScheme := runtime.NewScheme()
   385  
   386  	client := NewSimpleDynamicClient(emptyScheme, &u)
   387  	list, err := client.Resource(gvr).Namespace("namespace").List(context.Background(), metav1.ListOptions{})
   388  
   389  	if err != nil {
   390  		t.Error("error listing", err)
   391  	}
   392  
   393  	expectedList := &unstructured.UnstructuredList{}
   394  	expectedList.SetGroupVersionKind(listGVK)
   395  	expectedList.SetResourceVersion("") // by product of the fake setting resource version
   396  	expectedList.Items = append(expectedList.Items, u)
   397  
   398  	if diff := cmp.Diff(expectedList, list); diff != "" {
   399  		t.Fatal("unexpected diff (-want, +got): ", diff)
   400  	}
   401  }
   402  
   403  // This test ensures list works when the dynamic client is seeded with an empty scheme and
   404  // unstructured typed fixtures
   405  func TestListWithTypedFixtures(t *testing.T) {
   406  	gvr := schema.GroupVersionResource{Group: testGroup, Version: testVersion, Resource: testResource}
   407  	gvk := gvr.GroupVersion().WithKind(testKind)
   408  
   409  	listGVK := gvk
   410  	listGVK.Kind += "List"
   411  
   412  	r := mockResource{}
   413  	r.SetGroupVersionKind(gvk)
   414  	r.SetName("name")
   415  	r.SetNamespace("namespace")
   416  
   417  	u := unstructured.Unstructured{}
   418  	u.SetGroupVersionKind(r.GetObjectKind().GroupVersionKind())
   419  	u.SetName(r.GetName())
   420  	u.SetNamespace(r.GetNamespace())
   421  	// Needed see: https://github.com/kubernetes/kubernetes/issues/67610
   422  	unstructured.SetNestedField(u.Object, nil, "metadata", "creationTimestamp")
   423  
   424  	typedScheme := runtime.NewScheme()
   425  	typedScheme.AddKnownTypeWithName(gvk, &mockResource{})
   426  	typedScheme.AddKnownTypeWithName(listGVK, &mockResourceList{})
   427  
   428  	client := NewSimpleDynamicClient(typedScheme, &r)
   429  	list, err := client.Resource(gvr).Namespace("namespace").List(context.Background(), metav1.ListOptions{})
   430  
   431  	if err != nil {
   432  		t.Error("error listing", err)
   433  	}
   434  
   435  	expectedList := &unstructured.UnstructuredList{}
   436  	expectedList.SetGroupVersionKind(listGVK)
   437  	expectedList.SetResourceVersion("") // by product of the fake setting resource version
   438  	expectedList.Items = []unstructured.Unstructured{u}
   439  
   440  	if diff := cmp.Diff(expectedList, list); diff != "" {
   441  		t.Fatal("unexpected diff (-want, +got): ", diff)
   442  	}
   443  }
   444  
   445  type (
   446  	mockResource struct {
   447  		metav1.TypeMeta   `json:",inline"`
   448  		metav1.ObjectMeta `json:"metadata"`
   449  	}
   450  	mockResourceList struct {
   451  		metav1.TypeMeta `json:",inline"`
   452  		metav1.ListMeta `json:"metadata"`
   453  
   454  		Items []mockResource
   455  	}
   456  )
   457  
   458  func (l *mockResourceList) DeepCopyObject() runtime.Object {
   459  	o := *l
   460  	return &o
   461  }
   462  
   463  func (r *mockResource) DeepCopyObject() runtime.Object {
   464  	o := *r
   465  	return &o
   466  }
   467  
   468  var _ runtime.Object = (*mockResource)(nil)
   469  var _ runtime.Object = (*mockResourceList)(nil)