Merge feature/config-subcommands
This commit is contained in:
25
README.md
25
README.md
@@ -55,14 +55,17 @@ age-keygen -o key.txt
|
|||||||
# the public key is printed to stdout and also in key.txt
|
# the public key is printed to stdout and also in key.txt
|
||||||
|
|
||||||
# 3. Create a default config file
|
# 3. Create a default config file
|
||||||
vaultik init
|
vaultik config init
|
||||||
# Writes to the platform config directory with commented defaults:
|
# Writes to the platform config directory with commented defaults:
|
||||||
# macOS: ~/Library/Application Support/vaultik/config.yml
|
# macOS: ~/Library/Application Support/vaultik/config.yml
|
||||||
# Linux: ~/.config/vaultik/config.yml
|
# Linux: ~/.config/vaultik/config.yml
|
||||||
# root: /etc/vaultik/config.yml
|
# root: /etc/vaultik/config.yml
|
||||||
|
|
||||||
# 4. Edit the config: set age_recipients, snapshots, and storage_url
|
# 4. Edit the config: set age_recipients, snapshots, and storage_url
|
||||||
# (init prints the path it wrote to)
|
vaultik config edit # opens $EDITOR
|
||||||
|
# or set individual values:
|
||||||
|
vaultik config set storage_url "file:///mnt/backups"
|
||||||
|
vaultik config get storage_url
|
||||||
|
|
||||||
# 5. Run your first backup
|
# 5. Run your first backup
|
||||||
vaultik snapshot create
|
vaultik snapshot create
|
||||||
@@ -82,7 +85,10 @@ vaultik snapshot verify <snapshot-id>
|
|||||||
### commands
|
### commands
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
vaultik [--config <path>] init
|
vaultik [--config <path>] config init
|
||||||
|
vaultik [--config <path>] config edit
|
||||||
|
vaultik [--config <path>] config get <key>
|
||||||
|
vaultik [--config <path>] config set <key> <value>
|
||||||
vaultik [--config <path>] snapshot create [snapshot-names...] [--cron] [--prune] [--keep-newer-than <duration>] [--skip-errors]
|
vaultik [--config <path>] snapshot create [snapshot-names...] [--cron] [--prune] [--keep-newer-than <duration>] [--skip-errors]
|
||||||
vaultik [--config <path>] snapshot list [--json]
|
vaultik [--config <path>] snapshot list [--json]
|
||||||
vaultik [--config <path>] snapshot verify <snapshot-id> [--deep] [--json]
|
vaultik [--config <path>] snapshot verify <snapshot-id> [--deep] [--json]
|
||||||
@@ -114,12 +120,21 @@ vaultik version
|
|||||||
|
|
||||||
### command details
|
### command details
|
||||||
|
|
||||||
**init**: Write a default config file with commented explanations for every
|
**config init**: Write a default config file with commented explanations for
|
||||||
setting. Writes to the path from `--config`, `$VAULTIK_CONFIG`, or the
|
every setting. Writes to the path from `--config`, `$VAULTIK_CONFIG`, or the
|
||||||
platform config directory (`~/Library/Application Support/vaultik/` on macOS,
|
platform config directory (`~/Library/Application Support/vaultik/` on macOS,
|
||||||
`~/.config/vaultik/` on Linux, `/etc/vaultik/` as root). Refuses to overwrite an
|
`~/.config/vaultik/` on Linux, `/etc/vaultik/` as root). Refuses to overwrite an
|
||||||
existing file. Created with mode `0600` since it will contain credentials.
|
existing file. Created with mode `0600` since it will contain credentials.
|
||||||
|
|
||||||
|
**config edit**: Open the config file in `$EDITOR` (falls back to `vi`).
|
||||||
|
|
||||||
|
**config get**: Print a config value addressed by dotted YAML path
|
||||||
|
(e.g. `vaultik config get s3.bucket`). Non-scalar values print as YAML.
|
||||||
|
|
||||||
|
**config set**: Set a scalar config value by dotted YAML path
|
||||||
|
(e.g. `vaultik config set compression_level 9`). Comments and formatting
|
||||||
|
in the file are preserved; intermediate maps are created as needed.
|
||||||
|
|
||||||
**snapshot create**: Perform incremental backup of configured snapshots.
|
**snapshot create**: Perform incremental backup of configured snapshots.
|
||||||
* Optional snapshot names argument to create specific snapshots (default: all)
|
* Optional snapshot names argument to create specific snapshots (default: all)
|
||||||
* `--cron`: Silent unless error (for crontab)
|
* `--cron`: Silent unless error (for crontab)
|
||||||
|
|||||||
@@ -3,9 +3,12 @@ package cli
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultConfigTemplate = `# vaultik configuration
|
const defaultConfigTemplate = `# vaultik configuration
|
||||||
@@ -196,9 +199,25 @@ storage_url: ""
|
|||||||
# index_path: /path/to/index.sqlite
|
# index_path: /path/to/index.sqlite
|
||||||
`
|
`
|
||||||
|
|
||||||
// NewInitCommand creates the init command that writes a default config file.
|
// NewConfigCommand creates the config command group.
|
||||||
func NewInitCommand() *cobra.Command {
|
func NewConfigCommand() *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
|
Use: "config",
|
||||||
|
Short: "Manage the configuration file",
|
||||||
|
Long: "Commands for creating, editing, and querying the vaultik config file.",
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.AddCommand(newConfigInitCommand())
|
||||||
|
cmd.AddCommand(newConfigEditCommand())
|
||||||
|
cmd.AddCommand(newConfigGetCommand())
|
||||||
|
cmd.AddCommand(newConfigSetCommand())
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// newConfigInitCommand creates the 'config init' subcommand.
|
||||||
|
func newConfigInitCommand() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
Use: "init",
|
Use: "init",
|
||||||
Short: "Write a default config file",
|
Short: "Write a default config file",
|
||||||
Long: `Creates a default configuration file with commented explanations
|
Long: `Creates a default configuration file with commented explanations
|
||||||
@@ -230,8 +249,220 @@ on macOS, ~/.config/ on Linux, /etc/vaultik/ as root).`,
|
|||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return cmd
|
// newConfigEditCommand creates the 'config edit' subcommand.
|
||||||
|
func newConfigEditCommand() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "edit",
|
||||||
|
Short: "Open the config file in $EDITOR",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
path, err := ResolveConfigPath()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
editor := os.Getenv("EDITOR")
|
||||||
|
if editor == "" {
|
||||||
|
editor = "vi"
|
||||||
|
}
|
||||||
|
|
||||||
|
ed := exec.Command(editor, path)
|
||||||
|
ed.Stdin = os.Stdin
|
||||||
|
ed.Stdout = os.Stdout
|
||||||
|
ed.Stderr = os.Stderr
|
||||||
|
return ed.Run()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newConfigGetCommand creates the 'config get' subcommand.
|
||||||
|
func newConfigGetCommand() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "get <key>",
|
||||||
|
Short: "Print a config value by dotted path (e.g. s3.bucket)",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
path, err := ResolveConfigPath()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
root, err := loadYAMLFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
node, err := yamlPathGet(root, strings.Split(args[0], "."))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if node.Kind == yaml.ScalarNode {
|
||||||
|
fmt.Println(node.Value)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := yaml.Marshal(node)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshaling value: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Print(string(out))
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newConfigSetCommand creates the 'config set' subcommand.
|
||||||
|
func newConfigSetCommand() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "set <key> <value>",
|
||||||
|
Short: "Set a config value by dotted path (e.g. compression_level 5)",
|
||||||
|
Long: `Sets a scalar config value addressed by dotted YAML path and writes
|
||||||
|
the file back, preserving comments and formatting. Intermediate maps
|
||||||
|
are created as needed.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
vaultik config set compression_level 9
|
||||||
|
vaultik config set s3.bucket mybucket
|
||||||
|
vaultik config set storage_url "file:///mnt/backups"`,
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
path, err := ResolveConfigPath()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
root, err := loadYAMLFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := yamlPathSet(root, strings.Split(args[0], "."), args[1]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := yaml.Marshal(root)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshaling config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mode := os.FileMode(0o600)
|
||||||
|
if info, err := os.Stat(path); err == nil {
|
||||||
|
mode = info.Mode().Perm()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(path, out, mode); err != nil {
|
||||||
|
return fmt.Errorf("writing config file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%s = %s\n", args[0], args[1])
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadYAMLFile parses a YAML file into a yaml.Node document tree,
|
||||||
|
// which preserves comments and ordering for round-tripping.
|
||||||
|
func loadYAMLFile(path string) (*yaml.Node, error) {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("reading config file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var root yaml.Node
|
||||||
|
if err := yaml.Unmarshal(data, &root); err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing config file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// An empty file yields a zero node; normalize to an empty mapping document.
|
||||||
|
if root.Kind == 0 {
|
||||||
|
root = yaml.Node{
|
||||||
|
Kind: yaml.DocumentNode,
|
||||||
|
Content: []*yaml.Node{{Kind: yaml.MappingNode}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &root, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// yamlPathGet navigates a dotted key path through mapping nodes and
|
||||||
|
// returns the value node.
|
||||||
|
func yamlPathGet(root *yaml.Node, keys []string) (*yaml.Node, error) {
|
||||||
|
node := root
|
||||||
|
if node.Kind == yaml.DocumentNode {
|
||||||
|
if len(node.Content) == 0 {
|
||||||
|
return nil, fmt.Errorf("empty config file")
|
||||||
|
}
|
||||||
|
node = node.Content[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, key := range keys {
|
||||||
|
if node.Kind != yaml.MappingNode {
|
||||||
|
return nil, fmt.Errorf("key %q is not a map", strings.Join(keys[:i], "."))
|
||||||
|
}
|
||||||
|
found := false
|
||||||
|
for j := 0; j+1 < len(node.Content); j += 2 {
|
||||||
|
if node.Content[j].Value == key {
|
||||||
|
node = node.Content[j+1]
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return nil, fmt.Errorf("key not found: %s", strings.Join(keys[:i+1], "."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return node, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// yamlPathSet navigates a dotted key path, creating intermediate maps as
|
||||||
|
// needed, and sets the final key to the given scalar value.
|
||||||
|
func yamlPathSet(root *yaml.Node, keys []string, value string) error {
|
||||||
|
node := root
|
||||||
|
if node.Kind == yaml.DocumentNode {
|
||||||
|
if len(node.Content) == 0 {
|
||||||
|
node.Content = []*yaml.Node{{Kind: yaml.MappingNode}}
|
||||||
|
}
|
||||||
|
node = node.Content[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, key := range keys {
|
||||||
|
if node.Kind != yaml.MappingNode {
|
||||||
|
return fmt.Errorf("key %q is not a map", strings.Join(keys[:i], "."))
|
||||||
|
}
|
||||||
|
|
||||||
|
last := i == len(keys)-1
|
||||||
|
|
||||||
|
var valueNode *yaml.Node
|
||||||
|
for j := 0; j+1 < len(node.Content); j += 2 {
|
||||||
|
if node.Content[j].Value == key {
|
||||||
|
valueNode = node.Content[j+1]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if valueNode == nil {
|
||||||
|
keyNode := &yaml.Node{Kind: yaml.ScalarNode, Value: key}
|
||||||
|
valueNode = &yaml.Node{Kind: yaml.MappingNode}
|
||||||
|
if last {
|
||||||
|
valueNode = &yaml.Node{Kind: yaml.ScalarNode, Value: value}
|
||||||
|
}
|
||||||
|
node.Content = append(node.Content, keyNode, valueNode)
|
||||||
|
} else if last {
|
||||||
|
valueNode.Kind = yaml.ScalarNode
|
||||||
|
valueNode.Tag = ""
|
||||||
|
valueNode.Value = value
|
||||||
|
valueNode.Content = nil
|
||||||
|
valueNode.Style = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
node = valueNode
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// configPathForInit returns the config path to write, checking --config flag,
|
// configPathForInit returns the config path to write, checking --config flag,
|
||||||
145
internal/cli/config_test.go
Normal file
145
internal/cli/config_test.go
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.eeqj.de/sneak/vaultik/internal/config"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestDefaultConfigTemplateParses ensures the init template is valid YAML
|
||||||
|
// that unmarshals into the Config struct with the expected snapshots.
|
||||||
|
func TestDefaultConfigTemplateParses(t *testing.T) {
|
||||||
|
var cfg config.Config
|
||||||
|
if err := yaml.Unmarshal([]byte(defaultConfigTemplate), &cfg); err != nil {
|
||||||
|
t.Fatalf("default config template is not valid YAML: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cfg.AgeRecipients) != 1 {
|
||||||
|
t.Errorf("expected 1 placeholder age recipient, got %d", len(cfg.AgeRecipients))
|
||||||
|
}
|
||||||
|
|
||||||
|
home, ok := cfg.Snapshots["home"]
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected 'home' snapshot in default config")
|
||||||
|
}
|
||||||
|
if len(home.Paths) == 0 {
|
||||||
|
t.Error("home snapshot should have at least one path")
|
||||||
|
}
|
||||||
|
if len(home.Exclude) == 0 {
|
||||||
|
t.Error("home snapshot should have exclude patterns")
|
||||||
|
}
|
||||||
|
|
||||||
|
apps, ok := cfg.Snapshots["apps"]
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected 'apps' snapshot in default config")
|
||||||
|
}
|
||||||
|
if len(apps.Paths) != 1 || apps.Paths[0] != "/Applications" {
|
||||||
|
t.Errorf("apps snapshot should back up /Applications, got %v", apps.Paths)
|
||||||
|
}
|
||||||
|
if len(apps.Exclude) == 0 {
|
||||||
|
t.Error("apps snapshot should have exclude patterns")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const testYAML = `# top comment
|
||||||
|
compression_level: 3
|
||||||
|
s3:
|
||||||
|
bucket: oldbucket # inline comment
|
||||||
|
region: us-east-1
|
||||||
|
snapshots:
|
||||||
|
home:
|
||||||
|
paths:
|
||||||
|
- "~"
|
||||||
|
`
|
||||||
|
|
||||||
|
func parseTestYAML(t *testing.T) *yaml.Node {
|
||||||
|
t.Helper()
|
||||||
|
var root yaml.Node
|
||||||
|
if err := yaml.Unmarshal([]byte(testYAML), &root); err != nil {
|
||||||
|
t.Fatalf("parsing test yaml: %v", err)
|
||||||
|
}
|
||||||
|
return &root
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestYAMLPathGet(t *testing.T) {
|
||||||
|
root := parseTestYAML(t)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
path string
|
||||||
|
want string
|
||||||
|
err bool
|
||||||
|
}{
|
||||||
|
{"compression_level", "3", false},
|
||||||
|
{"s3.bucket", "oldbucket", false},
|
||||||
|
{"s3.region", "us-east-1", false},
|
||||||
|
{"s3.nonexistent", "", true},
|
||||||
|
{"nonexistent", "", true},
|
||||||
|
{"compression_level.sub", "", true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.path, func(t *testing.T) {
|
||||||
|
node, err := yamlPathGet(root, splitPath(tt.path))
|
||||||
|
if tt.err {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error for %q", tt.path)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if node.Value != tt.want {
|
||||||
|
t.Errorf("get %q = %q, want %q", tt.path, node.Value, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestYAMLPathSet(t *testing.T) {
|
||||||
|
root := parseTestYAML(t)
|
||||||
|
|
||||||
|
// Overwrite existing nested value
|
||||||
|
if err := yamlPathSet(root, splitPath("s3.bucket"), "newbucket"); err != nil {
|
||||||
|
t.Fatalf("set s3.bucket: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new nested key with intermediate map
|
||||||
|
if err := yamlPathSet(root, splitPath("s3.endpoint"), "s3.example.com"); err != nil {
|
||||||
|
t.Fatalf("set s3.endpoint: %v", err)
|
||||||
|
}
|
||||||
|
if err := yamlPathSet(root, splitPath("newmap.newkey"), "val"); err != nil {
|
||||||
|
t.Fatalf("set newmap.newkey: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Round-trip and verify values + comment preservation
|
||||||
|
out, err := yaml.Marshal(root)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal: %v", err)
|
||||||
|
}
|
||||||
|
text := string(out)
|
||||||
|
|
||||||
|
for _, want := range []string{"newbucket", "s3.example.com", "newkey: val", "# top comment", "# inline comment"} {
|
||||||
|
if !contains(text, want) {
|
||||||
|
t.Errorf("round-tripped YAML missing %q:\n%s", want, text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := yamlPathGet(root, splitPath("s3.bucket"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get after set: %v", err)
|
||||||
|
}
|
||||||
|
if got.Value != "newbucket" {
|
||||||
|
t.Errorf("s3.bucket = %q after set, want newbucket", got.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitPath(s string) []string {
|
||||||
|
return strings.Split(s, ".")
|
||||||
|
}
|
||||||
|
|
||||||
|
func contains(haystack, needle string) bool {
|
||||||
|
return strings.Contains(haystack, needle)
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@ func TestCLIEntry(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Verify all subcommands are registered
|
// Verify all subcommands are registered
|
||||||
expectedCommands := []string{"init", "snapshot", "store", "restore", "prune", "info", "version", "remote", "database"}
|
expectedCommands := []string{"config", "snapshot", "store", "restore", "prune", "info", "version", "remote", "database"}
|
||||||
for _, expected := range expectedCommands {
|
for _, expected := range expectedCommands {
|
||||||
found := false
|
found := false
|
||||||
for _, cmd := range cmd.Commands() {
|
for _, cmd := range cmd.Commands() {
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
package cli
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.eeqj.de/sneak/vaultik/internal/config"
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestDefaultConfigTemplateParses ensures the init template is valid YAML
|
|
||||||
// that unmarshals into the Config struct with the expected snapshots.
|
|
||||||
func TestDefaultConfigTemplateParses(t *testing.T) {
|
|
||||||
var cfg config.Config
|
|
||||||
if err := yaml.Unmarshal([]byte(defaultConfigTemplate), &cfg); err != nil {
|
|
||||||
t.Fatalf("default config template is not valid YAML: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cfg.AgeRecipients) != 1 {
|
|
||||||
t.Errorf("expected 1 placeholder age recipient, got %d", len(cfg.AgeRecipients))
|
|
||||||
}
|
|
||||||
|
|
||||||
home, ok := cfg.Snapshots["home"]
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("expected 'home' snapshot in default config")
|
|
||||||
}
|
|
||||||
if len(home.Paths) == 0 {
|
|
||||||
t.Error("home snapshot should have at least one path")
|
|
||||||
}
|
|
||||||
if len(home.Exclude) == 0 {
|
|
||||||
t.Error("home snapshot should have exclude patterns")
|
|
||||||
}
|
|
||||||
|
|
||||||
apps, ok := cfg.Snapshots["apps"]
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("expected 'apps' snapshot in default config")
|
|
||||||
}
|
|
||||||
if len(apps.Paths) != 1 || apps.Paths[0] != "/Applications" {
|
|
||||||
t.Errorf("apps snapshot should back up /Applications, got %v", apps.Paths)
|
|
||||||
}
|
|
||||||
if len(apps.Exclude) == 0 {
|
|
||||||
t.Error("apps snapshot should have exclude patterns")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -41,7 +41,7 @@ on the source system.`,
|
|||||||
|
|
||||||
// Add subcommands
|
// Add subcommands
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
NewInitCommand(),
|
NewConfigCommand(),
|
||||||
NewRestoreCommand(),
|
NewRestoreCommand(),
|
||||||
NewPruneCommand(),
|
NewPruneCommand(),
|
||||||
NewStoreCommand(),
|
NewStoreCommand(),
|
||||||
@@ -78,7 +78,7 @@ func ResolveConfigPath() (string, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", fmt.Errorf("no config file found; run 'vaultik init' to create one, or specify with --config")
|
return "", fmt.Errorf("no config file found; run 'vaultik config init' to create one, or specify with --config")
|
||||||
}
|
}
|
||||||
|
|
||||||
// defaultConfigPaths returns the ordered list of config paths to search.
|
// defaultConfigPaths returns the ordered list of config paths to search.
|
||||||
|
|||||||
Reference in New Issue
Block a user