k8s.io/client-go@v0.22.2/tools/remotecommand/v1.go (about)

     1  /*
     2  Copyright 2015 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package remotecommand
    18  
    19  import (
    20  	"fmt"
    21  	"io"
    22  	"io/ioutil"
    23  	"net/http"
    24  
    25  	"k8s.io/api/core/v1"
    26  	"k8s.io/apimachinery/pkg/util/httpstream"
    27  	"k8s.io/klog/v2"
    28  )
    29  
    30  // streamProtocolV1 implements the first version of the streaming exec & attach
    31  // protocol. This version has some bugs, such as not being able to detect when
    32  // non-interactive stdin data has ended. See http://issues.k8s.io/13394 and
    33  // http://issues.k8s.io/13395 for more details.
    34  type streamProtocolV1 struct {
    35  	StreamOptions
    36  
    37  	errorStream  httpstream.Stream
    38  	remoteStdin  httpstream.Stream
    39  	remoteStdout httpstream.Stream
    40  	remoteStderr httpstream.Stream
    41  }
    42  
    43  var _ streamProtocolHandler = &streamProtocolV1{}
    44  
    45  func newStreamProtocolV1(options StreamOptions) streamProtocolHandler {
    46  	return &streamProtocolV1{
    47  		StreamOptions: options,
    48  	}
    49  }
    50  
    51  func (p *streamProtocolV1) stream(conn streamCreator) error {
    52  	doneChan := make(chan struct{}, 2)
    53  	errorChan := make(chan error)
    54  
    55  	cp := func(s string, dst io.Writer, src io.Reader) {
    56  		klog.V(6).Infof("Copying %s", s)
    57  		defer klog.V(6).Infof("Done copying %s", s)
    58  		if _, err := io.Copy(dst, src); err != nil && err != io.EOF {
    59  			klog.Errorf("Error copying %s: %v", s, err)
    60  		}
    61  		if s == v1.StreamTypeStdout || s == v1.StreamTypeStderr {
    62  			doneChan <- struct{}{}
    63  		}
    64  	}
    65  
    66  	// set up all the streams first
    67  	var err error
    68  	headers := http.Header{}
    69  	headers.Set(v1.StreamType, v1.StreamTypeError)
    70  	p.errorStream, err = conn.CreateStream(headers)
    71  	if err != nil {
    72  		return err
    73  	}
    74  	defer p.errorStream.Reset()
    75  
    76  	// Create all the streams first, then start the copy goroutines. The server doesn't start its copy
    77  	// goroutines until it's received all of the streams. If the client creates the stdin stream and
    78  	// immediately begins copying stdin data to the server, it's possible to overwhelm and wedge the
    79  	// spdy frame handler in the server so that it is full of unprocessed frames. The frames aren't
    80  	// getting processed because the server hasn't started its copying, and it won't do that until it
    81  	// gets all the streams. By creating all the streams first, we ensure that the server is ready to
    82  	// process data before the client starts sending any. See https://issues.k8s.io/16373 for more info.
    83  	if p.Stdin != nil {
    84  		headers.Set(v1.StreamType, v1.StreamTypeStdin)
    85  		p.remoteStdin, err = conn.CreateStream(headers)
    86  		if err != nil {
    87  			return err
    88  		}
    89  		defer p.remoteStdin.Reset()
    90  	}
    91  
    92  	if p.Stdout != nil {
    93  		headers.Set(v1.StreamType, v1.StreamTypeStdout)
    94  		p.remoteStdout, err = conn.CreateStream(headers)
    95  		if err != nil {
    96  			return err
    97  		}
    98  		defer p.remoteStdout.Reset()
    99  	}
   100  
   101  	if p.Stderr != nil && !p.Tty {
   102  		headers.Set(v1.StreamType, v1.StreamTypeStderr)
   103  		p.remoteStderr, err = conn.CreateStream(headers)
   104  		if err != nil {
   105  			return err
   106  		}
   107  		defer p.remoteStderr.Reset()
   108  	}
   109  
   110  	// now that all the streams have been created, proceed with reading & copying
   111  
   112  	// always read from errorStream
   113  	go func() {
   114  		message, err := ioutil.ReadAll(p.errorStream)
   115  		if err != nil && err != io.EOF {
   116  			errorChan <- fmt.Errorf("Error reading from error stream: %s", err)
   117  			return
   118  		}
   119  		if len(message) > 0 {
   120  			errorChan <- fmt.Errorf("Error executing remote command: %s", message)
   121  			return
   122  		}
   123  	}()
   124  
   125  	if p.Stdin != nil {
   126  		// TODO this goroutine will never exit cleanly (the io.Copy never unblocks)
   127  		// because stdin is not closed until the process exits. If we try to call
   128  		// stdin.Close(), it returns no error but doesn't unblock the copy. It will
   129  		// exit when the process exits, instead.
   130  		go cp(v1.StreamTypeStdin, p.remoteStdin, readerWrapper{p.Stdin})
   131  	}
   132  
   133  	waitCount := 0
   134  	completedStreams := 0
   135  
   136  	if p.Stdout != nil {
   137  		waitCount++
   138  		go cp(v1.StreamTypeStdout, p.Stdout, p.remoteStdout)
   139  	}
   140  
   141  	if p.Stderr != nil && !p.Tty {
   142  		waitCount++
   143  		go cp(v1.StreamTypeStderr, p.Stderr, p.remoteStderr)
   144  	}
   145  
   146  Loop:
   147  	for {
   148  		select {
   149  		case <-doneChan:
   150  			completedStreams++
   151  			if completedStreams == waitCount {
   152  				break Loop
   153  			}
   154  		case err := <-errorChan:
   155  			return err
   156  		}
   157  	}
   158  
   159  	return nil
   160  }