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, } } }