Resilient HAProxy config: isolate broken services on validation failure
When haproxy -c fails, parse ALERT line numbers, map to service domains via # service: comments in generated config, exclude broken services, and regenerate. One retry — prevents a single bad registration from blocking all config updates.
This commit is contained in:
@@ -726,3 +726,110 @@ func TestGenerateWithOpts_NoL7Fields_BackwardCompat(t *testing.T) {
|
||||
t.Errorf("unexpected response header in simple route, got:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Service marker and validation error tests ---
|
||||
|
||||
func TestGenerateWithOpts_ServiceMarkers(t *testing.T) {
|
||||
m := NewManager("")
|
||||
instances := []InstanceRoute{
|
||||
{Name: "cloud", Domain: "cloud.example.com", BackendIP: "10.0.0.1", Subdomains: true},
|
||||
}
|
||||
httpRoutes := []HTTPRoute{
|
||||
{Name: "api", Domain: "api.example.com", Routes: []HTTPRouteBackend{{Backend: "10.0.0.5:8080"}}},
|
||||
}
|
||||
|
||||
out := m.GenerateWithOpts(instances, nil, GenerateOpts{HTTPRoutes: httpRoutes})
|
||||
|
||||
// L4 instance should have service marker
|
||||
if !strings.Contains(out, "# service: cloud.example.com") {
|
||||
t.Errorf("expected service marker for cloud.example.com, got:\n%s", out)
|
||||
}
|
||||
// L7 HTTP should have service marker
|
||||
if !strings.Contains(out, "# service: api.example.com") {
|
||||
t.Errorf("expected service marker for api.example.com, got:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseValidationErrors(t *testing.T) {
|
||||
output := `[NOTICE] (123) : haproxy version is 3.0.10
|
||||
[WARNING] (123) : config : parsing [/tmp/test.cfg:42] : some warning
|
||||
[ALERT] (123) : config : parsing [/tmp/test.cfg:111]: 'http-response set-header' expects 'if'
|
||||
[ALERT] (123) : config : parsing [/tmp/test.cfg:115]: unknown keyword
|
||||
[ALERT] (123) : config : Error(s) found in configuration file : /tmp/test.cfg`
|
||||
|
||||
lines := ParseValidationErrors(output)
|
||||
|
||||
if len(lines) != 2 {
|
||||
t.Fatalf("expected 2 failing lines, got %d: %v", len(lines), lines)
|
||||
}
|
||||
if lines[0] != 111 {
|
||||
t.Errorf("expected first line 111, got %d", lines[0])
|
||||
}
|
||||
if lines[1] != 115 {
|
||||
t.Errorf("expected second line 115, got %d", lines[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseValidationErrors_NoAlerts(t *testing.T) {
|
||||
output := `[NOTICE] (123) : haproxy version is 3.0.10
|
||||
[WARNING] (123) : config : parsing [/tmp/test.cfg:42] : some warning
|
||||
Warnings were found.`
|
||||
|
||||
lines := ParseValidationErrors(output)
|
||||
if len(lines) != 0 {
|
||||
t.Errorf("expected 0 failing lines for warnings-only output, got %d", len(lines))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindBrokenServices(t *testing.T) {
|
||||
config := `# boilerplate
|
||||
frontend l7_https
|
||||
bind 127.0.0.1:8443 ssl crt /etc/haproxy/certs/
|
||||
mode http
|
||||
|
||||
# service: good.example.com
|
||||
acl host_good hdr(host) -i good.example.com
|
||||
# service: bad.example.com
|
||||
acl host_bad hdr(host) -i bad.example.com
|
||||
http-response set-header X-Bad broken value if host_bad
|
||||
# service: also-good.example.com
|
||||
acl host_also_good hdr(host) -i also-good.example.com
|
||||
`
|
||||
// Line 10 is the broken http-response line (1-indexed)
|
||||
broken := FindBrokenServices(config, []int{10})
|
||||
|
||||
if len(broken) != 1 {
|
||||
t.Fatalf("expected 1 broken service, got %d: %v", len(broken), broken)
|
||||
}
|
||||
if broken[0] != "bad.example.com" {
|
||||
t.Errorf("expected bad.example.com, got %s", broken[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindBrokenServices_MultipleLinesOneDomain(t *testing.T) {
|
||||
config := `line1
|
||||
# service: bad.example.com
|
||||
line3
|
||||
line4
|
||||
line5
|
||||
`
|
||||
// Multiple lines from the same service should deduplicate
|
||||
broken := FindBrokenServices(config, []int{3, 4, 5})
|
||||
|
||||
if len(broken) != 1 {
|
||||
t.Fatalf("expected 1 broken service (deduped), got %d: %v", len(broken), broken)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindBrokenServices_NoMarker(t *testing.T) {
|
||||
config := `line1
|
||||
line2
|
||||
line3
|
||||
`
|
||||
// No service markers — should return empty
|
||||
broken := FindBrokenServices(config, []int{2})
|
||||
|
||||
if len(broken) != 0 {
|
||||
t.Errorf("expected 0 broken services when no markers, got %d", len(broken))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user