From 3d2a49777d76ef548acbd5aa3bbfbd065ee2696d Mon Sep 17 00:00:00 2001 From: Chris Henzie Date: Tue, 1 Jun 2021 11:59:13 -0700 Subject: [PATCH] Map PV access modes to CSI access modes Additionally enforce ReadWriteOncePod can only be used by itself --- accessmodes/access_modes.go | 128 +++++++++++++++++++++++++++++ accessmodes/access_modes_test.go | 134 +++++++++++++++++++++++++++++++ 2 files changed, 262 insertions(+) create mode 100644 accessmodes/access_modes.go create mode 100644 accessmodes/access_modes_test.go diff --git a/accessmodes/access_modes.go b/accessmodes/access_modes.go new file mode 100644 index 00000000..beb91b2a --- /dev/null +++ b/accessmodes/access_modes.go @@ -0,0 +1,128 @@ +/* +Copyright 2021 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 accessmodes + +import ( + "fmt" + + "github.com/container-storage-interface/spec/lib/go/csi" + v1 "k8s.io/api/core/v1" +) + +// ToCSIAccessMode maps PersistentVolume access modes in Kubernetes to CSI +// access modes. Which mapping is used depends if the driver supports the +// SINGLE_NODE_MULTI_WRITER capability. +func ToCSIAccessMode(pvAccessModes []v1.PersistentVolumeAccessMode, supportsSingleNodeMultiWriter bool) (csi.VolumeCapability_AccessMode_Mode, error) { + if supportsSingleNodeMultiWriter { + return toSingleNodeMultiWriterCapableCSIAccessMode(pvAccessModes) + } + return toCSIAccessMode(pvAccessModes) +} + +// toCSIAccessMode maps PersistentVolume access modes in Kubernetes to CSI +// access modes. +// +// +------------------+-------------------------+----------------------------------------+ +// | K8s AccessMode | CSI AccessMode | Additional Details | +// +------------------+-------------------------+----------------------------------------+ +// | ReadWriteMany | MULTI_NODE_MULTI_WRITER | | +// | ReadOnlyMany | MULTI_NODE_READER_ONLY | Cannot be combined with ReadWriteOnce | +// | ReadWriteOnce | SINGLE_NODE_WRITER | Cannot be combined with ReadOnlyMany | +// | ReadWriteOncePod | SINGLE_NODE_WRITER | Cannot be combined with any AccessMode | +// +------------------+-------------------------+----------------------------------------+ +func toCSIAccessMode(pvAccessModes []v1.PersistentVolumeAccessMode) (csi.VolumeCapability_AccessMode_Mode, error) { + m := uniqueAccessModes(pvAccessModes) + + switch { + // This mapping exists to enable CSI drivers that lack the + // SINGLE_NODE_MULTI_WRITER capability to work with the + // ReadWriteOncePod access mode. + case m[v1.ReadWriteOncePod]: + if len(m) > 1 { + return csi.VolumeCapability_AccessMode_UNKNOWN, fmt.Errorf("Kubernetes does not support use of ReadWriteOncePod with other access modes on the same PersistentVolume") + } + return csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, nil + + case m[v1.ReadWriteMany]: + // ReadWriteMany takes precedence, regardless of what other + // modes are set. + return csi.VolumeCapability_AccessMode_MULTI_NODE_MULTI_WRITER, nil + + case m[v1.ReadOnlyMany] && m[v1.ReadWriteOnce]: + // This is not possible in the CSI spec. + return csi.VolumeCapability_AccessMode_UNKNOWN, fmt.Errorf("CSI does not support ReadOnlyMany and ReadWriteOnce on the same PersistentVolume") + + case m[v1.ReadOnlyMany]: + return csi.VolumeCapability_AccessMode_MULTI_NODE_READER_ONLY, nil + + case m[v1.ReadWriteOnce]: + return csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, nil + + default: + return csi.VolumeCapability_AccessMode_UNKNOWN, fmt.Errorf("unsupported AccessMode combination: %+v", pvAccessModes) + } +} + +// toSingleNodeMultiWriterCapableCSIAccessMode maps PersistentVolume access +// modes in Kubernetes to CSI access modes for drivers that support the +// SINGLE_NODE_MULTI_WRITER capability. +// +// +------------------+---------------------------+----------------------------------------+ +// | K8s AccessMode | CSI AccessMode | Additional Details | +// +------------------+---------------------------+----------------------------------------+ +// | ReadWriteMany | MULTI_NODE_MULTI_WRITER | | +// | ReadOnlyMany | MULTI_NODE_READER_ONLY | Cannot be combined with ReadWriteOnce | +// | ReadWriteOnce | SINGLE_NODE_MULTI_WRITER | Cannot be combined with ReadOnlyMany | +// | ReadWriteOncePod | SINGLE_NODE_SINGLE_WRITER | Cannot be combined with any AccessMode | +// +------------------+---------------------------+----------------------------------------+ +func toSingleNodeMultiWriterCapableCSIAccessMode(pvAccessModes []v1.PersistentVolumeAccessMode) (csi.VolumeCapability_AccessMode_Mode, error) { + m := uniqueAccessModes(pvAccessModes) + + switch { + case m[v1.ReadWriteOncePod]: + if len(m) > 1 { + return csi.VolumeCapability_AccessMode_UNKNOWN, fmt.Errorf("Kubernetes does not support use of ReadWriteOncePod with other access modes on the same PersistentVolume") + } + return csi.VolumeCapability_AccessMode_SINGLE_NODE_SINGLE_WRITER, nil + + case m[v1.ReadWriteMany]: + // ReadWriteMany trumps everything, regardless of what other + // modes are set. + return csi.VolumeCapability_AccessMode_MULTI_NODE_MULTI_WRITER, nil + + case m[v1.ReadOnlyMany] && m[v1.ReadWriteOnce]: + // This is not possible in the CSI spec. + return csi.VolumeCapability_AccessMode_UNKNOWN, fmt.Errorf("CSI does not support ReadOnlyMany and ReadWriteOnce on the same PersistentVolume") + + case m[v1.ReadOnlyMany]: + return csi.VolumeCapability_AccessMode_MULTI_NODE_READER_ONLY, nil + + case m[v1.ReadWriteOnce]: + return csi.VolumeCapability_AccessMode_SINGLE_NODE_MULTI_WRITER, nil + + default: + return csi.VolumeCapability_AccessMode_UNKNOWN, fmt.Errorf("unsupported AccessMode combination: %+v", pvAccessModes) + } +} + +func uniqueAccessModes(pvAccessModes []v1.PersistentVolumeAccessMode) map[v1.PersistentVolumeAccessMode]bool { + m := map[v1.PersistentVolumeAccessMode]bool{} + for _, mode := range pvAccessModes { + m[mode] = true + } + return m +} diff --git a/accessmodes/access_modes_test.go b/accessmodes/access_modes_test.go new file mode 100644 index 00000000..d686a981 --- /dev/null +++ b/accessmodes/access_modes_test.go @@ -0,0 +1,134 @@ +/* +Copyright 2021 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 accessmodes + +import ( + "testing" + + "github.com/container-storage-interface/spec/lib/go/csi" + v1 "k8s.io/api/core/v1" +) + +func TestToCSIAccessMode(t *testing.T) { + tests := []struct { + name string + pvAccessModes []v1.PersistentVolumeAccessMode + expectedCSIAccessMode csi.VolumeCapability_AccessMode_Mode + expectError bool + supportsSingleNodeMultiWriter bool + }{ + { + name: "RWX", + pvAccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteMany}, + expectedCSIAccessMode: csi.VolumeCapability_AccessMode_MULTI_NODE_MULTI_WRITER, + }, + { + name: "ROX + RWO", + pvAccessModes: []v1.PersistentVolumeAccessMode{v1.ReadOnlyMany, v1.ReadWriteOnce}, + expectedCSIAccessMode: csi.VolumeCapability_AccessMode_UNKNOWN, + expectError: true, + }, + { + name: "ROX + RWOP", + pvAccessModes: []v1.PersistentVolumeAccessMode{v1.ReadOnlyMany, v1.ReadWriteOncePod}, + expectedCSIAccessMode: csi.VolumeCapability_AccessMode_UNKNOWN, + expectError: true, + }, + { + name: "ROX", + pvAccessModes: []v1.PersistentVolumeAccessMode{v1.ReadOnlyMany}, + expectedCSIAccessMode: csi.VolumeCapability_AccessMode_MULTI_NODE_READER_ONLY, + }, + { + name: "RWO", + pvAccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce}, + expectedCSIAccessMode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, + }, + { + name: "RWOP", + pvAccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteOncePod}, + expectedCSIAccessMode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER, + }, + { + name: "empty", + pvAccessModes: []v1.PersistentVolumeAccessMode{}, + expectedCSIAccessMode: csi.VolumeCapability_AccessMode_UNKNOWN, + expectError: true, + }, + { + name: "RWX with SINGLE_NODE_MULTI_WRITER capable driver", + pvAccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteMany}, + expectedCSIAccessMode: csi.VolumeCapability_AccessMode_MULTI_NODE_MULTI_WRITER, + supportsSingleNodeMultiWriter: true, + }, + { + name: "ROX + RWO with SINGLE_NODE_MULTI_WRITER capable driver", + pvAccessModes: []v1.PersistentVolumeAccessMode{v1.ReadOnlyMany, v1.ReadWriteOnce}, + expectedCSIAccessMode: csi.VolumeCapability_AccessMode_UNKNOWN, + expectError: true, + supportsSingleNodeMultiWriter: true, + }, + { + name: "ROX + RWOP with SINGLE_NODE_MULTI_WRITER capable driver", + pvAccessModes: []v1.PersistentVolumeAccessMode{v1.ReadOnlyMany, v1.ReadWriteOncePod}, + expectedCSIAccessMode: csi.VolumeCapability_AccessMode_UNKNOWN, + expectError: true, + supportsSingleNodeMultiWriter: true, + }, + { + name: "ROX with SINGLE_NODE_MULTI_WRITER capable driver", + pvAccessModes: []v1.PersistentVolumeAccessMode{v1.ReadOnlyMany}, + expectedCSIAccessMode: csi.VolumeCapability_AccessMode_MULTI_NODE_READER_ONLY, + supportsSingleNodeMultiWriter: true, + }, + { + name: "RWO with SINGLE_NODE_MULTI_WRITER capable driver", + pvAccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce}, + expectedCSIAccessMode: csi.VolumeCapability_AccessMode_SINGLE_NODE_MULTI_WRITER, + supportsSingleNodeMultiWriter: true, + }, + { + name: "RWOP with SINGLE_NODE_MULTI_WRITER capable driver", + pvAccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteOncePod}, + expectedCSIAccessMode: csi.VolumeCapability_AccessMode_SINGLE_NODE_SINGLE_WRITER, + supportsSingleNodeMultiWriter: true, + }, + { + name: "empty with SINGLE_NODE_MULTI_WRITER capable driver", + pvAccessModes: []v1.PersistentVolumeAccessMode{}, + expectedCSIAccessMode: csi.VolumeCapability_AccessMode_UNKNOWN, + expectError: true, + supportsSingleNodeMultiWriter: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + csiAccessMode, err := ToCSIAccessMode(test.pvAccessModes, test.supportsSingleNodeMultiWriter) + + if err == nil && test.expectError { + t.Errorf("test %s: expected error, got none", test.name) + } + if err != nil && !test.expectError { + t.Errorf("test %s: got error: %s", test.name, err) + } + if !test.expectError && csiAccessMode != test.expectedCSIAccessMode { + t.Errorf("test %s: unexpected access mode: %+v", test.name, csiAccessMode) + } + }) + } +}