Skip to content

Commit 65e0ddc

Browse files
core: Reloading with SIGUSR1 if config never changed via admin (#7258)
1 parent b2ab419 commit 65e0ddc

File tree

7 files changed

+167
-20
lines changed

7 files changed

+167
-20
lines changed

admin.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1029,6 +1029,13 @@ func handleConfig(w http.ResponseWriter, r *http.Request) error {
10291029
return err
10301030
}
10311031

1032+
// If this request changed the config, clear the last
1033+
// config info we have stored, if it is different from
1034+
// the original source.
1035+
ClearLastConfigIfDifferent(
1036+
r.Header.Get("Caddy-Config-Source-File"),
1037+
r.Header.Get("Caddy-Config-Source-Adapter"))
1038+
10321039
default:
10331040
return APIError{
10341041
HTTPStatus: http.StatusMethodNotAllowed,

caddy.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1197,6 +1197,91 @@ var (
11971197
rawCfgMu sync.RWMutex
11981198
)
11991199

1200+
// lastConfigFile and lastConfigAdapter remember the source config
1201+
// file and adapter used when Caddy was started via the CLI "run" command.
1202+
// These are consulted by the SIGUSR1 handler to attempt reloading from
1203+
// the same source. They are intentionally not set for other entrypoints
1204+
// such as "caddy start" or subcommands like file-server.
1205+
var (
1206+
lastConfigMu sync.RWMutex
1207+
lastConfigFile string
1208+
lastConfigAdapter string
1209+
)
1210+
1211+
// reloadFromSourceFunc is the type of stored callback
1212+
// which is called when we receive a SIGUSR1 signal.
1213+
type reloadFromSourceFunc func(file, adapter string) error
1214+
1215+
// reloadFromSourceCallback is the stored callback
1216+
// which is called when we receive a SIGUSR1 signal.
1217+
var reloadFromSourceCallback reloadFromSourceFunc
1218+
1219+
// errReloadFromSourceUnavailable is returned when no reload-from-source callback is set.
1220+
var errReloadFromSourceUnavailable = errors.New("reload from source unavailable in this process") //nolint:unused
1221+
1222+
// SetLastConfig records the given source file and adapter as the
1223+
// last-known external configuration source. Intended to be called
1224+
// only when starting via "caddy run --config <file> --adapter <adapter>".
1225+
func SetLastConfig(file, adapter string, fn reloadFromSourceFunc) {
1226+
lastConfigMu.Lock()
1227+
lastConfigFile = file
1228+
lastConfigAdapter = adapter
1229+
reloadFromSourceCallback = fn
1230+
lastConfigMu.Unlock()
1231+
}
1232+
1233+
// ClearLastConfigIfDifferent clears the recorded last-config if the provided
1234+
// source file/adapter do not match the recorded last-config. If both srcFile
1235+
// and srcAdapter are empty, the last-config is cleared.
1236+
func ClearLastConfigIfDifferent(srcFile, srcAdapter string) {
1237+
if (srcFile != "" || srcAdapter != "") && lastConfigMatches(srcFile, srcAdapter) {
1238+
return
1239+
}
1240+
SetLastConfig("", "", nil)
1241+
}
1242+
1243+
// getLastConfig returns the last-known config file and adapter.
1244+
func getLastConfig() (file, adapter string, fn reloadFromSourceFunc) {
1245+
lastConfigMu.RLock()
1246+
f, a, cb := lastConfigFile, lastConfigAdapter, reloadFromSourceCallback
1247+
lastConfigMu.RUnlock()
1248+
return f, a, cb
1249+
}
1250+
1251+
// lastConfigMatches returns true if the provided source file and/or adapter
1252+
// matches the recorded last-config. Matching rules (in priority order):
1253+
// 1. If srcAdapter is provided and differs from the recorded adapter, no match.
1254+
// 2. If srcFile exactly equals the recorded file, match.
1255+
// 3. If both sides can be made absolute and equal, match.
1256+
// 4. If basenames are equal, match.
1257+
func lastConfigMatches(srcFile, srcAdapter string) bool {
1258+
lf, la, _ := getLastConfig()
1259+
1260+
// If adapter is provided, it must match.
1261+
if srcAdapter != "" && srcAdapter != la {
1262+
return false
1263+
}
1264+
1265+
// Quick equality check.
1266+
if srcFile == lf {
1267+
return true
1268+
}
1269+
1270+
// Try absolute path comparison.
1271+
sAbs, sErr := filepath.Abs(srcFile)
1272+
lAbs, lErr := filepath.Abs(lf)
1273+
if sErr == nil && lErr == nil && sAbs == lAbs {
1274+
return true
1275+
}
1276+
1277+
// Final fallback: basename equality.
1278+
if filepath.Base(srcFile) == filepath.Base(lf) {
1279+
return true
1280+
}
1281+
1282+
return false
1283+
}
1284+
12001285
// errSameConfig is returned if the new config is the same
12011286
// as the old one. This isn't usually an actual, actionable
12021287
// error; it's mostly a sentinel value.

caddyconfig/load.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,13 @@ func (adminLoad) handleLoad(w http.ResponseWriter, r *http.Request) error {
121121
}
122122
}
123123

124+
// If this request changed the config, clear the last
125+
// config info we have stored, if it is different from
126+
// the original source.
127+
caddy.ClearLastConfigIfDifferent(
128+
r.Header.Get("Caddy-Config-Source-File"),
129+
r.Header.Get("Caddy-Config-Source-Adapter"))
130+
124131
caddy.Log().Named("admin.api").Info("load complete")
125132

126133
return nil

cmd/commandfuncs.go

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -231,8 +231,9 @@ func cmdRun(fl Flags) (int, error) {
231231
}
232232
// we don't use 'else' here since this value might have been changed in 'if' block; i.e. not mutually exclusive
233233
var configFile string
234+
var adapterUsed string
234235
if !resumeFlag {
235-
config, configFile, err = LoadConfig(configFlag, configAdapterFlag)
236+
config, configFile, adapterUsed, err = LoadConfig(configFlag, configAdapterFlag)
236237
if err != nil {
237238
logBuffer.FlushTo(defaultLogger)
238239
return caddy.ExitCodeFailedStartup, err
@@ -249,6 +250,19 @@ func cmdRun(fl Flags) (int, error) {
249250
}
250251
}
251252

253+
// If we have a source config file (we're running via 'caddy run --config ...'),
254+
// record it so SIGUSR1 can reload from the same file. Also provide a callback
255+
// that knows how to load/adapt that source when requested by the main process.
256+
if configFile != "" {
257+
caddy.SetLastConfig(configFile, adapterUsed, func(file, adapter string) error {
258+
cfg, _, _, err := LoadConfig(file, adapter)
259+
if err != nil {
260+
return err
261+
}
262+
return caddy.Load(cfg, true)
263+
})
264+
}
265+
252266
// run the initial config
253267
err = caddy.Load(config, true)
254268
if err != nil {
@@ -295,7 +309,7 @@ func cmdRun(fl Flags) (int, error) {
295309
// if enabled, reload config file automatically on changes
296310
// (this better only be used in dev!)
297311
if watchFlag {
298-
go watchConfigFile(configFile, configAdapterFlag)
312+
go watchConfigFile(configFile, adapterUsed)
299313
}
300314

301315
// warn if the environment does not provide enough information about the disk
@@ -350,7 +364,7 @@ func cmdReload(fl Flags) (int, error) {
350364
forceFlag := fl.Bool("force")
351365

352366
// get the config in caddy's native format
353-
config, configFile, err := LoadConfig(configFlag, configAdapterFlag)
367+
config, configFile, adapterUsed, err := LoadConfig(configFlag, configAdapterFlag)
354368
if err != nil {
355369
return caddy.ExitCodeFailedStartup, err
356370
}
@@ -368,6 +382,10 @@ func cmdReload(fl Flags) (int, error) {
368382
if forceFlag {
369383
headers.Set("Cache-Control", "must-revalidate")
370384
}
385+
// Provide the source file/adapter to the running process so it can
386+
// preserve its last-config knowledge if this reload came from the same source.
387+
headers.Set("Caddy-Config-Source-File", configFile)
388+
headers.Set("Caddy-Config-Source-Adapter", adapterUsed)
371389

372390
resp, err := AdminAPIRequest(adminAddr, http.MethodPost, "/load", headers, bytes.NewReader(config))
373391
if err != nil {
@@ -582,7 +600,7 @@ func cmdValidateConfig(fl Flags) (int, error) {
582600
fmt.Errorf("input file required when there is no Caddyfile in current directory (use --config flag)")
583601
}
584602

585-
input, _, err := LoadConfig(configFlag, adapterFlag)
603+
input, _, _, err := LoadConfig(configFlag, adapterFlag)
586604
if err != nil {
587605
return caddy.ExitCodeFailedStartup, err
588606
}
@@ -797,7 +815,7 @@ func DetermineAdminAPIAddress(address string, config []byte, configFile, configA
797815
loadedConfig := config
798816
if len(loadedConfig) == 0 {
799817
// get the config in caddy's native format
800-
loadedConfig, loadedConfigFile, err = LoadConfig(configFile, configAdapter)
818+
loadedConfig, loadedConfigFile, _, err = LoadConfig(configFile, configAdapter)
801819
if err != nil {
802820
return "", err
803821
}

cmd/main.go

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,12 @@ func handlePingbackConn(conn net.Conn, expect []byte) error {
100100
// there is no config available. It prints any warnings to stderr,
101101
// and returns the resulting JSON config bytes along with
102102
// the name of the loaded config file (if any).
103-
func LoadConfig(configFile, adapterName string) ([]byte, string, error) {
103+
// The return values are:
104+
// - config bytes (nil if no config)
105+
// - config file used ("" if none)
106+
// - adapter used ("" if none)
107+
// - error, if any
108+
func LoadConfig(configFile, adapterName string) ([]byte, string, string, error) {
104109
return loadConfigWithLogger(caddy.Log(), configFile, adapterName)
105110
}
106111

@@ -138,7 +143,7 @@ func isCaddyfile(configFile, adapterName string) (bool, error) {
138143
return false, nil
139144
}
140145

141-
func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([]byte, string, error) {
146+
func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([]byte, string, string, error) {
142147
// if no logger is provided, use a nop logger
143148
// just so we don't have to check for nil
144149
if logger == nil {
@@ -147,7 +152,7 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
147152

148153
// specifying an adapter without a config file is ambiguous
149154
if adapterName != "" && configFile == "" {
150-
return nil, "", fmt.Errorf("cannot adapt config without config file (use --config)")
155+
return nil, "", "", fmt.Errorf("cannot adapt config without config file (use --config)")
151156
}
152157

153158
// load initial config and adapter
@@ -158,13 +163,13 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
158163
if configFile == "-" {
159164
config, err = io.ReadAll(os.Stdin)
160165
if err != nil {
161-
return nil, "", fmt.Errorf("reading config from stdin: %v", err)
166+
return nil, "", "", fmt.Errorf("reading config from stdin: %v", err)
162167
}
163168
logger.Info("using config from stdin")
164169
} else {
165170
config, err = os.ReadFile(configFile)
166171
if err != nil {
167-
return nil, "", fmt.Errorf("reading config from file: %v", err)
172+
return nil, "", "", fmt.Errorf("reading config from file: %v", err)
168173
}
169174
logger.Info("using config from file", zap.String("file", configFile))
170175
}
@@ -179,7 +184,7 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
179184
cfgAdapter = nil
180185
} else if err != nil {
181186
// default Caddyfile exists, but error reading it
182-
return nil, "", fmt.Errorf("reading default Caddyfile: %v", err)
187+
return nil, "", "", fmt.Errorf("reading default Caddyfile: %v", err)
183188
} else {
184189
// success reading default Caddyfile
185190
configFile = "Caddyfile"
@@ -191,14 +196,14 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
191196
if yes, err := isCaddyfile(configFile, adapterName); yes {
192197
adapterName = "caddyfile"
193198
} else if err != nil {
194-
return nil, "", err
199+
return nil, "", "", err
195200
}
196201

197202
// load config adapter
198203
if adapterName != "" {
199204
cfgAdapter = caddyconfig.GetAdapter(adapterName)
200205
if cfgAdapter == nil {
201-
return nil, "", fmt.Errorf("unrecognized config adapter: %s", adapterName)
206+
return nil, "", "", fmt.Errorf("unrecognized config adapter: %s", adapterName)
202207
}
203208
}
204209

@@ -208,7 +213,7 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
208213
"filename": configFile,
209214
})
210215
if err != nil {
211-
return nil, "", fmt.Errorf("adapting config using %s: %v", adapterName, err)
216+
return nil, "", "", fmt.Errorf("adapting config using %s: %v", adapterName, err)
212217
}
213218
logger.Info("adapted config to JSON", zap.String("adapter", adapterName))
214219
for _, warn := range warnings {
@@ -226,11 +231,11 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
226231
// validate that the config is at least valid JSON
227232
err = json.Unmarshal(config, new(any))
228233
if err != nil {
229-
return nil, "", fmt.Errorf("config is not valid JSON: %v; did you mean to use a config adapter (the --adapter flag)?", err)
234+
return nil, "", "", fmt.Errorf("config is not valid JSON: %v; did you mean to use a config adapter (the --adapter flag)?", err)
230235
}
231236
}
232237

233-
return config, configFile, nil
238+
return config, configFile, adapterName, nil
234239
}
235240

236241
// watchConfigFile watches the config file at filename for changes
@@ -256,7 +261,7 @@ func watchConfigFile(filename, adapterName string) {
256261
}
257262

258263
// get current config
259-
lastCfg, _, err := loadConfigWithLogger(nil, filename, adapterName)
264+
lastCfg, _, _, err := loadConfigWithLogger(nil, filename, adapterName)
260265
if err != nil {
261266
logger().Error("unable to load latest config", zap.Error(err))
262267
return
@@ -268,7 +273,7 @@ func watchConfigFile(filename, adapterName string) {
268273
//nolint:staticcheck
269274
for range time.Tick(1 * time.Second) {
270275
// get current config
271-
newCfg, _, err := loadConfigWithLogger(nil, filename, adapterName)
276+
newCfg, _, _, err := loadConfigWithLogger(nil, filename, adapterName)
272277
if err != nil {
273278
logger().Error("unable to load latest config", zap.Error(err))
274279
return

cmd/storagefuncs.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ type storVal struct {
3636
// determineStorage returns the top-level storage module from the given config.
3737
// It may return nil even if no error.
3838
func determineStorage(configFile string, configAdapter string) (*storVal, error) {
39-
cfg, _, err := LoadConfig(configFile, configAdapter)
39+
cfg, _, _, err := LoadConfig(configFile, configAdapter)
4040
if err != nil {
4141
return nil, err
4242
}

sigtrap_posix.go

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package caddy
1818

1919
import (
2020
"context"
21+
"errors"
2122
"os"
2223
"os/signal"
2324
"syscall"
@@ -48,7 +49,31 @@ func trapSignalsPosix() {
4849
exitProcessFromSignal("SIGTERM")
4950

5051
case syscall.SIGUSR1:
51-
Log().Info("not implemented", zap.String("signal", "SIGUSR1"))
52+
logger := Log().With(zap.String("signal", "SIGUSR1"))
53+
// If we know the last source config file/adapter (set when starting
54+
// via `caddy run --config <file> --adapter <adapter>`), attempt
55+
// to reload from that source. Otherwise, ignore the signal.
56+
file, adapter, reloadCallback := getLastConfig()
57+
if file == "" {
58+
logger.Info("last config unknown, ignored SIGUSR1")
59+
break
60+
}
61+
logger = logger.With(
62+
zap.String("file", file),
63+
zap.String("adapter", adapter))
64+
if reloadCallback == nil {
65+
logger.Warn("no reload helper available, ignored SIGUSR1")
66+
break
67+
}
68+
logger.Info("reloading config from last-known source")
69+
if err := reloadCallback(file, adapter); errors.Is(err, errReloadFromSourceUnavailable) {
70+
// No reload helper available (likely not started via caddy run).
71+
logger.Warn("reload from source unavailable in this process; ignored SIGUSR1")
72+
} else if err != nil {
73+
logger.Error("failed to reload config from file", zap.Error(err))
74+
} else {
75+
logger.Info("successfully reloaded config from file")
76+
}
5277

5378
case syscall.SIGUSR2:
5479
Log().Info("not implemented", zap.String("signal", "SIGUSR2"))

0 commit comments

Comments
 (0)