summaryrefslogtreecommitdiff
path: root/cmd
diff options
context:
space:
mode:
Diffstat (limited to 'cmd')
-rw-r--r--cmd/cobra.go49
-rw-r--r--cmd/commandfuncs.go319
-rw-r--r--cmd/commands.go369
-rw-r--r--cmd/main.go108
-rw-r--r--cmd/packagesfuncs.go3
-rw-r--r--cmd/storagefuncs.go221
6 files changed, 727 insertions, 342 deletions
diff --git a/cmd/cobra.go b/cmd/cobra.go
index 203b7bd..c071f4a 100644
--- a/cmd/cobra.go
+++ b/cmd/cobra.go
@@ -1,7 +1,11 @@
package caddycmd
import (
+ "fmt"
+
"github.com/spf13/cobra"
+
+ "github.com/caddyserver/caddy/v2"
)
var rootCmd = &cobra.Command{
@@ -95,26 +99,59 @@ https://caddyserver.com/docs/running
// kind of annoying to have all the help text printed out if
// caddy has an error provisioning its modules, for instance...
SilenceUsage: true,
+ Version: onlyVersionText(),
}
const fullDocsFooter = `Full documentation is available at:
https://caddyserver.com/docs/command-line`
func init() {
+ rootCmd.SetVersionTemplate("{{.Version}}")
rootCmd.SetHelpTemplate(rootCmd.HelpTemplate() + "\n" + fullDocsFooter + "\n")
}
+func onlyVersionText() string {
+ _, f := caddy.Version()
+ return f
+}
+
func caddyCmdToCobra(caddyCmd Command) *cobra.Command {
cmd := &cobra.Command{
Use: caddyCmd.Name,
Short: caddyCmd.Short,
Long: caddyCmd.Long,
- RunE: func(cmd *cobra.Command, _ []string) error {
- fls := cmd.Flags()
- _, err := caddyCmd.Func(Flags{fls})
- return err
- },
}
- cmd.Flags().AddGoFlagSet(caddyCmd.Flags)
+ if caddyCmd.CobraFunc != nil {
+ caddyCmd.CobraFunc(cmd)
+ } else {
+ cmd.RunE = WrapCommandFuncForCobra(caddyCmd.Func)
+ cmd.Flags().AddGoFlagSet(caddyCmd.Flags)
+ }
return cmd
}
+
+// WrapCommandFuncForCobra wraps a Caddy CommandFunc for use
+// in a cobra command's RunE field.
+func WrapCommandFuncForCobra(f CommandFunc) func(cmd *cobra.Command, _ []string) error {
+ return func(cmd *cobra.Command, _ []string) error {
+ status, err := f(Flags{cmd.Flags()})
+ if status > 1 {
+ cmd.SilenceErrors = true
+ return &exitError{ExitCode: status, Err: err}
+ }
+ return err
+ }
+}
+
+// exitError carries the exit code from CommandFunc to Main()
+type exitError struct {
+ ExitCode int
+ Err error
+}
+
+func (e *exitError) Error() string {
+ if e.Err == nil {
+ return fmt.Sprintf("exiting with status %d", e.ExitCode)
+ }
+ return e.Err.Error()
+}
diff --git a/cmd/commandfuncs.go b/cmd/commandfuncs.go
index 09accd0..b0c576a 100644
--- a/cmd/commandfuncs.go
+++ b/cmd/commandfuncs.go
@@ -22,6 +22,7 @@ import (
"errors"
"fmt"
"io"
+ "io/fs"
"log"
"net"
"net/http"
@@ -32,18 +33,27 @@ import (
"strings"
"github.com/aryann/difflib"
+ "go.uber.org/zap"
+
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
- "go.uber.org/zap"
+ "github.com/caddyserver/caddy/v2/internal"
)
func cmdStart(fl Flags) (int, error) {
- startCmdConfigFlag := fl.String("config")
- startCmdConfigAdapterFlag := fl.String("adapter")
- startCmdPidfileFlag := fl.String("pidfile")
- startCmdWatchFlag := fl.Bool("watch")
- startCmdEnvfileFlag := fl.String("envfile")
+ configFlag := fl.String("config")
+ configAdapterFlag := fl.String("adapter")
+ pidfileFlag := fl.String("pidfile")
+ watchFlag := fl.Bool("watch")
+
+ var err error
+ var envfileFlag []string
+ envfileFlag, err = fl.GetStringSlice("envfile")
+ if err != nil {
+ return caddy.ExitCodeFailedStartup,
+ fmt.Errorf("reading envfile flag: %v", err)
+ }
// open a listener to which the child process will connect when
// it is ready to confirm that it has successfully started
@@ -64,22 +74,23 @@ func cmdStart(fl Flags) (int, error) {
// sure by giving it some random bytes and having it echo
// them back to us)
cmd := exec.Command(os.Args[0], "run", "--pingback", ln.Addr().String())
- if startCmdConfigFlag != "" {
- cmd.Args = append(cmd.Args, "--config", startCmdConfigFlag)
+ if configFlag != "" {
+ cmd.Args = append(cmd.Args, "--config", configFlag)
}
- if startCmdEnvfileFlag != "" {
- cmd.Args = append(cmd.Args, "--envfile", startCmdEnvfileFlag)
+
+ for _, envfile := range envfileFlag {
+ cmd.Args = append(cmd.Args, "--envfile", envfile)
}
- if startCmdConfigAdapterFlag != "" {
- cmd.Args = append(cmd.Args, "--adapter", startCmdConfigAdapterFlag)
+ if configAdapterFlag != "" {
+ cmd.Args = append(cmd.Args, "--adapter", configAdapterFlag)
}
- if startCmdWatchFlag {
+ if watchFlag {
cmd.Args = append(cmd.Args, "--watch")
}
- if startCmdPidfileFlag != "" {
- cmd.Args = append(cmd.Args, "--pidfile", startCmdPidfileFlag)
+ if pidfileFlag != "" {
+ cmd.Args = append(cmd.Args, "--pidfile", pidfileFlag)
}
- stdinpipe, err := cmd.StdinPipe()
+ stdinPipe, err := cmd.StdinPipe()
if err != nil {
return caddy.ExitCodeFailedStartup,
fmt.Errorf("creating stdin pipe: %v", err)
@@ -91,7 +102,8 @@ func cmdStart(fl Flags) (int, error) {
expect := make([]byte, 32)
_, err = rand.Read(expect)
if err != nil {
- return caddy.ExitCodeFailedStartup, fmt.Errorf("generating random confirmation bytes: %v", err)
+ return caddy.ExitCodeFailedStartup,
+ fmt.Errorf("generating random confirmation bytes: %v", err)
}
// begin writing the confirmation bytes to the child's
@@ -99,14 +111,15 @@ func cmdStart(fl Flags) (int, error) {
// started yet, and writing synchronously would result
// in a deadlock
go func() {
- _, _ = stdinpipe.Write(expect)
- stdinpipe.Close()
+ _, _ = stdinPipe.Write(expect)
+ stdinPipe.Close()
}()
// start the process
err = cmd.Start()
if err != nil {
- return caddy.ExitCodeFailedStartup, fmt.Errorf("starting caddy process: %v", err)
+ return caddy.ExitCodeFailedStartup,
+ fmt.Errorf("starting caddy process: %v", err)
}
// there are two ways we know we're done: either
@@ -154,41 +167,37 @@ func cmdStart(fl Flags) (int, error) {
func cmdRun(fl Flags) (int, error) {
caddy.TrapSignals()
- runCmdConfigFlag := fl.String("config")
- runCmdConfigAdapterFlag := fl.String("adapter")
- runCmdResumeFlag := fl.Bool("resume")
- runCmdLoadEnvfileFlag := fl.String("envfile")
- runCmdPrintEnvFlag := fl.Bool("environ")
- runCmdWatchFlag := fl.Bool("watch")
- runCmdPidfileFlag := fl.String("pidfile")
- runCmdPingbackFlag := fl.String("pingback")
+ configFlag := fl.String("config")
+ configAdapterFlag := fl.String("adapter")
+ resumeFlag := fl.Bool("resume")
+ printEnvFlag := fl.Bool("environ")
+ watchFlag := fl.Bool("watch")
+ pidfileFlag := fl.String("pidfile")
+ pingbackFlag := fl.String("pingback")
// load all additional envs as soon as possible
- if runCmdLoadEnvfileFlag != "" {
- if err := loadEnvFromFile(runCmdLoadEnvfileFlag); err != nil {
- return caddy.ExitCodeFailedStartup,
- fmt.Errorf("loading additional environment variables: %v", err)
- }
+ err := handleEnvFileFlag(fl)
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, err
}
// if we are supposed to print the environment, do that first
- if runCmdPrintEnvFlag {
+ if printEnvFlag {
printEnvironment()
}
// load the config, depending on flags
var config []byte
- var err error
- if runCmdResumeFlag {
+ if resumeFlag {
config, err = os.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
+ resumeFlag = false
} else if err != nil {
return caddy.ExitCodeFailedStartup, err
} else {
- if runCmdConfigFlag == "" {
+ if configFlag == "" {
caddy.Log().Info("resuming from last configuration",
zap.String("autosave_file", caddy.ConfigAutosavePath))
} else {
@@ -201,13 +210,23 @@ 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)
+ if !resumeFlag {
+ config, configFile, err = LoadConfig(configFlag, configAdapterFlag)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
}
+ // create pidfile now, in case loading config takes a while (issue #5477)
+ if pidfileFlag != "" {
+ err := caddy.PIDFile(pidfileFlag)
+ if err != nil {
+ caddy.Log().Error("unable to write PID file",
+ zap.String("pidfile", pidfileFlag),
+ zap.Error(err))
+ }
+ }
+
// run the initial config
err = caddy.Load(config, true)
if err != nil {
@@ -217,13 +236,13 @@ func cmdRun(fl Flags) (int, error) {
// if we are to report to another process the successful start
// of the server, do so now by echoing back contents of stdin
- if runCmdPingbackFlag != "" {
+ if pingbackFlag != "" {
confirmationBytes, err := io.ReadAll(os.Stdin)
if err != nil {
return caddy.ExitCodeFailedStartup,
fmt.Errorf("reading confirmation bytes from stdin: %v", err)
}
- conn, err := net.Dial("tcp", runCmdPingbackFlag)
+ conn, err := net.Dial("tcp", pingbackFlag)
if err != nil {
return caddy.ExitCodeFailedStartup,
fmt.Errorf("dialing confirmation address: %v", err)
@@ -232,24 +251,14 @@ func cmdRun(fl Flags) (int, error) {
_, err = conn.Write(confirmationBytes)
if err != nil {
return caddy.ExitCodeFailedStartup,
- fmt.Errorf("writing confirmation bytes to %s: %v", runCmdPingbackFlag, err)
+ fmt.Errorf("writing confirmation bytes to %s: %v", pingbackFlag, err)
}
}
// if enabled, reload config file automatically on changes
// (this better only be used in dev!)
- if runCmdWatchFlag {
- go watchConfigFile(configFile, runCmdConfigAdapterFlag)
- }
-
- // create pidfile
- if runCmdPidfileFlag != "" {
- err := caddy.PIDFile(runCmdPidfileFlag)
- if err != nil {
- caddy.Log().Error("unable to write PID file",
- zap.String("pidfile", runCmdPidfileFlag),
- zap.Error(err))
- }
+ if watchFlag {
+ go watchConfigFile(configFile, configAdapterFlag)
}
// warn if the environment does not provide enough information about the disk
@@ -275,11 +284,11 @@ func cmdRun(fl Flags) (int, error) {
}
func cmdStop(fl Flags) (int, error) {
- addrFlag := fl.String("address")
+ addressFlag := fl.String("address")
configFlag := fl.String("config")
configAdapterFlag := fl.String("adapter")
- adminAddr, err := DetermineAdminAPIAddress(addrFlag, nil, configFlag, configAdapterFlag)
+ adminAddr, err := DetermineAdminAPIAddress(addressFlag, nil, configFlag, configAdapterFlag)
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
}
@@ -297,7 +306,7 @@ func cmdStop(fl Flags) (int, error) {
func cmdReload(fl Flags) (int, error) {
configFlag := fl.String("config")
configAdapterFlag := fl.String("adapter")
- addrFlag := fl.String("address")
+ addressFlag := fl.String("address")
forceFlag := fl.Bool("force")
// get the config in caddy's native format
@@ -309,7 +318,7 @@ func cmdReload(fl Flags) (int, error) {
return caddy.ExitCodeFailedStartup, fmt.Errorf("no config file to load")
}
- adminAddr, err := DetermineAdminAPIAddress(addrFlag, config, configFlag, configAdapterFlag)
+ adminAddr, err := DetermineAdminAPIAddress(addressFlag, config, configFlag, configAdapterFlag)
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
}
@@ -411,60 +420,60 @@ func cmdListModules(fl Flags) (int, error) {
return caddy.ExitCodeSuccess, nil
}
-func cmdEnviron(_ Flags) (int, error) {
+func cmdEnviron(fl Flags) (int, error) {
+ // load all additional envs as soon as possible
+ err := handleEnvFileFlag(fl)
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, err
+ }
+
printEnvironment()
return caddy.ExitCodeSuccess, nil
}
func cmdAdaptConfig(fl Flags) (int, error) {
- adaptCmdInputFlag := fl.String("config")
- adaptCmdAdapterFlag := fl.String("adapter")
- adaptCmdPrettyFlag := fl.Bool("pretty")
- adaptCmdValidateFlag := fl.Bool("validate")
-
- // if no input file was specified, try a default
- // Caddyfile if the Caddyfile adapter is plugged in
- if adaptCmdInputFlag == "" && caddyconfig.GetAdapter("caddyfile") != nil {
- _, err := os.Stat("Caddyfile")
- if err == nil {
- // default Caddyfile exists
- adaptCmdInputFlag = "Caddyfile"
- caddy.Log().Info("using adjacent Caddyfile")
- } else if !os.IsNotExist(err) {
- // default Caddyfile exists, but error accessing it
- return caddy.ExitCodeFailedStartup, fmt.Errorf("accessing default Caddyfile: %v", err)
- }
+ inputFlag := fl.String("config")
+ adapterFlag := fl.String("adapter")
+ prettyFlag := fl.Bool("pretty")
+ validateFlag := fl.Bool("validate")
+
+ var err error
+ inputFlag, err = configFileWithRespectToDefault(caddy.Log(), inputFlag)
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, err
}
- if adaptCmdInputFlag == "" {
- return caddy.ExitCodeFailedStartup,
- fmt.Errorf("input file required when there is no Caddyfile in current directory (use --config flag)")
+ // load all additional envs as soon as possible
+ err = handleEnvFileFlag(fl)
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, err
}
- if adaptCmdAdapterFlag == "" {
+
+ if adapterFlag == "" {
return caddy.ExitCodeFailedStartup,
fmt.Errorf("adapter name is required (use --adapt flag or leave unspecified for default)")
}
- cfgAdapter := caddyconfig.GetAdapter(adaptCmdAdapterFlag)
+ cfgAdapter := caddyconfig.GetAdapter(adapterFlag)
if cfgAdapter == nil {
return caddy.ExitCodeFailedStartup,
- fmt.Errorf("unrecognized config adapter: %s", adaptCmdAdapterFlag)
+ fmt.Errorf("unrecognized config adapter: %s", adapterFlag)
}
- input, err := os.ReadFile(adaptCmdInputFlag)
+ input, err := os.ReadFile(inputFlag)
if err != nil {
return caddy.ExitCodeFailedStartup,
fmt.Errorf("reading input file: %v", err)
}
- opts := map[string]any{"filename": adaptCmdInputFlag}
+ opts := map[string]any{"filename": inputFlag}
adaptedConfig, warnings, err := cfgAdapter.Adapt(input, opts)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
- if adaptCmdPrettyFlag {
+ if prettyFlag {
var prettyBuf bytes.Buffer
err = json.Indent(&prettyBuf, adaptedConfig, "", "\t")
if err != nil {
@@ -482,15 +491,15 @@ func cmdAdaptConfig(fl Flags) (int, error) {
if warn.Directive != "" {
msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message)
}
- caddy.Log().Named(adaptCmdAdapterFlag).Warn(msg,
+ caddy.Log().Named(adapterFlag).Warn(msg,
zap.String("file", warn.File),
zap.Int("line", warn.Line))
}
// validate output if requested
- if adaptCmdValidateFlag {
+ if validateFlag {
var cfg *caddy.Config
- err = json.Unmarshal(adaptedConfig, &cfg)
+ err = caddy.StrictUnmarshalJSON(adaptedConfig, &cfg)
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("decoding config: %v", err)
}
@@ -504,26 +513,33 @@ func cmdAdaptConfig(fl Flags) (int, error) {
}
func cmdValidateConfig(fl Flags) (int, error) {
- validateCmdConfigFlag := fl.String("config")
- validateCmdAdapterFlag := fl.String("adapter")
- runCmdLoadEnvfileFlag := fl.String("envfile")
+ configFlag := fl.String("config")
+ adapterFlag := fl.String("adapter")
// load all additional envs as soon as possible
- if runCmdLoadEnvfileFlag != "" {
- if err := loadEnvFromFile(runCmdLoadEnvfileFlag); err != nil {
- return caddy.ExitCodeFailedStartup,
- fmt.Errorf("loading additional environment variables: %v", err)
- }
+ err := handleEnvFileFlag(fl)
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, err
+ }
+
+ // use default config and ensure a config file is specified
+ configFlag, err = configFileWithRespectToDefault(caddy.Log(), configFlag)
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, err
+ }
+ if configFlag == "" {
+ return caddy.ExitCodeFailedStartup,
+ fmt.Errorf("input file required when there is no Caddyfile in current directory (use --config flag)")
}
- input, _, err := LoadConfig(validateCmdConfigFlag, validateCmdAdapterFlag)
+ input, _, err := LoadConfig(configFlag, adapterFlag)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
input = caddy.RemoveMetaFields(input)
var cfg *caddy.Config
- err = json.Unmarshal(input, &cfg)
+ err = caddy.StrictUnmarshalJSON(input, &cfg)
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("decoding config: %v", err)
}
@@ -539,13 +555,13 @@ func cmdValidateConfig(fl Flags) (int, error) {
}
func cmdFmt(fl Flags) (int, error) {
- formatCmdConfigFile := fl.Arg(0)
- if formatCmdConfigFile == "" {
- formatCmdConfigFile = "Caddyfile"
+ configFile := fl.Arg(0)
+ if configFile == "" {
+ configFile = "Caddyfile"
}
// as a special case, read from stdin if the file name is "-"
- if formatCmdConfigFile == "-" {
+ if configFile == "-" {
input, err := io.ReadAll(os.Stdin)
if err != nil {
return caddy.ExitCodeFailedStartup,
@@ -555,7 +571,7 @@ func cmdFmt(fl Flags) (int, error) {
return caddy.ExitCodeSuccess, nil
}
- input, err := os.ReadFile(formatCmdConfigFile)
+ input, err := os.ReadFile(configFile)
if err != nil {
return caddy.ExitCodeFailedStartup,
fmt.Errorf("reading input file: %v", err)
@@ -564,7 +580,7 @@ func cmdFmt(fl Flags) (int, error) {
output := caddyfile.Format(input)
if fl.Bool("overwrite") {
- if err := os.WriteFile(formatCmdConfigFile, output, 0600); err != nil {
+ if err := os.WriteFile(configFile, output, 0o600); err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("overwriting formatted file: %v", err)
}
return caddy.ExitCodeSuccess, nil
@@ -588,13 +604,35 @@ func cmdFmt(fl Flags) (int, error) {
fmt.Print(string(output))
}
- if warning, diff := caddyfile.FormattingDifference(formatCmdConfigFile, input); diff {
- return caddy.ExitCodeFailedStartup, fmt.Errorf("%s:%d: Caddyfile input is not formatted", warning.File, warning.Line)
+ if warning, diff := caddyfile.FormattingDifference(configFile, input); diff {
+ return caddy.ExitCodeFailedStartup, fmt.Errorf(`%s:%d: Caddyfile input is not formatted; Tip: use '--overwrite' to update your Caddyfile in-place instead of previewing it. Consult '--help' for more options`,
+ warning.File,
+ warning.Line,
+ )
}
return caddy.ExitCodeSuccess, nil
}
+// handleEnvFileFlag loads the environment variables from the given --envfile
+// flag if specified. This should be called as early in the command function.
+func handleEnvFileFlag(fl Flags) error {
+ var err error
+ var envfileFlag []string
+ envfileFlag, err = fl.GetStringSlice("envfile")
+ if err != nil {
+ return fmt.Errorf("reading envfile flag: %v", err)
+ }
+
+ for _, envfile := range envfileFlag {
+ if err := loadEnvFromFile(envfile); err != nil {
+ return fmt.Errorf("loading additional environment variables: %v", err)
+ }
+ }
+
+ return nil
+}
+
// 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
@@ -607,7 +645,17 @@ func AdminAPIRequest(adminAddr, method, uri string, headers http.Header, body io
}
origin := "http://" + parsedAddr.JoinHostPort(0)
if parsedAddr.IsUnixNetwork() {
- origin = "http://unixsocket" // hack so that http.NewRequest() is happy
+ origin = "http://127.0.0.1" // bogus host is a hack so that http.NewRequest() is happy
+
+ // the unix address at this point might still contain the optional
+ // unix socket permissions, which are part of the address/host.
+ // those need to be removed first, as they aren't part of the
+ // resulting unix file path
+ addr, _, err := internal.SplitUnixSocketPermissionsBits(parsedAddr.Host)
+ if err != nil {
+ return nil, err
+ }
+ parsedAddr.Host = addr
}
// form the request
@@ -616,20 +664,24 @@ func AdminAPIRequest(adminAddr, method, uri string, headers http.Header, body io
return nil, fmt.Errorf("making request: %v", err)
}
if parsedAddr.IsUnixNetwork() {
- // When listening on a unix socket, the admin endpoint doesn't
- // accept any Host header because there is no host:port for
- // a unix socket's address. The server's host check is fairly
- // strict for security reasons, so we don't allow just any
- // Host header. For unix sockets, the Host header must be
- // empty. Unfortunately, Go makes it impossible to make HTTP
- // requests with an empty Host header... except with this one
- // weird trick. (Hopefully they don't fix it. It's already
- // hard enough to use HTTP over unix sockets.)
+ // We used to conform to RFC 2616 Section 14.26 which requires
+ // an empty host header when there is no host, as is the case
+ // with unix sockets. However, Go required a Host value so we
+ // used a hack of a space character as the host (it would see
+ // the Host was non-empty, then trim the space later). As of
+ // Go 1.20.6 (July 2023), this hack no longer works. See:
+ // https://github.com/golang/go/issues/60374
+ // See also the discussion here:
+ // https://github.com/golang/go/issues/61431
//
- // An equivalent curl command would be something like:
- // $ curl --unix-socket caddy.sock http:/:$REQUEST_URI
- req.URL.Host = " "
- req.Host = ""
+ // After that, we now require a Host value of either 127.0.0.1
+ // or ::1 if one is set. Above I choose to use 127.0.0.1. Even
+ // though the value should be completely irrelevant (it could be
+ // "srldkjfsd"), if for some reason the Host *is* used, at least
+ // we can have some reasonable assurance it will stay on the local
+ // machine and that browsers, if they ever allow access to unix
+ // sockets, can still enforce CORS, ensuring it is still coming
+ // from the local machine.
} else {
req.Header.Set("Origin", origin)
}
@@ -718,6 +770,31 @@ func DetermineAdminAPIAddress(address string, config []byte, configFile, configA
return caddy.DefaultAdminListen, nil
}
+// configFileWithRespectToDefault returns the filename to use for loading the config, based
+// on whether a config file is already specified and a supported default config file exists.
+func configFileWithRespectToDefault(logger *zap.Logger, configFile string) (string, error) {
+ const defaultCaddyfile = "Caddyfile"
+
+ // if no input file was specified, try a default Caddyfile if the Caddyfile adapter is plugged in
+ if configFile == "" && caddyconfig.GetAdapter("caddyfile") != nil {
+ _, err := os.Stat(defaultCaddyfile)
+ if err == nil {
+ // default Caddyfile exists
+ if logger != nil {
+ logger.Info("using adjacent Caddyfile")
+ }
+ return defaultCaddyfile, nil
+ }
+ if !errors.Is(err, fs.ErrNotExist) {
+ // problem checking
+ return configFile, fmt.Errorf("checking if default Caddyfile exists: %v", err)
+ }
+ }
+
+ // default config file does not exist or is irrelevant
+ return configFile, nil
+}
+
type moduleInfo struct {
caddyModuleID string
goModule *debug.Module
diff --git a/cmd/commands.go b/cmd/commands.go
index 9216b89..e5e1265 100644
--- a/cmd/commands.go
+++ b/cmd/commands.go
@@ -21,9 +21,10 @@ import (
"regexp"
"strings"
- "github.com/caddyserver/caddy/v2"
"github.com/spf13/cobra"
"github.com/spf13/cobra/doc"
+
+ "github.com/caddyserver/caddy/v2"
)
// Command represents a subcommand. Name, Func,
@@ -34,12 +35,6 @@ type Command struct {
// Required.
Name string
- // Func is a function that executes a subcommand using
- // the parsed flags. It returns an exit code and any
- // associated error.
- // Required.
- Func CommandFunc
-
// Usage is a brief message describing the syntax of
// the subcommand's flags and args. Use [] to indicate
// optional parameters and <> to enclose literal values
@@ -60,7 +55,21 @@ type Command struct {
Long string
// Flags is the flagset for command.
+ // This is ignored if CobraFunc is set.
Flags *flag.FlagSet
+
+ // Func is a function that executes a subcommand using
+ // the parsed flags. It returns an exit code and any
+ // associated error.
+ // Required if CobraFunc is not set.
+ Func CommandFunc
+
+ // CobraFunc allows further configuration of the command
+ // via cobra's APIs. If this is set, then Func and Flags
+ // are ignored, with the assumption that they are set in
+ // this function. A caddycmd.WrapCommandFuncForCobra helper
+ // exists to simplify porting CommandFunc to Cobra's RunE.
+ CobraFunc func(*cobra.Command)
}
// CommandFunc is a command's function. It runs the
@@ -79,34 +88,32 @@ var commands = make(map[string]Command)
func init() {
RegisterCommand(Command{
Name: "start",
- Func: cmdStart,
Usage: "[--config <path> [--adapter <name>]] [--envfile <path>] [--watch] [--pidfile <file>]",
Short: "Starts the Caddy process in the background and then returns",
Long: `
Starts the Caddy process, optionally bootstrapped with an initial config file.
This command unblocks after the server starts running or fails to run.
-If --envfile is specified, an environment file with environment variables in
-the KEY=VALUE format will be loaded into the Caddy process.
+If --envfile is specified, an environment file with environment variables
+in the KEY=VALUE format will be loaded into the Caddy process.
On Windows, the spawned child process will remain attached to the terminal, so
closing the window will forcefully stop Caddy; to avoid forgetting this, try
-using 'caddy run' instead to keep it in the foreground.`,
- Flags: func() *flag.FlagSet {
- fs := flag.NewFlagSet("start", flag.ExitOnError)
- fs.String("config", "", "Configuration file")
- fs.String("envfile", "", "Environment file to load")
- fs.String("adapter", "", "Name of config adapter to apply")
- fs.String("pidfile", "", "Path of file to which to write process ID")
- fs.Bool("watch", false, "Reload changed config file automatically")
- return fs
- }(),
+using 'caddy run' instead to keep it in the foreground.
+`,
+ CobraFunc: func(cmd *cobra.Command) {
+ cmd.Flags().StringP("config", "c", "", "Configuration file")
+ cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply")
+ cmd.Flags().StringSliceP("envfile", "", []string{}, "Environment file(s) to load")
+ cmd.Flags().BoolP("watch", "w", false, "Reload changed config file automatically")
+ cmd.Flags().StringP("pidfile", "", "", "Path of file to which to write process ID")
+ cmd.RunE = WrapCommandFuncForCobra(cmdStart)
+ },
})
RegisterCommand(Command{
Name: "run",
- Func: cmdRun,
- Usage: "[--config <path> [--adapter <name>]] [--envfile <path>] [--environ] [--resume] [--watch] [--pidfile <fil>]",
+ Usage: "[--config <path> [--adapter <name>]] [--envfile <path>] [--environ] [--resume] [--watch] [--pidfile <file>]",
Short: `Starts the Caddy process and blocks indefinitely`,
Long: `
Starts the Caddy process, optionally bootstrapped with an initial config file,
@@ -126,8 +133,8 @@ As a special case, if the current working directory has a file called
that file will be loaded and used to configure Caddy, even without any command
line flags.
-If --envfile is specified, an environment file with environment variables in
-the KEY=VALUE format will be loaded into the Caddy process.
+If --envfile is specified, an environment file with environment variables
+in the KEY=VALUE format will be loaded into the Caddy process.
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
@@ -138,44 +145,42 @@ save file. It is not an error if --resume is used and no autosave file exists.
If --watch is specified, the config file will be loaded automatically after
changes. ⚠️ This can make unintentional config changes easier; only use this
-option in a local development environment.`,
- 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.String("envfile", "", "Environment file to load")
- fs.Bool("environ", false, "Print environment")
- fs.Bool("resume", false, "Use saved config, if any (and prefer over --config file)")
- fs.Bool("watch", false, "Watch config file for changes and reload it automatically")
- fs.String("pidfile", "", "Path of file to which to write process ID")
- fs.String("pingback", "", "Echo confirmation bytes to this address on success")
- return fs
- }(),
+option in a local development environment.
+`,
+ CobraFunc: func(cmd *cobra.Command) {
+ cmd.Flags().StringP("config", "c", "", "Configuration file")
+ cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply")
+ cmd.Flags().StringSliceP("envfile", "", []string{}, "Environment file(s) to load")
+ cmd.Flags().BoolP("environ", "e", false, "Print environment")
+ cmd.Flags().BoolP("resume", "r", false, "Use saved config, if any (and prefer over --config file)")
+ cmd.Flags().BoolP("watch", "w", false, "Watch config file for changes and reload it automatically")
+ cmd.Flags().StringP("pidfile", "", "", "Path of file to which to write process ID")
+ cmd.Flags().StringP("pingback", "", "", "Echo confirmation bytes to this address on success")
+ cmd.RunE = WrapCommandFuncForCobra(cmdRun)
+ },
})
RegisterCommand(Command{
Name: "stop",
- Func: cmdStop,
- Usage: "[--address <interface>] [--config <path> [--adapter <name>]]",
+ Usage: "[--config <path> [--adapter <name>]] [--address <interface>]",
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, 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
- }(),
+using the --address flag, or from the given --config, if not the default.
+`,
+ CobraFunc: func(cmd *cobra.Command) {
+ cmd.Flags().StringP("config", "c", "", "Configuration file to use to parse the admin address, if --address is not used")
+ cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply (when --config is used)")
+ cmd.Flags().StringP("address", "", "", "The address to use to reach the admin API endpoint, if not the default")
+ cmd.RunE = WrapCommandFuncForCobra(cmdStop)
+ },
})
RegisterCommand(Command{
Name: "reload",
- Func: cmdReload,
Usage: "--config <path> [--adapter <name>] [--address <interface>]",
Short: "Changes the config of the running Caddy instance",
Long: `
@@ -185,20 +190,19 @@ workflows revolving around config files.
Since the admin endpoint is configurable, the endpoint configuration is loaded
from the --address flag if specified; otherwise it is loaded from the given
-config file; otherwise the default is assumed.`,
- Flags: func() *flag.FlagSet {
- fs := flag.NewFlagSet("reload", flag.ExitOnError)
- fs.String("config", "", "Configuration file (required)")
- fs.String("adapter", "", "Name of config adapter to apply")
- fs.String("address", "", "Address of the administration listener, if different from config")
- fs.Bool("force", false, "Force config reload, even if it is the same")
- return fs
- }(),
+config file; otherwise the default is assumed.
+`,
+ CobraFunc: func(cmd *cobra.Command) {
+ cmd.Flags().StringP("config", "c", "", "Configuration file (required)")
+ cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply")
+ cmd.Flags().StringP("address", "", "", "Address of the administration listener, if different from config")
+ cmd.Flags().BoolP("force", "f", false, "Force config reload, even if it is the same")
+ cmd.RunE = WrapCommandFuncForCobra(cmdReload)
+ },
})
RegisterCommand(Command{
Name: "version",
- Func: cmdVersion,
Short: "Prints the version",
Long: `
Prints the version of this Caddy binary.
@@ -213,31 +217,30 @@ detailed version information is printed as given by Go modules.
For more details about the full version string, see the Go module
documentation: https://go.dev/doc/modules/version-numbers
`,
+ Func: cmdVersion,
})
RegisterCommand(Command{
Name: "list-modules",
- Func: cmdListModules,
- Usage: "[--packages] [--versions]",
+ Usage: "[--packages] [--versions] [--skip-standard]",
Short: "Lists the installed Caddy modules",
- Flags: func() *flag.FlagSet {
- fs := flag.NewFlagSet("list-modules", flag.ExitOnError)
- fs.Bool("packages", false, "Print package paths")
- fs.Bool("versions", false, "Print version information")
- fs.Bool("skip-standard", false, "Skip printing standard modules")
- return fs
- }(),
+ CobraFunc: func(cmd *cobra.Command) {
+ cmd.Flags().BoolP("packages", "", false, "Print package paths")
+ cmd.Flags().BoolP("versions", "", false, "Print version information")
+ cmd.Flags().BoolP("skip-standard", "s", false, "Skip printing standard modules")
+ cmd.RunE = WrapCommandFuncForCobra(cmdListModules)
+ },
})
RegisterCommand(Command{
Name: "build-info",
- Func: cmdBuildInfo,
Short: "Prints information about this build",
+ Func: cmdBuildInfo,
})
RegisterCommand(Command{
Name: "environ",
- Func: cmdEnviron,
+ Usage: "[--envfile <path>]",
Short: "Prints the environment",
Long: `
Prints the environment as seen by this Caddy process.
@@ -247,6 +250,9 @@ configuration uses environment variables (e.g. "{env.VARIABLE}") then
this command can be useful for verifying that the variables will have
the values you expect in your config.
+If --envfile is specified, an environment file with environment variables
+in the KEY=VALUE format will be loaded into the Caddy process.
+
Note that environments may be different depending on how you run Caddy.
Environments for Caddy instances started by service managers such as
systemd are often different than the environment inherited from your
@@ -257,12 +263,15 @@ by adding the "--environ" flag.
Environments may contain sensitive data.
`,
+ CobraFunc: func(cmd *cobra.Command) {
+ cmd.Flags().StringSliceP("envfile", "", []string{}, "Environment file(s) to load")
+ cmd.RunE = WrapCommandFuncForCobra(cmdEnviron)
+ },
})
RegisterCommand(Command{
Name: "adapt",
- Func: cmdAdaptConfig,
- Usage: "--config <path> [--adapter <name>] [--pretty] [--validate]",
+ Usage: "--config <path> [--adapter <name>] [--pretty] [--validate] [--envfile <path>]",
Short: "Adapts a configuration to Caddy's native JSON",
Long: `
Adapts a configuration to Caddy's native JSON format and writes the
@@ -273,20 +282,23 @@ for human readability.
If --validate is used, the adapted config will be checked for validity.
If the config is invalid, an error will be printed to stderr and a non-
-zero exit status will be returned.`,
- Flags: func() *flag.FlagSet {
- fs := flag.NewFlagSet("adapt", flag.ExitOnError)
- fs.String("config", "", "Configuration file to adapt (required)")
- fs.String("adapter", "caddyfile", "Name of config adapter")
- fs.Bool("pretty", false, "Format the output for human readability")
- fs.Bool("validate", false, "Validate the output")
- return fs
- }(),
+zero exit status will be returned.
+
+If --envfile is specified, an environment file with environment variables
+in the KEY=VALUE format will be loaded into the Caddy process.
+`,
+ CobraFunc: func(cmd *cobra.Command) {
+ cmd.Flags().StringP("config", "c", "", "Configuration file to adapt (required)")
+ cmd.Flags().StringP("adapter", "a", "caddyfile", "Name of config adapter")
+ cmd.Flags().BoolP("pretty", "p", false, "Format the output for human readability")
+ cmd.Flags().BoolP("validate", "", false, "Validate the output")
+ cmd.Flags().StringSliceP("envfile", "", []string{}, "Environment file(s) to load")
+ cmd.RunE = WrapCommandFuncForCobra(cmdAdaptConfig)
+ },
})
RegisterCommand(Command{
Name: "validate",
- Func: cmdValidateConfig,
Usage: "--config <path> [--adapter <name>] [--envfile <path>]",
Short: "Tests whether a configuration file is valid",
Long: `
@@ -294,21 +306,70 @@ Loads and provisions the provided config, but does not start running it.
This reveals any errors with the configuration through the loading and
provisioning stages.
-If --envfile is specified, an environment file with environment variables in
-the KEY=VALUE format will be loaded into the Caddy process.`,
- Flags: func() *flag.FlagSet {
- fs := flag.NewFlagSet("validate", flag.ExitOnError)
- fs.String("config", "", "Input configuration file")
- fs.String("adapter", "", "Name of config adapter")
- fs.String("envfile", "", "Environment file to load")
- return fs
- }(),
+If --envfile is specified, an environment file with environment variables
+in the KEY=VALUE format will be loaded into the Caddy process.
+`,
+ CobraFunc: func(cmd *cobra.Command) {
+ cmd.Flags().StringP("config", "c", "", "Input configuration file")
+ cmd.Flags().StringP("adapter", "a", "", "Name of config adapter")
+ cmd.Flags().StringSliceP("envfile", "", []string{}, "Environment file(s) to load")
+ cmd.RunE = WrapCommandFuncForCobra(cmdValidateConfig)
+ },
+ })
+
+ RegisterCommand(Command{
+ Name: "storage",
+ Short: "Commands for working with Caddy's storage (EXPERIMENTAL)",
+ Long: `
+Allows exporting and importing Caddy's storage contents. The two commands can be
+combined in a pipeline to transfer directly from one storage to another:
+
+$ caddy storage export --config Caddyfile.old --output - |
+> caddy storage import --config Caddyfile.new --input -
+
+The - argument refers to stdout and stdin, respectively.
+
+NOTE: When importing to or exporting from file_system storage (the default), the command
+should be run as the user that owns the associated root path.
+
+EXPERIMENTAL: May be changed or removed.
+`,
+ CobraFunc: func(cmd *cobra.Command) {
+ exportCmd := &cobra.Command{
+ Use: "export --config <path> --output <path>",
+ Short: "Exports storage assets as a tarball",
+ Long: `
+The contents of the configured storage module (TLS certificates, etc)
+are exported via a tarball.
+
+--output is required, - can be given for stdout.
+`,
+ RunE: WrapCommandFuncForCobra(cmdExportStorage),
+ }
+ exportCmd.Flags().StringP("config", "c", "", "Input configuration file (required)")
+ exportCmd.Flags().StringP("output", "o", "", "Output path")
+ cmd.AddCommand(exportCmd)
+
+ importCmd := &cobra.Command{
+ Use: "import --config <path> --input <path>",
+ Short: "Imports storage assets from a tarball.",
+ Long: `
+Imports storage assets to the configured storage module. The import file must be
+a tar archive.
+
+--input is required, - can be given for stdin.
+`,
+ RunE: WrapCommandFuncForCobra(cmdImportStorage),
+ }
+ importCmd.Flags().StringP("config", "c", "", "Configuration file to load (required)")
+ importCmd.Flags().StringP("input", "i", "", "Tar of assets to load (required)")
+ cmd.AddCommand(importCmd)
+ },
})
RegisterCommand(Command{
Name: "fmt",
- Func: cmdFmt,
- Usage: "[--overwrite] [<path>]",
+ Usage: "[--overwrite] [--diff] [<path>]",
Short: "Formats a Caddyfile",
Long: `
Formats the Caddyfile by adding proper indentation and spaces to improve
@@ -324,44 +385,41 @@ is not a valid patch format.
If you wish you use stdin instead of a regular file, use - as the path.
When reading from stdin, the --overwrite flag has no effect: the result
-is always printed to stdout.`,
- Flags: func() *flag.FlagSet {
- fs := flag.NewFlagSet("fmt", flag.ExitOnError)
- fs.Bool("overwrite", false, "Overwrite the input file with the results")
- fs.Bool("diff", false, "Print the differences between the input file and the formatted output")
- return fs
- }(),
+is always printed to stdout.
+`,
+ CobraFunc: func(cmd *cobra.Command) {
+ cmd.Flags().BoolP("overwrite", "w", false, "Overwrite the input file with the results")
+ cmd.Flags().BoolP("diff", "d", false, "Print the differences between the input file and the formatted output")
+ cmd.RunE = WrapCommandFuncForCobra(cmdFmt)
+ },
})
RegisterCommand(Command{
Name: "upgrade",
- Func: cmdUpgrade,
Short: "Upgrade Caddy (EXPERIMENTAL)",
Long: `
Downloads an updated Caddy binary with the same modules/plugins at the
-latest versions. EXPERIMENTAL: May be changed or removed.`,
- Flags: func() *flag.FlagSet {
- fs := flag.NewFlagSet("upgrade", flag.ExitOnError)
- fs.Bool("keep-backup", false, "Keep the backed up binary, instead of deleting it")
- return fs
- }(),
+latest versions. EXPERIMENTAL: May be changed or removed.
+`,
+ CobraFunc: func(cmd *cobra.Command) {
+ cmd.Flags().BoolP("keep-backup", "k", false, "Keep the backed up binary, instead of deleting it")
+ cmd.RunE = WrapCommandFuncForCobra(cmdUpgrade)
+ },
})
RegisterCommand(Command{
Name: "add-package",
- Func: cmdAddPackage,
Usage: "<packages...>",
Short: "Adds Caddy packages (EXPERIMENTAL)",
Long: `
Downloads an updated Caddy binary with the specified packages (module/plugin)
-added. Retains existing packages. Returns an error if the any of packages are
+added. Retains existing packages. Returns an error if the any of packages are
already included. EXPERIMENTAL: May be changed or removed.
`,
- Flags: func() *flag.FlagSet {
- fs := flag.NewFlagSet("add-package", flag.ExitOnError)
- fs.Bool("keep-backup", false, "Keep the backed up binary, instead of deleting it")
- return fs
- }(),
+ CobraFunc: func(cmd *cobra.Command) {
+ cmd.Flags().BoolP("keep-backup", "k", false, "Keep the backed up binary, instead of deleting it")
+ cmd.RunE = WrapCommandFuncForCobra(cmdAddPackage)
+ },
})
RegisterCommand(Command{
@@ -370,35 +428,18 @@ already included. EXPERIMENTAL: May be changed or removed.
Usage: "<packages...>",
Short: "Removes Caddy packages (EXPERIMENTAL)",
Long: `
-Downloads an updated Caddy binaries without the specified packages (module/plugin).
-Returns an error if any of the packages are not included.
+Downloads an updated Caddy binaries without the specified packages (module/plugin).
+Returns an error if any of the packages are not included.
EXPERIMENTAL: May be changed or removed.
`,
- Flags: func() *flag.FlagSet {
- fs := flag.NewFlagSet("remove-package", flag.ExitOnError)
- fs.Bool("keep-backup", false, "Keep the backed up binary, instead of deleting it")
- return fs
- }(),
+ CobraFunc: func(cmd *cobra.Command) {
+ cmd.Flags().BoolP("keep-backup", "k", false, "Keep the backed up binary, instead of deleting it")
+ cmd.RunE = WrapCommandFuncForCobra(cmdRemovePackage)
+ },
})
RegisterCommand(Command{
- Name: "manpage",
- Func: func(fl Flags) (int, error) {
- dir := strings.TrimSpace(fl.String("directory"))
- if dir == "" {
- return caddy.ExitCodeFailedQuit, fmt.Errorf("designated output directory and specified section are required")
- }
- if err := os.MkdirAll(dir, 0755); err != nil {
- return caddy.ExitCodeFailedQuit, err
- }
- if err := doc.GenManTree(rootCmd, &doc.GenManHeader{
- Title: "Caddy",
- Section: "8", // https://en.wikipedia.org/wiki/Man_page#Manual_sections
- }, dir); err != nil {
- return caddy.ExitCodeFailedQuit, err
- }
- return caddy.ExitCodeSuccess, nil
- },
+ Name: "manpage",
Usage: "--directory <path>",
Short: "Generates the manual pages for Caddy commands",
Long: `
@@ -408,11 +449,25 @@ tagged into section 8 (System Administration).
The manual page files are generated into the directory specified by the
argument of --directory. If the directory does not exist, it will be created.
`,
- Flags: func() *flag.FlagSet {
- fs := flag.NewFlagSet("manpage", flag.ExitOnError)
- fs.String("directory", "", "The output directory where the manpages are generated")
- return fs
- }(),
+ CobraFunc: func(cmd *cobra.Command) {
+ cmd.Flags().StringP("directory", "o", "", "The output directory where the manpages are generated")
+ cmd.RunE = WrapCommandFuncForCobra(func(fl Flags) (int, error) {
+ dir := strings.TrimSpace(fl.String("directory"))
+ if dir == "" {
+ return caddy.ExitCodeFailedQuit, fmt.Errorf("designated output directory and specified section are required")
+ }
+ if err := os.MkdirAll(dir, 0o755); err != nil {
+ return caddy.ExitCodeFailedQuit, err
+ }
+ if err := doc.GenManTree(rootCmd, &doc.GenManHeader{
+ Title: "Caddy",
+ Section: "8", // https://en.wikipedia.org/wiki/Man_page#Manual_sections
+ }, dir); err != nil {
+ return caddy.ExitCodeFailedQuit, err
+ }
+ return caddy.ExitCodeSuccess, nil
+ })
+ },
})
// source: https://github.com/spf13/cobra/blob/main/shell_completions.md
@@ -420,40 +475,40 @@ argument of --directory. If the directory does not exist, it will be created.
Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate completion script",
Long: fmt.Sprintf(`To load completions:
-
+
Bash:
-
+
$ source <(%[1]s completion bash)
-
+
# To load completions for each session, execute once:
# Linux:
$ %[1]s completion bash > /etc/bash_completion.d/%[1]s
# macOS:
$ %[1]s completion bash > $(brew --prefix)/etc/bash_completion.d/%[1]s
-
+
Zsh:
-
+
# If shell completion is not already enabled in your environment,
# you will need to enable it. You can execute the following once:
-
+
$ echo "autoload -U compinit; compinit" >> ~/.zshrc
-
+
# To load completions for each session, execute once:
$ %[1]s completion zsh > "${fpath[1]}/_%[1]s"
-
+
# You will need to start a new shell for this setup to take effect.
-
+
fish:
-
+
$ %[1]s completion fish | source
-
+
# To load completions for each session, execute once:
$ %[1]s completion fish > ~/.config/fish/completions/%[1]s.fish
-
+
PowerShell:
-
+
PS> %[1]s completion powershell | Out-String | Invoke-Expression
-
+
# To load completions for every new session, run:
PS> %[1]s completion powershell > %[1]s.ps1
# and source this file from your PowerShell profile.
@@ -496,7 +551,7 @@ func RegisterCommand(cmd Command) {
if cmd.Name == "" {
panic("command name is required")
}
- if cmd.Func == nil {
+ if cmd.Func == nil && cmd.CobraFunc == nil {
panic("command function missing")
}
if cmd.Short == "" {
diff --git a/cmd/main.go b/cmd/main.go
index 17da8bd..fa15c08 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -17,6 +17,7 @@ package caddycmd
import (
"bufio"
"bytes"
+ "errors"
"flag"
"fmt"
"io"
@@ -30,11 +31,12 @@ import (
"strings"
"time"
- "github.com/caddyserver/caddy/v2"
- "github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/certmagic"
"github.com/spf13/pflag"
"go.uber.org/zap"
+
+ "github.com/caddyserver/caddy/v2"
+ "github.com/caddyserver/caddy/v2/caddyconfig"
)
func init() {
@@ -62,6 +64,10 @@ func Main() {
}
if err := rootCmd.Execute(); err != nil {
+ var exitError *exitError
+ if errors.As(err, &exitError) {
+ os.Exit(exitError.ExitCode)
+ }
os.Exit(1)
}
}
@@ -89,6 +95,10 @@ func handlePingbackConn(conn net.Conn, expect []byte) error {
// 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) {
+ return loadConfigWithLogger(caddy.Log(), configFile, adapterName)
+}
+
+func loadConfigWithLogger(logger *zap.Logger, 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)")
@@ -107,13 +117,14 @@ func LoadConfig(configFile, adapterName string) ([]byte, string, 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))
+ if logger != nil {
+ logger.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
- // plugged in, and if so, try using a default Caddyfile
+ // if the Caddyfile adapter is plugged in, we can try using an
+ // adjacent Caddyfile by default
cfgAdapter = caddyconfig.GetAdapter("caddyfile")
if cfgAdapter != nil {
config, err = os.ReadFile("Caddyfile")
@@ -126,7 +137,9 @@ func LoadConfig(configFile, adapterName string) ([]byte, string, error) {
} else {
// success reading default Caddyfile
configFile = "Caddyfile"
- caddy.Log().Info("using adjacent Caddyfile")
+ if logger != nil {
+ logger.Info("using adjacent Caddyfile")
+ }
}
}
}
@@ -161,7 +174,9 @@ func LoadConfig(configFile, adapterName string) ([]byte, string, error) {
if warn.Directive != "" {
msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message)
}
- caddy.Log().Warn(msg, zap.String("adapter", adapterName), zap.String("file", warn.File), zap.Int("line", warn.Line))
+ if logger != nil {
+ logger.Warn(msg, zap.String("adapter", adapterName), zap.String("file", warn.File), zap.Int("line", warn.Line))
+ }
}
config = adaptedConfig
}
@@ -174,6 +189,8 @@ func LoadConfig(configFile, adapterName string) ([]byte, string, error) {
// blocks indefinitely; it only quits if the poller has errors for
// long enough time. The filename passed in must be the actual
// config file used, not one to be discovered.
+// Each second the config files is loaded and parsed into an object
+// and is compared to the last config object that was loaded
func watchConfigFile(filename, adapterName string) {
defer func() {
if err := recover(); err != nil {
@@ -189,64 +206,36 @@ func watchConfigFile(filename, adapterName string) {
With(zap.String("config_file", filename))
}
- // get the initial timestamp on the config file
- info, err := os.Stat(filename)
+ // get current config
+ lastCfg, _, err := loadConfigWithLogger(nil, filename, adapterName)
if err != nil {
- logger().Error("cannot watch config file", zap.Error(err))
+ logger().Error("unable to load latest config", zap.Error(err))
return
}
- lastModified := info.ModTime()
logger().Info("watching config file for changes")
- // if the file disappears or something, we can
- // stop polling if the error lasts long enough
- var lastErr time.Time
- finalError := func(err error) bool {
- if lastErr.IsZero() {
- lastErr = time.Now()
- return false
- }
- if time.Since(lastErr) > 30*time.Second {
- logger().Error("giving up watching config file; too many errors",
- zap.Error(err))
- return true
- }
- return false
- }
-
// begin poller
//nolint:staticcheck
for range time.Tick(1 * time.Second) {
- // get the file info
- info, err := os.Stat(filename)
+ // get current config
+ newCfg, _, err := loadConfigWithLogger(nil, filename, adapterName)
if err != nil {
- if finalError(err) {
- return
- }
- continue
+ logger().Error("unable to load latest config", zap.Error(err))
+ return
}
- lastErr = time.Time{} // no error, so clear any memory of one
// if it hasn't changed, nothing to do
- if !info.ModTime().After(lastModified) {
+ if bytes.Equal(lastCfg, newCfg) {
continue
}
-
logger().Info("config file changed; reloading")
- // remember this timestamp
- lastModified = info.ModTime()
-
- // load the contents of the file
- config, _, err := LoadConfig(filename, adapterName)
- if err != nil {
- logger().Error("unable to load latest config", zap.Error(err))
- continue
- }
+ // remember the current config
+ lastCfg = newCfg
// apply the updated config
- err = caddy.Load(config, false)
+ err = caddy.Load(lastCfg, false)
if err != nil {
logger().Error("applying latest config", zap.Error(err))
continue
@@ -316,8 +305,12 @@ func loadEnvFromFile(envFile string) error {
}
for k, v := range envMap {
- if err := os.Setenv(k, v); err != nil {
- return fmt.Errorf("setting environment variables: %v", err)
+ // do not overwrite existing environment variables
+ _, exists := os.LookupEnv(k)
+ if !exists {
+ if err := os.Setenv(k, v); err != nil {
+ return fmt.Errorf("setting environment variables: %v", err)
+ }
}
}
@@ -374,18 +367,19 @@ func parseEnvFile(envInput io.Reader) (map[string]string, error) {
}
// quoted value: support newlines
- if strings.HasPrefix(val, `"`) {
- for !(strings.HasSuffix(line, `"`) && !strings.HasSuffix(line, `\"`)) {
- val = strings.ReplaceAll(val, `\"`, `"`)
+ if strings.HasPrefix(val, `"`) || strings.HasPrefix(val, "'") {
+ quote := string(val[0])
+ for !(strings.HasSuffix(line, quote) && !strings.HasSuffix(line, `\`+quote)) {
+ val = strings.ReplaceAll(val, `\`+quote, quote)
if !scanner.Scan() {
break
}
lineNumber++
- line = strings.ReplaceAll(scanner.Text(), `\"`, `"`)
+ line = strings.ReplaceAll(scanner.Text(), `\`+quote, quote)
val += "\n" + line
}
- val = strings.TrimPrefix(val, `"`)
- val = strings.TrimSuffix(val, `"`)
+ val = strings.TrimPrefix(val, quote)
+ val = strings.TrimSuffix(val, quote)
}
envMap[key] = val
diff --git a/cmd/packagesfuncs.go b/cmd/packagesfuncs.go
index 3aed0e8..5d77e4d 100644
--- a/cmd/packagesfuncs.go
+++ b/cmd/packagesfuncs.go
@@ -27,8 +27,9 @@ import (
"runtime/debug"
"strings"
- "github.com/caddyserver/caddy/v2"
"go.uber.org/zap"
+
+ "github.com/caddyserver/caddy/v2"
)
func cmdUpgrade(fl Flags) (int, error) {
diff --git a/cmd/storagefuncs.go b/cmd/storagefuncs.go
new file mode 100644
index 0000000..6a6daec
--- /dev/null
+++ b/cmd/storagefuncs.go
@@ -0,0 +1,221 @@
+// Copyright 2015 Matthew Holt and The Caddy Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package caddycmd
+
+import (
+ "archive/tar"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+
+ "github.com/caddyserver/certmagic"
+
+ "github.com/caddyserver/caddy/v2"
+)
+
+type storVal struct {
+ StorageRaw json.RawMessage `json:"storage,omitempty" caddy:"namespace=caddy.storage inline_key=module"`
+}
+
+// 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)
+ if err != nil {
+ return nil, err
+ }
+
+ // storage defaults to FileStorage if not explicitly
+ // defined in the config, so the config can be valid
+ // json but unmarshaling will fail.
+ if !json.Valid(cfg) {
+ return nil, &json.SyntaxError{}
+ }
+ var tmpStruct storVal
+ err = json.Unmarshal(cfg, &tmpStruct)
+ if err != nil {
+ // default case, ignore the error
+ var jsonError *json.SyntaxError
+ if errors.As(err, &jsonError) {
+ return nil, nil
+ }
+ return nil, err
+ }
+
+ return &tmpStruct, nil
+}
+
+func cmdImportStorage(fl Flags) (int, error) {
+ importStorageCmdConfigFlag := fl.String("config")
+ importStorageCmdImportFile := fl.String("input")
+
+ if importStorageCmdConfigFlag == "" {
+ return caddy.ExitCodeFailedStartup, errors.New("--config is required")
+ }
+ if importStorageCmdImportFile == "" {
+ return caddy.ExitCodeFailedStartup, errors.New("--input is required")
+ }
+
+ // extract storage from config if possible
+ storageCfg, err := determineStorage(importStorageCmdConfigFlag, "")
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, err
+ }
+
+ // load specified storage or fallback to default
+ var stor certmagic.Storage
+ ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
+ defer cancel()
+ if storageCfg != nil && storageCfg.StorageRaw != nil {
+ val, err := ctx.LoadModule(storageCfg, "StorageRaw")
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, err
+ }
+ stor, err = val.(caddy.StorageConverter).CertMagicStorage()
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, err
+ }
+ } else {
+ stor = caddy.DefaultStorage
+ }
+
+ // setup input
+ var f *os.File
+ if importStorageCmdImportFile == "-" {
+ f = os.Stdin
+ } else {
+ f, err = os.Open(importStorageCmdImportFile)
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("opening input file: %v", err)
+ }
+ defer f.Close()
+ }
+
+ // store each archive element
+ tr := tar.NewReader(f)
+ for {
+ hdr, err := tr.Next()
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ return caddy.ExitCodeFailedQuit, fmt.Errorf("reading archive: %v", err)
+ }
+
+ b, err := io.ReadAll(tr)
+ if err != nil {
+ return caddy.ExitCodeFailedQuit, fmt.Errorf("reading archive: %v", err)
+ }
+
+ err = stor.Store(ctx, hdr.Name, b)
+ if err != nil {
+ return caddy.ExitCodeFailedQuit, fmt.Errorf("reading archive: %v", err)
+ }
+ }
+
+ fmt.Println("Successfully imported storage")
+ return caddy.ExitCodeSuccess, nil
+}
+
+func cmdExportStorage(fl Flags) (int, error) {
+ exportStorageCmdConfigFlag := fl.String("config")
+ exportStorageCmdOutputFlag := fl.String("output")
+
+ if exportStorageCmdConfigFlag == "" {
+ return caddy.ExitCodeFailedStartup, errors.New("--config is required")
+ }
+ if exportStorageCmdOutputFlag == "" {
+ return caddy.ExitCodeFailedStartup, errors.New("--output is required")
+ }
+
+ // extract storage from config if possible
+ storageCfg, err := determineStorage(exportStorageCmdConfigFlag, "")
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, err
+ }
+
+ // load specified storage or fallback to default
+ var stor certmagic.Storage
+ ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
+ defer cancel()
+ if storageCfg != nil && storageCfg.StorageRaw != nil {
+ val, err := ctx.LoadModule(storageCfg, "StorageRaw")
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, err
+ }
+ stor, err = val.(caddy.StorageConverter).CertMagicStorage()
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, err
+ }
+ } else {
+ stor = caddy.DefaultStorage
+ }
+
+ // enumerate all keys
+ keys, err := stor.List(ctx, "", true)
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, err
+ }
+
+ // setup output
+ var f *os.File
+ if exportStorageCmdOutputFlag == "-" {
+ f = os.Stdout
+ } else {
+ f, err = os.Create(exportStorageCmdOutputFlag)
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("opening output file: %v", err)
+ }
+ defer f.Close()
+ }
+
+ // `IsTerminal: true` keys hold the values we
+ // care about, write them out
+ tw := tar.NewWriter(f)
+ for _, k := range keys {
+ info, err := stor.Stat(ctx, k)
+ if err != nil {
+ return caddy.ExitCodeFailedQuit, err
+ }
+
+ if info.IsTerminal {
+ v, err := stor.Load(ctx, k)
+ if err != nil {
+ return caddy.ExitCodeFailedQuit, err
+ }
+
+ hdr := &tar.Header{
+ Name: k,
+ Mode: 0o600,
+ Size: int64(len(v)),
+ }
+
+ if err = tw.WriteHeader(hdr); err != nil {
+ return caddy.ExitCodeFailedQuit, fmt.Errorf("writing archive: %v", err)
+ }
+ if _, err = tw.Write(v); err != nil {
+ return caddy.ExitCodeFailedQuit, fmt.Errorf("writing archive: %v", err)
+ }
+ }
+ }
+ if err = tw.Close(); err != nil {
+ return caddy.ExitCodeFailedQuit, fmt.Errorf("writing archive: %v", err)
+ }
+
+ return caddy.ExitCodeSuccess, nil
+}