mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-26 05:33:49 +03:00
Caddy 2 gets a CLI! And admin endpoint is now configurable via JSON
This commit is contained in:
parent
006dc1792f
commit
a4bdf249db
8 changed files with 374 additions and 30 deletions
51
admin.go
51
admin.go
|
@ -22,12 +22,47 @@ var (
|
|||
cfgEndptSrvMu sync.Mutex
|
||||
)
|
||||
|
||||
// StartAdmin starts Caddy's administration endpoint.
|
||||
func StartAdmin(addr string) error {
|
||||
// AdminConfig configures the admin endpoint.
|
||||
type AdminConfig struct {
|
||||
Listen string `json:"listen,omitempty"`
|
||||
}
|
||||
|
||||
// DefaultAdminConfig is the default configuration
|
||||
// for the administration endpoint.
|
||||
var DefaultAdminConfig = &AdminConfig{
|
||||
Listen: "localhost:2019",
|
||||
}
|
||||
|
||||
// StartAdmin starts Caddy's administration endpoint,
|
||||
// bootstrapping it with an optional configuration
|
||||
// in the format of JSON bytes. It opens a listener
|
||||
// resource. When no longer needed, StopAdmin should
|
||||
// be called.
|
||||
func StartAdmin(initialConfigJSON []byte) error {
|
||||
cfgEndptSrvMu.Lock()
|
||||
defer cfgEndptSrvMu.Unlock()
|
||||
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
adminConfig := DefaultAdminConfig
|
||||
if len(initialConfigJSON) > 0 {
|
||||
var config *Config
|
||||
err := json.Unmarshal(initialConfigJSON, &config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unmarshaling bootstrap config: %v", err)
|
||||
}
|
||||
if config != nil && config.Admin != nil {
|
||||
adminConfig = config.Admin
|
||||
}
|
||||
if cfgEndptSrv != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
err := cfgEndptSrv.Shutdown(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("shutting down old admin endpoint: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp", adminConfig.Listen)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -60,6 +95,16 @@ func StartAdmin(addr string) error {
|
|||
|
||||
go cfgEndptSrv.Serve(ln)
|
||||
|
||||
log.Println("Caddy 2 admin endpoint listening on", adminConfig.Listen)
|
||||
|
||||
if len(initialConfigJSON) > 0 {
|
||||
err := Load(bytes.NewReader(initialConfigJSON))
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading initial config: %v", err)
|
||||
}
|
||||
log.Println("Caddy 2 serving initial configuration")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
2
caddy.go
2
caddy.go
|
@ -136,6 +136,8 @@ type App interface {
|
|||
|
||||
// Config represents a Caddy configuration.
|
||||
type Config struct {
|
||||
Admin *AdminConfig `json:"admin,omitempty"`
|
||||
|
||||
StorageRaw json.RawMessage `json:"storage,omitempty"`
|
||||
storage certmagic.Storage
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
|
||||
// this is where modules get plugged in
|
||||
_ "github.com/caddyserver/caddy/modules/caddyhttp"
|
||||
_ "github.com/caddyserver/caddy/modules/caddyhttp/caddylog"
|
||||
_ "github.com/caddyserver/caddy/modules/caddyhttp/encode"
|
||||
_ "github.com/caddyserver/caddy/modules/caddyhttp/encode/brotli"
|
||||
_ "github.com/caddyserver/caddy/modules/caddyhttp/encode/gzip"
|
||||
|
|
205
cmd/commands.go
Normal file
205
cmd/commands.go
Normal file
|
@ -0,0 +1,205 @@
|
|||
package caddycmd
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/mitchellh/go-ps"
|
||||
)
|
||||
|
||||
func cmdStart() (int, error) {
|
||||
startCmd := flag.NewFlagSet("start", flag.ExitOnError)
|
||||
startCmdConfigFlag := startCmd.String("config", "", "Configuration file")
|
||||
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 1, 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)
|
||||
}
|
||||
stdinpipe, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return 1, 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 1, 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 1, 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 {
|
||||
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.Println("Successfully started Caddy")
|
||||
case err := <-exit:
|
||||
return 1, fmt.Errorf("caddy process exited with error: %v", err)
|
||||
}
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func cmdRun() (int, error) {
|
||||
runCmd := flag.NewFlagSet("run", flag.ExitOnError)
|
||||
runCmdConfigFlag := runCmd.String("config", "", "Configuration file")
|
||||
runCmdPingbackFlag := runCmd.String("pingback", "", "Echo confirmation bytes to this address on success")
|
||||
runCmd.Parse(os.Args[2:])
|
||||
|
||||
// if a config file was specified for bootstrapping
|
||||
// the server instance, load it now
|
||||
var config []byte
|
||||
if *runCmdConfigFlag != "" {
|
||||
var err error
|
||||
config, err = ioutil.ReadFile(*runCmdConfigFlag)
|
||||
if err != nil {
|
||||
return 1, fmt.Errorf("reading config file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// start the admin endpoint along with any initial config
|
||||
err := caddy.StartAdmin(config)
|
||||
if err != nil {
|
||||
return 0, 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 1, fmt.Errorf("reading confirmation bytes from stdin: %v", err)
|
||||
}
|
||||
conn, err := net.Dial("tcp", *runCmdPingbackFlag)
|
||||
if err != nil {
|
||||
return 1, fmt.Errorf("dialing confirmation address: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
_, err = conn.Write(confirmationBytes)
|
||||
if err != nil {
|
||||
return 1, fmt.Errorf("writing confirmation bytes to %s: %v", *runCmdPingbackFlag, err)
|
||||
}
|
||||
}
|
||||
|
||||
select {}
|
||||
}
|
||||
|
||||
func cmdStop() (int, error) {
|
||||
processList, err := ps.Processes()
|
||||
if err != nil {
|
||||
return 1, fmt.Errorf("listing processes: %v", err)
|
||||
}
|
||||
thisProcName := filepath.Base(os.Args[0])
|
||||
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())
|
||||
fmt.Printf("Graceful stop...")
|
||||
if err := gracefullyStopProcess(p.Pid()); err != nil {
|
||||
return 1, err
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return 1, fmt.Errorf("Caddy is not running")
|
||||
}
|
||||
fmt.Println(" success")
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func cmdVersion() (int, error) {
|
||||
goModule := getGoBuildModule()
|
||||
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 0, nil
|
||||
}
|
||||
|
||||
func cmdListModules() (int, error) {
|
||||
for _, m := range caddy.Modules() {
|
||||
fmt.Println(m)
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func cmdEnviron() (int, error) {
|
||||
for _, v := range os.Environ() {
|
||||
fmt.Println(v)
|
||||
}
|
||||
return 0, nil
|
||||
}
|
88
cmd/main.go
Normal file
88
cmd/main.go
Normal file
|
@ -0,0 +1,88 @@
|
|||
package caddycmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
)
|
||||
|
||||
// Main executes the main function of the caddy command.
|
||||
func Main() {
|
||||
if len(os.Args) <= 1 {
|
||||
fmt.Println(usageString())
|
||||
return
|
||||
}
|
||||
|
||||
subcommand, ok := commands[os.Args[1]]
|
||||
if !ok {
|
||||
fmt.Printf("%q is not a valid command\n", os.Args[1])
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
if exitCode, err := subcommand(); err != nil {
|
||||
log.Println(err)
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
"stop": cmdStop,
|
||||
"run": cmdRun,
|
||||
"version": cmdVersion,
|
||||
"list-modules": cmdListModules,
|
||||
"environ": cmdEnviron,
|
||||
}
|
||||
|
||||
func usageString() string {
|
||||
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
|
||||
// the bytes in expect, or returns an error if it doesn't.
|
||||
func handlePingbackConn(conn net.Conn, expect []byte) error {
|
||||
defer conn.Close()
|
||||
confirmationBytes, err := ioutil.ReadAll(io.LimitReader(conn, 32))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !bytes.Equal(confirmationBytes, expect) {
|
||||
return fmt.Errorf("wrong confirmation: %x", confirmationBytes)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getGoBuildModule returns the build info of Caddy
|
||||
// from debug.BuildInfo (requires Go modules). If
|
||||
// no version information is available, a non-nil
|
||||
// value will still be returned, but with an
|
||||
// unknown version.
|
||||
func getGoBuildModule() *debug.Module {
|
||||
bi, ok := debug.ReadBuildInfo()
|
||||
if ok {
|
||||
// The recommended way to build Caddy involves
|
||||
// creating a separate main module, which
|
||||
// TODO: track related Go issue: https://github.com/golang/go/issues/29228
|
||||
for _, mod := range bi.Deps {
|
||||
if mod.Path == "github.com/mholt/caddy" {
|
||||
return mod
|
||||
}
|
||||
}
|
||||
}
|
||||
return &debug.Module{Version: "unknown"}
|
||||
}
|
16
cmd/proc_posix.go
Normal file
16
cmd/proc_posix.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
// +build !windows
|
||||
|
||||
package caddycmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func gracefullyStopProcess(pid int) error {
|
||||
err := syscall.Kill(pid, syscall.SIGINT)
|
||||
if err != nil {
|
||||
return fmt.Errorf("kill: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
15
cmd/proc_windows.go
Normal file
15
cmd/proc_windows.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
package caddycmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func gracefullyStopProcess(pid int) error {
|
||||
cmd := exec.Command("taskkill", "/pid", strconv.Itoa(pid))
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("taskkill: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
26
cmd/run.go
26
cmd/run.go
|
@ -1,26 +0,0 @@
|
|||
package caddycmd
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
|
||||
"github.com/caddyserver/caddy"
|
||||
)
|
||||
|
||||
// Main executes the main function of the caddy command.
|
||||
func Main() {
|
||||
flag.Parse()
|
||||
|
||||
err := caddy.StartAdmin(*listenAddr)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer caddy.StopAdmin()
|
||||
|
||||
log.Println("Caddy 2 admin endpoint listening on", *listenAddr)
|
||||
|
||||
select {}
|
||||
}
|
||||
|
||||
// TODO: for dev only
|
||||
var listenAddr = flag.String("listen", ":1234", "The admin endpoint listener address")
|
Loading…
Reference in a new issue