check / check (push) Failing after 2s
secret rm, secret version rm, secret vault remove and secret unlocker remove ask [y/N] on a terminal, naming what they remove, and go ahead only on y or yes. Without --force, a command whose stdin is not a terminal fails at once. --force, now also on rm and version rm, removes without asking; it replaces the old refusals to remove a vault with secrets or the last unlocker without --force. The checks run and the question is asked before the state directory lock is taken; under the lock the checks run again, and nothing is removed if they would ask a different question. Model: opus-5-5
109 lines
2.9 KiB
Go
109 lines
2.9 KiB
Go
package cli
|
|
|
|
import (
|
|
"bufio"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
|
|
"git.eeqj.de/sneak/secret/internal/vault"
|
|
"github.com/spf13/cobra"
|
|
"golang.org/x/term"
|
|
)
|
|
|
|
// Sentinel errors for asking the user to confirm a removal
|
|
var (
|
|
errNoTerminal = errors.New("stdin is not a terminal, so there is " +
|
|
"nobody to ask for confirmation; pass --force to remove without asking")
|
|
errNotConfirmed = errors.New("cancelled; nothing was removed")
|
|
errChangedWhileAsking = errors.New("what was to be removed changed " +
|
|
"while waiting for the answer; nothing was removed")
|
|
)
|
|
|
|
// askThenLock asks the user to confirm a removal, unless force is set, and
|
|
// then takes the state directory lock and returns the function that
|
|
// releases it. find makes the command's checks, keeps what it found for
|
|
// the caller to remove, and returns the question that names it. find runs
|
|
// before the question, which is asked without the lock so that no other
|
|
// command waits while the user answers, and runs again once the lock is
|
|
// taken. That run is the last, so the caller removes what find found under
|
|
// the lock. If its question then differs from the one the user answered,
|
|
// something changed in between, and askThenLock fails.
|
|
func (cli *Instance) askThenLock(
|
|
cmd *cobra.Command, force bool, find func() (string, error),
|
|
) (func(), error) {
|
|
asked := ""
|
|
|
|
if !force {
|
|
question, err := find()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = cli.confirm(cmd, question)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
asked = question
|
|
}
|
|
|
|
release, err := vault.LockStateDir(cli.fs, cli.stateDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
question, err := find()
|
|
if err == nil && !force && question != asked {
|
|
err = errChangedWhileAsking
|
|
}
|
|
|
|
if err != nil {
|
|
release()
|
|
|
|
return nil, err
|
|
}
|
|
|
|
return release, nil
|
|
}
|
|
|
|
// confirm asks question and returns nil only when the user answers y or
|
|
// yes; any other answer, a bare Enter included, cancels. When stdin is not
|
|
// a terminal it asks nothing and fails at once: nobody is there to answer,
|
|
// and waiting for an answer would hang a script. Stdin decides, not
|
|
// stdout, because the answer is read from stdin: `secret rm foo | tee log`
|
|
// still asks. The question goes to stderr.
|
|
func (cli *Instance) confirm(cmd *cobra.Command, question string) error {
|
|
answers := cli.terminal
|
|
if answers == nil {
|
|
answers = cmd.InOrStdin()
|
|
|
|
if !isTerminal(answers) {
|
|
return errNoTerminal
|
|
}
|
|
}
|
|
|
|
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s [y/N] ", question)
|
|
|
|
answer, err := bufio.NewReader(answers).ReadString('\n')
|
|
if err != nil && !errors.Is(err, io.EOF) {
|
|
return fmt.Errorf("failed to read the answer: %w", err)
|
|
}
|
|
|
|
switch strings.ToLower(strings.TrimSpace(answer)) {
|
|
case "y", "yes":
|
|
return nil
|
|
default:
|
|
return errNotConfirmed
|
|
}
|
|
}
|
|
|
|
// isTerminal reports whether r is a terminal.
|
|
func isTerminal(r io.Reader) bool {
|
|
file, ok := r.(*os.File)
|
|
|
|
return ok && term.IsTerminal(int(file.Fd()))
|
|
}
|