functions

The Fool guy's FAAS
git clone git://git.lair.cx/functions
Log | Files | Refs | README

cmd_function.go (6014B)


      1 package main
      2 
      3 import (
      4 	"context"
      5 	"errors"
      6 	"fmt"
      7 	"log/slog"
      8 	"net/http"
      9 	"os"
     10 	"path"
     11 
     12 	"github.com/firecracker-microvm/firecracker-go-sdk"
     13 	"github.com/valyala/bytebufferpool"
     14 	"go.lair.cx/functions/configs"
     15 	"go.lair.cx/functions/internal/functions"
     16 	"go.lair.cx/functions/internal/vmutils"
     17 )
     18 
     19 const (
     20 	DefaultRootfsName = "rootfs.ext4"
     21 )
     22 
     23 type FunctionCmd struct {
     24 	List   FunctionListCmd   `cmd:"" help:"List functions."`
     25 	Create FunctionCreateCmd `cmd:"" help:"Create a function."`
     26 	Run    FunctionRunCmd    `cmd:"" help:"Run a function."`
     27 }
     28 
     29 type FunctionListCmd struct {
     30 	Namespace string `short:"N" arg:"" help:"Namespace of the function."`
     31 }
     32 
     33 func (c *FunctionListCmd) Run() error {
     34 	namespacePath := path.Join(configs.FuncDir, c.Namespace)
     35 
     36 	stat, err := os.Stat(namespacePath)
     37 	if err != nil {
     38 		return err
     39 	}
     40 
     41 	if !stat.IsDir() {
     42 		return errors.New("namespace is not a directory")
     43 	}
     44 
     45 	files, err := os.ReadDir(namespacePath)
     46 	if err != nil {
     47 		return err
     48 	}
     49 
     50 	for _, file := range files {
     51 		if !checkFilesInDir(
     52 			path.Join(namespacePath, file.Name()),
     53 			"function.json",
     54 		) {
     55 			continue
     56 		}
     57 
     58 		fmt.Println(file.Name())
     59 	}
     60 
     61 	return nil
     62 }
     63 
     64 type FunctionCreateCmd struct {
     65 	Namespace string `short:"N" arg:"" help:"Namespace of the function."`
     66 	Name      string `short:"n" arg:"" help:"Name of the function."`
     67 
     68 	Kernel string `short:"k" required:"" help:"File path for kernal image."`
     69 	Rootfs string `short:"r" help:"File path for rootfs image. Can be omitted if --source is provided."`
     70 	Source string `short:"s" help:"File path for source code. Can be omitted if --rootfs is provided."`
     71 	NoCopy bool   `help:"Instead of copying the file to the function folder, use that path as is."`
     72 }
     73 
     74 func (c *FunctionCreateCmd) Run() error {
     75 	namespacePath := path.Join(configs.FuncDir, c.Namespace)
     76 
     77 	stat, err := os.Stat(namespacePath)
     78 	if err != nil {
     79 		return err
     80 	}
     81 
     82 	if !stat.IsDir() {
     83 		return errors.New("namespace is not a directory")
     84 	}
     85 
     86 	functionPath := path.Join(namespacePath, c.Name)
     87 
     88 	if _, err := os.Stat(functionPath); err == nil {
     89 		return errors.New("function already exists")
     90 	}
     91 
     92 	err = os.Mkdir(functionPath, 0o755)
     93 	if err != nil {
     94 		return err
     95 	}
     96 
     97 	// --source or --rootfs must be provided.
     98 	if len(c.Source) == 0 && len(c.Rootfs) == 0 {
     99 		return errors.New("either --source or --rootfs must be provided")
    100 	}
    101 
    102 	// When --rootfs is not provided, we use the default name.
    103 	if len(c.Rootfs) == 0 {
    104 		c.Rootfs = path.Join(functionPath, DefaultRootfsName)
    105 	}
    106 
    107 	// Trying to build image from source, if provided.
    108 	if len(c.Source) > 0 {
    109 		if _, err := os.Stat(c.Rootfs); err == nil {
    110 			return errors.New("rootfs already exists")
    111 		}
    112 
    113 		err = functions.BuildRootfsFromSource(c.Source, c.Rootfs)
    114 		if err != nil {
    115 			return err
    116 		}
    117 	}
    118 
    119 	// When no-copy mode, the kernel and rootfs path must be absolute.
    120 	if c.NoCopy {
    121 		if !path.IsAbs(c.Kernel) {
    122 			return errors.New("kernel path must be absolute")
    123 		}
    124 
    125 		if !path.IsAbs(c.Rootfs) {
    126 			return errors.New("rootfs path must be absolute")
    127 		}
    128 	} else {
    129 		/*
    130 			kernelPath, err = copyFileToDir(c.Kernel, functionPath)
    131 			if err != nil {
    132 				return err
    133 			}
    134 
    135 			if len(c.Rootfs) > 0 {
    136 				rootfsPath, err = copyFileToDir(c.Rootfs, functionPath)
    137 				if err != nil {
    138 					return err
    139 				}
    140 			}
    141 		*/
    142 	}
    143 
    144 	return nil
    145 }
    146 
    147 type FunctionRunCmd struct {
    148 	Namespace string `short:"N" arg:"" help:"Namespace of the function."`
    149 	Name      string `short:"n" arg:"" help:"Name of the function."`
    150 	Kernel    string `short:"k" required:"" help:"File path for kernal image."`
    151 	Network   string `short:"n" help:"CNI network to use."`
    152 	Vsock     string `short:"V" required:"" help:"Path to vsock file."`
    153 
    154 	Attach  bool `short:"a" help:"Attach to the function vm."`
    155 	Verbose bool `short:"v" help:"Enable verbose output."`
    156 
    157 	Headers []string `name:"header" short:"H" help:"Extra headers to include in request."`
    158 	Data    string   `short:"d" help:"Data to pass to the function."`
    159 }
    160 
    161 func (c *FunctionRunCmd) Run() error {
    162 	ctx, cancel := context.WithCancel(context.Background())
    163 	defer cancel()
    164 
    165 	machine, err := c.buildMachine()
    166 	if err != nil {
    167 		return fmt.Errorf("failed to build machine: %w", err)
    168 	}
    169 
    170 	err = machine.Start(ctx)
    171 	if err != nil {
    172 		return fmt.Errorf("failed to start machine: %w", err)
    173 	}
    174 
    175 	defer c.cleanup(machine)
    176 
    177 	client, err := vmutils.NewHTTPClient(machine)
    178 	if err != nil {
    179 		return fmt.Errorf("failed to create http client: %w", err)
    180 	}
    181 
    182 	// TODO: Setup network if provided.
    183 	if len(c.Network) > 0 {
    184 		ip, gw, err := vmutils.GetNetworkInfo(machine)
    185 		if err != nil {
    186 			slog.Error("failed to get network info", "error", err)
    187 		} else {
    188 			slog.Info("network info", "ip", ip, "gw", gw)
    189 		}
    190 	}
    191 
    192 	buf := bytebufferpool.Get()
    193 	defer bytebufferpool.Put(buf)
    194 
    195 	err = runFunction(
    196 		client,
    197 		http.Header{},
    198 		[]byte(c.Data),
    199 		func(resp *http.Response) (err error) {
    200 			buf.ReadFrom(resp.Body)
    201 			return
    202 		},
    203 	)
    204 	if err != nil {
    205 		return err
    206 	}
    207 
    208 	fmt.Print(buf.String())
    209 
    210 	err = machine.Wait(ctx)
    211 	if err != nil {
    212 		return fmt.Errorf("vmutils: failed to wait for machine: %w", err)
    213 	}
    214 
    215 	return nil
    216 }
    217 
    218 func (c *FunctionRunCmd) buildMachine() (*firecracker.Machine, error) {
    219 	rootfsPath, err := getRootfsPath(c.Namespace, c.Name)
    220 	if err != nil {
    221 		return nil, err
    222 	}
    223 
    224 	configs := []func(*vmutils.MachineBuilder){
    225 		vmutils.WithVsock(c.Vsock),
    226 		vmutils.WithVerbose(c.Verbose),
    227 	}
    228 
    229 	if c.Attach {
    230 		configs = append(
    231 			configs,
    232 			vmutils.WithStdin(os.Stdin),
    233 			vmutils.WithStdout(os.Stdout),
    234 			vmutils.WithStderr(os.Stderr),
    235 		)
    236 	}
    237 
    238 	if c.Network != "" {
    239 		configs = append(configs, vmutils.WithNetwork(c.Network))
    240 	}
    241 
    242 	return vmutils.BuildMachine(
    243 		c.Kernel,
    244 		rootfsPath,
    245 		configs...,
    246 	)
    247 }
    248 
    249 func (c *FunctionRunCmd) cleanup(machine *firecracker.Machine) error {
    250 	// Stop the VMM to ensure that the VM is stopped
    251 	err := machine.StopVMM()
    252 	if err != nil {
    253 		return fmt.Errorf("failed to stop VMM: %w", err)
    254 	}
    255 
    256 	os.Remove(machine.Cfg.SocketPath)
    257 
    258 	for _, vsock := range machine.Cfg.VsockDevices {
    259 		os.Remove(vsock.Path)
    260 	}
    261 
    262 	return nil
    263 }