...

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

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

     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 attach
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"io"
    23  	"net/url"
    24  	"strings"
    25  	"time"
    26  
    27  	"github.com/spf13/cobra"
    28  
    29  	corev1 "k8s.io/api/core/v1"
    30  	"k8s.io/apimachinery/pkg/runtime"
    31  	"k8s.io/apimachinery/pkg/util/httpstream"
    32  	"k8s.io/cli-runtime/pkg/genericclioptions"
    33  	"k8s.io/cli-runtime/pkg/genericiooptions"
    34  	"k8s.io/cli-runtime/pkg/resource"
    35  	restclient "k8s.io/client-go/rest"
    36  	"k8s.io/client-go/tools/remotecommand"
    37  	"k8s.io/kubectl/pkg/cmd/exec"
    38  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    39  	"k8s.io/kubectl/pkg/cmd/util/podcmd"
    40  	"k8s.io/kubectl/pkg/polymorphichelpers"
    41  	"k8s.io/kubectl/pkg/scheme"
    42  	"k8s.io/kubectl/pkg/util/completion"
    43  	"k8s.io/kubectl/pkg/util/i18n"
    44  	"k8s.io/kubectl/pkg/util/templates"
    45  )
    46  
    47  var (
    48  	attachExample = templates.Examples(i18n.T(`
    49  		# Get output from running pod mypod; use the 'kubectl.kubernetes.io/default-container' annotation
    50  		# for selecting the container to be attached or the first container in the pod will be chosen
    51  		kubectl attach mypod
    52  
    53  		# Get output from ruby-container from pod mypod
    54  		kubectl attach mypod -c ruby-container
    55  
    56  		# Switch to raw terminal mode; sends stdin to 'bash' in ruby-container from pod mypod
    57  		# and sends stdout/stderr from 'bash' back to the client
    58  		kubectl attach mypod -c ruby-container -i -t
    59  
    60  		# Get output from the first pod of a replica set named nginx
    61  		kubectl attach rs/nginx
    62  		`))
    63  )
    64  
    65  const (
    66  	defaultPodAttachTimeout = 60 * time.Second
    67  	defaultPodLogsTimeout   = 20 * time.Second
    68  )
    69  
    70  // AttachOptions declare the arguments accepted by the Attach command
    71  type AttachOptions struct {
    72  	exec.StreamOptions
    73  
    74  	// whether to disable use of standard error when streaming output from tty
    75  	DisableStderr bool
    76  
    77  	CommandName string
    78  
    79  	Pod *corev1.Pod
    80  
    81  	AttachFunc       func(*AttachOptions, *corev1.Container, bool, remotecommand.TerminalSizeQueue) func() error
    82  	Resources        []string
    83  	Builder          func() *resource.Builder
    84  	AttachablePodFn  polymorphichelpers.AttachablePodForObjectFunc
    85  	restClientGetter genericclioptions.RESTClientGetter
    86  
    87  	Attach        RemoteAttach
    88  	GetPodTimeout time.Duration
    89  	Config        *restclient.Config
    90  }
    91  
    92  // NewAttachOptions creates the options for attach
    93  func NewAttachOptions(streams genericiooptions.IOStreams) *AttachOptions {
    94  	return &AttachOptions{
    95  		StreamOptions: exec.StreamOptions{
    96  			IOStreams: streams,
    97  		},
    98  		Attach:     &DefaultRemoteAttach{},
    99  		AttachFunc: DefaultAttachFunc,
   100  	}
   101  }
   102  
   103  // NewCmdAttach returns the attach Cobra command
   104  func NewCmdAttach(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
   105  	o := NewAttachOptions(streams)
   106  	cmd := &cobra.Command{
   107  		Use:                   "attach (POD | TYPE/NAME) -c CONTAINER",
   108  		DisableFlagsInUseLine: true,
   109  		Short:                 i18n.T("Attach to a running container"),
   110  		Long:                  i18n.T("Attach to a process that is already running inside an existing container."),
   111  		Example:               attachExample,
   112  		ValidArgsFunction:     completion.PodResourceNameCompletionFunc(f),
   113  		Run: func(cmd *cobra.Command, args []string) {
   114  			cmdutil.CheckErr(o.Complete(f, cmd, args))
   115  			cmdutil.CheckErr(o.Validate())
   116  			cmdutil.CheckErr(o.Run())
   117  		},
   118  	}
   119  	cmdutil.AddPodRunningTimeoutFlag(cmd, defaultPodAttachTimeout)
   120  	cmdutil.AddContainerVarFlags(cmd, &o.ContainerName, o.ContainerName)
   121  	cmd.Flags().BoolVarP(&o.Stdin, "stdin", "i", o.Stdin, "Pass stdin to the container")
   122  	cmd.Flags().BoolVarP(&o.TTY, "tty", "t", o.TTY, "Stdin is a TTY")
   123  	cmd.Flags().BoolVarP(&o.Quiet, "quiet", "q", o.Quiet, "Only print output from the remote session")
   124  	return cmd
   125  }
   126  
   127  // RemoteAttach defines the interface accepted by the Attach command - provided for test stubbing
   128  type RemoteAttach interface {
   129  	Attach(url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error
   130  }
   131  
   132  // DefaultAttachFunc is the default AttachFunc used
   133  func DefaultAttachFunc(o *AttachOptions, containerToAttach *corev1.Container, raw bool, sizeQueue remotecommand.TerminalSizeQueue) func() error {
   134  	return func() error {
   135  		restClient, err := restclient.RESTClientFor(o.Config)
   136  		if err != nil {
   137  			return err
   138  		}
   139  		req := restClient.Post().
   140  			Resource("pods").
   141  			Name(o.Pod.Name).
   142  			Namespace(o.Pod.Namespace).
   143  			SubResource("attach")
   144  		req.VersionedParams(&corev1.PodAttachOptions{
   145  			Container: containerToAttach.Name,
   146  			Stdin:     o.Stdin,
   147  			Stdout:    o.Out != nil,
   148  			Stderr:    !o.DisableStderr,
   149  			TTY:       raw,
   150  		}, scheme.ParameterCodec)
   151  
   152  		return o.Attach.Attach(req.URL(), o.Config, o.In, o.Out, o.ErrOut, raw, sizeQueue)
   153  	}
   154  }
   155  
   156  // DefaultRemoteAttach is the standard implementation of attaching
   157  type DefaultRemoteAttach struct{}
   158  
   159  // Attach executes attach to a running container
   160  func (*DefaultRemoteAttach) Attach(url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error {
   161  	exec, err := createExecutor(url, config)
   162  	if err != nil {
   163  		return err
   164  	}
   165  	return exec.StreamWithContext(context.Background(), remotecommand.StreamOptions{
   166  		Stdin:             stdin,
   167  		Stdout:            stdout,
   168  		Stderr:            stderr,
   169  		Tty:               tty,
   170  		TerminalSizeQueue: terminalSizeQueue,
   171  	})
   172  }
   173  
   174  // createExecutor returns the Executor or an error if one occurred.
   175  func createExecutor(url *url.URL, config *restclient.Config) (remotecommand.Executor, error) {
   176  	exec, err := remotecommand.NewSPDYExecutor(config, "POST", url)
   177  	if err != nil {
   178  		return nil, err
   179  	}
   180  	// Fallback executor is default, unless feature flag is explicitly disabled.
   181  	if !cmdutil.RemoteCommandWebsockets.IsDisabled() {
   182  		// WebSocketExecutor must be "GET" method as described in RFC 6455 Sec. 4.1 (page 17).
   183  		websocketExec, err := remotecommand.NewWebSocketExecutor(config, "GET", url.String())
   184  		if err != nil {
   185  			return nil, err
   186  		}
   187  		exec, err = remotecommand.NewFallbackExecutor(websocketExec, exec, httpstream.IsUpgradeFailure)
   188  		if err != nil {
   189  			return nil, err
   190  		}
   191  	}
   192  	return exec, nil
   193  }
   194  
   195  // Complete verifies command line arguments and loads data from the command environment
   196  func (o *AttachOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
   197  	var err error
   198  	o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace()
   199  	if err != nil {
   200  		return err
   201  	}
   202  
   203  	o.AttachablePodFn = polymorphichelpers.AttachablePodForObjectFn
   204  
   205  	o.GetPodTimeout, err = cmdutil.GetPodRunningTimeoutFlag(cmd)
   206  	if err != nil {
   207  		return cmdutil.UsageErrorf(cmd, err.Error())
   208  	}
   209  
   210  	o.Builder = f.NewBuilder
   211  	o.Resources = args
   212  	o.restClientGetter = f
   213  
   214  	config, err := f.ToRESTConfig()
   215  	if err != nil {
   216  		return err
   217  	}
   218  	o.Config = config
   219  
   220  	if o.CommandName == "" {
   221  		o.CommandName = cmd.CommandPath()
   222  	}
   223  
   224  	return nil
   225  }
   226  
   227  // Validate checks that the provided attach options are specified.
   228  func (o *AttachOptions) Validate() error {
   229  	if len(o.Resources) == 0 {
   230  		return fmt.Errorf("at least 1 argument is required for attach")
   231  	}
   232  	if len(o.Resources) > 2 {
   233  		return fmt.Errorf("expected POD, TYPE/NAME, or TYPE NAME, (at most 2 arguments) saw %d: %v", len(o.Resources), o.Resources)
   234  	}
   235  	if o.GetPodTimeout <= 0 {
   236  		return fmt.Errorf("--pod-running-timeout must be higher than zero")
   237  	}
   238  
   239  	return nil
   240  }
   241  
   242  // Run executes a validated remote execution against a pod.
   243  func (o *AttachOptions) Run() error {
   244  	if o.Pod == nil {
   245  		b := o.Builder().
   246  			WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...).
   247  			NamespaceParam(o.Namespace).DefaultNamespace()
   248  
   249  		switch len(o.Resources) {
   250  		case 1:
   251  			b.ResourceNames("pods", o.Resources[0])
   252  		case 2:
   253  			b.ResourceNames(o.Resources[0], o.Resources[1])
   254  		}
   255  
   256  		obj, err := b.Do().Object()
   257  		if err != nil {
   258  			return err
   259  		}
   260  
   261  		o.Pod, err = o.findAttachablePod(obj)
   262  		if err != nil {
   263  			return err
   264  		}
   265  
   266  		if o.Pod.Status.Phase == corev1.PodSucceeded || o.Pod.Status.Phase == corev1.PodFailed {
   267  			return fmt.Errorf("cannot attach a container in a completed pod; current phase is %s", o.Pod.Status.Phase)
   268  		}
   269  		// TODO: convert this to a clean "wait" behavior
   270  	}
   271  
   272  	// check for TTY
   273  	containerToAttach, err := o.containerToAttachTo(o.Pod)
   274  	if err != nil {
   275  		return fmt.Errorf("cannot attach to the container: %v", err)
   276  	}
   277  	if o.TTY && !containerToAttach.TTY {
   278  		o.TTY = false
   279  		if !o.Quiet && o.ErrOut != nil {
   280  			fmt.Fprintf(o.ErrOut, "error: Unable to use a TTY - container %s did not allocate one\n", containerToAttach.Name)
   281  		}
   282  	} else if !o.TTY && containerToAttach.TTY {
   283  		// the container was launched with a TTY, so we have to force a TTY here, otherwise you'll get
   284  		// an error "Unrecognized input header"
   285  		o.TTY = true
   286  	}
   287  
   288  	// ensure we can recover the terminal while attached
   289  	t := o.SetupTTY()
   290  
   291  	var sizeQueue remotecommand.TerminalSizeQueue
   292  	if t.Raw {
   293  		if size := t.GetSize(); size != nil {
   294  			// fake resizing +1 and then back to normal so that attach-detach-reattach will result in the
   295  			// screen being redrawn
   296  			sizePlusOne := *size
   297  			sizePlusOne.Width++
   298  			sizePlusOne.Height++
   299  
   300  			// this call spawns a goroutine to monitor/update the terminal size
   301  			sizeQueue = t.MonitorSize(&sizePlusOne, size)
   302  		}
   303  
   304  		o.DisableStderr = true
   305  	}
   306  
   307  	if !o.Quiet {
   308  		fmt.Fprintln(o.ErrOut, "If you don't see a command prompt, try pressing enter.")
   309  	}
   310  	if err := t.Safe(o.AttachFunc(o, containerToAttach, t.Raw, sizeQueue)); err != nil {
   311  		return err
   312  	}
   313  
   314  	if msg := o.reattachMessage(containerToAttach.Name, t.Raw); msg != "" {
   315  		fmt.Fprintln(o.Out, msg)
   316  	}
   317  	return nil
   318  }
   319  
   320  func (o *AttachOptions) findAttachablePod(obj runtime.Object) (*corev1.Pod, error) {
   321  	attachablePod, err := o.AttachablePodFn(o.restClientGetter, obj, o.GetPodTimeout)
   322  	if err != nil {
   323  		return nil, err
   324  	}
   325  
   326  	o.StreamOptions.PodName = attachablePod.Name
   327  	return attachablePod, nil
   328  }
   329  
   330  // containerToAttach returns a reference to the container to attach to, given by name.
   331  // use the kubectl.kubernetes.io/default-container annotation for selecting the container to be attached
   332  // or the first container in the pod will be chosen If name is empty.
   333  func (o *AttachOptions) containerToAttachTo(pod *corev1.Pod) (*corev1.Container, error) {
   334  	return podcmd.FindOrDefaultContainerByName(pod, o.ContainerName, o.Quiet, o.ErrOut)
   335  }
   336  
   337  // GetContainerName returns the name of the container to attach to, with a fallback.
   338  func (o *AttachOptions) GetContainerName(pod *corev1.Pod) (string, error) {
   339  	c, err := o.containerToAttachTo(pod)
   340  	if err != nil {
   341  		return "", err
   342  	}
   343  	return c.Name, nil
   344  }
   345  
   346  // reattachMessage returns a message to print after attach has completed, or
   347  // the empty string if no message should be printed.
   348  func (o *AttachOptions) reattachMessage(containerName string, rawTTY bool) string {
   349  	if o.Quiet || !o.Stdin || !rawTTY || o.Pod.Spec.RestartPolicy != corev1.RestartPolicyAlways {
   350  		return ""
   351  	}
   352  	if _, path := podcmd.FindContainerByName(o.Pod, containerName); strings.HasPrefix(path, "spec.ephemeralContainers") {
   353  		return fmt.Sprintf("Session ended, the ephemeral container will not be restarted but may be reattached using '%s %s -c %s -i -t' if it is still running", o.CommandName, o.Pod.Name, containerName)
   354  	}
   355  	return fmt.Sprintf("Session ended, resume using '%s %s -c %s -i -t' command when the pod is running", o.CommandName, o.Pod.Name, containerName)
   356  }
   357  

View as plain text