exec.go 11 KB

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