diff --git a/api-docs/swagger.yml b/api-docs/swagger.yml index 4c18542a3c3cbc7c01e858bf07ec93d2f898d340..2f9b564bc4b16120fd81fca48b0fad15a227e6af 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 85876d25e72f05de7d6589b069f213382611de01..f5c68734dcfcbda33fd267269cd81de2c8ddb258 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 aa638e01e3d70b0a40806d8c6c7f55d083fae471..83c4e37d16dc7bcd7c18a62054aa5f96670f197d 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 d66880622861bf1e61ff1f00253afed5dd638559..5c6fb5db62b092e9aceff1b44ab236ab550b7cbb 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")