...

Source file src/k8s.io/kubectl/pkg/cmd/exec/exec.go

Documentation: k8s.io/kubectl/pkg/cmd/exec

     1  /*
     2  Copyright 2014 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package exec
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"io"
    23  	"net/url"
    24  	"time"
    25  
    26  	dockerterm "github.com/moby/term"
    27  	"github.com/spf13/cobra"
    28  	corev1 "k8s.io/api/core/v1"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/apimachinery/pkg/util/httpstream"
    31  	"k8s.io/cli-runtime/pkg/genericclioptions"
    32  	"k8s.io/cli-runtime/pkg/genericiooptions"
    33  	"k8s.io/cli-runtime/pkg/resource"
    34  	coreclient "k8s.io/client-go/kubernetes/typed/core/v1"
    35  	restclient "k8s.io/client-go/rest"
    36  	"k8s.io/client-go/tools/remotecommand"
    37  
    38  	"k8s.io/apimachinery/pkg/api/meta"
    39  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    40  	"k8s.io/kubectl/pkg/cmd/util/podcmd"
    41  	"k8s.io/kubectl/pkg/polymorphichelpers"
    42  	"k8s.io/kubectl/pkg/scheme"
    43  	"k8s.io/kubectl/pkg/util/completion"
    44  	"k8s.io/kubectl/pkg/util/i18n"
    45  	"k8s.io/kubectl/pkg/util/interrupt"
    46  	"k8s.io/kubectl/pkg/util/templates"
    47  	"k8s.io/kubectl/pkg/util/term"
    48  )
    49  
    50  var (
    51  	execExample = templates.Examples(i18n.T(`
    52  		# Get output from running the 'date' command from pod mypod, using the first container by default
    53  		kubectl exec mypod -- date
    54  
    55  		# Get output from running the 'date' command in ruby-container from pod mypod
    56  		kubectl exec mypod -c ruby-container -- date
    57  
    58  		# Switch to raw terminal mode; sends stdin to 'bash' in ruby-container from pod mypod
    59  		# and sends stdout/stderr from 'bash' back to the client
    60  		kubectl exec mypod -c ruby-container -i -t -- bash -il
    61  
    62  		# List contents of /usr from the first container of pod mypod and sort by modification time
    63  		# If the command you want to execute in the pod has any flags in common (e.g. -i),
    64  		# you must use two dashes (--) to separate your command's flags/arguments
    65  		# Also note, do not surround your command and its flags/arguments with quotes
    66  		# unless that is how you would execute it normally (i.e., do ls -t /usr, not "ls -t /usr")
    67  		kubectl exec mypod -i -t -- ls -t /usr
    68  
    69  		# Get output from running 'date' command from the first pod of the deployment mydeployment, using the first container by default
    70  		kubectl exec deploy/mydeployment -- date
    71  
    72  		# Get output from running 'date' command from the first pod of the service myservice, using the first container by default
    73  		kubectl exec svc/myservice -- date
    74  		`))
    75  )
    76  
    77  const (
    78  	defaultPodExecTimeout = 60 * time.Second
    79  )
    80  
    81  func NewCmdExec(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
    82  	options := &ExecOptions{
    83  		StreamOptions: StreamOptions{
    84  			IOStreams: streams,
    85  		},
    86  
    87  		Executor: &DefaultRemoteExecutor{},
    88  	}
    89  	cmd := &cobra.Command{
    90  		Use:                   "exec (POD | TYPE/NAME) [-c CONTAINER] [flags] -- COMMAND [args...]",
    91  		DisableFlagsInUseLine: true,
    92  		Short:                 i18n.T("Execute a command in a container"),
    93  		Long:                  i18n.T("Execute a command in a container."),
    94  		Example:               execExample,
    95  		ValidArgsFunction:     completion.PodResourceNameCompletionFunc(f),
    96  		Run: func(cmd *cobra.Command, args []string) {
    97  			argsLenAtDash := cmd.ArgsLenAtDash()
    98  			cmdutil.CheckErr(options.Complete(f, cmd, args, argsLenAtDash))
    99  			cmdutil.CheckErr(options.Validate())
   100  			cmdutil.CheckErr(options.Run())
   101  		},
   102  	}
   103  	cmdutil.AddPodRunningTimeoutFlag(cmd, defaultPodExecTimeout)
   104  	cmdutil.AddJsonFilenameFlag(cmd.Flags(), &options.FilenameOptions.Filenames, "to use to exec into the resource")
   105  	// TODO support UID
   106  	cmdutil.AddContainerVarFlags(cmd, &options.ContainerName, options.ContainerName)
   107  	cmdutil.CheckErr(cmd.RegisterFlagCompletionFunc("container", completion.ContainerCompletionFunc(f)))
   108  
   109  	cmd.Flags().BoolVarP(&options.Stdin, "stdin", "i", options.Stdin, "Pass stdin to the container")
   110  	cmd.Flags().BoolVarP(&options.TTY, "tty", "t", options.TTY, "Stdin is a TTY")
   111  	cmd.Flags().BoolVarP(&options.Quiet, "quiet", "q", options.Quiet, "Only print output from the remote session")
   112  	return cmd
   113  }
   114  
   115  // RemoteExecutor defines the interface accepted by the Exec command - provided for test stubbing
   116  type RemoteExecutor interface {
   117  	Execute(url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error
   118  }
   119  
   120  // DefaultRemoteExecutor is the standard implementation of remote command execution
   121  type DefaultRemoteExecutor struct{}
   122  
   123  func (*DefaultRemoteExecutor) Execute(url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error {
   124  	exec, err := createExecutor(url, config)
   125  	if err != nil {
   126  		return err
   127  	}
   128  	return exec.StreamWithContext(context.Background(), remotecommand.StreamOptions{
   129  		Stdin:             stdin,
   130  		Stdout:            stdout,
   131  		Stderr:            stderr,
   132  		Tty:               tty,
   133  		TerminalSizeQueue: terminalSizeQueue,
   134  	})
   135  }
   136  
   137  // createExecutor returns the Executor or an error if one occurred.
   138  func createExecutor(url *url.URL, config *restclient.Config) (remotecommand.Executor, error) {
   139  	exec, err := remotecommand.NewSPDYExecutor(config, "POST", url)
   140  	if err != nil {
   141  		return nil, err
   142  	}
   143  	// Fallback executor is default, unless feature flag is explicitly disabled.
   144  	if !cmdutil.RemoteCommandWebsockets.IsDisabled() {
   145  		// WebSocketExecutor must be "GET" method as described in RFC 6455 Sec. 4.1 (page 17).
   146  		websocketExec, err := remotecommand.NewWebSocketExecutor(config, "GET", url.String())
   147  		if err != nil {
   148  			return nil, err
   149  		}
   150  		exec, err = remotecommand.NewFallbackExecutor(websocketExec, exec, httpstream.IsUpgradeFailure)
   151  		if err != nil {
   152  			return nil, err
   153  		}
   154  	}
   155  	return exec, nil
   156  }
   157  
   158  type StreamOptions struct {
   159  	Namespace     string
   160  	PodName       string
   161  	ContainerName string
   162  	Stdin         bool
   163  	TTY           bool
   164  	// minimize unnecessary output
   165  	Quiet bool
   166  	// InterruptParent, if set, is used to handle interrupts while attached
   167  	InterruptParent *interrupt.Handler
   168  
   169  	genericiooptions.IOStreams
   170  
   171  	// for testing
   172  	overrideStreams func() (io.ReadCloser, io.Writer, io.Writer)
   173  	isTerminalIn    func(t term.TTY) bool
   174  }
   175  
   176  // ExecOptions declare the arguments accepted by the Exec command
   177  type ExecOptions struct {
   178  	StreamOptions
   179  	resource.FilenameOptions
   180  
   181  	ResourceName     string
   182  	Command          []string
   183  	EnforceNamespace bool
   184  
   185  	Builder          func() *resource.Builder
   186  	ExecutablePodFn  polymorphichelpers.AttachablePodForObjectFunc
   187  	restClientGetter genericclioptions.RESTClientGetter
   188  
   189  	Pod           *corev1.Pod
   190  	Executor      RemoteExecutor
   191  	PodClient     coreclient.PodsGetter
   192  	GetPodTimeout time.Duration
   193  	Config        *restclient.Config
   194  }
   195  
   196  // Complete verifies command line arguments and loads data from the command environment
   197  func (p *ExecOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, argsIn []string, argsLenAtDash int) error {
   198  	if len(argsIn) > 0 && argsLenAtDash != 0 {
   199  		p.ResourceName = argsIn[0]
   200  	}
   201  	if argsLenAtDash > -1 {
   202  		p.Command = argsIn[argsLenAtDash:]
   203  	} else if len(argsIn) > 1 {
   204  		if !p.Quiet {
   205  			fmt.Fprint(p.ErrOut, "kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.\n")
   206  		}
   207  		p.Command = argsIn[1:]
   208  	} else if len(argsIn) > 0 && len(p.FilenameOptions.Filenames) != 0 {
   209  		if !p.Quiet {
   210  			fmt.Fprint(p.ErrOut, "kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.\n")
   211  		}
   212  		p.Command = argsIn[0:]
   213  		p.ResourceName = ""
   214  	}
   215  
   216  	var err error
   217  	p.Namespace, p.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
   218  	if err != nil {
   219  		return err
   220  	}
   221  
   222  	p.ExecutablePodFn = polymorphichelpers.AttachablePodForObjectFn
   223  
   224  	p.GetPodTimeout, err = cmdutil.GetPodRunningTimeoutFlag(cmd)
   225  	if err != nil {
   226  		return cmdutil.UsageErrorf(cmd, err.Error())
   227  	}
   228  
   229  	p.Builder = f.NewBuilder
   230  	p.restClientGetter = f
   231  
   232  	p.Config, err = f.ToRESTConfig()
   233  	if err != nil {
   234  		return err
   235  	}
   236  
   237  	clientset, err := f.KubernetesClientSet()
   238  	if err != nil {
   239  		return err
   240  	}
   241  	p.PodClient = clientset.CoreV1()
   242  
   243  	return nil
   244  }
   245  
   246  // Validate checks that the provided exec options are specified.
   247  func (p *ExecOptions) Validate() error {
   248  	if len(p.PodName) == 0 && len(p.ResourceName) == 0 && len(p.FilenameOptions.Filenames) == 0 {
   249  		return fmt.Errorf("pod, type/name or --filename must be specified")
   250  	}
   251  	if len(p.Command) == 0 {
   252  		return fmt.Errorf("you must specify at least one command for the container")
   253  	}
   254  	if p.Out == nil || p.ErrOut == nil {
   255  		return fmt.Errorf("both output and error output must be provided")
   256  	}
   257  	return nil
   258  }
   259  
   260  func (o *StreamOptions) SetupTTY() term.TTY {
   261  	t := term.TTY{
   262  		Parent: o.InterruptParent,
   263  		Out:    o.Out,
   264  	}
   265  
   266  	if !o.Stdin {
   267  		// need to nil out o.In to make sure we don't create a stream for stdin
   268  		o.In = nil
   269  		o.TTY = false
   270  		return t
   271  	}
   272  
   273  	t.In = o.In
   274  	if !o.TTY {
   275  		return t
   276  	}
   277  
   278  	if o.isTerminalIn == nil {
   279  		o.isTerminalIn = func(tty term.TTY) bool {
   280  			return tty.IsTerminalIn()
   281  		}
   282  	}
   283  	if !o.isTerminalIn(t) {
   284  		o.TTY = false
   285  
   286  		if !o.Quiet && o.ErrOut != nil {
   287  			fmt.Fprintln(o.ErrOut, "Unable to use a TTY - input is not a terminal or the right kind of file")
   288  		}
   289  
   290  		return t
   291  	}
   292  
   293  	// if we get to here, the user wants to attach stdin, wants a TTY, and o.In is a terminal, so we
   294  	// can safely set t.Raw to true
   295  	t.Raw = true
   296  
   297  	if o.overrideStreams == nil {
   298  		// use dockerterm.StdStreams() to get the right I/O handles on Windows
   299  		o.overrideStreams = dockerterm.StdStreams
   300  	}
   301  	stdin, stdout, _ := o.overrideStreams()
   302  	o.In = stdin
   303  	t.In = stdin
   304  	if o.Out != nil {
   305  		o.Out = stdout
   306  		t.Out = stdout
   307  	}
   308  
   309  	return t
   310  }
   311  
   312  // Run executes a validated remote execution against a pod.
   313  func (p *ExecOptions) Run() error {
   314  	var err error
   315  	// we still need legacy pod getter when PodName in ExecOptions struct is provided,
   316  	// since there are any other command run this function by providing Podname with PodsGetter
   317  	// and without resource builder, eg: `kubectl cp`.
   318  	if len(p.PodName) != 0 {
   319  		p.Pod, err = p.PodClient.Pods(p.Namespace).Get(context.TODO(), p.PodName, metav1.GetOptions{})
   320  		if err != nil {
   321  			return err
   322  		}
   323  	} else {
   324  		builder := p.Builder().
   325  			WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...).
   326  			FilenameParam(p.EnforceNamespace, &p.FilenameOptions).
   327  			NamespaceParam(p.Namespace).DefaultNamespace()
   328  		if len(p.ResourceName) > 0 {
   329  			builder = builder.ResourceNames("pods", p.ResourceName)
   330  		}
   331  
   332  		obj, err := builder.Do().Object()
   333  		if err != nil {
   334  			return err
   335  		}
   336  
   337  		if meta.IsListType(obj) {
   338  			return fmt.Errorf("cannot exec into multiple objects at a time")
   339  		}
   340  
   341  		p.Pod, err = p.ExecutablePodFn(p.restClientGetter, obj, p.GetPodTimeout)
   342  		if err != nil {
   343  			return err
   344  		}
   345  	}
   346  
   347  	pod := p.Pod
   348  
   349  	if pod.Status.Phase == corev1.PodSucceeded || pod.Status.Phase == corev1.PodFailed {
   350  		return fmt.Errorf("cannot exec into a container in a completed pod; current phase is %s", pod.Status.Phase)
   351  	}
   352  
   353  	containerName := p.ContainerName
   354  	if len(containerName) == 0 {
   355  		container, err := podcmd.FindOrDefaultContainerByName(pod, containerName, p.Quiet, p.ErrOut)
   356  		if err != nil {
   357  			return err
   358  		}
   359  		containerName = container.Name
   360  	}
   361  
   362  	// ensure we can recover the terminal while attached
   363  	t := p.SetupTTY()
   364  
   365  	var sizeQueue remotecommand.TerminalSizeQueue
   366  	if t.Raw {
   367  		// this call spawns a goroutine to monitor/update the terminal size
   368  		sizeQueue = t.MonitorSize(t.GetSize())
   369  
   370  		// unset p.Err if it was previously set because both stdout and stderr go over p.Out when tty is
   371  		// true
   372  		p.ErrOut = nil
   373  	}
   374  
   375  	fn := func() error {
   376  		restClient, err := restclient.RESTClientFor(p.Config)
   377  		if err != nil {
   378  			return err
   379  		}
   380  
   381  		// TODO: consider abstracting into a client invocation or client helper
   382  		req := restClient.Post().
   383  			Resource("pods").
   384  			Name(pod.Name).
   385  			Namespace(pod.Namespace).
   386  			SubResource("exec")
   387  		req.VersionedParams(&corev1.PodExecOptions{
   388  			Container: containerName,
   389  			Command:   p.Command,
   390  			Stdin:     p.Stdin,
   391  			Stdout:    p.Out != nil,
   392  			Stderr:    p.ErrOut != nil,
   393  			TTY:       t.Raw,
   394  		}, scheme.ParameterCodec)
   395  
   396  		return p.Executor.Execute(req.URL(), p.Config, p.In, p.Out, p.ErrOut, t.Raw, sizeQueue)
   397  	}
   398  
   399  	if err := t.Safe(fn); err != nil {
   400  		return err
   401  	}
   402  
   403  	return nil
   404  }
   405  

View as plain text