From 40e1fac1c20b76101a1f096df2f6c68c0c7a8c1e Mon Sep 17 00:00:00 2001
From: John McCabe <john@johnmccabe.net>
Date: Sat, 23 Sep 2017 23:23:55 +0100
Subject: [PATCH] Implement Swarm update handler using PUT

This commit implements an update handler for Docker Swarm, it queries the
current spec, updates values in-situ before calling ServiceUpdate.

The UpdateConfig FailureAction is set to rollback, so in the event of
supplying values to the update that would result in the service failing
then the update will be rolled back.

The UpdateConfig Parallelism param is set to an explicit value of 1 which
will result in functions being updated 1 by 1 rather than all at once.

It also moves the restartDelay declaration out of the create and update
handlers and into the main server function alongside maxRestarts.

And finally this commit uses the PUT HTTP verb for updates rather than
the non-HTTP UPDATE verb which was being used initially (also adding it
to the Swagger definition).

Signed-off-by: John McCabe <john@johnmccabe.net>
---
 api-docs/swagger.yml               |  17 +++++
 gateway/handlers/createhandler.go  |  29 ++++----
 gateway/handlers/update_handler.go | 109 ++++++++++++++++++++++++++++-
 gateway/server.go                  |   8 ++-
 4 files changed, 145 insertions(+), 18 deletions(-)

diff --git a/api-docs/swagger.yml b/api-docs/swagger.yml
index 4c18542a..2f9b564b 100644
--- a/api-docs/swagger.yml
+++ b/api-docs/swagger.yml
@@ -43,6 +43,23 @@ paths:
       responses:
         '200':
           description: OK
+    put:
+      summary: Update a function.
+      description: ''
+      consumes:
+        - application/json
+      produces:
+        - application/json
+      parameters:
+        - in: body
+          name: body
+          description: Function to update
+          required: true
+          schema:
+            $ref: '#/definitions/CreateFunctionRequest'
+      responses:
+        '200':
+          description: OK
     delete:
       summary: Remove a deployed function.
       description: ''
diff --git a/gateway/handlers/createhandler.go b/gateway/handlers/createhandler.go
index 85876d25..f5c68734 100644
--- a/gateway/handlers/createhandler.go
+++ b/gateway/handlers/createhandler.go
@@ -21,8 +21,10 @@ import (
 	"github.com/docker/docker/registry"
 )
 
+var linuxOnlyConstraints = []string{"node.platform.os == linux"}
+
 // MakeNewFunctionHandler creates a new function (service) inside the swarm network.
-func MakeNewFunctionHandler(metricsOptions metrics.MetricOptions, c *client.Client, maxRestarts uint64) http.HandlerFunc {
+func MakeNewFunctionHandler(metricsOptions metrics.MetricOptions, c *client.Client, maxRestarts uint64, restartDelay time.Duration) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		defer r.Body.Close()
 		body, _ := ioutil.ReadAll(r.Body)
@@ -51,7 +53,7 @@ func MakeNewFunctionHandler(metricsOptions metrics.MetricOptions, c *client.Clie
 			}
 			options.EncodedRegistryAuth = auth
 		}
-		spec := makeSpec(&request, maxRestarts)
+		spec := makeSpec(&request, maxRestarts, restartDelay)
 
 		response, err := c.ServiceCreate(context.Background(), spec, options)
 		if err != nil {
@@ -64,8 +66,7 @@ func MakeNewFunctionHandler(metricsOptions metrics.MetricOptions, c *client.Clie
 	}
 }
 
-func makeSpec(request *requests.CreateFunctionRequest, maxRestarts uint64) swarm.ServiceSpec {
-	linuxOnlyConstraints := []string{"node.platform.os == linux"}
+func makeSpec(request *requests.CreateFunctionRequest, maxRestarts uint64, restartDelay time.Duration) swarm.ServiceSpec {
 	constraints := []string{}
 	if request.Constraints != nil && len(request.Constraints) > 0 {
 		constraints = request.Constraints
@@ -76,7 +77,6 @@ func makeSpec(request *requests.CreateFunctionRequest, maxRestarts uint64) swarm
 	nets := []swarm.NetworkAttachmentConfig{
 		{Target: request.Network},
 	}
-	restartDelay := time.Second * 5
 
 	spec := swarm.ServiceSpec{
 		TaskTemplate: swarm.TaskSpec{
@@ -100,13 +100,7 @@ func makeSpec(request *requests.CreateFunctionRequest, maxRestarts uint64) swarm
 	}
 
 	// TODO: request.EnvProcess should only be set if it's not nil, otherwise we override anything in the Docker image already
-	var env []string
-	if len(request.EnvProcess) > 0 {
-		env = append(env, fmt.Sprintf("fprocess=%s", request.EnvProcess))
-	}
-	for k, v := range request.EnvVars {
-		env = append(env, fmt.Sprintf("%s=%s", k, v))
-	}
+	env := buildEnv(request.EnvProcess, request.EnvVars)
 
 	if len(env) > 0 {
 		spec.TaskTemplate.ContainerSpec.Env = env
@@ -115,6 +109,17 @@ func makeSpec(request *requests.CreateFunctionRequest, maxRestarts uint64) swarm
 	return spec
 }
 
+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
+}
+
 // BuildEncodedAuthConfig for private registry
 func BuildEncodedAuthConfig(basicAuthB64 string, dockerImage string) (string, error) {
 	// extract registry server address
diff --git a/gateway/handlers/update_handler.go b/gateway/handlers/update_handler.go
index aa638e01..83c4e37d 100644
--- a/gateway/handlers/update_handler.go
+++ b/gateway/handlers/update_handler.go
@@ -1,16 +1,119 @@
 package handlers
 
 import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"log"
 	"net/http"
+	"time"
 
 	"github.com/alexellis/faas/gateway/metrics"
+	"github.com/alexellis/faas/gateway/requests"
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/swarm"
 	"github.com/docker/docker/client"
 )
 
-// MakeUpdateFunctionHandler request to update an existing function with new configuration such as image, parameters etc.
-func MakeUpdateFunctionHandler(metricsOptions metrics.MetricOptions, c *client.Client, maxRestarts uint64) http.HandlerFunc {
+// MakeUpdateFunctionHandler request to update an existing function with new configuration such as image, envvars etc.
+func MakeUpdateFunctionHandler(metricsOptions metrics.MetricOptions, c *client.Client, maxRestarts uint64, restartDelay time.Duration) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
+		ctx := context.Background()
+
 		defer r.Body.Close()
-		w.WriteHeader(http.StatusNotImplemented)
+		body, _ := ioutil.ReadAll(r.Body)
+
+		request := requests.CreateFunctionRequest{}
+		err := json.Unmarshal(body, &request)
+		if err != nil {
+			log.Println("Error parsing request:", err)
+			w.WriteHeader(http.StatusBadRequest)
+			w.Write([]byte(err.Error()))
+			return
+		}
+
+		serviceInspectopts := types.ServiceInspectOptions{
+			InsertDefaults: true,
+		}
+
+		service, _, err := c.ServiceInspectWithRaw(ctx, request.Service, serviceInspectopts)
+		if err != nil {
+			log.Println("Error inspecting service", err)
+			w.WriteHeader(http.StatusNotFound)
+			w.Write([]byte(err.Error()))
+			return
+		}
+
+		updateSpec(&request, &service.Spec, maxRestarts, restartDelay)
+
+		updateOpts := types.ServiceUpdateOptions{}
+		updateOpts.RegistryAuthFrom = types.RegistryAuthFromSpec
+
+		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
+			}
+			updateOpts.EncodedRegistryAuth = auth
+		}
+
+		response, err := c.ServiceUpdate(ctx, service.ID, service.Version, service.Spec, updateOpts)
+		if err != nil {
+			log.Println("Error updating service:", err)
+			w.WriteHeader(http.StatusBadRequest)
+			w.Write([]byte("Update error: " + err.Error()))
+			return
+		}
+		log.Println(response.Warnings)
+	}
+}
+
+func updateSpec(request *requests.CreateFunctionRequest, spec *swarm.ServiceSpec, maxRestarts uint64, restartDelay time.Duration) {
+
+	constraints := []string{}
+	if request.Constraints != nil && len(request.Constraints) > 0 {
+		constraints = request.Constraints
+	} else {
+		constraints = linuxOnlyConstraints
+	}
+
+	nets := []swarm.NetworkAttachmentConfig{
+		{Target: request.Network},
+	}
+
+	spec.TaskTemplate.RestartPolicy.MaxAttempts = &maxRestarts
+	spec.TaskTemplate.RestartPolicy.Condition = swarm.RestartPolicyConditionAny
+	spec.TaskTemplate.RestartPolicy.Delay = &restartDelay
+	spec.TaskTemplate.ContainerSpec.Image = request.Image
+	spec.TaskTemplate.ContainerSpec.Labels = map[string]string{
+		"function": "true",
+		"uid":      fmt.Sprintf("%d", time.Now().Nanosecond()),
+	}
+	spec.TaskTemplate.Networks = nets
+	spec.TaskTemplate.Placement = &swarm.Placement{
+		Constraints: constraints,
+	}
+
+	spec.Annotations = swarm.Annotations{
+		Name: request.Service,
+	}
+
+	spec.RollbackConfig = &swarm.UpdateConfig{
+		FailureAction: "pause",
+	}
+
+	spec.UpdateConfig = &swarm.UpdateConfig{
+		Parallelism:   1,
+		FailureAction: "rollback",
+	}
+
+	env := buildEnv(request.EnvProcess, request.EnvVars)
+
+	if len(env) > 0 {
+		spec.TaskTemplate.ContainerSpec.Env = env
 	}
 }
diff --git a/gateway/server.go b/gateway/server.go
index d6688062..5c6fb5db 100644
--- a/gateway/server.go
+++ b/gateway/server.go
@@ -92,13 +92,15 @@ func main() {
 
 		// How many times to reschedule a function.
 		maxRestarts := uint64(5)
+		// Delay between container restarts
+		restartDelay := time.Second * 5
 
 		faasHandlers.Proxy = internalHandlers.MakeProxy(metricsOptions, true, dockerClient, &logger)
 		faasHandlers.RoutelessProxy = internalHandlers.MakeProxy(metricsOptions, false, dockerClient, &logger)
 		faasHandlers.ListFunctions = internalHandlers.MakeFunctionReader(metricsOptions, dockerClient)
-		faasHandlers.DeployFunction = internalHandlers.MakeNewFunctionHandler(metricsOptions, dockerClient, maxRestarts)
+		faasHandlers.DeployFunction = internalHandlers.MakeNewFunctionHandler(metricsOptions, dockerClient, maxRestarts, restartDelay)
 		faasHandlers.DeleteFunction = internalHandlers.MakeDeleteFunctionHandler(metricsOptions, dockerClient)
-		faasHandlers.UpdateFunction = internalHandlers.MakeUpdateFunctionHandler(metricsOptions, dockerClient, maxRestarts)
+		faasHandlers.UpdateFunction = internalHandlers.MakeUpdateFunctionHandler(metricsOptions, dockerClient, maxRestarts, restartDelay)
 
 		faasHandlers.Alert = internalHandlers.MakeAlertHandler(internalHandlers.NewSwarmServiceQuery(dockerClient))
 
@@ -130,7 +132,7 @@ func main() {
 	r.HandleFunc("/system/functions", listFunctions).Methods("GET")
 	r.HandleFunc("/system/functions", faasHandlers.DeployFunction).Methods("POST")
 	r.HandleFunc("/system/functions", faasHandlers.DeleteFunction).Methods("DELETE")
-	r.HandleFunc("/system/functions", faasHandlers.UpdateFunction).Methods("UPDATE")
+	r.HandleFunc("/system/functions", faasHandlers.UpdateFunction).Methods("PUT")
 
 	if faasHandlers.QueuedProxy != nil {
 		r.HandleFunc("/async-function/{name:[-a-zA-Z_0-9]+}/", faasHandlers.QueuedProxy).Methods("POST")
-- 
GitLab