exec.go 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  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 cmd
  14. import (
  15. "fmt"
  16. "io"
  17. "net/url"
  18. dockerterm "github.com/docker/docker/pkg/term"
  19. "github.com/golang/glog"
  20. "github.com/renstrom/dedent"
  21. "github.com/spf13/cobra"
  22. "k8s.io/kubernetes/pkg/api"
  23. "k8s.io/kubernetes/pkg/client/restclient"
  24. client "k8s.io/kubernetes/pkg/client/unversioned"
  25. "k8s.io/kubernetes/pkg/client/unversioned/remotecommand"
  26. cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
  27. remotecommandserver "k8s.io/kubernetes/pkg/kubelet/server/remotecommand"
  28. "k8s.io/kubernetes/pkg/util/interrupt"
  29. "k8s.io/kubernetes/pkg/util/term"
  30. )
  31. var (
  32. exec_example = dedent.Dedent(`
  33. # Get output from running 'date' from pod 123456-7890, using the first container by default
  34. kubectl exec 123456-7890 date
  35. # Get output from running 'date' in ruby-container from pod 123456-7890
  36. kubectl exec 123456-7890 -c ruby-container date
  37. # Switch to raw terminal mode, sends stdin to 'bash' in ruby-container from pod 123456-7890
  38. # and sends stdout/stderr from 'bash' back to the client
  39. kubectl exec 123456-7890 -c ruby-container -i -t -- bash -il`)
  40. )
  41. func NewCmdExec(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer) *cobra.Command {
  42. options := &ExecOptions{
  43. StreamOptions: StreamOptions{
  44. In: cmdIn,
  45. Out: cmdOut,
  46. Err: cmdErr,
  47. },
  48. Executor: &DefaultRemoteExecutor{},
  49. }
  50. cmd := &cobra.Command{
  51. Use: "exec POD [-c CONTAINER] -- COMMAND [args...]",
  52. Short: "Execute a command in a container",
  53. Long: "Execute a command in a container.",
  54. Example: exec_example,
  55. Run: func(cmd *cobra.Command, args []string) {
  56. argsLenAtDash := cmd.ArgsLenAtDash()
  57. cmdutil.CheckErr(options.Complete(f, cmd, args, argsLenAtDash))
  58. cmdutil.CheckErr(options.Validate())
  59. cmdutil.CheckErr(options.Run())
  60. },
  61. }
  62. cmd.Flags().StringVarP(&options.PodName, "pod", "p", "", "Pod name")
  63. // TODO support UID
  64. cmd.Flags().StringVarP(&options.ContainerName, "container", "c", "", "Container name. If omitted, the first container in the pod will be chosen")
  65. cmd.Flags().BoolVarP(&options.Stdin, "stdin", "i", false, "Pass stdin to the container")
  66. cmd.Flags().BoolVarP(&options.TTY, "tty", "t", false, "Stdin is a TTY")
  67. return cmd
  68. }
  69. // RemoteExecutor defines the interface accepted by the Exec command - provided for test stubbing
  70. type RemoteExecutor interface {
  71. Execute(method string, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue term.TerminalSizeQueue) error
  72. }
  73. // DefaultRemoteExecutor is the standard implementation of remote command execution
  74. type DefaultRemoteExecutor struct{}
  75. func (*DefaultRemoteExecutor) Execute(method string, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue term.TerminalSizeQueue) error {
  76. exec, err := remotecommand.NewExecutor(config, method, url)
  77. if err != nil {
  78. return err
  79. }
  80. return exec.Stream(remotecommand.StreamOptions{
  81. SupportedProtocols: remotecommandserver.SupportedStreamingProtocols,
  82. Stdin: stdin,
  83. Stdout: stdout,
  84. Stderr: stderr,
  85. Tty: tty,
  86. TerminalSizeQueue: terminalSizeQueue,
  87. })
  88. }
  89. type StreamOptions struct {
  90. Namespace string
  91. PodName string
  92. ContainerName string
  93. Stdin bool
  94. TTY bool
  95. // minimize unnecessary output
  96. Quiet bool
  97. // InterruptParent, if set, is used to handle interrupts while attached
  98. InterruptParent *interrupt.Handler
  99. In io.Reader
  100. Out io.Writer
  101. Err io.Writer
  102. // for testing
  103. overrideStreams func() (io.ReadCloser, io.Writer, io.Writer)
  104. isTerminalIn func(t term.TTY) bool
  105. }
  106. // ExecOptions declare the arguments accepted by the Exec command
  107. type ExecOptions struct {
  108. StreamOptions
  109. Command []string
  110. Executor RemoteExecutor
  111. Client *client.Client
  112. Config *restclient.Config
  113. }
  114. // Complete verifies command line arguments and loads data from the command environment
  115. func (p *ExecOptions) Complete(f *cmdutil.Factory, cmd *cobra.Command, argsIn []string, argsLenAtDash int) error {
  116. // Let kubectl exec follow rules for `--`, see #13004 issue
  117. if len(p.PodName) == 0 && (len(argsIn) == 0 || argsLenAtDash == 0) {
  118. return cmdutil.UsageError(cmd, "POD is required for exec")
  119. }
  120. if len(p.PodName) != 0 {
  121. printDeprecationWarning("exec POD", "-p POD")
  122. if len(argsIn) < 1 {
  123. return cmdutil.UsageError(cmd, "COMMAND is required for exec")
  124. }
  125. p.Command = argsIn
  126. } else {
  127. p.PodName = argsIn[0]
  128. p.Command = argsIn[1:]
  129. if len(p.Command) < 1 {
  130. return cmdutil.UsageError(cmd, "COMMAND is required for exec")
  131. }
  132. }
  133. namespace, _, err := f.DefaultNamespace()
  134. if err != nil {
  135. return err
  136. }
  137. p.Namespace = namespace
  138. config, err := f.ClientConfig()
  139. if err != nil {
  140. return err
  141. }
  142. p.Config = config
  143. client, err := f.Client()
  144. if err != nil {
  145. return err
  146. }
  147. p.Client = client
  148. return nil
  149. }
  150. // Validate checks that the provided exec options are specified.
  151. func (p *ExecOptions) Validate() error {
  152. if len(p.PodName) == 0 {
  153. return fmt.Errorf("pod name must be specified")
  154. }
  155. if len(p.Command) == 0 {
  156. return fmt.Errorf("you must specify at least one command for the container")
  157. }
  158. if p.Out == nil || p.Err == nil {
  159. return fmt.Errorf("both output and error output must be provided")
  160. }
  161. if p.Executor == nil || p.Client == nil || p.Config == nil {
  162. return fmt.Errorf("client, client config, and executor must be provided")
  163. }
  164. return nil
  165. }
  166. func (o *StreamOptions) setupTTY() term.TTY {
  167. t := term.TTY{
  168. Parent: o.InterruptParent,
  169. Out: o.Out,
  170. }
  171. if !o.Stdin {
  172. // need to nil out o.In to make sure we don't create a stream for stdin
  173. o.In = nil
  174. o.TTY = false
  175. return t
  176. }
  177. t.In = o.In
  178. if !o.TTY {
  179. return t
  180. }
  181. if o.isTerminalIn == nil {
  182. o.isTerminalIn = func(tty term.TTY) bool {
  183. return tty.IsTerminalIn()
  184. }
  185. }
  186. if !o.isTerminalIn(t) {
  187. o.TTY = false
  188. if o.Err != nil {
  189. fmt.Fprintln(o.Err, "Unable to use a TTY - input is not a terminal or the right kind of file")
  190. }
  191. return t
  192. }
  193. // if we get to here, the user wants to attach stdin, wants a TTY, and o.In is a terminal, so we
  194. // can safely set t.Raw to true
  195. t.Raw = true
  196. if o.overrideStreams == nil {
  197. // use dockerterm.StdStreams() to get the right I/O handles on Windows
  198. o.overrideStreams = dockerterm.StdStreams
  199. }
  200. stdin, stdout, _ := o.overrideStreams()
  201. o.In = stdin
  202. t.In = stdin
  203. if o.Out != nil {
  204. o.Out = stdout
  205. t.Out = stdout
  206. }
  207. return t
  208. }
  209. // Run executes a validated remote execution against a pod.
  210. func (p *ExecOptions) Run() error {
  211. pod, err := p.Client.Pods(p.Namespace).Get(p.PodName)
  212. if err != nil {
  213. return err
  214. }
  215. if pod.Status.Phase == api.PodSucceeded || pod.Status.Phase == api.PodFailed {
  216. return fmt.Errorf("cannot exec into a container in a completed pod; current phase is %s", pod.Status.Phase)
  217. }
  218. containerName := p.ContainerName
  219. if len(containerName) == 0 {
  220. glog.V(4).Infof("defaulting container name to %s", pod.Spec.Containers[0].Name)
  221. containerName = pod.Spec.Containers[0].Name
  222. }
  223. // ensure we can recover the terminal while attached
  224. t := p.setupTTY()
  225. var sizeQueue term.TerminalSizeQueue
  226. if t.Raw {
  227. // this call spawns a goroutine to monitor/update the terminal size
  228. sizeQueue = t.MonitorSize(t.GetSize())
  229. // unset p.Err if it was previously set because both stdout and stderr go over p.Out when tty is
  230. // true
  231. p.Err = nil
  232. }
  233. fn := func() error {
  234. // TODO: consider abstracting into a client invocation or client helper
  235. req := p.Client.RESTClient.Post().
  236. Resource("pods").
  237. Name(pod.Name).
  238. Namespace(pod.Namespace).
  239. SubResource("exec").
  240. Param("container", containerName)
  241. req.VersionedParams(&api.PodExecOptions{
  242. Container: containerName,
  243. Command: p.Command,
  244. Stdin: p.Stdin,
  245. Stdout: p.Out != nil,
  246. Stderr: p.Err != nil,
  247. TTY: t.Raw,
  248. }, api.ParameterCodec)
  249. return p.Executor.Execute("POST", req.URL(), p.Config, p.In, p.Out, p.Err, t.Raw, sizeQueue)
  250. }
  251. if err := t.Safe(fn); err != nil {
  252. return err
  253. }
  254. return nil
  255. }