1
16
17 package attach
18
19 import (
20 "context"
21 "fmt"
22 "io"
23 "net/url"
24 "strings"
25 "time"
26
27 "github.com/spf13/cobra"
28
29 corev1 "k8s.io/api/core/v1"
30 "k8s.io/apimachinery/pkg/runtime"
31 "k8s.io/apimachinery/pkg/util/httpstream"
32 "k8s.io/cli-runtime/pkg/genericclioptions"
33 "k8s.io/cli-runtime/pkg/genericiooptions"
34 "k8s.io/cli-runtime/pkg/resource"
35 restclient "k8s.io/client-go/rest"
36 "k8s.io/client-go/tools/remotecommand"
37 "k8s.io/kubectl/pkg/cmd/exec"
38 cmdutil "k8s.io/kubectl/pkg/cmd/util"
39 "k8s.io/kubectl/pkg/cmd/util/podcmd"
40 "k8s.io/kubectl/pkg/polymorphichelpers"
41 "k8s.io/kubectl/pkg/scheme"
42 "k8s.io/kubectl/pkg/util/completion"
43 "k8s.io/kubectl/pkg/util/i18n"
44 "k8s.io/kubectl/pkg/util/templates"
45 )
46
47 var (
48 attachExample = templates.Examples(i18n.T(`
49 # Get output from running pod mypod; use the 'kubectl.kubernetes.io/default-container' annotation
50 # for selecting the container to be attached or the first container in the pod will be chosen
51 kubectl attach mypod
52
53 # Get output from ruby-container from pod mypod
54 kubectl attach mypod -c ruby-container
55
56 # Switch to raw terminal mode; sends stdin to 'bash' in ruby-container from pod mypod
57 # and sends stdout/stderr from 'bash' back to the client
58 kubectl attach mypod -c ruby-container -i -t
59
60 # Get output from the first pod of a replica set named nginx
61 kubectl attach rs/nginx
62 `))
63 )
64
65 const (
66 defaultPodAttachTimeout = 60 * time.Second
67 defaultPodLogsTimeout = 20 * time.Second
68 )
69
70
71 type AttachOptions struct {
72 exec.StreamOptions
73
74
75 DisableStderr bool
76
77 CommandName string
78
79 Pod *corev1.Pod
80
81 AttachFunc func(*AttachOptions, *corev1.Container, bool, remotecommand.TerminalSizeQueue) func() error
82 Resources []string
83 Builder func() *resource.Builder
84 AttachablePodFn polymorphichelpers.AttachablePodForObjectFunc
85 restClientGetter genericclioptions.RESTClientGetter
86
87 Attach RemoteAttach
88 GetPodTimeout time.Duration
89 Config *restclient.Config
90 }
91
92
93 func NewAttachOptions(streams genericiooptions.IOStreams) *AttachOptions {
94 return &AttachOptions{
95 StreamOptions: exec.StreamOptions{
96 IOStreams: streams,
97 },
98 Attach: &DefaultRemoteAttach{},
99 AttachFunc: DefaultAttachFunc,
100 }
101 }
102
103
104 func NewCmdAttach(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
105 o := NewAttachOptions(streams)
106 cmd := &cobra.Command{
107 Use: "attach (POD | TYPE/NAME) -c CONTAINER",
108 DisableFlagsInUseLine: true,
109 Short: i18n.T("Attach to a running container"),
110 Long: i18n.T("Attach to a process that is already running inside an existing container."),
111 Example: attachExample,
112 ValidArgsFunction: completion.PodResourceNameCompletionFunc(f),
113 Run: func(cmd *cobra.Command, args []string) {
114 cmdutil.CheckErr(o.Complete(f, cmd, args))
115 cmdutil.CheckErr(o.Validate())
116 cmdutil.CheckErr(o.Run())
117 },
118 }
119 cmdutil.AddPodRunningTimeoutFlag(cmd, defaultPodAttachTimeout)
120 cmdutil.AddContainerVarFlags(cmd, &o.ContainerName, o.ContainerName)
121 cmd.Flags().BoolVarP(&o.Stdin, "stdin", "i", o.Stdin, "Pass stdin to the container")
122 cmd.Flags().BoolVarP(&o.TTY, "tty", "t", o.TTY, "Stdin is a TTY")
123 cmd.Flags().BoolVarP(&o.Quiet, "quiet", "q", o.Quiet, "Only print output from the remote session")
124 return cmd
125 }
126
127
128 type RemoteAttach interface {
129 Attach(url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error
130 }
131
132
133 func DefaultAttachFunc(o *AttachOptions, containerToAttach *corev1.Container, raw bool, sizeQueue remotecommand.TerminalSizeQueue) func() error {
134 return func() error {
135 restClient, err := restclient.RESTClientFor(o.Config)
136 if err != nil {
137 return err
138 }
139 req := restClient.Post().
140 Resource("pods").
141 Name(o.Pod.Name).
142 Namespace(o.Pod.Namespace).
143 SubResource("attach")
144 req.VersionedParams(&corev1.PodAttachOptions{
145 Container: containerToAttach.Name,
146 Stdin: o.Stdin,
147 Stdout: o.Out != nil,
148 Stderr: !o.DisableStderr,
149 TTY: raw,
150 }, scheme.ParameterCodec)
151
152 return o.Attach.Attach(req.URL(), o.Config, o.In, o.Out, o.ErrOut, raw, sizeQueue)
153 }
154 }
155
156
157 type DefaultRemoteAttach struct{}
158
159
160 func (*DefaultRemoteAttach) Attach(url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error {
161 exec, err := createExecutor(url, config)
162 if err != nil {
163 return err
164 }
165 return exec.StreamWithContext(context.Background(), remotecommand.StreamOptions{
166 Stdin: stdin,
167 Stdout: stdout,
168 Stderr: stderr,
169 Tty: tty,
170 TerminalSizeQueue: terminalSizeQueue,
171 })
172 }
173
174
175 func createExecutor(url *url.URL, config *restclient.Config) (remotecommand.Executor, error) {
176 exec, err := remotecommand.NewSPDYExecutor(config, "POST", url)
177 if err != nil {
178 return nil, err
179 }
180
181 if !cmdutil.RemoteCommandWebsockets.IsDisabled() {
182
183 websocketExec, err := remotecommand.NewWebSocketExecutor(config, "GET", url.String())
184 if err != nil {
185 return nil, err
186 }
187 exec, err = remotecommand.NewFallbackExecutor(websocketExec, exec, httpstream.IsUpgradeFailure)
188 if err != nil {
189 return nil, err
190 }
191 }
192 return exec, nil
193 }
194
195
196 func (o *AttachOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
197 var err error
198 o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace()
199 if err != nil {
200 return err
201 }
202
203 o.AttachablePodFn = polymorphichelpers.AttachablePodForObjectFn
204
205 o.GetPodTimeout, err = cmdutil.GetPodRunningTimeoutFlag(cmd)
206 if err != nil {
207 return cmdutil.UsageErrorf(cmd, err.Error())
208 }
209
210 o.Builder = f.NewBuilder
211 o.Resources = args
212 o.restClientGetter = f
213
214 config, err := f.ToRESTConfig()
215 if err != nil {
216 return err
217 }
218 o.Config = config
219
220 if o.CommandName == "" {
221 o.CommandName = cmd.CommandPath()
222 }
223
224 return nil
225 }
226
227
228 func (o *AttachOptions) Validate() error {
229 if len(o.Resources) == 0 {
230 return fmt.Errorf("at least 1 argument is required for attach")
231 }
232 if len(o.Resources) > 2 {
233 return fmt.Errorf("expected POD, TYPE/NAME, or TYPE NAME, (at most 2 arguments) saw %d: %v", len(o.Resources), o.Resources)
234 }
235 if o.GetPodTimeout <= 0 {
236 return fmt.Errorf("--pod-running-timeout must be higher than zero")
237 }
238
239 return nil
240 }
241
242
243 func (o *AttachOptions) Run() error {
244 if o.Pod == nil {
245 b := o.Builder().
246 WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...).
247 NamespaceParam(o.Namespace).DefaultNamespace()
248
249 switch len(o.Resources) {
250 case 1:
251 b.ResourceNames("pods", o.Resources[0])
252 case 2:
253 b.ResourceNames(o.Resources[0], o.Resources[1])
254 }
255
256 obj, err := b.Do().Object()
257 if err != nil {
258 return err
259 }
260
261 o.Pod, err = o.findAttachablePod(obj)
262 if err != nil {
263 return err
264 }
265
266 if o.Pod.Status.Phase == corev1.PodSucceeded || o.Pod.Status.Phase == corev1.PodFailed {
267 return fmt.Errorf("cannot attach a container in a completed pod; current phase is %s", o.Pod.Status.Phase)
268 }
269
270 }
271
272
273 containerToAttach, err := o.containerToAttachTo(o.Pod)
274 if err != nil {
275 return fmt.Errorf("cannot attach to the container: %v", err)
276 }
277 if o.TTY && !containerToAttach.TTY {
278 o.TTY = false
279 if !o.Quiet && o.ErrOut != nil {
280 fmt.Fprintf(o.ErrOut, "error: Unable to use a TTY - container %s did not allocate one\n", containerToAttach.Name)
281 }
282 } else if !o.TTY && containerToAttach.TTY {
283
284
285 o.TTY = true
286 }
287
288
289 t := o.SetupTTY()
290
291 var sizeQueue remotecommand.TerminalSizeQueue
292 if t.Raw {
293 if size := t.GetSize(); size != nil {
294
295
296 sizePlusOne := *size
297 sizePlusOne.Width++
298 sizePlusOne.Height++
299
300
301 sizeQueue = t.MonitorSize(&sizePlusOne, size)
302 }
303
304 o.DisableStderr = true
305 }
306
307 if !o.Quiet {
308 fmt.Fprintln(o.ErrOut, "If you don't see a command prompt, try pressing enter.")
309 }
310 if err := t.Safe(o.AttachFunc(o, containerToAttach, t.Raw, sizeQueue)); err != nil {
311 return err
312 }
313
314 if msg := o.reattachMessage(containerToAttach.Name, t.Raw); msg != "" {
315 fmt.Fprintln(o.Out, msg)
316 }
317 return nil
318 }
319
320 func (o *AttachOptions) findAttachablePod(obj runtime.Object) (*corev1.Pod, error) {
321 attachablePod, err := o.AttachablePodFn(o.restClientGetter, obj, o.GetPodTimeout)
322 if err != nil {
323 return nil, err
324 }
325
326 o.StreamOptions.PodName = attachablePod.Name
327 return attachablePod, nil
328 }
329
330
331
332
333 func (o *AttachOptions) containerToAttachTo(pod *corev1.Pod) (*corev1.Container, error) {
334 return podcmd.FindOrDefaultContainerByName(pod, o.ContainerName, o.Quiet, o.ErrOut)
335 }
336
337
338 func (o *AttachOptions) GetContainerName(pod *corev1.Pod) (string, error) {
339 c, err := o.containerToAttachTo(pod)
340 if err != nil {
341 return "", err
342 }
343 return c.Name, nil
344 }
345
346
347
348 func (o *AttachOptions) reattachMessage(containerName string, rawTTY bool) string {
349 if o.Quiet || !o.Stdin || !rawTTY || o.Pod.Spec.RestartPolicy != corev1.RestartPolicyAlways {
350 return ""
351 }
352 if _, path := podcmd.FindContainerByName(o.Pod, containerName); strings.HasPrefix(path, "spec.ephemeralContainers") {
353 return fmt.Sprintf("Session ended, the ephemeral container will not be restarted but may be reattached using '%s %s -c %s -i -t' if it is still running", o.CommandName, o.Pod.Name, containerName)
354 }
355 return fmt.Sprintf("Session ended, resume using '%s %s -c %s -i -t' command when the pod is running", o.CommandName, o.Pod.Name, containerName)
356 }
357
View as plain text