Skip to content

Commit 439f93c

Browse files
committed
kubectl: allow to preselect interesting container in logs
1 parent d52ecd5 commit 439f93c

File tree

2 files changed

+115
-0
lines changed

2 files changed

+115
-0
lines changed

staging/src/k8s.io/kubectl/pkg/polymorphichelpers/logsforobject.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ import (
3434
"k8s.io/kubectl/pkg/util/podutils"
3535
)
3636

37+
// defaultLogsContainerAnnotationName is an annotation name that can be used to preselect the interesting container
38+
// from a pod when running kubectl logs.
39+
const defaultLogsContainerAnnotationName = "kubectl.kubernetes.io/default-logs-container"
40+
3741
func logsForObject(restClientGetter genericclioptions.RESTClientGetter, object, options runtime.Object, timeout time.Duration, allContainers bool) (map[corev1.ObjectReference]rest.ResponseWrapper, error) {
3842
clientConfig, err := restClientGetter.ToRESTConfig()
3943
if err != nil {
@@ -69,6 +73,16 @@ func logsForObjectWithClient(clientset corev1client.CoreV1Interface, object, opt
6973
return ret, nil
7074

7175
case *corev1.Pod:
76+
// in case the "kubectl.kubernetes.io/default-logs-container" annotation is present, we preset the opts.Containers to default to selected
77+
// container. This gives users ability to preselect the most interesting container in pod.
78+
if annotations := t.GetAnnotations(); annotations != nil && len(opts.Container) == 0 && len(annotations[defaultLogsContainerAnnotationName]) > 0 {
79+
containerName := annotations[defaultLogsContainerAnnotationName]
80+
if exists, _ := findContainerByName(t, containerName); exists != nil {
81+
opts.Container = containerName
82+
} else {
83+
fmt.Fprintf(os.Stderr, "Default container name %q not found in a pod\n", containerName)
84+
}
85+
}
7286
// if allContainers is true, then we're going to locate all containers and then iterate through them. At that point, "allContainers" is false
7387
if !allContainers {
7488
var containerName string

staging/src/k8s.io/kubectl/pkg/polymorphichelpers/logsforobject_test.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,107 @@ func testPodWithTwoContainersAndTwoInitContainers() *corev1.Pod {
410410
}
411411
}
412412

413+
func TestLogsForObjectWithClient(t *testing.T) {
414+
cases := []struct {
415+
name string
416+
podFn func() *corev1.Pod
417+
podLogOptions *corev1.PodLogOptions
418+
expectedFieldPath string
419+
allContainers bool
420+
expectedError string
421+
}{
422+
{
423+
name: "two container pod without default container selected",
424+
podFn: testPodWithTwoContainers,
425+
podLogOptions: &corev1.PodLogOptions{},
426+
expectedError: `a container name must be specified for pod foo-two-containers, choose one of: [foo-2-c1 foo-2-c2]`,
427+
},
428+
{
429+
name: "two container pod with default container selected",
430+
podFn: func() *corev1.Pod {
431+
pod := testPodWithTwoContainers()
432+
pod.Annotations = map[string]string{defaultLogsContainerAnnotationName: "foo-2-c1"}
433+
return pod
434+
},
435+
podLogOptions: &corev1.PodLogOptions{},
436+
expectedFieldPath: `spec.containers{foo-2-c1}`,
437+
},
438+
{
439+
name: "two container pod with default container selected but also container set explicitly",
440+
podFn: func() *corev1.Pod {
441+
pod := testPodWithTwoContainers()
442+
pod.Annotations = map[string]string{defaultLogsContainerAnnotationName: "foo-2-c1"}
443+
return pod
444+
},
445+
podLogOptions: &corev1.PodLogOptions{
446+
Container: "foo-2-c2",
447+
},
448+
expectedFieldPath: `spec.containers{foo-2-c2}`,
449+
},
450+
{
451+
name: "two container pod with non-existing default container selected",
452+
podFn: func() *corev1.Pod {
453+
pod := testPodWithTwoContainers()
454+
pod.Annotations = map[string]string{defaultLogsContainerAnnotationName: "non-existing"}
455+
return pod
456+
},
457+
podLogOptions: &corev1.PodLogOptions{},
458+
expectedError: `a container name must be specified for pod foo-two-containers, choose one of: [foo-2-c1 foo-2-c2]`,
459+
},
460+
{
461+
name: "two container pod with default container set, but allContainers also set",
462+
podFn: func() *corev1.Pod {
463+
pod := testPodWithTwoContainers()
464+
pod.Annotations = map[string]string{defaultLogsContainerAnnotationName: "foo-2-c1"}
465+
return pod
466+
},
467+
allContainers: true,
468+
podLogOptions: &corev1.PodLogOptions{},
469+
expectedFieldPath: `spec.containers{foo-2-c2}`,
470+
},
471+
}
472+
473+
for _, tc := range cases {
474+
t.Run(tc.name, func(t *testing.T) {
475+
pod := tc.podFn()
476+
fakeClientset := fakeexternal.NewSimpleClientset(pod)
477+
responses, err := logsForObjectWithClient(fakeClientset.CoreV1(), pod, tc.podLogOptions, 20*time.Second, tc.allContainers)
478+
if err != nil {
479+
if len(tc.expectedError) > 0 {
480+
if err.Error() == tc.expectedError {
481+
return
482+
}
483+
}
484+
t.Errorf("unexpected error: %v", err)
485+
return
486+
}
487+
if len(tc.expectedError) > 0 {
488+
t.Errorf("expected error %q, got none", tc.expectedError)
489+
return
490+
}
491+
if !tc.allContainers && len(responses) != 1 {
492+
t.Errorf("expected one response, got %d", len(responses))
493+
return
494+
}
495+
if tc.allContainers && len(responses) != 2 {
496+
t.Errorf("expected 2 responses for allContainers, got %d", len(responses))
497+
return
498+
}
499+
// do not check actual responses in this case as we know there are at least two, which means the preselected
500+
// container was not used (which is desired).
501+
if tc.allContainers {
502+
return
503+
}
504+
for r := range responses {
505+
if r.FieldPath != tc.expectedFieldPath {
506+
t.Errorf("expected %q container to be preselected, got %q", tc.expectedFieldPath, r.FieldPath)
507+
}
508+
}
509+
})
510+
}
511+
512+
}
513+
413514
func testPodWithTwoContainersAndTwoInitAndOneEphemeralContainers() *corev1.Pod {
414515
return &corev1.Pod{
415516
TypeMeta: metav1.TypeMeta{

0 commit comments

Comments
 (0)