Files
wild-central/main.go
Paul Payne 995d2cd7da fix: Run Reconcile after SetPort so Central routes to correct port
Reconcile() was called before SetPort(), so getRunningPort() returned
the default 5055 (Wild Cloud's port) instead of 15055. Moved
Reconcile() after SetPort(port) so Central's config-driven HAProxy
route points to the actual running port.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 20:57:59 +00:00

202 lines
5.1 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)
}
// Tell the API what port it's running on and run initial reconciliation.
// Must happen AFTER port is known so Central's config-driven HAProxy
// route points to the correct port.
api.SetPort(port)
api.Reconcile()
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)
})
}