package config import ( "errors" "fmt" "os" "path/filepath" "strings" "github.com/spf13/cobra" "github.com/spf13/viper" ) type Config struct { HetznerAPIKey string NamecheapAPIKey string NamecheapUser string Domain string Region string ServerType string DeploymentID string ConfigDir string LetsEncryptEmail string } func LoadConfig(cmd *cobra.Command) (*Config, error) { return LoadConfigWithValidation(cmd, true) } func LoadConfigWithValidation(cmd *cobra.Command, validate bool) (*Config, error) { homeDir, err := os.UserHomeDir() if err != nil { return nil, fmt.Errorf("failed to get user home directory: %w", err) } configDir := filepath.Join(homeDir, ".sslh-lab") configFile := filepath.Join(configDir, "config.yaml") viper.SetConfigType("yaml") viper.SetConfigFile(configFile) viper.SetEnvPrefix("SSLH") viper.AutomaticEnv() viper.SetDefault("region", "nbg1") viper.SetDefault("server_type", "cpx22") viper.SetDefault("config_dir", configDir) if err := viper.ReadInConfig(); err != nil { var configFileNotFoundError viper.ConfigFileNotFoundError var pathError *os.PathError errMsg := strings.ToLower(err.Error()) if errors.As(err, &configFileNotFoundError) { // Config file not found is OK - we'll use defaults/env vars/flags } else if errors.As(err, &pathError) && os.IsNotExist(pathError) { // File doesn't exist - this is OK } else if os.IsNotExist(err) { // Direct IsNotExist check } else if strings.Contains(errMsg, "no such file") || strings.Contains(errMsg, "not found") { // File not found error (handled as OK) } else { // Any other error is a real problem return nil, fmt.Errorf("failed to read config file: %w", err) } } config := &Config{ HetznerAPIKey: getStringValue(cmd, "hetzner-key", "HETZNER_KEY"), NamecheapAPIKey: getStringValue(cmd, "namecheap-key", "NAMECHEAP_KEY"), NamecheapUser: getStringValue(cmd, "namecheap-user", "NAMECHEAP_USER"), Domain: getStringValue(cmd, "domain", "DOMAIN"), Region: getStringValue(cmd, "region", "REGION"), ServerType: getStringValue(cmd, "server-type", "SERVER_TYPE"), ConfigDir: viper.GetString("config_dir"), LetsEncryptEmail: getStringValue(cmd, "letsencrypt-email", "LETSENCRYPT_EMAIL"), } if validate { if err := config.Validate(); err != nil { return nil, fmt.Errorf("configuration validation failed: %w", err) } } return config, nil } func getStringValue(cmd *cobra.Command, flagName, envKey string) string { if cmd != nil { if flagValue, err := cmd.Flags().GetString(flagName); err == nil && flagValue != "" { return flagValue } } if envValue := viper.GetString(envKey); envValue != "" { return envValue } // For viper config file, convert hyphen to underscore (viper uses underscores) viperKey := strings.ReplaceAll(flagName, "-", "_") return viper.GetString(viperKey) } func (c *Config) Validate() error { if c.HetznerAPIKey == "" { return fmt.Errorf("hetzner API key is required (set via --hetzner-key, SSLH_HETZNER_KEY, or config file)") } if c.NamecheapAPIKey == "" { return fmt.Errorf("namecheap API key is required (set via --namecheap-key, SSLH_NAMECHEAP_KEY, or config file)") } if c.NamecheapUser == "" { return fmt.Errorf("namecheap username is required (set via --namecheap-user, SSLH_NAMECHEAP_USER, or config file)") } if c.Domain == "" { return fmt.Errorf("domain is required (set via --domain, SSLH_DOMAIN, or config file)") } return nil } func (c *Config) ValidateForTeardown() error { if c.HetznerAPIKey == "" { return fmt.Errorf("hetzner API key is required (set via --hetzner-key, SSLH_HETZNER_KEY, or config file)") } if c.NamecheapAPIKey == "" { return fmt.Errorf("namecheap API key is required (set via --namecheap-key, SSLH_NAMECHEAP_KEY, or config file)") } if c.NamecheapUser == "" { return fmt.Errorf("namecheap username is required (set via --namecheap-user, SSLH_NAMECHEAP_USER, or config file)") } return nil } func (c *Config) GetDeploymentDir() string { if c.DeploymentID == "" { return "" } return filepath.Join(c.ConfigDir, "deployments", c.DeploymentID) } func (c *Config) EnsureConfigDir() error { return os.MkdirAll(c.ConfigDir, 0755) } func (c *Config) EnsureDeploymentDir() error { if c.DeploymentID == "" { return fmt.Errorf("deployment ID not set") } return os.MkdirAll(c.GetDeploymentDir(), 0700) }