k8s.io/client-go@v0.31.1/rest/connection_test.go (about) 1 /* 2 Copyright 2019 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 rest 18 19 import ( 20 "context" 21 "fmt" 22 "io" 23 "net" 24 "net/http" 25 "net/http/httptest" 26 "net/url" 27 "strconv" 28 "strings" 29 "sync/atomic" 30 "testing" 31 "time" 32 33 "k8s.io/apimachinery/pkg/runtime/schema" 34 "k8s.io/apimachinery/pkg/runtime/serializer" 35 utilnet "k8s.io/apimachinery/pkg/util/net" 36 "k8s.io/apimachinery/pkg/util/wait" 37 ) 38 39 type tcpLB struct { 40 t *testing.T 41 ln net.Listener 42 serverURL string 43 dials int32 44 } 45 46 func (lb *tcpLB) handleConnection(in net.Conn, stopCh chan struct{}) { 47 out, err := net.Dial("tcp", lb.serverURL) 48 if err != nil { 49 lb.t.Log(err) 50 return 51 } 52 go io.Copy(out, in) 53 go io.Copy(in, out) 54 <-stopCh 55 if err := out.Close(); err != nil { 56 lb.t.Fatalf("failed to close connection: %v", err) 57 } 58 } 59 60 func (lb *tcpLB) serve(stopCh chan struct{}) { 61 conn, err := lb.ln.Accept() 62 if err != nil { 63 lb.t.Fatalf("failed to accept: %v", err) 64 } 65 atomic.AddInt32(&lb.dials, 1) 66 go lb.handleConnection(conn, stopCh) 67 } 68 69 func newLB(t *testing.T, serverURL string) *tcpLB { 70 ln, err := net.Listen("tcp", "127.0.0.1:0") 71 if err != nil { 72 t.Fatalf("failed to bind: %v", err) 73 } 74 lb := tcpLB{ 75 serverURL: serverURL, 76 ln: ln, 77 t: t, 78 } 79 return &lb 80 } 81 82 const ( 83 readIdleTimeout int = 1 84 pingTimeout int = 1 85 ) 86 87 func TestReconnectBrokenTCP(t *testing.T) { 88 t.Setenv("HTTP2_READ_IDLE_TIMEOUT_SECONDS", strconv.Itoa(readIdleTimeout)) 89 t.Setenv("HTTP2_PING_TIMEOUT_SECONDS", strconv.Itoa(pingTimeout)) 90 t.Setenv("DISABLE_HTTP2", "") 91 ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 92 fmt.Fprintf(w, "Hello, %s", r.Proto) 93 })) 94 ts.EnableHTTP2 = true 95 ts.StartTLS() 96 defer ts.Close() 97 98 u, err := url.Parse(ts.URL) 99 if err != nil { 100 t.Fatalf("failed to parse URL from %q: %v", ts.URL, err) 101 } 102 lb := newLB(t, u.Host) 103 defer lb.ln.Close() 104 stopCh := make(chan struct{}) 105 go lb.serve(stopCh) 106 transport, ok := ts.Client().Transport.(*http.Transport) 107 if !ok { 108 t.Fatalf("failed to assert *http.Transport") 109 } 110 config := &Config{ 111 Host: "https://" + lb.ln.Addr().String(), 112 Transport: utilnet.SetTransportDefaults(transport), 113 Timeout: 1 * time.Second, 114 // These fields are required to create a REST client. 115 ContentConfig: ContentConfig{ 116 GroupVersion: &schema.GroupVersion{}, 117 NegotiatedSerializer: &serializer.CodecFactory{}, 118 }, 119 } 120 client, err := RESTClientFor(config) 121 if err != nil { 122 t.Fatalf("failed to create REST client: %v", err) 123 } 124 data, err := client.Get().AbsPath("/").DoRaw(context.TODO()) 125 if err != nil { 126 t.Fatalf("unexpected err: %s: %v", data, err) 127 } 128 if string(data) != "Hello, HTTP/2.0" { 129 t.Fatalf("unexpected response: %s", data) 130 } 131 132 // Deliberately let the LB stop proxying traffic for the current 133 // connection. This mimics a broken TCP connection that's not properly 134 // closed. 135 close(stopCh) 136 137 stopCh = make(chan struct{}) 138 go lb.serve(stopCh) 139 // Sleep enough time for the HTTP/2 health check to detect and close 140 // the broken TCP connection. 141 time.Sleep(time.Duration(1+readIdleTimeout+pingTimeout) * time.Second) 142 // If the HTTP/2 health check were disabled, the broken connection 143 // would still be in the connection pool, the following request would 144 // then reuse the broken connection instead of creating a new one, and 145 // thus would fail. 146 data, err = client.Get().AbsPath("/").DoRaw(context.TODO()) 147 if err != nil { 148 t.Fatalf("unexpected err: %v", err) 149 } 150 if string(data) != "Hello, HTTP/2.0" { 151 t.Fatalf("unexpected response: %s", data) 152 } 153 dials := atomic.LoadInt32(&lb.dials) 154 if dials != 2 { 155 t.Fatalf("expected %d dials, got %d", 2, dials) 156 } 157 } 158 159 // 1. connect to https server with http1.1 using a TCP proxy 160 // 2. the connection has keepalive enabled so it will be reused 161 // 3. break the TCP connection stopping the proxy 162 // 4. close the idle connection to force creating a new connection 163 // 5. count that there are 2 connections to the server (we didn't reuse the original connection) 164 func TestReconnectBrokenTCP_HTTP1(t *testing.T) { 165 ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 166 fmt.Fprintf(w, "Hello, %s", r.Proto) 167 })) 168 ts.EnableHTTP2 = false 169 ts.StartTLS() 170 defer ts.Close() 171 172 u, err := url.Parse(ts.URL) 173 if err != nil { 174 t.Fatalf("failed to parse URL from %q: %v", ts.URL, err) 175 } 176 lb := newLB(t, u.Host) 177 defer lb.ln.Close() 178 stopCh := make(chan struct{}) 179 go lb.serve(stopCh) 180 transport, ok := ts.Client().Transport.(*http.Transport) 181 if !ok { 182 t.Fatal("failed to assert *http.Transport") 183 } 184 config := &Config{ 185 Host: "https://" + lb.ln.Addr().String(), 186 Transport: utilnet.SetTransportDefaults(transport), 187 // large timeout, otherwise the broken connection will be cleaned by it 188 Timeout: wait.ForeverTestTimeout, 189 // These fields are required to create a REST client. 190 ContentConfig: ContentConfig{ 191 GroupVersion: &schema.GroupVersion{}, 192 NegotiatedSerializer: &serializer.CodecFactory{}, 193 }, 194 } 195 config.TLSClientConfig.NextProtos = []string{"http/1.1"} 196 client, err := RESTClientFor(config) 197 if err != nil { 198 t.Fatalf("failed to create REST client: %v", err) 199 } 200 201 data, err := client.Get().AbsPath("/").DoRaw(context.TODO()) 202 if err != nil { 203 t.Fatalf("unexpected err: %s: %v", data, err) 204 } 205 if string(data) != "Hello, HTTP/1.1" { 206 t.Fatalf("unexpected response: %s", data) 207 } 208 209 // Deliberately let the LB stop proxying traffic for the current 210 // connection. This mimics a broken TCP connection that's not properly 211 // closed. 212 close(stopCh) 213 214 stopCh = make(chan struct{}) 215 go lb.serve(stopCh) 216 // Close the idle connections 217 utilnet.CloseIdleConnectionsFor(client.Client.Transport) 218 219 // If the client didn't close the idle connections, the broken connection 220 // would still be in the connection pool, the following request would 221 // then reuse the broken connection instead of creating a new one, and 222 // thus would fail. 223 data, err = client.Get().AbsPath("/").DoRaw(context.TODO()) 224 if err != nil { 225 t.Fatalf("unexpected err: %v", err) 226 } 227 if string(data) != "Hello, HTTP/1.1" { 228 t.Fatalf("unexpected response: %s", data) 229 } 230 dials := atomic.LoadInt32(&lb.dials) 231 if dials != 2 { 232 t.Fatalf("expected %d dials, got %d", 2, dials) 233 } 234 } 235 236 // 1. connect to https server with http1.1 using a TCP proxy making the connection to timeout 237 // 2. the connection has keepalive enabled so it will be reused 238 // 3. close the in-flight connection to force creating a new connection 239 // 4. count that there are 2 connections on the LB but only one succeeds 240 func TestReconnectBrokenTCPInFlight_HTTP1(t *testing.T) { 241 done := make(chan struct{}) 242 defer close(done) 243 received := make(chan struct{}) 244 245 ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 246 if r.URL.Path == "/hang" { 247 conn, _, _ := w.(http.Hijacker).Hijack() 248 close(received) 249 <-done 250 conn.Close() 251 } 252 fmt.Fprintf(w, "Hello, %s", r.Proto) 253 })) 254 ts.EnableHTTP2 = false 255 ts.StartTLS() 256 defer ts.Close() 257 258 u, err := url.Parse(ts.URL) 259 if err != nil { 260 t.Fatalf("failed to parse URL from %q: %v", ts.URL, err) 261 } 262 263 lb := newLB(t, u.Host) 264 defer lb.ln.Close() 265 stopCh := make(chan struct{}) 266 go lb.serve(stopCh) 267 268 transport, ok := ts.Client().Transport.(*http.Transport) 269 if !ok { 270 t.Fatal("failed to assert *http.Transport") 271 } 272 config := &Config{ 273 Host: "https://" + lb.ln.Addr().String(), 274 Transport: utilnet.SetTransportDefaults(transport), 275 // Use something extraordinary large to not hit the timeout 276 Timeout: wait.ForeverTestTimeout, 277 // These fields are required to create a REST client. 278 ContentConfig: ContentConfig{ 279 GroupVersion: &schema.GroupVersion{}, 280 NegotiatedSerializer: &serializer.CodecFactory{}, 281 }, 282 } 283 config.TLSClientConfig.NextProtos = []string{"http/1.1"} 284 285 client, err := RESTClientFor(config) 286 if err != nil { 287 t.Fatalf("failed to create REST client: %v", err) 288 } 289 290 // The request will connect, hang and eventually time out 291 // but we can use a context to close once the test is done 292 // we are only interested in have an inflight connection 293 ctx, cancel := context.WithCancel(context.Background()) 294 reqErrCh := make(chan error, 1) 295 defer close(reqErrCh) 296 go func() { 297 _, err = client.Get().AbsPath("/hang").DoRaw(ctx) 298 reqErrCh <- err 299 }() 300 301 // wait until it connect to the server 302 select { 303 case <-received: 304 case <-time.After(wait.ForeverTestTimeout): 305 t.Fatal("Test timed out waiting for first request to fail") 306 } 307 308 // Deliberately let the LB stop proxying traffic for the current 309 // connection. This mimics a broken TCP connection that's not properly 310 // closed. 311 close(stopCh) 312 313 stopCh = make(chan struct{}) 314 go lb.serve(stopCh) 315 316 // New request will fail if tries to reuse the connection 317 data, err := client.Get().AbsPath("/").DoRaw(context.Background()) 318 if err != nil { 319 t.Fatalf("unexpected err: %v", err) 320 } 321 if string(data) != "Hello, HTTP/1.1" { 322 t.Fatalf("unexpected response: %s", data) 323 } 324 dials := atomic.LoadInt32(&lb.dials) 325 if dials != 2 { 326 t.Fatalf("expected %d dials, got %d", 2, dials) 327 } 328 329 // cancel the in-flight connection 330 cancel() 331 select { 332 case <-reqErrCh: 333 if err == nil { 334 t.Fatal("Connection succeeded but was expected to timeout") 335 } 336 case <-time.After(10 * time.Second): 337 t.Fatal("Test timed out waiting for the request to fail") 338 } 339 340 } 341 342 func TestRestClientTimeout(t *testing.T) { 343 ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 344 time.Sleep(2 * time.Second) 345 fmt.Fprintf(w, "Hello, %s", r.Proto) 346 })) 347 ts.Start() 348 defer ts.Close() 349 350 config := &Config{ 351 Host: ts.URL, 352 Timeout: 1 * time.Second, 353 // These fields are required to create a REST client. 354 ContentConfig: ContentConfig{ 355 GroupVersion: &schema.GroupVersion{}, 356 NegotiatedSerializer: &serializer.CodecFactory{}, 357 }, 358 } 359 client, err := RESTClientFor(config) 360 if err != nil { 361 t.Fatalf("failed to create REST client: %v", err) 362 } 363 _, err = client.Get().AbsPath("/").DoRaw(context.TODO()) 364 if err == nil { 365 t.Fatalf("timeout error expected") 366 } 367 if !strings.Contains(err.Error(), "deadline exceeded") { 368 t.Fatalf("timeout error expected, received %v", err) 369 } 370 }