cmd: Expand cobra support, add short flags (#5379)

* cmd: Expand cobra support

* Convert commands to cobra, add short flags

* Fix version command typo

Co-authored-by: Emily Lange <git@indeednotjames.com>

* Apply suggestions from code review

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>

---------

Co-authored-by: Emily Lange <git@indeednotjames.com>
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
This commit is contained in:
Francis Lavoie 2023-02-24 18:09:12 -05:00 committed by GitHub
parent 167981d258
commit 9e6919550b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 199 additions and 210 deletions

View file

@ -109,12 +109,21 @@ func caddyCmdToCobra(caddyCmd Command) *cobra.Command {
Use: caddyCmd.Name, Use: caddyCmd.Name,
Short: caddyCmd.Short, Short: caddyCmd.Short,
Long: caddyCmd.Long, 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 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 {
_, err := f(Flags{cmd.Flags()})
return err
}
}

View file

@ -34,12 +34,6 @@ type Command struct {
// Required. // Required.
Name string 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 // Usage is a brief message describing the syntax of
// the subcommand's flags and args. Use [] to indicate // the subcommand's flags and args. Use [] to indicate
// optional parameters and <> to enclose literal values // optional parameters and <> to enclose literal values
@ -60,7 +54,21 @@ type Command struct {
Long string Long string
// Flags is the flagset for command. // Flags is the flagset for command.
// This is ignored if CobraFunc is set.
Flags *flag.FlagSet 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 // CommandFunc is a command's function. It runs the
@ -79,7 +87,6 @@ var commands = make(map[string]Command)
func init() { func init() {
RegisterCommand(Command{ RegisterCommand(Command{
Name: "start", Name: "start",
Func: cmdStart,
Usage: "[--config <path> [--adapter <name>]] [--envfile <path>] [--watch] [--pidfile <file>]", Usage: "[--config <path> [--adapter <name>]] [--envfile <path>] [--watch] [--pidfile <file>]",
Short: "Starts the Caddy process in the background and then returns", Short: "Starts the Caddy process in the background and then returns",
Long: ` Long: `
@ -93,21 +100,19 @@ 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 closing the window will forcefully stop Caddy; to avoid forgetting this, try
using 'caddy run' instead to keep it in the foreground. using 'caddy run' instead to keep it in the foreground.
`, `,
Flags: func() *flag.FlagSet { CobraFunc: func(cmd *cobra.Command) {
fs := flag.NewFlagSet("start", flag.ExitOnError) cmd.Flags().StringP("config", "c", "", "Configuration file")
fs.String("config", "", "Configuration file") cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply")
fs.String("adapter", "", "Name of config adapter to apply") cmd.Flags().StringP("envfile", "", "", "Environment file to load")
fs.String("envfile", "", "Environment file to load") cmd.Flags().BoolP("watch", "w", false, "Reload changed config file automatically")
fs.Bool("watch", false, "Reload changed config file automatically") cmd.Flags().StringP("pidfile", "", "", "Path of file to which to write process ID")
fs.String("pidfile", "", "Path of file to which to write process ID") cmd.RunE = WrapCommandFuncForCobra(cmdStart)
return fs },
}(),
}) })
RegisterCommand(Command{ RegisterCommand(Command{
Name: "run", Name: "run",
Func: cmdRun, Usage: "[--config <path> [--adapter <name>]] [--envfile <path>] [--environ] [--resume] [--watch] [--pidfile <file>]",
Usage: "[--config <path> [--adapter <name>]] [--envfile <path>] [--environ] [--resume] [--watch] [--pidfile <fil>]",
Short: `Starts the Caddy process and blocks indefinitely`, Short: `Starts the Caddy process and blocks indefinitely`,
Long: ` Long: `
Starts the Caddy process, optionally bootstrapped with an initial config file, Starts the Caddy process, optionally bootstrapped with an initial config file,
@ -141,24 +146,22 @@ If --watch is specified, the config file will be loaded automatically after
changes. This can make unintentional config changes easier; only use this changes. This can make unintentional config changes easier; only use this
option in a local development environment. option in a local development environment.
`, `,
Flags: func() *flag.FlagSet { CobraFunc: func(cmd *cobra.Command) {
fs := flag.NewFlagSet("run", flag.ExitOnError) cmd.Flags().StringP("config", "c", "", "Configuration file")
fs.String("config", "", "Configuration file") cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply")
fs.String("adapter", "", "Name of config adapter to apply") cmd.Flags().StringP("envfile", "", "", "Environment file to load")
fs.String("envfile", "", "Environment file to load") cmd.Flags().BoolP("environ", "e", false, "Print environment")
fs.Bool("environ", false, "Print environment") cmd.Flags().BoolP("resume", "r", false, "Use saved config, if any (and prefer over --config file)")
fs.Bool("resume", 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")
fs.Bool("watch", false, "Watch config file for changes and reload it automatically") cmd.Flags().StringP("pidfile", "", "", "Path of file to which to write process ID")
fs.String("pidfile", "", "Path of file to which to write process ID") cmd.Flags().StringP("pingback", "", "", "Echo confirmation bytes to this address on success")
fs.String("pingback", "", "Echo confirmation bytes to this address on success") cmd.RunE = WrapCommandFuncForCobra(cmdRun)
return fs },
}(),
}) })
RegisterCommand(Command{ RegisterCommand(Command{
Name: "stop", Name: "stop",
Func: cmdStop, Usage: "[--config <path> [--adapter <name>]] [--address <interface>]",
Usage: "[--address <interface>] [--config <path> [--adapter <name>]]",
Short: "Gracefully stops a started Caddy process", Short: "Gracefully stops a started Caddy process",
Long: ` Long: `
Stops the background Caddy process as gracefully as possible. Stops the background Caddy process as gracefully as possible.
@ -167,18 +170,16 @@ 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 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. using the --address flag, or from the given --config, if not the default.
`, `,
Flags: func() *flag.FlagSet { CobraFunc: func(cmd *cobra.Command) {
fs := flag.NewFlagSet("stop", flag.ExitOnError) cmd.Flags().StringP("config", "c", "", "Configuration file to use to parse the admin address, if --address is not used")
fs.String("address", "", "The address to use to reach the admin API endpoint, if not the default") cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply (when --config is used)")
fs.String("config", "", "Configuration file to use to parse the admin address, if --address is not used") cmd.Flags().StringP("address", "", "", "The address to use to reach the admin API endpoint, if not the default")
fs.String("adapter", "", "Name of config adapter to apply (when --config is used)") cmd.RunE = WrapCommandFuncForCobra(cmdStop)
return fs },
}(),
}) })
RegisterCommand(Command{ RegisterCommand(Command{
Name: "reload", Name: "reload",
Func: cmdReload,
Usage: "--config <path> [--adapter <name>] [--address <interface>]", Usage: "--config <path> [--adapter <name>] [--address <interface>]",
Short: "Changes the config of the running Caddy instance", Short: "Changes the config of the running Caddy instance",
Long: ` Long: `
@ -190,19 +191,17 @@ Since the admin endpoint is configurable, the endpoint configuration is loaded
from the --address flag if specified; otherwise it is loaded from the given from the --address flag if specified; otherwise it is loaded from the given
config file; otherwise the default is assumed. config file; otherwise the default is assumed.
`, `,
Flags: func() *flag.FlagSet { CobraFunc: func(cmd *cobra.Command) {
fs := flag.NewFlagSet("reload", flag.ExitOnError) cmd.Flags().StringP("config", "c", "", "Configuration file (required)")
fs.String("config", "", "Configuration file (required)") cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply")
fs.String("adapter", "", "Name of config adapter to apply") cmd.Flags().StringP("address", "", "", "Address of the administration listener, if different from config")
fs.String("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")
fs.Bool("force", false, "Force config reload, even if it is the same") cmd.RunE = WrapCommandFuncForCobra(cmdReload)
return fs },
}(),
}) })
RegisterCommand(Command{ RegisterCommand(Command{
Name: "version", Name: "version",
Func: cmdVersion,
Short: "Prints the version", Short: "Prints the version",
Long: ` Long: `
Prints the version of this Caddy binary. Prints the version of this Caddy binary.
@ -217,31 +216,29 @@ detailed version information is printed as given by Go modules.
For more details about the full version string, see the Go module For more details about the full version string, see the Go module
documentation: https://go.dev/doc/modules/version-numbers documentation: https://go.dev/doc/modules/version-numbers
`, `,
Func: cmdVersion,
}) })
RegisterCommand(Command{ RegisterCommand(Command{
Name: "list-modules", Name: "list-modules",
Func: cmdListModules, Usage: "[--packages] [--versions] [--skip-standard]",
Usage: "[--packages] [--versions]",
Short: "Lists the installed Caddy modules", Short: "Lists the installed Caddy modules",
Flags: func() *flag.FlagSet { CobraFunc: func(cmd *cobra.Command) {
fs := flag.NewFlagSet("list-modules", flag.ExitOnError) cmd.Flags().BoolP("packages", "", false, "Print package paths")
fs.Bool("packages", false, "Print package paths") cmd.Flags().BoolP("versions", "", false, "Print version information")
fs.Bool("versions", false, "Print version information") cmd.Flags().BoolP("skip-standard", "s", false, "Skip printing standard modules")
fs.Bool("skip-standard", false, "Skip printing standard modules") cmd.RunE = WrapCommandFuncForCobra(cmdListModules)
return fs },
}(),
}) })
RegisterCommand(Command{ RegisterCommand(Command{
Name: "build-info", Name: "build-info",
Func: cmdBuildInfo,
Short: "Prints information about this build", Short: "Prints information about this build",
Func: cmdBuildInfo,
}) })
RegisterCommand(Command{ RegisterCommand(Command{
Name: "environ", Name: "environ",
Func: cmdEnviron,
Short: "Prints the environment", Short: "Prints the environment",
Long: ` Long: `
Prints the environment as seen by this Caddy process. Prints the environment as seen by this Caddy process.
@ -261,11 +258,11 @@ by adding the "--environ" flag.
Environments may contain sensitive data. Environments may contain sensitive data.
`, `,
Func: cmdEnviron,
}) })
RegisterCommand(Command{ RegisterCommand(Command{
Name: "adapt", Name: "adapt",
Func: cmdAdaptConfig,
Usage: "--config <path> [--adapter <name>] [--pretty] [--validate]", Usage: "--config <path> [--adapter <name>] [--pretty] [--validate]",
Short: "Adapts a configuration to Caddy's native JSON", Short: "Adapts a configuration to Caddy's native JSON",
Long: ` Long: `
@ -279,19 +276,17 @@ 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- If the config is invalid, an error will be printed to stderr and a non-
zero exit status will be returned. zero exit status will be returned.
`, `,
Flags: func() *flag.FlagSet { CobraFunc: func(cmd *cobra.Command) {
fs := flag.NewFlagSet("adapt", flag.ExitOnError) cmd.Flags().StringP("config", "c", "", "Configuration file to adapt (required)")
fs.String("config", "", "Configuration file to adapt (required)") cmd.Flags().StringP("adapter", "a", "caddyfile", "Name of config adapter")
fs.String("adapter", "caddyfile", "Name of config adapter") cmd.Flags().BoolP("pretty", "p", false, "Format the output for human readability")
fs.Bool("pretty", false, "Format the output for human readability") cmd.Flags().BoolP("validate", "", false, "Validate the output")
fs.Bool("validate", false, "Validate the output") cmd.RunE = WrapCommandFuncForCobra(cmdAdaptConfig)
return fs },
}(),
}) })
RegisterCommand(Command{ RegisterCommand(Command{
Name: "validate", Name: "validate",
Func: cmdValidateConfig,
Usage: "--config <path> [--adapter <name>] [--envfile <path>]", Usage: "--config <path> [--adapter <name>] [--envfile <path>]",
Short: "Tests whether a configuration file is valid", Short: "Tests whether a configuration file is valid",
Long: ` Long: `
@ -302,18 +297,16 @@ provisioning stages.
If --envfile is specified, an environment file with environment variables in If --envfile is specified, an environment file with environment variables in
the KEY=VALUE format will be loaded into the Caddy process. the KEY=VALUE format will be loaded into the Caddy process.
`, `,
Flags: func() *flag.FlagSet { CobraFunc: func(cmd *cobra.Command) {
fs := flag.NewFlagSet("validate", flag.ExitOnError) cmd.Flags().StringP("config", "c", "", "Input configuration file")
fs.String("config", "", "Input configuration file") cmd.Flags().StringP("adapter", "a", "", "Name of config adapter")
fs.String("adapter", "", "Name of config adapter") cmd.Flags().StringP("envfile", "", "", "Environment file to load")
fs.String("envfile", "", "Environment file to load") cmd.RunE = WrapCommandFuncForCobra(cmdValidateConfig)
return fs },
}(),
}) })
RegisterCommand(Command{ RegisterCommand(Command{
Name: "fmt", Name: "fmt",
Func: cmdFmt,
Usage: "[--overwrite] [--diff] [<path>]", Usage: "[--overwrite] [--diff] [<path>]",
Short: "Formats a Caddyfile", Short: "Formats a Caddyfile",
Long: ` Long: `
@ -332,32 +325,28 @@ 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 When reading from stdin, the --overwrite flag has no effect: the result
is always printed to stdout. is always printed to stdout.
`, `,
Flags: func() *flag.FlagSet { CobraFunc: func(cmd *cobra.Command) {
fs := flag.NewFlagSet("fmt", flag.ExitOnError) cmd.Flags().BoolP("overwrite", "w", false, "Overwrite the input file with the results")
fs.Bool("overwrite", 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")
fs.Bool("diff", false, "Print the differences between the input file and the formatted output") cmd.RunE = WrapCommandFuncForCobra(cmdFmt)
return fs },
}(),
}) })
RegisterCommand(Command{ RegisterCommand(Command{
Name: "upgrade", Name: "upgrade",
Func: cmdUpgrade,
Short: "Upgrade Caddy (EXPERIMENTAL)", Short: "Upgrade Caddy (EXPERIMENTAL)",
Long: ` Long: `
Downloads an updated Caddy binary with the same modules/plugins at the Downloads an updated Caddy binary with the same modules/plugins at the
latest versions. EXPERIMENTAL: May be changed or removed. latest versions. EXPERIMENTAL: May be changed or removed.
`, `,
Flags: func() *flag.FlagSet { CobraFunc: func(cmd *cobra.Command) {
fs := flag.NewFlagSet("upgrade", flag.ExitOnError) cmd.Flags().BoolP("keep-backup", "k", false, "Keep the backed up binary, instead of deleting it")
fs.Bool("keep-backup", false, "Keep the backed up binary, instead of deleting it") cmd.RunE = WrapCommandFuncForCobra(cmdUpgrade)
return fs },
}(),
}) })
RegisterCommand(Command{ RegisterCommand(Command{
Name: "add-package", Name: "add-package",
Func: cmdAddPackage,
Usage: "<packages...>", Usage: "<packages...>",
Short: "Adds Caddy packages (EXPERIMENTAL)", Short: "Adds Caddy packages (EXPERIMENTAL)",
Long: ` Long: `
@ -365,11 +354,10 @@ 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. already included. EXPERIMENTAL: May be changed or removed.
`, `,
Flags: func() *flag.FlagSet { CobraFunc: func(cmd *cobra.Command) {
fs := flag.NewFlagSet("add-package", flag.ExitOnError) cmd.Flags().BoolP("keep-backup", "k", false, "Keep the backed up binary, instead of deleting it")
fs.Bool("keep-backup", false, "Keep the backed up binary, instead of deleting it") cmd.RunE = WrapCommandFuncForCobra(cmdAddPackage)
return fs },
}(),
}) })
RegisterCommand(Command{ RegisterCommand(Command{
@ -382,31 +370,14 @@ Downloads an updated Caddy binaries without the specified packages (module/plugi
Returns an error if any of the packages are not included. Returns an error if any of the packages are not included.
EXPERIMENTAL: May be changed or removed. EXPERIMENTAL: May be changed or removed.
`, `,
Flags: func() *flag.FlagSet { CobraFunc: func(cmd *cobra.Command) {
fs := flag.NewFlagSet("remove-package", flag.ExitOnError) cmd.Flags().BoolP("keep-backup", "k", false, "Keep the backed up binary, instead of deleting it")
fs.Bool("keep-backup", false, "Keep the backed up binary, instead of deleting it") cmd.RunE = WrapCommandFuncForCobra(cmdRemovePackage)
return fs },
}(),
}) })
RegisterCommand(Command{ RegisterCommand(Command{
Name: "manpage", 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
},
Usage: "--directory <path>", Usage: "--directory <path>",
Short: "Generates the manual pages for Caddy commands", Short: "Generates the manual pages for Caddy commands",
Long: ` Long: `
@ -416,11 +387,25 @@ tagged into section 8 (System Administration).
The manual page files are generated into the directory specified by the 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. argument of --directory. If the directory does not exist, it will be created.
`, `,
Flags: func() *flag.FlagSet { CobraFunc: func(cmd *cobra.Command) {
fs := flag.NewFlagSet("manpage", flag.ExitOnError) cmd.Flags().StringP("directory", "o", "", "The output directory where the manpages are generated")
fs.String("directory", "", "The output directory where the manpages are generated") cmd.RunE = WrapCommandFuncForCobra(func(fl Flags) (int, error) {
return fs 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
})
},
}) })
// source: https://github.com/spf13/cobra/blob/main/shell_completions.md // source: https://github.com/spf13/cobra/blob/main/shell_completions.md
@ -504,7 +489,7 @@ func RegisterCommand(cmd Command) {
if cmd.Name == "" { if cmd.Name == "" {
panic("command name is required") panic("command name is required")
} }
if cmd.Func == nil { if cmd.Func == nil && cmd.CobraFunc == nil {
panic("command function missing") panic("command function missing")
} }
if cmd.Short == "" { if cmd.Short == "" {

View file

@ -18,20 +18,19 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"encoding/base64" "encoding/base64"
"flag"
"fmt" "fmt"
"os" "os"
"os/signal" "os/signal"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
caddycmd "github.com/caddyserver/caddy/v2/cmd" caddycmd "github.com/caddyserver/caddy/v2/cmd"
"github.com/spf13/cobra"
"golang.org/x/term" "golang.org/x/term"
) )
func init() { func init() {
caddycmd.RegisterCommand(caddycmd.Command{ caddycmd.RegisterCommand(caddycmd.Command{
Name: "hash-password", Name: "hash-password",
Func: cmdHashPassword,
Usage: "[--algorithm <name>] [--salt <string>] [--plaintext <password>]", Usage: "[--algorithm <name>] [--salt <string>] [--plaintext <password>]",
Short: "Hashes a password and writes base64", Short: "Hashes a password and writes base64",
Long: ` Long: `
@ -50,13 +49,12 @@ be provided (scrypt).
Note that scrypt is deprecated. Please use 'bcrypt' instead. Note that scrypt is deprecated. Please use 'bcrypt' instead.
`, `,
Flags: func() *flag.FlagSet { CobraFunc: func(cmd *cobra.Command) {
fs := flag.NewFlagSet("hash-password", flag.ExitOnError) cmd.Flags().StringP("plaintext", "p", "", "The plaintext password")
fs.String("algorithm", "bcrypt", "Name of the hash algorithm") cmd.Flags().StringP("salt", "s", "", "The password salt")
fs.String("plaintext", "", "The plaintext password") cmd.Flags().StringP("algorithm", "a", "bcrypt", "Name of the hash algorithm")
fs.String("salt", "", "The password salt") cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdHashPassword)
return fs },
}(),
}) })
} }

View file

@ -16,7 +16,6 @@ package fileserver
import ( import (
"encoding/json" "encoding/json"
"flag"
"log" "log"
"strconv" "strconv"
"time" "time"
@ -27,13 +26,13 @@ import (
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
caddytpl "github.com/caddyserver/caddy/v2/modules/caddyhttp/templates" caddytpl "github.com/caddyserver/caddy/v2/modules/caddyhttp/templates"
"github.com/caddyserver/certmagic" "github.com/caddyserver/certmagic"
"github.com/spf13/cobra"
"go.uber.org/zap" "go.uber.org/zap"
) )
func init() { func init() {
caddycmd.RegisterCommand(caddycmd.Command{ caddycmd.RegisterCommand(caddycmd.Command{
Name: "file-server", Name: "file-server",
Func: cmdFileServer,
Usage: "[--domain <example.com>] [--root <path>] [--listen <addr>] [--browse] [--access-log]", Usage: "[--domain <example.com>] [--root <path>] [--listen <addr>] [--browse] [--access-log]",
Short: "Spins up a production-ready file server", Short: "Spins up a production-ready file server",
Long: ` Long: `
@ -49,17 +48,16 @@ using this option.
If --browse is enabled, requests for folders without an index file will If --browse is enabled, requests for folders without an index file will
respond with a file listing.`, respond with a file listing.`,
Flags: func() *flag.FlagSet { CobraFunc: func(cmd *cobra.Command) {
fs := flag.NewFlagSet("file-server", flag.ExitOnError) cmd.Flags().StringP("domain", "d", "", "Domain name at which to serve the files")
fs.String("domain", "", "Domain name at which to serve the files") cmd.Flags().StringP("root", "r", "", "The path to the root of the site")
fs.String("root", "", "The path to the root of the site") cmd.Flags().StringP("listen", "", "", "The address to which to bind the listener")
fs.String("listen", "", "The address to which to bind the listener") cmd.Flags().BoolP("browse", "b", false, "Enable directory browsing")
fs.Bool("browse", false, "Enable directory browsing") cmd.Flags().BoolP("templates", "t", false, "Enable template rendering")
fs.Bool("templates", false, "Enable template rendering") cmd.Flags().BoolP("access-log", "", false, "Enable the access log")
fs.Bool("access-log", false, "Enable the access log") cmd.Flags().BoolP("debug", "v", false, "Enable verbose debug logs")
fs.Bool("debug", false, "Enable verbose debug logs") cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdFileServer)
return fs },
}(),
}) })
} }

View file

@ -16,7 +16,6 @@ package reverseproxy
import ( import (
"encoding/json" "encoding/json"
"flag"
"fmt" "fmt"
"net/http" "net/http"
"strconv" "strconv"
@ -28,14 +27,14 @@ import (
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/headers" "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
"github.com/caddyserver/caddy/v2/modules/caddytls" "github.com/caddyserver/caddy/v2/modules/caddytls"
"github.com/spf13/cobra"
"go.uber.org/zap" "go.uber.org/zap"
) )
func init() { func init() {
caddycmd.RegisterCommand(caddycmd.Command{ caddycmd.RegisterCommand(caddycmd.Command{
Name: "reverse-proxy", Name: "reverse-proxy",
Func: cmdReverseProxy, Usage: "[--from <addr>] [--to <addr>] [--change-host-header] [--insecure] [--internal-certs] [--disable-redirects] [--access-log]",
Usage: "[--from <addr>] [--to <addr>] [--change-host-header] [--insecure] [--internal-certs] [--disable-redirects]",
Short: "A quick and production-ready reverse proxy", Short: "A quick and production-ready reverse proxy",
Long: ` Long: `
A simple but production-ready reverse proxy. Useful for quick deployments, A simple but production-ready reverse proxy. Useful for quick deployments,
@ -63,17 +62,17 @@ For proxying:
--insecure disables TLS verification with the upstream. WARNING: THIS --insecure disables TLS verification with the upstream. WARNING: THIS
DISABLES SECURITY BY NOT VERIFYING THE UPSTREAM'S CERTIFICATE. DISABLES SECURITY BY NOT VERIFYING THE UPSTREAM'S CERTIFICATE.
`, `,
Flags: func() *flag.FlagSet { CobraFunc: func(cmd *cobra.Command) {
fs := flag.NewFlagSet("reverse-proxy", flag.ExitOnError) cmd.Flags().StringP("from", "f", "localhost", "Address on which to receive traffic")
fs.String("from", "localhost", "Address on which to receive traffic") cmd.Flags().StringSliceP("to", "t", []string{}, "Upstream address(es) to which traffic should be sent")
fs.Var(&reverseProxyCmdTo, "to", "Upstream address(es) to which traffic should be sent") cmd.Flags().BoolP("change-host-header", "c", false, "Set upstream Host header to address of upstream")
fs.Bool("change-host-header", false, "Set upstream Host header to address of upstream") cmd.Flags().BoolP("insecure", "", false, "Disable TLS verification (WARNING: DISABLES SECURITY BY NOT VERIFYING TLS CERTIFICATES!)")
fs.Bool("insecure", false, "Disable TLS verification (WARNING: DISABLES SECURITY BY NOT VERIFYING TLS CERTIFICATES!)") cmd.Flags().BoolP("disable-redirects", "r", false, "Disable HTTP->HTTPS redirects")
fs.Bool("internal-certs", false, "Use internal CA for issuing certs") cmd.Flags().BoolP("internal-certs", "i", false, "Use internal CA for issuing certs")
fs.Bool("debug", false, "Enable verbose debug logs") cmd.Flags().BoolP("access-log", "", false, "Enable the access log")
fs.Bool("disable-redirects", false, "Disable HTTP->HTTPS redirects") cmd.Flags().BoolP("debug", "v", false, "Enable verbose debug logs")
return fs cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdReverseProxy)
}(), },
}) })
} }
@ -83,14 +82,19 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) {
from := fs.String("from") from := fs.String("from")
changeHost := fs.Bool("change-host-header") changeHost := fs.Bool("change-host-header")
insecure := fs.Bool("insecure") insecure := fs.Bool("insecure")
internalCerts := fs.Bool("internal-certs")
debug := fs.Bool("debug")
disableRedir := fs.Bool("disable-redirects") disableRedir := fs.Bool("disable-redirects")
internalCerts := fs.Bool("internal-certs")
accessLog := fs.Bool("access-log")
debug := fs.Bool("debug")
httpPort := strconv.Itoa(caddyhttp.DefaultHTTPPort) httpPort := strconv.Itoa(caddyhttp.DefaultHTTPPort)
httpsPort := strconv.Itoa(caddyhttp.DefaultHTTPSPort) httpsPort := strconv.Itoa(caddyhttp.DefaultHTTPSPort)
if len(reverseProxyCmdTo) == 0 { to, err := fs.GetStringSlice("to")
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid to flag: %v", err)
}
if len(to) == 0 {
return caddy.ExitCodeFailedStartup, fmt.Errorf("--to is required") return caddy.ExitCodeFailedStartup, fmt.Errorf("--to is required")
} }
@ -119,9 +123,9 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) {
// set up the upstream address; assume missing information from given parts // set up the upstream address; assume missing information from given parts
// mixing schemes isn't supported, so use first defined (if available) // mixing schemes isn't supported, so use first defined (if available)
toAddresses := make([]string, len(reverseProxyCmdTo)) toAddresses := make([]string, len(to))
var toScheme string var toScheme string
for i, toLoc := range reverseProxyCmdTo { for i, toLoc := range to {
addr, scheme, err := parseUpstreamDialAddress(toLoc) addr, scheme, err := parseUpstreamDialAddress(toLoc)
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid upstream address %s: %v", toLoc, err) return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid upstream address %s: %v", toLoc, err)
@ -180,6 +184,9 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) {
Routes: caddyhttp.RouteList{route}, Routes: caddyhttp.RouteList{route},
Listen: []string{":" + fromAddr.Port}, Listen: []string{":" + fromAddr.Port},
} }
if accessLog {
server.Logs = &caddyhttp.ServerLogConfig{}
}
if fromAddr.Scheme == "http" { if fromAddr.Scheme == "http" {
server.AutoHTTPS = &caddyhttp.AutoHTTPSConfig{Disabled: true} server.AutoHTTPS = &caddyhttp.AutoHTTPSConfig{Disabled: true}
@ -238,6 +245,3 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) {
select {} select {}
} }
// reverseProxyCmdTo holds the parsed values from repeated use of the --to flag.
var reverseProxyCmdTo caddycmd.StringSlice

View file

@ -17,7 +17,6 @@ package caddyhttp
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"flag"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@ -32,6 +31,7 @@ import (
"github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
caddycmd "github.com/caddyserver/caddy/v2/cmd" caddycmd "github.com/caddyserver/caddy/v2/cmd"
"github.com/spf13/cobra"
"go.uber.org/zap" "go.uber.org/zap"
) )
@ -39,7 +39,6 @@ func init() {
caddy.RegisterModule(StaticResponse{}) caddy.RegisterModule(StaticResponse{})
caddycmd.RegisterCommand(caddycmd.Command{ caddycmd.RegisterCommand(caddycmd.Command{
Name: "respond", Name: "respond",
Func: cmdRespond,
Usage: `[--status <code>] [--body <content>] [--listen <addr>] [--access-log] [--debug] [--header "Field: value"] <body|status>`, Usage: `[--status <code>] [--body <content>] [--listen <addr>] [--access-log] [--debug] [--header "Field: value"] <body|status>`,
Short: "Simple, hard-coded HTTP responses for development and testing", Short: "Simple, hard-coded HTTP responses for development and testing",
Long: ` Long: `
@ -71,16 +70,15 @@ Access/request logging and more verbose debug logging can also be enabled.
Response headers may be added using the --header flag for each header field. Response headers may be added using the --header flag for each header field.
`, `,
Flags: func() *flag.FlagSet { CobraFunc: func(cmd *cobra.Command) {
fs := flag.NewFlagSet("respond", flag.ExitOnError) cmd.Flags().StringP("listen", "l", ":0", "The address to which to bind the listener")
fs.String("listen", ":0", "The address to which to bind the listener") cmd.Flags().IntP("status", "s", http.StatusOK, "The response status code")
fs.Int("status", http.StatusOK, "The response status code") cmd.Flags().StringP("body", "b", "", "The body of the HTTP response")
fs.String("body", "", "The body of the HTTP response") cmd.Flags().BoolP("access-log", "", false, "Enable the access log")
fs.Bool("access-log", false, "Enable the access log") cmd.Flags().BoolP("debug", "v", false, "Enable more verbose debug-level logging")
fs.Bool("debug", false, "Enable more verbose debug-level logging") cmd.Flags().StringSliceP("header", "H", []string{}, "Set a header on the response (format: \"Field: value\")")
fs.Var(&respondCmdHeaders, "header", "Set a header on the response (format: \"Field: value\"") cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdRespond)
return fs },
}(),
}) })
} }
@ -318,8 +316,12 @@ func cmdRespond(fl caddycmd.Flags) (int, error) {
} }
// build headers map // build headers map
headers, err := fl.GetStringSlice("header")
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid header flag: %v", err)
}
hdr := make(http.Header) hdr := make(http.Header)
for i, h := range respondCmdHeaders { for i, h := range headers {
key, val, found := strings.Cut(h, ":") key, val, found := strings.Cut(h, ":")
key, val = strings.TrimSpace(key), strings.TrimSpace(val) key, val = strings.TrimSpace(key), strings.TrimSpace(val)
if !found || key == "" || val == "" { if !found || key == "" || val == "" {
@ -432,9 +434,6 @@ func cmdRespond(fl caddycmd.Flags) (int, error) {
select {} select {}
} }
// respondCmdHeaders holds the parsed values from repeated use of the --header flag.
var respondCmdHeaders caddycmd.StringSlice
// Interface guards // Interface guards
var ( var (
_ MiddlewareHandler = (*StaticResponse)(nil) _ MiddlewareHandler = (*StaticResponse)(nil)

View file

@ -18,7 +18,6 @@ import (
"crypto/x509" "crypto/x509"
"encoding/json" "encoding/json"
"encoding/pem" "encoding/pem"
"flag"
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
@ -27,12 +26,12 @@ import (
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
caddycmd "github.com/caddyserver/caddy/v2/cmd" caddycmd "github.com/caddyserver/caddy/v2/cmd"
"github.com/smallstep/truststore" "github.com/smallstep/truststore"
"github.com/spf13/cobra"
) )
func init() { func init() {
caddycmd.RegisterCommand(caddycmd.Command{ caddycmd.RegisterCommand(caddycmd.Command{
Name: "trust", Name: "trust",
Func: cmdTrust,
Usage: "[--ca <id>] [--address <listen>] [--config <path> [--adapter <name>]]", Usage: "[--ca <id>] [--address <listen>] [--config <path> [--adapter <name>]]",
Short: "Installs a CA certificate into local trust stores", Short: "Installs a CA certificate into local trust stores",
Long: ` Long: `
@ -53,19 +52,17 @@ This command will attempt to connect to Caddy's admin API running at
'` + caddy.DefaultAdminListen + `' to fetch the root certificate. You may '` + caddy.DefaultAdminListen + `' to fetch the root certificate. You may
explicitly specify the --address, or use the --config flag to load explicitly specify the --address, or use the --config flag to load
the admin address from your config, if not using the default.`, the admin address from your config, if not using the default.`,
Flags: func() *flag.FlagSet { CobraFunc: func(cmd *cobra.Command) {
fs := flag.NewFlagSet("trust", flag.ExitOnError) cmd.Flags().StringP("ca", "", "", "The ID of the CA to trust (defaults to 'local')")
fs.String("ca", "", "The ID of the CA to trust (defaults to 'local')") cmd.Flags().StringP("address", "", "", "Address of the administration API listener (if --config is not used)")
fs.String("address", "", "Address of the administration API listener (if --config is not used)") cmd.Flags().StringP("config", "c", "", "Configuration file (if --address is not used)")
fs.String("config", "", "Configuration file (if --address is not used)") cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply (if --config is used)")
fs.String("adapter", "", "Name of config adapter to apply (if --config is used)") cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdTrust)
return fs },
}(),
}) })
caddycmd.RegisterCommand(caddycmd.Command{ caddycmd.RegisterCommand(caddycmd.Command{
Name: "untrust", Name: "untrust",
Func: cmdUntrust,
Usage: "[--cert <path>] | [[--ca <id>] [--address <listen>] [--config <path> [--adapter <name>]]]", Usage: "[--cert <path>] | [[--ca <id>] [--address <listen>] [--config <path> [--adapter <name>]]]",
Short: "Untrusts a locally-trusted CA certificate", Short: "Untrusts a locally-trusted CA certificate",
Long: ` Long: `
@ -89,15 +86,14 @@ will attempt to connect to the Caddy's admin API running at
'` + caddy.DefaultAdminListen + `' to fetch the root certificate. '` + caddy.DefaultAdminListen + `' to fetch the root certificate.
You may explicitly specify the --address, or use the --config flag You may explicitly specify the --address, or use the --config flag
to load the admin address from your config, if not using the default.`, to load the admin address from your config, if not using the default.`,
Flags: func() *flag.FlagSet { CobraFunc: func(cmd *cobra.Command) {
fs := flag.NewFlagSet("untrust", flag.ExitOnError) cmd.Flags().StringP("cert", "p", "", "The path to the CA certificate to untrust")
fs.String("cert", "", "The path to the CA certificate to untrust") cmd.Flags().StringP("ca", "", "", "The ID of the CA to untrust (defaults to 'local')")
fs.String("ca", "", "The ID of the CA to untrust (defaults to 'local')") cmd.Flags().StringP("address", "", "", "Address of the administration API listener (if --config is not used)")
fs.String("address", "", "Address of the administration API listener (if --config is not used)") cmd.Flags().StringP("config", "c", "", "Configuration file (if --address is not used)")
fs.String("config", "", "Configuration file (if --address is not used)") cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply (if --config is used)")
fs.String("adapter", "", "Name of config adapter to apply (if --config is used)") cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdUntrust)
return fs },
}(),
}) })
} }