github.com/muratcelep/terraform@v1.1.0-beta2-not-internal-4/not-internal/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/bgentry/speakeasy"
    19  	"github.com/muratcelep/terraform/not-internal/terraform"
    20  	"github.com/mattn/go-isatty"
    21  	"github.com/mitchellh/colorstring"
    22  )
    23  
    24  var defaultInputReader io.Reader
    25  var defaultInputWriter io.Writer
    26  var testInputResponse []string
    27  var testInputResponseMap map[string]string
    28  
    29  // UIInput is an implementation of terraform.UIInput that asks the CLI
    30  // for input stdin.
    31  type 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  
    49  func (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  		delete(testInputResponseMap, opts.Id)
    94  		return v, nil
    95  	}
    96  
    97  	log.Printf("[DEBUG] command: asking for input: %q", opts.Query)
    98  
    99  	// Listen for interrupts so we can cancel the input ask
   100  	sigCh := make(chan os.Signal, 1)
   101  	signal.Notify(sigCh, os.Interrupt)
   102  	defer signal.Stop(sigCh)
   103  
   104  	// Build the output format for asking
   105  	var buf bytes.Buffer
   106  	buf.WriteString("[reset]")
   107  	buf.WriteString(fmt.Sprintf("[bold]%s[reset]\n", opts.Query))
   108  	if opts.Description != "" {
   109  		s := bufio.NewScanner(strings.NewReader(opts.Description))
   110  		for s.Scan() {
   111  			buf.WriteString(fmt.Sprintf("  %s\n", s.Text()))
   112  		}
   113  		buf.WriteString("\n")
   114  	}
   115  	if opts.Default != "" {
   116  		buf.WriteString("  [bold]Default:[reset] ")
   117  		buf.WriteString(opts.Default)
   118  		buf.WriteString("\n")
   119  	}
   120  	buf.WriteString("  [bold]Enter a value:[reset] ")
   121  
   122  	// Ask the user for their input
   123  	if _, err := fmt.Fprint(w, i.Colorize.Color(buf.String())); err != nil {
   124  		return "", err
   125  	}
   126  
   127  	// Listen for the input in a goroutine. This will allow us to
   128  	// interrupt this if we are interrupted (SIGINT).
   129  	go func() {
   130  		if !atomic.CompareAndSwapInt32(&i.listening, 0, 1) {
   131  			return // We are already listening for input.
   132  		}
   133  		defer atomic.CompareAndSwapInt32(&i.listening, 1, 0)
   134  
   135  		var line string
   136  		var err error
   137  		if opts.Secret && isatty.IsTerminal(os.Stdin.Fd()) {
   138  			line, err = speakeasy.Ask("")
   139  		} else {
   140  			buf := bufio.NewReader(r)
   141  			line, err = buf.ReadString('\n')
   142  		}
   143  		if err != nil {
   144  			log.Printf("[ERR] UIInput scan err: %s", err)
   145  			i.err <- string(err.Error())
   146  		} else {
   147  			i.result <- strings.TrimRightFunc(line, unicode.IsSpace)
   148  		}
   149  	}()
   150  
   151  	select {
   152  	case err := <-i.err:
   153  		return "", errors.New(err)
   154  
   155  	case line := <-i.result:
   156  		fmt.Fprint(w, "\n")
   157  
   158  		if line == "" {
   159  			line = opts.Default
   160  		}
   161  
   162  		return line, nil
   163  	case <-ctx.Done():
   164  		// Print a newline so that any further output starts properly
   165  		// on a new line.
   166  		fmt.Fprintln(w)
   167  
   168  		return "", ctx.Err()
   169  	case <-sigCh:
   170  		// Print a newline so that any further output starts properly
   171  		// on a new line.
   172  		fmt.Fprintln(w)
   173  
   174  		// Mark that we were interrupted so future Ask calls fail.
   175  		i.interrupted = true
   176  
   177  		return "", errors.New("interrupted")
   178  	}
   179  }
   180  
   181  func (i *UIInput) init() {
   182  	i.result = make(chan string)
   183  	i.err = make(chan string)
   184  
   185  	if i.Colorize == nil {
   186  		i.Colorize = &colorstring.Colorize{
   187  			Colors:  colorstring.DefaultColors,
   188  			Disable: true,
   189  		}
   190  	}
   191  }