...

Source file src/k8s.io/kubectl/pkg/cmd/logs/logs_test.go

Documentation: k8s.io/kubectl/pkg/cmd/logs

     1  /*
     2  Copyright 2014 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    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  		// checkErr breaks tests in case of errors, plus we just
   712  		// need to check errors returned by the command validation
   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  	// We need a WaitGroup in some test cases to make sure that we fetch logs concurrently.
   898  	// These test cases will finish successfully without the WaitGroup, but the WaitGroup
   899  	// will help us to identify regression when someone accidentally changes
   900  	// concurrent fetching to sequential
   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  	// Just copy everything for a test sake
   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