cmd: CLI improvements; add --validate to adapt command

This commit is contained in:
Matthew Holt 2019-10-01 11:02:13 -06:00
parent 5b36424cf0
commit 2c3657bb8a
No known key found for this signature in database
GPG key ID: 2A349DD577D586A5
2 changed files with 107 additions and 61 deletions

View file

@ -351,10 +351,11 @@ func cmdAdaptConfig(fl Flags) (int, error) {
adaptCmdInputFlag := fl.String("config") adaptCmdInputFlag := fl.String("config")
adaptCmdAdapterFlag := fl.String("adapter") adaptCmdAdapterFlag := fl.String("adapter")
adaptCmdPrettyFlag := fl.Bool("pretty") adaptCmdPrettyFlag := fl.Bool("pretty")
adaptCmdValidateFlag := fl.Bool("validate")
if adaptCmdAdapterFlag == "" || adaptCmdInputFlag == "" { if adaptCmdAdapterFlag == "" || adaptCmdInputFlag == "" {
return caddy.ExitCodeFailedStartup, return caddy.ExitCodeFailedStartup,
fmt.Errorf("usage: caddy adapt --adapter <name> --input <file>") fmt.Errorf("--adapter and --config flags are required")
} }
cfgAdapter := caddyconfig.GetAdapter(adaptCmdAdapterFlag) cfgAdapter := caddyconfig.GetAdapter(adaptCmdAdapterFlag)
@ -391,6 +392,19 @@ func cmdAdaptConfig(fl Flags) (int, error) {
// print result to stdout // print result to stdout
fmt.Println(string(adaptedConfig)) 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 return caddy.ExitCodeSuccess, nil
} }
@ -457,7 +471,8 @@ usage:
commands: commands:
` `
for _, cmd := range 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 <command>' for more information about a command.\n" s += "\nUse 'caddy help <command>' for more information about a command.\n"
@ -475,8 +490,16 @@ commands:
return caddy.ExitCodeFailedStartup, fmt.Errorf("unknown command: %s", args[0]) 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", result := fmt.Sprintf("%s\n\nusage:\n caddy %s %s\n",
strings.TrimSpace(subcommand.Long), helpText,
subcommand.Name, subcommand.Name,
strings.TrimSpace(subcommand.Usage), strings.TrimSpace(subcommand.Usage),
) )

View file

@ -19,29 +19,40 @@ import (
"regexp" "regexp"
) )
// Command represents a subcommand. All fields // Command represents a subcommand. Name, Func,
// are required to be set except for Flags if // and Short are required.
// there are no flags and Usage if there are
// no flags or arguments.
type Command struct { type Command struct {
// The name of the subcommand. Must conform to the
// format described by the RegisterCommand() godoc.
// Required.
Name string Name string
// Run is a function that executes a subcommand. // Run is a function that executes a subcommand using
// It returns an exit code and any associated error. // the parsed flags. It returns an exit code and any
// Takes non-flag commandline arguments as args. // associated error.
// Flag must be parsed before Run is executed. // Required.
Func CommandFunc 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 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 Short string
// Long is the message for 'caddy help <command>' // Long is the full help text shown to the user.
// Will be trimmed of whitespace on both ends before
// being printed.
Long string Long string
// Flags is flagset for command. // Flags is the flagset for command.
Flags *flag.FlagSet Flags *flag.FlagSet
} }
@ -54,14 +65,15 @@ var commands = map[string]Command{
"start": { "start": {
Name: "start", Name: "start",
Func: cmdStart, Func: cmdStart,
Usage: "[--config <path>] [--adapter <name>]", Usage: "[--config <path> [[--adapter <name>]]",
Short: "Starts the Caddy process and returns after server has started.", Short: "Starts the Caddy process in the background and then returns",
Long: ` Long: `
Starts the Caddy process, optionally bootstrapped with an initial Starts the Caddy process, optionally bootstrapped with an initial config file.
config file. Blocks until server is successfully running (or fails to run), This command unblocks after the server starts 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 On Windows, the spawned child process will remain attached to the terminal, so
details.`, 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 { Flags: func() *flag.FlagSet {
fs := flag.NewFlagSet("start", flag.ExitOnError) fs := flag.NewFlagSet("start", flag.ExitOnError)
fs.String("config", "", "Configuration file") fs.String("config", "", "Configuration file")
@ -73,13 +85,12 @@ details.`,
"run": { "run": {
Name: "run", Name: "run",
Func: cmdRun, Func: cmdRun,
Usage: "[--config <path>] [--adapter <name>] [--print-env]", Usage: "[--config <path> [--adapter <name>]] [--environ]",
Short: `Starts the Caddy process and blocks indefinitely.`, Short: `Starts the Caddy process and blocks indefinitely`,
Long: ` Long: `
Same as start, but blocks indefinitely; i.e. runs Caddy in "daemon" mode. On Starts the Caddy process, optionally bootstrapped with an initial config file,
Windows, this is recommended over caddy start when running Caddy manually since and blocks indefinitely until the server is stopped; i.e. runs Caddy in
it will be more obvious that Caddy is still running and bound to the terminal "daemon" mode (foreground).
window.
If a config file is specified, it will be applied immediately after the process 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 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. adaptation first, use the 'adapt' subcommand.
As a special case, if the current working directory has a file called 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 "Caddyfile" and the caddyfile config adapter is plugged in (default), then
file will be loaded and used to configure Caddy, even without any command line that file will be loaded and used to configure Caddy, even without any command
flags. line flags.
If --environ is specified, the environment as seen by the Caddy process will 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 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 { Flags: func() *flag.FlagSet {
fs := flag.NewFlagSet("run", flag.ExitOnError) fs := flag.NewFlagSet("run", flag.ExitOnError)
fs.String("config", "", "Configuration file") fs.String("config", "", "Configuration file")
@ -111,26 +122,32 @@ not quit after printing.`,
Name: "stop", Name: "stop",
Func: cmdStop, Func: cmdStop,
Short: "Gracefully stops the running Caddy process", Short: "Gracefully stops the running Caddy process",
Long: `Gracefully stops the running Caddy process. (Note: this will stop any process Long: `
named the same as the executable.) On Windows, this stop is forceful and Caddy Stops the running Caddy process as gracefully as possible.
will not have an opportunity to clean up any active locks; for a graceful
shutdown on Windows, use Ctrl+C or the /stop endpoint.`, 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": { "reload": {
Name: "reload", Name: "reload",
Func: cmdReload, Func: cmdReload,
Usage: "--config <path> [--adapter <name>] [--address <interface>]", Usage: "--config <path> [--adapter <name>] [--address <interface>]",
Short: "Gives the running Caddy instance a new configuration", Short: "Changes the config of the running Caddy instance",
Long: `Gives the running Caddy instance a new configuration. This has the same effect Long: `
as POSTing a document to the /load endpoint, but is convenient for simple Gives the running Caddy instance a new configuration. This has the same effect
workflows revolving around config files. Since the admin endpoint is as POSTing a document to the /load API endpoint, but is convenient for simple
configurable, the endpoint configuration is loaded from the --address flag if workflows revolving around config files.
specified; otherwise it is loaded from the given config file; otherwise the
default is assumed.`, 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 { Flags: func() *flag.FlagSet {
fs := flag.NewFlagSet("reload", flag.ExitOnError) 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("adapter", "", "Name of config adapter to apply")
fs.String("address", "", "Address of the administration listener, if different from config") fs.String("address", "", "Address of the administration listener, if different from config")
return fs return fs
@ -140,15 +157,14 @@ default is assumed.`,
"version": { "version": {
Name: "version", Name: "version",
Func: cmdVersion, Func: cmdVersion,
Short: "Prints the version.", Short: "Prints the version",
Long: `Prints the version.`,
}, },
"list-modules": { "list-modules": {
Name: "list-modules", Name: "list-modules",
Func: cmdListModules, Func: cmdListModules,
Short: "List installed Caddy modules.", Usage: "[--versions]",
Long: `List installed Caddy modules.`, Short: "Lists the installed Caddy modules",
Flags: func() *flag.FlagSet { Flags: func() *flag.FlagSet {
fs := flag.NewFlagSet("list-modules", flag.ExitOnError) fs := flag.NewFlagSet("list-modules", flag.ExitOnError)
fs.Bool("versions", false, "Print version information") fs.Bool("versions", false, "Print version information")
@ -159,24 +175,30 @@ default is assumed.`,
"environ": { "environ": {
Name: "environ", Name: "environ",
Func: cmdEnviron, Func: cmdEnviron,
Short: "Prints the environment as seen by Caddy.", Short: "Prints the environment",
Long: `Prints the environment as seen by Caddy.`,
}, },
"adapt": { "adapt": {
Name: "adapt", Name: "adapt",
Func: cmdAdaptConfig, Func: cmdAdaptConfig,
Usage: "--config <path> --adapter <name> [--pretty]", Usage: "--config <path> --adapter <name> [--pretty] [--validate]",
Short: "Adapts a configuration to Caddy's native JSON config structure", Short: "Adapts a configuration to Caddy's native JSON",
Long: ` Long: `
Adapts a configuration to Caddy's native JSON config structure and writes the 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, output to stdout, along with any warnings to stderr.
the output will be formatted with indentation for human readability.`,
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 { Flags: func() *flag.FlagSet {
fs := flag.NewFlagSet("adapt", flag.ExitOnError) fs := flag.NewFlagSet("adapt", flag.ExitOnError)
fs.String("config", "", "Configuration file to adapt") fs.String("config", "", "Configuration file to adapt (required)")
fs.String("adapter", "", "Name of config adapter") fs.String("adapter", "", "Name of config adapter (required)")
fs.Bool("pretty", false, "Format the output for human readability") fs.Bool("pretty", false, "Format the output for human readability")
fs.Bool("validate", false, "Validate the output")
return fs return fs
}(), }(),
}, },
@ -185,10 +207,11 @@ the output will be formatted with indentation for human readability.`,
Name: "validate", Name: "validate",
Func: cmdValidateConfig, Func: cmdValidateConfig,
Usage: "--config <path> [--adapter <name>]", Usage: "--config <path> [--adapter <name>]",
Short: "Tests whether a configuration file is valid.", Short: "Tests whether a configuration file is valid",
Long: ` Long: `
Loads and provisions the provided config, but does not start Loads and provisions the provided config, but does not start running it.
running it.`, This reveals any errors with the configuration through the loading and
provisioning stages.`,
Flags: func() *flag.FlagSet { Flags: func() *flag.FlagSet {
fs := flag.NewFlagSet("load", flag.ExitOnError) fs := flag.NewFlagSet("load", flag.ExitOnError)
fs.String("config", "", "Input configuration file") fs.String("config", "", "Input configuration file")
@ -208,7 +231,7 @@ func init() {
Name: "help", Name: "help",
Func: cmdHelp, Func: cmdHelp,
Usage: "<command>", Usage: "<command>",
Short: "Shows help for a Caddy subcommand.", Short: "Shows help for a Caddy subcommand",
} }
} }