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