Add SafeApply lifecycle, health tracking, convergence loop, and startup checks
SafeApply pattern (validate → backup → write → reload → verify → rollback): - HAProxy: SafeApply wraps existing validate+write+reload with backup and post-reload health check; rolls back to .bak on failure - dnsmasq: SafeApply validates via dnsmasq --test, backs up, atomic writes, restarts, verifies daemon is active; rolls back on failure - nftables: SafeApply validates via nft -c, backs up, atomic writes, applies to kernel, verifies table loaded; rolls back on failure Health tracking: - Add SubsystemHealth and Health structs to reconciler - Track per-subsystem status (ok/degraded/error) after each reconcile - Detect recovery: previous error → current ok broadcasts recovery event - GET /api/v1/health/reconcile endpoint exposes health state - HAProxy tracks excluded domains as "degraded" state SSE error events: - Broadcast haproxy:error, dnsmasq:error on SafeApply failure - Broadcast haproxy:recovered, dnsmasq:recovered on recovery from error Convergence loop: - 5-minute periodic reconcile drives system toward desired state - Catches config drift, daemon crashes, transient failures - Serialized by reconcile mutex — no race with event-driven reconciles Startup: - CheckPrerequisites verifies required (dnsmasq, haproxy) and optional (wg, authelia, cscli, cloudflared, nft) binaries before first reconcile
This commit is contained in:
@@ -36,22 +36,26 @@ func (s *stubHAProxy) GenerateWithOpts(l4 []haproxy.L4Route, _ []haproxy.CustomR
|
||||
s.lastL4Routes = l4
|
||||
return "generated-config"
|
||||
}
|
||||
func (s *stubHAProxy) WriteConfig(c string) error { s.writtenConfig = c; return s.writeErr }
|
||||
func (s *stubHAProxy) SafeApply(c string) error { s.writtenConfig = c; return s.writeErr }
|
||||
func (s *stubHAProxy) WriteConfig(c string) error { s.writtenConfig = c; return s.writeErr }
|
||||
func (s *stubHAProxy) ReloadService() error { s.reloadCalled = true; return nil }
|
||||
|
||||
type stubDNS struct {
|
||||
entries []dnsmasq.DNSEntry
|
||||
filterPath string
|
||||
updateCalled bool
|
||||
entries []dnsmasq.DNSEntry
|
||||
filterPath string
|
||||
applyCalled bool
|
||||
}
|
||||
|
||||
func (s *stubDNS) UpdateConfig(_ *config.State, entries []dnsmasq.DNSEntry, _ bool) error {
|
||||
func (s *stubDNS) Generate(_ *config.State, entries []dnsmasq.DNSEntry) string {
|
||||
s.entries = entries
|
||||
s.updateCalled = true
|
||||
return "generated-dns-config"
|
||||
}
|
||||
func (s *stubDNS) SafeApply(_ string) error {
|
||||
s.applyCalled = true
|
||||
return nil
|
||||
}
|
||||
func (s *stubDNS) SetFilterConfPath(p string) { s.filterPath = p }
|
||||
func (s *stubDNS) GetStatus() (*dnsmasq.Status, error) { return nil, nil }
|
||||
func (s *stubDNS) SetFilterConfPath(p string) { s.filterPath = p }
|
||||
func (s *stubDNS) GetStatus() (*dnsmasq.Status, error) { return nil, nil }
|
||||
|
||||
type stubAuth struct {
|
||||
userCount int
|
||||
@@ -188,7 +192,7 @@ func TestReconcile_EmptyDomains(t *testing.T) {
|
||||
r, hp, dns, ddns := newTestReconciler(t, nil)
|
||||
r.Reconcile()
|
||||
|
||||
if !dns.updateCalled {
|
||||
if !dns.applyCalled {
|
||||
t.Error("expected dnsmasq update")
|
||||
}
|
||||
if len(dns.entries) != 0 {
|
||||
|
||||
Reference in New Issue
Block a user