istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/model/virtualservice.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 "strings" 19 20 "k8s.io/apimachinery/pkg/types" 21 22 networking "istio.io/api/networking/v1alpha3" 23 "istio.io/istio/pkg/config" 24 "istio.io/istio/pkg/config/constants" 25 "istio.io/istio/pkg/config/host" 26 "istio.io/istio/pkg/config/schema/kind" 27 "istio.io/istio/pkg/config/visibility" 28 "istio.io/istio/pkg/maps" 29 "istio.io/istio/pkg/util/protomarshal" 30 "istio.io/istio/pkg/util/sets" 31 ) 32 33 // SelectVirtualServices selects the virtual services by matching given services' host names. 34 // This function is used by sidecar converter. 35 func SelectVirtualServices(vsidx virtualServiceIndex, configNamespace string, hostsByNamespace map[string]hostClassification) []config.Config { 36 importedVirtualServices := make([]config.Config, 0) 37 vsset := sets.New[types.NamespacedName]() 38 39 addVirtualService := func(vs config.Config, hc hostClassification) { 40 key := vs.NamespacedName() 41 if vsset.Contains(key) { 42 return 43 } 44 45 rule := vs.Spec.(*networking.VirtualService) 46 useGatewaySemantics := UseGatewaySemantics(vs) 47 for _, vh := range rule.Hosts { 48 if hc.VSMatches(host.Name(vh), useGatewaySemantics) { 49 importedVirtualServices = append(importedVirtualServices, vs) 50 vsset.Insert(key) 51 return 52 } 53 } 54 } 55 56 wnsImportedHosts, wnsFound := hostsByNamespace[wildcardNamespace] 57 loopAndAdd := func(vses []config.Config) { 58 for _, c := range vses { 59 configNamespace := c.Namespace 60 // Selection algorithm: 61 // virtualservices have a list of hosts in the API spec 62 // if any host in the list matches one service hostname, select the virtual service 63 // and break out of the loop. 64 65 // Check if there is an explicit import of form ns/* or ns/host 66 if importedHosts, nsFound := hostsByNamespace[configNamespace]; nsFound { 67 addVirtualService(c, importedHosts) 68 } 69 70 // Check if there is an import of form */host or */* 71 if wnsFound { 72 addVirtualService(c, wnsImportedHosts) 73 } 74 } 75 } 76 77 n := types.NamespacedName{Namespace: configNamespace, Name: constants.IstioMeshGateway} 78 loopAndAdd(vsidx.privateByNamespaceAndGateway[n]) 79 loopAndAdd(vsidx.exportedToNamespaceByGateway[n]) 80 loopAndAdd(vsidx.publicByGateway[constants.IstioMeshGateway]) 81 82 return importedVirtualServices 83 } 84 85 func resolveVirtualServiceShortnames(rule *networking.VirtualService, meta config.Meta) { 86 // Kubernetes Gateway API semantics support shortnames 87 if UseGatewaySemantics(config.Config{Meta: meta}) { 88 return 89 } 90 91 // resolve top level hosts 92 for i, h := range rule.Hosts { 93 rule.Hosts[i] = string(ResolveShortnameToFQDN(h, meta)) 94 } 95 // resolve gateways to bind to 96 for i, g := range rule.Gateways { 97 if g != constants.IstioMeshGateway { 98 rule.Gateways[i] = resolveGatewayName(g, meta) 99 } 100 } 101 // resolve host in http route.destination, route.mirror 102 for _, d := range rule.Http { 103 for _, m := range d.Match { 104 for i, g := range m.Gateways { 105 if g != constants.IstioMeshGateway { 106 m.Gateways[i] = resolveGatewayName(g, meta) 107 } 108 } 109 } 110 for _, w := range d.Route { 111 if w.Destination != nil { 112 w.Destination.Host = string(ResolveShortnameToFQDN(w.Destination.Host, meta)) 113 } 114 } 115 if d.Mirror != nil { 116 d.Mirror.Host = string(ResolveShortnameToFQDN(d.Mirror.Host, meta)) 117 } 118 for _, m := range d.Mirrors { 119 if m.Destination != nil { 120 m.Destination.Host = string(ResolveShortnameToFQDN(m.Destination.Host, meta)) 121 } 122 } 123 } 124 // resolve host in tcp route.destination 125 for _, d := range rule.Tcp { 126 for _, m := range d.Match { 127 for i, g := range m.Gateways { 128 if g != constants.IstioMeshGateway { 129 m.Gateways[i] = resolveGatewayName(g, meta) 130 } 131 } 132 } 133 for _, w := range d.Route { 134 if w.Destination != nil { 135 w.Destination.Host = string(ResolveShortnameToFQDN(w.Destination.Host, meta)) 136 } 137 } 138 } 139 // resolve host in tls route.destination 140 for _, tls := range rule.Tls { 141 for _, m := range tls.Match { 142 for i, g := range m.Gateways { 143 if g != constants.IstioMeshGateway { 144 m.Gateways[i] = resolveGatewayName(g, meta) 145 } 146 } 147 } 148 for _, w := range tls.Route { 149 if w.Destination != nil { 150 w.Destination.Host = string(ResolveShortnameToFQDN(w.Destination.Host, meta)) 151 } 152 } 153 } 154 } 155 156 // Return merged virtual services and the root->delegate vs map 157 func mergeVirtualServicesIfNeeded( 158 vServices []config.Config, 159 defaultExportTo sets.Set[visibility.Instance], 160 ) ([]config.Config, map[ConfigKey][]ConfigKey) { 161 out := make([]config.Config, 0, len(vServices)) 162 delegatesMap := map[types.NamespacedName]config.Config{} 163 delegatesExportToMap := make(map[types.NamespacedName]sets.Set[visibility.Instance]) 164 // root virtualservices with delegate 165 var rootVses []config.Config 166 167 // 1. classify virtualservices 168 for _, vs := range vServices { 169 rule := vs.Spec.(*networking.VirtualService) 170 // it is delegate, add it to the indexer cache along with the exportTo for the delegate 171 if len(rule.Hosts) == 0 { 172 delegatesMap[vs.NamespacedName()] = vs 173 var exportToSet sets.Set[visibility.Instance] 174 if len(rule.ExportTo) == 0 { 175 // No exportTo in virtualService. Use the global default 176 exportToSet = sets.NewWithLength[visibility.Instance](defaultExportTo.Len()) 177 for v := range defaultExportTo { 178 if v == visibility.Private { 179 exportToSet.Insert(visibility.Instance(vs.Namespace)) 180 } else { 181 exportToSet.Insert(v) 182 } 183 } 184 } else { 185 exportToSet = sets.NewWithLength[visibility.Instance](len(rule.ExportTo)) 186 for _, e := range rule.ExportTo { 187 if e == string(visibility.Private) { 188 exportToSet.Insert(visibility.Instance(vs.Namespace)) 189 } else { 190 exportToSet.Insert(visibility.Instance(e)) 191 } 192 } 193 } 194 delegatesExportToMap[vs.NamespacedName()] = exportToSet 195 196 continue 197 } 198 199 // root vs 200 if isRootVs(rule) { 201 rootVses = append(rootVses, vs) 202 continue 203 } 204 205 // the others are normal vs without delegate 206 out = append(out, vs) 207 } 208 209 delegatesByRoot := make(map[ConfigKey][]ConfigKey, len(rootVses)) 210 211 // 2. merge delegates and root 212 for _, root := range rootVses { 213 rootConfigKey := ConfigKey{Kind: kind.VirtualService, Name: root.Name, Namespace: root.Namespace} 214 rootVs := root.Spec.(*networking.VirtualService) 215 mergedRoutes := []*networking.HTTPRoute{} 216 for _, route := range rootVs.Http { 217 // it is root vs with delegate 218 if delegate := route.Delegate; delegate != nil { 219 delegateNamespace := delegate.Namespace 220 if delegateNamespace == "" { 221 delegateNamespace = root.Namespace 222 } 223 delegateConfigKey := ConfigKey{Kind: kind.VirtualService, Name: delegate.Name, Namespace: delegateNamespace} 224 delegatesByRoot[rootConfigKey] = append(delegatesByRoot[rootConfigKey], delegateConfigKey) 225 delegateVS, ok := delegatesMap[types.NamespacedName{Namespace: delegateNamespace, Name: delegate.Name}] 226 if !ok { 227 log.Warnf("delegate virtual service %s/%s of %s/%s not found", 228 delegateNamespace, delegate.Name, root.Namespace, root.Name) 229 // delegate not found, ignore only the current HTTP route 230 continue 231 } 232 // make sure that the delegate is visible to root virtual service's namespace 233 exportTo := delegatesExportToMap[types.NamespacedName{Namespace: delegateNamespace, Name: delegate.Name}] 234 if !exportTo.Contains(visibility.Public) && !exportTo.Contains(visibility.Instance(root.Namespace)) { 235 log.Warnf("delegate virtual service %s/%s of %s/%s is not exported to %s", 236 delegateNamespace, delegate.Name, root.Namespace, root.Name, root.Namespace) 237 continue 238 } 239 // DeepCopy to prevent mutate the original delegate, it can conflict 240 // when multiple routes delegate to one single VS. 241 copiedDelegate := config.DeepCopy(delegateVS.Spec) 242 vs := copiedDelegate.(*networking.VirtualService) 243 merged := mergeHTTPRoutes(route, vs.Http) 244 mergedRoutes = append(mergedRoutes, merged...) 245 } else { 246 mergedRoutes = append(mergedRoutes, route) 247 } 248 } 249 rootVs.Http = mergedRoutes 250 if log.DebugEnabled() { 251 vsString, _ := protomarshal.ToJSONWithIndent(rootVs, " ") 252 log.Debugf("merged virtualService: %s", vsString) 253 } 254 out = append(out, root) 255 } 256 257 sortConfigByCreationTime(out) 258 259 return out, delegatesByRoot 260 } 261 262 // merge root's route with delegate's and the merged route number equals the delegate's. 263 // if there is a conflict with root, the route is ignored 264 func mergeHTTPRoutes(root *networking.HTTPRoute, delegate []*networking.HTTPRoute) []*networking.HTTPRoute { 265 root.Delegate = nil 266 267 out := make([]*networking.HTTPRoute, 0, len(delegate)) 268 for _, subRoute := range delegate { 269 merged := mergeHTTPRoute(root, subRoute) 270 if merged != nil { 271 out = append(out, merged) 272 } 273 } 274 return out 275 } 276 277 // merge the two HTTPRoutes, if there is a conflict with root, the delegate route is ignored 278 func mergeHTTPRoute(root *networking.HTTPRoute, delegate *networking.HTTPRoute) *networking.HTTPRoute { 279 // suppose there are N1 match conditions in root, N2 match conditions in delegate 280 // if match condition of N2 is a subset of anyone in N1, this is a valid matching conditions 281 merged, conflict := mergeHTTPMatchRequests(root.Match, delegate.Match) 282 if conflict { 283 log.Warnf("HTTPMatchRequests conflict: root route %s, delegate route %s", root.Name, delegate.Name) 284 return nil 285 } 286 delegate.Match = merged 287 288 if delegate.Name == "" { 289 delegate.Name = root.Name 290 } else if root.Name != "" { 291 delegate.Name = root.Name + "-" + delegate.Name 292 } 293 if delegate.Rewrite == nil { 294 delegate.Rewrite = root.Rewrite 295 } 296 if delegate.DirectResponse == nil { 297 delegate.DirectResponse = root.DirectResponse 298 } 299 if delegate.Timeout == nil { 300 delegate.Timeout = root.Timeout 301 } 302 if delegate.Retries == nil { 303 delegate.Retries = root.Retries 304 } 305 if delegate.Fault == nil { 306 delegate.Fault = root.Fault 307 } 308 if delegate.Mirror == nil { 309 delegate.Mirror = root.Mirror 310 } 311 // nolint: staticcheck 312 if delegate.MirrorPercent == nil { 313 delegate.MirrorPercent = root.MirrorPercent 314 } 315 if delegate.MirrorPercentage == nil { 316 delegate.MirrorPercentage = root.MirrorPercentage 317 } 318 if delegate.CorsPolicy == nil { 319 delegate.CorsPolicy = root.CorsPolicy 320 } 321 if delegate.Mirrors == nil { 322 delegate.Mirrors = root.Mirrors 323 } 324 if delegate.Headers == nil { 325 delegate.Headers = root.Headers 326 } 327 return delegate 328 } 329 330 // return merged match conditions if not conflicts 331 func mergeHTTPMatchRequests(root, delegate []*networking.HTTPMatchRequest) (out []*networking.HTTPMatchRequest, conflict bool) { 332 if len(root) == 0 { 333 return delegate, false 334 } 335 336 if len(delegate) == 0 { 337 return root, false 338 } 339 340 // each HTTPMatchRequest of delegate must find a superset in root. 341 // otherwise it conflicts 342 for _, subMatch := range delegate { 343 foundMatch := false 344 for _, rootMatch := range root { 345 if hasConflict(rootMatch, subMatch) { 346 log.Warnf("HTTPMatchRequests conflict: root %v, delegate %v", rootMatch, subMatch) 347 continue 348 } 349 // merge HTTPMatchRequest 350 out = append(out, mergeHTTPMatchRequest(rootMatch, subMatch)) 351 foundMatch = true 352 } 353 if !foundMatch { 354 return nil, true 355 } 356 } 357 if len(out) == 0 { 358 conflict = true 359 } 360 return 361 } 362 363 func mergeHTTPMatchRequest(root, delegate *networking.HTTPMatchRequest) *networking.HTTPMatchRequest { 364 // nolint: govet 365 out := *delegate 366 if out.Name == "" { 367 out.Name = root.Name 368 } else if root.Name != "" { 369 out.Name = root.Name + "-" + out.Name 370 } 371 if out.Uri == nil { 372 out.Uri = root.Uri 373 } 374 if out.Scheme == nil { 375 out.Scheme = root.Scheme 376 } 377 if out.Method == nil { 378 out.Method = root.Method 379 } 380 if out.Authority == nil { 381 out.Authority = root.Authority 382 } 383 // headers 384 out.Headers = maps.MergeCopy(root.Headers, delegate.Headers) 385 386 // withoutheaders 387 out.WithoutHeaders = maps.MergeCopy(root.WithoutHeaders, delegate.WithoutHeaders) 388 389 // queryparams 390 out.QueryParams = maps.MergeCopy(root.QueryParams, delegate.QueryParams) 391 392 if out.Port == 0 { 393 out.Port = root.Port 394 } 395 396 // SourceLabels 397 out.SourceLabels = maps.MergeCopy(root.SourceLabels, delegate.SourceLabels) 398 399 if out.SourceNamespace == "" { 400 out.SourceNamespace = root.SourceNamespace 401 } 402 403 if len(out.Gateways) == 0 { 404 out.Gateways = root.Gateways 405 } 406 407 if len(out.StatPrefix) == 0 { 408 out.StatPrefix = root.StatPrefix 409 } 410 return &out 411 } 412 413 func hasConflict(root, leaf *networking.HTTPMatchRequest) bool { 414 roots := []*networking.StringMatch{root.Uri, root.Scheme, root.Method, root.Authority} 415 leaves := []*networking.StringMatch{leaf.Uri, leaf.Scheme, leaf.Method, leaf.Authority} 416 for i := range roots { 417 if stringMatchConflict(roots[i], leaves[i]) { 418 return true 419 } 420 } 421 // header conflicts 422 for key, leafHeader := range leaf.Headers { 423 if stringMatchConflict(root.Headers[key], leafHeader) { 424 return true 425 } 426 } 427 428 // without headers 429 for key, leafValue := range leaf.WithoutHeaders { 430 if stringMatchConflict(root.WithoutHeaders[key], leafValue) { 431 return true 432 } 433 } 434 435 // query params conflict 436 for key, value := range leaf.QueryParams { 437 if stringMatchConflict(root.QueryParams[key], value) { 438 return true 439 } 440 } 441 442 if root.IgnoreUriCase != leaf.IgnoreUriCase { 443 return true 444 } 445 if root.Port > 0 && leaf.Port > 0 && root.Port != leaf.Port { 446 return true 447 } 448 449 // sourceNamespace 450 if root.SourceNamespace != "" && leaf.SourceNamespace != root.SourceNamespace { 451 return true 452 } 453 454 // sourceLabels should not conflict, root should have superset of sourceLabels. 455 for key, leafValue := range leaf.SourceLabels { 456 if v, ok := root.SourceLabels[key]; ok && v != leafValue { 457 return true 458 } 459 } 460 461 // gateways should not conflict, root should have superset of gateways. 462 if len(root.Gateways) > 0 && len(leaf.Gateways) > 0 { 463 if len(root.Gateways) < len(leaf.Gateways) { 464 return true 465 } 466 rootGateway := sets.New(root.Gateways...) 467 for _, gw := range leaf.Gateways { 468 if !rootGateway.Contains(gw) { 469 return true 470 } 471 } 472 } 473 474 return false 475 } 476 477 func stringMatchConflict(root, leaf *networking.StringMatch) bool { 478 // no conflict when root or leaf is not specified 479 if root == nil || leaf == nil { 480 return false 481 } 482 // If root regex match is specified, delegate should not have other matches. 483 if root.GetRegex() != "" { 484 if leaf.GetRegex() != "" || leaf.GetPrefix() != "" || leaf.GetExact() != "" { 485 return true 486 } 487 } 488 // If delegate regex match is specified, root should not have other matches. 489 if leaf.GetRegex() != "" { 490 if root.GetRegex() != "" || root.GetPrefix() != "" || root.GetExact() != "" { 491 return true 492 } 493 } 494 // root is exact match 495 if exact := root.GetExact(); exact != "" { 496 // leaf is prefix match, conflict 497 if leaf.GetPrefix() != "" { 498 return true 499 } 500 // both exact, but not equal 501 if leaf.GetExact() != exact { 502 return true 503 } 504 return false 505 } 506 // root is prefix match 507 if prefix := root.GetPrefix(); prefix != "" { 508 // leaf is prefix match 509 if leaf.GetPrefix() != "" { 510 // leaf(`/a`) is not subset of root(`/a/b`) 511 return !strings.HasPrefix(leaf.GetPrefix(), prefix) 512 } 513 // leaf is exact match 514 if leaf.GetExact() != "" { 515 // leaf(`/a`) is not subset of root(`/a/b`) 516 return !strings.HasPrefix(leaf.GetExact(), prefix) 517 } 518 } 519 520 return true 521 } 522 523 func isRootVs(vs *networking.VirtualService) bool { 524 for _, route := range vs.Http { 525 // it is root vs with delegate 526 if route.Delegate != nil { 527 return true 528 } 529 } 530 return false 531 } 532 533 // UseIngressSemantics determines which logic we should use for VirtualService 534 // This allows ingress and VS to both be represented by VirtualService, but have different 535 // semantics. 536 func UseIngressSemantics(cfg config.Config) bool { 537 return cfg.Annotations[constants.InternalRouteSemantics] == constants.RouteSemanticsIngress 538 } 539 540 // UseGatewaySemantics determines which logic we should use for VirtualService 541 // This allows gateway-api and VS to both be represented by VirtualService, but have different 542 // semantics. 543 func UseGatewaySemantics(cfg config.Config) bool { 544 return cfg.Annotations[constants.InternalRouteSemantics] == constants.RouteSemanticsGateway 545 } 546 547 // VirtualServiceDependencies returns dependent configs of the vs, 548 // for internal vs generated from gateway-api routes, it returns the parent routes, 549 // otherwise it just returns the vs as is. 550 func VirtualServiceDependencies(vs config.Config) []ConfigKey { 551 if !UseGatewaySemantics(vs) { 552 return []ConfigKey{ 553 { 554 Kind: kind.VirtualService, 555 Namespace: vs.Namespace, 556 Name: vs.Name, 557 }, 558 } 559 } 560 561 // synthetic vs, get internal parents 562 internalParents := strings.Split(vs.Annotations[constants.InternalParentNames], ",") 563 out := make([]ConfigKey, 0, len(internalParents)) 564 for _, p := range internalParents { 565 // kind/name.namespace 566 ks, nsname, ok := strings.Cut(p, "/") 567 if !ok { 568 log.Errorf("invalid InternalParentName parts: %s", p) 569 continue 570 } 571 var k kind.Kind 572 switch ks { 573 case kind.HTTPRoute.String(): 574 k = kind.HTTPRoute 575 case kind.TCPRoute.String(): 576 k = kind.TCPRoute 577 case kind.TLSRoute.String(): 578 k = kind.TLSRoute 579 case kind.GRPCRoute.String(): 580 k = kind.GRPCRoute 581 case kind.UDPRoute.String(): 582 k = kind.UDPRoute 583 default: 584 // shouldn't happen 585 continue 586 } 587 name, ns, ok := strings.Cut(nsname, ".") 588 if !ok { 589 log.Errorf("invalid InternalParentName name: %s", nsname) 590 continue 591 } 592 out = append(out, ConfigKey{ 593 Kind: k, 594 Name: name, 595 Namespace: ns, 596 }) 597 } 598 return out 599 }