Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 14, 05:15 UTC
caddycmd: Add upgrade command (#3972)
Replaces the current Caddy executable with a new one from the build server. Honors custom builds, as long as plugins are registered on the Caddy website. Requires permissions to replace current executable, of course. This is an experimental command that may get changed or removed later.
Matt Holt committed
Jan 19, 2021 at 18:45 UTC
3366384d9347447632ac334ffbbe35fb18738b90
3 files changed
+208
-50
cmd/commandfuncs.go
+197
-50
@@ -25,6 +25,7 @@ import (
25
"log"
26
"net"
27
"net/http"
28
+ "net/url"
29
"os"
30
"os/exec"
31
"reflect"
@@ -364,11 +365,6 @@ func cmdListModules(fl Flags) (int, error) {
365
packages := fl.Bool("packages")
366
versions := fl.Bool("versions")
367
367
- type moduleInfo struct {
368
- caddyModuleID string
369
- goModule *debug.Module
370
- err error
371
- }
368
printModuleInfo := func(mi moduleInfo) {
369
fmt.Print(mi.caddyModuleID)
370
if versions && mi.goModule != nil {
@@ -387,10 +383,8 @@ func cmdListModules(fl Flags) (int, error) {
383
}
384
385
// organize modules by whether they come with the standard distribution
390
- var standard, nonstandard, unknown []moduleInfo
391
-
392
- bi, ok := debug.ReadBuildInfo()
393
- if !ok {
386
+ standard, nonstandard, unknown, err := getModules()
387
+ if err != nil {
388
// oh well, just print the module IDs and exit
389
for _, m := range caddy.Modules() {
390
fmt.Println(m)
@@ -398,47 +392,6 @@ func cmdListModules(fl Flags) (int, error) {
392
return caddy.ExitCodeSuccess, nil
393
}
394
401
- for _, modID := range caddy.Modules() {
402
- modInfo, err := caddy.GetModule(modID)
403
- if err != nil {
404
- // that's weird, shouldn't happen
405
- unknown = append(unknown, moduleInfo{caddyModuleID: modID, err: err})
406
- continue
407
- }
408
-
409
- // to get the Caddy plugin's version info, we need to know
410
- // the package that the Caddy module's value comes from; we
411
- // can use reflection but we need a non-pointer value (I'm
412
- // not sure why), and since New() should return a pointer
413
- // value, we need to dereference it first
414
- iface := interface{}(modInfo.New())
415
- if rv := reflect.ValueOf(iface); rv.Kind() == reflect.Ptr {
416
- iface = reflect.New(reflect.TypeOf(iface).Elem()).Elem().Interface()
417
- }
418
- modPkgPath := reflect.TypeOf(iface).PkgPath()
419
-
420
- // now we find the Go module that the Caddy module's package
421
- // belongs to; we assume the Caddy module package path will
422
- // be prefixed by its Go module path, and we will choose the
423
- // longest matching prefix in case there are nested modules
424
- var matched *debug.Module
425
- for _, dep := range bi.Deps {
426
- if strings.HasPrefix(modPkgPath, dep.Path) {
427
- if matched == nil || len(dep.Path) > len(matched.Path) {
428
- matched = dep
429
- }
430
- }
431
- }
432
-
433
- caddyModGoMod := moduleInfo{caddyModuleID: modID, goModule: matched}
434
-
435
- if strings.HasPrefix(modPkgPath, caddy.ImportPath) {
436
- standard = append(standard, caddyModGoMod)
437
- } else {
438
- nonstandard = append(nonstandard, caddyModGoMod)
439
- }
440
- }
441
-
395
if len(standard) > 0 {
396
for _, mod := range standard {
397
printModuleInfo(mod)
@@ -619,6 +572,144 @@ func cmdFmt(fl Flags) (int, error) {
572
return caddy.ExitCodeSuccess, nil
573
}
574
575
+func cmdUpgrade(_ Flags) (int, error) {
576
+ l := caddy.Log()
577
+
578
+ thisExecPath, err := os.Executable()
579
+ if err != nil {
580
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("determining current executable path: %v", err)
581
+ }
582
+ l.Info("this executable will be replaced", zap.String("path", thisExecPath))
583
+
584
+ // get the list of nonstandard plugins
585
+ _, nonstandard, _, err := getModules()
586
+ if err != nil {
587
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("unable to enumerate installed plugins: %v", err)
588
+ }
589
+ pluginPkgs := make(map[string]struct{})
590
+ for _, mod := range nonstandard {
591
+ if mod.goModule.Replace != nil {
592
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("cannot auto-upgrade when Go module has been replaced: %s => %s",
593
+ mod.goModule.Path, mod.goModule.Replace.Path)
594
+ }
595
+ l.Info("found non-standard module",
596
+ zap.String("id", mod.caddyModuleID),
597
+ zap.String("package", mod.goModule.Path))
598
+ pluginPkgs[mod.goModule.Path] = struct{}{}
599
+ }
600
+
601
+ // build the request URL to download this custom build
602
+ qs := url.Values{
603
+ "os": {runtime.GOOS},
604
+ "arch": {runtime.GOARCH},
605
+ }
606
+ for pkg := range pluginPkgs {
607
+ qs.Add("p", pkg)
608
+ }
609
+ urlStr := fmt.Sprintf("https://caddyserver.com/api/download?%s", qs.Encode())
610
+
611
+ // initiate the build
612
+ l.Info("requesting build",
613
+ zap.String("os", qs.Get("os")),
614
+ zap.String("arch", qs.Get("arch")),
615
+ zap.Strings("packages", qs["p"]))
616
+ resp, err := http.Get(urlStr)
617
+ if err != nil {
618
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("secure request failed: %v", err)
619
+ }
620
+ defer resp.Body.Close()
621
+ if resp.StatusCode >= 400 {
622
+ var details struct {
623
+ StatusCode int `json:"status_code"`
624
+ Error struct {
625
+ Message string `json:"message"`
626
+ ID string `json:"id"`
627
+ } `json:"error"`
628
+ }
629
+ err2 := json.NewDecoder(resp.Body).Decode(&details)
630
+ if err2 != nil {
631
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("download and error decoding failed: HTTP %d: %v", resp.StatusCode, err2)
632
+ }
633
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("download failed: HTTP %d: %s (id=%s)", resp.StatusCode, details.Error.Message, details.Error.ID)
634
+ }
635
+
636
+ // back up the current binary, in case something goes wrong we can replace it
637
+ backupExecPath := thisExecPath + ".tmp"
638
+ l.Info("build acquired; backing up current executable",
639
+ zap.String("current_path", thisExecPath),
640
+ zap.String("backup_path", backupExecPath))
641
+ err = os.Rename(thisExecPath, backupExecPath)
642
+ if err != nil {
643
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("backing up current binary: %v", err)
644
+ }
645
+ defer func() {
646
+ if err != nil {
647
+ err2 := os.Rename(backupExecPath, thisExecPath)
648
+ if err2 != nil {
649
+ l.Error("restoring original executable failed; will need to be restored manually",
650
+ zap.String("backup_path", backupExecPath),
651
+ zap.String("original_path", thisExecPath),
652
+ zap.Error(err2))
653
+ }
654
+ }
655
+ }()
656
+
657
+ // download the file; do this in a closure to close reliably before we execute it
658
+ writeFile := func() error {
659
+ destFile, err := os.OpenFile(thisExecPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0770)
660
+ if err != nil {
661
+ return fmt.Errorf("unable to open destination file: %v", err)
662
+ }
663
+ defer destFile.Close()
664
+
665
+ l.Info("downloading binary", zap.String("source", urlStr), zap.String("destination", thisExecPath))
666
+ _, err = io.Copy(destFile, resp.Body)
667
+ if err != nil {
668
+ return fmt.Errorf("unable to download file: %v", err)
669
+ }
670
+
671
+ err = destFile.Sync()
672
+ if err != nil {
673
+ return fmt.Errorf("syncing downloaded file to device: %v", err)
674
+ }
675
+
676
+ return nil
677
+ }
678
+ err = writeFile()
679
+ if err != nil {
680
+ return caddy.ExitCodeFailedStartup, err
681
+ }
682
+
683
+ // use the new binary to print out version and module info
684
+ fmt.Print("\nModule versions:\n\n")
685
+ cmd := exec.Command(thisExecPath, "list-modules", "--versions")
686
+ cmd.Stdout = os.Stdout
687
+ cmd.Stderr = os.Stderr
688
+ err = cmd.Run()
689
+ if err != nil {
690
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to execute: %v", err)
691
+ }
692
+ fmt.Println("\nVersion:")
693
+ cmd = exec.Command(thisExecPath, "version")
694
+ cmd.Stdout = os.Stdout
695
+ cmd.Stderr = os.Stderr
696
+ err = cmd.Run()
697
+ if err != nil {
698
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to execute: %v", err)
699
+ }
700
+ fmt.Println()
701
+
702
+ // clean up the backup file
703
+ err = os.Remove(backupExecPath)
704
+ if err != nil {
705
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to clean up backup binary: %v", err)
706
+ }
707
+
708
+ l.Info("upgrade successful; please restart any running Caddy instances", zap.String("executable", thisExecPath))
709
+
710
+ return caddy.ExitCodeSuccess, nil
711
+}
712
+
713
func cmdHelp(fl Flags) (int, error) {
714
const fullDocs = `Full documentation is available at:
715
https://caddyserver.com/docs/command-line`
@@ -683,6 +774,56 @@ commands:
774
return caddy.ExitCodeSuccess, nil
775
}
776
777
+func getModules() (standard, nonstandard, unknown []moduleInfo, err error) {
778
+ bi, ok := debug.ReadBuildInfo()
779
+ if !ok {
780
+ err = fmt.Errorf("no build info")
781
+ return
782
+ }
783
+
784
+ for _, modID := range caddy.Modules() {
785
+ modInfo, err := caddy.GetModule(modID)
786
+ if err != nil {
787
+ // that's weird, shouldn't happen
788
+ unknown = append(unknown, moduleInfo{caddyModuleID: modID, err: err})
789
+ continue
790
+ }
791
+
792
+ // to get the Caddy plugin's version info, we need to know
793
+ // the package that the Caddy module's value comes from; we
794
+ // can use reflection but we need a non-pointer value (I'm
795
+ // not sure why), and since New() should return a pointer
796
+ // value, we need to dereference it first
797
+ iface := interface{}(modInfo.New())
798
+ if rv := reflect.ValueOf(iface); rv.Kind() == reflect.Ptr {
799
+ iface = reflect.New(reflect.TypeOf(iface).Elem()).Elem().Interface()
800
+ }
801
+ modPkgPath := reflect.TypeOf(iface).PkgPath()
802
+
803
+ // now we find the Go module that the Caddy module's package
804
+ // belongs to; we assume the Caddy module package path will
805
+ // be prefixed by its Go module path, and we will choose the
806
+ // longest matching prefix in case there are nested modules
807
+ var matched *debug.Module
808
+ for _, dep := range bi.Deps {
809
+ if strings.HasPrefix(modPkgPath, dep.Path) {
810
+ if matched == nil || len(dep.Path) > len(matched.Path) {
811
+ matched = dep
812
+ }
813
+ }
814
+ }
815
+
816
+ caddyModGoMod := moduleInfo{caddyModuleID: modID, goModule: matched}
817
+
818
+ if strings.HasPrefix(modPkgPath, caddy.ImportPath) {
819
+ standard = append(standard, caddyModGoMod)
820
+ } else {
821
+ nonstandard = append(nonstandard, caddyModGoMod)
822
+ }
823
+ }
824
+ return
825
+}
826
+
827
// apiRequest makes an API request to the endpoint adminAddr with the
828
// given HTTP method and request URI. If body is non-nil, it will be
829
// assumed to be Content-Type application/json.
@@ -755,3 +896,9 @@ func apiRequest(adminAddr, method, uri string, body io.Reader) error {
896
897
return nil
898
}
899
+
900
+type moduleInfo struct {
901
+ caddyModuleID string
902
+ goModule *debug.Module
903
+ err error
904
+}
cmd/commands.go
+9
@@ -277,6 +277,15 @@ is always printed to stdout.`,
277
}(),
278
})
279
280
+ RegisterCommand(Command{
281
+ Name: "upgrade",
282
+ Func: cmdUpgrade,
283
+ Short: "Upgrade Caddy (EXPERIMENTAL)",
284
+ Long: `
285
+Downloads an updated Caddy binary with the same modules/plugins at the
286
+latest versions. EXPERIMENTAL: May be changed or removed.`,
287
+ })
288
+
289
}
290
291
// RegisterCommand registers the command cmd.
cmd/notify.go
+2
@@ -14,10 +14,12 @@
14
15
package caddycmd
16
17
+// NotifyReadiness notifies process manager of readiness.
18
func NotifyReadiness() error {
19
return notifyReadiness()
20
}
21
22
+// NotifyReloading notifies process manager of reloading.
23
func NotifyReloading() error {
24
return notifyReloading()
25
}