k8s.io/kubernetes@v1.29.3/pkg/controller/volume/persistentvolume/framework_test.go (about) 1 /* 2 Copyright 2016 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 persistentvolume 18 19 import ( 20 "context" 21 "fmt" 22 "reflect" 23 "strings" 24 "sync/atomic" 25 "testing" 26 "time" 27 28 "k8s.io/klog/v2" 29 30 v1 "k8s.io/api/core/v1" 31 storage "k8s.io/api/storage/v1" 32 "k8s.io/apimachinery/pkg/api/resource" 33 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 34 "k8s.io/apimachinery/pkg/types" 35 "k8s.io/apimachinery/pkg/util/wait" 36 "k8s.io/apimachinery/pkg/watch" 37 "k8s.io/client-go/informers" 38 clientset "k8s.io/client-go/kubernetes" 39 "k8s.io/client-go/kubernetes/fake" 40 corelisters "k8s.io/client-go/listers/core/v1" 41 storagelisters "k8s.io/client-go/listers/storage/v1" 42 "k8s.io/client-go/tools/cache" 43 "k8s.io/client-go/tools/record" 44 storagehelpers "k8s.io/component-helpers/storage/volume" 45 "k8s.io/kubernetes/pkg/controller" 46 pvtesting "k8s.io/kubernetes/pkg/controller/volume/persistentvolume/testing" 47 "k8s.io/kubernetes/pkg/volume" 48 "k8s.io/kubernetes/pkg/volume/util/recyclerclient" 49 ) 50 51 func init() { 52 klog.InitFlags(nil) 53 } 54 55 // This is a unit test framework for persistent volume controller. 56 // It fills the controller with test claims/volumes and can simulate these 57 // scenarios: 58 // 1) Call syncClaim/syncVolume once. 59 // 2) Call syncClaim/syncVolume several times (both simulating "claim/volume 60 // modified" events and periodic sync), until the controller settles down and 61 // does not modify anything. 62 // 3) Simulate almost real API server/etcd and call add/update/delete 63 // volume/claim. 64 // In all these scenarios, when the test finishes, the framework can compare 65 // resulting claims/volumes with list of expected claims/volumes and report 66 // differences. 67 68 // controllerTest contains a single controller test input. 69 // Each test has initial set of volumes and claims that are filled into the 70 // controller before the test starts. The test then contains a reference to 71 // function to call as the actual test. Available functions are: 72 // - testSyncClaim - calls syncClaim on the first claim in initialClaims. 73 // - testSyncClaimError - calls syncClaim on the first claim in initialClaims 74 // and expects an error to be returned. 75 // - testSyncVolume - calls syncVolume on the first volume in initialVolumes. 76 // - any custom function for specialized tests. 77 // 78 // The test then contains list of volumes/claims that are expected at the end 79 // of the test and list of generated events. 80 type controllerTest struct { 81 // Name of the test, for logging 82 name string 83 // Initial content of controller volume cache. 84 initialVolumes []*v1.PersistentVolume 85 // Expected content of controller volume cache at the end of the test. 86 expectedVolumes []*v1.PersistentVolume 87 // Initial content of controller claim cache. 88 initialClaims []*v1.PersistentVolumeClaim 89 // Expected content of controller claim cache at the end of the test. 90 expectedClaims []*v1.PersistentVolumeClaim 91 // Expected events - any event with prefix will pass, we don't check full 92 // event message. 93 expectedEvents []string 94 // Errors to produce on matching action 95 errors []pvtesting.ReactorError 96 // Function to call as the test. 97 test testCall 98 } 99 100 // annSkipLocalStore can be used to mark initial PVs or PVCs that are meant to be added only 101 // to the fake apiserver (i.e. available via Get) but not to the local store (i.e. the controller 102 // won't have them in its cache). 103 const annSkipLocalStore = "pv-testing-skip-local-store" 104 105 type testCall func(ctrl *PersistentVolumeController, reactor *pvtesting.VolumeReactor, test controllerTest) error 106 107 const testNamespace = "default" 108 const mockPluginName = "kubernetes.io/mock-volume" 109 110 var novolumes []*v1.PersistentVolume 111 var noclaims []*v1.PersistentVolumeClaim 112 var noevents = []string{} 113 var noerrors = []pvtesting.ReactorError{} 114 115 type volumeReactor struct { 116 *pvtesting.VolumeReactor 117 ctrl *PersistentVolumeController 118 } 119 120 func newVolumeReactor(ctx context.Context, client *fake.Clientset, ctrl *PersistentVolumeController, fakeVolumeWatch, fakeClaimWatch *watch.FakeWatcher, errors []pvtesting.ReactorError) *volumeReactor { 121 return &volumeReactor{ 122 pvtesting.NewVolumeReactor(ctx, client, fakeVolumeWatch, fakeClaimWatch, errors), 123 ctrl, 124 } 125 } 126 127 // waitForIdle waits until all tests, controllers and other goroutines do their 128 // job and no new actions are registered for 10 milliseconds. 129 func (r *volumeReactor) waitForIdle() { 130 r.ctrl.runningOperations.WaitForCompletion() 131 // Check every 10ms if the controller does something and stop if it's 132 // idle. 133 oldChanges := -1 134 for { 135 time.Sleep(10 * time.Millisecond) 136 changes := r.GetChangeCount() 137 if changes == oldChanges { 138 // No changes for last 10ms -> controller must be idle. 139 break 140 } 141 oldChanges = changes 142 } 143 } 144 145 // waitTest waits until all tests, controllers and other goroutines do their 146 // job and list of current volumes/claims is equal to list of expected 147 // volumes/claims (with ~10 second timeout). 148 func (r *volumeReactor) waitTest(test controllerTest) error { 149 // start with 10 ms, multiply by 2 each step, 10 steps = 10.23 seconds 150 backoff := wait.Backoff{ 151 Duration: 10 * time.Millisecond, 152 Jitter: 0, 153 Factor: 2, 154 Steps: 10, 155 } 156 err := wait.ExponentialBackoff(backoff, func() (done bool, err error) { 157 // Finish all operations that are in progress 158 r.ctrl.runningOperations.WaitForCompletion() 159 160 // Return 'true' if the reactor reached the expected state 161 err1 := r.CheckClaims(test.expectedClaims) 162 err2 := r.CheckVolumes(test.expectedVolumes) 163 if err1 == nil && err2 == nil { 164 return true, nil 165 } 166 return false, nil 167 }) 168 return err 169 } 170 171 // checkEvents compares all expectedEvents with events generated during the test 172 // and reports differences. 173 func checkEvents(t *testing.T, ctx context.Context, expectedEvents []string, ctrl *PersistentVolumeController) error { 174 var err error 175 176 // Read recorded events - wait up to 1 minute to get all the expected ones 177 // (just in case some goroutines are slower with writing) 178 timer := time.NewTimer(time.Minute) 179 defer timer.Stop() 180 logger := klog.FromContext(ctx) 181 fakeRecorder := ctrl.eventRecorder.(*record.FakeRecorder) 182 gotEvents := []string{} 183 finished := false 184 for len(gotEvents) < len(expectedEvents) && !finished { 185 select { 186 case event, ok := <-fakeRecorder.Events: 187 if ok { 188 logger.V(5).Info("Event recorder got event", "event", event) 189 gotEvents = append(gotEvents, event) 190 } else { 191 logger.V(5).Info("Event recorder finished") 192 finished = true 193 } 194 case _, _ = <-timer.C: 195 logger.V(5).Info("Event recorder timeout") 196 finished = true 197 } 198 } 199 200 // Evaluate the events 201 for i, expected := range expectedEvents { 202 if len(gotEvents) <= i { 203 t.Errorf("Event %q not emitted", expected) 204 err = fmt.Errorf("events do not match") 205 continue 206 } 207 received := gotEvents[i] 208 if !strings.HasPrefix(received, expected) { 209 t.Errorf("Unexpected event received, expected %q, got %q", expected, received) 210 err = fmt.Errorf("events do not match") 211 } 212 } 213 for i := len(expectedEvents); i < len(gotEvents); i++ { 214 t.Errorf("Unexpected event received: %q", gotEvents[i]) 215 err = fmt.Errorf("events do not match") 216 } 217 return err 218 } 219 220 func alwaysReady() bool { return true } 221 222 func newTestController(ctx context.Context, kubeClient clientset.Interface, informerFactory informers.SharedInformerFactory, enableDynamicProvisioning bool) (*PersistentVolumeController, error) { 223 if informerFactory == nil { 224 informerFactory = informers.NewSharedInformerFactory(kubeClient, controller.NoResyncPeriodFunc()) 225 } 226 params := ControllerParameters{ 227 KubeClient: kubeClient, 228 SyncPeriod: 5 * time.Second, 229 VolumePlugins: []volume.VolumePlugin{}, 230 VolumeInformer: informerFactory.Core().V1().PersistentVolumes(), 231 ClaimInformer: informerFactory.Core().V1().PersistentVolumeClaims(), 232 ClassInformer: informerFactory.Storage().V1().StorageClasses(), 233 PodInformer: informerFactory.Core().V1().Pods(), 234 NodeInformer: informerFactory.Core().V1().Nodes(), 235 EventRecorder: record.NewFakeRecorder(1000), 236 EnableDynamicProvisioning: enableDynamicProvisioning, 237 } 238 ctrl, err := NewController(ctx, params) 239 if err != nil { 240 return nil, fmt.Errorf("failed to construct persistentvolume controller: %v", err) 241 } 242 ctrl.volumeListerSynced = alwaysReady 243 ctrl.claimListerSynced = alwaysReady 244 ctrl.classListerSynced = alwaysReady 245 // Speed up the test 246 ctrl.createProvisionedPVInterval = 5 * time.Millisecond 247 return ctrl, nil 248 } 249 250 // newVolume returns a new volume with given attributes 251 func newVolume(name, capacity, boundToClaimUID, boundToClaimName string, phase v1.PersistentVolumePhase, reclaimPolicy v1.PersistentVolumeReclaimPolicy, class string, annotations ...string) *v1.PersistentVolume { 252 fs := v1.PersistentVolumeFilesystem 253 volume := v1.PersistentVolume{ 254 ObjectMeta: metav1.ObjectMeta{ 255 Name: name, 256 ResourceVersion: "1", 257 }, 258 Spec: v1.PersistentVolumeSpec{ 259 Capacity: v1.ResourceList{ 260 v1.ResourceName(v1.ResourceStorage): resource.MustParse(capacity), 261 }, 262 PersistentVolumeSource: v1.PersistentVolumeSource{ 263 GCEPersistentDisk: &v1.GCEPersistentDiskVolumeSource{}, 264 }, 265 AccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce, v1.ReadOnlyMany}, 266 PersistentVolumeReclaimPolicy: reclaimPolicy, 267 StorageClassName: class, 268 VolumeMode: &fs, 269 }, 270 Status: v1.PersistentVolumeStatus{ 271 Phase: phase, 272 }, 273 } 274 275 if boundToClaimName != "" { 276 volume.Spec.ClaimRef = &v1.ObjectReference{ 277 Kind: "PersistentVolumeClaim", 278 APIVersion: "v1", 279 UID: types.UID(boundToClaimUID), 280 Namespace: testNamespace, 281 Name: boundToClaimName, 282 } 283 } 284 285 if len(annotations) > 0 { 286 volume.Annotations = make(map[string]string) 287 for _, a := range annotations { 288 switch a { 289 case storagehelpers.AnnDynamicallyProvisioned: 290 volume.Annotations[a] = mockPluginName 291 default: 292 volume.Annotations[a] = "yes" 293 } 294 } 295 } 296 297 return &volume 298 } 299 300 // newExternalProvisionedVolume returns a new volume with given attributes 301 func newExternalProvisionedVolume(name, capacity, boundToClaimUID, boundToClaimName string, phase v1.PersistentVolumePhase, reclaimPolicy v1.PersistentVolumeReclaimPolicy, class string, driverName string, finalizers []string, annotations ...string) *v1.PersistentVolume { 302 fs := v1.PersistentVolumeFilesystem 303 volume := v1.PersistentVolume{ 304 ObjectMeta: metav1.ObjectMeta{ 305 Name: name, 306 ResourceVersion: "1", 307 Finalizers: finalizers, 308 }, 309 Spec: v1.PersistentVolumeSpec{ 310 Capacity: v1.ResourceList{ 311 v1.ResourceName(v1.ResourceStorage): resource.MustParse(capacity), 312 }, 313 PersistentVolumeSource: v1.PersistentVolumeSource{ 314 CSI: &v1.CSIPersistentVolumeSource{ 315 Driver: driverName, 316 VolumeHandle: "527b55dc-c7db-4574-9226-2e33318b06a3", 317 ReadOnly: false, 318 FSType: "ext4", 319 VolumeAttributes: map[string]string{ 320 "Test-Key": "Test-Value", 321 }, 322 }, 323 }, 324 AccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce, v1.ReadOnlyMany}, 325 PersistentVolumeReclaimPolicy: reclaimPolicy, 326 StorageClassName: class, 327 VolumeMode: &fs, 328 }, 329 Status: v1.PersistentVolumeStatus{ 330 Phase: phase, 331 }, 332 } 333 334 if boundToClaimName != "" { 335 volume.Spec.ClaimRef = &v1.ObjectReference{ 336 Kind: "PersistentVolumeClaim", 337 APIVersion: "v1", 338 UID: types.UID(boundToClaimUID), 339 Namespace: testNamespace, 340 Name: boundToClaimName, 341 } 342 } 343 344 if len(annotations) > 0 { 345 volume.Annotations = make(map[string]string) 346 for _, a := range annotations { 347 switch a { 348 case storagehelpers.AnnDynamicallyProvisioned: 349 volume.Annotations[a] = driverName 350 default: 351 volume.Annotations[a] = "yes" 352 } 353 } 354 } 355 356 return &volume 357 } 358 359 // newVolume returns a new volume with given attributes 360 func newVolumeWithFinalizers(name, capacity, boundToClaimUID, boundToClaimName string, phase v1.PersistentVolumePhase, reclaimPolicy v1.PersistentVolumeReclaimPolicy, class string, finalizers []string, annotations ...string) *v1.PersistentVolume { 361 retVolume := newVolume(name, capacity, boundToClaimUID, boundToClaimName, phase, reclaimPolicy, class, annotations...) 362 retVolume.SetFinalizers(finalizers) 363 return retVolume 364 } 365 366 // withLabels applies the given labels to the first volume in the array and 367 // returns the array. Meant to be used to compose volumes specified inline in 368 // a test. 369 func withLabels(labels map[string]string, volumes []*v1.PersistentVolume) []*v1.PersistentVolume { 370 volumes[0].Labels = labels 371 return volumes 372 } 373 374 // withLabelSelector sets the label selector of the first claim in the array 375 // to be MatchLabels of the given label set and returns the array. Meant 376 // to be used to compose claims specified inline in a test. 377 func withLabelSelector(labels map[string]string, claims []*v1.PersistentVolumeClaim) []*v1.PersistentVolumeClaim { 378 claims[0].Spec.Selector = &metav1.LabelSelector{ 379 MatchLabels: labels, 380 } 381 382 return claims 383 } 384 385 // withVolumeVolumeMode applies the given VolumeMode to the first volume in the array and 386 // returns the array. Meant to be used to compose volumes specified inline in 387 // a test. 388 func withVolumeVolumeMode(mode *v1.PersistentVolumeMode, volumes []*v1.PersistentVolume) []*v1.PersistentVolume { 389 volumes[0].Spec.VolumeMode = mode 390 return volumes 391 } 392 393 // withClaimVolumeMode applies the given VolumeMode to the first claim in the array and 394 // returns the array. Meant to be used to compose volumes specified inline in 395 // a test. 396 func withClaimVolumeMode(mode *v1.PersistentVolumeMode, claims []*v1.PersistentVolumeClaim) []*v1.PersistentVolumeClaim { 397 claims[0].Spec.VolumeMode = mode 398 return claims 399 } 400 401 // withExpectedCapacity sets the claim.Spec.Capacity of the first claim in the 402 // array to given value and returns the array. Meant to be used to compose 403 // claims specified inline in a test. 404 func withExpectedCapacity(capacity string, claims []*v1.PersistentVolumeClaim) []*v1.PersistentVolumeClaim { 405 claims[0].Status.Capacity = v1.ResourceList{ 406 v1.ResourceName(v1.ResourceStorage): resource.MustParse(capacity), 407 } 408 409 return claims 410 } 411 412 // withMessage saves given message into volume.Status.Message of the first 413 // volume in the array and returns the array. Meant to be used to compose 414 // volumes specified inline in a test. 415 func withMessage(message string, volumes []*v1.PersistentVolume) []*v1.PersistentVolume { 416 volumes[0].Status.Message = message 417 return volumes 418 } 419 420 // newVolumeArray returns array with a single volume that would be returned by 421 // newVolume() with the same parameters. 422 func newVolumeArray(name, capacity, boundToClaimUID, boundToClaimName string, phase v1.PersistentVolumePhase, reclaimPolicy v1.PersistentVolumeReclaimPolicy, class string, annotations ...string) []*v1.PersistentVolume { 423 return []*v1.PersistentVolume{ 424 newVolume(name, capacity, boundToClaimUID, boundToClaimName, phase, reclaimPolicy, class, annotations...), 425 } 426 } 427 428 func withVolumeDeletionTimestamp(pvs []*v1.PersistentVolume) []*v1.PersistentVolume { 429 result := []*v1.PersistentVolume{} 430 for _, pv := range pvs { 431 // Using time.Now() here will cause mismatching deletion timestamps in tests 432 deleteTime := metav1.Date(2020, time.February, 18, 10, 30, 30, 10, time.UTC) 433 pv.SetDeletionTimestamp(&deleteTime) 434 result = append(result, pv) 435 } 436 return result 437 } 438 439 func volumesWithFinalizers(pvs []*v1.PersistentVolume, finalizers []string) []*v1.PersistentVolume { 440 result := []*v1.PersistentVolume{} 441 for _, pv := range pvs { 442 pv.SetFinalizers(finalizers) 443 result = append(result, pv) 444 } 445 return result 446 } 447 448 // newClaim returns a new claim with given attributes 449 func newClaim(name, claimUID, capacity, boundToVolume string, phase v1.PersistentVolumeClaimPhase, class *string, annotations ...string) *v1.PersistentVolumeClaim { 450 fs := v1.PersistentVolumeFilesystem 451 claim := v1.PersistentVolumeClaim{ 452 ObjectMeta: metav1.ObjectMeta{ 453 Name: name, 454 Namespace: testNamespace, 455 UID: types.UID(claimUID), 456 ResourceVersion: "1", 457 }, 458 Spec: v1.PersistentVolumeClaimSpec{ 459 AccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce, v1.ReadOnlyMany}, 460 Resources: v1.VolumeResourceRequirements{ 461 Requests: v1.ResourceList{ 462 v1.ResourceName(v1.ResourceStorage): resource.MustParse(capacity), 463 }, 464 }, 465 VolumeName: boundToVolume, 466 StorageClassName: class, 467 VolumeMode: &fs, 468 }, 469 Status: v1.PersistentVolumeClaimStatus{ 470 Phase: phase, 471 }, 472 } 473 474 if len(annotations) > 0 { 475 claim.Annotations = make(map[string]string) 476 for _, a := range annotations { 477 switch a { 478 case storagehelpers.AnnBetaStorageProvisioner, storagehelpers.AnnStorageProvisioner: 479 claim.Annotations[a] = mockPluginName 480 default: 481 claim.Annotations[a] = "yes" 482 } 483 } 484 } 485 486 // Bound claims must have proper Status. 487 if phase == v1.ClaimBound { 488 claim.Status.AccessModes = claim.Spec.AccessModes 489 // For most of the tests it's enough to copy claim's requested capacity, 490 // individual tests can adjust it using withExpectedCapacity() 491 claim.Status.Capacity = claim.Spec.Resources.Requests 492 } 493 494 return &claim 495 } 496 497 // newClaimArray returns array with a single claim that would be returned by 498 // newClaim() with the same parameters. 499 func newClaimArray(name, claimUID, capacity, boundToVolume string, phase v1.PersistentVolumeClaimPhase, class *string, annotations ...string) []*v1.PersistentVolumeClaim { 500 return []*v1.PersistentVolumeClaim{ 501 newClaim(name, claimUID, capacity, boundToVolume, phase, class, annotations...), 502 } 503 } 504 505 // claimWithAnnotation saves given annotation into given claims. Meant to be 506 // used to compose claims specified inline in a test. 507 // TODO(refactor): This helper function (and other helpers related to claim 508 // arrays) could use some cleaning up (most assume an array size of one)- 509 // replace with annotateClaim at all callsites. The tests require claimArrays 510 // but mostly operate on single claims 511 func claimWithAnnotation(name, value string, claims []*v1.PersistentVolumeClaim) []*v1.PersistentVolumeClaim { 512 if claims[0].Annotations == nil { 513 claims[0].Annotations = map[string]string{name: value} 514 } else { 515 claims[0].Annotations[name] = value 516 } 517 return claims 518 } 519 520 func claimWithDataSource(name, kind, apiGroup string, claims []*v1.PersistentVolumeClaim) []*v1.PersistentVolumeClaim { 521 claims[0].Spec.DataSource = &v1.TypedLocalObjectReference{ 522 Name: name, 523 Kind: kind, 524 APIGroup: &apiGroup, 525 } 526 return claims 527 } 528 529 func annotateClaim(claim *v1.PersistentVolumeClaim, ann map[string]string) *v1.PersistentVolumeClaim { 530 if claim.Annotations == nil { 531 claim.Annotations = map[string]string{} 532 } 533 for key, val := range ann { 534 claim.Annotations[key] = val 535 } 536 return claim 537 } 538 539 // volumeWithAnnotation saves given annotation into given volume. 540 // Meant to be used to compose volume specified inline in a test. 541 func volumeWithAnnotation(name, value string, volume *v1.PersistentVolume) *v1.PersistentVolume { 542 if volume.Annotations == nil { 543 volume.Annotations = map[string]string{name: value} 544 } else { 545 volume.Annotations[name] = value 546 } 547 return volume 548 } 549 550 // volumesWithAnnotation saves given annotation into given volumes. 551 // Meant to be used to compose volumes specified inline in a test. 552 func volumesWithAnnotation(name, value string, volumes []*v1.PersistentVolume) []*v1.PersistentVolume { 553 for _, volume := range volumes { 554 volumeWithAnnotation(name, value, volume) 555 } 556 return volumes 557 } 558 559 // claimWithAccessMode saves given access into given claims. 560 // Meant to be used to compose claims specified inline in a test. 561 func claimWithAccessMode(modes []v1.PersistentVolumeAccessMode, claims []*v1.PersistentVolumeClaim) []*v1.PersistentVolumeClaim { 562 claims[0].Spec.AccessModes = modes 563 return claims 564 } 565 566 func testSyncClaim(ctrl *PersistentVolumeController, reactor *pvtesting.VolumeReactor, test controllerTest) error { 567 return ctrl.syncClaim(context.TODO(), test.initialClaims[0]) 568 } 569 570 func testSyncClaimError(ctrl *PersistentVolumeController, reactor *pvtesting.VolumeReactor, test controllerTest) error { 571 err := ctrl.syncClaim(context.TODO(), test.initialClaims[0]) 572 573 if err != nil { 574 return nil 575 } 576 return fmt.Errorf("syncClaim succeeded when failure was expected") 577 } 578 579 func testSyncVolume(ctrl *PersistentVolumeController, reactor *pvtesting.VolumeReactor, test controllerTest) error { 580 return ctrl.syncVolume(context.TODO(), test.initialVolumes[0]) 581 } 582 583 type operationType string 584 585 const operationDelete = "Delete" 586 const operationRecycle = "Recycle" 587 588 var ( 589 classGold = "gold" 590 classSilver = "silver" 591 classCopper = "copper" 592 classEmpty = "" 593 classNonExisting = "non-existing" 594 classExternal = "external" 595 classExternalWait = "external-wait" 596 classUnknownInternal = "unknown-internal" 597 classUnsupportedMountOptions = "unsupported-mountoptions" 598 classLarge = "large" 599 classWait = "wait" 600 classCSI = "csi" 601 602 modeWait = storage.VolumeBindingWaitForFirstConsumer 603 ) 604 605 // wrapTestWithPluginCalls returns a testCall that: 606 // - configures controller with a volume plugin that implements recycler, 607 // deleter and provisioner. The plugin returns provided errors when a volume 608 // is deleted, recycled or provisioned. 609 // - calls given testCall 610 func wrapTestWithPluginCalls(expectedRecycleCalls, expectedDeleteCalls []error, expectedProvisionCalls []provisionCall, toWrap testCall) testCall { 611 return func(ctrl *PersistentVolumeController, reactor *pvtesting.VolumeReactor, test controllerTest) error { 612 plugin := &mockVolumePlugin{ 613 recycleCalls: expectedRecycleCalls, 614 deleteCalls: expectedDeleteCalls, 615 provisionCalls: expectedProvisionCalls, 616 } 617 ctrl.volumePluginMgr.InitPlugins([]volume.VolumePlugin{plugin}, nil /* prober */, ctrl) 618 return toWrap(ctrl, reactor, test) 619 } 620 } 621 622 // wrapTestWithReclaimCalls returns a testCall that: 623 // - configures controller with recycler or deleter which will return provided 624 // errors when a volume is deleted or recycled 625 // - calls given testCall 626 func wrapTestWithReclaimCalls(operation operationType, expectedOperationCalls []error, toWrap testCall) testCall { 627 if operation == operationDelete { 628 return wrapTestWithPluginCalls(nil, expectedOperationCalls, nil, toWrap) 629 } else { 630 return wrapTestWithPluginCalls(expectedOperationCalls, nil, nil, toWrap) 631 } 632 } 633 634 // wrapTestWithProvisionCalls returns a testCall that: 635 // - configures controller with a provisioner which will return provided errors 636 // when a claim is provisioned 637 // - calls given testCall 638 func wrapTestWithProvisionCalls(expectedProvisionCalls []provisionCall, toWrap testCall) testCall { 639 return wrapTestWithPluginCalls(nil, nil, expectedProvisionCalls, toWrap) 640 } 641 642 type fakeCSINameTranslator struct{} 643 644 func (t fakeCSINameTranslator) GetCSINameFromInTreeName(pluginName string) (string, error) { 645 return "vendor.com/MockCSIDriver", nil 646 } 647 648 type fakeCSIMigratedPluginManager struct{} 649 650 func (t fakeCSIMigratedPluginManager) IsMigrationEnabledForPlugin(pluginName string) bool { 651 return true 652 } 653 654 // wrapTestWithCSIMigrationProvisionCalls returns a testCall that: 655 // - configures controller with a volume plugin that emulates CSI migration 656 // - calls given testCall 657 func wrapTestWithCSIMigrationProvisionCalls(toWrap testCall) testCall { 658 plugin := &mockVolumePlugin{} 659 return func(ctrl *PersistentVolumeController, reactor *pvtesting.VolumeReactor, test controllerTest) error { 660 ctrl.volumePluginMgr.InitPlugins([]volume.VolumePlugin{plugin}, nil /* prober */, ctrl) 661 ctrl.translator = fakeCSINameTranslator{} 662 ctrl.csiMigratedPluginManager = fakeCSIMigratedPluginManager{} 663 return toWrap(ctrl, reactor, test) 664 } 665 } 666 667 // wrapTestWithInjectedOperation returns a testCall that: 668 // - starts the controller and lets it run original testCall until 669 // scheduleOperation() call. It blocks the controller there and calls the 670 // injected function to simulate that something is happening when the 671 // controller waits for the operation lock. Controller is then resumed and we 672 // check how it behaves. 673 func wrapTestWithInjectedOperation(ctx context.Context, toWrap testCall, injectBeforeOperation func(ctrl *PersistentVolumeController, reactor *pvtesting.VolumeReactor)) testCall { 674 675 return func(ctrl *PersistentVolumeController, reactor *pvtesting.VolumeReactor, test controllerTest) error { 676 // Inject a hook before async operation starts 677 ctrl.preOperationHook = func(operationName string) { 678 // Inside the hook, run the function to inject 679 klog.FromContext(ctx).V(4).Info("Reactor: scheduleOperation reached, injecting call") 680 injectBeforeOperation(ctrl, reactor) 681 } 682 683 // Run the tested function (typically syncClaim/syncVolume) in a 684 // separate goroutine. 685 var testError error 686 var testFinished int32 687 688 go func() { 689 testError = toWrap(ctrl, reactor, test) 690 // Let the "main" test function know that syncVolume has finished. 691 atomic.StoreInt32(&testFinished, 1) 692 }() 693 694 // Wait for the controller to finish the test function. 695 for atomic.LoadInt32(&testFinished) == 0 { 696 time.Sleep(time.Millisecond * 10) 697 } 698 699 return testError 700 } 701 } 702 703 func evaluateTestResults(ctx context.Context, ctrl *PersistentVolumeController, reactor *pvtesting.VolumeReactor, test controllerTest, t *testing.T) { 704 // Evaluate results 705 if err := reactor.CheckClaims(test.expectedClaims); err != nil { 706 t.Errorf("Test %q: %v", test.name, err) 707 708 } 709 if err := reactor.CheckVolumes(test.expectedVolumes); err != nil { 710 t.Errorf("Test %q: %v", test.name, err) 711 } 712 713 if err := checkEvents(t, ctx, test.expectedEvents, ctrl); err != nil { 714 t.Errorf("Test %q: %v", test.name, err) 715 } 716 } 717 718 // Test single call to syncClaim and syncVolume methods. 719 // For all tests: 720 // 1. Fill in the controller with initial data 721 // 2. Call the tested function (syncClaim/syncVolume) via 722 // controllerTest.testCall *once*. 723 // 3. Compare resulting volumes and claims with expected volumes and claims. 724 func runSyncTests(t *testing.T, ctx context.Context, tests []controllerTest, storageClasses []*storage.StorageClass, pods []*v1.Pod) { 725 doit := func(t *testing.T, test controllerTest) { 726 // Initialize the controller 727 client := &fake.Clientset{} 728 ctrl, err := newTestController(ctx, client, nil, true) 729 if err != nil { 730 t.Fatalf("Test %q construct persistent volume failed: %v", test.name, err) 731 } 732 reactor := newVolumeReactor(ctx, client, ctrl, nil, nil, test.errors) 733 for _, claim := range test.initialClaims { 734 if metav1.HasAnnotation(claim.ObjectMeta, annSkipLocalStore) { 735 continue 736 } 737 ctrl.claims.Add(claim) 738 } 739 for _, volume := range test.initialVolumes { 740 if metav1.HasAnnotation(volume.ObjectMeta, annSkipLocalStore) { 741 continue 742 } 743 ctrl.volumes.store.Add(volume) 744 } 745 reactor.AddClaims(test.initialClaims) 746 reactor.AddVolumes(test.initialVolumes) 747 748 // Inject classes into controller via a custom lister. 749 indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{}) 750 for _, class := range storageClasses { 751 indexer.Add(class) 752 } 753 ctrl.classLister = storagelisters.NewStorageClassLister(indexer) 754 755 podIndexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{}) 756 for _, pod := range pods { 757 podIndexer.Add(pod) 758 ctrl.podIndexer.Add(pod) 759 } 760 ctrl.podLister = corelisters.NewPodLister(podIndexer) 761 762 // Run the tested functions 763 err = test.test(ctrl, reactor.VolumeReactor, test) 764 if err != nil { 765 t.Errorf("Test %q failed: %v", test.name, err) 766 } 767 768 // Wait for the target state 769 err = reactor.waitTest(test) 770 if err != nil { 771 t.Errorf("Test %q failed: %v", test.name, err) 772 } 773 774 evaluateTestResults(ctx, ctrl, reactor.VolumeReactor, test, t) 775 } 776 777 for _, test := range tests { 778 test := test 779 t.Run(test.name, func(t *testing.T) { 780 doit(t, test) 781 }) 782 } 783 } 784 785 // Test multiple calls to syncClaim/syncVolume and periodic sync of all 786 // volume/claims. For all tests, the test follows this pattern: 787 // 0. Load the controller with initial data. 788 // 1. Call controllerTest.testCall() once as in TestSync() 789 // 2. For all volumes/claims changed by previous syncVolume/syncClaim calls, 790 // call appropriate syncVolume/syncClaim (simulating "volume/claim changed" 791 // events). Go to 2. if these calls change anything. 792 // 3. When all changes are processed and no new changes were made, call 793 // syncVolume/syncClaim on all volumes/claims (simulating "periodic sync"). 794 // 4. If some changes were done by step 3., go to 2. (simulation of 795 // "volume/claim updated" events, eventually performing step 3. again) 796 // 5. When 3. does not do any changes, finish the tests and compare final set 797 // of volumes/claims with expected claims/volumes and report differences. 798 // 799 // Some limit of calls in enforced to prevent endless loops. 800 func runMultisyncTests(t *testing.T, ctx context.Context, tests []controllerTest, storageClasses []*storage.StorageClass, defaultStorageClass string) { 801 logger := klog.FromContext(ctx) 802 run := func(t *testing.T, test controllerTest) { 803 logger.V(4).Info("Starting multisync test", "testName", test.name) 804 805 // Initialize the controller 806 client := &fake.Clientset{} 807 ctrl, err := newTestController(ctx, client, nil, true) 808 if err != nil { 809 t.Fatalf("Test %q construct persistent volume failed: %v", test.name, err) 810 } 811 812 // Inject classes into controller via a custom lister. 813 indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{}) 814 for _, class := range storageClasses { 815 indexer.Add(class) 816 } 817 ctrl.classLister = storagelisters.NewStorageClassLister(indexer) 818 819 reactor := newVolumeReactor(ctx, client, ctrl, nil, nil, test.errors) 820 for _, claim := range test.initialClaims { 821 ctrl.claims.Add(claim) 822 } 823 for _, volume := range test.initialVolumes { 824 ctrl.volumes.store.Add(volume) 825 } 826 reactor.AddClaims(test.initialClaims) 827 reactor.AddVolumes(test.initialVolumes) 828 829 // Run the tested function 830 err = test.test(ctrl, reactor.VolumeReactor, test) 831 if err != nil { 832 t.Errorf("Test %q failed: %v", test.name, err) 833 } 834 835 // Simulate any "changed" events and "periodical sync" until we reach a 836 // stable state. 837 firstSync := true 838 counter := 0 839 for { 840 counter++ 841 logger.V(4).Info("Test", "testName", test.name, "iteration", counter) 842 843 if counter > 100 { 844 t.Errorf("Test %q failed: too many iterations", test.name) 845 break 846 } 847 848 // Wait for all goroutines to finish 849 reactor.waitForIdle() 850 851 obj := reactor.PopChange(ctx) 852 if obj == nil { 853 // Nothing was changed, should we exit? 854 if firstSync || reactor.GetChangeCount() > 0 { 855 // There were some changes after the last "periodic sync". 856 // Simulate "periodic sync" of everything (until it produces 857 // no changes). 858 firstSync = false 859 logger.V(4).Info("Test simulating periodical sync of all claims and volumes", "testName", test.name) 860 reactor.SyncAll() 861 } else { 862 // Last sync did not produce any updates, the test reached 863 // stable state -> finish. 864 break 865 } 866 } 867 // waiting here cools down exponential backoff 868 time.Sleep(600 * time.Millisecond) 869 870 // There were some changes, process them 871 switch obj.(type) { 872 case *v1.PersistentVolumeClaim: 873 claim := obj.(*v1.PersistentVolumeClaim) 874 // Simulate "claim updated" event 875 ctrl.claims.Update(claim) 876 err = ctrl.syncClaim(context.TODO(), claim) 877 if err != nil { 878 if err == pvtesting.ErrVersionConflict { 879 // Ignore version errors 880 logger.V(4).Info("Test intentionally ignores version error") 881 } else { 882 t.Errorf("Error calling syncClaim: %v", err) 883 // Finish the loop on the first error 884 break 885 } 886 } 887 // Process generated changes 888 continue 889 case *v1.PersistentVolume: 890 volume := obj.(*v1.PersistentVolume) 891 // Simulate "volume updated" event 892 ctrl.volumes.store.Update(volume) 893 err = ctrl.syncVolume(context.TODO(), volume) 894 if err != nil { 895 if err == pvtesting.ErrVersionConflict { 896 // Ignore version errors 897 logger.V(4).Info("Test intentionally ignores version error") 898 } else { 899 t.Errorf("Error calling syncVolume: %v", err) 900 // Finish the loop on the first error 901 break 902 } 903 } 904 // Process generated changes 905 continue 906 } 907 } 908 evaluateTestResults(ctx, ctrl, reactor.VolumeReactor, test, t) 909 logger.V(4).Info("Test finished after iterations", "testName", test.name, "iterations", counter) 910 } 911 912 for _, test := range tests { 913 test := test 914 t.Run(test.name, func(t *testing.T) { 915 t.Parallel() 916 run(t, test) 917 }) 918 } 919 } 920 921 // Dummy volume plugin for provisioning, deletion and recycling. It contains 922 // lists of expected return values to simulate errors. 923 type mockVolumePlugin struct { 924 provisionCalls []provisionCall 925 provisionCallCounter int 926 deleteCalls []error 927 deleteCallCounter int 928 recycleCalls []error 929 recycleCallCounter int 930 provisionOptions volume.VolumeOptions 931 } 932 933 type provisionCall struct { 934 expectedParameters map[string]string 935 ret error 936 } 937 938 var _ volume.VolumePlugin = &mockVolumePlugin{} 939 var _ volume.RecyclableVolumePlugin = &mockVolumePlugin{} 940 var _ volume.DeletableVolumePlugin = &mockVolumePlugin{} 941 var _ volume.ProvisionableVolumePlugin = &mockVolumePlugin{} 942 943 func (plugin *mockVolumePlugin) Init(host volume.VolumeHost) error { 944 return nil 945 } 946 947 func (plugin *mockVolumePlugin) GetPluginName() string { 948 return mockPluginName 949 } 950 951 func (plugin *mockVolumePlugin) GetVolumeName(spec *volume.Spec) (string, error) { 952 return spec.Name(), nil 953 } 954 955 func (plugin *mockVolumePlugin) CanSupport(spec *volume.Spec) bool { 956 return true 957 } 958 959 func (plugin *mockVolumePlugin) RequiresRemount(spec *volume.Spec) bool { 960 return false 961 } 962 963 func (plugin *mockVolumePlugin) SupportsMountOption() bool { 964 return false 965 } 966 967 func (plugin *mockVolumePlugin) SupportsBulkVolumeVerification() bool { 968 return false 969 } 970 971 func (plugin *mockVolumePlugin) ConstructVolumeSpec(volumeName, mountPath string) (volume.ReconstructedVolume, error) { 972 return volume.ReconstructedVolume{}, nil 973 } 974 975 func (plugin *mockVolumePlugin) SupportsSELinuxContextMount(spec *volume.Spec) (bool, error) { 976 return false, nil 977 } 978 979 func (plugin *mockVolumePlugin) NewMounter(spec *volume.Spec, podRef *v1.Pod, opts volume.VolumeOptions) (volume.Mounter, error) { 980 return nil, fmt.Errorf("Mounter is not supported by this plugin") 981 } 982 983 func (plugin *mockVolumePlugin) NewUnmounter(name string, podUID types.UID) (volume.Unmounter, error) { 984 return nil, fmt.Errorf("Unmounter is not supported by this plugin") 985 } 986 987 // Provisioner interfaces 988 989 func (plugin *mockVolumePlugin) NewProvisioner(logger klog.Logger, options volume.VolumeOptions) (volume.Provisioner, error) { 990 if len(plugin.provisionCalls) > 0 { 991 // mockVolumePlugin directly implements Provisioner interface 992 logger.V(4).Info("Mock plugin NewProvisioner called, returning mock provisioner") 993 plugin.provisionOptions = options 994 return plugin, nil 995 } else { 996 return nil, fmt.Errorf("Mock plugin error: no provisionCalls configured") 997 } 998 } 999 1000 func (plugin *mockVolumePlugin) Provision(selectedNode *v1.Node, allowedTopologies []v1.TopologySelectorTerm) (*v1.PersistentVolume, error) { 1001 if len(plugin.provisionCalls) <= plugin.provisionCallCounter { 1002 return nil, fmt.Errorf("Mock plugin error: unexpected provisioner call %d", plugin.provisionCallCounter) 1003 } 1004 var pv *v1.PersistentVolume 1005 call := plugin.provisionCalls[plugin.provisionCallCounter] 1006 if !reflect.DeepEqual(call.expectedParameters, plugin.provisionOptions.Parameters) { 1007 klog.TODO().Error(nil, "Invalid provisioner call", "gotOptions", plugin.provisionOptions.Parameters, "expectedOptions", call.expectedParameters) 1008 return nil, fmt.Errorf("Mock plugin error: invalid provisioner call") 1009 } 1010 if call.ret == nil { 1011 // Create a fake PV with known GCE volume (to match expected volume) 1012 capacity := plugin.provisionOptions.PVC.Spec.Resources.Requests[v1.ResourceName(v1.ResourceStorage)] 1013 accessModes := plugin.provisionOptions.PVC.Spec.AccessModes 1014 pv = &v1.PersistentVolume{ 1015 ObjectMeta: metav1.ObjectMeta{ 1016 Name: plugin.provisionOptions.PVName, 1017 }, 1018 Spec: v1.PersistentVolumeSpec{ 1019 Capacity: v1.ResourceList{ 1020 v1.ResourceName(v1.ResourceStorage): capacity, 1021 }, 1022 AccessModes: accessModes, 1023 PersistentVolumeReclaimPolicy: plugin.provisionOptions.PersistentVolumeReclaimPolicy, 1024 PersistentVolumeSource: v1.PersistentVolumeSource{ 1025 GCEPersistentDisk: &v1.GCEPersistentDiskVolumeSource{}, 1026 }, 1027 }, 1028 Status: v1.PersistentVolumeStatus{ 1029 Phase: v1.VolumeAvailable, 1030 }, 1031 } 1032 pv.Spec.VolumeMode = plugin.provisionOptions.PVC.Spec.VolumeMode 1033 } 1034 1035 plugin.provisionCallCounter++ 1036 klog.TODO().V(4).Info("Mock plugin Provision call nr", "provisionCallCounter", plugin.provisionCallCounter, "pv", klog.KObj(pv), "err", call.ret) 1037 return pv, call.ret 1038 } 1039 1040 // Deleter interfaces 1041 1042 func (plugin *mockVolumePlugin) NewDeleter(logger klog.Logger, spec *volume.Spec) (volume.Deleter, error) { 1043 if len(plugin.deleteCalls) > 0 { 1044 // mockVolumePlugin directly implements Deleter interface 1045 logger.V(4).Info("Mock plugin NewDeleter called, returning mock deleter") 1046 return plugin, nil 1047 } else { 1048 return nil, fmt.Errorf("Mock plugin error: no deleteCalls configured") 1049 } 1050 } 1051 1052 func (plugin *mockVolumePlugin) Delete() error { 1053 if len(plugin.deleteCalls) <= plugin.deleteCallCounter { 1054 return fmt.Errorf("Mock plugin error: unexpected deleter call %d", plugin.deleteCallCounter) 1055 } 1056 ret := plugin.deleteCalls[plugin.deleteCallCounter] 1057 plugin.deleteCallCounter++ 1058 klog.TODO().V(4).Info("Mock plugin Delete call nr", "deleteCallCounter", plugin.deleteCallCounter, "err", ret) 1059 return ret 1060 } 1061 1062 // Volume interfaces 1063 1064 func (plugin *mockVolumePlugin) GetPath() string { 1065 return "" 1066 } 1067 1068 func (plugin *mockVolumePlugin) GetMetrics() (*volume.Metrics, error) { 1069 return nil, nil 1070 } 1071 1072 // Recycler interfaces 1073 1074 func (plugin *mockVolumePlugin) Recycle(pvName string, spec *volume.Spec, eventRecorder recyclerclient.RecycleEventRecorder) error { 1075 if len(plugin.recycleCalls) == 0 { 1076 return fmt.Errorf("Mock plugin error: no recycleCalls configured") 1077 } 1078 1079 if len(plugin.recycleCalls) <= plugin.recycleCallCounter { 1080 return fmt.Errorf("Mock plugin error: unexpected recycle call %d", plugin.recycleCallCounter) 1081 } 1082 ret := plugin.recycleCalls[plugin.recycleCallCounter] 1083 plugin.recycleCallCounter++ 1084 klog.TODO().V(4).Info("Mock plugin Recycle call nr", "recycleCallCounter", plugin.recycleCallCounter, "err", ret) 1085 return ret 1086 }