mirror of
https://github.com/caddyserver/caddy.git
synced 2025-09-29 15:31:06 -04:00
core: Reloading with SIGUSR1
if config never changed via admin (#7258)
Some checks failed
Tests / test (./cmd/caddy/caddy, ~1.25.0, ubuntu-latest, 0, 1.25, linux) (push) Failing after 54s
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.25.0, 1.25, aix) (push) Failing after 15s
Cross-Build / build (~1.25.0, 1.25, darwin) (push) Failing after 15s
Cross-Build / build (~1.25.0, 1.25, dragonfly) (push) Failing after 16s
Cross-Build / build (~1.25.0, 1.25, freebsd) (push) Failing after 15s
Cross-Build / build (~1.25.0, 1.25, illumos) (push) Failing after 14s
Cross-Build / build (~1.25.0, 1.25, linux) (push) Failing after 28s
Cross-Build / build (~1.25.0, 1.25, netbsd) (push) Failing after 18s
Cross-Build / build (~1.25.0, 1.25, openbsd) (push) Failing after 16s
Cross-Build / build (~1.25.0, 1.25, solaris) (push) Failing after 14s
Cross-Build / build (~1.25.0, 1.25, windows) (push) Failing after 13s
Lint / lint (ubuntu-latest, linux) (push) Failing after 14s
Lint / govulncheck (push) Successful in 1m38s
Lint / dependency-review (push) Failing after 15s
Tests / test (./cmd/caddy/caddy, ~1.25.0, macos-14, 0, 1.25, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.25.0, windows-latest, True, 1.25, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Has started running
Some checks failed
Tests / test (./cmd/caddy/caddy, ~1.25.0, ubuntu-latest, 0, 1.25, linux) (push) Failing after 54s
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.25.0, 1.25, aix) (push) Failing after 15s
Cross-Build / build (~1.25.0, 1.25, darwin) (push) Failing after 15s
Cross-Build / build (~1.25.0, 1.25, dragonfly) (push) Failing after 16s
Cross-Build / build (~1.25.0, 1.25, freebsd) (push) Failing after 15s
Cross-Build / build (~1.25.0, 1.25, illumos) (push) Failing after 14s
Cross-Build / build (~1.25.0, 1.25, linux) (push) Failing after 28s
Cross-Build / build (~1.25.0, 1.25, netbsd) (push) Failing after 18s
Cross-Build / build (~1.25.0, 1.25, openbsd) (push) Failing after 16s
Cross-Build / build (~1.25.0, 1.25, solaris) (push) Failing after 14s
Cross-Build / build (~1.25.0, 1.25, windows) (push) Failing after 13s
Lint / lint (ubuntu-latest, linux) (push) Failing after 14s
Lint / govulncheck (push) Successful in 1m38s
Lint / dependency-review (push) Failing after 15s
Tests / test (./cmd/caddy/caddy, ~1.25.0, macos-14, 0, 1.25, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.25.0, windows-latest, True, 1.25, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Has started running
This commit is contained in:
parent
b2ab419922
commit
65e0ddc221
7
admin.go
7
admin.go
@ -1029,6 +1029,13 @@ func handleConfig(w http.ResponseWriter, r *http.Request) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// If this request changed the config, clear the last
|
||||
// config info we have stored, if it is different from
|
||||
// the original source.
|
||||
ClearLastConfigIfDifferent(
|
||||
r.Header.Get("Caddy-Config-Source-File"),
|
||||
r.Header.Get("Caddy-Config-Source-Adapter"))
|
||||
|
||||
default:
|
||||
return APIError{
|
||||
HTTPStatus: http.StatusMethodNotAllowed,
|
||||
|
85
caddy.go
85
caddy.go
@ -1197,6 +1197,91 @@ var (
|
||||
rawCfgMu sync.RWMutex
|
||||
)
|
||||
|
||||
// lastConfigFile and lastConfigAdapter remember the source config
|
||||
// file and adapter used when Caddy was started via the CLI "run" command.
|
||||
// These are consulted by the SIGUSR1 handler to attempt reloading from
|
||||
// the same source. They are intentionally not set for other entrypoints
|
||||
// such as "caddy start" or subcommands like file-server.
|
||||
var (
|
||||
lastConfigMu sync.RWMutex
|
||||
lastConfigFile string
|
||||
lastConfigAdapter string
|
||||
)
|
||||
|
||||
// reloadFromSourceFunc is the type of stored callback
|
||||
// which is called when we receive a SIGUSR1 signal.
|
||||
type reloadFromSourceFunc func(file, adapter string) error
|
||||
|
||||
// reloadFromSourceCallback is the stored callback
|
||||
// which is called when we receive a SIGUSR1 signal.
|
||||
var reloadFromSourceCallback reloadFromSourceFunc
|
||||
|
||||
// errReloadFromSourceUnavailable is returned when no reload-from-source callback is set.
|
||||
var errReloadFromSourceUnavailable = errors.New("reload from source unavailable in this process") //nolint:unused
|
||||
|
||||
// SetLastConfig records the given source file and adapter as the
|
||||
// last-known external configuration source. Intended to be called
|
||||
// only when starting via "caddy run --config <file> --adapter <adapter>".
|
||||
func SetLastConfig(file, adapter string, fn reloadFromSourceFunc) {
|
||||
lastConfigMu.Lock()
|
||||
lastConfigFile = file
|
||||
lastConfigAdapter = adapter
|
||||
reloadFromSourceCallback = fn
|
||||
lastConfigMu.Unlock()
|
||||
}
|
||||
|
||||
// ClearLastConfigIfDifferent clears the recorded last-config if the provided
|
||||
// source file/adapter do not match the recorded last-config. If both srcFile
|
||||
// and srcAdapter are empty, the last-config is cleared.
|
||||
func ClearLastConfigIfDifferent(srcFile, srcAdapter string) {
|
||||
if (srcFile != "" || srcAdapter != "") && lastConfigMatches(srcFile, srcAdapter) {
|
||||
return
|
||||
}
|
||||
SetLastConfig("", "", nil)
|
||||
}
|
||||
|
||||
// getLastConfig returns the last-known config file and adapter.
|
||||
func getLastConfig() (file, adapter string, fn reloadFromSourceFunc) {
|
||||
lastConfigMu.RLock()
|
||||
f, a, cb := lastConfigFile, lastConfigAdapter, reloadFromSourceCallback
|
||||
lastConfigMu.RUnlock()
|
||||
return f, a, cb
|
||||
}
|
||||
|
||||
// lastConfigMatches returns true if the provided source file and/or adapter
|
||||
// matches the recorded last-config. Matching rules (in priority order):
|
||||
// 1. If srcAdapter is provided and differs from the recorded adapter, no match.
|
||||
// 2. If srcFile exactly equals the recorded file, match.
|
||||
// 3. If both sides can be made absolute and equal, match.
|
||||
// 4. If basenames are equal, match.
|
||||
func lastConfigMatches(srcFile, srcAdapter string) bool {
|
||||
lf, la, _ := getLastConfig()
|
||||
|
||||
// If adapter is provided, it must match.
|
||||
if srcAdapter != "" && srcAdapter != la {
|
||||
return false
|
||||
}
|
||||
|
||||
// Quick equality check.
|
||||
if srcFile == lf {
|
||||
return true
|
||||
}
|
||||
|
||||
// Try absolute path comparison.
|
||||
sAbs, sErr := filepath.Abs(srcFile)
|
||||
lAbs, lErr := filepath.Abs(lf)
|
||||
if sErr == nil && lErr == nil && sAbs == lAbs {
|
||||
return true
|
||||
}
|
||||
|
||||
// Final fallback: basename equality.
|
||||
if filepath.Base(srcFile) == filepath.Base(lf) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// errSameConfig is returned if the new config is the same
|
||||
// as the old one. This isn't usually an actual, actionable
|
||||
// error; it's mostly a sentinel value.
|
||||
|
@ -121,6 +121,13 @@ func (adminLoad) handleLoad(w http.ResponseWriter, r *http.Request) error {
|
||||
}
|
||||
}
|
||||
|
||||
// If this request changed the config, clear the last
|
||||
// config info we have stored, if it is different from
|
||||
// the original source.
|
||||
caddy.ClearLastConfigIfDifferent(
|
||||
r.Header.Get("Caddy-Config-Source-File"),
|
||||
r.Header.Get("Caddy-Config-Source-Adapter"))
|
||||
|
||||
caddy.Log().Named("admin.api").Info("load complete")
|
||||
|
||||
return nil
|
||||
|
@ -231,8 +231,9 @@ func cmdRun(fl Flags) (int, error) {
|
||||
}
|
||||
// we don't use 'else' here since this value might have been changed in 'if' block; i.e. not mutually exclusive
|
||||
var configFile string
|
||||
var adapterUsed string
|
||||
if !resumeFlag {
|
||||
config, configFile, err = LoadConfig(configFlag, configAdapterFlag)
|
||||
config, configFile, adapterUsed, err = LoadConfig(configFlag, configAdapterFlag)
|
||||
if err != nil {
|
||||
logBuffer.FlushTo(defaultLogger)
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
@ -249,6 +250,19 @@ func cmdRun(fl Flags) (int, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a source config file (we're running via 'caddy run --config ...'),
|
||||
// record it so SIGUSR1 can reload from the same file. Also provide a callback
|
||||
// that knows how to load/adapt that source when requested by the main process.
|
||||
if configFile != "" {
|
||||
caddy.SetLastConfig(configFile, adapterUsed, func(file, adapter string) error {
|
||||
cfg, _, _, err := LoadConfig(file, adapter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return caddy.Load(cfg, true)
|
||||
})
|
||||
}
|
||||
|
||||
// run the initial config
|
||||
err = caddy.Load(config, true)
|
||||
if err != nil {
|
||||
@ -295,7 +309,7 @@ func cmdRun(fl Flags) (int, error) {
|
||||
// if enabled, reload config file automatically on changes
|
||||
// (this better only be used in dev!)
|
||||
if watchFlag {
|
||||
go watchConfigFile(configFile, configAdapterFlag)
|
||||
go watchConfigFile(configFile, adapterUsed)
|
||||
}
|
||||
|
||||
// warn if the environment does not provide enough information about the disk
|
||||
@ -350,7 +364,7 @@ func cmdReload(fl Flags) (int, error) {
|
||||
forceFlag := fl.Bool("force")
|
||||
|
||||
// get the config in caddy's native format
|
||||
config, configFile, err := LoadConfig(configFlag, configAdapterFlag)
|
||||
config, configFile, adapterUsed, err := LoadConfig(configFlag, configAdapterFlag)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
@ -368,6 +382,10 @@ func cmdReload(fl Flags) (int, error) {
|
||||
if forceFlag {
|
||||
headers.Set("Cache-Control", "must-revalidate")
|
||||
}
|
||||
// Provide the source file/adapter to the running process so it can
|
||||
// preserve its last-config knowledge if this reload came from the same source.
|
||||
headers.Set("Caddy-Config-Source-File", configFile)
|
||||
headers.Set("Caddy-Config-Source-Adapter", adapterUsed)
|
||||
|
||||
resp, err := AdminAPIRequest(adminAddr, http.MethodPost, "/load", headers, bytes.NewReader(config))
|
||||
if err != nil {
|
||||
@ -582,7 +600,7 @@ func cmdValidateConfig(fl Flags) (int, error) {
|
||||
fmt.Errorf("input file required when there is no Caddyfile in current directory (use --config flag)")
|
||||
}
|
||||
|
||||
input, _, err := LoadConfig(configFlag, adapterFlag)
|
||||
input, _, _, err := LoadConfig(configFlag, adapterFlag)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
@ -797,7 +815,7 @@ func DetermineAdminAPIAddress(address string, config []byte, configFile, configA
|
||||
loadedConfig := config
|
||||
if len(loadedConfig) == 0 {
|
||||
// get the config in caddy's native format
|
||||
loadedConfig, loadedConfigFile, err = LoadConfig(configFile, configAdapter)
|
||||
loadedConfig, loadedConfigFile, _, err = LoadConfig(configFile, configAdapter)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
31
cmd/main.go
31
cmd/main.go
@ -100,7 +100,12 @@ func handlePingbackConn(conn net.Conn, expect []byte) error {
|
||||
// there is no config available. It prints any warnings to stderr,
|
||||
// and returns the resulting JSON config bytes along with
|
||||
// the name of the loaded config file (if any).
|
||||
func LoadConfig(configFile, adapterName string) ([]byte, string, error) {
|
||||
// The return values are:
|
||||
// - config bytes (nil if no config)
|
||||
// - config file used ("" if none)
|
||||
// - adapter used ("" if none)
|
||||
// - error, if any
|
||||
func LoadConfig(configFile, adapterName string) ([]byte, string, string, error) {
|
||||
return loadConfigWithLogger(caddy.Log(), configFile, adapterName)
|
||||
}
|
||||
|
||||
@ -138,7 +143,7 @@ func isCaddyfile(configFile, adapterName string) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([]byte, string, error) {
|
||||
func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([]byte, string, string, error) {
|
||||
// if no logger is provided, use a nop logger
|
||||
// just so we don't have to check for nil
|
||||
if logger == nil {
|
||||
@ -147,7 +152,7 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
|
||||
|
||||
// specifying an adapter without a config file is ambiguous
|
||||
if adapterName != "" && configFile == "" {
|
||||
return nil, "", fmt.Errorf("cannot adapt config without config file (use --config)")
|
||||
return nil, "", "", fmt.Errorf("cannot adapt config without config file (use --config)")
|
||||
}
|
||||
|
||||
// load initial config and adapter
|
||||
@ -158,13 +163,13 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
|
||||
if configFile == "-" {
|
||||
config, err = io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("reading config from stdin: %v", err)
|
||||
return nil, "", "", fmt.Errorf("reading config from stdin: %v", err)
|
||||
}
|
||||
logger.Info("using config from stdin")
|
||||
} else {
|
||||
config, err = os.ReadFile(configFile)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("reading config from file: %v", err)
|
||||
return nil, "", "", fmt.Errorf("reading config from file: %v", err)
|
||||
}
|
||||
logger.Info("using config from file", zap.String("file", configFile))
|
||||
}
|
||||
@ -179,7 +184,7 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
|
||||
cfgAdapter = nil
|
||||
} else if err != nil {
|
||||
// default Caddyfile exists, but error reading it
|
||||
return nil, "", fmt.Errorf("reading default Caddyfile: %v", err)
|
||||
return nil, "", "", fmt.Errorf("reading default Caddyfile: %v", err)
|
||||
} else {
|
||||
// success reading default Caddyfile
|
||||
configFile = "Caddyfile"
|
||||
@ -191,14 +196,14 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
|
||||
if yes, err := isCaddyfile(configFile, adapterName); yes {
|
||||
adapterName = "caddyfile"
|
||||
} else if err != nil {
|
||||
return nil, "", err
|
||||
return nil, "", "", err
|
||||
}
|
||||
|
||||
// load config adapter
|
||||
if adapterName != "" {
|
||||
cfgAdapter = caddyconfig.GetAdapter(adapterName)
|
||||
if cfgAdapter == nil {
|
||||
return nil, "", fmt.Errorf("unrecognized config adapter: %s", adapterName)
|
||||
return nil, "", "", fmt.Errorf("unrecognized config adapter: %s", adapterName)
|
||||
}
|
||||
}
|
||||
|
||||
@ -208,7 +213,7 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
|
||||
"filename": configFile,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("adapting config using %s: %v", adapterName, err)
|
||||
return nil, "", "", fmt.Errorf("adapting config using %s: %v", adapterName, err)
|
||||
}
|
||||
logger.Info("adapted config to JSON", zap.String("adapter", adapterName))
|
||||
for _, warn := range warnings {
|
||||
@ -226,11 +231,11 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
|
||||
// validate that the config is at least valid JSON
|
||||
err = json.Unmarshal(config, new(any))
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("config is not valid JSON: %v; did you mean to use a config adapter (the --adapter flag)?", err)
|
||||
return nil, "", "", fmt.Errorf("config is not valid JSON: %v; did you mean to use a config adapter (the --adapter flag)?", err)
|
||||
}
|
||||
}
|
||||
|
||||
return config, configFile, nil
|
||||
return config, configFile, adapterName, nil
|
||||
}
|
||||
|
||||
// watchConfigFile watches the config file at filename for changes
|
||||
@ -256,7 +261,7 @@ func watchConfigFile(filename, adapterName string) {
|
||||
}
|
||||
|
||||
// get current config
|
||||
lastCfg, _, err := loadConfigWithLogger(nil, filename, adapterName)
|
||||
lastCfg, _, _, err := loadConfigWithLogger(nil, filename, adapterName)
|
||||
if err != nil {
|
||||
logger().Error("unable to load latest config", zap.Error(err))
|
||||
return
|
||||
@ -268,7 +273,7 @@ func watchConfigFile(filename, adapterName string) {
|
||||
//nolint:staticcheck
|
||||
for range time.Tick(1 * time.Second) {
|
||||
// get current config
|
||||
newCfg, _, err := loadConfigWithLogger(nil, filename, adapterName)
|
||||
newCfg, _, _, err := loadConfigWithLogger(nil, filename, adapterName)
|
||||
if err != nil {
|
||||
logger().Error("unable to load latest config", zap.Error(err))
|
||||
return
|
||||
|
@ -36,7 +36,7 @@ type storVal struct {
|
||||
// determineStorage returns the top-level storage module from the given config.
|
||||
// It may return nil even if no error.
|
||||
func determineStorage(configFile string, configAdapter string) (*storVal, error) {
|
||||
cfg, _, err := LoadConfig(configFile, configAdapter)
|
||||
cfg, _, _, err := LoadConfig(configFile, configAdapter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ package caddy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
@ -48,7 +49,31 @@ func trapSignalsPosix() {
|
||||
exitProcessFromSignal("SIGTERM")
|
||||
|
||||
case syscall.SIGUSR1:
|
||||
Log().Info("not implemented", zap.String("signal", "SIGUSR1"))
|
||||
logger := Log().With(zap.String("signal", "SIGUSR1"))
|
||||
// If we know the last source config file/adapter (set when starting
|
||||
// via `caddy run --config <file> --adapter <adapter>`), attempt
|
||||
// to reload from that source. Otherwise, ignore the signal.
|
||||
file, adapter, reloadCallback := getLastConfig()
|
||||
if file == "" {
|
||||
logger.Info("last config unknown, ignored SIGUSR1")
|
||||
break
|
||||
}
|
||||
logger = logger.With(
|
||||
zap.String("file", file),
|
||||
zap.String("adapter", adapter))
|
||||
if reloadCallback == nil {
|
||||
logger.Warn("no reload helper available, ignored SIGUSR1")
|
||||
break
|
||||
}
|
||||
logger.Info("reloading config from last-known source")
|
||||
if err := reloadCallback(file, adapter); errors.Is(err, errReloadFromSourceUnavailable) {
|
||||
// No reload helper available (likely not started via caddy run).
|
||||
logger.Warn("reload from source unavailable in this process; ignored SIGUSR1")
|
||||
} else if err != nil {
|
||||
logger.Error("failed to reload config from file", zap.Error(err))
|
||||
} else {
|
||||
logger.Info("successfully reloaded config from file")
|
||||
}
|
||||
|
||||
case syscall.SIGUSR2:
|
||||
Log().Info("not implemented", zap.String("signal", "SIGUSR2"))
|
||||
|
Loading…
x
Reference in New Issue
Block a user