diff --git a/admin.go b/admin.go index 8eef91653..ff5bc713c 100644 --- a/admin.go +++ b/admin.go @@ -109,6 +109,12 @@ type ConfigSettings struct { // // EXPERIMENTAL: Subject to change. LoadRaw json.RawMessage `json:"load,omitempty" caddy:"namespace=caddy.config_loaders inline_key=module"` + + // The interval to pull config. With a non-zero value, will pull config + // from config loader (eg. a http loader) with given interval. + // + // EXPERIMENTAL: Subject to change. + LoadInterval Duration `json:"load_interval,omitempty"` } // IdentityConfig configures management of this server's identity. An identity diff --git a/caddy.go b/caddy.go index 96dfea5c6..ba025b18a 100644 --- a/caddy.go +++ b/caddy.go @@ -268,8 +268,9 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error { newCfg != nil && newCfg.Admin != nil && newCfg.Admin.Config != nil && - newCfg.Admin.Config.LoadRaw != nil { - return fmt.Errorf("recursive config loading detected: pulled configs cannot pull other configs") + newCfg.Admin.Config.LoadRaw != nil && + newCfg.Admin.Config.LoadInterval <= 0 { + return fmt.Errorf("recursive config loading detected: pulled configs cannot pull other configs without positive load_interval") } // run the new config and start all its apps @@ -480,23 +481,42 @@ func finishSettingUp(ctx Context, cfg *Config) error { if err != nil { return fmt.Errorf("loading config loader module: %s", err) } - loadedConfig, err := val.(ConfigLoader).LoadConfig(ctx) - if err != nil { - return fmt.Errorf("loading dynamic config from %T: %v", val, err) - } - - // do this in a goroutine so current config can finish being loaded; otherwise deadlock - go func() { - Log().Info("applying dynamically-loaded config", zap.String("loader_module", val.(Module).CaddyModule().ID.Name())) + runLoadedConfig := func(config []byte) { + Log().Info("applying dynamically-loaded config", zap.String("loader_module", val.(Module).CaddyModule().ID.Name()), zap.Int("pull_interval", int(cfg.Admin.Config.LoadInterval))) currentCfgMu.Lock() - err := unsyncedDecodeAndRun(loadedConfig, false) + err := unsyncedDecodeAndRun(config, false) currentCfgMu.Unlock() if err == nil { Log().Info("dynamically-loaded config applied successfully") } else { Log().Error("running dynamically-loaded config failed", zap.Error(err)) } - }() + } + if cfg.Admin.Config.LoadInterval > 0 { + go func() { + select { + // if LoadInterval is positive, will wait for the interval and then run with new config + case <-time.After(time.Duration(cfg.Admin.Config.LoadInterval)): + loadedConfig, err := val.(ConfigLoader).LoadConfig(ctx) + if err != nil { + Log().Error("loading dynamic config failed", zap.Error(err)) + return + } + runLoadedConfig(loadedConfig) + case <-ctx.Done(): + return + } + }() + } else { + // if no LoadInterval is provided, will load config synchronously + loadedConfig, err := val.(ConfigLoader).LoadConfig(ctx) + if err != nil { + return fmt.Errorf("loading dynamic config from %T: %v", val, err) + } + // do this in a goroutine so current config can finish being loaded; otherwise deadlock + go runLoadedConfig(loadedConfig) + } + } return nil