1/* 2Copyright 2017 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 generic 18 19import ( 20 "encoding/json" 21 "reflect" 22 "testing" 23 24 "github.com/stretchr/testify/require" 25 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/util/diff" 31 "k8s.io/apiserver/pkg/admission" 32 "k8s.io/apiserver/pkg/apis/example" 33 examplev1 "k8s.io/apiserver/pkg/apis/example/v1" 34 example2v1 "k8s.io/apiserver/pkg/apis/example2/v1" 35) 36 37func initiateScheme(t *testing.T) *runtime.Scheme { 38 s := runtime.NewScheme() 39 require.NoError(t, example.AddToScheme(s)) 40 require.NoError(t, examplev1.AddToScheme(s)) 41 require.NoError(t, example2v1.AddToScheme(s)) 42 return s 43} 44 45func TestConvertToGVK(t *testing.T) { 46 scheme := initiateScheme(t) 47 o := admission.NewObjectInterfacesFromScheme(scheme) 48 table := map[string]struct { 49 obj runtime.Object 50 gvk schema.GroupVersionKind 51 expectedObj runtime.Object 52 }{ 53 "convert example#Pod to example/v1#Pod": { 54 obj: &example.Pod{ 55 ObjectMeta: metav1.ObjectMeta{ 56 Name: "pod1", 57 Labels: map[string]string{ 58 "key": "value", 59 }, 60 }, 61 Spec: example.PodSpec{ 62 RestartPolicy: example.RestartPolicy("never"), 63 }, 64 }, 65 gvk: examplev1.SchemeGroupVersion.WithKind("Pod"), 66 expectedObj: &examplev1.Pod{ 67 TypeMeta: metav1.TypeMeta{ 68 APIVersion: "example.apiserver.k8s.io/v1", 69 Kind: "Pod", 70 }, 71 ObjectMeta: metav1.ObjectMeta{ 72 Name: "pod1", 73 Labels: map[string]string{ 74 "key": "value", 75 }, 76 }, 77 Spec: examplev1.PodSpec{ 78 RestartPolicy: examplev1.RestartPolicy("never"), 79 }, 80 }, 81 }, 82 "convert example#replicaset to example2/v1#replicaset": { 83 obj: &example.ReplicaSet{ 84 ObjectMeta: metav1.ObjectMeta{ 85 Name: "rs1", 86 Labels: map[string]string{ 87 "key": "value", 88 }, 89 }, 90 Spec: example.ReplicaSetSpec{ 91 Replicas: 1, 92 }, 93 }, 94 gvk: example2v1.SchemeGroupVersion.WithKind("ReplicaSet"), 95 expectedObj: &example2v1.ReplicaSet{ 96 TypeMeta: metav1.TypeMeta{ 97 APIVersion: "example2.apiserver.k8s.io/v1", 98 Kind: "ReplicaSet", 99 }, 100 ObjectMeta: metav1.ObjectMeta{ 101 Name: "rs1", 102 Labels: map[string]string{ 103 "key": "value", 104 }, 105 }, 106 Spec: example2v1.ReplicaSetSpec{ 107 Replicas: func() *int32 { var i int32; i = 1; return &i }(), 108 }, 109 }, 110 }, 111 "no conversion for Unstructured object whose gvk matches the desired gvk": { 112 obj: &unstructured.Unstructured{ 113 Object: map[string]interface{}{ 114 "apiVersion": "mygroup.k8s.io/v1", 115 "kind": "Flunder", 116 "data": map[string]interface{}{ 117 "Key": "Value", 118 }, 119 }, 120 }, 121 gvk: schema.GroupVersionKind{Group: "mygroup.k8s.io", Version: "v1", Kind: "Flunder"}, 122 expectedObj: &unstructured.Unstructured{ 123 Object: map[string]interface{}{ 124 "apiVersion": "mygroup.k8s.io/v1", 125 "kind": "Flunder", 126 "data": map[string]interface{}{ 127 "Key": "Value", 128 }, 129 }, 130 }, 131 }, 132 } 133 134 for name, test := range table { 135 t.Run(name, func(t *testing.T) { 136 actual, err := ConvertToGVK(test.obj, test.gvk, o) 137 if err != nil { 138 t.Error(err) 139 } 140 if !reflect.DeepEqual(actual, test.expectedObj) { 141 t.Errorf("\nexpected:\n%#v\ngot:\n %#v\n", test.expectedObj, actual) 142 } 143 }) 144 } 145} 146 147// TestRuntimeSchemeConvert verifies that scheme.Convert(x, x, nil) for an unstructured x is a no-op. 148// This did not use to be like that and we had to wrap scheme.Convert before. 149func TestRuntimeSchemeConvert(t *testing.T) { 150 scheme := initiateScheme(t) 151 obj := &unstructured.Unstructured{ 152 Object: map[string]interface{}{ 153 "foo": "bar", 154 }, 155 } 156 clone := obj.DeepCopy() 157 158 if err := scheme.Convert(obj, obj, nil); err != nil { 159 t.Fatalf("unexpected convert error: %v", err) 160 } 161 if !reflect.DeepEqual(obj, clone) { 162 t.Errorf("unexpected mutation of self-converted Unstructured: obj=%#v, clone=%#v", obj, clone) 163 } 164} 165 166func TestConvertVersionedAttributes(t *testing.T) { 167 scheme := initiateScheme(t) 168 o := admission.NewObjectInterfacesFromScheme(scheme) 169 170 gvk := func(g, v, k string) schema.GroupVersionKind { 171 return schema.GroupVersionKind{g, v, k} 172 } 173 attrs := func(obj, oldObj runtime.Object) admission.Attributes { 174 return admission.NewAttributesRecord(obj, oldObj, schema.GroupVersionKind{}, "", "", schema.GroupVersionResource{}, "", "", nil, false, nil) 175 } 176 u := func(data string) *unstructured.Unstructured { 177 t.Helper() 178 m := map[string]interface{}{} 179 if err := json.Unmarshal([]byte(data), &m); err != nil { 180 t.Fatal(err) 181 } 182 return &unstructured.Unstructured{Object: m} 183 } 184 testcases := []struct { 185 Name string 186 Attrs *VersionedAttributes 187 GVK schema.GroupVersionKind 188 ExpectedAttrs *VersionedAttributes 189 }{ 190 { 191 Name: "noop", 192 Attrs: &VersionedAttributes{ 193 Attributes: attrs( 194 &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "newpod"}}, 195 &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "oldpod"}}, 196 ), 197 VersionedObject: &examplev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "newpodversioned"}}, 198 VersionedOldObject: &examplev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "oldpodversioned"}}, 199 VersionedKind: examplev1.SchemeGroupVersion.WithKind("Pod"), 200 Dirty: true, 201 }, 202 GVK: examplev1.SchemeGroupVersion.WithKind("Pod"), 203 ExpectedAttrs: &VersionedAttributes{ 204 Attributes: attrs( 205 &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "newpod"}}, 206 &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "oldpod"}}, 207 ), 208 VersionedObject: &examplev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "newpodversioned"}}, 209 VersionedOldObject: &examplev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "oldpodversioned"}}, 210 VersionedKind: examplev1.SchemeGroupVersion.WithKind("Pod"), 211 Dirty: true, 212 }, 213 }, 214 { 215 Name: "clean, typed", 216 Attrs: &VersionedAttributes{ 217 Attributes: attrs( 218 &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "newpod"}}, 219 &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "oldpod"}}, 220 ), 221 VersionedObject: &examplev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "newpodversioned"}}, 222 VersionedOldObject: &examplev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "oldpodversioned"}}, 223 VersionedKind: gvk("g", "v", "k"), 224 }, 225 GVK: examplev1.SchemeGroupVersion.WithKind("Pod"), 226 ExpectedAttrs: &VersionedAttributes{ 227 Attributes: attrs( 228 &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "newpod"}}, 229 &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "oldpod"}}, 230 ), 231 // name gets overwritten from converted attributes, type gets set explicitly 232 VersionedObject: &examplev1.Pod{TypeMeta: metav1.TypeMeta{APIVersion: "example.apiserver.k8s.io/v1", Kind: "Pod"}, ObjectMeta: metav1.ObjectMeta{Name: "newpod"}}, 233 VersionedOldObject: &examplev1.Pod{TypeMeta: metav1.TypeMeta{APIVersion: "example.apiserver.k8s.io/v1", Kind: "Pod"}, ObjectMeta: metav1.ObjectMeta{Name: "oldpod"}}, 234 VersionedKind: examplev1.SchemeGroupVersion.WithKind("Pod"), 235 }, 236 }, 237 { 238 Name: "clean, unstructured", 239 Attrs: &VersionedAttributes{ 240 Attributes: attrs( 241 u(`{"apiVersion": "mygroup.k8s.io/v1","kind": "Flunder","metadata":{"name":"newobj"}}`), 242 u(`{"apiVersion": "mygroup.k8s.io/v1","kind": "Flunder","metadata":{"name":"oldobj"}}`), 243 ), 244 VersionedObject: u(`{"apiVersion": "mygroup.k8s.io/v1","kind": "Flunder","metadata":{"name":"newobjversioned"}}`), 245 VersionedOldObject: u(`{"apiVersion": "mygroup.k8s.io/v1","kind": "Flunder","metadata":{"name":"oldobjversioned"}}`), 246 VersionedKind: gvk("g", "v", "k"), // claim a different current version to trigger conversion 247 }, 248 GVK: gvk("mygroup.k8s.io", "v1", "Flunder"), 249 ExpectedAttrs: &VersionedAttributes{ 250 Attributes: attrs( 251 u(`{"apiVersion": "mygroup.k8s.io/v1","kind": "Flunder","metadata":{"name":"newobj"}}`), 252 u(`{"apiVersion": "mygroup.k8s.io/v1","kind": "Flunder","metadata":{"name":"oldobj"}}`), 253 ), 254 VersionedObject: u(`{"apiVersion": "mygroup.k8s.io/v1","kind": "Flunder","metadata":{"name":"newobj"}}`), 255 VersionedOldObject: u(`{"apiVersion": "mygroup.k8s.io/v1","kind": "Flunder","metadata":{"name":"oldobj"}}`), 256 VersionedKind: gvk("mygroup.k8s.io", "v1", "Flunder"), 257 }, 258 }, 259 { 260 Name: "dirty, typed", 261 Attrs: &VersionedAttributes{ 262 Attributes: attrs( 263 &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "newpod"}}, 264 &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "oldpod"}}, 265 ), 266 VersionedObject: &examplev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "newpodversioned"}}, 267 VersionedOldObject: &examplev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "oldpodversioned"}}, 268 VersionedKind: gvk("g", "v", "k"), // claim a different current version to trigger conversion 269 Dirty: true, 270 }, 271 GVK: examplev1.SchemeGroupVersion.WithKind("Pod"), 272 ExpectedAttrs: &VersionedAttributes{ 273 Attributes: attrs( 274 &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "newpodversioned"}}, 275 &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "oldpod"}}, 276 ), 277 // new name gets preserved from versioned object, type gets set explicitly 278 VersionedObject: &examplev1.Pod{TypeMeta: metav1.TypeMeta{APIVersion: "example.apiserver.k8s.io/v1", Kind: "Pod"}, ObjectMeta: metav1.ObjectMeta{Name: "newpodversioned"}}, 279 // old name gets overwritten from converted attributes, type gets set explicitly 280 VersionedOldObject: &examplev1.Pod{TypeMeta: metav1.TypeMeta{APIVersion: "example.apiserver.k8s.io/v1", Kind: "Pod"}, ObjectMeta: metav1.ObjectMeta{Name: "oldpod"}}, 281 VersionedKind: examplev1.SchemeGroupVersion.WithKind("Pod"), 282 Dirty: false, 283 }, 284 }, 285 { 286 Name: "dirty, unstructured", 287 Attrs: &VersionedAttributes{ 288 Attributes: attrs( 289 u(`{"apiVersion": "mygroup.k8s.io/v1","kind": "Flunder","metadata":{"name":"newobj"}}`), 290 u(`{"apiVersion": "mygroup.k8s.io/v1","kind": "Flunder","metadata":{"name":"oldobj"}}`), 291 ), 292 VersionedObject: u(`{"apiVersion": "mygroup.k8s.io/v1","kind": "Flunder","metadata":{"name":"newobjversioned"}}`), 293 VersionedOldObject: u(`{"apiVersion": "mygroup.k8s.io/v1","kind": "Flunder","metadata":{"name":"oldobjversioned"}}`), 294 VersionedKind: gvk("g", "v", "k"), // claim a different current version to trigger conversion 295 Dirty: true, 296 }, 297 GVK: gvk("mygroup.k8s.io", "v1", "Flunder"), 298 ExpectedAttrs: &VersionedAttributes{ 299 Attributes: attrs( 300 u(`{"apiVersion": "mygroup.k8s.io/v1","kind": "Flunder","metadata":{"name":"newobjversioned"}}`), 301 u(`{"apiVersion": "mygroup.k8s.io/v1","kind": "Flunder","metadata":{"name":"oldobj"}}`), 302 ), 303 // new name gets preserved from versioned object, type gets set explicitly 304 VersionedObject: u(`{"apiVersion": "mygroup.k8s.io/v1","kind": "Flunder","metadata":{"name":"newobjversioned"}}`), 305 // old name gets overwritten from converted attributes, type gets set explicitly 306 VersionedOldObject: u(`{"apiVersion": "mygroup.k8s.io/v1","kind": "Flunder","metadata":{"name":"oldobj"}}`), 307 VersionedKind: gvk("mygroup.k8s.io", "v1", "Flunder"), 308 Dirty: false, 309 }, 310 }, 311 { 312 Name: "nil old object", 313 Attrs: &VersionedAttributes{ 314 Attributes: attrs( 315 &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "newpod"}}, 316 nil, 317 ), 318 VersionedObject: &examplev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "newpodversioned"}}, 319 VersionedOldObject: nil, 320 VersionedKind: gvk("g", "v", "k"), // claim a different current version to trigger conversion 321 Dirty: true, 322 }, 323 GVK: examplev1.SchemeGroupVersion.WithKind("Pod"), 324 ExpectedAttrs: &VersionedAttributes{ 325 Attributes: attrs( 326 &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "newpodversioned"}}, 327 nil, 328 ), 329 // new name gets preserved from versioned object, type gets set explicitly 330 VersionedObject: &examplev1.Pod{TypeMeta: metav1.TypeMeta{APIVersion: "example.apiserver.k8s.io/v1", Kind: "Pod"}, ObjectMeta: metav1.ObjectMeta{Name: "newpodversioned"}}, 331 VersionedOldObject: nil, 332 VersionedKind: examplev1.SchemeGroupVersion.WithKind("Pod"), 333 Dirty: false, 334 }, 335 }, 336 } 337 338 for _, tc := range testcases { 339 t.Run(tc.Name, func(t *testing.T) { 340 err := ConvertVersionedAttributes(tc.Attrs, tc.GVK, o) 341 if err != nil { 342 t.Fatal(err) 343 } 344 if e, a := tc.ExpectedAttrs.Attributes.GetObject(), tc.Attrs.Attributes.GetObject(); !reflect.DeepEqual(e, a) { 345 t.Errorf("unexpected diff:\n%s", diff.ObjectReflectDiff(e, a)) 346 } 347 if e, a := tc.ExpectedAttrs.Attributes.GetOldObject(), tc.Attrs.Attributes.GetOldObject(); !reflect.DeepEqual(e, a) { 348 t.Errorf("unexpected diff:\n%s", diff.ObjectReflectDiff(e, a)) 349 } 350 if e, a := tc.ExpectedAttrs.VersionedKind, tc.Attrs.VersionedKind; !reflect.DeepEqual(e, a) { 351 t.Errorf("unexpected diff:\n%s", diff.ObjectReflectDiff(e, a)) 352 } 353 if e, a := tc.ExpectedAttrs.VersionedObject, tc.Attrs.VersionedObject; !reflect.DeepEqual(e, a) { 354 t.Errorf("unexpected diff:\n%s", diff.ObjectReflectDiff(e, a)) 355 } 356 if e, a := tc.ExpectedAttrs.VersionedOldObject, tc.Attrs.VersionedOldObject; !reflect.DeepEqual(e, a) { 357 t.Errorf("unexpected diff:\n%s", diff.ObjectReflectDiff(e, a)) 358 } 359 if e, a := tc.ExpectedAttrs.Dirty, tc.Attrs.Dirty; !reflect.DeepEqual(e, a) { 360 t.Errorf("unexpected diff:\n%s", diff.ObjectReflectDiff(e, a)) 361 } 362 }) 363 } 364} 365