k8s.io/kubernetes@v1.29.3/pkg/controller/volume/persistentvolume/pv_controller_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  	"errors"
    22  	"reflect"
    23  	"testing"
    24  	"time"
    25  
    26  	v1 "k8s.io/api/core/v1"
    27  	storagev1 "k8s.io/api/storage/v1"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/apimachinery/pkg/util/wait"
    30  	"k8s.io/apimachinery/pkg/watch"
    31  	utilfeature "k8s.io/apiserver/pkg/util/feature"
    32  	"k8s.io/client-go/informers"
    33  	"k8s.io/client-go/kubernetes/fake"
    34  	storagelisters "k8s.io/client-go/listers/storage/v1"
    35  	core "k8s.io/client-go/testing"
    36  	"k8s.io/client-go/tools/cache"
    37  	featuregatetesting "k8s.io/component-base/featuregate/testing"
    38  	"k8s.io/component-helpers/storage/volume"
    39  	csitrans "k8s.io/csi-translation-lib"
    40  	"k8s.io/klog/v2/ktesting"
    41  	"k8s.io/kubernetes/pkg/controller"
    42  	pvtesting "k8s.io/kubernetes/pkg/controller/volume/persistentvolume/testing"
    43  	"k8s.io/kubernetes/pkg/features"
    44  	"k8s.io/kubernetes/pkg/volume/csimigration"
    45  	"k8s.io/kubernetes/pkg/volume/util"
    46  )
    47  
    48  // Test the real controller methods (add/update/delete claim/volume) with
    49  // a fake API server.
    50  // There is no controller API to 'initiate syncAll now', therefore these tests
    51  // can't reliably simulate periodic sync of volumes/claims - it would be
    52  // either very timing-sensitive or slow to wait for real periodic sync.
    53  func TestControllerSync(t *testing.T) {
    54  	// Default enable the HonorPVReclaimPolicy feature gate.
    55  	defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.HonorPVReclaimPolicy, true)()
    56  	tests := []controllerTest{
    57  		// [Unit test set 5] - controller tests.
    58  		// We test the controller as if
    59  		// it was connected to real API server, i.e. we call add/update/delete
    60  		// Claim/Volume methods. Also, all changes to volumes and claims are
    61  		// sent to add/update/delete Claim/Volume as real controller would do.
    62  		{
    63  			// addClaim gets a new claim. Check it's bound to a volume.
    64  			name:            "5-2 - complete bind",
    65  			initialVolumes:  newVolumeArray("volume5-2", "1Gi", "", "", v1.VolumeAvailable, v1.PersistentVolumeReclaimRetain, classEmpty),
    66  			expectedVolumes: newVolumeArray("volume5-2", "1Gi", "uid5-2", "claim5-2", v1.VolumeBound, v1.PersistentVolumeReclaimRetain, classEmpty, volume.AnnBoundByController),
    67  			initialClaims:   noclaims, /* added in testAddClaim5_2 */
    68  			expectedClaims:  newClaimArray("claim5-2", "uid5-2", "1Gi", "volume5-2", v1.ClaimBound, nil, volume.AnnBoundByController, volume.AnnBindCompleted),
    69  			expectedEvents:  noevents,
    70  			errors:          noerrors,
    71  			// Custom test function that generates an add event
    72  			test: func(ctrl *PersistentVolumeController, reactor *pvtesting.VolumeReactor, test controllerTest) error {
    73  				claim := newClaim("claim5-2", "uid5-2", "1Gi", "", v1.ClaimPending, nil)
    74  				reactor.AddClaimEvent(claim)
    75  				return nil
    76  			},
    77  		},
    78  		{
    79  			name:            "5-2-2 - complete bind when PV and PVC both exist",
    80  			initialVolumes:  newVolumeArray("volume5-2", "1Gi", "", "", v1.VolumeAvailable, v1.PersistentVolumeReclaimRetain, classEmpty),
    81  			expectedVolumes: newVolumeArray("volume5-2", "1Gi", "uid5-2", "claim5-2", v1.VolumeBound, v1.PersistentVolumeReclaimRetain, classEmpty, volume.AnnBoundByController),
    82  			initialClaims:   newClaimArray("claim5-2", "uid5-2", "1Gi", "", v1.ClaimPending, nil),
    83  			expectedClaims:  newClaimArray("claim5-2", "uid5-2", "1Gi", "volume5-2", v1.ClaimBound, nil, volume.AnnBoundByController, volume.AnnBindCompleted),
    84  			expectedEvents:  noevents,
    85  			errors:          noerrors,
    86  			test: func(ctrl *PersistentVolumeController, reactor *pvtesting.VolumeReactor, test controllerTest) error {
    87  				return nil
    88  			},
    89  		},
    90  		{
    91  			name:            "5-2-3 - complete bind when PV and PVC both exist and PV has AnnPreResizeCapacity annotation",
    92  			initialVolumes:  volumesWithAnnotation(util.AnnPreResizeCapacity, "1Gi", newVolumeArray("volume5-2", "2Gi", "", "", v1.VolumeAvailable, v1.PersistentVolumeReclaimRetain, classEmpty, volume.AnnBoundByController)),
    93  			expectedVolumes: volumesWithAnnotation(util.AnnPreResizeCapacity, "1Gi", newVolumeArray("volume5-2", "2Gi", "uid5-2", "claim5-2", v1.VolumeBound, v1.PersistentVolumeReclaimRetain, classEmpty, volume.AnnBoundByController)),
    94  			initialClaims:   withExpectedCapacity("2Gi", newClaimArray("claim5-2", "uid5-2", "2Gi", "", v1.ClaimPending, nil)),
    95  			expectedClaims:  withExpectedCapacity("1Gi", newClaimArray("claim5-2", "uid5-2", "2Gi", "volume5-2", v1.ClaimBound, nil, volume.AnnBoundByController, volume.AnnBindCompleted)),
    96  			expectedEvents:  noevents,
    97  			errors:          noerrors,
    98  			test: func(ctrl *PersistentVolumeController, reactor *pvtesting.VolumeReactor, test controllerTest) error {
    99  				return nil
   100  			},
   101  		},
   102  		{
   103  			// deleteClaim with a bound claim makes bound volume released.
   104  			name:            "5-3 - delete claim",
   105  			initialVolumes:  newVolumeArray("volume5-3", "10Gi", "uid5-3", "claim5-3", v1.VolumeBound, v1.PersistentVolumeReclaimRetain, classEmpty, volume.AnnBoundByController),
   106  			expectedVolumes: newVolumeArray("volume5-3", "10Gi", "uid5-3", "claim5-3", v1.VolumeReleased, v1.PersistentVolumeReclaimRetain, classEmpty, volume.AnnBoundByController),
   107  			initialClaims:   newClaimArray("claim5-3", "uid5-3", "1Gi", "volume5-3", v1.ClaimBound, nil, volume.AnnBoundByController, volume.AnnBindCompleted),
   108  			expectedClaims:  noclaims,
   109  			expectedEvents:  noevents,
   110  			errors:          noerrors,
   111  			// Custom test function that generates a delete event
   112  			test: func(ctrl *PersistentVolumeController, reactor *pvtesting.VolumeReactor, test controllerTest) error {
   113  				obj := ctrl.claims.List()[0]
   114  				claim := obj.(*v1.PersistentVolumeClaim)
   115  				reactor.DeleteClaimEvent(claim)
   116  				return nil
   117  			},
   118  		},
   119  		{
   120  			// deleteVolume with a bound volume. Check the claim is Lost.
   121  			name:            "5-4 - delete volume",
   122  			initialVolumes:  newVolumeArray("volume5-4", "1Gi", "uid5-4", "claim5-4", v1.VolumeBound, v1.PersistentVolumeReclaimRetain, classEmpty),
   123  			expectedVolumes: novolumes,
   124  			initialClaims:   newClaimArray("claim5-4", "uid5-4", "1Gi", "volume5-4", v1.ClaimBound, nil, volume.AnnBoundByController, volume.AnnBindCompleted),
   125  			expectedClaims:  newClaimArray("claim5-4", "uid5-4", "1Gi", "volume5-4", v1.ClaimLost, nil, volume.AnnBoundByController, volume.AnnBindCompleted),
   126  			expectedEvents:  []string{"Warning ClaimLost"},
   127  			errors:          noerrors,
   128  			// Custom test function that generates a delete event
   129  			test: func(ctrl *PersistentVolumeController, reactor *pvtesting.VolumeReactor, test controllerTest) error {
   130  				obj := ctrl.volumes.store.List()[0]
   131  				volume := obj.(*v1.PersistentVolume)
   132  				reactor.DeleteVolumeEvent(volume)
   133  				return nil
   134  			},
   135  		},
   136  		{
   137  			// deleteClaim with a bound claim makes bound volume released with external deleter.
   138  			// delete the corresponding volume from apiserver, and report latency metric
   139  			name: "5-5 - delete claim and delete volume report metric",
   140  			initialVolumes: volumesWithAnnotation(volume.AnnDynamicallyProvisioned, "gcr.io/vendor-csi",
   141  				newVolumeArray("volume5-5", "10Gi", "uid5-5", "claim5-5", v1.VolumeBound, v1.PersistentVolumeReclaimDelete, classExternal, volume.AnnBoundByController)),
   142  			expectedVolumes: novolumes,
   143  			initialClaims: claimWithAnnotation(volume.AnnStorageProvisioner, "gcr.io/vendor-csi",
   144  				newClaimArray("claim5-5", "uid5-5", "1Gi", "volume5-5", v1.ClaimBound, &classExternal, volume.AnnBoundByController, volume.AnnBindCompleted)),
   145  			expectedClaims: noclaims,
   146  			expectedEvents: noevents,
   147  			errors:         noerrors,
   148  			// Custom test function that generates a delete claim event which should have been caught by
   149  			// "deleteClaim" to remove the claim from controller's cache, after that, a volume deleted
   150  			// event will be generated to trigger "deleteVolume" call for metric reporting
   151  			test: func(ctrl *PersistentVolumeController, reactor *pvtesting.VolumeReactor, test controllerTest) error {
   152  				test.initialVolumes[0].Annotations[volume.AnnDynamicallyProvisioned] = "gcr.io/vendor-csi"
   153  				obj := ctrl.claims.List()[0]
   154  				claim := obj.(*v1.PersistentVolumeClaim)
   155  				reactor.DeleteClaimEvent(claim)
   156  				err := wait.Poll(10*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
   157  					return len(ctrl.claims.ListKeys()) == 0, nil
   158  				})
   159  				if err != nil {
   160  					return err
   161  				}
   162  				// claim has been removed from controller's cache, generate a volume deleted event
   163  				volume := ctrl.volumes.store.List()[0].(*v1.PersistentVolume)
   164  				reactor.DeleteVolumeEvent(volume)
   165  				return nil
   166  			},
   167  		},
   168  		{
   169  			// deleteClaim with a bound claim makes bound volume released with external deleter pending
   170  			// there should be an entry in operation timestamps cache in controller
   171  			name:            "5-6 - delete claim and waiting for external volume deletion",
   172  			initialVolumes:  volumesWithAnnotation(volume.AnnDynamicallyProvisioned, "gcr.io/vendor-csi", []*v1.PersistentVolume{newExternalProvisionedVolume("volume5-6", "10Gi", "uid5-6", "claim5-6", v1.VolumeBound, v1.PersistentVolumeReclaimDelete, classExternal, "fake.driver.csi", nil, volume.AnnBoundByController)}),
   173  			expectedVolumes: volumesWithAnnotation(volume.AnnDynamicallyProvisioned, "gcr.io/vendor-csi", []*v1.PersistentVolume{newExternalProvisionedVolume("volume5-6", "10Gi", "uid5-6", "claim5-6", v1.VolumeReleased, v1.PersistentVolumeReclaimDelete, classExternal, "fake.driver.csi", nil, volume.AnnBoundByController)}),
   174  			initialClaims: claimWithAnnotation(volume.AnnStorageProvisioner, "gcr.io/vendor-csi",
   175  				newClaimArray("claim5-6", "uid5-6", "1Gi", "volume5-6", v1.ClaimBound, &classExternal, volume.AnnBoundByController, volume.AnnBindCompleted)),
   176  			expectedClaims: noclaims,
   177  			expectedEvents: noevents,
   178  			errors:         noerrors,
   179  			// Custom test function that generates a delete claim event which should have been caught by
   180  			// "deleteClaim" to remove the claim from controller's cache and mark bound volume to be released
   181  			test: func(ctrl *PersistentVolumeController, reactor *pvtesting.VolumeReactor, test controllerTest) error {
   182  				// should have been provisioned by external provisioner
   183  				obj := ctrl.claims.List()[0]
   184  				claim := obj.(*v1.PersistentVolumeClaim)
   185  				reactor.DeleteClaimEvent(claim)
   186  				// wait until claim is cleared from cache, i.e., deleteClaim is called
   187  				err := wait.Poll(10*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
   188  					return len(ctrl.claims.ListKeys()) == 0, nil
   189  				})
   190  				if err != nil {
   191  					return err
   192  				}
   193  				// wait for volume delete operation to appear once volumeWorker() runs
   194  				return wait.PollImmediate(10*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
   195  					// make sure the operation timestamp cache is NOT empty
   196  					if ctrl.operationTimestamps.Has("volume5-6") {
   197  						return true, nil
   198  					}
   199  					t.Logf("missing volume5-6 from timestamp cache, will retry")
   200  					return false, nil
   201  				})
   202  			},
   203  		},
   204  		{
   205  			// deleteVolume event issued before deleteClaim, no metric should have been reported
   206  			// and no delete operation start timestamp should be inserted into controller.operationTimestamps cache
   207  			name:            "5-7 - delete volume event makes claim lost, delete claim event will not report metric",
   208  			initialVolumes:  newVolumeArray("volume5-7", "10Gi", "uid5-7", "claim5-7", v1.VolumeBound, v1.PersistentVolumeReclaimDelete, classExternal, volume.AnnBoundByController, volume.AnnDynamicallyProvisioned),
   209  			expectedVolumes: novolumes,
   210  			initialClaims: claimWithAnnotation(volume.AnnStorageProvisioner, "gcr.io/vendor-csi",
   211  				newClaimArray("claim5-7", "uid5-7", "1Gi", "volume5-7", v1.ClaimBound, &classExternal, volume.AnnBoundByController, volume.AnnBindCompleted)),
   212  			expectedClaims: noclaims,
   213  			expectedEvents: []string{"Warning ClaimLost"},
   214  			errors:         noerrors,
   215  			// Custom test function that generates a delete claim event which should have been caught by
   216  			// "deleteClaim" to remove the claim from controller's cache and mark bound volume to be released
   217  			test: func(ctrl *PersistentVolumeController, reactor *pvtesting.VolumeReactor, test controllerTest) error {
   218  				volume := ctrl.volumes.store.List()[0].(*v1.PersistentVolume)
   219  				reactor.DeleteVolumeEvent(volume)
   220  				err := wait.Poll(10*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
   221  					return len(ctrl.volumes.store.ListKeys()) == 0, nil
   222  				})
   223  				if err != nil {
   224  					return err
   225  				}
   226  
   227  				// Wait for the PVC to get fully processed. This avoids races between PV controller and DeleteClaimEvent
   228  				// below.
   229  				err = wait.Poll(10*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
   230  					obj := ctrl.claims.List()[0]
   231  					claim := obj.(*v1.PersistentVolumeClaim)
   232  					return claim.Status.Phase == v1.ClaimLost, nil
   233  				})
   234  				if err != nil {
   235  					return err
   236  				}
   237  
   238  				// trying to remove the claim as well
   239  				obj := ctrl.claims.List()[0]
   240  				claim := obj.(*v1.PersistentVolumeClaim)
   241  				reactor.DeleteClaimEvent(claim)
   242  				// wait until claim is cleared from cache, i.e., deleteClaim is called
   243  				err = wait.Poll(10*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
   244  					return len(ctrl.claims.ListKeys()) == 0, nil
   245  				})
   246  				if err != nil {
   247  					return err
   248  				}
   249  				// make sure operation timestamp cache is empty
   250  				if ctrl.operationTimestamps.Has("volume5-7") {
   251  					return errors.New("failed checking timestamp cache")
   252  				}
   253  				return nil
   254  			},
   255  		},
   256  		{
   257  			// delete a claim waiting for being bound cleans up provision(volume ref == "") entry from timestamp cache
   258  			name:            "5-8 - delete claim cleans up operation timestamp cache for provision",
   259  			initialVolumes:  novolumes,
   260  			expectedVolumes: novolumes,
   261  			initialClaims: claimWithAnnotation(volume.AnnStorageProvisioner, "gcr.io/vendor-csi",
   262  				newClaimArray("claim5-8", "uid5-8", "1Gi", "", v1.ClaimPending, &classExternal)),
   263  			expectedClaims: noclaims,
   264  			expectedEvents: []string{"Normal ExternalProvisioning"},
   265  			errors:         noerrors,
   266  			// Custom test function that generates a delete claim event which should have been caught by
   267  			// "deleteClaim" to remove the claim from controller's cache and mark bound volume to be released
   268  			test: func(ctrl *PersistentVolumeController, reactor *pvtesting.VolumeReactor, test controllerTest) error {
   269  				// wait until the provision timestamp has been inserted
   270  				err := wait.Poll(10*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
   271  					return ctrl.operationTimestamps.Has("default/claim5-8"), nil
   272  				})
   273  				if err != nil {
   274  					return err
   275  				}
   276  				// delete the claim
   277  				obj := ctrl.claims.List()[0]
   278  				claim := obj.(*v1.PersistentVolumeClaim)
   279  				reactor.DeleteClaimEvent(claim)
   280  				// wait until claim is cleared from cache, i.e., deleteClaim is called
   281  				err = wait.Poll(10*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
   282  					return len(ctrl.claims.ListKeys()) == 0, nil
   283  				})
   284  				if err != nil {
   285  					return err
   286  				}
   287  				// make sure operation timestamp cache is empty
   288  				if ctrl.operationTimestamps.Has("default/claim5-8") {
   289  					return errors.New("failed checking timestamp cache")
   290  				}
   291  				return nil
   292  			},
   293  		},
   294  		{
   295  			// Test that the finalizer gets removed if CSI migration is disabled. The in-tree finalizer is added
   296  			// back on the PV since migration is disabled.
   297  			name: "5-9 - volume has its external PV deletion protection finalizer removed as CSI migration is disabled",
   298  			initialVolumes: volumesWithFinalizers(
   299  				volumesWithAnnotation(volume.AnnMigratedTo, "pd.csi.storage.gke.io",
   300  					newVolumeArray("volume-5-9", "1Gi", "", "", v1.VolumeAvailable, v1.PersistentVolumeReclaimDelete, classEmpty, volume.AnnDynamicallyProvisioned)),
   301  				[]string{volume.PVDeletionProtectionFinalizer},
   302  			),
   303  			expectedVolumes: volumesWithFinalizers(newVolumeArray("volume-5-9", "1Gi", "", "", v1.VolumeAvailable, v1.PersistentVolumeReclaimDelete, classEmpty, volume.AnnDynamicallyProvisioned), []string{volume.PVDeletionInTreeProtectionFinalizer}),
   304  			initialClaims:   noclaims,
   305  			expectedClaims:  noclaims,
   306  			expectedEvents:  noevents,
   307  			errors:          noerrors,
   308  			test: func(ctrl *PersistentVolumeController, reactor *pvtesting.VolumeReactor, test controllerTest) error {
   309  				return nil
   310  			},
   311  		},
   312  	}
   313  	logger, ctx := ktesting.NewTestContext(t)
   314  	doit := func(test controllerTest) {
   315  		// Initialize the controller
   316  		client := &fake.Clientset{}
   317  
   318  		fakeVolumeWatch := watch.NewFake()
   319  		client.PrependWatchReactor("persistentvolumes", core.DefaultWatchReactor(fakeVolumeWatch, nil))
   320  		fakeClaimWatch := watch.NewFake()
   321  		client.PrependWatchReactor("persistentvolumeclaims", core.DefaultWatchReactor(fakeClaimWatch, nil))
   322  		client.PrependWatchReactor("storageclasses", core.DefaultWatchReactor(watch.NewFake(), nil))
   323  		client.PrependWatchReactor("nodes", core.DefaultWatchReactor(watch.NewFake(), nil))
   324  		client.PrependWatchReactor("pods", core.DefaultWatchReactor(watch.NewFake(), nil))
   325  
   326  		informers := informers.NewSharedInformerFactory(client, controller.NoResyncPeriodFunc())
   327  		ctrl, err := newTestController(ctx, client, informers, true)
   328  		if err != nil {
   329  			t.Fatalf("Test %q construct persistent volume failed: %v", test.name, err)
   330  		}
   331  
   332  		// Inject storage classes into controller via a custom lister for test [5-5]
   333  		storageClasses := []*storagev1.StorageClass{
   334  			makeStorageClass(classExternal, &modeImmediate),
   335  		}
   336  
   337  		storageClasses[0].Provisioner = "gcr.io/vendor-csi"
   338  		indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{})
   339  		for _, class := range storageClasses {
   340  			indexer.Add(class)
   341  		}
   342  		ctrl.classLister = storagelisters.NewStorageClassLister(indexer)
   343  
   344  		reactor := newVolumeReactor(ctx, client, ctrl, fakeVolumeWatch, fakeClaimWatch, test.errors)
   345  		for _, claim := range test.initialClaims {
   346  			claim = claim.DeepCopy()
   347  			reactor.AddClaim(claim)
   348  			go func(claim *v1.PersistentVolumeClaim) {
   349  				fakeClaimWatch.Add(claim)
   350  			}(claim)
   351  		}
   352  		for _, volume := range test.initialVolumes {
   353  			volume = volume.DeepCopy()
   354  			reactor.AddVolume(volume)
   355  			go func(volume *v1.PersistentVolume) {
   356  				fakeVolumeWatch.Add(volume)
   357  			}(volume)
   358  		}
   359  
   360  		// Start the controller
   361  		ctx, cancel := context.WithCancel(context.TODO())
   362  		informers.Start(ctx.Done())
   363  		informers.WaitForCacheSync(ctx.Done())
   364  		go ctrl.Run(ctx)
   365  
   366  		// Wait for the controller to pass initial sync and fill its caches.
   367  		err = wait.Poll(10*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
   368  			return len(ctrl.claims.ListKeys()) >= len(test.initialClaims) &&
   369  				len(ctrl.volumes.store.ListKeys()) >= len(test.initialVolumes), nil
   370  		})
   371  		if err != nil {
   372  			t.Errorf("Test %q controller sync failed: %v", test.name, err)
   373  		}
   374  		logger.V(4).Info("controller synced, starting test")
   375  
   376  		// Call the tested function
   377  		err = test.test(ctrl, reactor.VolumeReactor, test)
   378  		if err != nil {
   379  			t.Errorf("Test %q initial test call failed: %v", test.name, err)
   380  		}
   381  		// Simulate a periodic resync, just in case some events arrived in a
   382  		// wrong order.
   383  		ctrl.resync(ctx)
   384  
   385  		err = reactor.waitTest(test)
   386  		if err != nil {
   387  			t.Errorf("Failed to run test %s: %v", test.name, err)
   388  		}
   389  		cancel()
   390  
   391  		evaluateTestResults(ctx, ctrl, reactor.VolumeReactor, test, t)
   392  	}
   393  
   394  	for _, test := range tests {
   395  		test := test
   396  		t.Run(test.name, func(t *testing.T) {
   397  			doit(test)
   398  		})
   399  	}
   400  }
   401  
   402  func storeVersion(t *testing.T, prefix string, c cache.Store, version string, expectedReturn bool) {
   403  	pv := newVolume("pvName", "1Gi", "", "", v1.VolumeAvailable, v1.PersistentVolumeReclaimDelete, classEmpty)
   404  	pv.ResourceVersion = version
   405  	logger, _ := ktesting.NewTestContext(t)
   406  	ret, err := storeObjectUpdate(logger, c, pv, "volume")
   407  	if err != nil {
   408  		t.Errorf("%s: expected storeObjectUpdate to succeed, got: %v", prefix, err)
   409  	}
   410  	if expectedReturn != ret {
   411  		t.Errorf("%s: expected storeObjectUpdate to return %v, got: %v", prefix, expectedReturn, ret)
   412  	}
   413  
   414  	// find the stored version
   415  
   416  	pvObj, found, err := c.GetByKey("pvName")
   417  	if err != nil {
   418  		t.Errorf("expected volume 'pvName' in the cache, got error instead: %v", err)
   419  	}
   420  	if !found {
   421  		t.Errorf("expected volume 'pvName' in the cache but it was not found")
   422  	}
   423  	pv, ok := pvObj.(*v1.PersistentVolume)
   424  	if !ok {
   425  		t.Errorf("expected volume in the cache, got different object instead: %#v", pvObj)
   426  	}
   427  
   428  	if ret {
   429  		if pv.ResourceVersion != version {
   430  			t.Errorf("expected volume with version %s in the cache, got %s instead", version, pv.ResourceVersion)
   431  		}
   432  	} else {
   433  		if pv.ResourceVersion == version {
   434  			t.Errorf("expected volume with version other than %s in the cache, got %s instead", version, pv.ResourceVersion)
   435  		}
   436  	}
   437  }
   438  
   439  // TestControllerCache tests func storeObjectUpdate()
   440  func TestControllerCache(t *testing.T) {
   441  	// Cache under test
   442  	c := cache.NewStore(cache.DeletionHandlingMetaNamespaceKeyFunc)
   443  
   444  	// Store new PV
   445  	storeVersion(t, "Step1", c, "1", true)
   446  	// Store the same PV
   447  	storeVersion(t, "Step2", c, "1", true)
   448  	// Store newer PV
   449  	storeVersion(t, "Step3", c, "2", true)
   450  	// Store older PV - simulating old "PV updated" event or periodic sync with
   451  	// old data
   452  	storeVersion(t, "Step4", c, "1", false)
   453  	// Store newer PV - test integer parsing ("2" > "10" as string,
   454  	// while 2 < 10 as integers)
   455  	storeVersion(t, "Step5", c, "10", true)
   456  }
   457  
   458  func TestControllerCacheParsingError(t *testing.T) {
   459  	c := cache.NewStore(cache.DeletionHandlingMetaNamespaceKeyFunc)
   460  	// There must be something in the cache to compare with
   461  	storeVersion(t, "Step1", c, "1", true)
   462  
   463  	pv := newVolume("pvName", "1Gi", "", "", v1.VolumeAvailable, v1.PersistentVolumeReclaimDelete, classEmpty)
   464  	pv.ResourceVersion = "xxx"
   465  	logger, _ := ktesting.NewTestContext(t)
   466  	_, err := storeObjectUpdate(logger, c, pv, "volume")
   467  	if err == nil {
   468  		t.Errorf("Expected parsing error, got nil instead")
   469  	}
   470  }
   471  
   472  func makeStorageClass(scName string, mode *storagev1.VolumeBindingMode) *storagev1.StorageClass {
   473  	return &storagev1.StorageClass{
   474  		ObjectMeta: metav1.ObjectMeta{
   475  			Name: scName,
   476  		},
   477  		Provisioner:       "kubernetes.io/no-provisioner",
   478  		VolumeBindingMode: mode,
   479  	}
   480  }
   481  
   482  func makeDefaultStorageClass(scName string, mode *storagev1.VolumeBindingMode) *storagev1.StorageClass {
   483  	return &storagev1.StorageClass{
   484  		ObjectMeta: metav1.ObjectMeta{
   485  			Name: scName,
   486  			Annotations: map[string]string{
   487  				util.IsDefaultStorageClassAnnotation: "true",
   488  			},
   489  		},
   490  		Provisioner:       "kubernetes.io/no-provisioner",
   491  		VolumeBindingMode: mode,
   492  	}
   493  }
   494  
   495  func TestAnnealMigrationAnnotations(t *testing.T) {
   496  	// The gce-pd plugin is used to test a migrated plugin (as the feature is
   497  	// locked as of 1.25), and rbd is used as a non-migrated plugin (still alpha
   498  	// as of 1.25). As plugins are migrated, rbd should be changed to a non-
   499  	// migrated plugin. If there are no other non-migrated plugins, then those
   500  	// test cases are moot and they can be removed (keeping only the test cases
   501  	// with gce-pd).
   502  	const testPlugin = "non-migrated-plugin"
   503  	const migratedPlugin = "kubernetes.io/gce-pd"
   504  	const migratedDriver = "pd.csi.storage.gke.io"
   505  	const nonmigratedPlugin = "kubernetes.io/rbd"
   506  	const nonmigratedDriver = "rbd.csi.ceph.com"
   507  	tests := []struct {
   508  		name                 string
   509  		volumeAnnotations    map[string]string
   510  		expVolumeAnnotations map[string]string
   511  		claimAnnotations     map[string]string
   512  		expClaimAnnotations  map[string]string
   513  		testMigration        bool
   514  	}{
   515  		{
   516  			name:                 "migration on",
   517  			volumeAnnotations:    map[string]string{volume.AnnDynamicallyProvisioned: migratedPlugin},
   518  			expVolumeAnnotations: map[string]string{volume.AnnDynamicallyProvisioned: migratedPlugin, volume.AnnMigratedTo: migratedDriver},
   519  			claimAnnotations:     map[string]string{volume.AnnStorageProvisioner: migratedPlugin},
   520  			expClaimAnnotations:  map[string]string{volume.AnnStorageProvisioner: migratedPlugin, volume.AnnMigratedTo: migratedDriver},
   521  		},
   522  		{
   523  			name:                 "migration on with Beta storage provisioner annontation",
   524  			volumeAnnotations:    map[string]string{volume.AnnDynamicallyProvisioned: migratedPlugin},
   525  			expVolumeAnnotations: map[string]string{volume.AnnDynamicallyProvisioned: migratedPlugin, volume.AnnMigratedTo: migratedDriver},
   526  			claimAnnotations:     map[string]string{volume.AnnBetaStorageProvisioner: migratedPlugin},
   527  			expClaimAnnotations:  map[string]string{volume.AnnBetaStorageProvisioner: migratedPlugin, volume.AnnMigratedTo: migratedDriver},
   528  		},
   529  		{
   530  			name:                 "migration off",
   531  			volumeAnnotations:    map[string]string{volume.AnnDynamicallyProvisioned: nonmigratedPlugin},
   532  			expVolumeAnnotations: map[string]string{volume.AnnDynamicallyProvisioned: nonmigratedPlugin},
   533  			claimAnnotations:     map[string]string{volume.AnnStorageProvisioner: nonmigratedPlugin},
   534  			expClaimAnnotations:  map[string]string{volume.AnnStorageProvisioner: nonmigratedPlugin},
   535  		},
   536  		{
   537  			name:                 "migration off removes migrated to (rollback)",
   538  			volumeAnnotations:    map[string]string{volume.AnnDynamicallyProvisioned: nonmigratedPlugin, volume.AnnMigratedTo: nonmigratedDriver},
   539  			expVolumeAnnotations: map[string]string{volume.AnnDynamicallyProvisioned: nonmigratedPlugin},
   540  			claimAnnotations:     map[string]string{volume.AnnStorageProvisioner: nonmigratedPlugin, volume.AnnMigratedTo: nonmigratedDriver},
   541  			expClaimAnnotations:  map[string]string{volume.AnnStorageProvisioner: nonmigratedPlugin},
   542  		},
   543  		{
   544  			name:                 "migration off removes migrated to (rollback) with Beta storage provisioner annontation",
   545  			volumeAnnotations:    map[string]string{volume.AnnDynamicallyProvisioned: nonmigratedPlugin, volume.AnnMigratedTo: nonmigratedDriver},
   546  			expVolumeAnnotations: map[string]string{volume.AnnDynamicallyProvisioned: nonmigratedPlugin},
   547  			claimAnnotations:     map[string]string{volume.AnnBetaStorageProvisioner: nonmigratedPlugin, volume.AnnMigratedTo: nonmigratedDriver},
   548  			expClaimAnnotations:  map[string]string{volume.AnnBetaStorageProvisioner: nonmigratedPlugin},
   549  		},
   550  		{
   551  			name:                 "migration on, other plugin not affected",
   552  			volumeAnnotations:    map[string]string{volume.AnnDynamicallyProvisioned: testPlugin},
   553  			expVolumeAnnotations: map[string]string{volume.AnnDynamicallyProvisioned: testPlugin},
   554  			claimAnnotations:     map[string]string{volume.AnnStorageProvisioner: testPlugin},
   555  			expClaimAnnotations:  map[string]string{volume.AnnStorageProvisioner: testPlugin},
   556  		},
   557  		{
   558  			name:                 "not dynamically provisioned",
   559  			volumeAnnotations:    map[string]string{},
   560  			expVolumeAnnotations: map[string]string{},
   561  			claimAnnotations:     map[string]string{},
   562  			expClaimAnnotations:  map[string]string{},
   563  			testMigration:        false,
   564  		},
   565  		{
   566  			name:                 "nil annotations",
   567  			volumeAnnotations:    nil,
   568  			expVolumeAnnotations: nil,
   569  			claimAnnotations:     nil,
   570  			expClaimAnnotations:  nil,
   571  			testMigration:        false,
   572  		},
   573  	}
   574  
   575  	translator := csitrans.New()
   576  	cmpm := csimigration.NewPluginManager(translator, utilfeature.DefaultFeatureGate)
   577  	logger, _ := ktesting.NewTestContext(t)
   578  	for _, tc := range tests {
   579  		t.Run(tc.name, func(t *testing.T) {
   580  			if tc.volumeAnnotations != nil {
   581  				ann := tc.volumeAnnotations
   582  				updateMigrationAnnotations(logger, cmpm, translator, ann, false)
   583  				if !reflect.DeepEqual(tc.expVolumeAnnotations, ann) {
   584  					t.Errorf("got volume annoations: %v, but expected: %v", ann, tc.expVolumeAnnotations)
   585  				}
   586  			}
   587  			if tc.claimAnnotations != nil {
   588  				ann := tc.claimAnnotations
   589  				updateMigrationAnnotations(logger, cmpm, translator, ann, true)
   590  				if !reflect.DeepEqual(tc.expClaimAnnotations, ann) {
   591  					t.Errorf("got volume annoations: %v, but expected: %v", ann, tc.expVolumeAnnotations)
   592  				}
   593  			}
   594  
   595  		})
   596  	}
   597  }
   598  
   599  func TestModifyDeletionFinalizers(t *testing.T) {
   600  	// This set of tests ensures that protection finalizer is removed when CSI migration is disabled
   601  	// and PV controller needs to remove finalizers added by the external-provisioner. The rbd
   602  	// in-tree plugin is used as migration is disabled. When that plugin is migrated, a different
   603  	// non-migrated one should be used. If all plugins are migrated this test can be removed. The
   604  	// gce in-tree plugin is used for a migrated driver as it is feature-locked as of 1.25.
   605  	defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.HonorPVReclaimPolicy, true)()
   606  	const nonmigratedDriver = "rbd.csi.ceph.com"
   607  	const migratedPlugin = "kubernetes.io/gce-pd"
   608  	const migratedDriver = "pd.csi.storage.gke.io"
   609  	const customFinalizer = "test.volume.kubernetes.io/finalizer"
   610  	tests := []struct {
   611  		name                string
   612  		initialVolume       *v1.PersistentVolume
   613  		volumeAnnotations   map[string]string
   614  		expVolumeFinalizers []string
   615  		expModified         bool
   616  	}{
   617  		{
   618  			// Represents a CSI volume provisioned through external-provisioner, no CSI migration enabled.
   619  			name:                "13-1 migration was never enabled, volume has the finalizer",
   620  			initialVolume:       newExternalProvisionedVolume("volume-13-1", "1Gi", "uid11-23", "claim11-23", v1.VolumeBound, v1.PersistentVolumeReclaimDelete, classCopper, nonmigratedDriver, []string{volume.PVDeletionProtectionFinalizer}, volume.AnnDynamicallyProvisioned, volume.AnnBoundByController),
   621  			expVolumeFinalizers: []string{volume.PVDeletionProtectionFinalizer},
   622  			expModified:         false,
   623  		},
   624  		{
   625  			// Represents a volume provisioned through external-provisioner but the external-provisioner has
   626  			// yet to sync the volume to add the new finalizer
   627  			name:                "13-2 migration was never enabled, volume does not have the finalizer",
   628  			initialVolume:       newExternalProvisionedVolume("volume-13-2", "1Gi", "uid11-23", "claim11-23", v1.VolumeBound, v1.PersistentVolumeReclaimDelete, classCopper, nonmigratedDriver, nil, volume.AnnDynamicallyProvisioned, volume.AnnBoundByController),
   629  			expVolumeFinalizers: nil,
   630  			expModified:         false,
   631  		},
   632  		{
   633  			// Represents an in-tree volume that has the migrated-to annotation but the external-provisioner is
   634  			// yet to sync the volume and add the pv deletion protection finalizer. The custom finalizer is some
   635  			// pre-existing finalizer, for example the pv-protection finalizer. When csi-migration is disabled,
   636  			// the migrated-to annotation will be removed shortly when updateVolumeMigrationAnnotationsAndFinalizers
   637  			// is called followed by adding back the in-tree pv protection finalizer.
   638  			name:                "13-3 migration was disabled, volume has existing custom finalizer, does not have in-tree pv deletion protection finalizer",
   639  			initialVolume:       newVolumeWithFinalizers("volume-13-3", "1Gi", "uid11-23", "claim11-23", v1.VolumeBound, v1.PersistentVolumeReclaimDelete, classCopper, []string{customFinalizer}, volume.AnnDynamicallyProvisioned, volume.AnnBoundByController),
   640  			expVolumeFinalizers: []string{customFinalizer, volume.PVDeletionInTreeProtectionFinalizer},
   641  			expModified:         true,
   642  		},
   643  		{
   644  			name:                "13-4 migration was disabled, volume has no finalizers",
   645  			initialVolume:       newVolumeWithFinalizers("volume-13-4", "1Gi", "uid11-23", "claim11-23", v1.VolumeBound, v1.PersistentVolumeReclaimDelete, classCopper, nil, volume.AnnDynamicallyProvisioned, volume.AnnBoundByController),
   646  			expVolumeFinalizers: []string{volume.PVDeletionInTreeProtectionFinalizer},
   647  			expModified:         true,
   648  		},
   649  		{
   650  			// Represents roll back scenario where the external-provisioner has added the pv deletion protection
   651  			// finalizer and later the csi migration was disabled. The pv deletion protection finalizer added through
   652  			// external-provisioner will be removed and the in-tree pv deletion protection finalizer will be added.
   653  			name:                "13-5 migration was disabled, volume has external PV deletion finalizer",
   654  			initialVolume:       newVolumeWithFinalizers("volume-13-5", "1Gi", "uid11-23", "claim11-23", v1.VolumeBound, v1.PersistentVolumeReclaimDelete, classCopper, []string{volume.PVDeletionProtectionFinalizer}, volume.AnnDynamicallyProvisioned, volume.AnnBoundByController),
   655  			expVolumeFinalizers: []string{volume.PVDeletionInTreeProtectionFinalizer},
   656  			expModified:         true,
   657  		},
   658  		{
   659  			// Represents roll-back of csi-migration as 13-5, here there are multiple finalizers, only the pv deletion
   660  			// protection finalizer added by external-provisioner will be removed and the in-tree pv deletion protection
   661  			// finalizer will be added.
   662  			name:                "13-6 migration was disabled, volume has multiple finalizers",
   663  			initialVolume:       newVolumeWithFinalizers("volume-13-6", "1Gi", "uid11-23", "claim11-23", v1.VolumeBound, v1.PersistentVolumeReclaimDelete, classCopper, []string{volume.PVDeletionProtectionFinalizer, customFinalizer}, volume.AnnDynamicallyProvisioned, volume.AnnBoundByController),
   664  			expVolumeFinalizers: []string{customFinalizer, volume.PVDeletionInTreeProtectionFinalizer},
   665  			expModified:         true,
   666  		},
   667  		{
   668  			// csi migration is enabled, the pv controller should not delete the finalizer added by the
   669  			// external-provisioner and the in-tree finalizer should be deleted.
   670  			name:                "13-7 migration is enabled, volume has both the in-tree and external PV deletion protection finalizer",
   671  			initialVolume:       newVolumeWithFinalizers("volume-13-7", "1Gi", "uid11-23", "claim11-23", v1.VolumeBound, v1.PersistentVolumeReclaimDelete, classCopper, []string{volume.PVDeletionProtectionFinalizer, volume.PVDeletionInTreeProtectionFinalizer}, volume.AnnDynamicallyProvisioned, volume.AnnBoundByController),
   672  			volumeAnnotations:   map[string]string{volume.AnnDynamicallyProvisioned: migratedPlugin, volume.AnnMigratedTo: migratedDriver},
   673  			expVolumeFinalizers: []string{volume.PVDeletionProtectionFinalizer},
   674  			expModified:         true,
   675  		},
   676  		{
   677  			// csi-migration is not completely enabled as the specific plugin feature is not present. This is equivalent
   678  			// of disabled csi-migration.
   679  			name:                "13-8 migration is enabled but plugin migration feature is disabled, volume has the external PV deletion protection finalizer",
   680  			initialVolume:       newVolumeWithFinalizers("volume-13-8", "1Gi", "uid11-23", "claim11-23", v1.VolumeBound, v1.PersistentVolumeReclaimDelete, classCopper, []string{volume.PVDeletionProtectionFinalizer}, volume.AnnDynamicallyProvisioned, volume.AnnBoundByController),
   681  			expVolumeFinalizers: []string{volume.PVDeletionInTreeProtectionFinalizer},
   682  			expModified:         true,
   683  		},
   684  		{
   685  			// same as 13-8 but multiple finalizers exists, only the pv deletion protection finalizer needs to be
   686  			// removed and the in-tree pv deletion protection finalizer needs to be added.
   687  			name:                "13-9 migration is enabled but plugin migration feature is disabled, volume has multiple finalizers including external PV deletion protection finalizer",
   688  			initialVolume:       newVolumeWithFinalizers("volume-13-9", "1Gi", "uid11-23", "claim11-23", v1.VolumeBound, v1.PersistentVolumeReclaimDelete, classCopper, []string{volume.PVDeletionProtectionFinalizer, customFinalizer}, volume.AnnDynamicallyProvisioned, volume.AnnBoundByController),
   689  			expVolumeFinalizers: []string{customFinalizer, volume.PVDeletionInTreeProtectionFinalizer},
   690  			expModified:         true,
   691  		},
   692  		{
   693  			// corner error case.
   694  			name:                "13-10 missing annotations but finalizers exist",
   695  			initialVolume:       newVolumeWithFinalizers("volume-13-10", "1Gi", "uid11-23", "claim11-23", v1.VolumeBound, v1.PersistentVolumeReclaimDelete, classCopper, []string{volume.PVDeletionProtectionFinalizer}),
   696  			expVolumeFinalizers: []string{volume.PVDeletionProtectionFinalizer},
   697  			expModified:         false,
   698  		},
   699  		{
   700  			name:                "13-11 missing annotations and finalizers",
   701  			initialVolume:       newVolumeWithFinalizers("volume-13-11", "1Gi", "uid11-23", "claim11-23", v1.VolumeBound, v1.PersistentVolumeReclaimDelete, classCopper, nil),
   702  			expVolumeFinalizers: nil,
   703  			expModified:         false,
   704  		},
   705  		{
   706  			// When ReclaimPolicy is Retain ensure that in-tree pv deletion protection finalizer is not added.
   707  			name:                "13-12 migration is disabled, volume has no finalizers, reclaimPolicy is Retain",
   708  			initialVolume:       newVolumeWithFinalizers("volume-13-12", "1Gi", "uid11-23", "claim11-23", v1.VolumeBound, v1.PersistentVolumeReclaimRetain, classCopper, nil, volume.AnnDynamicallyProvisioned, volume.AnnBoundByController),
   709  			expVolumeFinalizers: nil,
   710  			expModified:         false,
   711  		},
   712  		{
   713  			// When ReclaimPolicy is Recycle ensure that in-tree pv deletion protection finalizer is not added.
   714  			name:                "13-13 migration is disabled, volume has no finalizers, reclaimPolicy is Recycle",
   715  			initialVolume:       newVolumeWithFinalizers("volume-13-13", "1Gi", "uid11-23", "claim11-23", v1.VolumeBound, v1.PersistentVolumeReclaimRecycle, classCopper, nil, volume.AnnDynamicallyProvisioned, volume.AnnBoundByController),
   716  			expVolumeFinalizers: nil,
   717  			expModified:         false,
   718  		},
   719  		{
   720  			// When ReclaimPolicy is Retain ensure that in-tree pv deletion protection finalizer present is removed.
   721  			name:                "13-14 migration is disabled, volume has in-tree pv deletion finalizers, reclaimPolicy is Retain",
   722  			initialVolume:       newVolumeWithFinalizers("volume-13-14", "1Gi", "uid11-23", "claim11-23", v1.VolumeBound, v1.PersistentVolumeReclaimRetain, classCopper, []string{volume.PVDeletionInTreeProtectionFinalizer}, volume.AnnDynamicallyProvisioned, volume.AnnBoundByController),
   723  			expVolumeFinalizers: nil,
   724  			expModified:         true,
   725  		},
   726  		{
   727  			// Statically provisioned volumes should not have the in-tree pv deletion protection finalizer
   728  			name:                "13-15 migration is disabled, statically provisioned PV",
   729  			initialVolume:       newVolumeWithFinalizers("volume-13-14", "1Gi", "", "", v1.VolumeAvailable, v1.PersistentVolumeReclaimDelete, classCopper, nil),
   730  			expVolumeFinalizers: nil,
   731  			expModified:         false,
   732  		},
   733  	}
   734  
   735  	translator := csitrans.New()
   736  	cmpm := csimigration.NewPluginManager(translator, utilfeature.DefaultFeatureGate)
   737  	logger, _ := ktesting.NewTestContext(t)
   738  	for _, tc := range tests {
   739  		t.Run(tc.name, func(t *testing.T) {
   740  			if tc.volumeAnnotations != nil {
   741  				tc.initialVolume.SetAnnotations(tc.volumeAnnotations)
   742  			}
   743  			modifiedFinalizers, modified := modifyDeletionFinalizers(logger, cmpm, tc.initialVolume)
   744  			if modified != tc.expModified {
   745  				t.Errorf("got modified: %v, but expected: %v", modified, tc.expModified)
   746  			}
   747  			if !reflect.DeepEqual(tc.expVolumeFinalizers, modifiedFinalizers) {
   748  				t.Errorf("got volume finaliers: %v, but expected: %v", modifiedFinalizers, tc.expVolumeFinalizers)
   749  			}
   750  
   751  		})
   752  	}
   753  }
   754  
   755  func TestRetroactiveStorageClassAssignment(t *testing.T) {
   756  	tests := []struct {
   757  		storageClasses []*storagev1.StorageClass
   758  		tests          []controllerTest
   759  	}{
   760  		// [Unit test set 15] - retroactive storage class assignment tests
   761  		{
   762  			storageClasses: []*storagev1.StorageClass{},
   763  			tests: []controllerTest{
   764  				{
   765  					name:            "15-1 - pvc storage class is not assigned retroactively if there are no default storage classes",
   766  					initialVolumes:  novolumes,
   767  					expectedVolumes: novolumes,
   768  					initialClaims:   newClaimArray("claim15-1", "uid15-1", "1Gi", "", v1.ClaimPending, nil),
   769  					expectedClaims:  newClaimArray("claim15-1", "uid15-1", "1Gi", "", v1.ClaimPending, nil),
   770  					expectedEvents:  noevents,
   771  					errors:          noerrors,
   772  					test:            testSyncClaim,
   773  				},
   774  			},
   775  		},
   776  		{
   777  			storageClasses: []*storagev1.StorageClass{
   778  				makeDefaultStorageClass(classGold, &modeImmediate),
   779  				makeStorageClass(classSilver, &modeImmediate),
   780  			},
   781  			tests: []controllerTest{
   782  				{
   783  					name:            "15-3 - pvc storage class is not assigned retroactively if claim is already bound",
   784  					initialVolumes:  novolumes,
   785  					expectedVolumes: novolumes,
   786  					initialClaims:   newClaimArray("claim15-3", "uid15-3", "1Gi", "test", v1.ClaimBound, &classCopper, volume.AnnBoundByController, volume.AnnBindCompleted),
   787  					expectedClaims:  newClaimArray("claim15-3", "uid15-3", "1Gi", "test", v1.ClaimLost, &classCopper, volume.AnnBoundByController, volume.AnnBindCompleted),
   788  					expectedEvents:  noevents,
   789  					errors:          noerrors,
   790  					test:            testSyncClaim,
   791  				},
   792  			},
   793  		},
   794  		{
   795  			storageClasses: []*storagev1.StorageClass{
   796  				makeDefaultStorageClass(classGold, &modeImmediate),
   797  				makeStorageClass(classSilver, &modeImmediate),
   798  			},
   799  			tests: []controllerTest{
   800  				{
   801  					name:            "15-4 - pvc storage class is not assigned retroactively if claim is already bound but annotations are missing",
   802  					initialVolumes:  novolumes,
   803  					expectedVolumes: novolumes,
   804  					initialClaims:   newClaimArray("claim15-4", "uid15-4", "1Gi", "test", v1.ClaimBound, &classCopper),
   805  					expectedClaims:  newClaimArray("claim15-4", "uid15-4", "1Gi", "test", v1.ClaimPending, &classCopper),
   806  					expectedEvents:  noevents,
   807  					errors:          noerrors,
   808  					test:            testSyncClaim,
   809  				},
   810  			},
   811  		},
   812  		{
   813  			storageClasses: []*storagev1.StorageClass{
   814  				makeDefaultStorageClass(classGold, &modeImmediate),
   815  				makeStorageClass(classSilver, &modeImmediate),
   816  			},
   817  			tests: []controllerTest{
   818  				{
   819  					name:            "15-5 - pvc storage class is assigned retroactively if there is a default",
   820  					initialVolumes:  novolumes,
   821  					expectedVolumes: novolumes,
   822  					initialClaims:   newClaimArray("claim15-5", "uid15-5", "1Gi", "", v1.ClaimPending, nil),
   823  					expectedClaims:  newClaimArray("claim15-5", "uid15-5", "1Gi", "", v1.ClaimPending, &classGold),
   824  					expectedEvents:  noevents,
   825  					errors:          noerrors,
   826  					test:            testSyncClaim,
   827  				},
   828  			},
   829  		},
   830  		{
   831  			storageClasses: []*storagev1.StorageClass{
   832  				makeDefaultStorageClass(classGold, &modeImmediate),
   833  				makeDefaultStorageClass(classSilver, &modeImmediate)},
   834  			tests: []controllerTest{
   835  				{
   836  					name:            "15-2 - pvc storage class is assigned retroactively if there are multiple default storage classes",
   837  					initialVolumes:  novolumes,
   838  					expectedVolumes: novolumes,
   839  					initialClaims:   newClaimArray("claim15-2", "uid15-2", "1Gi", "", v1.ClaimPending, nil),
   840  					expectedClaims:  newClaimArray("claim15-2", "uid15-2", "1Gi", "", v1.ClaimPending, &classGold),
   841  					expectedEvents:  noevents,
   842  					errors:          noerrors,
   843  					test:            testSyncClaim,
   844  				},
   845  			},
   846  		},
   847  		{
   848  			storageClasses: []*storagev1.StorageClass{
   849  				makeDefaultStorageClass(classGold, &modeImmediate),
   850  				makeStorageClass(classCopper, &modeImmediate),
   851  			},
   852  			tests: []controllerTest{
   853  				{
   854  					name:            "15-6 - pvc storage class is not changed if claim is not bound but already has a storage class",
   855  					initialVolumes:  novolumes,
   856  					expectedVolumes: novolumes,
   857  					initialClaims:   newClaimArray("claim15-6", "uid15-6", "1Gi", "", v1.ClaimPending, &classCopper),
   858  					expectedClaims:  newClaimArray("claim15-6", "uid15-6", "1Gi", "", v1.ClaimPending, &classCopper),
   859  					expectedEvents:  noevents,
   860  					errors:          noerrors,
   861  					test:            testSyncClaim,
   862  				},
   863  			},
   864  		},
   865  		{
   866  			storageClasses: []*storagev1.StorageClass{
   867  				makeDefaultStorageClass(classGold, &modeImmediate),
   868  				makeStorageClass(classCopper, &modeImmediate),
   869  			},
   870  			tests: []controllerTest{
   871  				{
   872  					name:            "15-7 - pvc storage class is not changed if claim is not bound but already set annotation \"volume.beta.kubernetes.io/storage-class\"",
   873  					initialVolumes:  novolumes,
   874  					expectedVolumes: novolumes,
   875  					initialClaims:   newClaimArray("claim15-7", "uid15-7", "1Gi", "", v1.ClaimPending, nil, v1.BetaStorageClassAnnotation),
   876  					expectedClaims:  newClaimArray("claim15-7", "uid15-7", "1Gi", "", v1.ClaimPending, nil, v1.BetaStorageClassAnnotation),
   877  					expectedEvents:  noevents,
   878  					errors:          noerrors,
   879  					test:            testSyncClaim,
   880  				},
   881  			},
   882  		},
   883  	}
   884  	_, ctx := ktesting.NewTestContext(t)
   885  	for _, test := range tests {
   886  		runSyncTests(t, ctx, test.tests, test.storageClasses, nil)
   887  	}
   888  }