mirror of
				https://github.com/k3s-io/kubernetes.git
				synced 2025-10-31 13:50:01 +00:00 
			
		
		
		
	So it is consistent with other methods performing the same check (one for internal and external types)
		
			
				
	
	
		
			1269 lines
		
	
	
		
			44 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			1269 lines
		
	
	
		
			44 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| /*
 | |
| Copyright 2015 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 glusterfs
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"crypto/tls"
 | |
| 	"fmt"
 | |
| 	"math"
 | |
| 	"math/rand"
 | |
| 	"net"
 | |
| 	"net/http"
 | |
| 	"os"
 | |
| 	"path/filepath"
 | |
| 	"runtime"
 | |
| 	"strconv"
 | |
| 	dstrings "strings"
 | |
| 	"sync"
 | |
| 
 | |
| 	gcli "github.com/heketi/heketi/client/api/go-client"
 | |
| 	gapi "github.com/heketi/heketi/pkg/glusterfs/api"
 | |
| 	"k8s.io/klog/v2"
 | |
| 	"k8s.io/mount-utils"
 | |
| 	utilstrings "k8s.io/utils/strings"
 | |
| 
 | |
| 	v1 "k8s.io/api/core/v1"
 | |
| 	"k8s.io/apimachinery/pkg/api/errors"
 | |
| 	"k8s.io/apimachinery/pkg/api/resource"
 | |
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 | |
| 	"k8s.io/apimachinery/pkg/labels"
 | |
| 	"k8s.io/apimachinery/pkg/types"
 | |
| 	"k8s.io/apimachinery/pkg/util/sets"
 | |
| 	"k8s.io/apimachinery/pkg/util/uuid"
 | |
| 	clientset "k8s.io/client-go/kubernetes"
 | |
| 	volumehelpers "k8s.io/cloud-provider/volume/helpers"
 | |
| 	storagehelpers "k8s.io/component-helpers/storage/volume"
 | |
| 	proxyutil "k8s.io/kubernetes/pkg/proxy/util"
 | |
| 	"k8s.io/kubernetes/pkg/volume"
 | |
| 	volutil "k8s.io/kubernetes/pkg/volume/util"
 | |
| )
 | |
| 
 | |
| // ProbeVolumePlugins is the primary entrypoint for volume plugins.
 | |
| func ProbeVolumePlugins() []volume.VolumePlugin {
 | |
| 	return []volume.VolumePlugin{&glusterfsPlugin{host: nil, gidTable: make(map[string]*MinMaxAllocator)}}
 | |
| }
 | |
| 
 | |
| type glusterfsPlugin struct {
 | |
| 	host         volume.VolumeHost
 | |
| 	gidTable     map[string]*MinMaxAllocator
 | |
| 	gidTableLock sync.Mutex
 | |
| }
 | |
| 
 | |
| var _ volume.VolumePlugin = &glusterfsPlugin{}
 | |
| var _ volume.PersistentVolumePlugin = &glusterfsPlugin{}
 | |
| var _ volume.DeletableVolumePlugin = &glusterfsPlugin{}
 | |
| var _ volume.ProvisionableVolumePlugin = &glusterfsPlugin{}
 | |
| var _ volume.ExpandableVolumePlugin = &glusterfsPlugin{}
 | |
| var _ volume.Provisioner = &glusterfsVolumeProvisioner{}
 | |
| var _ volume.Deleter = &glusterfsVolumeDeleter{}
 | |
| 
 | |
| const (
 | |
| 	glusterfsPluginName            = "kubernetes.io/glusterfs"
 | |
| 	volPrefix                      = "vol_"
 | |
| 	dynamicEpSvcPrefix             = "glusterfs-dynamic"
 | |
| 	replicaCount                   = 3
 | |
| 	secretKeyName                  = "key" // key name used in secret
 | |
| 	gciLinuxGlusterMountBinaryPath = "/sbin/mount.glusterfs"
 | |
| 	defaultGidMin                  = 2000
 | |
| 	defaultGidMax                  = math.MaxInt32
 | |
| 
 | |
| 	// maxCustomEpNamePrefix is the maximum number of chars.
 | |
| 	// which can be used as ep/svc name prefix. This number is carved
 | |
| 	// out from below formula.
 | |
| 	// max length of name of an ep - length of pvc uuid
 | |
| 	// where max length of name of an ep is 63 and length of uuid is 37
 | |
| 	maxCustomEpNamePrefixLen = 26
 | |
| 
 | |
| 	// absoluteGidMin/Max are currently the same as the
 | |
| 	// default values, but they play a different role and
 | |
| 	// could take a different value. Only thing we need is:
 | |
| 	// absGidMin <= defGidMin <= defGidMax <= absGidMax
 | |
| 	absoluteGidMin = 2000
 | |
| 	absoluteGidMax = math.MaxInt32
 | |
| 	heketiAnn      = "heketi-dynamic-provisioner"
 | |
| 	glusterTypeAnn = "gluster.org/type"
 | |
| 	glusterDescAnn = "Gluster-Internal: Dynamically provisioned PV"
 | |
| 	heketiVolIDAnn = "gluster.kubernetes.io/heketi-volume-id"
 | |
| 
 | |
| 	// Error string returned by heketi
 | |
| 	errIDNotFound = "Id not found"
 | |
| )
 | |
| 
 | |
| func (plugin *glusterfsPlugin) Init(host volume.VolumeHost) error {
 | |
| 	plugin.host = host
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (plugin *glusterfsPlugin) GetPluginName() string {
 | |
| 	return glusterfsPluginName
 | |
| }
 | |
| 
 | |
| func (plugin *glusterfsPlugin) GetVolumeName(spec *volume.Spec) (string, error) {
 | |
| 	return "", fmt.Errorf("GetVolumeName() is unimplemented for GlusterFS")
 | |
| }
 | |
| 
 | |
| func (plugin *glusterfsPlugin) CanSupport(spec *volume.Spec) bool {
 | |
| 	return (spec.PersistentVolume != nil && spec.PersistentVolume.Spec.Glusterfs != nil) ||
 | |
| 		(spec.Volume != nil && spec.Volume.Glusterfs != nil)
 | |
| }
 | |
| 
 | |
| func (plugin *glusterfsPlugin) RequiresRemount(spec *volume.Spec) bool {
 | |
| 	return false
 | |
| }
 | |
| 
 | |
| func (plugin *glusterfsPlugin) SupportsMountOption() bool {
 | |
| 	return true
 | |
| }
 | |
| 
 | |
| func (plugin *glusterfsPlugin) SupportsBulkVolumeVerification() bool {
 | |
| 	return false
 | |
| }
 | |
| 
 | |
| func (plugin *glusterfsPlugin) RequiresFSResize() bool {
 | |
| 	return false
 | |
| }
 | |
| 
 | |
| func (plugin *glusterfsPlugin) GetAccessModes() []v1.PersistentVolumeAccessMode {
 | |
| 	return []v1.PersistentVolumeAccessMode{
 | |
| 		v1.ReadWriteOnce,
 | |
| 		v1.ReadOnlyMany,
 | |
| 		v1.ReadWriteMany,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (plugin *glusterfsPlugin) NewMounter(spec *volume.Spec, pod *v1.Pod, _ volume.VolumeOptions) (volume.Mounter, error) {
 | |
| 	epName, epNamespace, err := plugin.getEndpointNameAndNamespace(spec, pod.Namespace)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	kubeClient := plugin.host.GetKubeClient()
 | |
| 	if kubeClient == nil {
 | |
| 		return nil, fmt.Errorf("failed to get kube client to initialize mounter")
 | |
| 	}
 | |
| 	ep, err := kubeClient.CoreV1().Endpoints(epNamespace).Get(context.TODO(), epName, metav1.GetOptions{})
 | |
| 	if err != nil {
 | |
| 		klog.Errorf("failed to get endpoint %s: %v", epName, err)
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	klog.V(4).Infof("glusterfs pv endpoint %v", ep)
 | |
| 	return plugin.newMounterInternal(spec, ep, pod, plugin.host.GetMounter(plugin.GetPluginName()))
 | |
| }
 | |
| 
 | |
| func (plugin *glusterfsPlugin) getEndpointNameAndNamespace(spec *volume.Spec, defaultNamespace string) (string, string, error) {
 | |
| 	if spec.Volume != nil && spec.Volume.Glusterfs != nil {
 | |
| 		endpoints := spec.Volume.Glusterfs.EndpointsName
 | |
| 		if endpoints == "" {
 | |
| 			return "", "", fmt.Errorf("no glusterFS endpoint specified")
 | |
| 		}
 | |
| 		return endpoints, defaultNamespace, nil
 | |
| 	} else if spec.PersistentVolume != nil &&
 | |
| 		spec.PersistentVolume.Spec.Glusterfs != nil {
 | |
| 		endpoints := spec.PersistentVolume.Spec.Glusterfs.EndpointsName
 | |
| 		endpointsNs := defaultNamespace
 | |
| 		overriddenNs := spec.PersistentVolume.Spec.Glusterfs.EndpointsNamespace
 | |
| 		if overriddenNs != nil {
 | |
| 			if len(*overriddenNs) > 0 {
 | |
| 				endpointsNs = *overriddenNs
 | |
| 			} else {
 | |
| 				return "", "", fmt.Errorf("endpointnamespace field set, but no endpointnamespace specified")
 | |
| 			}
 | |
| 		}
 | |
| 		return endpoints, endpointsNs, nil
 | |
| 	}
 | |
| 	return "", "", fmt.Errorf("spec does not reference a GlusterFS volume type")
 | |
| 
 | |
| }
 | |
| func (plugin *glusterfsPlugin) newMounterInternal(spec *volume.Spec, ep *v1.Endpoints, pod *v1.Pod, mounter mount.Interface) (volume.Mounter, error) {
 | |
| 	volPath, readOnly, err := getVolumeInfo(spec)
 | |
| 	if err != nil {
 | |
| 		klog.Errorf("failed to get volumesource: %v", err)
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	return &glusterfsMounter{
 | |
| 		glusterfs: &glusterfs{
 | |
| 			volName:         spec.Name(),
 | |
| 			mounter:         mounter,
 | |
| 			pod:             pod,
 | |
| 			plugin:          plugin,
 | |
| 			MetricsProvider: volume.NewMetricsStatFS(plugin.host.GetPodVolumeDir(pod.UID, utilstrings.EscapeQualifiedName(glusterfsPluginName), spec.Name())),
 | |
| 		},
 | |
| 		hosts:        ep,
 | |
| 		path:         volPath,
 | |
| 		readOnly:     readOnly,
 | |
| 		mountOptions: volutil.MountOptionFromSpec(spec),
 | |
| 	}, nil
 | |
| }
 | |
| 
 | |
| func (plugin *glusterfsPlugin) NewUnmounter(volName string, podUID types.UID) (volume.Unmounter, error) {
 | |
| 	return plugin.newUnmounterInternal(volName, podUID, plugin.host.GetMounter(plugin.GetPluginName()))
 | |
| }
 | |
| 
 | |
| func (plugin *glusterfsPlugin) newUnmounterInternal(volName string, podUID types.UID, mounter mount.Interface) (volume.Unmounter, error) {
 | |
| 	return &glusterfsUnmounter{&glusterfs{
 | |
| 		volName:         volName,
 | |
| 		mounter:         mounter,
 | |
| 		pod:             &v1.Pod{ObjectMeta: metav1.ObjectMeta{UID: podUID}},
 | |
| 		plugin:          plugin,
 | |
| 		MetricsProvider: volume.NewMetricsStatFS(plugin.host.GetPodVolumeDir(podUID, utilstrings.EscapeQualifiedName(glusterfsPluginName), volName)),
 | |
| 	}}, nil
 | |
| }
 | |
| 
 | |
| func (plugin *glusterfsPlugin) ConstructVolumeSpec(volumeName, mountPath string) (*volume.Spec, error) {
 | |
| 
 | |
| 	// To reconstruct volume spec we need endpoint where fetching endpoint from mount
 | |
| 	// string looks to be impossible, so returning error.
 | |
| 	return nil, fmt.Errorf("impossible to reconstruct glusterfs volume spec from volume mountpath")
 | |
| }
 | |
| 
 | |
| // Glusterfs volumes represent a bare host file or directory mount of an Glusterfs export.
 | |
| type glusterfs struct {
 | |
| 	volName string
 | |
| 	pod     *v1.Pod
 | |
| 	mounter mount.Interface
 | |
| 	plugin  *glusterfsPlugin
 | |
| 	volume.MetricsProvider
 | |
| }
 | |
| 
 | |
| type glusterfsMounter struct {
 | |
| 	*glusterfs
 | |
| 	hosts        *v1.Endpoints
 | |
| 	path         string
 | |
| 	readOnly     bool
 | |
| 	mountOptions []string
 | |
| }
 | |
| 
 | |
| var _ volume.Mounter = &glusterfsMounter{}
 | |
| 
 | |
| func (b *glusterfsMounter) GetAttributes() volume.Attributes {
 | |
| 	return volume.Attributes{
 | |
| 		ReadOnly:        b.readOnly,
 | |
| 		Managed:         false,
 | |
| 		SupportsSELinux: false,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // Checks prior to mount operations to verify that the required components (binaries, etc.)
 | |
| // to mount the volume are available on the underlying node.
 | |
| // If not, it returns an error
 | |
| func (b *glusterfsMounter) CanMount() error {
 | |
| 	exe := b.plugin.host.GetExec(b.plugin.GetPluginName())
 | |
| 	switch runtime.GOOS {
 | |
| 	case "linux":
 | |
| 		if _, err := exe.Command("test", "-x", gciLinuxGlusterMountBinaryPath).CombinedOutput(); err != nil {
 | |
| 			return fmt.Errorf("required binary %s is missing", gciLinuxGlusterMountBinaryPath)
 | |
| 		}
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // SetUp attaches the disk and bind mounts to the volume path.
 | |
| func (b *glusterfsMounter) SetUp(mounterArgs volume.MounterArgs) error {
 | |
| 	return b.SetUpAt(b.GetPath(), mounterArgs)
 | |
| }
 | |
| 
 | |
| func (b *glusterfsMounter) SetUpAt(dir string, mounterArgs volume.MounterArgs) error {
 | |
| 	notMnt, err := b.mounter.IsLikelyNotMountPoint(dir)
 | |
| 	klog.V(4).Infof("mount setup: %s %v %v", dir, !notMnt, err)
 | |
| 	if err != nil && !os.IsNotExist(err) {
 | |
| 		return err
 | |
| 	}
 | |
| 	if !notMnt {
 | |
| 		return nil
 | |
| 	}
 | |
| 	if err := os.MkdirAll(dir, 0750); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	err = b.setUpAtInternal(dir)
 | |
| 	if err == nil {
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	// Cleanup upon failure.
 | |
| 	mount.CleanupMountPoint(dir, b.mounter, false)
 | |
| 	return err
 | |
| }
 | |
| 
 | |
| func (glusterfsVolume *glusterfs) GetPath() string {
 | |
| 	name := glusterfsPluginName
 | |
| 	return glusterfsVolume.plugin.host.GetPodVolumeDir(glusterfsVolume.pod.UID, utilstrings.EscapeQualifiedName(name), glusterfsVolume.volName)
 | |
| }
 | |
| 
 | |
| type glusterfsUnmounter struct {
 | |
| 	*glusterfs
 | |
| }
 | |
| 
 | |
| var _ volume.Unmounter = &glusterfsUnmounter{}
 | |
| 
 | |
| func (c *glusterfsUnmounter) TearDown() error {
 | |
| 	return c.TearDownAt(c.GetPath())
 | |
| }
 | |
| 
 | |
| func (c *glusterfsUnmounter) TearDownAt(dir string) error {
 | |
| 	return mount.CleanupMountPoint(dir, c.mounter, false)
 | |
| }
 | |
| 
 | |
| func (b *glusterfsMounter) setUpAtInternal(dir string) error {
 | |
| 	var errs error
 | |
| 	options := []string{}
 | |
| 	hasLogFile := false
 | |
| 	hasLogLevel := false
 | |
| 	log := ""
 | |
| 	if b.readOnly {
 | |
| 		options = append(options, "ro")
 | |
| 	}
 | |
| 
 | |
| 	// Check for log-file,log-level options existence in user supplied mount options, if provided, use those.
 | |
| 	for _, userOpt := range b.mountOptions {
 | |
| 		switch {
 | |
| 		case dstrings.HasPrefix(userOpt, "log-file"):
 | |
| 			klog.V(4).Infof("log-file mount option has provided")
 | |
| 			hasLogFile = true
 | |
| 
 | |
| 		case dstrings.HasPrefix(userOpt, "log-level"):
 | |
| 			klog.V(4).Infof("log-level mount option has provided")
 | |
| 			hasLogLevel = true
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// If logfile has not been provided, create driver specific log file.
 | |
| 	if !hasLogFile {
 | |
| 		p := filepath.Join(b.glusterfs.plugin.host.GetPluginDir(glusterfsPluginName), b.glusterfs.volName)
 | |
| 		if err := os.MkdirAll(p, 0750); err != nil {
 | |
| 			return fmt.Errorf("failed to create directory %v: %v", p, err)
 | |
| 		}
 | |
| 
 | |
| 		// adding log-level ERROR to remove noise
 | |
| 		// and more specific log path so each pod has
 | |
| 		// its own log based on PV + Pod
 | |
| 		log = filepath.Join(p, b.pod.Name+"-glusterfs.log")
 | |
| 
 | |
| 		// Use derived log file in gluster fuse mount
 | |
| 		options = append(options, "log-file="+log)
 | |
| 	}
 | |
| 	if !hasLogLevel {
 | |
| 		options = append(options, "log-level=ERROR")
 | |
| 	}
 | |
| 	var addrlist []string
 | |
| 	if b.hosts == nil {
 | |
| 		return fmt.Errorf("glusterfs endpoint is nil in mounter")
 | |
| 	}
 | |
| 	addr := sets.String{}
 | |
| 	if b.hosts.Subsets != nil {
 | |
| 		for _, s := range b.hosts.Subsets {
 | |
| 			for _, a := range s.Addresses {
 | |
| 				if !addr.Has(a.IP) {
 | |
| 					addr.Insert(a.IP)
 | |
| 					addrlist = append(addrlist, a.IP)
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if (len(addrlist) > 0) && (addrlist[0] != "") {
 | |
| 		ip := addrlist[rand.Intn(len(addrlist))]
 | |
| 
 | |
| 		// Add backup-volfile-servers and auto_unmount options.
 | |
| 		// When ip is also in backup-volfile-servers, there will be a warning:
 | |
| 		// "gf_remember_backup_volfile_server] 0-glusterfs: failed to set volfile server: File exists".
 | |
| 		addr.Delete(ip)
 | |
| 		backups := addr.List()
 | |
| 		// Avoid an invalid empty backup-volfile-servers option.
 | |
| 		if len(backups) > 0 {
 | |
| 			options = append(options, "backup-volfile-servers="+dstrings.Join(addrlist[:], ":"))
 | |
| 		}
 | |
| 		options = append(options, "auto_unmount")
 | |
| 
 | |
| 		mountOptions := volutil.JoinMountOptions(b.mountOptions, options)
 | |
| 		// with `backup-volfile-servers` mount option in place, it is not required to
 | |
| 		// iterate over all the servers in the addrlist. A mount attempt with this option
 | |
| 		// will fetch all the servers mentioned in the backup-volfile-servers list.
 | |
| 		// Refer to backup-volfile-servers @ http://docs.gluster.org/en/latest/Administrator%20Guide/Setting%20Up%20Clients/
 | |
| 
 | |
| 		errs = b.mounter.Mount(ip+":"+b.path, dir, "glusterfs", mountOptions)
 | |
| 		if errs == nil {
 | |
| 			klog.Infof("successfully mounted directory %s", dir)
 | |
| 			return nil
 | |
| 		}
 | |
| 		if dstrings.Contains(errs.Error(), "Invalid option auto_unmount") ||
 | |
| 			dstrings.Contains(errs.Error(), "Invalid argument") {
 | |
| 			// Give a try without `auto_unmount` mount option, because
 | |
| 			// it could be that gluster fuse client is older version and
 | |
| 			// mount.glusterfs is unaware of `auto_unmount`.
 | |
| 			noAutoMountOptions := make([]string, 0, len(mountOptions))
 | |
| 			for _, opt := range mountOptions {
 | |
| 				if opt != "auto_unmount" {
 | |
| 					noAutoMountOptions = append(noAutoMountOptions, opt)
 | |
| 				}
 | |
| 			}
 | |
| 			errs = b.mounter.Mount(ip+":"+b.path, dir, "glusterfs", noAutoMountOptions)
 | |
| 			if errs == nil {
 | |
| 				klog.Infof("successfully mounted %s", dir)
 | |
| 				return nil
 | |
| 			}
 | |
| 		}
 | |
| 	} else {
 | |
| 		return fmt.Errorf("failed to execute mount command:[no valid ipaddress found in endpoint address list]")
 | |
| 	}
 | |
| 
 | |
| 	// Failed mount scenario.
 | |
| 	// Since glusterfs does not return error text
 | |
| 	// it all goes in a log file, we will read the log file
 | |
| 	logErr := readGlusterLog(log, b.pod.Name)
 | |
| 	if logErr != nil {
 | |
| 		return fmt.Errorf("mount failed: %v, the following error information was pulled from the glusterfs log to help diagnose this issue: %v", errs, logErr)
 | |
| 	}
 | |
| 	return fmt.Errorf("mount failed: %v", errs)
 | |
| 
 | |
| }
 | |
| 
 | |
| //getVolumeInfo returns 'path' and 'readonly' field values from the provided glusterfs spec.
 | |
| func getVolumeInfo(spec *volume.Spec) (string, bool, error) {
 | |
| 	if spec.Volume != nil && spec.Volume.Glusterfs != nil {
 | |
| 		return spec.Volume.Glusterfs.Path, spec.Volume.Glusterfs.ReadOnly, nil
 | |
| 	} else if spec.PersistentVolume != nil &&
 | |
| 		spec.PersistentVolume.Spec.Glusterfs != nil {
 | |
| 		return spec.PersistentVolume.Spec.Glusterfs.Path, spec.ReadOnly, nil
 | |
| 	}
 | |
| 	return "", false, fmt.Errorf("spec does not reference a Glusterfs volume type")
 | |
| }
 | |
| 
 | |
| func (plugin *glusterfsPlugin) NewProvisioner(options volume.VolumeOptions) (volume.Provisioner, error) {
 | |
| 	return plugin.newProvisionerInternal(options)
 | |
| }
 | |
| 
 | |
| func (plugin *glusterfsPlugin) newProvisionerInternal(options volume.VolumeOptions) (volume.Provisioner, error) {
 | |
| 	return &glusterfsVolumeProvisioner{
 | |
| 		glusterfsMounter: &glusterfsMounter{
 | |
| 			glusterfs: &glusterfs{
 | |
| 				plugin: plugin,
 | |
| 			},
 | |
| 		},
 | |
| 		options: options,
 | |
| 	}, nil
 | |
| }
 | |
| 
 | |
| type provisionerConfig struct {
 | |
| 	url                string
 | |
| 	user               string
 | |
| 	userKey            string
 | |
| 	secretNamespace    string
 | |
| 	secretName         string
 | |
| 	secretValue        string `datapolicy:"token"`
 | |
| 	clusterID          string
 | |
| 	gidMin             int
 | |
| 	gidMax             int
 | |
| 	volumeType         gapi.VolumeDurabilityInfo
 | |
| 	volumeOptions      []string
 | |
| 	volumeNamePrefix   string
 | |
| 	thinPoolSnapFactor float32
 | |
| 	customEpNamePrefix string
 | |
| }
 | |
| 
 | |
| type glusterfsVolumeProvisioner struct {
 | |
| 	*glusterfsMounter
 | |
| 	provisionerConfig
 | |
| 	options volume.VolumeOptions
 | |
| }
 | |
| 
 | |
| func convertGid(gidString string) (int, error) {
 | |
| 	gid64, err := strconv.ParseInt(gidString, 10, 32)
 | |
| 	if err != nil {
 | |
| 		return 0, fmt.Errorf("failed to parse gid %v: %v", gidString, err)
 | |
| 	}
 | |
| 	if gid64 < 0 {
 | |
| 		return 0, fmt.Errorf("negative GIDs %v are not allowed", gidString)
 | |
| 	}
 | |
| 
 | |
| 	// ParseInt returns a int64, but since we parsed only
 | |
| 	// for 32 bit, we can cast to int without loss:
 | |
| 	gid := int(gid64)
 | |
| 	return gid, nil
 | |
| }
 | |
| 
 | |
| func convertVolumeParam(volumeString string) (int, error) {
 | |
| 	count, err := strconv.Atoi(volumeString)
 | |
| 	if err != nil {
 | |
| 		return 0, fmt.Errorf("failed to parse volumestring %q: %v", volumeString, err)
 | |
| 	}
 | |
| 
 | |
| 	if count < 0 {
 | |
| 		return 0, fmt.Errorf("negative values are not allowed")
 | |
| 	}
 | |
| 	return count, nil
 | |
| }
 | |
| 
 | |
| func (plugin *glusterfsPlugin) NewDeleter(spec *volume.Spec) (volume.Deleter, error) {
 | |
| 	return plugin.newDeleterInternal(spec)
 | |
| }
 | |
| 
 | |
| func (plugin *glusterfsPlugin) newDeleterInternal(spec *volume.Spec) (volume.Deleter, error) {
 | |
| 	if spec.PersistentVolume != nil && spec.PersistentVolume.Spec.Glusterfs == nil {
 | |
| 		return nil, fmt.Errorf("spec.PersistentVolume.Spec.Glusterfs is nil")
 | |
| 	}
 | |
| 	return &glusterfsVolumeDeleter{
 | |
| 		glusterfsMounter: &glusterfsMounter{
 | |
| 			glusterfs: &glusterfs{
 | |
| 				volName: spec.Name(),
 | |
| 				plugin:  plugin,
 | |
| 			},
 | |
| 			path: spec.PersistentVolume.Spec.Glusterfs.Path,
 | |
| 		},
 | |
| 		spec: spec.PersistentVolume,
 | |
| 	}, nil
 | |
| }
 | |
| 
 | |
| type glusterfsVolumeDeleter struct {
 | |
| 	*glusterfsMounter
 | |
| 	provisionerConfig
 | |
| 	spec *v1.PersistentVolume
 | |
| }
 | |
| 
 | |
| func (d *glusterfsVolumeDeleter) GetPath() string {
 | |
| 	name := glusterfsPluginName
 | |
| 	return d.plugin.host.GetPodVolumeDir(d.glusterfsMounter.glusterfs.pod.UID, utilstrings.EscapeQualifiedName(name), d.glusterfsMounter.glusterfs.volName)
 | |
| }
 | |
| 
 | |
| // Traverse the PVs, fetching all the GIDs from those
 | |
| // in a given storage class, and mark them in the table.
 | |
| func (plugin *glusterfsPlugin) collectGids(className string, gidTable *MinMaxAllocator) error {
 | |
| 	kubeClient := plugin.host.GetKubeClient()
 | |
| 	if kubeClient == nil {
 | |
| 		return fmt.Errorf("failed to get kube client when collecting gids")
 | |
| 	}
 | |
| 	pvList, err := kubeClient.CoreV1().PersistentVolumes().List(context.TODO(), metav1.ListOptions{LabelSelector: labels.Everything().String()})
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to get existing persistent volumes")
 | |
| 	}
 | |
| 	for _, pv := range pvList.Items {
 | |
| 		if storagehelpers.GetPersistentVolumeClass(&pv) != className {
 | |
| 			continue
 | |
| 		}
 | |
| 		pvName := pv.ObjectMeta.Name
 | |
| 		gidStr, ok := pv.Annotations[volutil.VolumeGidAnnotationKey]
 | |
| 		if !ok {
 | |
| 			klog.Warningf("no GID found in pv %v", pvName)
 | |
| 			continue
 | |
| 		}
 | |
| 		gid, err := convertGid(gidStr)
 | |
| 		if err != nil {
 | |
| 			klog.Errorf("failed to parse gid %s: %v", gidStr, err)
 | |
| 			continue
 | |
| 		}
 | |
| 		_, err = gidTable.Allocate(gid)
 | |
| 		if err == ErrConflict {
 | |
| 			klog.Warningf("GID %v found in pv %v was already allocated", gid, pvName)
 | |
| 		} else if err != nil {
 | |
| 			return fmt.Errorf("failed to store gid %v found in pv %v: %v", gid, pvName, err)
 | |
| 		}
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // Return the gid table for a storage class.
 | |
| // - If this is the first time, fill it with all the gids
 | |
| //   used in PVs of this storage class by traversing the PVs.
 | |
| // - Adapt the range of the table to the current range of the SC.
 | |
| func (plugin *glusterfsPlugin) getGidTable(className string, min int, max int) (*MinMaxAllocator, error) {
 | |
| 	plugin.gidTableLock.Lock()
 | |
| 	gidTable, ok := plugin.gidTable[className]
 | |
| 	plugin.gidTableLock.Unlock()
 | |
| 
 | |
| 	if ok {
 | |
| 		err := gidTable.SetRange(min, max)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		return gidTable, nil
 | |
| 	}
 | |
| 
 | |
| 	// create a new table and fill it
 | |
| 	newGidTable, err := NewMinMaxAllocator(0, absoluteGidMax)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	// collect gids with the full range
 | |
| 	err = plugin.collectGids(className, newGidTable)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	// and only reduce the range afterwards
 | |
| 	err = newGidTable.SetRange(min, max)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	// if in the meantime a table appeared, use it
 | |
| 	plugin.gidTableLock.Lock()
 | |
| 	defer plugin.gidTableLock.Unlock()
 | |
| 	gidTable, ok = plugin.gidTable[className]
 | |
| 	if ok {
 | |
| 		err = gidTable.SetRange(min, max)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 		return gidTable, nil
 | |
| 	}
 | |
| 
 | |
| 	plugin.gidTable[className] = newGidTable
 | |
| 	return newGidTable, nil
 | |
| }
 | |
| 
 | |
| func (d *glusterfsVolumeDeleter) getGid() (int, bool, error) {
 | |
| 	gidStr, ok := d.spec.Annotations[volutil.VolumeGidAnnotationKey]
 | |
| 	if !ok {
 | |
| 		return 0, false, nil
 | |
| 	}
 | |
| 	gid, err := convertGid(gidStr)
 | |
| 	return gid, true, err
 | |
| }
 | |
| 
 | |
| func (d *glusterfsVolumeDeleter) Delete() error {
 | |
| 	klog.V(2).Infof("delete volume %s", d.glusterfsMounter.path)
 | |
| 	volumeName := d.glusterfsMounter.path
 | |
| 	volumeID, err := getVolumeID(d.spec, volumeName)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to get volumeID: %v", err)
 | |
| 	}
 | |
| 	class, err := volutil.GetClassForVolume(d.plugin.host.GetKubeClient(), d.spec)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	cfg, err := parseClassParameters(class.Parameters, d.plugin.host.GetKubeClient())
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	d.provisionerConfig = *cfg
 | |
| 	klog.V(4).Infof("deleting volume %q", volumeID)
 | |
| 	gid, exists, err := d.getGid()
 | |
| 	if err != nil {
 | |
| 		klog.Error(err)
 | |
| 	} else if exists {
 | |
| 		gidTable, err := d.plugin.getGidTable(class.Name, cfg.gidMin, cfg.gidMax)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to get gidTable: %v", err)
 | |
| 		}
 | |
| 		err = gidTable.Release(gid)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to release gid %v: %v", gid, err)
 | |
| 		}
 | |
| 	}
 | |
| 	cli := filterClient(gcli.NewClient(d.url, d.user, d.secretValue), d.plugin.host.GetFilteredDialOptions())
 | |
| 	if cli == nil {
 | |
| 		klog.Errorf("failed to create glusterfs REST client")
 | |
| 		return fmt.Errorf("failed to create glusterfs REST client, REST server authentication failed")
 | |
| 	}
 | |
| 	err = cli.VolumeDelete(volumeID)
 | |
| 	if err != nil {
 | |
| 		if dstrings.TrimSpace(err.Error()) != errIDNotFound {
 | |
| 			// don't log error details from client calls in events
 | |
| 			klog.V(4).Infof("failed to delete volume %s: %v", volumeName, err)
 | |
| 			return fmt.Errorf("failed to delete volume: see kube-controller-manager.log for details")
 | |
| 		}
 | |
| 		klog.V(2).Infof("volume %s not present in heketi, ignoring", volumeName)
 | |
| 	}
 | |
| 	klog.V(2).Infof("volume %s deleted successfully", volumeName)
 | |
| 
 | |
| 	//Deleter takes endpoint and namespace from pv spec.
 | |
| 	pvSpec := d.spec.Spec
 | |
| 	var dynamicEndpoint, dynamicNamespace string
 | |
| 	if pvSpec.ClaimRef == nil {
 | |
| 		klog.Errorf("ClaimRef is nil")
 | |
| 		return fmt.Errorf("ClaimRef is nil")
 | |
| 	}
 | |
| 	if pvSpec.ClaimRef.Namespace == "" {
 | |
| 		klog.Errorf("namespace is nil")
 | |
| 		return fmt.Errorf("namespace is nil")
 | |
| 	}
 | |
| 	dynamicNamespace = pvSpec.ClaimRef.Namespace
 | |
| 	if pvSpec.Glusterfs.EndpointsName != "" {
 | |
| 		dynamicEndpoint = pvSpec.Glusterfs.EndpointsName
 | |
| 	}
 | |
| 	klog.V(3).Infof("dynamic namespace and endpoint %v/%v", dynamicNamespace, dynamicEndpoint)
 | |
| 	err = d.deleteEndpointService(dynamicNamespace, dynamicEndpoint)
 | |
| 	if err != nil {
 | |
| 		klog.Errorf("failed to delete endpoint/service %v/%v: %v", dynamicNamespace, dynamicEndpoint, err)
 | |
| 	} else {
 | |
| 		klog.V(1).Infof("endpoint %v/%v is deleted successfully ", dynamicNamespace, dynamicEndpoint)
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func filterClient(client *gcli.Client, opts *proxyutil.FilteredDialOptions) *gcli.Client {
 | |
| 	if opts == nil {
 | |
| 		return client
 | |
| 	}
 | |
| 	dialer := proxyutil.NewFilteredDialContext(nil, nil, opts)
 | |
| 	client.SetClientFunc(func(tlsConfig *tls.Config, checkRedirect gcli.CheckRedirectFunc) (gcli.HttpPerformer, error) {
 | |
| 		transport := http.DefaultTransport.(*http.Transport).Clone()
 | |
| 		transport.DialContext = dialer
 | |
| 		transport.TLSClientConfig = tlsConfig
 | |
| 		return &http.Client{Transport: transport, CheckRedirect: checkRedirect}, nil
 | |
| 	})
 | |
| 	return client
 | |
| }
 | |
| 
 | |
| func (p *glusterfsVolumeProvisioner) Provision(selectedNode *v1.Node, allowedTopologies []v1.TopologySelectorTerm) (*v1.PersistentVolume, error) {
 | |
| 	if !volutil.ContainsAllAccessModes(p.plugin.GetAccessModes(), p.options.PVC.Spec.AccessModes) {
 | |
| 		return nil, fmt.Errorf("invalid AccessModes %v: only AccessModes %v are supported", p.options.PVC.Spec.AccessModes, p.plugin.GetAccessModes())
 | |
| 	}
 | |
| 	if p.options.PVC.Spec.Selector != nil {
 | |
| 		klog.V(4).Infof("not able to parse your claim Selector")
 | |
| 		return nil, fmt.Errorf("not able to parse your claim Selector")
 | |
| 	}
 | |
| 	if volutil.CheckPersistentVolumeClaimModeBlock(p.options.PVC) {
 | |
| 		return nil, fmt.Errorf("%s does not support block volume provisioning", p.plugin.GetPluginName())
 | |
| 	}
 | |
| 	klog.V(4).Infof("provision volume with options %v", p.options)
 | |
| 	scName := storagehelpers.GetPersistentVolumeClaimClass(p.options.PVC)
 | |
| 	cfg, err := parseClassParameters(p.options.Parameters, p.plugin.host.GetKubeClient())
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	p.provisionerConfig = *cfg
 | |
| 
 | |
| 	gidTable, err := p.plugin.getGidTable(scName, cfg.gidMin, cfg.gidMax)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to get gidTable: %v", err)
 | |
| 	}
 | |
| 	gid, _, err := gidTable.AllocateNext()
 | |
| 	if err != nil {
 | |
| 		klog.Errorf("failed to reserve GID from table: %v", err)
 | |
| 		return nil, fmt.Errorf("failed to reserve GID from table: %v", err)
 | |
| 	}
 | |
| 	klog.V(2).Infof("allocated GID %d for PVC %s", gid, p.options.PVC.Name)
 | |
| 	glusterfs, sizeGiB, volID, err := p.CreateVolume(gid)
 | |
| 	if err != nil {
 | |
| 		if releaseErr := gidTable.Release(gid); releaseErr != nil {
 | |
| 			klog.Errorf("error when releasing GID in storageclass %s: %v", scName, releaseErr)
 | |
| 		}
 | |
| 		return nil, fmt.Errorf("failed to create volume: %v", err)
 | |
| 	}
 | |
| 	mode := v1.PersistentVolumeFilesystem
 | |
| 	pv := new(v1.PersistentVolume)
 | |
| 	pv.Spec.PersistentVolumeSource.Glusterfs = glusterfs
 | |
| 	pv.Spec.PersistentVolumeReclaimPolicy = p.options.PersistentVolumeReclaimPolicy
 | |
| 	pv.Spec.AccessModes = p.options.PVC.Spec.AccessModes
 | |
| 	pv.Spec.VolumeMode = &mode
 | |
| 	if len(pv.Spec.AccessModes) == 0 {
 | |
| 		pv.Spec.AccessModes = p.plugin.GetAccessModes()
 | |
| 	}
 | |
| 	pv.Spec.MountOptions = p.options.MountOptions
 | |
| 	gidStr := strconv.FormatInt(int64(gid), 10)
 | |
| 	pv.Annotations = map[string]string{
 | |
| 		volutil.VolumeGidAnnotationKey:        gidStr,
 | |
| 		volutil.VolumeDynamicallyCreatedByKey: heketiAnn,
 | |
| 		glusterTypeAnn:                        "file",
 | |
| 		"Description":                         glusterDescAnn,
 | |
| 		heketiVolIDAnn:                        volID,
 | |
| 	}
 | |
| 	pv.Spec.Capacity = v1.ResourceList{
 | |
| 		v1.ResourceName(v1.ResourceStorage): resource.MustParse(fmt.Sprintf("%dGi", sizeGiB)),
 | |
| 	}
 | |
| 	return pv, nil
 | |
| }
 | |
| 
 | |
| func (p *glusterfsVolumeProvisioner) CreateVolume(gid int) (r *v1.GlusterfsPersistentVolumeSource, size int, volID string, err error) {
 | |
| 	var clusterIDs []string
 | |
| 	customVolumeName := ""
 | |
| 	epServiceName := ""
 | |
| 	kubeClient := p.plugin.host.GetKubeClient()
 | |
| 	if kubeClient == nil {
 | |
| 		return nil, 0, "", fmt.Errorf("failed to get kube client to update endpoint")
 | |
| 	}
 | |
| 	if len(p.provisionerConfig.customEpNamePrefix) == 0 {
 | |
| 		epServiceName = string(p.options.PVC.UID)
 | |
| 	} else {
 | |
| 		epServiceName = p.provisionerConfig.customEpNamePrefix + "-" + string(p.options.PVC.UID)
 | |
| 	}
 | |
| 	epNamespace := p.options.PVC.Namespace
 | |
| 	endpoint, service, err := p.createOrGetEndpointService(epNamespace, epServiceName, p.options.PVC)
 | |
| 	if err != nil {
 | |
| 		klog.Errorf("failed to create endpoint/service %v/%v: %v", epNamespace, epServiceName, err)
 | |
| 		return nil, 0, "", fmt.Errorf("failed to create endpoint/service %v/%v: %v", epNamespace, epServiceName, err)
 | |
| 	}
 | |
| 	klog.V(3).Infof("dynamic endpoint %v and service %v ", endpoint, service)
 | |
| 	capacity := p.options.PVC.Spec.Resources.Requests[v1.ResourceName(v1.ResourceStorage)]
 | |
| 
 | |
| 	// GlusterFS/heketi creates volumes in units of GiB.
 | |
| 	sz, err := volumehelpers.RoundUpToGiBInt(capacity)
 | |
| 	if err != nil {
 | |
| 		return nil, 0, "", err
 | |
| 	}
 | |
| 	klog.V(2).Infof("create volume of size %dGiB", sz)
 | |
| 	if p.url == "" {
 | |
| 		return nil, 0, "", fmt.Errorf("failed to create glusterfs REST client, REST URL is empty")
 | |
| 	}
 | |
| 	cli := filterClient(gcli.NewClient(p.url, p.user, p.secretValue), p.plugin.host.GetFilteredDialOptions())
 | |
| 	if cli == nil {
 | |
| 		return nil, 0, "", fmt.Errorf("failed to create glusterfs REST client, REST server authentication failed")
 | |
| 	}
 | |
| 	if p.provisionerConfig.clusterID != "" {
 | |
| 		clusterIDs = dstrings.Split(p.clusterID, ",")
 | |
| 		klog.V(4).Infof("provided clusterIDs %v", clusterIDs)
 | |
| 	}
 | |
| 
 | |
| 	if p.provisionerConfig.volumeNamePrefix != "" {
 | |
| 		customVolumeName = fmt.Sprintf("%s_%s_%s_%s", p.provisionerConfig.volumeNamePrefix, p.options.PVC.Namespace, p.options.PVC.Name, uuid.NewUUID())
 | |
| 	}
 | |
| 	gid64 := int64(gid)
 | |
| 	snaps := struct {
 | |
| 		Enable bool    `json:"enable"`
 | |
| 		Factor float32 `json:"factor"`
 | |
| 	}{
 | |
| 		true,
 | |
| 		p.provisionerConfig.thinPoolSnapFactor,
 | |
| 	}
 | |
| 	volumeReq := &gapi.VolumeCreateRequest{Size: sz, Name: customVolumeName, Clusters: clusterIDs, Gid: gid64, Durability: p.volumeType, GlusterVolumeOptions: p.volumeOptions, Snapshot: snaps}
 | |
| 	volume, err := cli.VolumeCreate(volumeReq)
 | |
| 	if err != nil {
 | |
| 		// don't log error details from client calls in events
 | |
| 		klog.V(4).Infof("failed to create volume: %v", err)
 | |
| 		return nil, 0, "", fmt.Errorf("failed to create volume: see kube-controller-manager.log for details")
 | |
| 	}
 | |
| 	klog.V(1).Infof("volume with size %d and name %s created", volume.Size, volume.Name)
 | |
| 	volID = volume.Id
 | |
| 	dynamicHostIps, err := getClusterNodes(cli, volume.Cluster)
 | |
| 	if err != nil {
 | |
| 		return nil, 0, "", fmt.Errorf("failed to get cluster nodes for volume %s: %v", volume, err)
 | |
| 	}
 | |
| 	addrlist := make([]v1.EndpointAddress, len(dynamicHostIps))
 | |
| 	for i, v := range dynamicHostIps {
 | |
| 		addrlist[i].IP = v
 | |
| 	}
 | |
| 	subset := make([]v1.EndpointSubset, 1)
 | |
| 	ports := []v1.EndpointPort{{Port: 1, Protocol: "TCP"}}
 | |
| 	endpoint.Subsets = subset
 | |
| 	endpoint.Subsets[0].Addresses = addrlist
 | |
| 	endpoint.Subsets[0].Ports = ports
 | |
| 	_, err = kubeClient.CoreV1().Endpoints(epNamespace).Update(context.TODO(), endpoint, metav1.UpdateOptions{})
 | |
| 	if err != nil {
 | |
| 		deleteErr := cli.VolumeDelete(volume.Id)
 | |
| 		if deleteErr != nil {
 | |
| 			// don't log error details from client calls in events
 | |
| 			klog.V(4).Infof("failed to delete volume: %v, manual deletion of the volume required", deleteErr)
 | |
| 		}
 | |
| 		klog.V(3).Infof("failed to update endpoint, deleting %s", endpoint)
 | |
| 		err = kubeClient.CoreV1().Services(epNamespace).Delete(context.TODO(), epServiceName, metav1.DeleteOptions{})
 | |
| 		if err != nil && errors.IsNotFound(err) {
 | |
| 			klog.V(1).Infof("service %s does not exist in namespace %s", epServiceName, epNamespace)
 | |
| 			err = nil
 | |
| 		}
 | |
| 		if err != nil {
 | |
| 			klog.Errorf("failed to delete service %s/%s: %v", epNamespace, epServiceName, err)
 | |
| 		}
 | |
| 		klog.V(1).Infof("service/endpoint: %s/%s deleted successfully", epNamespace, epServiceName)
 | |
| 		return nil, 0, "", fmt.Errorf("failed to update endpoint %s: %v", endpoint, err)
 | |
| 	}
 | |
| 	klog.V(3).Infof("endpoint %s updated successfully", endpoint)
 | |
| 	return &v1.GlusterfsPersistentVolumeSource{
 | |
| 		EndpointsName:      endpoint.Name,
 | |
| 		EndpointsNamespace: &epNamespace,
 | |
| 		Path:               volume.Name,
 | |
| 		ReadOnly:           false,
 | |
| 	}, sz, volID, nil
 | |
| }
 | |
| 
 | |
| // createOrGetEndpointService() makes sure an endpoint and service
 | |
| // exist for the given namespace, PVC name, endpoint name
 | |
| // I.e. the endpoint or service is only created
 | |
| // if it does not exist yet.
 | |
| func (p *glusterfsVolumeProvisioner) createOrGetEndpointService(namespace string, epServiceName string, pvc *v1.PersistentVolumeClaim) (endpoint *v1.Endpoints, service *v1.Service, err error) {
 | |
| 	pvcNameOrID := ""
 | |
| 	if len(pvc.Name) >= 63 {
 | |
| 		pvcNameOrID = string(pvc.UID)
 | |
| 	} else {
 | |
| 		pvcNameOrID = pvc.Name
 | |
| 	}
 | |
| 	endpoint = &v1.Endpoints{
 | |
| 		ObjectMeta: metav1.ObjectMeta{
 | |
| 			Namespace: namespace,
 | |
| 			Name:      epServiceName,
 | |
| 			Labels: map[string]string{
 | |
| 				"gluster.kubernetes.io/provisioned-for-pvc": pvcNameOrID,
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 	kubeClient := p.plugin.host.GetKubeClient()
 | |
| 	if kubeClient == nil {
 | |
| 		return nil, nil, fmt.Errorf("failed to get kube client when creating endpoint service")
 | |
| 	}
 | |
| 	_, err = kubeClient.CoreV1().Endpoints(namespace).Create(context.TODO(), endpoint, metav1.CreateOptions{})
 | |
| 	if err != nil && errors.IsAlreadyExists(err) {
 | |
| 		klog.V(1).Infof("endpoint %s already exist in namespace %s", endpoint, namespace)
 | |
| 		err = nil
 | |
| 	}
 | |
| 	if err != nil {
 | |
| 		klog.Errorf("failed to create endpoint: %v", err)
 | |
| 		return nil, nil, fmt.Errorf("failed to create endpoint: %v", err)
 | |
| 	}
 | |
| 	service = &v1.Service{
 | |
| 		ObjectMeta: metav1.ObjectMeta{
 | |
| 			Name:      epServiceName,
 | |
| 			Namespace: namespace,
 | |
| 			Labels: map[string]string{
 | |
| 				"gluster.kubernetes.io/provisioned-for-pvc": pvcNameOrID,
 | |
| 			},
 | |
| 		},
 | |
| 		Spec: v1.ServiceSpec{
 | |
| 			Ports: []v1.ServicePort{
 | |
| 				{Protocol: "TCP", Port: 1}}}}
 | |
| 	_, err = kubeClient.CoreV1().Services(namespace).Create(context.TODO(), service, metav1.CreateOptions{})
 | |
| 	if err != nil && errors.IsAlreadyExists(err) {
 | |
| 		klog.V(1).Infof("service %s already exist in namespace %s", service, namespace)
 | |
| 		err = nil
 | |
| 	}
 | |
| 	if err != nil {
 | |
| 		klog.Errorf("failed to create service: %v", err)
 | |
| 		return nil, nil, fmt.Errorf("error creating service: %v", err)
 | |
| 	}
 | |
| 	return endpoint, service, nil
 | |
| }
 | |
| 
 | |
| func (d *glusterfsVolumeDeleter) deleteEndpointService(namespace string, epServiceName string) (err error) {
 | |
| 	kubeClient := d.plugin.host.GetKubeClient()
 | |
| 	if kubeClient == nil {
 | |
| 		return fmt.Errorf("failed to get kube client when deleting endpoint service")
 | |
| 	}
 | |
| 	err = kubeClient.CoreV1().Services(namespace).Delete(context.TODO(), epServiceName, metav1.DeleteOptions{})
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to delete service %s/%s: %v", namespace, epServiceName, err)
 | |
| 	}
 | |
| 	klog.V(1).Infof("service/endpoint: %s/%s deleted successfully", namespace, epServiceName)
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // parseSecret finds a given Secret instance and reads user password from it.
 | |
| func parseSecret(namespace, secretName string, kubeClient clientset.Interface) (string, error) {
 | |
| 	secretMap, err := volutil.GetSecretForPV(namespace, secretName, glusterfsPluginName, kubeClient)
 | |
| 	if err != nil {
 | |
| 		klog.Errorf("failed to get secret: %s/%s: %v", namespace, secretName, err)
 | |
| 		return "", fmt.Errorf("failed to get secret %s/%s: %v", namespace, secretName, err)
 | |
| 	}
 | |
| 	if len(secretMap) == 0 {
 | |
| 		return "", fmt.Errorf("empty secret map")
 | |
| 	}
 | |
| 	secret := ""
 | |
| 	for k, v := range secretMap {
 | |
| 		if k == secretKeyName {
 | |
| 			return v, nil
 | |
| 		}
 | |
| 		secret = v
 | |
| 	}
 | |
| 
 | |
| 	// If not found, the last secret in the map wins as done before
 | |
| 	return secret, nil
 | |
| }
 | |
| 
 | |
| // getClusterNodes() returns the cluster nodes of a given cluster
 | |
| func getClusterNodes(cli *gcli.Client, cluster string) (dynamicHostIps []string, err error) {
 | |
| 	clusterinfo, err := cli.ClusterInfo(cluster)
 | |
| 	if err != nil {
 | |
| 		// don't log error details from client calls in events
 | |
| 		klog.V(4).Infof("failed to get cluster details: %v", err)
 | |
| 		return nil, fmt.Errorf("failed to get cluster details: see kube-controller-manager.log for details")
 | |
| 	}
 | |
| 
 | |
| 	// For the dynamically provisioned volume, we gather the list of node IPs
 | |
| 	// of the cluster on which provisioned volume belongs to, as there can be multiple
 | |
| 	// clusters.
 | |
| 	for _, node := range clusterinfo.Nodes {
 | |
| 		nodeInfo, err := cli.NodeInfo(string(node))
 | |
| 		if err != nil {
 | |
| 			// don't log error details from client calls in events
 | |
| 			klog.V(4).Infof("failed to get host ipaddress: %v", err)
 | |
| 			return nil, fmt.Errorf("failed to get host ipaddress: see kube-controller-manager.log for details")
 | |
| 		}
 | |
| 		ipaddr := dstrings.Join(nodeInfo.NodeAddRequest.Hostnames.Storage, "")
 | |
| 		// IP validates if a string is a valid IP address.
 | |
| 		ip := net.ParseIP(ipaddr)
 | |
| 		if ip == nil {
 | |
| 			return nil, fmt.Errorf("glusterfs server node ip address %s must be a valid IP address, (e.g. 10.9.8.7)", ipaddr)
 | |
| 		}
 | |
| 		dynamicHostIps = append(dynamicHostIps, ipaddr)
 | |
| 	}
 | |
| 	klog.V(3).Infof("host list :%v", dynamicHostIps)
 | |
| 	if len(dynamicHostIps) == 0 {
 | |
| 		return nil, fmt.Errorf("no hosts found: %v", err)
 | |
| 	}
 | |
| 	return dynamicHostIps, nil
 | |
| }
 | |
| 
 | |
| // parseClassParameters parses StorageClass parameters.
 | |
| func parseClassParameters(params map[string]string, kubeClient clientset.Interface) (*provisionerConfig, error) {
 | |
| 	var cfg provisionerConfig
 | |
| 	var err error
 | |
| 	cfg.gidMin = defaultGidMin
 | |
| 	cfg.gidMax = defaultGidMax
 | |
| 	cfg.customEpNamePrefix = dynamicEpSvcPrefix
 | |
| 
 | |
| 	authEnabled := true
 | |
| 	parseVolumeType := ""
 | |
| 	parseVolumeOptions := ""
 | |
| 	parseVolumeNamePrefix := ""
 | |
| 	parseThinPoolSnapFactor := ""
 | |
| 
 | |
| 	//thin pool snap factor default to 1.0
 | |
| 	cfg.thinPoolSnapFactor = float32(1.0)
 | |
| 
 | |
| 	for k, v := range params {
 | |
| 		switch dstrings.ToLower(k) {
 | |
| 		case "resturl":
 | |
| 			cfg.url = v
 | |
| 		case "restuser":
 | |
| 			cfg.user = v
 | |
| 		case "restuserkey":
 | |
| 			cfg.userKey = v
 | |
| 		case "secretname":
 | |
| 			cfg.secretName = v
 | |
| 		case "secretnamespace":
 | |
| 			cfg.secretNamespace = v
 | |
| 		case "clusterid":
 | |
| 			if len(v) != 0 {
 | |
| 				cfg.clusterID = v
 | |
| 			}
 | |
| 		case "restauthenabled":
 | |
| 			authEnabled = dstrings.ToLower(v) == "true"
 | |
| 		case "gidmin":
 | |
| 			parseGidMin, err := convertGid(v)
 | |
| 			if err != nil {
 | |
| 				return nil, fmt.Errorf("invalid gidMin value %q for volume plugin %s", k, glusterfsPluginName)
 | |
| 			}
 | |
| 			if parseGidMin < absoluteGidMin {
 | |
| 				return nil, fmt.Errorf("gidMin must be >= %v", absoluteGidMin)
 | |
| 			}
 | |
| 			if parseGidMin > absoluteGidMax {
 | |
| 				return nil, fmt.Errorf("gidMin must be <= %v", absoluteGidMax)
 | |
| 			}
 | |
| 			cfg.gidMin = parseGidMin
 | |
| 		case "gidmax":
 | |
| 			parseGidMax, err := convertGid(v)
 | |
| 			if err != nil {
 | |
| 				return nil, fmt.Errorf("invalid gidMax value %q for volume plugin %s", k, glusterfsPluginName)
 | |
| 			}
 | |
| 			if parseGidMax < absoluteGidMin {
 | |
| 				return nil, fmt.Errorf("gidMax must be >= %v", absoluteGidMin)
 | |
| 			}
 | |
| 			if parseGidMax > absoluteGidMax {
 | |
| 				return nil, fmt.Errorf("gidMax must be <= %v", absoluteGidMax)
 | |
| 			}
 | |
| 			cfg.gidMax = parseGidMax
 | |
| 		case "volumetype":
 | |
| 			parseVolumeType = v
 | |
| 
 | |
| 		case "volumeoptions":
 | |
| 			if len(v) != 0 {
 | |
| 				parseVolumeOptions = v
 | |
| 			}
 | |
| 		case "volumenameprefix":
 | |
| 			if len(v) != 0 {
 | |
| 				parseVolumeNamePrefix = v
 | |
| 			}
 | |
| 		case "snapfactor":
 | |
| 			if len(v) != 0 {
 | |
| 				parseThinPoolSnapFactor = v
 | |
| 			}
 | |
| 		case "customepnameprefix":
 | |
| 			// If the string has > 'maxCustomEpNamePrefixLen' chars, the final endpoint name will
 | |
| 			// exceed the limitation of 63 chars, so fail if prefix is > 'maxCustomEpNamePrefixLen'
 | |
| 			// characters. This is only applicable for 'customepnameprefix' string and default ep name
 | |
| 			// string will always pass.
 | |
| 			if len(v) <= maxCustomEpNamePrefixLen {
 | |
| 				cfg.customEpNamePrefix = v
 | |
| 			} else {
 | |
| 				return nil, fmt.Errorf("'customepnameprefix' value should be < %d characters", maxCustomEpNamePrefixLen)
 | |
| 			}
 | |
| 		default:
 | |
| 			return nil, fmt.Errorf("invalid option %q for volume plugin %s", k, glusterfsPluginName)
 | |
| 		}
 | |
| 	}
 | |
| 	if len(cfg.url) == 0 {
 | |
| 		return nil, fmt.Errorf("StorageClass for provisioner %s must contain 'resturl' parameter", glusterfsPluginName)
 | |
| 	}
 | |
| 	if len(parseVolumeType) == 0 {
 | |
| 		cfg.volumeType = gapi.VolumeDurabilityInfo{Type: gapi.DurabilityReplicate, Replicate: gapi.ReplicaDurability{Replica: replicaCount}}
 | |
| 	} else {
 | |
| 		parseVolumeTypeInfo := dstrings.Split(parseVolumeType, ":")
 | |
| 		switch parseVolumeTypeInfo[0] {
 | |
| 		case "replicate":
 | |
| 			if len(parseVolumeTypeInfo) >= 2 {
 | |
| 				newReplicaCount, err := convertVolumeParam(parseVolumeTypeInfo[1])
 | |
| 				if err != nil {
 | |
| 					return nil, fmt.Errorf("error parsing volumeType %q: %s", parseVolumeTypeInfo[1], err)
 | |
| 				}
 | |
| 				cfg.volumeType = gapi.VolumeDurabilityInfo{Type: gapi.DurabilityReplicate, Replicate: gapi.ReplicaDurability{Replica: newReplicaCount}}
 | |
| 			} else {
 | |
| 				cfg.volumeType = gapi.VolumeDurabilityInfo{Type: gapi.DurabilityReplicate, Replicate: gapi.ReplicaDurability{Replica: replicaCount}}
 | |
| 			}
 | |
| 		case "disperse":
 | |
| 			if len(parseVolumeTypeInfo) >= 3 {
 | |
| 				newDisperseData, err := convertVolumeParam(parseVolumeTypeInfo[1])
 | |
| 				if err != nil {
 | |
| 					return nil, fmt.Errorf("error parsing volumeType %q: %s", parseVolumeTypeInfo[1], err)
 | |
| 				}
 | |
| 				newDisperseRedundancy, err := convertVolumeParam(parseVolumeTypeInfo[2])
 | |
| 				if err != nil {
 | |
| 					return nil, fmt.Errorf("error parsing volumeType %q: %s", parseVolumeTypeInfo[2], err)
 | |
| 				}
 | |
| 				cfg.volumeType = gapi.VolumeDurabilityInfo{Type: gapi.DurabilityEC, Disperse: gapi.DisperseDurability{Data: newDisperseData, Redundancy: newDisperseRedundancy}}
 | |
| 			} else {
 | |
| 				return nil, fmt.Errorf("StorageClass for provisioner %q must have data:redundancy count set for disperse volumes in storage class option '%s'", glusterfsPluginName, "volumetype")
 | |
| 			}
 | |
| 		case "none":
 | |
| 			cfg.volumeType = gapi.VolumeDurabilityInfo{Type: gapi.DurabilityDistributeOnly}
 | |
| 		default:
 | |
| 			return nil, fmt.Errorf("error parsing value for option 'volumetype' for volume plugin %s", glusterfsPluginName)
 | |
| 		}
 | |
| 	}
 | |
| 	if !authEnabled {
 | |
| 		cfg.user = ""
 | |
| 		cfg.secretName = ""
 | |
| 		cfg.secretNamespace = ""
 | |
| 		cfg.userKey = ""
 | |
| 		cfg.secretValue = ""
 | |
| 	}
 | |
| 
 | |
| 	if len(cfg.secretName) != 0 || len(cfg.secretNamespace) != 0 {
 | |
| 		// secretName + Namespace has precedence over userKey
 | |
| 		if len(cfg.secretName) != 0 && len(cfg.secretNamespace) != 0 {
 | |
| 			cfg.secretValue, err = parseSecret(cfg.secretNamespace, cfg.secretName, kubeClient)
 | |
| 			if err != nil {
 | |
| 				return nil, err
 | |
| 			}
 | |
| 		} else {
 | |
| 			return nil, fmt.Errorf("StorageClass for provisioner %q must have secretNamespace and secretName either both set or both empty", glusterfsPluginName)
 | |
| 		}
 | |
| 	} else {
 | |
| 		cfg.secretValue = cfg.userKey
 | |
| 	}
 | |
| 	if cfg.gidMin > cfg.gidMax {
 | |
| 		return nil, fmt.Errorf("storageClass for provisioner %q must have gidMax value >= gidMin", glusterfsPluginName)
 | |
| 	}
 | |
| 	if len(parseVolumeOptions) != 0 {
 | |
| 		volOptions := dstrings.Split(parseVolumeOptions, ",")
 | |
| 		if len(volOptions) == 0 {
 | |
| 			return nil, fmt.Errorf("storageClass for provisioner %q must have valid (for e.g., 'client.ssl on') volume option", glusterfsPluginName)
 | |
| 		}
 | |
| 		cfg.volumeOptions = volOptions
 | |
| 	}
 | |
| 	if len(parseVolumeNamePrefix) != 0 {
 | |
| 		if dstrings.Contains(parseVolumeNamePrefix, "_") {
 | |
| 			return nil, fmt.Errorf("storageclass parameter 'volumenameprefix' should not contain '_' in its value")
 | |
| 		}
 | |
| 		cfg.volumeNamePrefix = parseVolumeNamePrefix
 | |
| 	}
 | |
| 	if len(parseThinPoolSnapFactor) != 0 {
 | |
| 		thinPoolSnapFactor, err := strconv.ParseFloat(parseThinPoolSnapFactor, 32)
 | |
| 		if err != nil {
 | |
| 			return nil, fmt.Errorf("failed to convert snapfactor %v to float: %v", parseThinPoolSnapFactor, err)
 | |
| 		}
 | |
| 		if thinPoolSnapFactor < 1.0 || thinPoolSnapFactor > 100.0 {
 | |
| 			return nil, fmt.Errorf("invalid snapshot factor %v, the value must be between 1 to 100", thinPoolSnapFactor)
 | |
| 		}
 | |
| 		cfg.thinPoolSnapFactor = float32(thinPoolSnapFactor)
 | |
| 	}
 | |
| 	return &cfg, nil
 | |
| }
 | |
| 
 | |
| // getVolumeID returns volumeID from the PV or volumename.
 | |
| func getVolumeID(pv *v1.PersistentVolume, volumeName string) (string, error) {
 | |
| 	volumeID := ""
 | |
| 
 | |
| 	// Get volID from pvspec if available, else fill it from volumename.
 | |
| 	if pv != nil {
 | |
| 		if pv.Annotations[heketiVolIDAnn] != "" {
 | |
| 			volumeID = pv.Annotations[heketiVolIDAnn]
 | |
| 		} else {
 | |
| 			volumeID = dstrings.TrimPrefix(volumeName, volPrefix)
 | |
| 		}
 | |
| 	} else {
 | |
| 		return volumeID, fmt.Errorf("provided PV spec is nil")
 | |
| 	}
 | |
| 	if volumeID == "" {
 | |
| 		return volumeID, fmt.Errorf("volume ID is empty")
 | |
| 	}
 | |
| 	return volumeID, nil
 | |
| }
 | |
| 
 | |
| func (plugin *glusterfsPlugin) ExpandVolumeDevice(spec *volume.Spec, newSize resource.Quantity, oldSize resource.Quantity) (resource.Quantity, error) {
 | |
| 	pvSpec := spec.PersistentVolume.Spec
 | |
| 	volumeName := pvSpec.Glusterfs.Path
 | |
| 	klog.V(2).Infof("received request to expand volume %s", volumeName)
 | |
| 	volumeID, err := getVolumeID(spec.PersistentVolume, volumeName)
 | |
| 	if err != nil {
 | |
| 		return oldSize, fmt.Errorf("failed to get volumeID for volume %s: %v", volumeName, err)
 | |
| 	}
 | |
| 	//Get details of StorageClass.
 | |
| 	class, err := volutil.GetClassForVolume(plugin.host.GetKubeClient(), spec.PersistentVolume)
 | |
| 	if err != nil {
 | |
| 		return oldSize, err
 | |
| 	}
 | |
| 	cfg, err := parseClassParameters(class.Parameters, plugin.host.GetKubeClient())
 | |
| 	if err != nil {
 | |
| 		return oldSize, err
 | |
| 	}
 | |
| 	klog.V(4).Infof("expanding volume: %q", volumeID)
 | |
| 
 | |
| 	//Create REST server connection
 | |
| 	cli := filterClient(gcli.NewClient(cfg.url, cfg.user, cfg.secretValue), plugin.host.GetFilteredDialOptions())
 | |
| 	if cli == nil {
 | |
| 		klog.Errorf("failed to create glusterfs REST client")
 | |
| 		return oldSize, fmt.Errorf("failed to create glusterfs REST client, REST server authentication failed")
 | |
| 	}
 | |
| 
 | |
| 	// Find out delta size
 | |
| 	expansionSize := newSize
 | |
| 	expansionSize.Sub(oldSize)
 | |
| 	expansionSizeGiB, err := volumehelpers.RoundUpToGiBInt(expansionSize)
 | |
| 	if err != nil {
 | |
| 		return oldSize, err
 | |
| 	}
 | |
| 
 | |
| 	// Find out requested Size
 | |
| 	requestGiB, err := volumehelpers.RoundUpToGiB(newSize)
 | |
| 	if err != nil {
 | |
| 		return oldSize, err
 | |
| 	}
 | |
| 
 | |
| 	//Check the existing volume size
 | |
| 	currentVolumeInfo, err := cli.VolumeInfo(volumeID)
 | |
| 	if err != nil {
 | |
| 		// don't log error details from client calls in events
 | |
| 		klog.V(4).Infof("error when fetching details of volume %s: %v", volumeName, err)
 | |
| 		return oldSize, fmt.Errorf("failed to get volume info %s: see kube-controller-manager.log for details", volumeName)
 | |
| 	}
 | |
| 	if int64(currentVolumeInfo.Size) >= requestGiB {
 | |
| 		return newSize, nil
 | |
| 	}
 | |
| 
 | |
| 	// Make volume expansion request
 | |
| 	volumeExpandReq := &gapi.VolumeExpandRequest{Size: expansionSizeGiB}
 | |
| 
 | |
| 	// Expand the volume
 | |
| 	volumeInfoRes, err := cli.VolumeExpand(volumeID, volumeExpandReq)
 | |
| 	if err != nil {
 | |
| 		// don't log error details from client calls in events
 | |
| 		klog.V(4).Infof("failed to expand volume %s: %v", volumeName, err)
 | |
| 		return oldSize, fmt.Errorf("failed to expand volume: see kube-controller-manager.log for details")
 | |
| 	}
 | |
| 	klog.V(2).Infof("volume %s expanded to new size %d successfully", volumeName, volumeInfoRes.Size)
 | |
| 	newVolumeSize := resource.MustParse(fmt.Sprintf("%dGi", volumeInfoRes.Size))
 | |
| 	return newVolumeSize, nil
 | |
| }
 |