mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-28 01:41:48 -06:00
36d0a50427
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.
191 lines
4.2 KiB
Go
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,
|
|
}
|
|
}
|
|
}
|