@@ -9,12 +9,15 @@ import (
99 "time"
1010
1111 "github.com/docker/buildx/driver/kubernetes/kubeclient"
12+ "github.com/pkg/errors"
1213 "github.com/sirupsen/logrus"
1314 corev1 "k8s.io/api/core/v1"
1415 "k8s.io/client-go/rest"
1516 "k8s.io/client-go/tools/remotecommand"
1617)
1718
19+ var errStreamEndedBeforeReady = errors .New ("exec stream ended before the connection was established" )
20+
1821func ExecConn (ctx context.Context , restClient rest.Interface , restConfig * rest.Config , namespace , pod , container string , cmd []string ) (net.Conn , error ) {
1922 req := restClient .
2023 Post ().
@@ -34,12 +37,19 @@ func ExecConn(ctx context.Context, restClient rest.Interface, restConfig *rest.C
3437 if err != nil {
3538 return nil , err
3639 }
37- return newExecConn (ctx , exec ), nil
40+ return newExecConn (ctx , exec )
3841}
3942
4043// newExecConn wires a remotecommand.Executor's stdin/stdout streams up as a net.Conn.
4144// It is split from ExecConn to ease testing.
42- func newExecConn (ctx context.Context , exec remotecommand.Executor ) net.Conn {
45+ //
46+ // StreamWithContext sets up the exec stream synchronously and then blocks for its
47+ // whole lifetime, so it runs in a goroutine. newExecConn waits for the stream to be
48+ // established before returning, otherwise a failed setup (e.g. "tls: internal error"
49+ // on a not-yet-ready node) would only show up on the first Read/Write, after Dial has
50+ // already reported success and past its retry. The executor reads stdin only once the
51+ // stream is established, so the first stdin read is used as the readiness signal.
52+ func newExecConn (ctx context.Context , exec remotecommand.Executor ) (net.Conn , error ) {
4353 stdinR , stdinW := io .Pipe ()
4454 stdoutR , stdoutW := io .Pipe ()
4555 kc := & kubeConn {
@@ -48,9 +58,14 @@ func newExecConn(ctx context.Context, exec remotecommand.Executor) net.Conn {
4858 localAddr : dummyAddr {network : "dummy" , s : "dummy-0" },
4959 remoteAddr : dummyAddr {network : "dummy" , s : "dummy-1" },
5060 }
61+
62+ ready := make (chan struct {})
63+ stdin := & readyReader {r : stdinR , ready : ready }
64+
65+ streamErr := make (chan error , 1 )
5166 go func () {
5267 serr := exec .StreamWithContext (ctx , remotecommand.StreamOptions {
53- Stdin : stdinR ,
68+ Stdin : stdin ,
5469 Stdout : stdoutW ,
5570 Stderr : os .Stderr ,
5671 Tty : false ,
@@ -61,8 +76,35 @@ func newExecConn(ctx context.Context, exec remotecommand.Executor) net.Conn {
6176 // Ensure the pipes are closed to unblock Read/Write on kubeConn and avoid infinite hangs.
6277 stdoutW .CloseWithError (serr )
6378 stdinR .CloseWithError (serr )
79+ streamErr <- serr
6480 }()
65- return kc
81+
82+ select {
83+ case <- ready :
84+ return kc , nil
85+ case serr := <- streamErr :
86+ _ = kc .Close ()
87+ if serr == nil {
88+ serr = errStreamEndedBeforeReady
89+ }
90+ return nil , serr
91+ case <- ctx .Done ():
92+ _ = kc .Close ()
93+ return nil , context .Cause (ctx )
94+ }
95+ }
96+
97+ // readyReader closes ready on the first Read. The exec stream reads stdin only once its
98+ // streams have been created, so the first read marks the connection as established.
99+ type readyReader struct {
100+ r io.Reader
101+ once sync.Once
102+ ready chan struct {}
103+ }
104+
105+ func (r * readyReader ) Read (p []byte ) (int , error ) {
106+ r .once .Do (func () { close (r .ready ) })
107+ return r .r .Read (p )
66108}
67109
68110type kubeConn struct {
0 commit comments