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