istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/test/framework/components/echo/calloptions.go (about) 1 // Copyright Istio Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package echo 16 17 import ( 18 "errors" 19 "fmt" 20 "net/http" 21 "time" 22 23 wrappers "google.golang.org/protobuf/types/known/wrapperspb" 24 25 "istio.io/istio/pkg/http/headers" 26 "istio.io/istio/pkg/test" 27 "istio.io/istio/pkg/test/echo/common" 28 "istio.io/istio/pkg/test/echo/common/scheme" 29 "istio.io/istio/pkg/test/util/retry" 30 ) 31 32 // HTTP settings 33 type HTTP struct { 34 // If true, h2c will be used in HTTP requests 35 HTTP2 bool 36 37 // If true, HTTP/3 request over QUIC will be used. 38 // It is mandatory to specify TLS settings 39 HTTP3 bool 40 41 // Path specifies the URL path for the HTTP(s) request. 42 Path string 43 44 // Method to send. Defaults to GET. 45 Method string 46 47 // Headers indicates headers that should be sent in the request. Ignored for WebSocket calls. 48 // If no Host header is provided, a default will be chosen for the target service endpoint. 49 Headers http.Header 50 51 // FollowRedirects will instruct the call to follow 301 redirects. Otherwise, the original 301 response 52 // is returned directly. 53 FollowRedirects bool 54 55 // HTTProxy used for making ingress echo call via proxy 56 HTTPProxy string 57 } 58 59 // TLS settings 60 type TLS struct { 61 // Use the custom certificate to make the call. This is mostly used to make mTLS request directly 62 // (without proxy) from naked client to test certificates issued by custom CA instead of the Istio self-signed CA. 63 Cert, Key, CaCert string 64 65 // Use the custom certificates file to make the call. 66 CertFile, KeyFile, CaCertFile string 67 68 // Skip verify peer's certificate. 69 InsecureSkipVerify bool 70 71 Alpn []string 72 ServerName string 73 } 74 75 type HBONE struct { 76 Address string 77 Headers http.Header 78 // If non-empty, make the request with the corresponding cert and key. 79 Cert string 80 Key string 81 // If non-empty, verify the server CA 82 CaCert string 83 // If non-empty, make the request with the corresponding cert and key file. 84 CertFile string 85 KeyFile string 86 // If non-empty, verify the server CA with the ca cert file. 87 CaCertFile string 88 // Skip verifying peer's certificate. 89 InsecureSkipVerify bool 90 } 91 92 // Retry settings 93 type Retry struct { 94 // NoRetry if true, no retry will be attempted. 95 NoRetry bool 96 97 // Options to be used when retrying. If not specified, defaults will be used. 98 Options []retry.Option 99 } 100 101 // TCP settings 102 type TCP struct { 103 // ExpectedResponse asserts this is in the response for TCP requests. 104 ExpectedResponse *wrappers.StringValue 105 } 106 107 // Target of a call. 108 type Target interface { 109 Configurable 110 WorkloadContainer 111 112 // Instances in this target. 113 Instances() Instances 114 } 115 116 // CallOptions defines options for calling a Endpoint. 117 type CallOptions struct { 118 // To is the Target to be called. 119 To Target 120 121 // ToWorkload will call a specific workload in this instance, rather than the Service. 122 // If there are multiple workloads in the Instance, the first is used. 123 // Can be used with `ToWorkload: to.WithWorkloads(someWl)` to send to a specific workload. 124 // When using the Port field, the ServicePort should be used. 125 ToWorkload Instance 126 127 // Port to be used for the call. Ignored if Scheme == DNS. If the Port.ServicePort is set, 128 // either Port.Protocol or Scheme must also be set. If Port.ServicePort is not set, 129 // the port is looked up in To by either Port.Name or Port.Protocol. 130 Port Port 131 132 // Scheme to be used when making the call. If not provided, the Scheme will be selected 133 // based on the Port.Protocol. 134 Scheme scheme.Instance 135 136 // Address specifies the host name or IP address to be used on the request. If not provided, 137 // an appropriate default is chosen for To. 138 Address string 139 140 // Count indicates the number of exchanges that should be made with the service endpoint. 141 // If Count <= 0, a default will be selected. If To is specified, the value will be set to 142 // the numWorkloads * DefaultCallsPerWorkload. Otherwise, defaults to 1. 143 Count int 144 145 // Timeout used for each individual request. Must be > 0, otherwise 5 seconds is used. 146 Timeout time.Duration 147 148 // NewConnectionPerRequest if true, the forwarder will establish a new connection to the server for 149 // each individual request. If false, it will attempt to reuse the same connection for the duration 150 // of the forward call. This is ignored for DNS, TCP, and TLS protocols, as well as 151 // Headless/StatefulSet deployments. 152 NewConnectionPerRequest bool 153 154 // ForceDNSLookup if true, the forwarder will force a DNS lookup for each individual request. This is 155 // useful for any situation where DNS is used for load balancing (e.g. headless). This is ignored if 156 // NewConnectionPerRequest is false or if the deployment is Headless or StatefulSet. 157 ForceDNSLookup bool 158 159 // Retry options for the call. 160 Retry Retry 161 162 // HTTP settings. 163 HTTP HTTP 164 165 // TCP settings. 166 TCP TCP 167 168 // TLS settings. 169 TLS TLS 170 171 // HBONE settings. 172 HBONE HBONE 173 174 // Message to be sent. 175 Message string 176 177 // Check the server responses. If none is provided, only the number of responses received 178 // will be checked. 179 Check Checker 180 181 // If we have been asked to do TCP comms with a PROXY protocol header, 182 // determine which version (1 or 2), and send the header. 183 // https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt 184 ProxyProtocolVersion int 185 186 PropagateResponse func(req *http.Request, resp *http.Response) 187 } 188 189 // GetHost returns the best default host for the call. Returns the first host defined from the following 190 // sources (in order of precedence): Host header, target's DefaultHostHeader, Address, target's FQDN. 191 func (o CallOptions) GetHost() string { 192 // First, use the host header, if specified. 193 if h := o.HTTP.Headers.Get(headers.Host); len(h) > 0 { 194 return h 195 } 196 197 // Next use the target's default, if specified. 198 if o.To != nil && len(o.To.Config().DefaultHostHeader) > 0 { 199 return o.To.Config().DefaultHostHeader 200 } 201 202 // Next, if the Address was manually specified use it as the Host. 203 if len(o.Address) > 0 { 204 return o.Address 205 } 206 207 // Finally, use the target's FQDN. 208 if o.To != nil { 209 return o.To.Config().ClusterLocalFQDN() 210 } 211 212 return "" 213 } 214 215 func (o CallOptions) DeepCopy() CallOptions { 216 clone := o 217 if o.TLS.Alpn != nil { 218 clone.TLS.Alpn = make([]string, len(o.TLS.Alpn)) 219 copy(clone.TLS.Alpn, o.TLS.Alpn) 220 } 221 return clone 222 } 223 224 // FillDefaults fills out any defaults that haven't been explicitly specified. 225 func (o *CallOptions) FillDefaults() error { 226 // Fill in the address if not set. 227 if err := o.fillAddress(); err != nil { 228 return err 229 } 230 231 // Fill in the port if not set or the service port is missing. 232 if err := o.fillPort(); err != nil { 233 return err 234 } 235 236 // Fill in the scheme if not set, using the port information. 237 if err := o.fillScheme(); err != nil { 238 return err 239 } 240 241 // Fill in HTTP headers 242 o.fillHeaders() 243 244 if o.Timeout <= 0 { 245 o.Timeout = common.DefaultRequestTimeout 246 } 247 248 // Fill the number of calls to make. 249 o.fillCallCount() 250 251 // Fill connection parameters based on scheme and workload type. 252 o.fillConnectionParams() 253 254 // Fill in default retry options, if not specified. 255 o.fillRetryOptions() 256 257 // check must be specified 258 if o.Check == nil { 259 panic("o.Check not set") 260 } 261 262 // If ProxyProtoVersion is not 0, 1, or 2, default to 0 (disabled) 263 o.fillProxyProtoVersion() 264 265 return nil 266 } 267 268 // FillDefaultsOrFail calls FillDefaults and fails if an error occurs. 269 func (o *CallOptions) FillDefaultsOrFail(t test.Failer) { 270 t.Helper() 271 if err := o.FillDefaults(); err != nil { 272 t.Fatal(err) 273 } 274 } 275 276 func (o *CallOptions) fillCallCount() { 277 if o.Count > 0 { 278 // Nothing to do. 279 return 280 } 281 282 o.Count = common.DefaultCount 283 284 // Try setting an appropriate count for the number of workloads. 285 newCount := DefaultCallsPerWorkload() * o.numWorkloads() 286 if newCount > o.Count { 287 o.Count = newCount 288 } 289 } 290 291 func (o *CallOptions) fillProxyProtoVersion() int { 292 if o.ProxyProtocolVersion > 0 && o.ProxyProtocolVersion < 3 { 293 // Nothing to do. 294 return o.ProxyProtocolVersion 295 } 296 return 0 297 } 298 299 func (o *CallOptions) numWorkloads() int { 300 if o.To == nil { 301 return 0 302 } 303 workloads, err := o.To.Workloads() 304 if err != nil { 305 return 0 306 } 307 return len(workloads) 308 } 309 310 func (o *CallOptions) fillConnectionParams() { 311 // Overrides connection parameters for scheme. 312 switch o.Scheme { 313 case scheme.DNS: 314 o.NewConnectionPerRequest = true 315 o.ForceDNSLookup = true 316 case scheme.TCP, scheme.TLS, scheme.WebSocket: 317 o.NewConnectionPerRequest = true 318 } 319 320 // Override connection parameters for workload type. 321 if o.To != nil { 322 toCfg := o.To.Config() 323 if toCfg.IsHeadless() || toCfg.IsStatefulSet() { 324 // Headless uses DNS for load balancing. Force DNS lookup each time so 325 // that we get proper load balancing behavior. 326 o.NewConnectionPerRequest = true 327 o.ForceDNSLookup = true 328 } 329 } 330 331 // ForceDNSLookup only applies when using new connections per request. 332 o.ForceDNSLookup = o.NewConnectionPerRequest && o.ForceDNSLookup 333 } 334 335 func (o *CallOptions) fillAddress() error { 336 if o.Address == "" { 337 if o.To != nil { 338 // No host specified, use the fully qualified domain name for the service. 339 o.Address = o.To.Config().ClusterLocalFQDN() 340 return nil 341 } 342 if o.ToWorkload != nil { 343 wl, err := o.ToWorkload.Workloads() 344 if err != nil { 345 return err 346 } 347 o.Address = wl[0].Address() 348 return nil 349 } 350 351 return errors.New("if address is not set, then To must be set") 352 } 353 return nil 354 } 355 356 func (o *CallOptions) fillPort() error { 357 if o.Scheme == scheme.DNS { 358 // Port is not used for DNS. 359 return nil 360 } 361 362 if o.Port.ServicePort > 0 { 363 if o.Port.Protocol == "" && o.Scheme == "" { 364 return errors.New("callOptions: servicePort specified, but no protocol or scheme was set") 365 } 366 367 // The service port was set explicitly. Nothing else to do. 368 return nil 369 } 370 371 if o.To != nil { 372 return o.fillPort2(o.To) 373 } else if o.ToWorkload != nil { 374 err := o.fillPort2(o.ToWorkload) 375 if err != nil { 376 return err 377 } 378 // Set the ServicePort to workload port since we are not reaching it through the Service 379 p := o.Port 380 p.ServicePort = p.WorkloadPort 381 o.Port = p 382 } 383 384 if o.Port.ServicePort <= 0 || (o.Port.Protocol == "" && o.Scheme == "") || o.Address == "" { 385 return fmt.Errorf("if target is not set, then port.servicePort, port.protocol or schema, and address must be set") 386 } 387 388 return nil 389 } 390 391 func (o *CallOptions) fillPort2(target Target) error { 392 servicePorts := target.Config().Ports.GetServicePorts() 393 394 if o.Port.Name != "" { 395 // Look up the port by name. 396 p, found := servicePorts.ForName(o.Port.Name) 397 if !found { 398 return fmt.Errorf("callOptions: no port named %s available in To Instance", o.Port.Name) 399 } 400 o.Port = p 401 return nil 402 } 403 404 if o.Port.Protocol != "" { 405 // Look up the port by protocol. 406 p, found := servicePorts.ForProtocol(o.Port.Protocol) 407 if !found { 408 return fmt.Errorf("callOptions: no port for protocol %s available in To Instance", o.Port.Protocol) 409 } 410 o.Port = p 411 return nil 412 } 413 414 if o.Port.ServicePort != 0 { 415 // We just have a single port number, populate the rest of the fields 416 p, found := servicePorts.ForServicePort(o.Port.ServicePort) 417 if !found { 418 return fmt.Errorf("callOptions: no port %d available in To Instance", o.Port.ServicePort) 419 } 420 o.Port = p 421 return nil 422 } 423 return nil 424 } 425 426 func (o *CallOptions) fillScheme() error { 427 if o.Scheme == "" { 428 // No protocol, fill it in. 429 var err error 430 if o.Scheme, err = o.Port.Scheme(); err != nil { 431 return err 432 } 433 } 434 return nil 435 } 436 437 func (o *CallOptions) fillHeaders() { 438 if o.ToWorkload != nil { 439 return 440 } 441 // Initialize the headers and add a default Host header if none provided. 442 if o.HTTP.Headers == nil { 443 o.HTTP.Headers = make(http.Header) 444 } else { 445 // Avoid mutating input, which can lead to concurrent writes 446 o.HTTP.Headers = o.HTTP.Headers.Clone() 447 } 448 449 if h := o.GetHost(); len(h) > 0 { 450 o.HTTP.Headers.Set(headers.Host, h) 451 } 452 } 453 454 func (o *CallOptions) fillRetryOptions() { 455 if o.Retry.NoRetry { 456 // User specified no-retry, nothing to do. 457 return 458 } 459 460 // NOTE: last option wins, so order in the list is important! 461 462 // Start by getting the defaults. 463 retryOpts := DefaultCallRetryOptions() 464 465 // Don't use converge unless we need it. When sending large batches of requests (for example, 466 // when we attempt to reach all clusters), converging will result in sending at least 467 // `converge * count` requests. When running multiple requests in parallel, this can contribute 468 // to resource (e.g. port) exhaustion in the echo servers. To avoid that problem, we disable 469 // converging by default, so long as the count is greater than the default converge value. 470 // This, of course, can be overridden if the user supplies their own converge value. 471 if o.Count > callConverge { 472 retryOpts = append(retryOpts, retry.Converge(1)) 473 } 474 475 // Now append user-provided options to override the defaults. 476 o.Retry.Options = append(retryOpts, o.Retry.Options...) 477 }