package config import ( "errors" "os" "path/filepath" "strings" "time" toml "github.com/pelletier/go-toml" "banger/internal/model" "banger/internal/paths" "banger/internal/runtimebundle" ) type fileConfig struct { RuntimeDir string `toml:"runtime_dir"` RepoRoot string `toml:"repo_root"` LogLevel string `toml:"log_level"` FirecrackerBin string `toml:"firecracker_bin"` SSHKeyPath string `toml:"ssh_key_path"` NamegenPath string `toml:"namegen_path"` CustomizeScript string `toml:"customize_script"` VSockAgent string `toml:"vsock_agent_path"` VSockPingHelper string `toml:"vsock_ping_helper_path"` DefaultWorkSeed string `toml:"default_work_seed"` DefaultImageName string `toml:"default_image_name"` DefaultRootfs string `toml:"default_rootfs"` DefaultBaseRootfs string `toml:"default_base_rootfs"` DefaultKernel string `toml:"default_kernel"` DefaultInitrd string `toml:"default_initrd"` DefaultModulesDir string `toml:"default_modules_dir"` DefaultPackages string `toml:"default_packages_file"` AutoStopStaleAfter string `toml:"auto_stop_stale_after"` StatsPollInterval string `toml:"stats_poll_interval"` MetricsPoll string `toml:"metrics_poll_interval"` BridgeName string `toml:"bridge_name"` BridgeIP string `toml:"bridge_ip"` CIDR string `toml:"cidr"` TapPoolSize int `toml:"tap_pool_size"` DefaultDNS string `toml:"default_dns"` } func Load(layout paths.Layout) (model.DaemonConfig, error) { cfg := model.DaemonConfig{ LogLevel: "info", AutoStopStaleAfter: 0, StatsPollInterval: model.DefaultStatsPollInterval, MetricsPollInterval: model.DefaultMetricsPollInterval, BridgeName: model.DefaultBridgeName, BridgeIP: model.DefaultBridgeIP, CIDR: model.DefaultCIDR, TapPoolSize: 4, DefaultDNS: model.DefaultDNS, DefaultImageName: "default", } path := filepath.Join(layout.ConfigDir, "config.toml") info, err := os.Stat(path) var file fileConfig if err != nil { if !os.IsNotExist(err) { return cfg, err } } else if !info.IsDir() { data, err := os.ReadFile(path) if err != nil { return cfg, err } if err := toml.Unmarshal(data, &file); err != nil { return cfg, err } } cfg.RuntimeDir = paths.ResolveRuntimeDir(file.RuntimeDir, file.RepoRoot) if err := applyRuntimeDefaults(&cfg); err != nil { return cfg, err } if file.FirecrackerBin != "" { cfg.FirecrackerBin = file.FirecrackerBin } if file.LogLevel != "" { cfg.LogLevel = file.LogLevel } if file.NamegenPath != "" { cfg.NamegenPath = file.NamegenPath } if file.CustomizeScript != "" { cfg.CustomizeScript = file.CustomizeScript } if file.VSockAgent != "" { cfg.VSockAgentPath = file.VSockAgent } else if file.VSockPingHelper != "" { cfg.VSockAgentPath = file.VSockPingHelper } if file.DefaultWorkSeed != "" { cfg.DefaultWorkSeed = file.DefaultWorkSeed } if file.DefaultImageName != "" { cfg.DefaultImageName = file.DefaultImageName } if file.DefaultRootfs != "" { cfg.DefaultRootfs = file.DefaultRootfs } if file.DefaultBaseRootfs != "" { cfg.DefaultBaseRootfs = file.DefaultBaseRootfs } if file.DefaultKernel != "" { cfg.DefaultKernel = file.DefaultKernel } if file.DefaultInitrd != "" { cfg.DefaultInitrd = file.DefaultInitrd } if file.DefaultModulesDir != "" { cfg.DefaultModulesDir = file.DefaultModulesDir } if file.DefaultPackages != "" { cfg.DefaultPackagesFile = file.DefaultPackages } if file.BridgeName != "" { cfg.BridgeName = file.BridgeName } if file.BridgeIP != "" { cfg.BridgeIP = file.BridgeIP } if file.CIDR != "" { cfg.CIDR = file.CIDR } if file.TapPoolSize > 0 { cfg.TapPoolSize = file.TapPoolSize } if file.DefaultDNS != "" { cfg.DefaultDNS = file.DefaultDNS } if file.AutoStopStaleAfter != "" { duration, err := time.ParseDuration(file.AutoStopStaleAfter) if err != nil { return cfg, err } cfg.AutoStopStaleAfter = duration } if file.StatsPollInterval != "" { duration, err := time.ParseDuration(file.StatsPollInterval) if err != nil { return cfg, err } cfg.StatsPollInterval = duration } if file.MetricsPoll != "" { duration, err := time.ParseDuration(file.MetricsPoll) if err != nil { return cfg, err } cfg.MetricsPollInterval = duration } if value := os.Getenv("BANGER_LOG_LEVEL"); value != "" { cfg.LogLevel = value } return cfg, nil } func applyRuntimeDefaults(cfg *model.DaemonConfig) error { if cfg.RuntimeDir == "" { return nil } meta, err := runtimebundle.LoadBundleMetadata(cfg.RuntimeDir) switch { case err == nil: applyBundleMetadataDefaults(cfg, cfg.RuntimeDir, meta) case errors.Is(err, os.ErrNotExist): applyLegacyRuntimeDefaults(cfg) default: return err } if cfg.DefaultRootfs == "" { cfg.DefaultRootfs = firstExistingRuntimePath( filepath.Join(cfg.RuntimeDir, "rootfs-docker.ext4"), filepath.Join(cfg.RuntimeDir, "rootfs.ext4"), ) } if cfg.DefaultBaseRootfs == "" { cfg.DefaultBaseRootfs = firstExistingRuntimePath( filepath.Join(cfg.RuntimeDir, "rootfs.ext4"), cfg.DefaultRootfs, ) } if cfg.DefaultWorkSeed == "" && cfg.DefaultRootfs != "" { cfg.DefaultWorkSeed = firstExistingRuntimePath(associatedWorkSeedPath(cfg.DefaultRootfs)) } return nil } func applyBundleMetadataDefaults(cfg *model.DaemonConfig, runtimeDir string, meta runtimebundle.BundleMetadata) { cfg.FirecrackerBin = defaultRuntimePath(cfg.FirecrackerBin, runtimeDir, meta.FirecrackerBin) cfg.SSHKeyPath = defaultRuntimePath(cfg.SSHKeyPath, runtimeDir, meta.SSHKeyPath) cfg.NamegenPath = defaultRuntimePath(cfg.NamegenPath, runtimeDir, meta.NamegenPath) cfg.CustomizeScript = defaultRuntimePath(cfg.CustomizeScript, runtimeDir, meta.CustomizeScript) cfg.VSockAgentPath = defaultRuntimePath(cfg.VSockAgentPath, runtimeDir, meta.VSockAgentPath) cfg.DefaultWorkSeed = defaultRuntimePath(cfg.DefaultWorkSeed, runtimeDir, meta.DefaultWorkSeed) cfg.DefaultKernel = defaultRuntimePath(cfg.DefaultKernel, runtimeDir, meta.DefaultKernel) cfg.DefaultInitrd = defaultRuntimePath(cfg.DefaultInitrd, runtimeDir, meta.DefaultInitrd) cfg.DefaultModulesDir = defaultRuntimePath(cfg.DefaultModulesDir, runtimeDir, meta.DefaultModulesDir) cfg.DefaultPackagesFile = defaultRuntimePath(cfg.DefaultPackagesFile, runtimeDir, meta.DefaultPackages) cfg.DefaultRootfs = defaultRuntimePath(cfg.DefaultRootfs, runtimeDir, meta.DefaultRootfs) cfg.DefaultBaseRootfs = defaultRuntimePath(cfg.DefaultBaseRootfs, runtimeDir, meta.DefaultBaseRootfs) } func applyLegacyRuntimeDefaults(cfg *model.DaemonConfig) { cfg.FirecrackerBin = defaultRuntimePath(cfg.FirecrackerBin, cfg.RuntimeDir, "firecracker") cfg.SSHKeyPath = defaultRuntimePath(cfg.SSHKeyPath, cfg.RuntimeDir, "id_ed25519") cfg.NamegenPath = defaultRuntimePath(cfg.NamegenPath, cfg.RuntimeDir, "namegen") cfg.CustomizeScript = defaultRuntimePath(cfg.CustomizeScript, cfg.RuntimeDir, "customize.sh") cfg.VSockAgentPath = firstExistingRuntimePath( defaultRuntimePath(cfg.VSockAgentPath, cfg.RuntimeDir, "banger-vsock-agent"), filepath.Join(cfg.RuntimeDir, "banger-vsock-pingd"), ) cfg.DefaultWorkSeed = defaultRuntimePath(cfg.DefaultWorkSeed, cfg.RuntimeDir, "rootfs-docker.work-seed.ext4") cfg.DefaultKernel = defaultRuntimePath(cfg.DefaultKernel, cfg.RuntimeDir, "wtf/root/boot/vmlinux-6.8.0-94-generic") cfg.DefaultInitrd = defaultRuntimePath(cfg.DefaultInitrd, cfg.RuntimeDir, "wtf/root/boot/initrd.img-6.8.0-94-generic") cfg.DefaultModulesDir = defaultRuntimePath(cfg.DefaultModulesDir, cfg.RuntimeDir, "wtf/root/lib/modules/6.8.0-94-generic") cfg.DefaultPackagesFile = defaultRuntimePath(cfg.DefaultPackagesFile, cfg.RuntimeDir, "packages.apt") } func defaultRuntimePath(current, runtimeDir, relative string) string { if current != "" || relative == "" { return current } return filepath.Join(runtimeDir, relative) } func firstExistingRuntimePath(paths ...string) string { for _, candidate := range paths { if candidate == "" { continue } if _, err := os.Stat(candidate); err == nil { return candidate } } return "" } func associatedWorkSeedPath(rootfsPath string) string { rootfsPath = strings.TrimSpace(rootfsPath) if rootfsPath == "" { return "" } if strings.HasSuffix(rootfsPath, ".ext4") { return strings.TrimSuffix(rootfsPath, ".ext4") + ".work-seed.ext4" } return rootfsPath + ".work-seed" }