Social cloud hosting
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add container isolation backend

- Add ContainerPool executor using Docker/Podman
- Add isolation config: auto, none, container, firecracker
- Auto-detect best available backend (KVM > Docker > none)
- Add base Dockerfile using debian-slim + Nix
- Runtime flakes are built inside container (single source of truth)
- Update build command to support --target=container

Co-Authored-By: Claude <noreply@anthropic.com>

+591 -69
+76 -41
internal/cli/build.go
··· 6 6 "os/exec" 7 7 "path/filepath" 8 8 9 + "github.com/neutrino2211/at-rund/internal/vm" 9 10 "github.com/spf13/cobra" 10 11 ) 11 12 12 13 var ( 13 14 buildRuntime string 15 + buildTarget string 14 16 ) 15 17 16 18 var buildCmd = &cobra.Command{ 17 19 Use: "build", 18 - Short: "Build Firecracker runtime images", 19 - Long: `Builds Firecracker kernel and rootfs images from Nix configurations.`, 20 + Short: "Build runtime images", 21 + Long: `Builds container or Firecracker images from Nix runtime definitions.`, 20 22 RunE: runBuild, 21 23 } 22 24 23 25 func init() { 24 26 buildCmd.Flags().StringVarP(&buildRuntime, "runtime", "r", "", "build specific runtime (default: all)") 27 + buildCmd.Flags().StringVarP(&buildTarget, "target", "t", "container", "build target: container or firecracker") 25 28 } 26 29 27 30 func runBuild(cmd *cobra.Command, args []string) error { 28 - dir := configDir() 29 - runtimesDir := filepath.Join(dir, "runtimes") 30 - imagesDir := filepath.Join(dir, "images") 31 - 32 - // Check nix is available 33 - if _, err := exec.LookPath("nix"); err != nil { 34 - return fmt.Errorf("nix not found in PATH. Install from https://nixos.org/download") 31 + // Find runtimes to build 32 + flakePath := vm.GetFlakePath() 33 + if flakePath == "" { 34 + return fmt.Errorf("could not find nix/runtimes directory") 35 35 } 36 36 37 - // Find runtimes to build 38 37 var runtimes []string 39 38 if buildRuntime != "" { 40 39 runtimes = []string{buildRuntime} 41 40 } else { 42 - entries, err := os.ReadDir(runtimesDir) 41 + entries, err := os.ReadDir(flakePath) 43 42 if err != nil { 44 43 return fmt.Errorf("failed to read runtimes directory: %w", err) 45 44 } 46 45 for _, e := range entries { 47 - if filepath.Ext(e.Name()) == ".nix" { 48 - name := e.Name()[:len(e.Name())-4] // strip .nix 49 - runtimes = append(runtimes, name) 46 + if e.IsDir() { 47 + // Check if it has a flake.nix 48 + if _, err := os.Stat(filepath.Join(flakePath, e.Name(), "flake.nix")); err == nil { 49 + runtimes = append(runtimes, e.Name()) 50 + } 50 51 } 51 52 } 52 53 } 53 54 54 55 if len(runtimes) == 0 { 55 - return fmt.Errorf("no runtimes found in %s", runtimesDir) 56 + return fmt.Errorf("no runtimes found in %s", flakePath) 57 + } 58 + 59 + fmt.Printf("Building %d runtime(s) for %s: %v\n\n", len(runtimes), buildTarget, runtimes) 60 + 61 + switch buildTarget { 62 + case "container": 63 + return buildContainerImages(runtimes, flakePath) 64 + case "firecracker": 65 + return buildFirecrackerImages(runtimes, flakePath) 66 + default: 67 + return fmt.Errorf("unknown target: %s (valid: container, firecracker)", buildTarget) 68 + } 69 + } 70 + 71 + func buildContainerImages(runtimes []string, flakePath string) error { 72 + // Check docker/podman is available 73 + containerRuntime := "docker" 74 + if _, err := exec.LookPath("docker"); err != nil { 75 + if _, err := exec.LookPath("podman"); err != nil { 76 + return fmt.Errorf("docker or podman required for container builds") 77 + } 78 + containerRuntime = "podman" 56 79 } 57 80 58 - fmt.Printf("Building %d runtime(s): %v\n\n", len(runtimes), runtimes) 81 + // Find the container Dockerfile 82 + dockerfilePath := filepath.Join(flakePath, "..", "container", "Dockerfile") 83 + if _, err := os.Stat(dockerfilePath); err != nil { 84 + return fmt.Errorf("container Dockerfile not found at %s", dockerfilePath) 85 + } 86 + contextPath := filepath.Join(flakePath, "..") 59 87 60 88 for _, runtime := range runtimes { 61 - fmt.Printf("Building %s...\n", runtime) 89 + imageName := fmt.Sprintf("at-rund-%s:latest", runtime) 90 + fmt.Printf("Building %s...\n", imageName) 62 91 63 - outputDir := filepath.Join(imagesDir, runtime) 64 - if err := os.MkdirAll(outputDir, 0755); err != nil { 65 - return fmt.Errorf("failed to create output dir: %w", err) 66 - } 92 + cmd := exec.Command(containerRuntime, "build", 93 + "--build-arg", fmt.Sprintf("RUNTIME=%s", runtime), 94 + "-t", imageName, 95 + "-f", dockerfilePath, 96 + contextPath, 97 + ) 98 + cmd.Stdout = os.Stdout 99 + cmd.Stderr = os.Stderr 67 100 68 - // TODO: Actually invoke nix build with the runtime config 69 - // For now, just stub it out 70 - if err := buildRuntimeImage(runtime, runtimesDir, outputDir); err != nil { 101 + if err := cmd.Run(); err != nil { 71 102 return fmt.Errorf("failed to build %s: %w", runtime, err) 72 103 } 73 104 74 - fmt.Printf(" ✓ Built %s -> %s\n", runtime, outputDir) 105 + fmt.Printf(" ✓ Built %s\n", imageName) 75 106 } 76 107 77 - fmt.Println("\nBuild complete.") 108 + fmt.Println("\nContainer images built successfully.") 78 109 return nil 79 110 } 80 111 81 - func buildRuntimeImage(runtime, runtimesDir, outputDir string) error { 82 - // This will invoke nix build with the appropriate flake 83 - // For now, create placeholder files to indicate structure 84 - 85 - nixFile := filepath.Join(runtimesDir, runtime+".nix") 86 - if _, err := os.Stat(nixFile); os.IsNotExist(err) { 87 - return fmt.Errorf("runtime config not found: %s", nixFile) 112 + func buildFirecrackerImages(runtimes []string, flakePath string) error { 113 + // Check nix is available 114 + if _, err := exec.LookPath("nix"); err != nil { 115 + return fmt.Errorf("nix not found in PATH") 88 116 } 89 117 90 - // TODO: Implement actual nix build 91 - // nix build .#firecracker-${runtime} -o ${outputDir} 92 - // 93 - // The flake would produce: 94 - // - kernel: Linux kernel image 95 - // - rootfs.ext4: Root filesystem with runtime + guest agent 118 + dir := configDir() 119 + imagesDir := filepath.Join(dir, "images") 120 + 121 + for _, runtime := range runtimes { 122 + fmt.Printf("Building %s for Firecracker...\n", runtime) 123 + 124 + outputDir := filepath.Join(imagesDir, runtime) 125 + if err := os.MkdirAll(outputDir, 0755); err != nil { 126 + return fmt.Errorf("failed to create output dir: %w", err) 127 + } 96 128 97 - fmt.Printf(" [stub] Would build from %s\n", nixFile) 98 - fmt.Printf(" [stub] Output: %s/kernel, %s/rootfs.ext4\n", outputDir, outputDir) 129 + // TODO: Implement actual Firecracker image build 130 + // nix build path/to/runtime#firecracker -o ${outputDir} 131 + fmt.Printf(" [stub] Firecracker build not yet implemented\n") 132 + fmt.Printf(" [stub] Would output to %s/kernel, %s/rootfs.ext4\n", outputDir, outputDir) 133 + } 99 134 100 135 return nil 101 136 }
+62 -22
internal/cli/serve.go
··· 3 3 import ( 4 4 "fmt" 5 5 "os" 6 + "os/exec" 6 7 "os/signal" 7 8 "syscall" 8 9 ··· 14 15 ) 15 16 16 17 var ( 17 - servePort int 18 - serveDev bool 19 - serveMock bool 18 + servePort int 19 + serveDev bool 20 + serveIsolation string 20 21 ) 21 22 22 23 var serveCmd = &cobra.Command{ 23 24 Use: "serve", 24 25 Short: "Start the at-rund server", 25 - Long: `Starts the HTTP server and initializes the Firecracker VM pool.`, 26 + Long: `Starts the HTTP server and initializes the execution backend.`, 26 27 RunE: runServe, 27 28 } 28 29 29 30 func init() { 30 31 serveCmd.Flags().IntVarP(&servePort, "port", "p", 0, "port to listen on (default: from config)") 31 - serveCmd.Flags().BoolVar(&serveDev, "dev", false, "enable development mode") 32 - serveCmd.Flags().BoolVar(&serveMock, "mock", false, "use mock VMs instead of Firecracker (auto-enabled if KVM unavailable)") 32 + serveCmd.Flags().BoolVar(&serveDev, "dev", false, "enable development mode (alias for --isolation=none)") 33 + serveCmd.Flags().StringVar(&serveIsolation, "isolation", "", "isolation backend: auto, none, container, firecracker") 33 34 } 34 35 35 36 func kvmAvailable() bool { 36 - // Check if /dev/kvm exists and is accessible 37 37 _, err := os.Stat("/dev/kvm") 38 38 return err == nil 39 39 } 40 40 41 + func dockerAvailable() bool { 42 + cmd := exec.Command("docker", "info") 43 + return cmd.Run() == nil 44 + } 45 + 46 + func podmanAvailable() bool { 47 + cmd := exec.Command("podman", "info") 48 + return cmd.Run() == nil 49 + } 50 + 51 + func detectIsolation() string { 52 + if kvmAvailable() { 53 + return "firecracker" 54 + } 55 + if dockerAvailable() || podmanAvailable() { 56 + return "container" 57 + } 58 + return "none" 59 + } 60 + 41 61 func runServe(cmd *cobra.Command, args []string) error { 42 62 // Load config 43 63 cfg, err := config.Load(cfgFile) ··· 50 70 cfg.Port = servePort 51 71 } 52 72 if serveDev { 53 - cfg.DevMode = true 73 + cfg.Isolation = "none" 74 + } 75 + if serveIsolation != "" { 76 + cfg.Isolation = serveIsolation 77 + } 78 + 79 + // Auto-detect isolation if set to auto or empty 80 + if cfg.Isolation == "" || cfg.Isolation == "auto" { 81 + cfg.Isolation = detectIsolation() 54 82 } 55 83 56 84 fmt.Printf("at-rund starting...\n") 57 85 fmt.Printf(" Port: %d\n", cfg.Port) 58 - fmt.Printf(" Dev mode: %v\n", cfg.DevMode) 86 + fmt.Printf(" Isolation: %s\n", cfg.Isolation) 59 87 if cfg.DID != "" { 60 88 fmt.Printf(" DID: %s\n", cfg.DID) 61 89 } 62 90 63 - // Initialize executor based on mode 91 + // Initialize executor based on isolation mode 64 92 var executor vm.Executor 93 + flakePath := vm.GetFlakePath() 65 94 66 - if cfg.DevMode || !kvmAvailable() { 67 - // Dev mode: use Nix directly (no VM isolation) 68 - if !kvmAvailable() && !cfg.DevMode { 69 - fmt.Println(" KVM not available, using Nix executor") 70 - } 71 - fmt.Println(" Executor: Nix (direct execution)") 72 - 73 - flakePath := vm.GetFlakePath() 95 + switch cfg.Isolation { 96 + case "none": 97 + fmt.Println(" Executor: Nix (direct execution, no isolation)") 74 98 executor, err = vm.NewNixPool(flakePath) 75 99 if err != nil { 76 - fmt.Println("\n Nix is required for dev mode. Install it with:") 100 + fmt.Println("\n Nix is required. Install it with:") 77 101 fmt.Println(" curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install") 78 102 fmt.Println("") 79 103 return fmt.Errorf("failed to initialize Nix executor: %w", err) 80 104 } 81 - } else { 82 - // Prod mode: use Firecracker VMs 105 + 106 + case "container": 107 + fmt.Println(" Executor: Container (namespace isolation)") 108 + executor, err = vm.NewContainerPool(flakePath) 109 + if err != nil { 110 + fmt.Println("\n Docker or Podman is required for container isolation.") 111 + fmt.Println(" Install Docker: https://docs.docker.com/engine/install/") 112 + fmt.Println(" Or use --isolation=none for dev mode") 113 + fmt.Println("") 114 + return fmt.Errorf("failed to initialize Container executor: %w", err) 115 + } 116 + 117 + case "firecracker": 83 118 fmt.Println(" Executor: Firecracker (VM isolation)") 84 - 119 + if !kvmAvailable() { 120 + return fmt.Errorf("firecracker requires KVM (/dev/kvm not accessible)") 121 + } 85 122 executor, err = vm.NewFirecrackerPool(nil) 86 123 if err != nil { 87 124 return fmt.Errorf("failed to initialize Firecracker pool: %w", err) 88 125 } 126 + 127 + default: 128 + return fmt.Errorf("unknown isolation mode: %s (valid: auto, none, container, firecracker)", cfg.Isolation) 89 129 } 90 130 defer executor.Shutdown() 91 131
+8 -6
internal/config/config.go
··· 10 10 ) 11 11 12 12 type Config struct { 13 - DID string `toml:"did"` 14 - Handle string `toml:"handle"` // For display (e.g., "alice.bsky.social") 15 - Port int `toml:"port"` 16 - DevMode bool `toml:"dev_mode"` 13 + DID string `toml:"did"` 14 + Handle string `toml:"handle"` // For display (e.g., "alice.bsky.social") 15 + Port int `toml:"port"` 16 + DevMode bool `toml:"dev_mode"` 17 + Isolation string `toml:"isolation"` // "auto", "none", "container", "firecracker" 17 18 18 19 Pool PoolConfig `toml:"pool"` 19 20 Runtimes map[string]string `toml:"runtimes"` ··· 117 118 118 119 func defaultConfig() *Config { 119 120 return &Config{ 120 - Port: 3000, 121 - DevMode: false, 121 + Port: 3000, 122 + DevMode: false, 123 + Isolation: "auto", 122 124 Pool: PoolConfig{ 123 125 PreWarm: 2, 124 126 MaxVMs: 20,
+375
internal/vm/container.go
··· 1 + package vm 2 + 3 + import ( 4 + "bytes" 5 + "encoding/json" 6 + "fmt" 7 + "os" 8 + "os/exec" 9 + "path/filepath" 10 + "strings" 11 + "time" 12 + ) 13 + 14 + // ContainerPool runs bundles in OCI containers (Docker/Podman) 15 + // Provides namespace isolation + seccomp without requiring KVM 16 + type ContainerPool struct { 17 + runtime string // "docker" or "podman" 18 + runtimesDir string 19 + runtimes map[string]ContainerRuntime 20 + } 21 + 22 + type ContainerRuntime struct { 23 + Name string 24 + MimeTypes []string 25 + Image string // OCI image name/tag 26 + } 27 + 28 + // DefaultContainerRuntimes returns built-in container runtime configurations 29 + func DefaultContainerRuntimes() map[string]ContainerRuntime { 30 + return map[string]ContainerRuntime{ 31 + "deno": { 32 + Name: "deno", 33 + MimeTypes: []string{"application/javascript+deno", "application/javascript+deno-atrun", "application/typescript+deno"}, 34 + Image: "at-rund-deno:latest", 35 + }, 36 + "node": { 37 + Name: "node", 38 + MimeTypes: []string{"application/javascript+node", "application/javascript"}, 39 + Image: "at-rund-node:latest", 40 + }, 41 + "python": { 42 + Name: "python", 43 + MimeTypes: []string{"application/python", "application/x-python"}, 44 + Image: "at-rund-python:latest", 45 + }, 46 + } 47 + } 48 + 49 + // detectContainerRuntime finds docker or podman 50 + func detectContainerRuntime() (string, error) { 51 + if path, err := exec.LookPath("docker"); err == nil { 52 + // Verify docker daemon is running 53 + cmd := exec.Command(path, "info") 54 + if err := cmd.Run(); err == nil { 55 + return "docker", nil 56 + } 57 + } 58 + 59 + if path, err := exec.LookPath("podman"); err == nil { 60 + cmd := exec.Command(path, "info") 61 + if err := cmd.Run(); err == nil { 62 + return "podman", nil 63 + } 64 + } 65 + 66 + return "", fmt.Errorf("no container runtime found (tried docker, podman)") 67 + } 68 + 69 + func NewContainerPool(runtimesDir string) (*ContainerPool, error) { 70 + runtime, err := detectContainerRuntime() 71 + if err != nil { 72 + return nil, err 73 + } 74 + 75 + return &ContainerPool{ 76 + runtime: runtime, 77 + runtimesDir: runtimesDir, 78 + runtimes: DefaultContainerRuntimes(), 79 + }, nil 80 + } 81 + 82 + func (p *ContainerPool) RuntimeForMimeType(mimeType string) (ContainerRuntime, bool) { 83 + for _, rt := range p.runtimes { 84 + for _, mt := range rt.MimeTypes { 85 + if mt == mimeType { 86 + return rt, true 87 + } 88 + } 89 + } 90 + return ContainerRuntime{}, false 91 + } 92 + 93 + func (p *ContainerPool) Execute(req ExecuteRequest, mimeType string) (*ExecuteResponse, error) { 94 + runtime, ok := p.RuntimeForMimeType(mimeType) 95 + if !ok { 96 + return nil, fmt.Errorf("no container runtime for mime type: %s", mimeType) 97 + } 98 + 99 + return p.executeInContainer(req, runtime) 100 + } 101 + 102 + func (p *ContainerPool) executeInContainer(req ExecuteRequest, runtime ContainerRuntime) (*ExecuteResponse, error) { 103 + start := time.Now() 104 + 105 + // Build the exec request JSON 106 + execReq := ExecRequest{ 107 + CodePath: "/bundle/code.js", // Path inside container 108 + Endpoint: req.Endpoint, 109 + Args: req.Args, 110 + Permissions: req.Permissions, 111 + Env: req.Env, 112 + Timeout: req.TimeoutSecs, 113 + } 114 + 115 + reqJSON, err := json.Marshal(execReq) 116 + if err != nil { 117 + return nil, fmt.Errorf("failed to marshal exec request: %w", err) 118 + } 119 + 120 + // Build container run command 121 + args := p.buildContainerArgs(req, runtime) 122 + 123 + cmd := exec.Command(p.runtime, args...) 124 + cmd.Stdin = bytes.NewReader(reqJSON) 125 + cmd.Env = os.Environ() 126 + 127 + var stdout, stderr bytes.Buffer 128 + cmd.Stdout = &stdout 129 + cmd.Stderr = &stderr 130 + 131 + err = cmd.Run() 132 + execTime := time.Since(start) 133 + 134 + // Parse the response 135 + var execResp ExecResponse 136 + if jsonErr := json.Unmarshal(stdout.Bytes(), &execResp); jsonErr != nil { 137 + errMsg := stderr.String() 138 + if errMsg == "" { 139 + errMsg = stdout.String() 140 + } 141 + if err != nil { 142 + errMsg = fmt.Sprintf("%s: %s", err.Error(), errMsg) 143 + } 144 + return &ExecuteResponse{ 145 + Success: false, 146 + Error: errMsg, 147 + Metrics: &Metrics{ExecutionTimeMs: execTime.Milliseconds()}, 148 + }, nil 149 + } 150 + 151 + return &ExecuteResponse{ 152 + Success: execResp.Success, 153 + Data: execResp.Data, 154 + Response: execResp.Response, 155 + Error: execResp.Error, 156 + Metrics: &Metrics{ 157 + ExecutionTimeMs: execTime.Milliseconds(), 158 + }, 159 + }, nil 160 + } 161 + 162 + func (p *ContainerPool) buildContainerArgs(req ExecuteRequest, runtime ContainerRuntime) []string { 163 + args := []string{ 164 + "run", 165 + "--rm", // Remove container after execution 166 + "-i", // Interactive (for stdin) 167 + "--network=none", // No network by default 168 + "--read-only", // Read-only root filesystem 169 + "--cap-drop=ALL", // Drop all capabilities 170 + } 171 + 172 + // Memory limit 173 + args = append(args, "--memory=256m", "--memory-swap=256m") 174 + 175 + // CPU limit 176 + args = append(args, "--cpus=1") 177 + 178 + // Mount the bundle code as read-only 179 + bundleDir := filepath.Dir(req.BundlePath) 180 + bundleFile := filepath.Base(req.BundlePath) 181 + args = append(args, "-v", fmt.Sprintf("%s:/bundle:ro", bundleDir)) 182 + 183 + // Create a writable /tmp inside container 184 + args = append(args, "--tmpfs", "/tmp:rw,noexec,nosuid,size=64m") 185 + 186 + // Handle network permissions 187 + if len(req.Permissions.Net) > 0 { 188 + // Enable networking if any net permissions specified 189 + // Remove the --network=none we added earlier 190 + for i, arg := range args { 191 + if arg == "--network=none" { 192 + args = append(args[:i], args[i+1:]...) 193 + break 194 + } 195 + } 196 + // Use bridge network (default) 197 + args = append(args, "--network=bridge") 198 + } 199 + 200 + // Handle write permissions by mounting specific paths 201 + for _, path := range req.Permissions.Write { 202 + // Create a tmpfs mount for each writable path 203 + args = append(args, "--tmpfs", fmt.Sprintf("%s:rw,size=64m", path)) 204 + } 205 + 206 + // Set environment variables 207 + for key, value := range req.Env { 208 + args = append(args, "-e", fmt.Sprintf("%s=%s", key, value)) 209 + } 210 + 211 + // Pass allowed env var names 212 + for _, envName := range req.Permissions.Env { 213 + if val := os.Getenv(envName); val != "" { 214 + args = append(args, "-e", fmt.Sprintf("%s=%s", envName, val)) 215 + } 216 + } 217 + 218 + // Set the working directory 219 + args = append(args, "-w", "/bundle") 220 + 221 + // Seccomp profile (use default for now, can customize later) 222 + // args = append(args, "--security-opt", "seccomp=/path/to/profile.json") 223 + 224 + // The image and command 225 + args = append(args, runtime.Image) 226 + args = append(args, "at-run-exec") 227 + 228 + // The executor script path needs to know the actual bundle filename 229 + // We pass this via environment 230 + args = insertEnvArg(args, "BUNDLE_FILE", bundleFile) 231 + 232 + return args 233 + } 234 + 235 + // insertEnvArg adds an -e flag before the image name in the args slice 236 + func insertEnvArg(args []string, key, value string) []string { 237 + // Find where the image name is (it's the second-to-last arg) 238 + imageIdx := len(args) - 2 239 + newArgs := make([]string, 0, len(args)+2) 240 + newArgs = append(newArgs, args[:imageIdx]...) 241 + newArgs = append(newArgs, "-e", fmt.Sprintf("%s=%s", key, value)) 242 + newArgs = append(newArgs, args[imageIdx:]...) 243 + return newArgs 244 + } 245 + 246 + func (p *ContainerPool) Stats() PoolStats { 247 + stats := PoolStats{ 248 + Runtimes: make(map[string]RuntimeStats), 249 + Total: TotalStats{ 250 + Idle: len(p.runtimes), 251 + Busy: 0, 252 + Max: 100, // Containers are lightweight, can run many 253 + }, 254 + } 255 + for name := range p.runtimes { 256 + stats.Runtimes[name] = RuntimeStats{Idle: 1, Busy: 0} 257 + } 258 + return stats 259 + } 260 + 261 + func (p *ContainerPool) Shutdown() {} 262 + 263 + func (p *ContainerPool) Drain() {} 264 + 265 + func (p *ContainerPool) Warm(count int) error { 266 + // Pull images if needed 267 + for _, rt := range p.runtimes { 268 + cmd := exec.Command(p.runtime, "image", "inspect", rt.Image) 269 + if err := cmd.Run(); err != nil { 270 + // Image doesn't exist locally, try to build it 271 + // For now, just log - image building will be separate 272 + fmt.Printf(" Warning: image %s not found\n", rt.Image) 273 + } 274 + } 275 + return nil 276 + } 277 + 278 + // BuildImage builds an OCI image for a runtime using Nix 279 + func (p *ContainerPool) BuildImage(runtimeName string) error { 280 + rt, ok := p.runtimes[runtimeName] 281 + if !ok { 282 + return fmt.Errorf("unknown runtime: %s", runtimeName) 283 + } 284 + 285 + // Look for Dockerfile or Nix expression in runtimes dir 286 + runtimeDir := filepath.Join(p.runtimesDir, runtimeName) 287 + 288 + // Check for Nix flake with container output 289 + flakePath := filepath.Join(runtimeDir, "flake.nix") 290 + if _, err := os.Stat(flakePath); err == nil { 291 + // Build using Nix 292 + return p.buildImageWithNix(runtimeDir, rt.Image) 293 + } 294 + 295 + // Check for Dockerfile 296 + dockerfilePath := filepath.Join(runtimeDir, "Dockerfile") 297 + if _, err := os.Stat(dockerfilePath); err == nil { 298 + return p.buildImageWithDockerfile(runtimeDir, rt.Image) 299 + } 300 + 301 + return fmt.Errorf("no Dockerfile or flake.nix found for runtime %s", runtimeName) 302 + } 303 + 304 + func (p *ContainerPool) buildImageWithNix(runtimeDir, imageName string) error { 305 + // Build the container image attribute from the flake 306 + cmd := exec.Command("nix", "build", runtimeDir+"#container", "-o", "result-container") 307 + cmd.Dir = runtimeDir 308 + 309 + var stderr bytes.Buffer 310 + cmd.Stderr = &stderr 311 + 312 + if err := cmd.Run(); err != nil { 313 + return fmt.Errorf("nix build failed: %s", stderr.String()) 314 + } 315 + 316 + // Load the image into docker/podman 317 + resultPath := filepath.Join(runtimeDir, "result-container") 318 + loadCmd := exec.Command(p.runtime, "load", "-i", resultPath) 319 + 320 + var loadStderr bytes.Buffer 321 + loadCmd.Stderr = &loadStderr 322 + 323 + if err := loadCmd.Run(); err != nil { 324 + return fmt.Errorf("failed to load image: %s", loadStderr.String()) 325 + } 326 + 327 + // Tag the image 328 + // The image from Nix might have a hash-based name, we need to retag it 329 + // For now, assume the Nix build produces the correct tag 330 + 331 + return nil 332 + } 333 + 334 + func (p *ContainerPool) buildImageWithDockerfile(runtimeDir, imageName string) error { 335 + cmd := exec.Command(p.runtime, "build", "-t", imageName, runtimeDir) 336 + 337 + var stderr bytes.Buffer 338 + cmd.Stderr = &stderr 339 + 340 + if err := cmd.Run(); err != nil { 341 + return fmt.Errorf("docker build failed: %s", stderr.String()) 342 + } 343 + 344 + return nil 345 + } 346 + 347 + // ImageExists checks if a container image exists locally 348 + func (p *ContainerPool) ImageExists(imageName string) bool { 349 + cmd := exec.Command(p.runtime, "image", "inspect", imageName) 350 + return cmd.Run() == nil 351 + } 352 + 353 + // ListImages returns all at-rund related images 354 + func (p *ContainerPool) ListImages() ([]string, error) { 355 + cmd := exec.Command(p.runtime, "images", "--format", "{{.Repository}}:{{.Tag}}", "--filter", "reference=at-rund-*") 356 + 357 + var stdout bytes.Buffer 358 + cmd.Stdout = &stdout 359 + 360 + if err := cmd.Run(); err != nil { 361 + return nil, err 362 + } 363 + 364 + lines := strings.Split(strings.TrimSpace(stdout.String()), "\n") 365 + var images []string 366 + for _, line := range lines { 367 + if line != "" { 368 + images = append(images, line) 369 + } 370 + } 371 + return images, nil 372 + } 373 + 374 + // Ensure ContainerPool implements Executor 375 + var _ Executor = (*ContainerPool)(nil)
+1
internal/vm/executor.go
··· 21 21 22 22 // Ensure implementations satisfy the interface 23 23 var _ Executor = (*NixPool)(nil) 24 + var _ Executor = (*ContainerPool)(nil) 24 25 var _ Executor = (*FirecrackerPool)(nil) 25 26 26 27 // FirecrackerPool is the production executor using Firecracker VMs
+32
nix/container/Dockerfile
··· 1 + FROM debian:bookworm-slim 2 + 3 + ARG RUNTIME=deno 4 + 5 + # Install Nix 6 + RUN apt-get update && apt-get install -y --no-install-recommends \ 7 + ca-certificates \ 8 + curl \ 9 + xz-utils \ 10 + && rm -rf /var/lib/apt/lists/* 11 + 12 + # Install Nix in single-user mode (simpler for containers) 13 + RUN curl -L https://nixos.org/nix/install | sh -s -- --no-daemon 14 + 15 + # Set up Nix environment 16 + ENV PATH="/root/.nix-profile/bin:$PATH" 17 + ENV NIX_PATH="nixpkgs=channel:nixos-24.11" 18 + 19 + # Copy runtime flakes 20 + COPY runtimes /runtimes 21 + 22 + # Build the runtime using its flake 23 + RUN . /root/.nix-profile/etc/profile.d/nix.sh && \ 24 + nix build /runtimes/${RUNTIME} --out-link /runtime && \ 25 + nix-collect-garbage -d 26 + 27 + # The runtime provides at-run-exec in its bin/ 28 + ENV PATH="/runtime/bin:$PATH" 29 + 30 + WORKDIR /bundle 31 + 32 + ENTRYPOINT ["at-run-exec"]
+37
nix/container/build.sh
··· 1 + #!/bin/bash 2 + # Build container images for at-rund runtimes 3 + # Usage: ./build.sh [runtime] 4 + # If no runtime specified, builds all 5 + 6 + set -e 7 + 8 + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 9 + RUNTIMES_DIR="$SCRIPT_DIR/../runtimes" 10 + 11 + build_runtime() { 12 + local runtime=$1 13 + echo "Building at-rund-${runtime}:latest..." 14 + 15 + docker build \ 16 + --build-arg RUNTIME="$runtime" \ 17 + -t "at-rund-${runtime}:latest" \ 18 + -f "$SCRIPT_DIR/Dockerfile" \ 19 + "$SCRIPT_DIR/.." 20 + 21 + echo "Built at-rund-${runtime}:latest" 22 + } 23 + 24 + if [ -n "$1" ]; then 25 + # Build specific runtime 26 + build_runtime "$1" 27 + else 28 + # Build all runtimes that have flakes 29 + for dir in "$RUNTIMES_DIR"/*/; do 30 + runtime=$(basename "$dir") 31 + if [ -f "$dir/flake.nix" ]; then 32 + build_runtime "$runtime" 33 + fi 34 + done 35 + fi 36 + 37 + echo "Done!"