diff --git a/pkg/polymorphichelpers/logsforobject.go b/pkg/polymorphichelpers/logsforobject.go index 4e55dc26..b45243ba 100644 --- a/pkg/polymorphichelpers/logsforobject.go +++ b/pkg/polymorphichelpers/logsforobject.go @@ -34,6 +34,10 @@ import ( "k8s.io/kubectl/pkg/util/podutils" ) +// defaultLogsContainerAnnotationName is an annotation name that can be used to preselect the interesting container +// from a pod when running kubectl logs. +const defaultLogsContainerAnnotationName = "kubectl.kubernetes.io/default-logs-container" + 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 { @@ -69,6 +73,16 @@ func logsForObjectWithClient(clientset corev1client.CoreV1Interface, object, opt return ret, nil case *corev1.Pod: + // in case the "kubectl.kubernetes.io/default-logs-container" annotation is present, we preset the opts.Containers to default to selected + // container. This gives users ability to preselect the most interesting container in pod. + if annotations := t.GetAnnotations(); annotations != nil && len(opts.Container) == 0 && len(annotations[defaultLogsContainerAnnotationName]) > 0 { + containerName := annotations[defaultLogsContainerAnnotationName] + if exists, _ := findContainerByName(t, containerName); exists != nil { + opts.Container = containerName + } else { + fmt.Fprintf(os.Stderr, "Default container name %q not found in a pod\n", containerName) + } + } // if allContainers is true, then we're going to locate all containers and then iterate through them. At that point, "allContainers" is false if !allContainers { var containerName string diff --git a/pkg/polymorphichelpers/logsforobject_test.go b/pkg/polymorphichelpers/logsforobject_test.go index 3ac89ee9..f8906941 100644 --- a/pkg/polymorphichelpers/logsforobject_test.go +++ b/pkg/polymorphichelpers/logsforobject_test.go @@ -410,6 +410,107 @@ func testPodWithTwoContainersAndTwoInitContainers() *corev1.Pod { } } +func TestLogsForObjectWithClient(t *testing.T) { + cases := []struct { + name string + podFn func() *corev1.Pod + podLogOptions *corev1.PodLogOptions + expectedFieldPath string + allContainers bool + expectedError string + }{ + { + name: "two container pod without default container selected", + podFn: testPodWithTwoContainers, + podLogOptions: &corev1.PodLogOptions{}, + expectedError: `a container name must be specified for pod foo-two-containers, choose one of: [foo-2-c1 foo-2-c2]`, + }, + { + name: "two container pod with default container selected", + podFn: func() *corev1.Pod { + pod := testPodWithTwoContainers() + pod.Annotations = map[string]string{defaultLogsContainerAnnotationName: "foo-2-c1"} + return pod + }, + podLogOptions: &corev1.PodLogOptions{}, + expectedFieldPath: `spec.containers{foo-2-c1}`, + }, + { + name: "two container pod with default container selected but also container set explicitly", + podFn: func() *corev1.Pod { + pod := testPodWithTwoContainers() + pod.Annotations = map[string]string{defaultLogsContainerAnnotationName: "foo-2-c1"} + return pod + }, + podLogOptions: &corev1.PodLogOptions{ + Container: "foo-2-c2", + }, + expectedFieldPath: `spec.containers{foo-2-c2}`, + }, + { + name: "two container pod with non-existing default container selected", + podFn: func() *corev1.Pod { + pod := testPodWithTwoContainers() + pod.Annotations = map[string]string{defaultLogsContainerAnnotationName: "non-existing"} + return pod + }, + podLogOptions: &corev1.PodLogOptions{}, + expectedError: `a container name must be specified for pod foo-two-containers, choose one of: [foo-2-c1 foo-2-c2]`, + }, + { + name: "two container pod with default container set, but allContainers also set", + podFn: func() *corev1.Pod { + pod := testPodWithTwoContainers() + pod.Annotations = map[string]string{defaultLogsContainerAnnotationName: "foo-2-c1"} + return pod + }, + allContainers: true, + podLogOptions: &corev1.PodLogOptions{}, + expectedFieldPath: `spec.containers{foo-2-c2}`, + }, + } + + for _, tc := range cases { + 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) + if err != nil { + if len(tc.expectedError) > 0 { + if err.Error() == tc.expectedError { + return + } + } + t.Errorf("unexpected error: %v", err) + return + } + if len(tc.expectedError) > 0 { + t.Errorf("expected error %q, got none", tc.expectedError) + return + } + if !tc.allContainers && len(responses) != 1 { + t.Errorf("expected one response, got %d", len(responses)) + return + } + if tc.allContainers && len(responses) != 2 { + t.Errorf("expected 2 responses for allContainers, got %d", len(responses)) + return + } + // do not check actual responses in this case as we know there are at least two, which means the preselected + // container was not used (which is desired). + if tc.allContainers { + return + } + for r := range responses { + if r.FieldPath != tc.expectedFieldPath { + t.Errorf("expected %q container to be preselected, got %q", tc.expectedFieldPath, r.FieldPath) + } + } + }) + } + +} + func testPodWithTwoContainersAndTwoInitAndOneEphemeralContainers() *corev1.Pod { return &corev1.Pod{ TypeMeta: metav1.TypeMeta{