Browse Source

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
master
aca 5 years ago
committed by Matt Holt
parent
commit
0006df6026
  1. 391
      cmd/commandfuncs.go
  2. 497
      cmd/commands.go
  3. 130
      cmd/main.go

391
cmd/commandfuncs.go

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

497
cmd/commands.go

@ -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:])
// 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
// 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
// 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)
}
// 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
// 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()
}()
// Usage is the one-line message explaining args, flags.
Usage string
// start the process
err = cmd.Start()
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("starting caddy process: %v", err)
}
// Short is the short description for command.
Short string
// 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)
// Long is the message for 'caddy help <command>'
Long string
// 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:])
// if we are supposed to print the environment, do that first
if *runCmdPrintEnvFlag {
exitCode, err := cmdEnviron()
if err != nil {
return exitCode, err
}
}
// 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 {}
// 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)
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
}(),
},
"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.
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.
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
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
}(),
},
"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)
}
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")
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.",
}
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)")
}
// 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)
// 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")
}
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)
if cmd.Func == nil {
panic("command function missing")
}
return caddy.ExitCodeFailedStartup,
fmt.Errorf("caddy responded with error: HTTP %d: %s", resp.StatusCode, respBody)
if cmd.Short == "" {
panic("command short string is required")
}
return caddy.ExitCodeSuccess, nil
}
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)
if _, exists := commands[cmd.Name]; exists {
panic("command already registered: " + cmd.Name)
}
return caddy.ExitCodeSuccess, nil
}
func cmdListModules() (int, error) {
for _, m := range caddy.Modules() {
fmt.Println(m)
if !commandNameRegex.MatchString(cmd.Name) {
panic("invalid command name")
}
return caddy.ExitCodeSuccess, nil
commands[cmd.Name] = cmd
}
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]$`)

130
cmd/main.go

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

Loading…
Cancel
Save