From 6b90a3d009aceee22a349e3db8c9d5b747122f49 Mon Sep 17 00:00:00 2001 From: Chris Henzie Date: Wed, 10 Mar 2021 13:30:38 -0800 Subject: [PATCH] Map PV access modes to CSI access modes based on driver capability --- pkg/controller/controller.go | 57 ++++++++-- pkg/controller/controller_test.go | 167 ++++++++++++++++++++++++++---- 2 files changed, 195 insertions(+), 29 deletions(-) diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index 54e2339f95..3c92a3ee1b 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -470,23 +470,70 @@ func getAccessMode(pvcAccessMode v1.PersistentVolumeAccessMode) *csi.VolumeCapab } } +func getSingleNodeMultiWriterCapableAccessMode(pvcAccessMode v1.PersistentVolumeAccessMode) *csi.VolumeCapability_AccessMode { + switch pvcAccessMode { + case v1.ReadWriteOnce: + return &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_MULTI_WRITER, + } + case v1.ReadWriteMany: + return &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_MULTI_NODE_MULTI_WRITER, + } + case v1.ReadOnlyMany: + return &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_MULTI_NODE_READER_ONLY, + } + case v1.ReadWriteOncePod: + return &csi.VolumeCapability_AccessMode{ + Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_SINGLE_WRITER, + } + default: + return nil + } +} + func getVolumeCapability( claim *v1.PersistentVolumeClaim, sc *storagev1.StorageClass, pvcAccessMode v1.PersistentVolumeAccessMode, fsType string, + supportsSingleNodeMultiWriter bool, ) *csi.VolumeCapability { + var accessMode *csi.VolumeCapability_AccessMode + if supportsSingleNodeMultiWriter { + accessMode = getSingleNodeMultiWriterCapableAccessMode(pvcAccessMode) + } else { + accessMode = getAccessMode(pvcAccessMode) + } + if util.CheckPersistentVolumeClaimModeBlock(claim) { return &csi.VolumeCapability{ AccessType: getAccessTypeBlock(), - AccessMode: getAccessMode(pvcAccessMode), + AccessMode: accessMode, } } return &csi.VolumeCapability{ AccessType: getAccessTypeMount(fsType, sc.MountOptions), - AccessMode: getAccessMode(pvcAccessMode), + AccessMode: accessMode, } +} +func (p *csiProvisioner) getVolumeCapabilities( + claim *v1.PersistentVolumeClaim, + sc *storagev1.StorageClass, + fsType string, +) []*csi.VolumeCapability { + supportsSingleNodeMultiWriter := false + if p.controllerCapabilities[csi.ControllerServiceCapability_RPC_SINGLE_NODE_MULTI_WRITER] { + supportsSingleNodeMultiWriter = true + } + + volumeCaps := make([]*csi.VolumeCapability, 0) + for _, pvcAccessMode := range claim.Spec.AccessModes { + volumeCaps = append(volumeCaps, getVolumeCapability(claim, sc, pvcAccessMode, fsType, supportsSingleNodeMultiWriter)) + } + return volumeCaps } type prepareProvisionResult struct { @@ -581,11 +628,7 @@ func (p *csiProvisioner) prepareProvision(ctx context.Context, claim *v1.Persist capacity := claim.Spec.Resources.Requests[v1.ResourceName(v1.ResourceStorage)] volSizeBytes := capacity.Value() - // Get access mode - volumeCaps := make([]*csi.VolumeCapability, 0) - for _, pvcAccessMode := range claim.Spec.AccessModes { - volumeCaps = append(volumeCaps, getVolumeCapability(claim, sc, pvcAccessMode, fsType)) - } + volumeCaps := p.getVolumeCapabilities(claim, sc, fsType) // Create a CSI CreateVolumeRequest and Response req := csi.CreateVolumeRequest{ diff --git a/pkg/controller/controller_test.go b/pkg/controller/controller_test.go index 3f881adb0d..df21a40ea4 100644 --- a/pkg/controller/controller_test.go +++ b/pkg/controller/controller_test.go @@ -462,6 +462,15 @@ func provisionCapabilities() (rpc.PluginCapabilitySet, rpc.ControllerCapabilityS } } +func provisionWithSingleNodeMultiWriterCapabilities() (rpc.PluginCapabilitySet, rpc.ControllerCapabilitySet) { + return rpc.PluginCapabilitySet{ + csi.PluginCapability_Service_CONTROLLER_SERVICE: true, + }, rpc.ControllerCapabilitySet{ + csi.ControllerServiceCapability_RPC_CREATE_DELETE_VOLUME: true, + csi.ControllerServiceCapability_RPC_SINGLE_NODE_MULTI_WRITER: true, + } +} + func provisionFromSnapshotCapabilities() (rpc.PluginCapabilitySet, rpc.ControllerCapabilitySet) { return rpc.PluginCapabilitySet{ csi.PluginCapability_Service_CONTROLLER_SERVICE: true, @@ -805,26 +814,27 @@ func TestGetSecretReference(t *testing.T) { } type provisioningTestcase struct { - capacity int64 // if zero, default capacity, otherwise available bytes - volOpts controller.ProvisionOptions - notNilSelector bool - makeVolumeNameErr bool - getSecretRefErr bool - getCredentialsErr bool - volWithLessCap bool - volWithZeroCap bool - expectedPVSpec *pvSpec - clientSetObjects []runtime.Object - createVolumeError error - expectErr bool - expectState controller.ProvisioningState - expectCreateVolDo func(t *testing.T, ctx context.Context, req *csi.CreateVolumeRequest) - withExtraMetadata bool - skipCreateVolume bool - deploymentNode string // fake distributed provisioning with this node as host - immediateBinding bool // enable immediate binding support for distributed provisioning - expectSelectedNode string // a specific selected-node of the PVC in the apiserver after the test, same as before if empty - expectNoProvision bool // if true, then ShouldProvision should return false + capacity int64 // if zero, default capacity, otherwise available bytes + volOpts controller.ProvisionOptions + notNilSelector bool + makeVolumeNameErr bool + getSecretRefErr bool + getCredentialsErr bool + volWithLessCap bool + volWithZeroCap bool + expectedPVSpec *pvSpec + clientSetObjects []runtime.Object + createVolumeError error + expectErr bool + expectState controller.ProvisioningState + expectCreateVolDo func(t *testing.T, ctx context.Context, req *csi.CreateVolumeRequest) + withExtraMetadata bool + skipCreateVolume bool + deploymentNode string // fake distributed provisioning with this node as host + immediateBinding bool // enable immediate binding support for distributed provisioning + expectSelectedNode string // a specific selected-node of the PVC in the apiserver after the test, same as before if empty + expectNoProvision bool // if true, then ShouldProvision should return false + supportsSingleNodeMultiWriter bool // if true, then provision with single node multi writer capabilities } type provisioningFSTypeTestcase struct { @@ -1258,7 +1268,7 @@ func provisionTestcases() (int64, map[string]provisioningTestcase) { }, expectState: controller.ProvisioningFinished, }, - "provision with access mode single writer": { + "provision with access mode single node writer": { volOpts: controller.ProvisionOptions{ StorageClass: &storagev1.StorageClass{ ReclaimPolicy: &deletePolicy, @@ -1310,6 +1320,112 @@ func provisionTestcases() (int64, map[string]provisioningTestcase) { }, expectState: controller.ProvisioningFinished, }, + "provision with access mode single node writer and single node multi writer capability": { + supportsSingleNodeMultiWriter: true, + volOpts: controller.ProvisionOptions{ + StorageClass: &storagev1.StorageClass{ + ReclaimPolicy: &deletePolicy, + Parameters: map[string]string{}, + }, + PVName: "test-name", + PVC: &v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + UID: "testid", + Annotations: driverNameAnnotation, + }, + Spec: v1.PersistentVolumeClaimSpec{ + Selector: nil, + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceName(v1.ResourceStorage): resource.MustParse(strconv.FormatInt(requestedBytes, 10)), + }, + }, + AccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce}, + }, + }, + }, + expectedPVSpec: &pvSpec{ + Name: "test-testi", + ReclaimPolicy: v1.PersistentVolumeReclaimDelete, + AccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce}, + Capacity: v1.ResourceList{ + v1.ResourceName(v1.ResourceStorage): bytesToQuantity(requestedBytes), + }, + CSIPVS: &v1.CSIPersistentVolumeSource{ + Driver: "test-driver", + VolumeHandle: "test-volume-id", + FSType: "ext4", + VolumeAttributes: map[string]string{ + "storage.kubernetes.io/csiProvisionerIdentity": "test-provisioner", + }, + }, + }, + expectCreateVolDo: func(t *testing.T, ctx context.Context, req *csi.CreateVolumeRequest) { + if len(req.GetVolumeCapabilities()) != 1 { + t.Errorf("Incorrect length in volume capabilities") + } + if req.GetVolumeCapabilities()[0].GetAccessMode() == nil { + t.Errorf("Expected access mode to be set") + } + if req.GetVolumeCapabilities()[0].GetAccessMode().GetMode() != csi.VolumeCapability_AccessMode_SINGLE_NODE_MULTI_WRITER { + t.Errorf("Expected single_node_multi_writer") + } + }, + expectState: controller.ProvisioningFinished, + }, + "provision with access mode single node single writer and single node multi writer capability": { + supportsSingleNodeMultiWriter: true, + volOpts: controller.ProvisionOptions{ + StorageClass: &storagev1.StorageClass{ + ReclaimPolicy: &deletePolicy, + Parameters: map[string]string{}, + }, + PVName: "test-name", + PVC: &v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + UID: "testid", + Annotations: driverNameAnnotation, + }, + Spec: v1.PersistentVolumeClaimSpec{ + Selector: nil, + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceName(v1.ResourceStorage): resource.MustParse(strconv.FormatInt(requestedBytes, 10)), + }, + }, + AccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteOncePod}, + }, + }, + }, + expectedPVSpec: &pvSpec{ + Name: "test-testi", + ReclaimPolicy: v1.PersistentVolumeReclaimDelete, + AccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteOncePod}, + Capacity: v1.ResourceList{ + v1.ResourceName(v1.ResourceStorage): bytesToQuantity(requestedBytes), + }, + CSIPVS: &v1.CSIPersistentVolumeSource{ + Driver: "test-driver", + VolumeHandle: "test-volume-id", + FSType: "ext4", + VolumeAttributes: map[string]string{ + "storage.kubernetes.io/csiProvisionerIdentity": "test-provisioner", + }, + }, + }, + expectCreateVolDo: func(t *testing.T, ctx context.Context, req *csi.CreateVolumeRequest) { + if len(req.GetVolumeCapabilities()) != 1 { + t.Errorf("Incorrect length in volume capabilities") + } + if req.GetVolumeCapabilities()[0].GetAccessMode() == nil { + t.Errorf("Expected access mode to be set") + } + if req.GetVolumeCapabilities()[0].GetAccessMode().GetMode() != csi.VolumeCapability_AccessMode_SINGLE_NODE_SINGLE_WRITER { + t.Errorf("Expected single_node_multi_writer") + } + }, + expectState: controller.ProvisioningFinished, + }, "provision with multiple access modes": { volOpts: controller.ProvisionOptions{ StorageClass: &storagev1.StorageClass{ @@ -2299,7 +2415,14 @@ func runProvisionTest(t *testing.T, tc provisioningTestcase, requestedBytes int6 } } - pluginCaps, controllerCaps := provisionCapabilities() + var pluginCaps rpc.PluginCapabilitySet + var controllerCaps rpc.ControllerCapabilitySet + if tc.supportsSingleNodeMultiWriter { + pluginCaps, controllerCaps = provisionWithSingleNodeMultiWriterCapabilities() + } else { + pluginCaps, controllerCaps = provisionCapabilities() + } + csiProvisioner := NewCSIProvisioner(clientSet, 5*time.Second, "test-provisioner", "test", 5, csiConn.conn, nil, provisionDriverName, pluginCaps, controllerCaps, supportsMigrationFromInTreePluginName, false, true, csitrans.New(), scInformer.Lister(), csiNodeInformer.Lister(), nodeInformer.Lister(), nil, nil, tc.withExtraMetadata, defaultfsType, nodeDeployment)