// Copyright Istio Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package istio import ( "fmt" "iter" "strings" "go.uber.org/atomic" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" inferencev1 "sigs.k8s.io/gateway-api-inference-extension/api/v1" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" gatewayalpha "sigs.k8s.io/gateway-api/apis/v1alpha2" gateway "sigs.k8s.io/gateway-api/apis/v1beta1" istio "istio.io/api/networking/v1alpha3" networkingclient "istio.io/client-go/pkg/apis/networking/v1" "istio.io/istio/pilot/pkg/model" "istio.io/istio/pkg/config" "istio.io/istio/pkg/config/constants" "istio.io/istio/pkg/config/gateway/kube" "istio.io/istio/pkg/config/schema/gvk" "istio.io/istio/pkg/kube/controllers" "istio.io/istio/pkg/kube/krt" "istio.io/istio/pkg/ptr" "istio.io/istio/pkg/slices" "istio.io/istio/pkg/util/sets" ) type AncestorBackend struct { Gateway types.NamespacedName Backend TypedNamespacedName } func (a AncestorBackend) Equals(other AncestorBackend) bool { return a.Gateway == other.Gateway && a.Backend == other.Backend } func (a AncestorBackend) ResourceName() string { return a.Gateway.String() + "/" + a.Backend.String() } func HTTPRouteCollection( httpRoutes krt.Collection[*gateway.HTTPRoute], inputs RouteContextInputs, opts krt.OptionsBuilder, ) RouteResult[*gateway.HTTPRoute, gateway.HTTPRouteStatus] { routeCount := gatewayRouteAttachmentCountCollection(inputs, httpRoutes, gvk.HTTPRoute, opts) ancestorBackends := krt.NewManyCollection(httpRoutes, func(krtctx krt.HandlerContext, obj *gateway.HTTPRoute) []AncestorBackend { return extractAncestorBackends(obj.Namespace, obj.Spec.ParentRefs, obj.Spec.Rules, func(r gateway.HTTPRouteRule) []gateway.HTTPBackendRef { return r.BackendRefs }) }, opts.WithName("HTTPAncestors")...) status, baseVirtualServices := krt.NewStatusManyCollection(httpRoutes, func(krtctx krt.HandlerContext, obj *gateway.HTTPRoute) ( *gateway.HTTPRouteStatus, []RouteWithKey, ) { ctx := inputs.WithCtx(krtctx) inferencePoolCfgPairs := []struct { name string cfg *inferencePoolConfig }{} status := obj.Status.DeepCopy() route := obj.Spec parentStatus, parentRefs, meshResult, gwResult := computeRoute(ctx, obj, func(mesh bool, obj *gateway.HTTPRoute) iter.Seq2[*istio.HTTPRoute, *ConfigError] { return func(yield func(*istio.HTTPRoute, *ConfigError) bool) { for n, r := range route.Rules { // split the rule to make sure each rule has up to one match matches := slices.Reference(r.Matches) if len(matches) == 0 { matches = append(matches, nil) } for _, m := range matches { if m != nil { r.Matches = []gateway.HTTPRouteMatch{*m} } istioRoute, ipCfg, configErr := convertHTTPRoute(ctx, r, obj, n, !mesh) if istioRoute != nil && ipCfg != nil && ipCfg.enableExtProc { inferencePoolCfgPairs = append(inferencePoolCfgPairs, struct { name string cfg *inferencePoolConfig }{name: istioRoute.Name, cfg: ipCfg}) } if !yield(istioRoute, configErr) { return } } } } }) // routeRuleToInferencePoolCfg stores inference pool configs discovered during route rule conversion, // keyed by the istio.HTTPRoute.Name. routeRuleToInferencePoolCfg := make(map[string]*inferencePoolConfig) for _, pair := range inferencePoolCfgPairs { routeRuleToInferencePoolCfg[pair.name] = pair.cfg } status.Parents = parentStatus count := 0 virtualServices := []RouteWithKey{} for _, parent := range filteredReferences(parentRefs) { // for gateway routes, build one VS per gateway+host routeKey := parent.InternalName vsHosts := hostnameToStringList(route.Hostnames) routes := gwResult.routes if parent.IsMesh() { routes = meshResult.routes // for mesh routes, build one VS per namespace/port->host routeKey = obj.Namespace if parent.OriginalReference.Port != nil { routes = augmentPortMatch(routes, *parent.OriginalReference.Port) routeKey += fmt.Sprintf("/%d", *parent.OriginalReference.Port) } ref := types.NamespacedName{ Namespace: string(ptr.OrDefault(parent.OriginalReference.Namespace, gateway.Namespace(obj.Namespace))), Name: string(parent.OriginalReference.Name), } if parent.InternalKind == gvk.ServiceEntry { ses := ptr.Flatten(krt.FetchOne(ctx.Krt, ctx.ServiceEntries, krt.FilterKey(ref.String()))) if ses != nil { vsHosts = ses.Spec.Hosts } else { // TODO: report an error vsHosts = []string{} } } else { vsHosts = []string{fmt.Sprintf("%s.%s.svc.%s", parent.OriginalReference.Name, ptr.OrDefault(parent.OriginalReference.Namespace, gateway.Namespace(obj.Namespace)), ctx.DomainSuffix)} } } if len(routes) == 0 { continue } // Create one VS per hostname with a single hostname. // This ensures we can treat each hostname independently, as the spec requires for _, h := range vsHosts { if !parent.hostnameAllowedByIsolation(h) { // TODO: standardize a status message for this upstream and report continue } name := fmt.Sprintf("%s-%d-%s", obj.Name, count, constants.KubernetesGatewayName) sortHTTPRoutes(routes) // Populate Extra field for inference pool configs extraData := make(map[string]any) currentRouteInferenceConfigs := make(map[string]kube.InferencePoolRouteRuleConfig) for _, httpRule := range routes { // These are []*istio.HTTPRoute if ipCfg, found := routeRuleToInferencePoolCfg[httpRule.Name]; found { currentRouteInferenceConfigs[httpRule.Name] = kube.InferencePoolRouteRuleConfig{ FQDN: ipCfg.endpointPickerDst, Port: ipCfg.endpointPickerPort, FailureModeAllow: ipCfg.endpointPickerFailureMode == string(inferencev1.EndpointPickerFailOpen), } } } if len(currentRouteInferenceConfigs) > 0 { extraData[constants.ConfigExtraPerRouteRuleInferencePoolConfigs] = currentRouteInferenceConfigs } cfg := &config.Config{ Meta: config.Meta{ CreationTimestamp: obj.CreationTimestamp.Time, GroupVersionKind: gvk.VirtualService, Name: name, Annotations: routeMeta(obj), Namespace: obj.Namespace, Domain: ctx.DomainSuffix, }, Spec: &istio.VirtualService{ Hosts: []string{h}, Gateways: []string{parent.InternalName}, Http: routes, }, Extra: extraData, } virtualServices = append(virtualServices, RouteWithKey{ Config: cfg, Key: routeKey + "/" + h, }) count++ } } return status, virtualServices }, opts.WithName("HTTPRoute")...) finalVirtualServices := mergeHTTPRoutes(baseVirtualServices, opts.WithName("HTTPRouteMerged")...) return RouteResult[*gateway.HTTPRoute, gateway.HTTPRouteStatus]{ VirtualServices: finalVirtualServices, RouteAttachments: routeCount, Status: status, Ancestors: ancestorBackends, } } func extractAncestorBackends[RT, BT any](ns string, prefs []gateway.ParentReference, rules []RT, extract func(RT) []BT) []AncestorBackend { gateways := sets.Set[types.NamespacedName]{} for _, r := range prefs { ref := normalizeReference(r.Group, r.Kind, gvk.KubernetesGateway) if ref != gvk.KubernetesGateway { continue } gateways.Insert(types.NamespacedName{ Namespace: defaultString(r.Namespace, ns), Name: string(r.Name), }) } backends := sets.Set[TypedNamespacedName]{} for _, r := range rules { for _, b := range extract(r) { ref, refNs, refName := GetBackendRef(b) k, ok := gvk.ToKind(ref) if !ok { continue } be := TypedNamespacedName{ NamespacedName: types.NamespacedName{ Namespace: defaultString(refNs, ns), Name: string(refName), }, Kind: k, } backends.Insert(be) } } gtw := slices.SortBy(gateways.UnsortedList(), types.NamespacedName.String) bes := slices.SortBy(backends.UnsortedList(), TypedNamespacedName.String) res := make([]AncestorBackend, 0, len(gtw)*len(bes)) for _, gw := range gtw { for _, be := range bes { res = append(res, AncestorBackend{ Gateway: gw, Backend: be, }) } } return res } type conversionResult[O any] struct { error *ConfigError routes []O } func GRPCRouteCollection( grpcRoutes krt.Collection[*gatewayv1.GRPCRoute], inputs RouteContextInputs, opts krt.OptionsBuilder, ) RouteResult[*gatewayv1.GRPCRoute, gatewayv1.GRPCRouteStatus] { routeCount := gatewayRouteAttachmentCountCollection(inputs, grpcRoutes, gvk.GRPCRoute, opts) ancestorBackends := krt.NewManyCollection(grpcRoutes, func(krtctx krt.HandlerContext, obj *gatewayv1.GRPCRoute) []AncestorBackend { return extractAncestorBackends(obj.Namespace, obj.Spec.ParentRefs, obj.Spec.Rules, func(r gatewayv1.GRPCRouteRule) []gatewayv1.GRPCBackendRef { return r.BackendRefs }) }, opts.WithName("GRPCAncestors")...) status, baseVirtualServices := krt.NewStatusManyCollection(grpcRoutes, func(krtctx krt.HandlerContext, obj *gatewayv1.GRPCRoute) ( *gatewayv1.GRPCRouteStatus, []RouteWithKey, ) { ctx := inputs.WithCtx(krtctx) // routeRuleToInferencePoolCfg stores inference pool configs discovered during route rule conversion. // Note: GRPCRoute currently doesn't have inference pool logic, but adding for consistency. routeRuleToInferencePoolCfg := make(map[string]*inferencePoolConfig) status := obj.Status.DeepCopy() route := obj.Spec parentStatus, parentRefs, meshResult, gwResult := computeRoute(ctx, obj, func(mesh bool, obj *gatewayv1.GRPCRoute) iter.Seq2[*istio.HTTPRoute, *ConfigError] { return func(yield func(*istio.HTTPRoute, *ConfigError) bool) { for n, r := range route.Rules { // split the rule to make sure each rule has up to one match matches := slices.Reference(r.Matches) if len(matches) == 0 { matches = append(matches, nil) } for _, m := range matches { if m != nil { r.Matches = []gatewayv1.GRPCRouteMatch{*m} } // GRPCRoute conversion currently doesn't return ipCfg. istioRoute, configErr := convertGRPCRoute(ctx, r, obj, n, !mesh) // Placeholder if GRPCRoute ever supports inference pools via ipCfg: // if istioRoute != nil && ipCfg != nil && ipCfg.enableExtProc { // routeRuleToInferencePoolCfg[istioRoute.Name] = ipCfg // } if !yield(istioRoute, configErr) { return } } } } }) status.Parents = parentStatus count := 0 virtualServices := []RouteWithKey{} for _, parent := range filteredReferences(parentRefs) { // for gateway routes, build one VS per gateway+host routeKey := parent.InternalName vsHosts := hostnameToStringList(route.Hostnames) routes := gwResult.routes if parent.IsMesh() { routes = meshResult.routes // for mesh routes, build one VS per namespace/port->host routeKey = obj.Namespace if parent.OriginalReference.Port != nil { routes = augmentPortMatch(routes, *parent.OriginalReference.Port) routeKey += fmt.Sprintf("/%d", *parent.OriginalReference.Port) } ref := types.NamespacedName{ Namespace: string(ptr.OrDefault(parent.OriginalReference.Namespace, gateway.Namespace(obj.Namespace))), Name: string(parent.OriginalReference.Name), } if parent.InternalKind == gvk.ServiceEntry { ses := ptr.Flatten(krt.FetchOne(ctx.Krt, ctx.ServiceEntries, krt.FilterKey(ref.String()))) if ses != nil { vsHosts = ses.Spec.Hosts } else { // TODO: report an error vsHosts = []string{} } } else { vsHosts = []string{fmt.Sprintf("%s.%s.svc.%s", parent.OriginalReference.Name, ptr.OrDefault(parent.OriginalReference.Namespace, gateway.Namespace(obj.Namespace)), ctx.DomainSuffix)} } } if len(routes) == 0 { continue } // Create one VS per hostname with a single hostname. // This ensures we can treat each hostname independently, as the spec requires for _, h := range vsHosts { if !parent.hostnameAllowedByIsolation(h) { // TODO: standardize a status message for this upstream and report continue } name := fmt.Sprintf("%s-%d-%s", obj.Name, count, constants.KubernetesGatewayName) sortHTTPRoutes(routes) // Populate Extra field for inference pool configs (if GRPCRoute supports them) extraData := make(map[string]any) currentRouteInferenceConfigs := make(map[string]kube.InferencePoolRouteRuleConfig) for _, httpRule := range routes { if ipCfg, found := routeRuleToInferencePoolCfg[httpRule.Name]; found { // This map will be empty for GRPCRoute for now currentRouteInferenceConfigs[httpRule.Name] = kube.InferencePoolRouteRuleConfig{ FQDN: ipCfg.endpointPickerDst, Port: ipCfg.endpointPickerPort, FailureModeAllow: ipCfg.endpointPickerFailureMode == string(inferencev1.EndpointPickerFailOpen), } } } if len(currentRouteInferenceConfigs) > 0 { extraData[constants.ConfigExtraPerRouteRuleInferencePoolConfigs] = currentRouteInferenceConfigs } cfg := &config.Config{ Meta: config.Meta{ CreationTimestamp: obj.CreationTimestamp.Time, GroupVersionKind: gvk.VirtualService, Name: name, Annotations: routeMeta(obj), Namespace: obj.Namespace, Domain: ctx.DomainSuffix, }, Spec: &istio.VirtualService{ Hosts: []string{h}, Gateways: []string{parent.InternalName}, Http: routes, }, Extra: extraData, } virtualServices = append(virtualServices, RouteWithKey{ Config: cfg, Key: routeKey + "/" + h, }) count++ } } return status, virtualServices }, opts.WithName("GRPCRoute")...) finalVirtualServices := mergeHTTPRoutes(baseVirtualServices, opts.WithName("GRPCRouteMerged")...) return RouteResult[*gatewayv1.GRPCRoute, gatewayv1.GRPCRouteStatus]{ VirtualServices: finalVirtualServices, RouteAttachments: routeCount, Status: status, Ancestors: ancestorBackends, } } func TCPRouteCollection( tcpRoutes krt.Collection[*gatewayalpha.TCPRoute], inputs RouteContextInputs, opts krt.OptionsBuilder, ) RouteResult[*gatewayalpha.TCPRoute, gatewayalpha.TCPRouteStatus] { routeCount := gatewayRouteAttachmentCountCollection(inputs, tcpRoutes, gvk.TCPRoute, opts) ancestorBackends := krt.NewManyCollection(tcpRoutes, func(krtctx krt.HandlerContext, obj *gatewayalpha.TCPRoute) []AncestorBackend { return extractAncestorBackends(obj.Namespace, obj.Spec.ParentRefs, obj.Spec.Rules, func(r gatewayalpha.TCPRouteRule) []gateway.BackendRef { return r.BackendRefs }) }, opts.WithName("TCPAncestors")...) status, virtualServices := krt.NewStatusManyCollection(tcpRoutes, func(krtctx krt.HandlerContext, obj *gatewayalpha.TCPRoute) ( *gatewayalpha.TCPRouteStatus, []*config.Config, ) { ctx := inputs.WithCtx(krtctx) status := obj.Status.DeepCopy() route := obj.Spec parentStatus, parentRefs, meshResult, gwResult := computeRoute(ctx, obj, func(mesh bool, obj *gatewayalpha.TCPRoute) iter.Seq2[*istio.TCPRoute, *ConfigError] { return func(yield func(*istio.TCPRoute, *ConfigError) bool) { for _, r := range route.Rules { if !yield(convertTCPRoute(ctx, r, obj, !mesh)) { return } } } }) status.Parents = parentStatus vs := []*config.Config{} count := 0 for _, parent := range filteredReferences(parentRefs) { routes := gwResult.routes vsHosts := []string{"*"} if parent.IsMesh() { routes = meshResult.routes if parent.OriginalReference.Port != nil { routes = augmentTCPPortMatch(routes, *parent.OriginalReference.Port) } ref := types.NamespacedName{ Namespace: string(ptr.OrDefault(parent.OriginalReference.Namespace, gateway.Namespace(obj.Namespace))), Name: string(parent.OriginalReference.Name), } if parent.InternalKind == gvk.ServiceEntry { ses := ptr.Flatten(krt.FetchOne(ctx.Krt, ctx.ServiceEntries, krt.FilterKey(ref.String()))) if ses != nil { vsHosts = ses.Spec.Hosts } else { // TODO: report an error vsHosts = []string{} } } else { vsHosts = []string{fmt.Sprintf("%s.%s.svc.%s", ref.Name, ref.Namespace, ctx.DomainSuffix)} } } for _, host := range vsHosts { name := fmt.Sprintf("%s-tcp-%d-%s", obj.Name, count, constants.KubernetesGatewayName) // Create one VS per hostname with a single hostname. // This ensures we can treat each hostname independently, as the spec requires vs = append(vs, &config.Config{ Meta: config.Meta{ CreationTimestamp: obj.CreationTimestamp.Time, GroupVersionKind: gvk.VirtualService, Name: name, Annotations: routeMeta(obj), Namespace: obj.Namespace, Domain: ctx.DomainSuffix, }, Spec: &istio.VirtualService{ // We can use wildcard here since each listener can have at most one route bound to it, so we have // a single VS per Gateway. Hosts: []string{host}, Gateways: []string{parent.InternalName}, Tcp: routes, }, }) count++ } } return status, vs }, opts.WithName("TCPRoute")...) return RouteResult[*gatewayalpha.TCPRoute, gatewayalpha.TCPRouteStatus]{ VirtualServices: virtualServices, RouteAttachments: routeCount, Status: status, Ancestors: ancestorBackends, } } func TLSRouteCollection( tlsRoutes krt.Collection[*gatewayalpha.TLSRoute], inputs RouteContextInputs, opts krt.OptionsBuilder, ) RouteResult[*gatewayalpha.TLSRoute, gatewayalpha.TLSRouteStatus] { routeCount := gatewayRouteAttachmentCountCollection(inputs, tlsRoutes, gvk.TLSRoute, opts) ancestorBackends := krt.NewManyCollection(tlsRoutes, func(krtctx krt.HandlerContext, obj *gatewayalpha.TLSRoute) []AncestorBackend { return extractAncestorBackends(obj.Namespace, obj.Spec.ParentRefs, obj.Spec.Rules, func(r gatewayalpha.TLSRouteRule) []gateway.BackendRef { return r.BackendRefs }) }, opts.WithName("TLSAncestors")...) status, virtualServices := krt.NewStatusManyCollection(tlsRoutes, func(krtctx krt.HandlerContext, obj *gatewayalpha.TLSRoute) ( *gatewayalpha.TLSRouteStatus, []*config.Config, ) { ctx := inputs.WithCtx(krtctx) status := obj.Status.DeepCopy() route := obj.Spec parentStatus, parentRefs, meshResult, gwResult := computeRoute(ctx, obj, func(mesh bool, obj *gatewayalpha.TLSRoute) iter.Seq2[*istio.TLSRoute, *ConfigError] { return func(yield func(*istio.TLSRoute, *ConfigError) bool) { for _, r := range route.Rules { if !yield(convertTLSRoute(ctx, r, obj, !mesh)) { return } } } }) status.Parents = parentStatus count := 0 vs := []*config.Config{} for _, parent := range filteredReferences(parentRefs) { routes := gwResult.routes vsHosts := hostnameToStringList(route.Hostnames) if parent.IsMesh() { routes = meshResult.routes ref := types.NamespacedName{ Namespace: string(ptr.OrDefault(parent.OriginalReference.Namespace, gateway.Namespace(obj.Namespace))), Name: string(parent.OriginalReference.Name), } if parent.InternalKind == gvk.ServiceEntry { ses := ptr.Flatten(krt.FetchOne(ctx.Krt, ctx.ServiceEntries, krt.FilterKey(ref.String()))) if ses != nil { vsHosts = ses.Spec.Hosts } else { // TODO: report an error vsHosts = []string{} } } else { vsHosts = []string{fmt.Sprintf("%s.%s.svc.%s", ref.Name, ref.Namespace, ctx.DomainSuffix)} } routes = augmentTLSPortMatch(routes, parent.OriginalReference.Port, vsHosts) } for _, host := range vsHosts { name := fmt.Sprintf("%s-tls-%d-%s", obj.Name, count, constants.KubernetesGatewayName) filteredRoutes := routes if parent.IsMesh() { filteredRoutes = compatibleRoutesForHost(routes, host) } // Create one VS per hostname with a single hostname. // This ensures we can treat each hostname independently, as the spec requires vs = append(vs, &config.Config{ Meta: config.Meta{ CreationTimestamp: obj.CreationTimestamp.Time, GroupVersionKind: gvk.VirtualService, Name: name, Annotations: routeMeta(obj), Namespace: obj.Namespace, Domain: ctx.DomainSuffix, }, Spec: &istio.VirtualService{ Hosts: []string{host}, Gateways: []string{parent.InternalName}, Tls: filteredRoutes, }, }) count++ } } return status, vs }, opts.WithName("TLSRoute")...) return RouteResult[*gatewayalpha.TLSRoute, gatewayalpha.TLSRouteStatus]{ VirtualServices: virtualServices, RouteAttachments: routeCount, Status: status, Ancestors: ancestorBackends, } } // computeRoute holds the common route building logic shared amongst all types func computeRoute[T controllers.Object, O comparable](ctx RouteContext, obj T, translator func( mesh bool, obj T, ) iter.Seq2[O, *ConfigError], ) ([]gateway.RouteParentStatus, []routeParentReference, conversionResult[O], conversionResult[O]) { parentRefs := extractParentReferenceInfo(ctx, ctx.RouteParents, obj) convertRules := func(mesh bool) conversionResult[O] { res := conversionResult[O]{} for vs, err := range translator(mesh, obj) { // This was a hard error if controllers.IsNil(vs) { res.error = err return conversionResult[O]{error: err} } // Got an error but also routes if err != nil { res.error = err } res.routes = append(res.routes, vs) } return res } meshResult, gwResult := buildMeshAndGatewayRoutes(parentRefs, convertRules) rpResults := slices.Map(parentRefs, func(r routeParentReference) RouteParentResult { res := RouteParentResult{ OriginalReference: r.OriginalReference, DeniedReason: r.DeniedReason, RouteError: gwResult.error, } if r.IsMesh() { res.RouteError = meshResult.error res.WaypointError = r.WaypointError } return res }) parents := createRouteStatus(rpResults, obj.GetNamespace(), obj.GetGeneration(), GetCommonRouteStateParents(obj)) return parents, parentRefs, meshResult, gwResult } // RouteContext defines a common set of inputs to a route collection. This should be built once per route translation and // not shared outside of that. // The embedded RouteContextInputs is typically based into a collection, then translated to a RouteContext with RouteContextInputs.WithCtx(). type RouteContext struct { Krt krt.HandlerContext RouteContextInputs } func (r RouteContext) LookupHostname(hostname string, namespace string, kind string) *model.Service { if c := r.internalContext.Get(r.Krt).Load(); c != nil { return c.GetService(hostname, namespace, kind) } return nil } type RouteContextInputs struct { Grants ReferenceGrants RouteParents RouteParents DomainSuffix string Services krt.Collection[*corev1.Service] Namespaces krt.Collection[*corev1.Namespace] ServiceEntries krt.Collection[*networkingclient.ServiceEntry] InferencePools krt.Collection[*inferencev1.InferencePool] internalContext krt.RecomputeProtected[*atomic.Pointer[GatewayContext]] } func (i RouteContextInputs) WithCtx(krtctx krt.HandlerContext) RouteContext { return RouteContext{ Krt: krtctx, RouteContextInputs: i, } } type RouteWithKey struct { *config.Config Key string } func (r RouteWithKey) ResourceName() string { return config.NamespacedName(r.Config).String() } func (r RouteWithKey) Equals(o RouteWithKey) bool { return r.Config.Equals(o.Config) } // buildMeshAndGatewayRoutes contains common logic to build a set of routes with mesh and/or gateway semantics func buildMeshAndGatewayRoutes[T any](parentRefs []routeParentReference, convertRules func(mesh bool) T) (T, T) { var meshResult, gwResult T needMesh, needGw := parentTypes(parentRefs) if needMesh { meshResult = convertRules(true) } if needGw { gwResult = convertRules(false) } return meshResult, gwResult } // RouteResult holds the result of a route collection type RouteResult[I controllers.Object, IStatus any] struct { // VirtualServices are the primary output that configures the internal routing logic VirtualServices krt.Collection[*config.Config] // RouteAttachments holds information about parent attachment to routes, used for computed the `attachedRoutes` count. RouteAttachments krt.Collection[RouteAttachment] // Status stores the status reports for the incoming object Status krt.StatusCollection[I, IStatus] // Ancestors stores information about Gateway --> Backend references Ancestors krt.Collection[AncestorBackend] } type RouteAttachment struct { From TypedResource // To is assumed to be a Gateway To types.NamespacedName ListenerName string } func (r RouteAttachment) ResourceName() string { return r.From.Kind.String() + "/" + r.From.Name.String() + "/" + r.To.String() + "/" + r.ListenerName } func (r RouteAttachment) Equals(other RouteAttachment) bool { return r.From == other.From && r.To == other.To && r.ListenerName == other.ListenerName } // gatewayRouteAttachmentCountCollection holds the generic logic to determine the parents a route is attached to, used for // computing the aggregated `attachedRoutes` status in Gateway. func gatewayRouteAttachmentCountCollection[T controllers.Object]( inputs RouteContextInputs, col krt.Collection[T], kind config.GroupVersionKind, opts krt.OptionsBuilder, ) krt.Collection[RouteAttachment] { return krt.NewManyCollection(col, func(krtctx krt.HandlerContext, obj T) []RouteAttachment { ctx := inputs.WithCtx(krtctx) from := TypedResource{ Kind: kind, Name: config.NamespacedName(obj), } parentRefs := extractParentReferenceInfo(ctx, inputs.RouteParents, obj) return slices.MapFilter(filteredReferences(parentRefs), func(e routeParentReference) *RouteAttachment { if e.ParentKey.Kind != gvk.KubernetesGateway { return nil } return &RouteAttachment{ From: from, To: types.NamespacedName{ Name: e.ParentKey.Name, Namespace: e.ParentKey.Namespace, }, ListenerName: string(e.ParentSection), } }) }, opts.WithName(kind.Kind+"/count")...) } // mergeHTTPRoutes merges HTTProutes by key. Gateway API has semantics for the ordering of `match` rules, that merges across resource. // So we merge everything (by key) following that ordering logic, and sort into a linear list (how VirtualService semantics work). func mergeHTTPRoutes(baseVirtualServices krt.Collection[RouteWithKey], opts ...krt.CollectionOption) krt.Collection[*config.Config] { idx := krt.NewIndex(baseVirtualServices, "key", func(o RouteWithKey) []string { return []string{o.Key} }).AsCollection(opts...) finalVirtualServices := krt.NewCollection(idx, func(ctx krt.HandlerContext, object krt.IndexObject[string, RouteWithKey]) **config.Config { configs := object.Objects if len(configs) == 1 { base := configs[0].Config nm := base.Meta.DeepCopy() // When dealing with a merge, we MUST take into account the merge key into the name. // Otherwise, we end up with broken state, where two inputs map to the same output which is not allowed by krt. // Because a lot of code assumes the object key is 'namespace/name', and the key always has slashes, we also translate the / nm.Name = strings.ReplaceAll(object.Key, "/", "~") return ptr.Of(&config.Config{ Meta: nm, Spec: base.Spec, Status: base.Status, Extra: base.Extra, }) } sortRoutesByCreationTime(configs) base := configs[0].DeepCopy() baseVS := base.Spec.(*istio.VirtualService) for _, config := range configs[1:] { thisVS := config.Spec.(*istio.VirtualService) baseVS.Http = append(baseVS.Http, thisVS.Http...) // append parents base.Annotations[constants.InternalParentNames] = fmt.Sprintf("%s,%s", base.Annotations[constants.InternalParentNames], config.Annotations[constants.InternalParentNames]) } sortHTTPRoutes(baseVS.Http) base.Name = strings.ReplaceAll(object.Key, "/", "~") return ptr.Of(&base) }, opts...) return finalVirtualServices }