Support routes.

This commit is contained in:
2026-07-10 05:21:43 +00:00
parent 5c26c7530a
commit 43253ca120
8 changed files with 597 additions and 85 deletions

View File

@@ -3,6 +3,8 @@ package haproxy
import (
"strings"
"testing"
"github.com/wild-cloud/wild-central/internal/services"
)
func TestNewManager_DefaultPath(t *testing.T) {
@@ -174,8 +176,8 @@ func TestGenerate_ACLOrdering(t *testing.T) {
{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"},
{Name: "wild-central", Domain: "central.payne.io", Routes: []HTTPRouteBackend{{Backend: "127.0.0.1:15055"}}},
{Name: "wild-cloud", Domain: "wild-cloud.payne.io", Routes: []HTTPRouteBackend{{Backend: "127.0.0.1:5055"}}},
}
out := m.GenerateWithOpts(instances, nil, GenerateOpts{HTTPRoutes: httpRoutes})
@@ -226,8 +228,8 @@ func TestGenerate_CustomRoute(t *testing.T) {
func TestGenerateWithOpts_HTTPRoutes_L7Frontend(t *testing.T) {
m := NewManager("")
httpRoutes := []HTTPRoute{
{Name: "my-api", Domain: "my-api.payne.io", Backend: "192.168.8.60:9001", HealthPath: "/health"},
{Name: "dashboard", Domain: "dashboard.payne.io", Backend: "192.168.8.60:8080"},
{Name: "my-api", Domain: "my-api.payne.io", Routes: []HTTPRouteBackend{{Backend: "192.168.8.60:9001", HealthPath: "/health"}}},
{Name: "dashboard", Domain: "dashboard.payne.io", Routes: []HTTPRouteBackend{{Backend: "192.168.8.60:8080"}}},
}
out := m.GenerateWithOpts(nil, nil, GenerateOpts{
@@ -252,7 +254,7 @@ func TestGenerateWithOpts_HTTPRoutes_L7Frontend(t *testing.T) {
}
// L7 backends
if !strings.Contains(out, "backend be_l7_my_api") {
if !strings.Contains(out, "backend be_l7_my_api_0") {
t.Errorf("expected L7 backend for my-api, got:\n%s", out)
}
if !strings.Contains(out, "server s0 192.168.8.60:9001 check") {
@@ -263,7 +265,7 @@ func TestGenerateWithOpts_HTTPRoutes_L7Frontend(t *testing.T) {
}
// Dashboard backend (no health check)
if !strings.Contains(out, "backend be_l7_dashboard") {
if !strings.Contains(out, "backend be_l7_dashboard_0") {
t.Errorf("expected L7 backend for dashboard, got:\n%s", out)
}
if !strings.Contains(out, "server s0 192.168.8.60:8080") {
@@ -277,7 +279,7 @@ func TestGenerateWithOpts_MixedL4AndL7(t *testing.T) {
{Name: "payne-cloud", Domain: "cloud.payne.io", BackendIP: "192.168.8.100"},
}
httpRoutes := []HTTPRoute{
{Name: "my-api", Domain: "my-api.payne.io", Backend: "192.168.8.60:9001"},
{Name: "my-api", Domain: "my-api.payne.io", Routes: []HTTPRouteBackend{{Backend: "192.168.8.60:9001"}}},
}
out := m.GenerateWithOpts(instances, nil, GenerateOpts{
@@ -291,7 +293,7 @@ func TestGenerateWithOpts_MixedL4AndL7(t *testing.T) {
if !strings.Contains(out, "frontend l7_https") {
t.Errorf("expected L7 frontend, got:\n%s", out)
}
if !strings.Contains(out, "backend be_l7_my_api") {
if !strings.Contains(out, "backend be_l7_my_api_0") {
t.Errorf("expected L7 backend for my-api, got:\n%s", out)
}
// L7 fallback should be present
@@ -311,8 +313,8 @@ func TestGenerateWithOpts_ACLOrdering_L7BeforeL4Wildcard(t *testing.T) {
{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"},
{Name: "wild-central", Domain: "central.payne.io", Routes: []HTTPRouteBackend{{Backend: "127.0.0.1:15055"}}},
{Name: "wild-cloud", Domain: "wild-cloud.payne.io", Routes: []HTTPRouteBackend{{Backend: "127.0.0.1:5055"}}},
}
out := m.GenerateWithOpts(instances, nil, GenerateOpts{HTTPRoutes: httpRoutes})
@@ -443,7 +445,7 @@ func TestGenerateWithOpts_OnlyHTTPRoutes(t *testing.T) {
m := NewManager("")
httpRoutes := []HTTPRoute{
{Name: "my-api", Domain: "api.example.com", Backend: "192.168.1.10:9001"},
{Name: "my-api", Domain: "api.example.com", Routes: []HTTPRouteBackend{{Backend: "192.168.1.10:9001"}}},
}
out := m.GenerateWithOpts(nil, nil, GenerateOpts{HTTPRoutes: httpRoutes})
@@ -457,7 +459,7 @@ func TestGenerateWithOpts_OnlyHTTPRoutes(t *testing.T) {
t.Errorf("expected L7 frontend for HTTP routes, got:\n%s", out)
}
// L7 backend should be present
if !strings.Contains(out, "backend be_l7_my_api") {
if !strings.Contains(out, "backend be_l7_my_api_0") {
t.Errorf("expected L7 backend for my-api, got:\n%s", out)
}
// No L4 instance backends
@@ -497,7 +499,7 @@ func TestGenerateWithOpts_CertsDir(t *testing.T) {
m := NewManager("")
httpRoutes := []HTTPRoute{
{Name: "app", Domain: "app.example.com", Backend: "127.0.0.1:8080"},
{Name: "app", Domain: "app.example.com", Routes: []HTTPRouteBackend{{Backend: "127.0.0.1:8080"}}},
}
out := m.GenerateWithOpts(nil, nil, GenerateOpts{
@@ -514,7 +516,7 @@ func TestGenerateWithOpts_DefaultCertsDir(t *testing.T) {
m := NewManager("")
httpRoutes := []HTTPRoute{
{Name: "app", Domain: "app.example.com", Backend: "127.0.0.1:8080"},
{Name: "app", Domain: "app.example.com", Routes: []HTTPRouteBackend{{Backend: "127.0.0.1:8080"}}},
}
// Empty CertsDir should default to /etc/haproxy/certs/
@@ -556,7 +558,7 @@ func TestGenerateWithOpts_HealthCheckInL7Backend(t *testing.T) {
m := NewManager("")
httpRoutes := []HTTPRoute{
{Name: "api", Domain: "api.example.com", Backend: "10.0.0.5:8080", HealthPath: "/healthz"},
{Name: "api", Domain: "api.example.com", Routes: []HTTPRouteBackend{{Backend: "10.0.0.5:8080", HealthPath: "/healthz"}}},
}
out := m.GenerateWithOpts(nil, nil, GenerateOpts{HTTPRoutes: httpRoutes})
@@ -568,3 +570,159 @@ func TestGenerateWithOpts_HealthCheckInL7Backend(t *testing.T) {
t.Errorf("expected server line with 'check' flag for health check, got:\n%s", out)
}
}
// --- L7 extended features tests (paths, headers, IP whitelisting) ---
func TestGenerateWithOpts_PathRestriction(t *testing.T) {
m := NewManager("")
httpRoutes := []HTTPRoute{
{
Name: "synapse-fed",
Domain: "payne.io",
Routes: []HTTPRouteBackend{{
Backend: "192.168.8.80:80",
Paths: []string{"/.well-known/matrix", "/_matrix"},
}},
},
}
out := m.GenerateWithOpts(nil, nil, GenerateOpts{HTTPRoutes: httpRoutes})
if !strings.Contains(out, "acl path_synapse_fed_0 path_beg /.well-known/matrix /_matrix") {
t.Errorf("expected path ACL for synapse-fed, got:\n%s", out)
}
if !strings.Contains(out, "use_backend be_l7_synapse_fed_0 if host_synapse_fed path_synapse_fed_0") {
t.Errorf("expected path-restricted use_backend, got:\n%s", out)
}
}
func TestGenerateWithOpts_ResponseHeaders(t *testing.T) {
m := NewManager("")
httpRoutes := []HTTPRoute{
{
Name: "plausible",
Domain: "plausible.cloud.payne.io",
Routes: []HTTPRouteBackend{{
Backend: "192.168.8.80:80",
Headers: &services.HeaderConfig{
Response: map[string]string{
"Access-Control-Allow-Private-Network": "true",
},
},
}},
},
}
out := m.GenerateWithOpts(nil, nil, GenerateOpts{HTTPRoutes: httpRoutes})
if !strings.Contains(out, `http-response set-header Access-Control-Allow-Private-Network "true" if host_plausible`) {
t.Errorf("expected response header rule, got:\n%s", out)
}
}
func TestGenerateWithOpts_RequestHeaders(t *testing.T) {
m := NewManager("")
httpRoutes := []HTTPRoute{
{
Name: "my-api",
Domain: "api.payne.io",
Routes: []HTTPRouteBackend{{
Backend: "192.168.8.80:80",
Headers: &services.HeaderConfig{
Request: map[string]string{
"X-Forwarded-Proto": "https",
},
},
}},
},
}
out := m.GenerateWithOpts(nil, nil, GenerateOpts{HTTPRoutes: httpRoutes})
if !strings.Contains(out, `http-request set-header X-Forwarded-Proto "https" if host_my_api`) {
t.Errorf("expected request header rule, got:\n%s", out)
}
}
func TestGenerateWithOpts_IPWhitelist(t *testing.T) {
m := NewManager("")
httpRoutes := []HTTPRoute{
{
Name: "headlamp",
Domain: "headlamp.internal.cloud.payne.io",
Routes: []HTTPRouteBackend{{
Backend: "192.168.8.80:80",
IPAllow: []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"},
}},
},
}
out := m.GenerateWithOpts(nil, nil, GenerateOpts{HTTPRoutes: httpRoutes})
if !strings.Contains(out, "acl ip_headlamp_0 src 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16") {
t.Errorf("expected IP ACL for headlamp, got:\n%s", out)
}
if !strings.Contains(out, "http-request deny if host_headlamp !ip_headlamp_0") {
t.Errorf("expected IP deny rule for headlamp, got:\n%s", out)
}
}
func TestGenerateWithOpts_PathRoutingOrder(t *testing.T) {
m := NewManager("")
// Path-restricted routes should appear BEFORE unrestricted routes
// In the new model, multiple backends for the same domain go in one HTTPRoute
httpRoutes := []HTTPRoute{
{Name: "app", Domain: "payne.io", Routes: []HTTPRouteBackend{
{Backend: "192.168.8.80:80", Paths: []string{"/_matrix"}},
{Backend: "192.168.8.80:80"},
}},
}
out := m.GenerateWithOpts(nil, nil, GenerateOpts{HTTPRoutes: httpRoutes})
pathPos := strings.Index(out, "use_backend be_l7_app_0 if")
hostPos := strings.Index(out, "use_backend be_l7_app_1 if")
if pathPos < 0 {
t.Fatalf("expected path-restricted use_backend for route 0, got:\n%s", out)
}
if hostPos < 0 {
t.Fatalf("expected unrestricted use_backend for route 1, got:\n%s", out)
}
if pathPos > hostPos {
t.Errorf("path-restricted routes (pos %d) must appear before unrestricted (pos %d):\n%s", pathPos, hostPos, out)
}
}
func TestGenerateWithOpts_NoL7Fields_BackwardCompat(t *testing.T) {
m := NewManager("")
// Route with no paths/headers/ipAllow should work exactly as before
httpRoutes := []HTTPRoute{
{Name: "simple", Domain: "simple.payne.io", Routes: []HTTPRouteBackend{{Backend: "192.168.8.80:80"}}},
}
out := m.GenerateWithOpts(nil, nil, GenerateOpts{HTTPRoutes: httpRoutes})
if !strings.Contains(out, "acl host_simple hdr(host) -i simple.payne.io") {
t.Errorf("expected host ACL, got:\n%s", out)
}
if !strings.Contains(out, "use_backend be_l7_simple_0 if host_simple") {
t.Errorf("expected simple use_backend, got:\n%s", out)
}
// Should NOT have path, IP, or header rules
if strings.Contains(out, "path_beg") {
t.Errorf("unexpected path ACL in simple route, got:\n%s", out)
}
if strings.Contains(out, "http-request deny") {
t.Errorf("unexpected IP deny in simple route, got:\n%s", out)
}
if strings.Contains(out, "http-response set-header") {
t.Errorf("unexpected response header in simple route, got:\n%s", out)
}
}