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 dynamicinformer_test
18
19import (
20	"context"
21	"testing"
22	"time"
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/util/diff"
30	"k8s.io/client-go/dynamic/dynamicinformer"
31	"k8s.io/client-go/dynamic/fake"
32	"k8s.io/client-go/tools/cache"
33)
34
35func TestDynamicSharedInformerFactory(t *testing.T) {
36	scenarios := []struct {
37		name        string
38		existingObj *unstructured.Unstructured
39		gvr         schema.GroupVersionResource
40		ns          string
41		trigger     func(gvr schema.GroupVersionResource, ns string, fakeClient *fake.FakeDynamicClient, testObject *unstructured.Unstructured) *unstructured.Unstructured
42		handler     func(rcvCh chan<- *unstructured.Unstructured) *cache.ResourceEventHandlerFuncs
43	}{
44		// scenario 1
45		{
46			name: "scenario 1: test if adding an object triggers AddFunc",
47			ns:   "ns-foo",
48			gvr:  schema.GroupVersionResource{Group: "extensions", Version: "v1beta1", Resource: "deployments"},
49			trigger: func(gvr schema.GroupVersionResource, ns string, fakeClient *fake.FakeDynamicClient, _ *unstructured.Unstructured) *unstructured.Unstructured {
50				testObject := newUnstructured("extensions/v1beta1", "Deployment", "ns-foo", "name-foo")
51				createdObj, err := fakeClient.Resource(gvr).Namespace(ns).Create(context.TODO(), testObject, metav1.CreateOptions{})
52				if err != nil {
53					t.Error(err)
54				}
55				return createdObj
56			},
57			handler: func(rcvCh chan<- *unstructured.Unstructured) *cache.ResourceEventHandlerFuncs {
58				return &cache.ResourceEventHandlerFuncs{
59					AddFunc: func(obj interface{}) {
60						rcvCh <- obj.(*unstructured.Unstructured)
61					},
62				}
63			},
64		},
65
66		// scenario 2
67		{
68			name:        "scenario 2: tests if updating an object triggers UpdateFunc",
69			ns:          "ns-foo",
70			gvr:         schema.GroupVersionResource{Group: "extensions", Version: "v1beta1", Resource: "deployments"},
71			existingObj: newUnstructured("extensions/v1beta1", "Deployment", "ns-foo", "name-foo"),
72			trigger: func(gvr schema.GroupVersionResource, ns string, fakeClient *fake.FakeDynamicClient, testObject *unstructured.Unstructured) *unstructured.Unstructured {
73				testObject.Object["spec"] = "updatedName"
74				updatedObj, err := fakeClient.Resource(gvr).Namespace(ns).Update(context.TODO(), testObject, metav1.UpdateOptions{})
75				if err != nil {
76					t.Error(err)
77				}
78				return updatedObj
79			},
80			handler: func(rcvCh chan<- *unstructured.Unstructured) *cache.ResourceEventHandlerFuncs {
81				return &cache.ResourceEventHandlerFuncs{
82					UpdateFunc: func(old, updated interface{}) {
83						rcvCh <- updated.(*unstructured.Unstructured)
84					},
85				}
86			},
87		},
88
89		// scenario 3
90		{
91			name:        "scenario 3: test if deleting an object triggers DeleteFunc",
92			ns:          "ns-foo",
93			gvr:         schema.GroupVersionResource{Group: "extensions", Version: "v1beta1", Resource: "deployments"},
94			existingObj: newUnstructured("extensions/v1beta1", "Deployment", "ns-foo", "name-foo"),
95			trigger: func(gvr schema.GroupVersionResource, ns string, fakeClient *fake.FakeDynamicClient, testObject *unstructured.Unstructured) *unstructured.Unstructured {
96				err := fakeClient.Resource(gvr).Namespace(ns).Delete(context.TODO(), testObject.GetName(), metav1.DeleteOptions{})
97				if err != nil {
98					t.Error(err)
99				}
100				return testObject
101			},
102			handler: func(rcvCh chan<- *unstructured.Unstructured) *cache.ResourceEventHandlerFuncs {
103				return &cache.ResourceEventHandlerFuncs{
104					DeleteFunc: func(obj interface{}) {
105						rcvCh <- obj.(*unstructured.Unstructured)
106					},
107				}
108			},
109		},
110	}
111
112	for _, ts := range scenarios {
113		t.Run(ts.name, func(t *testing.T) {
114			// test data
115			timeout := time.Duration(3 * time.Second)
116			ctx, cancel := context.WithTimeout(context.Background(), timeout)
117			defer cancel()
118			scheme := runtime.NewScheme()
119			informerReciveObjectCh := make(chan *unstructured.Unstructured, 1)
120			objs := []runtime.Object{}
121			if ts.existingObj != nil {
122				objs = append(objs, ts.existingObj)
123			}
124			fakeClient := fake.NewSimpleDynamicClient(scheme, objs...)
125			target := dynamicinformer.NewDynamicSharedInformerFactory(fakeClient, 0)
126
127			// act
128			informerListerForGvr := target.ForResource(ts.gvr)
129			informerListerForGvr.Informer().AddEventHandler(ts.handler(informerReciveObjectCh))
130			target.Start(ctx.Done())
131			if synced := target.WaitForCacheSync(ctx.Done()); !synced[ts.gvr] {
132				t.Errorf("informer for %s hasn't synced", ts.gvr)
133			}
134
135			testObject := ts.trigger(ts.gvr, ts.ns, fakeClient, ts.existingObj)
136			select {
137			case objFromInformer := <-informerReciveObjectCh:
138				if !equality.Semantic.DeepEqual(testObject, objFromInformer) {
139					t.Fatalf("%v", diff.ObjectDiff(testObject, objFromInformer))
140				}
141			case <-ctx.Done():
142				t.Errorf("tested informer haven't received an object, waited %v", timeout)
143			}
144		})
145	}
146}
147
148func newUnstructured(apiVersion, kind, namespace, name string) *unstructured.Unstructured {
149	return &unstructured.Unstructured{
150		Object: map[string]interface{}{
151			"apiVersion": apiVersion,
152			"kind":       kind,
153			"metadata": map[string]interface{}{
154				"namespace": namespace,
155				"name":      name,
156			},
157			"spec": name,
158		},
159	}
160}
161