mirror of
https://github.com/openfaas/faasd.git
synced 2025-06-16 21:06:53 +00:00
The use of containerd and CNI functions has been refactored to reuse the same codebase. Added all network functionality to own directory and package. Removed netlink and weave library in favor of using CNI plugin result files. Rename containers handler to functions to clear-up functionality. Signed-off-by: Carlos de Paula <me@carlosedp.com>
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/alexellis/faasd/pkg/cninetwork"
|
|
"github.com/alexellis/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
|
|
}
|