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. 521
      cmd/commands.go
  3. 128
      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
}

521
cmd/commands.go

@ -15,343 +15,204 @@
package caddycmd package caddycmd
import ( import (
"bytes"
"crypto/rand"
"encoding/json"
"flag" "flag"
"fmt" "regexp"
"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() (int, error) { // Command represents a subcommand. All fields
startCmd := flag.NewFlagSet("start", flag.ExitOnError) // are required to be set except for Flags if
startCmdConfigFlag := startCmd.String("config", "", "Configuration file") // there are no flags and Usage if there are
startCmdConfigAdapterFlag := startCmd.String("config-adapter", "", "Name of config adapter to apply") // no flags or arguments.
startCmd.Parse(os.Args[2:]) 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 // Run is a function that executes a subcommand.
ln, err := net.Listen("tcp", "127.0.0.1:0") // It returns an exit code and any associated error.
if err != nil { // Takes non-flag commandline arguments as args.
return caddy.ExitCodeFailedStartup, // Flag must be parsed before Run is executed.
fmt.Errorf("opening listener for success confirmation: %v", err) Func CommandFunc
}
defer ln.Close() // Usage is the one-line message explaining args, flags.
Usage string
// craft the command with a pingback address and with a
// pipe for its stdin, so we can tell it our confirmation // Short is the short description for command.
// code that we expect so that some random port scan at Short string
// the most unfortunate time won't fool us into thinking
// the child succeeded (i.e. the alternative is to just // Long is the message for 'caddy help <command>'
// wait for any connection on our listener, but better to Long string
// ensure it's the process we're expecting - we can be
// sure by giving it some random bytes and having it echo // Flags is flagset for command.
// them back to us) Flags *flag.FlagSet
cmd := exec.Command(os.Args[0], "run", "--pingback", ln.Addr().String()) }
if *startCmdConfigFlag != "" {
cmd.Args = append(cmd.Args, "--config", *startCmdConfigFlag) // CommandFunc is a command's function. It runs the
} // command and returns the proper exit code along with
if *startCmdConfigAdapterFlag != "" { // any error that occurred.
cmd.Args = append(cmd.Args, "--config-adapter", *startCmdConfigAdapterFlag) type CommandFunc func(Flags) (int, error)
}
stdinpipe, err := cmd.StdinPipe() var commands = map[string]Command{
if err != nil { "start": {
return caddy.ExitCodeFailedStartup, Name: "start",
fmt.Errorf("creating stdin pipe: %v", err) Func: cmdStart,
} Usage: "[--config <path>] [--config-adapter <name>]",
cmd.Stdout = os.Stdout Short: "Starts the Caddy process and returns after server has started.",
cmd.Stderr = os.Stderr Long: `
Starts the Caddy process, optionally bootstrapped with an initial
// generate the random bytes we'll send to the child process config file. Blocks until server is successfully running (or fails to run),
expect := make([]byte, 32) then returns. On Windows, the child process will remain attached to the
_, err = rand.Read(expect) terminal, so closing the window will forcefully stop Caddy. See run for more
if err != nil { details.`,
return caddy.ExitCodeFailedStartup, fmt.Errorf("generating random confirmation bytes: %v", err) Flags: func() *flag.FlagSet {
} fs := flag.NewFlagSet("start", flag.ExitOnError)
fs.String("config", "", "Configuration file")
// begin writing the confirmation bytes to the child's fs.String("config-adapter", "", "Name of config adapter to apply")
// stdin; use a goroutine since the child hasn't been return fs
// started yet, and writing sychronously would result }(),
// in a deadlock },
go func() {
stdinpipe.Write(expect) "run": {
stdinpipe.Close() Name: "run",
}() Func: cmdRun,
Usage: "[--config <path>] [--config-adapter <name>] [--print-env]",
// start the process Short: `Starts the Caddy process and blocks indefinitely.`,
err = cmd.Start() Long: `
if err != nil { Same as start, but blocks indefinitely; i.e. runs Caddy in "daemon" mode. On
return caddy.ExitCodeFailedStartup, fmt.Errorf("starting caddy process: %v", err) 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.
// there are two ways we know we're done: either
// the process will connect to our listener, or If a config file is specified, it will be applied immediately after the process
// it will exit with an error is running. If the config file is not in Caddy's native JSON format, you can
success, exit := make(chan struct{}), make(chan error) 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
// in one goroutine, we await the success of the child process warnings will be printed to the log, but beware that any adaptation without
go func() { errors will immediately be used. If you want to review the results of the
for { adaptation first, use adapt-config.
conn, err := ln.Accept()
if err != nil { As a special case, if the current working directory has a file called
if !strings.Contains(err.Error(), "use of closed network connection") { "Caddyfile" and the caddyfile config adapter is plugged in (default), then that
log.Println(err) file will be loaded and used to configure Caddy, even without any command line
} flags.
break
} If --print-env is specified, the environment as seen by the Caddy process will
err = handlePingbackConn(conn, expect) be printed before starting. This is the same as the environ command but does
if err == nil { not quit after printing.`,
close(success) Flags: func() *flag.FlagSet {
break fs := flag.NewFlagSet("run", flag.ExitOnError)
} fs.String("config", "", "Configuration file")
log.Println(err) 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
// 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 "stop": {
}() Name: "stop",
Func: cmdStop,
// when one of the goroutines unblocks, we're done and can exit Short: "Gracefully stops the running Caddy process",
select { Long: `Gracefully stops the running Caddy process. (Note: this will stop any process
case <-success: named the same as the executable.) On Windows, this stop is forceful and Caddy
fmt.Printf("Successfully started Caddy (pid=%d)\n", cmd.Process.Pid) will not have an opportunity to clean up any active locks; for a graceful
case err := <-exit: shutdown on Windows, use Ctrl+C or the /stop endpoint.`,
return caddy.ExitCodeFailedStartup, },
fmt.Errorf("caddy process exited with error: %v", err)
} "reload": {
Name: "reload",
return caddy.ExitCodeSuccess, nil Func: cmdReload,
} Usage: "--config <path> [--config-adapter <name>] [--address <interface>]",
Short: "Gives the running Caddy instance a new configuration",
func cmdRun() (int, error) { Long: `Gives the running Caddy instance a new configuration. This has the same effect
runCmd := flag.NewFlagSet("run", flag.ExitOnError) as POSTing a document to the /load endpoint, but is convenient for simple
runCmdConfigFlag := runCmd.String("config", "", "Configuration file") workflows revolving around config files. Since the admin endpoint is
runCmdConfigAdapterFlag := runCmd.String("config-adapter", "", "Name of config adapter to apply") configurable, the endpoint configuration is loaded from the --address flag if
runCmdPrintEnvFlag := runCmd.Bool("print-env", false, "Print environment") specified; otherwise it is loaded from the given config file; otherwise the
runCmdPingbackFlag := runCmd.String("pingback", "", "Echo confirmation bytes to this address on success") default is assumed.`,
runCmd.Parse(os.Args[2:]) Flags: func() *flag.FlagSet {
fs := flag.NewFlagSet("load", flag.ExitOnError)
// if we are supposed to print the environment, do that first fs.String("config", "", "Configuration file")
if *runCmdPrintEnvFlag { fs.String("config-adapter", "", "Name of config adapter to apply")
exitCode, err := cmdEnviron() fs.String("address", "", "Address of the administration listener, if different from config")
if err != nil { return fs
return exitCode, err }(),
} },
}
"version": {
// get the config in caddy's native format Name: "version",
config, err := loadConfig(*runCmdConfigFlag, *runCmdConfigAdapterFlag) Func: cmdVersion,
if err != nil { Short: "Prints the version.",
return caddy.ExitCodeFailedStartup, err Long: `Prints the version.`,
} },
// set a fitting User-Agent for ACME requests "list-modules": {
goModule := caddy.GoModule() Name: "list-modules",
cleanModVersion := strings.TrimPrefix(goModule.Version, "v") Func: cmdListModules,
certmagic.UserAgent = "Caddy/" + cleanModVersion Short: "List installed Caddy modules.",
Long: `List installed Caddy modules.`,
// start the admin endpoint along with any initial config },
err = caddy.StartAdmin(config)
if err != nil { "environ": {
return caddy.ExitCodeFailedStartup, Name: "environ",
fmt.Errorf("starting caddy administration endpoint: %v", err) Func: cmdEnviron,
} Short: "Prints the environment as seen by Caddy.",
defer caddy.StopAdmin() Long: `Prints the environment as seen by Caddy.`,
},
// if we are to report to another process the successful start
// of the server, do so now by echoing back contents of stdin "adapt-config": {
if *runCmdPingbackFlag != "" { Name: "adapt-config",
confirmationBytes, err := ioutil.ReadAll(os.Stdin) Func: cmdAdaptConfig,
if err != nil { Usage: "--input <path> --adapter <name> [--pretty]",
return caddy.ExitCodeFailedStartup, Short: "Adapts a configuration to Caddy's native JSON config structure",
fmt.Errorf("reading confirmation bytes from stdin: %v", err) Long: `
} Adapts a configuration to Caddy's native JSON config structure and writes the
conn, err := net.Dial("tcp", *runCmdPingbackFlag) output to stdout, along with any warnings to stderr. If --pretty is specified,
if err != nil { the output will be formatted with indentation for human readability.`,
return caddy.ExitCodeFailedStartup, },
fmt.Errorf("dialing confirmation address: %v", err) }
}
defer conn.Close() func init() {
_, err = conn.Write(confirmationBytes) // the help command is special in that its func
if err != nil { // refers to the commands map; thus, defining it
return caddy.ExitCodeFailedStartup, // inline with the commands map's initialization
fmt.Errorf("writing confirmation bytes to %s: %v", *runCmdPingbackFlag, err) // yields a compile-time error, so we have to
} // define this command separately
} commands["help"] = Command{
Name: "help",
select {} Func: cmdHelp,
} Usage: "<command>",
Short: "Shows help for a Caddy subcommand.",
func cmdStop() (int, error) { }
processList, err := ps.Processes() }
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("listing processes: %v", err) // RegisterCommand registers the command cmd.
} // cmd.Name must be unique and conform to the
thisProcName := getProcessName() // following format:
var found bool //
for _, p := range processList { // - lowercase
// the process we're looking for should have the same name but different PID // - alphanumeric and hyphen characters only
if p.Executable() == thisProcName && p.Pid() != os.Getpid() { // - cannot start or end with a hyphen
found = true // - hyphen cannot be adjacent to another hyphen
fmt.Printf("pid=%d\n", p.Pid()) //
// This function panics if the name is already registered,
if err := gracefullyStopProcess(p.Pid()); err != nil { // if the name does not meet the described format, or if
return caddy.ExitCodeFailedStartup, err // any of the fields are missing from cmd.
} func RegisterCommand(cmd Command) {
} if cmd.Name == "" {
} panic("command name is required")
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)")
}
// 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() (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)
} }
if cmd.Func == nil {
input, err := ioutil.ReadFile(*adaptCmdInputFlag) panic("command function missing")
if err != nil {
return caddy.ExitCodeFailedStartup,
fmt.Errorf("reading input file: %v", err)
} }
if cmd.Short == "" {
opts := make(map[string]interface{}) panic("command short string is required")
if *adaptCmdPrettyFlag {
opts["pretty"] = "true"
} }
if _, exists := commands[cmd.Name]; exists {
adaptedConfig, warnings, err := cfgAdapter.Adapt(input, opts) panic("command already registered: " + cmd.Name)
if err != nil {
return caddy.ExitCodeFailedStartup, err
} }
if !commandNameRegex.MatchString(cmd.Name) {
// print warnings to stderr panic("invalid command name")
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) commands[cmd.Name] = cmd
} }
// print result to stdout var commandNameRegex = regexp.MustCompile(`^[a-z0-9]$|^([a-z0-9]+-?[a-z0-9]*)+[a-z0-9]$`)
fmt.Println(string(adaptedConfig))
return caddy.ExitCodeSuccess, nil
}

128
cmd/main.go

@ -23,6 +23,9 @@ import (
"log" "log"
"net" "net"
"os" "os"
"strconv"
"strings"
"time"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig"
@ -33,45 +36,43 @@ import (
func Main() { func Main() {
caddy.TrapSignals() caddy.TrapSignals()
if len(os.Args) < 2 { switch len(os.Args) {
fmt.Println(usageString()) case 0:
return 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 { 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) os.Exit(caddy.ExitCodeFailedStartup)
} }
if exitCode, err := subcommand(); err != nil { fs := subcommand.Flags
log.Println(err) if fs == nil {
os.Exit(exitCode) fs = flag.NewFlagSet(subcommand.Name, flag.ExitOnError)
}
} }
// commandFunc is a function that executes err := fs.Parse(os.Args[2:])
// a subcommand. It returns an exit code and if err != nil {
// any associated error. log.Println(err)
type commandFunc func() (int, error) os.Exit(caddy.ExitCodeFailedStartup)
}
var commands = map[string]commandFunc{ exitCode, err := subcommand.Func(Flags{fs})
"start": cmdStart, if err != nil {
"run": cmdRun, log.Printf("%s: %v", subcommand.Name, err)
"stop": cmdStop,
"reload": cmdReload,
"version": cmdVersion,
"list-modules": cmdListModules,
"environ": cmdEnviron,
"adapt-config": cmdAdaptConfig,
} }
func usageString() string { os.Exit(exitCode)
buf := new(bytes.Buffer)
buf.WriteString("usage: caddy <command> [<args>]")
flag.CommandLine.SetOutput(buf)
flag.CommandLine.PrintDefaults()
return buf.String()
} }
// handlePingbackConn reads from conn and ensures it matches // handlePingbackConn reads from conn and ensures it matches
@ -156,3 +157,74 @@ func loadConfig(configFile, adapterName string) ([]byte, error) {
return config, nil 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