Skip to content
Snippets Groups Projects
create_handler.go 7.68 KiB
Newer Older
  • Learn to ignore specific revisions
  • package handlers
    
    import (
    	"context"
    
    	"encoding/base64"
    
    	"encoding/json"
    
    	"fmt"
    
    	"io/ioutil"
    
    	"log"
    
    	"net/http"
    
    Alex Ellis's avatar
    Alex Ellis committed
    	"strconv"
    
    	"github.com/docker/distribution/reference"
    
    	"github.com/docker/docker/api/types"
    
    	"github.com/docker/docker/api/types/filters"
    
    	"github.com/docker/docker/api/types/swarm"
    
    	"github.com/docker/docker/client"
    
    	"github.com/docker/docker/registry"
    
    Alex Ellis's avatar
    Alex Ellis committed
    	units "github.com/docker/go-units"
    
    Alex Ellis's avatar
    Alex Ellis committed
    	"github.com/openfaas/faas/gateway/metrics"
    	"github.com/openfaas/faas/gateway/requests"
    
    var linuxOnlyConstraints = []string{"node.platform.os == linux"}
    
    
    Alex's avatar
    Alex committed
    // MakeNewFunctionHandler creates a new function (service) inside the swarm network.
    
    func MakeNewFunctionHandler(metricsOptions metrics.MetricOptions, c *client.Client, maxRestarts uint64, restartDelay time.Duration) http.HandlerFunc {
    
    Alex's avatar
    Alex committed
    	return func(w http.ResponseWriter, r *http.Request) {
    		defer r.Body.Close()
    
    		body, _ := ioutil.ReadAll(r.Body)
    
    Alex's avatar
    Alex committed
    
    		request := requests.CreateFunctionRequest{}
    		err := json.Unmarshal(body, &request)
    		if err != nil {
    
    			log.Println("Error parsing request:", err)
    
    Alex's avatar
    Alex committed
    			w.WriteHeader(http.StatusBadRequest)
    			return
    		}
    
    
    		options := types.ServiceCreateOptions{}
    
    		if len(request.RegistryAuth) > 0 {
    			auth, err := BuildEncodedAuthConfig(request.RegistryAuth, request.Image)
    			if err != nil {
    
    				log.Println("Error building registry auth configuration:", err)
    
    				w.WriteHeader(http.StatusBadRequest)
    				w.Write([]byte("Invalid registry auth"))
    				return
    			}
    			options.EncodedRegistryAuth = auth
    		}
    
    Alex Ellis's avatar
    Alex Ellis committed
    
    
    		spec, err := makeSpec(c, &request, maxRestarts, restartDelay)
    		if err != nil {
    			log.Println(err)
    			w.WriteHeader(http.StatusBadRequest)
    			w.Write([]byte("Deployment error: " + err.Error()))
    			return
    		}
    
    
    		response, err := c.ServiceCreate(context.Background(), spec, options)
    		if err != nil {
    
    			log.Println("Error creating service:", err)
    
    			w.WriteHeader(http.StatusBadRequest)
    			w.Write([]byte("Deployment error: " + err.Error()))
    			return
    
    		}
    		log.Println(response.ID, response.Warnings)
    	}
    }
    
    
    func makeSpec(c *client.Client, request *requests.CreateFunctionRequest, maxRestarts uint64, restartDelay time.Duration) (swarm.ServiceSpec, error) {
    	linuxOnlyConstraints := []string{"node.platform.os == linux"}
    
    Alex Ellis's avatar
    Alex Ellis committed
    	constraints := []string{}
    
    Alex Ellis's avatar
    Alex Ellis committed
    
    
    Alex Ellis's avatar
    Alex Ellis committed
    	if request.Constraints != nil && len(request.Constraints) > 0 {
    		constraints = request.Constraints
    	} else {
    		constraints = linuxOnlyConstraints
    	}
    
    
    	labels := map[string]string{
    		"com.openfaas.function": request.Service,
    		"function":              "true", // backwards-compatible
    	}
    
    
    Alex Ellis's avatar
    Alex Ellis committed
    	if request.Labels != nil {
    
    		for k, v := range *request.Labels {
    
    Alex Ellis's avatar
    Alex Ellis committed
    			labels[k] = v
    		}
    	}
    
    Alex Ellis's avatar
    Alex Ellis committed
    
    	resources := buildResources(request)
    
    
    	nets := []swarm.NetworkAttachmentConfig{
    
    		{
    			Target: request.Network,
    		},
    
    	secrets, err := makeSecretsArray(c, request.Secrets)
    	if err != nil {
    		return swarm.ServiceSpec{}, err
    	}
    
    
    	spec := swarm.ServiceSpec{
    
    		Annotations: swarm.Annotations{
    			Name:   request.Service,
    			Labels: labels,
    		},
    
    		TaskTemplate: swarm.TaskSpec{
    			RestartPolicy: &swarm.RestartPolicy{
    
    				MaxAttempts: &maxRestarts,
    				Condition:   swarm.RestartPolicyConditionAny,
    				Delay:       &restartDelay,
    
    			},
    			ContainerSpec: swarm.ContainerSpec{
    
    				Image:   request.Image,
    				Labels:  labels,
    				Secrets: secrets,
    
    Alex Ellis's avatar
    Alex Ellis committed
    			Networks:  nets,
    			Resources: resources,
    
    			Placement: &swarm.Placement{
    
    Alex Ellis's avatar
    Alex Ellis committed
    				Constraints: constraints,
    
    Alex Ellis's avatar
    Alex Ellis committed
    		Mode: swarm.ServiceMode{
    			Replicated: &swarm.ReplicatedService{
    				Replicas: getMinReplicas(request),
    			},
    		},
    
    
    	// TODO: request.EnvProcess should only be set if it's not nil, otherwise we override anything in the Docker image already
    
    	env := buildEnv(request.EnvProcess, request.EnvVars)
    
    	if len(env) > 0 {
    		spec.TaskTemplate.ContainerSpec.Env = env
    	}
    
    
    	return spec, nil
    
    func buildEnv(envProcess string, envVars map[string]string) []string {
    	var env []string
    	if len(envProcess) > 0 {
    		env = append(env, fmt.Sprintf("fprocess=%s", envProcess))
    	}
    
    	for k, v := range envVars {
    		env = append(env, fmt.Sprintf("%s=%s", k, v))
    	}
    	return env
    }
    
    
    Alex Ellis's avatar
    Alex Ellis committed
    // BuildEncodedAuthConfig for private registry
    
    func BuildEncodedAuthConfig(basicAuthB64 string, dockerImage string) (string, error) {
    	// extract registry server address
    	distributionRef, err := reference.ParseNormalizedNamed(dockerImage)
    	if err != nil {
    		return "", err
    	}
    	repoInfo, err := registry.ParseRepositoryInfo(distributionRef)
    	if err != nil {
    		return "", err
    	}
    	// extract registry user & password
    	user, password, err := userPasswordFromBasicAuth(basicAuthB64)
    	if err != nil {
    		return "", err
    	}
    	// build encoded registry auth config
    	buf, err := json.Marshal(types.AuthConfig{
    		Username:      user,
    		Password:      password,
    		ServerAddress: repoInfo.Index.Name,
    	})
    	if err != nil {
    		return "", err
    	}
    	return base64.URLEncoding.EncodeToString(buf), nil
    }
    
    func userPasswordFromBasicAuth(basicAuthB64 string) (string, string, error) {
    	c, err := base64.StdEncoding.DecodeString(basicAuthB64)
    	if err != nil {
    		return "", "", err
    	}
    	cs := string(c)
    	s := strings.IndexByte(cs, ':')
    	if s < 0 {
    		return "", "", errors.New("Invalid basic auth")
    	}
    	return cs[:s], cs[s+1:], nil
    }
    
    Alex Ellis's avatar
    Alex Ellis committed
    
    func ParseMemory(value string) (int64, error) {
    	return units.RAMInBytes(value)
    }
    
    func buildResources(request *requests.CreateFunctionRequest) *swarm.ResourceRequirements {
    	var resources *swarm.ResourceRequirements
    
    	if request.Requests != nil || request.Limits != nil {
    
    		resources = &swarm.ResourceRequirements{}
    		if request.Limits != nil {
    			memoryBytes, err := ParseMemory(request.Limits.Memory)
    			if err != nil {
    				log.Printf("Error parsing memory limit: %T", err)
    			}
    
    			resources.Limits = &swarm.Resources{
    				MemoryBytes: memoryBytes,
    			}
    		}
    
    		if request.Requests != nil {
    			memoryBytes, err := ParseMemory(request.Requests.Memory)
    			if err != nil {
    				log.Printf("Error parsing memory request: %T", err)
    			}
    
    			resources.Reservations = &swarm.Resources{
    				MemoryBytes: memoryBytes,
    			}
    
    		}
    	}
    	return resources
    }
    
    Alex Ellis's avatar
    Alex Ellis committed
    
    func getMinReplicas(request *requests.CreateFunctionRequest) *uint64 {
    	replicas := uint64(1)
    
    	if request.Labels != nil {
    		if val, exists := (*request.Labels)["com.openfaas.scale.min"]; exists {
    			value, err := strconv.Atoi(val)
    			if err != nil {
    				log.Println(err)
    			}
    			replicas = uint64(value)
    		}
    	}
    	return &replicas
    }
    
    
    func makeSecretsArray(c *client.Client, secretNames []string) ([]*swarm.SecretReference, error) {
    	values := []*swarm.SecretReference{}
    
    	if len(secretNames) == 0 {
    		return values, nil
    	}
    
    	requestedSecrets := make(map[string]bool)
    	ctx := context.Background()
    
    	// query the Swarm for the requested secret ids, these are required to complete
    	// the spec
    	args := filters.NewArgs()
    	for _, name := range secretNames {
    		args.Add("name", name)
    	}
    
    	secrets, err := c.SecretList(ctx, types.SecretListOptions{
    		Filters: args,
    	})
    	if err != nil {
    		return nil, err
    	}
    
    	// create map of matching secrets for easy lookup
    	foundSecrets := make(map[string]string)
    	for _, secret := range secrets {
    		foundSecrets[secret.Spec.Annotations.Name] = secret.ID
    	}
    
    	// mimics the simple syntax for `docker service create --secret foo`
    	// and the code is based on the docker cli
    	for _, secretName := range secretNames {
    		if _, exists := requestedSecrets[secretName]; exists {
    			return nil, fmt.Errorf("duplicate secret target for %s not allowed", secretName)
    		}
    
    		id, ok := foundSecrets[secretName]
    		if !ok {
    			return nil, fmt.Errorf("secret not found: %s", secretName)
    		}
    
    		options := &swarm.SecretReference{
    			File: &swarm.SecretReferenceFileTarget{
    				UID:  "0",
    				GID:  "0",
    				Mode: 0444,
    				Name: secretName,
    			},
    			SecretID:   id,
    			SecretName: secretName,
    		}
    
    		requestedSecrets[secretName] = true
    		values = append(values, options)
    	}
    
    	return values, nil
    }