feat: Add comprehensive tests + fix startup reconciliation
Tests added (13 new): - HAProxy ACL ordering: L7 before L4 wildcard, L4 exact before L4 wildcard - HAProxy subdomains: false=exact only, true=wildcard+exact - Reconciliation integration: HAProxy config from services, DNS config from services, Central config domain injection, correct DNS target IPs - dnsmasq: reach:internal has local=/, reach:public does not - Services: idempotent registration, no duplicates in list Bug fix: - Run initial reconciliation on startup so Central's own domain (from config) gets HAProxy + DNS routes even with zero registered services. Previously, Central only got routes after the first service was registered, causing 503 on fresh restart. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -300,6 +300,114 @@ func TestGenerateWithOpts_MixedL4AndL7(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateWithOpts_ACLOrdering_L7BeforeL4Wildcard(t *testing.T) {
|
||||
m := NewManager("")
|
||||
|
||||
// Scenario: cloud.payne.io with subdomains:true is an L4 wildcard route.
|
||||
// wild-cloud.payne.io is an L7 HTTP service. The L7 exact match MUST
|
||||
// appear before the L4 wildcard, otherwise HAProxy matches
|
||||
// wild-cloud.payne.io against *.payne.io and sends it to the wrong backend.
|
||||
instances := []InstanceRoute{
|
||||
{Name: "payne-io", Domain: "payne.io", BackendIP: "192.168.8.240", Subdomains: true},
|
||||
}
|
||||
httpRoutes := []HTTPRoute{
|
||||
{Name: "wild-central", Domain: "central.payne.io", Backend: "127.0.0.1:15055"},
|
||||
{Name: "wild-cloud", Domain: "wild-cloud.payne.io", Backend: "127.0.0.1:5055"},
|
||||
}
|
||||
out := m.GenerateWithOpts(instances, nil, GenerateOpts{HTTPRoutes: httpRoutes})
|
||||
|
||||
// All L7 exact matches must appear before L4 wildcard matches
|
||||
l7Central := strings.Index(out, "is_l7_wild_central")
|
||||
l7WildCloud := strings.Index(out, "is_l7_wild_cloud")
|
||||
l4Wildcard := strings.Index(out, "req_ssl_sni -m end .payne.io")
|
||||
|
||||
if l7Central < 0 {
|
||||
t.Fatalf("expected L7 ACL for wild-central, got:\n%s", out)
|
||||
}
|
||||
if l7WildCloud < 0 {
|
||||
t.Fatalf("expected L7 ACL for wild-cloud, got:\n%s", out)
|
||||
}
|
||||
if l4Wildcard < 0 {
|
||||
t.Fatalf("expected L4 wildcard ACL for payne.io, got:\n%s", out)
|
||||
}
|
||||
|
||||
if l7Central > l4Wildcard {
|
||||
t.Errorf("L7 central.payne.io (pos %d) must appear before L4 *.payne.io wildcard (pos %d)", l7Central, l4Wildcard)
|
||||
}
|
||||
if l7WildCloud > l4Wildcard {
|
||||
t.Errorf("L7 wild-cloud.payne.io (pos %d) must appear before L4 *.payne.io wildcard (pos %d)", l7WildCloud, l4Wildcard)
|
||||
}
|
||||
|
||||
// Verify the config order overall: L7 exact → L4 wildcard → default
|
||||
defaultBackend := strings.Index(out, "default_backend be_l7_termination")
|
||||
if defaultBackend < 0 {
|
||||
t.Fatalf("expected default_backend for L7 termination, got:\n%s", out)
|
||||
}
|
||||
if l4Wildcard > defaultBackend {
|
||||
t.Errorf("L4 wildcard (pos %d) must appear before default_backend (pos %d)", l4Wildcard, defaultBackend)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateWithOpts_SubdomainsFalse_ExactOnly(t *testing.T) {
|
||||
m := NewManager("")
|
||||
instances := []InstanceRoute{
|
||||
{Name: "payne-io", Domain: "payne.io", BackendIP: "192.168.8.20", Subdomains: false},
|
||||
}
|
||||
out := m.GenerateWithOpts(instances, nil, GenerateOpts{})
|
||||
|
||||
// subdomains:false must produce ONLY -m str (exact), NOT -m end (wildcard)
|
||||
if !strings.Contains(out, "req_ssl_sni -m str payne.io") {
|
||||
t.Errorf("expected exact match ACL for payne.io, got:\n%s", out)
|
||||
}
|
||||
if strings.Contains(out, "req_ssl_sni -m end .payne.io") {
|
||||
t.Errorf("subdomains:false must NOT produce wildcard ACL, got:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateWithOpts_SubdomainsTrue_WildcardAndExact(t *testing.T) {
|
||||
m := NewManager("")
|
||||
instances := []InstanceRoute{
|
||||
{Name: "cloud-payne", Domain: "cloud.payne.io", BackendIP: "192.168.8.20", Subdomains: true},
|
||||
}
|
||||
out := m.GenerateWithOpts(instances, nil, GenerateOpts{})
|
||||
|
||||
// subdomains:true must produce BOTH -m end (wildcard) AND -m str (exact)
|
||||
if !strings.Contains(out, "req_ssl_sni -m end .cloud.payne.io") {
|
||||
t.Errorf("subdomains:true must produce wildcard ACL (-m end), got:\n%s", out)
|
||||
}
|
||||
if !strings.Contains(out, "req_ssl_sni -m str cloud.payne.io") {
|
||||
t.Errorf("subdomains:true must also produce exact ACL (-m str), got:\n%s", out)
|
||||
}
|
||||
|
||||
// Both ACLs should use the same backend
|
||||
if !strings.Contains(out, "use_backend be_https_cloud_payne if is_cloud_payne") {
|
||||
t.Errorf("expected use_backend for cloud_payne, got:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateWithOpts_ACLOrdering_L4ExactBeforeL4Wildcard(t *testing.T) {
|
||||
m := NewManager("")
|
||||
// payne.io exact (subdomains:false) must appear before cloud.payne.io wildcard (subdomains:true)
|
||||
instances := []InstanceRoute{
|
||||
{Name: "cloud-payne", Domain: "cloud.payne.io", BackendIP: "192.168.8.20", Subdomains: true},
|
||||
{Name: "payne-io", Domain: "payne.io", BackendIP: "192.168.8.20", Subdomains: false},
|
||||
}
|
||||
out := m.GenerateWithOpts(instances, nil, GenerateOpts{})
|
||||
|
||||
exactPos := strings.Index(out, "req_ssl_sni -m str payne.io")
|
||||
wildcardPos := strings.Index(out, "req_ssl_sni -m end .cloud.payne.io")
|
||||
|
||||
if exactPos < 0 {
|
||||
t.Fatalf("expected exact match ACL for payne.io, got:\n%s", out)
|
||||
}
|
||||
if wildcardPos < 0 {
|
||||
t.Fatalf("expected wildcard ACL for cloud.payne.io, got:\n%s", out)
|
||||
}
|
||||
if exactPos > wildcardPos {
|
||||
t.Errorf("L4 exact match (pos %d) must come before L4 wildcard (pos %d):\n%s", exactPos, wildcardPos, out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateWithOpts_BackwardCompatible(t *testing.T) {
|
||||
m := NewManager("")
|
||||
instances := []InstanceRoute{
|
||||
|
||||
Reference in New Issue
Block a user