driver/kubernetes: close exec-stream pipes when the stream ends
Signed-off-by: Pierre Gimalac <23154723+pgimalac@users.noreply.github.com>
This commit is contained in:
@@ -34,6 +34,12 @@ func ExecConn(ctx context.Context, restClient rest.Interface, restConfig *rest.C
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return newExecConn(ctx, exec), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// newExecConn wires a remotecommand.Executor's stdin/stdout streams up as a net.Conn.
|
||||||
|
// It is split from ExecConn to ease testing.
|
||||||
|
func newExecConn(ctx context.Context, exec remotecommand.Executor) net.Conn {
|
||||||
stdinR, stdinW := io.Pipe()
|
stdinR, stdinW := io.Pipe()
|
||||||
stdoutR, stdoutW := io.Pipe()
|
stdoutR, stdoutW := io.Pipe()
|
||||||
kc := &kubeConn{
|
kc := &kubeConn{
|
||||||
@@ -52,8 +58,11 @@ func ExecConn(ctx context.Context, restClient rest.Interface, restConfig *rest.C
|
|||||||
if serr != nil && serr != context.Canceled {
|
if serr != nil && serr != context.Canceled {
|
||||||
logrus.Error(serr)
|
logrus.Error(serr)
|
||||||
}
|
}
|
||||||
|
// Ensure the pipes are closed to unblock Read/Write on kubeConn and avoid infinite hangs.
|
||||||
|
stdoutW.CloseWithError(serr)
|
||||||
|
stdinR.CloseWithError(serr)
|
||||||
}()
|
}()
|
||||||
return kc, nil
|
return kc
|
||||||
}
|
}
|
||||||
|
|
||||||
type kubeConn struct {
|
type kubeConn struct {
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
package execconn
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"k8s.io/client-go/tools/remotecommand"
|
||||||
|
)
|
||||||
|
|
||||||
|
// fakeExecutor is a fake remotecommand.Executor whose StreamWithContext blocks until
|
||||||
|
// unblock is closed, then returns err. It stands in for the real SPDY exec stream
|
||||||
|
// to a builder pod, so the pipe-closing behavior in newExecConn can be tested
|
||||||
|
// without a real Kubernetes API server.
|
||||||
|
type fakeExecutor struct {
|
||||||
|
unblock chan struct{}
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeExecutor) Stream(_ remotecommand.StreamOptions) error {
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeExecutor) StreamWithContext(ctx context.Context, _ remotecommand.StreamOptions) error {
|
||||||
|
select {
|
||||||
|
case <-f.unblock:
|
||||||
|
return f.err
|
||||||
|
case <-ctx.Done():
|
||||||
|
return context.Cause(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewExecConnPropagatesStreamEnd(t *testing.T) {
|
||||||
|
t.Run("stream ends with an error", func(t *testing.T) {
|
||||||
|
streamErr := errors.New("exec stream terminated")
|
||||||
|
fe := &fakeExecutor{
|
||||||
|
unblock: make(chan struct{}),
|
||||||
|
err: streamErr,
|
||||||
|
}
|
||||||
|
|
||||||
|
conn := newExecConn(context.Background(), fe)
|
||||||
|
close(fe.unblock)
|
||||||
|
|
||||||
|
_, err := conn.Read(make([]byte, 16))
|
||||||
|
require.ErrorIs(t, err, streamErr)
|
||||||
|
|
||||||
|
_, err = conn.Write([]byte("test"))
|
||||||
|
require.ErrorIs(t, err, streamErr)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("stream ends with no error", func(t *testing.T) {
|
||||||
|
fe := &fakeExecutor{unblock: make(chan struct{})}
|
||||||
|
|
||||||
|
conn := newExecConn(context.Background(), fe)
|
||||||
|
close(fe.unblock) // StreamWithContext returns nil
|
||||||
|
|
||||||
|
_, err := conn.Read(make([]byte, 16))
|
||||||
|
require.ErrorIs(t, err, io.EOF)
|
||||||
|
|
||||||
|
_, err = conn.Write([]byte("test"))
|
||||||
|
require.ErrorIs(t, err, io.ErrClosedPipe)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("stream still active: reads stay blocked, not closed early", func(t *testing.T) {
|
||||||
|
fe := &fakeExecutor{unblock: make(chan struct{})}
|
||||||
|
conn := newExecConn(context.Background(), fe)
|
||||||
|
defer close(fe.unblock)
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
buf := make([]byte, 16)
|
||||||
|
_, _ = conn.Read(buf) //nolint:errcheck
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
t.Fatal("Read returned before the exec stream ended; it should still be blocked")
|
||||||
|
case <-time.After(200 * time.Millisecond):
|
||||||
|
// expected: still blocked, exactly like a real in-progress build
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("stream still active: writes stay blocked, not closed early", func(t *testing.T) {
|
||||||
|
fe := &fakeExecutor{unblock: make(chan struct{})}
|
||||||
|
conn := newExecConn(context.Background(), fe)
|
||||||
|
defer close(fe.unblock)
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
_, _ = conn.Write([]byte("test")) //nolint:errcheck
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
t.Fatal("Write returned before the exec stream ended; it should still be blocked")
|
||||||
|
case <-time.After(200 * time.Millisecond):
|
||||||
|
// expected: still blocked, exactly like a real in-progress build
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("stream cancelled by context", func(t *testing.T) {
|
||||||
|
fe := &fakeExecutor{unblock: make(chan struct{})}
|
||||||
|
ctx, cancel := context.WithCancelCause(context.Background())
|
||||||
|
conn := newExecConn(ctx, fe)
|
||||||
|
defer close(fe.unblock)
|
||||||
|
|
||||||
|
cancel(context.Canceled)
|
||||||
|
|
||||||
|
_, err := conn.Read(make([]byte, 16))
|
||||||
|
require.ErrorIs(t, err, context.Canceled)
|
||||||
|
|
||||||
|
_, err = conn.Write([]byte("test"))
|
||||||
|
require.ErrorIs(t, err, context.Canceled)
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user