Files
higress/pkg/ingress/kube/gateway/istio/route_collections.go
2025-11-26 10:15:00 +08:00

782 lines
28 KiB
Go

// 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
}