istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/config/kube/gateway/context.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 gateway
    16  
    17  import (
    18  	"fmt"
    19  	"sort"
    20  	"strconv"
    21  	"strings"
    22  
    23  	corev1 "k8s.io/api/core/v1"
    24  
    25  	networking "istio.io/api/networking/v1alpha3"
    26  	"istio.io/istio/pilot/pkg/model"
    27  	"istio.io/istio/pkg/cluster"
    28  	"istio.io/istio/pkg/config/constants"
    29  	"istio.io/istio/pkg/config/host"
    30  	"istio.io/istio/pkg/util/sets"
    31  )
    32  
    33  // GatewayContext contains a minimal subset of push context functionality to be exposed to GatewayAPIControllers
    34  type GatewayContext struct {
    35  	ps      *model.PushContext
    36  	cluster cluster.ID
    37  }
    38  
    39  func NewGatewayContext(ps *model.PushContext, cluster cluster.ID) GatewayContext {
    40  	return GatewayContext{ps, cluster}
    41  }
    42  
    43  // ResolveGatewayInstances attempts to resolve all instances that a gateway will be exposed on.
    44  // Note: this function considers *all* instances of the service; its possible those instances will not actually be properly functioning
    45  // gateways, so this is not 100% accurate, but sufficient to expose intent to users.
    46  // The actual configuration generation is done on a per-workload basis and will get the exact set of matched instances for that workload.
    47  // Four sets are exposed:
    48  // * Internal addresses (eg istio-ingressgateway.istio-system.svc.cluster.local:80).
    49  // * Internal IP addresses (eg 1.2.3.4). This comes from ClusterIP.
    50  // * External addresses (eg 1.2.3.4), this comes from LoadBalancer services. There may be multiple in some cases (especially multi cluster).
    51  // * Pending addresses (eg istio-ingressgateway.istio-system.svc), are LoadBalancer-type services with pending external addresses.
    52  // * Warnings for references that could not be resolved. These are intended to be user facing.
    53  func (gc GatewayContext) ResolveGatewayInstances(
    54  	namespace string,
    55  	gwsvcs []string,
    56  	servers []*networking.Server,
    57  ) (internal, internalIP, external, pending, warns []string, allUsable bool) {
    58  	ports := map[int]struct{}{}
    59  	for _, s := range servers {
    60  		ports[int(s.Port.Number)] = struct{}{}
    61  	}
    62  	foundInternal := sets.New[string]()
    63  	foundInternalIP := sets.New[string]()
    64  	foundExternal := sets.New[string]()
    65  	foundPending := sets.New[string]()
    66  	warnings := []string{}
    67  	foundUnusable := false
    68  	log.Debugf("Resolving gateway instances for %v in namespace %s", gwsvcs, namespace)
    69  	for _, g := range gwsvcs {
    70  		svc, f := gc.ps.ServiceIndex.HostnameAndNamespace[host.Name(g)][namespace]
    71  		if !f {
    72  			otherNamespaces := []string{}
    73  			for ns := range gc.ps.ServiceIndex.HostnameAndNamespace[host.Name(g)] {
    74  				otherNamespaces = append(otherNamespaces, `"`+ns+`"`) // Wrap in quotes for output
    75  			}
    76  			if len(otherNamespaces) > 0 {
    77  				sort.Strings(otherNamespaces)
    78  				warnings = append(warnings, fmt.Sprintf("hostname %q not found in namespace %q, but it was found in namespace(s) %v",
    79  					g, namespace, strings.Join(otherNamespaces, ", ")))
    80  			} else {
    81  				warnings = append(warnings, fmt.Sprintf("hostname %q not found", g))
    82  			}
    83  			foundUnusable = true
    84  			continue
    85  		}
    86  		svcKey := svc.Key()
    87  		for port := range ports {
    88  			instances := gc.ps.ServiceEndpointsByPort(svc, port, nil)
    89  			if len(instances) > 0 {
    90  				foundInternal.Insert(fmt.Sprintf("%s:%d", g, port))
    91  				foundInternalIP.InsertAll(svc.GetAddresses(&model.Proxy{Metadata: &model.NodeMetadata{ClusterID: gc.cluster}})...)
    92  				if svc.Attributes.ClusterExternalAddresses.Len() > 0 {
    93  					// Fetch external IPs from all clusters
    94  					svc.Attributes.ClusterExternalAddresses.ForEach(func(c cluster.ID, externalIPs []string) {
    95  						foundExternal.InsertAll(externalIPs...)
    96  					})
    97  				} else if corev1.ServiceType(svc.Attributes.Type) == corev1.ServiceTypeLoadBalancer {
    98  					if !foundPending.Contains(g) {
    99  						warnings = append(warnings, fmt.Sprintf("address pending for hostname %q", g))
   100  						foundPending.Insert(g)
   101  					}
   102  				}
   103  			} else {
   104  				instancesByPort := gc.ps.ServiceEndpoints(svcKey)
   105  				if instancesEmpty(instancesByPort) {
   106  					warnings = append(warnings, fmt.Sprintf("no instances found for hostname %q", g))
   107  				} else {
   108  					hintPort := sets.New[string]()
   109  					for servicePort, instances := range instancesByPort {
   110  						for _, i := range instances {
   111  							if i.EndpointPort == uint32(port) {
   112  								hintPort.Insert(strconv.Itoa(servicePort))
   113  							}
   114  						}
   115  					}
   116  					if hintPort.Len() > 0 {
   117  						warnings = append(warnings, fmt.Sprintf(
   118  							"port %d not found for hostname %q (hint: the service port should be specified, not the workload port. Did you mean one of these ports: %v?)",
   119  							port, g, sets.SortedList(hintPort)))
   120  						foundUnusable = true
   121  					} else {
   122  						_, isManaged := svc.Attributes.Labels[constants.ManagedGatewayLabel]
   123  						var portExistsOnService bool
   124  						for _, p := range svc.Ports {
   125  							if p.Port == port {
   126  								portExistsOnService = true
   127  								break
   128  							}
   129  						}
   130  						// If this is a managed gateway, the only possible explanation for no instances for the port
   131  						// is a delay in endpoint sync. Therefore, we don't want to warn/change the Programmed condition
   132  						// in this case as long as the port exists on the `Service` object.
   133  						if !isManaged || !portExistsOnService {
   134  							warnings = append(warnings, fmt.Sprintf("port %d not found for hostname %q", port, g))
   135  							foundUnusable = true
   136  						}
   137  					}
   138  				}
   139  			}
   140  		}
   141  	}
   142  	sort.Strings(warnings)
   143  	return sets.SortedList(foundInternal), sets.SortedList(foundInternalIP), sets.SortedList(foundExternal), sets.SortedList(foundPending),
   144  		warnings, !foundUnusable
   145  }
   146  
   147  func (gc GatewayContext) GetService(hostname, namespace string) *model.Service {
   148  	return gc.ps.ServiceIndex.HostnameAndNamespace[host.Name(hostname)][namespace]
   149  }
   150  
   151  func instancesEmpty(m map[int][]*model.IstioEndpoint) bool {
   152  	for _, instances := range m {
   153  		if len(instances) > 0 {
   154  			return false
   155  		}
   156  	}
   157  	return true
   158  }