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

@@ -8,6 +8,7 @@ import (
"testing"
"github.com/wild-cloud/wild-central/internal/config"
"github.com/wild-cloud/wild-central/internal/dnsmasq"
"github.com/wild-cloud/wild-central/internal/domains"
"github.com/wild-cloud/wild-central/internal/haproxy"
)
@@ -205,14 +206,14 @@ func TestReconciliation_DnsmasqConfigFromDomains(t *testing.T) {
}
}
// Build dnsmasq instance configs the same way reconcileNetworking does
// Build dnsmasq entries the same way reconcileNetworking does
doms, err := api.domains.List()
if err != nil {
t.Fatalf("List failed: %v", err)
}
globalCfg := &config.State{}
var instanceConfigs []config.InstanceConfig
var dnsEntries []dnsmasq.DNSEntry
for _, dom := range doms {
if dom.DomainName == "" {
@@ -224,19 +225,13 @@ func TestReconciliation_DnsmasqConfigFromDomains(t *testing.T) {
dnsIP = extractHost(dom.Backend.Address)
}
ic := config.InstanceConfig{}
ic.Cluster.LoadBalancerIp = dnsIP
if !dom.Public {
ic.Cloud.InternalDomain = dom.DomainName
} else {
ic.Cloud.Domain = dom.DomainName
}
instanceConfigs = append(instanceConfigs, ic)
dnsEntries = append(dnsEntries, dnsmasq.DNSEntry{
Domain: dom.DomainName,
IP: dnsIP,
})
}
dnsmasqCfg := api.dnsmasq.Generate(globalCfg, instanceConfigs)
dnsmasqCfg := api.dnsmasq.Generate(globalCfg, dnsEntries)
// --- TCP passthrough (public) → backend IP ---
if !strings.Contains(dnsmasqCfg, "address=/cloud.payne.io/192.168.8.240") {
@@ -251,17 +246,15 @@ func TestReconciliation_DnsmasqConfigFromDomains(t *testing.T) {
t.Errorf("http/public domain must point DNS to Central IP (192.168.8.151):\n%s", dnsmasqCfg)
}
// --- internal → local=/ present ---
// --- All domains get local=/ to prevent AAAA leaking upstream (Happy Eyeballs) ---
if !strings.Contains(dnsmasqCfg, "local=/my-api.payne.io/") {
t.Errorf("internal domain must have local=/ entry:\n%s", dnsmasqCfg)
t.Errorf("domain must have local=/ entry:\n%s", dnsmasqCfg)
}
// --- public → NO local=/ ---
if strings.Contains(dnsmasqCfg, "local=/cloud.payne.io/") {
t.Errorf("public domain must NOT have local=/ entry:\n%s", dnsmasqCfg)
if !strings.Contains(dnsmasqCfg, "local=/cloud.payne.io/") {
t.Errorf("domain must have local=/ entry (prevents AAAA leaking):\n%s", dnsmasqCfg)
}
if strings.Contains(dnsmasqCfg, "local=/public-app.payne.io/") {
t.Errorf("public domain must NOT have local=/ entry:\n%s", dnsmasqCfg)
if !strings.Contains(dnsmasqCfg, "local=/public-app.payne.io/") {
t.Errorf("domain must have local=/ entry (prevents AAAA leaking):\n%s", dnsmasqCfg)
}
}
@@ -345,24 +338,19 @@ func TestReconciliation_TCPPassthroughDNSTarget(t *testing.T) {
doms, _ := api.domains.List()
globalCfg := &config.State{}
var instanceConfigs []config.InstanceConfig
var dnsEntries []dnsmasq.DNSEntry
for _, dom := range doms {
dnsIP := centralIP
if dom.Backend.Type == domains.BackendTCPPassthrough {
dnsIP = extractHost(dom.Backend.Address)
}
ic := config.InstanceConfig{}
ic.Cluster.LoadBalancerIp = dnsIP
if !dom.Public {
ic.Cloud.InternalDomain = dom.DomainName
} else {
ic.Cloud.Domain = dom.DomainName
}
instanceConfigs = append(instanceConfigs, ic)
dnsEntries = append(dnsEntries, dnsmasq.DNSEntry{
Domain: dom.DomainName,
IP: dnsIP,
})
}
dnsmasqCfg := api.dnsmasq.Generate(globalCfg, instanceConfigs)
dnsmasqCfg := api.dnsmasq.Generate(globalCfg, dnsEntries)
// TCP passthrough → DNS points to backend (k8s LB), NOT central
if !strings.Contains(dnsmasqCfg, "address=/cloud.payne.io/192.168.8.240") {
@@ -440,7 +428,7 @@ func TestReconciliation_DNSOnlyNoGateway(t *testing.T) {
// Build DNS config — dns-only should point to its backend IP
globalCfg := &config.State{}
var instanceConfigs []config.InstanceConfig
var dnsEntries []dnsmasq.DNSEntry
for _, dom := range doms {
dnsIP := centralIP
@@ -448,18 +436,13 @@ func TestReconciliation_DNSOnlyNoGateway(t *testing.T) {
if bt == domains.BackendTCPPassthrough || bt == domains.BackendDNSOnly {
dnsIP = extractHost(dom.Backend.Address)
}
ic := config.InstanceConfig{}
ic.Cluster.LoadBalancerIp = dnsIP
if !dom.Public {
ic.Cloud.InternalDomain = dom.DomainName
} else {
ic.Cloud.Domain = dom.DomainName
}
instanceConfigs = append(instanceConfigs, ic)
dnsEntries = append(dnsEntries, dnsmasq.DNSEntry{
Domain: dom.DomainName,
IP: dnsIP,
})
}
dnsmasqCfg := api.dnsmasq.Generate(globalCfg, instanceConfigs)
dnsmasqCfg := api.dnsmasq.Generate(globalCfg, dnsEntries)
// dns-only domain should resolve to its backend IP (192.168.8.222)
if !strings.Contains(dnsmasqCfg, "address=/dev.payne.io/192.168.8.222") {