k8s.io/kubernetes@v1.29.3/pkg/controller/volume/persistentvolume/provision_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 "errors" 21 22 utilfeature "k8s.io/apiserver/pkg/util/feature" 23 featuregatetesting "k8s.io/component-base/featuregate/testing" 24 "k8s.io/klog/v2/ktesting" 25 "k8s.io/kubernetes/pkg/features" 26 "testing" 27 28 v1 "k8s.io/api/core/v1" 29 storage "k8s.io/api/storage/v1" 30 apierrors "k8s.io/apimachinery/pkg/api/errors" 31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 corelisters "k8s.io/client-go/listers/core/v1" 33 "k8s.io/client-go/tools/cache" 34 "k8s.io/component-helpers/storage/volume" 35 api "k8s.io/kubernetes/pkg/apis/core" 36 pvtesting "k8s.io/kubernetes/pkg/controller/volume/persistentvolume/testing" 37 ) 38 39 var class1Parameters = map[string]string{ 40 "param1": "value1", 41 } 42 var class2Parameters = map[string]string{ 43 "param2": "value2", 44 } 45 var deleteReclaimPolicy = v1.PersistentVolumeReclaimDelete 46 var modeImmediate = storage.VolumeBindingImmediate 47 var storageClasses = []*storage.StorageClass{ 48 { 49 TypeMeta: metav1.TypeMeta{ 50 Kind: "StorageClass", 51 }, 52 53 ObjectMeta: metav1.ObjectMeta{ 54 Name: "gold", 55 }, 56 57 Provisioner: mockPluginName, 58 Parameters: class1Parameters, 59 ReclaimPolicy: &deleteReclaimPolicy, 60 VolumeBindingMode: &modeImmediate, 61 }, 62 { 63 TypeMeta: metav1.TypeMeta{ 64 Kind: "StorageClass", 65 }, 66 ObjectMeta: metav1.ObjectMeta{ 67 Name: "silver", 68 }, 69 Provisioner: mockPluginName, 70 Parameters: class2Parameters, 71 ReclaimPolicy: &deleteReclaimPolicy, 72 VolumeBindingMode: &modeImmediate, 73 }, 74 { 75 TypeMeta: metav1.TypeMeta{ 76 Kind: "StorageClass", 77 }, 78 ObjectMeta: metav1.ObjectMeta{ 79 Name: "copper", 80 }, 81 Provisioner: mockPluginName, 82 Parameters: class1Parameters, 83 ReclaimPolicy: &deleteReclaimPolicy, 84 VolumeBindingMode: &modeWait, 85 }, 86 { 87 TypeMeta: metav1.TypeMeta{ 88 Kind: "StorageClass", 89 }, 90 ObjectMeta: metav1.ObjectMeta{ 91 Name: "external", 92 }, 93 Provisioner: "vendor.com/my-volume", 94 Parameters: class1Parameters, 95 ReclaimPolicy: &deleteReclaimPolicy, 96 VolumeBindingMode: &modeImmediate, 97 }, 98 { 99 TypeMeta: metav1.TypeMeta{ 100 Kind: "StorageClass", 101 }, 102 ObjectMeta: metav1.ObjectMeta{ 103 Name: "external-wait", 104 }, 105 Provisioner: "vendor.com/my-volume-wait", 106 Parameters: class1Parameters, 107 ReclaimPolicy: &deleteReclaimPolicy, 108 VolumeBindingMode: &modeWait, 109 }, 110 { 111 TypeMeta: metav1.TypeMeta{ 112 Kind: "StorageClass", 113 }, 114 ObjectMeta: metav1.ObjectMeta{ 115 Name: "unknown-internal", 116 }, 117 Provisioner: "kubernetes.io/unknown", 118 Parameters: class1Parameters, 119 ReclaimPolicy: &deleteReclaimPolicy, 120 VolumeBindingMode: &modeImmediate, 121 }, 122 { 123 TypeMeta: metav1.TypeMeta{ 124 Kind: "StorageClass", 125 }, 126 ObjectMeta: metav1.ObjectMeta{ 127 Name: "unsupported-mountoptions", 128 }, 129 Provisioner: mockPluginName, 130 Parameters: class1Parameters, 131 ReclaimPolicy: &deleteReclaimPolicy, 132 MountOptions: []string{"foo"}, 133 VolumeBindingMode: &modeImmediate, 134 }, 135 { 136 TypeMeta: metav1.TypeMeta{ 137 Kind: "StorageClass", 138 }, 139 140 ObjectMeta: metav1.ObjectMeta{ 141 Name: "csi", 142 }, 143 144 Provisioner: "mydriver.csi.k8s.io", 145 Parameters: class1Parameters, 146 ReclaimPolicy: &deleteReclaimPolicy, 147 VolumeBindingMode: &modeImmediate, 148 }, 149 } 150 151 // call to storageClass 1, returning an error 152 var provision1Error = provisionCall{ 153 ret: errors.New("Mock provisioner error"), 154 expectedParameters: class1Parameters, 155 } 156 157 // call to storageClass 1, returning a valid PV 158 var provision1Success = provisionCall{ 159 ret: nil, 160 expectedParameters: class1Parameters, 161 } 162 163 // call to storageClass 2, returning a valid PV 164 var provision2Success = provisionCall{ 165 ret: nil, 166 expectedParameters: class2Parameters, 167 } 168 169 // Test single call to syncVolume, expecting provisioning to happen. 170 // 1. Fill in the controller with initial data 171 // 2. Call the syncVolume *once*. 172 // 3. Compare resulting volumes with expected volumes. 173 func TestProvisionSync(t *testing.T) { 174 // Default enable the HonorPVReclaimPolicy feature gate. 175 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.HonorPVReclaimPolicy, true)() 176 _, ctx := ktesting.NewTestContext(t) 177 tests := []controllerTest{ 178 { 179 // Provision a volume (with a default class) 180 name: "11-1 - successful provision with storage class 1", 181 initialVolumes: novolumes, 182 expectedVolumes: volumesWithFinalizers(newVolumeArray("pvc-uid11-1", "1Gi", "uid11-1", "claim11-1", v1.VolumeBound, v1.PersistentVolumeReclaimDelete, classGold, volume.AnnBoundByController, volume.AnnDynamicallyProvisioned), []string{volume.PVDeletionInTreeProtectionFinalizer}), 183 // Binding will be completed in the next syncClaim 184 initialClaims: newClaimArray("claim11-1", "uid11-1", "1Gi", "", v1.ClaimPending, &classGold), 185 expectedClaims: newClaimArray("claim11-1", "uid11-1", "1Gi", "", v1.ClaimPending, &classGold, volume.AnnStorageProvisioner, volume.AnnBetaStorageProvisioner), 186 expectedEvents: []string{"Normal ProvisioningSucceeded"}, 187 errors: noerrors, 188 test: wrapTestWithProvisionCalls([]provisionCall{provision1Success}, testSyncClaim), 189 }, 190 { 191 // Provision failure - plugin not found 192 name: "11-2 - plugin not found", 193 initialVolumes: novolumes, 194 expectedVolumes: novolumes, 195 initialClaims: newClaimArray("claim11-2", "uid11-2", "1Gi", "", v1.ClaimPending, &classGold), 196 expectedClaims: newClaimArray("claim11-2", "uid11-2", "1Gi", "", v1.ClaimPending, &classGold), 197 expectedEvents: []string{"Warning ProvisioningFailed"}, 198 errors: noerrors, 199 test: testSyncClaim, 200 }, 201 { 202 // Provision failure - newProvisioner returns error 203 name: "11-3 - newProvisioner failure", 204 initialVolumes: novolumes, 205 expectedVolumes: novolumes, 206 initialClaims: newClaimArray("claim11-3", "uid11-3", "1Gi", "", v1.ClaimPending, &classGold), 207 expectedClaims: newClaimArray("claim11-3", "uid11-3", "1Gi", "", v1.ClaimPending, &classGold, volume.AnnStorageProvisioner, volume.AnnBetaStorageProvisioner), 208 expectedEvents: []string{"Warning ProvisioningFailed"}, 209 errors: noerrors, 210 test: wrapTestWithProvisionCalls([]provisionCall{}, testSyncClaim), 211 }, 212 { 213 // Provision failure - Provision returns error 214 name: "11-4 - provision failure", 215 initialVolumes: novolumes, 216 expectedVolumes: novolumes, 217 initialClaims: newClaimArray("claim11-4", "uid11-4", "1Gi", "", v1.ClaimPending, &classGold), 218 expectedClaims: newClaimArray("claim11-4", "uid11-4", "1Gi", "", v1.ClaimPending, &classGold, volume.AnnStorageProvisioner, volume.AnnBetaStorageProvisioner), 219 expectedEvents: []string{"Warning ProvisioningFailed"}, 220 errors: noerrors, 221 test: wrapTestWithProvisionCalls([]provisionCall{provision1Error}, testSyncClaim), 222 }, 223 { 224 // No provisioning if there is a matching volume available 225 name: "11-6 - provisioning when there is a volume available", 226 initialVolumes: newVolumeArray("volume11-6", "1Gi", "", "", v1.VolumeAvailable, v1.PersistentVolumeReclaimRetain, classGold), 227 expectedVolumes: newVolumeArray("volume11-6", "1Gi", "uid11-6", "claim11-6", v1.VolumeBound, v1.PersistentVolumeReclaimRetain, classGold, volume.AnnBoundByController), 228 initialClaims: newClaimArray("claim11-6", "uid11-6", "1Gi", "", v1.ClaimPending, &classGold), 229 expectedClaims: newClaimArray("claim11-6", "uid11-6", "1Gi", "volume11-6", v1.ClaimBound, &classGold, volume.AnnBoundByController, volume.AnnBindCompleted), 230 expectedEvents: noevents, 231 errors: noerrors, 232 // No provisioning plugin confingure - makes the test fail when 233 // the controller erroneously tries to provision something 234 test: wrapTestWithProvisionCalls([]provisionCall{provision1Success}, testSyncClaim), 235 }, 236 { 237 // Provision success? - claim is bound before provisioner creates 238 // a volume. 239 name: "11-7 - claim is bound before provisioning", 240 initialVolumes: novolumes, 241 expectedVolumes: newVolumeArray("pvc-uid11-7", "1Gi", "uid11-7", "claim11-7", v1.VolumeBound, v1.PersistentVolumeReclaimDelete, classGold, volume.AnnBoundByController, volume.AnnDynamicallyProvisioned), 242 initialClaims: newClaimArray("claim11-7", "uid11-7", "1Gi", "", v1.ClaimPending, &classGold), 243 // The claim would be bound in next syncClaim 244 expectedClaims: newClaimArray("claim11-7", "uid11-7", "1Gi", "", v1.ClaimPending, &classGold, volume.AnnStorageProvisioner, volume.AnnBetaStorageProvisioner), 245 expectedEvents: noevents, 246 errors: noerrors, 247 test: wrapTestWithInjectedOperation(ctx, wrapTestWithProvisionCalls([]provisionCall{}, testSyncClaim), func(ctrl *PersistentVolumeController, reactor *pvtesting.VolumeReactor) { 248 // Create a volume before provisionClaimOperation starts. 249 // This similates a parallel controller provisioning the volume. 250 volume := newVolume("pvc-uid11-7", "1Gi", "uid11-7", "claim11-7", v1.VolumeBound, v1.PersistentVolumeReclaimDelete, classGold, volume.AnnBoundByController, volume.AnnDynamicallyProvisioned) 251 reactor.AddVolume(volume) 252 }), 253 }, 254 { 255 // Provision success - cannot save provisioned PV once, 256 // second retry succeeds 257 name: "11-8 - cannot save provisioned volume", 258 initialVolumes: novolumes, 259 expectedVolumes: volumesWithFinalizers(newVolumeArray("pvc-uid11-8", "1Gi", "uid11-8", "claim11-8", v1.VolumeBound, v1.PersistentVolumeReclaimDelete, classGold, volume.AnnBoundByController, volume.AnnDynamicallyProvisioned), []string{volume.PVDeletionInTreeProtectionFinalizer}), 260 initialClaims: newClaimArray("claim11-8", "uid11-8", "1Gi", "", v1.ClaimPending, &classGold), 261 // Binding will be completed in the next syncClaim 262 expectedClaims: newClaimArray("claim11-8", "uid11-8", "1Gi", "", v1.ClaimPending, &classGold, volume.AnnStorageProvisioner, volume.AnnBetaStorageProvisioner), 263 expectedEvents: []string{"Normal ProvisioningSucceeded"}, 264 errors: []pvtesting.ReactorError{ 265 // Inject error to the first 266 // kubeclient.PersistentVolumes.Create() call. All other calls 267 // will succeed. 268 {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error")}, 269 }, 270 test: wrapTestWithProvisionCalls([]provisionCall{provision1Success}, testSyncClaim), 271 }, 272 { 273 // Provision success? - cannot save provisioned PV five times, 274 // volume is deleted and delete succeeds 275 name: "11-9 - cannot save provisioned volume, delete succeeds", 276 initialVolumes: novolumes, 277 expectedVolumes: novolumes, 278 initialClaims: newClaimArray("claim11-9", "uid11-9", "1Gi", "", v1.ClaimPending, &classGold), 279 expectedClaims: newClaimArray("claim11-9", "uid11-9", "1Gi", "", v1.ClaimPending, &classGold, volume.AnnStorageProvisioner, volume.AnnBetaStorageProvisioner), 280 expectedEvents: []string{"Warning ProvisioningFailed"}, 281 errors: []pvtesting.ReactorError{ 282 // Inject error to five kubeclient.PersistentVolumes.Create() 283 // calls 284 {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error1")}, 285 {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error2")}, 286 {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error3")}, 287 {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error4")}, 288 {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error5")}, 289 }, 290 test: wrapTestWithPluginCalls( 291 nil, // recycle calls 292 []error{nil}, // delete calls 293 []provisionCall{provision1Success}, // provision calls 294 testSyncClaim, 295 ), 296 }, 297 { 298 // Provision failure - cannot save provisioned PV five times, 299 // volume delete failed - no plugin found 300 name: "11-10 - cannot save provisioned volume, no delete plugin found", 301 initialVolumes: novolumes, 302 expectedVolumes: novolumes, 303 initialClaims: newClaimArray("claim11-10", "uid11-10", "1Gi", "", v1.ClaimPending, &classGold), 304 expectedClaims: newClaimArray("claim11-10", "uid11-10", "1Gi", "", v1.ClaimPending, &classGold, volume.AnnStorageProvisioner, volume.AnnBetaStorageProvisioner), 305 expectedEvents: []string{"Warning ProvisioningFailed", "Warning ProvisioningCleanupFailed"}, 306 errors: []pvtesting.ReactorError{ 307 // Inject error to five kubeclient.PersistentVolumes.Create() 308 // calls 309 {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error1")}, 310 {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error2")}, 311 {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error3")}, 312 {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error4")}, 313 {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error5")}, 314 }, 315 // No deleteCalls are configured, which results into no deleter plugin available for the volume 316 test: wrapTestWithProvisionCalls([]provisionCall{provision1Success}, testSyncClaim), 317 }, 318 { 319 // Provision failure - cannot save provisioned PV five times, 320 // volume delete failed - deleter returns error five times 321 name: "11-11 - cannot save provisioned volume, deleter fails", 322 initialVolumes: novolumes, 323 expectedVolumes: novolumes, 324 initialClaims: newClaimArray("claim11-11", "uid11-11", "1Gi", "", v1.ClaimPending, &classGold), 325 expectedClaims: newClaimArray("claim11-11", "uid11-11", "1Gi", "", v1.ClaimPending, &classGold, volume.AnnStorageProvisioner, volume.AnnBetaStorageProvisioner), 326 expectedEvents: []string{"Warning ProvisioningFailed", "Warning ProvisioningCleanupFailed"}, 327 errors: []pvtesting.ReactorError{ 328 // Inject error to five kubeclient.PersistentVolumes.Create() 329 // calls 330 {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error1")}, 331 {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error2")}, 332 {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error3")}, 333 {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error4")}, 334 {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error5")}, 335 }, 336 test: wrapTestWithPluginCalls( 337 nil, // recycle calls 338 []error{ // delete calls 339 errors.New("Mock deletion error1"), 340 errors.New("Mock deletion error2"), 341 errors.New("Mock deletion error3"), 342 errors.New("Mock deletion error4"), 343 errors.New("Mock deletion error5"), 344 }, 345 []provisionCall{provision1Success}, // provision calls 346 testSyncClaim), 347 }, 348 { 349 // Provision failure - cannot save provisioned PV five times, 350 // volume delete succeeds 2nd time 351 name: "11-12 - cannot save provisioned volume, delete succeeds 2nd time", 352 initialVolumes: novolumes, 353 expectedVolumes: novolumes, 354 initialClaims: newClaimArray("claim11-12", "uid11-12", "1Gi", "", v1.ClaimPending, &classGold), 355 expectedClaims: newClaimArray("claim11-12", "uid11-12", "1Gi", "", v1.ClaimPending, &classGold, volume.AnnStorageProvisioner, volume.AnnBetaStorageProvisioner), 356 expectedEvents: []string{"Warning ProvisioningFailed"}, 357 errors: []pvtesting.ReactorError{ 358 // Inject error to five kubeclient.PersistentVolumes.Create() 359 // calls 360 {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error1")}, 361 {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error2")}, 362 {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error3")}, 363 {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error4")}, 364 {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error5")}, 365 }, 366 test: wrapTestWithPluginCalls( 367 nil, // recycle calls 368 []error{ // delete calls 369 errors.New("Mock deletion error1"), 370 nil, 371 }, // provison calls 372 []provisionCall{provision1Success}, 373 testSyncClaim, 374 ), 375 }, 376 { 377 // Provision a volume (with non-default class) 378 name: "11-13 - successful provision with storage class 2", 379 initialVolumes: novolumes, 380 expectedVolumes: volumesWithFinalizers(newVolumeArray("pvc-uid11-13", "1Gi", "uid11-13", "claim11-13", v1.VolumeBound, v1.PersistentVolumeReclaimDelete, classSilver, volume.AnnBoundByController, volume.AnnDynamicallyProvisioned), []string{volume.PVDeletionInTreeProtectionFinalizer}), 381 initialClaims: newClaimArray("claim11-13", "uid11-13", "1Gi", "", v1.ClaimPending, &classSilver), 382 // Binding will be completed in the next syncClaim 383 expectedClaims: newClaimArray("claim11-13", "uid11-13", "1Gi", "", v1.ClaimPending, &classSilver, volume.AnnStorageProvisioner, volume.AnnBetaStorageProvisioner), 384 expectedEvents: []string{"Normal ProvisioningSucceeded"}, 385 errors: noerrors, 386 test: wrapTestWithProvisionCalls([]provisionCall{provision2Success}, testSyncClaim), 387 }, 388 { 389 // Provision error - non existing class 390 name: "11-14 - fail due to non-existing class", 391 initialVolumes: novolumes, 392 expectedVolumes: novolumes, 393 initialClaims: newClaimArray("claim11-14", "uid11-14", "1Gi", "", v1.ClaimPending, &classNonExisting), 394 expectedClaims: newClaimArray("claim11-14", "uid11-14", "1Gi", "", v1.ClaimPending, &classNonExisting), 395 expectedEvents: noevents, 396 errors: noerrors, 397 test: wrapTestWithProvisionCalls([]provisionCall{}, testSyncClaim), 398 }, 399 { 400 // No provisioning with class="" 401 name: "11-15 - no provisioning with class=''", 402 initialVolumes: novolumes, 403 expectedVolumes: novolumes, 404 initialClaims: newClaimArray("claim11-15", "uid11-15", "1Gi", "", v1.ClaimPending, &classEmpty), 405 expectedClaims: newClaimArray("claim11-15", "uid11-15", "1Gi", "", v1.ClaimPending, &classEmpty), 406 expectedEvents: noevents, 407 errors: noerrors, 408 test: wrapTestWithProvisionCalls([]provisionCall{}, testSyncClaim), 409 }, 410 { 411 // No provisioning with class=nil 412 name: "11-16 - no provisioning with class=nil", 413 initialVolumes: novolumes, 414 expectedVolumes: novolumes, 415 initialClaims: newClaimArray("claim11-15", "uid11-15", "1Gi", "", v1.ClaimPending, nil), 416 expectedClaims: newClaimArray("claim11-15", "uid11-15", "1Gi", "", v1.ClaimPending, nil), 417 expectedEvents: noevents, 418 errors: noerrors, 419 test: wrapTestWithProvisionCalls([]provisionCall{}, testSyncClaim), 420 }, 421 { 422 // No provisioning + normal event with external provisioner 423 name: "11-17 - external provisioner", 424 initialVolumes: novolumes, 425 expectedVolumes: novolumes, 426 initialClaims: newClaimArray("claim11-17", "uid11-17", "1Gi", "", v1.ClaimPending, &classExternal), 427 expectedClaims: claimWithAnnotation(volume.AnnBetaStorageProvisioner, "vendor.com/my-volume", 428 claimWithAnnotation(volume.AnnStorageProvisioner, "vendor.com/my-volume", 429 newClaimArray("claim11-17", "uid11-17", "1Gi", "", v1.ClaimPending, &classExternal))), 430 expectedEvents: []string{"Normal ExternalProvisioning"}, 431 errors: noerrors, 432 test: wrapTestWithProvisionCalls([]provisionCall{}, testSyncClaim), 433 }, 434 { 435 // No provisioning + warning event with unknown internal provisioner 436 name: "11-18 - unknown internal provisioner", 437 initialVolumes: novolumes, 438 expectedVolumes: novolumes, 439 initialClaims: newClaimArray("claim11-18", "uid11-18", "1Gi", "", v1.ClaimPending, &classUnknownInternal), 440 expectedClaims: newClaimArray("claim11-18", "uid11-18", "1Gi", "", v1.ClaimPending, &classUnknownInternal), 441 expectedEvents: []string{"Warning ProvisioningFailed"}, 442 errors: noerrors, 443 test: wrapTestWithProvisionCalls([]provisionCall{}, testSyncClaim), 444 }, 445 { 446 // Provision success - first save of a PV to API server fails (API 447 // server has written the object to etcd, but crashed before sending 448 // 200 OK response to the controller). Controller retries and the 449 // second save of the PV returns "AlreadyExists" because the PV 450 // object already is in the API server. 451 // 452 "11-19 - provisioned volume saved but API server crashed", 453 novolumes, 454 // We don't actually simulate API server saving the object and 455 // crashing afterwards, Create() just returns error without saving 456 // the volume in this test. So the set of expected volumes at the 457 // end of the test is empty. 458 novolumes, 459 newClaimArray("claim11-19", "uid11-19", "1Gi", "", v1.ClaimPending, &classGold), 460 newClaimArray("claim11-19", "uid11-19", "1Gi", "", v1.ClaimPending, &classGold, volume.AnnStorageProvisioner, volume.AnnBetaStorageProvisioner), 461 noevents, 462 []pvtesting.ReactorError{ 463 // Inject errors to simulate crashed API server during 464 // kubeclient.PersistentVolumes.Create() 465 {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error1")}, 466 {Verb: "create", Resource: "persistentvolumes", Error: apierrors.NewAlreadyExists(api.Resource("persistentvolumes"), "")}, 467 }, 468 wrapTestWithPluginCalls( 469 nil, // recycle calls 470 nil, // delete calls - if Delete was called the test would fail 471 []provisionCall{provision1Success}, 472 testSyncClaim, 473 ), 474 }, 475 { 476 // No provisioning + warning event with unsupported storageClass.mountOptions 477 name: "11-20 - unsupported storageClass.mountOptions", 478 initialVolumes: novolumes, 479 expectedVolumes: novolumes, 480 initialClaims: newClaimArray("claim11-20", "uid11-20", "1Gi", "", v1.ClaimPending, &classUnsupportedMountOptions), 481 expectedClaims: newClaimArray("claim11-20", "uid11-20", "1Gi", "", v1.ClaimPending, &classUnsupportedMountOptions, volume.AnnStorageProvisioner, volume.AnnBetaStorageProvisioner), 482 // Expect event to be prefixed with "Mount options" because saving PV will fail anyway 483 expectedEvents: []string{"Warning ProvisioningFailed Mount options"}, 484 errors: noerrors, 485 test: wrapTestWithProvisionCalls([]provisionCall{}, testSyncClaim), 486 }, 487 { 488 // No provisioning due to CSI migration + normal event with external provisioner 489 name: "11-21 - external provisioner for CSI migration", 490 initialVolumes: novolumes, 491 expectedVolumes: novolumes, 492 initialClaims: newClaimArray("claim11-21", "uid11-21", "1Gi", "", v1.ClaimPending, &classGold), 493 expectedClaims: []*v1.PersistentVolumeClaim{ 494 annotateClaim( 495 newClaim("claim11-21", "uid11-21", "1Gi", "", v1.ClaimPending, &classGold), 496 map[string]string{ 497 volume.AnnStorageProvisioner: "vendor.com/MockCSIDriver", 498 volume.AnnBetaStorageProvisioner: "vendor.com/MockCSIDriver", 499 volume.AnnMigratedTo: "vendor.com/MockCSIDriver", 500 }), 501 }, 502 expectedEvents: []string{"Normal ExternalProvisioning"}, 503 errors: noerrors, 504 test: wrapTestWithCSIMigrationProvisionCalls(testSyncClaim), 505 }, 506 { 507 // volume provisioned and available 508 // in this case, NO normal event with external provisioner should be issued 509 name: "11-22 - external provisioner with volume available", 510 initialVolumes: newVolumeArray("volume11-22", "1Gi", "", "", v1.VolumeAvailable, v1.PersistentVolumeReclaimRetain, classExternal), 511 expectedVolumes: newVolumeArray("volume11-22", "1Gi", "uid11-22", "claim11-22", v1.VolumeBound, v1.PersistentVolumeReclaimRetain, classExternal, volume.AnnBoundByController), 512 initialClaims: newClaimArray("claim11-22", "uid11-22", "1Gi", "", v1.ClaimPending, &classExternal), 513 expectedClaims: newClaimArray("claim11-22", "uid11-22", "1Gi", "volume11-22", v1.ClaimBound, &classExternal, volume.AnnBoundByController, volume.AnnBindCompleted), 514 expectedEvents: noevents, 515 errors: noerrors, 516 test: wrapTestWithProvisionCalls([]provisionCall{}, testSyncClaim), 517 }, 518 { 519 // volume provision for PVC scheduled 520 "11-23 - skip finding PV and provision for PVC annotated with AnnSelectedNode", 521 newVolumeArray("volume11-23", "1Gi", "", "", v1.VolumeAvailable, v1.PersistentVolumeReclaimDelete, classCopper), 522 []*v1.PersistentVolume{ 523 newVolumeWithFinalizers("volume11-23", "1Gi", "", "", v1.VolumeAvailable, v1.PersistentVolumeReclaimDelete, classCopper, nil /*No Finalizer is added here since the test doesn't trigger syncVolume, instead just syncClaim*/), 524 newVolumeWithFinalizers("pvc-uid11-23", "1Gi", "uid11-23", "claim11-23", v1.VolumeBound, v1.PersistentVolumeReclaimDelete, classCopper, []string{volume.PVDeletionInTreeProtectionFinalizer}, volume.AnnDynamicallyProvisioned, volume.AnnBoundByController), 525 }, 526 claimWithAnnotation(volume.AnnSelectedNode, "node1", 527 newClaimArray("claim11-23", "uid11-23", "1Gi", "", v1.ClaimPending, &classCopper)), 528 claimWithAnnotation(volume.AnnSelectedNode, "node1", 529 newClaimArray("claim11-23", "uid11-23", "1Gi", "", v1.ClaimPending, &classCopper, volume.AnnStorageProvisioner, volume.AnnBetaStorageProvisioner)), 530 []string{"Normal ProvisioningSucceeded"}, 531 noerrors, 532 wrapTestWithInjectedOperation(ctx, wrapTestWithProvisionCalls([]provisionCall{provision1Success}, testSyncClaim), 533 func(ctrl *PersistentVolumeController, reactor *pvtesting.VolumeReactor) { 534 nodesIndexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{}) 535 node := &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "node1"}} 536 nodesIndexer.Add(node) 537 ctrl.NodeLister = corelisters.NewNodeLister(nodesIndexer) 538 }), 539 }, 540 { 541 // volume provision for PVC that scheduled 542 name: "11-24 - skip finding PV and wait external provisioner for PVC annotated with AnnSelectedNode", 543 initialVolumes: newVolumeArray("volume11-24", "1Gi", "", "", v1.VolumeAvailable, v1.PersistentVolumeReclaimDelete, classExternalWait), 544 expectedVolumes: newVolumeArray("volume11-24", "1Gi", "", "", v1.VolumeAvailable, v1.PersistentVolumeReclaimDelete, classExternalWait), 545 initialClaims: claimWithAnnotation(volume.AnnSelectedNode, "node1", 546 newClaimArray("claim11-24", "uid11-24", "1Gi", "", v1.ClaimPending, &classExternalWait)), 547 expectedClaims: claimWithAnnotation(volume.AnnBetaStorageProvisioner, "vendor.com/my-volume-wait", 548 claimWithAnnotation(volume.AnnStorageProvisioner, "vendor.com/my-volume-wait", 549 claimWithAnnotation(volume.AnnSelectedNode, "node1", 550 newClaimArray("claim11-24", "uid11-24", "1Gi", "", v1.ClaimPending, &classExternalWait)))), 551 expectedEvents: []string{"Normal ExternalProvisioning"}, 552 errors: noerrors, 553 test: testSyncClaim, 554 }, 555 { 556 // Provision a volume with a data source will fail 557 // for in-tree plugins 558 name: "11-25 - failed in-tree provision with data source", 559 initialVolumes: novolumes, 560 expectedVolumes: novolumes, 561 initialClaims: claimWithDataSource("test-snap", "VolumeSnapshot", "snapshot.storage.k8s.io", newClaimArray("claim11-25", "uid11-25", "1Gi", "", v1.ClaimPending, &classGold)), 562 expectedClaims: claimWithDataSource("test-snap", "VolumeSnapshot", "snapshot.storage.k8s.io", newClaimArray("claim11-25", "uid11-25", "1Gi", "", v1.ClaimPending, &classGold)), 563 expectedEvents: []string{"Warning ProvisioningFailed"}, 564 errors: noerrors, 565 test: testSyncClaim, 566 }, 567 { 568 // Provision a volume with a data source will proceed 569 // for CSI plugins 570 "11-26 - csi with data source", 571 novolumes, 572 novolumes, 573 claimWithAnnotation(volume.AnnStorageProvisioner, "mydriver.csi.k8s.io", 574 claimWithDataSource("test-snap", "VolumeSnapshot", "snapshot.storage.k8s.io", newClaimArray("claim11-26", "uid11-26", "1Gi", "", v1.ClaimPending, &classCSI))), 575 claimWithAnnotation(volume.AnnStorageProvisioner, "mydriver.csi.k8s.io", 576 claimWithDataSource("test-snap", "VolumeSnapshot", "snapshot.storage.k8s.io", newClaimArray("claim11-26", "uid11-26", "1Gi", "", v1.ClaimPending, &classCSI))), 577 []string{"Normal ExternalProvisioning"}, 578 noerrors, 579 wrapTestWithProvisionCalls([]provisionCall{}, testSyncClaim), 580 }, 581 } 582 runSyncTests(t, ctx, tests, storageClasses, []*v1.Pod{}) 583 } 584 585 // Test multiple calls to syncClaim/syncVolume and periodic sync of all 586 // volume/claims. The test follows this pattern: 587 // 0. Load the controller with initial data. 588 // 1. Call controllerTest.testCall() once as in TestSync() 589 // 2. For all volumes/claims changed by previous syncVolume/syncClaim calls, 590 // call appropriate syncVolume/syncClaim (simulating "volume/claim changed" 591 // events). Go to 2. if these calls change anything. 592 // 3. When all changes are processed and no new changes were made, call 593 // syncVolume/syncClaim on all volumes/claims (simulating "periodic sync"). 594 // 4. If some changes were done by step 3., go to 2. (simulation of 595 // "volume/claim updated" events, eventually performing step 3. again) 596 // 5. When 3. does not do any changes, finish the tests and compare final set 597 // of volumes/claims with expected claims/volumes and report differences. 598 // 599 // Some limit of calls in enforced to prevent endless loops. 600 func TestProvisionMultiSync(t *testing.T) { 601 _, ctx := ktesting.NewTestContext(t) 602 tests := []controllerTest{ 603 { 604 // Provision a volume with binding 605 name: "12-1 - successful provision", 606 initialVolumes: novolumes, 607 expectedVolumes: newVolumeArray("pvc-uid12-1", "1Gi", "uid12-1", "claim12-1", v1.VolumeBound, v1.PersistentVolumeReclaimDelete, classGold, volume.AnnBoundByController, volume.AnnDynamicallyProvisioned), 608 initialClaims: newClaimArray("claim12-1", "uid12-1", "1Gi", "", v1.ClaimPending, &classGold), 609 expectedClaims: newClaimArray("claim12-1", "uid12-1", "1Gi", "pvc-uid12-1", v1.ClaimBound, &classGold, volume.AnnBoundByController, volume.AnnBindCompleted, volume.AnnStorageProvisioner, volume.AnnBetaStorageProvisioner), 610 expectedEvents: noevents, 611 errors: noerrors, 612 test: wrapTestWithProvisionCalls([]provisionCall{provision1Success}, testSyncClaim), 613 }, 614 { 615 // provision a volume (external provisioner) and binding + normal event with external provisioner 616 name: "12-2 - external provisioner with volume provisioned success", 617 initialVolumes: novolumes, 618 expectedVolumes: newVolumeArray("pvc-uid12-2", "1Gi", "uid12-2", "claim12-2", v1.VolumeBound, v1.PersistentVolumeReclaimRetain, classExternal, volume.AnnBoundByController), 619 initialClaims: newClaimArray("claim12-2", "uid12-2", "1Gi", "", v1.ClaimPending, &classExternal), 620 expectedClaims: claimWithAnnotation(volume.AnnBetaStorageProvisioner, "vendor.com/my-volume", 621 claimWithAnnotation(volume.AnnStorageProvisioner, "vendor.com/my-volume", 622 newClaimArray("claim12-2", "uid12-2", "1Gi", "pvc-uid12-2", v1.ClaimBound, &classExternal, volume.AnnBoundByController, volume.AnnBindCompleted))), 623 expectedEvents: []string{"Normal ExternalProvisioning"}, 624 errors: noerrors, 625 test: wrapTestWithInjectedOperation(ctx, wrapTestWithProvisionCalls([]provisionCall{}, testSyncClaim), func(ctrl *PersistentVolumeController, reactor *pvtesting.VolumeReactor) { 626 // Create a volume before syncClaim tries to bind a PV to PVC 627 // This simulates external provisioner creating a volume while the controller 628 // is waiting for a volume to bind to the existed claim 629 // the external provisioner workflow implemented in "provisionClaimOperationCSI" 630 // should issue an ExternalProvisioning event to signal that some external provisioner 631 // is working on provisioning the PV, also add the operation start timestamp into local cache 632 // operationTimestamps. Rely on the existences of the start time stamp to create a PV for binding 633 if ctrl.operationTimestamps.Has("default/claim12-2") { 634 volume := newVolume("pvc-uid12-2", "1Gi", "", "", v1.VolumeAvailable, v1.PersistentVolumeReclaimRetain, classExternal) 635 ctrl.volumes.store.Add(volume) // add the volume to controller 636 reactor.AddVolume(volume) 637 } 638 }), 639 }, 640 { 641 // provision a volume (external provisioner) but binding will not happen + normal event with external provisioner 642 name: "12-3 - external provisioner with volume to be provisioned", 643 initialVolumes: novolumes, 644 expectedVolumes: novolumes, 645 initialClaims: newClaimArray("claim12-3", "uid12-3", "1Gi", "", v1.ClaimPending, &classExternal), 646 expectedClaims: claimWithAnnotation(volume.AnnBetaStorageProvisioner, "vendor.com/my-volume", 647 claimWithAnnotation(volume.AnnStorageProvisioner, "vendor.com/my-volume", 648 newClaimArray("claim12-3", "uid12-3", "1Gi", "", v1.ClaimPending, &classExternal))), 649 expectedEvents: []string{"Normal ExternalProvisioning"}, 650 errors: noerrors, 651 test: wrapTestWithProvisionCalls([]provisionCall{provision1Success}, testSyncClaim), 652 }, 653 { 654 // provision a volume (external provisioner) and binding + normal event with external provisioner 655 name: "12-4 - external provisioner with volume provisioned/bound success", 656 initialVolumes: novolumes, 657 expectedVolumes: newVolumeArray("pvc-uid12-4", "1Gi", "uid12-4", "claim12-4", v1.VolumeBound, v1.PersistentVolumeReclaimRetain, classExternal, volume.AnnBoundByController), 658 initialClaims: newClaimArray("claim12-4", "uid12-4", "1Gi", "", v1.ClaimPending, &classExternal), 659 expectedClaims: claimWithAnnotation(volume.AnnBetaStorageProvisioner, "vendor.com/my-volume", 660 claimWithAnnotation(volume.AnnStorageProvisioner, "vendor.com/my-volume", 661 newClaimArray("claim12-4", "uid12-4", "1Gi", "pvc-uid12-4", v1.ClaimBound, &classExternal, volume.AnnBoundByController, volume.AnnBindCompleted))), 662 expectedEvents: []string{"Normal ExternalProvisioning"}, 663 errors: noerrors, 664 test: wrapTestWithInjectedOperation(ctx, wrapTestWithProvisionCalls([]provisionCall{}, testSyncClaim), func(ctrl *PersistentVolumeController, reactor *pvtesting.VolumeReactor) { 665 // Create a volume before syncClaim tries to bind a PV to PVC 666 // This simulates external provisioner creating a volume while the controller 667 // is waiting for a volume to bind to the existed claim 668 // the external provisioner workflow implemented in "provisionClaimOperationCSI" 669 // should issue an ExternalProvisioning event to signal that some external provisioner 670 // is working on provisioning the PV, also add the operation start timestamp into local cache 671 // operationTimestamps. Rely on the existences of the start time stamp to create a PV for binding 672 if ctrl.operationTimestamps.Has("default/claim12-4") { 673 volume := newVolume("pvc-uid12-4", "1Gi", "uid12-4", "claim12-4", v1.VolumeBound, v1.PersistentVolumeReclaimRetain, classExternal, volume.AnnBoundByController) 674 ctrl.volumes.store.Add(volume) // add the volume to controller 675 reactor.AddVolume(volume) 676 } 677 }), 678 }, 679 } 680 681 runMultisyncTests(t, ctx, tests, storageClasses, storageClasses[0].Name) 682 } 683 684 // When provisioning is disabled, provisioning a claim should instantly return nil 685 func TestDisablingDynamicProvisioner(t *testing.T) { 686 _, ctx := ktesting.NewTestContext(t) 687 ctrl, err := newTestController(ctx, nil, nil, false) 688 if err != nil { 689 t.Fatalf("Construct PersistentVolume controller failed: %v", err) 690 } 691 retVal := ctrl.provisionClaim(ctx, nil) 692 if retVal != nil { 693 t.Errorf("Expected nil return but got %v", retVal) 694 } 695 }