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 }