Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc804e96fb | ||
|
|
ab76cb3662 | ||
|
|
2965bdadc1 | ||
|
|
40f7061b04 |
@@ -28,7 +28,7 @@ func (h *Handler) GetConfigYAML(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
var node yaml.Node
|
var node yaml.Node
|
||||||
if err := yaml.Unmarshal(data, &node); err != nil {
|
if err = yaml.Unmarshal(data, &node); err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "parse_failed", "message": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "parse_failed", "message": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -41,17 +41,18 @@ func (h *Handler) GetConfigYAML(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func WriteConfig(path string, data []byte) error {
|
func WriteConfig(path string, data []byte) error {
|
||||||
|
data = config.NormalizeCommentIndentation(data)
|
||||||
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := f.Write(data); err != nil {
|
if _, errWrite := f.Write(data); errWrite != nil {
|
||||||
f.Close()
|
_ = f.Close()
|
||||||
return err
|
return errWrite
|
||||||
}
|
}
|
||||||
if err := f.Sync(); err != nil {
|
if errSync := f.Sync(); errSync != nil {
|
||||||
f.Close()
|
_ = f.Close()
|
||||||
return err
|
return errSync
|
||||||
}
|
}
|
||||||
return f.Close()
|
return f.Close()
|
||||||
}
|
}
|
||||||
@@ -63,7 +64,7 @@ func (h *Handler) PutConfigYAML(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
var cfg config.Config
|
var cfg config.Config
|
||||||
if err := yaml.Unmarshal(body, &cfg); err != nil {
|
if err = yaml.Unmarshal(body, &cfg); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_yaml", "message": err.Error()})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_yaml", "message": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -75,18 +76,20 @@ func (h *Handler) PutConfigYAML(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
tempFile := tmpFile.Name()
|
tempFile := tmpFile.Name()
|
||||||
if _, err := tmpFile.Write(body); err != nil {
|
if _, errWrite := tmpFile.Write(body); errWrite != nil {
|
||||||
tmpFile.Close()
|
_ = tmpFile.Close()
|
||||||
os.Remove(tempFile)
|
_ = os.Remove(tempFile)
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "write_failed", "message": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "write_failed", "message": errWrite.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := tmpFile.Close(); err != nil {
|
if errClose := tmpFile.Close(); errClose != nil {
|
||||||
os.Remove(tempFile)
|
_ = os.Remove(tempFile)
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "write_failed", "message": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "write_failed", "message": errClose.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer os.Remove(tempFile)
|
defer func() {
|
||||||
|
_ = os.Remove(tempFile)
|
||||||
|
}()
|
||||||
_, err = config.LoadConfigOptional(tempFile, false)
|
_, err = config.LoadConfigOptional(tempFile, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "invalid_config", "message": err.Error()})
|
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "invalid_config", "message": err.Error()})
|
||||||
@@ -153,6 +156,14 @@ func (h *Handler) PutRequestLog(c *gin.Context) {
|
|||||||
h.updateBoolField(c, func(v bool) { h.cfg.RequestLog = v })
|
h.updateBoolField(c, func(v bool) { h.cfg.RequestLog = v })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Websocket auth
|
||||||
|
func (h *Handler) GetWebsocketAuth(c *gin.Context) {
|
||||||
|
c.JSON(200, gin.H{"ws-auth": h.cfg.WebsocketAuth})
|
||||||
|
}
|
||||||
|
func (h *Handler) PutWebsocketAuth(c *gin.Context) {
|
||||||
|
h.updateBoolField(c, func(v bool) { h.cfg.WebsocketAuth = v })
|
||||||
|
}
|
||||||
|
|
||||||
// Request retry
|
// Request retry
|
||||||
func (h *Handler) GetRequestRetry(c *gin.Context) {
|
func (h *Handler) GetRequestRetry(c *gin.Context) {
|
||||||
c.JSON(200, gin.H{"request-retry": h.cfg.RequestRetry})
|
c.JSON(200, gin.H{"request-retry": h.cfg.RequestRetry})
|
||||||
|
|||||||
156
internal/api/handlers/management/vertex_import.go
Normal file
156
internal/api/handlers/management/vertex_import.go
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
package management
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/vertex"
|
||||||
|
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ImportVertexCredential handles uploading a Vertex service account JSON and saving it as an auth record.
|
||||||
|
func (h *Handler) ImportVertexCredential(c *gin.Context) {
|
||||||
|
if h == nil || h.cfg == nil {
|
||||||
|
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "config unavailable"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if h.cfg.AuthDir == "" {
|
||||||
|
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "auth directory not configured"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fileHeader, err := c.FormFile("file")
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "file required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := fileHeader.Open()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("failed to read file: %v", err)})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
data, err := io.ReadAll(file)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("failed to read file: %v", err)})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var serviceAccount map[string]any
|
||||||
|
if err := json.Unmarshal(data, &serviceAccount); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid json", "message": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizedSA, err := vertex.NormalizeServiceAccountMap(serviceAccount)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid service account", "message": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
serviceAccount = normalizedSA
|
||||||
|
|
||||||
|
projectID := strings.TrimSpace(valueAsString(serviceAccount["project_id"]))
|
||||||
|
if projectID == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "project_id missing"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
email := strings.TrimSpace(valueAsString(serviceAccount["client_email"]))
|
||||||
|
|
||||||
|
location := strings.TrimSpace(c.PostForm("location"))
|
||||||
|
if location == "" {
|
||||||
|
location = strings.TrimSpace(c.Query("location"))
|
||||||
|
}
|
||||||
|
if location == "" {
|
||||||
|
location = "us-central1"
|
||||||
|
}
|
||||||
|
|
||||||
|
fileName := fmt.Sprintf("vertex-%s.json", sanitizeVertexFilePart(projectID))
|
||||||
|
label := labelForVertex(projectID, email)
|
||||||
|
storage := &vertex.VertexCredentialStorage{
|
||||||
|
ServiceAccount: serviceAccount,
|
||||||
|
ProjectID: projectID,
|
||||||
|
Email: email,
|
||||||
|
Location: location,
|
||||||
|
Type: "vertex",
|
||||||
|
}
|
||||||
|
metadata := map[string]any{
|
||||||
|
"service_account": serviceAccount,
|
||||||
|
"project_id": projectID,
|
||||||
|
"email": email,
|
||||||
|
"location": location,
|
||||||
|
"type": "vertex",
|
||||||
|
"label": label,
|
||||||
|
}
|
||||||
|
record := &coreauth.Auth{
|
||||||
|
ID: fileName,
|
||||||
|
Provider: "vertex",
|
||||||
|
FileName: fileName,
|
||||||
|
Storage: storage,
|
||||||
|
Label: label,
|
||||||
|
Metadata: metadata,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
if reqCtx := c.Request.Context(); reqCtx != nil {
|
||||||
|
ctx = reqCtx
|
||||||
|
}
|
||||||
|
savedPath, err := h.saveTokenRecord(ctx, record)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "save_failed", "message": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"status": "ok",
|
||||||
|
"auth-file": savedPath,
|
||||||
|
"project_id": projectID,
|
||||||
|
"email": email,
|
||||||
|
"location": location,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func valueAsString(v any) string {
|
||||||
|
if v == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
switch t := v.(type) {
|
||||||
|
case string:
|
||||||
|
return t
|
||||||
|
default:
|
||||||
|
return fmt.Sprint(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizeVertexFilePart(s string) string {
|
||||||
|
out := strings.TrimSpace(s)
|
||||||
|
replacers := []string{"/", "_", "\\", "_", ":", "_", " ", "-"}
|
||||||
|
for i := 0; i < len(replacers); i += 2 {
|
||||||
|
out = strings.ReplaceAll(out, replacers[i], replacers[i+1])
|
||||||
|
}
|
||||||
|
if out == "" {
|
||||||
|
return "vertex"
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func labelForVertex(projectID, email string) string {
|
||||||
|
p := strings.TrimSpace(projectID)
|
||||||
|
e := strings.TrimSpace(email)
|
||||||
|
if p != "" && e != "" {
|
||||||
|
return fmt.Sprintf("%s (%s)", p, e)
|
||||||
|
}
|
||||||
|
if p != "" {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
if e != "" {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return "vertex"
|
||||||
|
}
|
||||||
@@ -484,6 +484,9 @@ func (s *Server) registerManagementRoutes() {
|
|||||||
mgmt.GET("/request-log", s.mgmt.GetRequestLog)
|
mgmt.GET("/request-log", s.mgmt.GetRequestLog)
|
||||||
mgmt.PUT("/request-log", s.mgmt.PutRequestLog)
|
mgmt.PUT("/request-log", s.mgmt.PutRequestLog)
|
||||||
mgmt.PATCH("/request-log", s.mgmt.PutRequestLog)
|
mgmt.PATCH("/request-log", s.mgmt.PutRequestLog)
|
||||||
|
mgmt.GET("/ws-auth", s.mgmt.GetWebsocketAuth)
|
||||||
|
mgmt.PUT("/ws-auth", s.mgmt.PutWebsocketAuth)
|
||||||
|
mgmt.PATCH("/ws-auth", s.mgmt.PutWebsocketAuth)
|
||||||
|
|
||||||
mgmt.GET("/request-retry", s.mgmt.GetRequestRetry)
|
mgmt.GET("/request-retry", s.mgmt.GetRequestRetry)
|
||||||
mgmt.PUT("/request-retry", s.mgmt.PutRequestRetry)
|
mgmt.PUT("/request-retry", s.mgmt.PutRequestRetry)
|
||||||
@@ -508,6 +511,7 @@ func (s *Server) registerManagementRoutes() {
|
|||||||
mgmt.GET("/auth-files/download", s.mgmt.DownloadAuthFile)
|
mgmt.GET("/auth-files/download", s.mgmt.DownloadAuthFile)
|
||||||
mgmt.POST("/auth-files", s.mgmt.UploadAuthFile)
|
mgmt.POST("/auth-files", s.mgmt.UploadAuthFile)
|
||||||
mgmt.DELETE("/auth-files", s.mgmt.DeleteAuthFile)
|
mgmt.DELETE("/auth-files", s.mgmt.DeleteAuthFile)
|
||||||
|
mgmt.POST("/vertex/import", s.mgmt.ImportVertexCredential)
|
||||||
|
|
||||||
mgmt.GET("/anthropic-auth-url", s.mgmt.RequestAnthropicToken)
|
mgmt.GET("/anthropic-auth-url", s.mgmt.RequestAnthropicToken)
|
||||||
mgmt.GET("/codex-auth-url", s.mgmt.RequestCodexToken)
|
mgmt.GET("/codex-auth-url", s.mgmt.RequestCodexToken)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
@@ -462,13 +463,19 @@ func SaveConfigPreserveComments(configFile string, cfg *Config) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer func() { _ = f.Close() }()
|
defer func() { _ = f.Close() }()
|
||||||
enc := yaml.NewEncoder(f)
|
var buf bytes.Buffer
|
||||||
|
enc := yaml.NewEncoder(&buf)
|
||||||
enc.SetIndent(2)
|
enc.SetIndent(2)
|
||||||
if err = enc.Encode(&original); err != nil {
|
if err = enc.Encode(&original); err != nil {
|
||||||
_ = enc.Close()
|
_ = enc.Close()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return enc.Close()
|
if err = enc.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data = NormalizeCommentIndentation(buf.Bytes())
|
||||||
|
_, err = f.Write(data)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func sanitizeConfigForPersist(cfg *Config) *Config {
|
func sanitizeConfigForPersist(cfg *Config) *Config {
|
||||||
@@ -518,13 +525,40 @@ func SaveConfigPreserveCommentsUpdateNestedScalar(configFile string, path []stri
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer func() { _ = f.Close() }()
|
defer func() { _ = f.Close() }()
|
||||||
enc := yaml.NewEncoder(f)
|
var buf bytes.Buffer
|
||||||
|
enc := yaml.NewEncoder(&buf)
|
||||||
enc.SetIndent(2)
|
enc.SetIndent(2)
|
||||||
if err = enc.Encode(&root); err != nil {
|
if err = enc.Encode(&root); err != nil {
|
||||||
_ = enc.Close()
|
_ = enc.Close()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return enc.Close()
|
if err = enc.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data = NormalizeCommentIndentation(buf.Bytes())
|
||||||
|
_, err = f.Write(data)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// NormalizeCommentIndentation removes indentation from standalone YAML comment lines to keep them left aligned.
|
||||||
|
func NormalizeCommentIndentation(data []byte) []byte {
|
||||||
|
lines := bytes.Split(data, []byte("\n"))
|
||||||
|
changed := false
|
||||||
|
for i, line := range lines {
|
||||||
|
trimmed := bytes.TrimLeft(line, " \t")
|
||||||
|
if len(trimmed) == 0 || trimmed[0] != '#' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(trimmed) == len(line) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
lines[i] = append([]byte(nil), trimmed...)
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if !changed {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
return bytes.Join(lines, []byte("\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// getOrCreateMapValue finds the value node for a given key in a mapping node.
|
// getOrCreateMapValue finds the value node for a given key in a mapping node.
|
||||||
@@ -766,6 +800,7 @@ func matchSequenceElement(original []*yaml.Node, used []bool, target *yaml.Node)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
}
|
}
|
||||||
// Fallback to structural equality to preserve nodes lacking explicit identifiers.
|
// Fallback to structural equality to preserve nodes lacking explicit identifiers.
|
||||||
for i := range original {
|
for i := range original {
|
||||||
|
|||||||
@@ -159,7 +159,6 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fmt.Printf("11111")
|
|
||||||
|
|
||||||
for i := 0; i < len(arr); i++ {
|
for i := 0; i < len(arr); i++ {
|
||||||
m := arr[i]
|
m := arr[i]
|
||||||
|
|||||||
@@ -41,24 +41,26 @@ type authDirProvider interface {
|
|||||||
|
|
||||||
// Watcher manages file watching for configuration and authentication files
|
// Watcher manages file watching for configuration and authentication files
|
||||||
type Watcher struct {
|
type Watcher struct {
|
||||||
configPath string
|
configPath string
|
||||||
authDir string
|
authDir string
|
||||||
config *config.Config
|
config *config.Config
|
||||||
clientsMutex sync.RWMutex
|
clientsMutex sync.RWMutex
|
||||||
reloadCallback func(*config.Config)
|
configReloadMu sync.Mutex
|
||||||
watcher *fsnotify.Watcher
|
configReloadTimer *time.Timer
|
||||||
lastAuthHashes map[string]string
|
reloadCallback func(*config.Config)
|
||||||
lastConfigHash string
|
watcher *fsnotify.Watcher
|
||||||
authQueue chan<- AuthUpdate
|
lastAuthHashes map[string]string
|
||||||
currentAuths map[string]*coreauth.Auth
|
lastConfigHash string
|
||||||
dispatchMu sync.Mutex
|
authQueue chan<- AuthUpdate
|
||||||
dispatchCond *sync.Cond
|
currentAuths map[string]*coreauth.Auth
|
||||||
pendingUpdates map[string]AuthUpdate
|
dispatchMu sync.Mutex
|
||||||
pendingOrder []string
|
dispatchCond *sync.Cond
|
||||||
dispatchCancel context.CancelFunc
|
pendingUpdates map[string]AuthUpdate
|
||||||
storePersister storePersister
|
pendingOrder []string
|
||||||
mirroredAuthDir string
|
dispatchCancel context.CancelFunc
|
||||||
oldConfigYaml []byte
|
storePersister storePersister
|
||||||
|
mirroredAuthDir string
|
||||||
|
oldConfigYaml []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
type stableIDGenerator struct {
|
type stableIDGenerator struct {
|
||||||
@@ -113,7 +115,8 @@ type AuthUpdate struct {
|
|||||||
const (
|
const (
|
||||||
// replaceCheckDelay is a short delay to allow atomic replace (rename) to settle
|
// replaceCheckDelay is a short delay to allow atomic replace (rename) to settle
|
||||||
// before deciding whether a Remove event indicates a real deletion.
|
// before deciding whether a Remove event indicates a real deletion.
|
||||||
replaceCheckDelay = 50 * time.Millisecond
|
replaceCheckDelay = 50 * time.Millisecond
|
||||||
|
configReloadDebounce = 150 * time.Millisecond
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewWatcher creates a new file watcher instance
|
// NewWatcher creates a new file watcher instance
|
||||||
@@ -172,9 +175,19 @@ func (w *Watcher) Start(ctx context.Context) error {
|
|||||||
// Stop stops the file watcher
|
// Stop stops the file watcher
|
||||||
func (w *Watcher) Stop() error {
|
func (w *Watcher) Stop() error {
|
||||||
w.stopDispatch()
|
w.stopDispatch()
|
||||||
|
w.stopConfigReloadTimer()
|
||||||
return w.watcher.Close()
|
return w.watcher.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *Watcher) stopConfigReloadTimer() {
|
||||||
|
w.configReloadMu.Lock()
|
||||||
|
if w.configReloadTimer != nil {
|
||||||
|
w.configReloadTimer.Stop()
|
||||||
|
w.configReloadTimer = nil
|
||||||
|
}
|
||||||
|
w.configReloadMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
// SetConfig updates the current configuration
|
// SetConfig updates the current configuration
|
||||||
func (w *Watcher) SetConfig(cfg *config.Config) {
|
func (w *Watcher) SetConfig(cfg *config.Config) {
|
||||||
w.clientsMutex.Lock()
|
w.clientsMutex.Lock()
|
||||||
@@ -476,40 +489,7 @@ func (w *Watcher) handleEvent(event fsnotify.Event) {
|
|||||||
// Handle config file changes
|
// Handle config file changes
|
||||||
if isConfigEvent {
|
if isConfigEvent {
|
||||||
log.Debugf("config file change details - operation: %s, timestamp: %s", event.Op.String(), now.Format("2006-01-02 15:04:05.000"))
|
log.Debugf("config file change details - operation: %s, timestamp: %s", event.Op.String(), now.Format("2006-01-02 15:04:05.000"))
|
||||||
data, err := os.ReadFile(w.configPath)
|
w.scheduleConfigReload()
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to read config file for hash check: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(data) == 0 {
|
|
||||||
log.Debugf("ignoring empty config file write event")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
sum := sha256.Sum256(data)
|
|
||||||
newHash := hex.EncodeToString(sum[:])
|
|
||||||
|
|
||||||
w.clientsMutex.RLock()
|
|
||||||
currentHash := w.lastConfigHash
|
|
||||||
w.clientsMutex.RUnlock()
|
|
||||||
|
|
||||||
if currentHash != "" && currentHash == newHash {
|
|
||||||
log.Debugf("config file content unchanged (hash match), skipping reload")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Printf("config file changed, reloading: %s\n", w.configPath)
|
|
||||||
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.lastConfigHash = finalHash
|
|
||||||
w.clientsMutex.Unlock()
|
|
||||||
w.persistConfigAsync()
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -530,6 +510,57 @@ func (w *Watcher) handleEvent(event fsnotify.Event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *Watcher) scheduleConfigReload() {
|
||||||
|
w.configReloadMu.Lock()
|
||||||
|
defer w.configReloadMu.Unlock()
|
||||||
|
if w.configReloadTimer != nil {
|
||||||
|
w.configReloadTimer.Stop()
|
||||||
|
}
|
||||||
|
w.configReloadTimer = time.AfterFunc(configReloadDebounce, func() {
|
||||||
|
w.configReloadMu.Lock()
|
||||||
|
w.configReloadTimer = nil
|
||||||
|
w.configReloadMu.Unlock()
|
||||||
|
w.reloadConfigIfChanged()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watcher) reloadConfigIfChanged() {
|
||||||
|
data, err := os.ReadFile(w.configPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to read config file for hash check: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(data) == 0 {
|
||||||
|
log.Debugf("ignoring empty config file write event")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sum := sha256.Sum256(data)
|
||||||
|
newHash := hex.EncodeToString(sum[:])
|
||||||
|
|
||||||
|
w.clientsMutex.RLock()
|
||||||
|
currentHash := w.lastConfigHash
|
||||||
|
w.clientsMutex.RUnlock()
|
||||||
|
|
||||||
|
if currentHash != "" && currentHash == newHash {
|
||||||
|
log.Debugf("config file content unchanged (hash match), skipping reload")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("config file changed, reloading: %s\n", w.configPath)
|
||||||
|
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.lastConfigHash = finalHash
|
||||||
|
w.clientsMutex.Unlock()
|
||||||
|
w.persistConfigAsync()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// reloadConfig reloads the configuration and triggers a full reload
|
// reloadConfig reloads the configuration and triggers a full reload
|
||||||
func (w *Watcher) reloadConfig() bool {
|
func (w *Watcher) reloadConfig() bool {
|
||||||
log.Debug("=========================== CONFIG RELOAD ============================")
|
log.Debug("=========================== CONFIG RELOAD ============================")
|
||||||
|
|||||||
Reference in New Issue
Block a user