k3s/vendor/k8s.io/csi-translation-lib/plugins/aws_ebs.go

303 lines
11 KiB
Go
Raw Normal View History

2019-04-07 17:07:55 +00:00
/*
Copyright 2019 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 plugins
import (
"fmt"
"net/url"
"regexp"
"strconv"
"strings"
2019-12-12 01:27:03 +00:00
v1 "k8s.io/api/core/v1"
2019-08-30 18:33:25 +00:00
storage "k8s.io/api/storage/v1"
2019-09-27 21:51:53 +00:00
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets"
2019-04-07 17:07:55 +00:00
)
const (
// AWSEBSDriverName is the name of the CSI driver for EBS
AWSEBSDriverName = "ebs.csi.aws.com"
// AWSEBSInTreePluginName is the name of the intree plugin for EBS
AWSEBSInTreePluginName = "kubernetes.io/aws-ebs"
2019-12-12 01:27:03 +00:00
// AWSEBSTopologyKey is the zonal topology key for AWS EBS CSI driver
AWSEBSTopologyKey = "topology." + AWSEBSDriverName + "/zone"
// iopsPerGBKey is StorageClass parameter name that specifies IOPS
// Per GB.
iopsPerGBKey = "iopspergb"
// allowIncreaseIOPSKey is parameter name that allows the CSI driver
// to increase IOPS to the minimum value supported by AWS when IOPS
// Per GB is too low for a given volume size. This preserves current
// in-tree volume plugin behavior.
allowIncreaseIOPSKey = "allowautoiopspergbincrease"
2019-04-07 17:07:55 +00:00
)
var _ InTreePlugin = &awsElasticBlockStoreCSITranslator{}
// awsElasticBlockStoreTranslator handles translation of PV spec from In-tree EBS to CSI EBS and vice versa
type awsElasticBlockStoreCSITranslator struct{}
// NewAWSElasticBlockStoreCSITranslator returns a new instance of awsElasticBlockStoreTranslator
func NewAWSElasticBlockStoreCSITranslator() InTreePlugin {
return &awsElasticBlockStoreCSITranslator{}
}
2019-12-12 01:27:03 +00:00
// TranslateInTreeStorageClassToCSI translates InTree EBS storage class parameters to CSI storage class
2019-08-30 18:33:25 +00:00
func (t *awsElasticBlockStoreCSITranslator) TranslateInTreeStorageClassToCSI(sc *storage.StorageClass) (*storage.StorageClass, error) {
2019-12-12 01:27:03 +00:00
var (
generatedTopologies []v1.TopologySelectorTerm
params = map[string]string{}
)
for k, v := range sc.Parameters {
switch strings.ToLower(k) {
case fsTypeKey:
params[csiFsTypeKey] = v
case zoneKey:
generatedTopologies = generateToplogySelectors(AWSEBSTopologyKey, []string{v})
case zonesKey:
generatedTopologies = generateToplogySelectors(AWSEBSTopologyKey, strings.Split(v, ","))
case iopsPerGBKey:
// Keep iopsPerGBKey
params[k] = v
// Preserve current in-tree volume plugin behavior and allow the CSI
// driver to bump volume IOPS when volume size * iopsPerGB is too low.
params[allowIncreaseIOPSKey] = "true"
2019-12-12 01:27:03 +00:00
default:
params[k] = v
}
}
if len(generatedTopologies) > 0 && len(sc.AllowedTopologies) > 0 {
return nil, fmt.Errorf("cannot simultaneously set allowed topologies and zone/zones parameters")
} else if len(generatedTopologies) > 0 {
sc.AllowedTopologies = generatedTopologies
} else if len(sc.AllowedTopologies) > 0 {
newTopologies, err := translateAllowedTopologies(sc.AllowedTopologies, AWSEBSTopologyKey)
if err != nil {
return nil, fmt.Errorf("failed translating allowed topologies: %v", err)
}
sc.AllowedTopologies = newTopologies
}
sc.Parameters = params
2019-08-30 18:33:25 +00:00
return sc, nil
}
// TranslateInTreeInlineVolumeToCSI takes a Volume with AWSElasticBlockStore set from in-tree
// and converts the AWSElasticBlockStore source to a CSIPersistentVolumeSource
2021-05-14 17:12:55 +00:00
func (t *awsElasticBlockStoreCSITranslator) TranslateInTreeInlineVolumeToCSI(volume *v1.Volume, podNamespace string) (*v1.PersistentVolume, error) {
2019-08-30 18:33:25 +00:00
if volume == nil || volume.AWSElasticBlockStore == nil {
return nil, fmt.Errorf("volume is nil or AWS EBS not defined on volume")
}
ebsSource := volume.AWSElasticBlockStore
2019-12-12 01:27:03 +00:00
volumeHandle, err := KubernetesVolumeIDToEBSVolumeID(ebsSource.VolumeID)
if err != nil {
return nil, fmt.Errorf("failed to translate Kubernetes ID to EBS Volume ID %v", err)
}
2019-08-30 18:33:25 +00:00
pv := &v1.PersistentVolume{
2019-09-27 21:51:53 +00:00
ObjectMeta: metav1.ObjectMeta{
2019-12-12 01:27:03 +00:00
// Must be unique per disk as it is used as the unique part of the
// staging path
Name: fmt.Sprintf("%s-%s", AWSEBSDriverName, volumeHandle),
2019-09-27 21:51:53 +00:00
},
2019-08-30 18:33:25 +00:00
Spec: v1.PersistentVolumeSpec{
PersistentVolumeSource: v1.PersistentVolumeSource{
CSI: &v1.CSIPersistentVolumeSource{
Driver: AWSEBSDriverName,
2019-12-12 01:27:03 +00:00
VolumeHandle: volumeHandle,
2019-08-30 18:33:25 +00:00
ReadOnly: ebsSource.ReadOnly,
FSType: ebsSource.FSType,
VolumeAttributes: map[string]string{
"partition": strconv.FormatInt(int64(ebsSource.Partition), 10),
},
},
},
AccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce},
},
}
return pv, nil
2019-04-07 17:07:55 +00:00
}
// TranslateInTreePVToCSI takes a PV with AWSElasticBlockStore set from in-tree
// and converts the AWSElasticBlockStore source to a CSIPersistentVolumeSource
func (t *awsElasticBlockStoreCSITranslator) TranslateInTreePVToCSI(pv *v1.PersistentVolume) (*v1.PersistentVolume, error) {
if pv == nil || pv.Spec.AWSElasticBlockStore == nil {
return nil, fmt.Errorf("pv is nil or AWS EBS not defined on pv")
}
ebsSource := pv.Spec.AWSElasticBlockStore
volumeHandle, err := KubernetesVolumeIDToEBSVolumeID(ebsSource.VolumeID)
if err != nil {
return nil, fmt.Errorf("failed to translate Kubernetes ID to EBS Volume ID %v", err)
}
csiSource := &v1.CSIPersistentVolumeSource{
Driver: AWSEBSDriverName,
VolumeHandle: volumeHandle,
ReadOnly: ebsSource.ReadOnly,
FSType: ebsSource.FSType,
VolumeAttributes: map[string]string{
"partition": strconv.FormatInt(int64(ebsSource.Partition), 10),
},
}
if err := translateTopologyFromInTreeToCSI(pv, AWSEBSTopologyKey); err != nil {
2019-12-12 01:27:03 +00:00
return nil, fmt.Errorf("failed to translate topology: %v", err)
}
2019-04-07 17:07:55 +00:00
pv.Spec.AWSElasticBlockStore = nil
pv.Spec.CSI = csiSource
return pv, nil
}
// TranslateCSIPVToInTree takes a PV with CSIPersistentVolumeSource set and
// translates the EBS CSI source to a AWSElasticBlockStore source.
func (t *awsElasticBlockStoreCSITranslator) TranslateCSIPVToInTree(pv *v1.PersistentVolume) (*v1.PersistentVolume, error) {
if pv == nil || pv.Spec.CSI == nil {
return nil, fmt.Errorf("pv is nil or CSI source not defined on pv")
}
csiSource := pv.Spec.CSI
ebsSource := &v1.AWSElasticBlockStoreVolumeSource{
VolumeID: csiSource.VolumeHandle,
FSType: csiSource.FSType,
ReadOnly: csiSource.ReadOnly,
}
if partition, ok := csiSource.VolumeAttributes["partition"]; ok {
partValue, err := strconv.Atoi(partition)
if err != nil {
2019-09-27 21:51:53 +00:00
return nil, fmt.Errorf("failed to convert partition %v to integer: %v", partition, err)
2019-04-07 17:07:55 +00:00
}
ebsSource.Partition = int32(partValue)
}
// translate CSI topology to In-tree topology for rollback compatibility
if err := translateTopologyFromCSIToInTree(pv, AWSEBSTopologyKey, getAwsRegionFromZones); err != nil {
return nil, fmt.Errorf("failed to translate topology. PV:%+v. Error:%v", *pv, err)
}
2019-04-07 17:07:55 +00:00
pv.Spec.CSI = nil
pv.Spec.AWSElasticBlockStore = ebsSource
return pv, nil
}
2019-08-30 18:33:25 +00:00
// CanSupport tests whether the plugin supports a given persistent volume
2019-04-07 17:07:55 +00:00
// specification from the API. The spec pointer should be considered
// const.
func (t *awsElasticBlockStoreCSITranslator) CanSupport(pv *v1.PersistentVolume) bool {
return pv != nil && pv.Spec.AWSElasticBlockStore != nil
}
2019-08-30 18:33:25 +00:00
// CanSupportInline tests whether the plugin supports a given inline volume
// specification from the API. The spec pointer should be considered
// const.
func (t *awsElasticBlockStoreCSITranslator) CanSupportInline(volume *v1.Volume) bool {
return volume != nil && volume.AWSElasticBlockStore != nil
}
2019-04-07 17:07:55 +00:00
// GetInTreePluginName returns the name of the intree plugin driver
func (t *awsElasticBlockStoreCSITranslator) GetInTreePluginName() string {
return AWSEBSInTreePluginName
}
// GetCSIPluginName returns the name of the CSI plugin
func (t *awsElasticBlockStoreCSITranslator) GetCSIPluginName() string {
return AWSEBSDriverName
}
2019-12-12 01:27:03 +00:00
func (t *awsElasticBlockStoreCSITranslator) RepairVolumeHandle(volumeHandle, nodeID string) (string, error) {
return volumeHandle, nil
}
2019-04-07 17:07:55 +00:00
// awsVolumeRegMatch represents Regex Match for AWS volume.
var awsVolumeRegMatch = regexp.MustCompile("^vol-[^/]*$")
// KubernetesVolumeIDToEBSVolumeID translates Kubernetes volume ID to EBS volume ID
// KubernetesVolumeID forms:
2019-04-07 17:07:55 +00:00
// * aws://<zone>/<awsVolumeId>
// * aws:///<awsVolumeId>
// * <awsVolumeId>
// EBS Volume ID form:
// * vol-<alphanumberic>
// This translation shouldn't be needed and should be fixed in long run
// See https://github.com/kubernetes/kubernetes/issues/73730
func KubernetesVolumeIDToEBSVolumeID(kubernetesID string) (string, error) {
// name looks like aws://availability-zone/awsVolumeId
// The original idea of the URL-style name was to put the AZ into the
// host, so we could find the AZ immediately from the name without
// querying the API. But it turns out we don't actually need it for
// multi-AZ clusters, as we put the AZ into the labels on the PV instead.
// However, if in future we want to support multi-AZ cluster
// volume-awareness without using PersistentVolumes, we likely will
// want the AZ in the host.
if !strings.HasPrefix(kubernetesID, "aws://") {
// Assume a bare aws volume id (vol-1234...)
return kubernetesID, nil
}
url, err := url.Parse(kubernetesID)
if err != nil {
// TODO: Maybe we should pass a URL into the Volume functions
return "", fmt.Errorf("Invalid disk name (%s): %v", kubernetesID, err)
}
if url.Scheme != "aws" {
return "", fmt.Errorf("Invalid scheme for AWS volume (%s)", kubernetesID)
}
awsID := url.Path
awsID = strings.Trim(awsID, "/")
// We sanity check the resulting volume; the two known formats are
// vol-12345678 and vol-12345678abcdef01
if !awsVolumeRegMatch.MatchString(awsID) {
return "", fmt.Errorf("Invalid format for AWS volume (%s)", kubernetesID)
}
return awsID, nil
}
func getAwsRegionFromZones(zones []string) (string, error) {
regions := sets.String{}
if len(zones) < 1 {
return "", fmt.Errorf("no zones specified")
}
// AWS zones can be in four forms:
// us-west-2a, us-gov-east-1a, us-west-2-lax-1a (local zone) and us-east-1-wl1-bos-wlz-1 (wavelength).
for _, zone := range zones {
splitZone := strings.Split(zone, "-")
if (len(splitZone) == 3 || len(splitZone) == 4) && len(splitZone[len(splitZone)-1]) == 2 {
// this would break if we ever have a location with more than 9 regions, ie us-west-10.
splitZone[len(splitZone)-1] = splitZone[len(splitZone)-1][:1]
regions.Insert(strings.Join(splitZone, "-"))
} else if len(splitZone) == 5 || len(splitZone) == 7 {
// local zone or wavelength
regions.Insert(strings.Join(splitZone[:3], "-"))
} else {
return "", fmt.Errorf("Unexpected zone format: %v is not a valid AWS zone", zone)
}
}
if regions.Len() != 1 {
return "", fmt.Errorf("multiple or no regions gotten from zones, got: %v", regions)
}
return regions.UnsortedList()[0], nil
}