mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-27 06:03:48 +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
|
cfgEndptSrvMu sync.Mutex
|
||||||
)
|
)
|
||||||
|
|
||||||
// StartAdmin starts Caddy's administration endpoint.
|
// AdminConfig configures the admin endpoint.
|
||||||
func StartAdmin(addr string) error {
|
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()
|
cfgEndptSrvMu.Lock()
|
||||||
defer cfgEndptSrvMu.Unlock()
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -60,6 +95,16 @@ func StartAdmin(addr string) error {
|
||||||
|
|
||||||
go cfgEndptSrv.Serve(ln)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
2
caddy.go
2
caddy.go
|
@ -136,6 +136,8 @@ type App interface {
|
||||||
|
|
||||||
// Config represents a Caddy configuration.
|
// Config represents a Caddy configuration.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
Admin *AdminConfig `json:"admin,omitempty"`
|
||||||
|
|
||||||
StorageRaw json.RawMessage `json:"storage,omitempty"`
|
StorageRaw json.RawMessage `json:"storage,omitempty"`
|
||||||
storage certmagic.Storage
|
storage certmagic.Storage
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,6 @@ import (
|
||||||
|
|
||||||
// this is where modules get plugged in
|
// this is where modules get plugged in
|
||||||
_ "github.com/caddyserver/caddy/modules/caddyhttp"
|
_ "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"
|
||||||
_ "github.com/caddyserver/caddy/modules/caddyhttp/encode/brotli"
|
_ "github.com/caddyserver/caddy/modules/caddyhttp/encode/brotli"
|
||||||
_ "github.com/caddyserver/caddy/modules/caddyhttp/encode/gzip"
|
_ "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