github.com/iaas-resource-provision/iaas-rpc@v1.0.7-0.20211021023331-ed21f798c408/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/iaas-resource-provision/iaas-rpc/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  		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  
   180  func (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  }