k8s.io/kubernetes@v1.29.3/pkg/controller/volume/pvprotection/pv_protection_controller_test.go (about) 1 /* 2 Copyright 2018 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 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 // Update fails 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 // Update succeeds 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 // Object to insert into fake kubeclient before the test starts. 106 initialObjects []runtime.Object 107 // Optional client reactors. 108 reactors []reaction 109 // PV event to simulate. This PV will be automatically added to 110 // initialObjects. 111 updatedPV *v1.PersistentVolume 112 // List of expected kubeclient actions that should happen during the 113 // test. 114 expectedActions []clienttesting.Action 115 }{ 116 // PV events 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 /* update fails twice*/), 138 }, 139 }, 140 expectedActions: []clienttesting.Action{ 141 // This fails 142 clienttesting.NewUpdateAction(pvVer, "", withProtectionFinalizer(pv())), 143 // This fails too 144 clienttesting.NewUpdateAction(pvVer, "", withProtectionFinalizer(pv())), 145 // This succeeds 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 /* update fails twice*/), 164 }, 165 }, 166 expectedActions: []clienttesting.Action{ 167 // Fails 168 clienttesting.NewUpdateAction(pvVer, "", deleted(pv())), 169 // Fails too 170 clienttesting.NewUpdateAction(pvVer, "", deleted(pv())), 171 // Succeeds 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 // Create client with initial data 184 objs := test.initialObjects 185 if test.updatedPV != nil { 186 objs = append(objs, test.updatedPV) 187 } 188 189 client := fake.NewSimpleClientset(objs...) 190 191 // Create informers 192 informers := informers.NewSharedInformerFactory(client, controller.NoResyncPeriodFunc()) 193 pvInformer := informers.Core().V1().PersistentVolumes() 194 195 // Populate the informers with initial objects so the controller can 196 // Get() it. 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 // Add reactor to inject test errors. 207 for _, reactor := range test.reactors { 208 client.Fake.PrependReactor(reactor.verb, reactor.resource, reactor.reactorfn) 209 } 210 211 // Create the controller 212 logger, _ := ktesting.NewTestContext(t) 213 ctrl := NewPVProtectionController(logger, pvInformer, client) 214 215 // Start the test by simulating an event 216 if test.updatedPV != nil { 217 ctrl.pvAddedUpdated(logger, test.updatedPV) 218 } 219 220 // Process the controller queue until we get expected results 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 // There is still some work in the queue, process it now 234 continue 235 } 236 currentActionCount := len(client.Actions()) 237 if currentActionCount < len(test.expectedActions) { 238 // Do not log evey wait, only when the action count changes. 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 // The test expected more to happen, wait for the actions. 244 // Most probably it's exponential backoff 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 }