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 }