services -> domains
This commit is contained in:
@@ -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..."})
|
||||
|
||||
Reference in New Issue
Block a user