feat: add all-pods log flag to kubectl

Signed-off-by: Case Wylie <cmwylie19@defenseunicorns.com>
This commit is contained in:
Case Wylie 2024-05-07 19:43:46 -04:00
parent 1308e661e0
commit 6db859eb5d
No known key found for this signature in database
GPG Key ID: 6B10B2DED4F87BBD
8 changed files with 398 additions and 59 deletions

View File

@ -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
}

View File

@ -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 {

View File

@ -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

View File

@ -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 {

View File

@ -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)

View File

@ -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)
}

View File

@ -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 {

View File

@ -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())
})
})
})
})