Merge pull request #82 from router-for-me/mgmt
feat: Implement hot-reloading for management endpoints
This commit is contained in:
+58
-4
@@ -13,6 +13,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -126,6 +127,11 @@ type Server struct {
|
|||||||
// management handler
|
// management handler
|
||||||
mgmt *managementHandlers.Handler
|
mgmt *managementHandlers.Handler
|
||||||
|
|
||||||
|
// managementRoutesRegistered tracks whether the management routes have been attached to the engine.
|
||||||
|
managementRoutesRegistered atomic.Bool
|
||||||
|
// managementRoutesEnabled controls whether management endpoints serve real handlers.
|
||||||
|
managementRoutesEnabled atomic.Bool
|
||||||
|
|
||||||
localPassword string
|
localPassword string
|
||||||
|
|
||||||
keepAliveEnabled bool
|
keepAliveEnabled bool
|
||||||
@@ -210,6 +216,12 @@ func NewServer(cfg *config.Config, authManager *auth.Manager, accessManager *sdk
|
|||||||
optionState.routerConfigurator(engine, s.handlers, cfg)
|
optionState.routerConfigurator(engine, s.handlers, cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Register management routes only when a secret is present at startup.
|
||||||
|
s.managementRoutesEnabled.Store(cfg.RemoteManagement.SecretKey != "")
|
||||||
|
if cfg.RemoteManagement.SecretKey != "" {
|
||||||
|
s.registerManagementRoutes()
|
||||||
|
}
|
||||||
|
|
||||||
if optionState.keepAliveEnabled {
|
if optionState.keepAliveEnabled {
|
||||||
s.enableKeepAlive(optionState.keepAliveTimeout, optionState.keepAliveOnTimeout)
|
s.enableKeepAlive(optionState.keepAliveTimeout, optionState.keepAliveOnTimeout)
|
||||||
}
|
}
|
||||||
@@ -308,11 +320,21 @@ func (s *Server) setupRoutes() {
|
|||||||
c.String(http.StatusOK, "<html><body><h1>Authentication successful!</h1><p>You can close this window.</p></body></html>")
|
c.String(http.StatusOK, "<html><body><h1>Authentication successful!</h1><p>You can close this window.</p></body></html>")
|
||||||
})
|
})
|
||||||
|
|
||||||
// Management API routes (delegated to management handlers)
|
// Management routes are registered lazily by registerManagementRoutes when a secret is configured.
|
||||||
// New logic: if remote-management-key is empty, do not expose any management endpoint (404).
|
}
|
||||||
if s.cfg.RemoteManagement.SecretKey != "" {
|
|
||||||
|
func (s *Server) registerManagementRoutes() {
|
||||||
|
if s == nil || s.engine == nil || s.mgmt == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !s.managementRoutesRegistered.CompareAndSwap(false, true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("management routes registered after secret key configuration")
|
||||||
|
|
||||||
mgmt := s.engine.Group("/v0/management")
|
mgmt := s.engine.Group("/v0/management")
|
||||||
mgmt.Use(s.mgmt.Middleware())
|
mgmt.Use(s.managementAvailabilityMiddleware(), s.mgmt.Middleware())
|
||||||
{
|
{
|
||||||
mgmt.GET("/usage", s.mgmt.GetUsageStatistics)
|
mgmt.GET("/usage", s.mgmt.GetUsageStatistics)
|
||||||
mgmt.GET("/config", s.mgmt.GetConfig)
|
mgmt.GET("/config", s.mgmt.GetConfig)
|
||||||
@@ -387,6 +409,15 @@ func (s *Server) setupRoutes() {
|
|||||||
mgmt.GET("/qwen-auth-url", s.mgmt.RequestQwenToken)
|
mgmt.GET("/qwen-auth-url", s.mgmt.RequestQwenToken)
|
||||||
mgmt.GET("/get-auth-status", s.mgmt.GetAuthStatus)
|
mgmt.GET("/get-auth-status", s.mgmt.GetAuthStatus)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) managementAvailabilityMiddleware() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
if !s.managementRoutesEnabled.Load() {
|
||||||
|
c.AbortWithStatus(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -641,6 +672,29 @@ func (s *Server) UpdateClients(cfg *config.Config) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prevSecretEmpty := true
|
||||||
|
if oldCfg != nil {
|
||||||
|
prevSecretEmpty = oldCfg.RemoteManagement.SecretKey == ""
|
||||||
|
}
|
||||||
|
newSecretEmpty := cfg.RemoteManagement.SecretKey == ""
|
||||||
|
switch {
|
||||||
|
case prevSecretEmpty && !newSecretEmpty:
|
||||||
|
s.registerManagementRoutes()
|
||||||
|
if s.managementRoutesEnabled.CompareAndSwap(false, true) {
|
||||||
|
log.Info("management routes enabled after secret key update")
|
||||||
|
} else {
|
||||||
|
s.managementRoutesEnabled.Store(true)
|
||||||
|
}
|
||||||
|
case !prevSecretEmpty && newSecretEmpty:
|
||||||
|
if s.managementRoutesEnabled.CompareAndSwap(true, false) {
|
||||||
|
log.Info("management routes disabled after secret key removal")
|
||||||
|
} else {
|
||||||
|
s.managementRoutesEnabled.Store(false)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
s.managementRoutesEnabled.Store(!newSecretEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
s.applyAccessConfig(oldCfg, cfg)
|
s.applyAccessConfig(oldCfg, cfg)
|
||||||
s.cfg = cfg
|
s.cfg = cfg
|
||||||
s.handlers.UpdateClients(&cfg.SDKConfig)
|
s.handlers.UpdateClients(&cfg.SDKConfig)
|
||||||
|
|||||||
@@ -430,8 +430,15 @@ func (w *Watcher) handleEvent(event fsnotify.Event) {
|
|||||||
}
|
}
|
||||||
fmt.Printf("config file changed, reloading: %s\n", w.configPath)
|
fmt.Printf("config file changed, reloading: %s\n", w.configPath)
|
||||||
if w.reloadConfig() {
|
if w.reloadConfig() {
|
||||||
|
finalHash := newHash
|
||||||
|
if updatedData, errRead := os.ReadFile(w.configPath); errRead == nil && len(updatedData) > 0 {
|
||||||
|
sumUpdated := sha256.Sum256(updatedData)
|
||||||
|
finalHash = hex.EncodeToString(sumUpdated[:])
|
||||||
|
} else if errRead != nil {
|
||||||
|
log.WithError(errRead).Debug("failed to compute updated config hash after reload")
|
||||||
|
}
|
||||||
w.clientsMutex.Lock()
|
w.clientsMutex.Lock()
|
||||||
w.lastConfigHash = newHash
|
w.lastConfigHash = finalHash
|
||||||
w.clientsMutex.Unlock()
|
w.clientsMutex.Unlock()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -532,6 +539,21 @@ func (w *Watcher) reloadConfig() bool {
|
|||||||
if oldConfig.RemoteManagement.AllowRemote != newConfig.RemoteManagement.AllowRemote {
|
if oldConfig.RemoteManagement.AllowRemote != newConfig.RemoteManagement.AllowRemote {
|
||||||
log.Debugf(" remote-management.allow-remote: %t -> %t", oldConfig.RemoteManagement.AllowRemote, newConfig.RemoteManagement.AllowRemote)
|
log.Debugf(" remote-management.allow-remote: %t -> %t", oldConfig.RemoteManagement.AllowRemote, newConfig.RemoteManagement.AllowRemote)
|
||||||
}
|
}
|
||||||
|
if oldConfig.RemoteManagement.SecretKey != newConfig.RemoteManagement.SecretKey {
|
||||||
|
switch {
|
||||||
|
case oldConfig.RemoteManagement.SecretKey == "" && newConfig.RemoteManagement.SecretKey != "":
|
||||||
|
log.Debug(" remote-management.secret-key: created")
|
||||||
|
case oldConfig.RemoteManagement.SecretKey != "" && newConfig.RemoteManagement.SecretKey == "":
|
||||||
|
log.Debug(" remote-management.secret-key: deleted")
|
||||||
|
default:
|
||||||
|
log.Debug(" remote-management.secret-key: updated")
|
||||||
|
}
|
||||||
|
if newConfig.RemoteManagement.SecretKey == "" {
|
||||||
|
log.Info("management routes will be disabled after secret key removal")
|
||||||
|
} else {
|
||||||
|
log.Info("management routes will be enabled after secret key update")
|
||||||
|
}
|
||||||
|
}
|
||||||
if oldConfig.RemoteManagement.DisableControlPanel != newConfig.RemoteManagement.DisableControlPanel {
|
if oldConfig.RemoteManagement.DisableControlPanel != newConfig.RemoteManagement.DisableControlPanel {
|
||||||
log.Debugf(" remote-management.disable-control-panel: %t -> %t", oldConfig.RemoteManagement.DisableControlPanel, newConfig.RemoteManagement.DisableControlPanel)
|
log.Debugf(" remote-management.disable-control-panel: %t -> %t", oldConfig.RemoteManagement.DisableControlPanel, newConfig.RemoteManagement.DisableControlPanel)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user