package config import ( "bytes" "fmt" "log/slog" "os/exec" "path/filepath" "strings" "github.com/wild-cloud/wild-central/internal/network" "github.com/wild-cloud/wild-central/internal/storage" ) // YQ provides a wrapper around the yq command-line tool type YQ struct { yqPath string } // NewYQ creates a new YQ wrapper func NewYQ() *YQ { path, err := exec.LookPath("yq") if err != nil { path = "yq" } return &YQ{yqPath: path} } // Get retrieves a value from a YAML file using a yq expression func (y *YQ) Get(filePath, expression string) (string, error) { cmd := exec.Command(y.yqPath, expression, filePath) var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr if err := cmd.Run(); err != nil { return "", fmt.Errorf("yq get failed: %w, stderr: %s", err, stderr.String()) } return strings.TrimSpace(stdout.String()), nil } // Set sets a value in a YAML file using a yq expression func (y *YQ) Set(filePath, expression, value string) error { if !strings.HasPrefix(expression, ".") { expression = "." + expression } quotedValue := fmt.Sprintf(`"%s"`, strings.ReplaceAll(value, `"`, `\"`)) setExpr := fmt.Sprintf("%s = %s", expression, quotedValue) cmd := exec.Command(y.yqPath, "-i", setExpr, filePath) var stderr bytes.Buffer cmd.Stderr = &stderr if err := cmd.Run(); err != nil { return fmt.Errorf("yq set failed: %w, stderr: %s", err, stderr.String()) } return nil } // Delete removes a key from a YAML file func (y *YQ) Delete(filePath, expression string) error { delExpr := fmt.Sprintf("del(%s)", expression) cmd := exec.Command(y.yqPath, "-i", delExpr, filePath) var stderr bytes.Buffer cmd.Stderr = &stderr if err := cmd.Run(); err != nil { return fmt.Errorf("yq delete failed: %w, stderr: %s", err, stderr.String()) } return nil } // Validate checks if a YAML file is valid func (y *YQ) Validate(filePath string) error { cmd := exec.Command(y.yqPath, "eval", ".", filePath) var stderr bytes.Buffer cmd.Stderr = &stderr if err := cmd.Run(); err != nil { return fmt.Errorf("yaml validation failed: %w, stderr: %s", err, stderr.String()) } return nil } // Manager handles configuration file operations with idempotency type Manager struct { yq *YQ } // NewManager creates a new config manager func NewManager() *Manager { return &Manager{ yq: NewYQ(), } } // EnsureGlobalConfig ensures a global config file exists with proper structure func (m *Manager) EnsureGlobalConfig(dataDir string) error { configPath := filepath.Join(dataDir, "config.yaml") // Check if config already exists if storage.FileExists(configPath) { // Validate existing config if err := m.yq.Validate(configPath); err != nil { return fmt.Errorf("invalid config file: %w", err) } return nil } // Create config structure with detected defaults initialConfig := &GlobalConfig{} // Detect network configuration netInfo, err := network.DetectNetworkInfo() if err != nil { slog.Info("network detection failed, using defaults", "component", "config", "error", err) } else { // Set detected values initialConfig.Cloud.Router.IP = netInfo.Gateway slog.Info("detected network", "component", "config", "gateway", netInfo.Gateway, "interface", netInfo.PrimaryInterface) } // Ensure data directory exists if err := storage.EnsureDir(dataDir, 0755); err != nil { return err } // Save config using the model's save function return SaveGlobalConfig(initialConfig, configPath) } // EnsureInstanceConfig ensures an instance config file exists with proper structure func (m *Manager) EnsureInstanceConfig(name string, dataDir string) error { configPath := filepath.Join(dataDir, "instances", name, "config", "instance.yaml") // Check if config already exists if storage.FileExists(configPath) { // Validate existing config if err := m.yq.Validate(configPath); err != nil { return fmt.Errorf("invalid config file: %w", err) } return nil } // Ensure config directory exists if err := storage.EnsureDir(filepath.Join(dataDir, "instances", name, "config"), 0755); err != nil { return err } initialConfig := &InstanceConfig{} initialConfig.Cluster.Name = name initialConfig.Cluster.Nodes.Active = make(map[string]NodeConfig) initialConfig.Apps = make(map[string]any) return SaveCloudConfig(initialConfig, configPath) } // GetConfigValue retrieves a value from a config file func (m *Manager) GetConfigValue(configPath, key string) (string, error) { if !storage.FileExists(configPath) { return "", fmt.Errorf("config file not found: %s", configPath) } value, err := m.yq.Get(configPath, fmt.Sprintf(".%s", key)) if err != nil { return "", fmt.Errorf("getting config value %s: %w", key, err) } return value, nil } // SetConfigValue sets a value in a config file func (m *Manager) SetConfigValue(configPath, key, value string) error { if !storage.FileExists(configPath) { return fmt.Errorf("config file not found: %s", configPath) } // Acquire lock before modifying lockPath := configPath + ".lock" return storage.WithLock(lockPath, func() error { return m.yq.Set(configPath, fmt.Sprintf(".%s", key), value) }) } // EnsureConfigValue sets a value only if it's not already set (idempotent) func (m *Manager) EnsureConfigValue(configPath, key, value string) error { if !storage.FileExists(configPath) { return fmt.Errorf("config file not found: %s", configPath) } // Check if value already set currentValue, err := m.GetConfigValue(configPath, key) if err == nil && currentValue != "" && currentValue != "null" { // Value already set, skip return nil } // Set the value return m.SetConfigValue(configPath, key, value) } // ValidateConfig validates a config file func (m *Manager) ValidateConfig(configPath string) error { if !storage.FileExists(configPath) { return fmt.Errorf("config file not found: %s", configPath) } return m.yq.Validate(configPath) } // CopyConfig copies a config file to a new location func (m *Manager) CopyConfig(srcPath, dstPath string) error { if !storage.FileExists(srcPath) { return fmt.Errorf("source config file not found: %s", srcPath) } // Read source content, err := storage.ReadFile(srcPath) if err != nil { return err } // Ensure destination directory exists if err := storage.EnsureDir(filepath.Dir(dstPath), 0755); err != nil { return err } // Write destination return storage.WriteFile(dstPath, content, 0644) } // GetInstanceConfigPath returns the path to an instance's config file func GetInstanceConfigPath(dataDir, instanceName string) string { return filepath.Join(dataDir, "instances", instanceName, "config", "instance.yaml") } // GetInstanceSecretsPath returns the path to an instance's secrets file func GetInstanceSecretsPath(dataDir, instanceName string) string { return filepath.Join(dataDir, "instances", instanceName, "config", "secrets.yaml") } // GetInstancePath returns the path to an instance directory func GetInstancePath(dataDir, instanceName string) string { return filepath.Join(dataDir, "instances", instanceName) }