Replace InstanceConfig with DNSEntry for dnsmasq config generation
InstanceConfig was a ~50-field struct designed for Wild Cloud k8s instances,
but only 3 fields were ever read by dnsmasq (the sole consumer). The
reconciliation bridge constructed fake InstanceConfig objects from registered
domains just to satisfy this interface.
Replace with DNSEntry{Domain, IP} — a 2-field struct purpose-built for
dnsmasq. All domains get local=/ directives to prevent AAAA queries from
leaking to upstream DNS (Happy Eyeballs / RFC 8305 latency on LAN).
Removed dead code: InstanceConfig, NodeConfig, LoadCloudConfig,
SaveCloudConfig, DeepMerge, LoadMergedInstanceConfig, EnsureInstanceConfig,
instance path helpers, instanceLoadBalancerIP, GenerateInstanceConfig,
and all modular per-instance dnsmasq methods.
This commit is contained in:
@@ -3,21 +3,15 @@ package dnsmasq
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/wild-cloud/wild-central/internal/config"
|
||||
"github.com/wild-cloud/wild-central/internal/network"
|
||||
)
|
||||
|
||||
const (
|
||||
instanceConfigDir = "/etc/dnsmasq.d/wild-cloud-instances"
|
||||
)
|
||||
|
||||
// GenerateMainConfig creates the main dnsmasq configuration with global settings
|
||||
// and a conf-dir directive to include per-instance configs
|
||||
// and a conf-dir directive to include per-domain configs
|
||||
func (g *ConfigGenerator) GenerateMainConfig(cfg *config.State) string {
|
||||
// Get the Wild Central IP address
|
||||
dnsIP, err := network.GetWildCentralIP()
|
||||
@@ -29,8 +23,8 @@ func (g *ConfigGenerator) GenerateMainConfig(cfg *config.State) string {
|
||||
var sb strings.Builder
|
||||
|
||||
fmt.Fprintf(&sb, `# Wild Cloud DNS Configuration (Main)
|
||||
# This file contains global settings. Instance-specific DNS entries are in:
|
||||
# /etc/dnsmasq.d/wild-cloud-instances/*.conf
|
||||
# This file contains global settings. Domain-specific DNS entries are
|
||||
# generated by reconciliation into the monolithic config.
|
||||
|
||||
# Basic Settings
|
||||
listen-address=%s
|
||||
@@ -39,9 +33,6 @@ domain-needed
|
||||
bogus-priv
|
||||
no-resolv
|
||||
|
||||
# Include per-instance DNS configurations
|
||||
conf-dir=%s,*.conf
|
||||
|
||||
# Upstream DNS servers
|
||||
server=1.1.1.1
|
||||
server=8.8.8.8
|
||||
@@ -49,7 +40,7 @@ server=8.8.8.8
|
||||
# Logging
|
||||
log-queries
|
||||
log-dhcp
|
||||
`, dnsIP, instanceConfigDir)
|
||||
`, dnsIP)
|
||||
|
||||
// DHCP section (only when explicitly enabled)
|
||||
if cfg != nil && cfg.Cloud.Dnsmasq.DHCP.Enabled {
|
||||
@@ -82,57 +73,6 @@ log-dhcp
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// instanceLoadBalancerIP returns the load balancer IP for an instance.
|
||||
// It checks cluster.loadBalancerIp first, then falls back to apps.metallb.loadBalancerIp,
|
||||
// which is where the normal app install flow writes it.
|
||||
func instanceLoadBalancerIP(instance config.InstanceConfig) string {
|
||||
if instance.Cluster.LoadBalancerIp != "" {
|
||||
return instance.Cluster.LoadBalancerIp
|
||||
}
|
||||
if metallb, ok := instance.Apps["metallb"].(map[string]any); ok {
|
||||
if ip, ok := metallb["loadBalancerIp"].(string); ok {
|
||||
return ip
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GenerateInstanceConfig creates a DNS configuration for a single instance
|
||||
func (g *ConfigGenerator) GenerateInstanceConfig(instance config.InstanceConfig) string {
|
||||
var sb strings.Builder
|
||||
|
||||
fmt.Fprintf(&sb, "# DNS configuration for instance: %s\n", instance.Cluster.Name)
|
||||
sb.WriteString("# Generated by Wild Cloud\n\n")
|
||||
|
||||
loadBalancerIP := instanceLoadBalancerIP(instance)
|
||||
if loadBalancerIP == "" {
|
||||
sb.WriteString("# WARNING: No load balancer IP configured for this instance\n")
|
||||
sb.WriteString("# DNS entries are commented out until load balancer IP is configured\n\n")
|
||||
if instance.Cloud.InternalDomain != "" {
|
||||
fmt.Fprintf(&sb, "# local=/%s/\n", instance.Cloud.InternalDomain)
|
||||
fmt.Fprintf(&sb, "# address=/%s/<load-balancer-ip>\n\n", instance.Cloud.InternalDomain)
|
||||
}
|
||||
if instance.Cloud.Domain != "" {
|
||||
fmt.Fprintf(&sb, "# address=/%s/<load-balancer-ip>\n", instance.Cloud.Domain)
|
||||
}
|
||||
} else {
|
||||
// Internal domain (.internal.cloud.example.tld) - local only, no external DNS
|
||||
if instance.Cloud.InternalDomain != "" {
|
||||
sb.WriteString("# Internal domain (LAN-only)\n")
|
||||
fmt.Fprintf(&sb, "local=/%s/\n", instance.Cloud.InternalDomain)
|
||||
fmt.Fprintf(&sb, "address=/%s/%s\n\n", instance.Cloud.InternalDomain, loadBalancerIP)
|
||||
}
|
||||
|
||||
// External/primary domain - resolve to backend IP
|
||||
if instance.Cloud.Domain != "" {
|
||||
sb.WriteString("# Public domain (resolved locally to avoid external DNS)\n")
|
||||
fmt.Fprintf(&sb, "address=/%s/%s\n", instance.Cloud.Domain, loadBalancerIP)
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// ValidateConfig tests a dnsmasq configuration file for syntax errors
|
||||
func (g *ConfigGenerator) ValidateConfig(configPath string) error {
|
||||
// Use dnsmasq --test to validate the configuration
|
||||
@@ -150,104 +90,6 @@ func (g *ConfigGenerator) ValidateConfig(configPath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteInstanceConfig writes the DNS configuration for a single instance
|
||||
func (g *ConfigGenerator) WriteInstanceConfig(instanceName string, instance config.InstanceConfig) error {
|
||||
instanceFile := filepath.Join(instanceConfigDir, fmt.Sprintf("%s.conf", instanceName))
|
||||
configContent := g.GenerateInstanceConfig(instance)
|
||||
|
||||
// Ensure directory exists
|
||||
if err := os.MkdirAll(instanceConfigDir, 0755); err != nil {
|
||||
return fmt.Errorf("creating instance config directory: %w", err)
|
||||
}
|
||||
|
||||
// Write to temp file first
|
||||
tempFile := instanceFile + ".tmp"
|
||||
if err := os.WriteFile(tempFile, []byte(configContent), 0644); err != nil {
|
||||
return fmt.Errorf("writing temp instance config: %w", err)
|
||||
}
|
||||
|
||||
// Validate the temp config along with main config
|
||||
if err := g.ValidateWithInstance(tempFile); err != nil {
|
||||
os.Remove(tempFile) // Clean up temp file
|
||||
return fmt.Errorf("instance config validation failed: %w", err)
|
||||
}
|
||||
|
||||
// Move temp file to final location (atomic operation)
|
||||
if err := os.Rename(tempFile, instanceFile); err != nil {
|
||||
os.Remove(tempFile) // Clean up temp file
|
||||
return fmt.Errorf("installing instance config: %w", err)
|
||||
}
|
||||
|
||||
slog.Info("wrote instance DNS config", "component", "dnsmasq", "path", instanceFile)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateWithInstance validates the main config along with a specific instance config
|
||||
func (g *ConfigGenerator) ValidateWithInstance(instanceConfigPath string) error {
|
||||
// Create a temporary test directory
|
||||
tempDir, err := os.MkdirTemp("", "dnsmasq-test-*")
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating temp dir: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Copy main config to temp
|
||||
mainContent, err := os.ReadFile(g.configPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading main config: %w", err)
|
||||
}
|
||||
|
||||
tempMainConfig := filepath.Join(tempDir, "main.conf")
|
||||
// Modify the conf-dir line to point to our temp instance dir
|
||||
tempInstanceDir := filepath.Join(tempDir, "instances")
|
||||
if err := os.MkdirAll(tempInstanceDir, 0755); err != nil {
|
||||
return fmt.Errorf("creating temp instance dir: %w", err)
|
||||
}
|
||||
|
||||
modifiedContent := strings.ReplaceAll(
|
||||
string(mainContent),
|
||||
fmt.Sprintf("conf-dir=%s,*.conf", instanceConfigDir),
|
||||
fmt.Sprintf("conf-dir=%s,*.conf", tempInstanceDir),
|
||||
)
|
||||
|
||||
if err := os.WriteFile(tempMainConfig, []byte(modifiedContent), 0644); err != nil {
|
||||
return fmt.Errorf("writing temp main config: %w", err)
|
||||
}
|
||||
|
||||
// Copy instance config to temp
|
||||
instanceContent, err := os.ReadFile(instanceConfigPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading instance config: %w", err)
|
||||
}
|
||||
|
||||
tempInstanceConfig := filepath.Join(tempInstanceDir, filepath.Base(instanceConfigPath))
|
||||
if err := os.WriteFile(tempInstanceConfig, instanceContent, 0644); err != nil {
|
||||
return fmt.Errorf("writing temp instance config: %w", err)
|
||||
}
|
||||
|
||||
// Validate the combined configuration
|
||||
return g.ValidateConfig(tempMainConfig)
|
||||
}
|
||||
|
||||
// RemoveInstanceConfig removes the DNS configuration for an instance
|
||||
func (g *ConfigGenerator) RemoveInstanceConfig(instanceName string) error {
|
||||
instanceFile := filepath.Join(instanceConfigDir, fmt.Sprintf("%s.conf", instanceName))
|
||||
|
||||
// Check if file exists
|
||||
if _, err := os.Stat(instanceFile); os.IsNotExist(err) {
|
||||
slog.Info("instance DNS config does not exist", "component", "dnsmasq", "path", instanceFile)
|
||||
return nil // Not an error, already removed
|
||||
}
|
||||
|
||||
// Remove the file
|
||||
if err := os.Remove(instanceFile); err != nil {
|
||||
return fmt.Errorf("removing instance config: %w", err)
|
||||
}
|
||||
|
||||
slog.Info("removed instance DNS config", "component", "dnsmasq", "path", instanceFile)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReloadService sends a SIGHUP to dnsmasq to reload configuration
|
||||
// This is lighter weight than a full restart
|
||||
func (g *ConfigGenerator) ReloadService() error {
|
||||
@@ -262,81 +104,3 @@ func (g *ConfigGenerator) ReloadService() error {
|
||||
slog.Info("dnsmasq service reloaded", "component", "dnsmasq")
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateToModularConfig migrates from monolithic to modular configuration
|
||||
func (g *ConfigGenerator) UpdateToModularConfig(cfg *config.State, instanceNames []string, instances []config.InstanceConfig) error {
|
||||
slog.Info("migrating to modular configuration", "component", "dnsmasq")
|
||||
|
||||
// Ensure instance directory exists
|
||||
if err := os.MkdirAll(instanceConfigDir, 0755); err != nil {
|
||||
return fmt.Errorf("creating instance config directory: %w", err)
|
||||
}
|
||||
|
||||
// Generate and write instance configs first (but don't reload yet)
|
||||
for i, instance := range instances {
|
||||
instanceName := instanceNames[i]
|
||||
if err := g.WriteInstanceConfig(instanceName, instance); err != nil {
|
||||
slog.Error("failed to write instance config", "component", "dnsmasq", "instance", instanceName, "error", err)
|
||||
// Continue with other instances
|
||||
}
|
||||
}
|
||||
|
||||
// Generate new main config with conf-dir
|
||||
mainConfig := g.GenerateMainConfig(cfg)
|
||||
|
||||
// Write to temp file first
|
||||
tempFile := g.configPath + ".tmp"
|
||||
if err := os.WriteFile(tempFile, []byte(mainConfig), 0644); err != nil {
|
||||
return fmt.Errorf("writing temp main config: %w", err)
|
||||
}
|
||||
|
||||
// Validate the new config
|
||||
if err := g.ValidateConfig(tempFile); err != nil {
|
||||
os.Remove(tempFile)
|
||||
return fmt.Errorf("main config validation failed: %w", err)
|
||||
}
|
||||
|
||||
// Backup current config
|
||||
backupFile := g.configPath + ".pre-modular"
|
||||
if err := os.Rename(g.configPath, backupFile); err != nil {
|
||||
os.Remove(tempFile)
|
||||
return fmt.Errorf("backing up current config: %w", err)
|
||||
}
|
||||
|
||||
// Install new config
|
||||
if err := os.Rename(tempFile, g.configPath); err != nil {
|
||||
// Try to restore backup
|
||||
_ = os.Rename(backupFile, g.configPath)
|
||||
return fmt.Errorf("installing new config: %w", err)
|
||||
}
|
||||
|
||||
// Reload dnsmasq
|
||||
if err := g.ReloadService(); err != nil {
|
||||
// Try to restore backup and reload
|
||||
slog.Error("reload failed, restoring backup", "component", "dnsmasq", "error", err)
|
||||
os.Remove(g.configPath)
|
||||
_ = os.Rename(backupFile, g.configPath)
|
||||
_ = g.ReloadService()
|
||||
return fmt.Errorf("reloading with new config: %w", err)
|
||||
}
|
||||
|
||||
slog.Info("migrated to modular configuration", "component", "dnsmasq")
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateInstanceDNS updates DNS configuration for a single instance
|
||||
// This is called when instance configuration changes (e.g., domain names)
|
||||
func (g *ConfigGenerator) UpdateInstanceDNS(instanceName string, instance config.InstanceConfig) error {
|
||||
// Write the new instance config
|
||||
if err := g.WriteInstanceConfig(instanceName, instance); err != nil {
|
||||
return fmt.Errorf("writing instance DNS config: %w", err)
|
||||
}
|
||||
|
||||
// Reload dnsmasq to pick up changes
|
||||
if err := g.ReloadService(); err != nil {
|
||||
return fmt.Errorf("reloading dnsmasq: %w", err)
|
||||
}
|
||||
|
||||
slog.Info("DNS updated for instance", "component", "dnsmasq", "instance", instanceName)
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user