package config import ( "fmt" "strings" "sync" "github.com/spf13/afero" ) // Manager manages application configuration with value resolution. type Manager struct { mu sync.RWMutex config map[string]interface{} environment string resolver *Resolver loader *Loader configFile string resolvedCache map[string]interface{} fs afero.Fs } // NewManager creates a new configuration manager. func NewManager() *Manager { fs := afero.NewOsFs() return &Manager{ config: make(map[string]interface{}), loader: NewLoader(fs), resolvedCache: make(map[string]interface{}), fs: fs, } } // SetFs sets the filesystem to use for all file operations. // This is primarily useful for testing with an in-memory filesystem. func (m *Manager) SetFs(fs afero.Fs) { m.mu.Lock() defer m.mu.Unlock() m.fs = fs m.loader = NewLoader(fs) // If we have a resolver, recreate it with the new fs if m.resolver != nil { gcpProject := "" awsRegion := "us-east-1" // Try to get the current settings if gcpProj := m.getConfigValue("GCPProject", ""); gcpProj != nil { if str, ok := gcpProj.(string); ok { gcpProject = str } } if awsReg := m.getConfigValue("AWSRegion", "us-east-1"); awsReg != nil { if str, ok := awsReg.(string); ok { awsRegion = str } } m.resolver = NewResolver(gcpProject, awsRegion, fs) } // Clear caches as filesystem changed m.resolvedCache = make(map[string]interface{}) } // LoadFile loads configuration from a specific file. func (m *Manager) LoadFile(configFile string) error { m.mu.Lock() defer m.mu.Unlock() config, err := m.loader.LoadYAML(configFile) if err != nil { return err } m.config = config m.configFile = configFile m.resolvedCache = make(map[string]interface{}) // Clear cache return nil } // loadConfig loads the configuration from file. func (m *Manager) loadConfig() error { if m.configFile == "" { // Try to find config.yaml configPath, err := m.loader.FindConfigFile("config.yaml") if err != nil { return err } m.configFile = configPath } config, err := m.loader.LoadYAML(m.configFile) if err != nil { return err } m.config = config m.resolvedCache = make(map[string]interface{}) // Clear cache return nil } // SetEnvironment sets the active environment. func (m *Manager) SetEnvironment(environment string) { m.mu.Lock() defer m.mu.Unlock() m.environment = strings.ToLower(environment) // Create resolver with GCP project and AWS region if available gcpProject := m.getConfigValue("GCPProject", "") awsRegion := m.getConfigValue("AWSRegion", "us-east-1") if gcpProjectStr, ok := gcpProject.(string); ok { if awsRegionStr, ok := awsRegion.(string); ok { m.resolver = NewResolver(gcpProjectStr, awsRegionStr, m.fs) } } // Clear resolved cache when environment changes m.resolvedCache = make(map[string]interface{}) } // Get retrieves a configuration value. func (m *Manager) Get(key string, defaultValue interface{}) interface{} { m.mu.RLock() // Ensure config is loaded if m.config == nil || len(m.config) == 0 { // Need to upgrade to write lock to load config m.mu.RUnlock() m.mu.Lock() // Double-check after acquiring write lock if m.config == nil || len(m.config) == 0 { if err := m.loadConfig(); err != nil { // Config file not found is expected when all values // come from environment variables. Only log at debug // level to avoid confusing "Failed to load config" // messages during normal operation. _ = err m.mu.Unlock() return defaultValue } } // Downgrade back to read lock m.mu.Unlock() m.mu.RLock() } defer m.mu.RUnlock() // Check cache first cacheKey := fmt.Sprintf("config.%s", key) if cached, ok := m.resolvedCache[cacheKey]; ok { return cached } // Try environment-specific config first var rawValue interface{} if m.environment != "" { envMap, ok := m.config["environments"].(map[string]interface{}) if ok { if env, ok := envMap[m.environment].(map[string]interface{}); ok { if config, ok := env["config"].(map[string]interface{}); ok { if val, exists := config[key]; exists { rawValue = val } } } } } // Fall back to configDefaults if rawValue == nil { if defaults, ok := m.config["configDefaults"].(map[string]interface{}); ok { if val, exists := defaults[key]; exists { rawValue = val } } } if rawValue == nil { return defaultValue } // Resolve the value if we have a resolver var resolvedValue interface{} if m.resolver != nil { resolvedValue = m.resolver.Resolve(rawValue) } else { resolvedValue = rawValue } // Cache the resolved value m.resolvedCache[cacheKey] = resolvedValue return resolvedValue } // GetSecret retrieves a secret value for the current environment. func (m *Manager) GetSecret(key string, defaultValue interface{}) interface{} { m.mu.RLock() // Ensure config is loaded if m.config == nil || len(m.config) == 0 { // Need to upgrade to write lock to load config m.mu.RUnlock() m.mu.Lock() // Double-check after acquiring write lock if m.config == nil || len(m.config) == 0 { if err := m.loadConfig(); err != nil { // Config file not found is expected when all values // come from environment variables. _ = err m.mu.Unlock() return defaultValue } } // Downgrade back to read lock m.mu.Unlock() m.mu.RLock() } defer m.mu.RUnlock() if m.environment == "" { return defaultValue } // Get the current environment's config envMap, ok := m.config["environments"].(map[string]interface{}) if !ok { return defaultValue } env, ok := envMap[m.environment].(map[string]interface{}) if !ok { return defaultValue } secrets, ok := env["secrets"].(map[string]interface{}) if !ok { return defaultValue } secretValue, exists := secrets[key] if !exists { return defaultValue } // Resolve the value if m.resolver != nil { resolved := m.resolver.Resolve(secretValue) if resolved == nil { return defaultValue } return resolved } return secretValue } // getConfigValue is an internal helper to get config values without locking. func (m *Manager) getConfigValue(key string, defaultValue interface{}) interface{} { // Try environment-specific config first var rawValue interface{} if m.environment != "" { envMap, ok := m.config["environments"].(map[string]interface{}) if ok { if env, ok := envMap[m.environment].(map[string]interface{}); ok { if config, ok := env["config"].(map[string]interface{}); ok { if val, exists := config[key]; exists { rawValue = val } } } } } // Fall back to configDefaults if rawValue == nil { if defaults, ok := m.config["configDefaults"].(map[string]interface{}); ok { if val, exists := defaults[key]; exists { rawValue = val } } } if rawValue == nil { return defaultValue } return rawValue } // Reload reloads the configuration from file. func (m *Manager) Reload() error { m.mu.Lock() defer m.mu.Unlock() return m.loadConfig() } // GetAllConfig returns all configuration values for the current environment. func (m *Manager) GetAllConfig() map[string]interface{} { m.mu.RLock() defer m.mu.RUnlock() result := make(map[string]interface{}) // Start with configDefaults if defaults, ok := m.config["configDefaults"].(map[string]interface{}); ok { for k, v := range defaults { if m.resolver != nil { result[k] = m.resolver.Resolve(v) } else { result[k] = v } } } // Override with environment-specific config if m.environment != "" { envMap, ok := m.config["environments"].(map[string]interface{}) if ok { if env, ok := envMap[m.environment].(map[string]interface{}); ok { if config, ok := env["config"].(map[string]interface{}); ok { for k, v := range config { if m.resolver != nil { result[k] = m.resolver.Resolve(v) } else { result[k] = v } } } } } } return result } // GetAllSecrets returns all secrets for the current environment. func (m *Manager) GetAllSecrets() map[string]interface{} { m.mu.RLock() defer m.mu.RUnlock() if m.environment == "" { return make(map[string]interface{}) } envMap, ok := m.config["environments"].(map[string]interface{}) if !ok { return make(map[string]interface{}) } env, ok := envMap[m.environment].(map[string]interface{}) if !ok { return make(map[string]interface{}) } secrets, ok := env["secrets"].(map[string]interface{}) if !ok { return make(map[string]interface{}) } // Resolve all secrets result := make(map[string]interface{}) for k, v := range secrets { if m.resolver != nil { result[k] = m.resolver.Resolve(v) } else { result[k] = v } } return result }