summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd/commandfuncs.go247
-rw-r--r--cmd/commands.go9
-rw-r--r--cmd/notify.go2
3 files changed, 208 insertions, 50 deletions
diff --git a/cmd/commandfuncs.go b/cmd/commandfuncs.go
index 3bf4b8d..9640f4b 100644
--- a/cmd/commandfuncs.go
+++ b/cmd/commandfuncs.go
@@ -25,6 +25,7 @@ import (
"log"
"net"
"net/http"
+ "net/url"
"os"
"os/exec"
"reflect"
@@ -364,11 +365,6 @@ func cmdListModules(fl Flags) (int, error) {
packages := fl.Bool("packages")
versions := fl.Bool("versions")
- type moduleInfo struct {
- caddyModuleID string
- goModule *debug.Module
- err error
- }
printModuleInfo := func(mi moduleInfo) {
fmt.Print(mi.caddyModuleID)
if versions && mi.goModule != nil {
@@ -387,10 +383,8 @@ func cmdListModules(fl Flags) (int, error) {
}
// organize modules by whether they come with the standard distribution
- var standard, nonstandard, unknown []moduleInfo
-
- bi, ok := debug.ReadBuildInfo()
- if !ok {
+ standard, nonstandard, unknown, err := getModules()
+ if err != nil {
// oh well, just print the module IDs and exit
for _, m := range caddy.Modules() {
fmt.Println(m)
@@ -398,47 +392,6 @@ func cmdListModules(fl Flags) (int, error) {
return caddy.ExitCodeSuccess, nil
}
- for _, modID := range caddy.Modules() {
- modInfo, err := caddy.GetModule(modID)
- if err != nil {
- // that's weird, shouldn't happen
- unknown = append(unknown, moduleInfo{caddyModuleID: modID, err: err})
- continue
- }
-
- // to get the Caddy plugin's version info, we need to know
- // the package that the Caddy module's value comes from; we
- // can use reflection but we need a non-pointer value (I'm
- // not sure why), and since New() should return a pointer
- // value, we need to dereference it first
- iface := interface{}(modInfo.New())
- if rv := reflect.ValueOf(iface); rv.Kind() == reflect.Ptr {
- iface = reflect.New(reflect.TypeOf(iface).Elem()).Elem().Interface()
- }
- modPkgPath := reflect.TypeOf(iface).PkgPath()
-
- // now we find the Go module that the Caddy module's package
- // belongs to; we assume the Caddy module package path will
- // be prefixed by its Go module path, and we will choose the
- // longest matching prefix in case there are nested modules
- var matched *debug.Module
- for _, dep := range bi.Deps {
- if strings.HasPrefix(modPkgPath, dep.Path) {
- if matched == nil || len(dep.Path) > len(matched.Path) {
- matched = dep
- }
- }
- }
-
- caddyModGoMod := moduleInfo{caddyModuleID: modID, goModule: matched}
-
- if strings.HasPrefix(modPkgPath, caddy.ImportPath) {
- standard = append(standard, caddyModGoMod)
- } else {
- nonstandard = append(nonstandard, caddyModGoMod)
- }
- }
-
if len(standard) > 0 {
for _, mod := range standard {
printModuleInfo(mod)
@@ -619,6 +572,144 @@ func cmdFmt(fl Flags) (int, error) {
return caddy.ExitCodeSuccess, nil
}
+func cmdUpgrade(_ Flags) (int, error) {
+ l := caddy.Log()
+
+ thisExecPath, err := os.Executable()
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("determining current executable path: %v", err)
+ }
+ l.Info("this executable will be replaced", zap.String("path", thisExecPath))
+
+ // get the list of nonstandard plugins
+ _, nonstandard, _, err := getModules()
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("unable to enumerate installed plugins: %v", err)
+ }
+ pluginPkgs := make(map[string]struct{})
+ for _, mod := range nonstandard {
+ if mod.goModule.Replace != nil {
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("cannot auto-upgrade when Go module has been replaced: %s => %s",
+ mod.goModule.Path, mod.goModule.Replace.Path)
+ }
+ l.Info("found non-standard module",
+ zap.String("id", mod.caddyModuleID),
+ zap.String("package", mod.goModule.Path))
+ pluginPkgs[mod.goModule.Path] = struct{}{}
+ }
+
+ // build the request URL to download this custom build
+ qs := url.Values{
+ "os": {runtime.GOOS},
+ "arch": {runtime.GOARCH},
+ }
+ for pkg := range pluginPkgs {
+ qs.Add("p", pkg)
+ }
+ urlStr := fmt.Sprintf("https://caddyserver.com/api/download?%s", qs.Encode())
+
+ // initiate the build
+ l.Info("requesting build",
+ zap.String("os", qs.Get("os")),
+ zap.String("arch", qs.Get("arch")),
+ zap.Strings("packages", qs["p"]))
+ resp, err := http.Get(urlStr)
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("secure request failed: %v", err)
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode >= 400 {
+ var details struct {
+ StatusCode int `json:"status_code"`
+ Error struct {
+ Message string `json:"message"`
+ ID string `json:"id"`
+ } `json:"error"`
+ }
+ err2 := json.NewDecoder(resp.Body).Decode(&details)
+ if err2 != nil {
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("download and error decoding failed: HTTP %d: %v", resp.StatusCode, err2)
+ }
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("download failed: HTTP %d: %s (id=%s)", resp.StatusCode, details.Error.Message, details.Error.ID)
+ }
+
+ // back up the current binary, in case something goes wrong we can replace it
+ backupExecPath := thisExecPath + ".tmp"
+ l.Info("build acquired; backing up current executable",
+ zap.String("current_path", thisExecPath),
+ zap.String("backup_path", backupExecPath))
+ err = os.Rename(thisExecPath, backupExecPath)
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("backing up current binary: %v", err)
+ }
+ defer func() {
+ if err != nil {
+ err2 := os.Rename(backupExecPath, thisExecPath)
+ if err2 != nil {
+ l.Error("restoring original executable failed; will need to be restored manually",
+ zap.String("backup_path", backupExecPath),
+ zap.String("original_path", thisExecPath),
+ zap.Error(err2))
+ }
+ }
+ }()
+
+ // download the file; do this in a closure to close reliably before we execute it
+ writeFile := func() error {
+ destFile, err := os.OpenFile(thisExecPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0770)
+ if err != nil {
+ return fmt.Errorf("unable to open destination file: %v", err)
+ }
+ defer destFile.Close()
+
+ l.Info("downloading binary", zap.String("source", urlStr), zap.String("destination", thisExecPath))
+ _, err = io.Copy(destFile, resp.Body)
+ if err != nil {
+ return fmt.Errorf("unable to download file: %v", err)
+ }
+
+ err = destFile.Sync()
+ if err != nil {
+ return fmt.Errorf("syncing downloaded file to device: %v", err)
+ }
+
+ return nil
+ }
+ err = writeFile()
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, err
+ }
+
+ // use the new binary to print out version and module info
+ fmt.Print("\nModule versions:\n\n")
+ cmd := exec.Command(thisExecPath, "list-modules", "--versions")
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ err = cmd.Run()
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to execute: %v", err)
+ }
+ fmt.Println("\nVersion:")
+ cmd = exec.Command(thisExecPath, "version")
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ err = cmd.Run()
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to execute: %v", err)
+ }
+ fmt.Println()
+
+ // clean up the backup file
+ err = os.Remove(backupExecPath)
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to clean up backup binary: %v", err)
+ }
+
+ l.Info("upgrade successful; please restart any running Caddy instances", zap.String("executable", thisExecPath))
+
+ return caddy.ExitCodeSuccess, nil
+}
+
func cmdHelp(fl Flags) (int, error) {
const fullDocs = `Full documentation is available at:
https://caddyserver.com/docs/command-line`
@@ -683,6 +774,56 @@ commands:
return caddy.ExitCodeSuccess, nil
}
+func getModules() (standard, nonstandard, unknown []moduleInfo, err error) {
+ bi, ok := debug.ReadBuildInfo()
+ if !ok {
+ err = fmt.Errorf("no build info")
+ return
+ }
+
+ for _, modID := range caddy.Modules() {
+ modInfo, err := caddy.GetModule(modID)
+ if err != nil {
+ // that's weird, shouldn't happen
+ unknown = append(unknown, moduleInfo{caddyModuleID: modID, err: err})
+ continue
+ }
+
+ // to get the Caddy plugin's version info, we need to know
+ // the package that the Caddy module's value comes from; we
+ // can use reflection but we need a non-pointer value (I'm
+ // not sure why), and since New() should return a pointer
+ // value, we need to dereference it first
+ iface := interface{}(modInfo.New())
+ if rv := reflect.ValueOf(iface); rv.Kind() == reflect.Ptr {
+ iface = reflect.New(reflect.TypeOf(iface).Elem()).Elem().Interface()
+ }
+ modPkgPath := reflect.TypeOf(iface).PkgPath()
+
+ // now we find the Go module that the Caddy module's package
+ // belongs to; we assume the Caddy module package path will
+ // be prefixed by its Go module path, and we will choose the
+ // longest matching prefix in case there are nested modules
+ var matched *debug.Module
+ for _, dep := range bi.Deps {
+ if strings.HasPrefix(modPkgPath, dep.Path) {
+ if matched == nil || len(dep.Path) > len(matched.Path) {
+ matched = dep
+ }
+ }
+ }
+
+ caddyModGoMod := moduleInfo{caddyModuleID: modID, goModule: matched}
+
+ if strings.HasPrefix(modPkgPath, caddy.ImportPath) {
+ standard = append(standard, caddyModGoMod)
+ } else {
+ nonstandard = append(nonstandard, caddyModGoMod)
+ }
+ }
+ return
+}
+
// apiRequest makes an API request to the endpoint adminAddr with the
// given HTTP method and request URI. If body is non-nil, it will be
// assumed to be Content-Type application/json.
@@ -755,3 +896,9 @@ func apiRequest(adminAddr, method, uri string, body io.Reader) error {
return nil
}
+
+type moduleInfo struct {
+ caddyModuleID string
+ goModule *debug.Module
+ err error
+}
diff --git a/cmd/commands.go b/cmd/commands.go
index e4a2b91..1765194 100644
--- a/cmd/commands.go
+++ b/cmd/commands.go
@@ -277,6 +277,15 @@ is always printed to stdout.`,
}(),
})
+ RegisterCommand(Command{
+ Name: "upgrade",
+ Func: cmdUpgrade,
+ Short: "Upgrade Caddy (EXPERIMENTAL)",
+ Long: `
+Downloads an updated Caddy binary with the same modules/plugins at the
+latest versions. EXPERIMENTAL: May be changed or removed.`,
+ })
+
}
// RegisterCommand registers the command cmd.
diff --git a/cmd/notify.go b/cmd/notify.go
index 920f2a2..21e0e69 100644
--- a/cmd/notify.go
+++ b/cmd/notify.go
@@ -14,10 +14,12 @@
package caddycmd
+// NotifyReadiness notifies process manager of readiness.
func NotifyReadiness() error {
return notifyReadiness()
}
+// NotifyReloading notifies process manager of reloading.
func NotifyReloading() error {
return notifyReloading()
}