1package command
2
3import (
4	"bufio"
5	"bytes"
6	"context"
7	"errors"
8	"fmt"
9	"io"
10	"log"
11	"os"
12	"os/signal"
13	"strings"
14	"sync"
15	"sync/atomic"
16	"unicode"
17
18	"github.com/bgentry/speakeasy"
19	"github.com/hashicorp/terraform/internal/terraform"
20	"github.com/mattn/go-isatty"
21	"github.com/mitchellh/colorstring"
22)
23
24var defaultInputReader io.Reader
25var defaultInputWriter io.Writer
26var testInputResponse []string
27var testInputResponseMap map[string]string
28
29// UIInput is an implementation of terraform.UIInput that asks the CLI
30// for input stdin.
31type UIInput struct {
32	// Colorize will color the output.
33	Colorize *colorstring.Colorize
34
35	// Reader and Writer for IO. If these aren't set, they will default to
36	// Stdin and Stdout respectively.
37	Reader io.Reader
38	Writer io.Writer
39
40	listening int32
41	result    chan string
42	err       chan string
43
44	interrupted bool
45	l           sync.Mutex
46	once        sync.Once
47}
48
49func (i *UIInput) Input(ctx context.Context, opts *terraform.InputOpts) (string, error) {
50	i.once.Do(i.init)
51
52	r := i.Reader
53	w := i.Writer
54	if r == nil {
55		r = defaultInputReader
56	}
57	if w == nil {
58		w = defaultInputWriter
59	}
60	if r == nil {
61		r = os.Stdin
62	}
63	if w == nil {
64		w = os.Stdout
65	}
66
67	// Make sure we only ask for input once at a time. Terraform
68	// should enforce this, but it doesn't hurt to verify.
69	i.l.Lock()
70	defer i.l.Unlock()
71
72	// If we're interrupted, then don't ask for input
73	if i.interrupted {
74		return "", errors.New("interrupted")
75	}
76
77	// If we have test results, return those. testInputResponse is the
78	// "old" way of doing it and we should remove that.
79	if testInputResponse != nil {
80		v := testInputResponse[0]
81		testInputResponse = testInputResponse[1:]
82		return v, nil
83	}
84
85	// testInputResponseMap is the new way for test responses, based on
86	// the query ID.
87	if testInputResponseMap != nil {
88		v, ok := testInputResponseMap[opts.Id]
89		if !ok {
90			return "", fmt.Errorf("unexpected input request in test: %s", opts.Id)
91		}
92
93		return v, nil
94	}
95
96	log.Printf("[DEBUG] command: asking for input: %q", opts.Query)
97
98	// Listen for interrupts so we can cancel the input ask
99	sigCh := make(chan os.Signal, 1)
100	signal.Notify(sigCh, os.Interrupt)
101	defer signal.Stop(sigCh)
102
103	// Build the output format for asking
104	var buf bytes.Buffer
105	buf.WriteString("[reset]")
106	buf.WriteString(fmt.Sprintf("[bold]%s[reset]\n", opts.Query))
107	if opts.Description != "" {
108		s := bufio.NewScanner(strings.NewReader(opts.Description))
109		for s.Scan() {
110			buf.WriteString(fmt.Sprintf("  %s\n", s.Text()))
111		}
112		buf.WriteString("\n")
113	}
114	if opts.Default != "" {
115		buf.WriteString("  [bold]Default:[reset] ")
116		buf.WriteString(opts.Default)
117		buf.WriteString("\n")
118	}
119	buf.WriteString("  [bold]Enter a value:[reset] ")
120
121	// Ask the user for their input
122	if _, err := fmt.Fprint(w, i.Colorize.Color(buf.String())); err != nil {
123		return "", err
124	}
125
126	// Listen for the input in a goroutine. This will allow us to
127	// interrupt this if we are interrupted (SIGINT).
128	go func() {
129		if !atomic.CompareAndSwapInt32(&i.listening, 0, 1) {
130			return // We are already listening for input.
131		}
132		defer atomic.CompareAndSwapInt32(&i.listening, 1, 0)
133
134		var line string
135		var err error
136		if opts.Secret && isatty.IsTerminal(os.Stdin.Fd()) {
137			line, err = speakeasy.Ask("")
138		} else {
139			buf := bufio.NewReader(r)
140			line, err = buf.ReadString('\n')
141		}
142		if err != nil {
143			log.Printf("[ERR] UIInput scan err: %s", err)
144			i.err <- string(err.Error())
145		} else {
146			i.result <- strings.TrimRightFunc(line, unicode.IsSpace)
147		}
148	}()
149
150	select {
151	case err := <-i.err:
152		return "", errors.New(err)
153
154	case line := <-i.result:
155		fmt.Fprint(w, "\n")
156
157		if line == "" {
158			line = opts.Default
159		}
160
161		return line, nil
162	case <-ctx.Done():
163		// Print a newline so that any further output starts properly
164		// on a new line.
165		fmt.Fprintln(w)
166
167		return "", ctx.Err()
168	case <-sigCh:
169		// Print a newline so that any further output starts properly
170		// on a new line.
171		fmt.Fprintln(w)
172
173		// Mark that we were interrupted so future Ask calls fail.
174		i.interrupted = true
175
176		return "", errors.New("interrupted")
177	}
178}
179
180func (i *UIInput) init() {
181	i.result = make(chan string)
182	i.err = make(chan string)
183
184	if i.Colorize == nil {
185		i.Colorize = &colorstring.Colorize{
186			Colors:  colorstring.DefaultColors,
187			Disable: true,
188		}
189	}
190}
191