blob: 302950dffff5588ab10990290a0a4a408c947d53 [file] [log] [blame]
// Copyright Istio Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package echo
import (
"errors"
"fmt"
"net/http"
"time"
)
import (
wrappers "google.golang.org/protobuf/types/known/wrapperspb"
)
import (
"github.com/apache/dubbo-go-pixiu/pkg/http/headers"
"github.com/apache/dubbo-go-pixiu/pkg/test"
"github.com/apache/dubbo-go-pixiu/pkg/test/echo/common"
"github.com/apache/dubbo-go-pixiu/pkg/test/echo/common/scheme"
"github.com/apache/dubbo-go-pixiu/pkg/test/util/retry"
)
// HTTP settings
type HTTP struct {
// If true, h2c will be used in HTTP requests
HTTP2 bool
// If true, HTTP/3 request over QUIC will be used.
// It is mandatory to specify TLS settings
HTTP3 bool
// Path specifies the URL path for the HTTP(s) request.
Path string
// Method to send. Defaults to GET.
Method string
// Headers indicates headers that should be sent in the request. Ignored for WebSocket calls.
// If no Host header is provided, a default will be chosen for the target service endpoint.
Headers http.Header
// FollowRedirects will instruct the call to follow 301 redirects. Otherwise, the original 301 response
// is returned directly.
FollowRedirects bool
// HTTProxy used for making ingress echo call via proxy
HTTPProxy string
}
// TLS settings
type TLS struct {
// Use the custom certificate to make the call. This is mostly used to make mTLS request directly
// (without proxy) from naked client to test certificates issued by custom CA instead of the Istio self-signed CA.
Cert, Key, CaCert string
// Use the custom certificates file to make the call.
CertFile, KeyFile, CaCertFile string
// Skip verify peer's certificate.
InsecureSkipVerify bool
Alpn []string
ServerName string
}
// Retry settings
type Retry struct {
// NoRetry if true, no retry will be attempted.
NoRetry bool
// Options to be used when retrying. If not specified, defaults will be used.
Options []retry.Option
}
// TCP settings
type TCP struct {
// ExpectedResponse asserts this is in the response for TCP requests.
ExpectedResponse *wrappers.StringValue
}
// Target of a call.
type Target interface {
Configurable
WorkloadContainer
// Instances in this target.
Instances() Instances
}
// CallOptions defines options for calling a Endpoint.
type CallOptions struct {
// To is the Target to be called.
To Target
// ToWorkload will call a specific workload in this instance, rather than the Service.
// If there are multiple workloads in the Instance, the first is used.
// Can be used with `ToWorkload: to.WithWorkloads(someWl)` to send to a specific workload.
// When using the Port field, the ServicePort should be used.
ToWorkload Instance
// Port to be used for the call. Ignored if Scheme == DNS. If the Port.ServicePort is set,
// either Port.Protocol or Scheme must also be set. If Port.ServicePort is not set,
// the port is looked up in To by either Port.Name or Port.Protocol.
Port Port
// Scheme to be used when making the call. If not provided, the Scheme will be selected
// based on the Port.Protocol.
Scheme scheme.Instance
// Address specifies the host name or IP address to be used on the request. If not provided,
// an appropriate default is chosen for To.
Address string
// Count indicates the number of exchanges that should be made with the service endpoint.
// If Count <= 0, defaults to 1.
Count int
// Timeout used for each individual request. Must be > 0, otherwise 5 seconds is used.
Timeout time.Duration
// Retry options for the call.
Retry Retry
// HTTP settings.
HTTP HTTP
// TCP settings.
TCP TCP
// TLS settings.
TLS TLS
// Message to be sent.
Message string
// Check the server responses. If none is provided, only the number of responses received
// will be checked.
Check Checker
}
// GetHost returns the best default host for the call. Returns the first host defined from the following
// sources (in order of precedence): Host header, target's DefaultHostHeader, Address, target's FQDN.
func (o CallOptions) GetHost() string {
// First, use the host header, if specified.
if h := o.HTTP.Headers.Get(headers.Host); len(h) > 0 {
return h
}
// Next use the target's default, if specified.
if o.To != nil && len(o.To.Config().DefaultHostHeader) > 0 {
return o.To.Config().DefaultHostHeader
}
// Next, if the Address was manually specified use it as the Host.
if len(o.Address) > 0 {
return o.Address
}
// Finally, use the target's FQDN.
if o.To != nil {
return o.To.Config().ClusterLocalFQDN()
}
return ""
}
func (o CallOptions) DeepCopy() CallOptions {
clone := o
if o.TLS.Alpn != nil {
clone.TLS.Alpn = make([]string, len(o.TLS.Alpn))
copy(clone.TLS.Alpn, o.TLS.Alpn)
}
return clone
}
// FillDefaults fills out any defaults that haven't been explicitly specified.
func (o *CallOptions) FillDefaults() error {
// Fill in the address if not set.
if err := o.fillAddress(); err != nil {
return err
}
// Fill in the port if not set or the service port is missing.
if err := o.fillPort(); err != nil {
return err
}
// Fill in the scheme if not set, using the port information.
if err := o.fillScheme(); err != nil {
return err
}
// Fill in HTTP headers
o.fillHeaders()
if o.Timeout <= 0 {
o.Timeout = common.DefaultRequestTimeout
}
if o.Count <= 0 {
o.Count = common.DefaultCount
}
// Add any user-specified options after the default options (last option wins for each type of option).
o.Retry.Options = append(append([]retry.Option{}, DefaultCallRetryOptions()...), o.Retry.Options...)
// If no Check was specified, assume no error.
if o.Check == nil {
o.Check = NoChecker()
}
return nil
}
// FillDefaultsOrFail calls FillDefaults and fails if an error occurs.
func (o *CallOptions) FillDefaultsOrFail(t test.Failer) {
t.Helper()
if err := o.FillDefaults(); err != nil {
t.Fatal(err)
}
}
func (o *CallOptions) fillAddress() error {
if o.Address == "" {
if o.To != nil {
// No host specified, use the fully qualified domain name for the service.
o.Address = o.To.Config().ClusterLocalFQDN()
return nil
}
if o.ToWorkload != nil {
wl, err := o.ToWorkload.Workloads()
if err != nil {
return err
}
o.Address = wl[0].Address()
return nil
}
return errors.New("if address is not set, then To must be set")
}
return nil
}
func (o *CallOptions) fillPort() error {
if o.Scheme == scheme.DNS {
// Port is not used for DNS.
return nil
}
if o.Port.ServicePort > 0 {
if o.Port.Protocol == "" && o.Scheme == "" {
return errors.New("callOptions: servicePort specified, but no protocol or scheme was set")
}
// The service port was set explicitly. Nothing else to do.
return nil
}
if o.To != nil {
return o.fillPort2(o.To)
} else if o.ToWorkload != nil {
err := o.fillPort2(o.ToWorkload)
if err != nil {
return err
}
// Set the ServicePort to workload port since we are not reaching it through the Service
p := o.Port
p.ServicePort = p.WorkloadPort
o.Port = p
}
if o.Port.ServicePort <= 0 || (o.Port.Protocol == "" && o.Scheme == "") || o.Address == "" {
return fmt.Errorf("if target is not set, then port.servicePort, port.protocol or schema, and address must be set")
}
return nil
}
func (o *CallOptions) fillPort2(target Target) error {
servicePorts := target.Config().Ports.GetServicePorts()
if o.Port.Name != "" {
// Look up the port by name.
p, found := servicePorts.ForName(o.Port.Name)
if !found {
return fmt.Errorf("callOptions: no port named %s available in To Instance", o.Port.Name)
}
o.Port = p
return nil
}
if o.Port.Protocol != "" {
// Look up the port by protocol.
p, found := servicePorts.ForProtocol(o.Port.Protocol)
if !found {
return fmt.Errorf("callOptions: no port for protocol %s available in To Instance", o.Port.Protocol)
}
o.Port = p
return nil
}
if o.Port.ServicePort != 0 {
// We just have a single port number, populate the rest of the fields
p, found := servicePorts.ForServicePort(o.Port.ServicePort)
if !found {
return fmt.Errorf("callOptions: no port %d available in To Instance", o.Port.ServicePort)
}
o.Port = p
return nil
}
return nil
}
func (o *CallOptions) fillScheme() error {
if o.Scheme == "" {
// No protocol, fill it in.
var err error
if o.Scheme, err = o.Port.Scheme(); err != nil {
return err
}
}
return nil
}
func (o *CallOptions) fillHeaders() {
if o.ToWorkload != nil {
return
}
// Initialize the headers and add a default Host header if none provided.
if o.HTTP.Headers == nil {
o.HTTP.Headers = make(http.Header)
} else {
// Avoid mutating input, which can lead to concurrent writes
o.HTTP.Headers = o.HTTP.Headers.Clone()
}
if h := o.GetHost(); len(h) > 0 {
o.HTTP.Headers.Set(headers.Host, h)
}
}