Refactor architecture: extract reconciler, add interfaces, reduce complexity, improve test coverage

Architecture:
- Extract reconcileNetworking into internal/reconcile package with 7 consumer-side interfaces
- Add locked modifyState helper to fix state.yaml read-modify-write race condition
- Extract CrowdSec and VPN handler groups with interfaces documenting dependency surface
- Replace raw map[string]any YAML manipulation with typed AddDHCPStaticLease/RemoveDHCPStaticLease
- Extract Cloudflare API functions into cfClient struct, eliminating repeated auth boilerplate

Complexity reduction:
- haproxy.GenerateWithOpts: 50 → 6 (extracted 8 focused helpers)
- reconcile.Reconcile: 42 → 11 (extracted buildRoutes, buildDNSEntries, writeHAProxyConfig)
- AutheliaUpdateConfig: 25 → 10 (extracted enableAuthelia/disableAuthelia)

Test coverage improvements:
- reconcile: 4.5% → 56.8% (stub-based tests for route building, DNS entries, orchestration)
- dnsfilter: 10.7% → 45.6% (Manager.Compile, AddList, ToggleList, custom entries)
- config: 67.3% → 89.8% (DHCP static lease mutation tests)
This commit is contained in:
2026-07-14 04:21:30 +00:00
parent 1e7d93256e
commit 428d47f876
35 changed files with 1856 additions and 1194 deletions

View File

@@ -24,35 +24,37 @@ import (
"github.com/wild-cloud/wild-central/internal/ddns"
"github.com/wild-cloud/wild-central/internal/dnsfilter"
"github.com/wild-cloud/wild-central/internal/dnsmasq"
"github.com/wild-cloud/wild-central/internal/domains"
"github.com/wild-cloud/wild-central/internal/haproxy"
"github.com/wild-cloud/wild-central/internal/network"
"github.com/wild-cloud/wild-central/internal/nftables"
"github.com/wild-cloud/wild-central/internal/reconcile"
"github.com/wild-cloud/wild-central/internal/secrets"
"github.com/wild-cloud/wild-central/internal/domains"
"github.com/wild-cloud/wild-central/internal/sse"
"github.com/wild-cloud/wild-central/internal/wireguard"
)
// API holds all dependencies for Central API handlers
type API struct {
dataDir string
version string
allowedOrigins []string
ctx gocontext.Context // parent context for restartable goroutines
secrets *secrets.Manager
dnsmasq *dnsmasq.ConfigGenerator
haproxy *haproxy.Manager
nftables *nftables.Manager
ddns *ddns.Runner
crowdsec *crowdsec.Manager
vpn *wireguard.Manager
certbot *certbot.Manager
authelia *authelia.Manager // Authelia authentication service manager
dnsFilter *dnsfilter.Manager // DNS filtering manager
dnsFilterRunner *dnsfilter.Runner // DNS filter background update runner
domains *domains.Manager // Domain registration manager
sseManager *sse.Manager // SSE manager for real-time events
port int // Running API port (for config-driven routes)
dataDir string
version string
allowedOrigins []string
ctx gocontext.Context // parent context for restartable goroutines
reconciler *reconcile.Reconciler
secrets *secrets.Manager
dnsmasq *dnsmasq.ConfigGenerator
haproxy *haproxy.Manager
nftables *nftables.Manager
ddns *ddns.Runner
crowdsec *crowdsec.Manager
vpn *wireguard.Manager
certbot *certbot.Manager
authelia *authelia.Manager // Authelia authentication service manager
dnsFilter *dnsfilter.Manager // DNS filtering manager
dnsFilterRunner *dnsfilter.Runner // DNS filter background update runner
domains *domains.Manager // Domain registration manager
sseManager *sse.Manager // SSE manager for real-time events
port int // Running API port (for config-driven routes)
}
// NewAPI creates a new Central API handler with all dependencies
@@ -77,8 +79,8 @@ func NewAPI(dataDir, version string, allowedOrigins []string) (*API, error) {
crowdsec: crowdsec.NewManager(),
vpn: wireguard.NewManager(dataDir, vpnConfigPath),
certbot: certbot.NewManager(""),
authelia: authelia.NewManager(filepath.Join(dataDir, "authelia")),
dnsFilter: dnsfilter.NewManager(filepath.Join(dataDir, "dns-filter")),
authelia: authelia.NewManager(filepath.Join(dataDir, "authelia")),
dnsFilter: dnsfilter.NewManager(filepath.Join(dataDir, "dns-filter")),
domains: domains.NewManager(dataDir),
sseManager: sseManager,
}
@@ -89,9 +91,22 @@ func NewAPI(dataDir, version string, allowedOrigins []string) (*API, error) {
filterDir := envOrDefault("WILD_CENTRAL_FILTER_DIR", "/var/lib/wild-central/dns-filter")
api.dnsFilter.SetHostsFilePath(filepath.Join(filterDir, "blocked.conf"))
// Build the reconciler — the core domain→config→daemon pipeline
api.reconciler = &reconcile.Reconciler{
Domains: api.domains,
HAProxy: api.haproxy,
DNS: api.dnsmasq,
Auth: api.authelia,
DDNS: api.ddns,
DNSFilter: api.dnsFilter,
SSE: sseManager,
StatePath: filepath.Join(dataDir, "state.yaml"),
GenerateAutheliaConfig: api.generateAutheliaConfig,
}
// Wire up domain registration reconciliation: when domains change,
// regenerate dnsmasq DNS entries and HAProxy routes.
api.domains.SetReconcileFn(api.reconcileNetworking)
api.domains.SetReconcileFn(api.reconciler.Reconcile)
return api, nil
}
@@ -259,15 +274,16 @@ func (api *API) RegisterRoutes(r *mux.Router) {
r.HandleFunc("/api/v1/ddns/trigger", api.DDNSTrigger).Methods("POST")
// WireGuard VPN management
r.HandleFunc("/api/v1/vpn/status", api.VpnStatus).Methods("GET")
r.HandleFunc("/api/v1/vpn/config", api.VpnGetConfig).Methods("GET")
r.HandleFunc("/api/v1/vpn/config", api.VpnUpdateConfig).Methods("PUT")
r.HandleFunc("/api/v1/vpn/keygen", api.VpnGenerateKeypair).Methods("POST")
r.HandleFunc("/api/v1/vpn/apply", api.VpnApply).Methods("POST")
r.HandleFunc("/api/v1/vpn/peers", api.VpnListPeers).Methods("GET")
r.HandleFunc("/api/v1/vpn/peers", api.VpnAddPeer).Methods("POST")
r.HandleFunc("/api/v1/vpn/peers/{id}/config", api.VpnGetPeerConfig).Methods("GET")
r.HandleFunc("/api/v1/vpn/peers/{id}", api.VpnDeletePeer).Methods("DELETE")
vpn := &vpnHandlers{vpn: api.vpn, statePath: api.statePath(), syncNftables: api.syncNftablesOnly}
r.HandleFunc("/api/v1/vpn/status", vpn.Status).Methods("GET")
r.HandleFunc("/api/v1/vpn/config", vpn.GetConfig).Methods("GET")
r.HandleFunc("/api/v1/vpn/config", vpn.UpdateConfig).Methods("PUT")
r.HandleFunc("/api/v1/vpn/keygen", vpn.GenerateKeypair).Methods("POST")
r.HandleFunc("/api/v1/vpn/apply", vpn.Apply).Methods("POST")
r.HandleFunc("/api/v1/vpn/peers", vpn.ListPeers).Methods("GET")
r.HandleFunc("/api/v1/vpn/peers", vpn.AddPeer).Methods("POST")
r.HandleFunc("/api/v1/vpn/peers/{id}/config", vpn.GetPeerConfig).Methods("GET")
r.HandleFunc("/api/v1/vpn/peers/{id}", vpn.DeletePeer).Methods("DELETE")
// TLS certificate management
r.HandleFunc("/api/v1/cert/status", api.CertStatus).Methods("GET")
@@ -275,19 +291,20 @@ func (api *API) RegisterRoutes(r *mux.Router) {
r.HandleFunc("/api/v1/cert/renew", api.CertRenew).Methods("POST")
// CrowdSec LAPI management
r.HandleFunc("/api/v1/crowdsec/status", api.CrowdSecStatus).Methods("GET")
r.HandleFunc("/api/v1/crowdsec/summary", api.CrowdSecGetSummary).Methods("GET")
r.HandleFunc("/api/v1/crowdsec/alerts", api.CrowdSecGetAlerts).Methods("GET")
r.HandleFunc("/api/v1/crowdsec/decisions", api.CrowdSecGetDecisions).Methods("GET")
r.HandleFunc("/api/v1/crowdsec/decisions", api.CrowdSecAddDecision).Methods("POST")
r.HandleFunc("/api/v1/crowdsec/decisions/{id}", api.CrowdSecDeleteDecision).Methods("DELETE")
r.HandleFunc("/api/v1/crowdsec/decisions/ip/{ip}", api.CrowdSecDeleteDecisionByIP).Methods("DELETE")
r.HandleFunc("/api/v1/crowdsec/machines", api.CrowdSecGetMachines).Methods("GET")
r.HandleFunc("/api/v1/crowdsec/machines", api.CrowdSecAddMachine).Methods("POST")
r.HandleFunc("/api/v1/crowdsec/machines/{name}", api.CrowdSecDeleteMachine).Methods("DELETE")
r.HandleFunc("/api/v1/crowdsec/bouncers", api.CrowdSecGetBouncers).Methods("GET")
r.HandleFunc("/api/v1/crowdsec/bouncers", api.CrowdSecAddBouncer).Methods("POST")
r.HandleFunc("/api/v1/crowdsec/bouncers/{name}", api.CrowdSecDeleteBouncer).Methods("DELETE")
csec := &crowdsecHandlers{crowdsec: api.crowdsec}
r.HandleFunc("/api/v1/crowdsec/status", csec.Status).Methods("GET")
r.HandleFunc("/api/v1/crowdsec/summary", csec.GetSummary).Methods("GET")
r.HandleFunc("/api/v1/crowdsec/alerts", csec.GetAlerts).Methods("GET")
r.HandleFunc("/api/v1/crowdsec/decisions", csec.GetDecisions).Methods("GET")
r.HandleFunc("/api/v1/crowdsec/decisions", csec.AddDecision).Methods("POST")
r.HandleFunc("/api/v1/crowdsec/decisions/{id}", csec.DeleteDecision).Methods("DELETE")
r.HandleFunc("/api/v1/crowdsec/decisions/ip/{ip}", csec.DeleteDecisionByIP).Methods("DELETE")
r.HandleFunc("/api/v1/crowdsec/machines", csec.GetMachines).Methods("GET")
r.HandleFunc("/api/v1/crowdsec/machines", csec.AddMachine).Methods("POST")
r.HandleFunc("/api/v1/crowdsec/machines/{name}", csec.DeleteMachine).Methods("DELETE")
r.HandleFunc("/api/v1/crowdsec/bouncers", csec.GetBouncers).Methods("GET")
r.HandleFunc("/api/v1/crowdsec/bouncers", csec.AddBouncer).Methods("POST")
r.HandleFunc("/api/v1/crowdsec/bouncers/{name}", csec.DeleteBouncer).Methods("DELETE")
// Cloudflare management
r.HandleFunc("/api/v1/cloudflare/verify", api.CloudflareVerify).Methods("GET")
@@ -399,8 +416,8 @@ func (api *API) StatusHandler(w http.ResponseWriter, r *http.Request, startTime
// getDaemonStatus checks whether each managed daemon is active and gets its version.
func getDaemonStatus() map[string]map[string]any {
type daemonInfo struct {
unit string
verCmd []string // command to get version
unit string
verCmd []string // command to get version
verParse func(string) string // extract version from output
}