istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/test/framework/components/istio/ingress.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 istio
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"io"
    21  	"net"
    22  	"net/netip"
    23  	"strconv"
    24  	"time"
    25  
    26  	"k8s.io/apimachinery/pkg/types"
    27  
    28  	"istio.io/istio/pkg/http/headers"
    29  	"istio.io/istio/pkg/test"
    30  	"istio.io/istio/pkg/test/echo/common/scheme"
    31  	"istio.io/istio/pkg/test/framework/components/cluster"
    32  	"istio.io/istio/pkg/test/framework/components/echo"
    33  	"istio.io/istio/pkg/test/framework/components/echo/common"
    34  	"istio.io/istio/pkg/test/framework/components/environment/kube"
    35  	"istio.io/istio/pkg/test/framework/components/istio/ingress"
    36  	"istio.io/istio/pkg/test/framework/resource"
    37  	"istio.io/istio/pkg/test/scopes"
    38  	"istio.io/istio/pkg/test/util/retry"
    39  )
    40  
    41  const (
    42  	defaultIngressIstioNameLabel = "ingressgateway"
    43  	defaultIngressIstioLabel     = "istio=" + defaultIngressIstioNameLabel
    44  	defaultIngressServiceName    = "istio-" + defaultIngressIstioNameLabel
    45  
    46  	discoveryPort = 15012
    47  )
    48  
    49  var (
    50  	getAddressTimeout = retry.Timeout(3 * time.Minute)
    51  	getAddressDelay   = retry.BackoffDelay(500 * time.Millisecond)
    52  
    53  	_ ingress.Instance = &ingressImpl{}
    54  	_ io.Closer        = &ingressImpl{}
    55  )
    56  
    57  type ingressConfig struct {
    58  	// Service is the kubernetes Service name for the cluster
    59  	Service types.NamespacedName
    60  	// LabelSelector is the value for the label on the ingress kubernetes objects
    61  	LabelSelector string
    62  
    63  	// Cluster to be used in a multicluster environment
    64  	Cluster cluster.Cluster
    65  }
    66  
    67  func newIngress(ctx resource.Context, cfg ingressConfig) (i ingress.Instance) {
    68  	if cfg.LabelSelector == "" {
    69  		cfg.LabelSelector = defaultIngressIstioLabel
    70  	}
    71  	c := &ingressImpl{
    72  		service:       cfg.Service,
    73  		labelSelector: cfg.LabelSelector,
    74  		env:           ctx.Environment().(*kube.Environment),
    75  		cluster:       ctx.Clusters().GetOrDefault(cfg.Cluster),
    76  		caller:        common.NewCaller(),
    77  	}
    78  	return c
    79  }
    80  
    81  type ingressImpl struct {
    82  	service       types.NamespacedName
    83  	labelSelector string
    84  
    85  	env     *kube.Environment
    86  	cluster cluster.Cluster
    87  	caller  *common.Caller
    88  }
    89  
    90  func (c *ingressImpl) Close() error {
    91  	return c.caller.Close()
    92  }
    93  
    94  // getAddressesInner returns the external addresses for the given port. When we don't have support for LoadBalancer,
    95  // the returned list will contain will have the externally reachable NodePort address and port.
    96  func (c *ingressImpl) getAddressesInner(port int) ([]string, []int, error) {
    97  	attempts := 0
    98  	remoteAddrs, err := retry.UntilComplete(func() (addrs any, completed bool, err error) {
    99  		attempts++
   100  		addrs, completed, err = getRemoteServiceAddresses(c.env.Settings(), c.cluster, c.service.Namespace, c.labelSelector, c.service.Name, port)
   101  		if err != nil && attempts > 1 {
   102  			// Log if we fail more than once to avoid test appearing to hang
   103  			// LB provision be slow, so timeout here needs to be long we should give context
   104  			scopes.Framework.Warnf("failed to get address for port %v: %v", port, err)
   105  		}
   106  		return
   107  	}, getAddressTimeout, getAddressDelay)
   108  	var anyRemoteAddrs []interface{}
   109  	// Perform type assertion and construct a new slice of `any`
   110  	anyRemoteAddrs, _ = remoteAddrs.([]any)
   111  
   112  	if err != nil {
   113  		return nil, nil, err
   114  	}
   115  	var addrs []string
   116  	var ports []int
   117  	for _, addr := range anyRemoteAddrs {
   118  		switch v := addr.(type) {
   119  		case string:
   120  			host, portStr, err := net.SplitHostPort(v)
   121  			if err != nil {
   122  				return nil, nil, err
   123  			}
   124  			mappedPort, err := strconv.Atoi(portStr)
   125  			if err != nil {
   126  				return nil, nil, err
   127  			}
   128  			addrs = append(addrs, host)
   129  			ports = append(ports, mappedPort)
   130  		case netip.AddrPort:
   131  			addrs = append(addrs, v.Addr().String())
   132  			ports = append(ports, int(v.Port()))
   133  		}
   134  	}
   135  	if len(addrs) > 0 {
   136  		return addrs, ports, nil
   137  	}
   138  
   139  	return nil, nil, fmt.Errorf("failed to get address for port %v", port)
   140  }
   141  
   142  // AddressForPort returns the externally reachable host and port of the component for the given port.
   143  func (c *ingressImpl) AddressesForPort(port int) ([]string, []int) {
   144  	addrs, ports, err := c.getAddressesInner(port)
   145  	if err != nil {
   146  		scopes.Framework.Error(err)
   147  		return nil, nil
   148  	}
   149  	return addrs, ports
   150  }
   151  
   152  func (c *ingressImpl) Cluster() cluster.Cluster {
   153  	return c.cluster
   154  }
   155  
   156  // HTTPAddresses returns the externally reachable HTTP hosts and port (80) of the component.
   157  func (c *ingressImpl) HTTPAddresses() ([]string, []int) {
   158  	return c.AddressesForPort(80)
   159  }
   160  
   161  // TCPAddresses returns the externally reachable TCP hosts and port (31400) of the component.
   162  func (c *ingressImpl) TCPAddresses() ([]string, []int) {
   163  	return c.AddressesForPort(31400)
   164  }
   165  
   166  // HTTPSAddresses returns the externally reachable TCP hosts and port (443) of the component.
   167  func (c *ingressImpl) HTTPSAddresses() ([]string, []int) {
   168  	return c.AddressesForPort(443)
   169  }
   170  
   171  // DiscoveryAddresses returns the externally reachable discovery addresses (15012) of the component.
   172  func (c *ingressImpl) DiscoveryAddresses() []netip.AddrPort {
   173  	hosts, ports := c.AddressesForPort(discoveryPort)
   174  	var addrs []netip.AddrPort
   175  	if hosts == nil {
   176  		return []netip.AddrPort{{}}
   177  	}
   178  	for i, host := range hosts {
   179  		ip, err := netip.ParseAddr(host)
   180  		if err != nil {
   181  			return []netip.AddrPort{}
   182  		}
   183  		addrs = append(addrs, netip.AddrPortFrom(ip, uint16(ports[i])))
   184  	}
   185  
   186  	return addrs
   187  }
   188  
   189  func (c *ingressImpl) Call(options echo.CallOptions) (echo.CallResult, error) {
   190  	return c.callEcho(options)
   191  }
   192  
   193  func (c *ingressImpl) CallOrFail(t test.Failer, options echo.CallOptions) echo.CallResult {
   194  	t.Helper()
   195  	resp, err := c.Call(options)
   196  	if err != nil {
   197  		t.Fatal(err)
   198  	}
   199  	return resp
   200  }
   201  
   202  func (c *ingressImpl) callEcho(opts echo.CallOptions) (echo.CallResult, error) {
   203  	var (
   204  		addr string
   205  		port int
   206  	)
   207  	opts = opts.DeepCopy()
   208  	var addrs []string
   209  	var ports []int
   210  	if opts.Port.ServicePort == 0 {
   211  		s, err := c.schemeFor(opts)
   212  		if err != nil {
   213  			return echo.CallResult{}, err
   214  		}
   215  		opts.Scheme = s
   216  
   217  		// Default port based on protocol
   218  		switch s {
   219  		case scheme.HTTP:
   220  			addrs, ports = c.HTTPAddresses()
   221  		case scheme.HTTPS:
   222  			addrs, ports = c.HTTPSAddresses()
   223  		case scheme.TCP:
   224  			addrs, ports = c.TCPAddresses()
   225  		default:
   226  			return echo.CallResult{}, fmt.Errorf("ingress: scheme %v not supported. Options: %v+", s, opts)
   227  		}
   228  	} else {
   229  		addrs, ports = c.AddressesForPort(opts.Port.ServicePort)
   230  	}
   231  	if addrs == nil || ports == nil {
   232  		scopes.Framework.Warnf("failed to get host and port for %s/%d", opts.Port.Protocol, opts.Port.ServicePort)
   233  	}
   234  	addr = addrs[0]
   235  	port = ports[0]
   236  	// Even if they set ServicePort, when load balancer is disabled, we may need to switch to NodePort, so replace it.
   237  	opts.Port.ServicePort = port
   238  	if opts.HTTP.Headers == nil {
   239  		opts.HTTP.Headers = map[string][]string{}
   240  	}
   241  	if host := opts.GetHost(); len(host) > 0 {
   242  		opts.HTTP.Headers.Set(headers.Host, host)
   243  	}
   244  	// Default address based on port
   245  	opts.Address = addr
   246  	if len(c.cluster.HTTPProxy()) > 0 && !c.cluster.ProxyKubectlOnly() {
   247  		opts.HTTP.HTTPProxy = c.cluster.HTTPProxy()
   248  	}
   249  	return c.caller.CallEcho(c, opts)
   250  }
   251  
   252  func (c *ingressImpl) schemeFor(opts echo.CallOptions) (scheme.Instance, error) {
   253  	if opts.Scheme == "" && opts.Port.Protocol == "" {
   254  		return "", fmt.Errorf("must provide either protocol or scheme")
   255  	}
   256  
   257  	if opts.Scheme != "" {
   258  		return opts.Scheme, nil
   259  	}
   260  
   261  	return opts.Port.Scheme()
   262  }
   263  
   264  func (c *ingressImpl) PodID(i int) (string, error) {
   265  	pods, err := c.env.Clusters().Default().PodsForSelector(context.TODO(), c.service.Namespace, c.labelSelector)
   266  	if err != nil {
   267  		return "", fmt.Errorf("unable to get ingressImpl gateway stats: %v", err)
   268  	}
   269  	if i < 0 || i >= len(pods.Items) {
   270  		return "", fmt.Errorf("pod index out of boundary (%d): %d", len(pods.Items), i)
   271  	}
   272  	return pods.Items[i].Name, nil
   273  }
   274  
   275  func (c *ingressImpl) Namespace() string {
   276  	return c.service.Namespace
   277  }