mirror of
https://github.com/openfaas/faasd.git
synced 2025-06-08 16:06:47 +00:00
203 lines
5.0 KiB
Go
203 lines
5.0 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
|
|
cninetwork "github.com/openfaas/faasd/pkg/cninetwork"
|
|
"github.com/openfaas/faasd/pkg/service"
|
|
"github.com/containerd/containerd"
|
|
"github.com/containerd/containerd/cio"
|
|
"github.com/containerd/containerd/namespaces"
|
|
"github.com/containerd/containerd/oci"
|
|
gocni "github.com/containerd/go-cni"
|
|
"github.com/opencontainers/runtime-spec/specs-go"
|
|
"github.com/openfaas/faas-provider/types"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
func MakeDeployHandler(client *containerd.Client, cni gocni.CNI, secretMountPath string) func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
if r.Body == nil {
|
|
http.Error(w, "expected a body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
defer r.Body.Close()
|
|
|
|
body, _ := ioutil.ReadAll(r.Body)
|
|
log.Printf("[Deploy] request: %s\n", string(body))
|
|
|
|
req := types.FunctionDeployment{}
|
|
err := json.Unmarshal(body, &req)
|
|
if err != nil {
|
|
log.Printf("[Deploy] - error parsing input: %s\n", err)
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
|
|
return
|
|
}
|
|
|
|
err = validateSecrets(secretMountPath, req.Secrets)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
}
|
|
|
|
name := req.Service
|
|
ctx := namespaces.WithNamespace(context.Background(), FunctionNamespace)
|
|
|
|
deployErr := deploy(ctx, req, client, cni, secretMountPath)
|
|
if deployErr != nil {
|
|
log.Printf("[Deploy] error deploying %s, error: %s\n", name, deployErr)
|
|
http.Error(w, deployErr.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func deploy(ctx context.Context, req types.FunctionDeployment, client *containerd.Client, cni gocni.CNI, secretMountPath string) error {
|
|
|
|
imgRef := "docker.io/" + req.Image
|
|
if strings.Index(req.Image, ":") == -1 {
|
|
imgRef = imgRef + ":latest"
|
|
}
|
|
|
|
snapshotter := ""
|
|
if val, ok := os.LookupEnv("snapshotter"); ok {
|
|
snapshotter = val
|
|
}
|
|
|
|
image, err := service.PrepareImage(ctx, client, imgRef, snapshotter)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "unable to pull image %s", imgRef)
|
|
}
|
|
|
|
size, _ := image.Size(ctx)
|
|
log.Printf("Deploy %s size: %d\n", image.Name(), size)
|
|
|
|
envs := prepareEnv(req.EnvProcess, req.EnvVars)
|
|
mounts := getMounts()
|
|
|
|
for _, secret := range req.Secrets {
|
|
mounts = append(mounts, specs.Mount{
|
|
Destination: path.Join("/var/openfaas/secrets", secret),
|
|
Type: "bind",
|
|
Source: path.Join(secretMountPath, secret),
|
|
Options: []string{"rbind", "ro"},
|
|
})
|
|
}
|
|
|
|
name := req.Service
|
|
|
|
container, err := client.NewContainer(
|
|
ctx,
|
|
name,
|
|
containerd.WithImage(image),
|
|
containerd.WithSnapshotter(snapshotter),
|
|
containerd.WithNewSnapshot(name+"-snapshot", image),
|
|
containerd.WithNewSpec(oci.WithImageConfig(image),
|
|
oci.WithCapabilities([]string{"CAP_NET_RAW"}),
|
|
oci.WithMounts(mounts),
|
|
oci.WithEnv(envs)),
|
|
)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("unable to create container: %s, error: %s", name, err)
|
|
}
|
|
|
|
return createTask(ctx, client, container, cni)
|
|
|
|
}
|
|
|
|
func createTask(ctx context.Context, client *containerd.Client, container containerd.Container, cni gocni.CNI) error {
|
|
|
|
name := container.ID()
|
|
task, taskErr := container.NewTask(ctx, cio.NewCreator(cio.WithStdio))
|
|
if taskErr != nil {
|
|
return fmt.Errorf("unable to start task: %s, error: %s", name, taskErr)
|
|
}
|
|
|
|
log.Printf("Container ID: %s\tTask ID %s:\tTask PID: %d\t\n", name, task.ID(), task.Pid())
|
|
|
|
labels := map[string]string{}
|
|
network, err := cninetwork.CreateCNINetwork(ctx, cni, task, labels)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ip, err := cninetwork.GetIPAddress(network, task)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
log.Printf("%s has IP: %s.\n", name, ip.String())
|
|
|
|
_, waitErr := task.Wait(ctx)
|
|
if waitErr != nil {
|
|
return errors.Wrapf(waitErr, "Unable to wait for task to start: %s", name)
|
|
}
|
|
|
|
if startErr := task.Start(ctx); startErr != nil {
|
|
return errors.Wrapf(startErr, "Unable to start task: %s", name)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func prepareEnv(envProcess string, reqEnvVars map[string]string) []string {
|
|
envs := []string{}
|
|
fprocessFound := false
|
|
fprocess := "fprocess=" + envProcess
|
|
if len(envProcess) > 0 {
|
|
fprocessFound = true
|
|
}
|
|
|
|
for k, v := range reqEnvVars {
|
|
if k == "fprocess" {
|
|
fprocessFound = true
|
|
fprocess = v
|
|
} else {
|
|
envs = append(envs, k+"="+v)
|
|
}
|
|
}
|
|
if fprocessFound {
|
|
envs = append(envs, fprocess)
|
|
}
|
|
return envs
|
|
}
|
|
|
|
func getMounts() []specs.Mount {
|
|
wd, _ := os.Getwd()
|
|
mounts := []specs.Mount{}
|
|
mounts = append(mounts, specs.Mount{
|
|
Destination: "/etc/resolv.conf",
|
|
Type: "bind",
|
|
Source: path.Join(wd, "resolv.conf"),
|
|
Options: []string{"rbind", "ro"},
|
|
})
|
|
|
|
mounts = append(mounts, specs.Mount{
|
|
Destination: "/etc/hosts",
|
|
Type: "bind",
|
|
Source: path.Join(wd, "hosts"),
|
|
Options: []string{"rbind", "ro"},
|
|
})
|
|
return mounts
|
|
}
|
|
|
|
func validateSecrets(secretMountPath string, secrets []string) error {
|
|
for _, secret := range secrets {
|
|
if _, err := os.Stat(path.Join(secretMountPath, secret)); err != nil {
|
|
return fmt.Errorf("unable to find secret: %s", secret)
|
|
}
|
|
}
|
|
return nil
|
|
}
|