attach.go 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  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. "github.com/golang/glog"
  19. "github.com/renstrom/dedent"
  20. "github.com/spf13/cobra"
  21. "k8s.io/kubernetes/pkg/api"
  22. "k8s.io/kubernetes/pkg/client/restclient"
  23. client "k8s.io/kubernetes/pkg/client/unversioned"
  24. "k8s.io/kubernetes/pkg/client/unversioned/remotecommand"
  25. cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
  26. remotecommandserver "k8s.io/kubernetes/pkg/kubelet/server/remotecommand"
  27. utilerrors "k8s.io/kubernetes/pkg/util/errors"
  28. "k8s.io/kubernetes/pkg/util/term"
  29. )
  30. var (
  31. attach_example = dedent.Dedent(`
  32. # Get output from running pod 123456-7890, using the first container by default
  33. kubectl attach 123456-7890
  34. # Get output from ruby-container from pod 123456-7890
  35. kubectl attach 123456-7890 -c ruby-container
  36. # Switch to raw terminal mode, sends stdin to 'bash' in ruby-container from pod 123456-7890
  37. # and sends stdout/stderr from 'bash' back to the client
  38. kubectl attach 123456-7890 -c ruby-container -i -t`)
  39. )
  40. func NewCmdAttach(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer) *cobra.Command {
  41. options := &AttachOptions{
  42. StreamOptions: StreamOptions{
  43. In: cmdIn,
  44. Out: cmdOut,
  45. Err: cmdErr,
  46. },
  47. Attach: &DefaultRemoteAttach{},
  48. }
  49. cmd := &cobra.Command{
  50. Use: "attach POD -c CONTAINER",
  51. Short: "Attach to a running container",
  52. Long: "Attach to a process that is already running inside an existing container.",
  53. Example: attach_example,
  54. Run: func(cmd *cobra.Command, args []string) {
  55. cmdutil.CheckErr(options.Complete(f, cmd, args))
  56. cmdutil.CheckErr(options.Validate())
  57. cmdutil.CheckErr(options.Run())
  58. },
  59. }
  60. // TODO support UID
  61. cmd.Flags().StringVarP(&options.ContainerName, "container", "c", "", "Container name. If omitted, the first container in the pod will be chosen")
  62. cmd.Flags().BoolVarP(&options.Stdin, "stdin", "i", false, "Pass stdin to the container")
  63. cmd.Flags().BoolVarP(&options.TTY, "tty", "t", false, "Stdin is a TTY")
  64. return cmd
  65. }
  66. // RemoteAttach defines the interface accepted by the Attach command - provided for test stubbing
  67. type RemoteAttach interface {
  68. Attach(method string, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue term.TerminalSizeQueue) error
  69. }
  70. // DefaultRemoteAttach is the standard implementation of attaching
  71. type DefaultRemoteAttach struct{}
  72. func (*DefaultRemoteAttach) Attach(method string, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue term.TerminalSizeQueue) error {
  73. exec, err := remotecommand.NewExecutor(config, method, url)
  74. if err != nil {
  75. return err
  76. }
  77. return exec.Stream(remotecommand.StreamOptions{
  78. SupportedProtocols: remotecommandserver.SupportedStreamingProtocols,
  79. Stdin: stdin,
  80. Stdout: stdout,
  81. Stderr: stderr,
  82. Tty: tty,
  83. TerminalSizeQueue: terminalSizeQueue,
  84. })
  85. }
  86. // AttachOptions declare the arguments accepted by the Exec command
  87. type AttachOptions struct {
  88. StreamOptions
  89. CommandName string
  90. Pod *api.Pod
  91. Attach RemoteAttach
  92. Client *client.Client
  93. Config *restclient.Config
  94. }
  95. // Complete verifies command line arguments and loads data from the command environment
  96. func (p *AttachOptions) Complete(f *cmdutil.Factory, cmd *cobra.Command, argsIn []string) error {
  97. if len(argsIn) == 0 {
  98. return cmdutil.UsageError(cmd, "POD is required for attach")
  99. }
  100. if len(argsIn) > 1 {
  101. return cmdutil.UsageError(cmd, fmt.Sprintf("expected a single argument: POD, saw %d: %s", len(argsIn), argsIn))
  102. }
  103. p.PodName = argsIn[0]
  104. namespace, _, err := f.DefaultNamespace()
  105. if err != nil {
  106. return err
  107. }
  108. p.Namespace = namespace
  109. config, err := f.ClientConfig()
  110. if err != nil {
  111. return err
  112. }
  113. p.Config = config
  114. client, err := f.Client()
  115. if err != nil {
  116. return err
  117. }
  118. p.Client = client
  119. if p.CommandName == "" {
  120. p.CommandName = cmd.CommandPath()
  121. }
  122. return nil
  123. }
  124. // Validate checks that the provided attach options are specified.
  125. func (p *AttachOptions) Validate() error {
  126. allErrs := []error{}
  127. if len(p.PodName) == 0 {
  128. allErrs = append(allErrs, fmt.Errorf("pod name must be specified"))
  129. }
  130. if p.Out == nil || p.Err == nil {
  131. allErrs = append(allErrs, fmt.Errorf("both output and error output must be provided"))
  132. }
  133. if p.Attach == nil || p.Client == nil || p.Config == nil {
  134. allErrs = append(allErrs, fmt.Errorf("client, client config, and attach must be provided"))
  135. }
  136. return utilerrors.NewAggregate(allErrs)
  137. }
  138. // Run executes a validated remote execution against a pod.
  139. func (p *AttachOptions) Run() error {
  140. if p.Pod == nil {
  141. pod, err := p.Client.Pods(p.Namespace).Get(p.PodName)
  142. if err != nil {
  143. return err
  144. }
  145. if pod.Status.Phase == api.PodSucceeded || pod.Status.Phase == api.PodFailed {
  146. return fmt.Errorf("cannot attach a container in a completed pod; current phase is %s", pod.Status.Phase)
  147. }
  148. p.Pod = pod
  149. // TODO: convert this to a clean "wait" behavior
  150. }
  151. pod := p.Pod
  152. // check for TTY
  153. containerToAttach, err := p.containerToAttachTo(pod)
  154. if err != nil {
  155. return fmt.Errorf("cannot attach to the container: %v", err)
  156. }
  157. if p.TTY && !containerToAttach.TTY {
  158. p.TTY = false
  159. if p.Err != nil {
  160. fmt.Fprintf(p.Err, "Unable to use a TTY - container %s did not allocate one\n", containerToAttach.Name)
  161. }
  162. } else if !p.TTY && containerToAttach.TTY {
  163. // the container was launched with a TTY, so we have to force a TTY here, otherwise you'll get
  164. // an error "Unrecognized input header"
  165. p.TTY = true
  166. }
  167. // ensure we can recover the terminal while attached
  168. t := p.setupTTY()
  169. // save p.Err so we can print the command prompt message below
  170. stderr := p.Err
  171. var sizeQueue term.TerminalSizeQueue
  172. if t.Raw {
  173. if size := t.GetSize(); size != nil {
  174. // fake resizing +1 and then back to normal so that attach-detach-reattach will result in the
  175. // screen being redrawn
  176. sizePlusOne := *size
  177. sizePlusOne.Width++
  178. sizePlusOne.Height++
  179. // this call spawns a goroutine to monitor/update the terminal size
  180. sizeQueue = t.MonitorSize(&sizePlusOne, size)
  181. }
  182. // unset p.Err if it was previously set because both stdout and stderr go over p.Out when tty is
  183. // true
  184. p.Err = nil
  185. }
  186. fn := func() error {
  187. if !p.Quiet && stderr != nil {
  188. fmt.Fprintln(stderr, "If you don't see a command prompt, try pressing enter.")
  189. }
  190. // TODO: consider abstracting into a client invocation or client helper
  191. req := p.Client.RESTClient.Post().
  192. Resource("pods").
  193. Name(pod.Name).
  194. Namespace(pod.Namespace).
  195. SubResource("attach")
  196. req.VersionedParams(&api.PodAttachOptions{
  197. Container: containerToAttach.Name,
  198. Stdin: p.Stdin,
  199. Stdout: p.Out != nil,
  200. Stderr: p.Err != nil,
  201. TTY: t.Raw,
  202. }, api.ParameterCodec)
  203. return p.Attach.Attach("POST", req.URL(), p.Config, p.In, p.Out, p.Err, t.Raw, sizeQueue)
  204. }
  205. if err := t.Safe(fn); err != nil {
  206. return err
  207. }
  208. if p.Stdin && t.Raw && pod.Spec.RestartPolicy == api.RestartPolicyAlways {
  209. fmt.Fprintf(p.Out, "Session ended, resume using '%s %s -c %s -i -t' command when the pod is running\n", p.CommandName, pod.Name, containerToAttach.Name)
  210. }
  211. return nil
  212. }
  213. // containerToAttach returns a reference to the container to attach to, given
  214. // by name or the first container if name is empty.
  215. func (p *AttachOptions) containerToAttachTo(pod *api.Pod) (*api.Container, error) {
  216. if len(p.ContainerName) > 0 {
  217. for i := range pod.Spec.Containers {
  218. if pod.Spec.Containers[i].Name == p.ContainerName {
  219. return &pod.Spec.Containers[i], nil
  220. }
  221. }
  222. for i := range pod.Spec.InitContainers {
  223. if pod.Spec.InitContainers[i].Name == p.ContainerName {
  224. return &pod.Spec.InitContainers[i], nil
  225. }
  226. }
  227. return nil, fmt.Errorf("container not found (%s)", p.ContainerName)
  228. }
  229. glog.V(4).Infof("defaulting container name to %s", pod.Spec.Containers[0].Name)
  230. return &pod.Spec.Containers[0], nil
  231. }
  232. // GetContainerName returns the name of the container to attach to, with a fallback.
  233. func (p *AttachOptions) GetContainerName(pod *api.Pod) (string, error) {
  234. c, err := p.containerToAttachTo(pod)
  235. if err != nil {
  236. return "", err
  237. }
  238. return c.Name, nil
  239. }