Browse Source
			
			
			
			
				
		* 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 pluggablemaster
							committed by
							
								 Matt Holt
								Matt Holt
							
						
					
				
				 3 changed files with 671 additions and 347 deletions
			
			
		| @ -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 | |||
| } | |||
					Loading…
					
					
				
		Reference in new issue