package config import ( "fmt" "os" "strings" // Added for PreviewTLD validation "gopkg.in/yaml.v3" ) // Config holds application configuration type Config struct { Server *Server `yaml:"server"` Database *Database `yaml:"database"` Auth *Auth `yaml:"auth"` Providers map[string]map[string]string `yaml:"providers"` Debug bool `yaml:"debug"` LocalPreview bool `yaml:"local_preview"` // For development/testing only - use false for production PreviewTLD string `yaml:"preview_tld"` BuildkitHost string `yaml:"buildkit_host"` ReistryUrl string `yaml:"registry_url"` // URL of the Docker registry } func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { type plain Config err := unmarshal((*plain)(c)) if err != nil { return err } // Set default preview TLD if not specified if c.PreviewTLD == "" { c.PreviewTLD = "home.local" } // Set default Buildkit host if not specified if c.BuildkitHost == "" { c.BuildkitHost = "unix:///run/buildkit/buildkitd.sock" } return nil } // Validate performs a comprehensive validation of the configuration. func (c *Config) Validate() error { if c.Server == nil { return fmt.Errorf("server configuration block is required") } if err := c.Server.Validate(); err != nil { return fmt.Errorf("server config validation failed: %w", err) } if c.Database == nil { return fmt.Errorf("database configuration block is required") } if err := c.Database.Validate(); err != nil { return fmt.Errorf("database config validation failed: %w", err) } if c.Auth == nil { return fmt.Errorf("auth configuration block is required") } if err := c.Auth.Validate(); err != nil { return fmt.Errorf("auth config validation failed: %w", err) } if len(c.Providers) == 0 { // Corrected: Removed redundant nil check return fmt.Errorf("at least one provider configuration is required in 'providers' block") } for providerName, providerConfig := range c.Providers { if len(providerConfig) == 0 { return fmt.Errorf("provider '%s' configuration is empty", providerName) } } return nil } // Server holds server configuration type Server struct { Host string `yaml:"host"` Port int `yaml:"port"` Tls *TlsConfig `yaml:"tls"` } func (s *Server) UnmarshalYAML(unmarshal func(interface{}) error) error { type plain Server err := unmarshal((*plain)(s)) if err != nil { return err } if s.Host == "" { s.Host = "0.0.0.0" // Default to all interfaces } if s.Port == 0 { s.Port = 443 // Default port } return nil } // Validate performs validation for Server configuration. func (s *Server) Validate() error { if strings.TrimSpace(s.Host) == "" { return fmt.Errorf("server host is required and cannot be empty") } if s.Port <= 0 || s.Port > 65535 { return fmt.Errorf("server port must be between 1 and 65535, got %d", s.Port) } if s.Tls == nil { return fmt.Errorf("TLS configuration block is required") } if err := s.Tls.Validate(); err != nil { return fmt.Errorf("TLS config validation failed: %w", err) } return nil } // TlsConfig holds TLS configuration type TlsConfig struct { Enabled bool `yaml:"enabled"` CertFile string `yaml:"cert_file"` KeyFile string `yaml:"key_file"` } func (t *TlsConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { type plain TlsConfig err := unmarshal((*plain)(t)) if err != nil { return err } return nil } // Validate performs validation for TlsConfig. func (t *TlsConfig) Validate() error { if t.Enabled { if strings.TrimSpace(t.CertFile) == "" { return fmt.Errorf("TLS cert_file is required and cannot be empty when TLS is enabled") } if strings.TrimSpace(t.KeyFile) == "" { return fmt.Errorf("TLS key_file is required and cannot be empty when TLS is enabled") } } return nil } // Database holds database configuration type Database struct { DSN string `yaml:"dsn"` } func (d *Database) UnmarshalYAML(unmarshal func(interface{}) error) error { type plain Database err := unmarshal((*plain)(d)) if err != nil { return err } return nil } // Validate performs validation for Database configuration. func (d *Database) Validate() error { if strings.TrimSpace(d.DSN) == "" { return fmt.Errorf("database DSN is required and cannot be empty") } if !strings.HasPrefix(d.DSN, "file:") && d.DSN != ":memory:" && !strings.Contains(d.DSN, ".db") { // This is a basic check, might need refinement based on actual DSN formats used fmt.Printf("Warning: DSN '%s' might not be a typical SQLite DSN\n", d.DSN) } return nil } // Sql holds SQL database configuration type Sqlite struct { File string `yaml:"file"` } func (c *Sqlite) UnmarshalYAML(unmarshal func(interface{}) error) error { type plain Sqlite err := unmarshal((*plain)(c)) if err != nil { return err } if c.File == "" { return fmt.Errorf("SQLite file is required") } return nil } // Auth holds authentication configuration type Auth struct { PrivateKey string `yaml:"private_key"` TokenDuration int `yaml:"token_duration"` CleanupInterval int `yaml:"cleanup_interval"` } func (a *Auth) UnmarshalYAML(unmarshal func(interface{}) error) error { type plain Auth err := unmarshal((*plain)(a)) if err != nil { return err } if a.TokenDuration == 0 { a.TokenDuration = 3600 // Default to 1 hour } if a.CleanupInterval == 0 { a.CleanupInterval = 3600 // Default to 1 hour } return nil } // Validate performs validation for Auth configuration. func (a *Auth) Validate() error { if strings.TrimSpace(a.PrivateKey) == "" { return fmt.Errorf("auth private_key is required and cannot be empty") } if a.TokenDuration <= 0 { return fmt.Errorf("auth token_duration must be a positive integer, got %d", a.TokenDuration) } if a.CleanupInterval <= 0 { return fmt.Errorf("auth cleanup_interval must be a positive integer, got %d", a.CleanupInterval) } return nil } func Load(configPath string) (*Config, error) { cnf := &Config{} b, err := os.ReadFile(configPath) if err != nil { return nil, err } err = yaml.Unmarshal(b, cnf) if err != nil { return nil, err } // Validate the loaded configuration if err := cnf.Validate(); err != nil { return nil, fmt.Errorf("configuration validation failed: %w", err) } return cnf, nil }