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
This commit is contained in:
aca 2019-10-01 12:23:58 +09:00 committed by Matt Holt
parent c95db3551d
commit 0006df6026
3 changed files with 665 additions and 341 deletions

391
cmd/commandfuncs.go Normal file
View File

@ -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
}

View File

@ -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:])
// 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)
// if we are supposed to print the environment, do that first
if *runCmdPrintEnvFlag {
exitCode, err := cmdEnviron()
if err != nil {
return exitCode, err
}
}
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
}(),
},
// get the config in caddy's native format
config, err := loadConfig(*runCmdConfigFlag, *runCmdConfigAdapterFlag)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
"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.
// set a fitting User-Agent for ACME requests
goModule := caddy.GoModule()
cleanModVersion := strings.TrimPrefix(goModule.Version, "v")
certmagic.UserAgent = "Caddy/" + cleanModVersion
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.
// 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()
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 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)
}
}
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
}(),
},
select {}
"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)
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.",
}
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() (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)")
// 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")
}
// get the config in caddy's native format
config, err := loadConfig(*reloadCmdConfigFlag, *reloadCmdConfigAdapterFlag)
if err != nil {
return caddy.ExitCodeFailedStartup, err
if cmd.Func == nil {
panic("command function missing")
}
// 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 cmd.Short == "" {
panic("command short string is required")
}
if adminAddr == "" {
adminAddr = caddy.DefaultAdminListen
if _, exists := commands[cmd.Name]; exists {
panic("command already registered: " + cmd.Name)
}
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 !commandNameRegex.MatchString(cmd.Name) {
panic("invalid command name")
}
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
commands[cmd.Name] = cmd
}
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)
}
return caddy.ExitCodeSuccess, nil
}
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]$`)

View File

@ -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 {
log.Println(err)
os.Exit(exitCode)
fs := subcommand.Flags
if fs == nil {
fs = flag.NewFlagSet(subcommand.Name, flag.ExitOnError)
}
}
// commandFunc is a function that executes
// a subcommand. It returns an exit code and
// any associated error.
type commandFunc func() (int, error)
err := fs.Parse(os.Args[2:])
if err != nil {
log.Println(err)
os.Exit(caddy.ExitCodeFailedStartup)
}
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)
}
}