github.com/kiali/kiali@v1.84.0/business/checkers/gateways/multi_match_checker.go (about)

     1  package gateways
     2  
     3  import (
     4  	"regexp"
     5  	"strconv"
     6  	"strings"
     7  
     8  	api_networking_v1beta1 "istio.io/api/networking/v1beta1"
     9  	networking_v1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1"
    10  
    11  	"k8s.io/apimachinery/pkg/labels"
    12  
    13  	"github.com/kiali/kiali/config"
    14  	"github.com/kiali/kiali/models"
    15  	"github.com/kiali/kiali/util/intutil"
    16  )
    17  
    18  type MultiMatchChecker struct {
    19  	Cluster         string
    20  	Gateways        []*networking_v1beta1.Gateway
    21  	existingList    map[string][]Host
    22  	hostRegexpCache map[string]regexp.Regexp
    23  }
    24  
    25  const (
    26  	GatewayCheckerType     = "gateway"
    27  	wildCardMatch          = "*"
    28  	targetNamespaceAll     = "*"
    29  	targetNamespaceCurrent = "."
    30  )
    31  
    32  type Host struct {
    33  	Port            int
    34  	Hostname        string
    35  	Namespace       string
    36  	ServerIndex     int
    37  	HostIndex       int
    38  	GatewayRuleName string
    39  	TargetNamespace string
    40  }
    41  
    42  // Check validates that no two gateways share the same host+port combination
    43  func (m MultiMatchChecker) Check() models.IstioValidations {
    44  	validations := models.IstioValidations{}
    45  	m.existingList = map[string][]Host{}
    46  	m.hostRegexpCache = map[string]regexp.Regexp{}
    47  
    48  	for _, g := range m.Gateways {
    49  		gatewayRuleName := g.Name
    50  		gatewayNamespace := g.Namespace
    51  
    52  		selectorString := ""
    53  		if len(g.Spec.Selector) > 0 {
    54  			selectorString = labels.Set(g.Spec.Selector).String()
    55  		}
    56  		for i, server := range g.Spec.Servers {
    57  			if server == nil {
    58  				continue
    59  			}
    60  			hosts := parsePortAndHostnames(server)
    61  			for hi, host := range hosts {
    62  				host.ServerIndex = i
    63  				host.HostIndex = hi
    64  				host.GatewayRuleName = gatewayRuleName
    65  				host.Namespace = gatewayNamespace
    66  				// Hostname can be given in <target-namespace>/Hostname syntax
    67  				host.TargetNamespace = targetNamespaceAll
    68  				namespaceAndHost := strings.Split(host.Hostname, "/")
    69  				if len(namespaceAndHost) > 1 {
    70  					host.Hostname = namespaceAndHost[1]
    71  					host.TargetNamespace = namespaceAndHost[0]
    72  					// replace targetNamespaceCurrent with GW namespace to simplify duplicate checking
    73  					if host.TargetNamespace == targetNamespaceCurrent {
    74  						host.TargetNamespace = gatewayNamespace
    75  					}
    76  				}
    77  				duplicate, dhosts := m.findMatch(host, selectorString)
    78  				if duplicate {
    79  					// The above is referenced by each one below..
    80  					currentHostValidation := createError(host.GatewayRuleName, host.Namespace, m.Cluster, host.ServerIndex, host.HostIndex)
    81  					existingHosts := make(map[string]bool)
    82  					for i := 0; i < len(dhosts); i++ {
    83  						dh := dhosts[i]
    84  						// we skip CurrentHostValidation
    85  						// skip duplicate references when one gateway has several duplicate hosts
    86  						if (dh.Namespace == gatewayNamespace && dh.GatewayRuleName == gatewayRuleName) || existingHosts[dh.Namespace+"/"+dh.GatewayRuleName] {
    87  							continue
    88  						}
    89  						existingHosts[dh.Namespace+"/"+dh.GatewayRuleName] = true
    90  						refValidation := createError(dh.GatewayRuleName, dh.Namespace, m.Cluster, dh.ServerIndex, dh.HostIndex)
    91  						refValidation = refValidation.MergeReferences(currentHostValidation)
    92  						currentHostValidation = currentHostValidation.MergeReferences(refValidation)
    93  						validations = validations.MergeValidations(refValidation)
    94  					}
    95  					validations = validations.MergeValidations(currentHostValidation)
    96  				}
    97  				m.existingList[selectorString] = append(m.existingList[selectorString], host)
    98  			}
    99  		}
   100  	}
   101  
   102  	return validations
   103  }
   104  
   105  func createError(gatewayRuleName, namespace, cluster string, serverIndex, hostIndex int) models.IstioValidations {
   106  	key := models.IstioValidationKey{Name: gatewayRuleName, Namespace: namespace, ObjectType: GatewayCheckerType, Cluster: cluster}
   107  	checks := models.Build("gateways.multimatch",
   108  		"spec/servers["+strconv.Itoa(serverIndex)+"]/hosts["+strconv.Itoa(hostIndex)+"]")
   109  	rrValidation := &models.IstioValidation{
   110  		Name:       gatewayRuleName,
   111  		ObjectType: GatewayCheckerType,
   112  		Valid:      true,
   113  		Checks: []*models.IstioCheck{
   114  			&checks,
   115  		},
   116  	}
   117  
   118  	return models.IstioValidations{key: rrValidation}
   119  }
   120  
   121  func parsePortAndHostnames(serverDef *api_networking_v1beta1.Server) []Host {
   122  	var port int
   123  	if serverDef.Port != nil {
   124  		if n, e := intutil.Convert(serverDef.Port.Number); e == nil {
   125  			port = n
   126  		}
   127  	}
   128  	if len(serverDef.Hosts) > 0 {
   129  		hosts := make([]Host, 0, len(serverDef.Hosts))
   130  		for _, hostname := range serverDef.Hosts {
   131  			hosts = append(hosts, Host{
   132  				Port:     port,
   133  				Hostname: hostname,
   134  			})
   135  		}
   136  		return hosts
   137  	}
   138  	return nil
   139  }
   140  
   141  // findMatch uses a linear search with regexp to check for matching gateway host + port combinations. If this becomes a bottleneck for performance, replace with a graph or trie algorithm.
   142  func (m MultiMatchChecker) findMatch(host Host, selector string) (bool, []Host) {
   143  	duplicates := make([]Host, 0)
   144  	conf := config.Get()
   145  
   146  	for groupSelector, hostGroup := range m.existingList {
   147  		if groupSelector != selector {
   148  			continue
   149  		}
   150  
   151  		for _, h := range hostGroup {
   152  			// only compare hosts that share the target namespace or hosts where at least one of the pair is exported to all namespaces
   153  			if h.TargetNamespace == targetNamespaceAll || host.TargetNamespace == targetNamespaceAll || h.TargetNamespace == host.TargetNamespace {
   154  				if h.Port == host.Port {
   155  					// wildcardMatches will always match unless SkipWildcardGatewayHosts is set 'true'
   156  					if host.Hostname == wildCardMatch || h.Hostname == wildCardMatch {
   157  						if !conf.KialiFeatureFlags.Validations.SkipWildcardGatewayHosts {
   158  							duplicates = append(duplicates, host)
   159  							duplicates = append(duplicates, h)
   160  						}
   161  						continue
   162  					}
   163  
   164  					// DNS is case-insensitive
   165  					current := strings.ToLower(host.Hostname)
   166  					previous := strings.ToLower(h.Hostname)
   167  
   168  					// lazily compile hostname Regex
   169  					currentRegexp, ok := m.hostRegexpCache[current]
   170  					if !ok {
   171  						currentRegexp = *regexpFromHostname(current)
   172  						m.hostRegexpCache[current] = currentRegexp
   173  					}
   174  					previousRegexp, ok := m.hostRegexpCache[previous]
   175  					if !ok {
   176  						previousRegexp = *regexpFromHostname(previous)
   177  						m.hostRegexpCache[previous] = previousRegexp
   178  					}
   179  
   180  					if currentRegexp.MatchString(previous) ||
   181  						previousRegexp.MatchString(current) {
   182  						duplicates = append(duplicates, host)
   183  						duplicates = append(duplicates, h)
   184  						continue
   185  					}
   186  				}
   187  			}
   188  		}
   189  	}
   190  	return len(duplicates) > 0, duplicates
   191  }
   192  
   193  func regexpFromHostname(hostname string) *regexp.Regexp {
   194  	// Escaping dot chars for RegExp. Dot char means all possible chars.
   195  	// This protects this validation to false positive for (api-dev.example.com and api.dev.example.com)
   196  	escaped := strings.Replace(hostname, ".", "\\.", -1)
   197  
   198  	// We anchor the beginning and end of the string when it's
   199  	// to be used as a regex, so that we don't get spurious
   200  	// substring matches, e.g., "example.com" matching
   201  	// "foo.example.com".
   202  	anchored := strings.Join([]string{"^", escaped, "$"}, "")
   203  
   204  	return regexp.MustCompile(anchored)
   205  }