Support routes.
This commit is contained in:
@@ -7,9 +7,12 @@ import (
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/wild-cloud/wild-central/internal/services"
|
||||
)
|
||||
|
||||
// InstanceRoute represents a Wild Cloud instance's ingress routing configuration
|
||||
@@ -28,13 +31,23 @@ type CustomRoute struct {
|
||||
Backend string // e.g. "192.168.8.10:22"
|
||||
}
|
||||
|
||||
// HTTPRoute represents an L7 HTTP reverse proxy route (for Wild Works services).
|
||||
// HTTPRouteBackend maps path prefixes to a specific backend with its own L7 options.
|
||||
type HTTPRouteBackend struct {
|
||||
Paths []string // path prefixes (empty = catch-all)
|
||||
Backend string // host:port
|
||||
HealthPath string // optional health check path
|
||||
Headers *services.HeaderConfig // per-route headers
|
||||
IPAllow []string // per-route CIDR whitelist
|
||||
}
|
||||
|
||||
// HTTPRoute represents an L7 HTTP reverse proxy route.
|
||||
// Central terminates TLS using a wildcard certificate and proxies by Host header.
|
||||
// Routes are evaluated in order — path-specific before catch-all.
|
||||
type HTTPRoute struct {
|
||||
Name string // e.g. "my-api"
|
||||
Domain string // e.g. "my-api.payne.io"
|
||||
Backend string // e.g. "192.168.8.60:9001"
|
||||
HealthPath string // e.g. "/health" (optional, for active health checks)
|
||||
Name string // e.g. "my-api"
|
||||
Domain string // e.g. "my-api.payne.io"
|
||||
Subdomains bool // if true, also match *.domain
|
||||
Routes []HTTPRouteBackend // ordered path→backend mappings
|
||||
}
|
||||
|
||||
const defaultConfigPath = "/etc/haproxy/haproxy.cfg"
|
||||
@@ -134,14 +147,28 @@ frontend http_in
|
||||
// 3. L4 wildcard matches (*.cloud.payne.io) → instance backends
|
||||
// 4. Default → L7 termination (if any HTTP routes exist)
|
||||
|
||||
// 1. L7 HTTP services — exact match, route to L7 TLS termination frontend
|
||||
// 1. L7 HTTP services — route to L7 TLS termination frontend
|
||||
if len(opts.HTTPRoutes) > 0 {
|
||||
sb.WriteString(" # L7 HTTP services (exact match → TLS termination)\n")
|
||||
sb.WriteString(" # L7 HTTP services (→ TLS termination)\n")
|
||||
// Exact matches first
|
||||
for _, route := range opts.HTTPRoutes {
|
||||
if route.Subdomains {
|
||||
continue // handled below
|
||||
}
|
||||
aclName := "is_l7_" + sanitizeName(route.Name)
|
||||
fmt.Fprintf(&sb, " acl %s req_ssl_sni -m str %s\n", aclName, route.Domain)
|
||||
fmt.Fprintf(&sb, " use_backend be_l7_termination if %s\n", aclName)
|
||||
}
|
||||
// Wildcard matches after exact
|
||||
for _, route := range opts.HTTPRoutes {
|
||||
if !route.Subdomains {
|
||||
continue
|
||||
}
|
||||
aclName := "is_l7_" + sanitizeName(route.Name)
|
||||
fmt.Fprintf(&sb, " acl %s req_ssl_sni -m end .%s\n", aclName, route.Domain)
|
||||
fmt.Fprintf(&sb, " acl %s req_ssl_sni -m str %s\n", aclName, route.Domain)
|
||||
fmt.Fprintf(&sb, " use_backend be_l7_termination if %s\n", aclName)
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
@@ -209,24 +236,99 @@ frontend http_in
|
||||
sb.WriteString(" mode http\n")
|
||||
sb.WriteString("\n")
|
||||
|
||||
for _, route := range opts.HTTPRoutes {
|
||||
aclName := "host_" + sanitizeName(route.Name)
|
||||
fmt.Fprintf(&sb, " acl %s hdr(host) -i %s\n", aclName, route.Domain)
|
||||
fmt.Fprintf(&sb, " use_backend be_l7_%s if %s\n", sanitizeName(route.Name), aclName)
|
||||
// Host ACLs for all services
|
||||
for _, httpRoute := range opts.HTTPRoutes {
|
||||
hostACL := "host_" + sanitizeName(httpRoute.Name)
|
||||
if httpRoute.Subdomains {
|
||||
fmt.Fprintf(&sb, " acl %s hdr(host) -i -m end .%s\n", hostACL, httpRoute.Domain)
|
||||
fmt.Fprintf(&sb, " acl %s hdr(host) -i -m str %s\n", hostACL, httpRoute.Domain)
|
||||
} else {
|
||||
fmt.Fprintf(&sb, " acl %s hdr(host) -i %s\n", hostACL, httpRoute.Domain)
|
||||
}
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
|
||||
// L7 backends
|
||||
for _, route := range opts.HTTPRoutes {
|
||||
fmt.Fprintf(&sb, "backend be_l7_%s\n", sanitizeName(route.Name))
|
||||
sb.WriteString(" mode http\n")
|
||||
if route.HealthPath != "" {
|
||||
fmt.Fprintf(&sb, " option httpchk GET %s\n", route.HealthPath)
|
||||
fmt.Fprintf(&sb, " server s0 %s check\n", route.Backend)
|
||||
} else {
|
||||
fmt.Fprintf(&sb, " server s0 %s\n", route.Backend)
|
||||
// Per-route ACLs (IP whitelisting, headers) and routing rules
|
||||
for _, httpRoute := range opts.HTTPRoutes {
|
||||
hostACL := "host_" + sanitizeName(httpRoute.Name)
|
||||
routeName := sanitizeName(httpRoute.Name)
|
||||
|
||||
for i, rb := range httpRoute.Routes {
|
||||
aclSuffix := fmt.Sprintf("%s_%d", routeName, i)
|
||||
|
||||
// IP whitelisting for this route
|
||||
if len(rb.IPAllow) > 0 {
|
||||
ipACL := "ip_" + aclSuffix
|
||||
fmt.Fprintf(&sb, " acl %s src %s\n", ipACL, strings.Join(rb.IPAllow, " "))
|
||||
if len(rb.Paths) > 0 {
|
||||
pathACL := "path_" + aclSuffix
|
||||
fmt.Fprintf(&sb, " acl %s path_beg %s\n", pathACL, strings.Join(rb.Paths, " "))
|
||||
fmt.Fprintf(&sb, " http-request deny if %s %s !%s\n", hostACL, pathACL, ipACL)
|
||||
} else {
|
||||
fmt.Fprintf(&sb, " http-request deny if %s !%s\n", hostACL, ipACL)
|
||||
}
|
||||
}
|
||||
|
||||
// Request headers
|
||||
if rb.Headers != nil {
|
||||
for _, k := range sortedKeys(rb.Headers.Request) {
|
||||
fmt.Fprintf(&sb, " http-request set-header %s %q if %s\n", k, rb.Headers.Request[k], hostACL)
|
||||
}
|
||||
}
|
||||
|
||||
// Response headers
|
||||
if rb.Headers != nil {
|
||||
for _, k := range sortedKeys(rb.Headers.Response) {
|
||||
fmt.Fprintf(&sb, " http-response set-header %s %q if %s\n", k, rb.Headers.Response[k], hostACL)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString("\n")
|
||||
|
||||
// Routing rules: path-specific first, then catch-all
|
||||
for _, httpRoute := range opts.HTTPRoutes {
|
||||
hostACL := "host_" + sanitizeName(httpRoute.Name)
|
||||
routeName := sanitizeName(httpRoute.Name)
|
||||
|
||||
// Path-specific routes first (most specific wins)
|
||||
for i, rb := range httpRoute.Routes {
|
||||
if len(rb.Paths) == 0 {
|
||||
continue
|
||||
}
|
||||
backendName := fmt.Sprintf("be_l7_%s_%d", routeName, i)
|
||||
pathACL := fmt.Sprintf("path_%s_%d", routeName, i)
|
||||
fmt.Fprintf(&sb, " acl %s path_beg %s\n", pathACL, strings.Join(rb.Paths, " "))
|
||||
fmt.Fprintf(&sb, " use_backend %s if %s %s\n", backendName, hostACL, pathACL)
|
||||
}
|
||||
|
||||
// Catch-all route last
|
||||
for i, rb := range httpRoute.Routes {
|
||||
if len(rb.Paths) > 0 {
|
||||
continue
|
||||
}
|
||||
backendName := fmt.Sprintf("be_l7_%s_%d", routeName, i)
|
||||
fmt.Fprintf(&sb, " use_backend %s if %s\n", backendName, hostACL)
|
||||
}
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
|
||||
// L7 backends — one per route
|
||||
for _, httpRoute := range opts.HTTPRoutes {
|
||||
routeName := sanitizeName(httpRoute.Name)
|
||||
for i, rb := range httpRoute.Routes {
|
||||
backendName := fmt.Sprintf("be_l7_%s_%d", routeName, i)
|
||||
fmt.Fprintf(&sb, "backend %s\n", backendName)
|
||||
sb.WriteString(" mode http\n")
|
||||
if rb.HealthPath != "" {
|
||||
fmt.Fprintf(&sb, " option httpchk GET %s\n", rb.HealthPath)
|
||||
fmt.Fprintf(&sb, " server s0 %s check\n", rb.Backend)
|
||||
} else {
|
||||
fmt.Fprintf(&sb, " server s0 %s\n", rb.Backend)
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -247,6 +349,16 @@ frontend http_in
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// sortedKeys returns the keys of a map in sorted order for deterministic output.
|
||||
func sortedKeys(m map[string]string) []string {
|
||||
keys := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
// sanitizeName converts a name to a valid HAProxy identifier (alphanumeric + underscore)
|
||||
func sanitizeName(name string) string {
|
||||
return strings.Map(func(r rune) rune {
|
||||
|
||||
Reference in New Issue
Block a user