package config import ( "fmt" "os" "path/filepath" "strconv" "strings" "gopkg.in/yaml.v3" ) // Manager handles configuration and secrets files type Manager struct { configPath string secretsPath string } // NewManager creates a new configuration manager func NewManager(configPath, secretsPath string) *Manager { return &Manager{ configPath: configPath, secretsPath: secretsPath, } } // Get retrieves a value from the config file using dot-notation path func (m *Manager) Get(path string) (interface{}, error) { return m.getValue(m.configPath, path) } // Set sets a value in the config file using dot-notation path func (m *Manager) Set(path, value string) error { return m.setValue(m.configPath, path, value) } // GetSecret retrieves a value from the secrets file using dot-notation path func (m *Manager) GetSecret(path string) (interface{}, error) { return m.getValue(m.secretsPath, path) } // SetSecret sets a value in the secrets file using dot-notation path func (m *Manager) SetSecret(path, value string) error { return m.setValue(m.secretsPath, path, value) } // getValue retrieves a value from a YAML file using dot-notation path func (m *Manager) getValue(filePath, path string) (interface{}, error) { // Check if file exists if _, err := os.Stat(filePath); os.IsNotExist(err) { return nil, fmt.Errorf("file not found: %s", filePath) } // Read file data, err := os.ReadFile(filePath) if err != nil { return nil, fmt.Errorf("reading file: %w", err) } // Parse YAML var yamlData interface{} if err := yaml.Unmarshal(data, &yamlData); err != nil { return nil, fmt.Errorf("parsing YAML: %w", err) } // Navigate to the specified path value, err := m.navigatePath(yamlData, path) if err != nil { return nil, err } return value, nil } // setValue sets a value in a YAML file using dot-notation path func (m *Manager) setValue(filePath, path, value string) error { // Ensure directory exists if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil { return fmt.Errorf("creating directory: %w", err) } // Read existing file or create empty structure var yamlData interface{} if data, err := os.ReadFile(filePath); err == nil { if err := yaml.Unmarshal(data, &yamlData); err != nil { return fmt.Errorf("parsing existing YAML: %w", err) } } else if !os.IsNotExist(err) { return fmt.Errorf("reading file: %w", err) } // If no existing data, start with empty map if yamlData == nil { yamlData = make(map[string]interface{}) } // Parse the value as YAML to handle different types var parsedValue interface{} if err := yaml.Unmarshal([]byte(value), &parsedValue); err != nil { // If it fails to parse as YAML, treat as string parsedValue = value } // Set the value at the specified path if err := m.setValueAtPath(yamlData, path, parsedValue); err != nil { return fmt.Errorf("setting value at path: %w", err) } // Marshal back to YAML data, err := yaml.Marshal(yamlData) if err != nil { return fmt.Errorf("marshaling YAML: %w", err) } // Write file if err := os.WriteFile(filePath, data, 0600); err != nil { return fmt.Errorf("writing file: %w", err) } return nil } // navigatePath navigates through a nested data structure using dot-notation path func (m *Manager) navigatePath(data interface{}, path string) (interface{}, error) { if path == "" { return data, nil } parts := m.parsePath(path) current := data for _, part := range parts { if part.isArray { // Handle array access like "items[0]" slice, ok := current.([]interface{}) if !ok { return nil, fmt.Errorf("path component %s is not an array", part.key) } if part.index < 0 || part.index >= len(slice) { return nil, fmt.Errorf("array index %d out of range for %s", part.index, part.key) } current = slice[part.index] } else { // Handle map access m, ok := current.(map[string]interface{}) if !ok { return nil, fmt.Errorf("path component %s is not a map", part.key) } var exists bool current, exists = m[part.key] if !exists { return nil, nil // Key not found } } } return current, nil } // setValueAtPath sets a value at the specified path, creating nested structures as needed func (m *Manager) setValueAtPath(data interface{}, path string, value interface{}) error { if path == "" { return fmt.Errorf("empty path") } parts := m.parsePath(path) current := data // Navigate to the parent of the target for _, part := range parts[:len(parts)-1] { if part.isArray { slice, ok := current.([]interface{}) if !ok { return fmt.Errorf("path component %s is not an array", part.key) } if part.index < 0 || part.index >= len(slice) { return fmt.Errorf("array index %d out of range for %s", part.index, part.key) } current = slice[part.index] } else { m, ok := current.(map[string]interface{}) if !ok { return fmt.Errorf("path component %s is not a map", part.key) } next, exists := m[part.key] if !exists { // Create new map for next level next = make(map[string]interface{}) m[part.key] = next } current = next } } // Set the final value finalPart := parts[len(parts)-1] if finalPart.isArray { return fmt.Errorf("cannot set array element directly") } else { m, ok := current.(map[string]interface{}) if !ok { return fmt.Errorf("cannot set value on non-map") } m[finalPart.key] = value } return nil } // pathPart represents a single component in a dot-notation path type pathPart struct { key string isArray bool index int } // parsePath parses a dot-notation path into components func (m *Manager) parsePath(path string) []pathPart { var parts []pathPart components := strings.Split(path, ".") for _, component := range components { if strings.Contains(component, "[") && strings.Contains(component, "]") { // Handle array syntax like "items[0]" openBracket := strings.Index(component, "[") closeBracket := strings.Index(component, "]") key := component[:openBracket] indexStr := component[openBracket+1 : closeBracket] if key != "" { parts = append(parts, pathPart{ key: key, isArray: false, }) } if index, err := strconv.Atoi(indexStr); err == nil { parts = append(parts, pathPart{ key: key, isArray: true, index: index, }) } } else { parts = append(parts, pathPart{ key: component, isArray: false, }) } } return parts } // LoadConfig loads the entire config file as a map func (m *Manager) LoadConfig() (map[string]interface{}, error) { data, err := m.getValue(m.configPath, "") if err != nil { return nil, err } if data == nil { return make(map[string]interface{}), nil } configMap, ok := data.(map[string]interface{}) if !ok { return nil, fmt.Errorf("config file is not a valid YAML map") } return configMap, nil } // LoadSecrets loads the entire secrets file as a map func (m *Manager) LoadSecrets() (map[string]interface{}, error) { data, err := m.getValue(m.secretsPath, "") if err != nil { return nil, err } if data == nil { return make(map[string]interface{}), nil } secretsMap, ok := data.(map[string]interface{}) if !ok { return nil, fmt.Errorf("secrets file is not a valid YAML map") } return secretsMap, nil }