1
16
17 package logs
18
19 import (
20 "bytes"
21 "context"
22 "errors"
23 "fmt"
24 "io"
25 "net/http"
26 "strings"
27 "sync"
28 "testing"
29 "testing/iotest"
30 "time"
31
32 corev1 "k8s.io/api/core/v1"
33 apierrors "k8s.io/apimachinery/pkg/api/errors"
34 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
35 "k8s.io/apimachinery/pkg/runtime"
36 "k8s.io/apimachinery/pkg/runtime/schema"
37 "k8s.io/cli-runtime/pkg/genericclioptions"
38 "k8s.io/cli-runtime/pkg/genericiooptions"
39 restclient "k8s.io/client-go/rest"
40 "k8s.io/client-go/rest/fake"
41 cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
42 "k8s.io/kubectl/pkg/scheme"
43 )
44
45 func TestLog(t *testing.T) {
46 tests := []struct {
47 name string
48 opts func(genericiooptions.IOStreams) *LogsOptions
49 expectedErr string
50 expectedOutSubstrings []string
51 }{
52 {
53 name: "v1 - pod log",
54 opts: func(streams genericiooptions.IOStreams) *LogsOptions {
55 mock := &logTestMock{
56 logsForObjectRequests: map[corev1.ObjectReference]restclient.ResponseWrapper{
57 {
58 Kind: "Pod",
59 Name: "some-pod",
60 FieldPath: "spec.containers{some-container}",
61 }: &responseWrapperMock{data: strings.NewReader("test log content\n")},
62 },
63 }
64
65 o := NewLogsOptions(streams, false)
66 o.LogsForObject = mock.mockLogsForObject
67 o.ConsumeRequestFn = mock.mockConsumeRequest
68
69 return o
70 },
71 expectedOutSubstrings: []string{"test log content\n"},
72 },
73 {
74 name: "pod logs with prefix",
75 opts: func(streams genericiooptions.IOStreams) *LogsOptions {
76 mock := &logTestMock{
77 logsForObjectRequests: map[corev1.ObjectReference]restclient.ResponseWrapper{
78 {
79 Kind: "Pod",
80 Name: "test-pod",
81 FieldPath: "spec.containers{test-container}",
82 }: &responseWrapperMock{data: strings.NewReader("test log content\n")},
83 },
84 }
85
86 o := NewLogsOptions(streams, false)
87 o.LogsForObject = mock.mockLogsForObject
88 o.ConsumeRequestFn = mock.mockConsumeRequest
89 o.Prefix = true
90
91 return o
92 },
93 expectedOutSubstrings: []string{"[pod/test-pod/test-container] test log content\n"},
94 },
95 {
96 name: "pod logs with prefix: init container",
97 opts: func(streams genericiooptions.IOStreams) *LogsOptions {
98 mock := &logTestMock{
99 logsForObjectRequests: map[corev1.ObjectReference]restclient.ResponseWrapper{
100 {
101 Kind: "Pod",
102 Name: "test-pod",
103 FieldPath: "spec.initContainers{test-container}",
104 }: &responseWrapperMock{data: strings.NewReader("test log content\n")},
105 },
106 }
107
108 o := NewLogsOptions(streams, false)
109 o.LogsForObject = mock.mockLogsForObject
110 o.ConsumeRequestFn = mock.mockConsumeRequest
111 o.Prefix = true
112
113 return o
114 },
115 expectedOutSubstrings: []string{"[pod/test-pod/test-container] test log content\n"},
116 },
117 {
118 name: "pod logs with prefix: ephemeral container",
119 opts: func(streams genericiooptions.IOStreams) *LogsOptions {
120 mock := &logTestMock{
121 logsForObjectRequests: map[corev1.ObjectReference]restclient.ResponseWrapper{
122 {
123 Kind: "Pod",
124 Name: "test-pod",
125 FieldPath: "spec.ephemeralContainers{test-container}",
126 }: &responseWrapperMock{data: strings.NewReader("test log content\n")},
127 },
128 }
129
130 o := NewLogsOptions(streams, false)
131 o.LogsForObject = mock.mockLogsForObject
132 o.ConsumeRequestFn = mock.mockConsumeRequest
133 o.Prefix = true
134
135 return o
136 },
137 expectedOutSubstrings: []string{"[pod/test-pod/test-container] test log content\n"},
138 },
139 {
140 name: "get logs from multiple requests sequentially",
141 opts: func(streams genericiooptions.IOStreams) *LogsOptions {
142 mock := &logTestMock{
143 logsForObjectRequests: map[corev1.ObjectReference]restclient.ResponseWrapper{
144 {
145 Kind: "Pod",
146 Name: "some-pod-1",
147 FieldPath: "spec.containers{some-container}",
148 }: &responseWrapperMock{data: strings.NewReader("test log content from source 1\n")},
149 {
150 Kind: "Pod",
151 Name: "some-pod-2",
152 FieldPath: "spec.containers{some-container}",
153 }: &responseWrapperMock{data: strings.NewReader("test log content from source 2\n")},
154 {
155 Kind: "Pod",
156 Name: "some-pod-3",
157 FieldPath: "spec.containers{some-container}",
158 }: &responseWrapperMock{data: strings.NewReader("test log content from source 3\n")},
159 },
160 }
161
162 o := NewLogsOptions(streams, false)
163 o.LogsForObject = mock.mockLogsForObject
164 o.ConsumeRequestFn = mock.mockConsumeRequest
165 return o
166 },
167 expectedOutSubstrings: []string{
168 "test log content from source 1\n",
169 "test log content from source 2\n",
170 "test log content from source 3\n",
171 },
172 },
173 {
174 name: "follow logs from multiple requests concurrently",
175 opts: func(streams genericiooptions.IOStreams) *LogsOptions {
176 wg := &sync.WaitGroup{}
177 mock := &logTestMock{
178 logsForObjectRequests: map[corev1.ObjectReference]restclient.ResponseWrapper{
179 {
180 Kind: "Pod",
181 Name: "some-pod-1",
182 FieldPath: "spec.containers{some-container-1}",
183 }: &responseWrapperMock{data: strings.NewReader("test log content from source 1\n")},
184 {
185 Kind: "Pod",
186 Name: "some-pod-2",
187 FieldPath: "spec.containers{some-container-2}",
188 }: &responseWrapperMock{data: strings.NewReader("test log content from source 2\n")},
189 {
190 Kind: "Pod",
191 Name: "some-pod-3",
192 FieldPath: "spec.containers{some-container-3}",
193 }: &responseWrapperMock{data: strings.NewReader("test log content from source 3\n")},
194 },
195 wg: wg,
196 }
197 wg.Add(3)
198
199 o := NewLogsOptions(streams, false)
200 o.LogsForObject = mock.mockLogsForObject
201 o.ConsumeRequestFn = mock.mockConsumeRequest
202 o.Follow = true
203 return o
204 },
205 expectedOutSubstrings: []string{
206 "test log content from source 1\n",
207 "test log content from source 2\n",
208 "test log content from source 3\n",
209 },
210 },
211 {
212 name: "fail to follow logs from multiple requests when there are more logs sources then MaxFollowConcurrency allows",
213 opts: func(streams genericiooptions.IOStreams) *LogsOptions {
214 wg := &sync.WaitGroup{}
215 mock := &logTestMock{
216 logsForObjectRequests: map[corev1.ObjectReference]restclient.ResponseWrapper{
217 {
218 Kind: "Pod",
219 Name: "test-pod-1",
220 FieldPath: "spec.containers{test-container-1}",
221 }: &responseWrapperMock{data: strings.NewReader("test log content\n")},
222 {
223 Kind: "Pod",
224 Name: "test-pod-2",
225 FieldPath: "spec.containers{test-container-2}",
226 }: &responseWrapperMock{data: strings.NewReader("test log content\n")},
227 {
228 Kind: "Pod",
229 Name: "test-pod-3",
230 FieldPath: "spec.containers{test-container-3}",
231 }: &responseWrapperMock{data: strings.NewReader("test log content\n")},
232 },
233 wg: wg,
234 }
235 wg.Add(3)
236
237 o := NewLogsOptions(streams, false)
238 o.LogsForObject = mock.mockLogsForObject
239 o.ConsumeRequestFn = mock.mockConsumeRequest
240 o.MaxFollowConcurrency = 2
241 o.Follow = true
242 return o
243 },
244 expectedErr: "you are attempting to follow 3 log streams, but maximum allowed concurrency is 2, use --max-log-requests to increase the limit",
245 },
246 {
247 name: "fail if LogsForObject fails",
248 opts: func(streams genericiooptions.IOStreams) *LogsOptions {
249 o := NewLogsOptions(streams, false)
250 o.LogsForObject = func(restClientGetter genericclioptions.RESTClientGetter, object, options runtime.Object, timeout time.Duration, allContainers bool) (map[corev1.ObjectReference]restclient.ResponseWrapper, error) {
251 return nil, errors.New("Error from the LogsForObject")
252 }
253 return o
254 },
255 expectedErr: "Error from the LogsForObject",
256 },
257 {
258 name: "fail to get logs, if ConsumeRequestFn fails",
259 opts: func(streams genericiooptions.IOStreams) *LogsOptions {
260 mock := &logTestMock{
261 logsForObjectRequests: map[corev1.ObjectReference]restclient.ResponseWrapper{
262 {
263 Kind: "Pod",
264 Name: "test-pod-1",
265 FieldPath: "spec.containers{test-container-1}",
266 }: &responseWrapperMock{},
267 {
268 Kind: "Pod",
269 Name: "test-pod-2",
270 FieldPath: "spec.containers{test-container-1}",
271 }: &responseWrapperMock{},
272 },
273 }
274
275 o := NewLogsOptions(streams, false)
276 o.LogsForObject = mock.mockLogsForObject
277 o.ConsumeRequestFn = func(req restclient.ResponseWrapper, out io.Writer) error {
278 return errors.New("Error from the ConsumeRequestFn")
279 }
280 return o
281 },
282 expectedErr: "Error from the ConsumeRequestFn",
283 },
284 {
285 name: "follow logs from multiple requests concurrently with prefix",
286 opts: func(streams genericiooptions.IOStreams) *LogsOptions {
287 wg := &sync.WaitGroup{}
288 mock := &logTestMock{
289 logsForObjectRequests: map[corev1.ObjectReference]restclient.ResponseWrapper{
290 {
291 Kind: "Pod",
292 Name: "test-pod-1",
293 FieldPath: "spec.containers{test-container-1}",
294 }: &responseWrapperMock{data: strings.NewReader("test log content from source 1\n")},
295 {
296 Kind: "Pod",
297 Name: "test-pod-2",
298 FieldPath: "spec.containers{test-container-2}",
299 }: &responseWrapperMock{data: strings.NewReader("test log content from source 2\n")},
300 {
301 Kind: "Pod",
302 Name: "test-pod-3",
303 FieldPath: "spec.containers{test-container-3}",
304 }: &responseWrapperMock{data: strings.NewReader("test log content from source 3\n")},
305 },
306 wg: wg,
307 }
308 wg.Add(3)
309
310 o := NewLogsOptions(streams, false)
311 o.LogsForObject = mock.mockLogsForObject
312 o.ConsumeRequestFn = mock.mockConsumeRequest
313 o.Follow = true
314 o.Prefix = true
315 return o
316 },
317 expectedOutSubstrings: []string{
318 "[pod/test-pod-1/test-container-1] test log content from source 1\n",
319 "[pod/test-pod-2/test-container-2] test log content from source 2\n",
320 "[pod/test-pod-3/test-container-3] test log content from source 3\n",
321 },
322 },
323 {
324 name: "fail to follow logs from multiple requests, if ConsumeRequestFn fails",
325 opts: func(streams genericiooptions.IOStreams) *LogsOptions {
326 wg := &sync.WaitGroup{}
327 mock := &logTestMock{
328 logsForObjectRequests: map[corev1.ObjectReference]restclient.ResponseWrapper{
329 {
330 Kind: "Pod",
331 Name: "test-pod-1",
332 FieldPath: "spec.containers{test-container-1}",
333 }: &responseWrapperMock{},
334 {
335 Kind: "Pod",
336 Name: "test-pod-2",
337 FieldPath: "spec.containers{test-container-2}",
338 }: &responseWrapperMock{},
339 {
340 Kind: "Pod",
341 Name: "test-pod-3",
342 FieldPath: "spec.containers{test-container-3}",
343 }: &responseWrapperMock{},
344 },
345 wg: wg,
346 }
347 wg.Add(3)
348
349 o := NewLogsOptions(streams, false)
350 o.LogsForObject = mock.mockLogsForObject
351 o.ConsumeRequestFn = func(req restclient.ResponseWrapper, out io.Writer) error {
352 return errors.New("Error from the ConsumeRequestFn")
353 }
354 o.Follow = true
355 return o
356 },
357 expectedErr: "Error from the ConsumeRequestFn",
358 },
359 {
360 name: "fail to follow logs, if ConsumeRequestFn fails",
361 opts: func(streams genericiooptions.IOStreams) *LogsOptions {
362 mock := &logTestMock{
363 logsForObjectRequests: map[corev1.ObjectReference]restclient.ResponseWrapper{
364 {
365 Kind: "Pod",
366 Name: "test-pod-1",
367 FieldPath: "spec.containers{test-container-1}",
368 }: &responseWrapperMock{},
369 },
370 }
371
372 o := NewLogsOptions(streams, false)
373 o.LogsForObject = mock.mockLogsForObject
374 o.ConsumeRequestFn = func(req restclient.ResponseWrapper, out io.Writer) error {
375 return errors.New("Error from the ConsumeRequestFn")
376 }
377 o.Follow = true
378 return o
379 },
380 expectedErr: "Error from the ConsumeRequestFn",
381 },
382 {
383 name: "get logs from multiple requests and ignores the error if the container fails",
384 opts: func(streams genericiooptions.IOStreams) *LogsOptions {
385 mock := &logTestMock{
386 logsForObjectRequests: map[corev1.ObjectReference]restclient.ResponseWrapper{
387 {
388 Kind: "Pod",
389 Name: "some-pod-error-container",
390 FieldPath: "spec.containers{some-container}",
391 }: &responseWrapperMock{err: errors.New("error-container")},
392 {
393 Kind: "Pod",
394 Name: "some-pod-1",
395 FieldPath: "spec.containers{some-container}",
396 }: &responseWrapperMock{data: strings.NewReader("test log content from source 1\n")},
397 {
398 Kind: "Pod",
399 Name: "some-pod-2",
400 FieldPath: "spec.containers{some-container}",
401 }: &responseWrapperMock{data: strings.NewReader("test log content from source 2\n")},
402 },
403 }
404
405 o := NewLogsOptions(streams, false)
406 o.LogsForObject = mock.mockLogsForObject
407 o.ConsumeRequestFn = mock.mockConsumeRequest
408 o.IgnoreLogErrors = true
409 return o
410 },
411 expectedOutSubstrings: []string{
412 "error-container\n",
413 "test log content from source 1\n",
414 "test log content from source 2\n",
415 },
416 },
417 {
418 name: "get logs from multiple requests and an container fails",
419 opts: func(streams genericiooptions.IOStreams) *LogsOptions {
420 mock := &logTestMock{
421 logsForObjectRequests: map[corev1.ObjectReference]restclient.ResponseWrapper{
422 {
423 Kind: "Pod",
424 Name: "some-pod-error-container",
425 FieldPath: "spec.containers{some-container}",
426 }: &responseWrapperMock{err: errors.New("error-container")},
427 {
428 Kind: "Pod",
429 Name: "some-pod",
430 FieldPath: "spec.containers{some-container}",
431 }: &responseWrapperMock{data: strings.NewReader("test log content from source\n")},
432 },
433 }
434
435 o := NewLogsOptions(streams, false)
436 o.LogsForObject = mock.mockLogsForObject
437 o.ConsumeRequestFn = mock.mockConsumeRequest
438 return o
439 },
440 expectedErr: "error-container",
441 },
442 {
443 name: "follow logs from multiple requests and ignores the error if the container fails",
444 opts: func(streams genericiooptions.IOStreams) *LogsOptions {
445 mock := &logTestMock{
446 logsForObjectRequests: map[corev1.ObjectReference]restclient.ResponseWrapper{
447 {
448 Kind: "Pod",
449 Name: "some-pod-error-container",
450 FieldPath: "spec.containers{some-container}",
451 }: &responseWrapperMock{err: errors.New("error-container")},
452 {
453 Kind: "Pod",
454 Name: "some-pod-1",
455 FieldPath: "spec.containers{some-container}",
456 }: &responseWrapperMock{data: strings.NewReader("test log content from source 1\n")},
457 {
458 Kind: "Pod",
459 Name: "some-pod-2",
460 FieldPath: "spec.containers{some-container}",
461 }: &responseWrapperMock{data: strings.NewReader("test log content from source 2\n")},
462 },
463 }
464
465 o := NewLogsOptions(streams, false)
466 o.LogsForObject = mock.mockLogsForObject
467 o.ConsumeRequestFn = mock.mockConsumeRequest
468 o.IgnoreLogErrors = true
469 o.Follow = true
470 return o
471 },
472 expectedOutSubstrings: []string{
473 "error-container\n",
474 "test log content from source 1\n",
475 "test log content from source 2\n",
476 },
477 },
478 {
479 name: "follow logs from multiple requests and an container fails",
480 opts: func(streams genericiooptions.IOStreams) *LogsOptions {
481 mock := &logTestMock{
482 logsForObjectRequests: map[corev1.ObjectReference]restclient.ResponseWrapper{
483 {
484 Kind: "Pod",
485 Name: "some-pod-error-container",
486 FieldPath: "spec.containers{some-container}",
487 }: &responseWrapperMock{err: errors.New("error-container")},
488 {
489 Kind: "Pod",
490 Name: "some-pod",
491 FieldPath: "spec.containers{some-container}",
492 }: &responseWrapperMock{data: strings.NewReader("test log content from source\n")},
493 },
494 }
495
496 o := NewLogsOptions(streams, false)
497 o.LogsForObject = mock.mockLogsForObject
498 o.ConsumeRequestFn = mock.mockConsumeRequest
499 o.Follow = true
500 return o
501 },
502 expectedErr: "error-container",
503 },
504 }
505 for _, test := range tests {
506 t.Run(test.name, func(t *testing.T) {
507 tf := cmdtesting.NewTestFactory().WithNamespace("test")
508 defer tf.Cleanup()
509
510 streams, _, buf, _ := genericiooptions.NewTestIOStreams()
511
512 opts := test.opts(streams)
513 opts.Namespace = "test"
514 opts.Object = testPod()
515 opts.Options = &corev1.PodLogOptions{}
516 err := opts.RunLogs()
517
518 if err == nil && len(test.expectedErr) > 0 {
519 t.Fatalf("expected error %q, got none", test.expectedErr)
520 }
521
522 if err != nil && !strings.Contains(err.Error(), test.expectedErr) {
523 t.Errorf("%s: expected to find:\n\t%s\nfound:\n\t%s\n", test.name, test.expectedErr, err.Error())
524 }
525
526 bufStr := buf.String()
527 if test.expectedOutSubstrings != nil {
528 for _, substr := range test.expectedOutSubstrings {
529 if !strings.Contains(bufStr, substr) {
530 t.Errorf("%s: expected to contain %#v. Output: %#v", test.name, substr, bufStr)
531 }
532 }
533 }
534 })
535 }
536 }
537
538 func testPod() *corev1.Pod {
539 return &corev1.Pod{
540 ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "test", ResourceVersion: "10"},
541 Spec: corev1.PodSpec{
542 RestartPolicy: corev1.RestartPolicyAlways,
543 DNSPolicy: corev1.DNSClusterFirst,
544 Containers: []corev1.Container{
545 {
546 Name: "bar",
547 },
548 },
549 },
550 }
551 }
552
553 func TestValidateLogOptions(t *testing.T) {
554 f := cmdtesting.NewTestFactory()
555 defer f.Cleanup()
556 f.WithNamespace("")
557
558 tests := []struct {
559 name string
560 args []string
561 opts func(genericiooptions.IOStreams) *LogsOptions
562 expected string
563 }{
564 {
565 name: "since & since-time",
566 opts: func(streams genericiooptions.IOStreams) *LogsOptions {
567 o := NewLogsOptions(streams, false)
568 o.SinceSeconds = time.Hour
569 o.SinceTime = "2006-01-02T15:04:05Z"
570
571 var err error
572 o.Options, err = o.ToLogOptions()
573 if err != nil {
574 t.Fatalf("unexpected error: %v", err)
575 }
576
577 return o
578 },
579 args: []string{"foo"},
580 expected: "at most one of `sinceTime` or `sinceSeconds` may be specified",
581 },
582 {
583 name: "negative since-time",
584 opts: func(streams genericiooptions.IOStreams) *LogsOptions {
585 o := NewLogsOptions(streams, false)
586 o.SinceSeconds = -1 * time.Second
587
588 var err error
589 o.Options, err = o.ToLogOptions()
590 if err != nil {
591 t.Fatalf("unexpected error: %v", err)
592 }
593
594 return o
595 },
596 args: []string{"foo"},
597 expected: "must be greater than 0",
598 },
599 {
600 name: "negative limit-bytes",
601 opts: func(streams genericiooptions.IOStreams) *LogsOptions {
602 o := NewLogsOptions(streams, false)
603 o.LimitBytes = -100
604
605 var err error
606 o.Options, err = o.ToLogOptions()
607 if err != nil {
608 t.Fatalf("unexpected error: %v", err)
609 }
610
611 return o
612 },
613 args: []string{"foo"},
614 expected: "must be greater than 0",
615 },
616 {
617 name: "negative tail",
618 opts: func(streams genericiooptions.IOStreams) *LogsOptions {
619 o := NewLogsOptions(streams, false)
620 o.Tail = -100
621
622 var err error
623 o.Options, err = o.ToLogOptions()
624 if err != nil {
625 t.Fatalf("unexpected error: %v", err)
626 }
627
628 return o
629 },
630 args: []string{"foo"},
631 expected: "--tail must be greater than or equal to -1",
632 },
633 {
634 name: "container name combined with --all-containers",
635 opts: func(streams genericiooptions.IOStreams) *LogsOptions {
636 o := NewLogsOptions(streams, true)
637 o.Container = "my-container"
638
639 var err error
640 o.Options, err = o.ToLogOptions()
641 if err != nil {
642 t.Fatalf("unexpected error: %v", err)
643 }
644
645 return o
646 },
647 args: []string{"my-pod", "my-container"},
648 expected: "--all-containers=true should not be specified with container",
649 },
650 {
651 name: "container name combined with second argument",
652 opts: func(streams genericiooptions.IOStreams) *LogsOptions {
653 o := NewLogsOptions(streams, false)
654 o.Container = "my-container"
655 o.ContainerNameSpecified = true
656
657 var err error
658 o.Options, err = o.ToLogOptions()
659 if err != nil {
660 t.Fatalf("unexpected error: %v", err)
661 }
662
663 return o
664 },
665 args: []string{"my-pod", "my-container"},
666 expected: "only one of -c or an inline",
667 },
668 }
669 for _, test := range tests {
670 streams := genericiooptions.NewTestIOStreamsDiscard()
671
672 o := test.opts(streams)
673 o.Resources = test.args
674
675 err := o.Validate()
676 if err == nil {
677 t.Fatalf("expected error %q, got none", test.expected)
678 }
679
680 if !strings.Contains(err.Error(), test.expected) {
681 t.Errorf("%s: expected to find:\n\t%s\nfound:\n\t%s\n", test.name, test.expected, err.Error())
682 }
683 }
684 }
685
686 func TestLogComplete(t *testing.T) {
687 f := cmdtesting.NewTestFactory()
688 defer f.Cleanup()
689
690 tests := []struct {
691 name string
692 args []string
693 opts func(genericiooptions.IOStreams) *LogsOptions
694 expected string
695 }{
696 {
697 name: "One args case",
698 args: []string{"foo"},
699 opts: func(streams genericiooptions.IOStreams) *LogsOptions {
700 o := NewLogsOptions(streams, false)
701 o.Selector = "foo"
702 return o
703 },
704 expected: "only a selector (-l) or a POD name is allowed",
705 },
706 }
707 for _, test := range tests {
708 cmd := NewCmdLogs(f, genericiooptions.NewTestIOStreamsDiscard())
709 out := ""
710
711
712
713 o := test.opts(genericiooptions.NewTestIOStreamsDiscard())
714 err := o.Complete(f, cmd, test.args)
715 if err == nil {
716 t.Fatalf("expected error %q, got none", test.expected)
717 }
718
719 out = err.Error()
720 if !strings.Contains(out, test.expected) {
721 t.Errorf("%s: expected to find:\n\t%s\nfound:\n\t%s\n", test.name, test.expected, out)
722 }
723 }
724 }
725
726 func TestDefaultConsumeRequest(t *testing.T) {
727 tests := []struct {
728 name string
729 request restclient.ResponseWrapper
730 expectedErr string
731 expectedOut string
732 }{
733 {
734 name: "error from request stream",
735 request: &responseWrapperMock{
736 err: errors.New("err from the stream"),
737 },
738 expectedErr: "err from the stream",
739 },
740 {
741 name: "error while reading",
742 request: &responseWrapperMock{
743 data: iotest.TimeoutReader(strings.NewReader("Some data")),
744 },
745 expectedErr: iotest.ErrTimeout.Error(),
746 expectedOut: "Some data",
747 },
748 {
749 name: "read with empty string",
750 request: &responseWrapperMock{
751 data: strings.NewReader(""),
752 },
753 expectedOut: "",
754 },
755 {
756 name: "read without new lines",
757 request: &responseWrapperMock{
758 data: strings.NewReader("some string without a new line"),
759 },
760 expectedOut: "some string without a new line",
761 },
762 {
763 name: "read with newlines in the middle",
764 request: &responseWrapperMock{
765 data: strings.NewReader("foo\nbar"),
766 },
767 expectedOut: "foo\nbar",
768 },
769 {
770 name: "read with newline at the end",
771 request: &responseWrapperMock{
772 data: strings.NewReader("foo\n"),
773 },
774 expectedOut: "foo\n",
775 },
776 }
777 for _, test := range tests {
778 t.Run(test.name, func(t *testing.T) {
779 buf := &bytes.Buffer{}
780 err := DefaultConsumeRequest(test.request, buf)
781
782 if err != nil && !strings.Contains(err.Error(), test.expectedErr) {
783 t.Errorf("%s: expected to find:\n\t%s\nfound:\n\t%s\n", test.name, test.expectedErr, err.Error())
784 }
785
786 if buf.String() != test.expectedOut {
787 t.Errorf("%s: did not get expected log content. Got: %s", test.name, buf.String())
788 }
789 })
790 }
791 }
792
793 func TestNoResourceFoundMessage(t *testing.T) {
794 tf := cmdtesting.NewTestFactory().WithNamespace("test")
795 defer tf.Cleanup()
796
797 ns := scheme.Codecs.WithoutConversion()
798 codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
799 pods, _, _ := cmdtesting.EmptyTestData()
800 tf.UnstructuredClient = &fake.RESTClient{
801 NegotiatedSerializer: ns,
802 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
803 switch req.URL.Path {
804 case "/namespaces/test/pods":
805 if req.URL.Query().Get("labelSelector") == "foo" {
806 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, pods)}, nil
807 }
808 t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
809 return nil, nil
810 default:
811 t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
812 return nil, nil
813 }
814 }),
815 }
816
817 streams, _, buf, errbuf := genericiooptions.NewTestIOStreams()
818 cmd := NewCmdLogs(tf, streams)
819 o := NewLogsOptions(streams, false)
820 o.Selector = "foo"
821 err := o.Complete(tf, cmd, []string{})
822
823 if err != nil {
824 t.Fatalf("Unexpected error, expected none, got %v", err)
825 }
826
827 expected := ""
828 if e, a := expected, buf.String(); e != a {
829 t.Errorf("expected to find:\n\t%s\nfound:\n\t%s\n", e, a)
830 }
831
832 expectedErr := "No resources found in test namespace.\n"
833 if e, a := expectedErr, errbuf.String(); e != a {
834 t.Errorf("expected to find:\n\t%s\nfound:\n\t%s\n", e, a)
835 }
836 }
837
838 func TestNoPodInNamespaceFoundMessage(t *testing.T) {
839 namespace, podName := "test", "bar"
840
841 tf := cmdtesting.NewTestFactory().WithNamespace(namespace)
842 defer tf.Cleanup()
843
844 ns := scheme.Codecs.WithoutConversion()
845 codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
846 errStatus := apierrors.NewNotFound(schema.GroupResource{Resource: "pods"}, podName).Status()
847
848 tf.UnstructuredClient = &fake.RESTClient{
849 NegotiatedSerializer: ns,
850 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
851 switch req.URL.Path {
852 case fmt.Sprintf("/namespaces/%s/pods/%s", namespace, podName):
853 fallthrough
854 case fmt.Sprintf("/namespaces/%s/pods", namespace):
855 fallthrough
856 case fmt.Sprintf("/api/v1/namespaces/%s", namespace):
857 return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &errStatus)}, nil
858 default:
859 t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
860 return nil, nil
861 }
862 }),
863 }
864
865 streams, _, _, _ := genericiooptions.NewTestIOStreams()
866 cmd := NewCmdLogs(tf, streams)
867 o := NewLogsOptions(streams, false)
868 err := o.Complete(tf, cmd, []string{podName})
869
870 if err == nil {
871 t.Fatal("Expected NotFound error, got nil")
872 }
873
874 expected := fmt.Sprintf("error from server (NotFound): pods %q not found in namespace %q", podName, namespace)
875 if e, a := expected, err.Error(); e != a {
876 t.Errorf("expected to find:\n\t%s\nfound:\n\t%s\n", e, a)
877 }
878 }
879
880 type responseWrapperMock struct {
881 data io.Reader
882 err error
883 }
884
885 func (r *responseWrapperMock) DoRaw(context.Context) ([]byte, error) {
886 data, _ := io.ReadAll(r.data)
887 return data, r.err
888 }
889
890 func (r *responseWrapperMock) Stream(context.Context) (io.ReadCloser, error) {
891 return io.NopCloser(r.data), r.err
892 }
893
894 type logTestMock struct {
895 logsForObjectRequests map[corev1.ObjectReference]restclient.ResponseWrapper
896
897
898
899
900
901 wg *sync.WaitGroup
902 }
903
904 func (l *logTestMock) mockConsumeRequest(request restclient.ResponseWrapper, out io.Writer) error {
905 readCloser, err := request.Stream(context.Background())
906 if err != nil {
907 return err
908 }
909 defer readCloser.Close()
910
911
912 _, err = io.Copy(out, readCloser)
913 if l.wg != nil {
914 l.wg.Done()
915 l.wg.Wait()
916 }
917 return err
918 }
919
920 func (l *logTestMock) mockLogsForObject(restClientGetter genericclioptions.RESTClientGetter, object, options runtime.Object, timeout time.Duration, allContainers bool) (map[corev1.ObjectReference]restclient.ResponseWrapper, error) {
921 switch object.(type) {
922 case *corev1.Pod:
923 _, ok := options.(*corev1.PodLogOptions)
924 if !ok {
925 return nil, errors.New("provided options object is not a PodLogOptions")
926 }
927
928 return l.logsForObjectRequests, nil
929 default:
930 return nil, fmt.Errorf("cannot get the logs from %T", object)
931 }
932 }
933
View as plain text