Files
wild-central/internal/config/manager.go
Paul Payne beb643f76f feat: Extract Wild Central as standalone Go service
Extract the Central networking functionality from wild-cloud/api into
a standalone service. Wild Central manages DNS (dnsmasq), gateway
(HAProxy), firewall (nftables), VPN (WireGuard), TLS (certbot),
security (CrowdSec), and DDNS — all the network appliance concerns.

All Cloud-specific code (instances, clusters, nodes, apps, backups,
operations, kubectl/talosctl tooling) has been removed. The API struct
and route registration contain only Central endpoints. Tests updated
to match the new API signature.

Builds and all tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-08 23:31:16 +00:00

240 lines
6.9 KiB
Go

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)
}