On startup, Wild Central registers its own domain (from config cloud.central.domain) as an HTTP service with itself. This creates the DNS entry, proxy route, and TLS cert needed to access the Central UI via its domain name. Registration is idempotent — safe on every restart. Skips silently if no domain is configured. Logs a warning if registration fails. Verified: after startup, /api/v1/services shows wild-central registered with domain central.payne.io. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
199 lines
5.0 KiB
Go
199 lines
5.0 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/gorilla/mux"
|
|
|
|
v1 "github.com/wild-cloud/wild-central/internal/api/v1"
|
|
"github.com/wild-cloud/wild-central/internal/frontend"
|
|
"github.com/wild-cloud/wild-central/internal/logging"
|
|
"github.com/wild-cloud/wild-central/internal/natsbus"
|
|
)
|
|
|
|
var startTime time.Time
|
|
|
|
// Version is set at build time via -ldflags "-X main.Version=x.y.z".
|
|
var Version = "dev"
|
|
|
|
func splitAndTrim(s string, sep string) []string {
|
|
parts := strings.Split(s, sep)
|
|
result := make([]string, 0, len(parts))
|
|
for _, part := range parts {
|
|
if trimmed := strings.TrimSpace(part); trimmed != "" {
|
|
result = append(result, trimmed)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func buildAllowedOrigins() []string {
|
|
if corsOrigins := os.Getenv("WILD_CENTRAL_CORS_ORIGINS"); corsOrigins != "" {
|
|
origins := splitAndTrim(corsOrigins, ",")
|
|
slog.Info("CORS configured with explicit origins", "origins", origins)
|
|
return origins
|
|
}
|
|
|
|
allowedOrigins := []string{
|
|
"http://localhost",
|
|
"http://localhost:80",
|
|
"http://127.0.0.1",
|
|
"http://127.0.0.1:80",
|
|
}
|
|
|
|
if hostname, err := os.Hostname(); err == nil && hostname != "" {
|
|
for _, port := range []string{"", ":80", ":5173", ":5174"} {
|
|
for _, suffix := range []string{"", ".local", ".lan"} {
|
|
allowedOrigins = append(allowedOrigins,
|
|
fmt.Sprintf("http://%s%s%s", hostname, suffix, port),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
allowedOrigins = append(allowedOrigins,
|
|
"http://localhost:5173",
|
|
"http://localhost:5174",
|
|
"http://localhost:3000",
|
|
"http://127.0.0.1:5173",
|
|
"http://127.0.0.1:5174",
|
|
"http://127.0.0.1:3000",
|
|
)
|
|
|
|
return allowedOrigins
|
|
}
|
|
|
|
func main() {
|
|
slog.SetDefault(slog.New(logging.NewConsoleHandler(os.Stderr, &slog.HandlerOptions{
|
|
Level: slog.LevelInfo,
|
|
})))
|
|
|
|
startTime = time.Now()
|
|
|
|
dataDir := os.Getenv("WILD_CENTRAL_DATA_DIR")
|
|
if dataDir == "" {
|
|
dataDir = "/var/lib/wild-central"
|
|
}
|
|
|
|
slog.Info("configured directories", "dataDir", dataDir)
|
|
|
|
// Start embedded NATS JetStream server
|
|
natsDataDir := filepath.Join(dataDir, "nats")
|
|
natsPort := 4222
|
|
if v := os.Getenv("WILD_CENTRAL_NATS_PORT"); v != "" {
|
|
fmt.Sscanf(v, "%d", &natsPort)
|
|
}
|
|
|
|
natsSrv, err := natsbus.Start(natsbus.Config{
|
|
Port: natsPort,
|
|
DataDir: natsDataDir,
|
|
})
|
|
if err != nil {
|
|
slog.Error("failed to start NATS server", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
allowedOrigins := buildAllowedOrigins()
|
|
|
|
api, err := v1.NewAPI(dataDir, Version, allowedOrigins)
|
|
if err != nil {
|
|
slog.Error("failed to initialize API", "error", err)
|
|
natsSrv.Shutdown()
|
|
os.Exit(1)
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
api.StartCentralStatusBroadcaster(startTime)
|
|
slog.Info("central status broadcaster started")
|
|
|
|
api.StartDDNS(ctx)
|
|
|
|
router := mux.NewRouter()
|
|
api.RegisterRoutes(router)
|
|
|
|
router.HandleFunc("/api/v1/health", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
fmt.Fprintf(w, `{"status":"ok"}`)
|
|
}).Methods("GET")
|
|
|
|
router.HandleFunc("/api/v1/status", func(w http.ResponseWriter, r *http.Request) {
|
|
api.StatusHandler(w, r, startTime, dataDir)
|
|
}).Methods("GET")
|
|
|
|
// Frontend: serve web UI
|
|
staticDir := os.Getenv("WILD_CENTRAL_STATIC_DIR")
|
|
if staticDir == "" {
|
|
staticDir = "/var/www/html/wild-central"
|
|
}
|
|
viteURL := os.Getenv("WILD_CENTRAL_VITE_URL")
|
|
if viteURL == "" {
|
|
viteURL = "http://localhost:5173"
|
|
}
|
|
router.PathPrefix("/").Handler(frontend.Handler(staticDir, viteURL))
|
|
|
|
handler := corsHandler(router, allowedOrigins)
|
|
|
|
host := "0.0.0.0"
|
|
port := 5055
|
|
if v := os.Getenv("WILD_CENTRAL_PORT"); v != "" {
|
|
fmt.Sscanf(v, "%d", &port)
|
|
}
|
|
|
|
// Self-register Wild Central's own domain (idempotent, skips if no domain configured)
|
|
api.RegisterSelf(port)
|
|
|
|
addr := fmt.Sprintf("%s:%d", host, port)
|
|
slog.Info("wild-central started", "addr", addr, "version", Version)
|
|
|
|
sigChan := make(chan os.Signal, 1)
|
|
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
|
|
|
go func() {
|
|
if err := http.ListenAndServe(addr, handler); err != nil {
|
|
slog.Error("server failed to start", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
}()
|
|
|
|
sig := <-sigChan
|
|
slog.Info("shutdown signal received", "signal", sig)
|
|
cancel()
|
|
natsSrv.Shutdown()
|
|
slog.Info("wild-central stopped")
|
|
}
|
|
|
|
func corsHandler(handler http.Handler, allowedOrigins []string) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
origin := r.Header.Get("Origin")
|
|
for _, allowed := range allowedOrigins {
|
|
if origin == allowed {
|
|
w.Header().Set("Access-Control-Allow-Origin", origin)
|
|
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
|
|
w.Header().Set("Access-Control-Allow-Headers", "Accept, Authorization, Content-Type, X-CSRF-Token")
|
|
w.Header().Set("Access-Control-Expose-Headers", "Link")
|
|
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
|
w.Header().Set("Access-Control-Max-Age", "300")
|
|
break
|
|
}
|
|
}
|
|
|
|
if r.Method == http.MethodOptions {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return
|
|
}
|
|
|
|
handler.ServeHTTP(w, r)
|
|
})
|
|
}
|