1
16
17 package pvprotection
18
19 import (
20 "context"
21 "errors"
22 "reflect"
23 "testing"
24 "time"
25
26 v1 "k8s.io/api/core/v1"
27 apierrors "k8s.io/apimachinery/pkg/api/errors"
28 "k8s.io/apimachinery/pkg/api/meta"
29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
30 "k8s.io/apimachinery/pkg/runtime"
31 "k8s.io/apimachinery/pkg/runtime/schema"
32 "k8s.io/apimachinery/pkg/util/dump"
33 "k8s.io/client-go/informers"
34 "k8s.io/client-go/kubernetes/fake"
35 clienttesting "k8s.io/client-go/testing"
36 "k8s.io/klog/v2/ktesting"
37 "k8s.io/kubernetes/pkg/controller"
38 volumeutil "k8s.io/kubernetes/pkg/volume/util"
39 )
40
41 const defaultPVName = "default-pv"
42
43 type reaction struct {
44 verb string
45 resource string
46 reactorfn clienttesting.ReactionFunc
47 }
48
49 func pv() *v1.PersistentVolume {
50 return &v1.PersistentVolume{
51 ObjectMeta: metav1.ObjectMeta{
52 Name: defaultPVName,
53 },
54 }
55 }
56
57 func boundPV() *v1.PersistentVolume {
58 return &v1.PersistentVolume{
59 ObjectMeta: metav1.ObjectMeta{
60 Name: defaultPVName,
61 },
62 Status: v1.PersistentVolumeStatus{
63 Phase: v1.VolumeBound,
64 },
65 }
66 }
67
68 func withProtectionFinalizer(pv *v1.PersistentVolume) *v1.PersistentVolume {
69 pv.Finalizers = append(pv.Finalizers, volumeutil.PVProtectionFinalizer)
70 return pv
71 }
72
73 func generateUpdateErrorFunc(t *testing.T, failures int) clienttesting.ReactionFunc {
74 i := 0
75 return func(action clienttesting.Action) (bool, runtime.Object, error) {
76 i++
77 if i <= failures {
78
79 update, ok := action.(clienttesting.UpdateAction)
80
81 if !ok {
82 t.Fatalf("Reactor got non-update action: %+v", action)
83 }
84 acc, _ := meta.Accessor(update.GetObject())
85 return true, nil, apierrors.NewForbidden(update.GetResource().GroupResource(), acc.GetName(), errors.New("Mock error"))
86 }
87
88 return false, nil, nil
89 }
90 }
91
92 func deleted(pv *v1.PersistentVolume) *v1.PersistentVolume {
93 pv.DeletionTimestamp = &metav1.Time{}
94 return pv
95 }
96
97 func TestPVProtectionController(t *testing.T) {
98 pvVer := schema.GroupVersionResource{
99 Group: v1.GroupName,
100 Version: "v1",
101 Resource: "persistentvolumes",
102 }
103 tests := []struct {
104 name string
105
106 initialObjects []runtime.Object
107
108 reactors []reaction
109
110
111 updatedPV *v1.PersistentVolume
112
113
114 expectedActions []clienttesting.Action
115 }{
116
117
118 {
119 name: "PV without finalizer -> finalizer is added",
120 updatedPV: pv(),
121 expectedActions: []clienttesting.Action{
122 clienttesting.NewUpdateAction(pvVer, "", withProtectionFinalizer(pv())),
123 },
124 },
125 {
126 name: "PVC with finalizer -> no action",
127 updatedPV: withProtectionFinalizer(pv()),
128 expectedActions: []clienttesting.Action{},
129 },
130 {
131 name: "saving PVC finalizer fails -> controller retries",
132 updatedPV: pv(),
133 reactors: []reaction{
134 {
135 verb: "update",
136 resource: "persistentvolumes",
137 reactorfn: generateUpdateErrorFunc(t, 2 ),
138 },
139 },
140 expectedActions: []clienttesting.Action{
141
142 clienttesting.NewUpdateAction(pvVer, "", withProtectionFinalizer(pv())),
143
144 clienttesting.NewUpdateAction(pvVer, "", withProtectionFinalizer(pv())),
145
146 clienttesting.NewUpdateAction(pvVer, "", withProtectionFinalizer(pv())),
147 },
148 },
149 {
150 name: "deleted PV with finalizer -> finalizer is removed",
151 updatedPV: deleted(withProtectionFinalizer(pv())),
152 expectedActions: []clienttesting.Action{
153 clienttesting.NewUpdateAction(pvVer, "", deleted(pv())),
154 },
155 },
156 {
157 name: "finalizer removal fails -> controller retries",
158 updatedPV: deleted(withProtectionFinalizer(pv())),
159 reactors: []reaction{
160 {
161 verb: "update",
162 resource: "persistentvolumes",
163 reactorfn: generateUpdateErrorFunc(t, 2 ),
164 },
165 },
166 expectedActions: []clienttesting.Action{
167
168 clienttesting.NewUpdateAction(pvVer, "", deleted(pv())),
169
170 clienttesting.NewUpdateAction(pvVer, "", deleted(pv())),
171
172 clienttesting.NewUpdateAction(pvVer, "", deleted(pv())),
173 },
174 },
175 {
176 name: "deleted PVC with finalizer + PV is bound -> finalizer is not removed",
177 updatedPV: deleted(withProtectionFinalizer(boundPV())),
178 expectedActions: []clienttesting.Action{},
179 },
180 }
181
182 for _, test := range tests {
183
184 objs := test.initialObjects
185 if test.updatedPV != nil {
186 objs = append(objs, test.updatedPV)
187 }
188
189 client := fake.NewSimpleClientset(objs...)
190
191
192 informers := informers.NewSharedInformerFactory(client, controller.NoResyncPeriodFunc())
193 pvInformer := informers.Core().V1().PersistentVolumes()
194
195
196
197 for _, obj := range objs {
198 switch obj.(type) {
199 case *v1.PersistentVolume:
200 pvInformer.Informer().GetStore().Add(obj)
201 default:
202 t.Fatalf("Unknown initialObject type: %+v", obj)
203 }
204 }
205
206
207 for _, reactor := range test.reactors {
208 client.Fake.PrependReactor(reactor.verb, reactor.resource, reactor.reactorfn)
209 }
210
211
212 logger, _ := ktesting.NewTestContext(t)
213 ctrl := NewPVProtectionController(logger, pvInformer, client)
214
215
216 if test.updatedPV != nil {
217 ctrl.pvAddedUpdated(logger, test.updatedPV)
218 }
219
220
221 timeout := time.Now().Add(10 * time.Second)
222 lastReportedActionCount := 0
223 for {
224 if time.Now().After(timeout) {
225 t.Errorf("Test %q: timed out", test.name)
226 break
227 }
228 if ctrl.queue.Len() > 0 {
229 logger.V(5).Info("Non-empty events queue, processing one", "test", test.name, "queueLength", ctrl.queue.Len())
230 ctrl.processNextWorkItem(context.TODO())
231 }
232 if ctrl.queue.Len() > 0 {
233
234 continue
235 }
236 currentActionCount := len(client.Actions())
237 if currentActionCount < len(test.expectedActions) {
238
239 if lastReportedActionCount < currentActionCount {
240 logger.V(5).Info("Waiting for the remaining actions", "test", test.name, "currentActionCount", currentActionCount, "expectedActionCount", len(test.expectedActions))
241 lastReportedActionCount = currentActionCount
242 }
243
244
245 time.Sleep(10 * time.Millisecond)
246 continue
247 }
248 break
249 }
250 actions := client.Actions()
251
252 if !reflect.DeepEqual(actions, test.expectedActions) {
253 t.Errorf("Test %q: action not expected\nExpected:\n%s\ngot:\n%s", test.name, dump.Pretty(test.expectedActions), dump.Pretty(actions))
254 }
255
256 }
257
258 }
259
View as plain text