fix: Prevent broken dnsmasq entries for services without internal domain
Services registered via the service API have a domain but no internal domain (that's a Wild Cloud instance concept). The dnsmasq generator was producing broken entries like `local=//` and `address=//`. Fix: skip internal domain entries when InternalDomain is empty, in both config.go and config_modular.go. Also fix DNS entries to point to Central's IP (where HAProxy listens) rather than the backend address — all traffic flows through Central. Added 3 tests covering: service-only, mixed services+instances. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/wild-cloud/wild-central/internal/certbot"
|
"github.com/wild-cloud/wild-central/internal/certbot"
|
||||||
"github.com/wild-cloud/wild-central/internal/config"
|
"github.com/wild-cloud/wild-central/internal/config"
|
||||||
"github.com/wild-cloud/wild-central/internal/haproxy"
|
"github.com/wild-cloud/wild-central/internal/haproxy"
|
||||||
|
"github.com/wild-cloud/wild-central/internal/network"
|
||||||
"github.com/wild-cloud/wild-central/internal/services"
|
"github.com/wild-cloud/wild-central/internal/services"
|
||||||
"github.com/wild-cloud/wild-central/internal/sse"
|
"github.com/wild-cloud/wild-central/internal/sse"
|
||||||
)
|
)
|
||||||
@@ -81,8 +82,14 @@ func (api *API) reconcileNetworking() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate dnsmasq DNS entries from registered services
|
// Generate dnsmasq DNS entries from registered services.
|
||||||
// Build instance configs for backward compatibility with dnsmasq generator
|
// All service domains resolve to Central's IP (where HAProxy listens).
|
||||||
|
// HAProxy then routes to the actual backend based on SNI or Host header.
|
||||||
|
centralIP, _ := network.GetWildCentralIP()
|
||||||
|
if centralIP == "" {
|
||||||
|
centralIP = "127.0.0.1"
|
||||||
|
}
|
||||||
|
|
||||||
var instanceConfigs []config.InstanceConfig
|
var instanceConfigs []config.InstanceConfig
|
||||||
for _, svc := range svcs {
|
for _, svc := range svcs {
|
||||||
if svc.Reach == services.ReachOff || svc.Domain == "" {
|
if svc.Reach == services.ReachOff || svc.Domain == "" {
|
||||||
@@ -90,7 +97,7 @@ func (api *API) reconcileNetworking() {
|
|||||||
}
|
}
|
||||||
ic := config.InstanceConfig{}
|
ic := config.InstanceConfig{}
|
||||||
ic.Cloud.Domain = svc.Domain
|
ic.Cloud.Domain = svc.Domain
|
||||||
ic.Cluster.LoadBalancerIp = extractHost(svc.Backend.Address)
|
ic.Cluster.LoadBalancerIp = centralIP // DNS points to Central, not the backend
|
||||||
instanceConfigs = append(instanceConfigs, ic)
|
instanceConfigs = append(instanceConfigs, ic)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,12 +57,15 @@ func (g *ConfigGenerator) Generate(cfg *config.GlobalConfig, clouds []config.Ins
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Internal domain (.internal.cloud.example.tld) - local only, no external DNS
|
// Internal domain (.internal.cloud.example.tld) - local only, no external DNS
|
||||||
|
if cloud.Cloud.InternalDomain != "" {
|
||||||
resolution_section += fmt.Sprintf("local=/%s/\naddress=/%s/%s\n", cloud.Cloud.InternalDomain, cloud.Cloud.InternalDomain, loadBalancerIP)
|
resolution_section += fmt.Sprintf("local=/%s/\naddress=/%s/%s\n", cloud.Cloud.InternalDomain, cloud.Cloud.InternalDomain, loadBalancerIP)
|
||||||
|
}
|
||||||
|
|
||||||
// External domain (cloud.example.tld) - resolve to load balancer IP without external DNS lookup
|
// External/primary domain - resolve to backend IP
|
||||||
// This makes LAN traffic go directly to load balancer instead of routing through external DNS first
|
if cloud.Cloud.Domain != "" {
|
||||||
resolution_section += fmt.Sprintf("address=/%s/%s\n", cloud.Cloud.Domain, loadBalancerIP)
|
resolution_section += fmt.Sprintf("address=/%s/%s\n", cloud.Cloud.Domain, loadBalancerIP)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
template := `# Configuration file for dnsmasq.
|
template := `# Configuration file for dnsmasq.
|
||||||
|
|
||||||
|
|||||||
@@ -117,14 +117,18 @@ func (g *ConfigGenerator) GenerateInstanceConfig(instance config.InstanceConfig)
|
|||||||
fmt.Fprintf(&sb, "# address=/%s/<load-balancer-ip>\n", instance.Cloud.Domain)
|
fmt.Fprintf(&sb, "# address=/%s/<load-balancer-ip>\n", instance.Cloud.Domain)
|
||||||
} else {
|
} else {
|
||||||
// Internal domain (.internal.cloud.example.tld) - local only, no external DNS
|
// Internal domain (.internal.cloud.example.tld) - local only, no external DNS
|
||||||
|
if instance.Cloud.InternalDomain != "" {
|
||||||
sb.WriteString("# Internal domain (LAN-only)\n")
|
sb.WriteString("# Internal domain (LAN-only)\n")
|
||||||
fmt.Fprintf(&sb, "local=/%s/\n", instance.Cloud.InternalDomain)
|
fmt.Fprintf(&sb, "local=/%s/\n", instance.Cloud.InternalDomain)
|
||||||
fmt.Fprintf(&sb, "address=/%s/%s\n\n", instance.Cloud.InternalDomain, loadBalancerIP)
|
fmt.Fprintf(&sb, "address=/%s/%s\n\n", instance.Cloud.InternalDomain, loadBalancerIP)
|
||||||
|
}
|
||||||
|
|
||||||
// External domain (cloud.example.tld) - resolve to load balancer IP
|
// External/primary domain - resolve to backend IP
|
||||||
|
if instance.Cloud.Domain != "" {
|
||||||
sb.WriteString("# Public domain (resolved locally to avoid external DNS)\n")
|
sb.WriteString("# Public domain (resolved locally to avoid external DNS)\n")
|
||||||
fmt.Fprintf(&sb, "address=/%s/%s\n", instance.Cloud.Domain, loadBalancerIP)
|
fmt.Fprintf(&sb, "address=/%s/%s\n", instance.Cloud.Domain, loadBalancerIP)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -310,3 +310,71 @@ func TestNewConfigGenerator_DefaultPath(t *testing.T) {
|
|||||||
t.Errorf("got %q, want /etc/dnsmasq.d/wild-cloud.conf", g.GetConfigPath())
|
t.Errorf("got %q, want /etc/dnsmasq.d/wild-cloud.conf", g.GetConfigPath())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test: Service registration — domain set but no internal domain
|
||||||
|
func TestGenerate_ServiceWithoutInternalDomain(t *testing.T) {
|
||||||
|
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
|
||||||
|
globalCfg := &config.GlobalConfig{}
|
||||||
|
|
||||||
|
// Service registration: only domain + backend IP, no internal domain
|
||||||
|
inst := instanceWith("my-api.payne.io", "", "192.168.8.151")
|
||||||
|
out := g.Generate(globalCfg, []config.InstanceConfig{inst})
|
||||||
|
|
||||||
|
// Should have address entry for the domain
|
||||||
|
if !strings.Contains(out, "address=/my-api.payne.io/192.168.8.151") {
|
||||||
|
t.Errorf("expected address entry for service domain, got:\n%s", out)
|
||||||
|
}
|
||||||
|
// Must NOT have empty local=// entry
|
||||||
|
if strings.Contains(out, "local=//") {
|
||||||
|
t.Errorf("unexpected empty local=// in output:\n%s", out)
|
||||||
|
}
|
||||||
|
// Must NOT have empty address=// entry
|
||||||
|
if strings.Contains(out, "address=//") {
|
||||||
|
t.Errorf("unexpected empty address=// in output:\n%s", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test: GenerateInstanceConfig — service without internal domain
|
||||||
|
func TestGenerateInstanceConfig_ServiceWithoutInternalDomain(t *testing.T) {
|
||||||
|
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
|
||||||
|
inst := instanceWith("central.payne.io", "", "192.168.8.151")
|
||||||
|
|
||||||
|
out := g.GenerateInstanceConfig(inst)
|
||||||
|
|
||||||
|
if !strings.Contains(out, "address=/central.payne.io/192.168.8.151") {
|
||||||
|
t.Errorf("expected address entry, got:\n%s", out)
|
||||||
|
}
|
||||||
|
if strings.Contains(out, "local=//") {
|
||||||
|
t.Errorf("unexpected empty local=// in output:\n%s", out)
|
||||||
|
}
|
||||||
|
if strings.Contains(out, "address=//") {
|
||||||
|
t.Errorf("unexpected empty address=// in output:\n%s", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test: Mix of instances with and without internal domains
|
||||||
|
func TestGenerate_MixedServicesAndInstances(t *testing.T) {
|
||||||
|
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
|
||||||
|
globalCfg := &config.GlobalConfig{}
|
||||||
|
|
||||||
|
instances := []config.InstanceConfig{
|
||||||
|
instanceWith("cloud.payne.io", "internal.cloud.payne.io", "192.168.8.240"), // k8s instance
|
||||||
|
instanceWith("central.payne.io", "", "192.168.8.151"), // service (no internal domain)
|
||||||
|
instanceWith("wild-cloud.payne.io", "", "192.168.8.151"), // service (no internal domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
out := g.Generate(globalCfg, instances)
|
||||||
|
|
||||||
|
// k8s instance should have both local= and address= for internal domain
|
||||||
|
if !strings.Contains(out, "local=/internal.cloud.payne.io/") {
|
||||||
|
t.Errorf("expected local=/ for k8s internal domain, got:\n%s", out)
|
||||||
|
}
|
||||||
|
// Services should have address= entries
|
||||||
|
if !strings.Contains(out, "address=/central.payne.io/192.168.8.151") {
|
||||||
|
t.Errorf("expected address for central, got:\n%s", out)
|
||||||
|
}
|
||||||
|
// No broken empty entries
|
||||||
|
if strings.Contains(out, "local=//") {
|
||||||
|
t.Errorf("unexpected empty local=// in output:\n%s", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user