k8s.io/client-go@v0.22.2/tools/remotecommand/remotecommand_test.go (about) 1 /* 2 Copyright 2020 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 "encoding/json" 21 "errors" 22 "io" 23 "io/ioutil" 24 v1 "k8s.io/api/core/v1" 25 apierrors "k8s.io/apimachinery/pkg/api/errors" 26 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 "k8s.io/apimachinery/pkg/util/httpstream" 28 "k8s.io/apimachinery/pkg/util/httpstream/spdy" 29 remotecommandconsts "k8s.io/apimachinery/pkg/util/remotecommand" 30 "k8s.io/apimachinery/pkg/util/wait" 31 "k8s.io/client-go/rest" 32 "net/http" 33 "net/http/httptest" 34 "net/url" 35 "strings" 36 "testing" 37 "time" 38 ) 39 40 type AttachFunc func(in io.Reader, out, err io.WriteCloser, tty bool, resize <-chan TerminalSize) error 41 type streamContext struct { 42 conn io.Closer 43 stdinStream io.ReadCloser 44 stdoutStream io.WriteCloser 45 stderrStream io.WriteCloser 46 writeStatus func(status *apierrors.StatusError) error 47 } 48 49 type streamAndReply struct { 50 httpstream.Stream 51 replySent <-chan struct{} 52 } 53 54 type fakeMassiveDataPty struct{} 55 56 func (s *fakeMassiveDataPty) Read(p []byte) (int, error) { 57 time.Sleep(time.Duration(1) * time.Second) 58 return copy(p, []byte{}), errors.New("client crashed after 1 second") 59 } 60 61 func (s *fakeMassiveDataPty) Write(p []byte) (int, error) { 62 time.Sleep(time.Duration(1) * time.Second) 63 return len(p), errors.New("return err") 64 } 65 66 func fakeMassiveDataAttacher(stdin io.Reader, stdout, stderr io.WriteCloser, tty bool, resize <-chan TerminalSize) error { 67 68 copyDone := make(chan struct{}, 3) 69 70 if stdin == nil { 71 return errors.New("stdin is requested") // we need stdin to notice the conn break 72 } 73 74 go func() { 75 io.Copy(ioutil.Discard, stdin) 76 copyDone <- struct{}{} 77 }() 78 79 go func() { 80 if stdout == nil { 81 return 82 } 83 copyDone <- writeMassiveData(stdout) 84 }() 85 86 go func() { 87 if stderr == nil { 88 return 89 } 90 copyDone <- writeMassiveData(stderr) 91 }() 92 93 select { 94 case <-copyDone: 95 return nil 96 } 97 } 98 99 func writeMassiveData(stdStream io.Writer) struct{} { // write to stdin or stdout 100 for { 101 _, err := io.Copy(stdStream, strings.NewReader("something")) 102 if err != nil && err.Error() != "EOF" { 103 break 104 } 105 } 106 return struct{}{} 107 } 108 109 func TestSPDYExecutorStream(t *testing.T) { 110 tests := []struct { 111 name string 112 options StreamOptions 113 expectError string 114 attacher AttachFunc 115 }{ 116 { 117 name: "stdoutBlockTest", 118 options: StreamOptions{ 119 Stdin: &fakeMassiveDataPty{}, 120 Stdout: &fakeMassiveDataPty{}, 121 }, 122 expectError: "", 123 attacher: fakeMassiveDataAttacher, 124 }, 125 { 126 name: "stderrBlockTest", 127 options: StreamOptions{ 128 Stdin: &fakeMassiveDataPty{}, 129 Stderr: &fakeMassiveDataPty{}, 130 }, 131 expectError: "", 132 attacher: fakeMassiveDataAttacher, 133 }, 134 } 135 136 for _, test := range tests { 137 server := newTestHTTPServer(test.attacher, &test.options) 138 139 err := attach2Server(server.URL, test.options) 140 gotError := "" 141 if err != nil { 142 gotError = err.Error() 143 } 144 if test.expectError != gotError { 145 t.Errorf("%s: expected [%v], got [%v]", test.name, test.expectError, gotError) 146 } 147 148 server.Close() 149 } 150 151 } 152 153 func newTestHTTPServer(f AttachFunc, options *StreamOptions) *httptest.Server { 154 server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { 155 ctx, err := createHTTPStreams(writer, request, options) 156 if err != nil { 157 return 158 } 159 defer ctx.conn.Close() 160 161 // handle input output 162 err = f(ctx.stdinStream, ctx.stdoutStream, ctx.stderrStream, false, nil) 163 if err != nil { 164 ctx.writeStatus(apierrors.NewInternalError(err)) 165 } else { 166 ctx.writeStatus(&apierrors.StatusError{ErrStatus: metav1.Status{ 167 Status: metav1.StatusSuccess, 168 }}) 169 } 170 })) 171 return server 172 } 173 174 func attach2Server(rawURL string, options StreamOptions) error { 175 uri, _ := url.Parse(rawURL) 176 exec, err := NewSPDYExecutor(&rest.Config{Host: uri.Host}, "POST", uri) 177 if err != nil { 178 return err 179 } 180 181 e := make(chan error) 182 go func(e chan error) { 183 e <- exec.Stream(options) 184 }(e) 185 select { 186 case err := <-e: 187 return err 188 case <-time.After(wait.ForeverTestTimeout): 189 return errors.New("execute timeout") 190 } 191 } 192 193 // simplify createHttpStreams , only support StreamProtocolV4Name 194 func createHTTPStreams(w http.ResponseWriter, req *http.Request, opts *StreamOptions) (*streamContext, error) { 195 _, err := httpstream.Handshake(req, w, []string{remotecommandconsts.StreamProtocolV4Name}) 196 if err != nil { 197 return nil, err 198 } 199 200 upgrader := spdy.NewResponseUpgrader() 201 streamCh := make(chan streamAndReply) 202 conn := upgrader.UpgradeResponse(w, req, func(stream httpstream.Stream, replySent <-chan struct{}) error { 203 streamCh <- streamAndReply{Stream: stream, replySent: replySent} 204 return nil 205 }) 206 ctx := &streamContext{ 207 conn: conn, 208 } 209 210 // wait for stream 211 replyChan := make(chan struct{}, 4) 212 defer close(replyChan) 213 receivedStreams := 0 214 expectedStreams := 1 215 if opts.Stdout != nil { 216 expectedStreams++ 217 } 218 if opts.Stdin != nil { 219 expectedStreams++ 220 } 221 if opts.Stderr != nil { 222 expectedStreams++ 223 } 224 WaitForStreams: 225 for { 226 select { 227 case stream := <-streamCh: 228 streamType := stream.Headers().Get(v1.StreamType) 229 switch streamType { 230 case v1.StreamTypeError: 231 replyChan <- struct{}{} 232 ctx.writeStatus = v4WriteStatusFunc(stream) 233 case v1.StreamTypeStdout: 234 replyChan <- struct{}{} 235 ctx.stdoutStream = stream 236 case v1.StreamTypeStdin: 237 replyChan <- struct{}{} 238 ctx.stdinStream = stream 239 case v1.StreamTypeStderr: 240 replyChan <- struct{}{} 241 ctx.stderrStream = stream 242 default: 243 // add other stream ... 244 return nil, errors.New("unimplemented stream type") 245 } 246 case <-replyChan: 247 receivedStreams++ 248 if receivedStreams == expectedStreams { 249 break WaitForStreams 250 } 251 } 252 } 253 254 return ctx, nil 255 } 256 257 func v4WriteStatusFunc(stream io.Writer) func(status *apierrors.StatusError) error { 258 return func(status *apierrors.StatusError) error { 259 bs, err := json.Marshal(status.Status()) 260 if err != nil { 261 return err 262 } 263 _, err = stream.Write(bs) 264 return err 265 } 266 }