istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/config/kube/gateway/conversion.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 "crypto/tls" 19 "fmt" 20 "net" 21 "net/netip" 22 "sort" 23 "strings" 24 "time" 25 26 "google.golang.org/protobuf/types/known/durationpb" 27 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 klabels "k8s.io/apimachinery/pkg/labels" 29 k8s "sigs.k8s.io/gateway-api/apis/v1" 30 k8salpha "sigs.k8s.io/gateway-api/apis/v1alpha2" 31 k8sbeta "sigs.k8s.io/gateway-api/apis/v1beta1" 32 33 istio "istio.io/api/networking/v1alpha3" 34 "istio.io/istio/pilot/pkg/features" 35 "istio.io/istio/pilot/pkg/model" 36 creds "istio.io/istio/pilot/pkg/model/credentials" 37 "istio.io/istio/pilot/pkg/model/kstatus" 38 "istio.io/istio/pilot/pkg/serviceregistry/kube" 39 "istio.io/istio/pkg/config" 40 "istio.io/istio/pkg/config/constants" 41 kubeconfig "istio.io/istio/pkg/config/gateway/kube" 42 "istio.io/istio/pkg/config/host" 43 "istio.io/istio/pkg/config/protocol" 44 "istio.io/istio/pkg/config/schema/gvk" 45 "istio.io/istio/pkg/config/schema/kind" 46 "istio.io/istio/pkg/ptr" 47 "istio.io/istio/pkg/slices" 48 "istio.io/istio/pkg/util/sets" 49 ) 50 51 func sortConfigByCreationTime(configs []config.Config) { 52 sort.Slice(configs, func(i, j int) bool { 53 if configs[i].CreationTimestamp.Equal(configs[j].CreationTimestamp) { 54 in := configs[i].Namespace + "/" + configs[i].Name 55 jn := configs[j].Namespace + "/" + configs[j].Name 56 return in < jn 57 } 58 return configs[i].CreationTimestamp.Before(configs[j].CreationTimestamp) 59 }) 60 } 61 62 // convertResources is the top level entrypoint to our conversion logic, computing the full state based 63 // on KubernetesResources inputs. 64 func convertResources(r GatewayResources) IstioResources { 65 // sort HTTPRoutes by creation timestamp and namespace/name 66 sortConfigByCreationTime(r.HTTPRoute) 67 sortConfigByCreationTime(r.GRPCRoute) 68 69 result := IstioResources{} 70 ctx := configContext{ 71 GatewayResources: r, 72 AllowedReferences: convertReferencePolicies(r), 73 resourceReferences: make(map[model.ConfigKey][]model.ConfigKey), 74 } 75 76 gw, gwMap, nsReferences := convertGateways(ctx) 77 ctx.GatewayReferences = gwMap 78 result.Gateway = gw 79 80 result.VirtualService = convertVirtualService(ctx) 81 82 // Once we have gone through all route computation, we will know how many routes bound to each gateway. 83 // Report this in the status. 84 for _, dm := range gwMap { 85 for _, pri := range dm { 86 if pri.ReportAttachedRoutes != nil { 87 pri.ReportAttachedRoutes() 88 } 89 } 90 } 91 result.AllowedReferences = ctx.AllowedReferences 92 result.ReferencedNamespaceKeys = nsReferences 93 result.ResourceReferences = ctx.resourceReferences 94 return result 95 } 96 97 // convertReferencePolicies extracts all ReferencePolicy into an easily accessibly index. 98 func convertReferencePolicies(r GatewayResources) AllowedReferences { 99 res := map[Reference]map[Reference]*Grants{} 100 type namespacedGrant struct { 101 Namespace string 102 Grant *k8sbeta.ReferenceGrantSpec 103 } 104 specs := make([]namespacedGrant, 0, len(r.ReferenceGrant)) 105 106 for _, obj := range r.ReferenceGrant { 107 rp := obj.Spec.(*k8sbeta.ReferenceGrantSpec) 108 specs = append(specs, namespacedGrant{Namespace: obj.Namespace, Grant: rp}) 109 } 110 for _, ng := range specs { 111 rp := ng.Grant 112 for _, from := range rp.From { 113 fromKey := Reference{ 114 Namespace: from.Namespace, 115 } 116 if string(from.Group) == gvk.KubernetesGateway.Group && string(from.Kind) == gvk.KubernetesGateway.Kind { 117 fromKey.Kind = gvk.KubernetesGateway 118 } else if string(from.Group) == gvk.HTTPRoute.Group && string(from.Kind) == gvk.HTTPRoute.Kind { 119 fromKey.Kind = gvk.HTTPRoute 120 } else if string(from.Group) == gvk.TLSRoute.Group && string(from.Kind) == gvk.TLSRoute.Kind { 121 fromKey.Kind = gvk.TLSRoute 122 } else if string(from.Group) == gvk.TCPRoute.Group && string(from.Kind) == gvk.TCPRoute.Kind { 123 fromKey.Kind = gvk.TCPRoute 124 } else { 125 // Not supported type. Not an error; may be for another controller 126 continue 127 } 128 for _, to := range rp.To { 129 toKey := Reference{ 130 Namespace: k8s.Namespace(ng.Namespace), 131 } 132 if to.Group == "" && string(to.Kind) == gvk.Secret.Kind { 133 toKey.Kind = gvk.Secret 134 } else if to.Group == "" && string(to.Kind) == gvk.Service.Kind { 135 toKey.Kind = gvk.Service 136 } else { 137 // Not supported type. Not an error; may be for another controller 138 continue 139 } 140 if _, f := res[fromKey]; !f { 141 res[fromKey] = map[Reference]*Grants{} 142 } 143 if _, f := res[fromKey][toKey]; !f { 144 res[fromKey][toKey] = &Grants{ 145 AllowedNames: sets.New[string](), 146 } 147 } 148 if to.Name != nil { 149 res[fromKey][toKey].AllowedNames.Insert(string(*to.Name)) 150 } else { 151 res[fromKey][toKey].AllowAll = true 152 } 153 } 154 } 155 } 156 return res 157 } 158 159 // convertVirtualService takes all xRoute types and generates corresponding VirtualServices. 160 func convertVirtualService(r configContext) []config.Config { 161 result := []config.Config{} 162 for _, obj := range r.TCPRoute { 163 result = append(result, buildTCPVirtualService(r, obj)...) 164 } 165 166 for _, obj := range r.TLSRoute { 167 result = append(result, buildTLSVirtualService(r, obj)...) 168 } 169 170 // for gateway routes, build one VS per gateway+host 171 gatewayRoutes := make(map[string]map[string]*config.Config) 172 // for mesh routes, build one VS per namespace+host 173 meshRoutes := make(map[string]map[string]*config.Config) 174 for _, obj := range r.HTTPRoute { 175 buildHTTPVirtualServices(r, obj, gatewayRoutes, meshRoutes) 176 } 177 for _, obj := range r.GRPCRoute { 178 buildGRPCVirtualServices(r, obj, gatewayRoutes, meshRoutes) 179 } 180 for _, vsByHost := range gatewayRoutes { 181 for _, vsConfig := range vsByHost { 182 result = append(result, *vsConfig) 183 } 184 } 185 for _, vsByHost := range meshRoutes { 186 for _, vsConfig := range vsByHost { 187 result = append(result, *vsConfig) 188 } 189 } 190 return result 191 } 192 193 func convertHTTPRoute(r k8s.HTTPRouteRule, ctx configContext, 194 obj config.Config, pos int, enforceRefGrant bool, 195 ) (*istio.HTTPRoute, *ConfigError) { 196 // TODO: implement rewrite, timeout, corspolicy, retries 197 vs := &istio.HTTPRoute{} 198 // Auto-name the route. If upstream defines an explicit name, will use it instead 199 // The position within the route is unique 200 vs.Name = fmt.Sprintf("%s.%s.%d", obj.Namespace, obj.Name, pos) 201 202 for _, match := range r.Matches { 203 uri, err := createURIMatch(match) 204 if err != nil { 205 return nil, err 206 } 207 headers, err := createHeadersMatch(match) 208 if err != nil { 209 return nil, err 210 } 211 qp, err := createQueryParamsMatch(match) 212 if err != nil { 213 return nil, err 214 } 215 method, err := createMethodMatch(match) 216 if err != nil { 217 return nil, err 218 } 219 vs.Match = append(vs.Match, &istio.HTTPMatchRequest{ 220 Uri: uri, 221 Headers: headers, 222 QueryParams: qp, 223 Method: method, 224 }) 225 } 226 for _, filter := range r.Filters { 227 switch filter.Type { 228 case k8s.HTTPRouteFilterRequestHeaderModifier: 229 h := createHeadersFilter(filter.RequestHeaderModifier) 230 if h == nil { 231 continue 232 } 233 if vs.Headers == nil { 234 vs.Headers = &istio.Headers{} 235 } 236 vs.Headers.Request = h 237 case k8s.HTTPRouteFilterResponseHeaderModifier: 238 h := createHeadersFilter(filter.ResponseHeaderModifier) 239 if h == nil { 240 continue 241 } 242 if vs.Headers == nil { 243 vs.Headers = &istio.Headers{} 244 } 245 vs.Headers.Response = h 246 case k8s.HTTPRouteFilterRequestRedirect: 247 vs.Redirect = createRedirectFilter(filter.RequestRedirect) 248 case k8s.HTTPRouteFilterRequestMirror: 249 mirror, err := createMirrorFilter(ctx, filter.RequestMirror, obj.Namespace, enforceRefGrant, gvk.HTTPRoute) 250 if err != nil { 251 return nil, err 252 } 253 vs.Mirrors = append(vs.Mirrors, mirror) 254 case k8s.HTTPRouteFilterURLRewrite: 255 vs.Rewrite = createRewriteFilter(filter.URLRewrite) 256 default: 257 return nil, &ConfigError{ 258 Reason: InvalidFilter, 259 Message: fmt.Sprintf("unsupported filter type %q", filter.Type), 260 } 261 } 262 } 263 264 if r.Timeouts != nil { 265 if r.Timeouts.Request != nil { 266 request, _ := time.ParseDuration(string(*r.Timeouts.Request)) 267 if request != 0 { 268 vs.Timeout = durationpb.New(request) 269 } 270 } 271 if r.Timeouts.BackendRequest != nil { 272 backendRequest, _ := time.ParseDuration(string(*r.Timeouts.BackendRequest)) 273 if backendRequest != 0 { 274 timeout := durationpb.New(backendRequest) 275 if vs.Retries != nil { 276 vs.Retries.PerTryTimeout = timeout 277 } else { 278 vs.Timeout = timeout 279 } 280 } 281 } 282 } 283 284 if weightSum(r.BackendRefs) == 0 && vs.Redirect == nil { 285 // The spec requires us to return 500 when there are no >0 weight backends 286 vs.DirectResponse = &istio.HTTPDirectResponse{ 287 Status: 500, 288 } 289 } else { 290 route, backendErr, err := buildHTTPDestination(ctx, r.BackendRefs, obj.Namespace, enforceRefGrant) 291 if err != nil { 292 return nil, err 293 } 294 vs.Route = route 295 return vs, backendErr 296 } 297 298 return vs, nil 299 } 300 301 func convertGRPCRoute(r k8s.GRPCRouteRule, ctx configContext, 302 obj config.Config, pos int, enforceRefGrant bool, 303 ) (*istio.HTTPRoute, *ConfigError) { 304 // TODO: implement rewrite, timeout, mirror, corspolicy, retries 305 vs := &istio.HTTPRoute{} 306 // Auto-name the route. If upstream defines an explicit name, will use it instead 307 // The position within the route is unique 308 vs.Name = fmt.Sprintf("%s.%s.%d", obj.Namespace, obj.Name, pos) 309 310 for _, match := range r.Matches { 311 uri, err := createGRPCURIMatch(match) 312 if err != nil { 313 return nil, err 314 } 315 headers, err := createGRPCHeadersMatch(match) 316 if err != nil { 317 return nil, err 318 } 319 vs.Match = append(vs.Match, &istio.HTTPMatchRequest{ 320 Uri: uri, 321 Headers: headers, 322 }) 323 } 324 for _, filter := range r.Filters { 325 switch filter.Type { 326 case k8s.GRPCRouteFilterRequestHeaderModifier: 327 h := createHeadersFilter(filter.RequestHeaderModifier) 328 if h == nil { 329 continue 330 } 331 if vs.Headers == nil { 332 vs.Headers = &istio.Headers{} 333 } 334 vs.Headers.Request = h 335 case k8s.GRPCRouteFilterResponseHeaderModifier: 336 h := createHeadersFilter(filter.ResponseHeaderModifier) 337 if h == nil { 338 continue 339 } 340 if vs.Headers == nil { 341 vs.Headers = &istio.Headers{} 342 } 343 vs.Headers.Response = h 344 case k8s.GRPCRouteFilterRequestMirror: 345 mirror, err := createMirrorFilter(ctx, filter.RequestMirror, obj.Namespace, enforceRefGrant, gvk.GRPCRoute) 346 if err != nil { 347 return nil, err 348 } 349 vs.Mirrors = append(vs.Mirrors, mirror) 350 default: 351 return nil, &ConfigError{ 352 Reason: InvalidFilter, 353 Message: fmt.Sprintf("unsupported filter type %q", filter.Type), 354 } 355 } 356 } 357 358 if grpcWeightSum(r.BackendRefs) == 0 && vs.Redirect == nil { 359 // The spec requires us to return 500 when there are no >0 weight backends 360 vs.DirectResponse = &istio.HTTPDirectResponse{ 361 Status: 500, 362 } 363 } else { 364 route, backendErr, err := buildGRPCDestination(ctx, r.BackendRefs, obj.Namespace, enforceRefGrant) 365 if err != nil { 366 return nil, err 367 } 368 vs.Route = route 369 return vs, backendErr 370 } 371 372 return vs, nil 373 } 374 375 func parentTypes(rpi []routeParentReference) (mesh, gateway bool) { 376 for _, r := range rpi { 377 if r.IsMesh() { 378 mesh = true 379 } else { 380 gateway = true 381 } 382 } 383 return 384 } 385 386 func buildHTTPVirtualServices( 387 ctx configContext, 388 obj config.Config, 389 gatewayRoutes map[string]map[string]*config.Config, 390 meshRoutes map[string]map[string]*config.Config, 391 ) { 392 route := obj.Spec.(*k8s.HTTPRouteSpec) 393 parentRefs := extractParentReferenceInfo(ctx.GatewayReferences, route.ParentRefs, route.Hostnames, gvk.HTTPRoute, obj.Namespace) 394 reportStatus := func(results []RouteParentResult) { 395 obj.Status.(*kstatus.WrappedStatus).Mutate(func(s config.Status) config.Status { 396 rs := s.(*k8s.HTTPRouteStatus) 397 rs.Parents = createRouteStatus(results, obj, rs.Parents) 398 return rs 399 }) 400 } 401 402 type conversionResult struct { 403 error *ConfigError 404 routes []*istio.HTTPRoute 405 } 406 convertRules := func(mesh bool) conversionResult { 407 res := conversionResult{} 408 for n, r := range route.Rules { 409 // split the rule to make sure each rule has up to one match 410 matches := slices.Reference(r.Matches) 411 if len(matches) == 0 { 412 matches = append(matches, nil) 413 } 414 for _, m := range matches { 415 if m != nil { 416 r.Matches = []k8s.HTTPRouteMatch{*m} 417 } 418 vs, err := convertHTTPRoute(r, ctx, obj, n, !mesh) 419 // This was a hard error 420 if vs == nil { 421 res.error = err 422 return conversionResult{error: err} 423 } 424 // Got an error but also routes 425 if err != nil { 426 res.error = err 427 } 428 429 res.routes = append(res.routes, vs) 430 } 431 } 432 return res 433 } 434 meshResult, gwResult := buildMeshAndGatewayRoutes(parentRefs, convertRules) 435 436 reportStatus(slices.Map(parentRefs, func(r routeParentReference) RouteParentResult { 437 res := RouteParentResult{ 438 OriginalReference: r.OriginalReference, 439 DeniedReason: r.DeniedReason, 440 RouteError: gwResult.error, 441 } 442 if r.IsMesh() { 443 res.RouteError = meshResult.error 444 } 445 return res 446 })) 447 count := 0 448 for _, parent := range filteredReferences(parentRefs) { 449 // for gateway routes, build one VS per gateway+host 450 routeMap := gatewayRoutes 451 routeKey := parent.InternalName 452 vsHosts := hostnameToStringList(route.Hostnames) 453 routes := gwResult.routes 454 if parent.IsMesh() { 455 routes = meshResult.routes 456 // for mesh routes, build one VS per namespace/port->host 457 routeMap = meshRoutes 458 routeKey = obj.Namespace 459 if parent.OriginalReference.Port != nil { 460 routes = augmentPortMatch(routes, *parent.OriginalReference.Port) 461 routeKey += fmt.Sprintf("/%d", *parent.OriginalReference.Port) 462 } 463 if parent.InternalKind == gvk.ServiceEntry { 464 vsHosts = serviceEntryHosts(ctx.ServiceEntry, 465 string(parent.OriginalReference.Name), 466 string(ptr.OrDefault(parent.OriginalReference.Namespace, k8s.Namespace(obj.Namespace)))) 467 } else { 468 vsHosts = []string{fmt.Sprintf("%s.%s.svc.%s", 469 parent.OriginalReference.Name, ptr.OrDefault(parent.OriginalReference.Namespace, k8s.Namespace(obj.Namespace)), ctx.Domain)} 470 } 471 } 472 if len(routes) == 0 { 473 continue 474 } 475 if _, f := routeMap[routeKey]; !f { 476 routeMap[routeKey] = make(map[string]*config.Config) 477 } 478 479 // Create one VS per hostname with a single hostname. 480 // This ensures we can treat each hostname independently, as the spec requires 481 for _, h := range vsHosts { 482 if !parent.hostnameAllowedByIsolation(h) { 483 // TODO: standardize a status message for this upstream and report 484 continue 485 } 486 if cfg := routeMap[routeKey][h]; cfg != nil { 487 // merge http routes 488 vs := cfg.Spec.(*istio.VirtualService) 489 vs.Http = append(vs.Http, routes...) 490 // append parents 491 cfg.Annotations[constants.InternalParentNames] = fmt.Sprintf("%s,%s/%s.%s", 492 cfg.Annotations[constants.InternalParentNames], obj.GroupVersionKind.Kind, obj.Name, obj.Namespace) 493 } else { 494 name := fmt.Sprintf("%s-%d-%s", obj.Name, count, constants.KubernetesGatewayName) 495 routeMap[routeKey][h] = &config.Config{ 496 Meta: config.Meta{ 497 CreationTimestamp: obj.CreationTimestamp, 498 GroupVersionKind: gvk.VirtualService, 499 Name: name, 500 Annotations: routeMeta(obj), 501 Namespace: obj.Namespace, 502 Domain: ctx.Domain, 503 }, 504 Spec: &istio.VirtualService{ 505 Hosts: []string{h}, 506 Gateways: []string{parent.InternalName}, 507 Http: routes, 508 }, 509 } 510 count++ 511 } 512 } 513 } 514 for _, vsByHost := range gatewayRoutes { 515 for _, cfg := range vsByHost { 516 vs := cfg.Spec.(*istio.VirtualService) 517 sortHTTPRoutes(vs.Http) 518 } 519 } 520 for _, vsByHost := range meshRoutes { 521 for _, cfg := range vsByHost { 522 vs := cfg.Spec.(*istio.VirtualService) 523 sortHTTPRoutes(vs.Http) 524 } 525 } 526 } 527 528 func serviceEntryHosts(ses []config.Config, name, namespace string) []string { 529 for _, obj := range ses { 530 if obj.Meta.Name == name { 531 ns := obj.Meta.Namespace 532 if ns == "" { 533 ns = metav1.NamespaceDefault 534 } 535 if ns == namespace { 536 se := obj.Spec.(*istio.ServiceEntry) 537 return se.Hosts 538 } 539 } 540 } 541 return []string{} 542 } 543 544 func buildMeshAndGatewayRoutes[T any](parentRefs []routeParentReference, convertRules func(mesh bool) T) (T, T) { 545 var meshResult, gwResult T 546 needMesh, needGw := parentTypes(parentRefs) 547 if needMesh { 548 meshResult = convertRules(true) 549 } 550 if needGw { 551 gwResult = convertRules(false) 552 } 553 return meshResult, gwResult 554 } 555 556 func augmentPortMatch(routes []*istio.HTTPRoute, port k8s.PortNumber) []*istio.HTTPRoute { 557 res := make([]*istio.HTTPRoute, 0, len(routes)) 558 for _, r := range routes { 559 r = r.DeepCopy() 560 for _, m := range r.Match { 561 m.Port = uint32(port) 562 } 563 if len(r.Match) == 0 { 564 r.Match = []*istio.HTTPMatchRequest{{ 565 Port: uint32(port), 566 }} 567 } 568 res = append(res, r) 569 } 570 return res 571 } 572 573 func augmentTCPPortMatch(routes []*istio.TCPRoute, port k8s.PortNumber) []*istio.TCPRoute { 574 res := make([]*istio.TCPRoute, 0, len(routes)) 575 for _, r := range routes { 576 r = r.DeepCopy() 577 for _, m := range r.Match { 578 m.Port = uint32(port) 579 } 580 if len(r.Match) == 0 { 581 r.Match = []*istio.L4MatchAttributes{{ 582 Port: uint32(port), 583 }} 584 } 585 res = append(res, r) 586 } 587 return res 588 } 589 590 func augmentTLSPortMatch(routes []*istio.TLSRoute, port *k8s.PortNumber, parentHosts []string) []*istio.TLSRoute { 591 res := make([]*istio.TLSRoute, 0, len(routes)) 592 for _, r := range routes { 593 r = r.DeepCopy() 594 if len(r.Match) == 1 && slices.Equal(r.Match[0].SniHosts, []string{"*"}) { 595 // For mesh, we use parent hosts for SNI if TLSRroute.hostnames were not specified. 596 r.Match[0].SniHosts = parentHosts 597 } 598 for _, m := range r.Match { 599 if port != nil { 600 m.Port = uint32(*port) 601 } 602 } 603 res = append(res, r) 604 } 605 return res 606 } 607 608 func compatibleRoutesForHost(routes []*istio.TLSRoute, parentHost string) []*istio.TLSRoute { 609 res := make([]*istio.TLSRoute, 0, len(routes)) 610 for _, r := range routes { 611 if len(r.Match) == 1 && len(r.Match[0].SniHosts) > 1 { 612 r = r.DeepCopy() 613 sniHosts := []string{} 614 for _, h := range r.Match[0].SniHosts { 615 if host.Name(parentHost).Matches(host.Name(h)) { 616 sniHosts = append(sniHosts, h) 617 } 618 } 619 r.Match[0].SniHosts = sniHosts 620 } 621 res = append(res, r) 622 } 623 return res 624 } 625 626 func buildGRPCVirtualServices( 627 ctx configContext, 628 obj config.Config, 629 gatewayRoutes map[string]map[string]*config.Config, 630 meshRoutes map[string]map[string]*config.Config, 631 ) { 632 route := obj.Spec.(*k8s.GRPCRouteSpec) 633 parentRefs := extractParentReferenceInfo(ctx.GatewayReferences, route.ParentRefs, route.Hostnames, gvk.GRPCRoute, obj.Namespace) 634 reportStatus := func(results []RouteParentResult) { 635 obj.Status.(*kstatus.WrappedStatus).Mutate(func(s config.Status) config.Status { 636 rs := s.(*k8s.GRPCRouteStatus) 637 rs.Parents = createRouteStatus(results, obj, rs.Parents) 638 return rs 639 }) 640 } 641 642 type conversionResult struct { 643 error *ConfigError 644 routes []*istio.HTTPRoute 645 } 646 convertRules := func(mesh bool) conversionResult { 647 res := conversionResult{} 648 for n, r := range route.Rules { 649 // split the rule to make sure each rule has up to one match 650 matches := slices.Reference(r.Matches) 651 if len(matches) == 0 { 652 matches = append(matches, nil) 653 } 654 for _, m := range matches { 655 if m != nil { 656 r.Matches = []k8s.GRPCRouteMatch{*m} 657 } 658 vs, err := convertGRPCRoute(r, ctx, obj, n, !mesh) 659 // This was a hard error 660 if vs == nil { 661 res.error = err 662 return conversionResult{error: err} 663 } 664 // Got an error but also routes 665 if err != nil { 666 res.error = err 667 } 668 669 res.routes = append(res.routes, vs) 670 } 671 } 672 return res 673 } 674 meshResult, gwResult := buildMeshAndGatewayRoutes(parentRefs, convertRules) 675 676 reportStatus(slices.Map(parentRefs, func(r routeParentReference) RouteParentResult { 677 res := RouteParentResult{ 678 OriginalReference: r.OriginalReference, 679 DeniedReason: r.DeniedReason, 680 RouteError: gwResult.error, 681 } 682 if r.IsMesh() { 683 res.RouteError = meshResult.error 684 } 685 return res 686 })) 687 count := 0 688 for _, parent := range filteredReferences(parentRefs) { 689 // for gateway routes, build one VS per gateway+host 690 routeMap := gatewayRoutes 691 routeKey := parent.InternalName 692 vsHosts := hostnameToStringList(route.Hostnames) 693 routes := gwResult.routes 694 if parent.IsMesh() { 695 routes = meshResult.routes 696 // for mesh routes, build one VS per namespace/port->host 697 routeMap = meshRoutes 698 routeKey = obj.Namespace 699 if parent.OriginalReference.Port != nil { 700 routes = augmentPortMatch(routes, *parent.OriginalReference.Port) 701 routeKey += fmt.Sprintf("/%d", *parent.OriginalReference.Port) 702 } 703 if parent.InternalKind == gvk.ServiceEntry { 704 vsHosts = serviceEntryHosts(ctx.ServiceEntry, 705 string(parent.OriginalReference.Name), 706 string(ptr.OrDefault(parent.OriginalReference.Namespace, k8s.Namespace(obj.Namespace)))) 707 } else { 708 vsHosts = []string{fmt.Sprintf("%s.%s.svc.%s", 709 parent.OriginalReference.Name, ptr.OrDefault(parent.OriginalReference.Namespace, k8s.Namespace(obj.Namespace)), ctx.Domain)} 710 } 711 } 712 if len(routes) == 0 { 713 continue 714 } 715 if _, f := routeMap[routeKey]; !f { 716 routeMap[routeKey] = make(map[string]*config.Config) 717 } 718 719 // Create one VS per hostname with a single hostname. 720 // This ensures we can treat each hostname independently, as the spec requires 721 for _, h := range vsHosts { 722 if cfg := routeMap[routeKey][h]; cfg != nil { 723 // merge http routes 724 vs := cfg.Spec.(*istio.VirtualService) 725 vs.Http = append(vs.Http, routes...) 726 // append parents 727 cfg.Annotations[constants.InternalParentNames] = fmt.Sprintf("%s,%s/%s.%s", 728 cfg.Annotations[constants.InternalParentNames], obj.GroupVersionKind.Kind, obj.Name, obj.Namespace) 729 } else { 730 name := fmt.Sprintf("%s-%d-%s", obj.Name, count, constants.KubernetesGatewayName) 731 routeMap[routeKey][h] = &config.Config{ 732 Meta: config.Meta{ 733 CreationTimestamp: obj.CreationTimestamp, 734 GroupVersionKind: gvk.VirtualService, 735 Name: name, 736 Annotations: routeMeta(obj), 737 Namespace: obj.Namespace, 738 Domain: ctx.Domain, 739 }, 740 Spec: &istio.VirtualService{ 741 Hosts: []string{h}, 742 Gateways: []string{parent.InternalName}, 743 Http: routes, 744 }, 745 } 746 count++ 747 } 748 } 749 } 750 for _, vsByHost := range gatewayRoutes { 751 for _, cfg := range vsByHost { 752 vs := cfg.Spec.(*istio.VirtualService) 753 sortHTTPRoutes(vs.Http) 754 } 755 } 756 for _, vsByHost := range meshRoutes { 757 for _, cfg := range vsByHost { 758 vs := cfg.Spec.(*istio.VirtualService) 759 sortHTTPRoutes(vs.Http) 760 } 761 } 762 } 763 764 func routeMeta(obj config.Config) map[string]string { 765 m := parentMeta(obj, nil) 766 m[constants.InternalRouteSemantics] = constants.RouteSemanticsGateway 767 return m 768 } 769 770 // sortHTTPRoutes sorts generated vs routes to meet gateway-api requirements 771 // see https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.HTTPRouteRule 772 func sortHTTPRoutes(routes []*istio.HTTPRoute) { 773 sort.SliceStable(routes, func(i, j int) bool { 774 if len(routes[i].Match) == 0 { 775 return false 776 } else if len(routes[j].Match) == 0 { 777 return true 778 } 779 // Only look at match[0], we always generate only one match 780 m1, m2 := routes[i].Match[0], routes[j].Match[0] 781 r1, r2 := getURIRank(m1), getURIRank(m2) 782 len1, len2 := getURILength(m1), getURILength(m2) 783 switch { 784 // 1: Exact/Prefix/Regex 785 case r1 != r2: 786 return r1 > r2 787 case len1 != len2: 788 return len1 > len2 789 // 2: method math 790 case (m1.Method == nil) != (m2.Method == nil): 791 return m1.Method != nil 792 // 3: number of header matches 793 case len(m1.Headers) != len(m2.Headers): 794 return len(m1.Headers) > len(m2.Headers) 795 // 4: number of query matches 796 default: 797 return len(m1.QueryParams) > len(m2.QueryParams) 798 } 799 }) 800 } 801 802 // getURIRank ranks a URI match type. Exact > Prefix > Regex 803 func getURIRank(match *istio.HTTPMatchRequest) int { 804 if match.Uri == nil { 805 return -1 806 } 807 switch match.Uri.MatchType.(type) { 808 case *istio.StringMatch_Exact: 809 return 3 810 case *istio.StringMatch_Prefix: 811 return 2 812 case *istio.StringMatch_Regex: 813 return 1 814 } 815 // should not happen 816 return -1 817 } 818 819 func getURILength(match *istio.HTTPMatchRequest) int { 820 if match.Uri == nil { 821 return 0 822 } 823 switch match.Uri.MatchType.(type) { 824 case *istio.StringMatch_Prefix: 825 return len(match.Uri.GetPrefix()) 826 case *istio.StringMatch_Exact: 827 return len(match.Uri.GetExact()) 828 case *istio.StringMatch_Regex: 829 return len(match.Uri.GetRegex()) 830 } 831 // should not happen 832 return -1 833 } 834 835 func parentMeta(obj config.Config, sectionName *k8s.SectionName) map[string]string { 836 name := fmt.Sprintf("%s/%s.%s", obj.GroupVersionKind.Kind, obj.Name, obj.Namespace) 837 if sectionName != nil { 838 name = fmt.Sprintf("%s/%s/%s.%s", obj.GroupVersionKind.Kind, obj.Name, *sectionName, obj.Namespace) 839 } 840 return map[string]string{ 841 constants.InternalParentNames: name, 842 } 843 } 844 845 func hostnameToStringList(h []k8s.Hostname) []string { 846 // In the Istio API, empty hostname is not allowed. In the Kubernetes API hosts means "any" 847 if len(h) == 0 { 848 return []string{"*"} 849 } 850 return slices.Map(h, func(e k8s.Hostname) string { 851 return string(e) 852 }) 853 } 854 855 func toInternalParentReference(p k8s.ParentReference, localNamespace string) (parentKey, error) { 856 empty := parentKey{} 857 kind := ptr.OrDefault((*string)(p.Kind), gvk.KubernetesGateway.Kind) 858 group := ptr.OrDefault((*string)(p.Group), gvk.KubernetesGateway.Group) 859 var ik config.GroupVersionKind 860 var ns string 861 // Currently supported types are Gateway, Service, and ServiceEntry 862 if kind == gvk.KubernetesGateway.Kind && group == gvk.KubernetesGateway.Group { 863 ik = gvk.KubernetesGateway 864 } else if kind == gvk.Service.Kind && group == gvk.Service.Group { 865 ik = gvk.Service 866 } else if kind == gvk.ServiceEntry.Kind && group == gvk.ServiceEntry.Group { 867 ik = gvk.ServiceEntry 868 } else { 869 return empty, fmt.Errorf("unsupported parentKey: %v/%v", p.Group, kind) 870 } 871 // Unset namespace means "same namespace" 872 ns = ptr.OrDefault((*string)(p.Namespace), localNamespace) 873 return parentKey{ 874 Kind: ik, 875 Name: string(p.Name), 876 Namespace: ns, 877 }, nil 878 } 879 880 func referenceAllowed( 881 parent *parentInfo, 882 routeKind config.GroupVersionKind, 883 parentRef parentReference, 884 hostnames []k8s.Hostname, 885 namespace string, 886 ) *ParentError { 887 if parentRef.Kind == gvk.Service || parentRef.Kind == gvk.ServiceEntry { 888 // TODO: check if the service reference is valid 889 if false { 890 return &ParentError{ 891 Reason: ParentErrorParentRefConflict, 892 Message: fmt.Sprintf("parent service: %q is invalid", parentRef.Name), 893 } 894 } 895 } else { 896 // First, check section and port apply. This must come first 897 if parentRef.Port != 0 && parentRef.Port != parent.Port { 898 return &ParentError{ 899 Reason: ParentErrorNotAccepted, 900 Message: fmt.Sprintf("port %v not found", parentRef.Port), 901 } 902 } 903 if len(parentRef.SectionName) > 0 && parentRef.SectionName != parent.SectionName { 904 return &ParentError{ 905 Reason: ParentErrorNotAccepted, 906 Message: fmt.Sprintf("sectionName %q not found", parentRef.SectionName), 907 } 908 } 909 910 // Next check the hostnames are a match. This is a bi-directional wildcard match. Only one route 911 // hostname must match for it to be allowed (but the others will be filtered at runtime) 912 // If either is empty its treated as a wildcard which always matches 913 914 if len(hostnames) == 0 { 915 hostnames = []k8s.Hostname{"*"} 916 } 917 if len(parent.Hostnames) > 0 { 918 // TODO: the spec actually has a label match, not a string match. That is, *.com does not match *.apple.com 919 // We are doing a string match here 920 matched := false 921 hostMatched := false 922 out: 923 for _, routeHostname := range hostnames { 924 for _, parentHostNamespace := range parent.Hostnames { 925 spl := strings.Split(parentHostNamespace, "/") 926 parentNamespace, parentHostname := spl[0], spl[1] 927 hostnameMatch := host.Name(parentHostname).Matches(host.Name(routeHostname)) 928 namespaceMatch := parentNamespace == "*" || parentNamespace == namespace 929 hostMatched = hostMatched || hostnameMatch 930 if hostnameMatch && namespaceMatch { 931 matched = true 932 break out 933 } 934 } 935 } 936 if !matched { 937 if hostMatched { 938 return &ParentError{ 939 Reason: ParentErrorNotAllowed, 940 Message: fmt.Sprintf( 941 "hostnames matched parent hostname %q, but namespace %q is not allowed by the parent", 942 parent.OriginalHostname, namespace, 943 ), 944 } 945 } 946 return &ParentError{ 947 Reason: ParentErrorNoHostname, 948 Message: fmt.Sprintf( 949 "no hostnames matched parent hostname %q", 950 parent.OriginalHostname, 951 ), 952 } 953 } 954 } 955 } 956 // Also make sure this route kind is allowed 957 matched := false 958 for _, ak := range parent.AllowedKinds { 959 if string(ak.Kind) == routeKind.Kind && ptr.OrDefault((*string)(ak.Group), gvk.GatewayClass.Group) == routeKind.Group { 960 matched = true 961 break 962 } 963 } 964 if !matched { 965 return &ParentError{ 966 Reason: ParentErrorNotAllowed, 967 Message: fmt.Sprintf("kind %v is not allowed", routeKind), 968 } 969 } 970 return nil 971 } 972 973 func extractParentReferenceInfo(gateways map[parentKey][]*parentInfo, routeRefs []k8s.ParentReference, 974 hostnames []k8s.Hostname, kind config.GroupVersionKind, localNamespace string, 975 ) []routeParentReference { 976 parentRefs := []routeParentReference{} 977 for _, ref := range routeRefs { 978 ir, err := toInternalParentReference(ref, localNamespace) 979 if err != nil { 980 // Cannot handle the reference. Maybe it is for another controller, so we just ignore it 981 continue 982 } 983 pk := parentReference{ 984 parentKey: ir, 985 SectionName: ptr.OrEmpty(ref.SectionName), 986 Port: ptr.OrEmpty(ref.Port), 987 } 988 gk := ir 989 if ir.Kind == gvk.Service || ir.Kind == gvk.ServiceEntry { 990 gk = meshParentKey 991 } 992 appendParent := func(pr *parentInfo, pk parentReference) { 993 bannedHostnames := sets.New[string]() 994 for _, gw := range gateways[gk] { 995 if gw == pr { 996 continue // do not ban ourself 997 } 998 if gw.Port != pr.Port { 999 // We only care about listeners on the same port 1000 continue 1001 } 1002 if gw.Protocol != pr.Protocol { 1003 // We only care about listeners on the same protocol 1004 continue 1005 } 1006 bannedHostnames.Insert(gw.OriginalHostname) 1007 } 1008 rpi := routeParentReference{ 1009 InternalName: pr.InternalName, 1010 InternalKind: ir.Kind, 1011 Hostname: pr.OriginalHostname, 1012 DeniedReason: referenceAllowed(pr, kind, pk, hostnames, localNamespace), 1013 OriginalReference: ref, 1014 BannedHostnames: bannedHostnames.Copy().Delete(pr.OriginalHostname), 1015 } 1016 if rpi.DeniedReason == nil { 1017 // Record that we were able to bind to the parent 1018 pr.AttachedRoutes++ 1019 } 1020 parentRefs = append(parentRefs, rpi) 1021 } 1022 for _, gw := range gateways[gk] { 1023 // Append all matches. Note we may be adding mismatch section or ports; this is handled later 1024 appendParent(gw, pk) 1025 } 1026 } 1027 // Ensure stable order 1028 slices.SortBy(parentRefs, func(a routeParentReference) string { 1029 return parentRefString(a.OriginalReference) 1030 }) 1031 return parentRefs 1032 } 1033 1034 func buildTCPVirtualService(ctx configContext, obj config.Config) []config.Config { 1035 route := obj.Spec.(*k8salpha.TCPRouteSpec) 1036 parentRefs := extractParentReferenceInfo(ctx.GatewayReferences, route.ParentRefs, nil, gvk.TCPRoute, obj.Namespace) 1037 1038 reportStatus := func(results []RouteParentResult) { 1039 obj.Status.(*kstatus.WrappedStatus).Mutate(func(s config.Status) config.Status { 1040 rs := s.(*k8salpha.TCPRouteStatus) 1041 rs.Parents = createRouteStatus(results, obj, rs.Parents) 1042 return rs 1043 }) 1044 } 1045 type conversionResult struct { 1046 error *ConfigError 1047 routes []*istio.TCPRoute 1048 } 1049 convertRules := func(mesh bool) conversionResult { 1050 res := conversionResult{} 1051 for _, r := range route.Rules { 1052 vs, err := convertTCPRoute(ctx, r, obj, !mesh) 1053 // This was a hard error 1054 if vs == nil { 1055 res.error = err 1056 return conversionResult{error: err} 1057 } 1058 // Got an error but also routes 1059 if err != nil { 1060 res.error = err 1061 } 1062 res.routes = append(res.routes, vs) 1063 } 1064 return res 1065 } 1066 meshResult, gwResult := buildMeshAndGatewayRoutes(parentRefs, convertRules) 1067 reportStatus(slices.Map(parentRefs, func(r routeParentReference) RouteParentResult { 1068 res := RouteParentResult{ 1069 OriginalReference: r.OriginalReference, 1070 DeniedReason: r.DeniedReason, 1071 RouteError: gwResult.error, 1072 } 1073 if r.IsMesh() { 1074 res.RouteError = meshResult.error 1075 } 1076 return res 1077 })) 1078 1079 vs := []config.Config{} 1080 for _, parent := range filteredReferences(parentRefs) { 1081 routes := gwResult.routes 1082 vsHosts := []string{"*"} 1083 if parent.IsMesh() { 1084 routes = meshResult.routes 1085 if parent.OriginalReference.Port != nil { 1086 routes = augmentTCPPortMatch(routes, *parent.OriginalReference.Port) 1087 } 1088 if parent.InternalKind == gvk.ServiceEntry { 1089 vsHosts = serviceEntryHosts(ctx.ServiceEntry, 1090 string(parent.OriginalReference.Name), 1091 string(ptr.OrDefault(parent.OriginalReference.Namespace, k8s.Namespace(obj.Namespace)))) 1092 } else { 1093 vsHosts = []string{fmt.Sprintf("%s.%s.svc.%s", 1094 parent.OriginalReference.Name, ptr.OrDefault(parent.OriginalReference.Namespace, k8s.Namespace(obj.Namespace)), ctx.Domain)} 1095 } 1096 } 1097 for i, host := range vsHosts { 1098 name := fmt.Sprintf("%s-tcp-%d-%s", obj.Name, i, constants.KubernetesGatewayName) 1099 // Create one VS per hostname with a single hostname. 1100 // This ensures we can treat each hostname independently, as the spec requires 1101 vs = append(vs, config.Config{ 1102 Meta: config.Meta{ 1103 CreationTimestamp: obj.CreationTimestamp, 1104 GroupVersionKind: gvk.VirtualService, 1105 Name: name, 1106 Annotations: routeMeta(obj), 1107 Namespace: obj.Namespace, 1108 Domain: ctx.Domain, 1109 }, 1110 Spec: &istio.VirtualService{ 1111 // We can use wildcard here since each listener can have at most one route bound to it, so we have 1112 // a single VS per Gateway. 1113 Hosts: []string{host}, 1114 Gateways: []string{parent.InternalName}, 1115 Tcp: routes, 1116 }, 1117 }) 1118 } 1119 } 1120 return vs 1121 } 1122 1123 func buildTLSVirtualService(ctx configContext, obj config.Config) []config.Config { 1124 route := obj.Spec.(*k8salpha.TLSRouteSpec) 1125 parentRefs := extractParentReferenceInfo(ctx.GatewayReferences, route.ParentRefs, nil, gvk.TLSRoute, obj.Namespace) 1126 1127 reportStatus := func(results []RouteParentResult) { 1128 obj.Status.(*kstatus.WrappedStatus).Mutate(func(s config.Status) config.Status { 1129 rs := s.(*k8salpha.TLSRouteStatus) 1130 rs.Parents = createRouteStatus(results, obj, rs.Parents) 1131 return rs 1132 }) 1133 } 1134 type conversionResult struct { 1135 error *ConfigError 1136 routes []*istio.TLSRoute 1137 } 1138 convertRules := func(mesh bool) conversionResult { 1139 res := conversionResult{} 1140 for _, r := range route.Rules { 1141 vs, err := convertTLSRoute(ctx, r, obj, !mesh) 1142 // This was a hard error 1143 if vs == nil { 1144 res.error = err 1145 return conversionResult{error: err} 1146 } 1147 // Got an error but also routes 1148 if err != nil { 1149 res.error = err 1150 } 1151 res.routes = append(res.routes, vs) 1152 } 1153 return res 1154 } 1155 meshResult, gwResult := buildMeshAndGatewayRoutes(parentRefs, convertRules) 1156 reportStatus(slices.Map(parentRefs, func(r routeParentReference) RouteParentResult { 1157 res := RouteParentResult{ 1158 OriginalReference: r.OriginalReference, 1159 DeniedReason: r.DeniedReason, 1160 RouteError: gwResult.error, 1161 } 1162 if r.IsMesh() { 1163 res.RouteError = meshResult.error 1164 } 1165 return res 1166 })) 1167 1168 vs := []config.Config{} 1169 for _, parent := range filteredReferences(parentRefs) { 1170 routes := gwResult.routes 1171 vsHosts := hostnameToStringList(route.Hostnames) 1172 if parent.IsMesh() { 1173 routes = meshResult.routes 1174 if parent.InternalKind == gvk.ServiceEntry { 1175 vsHosts = serviceEntryHosts(ctx.ServiceEntry, 1176 string(parent.OriginalReference.Name), 1177 string(ptr.OrDefault(parent.OriginalReference.Namespace, k8s.Namespace(obj.Namespace)))) 1178 } else { 1179 host := fmt.Sprintf("%s.%s.svc.%s", 1180 parent.OriginalReference.Name, ptr.OrDefault(parent.OriginalReference.Namespace, k8s.Namespace(obj.Namespace)), ctx.Domain) 1181 vsHosts = []string{host} 1182 } 1183 routes = augmentTLSPortMatch(routes, parent.OriginalReference.Port, vsHosts) 1184 } 1185 1186 for i, host := range vsHosts { 1187 name := fmt.Sprintf("%s-tls-%d-%s", obj.Name, i, constants.KubernetesGatewayName) 1188 filteredRoutes := routes 1189 if parent.IsMesh() { 1190 filteredRoutes = compatibleRoutesForHost(routes, host) 1191 } 1192 // Create one VS per hostname with a single hostname. 1193 // This ensures we can treat each hostname independently, as the spec requires 1194 vs = append(vs, config.Config{ 1195 Meta: config.Meta{ 1196 CreationTimestamp: obj.CreationTimestamp, 1197 GroupVersionKind: gvk.VirtualService, 1198 Name: name, 1199 Annotations: routeMeta(obj), 1200 Namespace: obj.Namespace, 1201 Domain: ctx.Domain, 1202 }, 1203 Spec: &istio.VirtualService{ 1204 Hosts: []string{host}, 1205 Gateways: []string{parent.InternalName}, 1206 Tls: filteredRoutes, 1207 }, 1208 }) 1209 } 1210 } 1211 return vs 1212 } 1213 1214 func convertTCPRoute(ctx configContext, r k8salpha.TCPRouteRule, obj config.Config, enforceRefGrant bool) (*istio.TCPRoute, *ConfigError) { 1215 if tcpWeightSum(r.BackendRefs) == 0 { 1216 // The spec requires us to reject connections when there are no >0 weight backends 1217 // We don't have a great way to do it. TODO: add a fault injection API for TCP? 1218 return &istio.TCPRoute{ 1219 Route: []*istio.RouteDestination{{ 1220 Destination: &istio.Destination{ 1221 Host: "internal.cluster.local", 1222 Subset: "zero-weight", 1223 Port: &istio.PortSelector{Number: 65535}, 1224 }, 1225 Weight: 0, 1226 }}, 1227 }, nil 1228 } 1229 dest, backendErr, err := buildTCPDestination(ctx, r.BackendRefs, obj.Namespace, enforceRefGrant, gvk.TCPRoute) 1230 if err != nil { 1231 return nil, err 1232 } 1233 return &istio.TCPRoute{ 1234 Route: dest, 1235 }, backendErr 1236 } 1237 1238 func convertTLSRoute(ctx configContext, r k8salpha.TLSRouteRule, obj config.Config, enforceRefGrant bool) (*istio.TLSRoute, *ConfigError) { 1239 if tcpWeightSum(r.BackendRefs) == 0 { 1240 // The spec requires us to reject connections when there are no >0 weight backends 1241 // We don't have a great way to do it. TODO: add a fault injection API for TCP? 1242 return &istio.TLSRoute{ 1243 Route: []*istio.RouteDestination{{ 1244 Destination: &istio.Destination{ 1245 Host: "internal.cluster.local", 1246 Subset: "zero-weight", 1247 Port: &istio.PortSelector{Number: 65535}, 1248 }, 1249 Weight: 0, 1250 }}, 1251 }, nil 1252 } 1253 dest, backendErr, err := buildTCPDestination(ctx, r.BackendRefs, obj.Namespace, enforceRefGrant, gvk.TLSRoute) 1254 if err != nil { 1255 return nil, err 1256 } 1257 return &istio.TLSRoute{ 1258 Match: buildTLSMatch(obj.Spec.(*k8salpha.TLSRouteSpec).Hostnames), 1259 Route: dest, 1260 }, backendErr 1261 } 1262 1263 func buildTCPDestination( 1264 ctx configContext, 1265 forwardTo []k8s.BackendRef, 1266 ns string, 1267 enforceRefGrant bool, 1268 k config.GroupVersionKind, 1269 ) ([]*istio.RouteDestination, *ConfigError, *ConfigError) { 1270 if forwardTo == nil { 1271 return nil, nil, nil 1272 } 1273 1274 weights := []int{} 1275 action := []k8s.BackendRef{} 1276 for _, w := range forwardTo { 1277 wt := int(ptr.OrDefault(w.Weight, 1)) 1278 if wt == 0 { 1279 continue 1280 } 1281 action = append(action, w) 1282 weights = append(weights, wt) 1283 } 1284 if len(weights) == 1 { 1285 weights = []int{0} 1286 } 1287 1288 var invalidBackendErr *ConfigError 1289 res := []*istio.RouteDestination{} 1290 for i, fwd := range action { 1291 dst, err := buildDestination(ctx, fwd, ns, enforceRefGrant, k) 1292 if err != nil { 1293 if isInvalidBackend(err) { 1294 invalidBackendErr = err 1295 // keep going, we will gracefully drop invalid backends 1296 } else { 1297 return nil, nil, err 1298 } 1299 } 1300 res = append(res, &istio.RouteDestination{ 1301 Destination: dst, 1302 Weight: int32(weights[i]), 1303 }) 1304 } 1305 return res, invalidBackendErr, nil 1306 } 1307 1308 func buildTLSMatch(hostnames []k8s.Hostname) []*istio.TLSMatchAttributes { 1309 // Currently, the spec only supports extensions beyond hostname, which are not currently implemented by Istio. 1310 return []*istio.TLSMatchAttributes{{ 1311 SniHosts: hostnamesToStringListWithWildcard(hostnames), 1312 }} 1313 } 1314 1315 func hostnamesToStringListWithWildcard(h []k8s.Hostname) []string { 1316 if len(h) == 0 { 1317 return []string{"*"} 1318 } 1319 res := make([]string, 0, len(h)) 1320 for _, i := range h { 1321 res = append(res, string(i)) 1322 } 1323 return res 1324 } 1325 1326 func weightSum(forwardTo []k8s.HTTPBackendRef) int { 1327 sum := int32(0) 1328 for _, w := range forwardTo { 1329 sum += ptr.OrDefault(w.Weight, 1) 1330 } 1331 return int(sum) 1332 } 1333 1334 func grpcWeightSum(forwardTo []k8s.GRPCBackendRef) int { 1335 sum := int32(0) 1336 for _, w := range forwardTo { 1337 sum += ptr.OrDefault(w.Weight, 1) 1338 } 1339 return int(sum) 1340 } 1341 1342 func tcpWeightSum(forwardTo []k8s.BackendRef) int { 1343 sum := int32(0) 1344 for _, w := range forwardTo { 1345 sum += ptr.OrDefault(w.Weight, 1) 1346 } 1347 return int(sum) 1348 } 1349 1350 func buildHTTPDestination( 1351 ctx configContext, 1352 forwardTo []k8s.HTTPBackendRef, 1353 ns string, 1354 enforceRefGrant bool, 1355 ) ([]*istio.HTTPRouteDestination, *ConfigError, *ConfigError) { 1356 if forwardTo == nil { 1357 return nil, nil, nil 1358 } 1359 weights := []int{} 1360 action := []k8s.HTTPBackendRef{} 1361 for _, w := range forwardTo { 1362 wt := int(ptr.OrDefault(w.Weight, 1)) 1363 if wt == 0 { 1364 continue 1365 } 1366 action = append(action, w) 1367 weights = append(weights, wt) 1368 } 1369 if len(weights) == 1 { 1370 weights = []int{0} 1371 } 1372 1373 var invalidBackendErr *ConfigError 1374 res := []*istio.HTTPRouteDestination{} 1375 for i, fwd := range action { 1376 dst, err := buildDestination(ctx, fwd.BackendRef, ns, enforceRefGrant, gvk.HTTPRoute) 1377 if err != nil { 1378 if isInvalidBackend(err) { 1379 invalidBackendErr = err 1380 // keep going, we will gracefully drop invalid backends 1381 } else { 1382 return nil, nil, err 1383 } 1384 } 1385 rd := &istio.HTTPRouteDestination{ 1386 Destination: dst, 1387 Weight: int32(weights[i]), 1388 } 1389 for _, filter := range fwd.Filters { 1390 switch filter.Type { 1391 case k8s.HTTPRouteFilterRequestHeaderModifier: 1392 h := createHeadersFilter(filter.RequestHeaderModifier) 1393 if h == nil { 1394 continue 1395 } 1396 if rd.Headers == nil { 1397 rd.Headers = &istio.Headers{} 1398 } 1399 rd.Headers.Request = h 1400 case k8s.HTTPRouteFilterResponseHeaderModifier: 1401 h := createHeadersFilter(filter.ResponseHeaderModifier) 1402 if h == nil { 1403 continue 1404 } 1405 if rd.Headers == nil { 1406 rd.Headers = &istio.Headers{} 1407 } 1408 rd.Headers.Response = h 1409 default: 1410 return nil, nil, &ConfigError{Reason: InvalidFilter, Message: fmt.Sprintf("unsupported filter type %q", filter.Type)} 1411 } 1412 } 1413 res = append(res, rd) 1414 } 1415 return res, invalidBackendErr, nil 1416 } 1417 1418 func buildGRPCDestination( 1419 ctx configContext, 1420 forwardTo []k8s.GRPCBackendRef, 1421 ns string, 1422 enforceRefGrant bool, 1423 ) ([]*istio.HTTPRouteDestination, *ConfigError, *ConfigError) { 1424 if forwardTo == nil { 1425 return nil, nil, nil 1426 } 1427 weights := []int{} 1428 action := []k8s.GRPCBackendRef{} 1429 for _, w := range forwardTo { 1430 wt := int(ptr.OrDefault(w.Weight, 1)) 1431 if wt == 0 { 1432 continue 1433 } 1434 action = append(action, w) 1435 weights = append(weights, wt) 1436 } 1437 if len(weights) == 1 { 1438 weights = []int{0} 1439 } 1440 1441 var invalidBackendErr *ConfigError 1442 res := []*istio.HTTPRouteDestination{} 1443 for i, fwd := range action { 1444 dst, err := buildDestination(ctx, fwd.BackendRef, ns, enforceRefGrant, gvk.GRPCRoute) 1445 if err != nil { 1446 if isInvalidBackend(err) { 1447 invalidBackendErr = err 1448 // keep going, we will gracefully drop invalid backends 1449 } else { 1450 return nil, nil, err 1451 } 1452 } 1453 rd := &istio.HTTPRouteDestination{ 1454 Destination: dst, 1455 Weight: int32(weights[i]), 1456 } 1457 for _, filter := range fwd.Filters { 1458 switch filter.Type { 1459 case k8s.GRPCRouteFilterRequestHeaderModifier: 1460 h := createHeadersFilter(filter.RequestHeaderModifier) 1461 if h == nil { 1462 continue 1463 } 1464 if rd.Headers == nil { 1465 rd.Headers = &istio.Headers{} 1466 } 1467 rd.Headers.Request = h 1468 case k8s.GRPCRouteFilterResponseHeaderModifier: 1469 h := createHeadersFilter(filter.ResponseHeaderModifier) 1470 if h == nil { 1471 continue 1472 } 1473 if rd.Headers == nil { 1474 rd.Headers = &istio.Headers{} 1475 } 1476 rd.Headers.Response = h 1477 default: 1478 return nil, nil, &ConfigError{Reason: InvalidFilter, Message: fmt.Sprintf("unsupported filter type %q", filter.Type)} 1479 } 1480 } 1481 res = append(res, rd) 1482 } 1483 return res, invalidBackendErr, nil 1484 } 1485 1486 func buildDestination(ctx configContext, to k8s.BackendRef, ns string, enforceRefGrant bool, k config.GroupVersionKind) (*istio.Destination, *ConfigError) { 1487 // check if the reference is allowed 1488 if enforceRefGrant { 1489 refs := ctx.AllowedReferences 1490 if toNs := to.Namespace; toNs != nil && string(*toNs) != ns { 1491 if !refs.BackendAllowed(k, to.Name, *toNs, ns) { 1492 return &istio.Destination{}, &ConfigError{ 1493 Reason: InvalidDestinationPermit, 1494 Message: fmt.Sprintf("backendRef %v/%v not accessible to a %s in namespace %q (missing a ReferenceGrant?)", to.Name, *toNs, k.Kind, ns), 1495 } 1496 } 1497 } 1498 } 1499 1500 namespace := ptr.OrDefault((*string)(to.Namespace), ns) 1501 var invalidBackendErr *ConfigError 1502 if nilOrEqual((*string)(to.Group), "") && nilOrEqual((*string)(to.Kind), gvk.Service.Kind) { 1503 // Service 1504 if to.Port == nil { 1505 // "Port is required when the referent is a Kubernetes Service." 1506 return nil, &ConfigError{Reason: InvalidDestination, Message: "port is required in backendRef"} 1507 } 1508 if strings.Contains(string(to.Name), ".") { 1509 return nil, &ConfigError{Reason: InvalidDestination, Message: "serviceName invalid; the name of the Service must be used, not the hostname."} 1510 } 1511 hostname := fmt.Sprintf("%s.%s.svc.%s", to.Name, namespace, ctx.Domain) 1512 if ctx.Context.GetService(hostname, namespace) == nil { 1513 invalidBackendErr = &ConfigError{Reason: InvalidDestinationNotFound, Message: fmt.Sprintf("backend(%s) not found", hostname)} 1514 } 1515 return &istio.Destination{ 1516 // TODO: implement ReferencePolicy for cross namespace 1517 Host: hostname, 1518 Port: &istio.PortSelector{Number: uint32(*to.Port)}, 1519 }, invalidBackendErr 1520 } 1521 if nilOrEqual((*string)(to.Group), features.MCSAPIGroup) && nilOrEqual((*string)(to.Kind), "ServiceImport") { 1522 // Service import 1523 hostname := fmt.Sprintf("%s.%s.svc.clusterset.local", to.Name, namespace) 1524 if !features.EnableMCSHost { 1525 // They asked for ServiceImport, but actually don't have full support enabled... 1526 // No problem, we can just treat it as Service, which is already cross-cluster in this mode anyways 1527 hostname = fmt.Sprintf("%s.%s.svc.%s", to.Name, namespace, ctx.Domain) 1528 } 1529 if to.Port == nil { 1530 // We don't know where to send without port 1531 return nil, &ConfigError{Reason: InvalidDestination, Message: "port is required in backendRef"} 1532 } 1533 if strings.Contains(string(to.Name), ".") { 1534 return nil, &ConfigError{Reason: InvalidDestination, Message: "serviceName invalid; the name of the Service must be used, not the hostname."} 1535 } 1536 if ctx.Context.GetService(hostname, namespace) == nil { 1537 invalidBackendErr = &ConfigError{Reason: InvalidDestinationNotFound, Message: fmt.Sprintf("backend(%s) not found", hostname)} 1538 } 1539 return &istio.Destination{ 1540 Host: hostname, 1541 Port: &istio.PortSelector{Number: uint32(*to.Port)}, 1542 }, invalidBackendErr 1543 } 1544 if nilOrEqual((*string)(to.Group), gvk.ServiceEntry.Group) && nilOrEqual((*string)(to.Kind), "Hostname") { 1545 // Hostname synthetic type 1546 if to.Port == nil { 1547 // We don't know where to send without port 1548 return nil, &ConfigError{Reason: InvalidDestination, Message: "port is required in backendRef"} 1549 } 1550 if to.Namespace != nil { 1551 return nil, &ConfigError{Reason: InvalidDestination, Message: "namespace may not be set with Hostname type"} 1552 } 1553 hostname := string(to.Name) 1554 if ctx.Context.GetService(hostname, namespace) == nil { 1555 invalidBackendErr = &ConfigError{Reason: InvalidDestinationNotFound, Message: fmt.Sprintf("backend(%s) not found", hostname)} 1556 } 1557 return &istio.Destination{ 1558 Host: string(to.Name), 1559 Port: &istio.PortSelector{Number: uint32(*to.Port)}, 1560 }, invalidBackendErr 1561 } 1562 return &istio.Destination{}, &ConfigError{ 1563 Reason: InvalidDestinationKind, 1564 Message: fmt.Sprintf("referencing unsupported backendRef: group %q kind %q", ptr.OrEmpty(to.Group), ptr.OrEmpty(to.Kind)), 1565 } 1566 } 1567 1568 // https://github.com/kubernetes-sigs/gateway-api/blob/cea484e38e078a2c1997d8c7a62f410a1540f519/apis/v1beta1/httproute_types.go#L207-L212 1569 func isInvalidBackend(err *ConfigError) bool { 1570 return err.Reason == InvalidDestinationPermit || 1571 err.Reason == InvalidDestinationNotFound || 1572 err.Reason == InvalidDestinationKind 1573 } 1574 1575 func headerListToMap(hl []k8s.HTTPHeader) map[string]string { 1576 if len(hl) == 0 { 1577 return nil 1578 } 1579 res := map[string]string{} 1580 for _, e := range hl { 1581 k := strings.ToLower(string(e.Name)) 1582 if _, f := res[k]; f { 1583 // "Subsequent entries with an equivalent header name MUST be ignored" 1584 continue 1585 } 1586 res[k] = e.Value 1587 } 1588 return res 1589 } 1590 1591 func createMirrorFilter(ctx configContext, filter *k8s.HTTPRequestMirrorFilter, ns string, 1592 enforceRefGrant bool, k config.GroupVersionKind, 1593 ) (*istio.HTTPMirrorPolicy, *ConfigError) { 1594 if filter == nil { 1595 return nil, nil 1596 } 1597 var weightOne int32 = 1 1598 dst, err := buildDestination(ctx, k8s.BackendRef{ 1599 BackendObjectReference: filter.BackendRef, 1600 Weight: &weightOne, 1601 }, ns, enforceRefGrant, k) 1602 if err != nil { 1603 return nil, err 1604 } 1605 return &istio.HTTPMirrorPolicy{Destination: dst}, nil 1606 } 1607 1608 func createRewriteFilter(filter *k8s.HTTPURLRewriteFilter) *istio.HTTPRewrite { 1609 if filter == nil { 1610 return nil 1611 } 1612 rewrite := &istio.HTTPRewrite{} 1613 if filter.Path != nil { 1614 switch filter.Path.Type { 1615 case k8s.PrefixMatchHTTPPathModifier: 1616 rewrite.Uri = strings.TrimSuffix(*filter.Path.ReplacePrefixMatch, "/") 1617 if rewrite.Uri == "" { 1618 // `/` means removing the prefix 1619 rewrite.Uri = "/" 1620 } 1621 case k8s.FullPathHTTPPathModifier: 1622 rewrite.UriRegexRewrite = &istio.RegexRewrite{ 1623 Match: "/.*", 1624 Rewrite: *filter.Path.ReplaceFullPath, 1625 } 1626 } 1627 } 1628 if filter.Hostname != nil { 1629 rewrite.Authority = string(*filter.Hostname) 1630 } 1631 // Nothing done 1632 if rewrite.Uri == "" && rewrite.UriRegexRewrite == nil && rewrite.Authority == "" { 1633 return nil 1634 } 1635 return rewrite 1636 } 1637 1638 func createRedirectFilter(filter *k8s.HTTPRequestRedirectFilter) *istio.HTTPRedirect { 1639 if filter == nil { 1640 return nil 1641 } 1642 resp := &istio.HTTPRedirect{} 1643 if filter.StatusCode != nil { 1644 // Istio allows 301, 302, 303, 307, 308. 1645 // Gateway allows only 301 and 302. 1646 resp.RedirectCode = uint32(*filter.StatusCode) 1647 } 1648 if filter.Hostname != nil { 1649 resp.Authority = string(*filter.Hostname) 1650 } 1651 if filter.Scheme != nil { 1652 // Both allow http and https 1653 resp.Scheme = *filter.Scheme 1654 } 1655 if filter.Port != nil { 1656 resp.RedirectPort = &istio.HTTPRedirect_Port{Port: uint32(*filter.Port)} 1657 } else { 1658 // "When empty, port (if specified) of the request is used." 1659 // this differs from Istio default 1660 if filter.Scheme != nil { 1661 resp.RedirectPort = &istio.HTTPRedirect_DerivePort{DerivePort: istio.HTTPRedirect_FROM_PROTOCOL_DEFAULT} 1662 } else { 1663 resp.RedirectPort = &istio.HTTPRedirect_DerivePort{DerivePort: istio.HTTPRedirect_FROM_REQUEST_PORT} 1664 } 1665 } 1666 if filter.Path != nil { 1667 switch filter.Path.Type { 1668 case k8s.FullPathHTTPPathModifier: 1669 resp.Uri = *filter.Path.ReplaceFullPath 1670 case k8s.PrefixMatchHTTPPathModifier: 1671 resp.Uri = fmt.Sprintf("%%PREFIX()%%%s", *filter.Path.ReplacePrefixMatch) 1672 } 1673 } 1674 return resp 1675 } 1676 1677 func createHeadersFilter(filter *k8s.HTTPHeaderFilter) *istio.Headers_HeaderOperations { 1678 if filter == nil { 1679 return nil 1680 } 1681 return &istio.Headers_HeaderOperations{ 1682 Add: headerListToMap(filter.Add), 1683 Remove: filter.Remove, 1684 Set: headerListToMap(filter.Set), 1685 } 1686 } 1687 1688 // nolint: unparam 1689 func createMethodMatch(match k8s.HTTPRouteMatch) (*istio.StringMatch, *ConfigError) { 1690 if match.Method == nil { 1691 return nil, nil 1692 } 1693 return &istio.StringMatch{ 1694 MatchType: &istio.StringMatch_Exact{Exact: string(*match.Method)}, 1695 }, nil 1696 } 1697 1698 func createQueryParamsMatch(match k8s.HTTPRouteMatch) (map[string]*istio.StringMatch, *ConfigError) { 1699 res := map[string]*istio.StringMatch{} 1700 for _, qp := range match.QueryParams { 1701 tp := k8s.QueryParamMatchExact 1702 if qp.Type != nil { 1703 tp = *qp.Type 1704 } 1705 switch tp { 1706 case k8s.QueryParamMatchExact: 1707 res[string(qp.Name)] = &istio.StringMatch{ 1708 MatchType: &istio.StringMatch_Exact{Exact: qp.Value}, 1709 } 1710 case k8s.QueryParamMatchRegularExpression: 1711 res[string(qp.Name)] = &istio.StringMatch{ 1712 MatchType: &istio.StringMatch_Regex{Regex: qp.Value}, 1713 } 1714 default: 1715 // Should never happen, unless a new field is added 1716 return nil, &ConfigError{Reason: InvalidConfiguration, Message: fmt.Sprintf("unknown type: %q is not supported QueryParams type", tp)} 1717 } 1718 } 1719 1720 if len(res) == 0 { 1721 return nil, nil 1722 } 1723 return res, nil 1724 } 1725 1726 func createHeadersMatch(match k8s.HTTPRouteMatch) (map[string]*istio.StringMatch, *ConfigError) { 1727 res := map[string]*istio.StringMatch{} 1728 for _, header := range match.Headers { 1729 tp := k8s.HeaderMatchExact 1730 if header.Type != nil { 1731 tp = *header.Type 1732 } 1733 switch tp { 1734 case k8s.HeaderMatchExact: 1735 res[string(header.Name)] = &istio.StringMatch{ 1736 MatchType: &istio.StringMatch_Exact{Exact: header.Value}, 1737 } 1738 case k8s.HeaderMatchRegularExpression: 1739 res[string(header.Name)] = &istio.StringMatch{ 1740 MatchType: &istio.StringMatch_Regex{Regex: header.Value}, 1741 } 1742 default: 1743 // Should never happen, unless a new field is added 1744 return nil, &ConfigError{Reason: InvalidConfiguration, Message: fmt.Sprintf("unknown type: %q is not supported HeaderMatch type", tp)} 1745 } 1746 } 1747 1748 if len(res) == 0 { 1749 return nil, nil 1750 } 1751 return res, nil 1752 } 1753 1754 func createGRPCHeadersMatch(match k8s.GRPCRouteMatch) (map[string]*istio.StringMatch, *ConfigError) { 1755 res := map[string]*istio.StringMatch{} 1756 for _, header := range match.Headers { 1757 tp := k8s.HeaderMatchExact 1758 if header.Type != nil { 1759 tp = *header.Type 1760 } 1761 switch tp { 1762 case k8s.HeaderMatchExact: 1763 res[string(header.Name)] = &istio.StringMatch{ 1764 MatchType: &istio.StringMatch_Exact{Exact: header.Value}, 1765 } 1766 case k8s.HeaderMatchRegularExpression: 1767 res[string(header.Name)] = &istio.StringMatch{ 1768 MatchType: &istio.StringMatch_Regex{Regex: header.Value}, 1769 } 1770 default: 1771 // Should never happen, unless a new field is added 1772 return nil, &ConfigError{Reason: InvalidConfiguration, Message: fmt.Sprintf("unknown type: %q is not supported HeaderMatch type", tp)} 1773 } 1774 } 1775 1776 if len(res) == 0 { 1777 return nil, nil 1778 } 1779 return res, nil 1780 } 1781 1782 func createURIMatch(match k8s.HTTPRouteMatch) (*istio.StringMatch, *ConfigError) { 1783 tp := k8s.PathMatchPathPrefix 1784 if match.Path.Type != nil { 1785 tp = *match.Path.Type 1786 } 1787 dest := "/" 1788 if match.Path.Value != nil { 1789 dest = *match.Path.Value 1790 } 1791 switch tp { 1792 case k8s.PathMatchPathPrefix: 1793 // "When specified, a trailing `/` is ignored." 1794 if dest != "/" { 1795 dest = strings.TrimSuffix(dest, "/") 1796 } 1797 return &istio.StringMatch{ 1798 MatchType: &istio.StringMatch_Prefix{Prefix: dest}, 1799 }, nil 1800 case k8s.PathMatchExact: 1801 return &istio.StringMatch{ 1802 MatchType: &istio.StringMatch_Exact{Exact: dest}, 1803 }, nil 1804 case k8s.PathMatchRegularExpression: 1805 return &istio.StringMatch{ 1806 MatchType: &istio.StringMatch_Regex{Regex: dest}, 1807 }, nil 1808 default: 1809 // Should never happen, unless a new field is added 1810 return nil, &ConfigError{Reason: InvalidConfiguration, Message: fmt.Sprintf("unknown type: %q is not supported Path match type", tp)} 1811 } 1812 } 1813 1814 func createGRPCURIMatch(match k8s.GRPCRouteMatch) (*istio.StringMatch, *ConfigError) { 1815 m := match.Method 1816 if m == nil { 1817 return nil, nil 1818 } 1819 tp := k8s.GRPCMethodMatchExact 1820 if m.Type != nil { 1821 tp = *m.Type 1822 } 1823 if m.Method == nil && m.Service == nil { 1824 // Should never happen, invalid per spec 1825 return nil, &ConfigError{Reason: InvalidConfiguration, Message: "gRPC match must have method or service defined"} 1826 } 1827 // gRPC format is /<Service>/<Method>. Since we don't natively understand this, convert to various string matches 1828 switch tp { 1829 case k8s.GRPCMethodMatchExact: 1830 if m.Method == nil { 1831 return &istio.StringMatch{ 1832 MatchType: &istio.StringMatch_Prefix{Prefix: fmt.Sprintf("/%s/", *m.Service)}, 1833 }, nil 1834 } 1835 if m.Service == nil { 1836 return &istio.StringMatch{ 1837 MatchType: &istio.StringMatch_Regex{Regex: fmt.Sprintf("/[^/]+/%s", *m.Method)}, 1838 }, nil 1839 } 1840 return &istio.StringMatch{ 1841 MatchType: &istio.StringMatch_Exact{Exact: fmt.Sprintf("/%s/%s", *m.Service, *m.Method)}, 1842 }, nil 1843 case k8s.GRPCMethodMatchRegularExpression: 1844 if m.Method == nil { 1845 return &istio.StringMatch{ 1846 MatchType: &istio.StringMatch_Regex{Regex: fmt.Sprintf("/%s/.+", *m.Service)}, 1847 }, nil 1848 } 1849 if m.Service == nil { 1850 return &istio.StringMatch{ 1851 MatchType: &istio.StringMatch_Regex{Regex: fmt.Sprintf("/[^/]+/%s", *m.Method)}, 1852 }, nil 1853 } 1854 return &istio.StringMatch{ 1855 MatchType: &istio.StringMatch_Regex{Regex: fmt.Sprintf("/%s/%s", *m.Service, *m.Method)}, 1856 }, nil 1857 default: 1858 // Should never happen, unless a new field is added 1859 return nil, &ConfigError{Reason: InvalidConfiguration, Message: fmt.Sprintf("unknown type: %q is not supported Path match type", tp)} 1860 } 1861 } 1862 1863 // getGatewayClass finds all gateway class that are owned by Istio 1864 // Response is ClassName -> Controller type 1865 func getGatewayClasses(r GatewayResources, supportedFeatures []k8s.SupportedFeature) map[string]k8s.GatewayController { 1866 res := map[string]k8s.GatewayController{} 1867 // Setup builtin ones - these can be overridden possibly 1868 for name, controller := range builtinClasses { 1869 res[string(name)] = controller 1870 } 1871 for _, obj := range r.GatewayClass { 1872 gwc := obj.Spec.(*k8s.GatewayClassSpec) 1873 _, known := classInfos[gwc.ControllerName] 1874 if !known { 1875 continue 1876 } 1877 res[obj.Name] = gwc.ControllerName 1878 1879 // Set status. If we created it, it may already be there. If not, set it again 1880 obj.Status.(*kstatus.WrappedStatus).Mutate(func(s config.Status) config.Status { 1881 gcs := s.(*k8s.GatewayClassStatus) 1882 *gcs = GetClassStatus(gcs, obj.Generation) 1883 gcs.SupportedFeatures = supportedFeatures 1884 return gcs 1885 }) 1886 } 1887 1888 return res 1889 } 1890 1891 // parentKey holds info about a parentRef (eg route binding to a Gateway). This is a mirror of 1892 // k8s.ParentReference in a form that can be stored in a map 1893 type parentKey struct { 1894 Kind config.GroupVersionKind 1895 // Name is the original name of the resource (eg Kubernetes Gateway name) 1896 Name string 1897 // Namespace is the namespace of the resource 1898 Namespace string 1899 } 1900 1901 type parentReference struct { 1902 parentKey 1903 1904 SectionName k8s.SectionName 1905 Port k8s.PortNumber 1906 } 1907 1908 var meshGVK = config.GroupVersionKind{ 1909 Group: gvk.KubernetesGateway.Group, 1910 Version: gvk.KubernetesGateway.Version, 1911 Kind: "Mesh", 1912 } 1913 1914 var meshParentKey = parentKey{ 1915 Kind: meshGVK, 1916 Name: "istio", 1917 } 1918 1919 type configContext struct { 1920 GatewayResources 1921 AllowedReferences AllowedReferences 1922 GatewayReferences map[parentKey][]*parentInfo 1923 1924 // key: referenced resources(e.g. secrets), value: gateway-api resources(e.g. gateways) 1925 resourceReferences map[model.ConfigKey][]model.ConfigKey 1926 } 1927 1928 // parentInfo holds info about a "parent" - something that can be referenced as a ParentRef in the API. 1929 // Today, this is just Gateway and Mesh. 1930 type parentInfo struct { 1931 // InternalName refers to the internal name we can reference it by. For example, "mesh" or "my-ns/my-gateway" 1932 InternalName string 1933 // AllowedKinds indicates which kinds can be admitted by this parent 1934 AllowedKinds []k8s.RouteGroupKind 1935 // Hostnames is the hostnames that must be match to reference to the parent. For gateway this is listener hostname 1936 // Format is ns/hostname 1937 Hostnames []string 1938 // OriginalHostname is the unprocessed form of Hostnames; how it appeared in users' config 1939 OriginalHostname string 1940 1941 // AttachedRoutes keeps track of how many routes are attached to this parent. This is tracked for status. 1942 // Because this is mutate in the route generation, parentInfo must be passed as a pointer 1943 AttachedRoutes int32 1944 // ReportAttachedRoutes is a callback that should be triggered once all AttachedRoutes are computed, to 1945 // actually store the attached route count in the status 1946 ReportAttachedRoutes func() 1947 SectionName k8s.SectionName 1948 Port k8s.PortNumber 1949 Protocol k8s.ProtocolType 1950 } 1951 1952 // routeParentReference holds information about a route's parent reference 1953 type routeParentReference struct { 1954 // InternalName refers to the internal name of the parent we can reference it by. For example, "mesh" or "my-ns/my-gateway" 1955 InternalName string 1956 // InternalKind is the Group/Kind of the parent 1957 InternalKind config.GroupVersionKind 1958 // DeniedReason, if present, indicates why the reference was not valid 1959 DeniedReason *ParentError 1960 // OriginalReference contains the original reference 1961 OriginalReference k8s.ParentReference 1962 // Hostname is the hostname match of the parent, if any 1963 Hostname string 1964 BannedHostnames sets.Set[string] 1965 } 1966 1967 func (r routeParentReference) IsMesh() bool { 1968 return r.InternalName == "mesh" 1969 } 1970 1971 func (r routeParentReference) hostnameAllowedByIsolation(rawRouteHost string) bool { 1972 routeHost := host.Name(rawRouteHost) 1973 ourListener := host.Name(r.Hostname) 1974 if len(ourListener) > 0 && !ourListener.IsWildCarded() { 1975 // Short circuit: this logic only applies to wildcards 1976 // Not required for correctness, just an optimization 1977 return true 1978 } 1979 if len(ourListener) > 0 && !routeHost.Matches(ourListener) { 1980 return false 1981 } 1982 for checkListener := range r.BannedHostnames { 1983 // We have 3 hostnames here: 1984 // * routeHost, the hostname in the route entry 1985 // * ourListener, the hostname of the listener the route is bound to 1986 // * checkListener, the hostname of the other listener we are comparing to 1987 // We want to return false if checkListener would match the routeHost and it would be a more exact match 1988 if len(ourListener) > len(checkListener) { 1989 // If our hostname is longer, it must be more exact than the check 1990 continue 1991 } 1992 // Ours is shorter. If it matches the checkListener, then it should ONLY match that one 1993 // Note protocol, port, etc are already considered when we construct bannedHostnames 1994 if routeHost.SubsetOf(host.Name(checkListener)) { 1995 return false 1996 } 1997 } 1998 return true 1999 } 2000 2001 func filteredReferences(parents []routeParentReference) []routeParentReference { 2002 ret := make([]routeParentReference, 0, len(parents)) 2003 for _, p := range parents { 2004 if p.DeniedReason != nil { 2005 // We should filter this out 2006 continue 2007 } 2008 ret = append(ret, p) 2009 } 2010 // To ensure deterministic order, sort them 2011 sort.Slice(ret, func(i, j int) bool { 2012 return ret[i].InternalName < ret[j].InternalName 2013 }) 2014 return ret 2015 } 2016 2017 func getDefaultName(name string, kgw *k8s.GatewaySpec, disableNameSuffix bool) string { 2018 if disableNameSuffix { 2019 return name 2020 } 2021 return fmt.Sprintf("%v-%v", name, kgw.GatewayClassName) 2022 } 2023 2024 func convertGateways(r configContext) ([]config.Config, map[parentKey][]*parentInfo, sets.String) { 2025 // result stores our generated Istio Gateways 2026 result := []config.Config{} 2027 // gwMap stores an index to access parentInfo (which corresponds to a Kubernetes Gateway) 2028 gwMap := map[parentKey][]*parentInfo{} 2029 // namespaceLabelReferences keeps track of all namespace label keys referenced by Gateways. This is 2030 // used to ensure we handle namespace updates for those keys. 2031 namespaceLabelReferences := sets.New[string]() 2032 classes := getGatewayClasses(r.GatewayResources, gatewaySupportedFeatures) 2033 for _, obj := range r.Gateway { 2034 obj := obj 2035 kgw := obj.Spec.(*k8s.GatewaySpec) 2036 controllerName, f := classes[string(kgw.GatewayClassName)] 2037 if !f { 2038 // No gateway class found, this may be meant for another controller; should be skipped. 2039 continue 2040 } 2041 classInfo, f := classInfos[controllerName] 2042 if !f { 2043 continue 2044 } 2045 if classInfo.disableRouteGeneration { 2046 // We found it, but don't want to handle this class 2047 continue 2048 } 2049 2050 servers := []*istio.Server{} 2051 2052 // Extract the addresses. A gateway will bind to a specific Service 2053 gatewayServices, err := extractGatewayServices(r.GatewayResources, kgw, obj, classInfo) 2054 if len(gatewayServices) == 0 && err != nil { 2055 // Short circuit if its a hard failure 2056 reportGatewayStatus(r, obj, classInfo, gatewayServices, servers, err) 2057 continue 2058 } 2059 for i, l := range kgw.Listeners { 2060 i := i 2061 namespaceLabelReferences.InsertAll(getNamespaceLabelReferences(l.AllowedRoutes)...) 2062 server, programmed := buildListener(r, obj, l, i, controllerName) 2063 2064 servers = append(servers, server) 2065 if controllerName == constants.ManagedGatewayMeshController { 2066 // Waypoint doesn't actually convert the routes to VirtualServices 2067 continue 2068 } 2069 meta := parentMeta(obj, &l.Name) 2070 meta[constants.InternalGatewaySemantics] = constants.GatewaySemanticsGateway 2071 meta[model.InternalGatewayServiceAnnotation] = strings.Join(gatewayServices, ",") 2072 2073 // Each listener generates an Istio Gateway with a single Server. This allows binding to a specific listener. 2074 gatewayConfig := config.Config{ 2075 Meta: config.Meta{ 2076 CreationTimestamp: obj.CreationTimestamp, 2077 GroupVersionKind: gvk.Gateway, 2078 Name: kubeconfig.InternalGatewayName(obj.Name, string(l.Name)), 2079 Annotations: meta, 2080 Namespace: obj.Namespace, 2081 Domain: r.Domain, 2082 }, 2083 Spec: &istio.Gateway{ 2084 Servers: []*istio.Server{server}, 2085 }, 2086 } 2087 ref := parentKey{ 2088 Kind: gvk.KubernetesGateway, 2089 Name: obj.Name, 2090 Namespace: obj.Namespace, 2091 } 2092 if _, f := gwMap[ref]; !f { 2093 gwMap[ref] = []*parentInfo{} 2094 } 2095 2096 allowed, _ := generateSupportedKinds(l) 2097 pri := &parentInfo{ 2098 InternalName: obj.Namespace + "/" + gatewayConfig.Name, 2099 AllowedKinds: allowed, 2100 Hostnames: server.Hosts, 2101 OriginalHostname: string(ptr.OrEmpty(l.Hostname)), 2102 SectionName: l.Name, 2103 Port: l.Port, 2104 Protocol: l.Protocol, 2105 } 2106 pri.ReportAttachedRoutes = func() { 2107 reportListenerAttachedRoutes(i, obj, pri.AttachedRoutes) 2108 } 2109 gwMap[ref] = append(gwMap[ref], pri) 2110 2111 if programmed { 2112 result = append(result, gatewayConfig) 2113 } 2114 } 2115 2116 // If "gateway.istio.io/alias-for" annotation is present, any Route 2117 // that binds to the gateway will bind to its alias instead. 2118 // The typical usage is when the original gateway is not managed by the gateway controller 2119 // but the ( generated ) alias is. This allows people to build their own 2120 // gateway controllers on top of Istio Gateway Controller. 2121 if obj.Annotations != nil && obj.Annotations[gatewayAliasForAnnotationKey] != "" { 2122 ref := parentKey{ 2123 Kind: gvk.KubernetesGateway, 2124 Name: obj.Annotations[gatewayAliasForAnnotationKey], 2125 Namespace: obj.Namespace, 2126 } 2127 alias := parentKey{ 2128 Kind: gvk.KubernetesGateway, 2129 Name: obj.Name, 2130 Namespace: obj.Namespace, 2131 } 2132 gwMap[ref] = gwMap[alias] 2133 } 2134 2135 reportGatewayStatus(r, obj, classInfo, gatewayServices, servers, err) 2136 } 2137 // Insert a parent for Mesh references. 2138 gwMap[meshParentKey] = []*parentInfo{ 2139 { 2140 InternalName: "mesh", 2141 // Mesh has no configurable AllowedKinds, so allow all supported 2142 AllowedKinds: []k8s.RouteGroupKind{ 2143 {Group: (*k8s.Group)(ptr.Of(gvk.HTTPRoute.Group)), Kind: k8s.Kind(gvk.HTTPRoute.Kind)}, 2144 {Group: (*k8s.Group)(ptr.Of(gvk.GRPCRoute.Group)), Kind: k8s.Kind(gvk.GRPCRoute.Kind)}, 2145 {Group: (*k8s.Group)(ptr.Of(gvk.TCPRoute.Group)), Kind: k8s.Kind(gvk.TCPRoute.Kind)}, 2146 {Group: (*k8s.Group)(ptr.Of(gvk.TLSRoute.Group)), Kind: k8s.Kind(gvk.TLSRoute.Kind)}, 2147 }, 2148 }, 2149 } 2150 return result, gwMap, namespaceLabelReferences 2151 } 2152 2153 // Gateway currently requires a listener (https://github.com/kubernetes-sigs/gateway-api/pull/1596). 2154 // We don't *really* care about the listener, but it may make sense to add a warning if users do not 2155 // configure it in an expected way so that we have consistency and can make changes in the future as needed. 2156 // We could completely reject but that seems more likely to cause pain. 2157 func unexpectedWaypointListener(l k8s.Listener) bool { 2158 if l.Port != 15008 { 2159 return true 2160 } 2161 if l.Protocol != k8s.ProtocolType(protocol.HBONE) { 2162 return true 2163 } 2164 return false 2165 } 2166 2167 func getListenerNames(obj config.Config) sets.Set[k8s.SectionName] { 2168 res := sets.New[k8s.SectionName]() 2169 for _, l := range obj.Spec.(*k8s.GatewaySpec).Listeners { 2170 res.Insert(l.Name) 2171 } 2172 return res 2173 } 2174 2175 func reportGatewayStatus( 2176 r configContext, 2177 obj config.Config, 2178 classInfo classInfo, 2179 gatewayServices []string, 2180 servers []*istio.Server, 2181 gatewayErr *ConfigError, 2182 ) { 2183 // TODO: we lose address if servers is empty due to an error 2184 internal, internalIP, external, pending, warnings, allUsable := r.Context.ResolveGatewayInstances(obj.Namespace, gatewayServices, servers) 2185 2186 // Setup initial conditions to the success state. If we encounter errors, we will update this. 2187 // We have two status 2188 // Accepted: is the configuration valid. We only have errors in listeners, and the status is not supposed to 2189 // be tied to listeners, so this is always accepted 2190 // Programmed: is the data plane "ready" (note: eventually consistent) 2191 gatewayConditions := map[string]*condition{ 2192 string(k8s.GatewayConditionAccepted): { 2193 reason: string(k8s.GatewayReasonAccepted), 2194 message: "Resource accepted", 2195 }, 2196 string(k8s.GatewayConditionProgrammed): { 2197 reason: string(k8s.GatewayReasonProgrammed), 2198 message: "Resource programmed", 2199 }, 2200 } 2201 2202 if gatewayErr != nil { 2203 gatewayConditions[string(k8s.GatewayConditionAccepted)].error = gatewayErr 2204 } 2205 2206 if len(internal) > 0 { 2207 msg := fmt.Sprintf("Resource programmed, assigned to service(s) %s", humanReadableJoin(internal)) 2208 gatewayConditions[string(k8s.GatewayReasonProgrammed)].message = msg 2209 } 2210 2211 if len(gatewayServices) == 0 { 2212 gatewayConditions[string(k8s.GatewayReasonProgrammed)].error = &ConfigError{ 2213 Reason: InvalidAddress, 2214 Message: "Failed to assign to any requested addresses", 2215 } 2216 } else if len(warnings) > 0 { 2217 var msg string 2218 var reason string 2219 if len(internal) != 0 { 2220 msg = fmt.Sprintf("Assigned to service(s) %s, but failed to assign to all requested addresses: %s", 2221 humanReadableJoin(internal), strings.Join(warnings, "; ")) 2222 } else { 2223 msg = fmt.Sprintf("Failed to assign to any requested addresses: %s", strings.Join(warnings, "; ")) 2224 } 2225 if allUsable { 2226 reason = string(k8s.GatewayReasonAddressNotAssigned) 2227 } else { 2228 reason = string(k8s.GatewayReasonAddressNotUsable) 2229 } 2230 gatewayConditions[string(k8s.GatewayConditionProgrammed)].error = &ConfigError{ 2231 // TODO: this only checks Service ready, we should also check Deployment ready? 2232 Reason: reason, 2233 Message: msg, 2234 } 2235 } 2236 obj.Status.(*kstatus.WrappedStatus).Mutate(func(s config.Status) config.Status { 2237 gs := s.(*k8s.GatewayStatus) 2238 addressesToReport := external 2239 if len(addressesToReport) == 0 { 2240 wantAddressType := classInfo.addressType 2241 if override, ok := obj.Annotations[addressTypeOverride]; ok { 2242 wantAddressType = k8s.AddressType(override) 2243 } 2244 // There are no external addresses, so report the internal ones 2245 // TODO: should we always report both? 2246 if wantAddressType == k8s.IPAddressType { 2247 addressesToReport = internalIP 2248 } else { 2249 for _, hostport := range internal { 2250 svchost, _, _ := net.SplitHostPort(hostport) 2251 if !slices.Contains(pending, svchost) && !slices.Contains(addressesToReport, svchost) { 2252 addressesToReport = append(addressesToReport, svchost) 2253 } 2254 } 2255 } 2256 } 2257 // Do not report an address until we are ready. But once we are ready, never remove the address. 2258 if len(addressesToReport) > 0 { 2259 gs.Addresses = make([]k8s.GatewayStatusAddress, 0, len(addressesToReport)) 2260 for _, addr := range addressesToReport { 2261 var addrType k8s.AddressType 2262 if _, err := netip.ParseAddr(addr); err == nil { 2263 addrType = k8s.IPAddressType 2264 } else { 2265 addrType = k8s.HostnameAddressType 2266 } 2267 gs.Addresses = append(gs.Addresses, k8s.GatewayStatusAddress{ 2268 Value: addr, 2269 Type: &addrType, 2270 }) 2271 } 2272 } 2273 // Prune listeners that have been removed 2274 haveListeners := getListenerNames(obj) 2275 listeners := make([]k8s.ListenerStatus, 0, len(gs.Listeners)) 2276 for _, l := range gs.Listeners { 2277 if haveListeners.Contains(l.Name) { 2278 haveListeners.Delete(l.Name) 2279 listeners = append(listeners, l) 2280 } 2281 } 2282 gs.Listeners = listeners 2283 gs.Conditions = setConditions(obj.Generation, gs.Conditions, gatewayConditions) 2284 return gs 2285 }) 2286 } 2287 2288 // IsManaged checks if a Gateway is managed (ie we create the Deployment and Service) or unmanaged. 2289 // This is based on the address field of the spec. If address is set with a Hostname type, it should point to an existing 2290 // Service that handles the gateway traffic. If it is not set, or refers to only a single IP, we will consider it managed and provision the Service. 2291 // If there is an IP, we will set the `loadBalancerIP` type. 2292 // While there is no defined standard for this in the API yet, it is tracked in https://github.com/kubernetes-sigs/gateway-api/issues/892. 2293 // So far, this mirrors how out of clusters work (address set means to use existing IP, unset means to provision one), 2294 // and there has been growing consensus on this model for in cluster deployments. 2295 // 2296 // Currently, the supported options are: 2297 // * 1 Hostname value. This can be short Service name ingress, or FQDN ingress.ns.svc.cluster.local, example.com. If its a non-k8s FQDN it is a ServiceEntry. 2298 // * 1 IP address. This is managed, with IP explicit 2299 // * Nothing. This is managed, with IP auto assigned 2300 // 2301 // Not supported: 2302 // Multiple hostname/IP - It is feasible but preference is to create multiple Gateways. This would also break the 1:1 mapping of GW:Service 2303 // Mixed hostname and IP - doesn't make sense; user should define the IP in service 2304 // NamedAddress - Service has no concept of named address. For cloud's that have named addresses they can be configured by annotations, 2305 // 2306 // which users can add to the Gateway. 2307 func IsManaged(gw *k8s.GatewaySpec) bool { 2308 if len(gw.Addresses) == 0 { 2309 return true 2310 } 2311 if len(gw.Addresses) > 1 { 2312 return false 2313 } 2314 if t := gw.Addresses[0].Type; t == nil || *t == k8s.IPAddressType { 2315 return true 2316 } 2317 return false 2318 } 2319 2320 func extractGatewayServices(r GatewayResources, kgw *k8s.GatewaySpec, obj config.Config, info classInfo) ([]string, *ConfigError) { 2321 if IsManaged(kgw) { 2322 name := model.GetOrDefault(obj.Annotations[gatewayNameOverride], getDefaultName(obj.Name, kgw, info.disableNameSuffix)) 2323 return []string{fmt.Sprintf("%s.%s.svc.%v", name, obj.Namespace, r.Domain)}, nil 2324 } 2325 gatewayServices := []string{} 2326 skippedAddresses := []string{} 2327 for _, addr := range kgw.Addresses { 2328 if addr.Type != nil && *addr.Type != k8s.HostnameAddressType { 2329 // We only support HostnameAddressType. Keep track of invalid ones so we can report in status. 2330 skippedAddresses = append(skippedAddresses, addr.Value) 2331 continue 2332 } 2333 // TODO: For now we are using Addresses. There has been some discussion of allowing inline 2334 // parameters on the class field like a URL, in which case we will probably just use that. See 2335 // https://github.com/kubernetes-sigs/gateway-api/pull/614 2336 fqdn := addr.Value 2337 if !strings.Contains(fqdn, ".") { 2338 // Short name, expand it 2339 fqdn = fmt.Sprintf("%s.%s.svc.%s", fqdn, obj.Namespace, r.Domain) 2340 } 2341 gatewayServices = append(gatewayServices, fqdn) 2342 } 2343 if len(skippedAddresses) > 0 { 2344 // Give error but return services, this is a soft failure 2345 return gatewayServices, &ConfigError{ 2346 Reason: InvalidAddress, 2347 Message: fmt.Sprintf("only Hostname is supported, ignoring %v", skippedAddresses), 2348 } 2349 } 2350 if _, f := obj.Annotations[serviceTypeOverride]; f { 2351 // Give error but return services, this is a soft failure 2352 // Remove entirely in 1.20 2353 return gatewayServices, &ConfigError{ 2354 Reason: DeprecateFieldUsage, 2355 Message: fmt.Sprintf("annotation %v is deprecated, use Spec.Infrastructure.Routeability", serviceTypeOverride), 2356 } 2357 } 2358 return gatewayServices, nil 2359 } 2360 2361 // getNamespaceLabelReferences fetches all label keys used in namespace selectors. Return order may not be stable. 2362 func getNamespaceLabelReferences(routes *k8s.AllowedRoutes) []string { 2363 if routes == nil || routes.Namespaces == nil || routes.Namespaces.Selector == nil { 2364 return nil 2365 } 2366 res := []string{} 2367 for k := range routes.Namespaces.Selector.MatchLabels { 2368 res = append(res, k) 2369 } 2370 for _, me := range routes.Namespaces.Selector.MatchExpressions { 2371 if me.Operator == metav1.LabelSelectorOpNotIn || me.Operator == metav1.LabelSelectorOpDoesNotExist { 2372 // Over-matching is fine because this only controls the set of namespace 2373 // label change events to watch and the actual binding enforcement happens 2374 // by checking the intersection of the generated VirtualService.spec.hosts 2375 // and Istio Gateway.spec.servers.hosts arrays - we just can't miss 2376 // potentially relevant namespace label events here. 2377 res = append(res, "*") 2378 } 2379 2380 res = append(res, me.Key) 2381 } 2382 return res 2383 } 2384 2385 func buildListener(r configContext, obj config.Config, l k8s.Listener, listenerIndex int, controllerName k8s.GatewayController) (*istio.Server, bool) { 2386 listenerConditions := map[string]*condition{ 2387 string(k8s.ListenerConditionAccepted): { 2388 reason: string(k8s.ListenerReasonAccepted), 2389 message: "No errors found", 2390 }, 2391 string(k8s.ListenerConditionProgrammed): { 2392 reason: string(k8s.ListenerReasonProgrammed), 2393 message: "No errors found", 2394 }, 2395 string(k8s.ListenerConditionConflicted): { 2396 reason: string(k8s.ListenerReasonNoConflicts), 2397 message: "No errors found", 2398 status: kstatus.StatusFalse, 2399 }, 2400 string(k8s.ListenerConditionResolvedRefs): { 2401 reason: string(k8s.ListenerReasonResolvedRefs), 2402 message: "No errors found", 2403 }, 2404 } 2405 2406 ok := true 2407 tls, err := buildTLS(r, l.TLS, obj, kube.IsAutoPassthrough(obj.Labels, l)) 2408 if err != nil { 2409 listenerConditions[string(k8s.ListenerConditionResolvedRefs)].error = err 2410 listenerConditions[string(k8s.GatewayConditionProgrammed)].error = &ConfigError{ 2411 Reason: string(k8s.GatewayReasonInvalid), 2412 Message: "Bad TLS configuration", 2413 } 2414 ok = false 2415 } 2416 hostnames := buildHostnameMatch(obj.Namespace, r.GatewayResources, l) 2417 server := &istio.Server{ 2418 Port: &istio.Port{ 2419 // Name is required. We only have one server per Gateway, so we can just name them all the same 2420 Name: "default", 2421 Number: uint32(l.Port), 2422 Protocol: listenerProtocolToIstio(l.Protocol), 2423 }, 2424 Hosts: hostnames, 2425 Tls: tls, 2426 } 2427 if controllerName == constants.ManagedGatewayMeshController { 2428 if unexpectedWaypointListener(l) { 2429 listenerConditions[string(k8s.ListenerConditionAccepted)].error = &ConfigError{ 2430 Reason: string(k8s.ListenerReasonUnsupportedProtocol), 2431 Message: `Expected a single listener on port 15008 with protocol "HBONE"`, 2432 } 2433 } 2434 } 2435 2436 reportListenerCondition(listenerIndex, l, obj, listenerConditions) 2437 return server, ok 2438 } 2439 2440 func listenerProtocolToIstio(protocol k8s.ProtocolType) string { 2441 // Currently, all gateway-api protocols are valid Istio protocols. 2442 return string(protocol) 2443 } 2444 2445 func buildTLS(ctx configContext, tls *k8s.GatewayTLSConfig, gw config.Config, isAutoPassthrough bool) (*istio.ServerTLSSettings, *ConfigError) { 2446 if tls == nil { 2447 return nil, nil 2448 } 2449 // Explicitly not supported: file mounted 2450 // Not yet implemented: TLS mode, https redirect, max protocol version, SANs, CipherSuites, VerifyCertificate 2451 out := &istio.ServerTLSSettings{ 2452 HttpsRedirect: false, 2453 } 2454 mode := k8s.TLSModeTerminate 2455 if tls.Mode != nil { 2456 mode = *tls.Mode 2457 } 2458 namespace := gw.Namespace 2459 switch mode { 2460 case k8s.TLSModeTerminate: 2461 out.Mode = istio.ServerTLSSettings_SIMPLE 2462 if tls.Options != nil { 2463 switch tls.Options[gatewayTLSTerminateModeKey] { 2464 case "MUTUAL": 2465 out.Mode = istio.ServerTLSSettings_MUTUAL 2466 case "ISTIO_MUTUAL": 2467 out.Mode = istio.ServerTLSSettings_ISTIO_MUTUAL 2468 return out, nil 2469 } 2470 } 2471 if len(tls.CertificateRefs) != 1 { 2472 // This is required in the API, should be rejected in validation 2473 return out, &ConfigError{Reason: InvalidTLS, Message: "exactly 1 certificateRefs should be present for TLS termination"} 2474 } 2475 cred, err := buildSecretReference(ctx, tls.CertificateRefs[0], gw) 2476 if err != nil { 2477 return out, err 2478 } 2479 credNs := ptr.OrDefault((*string)(tls.CertificateRefs[0].Namespace), namespace) 2480 sameNamespace := credNs == namespace 2481 if !sameNamespace && !ctx.AllowedReferences.SecretAllowed(creds.ToResourceName(cred), namespace) { 2482 return out, &ConfigError{ 2483 Reason: InvalidListenerRefNotPermitted, 2484 Message: fmt.Sprintf( 2485 "certificateRef %v/%v not accessible to a Gateway in namespace %q (missing a ReferenceGrant?)", 2486 tls.CertificateRefs[0].Name, credNs, namespace, 2487 ), 2488 } 2489 } 2490 out.CredentialName = cred 2491 case k8s.TLSModePassthrough: 2492 out.Mode = istio.ServerTLSSettings_PASSTHROUGH 2493 if isAutoPassthrough { 2494 out.Mode = istio.ServerTLSSettings_AUTO_PASSTHROUGH 2495 } 2496 } 2497 return out, nil 2498 } 2499 2500 func buildSecretReference(ctx configContext, ref k8s.SecretObjectReference, gw config.Config) (string, *ConfigError) { 2501 if !nilOrEqual((*string)(ref.Group), gvk.Secret.Group) || !nilOrEqual((*string)(ref.Kind), gvk.Secret.Kind) { 2502 return "", &ConfigError{Reason: InvalidTLS, Message: fmt.Sprintf("invalid certificate reference %v, only secret is allowed", objectReferenceString(ref))} 2503 } 2504 2505 secret := model.ConfigKey{ 2506 Kind: kind.Secret, 2507 Name: string(ref.Name), 2508 Namespace: ptr.OrDefault((*string)(ref.Namespace), gw.Namespace), 2509 } 2510 2511 ctx.resourceReferences[secret] = append(ctx.resourceReferences[secret], model.ConfigKey{ 2512 Kind: kind.KubernetesGateway, 2513 Namespace: gw.Namespace, 2514 Name: gw.Name, 2515 }) 2516 2517 if ctx.Credentials != nil { 2518 if certInfo, err := ctx.Credentials.GetCertInfo(secret.Name, secret.Namespace); err != nil { 2519 return "", &ConfigError{ 2520 Reason: InvalidTLS, 2521 Message: fmt.Sprintf("invalid certificate reference %v, %v", objectReferenceString(ref), err), 2522 } 2523 } else if _, err = tls.X509KeyPair(certInfo.Cert, certInfo.Key); err != nil { 2524 return "", &ConfigError{ 2525 Reason: InvalidTLS, 2526 Message: fmt.Sprintf("invalid certificate reference %v, the certificate is malformed: %v", objectReferenceString(ref), err), 2527 } 2528 } 2529 } 2530 2531 return creds.ToKubernetesGatewayResource(secret.Namespace, secret.Name), nil 2532 } 2533 2534 func objectReferenceString(ref k8s.SecretObjectReference) string { 2535 return fmt.Sprintf("%s/%s/%s.%s", 2536 ptr.OrEmpty(ref.Group), 2537 ptr.OrEmpty(ref.Kind), 2538 ref.Name, 2539 ptr.OrEmpty(ref.Namespace)) 2540 } 2541 2542 func parentRefString(ref k8s.ParentReference) string { 2543 return fmt.Sprintf("%s/%s/%s/%s/%d.%s", 2544 ptr.OrEmpty(ref.Group), 2545 ptr.OrEmpty(ref.Kind), 2546 ref.Name, 2547 ptr.OrEmpty(ref.SectionName), 2548 ptr.OrEmpty(ref.Port), 2549 ptr.OrEmpty(ref.Namespace)) 2550 } 2551 2552 // buildHostnameMatch generates a Gateway.spec.servers.hosts section from a listener 2553 func buildHostnameMatch(localNamespace string, r GatewayResources, l k8s.Listener) []string { 2554 // We may allow all hostnames or a specific one 2555 hostname := "*" 2556 if l.Hostname != nil { 2557 hostname = string(*l.Hostname) 2558 } 2559 2560 resp := []string{} 2561 for _, ns := range namespacesFromSelector(localNamespace, r, l.AllowedRoutes) { 2562 // This check is necessary to prevent adding a hostname with an invalid empty namespace 2563 if len(ns) > 0 { 2564 resp = append(resp, fmt.Sprintf("%s/%s", ns, hostname)) 2565 } 2566 } 2567 2568 // If nothing matched use ~ namespace (match nothing). We need this since its illegal to have an 2569 // empty hostname list, but we still need the Gateway provisioned to ensure status is properly set and 2570 // SNI matches are established; we just don't want to actually match any routing rules (yet). 2571 if len(resp) == 0 { 2572 return []string{"~/" + hostname} 2573 } 2574 return resp 2575 } 2576 2577 // namespacesFromSelector determines a list of allowed namespaces for a given AllowedRoutes 2578 func namespacesFromSelector(localNamespace string, r GatewayResources, lr *k8s.AllowedRoutes) []string { 2579 // Default is to allow only the same namespace 2580 if lr == nil || lr.Namespaces == nil || lr.Namespaces.From == nil || *lr.Namespaces.From == k8s.NamespacesFromSame { 2581 return []string{localNamespace} 2582 } 2583 if *lr.Namespaces.From == k8s.NamespacesFromAll { 2584 return []string{"*"} 2585 } 2586 2587 if lr.Namespaces.Selector == nil { 2588 // Should never happen, invalid config 2589 return []string{"*"} 2590 } 2591 2592 // gateway-api has selectors, but Istio Gateway just has a list of names. We will run the selector 2593 // against all namespaces and get a list of matching namespaces that can be converted into a list 2594 // Istio can handle. 2595 ls, err := metav1.LabelSelectorAsSelector(lr.Namespaces.Selector) 2596 if err != nil { 2597 return nil 2598 } 2599 namespaces := []string{} 2600 for _, ns := range r.Namespaces { 2601 if ls.Matches(toNamespaceSet(ns.Name, ns.Labels)) { 2602 namespaces = append(namespaces, ns.Name) 2603 } 2604 } 2605 // Ensure stable order 2606 sort.Strings(namespaces) 2607 return namespaces 2608 } 2609 2610 func nilOrEqual(have *string, expected string) bool { 2611 return have == nil || *have == expected 2612 } 2613 2614 func humanReadableJoin(ss []string) string { 2615 switch len(ss) { 2616 case 0: 2617 return "" 2618 case 1: 2619 return ss[0] 2620 case 2: 2621 return ss[0] + " and " + ss[1] 2622 default: 2623 return strings.Join(ss[:len(ss)-1], ", ") + ", and " + ss[len(ss)-1] 2624 } 2625 } 2626 2627 // NamespaceNameLabel represents that label added automatically to namespaces is newer Kubernetes clusters 2628 const NamespaceNameLabel = "kubernetes.io/metadata.name" 2629 2630 // toNamespaceSet converts a set of namespace labels to a Set that can be used to select against. 2631 func toNamespaceSet(name string, labels map[string]string) klabels.Set { 2632 // If namespace label is not set, implicitly insert it to support older Kubernetes versions 2633 if labels[NamespaceNameLabel] == name { 2634 // Already set, avoid copies 2635 return labels 2636 } 2637 // First we need a copy to not modify the underlying object 2638 ret := make(map[string]string, len(labels)+1) 2639 for k, v := range labels { 2640 ret[k] = v 2641 } 2642 ret[NamespaceNameLabel] = name 2643 return ret 2644 } 2645 2646 func (kr GatewayResources) FuzzValidate() bool { 2647 for _, gwc := range kr.GatewayClass { 2648 if gwc.Spec == nil { 2649 return false 2650 } 2651 } 2652 for _, rp := range kr.ReferenceGrant { 2653 if rp.Spec == nil { 2654 return false 2655 } 2656 } 2657 for _, hr := range kr.HTTPRoute { 2658 if hr.Spec == nil { 2659 return false 2660 } 2661 } 2662 for _, hr := range kr.GRPCRoute { 2663 if hr.Spec == nil { 2664 return false 2665 } 2666 } 2667 for _, tr := range kr.TLSRoute { 2668 if tr.Spec == nil { 2669 return false 2670 } 2671 } 2672 for _, g := range kr.Gateway { 2673 if g.Spec == nil { 2674 return false 2675 } 2676 } 2677 for _, tr := range kr.TCPRoute { 2678 if tr.Spec == nil { 2679 return false 2680 } 2681 } 2682 return true 2683 }