feat: Fix HAProxy ACL ordering + Subdomains field
Rewrite HAProxy config generation with correct ACL ordering: 1. L7 HTTP exact matches first (central, wild-cloud app) 2. L4 exact matches (payne.io, civilsociety.dev) 3. L4 wildcard matches (*.cloud.payne.io) — only when subdomains:true 4. Default → L7 termination Key changes: - InstanceRoute: removed ExtraDomains, added Subdomains bool - GenerateOpts: removed CentralDomain (Central is now just another HTTPRoute injected by reconciliation), renamed WildcardCert→CertsDir - Central's domain comes from config, injected as HTTPRoute with the actual running port (no more hardcoded 5055) - Added API.SetPort() so reconciliation knows the running port - subdomains:false → exact match only (no *.domain shadowing) New tests: TestGenerate_WithSubdomains, TestGenerate_ACLOrdering (verifies L7 exact matches come before L4 wildcards). Fixes: BUG 1 (central routing to wrong port), BUG 2 (wild-cloud getting traefik cert), BUG 3 (*.payne.io too broad). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -93,11 +93,11 @@ func TestGenerate_NoInstances_NoHTTPSFrontend(t *testing.T) {
|
||||
func TestGenerate_WithInstance_SNIACLs(t *testing.T) {
|
||||
m := NewManager("")
|
||||
instances := []InstanceRoute{
|
||||
{Name: "payne-cloud", Domain: "cloud.payne.io", BackendIP: "192.168.8.20"},
|
||||
{Name: "payne-cloud", Domain: "cloud.payne.io", BackendIP: "192.168.8.20", Subdomains: true},
|
||||
}
|
||||
out := m.Generate(instances, nil)
|
||||
|
||||
// Both ACL lines for OR-matching (subdomain and exact)
|
||||
// With subdomains:true, both ACL lines for OR-matching
|
||||
if !strings.Contains(out, "req_ssl_sni -m end .cloud.payne.io") {
|
||||
t.Errorf("expected subdomain SNI ACL, got:\n%s", out)
|
||||
}
|
||||
@@ -146,31 +146,57 @@ func TestGenerate_MultipleInstances(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerate_WithExtraDomains(t *testing.T) {
|
||||
func TestGenerate_WithSubdomains(t *testing.T) {
|
||||
m := NewManager("")
|
||||
instances := []InstanceRoute{
|
||||
{
|
||||
Name: "payne-cloud",
|
||||
Domain: "cloud.payne.io",
|
||||
BackendIP: "192.168.8.20",
|
||||
ExtraDomains: []string{"payne.io", "mywildcloud.org"},
|
||||
},
|
||||
{Name: "test-cloud", Domain: "cloud2.payne.io", BackendIP: "192.168.8.30"},
|
||||
{Name: "payne-cloud", 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.Generate(instances, nil)
|
||||
|
||||
// Extra domains should add ACL entries under the same payne-cloud ACL name
|
||||
for _, domain := range []string{"payne.io", "mywildcloud.org"} {
|
||||
if !strings.Contains(out, "req_ssl_sni -m end ."+domain) {
|
||||
t.Errorf("expected subdomain ACL for %s, got:\n%s", domain, out)
|
||||
}
|
||||
if !strings.Contains(out, "req_ssl_sni -m str "+domain) {
|
||||
t.Errorf("expected exact ACL for %s, got:\n%s", domain, out)
|
||||
}
|
||||
// cloud.payne.io with subdomains:true should have wildcard ACL
|
||||
if !strings.Contains(out, "req_ssl_sni -m end .cloud.payne.io") {
|
||||
t.Errorf("expected wildcard ACL for cloud.payne.io, got:\n%s", out)
|
||||
}
|
||||
// Extra domains should route to payne-cloud, not test-cloud
|
||||
if !strings.Contains(out, "use_backend be_https_payne_cloud if is_payne_cloud") {
|
||||
t.Errorf("expected payne-cloud use_backend, got:\n%s", out)
|
||||
// payne.io with subdomains:false should have exact match only
|
||||
if !strings.Contains(out, "req_ssl_sni -m str payne.io") {
|
||||
t.Errorf("expected exact ACL for payne.io, got:\n%s", out)
|
||||
}
|
||||
// payne.io should NOT have wildcard match
|
||||
if strings.Contains(out, "req_ssl_sni -m end .payne.io") {
|
||||
t.Errorf("unexpected wildcard ACL for payne.io (subdomains:false), got:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerate_ACLOrdering(t *testing.T) {
|
||||
m := NewManager("")
|
||||
instances := []InstanceRoute{
|
||||
{Name: "payne-cloud", Domain: "cloud.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})
|
||||
|
||||
// L7 exact matches must appear BEFORE L4 wildcard matches
|
||||
l7Pos := strings.Index(out, "is_l7_wild_central")
|
||||
l4Pos := strings.Index(out, "is_payne_cloud")
|
||||
|
||||
if l7Pos < 0 {
|
||||
t.Fatalf("expected L7 ACL for wild-central, got:\n%s", out)
|
||||
}
|
||||
if l4Pos < 0 {
|
||||
t.Fatalf("expected L4 ACL for payne-cloud, got:\n%s", out)
|
||||
}
|
||||
if l7Pos > l4Pos {
|
||||
t.Errorf("L7 exact match (pos %d) must come before L4 wildcard (pos %d):\n%s", l7Pos, l4Pos, out)
|
||||
}
|
||||
|
||||
// wild-cloud.payne.io should NOT match payne-cloud's wildcard
|
||||
// (it should be caught by the L7 exact match first)
|
||||
if strings.Contains(out, "use_backend be_https_payne_cloud if is_l7_wild_cloud") {
|
||||
t.Errorf("wild-cloud.payne.io should route to L7, not payne-cloud")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,16 +231,16 @@ func TestGenerateWithOpts_HTTPRoutes_L7Frontend(t *testing.T) {
|
||||
}
|
||||
|
||||
out := m.GenerateWithOpts(nil, nil, GenerateOpts{
|
||||
HTTPRoutes: httpRoutes,
|
||||
WildcardCert: "/etc/haproxy/certs/wildcard.pem",
|
||||
HTTPRoutes: httpRoutes,
|
||||
CertsDir: "/etc/haproxy/certs/",
|
||||
})
|
||||
|
||||
// L7 TLS termination frontend should exist
|
||||
if !strings.Contains(out, "frontend l7_https") {
|
||||
t.Errorf("expected L7 frontend, got:\n%s", out)
|
||||
}
|
||||
if !strings.Contains(out, "ssl crt /etc/haproxy/certs/wildcard.pem") {
|
||||
t.Errorf("expected wildcard cert in L7 bind, got:\n%s", out)
|
||||
if !strings.Contains(out, "ssl crt /etc/haproxy/certs/") {
|
||||
t.Errorf("expected certs dir in L7 bind, got:\n%s", out)
|
||||
}
|
||||
|
||||
// Host-based ACLs
|
||||
|
||||
Reference in New Issue
Block a user