fix: change Caddy config reload from async to sync for deterministic applied state

This commit is contained in:
GitHub Actions
2026-02-13 18:50:04 +00:00
parent 0024b81e39
commit a44530a682
2 changed files with 84 additions and 20 deletions

View File

@@ -177,18 +177,18 @@ func (h *SettingsHandler) UpdateSetting(c *gin.Context) {
h.Cerberus.InvalidateCache()
}
// Trigger async Caddy config reload (doesn't block HTTP response)
// Trigger sync Caddy config reload so callers can rely on deterministic applied state
if h.CaddyManager != nil {
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
defer cancel()
if err := h.CaddyManager.ApplyConfig(ctx); err != nil {
logger.Log().WithError(err).Warn("Failed to reload Caddy config after security setting change")
} else {
logger.Log().WithField("setting_key", req.Key).Info("Caddy config reloaded after security setting change")
}
}()
if err := h.CaddyManager.ApplyConfig(ctx); err != nil {
logger.Log().WithError(err).Warn("Failed to reload Caddy config after security setting change")
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to reload configuration"})
return
}
logger.Log().WithField("setting_key", req.Key).Info("Caddy config reloaded after security setting change")
}
}
@@ -283,18 +283,18 @@ func (h *SettingsHandler) PatchConfig(c *gin.Context) {
h.Cerberus.InvalidateCache()
}
// Trigger async Caddy config reload
// Trigger sync Caddy config reload so callers can rely on deterministic applied state
if h.CaddyManager != nil {
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
defer cancel()
if err := h.CaddyManager.ApplyConfig(ctx); err != nil {
logger.Log().WithError(err).Warn("Failed to reload Caddy config after security settings change")
} else {
logger.Log().Info("Caddy config reloaded after security settings change")
}
}()
if err := h.CaddyManager.ApplyConfig(ctx); err != nil {
logger.Log().WithError(err).Warn("Failed to reload Caddy config after security settings change")
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to reload configuration"})
return
}
logger.Log().Info("Caddy config reloaded after security settings change")
}
}

View File

@@ -3,6 +3,7 @@ package handlers_test
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"net"
@@ -22,6 +23,19 @@ import (
"github.com/Wikid82/charon/backend/internal/models"
)
type mockCaddyConfigManager struct {
applyFunc func(context.Context) error
calls int
}
func (m *mockCaddyConfigManager) ApplyConfig(ctx context.Context) error {
m.calls++
if m.applyFunc != nil {
return m.applyFunc(ctx)
}
return nil
}
func startTestSMTPServer(t *testing.T) (host string, port int) {
t.Helper()
@@ -295,6 +309,56 @@ func TestSettingsHandler_UpdateSetting_EnablesCerberusWhenACLEnabled(t *testing.
assert.True(t, cfg.Enabled)
}
func TestSettingsHandler_UpdateSetting_SecurityKeyAppliesConfigSynchronously(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupSettingsTestDB(t)
mgr := &mockCaddyConfigManager{}
handler := handlers.NewSettingsHandlerWithDeps(db, mgr, nil, nil, "")
router := newAdminRouter()
router.POST("/settings", handler.UpdateSetting)
payload := map[string]string{
"key": "security.waf.enabled",
"value": "true",
}
body, _ := json.Marshal(payload)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/settings", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, 1, mgr.calls)
}
func TestSettingsHandler_UpdateSetting_SecurityKeyApplyFailureReturnsError(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupSettingsTestDB(t)
mgr := &mockCaddyConfigManager{applyFunc: func(context.Context) error {
return fmt.Errorf("apply failed")
}}
handler := handlers.NewSettingsHandlerWithDeps(db, mgr, nil, nil, "")
router := newAdminRouter()
router.POST("/settings", handler.UpdateSetting)
payload := map[string]string{
"key": "security.waf.enabled",
"value": "true",
}
body, _ := json.Marshal(payload)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/settings", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Equal(t, 1, mgr.calls)
}
func TestSettingsHandler_PatchConfig_SyncsAdminWhitelist(t *testing.T) {
gin.SetMode(gin.TestMode)
db := setupSettingsTestDB(t)