1/* 2Copyright 2018 The Kubernetes Authors. 3 4Licensed under the Apache License, Version 2.0 (the "License"); 5you may not use this file except in compliance with the License. 6You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10Unless required by applicable law or agreed to in writing, software 11distributed under the License is distributed on an "AS IS" BASIS, 12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13See the License for the specific language governing permissions and 14limitations under the License. 15*/ 16 17package fake 18 19import ( 20 "context" 21 "fmt" 22 "testing" 23 24 "k8s.io/apimachinery/pkg/api/equality" 25 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 27 "k8s.io/apimachinery/pkg/runtime" 28 "k8s.io/apimachinery/pkg/runtime/schema" 29 "k8s.io/apimachinery/pkg/types" 30 "k8s.io/apimachinery/pkg/util/diff" 31) 32 33const ( 34 testGroup = "testgroup" 35 testVersion = "testversion" 36 testResource = "testkinds" 37 testNamespace = "testns" 38 testName = "testname" 39 testKind = "TestKind" 40 testAPIVersion = "testgroup/testversion" 41) 42 43func 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 56func newUnstructuredWithSpec(spec map[string]interface{}) *unstructured.Unstructured { 57 u := newUnstructured(testAPIVersion, testKind, testNamespace, testName) 58 u.Object["spec"] = spec 59 return u 60} 61 62func 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(diff.ObjectGoPrintDiff(expected, get)) 83 } 84} 85 86func 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(diff.ObjectGoPrintDiff(expectedList, list)) 102 } 103} 104 105func 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(diff.ObjectGoPrintDiff(expectedObj, get)) 120 } 121} 122 123func 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(diff.ObjectGoPrintDiff(expected, listFirst.Items)) 148 } 149} 150 151func 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 "resourceVersion": "", 181 }, 182 }, 183 Items: []unstructured.Unstructured{ 184 *newUnstructured("group/version", "TheKind", "ns-foo", "name-bar"), 185 *newUnstructured("group/version", "TheKind", "ns-foo", "name-baz"), 186 *newUnstructured("group/version", "TheKind", "ns-foo", "name-foo"), 187 }, 188 } 189 if !equality.Semantic.DeepEqual(listFirst, expectedList) { 190 t.Fatal(diff.ObjectGoPrintDiff(expectedList, listFirst)) 191 } 192} 193 194type patchTestCase struct { 195 name string 196 object runtime.Object 197 patchType types.PatchType 198 patchBytes []byte 199 wantErrMsg string 200 expectedPatchedObject runtime.Object 201} 202 203func (tc *patchTestCase) runner(t *testing.T) { 204 client := NewSimpleDynamicClient(runtime.NewScheme(), tc.object) 205 resourceInterface := client.Resource(schema.GroupVersionResource{Group: testGroup, Version: testVersion, Resource: testResource}).Namespace(testNamespace) 206 207 got, recErr := resourceInterface.Patch(context.TODO(), testName, tc.patchType, tc.patchBytes, metav1.PatchOptions{}) 208 209 if err := tc.verifyErr(recErr); err != nil { 210 t.Error(err) 211 } 212 213 if err := tc.verifyResult(got); err != nil { 214 t.Error(err) 215 } 216 217} 218 219// verifyErr verifies that the given error returned from Patch is the error 220// expected by the test case. 221func (tc *patchTestCase) verifyErr(err error) error { 222 if tc.wantErrMsg != "" && err == nil { 223 return fmt.Errorf("want error, got nil") 224 } 225 226 if tc.wantErrMsg == "" && err != nil { 227 return fmt.Errorf("want no error, got %v", err) 228 } 229 230 if err != nil { 231 if want, got := tc.wantErrMsg, err.Error(); want != got { 232 return fmt.Errorf("incorrect error: want: %q got: %q", want, got) 233 } 234 } 235 return nil 236} 237 238func (tc *patchTestCase) verifyResult(result *unstructured.Unstructured) error { 239 if tc.expectedPatchedObject == nil && result == nil { 240 return nil 241 } 242 if !equality.Semantic.DeepEqual(result, tc.expectedPatchedObject) { 243 return fmt.Errorf("unexpected diff in received object: %s", diff.ObjectGoPrintDiff(tc.expectedPatchedObject, result)) 244 } 245 return nil 246} 247 248func TestPatch(t *testing.T) { 249 testCases := []patchTestCase{ 250 { 251 name: "jsonpatch fails with merge type", 252 object: newUnstructuredWithSpec(map[string]interface{}{"foo": "bar"}), 253 patchType: types.StrategicMergePatchType, 254 patchBytes: []byte(`[]`), 255 wantErrMsg: "invalid JSON document", 256 }, { 257 name: "jsonpatch works with empty patch", 258 object: newUnstructuredWithSpec(map[string]interface{}{"foo": "bar"}), 259 patchType: types.JSONPatchType, 260 // No-op 261 patchBytes: []byte(`[]`), 262 expectedPatchedObject: newUnstructuredWithSpec(map[string]interface{}{"foo": "bar"}), 263 }, { 264 name: "jsonpatch works with simple change patch", 265 object: newUnstructuredWithSpec(map[string]interface{}{"foo": "bar"}), 266 patchType: types.JSONPatchType, 267 // change spec.foo from bar to foobar 268 patchBytes: []byte(`[{"op": "replace", "path": "/spec/foo", "value": "foobar"}]`), 269 expectedPatchedObject: newUnstructuredWithSpec(map[string]interface{}{"foo": "foobar"}), 270 }, { 271 name: "jsonpatch works with simple addition", 272 object: newUnstructuredWithSpec(map[string]interface{}{"foo": "bar"}), 273 patchType: types.JSONPatchType, 274 // add spec.newvalue = dummy 275 patchBytes: []byte(`[{"op": "add", "path": "/spec/newvalue", "value": "dummy"}]`), 276 expectedPatchedObject: newUnstructuredWithSpec(map[string]interface{}{"foo": "bar", "newvalue": "dummy"}), 277 }, { 278 name: "jsonpatch works with simple deletion", 279 object: newUnstructuredWithSpec(map[string]interface{}{"foo": "bar", "toremove": "shouldnotbehere"}), 280 patchType: types.JSONPatchType, 281 // remove spec.newvalue = dummy 282 patchBytes: []byte(`[{"op": "remove", "path": "/spec/toremove"}]`), 283 expectedPatchedObject: newUnstructuredWithSpec(map[string]interface{}{"foo": "bar"}), 284 }, { 285 name: "strategic merge patch fails with JSONPatch", 286 object: newUnstructuredWithSpec(map[string]interface{}{"foo": "bar"}), 287 patchType: types.StrategicMergePatchType, 288 // add spec.newvalue = dummy 289 patchBytes: []byte(`[{"op": "add", "path": "/spec/newvalue", "value": "dummy"}]`), 290 wantErrMsg: "invalid JSON document", 291 }, { 292 name: "merge patch works with simple replacement", 293 object: newUnstructuredWithSpec(map[string]interface{}{"foo": "bar"}), 294 patchType: types.MergePatchType, 295 patchBytes: []byte(`{ "spec": { "foo": "baz" } }`), 296 expectedPatchedObject: newUnstructuredWithSpec(map[string]interface{}{"foo": "baz"}), 297 }, 298 // TODO: Add tests for strategic merge using v1.Pod for example to ensure the test cases 299 // demonstrate expected use cases. 300 } 301 302 for _, tc := range testCases { 303 t.Run(tc.name, tc.runner) 304 } 305} 306