Skip to content
Snippets Groups Projects
efs-provisioner.go 8.33 KiB
Newer Older
  • Learn to ignore specific revisions
  • Matthew Wong's avatar
    Matthew Wong committed
    /*
    Copyright 2017 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 main
    
    import (
    	"flag"
    	"fmt"
    	"os"
    	"os/exec"
    	"path"
    	"strconv"
    	"strings"
    
    	"github.com/aws/aws-sdk-go/aws"
    	"github.com/aws/aws-sdk-go/aws/session"
    	"github.com/aws/aws-sdk-go/service/efs"
    	"github.com/golang/glog"
    
    	"github.com/kubernetes-sigs/sig-storage-lib-external-provisioner/controller"
    	"github.com/kubernetes-sigs/sig-storage-lib-external-provisioner/gidallocator"
    	"github.com/kubernetes-sigs/sig-storage-lib-external-provisioner/mount"
    
    	"k8s.io/api/core/v1"
    
    	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    	"k8s.io/apimachinery/pkg/util/wait"
    
    Matthew Wong's avatar
    Matthew Wong committed
    	"k8s.io/client-go/kubernetes"
    	"k8s.io/client-go/rest"
    )
    
    const (
    	provisionerNameKey = "PROVISIONER_NAME"
    
    Matthew Wong's avatar
    Matthew Wong committed
    	fileSystemIDKey    = "FILE_SYSTEM_ID"
    
    Matthew Wong's avatar
    Matthew Wong committed
    	awsRegionKey       = "AWS_REGION"
    
    	dnsNameKey         = "DNS_NAME"
    
    Matthew Wong's avatar
    Matthew Wong committed
    )
    
    type efsProvisioner struct {
    	dnsName    string
    	mountpoint string
    	source     string
    	allocator  gidallocator.Allocator
    }
    
    
    Matthew Wong's avatar
    Matthew Wong committed
    // NewEFSProvisioner creates an AWS EFS volume provisioner
    
    Matthew Wong's avatar
    Matthew Wong committed
    func NewEFSProvisioner(client kubernetes.Interface) controller.Provisioner {
    
    Matthew Wong's avatar
    Matthew Wong committed
    	fileSystemID := os.Getenv(fileSystemIDKey)
    	if fileSystemID == "" {
    		glog.Fatalf("environment variable %s is not set! Please set it.", fileSystemIDKey)
    
    Matthew Wong's avatar
    Matthew Wong committed
    	}
    
    	awsRegion := os.Getenv(awsRegionKey)
    	if awsRegion == "" {
    
    Matthew Wong's avatar
    Matthew Wong committed
    		glog.Fatalf("environment variable %s is not set! Please set it.", awsRegionKey)
    
    	dnsName := os.Getenv(dnsNameKey)
    	glog.Errorf("%v", dnsName)
    	if dnsName == "" {
    		dnsName = getDNSName(fileSystemID, awsRegion)
    	}
    
    Matthew Wong's avatar
    Matthew Wong committed
    
    	mountpoint, source, err := getMount(dnsName)
    	if err != nil {
    		glog.Fatal(err)
    	}
    
    	sess, err := session.NewSession()
    	if err != nil {
    
    		glog.Warningf("couldn't create an AWS session: %v", err)
    
    Matthew Wong's avatar
    Matthew Wong committed
    	}
    
    	svc := efs.New(sess, &aws.Config{Region: aws.String(awsRegion)})
    	params := &efs.DescribeFileSystemsInput{
    
    Matthew Wong's avatar
    Matthew Wong committed
    		FileSystemId: aws.String(fileSystemID),
    
    Matthew Wong's avatar
    Matthew Wong committed
    	}
    
    	_, err = svc.DescribeFileSystems(params)
    	if err != nil {
    
    		glog.Warningf("couldn't confirm that the EFS file system exists: %v", err)
    
    Matthew Wong's avatar
    Matthew Wong committed
    	}
    
    	return &efsProvisioner{
    		dnsName:    dnsName,
    		mountpoint: mountpoint,
    		source:     source,
    		allocator:  gidallocator.New(client),
    	}
    }
    
    
    Matthew Wong's avatar
    Matthew Wong committed
    func getDNSName(fileSystemID, awsRegion string) string {
    	return fileSystemID + ".efs." + awsRegion + ".amazonaws.com"
    
    Matthew Wong's avatar
    Matthew Wong committed
    }
    
    func getMount(dnsName string) (string, string, error) {
    	entries, err := mount.GetMounts()
    	if err != nil {
    		return "", "", err
    	}
    	for _, e := range entries {
    		if strings.HasPrefix(e.Source, dnsName) {
    			return e.Mountpoint, e.Source, nil
    		}
    	}
    
    
    	entriesStr := ""
    	for _, e := range entries {
    		entriesStr += e.Source + ":" + e.Mountpoint + ", "
    	}
    	return "", "", fmt.Errorf("no mount entry found for %s among entries %s", dnsName, entriesStr)
    
    Matthew Wong's avatar
    Matthew Wong committed
    }
    
    var _ controller.Provisioner = &efsProvisioner{}
    
    // Provision creates a storage asset and returns a PV object representing it.
    func (p *efsProvisioner) Provision(options controller.VolumeOptions) (*v1.PersistentVolume, error) {
    	if options.PVC.Spec.Selector != nil {
    		return nil, fmt.Errorf("claim.Spec.Selector is not supported")
    	}
    
    
    	gidAllocate := true
    	for k, v := range options.Parameters {
    		switch strings.ToLower(k) {
    		case "gidmin":
    		// Let allocator handle
    		case "gidmax":
    		// Let allocator handle
    		case "gidallocate":
    
    Matthew Wong's avatar
    Matthew Wong committed
    			b, err := strconv.ParseBool(v)
    
    			if err != nil {
    				return nil, fmt.Errorf("invalid value %s for parameter %s: %v", v, k, err)
    			}
    
    Matthew Wong's avatar
    Matthew Wong committed
    			gidAllocate = b
    
    		}
    	}
    
    	var gid *int
    	if gidAllocate {
    		allocate, err := p.allocator.AllocateNext(options)
    		if err != nil {
    			return nil, err
    		}
    		gid = &allocate
    
    Matthew Wong's avatar
    Matthew Wong committed
    	err := p.createVolume(p.getLocalPath(options), gid)
    
    Matthew Wong's avatar
    Matthew Wong committed
    	if err != nil {
    		return nil, err
    	}
    
    
    	mountOptions := []string{"vers=4.1"}
    	if options.MountOptions != nil {
    		mountOptions = options.MountOptions
    	}
    
    
    Matthew Wong's avatar
    Matthew Wong committed
    	pv := &v1.PersistentVolume{
    
    		ObjectMeta: metav1.ObjectMeta{
    
    Matthew Wong's avatar
    Matthew Wong committed
    			Name: options.PVName,
    		},
    		Spec: v1.PersistentVolumeSpec{
    			PersistentVolumeReclaimPolicy: options.PersistentVolumeReclaimPolicy,
    			AccessModes:                   options.PVC.Spec.AccessModes,
    			Capacity: v1.ResourceList{
    				v1.ResourceName(v1.ResourceStorage): options.PVC.Spec.Resources.Requests[v1.ResourceName(v1.ResourceStorage)],
    			},
    			PersistentVolumeSource: v1.PersistentVolumeSource{
    				NFS: &v1.NFSVolumeSource{
    					Server:   p.dnsName,
    					Path:     p.getRemotePath(options),
    					ReadOnly: false,
    				},
    			},
    
    			MountOptions: mountOptions,
    
    Matthew Wong's avatar
    Matthew Wong committed
    		},
    	}
    
    	if gidAllocate {
    		pv.ObjectMeta.Annotations = map[string]string{
    			gidallocator.VolumeGidAnnotationKey: strconv.FormatInt(int64(*gid), 10),
    		}
    	}
    
    Matthew Wong's avatar
    Matthew Wong committed
    
    	return pv, nil
    }
    
    
    func (p *efsProvisioner) createVolume(path string, gid *int) error {
    	perm := os.FileMode(0777)
    	if gid != nil {
    		perm = os.FileMode(0771 | os.ModeSetgid)
    	}
    
    Matthew Wong's avatar
    Matthew Wong committed
    
    	if err := os.MkdirAll(path, perm); err != nil {
    		return err
    	}
    
    	// Due to umask, need to chmod
    
    Matthew Wong's avatar
    Matthew Wong committed
    	if err := os.Chmod(path, perm); err != nil {
    
    Matthew Wong's avatar
    Matthew Wong committed
    		os.RemoveAll(path)
    
    Matthew Wong's avatar
    Matthew Wong committed
    		return err
    
    	if gid != nil {
    		cmd := exec.Command("chgrp", strconv.Itoa(*gid), path)
    		out, err := cmd.CombinedOutput()
    		if err != nil {
    			os.RemoveAll(path)
    			return fmt.Errorf("chgrp failed with error: %v, output: %s", err, out)
    		}
    
    Matthew Wong's avatar
    Matthew Wong committed
    	}
    
    	return nil
    }
    
    func (p *efsProvisioner) getLocalPath(options controller.VolumeOptions) string {
    	return path.Join(p.mountpoint, p.getDirectoryName(options))
    }
    
    func (p *efsProvisioner) getRemotePath(options controller.VolumeOptions) string {
    	sourcePath := path.Clean(strings.Replace(p.source, p.dnsName+":", "", 1))
    	return path.Join(sourcePath, p.getDirectoryName(options))
    }
    
    func (p *efsProvisioner) getDirectoryName(options controller.VolumeOptions) string {
    	return options.PVC.Name + "-" + options.PVName
    }
    
    // Delete removes the storage asset that was created by Provision represented
    // by the given PV.
    func (p *efsProvisioner) Delete(volume *v1.PersistentVolume) error {
    	//TODO ignorederror
    	err := p.allocator.Release(volume)
    	if err != nil {
    		return err
    	}
    
    	path, err := p.getLocalPathToDelete(volume.Spec.NFS)
    	if err != nil {
    		return err
    	}
    
    	if err := os.RemoveAll(path); err != nil {
    		return err
    	}
    
    	return nil
    }
    
    func (p *efsProvisioner) getLocalPathToDelete(nfs *v1.NFSVolumeSource) (string, error) {
    	if nfs.Server != p.dnsName {
    		return "", fmt.Errorf("volume's NFS server %s is not equal to the server %s from which this provisioner creates volumes", nfs.Server, p.dnsName)
    	}
    
    	sourcePath := path.Clean(strings.Replace(p.source, p.dnsName+":", "", 1))
    	if !strings.HasPrefix(nfs.Path, sourcePath) {
    		return "", fmt.Errorf("volume's NFS path %s is not a child of the server path %s mounted in this provisioner at %s", nfs.Path, p.source, p.mountpoint)
    	}
    
    	subpath := strings.Replace(nfs.Path, sourcePath, "", 1)
    
    	return path.Join(p.mountpoint, subpath), nil
    }
    
    func main() {
    	flag.Parse()
    	flag.Set("logtostderr", "true")
    
    	// Create an InClusterConfig and use it to create a client for the controller
    	// to use to communicate with Kubernetes
    	config, err := rest.InClusterConfig()
    	if err != nil {
    		glog.Fatalf("Failed to create config: %v", err)
    	}
    	clientset, err := kubernetes.NewForConfig(config)
    	if err != nil {
    		glog.Fatalf("Failed to create client: %v", err)
    	}
    
    	// The controller needs to know what the server version is because out-of-tree
    	// provisioners aren't officially supported until 1.5
    	serverVersion, err := clientset.Discovery().ServerVersion()
    	if err != nil {
    		glog.Fatalf("Error getting server version: %v", err)
    	}
    
    	// Create the provisioner: it implements the Provisioner interface expected by
    	// the controller
    	efsProvisioner := NewEFSProvisioner(clientset)
    
    	provisionerName := os.Getenv(provisionerNameKey)
    	if provisionerName == "" {
    
    Matthew Wong's avatar
    Matthew Wong committed
    		glog.Fatalf("environment variable %s is not set! Please set it.", provisionerNameKey)
    
    Matthew Wong's avatar
    Matthew Wong committed
    	}
    
    	// Start the provision controller which will dynamically provision efs NFS
    	// PVs
    
    	pc := controller.NewProvisionController(
    		clientset,
    		provisionerName,
    		efsProvisioner,
    		serverVersion.GitVersion,
    	)
    
    
    Matthew Wong's avatar
    Matthew Wong committed
    	pc.Run(wait.NeverStop)
    }