services -> domains

This commit is contained in:
2026-07-10 20:46:22 +00:00
parent 3c99d26830
commit 79c0c32b98
45 changed files with 1447 additions and 1270 deletions

View File

@@ -7,6 +7,7 @@ import (
"log/slog"
"net/http"
"os"
"os/exec"
"path/filepath"
"syscall"
"time"
@@ -25,7 +26,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/domains"
"github.com/wild-cloud/wild-central/internal/sse"
"github.com/wild-cloud/wild-central/internal/storage"
"github.com/wild-cloud/wild-central/internal/wireguard"
@@ -46,7 +47,7 @@ type API struct {
crowdsec *crowdsec.Manager
vpn *wireguard.Manager
certbot *certbot.Manager
services *services.Manager // Service registration manager
domains *domains.Manager // Domain registration manager
sseManager *sse.Manager // SSE manager for real-time events
port int // Running API port (for config-driven routes)
}
@@ -76,13 +77,13 @@ func NewAPI(dataDir, version string, allowedOrigins []string) (*API, error) {
crowdsec: crowdsec.NewManager(),
vpn: wireguard.NewManager(dataDir, vpnConfigPath),
certbot: certbot.NewManager(""),
services: services.NewManager(dataDir),
domains: domains.NewManager(dataDir),
sseManager: sseManager,
}
// Wire up service registration reconciliation: when services change,
// Wire up domain registration reconciliation: when domains change,
// regenerate dnsmasq DNS entries and HAProxy routes.
api.services.SetReconcileFn(api.reconcileNetworking)
api.domains.SetReconcileFn(api.reconcileNetworking)
return api, nil
}
@@ -107,30 +108,49 @@ func envOrDefault(key, defaultVal string) string {
return defaultVal
}
// StartDDNS loads DDNS config and starts the background goroutine.
// StartDDNS launches the DDNS background goroutine with a params callback
// that reads fresh state (token, public domains, interval) on each tick.
func (api *API) StartDDNS(ctx gocontext.Context) {
api.ctx = ctx
api.reloadDDNSIfEnabled()
api.ddns.Start(ctx, api.ddnsParams)
}
// reloadDDNSIfEnabled re-reads DDNS config and secrets and restarts the DDNS goroutine.
// ddnsParams returns the current DDNS parameters derived from runtime state.
// Called by the DDNS runner on each tick — always returns fresh values.
func (api *API) ddnsParams() ddns.Params {
state, err := config.LoadState(api.statePath())
if err != nil || !state.Cloud.DDNS.Enabled {
return ddns.Params{}
}
doms, _ := api.domains.List()
var records []string
for _, dom := range doms {
if dom.Public && dom.DomainName != "" {
records = append(records, dom.DomainName)
}
}
return ddns.Params{
APIToken: api.getCloudflareToken(),
Records: records,
IntervalMinutes: state.Cloud.DDNS.IntervalMinutes,
}
}
// reloadDDNSIfEnabled starts the runner if DDNS is enabled, or stops it if disabled.
// When already running, triggers an immediate check with fresh params.
func (api *API) reloadDDNSIfEnabled() {
globalCfg, err := config.LoadState(api.statePath())
if err != nil || !globalCfg.Cloud.DDNS.Enabled {
state, _ := config.LoadState(api.statePath())
if state == nil || !state.Cloud.DDNS.Enabled {
api.ddns.Stop()
return
}
apiToken := api.getCloudflareToken()
cfg := ddns.Config{
Enabled: true,
APIToken: apiToken,
Records: globalCfg.Cloud.DDNS.Records,
IntervalMinutes: globalCfg.Cloud.DDNS.IntervalMinutes,
if !api.ddns.GetStatus().Enabled {
api.ddns.Start(api.ctx, api.ddnsParams)
} else {
api.ddns.Trigger()
}
api.ddns.Start(api.ctx, cfg)
}
// StartCentralStatusBroadcaster starts periodic broadcasting of central status
@@ -151,9 +171,9 @@ func (api *API) StartCentralStatusBroadcaster(startTime time.Time) {
func (api *API) RegisterRoutes(r *mux.Router) {
r.Use(RequestLoggingMiddleware)
// Global Configuration
r.HandleFunc("/api/v1/config", api.GetGlobalConfig).Methods("GET")
r.HandleFunc("/api/v1/config", api.UpdateGlobalConfig).Methods("PUT")
// Global State (runtime settings — operator, DDNS, DHCP, etc.)
r.HandleFunc("/api/v1/state", api.GetState).Methods("GET")
r.HandleFunc("/api/v1/state", api.UpdateState).Methods("PUT")
// Global Secrets
r.HandleFunc("/api/v1/secrets", api.GetGlobalSecrets).Methods("GET")
@@ -228,13 +248,13 @@ func (api *API) RegisterRoutes(r *mux.Router) {
r.HandleFunc("/api/v1/cloudflare/zone", api.CloudflareGetZoneID).Methods("GET")
r.HandleFunc("/api/v1/cloudflare/zone", api.CloudflareSetZoneID).Methods("POST")
// Service registration — domain is the unique key
r.HandleFunc("/api/v1/services", api.ServicesListAll).Methods("GET")
r.HandleFunc("/api/v1/services", api.ServicesRegister).Methods("POST")
r.HandleFunc("/api/v1/services/deregister", api.ServicesDeregisterBySource).Methods("DELETE")
r.HandleFunc("/api/v1/services/{domain:.+}", api.ServicesGet).Methods("GET")
r.HandleFunc("/api/v1/services/{domain:.+}", api.ServicesUpdate).Methods("PATCH")
r.HandleFunc("/api/v1/services/{domain:.+}", api.ServicesDeregister).Methods("DELETE")
// Domain registration — domain is the unique key
r.HandleFunc("/api/v1/domains", api.DomainsListAll).Methods("GET")
r.HandleFunc("/api/v1/domains", api.DomainsRegister).Methods("POST")
r.HandleFunc("/api/v1/domains/deregister", api.DomainsDeregisterBySource).Methods("DELETE")
r.HandleFunc("/api/v1/domains/{domain:.+}", api.DomainsGet).Methods("GET")
r.HandleFunc("/api/v1/domains/{domain:.+}", api.DomainsUpdate).Methods("PATCH")
r.HandleFunc("/api/v1/domains/{domain:.+}", api.DomainsDeregister).Methods("DELETE")
// SSE events
r.HandleFunc("/api/v1/events", api.GlobalEventStream).Methods("GET")
@@ -242,10 +262,9 @@ func (api *API) RegisterRoutes(r *mux.Router) {
// --- Global Config/Secrets handlers ---
// GetGlobalConfig returns the global state wrapped in { configured, config }.
func (api *API) GetGlobalConfig(w http.ResponseWriter, r *http.Request) {
configPath := api.statePath()
data, err := os.ReadFile(configPath)
// GetState returns the global state wrapped in { configured, state }.
func (api *API) GetState(w http.ResponseWriter, r *http.Request) {
data, err := os.ReadFile(api.statePath())
if err != nil {
respondJSON(w, http.StatusOK, map[string]any{
"configured": false,
@@ -253,20 +272,20 @@ func (api *API) GetGlobalConfig(w http.ResponseWriter, r *http.Request) {
return
}
var configMap map[string]any
if err := yaml.Unmarshal(data, &configMap); err != nil {
respondError(w, http.StatusInternalServerError, "Failed to parse config")
var stateMap map[string]any
if err := yaml.Unmarshal(data, &stateMap); err != nil {
respondError(w, http.StatusInternalServerError, "Failed to parse state")
return
}
respondJSON(w, http.StatusOK, map[string]any{
"configured": true,
"config": configMap,
"state": stateMap,
})
}
// UpdateGlobalConfig updates the global state
func (api *API) UpdateGlobalConfig(w http.ResponseWriter, r *http.Request) {
// UpdateState updates the global state
func (api *API) UpdateState(w http.ResponseWriter, r *http.Request) {
configPath := api.statePath()
body, err := io.ReadAll(r.Body)
@@ -320,7 +339,7 @@ func (api *API) UpdateGlobalConfig(w http.ResponseWriter, r *http.Request) {
// Reload DDNS and re-register Central's service if config changed
go api.reloadDDNSIfEnabled()
go api.EnsureCentralService()
go api.EnsureCentralDomain()
respondMessage(w, http.StatusOK, "Config updated successfully")
}
@@ -380,9 +399,32 @@ func (api *API) StatusHandler(w http.ResponseWriter, r *http.Request, startTime
"uptime": uptime.String(),
"uptimeSeconds": int(uptime.Seconds()),
"dataDir": dataDir,
"daemons": getDaemonStatus(),
})
}
// getDaemonStatus checks whether each managed daemon is active.
func getDaemonStatus() map[string]map[string]bool {
daemons := map[string]string{
"dnsmasq": "dnsmasq.service",
"haproxy": "haproxy.service",
"wireguard": "wg-quick@wg0.service",
"crowdsec": "crowdsec.service",
}
result := make(map[string]map[string]bool, len(daemons)+1)
for name, unit := range daemons {
err := exec.Command("systemctl", "is-active", "--quiet", unit).Run()
result[name] = map[string]bool{"active": err == nil}
}
// nftables has no persistent service — check if the wild-cloud table exists
err := exec.Command("sudo", "nft", "list", "table", "inet", "wild-cloud").Run()
result["nftables"] = map[string]bool{"active": err == nil}
return result
}
// DaemonRestart triggers a graceful self-restart via SIGTERM
func (api *API) DaemonRestart(w http.ResponseWriter, r *http.Request) {
respondJSON(w, http.StatusAccepted, map[string]any{"message": "Restarting Wild Central..."})