feat: Add service registration API
Add the service registration abstraction — the key contract between Central and its consumers (Wild Cloud, Wild Works). Services register with Central to get DNS, gateway routing, TLS, and public exposure. - internal/services/manager.go: CRUD for service registrations stored as YAML files, with reach model (off/internal/public), backend types (tcp-passthrough/http/static), and TLS modes - internal/api/v1/handlers_services.go: HTTP endpoints POST/GET/PATCH/DELETE /api/v1/services - Full test coverage for registration, update, deregister, validation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/wild-cloud/wild-central/internal/network"
|
||||
"github.com/wild-cloud/wild-central/internal/nftables"
|
||||
"github.com/wild-cloud/wild-central/internal/secrets"
|
||||
"github.com/wild-cloud/wild-central/internal/services"
|
||||
"github.com/wild-cloud/wild-central/internal/sse"
|
||||
"github.com/wild-cloud/wild-central/internal/storage"
|
||||
"github.com/wild-cloud/wild-central/internal/wireguard"
|
||||
@@ -44,7 +45,8 @@ type API struct {
|
||||
crowdsec *crowdsec.Manager
|
||||
vpn *wireguard.Manager
|
||||
certbot *certbot.Manager
|
||||
sseManager *sse.Manager // SSE manager for real-time events
|
||||
services *services.Manager // Service registration manager
|
||||
sseManager *sse.Manager // SSE manager for real-time events
|
||||
}
|
||||
|
||||
// NewAPI creates a new Central API handler with all dependencies
|
||||
@@ -76,6 +78,7 @@ func NewAPI(dataDir, version string, allowedOrigins []string) (*API, error) {
|
||||
crowdsec: crowdsec.NewManager(),
|
||||
vpn: wireguard.NewManager(dataDir, vpnConfigPath),
|
||||
certbot: certbot.NewManager(""),
|
||||
services: services.NewManager(dataDir),
|
||||
sseManager: sseManager,
|
||||
}
|
||||
|
||||
@@ -205,6 +208,13 @@ func (api *API) RegisterRoutes(r *mux.Router) {
|
||||
r.HandleFunc("/api/v1/crowdsec/bouncers", api.CrowdSecGetBouncers).Methods("GET")
|
||||
r.HandleFunc("/api/v1/crowdsec/bouncers/{name}", api.CrowdSecDeleteBouncer).Methods("DELETE")
|
||||
|
||||
// Service registration
|
||||
r.HandleFunc("/api/v1/services", api.ServicesListAll).Methods("GET")
|
||||
r.HandleFunc("/api/v1/services", api.ServicesRegister).Methods("POST")
|
||||
r.HandleFunc("/api/v1/services/{name}", api.ServicesGet).Methods("GET")
|
||||
r.HandleFunc("/api/v1/services/{name}", api.ServicesUpdate).Methods("PATCH")
|
||||
r.HandleFunc("/api/v1/services/{name}", api.ServicesDeregister).Methods("DELETE")
|
||||
|
||||
// SSE events
|
||||
r.HandleFunc("/api/v1/events", api.GlobalEventStream).Methods("GET")
|
||||
}
|
||||
|
||||
94
internal/api/v1/handlers_services.go
Normal file
94
internal/api/v1/handlers_services.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/wild-cloud/wild-central/internal/services"
|
||||
)
|
||||
|
||||
// ServicesListAll lists all registered services.
|
||||
func (api *API) ServicesListAll(w http.ResponseWriter, r *http.Request) {
|
||||
svcs, err := api.services.List()
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to list services: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if svcs == nil {
|
||||
svcs = []services.Service{}
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, map[string]any{
|
||||
"services": svcs,
|
||||
})
|
||||
}
|
||||
|
||||
// ServicesGet retrieves a service by name.
|
||||
func (api *API) ServicesGet(w http.ResponseWriter, r *http.Request) {
|
||||
name := mux.Vars(r)["name"]
|
||||
|
||||
svc, err := api.services.Get(name)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusNotFound, fmt.Sprintf("Service not found: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, svc)
|
||||
}
|
||||
|
||||
// ServicesRegister creates or updates a service registration.
|
||||
func (api *API) ServicesRegister(w http.ResponseWriter, r *http.Request) {
|
||||
var svc services.Service
|
||||
if err := json.NewDecoder(r.Body).Decode(&svc); err != nil {
|
||||
respondError(w, http.StatusBadRequest, fmt.Sprintf("Invalid request body: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := api.services.Register(svc); err != nil {
|
||||
respondError(w, http.StatusBadRequest, fmt.Sprintf("Failed to register service: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusCreated, map[string]any{
|
||||
"message": fmt.Sprintf("Service %q registered", svc.Name),
|
||||
"service": svc,
|
||||
})
|
||||
}
|
||||
|
||||
// ServicesUpdate applies partial updates to a service.
|
||||
func (api *API) ServicesUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
name := mux.Vars(r)["name"]
|
||||
|
||||
var updates map[string]any
|
||||
if err := json.NewDecoder(r.Body).Decode(&updates); err != nil {
|
||||
respondError(w, http.StatusBadRequest, fmt.Sprintf("Invalid request body: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := api.services.Update(name, updates); err != nil {
|
||||
respondError(w, http.StatusNotFound, fmt.Sprintf("Failed to update service: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
svc, _ := api.services.Get(name)
|
||||
respondJSON(w, http.StatusOK, map[string]any{
|
||||
"message": fmt.Sprintf("Service %q updated", name),
|
||||
"service": svc,
|
||||
})
|
||||
}
|
||||
|
||||
// ServicesDeregister removes a service registration.
|
||||
func (api *API) ServicesDeregister(w http.ResponseWriter, r *http.Request) {
|
||||
name := mux.Vars(r)["name"]
|
||||
|
||||
if err := api.services.Deregister(name); err != nil {
|
||||
respondError(w, http.StatusNotFound, fmt.Sprintf("Failed to deregister service: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
respondMessage(w, http.StatusOK, fmt.Sprintf("Service %q deregistered", name))
|
||||
}
|
||||
Reference in New Issue
Block a user