opentofu/internal/command/ui_input.go
Martin Atkins 36d0a50427 Move terraform/ to internal/terraform/
This is part of a general effort to move all of Terraform's non-library
package surface under internal in order to reinforce that these are for
internal use within Terraform only.

If you were previously importing packages under this prefix into an
external codebase, you could pin to an earlier release tag as an interim
solution until you've make a plan to achieve the same functionality some
other way.
2021-05-17 14:09:07 -07:00

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