diff options
Diffstat (limited to 'cmd')
-rw-r--r-- | cmd/commandfuncs.go | 112 | ||||
-rw-r--r-- | cmd/commands.go | 7 | ||||
-rw-r--r-- | cmd/main.go | 8 |
3 files changed, 83 insertions, 44 deletions
diff --git a/cmd/commandfuncs.go b/cmd/commandfuncs.go index ec56ab9..d308aeb 100644 --- a/cmd/commandfuncs.go +++ b/cmd/commandfuncs.go @@ -202,7 +202,7 @@ 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 if !runCmdResumeFlag { - config, configFile, err = loadConfig(runCmdConfigFlag, runCmdConfigAdapterFlag) + config, configFile, err = LoadConfig(runCmdConfigFlag, runCmdConfigAdapterFlag) if err != nil { return caddy.ExitCodeFailedStartup, err } @@ -275,25 +275,33 @@ func cmdRun(fl Flags) (int, error) { } func cmdStop(fl Flags) (int, error) { - stopCmdAddrFlag := fl.String("address") + addrFlag := fl.String("address") + configFlag := fl.String("config") + configAdapterFlag := fl.String("adapter") - err := apiRequest(stopCmdAddrFlag, http.MethodPost, "/stop", nil, nil) + adminAddr, err := DetermineAdminAPIAddress(addrFlag, configFlag, configAdapterFlag) + if err != nil { + return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err) + } + + resp, err := AdminAPIRequest(adminAddr, http.MethodPost, "/stop", nil, nil) if err != nil { caddy.Log().Warn("failed using API to stop instance", zap.Error(err)) return caddy.ExitCodeFailedStartup, err } + defer resp.Body.Close() return caddy.ExitCodeSuccess, nil } func cmdReload(fl Flags) (int, error) { - reloadCmdConfigFlag := fl.String("config") - reloadCmdConfigAdapterFlag := fl.String("adapter") - reloadCmdAddrFlag := fl.String("address") - reloadCmdForceFlag := fl.Bool("force") + configFlag := fl.String("config") + configAdapterFlag := fl.String("adapter") + addrFlag := fl.String("address") + forceFlag := fl.Bool("force") // get the config in caddy's native format - config, configFile, err := loadConfig(reloadCmdConfigFlag, reloadCmdConfigAdapterFlag) + config, configFile, err := LoadConfig(configFlag, configAdapterFlag) if err != nil { return caddy.ExitCodeFailedStartup, err } @@ -301,30 +309,22 @@ func cmdReload(fl Flags) (int, error) { return caddy.ExitCodeFailedStartup, fmt.Errorf("no config file to load") } - // get the address of the admin listener; use flag if specified - adminAddr := reloadCmdAddrFlag - if adminAddr == "" && len(config) > 0 { - var tmpStruct struct { - Admin caddy.AdminConfig `json:"admin"` - } - err = json.Unmarshal(config, &tmpStruct) - if err != nil { - return caddy.ExitCodeFailedStartup, - fmt.Errorf("unmarshaling admin listener address from config: %v", err) - } - adminAddr = tmpStruct.Admin.Listen + adminAddr, err := DetermineAdminAPIAddress(addrFlag, configFlag, configAdapterFlag) + if err != nil { + return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err) } // optionally force a config reload headers := make(http.Header) - if reloadCmdForceFlag { + if forceFlag { headers.Set("Cache-Control", "must-revalidate") } - err = apiRequest(adminAddr, http.MethodPost, "/load", headers, bytes.NewReader(config)) + resp, err := AdminAPIRequest(adminAddr, http.MethodPost, "/load", headers, bytes.NewReader(config)) if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("sending configuration to instance: %v", err) } + defer resp.Body.Close() return caddy.ExitCodeSuccess, nil } @@ -518,7 +518,7 @@ func cmdValidateConfig(fl Flags) (int, error) { validateCmdConfigFlag := fl.String("config") validateCmdAdapterFlag := fl.String("adapter") - input, _, err := loadConfig(validateCmdConfigFlag, validateCmdAdapterFlag) + input, _, err := LoadConfig(validateCmdConfigFlag, validateCmdAdapterFlag) if err != nil { return caddy.ExitCodeFailedStartup, err } @@ -640,17 +640,15 @@ commands: return caddy.ExitCodeSuccess, nil } -// apiRequest makes an API request to the endpoint adminAddr with the -// given HTTP method and request URI. If body is non-nil, it will be -// assumed to be Content-Type application/json. -func apiRequest(adminAddr, method, uri string, headers http.Header, body io.Reader) error { - // parse the admin address - if adminAddr == "" { - adminAddr = caddy.DefaultAdminListen - } +// AdminAPIRequest makes an API request according to the CLI flags given, +// with the given HTTP method and request URI. If body is non-nil, it will +// be assumed to be Content-Type application/json. The caller should close +// the response body. Should only be used by Caddy CLI commands which +// need to interact with a running instance of Caddy via the admin API. +func AdminAPIRequest(adminAddr, method, uri string, headers http.Header, body io.Reader) (*http.Response, error) { parsedAddr, err := caddy.ParseNetworkAddress(adminAddr) if err != nil || parsedAddr.PortRangeSize() > 1 { - return fmt.Errorf("invalid admin address %s: %v", adminAddr, err) + return nil, fmt.Errorf("invalid admin address %s: %v", adminAddr, err) } origin := parsedAddr.JoinHostPort(0) if parsedAddr.IsUnixNetwork() { @@ -660,7 +658,7 @@ func apiRequest(adminAddr, method, uri string, headers http.Header, body io.Read // form the request req, err := http.NewRequest(method, "http://"+origin+uri, body) if err != nil { - return fmt.Errorf("making request: %v", err) + return nil, fmt.Errorf("making request: %v", err) } if parsedAddr.IsUnixNetwork() { // When listening on a unix socket, the admin endpoint doesn't @@ -700,20 +698,58 @@ func apiRequest(adminAddr, method, uri string, headers http.Header, body io.Read resp, err := client.Do(req) if err != nil { - return fmt.Errorf("performing request: %v", err) + return nil, fmt.Errorf("performing request: %v", err) } - defer resp.Body.Close() // if it didn't work, let the user know if resp.StatusCode >= 400 { respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1024*10)) if err != nil { - return fmt.Errorf("HTTP %d: reading error message: %v", resp.StatusCode, err) + return nil, fmt.Errorf("HTTP %d: reading error message: %v", resp.StatusCode, err) + } + return nil, fmt.Errorf("caddy responded with error: HTTP %d: %s", resp.StatusCode, respBody) + } + + return resp, nil +} + +// DetermineAdminAPIAddress determines which admin API endpoint address should +// be used based on the inputs. By priority: if `address` is specified, then +// it is returned; if `configFile` (and `configAdapter`) are specified, then that +// config will be loaded to find the admin address; otherwise, the default +// admin listen address will be returned. +func DetermineAdminAPIAddress(address, configFile, configAdapter string) (string, error) { + // Prefer the address if specified and non-empty + if address != "" { + return address, nil + } + + // Try to load the config from file if specified, with the given adapter name + if configFile != "" { + // get the config in caddy's native format + config, loadedConfigFile, err := LoadConfig(configFile, configAdapter) + if err != nil { + return "", err + } + if loadedConfigFile == "" { + return "", fmt.Errorf("no config file to load") + } + + // get the address of the admin listener + if len(config) > 0 { + var tmpStruct struct { + Admin caddy.AdminConfig `json:"admin"` + } + err = json.Unmarshal(config, &tmpStruct) + if err != nil { + return "", fmt.Errorf("unmarshaling admin listener address from config: %v", err) + } + return tmpStruct.Admin.Listen, nil } - return fmt.Errorf("caddy responded with error: HTTP %d: %s", resp.StatusCode, respBody) } - return nil + // Fallback to the default listen address otherwise + return caddy.DefaultAdminListen, nil } type moduleInfo struct { diff --git a/cmd/commands.go b/cmd/commands.go index 1e2c40d..0c68b7c 100644 --- a/cmd/commands.go +++ b/cmd/commands.go @@ -156,16 +156,19 @@ development environment.`, RegisterCommand(Command{ Name: "stop", Func: cmdStop, + Usage: "[--address <interface>] [--config <path> [--adapter <name>]]", Short: "Gracefully stops a started Caddy process", Long: ` Stops the background Caddy process as gracefully as possible. It requires that the admin API is enabled and accessible, since it will -use the API's /stop endpoint. The address of this request can be -customized using the --address flag if it is not the default.`, +use the API's /stop endpoint. The address of this request can be customized +using the --address flag, or from the given --config, if not the default.`, Flags: func() *flag.FlagSet { fs := flag.NewFlagSet("stop", flag.ExitOnError) fs.String("address", "", "The address to use to reach the admin API endpoint, if not the default") + fs.String("config", "", "Configuration file to use to parse the admin address, if --address is not used") + fs.String("adapter", "", "Name of config adapter to apply (when --config is used)") return fs }(), }) diff --git a/cmd/main.go b/cmd/main.go index 7c33c55..f111ba4 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -103,15 +103,15 @@ func handlePingbackConn(conn net.Conn, expect []byte) error { return nil } -// loadConfig loads the config from configFile and adapts it +// LoadConfig loads the config from configFile and adapts it // using adapterName. If adapterName is specified, configFile // must be also. If no configFile is specified, it tries // loading a default config file. The lack of a config file is // not treated as an error, but false will be returned if // there is no config available. It prints any warnings to stderr, // and returns the resulting JSON config bytes along with -// whether a config file was loaded or not. -func loadConfig(configFile, adapterName string) ([]byte, string, error) { +// the name of the loaded config file (if any). +func LoadConfig(configFile, adapterName string) ([]byte, string, error) { // specifying an adapter without a config file is ambiguous if adapterName != "" && configFile == "" { return nil, "", fmt.Errorf("cannot adapt config without config file (use --config)") @@ -262,7 +262,7 @@ func watchConfigFile(filename, adapterName string) { lastModified = info.ModTime() // load the contents of the file - config, _, err := loadConfig(filename, adapterName) + config, _, err := LoadConfig(filename, adapterName) if err != nil { logger().Error("unable to load latest config", zap.Error(err)) continue |