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:
2026-07-11 20:42:33 +00:00
parent 1f3fcf50b4
commit f9d87ff975
10 changed files with 192 additions and 1517 deletions

View File

@@ -3,7 +3,6 @@ package config
import (
"encoding/json"
"fmt"
"log/slog"
"os"
"path/filepath"
"strings"
@@ -158,168 +157,3 @@ func (c *State) IsEmpty() bool {
return c.Cloud.Central.Domain == "" && c.Operator.Email == ""
}
type NodeConfig struct {
Role string `yaml:"role" json:"role"`
Interface string `yaml:"interface" json:"interface"`
Disk string `yaml:"disk" json:"disk"`
CurrentIp string `yaml:"currentIp" json:"currentIp"`
}
type InstanceConfig struct {
Operator struct {
Email string `yaml:"email" json:"email"`
} `yaml:"operator" json:"operator"`
Cloud struct {
BaseDomain string `yaml:"baseDomain" json:"baseDomain"`
Domain string `yaml:"domain" json:"domain"`
InternalDomain string `yaml:"internalDomain" json:"internalDomain"`
DHCPRange string `yaml:"dhcpRange" json:"dhcpRange"`
NFS struct {
Host string `yaml:"host" json:"host"`
MediaPath string `yaml:"mediaPath" json:"mediaPath"`
StorageCapacity string `yaml:"storageCapacity" json:"storageCapacity"`
} `yaml:"nfs" json:"nfs"`
DockerRegistryHost string `yaml:"dockerRegistryHost" json:"dockerRegistryHost"`
} `yaml:"cloud" json:"cloud"`
Cluster struct {
Name string `yaml:"name" json:"name"`
LoadBalancerIp string `yaml:"loadBalancerIp" json:"loadBalancerIp"`
IpAddressPool string `yaml:"ipAddressPool" json:"ipAddressPool"`
HostnamePrefix string `yaml:"hostnamePrefix" json:"hostnamePrefix"`
CertManager struct {
Cloudflare struct {
Domain string `yaml:"domain" json:"domain"`
} `yaml:"cloudflare" json:"cloudflare"`
} `yaml:"certManager" json:"certManager"`
ExternalDns struct {
OwnerId string `yaml:"ownerId" json:"ownerId"`
} `yaml:"externalDns" json:"externalDns"`
InternalDns struct {
ExternalResolver string `yaml:"externalResolver" json:"externalResolver"`
} `yaml:"internalDns" json:"internalDns"`
DockerRegistry struct {
Storage string `yaml:"storage" json:"storage"`
} `yaml:"dockerRegistry" json:"dockerRegistry"`
Nodes struct {
Talos struct {
Version string `yaml:"version" json:"version"`
SchematicId string `yaml:"schematicId" json:"schematicId"`
} `yaml:"talos" json:"talos"`
Control struct {
Vip string `yaml:"vip" json:"vip"`
} `yaml:"control" json:"control"`
Active map[string]NodeConfig `yaml:"active" json:"active"`
} `yaml:"nodes" json:"nodes"`
} `yaml:"cluster" json:"cluster"`
Apps map[string]any `yaml:"apps" json:"apps"`
}
func LoadCloudConfig(configPath string) (*InstanceConfig, error) {
data, err := os.ReadFile(configPath)
if err != nil {
return nil, fmt.Errorf("reading config file %s: %w", configPath, err)
}
config := &InstanceConfig{}
if err := yaml.Unmarshal(data, config); err != nil {
return nil, fmt.Errorf("parsing config file: %w", err)
}
return config, nil
}
func SaveCloudConfig(config *InstanceConfig, configPath string) error {
// Ensure the directory exists
if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil {
return fmt.Errorf("creating config directory: %w", err)
}
data, err := yaml.Marshal(config)
if err != nil {
return fmt.Errorf("marshaling config: %w", err)
}
return os.WriteFile(configPath, data, 0644)
}
// DeepMerge recursively merges src into dst, with src values taking precedence.
// Nested maps are merged recursively; all other types are overwritten by src.
func DeepMerge(dst, src map[string]any) map[string]any {
result := make(map[string]any)
for k, v := range dst {
result[k] = v
}
for k, v := range src {
if srcMap, ok := v.(map[string]any); ok {
if dstMap, ok := result[k].(map[string]any); ok {
result[k] = DeepMerge(dstMap, srcMap)
continue
}
}
result[k] = v
}
return result
}
// LoadMergedInstanceConfig loads the global config as a base and merges the
// instance config on top. Returns the merged result as an untyped map suitable
// for passing to gomplate as template context.
// If the global config is missing, returns the instance config alone.
// Assembles apps.* from config/apps/*/config.yaml files.
func LoadMergedInstanceConfig(dataDir, instanceName string) (map[string]any, error) {
instanceConfigPath := filepath.Join(dataDir, "instances", instanceName, "config", "instance.yaml")
globalConfigPath := filepath.Join(dataDir, "state.yaml")
instanceData, err := os.ReadFile(instanceConfigPath)
if err != nil {
return nil, fmt.Errorf("reading instance config %s: %w", instanceConfigPath, err)
}
var instanceMap map[string]any
if err := yaml.Unmarshal(instanceData, &instanceMap); err != nil {
return nil, fmt.Errorf("parsing instance config: %w", err)
}
if instanceMap == nil {
instanceMap = make(map[string]any)
}
// Assemble apps.* map from per-app config files
appsConfigDir := filepath.Join(dataDir, "instances", instanceName, "config", "apps")
if entries, err := os.ReadDir(appsConfigDir); err == nil {
appsMap := make(map[string]any)
for _, entry := range entries {
if !entry.IsDir() {
continue
}
appName := entry.Name()
appConfigPath := filepath.Join(dataDir, "instances", instanceName, "config", "apps", appName, "config.yaml")
appData, err := os.ReadFile(appConfigPath)
if err != nil {
continue
}
var appConfig map[string]any
if err := yaml.Unmarshal(appData, &appConfig); err == nil && appConfig != nil {
appsMap[appName] = appConfig
}
}
if len(appsMap) > 0 {
instanceMap["apps"] = appsMap
}
}
globalData, err := os.ReadFile(globalConfigPath)
if err != nil {
if os.IsNotExist(err) {
slog.Warn("no global config found, using instance config only", "path", globalConfigPath)
return instanceMap, nil
}
return nil, fmt.Errorf("reading global config %s: %w", globalConfigPath, err)
}
var globalMap map[string]any
if err := yaml.Unmarshal(globalData, &globalMap); err != nil {
return nil, fmt.Errorf("parsing global config: %w", err)
}
return DeepMerge(globalMap, instanceMap), nil
}