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 }