@marcel91200 / caddy Mirror · read-only / commits / 078f130a
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 13, 01:45 UTC

cmd: Implement storage import/export (#5532)

* cmd: Implement 'storage import' and 'storage export' CLI commands. These commands use the certmagic.Storage interface. In particular, storage implementations should ensure that their List() functions correctly enumerate all keys when called with an empty prefix and recursive == true. Also, Stat() calls on keys holding values instead of nested keys are expected to set KeyInfo.IsTerminal = true. * remove errors.Join

Cass C committed Jun 2, 2023 at 15:04 UTC 078f130a51b1546d63d770acdcbdec64cc7323a6
2 files changed +270
cmd/commands.go
+50
@@ -305,6 +305,56 @@ the KEY=VALUE format will be loaded into the Caddy process.
305 },
306 })
307
308 + RegisterCommand(Command{
309 + Name: "storage",
310 + Short: "Commands for working with Caddy's storage (EXPERIMENTAL)",
311 + Long: `
312 +Allows exporting and importing Caddy's storage contents. The two commands can be
313 +combined in a pipeline to transfer directly from one storage to another:
314 +
315 +$ caddy storage export --config Caddyfile.old --output - |
316 +> caddy storage import --config Caddyfile.new --input -
317 +
318 +The - argument refers to stdout and stdin, respectively.
319 +
320 +NOTE: When importing to or exporting from file_system storage (the default), the command
321 +should be run as the user that owns the associated root path.
322 +
323 +EXPERIMENTAL: May be changed or removed.
324 +`,
325 + CobraFunc: func(cmd *cobra.Command) {
326 + exportCmd := &cobra.Command{
327 + Use: "export --config <path> --output <path>",
328 + Short: "Exports storage assets as a tarball",
329 + Long: `
330 +The contents of the configured storage module (TLS certificates, etc)
331 +are exported via a tarball.
332 +
333 +--output is required, - can be given for stdout.
334 +`,
335 + RunE: WrapCommandFuncForCobra(cmdExportStorage),
336 + }
337 + exportCmd.Flags().StringP("config", "c", "", "Input configuration file (required)")
338 + exportCmd.Flags().StringP("output", "o", "", "Output path")
339 + cmd.AddCommand(exportCmd)
340 +
341 + importCmd := &cobra.Command{
342 + Use: "import --config <path> --input <path>",
343 + Short: "Imports storage assets from a tarball.",
344 + Long: `
345 +Imports storage assets to the configured storage module. The import file must be
346 +a tar archive.
347 +
348 +--input is required, - can be given for stdin.
349 +`,
350 + RunE: WrapCommandFuncForCobra(cmdImportStorage),
351 + }
352 + importCmd.Flags().StringP("config", "c", "", "Configuration file to load (required)")
353 + importCmd.Flags().StringP("input", "i", "", "Tar of assets to load (required)")
354 + cmd.AddCommand(importCmd)
355 + },
356 + })
357 +
358 RegisterCommand(Command{
359 Name: "fmt",
360 Usage: "[--overwrite] [--diff] [<path>]",
cmd/storagefuncs.go new
+220
@@ -0,0 +1,220 @@
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 + "archive/tar"
19 + "context"
20 + "encoding/json"
21 + "errors"
22 + "fmt"
23 + "io"
24 + "os"
25 +
26 + "github.com/caddyserver/caddy/v2"
27 + "github.com/caddyserver/certmagic"
28 +)
29 +
30 +type storVal struct {
31 + StorageRaw json.RawMessage `json:"storage,omitempty" caddy:"namespace=caddy.storage inline_key=module"`
32 +}
33 +
34 +// determineStorage returns the top-level storage module from the given config.
35 +// It may return nil even if no error.
36 +func determineStorage(configFile string, configAdapter string) (*storVal, error) {
37 + cfg, _, err := LoadConfig(configFile, configAdapter)
38 + if err != nil {
39 + return nil, err
40 + }
41 +
42 + // storage defaults to FileStorage if not explicitly
43 + // defined in the config, so the config can be valid
44 + // json but unmarshaling will fail.
45 + if !json.Valid(cfg) {
46 + return nil, &json.SyntaxError{}
47 + }
48 + var tmpStruct storVal
49 + err = json.Unmarshal(cfg, &tmpStruct)
50 + if err != nil {
51 + // default case, ignore the error
52 + var jsonError *json.SyntaxError
53 + if errors.As(err, &jsonError) {
54 + return nil, nil
55 + }
56 + return nil, err
57 + }
58 +
59 + return &tmpStruct, nil
60 +}
61 +
62 +func cmdImportStorage(fl Flags) (int, error) {
63 + importStorageCmdConfigFlag := fl.String("config")
64 + importStorageCmdImportFile := fl.String("input")
65 +
66 + if importStorageCmdConfigFlag == "" {
67 + return caddy.ExitCodeFailedStartup, errors.New("--config is required")
68 + }
69 + if importStorageCmdImportFile == "" {
70 + return caddy.ExitCodeFailedStartup, errors.New("--input is required")
71 + }
72 +
73 + // extract storage from config if possible
74 + storageCfg, err := determineStorage(importStorageCmdConfigFlag, "")
75 + if err != nil {
76 + return caddy.ExitCodeFailedStartup, err
77 + }
78 +
79 + // load specified storage or fallback to default
80 + var stor certmagic.Storage
81 + ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
82 + defer cancel()
83 + if storageCfg != nil && storageCfg.StorageRaw != nil {
84 + val, err := ctx.LoadModule(storageCfg, "StorageRaw")
85 + if err != nil {
86 + return caddy.ExitCodeFailedStartup, err
87 + }
88 + stor, err = val.(caddy.StorageConverter).CertMagicStorage()
89 + if err != nil {
90 + return caddy.ExitCodeFailedStartup, err
91 + }
92 + } else {
93 + stor = caddy.DefaultStorage
94 + }
95 +
96 + // setup input
97 + var f *os.File
98 + if importStorageCmdImportFile == "-" {
99 + f = os.Stdin
100 + } else {
101 + f, err = os.Open(importStorageCmdImportFile)
102 + if err != nil {
103 + return caddy.ExitCodeFailedStartup, fmt.Errorf("opening input file: %v", err)
104 + }
105 + defer f.Close()
106 + }
107 +
108 + // store each archive element
109 + tr := tar.NewReader(f)
110 + for {
111 + hdr, err := tr.Next()
112 + if err == io.EOF {
113 + break
114 + }
115 + if err != nil {
116 + return caddy.ExitCodeFailedQuit, fmt.Errorf("reading archive: %v", err)
117 + }
118 +
119 + b, err := io.ReadAll(tr)
120 + if err != nil {
121 + return caddy.ExitCodeFailedQuit, fmt.Errorf("reading archive: %v", err)
122 + }
123 +
124 + err = stor.Store(ctx, hdr.Name, b)
125 + if err != nil {
126 + return caddy.ExitCodeFailedQuit, fmt.Errorf("reading archive: %v", err)
127 + }
128 + }
129 +
130 + fmt.Println("Successfully imported storage")
131 + return caddy.ExitCodeSuccess, nil
132 +}
133 +
134 +func cmdExportStorage(fl Flags) (int, error) {
135 + exportStorageCmdConfigFlag := fl.String("config")
136 + exportStorageCmdOutputFlag := fl.String("output")
137 +
138 + if exportStorageCmdConfigFlag == "" {
139 + return caddy.ExitCodeFailedStartup, errors.New("--config is required")
140 + }
141 + if exportStorageCmdOutputFlag == "" {
142 + return caddy.ExitCodeFailedStartup, errors.New("--output is required")
143 + }
144 +
145 + // extract storage from config if possible
146 + storageCfg, err := determineStorage(exportStorageCmdConfigFlag, "")
147 + if err != nil {
148 + return caddy.ExitCodeFailedStartup, err
149 + }
150 +
151 + // load specified storage or fallback to default
152 + var stor certmagic.Storage
153 + ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
154 + defer cancel()
155 + if storageCfg != nil && storageCfg.StorageRaw != nil {
156 + val, err := ctx.LoadModule(storageCfg, "StorageRaw")
157 + if err != nil {
158 + return caddy.ExitCodeFailedStartup, err
159 + }
160 + stor, err = val.(caddy.StorageConverter).CertMagicStorage()
161 + if err != nil {
162 + return caddy.ExitCodeFailedStartup, err
163 + }
164 + } else {
165 + stor = caddy.DefaultStorage
166 + }
167 +
168 + // enumerate all keys
169 + keys, err := stor.List(ctx, "", true)
170 + if err != nil {
171 + return caddy.ExitCodeFailedStartup, err
172 + }
173 +
174 + // setup output
175 + var f *os.File
176 + if exportStorageCmdOutputFlag == "-" {
177 + f = os.Stdout
178 + } else {
179 + f, err = os.Create(exportStorageCmdOutputFlag)
180 + if err != nil {
181 + return caddy.ExitCodeFailedStartup, fmt.Errorf("opening output file: %v", err)
182 + }
183 + defer f.Close()
184 + }
185 +
186 + // `IsTerminal: true` keys hold the values we
187 + // care about, write them out
188 + tw := tar.NewWriter(f)
189 + for _, k := range keys {
190 + info, err := stor.Stat(ctx, k)
191 + if err != nil {
192 + return caddy.ExitCodeFailedQuit, err
193 + }
194 +
195 + if info.IsTerminal {
196 + v, err := stor.Load(ctx, k)
197 + if err != nil {
198 + return caddy.ExitCodeFailedQuit, err
199 + }
200 +
201 + hdr := &tar.Header{
202 + Name: k,
203 + Mode: 0600,
204 + Size: int64(len(v)),
205 + }
206 +
207 + if err = tw.WriteHeader(hdr); err != nil {
208 + return caddy.ExitCodeFailedQuit, fmt.Errorf("writing archive: %v", err)
209 + }
210 + if _, err = tw.Write(v); err != nil {
211 + return caddy.ExitCodeFailedQuit, fmt.Errorf("writing archive: %v", err)
212 + }
213 + }
214 + }
215 + if err = tw.Close(); err != nil {
216 + return caddy.ExitCodeFailedQuit, fmt.Errorf("writing archive: %v", err)
217 + }
218 +
219 + return caddy.ExitCodeSuccess, nil
220 +}