374 lines
8.5 KiB
Go
374 lines
8.5 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"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 {
|
|
log.Printf("Failed to load config: %v", 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 {
|
|
log.Printf("Failed to load config: %v", err)
|
|
m.mu.Unlock()
|
|
return defaultValue
|
|
}
|
|
}
|
|
// Downgrade back to read lock
|
|
m.mu.Unlock()
|
|
m.mu.RLock()
|
|
}
|
|
defer m.mu.RUnlock()
|
|
|
|
if m.environment == "" {
|
|
log.Printf("No environment set when getting secret '%s'", key)
|
|
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
|
|
}
|