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  }