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