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