| /* |
| Copyright 2016 The Kubernetes Authors. |
| |
| Licensed under the Apache License, Version 2.0 (the "License"); |
| you may not use this file except in compliance with the License. |
| You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| */ |
| |
| package persistentvolume |
| |
| import ( |
| "errors" |
| |
| utilfeature "k8s.io/apiserver/pkg/util/feature" |
| featuregatetesting "k8s.io/component-base/featuregate/testing" |
| "k8s.io/klog/v2/ktesting" |
| "k8s.io/kubernetes/pkg/features" |
| "testing" |
| |
| v1 "k8s.io/api/core/v1" |
| storage "k8s.io/api/storage/v1" |
| apierrors "k8s.io/apimachinery/pkg/api/errors" |
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
| corelisters "k8s.io/client-go/listers/core/v1" |
| "k8s.io/client-go/tools/cache" |
| "k8s.io/component-helpers/storage/volume" |
| api "k8s.io/kubernetes/pkg/apis/core" |
| pvtesting "k8s.io/kubernetes/pkg/controller/volume/persistentvolume/testing" |
| ) |
| |
| var class1Parameters = map[string]string{ |
| "param1": "value1", |
| } |
| var class2Parameters = map[string]string{ |
| "param2": "value2", |
| } |
| var deleteReclaimPolicy = v1.PersistentVolumeReclaimDelete |
| var modeImmediate = storage.VolumeBindingImmediate |
| var storageClasses = []*storage.StorageClass{ |
| { |
| TypeMeta: metav1.TypeMeta{ |
| Kind: "StorageClass", |
| }, |
| |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "gold", |
| }, |
| |
| Provisioner: mockPluginName, |
| Parameters: class1Parameters, |
| ReclaimPolicy: &deleteReclaimPolicy, |
| VolumeBindingMode: &modeImmediate, |
| }, |
| { |
| TypeMeta: metav1.TypeMeta{ |
| Kind: "StorageClass", |
| }, |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "silver", |
| }, |
| Provisioner: mockPluginName, |
| Parameters: class2Parameters, |
| ReclaimPolicy: &deleteReclaimPolicy, |
| VolumeBindingMode: &modeImmediate, |
| }, |
| { |
| TypeMeta: metav1.TypeMeta{ |
| Kind: "StorageClass", |
| }, |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "copper", |
| }, |
| Provisioner: mockPluginName, |
| Parameters: class1Parameters, |
| ReclaimPolicy: &deleteReclaimPolicy, |
| VolumeBindingMode: &modeWait, |
| }, |
| { |
| TypeMeta: metav1.TypeMeta{ |
| Kind: "StorageClass", |
| }, |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "external", |
| }, |
| Provisioner: "vendor.com/my-volume", |
| Parameters: class1Parameters, |
| ReclaimPolicy: &deleteReclaimPolicy, |
| VolumeBindingMode: &modeImmediate, |
| }, |
| { |
| TypeMeta: metav1.TypeMeta{ |
| Kind: "StorageClass", |
| }, |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "external-wait", |
| }, |
| Provisioner: "vendor.com/my-volume-wait", |
| Parameters: class1Parameters, |
| ReclaimPolicy: &deleteReclaimPolicy, |
| VolumeBindingMode: &modeWait, |
| }, |
| { |
| TypeMeta: metav1.TypeMeta{ |
| Kind: "StorageClass", |
| }, |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "unknown-internal", |
| }, |
| Provisioner: "kubernetes.io/unknown", |
| Parameters: class1Parameters, |
| ReclaimPolicy: &deleteReclaimPolicy, |
| VolumeBindingMode: &modeImmediate, |
| }, |
| { |
| TypeMeta: metav1.TypeMeta{ |
| Kind: "StorageClass", |
| }, |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "unsupported-mountoptions", |
| }, |
| Provisioner: mockPluginName, |
| Parameters: class1Parameters, |
| ReclaimPolicy: &deleteReclaimPolicy, |
| MountOptions: []string{"foo"}, |
| VolumeBindingMode: &modeImmediate, |
| }, |
| { |
| TypeMeta: metav1.TypeMeta{ |
| Kind: "StorageClass", |
| }, |
| |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "csi", |
| }, |
| |
| Provisioner: "mydriver.csi.k8s.io", |
| Parameters: class1Parameters, |
| ReclaimPolicy: &deleteReclaimPolicy, |
| VolumeBindingMode: &modeImmediate, |
| }, |
| } |
| |
| // call to storageClass 1, returning an error |
| var provision1Error = provisionCall{ |
| ret: errors.New("Mock provisioner error"), |
| expectedParameters: class1Parameters, |
| } |
| |
| // call to storageClass 1, returning a valid PV |
| var provision1Success = provisionCall{ |
| ret: nil, |
| expectedParameters: class1Parameters, |
| } |
| |
| // call to storageClass 2, returning a valid PV |
| var provision2Success = provisionCall{ |
| ret: nil, |
| expectedParameters: class2Parameters, |
| } |
| |
| // Test single call to syncVolume, expecting provisioning to happen. |
| // 1. Fill in the controller with initial data |
| // 2. Call the syncVolume *once*. |
| // 3. Compare resulting volumes with expected volumes. |
| func TestProvisionSync(t *testing.T) { |
| // Default enable the HonorPVReclaimPolicy feature gate. |
| defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.HonorPVReclaimPolicy, true)() |
| _, ctx := ktesting.NewTestContext(t) |
| tests := []controllerTest{ |
| { |
| // Provision a volume (with a default class) |
| name: "11-1 - successful provision with storage class 1", |
| initialVolumes: novolumes, |
| expectedVolumes: volumesWithFinalizers(newVolumeArray("pvc-uid11-1", "1Gi", "uid11-1", "claim11-1", v1.VolumeBound, v1.PersistentVolumeReclaimDelete, classGold, volume.AnnBoundByController, volume.AnnDynamicallyProvisioned), []string{volume.PVDeletionInTreeProtectionFinalizer}), |
| // Binding will be completed in the next syncClaim |
| initialClaims: newClaimArray("claim11-1", "uid11-1", "1Gi", "", v1.ClaimPending, &classGold), |
| expectedClaims: newClaimArray("claim11-1", "uid11-1", "1Gi", "", v1.ClaimPending, &classGold, volume.AnnStorageProvisioner, volume.AnnBetaStorageProvisioner), |
| expectedEvents: []string{"Normal ProvisioningSucceeded"}, |
| errors: noerrors, |
| test: wrapTestWithProvisionCalls([]provisionCall{provision1Success}, testSyncClaim), |
| }, |
| { |
| // Provision failure - plugin not found |
| name: "11-2 - plugin not found", |
| initialVolumes: novolumes, |
| expectedVolumes: novolumes, |
| initialClaims: newClaimArray("claim11-2", "uid11-2", "1Gi", "", v1.ClaimPending, &classGold), |
| expectedClaims: newClaimArray("claim11-2", "uid11-2", "1Gi", "", v1.ClaimPending, &classGold), |
| expectedEvents: []string{"Warning ProvisioningFailed"}, |
| errors: noerrors, |
| test: testSyncClaim, |
| }, |
| { |
| // Provision failure - newProvisioner returns error |
| name: "11-3 - newProvisioner failure", |
| initialVolumes: novolumes, |
| expectedVolumes: novolumes, |
| initialClaims: newClaimArray("claim11-3", "uid11-3", "1Gi", "", v1.ClaimPending, &classGold), |
| expectedClaims: newClaimArray("claim11-3", "uid11-3", "1Gi", "", v1.ClaimPending, &classGold, volume.AnnStorageProvisioner, volume.AnnBetaStorageProvisioner), |
| expectedEvents: []string{"Warning ProvisioningFailed"}, |
| errors: noerrors, |
| test: wrapTestWithProvisionCalls([]provisionCall{}, testSyncClaim), |
| }, |
| { |
| // Provision failure - Provision returns error |
| name: "11-4 - provision failure", |
| initialVolumes: novolumes, |
| expectedVolumes: novolumes, |
| initialClaims: newClaimArray("claim11-4", "uid11-4", "1Gi", "", v1.ClaimPending, &classGold), |
| expectedClaims: newClaimArray("claim11-4", "uid11-4", "1Gi", "", v1.ClaimPending, &classGold, volume.AnnStorageProvisioner, volume.AnnBetaStorageProvisioner), |
| expectedEvents: []string{"Warning ProvisioningFailed"}, |
| errors: noerrors, |
| test: wrapTestWithProvisionCalls([]provisionCall{provision1Error}, testSyncClaim), |
| }, |
| { |
| // No provisioning if there is a matching volume available |
| name: "11-6 - provisioning when there is a volume available", |
| initialVolumes: newVolumeArray("volume11-6", "1Gi", "", "", v1.VolumeAvailable, v1.PersistentVolumeReclaimRetain, classGold), |
| expectedVolumes: newVolumeArray("volume11-6", "1Gi", "uid11-6", "claim11-6", v1.VolumeBound, v1.PersistentVolumeReclaimRetain, classGold, volume.AnnBoundByController), |
| initialClaims: newClaimArray("claim11-6", "uid11-6", "1Gi", "", v1.ClaimPending, &classGold), |
| expectedClaims: newClaimArray("claim11-6", "uid11-6", "1Gi", "volume11-6", v1.ClaimBound, &classGold, volume.AnnBoundByController, volume.AnnBindCompleted), |
| expectedEvents: noevents, |
| errors: noerrors, |
| // No provisioning plugin confingure - makes the test fail when |
| // the controller erroneously tries to provision something |
| test: wrapTestWithProvisionCalls([]provisionCall{provision1Success}, testSyncClaim), |
| }, |
| { |
| // Provision success? - claim is bound before provisioner creates |
| // a volume. |
| name: "11-7 - claim is bound before provisioning", |
| initialVolumes: novolumes, |
| expectedVolumes: newVolumeArray("pvc-uid11-7", "1Gi", "uid11-7", "claim11-7", v1.VolumeBound, v1.PersistentVolumeReclaimDelete, classGold, volume.AnnBoundByController, volume.AnnDynamicallyProvisioned), |
| initialClaims: newClaimArray("claim11-7", "uid11-7", "1Gi", "", v1.ClaimPending, &classGold), |
| // The claim would be bound in next syncClaim |
| expectedClaims: newClaimArray("claim11-7", "uid11-7", "1Gi", "", v1.ClaimPending, &classGold, volume.AnnStorageProvisioner, volume.AnnBetaStorageProvisioner), |
| expectedEvents: noevents, |
| errors: noerrors, |
| test: wrapTestWithInjectedOperation(ctx, wrapTestWithProvisionCalls([]provisionCall{}, testSyncClaim), func(ctrl *PersistentVolumeController, reactor *pvtesting.VolumeReactor) { |
| // Create a volume before provisionClaimOperation starts. |
| // This similates a parallel controller provisioning the volume. |
| volume := newVolume("pvc-uid11-7", "1Gi", "uid11-7", "claim11-7", v1.VolumeBound, v1.PersistentVolumeReclaimDelete, classGold, volume.AnnBoundByController, volume.AnnDynamicallyProvisioned) |
| reactor.AddVolume(volume) |
| }), |
| }, |
| { |
| // Provision success - cannot save provisioned PV once, |
| // second retry succeeds |
| name: "11-8 - cannot save provisioned volume", |
| initialVolumes: novolumes, |
| expectedVolumes: volumesWithFinalizers(newVolumeArray("pvc-uid11-8", "1Gi", "uid11-8", "claim11-8", v1.VolumeBound, v1.PersistentVolumeReclaimDelete, classGold, volume.AnnBoundByController, volume.AnnDynamicallyProvisioned), []string{volume.PVDeletionInTreeProtectionFinalizer}), |
| initialClaims: newClaimArray("claim11-8", "uid11-8", "1Gi", "", v1.ClaimPending, &classGold), |
| // Binding will be completed in the next syncClaim |
| expectedClaims: newClaimArray("claim11-8", "uid11-8", "1Gi", "", v1.ClaimPending, &classGold, volume.AnnStorageProvisioner, volume.AnnBetaStorageProvisioner), |
| expectedEvents: []string{"Normal ProvisioningSucceeded"}, |
| errors: []pvtesting.ReactorError{ |
| // Inject error to the first |
| // kubeclient.PersistentVolumes.Create() call. All other calls |
| // will succeed. |
| {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error")}, |
| }, |
| test: wrapTestWithProvisionCalls([]provisionCall{provision1Success}, testSyncClaim), |
| }, |
| { |
| // Provision success? - cannot save provisioned PV five times, |
| // volume is deleted and delete succeeds |
| name: "11-9 - cannot save provisioned volume, delete succeeds", |
| initialVolumes: novolumes, |
| expectedVolumes: novolumes, |
| initialClaims: newClaimArray("claim11-9", "uid11-9", "1Gi", "", v1.ClaimPending, &classGold), |
| expectedClaims: newClaimArray("claim11-9", "uid11-9", "1Gi", "", v1.ClaimPending, &classGold, volume.AnnStorageProvisioner, volume.AnnBetaStorageProvisioner), |
| expectedEvents: []string{"Warning ProvisioningFailed"}, |
| errors: []pvtesting.ReactorError{ |
| // Inject error to five kubeclient.PersistentVolumes.Create() |
| // calls |
| {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error1")}, |
| {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error2")}, |
| {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error3")}, |
| {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error4")}, |
| {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error5")}, |
| }, |
| test: wrapTestWithPluginCalls( |
| nil, // recycle calls |
| []error{nil}, // delete calls |
| []provisionCall{provision1Success}, // provision calls |
| testSyncClaim, |
| ), |
| }, |
| { |
| // Provision failure - cannot save provisioned PV five times, |
| // volume delete failed - no plugin found |
| name: "11-10 - cannot save provisioned volume, no delete plugin found", |
| initialVolumes: novolumes, |
| expectedVolumes: novolumes, |
| initialClaims: newClaimArray("claim11-10", "uid11-10", "1Gi", "", v1.ClaimPending, &classGold), |
| expectedClaims: newClaimArray("claim11-10", "uid11-10", "1Gi", "", v1.ClaimPending, &classGold, volume.AnnStorageProvisioner, volume.AnnBetaStorageProvisioner), |
| expectedEvents: []string{"Warning ProvisioningFailed", "Warning ProvisioningCleanupFailed"}, |
| errors: []pvtesting.ReactorError{ |
| // Inject error to five kubeclient.PersistentVolumes.Create() |
| // calls |
| {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error1")}, |
| {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error2")}, |
| {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error3")}, |
| {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error4")}, |
| {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error5")}, |
| }, |
| // No deleteCalls are configured, which results into no deleter plugin available for the volume |
| test: wrapTestWithProvisionCalls([]provisionCall{provision1Success}, testSyncClaim), |
| }, |
| { |
| // Provision failure - cannot save provisioned PV five times, |
| // volume delete failed - deleter returns error five times |
| name: "11-11 - cannot save provisioned volume, deleter fails", |
| initialVolumes: novolumes, |
| expectedVolumes: novolumes, |
| initialClaims: newClaimArray("claim11-11", "uid11-11", "1Gi", "", v1.ClaimPending, &classGold), |
| expectedClaims: newClaimArray("claim11-11", "uid11-11", "1Gi", "", v1.ClaimPending, &classGold, volume.AnnStorageProvisioner, volume.AnnBetaStorageProvisioner), |
| expectedEvents: []string{"Warning ProvisioningFailed", "Warning ProvisioningCleanupFailed"}, |
| errors: []pvtesting.ReactorError{ |
| // Inject error to five kubeclient.PersistentVolumes.Create() |
| // calls |
| {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error1")}, |
| {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error2")}, |
| {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error3")}, |
| {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error4")}, |
| {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error5")}, |
| }, |
| test: wrapTestWithPluginCalls( |
| nil, // recycle calls |
| []error{ // delete calls |
| errors.New("Mock deletion error1"), |
| errors.New("Mock deletion error2"), |
| errors.New("Mock deletion error3"), |
| errors.New("Mock deletion error4"), |
| errors.New("Mock deletion error5"), |
| }, |
| []provisionCall{provision1Success}, // provision calls |
| testSyncClaim), |
| }, |
| { |
| // Provision failure - cannot save provisioned PV five times, |
| // volume delete succeeds 2nd time |
| name: "11-12 - cannot save provisioned volume, delete succeeds 2nd time", |
| initialVolumes: novolumes, |
| expectedVolumes: novolumes, |
| initialClaims: newClaimArray("claim11-12", "uid11-12", "1Gi", "", v1.ClaimPending, &classGold), |
| expectedClaims: newClaimArray("claim11-12", "uid11-12", "1Gi", "", v1.ClaimPending, &classGold, volume.AnnStorageProvisioner, volume.AnnBetaStorageProvisioner), |
| expectedEvents: []string{"Warning ProvisioningFailed"}, |
| errors: []pvtesting.ReactorError{ |
| // Inject error to five kubeclient.PersistentVolumes.Create() |
| // calls |
| {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error1")}, |
| {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error2")}, |
| {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error3")}, |
| {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error4")}, |
| {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error5")}, |
| }, |
| test: wrapTestWithPluginCalls( |
| nil, // recycle calls |
| []error{ // delete calls |
| errors.New("Mock deletion error1"), |
| nil, |
| }, // provison calls |
| []provisionCall{provision1Success}, |
| testSyncClaim, |
| ), |
| }, |
| { |
| // Provision a volume (with non-default class) |
| name: "11-13 - successful provision with storage class 2", |
| initialVolumes: novolumes, |
| expectedVolumes: volumesWithFinalizers(newVolumeArray("pvc-uid11-13", "1Gi", "uid11-13", "claim11-13", v1.VolumeBound, v1.PersistentVolumeReclaimDelete, classSilver, volume.AnnBoundByController, volume.AnnDynamicallyProvisioned), []string{volume.PVDeletionInTreeProtectionFinalizer}), |
| initialClaims: newClaimArray("claim11-13", "uid11-13", "1Gi", "", v1.ClaimPending, &classSilver), |
| // Binding will be completed in the next syncClaim |
| expectedClaims: newClaimArray("claim11-13", "uid11-13", "1Gi", "", v1.ClaimPending, &classSilver, volume.AnnStorageProvisioner, volume.AnnBetaStorageProvisioner), |
| expectedEvents: []string{"Normal ProvisioningSucceeded"}, |
| errors: noerrors, |
| test: wrapTestWithProvisionCalls([]provisionCall{provision2Success}, testSyncClaim), |
| }, |
| { |
| // Provision error - non existing class |
| name: "11-14 - fail due to non-existing class", |
| initialVolumes: novolumes, |
| expectedVolumes: novolumes, |
| initialClaims: newClaimArray("claim11-14", "uid11-14", "1Gi", "", v1.ClaimPending, &classNonExisting), |
| expectedClaims: newClaimArray("claim11-14", "uid11-14", "1Gi", "", v1.ClaimPending, &classNonExisting), |
| expectedEvents: noevents, |
| errors: noerrors, |
| test: wrapTestWithProvisionCalls([]provisionCall{}, testSyncClaim), |
| }, |
| { |
| // No provisioning with class="" |
| name: "11-15 - no provisioning with class=''", |
| initialVolumes: novolumes, |
| expectedVolumes: novolumes, |
| initialClaims: newClaimArray("claim11-15", "uid11-15", "1Gi", "", v1.ClaimPending, &classEmpty), |
| expectedClaims: newClaimArray("claim11-15", "uid11-15", "1Gi", "", v1.ClaimPending, &classEmpty), |
| expectedEvents: noevents, |
| errors: noerrors, |
| test: wrapTestWithProvisionCalls([]provisionCall{}, testSyncClaim), |
| }, |
| { |
| // No provisioning with class=nil |
| name: "11-16 - no provisioning with class=nil", |
| initialVolumes: novolumes, |
| expectedVolumes: novolumes, |
| initialClaims: newClaimArray("claim11-15", "uid11-15", "1Gi", "", v1.ClaimPending, nil), |
| expectedClaims: newClaimArray("claim11-15", "uid11-15", "1Gi", "", v1.ClaimPending, nil), |
| expectedEvents: noevents, |
| errors: noerrors, |
| test: wrapTestWithProvisionCalls([]provisionCall{}, testSyncClaim), |
| }, |
| { |
| // No provisioning + normal event with external provisioner |
| name: "11-17 - external provisioner", |
| initialVolumes: novolumes, |
| expectedVolumes: novolumes, |
| initialClaims: newClaimArray("claim11-17", "uid11-17", "1Gi", "", v1.ClaimPending, &classExternal), |
| expectedClaims: claimWithAnnotation(volume.AnnBetaStorageProvisioner, "vendor.com/my-volume", |
| claimWithAnnotation(volume.AnnStorageProvisioner, "vendor.com/my-volume", |
| newClaimArray("claim11-17", "uid11-17", "1Gi", "", v1.ClaimPending, &classExternal))), |
| expectedEvents: []string{"Normal ExternalProvisioning"}, |
| errors: noerrors, |
| test: wrapTestWithProvisionCalls([]provisionCall{}, testSyncClaim), |
| }, |
| { |
| // No provisioning + warning event with unknown internal provisioner |
| name: "11-18 - unknown internal provisioner", |
| initialVolumes: novolumes, |
| expectedVolumes: novolumes, |
| initialClaims: newClaimArray("claim11-18", "uid11-18", "1Gi", "", v1.ClaimPending, &classUnknownInternal), |
| expectedClaims: newClaimArray("claim11-18", "uid11-18", "1Gi", "", v1.ClaimPending, &classUnknownInternal), |
| expectedEvents: []string{"Warning ProvisioningFailed"}, |
| errors: noerrors, |
| test: wrapTestWithProvisionCalls([]provisionCall{}, testSyncClaim), |
| }, |
| { |
| // Provision success - first save of a PV to API server fails (API |
| // server has written the object to etcd, but crashed before sending |
| // 200 OK response to the controller). Controller retries and the |
| // second save of the PV returns "AlreadyExists" because the PV |
| // object already is in the API server. |
| // |
| "11-19 - provisioned volume saved but API server crashed", |
| novolumes, |
| // We don't actually simulate API server saving the object and |
| // crashing afterwards, Create() just returns error without saving |
| // the volume in this test. So the set of expected volumes at the |
| // end of the test is empty. |
| novolumes, |
| newClaimArray("claim11-19", "uid11-19", "1Gi", "", v1.ClaimPending, &classGold), |
| newClaimArray("claim11-19", "uid11-19", "1Gi", "", v1.ClaimPending, &classGold, volume.AnnStorageProvisioner, volume.AnnBetaStorageProvisioner), |
| noevents, |
| []pvtesting.ReactorError{ |
| // Inject errors to simulate crashed API server during |
| // kubeclient.PersistentVolumes.Create() |
| {Verb: "create", Resource: "persistentvolumes", Error: errors.New("Mock creation error1")}, |
| {Verb: "create", Resource: "persistentvolumes", Error: apierrors.NewAlreadyExists(api.Resource("persistentvolumes"), "")}, |
| }, |
| wrapTestWithPluginCalls( |
| nil, // recycle calls |
| nil, // delete calls - if Delete was called the test would fail |
| []provisionCall{provision1Success}, |
| testSyncClaim, |
| ), |
| }, |
| { |
| // No provisioning + warning event with unsupported storageClass.mountOptions |
| name: "11-20 - unsupported storageClass.mountOptions", |
| initialVolumes: novolumes, |
| expectedVolumes: novolumes, |
| initialClaims: newClaimArray("claim11-20", "uid11-20", "1Gi", "", v1.ClaimPending, &classUnsupportedMountOptions), |
| expectedClaims: newClaimArray("claim11-20", "uid11-20", "1Gi", "", v1.ClaimPending, &classUnsupportedMountOptions, volume.AnnStorageProvisioner, volume.AnnBetaStorageProvisioner), |
| // Expect event to be prefixed with "Mount options" because saving PV will fail anyway |
| expectedEvents: []string{"Warning ProvisioningFailed Mount options"}, |
| errors: noerrors, |
| test: wrapTestWithProvisionCalls([]provisionCall{}, testSyncClaim), |
| }, |
| { |
| // No provisioning due to CSI migration + normal event with external provisioner |
| name: "11-21 - external provisioner for CSI migration", |
| initialVolumes: novolumes, |
| expectedVolumes: novolumes, |
| initialClaims: newClaimArray("claim11-21", "uid11-21", "1Gi", "", v1.ClaimPending, &classGold), |
| expectedClaims: []*v1.PersistentVolumeClaim{ |
| annotateClaim( |
| newClaim("claim11-21", "uid11-21", "1Gi", "", v1.ClaimPending, &classGold), |
| map[string]string{ |
| volume.AnnStorageProvisioner: "vendor.com/MockCSIDriver", |
| volume.AnnBetaStorageProvisioner: "vendor.com/MockCSIDriver", |
| volume.AnnMigratedTo: "vendor.com/MockCSIDriver", |
| }), |
| }, |
| expectedEvents: []string{"Normal ExternalProvisioning"}, |
| errors: noerrors, |
| test: wrapTestWithCSIMigrationProvisionCalls(testSyncClaim), |
| }, |
| { |
| // volume provisioned and available |
| // in this case, NO normal event with external provisioner should be issued |
| name: "11-22 - external provisioner with volume available", |
| initialVolumes: newVolumeArray("volume11-22", "1Gi", "", "", v1.VolumeAvailable, v1.PersistentVolumeReclaimRetain, classExternal), |
| expectedVolumes: newVolumeArray("volume11-22", "1Gi", "uid11-22", "claim11-22", v1.VolumeBound, v1.PersistentVolumeReclaimRetain, classExternal, volume.AnnBoundByController), |
| initialClaims: newClaimArray("claim11-22", "uid11-22", "1Gi", "", v1.ClaimPending, &classExternal), |
| expectedClaims: newClaimArray("claim11-22", "uid11-22", "1Gi", "volume11-22", v1.ClaimBound, &classExternal, volume.AnnBoundByController, volume.AnnBindCompleted), |
| expectedEvents: noevents, |
| errors: noerrors, |
| test: wrapTestWithProvisionCalls([]provisionCall{}, testSyncClaim), |
| }, |
| { |
| // volume provision for PVC scheduled |
| "11-23 - skip finding PV and provision for PVC annotated with AnnSelectedNode", |
| newVolumeArray("volume11-23", "1Gi", "", "", v1.VolumeAvailable, v1.PersistentVolumeReclaimDelete, classCopper), |
| []*v1.PersistentVolume{ |
| 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*/), |
| newVolumeWithFinalizers("pvc-uid11-23", "1Gi", "uid11-23", "claim11-23", v1.VolumeBound, v1.PersistentVolumeReclaimDelete, classCopper, []string{volume.PVDeletionInTreeProtectionFinalizer}, volume.AnnDynamicallyProvisioned, volume.AnnBoundByController), |
| }, |
| claimWithAnnotation(volume.AnnSelectedNode, "node1", |
| newClaimArray("claim11-23", "uid11-23", "1Gi", "", v1.ClaimPending, &classCopper)), |
| claimWithAnnotation(volume.AnnSelectedNode, "node1", |
| newClaimArray("claim11-23", "uid11-23", "1Gi", "", v1.ClaimPending, &classCopper, volume.AnnStorageProvisioner, volume.AnnBetaStorageProvisioner)), |
| []string{"Normal ProvisioningSucceeded"}, |
| noerrors, |
| wrapTestWithInjectedOperation(ctx, wrapTestWithProvisionCalls([]provisionCall{provision1Success}, testSyncClaim), |
| func(ctrl *PersistentVolumeController, reactor *pvtesting.VolumeReactor) { |
| nodesIndexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{}) |
| node := &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "node1"}} |
| nodesIndexer.Add(node) |
| ctrl.NodeLister = corelisters.NewNodeLister(nodesIndexer) |
| }), |
| }, |
| { |
| // volume provision for PVC that scheduled |
| name: "11-24 - skip finding PV and wait external provisioner for PVC annotated with AnnSelectedNode", |
| initialVolumes: newVolumeArray("volume11-24", "1Gi", "", "", v1.VolumeAvailable, v1.PersistentVolumeReclaimDelete, classExternalWait), |
| expectedVolumes: newVolumeArray("volume11-24", "1Gi", "", "", v1.VolumeAvailable, v1.PersistentVolumeReclaimDelete, classExternalWait), |
| initialClaims: claimWithAnnotation(volume.AnnSelectedNode, "node1", |
| newClaimArray("claim11-24", "uid11-24", "1Gi", "", v1.ClaimPending, &classExternalWait)), |
| expectedClaims: claimWithAnnotation(volume.AnnBetaStorageProvisioner, "vendor.com/my-volume-wait", |
| claimWithAnnotation(volume.AnnStorageProvisioner, "vendor.com/my-volume-wait", |
| claimWithAnnotation(volume.AnnSelectedNode, "node1", |
| newClaimArray("claim11-24", "uid11-24", "1Gi", "", v1.ClaimPending, &classExternalWait)))), |
| expectedEvents: []string{"Normal ExternalProvisioning"}, |
| errors: noerrors, |
| test: testSyncClaim, |
| }, |
| { |
| // Provision a volume with a data source will fail |
| // for in-tree plugins |
| name: "11-25 - failed in-tree provision with data source", |
| initialVolumes: novolumes, |
| expectedVolumes: novolumes, |
| initialClaims: claimWithDataSource("test-snap", "VolumeSnapshot", "snapshot.storage.k8s.io", newClaimArray("claim11-25", "uid11-25", "1Gi", "", v1.ClaimPending, &classGold)), |
| expectedClaims: claimWithDataSource("test-snap", "VolumeSnapshot", "snapshot.storage.k8s.io", newClaimArray("claim11-25", "uid11-25", "1Gi", "", v1.ClaimPending, &classGold)), |
| expectedEvents: []string{"Warning ProvisioningFailed"}, |
| errors: noerrors, |
| test: testSyncClaim, |
| }, |
| { |
| // Provision a volume with a data source will proceed |
| // for CSI plugins |
| "11-26 - csi with data source", |
| novolumes, |
| novolumes, |
| claimWithAnnotation(volume.AnnStorageProvisioner, "mydriver.csi.k8s.io", |
| claimWithDataSource("test-snap", "VolumeSnapshot", "snapshot.storage.k8s.io", newClaimArray("claim11-26", "uid11-26", "1Gi", "", v1.ClaimPending, &classCSI))), |
| claimWithAnnotation(volume.AnnStorageProvisioner, "mydriver.csi.k8s.io", |
| claimWithDataSource("test-snap", "VolumeSnapshot", "snapshot.storage.k8s.io", newClaimArray("claim11-26", "uid11-26", "1Gi", "", v1.ClaimPending, &classCSI))), |
| []string{"Normal ExternalProvisioning"}, |
| noerrors, |
| wrapTestWithProvisionCalls([]provisionCall{}, testSyncClaim), |
| }, |
| } |
| runSyncTests(t, ctx, tests, storageClasses, []*v1.Pod{}) |
| } |
| |
| // Test multiple calls to syncClaim/syncVolume and periodic sync of all |
| // volume/claims. The test follows this pattern: |
| // 0. Load the controller with initial data. |
| // 1. Call controllerTest.testCall() once as in TestSync() |
| // 2. For all volumes/claims changed by previous syncVolume/syncClaim calls, |
| // call appropriate syncVolume/syncClaim (simulating "volume/claim changed" |
| // events). Go to 2. if these calls change anything. |
| // 3. When all changes are processed and no new changes were made, call |
| // syncVolume/syncClaim on all volumes/claims (simulating "periodic sync"). |
| // 4. If some changes were done by step 3., go to 2. (simulation of |
| // "volume/claim updated" events, eventually performing step 3. again) |
| // 5. When 3. does not do any changes, finish the tests and compare final set |
| // of volumes/claims with expected claims/volumes and report differences. |
| // |
| // Some limit of calls in enforced to prevent endless loops. |
| func TestProvisionMultiSync(t *testing.T) { |
| _, ctx := ktesting.NewTestContext(t) |
| tests := []controllerTest{ |
| { |
| // Provision a volume with binding |
| name: "12-1 - successful provision", |
| initialVolumes: novolumes, |
| expectedVolumes: newVolumeArray("pvc-uid12-1", "1Gi", "uid12-1", "claim12-1", v1.VolumeBound, v1.PersistentVolumeReclaimDelete, classGold, volume.AnnBoundByController, volume.AnnDynamicallyProvisioned), |
| initialClaims: newClaimArray("claim12-1", "uid12-1", "1Gi", "", v1.ClaimPending, &classGold), |
| expectedClaims: newClaimArray("claim12-1", "uid12-1", "1Gi", "pvc-uid12-1", v1.ClaimBound, &classGold, volume.AnnBoundByController, volume.AnnBindCompleted, volume.AnnStorageProvisioner, volume.AnnBetaStorageProvisioner), |
| expectedEvents: noevents, |
| errors: noerrors, |
| test: wrapTestWithProvisionCalls([]provisionCall{provision1Success}, testSyncClaim), |
| }, |
| { |
| // provision a volume (external provisioner) and binding + normal event with external provisioner |
| name: "12-2 - external provisioner with volume provisioned success", |
| initialVolumes: novolumes, |
| expectedVolumes: newVolumeArray("pvc-uid12-2", "1Gi", "uid12-2", "claim12-2", v1.VolumeBound, v1.PersistentVolumeReclaimRetain, classExternal, volume.AnnBoundByController), |
| initialClaims: newClaimArray("claim12-2", "uid12-2", "1Gi", "", v1.ClaimPending, &classExternal), |
| expectedClaims: claimWithAnnotation(volume.AnnBetaStorageProvisioner, "vendor.com/my-volume", |
| claimWithAnnotation(volume.AnnStorageProvisioner, "vendor.com/my-volume", |
| newClaimArray("claim12-2", "uid12-2", "1Gi", "pvc-uid12-2", v1.ClaimBound, &classExternal, volume.AnnBoundByController, volume.AnnBindCompleted))), |
| expectedEvents: []string{"Normal ExternalProvisioning"}, |
| errors: noerrors, |
| test: wrapTestWithInjectedOperation(ctx, wrapTestWithProvisionCalls([]provisionCall{}, testSyncClaim), func(ctrl *PersistentVolumeController, reactor *pvtesting.VolumeReactor) { |
| // Create a volume before syncClaim tries to bind a PV to PVC |
| // This simulates external provisioner creating a volume while the controller |
| // is waiting for a volume to bind to the existed claim |
| // the external provisioner workflow implemented in "provisionClaimOperationCSI" |
| // should issue an ExternalProvisioning event to signal that some external provisioner |
| // is working on provisioning the PV, also add the operation start timestamp into local cache |
| // operationTimestamps. Rely on the existences of the start time stamp to create a PV for binding |
| if ctrl.operationTimestamps.Has("default/claim12-2") { |
| volume := newVolume("pvc-uid12-2", "1Gi", "", "", v1.VolumeAvailable, v1.PersistentVolumeReclaimRetain, classExternal) |
| ctrl.volumes.store.Add(volume) // add the volume to controller |
| reactor.AddVolume(volume) |
| } |
| }), |
| }, |
| { |
| // provision a volume (external provisioner) but binding will not happen + normal event with external provisioner |
| name: "12-3 - external provisioner with volume to be provisioned", |
| initialVolumes: novolumes, |
| expectedVolumes: novolumes, |
| initialClaims: newClaimArray("claim12-3", "uid12-3", "1Gi", "", v1.ClaimPending, &classExternal), |
| expectedClaims: claimWithAnnotation(volume.AnnBetaStorageProvisioner, "vendor.com/my-volume", |
| claimWithAnnotation(volume.AnnStorageProvisioner, "vendor.com/my-volume", |
| newClaimArray("claim12-3", "uid12-3", "1Gi", "", v1.ClaimPending, &classExternal))), |
| expectedEvents: []string{"Normal ExternalProvisioning"}, |
| errors: noerrors, |
| test: wrapTestWithProvisionCalls([]provisionCall{provision1Success}, testSyncClaim), |
| }, |
| { |
| // provision a volume (external provisioner) and binding + normal event with external provisioner |
| name: "12-4 - external provisioner with volume provisioned/bound success", |
| initialVolumes: novolumes, |
| expectedVolumes: newVolumeArray("pvc-uid12-4", "1Gi", "uid12-4", "claim12-4", v1.VolumeBound, v1.PersistentVolumeReclaimRetain, classExternal, volume.AnnBoundByController), |
| initialClaims: newClaimArray("claim12-4", "uid12-4", "1Gi", "", v1.ClaimPending, &classExternal), |
| expectedClaims: claimWithAnnotation(volume.AnnBetaStorageProvisioner, "vendor.com/my-volume", |
| claimWithAnnotation(volume.AnnStorageProvisioner, "vendor.com/my-volume", |
| newClaimArray("claim12-4", "uid12-4", "1Gi", "pvc-uid12-4", v1.ClaimBound, &classExternal, volume.AnnBoundByController, volume.AnnBindCompleted))), |
| expectedEvents: []string{"Normal ExternalProvisioning"}, |
| errors: noerrors, |
| test: wrapTestWithInjectedOperation(ctx, wrapTestWithProvisionCalls([]provisionCall{}, testSyncClaim), func(ctrl *PersistentVolumeController, reactor *pvtesting.VolumeReactor) { |
| // Create a volume before syncClaim tries to bind a PV to PVC |
| // This simulates external provisioner creating a volume while the controller |
| // is waiting for a volume to bind to the existed claim |
| // the external provisioner workflow implemented in "provisionClaimOperationCSI" |
| // should issue an ExternalProvisioning event to signal that some external provisioner |
| // is working on provisioning the PV, also add the operation start timestamp into local cache |
| // operationTimestamps. Rely on the existences of the start time stamp to create a PV for binding |
| if ctrl.operationTimestamps.Has("default/claim12-4") { |
| volume := newVolume("pvc-uid12-4", "1Gi", "uid12-4", "claim12-4", v1.VolumeBound, v1.PersistentVolumeReclaimRetain, classExternal, volume.AnnBoundByController) |
| ctrl.volumes.store.Add(volume) // add the volume to controller |
| reactor.AddVolume(volume) |
| } |
| }), |
| }, |
| } |
| |
| runMultisyncTests(t, ctx, tests, storageClasses, storageClasses[0].Name) |
| } |
| |
| // When provisioning is disabled, provisioning a claim should instantly return nil |
| func TestDisablingDynamicProvisioner(t *testing.T) { |
| _, ctx := ktesting.NewTestContext(t) |
| ctrl, err := newTestController(ctx, nil, nil, false) |
| if err != nil { |
| t.Fatalf("Construct PersistentVolume controller failed: %v", err) |
| } |
| retVal := ctrl.provisionClaim(ctx, nil) |
| if retVal != nil { |
| t.Errorf("Expected nil return but got %v", retVal) |
| } |
| } |