diff --git a/cmd/commandfuncs.go b/cmd/commandfuncs.go index be9da93c..715f064d 100644 --- a/cmd/commandfuncs.go +++ b/cmd/commandfuncs.go @@ -26,6 +26,8 @@ import ( "net/http" "os" "os/exec" + "reflect" + "runtime/debug" "strings" "github.com/caddyserver/caddy/v2" @@ -36,7 +38,7 @@ import ( func cmdStart(fl Flags) (int, error) { startCmdConfigFlag := fl.String("config") - startCmdConfigAdapterFlag := fl.String("config-adapter") + startCmdConfigAdapterFlag := fl.String("adapter") // open a listener to which the child process will connect when // it is ready to confirm that it has successfully started @@ -61,7 +63,7 @@ func cmdStart(fl Flags) (int, error) { cmd.Args = append(cmd.Args, "--config", startCmdConfigFlag) } if startCmdConfigAdapterFlag != "" { - cmd.Args = append(cmd.Args, "--config-adapter", startCmdConfigAdapterFlag) + cmd.Args = append(cmd.Args, "--adapter", startCmdConfigAdapterFlag) } stdinpipe, err := cmd.StdinPipe() if err != nil { @@ -137,8 +139,8 @@ func cmdStart(fl Flags) (int, error) { func cmdRun(fl Flags) (int, error) { runCmdConfigFlag := fl.String("config") - runCmdConfigAdapterFlag := fl.String("config-adapter") - runCmdPrintEnvFlag := fl.Bool("print-env") + runCmdConfigAdapterFlag := fl.String("adapter") + runCmdPrintEnvFlag := fl.Bool("environ") runCmdPingbackFlag := fl.String("pingback") // if we are supposed to print the environment, do that first @@ -216,7 +218,7 @@ func cmdStop(_ Flags) (int, error) { func cmdReload(fl Flags) (int, error) { reloadCmdConfigFlag := fl.String("config") - reloadCmdConfigAdapterFlag := fl.String("config-adapter") + reloadCmdConfigAdapterFlag := fl.String("adapter") reloadCmdAddrFlag := fl.String("address") // a configuration is required @@ -282,10 +284,61 @@ func cmdVersion(_ Flags) (int, error) { return caddy.ExitCodeSuccess, nil } -func cmdListModules(_ Flags) (int, error) { - for _, m := range caddy.Modules() { - fmt.Println(m) +func cmdListModules(fl Flags) (int, error) { + versions := fl.Bool("versions") + + bi, ok := debug.ReadBuildInfo() + if !ok || !versions { + // if there's no build information, + // just print out the modules + for _, m := range caddy.Modules() { + fmt.Println(m) + } + return caddy.ExitCodeSuccess, nil } + + for _, modName := range caddy.Modules() { + modInfo, err := caddy.GetModule(modName) + if err != nil { + // that's weird + fmt.Println(modName) + continue + } + + // to get the Caddy plugin's version info, we need to know + // the package that the Caddy module's value comes from; we + // can use reflection but we need a non-pointer value (I'm + // not sure why), and since New() should return a pointer + // value, we need to dereference it first + iface := interface{}(modInfo.New()) + if rv := reflect.ValueOf(iface); rv.Kind() == reflect.Ptr { + iface = reflect.New(reflect.TypeOf(iface).Elem()).Elem().Interface() + } + modPkgPath := reflect.TypeOf(iface).PkgPath() + + // now we find the Go module that the Caddy module's package + // belongs to; we assume the Caddy module package path will + // be prefixed by its Go module path, and we will choose the + // longest matching prefix in case there are nested modules + var matched *debug.Module + for _, dep := range bi.Deps { + if strings.HasPrefix(modPkgPath, dep.Path) { + if matched == nil || len(dep.Path) > len(matched.Path) { + matched = dep + } + } + } + + // if we could find no matching module, just print out + // the module name instead + if matched == nil { + fmt.Println(modName) + continue + } + + fmt.Printf("%s %s\n", modName, matched.Version) + } + return caddy.ExitCodeSuccess, nil } @@ -295,13 +348,13 @@ func cmdEnviron(_ Flags) (int, error) { } func cmdAdaptConfig(fl Flags) (int, error) { + adaptCmdInputFlag := fl.String("config") 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 --input ") + fmt.Errorf("usage: caddy adapt --adapter --input ") } cfgAdapter := caddyconfig.GetAdapter(adaptCmdAdapterFlag) @@ -341,6 +394,55 @@ func cmdAdaptConfig(fl Flags) (int, error) { return caddy.ExitCodeSuccess, nil } +func cmdValidateConfig(fl Flags) (int, error) { + validateCmdConfigFlag := fl.String("config") + validateCmdAdapterFlag := fl.String("adapter") + + input, err := ioutil.ReadFile(validateCmdConfigFlag) + if err != nil { + return caddy.ExitCodeFailedStartup, + fmt.Errorf("reading input file: %v", err) + } + + if validateCmdAdapterFlag != "" { + cfgAdapter := caddyconfig.GetAdapter(validateCmdAdapterFlag) + if cfgAdapter == nil { + return caddy.ExitCodeFailedStartup, + fmt.Errorf("unrecognized config adapter: %s", validateCmdAdapterFlag) + } + + adaptedConfig, warnings, err := cfgAdapter.Adapt(input, nil) + 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", validateCmdAdapterFlag, warn.File, warn.Line, msg) + } + + input = adaptedConfig + } + + var cfg *caddy.Config + err = json.Unmarshal(input, &cfg) + if err != nil { + return caddy.ExitCodeFailedStartup, fmt.Errorf("decoding config: %v", err) + } + + err = caddy.Validate(cfg) + if err != nil { + return caddy.ExitCodeFailedStartup, err + } + + fmt.Println("Valid configuration") + + 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` diff --git a/cmd/commands.go b/cmd/commands.go index 8a9d4824..731e8139 100644 --- a/cmd/commands.go +++ b/cmd/commands.go @@ -54,7 +54,7 @@ var commands = map[string]Command{ "start": { Name: "start", Func: cmdStart, - Usage: "[--config ] [--config-adapter ]", + Usage: "[--config ] [--adapter ]", Short: "Starts the Caddy process and returns after server has started.", Long: ` Starts the Caddy process, optionally bootstrapped with an initial @@ -65,7 +65,7 @@ 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") + fs.String("adapter", "", "Name of config adapter to apply") return fs }(), }, @@ -73,7 +73,7 @@ details.`, "run": { Name: "run", Func: cmdRun, - Usage: "[--config ] [--config-adapter ] [--print-env]", + Usage: "[--config ] [--adapter ] [--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 @@ -83,25 +83,25 @@ 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 +specify an adapter with --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. +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. -If --print-env 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 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("adapter", "", "Name of config adapter to apply") + fs.Bool("environ", false, "Print environment") fs.String("pingback", "", "Echo confirmation bytes to this address on success") return fs }(), @@ -120,7 +120,7 @@ shutdown on Windows, use Ctrl+C or the /stop endpoint.`, "reload": { Name: "reload", Func: cmdReload, - Usage: "--config [--config-adapter ] [--address ]", + 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 @@ -129,9 +129,9 @@ 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 := flag.NewFlagSet("reload", flag.ExitOnError) fs.String("config", "", "Configuration file") - fs.String("config-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") return fs }(), @@ -149,6 +149,11 @@ default is assumed.`, Func: cmdListModules, Short: "List installed Caddy modules.", Long: `List installed Caddy modules.`, + Flags: func() *flag.FlagSet { + fs := flag.NewFlagSet("list-modules", flag.ExitOnError) + fs.Bool("versions", false, "Print version information") + return fs + }(), }, "environ": { @@ -158,15 +163,38 @@ default is assumed.`, Long: `Prints the environment as seen by Caddy.`, }, - "adapt-config": { - Name: "adapt-config", + "adapt": { + Name: "adapt", Func: cmdAdaptConfig, - Usage: "--input --adapter [--pretty]", + Usage: "--config --adapter [--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.`, + 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.Bool("pretty", false, "Format the output for human readability") + return fs + }(), + }, + + "validate": { + Name: "validate", + Func: cmdValidateConfig, + Usage: "--config [--adapter ]", + Short: "Tests whether a configuration file is valid.", + Long: ` +Loads and provisions the provided config, but does not start +running it.`, + Flags: func() *flag.FlagSet { + fs := flag.NewFlagSet("load", flag.ExitOnError) + fs.String("config", "", "Input configuration file") + fs.String("adapter", "", "Name of config adapter") + return fs + }(), }, }