...

Source file src/sigs.k8s.io/gateway-api/conformance/utils/http/http.go

Documentation: sigs.k8s.io/gateway-api/conformance/utils/http

     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  }
   424  

View as plain text