summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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
+}