package v1 import ( "fmt" "os" "path/filepath" "strings" "testing" "github.com/wild-cloud/wild-central/internal/config" "github.com/wild-cloud/wild-central/internal/haproxy" "github.com/wild-cloud/wild-central/internal/services" ) // TestReconciliation_HAProxyConfigFromServices verifies that the reconciliation // logic correctly maps registered services to HAProxy configuration. This // replicates the route-building logic from reconcileNetworking() and asserts // on the generated config without requiring haproxy or dnsmasq binaries. func TestReconciliation_HAProxyConfigFromServices(t *testing.T) { api, _ := setupTestAPI(t) // Register a mix of services like a real deployment testServices := []services.Service{ { Domain: "cloud.payne.io", Source: "wild-cloud", Backend: services.Backend{Address: "192.168.8.240:443", Type: services.BackendTCPPassthrough}, Subdomains: true, Public: true, }, { Domain: "payne.io", Source: "wild-cloud", Backend: services.Backend{Address: "192.168.8.240:443", Type: services.BackendTCPPassthrough}, Subdomains: false, Public: true, }, { Domain: "wild-cloud.payne.io", Source: "wild-works", Backend: services.Backend{Address: "127.0.0.1:5055", Type: services.BackendHTTP}, // Public defaults to false }, { Domain: "my-api.payne.io", Source: "wild-works", Backend: services.Backend{Address: "192.168.8.60:9001", Type: services.BackendHTTP, Health: "/health"}, // Public defaults to false }, } for _, svc := range testServices { if err := api.services.Register(svc); err != nil { t.Fatalf("Register %s failed: %v", svc.Domain, err) } } // Replicate reconcileNetworking route-building logic svcs, err := api.services.List() if err != nil { t.Fatalf("List failed: %v", err) } var instanceRoutes []haproxy.InstanceRoute var httpRoutes []haproxy.HTTPRoute for _, svc := range svcs { switch svc.Backend.Type { case services.BackendTCPPassthrough: instanceRoutes = append(instanceRoutes, haproxy.InstanceRoute{ Name: svc.Domain, Domain: svc.Domain, BackendIP: extractHost(svc.Backend.Address), Subdomains: svc.Subdomains, }) case services.BackendHTTP: httpRoutes = append(httpRoutes, haproxy.HTTPRoute{ Name: svc.Domain, Domain: svc.Domain, Backend: svc.Backend.Address, HealthPath: svc.Backend.Health, }) } } // Central registers itself as a service; add it to the test data if err := api.services.Register(services.Service{ Domain: "central.payne.io", Source: "central", Backend: services.Backend{Address: "127.0.0.1:5055", Type: services.BackendHTTP}, // Public defaults to false TLS: services.TLSTerminate, }); err != nil { t.Fatalf("Register central failed: %v", err) } // Re-list to pick up Central svcs, err = api.services.List() if err != nil { t.Fatalf("List failed: %v", err) } httpRoutes = nil for _, svc := range svcs { if svc.Backend.Type == services.BackendHTTP { httpRoutes = append(httpRoutes, haproxy.HTTPRoute{ Name: svc.Domain, Domain: svc.Domain, Backend: svc.Backend.Address, HealthPath: svc.Backend.Health, }) } } cfg := api.haproxy.GenerateWithOpts(instanceRoutes, nil, haproxy.GenerateOpts{ HTTPRoutes: httpRoutes, CertsDir: "/etc/haproxy/certs/", }) // --- Assertions on HAProxy config --- // L7 services have exact-match ACLs in the SNI frontend if !strings.Contains(cfg, "acl is_l7_central_payne_io req_ssl_sni -m str central.payne.io") { t.Errorf("expected L7 exact-match ACL for central.payne.io:\n%s", cfg) } if !strings.Contains(cfg, "acl is_l7_wild_cloud_payne_io req_ssl_sni -m str wild-cloud.payne.io") { t.Errorf("expected L7 exact-match ACL for wild-cloud.payne.io:\n%s", cfg) } // L4 services with subdomains:true have wildcard ACLs if !strings.Contains(cfg, "req_ssl_sni -m end .cloud.payne.io") { t.Errorf("expected L4 wildcard ACL for cloud.payne.io (subdomains:true):\n%s", cfg) } // L4 services with subdomains:false have exact-match only if !strings.Contains(cfg, "req_ssl_sni -m str payne.io") { t.Errorf("expected L4 exact-match ACL for payne.io:\n%s", cfg) } if strings.Contains(cfg, "req_ssl_sni -m end .payne.io") { t.Errorf("payne.io (subdomains:false) must NOT have wildcard ACL:\n%s", cfg) } // Central's config domain appears as an L7 route if !strings.Contains(cfg, "hdr(host) -i central.payne.io") { t.Errorf("expected Host header ACL for central.payne.io in L7 frontend:\n%s", cfg) } // L7 ACLs appear BEFORE L4 wildcard ACLs l7Pos := strings.Index(cfg, "is_l7_central_payne_io") l4WildcardPos := strings.Index(cfg, "req_ssl_sni -m end .cloud.payne.io") if l7Pos > l4WildcardPos { t.Errorf("L7 exact matches (pos %d) must appear before L4 wildcards (pos %d)", l7Pos, l4WildcardPos) } // L4 exact (payne.io) appears before L4 wildcard (*.cloud.payne.io) l4ExactPos := strings.Index(cfg, "acl is_payne_io req_ssl_sni -m str payne.io") if l4ExactPos < 0 { t.Fatalf("expected L4 exact ACL for payne.io:\n%s", cfg) } if l4ExactPos > l4WildcardPos { t.Errorf("L4 exact match (pos %d) must come before L4 wildcard (pos %d)", l4ExactPos, l4WildcardPos) } // Health check path is present for my-api if !strings.Contains(cfg, "option httpchk GET /health") { t.Errorf("expected health check for my-api:\n%s", cfg) } } // TestReconciliation_DnsmasqConfigFromServices verifies that reconciliation // correctly generates dnsmasq config from registered services, including // the critical reach-based local=/ directives. func TestReconciliation_DnsmasqConfigFromServices(t *testing.T) { api, _ := setupTestAPI(t) centralIP := "192.168.8.151" // Register services with different reach and backend types testServices := []services.Service{ { // tcp-passthrough, public → DNS points to k8s LB IP, no local=/ Domain: "cloud.payne.io", Source: "wild-cloud", Backend: services.Backend{Address: "192.168.8.240:443", Type: services.BackendTCPPassthrough}, Subdomains: true, Public: true, }, { // http, internal → DNS points to Central IP, has local=/ Domain: "my-api.payne.io", Source: "wild-works", Backend: services.Backend{Address: "192.168.8.60:9001", Type: services.BackendHTTP}, // Public defaults to false }, { // http, public → DNS points to Central IP, NO local=/ Domain: "public-app.payne.io", Source: "wild-works", Backend: services.Backend{Address: "192.168.8.60:8080", Type: services.BackendHTTP}, Public: true, }, } for _, svc := range testServices { if err := api.services.Register(svc); err != nil { t.Fatalf("Register %s failed: %v", svc.Domain, err) } } // Build dnsmasq instance configs the same way reconcileNetworking does svcs, err := api.services.List() if err != nil { t.Fatalf("List failed: %v", err) } globalCfg := &config.GlobalConfig{} var instanceConfigs []config.InstanceConfig for _, svc := range svcs { if svc.Domain == "" { continue } dnsIP := centralIP if svc.Backend.Type == services.BackendTCPPassthrough { dnsIP = extractHost(svc.Backend.Address) } ic := config.InstanceConfig{} ic.Cluster.LoadBalancerIp = dnsIP if !svc.Public { ic.Cloud.InternalDomain = svc.Domain } else { ic.Cloud.Domain = svc.Domain } instanceConfigs = append(instanceConfigs, ic) } dnsmasqCfg := api.dnsmasq.Generate(globalCfg, instanceConfigs) // --- TCP passthrough (public) → backend IP --- if !strings.Contains(dnsmasqCfg, "address=/cloud.payne.io/192.168.8.240") { t.Errorf("tcp-passthrough service must point DNS to backend IP (192.168.8.240):\n%s", dnsmasqCfg) } // --- HTTP services → Central IP --- if !strings.Contains(dnsmasqCfg, "address=/my-api.payne.io/192.168.8.151") { t.Errorf("http/internal service must point DNS to Central IP (192.168.8.151):\n%s", dnsmasqCfg) } if !strings.Contains(dnsmasqCfg, "address=/public-app.payne.io/192.168.8.151") { t.Errorf("http/public service must point DNS to Central IP (192.168.8.151):\n%s", dnsmasqCfg) } // --- reach:internal → local=/ present --- if !strings.Contains(dnsmasqCfg, "local=/my-api.payne.io/") { t.Errorf("reach:internal service must have local=/ entry:\n%s", dnsmasqCfg) } // --- reach:public → NO local=/ --- if strings.Contains(dnsmasqCfg, "local=/cloud.payne.io/") { t.Errorf("reach:public service must NOT have local=/ entry:\n%s", dnsmasqCfg) } if strings.Contains(dnsmasqCfg, "local=/public-app.payne.io/") { t.Errorf("reach:public service must NOT have local=/ entry:\n%s", dnsmasqCfg) } } // TestReconciliation_CentralDomainInHAProxy verifies that Central, registered // as a service, produces correct HAProxy L7 routes. func TestReconciliation_CentralDomainInHAProxy(t *testing.T) { api, _ := setupTestAPI(t) centralPort := 15055 // Register Central as a service (as EnsureCentralService does) if err := api.services.Register(services.Service{ Domain: "central.payne.io", Source: "central", Backend: services.Backend{Address: fmt.Sprintf("127.0.0.1:%d", centralPort), Type: services.BackendHTTP}, // Public defaults to false TLS: services.TLSTerminate, }); err != nil { t.Fatalf("Register central failed: %v", err) } // Register another HTTP service if err := api.services.Register(services.Service{ Domain: "my-app.payne.io", Source: "wild-works", Backend: services.Backend{Address: "192.168.8.60:9001", Type: services.BackendHTTP}, // Public defaults to false }); err != nil { t.Fatalf("Register failed: %v", err) } svcs, _ := api.services.List() var httpRoutes []haproxy.HTTPRoute for _, svc := range svcs { if svc.Backend.Type == services.BackendHTTP { httpRoutes = append(httpRoutes, haproxy.HTTPRoute{ Name: svc.Domain, Domain: svc.Domain, Backend: svc.Backend.Address, }) } } cfg := api.haproxy.GenerateWithOpts(nil, nil, haproxy.GenerateOpts{ HTTPRoutes: httpRoutes, }) // Central domain should be present as both SNI ACL and Host header ACL if !strings.Contains(cfg, "req_ssl_sni -m str central.payne.io") { t.Errorf("expected Central domain SNI ACL:\n%s", cfg) } if !strings.Contains(cfg, "hdr(host) -i central.payne.io") { t.Errorf("expected Central domain Host ACL:\n%s", cfg) } // Central backend must use the configured port (15055) if !strings.Contains(cfg, fmt.Sprintf("server s0 127.0.0.1:%d", centralPort)) { t.Errorf("Central backend must use port %d:\n%s", centralPort, cfg) } } // TestReconciliation_TCPPassthroughDNSTarget verifies that tcp-passthrough // services get DNS pointing to the backend IP, not Central's IP. func TestReconciliation_TCPPassthroughDNSTarget(t *testing.T) { api, _ := setupTestAPI(t) centralIP := "192.168.8.151" // Register a k8s instance with tcp-passthrough if err := api.services.Register(services.Service{ Domain: "cloud.payne.io", Source: "wild-cloud", Backend: services.Backend{Address: "192.168.8.240:443", Type: services.BackendTCPPassthrough}, Subdomains: true, Public: true, }); err != nil { t.Fatalf("Register failed: %v", err) } svcs, _ := api.services.List() globalCfg := &config.GlobalConfig{} var instanceConfigs []config.InstanceConfig for _, svc := range svcs { dnsIP := centralIP if svc.Backend.Type == services.BackendTCPPassthrough { dnsIP = extractHost(svc.Backend.Address) } ic := config.InstanceConfig{} ic.Cluster.LoadBalancerIp = dnsIP if !svc.Public { ic.Cloud.InternalDomain = svc.Domain } else { ic.Cloud.Domain = svc.Domain } instanceConfigs = append(instanceConfigs, ic) } dnsmasqCfg := api.dnsmasq.Generate(globalCfg, instanceConfigs) // TCP passthrough → DNS points to backend (k8s LB), NOT central if !strings.Contains(dnsmasqCfg, "address=/cloud.payne.io/192.168.8.240") { t.Errorf("tcp-passthrough DNS must point to backend IP 192.168.8.240:\n%s", dnsmasqCfg) } if strings.Contains(dnsmasqCfg, "address=/cloud.payne.io/192.168.8.151") { t.Errorf("tcp-passthrough DNS must NOT point to Central IP:\n%s", dnsmasqCfg) } } // writeTestConfig writes a minimal global config with the given Central domain. func writeTestConfig(t *testing.T, dataDir, centralDomain string) { t.Helper() configPath := filepath.Join(dataDir, "config.yaml") content := fmt.Sprintf("cloud:\n central:\n domain: %s\n", centralDomain) if err := os.WriteFile(configPath, []byte(content), 0644); err != nil { t.Fatalf("write config: %v", err) } } func TestEnsureCentralService_RegistersWhenDomainConfigured(t *testing.T) { api, dataDir := setupTestAPI(t) api.SetPort(15055) writeTestConfig(t, dataDir, "central.example.com") api.EnsureCentralService() svc, err := api.services.Get("central.example.com") if err != nil { t.Fatalf("expected Central service to be registered: %v", err) } if svc.Source != "central" { t.Errorf("source = %q, want %q", svc.Source, "central") } if svc.Backend.Address != "127.0.0.1:15055" { t.Errorf("backend = %q, want %q", svc.Backend.Address, "127.0.0.1:15055") } if svc.TLS != services.TLSTerminate { t.Errorf("tls = %q, want %q", svc.TLS, services.TLSTerminate) } } func TestEnsureCentralService_Idempotent(t *testing.T) { api, dataDir := setupTestAPI(t) api.SetPort(15055) writeTestConfig(t, dataDir, "central.example.com") api.EnsureCentralService() api.EnsureCentralService() // second call should be a no-op svcs, _ := api.services.List() count := 0 for _, s := range svcs { if s.Source == "central" { count++ } } if count != 1 { t.Errorf("expected 1 central service, got %d", count) } } func TestEnsureCentralService_CleansUpOnDomainChange(t *testing.T) { api, dataDir := setupTestAPI(t) api.SetPort(15055) // Register with old domain writeTestConfig(t, dataDir, "old.example.com") api.EnsureCentralService() // Change domain writeTestConfig(t, dataDir, "new.example.com") api.EnsureCentralService() // Old domain should be gone if _, err := api.services.Get("old.example.com"); err == nil { t.Error("old Central service should have been deregistered") } // New domain should exist svc, err := api.services.Get("new.example.com") if err != nil { t.Fatalf("new Central service should be registered: %v", err) } if svc.Source != "central" { t.Errorf("source = %q, want %q", svc.Source, "central") } } func TestEnsureCentralService_NoDomainIsNoop(t *testing.T) { api, _ := setupTestAPI(t) api.SetPort(15055) // Default config has no Central domain api.EnsureCentralService() svcs, _ := api.services.List() for _, s := range svcs { if s.Source == "central" { t.Error("no Central service should be registered when domain is empty") } } }