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 }