kubeform.dev/terraform-backend-sdk@v0.0.0-20220310143633-45f07fe731c5/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 "kubeform.dev/terraform-backend-sdk/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 }