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  }