Merge pull request #136411 from olamilekan000/fix-cli-throwing-an-error-when-tailing-logs

fix cli throwing an error when trying to tail the logs for a Pod
This commit is contained in:
Kubernetes Prow Robot
2026-02-16 18:00:00 +05:30
committed by GitHub
2 changed files with 203 additions and 25 deletions

View File

@@ -32,6 +32,7 @@ import (
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/cli-runtime/pkg/genericiooptions"
"k8s.io/client-go/rest"
@@ -404,17 +405,11 @@ func (o LogsOptions) parallelConsumeRequest(ctx context.Context, requests map[co
go func(objRef corev1.ObjectReference, request rest.ResponseWrapper) {
defer wg.Done()
out := o.addPrefixIfNeeded(objRef, writer)
if err := o.ConsumeRequestFn(ctx, request, out); err != nil {
if !o.IgnoreLogErrors {
writer.CloseWithError(err)
// It's important to return here to propagate the error via the pipe
return
}
fmt.Fprintf(writer, "error: %v\n", err)
if err := o.consumeWithRetry(ctx, request, out, writer); err != nil {
writer.CloseWithError(err)
// It's important to return here to propagate the error via the pipe
return
}
}(objRef, request)
}
@@ -430,18 +425,45 @@ func (o LogsOptions) parallelConsumeRequest(ctx context.Context, requests map[co
func (o LogsOptions) sequentialConsumeRequest(ctx context.Context, requests map[corev1.ObjectReference]rest.ResponseWrapper) error {
for objRef, request := range requests {
out := o.addPrefixIfNeeded(objRef, o.Out)
if err := o.ConsumeRequestFn(ctx, request, out); err != nil {
if !o.IgnoreLogErrors {
return err
}
fmt.Fprintf(o.Out, "error: %v\n", err)
if err := o.consumeWithRetry(ctx, request, out, o.Out); err != nil {
return err
}
}
return nil
}
func (o LogsOptions) consumeWithRetry(ctx context.Context, request rest.ResponseWrapper, out io.Writer, internalErrOut io.Writer) error {
if !o.Follow {
err := o.ConsumeRequestFn(ctx, request, out)
if err != nil && o.IgnoreLogErrors {
_, _ = fmt.Fprint(internalErrOut, "error: "+err.Error()+"\n")
return nil
}
return err
}
return wait.PollUntilContextTimeout(ctx, 1*time.Second, o.GetPodTimeout, true, func(_ context.Context) (bool, error) {
err := o.ConsumeRequestFn(ctx, request, out)
if err == nil {
return true, nil
}
if o.IgnoreLogErrors {
_, _ = fmt.Fprint(internalErrOut, "error: "+err.Error()+"\n")
return true, nil
}
_, _ = o.ErrOut.Write([]byte(err.Error() + "\n"))
if _, ok := err.(apierrors.APIStatus); ok {
return false, nil
}
return false, err
})
}
func (o LogsOptions) addPrefixIfNeeded(ref corev1.ObjectReference, writer io.Writer) io.Writer {
if !o.Prefix || ref.FieldPath == "" || ref.Name == "" {
return writer

View File

@@ -64,6 +64,7 @@ func TestLog(t *testing.T) {
}
o := NewLogsOptions(streams)
o.GetPodTimeout = 5 * time.Millisecond
o.LogsForObject = mock.mockLogsForObject
o.ConsumeRequestFn = mock.mockConsumeRequest
@@ -85,6 +86,7 @@ func TestLog(t *testing.T) {
}
o := NewLogsOptions(streams)
o.GetPodTimeout = 5 * time.Millisecond
o.LogsForObject = mock.mockLogsForObject
o.ConsumeRequestFn = mock.mockConsumeRequest
o.Prefix = true
@@ -112,6 +114,7 @@ func TestLog(t *testing.T) {
}
o := NewLogsOptions(streams)
o.GetPodTimeout = 5 * time.Millisecond
o.LogsForObject = mock.mockLogsForObject
o.ConsumeRequestFn = mock.mockConsumeRequest
o.Prefix = true
@@ -136,6 +139,7 @@ func TestLog(t *testing.T) {
}
o := NewLogsOptions(streams)
o.GetPodTimeout = 5 * time.Millisecond
o.LogsForObject = mock.mockLogsForObject
o.ConsumeRequestFn = mock.mockConsumeRequest
o.Prefix = true
@@ -158,6 +162,7 @@ func TestLog(t *testing.T) {
}
o := NewLogsOptions(streams)
o.GetPodTimeout = 5 * time.Millisecond
o.LogsForObject = mock.mockLogsForObject
o.ConsumeRequestFn = mock.mockConsumeRequest
o.Prefix = true
@@ -190,6 +195,7 @@ func TestLog(t *testing.T) {
}
o := NewLogsOptions(streams)
o.GetPodTimeout = 5 * time.Millisecond
o.LogsForObject = mock.mockLogsForObject
o.ConsumeRequestFn = mock.mockConsumeRequest
return o
@@ -210,23 +216,36 @@ func TestLog(t *testing.T) {
Kind: "Pod",
Name: "some-pod-1",
FieldPath: "spec.containers{some-container-1}",
}: &responseWrapperMock{data: strings.NewReader("test log content from source 1\n")},
}: &statefulResponseWrapperMock{
results: []mockResult{
{data: strings.NewReader("test log content from source 1\n")},
},
},
{
Kind: "Pod",
Name: "some-pod-2",
FieldPath: "spec.containers{some-container-2}",
}: &responseWrapperMock{data: strings.NewReader("test log content from source 2\n")},
}: &statefulResponseWrapperMock{
results: []mockResult{
{data: strings.NewReader("test log content from source 2\n")},
},
},
{
Kind: "Pod",
Name: "some-pod-3",
FieldPath: "spec.containers{some-container-3}",
}: &responseWrapperMock{data: strings.NewReader("test log content from source 3\n")},
}: &statefulResponseWrapperMock{
results: []mockResult{
{data: strings.NewReader("test log content from source 3\n")},
},
},
},
wg: wg,
}
wg.Add(3)
o := NewLogsOptions(streams)
o.GetPodTimeout = 5 * time.Millisecond
o.LogsForObject = mock.mockLogsForObject
o.ConsumeRequestFn = mock.mockConsumeRequest
o.Follow = true
@@ -268,6 +287,7 @@ func TestLog(t *testing.T) {
o.LogsForObject = mock.mockLogsForObject
o.ConsumeRequestFn = mock.mockConsumeRequest
o.MaxFollowConcurrency = 2
o.GetPodTimeout = 5 * time.Millisecond
o.Follow = true
return o
},
@@ -277,6 +297,7 @@ func TestLog(t *testing.T) {
name: "fail if LogsForObject fails",
opts: func(streams genericiooptions.IOStreams) *LogsOptions {
o := NewLogsOptions(streams)
o.GetPodTimeout = 5 * time.Millisecond
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")
}
@@ -303,6 +324,7 @@ func TestLog(t *testing.T) {
}
o := NewLogsOptions(streams)
o.GetPodTimeout = 5 * time.Millisecond
o.LogsForObject = mock.mockLogsForObject
o.ConsumeRequestFn = func(ctx context.Context, req restclient.ResponseWrapper, out io.Writer) error {
return errors.New("Error from the ConsumeRequestFn")
@@ -321,17 +343,29 @@ func TestLog(t *testing.T) {
Kind: "Pod",
Name: "test-pod-1",
FieldPath: "spec.containers{test-container-1}",
}: &responseWrapperMock{data: strings.NewReader("test log content from source 1\n")},
}: &statefulResponseWrapperMock{
results: []mockResult{
{data: strings.NewReader("test log content from source 1\n")},
},
},
{
Kind: "Pod",
Name: "test-pod-2",
FieldPath: "spec.containers{test-container-2}",
}: &responseWrapperMock{data: strings.NewReader("test log content from source 2\n")},
}: &statefulResponseWrapperMock{
results: []mockResult{
{data: strings.NewReader("test log content from source 2\n")},
},
},
{
Kind: "Pod",
Name: "test-pod-3",
FieldPath: "spec.containers{test-container-3}",
}: &responseWrapperMock{data: strings.NewReader("test log content from source 3\n")},
}: &statefulResponseWrapperMock{
results: []mockResult{
{data: strings.NewReader("test log content from source 3\n")},
},
},
},
wg: wg,
}
@@ -340,6 +374,7 @@ func TestLog(t *testing.T) {
o := NewLogsOptions(streams)
o.LogsForObject = mock.mockLogsForObject
o.ConsumeRequestFn = mock.mockConsumeRequest
o.GetPodTimeout = 5 * time.Millisecond
o.Follow = true
o.Prefix = true
return o
@@ -360,17 +395,29 @@ func TestLog(t *testing.T) {
Kind: "Pod",
Name: "test-pod-1",
FieldPath: "spec.containers{test-container-1}",
}: &responseWrapperMock{},
}: &statefulResponseWrapperMock{
results: []mockResult{
{},
},
},
{
Kind: "Pod",
Name: "test-pod-2",
FieldPath: "spec.containers{test-container-2}",
}: &responseWrapperMock{},
}: &statefulResponseWrapperMock{
results: []mockResult{
{},
},
},
{
Kind: "Pod",
Name: "test-pod-3",
FieldPath: "spec.containers{test-container-3}",
}: &responseWrapperMock{},
}: &statefulResponseWrapperMock{
results: []mockResult{
{},
},
},
},
wg: wg,
}
@@ -382,6 +429,7 @@ func TestLog(t *testing.T) {
return errors.New("Error from the ConsumeRequestFn")
}
o.Follow = true
o.GetPodTimeout = 5 * time.Millisecond
return o
},
expectedErr: "Error from the ConsumeRequestFn",
@@ -405,6 +453,7 @@ func TestLog(t *testing.T) {
return errors.New("Error from the ConsumeRequestFn")
}
o.Follow = true
o.GetPodTimeout = 5 * time.Millisecond
return o
},
expectedErr: "Error from the ConsumeRequestFn",
@@ -433,6 +482,7 @@ func TestLog(t *testing.T) {
}
o := NewLogsOptions(streams)
o.GetPodTimeout = 5 * time.Millisecond
o.LogsForObject = mock.mockLogsForObject
o.ConsumeRequestFn = mock.mockConsumeRequest
o.IgnoreLogErrors = true
@@ -463,6 +513,7 @@ func TestLog(t *testing.T) {
}
o := NewLogsOptions(streams)
o.GetPodTimeout = 5 * time.Millisecond
o.LogsForObject = mock.mockLogsForObject
o.ConsumeRequestFn = mock.mockConsumeRequest
return o
@@ -494,6 +545,7 @@ func TestLog(t *testing.T) {
o := NewLogsOptions(streams)
o.LogsForObject = mock.mockLogsForObject
o.GetPodTimeout = 5 * time.Millisecond
o.ConsumeRequestFn = mock.mockConsumeRequest
o.IgnoreLogErrors = true
o.Follow = true
@@ -527,10 +579,73 @@ func TestLog(t *testing.T) {
o.LogsForObject = mock.mockLogsForObject
o.ConsumeRequestFn = mock.mockConsumeRequest
o.Follow = true
o.GetPodTimeout = 5 * time.Second
return o
},
expectedErr: "error-container",
},
{
name: "follow logs with retry on waiting container",
opts: func(streams genericiooptions.IOStreams) *LogsOptions {
mock := &logTestMock{
logsForObjectRequests: map[corev1.ObjectReference]restclient.ResponseWrapper{
{
Kind: "Pod",
Name: "some-pod",
FieldPath: "spec.containers{some-container}",
}: &statefulResponseWrapperMock{
results: []mockResult{
{
err: apierrors.NewBadRequest("container \"some-container\" in pod \"some-pod\" is waiting to start: ContainerCreating"),
},
{
data: strings.NewReader("test log content\n"),
},
},
},
},
}
o := NewLogsOptions(streams)
o.LogsForObject = mock.mockLogsForObject
o.ConsumeRequestFn = mock.mockConsumeRequest
o.Follow = true
o.GetPodTimeout = 5 * time.Second
return o
},
expectedOutSubstrings: []string{
"test log content\n",
},
},
{
name: "follow logs with retry and timeout",
opts: func(streams genericiooptions.IOStreams) *LogsOptions {
mock := &logTestMock{
logsForObjectRequests: map[corev1.ObjectReference]restclient.ResponseWrapper{
{
Kind: "Pod",
Name: "some-pod",
FieldPath: "spec.containers{some-container}",
}: &statefulResponseWrapperMock{
results: []mockResult{
{
err: apierrors.NewBadRequest("container \"some-container\" in pod \"some-pod\" is waiting to start: ContainerCreating"),
},
{
err: apierrors.NewBadRequest("container \"some-container\" in pod \"some-pod\" is waiting to start: ContainerCreating"),
},
},
},
},
}
o := NewLogsOptions(streams)
o.LogsForObject = mock.mockLogsForObject
o.ConsumeRequestFn = mock.mockConsumeRequest
o.Follow = true
o.GetPodTimeout = 1 * time.Millisecond
return o
},
expectedErr: "context deadline exceeded",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
@@ -538,6 +653,7 @@ func TestLog(t *testing.T) {
defer tf.Cleanup()
streams, _, buf, _ := genericiooptions.NewTestIOStreams()
streams.ErrOut = &threadSafeWriter{writer: streams.ErrOut}
opts := test.opts(streams)
opts.Namespace = "test"
@@ -968,3 +1084,43 @@ func (l *logTestMock) mockLogsForObject(restClientGetter genericclioptions.RESTC
return nil, fmt.Errorf("cannot get the logs from %T", object)
}
}
type mockResult struct {
err error
data io.Reader
}
type statefulResponseWrapperMock struct {
lock sync.Mutex
calls int
results []mockResult
}
func (r *statefulResponseWrapperMock) DoRaw(context.Context) ([]byte, error) {
return nil, nil
}
func (r *statefulResponseWrapperMock) Stream(context.Context) (io.ReadCloser, error) {
r.lock.Lock()
defer r.lock.Unlock()
if r.calls >= len(r.results) {
return nil, fmt.Errorf("unexpected call %d", r.calls)
}
res := r.results[r.calls]
r.calls++
if res.err != nil {
return nil, res.err
}
return io.NopCloser(res.data), nil
}
type threadSafeWriter struct {
mu sync.Mutex
writer io.Writer
}
func (tsw *threadSafeWriter) Write(p []byte) (n int, err error) {
tsw.mu.Lock()
defer tsw.mu.Unlock()
return tsw.writer.Write(p)
}