computer-host/internal/config/config.go

197 lines
6.2 KiB
Go

package config
import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/getcompanion-ai/computer-host/internal/firecracker"
)
const defaultSocketName = "firecracker-host.sock"
// Config contains the host-local daemon settings.
type Config struct {
RootDir string
StatePath string
OperationsPath string
ArtifactsDir string
MachineDisksDir string
SnapshotsDir string
RuntimeDir string
DiskCloneMode DiskCloneMode
DriveIOEngine firecracker.DriveIOEngine
EnablePCI bool
SocketPath string
HTTPAddr string
EgressInterface string
ReconcileInterval time.Duration
FirecrackerBinaryPath string
JailerBinaryPath string
GuestLoginCAPublicKey string
}
// DiskCloneMode controls how the daemon materializes writable machine disks.
type DiskCloneMode string
const (
// DiskCloneModeReflink requires an O(1) copy-on-write clone and never falls back to a full copy.
DiskCloneModeReflink DiskCloneMode = "reflink"
// DiskCloneModeCopy performs a full sparse copy. Use only for local development or emergency fallback.
DiskCloneModeCopy DiskCloneMode = "copy"
)
// Load loads and validates the firecracker-host daemon configuration from the environment.
func Load() (Config, error) {
rootDir := filepath.Clean(strings.TrimSpace(os.Getenv("FIRECRACKER_HOST_ROOT_DIR")))
enablePCI, err := loadBool("FIRECRACKER_HOST_ENABLE_PCI")
if err != nil {
return Config{}, err
}
cfg := Config{
RootDir: rootDir,
StatePath: filepath.Join(rootDir, "state", "state.json"),
OperationsPath: filepath.Join(rootDir, "state", "ops.json"),
ArtifactsDir: filepath.Join(rootDir, "artifacts"),
MachineDisksDir: filepath.Join(rootDir, "machine-disks"),
SnapshotsDir: filepath.Join(rootDir, "snapshots"),
RuntimeDir: filepath.Join(rootDir, "runtime"),
DiskCloneMode: loadDiskCloneMode(os.Getenv("FIRECRACKER_HOST_DISK_CLONE_MODE")),
DriveIOEngine: loadDriveIOEngine(os.Getenv("FIRECRACKER_HOST_DRIVE_IO_ENGINE")),
EnablePCI: enablePCI,
SocketPath: filepath.Join(rootDir, defaultSocketName),
HTTPAddr: strings.TrimSpace(os.Getenv("FIRECRACKER_HOST_HTTP_ADDR")),
EgressInterface: strings.TrimSpace(os.Getenv("FIRECRACKER_HOST_EGRESS_INTERFACE")),
FirecrackerBinaryPath: strings.TrimSpace(os.Getenv("FIRECRACKER_BINARY_PATH")),
JailerBinaryPath: strings.TrimSpace(os.Getenv("JAILER_BINARY_PATH")),
GuestLoginCAPublicKey: strings.TrimSpace(os.Getenv("GUEST_LOGIN_CA_PUBLIC_KEY")),
}
cfg.ReconcileInterval, err = durationDefault("FIRECRACKER_HOST_RECONCILE_INTERVAL", 5*time.Second)
if err != nil {
return Config{}, err
}
if err := cfg.Validate(); err != nil {
return Config{}, err
}
return cfg, nil
}
// Validate reports whether the host configuration is usable.
func (c Config) Validate() error {
if c.RootDir == "" {
return fmt.Errorf("FIRECRACKER_HOST_ROOT_DIR is required")
}
if c.FirecrackerBinaryPath == "" {
return fmt.Errorf("FIRECRACKER_BINARY_PATH is required")
}
if c.JailerBinaryPath == "" {
return fmt.Errorf("JAILER_BINARY_PATH is required")
}
if strings.TrimSpace(c.StatePath) == "" {
return fmt.Errorf("state path is required")
}
if strings.TrimSpace(c.OperationsPath) == "" {
return fmt.Errorf("operations path is required")
}
if strings.TrimSpace(c.ArtifactsDir) == "" {
return fmt.Errorf("artifacts dir is required")
}
if strings.TrimSpace(c.MachineDisksDir) == "" {
return fmt.Errorf("machine disks dir is required")
}
if strings.TrimSpace(c.SnapshotsDir) == "" {
return fmt.Errorf("snapshots dir is required")
}
if strings.TrimSpace(c.RuntimeDir) == "" {
return fmt.Errorf("runtime dir is required")
}
if err := c.DiskCloneMode.Validate(); err != nil {
return err
}
if err := validateDriveIOEngine(c.DriveIOEngine); err != nil {
return err
}
if strings.TrimSpace(c.SocketPath) == "" {
return fmt.Errorf("socket path is required")
}
if strings.TrimSpace(c.EgressInterface) == "" {
return fmt.Errorf("FIRECRACKER_HOST_EGRESS_INTERFACE is required")
}
if c.ReconcileInterval <= 0 {
return fmt.Errorf("FIRECRACKER_HOST_RECONCILE_INTERVAL must be greater than zero")
}
return nil
}
// FirecrackerRuntimeConfig converts the daemon config into the Firecracker runtime config.
func (c Config) FirecrackerRuntimeConfig() firecracker.RuntimeConfig {
return firecracker.RuntimeConfig{
RootDir: c.RuntimeDir,
EgressInterface: c.EgressInterface,
FirecrackerBinaryPath: c.FirecrackerBinaryPath,
JailerBinaryPath: c.JailerBinaryPath,
EnablePCI: c.EnablePCI,
}
}
func loadDiskCloneMode(raw string) DiskCloneMode {
value := strings.TrimSpace(raw)
if value == "" {
return DiskCloneModeReflink
}
return DiskCloneMode(value)
}
// Validate reports whether the clone mode is safe to use.
func (m DiskCloneMode) Validate() error {
switch m {
case DiskCloneModeReflink, DiskCloneModeCopy:
return nil
default:
return fmt.Errorf("FIRECRACKER_HOST_DISK_CLONE_MODE must be %q or %q", DiskCloneModeReflink, DiskCloneModeCopy)
}
}
func loadDriveIOEngine(raw string) firecracker.DriveIOEngine {
value := strings.TrimSpace(raw)
if value == "" {
return firecracker.DriveIOEngineSync
}
return firecracker.DriveIOEngine(value)
}
func validateDriveIOEngine(engine firecracker.DriveIOEngine) error {
switch engine {
case firecracker.DriveIOEngineSync, firecracker.DriveIOEngineAsync:
return nil
default:
return fmt.Errorf("FIRECRACKER_HOST_DRIVE_IO_ENGINE must be %q or %q", firecracker.DriveIOEngineSync, firecracker.DriveIOEngineAsync)
}
}
func loadBool(name string) (bool, error) {
value := strings.TrimSpace(os.Getenv(name))
if value == "" {
return false, nil
}
parsed, err := strconv.ParseBool(value)
if err != nil {
return false, fmt.Errorf("%s must be a boolean: %w", name, err)
}
return parsed, nil
}
func durationDefault(name string, fallback time.Duration) (time.Duration, error) {
value := strings.TrimSpace(os.Getenv(name))
if value == "" {
return fallback, nil
}
parsed, err := time.ParseDuration(value)
if err != nil {
return 0, fmt.Errorf("%s must be a duration: %w", name, err)
}
return parsed, nil
}