k8s.io/kubernetes@v1.29.3/pkg/controller/volume/pvcprotection/pvc_protection_controller_test.go (about) 1 /* 2 Copyright 2017 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 pvcprotection 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/types" 33 "k8s.io/apimachinery/pkg/util/dump" 34 "k8s.io/client-go/informers" 35 "k8s.io/client-go/kubernetes/fake" 36 clienttesting "k8s.io/client-go/testing" 37 "k8s.io/klog/v2/ktesting" 38 "k8s.io/kubernetes/pkg/controller" 39 volumeutil "k8s.io/kubernetes/pkg/volume/util" 40 ) 41 42 type reaction struct { 43 verb string 44 resource string 45 reactorfn clienttesting.ReactionFunc 46 } 47 48 const ( 49 defaultNS = "default" 50 defaultPVCName = "pvc1" 51 defaultPodName = "pod1" 52 defaultNodeName = "node1" 53 defaultUID = "uid1" 54 ) 55 56 func pod() *v1.Pod { 57 return &v1.Pod{ 58 ObjectMeta: metav1.ObjectMeta{ 59 Name: defaultPodName, 60 Namespace: defaultNS, 61 UID: defaultUID, 62 }, 63 Spec: v1.PodSpec{ 64 NodeName: defaultNodeName, 65 }, 66 Status: v1.PodStatus{ 67 Phase: v1.PodPending, 68 }, 69 } 70 } 71 72 func unscheduled(pod *v1.Pod) *v1.Pod { 73 pod.Spec.NodeName = "" 74 return pod 75 } 76 77 func withPVC(pvcName string, pod *v1.Pod) *v1.Pod { 78 volume := v1.Volume{ 79 Name: pvcName, 80 VolumeSource: v1.VolumeSource{ 81 PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{ 82 ClaimName: pvcName, 83 }, 84 }, 85 } 86 pod.Spec.Volumes = append(pod.Spec.Volumes, volume) 87 return pod 88 } 89 90 func withEmptyDir(pod *v1.Pod) *v1.Pod { 91 volume := v1.Volume{ 92 Name: "emptyDir", 93 VolumeSource: v1.VolumeSource{ 94 EmptyDir: &v1.EmptyDirVolumeSource{}, 95 }, 96 } 97 pod.Spec.Volumes = append(pod.Spec.Volumes, volume) 98 return pod 99 } 100 101 func withStatus(phase v1.PodPhase, pod *v1.Pod) *v1.Pod { 102 pod.Status.Phase = phase 103 return pod 104 } 105 106 func withUID(uid types.UID, pod *v1.Pod) *v1.Pod { 107 pod.ObjectMeta.UID = uid 108 return pod 109 } 110 111 func pvc() *v1.PersistentVolumeClaim { 112 return &v1.PersistentVolumeClaim{ 113 ObjectMeta: metav1.ObjectMeta{ 114 Name: defaultPVCName, 115 Namespace: defaultNS, 116 }, 117 } 118 } 119 120 func withProtectionFinalizer(pvc *v1.PersistentVolumeClaim) *v1.PersistentVolumeClaim { 121 pvc.Finalizers = append(pvc.Finalizers, volumeutil.PVCProtectionFinalizer) 122 return pvc 123 } 124 125 func deleted(pvc *v1.PersistentVolumeClaim) *v1.PersistentVolumeClaim { 126 pvc.DeletionTimestamp = &metav1.Time{} 127 return pvc 128 } 129 130 func generateUpdateErrorFunc(t *testing.T, failures int) clienttesting.ReactionFunc { 131 i := 0 132 return func(action clienttesting.Action) (bool, runtime.Object, error) { 133 i++ 134 if i <= failures { 135 // Update fails 136 update, ok := action.(clienttesting.UpdateAction) 137 138 if !ok { 139 t.Fatalf("Reactor got non-update action: %+v", action) 140 } 141 acc, _ := meta.Accessor(update.GetObject()) 142 return true, nil, apierrors.NewForbidden(update.GetResource().GroupResource(), acc.GetName(), errors.New("Mock error")) 143 } 144 // Update succeeds 145 return false, nil, nil 146 } 147 } 148 149 func TestPVCProtectionController(t *testing.T) { 150 pvcGVR := schema.GroupVersionResource{ 151 Group: v1.GroupName, 152 Version: "v1", 153 Resource: "persistentvolumeclaims", 154 } 155 podGVR := schema.GroupVersionResource{ 156 Group: v1.GroupName, 157 Version: "v1", 158 Resource: "pods", 159 } 160 podGVK := schema.GroupVersionKind{ 161 Group: v1.GroupName, 162 Version: "v1", 163 Kind: "Pod", 164 } 165 166 tests := []struct { 167 name string 168 // Object to insert into fake kubeclient before the test starts. 169 initialObjects []runtime.Object 170 // Whether not to insert the content of initialObjects into the 171 // informers before the test starts. Set it to true to simulate the case 172 // where informers have not been notified yet of certain API objects. 173 informersAreLate bool 174 // Optional client reactors. 175 reactors []reaction 176 // PVC event to simulate. This PVC will be automatically added to 177 // initialObjects. 178 updatedPVC *v1.PersistentVolumeClaim 179 // Pod event to simulate. This Pod will be automatically added to 180 // initialObjects. 181 updatedPod *v1.Pod 182 // Pod event to simulate. This Pod is *not* added to 183 // initialObjects. 184 deletedPod *v1.Pod 185 // List of expected kubeclient actions that should happen during the 186 // test. 187 expectedActions []clienttesting.Action 188 }{ 189 // 190 // PVC events 191 // 192 { 193 name: "PVC without finalizer -> finalizer is added", 194 updatedPVC: pvc(), 195 expectedActions: []clienttesting.Action{ 196 clienttesting.NewUpdateAction(pvcGVR, defaultNS, withProtectionFinalizer(pvc())), 197 }, 198 }, 199 { 200 name: "PVC with finalizer -> no action", 201 updatedPVC: withProtectionFinalizer(pvc()), 202 expectedActions: []clienttesting.Action{}, 203 }, 204 { 205 name: "saving PVC finalizer fails -> controller retries", 206 updatedPVC: pvc(), 207 reactors: []reaction{ 208 { 209 verb: "update", 210 resource: "persistentvolumeclaims", 211 reactorfn: generateUpdateErrorFunc(t, 2 /* update fails twice*/), 212 }, 213 }, 214 expectedActions: []clienttesting.Action{ 215 // This fails 216 clienttesting.NewUpdateAction(pvcGVR, defaultNS, withProtectionFinalizer(pvc())), 217 // This fails too 218 clienttesting.NewUpdateAction(pvcGVR, defaultNS, withProtectionFinalizer(pvc())), 219 // This succeeds 220 clienttesting.NewUpdateAction(pvcGVR, defaultNS, withProtectionFinalizer(pvc())), 221 }, 222 }, 223 { 224 name: "deleted PVC with finalizer -> finalizer is removed", 225 updatedPVC: deleted(withProtectionFinalizer(pvc())), 226 expectedActions: []clienttesting.Action{ 227 clienttesting.NewListAction(podGVR, podGVK, defaultNS, metav1.ListOptions{}), 228 clienttesting.NewUpdateAction(pvcGVR, defaultNS, deleted(pvc())), 229 }, 230 }, 231 { 232 name: "finalizer removal fails -> controller retries", 233 updatedPVC: deleted(withProtectionFinalizer(pvc())), 234 reactors: []reaction{ 235 { 236 verb: "update", 237 resource: "persistentvolumeclaims", 238 reactorfn: generateUpdateErrorFunc(t, 2 /* update fails twice*/), 239 }, 240 }, 241 expectedActions: []clienttesting.Action{ 242 clienttesting.NewListAction(podGVR, podGVK, defaultNS, metav1.ListOptions{}), 243 // Fails 244 clienttesting.NewUpdateAction(pvcGVR, defaultNS, deleted(pvc())), 245 clienttesting.NewListAction(podGVR, podGVK, defaultNS, metav1.ListOptions{}), 246 // Fails too 247 clienttesting.NewUpdateAction(pvcGVR, defaultNS, deleted(pvc())), 248 clienttesting.NewListAction(podGVR, podGVK, defaultNS, metav1.ListOptions{}), 249 // Succeeds 250 clienttesting.NewUpdateAction(pvcGVR, defaultNS, deleted(pvc())), 251 }, 252 }, 253 { 254 name: "deleted PVC with finalizer + pod with the PVC exists -> finalizer is not removed", 255 initialObjects: []runtime.Object{ 256 withPVC(defaultPVCName, pod()), 257 }, 258 updatedPVC: deleted(withProtectionFinalizer(pvc())), 259 expectedActions: []clienttesting.Action{}, 260 }, 261 { 262 name: "deleted PVC with finalizer + pod with unrelated PVC and EmptyDir exists -> finalizer is removed", 263 initialObjects: []runtime.Object{ 264 withEmptyDir(withPVC("unrelatedPVC", pod())), 265 }, 266 updatedPVC: deleted(withProtectionFinalizer(pvc())), 267 expectedActions: []clienttesting.Action{ 268 clienttesting.NewListAction(podGVR, podGVK, defaultNS, metav1.ListOptions{}), 269 clienttesting.NewUpdateAction(pvcGVR, defaultNS, deleted(pvc())), 270 }, 271 }, 272 { 273 name: "deleted PVC with finalizer + pod with the PVC finished but is not deleted -> finalizer is not removed", 274 initialObjects: []runtime.Object{ 275 withStatus(v1.PodFailed, withPVC(defaultPVCName, pod())), 276 }, 277 updatedPVC: deleted(withProtectionFinalizer(pvc())), 278 expectedActions: []clienttesting.Action{}, 279 }, 280 { 281 name: "deleted PVC with finalizer + pod with the PVC exists but is not in the Informer's cache yet -> finalizer is not removed", 282 initialObjects: []runtime.Object{ 283 withPVC(defaultPVCName, pod()), 284 }, 285 informersAreLate: true, 286 updatedPVC: deleted(withProtectionFinalizer(pvc())), 287 expectedActions: []clienttesting.Action{ 288 clienttesting.NewListAction(podGVR, podGVK, defaultNS, metav1.ListOptions{}), 289 }, 290 }, 291 // 292 // Pod events 293 // 294 { 295 name: "updated running Pod -> no action", 296 initialObjects: []runtime.Object{ 297 deleted(withProtectionFinalizer(pvc())), 298 }, 299 updatedPod: withStatus(v1.PodRunning, withPVC(defaultPVCName, pod())), 300 expectedActions: []clienttesting.Action{}, 301 }, 302 { 303 name: "updated finished Pod -> finalizer is not removed", 304 initialObjects: []runtime.Object{ 305 deleted(withProtectionFinalizer(pvc())), 306 }, 307 updatedPod: withStatus(v1.PodSucceeded, withPVC(defaultPVCName, pod())), 308 expectedActions: []clienttesting.Action{}, 309 }, 310 { 311 name: "updated unscheduled Pod -> finalizer is removed", 312 initialObjects: []runtime.Object{ 313 deleted(withProtectionFinalizer(pvc())), 314 }, 315 updatedPod: unscheduled(withPVC(defaultPVCName, pod())), 316 expectedActions: []clienttesting.Action{ 317 clienttesting.NewListAction(podGVR, podGVK, defaultNS, metav1.ListOptions{}), 318 clienttesting.NewUpdateAction(pvcGVR, defaultNS, deleted(pvc())), 319 }, 320 }, 321 { 322 name: "deleted running Pod -> finalizer is removed", 323 initialObjects: []runtime.Object{ 324 deleted(withProtectionFinalizer(pvc())), 325 }, 326 deletedPod: withStatus(v1.PodRunning, withPVC(defaultPVCName, pod())), 327 expectedActions: []clienttesting.Action{ 328 clienttesting.NewListAction(podGVR, podGVK, defaultNS, metav1.ListOptions{}), 329 clienttesting.NewUpdateAction(pvcGVR, defaultNS, deleted(pvc())), 330 }, 331 }, 332 { 333 name: "pod delete and create with same namespaced name seen as an update, old pod used deleted PVC -> finalizer is removed", 334 initialObjects: []runtime.Object{ 335 deleted(withProtectionFinalizer(pvc())), 336 }, 337 deletedPod: withPVC(defaultPVCName, pod()), 338 updatedPod: withUID("uid2", pod()), 339 expectedActions: []clienttesting.Action{ 340 clienttesting.NewListAction(podGVR, podGVK, defaultNS, metav1.ListOptions{}), 341 clienttesting.NewUpdateAction(pvcGVR, defaultNS, deleted(pvc())), 342 }, 343 }, 344 { 345 name: "pod delete and create with same namespaced name seen as an update, old pod used non-deleted PVC -> finalizer is not removed", 346 initialObjects: []runtime.Object{ 347 withProtectionFinalizer(pvc()), 348 }, 349 deletedPod: withPVC(defaultPVCName, pod()), 350 updatedPod: withUID("uid2", pod()), 351 expectedActions: []clienttesting.Action{}, 352 }, 353 { 354 name: "pod delete and create with same namespaced name seen as an update, both pods reference deleted PVC -> finalizer is not removed", 355 initialObjects: []runtime.Object{ 356 deleted(withProtectionFinalizer(pvc())), 357 }, 358 deletedPod: withPVC(defaultPVCName, pod()), 359 updatedPod: withUID("uid2", withPVC(defaultPVCName, pod())), 360 expectedActions: []clienttesting.Action{}, 361 }, 362 { 363 name: "pod update from unscheduled to scheduled, deleted PVC is referenced -> finalizer is not removed", 364 initialObjects: []runtime.Object{ 365 deleted(withProtectionFinalizer(pvc())), 366 }, 367 deletedPod: unscheduled(withPVC(defaultPVCName, pod())), 368 updatedPod: withPVC(defaultPVCName, pod()), 369 expectedActions: []clienttesting.Action{}, 370 }, 371 } 372 373 for _, test := range tests { 374 // Create initial data for client and informers. 375 var ( 376 clientObjs []runtime.Object 377 informersObjs []runtime.Object 378 ) 379 if test.updatedPVC != nil { 380 clientObjs = append(clientObjs, test.updatedPVC) 381 informersObjs = append(informersObjs, test.updatedPVC) 382 } 383 if test.updatedPod != nil { 384 clientObjs = append(clientObjs, test.updatedPod) 385 informersObjs = append(informersObjs, test.updatedPod) 386 } 387 clientObjs = append(clientObjs, test.initialObjects...) 388 if !test.informersAreLate { 389 informersObjs = append(informersObjs, test.initialObjects...) 390 } 391 392 // Create client with initial data 393 client := fake.NewSimpleClientset(clientObjs...) 394 395 // Create informers 396 informers := informers.NewSharedInformerFactory(client, controller.NoResyncPeriodFunc()) 397 pvcInformer := informers.Core().V1().PersistentVolumeClaims() 398 podInformer := informers.Core().V1().Pods() 399 400 // Create the controller 401 logger, _ := ktesting.NewTestContext(t) 402 ctrl, err := NewPVCProtectionController(logger, pvcInformer, podInformer, client) 403 if err != nil { 404 t.Fatalf("unexpected error: %v", err) 405 } 406 407 // Populate the informers with initial objects so the controller can 408 // Get() and List() it. 409 for _, obj := range informersObjs { 410 switch obj.(type) { 411 case *v1.PersistentVolumeClaim: 412 pvcInformer.Informer().GetStore().Add(obj) 413 case *v1.Pod: 414 podInformer.Informer().GetStore().Add(obj) 415 default: 416 t.Fatalf("Unknown initalObject type: %+v", obj) 417 } 418 } 419 420 // Add reactor to inject test errors. 421 for _, reactor := range test.reactors { 422 client.Fake.PrependReactor(reactor.verb, reactor.resource, reactor.reactorfn) 423 } 424 425 // Start the test by simulating an event 426 if test.updatedPVC != nil { 427 ctrl.pvcAddedUpdated(logger, test.updatedPVC) 428 } 429 switch { 430 case test.deletedPod != nil && test.updatedPod != nil && test.deletedPod.Namespace == test.updatedPod.Namespace && test.deletedPod.Name == test.updatedPod.Name: 431 ctrl.podAddedDeletedUpdated(logger, test.deletedPod, test.updatedPod, false) 432 case test.updatedPod != nil: 433 ctrl.podAddedDeletedUpdated(logger, nil, test.updatedPod, false) 434 case test.deletedPod != nil: 435 ctrl.podAddedDeletedUpdated(logger, nil, test.deletedPod, true) 436 } 437 438 // Process the controller queue until we get expected results 439 timeout := time.Now().Add(10 * time.Second) 440 lastReportedActionCount := 0 441 for { 442 if time.Now().After(timeout) { 443 t.Errorf("Test %q: timed out", test.name) 444 break 445 } 446 if ctrl.queue.Len() > 0 { 447 logger.V(5).Info("Non-empty queue, processing one", "test", test.name, "queueLength", ctrl.queue.Len()) 448 ctrl.processNextWorkItem(context.TODO()) 449 } 450 if ctrl.queue.Len() > 0 { 451 // There is still some work in the queue, process it now 452 continue 453 } 454 currentActionCount := len(client.Actions()) 455 if currentActionCount < len(test.expectedActions) { 456 // Do not log every wait, only when the action count changes. 457 if lastReportedActionCount < currentActionCount { 458 logger.V(5).Info("Waiting for the remaining actions", "test", test.name, "currentActionCount", currentActionCount, "expectedActionCount", len(test.expectedActions)) 459 lastReportedActionCount = currentActionCount 460 } 461 // The test expected more to happen, wait for the actions. 462 // Most probably it's exponential backoff 463 time.Sleep(10 * time.Millisecond) 464 continue 465 } 466 break 467 } 468 actions := client.Actions() 469 for i, action := range actions { 470 if len(test.expectedActions) < i+1 { 471 t.Errorf("Test %q: %d unexpected actions: %+v", test.name, len(actions)-len(test.expectedActions), dump.Pretty(actions[i:])) 472 break 473 } 474 475 expectedAction := test.expectedActions[i] 476 if !reflect.DeepEqual(expectedAction, action) { 477 t.Errorf("Test %q: action %d\nExpected:\n%s\ngot:\n%s", test.name, i, dump.Pretty(expectedAction), dump.Pretty(action)) 478 } 479 } 480 481 if len(test.expectedActions) > len(actions) { 482 t.Errorf("Test %q: %d additional expected actions", test.name, len(test.expectedActions)-len(actions)) 483 for _, a := range test.expectedActions[len(actions):] { 484 t.Logf(" %+v", a) 485 } 486 } 487 488 } 489 }