github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/command/alloc_exec.go (about) 1 package command 2 3 import ( 4 "bytes" 5 "context" 6 "errors" 7 "fmt" 8 "io" 9 "os" 10 "os/signal" 11 "strings" 12 "syscall" 13 14 "github.com/docker/docker/pkg/term" 15 "github.com/hashicorp/nomad/api" 16 "github.com/hashicorp/nomad/api/contexts" 17 "github.com/hashicorp/nomad/helper/escapingio" 18 "github.com/posener/complete" 19 ) 20 21 type AllocExecCommand struct { 22 Meta 23 24 Stdin io.Reader 25 Stdout io.WriteCloser 26 Stderr io.WriteCloser 27 } 28 29 func (l *AllocExecCommand) Help() string { 30 helpText := ` 31 Usage: nomad alloc exec [options] <allocation> <command> 32 33 Run command inside the environment of the given allocation and task. 34 35 When ACLs are enabled, this command requires a token with the 'alloc-exec', 36 'read-job', and 'list-jobs' capabilities for the allocation's namespace. If 37 the task driver does not have file system isolation (as with 'raw_exec'), 38 this command requires the 'alloc-node-exec', 'read-job', and 'list-jobs' 39 capabilities for the allocation's namespace. 40 41 General Options: 42 43 ` + generalOptionsUsage(usageOptsDefault) + ` 44 45 Exec Specific Options: 46 47 -task <task-name> 48 Sets the task to exec command in 49 50 -job 51 Use a random allocation from the specified job ID. 52 53 -i 54 Pass stdin to the container, defaults to true. Pass -i=false to disable. 55 56 -t 57 Allocate a pseudo-tty, defaults to true if stdin is detected to be a tty session. 58 Pass -t=false to disable explicitly. 59 60 -e <escape_char> 61 Sets the escape character for sessions with a pty (default: '~'). The escape 62 character is only recognized at the beginning of a line. The escape character 63 followed by a dot ('.') closes the connection. Setting the character to 64 'none' disables any escapes and makes the session fully transparent. 65 ` 66 return strings.TrimSpace(helpText) 67 } 68 69 func (l *AllocExecCommand) Synopsis() string { 70 return "Execute commands in task" 71 } 72 73 func (c *AllocExecCommand) AutocompleteFlags() complete.Flags { 74 return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient), 75 complete.Flags{ 76 "--task": complete.PredictAnything, 77 "-job": complete.PredictAnything, 78 "-i": complete.PredictNothing, 79 "-t": complete.PredictNothing, 80 "-e": complete.PredictSet("none", "~"), 81 }) 82 } 83 84 func (l *AllocExecCommand) AutocompleteArgs() complete.Predictor { 85 return complete.PredictFunc(func(a complete.Args) []string { 86 client, err := l.Meta.Client() 87 if err != nil { 88 return nil 89 } 90 91 resp, _, err := client.Search().PrefixSearch(a.Last, contexts.Allocs, nil) 92 if err != nil { 93 return []string{} 94 } 95 return resp.Matches[contexts.Allocs] 96 }) 97 } 98 99 func (l *AllocExecCommand) Name() string { return "alloc exec" } 100 101 func (l *AllocExecCommand) Run(args []string) int { 102 var job, stdinOpt, ttyOpt bool 103 var task, escapeChar string 104 105 flags := l.Meta.FlagSet(l.Name(), FlagSetClient) 106 flags.Usage = func() { l.Ui.Output(l.Help()) } 107 flags.BoolVar(&job, "job", false, "") 108 flags.BoolVar(&stdinOpt, "i", true, "") 109 flags.BoolVar(&ttyOpt, "t", isTty(), "") 110 flags.StringVar(&escapeChar, "e", "~", "") 111 flags.StringVar(&task, "task", "", "") 112 113 if err := flags.Parse(args); err != nil { 114 return 1 115 } 116 117 args = flags.Args() 118 119 if len(args) < 1 { 120 if job { 121 l.Ui.Error("A job ID is required") 122 } else { 123 l.Ui.Error("An allocation ID is required") 124 } 125 l.Ui.Error(commandErrorText(l)) 126 return 1 127 } 128 129 if !job && len(args[0]) == 1 { 130 l.Ui.Error("Alloc ID must contain at least two characters") 131 return 1 132 } 133 134 if len(args) < 2 { 135 l.Ui.Error("A command is required") 136 l.Ui.Error(commandErrorText(l)) 137 return 1 138 } 139 140 if ttyOpt && !stdinOpt { 141 l.Ui.Error("-i must be enabled if running with tty") 142 return 1 143 } 144 145 if escapeChar == "none" { 146 escapeChar = "" 147 } 148 149 if len(escapeChar) > 1 { 150 l.Ui.Error("-e requires 'none' or a single character") 151 return 1 152 } 153 154 client, err := l.Meta.Client() 155 if err != nil { 156 l.Ui.Error(fmt.Sprintf("Error initializing client: %v", err)) 157 return 1 158 } 159 160 var allocStub *api.AllocationListStub 161 if job { 162 jobID := args[0] 163 allocStub, err = getRandomJobAlloc(client, jobID) 164 if err != nil { 165 l.Ui.Error(fmt.Sprintf("Error fetching allocations: %v", err)) 166 return 1 167 } 168 } else { 169 allocID := args[0] 170 allocs, _, err := client.Allocations().PrefixList(sanitizeUUIDPrefix(allocID)) 171 if err != nil { 172 l.Ui.Error(fmt.Sprintf("Error querying allocation: %v", err)) 173 return 1 174 } 175 176 if len(allocs) == 0 { 177 l.Ui.Error(fmt.Sprintf("No allocation(s) with prefix or id %q found", allocID)) 178 return 1 179 } 180 181 if len(allocs) > 1 { 182 out := formatAllocListStubs(allocs, false, shortId) 183 l.Ui.Error(fmt.Sprintf("Prefix matched multiple allocations\n\n%s", out)) 184 return 1 185 } 186 187 allocStub = allocs[0] 188 } 189 190 q := &api.QueryOptions{Namespace: allocStub.Namespace} 191 alloc, _, err := client.Allocations().Info(allocStub.ID, q) 192 if err != nil { 193 l.Ui.Error(fmt.Sprintf("Error querying allocation: %s", err)) 194 return 1 195 } 196 197 if task != "" { 198 err = validateTaskExistsInAllocation(task, alloc) 199 } else { 200 task, err = lookupAllocTask(alloc) 201 } 202 if err != nil { 203 l.Ui.Error(err.Error()) 204 return 1 205 } 206 207 if !stdinOpt { 208 l.Stdin = bytes.NewReader(nil) 209 } 210 211 if l.Stdin == nil { 212 l.Stdin = os.Stdin 213 } 214 215 if l.Stdout == nil { 216 l.Stdout = os.Stdout 217 } 218 219 if l.Stderr == nil { 220 l.Stderr = os.Stderr 221 } 222 223 code, err := l.execImpl(client, alloc, task, ttyOpt, args[1:], escapeChar, l.Stdin, l.Stdout, l.Stderr) 224 if err != nil { 225 l.Ui.Error(fmt.Sprintf("failed to exec into task: %v", err)) 226 return 1 227 } 228 229 return code 230 } 231 232 // execImpl invokes the Alloc Exec api call, it also prepares and restores terminal states as necessary. 233 func (l *AllocExecCommand) execImpl(client *api.Client, alloc *api.Allocation, task string, tty bool, 234 command []string, escapeChar string, stdin io.Reader, stdout, stderr io.WriteCloser) (int, error) { 235 236 sizeCh := make(chan api.TerminalSize, 1) 237 238 ctx, cancelFn := context.WithCancel(context.Background()) 239 defer cancelFn() 240 241 // When tty, ensures we capture all user input and monitor terminal resizes. 242 if tty { 243 if stdin == nil { 244 return -1, fmt.Errorf("stdin is null") 245 } 246 247 inCleanup, err := setRawTerminal(stdin) 248 if err != nil { 249 return -1, err 250 } 251 defer inCleanup() 252 253 outCleanup, err := setRawTerminalOutput(stdout) 254 if err != nil { 255 return -1, err 256 } 257 defer outCleanup() 258 259 sizeCleanup, err := watchTerminalSize(stdout, sizeCh) 260 if err != nil { 261 return -1, err 262 } 263 defer sizeCleanup() 264 265 if escapeChar != "" { 266 stdin = escapingio.NewReader(stdin, escapeChar[0], func(c byte) bool { 267 switch c { 268 case '.': 269 // need to restore tty state so error reporting here 270 // gets emitted at beginning of line 271 outCleanup() 272 inCleanup() 273 274 stderr.Write([]byte("\nConnection closed\n")) 275 cancelFn() 276 return true 277 default: 278 return false 279 } 280 }) 281 } 282 } 283 284 signalCh := make(chan os.Signal, 1) 285 signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM) 286 go func() { 287 for range signalCh { 288 cancelFn() 289 } 290 }() 291 292 return client.Allocations().Exec(ctx, 293 alloc, task, tty, command, stdin, stdout, stderr, sizeCh, nil) 294 } 295 296 // isTty returns true if both stdin and stdout are a TTY 297 func isTty() bool { 298 _, isStdinTerminal := term.GetFdInfo(os.Stdin) 299 _, isStdoutTerminal := term.GetFdInfo(os.Stdout) 300 return isStdinTerminal && isStdoutTerminal 301 } 302 303 // setRawTerminal sets the stream terminal in raw mode, so process captures 304 // Ctrl+C and other commands to forward to remote process. 305 // It returns a cleanup function that restores terminal to original mode. 306 func setRawTerminal(stream interface{}) (cleanup func(), err error) { 307 fd, isTerminal := term.GetFdInfo(stream) 308 if !isTerminal { 309 return nil, errors.New("not a terminal") 310 } 311 312 state, err := term.SetRawTerminal(fd) 313 if err != nil { 314 return nil, err 315 } 316 317 return func() { term.RestoreTerminal(fd, state) }, nil 318 } 319 320 // setRawTerminalOutput sets the output stream in Windows to raw mode, 321 // so it disables LF -> CRLF translation. 322 // It's basically a no-op on unix. 323 func setRawTerminalOutput(stream interface{}) (cleanup func(), err error) { 324 fd, isTerminal := term.GetFdInfo(stream) 325 if !isTerminal { 326 return nil, errors.New("not a terminal") 327 } 328 329 state, err := term.SetRawTerminalOutput(fd) 330 if err != nil { 331 return nil, err 332 } 333 334 return func() { term.RestoreTerminal(fd, state) }, nil 335 } 336 337 // watchTerminalSize watches terminal size changes to propagate to remote tty. 338 func watchTerminalSize(out io.Writer, resize chan<- api.TerminalSize) (func(), error) { 339 fd, isTerminal := term.GetFdInfo(out) 340 if !isTerminal { 341 return nil, errors.New("not a terminal") 342 } 343 344 ctx, cancel := context.WithCancel(context.Background()) 345 346 signalCh := make(chan os.Signal, 1) 347 setupWindowNotification(signalCh) 348 349 sendTerminalSize := func() { 350 s, err := term.GetWinsize(fd) 351 if err != nil { 352 return 353 } 354 355 resize <- api.TerminalSize{ 356 Height: int(s.Height), 357 Width: int(s.Width), 358 } 359 } 360 go func() { 361 for { 362 select { 363 case <-ctx.Done(): 364 return 365 case <-signalCh: 366 sendTerminalSize() 367 } 368 } 369 }() 370 371 go func() { 372 // send initial size 373 sendTerminalSize() 374 }() 375 376 return cancel, nil 377 }