1
16
17 package attach
18
19 import (
20 "bytes"
21 "fmt"
22 "io"
23 "net/http"
24 "net/url"
25 "strings"
26 "testing"
27 "time"
28
29 corev1 "k8s.io/api/core/v1"
30 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
31 "k8s.io/apimachinery/pkg/runtime"
32 "k8s.io/apimachinery/pkg/runtime/schema"
33 "k8s.io/cli-runtime/pkg/genericclioptions"
34 "k8s.io/cli-runtime/pkg/genericiooptions"
35 restclient "k8s.io/client-go/rest"
36 "k8s.io/client-go/rest/fake"
37 "k8s.io/client-go/tools/remotecommand"
38 "k8s.io/kubectl/pkg/cmd/exec"
39 cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
40 cmdutil "k8s.io/kubectl/pkg/cmd/util"
41 "k8s.io/kubectl/pkg/cmd/util/podcmd"
42 "k8s.io/kubectl/pkg/polymorphichelpers"
43 "k8s.io/kubectl/pkg/scheme"
44 )
45
46 type fakeRemoteAttach struct {
47 url *url.URL
48 err error
49 }
50
51 func (f *fakeRemoteAttach) Attach(url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error {
52 f.url = url
53 return f.err
54 }
55
56 func fakeAttachablePodFn(pod *corev1.Pod) polymorphichelpers.AttachablePodForObjectFunc {
57 return func(getter genericclioptions.RESTClientGetter, obj runtime.Object, timeout time.Duration) (*corev1.Pod, error) {
58 return pod, nil
59 }
60 }
61
62 func TestPodAndContainerAttach(t *testing.T) {
63 tests := []struct {
64 name string
65 args []string
66 options *AttachOptions
67 expectError string
68 expectedPodName string
69 expectedContainerName string
70 expectOut string
71 obj *corev1.Pod
72 }{
73 {
74 name: "empty",
75 options: &AttachOptions{GetPodTimeout: 1},
76 expectError: "at least 1 argument is required",
77 },
78 {
79 name: "too many args",
80 options: &AttachOptions{GetPodTimeout: 2},
81 args: []string{"one", "two", "three"},
82 expectError: "at most 2 arguments",
83 },
84 {
85 name: "no container, no flags",
86 options: &AttachOptions{GetPodTimeout: defaultPodLogsTimeout},
87 args: []string{"foo"},
88 expectedPodName: "foo",
89 expectedContainerName: "bar",
90 obj: attachPod(),
91 expectOut: `Defaulted container "bar" out of: bar, debugger (ephem), initfoo (init)`,
92 },
93 {
94 name: "no container, no flags, sets default expected container as annotation",
95 options: &AttachOptions{GetPodTimeout: defaultPodLogsTimeout},
96 args: []string{"foo"},
97 expectedPodName: "foo",
98 expectedContainerName: "bar",
99 obj: setDefaultContainer(attachPod(), "initfoo"),
100 expectOut: ``,
101 },
102 {
103 name: "no container, no flags, sets default missing container as annotation",
104 options: &AttachOptions{GetPodTimeout: defaultPodLogsTimeout},
105 args: []string{"foo"},
106 expectedPodName: "foo",
107 expectedContainerName: "bar",
108 obj: setDefaultContainer(attachPod(), "does-not-exist"),
109 expectOut: `Defaulted container "bar" out of: bar, debugger (ephem), initfoo (init)`,
110 },
111 {
112 name: "container in flag",
113 options: &AttachOptions{StreamOptions: exec.StreamOptions{ContainerName: "bar"}, GetPodTimeout: 10000000},
114 args: []string{"foo"},
115 expectedPodName: "foo",
116 expectedContainerName: "bar",
117 obj: attachPod(),
118 },
119 {
120 name: "init container in flag",
121 options: &AttachOptions{StreamOptions: exec.StreamOptions{ContainerName: "initfoo"}, GetPodTimeout: 30},
122 args: []string{"foo"},
123 expectedPodName: "foo",
124 expectedContainerName: "initfoo",
125 obj: attachPod(),
126 },
127 {
128 name: "ephemeral container in flag",
129 options: &AttachOptions{StreamOptions: exec.StreamOptions{ContainerName: "debugger"}, GetPodTimeout: 30},
130 args: []string{"foo"},
131 expectedPodName: "foo",
132 expectedContainerName: "debugger",
133 obj: attachPod(),
134 },
135 {
136 name: "non-existing container",
137 options: &AttachOptions{StreamOptions: exec.StreamOptions{ContainerName: "wrong"}, GetPodTimeout: 10},
138 args: []string{"foo"},
139 expectedPodName: "foo",
140 expectError: "container wrong not found in pod foo",
141 obj: attachPod(),
142 },
143 {
144 name: "no container, no flags, pods and name",
145 options: &AttachOptions{GetPodTimeout: 10000},
146 args: []string{"pods", "foo"},
147 expectedPodName: "foo",
148 expectedContainerName: "bar",
149 obj: attachPod(),
150 },
151 {
152 name: "invalid get pod timeout value",
153 options: &AttachOptions{GetPodTimeout: 0},
154 args: []string{"pod/foo"},
155 expectedPodName: "foo",
156 expectedContainerName: "bar",
157 obj: attachPod(),
158 expectError: "must be higher than zero",
159 },
160 }
161
162 for _, test := range tests {
163 t.Run(test.name, func(t *testing.T) {
164
165 test.options.AttachablePodFn = fakeAttachablePodFn(test.obj)
166 test.options.Resources = test.args
167
168 if err := test.options.Validate(); err != nil {
169 if test.expectError == "" || !strings.Contains(err.Error(), test.expectError) {
170 t.Errorf("unexpected error: expected %q, got %q", test.expectError, err)
171 }
172 return
173 }
174
175 pod, err := test.options.findAttachablePod(&corev1.Pod{
176 ObjectMeta: metav1.ObjectMeta{Name: "test-pod", Namespace: "test"},
177 Spec: corev1.PodSpec{
178 InitContainers: []corev1.Container{
179 {
180 Name: "initfoo",
181 },
182 },
183 Containers: []corev1.Container{
184 {
185 Name: "foobar",
186 },
187 },
188 EphemeralContainers: []corev1.EphemeralContainer{
189 {
190 EphemeralContainerCommon: corev1.EphemeralContainerCommon{
191 Name: "ephemfoo",
192 },
193 },
194 },
195 },
196 })
197 if err != nil {
198 if test.expectError == "" || !strings.Contains(err.Error(), test.expectError) {
199 t.Errorf("unexpected error: expected %q, got %q", err, test.expectError)
200 }
201 return
202 }
203
204 if pod.Name != test.expectedPodName {
205 t.Errorf("unexpected pod name: expected %q, got %q", test.expectedContainerName, pod.Name)
206 }
207
208 var buf bytes.Buffer
209 test.options.ErrOut = &buf
210 container, err := test.options.containerToAttachTo(attachPod())
211
212 if len(test.expectOut) > 0 && !strings.Contains(buf.String(), test.expectOut) {
213 t.Errorf("unexpected output: output did not contain %q\n---\n%s", test.expectOut, buf.String())
214 }
215
216 if err != nil {
217 if test.expectError == "" || !strings.Contains(err.Error(), test.expectError) {
218 t.Errorf("unexpected error: expected %q, got %q", test.expectError, err)
219 }
220 return
221 }
222
223 if container.Name != test.expectedContainerName {
224 t.Errorf("unexpected container name: expected %q, got %q", test.expectedContainerName, container.Name)
225 }
226
227 if test.options.PodName != test.expectedPodName {
228 t.Errorf("%s: expected: %s, got: %s", test.name, test.expectedPodName, test.options.PodName)
229 }
230
231 if len(test.expectError) > 0 {
232 t.Fatalf("expected error %q, but saw none", test.expectError)
233 }
234 })
235 }
236 }
237
238 func TestAttach(t *testing.T) {
239 version := "v1"
240 tests := []struct {
241 name, version, podPath, fetchPodPath, attachPath, container string
242 pod *corev1.Pod
243 remoteAttachErr bool
244 expectedErr string
245 }{
246 {
247 name: "pod attach",
248 version: version,
249 podPath: "/api/" + version + "/namespaces/test/pods/foo",
250 fetchPodPath: "/namespaces/test/pods/foo",
251 attachPath: "/api/" + version + "/namespaces/test/pods/foo/attach",
252 pod: attachPod(),
253 container: "bar",
254 },
255 {
256 name: "pod attach error",
257 version: version,
258 podPath: "/api/" + version + "/namespaces/test/pods/foo",
259 fetchPodPath: "/namespaces/test/pods/foo",
260 attachPath: "/api/" + version + "/namespaces/test/pods/foo/attach",
261 pod: attachPod(),
262 remoteAttachErr: true,
263 container: "bar",
264 expectedErr: "attach error",
265 },
266 {
267 name: "container not found error",
268 version: version,
269 podPath: "/api/" + version + "/namespaces/test/pods/foo",
270 fetchPodPath: "/namespaces/test/pods/foo",
271 attachPath: "/api/" + version + "/namespaces/test/pods/foo/attach",
272 pod: attachPod(),
273 container: "foo",
274 expectedErr: "cannot attach to the container: container foo not found in pod foo",
275 },
276 }
277 for _, test := range tests {
278 t.Run(test.name, func(t *testing.T) {
279 tf := cmdtesting.NewTestFactory().WithNamespace("test")
280 defer tf.Cleanup()
281
282 codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
283 ns := scheme.Codecs.WithoutConversion()
284
285 tf.Client = &fake.RESTClient{
286 GroupVersion: schema.GroupVersion{Group: "", Version: "v1"},
287 NegotiatedSerializer: ns,
288 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
289 switch p, m := req.URL.Path, req.Method; {
290 case p == test.podPath && m == "GET":
291 body := cmdtesting.ObjBody(codec, test.pod)
292 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil
293 case p == test.fetchPodPath && m == "GET":
294 body := cmdtesting.ObjBody(codec, test.pod)
295 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil
296 default:
297 t.Errorf("%s: unexpected request: %s %#v\n%#v", p, req.Method, req.URL, req)
298 return nil, fmt.Errorf("unexpected request")
299 }
300 }),
301 }
302 tf.ClientConfigVal = &restclient.Config{APIPath: "/api", ContentConfig: restclient.ContentConfig{NegotiatedSerializer: scheme.Codecs, GroupVersion: &schema.GroupVersion{Version: test.version}}}
303
304 remoteAttach := &fakeRemoteAttach{}
305 if test.remoteAttachErr {
306 remoteAttach.err = fmt.Errorf("attach error")
307 }
308 options := &AttachOptions{
309 StreamOptions: exec.StreamOptions{
310 ContainerName: test.container,
311 IOStreams: genericiooptions.NewTestIOStreamsDiscard(),
312 },
313 Attach: remoteAttach,
314 GetPodTimeout: 1000,
315 }
316
317 options.restClientGetter = tf
318 options.Namespace = "test"
319 options.Resources = []string{"foo"}
320 options.Builder = tf.NewBuilder
321 options.AttachablePodFn = fakeAttachablePodFn(test.pod)
322 options.AttachFunc = func(opts *AttachOptions, containerToAttach *corev1.Container, raw bool, sizeQueue remotecommand.TerminalSizeQueue) func() error {
323 return func() error {
324 u, err := url.Parse(fmt.Sprintf("%s?container=%s", test.attachPath, containerToAttach.Name))
325 if err != nil {
326 return err
327 }
328
329 return options.Attach.Attach(u, nil, nil, nil, nil, raw, sizeQueue)
330 }
331 }
332
333 err := options.Run()
334 if test.expectedErr != "" && err.Error() != test.expectedErr {
335 t.Errorf("%s: Unexpected exec error: %v", test.name, err)
336 return
337 }
338 if test.expectedErr == "" && err != nil {
339 t.Errorf("%s: Unexpected error: %v", test.name, err)
340 return
341 }
342 if test.expectedErr != "" {
343 return
344 }
345 if remoteAttach.url.Path != test.attachPath {
346 t.Errorf("%s: Did not get expected path for exec request: %q %q", test.name, test.attachPath, remoteAttach.url.Path)
347 return
348 }
349 if remoteAttach.url.Query().Get("container") != "bar" {
350 t.Errorf("%s: Did not have query parameters: %s", test.name, remoteAttach.url.Query())
351 }
352 })
353 }
354 }
355
356 func TestAttachWarnings(t *testing.T) {
357 version := "v1"
358 tests := []struct {
359 name, container, version, podPath, fetchPodPath, expectedErr string
360 pod *corev1.Pod
361 stdin, tty bool
362 }{
363 {
364 name: "fallback tty if not supported",
365 version: version,
366 podPath: "/api/" + version + "/namespaces/test/pods/foo",
367 fetchPodPath: "/namespaces/test/pods/foo",
368 pod: attachPod(),
369 stdin: true,
370 tty: true,
371 expectedErr: "Unable to use a TTY - container bar did not allocate one",
372 },
373 }
374 for _, test := range tests {
375 t.Run(test.name, func(t *testing.T) {
376 tf := cmdtesting.NewTestFactory().WithNamespace("test")
377 defer tf.Cleanup()
378
379 streams, _, _, bufErr := genericiooptions.NewTestIOStreams()
380
381 codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
382 ns := scheme.Codecs.WithoutConversion()
383
384 tf.Client = &fake.RESTClient{
385 GroupVersion: schema.GroupVersion{Group: "", Version: "v1"},
386 NegotiatedSerializer: ns,
387 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
388 switch p, m := req.URL.Path, req.Method; {
389 case p == test.podPath && m == "GET":
390 body := cmdtesting.ObjBody(codec, test.pod)
391 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil
392 case p == test.fetchPodPath && m == "GET":
393 body := cmdtesting.ObjBody(codec, test.pod)
394 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: body}, nil
395 default:
396 t.Errorf("%s: unexpected request: %s %#v\n%#v", p, req.Method, req.URL, req)
397 return nil, fmt.Errorf("unexpected request")
398 }
399 }),
400 }
401 tf.ClientConfigVal = &restclient.Config{APIPath: "/api", ContentConfig: restclient.ContentConfig{NegotiatedSerializer: scheme.Codecs, GroupVersion: &schema.GroupVersion{Version: test.version}}}
402
403 options := &AttachOptions{
404 StreamOptions: exec.StreamOptions{
405 Stdin: test.stdin,
406 TTY: test.tty,
407 ContainerName: test.container,
408 IOStreams: streams,
409 },
410
411 Attach: &fakeRemoteAttach{},
412 GetPodTimeout: 1000,
413 }
414
415 options.restClientGetter = tf
416 options.Namespace = "test"
417 options.Resources = []string{"foo"}
418 options.Builder = tf.NewBuilder
419 options.AttachablePodFn = fakeAttachablePodFn(test.pod)
420 options.AttachFunc = func(opts *AttachOptions, containerToAttach *corev1.Container, raw bool, sizeQueue remotecommand.TerminalSizeQueue) func() error {
421 return func() error {
422 u, err := url.Parse("http://foo.bar")
423 if err != nil {
424 return err
425 }
426
427 return options.Attach.Attach(u, nil, nil, nil, nil, raw, sizeQueue)
428 }
429 }
430
431 if err := options.Run(); err != nil {
432 t.Fatal(err)
433 }
434
435 if test.stdin && test.tty {
436 if !test.pod.Spec.Containers[0].TTY {
437 if !strings.Contains(bufErr.String(), test.expectedErr) {
438 t.Errorf("%s: Expected TTY fallback warning for attach request: %s", test.name, bufErr.String())
439 return
440 }
441 }
442 }
443 })
444 }
445 }
446
447 func attachPod() *corev1.Pod {
448 return &corev1.Pod{
449 ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "test", ResourceVersion: "10"},
450 Spec: corev1.PodSpec{
451 RestartPolicy: corev1.RestartPolicyAlways,
452 DNSPolicy: corev1.DNSClusterFirst,
453 Containers: []corev1.Container{
454 {
455 Name: "bar",
456 },
457 },
458 InitContainers: []corev1.Container{
459 {
460 Name: "initfoo",
461 },
462 },
463 EphemeralContainers: []corev1.EphemeralContainer{
464 {
465 EphemeralContainerCommon: corev1.EphemeralContainerCommon{
466 Name: "debugger",
467 },
468 },
469 },
470 },
471 Status: corev1.PodStatus{
472 Phase: corev1.PodRunning,
473 },
474 }
475 }
476
477 func setDefaultContainer(pod *corev1.Pod, name string) *corev1.Pod {
478 if pod.Annotations == nil {
479 pod.Annotations = make(map[string]string)
480 }
481 pod.Annotations[podcmd.DefaultContainerAnnotationName] = name
482 return pod
483 }
484
485 func TestReattachMessage(t *testing.T) {
486 tests := []struct {
487 name string
488 pod *corev1.Pod
489 rawTTY, stdin bool
490 container string
491 expected string
492 }{
493 {
494 name: "normal interactive session",
495 pod: attachPod(),
496 container: "bar",
497 rawTTY: true,
498 stdin: true,
499 expected: "Session ended, resume using",
500 },
501 {
502 name: "no stdin",
503 pod: attachPod(),
504 container: "bar",
505 rawTTY: true,
506 stdin: false,
507 expected: "",
508 },
509 {
510 name: "not connected to a real TTY",
511 pod: attachPod(),
512 container: "bar",
513 rawTTY: false,
514 stdin: true,
515 expected: "",
516 },
517 {
518 name: "no restarts",
519 pod: &corev1.Pod{
520 ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "test"},
521 Spec: corev1.PodSpec{
522 RestartPolicy: corev1.RestartPolicyNever,
523 Containers: []corev1.Container{{Name: "bar"}},
524 },
525 Status: corev1.PodStatus{Phase: corev1.PodRunning},
526 },
527 container: "bar",
528 rawTTY: true,
529 stdin: true,
530 expected: "",
531 },
532 {
533 name: "ephemeral container",
534 pod: attachPod(),
535 container: "debugger",
536 rawTTY: true,
537 stdin: true,
538 expected: "Session ended, the ephemeral container will not be restarted",
539 },
540 }
541 for _, test := range tests {
542 t.Run(test.name, func(t *testing.T) {
543 options := &AttachOptions{
544 StreamOptions: exec.StreamOptions{
545 Stdin: test.stdin,
546 },
547 Pod: test.pod,
548 }
549 if msg := options.reattachMessage(test.container, test.rawTTY); test.expected == "" && msg != "" {
550 t.Errorf("reattachMessage(%v, %v) = %q, want empty string", test.container, test.rawTTY, msg)
551 } else if !strings.Contains(msg, test.expected) {
552 t.Errorf("reattachMessage(%v, %v) = %q, want string containing %q", test.container, test.rawTTY, msg, test.expected)
553 }
554 })
555 }
556 }
557
558 func TestCreateExecutor(t *testing.T) {
559 url, err := url.Parse("http://localhost:8080/index.html")
560 if err != nil {
561 t.Fatalf("unable to parse test url: %v", err)
562 }
563 config := cmdtesting.DefaultClientConfig()
564
565 executor, err := createExecutor(url, config)
566 if err != nil {
567 t.Fatalf("unable to create executor: %v", err)
568 }
569 if _, isFallback := executor.(*remotecommand.FallbackExecutor); !isFallback {
570 t.Errorf("expected fallback executor, got %#v", executor)
571 }
572
573 t.Setenv(string(cmdutil.RemoteCommandWebsockets), "true")
574 executor, err = createExecutor(url, config)
575 if err != nil {
576 t.Fatalf("unable to create executor: %v", err)
577 }
578 if _, isFallback := executor.(*remotecommand.FallbackExecutor); !isFallback {
579 t.Errorf("expected fallback executor, got %#v", executor)
580 }
581
582 t.Setenv(string(cmdutil.RemoteCommandWebsockets), "false")
583 executor, err = createExecutor(url, config)
584 if err != nil {
585 t.Fatalf("unable to create executor: %v", err)
586 }
587 if _, isFallback := executor.(*remotecommand.FallbackExecutor); isFallback {
588 t.Errorf("expected fallback executor, got %#v", executor)
589 }
590 }
591
View as plain text