diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/logs/logs.go b/staging/src/k8s.io/kubectl/pkg/cmd/logs/logs.go index 0df11878547..190ea4647f2 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/logs/logs.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/logs/logs.go @@ -99,6 +99,7 @@ type LogsOptions struct { Namespace string ResourceArg string AllContainers bool + AllPods bool Options runtime.Object Resources []string @@ -122,10 +123,11 @@ type LogsOptions struct { MaxFollowConcurrency int Prefix bool - Object runtime.Object - GetPodTimeout time.Duration - RESTClientGetter genericclioptions.RESTClientGetter - LogsForObject polymorphichelpers.LogsForObjectFunc + Object runtime.Object + GetPodTimeout time.Duration + RESTClientGetter genericclioptions.RESTClientGetter + LogsForObject polymorphichelpers.LogsForObjectFunc + AllPodLogsForObject polymorphichelpers.AllPodLogsForObjectFunc genericiooptions.IOStreams @@ -134,10 +136,9 @@ type LogsOptions struct { containerNameFromRefSpecRegexp *regexp.Regexp } -func NewLogsOptions(streams genericiooptions.IOStreams, allContainers bool) *LogsOptions { +func NewLogsOptions(streams genericiooptions.IOStreams) *LogsOptions { return &LogsOptions{ IOStreams: streams, - AllContainers: allContainers, Tail: -1, MaxFollowConcurrency: 5, @@ -147,7 +148,7 @@ func NewLogsOptions(streams genericiooptions.IOStreams, allContainers bool) *Log // NewCmdLogs creates a new pod logs command func NewCmdLogs(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command { - o := NewLogsOptions(streams, false) + o := NewLogsOptions(streams) cmd := &cobra.Command{ Use: logsUsageStr, @@ -167,6 +168,7 @@ func NewCmdLogs(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Co } func (o *LogsOptions) AddFlags(cmd *cobra.Command) { + cmd.Flags().BoolVar(&o.AllPods, "all-pods", o.AllPods, "Get logs from all pod(s). Sets prefix to true.") cmd.Flags().BoolVar(&o.AllContainers, "all-containers", o.AllContainers, "Get all containers' logs in the pod(s).") cmd.Flags().BoolVarP(&o.Follow, "follow", "f", o.Follow, "Specify if the logs should be streamed.") cmd.Flags().BoolVar(&o.Timestamps, "timestamps", o.Timestamps, "Include timestamps on each line in the log output") @@ -243,6 +245,11 @@ func (o *LogsOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []str default: return cmdutil.UsageErrorf(cmd, "%s", logsUsageErrStr) } + + if o.AllPods { + o.Prefix = true + } + var err error o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace() if err != nil { @@ -263,6 +270,7 @@ func (o *LogsOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []str o.RESTClientGetter = f o.LogsForObject = polymorphichelpers.LogsForObjectFn + o.AllPodLogsForObject = polymorphichelpers.AllPodLogsForObjectFn if o.Object == nil { builder := f.NewBuilder(). @@ -328,7 +336,13 @@ func (o LogsOptions) Validate() error { // RunLogs retrieves a pod log func (o LogsOptions) RunLogs() error { - requests, err := o.LogsForObject(o.RESTClientGetter, o.Object, o.Options, o.GetPodTimeout, o.AllContainers) + var requests map[corev1.ObjectReference]rest.ResponseWrapper + var err error + if o.AllPods { + requests, err = o.AllPodLogsForObject(o.RESTClientGetter, o.Object, o.Options, o.GetPodTimeout, o.AllContainers) + } else { + requests, err = o.LogsForObject(o.RESTClientGetter, o.Object, o.Options, o.GetPodTimeout, o.AllContainers) + } if err != nil { return err } diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/logs/logs_test.go b/staging/src/k8s.io/kubectl/pkg/cmd/logs/logs_test.go index 80d584fd076..61b10ec93fd 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/logs/logs_test.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/logs/logs_test.go @@ -29,6 +29,7 @@ import ( "testing/iotest" "time" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -62,7 +63,7 @@ func TestLog(t *testing.T) { }, } - o := NewLogsOptions(streams, false) + o := NewLogsOptions(streams) o.LogsForObject = mock.mockLogsForObject o.ConsumeRequestFn = mock.mockConsumeRequest @@ -83,7 +84,7 @@ func TestLog(t *testing.T) { }, } - o := NewLogsOptions(streams, false) + o := NewLogsOptions(streams) o.LogsForObject = mock.mockLogsForObject o.ConsumeRequestFn = mock.mockConsumeRequest o.Prefix = true @@ -92,6 +93,32 @@ func TestLog(t *testing.T) { }, expectedOutSubstrings: []string{"[pod/test-pod/test-container] test log content\n"}, }, + { + name: "stateful set logs with all pods", + opts: func(streams genericiooptions.IOStreams) *LogsOptions { + mock := &logTestMock{ + logsForObjectRequests: map[corev1.ObjectReference]restclient.ResponseWrapper{ + { + Kind: "Pod", + Name: "test-sts-0", + FieldPath: "spec.containers{test-container}", + }: &responseWrapperMock{data: strings.NewReader("test log content for pod test-sts-0\n")}, + { + Kind: "Pod", + Name: "test-sts-1", + FieldPath: "spec.containers{test-container}", + }: &responseWrapperMock{data: strings.NewReader("test log content for pod test-sts-1\n")}, + }, + } + + o := NewLogsOptions(streams) + o.LogsForObject = mock.mockLogsForObject + o.ConsumeRequestFn = mock.mockConsumeRequest + o.Prefix = true + return o + }, + expectedOutSubstrings: []string{"[pod/test-sts-0/test-container] test log content for pod test-sts-0\n[pod/test-sts-1/test-container] test log content for pod test-sts-1\n"}, + }, { name: "pod logs with prefix: init container", opts: func(streams genericiooptions.IOStreams) *LogsOptions { @@ -105,7 +132,7 @@ func TestLog(t *testing.T) { }, } - o := NewLogsOptions(streams, false) + o := NewLogsOptions(streams) o.LogsForObject = mock.mockLogsForObject o.ConsumeRequestFn = mock.mockConsumeRequest o.Prefix = true @@ -127,7 +154,7 @@ func TestLog(t *testing.T) { }, } - o := NewLogsOptions(streams, false) + o := NewLogsOptions(streams) o.LogsForObject = mock.mockLogsForObject o.ConsumeRequestFn = mock.mockConsumeRequest o.Prefix = true @@ -159,7 +186,7 @@ func TestLog(t *testing.T) { }, } - o := NewLogsOptions(streams, false) + o := NewLogsOptions(streams) o.LogsForObject = mock.mockLogsForObject o.ConsumeRequestFn = mock.mockConsumeRequest return o @@ -196,7 +223,7 @@ func TestLog(t *testing.T) { } wg.Add(3) - o := NewLogsOptions(streams, false) + o := NewLogsOptions(streams) o.LogsForObject = mock.mockLogsForObject o.ConsumeRequestFn = mock.mockConsumeRequest o.Follow = true @@ -234,7 +261,7 @@ func TestLog(t *testing.T) { } wg.Add(3) - o := NewLogsOptions(streams, false) + o := NewLogsOptions(streams) o.LogsForObject = mock.mockLogsForObject o.ConsumeRequestFn = mock.mockConsumeRequest o.MaxFollowConcurrency = 2 @@ -246,7 +273,7 @@ func TestLog(t *testing.T) { { name: "fail if LogsForObject fails", opts: func(streams genericiooptions.IOStreams) *LogsOptions { - o := NewLogsOptions(streams, false) + o := NewLogsOptions(streams) o.LogsForObject = func(restClientGetter genericclioptions.RESTClientGetter, object, options runtime.Object, timeout time.Duration, allContainers bool) (map[corev1.ObjectReference]restclient.ResponseWrapper, error) { return nil, errors.New("Error from the LogsForObject") } @@ -272,7 +299,7 @@ func TestLog(t *testing.T) { }, } - o := NewLogsOptions(streams, false) + o := NewLogsOptions(streams) o.LogsForObject = mock.mockLogsForObject o.ConsumeRequestFn = func(req restclient.ResponseWrapper, out io.Writer) error { return errors.New("Error from the ConsumeRequestFn") @@ -307,7 +334,7 @@ func TestLog(t *testing.T) { } wg.Add(3) - o := NewLogsOptions(streams, false) + o := NewLogsOptions(streams) o.LogsForObject = mock.mockLogsForObject o.ConsumeRequestFn = mock.mockConsumeRequest o.Follow = true @@ -346,7 +373,7 @@ func TestLog(t *testing.T) { } wg.Add(3) - o := NewLogsOptions(streams, false) + o := NewLogsOptions(streams) o.LogsForObject = mock.mockLogsForObject o.ConsumeRequestFn = func(req restclient.ResponseWrapper, out io.Writer) error { return errors.New("Error from the ConsumeRequestFn") @@ -369,7 +396,7 @@ func TestLog(t *testing.T) { }, } - o := NewLogsOptions(streams, false) + o := NewLogsOptions(streams) o.LogsForObject = mock.mockLogsForObject o.ConsumeRequestFn = func(req restclient.ResponseWrapper, out io.Writer) error { return errors.New("Error from the ConsumeRequestFn") @@ -402,7 +429,7 @@ func TestLog(t *testing.T) { }, } - o := NewLogsOptions(streams, false) + o := NewLogsOptions(streams) o.LogsForObject = mock.mockLogsForObject o.ConsumeRequestFn = mock.mockConsumeRequest o.IgnoreLogErrors = true @@ -432,7 +459,7 @@ func TestLog(t *testing.T) { }, } - o := NewLogsOptions(streams, false) + o := NewLogsOptions(streams) o.LogsForObject = mock.mockLogsForObject o.ConsumeRequestFn = mock.mockConsumeRequest return o @@ -462,7 +489,7 @@ func TestLog(t *testing.T) { }, } - o := NewLogsOptions(streams, false) + o := NewLogsOptions(streams) o.LogsForObject = mock.mockLogsForObject o.ConsumeRequestFn = mock.mockConsumeRequest o.IgnoreLogErrors = true @@ -493,7 +520,7 @@ func TestLog(t *testing.T) { }, } - o := NewLogsOptions(streams, false) + o := NewLogsOptions(streams) o.LogsForObject = mock.mockLogsForObject o.ConsumeRequestFn = mock.mockConsumeRequest o.Follow = true @@ -564,7 +591,7 @@ func TestValidateLogOptions(t *testing.T) { { name: "since & since-time", opts: func(streams genericiooptions.IOStreams) *LogsOptions { - o := NewLogsOptions(streams, false) + o := NewLogsOptions(streams) o.SinceSeconds = time.Hour o.SinceTime = "2006-01-02T15:04:05Z" @@ -582,7 +609,7 @@ func TestValidateLogOptions(t *testing.T) { { name: "negative since-time", opts: func(streams genericiooptions.IOStreams) *LogsOptions { - o := NewLogsOptions(streams, false) + o := NewLogsOptions(streams) o.SinceSeconds = -1 * time.Second var err error @@ -599,7 +626,7 @@ func TestValidateLogOptions(t *testing.T) { { name: "negative limit-bytes", opts: func(streams genericiooptions.IOStreams) *LogsOptions { - o := NewLogsOptions(streams, false) + o := NewLogsOptions(streams) o.LimitBytes = -100 var err error @@ -616,7 +643,7 @@ func TestValidateLogOptions(t *testing.T) { { name: "negative tail", opts: func(streams genericiooptions.IOStreams) *LogsOptions { - o := NewLogsOptions(streams, false) + o := NewLogsOptions(streams) o.Tail = -100 var err error @@ -633,7 +660,8 @@ func TestValidateLogOptions(t *testing.T) { { name: "container name combined with --all-containers", opts: func(streams genericiooptions.IOStreams) *LogsOptions { - o := NewLogsOptions(streams, true) + o := NewLogsOptions(streams) + o.AllContainers = true o.Container = "my-container" var err error @@ -650,7 +678,7 @@ func TestValidateLogOptions(t *testing.T) { { name: "container name combined with second argument", opts: func(streams genericiooptions.IOStreams) *LogsOptions { - o := NewLogsOptions(streams, false) + o := NewLogsOptions(streams) o.Container = "my-container" o.ContainerNameSpecified = true @@ -697,7 +725,7 @@ func TestLogComplete(t *testing.T) { name: "One args case", args: []string{"foo"}, opts: func(streams genericiooptions.IOStreams) *LogsOptions { - o := NewLogsOptions(streams, false) + o := NewLogsOptions(streams) o.Selector = "foo" return o }, @@ -816,7 +844,7 @@ func TestNoResourceFoundMessage(t *testing.T) { streams, _, buf, errbuf := genericiooptions.NewTestIOStreams() cmd := NewCmdLogs(tf, streams) - o := NewLogsOptions(streams, false) + o := NewLogsOptions(streams) o.Selector = "foo" err := o.Complete(tf, cmd, []string{}) @@ -864,7 +892,7 @@ func TestNoPodInNamespaceFoundMessage(t *testing.T) { streams, _, _, _ := genericiooptions.NewTestIOStreams() cmd := NewCmdLogs(tf, streams) - o := NewLogsOptions(streams, false) + o := NewLogsOptions(streams) err := o.Complete(tf, cmd, []string{podName}) if err == nil { @@ -919,6 +947,13 @@ func (l *logTestMock) mockConsumeRequest(request restclient.ResponseWrapper, out func (l *logTestMock) mockLogsForObject(restClientGetter genericclioptions.RESTClientGetter, object, options runtime.Object, timeout time.Duration, allContainers bool) (map[corev1.ObjectReference]restclient.ResponseWrapper, error) { switch object.(type) { + case *appsv1.Deployment: + _, ok := options.(*corev1.PodLogOptions) + if !ok { + return nil, errors.New("provided options object is not a PodLogOptions") + } + + return l.logsForObjectRequests, nil case *corev1.Pod: _, ok := options.(*corev1.PodLogOptions) if !ok { diff --git a/staging/src/k8s.io/kubectl/pkg/polymorphichelpers/helpers.go b/staging/src/k8s.io/kubectl/pkg/polymorphichelpers/helpers.go index 762d953f2c6..30adb24ed68 100644 --- a/staging/src/k8s.io/kubectl/pkg/polymorphichelpers/helpers.go +++ b/staging/src/k8s.io/kubectl/pkg/polymorphichelpers/helpers.go @@ -36,15 +36,15 @@ import ( watchtools "k8s.io/client-go/tools/watch" ) -// GetFirstPod returns a pod matching the namespace and label selector -// and the number of all pods that match the label selector. -func GetFirstPod(client coreclient.PodsGetter, namespace string, selector string, timeout time.Duration, sortBy func([]*corev1.Pod) sort.Interface) (*corev1.Pod, int, error) { +// GetPodList returns a PodList matching the namespace and label selector +func GetPodList(client coreclient.PodsGetter, namespace string, selector string, timeout time.Duration, sortBy func([]*corev1.Pod) sort.Interface) (*corev1.PodList, error) { options := metav1.ListOptions{LabelSelector: selector} podList, err := client.Pods(namespace).List(context.TODO(), options) if err != nil { - return nil, 0, err + return nil, err } + pods := []*corev1.Pod{} for i := range podList.Items { pod := podList.Items[i] @@ -52,14 +52,17 @@ func GetFirstPod(client coreclient.PodsGetter, namespace string, selector string } if len(pods) > 0 { sort.Sort(sortBy(pods)) - return pods[0], len(podList.Items), nil + for i, pod := range pods { + podList.Items[i] = *pod + } + return podList, nil } // Watch until we observe a pod options.ResourceVersion = podList.ResourceVersion w, err := client.Pods(namespace).Watch(context.TODO(), options) if err != nil { - return nil, 0, err + return nil, err } defer w.Stop() @@ -70,14 +73,30 @@ func GetFirstPod(client coreclient.PodsGetter, namespace string, selector string ctx, cancel := watchtools.ContextWithOptionalTimeout(context.Background(), timeout) defer cancel() event, err := watchtools.UntilWithoutRetry(ctx, w, condition) + if err != nil { + return nil, err + } + + po, ok := event.Object.(*corev1.Pod) + if !ok { + return nil, fmt.Errorf("%#v is not a pod event", event) + } else { + podList.Items = append(podList.Items, *po) + } + return podList, nil +} + +// GetFirstPod returns a pod matching the namespace and label selector +// and the number of all pods that match the label selector. +func GetFirstPod(client coreclient.PodsGetter, namespace string, selector string, timeout time.Duration, sortBy func([]*corev1.Pod) sort.Interface) (*corev1.Pod, int, error) { + + podList, err := GetPodList(client, namespace, selector, timeout, sortBy) if err != nil { return nil, 0, err } - pod, ok := event.Object.(*corev1.Pod) - if !ok { - return nil, 0, fmt.Errorf("%#v is not a pod event", event) - } - return pod, 1, nil + + return &podList.Items[0], len(podList.Items), nil + } // SelectorsForObject returns the pod label selector for a given object diff --git a/staging/src/k8s.io/kubectl/pkg/polymorphichelpers/helpers_test.go b/staging/src/k8s.io/kubectl/pkg/polymorphichelpers/helpers_test.go index 70afa2acac8..76628cf6312 100644 --- a/staging/src/k8s.io/kubectl/pkg/polymorphichelpers/helpers_test.go +++ b/staging/src/k8s.io/kubectl/pkg/polymorphichelpers/helpers_test.go @@ -32,6 +32,98 @@ import ( "k8s.io/kubectl/pkg/util/podutils" ) +func TestGetPodList(t *testing.T) { + labelSet := map[string]string{"test": "selector"} + tests := []struct { + name string + + podList *corev1.PodList + watching []watch.Event + sortBy func([]*corev1.Pod) sort.Interface + + expected *corev1.PodList + expectedNum int + expectedErr bool + }{ + { + name: "kubectl logs - two ready pods", + podList: newPodList(2, -1, -1, labelSet), + sortBy: func(pods []*corev1.Pod) sort.Interface { return podutils.ByLogging(pods) }, + expected: &corev1.PodList{ + Items: []corev1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-1", + Namespace: metav1.NamespaceDefault, + CreationTimestamp: metav1.Date(2016, time.April, 1, 1, 0, 0, 0, time.UTC), + Labels: map[string]string{"test": "selector"}, + }, + Status: corev1.PodStatus{ + Conditions: []corev1.PodCondition{ + { + Status: corev1.ConditionTrue, + Type: corev1.PodReady, + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-2", + Namespace: metav1.NamespaceDefault, + CreationTimestamp: metav1.Date(2016, time.April, 1, 1, 0, 1, 0, time.UTC), + Labels: map[string]string{"test": "selector"}, + }, + Status: corev1.PodStatus{ + Conditions: []corev1.PodCondition{ + { + Status: corev1.ConditionTrue, + Type: corev1.PodReady, + }, + }, + }, + }, + }, + }, + expectedNum: 2, + }, + } + + for i := range tests { + test := tests[i] + fake := fakeexternal.NewSimpleClientset(test.podList) + if len(test.watching) > 0 { + watcher := watch.NewFake() + for _, event := range test.watching { + switch event.Type { + case watch.Added: + go watcher.Add(event.Object) + case watch.Modified: + go watcher.Modify(event.Object) + } + } + fake.PrependWatchReactor("pods", testcore.DefaultWatchReactor(watcher, nil)) + } + selector := labels.Set(labelSet).AsSelector() + podList, err := GetPodList(fake.CoreV1(), metav1.NamespaceDefault, selector.String(), 1*time.Minute, test.sortBy) + + if !test.expectedErr && err != nil { + t.Errorf("%s: unexpected error: %v", test.name, err) + continue + } + if test.expectedErr && err == nil { + t.Errorf("%s: expected an error", test.name) + continue + } + if test.expectedNum != len(podList.Items) { + t.Errorf("%s: expected %d pods, got %d", test.name, test.expectedNum, len(podList.Items)) + continue + } + if !apiequality.Semantic.DeepEqual(test.expected, podList) { + t.Errorf("%s:\nexpected podList:\n%#v\ngot:\n%#v\n\n", test.name, test.expected, podList) + } + } +} func TestGetFirstPod(t *testing.T) { labelSet := map[string]string{"test": "selector"} tests := []struct { diff --git a/staging/src/k8s.io/kubectl/pkg/polymorphichelpers/interface.go b/staging/src/k8s.io/kubectl/pkg/polymorphichelpers/interface.go index 89c4e9facf6..ad8d1d6d58c 100644 --- a/staging/src/k8s.io/kubectl/pkg/polymorphichelpers/interface.go +++ b/staging/src/k8s.io/kubectl/pkg/polymorphichelpers/interface.go @@ -27,6 +27,12 @@ import ( "k8s.io/client-go/rest" ) +// AllPodLogsForObjectFunc is a function type that can tell you how to get logs for a runtime.object +type AllPodLogsForObjectFunc func(restClientGetter genericclioptions.RESTClientGetter, object, options runtime.Object, timeout time.Duration, allContainers bool) (map[v1.ObjectReference]rest.ResponseWrapper, error) + +// AllPodLogsForObjectFn gives a way to easily override the function for unit testing if needed. +var AllPodLogsForObjectFn AllPodLogsForObjectFunc = allPodLogsForObject + // LogsForObjectFunc is a function type that can tell you how to get logs for a runtime.object type LogsForObjectFunc func(restClientGetter genericclioptions.RESTClientGetter, object, options runtime.Object, timeout time.Duration, allContainers bool) (map[v1.ObjectReference]rest.ResponseWrapper, error) diff --git a/staging/src/k8s.io/kubectl/pkg/polymorphichelpers/logsforobject.go b/staging/src/k8s.io/kubectl/pkg/polymorphichelpers/logsforobject.go index c436c06d313..87968b789d0 100644 --- a/staging/src/k8s.io/kubectl/pkg/polymorphichelpers/logsforobject.go +++ b/staging/src/k8s.io/kubectl/pkg/polymorphichelpers/logsforobject.go @@ -34,6 +34,19 @@ import ( "k8s.io/kubectl/pkg/util/podutils" ) +func allPodLogsForObject(restClientGetter genericclioptions.RESTClientGetter, object, options runtime.Object, timeout time.Duration, allContainers bool) (map[corev1.ObjectReference]rest.ResponseWrapper, error) { + clientConfig, err := restClientGetter.ToRESTConfig() + if err != nil { + return nil, err + } + + clientset, err := corev1client.NewForConfig(clientConfig) + if err != nil { + return nil, err + } + return logsForObjectWithClient(clientset, object, options, timeout, allContainers, true) +} + func logsForObject(restClientGetter genericclioptions.RESTClientGetter, object, options runtime.Object, timeout time.Duration, allContainers bool) (map[corev1.ObjectReference]rest.ResponseWrapper, error) { clientConfig, err := restClientGetter.ToRESTConfig() if err != nil { @@ -44,11 +57,11 @@ func logsForObject(restClientGetter genericclioptions.RESTClientGetter, object, if err != nil { return nil, err } - return logsForObjectWithClient(clientset, object, options, timeout, allContainers) + return logsForObjectWithClient(clientset, object, options, timeout, allContainers, false) } // this is split for easy test-ability -func logsForObjectWithClient(clientset corev1client.CoreV1Interface, object, options runtime.Object, timeout time.Duration, allContainers bool) (map[corev1.ObjectReference]rest.ResponseWrapper, error) { +func logsForObjectWithClient(clientset corev1client.CoreV1Interface, object, options runtime.Object, timeout time.Duration, allContainers bool, allPods bool) (map[corev1.ObjectReference]rest.ResponseWrapper, error) { opts, ok := options.(*corev1.PodLogOptions) if !ok { return nil, errors.New("provided options object is not a PodLogOptions") @@ -58,7 +71,7 @@ func logsForObjectWithClient(clientset corev1client.CoreV1Interface, object, opt case *corev1.PodList: ret := make(map[corev1.ObjectReference]rest.ResponseWrapper) for i := range t.Items { - currRet, err := logsForObjectWithClient(clientset, &t.Items[i], options, timeout, allContainers) + currRet, err := logsForObjectWithClient(clientset, &t.Items[i], options, timeout, allContainers, allPods) if err != nil { return nil, err } @@ -95,7 +108,9 @@ func logsForObjectWithClient(clientset corev1client.CoreV1Interface, object, opt // Default to the first container name(aligning behavior with `kubectl exec'). currOpts.Container = t.Spec.Containers[0].Name if len(t.Spec.Containers) > 1 || len(t.Spec.InitContainers) > 0 || len(t.Spec.EphemeralContainers) > 0 { - fmt.Fprintf(os.Stderr, "Defaulted container %q out of: %s\n", currOpts.Container, podcmd.AllContainerNames(t)) + if !allPods { + fmt.Fprintf(os.Stderr, "Defaulted container %q out of: %s\n", currOpts.Container, podcmd.AllContainerNames(t)) + } } } @@ -117,7 +132,7 @@ func logsForObjectWithClient(clientset corev1client.CoreV1Interface, object, opt for _, c := range t.Spec.InitContainers { currOpts := opts.DeepCopy() currOpts.Container = c.Name - currRet, err := logsForObjectWithClient(clientset, t, currOpts, timeout, false) + currRet, err := logsForObjectWithClient(clientset, t, currOpts, timeout, false, false) if err != nil { return nil, err } @@ -128,7 +143,7 @@ func logsForObjectWithClient(clientset corev1client.CoreV1Interface, object, opt for _, c := range t.Spec.Containers { currOpts := opts.DeepCopy() currOpts.Container = c.Name - currRet, err := logsForObjectWithClient(clientset, t, currOpts, timeout, false) + currRet, err := logsForObjectWithClient(clientset, t, currOpts, timeout, false, false) if err != nil { return nil, err } @@ -139,7 +154,7 @@ func logsForObjectWithClient(clientset corev1client.CoreV1Interface, object, opt for _, c := range t.Spec.EphemeralContainers { currOpts := opts.DeepCopy() currOpts.Container = c.Name - currRet, err := logsForObjectWithClient(clientset, t, currOpts, timeout, false) + currRet, err := logsForObjectWithClient(clientset, t, currOpts, timeout, false, false) if err != nil { return nil, err } @@ -161,9 +176,18 @@ func logsForObjectWithClient(clientset corev1client.CoreV1Interface, object, opt if err != nil { return nil, err } + var targetObj runtime.Object = pod + if numPods > 1 { - fmt.Fprintf(os.Stderr, "Found %v pods, using pod/%v\n", numPods, pod.Name) + if allPods { + targetObj, err = GetPodList(clientset, namespace, selector.String(), timeout, sortBy) + if err != nil { + return nil, err + } + } else { + fmt.Fprintf(os.Stderr, "Found %v pods, using pod/%v\n", numPods, pod.Name) + } } - return logsForObjectWithClient(clientset, pod, options, timeout, allContainers) + return logsForObjectWithClient(clientset, targetObj, options, timeout, allContainers, allPods) } diff --git a/staging/src/k8s.io/kubectl/pkg/polymorphichelpers/logsforobject_test.go b/staging/src/k8s.io/kubectl/pkg/polymorphichelpers/logsforobject_test.go index 123d78db2ac..d2ded0d895f 100644 --- a/staging/src/k8s.io/kubectl/pkg/polymorphichelpers/logsforobject_test.go +++ b/staging/src/k8s.io/kubectl/pkg/polymorphichelpers/logsforobject_test.go @@ -46,6 +46,7 @@ func TestLogsForObject(t *testing.T) { obj runtime.Object opts *corev1.PodLogOptions allContainers bool + allPods bool clientsetPods []runtime.Object actions []testclient.Action @@ -73,6 +74,7 @@ func TestLogsForObject(t *testing.T) { obj: testPodWithTwoContainersAndTwoInitAndOneEphemeralContainers(), opts: &corev1.PodLogOptions{}, allContainers: true, + allPods: false, actions: []testclient.Action{ getLogsAction("test", &corev1.PodLogOptions{Container: "foo-2-and-2-and-1-initc1"}), getLogsAction("test", &corev1.PodLogOptions{Container: "foo-2-and-2-and-1-initc2"}), @@ -221,6 +223,7 @@ func TestLogsForObject(t *testing.T) { }, opts: &corev1.PodLogOptions{}, allContainers: true, + allPods: false, actions: []testclient.Action{ getLogsAction("test", &corev1.PodLogOptions{Container: "foo-2-and-2-initc1"}), getLogsAction("test", &corev1.PodLogOptions{Container: "foo-2-and-2-initc2"}), @@ -385,7 +388,7 @@ func TestLogsForObject(t *testing.T) { for _, test := range tests { fakeClientset := fakeexternal.NewSimpleClientset(test.clientsetPods...) - responses, err := logsForObjectWithClient(fakeClientset.CoreV1(), test.obj, test.opts, 20*time.Second, test.allContainers) + responses, err := logsForObjectWithClient(fakeClientset.CoreV1(), test.obj, test.opts, 20*time.Second, test.allContainers, test.allPods) if test.expectedErr == "" && err != nil { t.Errorf("%s: unexpected error: %v", test.name, err) continue @@ -504,6 +507,7 @@ func TestLogsForObjectWithClient(t *testing.T) { podLogOptions *corev1.PodLogOptions expectedFieldPath string allContainers bool + allPods bool expectedError string }{ { @@ -552,6 +556,7 @@ func TestLogsForObjectWithClient(t *testing.T) { return pod }, allContainers: true, + allPods: false, podLogOptions: &corev1.PodLogOptions{}, expectedFieldPath: `spec.containers{foo-2-c2}`, }, @@ -561,7 +566,7 @@ func TestLogsForObjectWithClient(t *testing.T) { t.Run(tc.name, func(t *testing.T) { pod := tc.podFn() fakeClientset := fakeexternal.NewSimpleClientset(pod) - responses, err := logsForObjectWithClient(fakeClientset.CoreV1(), pod, tc.podLogOptions, 20*time.Second, tc.allContainers) + responses, err := logsForObjectWithClient(fakeClientset.CoreV1(), pod, tc.podLogOptions, 20*time.Second, tc.allContainers, tc.allPods) if err != nil { if len(tc.expectedError) > 0 { if err.Error() == tc.expectedError { diff --git a/test/e2e/kubectl/logs.go b/test/e2e/kubectl/logs.go index e54be29f367..31d432aae52 100644 --- a/test/e2e/kubectl/logs.go +++ b/test/e2e/kubectl/logs.go @@ -20,26 +20,74 @@ package kubectl import ( "context" + "fmt" "strconv" "strings" "time" + "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" + + appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/uuid" clientset "k8s.io/client-go/kubernetes" "k8s.io/kubectl/pkg/cmd/util/podcmd" "k8s.io/kubernetes/test/e2e/framework" + e2edeployment "k8s.io/kubernetes/test/e2e/framework/deployment" e2ekubectl "k8s.io/kubernetes/test/e2e/framework/kubectl" e2epod "k8s.io/kubernetes/test/e2e/framework/pod" e2eoutput "k8s.io/kubernetes/test/e2e/framework/pod/output" imageutils "k8s.io/kubernetes/test/utils/image" admissionapi "k8s.io/pod-security-admission/api" - - "github.com/onsi/ginkgo/v2" - "github.com/onsi/gomega" ) +func testingDeployment(name, ns string, numberOfPods int32) appsv1.Deployment { + return appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: ns, + Labels: map[string]string{ + "name": name, + }, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: &numberOfPods, + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "name": name, + }, + }, + Template: v1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "name": name, + }, + Annotations: map[string]string{ + podcmd.DefaultContainerAnnotationName: "container-2", + }, + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: "container-1", + Image: imageutils.GetE2EImage(imageutils.Agnhost), + Args: []string{"logs-generator", "--log-lines-total", "10", "--run-duration", "5s"}, + }, + { + Name: "container-2", + Image: imageutils.GetE2EImage(imageutils.Agnhost), + Args: []string{"logs-generator", "--log-lines-total", "20", "--run-duration", "5s"}, + }, + }, + RestartPolicy: v1.RestartPolicyAlways, + }, + }, + }, + } +} + func testingPod(name, value, defaultContainerName string) v1.Pod { return v1.Pod{ ObjectMeta: metav1.ObjectMeta{ @@ -93,7 +141,7 @@ var _ = SIGDescribe("Kubectl logs", func() { podName := "logs-generator" containerName := "logs-generator" ginkgo.BeforeEach(func() { - ginkgo.By("creating an pod") + ginkgo.By("creating a pod") // Agnhost image generates logs for a total of 100 lines over 20s. e2ekubectl.RunKubectlOrDie(ns, "run", podName, "--image="+imageutils.GetE2EImage(imageutils.Agnhost), "--restart=Never", podRunningTimeoutArg, "--", "logs-generator", "--log-lines-total", "100", "--run-duration", "20s") }) @@ -209,4 +257,100 @@ var _ = SIGDescribe("Kubectl logs", func() { }) }) + ginkgo.Describe("all pod logs", func() { + ginkgo.Describe("the Deployment has 2 replicas and each pod has 2 containers", func() { + var deploy *appsv1.Deployment + deployName := "deploy-" + string(uuid.NewUUID()) + numberReplicas := int32(2) + ginkgo.BeforeEach(func(ctx context.Context) { + deployClient := c.AppsV1().Deployments(ns) + ginkgo.By("constructing the Deployment") + deployCopy := testingDeployment(deployName, ns, numberReplicas) + deploy = &deployCopy + ginkgo.By("creating the Deployment") + var err error + deploy, err = deployClient.Create(ctx, deploy, metav1.CreateOptions{}) + if err != nil { + framework.Failf("Failed to create Deployment: %v", err) + } + + if err = e2edeployment.WaitForDeploymentComplete(c, deploy); err != nil { + framework.Failf("Failed to wait for Deployment to complete: %v", err) + } + + }) + + ginkgo.AfterEach(func() { + e2ekubectl.RunKubectlOrDie(ns, "delete", "deploy", deployName) + }) + + ginkgo.It("should get logs from all pods based on default container", func(ctx context.Context) { + ginkgo.By("Waiting for Deployment pods to be running.") + + // get the pod names + pods, err := e2edeployment.GetPodsForDeployment(ctx, c, deploy) + if err != nil { + framework.Failf("Failed to get pods for Deployment: %v", err) + } + + podOne := pods.Items[0].GetName() + podTwo := pods.Items[1].GetName() + + ginkgo.By("expecting logs from both replicas in Deployment") + out := e2ekubectl.RunKubectlOrDie(ns, "logs", fmt.Sprintf("deploy/%s", deployName), "--all-pods") + framework.Logf("got output %q", out) + logLines := strings.Split(out, "\n") + logFound := false + for _, line := range logLines { + var deployPod bool + if line != "" { + if strings.Contains(line, fmt.Sprintf("[pod/%s/container-2]", podOne)) || strings.Contains(line, fmt.Sprintf("[pod/%s/container-2]", podTwo)) { + logFound = true + deployPod = true + } + gomega.Expect(deployPod).To(gomega.BeTrueBecause("each log should be from the default container from each pod in the Deployment")) + } + + } + gomega.Expect(logFound).To(gomega.BeTrueBecause("log should be present")) + }) + + ginkgo.It("should get logs from each pod and each container in Deployment", func(ctx context.Context) { + ginkgo.By("Waiting for Deployment pods to be running.") + + pods, err := e2edeployment.GetPodsForDeployment(ctx, c, deploy) + if err != nil { + framework.Failf("Failed to get pods for Deployment: %v", err) + } + + podOne := pods.Items[0].GetName() + podTwo := pods.Items[1].GetName() + + ginkgo.By("all containers and all containers") + out := e2ekubectl.RunKubectlOrDie(ns, "logs", fmt.Sprintf("deploy/%s", deployName), "--all-pods", "--all-containers") + framework.Logf("got output %q", out) + logLines := strings.Split(out, "\n") + logFound := false + for _, line := range logLines { + if line != "" { + var deployPodContainer bool + if strings.Contains(line, fmt.Sprintf("[pod/%s/container-1]", podOne)) || strings.Contains(line, fmt.Sprintf("[pod/%s/container-1]", podTwo)) || strings.Contains(line, fmt.Sprintf("[pod/%s/container-2]", podOne)) || strings.Contains(line, fmt.Sprintf("[pod/%s/container-2]", podTwo)) { + logFound = true + deployPodContainer = true + } + gomega.Expect(deployPodContainer).To(gomega.BeTrueBecause("each log should be from all containers from all pods in the Deployment")) + } + } + gomega.Expect(logFound).To(gomega.BeTrueBecause("log should be present")) + gomega.Expect(strings.Contains(out, fmt.Sprintf("[pod/%s/container-1]", podOne))).To(gomega.BeTrueBecause("pod 1 container 1 log should be present")) + gomega.Expect(strings.Contains(out, fmt.Sprintf("[pod/%s/container-2]", podOne))).To(gomega.BeTrueBecause("pod 1 container 2 log should be present")) + gomega.Expect(strings.Contains(out, fmt.Sprintf("[pod/%s/container-1]", podTwo))).To(gomega.BeTrueBecause("pod 2 container 1 log should be present")) + gomega.Expect(strings.Contains(out, fmt.Sprintf("[pod/%s/container-2]", podTwo))).To(gomega.BeTrueBecause("pod 2 container 2 log should be present")) + gomega.Expect(out).NotTo(gomega.BeEmpty()) + + }) + + }) + }) + })