You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

316 lines
9.1 KiB

package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"lol.mleku.dev/chk"
"lol.mleku.dev/log"
)
// AdminServer provides HTTP endpoints for managing the launcher.
type AdminServer struct {
cfg *Config
supervisor *Supervisor
updater *Updater
auth *AuthMiddleware
server *http.Server
startTime time.Time
}
// NewAdminServer creates a new admin HTTP server.
func NewAdminServer(cfg *Config, supervisor *Supervisor) *AdminServer {
return &AdminServer{
cfg: cfg,
supervisor: supervisor,
updater: NewUpdater(cfg.BinDir),
auth: NewAuthMiddleware(cfg.AdminOwners),
startTime: time.Now(),
}
}
// Start starts the admin HTTP server.
func (s *AdminServer) Start(ctx context.Context) error {
mux := http.NewServeMux()
// Public endpoints
mux.HandleFunc("/admin", s.serveUI)
mux.HandleFunc("/admin/", s.serveUI)
// Authenticated API endpoints
mux.HandleFunc("/api/status", s.auth.RequireAuth(s.handleStatus))
mux.HandleFunc("/api/config", s.auth.RequireAuth(s.handleConfig))
mux.HandleFunc("/api/binaries", s.auth.RequireAuth(s.handleBinaries))
mux.HandleFunc("/api/update", s.auth.RequireAuth(s.handleUpdate))
mux.HandleFunc("/api/restart", s.auth.RequireAuth(s.handleRestart))
mux.HandleFunc("/api/rollback", s.auth.RequireAuth(s.handleRollback))
addr := fmt.Sprintf(":%d", s.cfg.AdminPort)
s.server = &http.Server{
Addr: addr,
Handler: mux,
}
log.I.F("starting admin server on %s", addr)
go func() {
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
s.server.Shutdown(shutdownCtx)
}()
return s.server.ListenAndServe()
}
// StatusResponse is the response for GET /api/status
type StatusResponse struct {
Version string `json:"version"`
Uptime string `json:"uptime"`
Processes []ProcessStatus `json:"processes"`
}
// ProcessStatus represents the status of a single managed process.
type ProcessStatus struct {
Name string `json:"name"`
Binary string `json:"binary"`
Version string `json:"version"`
Status string `json:"status"`
PID int `json:"pid"`
Restarts int `json:"restarts"`
StartedAt string `json:"started_at,omitempty"`
}
func (s *AdminServer) handleStatus(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
uptime := time.Since(s.startTime).Round(time.Second).String()
processes := s.supervisor.GetProcessStatuses()
response := StatusResponse{
Version: s.updater.CurrentVersion(),
Uptime: uptime,
Processes: processes,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// ConfigResponse is the response for GET /api/config
type ConfigResponse struct {
DBBackend string `json:"db_backend"`
DBBinary string `json:"db_binary"`
RelayBinary string `json:"relay_binary"`
ACLBinary string `json:"acl_binary"`
DBListen string `json:"db_listen"`
ACLListen string `json:"acl_listen"`
ACLEnabled bool `json:"acl_enabled"`
ACLMode string `json:"acl_mode"`
DataDir string `json:"data_dir"`
LogLevel string `json:"log_level"`
DistributedSyncEnabled bool `json:"distributed_sync_enabled"`
ClusterSyncEnabled bool `json:"cluster_sync_enabled"`
RelayGroupEnabled bool `json:"relay_group_enabled"`
NegentropyEnabled bool `json:"negentropy_enabled"`
AdminOwners []string `json:"admin_owners"`
BinDir string `json:"bin_dir"`
}
func (s *AdminServer) handleConfig(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
s.handleGetConfig(w, r)
case http.MethodPost:
s.handleSetConfig(w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func (s *AdminServer) handleGetConfig(w http.ResponseWriter, r *http.Request) {
response := ConfigResponse{
DBBackend: s.cfg.DBBackend,
DBBinary: s.cfg.DBBinary,
RelayBinary: s.cfg.RelayBinary,
ACLBinary: s.cfg.ACLBinary,
DBListen: s.cfg.DBListen,
ACLListen: s.cfg.ACLListen,
ACLEnabled: s.cfg.ACLEnabled,
ACLMode: s.cfg.ACLMode,
DataDir: s.cfg.DataDir,
LogLevel: s.cfg.LogLevel,
DistributedSyncEnabled: s.cfg.DistributedSyncEnabled,
ClusterSyncEnabled: s.cfg.ClusterSyncEnabled,
RelayGroupEnabled: s.cfg.RelayGroupEnabled,
NegentropyEnabled: s.cfg.NegentropyEnabled,
AdminOwners: s.auth.Owners(),
BinDir: s.cfg.BinDir,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
func (s *AdminServer) handleSetConfig(w http.ResponseWriter, r *http.Request) {
// TODO: Implement config update (requires restart)
http.Error(w, "Config update not implemented yet", http.StatusNotImplemented)
}
// BinariesResponse is the response for GET /api/binaries
type BinariesResponse struct {
CurrentVersion string `json:"current_version"`
AvailableVersions []VersionInfo `json:"available_versions"`
}
func (s *AdminServer) handleBinaries(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
response := BinariesResponse{
CurrentVersion: s.updater.CurrentVersion(),
AvailableVersions: s.updater.ListVersions(),
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// UpdateRequest is the request body for POST /api/update
type UpdateRequest struct {
Version string `json:"version"`
URLs map[string]string `json:"urls"` // binary name -> download URL
}
// UpdateResponse is the response for POST /api/update
type UpdateResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Version string `json:"version"`
DownloadedFiles []string `json:"downloaded_files"`
}
func (s *AdminServer) handleUpdate(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req UpdateRequest
if err := json.NewDecoder(r.Body).Decode(&req); chk.E(err) {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
if req.Version == "" {
http.Error(w, "Version is required", http.StatusBadRequest)
return
}
if len(req.URLs) == 0 {
http.Error(w, "At least one binary URL is required", http.StatusBadRequest)
return
}
// Perform the update
downloadedFiles, err := s.updater.Update(req.Version, req.URLs)
if chk.E(err) {
response := UpdateResponse{
Success: false,
Message: err.Error(),
Version: req.Version,
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(response)
return
}
response := UpdateResponse{
Success: true,
Message: fmt.Sprintf("Successfully updated to version %s", req.Version),
Version: req.Version,
DownloadedFiles: downloadedFiles,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// RestartResponse is the response for POST /api/restart
type RestartResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
}
func (s *AdminServer) handleRestart(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Signal supervisor to restart all processes
go func() {
if err := s.supervisor.RestartAll(); chk.E(err) {
log.E.F("restart failed: %v", err)
}
}()
response := RestartResponse{
Success: true,
Message: "Restart initiated",
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// RollbackResponse is the response for POST /api/rollback
type RollbackResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
PreviousVersion string `json:"previous_version"`
CurrentVersion string `json:"current_version"`
}
func (s *AdminServer) handleRollback(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
previousVersion := s.updater.CurrentVersion()
if err := s.updater.Rollback(); chk.E(err) {
response := RollbackResponse{
Success: false,
Message: err.Error(),
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(response)
return
}
response := RollbackResponse{
Success: true,
Message: "Rollback successful - restart required to apply",
PreviousVersion: previousVersion,
CurrentVersion: s.updater.CurrentVersion(),
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
func (s *AdminServer) serveUI(w http.ResponseWriter, r *http.Request) {
s.serveAdminUI(w, r)
}