sigs.k8s.io/gateway-api@v1.0.0/conformance/utils/http/http.go (about) 1 /* 2 Copyright 2022 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 http 18 19 import ( 20 "fmt" 21 "net" 22 "net/url" 23 "strings" 24 "testing" 25 "time" 26 27 "sigs.k8s.io/gateway-api/conformance/utils/config" 28 "sigs.k8s.io/gateway-api/conformance/utils/roundtripper" 29 ) 30 31 // ExpectedResponse defines the response expected for a given request. 32 type ExpectedResponse struct { 33 // Request defines the request to make. 34 Request Request 35 36 // ExpectedRequest defines the request that 37 // is expected to arrive at the backend. If 38 // not specified, the backend request will be 39 // expected to match Request. 40 ExpectedRequest *ExpectedRequest 41 42 RedirectRequest *roundtripper.RedirectRequest 43 44 // BackendSetResponseHeaders is a set of headers 45 // the echoserver should set in its response. 46 BackendSetResponseHeaders map[string]string 47 48 // Response defines what response the test case 49 // should receive. 50 Response Response 51 52 Backend string 53 Namespace string 54 55 // MirroredTo is the destination BackendRefs of the mirrored request. 56 MirroredTo []BackendRef 57 58 // User Given TestCase name 59 TestCaseName string 60 } 61 62 // Request can be used as both the request to make and a means to verify 63 // that echoserver received the expected request. Note that multiple header 64 // values can be provided, as a comma-separated value. 65 type Request struct { 66 Host string 67 Method string 68 Path string 69 Headers map[string]string 70 UnfollowRedirect bool 71 Protocol string 72 } 73 74 // ExpectedRequest defines expected properties of a request that reaches a backend. 75 type ExpectedRequest struct { 76 Request 77 78 // AbsentHeaders are names of headers that are expected 79 // *not* to be present on the request. 80 AbsentHeaders []string 81 } 82 83 // Response defines expected properties of a response from a backend. 84 type Response struct { 85 StatusCode int 86 Headers map[string]string 87 AbsentHeaders []string 88 } 89 90 type BackendRef struct { 91 Name string 92 Namespace string 93 } 94 95 // MakeRequestAndExpectEventuallyConsistentResponse makes a request with the given parameters, 96 // understanding that the request may fail for some amount of time. 97 // 98 // Once the request succeeds consistently with the response having the expected status code, make 99 // additional assertions on the response body using the provided ExpectedResponse. 100 func MakeRequestAndExpectEventuallyConsistentResponse(t *testing.T, r roundtripper.RoundTripper, timeoutConfig config.TimeoutConfig, gwAddr string, expected ExpectedResponse) { 101 t.Helper() 102 103 req := MakeRequest(t, &expected, gwAddr, "HTTP", "http") 104 105 WaitForConsistentResponse(t, r, req, expected, timeoutConfig.RequiredConsecutiveSuccesses, timeoutConfig.MaxTimeToConsistency) 106 } 107 108 func MakeRequest(t *testing.T, expected *ExpectedResponse, gwAddr, protocol, scheme string) roundtripper.Request { 109 t.Helper() 110 111 if expected.Request.Method == "" { 112 expected.Request.Method = "GET" 113 } 114 115 if expected.Response.StatusCode == 0 { 116 expected.Response.StatusCode = 200 117 } 118 119 if expected.Request.Protocol == "" { 120 expected.Request.Protocol = protocol 121 } 122 123 path, query, _ := strings.Cut(expected.Request.Path, "?") 124 reqURL := url.URL{Scheme: scheme, Host: CalculateHost(t, gwAddr, scheme), Path: path, RawQuery: query} 125 126 t.Logf("Making %s request to %s", expected.Request.Method, reqURL.String()) 127 128 req := roundtripper.Request{ 129 Method: expected.Request.Method, 130 Host: expected.Request.Host, 131 URL: reqURL, 132 Protocol: expected.Request.Protocol, 133 Headers: map[string][]string{}, 134 UnfollowRedirect: expected.Request.UnfollowRedirect, 135 } 136 137 if expected.Request.Headers != nil { 138 for name, value := range expected.Request.Headers { 139 req.Headers[name] = []string{value} 140 } 141 } 142 143 backendSetHeaders := []string{} 144 for name, val := range expected.BackendSetResponseHeaders { 145 backendSetHeaders = append(backendSetHeaders, name+":"+val) 146 } 147 req.Headers["X-Echo-Set-Header"] = []string{strings.Join(backendSetHeaders, ",")} 148 149 return req 150 } 151 152 // CalculateHost will calculate the Host header as per [HTTP spec]. To 153 // summarize, host will not include any port if it is implied from the scheme. In 154 // case of any error, the input gwAddr will be returned as the default. 155 // 156 // [HTTP spec]: https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.23 157 func CalculateHost(t *testing.T, gwAddr, scheme string) string { 158 host, port, err := net.SplitHostPort(gwAddr) // note: this will strip brackets of an IPv6 address 159 if err != nil && strings.Contains(err.Error(), "too many colons in address") { 160 // This is an IPv6 address; assume it's valid ipv6 161 // Assume caller won't add a port without brackets 162 gwAddr = "[" + gwAddr + "]" 163 host, port, err = net.SplitHostPort(gwAddr) 164 } 165 if err != nil { 166 t.Logf("Failed to parse host %q: %v", gwAddr, err) 167 return gwAddr 168 } 169 if strings.ToLower(scheme) == "http" && port == "80" { 170 return ipv6SafeHost(host) 171 } 172 if strings.ToLower(scheme) == "https" && port == "443" { 173 return ipv6SafeHost(host) 174 } 175 return gwAddr 176 } 177 178 func ipv6SafeHost(host string) string { 179 // We assume that host is a literal IPv6 address if host has 180 // colons. 181 // Per https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2. 182 // This is like net.JoinHostPort, but we don't need a port. 183 if strings.Contains(host, ":") { 184 return "[" + host + "]" 185 } 186 return host 187 } 188 189 // AwaitConvergence runs the given function until it returns 'true' `threshold` times in a row. 190 // Each failed attempt has a 1s delay; successful attempts have no delay. 191 func AwaitConvergence(t *testing.T, threshold int, maxTimeToConsistency time.Duration, fn func(elapsed time.Duration) bool) { 192 successes := 0 193 attempts := 0 194 start := time.Now() 195 to := time.After(maxTimeToConsistency) 196 delay := time.Second 197 for { 198 select { 199 case <-to: 200 t.Fatalf("timeout while waiting after %d attempts", attempts) 201 default: 202 } 203 204 completed := fn(time.Now().Sub(start)) 205 attempts++ 206 if completed { 207 successes++ 208 if successes >= threshold { 209 return 210 } 211 // Skip delay if we have a success 212 continue 213 } 214 215 successes = 0 216 select { 217 // Capture the overall timeout 218 case <-to: 219 t.Fatalf("timeout while waiting after %d attempts, %d/%d successes", attempts, successes, threshold) 220 // And the per-try delay 221 case <-time.After(delay): 222 } 223 } 224 } 225 226 // WaitForConsistentResponse repeats the provided request until it completes with a response having 227 // the expected response consistently. The provided threshold determines how many times in 228 // a row this must occur to be considered "consistent". 229 func WaitForConsistentResponse(t *testing.T, r roundtripper.RoundTripper, req roundtripper.Request, expected ExpectedResponse, threshold int, maxTimeToConsistency time.Duration) { 230 AwaitConvergence(t, threshold, maxTimeToConsistency, func(elapsed time.Duration) bool { 231 cReq, cRes, err := r.CaptureRoundTrip(req) 232 if err != nil { 233 t.Logf("Request failed, not ready yet: %v (after %v)", err.Error(), elapsed) 234 return false 235 } 236 237 if err := CompareRequest(t, &req, cReq, cRes, expected); err != nil { 238 t.Logf("Response expectation failed for request: %+v not ready yet: %v (after %v)", req, err, elapsed) 239 return false 240 } 241 242 return true 243 }) 244 t.Logf("Request passed") 245 } 246 247 func CompareRequest(t *testing.T, req *roundtripper.Request, cReq *roundtripper.CapturedRequest, cRes *roundtripper.CapturedResponse, expected ExpectedResponse) error { 248 if roundtripper.IsTimeoutError(cRes.StatusCode) { 249 if roundtripper.IsTimeoutError(expected.Response.StatusCode) { 250 return nil 251 } 252 } 253 if expected.Response.StatusCode != cRes.StatusCode { 254 return fmt.Errorf("expected status code to be %d, got %d", expected.Response.StatusCode, cRes.StatusCode) 255 } 256 if cRes.StatusCode == 200 { 257 // The request expected to arrive at the backend is 258 // the same as the request made, unless otherwise 259 // specified. 260 if expected.ExpectedRequest == nil { 261 expected.ExpectedRequest = &ExpectedRequest{Request: expected.Request} 262 } 263 264 if expected.ExpectedRequest.Method == "" { 265 expected.ExpectedRequest.Method = "GET" 266 } 267 268 if expected.ExpectedRequest.Host != "" && expected.ExpectedRequest.Host != cReq.Host { 269 return fmt.Errorf("expected host to be %s, got %s", expected.ExpectedRequest.Host, cReq.Host) 270 } 271 272 if expected.ExpectedRequest.Path != cReq.Path { 273 return fmt.Errorf("expected path to be %s, got %s", expected.ExpectedRequest.Path, cReq.Path) 274 } 275 if expected.ExpectedRequest.Method != cReq.Method { 276 return fmt.Errorf("expected method to be %s, got %s", expected.ExpectedRequest.Method, cReq.Method) 277 } 278 if expected.Namespace != cReq.Namespace { 279 return fmt.Errorf("expected namespace to be %s, got %s", expected.Namespace, cReq.Namespace) 280 } 281 if expected.ExpectedRequest.Headers != nil { 282 if cReq.Headers == nil { 283 return fmt.Errorf("no headers captured, expected %v", len(expected.ExpectedRequest.Headers)) 284 } 285 for name, val := range cReq.Headers { 286 cReq.Headers[strings.ToLower(name)] = val 287 } 288 for name, expectedVal := range expected.ExpectedRequest.Headers { 289 actualVal, ok := cReq.Headers[strings.ToLower(name)] 290 if !ok { 291 return fmt.Errorf("expected %s header to be set, actual headers: %v", name, cReq.Headers) 292 } else if strings.Join(actualVal, ",") != expectedVal { 293 return fmt.Errorf("expected %s header to be set to %s, got %s", name, expectedVal, strings.Join(actualVal, ",")) 294 } 295 } 296 } 297 298 if expected.Response.Headers != nil { 299 if cRes.Headers == nil { 300 return fmt.Errorf("no headers captured, expected %v", len(expected.ExpectedRequest.Headers)) 301 } 302 for name, val := range cRes.Headers { 303 cRes.Headers[strings.ToLower(name)] = val 304 } 305 306 for name, expectedVal := range expected.Response.Headers { 307 actualVal, ok := cRes.Headers[strings.ToLower(name)] 308 if !ok { 309 return fmt.Errorf("expected %s header to be set, actual headers: %v", name, cRes.Headers) 310 } else if strings.Join(actualVal, ",") != expectedVal { 311 return fmt.Errorf("expected %s header to be set to %s, got %s", name, expectedVal, strings.Join(actualVal, ",")) 312 } 313 } 314 } 315 316 if len(expected.Response.AbsentHeaders) > 0 { 317 for name, val := range cRes.Headers { 318 cRes.Headers[strings.ToLower(name)] = val 319 } 320 321 for _, name := range expected.Response.AbsentHeaders { 322 val, ok := cRes.Headers[strings.ToLower(name)] 323 if ok { 324 return fmt.Errorf("expected %s header to not be set, got %s", name, val) 325 } 326 } 327 } 328 329 // Verify that headers expected *not* to be present on the 330 // request are actually not present. 331 if len(expected.ExpectedRequest.AbsentHeaders) > 0 { 332 for name, val := range cReq.Headers { 333 cReq.Headers[strings.ToLower(name)] = val 334 } 335 336 for _, name := range expected.ExpectedRequest.AbsentHeaders { 337 val, ok := cReq.Headers[strings.ToLower(name)] 338 if ok { 339 return fmt.Errorf("expected %s header to not be set, got %s", name, val) 340 } 341 } 342 } 343 344 if !strings.HasPrefix(cReq.Pod, expected.Backend) { 345 return fmt.Errorf("expected pod name to start with %s, got %s", expected.Backend, cReq.Pod) 346 } 347 } else if roundtripper.IsRedirect(cRes.StatusCode) { 348 if expected.RedirectRequest == nil { 349 return nil 350 } 351 352 setRedirectRequestDefaults(req, cRes, &expected) 353 354 if expected.RedirectRequest.Host != cRes.RedirectRequest.Host { 355 return fmt.Errorf("expected redirected hostname to be %q, got %q", expected.RedirectRequest.Host, cRes.RedirectRequest.Host) 356 } 357 358 gotPort := cRes.RedirectRequest.Port 359 if expected.RedirectRequest.Port == "" { 360 // If the test didn't specify any expected redirect port, we'll try to use 361 // the scheme to determine sensible defaults for the port. Well known 362 // schemes like "http" and "https" MAY skip setting any port. 363 if strings.ToLower(cRes.RedirectRequest.Scheme) == "http" && gotPort != "80" && gotPort != "" { 364 return fmt.Errorf("for http scheme, expected redirected port to be 80 or not set, got %q", gotPort) 365 } 366 if strings.ToLower(cRes.RedirectRequest.Scheme) == "https" && gotPort != "443" && gotPort != "" { 367 return fmt.Errorf("for https scheme, expected redirected port to be 443 or not set, got %q", gotPort) 368 } 369 t.Logf("Can't validate redirectPort for unrecognized scheme %v", cRes.RedirectRequest.Scheme) 370 } else if expected.RedirectRequest.Port != gotPort { 371 // An expected port was specified in the tests but it didn't match with 372 // gotPort. 373 return fmt.Errorf("expected redirected port to be %q, got %q", expected.RedirectRequest.Port, gotPort) 374 } 375 376 if expected.RedirectRequest.Scheme != cRes.RedirectRequest.Scheme { 377 return fmt.Errorf("expected redirected scheme to be %q, got %q", expected.RedirectRequest.Scheme, cRes.RedirectRequest.Scheme) 378 } 379 380 if expected.RedirectRequest.Path != cRes.RedirectRequest.Path { 381 return fmt.Errorf("expected redirected path to be %q, got %q", expected.RedirectRequest.Path, cRes.RedirectRequest.Path) 382 } 383 } 384 return nil 385 } 386 387 // GetTestCaseName gets the user-defined test case name or generates one from expected response to a given request. 388 func (er *ExpectedResponse) GetTestCaseName(i int) string { 389 // If TestCase name is provided then use that or else generate one. 390 if er.TestCaseName != "" { 391 return er.TestCaseName 392 } 393 394 headerStr := "" 395 reqStr := "" 396 397 if er.Request.Headers != nil { 398 headerStr = " with headers" 399 } 400 401 reqStr = fmt.Sprintf("%d request to '%s%s'%s", i, er.Request.Host, er.Request.Path, headerStr) 402 403 if er.Backend != "" { 404 return fmt.Sprintf("%s should go to %s", reqStr, er.Backend) 405 } 406 return fmt.Sprintf("%s should receive a %d", reqStr, er.Response.StatusCode) 407 } 408 409 func setRedirectRequestDefaults(req *roundtripper.Request, cRes *roundtripper.CapturedResponse, expected *ExpectedResponse) { 410 // If the expected host is nil it means we do not test host redirect. 411 // In that case we are setting it to the one we got from the response because we do not know the ip/host of the gateway. 412 if expected.RedirectRequest.Host == "" { 413 expected.RedirectRequest.Host = cRes.RedirectRequest.Host 414 } 415 416 if expected.RedirectRequest.Scheme == "" { 417 expected.RedirectRequest.Scheme = req.URL.Scheme 418 } 419 420 if expected.RedirectRequest.Path == "" { 421 expected.RedirectRequest.Path = req.URL.Path 422 } 423 }