@marcel91200 / caddy Mirror · read-only / commits / 68c5c716
Fork 0

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

cmd: New `add-package` and `remove-package` commands (#4226)

* adding package command * add-package command name * refactoring duplicate code * fixed by review * fixed by review * remove-package command * commands in different files, common utils * fix add, remove, upgrade packages in 1 file * copyright and downloadPath moved * refactor * downloadPath do no export * adding/removing multiple packages * addPackages/removePackages, comments, command-desc * add-package, process case len(args) == 0 Co-authored-by: Francis Lavoie <lavofr@gmail.com>

Oleg committed Aug 12, 2021 at 02:31 UTC 68c5c71659109b10226f10873f7dc67102b9dc14
3 files changed +330 -197
cmd/commandfuncs.go
-197
@@ -25,10 +25,8 @@ import (
25 "log"
26 "net"
27 "net/http"
28 - "net/url"
28 "os"
29 "os/exec"
31 - "reflect"
30 "runtime"
31 "runtime/debug"
32 "sort"
@@ -570,151 +568,6 @@ func cmdFmt(fl Flags) (int, error) {
568 return caddy.ExitCodeSuccess, nil
569 }
570
573 -func cmdUpgrade(_ Flags) (int, error) {
574 - l := caddy.Log()
575 -
576 - thisExecPath, err := os.Executable()
577 - if err != nil {
578 - return caddy.ExitCodeFailedStartup, fmt.Errorf("determining current executable path: %v", err)
579 - }
580 - thisExecStat, err := os.Stat(thisExecPath)
581 - if err != nil {
582 - return caddy.ExitCodeFailedStartup, fmt.Errorf("retrieving current executable permission bits: %v", err)
583 - }
584 - l.Info("this executable will be replaced", zap.String("path", thisExecPath))
585 -
586 - // get the list of nonstandard plugins
587 - _, nonstandard, _, err := getModules()
588 - if err != nil {
589 - return caddy.ExitCodeFailedStartup, fmt.Errorf("unable to enumerate installed plugins: %v", err)
590 - }
591 - pluginPkgs := make(map[string]struct{})
592 - for _, mod := range nonstandard {
593 - if mod.goModule.Replace != nil {
594 - return caddy.ExitCodeFailedStartup, fmt.Errorf("cannot auto-upgrade when Go module has been replaced: %s => %s",
595 - mod.goModule.Path, mod.goModule.Replace.Path)
596 - }
597 - l.Info("found non-standard module",
598 - zap.String("id", mod.caddyModuleID),
599 - zap.String("package", mod.goModule.Path))
600 - pluginPkgs[mod.goModule.Path] = struct{}{}
601 - }
602 -
603 - // build the request URL to download this custom build
604 - qs := url.Values{
605 - "os": {runtime.GOOS},
606 - "arch": {runtime.GOARCH},
607 - }
608 - for pkg := range pluginPkgs {
609 - qs.Add("p", pkg)
610 - }
611 - urlStr := fmt.Sprintf("https://caddyserver.com/api/download?%s", qs.Encode())
612 -
613 - // initiate the build
614 - l.Info("requesting build",
615 - zap.String("os", qs.Get("os")),
616 - zap.String("arch", qs.Get("arch")),
617 - zap.Strings("packages", qs["p"]))
618 - resp, err := http.Get(urlStr)
619 - if err != nil {
620 - return caddy.ExitCodeFailedStartup, fmt.Errorf("secure request failed: %v", err)
621 - }
622 - defer resp.Body.Close()
623 - if resp.StatusCode >= 400 {
624 - var details struct {
625 - StatusCode int `json:"status_code"`
626 - Error struct {
627 - Message string `json:"message"`
628 - ID string `json:"id"`
629 - } `json:"error"`
630 - }
631 - err2 := json.NewDecoder(resp.Body).Decode(&details)
632 - if err2 != nil {
633 - return caddy.ExitCodeFailedStartup, fmt.Errorf("download and error decoding failed: HTTP %d: %v", resp.StatusCode, err2)
634 - }
635 - return caddy.ExitCodeFailedStartup, fmt.Errorf("download failed: HTTP %d: %s (id=%s)", resp.StatusCode, details.Error.Message, details.Error.ID)
636 - }
637 -
638 - // back up the current binary, in case something goes wrong we can replace it
639 - backupExecPath := thisExecPath + ".tmp"
640 - l.Info("build acquired; backing up current executable",
641 - zap.String("current_path", thisExecPath),
642 - zap.String("backup_path", backupExecPath))
643 - err = os.Rename(thisExecPath, backupExecPath)
644 - if err != nil {
645 - return caddy.ExitCodeFailedStartup, fmt.Errorf("backing up current binary: %v", err)
646 - }
647 - defer func() {
648 - if err != nil {
649 - err2 := os.Rename(backupExecPath, thisExecPath)
650 - if err2 != nil {
651 - l.Error("restoring original executable failed; will need to be restored manually",
652 - zap.String("backup_path", backupExecPath),
653 - zap.String("original_path", thisExecPath),
654 - zap.Error(err2))
655 - }
656 - }
657 - }()
658 -
659 - // download the file; do this in a closure to close reliably before we execute it
660 - writeFile := func() error {
661 - destFile, err := os.OpenFile(thisExecPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, thisExecStat.Mode())
662 - if err != nil {
663 - return fmt.Errorf("unable to open destination file: %v", err)
664 - }
665 - defer destFile.Close()
666 -
667 - l.Info("downloading binary", zap.String("source", urlStr), zap.String("destination", thisExecPath))
668 -
669 - _, err = io.Copy(destFile, resp.Body)
670 - if err != nil {
671 - return fmt.Errorf("unable to download file: %v", err)
672 - }
673 -
674 - err = destFile.Sync()
675 - if err != nil {
676 - return fmt.Errorf("syncing downloaded file to device: %v", err)
677 - }
678 -
679 - return nil
680 - }
681 - err = writeFile()
682 - if err != nil {
683 - return caddy.ExitCodeFailedStartup, err
684 - }
685 -
686 - l.Info("download successful; displaying new binary details", zap.String("location", thisExecPath))
687 -
688 - // use the new binary to print out version and module info
689 - fmt.Print("\nModule versions:\n\n")
690 - cmd := exec.Command(thisExecPath, "list-modules", "--versions")
691 - cmd.Stdout = os.Stdout
692 - cmd.Stderr = os.Stderr
693 - err = cmd.Run()
694 - if err != nil {
695 - return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to execute: %v", err)
696 - }
697 - fmt.Println("\nVersion:")
698 - cmd = exec.Command(thisExecPath, "version")
699 - cmd.Stdout = os.Stdout
700 - cmd.Stderr = os.Stderr
701 - err = cmd.Run()
702 - if err != nil {
703 - return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to execute: %v", err)
704 - }
705 - fmt.Println()
706 -
707 - // clean up the backup file
708 - err = os.Remove(backupExecPath)
709 - if err != nil {
710 - return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to clean up backup binary: %v", err)
711 - }
712 -
713 - l.Info("upgrade successful; please restart any running Caddy instances", zap.String("executable", thisExecPath))
714 -
715 - return caddy.ExitCodeSuccess, nil
716 -}
717 -
571 func cmdHelp(fl Flags) (int, error) {
572 const fullDocs = `Full documentation is available at:
573 https://caddyserver.com/docs/command-line`
@@ -779,56 +632,6 @@ commands:
632 return caddy.ExitCodeSuccess, nil
633 }
634
782 -func getModules() (standard, nonstandard, unknown []moduleInfo, err error) {
783 - bi, ok := debug.ReadBuildInfo()
784 - if !ok {
785 - err = fmt.Errorf("no build info")
786 - return
787 - }
788 -
789 - for _, modID := range caddy.Modules() {
790 - modInfo, err := caddy.GetModule(modID)
791 - if err != nil {
792 - // that's weird, shouldn't happen
793 - unknown = append(unknown, moduleInfo{caddyModuleID: modID, err: err})
794 - continue
795 - }
796 -
797 - // to get the Caddy plugin's version info, we need to know
798 - // the package that the Caddy module's value comes from; we
799 - // can use reflection but we need a non-pointer value (I'm
800 - // not sure why), and since New() should return a pointer
801 - // value, we need to dereference it first
802 - iface := interface{}(modInfo.New())
803 - if rv := reflect.ValueOf(iface); rv.Kind() == reflect.Ptr {
804 - iface = reflect.New(reflect.TypeOf(iface).Elem()).Elem().Interface()
805 - }
806 - modPkgPath := reflect.TypeOf(iface).PkgPath()
807 -
808 - // now we find the Go module that the Caddy module's package
809 - // belongs to; we assume the Caddy module package path will
810 - // be prefixed by its Go module path, and we will choose the
811 - // longest matching prefix in case there are nested modules
812 - var matched *debug.Module
813 - for _, dep := range bi.Deps {
814 - if strings.HasPrefix(modPkgPath, dep.Path) {
815 - if matched == nil || len(dep.Path) > len(matched.Path) {
816 - matched = dep
817 - }
818 - }
819 - }
820 -
821 - caddyModGoMod := moduleInfo{caddyModuleID: modID, goModule: matched}
822 -
823 - if strings.HasPrefix(modPkgPath, caddy.ImportPath) {
824 - standard = append(standard, caddyModGoMod)
825 - } else {
826 - nonstandard = append(nonstandard, caddyModGoMod)
827 - }
828 - }
829 - return
830 -}
831 -
635 // apiRequest makes an API request to the endpoint adminAddr with the
636 // given HTTP method and request URI. If body is non-nil, it will be
637 // assumed to be Content-Type application/json.
cmd/commands.go
+24
@@ -291,6 +291,30 @@ Downloads an updated Caddy binary with the same modules/plugins at the
291 latest versions. EXPERIMENTAL: May be changed or removed.`,
292 })
293
294 + RegisterCommand(Command{
295 + Name: "add-package",
296 + Func: cmdAddPackage,
297 + Usage: "<packages...>",
298 + Short: "Adds Caddy packages (EXPERIMENTAL)",
299 + Long: `
300 +Downloads an updated Caddy binary with the specified packages (module/plugin)
301 +added. Retains existing packages. Returns an error if the any of packages are
302 +already included. EXPERIMENTAL: May be changed or removed.
303 +`,
304 + })
305 +
306 + RegisterCommand(Command{
307 + Name: "remove-package",
308 + Func: cmdRemovePackage,
309 + Usage: "<packages...>",
310 + Short: "Removes Caddy packages (EXPERIMENTAL)",
311 + Long: `
312 +Downloads an updated Caddy binaries without the specified packages (module/plugin).
313 +Returns an error if any of the packages are not included.
314 +EXPERIMENTAL: May be changed or removed.
315 +`,
316 + })
317 +
318 }
319
320 // RegisterCommand registers the command cmd.
cmd/packagesfuncs.go new
+306
@@ -0,0 +1,306 @@
1 +// Copyright 2015 Matthew Holt and The Caddy Authors
2 +//
3 +// Licensed under the Apache License, Version 2.0 (the "License");
4 +// you may not use this file except in compliance with the License.
5 +// You may obtain a copy of the License at
6 +//
7 +// http://www.apache.org/licenses/LICENSE-2.0
8 +//
9 +// Unless required by applicable law or agreed to in writing, software
10 +// distributed under the License is distributed on an "AS IS" BASIS,
11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 +// See the License for the specific language governing permissions and
13 +// limitations under the License.
14 +
15 +package caddycmd
16 +
17 +import (
18 + "encoding/json"
19 + "fmt"
20 + "io"
21 + "net/http"
22 + "net/url"
23 + "os"
24 + "os/exec"
25 + "reflect"
26 + "runtime"
27 + "runtime/debug"
28 + "strings"
29 +
30 + "github.com/caddyserver/caddy/v2"
31 + "go.uber.org/zap"
32 +)
33 +
34 +func cmdUpgrade(_ Flags) (int, error) {
35 + _, nonstandard, _, err := getModules()
36 + if err != nil {
37 + return caddy.ExitCodeFailedStartup, fmt.Errorf("unable to enumerate installed plugins: %v", err)
38 + }
39 + pluginPkgs, err := getPluginPackages(nonstandard)
40 + if err != nil {
41 + return caddy.ExitCodeFailedStartup, err
42 + }
43 +
44 + return upgradeBuild(pluginPkgs)
45 +}
46 +
47 +func cmdAddPackage(fl Flags) (int, error) {
48 + if len(fl.Args()) == 0 {
49 + return caddy.ExitCodeFailedStartup, fmt.Errorf("at least one package name must be specified")
50 + }
51 + _, nonstandard, _, err := getModules()
52 + if err != nil {
53 + return caddy.ExitCodeFailedStartup, fmt.Errorf("unable to enumerate installed plugins: %v", err)
54 + }
55 + pluginPkgs, err := getPluginPackages(nonstandard)
56 + if err != nil {
57 + return caddy.ExitCodeFailedStartup, err
58 + }
59 +
60 + for _, arg := range fl.Args() {
61 + if _, ok := pluginPkgs[arg]; ok {
62 + return caddy.ExitCodeFailedStartup, fmt.Errorf("package is already added")
63 + }
64 + pluginPkgs[arg] = struct{}{}
65 + }
66 +
67 + return upgradeBuild(pluginPkgs)
68 +}
69 +
70 +func cmdRemovePackage(fl Flags) (int, error) {
71 + if len(fl.Args()) == 0 {
72 + return caddy.ExitCodeFailedStartup, fmt.Errorf("at least one package name must be specified")
73 + }
74 + _, nonstandard, _, err := getModules()
75 + if err != nil {
76 + return caddy.ExitCodeFailedStartup, fmt.Errorf("unable to enumerate installed plugins: %v", err)
77 + }
78 + pluginPkgs, err := getPluginPackages(nonstandard)
79 + if err != nil {
80 + return caddy.ExitCodeFailedStartup, err
81 + }
82 +
83 + for _, arg := range fl.Args() {
84 + if _, ok := pluginPkgs[arg]; !ok {
85 + // package does not exist
86 + return caddy.ExitCodeFailedStartup, fmt.Errorf("package is not added")
87 + }
88 + delete(pluginPkgs, arg)
89 + }
90 +
91 + return upgradeBuild(pluginPkgs)
92 +}
93 +
94 +func upgradeBuild(pluginPkgs map[string]struct{}) (int, error) {
95 + l := caddy.Log()
96 +
97 + thisExecPath, err := os.Executable()
98 + if err != nil {
99 + return caddy.ExitCodeFailedStartup, fmt.Errorf("determining current executable path: %v", err)
100 + }
101 + thisExecStat, err := os.Stat(thisExecPath)
102 + if err != nil {
103 + return caddy.ExitCodeFailedStartup, fmt.Errorf("retrieving current executable permission bits: %v", err)
104 + }
105 + l.Info("this executable will be replaced", zap.String("path", thisExecPath))
106 +
107 + // build the request URL to download this custom build
108 + qs := url.Values{
109 + "os": {runtime.GOOS},
110 + "arch": {runtime.GOARCH},
111 + }
112 + for pkg := range pluginPkgs {
113 + qs.Add("p", pkg)
114 + }
115 +
116 + // initiate the build
117 + resp, err := downloadBuild(qs)
118 + if err != nil {
119 + return caddy.ExitCodeFailedStartup, fmt.Errorf("download failed: %v", err)
120 + }
121 + defer resp.Body.Close()
122 +
123 + // back up the current binary, in case something goes wrong we can replace it
124 + backupExecPath := thisExecPath + ".tmp"
125 + l.Info("build acquired; backing up current executable",
126 + zap.String("current_path", thisExecPath),
127 + zap.String("backup_path", backupExecPath))
128 + err = os.Rename(thisExecPath, backupExecPath)
129 + if err != nil {
130 + return caddy.ExitCodeFailedStartup, fmt.Errorf("backing up current binary: %v", err)
131 + }
132 + defer func() {
133 + if err != nil {
134 + err2 := os.Rename(backupExecPath, thisExecPath)
135 + if err2 != nil {
136 + l.Error("restoring original executable failed; will need to be restored manually",
137 + zap.String("backup_path", backupExecPath),
138 + zap.String("original_path", thisExecPath),
139 + zap.Error(err2))
140 + }
141 + }
142 + }()
143 +
144 + // download the file; do this in a closure to close reliably before we execute it
145 + err = writeCaddyBinary(thisExecPath, &resp.Body, thisExecStat)
146 + if err != nil {
147 + return caddy.ExitCodeFailedStartup, err
148 + }
149 +
150 + l.Info("download successful; displaying new binary details", zap.String("location", thisExecPath))
151 +
152 + // use the new binary to print out version and module info
153 + fmt.Print("\nModule versions:\n\n")
154 + if err = listModules(thisExecPath); err != nil {
155 + return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to execute: %v", err)
156 + }
157 + fmt.Println("\nVersion:")
158 + if err = showVersion(thisExecPath); err != nil {
159 + return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to execute: %v", err)
160 + }
161 + fmt.Println()
162 +
163 + // clean up the backup file
164 + if err = os.Remove(backupExecPath); err != nil {
165 + return caddy.ExitCodeFailedStartup, fmt.Errorf("download succeeded, but unable to clean up backup binary: %v", err)
166 + }
167 + l.Info("upgrade successful; please restart any running Caddy instances", zap.String("executable", thisExecPath))
168 +
169 + return caddy.ExitCodeSuccess, nil
170 +}
171 +
172 +func getModules() (standard, nonstandard, unknown []moduleInfo, err error) {
173 + bi, ok := debug.ReadBuildInfo()
174 + if !ok {
175 + err = fmt.Errorf("no build info")
176 + return
177 + }
178 +
179 + for _, modID := range caddy.Modules() {
180 + modInfo, err := caddy.GetModule(modID)
181 + if err != nil {
182 + // that's weird, shouldn't happen
183 + unknown = append(unknown, moduleInfo{caddyModuleID: modID, err: err})
184 + continue
185 + }
186 +
187 + // to get the Caddy plugin's version info, we need to know
188 + // the package that the Caddy module's value comes from; we
189 + // can use reflection but we need a non-pointer value (I'm
190 + // not sure why), and since New() should return a pointer
191 + // value, we need to dereference it first
192 + iface := interface{}(modInfo.New())
193 + if rv := reflect.ValueOf(iface); rv.Kind() == reflect.Ptr {
194 + iface = reflect.New(reflect.TypeOf(iface).Elem()).Elem().Interface()
195 + }
196 + modPkgPath := reflect.TypeOf(iface).PkgPath()
197 +
198 + // now we find the Go module that the Caddy module's package
199 + // belongs to; we assume the Caddy module package path will
200 + // be prefixed by its Go module path, and we will choose the
201 + // longest matching prefix in case there are nested modules
202 + var matched *debug.Module
203 + for _, dep := range bi.Deps {
204 + if strings.HasPrefix(modPkgPath, dep.Path) {
205 + if matched == nil || len(dep.Path) > len(matched.Path) {
206 + matched = dep
207 + }
208 + }
209 + }
210 +
211 + caddyModGoMod := moduleInfo{caddyModuleID: modID, goModule: matched}
212 +
213 + if strings.HasPrefix(modPkgPath, caddy.ImportPath) {
214 + standard = append(standard, caddyModGoMod)
215 + } else {
216 + nonstandard = append(nonstandard, caddyModGoMod)
217 + }
218 + }
219 + return
220 +}
221 +
222 +func listModules(path string) error {
223 + cmd := exec.Command(path, "list-modules", "--versions")
224 + cmd.Stdout = os.Stdout
225 + cmd.Stderr = os.Stderr
226 + err := cmd.Run()
227 + if err != nil {
228 + return fmt.Errorf("download succeeded, but unable to execute: %v", err)
229 + }
230 + return nil
231 +}
232 +
233 +func showVersion(path string) error {
234 + cmd := exec.Command(path, "version")
235 + cmd.Stdout = os.Stdout
236 + cmd.Stderr = os.Stderr
237 + err := cmd.Run()
238 + if err != nil {
239 + return fmt.Errorf("download succeeded, but unable to execute: %v", err)
240 + }
241 + return nil
242 +}
243 +
244 +func downloadBuild(qs url.Values) (*http.Response, error) {
245 + l := caddy.Log()
246 + l.Info("requesting build",
247 + zap.String("os", qs.Get("os")),
248 + zap.String("arch", qs.Get("arch")),
249 + zap.Strings("packages", qs["p"]))
250 + resp, err := http.Get(fmt.Sprintf("%s?%s", downloadPath, qs.Encode()))
251 + if err != nil {
252 + return nil, fmt.Errorf("secure request failed: %v", err)
253 + }
254 + if resp.StatusCode >= 400 {
255 + var details struct {
256 + StatusCode int `json:"status_code"`
257 + Error struct {
258 + Message string `json:"message"`
259 + ID string `json:"id"`
260 + } `json:"error"`
261 + }
262 + err2 := json.NewDecoder(resp.Body).Decode(&details)
263 + if err2 != nil {
264 + return nil, fmt.Errorf("download and error decoding failed: HTTP %d: %v", resp.StatusCode, err2)
265 + }
266 + return nil, fmt.Errorf("download failed: HTTP %d: %s (id=%s)", resp.StatusCode, details.Error.Message, details.Error.ID)
267 + }
268 + return resp, nil
269 +}
270 +
271 +func getPluginPackages(modules []moduleInfo) (map[string]struct{}, error) {
272 + pluginPkgs := make(map[string]struct{})
273 + for _, mod := range modules {
274 + if mod.goModule.Replace != nil {
275 + return nil, fmt.Errorf("cannot auto-upgrade when Go module has been replaced: %s => %s",
276 + mod.goModule.Path, mod.goModule.Replace.Path)
277 + }
278 + pluginPkgs[mod.goModule.Path] = struct{}{}
279 + }
280 + return pluginPkgs, nil
281 +}
282 +
283 +func writeCaddyBinary(path string, body *io.ReadCloser, fileInfo os.FileInfo) error {
284 + l := caddy.Log()
285 + destFile, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileInfo.Mode())
286 + if err != nil {
287 + return fmt.Errorf("unable to open destination file: %v", err)
288 + }
289 + defer destFile.Close()
290 +
291 + l.Info("downloading binary", zap.String("destination", path))
292 +
293 + _, err = io.Copy(destFile, *body)
294 + if err != nil {
295 + return fmt.Errorf("unable to download file: %v", err)
296 + }
297 +
298 + err = destFile.Sync()
299 + if err != nil {
300 + return fmt.Errorf("syncing downloaded file to device: %v", err)
301 + }
302 +
303 + return nil
304 +}
305 +
306 +const downloadPath = "https://caddyserver.com/api/download"