istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/model/sidecar.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 model 16 17 import ( 18 "encoding/json" 19 "sort" 20 "strings" 21 22 "k8s.io/apimachinery/pkg/types" 23 24 networking "istio.io/api/networking/v1alpha3" 25 "istio.io/istio/pilot/pkg/features" 26 "istio.io/istio/pilot/pkg/serviceregistry/provider" 27 "istio.io/istio/pkg/config" 28 "istio.io/istio/pkg/config/constants" 29 "istio.io/istio/pkg/config/host" 30 "istio.io/istio/pkg/config/labels" 31 "istio.io/istio/pkg/config/protocol" 32 "istio.io/istio/pkg/config/schema/kind" 33 "istio.io/istio/pkg/maps" 34 "istio.io/istio/pkg/slices" 35 "istio.io/istio/pkg/util/sets" 36 ) 37 38 const ( 39 wildcardNamespace = "*" 40 currentNamespace = "." 41 wildcardService = host.Name("*") 42 ) 43 44 var ( 45 sidecarScopedKnownConfigTypes = sets.New( 46 kind.ServiceEntry, 47 kind.VirtualService, 48 kind.DestinationRule, 49 kind.Sidecar, 50 51 kind.HTTPRoute, 52 kind.TCPRoute, 53 kind.TLSRoute, 54 kind.GRPCRoute, 55 ) 56 57 // clusterScopedKnownConfigTypes includes configs when they are in root namespace, 58 // they will be applied to all namespaces within the cluster. 59 clusterScopedKnownConfigTypes = sets.New( 60 kind.EnvoyFilter, 61 kind.AuthorizationPolicy, 62 kind.RequestAuthentication, 63 kind.WasmPlugin, 64 ) 65 ) 66 67 type hostClassification struct { 68 exactHosts sets.Set[host.Name] 69 allHosts []host.Name 70 } 71 72 // Matches checks if the hostClassification(sidecar egress hosts) matches the Service's hostname 73 func (hc hostClassification) Matches(h host.Name) bool { 74 // exact lookup is fast, so check that first 75 if hc.exactHosts.Contains(h) { 76 return true 77 } 78 79 // exactHosts not found, fallback to loop allHosts 80 hIsWildCarded := h.IsWildCarded() 81 for _, importedHost := range hc.allHosts { 82 // If both are exact hosts, then fallback is not needed. 83 // In this scenario it should be determined by exact lookup. 84 if !hIsWildCarded && !importedHost.IsWildCarded() { 85 continue 86 } 87 // Check if the hostnames match per usual hostname matching rules 88 if h.SubsetOf(importedHost) { 89 return true 90 } 91 } 92 return false 93 } 94 95 // VSMatches checks if the hostClassification(sidecar egress hosts) matches the VirtualService's host 96 func (hc hostClassification) VSMatches(vsHost host.Name, useGatewaySemantics bool) bool { 97 // first, check exactHosts 98 if hc.exactHosts.Contains(vsHost) { 99 return true 100 } 101 102 // exactHosts not found, fallback to loop allHosts 103 hIsWildCard := vsHost.IsWildCarded() 104 for _, importedHost := range hc.allHosts { 105 // If both are exact hosts, then fallback is not needed. 106 // In this scenario it should be determined by exact lookup. 107 if !hIsWildCard && !importedHost.IsWildCarded() { 108 continue 109 } 110 111 var match bool 112 if useGatewaySemantics { 113 // The new way. Matching logic exactly mirrors Service matching 114 // If a route defines `*.com` and we import `a.com`, it will not match 115 match = vsHost.SubsetOf(importedHost) 116 } else { 117 // The old way. We check Matches which is bi-directional. This is for backwards compatibility 118 match = vsHost.Matches(importedHost) 119 } 120 if match { 121 return true 122 } 123 } 124 return false 125 } 126 127 // SidecarScope is a wrapper over the Sidecar resource with some 128 // preprocessed data to determine the list of services, virtualServices, 129 // and destinationRules that are accessible to a given 130 // sidecar. Precomputing the list of services, virtual services, dest rules 131 // for a sidecar improves performance as we no longer need to compute this 132 // list for every sidecar. We simply have to match a sidecar to a 133 // SidecarScope. Note that this is not the same as public/private scoped 134 // services. The list of services seen by every sidecar scope (namespace 135 // wide or per workload) depends on the imports, the listeners, and other 136 // settings. 137 // 138 // Every proxy workload of SidecarProxy type will always map to a 139 // SidecarScope object. If the proxy's namespace does not have a user 140 // specified Sidecar CRD, we will construct one that has a catch all egress 141 // listener that imports every public service/virtualService in the mesh. 142 type SidecarScope struct { 143 Name string 144 // This is the namespace where the sidecar takes effect, 145 // maybe different from the ns where sidecar resides if sidecar is in root ns. 146 Namespace string 147 // The cr itself. Can be nil if we are constructing the default 148 // sidecar scope 149 Sidecar *networking.Sidecar 150 151 // Version this sidecar was computed for 152 Version string 153 154 // Set of egress listeners, and their associated services. A sidecar 155 // scope should have either ingress/egress listeners or both. For 156 // every proxy workload that maps to a sidecar API object (or the 157 // default object), we will go through every egress listener in the 158 // object and process the Envoy listener or RDS based on the imported 159 // services/virtual services in that listener. 160 EgressListeners []*IstioEgressListenerWrapper 161 162 // Union of services imported across all egress listeners for use by CDS code. 163 services []*Service 164 servicesByHostname map[host.Name]*Service 165 166 // Destination rules imported across all egress listeners. This 167 // contains the computed set based on public/private destination rules 168 // as well as the inherited ones, in addition to the wildcard matches 169 // such as *.com applying to foo.bar.com. Each hostname in this map 170 // corresponds to a service in the services array above. When computing 171 // CDS, we simply have to find the matching service and return the 172 // destination rule. 173 destinationRules map[host.Name][]*ConsolidatedDestRule 174 destinationRulesByNames map[types.NamespacedName]*config.Config 175 176 // OutboundTrafficPolicy defines the outbound traffic policy for this sidecar. 177 // If OutboundTrafficPolicy is ALLOW_ANY traffic to unknown destinations will 178 // be forwarded. 179 OutboundTrafficPolicy *networking.OutboundTrafficPolicy 180 181 // Set of known configs this sidecar depends on. 182 // This field will be used to determine the config/resource scope 183 // which means which config changes will affect the proxies within this scope. 184 configDependencies sets.Set[ConfigHash] 185 } 186 187 // MarshalJSON implements json.Marshaller 188 func (sc *SidecarScope) MarshalJSON() ([]byte, error) { 189 // Json cannot expose unexported fields, so copy the ones we want here 190 return json.MarshalIndent(map[string]any{ 191 "version": sc.Version, 192 "name": sc.Name, 193 "namespace": sc.Namespace, 194 "outboundTrafficPolicy": sc.OutboundTrafficPolicy, 195 "services": sc.services, 196 "servicesByHostname": sc.servicesByHostname, 197 "sidecar": sc.Sidecar, 198 "destinationRules": sc.destinationRules, 199 }, "", " ") 200 } 201 202 // IstioEgressListenerWrapper is a wrapper for 203 // networking.IstioEgressListener object. The wrapper provides performance 204 // optimizations as it allows us to precompute and store the list of 205 // services/virtualServices that apply to this listener. 206 type IstioEgressListenerWrapper struct { 207 // The actual IstioEgressListener api object from the Config. It can be 208 // nil if this is for the default sidecar scope. 209 IstioListener *networking.IstioEgressListener 210 211 // Specifies whether matching ports is required. 212 matchPort bool 213 214 // List of services imported by this egress listener above. 215 // This will be used by LDS and RDS code when 216 // building the set of virtual hosts or the tcp filterchain matches for 217 // a given listener port. Two listeners, on user specified ports or 218 // unix domain sockets could have completely different sets of 219 // services. So a global list of services per sidecar scope will be 220 // incorrect. Hence the per listener set of services. 221 services []*Service 222 223 // List of virtual services imported by this egress listener above. 224 // As with per listener services, this 225 // will be used by RDS code to compute the virtual host configs for 226 // http listeners, as well as by TCP/TLS filter code to compute the 227 // service routing configs and the filter chain matches. We need a 228 // virtualService set per listener and not one per sidecarScope because 229 // each listener imports an independent set of virtual services. 230 // Listener 1 could import a public virtual service for serviceA from 231 // namespace A that has some path rewrite, while listener2 could import 232 // a private virtual service for serviceA from the local namespace, 233 // with a different path rewrite or no path rewrites. 234 virtualServices []config.Config 235 236 // An index of hostname to the namespaced name of the VirtualService containing the most 237 // relevant host match. Depending on the `PERSIST_OLDEST_FIRST_HEURISTIC_FOR_VIRTUAL_SERVICE_HOST_MATCHING` 238 // feature flag, it could be the most specific host match or the oldest host match. 239 mostSpecificWildcardVsIndex map[host.Name]types.NamespacedName 240 } 241 242 const defaultSidecar = "default-sidecar" 243 244 // DefaultSidecarScopeForGateway builds a SidecarScope contains services and destinationRules for a given gateway/waypoint. 245 func DefaultSidecarScopeForGateway(ps *PushContext, configNamespace string) *SidecarScope { 246 services := ps.servicesExportedToNamespace(configNamespace) 247 out := &SidecarScope{ 248 Name: defaultSidecar, 249 Namespace: configNamespace, 250 destinationRules: make(map[host.Name][]*ConsolidatedDestRule), 251 destinationRulesByNames: make(map[types.NamespacedName]*config.Config), 252 servicesByHostname: make(map[host.Name]*Service, len(services)), 253 Version: ps.PushVersion, 254 } 255 256 servicesAdded := make(map[host.Name]sidecarServiceIndex) 257 for _, s := range services { 258 out.appendSidecarServices(servicesAdded, s) 259 } 260 261 // Now that we have all the services that sidecars using this scope (in 262 // this config namespace) will see, identify all the destinationRules 263 // that these services need 264 for _, s := range out.services { 265 if dr := ps.destinationRule(configNamespace, s); dr != nil { 266 out.destinationRules[s.Hostname] = dr 267 for _, cdr := range dr { 268 for _, from := range cdr.from { 269 out.destinationRulesByNames[from] = cdr.rule 270 } 271 } 272 } 273 } 274 275 // waypoint need to get vses from the egress listener 276 defaultEgressListener := &IstioEgressListenerWrapper{ 277 virtualServices: ps.VirtualServicesForGateway(configNamespace, constants.IstioMeshGateway), 278 } 279 out.EgressListeners = []*IstioEgressListenerWrapper{defaultEgressListener} 280 281 return out 282 } 283 284 // DefaultSidecarScopeForNamespace is a sidecar scope object with a default catch all egress listener 285 // that matches the default Istio behavior: a sidecar has listeners for all services in the mesh 286 // We use this scope when the user has not set any sidecar Config for a given config namespace. 287 func DefaultSidecarScopeForNamespace(ps *PushContext, configNamespace string) *SidecarScope { 288 defaultEgressListener := &IstioEgressListenerWrapper{ 289 IstioListener: &networking.IstioEgressListener{ 290 Hosts: []string{"*/*"}, 291 }, 292 } 293 services := ps.servicesExportedToNamespace(configNamespace) 294 defaultEgressListener.virtualServices = ps.VirtualServicesForGateway(configNamespace, constants.IstioMeshGateway) 295 defaultEgressListener.mostSpecificWildcardVsIndex = computeWildcardHostVirtualServiceIndex( 296 defaultEgressListener.virtualServices, services) 297 298 out := &SidecarScope{ 299 Name: defaultSidecar, 300 Namespace: configNamespace, 301 EgressListeners: []*IstioEgressListenerWrapper{defaultEgressListener}, 302 destinationRules: make(map[host.Name][]*ConsolidatedDestRule), 303 destinationRulesByNames: make(map[types.NamespacedName]*config.Config), 304 servicesByHostname: make(map[host.Name]*Service, len(defaultEgressListener.services)), 305 configDependencies: make(sets.Set[ConfigHash]), 306 Version: ps.PushVersion, 307 } 308 309 servicesAdded := make(map[host.Name]sidecarServiceIndex) 310 for _, s := range services { 311 out.appendSidecarServices(servicesAdded, s) 312 } 313 defaultEgressListener.services = out.services 314 315 // add dependencies on delegate virtual services 316 delegates := ps.DelegateVirtualServices(defaultEgressListener.virtualServices) 317 for _, delegate := range delegates { 318 out.AddConfigDependencies(delegate) 319 } 320 for _, vs := range defaultEgressListener.virtualServices { 321 for _, cfg := range VirtualServiceDependencies(vs) { 322 out.AddConfigDependencies(cfg.HashCode()) 323 } 324 } 325 326 // Now that we have all the services that sidecars using this scope (in 327 // this config namespace) will see, identify all the destinationRules 328 // that these services need 329 for _, s := range out.services { 330 if dr := ps.destinationRule(configNamespace, s); dr != nil { 331 out.destinationRules[s.Hostname] = dr 332 for _, cdr := range dr { 333 for _, from := range cdr.from { 334 out.destinationRulesByNames[from] = cdr.rule 335 out.AddConfigDependencies(ConfigKey{ 336 Kind: kind.DestinationRule, 337 Name: from.Name, 338 Namespace: from.Namespace, 339 }.HashCode()) 340 } 341 } 342 } 343 out.AddConfigDependencies(ConfigKey{ 344 Kind: kind.ServiceEntry, 345 Name: string(s.Hostname), 346 Namespace: s.Attributes.Namespace, 347 }.HashCode()) 348 } 349 350 if ps.Mesh.OutboundTrafficPolicy != nil { 351 out.OutboundTrafficPolicy = &networking.OutboundTrafficPolicy{ 352 Mode: networking.OutboundTrafficPolicy_Mode(ps.Mesh.OutboundTrafficPolicy.Mode), 353 } 354 } 355 356 return out 357 } 358 359 // convertToSidecarScope converts from Sidecar config to SidecarScope object 360 func convertToSidecarScope(ps *PushContext, sidecarConfig *config.Config, configNamespace string) *SidecarScope { 361 if sidecarConfig == nil { 362 return DefaultSidecarScopeForNamespace(ps, configNamespace) 363 } 364 365 sidecar := sidecarConfig.Spec.(*networking.Sidecar) 366 out := &SidecarScope{ 367 Name: sidecarConfig.Name, 368 Namespace: configNamespace, 369 Sidecar: sidecar, 370 servicesByHostname: make(map[host.Name]*Service), 371 configDependencies: make(sets.Set[ConfigHash]), 372 Version: ps.PushVersion, 373 } 374 375 out.AddConfigDependencies(ConfigKey{ 376 Kind: kind.Sidecar, 377 Name: sidecarConfig.Name, 378 Namespace: sidecarConfig.Namespace, 379 }.HashCode()) 380 381 egressConfigs := sidecar.Egress 382 // If egress not set, setup a default listener 383 if len(egressConfigs) == 0 { 384 egressConfigs = append(egressConfigs, &networking.IstioEgressListener{Hosts: []string{"*/*"}}) 385 } 386 out.EgressListeners = make([]*IstioEgressListenerWrapper, 0, len(egressConfigs)) 387 for _, e := range egressConfigs { 388 out.EgressListeners = append(out.EgressListeners, 389 convertIstioListenerToWrapper(ps, configNamespace, e)) 390 } 391 392 // Now collect all the imported services across all egress listeners in 393 // this sidecar crd. This is needed to generate CDS output 394 out.collectImportedServices(ps, configNamespace) 395 396 // Now that we have all the services that sidecars using this scope (in 397 // this config namespace) will see, identify all the destinationRules 398 // that these services need 399 out.selectDestinationRules(ps, configNamespace) 400 401 if sidecar.OutboundTrafficPolicy == nil { 402 if ps.Mesh.OutboundTrafficPolicy != nil { 403 out.OutboundTrafficPolicy = &networking.OutboundTrafficPolicy{ 404 Mode: networking.OutboundTrafficPolicy_Mode(ps.Mesh.OutboundTrafficPolicy.Mode), 405 } 406 } 407 } else { 408 out.OutboundTrafficPolicy = sidecar.OutboundTrafficPolicy 409 } 410 411 return out 412 } 413 414 func (sc *SidecarScope) collectImportedServices(ps *PushContext, configNamespace string) { 415 serviceMatchingPort := func(s *Service, ilw *IstioEgressListenerWrapper, ports sets.Set[int]) *Service { 416 if ilw.matchPort { 417 return serviceMatchingListenerPort(s, ilw) 418 } 419 return serviceMatchingVirtualServicePorts(s, ports) 420 } 421 422 servicesAdded := make(map[host.Name]sidecarServiceIndex) 423 for _, ilw := range sc.EgressListeners { 424 // First add the explicitly requested services, which take priority 425 for _, s := range ilw.services { 426 sc.appendSidecarServices(servicesAdded, s) 427 } 428 429 // add dependencies on delegate virtual services 430 delegates := ps.DelegateVirtualServices(ilw.virtualServices) 431 sc.AddConfigDependencies(delegates...) 432 433 // Infer more possible destinations from virtual services 434 // Services chosen here will not override services explicitly requested in ilw.services. 435 // That way, if there is ambiguity around what hostname to pick, a user can specify the one they 436 // want in the hosts field, and the potentially random choice below won't matter 437 for _, vs := range ilw.virtualServices { 438 for _, cfg := range VirtualServiceDependencies(vs) { 439 sc.AddConfigDependencies(cfg.HashCode()) 440 } 441 v := vs.Spec.(*networking.VirtualService) 442 for h, ports := range virtualServiceDestinations(v) { 443 byNamespace := ps.ServiceIndex.HostnameAndNamespace[host.Name(h)] 444 // Default to this hostname in our config namespace 445 if s, ok := byNamespace[configNamespace]; ok { 446 // This won't overwrite hostnames that have already been found eg because they were requested in hosts 447 if matchedSvc := serviceMatchingPort(s, ilw, ports); matchedSvc != nil { 448 sc.appendSidecarServices(servicesAdded, matchedSvc) 449 } 450 } else { 451 // We couldn't find the hostname in our config namespace 452 // We have to pick one arbitrarily for now, so we'll pick the first namespace alphabetically 453 // TODO: could we choose services more intelligently based on their ports? 454 if len(byNamespace) == 0 { 455 // This hostname isn't found anywhere 456 log.Debugf("Could not find service hostname %s parsed from %s", h, vs.Key()) 457 continue 458 } 459 // This won't overwrite hostnames that have already been found eg because they were requested in hosts 460 if ns := pickFirstVisibleNamespace(ps, byNamespace, configNamespace); ns != "" { 461 if matchedSvc := serviceMatchingPort(byNamespace[ns], ilw, ports); matchedSvc != nil { 462 sc.appendSidecarServices(servicesAdded, matchedSvc) 463 } 464 } 465 } 466 } 467 } 468 } 469 } 470 471 func (sc *SidecarScope) selectDestinationRules(ps *PushContext, configNamespace string) { 472 sc.destinationRules = make(map[host.Name][]*ConsolidatedDestRule) 473 sc.destinationRulesByNames = make(map[types.NamespacedName]*config.Config) 474 for _, s := range sc.services { 475 drList := ps.destinationRule(configNamespace, s) 476 if drList != nil { 477 sc.destinationRules[s.Hostname] = drList 478 for _, dr := range drList { 479 for _, key := range dr.from { 480 sc.AddConfigDependencies(ConfigKey{ 481 Kind: kind.DestinationRule, 482 Name: key.Name, 483 Namespace: key.Namespace, 484 }.HashCode()) 485 486 sc.destinationRulesByNames[key] = dr.rule 487 } 488 } 489 } 490 sc.AddConfigDependencies(ConfigKey{ 491 Kind: kind.ServiceEntry, 492 Name: string(s.Hostname), 493 Namespace: s.Attributes.Namespace, 494 }.HashCode()) 495 } 496 } 497 498 func convertIstioListenerToWrapper(ps *PushContext, configNamespace string, 499 istioListener *networking.IstioEgressListener, 500 ) *IstioEgressListenerWrapper { 501 out := &IstioEgressListenerWrapper{ 502 IstioListener: istioListener, 503 matchPort: needsPortMatch(istioListener), 504 } 505 506 hostsByNamespace := make(map[string]hostClassification) 507 for _, h := range istioListener.Hosts { 508 parts := strings.SplitN(h, "/", 2) 509 if len(parts) < 2 { 510 log.Errorf("Illegal host in sidecar resource: %s, host must be of form namespace/dnsName", h) 511 continue 512 } 513 if parts[0] == currentNamespace { 514 parts[0] = configNamespace 515 } 516 517 ns := parts[0] 518 hName := host.Name(parts[1]) 519 if _, exists := hostsByNamespace[ns]; !exists { 520 hostsByNamespace[ns] = hostClassification{exactHosts: sets.New[host.Name](), allHosts: make([]host.Name, 0)} 521 } 522 523 // exact hosts are saved separately for map lookup 524 if !hName.IsWildCarded() { 525 hostsByNamespace[ns].exactHosts.Insert(hName) 526 } 527 528 // allHosts contains the exact hosts and wildcard hosts, 529 // since SelectVirtualServices will use `Matches` semantic matching. 530 hc := hostsByNamespace[ns] 531 hc.allHosts = append(hc.allHosts, hName) 532 hostsByNamespace[ns] = hc 533 } 534 535 out.virtualServices = SelectVirtualServices(ps.virtualServiceIndex, configNamespace, hostsByNamespace) 536 svces := ps.servicesExportedToNamespace(configNamespace) 537 out.services = out.selectServices(svces, configNamespace, hostsByNamespace) 538 out.mostSpecificWildcardVsIndex = computeWildcardHostVirtualServiceIndex(out.virtualServices, out.services) 539 540 return out 541 } 542 543 // GetEgressListenerForRDS returns the egress listener corresponding to 544 // the listener port or the bind address or the catch all listener 545 func (sc *SidecarScope) GetEgressListenerForRDS(port int, bind string) *IstioEgressListenerWrapper { 546 if sc == nil { 547 return nil 548 } 549 550 for _, e := range sc.EgressListeners { 551 // We hit a catchall listener. This is the last listener in the list of listeners 552 // return as is 553 if e.IstioListener == nil || e.IstioListener.Port == nil { 554 return e 555 } 556 557 // Check if the ports match 558 // for unix domain sockets (i.e. port == 0), check if the bind is equal to the routeName 559 if int(e.IstioListener.Port.Number) == port { 560 if port == 0 { // unix domain socket 561 if e.IstioListener.Bind == bind { 562 return e 563 } 564 // no match.. continue searching 565 continue 566 } 567 // this is a non-zero port match 568 return e 569 } 570 } 571 572 // This should never be reached unless user explicitly set an empty array for egress 573 // listeners which we actually forbid 574 return nil 575 } 576 577 // HasIngressListener returns if the sidecar scope has ingress listener set 578 func (sc *SidecarScope) HasIngressListener() bool { 579 if sc == nil { 580 return false 581 } 582 583 if sc.Sidecar == nil || len(sc.Sidecar.Ingress) == 0 { 584 return false 585 } 586 587 return true 588 } 589 590 // InboundConnectionPoolForPort returns the connection pool settings for a specific inbound port. If there's not a 591 // setting for that specific port, then the settings at the Sidecar resource are returned. If neither exist, 592 // then nil is returned so the caller can decide what values to fall back on. 593 func (sc *SidecarScope) InboundConnectionPoolForPort(port int) *networking.ConnectionPoolSettings { 594 if sc == nil || sc.Sidecar == nil { 595 return nil 596 } 597 598 for _, in := range sc.Sidecar.Ingress { 599 if int(in.Port.Number) == port { 600 if in.GetConnectionPool() != nil { 601 return in.ConnectionPool 602 } 603 } 604 } 605 606 // if set, it'll be non-nil and have values (guaranteed by validation); or if unset it'll be nil 607 return sc.Sidecar.GetInboundConnectionPool() 608 } 609 610 // Services returns the list of services imported by this egress listener 611 func (ilw *IstioEgressListenerWrapper) Services() []*Service { 612 return ilw.services 613 } 614 615 // VirtualServices returns the list of virtual services imported by this 616 // egress listener 617 func (ilw *IstioEgressListenerWrapper) VirtualServices() []config.Config { 618 return ilw.virtualServices 619 } 620 621 // MostSpecificWildcardVirtualServiceIndex returns the mostSpecificWildcardVsIndex for this egress 622 // listener. 623 func (ilw *IstioEgressListenerWrapper) MostSpecificWildcardVirtualServiceIndex() map[host.Name]types.NamespacedName { 624 return ilw.mostSpecificWildcardVsIndex 625 } 626 627 // DependsOnConfig determines if the proxy depends on the given config. 628 // Returns whether depends on this config or this kind of config is not scopeZd(unknown to be depended) here. 629 func (sc *SidecarScope) DependsOnConfig(config ConfigKey, rootNs string) bool { 630 if sc == nil { 631 return true 632 } 633 634 // This kind of config will trigger a change if made in the root namespace or the same namespace 635 if clusterScopedKnownConfigTypes.Contains(config.Kind) { 636 return config.Namespace == rootNs || config.Namespace == sc.Namespace 637 } 638 639 // This kind of config is unknown to sidecarScope. 640 if _, f := sidecarScopedKnownConfigTypes[config.Kind]; !f { 641 return true 642 } 643 644 return sc.configDependencies.Contains(config.HashCode()) 645 } 646 647 func (sc *SidecarScope) GetService(hostname host.Name) *Service { 648 if sc == nil { 649 return nil 650 } 651 return sc.servicesByHostname[hostname] 652 } 653 654 // AddConfigDependencies add extra config dependencies to this scope. This action should be done before the 655 // SidecarScope being used to avoid concurrent read/write. 656 func (sc *SidecarScope) AddConfigDependencies(dependencies ...ConfigHash) { 657 if sc == nil { 658 return 659 } 660 if sc.configDependencies == nil { 661 sc.configDependencies = sets.New(dependencies...) 662 } else { 663 sc.configDependencies.InsertAll(dependencies...) 664 } 665 } 666 667 // DestinationRule returns a destinationrule for a svc. 668 func (sc *SidecarScope) DestinationRule(direction TrafficDirection, proxy *Proxy, svc host.Name) *ConsolidatedDestRule { 669 destinationRules := sc.destinationRules[svc] 670 var catchAllDr *ConsolidatedDestRule 671 for _, destRule := range destinationRules { 672 destinationRule := destRule.rule.Spec.(*networking.DestinationRule) 673 if destinationRule.GetWorkloadSelector() == nil { 674 catchAllDr = destRule 675 } 676 // filter DestinationRule based on workloadSelector for outbound configs. 677 // WorkloadSelector configuration is honored only for outbound configuration, because 678 // for inbound configuration, the settings at sidecar would be more explicit and the preferred way forward. 679 if sc.Namespace == destRule.rule.Namespace && 680 destinationRule.GetWorkloadSelector() != nil && direction == TrafficDirectionOutbound { 681 workloadSelector := labels.Instance(destinationRule.GetWorkloadSelector().GetMatchLabels()) 682 // return destination rule if workload selector matches 683 if workloadSelector.SubsetOf(proxy.Labels) { 684 return destRule 685 } 686 } 687 } 688 // If there is no workload specific destinationRule, return the wild carded dr if present. 689 if catchAllDr != nil { 690 return catchAllDr 691 } 692 return nil 693 } 694 695 // DestinationRuleConfig returns merged destination rules for a svc. 696 func (sc *SidecarScope) DestinationRuleConfig(direction TrafficDirection, proxy *Proxy, svc host.Name) *config.Config { 697 cdr := sc.DestinationRule(direction, proxy, svc) 698 if cdr == nil { 699 return nil 700 } 701 return cdr.rule 702 } 703 704 // Services returns the list of services that are visible to a sidecar. 705 func (sc *SidecarScope) Services() []*Service { 706 return sc.services 707 } 708 709 // Testing Only. This allows tests to inject a config without having the mock. 710 func (sc *SidecarScope) SetDestinationRulesForTesting(configs []config.Config) { 711 sc.destinationRulesByNames = make(map[types.NamespacedName]*config.Config) 712 for _, c := range configs { 713 c := c 714 sc.destinationRulesByNames[types.NamespacedName{Name: c.Name, Namespace: c.Namespace}] = &c 715 } 716 } 717 718 func (sc *SidecarScope) DestinationRuleByName(name, namespace string) *config.Config { 719 if sc == nil { 720 return nil 721 } 722 return sc.destinationRulesByNames[types.NamespacedName{ 723 Name: name, 724 Namespace: namespace, 725 }] 726 } 727 728 // ServicesForHostname returns a list of services that fall under the hostname provided. This hostname 729 // can be a wildcard. 730 func (sc *SidecarScope) ServicesForHostname(hostname host.Name) []*Service { 731 if !hostname.IsWildCarded() { 732 if svc, f := sc.servicesByHostname[hostname]; f { 733 return []*Service{svc} 734 } 735 return nil 736 } 737 services := make([]*Service, 0) 738 for _, svc := range sc.services { 739 if hostname.Matches(svc.Hostname) { 740 services = append(services, svc) 741 } 742 } 743 return services 744 } 745 746 // Return filtered services through the hosts field in the egress portion of the Sidecar config. 747 // Note that the returned service could be trimmed. 748 // TODO: support merging services within this egress listener to align with SidecarScope's behavior. 749 func (ilw *IstioEgressListenerWrapper) selectServices(services []*Service, configNamespace string, hostsByNamespace map[string]hostClassification) []*Service { 750 importedServices := make([]*Service, 0) 751 wildcardHosts, wnsFound := hostsByNamespace[wildcardNamespace] 752 for _, s := range services { 753 configNamespace := s.Attributes.Namespace 754 755 // Check if there is an explicit import of form ns/* or ns/host 756 if importedHosts, nsFound := hostsByNamespace[configNamespace]; nsFound { 757 if svc := matchingAliasService(importedHosts, matchingService(importedHosts, s, ilw)); svc != nil { 758 importedServices = append(importedServices, svc) 759 continue 760 } 761 } 762 763 // Check if there is an import of form */host or */* 764 if wnsFound { 765 if svc := matchingAliasService(wildcardHosts, matchingService(wildcardHosts, s, ilw)); svc != nil { 766 importedServices = append(importedServices, svc) 767 } 768 } 769 } 770 771 validServices := make(map[host.Name]string, len(importedServices)) 772 for _, svc := range importedServices { 773 _, f := validServices[svc.Hostname] 774 // Select a single namespace for a given hostname. 775 // If the same hostname is imported from multiple namespaces, pick the one in the configNamespace 776 // If neither are in configNamespace, an arbitrary one will be chosen 777 if !f || svc.Attributes.Namespace == configNamespace { 778 validServices[svc.Hostname] = svc.Attributes.Namespace 779 } 780 } 781 782 // Filter down to just instances in scope for the service 783 return slices.FilterInPlace(importedServices, func(svc *Service) bool { 784 return validServices[svc.Hostname] == svc.Attributes.Namespace 785 }) 786 } 787 788 // Return the original service or a trimmed service which has a subset of the ports in original service. 789 func matchingService(importedHosts hostClassification, service *Service, ilw *IstioEgressListenerWrapper) *Service { 790 if importedHosts.Matches(service.Hostname) { 791 if ilw.matchPort { 792 return serviceMatchingListenerPort(service, ilw) 793 } 794 return service 795 } 796 return nil 797 } 798 799 // matchingAliasService the original service or a trimmed service which has a subset of aliases, based on imports from sidecar 800 func matchingAliasService(importedHosts hostClassification, service *Service) *Service { 801 if service == nil { 802 return nil 803 } 804 matched := make([]NamespacedHostname, 0, len(service.Attributes.Aliases)) 805 for _, alias := range service.Attributes.Aliases { 806 if importedHosts.Matches(alias.Hostname) { 807 matched = append(matched, alias) 808 } 809 } 810 811 if len(matched) == len(service.Attributes.Aliases) { 812 return service 813 } 814 service = service.DeepCopy() 815 service.Attributes.Aliases = matched 816 return service 817 } 818 819 // serviceMatchingListenerPort constructs service with listener port. 820 func serviceMatchingListenerPort(service *Service, ilw *IstioEgressListenerWrapper) *Service { 821 for _, port := range service.Ports { 822 if port.Port == int(ilw.IstioListener.Port.GetNumber()) { 823 sc := service.DeepCopy() 824 sc.Ports = []*Port{port} 825 return sc 826 } 827 } 828 return nil 829 } 830 831 func serviceMatchingVirtualServicePorts(service *Service, vsDestPorts sets.Set[int]) *Service { 832 // A value of 0 in vsDestPorts is used as a sentinel to indicate a dependency 833 // on every port of the service. 834 if len(vsDestPorts) == 0 || vsDestPorts.Contains(0) { 835 return service 836 } 837 838 foundPorts := make([]*Port, 0) 839 for _, port := range service.Ports { 840 if vsDestPorts.Contains(port.Port) { 841 foundPorts = append(foundPorts, port) 842 } 843 } 844 845 if len(foundPorts) == len(service.Ports) { 846 return service 847 } 848 849 if len(foundPorts) > 0 { 850 sc := service.DeepCopy() 851 sc.Ports = foundPorts 852 return sc 853 } 854 855 // If the service has more than one port, and the Virtual Service only 856 // specifies destination ports not found in the service, we'll simply 857 // not add the service to the sidecar as an optimization, because 858 // traffic will not route properly anyway. This matches the above 859 // behavior in serviceMatchingListenerPort for ports specified on the 860 // sidecar egress listener. 861 log.Warnf("Failed to find any VirtualService destination ports %v exposed by Service %s", vsDestPorts, service.Hostname) 862 return nil 863 } 864 865 // computeWildcardHostVirtualServiceIndex computes the wildcardHostVirtualServiceIndex for a given 866 // (sorted) list of virtualServices. This is used to optimize the lookup of the most specific wildcard host. 867 // 868 // N.B the caller MUST presort virtualServices based on the desired precedence for duplicate hostnames. 869 // This function will persist that order and not overwrite any previous entries for a given hostname. 870 func computeWildcardHostVirtualServiceIndex(virtualServices []config.Config, services []*Service) map[host.Name]types.NamespacedName { 871 fqdnVirtualServiceHostIndex := make(map[host.Name]config.Config, len(virtualServices)) 872 wildcardVirtualServiceHostIndex := make(map[host.Name]config.Config, len(virtualServices)) 873 for _, vs := range virtualServices { 874 v := vs.Spec.(*networking.VirtualService) 875 for _, h := range v.Hosts { 876 // We may have duplicate (not just overlapping) hosts; assume the list of VS is sorted already 877 // and never overwrite existing entries 878 if host.Name(h).IsWildCarded() { 879 _, exists := wildcardVirtualServiceHostIndex[host.Name(h)] 880 if !exists { 881 wildcardVirtualServiceHostIndex[host.Name(h)] = vs 882 } 883 } else { 884 _, exists := fqdnVirtualServiceHostIndex[host.Name(h)] 885 if !exists { 886 fqdnVirtualServiceHostIndex[host.Name(h)] = vs 887 } 888 } 889 } 890 } 891 892 mostSpecificWildcardVsIndex := make(map[host.Name]types.NamespacedName) 893 comparator := MostSpecificHostMatch[config.Config] 894 if features.PersistOldestWinsHeuristicForVirtualServiceHostMatching { 895 comparator = OldestMatchingHost 896 } 897 for _, svc := range services { 898 _, ref, exists := comparator(svc.Hostname, fqdnVirtualServiceHostIndex, wildcardVirtualServiceHostIndex) 899 if !exists { 900 // This svc doesn't have a virtualService; skip 901 continue 902 } 903 mostSpecificWildcardVsIndex[svc.Hostname] = ref.NamespacedName() 904 } 905 906 return mostSpecificWildcardVsIndex 907 } 908 909 func needsPortMatch(l *networking.IstioEgressListener) bool { 910 // If a listener is defined with a port, we should match services with port except in the following case. 911 // - If Port's protocol is proxy protocol(HTTP_PROXY) in which case the egress listener is used as generic egress http proxy. 912 return l != nil && l.Port.GetNumber() != 0 && 913 protocol.Parse(l.Port.Protocol) != protocol.HTTP_PROXY 914 } 915 916 type sidecarServiceIndex struct { 917 svc *Service 918 index int // index record the position of the svc in slice 919 } 920 921 // append services to the sidecar scope, and merge services with the same hostname. 922 func (sc *SidecarScope) appendSidecarServices(servicesAdded map[host.Name]sidecarServiceIndex, s *Service) { 923 if s == nil { 924 return 925 } 926 if foundSvc, found := servicesAdded[s.Hostname]; !found { 927 sc.services = append(sc.services, s) 928 servicesAdded[s.Hostname] = sidecarServiceIndex{s, len(sc.services) - 1} 929 sc.servicesByHostname[s.Hostname] = s 930 } else { 931 existing := foundSvc.svc 932 // We do not merge k8s service with any other services from other registries 933 if existing.Attributes.ServiceRegistry == provider.Kubernetes && s.Attributes.ServiceRegistry != provider.Kubernetes { 934 log.Debugf("Service %s/%s from registry %s ignored as there is an existing service in Kubernetes already %s/%s/%s", 935 s.Attributes.Namespace, s.Hostname, s.Attributes.ServiceRegistry, 936 existing.Attributes.Namespace, existing.Hostname, existing.Attributes.ServiceRegistry) 937 return 938 } 939 // In some scenarios, there may be multiple Services defined for the same hostname due to ServiceEntry allowing 940 // arbitrary hostnames. In these cases, we want to pick the first Service, which is the oldest. This ensures 941 // newly created Services cannot take ownership unexpectedly. However, the Service is from Kubernetes it should 942 // take precedence over ones not. This prevents someone from "domain squatting" on the hostname before a Kubernetes Service is created. 943 if existing.Attributes.ServiceRegistry != provider.Kubernetes && s.Attributes.ServiceRegistry == provider.Kubernetes { 944 log.Debugf("Service %s/%s from registry %s ignored as there is a Kubernetes service with the same host name %s/%s/%s", 945 existing.Attributes.Namespace, existing.Hostname, existing.Attributes.ServiceRegistry, 946 s.Attributes.Namespace, s.Hostname, s.Attributes.ServiceRegistry) 947 // replace service in slice 948 sc.services[foundSvc.index] = s 949 // Update index as well, so that future reads will merge into the new service 950 foundSvc.svc = s 951 servicesAdded[foundSvc.svc.Hostname] = foundSvc 952 sc.servicesByHostname[s.Hostname] = s 953 return 954 } 955 956 if !canMergeServices(existing, s) { 957 log.Debugf("Service %s/%s from registry %s ignored by %s/%s/%s", s.Attributes.Namespace, s.Hostname, s.Attributes.ServiceRegistry, 958 existing.Attributes.Namespace, existing.Hostname, existing.Attributes.ServiceRegistry) 959 return 960 } 961 962 // If it comes here, it means we can merge the services. 963 // Merge the ports to service when each listener generates partial service. 964 // We only merge if the found service is in the same namespace as the one we're trying to add 965 copied := foundSvc.svc.DeepCopy() 966 for _, p := range s.Ports { 967 found := false 968 for _, osp := range copied.Ports { 969 if p.Port == osp.Port { 970 found = true 971 break 972 } 973 } 974 if !found { 975 copied.Ports = append(copied.Ports, p) 976 } 977 } 978 // replace service in slice 979 sc.services[foundSvc.index] = copied 980 // Update index as well, so that future reads will merge into the new service 981 foundSvc.svc = copied 982 servicesAdded[foundSvc.svc.Hostname] = foundSvc 983 // update the existing service in the map to the merged one 984 sc.servicesByHostname[s.Hostname] = copied 985 } 986 } 987 988 func canMergeServices(s1, s2 *Service) bool { 989 // Hostname has been compared in the caller `appendSidecarServices`, so we donot need to compare again. 990 if s1.Attributes.Namespace != s2.Attributes.Namespace { 991 return false 992 } 993 if s1.Resolution != s2.Resolution { 994 return false 995 } 996 // kuberneres service registry has been checked before 997 if s1.Attributes.ServiceRegistry != s2.Attributes.ServiceRegistry { 998 return false 999 } 1000 1001 if !maps.Equal(s1.Attributes.Labels, s2.Attributes.Labels) { 1002 return false 1003 } 1004 1005 if !maps.Equal(s1.Attributes.LabelSelectors, s2.Attributes.LabelSelectors) { 1006 return false 1007 } 1008 1009 if !maps.Equal(s1.Attributes.ExportTo, s2.Attributes.ExportTo) { 1010 return false 1011 } 1012 1013 return true 1014 } 1015 1016 // Pick the Service namespace visible to the configNamespace namespace. 1017 // If it does not exist, return an empty string, 1018 // If there are more than one, pick the first alphabetically. 1019 func pickFirstVisibleNamespace(ps *PushContext, byNamespace map[string]*Service, configNamespace string) string { 1020 nss := make([]string, 0, len(byNamespace)) 1021 for ns := range byNamespace { 1022 if ps.IsServiceVisible(byNamespace[ns], configNamespace) { 1023 nss = append(nss, ns) 1024 } 1025 } 1026 if len(nss) > 0 { 1027 sort.Strings(nss) 1028 return nss[0] 1029 } 1030 return "" 1031 }