diff --git a/staging/src/k8s.io/csi-translation-lib/plugins/vsphere_volume.go b/staging/src/k8s.io/csi-translation-lib/plugins/vsphere_volume.go index 83058b374a6..13c37486c94 100644 --- a/staging/src/k8s.io/csi-translation-lib/plugins/vsphere_volume.go +++ b/staging/src/k8s.io/csi-translation-lib/plugins/vsphere_volume.go @@ -32,6 +32,12 @@ const ( // VSphereInTreePluginName is the name of the in-tree plugin for vSphere Volume VSphereInTreePluginName = "kubernetes.io/vsphere-volume" + // vSphereCSITopologyZoneKey is the zonal topology key for vSphere CSI Driver + vSphereCSITopologyZoneKey = "topology.csi.vmware.com/zone" + + // vSphereCSITopologyRegionKey is the region topology key for vSphere CSI Driver + vSphereCSITopologyRegionKey = "topology.csi.vmware.com/region" + // paramStoragePolicyName used to supply SPBM Policy name for Volume provisioning paramStoragePolicyName = "storagepolicyname" @@ -104,7 +110,14 @@ func (t *vSphereCSITranslator) TranslateInTreeStorageClassToCSI(sc *storage.Stor // When this is true, Driver returns initialvolumefilepath in the VolumeContext, which is // used in TranslateCSIPVToInTree params[paramcsiMigration] = "true" - // Note: sc.AllowedTopologies for Topology based volume provisioning will be supplied as it is. + // translate AllowedTopologies to vSphere CSI Driver topology + if len(sc.AllowedTopologies) > 0 { + newTopologies, err := translateAllowedTopologies(sc.AllowedTopologies, vSphereCSITopologyZoneKey) + if err != nil { + return nil, fmt.Errorf("failed translating allowed topologies: %v", err) + } + sc.AllowedTopologies = newTopologies + } sc.Parameters = params return sc, nil } @@ -154,6 +167,10 @@ func (t *vSphereCSITranslator) TranslateInTreePVToCSI(pv *v1.PersistentVolume) ( if pv.Spec.VsphereVolume.StoragePolicyName != "" { csiSource.VolumeAttributes[paramStoragePolicyName] = pv.Spec.VsphereVolume.StoragePolicyName } + // translate in-tree topology to CSI topology for migration + if err := translateTopologyFromInTreevSphereToCSI(pv, vSphereCSITopologyZoneKey, vSphereCSITopologyRegionKey); err != nil { + return nil, fmt.Errorf("failed to translate topology: %v", err) + } pv.Spec.VsphereVolume = nil pv.Spec.CSI = csiSource return pv, nil @@ -173,6 +190,10 @@ func (t *vSphereCSITranslator) TranslateCSIPVToInTree(pv *v1.PersistentVolume) ( if ok { vsphereVirtualDiskVolumeSource.VolumePath = volumeFilePath } + // translate CSI topology to In-tree topology for rollback compatibility. + if err := translateTopologyFromCSIToInTreevSphere(pv, vSphereCSITopologyZoneKey, vSphereCSITopologyRegionKey); err != nil { + return nil, fmt.Errorf("failed to translate topology. PV:%+v. Error:%v", *pv, err) + } pv.Spec.CSI = nil pv.Spec.VsphereVolume = vsphereVirtualDiskVolumeSource return pv, nil @@ -206,3 +227,76 @@ func (t *vSphereCSITranslator) GetCSIPluginName() string { func (t *vSphereCSITranslator) RepairVolumeHandle(volumeHandle, nodeID string) (string, error) { return volumeHandle, nil } + +// translateTopologyFromInTreevSphereToCSI converts existing zone labels or in-tree vsphere topology to +// vSphere CSI topology. +func translateTopologyFromInTreevSphereToCSI(pv *v1.PersistentVolume, csiTopologyKeyZone string, csiTopologyKeyRegion string) error { + zoneLabel, regionLabel := getTopologyLabel(pv) + + // If Zone kubernetes topology exist, replace it to use csiTopologyKeyZone + zones := getTopologyValues(pv, zoneLabel) + if len(zones) > 0 { + replaceTopology(pv, zoneLabel, csiTopologyKeyZone) + } else { + // if nothing is in the NodeAffinity, try to fetch the topology from PV labels + if label, ok := pv.Labels[zoneLabel]; ok { + if len(label) > 0 { + addTopology(pv, csiTopologyKeyZone, []string{label}) + } + } + } + + // If region kubernetes topology exist, replace it to use csiTopologyKeyRegion + regions := getTopologyValues(pv, regionLabel) + if len(regions) > 0 { + replaceTopology(pv, regionLabel, csiTopologyKeyRegion) + } else { + // if nothing is in the NodeAffinity, try to fetch the topology from PV labels + if label, ok := pv.Labels[regionLabel]; ok { + if len(label) > 0 { + addTopology(pv, csiTopologyKeyRegion, []string{label}) + } + } + } + return nil +} + +// translateTopologyFromCSIToInTreevSphere converts CSI zone/region affinity rules to in-tree vSphere zone/region labels +func translateTopologyFromCSIToInTreevSphere(pv *v1.PersistentVolume, + csiTopologyKeyZone string, csiTopologyKeyRegion string) error { + zoneLabel, regionLabel := getTopologyLabel(pv) + + // Replace all CSI topology to Kubernetes Zone label + err := replaceTopology(pv, csiTopologyKeyZone, zoneLabel) + if err != nil { + return fmt.Errorf("failed to replace CSI topology to Kubernetes topology, error: %v", err) + } + + // Replace all CSI topology to Kubernetes Region label + err = replaceTopology(pv, csiTopologyKeyRegion, regionLabel) + if err != nil { + return fmt.Errorf("failed to replace CSI topology to Kubernetes topology, error: %v", err) + } + + zoneVals := getTopologyValues(pv, zoneLabel) + if len(zoneVals) > 0 { + if pv.Labels == nil { + pv.Labels = make(map[string]string) + } + _, zoneOK := pv.Labels[zoneLabel] + if !zoneOK { + pv.Labels[zoneLabel] = zoneVals[0] + } + } + regionVals := getTopologyValues(pv, regionLabel) + if len(regionVals) > 0 { + if pv.Labels == nil { + pv.Labels = make(map[string]string) + } + _, regionOK := pv.Labels[regionLabel] + if !regionOK { + pv.Labels[regionLabel] = regionVals[0] + } + } + return nil +} diff --git a/staging/src/k8s.io/csi-translation-lib/plugins/vsphere_volume_test.go b/staging/src/k8s.io/csi-translation-lib/plugins/vsphere_volume_test.go index 78c877b761c..2979d4641c5 100644 --- a/staging/src/k8s.io/csi-translation-lib/plugins/vsphere_volume_test.go +++ b/staging/src/k8s.io/csi-translation-lib/plugins/vsphere_volume_test.go @@ -33,19 +33,17 @@ func TestTranslatevSphereInTreeStorageClassToCSI(t *testing.T) { Key: v1.LabelTopologyZone, Values: []string{"zone-a"}, }, - { - Key: v1.LabelTopologyRegion, - Values: []string{"region-a"}, - }, }} topologySelectorTermWithBetaLabels := v1.TopologySelectorTerm{[]v1.TopologySelectorLabelRequirement{ { Key: v1.LabelFailureDomainBetaZone, Values: []string{"zone-a"}, }, + }} + expectedTopologySelectorTerm := v1.TopologySelectorTerm{[]v1.TopologySelectorLabelRequirement{ { - Key: v1.LabelFailureDomainBetaRegion, - Values: []string{"region-a"}, + Key: vSphereCSITopologyZoneKey, + Values: []string{"zone-a"}, }, }} cases := []struct { @@ -88,27 +86,27 @@ func TestTranslatevSphereInTreeStorageClassToCSI(t *testing.T) { { name: "translate with no parameter and allowedTopology", sc: NewStorageClass(map[string]string{}, []v1.TopologySelectorTerm{topologySelectorTerm}), - expSc: NewStorageClass(map[string]string{paramcsiMigration: "true"}, []v1.TopologySelectorTerm{topologySelectorTerm}), + expSc: NewStorageClass(map[string]string{paramcsiMigration: "true"}, []v1.TopologySelectorTerm{expectedTopologySelectorTerm}), }, { name: "translate with storagepolicyname and allowedTopology", sc: NewStorageClass(map[string]string{"storagepolicyname": "test-policy-name"}, []v1.TopologySelectorTerm{topologySelectorTerm}), - expSc: NewStorageClass(map[string]string{"storagepolicyname": "test-policy-name", paramcsiMigration: "true"}, []v1.TopologySelectorTerm{topologySelectorTerm}), + expSc: NewStorageClass(map[string]string{"storagepolicyname": "test-policy-name", paramcsiMigration: "true"}, []v1.TopologySelectorTerm{expectedTopologySelectorTerm}), }, { name: "translate with storagepolicyname and allowedTopology beta labels", sc: NewStorageClass(map[string]string{"storagepolicyname": "test-policy-name"}, []v1.TopologySelectorTerm{topologySelectorTermWithBetaLabels}), - expSc: NewStorageClass(map[string]string{"storagepolicyname": "test-policy-name", paramcsiMigration: "true"}, []v1.TopologySelectorTerm{topologySelectorTermWithBetaLabels}), + expSc: NewStorageClass(map[string]string{"storagepolicyname": "test-policy-name", paramcsiMigration: "true"}, []v1.TopologySelectorTerm{expectedTopologySelectorTerm}), }, { name: "translate with raw vSAN policy parameters, datastore and diskformat", sc: NewStorageClass(map[string]string{"hostfailurestotolerate": "2", "datastore": "vsanDatastore", "diskformat": "thin"}, []v1.TopologySelectorTerm{topologySelectorTerm}), - expSc: NewStorageClass(map[string]string{"hostfailurestotolerate-migrationparam": "2", "datastore-migrationparam": "vsanDatastore", "diskformat-migrationparam": "thin", paramcsiMigration: "true"}, []v1.TopologySelectorTerm{topologySelectorTerm}), + expSc: NewStorageClass(map[string]string{"hostfailurestotolerate-migrationparam": "2", "datastore-migrationparam": "vsanDatastore", "diskformat-migrationparam": "thin", paramcsiMigration: "true"}, []v1.TopologySelectorTerm{expectedTopologySelectorTerm}), }, { name: "translate with all parameters", sc: NewStorageClass(map[string]string{"storagepolicyname": "test-policy-name", "datastore": "test-datastore-name", "fstype": "ext4", "diskformat": "thin", "hostfailurestotolerate": "1", "forceprovisioning": "yes", "cachereservation": "25", "diskstripes": "4", "objectspacereservation": "10", "iopslimit": "32"}, []v1.TopologySelectorTerm{topologySelectorTerm}), - expSc: NewStorageClass(map[string]string{"storagepolicyname": "test-policy-name", "datastore-migrationparam": "test-datastore-name", "csi.storage.k8s.io/fstype": "ext4", "diskformat-migrationparam": "thin", "hostfailurestotolerate-migrationparam": "1", "forceprovisioning-migrationparam": "yes", "cachereservation-migrationparam": "25", "diskstripes-migrationparam": "4", "objectspacereservation-migrationparam": "10", "iopslimit-migrationparam": "32", paramcsiMigration: "true"}, []v1.TopologySelectorTerm{topologySelectorTerm}), + expSc: NewStorageClass(map[string]string{"storagepolicyname": "test-policy-name", "datastore-migrationparam": "test-datastore-name", "csi.storage.k8s.io/fstype": "ext4", "diskformat-migrationparam": "thin", "hostfailurestotolerate-migrationparam": "1", "forceprovisioning-migrationparam": "yes", "cachereservation-migrationparam": "25", "diskstripes-migrationparam": "4", "objectspacereservation-migrationparam": "10", "iopslimit-migrationparam": "32", paramcsiMigration: "true"}, []v1.TopologySelectorTerm{expectedTopologySelectorTerm}), }, } for _, tc := range cases { @@ -194,6 +192,84 @@ func TestTranslateVSphereCSIPVToInTree(t *testing.T) { }, expErr: false, }, + { + name: "translate valid vSphere CSI PV with topology Node Affinity rules to vSphere CSI PV with topology labels", + csiPV: &v1.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pvc-d8b4475f-2c47-486e-9b57-43ae006f9b59", + }, + Spec: v1.PersistentVolumeSpec{ + PersistentVolumeSource: v1.PersistentVolumeSource{ + CSI: &v1.CSIPersistentVolumeSource{ + Driver: VSphereDriverName, + VolumeHandle: "e4073a6d-642e-4dff-8f4a-b4e3a47c4bbd", + FSType: "ext4", + VolumeAttributes: map[string]string{ + paramStoragePolicyName: "vSAN Default Storage Policy", + AttributeInitialVolumeFilepath: "[vsanDatastore] 6785a85e-268e-6352-a2e8-02008b7afadd/kubernetes-dynamic-pvc-68734c9f-a679-42e6-a694-39632c51e31f.vmdk", + }, + }, + }, + AccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce}, + NodeAffinity: &v1.VolumeNodeAffinity{ + Required: &v1.NodeSelector{ + NodeSelectorTerms: []v1.NodeSelectorTerm{ + { + MatchExpressions: []v1.NodeSelectorRequirement{ + { + Key: "topology.csi.vmware.com/zone", + Operator: v1.NodeSelectorOpIn, + Values: []string{"z1"}, + }, + { + Key: "topology.csi.vmware.com/region", + Operator: v1.NodeSelectorOpIn, + Values: []string{"r1"}, + }, + }, + }, + }, + }, + }, + }, + }, + intreePV: &v1.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pvc-d8b4475f-2c47-486e-9b57-43ae006f9b59", + Labels: map[string]string{"topology.kubernetes.io/zone": "z1", "topology.kubernetes.io/region": "r1"}, + }, + Spec: v1.PersistentVolumeSpec{ + PersistentVolumeSource: v1.PersistentVolumeSource{ + VsphereVolume: &v1.VsphereVirtualDiskVolumeSource{ + VolumePath: "[vsanDatastore] 6785a85e-268e-6352-a2e8-02008b7afadd/kubernetes-dynamic-pvc-68734c9f-a679-42e6-a694-39632c51e31f.vmdk", + FSType: "ext4", + }, + }, + AccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce}, + NodeAffinity: &v1.VolumeNodeAffinity{ + Required: &v1.NodeSelector{ + NodeSelectorTerms: []v1.NodeSelectorTerm{ + { + MatchExpressions: []v1.NodeSelectorRequirement{ + { + Key: "topology.kubernetes.io/zone", + Operator: v1.NodeSelectorOpIn, + Values: []string{"z1"}, + }, + { + Key: "topology.kubernetes.io/region", + Operator: v1.NodeSelectorOpIn, + Values: []string{"r1"}, + }, + }, + }, + }, + }, + }, + }, + }, + expErr: false, + }, } for _, tc := range cases { @@ -264,6 +340,124 @@ func TestTranslateVSphereInTreePVToCSI(t *testing.T) { }, expErr: false, }, + { + name: "translate valid vSphere in-tree PV with beta topology labels to vSphere CSI PV", + intreePV: &v1.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pvc-d8b4475f-2c47-486e-9b57-43ae006f9b59", + Labels: map[string]string{"failure-domain.beta.kubernetes.io/zone": "z1", "failure-domain.beta.kubernetes.io/region": "r1"}, + }, + Spec: v1.PersistentVolumeSpec{ + PersistentVolumeSource: v1.PersistentVolumeSource{ + VsphereVolume: &v1.VsphereVirtualDiskVolumeSource{ + VolumePath: "[vsanDatastore] 6785a85e-268e-6352-a2e8-02008b7afadd/kubernetes-dynamic-pvc-68734c9f-a679-42e6-a694-39632c51e31f.vmdk", + FSType: "ext4", + StoragePolicyName: "vSAN Default Storage Policy", + }, + }, + AccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce}, + }, + }, + csiPV: &v1.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pvc-d8b4475f-2c47-486e-9b57-43ae006f9b59", + Labels: map[string]string{"failure-domain.beta.kubernetes.io/zone": "z1", "failure-domain.beta.kubernetes.io/region": "r1"}, + }, + Spec: v1.PersistentVolumeSpec{ + PersistentVolumeSource: v1.PersistentVolumeSource{ + CSI: &v1.CSIPersistentVolumeSource{ + Driver: VSphereDriverName, + VolumeHandle: "[vsanDatastore] 6785a85e-268e-6352-a2e8-02008b7afadd/kubernetes-dynamic-pvc-68734c9f-a679-42e6-a694-39632c51e31f.vmdk", + FSType: "ext4", + VolumeAttributes: map[string]string{ + paramStoragePolicyName: "vSAN Default Storage Policy", + }, + }, + }, + AccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce}, + NodeAffinity: &v1.VolumeNodeAffinity{ + Required: &v1.NodeSelector{ + NodeSelectorTerms: []v1.NodeSelectorTerm{ + { + MatchExpressions: []v1.NodeSelectorRequirement{ + { + Key: "topology.csi.vmware.com/zone", + Operator: v1.NodeSelectorOpIn, + Values: []string{"z1"}, + }, + { + Key: "topology.csi.vmware.com/region", + Operator: v1.NodeSelectorOpIn, + Values: []string{"r1"}, + }, + }, + }, + }, + }, + }, + }, + }, + expErr: false, + }, + { + name: "translate valid vSphere in-tree PV with GA topology labels to vSphere CSI PV", + intreePV: &v1.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pvc-d8b4475f-2c47-486e-9b57-43ae006f9b59", + Labels: map[string]string{"topology.kubernetes.io/zone": "z1", "topology.kubernetes.io/region": "r1"}, + }, + Spec: v1.PersistentVolumeSpec{ + PersistentVolumeSource: v1.PersistentVolumeSource{ + VsphereVolume: &v1.VsphereVirtualDiskVolumeSource{ + VolumePath: "[vsanDatastore] 6785a85e-268e-6352-a2e8-02008b7afadd/kubernetes-dynamic-pvc-68734c9f-a679-42e6-a694-39632c51e31f.vmdk", + FSType: "ext4", + StoragePolicyName: "vSAN Default Storage Policy", + }, + }, + AccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce}, + }, + }, + csiPV: &v1.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pvc-d8b4475f-2c47-486e-9b57-43ae006f9b59", + Labels: map[string]string{"topology.kubernetes.io/zone": "z1", "topology.kubernetes.io/region": "r1"}, + }, + Spec: v1.PersistentVolumeSpec{ + PersistentVolumeSource: v1.PersistentVolumeSource{ + CSI: &v1.CSIPersistentVolumeSource{ + Driver: VSphereDriverName, + VolumeHandle: "[vsanDatastore] 6785a85e-268e-6352-a2e8-02008b7afadd/kubernetes-dynamic-pvc-68734c9f-a679-42e6-a694-39632c51e31f.vmdk", + FSType: "ext4", + VolumeAttributes: map[string]string{ + paramStoragePolicyName: "vSAN Default Storage Policy", + }, + }, + }, + AccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce}, + NodeAffinity: &v1.VolumeNodeAffinity{ + Required: &v1.NodeSelector{ + NodeSelectorTerms: []v1.NodeSelectorTerm{ + { + MatchExpressions: []v1.NodeSelectorRequirement{ + { + Key: "topology.csi.vmware.com/zone", + Operator: v1.NodeSelectorOpIn, + Values: []string{"z1"}, + }, + { + Key: "topology.csi.vmware.com/region", + Operator: v1.NodeSelectorOpIn, + Values: []string{"r1"}, + }, + }, + }, + }, + }, + }, + }, + }, + expErr: false, + }, } for _, tc := range cases {