client-go: transform watchErrorStream to wrap the underlying error (#129765)

* `client-go`: transform `watchErrorStream` to wrap the underlying error

This PR transforms the `client-go`'s `watchErrorStream` to wrap the error instead of transforming it into a single string. This enables clients to use `errors.Is/As/Unwrap` with the errors that come out of `StreamWithContext`

Fixes https://github.com/kubernetes/kubernetes/issues/129763

* adjust unit tests

Kubernetes-commit: 067012f5844b7390e7279f575342ae0536f80520
This commit is contained in:
Tiago Silva 2025-01-23 20:07:37 +00:00 committed by Kubernetes Publisher
parent 3b09c13448
commit 9f1cce41d5
2 changed files with 29 additions and 11 deletions

View File

@ -41,7 +41,7 @@ func watchErrorStream(errorStream io.Reader, d errorStreamDecoder) chan error {
message, err := io.ReadAll(errorStream) message, err := io.ReadAll(errorStream)
switch { switch {
case err != nil && err != io.EOF: case err != nil && err != io.EOF:
errorChan <- fmt.Errorf("error reading from error stream: %s", err) errorChan <- fmt.Errorf("error reading from error stream: %w", err)
case len(message) > 0: case len(message) > 0:
errorChan <- d.decode(message) errorChan <- d.decode(message)
default: default:

View File

@ -19,12 +19,13 @@ package remotecommand
import ( import (
"errors" "errors"
"io" "io"
"net"
"net/http" "net/http"
"strings" "strings"
"testing" "testing"
"time" "time"
"k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/httpstream" "k8s.io/apimachinery/pkg/util/httpstream"
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
) )
@ -181,17 +182,34 @@ func TestV2ErrorStreamReading(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
stream io.Reader stream io.Reader
expectedError error expectedError func(*testing.T, error)
}{ }{
{ {
name: "error reading from stream", name: "error reading from stream",
stream: &fakeReader{errors.New("foo")}, stream: &fakeReader{errors.New("foo")},
expectedError: errors.New("error reading from error stream: foo"), expectedError: func(t *testing.T, err error) {
if e, a := "error reading from error stream: foo", err.Error(); e != a {
t.Errorf("expected '%s', got '%s'", e, a)
}
},
}, },
{ {
name: "stream returns an error", name: "stream returns an error",
stream: strings.NewReader("some error"), stream: strings.NewReader("some error"),
expectedError: errors.New("error executing remote command: some error"), expectedError: func(t *testing.T, err error) {
if e, a := "error executing remote command: some error", err.Error(); e != a {
t.Errorf("expected '%s', got '%s'", e, a)
}
},
},
{
name: "typed error",
stream: &fakeReader{net.ErrClosed},
expectedError: func(t *testing.T, err error) {
if !errors.Is(err, net.ErrClosed) {
t.Errorf("expected errors.Is(err, net.ErrClosed), failed on %#v", err)
}
},
}, },
} }
@ -214,8 +232,8 @@ func TestV2ErrorStreamReading(t *testing.T) {
if test.expectedError != nil { if test.expectedError != nil {
if err == nil { if err == nil {
t.Errorf("%s: expected an error", test.name) t.Errorf("%s: expected an error", test.name)
} else if e, a := test.expectedError, err; e.Error() != a.Error() { } else {
t.Errorf("%s: expected %q, got %q", test.name, e, a) test.expectedError(t, err)
} }
continue continue
} }