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:
@@ -8,12 +8,36 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/wild-cloud/wild-central/internal/crowdsec"
|
||||
)
|
||||
|
||||
// CrowdSecManager is the interface for CrowdSec operations used by these handlers.
|
||||
type CrowdSecManager interface {
|
||||
GetStatus() (*crowdsec.Status, error)
|
||||
GetBanSummary() (*crowdsec.BanSummary, error)
|
||||
GetDecisions() ([]crowdsec.Decision, error)
|
||||
AddDecision(ip, decType, reason, duration string) error
|
||||
DeleteDecision(id int) error
|
||||
DeleteDecisionByIP(ip string) error
|
||||
GetAlerts(limit int) ([]crowdsec.Alert, error)
|
||||
GetMachines() ([]crowdsec.Machine, error)
|
||||
AddMachine(name, password string) error
|
||||
DeleteMachine(name string) error
|
||||
GetBouncers() ([]crowdsec.Bouncer, error)
|
||||
AddBouncer(name, apiKey string) error
|
||||
DeleteBouncer(name string) error
|
||||
}
|
||||
|
||||
// crowdsecHandlers groups CrowdSec HTTP handlers with their single dependency.
|
||||
type crowdsecHandlers struct {
|
||||
crowdsec CrowdSecManager
|
||||
}
|
||||
|
||||
// CrowdSecStatus returns whether CrowdSec is running on Wild Central
|
||||
// and lists registered machines and bouncers.
|
||||
func (api *API) CrowdSecStatus(w http.ResponseWriter, r *http.Request) {
|
||||
status, err := api.crowdsec.GetStatus()
|
||||
func (h *crowdsecHandlers) Status(w http.ResponseWriter, r *http.Request) {
|
||||
status, err := h.crowdsec.GetStatus()
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get CrowdSec status: %v", err))
|
||||
return
|
||||
@@ -21,10 +45,9 @@ func (api *API) CrowdSecStatus(w http.ResponseWriter, r *http.Request) {
|
||||
respondJSON(w, http.StatusOK, status)
|
||||
}
|
||||
|
||||
// CrowdSecGetSummary returns aggregated ban counts by scenario across all active decisions.
|
||||
// This includes CAPI community bans — can return tens of thousands of entries.
|
||||
func (api *API) CrowdSecGetSummary(w http.ResponseWriter, r *http.Request) {
|
||||
summary, err := api.crowdsec.GetBanSummary()
|
||||
// GetSummary returns aggregated ban counts by scenario across all active decisions.
|
||||
func (h *crowdsecHandlers) GetSummary(w http.ResponseWriter, r *http.Request) {
|
||||
summary, err := h.crowdsec.GetBanSummary()
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get ban summary: %v", err))
|
||||
return
|
||||
@@ -32,9 +55,9 @@ func (api *API) CrowdSecGetSummary(w http.ResponseWriter, r *http.Request) {
|
||||
respondJSON(w, http.StatusOK, summary)
|
||||
}
|
||||
|
||||
// CrowdSecGetDecisions returns active ban/captcha decisions
|
||||
func (api *API) CrowdSecGetDecisions(w http.ResponseWriter, r *http.Request) {
|
||||
decisions, err := api.crowdsec.GetDecisions()
|
||||
// GetDecisions returns active ban/captcha decisions
|
||||
func (h *crowdsecHandlers) GetDecisions(w http.ResponseWriter, r *http.Request) {
|
||||
decisions, err := h.crowdsec.GetDecisions()
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get decisions: %v", err))
|
||||
return
|
||||
@@ -42,24 +65,23 @@ func (api *API) CrowdSecGetDecisions(w http.ResponseWriter, r *http.Request) {
|
||||
respondJSON(w, http.StatusOK, map[string]any{"decisions": decisions})
|
||||
}
|
||||
|
||||
// CrowdSecDeleteDecision removes a ban decision by numeric ID
|
||||
func (api *API) CrowdSecDeleteDecision(w http.ResponseWriter, r *http.Request) {
|
||||
// DeleteDecision removes a ban decision by numeric ID
|
||||
func (h *crowdsecHandlers) DeleteDecision(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, err := strconv.Atoi(vars["id"])
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, "invalid decision ID")
|
||||
return
|
||||
}
|
||||
if err := api.crowdsec.DeleteDecision(id); err != nil {
|
||||
if err := h.crowdsec.DeleteDecision(id); err != nil {
|
||||
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to delete decision: %v", err))
|
||||
return
|
||||
}
|
||||
respondJSON(w, http.StatusOK, map[string]string{"message": "Decision deleted"})
|
||||
}
|
||||
|
||||
// CrowdSecAddDecision adds a manual ban or allow decision for an IP.
|
||||
// Body: { "ip": "1.2.3.4", "type": "ban", "reason": "manual", "duration": "24h" }
|
||||
func (api *API) CrowdSecAddDecision(w http.ResponseWriter, r *http.Request) {
|
||||
// AddDecision adds a manual ban or allow decision for an IP.
|
||||
func (h *crowdsecHandlers) AddDecision(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
IP string `json:"ip"`
|
||||
Type string `json:"type"`
|
||||
@@ -84,22 +106,22 @@ func (api *API) CrowdSecAddDecision(w http.ResponseWriter, r *http.Request) {
|
||||
if req.Duration == "" {
|
||||
req.Duration = "24h"
|
||||
}
|
||||
if err := api.crowdsec.AddDecision(req.IP, req.Type, req.Reason, req.Duration); err != nil {
|
||||
if err := h.crowdsec.AddDecision(req.IP, req.Type, req.Reason, req.Duration); err != nil {
|
||||
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to add decision: %v", err))
|
||||
return
|
||||
}
|
||||
respondJSON(w, http.StatusOK, map[string]string{"message": fmt.Sprintf("Decision added: %s %s for %s", req.Type, req.IP, req.Duration)})
|
||||
}
|
||||
|
||||
// CrowdSecDeleteDecisionByIP removes all decisions for a specific IP address
|
||||
func (api *API) CrowdSecDeleteDecisionByIP(w http.ResponseWriter, r *http.Request) {
|
||||
// DeleteDecisionByIP removes all decisions for a specific IP address
|
||||
func (h *crowdsecHandlers) DeleteDecisionByIP(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
ip := vars["ip"]
|
||||
if ip == "" {
|
||||
respondError(w, http.StatusBadRequest, "ip is required")
|
||||
return
|
||||
}
|
||||
if err := api.crowdsec.DeleteDecisionByIP(ip); err != nil {
|
||||
if err := h.crowdsec.DeleteDecisionByIP(ip); err != nil {
|
||||
// cscli exits non-zero when there's nothing to delete — treat as success
|
||||
if strings.Contains(err.Error(), "no decision") || strings.Contains(err.Error(), "0 decision") {
|
||||
respondJSON(w, http.StatusOK, map[string]string{"message": "No active decision for " + ip})
|
||||
@@ -111,16 +133,15 @@ func (api *API) CrowdSecDeleteDecisionByIP(w http.ResponseWriter, r *http.Reques
|
||||
respondJSON(w, http.StatusOK, map[string]string{"message": "Decisions removed for " + ip})
|
||||
}
|
||||
|
||||
// CrowdSecGetAlerts returns recent detection events from registered agents.
|
||||
// Query param: limit (default 50)
|
||||
func (api *API) CrowdSecGetAlerts(w http.ResponseWriter, r *http.Request) {
|
||||
// GetAlerts returns recent detection events from registered agents.
|
||||
func (h *crowdsecHandlers) GetAlerts(w http.ResponseWriter, r *http.Request) {
|
||||
limit := 50
|
||||
if l := r.URL.Query().Get("limit"); l != "" {
|
||||
if n, err := strconv.Atoi(l); err == nil && n > 0 && n <= 200 {
|
||||
limit = n
|
||||
}
|
||||
}
|
||||
alerts, err := api.crowdsec.GetAlerts(limit)
|
||||
alerts, err := h.crowdsec.GetAlerts(limit)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get alerts: %v", err))
|
||||
return
|
||||
@@ -128,9 +149,9 @@ func (api *API) CrowdSecGetAlerts(w http.ResponseWriter, r *http.Request) {
|
||||
respondJSON(w, http.StatusOK, map[string]any{"alerts": alerts})
|
||||
}
|
||||
|
||||
// CrowdSecGetMachines returns all registered CrowdSec agent machines
|
||||
func (api *API) CrowdSecGetMachines(w http.ResponseWriter, r *http.Request) {
|
||||
machines, err := api.crowdsec.GetMachines()
|
||||
// GetMachines returns all registered CrowdSec agent machines
|
||||
func (h *crowdsecHandlers) GetMachines(w http.ResponseWriter, r *http.Request) {
|
||||
machines, err := h.crowdsec.GetMachines()
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get machines: %v", err))
|
||||
return
|
||||
@@ -138,20 +159,20 @@ func (api *API) CrowdSecGetMachines(w http.ResponseWriter, r *http.Request) {
|
||||
respondJSON(w, http.StatusOK, map[string]any{"machines": machines})
|
||||
}
|
||||
|
||||
// CrowdSecDeleteMachine removes a registered machine
|
||||
func (api *API) CrowdSecDeleteMachine(w http.ResponseWriter, r *http.Request) {
|
||||
// DeleteMachine removes a registered machine
|
||||
func (h *crowdsecHandlers) DeleteMachine(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
name := vars["name"]
|
||||
if err := api.crowdsec.DeleteMachine(name); err != nil {
|
||||
if err := h.crowdsec.DeleteMachine(name); err != nil {
|
||||
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to delete machine: %v", err))
|
||||
return
|
||||
}
|
||||
respondJSON(w, http.StatusOK, map[string]string{"message": "Machine deleted"})
|
||||
}
|
||||
|
||||
// CrowdSecGetBouncers returns all registered bouncers
|
||||
func (api *API) CrowdSecGetBouncers(w http.ResponseWriter, r *http.Request) {
|
||||
bouncers, err := api.crowdsec.GetBouncers()
|
||||
// GetBouncers returns all registered bouncers
|
||||
func (h *crowdsecHandlers) GetBouncers(w http.ResponseWriter, r *http.Request) {
|
||||
bouncers, err := h.crowdsec.GetBouncers()
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get bouncers: %v", err))
|
||||
return
|
||||
@@ -159,20 +180,19 @@ func (api *API) CrowdSecGetBouncers(w http.ResponseWriter, r *http.Request) {
|
||||
respondJSON(w, http.StatusOK, map[string]any{"bouncers": bouncers})
|
||||
}
|
||||
|
||||
// CrowdSecDeleteBouncer removes a registered bouncer
|
||||
func (api *API) CrowdSecDeleteBouncer(w http.ResponseWriter, r *http.Request) {
|
||||
// DeleteBouncer removes a registered bouncer
|
||||
func (h *crowdsecHandlers) DeleteBouncer(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
name := vars["name"]
|
||||
if err := api.crowdsec.DeleteBouncer(name); err != nil {
|
||||
if err := h.crowdsec.DeleteBouncer(name); err != nil {
|
||||
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to delete bouncer: %v", err))
|
||||
return
|
||||
}
|
||||
respondJSON(w, http.StatusOK, map[string]string{"message": "Bouncer deleted"})
|
||||
}
|
||||
|
||||
// CrowdSecAddMachine registers a new CrowdSec agent machine with pre-generated credentials.
|
||||
// Body: { "name": "...", "password": "..." }
|
||||
func (api *API) CrowdSecAddMachine(w http.ResponseWriter, r *http.Request) {
|
||||
// AddMachine registers a new CrowdSec agent machine with pre-generated credentials.
|
||||
func (h *crowdsecHandlers) AddMachine(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
Password string `json:"password"`
|
||||
@@ -185,16 +205,15 @@ func (api *API) CrowdSecAddMachine(w http.ResponseWriter, r *http.Request) {
|
||||
respondError(w, http.StatusBadRequest, "name and password are required")
|
||||
return
|
||||
}
|
||||
if err := api.crowdsec.AddMachine(req.Name, req.Password); err != nil {
|
||||
if err := h.crowdsec.AddMachine(req.Name, req.Password); err != nil {
|
||||
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to add machine: %v", err))
|
||||
return
|
||||
}
|
||||
respondJSON(w, http.StatusCreated, map[string]string{"message": fmt.Sprintf("Machine %s registered", req.Name)})
|
||||
}
|
||||
|
||||
// CrowdSecAddBouncer registers a new bouncer with a pre-generated API key.
|
||||
// Body: { "name": "...", "apiKey": "..." }
|
||||
func (api *API) CrowdSecAddBouncer(w http.ResponseWriter, r *http.Request) {
|
||||
// AddBouncer registers a new bouncer with a pre-generated API key.
|
||||
func (h *crowdsecHandlers) AddBouncer(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
APIKey string `json:"apiKey"`
|
||||
@@ -207,7 +226,7 @@ func (api *API) CrowdSecAddBouncer(w http.ResponseWriter, r *http.Request) {
|
||||
respondError(w, http.StatusBadRequest, "name and apiKey are required")
|
||||
return
|
||||
}
|
||||
if err := api.crowdsec.AddBouncer(req.Name, req.APIKey); err != nil {
|
||||
if err := h.crowdsec.AddBouncer(req.Name, req.APIKey); err != nil {
|
||||
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to add bouncer: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user