summaryrefslogtreecommitdiff
path: root/cmd
diff options
context:
space:
mode:
authoraca <acadx0@gmail.com>2019-10-01 12:23:58 +0900
committerMatt Holt <mholt@users.noreply.github.com>2019-09-30 21:23:58 -0600
commit0006df60264e7fdc55fc1a0962109e8198fa9d71 (patch)
treec85dc2eead63987288285d1426c097f820088a73 /cmd
parentc95db3551d79112513196b71841de8d010a42b25 (diff)
cmd: Refactor subcommands, add help, make them pluggable
* cli: Change command structure, add help subcommand (#328) * cli: improve subcommand structure - make help command as normal subcommand - add flag usage message for each command * cmd: Refactor subcommands and command line help; make commands pluggable
Diffstat (limited to 'cmd')
-rw-r--r--cmd/commandfuncs.go391
-rw-r--r--cmd/commands.go497
-rw-r--r--cmd/main.go130
3 files changed, 671 insertions, 347 deletions
diff --git a/cmd/commandfuncs.go b/cmd/commandfuncs.go
new file mode 100644
index 0000000..be9da93
--- /dev/null
+++ b/cmd/commandfuncs.go
@@ -0,0 +1,391 @@
+// 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 (
+ "bytes"
+ "crypto/rand"
+ "encoding/json"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "log"
+ "net"
+ "net/http"
+ "os"
+ "os/exec"
+ "strings"
+
+ "github.com/caddyserver/caddy/v2"
+ "github.com/caddyserver/caddy/v2/caddyconfig"
+ "github.com/keybase/go-ps"
+ "github.com/mholt/certmagic"
+)
+
+func cmdStart(fl Flags) (int, error) {
+ startCmdConfigFlag := fl.String("config")
+ startCmdConfigAdapterFlag := fl.String("config-adapter")
+
+ // open a listener to which the child process will connect when
+ // it is ready to confirm that it has successfully started
+ ln, err := net.Listen("tcp", "127.0.0.1:0")
+ if err != nil {
+ return caddy.ExitCodeFailedStartup,
+ fmt.Errorf("opening listener for success confirmation: %v", err)
+ }
+ defer ln.Close()
+
+ // craft the command with a pingback address and with a
+ // pipe for its stdin, so we can tell it our confirmation
+ // code that we expect so that some random port scan at
+ // the most unfortunate time won't fool us into thinking
+ // the child succeeded (i.e. the alternative is to just
+ // wait for any connection on our listener, but better to
+ // ensure it's the process we're expecting - we can be
+ // 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 startCmdConfigAdapterFlag != "" {
+ cmd.Args = append(cmd.Args, "--config-adapter", startCmdConfigAdapterFlag)
+ }
+ stdinpipe, err := cmd.StdinPipe()
+ if err != nil {
+ return caddy.ExitCodeFailedStartup,
+ fmt.Errorf("creating stdin pipe: %v", err)
+ }
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+
+ // generate the random bytes we'll send to the child process
+ expect := make([]byte, 32)
+ _, err = rand.Read(expect)
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("generating random confirmation bytes: %v", err)
+ }
+
+ // begin writing the confirmation bytes to the child's
+ // stdin; use a goroutine since the child hasn't been
+ // started yet, and writing sychronously would result
+ // in a deadlock
+ go func() {
+ stdinpipe.Write(expect)
+ stdinpipe.Close()
+ }()
+
+ // start the process
+ err = cmd.Start()
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("starting caddy process: %v", err)
+ }
+
+ // there are two ways we know we're done: either
+ // the process will connect to our listener, or
+ // it will exit with an error
+ success, exit := make(chan struct{}), make(chan error)
+
+ // in one goroutine, we await the success of the child process
+ go func() {
+ for {
+ conn, err := ln.Accept()
+ if err != nil {
+ if !strings.Contains(err.Error(), "use of closed network connection") {
+ log.Println(err)
+ }
+ break
+ }
+ err = handlePingbackConn(conn, expect)
+ if err == nil {
+ close(success)
+ break
+ }
+ log.Println(err)
+ }
+ }()
+
+ // in another goroutine, we await the failure of the child process
+ go func() {
+ err := cmd.Wait() // don't send on this line! Wait blocks, but send starts before it unblocks
+ exit <- err // sending on separate line ensures select won't trigger until after Wait unblocks
+ }()
+
+ // when one of the goroutines unblocks, we're done and can exit
+ select {
+ case <-success:
+ fmt.Printf("Successfully started Caddy (pid=%d)\n", cmd.Process.Pid)
+ case err := <-exit:
+ return caddy.ExitCodeFailedStartup,
+ fmt.Errorf("caddy process exited with error: %v", err)
+ }
+
+ return caddy.ExitCodeSuccess, nil
+}
+
+func cmdRun(fl Flags) (int, error) {
+ runCmdConfigFlag := fl.String("config")
+ runCmdConfigAdapterFlag := fl.String("config-adapter")
+ runCmdPrintEnvFlag := fl.Bool("print-env")
+ runCmdPingbackFlag := fl.String("pingback")
+
+ // if we are supposed to print the environment, do that first
+ if runCmdPrintEnvFlag {
+ printEnvironment()
+ }
+
+ // get the config in caddy's native format
+ 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")
+ certmagic.UserAgent = "Caddy/" + cleanModVersion
+
+ // start the admin endpoint along with any initial config
+ err = caddy.StartAdmin(config)
+ if err != nil {
+ return caddy.ExitCodeFailedStartup,
+ fmt.Errorf("starting caddy administration endpoint: %v", err)
+ }
+ defer caddy.StopAdmin()
+
+ // 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 != "" {
+ confirmationBytes, err := ioutil.ReadAll(os.Stdin)
+ if err != nil {
+ return caddy.ExitCodeFailedStartup,
+ fmt.Errorf("reading confirmation bytes from stdin: %v", err)
+ }
+ conn, err := net.Dial("tcp", runCmdPingbackFlag)
+ if err != nil {
+ return caddy.ExitCodeFailedStartup,
+ fmt.Errorf("dialing confirmation address: %v", err)
+ }
+ defer conn.Close()
+ _, err = conn.Write(confirmationBytes)
+ if err != nil {
+ return caddy.ExitCodeFailedStartup,
+ fmt.Errorf("writing confirmation bytes to %s: %v", runCmdPingbackFlag, err)
+ }
+ }
+
+ select {}
+}
+
+func cmdStop(_ Flags) (int, error) {
+ processList, err := ps.Processes()
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("listing processes: %v", err)
+ }
+ thisProcName := getProcessName()
+ var found bool
+ for _, p := range processList {
+ // the process we're looking for should have the same name but different PID
+ if p.Executable() == thisProcName && p.Pid() != os.Getpid() {
+ found = true
+ fmt.Printf("pid=%d\n", p.Pid())
+
+ if err := gracefullyStopProcess(p.Pid()); err != nil {
+ return caddy.ExitCodeFailedStartup, err
+ }
+ }
+ }
+ if !found {
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("Caddy is not running")
+ }
+ fmt.Println(" success")
+ return caddy.ExitCodeSuccess, nil
+}
+
+func cmdReload(fl Flags) (int, error) {
+ reloadCmdConfigFlag := fl.String("config")
+ reloadCmdConfigAdapterFlag := fl.String("config-adapter")
+ reloadCmdAddrFlag := fl.String("address")
+
+ // a configuration is required
+ if reloadCmdConfigFlag == "" {
+ return caddy.ExitCodeFailedStartup,
+ fmt.Errorf("no configuration to load (use --config)")
+ }
+
+ // get the config in caddy's native format
+ config, err := loadConfig(reloadCmdConfigFlag, reloadCmdConfigAdapterFlag)
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, err
+ }
+
+ // get the address of the admin listener and craft endpoint URL
+ adminAddr := reloadCmdAddrFlag
+ if adminAddr == "" {
+ 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
+ }
+ if adminAddr == "" {
+ adminAddr = caddy.DefaultAdminListen
+ }
+ adminEndpoint := fmt.Sprintf("http://%s/load", adminAddr)
+
+ // send the configuration to the instance
+ resp, err := http.Post(adminEndpoint, "application/json", bytes.NewReader(config))
+ if err != nil {
+ return caddy.ExitCodeFailedStartup,
+ fmt.Errorf("sending configuration to instance: %v", err)
+ }
+ defer resp.Body.Close()
+
+ // if it didn't work, let the user know
+ if resp.StatusCode >= 400 {
+ respBody, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1024*10))
+ if err != nil {
+ return caddy.ExitCodeFailedStartup,
+ fmt.Errorf("HTTP %d: reading error message: %v", resp.StatusCode, err)
+ }
+ return caddy.ExitCodeFailedStartup,
+ fmt.Errorf("caddy responded with error: HTTP %d: %s", resp.StatusCode, respBody)
+ }
+
+ return caddy.ExitCodeSuccess, nil
+}
+
+func cmdVersion(_ Flags) (int, error) {
+ goModule := caddy.GoModule()
+ if goModule.Sum != "" {
+ // a build with a known version will also have a checksum
+ fmt.Printf("%s %s\n", goModule.Version, goModule.Sum)
+ } else {
+ fmt.Println(goModule.Version)
+ }
+ return caddy.ExitCodeSuccess, nil
+}
+
+func cmdListModules(_ Flags) (int, error) {
+ for _, m := range caddy.Modules() {
+ fmt.Println(m)
+ }
+ return caddy.ExitCodeSuccess, nil
+}
+
+func cmdEnviron(_ Flags) (int, error) {
+ printEnvironment()
+ return caddy.ExitCodeSuccess, nil
+}
+
+func cmdAdaptConfig(fl Flags) (int, error) {
+ adaptCmdAdapterFlag := fl.String("adapter")
+ adaptCmdInputFlag := fl.String("input")
+ adaptCmdPrettyFlag := fl.Bool("pretty")
+
+ if adaptCmdAdapterFlag == "" || adaptCmdInputFlag == "" {
+ return caddy.ExitCodeFailedStartup,
+ fmt.Errorf("usage: caddy adapt-config --adapter <name> --input <file>")
+ }
+
+ cfgAdapter := caddyconfig.GetAdapter(adaptCmdAdapterFlag)
+ if cfgAdapter == nil {
+ return caddy.ExitCodeFailedStartup,
+ fmt.Errorf("unrecognized config adapter: %s", adaptCmdAdapterFlag)
+ }
+
+ input, err := ioutil.ReadFile(adaptCmdInputFlag)
+ if err != nil {
+ return caddy.ExitCodeFailedStartup,
+ fmt.Errorf("reading input file: %v", err)
+ }
+
+ opts := make(map[string]interface{})
+ if adaptCmdPrettyFlag {
+ opts["pretty"] = "true"
+ }
+
+ adaptedConfig, warnings, err := cfgAdapter.Adapt(input, opts)
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, err
+ }
+
+ // print warnings to stderr
+ for _, warn := range warnings {
+ msg := warn.Message
+ if warn.Directive != "" {
+ msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message)
+ }
+ log.Printf("[WARNING][%s] %s:%d: %s", adaptCmdAdapterFlag, warn.File, warn.Line, msg)
+ }
+
+ // print result to stdout
+ fmt.Println(string(adaptedConfig))
+
+ return caddy.ExitCodeSuccess, nil
+}
+
+func cmdHelp(fl Flags) (int, error) {
+ const fullDocs = `Full documentation is available at:
+https://github.com/caddyserver/caddy/wiki/v2:-Documentation`
+
+ args := fl.Args()
+ if len(args) == 0 {
+ s := `Caddy is an extensible server platform.
+
+usage:
+ caddy <command> [<args...>]
+
+commands:
+`
+ for _, cmd := range commands {
+ s += fmt.Sprintf(" %-15s %s\n", cmd.Name, cmd.Short)
+ }
+
+ s += "\nUse 'caddy help <command>' for more information about a command.\n"
+ s += "\n" + fullDocs + "\n"
+
+ fmt.Print(s)
+
+ return caddy.ExitCodeSuccess, nil
+ } else if len(args) > 1 {
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("can only give help with one command")
+ }
+
+ subcommand, ok := commands[args[0]]
+ if !ok {
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("unknown command: %s", args[0])
+ }
+
+ result := fmt.Sprintf("%s\n\nusage:\n caddy %s %s\n",
+ strings.TrimSpace(subcommand.Long),
+ subcommand.Name,
+ strings.TrimSpace(subcommand.Usage),
+ )
+
+ if help := flagHelp(subcommand.Flags); help != "" {
+ result += fmt.Sprintf("\nflags:\n%s", help)
+ }
+
+ result += "\n" + fullDocs + "\n"
+
+ fmt.Print(result)
+
+ return caddy.ExitCodeSuccess, nil
+}
diff --git a/cmd/commands.go b/cmd/commands.go
index 2526e45..8a9d482 100644
--- a/cmd/commands.go
+++ b/cmd/commands.go
@@ -15,343 +15,204 @@
package caddycmd
import (
- "bytes"
- "crypto/rand"
- "encoding/json"
"flag"
- "fmt"
- "io"
- "io/ioutil"
- "log"
- "net"
- "net/http"
- "os"
- "os/exec"
-
- "strings"
-
- "github.com/caddyserver/caddy/v2"
- "github.com/caddyserver/caddy/v2/caddyconfig"
- "github.com/keybase/go-ps"
- "github.com/mholt/certmagic"
+ "regexp"
)
-func cmdStart() (int, error) {
- startCmd := flag.NewFlagSet("start", flag.ExitOnError)
- startCmdConfigFlag := startCmd.String("config", "", "Configuration file")
- startCmdConfigAdapterFlag := startCmd.String("config-adapter", "", "Name of config adapter to apply")
- startCmd.Parse(os.Args[2:])
+// Command represents a subcommand. All fields
+// are required to be set except for Flags if
+// there are no flags and Usage if there are
+// no flags or arguments.
+type Command struct {
+ Name string
- // open a listener to which the child process will connect when
- // it is ready to confirm that it has successfully started
- ln, err := net.Listen("tcp", "127.0.0.1:0")
- if err != nil {
- return caddy.ExitCodeFailedStartup,
- fmt.Errorf("opening listener for success confirmation: %v", err)
- }
- defer ln.Close()
+ // Run is a function that executes a subcommand.
+ // It returns an exit code and any associated error.
+ // Takes non-flag commandline arguments as args.
+ // Flag must be parsed before Run is executed.
+ Func CommandFunc
- // craft the command with a pingback address and with a
- // pipe for its stdin, so we can tell it our confirmation
- // code that we expect so that some random port scan at
- // the most unfortunate time won't fool us into thinking
- // the child succeeded (i.e. the alternative is to just
- // wait for any connection on our listener, but better to
- // ensure it's the process we're expecting - we can be
- // 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 *startCmdConfigAdapterFlag != "" {
- cmd.Args = append(cmd.Args, "--config-adapter", *startCmdConfigAdapterFlag)
- }
- stdinpipe, err := cmd.StdinPipe()
- if err != nil {
- return caddy.ExitCodeFailedStartup,
- fmt.Errorf("creating stdin pipe: %v", err)
- }
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
+ // Usage is the one-line message explaining args, flags.
+ Usage string
- // generate the random bytes we'll send to the child process
- expect := make([]byte, 32)
- _, err = rand.Read(expect)
- if err != nil {
- return caddy.ExitCodeFailedStartup, fmt.Errorf("generating random confirmation bytes: %v", err)
- }
+ // Short is the short description for command.
+ Short string
- // begin writing the confirmation bytes to the child's
- // stdin; use a goroutine since the child hasn't been
- // started yet, and writing sychronously would result
- // in a deadlock
- go func() {
- stdinpipe.Write(expect)
- stdinpipe.Close()
- }()
+ // Long is the message for 'caddy help <command>'
+ Long string
- // start the process
- err = cmd.Start()
- if err != nil {
- return caddy.ExitCodeFailedStartup, fmt.Errorf("starting caddy process: %v", err)
- }
-
- // there are two ways we know we're done: either
- // the process will connect to our listener, or
- // it will exit with an error
- success, exit := make(chan struct{}), make(chan error)
-
- // in one goroutine, we await the success of the child process
- go func() {
- for {
- conn, err := ln.Accept()
- if err != nil {
- if !strings.Contains(err.Error(), "use of closed network connection") {
- log.Println(err)
- }
- break
- }
- err = handlePingbackConn(conn, expect)
- if err == nil {
- close(success)
- break
- }
- log.Println(err)
- }
- }()
-
- // in another goroutine, we await the failure of the child process
- go func() {
- err := cmd.Wait() // don't send on this line! Wait blocks, but send starts before it unblocks
- exit <- err // sending on separate line ensures select won't trigger until after Wait unblocks
- }()
-
- // when one of the goroutines unblocks, we're done and can exit
- select {
- case <-success:
- fmt.Printf("Successfully started Caddy (pid=%d)\n", cmd.Process.Pid)
- case err := <-exit:
- return caddy.ExitCodeFailedStartup,
- fmt.Errorf("caddy process exited with error: %v", err)
- }
-
- return caddy.ExitCodeSuccess, nil
+ // Flags is flagset for command.
+ Flags *flag.FlagSet
}
-func cmdRun() (int, error) {
- runCmd := flag.NewFlagSet("run", flag.ExitOnError)
- runCmdConfigFlag := runCmd.String("config", "", "Configuration file")
- runCmdConfigAdapterFlag := runCmd.String("config-adapter", "", "Name of config adapter to apply")
- runCmdPrintEnvFlag := runCmd.Bool("print-env", false, "Print environment")
- runCmdPingbackFlag := runCmd.String("pingback", "", "Echo confirmation bytes to this address on success")
- runCmd.Parse(os.Args[2:])
-
- // if we are supposed to print the environment, do that first
- if *runCmdPrintEnvFlag {
- exitCode, err := cmdEnviron()
- if err != nil {
- return exitCode, err
- }
- }
-
- // get the config in caddy's native format
- 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")
- certmagic.UserAgent = "Caddy/" + cleanModVersion
-
- // start the admin endpoint along with any initial config
- err = caddy.StartAdmin(config)
- if err != nil {
- return caddy.ExitCodeFailedStartup,
- fmt.Errorf("starting caddy administration endpoint: %v", err)
- }
- defer caddy.StopAdmin()
-
- // 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 != "" {
- confirmationBytes, err := ioutil.ReadAll(os.Stdin)
- if err != nil {
- return caddy.ExitCodeFailedStartup,
- fmt.Errorf("reading confirmation bytes from stdin: %v", err)
- }
- conn, err := net.Dial("tcp", *runCmdPingbackFlag)
- if err != nil {
- return caddy.ExitCodeFailedStartup,
- fmt.Errorf("dialing confirmation address: %v", err)
- }
- defer conn.Close()
- _, err = conn.Write(confirmationBytes)
- if err != nil {
- return caddy.ExitCodeFailedStartup,
- fmt.Errorf("writing confirmation bytes to %s: %v", *runCmdPingbackFlag, err)
- }
- }
-
- select {}
+// CommandFunc is a command's function. It runs the
+// command and returns the proper exit code along with
+// any error that occurred.
+type CommandFunc func(Flags) (int, error)
+
+var commands = map[string]Command{
+ "start": {
+ Name: "start",
+ Func: cmdStart,
+ Usage: "[--config <path>] [--config-adapter <name>]",
+ Short: "Starts the Caddy process and returns after server has started.",
+ Long: `
+Starts the Caddy process, optionally bootstrapped with an initial
+config file. Blocks until server is successfully running (or fails to run),
+then returns. On Windows, the child process will remain attached to the
+terminal, so closing the window will forcefully stop Caddy. See run for more
+details.`,
+ Flags: func() *flag.FlagSet {
+ fs := flag.NewFlagSet("start", flag.ExitOnError)
+ fs.String("config", "", "Configuration file")
+ fs.String("config-adapter", "", "Name of config adapter to apply")
+ return fs
+ }(),
+ },
+
+ "run": {
+ Name: "run",
+ Func: cmdRun,
+ Usage: "[--config <path>] [--config-adapter <name>] [--print-env]",
+ Short: `Starts the Caddy process and blocks indefinitely.`,
+ Long: `
+Same as start, but blocks indefinitely; i.e. runs Caddy in "daemon" mode. On
+Windows, this is recommended over caddy start when running Caddy manually since
+it will be more obvious that Caddy is still running and bound to the terminal
+window.
+
+If a config file is specified, it will be applied immediately after the process
+is running. If the config file is not in Caddy's native JSON format, you can
+specify an adapter with --config-adapter to adapt the given config file to
+Caddy's native format. The config adapter must be a registered module. Any
+warnings will be printed to the log, but beware that any adaptation without
+errors will immediately be used. If you want to review the results of the
+adaptation first, use adapt-config.
+
+As a special case, if the current working directory has a file called
+"Caddyfile" and the caddyfile config adapter is plugged in (default), then that
+file will be loaded and used to configure Caddy, even without any command line
+flags.
+
+If --print-env 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.`,
+ Flags: func() *flag.FlagSet {
+ fs := flag.NewFlagSet("run", flag.ExitOnError)
+ fs.String("config", "", "Configuration file")
+ fs.String("config-adapter", "", "Name of config adapter to apply")
+ fs.Bool("print-env", false, "Print environment")
+ fs.String("pingback", "", "Echo confirmation bytes to this address on success")
+ return fs
+ }(),
+ },
+
+ "stop": {
+ Name: "stop",
+ Func: cmdStop,
+ Short: "Gracefully stops the running Caddy process",
+ Long: `Gracefully stops the running Caddy process. (Note: this will stop any process
+named the same as the executable.) On Windows, this stop is forceful and Caddy
+will not have an opportunity to clean up any active locks; for a graceful
+shutdown on Windows, use Ctrl+C or the /stop endpoint.`,
+ },
+
+ "reload": {
+ Name: "reload",
+ Func: cmdReload,
+ Usage: "--config <path> [--config-adapter <name>] [--address <interface>]",
+ Short: "Gives the running Caddy instance a new configuration",
+ Long: `Gives the running Caddy instance a new configuration. This has the same effect
+as POSTing a document to the /load endpoint, but is convenient for simple
+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("load", flag.ExitOnError)
+ fs.String("config", "", "Configuration file")
+ fs.String("config-adapter", "", "Name of config adapter to apply")
+ fs.String("address", "", "Address of the administration listener, if different from config")
+ return fs
+ }(),
+ },
+
+ "version": {
+ Name: "version",
+ Func: cmdVersion,
+ Short: "Prints the version.",
+ Long: `Prints the version.`,
+ },
+
+ "list-modules": {
+ Name: "list-modules",
+ Func: cmdListModules,
+ Short: "List installed Caddy modules.",
+ Long: `List installed Caddy modules.`,
+ },
+
+ "environ": {
+ Name: "environ",
+ Func: cmdEnviron,
+ Short: "Prints the environment as seen by Caddy.",
+ Long: `Prints the environment as seen by Caddy.`,
+ },
+
+ "adapt-config": {
+ Name: "adapt-config",
+ Func: cmdAdaptConfig,
+ Usage: "--input <path> --adapter <name> [--pretty]",
+ Short: "Adapts a configuration to Caddy's native JSON config structure",
+ Long: `
+Adapts a configuration to Caddy's native JSON config structure and writes the
+output to stdout, along with any warnings to stderr. If --pretty is specified,
+the output will be formatted with indentation for human readability.`,
+ },
}
-func cmdStop() (int, error) {
- processList, err := ps.Processes()
- if err != nil {
- return caddy.ExitCodeFailedStartup, fmt.Errorf("listing processes: %v", err)
- }
- thisProcName := getProcessName()
- var found bool
- for _, p := range processList {
- // the process we're looking for should have the same name but different PID
- if p.Executable() == thisProcName && p.Pid() != os.Getpid() {
- found = true
- fmt.Printf("pid=%d\n", p.Pid())
-
- if err := gracefullyStopProcess(p.Pid()); err != nil {
- return caddy.ExitCodeFailedStartup, err
- }
- }
- }
- if !found {
- return caddy.ExitCodeFailedStartup, fmt.Errorf("Caddy is not running")
+func init() {
+ // the help command is special in that its func
+ // refers to the commands map; thus, defining it
+ // inline with the commands map's initialization
+ // yields a compile-time error, so we have to
+ // define this command separately
+ commands["help"] = Command{
+ Name: "help",
+ Func: cmdHelp,
+ Usage: "<command>",
+ Short: "Shows help for a Caddy subcommand.",
}
- fmt.Println(" success")
- return caddy.ExitCodeSuccess, nil
}
-func cmdReload() (int, error) {
- reloadCmd := flag.NewFlagSet("load", flag.ExitOnError)
- reloadCmdConfigFlag := reloadCmd.String("config", "", "Configuration file")
- reloadCmdConfigAdapterFlag := reloadCmd.String("config-adapter", "", "Name of config adapter to apply")
- reloadCmdAddrFlag := reloadCmd.String("address", "", "Address of the administration listener, if different from config")
- reloadCmd.Parse(os.Args[2:])
-
- // a configuration is required
- if *reloadCmdConfigFlag == "" {
- return caddy.ExitCodeFailedStartup,
- fmt.Errorf("no configuration to load (use --config)")
- }
-
- // get the config in caddy's native format
- config, err := loadConfig(*reloadCmdConfigFlag, *reloadCmdConfigAdapterFlag)
- if err != nil {
- return caddy.ExitCodeFailedStartup, err
- }
-
- // get the address of the admin listener and craft endpoint URL
- adminAddr := *reloadCmdAddrFlag
- if adminAddr == "" {
- 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
+// RegisterCommand registers the command cmd.
+// cmd.Name must be unique and conform to the
+// following format:
+//
+// - lowercase
+// - alphanumeric and hyphen characters only
+// - cannot start or end with a hyphen
+// - hyphen cannot be adjacent to another hyphen
+//
+// This function panics if the name is already registered,
+// if the name does not meet the described format, or if
+// any of the fields are missing from cmd.
+func RegisterCommand(cmd Command) {
+ if cmd.Name == "" {
+ panic("command name is required")
}
- if adminAddr == "" {
- adminAddr = caddy.DefaultAdminListen
+ if cmd.Func == nil {
+ panic("command function missing")
}
- adminEndpoint := fmt.Sprintf("http://%s/load", adminAddr)
-
- // send the configuration to the instance
- resp, err := http.Post(adminEndpoint, "application/json", bytes.NewReader(config))
- if err != nil {
- return caddy.ExitCodeFailedStartup,
- fmt.Errorf("sending configuration to instance: %v", err)
+ if cmd.Short == "" {
+ panic("command short string is required")
}
- defer resp.Body.Close()
-
- // if it didn't work, let the user know
- if resp.StatusCode >= 400 {
- respBody, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1024*10))
- if err != nil {
- return caddy.ExitCodeFailedStartup,
- fmt.Errorf("HTTP %d: reading error message: %v", resp.StatusCode, err)
- }
- return caddy.ExitCodeFailedStartup,
- fmt.Errorf("caddy responded with error: HTTP %d: %s", resp.StatusCode, respBody)
+ if _, exists := commands[cmd.Name]; exists {
+ panic("command already registered: " + cmd.Name)
}
-
- return caddy.ExitCodeSuccess, nil
-}
-
-func cmdVersion() (int, error) {
- goModule := caddy.GoModule()
- if goModule.Sum != "" {
- // a build with a known version will also have a checksum
- fmt.Printf("%s %s\n", goModule.Version, goModule.Sum)
- } else {
- fmt.Println(goModule.Version)
- }
- return caddy.ExitCodeSuccess, nil
-}
-
-func cmdListModules() (int, error) {
- for _, m := range caddy.Modules() {
- fmt.Println(m)
- }
- return caddy.ExitCodeSuccess, nil
-}
-
-func cmdEnviron() (int, error) {
- for _, v := range os.Environ() {
- fmt.Println(v)
+ if !commandNameRegex.MatchString(cmd.Name) {
+ panic("invalid command name")
}
- return caddy.ExitCodeSuccess, nil
+ commands[cmd.Name] = cmd
}
-func cmdAdaptConfig() (int, error) {
- adaptCmd := flag.NewFlagSet("adapt", flag.ExitOnError)
- adaptCmdAdapterFlag := adaptCmd.String("adapter", "", "Name of config adapter")
- adaptCmdInputFlag := adaptCmd.String("input", "", "Configuration file to adapt")
- adaptCmdPrettyFlag := adaptCmd.Bool("pretty", false, "Format the output for human readability")
- adaptCmd.Parse(os.Args[2:])
-
- if *adaptCmdAdapterFlag == "" || *adaptCmdInputFlag == "" {
- return caddy.ExitCodeFailedStartup,
- fmt.Errorf("usage: caddy adapt-config --adapter <name> --input <file>")
- }
-
- cfgAdapter := caddyconfig.GetAdapter(*adaptCmdAdapterFlag)
- if cfgAdapter == nil {
- return caddy.ExitCodeFailedStartup,
- fmt.Errorf("unrecognized config adapter: %s", *adaptCmdAdapterFlag)
- }
-
- input, err := ioutil.ReadFile(*adaptCmdInputFlag)
- if err != nil {
- return caddy.ExitCodeFailedStartup,
- fmt.Errorf("reading input file: %v", err)
- }
-
- opts := make(map[string]interface{})
- if *adaptCmdPrettyFlag {
- opts["pretty"] = "true"
- }
-
- adaptedConfig, warnings, err := cfgAdapter.Adapt(input, opts)
- if err != nil {
- return caddy.ExitCodeFailedStartup, err
- }
-
- // print warnings to stderr
- for _, warn := range warnings {
- msg := warn.Message
- if warn.Directive != "" {
- msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message)
- }
- log.Printf("[WARNING][%s] %s:%d: %s", *adaptCmdAdapterFlag, warn.File, warn.Line, msg)
- }
-
- // print result to stdout
- fmt.Println(string(adaptedConfig))
-
- return caddy.ExitCodeSuccess, nil
-}
+var commandNameRegex = regexp.MustCompile(`^[a-z0-9]$|^([a-z0-9]+-?[a-z0-9]*)+[a-z0-9]$`)
diff --git a/cmd/main.go b/cmd/main.go
index 5b97992..aea020f 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -23,6 +23,9 @@ import (
"log"
"net"
"os"
+ "strconv"
+ "strings"
+ "time"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
@@ -33,45 +36,43 @@ import (
func Main() {
caddy.TrapSignals()
- if len(os.Args) < 2 {
- fmt.Println(usageString())
- return
+ switch len(os.Args) {
+ case 0:
+ log.Printf("[FATAL] no arguments provided by OS; args[0] must be command")
+ os.Exit(caddy.ExitCodeFailedStartup)
+ case 1:
+ os.Args = append(os.Args, "help")
}
- subcommand, ok := commands[os.Args[1]]
+ subcommandName := os.Args[1]
+ subcommand, ok := commands[subcommandName]
if !ok {
- fmt.Printf("%q is not a valid command\n", os.Args[1])
+ if strings.HasPrefix(os.Args[1], "-") {
+ // user probably forgot to type the subcommand
+ log.Println("[ERROR] first argument must be a subcommand; see 'caddy help'")
+ } else {
+ log.Printf("[ERROR] '%s' is not a recognized subcommand; see 'caddy help'", os.Args[1])
+ }
os.Exit(caddy.ExitCodeFailedStartup)
}
- if exitCode, err := subcommand(); err != nil {
+ fs := subcommand.Flags
+ if fs == nil {
+ fs = flag.NewFlagSet(subcommand.Name, flag.ExitOnError)
+ }
+
+ err := fs.Parse(os.Args[2:])
+ if err != nil {
log.Println(err)
- os.Exit(exitCode)
+ os.Exit(caddy.ExitCodeFailedStartup)
}
-}
-// commandFunc is a function that executes
-// a subcommand. It returns an exit code and
-// any associated error.
-type commandFunc func() (int, error)
-
-var commands = map[string]commandFunc{
- "start": cmdStart,
- "run": cmdRun,
- "stop": cmdStop,
- "reload": cmdReload,
- "version": cmdVersion,
- "list-modules": cmdListModules,
- "environ": cmdEnviron,
- "adapt-config": cmdAdaptConfig,
-}
+ exitCode, err := subcommand.Func(Flags{fs})
+ if err != nil {
+ log.Printf("%s: %v", subcommand.Name, err)
+ }
-func usageString() string {
- buf := new(bytes.Buffer)
- buf.WriteString("usage: caddy <command> [<args>]")
- flag.CommandLine.SetOutput(buf)
- flag.CommandLine.PrintDefaults()
- return buf.String()
+ os.Exit(exitCode)
}
// handlePingbackConn reads from conn and ensures it matches
@@ -156,3 +157,74 @@ func loadConfig(configFile, adapterName string) ([]byte, error) {
return config, nil
}
+
+// Flags wraps a FlagSet so that typed values
+// from flags can be easily retrieved.
+type Flags struct {
+ *flag.FlagSet
+}
+
+// String returns the string representation of the
+// flag given by name. It panics if the flag is not
+// in the flag set.
+func (f Flags) String(name string) string {
+ return f.FlagSet.Lookup(name).Value.String()
+}
+
+// Bool returns the boolean representation of the
+// flag given by name. It returns false if the flag
+// is not a boolean type. It panics if the flag is
+// not in the flag set.
+func (f Flags) Bool(name string) bool {
+ val, _ := strconv.ParseBool(f.String(name))
+ return val
+}
+
+// Int returns the integer representation of the
+// flag given by name. It returns 0 if the flag
+// is not an integer type. It panics if the flag is
+// not in the flag set.
+func (f Flags) Int(name string) int {
+ val, _ := strconv.ParseInt(f.String(name), 0, strconv.IntSize)
+ return int(val)
+}
+
+// Float64 returns the float64 representation of the
+// flag given by name. It returns false if the flag
+// is not a float63 type. It panics if the flag is
+// not in the flag set.
+func (f Flags) Float64(name string) float64 {
+ val, _ := strconv.ParseFloat(f.String(name), 64)
+ return val
+}
+
+// Duration returns the duration representation of the
+// flag given by name. It returns false if the flag
+// is not a duration type. It panics if the flag is
+// not in the flag set.
+func (f Flags) Duration(name string) time.Duration {
+ val, _ := time.ParseDuration(f.String(name))
+ return val
+}
+
+// flagHelp returns the help text for fs.
+func flagHelp(fs *flag.FlagSet) string {
+ if fs == nil {
+ return ""
+ }
+
+ // temporarily redirect output
+ out := fs.Output()
+ defer fs.SetOutput(out)
+
+ buf := new(bytes.Buffer)
+ fs.SetOutput(buf)
+ fs.PrintDefaults()
+ return buf.String()
+}
+
+func printEnvironment() {
+ for _, v := range os.Environ() {
+ fmt.Println(v)
+ }
+}