github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/command/ui_input.go (about)

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