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

@@ -11,9 +11,30 @@ import (
"github.com/wild-cloud/wild-central/internal/wireguard"
)
// VpnStatus returns the current WireGuard interface status.
func (api *API) VpnStatus(w http.ResponseWriter, r *http.Request) {
status, err := api.vpn.GetStatus()
// VPNManager is the interface for VPN operations used by these handlers.
type VPNManager interface {
GetStatus() (*wireguard.Status, error)
GetConfig() (*wireguard.Config, error)
SaveConfig(cfg *wireguard.Config) error
GetPublicKey() string
GenerateKeypair() error
Apply() error
ListPeers() ([]*wireguard.Peer, error)
AddPeer(name string) (*wireguard.Peer, error)
GetPeer(id string) (*wireguard.Peer, error)
DeletePeer(id string) error
GeneratePeerConfigText(peerID string) (string, error)
}
// vpnHandlers groups VPN HTTP handlers with their dependencies.
type vpnHandlers struct {
vpn VPNManager
statePath string
syncNftables func(globalCfg *config.State) // callback to resync firewall after config changes
}
func (h *vpnHandlers) Status(w http.ResponseWriter, r *http.Request) {
status, err := h.vpn.GetStatus()
if err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get VPN status: %v", err))
return
@@ -21,9 +42,8 @@ func (api *API) VpnStatus(w http.ResponseWriter, r *http.Request) {
respondJSON(w, http.StatusOK, status)
}
// VpnGetConfig returns the server interface configuration and public key.
func (api *API) VpnGetConfig(w http.ResponseWriter, r *http.Request) {
cfg, err := api.vpn.GetConfig()
func (h *vpnHandlers) GetConfig(w http.ResponseWriter, r *http.Request) {
cfg, err := h.vpn.GetConfig()
if err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get VPN config: %v", err))
return
@@ -35,12 +55,11 @@ func (api *API) VpnGetConfig(w http.ResponseWriter, r *http.Request) {
"endpoint": cfg.Endpoint,
"dns": cfg.DNS,
"lanCIDR": cfg.LanCIDR,
"publicKey": api.vpn.GetPublicKey(),
"publicKey": h.vpn.GetPublicKey(),
})
}
// VpnUpdateConfig updates the server interface configuration.
func (api *API) VpnUpdateConfig(w http.ResponseWriter, r *http.Request) {
func (h *vpnHandlers) UpdateConfig(w http.ResponseWriter, r *http.Request) {
var req wireguard.Config
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, fmt.Sprintf("Invalid request: %v", err))
@@ -49,41 +68,38 @@ func (api *API) VpnUpdateConfig(w http.ResponseWriter, r *http.Request) {
if req.ListenPort == 0 {
req.ListenPort = 51820
}
if err := api.vpn.SaveConfig(&req); err != nil {
if err := h.vpn.SaveConfig(&req); err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to save VPN config: %v", err))
return
}
// Resync nftables so the VPN listen port is automatically allowed/removed
if globalCfg, err := config.LoadState(api.statePath()); err == nil {
go api.syncNftablesOnly(globalCfg)
if globalCfg, err := config.LoadState(h.statePath); err == nil {
go h.syncNftables(globalCfg)
}
respondJSON(w, http.StatusOK, map[string]string{"message": "VPN configuration saved"})
}
// VpnGenerateKeypair generates a new server keypair.
func (api *API) VpnGenerateKeypair(w http.ResponseWriter, r *http.Request) {
if err := api.vpn.GenerateKeypair(); err != nil {
func (h *vpnHandlers) GenerateKeypair(w http.ResponseWriter, r *http.Request) {
if err := h.vpn.GenerateKeypair(); err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to generate keypair: %v", err))
return
}
respondJSON(w, http.StatusOK, map[string]string{
"message": "Server keypair generated",
"publicKey": api.vpn.GetPublicKey(),
"publicKey": h.vpn.GetPublicKey(),
})
}
// VpnApply writes the wg0.conf and brings the WireGuard interface up.
func (api *API) VpnApply(w http.ResponseWriter, r *http.Request) {
if err := api.vpn.Apply(); err != nil {
func (h *vpnHandlers) Apply(w http.ResponseWriter, r *http.Request) {
if err := h.vpn.Apply(); err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to apply VPN config: %v", err))
return
}
respondJSON(w, http.StatusOK, map[string]string{"message": "WireGuard interface applied successfully"})
}
// VpnListPeers returns all configured peers (without private keys).
func (api *API) VpnListPeers(w http.ResponseWriter, r *http.Request) {
peers, err := api.vpn.ListPeers()
func (h *vpnHandlers) ListPeers(w http.ResponseWriter, r *http.Request) {
peers, err := h.vpn.ListPeers()
if err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to list peers: %v", err))
return
@@ -91,8 +107,7 @@ func (api *API) VpnListPeers(w http.ResponseWriter, r *http.Request) {
respondJSON(w, http.StatusOK, map[string]any{"peers": redactPeers(peers)})
}
// VpnAddPeer adds a new peer, auto-generating a keypair and assigning an IP.
func (api *API) VpnAddPeer(w http.ResponseWriter, r *http.Request) {
func (h *vpnHandlers) AddPeer(w http.ResponseWriter, r *http.Request) {
var req struct {
Name string `json:"name"`
}
@@ -100,7 +115,7 @@ func (api *API) VpnAddPeer(w http.ResponseWriter, r *http.Request) {
respondError(w, http.StatusBadRequest, "name is required")
return
}
peer, err := api.vpn.AddPeer(req.Name)
peer, err := h.vpn.AddPeer(req.Name)
if err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to add peer: %v", err))
return
@@ -108,15 +123,14 @@ func (api *API) VpnAddPeer(w http.ResponseWriter, r *http.Request) {
respondJSON(w, http.StatusCreated, redactPeer(peer))
}
// VpnGetPeerConfig returns the wg-quick config text for a peer (used by clients and QR generation).
func (api *API) VpnGetPeerConfig(w http.ResponseWriter, r *http.Request) {
func (h *vpnHandlers) GetPeerConfig(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
text, err := api.vpn.GeneratePeerConfigText(id)
text, err := h.vpn.GeneratePeerConfigText(id)
if err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to generate peer config: %v", err))
return
}
peer, err := api.vpn.GetPeer(id)
peer, err := h.vpn.GetPeer(id)
if err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get peer: %v", err))
return
@@ -127,10 +141,9 @@ func (api *API) VpnGetPeerConfig(w http.ResponseWriter, r *http.Request) {
})
}
// VpnDeletePeer removes a peer by ID.
func (api *API) VpnDeletePeer(w http.ResponseWriter, r *http.Request) {
func (h *vpnHandlers) DeletePeer(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
if err := api.vpn.DeletePeer(id); err != nil {
if err := h.vpn.DeletePeer(id); err != nil {
respondError(w, http.StatusNotFound, fmt.Sprintf("Failed to delete peer: %v", err))
return
}