Fix every finding surfaced by the canonical .golangci.yml with golangci-lint v2.12.2 (refs #61), behavior-preserving throughout: - err113: dynamic errors replaced with package-level sentinels and %w wrapping; direct comparisons converted to errors.Is - goprintffuncname: printf-style helpers renamed with an f suffix (ui.Writer message methods, cli.ReportErrorf, database.Fatalf, vaultik stdoutf) and all call sites updated - revive: stuttering type names renamed (blob.Handler, blob.WithReader, blob.ChunkPosition, storage.URL, storage.Info), doc comments added, unused parameters blanked, package comments added - contextcheck/noctx: ctx threaded through blob.Packer (AddChunk/Flush/FinalizeBlob/PackChunks) and scanner call sites; context-aware exec and sql variants used - funlen/cyclop/gocognit/nestif/dupl: oversized or duplicated functions split into focused helpers across production and test code - paralleltest/tparallel/thelper/usetesting/testpackage: tests parallelized where safe (global log.Initialize kept in the serial phase), helpers marked, t.TempDir adopted, external test packages where only exported API is used - gosec: integer conversions clamped or justified, header timeouts added, remaining findings suppressed with per-site justifications - mnd/goconst/lll/wsl_v5/nlreturn/noinlineerr/errcheck and other mechanical findings fixed directly Remove the deprecated log.LogOptions alias (callers migrated to log.Options). make check is green.
614 lines
18 KiB
Go
614 lines
18 KiB
Go
package cli
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// configFileMode is the permission set for freshly written config files;
|
|
// configs may hold S3 credentials, so keep them owner-only.
|
|
const configFileMode = 0o600
|
|
|
|
// configSetArgs is the argument count of `config set <key> <value>`.
|
|
const configSetArgs = 2
|
|
|
|
// configDirMode is the permission set for created config directories;
|
|
// parent config dirs (e.g. ~/.config) are conventionally traversable.
|
|
const configDirMode = 0o755
|
|
|
|
var (
|
|
errConfigExists = errors.New("config file already exists")
|
|
errEmptyConfig = errors.New("empty config file")
|
|
errKeyNotFound = errors.New("key not found")
|
|
errNeedNumericIndex = errors.New("key is a list; use a numeric index")
|
|
errIndexOutOfRange = errors.New("index out of range")
|
|
errNotMapOrList = errors.New("key is not a map or list")
|
|
)
|
|
|
|
const defaultConfigTemplate = `# vaultik configuration
|
|
# Documentation: https://sneak.berlin/go/vaultik
|
|
|
|
# ─── REQUIRED ────────────────────────────────────────────────────────────────
|
|
|
|
# Age recipient public keys for encryption.
|
|
# Backups are encrypted to ALL listed recipients. Any one of the corresponding
|
|
# private keys can decrypt. Generate a keypair with:
|
|
# age-keygen -o vaultik_backup_private_key.txt
|
|
# grep 'public key' vaultik_backup_private_key.txt
|
|
age_recipients:
|
|
- age1REPLACE_WITH_YOUR_PUBLIC_KEY
|
|
|
|
# Named snapshots. Each snapshot backs up one or more paths and can have its
|
|
# own exclude patterns in addition to the global excludes below.
|
|
#
|
|
# Exclude pattern semantics:
|
|
# - Patterns starting with / are anchored to the snapshot path root
|
|
# (e.g. "/Library/Caches" matches only ~/Library/Caches in a ~ snapshot)
|
|
# - Patterns without a leading / match anywhere in the tree
|
|
# (e.g. ".cache" matches any directory named .cache at any depth)
|
|
# - Globs are supported: *, **, ?
|
|
snapshots:
|
|
home:
|
|
paths:
|
|
- "~"
|
|
exclude:
|
|
# Trash, temp, and filesystem metadata
|
|
- "/.Trash"
|
|
- "/.Trashes"
|
|
- "/.fseventsd"
|
|
- "/.Spotlight-V100"
|
|
- "/.TemporaryItems"
|
|
- "/tmp"
|
|
- "/.rnd"
|
|
- ".DS_Store"
|
|
# Caches and package manager state (rebuildable)
|
|
- ".cache"
|
|
- ".bundle"
|
|
- "/.cpan/build"
|
|
- "/.cpan/sources"
|
|
- "/.gradle/caches"
|
|
- "/.dropbox"
|
|
- "/.minikube/cache"
|
|
- "/.local/share/containers/podman/machine"
|
|
- "/.persepolis"
|
|
- "/Library/Caches"
|
|
- "/Library/Logs"
|
|
- "/Library/Cookies"
|
|
- "/Library/Metadata"
|
|
- "/Library/Suggestions"
|
|
- "/Library/PubSub"
|
|
- "/Library/Homebrew"
|
|
- "/Library/Developer"
|
|
- "/Library/Google/GoogleSoftwareUpdate"
|
|
- "/Library/Preferences/Macromedia/Flash Player"
|
|
- "/Library/Preferences/SDMHelpData"
|
|
- "/Library/VoiceTrigger/SAT"
|
|
# Language/toolchain package caches (rebuildable from registries)
|
|
- "/.npm"
|
|
- "/.cargo/registry"
|
|
- "/.cargo/git"
|
|
- "/.rustup/toolchains"
|
|
- "/go/pkg/mod"
|
|
- "/.m2/repository"
|
|
- "/.vagrant.d/boxes"
|
|
- "node_modules"
|
|
- "__pycache__"
|
|
- ".venv"
|
|
# Virtual machine disk images (huge; remove these lines to back them up)
|
|
- "/Parallels"
|
|
- "/Virtual Machines.localized"
|
|
- "/VirtualBox VMs"
|
|
- "/.orbstack"
|
|
- "/Library/Containers/com.utmapp.UTM"
|
|
# Downloaded LLM models (huge, re-downloadable)
|
|
- "/.ollama/models"
|
|
- "/.lmstudio/models"
|
|
# Cloud-synced storage. These are synced to a provider already, and on
|
|
# modern macOS may contain dataless placeholder files that the backup
|
|
# would force-download in full.
|
|
- "/Library/CloudStorage"
|
|
- "/Library/Mobile Documents"
|
|
# Android SDK and emulator images (re-downloadable)
|
|
- "/Library/Android/sdk"
|
|
- "/.android/avd"
|
|
# Cloud-synced or restorable-from-server data
|
|
- "/Library/Mail"
|
|
- "/Library/Mail Downloads"
|
|
- "/Library/Safari"
|
|
- "/Library/Application Support/Evernote"
|
|
- "/Library/Application Support/MobileSync"
|
|
- "/Library/Application Support/SyncServices"
|
|
- "/Library/Application Support/protonmail/bridge/cache"
|
|
- "/Library/Application Support/Syncthing/index-*"
|
|
- "/Library/Syncthing/folders"
|
|
- "/Documents/Dropbox/.dropbox.cache"
|
|
# Large rebuildable app data (games, media caches, device backups)
|
|
- "/Applications/Fortnite"
|
|
- "/Documents/Steam Content"
|
|
- "/Library/Application Support/Ableton"
|
|
- "/Library/Application Support/CrossOver Games"
|
|
- "/Library/Application Support/SecondLife/cache"
|
|
- "/Library/Application Support/Steam/SteamApps"
|
|
- "/Library/Containers/com.docker.docker"
|
|
- "/Library/Group Containers/group.com.apple.secure-control-center-preferences"
|
|
- "/Library/iTunes/iPad Software Updates"
|
|
- "/Library/iTunes/iPhone Software Updates"
|
|
- "/Movies/CacheClip"
|
|
- "/Movies/ProxyMedia"
|
|
- "/Music/iTunes/Album Artwork"
|
|
- "/Pictures/iPod Photo Cache"
|
|
|
|
# Third-party applications. OS-provided apps live in /System/Applications
|
|
# on modern macOS and are never in /Applications, but Apple-installed
|
|
# App Store apps (Safari, GarageBand, iWork, iMovie) are excluded since
|
|
# they are re-downloadable.
|
|
apps:
|
|
paths:
|
|
- /Applications
|
|
exclude:
|
|
- ".DS_Store"
|
|
- "/Safari.app"
|
|
- "/GarageBand.app"
|
|
- "/iMovie.app"
|
|
- "/Keynote.app"
|
|
- "/Numbers.app"
|
|
- "/Pages.app"
|
|
- "/Xcode.app"
|
|
- "/Spotify.app"
|
|
- "/Steam.app"
|
|
- "/VirtualBox.app"
|
|
- "/Utilities/Adobe Installers"
|
|
|
|
# Storage backend (pick ONE of the three forms below).
|
|
#
|
|
# S3-compatible:
|
|
# storage_url: "s3://mybucket/backups?endpoint=s3.example.com®ion=us-east-1"
|
|
# (also set s3.access_key_id and s3.secret_access_key below)
|
|
#
|
|
# Local filesystem:
|
|
# storage_url: "file:///mnt/backups/vaultik"
|
|
#
|
|
# Rclone (requires rclone configured separately):
|
|
# storage_url: "rclone://myremote/path/to/backups"
|
|
storage_url: ""
|
|
|
|
# ─── S3 CREDENTIALS (required for s3:// storage_url) ────────────────────────
|
|
|
|
# s3:
|
|
# access_key_id: YOUR_ACCESS_KEY
|
|
# secret_access_key: YOUR_SECRET_KEY
|
|
# # region: us-east-1 # Default: us-east-1
|
|
# # use_ssl: true # Default: true
|
|
# # part_size: 5MB # Multipart upload part size. Default: 5MB
|
|
|
|
# ─── OPTIONAL ────────────────────────────────────────────────────────────────
|
|
|
|
# Global exclude patterns applied to ALL snapshots.
|
|
# Snapshot-specific excludes are additive.
|
|
# exclude:
|
|
# - "*.log"
|
|
# - "*.tmp"
|
|
# - ".git"
|
|
# - "node_modules"
|
|
|
|
# Average chunk size for content-defined chunking (FastCDC).
|
|
# Smaller = better deduplication but more metadata overhead.
|
|
# Accepts: 1MB, 10M, 64KB, etc.
|
|
# Default: 10MB
|
|
# chunk_size: 10MB
|
|
|
|
# Maximum blob size before splitting into a new blob.
|
|
# Accepts: 1GB, 10G, 500MB, etc.
|
|
# Default: 10GB
|
|
# blob_size_limit: 10GB
|
|
|
|
# Zstd compression level (1-19). Higher = better ratio but slower.
|
|
# Default: 3
|
|
# compression_level: 3
|
|
|
|
# Hostname used in snapshot IDs. Default: system hostname.
|
|
# hostname: myserver
|
|
|
|
# Path to the local SQLite index database.
|
|
# Default: the platform data directory, e.g.
|
|
# macOS: ~/Library/Application Support/vaultik/index.sqlite
|
|
# Linux: ~/.local/share/vaultik/index.sqlite
|
|
# index_path: /path/to/index.sqlite
|
|
`
|
|
|
|
// NewConfigCommand creates the config command group.
|
|
func NewConfigCommand() *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",
|
|
Short: "Write a default config file",
|
|
Long: `Creates a default configuration file with commented explanations
|
|
for every setting. If a config file already exists at the target path,
|
|
the command refuses to overwrite it.
|
|
|
|
The config is written to the path from --config, $VAULTIK_CONFIG, or
|
|
the platform default config directory (e.g. ~/Library/Application Support/
|
|
on macOS, ~/.config/ on Linux, /etc/vaultik/ as root).`,
|
|
Args: cobra.NoArgs,
|
|
RunE: func(_ *cobra.Command, _ []string) error {
|
|
path := configPathForInit()
|
|
|
|
_, err := os.Stat(path)
|
|
if err == nil {
|
|
return fmt.Errorf("%w: %s", errConfigExists, path)
|
|
}
|
|
|
|
dir := filepath.Dir(path)
|
|
|
|
err = os.MkdirAll(dir, configDirMode)
|
|
if err != nil {
|
|
return fmt.Errorf("creating config directory %s: %w", dir, err)
|
|
}
|
|
|
|
err = os.WriteFile(path, []byte(defaultConfigTemplate), configFileMode)
|
|
if err != nil {
|
|
return fmt.Errorf("writing config file: %w", err)
|
|
}
|
|
|
|
_, _ = fmt.Fprintf(os.Stdout, "Config written to %s\n", path)
|
|
_, _ = fmt.Fprintln(os.Stdout,
|
|
"Edit it to set your age_recipients, snapshots, and storage_url.")
|
|
|
|
return nil
|
|
},
|
|
}
|
|
}
|
|
|
|
// 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, _ []string) error {
|
|
path, err := ResolveConfigPath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
editor := os.Getenv("EDITOR")
|
|
if editor == "" {
|
|
editor = "vi"
|
|
}
|
|
|
|
//nolint:gosec // G204: launching the operator's own $EDITOR is the point
|
|
ed := exec.CommandContext(cmd.Context(), 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. storage_url, compression_level)",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(_ *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.Fprintln(os.Stdout, node.Value)
|
|
|
|
return nil
|
|
}
|
|
|
|
out, err := yaml.Marshal(node)
|
|
if err != nil {
|
|
return fmt.Errorf("marshaling value: %w", err)
|
|
}
|
|
|
|
_, _ = fmt.Fprint(os.Stdout, 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 storage_url "file:///mnt/backups"
|
|
vaultik config set storage_url "s3://bucket/prefix?endpoint=host®ion=us-east-1"
|
|
vaultik config set compression_level 9
|
|
vaultik config set s3.bucket mybucket # legacy S3 fields still supported`,
|
|
Args: cobra.ExactArgs(configSetArgs),
|
|
RunE: func(_ *cobra.Command, args []string) error {
|
|
path, err := ResolveConfigPath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
root, err := loadYAMLFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = yamlPathSet(root, strings.Split(args[0], "."), args[1])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
out, err := yaml.Marshal(root)
|
|
if err != nil {
|
|
return fmt.Errorf("marshaling config: %w", err)
|
|
}
|
|
|
|
mode := os.FileMode(configFileMode)
|
|
|
|
info, statErr := os.Stat(path)
|
|
if statErr == nil {
|
|
mode = info.Mode().Perm()
|
|
}
|
|
|
|
err = os.WriteFile(path, out, mode)
|
|
if err != nil {
|
|
return fmt.Errorf("writing config file: %w", err)
|
|
}
|
|
|
|
_, _ = fmt.Fprintf(os.Stdout, "%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) //nolint:gosec // G304: config path is operator-supplied
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading config file: %w", err)
|
|
}
|
|
|
|
var root yaml.Node
|
|
|
|
err = yaml.Unmarshal(data, &root)
|
|
if 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 and sequence
|
|
// nodes and returns the value node. Numeric path components index into
|
|
// sequences (e.g. "age_recipients.0").
|
|
func yamlPathGet(root *yaml.Node, keys []string) (*yaml.Node, error) {
|
|
node := root
|
|
if node.Kind == yaml.DocumentNode {
|
|
if len(node.Content) == 0 {
|
|
return nil, errEmptyConfig
|
|
}
|
|
|
|
node = node.Content[0]
|
|
}
|
|
|
|
for i, key := range keys {
|
|
switch node.Kind {
|
|
case yaml.MappingNode:
|
|
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("%w: %s",
|
|
errKeyNotFound, strings.Join(keys[:i+1], "."))
|
|
}
|
|
case yaml.SequenceNode:
|
|
idx, err := strconv.Atoi(key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: %s",
|
|
errNeedNumericIndex, strings.Join(keys[:i], "."))
|
|
}
|
|
|
|
if idx < 0 || idx >= len(node.Content) {
|
|
return nil, fmt.Errorf("%w: index %d for %s (len %d)",
|
|
errIndexOutOfRange, idx, strings.Join(keys[:i], "."),
|
|
len(node.Content))
|
|
}
|
|
|
|
node = node.Content[idx]
|
|
case yaml.DocumentNode, yaml.ScalarNode, yaml.AliasNode:
|
|
return nil, fmt.Errorf("%w: %s",
|
|
errNotMapOrList, strings.Join(keys[:i], "."))
|
|
default:
|
|
return nil, fmt.Errorf("%w: %s",
|
|
errNotMapOrList, strings.Join(keys[:i], "."))
|
|
}
|
|
}
|
|
|
|
return node, nil
|
|
}
|
|
|
|
// yamlPathSet navigates a dotted key path, creating intermediate maps as
|
|
// needed, and sets the final key to the given scalar value. Numeric path
|
|
// components index into sequences; an index equal to the sequence length
|
|
// appends a new element (e.g. "age_recipients.1" on a 1-element list).
|
|
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 {
|
|
last := i == len(keys)-1
|
|
|
|
switch node.Kind {
|
|
case yaml.MappingNode:
|
|
node = yamlSetInMapping(node, key, value, last)
|
|
case yaml.SequenceNode:
|
|
next, err := yamlSetInSequence(node, keys, i, value, last)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
node = next
|
|
case yaml.DocumentNode, yaml.ScalarNode, yaml.AliasNode:
|
|
return fmt.Errorf("%w: %s",
|
|
errNotMapOrList, strings.Join(keys[:i], "."))
|
|
default:
|
|
return fmt.Errorf("%w: %s",
|
|
errNotMapOrList, strings.Join(keys[:i], "."))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// yamlSetInMapping resolves (creating if needed) the value node for key
|
|
// within a mapping node, setting it to value when it is the final path
|
|
// element, and returns the node to descend into.
|
|
func yamlSetInMapping(node *yaml.Node, key, value string, last bool) *yaml.Node {
|
|
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 {
|
|
setScalar(valueNode, value)
|
|
}
|
|
|
|
return valueNode
|
|
}
|
|
|
|
// yamlSetInSequence indexes (or appends to) a sequence node using the
|
|
// numeric path element keys[i], setting the element to value when it is
|
|
// the final path element, and returns the node to descend into.
|
|
func yamlSetInSequence(
|
|
node *yaml.Node, keys []string, i int, value string, last bool,
|
|
) (*yaml.Node, error) {
|
|
idx, err := strconv.Atoi(keys[i])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: %s",
|
|
errNeedNumericIndex, strings.Join(keys[:i], "."))
|
|
}
|
|
|
|
if idx < 0 || idx > len(node.Content) {
|
|
return nil, fmt.Errorf("%w: index %d for %s (len %d)",
|
|
errIndexOutOfRange, idx, strings.Join(keys[:i], "."),
|
|
len(node.Content))
|
|
}
|
|
|
|
if idx == len(node.Content) {
|
|
newNode := &yaml.Node{Kind: yaml.MappingNode}
|
|
if last {
|
|
newNode = &yaml.Node{Kind: yaml.ScalarNode, Value: value}
|
|
}
|
|
|
|
node.Content = append(node.Content, newNode)
|
|
} else if last {
|
|
setScalar(node.Content[idx], value)
|
|
}
|
|
|
|
return node.Content[idx], nil
|
|
}
|
|
|
|
// setScalar overwrites a node in place with a plain scalar value.
|
|
func setScalar(n *yaml.Node, value string) {
|
|
n.Kind = yaml.ScalarNode
|
|
n.Tag = ""
|
|
n.Value = value
|
|
n.Content = nil
|
|
n.Style = 0
|
|
}
|
|
|
|
// configPathForInit returns the config path to write, checking --config flag,
|
|
// VAULTIK_CONFIG env, and the platform default.
|
|
func configPathForInit() string {
|
|
if rootFlags.ConfigPath != "" {
|
|
return rootFlags.ConfigPath
|
|
}
|
|
|
|
if envPath := os.Getenv("VAULTIK_CONFIG"); envPath != "" {
|
|
return envPath
|
|
}
|
|
|
|
return DefaultConfigPath()
|
|
}
|