summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCass C <wcaplinger@gmail.com>2023-06-02 15:04:31 -0400
committerGitHub <noreply@github.com>2023-06-02 13:04:31 -0600
commit078f130a51b1546d63d770acdcbdec64cc7323a6 (patch)
tree0402f71cc005f1b97961cc03372b7ce82d6c254f
parent9c180a59880160d775095f1f40650d8b8bc988a4 (diff)
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
-rw-r--r--cmd/commands.go50
-rw-r--r--cmd/storagefuncs.go220
2 files changed, 270 insertions, 0 deletions
diff --git a/cmd/commands.go b/cmd/commands.go
index a06336d..d1b76f4 100644
--- a/cmd/commands.go
+++ b/cmd/commands.go
@@ -306,6 +306,56 @@ the KEY=VALUE format will be loaded into the Caddy process.
})
RegisterCommand(Command{
+ Name: "storage",
+ Short: "Commands for working with Caddy's storage (EXPERIMENTAL)",
+ Long: `
+Allows exporting and importing Caddy's storage contents. The two commands can be
+combined in a pipeline to transfer directly from one storage to another:
+
+$ caddy storage export --config Caddyfile.old --output - |
+> caddy storage import --config Caddyfile.new --input -
+
+The - argument refers to stdout and stdin, respectively.
+
+NOTE: When importing to or exporting from file_system storage (the default), the command
+should be run as the user that owns the associated root path.
+
+EXPERIMENTAL: May be changed or removed.
+`,
+ CobraFunc: func(cmd *cobra.Command) {
+ exportCmd := &cobra.Command{
+ Use: "export --config <path> --output <path>",
+ Short: "Exports storage assets as a tarball",
+ Long: `
+The contents of the configured storage module (TLS certificates, etc)
+are exported via a tarball.
+
+--output is required, - can be given for stdout.
+`,
+ RunE: WrapCommandFuncForCobra(cmdExportStorage),
+ }
+ exportCmd.Flags().StringP("config", "c", "", "Input configuration file (required)")
+ exportCmd.Flags().StringP("output", "o", "", "Output path")
+ cmd.AddCommand(exportCmd)
+
+ importCmd := &cobra.Command{
+ Use: "import --config <path> --input <path>",
+ Short: "Imports storage assets from a tarball.",
+ Long: `
+Imports storage assets to the configured storage module. The import file must be
+a tar archive.
+
+--input is required, - can be given for stdin.
+`,
+ RunE: WrapCommandFuncForCobra(cmdImportStorage),
+ }
+ importCmd.Flags().StringP("config", "c", "", "Configuration file to load (required)")
+ importCmd.Flags().StringP("input", "i", "", "Tar of assets to load (required)")
+ cmd.AddCommand(importCmd)
+ },
+ })
+
+ RegisterCommand(Command{
Name: "fmt",
Usage: "[--overwrite] [--diff] [<path>]",
Short: "Formats a Caddyfile",
diff --git a/cmd/storagefuncs.go b/cmd/storagefuncs.go
new file mode 100644
index 0000000..75790ab
--- /dev/null
+++ b/cmd/storagefuncs.go
@@ -0,0 +1,220 @@
+// Copyright 2015 Matthew Holt and The Caddy Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package caddycmd
+
+import (
+ "archive/tar"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+
+ "github.com/caddyserver/caddy/v2"
+ "github.com/caddyserver/certmagic"
+)
+
+type storVal struct {
+ StorageRaw json.RawMessage `json:"storage,omitempty" caddy:"namespace=caddy.storage inline_key=module"`
+}
+
+// determineStorage returns the top-level storage module from the given config.
+// It may return nil even if no error.
+func determineStorage(configFile string, configAdapter string) (*storVal, error) {
+ cfg, _, err := LoadConfig(configFile, configAdapter)
+ if err != nil {
+ return nil, err
+ }
+
+ // storage defaults to FileStorage if not explicitly
+ // defined in the config, so the config can be valid
+ // json but unmarshaling will fail.
+ if !json.Valid(cfg) {
+ return nil, &json.SyntaxError{}
+ }
+ var tmpStruct storVal
+ err = json.Unmarshal(cfg, &tmpStruct)
+ if err != nil {
+ // default case, ignore the error
+ var jsonError *json.SyntaxError
+ if errors.As(err, &jsonError) {
+ return nil, nil
+ }
+ return nil, err
+ }
+
+ return &tmpStruct, nil
+}
+
+func cmdImportStorage(fl Flags) (int, error) {
+ importStorageCmdConfigFlag := fl.String("config")
+ importStorageCmdImportFile := fl.String("input")
+
+ if importStorageCmdConfigFlag == "" {
+ return caddy.ExitCodeFailedStartup, errors.New("--config is required")
+ }
+ if importStorageCmdImportFile == "" {
+ return caddy.ExitCodeFailedStartup, errors.New("--input is required")
+ }
+
+ // extract storage from config if possible
+ storageCfg, err := determineStorage(importStorageCmdConfigFlag, "")
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, err
+ }
+
+ // load specified storage or fallback to default
+ var stor certmagic.Storage
+ ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
+ defer cancel()
+ if storageCfg != nil && storageCfg.StorageRaw != nil {
+ val, err := ctx.LoadModule(storageCfg, "StorageRaw")
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, err
+ }
+ stor, err = val.(caddy.StorageConverter).CertMagicStorage()
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, err
+ }
+ } else {
+ stor = caddy.DefaultStorage
+ }
+
+ // setup input
+ var f *os.File
+ if importStorageCmdImportFile == "-" {
+ f = os.Stdin
+ } else {
+ f, err = os.Open(importStorageCmdImportFile)
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("opening input file: %v", err)
+ }
+ defer f.Close()
+ }
+
+ // store each archive element
+ tr := tar.NewReader(f)
+ for {
+ hdr, err := tr.Next()
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ return caddy.ExitCodeFailedQuit, fmt.Errorf("reading archive: %v", err)
+ }
+
+ b, err := io.ReadAll(tr)
+ if err != nil {
+ return caddy.ExitCodeFailedQuit, fmt.Errorf("reading archive: %v", err)
+ }
+
+ err = stor.Store(ctx, hdr.Name, b)
+ if err != nil {
+ return caddy.ExitCodeFailedQuit, fmt.Errorf("reading archive: %v", err)
+ }
+ }
+
+ fmt.Println("Successfully imported storage")
+ return caddy.ExitCodeSuccess, nil
+}
+
+func cmdExportStorage(fl Flags) (int, error) {
+ exportStorageCmdConfigFlag := fl.String("config")
+ exportStorageCmdOutputFlag := fl.String("output")
+
+ if exportStorageCmdConfigFlag == "" {
+ return caddy.ExitCodeFailedStartup, errors.New("--config is required")
+ }
+ if exportStorageCmdOutputFlag == "" {
+ return caddy.ExitCodeFailedStartup, errors.New("--output is required")
+ }
+
+ // extract storage from config if possible
+ storageCfg, err := determineStorage(exportStorageCmdConfigFlag, "")
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, err
+ }
+
+ // load specified storage or fallback to default
+ var stor certmagic.Storage
+ ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
+ defer cancel()
+ if storageCfg != nil && storageCfg.StorageRaw != nil {
+ val, err := ctx.LoadModule(storageCfg, "StorageRaw")
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, err
+ }
+ stor, err = val.(caddy.StorageConverter).CertMagicStorage()
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, err
+ }
+ } else {
+ stor = caddy.DefaultStorage
+ }
+
+ // enumerate all keys
+ keys, err := stor.List(ctx, "", true)
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, err
+ }
+
+ // setup output
+ var f *os.File
+ if exportStorageCmdOutputFlag == "-" {
+ f = os.Stdout
+ } else {
+ f, err = os.Create(exportStorageCmdOutputFlag)
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("opening output file: %v", err)
+ }
+ defer f.Close()
+ }
+
+ // `IsTerminal: true` keys hold the values we
+ // care about, write them out
+ tw := tar.NewWriter(f)
+ for _, k := range keys {
+ info, err := stor.Stat(ctx, k)
+ if err != nil {
+ return caddy.ExitCodeFailedQuit, err
+ }
+
+ if info.IsTerminal {
+ v, err := stor.Load(ctx, k)
+ if err != nil {
+ return caddy.ExitCodeFailedQuit, err
+ }
+
+ hdr := &tar.Header{
+ Name: k,
+ Mode: 0600,
+ Size: int64(len(v)),
+ }
+
+ if err = tw.WriteHeader(hdr); err != nil {
+ return caddy.ExitCodeFailedQuit, fmt.Errorf("writing archive: %v", err)
+ }
+ if _, err = tw.Write(v); err != nil {
+ return caddy.ExitCodeFailedQuit, fmt.Errorf("writing archive: %v", err)
+ }
+ }
+ }
+ if err = tw.Close(); err != nil {
+ return caddy.ExitCodeFailedQuit, fmt.Errorf("writing archive: %v", err)
+ }
+
+ return caddy.ExitCodeSuccess, nil
+}