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  }