Central now starts an embedded NATS JetStream server as its core coordination bus. This provides: - wild-services KV bucket: service registrations from Cloud/Works - wild-presence KV bucket: node liveness with TTL-based keys - wild-events stream: events from all sources (24h retention) NATS is started in main.go before the API and shut down on SIGTERM. Cloud and Works will connect to Central's NATS (port 4222) to register services and maintain presence. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
193 lines
4.8 KiB
Go
193 lines
4.8 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
|
|
|
|
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)
|
|
})
|
|
}
|