From 5a0603ed72cead424a34b3bc5af3a5b1629ac187 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Tue, 31 Dec 2019 16:56:19 -0700 Subject: [PATCH] Config auto-save; run --resume flag; update environ output (close #2903) Config auto-saving is on by default and can be disabled. The --environ flag (or environ subcommand) now print more useful information from Caddy and the runtime, including some nifty paths. --- admin.go | 16 +++++++++++++++- caddy.go | 43 +++++++++++++++++++++++++++++-------------- cmd/commandfuncs.go | 34 ++++++++++++++++++++++++++-------- cmd/commands.go | 6 +++++- cmd/main.go | 19 +++++++++++++++++++ 5 files changed, 94 insertions(+), 24 deletions(-) diff --git a/admin.go b/admin.go index bf1198594..7a5d0b668 100644 --- a/admin.go +++ b/admin.go @@ -66,6 +66,16 @@ type AdminConfig struct { // will be the default value. If set but empty, no origins will // be allowed. Origins []string `json:"origins,omitempty"` + + // Options related to configuration management. + Config *ConfigSettings `json:"config,omitempty"` +} + +// ConfigSettings configures the, uh, configuration... and +// management thereof. +type ConfigSettings struct { + // Whether to keep a copy of the active config on disk. Default is true. + Persist *bool `json:"persist,omitempty"` } // listenAddr extracts a singular listen address from ac.Listen, @@ -775,7 +785,11 @@ traverseLoop: return nil } -// RemoveMetaFields removes meta fields like "@id" from a JSON message. +// RemoveMetaFields removes meta fields like "@id" from a JSON message +// by using a simple regular expression. (An alternate way to do this +// would be to delete them from the raw, map[string]interface{} +// representation as they are indexed, then iterate the index we made +// and add them back after encoding as JSON, but this is simpler.) func RemoveMetaFields(rawJSON []byte) []byte { return idRegexp.ReplaceAllFunc(rawJSON, func(in []byte) []byte { // matches with a comma on both sides (when "@id" property is diff --git a/caddy.go b/caddy.go index f50598efa..fefe50b22 100644 --- a/caddy.go +++ b/caddy.go @@ -20,6 +20,7 @@ import ( "encoding/json" "fmt" "io" + "io/ioutil" "log" "net/http" "path" @@ -30,6 +31,7 @@ import ( "time" "github.com/mholt/certmagic" + "go.uber.org/zap" ) // Config is the top (or beginning) of the Caddy configuration structure. @@ -148,13 +150,6 @@ func changeConfig(method, path string, input []byte, forceReload bool) error { } } - // remove any @id fields from the JSON, which would cause - // loading to break since the field wouldn't be recognized - // (an alternate way to do this would be to delete them from - // rawCfg as they are indexed, then iterate the index we made - // and add them back after encoding as JSON) - newCfg = RemoveMetaFields(newCfg) - // load this new config; if it fails, we need to revert to // our old representation of caddy's actual config err = unsyncedDecodeAndRun(newCfg) @@ -232,15 +227,19 @@ func indexConfigObjects(ptr interface{}, configPath string, index map[string]str return nil } -// unsyncedDecodeAndRun decodes cfgJSON and runs -// it as the new config, replacing any other -// current config. It does not update the raw -// config state, as this is a lower-level function; -// most callers will want to use Load instead. -// A write lock on currentCfgMu is required! +// unsyncedDecodeAndRun removes any meta fields (like @id tags) +// from cfgJSON, decodes the result into a *Config, and runs +// it as the new config, replacing any other current config. +// It does NOT update the raw config state, as this is a +// lower-level function; most callers will want to use Load +// instead. A write lock on currentCfgMu is required! func unsyncedDecodeAndRun(cfgJSON []byte) error { + // remove any @id fields from the JSON, which would cause + // loading to break since the field wouldn't be recognized + strippedCfgJSON := RemoveMetaFields(cfgJSON) + var newCfg *Config - err := strictUnmarshalJSON(cfgJSON, &newCfg) + err := strictUnmarshalJSON(strippedCfgJSON, &newCfg) if err != nil { return err } @@ -258,6 +257,22 @@ func unsyncedDecodeAndRun(cfgJSON []byte) error { // Stop, Cleanup each old app unsyncedStop(oldCfg) + // autosave a non-nil config, if not disabled + if newCfg != nil && + (newCfg.Admin == nil || + newCfg.Admin.Config == nil || + newCfg.Admin.Config.Persist == nil || + *newCfg.Admin.Config.Persist) { + err := ioutil.WriteFile(ConfigAutosavePath, cfgJSON, 0600) + if err == nil { + Log().Info("autosaved config", zap.String("file", ConfigAutosavePath)) + } else { + Log().Error("unable to autosave config", + zap.String("file", ConfigAutosavePath), + zap.Error(err)) + } + } + return nil } diff --git a/cmd/commandfuncs.go b/cmd/commandfuncs.go index cc55df2f0..2d8e9d832 100644 --- a/cmd/commandfuncs.go +++ b/cmd/commandfuncs.go @@ -27,6 +27,7 @@ import ( "os" "os/exec" "reflect" + "runtime" "runtime/debug" "sort" "strings" @@ -141,6 +142,7 @@ func cmdStart(fl Flags) (int, error) { func cmdRun(fl Flags) (int, error) { runCmdConfigFlag := fl.String("config") runCmdConfigAdapterFlag := fl.String("adapter") + runCmdResumeFlag := fl.Bool("resume") runCmdPrintEnvFlag := fl.Bool("environ") runCmdPingbackFlag := fl.String("pingback") @@ -149,14 +151,32 @@ func cmdRun(fl Flags) (int, error) { printEnvironment() } - // get the config in caddy's native format - config, err := loadConfig(runCmdConfigFlag, runCmdConfigAdapterFlag) - if err != nil { - return caddy.ExitCodeFailedStartup, err - } // TODO: This is TEMPORARY, until the RCs moveStorage() + // load the config, depending on flags + var config []byte + var err error + if runCmdResumeFlag { + config, err = ioutil.ReadFile(caddy.ConfigAutosavePath) + if os.IsNotExist(err) { + // not a bad error; just can't resume if autosave file doesn't exist + caddy.Log().Info("no autosave file exists", zap.String("autosave_file", caddy.ConfigAutosavePath)) + runCmdResumeFlag = false + } else if err != nil { + return caddy.ExitCodeFailedStartup, err + } else { + caddy.Log().Info("resuming from last configuration", zap.String("autosave_file", caddy.ConfigAutosavePath)) + } + } + // we don't use 'else' here since this value might have been changed in 'if' block; i.e. not mutually exclusive + if !runCmdResumeFlag { + config, err = loadConfig(runCmdConfigFlag, runCmdConfigAdapterFlag) + if err != nil { + return caddy.ExitCodeFailedStartup, err + } + } + // set a fitting User-Agent for ACME requests goModule := caddy.GoModule() cleanModVersion := strings.TrimPrefix(goModule.Version, "v") @@ -167,9 +187,7 @@ func cmdRun(fl Flags) (int, error) { if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("loading initial config: %v", err) } - if len(config) > 0 { - caddy.Log().Named("admin").Info("Caddy 2 serving initial configuration") - } + caddy.Log().Info("serving initial configuration") // if we are to report to another process the successful start // of the server, do so now by echoing back contents of stdin diff --git a/cmd/commands.go b/cmd/commands.go index 971d8d930..93ebeff5b 100644 --- a/cmd/commands.go +++ b/cmd/commands.go @@ -116,11 +116,15 @@ line flags. If --environ is specified, the environment as seen by the Caddy process will be printed before starting. This is the same as the environ command but does -not quit after printing, and can be useful for troubleshooting.`, +not quit after printing, and can be useful for troubleshooting. + +The --resume flag will override the --config flag if there is a config auto- +save file. It is not an error if --resume is used and no autosave file exists.`, Flags: func() *flag.FlagSet { fs := flag.NewFlagSet("run", flag.ExitOnError) fs.String("config", "", "Configuration file") fs.String("adapter", "", "Name of config adapter to apply") + fs.Bool("resume", false, "Use saved config, if any (and prefer over --config file)") fs.Bool("environ", false, "Print environment") fs.String("pingback", "", "Echo confirmation bytes to this address on success") return fs diff --git a/cmd/main.go b/cmd/main.go index ca9b91457..564ef9f34 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -110,6 +110,9 @@ func loadConfig(configFile, adapterName string) ([]byte, error) { if err != nil { return nil, fmt.Errorf("reading config file: %v", err) } + caddy.Log().Info("using provided configuration", + zap.String("config_file", configFile), + zap.String("config_adapter", adapterName)) } else if adapterName == "" { // as a special case when no config file or adapter // is specified, see if the Caddyfile adapter is @@ -126,6 +129,7 @@ func loadConfig(configFile, adapterName string) ([]byte, error) { } else { // success reading default Caddyfile configFile = "Caddyfile" + caddy.Log().Info("using adjacent Caddyfile") } } } @@ -225,6 +229,21 @@ func flagHelp(fs *flag.FlagSet) string { } func printEnvironment() { + fmt.Printf("caddy.HomeDir=%s\n", caddy.HomeDir()) + fmt.Printf("caddy.AppDataDir=%s\n", caddy.AppDataDir()) + fmt.Printf("caddy.AppConfigDir=%s\n", caddy.AppConfigDir()) + fmt.Printf("caddy.ConfigAutosavePath=%s\n", caddy.ConfigAutosavePath) + fmt.Printf("runtime.GOOS=%s\n", runtime.GOOS) + fmt.Printf("runtime.GOARCH=%s\n", runtime.GOARCH) + fmt.Printf("runtime.Compiler=%s\n", runtime.Compiler) + fmt.Printf("runtime.NumCPU=%d\n", runtime.NumCPU()) + fmt.Printf("runtime.GOMAXPROCS=%d\n", runtime.GOMAXPROCS(0)) + fmt.Printf("runtime.Version=%s\n", runtime.Version()) + cwd, err := os.Getwd() + if err != nil { + cwd = fmt.Sprintf("", err) + } + fmt.Printf("os.Getwd=%s\n\n", cwd) for _, v := range os.Environ() { fmt.Println(v) }