knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/test/spoof/spoof.go (about) 1 /* 2 Copyright 2019 The Knative 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 // spoof contains logic to make polling HTTP requests against an endpoint with optional host spoofing. 18 19 package spoof 20 21 import ( 22 "context" 23 "errors" 24 "fmt" 25 "io" 26 "net" 27 "net/http" 28 "net/url" 29 "time" 30 31 "k8s.io/apimachinery/pkg/util/wait" 32 "k8s.io/client-go/kubernetes" 33 "knative.dev/pkg/test/ingress" 34 "knative.dev/pkg/test/logging" 35 36 "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" 37 "go.opentelemetry.io/otel" 38 "go.opentelemetry.io/otel/trace" 39 ) 40 41 var tracer trace.Tracer 42 43 func init() { 44 tracer = otel.GetTracerProvider().Tracer("knative.dev/pkg/test/spoof") 45 } 46 47 // Response is a stripped down subset of http.Response. The is primarily useful 48 // for ResponseCheckers to inspect the response body without consuming it. 49 // Notably, Body is a byte slice instead of an io.ReadCloser. 50 type Response struct { 51 Status string 52 StatusCode int 53 Header http.Header 54 Body []byte 55 } 56 57 func (r *Response) String() string { 58 return fmt.Sprintf("status: %d, body: %s, headers: %v", r.StatusCode, string(r.Body), r.Header) 59 } 60 61 // https://medium.com/stupid-gopher-tricks/ensuring-go-interface-satisfaction-at-compile-time-1ed158e8fa17 62 var dialContext = (&net.Dialer{}).DialContext 63 64 // ResponseChecker is used to determine when SpoofingClient.Poll is done polling. 65 // This allows you to predicate wait.PollImmediate on the request's http.Response. 66 // 67 // See the apimachinery wait package: 68 // https://github.com/kubernetes/apimachinery/blob/cf7ae2f57dabc02a3d215f15ca61ae1446f3be8f/pkg/util/wait/wait.go#L172 69 type ResponseChecker func(resp *Response) (done bool, err error) 70 71 // ErrorRetryChecker is used to determine if an error should be retried or not. 72 // If an error should be retried, it should return true and the wrapped error to explain why to retry. 73 type ErrorRetryChecker func(e error) (retry bool, err error) 74 75 // ResponseRetryChecker is used to determine if a response should be retried or not. 76 // If a response should be retried, it should return true and an error to explain why to retry. 77 // 78 // This is distinct from ResponseChecker in that it shall be used to retry responses, 79 // where the HTTP request was technically successful (it returned something) but indicates 80 // an error (e.g. the overload page of a loadbalancer). 81 type ResponseRetryChecker func(resp *Response) (retry bool, err error) 82 83 // SpoofingClient is a minimal HTTP client wrapper that spoofs the domain of requests 84 // for non-resolvable domains. 85 type SpoofingClient struct { 86 Client *http.Client 87 RequestInterval time.Duration 88 RequestTimeout time.Duration 89 Logf logging.FormatLogger 90 } 91 92 // TransportOption allows callers to customize the http.Transport used by a SpoofingClient 93 type TransportOption func(transport *http.Transport) *http.Transport 94 95 // New returns a SpoofingClient that rewrites requests if the target domain is not `resolvable`. 96 // It does this by looking up the ingress at construction time, so reusing a client will not 97 // follow the ingress if it moves (or if there are multiple ingresses). 98 // 99 // If that's a problem, see test/request.go#WaitForEndpointState for oneshot spoofing. 100 func New( 101 ctx context.Context, 102 kubeClientset kubernetes.Interface, 103 logf logging.FormatLogger, 104 domain string, 105 resolvable bool, 106 endpointOverride string, 107 requestInterval, requestTimeout time.Duration, 108 opts ...TransportOption, 109 ) (*SpoofingClient, error) { 110 endpoint, mapper, err := ResolveEndpoint(ctx, kubeClientset, domain, resolvable, endpointOverride) 111 if err != nil { 112 return nil, fmt.Errorf("failed to get the cluster endpoint: %w", err) 113 } 114 115 // Spoof the hostname at the resolver level 116 logf("Spoofing %s -> %s", domain, endpoint) 117 transport := &http.Transport{ 118 DialContext: func(ctx context.Context, network, addr string) (conn net.Conn, e error) { 119 _, port, err := net.SplitHostPort(addr) 120 if err != nil { 121 return nil, err 122 } 123 // The original hostname:port is spoofed by replacing the hostname by the value 124 // returned by ResolveEndpoint. 125 return dialContext(ctx, network, net.JoinHostPort(endpoint, mapper(port))) 126 }, 127 } 128 129 for _, opt := range opts { 130 transport = opt(transport) 131 } 132 133 roundTripper := otelhttp.NewTransport(transport) 134 135 sc := &SpoofingClient{ 136 Client: &http.Client{Transport: roundTripper}, 137 RequestInterval: requestInterval, 138 RequestTimeout: requestTimeout, 139 Logf: logf, 140 } 141 return sc, nil 142 } 143 144 // ResolveEndpoint resolves the endpoint address considering whether the domain is resolvable and taking into 145 // account whether the user overrode the endpoint address externally 146 func ResolveEndpoint(ctx context.Context, kubeClientset kubernetes.Interface, domain string, resolvable bool, endpointOverride string) (string, func(string) string, error) { 147 id := func(in string) string { return in } 148 // If the domain is resolvable, it can be used directly 149 if resolvable { 150 return domain, id, nil 151 } 152 // Otherwise, use the actual cluster endpoint 153 return ingress.GetIngressEndpoint(ctx, kubeClientset, endpointOverride) 154 } 155 156 // Do dispatches to the underlying http.Client.Do, spoofing domains as needed 157 // and transforming the http.Response into a spoof.Response. 158 // Each response is augmented with "X-Trace-Id" header that identifies the trace corresponding to the request. 159 func (sc *SpoofingClient) Do(req *http.Request, errorRetryCheckers ...interface{}) (*Response, error) { 160 return sc.Poll(req, func(*Response) (bool, error) { return true, nil }, errorRetryCheckers...) 161 } 162 163 // Poll executes an http request until it satisfies the inState condition or, if there's an error, 164 // none of the error retry checkers permit a retry. 165 // If no retry checkers are specified `DefaultErrorRetryChecker` will be used. 166 func (sc *SpoofingClient) Poll(req *http.Request, inState ResponseChecker, checkers ...interface{}) (*Response, error) { 167 if len(checkers) == 0 { 168 checkers = []interface{}{ErrorRetryChecker(DefaultErrorRetryChecker), ResponseRetryChecker(DefaultResponseRetryChecker)} 169 } 170 171 var resp *Response 172 err := wait.PollUntilContextTimeout(context.Background(), sc.RequestInterval, sc.RequestTimeout, true, func(ctx context.Context) (bool, error) { 173 // Starting span to capture zipkin trace. 174 traceContext, span := tracer.Start(req.Context(), "SpoofingClient-Trace") 175 defer span.End() 176 rawResp, err := sc.Client.Do(req.WithContext(traceContext)) 177 if err != nil { 178 for _, checker := range checkers { 179 if ec, ok := checker.(ErrorRetryChecker); ok { 180 retry, newErr := ec(err) 181 if retry { 182 sc.Logf("Retrying %s: %v", req.URL.String(), newErr) 183 return false, nil 184 } 185 } 186 } 187 sc.Logf("NOT Retrying %s: %v", req.URL.String(), err) 188 return true, err 189 } 190 defer rawResp.Body.Close() 191 192 body, err := io.ReadAll(rawResp.Body) 193 if err != nil { 194 return true, err 195 } 196 197 resp = &Response{ 198 Status: rawResp.Status, 199 StatusCode: rawResp.StatusCode, 200 Header: rawResp.Header, 201 Body: body, 202 } 203 204 // This is distinct from inState in that it allows us to uniformly check for 205 // error responses to retry HTTP requests that have technically been successful, 206 // but haven't reached their destination (e.g. got a loadbalancer overload page). 207 for _, checker := range checkers { 208 if rc, ok := checker.(ResponseRetryChecker); ok { 209 retry, newErr := rc(resp) 210 if retry { 211 sc.Logf("Retrying %s: %v", req.URL.String(), newErr) 212 return false, nil 213 } 214 } 215 } 216 217 return inState(resp) 218 }) 219 if err != nil { 220 return resp, fmt.Errorf("response: %s did not pass checks: %w", resp, err) 221 } 222 return resp, nil 223 } 224 225 // DefaultErrorRetryChecker implements the defaults for retrying on error. 226 func DefaultErrorRetryChecker(err error) (bool, error) { 227 if isTCPTimeout(err) { 228 return true, fmt.Errorf("retrying for TCP timeout: %w", err) 229 } 230 // Retrying on DNS error, since we may be using sslip.io or nip.io in tests. 231 if isDNSError(err) { 232 return true, fmt.Errorf("retrying for DNS error: %w", err) 233 } 234 // Repeat the poll on `connection refused` errors, which are usually transient Istio errors. 235 if isConnectionRefused(err) { 236 return true, fmt.Errorf("retrying for connection refused: %w", err) 237 } 238 if isConnectionReset(err) { 239 return true, fmt.Errorf("retrying for connection reset: %w", err) 240 } 241 // Retry on connection/network errors. 242 if errors.Is(err, io.EOF) { 243 return true, fmt.Errorf("retrying for: %w", err) 244 } 245 // No route to host errors are in the same category as connection refused errors and 246 // are usually transient. 247 if isNoRouteToHostError(err) { 248 return true, fmt.Errorf("retrying for 'no route to host' error: %w", err) 249 } 250 return false, err 251 } 252 253 // DefaultResponseRetryChecker implements the defaults for retrying on response. 254 func DefaultResponseRetryChecker(resp *Response) (bool, error) { 255 if isResponseDNSError(resp) { 256 return true, fmt.Errorf("retrying for DNS related failure response: %v", resp) 257 } 258 return false, nil 259 } 260 261 func (sc *SpoofingClient) WaitForEndpointState( 262 ctx context.Context, 263 url *url.URL, 264 inState ResponseChecker, 265 desc string, 266 opts ...RequestOption, 267 ) (*Response, error) { 268 return sc.endpointState( 269 ctx, 270 url, 271 inState, 272 desc, 273 func(req *http.Request, check ResponseChecker) (*Response, error) { return sc.Poll(req, check) }, 274 "WaitForEndpointState", 275 opts...) 276 } 277 278 func (sc *SpoofingClient) endpointState( 279 ctx context.Context, 280 url *url.URL, 281 inState ResponseChecker, 282 desc string, 283 f func(*http.Request, ResponseChecker) (*Response, error), 284 logName string, 285 opts ...RequestOption, 286 ) (*Response, error) { 287 defer logging.GetEmitableSpan(ctx, logName+"/"+desc).End() 288 289 if url.Scheme == "" || url.Host == "" { 290 return nil, fmt.Errorf("invalid URL: %q", url.String()) 291 } 292 293 req, err := http.NewRequest(http.MethodGet, url.String(), nil) 294 if err != nil { 295 return nil, err 296 } 297 298 for _, opt := range opts { 299 opt(req) 300 } 301 302 return f(req, inState) 303 } 304 305 func (sc *SpoofingClient) Check(req *http.Request, inState ResponseChecker, checkers ...interface{}) (*Response, error) { 306 resp, err := sc.Do(req, checkers...) 307 if err != nil { 308 return nil, err 309 } 310 311 ok, err := inState(resp) 312 if err != nil { 313 return resp, fmt.Errorf("response: %s did not pass checks: %w", resp, err) 314 } 315 if ok { 316 return resp, nil 317 } 318 319 return nil, err 320 } 321 322 func (sc *SpoofingClient) CheckEndpointState( 323 ctx context.Context, 324 url *url.URL, 325 inState ResponseChecker, 326 desc string, 327 opts ...RequestOption, 328 ) (*Response, error) { 329 return sc.endpointState( 330 ctx, 331 url, 332 inState, 333 desc, 334 func(req *http.Request, check ResponseChecker) (*Response, error) { return sc.Check(req, check) }, 335 "CheckEndpointState", 336 opts...) 337 }