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