check / check (push) Failing after 2s
vault.CreateVault takes the unlocker passphrase and writes the vault directory, its metadata, long-term public key and passphrase unlocker into a temporary directory, renames that into vaults.d once complete, and only then makes the vault current. secret init and secret vault create call it once instead of adding the unlocker afterwards, so a kill part-way leaves either no vault, whose temporary directory the next command that takes the lock deletes, or a complete one. A test records the state directory before every change the call makes and checks each state, and the command run again from it. Model: opus-5-5
681 lines
18 KiB
Go
681 lines
18 KiB
Go
package cli
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.eeqj.de/sneak/secret/internal/secret"
|
|
"git.eeqj.de/sneak/secret/internal/vault"
|
|
"git.eeqj.de/sneak/secret/pkg/agehd"
|
|
"github.com/awnumar/memguard"
|
|
"github.com/spf13/afero"
|
|
"github.com/spf13/cobra"
|
|
"github.com/tyler-smith/go-bip39"
|
|
)
|
|
|
|
// Sentinel errors for vault operations
|
|
var (
|
|
errMnemonicEmpty = errors.New("mnemonic cannot be empty")
|
|
errInvalidMnemonicPhrase = errors.New("invalid BIP39 mnemonic phrase")
|
|
errInvalidMnemonic = errors.New("invalid BIP39 mnemonic")
|
|
errVaultHasLongTermKey = errors.New(
|
|
"already has a long-term key configured")
|
|
errMnemonicEnvNotSet = errors.New(
|
|
"SB_SECRET_MNEMONIC environment variable not set")
|
|
errPassphraseEnvNotSet = errors.New(
|
|
"SB_UNLOCK_PASSPHRASE environment variable not set")
|
|
errCannotRemoveLastVault = errors.New("cannot remove the last vault")
|
|
)
|
|
|
|
func newVaultCmd() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "vault",
|
|
Short: "Manage vaults",
|
|
Long: `Create, list, and select vaults for organizing secrets.`,
|
|
}
|
|
|
|
cmd.AddCommand(newVaultListCmd())
|
|
cmd.AddCommand(newVaultCreateCmd())
|
|
cmd.AddCommand(newVaultSelectCmd())
|
|
cmd.AddCommand(newVaultImportCmd())
|
|
cmd.AddCommand(newVaultRemoveCmd())
|
|
|
|
return cmd
|
|
}
|
|
|
|
func newVaultListCmd() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: cmdUseList,
|
|
Aliases: []string{"ls"},
|
|
Short: "List available vaults",
|
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
|
jsonOutput, _ := cmd.Flags().GetBool("json")
|
|
|
|
cli, err := NewCLIInstance()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to initialize CLI: %w", err)
|
|
}
|
|
|
|
return cli.ListVaults(cmd, jsonOutput)
|
|
},
|
|
}
|
|
|
|
cmd.Flags().Bool("json", false, "Output in JSON format")
|
|
|
|
return cmd
|
|
}
|
|
|
|
func newVaultCreateCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "create <name>",
|
|
Short: "Create a new vault",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
cli, err := NewCLIInstance()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to initialize CLI: %w", err)
|
|
}
|
|
|
|
destroySecrets := cli.readSecretEnv()
|
|
defer destroySecrets()
|
|
|
|
return cli.CreateVault(cmd, args[0])
|
|
},
|
|
}
|
|
}
|
|
|
|
func newVaultSelectCmd() *cobra.Command {
|
|
cli, err := NewCLIInstance()
|
|
if err != nil {
|
|
log.Fatalf("failed to initialize CLI: %v", err)
|
|
}
|
|
|
|
return &cobra.Command{
|
|
Use: "select <name>",
|
|
Short: "Select a vault as current",
|
|
Args: cobra.ExactArgs(1),
|
|
ValidArgsFunction: getVaultNamesCompletionFunc(cli.fs, cli.stateDir),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
cli, err := NewCLIInstance()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to initialize CLI: %w", err)
|
|
}
|
|
|
|
return cli.SelectVault(cmd, args[0])
|
|
},
|
|
}
|
|
}
|
|
|
|
func newVaultImportCmd() *cobra.Command {
|
|
cli, err := NewCLIInstance()
|
|
if err != nil {
|
|
log.Fatalf("failed to initialize CLI: %v", err)
|
|
}
|
|
|
|
return &cobra.Command{
|
|
Use: "import <vault-name>",
|
|
Short: "Import a mnemonic into a vault",
|
|
Long: `Import a BIP39 mnemonic phrase into the specified vault ` +
|
|
`(default if not specified).`,
|
|
Args: cobra.MaximumNArgs(1),
|
|
ValidArgsFunction: getVaultNamesCompletionFunc(cli.fs, cli.stateDir),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
vaultName := "default"
|
|
if len(args) > 0 {
|
|
vaultName = args[0]
|
|
}
|
|
|
|
cli, err := NewCLIInstance()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to initialize CLI: %w", err)
|
|
}
|
|
|
|
destroySecrets := cli.readSecretEnv()
|
|
defer destroySecrets()
|
|
|
|
return cli.VaultImport(cmd, vaultName)
|
|
},
|
|
}
|
|
}
|
|
|
|
func newVaultRemoveCmd() *cobra.Command {
|
|
cli, err := NewCLIInstance()
|
|
if err != nil {
|
|
log.Fatalf("failed to initialize CLI: %v", err)
|
|
}
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "remove <name>",
|
|
Aliases: []string{"rm"},
|
|
Short: "Remove a vault",
|
|
Long: `Remove a vault and all its secrets. Asks for ` +
|
|
`confirmation first, naming how many secrets the vault ` +
|
|
`holds; when stdin is not a terminal, fails unless --force ` +
|
|
`is given. Will automatically switch to another vault if ` +
|
|
`removing the currently selected one. The last vault ` +
|
|
`cannot be removed.`,
|
|
Args: cobra.ExactArgs(1),
|
|
ValidArgsFunction: getVaultNamesCompletionFunc(cli.fs, cli.stateDir),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
force, _ := cmd.Flags().GetBool("force")
|
|
|
|
cli, err := NewCLIInstance()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to initialize CLI: %w", err)
|
|
}
|
|
|
|
return cli.RemoveVault(cmd, args[0], force)
|
|
},
|
|
}
|
|
|
|
cmd.Flags().BoolP("force", "f", false,
|
|
"Remove without asking for confirmation, even a vault that contains secrets")
|
|
|
|
return cmd
|
|
}
|
|
|
|
// ListVaults lists all available vaults
|
|
func (cli *Instance) ListVaults(cmd *cobra.Command, jsonOutput bool) error {
|
|
vaults, err := vault.ListVaults(cli.fs, cli.stateDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if jsonOutput { //nolint:nestif // Separate JSON and text output formatting logic
|
|
// Get current vault name for context
|
|
currentVault := ""
|
|
|
|
currentVlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
|
if err == nil {
|
|
currentVault = currentVlt.GetName()
|
|
}
|
|
|
|
result := map[string]any{
|
|
"vaults": vaults,
|
|
"currentVault": currentVault,
|
|
}
|
|
|
|
jsonBytes, err := json.MarshalIndent(result, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cmd.Println(string(jsonBytes))
|
|
} else {
|
|
// Text output
|
|
cmd.Println("Available vaults:")
|
|
|
|
if len(vaults) == 0 {
|
|
cmd.Println(" (none)")
|
|
} else {
|
|
// Try to get current vault for marking
|
|
currentVault := ""
|
|
|
|
currentVlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
|
if err == nil {
|
|
currentVault = currentVlt.GetName()
|
|
}
|
|
|
|
for _, vaultName := range vaults {
|
|
if vaultName == currentVault {
|
|
cmd.Printf(" %s (current)\n", vaultName)
|
|
} else {
|
|
cmd.Printf(" %s\n", vaultName)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// resolvePassphrase returns the unlock passphrase from the environment,
|
|
// cli.UnlockPassphrase, or prompts the user for it with confirmation. The
|
|
// returned cleanup function must be deferred by the caller.
|
|
func (cli *Instance) resolvePassphrase() (*memguard.LockedBuffer, func(), error) {
|
|
if cli.UnlockPassphrase != nil {
|
|
secret.Debug("Using unlock passphrase from environment variable")
|
|
|
|
return cli.UnlockPassphrase, func() {}, nil
|
|
}
|
|
|
|
secret.Debug("Prompting user for unlock passphrase")
|
|
|
|
// Use secure passphrase input with confirmation
|
|
passphraseBuffer, err := readSecurePassphrase("Enter passphrase for unlocker: ")
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to read passphrase: %w", err)
|
|
}
|
|
|
|
return passphraseBuffer, passphraseBuffer.Destroy, nil
|
|
}
|
|
|
|
// CreateVault creates a new vault
|
|
func (cli *Instance) CreateVault(cmd *cobra.Command, name string) error {
|
|
secret.Debug("Creating new vault", "name", name, "state_dir", cli.stateDir)
|
|
|
|
release, err := vault.LockStateDir(cli.fs, cli.stateDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer release()
|
|
|
|
mnemonic, cleanupMnemonic, err := cli.promptMnemonic()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cleanupMnemonic()
|
|
|
|
mnemonicStr := mnemonic.String()
|
|
if mnemonicStr == "" {
|
|
return errMnemonicEmpty
|
|
}
|
|
|
|
// Validate the mnemonic
|
|
mnemonicWords := strings.Fields(mnemonicStr)
|
|
secret.Debug("Validating BIP39 mnemonic", "word_count", len(mnemonicWords))
|
|
|
|
if !bip39.IsMnemonicValid(mnemonicStr) {
|
|
return errInvalidMnemonicPhrase
|
|
}
|
|
|
|
// Ask for the unlocker passphrase before creating the vault, so that
|
|
// stopping at the prompt leaves no vault without an unlocker behind
|
|
passphraseBuffer, cleanupPassphrase, err := cli.resolvePassphrase()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cleanupPassphrase()
|
|
|
|
// Create the vault with its passphrase unlocker
|
|
vlt, err := vault.CreateVault(cli.fs, cli.stateDir, name,
|
|
mnemonic, passphraseBuffer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ltIdentity, err := vlt.GetOrDeriveLongTermKey()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get long-term key: %w", err)
|
|
}
|
|
|
|
unlocker, err := vlt.GetCurrentUnlocker()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cmd.Printf("Created vault '%s'\n", vlt.GetName())
|
|
cmd.Printf("Long-term public key: %s\n", ltIdentity.Recipient().String())
|
|
cmd.Printf("Unlocker ID: %s\n", unlocker.GetID())
|
|
|
|
return nil
|
|
}
|
|
|
|
// SelectVault selects a vault as the current one
|
|
func (cli *Instance) SelectVault(cmd *cobra.Command, name string) error {
|
|
release, err := vault.LockStateDir(cli.fs, cli.stateDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer release()
|
|
|
|
err = vault.SelectVault(cli.fs, cli.stateDir, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cmd.Printf("Selected vault '%s' as current\n", name)
|
|
|
|
return nil
|
|
}
|
|
|
|
// vaultImportPreflight verifies the vault exists without a long-term key
|
|
// and returns the vault directory, public key path, and validated mnemonic
|
|
func (cli *Instance) vaultImportPreflight(
|
|
vlt *vault.Vault, vaultName string,
|
|
) (string, string, string, error) {
|
|
// Check if vault exists
|
|
vaultDir, err := vlt.GetDirectory()
|
|
if err != nil {
|
|
return "", "", "", err
|
|
}
|
|
|
|
exists, err := afero.DirExists(cli.fs, vaultDir)
|
|
if err != nil {
|
|
return "", "", "", fmt.Errorf("failed to check if vault exists: %w", err)
|
|
}
|
|
|
|
if !exists {
|
|
return "", "", "", fmt.Errorf("vault '%s' %w",
|
|
vaultName, errVaultDoesNotExist)
|
|
}
|
|
|
|
// Check if vault already has a public key
|
|
pubKeyPath := vaultDir + "/pub.age"
|
|
|
|
exists, err = afero.Exists(cli.fs, pubKeyPath)
|
|
if err != nil {
|
|
return "", "", "", fmt.Errorf("failed to check %s: %w", pubKeyPath, err)
|
|
}
|
|
|
|
if exists {
|
|
return "", "", "", fmt.Errorf("vault '%s' %w",
|
|
vaultName, errVaultHasLongTermKey)
|
|
}
|
|
|
|
// Get mnemonic from environment
|
|
if cli.Mnemonic == nil {
|
|
return "", "", "", errMnemonicEnvNotSet
|
|
}
|
|
|
|
mnemonic := cli.Mnemonic.String()
|
|
|
|
// Validate the mnemonic
|
|
mnemonicWords := strings.Fields(mnemonic)
|
|
secret.Debug("Validating BIP39 mnemonic", "word_count", len(mnemonicWords))
|
|
|
|
if !bip39.IsMnemonicValid(mnemonic) {
|
|
return "", "", "", errInvalidMnemonic
|
|
}
|
|
|
|
return vaultDir, pubKeyPath, mnemonic, nil
|
|
}
|
|
|
|
// updateVaultImportMetadata stores the derivation info in vault metadata
|
|
func updateVaultImportMetadata(
|
|
fs afero.Fs, vaultDir string, derivationIndex uint32,
|
|
publicKeyHash, familyHash string,
|
|
) error {
|
|
// Load existing metadata
|
|
existingMetadata, err := vault.LoadVaultMetadata(fs, vaultDir)
|
|
if err != nil {
|
|
// If metadata doesn't exist, create new
|
|
existingMetadata = &vault.Metadata{
|
|
CreatedAt: time.Now(),
|
|
}
|
|
}
|
|
|
|
// Update metadata with new derivation info
|
|
existingMetadata.DerivationIndex = derivationIndex
|
|
existingMetadata.PublicKeyHash = publicKeyHash
|
|
existingMetadata.MnemonicFamilyHash = familyHash
|
|
|
|
err = vault.SaveVaultMetadata(fs, vaultDir, existingMetadata)
|
|
if err != nil {
|
|
secret.Debug("Failed to save vault metadata", "error", err)
|
|
|
|
return fmt.Errorf("failed to save vault metadata: %w", err)
|
|
}
|
|
|
|
secret.Debug("Saved vault metadata with derivation index and public key hash")
|
|
|
|
return nil
|
|
}
|
|
|
|
// VaultImport imports a mnemonic into a specific vault, holding the state
|
|
// directory lock while importMnemonic runs
|
|
func (cli *Instance) VaultImport(cmd *cobra.Command, vaultName string) error {
|
|
err := vault.ValidateVaultName(vaultName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
release, err := vault.LockStateDir(cli.fs, cli.stateDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer release()
|
|
|
|
return cli.importMnemonic(cmd, vaultName)
|
|
}
|
|
|
|
// importMnemonic gives the vault a long-term key derived from the mnemonic
|
|
// and a passphrase unlocker
|
|
func (cli *Instance) importMnemonic(cmd *cobra.Command, vaultName string) error {
|
|
secret.Debug("Importing mnemonic into vault",
|
|
"vault_name", vaultName, "state_dir", cli.stateDir)
|
|
|
|
// Get the specific vault by name
|
|
vlt := vault.NewVault(cli.fs, cli.stateDir, vaultName)
|
|
|
|
vaultDir, pubKeyPath, mnemonic, err := cli.vaultImportPreflight(vlt, vaultName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get the next available derivation index for this mnemonic
|
|
derivationIndex, err := vault.GetNextDerivationIndex(cli.fs, cli.stateDir, mnemonic)
|
|
if err != nil {
|
|
secret.Debug("Failed to get next derivation index", "error", err)
|
|
|
|
return fmt.Errorf("failed to get next derivation index: %w", err)
|
|
}
|
|
|
|
secret.Debug("Using derivation index", "index", derivationIndex)
|
|
|
|
// Derive long-term key from mnemonic with the appropriate index
|
|
secret.Debug("Deriving long-term key from mnemonic", "index", derivationIndex)
|
|
|
|
ltIdentity, err := agehd.DeriveIdentity(mnemonic, derivationIndex)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to derive long-term key: %w", err)
|
|
}
|
|
|
|
// Store long-term public key in vault
|
|
ltPublicKey := ltIdentity.Recipient().String()
|
|
secret.Debug("Storing long-term public key",
|
|
"pubkey", ltPublicKey, "vault_dir", vaultDir)
|
|
|
|
err = secret.WriteFileAtomic(cli.fs, pubKeyPath, []byte(ltPublicKey))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to store long-term public key: %w", err)
|
|
}
|
|
|
|
// Calculate public key hash from the actual derivation index being used
|
|
// This is used to verify that the derived key matches what was stored
|
|
publicKeyHash := vault.ComputeDoubleSHA256([]byte(ltIdentity.Recipient().String()))
|
|
|
|
// Calculate family hash from index 0 (same for all vaults with this
|
|
// mnemonic). This is used to identify which vaults belong to the same
|
|
// mnemonic family.
|
|
identity0, err := agehd.DeriveIdentity(mnemonic, 0)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to derive identity for index 0: %w", err)
|
|
}
|
|
|
|
familyHash := vault.ComputeDoubleSHA256([]byte(identity0.Recipient().String()))
|
|
|
|
err = updateVaultImportMetadata(
|
|
cli.fs, vaultDir, derivationIndex, publicKeyHash, familyHash)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get passphrase from environment variable
|
|
passphraseBuffer := cli.UnlockPassphrase
|
|
if passphraseBuffer == nil {
|
|
return errPassphraseEnvNotSet
|
|
}
|
|
|
|
secret.Debug("Using unlock passphrase from environment variable")
|
|
|
|
// Unlock the vault with the derived long-term key
|
|
vlt.Unlock(ltIdentity)
|
|
|
|
// Create passphrase-protected unlocker
|
|
secret.Debug("Creating passphrase-protected unlocker")
|
|
|
|
passphraseUnlocker, err := vlt.CreatePassphraseUnlocker(passphraseBuffer)
|
|
if err != nil {
|
|
secret.Debug("Failed to create unlocker", "error", err)
|
|
|
|
return fmt.Errorf("failed to create unlocker: %w", err)
|
|
}
|
|
|
|
cmd.Printf("Successfully imported mnemonic into vault '%s'\n", vaultName)
|
|
cmd.Printf("Long-term public key: %s\n", ltPublicKey)
|
|
cmd.Printf("Unlocker ID: %s\n", passphraseUnlocker.GetID())
|
|
|
|
return nil
|
|
}
|
|
|
|
// countVaultSecrets returns the number of secrets in the vault directory
|
|
func (cli *Instance) countVaultSecrets(vaultDir string) (int, error) {
|
|
secretsDir := filepath.Join(vaultDir, "secrets.d")
|
|
|
|
exists, err := afero.DirExists(cli.fs, secretsDir)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to check secrets directory %s: %w",
|
|
secretsDir, err)
|
|
}
|
|
|
|
if !exists {
|
|
return 0, nil
|
|
}
|
|
|
|
entries, err := afero.ReadDir(cli.fs, secretsDir)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to read secrets directory %s: %w",
|
|
secretsDir, err)
|
|
}
|
|
|
|
return len(entries), nil
|
|
}
|
|
|
|
// switchAwayFromVault selects another vault as current before removal
|
|
func (cli *Instance) switchAwayFromVault(
|
|
cmd *cobra.Command, vaults []string, name string,
|
|
) error {
|
|
// Find another vault to switch to
|
|
var newVault string
|
|
|
|
for _, v := range vaults {
|
|
if v != name {
|
|
newVault = v
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
// Switch to the new vault
|
|
err := vault.SelectVault(cli.fs, cli.stateDir, newVault)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to switch to vault '%s': %w", newVault, err)
|
|
}
|
|
|
|
cmd.Printf("Switched current vault to '%s'\n", newVault)
|
|
|
|
return nil
|
|
}
|
|
|
|
// RemoveVault removes a vault and all its secrets, after asking the user
|
|
// to confirm unless force is set.
|
|
func (cli *Instance) RemoveVault(cmd *cobra.Command, name string, force bool) error {
|
|
err := vault.ValidateVaultName(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var found vaultToRemove
|
|
|
|
release, err := cli.askThenLock(cmd, force, func() (string, error) {
|
|
var err error
|
|
|
|
found, err = cli.findVaultToRemove(name)
|
|
|
|
return found.question, err
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer release()
|
|
|
|
// If removing current vault, switch to another vault first
|
|
if found.isCurrent {
|
|
err = cli.switchAwayFromVault(cmd, found.vaults, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Remove the vault directory
|
|
err = secret.RemoveDirAtomic(cli.fs, found.dir)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to remove vault directory: %w", err)
|
|
}
|
|
|
|
cmd.Printf("Removed vault '%s'\n", name)
|
|
|
|
if found.secrets > 0 {
|
|
cmd.Printf("Warning: Vault contained secrets that have been " +
|
|
"permanently deleted\n")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// vaultToRemove is what removing a vault removes, as findVaultToRemove
|
|
// found it.
|
|
type vaultToRemove struct {
|
|
// dir is the vault's directory, which holds all its secrets.
|
|
dir string
|
|
secrets int
|
|
// vaults lists every vault, this one included, and isCurrent is set
|
|
// when this one is the current vault.
|
|
vaults []string
|
|
isCurrent bool
|
|
// question names what is removed, for the user to confirm.
|
|
question string
|
|
}
|
|
|
|
// findVaultToRemove checks that the vault exists and is not the last one,
|
|
// and counts its secrets.
|
|
func (cli *Instance) findVaultToRemove(name string) (vaultToRemove, error) {
|
|
vaults, err := vault.ListVaults(cli.fs, cli.stateDir)
|
|
if err != nil {
|
|
return vaultToRemove{}, fmt.Errorf("failed to list vaults: %w", err)
|
|
}
|
|
|
|
if !slices.Contains(vaults, name) {
|
|
return vaultToRemove{},
|
|
fmt.Errorf("vault '%s' %w", name, errVaultDoesNotExist)
|
|
}
|
|
|
|
if len(vaults) == 1 {
|
|
return vaultToRemove{}, errCannotRemoveLastVault
|
|
}
|
|
|
|
currentVault, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
|
if err != nil {
|
|
return vaultToRemove{},
|
|
fmt.Errorf("failed to get current vault: %w", err)
|
|
}
|
|
|
|
vaultDir, err := vault.NewVault(cli.fs, cli.stateDir, name).GetDirectory()
|
|
if err != nil {
|
|
return vaultToRemove{},
|
|
fmt.Errorf("failed to get vault directory: %w", err)
|
|
}
|
|
|
|
secrets, err := cli.countVaultSecrets(vaultDir)
|
|
if err != nil {
|
|
return vaultToRemove{}, err
|
|
}
|
|
|
|
return vaultToRemove{
|
|
dir: vaultDir,
|
|
secrets: secrets,
|
|
vaults: vaults,
|
|
isCurrent: currentVault.GetName() == name,
|
|
question: fmt.Sprintf(
|
|
"Permanently remove vault '%s' and its %d secret(s)?",
|
|
name, secrets),
|
|
}, nil
|
|
}
|