1
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
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
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
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
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
144 if !cmdutil.RemoteCommandWebsockets.IsDisabled() {
145
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
165 Quiet bool
166
167 InterruptParent *interrupt.Handler
168
169 genericiooptions.IOStreams
170
171
172 overrideStreams func() (io.ReadCloser, io.Writer, io.Writer)
173 isTerminalIn func(t term.TTY) bool
174 }
175
176
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
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
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
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
294
295 t.Raw = true
296
297 if o.overrideStreams == nil {
298
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
313 func (p *ExecOptions) Run() error {
314 var err error
315
316
317
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
363 t := p.SetupTTY()
364
365 var sizeQueue remotecommand.TerminalSizeQueue
366 if t.Raw {
367
368 sizeQueue = t.MonitorSize(t.GetSize())
369
370
371
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
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