From 3428089becf800ea0d150da4691c43706265238b Mon Sep 17 00:00:00 2001 From: LordMathis Date: Sat, 19 Jul 2025 19:38:47 +0200 Subject: [PATCH] Add configuration loading with defaults and environment variable support --- server/cmd/llamactl.go | 6 ++ server/go.mod | 2 +- server/pkg/config.go | 232 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 server/pkg/config.go diff --git a/server/cmd/llamactl.go b/server/cmd/llamactl.go index 98dfa57..a294681 100644 --- a/server/cmd/llamactl.go +++ b/server/cmd/llamactl.go @@ -14,6 +14,12 @@ import ( // @basePath /api/v1 func main() { + config, err := llamactl.LoadConfig("") + if err != nil { + fmt.Printf("Error loading config: %v\n", err) + fmt.Println("Using default configuration.") + } + // Initialize the instance manager instanceManager := llamactl.NewInstanceManager() diff --git a/server/go.mod b/server/go.mod index ef37235..836367b 100644 --- a/server/go.mod +++ b/server/go.mod @@ -6,6 +6,7 @@ require ( github.com/go-chi/chi/v5 v5.2.2 github.com/swaggo/http-swagger v1.3.4 github.com/swaggo/swag v1.16.5 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -21,5 +22,4 @@ require ( golang.org/x/net v0.42.0 // indirect golang.org/x/sync v0.16.0 // indirect golang.org/x/tools v0.35.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/server/pkg/config.go b/server/pkg/config.go new file mode 100644 index 0000000..4b334d4 --- /dev/null +++ b/server/pkg/config.go @@ -0,0 +1,232 @@ +package llamactl + +import ( + "os" + "path/filepath" + "runtime" + "strconv" + "strings" + "time" + + "gopkg.in/yaml.v3" +) + +// Config represents the configuration for llamactl +type Config struct { + Server ServerConfig `yaml:"server"` + Instances InstancesConfig `yaml:"instances"` +} + +// ServerConfig contains HTTP server configuration +type ServerConfig struct { + // Server host to bind to + Host string `yaml:"host"` + + // Server port to bind to + Port int `yaml:"port"` +} + +// InstancesConfig contains instance management configuration +type InstancesConfig struct { + // Port range for instances (e.g., "8000-9000") + PortRange string `yaml:"port_range"` + + // Directory where instance logs will be stored + LogDirectory string `yaml:"log_directory"` + + // Maximum number of instances that can be created + MaxInstances int `yaml:"max_instances"` + + // Path to llama-server executable + LlamaExecutable string `yaml:"llama_executable"` + + // Default auto-restart setting for new instances + DefaultAutoRestart bool `yaml:"default_auto_restart"` + + // Default max restarts for new instances + DefaultMaxRestarts int `yaml:"default_max_restarts"` + + // Default restart delay for new instances + DefaultRestartDelay Duration `yaml:"default_restart_delay"` +} + +// LoadConfig loads configuration with the following precedence: +// 1. Hardcoded defaults +// 2. Config file +// 3. Environment variables +func LoadConfig(configPath string) (Config, error) { + // 1. Start with defaults + cfg := Config{ + Server: ServerConfig{ + Host: "", + Port: 8080, + }, + Instances: InstancesConfig{ + PortRange: "8000-9000", + LogDirectory: "/tmp/llamactl", + MaxInstances: 10, + LlamaExecutable: "llama-server", + DefaultAutoRestart: false, + DefaultMaxRestarts: 3, + DefaultRestartDelay: Duration(5 * time.Second), + }, + } + + // 2. Load from config file + if err := loadConfigFile(&cfg, configPath); err != nil { + return cfg, err + } + + // 3. Override with environment variables + loadEnvVars(&cfg) + + return cfg, nil +} + +// loadConfigFile attempts to load config from file with fallback locations +func loadConfigFile(cfg *Config, configPath string) error { + var configLocations []string + + // If specific config path provided, use only that + if configPath != "" { + configLocations = []string{configPath} + } else { + // Default config file locations (in order of precedence) + configLocations = getDefaultConfigLocations() + } + + for _, path := range configLocations { + if data, err := os.ReadFile(path); err == nil { + if err := yaml.Unmarshal(data, cfg); err != nil { + return err + } + return nil + } + } + + return nil +} + +// loadEnvVars overrides config with environment variables +func loadEnvVars(cfg *Config) { + // Server config + if host := os.Getenv("LLAMACTL_HOST"); host != "" { + cfg.Server.Host = host + } + if port := os.Getenv("LLAMACTL_PORT"); port != "" { + if p, err := strconv.Atoi(port); err == nil { + cfg.Server.Port = p + } + } + + // Instance config + if portRange := os.Getenv("LLAMACTL_INSTANCE_PORT_RANGE"); portRange != "" { + cfg.Instances.PortRange = portRange + } + if logDir := os.Getenv("LLAMACTL_LOG_DIR"); logDir != "" { + cfg.Instances.LogDirectory = logDir + } + if maxInstances := os.Getenv("LLAMACTL_MAX_INSTANCES"); maxInstances != "" { + if m, err := strconv.Atoi(maxInstances); err == nil { + cfg.Instances.MaxInstances = m + } + } + if llamaExec := os.Getenv("LLAMACTL_LLAMA_EXECUTABLE"); llamaExec != "" { + cfg.Instances.LlamaExecutable = llamaExec + } + if autoRestart := os.Getenv("LLAMACTL_DEFAULT_AUTO_RESTART"); autoRestart != "" { + if b, err := strconv.ParseBool(autoRestart); err == nil { + cfg.Instances.DefaultAutoRestart = b + } + } + if maxRestarts := os.Getenv("LLAMACTL_DEFAULT_MAX_RESTARTS"); maxRestarts != "" { + if m, err := strconv.Atoi(maxRestarts); err == nil { + cfg.Instances.DefaultMaxRestarts = m + } + } + if restartDelay := os.Getenv("LLAMACTL_DEFAULT_RESTART_DELAY"); restartDelay != "" { + if d, err := parseDelaySeconds(restartDelay); err == nil { + cfg.Instances.DefaultRestartDelay = Duration(d) + } + } +} + +// parseDelaySeconds parses a string as seconds and returns a time.Duration +// Accepts both plain numbers (seconds) and duration strings like "5s", "30s" +func parseDelaySeconds(s string) (time.Duration, error) { + // If it contains letters, try parsing as duration + if strings.ContainsAny(s, "smh") { + return time.ParseDuration(s) + } + + // Otherwise parse as seconds + seconds, err := strconv.ParseFloat(s, 64) + if err != nil { + return 0, err + } + + return time.Duration(seconds * float64(time.Second)), nil +} + +// getDefaultConfigLocations returns platform-specific config file locations +func getDefaultConfigLocations() []string { + var locations []string + + // Current directory (cross-platform) + locations = append(locations, + "./llamactl.yaml", + "./config.yaml", + ) + + homeDir, _ := os.UserHomeDir() + + switch runtime.GOOS { + case "windows": + // Windows: Use APPDATA and ProgramData + if appData := os.Getenv("APPDATA"); appData != "" { + locations = append(locations, filepath.Join(appData, "llamactl", "config.yaml")) + } + if programData := os.Getenv("PROGRAMDATA"); programData != "" { + locations = append(locations, filepath.Join(programData, "llamactl", "config.yaml")) + } + // Fallback to user home + if homeDir != "" { + locations = append(locations, filepath.Join(homeDir, "llamactl", "config.yaml")) + } + + case "darwin": + // macOS: Use proper Application Support directories + if homeDir != "" { + locations = append(locations, + filepath.Join(homeDir, "Library", "Application Support", "llamactl", "config.yaml"), + filepath.Join(homeDir, ".config", "llamactl", "config.yaml"), // XDG fallback + ) + } + locations = append(locations, "/Library/Application Support/llamactl/config.yaml") + locations = append(locations, "/etc/llamactl/config.yaml") // Unix fallback + + default: + // User config: $XDG_CONFIG_HOME/llamactl/config.yaml or ~/.config/llamactl/config.yaml + configHome := os.Getenv("XDG_CONFIG_HOME") + if configHome == "" && homeDir != "" { + configHome = filepath.Join(homeDir, ".config") + } + if configHome != "" { + locations = append(locations, filepath.Join(configHome, "llamactl", "config.yaml")) + } + + // System config: /etc/llamactl/config.yaml + locations = append(locations, "/etc/llamactl/config.yaml") + + // Additional system locations + if xdgConfigDirs := os.Getenv("XDG_CONFIG_DIRS"); xdgConfigDirs != "" { + for _, dir := range strings.Split(xdgConfigDirs, ":") { + if dir != "" { + locations = append(locations, filepath.Join(dir, "llamactl", "config.yaml")) + } + } + } + } + + return locations +}