From 2c3657bb8a37ce9c644dc1889223e37ca9988148 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Tue, 1 Oct 2019 11:02:13 -0600 Subject: [PATCH] cmd: CLI improvements; add --validate to adapt command --- cmd/commandfuncs.go | 29 ++++++++- cmd/commands.go | 139 ++++++++++++++++++++++++++------------------ 2 files changed, 107 insertions(+), 61 deletions(-) diff --git a/cmd/commandfuncs.go b/cmd/commandfuncs.go index 715f064d..5fba3687 100644 --- a/cmd/commandfuncs.go +++ b/cmd/commandfuncs.go @@ -351,10 +351,11 @@ func cmdAdaptConfig(fl Flags) (int, error) { adaptCmdInputFlag := fl.String("config") adaptCmdAdapterFlag := fl.String("adapter") adaptCmdPrettyFlag := fl.Bool("pretty") + adaptCmdValidateFlag := fl.Bool("validate") if adaptCmdAdapterFlag == "" || adaptCmdInputFlag == "" { return caddy.ExitCodeFailedStartup, - fmt.Errorf("usage: caddy adapt --adapter --input ") + fmt.Errorf("--adapter and --config flags are required") } cfgAdapter := caddyconfig.GetAdapter(adaptCmdAdapterFlag) @@ -391,6 +392,19 @@ func cmdAdaptConfig(fl Flags) (int, error) { // print result to stdout fmt.Println(string(adaptedConfig)) + // validate output if requested + if adaptCmdValidateFlag { + var cfg *caddy.Config + err = json.Unmarshal(adaptedConfig, &cfg) + if err != nil { + return caddy.ExitCodeFailedStartup, fmt.Errorf("decoding config: %v", err) + } + err = caddy.Validate(cfg) + if err != nil { + return caddy.ExitCodeFailedStartup, fmt.Errorf("validation: %v", err) + } + } + return caddy.ExitCodeSuccess, nil } @@ -457,7 +471,8 @@ usage: commands: ` for _, cmd := range commands { - s += fmt.Sprintf(" %-15s %s\n", cmd.Name, cmd.Short) + short := strings.TrimSuffix(cmd.Short, ".") + s += fmt.Sprintf(" %-15s %s\n", cmd.Name, short) } s += "\nUse 'caddy help ' for more information about a command.\n" @@ -475,8 +490,16 @@ commands: return caddy.ExitCodeFailedStartup, fmt.Errorf("unknown command: %s", args[0]) } + helpText := strings.TrimSpace(subcommand.Long) + if helpText == "" { + helpText = subcommand.Short + if !strings.HasSuffix(helpText, ".") { + helpText += "." + } + } + result := fmt.Sprintf("%s\n\nusage:\n caddy %s %s\n", - strings.TrimSpace(subcommand.Long), + helpText, subcommand.Name, strings.TrimSpace(subcommand.Usage), ) diff --git a/cmd/commands.go b/cmd/commands.go index 731e8139..7473053a 100644 --- a/cmd/commands.go +++ b/cmd/commands.go @@ -19,29 +19,40 @@ import ( "regexp" ) -// 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. +// Command represents a subcommand. Name, Func, +// and Short are required. type Command struct { + // The name of the subcommand. Must conform to the + // format described by the RegisterCommand() godoc. + // Required. Name string - // 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. + // Run 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 the one-line message explaining args, flags. + // 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 + // intended to be replaced by the user. Do not prefix + // the string with "caddy" or the name of the command + // since these will be prepended for you; only include + // the actual parameters for this command. Usage string - // Short is the short description for command. + // Short is a one-line message explaining what the + // command does. Should not end with punctuation. + // Required. Short string - // Long is the message for 'caddy help ' + // Long is the full help text shown to the user. + // Will be trimmed of whitespace on both ends before + // being printed. Long string - // Flags is flagset for command. + // Flags is the flagset for command. Flags *flag.FlagSet } @@ -54,14 +65,15 @@ var commands = map[string]Command{ "start": { Name: "start", Func: cmdStart, - Usage: "[--config ] [--adapter ]", - Short: "Starts the Caddy process and returns after server has started.", + Usage: "[--config [[--adapter ]]", + Short: "Starts the Caddy process in the background and then returns", 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.`, +Starts the Caddy process, optionally bootstrapped with an initial config file. +This command unblocks after the server starts running or fails to run. + +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") @@ -73,13 +85,12 @@ details.`, "run": { Name: "run", Func: cmdRun, - Usage: "[--config ] [--adapter ] [--print-env]", - Short: `Starts the Caddy process and blocks indefinitely.`, + Usage: "[--config [--adapter ]] [--environ]", + 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. +Starts the Caddy process, optionally bootstrapped with an initial config file, +and blocks indefinitely until the server is stopped; i.e. runs Caddy in +"daemon" mode (foreground). 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 @@ -90,13 +101,13 @@ errors will immediately be used. If you want to review the results of the adaptation first, use the 'adapt' subcommand. 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. +"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 --environ is specified, the environment as seen by the Caddy process will be printed before starting. This is the same as the environ command but does -not quit after printing.`, +not quit after printing, and can be useful for troubleshooting.`, Flags: func() *flag.FlagSet { fs := flag.NewFlagSet("run", flag.ExitOnError) fs.String("config", "", "Configuration file") @@ -111,26 +122,32 @@ not quit after printing.`, 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.`, + Long: ` +Stops the running Caddy process as gracefully as possible. + +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 API endpoint. + +Note: this will stop any process named the same as the executable (os.Args[0]).`, }, "reload": { Name: "reload", Func: cmdReload, Usage: "--config [--adapter ] [--address ]", - 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.`, + Short: "Changes the config of the running Caddy instance", + Long: ` +Gives the running Caddy instance a new configuration. This has the same effect +as POSTing a document to the /load API 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("reload", flag.ExitOnError) - fs.String("config", "", "Configuration file") + 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") return fs @@ -140,15 +157,14 @@ default is assumed.`, "version": { Name: "version", Func: cmdVersion, - Short: "Prints the version.", - Long: `Prints the version.`, + Short: "Prints the version", }, "list-modules": { Name: "list-modules", Func: cmdListModules, - Short: "List installed Caddy modules.", - Long: `List installed Caddy modules.`, + Usage: "[--versions]", + Short: "Lists the installed Caddy modules", Flags: func() *flag.FlagSet { fs := flag.NewFlagSet("list-modules", flag.ExitOnError) fs.Bool("versions", false, "Print version information") @@ -159,24 +175,30 @@ default is assumed.`, "environ": { Name: "environ", Func: cmdEnviron, - Short: "Prints the environment as seen by Caddy.", - Long: `Prints the environment as seen by Caddy.`, + Short: "Prints the environment", }, "adapt": { Name: "adapt", Func: cmdAdaptConfig, - Usage: "--config --adapter [--pretty]", - Short: "Adapts a configuration to Caddy's native JSON config structure", + Usage: "--config --adapter [--pretty] [--validate]", + Short: "Adapts a configuration to Caddy's native JSON", 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.`, +Adapts a configuration to Caddy's native JSON format 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. + +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") - fs.String("adapter", "", "Name of config adapter") + fs.String("config", "", "Configuration file to adapt (required)") + fs.String("adapter", "", "Name of config adapter (required)") fs.Bool("pretty", false, "Format the output for human readability") + fs.Bool("validate", false, "Validate the output") return fs }(), }, @@ -185,10 +207,11 @@ the output will be formatted with indentation for human readability.`, Name: "validate", Func: cmdValidateConfig, Usage: "--config [--adapter ]", - Short: "Tests whether a configuration file is valid.", + Short: "Tests whether a configuration file is valid", Long: ` -Loads and provisions the provided config, but does not start -running it.`, +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.`, Flags: func() *flag.FlagSet { fs := flag.NewFlagSet("load", flag.ExitOnError) fs.String("config", "", "Input configuration file") @@ -208,7 +231,7 @@ func init() { Name: "help", Func: cmdHelp, Usage: "", - Short: "Shows help for a Caddy subcommand.", + Short: "Shows help for a Caddy subcommand", } }