refactor: Replace 'reach' field with 'public' boolean in service registration and related components

This commit is contained in:
2026-07-10 02:13:37 +00:00
parent fffc84e14c
commit 4feaa63da0
12 changed files with 112 additions and 220 deletions

View File

@@ -221,8 +221,10 @@ func (api *API) RegisterRoutes(r *mux.Router) {
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")
// Cloudflare management

View File

@@ -170,5 +170,46 @@ func (api *API) CrowdSecDeleteBouncer(w http.ResponseWriter, r *http.Request) {
respondJSON(w, http.StatusOK, map[string]string{"message": "Bouncer deleted"})
}
// Note: CrowdSecProvision (instance-specific provisioning) has been removed.
// Instance provisioning will be handled by the Wild Cloud API, not Wild Central.
// CrowdSecAddMachine registers a new CrowdSec agent machine with pre-generated credentials.
// Body: { "name": "...", "password": "..." }
func (api *API) CrowdSecAddMachine(w http.ResponseWriter, r *http.Request) {
var req struct {
Name string `json:"name"`
Password string `json:"password"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "invalid request body")
return
}
if req.Name == "" || req.Password == "" {
respondError(w, http.StatusBadRequest, "name and password are required")
return
}
if err := api.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) {
var req struct {
Name string `json:"name"`
APIKey string `json:"apiKey"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "invalid request body")
return
}
if req.Name == "" || req.APIKey == "" {
respondError(w, http.StatusBadRequest, "name and apiKey are required")
return
}
if err := api.crowdsec.AddBouncer(req.Name, req.APIKey); err != nil {
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to add bouncer: %v", err))
return
}
respondJSON(w, http.StatusCreated, map[string]string{"message": fmt.Sprintf("Bouncer %s registered", req.Name)})
}

View File

@@ -26,26 +26,26 @@ func TestReconciliation_HAProxyConfigFromServices(t *testing.T) {
Source: "wild-cloud",
Backend: services.Backend{Address: "192.168.8.240:443", Type: services.BackendTCPPassthrough},
Subdomains: true,
Reach: services.ReachPublic,
Public: true,
},
{
Domain: "payne.io",
Source: "wild-cloud",
Backend: services.Backend{Address: "192.168.8.240:443", Type: services.BackendTCPPassthrough},
Subdomains: false,
Reach: services.ReachPublic,
Public: true,
},
{
Domain: "wild-cloud.payne.io",
Source: "wild-works",
Backend: services.Backend{Address: "127.0.0.1:5055", Type: services.BackendHTTP},
Reach: services.ReachInternal,
// Public defaults to false
},
{
Domain: "my-api.payne.io",
Source: "wild-works",
Backend: services.Backend{Address: "192.168.8.60:9001", Type: services.BackendHTTP, Health: "/health"},
Reach: services.ReachInternal,
// Public defaults to false
},
}
@@ -88,7 +88,7 @@ func TestReconciliation_HAProxyConfigFromServices(t *testing.T) {
Domain: "central.payne.io",
Source: "central",
Backend: services.Backend{Address: "127.0.0.1:5055", Type: services.BackendHTTP},
Reach: services.ReachInternal,
// Public defaults to false
TLS: services.TLSTerminate,
}); err != nil {
t.Fatalf("Register central failed: %v", err)
@@ -181,21 +181,21 @@ func TestReconciliation_DnsmasqConfigFromServices(t *testing.T) {
Source: "wild-cloud",
Backend: services.Backend{Address: "192.168.8.240:443", Type: services.BackendTCPPassthrough},
Subdomains: true,
Reach: services.ReachPublic,
Public: true,
},
{
// http, internal → DNS points to Central IP, has local=/
Domain: "my-api.payne.io",
Source: "wild-works",
Backend: services.Backend{Address: "192.168.8.60:9001", Type: services.BackendHTTP},
Reach: services.ReachInternal,
// Public defaults to false
},
{
// http, public → DNS points to Central IP, NO local=/
Domain: "public-app.payne.io",
Source: "wild-works",
Backend: services.Backend{Address: "192.168.8.60:8080", Type: services.BackendHTTP},
Reach: services.ReachPublic,
Public: true,
},
}
@@ -227,7 +227,7 @@ func TestReconciliation_DnsmasqConfigFromServices(t *testing.T) {
ic := config.InstanceConfig{}
ic.Cluster.LoadBalancerIp = dnsIP
if svc.Reach == services.ReachInternal {
if !svc.Public {
ic.Cloud.InternalDomain = svc.Domain
} else {
ic.Cloud.Domain = svc.Domain
@@ -277,7 +277,7 @@ func TestReconciliation_CentralDomainInHAProxy(t *testing.T) {
Domain: "central.payne.io",
Source: "central",
Backend: services.Backend{Address: fmt.Sprintf("127.0.0.1:%d", centralPort), Type: services.BackendHTTP},
Reach: services.ReachInternal,
// Public defaults to false
TLS: services.TLSTerminate,
}); err != nil {
t.Fatalf("Register central failed: %v", err)
@@ -288,7 +288,7 @@ func TestReconciliation_CentralDomainInHAProxy(t *testing.T) {
Domain: "my-app.payne.io",
Source: "wild-works",
Backend: services.Backend{Address: "192.168.8.60:9001", Type: services.BackendHTTP},
Reach: services.ReachInternal,
// Public defaults to false
}); err != nil {
t.Fatalf("Register failed: %v", err)
}
@@ -337,7 +337,7 @@ func TestReconciliation_TCPPassthroughDNSTarget(t *testing.T) {
Source: "wild-cloud",
Backend: services.Backend{Address: "192.168.8.240:443", Type: services.BackendTCPPassthrough},
Subdomains: true,
Reach: services.ReachPublic,
Public: true,
}); err != nil {
t.Fatalf("Register failed: %v", err)
}
@@ -354,7 +354,7 @@ func TestReconciliation_TCPPassthroughDNSTarget(t *testing.T) {
ic := config.InstanceConfig{}
ic.Cluster.LoadBalancerIp = dnsIP
if svc.Reach == services.ReachInternal {
if !svc.Public {
ic.Cloud.InternalDomain = svc.Domain
} else {
ic.Cloud.Domain = svc.Domain

View File

@@ -20,7 +20,6 @@ func TestServicesRegister(t *testing.T) {
"address": "192.168.8.60:9001",
"type": "http",
},
"reach": "internal",
})
req := httptest.NewRequest("POST", "/api/v1/services", bytes.NewReader(body))
@@ -57,7 +56,7 @@ func TestServicesRegister_TCPPassthrough(t *testing.T) {
"type": "tcp-passthrough",
},
"subdomains": true,
"reach": "public",
"public": true,
})
req := httptest.NewRequest("POST", "/api/v1/services", bytes.NewReader(body))
@@ -107,7 +106,6 @@ func TestServicesGet(t *testing.T) {
"domain": "get-test.example.com",
"source": "test",
"backend": map[string]any{"address": "127.0.0.1:9000", "type": "http"},
"reach": "internal",
})
req := httptest.NewRequest("POST", "/api/v1/services", bytes.NewReader(body))
w := httptest.NewRecorder()
@@ -151,14 +149,13 @@ func TestServicesUpdate(t *testing.T) {
"domain": "update-test.example.com",
"source": "test",
"backend": map[string]any{"address": "127.0.0.1:9000", "type": "http"},
"reach": "internal",
})
req := httptest.NewRequest("POST", "/api/v1/services", bytes.NewReader(body))
w := httptest.NewRecorder()
api.ServicesRegister(w, req)
// Update reach to public
update, _ := json.Marshal(map[string]any{"reach": "public"})
// Update public to true
update, _ := json.Marshal(map[string]any{"public": true})
req = httptest.NewRequest("PATCH", "/api/v1/services/update-test.example.com", bytes.NewReader(update))
req = mux.SetURLVars(req, map[string]string{"domain": "update-test.example.com"})
w = httptest.NewRecorder()
@@ -171,8 +168,8 @@ func TestServicesUpdate(t *testing.T) {
var resp map[string]any
json.Unmarshal(w.Body.Bytes(), &resp)
svc := resp["service"].(map[string]any)
if svc["reach"] != "public" {
t.Errorf("expected reach=public, got %v", svc["reach"])
if svc["public"] != true {
t.Errorf("expected public=true, got %v", svc["public"])
}
}
@@ -184,7 +181,6 @@ func TestServicesDeregister(t *testing.T) {
"domain": "delete-me.example.com",
"source": "test",
"backend": map[string]any{"address": "127.0.0.1:9000", "type": "http"},
"reach": "internal",
})
req := httptest.NewRequest("POST", "/api/v1/services", bytes.NewReader(body))
w := httptest.NewRecorder()
@@ -216,7 +212,6 @@ func TestServicesRegister_Validation(t *testing.T) {
// Missing domain
body, _ := json.Marshal(map[string]any{
"backend": map[string]any{"address": "127.0.0.1:80", "type": "http"},
"reach": "internal",
})
req := httptest.NewRequest("POST", "/api/v1/services", bytes.NewReader(body))
w := httptest.NewRecorder()

View File

@@ -52,7 +52,7 @@ func (api *API) EnsureCentralService() {
Address: backend,
Type: services.BackendHTTP,
},
Reach: services.ReachInternal,
// Public defaults to false (private)
TLS: services.TLSTerminate,
})
}
@@ -159,7 +159,7 @@ func (api *API) reconcileNetworking() {
ic := config.InstanceConfig{}
ic.Cluster.LoadBalancerIp = dnsIP
if svc.Reach == services.ReachInternal {
if !svc.Public {
// Internal-only: local=/ prevents upstream DNS forwarding
ic.Cloud.InternalDomain = svc.Domain
} else {

View File

@@ -17,14 +17,6 @@ import (
"gopkg.in/yaml.v3"
)
// Reach describes how a service is exposed.
type Reach string
const (
ReachInternal Reach = "internal" // LAN-visible — DNS + proxy + TLS
ReachPublic Reach = "public" // internet-visible — + DDNS + external HAProxy
)
// BackendType describes how the gateway handles traffic for this service.
type BackendType string
@@ -54,7 +46,7 @@ type Service struct {
Source string `yaml:"source,omitempty" json:"source,omitempty"` // who registered: wild-cloud, wild-works, manual
Backend Backend `yaml:"backend" json:"backend"` // where to route
Subdomains bool `yaml:"subdomains,omitempty" json:"subdomains,omitempty"` // also match *.domain
Reach Reach `yaml:"reach" json:"reach"` // internal or public
Public bool `yaml:"public,omitempty" json:"public,omitempty"` // true = internet-visible (DDNS + external), false = LAN only
TLS TLSMode `yaml:"tls,omitempty" json:"tls,omitempty"` // passthrough or terminate
}
@@ -106,9 +98,7 @@ func (m *Manager) Register(svc Service) error {
if svc.Backend.Type == "" {
return fmt.Errorf("backend type is required")
}
if svc.Reach == "" {
return fmt.Errorf("reach is required")
}
// Public field is a bool — no validation needed (defaults to false)
// Default TLS mode based on backend type
if svc.TLS == "" {
@@ -133,7 +123,7 @@ func (m *Manager) Register(svc Service) error {
return fmt.Errorf("writing service file: %w", err)
}
slog.Info("service registered", "domain", svc.Domain, "reach", svc.Reach, "source", svc.Source, "subdomains", svc.Subdomains)
slog.Info("service registered", "domain", svc.Domain, "public", svc.Public, "source", svc.Source, "subdomains", svc.Subdomains)
if m.reconcileFn != nil {
go m.reconcileFn()
@@ -249,8 +239,8 @@ func (m *Manager) Update(domain string, updates map[string]any) error {
return err
}
if reach, ok := updates["reach"].(string); ok {
svc.Reach = Reach(reach)
if public, ok := updates["public"].(bool); ok {
svc.Public = public
}
if sub, ok := updates["subdomains"].(bool); ok {
svc.Subdomains = sub

View File

@@ -16,7 +16,7 @@ func TestRegisterAndGet(t *testing.T) {
Address: "192.168.8.60:9001",
Type: BackendHTTP,
},
Reach: ReachInternal,
// Public defaults to false
}
if err := mgr.Register(svc); err != nil {
@@ -53,7 +53,7 @@ func TestRegisterTCPPassthrough(t *testing.T) {
Type: BackendTCPPassthrough,
},
Subdomains: true,
Reach: ReachPublic,
Public: true,
}
if err := mgr.Register(svc); err != nil {
@@ -71,8 +71,8 @@ func TestRegisterTCPPassthrough(t *testing.T) {
if !got.Subdomains {
t.Error("expected subdomains=true")
}
if got.Reach != ReachPublic {
t.Errorf("expected reach public, got %s", got.Reach)
if !got.Public {
t.Error("expected public=true")
}
}
@@ -84,7 +84,7 @@ func TestListServices(t *testing.T) {
Domain: domain,
Source: "test",
Backend: Backend{Address: "127.0.0.1:8080", Type: BackendHTTP},
Reach: ReachInternal,
// Public defaults to false
}); err != nil {
t.Fatalf("Register %s failed: %v", domain, err)
}
@@ -106,7 +106,7 @@ func TestDeregister(t *testing.T) {
Domain: "temp.example.com",
Source: "test",
Backend: Backend{Address: "127.0.0.1:8080", Type: BackendHTTP},
Reach: ReachInternal,
// Public defaults to false
}); err != nil {
t.Fatalf("Register failed: %v", err)
}
@@ -129,7 +129,7 @@ func TestDeregisterBySource(t *testing.T) {
Domain: domain,
Source: "wild-cloud",
Backend: Backend{Address: "192.168.8.240:443", Type: BackendTCPPassthrough},
Reach: ReachPublic,
Public: true,
})
}
// Register 1 service from wild-works (different source)
@@ -137,7 +137,7 @@ func TestDeregisterBySource(t *testing.T) {
Domain: "my-api.payne.io",
Source: "wild-works",
Backend: Backend{Address: "127.0.0.1:9001", Type: BackendHTTP},
Reach: ReachInternal,
// Public defaults to false
})
// Deregister all wild-cloud services with backend 192.168.8.240:443
@@ -161,19 +161,19 @@ func TestUpdate(t *testing.T) {
Domain: "updatable.example.com",
Source: "test",
Backend: Backend{Address: "127.0.0.1:8080", Type: BackendHTTP},
Reach: ReachInternal,
// Public defaults to false
})
if err := mgr.Update("updatable.example.com", map[string]any{
"reach": "public",
"public": true,
"subdomains": true,
}); err != nil {
t.Fatalf("Update failed: %v", err)
}
got, _ := mgr.Get("updatable.example.com")
if got.Reach != ReachPublic {
t.Errorf("expected reach public after update, got %s", got.Reach)
if !got.Public {
t.Error("expected public=true after update")
}
if !got.Subdomains {
t.Error("expected subdomains=true after update")
@@ -184,24 +184,19 @@ func TestRegisterValidation(t *testing.T) {
mgr := NewManager(t.TempDir())
// Missing domain
if err := mgr.Register(Service{Backend: Backend{Address: "127.0.0.1:80", Type: BackendHTTP}, Reach: ReachInternal}); err == nil {
if err := mgr.Register(Service{Backend: Backend{Address: "127.0.0.1:80", Type: BackendHTTP}}); err == nil {
t.Error("expected error for missing domain")
}
// Missing backend address
if err := mgr.Register(Service{Domain: "x.com", Backend: Backend{Type: BackendHTTP}, Reach: ReachInternal}); err == nil {
if err := mgr.Register(Service{Domain: "x.com", Backend: Backend{Type: BackendHTTP}}); err == nil {
t.Error("expected error for missing backend address")
}
// Missing backend type
if err := mgr.Register(Service{Domain: "x.com", Backend: Backend{Address: "127.0.0.1:80"}, Reach: ReachInternal}); err == nil {
if err := mgr.Register(Service{Domain: "x.com", Backend: Backend{Address: "127.0.0.1:80"}}); err == nil {
t.Error("expected error for missing backend type")
}
// Missing reach
if err := mgr.Register(Service{Domain: "x.com", Backend: Backend{Address: "127.0.0.1:80", Type: BackendHTTP}}); err == nil {
t.Error("expected error for missing reach")
}
}
func TestDefaultSource(t *testing.T) {
@@ -210,7 +205,7 @@ func TestDefaultSource(t *testing.T) {
mgr.Register(Service{
Domain: "test.com",
Backend: Backend{Address: "127.0.0.1:80", Type: BackendHTTP},
Reach: ReachInternal,
// Public defaults to false
})
got, _ := mgr.Get("test.com")
@@ -226,7 +221,7 @@ func TestRegisterIdempotent(t *testing.T) {
Domain: "idempotent.example.com",
Source: "test",
Backend: Backend{Address: "127.0.0.1:8080", Type: BackendHTTP},
Reach: ReachInternal,
// Public defaults to false
}
// Register twice with same domain
@@ -274,7 +269,7 @@ func TestListNoDuplicates(t *testing.T) {
Domain: domain,
Source: "test",
Backend: Backend{Address: "127.0.0.1:8080", Type: BackendHTTP},
Reach: ReachInternal,
// Public defaults to false
}); err != nil {
t.Fatalf("Register %s failed: %v", domain, err)
}
@@ -285,7 +280,7 @@ func TestListNoDuplicates(t *testing.T) {
Domain: "beta.example.com",
Source: "test",
Backend: Backend{Address: "127.0.0.1:9090", Type: BackendHTTP},
Reach: ReachInternal,
// Public defaults to false
}); err != nil {
t.Fatalf("Re-register beta failed: %v", err)
}
@@ -353,13 +348,13 @@ func TestDeregisterBySource_NoMatch(t *testing.T) {
Domain: "a.example.com",
Source: "wild-cloud",
Backend: Backend{Address: "10.0.0.1:443", Type: BackendTCPPassthrough},
Reach: ReachPublic,
Public: true,
})
mgr.Register(Service{
Domain: "b.example.com",
Source: "wild-works",
Backend: Backend{Address: "10.0.0.2:8080", Type: BackendHTTP},
Reach: ReachInternal,
// Public defaults to false
})
// Deregister with a source that doesn't match anything
@@ -383,13 +378,13 @@ func TestDeregisterBySource_PartialMatch(t *testing.T) {
Domain: "a.example.com",
Source: "wild-cloud",
Backend: Backend{Address: "10.0.0.1:443", Type: BackendTCPPassthrough},
Reach: ReachPublic,
Public: true,
})
mgr.Register(Service{
Domain: "b.example.com",
Source: "wild-cloud",
Backend: Backend{Address: "10.0.0.2:443", Type: BackendTCPPassthrough},
Reach: ReachPublic,
Public: true,
})
// Deregister only the ones matching source AND backend
@@ -411,7 +406,7 @@ func TestUpdate_NotFound(t *testing.T) {
mgr := NewManager(t.TempDir())
err := mgr.Update("nonexistent.example.com", map[string]any{
"reach": "public",
"public": true,
})
if err == nil {
t.Error("expected error when updating non-existent domain, got nil")
@@ -426,7 +421,7 @@ func TestRegister_OverwritePreservesFile(t *testing.T) {
Domain: "overwrite.example.com",
Source: "test",
Backend: Backend{Address: "127.0.0.1:8080", Type: BackendHTTP},
Reach: ReachInternal,
// Public defaults to false
}
// Register first time
@@ -477,7 +472,7 @@ func TestList_IgnoresNonYAML(t *testing.T) {
Domain: "real.example.com",
Source: "test",
Backend: Backend{Address: "127.0.0.1:8080", Type: BackendHTTP},
Reach: ReachInternal,
// Public defaults to false
})
// Create non-YAML files in the services directory