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 }