k8s.io/kubernetes@v1.29.3/test/integration/apimachinery/watch_timeout_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 apimachinery 18 19 import ( 20 "bytes" 21 "context" 22 "fmt" 23 "io" 24 "net/http" 25 "net/http/httptest" 26 "net/http/httputil" 27 "net/url" 28 "strings" 29 "testing" 30 "time" 31 32 "golang.org/x/net/websocket" 33 34 corev1 "k8s.io/api/core/v1" 35 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 36 "k8s.io/apimachinery/pkg/runtime" 37 "k8s.io/apimachinery/pkg/watch" 38 "k8s.io/client-go/kubernetes" 39 restclient "k8s.io/client-go/rest" 40 "k8s.io/client-go/tools/cache" 41 kubectlproxy "k8s.io/kubectl/pkg/proxy" 42 kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing" 43 "k8s.io/kubernetes/test/integration/framework" 44 ) 45 46 type extractRT struct { 47 http.Header 48 } 49 50 func (rt *extractRT) RoundTrip(req *http.Request) (*http.Response, error) { 51 rt.Header = req.Header 52 return &http.Response{}, nil 53 } 54 55 // headersForConfig extracts any http client logic necessary for the provided 56 // config. 57 func headersForConfig(c *restclient.Config, url *url.URL) (http.Header, error) { 58 extract := &extractRT{} 59 rt, err := restclient.HTTPWrappersForConfig(c, extract) 60 if err != nil { 61 return nil, err 62 } 63 request, err := http.NewRequest("GET", url.String(), nil) 64 if err != nil { 65 return nil, err 66 } 67 if _, err := rt.RoundTrip(request); err != nil { 68 return nil, err 69 } 70 return extract.Header, nil 71 } 72 73 // websocketConfig constructs a websocket config to the provided URL, using the client 74 // config, with the specified protocols. 75 func websocketConfig(url *url.URL, config *restclient.Config, protocols []string) (*websocket.Config, error) { 76 tlsConfig, err := restclient.TLSConfigFor(config) 77 if err != nil { 78 return nil, fmt.Errorf("Failed to create tls config: %v", err) 79 } 80 if url.Scheme == "https" { 81 url.Scheme = "wss" 82 } else { 83 url.Scheme = "ws" 84 } 85 headers, err := headersForConfig(config, url) 86 if err != nil { 87 return nil, fmt.Errorf("Failed to load http headers: %v", err) 88 } 89 cfg, err := websocket.NewConfig(url.String(), "http://localhost") 90 if err != nil { 91 return nil, fmt.Errorf("Failed to create websocket config: %v", err) 92 } 93 cfg.Header = headers 94 cfg.TlsConfig = tlsConfig 95 cfg.Protocol = protocols 96 return cfg, err 97 } 98 99 func TestWebsocketWatchClientTimeout(t *testing.T) { 100 // server setup 101 server := kubeapiservertesting.StartTestServerOrDie(t, nil, nil, framework.SharedEtcd()) 102 defer server.TearDownFn() 103 104 // object setup 105 service := &corev1.Service{ 106 ObjectMeta: metav1.ObjectMeta{Name: "test"}, 107 Spec: corev1.ServiceSpec{ 108 Ports: []corev1.ServicePort{{Name: "http", Port: 80}}, 109 }, 110 } 111 configmap := &corev1.ConfigMap{ 112 ObjectMeta: metav1.ObjectMeta{Name: "test"}, 113 } 114 clientset, err := kubernetes.NewForConfig(server.ClientConfig) 115 if err != nil { 116 t.Fatal(err) 117 } 118 if _, err := clientset.CoreV1().Services("default").Create(context.TODO(), service, metav1.CreateOptions{}); err != nil { 119 t.Fatal(err) 120 } 121 if _, err := clientset.CoreV1().ConfigMaps("default").Create(context.TODO(), configmap, metav1.CreateOptions{}); err != nil { 122 t.Fatal(err) 123 } 124 125 testcases := []struct { 126 name string 127 path string 128 timeout time.Duration 129 expectResult string 130 }{ 131 { 132 name: "configmaps", 133 path: "/api/v1/configmaps?watch=true&timeoutSeconds=5", 134 timeout: 10 * time.Second, 135 expectResult: `"name":"test"`, 136 }, 137 { 138 name: "services", 139 path: "/api/v1/services?watch=true&timeoutSeconds=5", 140 timeout: 10 * time.Second, 141 expectResult: `"name":"test"`, 142 }, 143 } 144 145 for _, tc := range testcases { 146 t.Run(tc.name, func(t *testing.T) { 147 url, err := url.Parse(server.ClientConfig.Host + tc.path) 148 if err != nil { 149 t.Fatal(err) 150 } 151 wsc, err := websocketConfig(url, server.ClientConfig, nil) 152 if err != nil { 153 t.Fatal(err) 154 } 155 156 wsConn, err := websocket.DialConfig(wsc) 157 if err != nil { 158 t.Fatal(err) 159 } 160 defer wsConn.Close() 161 162 resultCh := make(chan string) 163 go func() { 164 defer close(resultCh) 165 buf := &bytes.Buffer{} 166 for { 167 var msg []byte 168 if err := websocket.Message.Receive(wsConn, &msg); err != nil { 169 if err == io.EOF { 170 resultCh <- buf.String() 171 return 172 } 173 if !t.Failed() { 174 // if we didn't already fail, treat this as an error 175 t.Errorf("Failed to read completely from websocket %v", err) 176 } 177 return 178 } 179 if len(msg) == 0 { 180 t.Logf("zero-length message") 181 continue 182 } 183 t.Logf("Read %v %v", len(msg), string(msg)) 184 buf.Write(msg) 185 } 186 }() 187 188 select { 189 case resultString := <-resultCh: 190 if !strings.Contains(resultString, tc.expectResult) { 191 t.Fatalf("Unexpected result:\n%s", resultString) 192 } 193 case <-time.After(tc.timeout): 194 t.Fatalf("hit timeout before connection closed") 195 } 196 }) 197 } 198 } 199 200 func TestWatchClientTimeoutXXX(t *testing.T) { 201 // server setup 202 server := kubeapiservertesting.StartTestServerOrDie(t, nil, nil, framework.SharedEtcd()) 203 defer server.TearDownFn() 204 205 t.Run("direct", func(t *testing.T) { 206 t.Logf("client at %s", server.ClientConfig.Host) 207 testWatchClientTimeouts(t, restclient.CopyConfig(server.ClientConfig)) 208 }) 209 210 t.Run("reverse proxy", func(t *testing.T) { 211 u, _ := url.Parse(server.ClientConfig.Host) 212 proxy := httputil.NewSingleHostReverseProxy(u) 213 proxy.FlushInterval = -1 214 215 transport, err := restclient.TransportFor(server.ClientConfig) 216 if err != nil { 217 t.Fatal(err) 218 } 219 proxy.Transport = transport 220 221 proxyServer := httptest.NewServer(proxy) 222 defer proxyServer.Close() 223 224 t.Logf("client to %s, backend at %s", proxyServer.URL, server.ClientConfig.Host) 225 testWatchClientTimeouts(t, &restclient.Config{Host: proxyServer.URL}) 226 }) 227 228 t.Run("kubectl proxy", func(t *testing.T) { 229 kubectlProxyServer, err := kubectlproxy.NewServer("", "/", "/static/", nil, server.ClientConfig, 0, false) 230 if err != nil { 231 t.Fatal(err) 232 } 233 kubectlProxyListener, err := kubectlProxyServer.Listen("", 0) 234 if err != nil { 235 t.Fatal(err) 236 } 237 defer kubectlProxyListener.Close() 238 go kubectlProxyServer.ServeOnListener(kubectlProxyListener) 239 240 t.Logf("client to %s, backend at %s", kubectlProxyListener.Addr().String(), server.ClientConfig.Host) 241 testWatchClientTimeouts(t, &restclient.Config{Host: "http://" + kubectlProxyListener.Addr().String()}) 242 }) 243 } 244 245 func testWatchClientTimeouts(t *testing.T, config *restclient.Config) { 246 t.Run("timeout", func(t *testing.T) { 247 testWatchClientTimeout(t, config, time.Second, 0) 248 }) 249 t.Run("timeoutSeconds", func(t *testing.T) { 250 testWatchClientTimeout(t, config, 0, time.Second) 251 }) 252 t.Run("timeout+timeoutSeconds", func(t *testing.T) { 253 testWatchClientTimeout(t, config, time.Second, time.Second) 254 }) 255 } 256 257 func testWatchClientTimeout(t *testing.T, config *restclient.Config, timeout, timeoutSeconds time.Duration) { 258 config.Timeout = timeout 259 client, err := kubernetes.NewForConfig(config) 260 if err != nil { 261 t.Fatal(err) 262 } 263 264 listCount := 0 265 watchCount := 0 266 stopCh := make(chan struct{}) 267 listWatch := &cache.ListWatch{ 268 ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { 269 t.Logf("listing (version=%s continue=%s)", options.ResourceVersion, options.Continue) 270 listCount++ 271 if listCount > 1 { 272 t.Errorf("listed more than once") 273 close(stopCh) 274 } 275 return client.CoreV1().ConfigMaps(metav1.NamespaceAll).List(context.TODO(), options) 276 }, 277 WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { 278 t.Logf("watching (version=%s)", options.ResourceVersion) 279 if timeoutSeconds != 0 { 280 timeout := int64(timeoutSeconds / time.Second) 281 options.TimeoutSeconds = &timeout 282 } 283 watchCount++ 284 if watchCount > 1 { 285 // success, restarted watch 286 close(stopCh) 287 } 288 return client.CoreV1().ConfigMaps(metav1.NamespaceAll).Watch(context.TODO(), options) 289 }, 290 } 291 _, informer := cache.NewIndexerInformer(listWatch, &corev1.ConfigMap{}, 30*time.Minute, cache.ResourceEventHandlerFuncs{}, cache.Indexers{}) 292 informer.Run(stopCh) 293 select { 294 case <-stopCh: 295 case <-time.After(time.Minute): 296 t.Fatal("timeout") 297 } 298 }