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