package v1 import ( "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, Reach: services.ReachPublic, }, { Domain: "payne.io", Source: "wild-cloud", Backend: services.Backend{Address: "192.168.8.240:443", Type: services.BackendTCPPassthrough}, Subdomains: false, Reach: services.ReachPublic, }, { Domain: "wild-cloud.payne.io", Source: "wild-works", Backend: services.Backend{Address: "127.0.0.1:5055", Type: services.BackendHTTP}, Reach: services.ReachInternal, }, { Domain: "my-api.payne.io", Source: "wild-works", Backend: services.Backend{Address: "192.168.8.60:9001", Type: services.BackendHTTP, Health: "/health"}, Reach: services.ReachInternal, }, } 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, }) } } // Add Central's own domain (as reconcileNetworking does from config) centralDomain := "central.payne.io" httpRoutes = append([]haproxy.HTTPRoute{{ Name: "wild-central", Domain: centralDomain, Backend: "127.0.0.1:5055", }}, httpRoutes...) 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_wild_central 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_wild_central") 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, Reach: services.ReachPublic, }, { // 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}, Reach: services.ReachInternal, }, { // 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}, Reach: services.ReachPublic, }, } 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.Reach == services.ReachInternal { 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 the Central domain // from global config is injected as the first L7 HTTP route. func TestReconciliation_CentralDomainInHAProxy(t *testing.T) { api, _ := setupTestAPI(t) // Register one 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}, Reach: services.ReachInternal, }); 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, }) } } // Central domain goes first (as reconcileNetworking does) centralDomain := "central.payne.io" httpRoutes = append([]haproxy.HTTPRoute{{ Name: "wild-central", Domain: centralDomain, Backend: "127.0.0.1:5055", }}, httpRoutes...) 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 route should be the first L7 route centralPos := strings.Index(cfg, "is_l7_wild_central") appPos := strings.Index(cfg, "is_l7_my_app_payne_io") if centralPos < 0 || appPos < 0 { t.Fatalf("expected both L7 ACLs in config:\n%s", cfg) } if centralPos > appPos { t.Errorf("Central domain (pos %d) should appear before app domain (pos %d)", centralPos, appPos) } } // 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, Reach: services.ReachPublic, }); 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.Reach == services.ReachInternal { 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) } }