1/*
2Copyright 2015 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 testing
18
19import (
20	"fmt"
21	"math/rand"
22	"strconv"
23	"sync"
24	"testing"
25
26	"github.com/stretchr/testify/assert"
27
28	"k8s.io/apimachinery/pkg/api/errors"
29	"k8s.io/apimachinery/pkg/api/meta"
30	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
31	runtime "k8s.io/apimachinery/pkg/runtime"
32	"k8s.io/apimachinery/pkg/runtime/schema"
33	serializer "k8s.io/apimachinery/pkg/runtime/serializer"
34	"k8s.io/apimachinery/pkg/types"
35	"k8s.io/apimachinery/pkg/watch"
36)
37
38func getArbitraryResource(s schema.GroupVersionResource, name, namespace string) *unstructured.Unstructured {
39	return &unstructured.Unstructured{
40		Object: map[string]interface{}{
41			"kind":       s.Resource,
42			"apiVersion": s.Version,
43			"metadata": map[string]interface{}{
44				"name":            name,
45				"namespace":       namespace,
46				"generateName":    "test_generateName",
47				"uid":             "test_uid",
48				"resourceVersion": "test_resourceVersion",
49				"selfLink":        "test_selfLink",
50			},
51			"data": strconv.Itoa(rand.Int()),
52		},
53	}
54}
55
56func TestWatchCallNonNamespace(t *testing.T) {
57	testResource := schema.GroupVersionResource{Group: "", Version: "test_version", Resource: "test_kind"}
58	testObj := getArbitraryResource(testResource, "test_name", "test_namespace")
59	accessor, err := meta.Accessor(testObj)
60	if err != nil {
61		t.Fatalf("unexpected error: %v", err)
62	}
63	ns := accessor.GetNamespace()
64	scheme := runtime.NewScheme()
65	codecs := serializer.NewCodecFactory(scheme)
66	o := NewObjectTracker(scheme, codecs.UniversalDecoder())
67	watch, err := o.Watch(testResource, ns)
68	if err != nil {
69		t.Fatalf("test resource watch failed in %s: %v ", ns, err)
70	}
71	go func() {
72		err := o.Create(testResource, testObj, ns)
73		if err != nil {
74			t.Errorf("test resource creation failed: %v", err)
75		}
76	}()
77	out := <-watch.ResultChan()
78	assert.Equal(t, testObj, out.Object, "watched object mismatch")
79}
80
81func TestWatchCallAllNamespace(t *testing.T) {
82	testResource := schema.GroupVersionResource{Group: "", Version: "test_version", Resource: "test_kind"}
83	testObj := getArbitraryResource(testResource, "test_name", "test_namespace")
84	accessor, err := meta.Accessor(testObj)
85	if err != nil {
86		t.Fatalf("unexpected error: %v", err)
87	}
88	ns := accessor.GetNamespace()
89	scheme := runtime.NewScheme()
90	codecs := serializer.NewCodecFactory(scheme)
91	o := NewObjectTracker(scheme, codecs.UniversalDecoder())
92	w, err := o.Watch(testResource, "test_namespace")
93	if err != nil {
94		t.Fatalf("test resource watch failed in test_namespace: %v", err)
95	}
96	wAll, err := o.Watch(testResource, "")
97	if err != nil {
98		t.Fatalf("test resource watch failed in all namespaces: %v", err)
99	}
100	go func() {
101		err := o.Create(testResource, testObj, ns)
102		assert.NoError(t, err, "test resource creation failed")
103	}()
104	out := <-w.ResultChan()
105	outAll := <-wAll.ResultChan()
106	assert.Equal(t, watch.Added, out.Type, "watch event mismatch")
107	assert.Equal(t, watch.Added, outAll.Type, "watch event mismatch")
108	assert.Equal(t, testObj, out.Object, "watched created object mismatch")
109	assert.Equal(t, testObj, outAll.Object, "watched created object mismatch")
110	go func() {
111		err := o.Update(testResource, testObj, ns)
112		assert.NoError(t, err, "test resource updating failed")
113	}()
114	out = <-w.ResultChan()
115	outAll = <-wAll.ResultChan()
116	assert.Equal(t, watch.Modified, out.Type, "watch event mismatch")
117	assert.Equal(t, watch.Modified, outAll.Type, "watch event mismatch")
118	assert.Equal(t, testObj, out.Object, "watched updated object mismatch")
119	assert.Equal(t, testObj, outAll.Object, "watched updated object mismatch")
120	go func() {
121		err := o.Delete(testResource, "test_namespace", "test_name")
122		assert.NoError(t, err, "test resource deletion failed")
123	}()
124	out = <-w.ResultChan()
125	outAll = <-wAll.ResultChan()
126	assert.Equal(t, watch.Deleted, out.Type, "watch event mismatch")
127	assert.Equal(t, watch.Deleted, outAll.Type, "watch event mismatch")
128	assert.Equal(t, testObj, out.Object, "watched deleted object mismatch")
129	assert.Equal(t, testObj, outAll.Object, "watched deleted object mismatch")
130}
131
132func TestWatchCallMultipleInvocation(t *testing.T) {
133	cases := []struct {
134		name string
135		op   watch.EventType
136		ns   string
137	}{
138		{
139			"foo",
140			watch.Added,
141			"test_namespace",
142		},
143		{
144			"bar",
145			watch.Added,
146			"test_namespace",
147		},
148		{
149			"baz",
150			watch.Added,
151			"",
152		},
153		{
154			"bar",
155			watch.Modified,
156			"test_namespace",
157		},
158		{
159			"baz",
160			watch.Modified,
161			"",
162		},
163		{
164			"foo",
165			watch.Deleted,
166			"test_namespace",
167		},
168		{
169			"bar",
170			watch.Deleted,
171			"test_namespace",
172		},
173		{
174			"baz",
175			watch.Deleted,
176			"",
177		},
178	}
179
180	scheme := runtime.NewScheme()
181	codecs := serializer.NewCodecFactory(scheme)
182	testResource := schema.GroupVersionResource{Group: "", Version: "test_version", Resource: "test_kind"}
183
184	o := NewObjectTracker(scheme, codecs.UniversalDecoder())
185	watchNamespaces := []string{
186		"",
187		"",
188		"test_namespace",
189		"test_namespace",
190	}
191	var wg sync.WaitGroup
192	wg.Add(len(watchNamespaces))
193	for idx, watchNamespace := range watchNamespaces {
194		i := idx
195		watchNamespace := watchNamespace
196		w, err := o.Watch(testResource, watchNamespace)
197		if err != nil {
198			t.Fatalf("test resource watch failed in %s: %v", watchNamespace, err)
199		}
200		go func() {
201			assert.NoError(t, err, "watch invocation failed")
202			for _, c := range cases {
203				if watchNamespace == "" || c.ns == watchNamespace {
204					fmt.Printf("%#v %#v\n", c, i)
205					event := <-w.ResultChan()
206					accessor, err := meta.Accessor(event.Object)
207					if err != nil {
208						t.Errorf("unexpected error: %v", err)
209						break
210					}
211					assert.Equal(t, c.op, event.Type, "watch event mismatched")
212					assert.Equal(t, c.name, accessor.GetName(), "watched object mismatch")
213					assert.Equal(t, c.ns, accessor.GetNamespace(), "watched object mismatch")
214				}
215			}
216			wg.Done()
217		}()
218	}
219	for _, c := range cases {
220		switch c.op {
221		case watch.Added:
222			obj := getArbitraryResource(testResource, c.name, c.ns)
223			o.Create(testResource, obj, c.ns)
224		case watch.Modified:
225			obj := getArbitraryResource(testResource, c.name, c.ns)
226			o.Update(testResource, obj, c.ns)
227		case watch.Deleted:
228			o.Delete(testResource, c.ns, c.name)
229		}
230	}
231	wg.Wait()
232}
233
234func TestWatchAddAfterStop(t *testing.T) {
235	testResource := schema.GroupVersionResource{Group: "", Version: "test_version", Resource: "test_kind"}
236	testObj := getArbitraryResource(testResource, "test_name", "test_namespace")
237	accessor, err := meta.Accessor(testObj)
238	if err != nil {
239		t.Fatalf("unexpected error: %v", err)
240	}
241
242	ns := accessor.GetNamespace()
243	scheme := runtime.NewScheme()
244	codecs := serializer.NewCodecFactory(scheme)
245	o := NewObjectTracker(scheme, codecs.UniversalDecoder())
246	watch, err := o.Watch(testResource, ns)
247	if err != nil {
248		t.Errorf("watch creation failed: %v", err)
249	}
250
251	// When the watch is stopped it should ignore later events without panicking.
252	defer func() {
253		if r := recover(); r != nil {
254			t.Errorf("Watch panicked when it should have ignored create after stop: %v", r)
255		}
256	}()
257
258	watch.Stop()
259	err = o.Create(testResource, testObj, ns)
260	if err != nil {
261		t.Errorf("test resource creation failed: %v", err)
262	}
263}
264
265func TestPatchWithMissingObject(t *testing.T) {
266	nodesResource := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "nodes"}
267
268	scheme := runtime.NewScheme()
269	codecs := serializer.NewCodecFactory(scheme)
270	o := NewObjectTracker(scheme, codecs.UniversalDecoder())
271	reaction := ObjectReaction(o)
272	action := NewRootPatchSubresourceAction(nodesResource, "node-1", types.StrategicMergePatchType, []byte(`{}`))
273	handled, node, err := reaction(action)
274	assert.True(t, handled)
275	assert.Nil(t, node)
276	assert.EqualError(t, err, `nodes "node-1" not found`)
277}
278
279func TestGetWithExactMatch(t *testing.T) {
280	scheme := runtime.NewScheme()
281	codecs := serializer.NewCodecFactory(scheme)
282
283	constructObject := func(s schema.GroupVersionResource, name, namespace string) (*unstructured.Unstructured, schema.GroupVersionResource) {
284		obj := getArbitraryResource(s, name, namespace)
285		gvks, _, err := scheme.ObjectKinds(obj)
286		assert.NoError(t, err)
287		gvr, _ := meta.UnsafeGuessKindToResource(gvks[0])
288		return obj, gvr
289	}
290
291	var err error
292	// Object with empty namespace
293	o := NewObjectTracker(scheme, codecs.UniversalDecoder())
294	nodeResource := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "node"}
295	node, gvr := constructObject(nodeResource, "node", "")
296
297	assert.Nil(t, o.Add(node))
298
299	// Exact match
300	_, err = o.Get(gvr, "", "node")
301	assert.NoError(t, err)
302
303	// Unexpected namespace provided
304	_, err = o.Get(gvr, "ns", "node")
305	assert.Error(t, err)
306	errNotFound := errors.NewNotFound(gvr.GroupResource(), "node")
307	assert.EqualError(t, err, errNotFound.Error())
308
309	// Object with non-empty namespace
310	o = NewObjectTracker(scheme, codecs.UniversalDecoder())
311	podResource := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pod"}
312	pod, gvr := constructObject(podResource, "pod", "default")
313	assert.Nil(t, o.Add(pod))
314
315	// Exact match
316	_, err = o.Get(gvr, "default", "pod")
317	assert.NoError(t, err)
318
319	// Missing namespace
320	_, err = o.Get(gvr, "", "pod")
321	assert.Error(t, err)
322	errNotFound = errors.NewNotFound(gvr.GroupResource(), "pod")
323	assert.EqualError(t, err, errNotFound.Error())
324}
325
326func Test_resourceCovers(t *testing.T) {
327	type args struct {
328		resource string
329		action   Action
330	}
331	tests := []struct {
332		name string
333		args args
334		want bool
335	}{
336		{
337			args: args{
338				resource: "*",
339				action:   ActionImpl{},
340			},
341			want: true,
342		},
343		{
344			args: args{
345				resource: "serviceaccounts",
346				action:   ActionImpl{},
347			},
348			want: false,
349		},
350		{
351			args: args{
352				resource: "serviceaccounts",
353				action: ActionImpl{
354					Resource: schema.GroupVersionResource{
355						Resource: "serviceaccounts",
356					},
357				},
358			},
359			want: true,
360		},
361		{
362			args: args{
363				resource: "serviceaccounts/token",
364				action: ActionImpl{
365					Resource: schema.GroupVersionResource{},
366				},
367			},
368			want: false,
369		},
370		{
371			args: args{
372				resource: "serviceaccounts/token",
373				action: ActionImpl{
374					Resource: schema.GroupVersionResource{
375						Resource: "serviceaccounts",
376					},
377				},
378			},
379			want: false,
380		},
381		{
382			args: args{
383				resource: "serviceaccounts/token",
384				action: ActionImpl{
385					Resource:    schema.GroupVersionResource{},
386					Subresource: "token",
387				},
388			},
389			want: false,
390		},
391		{
392			args: args{
393				resource: "serviceaccounts/token",
394				action: ActionImpl{
395					Resource: schema.GroupVersionResource{
396						Resource: "serviceaccounts",
397					},
398					Subresource: "token",
399				},
400			},
401			want: true,
402		},
403	}
404	for _, tt := range tests {
405		t.Run(tt.name, func(t *testing.T) {
406			if got := resourceCovers(tt.args.resource, tt.args.action); got != tt.want {
407				t.Errorf("resourceCovers() = %v, want %v", got, tt.want)
408			}
409		})
410	}
411}
412