mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-30 10:47:14 -06:00
8684a85e26
Remove answers from testInputResponse as they are given, and raise an
error during cleanup if any answers remain unused.
This enables tests to ensure that the expected mock answers are actually
used in a test; previously, an entire branch of code including an input
sequence could be omitted and the test(s) would not fail.
The only test that had unused answers in this map is one leftover from
legacy state migrations, a prompt that was removed in
7c93b2e5e6
192 lines
4.2 KiB
Go
192 lines
4.2 KiB
Go
package command
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"os/signal"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"unicode"
|
|
|
|
"github.com/bgentry/speakeasy"
|
|
"github.com/hashicorp/terraform/internal/terraform"
|
|
"github.com/mattn/go-isatty"
|
|
"github.com/mitchellh/colorstring"
|
|
)
|
|
|
|
var defaultInputReader io.Reader
|
|
var defaultInputWriter io.Writer
|
|
var testInputResponse []string
|
|
var testInputResponseMap map[string]string
|
|
|
|
// UIInput is an implementation of terraform.UIInput that asks the CLI
|
|
// for input stdin.
|
|
type UIInput struct {
|
|
// Colorize will color the output.
|
|
Colorize *colorstring.Colorize
|
|
|
|
// Reader and Writer for IO. If these aren't set, they will default to
|
|
// Stdin and Stdout respectively.
|
|
Reader io.Reader
|
|
Writer io.Writer
|
|
|
|
listening int32
|
|
result chan string
|
|
err chan string
|
|
|
|
interrupted bool
|
|
l sync.Mutex
|
|
once sync.Once
|
|
}
|
|
|
|
func (i *UIInput) Input(ctx context.Context, opts *terraform.InputOpts) (string, error) {
|
|
i.once.Do(i.init)
|
|
|
|
r := i.Reader
|
|
w := i.Writer
|
|
if r == nil {
|
|
r = defaultInputReader
|
|
}
|
|
if w == nil {
|
|
w = defaultInputWriter
|
|
}
|
|
if r == nil {
|
|
r = os.Stdin
|
|
}
|
|
if w == nil {
|
|
w = os.Stdout
|
|
}
|
|
|
|
// Make sure we only ask for input once at a time. Terraform
|
|
// should enforce this, but it doesn't hurt to verify.
|
|
i.l.Lock()
|
|
defer i.l.Unlock()
|
|
|
|
// If we're interrupted, then don't ask for input
|
|
if i.interrupted {
|
|
return "", errors.New("interrupted")
|
|
}
|
|
|
|
// If we have test results, return those. testInputResponse is the
|
|
// "old" way of doing it and we should remove that.
|
|
if testInputResponse != nil {
|
|
v := testInputResponse[0]
|
|
testInputResponse = testInputResponse[1:]
|
|
return v, nil
|
|
}
|
|
|
|
// testInputResponseMap is the new way for test responses, based on
|
|
// the query ID.
|
|
if testInputResponseMap != nil {
|
|
v, ok := testInputResponseMap[opts.Id]
|
|
if !ok {
|
|
return "", fmt.Errorf("unexpected input request in test: %s", opts.Id)
|
|
}
|
|
|
|
delete(testInputResponseMap, opts.Id)
|
|
return v, nil
|
|
}
|
|
|
|
log.Printf("[DEBUG] command: asking for input: %q", opts.Query)
|
|
|
|
// Listen for interrupts so we can cancel the input ask
|
|
sigCh := make(chan os.Signal, 1)
|
|
signal.Notify(sigCh, os.Interrupt)
|
|
defer signal.Stop(sigCh)
|
|
|
|
// Build the output format for asking
|
|
var buf bytes.Buffer
|
|
buf.WriteString("[reset]")
|
|
buf.WriteString(fmt.Sprintf("[bold]%s[reset]\n", opts.Query))
|
|
if opts.Description != "" {
|
|
s := bufio.NewScanner(strings.NewReader(opts.Description))
|
|
for s.Scan() {
|
|
buf.WriteString(fmt.Sprintf(" %s\n", s.Text()))
|
|
}
|
|
buf.WriteString("\n")
|
|
}
|
|
if opts.Default != "" {
|
|
buf.WriteString(" [bold]Default:[reset] ")
|
|
buf.WriteString(opts.Default)
|
|
buf.WriteString("\n")
|
|
}
|
|
buf.WriteString(" [bold]Enter a value:[reset] ")
|
|
|
|
// Ask the user for their input
|
|
if _, err := fmt.Fprint(w, i.Colorize.Color(buf.String())); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// Listen for the input in a goroutine. This will allow us to
|
|
// interrupt this if we are interrupted (SIGINT).
|
|
go func() {
|
|
if !atomic.CompareAndSwapInt32(&i.listening, 0, 1) {
|
|
return // We are already listening for input.
|
|
}
|
|
defer atomic.CompareAndSwapInt32(&i.listening, 1, 0)
|
|
|
|
var line string
|
|
var err error
|
|
if opts.Secret && isatty.IsTerminal(os.Stdin.Fd()) {
|
|
line, err = speakeasy.Ask("")
|
|
} else {
|
|
buf := bufio.NewReader(r)
|
|
line, err = buf.ReadString('\n')
|
|
}
|
|
if err != nil {
|
|
log.Printf("[ERR] UIInput scan err: %s", err)
|
|
i.err <- string(err.Error())
|
|
} else {
|
|
i.result <- strings.TrimRightFunc(line, unicode.IsSpace)
|
|
}
|
|
}()
|
|
|
|
select {
|
|
case err := <-i.err:
|
|
return "", errors.New(err)
|
|
|
|
case line := <-i.result:
|
|
fmt.Fprint(w, "\n")
|
|
|
|
if line == "" {
|
|
line = opts.Default
|
|
}
|
|
|
|
return line, nil
|
|
case <-ctx.Done():
|
|
// Print a newline so that any further output starts properly
|
|
// on a new line.
|
|
fmt.Fprintln(w)
|
|
|
|
return "", ctx.Err()
|
|
case <-sigCh:
|
|
// Print a newline so that any further output starts properly
|
|
// on a new line.
|
|
fmt.Fprintln(w)
|
|
|
|
// Mark that we were interrupted so future Ask calls fail.
|
|
i.interrupted = true
|
|
|
|
return "", errors.New("interrupted")
|
|
}
|
|
}
|
|
|
|
func (i *UIInput) init() {
|
|
i.result = make(chan string)
|
|
i.err = make(chan string)
|
|
|
|
if i.Colorize == nil {
|
|
i.Colorize = &colorstring.Colorize{
|
|
Colors: colorstring.DefaultColors,
|
|
Disable: true,
|
|
}
|
|
}
|
|
}
|