attach.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  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 attach
  14. import (
  15. "fmt"
  16. "io"
  17. "net/url"
  18. "time"
  19. "github.com/spf13/cobra"
  20. "k8s.io/klog"
  21. corev1 "k8s.io/api/core/v1"
  22. "k8s.io/apimachinery/pkg/runtime"
  23. "k8s.io/cli-runtime/pkg/genericclioptions"
  24. "k8s.io/cli-runtime/pkg/resource"
  25. restclient "k8s.io/client-go/rest"
  26. "k8s.io/client-go/tools/remotecommand"
  27. "k8s.io/kubernetes/pkg/kubectl/cmd/exec"
  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/templates"
  33. )
  34. var (
  35. attachExample = templates.Examples(i18n.T(`
  36. # Get output from running pod 123456-7890, using the first container by default
  37. kubectl attach 123456-7890
  38. # Get output from ruby-container from pod 123456-7890
  39. kubectl attach 123456-7890 -c ruby-container
  40. # Switch to raw terminal mode, sends stdin to 'bash' in ruby-container from pod 123456-7890
  41. # and sends stdout/stderr from 'bash' back to the client
  42. kubectl attach 123456-7890 -c ruby-container -i -t
  43. # Get output from the first pod of a ReplicaSet named nginx
  44. kubectl attach rs/nginx
  45. `))
  46. )
  47. const (
  48. defaultPodAttachTimeout = 60 * time.Second
  49. defaultPodLogsTimeout = 20 * time.Second
  50. )
  51. // AttachOptions declare the arguments accepted by the Attach command
  52. type AttachOptions struct {
  53. exec.StreamOptions
  54. // whether to disable use of standard error when streaming output from tty
  55. DisableStderr bool
  56. CommandName string
  57. ParentCommandName string
  58. EnableSuggestedCmdUsage bool
  59. Pod *corev1.Pod
  60. AttachFunc func(*AttachOptions, *corev1.Container, bool, remotecommand.TerminalSizeQueue) func() error
  61. Resources []string
  62. Builder func() *resource.Builder
  63. AttachablePodFn polymorphichelpers.AttachablePodForObjectFunc
  64. restClientGetter genericclioptions.RESTClientGetter
  65. Attach RemoteAttach
  66. GetPodTimeout time.Duration
  67. Config *restclient.Config
  68. }
  69. // NewAttachOptions creates the options for attach
  70. func NewAttachOptions(streams genericclioptions.IOStreams) *AttachOptions {
  71. return &AttachOptions{
  72. StreamOptions: exec.StreamOptions{
  73. IOStreams: streams,
  74. },
  75. Attach: &DefaultRemoteAttach{},
  76. AttachFunc: DefaultAttachFunc,
  77. }
  78. }
  79. // NewCmdAttach returns the attach Cobra command
  80. func NewCmdAttach(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command {
  81. o := NewAttachOptions(streams)
  82. cmd := &cobra.Command{
  83. Use: "attach (POD | TYPE/NAME) -c CONTAINER",
  84. DisableFlagsInUseLine: true,
  85. Short: i18n.T("Attach to a running container"),
  86. Long: "Attach to a process that is already running inside an existing container.",
  87. Example: attachExample,
  88. Run: func(cmd *cobra.Command, args []string) {
  89. cmdutil.CheckErr(o.Complete(f, cmd, args))
  90. cmdutil.CheckErr(o.Validate())
  91. cmdutil.CheckErr(o.Run())
  92. },
  93. }
  94. cmdutil.AddPodRunningTimeoutFlag(cmd, defaultPodAttachTimeout)
  95. cmd.Flags().StringVarP(&o.ContainerName, "container", "c", o.ContainerName, "Container name. If omitted, the first container in the pod will be chosen")
  96. cmd.Flags().BoolVarP(&o.Stdin, "stdin", "i", o.Stdin, "Pass stdin to the container")
  97. cmd.Flags().BoolVarP(&o.TTY, "tty", "t", o.TTY, "Stdin is a TTY")
  98. return cmd
  99. }
  100. // RemoteAttach defines the interface accepted by the Attach command - provided for test stubbing
  101. type RemoteAttach interface {
  102. Attach(method string, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error
  103. }
  104. // DefaultAttachFunc is the default AttachFunc used
  105. func DefaultAttachFunc(o *AttachOptions, containerToAttach *corev1.Container, raw bool, sizeQueue remotecommand.TerminalSizeQueue) func() error {
  106. return func() error {
  107. restClient, err := restclient.RESTClientFor(o.Config)
  108. if err != nil {
  109. return err
  110. }
  111. req := restClient.Post().
  112. Resource("pods").
  113. Name(o.Pod.Name).
  114. Namespace(o.Pod.Namespace).
  115. SubResource("attach")
  116. req.VersionedParams(&corev1.PodAttachOptions{
  117. Container: containerToAttach.Name,
  118. Stdin: o.Stdin,
  119. Stdout: o.Out != nil,
  120. Stderr: !o.DisableStderr,
  121. TTY: raw,
  122. }, scheme.ParameterCodec)
  123. return o.Attach.Attach("POST", req.URL(), o.Config, o.In, o.Out, o.ErrOut, raw, sizeQueue)
  124. }
  125. }
  126. // DefaultRemoteAttach is the standard implementation of attaching
  127. type DefaultRemoteAttach struct{}
  128. // Attach executes attach to a running container
  129. func (*DefaultRemoteAttach) Attach(method string, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error {
  130. exec, err := remotecommand.NewSPDYExecutor(config, method, url)
  131. if err != nil {
  132. return err
  133. }
  134. return exec.Stream(remotecommand.StreamOptions{
  135. Stdin: stdin,
  136. Stdout: stdout,
  137. Stderr: stderr,
  138. Tty: tty,
  139. TerminalSizeQueue: terminalSizeQueue,
  140. })
  141. }
  142. // Complete verifies command line arguments and loads data from the command environment
  143. func (o *AttachOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
  144. var err error
  145. o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace()
  146. if err != nil {
  147. return err
  148. }
  149. o.AttachablePodFn = polymorphichelpers.AttachablePodForObjectFn
  150. o.GetPodTimeout, err = cmdutil.GetPodRunningTimeoutFlag(cmd)
  151. if err != nil {
  152. return cmdutil.UsageErrorf(cmd, err.Error())
  153. }
  154. o.Builder = f.NewBuilder
  155. o.Resources = args
  156. o.restClientGetter = f
  157. cmdParent := cmd.Parent()
  158. if cmdParent != nil {
  159. o.ParentCommandName = cmdParent.CommandPath()
  160. }
  161. if len(o.ParentCommandName) > 0 && cmdutil.IsSiblingCommandExists(cmd, "describe") {
  162. o.EnableSuggestedCmdUsage = true
  163. }
  164. config, err := f.ToRESTConfig()
  165. if err != nil {
  166. return err
  167. }
  168. o.Config = config
  169. if o.CommandName == "" {
  170. o.CommandName = cmd.CommandPath()
  171. }
  172. return nil
  173. }
  174. // Validate checks that the provided attach options are specified.
  175. func (o *AttachOptions) Validate() error {
  176. if len(o.Resources) == 0 {
  177. return fmt.Errorf("at least 1 argument is required for attach")
  178. }
  179. if len(o.Resources) > 2 {
  180. return fmt.Errorf("expected POD, TYPE/NAME, or TYPE NAME, (at most 2 arguments) saw %d: %v", len(o.Resources), o.Resources)
  181. }
  182. if o.GetPodTimeout <= 0 {
  183. return fmt.Errorf("--pod-running-timeout must be higher than zero")
  184. }
  185. return nil
  186. }
  187. // Run executes a validated remote execution against a pod.
  188. func (o *AttachOptions) Run() error {
  189. if o.Pod == nil {
  190. b := o.Builder().
  191. WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...).
  192. NamespaceParam(o.Namespace).DefaultNamespace()
  193. switch len(o.Resources) {
  194. case 1:
  195. b.ResourceNames("pods", o.Resources[0])
  196. case 2:
  197. b.ResourceNames(o.Resources[0], o.Resources[1])
  198. }
  199. obj, err := b.Do().Object()
  200. if err != nil {
  201. return err
  202. }
  203. o.Pod, err = o.findAttachablePod(obj)
  204. if err != nil {
  205. return err
  206. }
  207. if o.Pod.Status.Phase == corev1.PodSucceeded || o.Pod.Status.Phase == corev1.PodFailed {
  208. return fmt.Errorf("cannot attach a container in a completed pod; current phase is %s", o.Pod.Status.Phase)
  209. }
  210. // TODO: convert this to a clean "wait" behavior
  211. }
  212. // check for TTY
  213. containerToAttach, err := o.containerToAttachTo(o.Pod)
  214. if err != nil {
  215. return fmt.Errorf("cannot attach to the container: %v", err)
  216. }
  217. if o.TTY && !containerToAttach.TTY {
  218. o.TTY = false
  219. if o.ErrOut != nil {
  220. fmt.Fprintf(o.ErrOut, "Unable to use a TTY - container %s did not allocate one\n", containerToAttach.Name)
  221. }
  222. } else if !o.TTY && containerToAttach.TTY {
  223. // the container was launched with a TTY, so we have to force a TTY here, otherwise you'll get
  224. // an error "Unrecognized input header"
  225. o.TTY = true
  226. }
  227. // ensure we can recover the terminal while attached
  228. t := o.SetupTTY()
  229. var sizeQueue remotecommand.TerminalSizeQueue
  230. if t.Raw {
  231. if size := t.GetSize(); size != nil {
  232. // fake resizing +1 and then back to normal so that attach-detach-reattach will result in the
  233. // screen being redrawn
  234. sizePlusOne := *size
  235. sizePlusOne.Width++
  236. sizePlusOne.Height++
  237. // this call spawns a goroutine to monitor/update the terminal size
  238. sizeQueue = t.MonitorSize(&sizePlusOne, size)
  239. }
  240. o.DisableStderr = true
  241. }
  242. if !o.Quiet {
  243. fmt.Fprintln(o.ErrOut, "If you don't see a command prompt, try pressing enter.")
  244. }
  245. if err := t.Safe(o.AttachFunc(o, containerToAttach, t.Raw, sizeQueue)); err != nil {
  246. return err
  247. }
  248. if o.Stdin && t.Raw && o.Pod.Spec.RestartPolicy == corev1.RestartPolicyAlways {
  249. fmt.Fprintf(o.Out, "Session ended, resume using '%s %s -c %s -i -t' command when the pod is running\n", o.CommandName, o.Pod.Name, containerToAttach.Name)
  250. }
  251. return nil
  252. }
  253. func (o *AttachOptions) findAttachablePod(obj runtime.Object) (*corev1.Pod, error) {
  254. attachablePod, err := o.AttachablePodFn(o.restClientGetter, obj, o.GetPodTimeout)
  255. if err != nil {
  256. return nil, err
  257. }
  258. o.StreamOptions.PodName = attachablePod.Name
  259. return attachablePod, nil
  260. }
  261. // containerToAttach returns a reference to the container to attach to, given
  262. // by name or the first container if name is empty.
  263. func (o *AttachOptions) containerToAttachTo(pod *corev1.Pod) (*corev1.Container, error) {
  264. if len(o.ContainerName) > 0 {
  265. for i := range pod.Spec.Containers {
  266. if pod.Spec.Containers[i].Name == o.ContainerName {
  267. return &pod.Spec.Containers[i], nil
  268. }
  269. }
  270. for i := range pod.Spec.InitContainers {
  271. if pod.Spec.InitContainers[i].Name == o.ContainerName {
  272. return &pod.Spec.InitContainers[i], nil
  273. }
  274. }
  275. return nil, fmt.Errorf("container not found (%s)", o.ContainerName)
  276. }
  277. if o.EnableSuggestedCmdUsage {
  278. fmt.Fprintf(o.ErrOut, "Defaulting container name to %s.\n", pod.Spec.Containers[0].Name)
  279. fmt.Fprintf(o.ErrOut, "Use '%s describe pod/%s -n %s' to see all of the containers in this pod.\n", o.ParentCommandName, o.PodName, o.Namespace)
  280. }
  281. klog.V(4).Infof("defaulting container name to %s", pod.Spec.Containers[0].Name)
  282. return &pod.Spec.Containers[0], nil
  283. }
  284. // GetContainerName returns the name of the container to attach to, with a fallback.
  285. func (o *AttachOptions) GetContainerName(pod *corev1.Pod) (string, error) {
  286. c, err := o.containerToAttachTo(pod)
  287. if err != nil {
  288. return "", err
  289. }
  290. return c.Name, nil
  291. }