// 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 ( "cmp" "crypto/tls" "crypto/x509" "fmt" higressconfig "github.com/alibaba/higress/v2/pkg/config" "github.com/alibaba/higress/v2/pkg/ingress/kube/util" "istio.io/istio/pilot/pkg/credentials" "net" "path" inferencev1 "sigs.k8s.io/gateway-api-inference-extension/api/v1" "sort" "strconv" "strings" "time" "google.golang.org/protobuf/types/known/durationpb" wrappers "google.golang.org/protobuf/types/known/wrapperspb" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" klabels "k8s.io/apimachinery/pkg/labels" k8s "sigs.k8s.io/gateway-api/apis/v1" k8salpha "sigs.k8s.io/gateway-api/apis/v1alpha2" k8sbeta "sigs.k8s.io/gateway-api/apis/v1beta1" gatewayx "sigs.k8s.io/gateway-api/apisx/v1alpha1" "istio.io/api/label" istio "istio.io/api/networking/v1alpha3" kubecreds "istio.io/istio/pilot/pkg/credentials/kube" "istio.io/istio/pilot/pkg/features" "istio.io/istio/pilot/pkg/model" creds "istio.io/istio/pilot/pkg/model/credentials" "istio.io/istio/pilot/pkg/model/kstatus" "istio.io/istio/pilot/pkg/serviceregistry/kube" "istio.io/istio/pkg/config" "istio.io/istio/pkg/config/constants" "istio.io/istio/pkg/config/host" "istio.io/istio/pkg/config/protocol" "istio.io/istio/pkg/config/schema/collections" "istio.io/istio/pkg/config/schema/gvk" "istio.io/istio/pkg/config/schema/kind" schematypes "istio.io/istio/pkg/config/schema/kubetypes" "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" ) const ( gatewayTLSTerminateModeKey = "gateway.higress.io/tls-terminate-mode" addressTypeOverride = "networking.higress.io/address-type" gatewayClassDefaults = "gateway.higress.io/defaults-for-class" gatewayNameOverride = "gateway.higress.io/name-override" serviceTypeOverride = "networking.istio.io/service-type" ) func sortConfigByCreationTime(configs []config.Config) { sort.Slice(configs, func(i, j int) bool { if r := configs[i].CreationTimestamp.Compare(configs[j].CreationTimestamp); r != 0 { return r == -1 // -1 means i is less than j, so return true } if r := cmp.Compare(configs[i].Namespace, configs[j].Namespace); r != 0 { return r == -1 } return cmp.Compare(configs[i].Name, configs[j].Name) == -1 }) } func sortRoutesByCreationTime(configs []RouteWithKey) { sort.Slice(configs, func(i, j int) bool { if r := configs[i].CreationTimestamp.Compare(configs[j].CreationTimestamp); r != 0 { return r == -1 // -1 means i is less than j, so return true } if r := cmp.Compare(configs[i].Namespace, configs[j].Namespace); r != 0 { return r == -1 } return cmp.Compare(configs[i].Name, configs[j].Name) == -1 }) } func sortedConfigByCreationTime(configs []config.Config) []config.Config { sortConfigByCreationTime(configs) return configs } func convertHTTPRoute(ctx RouteContext, r k8s.HTTPRouteRule, obj *k8sbeta.HTTPRoute, pos int, enforceRefGrant bool, ) (*istio.HTTPRoute, *inferencePoolConfig, *ConfigError) { vs := &istio.HTTPRoute{} // Start - Modified by Higress //if r.Name != nil { // vs.Name = string(*r.Name) //} else { // // Auto-name the route. If upstream defines an explicit name, will use it instead // // The position within the route is unique // vs.Name = obj.Namespace + "." + obj.Name + "." + strconv.Itoa(pos) // format: %s.%s.%d //} // The best practice for Higress is to configure one HTTP route per route match. vs.Name = generateRouteName(obj, "HTTP") // End - Modified by Higress for _, match := range r.Matches { uri, err := createURIMatch(match) if err != nil { return nil, nil, err } headers, err := createHeadersMatch(match) if err != nil { return nil, nil, err } qp, err := createQueryParamsMatch(match) if err != nil { return nil, nil, err } method, err := createMethodMatch(match) if err != nil { return nil, nil, err } vs.Match = append(vs.Match, &istio.HTTPMatchRequest{ Uri: uri, Headers: headers, QueryParams: qp, Method: method, }) } var mirrorBackendErr *ConfigError for _, filter := range r.Filters { switch filter.Type { case k8s.HTTPRouteFilterRequestHeaderModifier: h := createHeadersFilter(filter.RequestHeaderModifier) if h == nil { continue } if vs.Headers == nil { vs.Headers = &istio.Headers{} } vs.Headers.Request = h case k8s.HTTPRouteFilterResponseHeaderModifier: h := createHeadersFilter(filter.ResponseHeaderModifier) if h == nil { continue } if vs.Headers == nil { vs.Headers = &istio.Headers{} } vs.Headers.Response = h case k8s.HTTPRouteFilterRequestRedirect: vs.Redirect = createRedirectFilter(filter.RequestRedirect) case k8s.HTTPRouteFilterRequestMirror: mirror, err := createMirrorFilter(ctx, filter.RequestMirror, obj.Namespace, enforceRefGrant, gvk.HTTPRoute) if err != nil { mirrorBackendErr = err } else { vs.Mirrors = append(vs.Mirrors, mirror) } case k8s.HTTPRouteFilterURLRewrite: vs.Rewrite = createRewriteFilter(filter.URLRewrite) case k8s.HTTPRouteFilterCORS: vs.CorsPolicy = createCorsFilter(filter.CORS) default: return nil, nil, &ConfigError{ Reason: InvalidFilter, Message: fmt.Sprintf("unsupported filter type %q", filter.Type), } } } if r.Retry != nil { // "Implementations SHOULD retry on connection errors (disconnect, reset, timeout, // TCP failure) if a retry stanza is configured." retryOn := []string{"connect-failure", "refused-stream", "unavailable", "cancelled"} for _, codes := range r.Retry.Codes { retryOn = append(retryOn, strconv.Itoa(int(codes))) } vs.Retries = &istio.HTTPRetry{ // If unset, default is implementation specific. // VirtualService.retry has no default when set -- users are expected to set it if they customize `retry`. // However, the default retry if none are set is "2", so we use that as the default. Attempts: int32(ptr.OrDefault(r.Retry.Attempts, 2)), PerTryTimeout: nil, RetryOn: strings.Join(retryOn, ","), } if vs.Retries.Attempts == 0 { // Invalid to set this when there are no attempts vs.Retries.RetryOn = "" } if r.Retry.Backoff != nil { retrybackOff, _ := time.ParseDuration(string(*r.Retry.Backoff)) vs.Retries.Backoff = durationpb.New(retrybackOff) } } if r.Timeouts != nil { if r.Timeouts.Request != nil { request, _ := time.ParseDuration(string(*r.Timeouts.Request)) if request != 0 { vs.Timeout = durationpb.New(request) } } if r.Timeouts.BackendRequest != nil { backendRequest, _ := time.ParseDuration(string(*r.Timeouts.BackendRequest)) if backendRequest != 0 { timeout := durationpb.New(backendRequest) if vs.Retries != nil { vs.Retries.PerTryTimeout = timeout } else { vs.Timeout = timeout } } } } if weightSum(r.BackendRefs) == 0 && vs.Redirect == nil { // The spec requires us to return 500 when there are no >0 weight backends vs.DirectResponse = &istio.HTTPDirectResponse{ Status: 500, } } else { route, ipCfg, backendErr, err := buildHTTPDestination(ctx, r.BackendRefs, obj.Namespace, enforceRefGrant) if err != nil { return nil, nil, err } vs.Route = route return vs, ipCfg, joinErrors(backendErr, mirrorBackendErr) } return vs, nil, mirrorBackendErr } func joinErrors(a *ConfigError, b *ConfigError) *ConfigError { if b == nil { return a } if a == nil { return b } a.Message += "; " + b.Message return a } func convertGRPCRoute(ctx RouteContext, r k8s.GRPCRouteRule, obj *k8s.GRPCRoute, pos int, enforceRefGrant bool, ) (*istio.HTTPRoute, *ConfigError) { // Assuming GRPCRoute doesn't need inferencePoolConfig for now vs := &istio.HTTPRoute{} // Start - Modified by Higress //if r.Name != nil { // vs.Name = string(*r.Name) //} else { // // Auto-name the route. If upstream defines an explicit name, will use it instead // // The position within the route is unique // vs.Name = obj.Namespace + "." + obj.Name + "." + strconv.Itoa(pos) // format:%s.%s.%d //} // The best practice for Higress is to configure one HTTP route per route match. vs.Name = generateRouteName(obj, "GRPC") // End - Modified by Higress for _, match := range r.Matches { uri, err := createGRPCURIMatch(match) if err != nil { return nil, err } headers, err := createGRPCHeadersMatch(match) if err != nil { return nil, err } vs.Match = append(vs.Match, &istio.HTTPMatchRequest{ Uri: uri, Headers: headers, }) } for _, filter := range r.Filters { switch filter.Type { case k8s.GRPCRouteFilterRequestHeaderModifier: h := createHeadersFilter(filter.RequestHeaderModifier) if h == nil { continue } if vs.Headers == nil { vs.Headers = &istio.Headers{} } vs.Headers.Request = h case k8s.GRPCRouteFilterResponseHeaderModifier: h := createHeadersFilter(filter.ResponseHeaderModifier) if h == nil { continue } if vs.Headers == nil { vs.Headers = &istio.Headers{} } vs.Headers.Response = h case k8s.GRPCRouteFilterRequestMirror: mirror, err := createMirrorFilter(ctx, filter.RequestMirror, obj.Namespace, enforceRefGrant, gvk.GRPCRoute) if err != nil { return nil, err } vs.Mirrors = append(vs.Mirrors, mirror) default: return nil, &ConfigError{ Reason: InvalidFilter, Message: fmt.Sprintf("unsupported filter type %q", filter.Type), } } } if grpcWeightSum(r.BackendRefs) == 0 && vs.Redirect == nil { // The spec requires us to return 500 when there are no >0 weight backends vs.DirectResponse = &istio.HTTPDirectResponse{ Status: 500, } } else { route, backendErr, err := buildGRPCDestination(ctx, r.BackendRefs, obj.Namespace, enforceRefGrant) if err != nil { return nil, err } vs.Route = route return vs, backendErr } return vs, nil } func parentTypes(rpi []routeParentReference) (mesh, gateway bool) { for _, r := range rpi { if r.IsMesh() { mesh = true } else { gateway = true } } return mesh, gateway } func augmentPortMatch(routes []*istio.HTTPRoute, port k8s.PortNumber) []*istio.HTTPRoute { res := make([]*istio.HTTPRoute, 0, len(routes)) for _, r := range routes { r = r.DeepCopy() for _, m := range r.Match { m.Port = uint32(port) } if len(r.Match) == 0 { r.Match = []*istio.HTTPMatchRequest{{ Port: uint32(port), }} } res = append(res, r) } return res } func augmentTCPPortMatch(routes []*istio.TCPRoute, port k8s.PortNumber) []*istio.TCPRoute { res := make([]*istio.TCPRoute, 0, len(routes)) for _, r := range routes { r = r.DeepCopy() for _, m := range r.Match { m.Port = uint32(port) } if len(r.Match) == 0 { r.Match = []*istio.L4MatchAttributes{{ Port: uint32(port), }} } res = append(res, r) } return res } func augmentTLSPortMatch(routes []*istio.TLSRoute, port *k8s.PortNumber, parentHosts []string) []*istio.TLSRoute { res := make([]*istio.TLSRoute, 0, len(routes)) for _, r := range routes { r = r.DeepCopy() if len(r.Match) == 1 && slices.Equal(r.Match[0].SniHosts, []string{"*"}) { // For mesh, we use parent hosts for SNI if TLSRroute.hostnames were not specified. r.Match[0].SniHosts = parentHosts } for _, m := range r.Match { if port != nil { m.Port = uint32(*port) } } res = append(res, r) } return res } func compatibleRoutesForHost(routes []*istio.TLSRoute, parentHost string) []*istio.TLSRoute { res := make([]*istio.TLSRoute, 0, len(routes)) for _, r := range routes { if len(r.Match) == 1 && len(r.Match[0].SniHosts) > 1 { r = r.DeepCopy() sniHosts := []string{} for _, h := range r.Match[0].SniHosts { if host.Name(parentHost).Matches(host.Name(h)) { sniHosts = append(sniHosts, h) } } r.Match[0].SniHosts = sniHosts } res = append(res, r) } return res } func routeMeta(obj controllers.Object) map[string]string { m := parentMeta(obj, nil) m[constants.InternalRouteSemantics] = constants.RouteSemanticsGateway return m } // sortHTTPRoutes sorts generated vs routes to meet gateway-api requirements // see https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.HTTPRouteRule func sortHTTPRoutes(routes []*istio.HTTPRoute) { sort.SliceStable(routes, func(i, j int) bool { if len(routes[i].Match) == 0 { return false } else if len(routes[j].Match) == 0 { return true } // Start - Added by Higress if isCatchAllMatch(routes[i].Match[0]) { return false } else if isCatchAllMatch(routes[j].Match[0]) { return true } // End - Added by Higress // Only look at match[0], we always generate only one match m1, m2 := routes[i].Match[0], routes[j].Match[0] r1, r2 := getURIRank(m1), getURIRank(m2) len1, len2 := getURILength(m1), getURILength(m2) switch { // 1: Exact/Prefix/Regex case r1 != r2: return r1 > r2 case len1 != len2: return len1 > len2 // 2: method math case (m1.Method == nil) != (m2.Method == nil): return m1.Method != nil // 3: number of header matches case len(m1.Headers) != len(m2.Headers): return len(m1.Headers) > len(m2.Headers) // 4: number of query matches default: return len(m1.QueryParams) > len(m2.QueryParams) } }) } func parentMeta(obj controllers.Object, sectionName *k8s.SectionName) map[string]string { name := fmt.Sprintf("%s/%s.%s", schematypes.GvkFromObject(obj).Kind, obj.GetName(), obj.GetNamespace()) if sectionName != nil { name = fmt.Sprintf("%s/%s/%s.%s", schematypes.GvkFromObject(obj).Kind, obj.GetName(), *sectionName, obj.GetNamespace()) } return map[string]string{ constants.InternalParentNames: name, } } // getURIRank ranks a URI match type. Exact > Prefix > Regex func getURIRank(match *istio.HTTPMatchRequest) int { if match.Uri == nil { return -1 } switch match.Uri.MatchType.(type) { case *istio.StringMatch_Exact: return 3 case *istio.StringMatch_Prefix: return 2 case *istio.StringMatch_Regex: return 1 } // should not happen return -1 } func getURILength(match *istio.HTTPMatchRequest) int { if match.Uri == nil { return 0 } switch match.Uri.MatchType.(type) { case *istio.StringMatch_Prefix: return len(match.Uri.GetPrefix()) case *istio.StringMatch_Exact: return len(match.Uri.GetExact()) case *istio.StringMatch_Regex: return len(match.Uri.GetRegex()) } // should not happen return -1 } func hostnameToStringList(h []k8s.Hostname) []string { // In the Istio API, empty hostname is not allowed. In the Kubernetes API hosts means "any" if len(h) == 0 { return []string{"*"} } return slices.Map(h, func(e k8s.Hostname) string { return string(e) }) } var allowedParentReferences = sets.New( gvk.KubernetesGateway, gvk.Service, gvk.ServiceEntry, gvk.XListenerSet, ) func toInternalParentReference(p k8s.ParentReference, localNamespace string) (parentKey, error) { ref := normalizeReference(p.Group, p.Kind, gvk.KubernetesGateway) if !allowedParentReferences.Contains(ref) { return parentKey{}, fmt.Errorf("unsupported parent: %v/%v", p.Group, p.Kind) } return parentKey{ Kind: ref, Name: string(p.Name), // Unset namespace means "same namespace" Namespace: defaultString(p.Namespace, localNamespace), }, nil } // waypointConfigured returns true if a waypoint is configured via expected label's key-value pair. func waypointConfigured(labels map[string]string) bool { if val, ok := labels[label.IoIstioUseWaypoint.Name]; ok && len(val) > 0 && !strings.EqualFold(val, "none") { return true } return false } func referenceAllowed( ctx RouteContext, parent *parentInfo, routeKind config.GroupVersionKind, parentRef parentReference, hostnames []k8s.Hostname, localNamespace string, ) (*ParentError, *WaypointError) { if parentRef.Kind == gvk.Service { key := parentRef.Namespace + "/" + parentRef.Name svc := ptr.Flatten(krt.FetchOne(ctx.Krt, ctx.Services, krt.FilterKey(key))) // check that the referenced svc exists if svc == nil { return &ParentError{ Reason: ParentErrorNotAccepted, Message: fmt.Sprintf("parent service: %q not found", parentRef.Name), }, &WaypointError{ Reason: WaypointErrorReasonNoMatchingParent, Message: WaypointErrorMsgNoMatchingParent, } } // check that the reference has the use-waypoint label if !waypointConfigured(svc.Labels) { // if reference does not have use-waypoint label, check the namespace of the reference ns := ptr.Flatten(krt.FetchOne(ctx.Krt, ctx.Namespaces, krt.FilterKey(svc.Namespace))) if ns != nil { if !waypointConfigured(ns.Labels) { return nil, &WaypointError{ Reason: WaypointErrorReasonMissingLabel, Message: WaypointErrorMsgMissingLabel, } } } } } else if parentRef.Kind == gvk.ServiceEntry { // check that the referenced svc entry exists key := parentRef.Namespace + "/" + parentRef.Name svcEntry := ptr.Flatten(krt.FetchOne(ctx.Krt, ctx.ServiceEntries, krt.FilterKey(key))) if svcEntry == nil { return &ParentError{ Reason: ParentErrorNotAccepted, Message: fmt.Sprintf("parent service entry: %q not found", parentRef.Name), }, &WaypointError{ Reason: WaypointErrorReasonNoMatchingParent, Message: WaypointErrorMsgNoMatchingParent, } } // check that the reference has the use-waypoint label if !waypointConfigured(svcEntry.Labels) { // if reference does not have use-waypoint label, check the namespace of the reference ns := ptr.Flatten(krt.FetchOne(ctx.Krt, ctx.Namespaces, krt.FilterKey(parentRef.Namespace))) if ns != nil { if !waypointConfigured(ns.Labels) { return nil, &WaypointError{ Reason: WaypointErrorReasonMissingLabel, Message: WaypointErrorMsgMissingLabel, } } } } } else { // First, check section and port apply. This must come first if parentRef.Port != 0 && parentRef.Port != parent.Port { return &ParentError{ Reason: ParentErrorNotAccepted, Message: fmt.Sprintf("port %v not found", parentRef.Port), }, nil } if len(parentRef.SectionName) > 0 && parentRef.SectionName != parent.SectionName { return &ParentError{ Reason: ParentErrorNotAccepted, Message: fmt.Sprintf("sectionName %q not found", parentRef.SectionName), }, nil } // Next check the hostnames are a match. This is a bi-directional wildcard match. Only one route // hostname must match for it to be allowed (but the others will be filtered at runtime) // If either is empty its treated as a wildcard which always matches if len(hostnames) == 0 { hostnames = []k8s.Hostname{"*"} } if len(parent.Hostnames) > 0 { // TODO: the spec actually has a label match, not a string match. That is, *.com does not match *.apple.com // We are doing a string match here matched := false hostMatched := false out: for _, routeHostname := range hostnames { for _, parentHostNamespace := range parent.Hostnames { var parentNamespace, parentHostname string // When parentHostNamespace lacks a '/', it was likely sanitized from '*/host' to 'host' // by sanitizeServerHostNamespace. Set parentNamespace to '*' to reflect the wildcard namespace // and parentHostname to the sanitized host to prevent an index out of range panic. if strings.Contains(parentHostNamespace, "/") { spl := strings.Split(parentHostNamespace, "/") parentNamespace, parentHostname = spl[0], spl[1] } else { parentNamespace, parentHostname = "*", parentHostNamespace } hostnameMatch := host.Name(parentHostname).Matches(host.Name(routeHostname)) namespaceMatch := parentNamespace == "*" || parentNamespace == localNamespace hostMatched = hostMatched || hostnameMatch if hostnameMatch && namespaceMatch { matched = true break out } } } if !matched { if hostMatched { return &ParentError{ Reason: ParentErrorNotAllowed, Message: fmt.Sprintf( "hostnames matched parent hostname %q, but namespace %q is not allowed by the parent", parent.OriginalHostname, localNamespace, ), }, nil } return &ParentError{ Reason: ParentErrorNoHostname, Message: fmt.Sprintf( "no hostnames matched parent hostname %q", parent.OriginalHostname, ), }, nil } } } // Also make sure this route kind is allowed matched := false for _, ak := range parent.AllowedKinds { if string(ak.Kind) == routeKind.Kind && ptr.OrDefault((*string)(ak.Group), gvk.GatewayClass.Group) == routeKind.Group { matched = true break } } if !matched { return &ParentError{ Reason: ParentErrorNotAllowed, Message: fmt.Sprintf("kind %v is not allowed", routeKind), }, nil } return nil, nil } func extractParentReferenceInfo(ctx RouteContext, parents RouteParents, obj controllers.Object) []routeParentReference { routeRefs, hostnames, kind := GetCommonRouteInfo(obj) localNamespace := obj.GetNamespace() parentRefs := []routeParentReference{} for _, ref := range routeRefs { ir, err := toInternalParentReference(ref, localNamespace) if err != nil { // Cannot handle the reference. Maybe it is for another controller, so we just ignore it continue } pk := parentReference{ parentKey: ir, SectionName: ptr.OrEmpty(ref.SectionName), Port: ptr.OrEmpty(ref.Port), } gk := ir if ir.Kind == gvk.Service || ir.Kind == gvk.ServiceEntry { gk = meshParentKey } currentParents := parents.fetch(ctx.Krt, gk) appendParent := func(pr *parentInfo, pk parentReference) { bannedHostnames := sets.New[string]() for _, gw := range currentParents { if gw == pr { continue // do not ban ourself } if gw.Port != pr.Port { // We only care about listeners on the same port continue } if gw.Protocol != pr.Protocol { // We only care about listeners on the same protocol continue } bannedHostnames.Insert(gw.OriginalHostname) } deniedReason, waypointError := referenceAllowed(ctx, pr, kind, pk, hostnames, localNamespace) rpi := routeParentReference{ InternalName: pr.InternalName, InternalKind: ir.Kind, Hostname: pr.OriginalHostname, DeniedReason: deniedReason, OriginalReference: ref, BannedHostnames: bannedHostnames.Copy().Delete(pr.OriginalHostname), ParentKey: ir, ParentSection: pr.SectionName, WaypointError: waypointError, } parentRefs = append(parentRefs, rpi) } for _, gw := range currentParents { // Append all matches. Note we may be adding mismatch section or ports; this is handled later appendParent(gw, pk) } } // Ensure stable order slices.SortBy(parentRefs, func(a routeParentReference) string { return parentRefString(a.OriginalReference, localNamespace) }) return parentRefs } func convertTCPRoute(ctx RouteContext, r k8salpha.TCPRouteRule, obj *k8salpha.TCPRoute, enforceRefGrant bool) (*istio.TCPRoute, *ConfigError) { if tcpWeightSum(r.BackendRefs) == 0 { // The spec requires us to reject connections when there are no >0 weight backends // We don't have a great way to do it. TODO: add a fault injection API for TCP? return &istio.TCPRoute{ Route: []*istio.RouteDestination{{ Destination: &istio.Destination{ Host: "internal.cluster.local", Subset: "zero-weight", Port: &istio.PortSelector{Number: 65535}, }, Weight: 0, }}, }, nil } dest, backendErr, err := buildTCPDestination(ctx, r.BackendRefs, obj.Namespace, enforceRefGrant, gvk.TCPRoute) if err != nil { return nil, err } return &istio.TCPRoute{ Route: dest, }, backendErr } func convertTLSRoute(ctx RouteContext, r k8salpha.TLSRouteRule, obj *k8salpha.TLSRoute, enforceRefGrant bool) (*istio.TLSRoute, *ConfigError) { if tcpWeightSum(r.BackendRefs) == 0 { // The spec requires us to reject connections when there are no >0 weight backends // We don't have a great way to do it. TODO: add a fault injection API for TCP? return &istio.TLSRoute{ Route: []*istio.RouteDestination{{ Destination: &istio.Destination{ Host: "internal.cluster.local", Subset: "zero-weight", Port: &istio.PortSelector{Number: 65535}, }, Weight: 0, }}, }, nil } dest, backendErr, err := buildTCPDestination(ctx, r.BackendRefs, obj.Namespace, enforceRefGrant, gvk.TLSRoute) if err != nil { return nil, err } return &istio.TLSRoute{ Match: buildTLSMatch(obj.Spec.Hostnames), Route: dest, }, backendErr } func buildTCPDestination( ctx RouteContext, forwardTo []k8s.BackendRef, ns string, enforceRefGrant bool, k config.GroupVersionKind, ) ([]*istio.RouteDestination, *ConfigError, *ConfigError) { if forwardTo == nil { return nil, nil, nil } weights := []int{} action := []k8s.BackendRef{} for _, w := range forwardTo { wt := int(ptr.OrDefault(w.Weight, 1)) if wt == 0 { continue } action = append(action, w) weights = append(weights, wt) } if len(weights) == 1 { weights = []int{0} } var invalidBackendErr *ConfigError res := []*istio.RouteDestination{} for i, fwd := range action { dst, _, err := buildDestination(ctx, fwd, ns, enforceRefGrant, k) if err != nil { if isInvalidBackend(err) { invalidBackendErr = err // keep going, we will gracefully drop invalid backends } else { return nil, nil, err } } res = append(res, &istio.RouteDestination{ Destination: dst, Weight: int32(weights[i]), }) } return res, invalidBackendErr, nil } func buildTLSMatch(hostnames []k8s.Hostname) []*istio.TLSMatchAttributes { // Currently, the spec only supports extensions beyond hostname, which are not currently implemented by Istio. return []*istio.TLSMatchAttributes{{ SniHosts: hostnamesToStringListWithWildcard(hostnames), }} } func hostnamesToStringListWithWildcard(h []k8s.Hostname) []string { if len(h) == 0 { return []string{"*"} } res := make([]string, 0, len(h)) for _, i := range h { res = append(res, string(i)) } return res } func weightSum(forwardTo []k8s.HTTPBackendRef) int { sum := int32(0) for _, w := range forwardTo { sum += ptr.OrDefault(w.Weight, 1) } return int(sum) } func grpcWeightSum(forwardTo []k8s.GRPCBackendRef) int { sum := int32(0) for _, w := range forwardTo { sum += ptr.OrDefault(w.Weight, 1) } return int(sum) } func tcpWeightSum(forwardTo []k8s.BackendRef) int { sum := int32(0) for _, w := range forwardTo { sum += ptr.OrDefault(w.Weight, 1) } return int(sum) } func buildHTTPDestination( ctx RouteContext, forwardTo []k8s.HTTPBackendRef, ns string, enforceRefGrant bool, ) ([]*istio.HTTPRouteDestination, *inferencePoolConfig, *ConfigError, *ConfigError) { if forwardTo == nil { return nil, nil, nil, nil } weights := []int{} action := []k8s.HTTPBackendRef{} for _, w := range forwardTo { wt := int(ptr.OrDefault(w.Weight, 1)) if wt == 0 { continue } action = append(action, w) weights = append(weights, wt) } if len(weights) == 1 { weights = []int{0} } var invalidBackendErr *ConfigError var ipCfg *inferencePoolConfig res := []*istio.HTTPRouteDestination{} for i, fwd := range action { dst, ipconfig, err := buildDestination(ctx, fwd.BackendRef, ns, enforceRefGrant, gvk.HTTPRoute) ipCfg = ipconfig if err != nil { if isInvalidBackend(err) { invalidBackendErr = err // keep going, we will gracefully drop invalid backends } else { return nil, ipCfg, nil, err } } rd := &istio.HTTPRouteDestination{ Destination: dst, Weight: int32(weights[i]), } for _, filter := range fwd.Filters { switch filter.Type { case k8s.HTTPRouteFilterRequestHeaderModifier: h := createHeadersFilter(filter.RequestHeaderModifier) if h == nil { continue } if rd.Headers == nil { rd.Headers = &istio.Headers{} } rd.Headers.Request = h case k8s.HTTPRouteFilterResponseHeaderModifier: h := createHeadersFilter(filter.ResponseHeaderModifier) if h == nil { continue } if rd.Headers == nil { rd.Headers = &istio.Headers{} } rd.Headers.Response = h default: return nil, ipCfg, nil, &ConfigError{Reason: InvalidFilter, Message: fmt.Sprintf("unsupported filter type %q", filter.Type)} } } res = append(res, rd) } return res, ipCfg, invalidBackendErr, nil } func buildGRPCDestination( ctx RouteContext, forwardTo []k8s.GRPCBackendRef, ns string, enforceRefGrant bool, ) ([]*istio.HTTPRouteDestination, *ConfigError, *ConfigError) { if forwardTo == nil { return nil, nil, nil } weights := []int{} action := []k8s.GRPCBackendRef{} for _, w := range forwardTo { wt := int(ptr.OrDefault(w.Weight, 1)) if wt == 0 { continue } action = append(action, w) weights = append(weights, wt) } if len(weights) == 1 { weights = []int{0} } var invalidBackendErr *ConfigError res := []*istio.HTTPRouteDestination{} for i, fwd := range action { dst, _, err := buildDestination(ctx, fwd.BackendRef, ns, enforceRefGrant, gvk.GRPCRoute) if err != nil { if isInvalidBackend(err) { invalidBackendErr = err // keep going, we will gracefully drop invalid backends } else { return nil, nil, err } } rd := &istio.HTTPRouteDestination{ Destination: dst, Weight: int32(weights[i]), } for _, filter := range fwd.Filters { switch filter.Type { case k8s.GRPCRouteFilterRequestHeaderModifier: h := createHeadersFilter(filter.RequestHeaderModifier) if h == nil { continue } if rd.Headers == nil { rd.Headers = &istio.Headers{} } rd.Headers.Request = h case k8s.GRPCRouteFilterResponseHeaderModifier: h := createHeadersFilter(filter.ResponseHeaderModifier) if h == nil { continue } if rd.Headers == nil { rd.Headers = &istio.Headers{} } rd.Headers.Response = h default: return nil, nil, &ConfigError{Reason: InvalidFilter, Message: fmt.Sprintf("unsupported filter type %q", filter.Type)} } } res = append(res, rd) } return res, invalidBackendErr, nil } type inferencePoolConfig struct { enableExtProc bool endpointPickerDst string endpointPickerPort string endpointPickerFailureMode string } func buildDestination(ctx RouteContext, to k8s.BackendRef, ns string, enforceRefGrant bool, k config.GroupVersionKind, ) (*istio.Destination, *inferencePoolConfig, *ConfigError) { ref := normalizeReference(to.Group, to.Kind, gvk.Service) // check if the reference is allowed if enforceRefGrant { if toNs := to.Namespace; toNs != nil && string(*toNs) != ns { if !ctx.Grants.BackendAllowed(ctx.Krt, k, ref, to.Name, *toNs, ns) { return &istio.Destination{}, nil, &ConfigError{ Reason: InvalidDestinationPermit, Message: fmt.Sprintf("backendRef %v/%v not accessible to a %s in namespace %q (missing a ReferenceGrant?)", to.Name, *toNs, k.Kind, ns), } } } } namespace := ptr.OrDefault((*string)(to.Namespace), ns) var invalidBackendErr *ConfigError var hostname string switch ref { case gvk.Service: if strings.Contains(string(to.Name), ".") { return nil, nil, &ConfigError{Reason: InvalidDestination, Message: "service name invalid; the name of the Service must be used, not the hostname."} } hostname = fmt.Sprintf("%s.%s.svc.%s", to.Name, namespace, ctx.DomainSuffix) // Start - Updated by Higress //key := namespace + "/" + string(to.Name) //svc := ptr.Flatten(krt.FetchOne(ctx.Krt, ctx.Services, krt.FilterKey(key))) svc := ctx.LookupHostname(hostname, namespace, "Service") // End - Updated by Higress if svc == nil { invalidBackendErr = &ConfigError{Reason: InvalidDestinationNotFound, Message: fmt.Sprintf("backend(%s) not found", hostname)} } case config.GroupVersionKind{Group: gvk.ServiceEntry.Group, Kind: "Hostname"}: if to.Namespace != nil { return nil, nil, &ConfigError{Reason: InvalidDestination, Message: "namespace may not be set with Hostname type"} } hostname = string(to.Name) // Start - Updated by Higress if ctx.LookupHostname(hostname, namespace, "Hostname") == nil { // End - Updated by Higress invalidBackendErr = &ConfigError{Reason: InvalidDestinationNotFound, Message: fmt.Sprintf("backend(%s) not found", hostname)} } case config.GroupVersionKind{Group: features.MCSAPIGroup, Kind: "ServiceImport"}: hostname = fmt.Sprintf("%s.%s.svc.clusterset.local", to.Name, namespace) if !features.EnableMCSHost { // They asked for ServiceImport, but actually don't have full support enabled... // No problem, we can just treat it as Service, which is already cross-cluster in this mode anyways hostname = fmt.Sprintf("%s.%s.svc.%s", to.Name, namespace, ctx.DomainSuffix) } // TODO: currently we are always looking for Service. We should be looking for ServiceImport when features.EnableMCSHost // Start - Updated by Higress //key := namespace + "/" + string(to.Name) //svc := ptr.Flatten(krt.FetchOne(ctx.Krt, ctx.Services, krt.FilterKey(key))) svc := ctx.LookupHostname(hostname, namespace, "ServiceImport") // End - Updated by Higress if svc == nil { invalidBackendErr = &ConfigError{Reason: InvalidDestinationNotFound, Message: fmt.Sprintf("backend(%s) not found", hostname)} } case gvk.InferencePool: if !features.EnableGatewayAPIInferenceExtension { return &istio.Destination{}, nil, &ConfigError{ Reason: InvalidDestinationKind, Message: "InferencePool is not enabled. To enable, set ENABLE_GATEWAY_API_INFERENCE_EXTENSION to true in istiod", } } if strings.Contains(string(to.Name), ".") { return nil, nil, &ConfigError{ Reason: InvalidDestination, Message: "InferencePool.Name invalid; the name of the InferencePool must be used, not the hostname.", } } infPool := ptr.Flatten(krt.FetchOne(ctx.Krt, ctx.InferencePools, krt.FilterKey(namespace+"/"+string(to.Name)))) if infPool == nil { // Inference pool doesn't exist invalidBackendErr = &ConfigError{Reason: InvalidDestinationNotFound, Message: fmt.Sprintf("backend(%s) not found", to.Name)} return &istio.Destination{}, nil, invalidBackendErr } inferencePoolServiceName, _ := InferencePoolServiceName(string(to.Name)) hostname := fmt.Sprintf("%s.%s.svc.%s", inferencePoolServiceName, namespace, ctx.DomainSuffix) svc := ctx.LookupHostname(hostname, namespace, "InferencePool") if svc == nil { invalidBackendErr = &ConfigError{Reason: InvalidDestinationNotFound, Message: fmt.Sprintf("backend(%s) not found", hostname)} return &istio.Destination{}, nil, invalidBackendErr } if svc.Attributes.Labels == nil { invalidBackendErr = &ConfigError{Reason: InvalidDestination, Message: "InferencePool service invalid, extensionRef labels not found"} return &istio.Destination{}, nil, invalidBackendErr } ipCfg := &inferencePoolConfig{ enableExtProc: true, } if dst, ok := svc.Attributes.Labels[InferencePoolExtensionRefSvc]; ok { ipCfg.endpointPickerDst = fmt.Sprintf("%s.%s.svc.%s", dst, infPool.Namespace, ctx.DomainSuffix) } if p, ok := svc.Attributes.Labels[InferencePoolExtensionRefPort]; ok { ipCfg.endpointPickerPort = p } if fm, ok := svc.Attributes.Labels[InferencePoolExtensionRefFailureMode]; ok { ipCfg.endpointPickerFailureMode = fm } if ipCfg.endpointPickerDst == "" || ipCfg.endpointPickerPort == "" || ipCfg.endpointPickerFailureMode == "" { invalidBackendErr = &ConfigError{Reason: InvalidDestination, Message: "InferencePool service invalid, extensionRef labels not found"} } return &istio.Destination{ Host: hostname, // Port: &istio.PortSelector{Number: uint32(*to.Port)}, }, ipCfg, invalidBackendErr default: return &istio.Destination{}, nil, &ConfigError{ Reason: InvalidDestinationKind, Message: fmt.Sprintf("referencing unsupported backendRef: group %q kind %q", ptr.OrEmpty(to.Group), ptr.OrEmpty(to.Kind)), } } // Start - Added by Higress if equal((*string)(to.Group), "networking.higress.io") && nilOrEqual((*string)(to.Kind), "Service") { var port *istio.PortSelector if to.Port != nil { port = &istio.PortSelector{Number: uint32(*to.Port)} } return &istio.Destination{ Host: string(to.Name), Port: port, }, nil, nil } // End - Added by Higress // All types currently require a Port, so we do this for everything; consider making this per-type if we have future types // that do not require port. if to.Port == nil { // "Port is required when the referent is a Kubernetes Service." return nil, nil, &ConfigError{Reason: InvalidDestination, Message: "port is required in backendRef"} } return &istio.Destination{ Host: hostname, Port: &istio.PortSelector{Number: uint32(*to.Port)}, }, nil, invalidBackendErr } // https://github.com/kubernetes-sigs/gateway-api/blob/cea484e38e078a2c1997d8c7a62f410a1540f519/apis/v1beta1/httproute_types.go#L207-L212 func isInvalidBackend(err *ConfigError) bool { return err.Reason == InvalidDestinationPermit || err.Reason == InvalidDestinationNotFound || err.Reason == InvalidDestinationKind } func headerListToMap(hl []k8s.HTTPHeader) map[string]string { if len(hl) == 0 { return nil } res := map[string]string{} for _, e := range hl { k := strings.ToLower(string(e.Name)) if _, f := res[k]; f { // "Subsequent entries with an equivalent header name MUST be ignored" continue } res[k] = e.Value } return res } func createMirrorFilter(ctx RouteContext, filter *k8s.HTTPRequestMirrorFilter, ns string, enforceRefGrant bool, k config.GroupVersionKind, ) (*istio.HTTPMirrorPolicy, *ConfigError) { if filter == nil { return nil, nil } var weightOne int32 = 1 dst, _, err := buildDestination(ctx, k8s.BackendRef{ BackendObjectReference: filter.BackendRef, Weight: &weightOne, }, ns, enforceRefGrant, k) if err != nil { return nil, err } var percent *istio.Percent if f := filter.Fraction; f != nil { percent = &istio.Percent{Value: (100 * float64(f.Numerator)) / float64(ptr.OrDefault(f.Denominator, int32(100)))} } else if p := filter.Percent; p != nil { percent = &istio.Percent{Value: float64(*p)} } return &istio.HTTPMirrorPolicy{Destination: dst, Percentage: percent}, nil } func createRewriteFilter(filter *k8s.HTTPURLRewriteFilter) *istio.HTTPRewrite { if filter == nil { return nil } rewrite := &istio.HTTPRewrite{} if filter.Path != nil { switch filter.Path.Type { case k8s.PrefixMatchHTTPPathModifier: rewrite.Uri = strings.TrimSuffix(*filter.Path.ReplacePrefixMatch, "/") if rewrite.Uri == "" { // `/` means removing the prefix rewrite.Uri = "/" } case k8s.FullPathHTTPPathModifier: rewrite.UriRegexRewrite = &istio.RegexRewrite{ Match: "/.*", Rewrite: *filter.Path.ReplaceFullPath, } } } if filter.Hostname != nil { rewrite.Authority = string(*filter.Hostname) } // Nothing done if rewrite.Uri == "" && rewrite.UriRegexRewrite == nil && rewrite.Authority == "" { return nil } return rewrite } func createCorsFilter(filter *k8s.HTTPCORSFilter) *istio.CorsPolicy { if filter == nil { return nil } res := &istio.CorsPolicy{} for _, r := range filter.AllowOrigins { rs := string(r) if len(rs) == 0 { continue // Not valid anyways, but double check } // TODO: support wildcards (https://github.com/kubernetes-sigs/gateway-api/issues/3648) res.AllowOrigins = append(res.AllowOrigins, &istio.StringMatch{ MatchType: &istio.StringMatch_Exact{Exact: string(r)}, }) } if ptr.OrEmpty(filter.AllowCredentials) { res.AllowCredentials = wrappers.Bool(true) } for _, r := range filter.AllowMethods { res.AllowMethods = append(res.AllowMethods, string(r)) } for _, r := range filter.AllowHeaders { res.AllowHeaders = append(res.AllowHeaders, string(r)) } for _, r := range filter.ExposeHeaders { res.ExposeHeaders = append(res.ExposeHeaders, string(r)) } if filter.MaxAge > 0 { res.MaxAge = durationpb.New(time.Duration(filter.MaxAge) * time.Second) } return res } func createRedirectFilter(filter *k8s.HTTPRequestRedirectFilter) *istio.HTTPRedirect { if filter == nil { return nil } resp := &istio.HTTPRedirect{} if filter.StatusCode != nil { // Istio allows 301, 302, 303, 307, 308. // Gateway allows only 301 and 302. resp.RedirectCode = uint32(*filter.StatusCode) } if filter.Hostname != nil { resp.Authority = string(*filter.Hostname) } if filter.Scheme != nil { // Both allow http and https resp.Scheme = *filter.Scheme } if filter.Port != nil { resp.RedirectPort = &istio.HTTPRedirect_Port{Port: uint32(*filter.Port)} } else { // "When empty, port (if specified) of the request is used." // this differs from Istio default if filter.Scheme != nil { resp.RedirectPort = &istio.HTTPRedirect_DerivePort{DerivePort: istio.HTTPRedirect_FROM_PROTOCOL_DEFAULT} } else { resp.RedirectPort = &istio.HTTPRedirect_DerivePort{DerivePort: istio.HTTPRedirect_FROM_REQUEST_PORT} } } if filter.Path != nil { switch filter.Path.Type { case k8s.FullPathHTTPPathModifier: resp.Uri = *filter.Path.ReplaceFullPath case k8s.PrefixMatchHTTPPathModifier: resp.Uri = fmt.Sprintf("%%PREFIX()%%%s", *filter.Path.ReplacePrefixMatch) } } return resp } func createHeadersFilter(filter *k8s.HTTPHeaderFilter) *istio.Headers_HeaderOperations { if filter == nil { return nil } return &istio.Headers_HeaderOperations{ Add: headerListToMap(filter.Add), Remove: filter.Remove, Set: headerListToMap(filter.Set), } } // nolint: unparam func createMethodMatch(match k8s.HTTPRouteMatch) (*istio.StringMatch, *ConfigError) { if match.Method == nil { return nil, nil } return &istio.StringMatch{ MatchType: &istio.StringMatch_Exact{Exact: string(*match.Method)}, }, nil } func createQueryParamsMatch(match k8s.HTTPRouteMatch) (map[string]*istio.StringMatch, *ConfigError) { res := map[string]*istio.StringMatch{} for _, qp := range match.QueryParams { tp := k8s.QueryParamMatchExact if qp.Type != nil { tp = *qp.Type } switch tp { case k8s.QueryParamMatchExact: res[string(qp.Name)] = &istio.StringMatch{ MatchType: &istio.StringMatch_Exact{Exact: qp.Value}, } case k8s.QueryParamMatchRegularExpression: res[string(qp.Name)] = &istio.StringMatch{ MatchType: &istio.StringMatch_Regex{Regex: qp.Value}, } default: // Should never happen, unless a new field is added return nil, &ConfigError{Reason: InvalidConfiguration, Message: fmt.Sprintf("unknown type: %q is not supported QueryParams type", tp)} } } if len(res) == 0 { return nil, nil } return res, nil } func createHeadersMatch(match k8s.HTTPRouteMatch) (map[string]*istio.StringMatch, *ConfigError) { res := map[string]*istio.StringMatch{} for _, header := range match.Headers { tp := k8s.HeaderMatchExact if header.Type != nil { tp = *header.Type } switch tp { case k8s.HeaderMatchExact: res[string(header.Name)] = &istio.StringMatch{ MatchType: &istio.StringMatch_Exact{Exact: header.Value}, } case k8s.HeaderMatchRegularExpression: res[string(header.Name)] = &istio.StringMatch{ MatchType: &istio.StringMatch_Regex{Regex: header.Value}, } default: // Should never happen, unless a new field is added return nil, &ConfigError{Reason: InvalidConfiguration, Message: fmt.Sprintf("unknown type: %q is not supported HeaderMatch type", tp)} } } if len(res) == 0 { return nil, nil } return res, nil } func createGRPCHeadersMatch(match k8s.GRPCRouteMatch) (map[string]*istio.StringMatch, *ConfigError) { res := map[string]*istio.StringMatch{} for _, header := range match.Headers { tp := k8s.GRPCHeaderMatchExact if header.Type != nil { tp = *header.Type } switch tp { case k8s.GRPCHeaderMatchExact: res[string(header.Name)] = &istio.StringMatch{ MatchType: &istio.StringMatch_Exact{Exact: header.Value}, } case k8s.GRPCHeaderMatchRegularExpression: res[string(header.Name)] = &istio.StringMatch{ MatchType: &istio.StringMatch_Regex{Regex: header.Value}, } default: // Should never happen, unless a new field is added return nil, &ConfigError{Reason: InvalidConfiguration, Message: fmt.Sprintf("unknown type: %q is not supported HeaderMatch type", tp)} } } if len(res) == 0 { return nil, nil } return res, nil } func createURIMatch(match k8s.HTTPRouteMatch) (*istio.StringMatch, *ConfigError) { tp := k8s.PathMatchPathPrefix if match.Path.Type != nil { tp = *match.Path.Type } dest := "/" if match.Path.Value != nil { dest = *match.Path.Value } switch tp { case k8s.PathMatchPathPrefix: // "When specified, a trailing `/` is ignored." if dest != "/" { dest = strings.TrimSuffix(dest, "/") } return &istio.StringMatch{ MatchType: &istio.StringMatch_Prefix{Prefix: dest}, }, nil case k8s.PathMatchExact: return &istio.StringMatch{ MatchType: &istio.StringMatch_Exact{Exact: dest}, }, nil case k8s.PathMatchRegularExpression: return &istio.StringMatch{ MatchType: &istio.StringMatch_Regex{Regex: dest}, }, nil default: // Should never happen, unless a new field is added return nil, &ConfigError{Reason: InvalidConfiguration, Message: fmt.Sprintf("unknown type: %q is not supported Path match type", tp)} } } func createGRPCURIMatch(match k8s.GRPCRouteMatch) (*istio.StringMatch, *ConfigError) { m := match.Method if m == nil { return nil, nil } tp := k8s.GRPCMethodMatchExact if m.Type != nil { tp = *m.Type } if m.Method == nil && m.Service == nil { // Should never happen, invalid per spec return nil, &ConfigError{Reason: InvalidConfiguration, Message: "gRPC match must have method or service defined"} } // gRPC format is //. Since we don't natively understand this, convert to various string matches switch tp { case k8s.GRPCMethodMatchExact: if m.Method == nil { return &istio.StringMatch{ MatchType: &istio.StringMatch_Prefix{Prefix: fmt.Sprintf("/%s/", *m.Service)}, }, nil } if m.Service == nil { return &istio.StringMatch{ MatchType: &istio.StringMatch_Regex{Regex: fmt.Sprintf("/[^/]+/%s", *m.Method)}, }, nil } return &istio.StringMatch{ MatchType: &istio.StringMatch_Exact{Exact: fmt.Sprintf("/%s/%s", *m.Service, *m.Method)}, }, nil case k8s.GRPCMethodMatchRegularExpression: if m.Method == nil { return &istio.StringMatch{ MatchType: &istio.StringMatch_Regex{Regex: fmt.Sprintf("/%s/.+", *m.Service)}, }, nil } if m.Service == nil { return &istio.StringMatch{ MatchType: &istio.StringMatch_Regex{Regex: fmt.Sprintf("/[^/]+/%s", *m.Method)}, }, nil } return &istio.StringMatch{ MatchType: &istio.StringMatch_Regex{Regex: fmt.Sprintf("/%s/%s", *m.Service, *m.Method)}, }, nil default: // Should never happen, unless a new field is added return nil, &ConfigError{Reason: InvalidConfiguration, Message: fmt.Sprintf("unknown type: %q is not supported Path match type", tp)} } } // parentKey holds info about a parentRef (eg route binding to a Gateway). This is a mirror of // k8s.ParentReference in a form that can be stored in a map type parentKey struct { Kind config.GroupVersionKind // Name is the original name of the resource (eg Kubernetes Gateway name) Name string // Namespace is the namespace of the resource Namespace string } func (p parentKey) String() string { return p.Kind.String() + "/" + p.Namespace + "/" + p.Name } type parentReference struct { parentKey SectionName k8s.SectionName Port k8s.PortNumber } func (p parentReference) String() string { return p.parentKey.String() + "/" + string(p.SectionName) + "/" + fmt.Sprint(p.Port) } var meshGVK = config.GroupVersionKind{ Group: gvk.KubernetesGateway.Group, Version: gvk.KubernetesGateway.Version, Kind: "Mesh", } var meshParentKey = parentKey{ Kind: meshGVK, Name: "istio", } // parentInfo holds info about a "parent" - something that can be referenced as a ParentRef in the API. // Today, this is just Gateway and Mesh. type parentInfo struct { // InternalName refers to the internal name we can reference it by. For example, "mesh" or "my-ns/my-gateway" InternalName string // AllowedKinds indicates which kinds can be admitted by this parent AllowedKinds []k8s.RouteGroupKind // Hostnames is the hostnames that must be match to reference to the parent. For gateway this is listener hostname // Format is ns/hostname or just hostname, which is equivalent to */hostname Hostnames []string // OriginalHostname is the unprocessed form of Hostnames; how it appeared in users' config OriginalHostname string SectionName k8s.SectionName Port k8s.PortNumber Protocol k8s.ProtocolType } // routeParentReference holds information about a route's parent reference type routeParentReference struct { // InternalName refers to the internal name of the parent we can reference it by. For example, "mesh" or "my-ns/my-gateway" InternalName string // InternalKind is the Group/Kind of the parent InternalKind config.GroupVersionKind // DeniedReason, if present, indicates why the reference was not valid DeniedReason *ParentError // OriginalReference contains the original reference OriginalReference k8s.ParentReference // Hostname is the hostname match of the parent, if any Hostname string BannedHostnames sets.Set[string] ParentKey parentKey ParentSection k8s.SectionName // WaypointError, if present, indicates why the reference does not have valid configuration for generating a Waypoint WaypointError *WaypointError } func (r routeParentReference) IsMesh() bool { return r.InternalName == "mesh" } func (r routeParentReference) hostnameAllowedByIsolation(rawRouteHost string) bool { routeHost := host.Name(rawRouteHost) ourListener := host.Name(r.Hostname) if len(ourListener) > 0 && !ourListener.IsWildCarded() { // Short circuit: this logic only applies to wildcards // Not required for correctness, just an optimization return true } if len(ourListener) > 0 && !routeHost.Matches(ourListener) { return false } for checkListener := range r.BannedHostnames { // We have 3 hostnames here: // * routeHost, the hostname in the route entry // * ourListener, the hostname of the listener the route is bound to // * checkListener, the hostname of the other listener we are comparing to // We want to return false if checkListener would match the routeHost and it would be a more exact match if len(ourListener) > len(checkListener) { // If our hostname is longer, it must be more exact than the check continue } // Ours is shorter. If it matches the checkListener, then it should ONLY match that one // Note protocol, port, etc are already considered when we construct bannedHostnames if routeHost.SubsetOf(host.Name(checkListener)) { return false } } return true } func filteredReferences(parents []routeParentReference) []routeParentReference { ret := make([]routeParentReference, 0, len(parents)) for _, p := range parents { if p.DeniedReason != nil { // We should filter this out continue } ret = append(ret, p) } // To ensure deterministic order, sort them sort.Slice(ret, func(i, j int) bool { return ret[i].InternalName < ret[j].InternalName }) return ret } func getDefaultName(name string, kgw *k8s.GatewaySpec, disableNameSuffix bool) string { if disableNameSuffix { return name } return fmt.Sprintf("%v-%v", name, kgw.GatewayClassName) } // Gateway currently requires a listener (https://github.com/kubernetes-sigs/gateway-api/pull/1596). // We don't *really* care about the listener, but it may make sense to add a warning if users do not // configure it in an expected way so that we have consistency and can make changes in the future as needed. // We could completely reject but that seems more likely to cause pain. func unexpectedWaypointListener(l k8s.Listener) bool { if l.Port != 15008 { return true } if l.Protocol != k8s.ProtocolType(protocol.HBONE) { return true } return false } func unexpectedEastWestWaypointListener(l k8s.Listener) bool { if l.Port != 15008 { return true } if l.Protocol != k8s.ProtocolType(protocol.HBONE) { return true } if l.TLS == nil || *l.TLS.Mode != k8s.TLSModeTerminate { return true } // TODO: Should we check that there aren't more things set return false } func getListenerNames(spec *k8s.GatewaySpec) sets.Set[k8s.SectionName] { res := sets.New[k8s.SectionName]() for _, l := range spec.Listeners { res.Insert(l.Name) } return res } func reportGatewayStatus( r *GatewayContext, obj *k8sbeta.Gateway, gs *k8sbeta.GatewayStatus, gatewayServices []string, servers []*istio.Server, listenerSetCount int, gatewayErr *ConfigError, ) { // TODO: we lose address if servers is empty due to an error internal, external, pending, warnings, allUsable := r.ResolveGatewayInstances(obj.Namespace, gatewayServices, servers) // Setup initial conditions to the success state. If we encounter errors, we will update this. // We have two status // Accepted: is the configuration valid. We only have errors in listeners, and the status is not supposed to // be tied to listeners, so this is always accepted // Programmed: is the data plane "ready" (note: eventually consistent) gatewayConditions := map[string]*condition{ string(k8s.GatewayConditionAccepted): { reason: string(k8s.GatewayReasonAccepted), message: "Resource accepted", }, string(k8s.GatewayConditionProgrammed): { reason: string(k8s.GatewayReasonProgrammed), message: "Resource programmed", }, } if gatewayErr != nil { gatewayConditions[string(k8s.GatewayConditionAccepted)].error = gatewayErr } // Not defined in upstream API const AttachedListenerSets = "AttachedListenerSets" if obj.Spec.AllowedListeners != nil { gatewayConditions[AttachedListenerSets] = &condition{ reason: "ListenersAttached", message: "At least one ListenerSet is attached", } if !features.EnableAlphaGatewayAPI { gatewayConditions[AttachedListenerSets].error = &ConfigError{ Reason: "Unsupported", Message: fmt.Sprintf("AllowedListeners is configured, but ListenerSets are not enabled (set %v=true)", features.EnableAlphaGatewayAPIName), } } else if listenerSetCount == 0 { gatewayConditions[AttachedListenerSets].error = &ConfigError{ Reason: "NoListenersAttached", Message: "AllowedListeners is configured, but no ListenerSets are attached", } } } setProgrammedCondition(gatewayConditions, internal, gatewayServices, warnings, allUsable) addressesToReport := external addrType := k8s.IPAddressType if len(addressesToReport) == 0 { addrType = k8s.HostnameAddressType for _, hostport := range internal { svchost, _, _ := net.SplitHostPort(hostport) if !slices.Contains(pending, svchost) && !slices.Contains(addressesToReport, svchost) { addressesToReport = append(addressesToReport, svchost) } } } gs.Addresses = make([]k8s.GatewayStatusAddress, 0, len(addressesToReport)) for _, addr := range addressesToReport { gs.Addresses = append(gs.Addresses, k8s.GatewayStatusAddress{ Value: addr, Type: &addrType, }) } // Prune listeners that have been removed haveListeners := getListenerNames(&obj.Spec) listeners := make([]k8s.ListenerStatus, 0, len(gs.Listeners)) for _, l := range gs.Listeners { if haveListeners.Contains(l.Name) { haveListeners.Delete(l.Name) listeners = append(listeners, l) } } gs.Listeners = listeners gs.Conditions = setConditions(obj.Generation, gs.Conditions, gatewayConditions) } func reportListenerSetStatus( r *GatewayContext, parentGwObj *k8sbeta.Gateway, obj *gatewayx.XListenerSet, gs *gatewayx.ListenerSetStatus, gatewayServices []string, servers []*istio.Server, gatewayErr *ConfigError, ) { internal, _, _, warnings, allUsable := r.ResolveGatewayInstances(parentGwObj.Namespace, gatewayServices, servers) // Setup initial conditions to the success state. If we encounter errors, we will update this. // We have two status // Accepted: is the configuration valid. We only have errors in listeners, and the status is not supposed to // be tied to listeners, so this is always accepted // Programmed: is the data plane "ready" (note: eventually consistent) gatewayConditions := map[string]*condition{ string(k8s.GatewayConditionAccepted): { reason: string(k8s.GatewayReasonAccepted), message: "Resource accepted", }, string(k8s.GatewayConditionProgrammed): { reason: string(k8s.GatewayReasonProgrammed), message: "Resource programmed", }, } if gatewayErr != nil { gatewayErr.Message = "Parent not accepted: " + gatewayErr.Message gatewayConditions[string(k8s.GatewayConditionAccepted)].error = gatewayErr } setProgrammedCondition(gatewayConditions, internal, gatewayServices, warnings, allUsable) gs.Conditions = setConditions(obj.Generation, gs.Conditions, gatewayConditions) } func setProgrammedCondition(gatewayConditions map[string]*condition, internal []string, gatewayServices []string, warnings []string, allUsable bool) { if len(internal) > 0 { msg := fmt.Sprintf("Resource programmed, assigned to service(s) %s", humanReadableJoin(internal)) gatewayConditions[string(k8s.GatewayConditionProgrammed)].message = msg } if len(gatewayServices) == 0 { gatewayConditions[string(k8s.GatewayConditionProgrammed)].error = &ConfigError{ Reason: InvalidAddress, Message: "Failed to assign to any requested addresses", } } else if len(warnings) > 0 { // Start - Updated by Higress var msg string //var reason string if len(internal) != 0 { msg = fmt.Sprintf("Assigned to service(s) %s, but failed to assign to all requested addresses: %s", humanReadableJoin(internal), strings.Join(warnings, "; ")) } else { msg = fmt.Sprintf("Failed to assign to any requested addresses: %s", strings.Join(warnings, "; ")) } // //if allUsable { // reason = string(k8s.GatewayReasonAddressNotAssigned) //} else { // reason = string(k8s.GatewayReasonAddressNotUsable) //} // End - Updated by Higress gatewayConditions[string(k8s.GatewayConditionProgrammed)].error = &ConfigError{ // TODO: this only checks Service ready, we should also check Deployment ready? Reason: string(k8s.GatewayReasonInvalid), Message: msg, } } } // reportUnmanagedGatewayStatus reports a status message for an unmanaged gateway. // For these gateways, we don't deploy them. However, all gateways ought to have a status message, even if its basically // just to say something read it func reportUnmanagedGatewayStatus( status *k8sbeta.GatewayStatus, obj *k8sbeta.Gateway, ) { gatewayConditions := map[string]*condition{ string(k8s.GatewayConditionAccepted): { reason: string(k8s.GatewayReasonAccepted), message: "Resource accepted", }, string(k8s.GatewayConditionProgrammed): { reason: string(k8s.GatewayReasonProgrammed), // Set to true anyway since this is basically declaring it as valid message: "This Gateway is remote; Istio will not program it", }, } status.Addresses = slices.Map(obj.Spec.Addresses, func(e k8s.GatewaySpecAddress) k8s.GatewayStatusAddress { return k8s.GatewayStatusAddress(e) }) status.Listeners = nil status.Conditions = setConditions(obj.Generation, status.Conditions, gatewayConditions) } // reportUnsupportedListenerSet reports a status message for a ListenerSet that is not supported func reportUnsupportedListenerSet(class string, status *gatewayx.ListenerSetStatus, obj *gatewayx.XListenerSet) { gatewayConditions := map[string]*condition{ string(k8s.GatewayConditionAccepted): { reason: string(k8s.GatewayReasonAccepted), error: &ConfigError{ Reason: string(gatewayx.ListenerSetReasonNotAllowed), Message: fmt.Sprintf("The %q GatewayClass does not support ListenerSet", class), }, }, string(k8s.GatewayConditionProgrammed): { reason: string(k8s.GatewayReasonProgrammed), error: &ConfigError{ Reason: string(gatewayx.ListenerSetReasonNotAllowed), Message: fmt.Sprintf("The %q GatewayClass does not support ListenerSet", class), }, }, } status.Listeners = nil status.Conditions = setConditions(obj.Generation, status.Conditions, gatewayConditions) } // reportNotAllowedListenerSet reports a status message for a ListenerSet that is not allowed to be selected func reportNotAllowedListenerSet(status *gatewayx.ListenerSetStatus, obj *gatewayx.XListenerSet) { gatewayConditions := map[string]*condition{ string(k8s.GatewayConditionAccepted): { reason: string(k8s.GatewayReasonAccepted), error: &ConfigError{ Reason: string(gatewayx.ListenerSetReasonNotAllowed), Message: "The parent Gateway does not allow this reference; check the 'spec.allowedRoutes'", }, }, string(k8s.GatewayConditionProgrammed): { reason: string(k8s.GatewayReasonProgrammed), error: &ConfigError{ Reason: string(gatewayx.ListenerSetReasonNotAllowed), Message: "The parent Gateway does not allow this reference; check the 'spec.allowedRoutes'", }, }, } status.Listeners = nil status.Conditions = setConditions(obj.Generation, status.Conditions, gatewayConditions) } // IsManaged checks if a Gateway is managed (ie we create the Deployment and Service) or unmanaged. // This is based on the address field of the spec. If address is set with a Hostname type, it should point to an existing // 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. // If there is an IP, we will set the `loadBalancerIP` type. // 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. // So far, this mirrors how out of clusters work (address set means to use existing IP, unset means to provision one), // and there has been growing consensus on this model for in cluster deployments. // // Currently, the supported options are: // * 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. // * 1 IP address. This is managed, with IP explicit // * Nothing. This is managed, with IP auto assigned // // Not supported: // Multiple hostname/IP - It is feasible but preference is to create multiple Gateways. This would also break the 1:1 mapping of GW:Service // Mixed hostname and IP - doesn't make sense; user should define the IP in service // NamedAddress - Service has no concept of named address. For cloud's that have named addresses they can be configured by annotations, // // which users can add to the Gateway. // // If manual deployments are disabled, IsManaged() always returns true. func IsManaged(gw *k8s.GatewaySpec) bool { if !features.EnableGatewayAPIManualDeployment { return true } if len(gw.Addresses) == 0 { return true } if len(gw.Addresses) > 1 { return false } if t := gw.Addresses[0].Type; t == nil || *t == k8s.IPAddressType { return true } return false } // Start - Added by Higress // UseDefaultService checks if a Gateway shall be bound to the default gateway service // This is based on the addresses field of the spec // If addresses field contains any item with a Hostname type, it should point to the existing // Services that handles the gateway traffic // If it is not set, or all items refer to only a single IP, we will consider it pointed to the default data plane service. // 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. func UseDefaultService(gw *k8s.GatewaySpec) bool { if len(gw.Addresses) == 0 { return true } for _, addr := range gw.Addresses { if t := addr.Type; t == nil || *t == k8s.HostnameAddressType { return false } } return true } // End - Added by Higress func extractGatewayServices(domainSuffix string, kgw *k8sbeta.Gateway, info classInfo) ([]string, bool, *ConfigError) { // Start - Updated by Higress if UseDefaultService(&kgw.Spec) { // name := model.GetOrDefault(obj.Annotations[gatewayNameOverride], getDefaultName(obj.Name, kgw)) // return []string{fmt.Sprintf("%s.%s.svc.%v", name, obj.Namespace, r.Domain)}, true, nil name := kgw.Annotations[gatewayNameOverride] if len(name) > 0 { return []string{fmt.Sprintf("%s.%s.svc.%v", name, kgw.Namespace, domainSuffix)}, false, nil } return []string{fmt.Sprintf("%s.%s.svc.%s", higressconfig.GatewayName, higressconfig.PodNamespace, util.GetDomainSuffix())}, true, nil } gatewayServices := []string{} skippedAddresses := []string{} for _, addr := range kgw.Spec.Addresses { if addr.Type != nil && *addr.Type != k8s.HostnameAddressType { // We only support HostnameAddressType. Keep track of invalid ones so we can report in status. skippedAddresses = append(skippedAddresses, addr.Value) continue } // TODO: For now we are using Addresses. There has been some discussion of allowing inline // parameters on the class field like a URL, in which case we will probably just use that. See // https://github.com/kubernetes-sigs/gateway-api/pull/614 fqdn := addr.Value if !strings.Contains(fqdn, ".") { // Short name, expand it fqdn = fmt.Sprintf("%s.%s.svc.%s", fqdn, kgw.Namespace, domainSuffix) } gatewayServices = append(gatewayServices, fqdn) } if len(skippedAddresses) > 0 { // Give error but return services, this is a soft failure return gatewayServices, false, &ConfigError{ Reason: InvalidAddress, Message: fmt.Sprintf("only Hostname is supported, ignoring %v", skippedAddresses), } } if _, f := kgw.Annotations[serviceTypeOverride]; f { // Give error but return services, this is a soft failure // Remove entirely in 1.20 return gatewayServices, false, &ConfigError{ Reason: DeprecateFieldUsage, Message: fmt.Sprintf("annotation %v is deprecated, use Spec.Infrastructure.Routeability", serviceTypeOverride), } } return gatewayServices, false, nil } func buildListener( ctx krt.HandlerContext, configMaps krt.Collection[*corev1.ConfigMap], secrets krt.Collection[*corev1.Secret], grants ReferenceGrants, namespaces krt.Collection[*corev1.Namespace], obj controllers.Object, status []k8s.ListenerStatus, gw k8s.GatewaySpec, l k8s.Listener, listenerIndex int, controllerName k8s.GatewayController, portErr error, ) (*istio.Server, []k8s.ListenerStatus, bool) { listenerConditions := map[string]*condition{ string(k8s.ListenerConditionAccepted): { reason: string(k8s.ListenerReasonAccepted), message: "No errors found", }, string(k8s.ListenerConditionProgrammed): { reason: string(k8s.ListenerReasonProgrammed), message: "No errors found", }, string(k8s.ListenerConditionConflicted): { reason: string(k8s.ListenerReasonNoConflicts), message: "No errors found", status: kstatus.StatusFalse, }, string(k8s.ListenerConditionResolvedRefs): { reason: string(k8s.ListenerReasonResolvedRefs), message: "No errors found", }, } ok := true tls, err := buildTLS(ctx, configMaps, secrets, grants, resolveGatewayTLS(l.Port, gw.TLS), l.TLS, obj, kube.IsAutoPassthrough(obj.GetLabels(), l)) if err != nil { listenerConditions[string(k8s.ListenerConditionResolvedRefs)].error = err listenerConditions[string(k8s.GatewayConditionProgrammed)].error = &ConfigError{ Reason: string(k8s.GatewayReasonInvalid), Message: "Bad TLS configuration", } ok = false } hostnames := buildHostnameMatch(ctx, obj.GetNamespace(), namespaces, l) if portErr != nil { listenerConditions[string(k8s.ListenerConditionAccepted)].error = &ConfigError{ Reason: string(k8s.ListenerReasonUnsupportedProtocol), Message: portErr.Error(), } ok = false } protocol, perr := listenerProtocolToIstio(controllerName, l.Protocol) if perr != nil { listenerConditions[string(k8s.ListenerConditionAccepted)].error = &ConfigError{ Reason: string(k8s.ListenerReasonUnsupportedProtocol), Message: perr.Error(), } ok = false } if controllerName == constants.ManagedGatewayMeshController { if unexpectedWaypointListener(l) { listenerConditions[string(k8s.ListenerConditionAccepted)].error = &ConfigError{ Reason: string(k8s.ListenerReasonUnsupportedProtocol), Message: `Expected a single listener on port 15008 with protocol "HBONE"`, } } } if controllerName == constants.ManagedGatewayEastWestController { if unexpectedEastWestWaypointListener(l) { listenerConditions[string(k8s.ListenerConditionAccepted)].error = &ConfigError{ Reason: string(k8s.ListenerReasonUnsupportedProtocol), Message: `Expected a single listener on port 15008 with protocol "HBONE" and TLS.Mode == Terminate`, } } } server := &istio.Server{ Port: &istio.Port{ // Name is required. We only have one server per Gateway, so we can just name them all the same Name: "default", Number: uint32(l.Port), Protocol: protocol, }, Hosts: hostnames, Tls: tls, } updatedStatus := reportListenerCondition(listenerIndex, l, obj, status, listenerConditions) return server, updatedStatus, ok } var supportedProtocols = sets.New( k8s.HTTPProtocolType, k8s.HTTPSProtocolType, k8s.TLSProtocolType, k8s.TCPProtocolType, k8s.ProtocolType(protocol.HBONE)) func listenerProtocolToIstio(name k8s.GatewayController, p k8s.ProtocolType) (string, error) { switch p { // Standard protocol types case k8s.HTTPProtocolType: return string(p), nil case k8s.HTTPSProtocolType: return string(p), nil case k8s.TLSProtocolType, k8s.TCPProtocolType: if !features.EnableAlphaGatewayAPI { return "", fmt.Errorf("protocol %q is supported, but only when %v=true is configured", p, features.EnableAlphaGatewayAPIName) } return string(p), nil // Our own custom types case k8s.ProtocolType(protocol.HBONE): if name != constants.ManagedGatewayMeshController && name != constants.ManagedGatewayEastWestController { return "", fmt.Errorf("protocol %q is only supported for waypoint proxies", p) } return string(p), nil } up := k8s.ProtocolType(strings.ToUpper(string(p))) if supportedProtocols.Contains(up) { return "", fmt.Errorf("protocol %q is unsupported. hint: %q (uppercase) may be supported", p, up) } // Note: the k8s.UDPProtocolType is explicitly left to hit this path return "", fmt.Errorf("protocol %q is unsupported", p) } func resolveGatewayTLS(port k8s.PortNumber, gw *k8s.GatewayTLSConfig) *k8s.TLSConfig { if gw == nil || gw.Frontend == nil { return nil } f := gw.Frontend pp := slices.FindFunc(f.PerPort, func(portConfig k8s.TLSPortConfig) bool { return portConfig.Port == port }) if pp != nil { return &pp.TLS } return &f.Default } func buildTLS( ctx krt.HandlerContext, configMaps krt.Collection[*corev1.ConfigMap], secrets krt.Collection[*corev1.Secret], grants ReferenceGrants, gatewayTLS *k8s.TLSConfig, tls *k8s.ListenerTLSConfig, gw controllers.Object, isAutoPassthrough bool, ) (*istio.ServerTLSSettings, *ConfigError) { if tls == nil { return nil, nil } // Explicitly not supported: file mounted // Not yet implemented: TLS mode, https redirect, max protocol version, SANs, CipherSuites, VerifyCertificate out := &istio.ServerTLSSettings{ HttpsRedirect: false, } mode := k8s.TLSModeTerminate if tls.Mode != nil { mode = *tls.Mode } namespace := gw.GetNamespace() switch mode { case k8s.TLSModeTerminate: out.Mode = istio.ServerTLSSettings_SIMPLE if tls.Options != nil { switch tls.Options[gatewayTLSTerminateModeKey] { case "MUTUAL": out.Mode = istio.ServerTLSSettings_MUTUAL case "OPTIONAL_MUTUAL": out.Mode = istio.ServerTLSSettings_OPTIONAL_MUTUAL case "ISTIO_SIMPLE": // Simple TLS but with builtin workload certificate. // equivalent to `credentialName: builtin:// out.Mode = istio.ServerTLSSettings_SIMPLE out.CredentialName = creds.BuiltinGatewaySecretTypeURI return out, nil case "ISTIO_MUTUAL": out.Mode = istio.ServerTLSSettings_ISTIO_MUTUAL return out, nil } } if len(tls.CertificateRefs) > 2 { return out, &ConfigError{ Reason: InvalidTLS, Message: "TLS mode can only support up to 2 server certificates", } } credNames := make([]string, len(tls.CertificateRefs)) validCertCount := 0 var combinedErr *ConfigError for i, certRef := range tls.CertificateRefs { cred, err := buildSecretReference(ctx, certRef, gw, secrets) if err != nil { combinedErr = joinErrors(combinedErr, err) continue } credNs := ptr.OrDefault((*string)(certRef.Namespace), namespace) sameNamespace := credNs == namespace objectKind := schematypes.GvkFromObject(gw) if !sameNamespace && !grants.SecretAllowed(ctx, objectKind, creds.ToResourceName(cred), namespace) { combinedErr = joinErrors(combinedErr, &ConfigError{ Reason: InvalidListenerRefNotPermitted, Message: fmt.Sprintf( "certificateRef %v/%v not accessible to a Gateway in namespace %q (missing a ReferenceGrant?)", certRef.Name, credNs, namespace, ), }) continue } credNames[i] = cred validCertCount++ } if validCertCount == 0 { // If we have no valid certificates, return an error return out, combinedErr } if validCertCount == 1 { out.CredentialName = credNames[0] } else { out.CredentialNames = credNames } if gatewayTLS != nil && gatewayTLS.Validation != nil && len(gatewayTLS.Validation.CACertificateRefs) > 0 { // TODO: add 'Mode' if len(gatewayTLS.Validation.CACertificateRefs) > 1 { return out, &ConfigError{ Reason: InvalidTLS, Message: "only one caCertificateRef is supported", } } caCertRef := gatewayTLS.Validation.CACertificateRefs[0] cred, err := buildCaCertificateReference(ctx, caCertRef, gw, configMaps, secrets) if err != nil { return out, err } if cred.Namespace != namespace && !grants.SecretAllowed(ctx, schematypes.GvkFromObject(gw), cred.ResourceName, namespace) { return out, &ConfigError{ Reason: InvalidListenerRefNotPermitted, Message: fmt.Sprintf( "caCertificateRef %v/%v not accessible to a Gateway in namespace %q (missing a ReferenceGrant?)", cred.Namespace, caCertRef.Name, namespace, ), } } out.Mode = istio.ServerTLSSettings_MUTUAL //out.CaCertCredentialName = cred.ResourceName } case k8s.TLSModePassthrough: out.Mode = istio.ServerTLSSettings_PASSTHROUGH if isAutoPassthrough { out.Mode = istio.ServerTLSSettings_AUTO_PASSTHROUGH } } return out, nil } func buildSecretReference( ctx krt.HandlerContext, ref k8s.SecretObjectReference, gw controllers.Object, secrets krt.Collection[*corev1.Secret], ) (string, *ConfigError) { if normalizeReference(ref.Group, ref.Kind, gvk.Secret) != gvk.Secret { return "", &ConfigError{Reason: InvalidTLS, Message: fmt.Sprintf("invalid certificate reference %v, only secret is allowed", secretObjectReferenceString(ref))} } secret := model.ConfigKey{ Kind: kind.Secret, Name: string(ref.Name), Namespace: ptr.OrDefault((*string)(ref.Namespace), gw.GetNamespace()), } key := secret.Namespace + "/" + secret.Name scrt := ptr.Flatten(krt.FetchOne(ctx, secrets, krt.FilterKey(key))) if scrt == nil { return "", &ConfigError{ Reason: InvalidTLS, Message: fmt.Sprintf("invalid certificate reference %v, secret %v not found", secretObjectReferenceString(ref), key), } } certInfo, err := kubecreds.ExtractCertInfo(scrt) if err != nil { return "", &ConfigError{ Reason: InvalidTLS, Message: fmt.Sprintf("invalid certificate reference %v, %v", secretObjectReferenceString(ref), err), } } if _, err = tls.X509KeyPair(certInfo.Cert, certInfo.Key); err != nil { return "", &ConfigError{ Reason: InvalidTLS, Message: fmt.Sprintf("invalid certificate reference %v, the certificate is malformed: %v", secretObjectReferenceString(ref), err), } } return creds.ToKubernetesGatewayResource(secret.Namespace, secret.Name), nil } func buildCaCertificateReference( ctx krt.HandlerContext, ref k8s.ObjectReference, gw controllers.Object, configMaps krt.Collection[*corev1.ConfigMap], secrets krt.Collection[*corev1.Secret], ) (*creds.SecretResource, *ConfigError) { var resourceType string var resourceKind kind.Kind var certInfo *credentials.CertInfo var certInfoErr error namespace := ptr.OrDefault((*string)(ref.Namespace), gw.GetNamespace()) name := string(ref.Name) switch normalizeReference(&ref.Group, &ref.Kind, config.GroupVersionKind{}) { case gvk.ConfigMap: resourceType = creds.KubernetesConfigMapType resourceKind = kind.ConfigMap key := namespace + "/" + name cm := ptr.Flatten(krt.FetchOne(ctx, configMaps, krt.FilterKey(key))) if cm == nil { return nil, &ConfigError{ Reason: InvalidTLS, Message: fmt.Sprintf("invalid CA certificate reference %v, configmap %v not found", objectReferenceString(ref), key), } } certInfo, certInfoErr = kubecreds.ExtractRootFromString(cm.Data) case gvk.Secret: resourceType = creds.KubernetesGatewaySecretType resourceKind = kind.Secret key := namespace + "/" + name scrt := ptr.Flatten(krt.FetchOne(ctx, secrets, krt.FilterKey(key))) if scrt == nil { return nil, &ConfigError{ Reason: InvalidTLS, Message: fmt.Sprintf("invalid CA certificate reference %v, secret %v not found", objectReferenceString(ref), key), } } certInfo, certInfoErr = kubecreds.ExtractRoot(scrt.Data) default: return nil, &ConfigError{ Reason: InvalidTLS, Message: fmt.Sprintf("invalid CA certificate reference %v, only secret and configmap are allowed", objectReferenceString(ref)), } } if certInfoErr != nil { return nil, &ConfigError{ Reason: InvalidTLS, Message: fmt.Sprintf("invalid CA certificate reference %v, %v", objectReferenceString(ref), certInfoErr), } } if !x509.NewCertPool().AppendCertsFromPEM(certInfo.Cert) { return nil, &ConfigError{ Reason: InvalidTLS, Message: fmt.Sprintf("invalid CA certificate reference %v, the bundle is malformed", objectReferenceString(ref)), } } log.Warnf("buildCaCertificateReference %s://%s/%s%s", resourceType, namespace, ref.Name, creds.SdsCaSuffix) return &creds.SecretResource{ ResourceType: resourceType, ResourceKind: resourceKind, Name: name + creds.SdsCaSuffix, Namespace: namespace, ResourceName: fmt.Sprintf("%s://%s/%s%s", resourceType, namespace, ref.Name, creds.SdsCaSuffix), Cluster: "", }, nil } func objectReferenceString(ref k8s.ObjectReference) string { return fmt.Sprintf("%s/%s/%s.%s", ref.Group, ref.Kind, ref.Name, ptr.OrEmpty(ref.Namespace)) } func secretObjectReferenceString(ref k8s.SecretObjectReference) string { return fmt.Sprintf("%s/%s/%s.%s", ptr.OrEmpty(ref.Group), ptr.OrEmpty(ref.Kind), ref.Name, ptr.OrEmpty(ref.Namespace)) } func parentRefString(ref k8s.ParentReference, objectNamespace string) string { return fmt.Sprintf("%s/%s/%s/%s/%d.%s", defaultString(ref.Group, gvk.KubernetesGateway.Group), defaultString(ref.Kind, gvk.KubernetesGateway.Kind), ref.Name, ptr.OrEmpty(ref.SectionName), ptr.OrEmpty(ref.Port), defaultString(ref.Namespace, objectNamespace)) } // buildHostnameMatch generates a Gateway.spec.servers.hosts section from a listener func buildHostnameMatch(ctx krt.HandlerContext, localNamespace string, namespaces krt.Collection[*corev1.Namespace], l k8s.Listener) []string { // We may allow all hostnames or a specific one hostname := "*" if l.Hostname != nil { hostname = string(*l.Hostname) } resp := []string{} for _, ns := range namespacesFromSelector(ctx, localNamespace, namespaces, l.AllowedRoutes) { // This check is necessary to prevent adding a hostname with an invalid empty namespace if len(ns) > 0 { resp = append(resp, fmt.Sprintf("%s/%s", ns, hostname)) } } // If nothing matched use ~ namespace (match nothing). We need this since its illegal to have an // empty hostname list, but we still need the Gateway provisioned to ensure status is properly set and // SNI matches are established; we just don't want to actually match any routing rules (yet). if len(resp) == 0 { return []string{"~/" + hostname} } return resp } // namespacesFromSelector determines a list of allowed namespaces for a given AllowedRoutes func namespacesFromSelector(ctx krt.HandlerContext, localNamespace string, namespaceCol krt.Collection[*corev1.Namespace], lr *k8s.AllowedRoutes) []string { // Default is to allow only the same namespace if lr == nil || lr.Namespaces == nil || lr.Namespaces.From == nil || *lr.Namespaces.From == k8s.NamespacesFromSame { return []string{localNamespace} } if *lr.Namespaces.From == k8s.NamespacesFromAll { return []string{"*"} } if lr.Namespaces.Selector == nil { // Should never happen, invalid config return []string{"*"} } // gateway-api has selectors, but Istio Gateway just has a list of names. We will run the selector // against all namespaces and get a list of matching namespaces that can be converted into a list // Istio can handle. ls, err := metav1.LabelSelectorAsSelector(lr.Namespaces.Selector) if err != nil { return nil } namespaces := []string{} namespaceObjects := krt.Fetch(ctx, namespaceCol) for _, ns := range namespaceObjects { if ls.Matches(toNamespaceSet(ns.Name, ns.Labels)) { namespaces = append(namespaces, ns.Name) } } // Ensure stable order sort.Strings(namespaces) return namespaces } // namespaceAcceptedByAllowListeners determines a list of allowed namespaces for a given AllowedListener func namespaceAcceptedByAllowListeners(localNamespace string, parent *k8sbeta.Gateway, lookupNamespace func(string) *corev1.Namespace) bool { lr := parent.Spec.AllowedListeners // Default allows none if lr == nil || lr.Namespaces == nil { return false } n := *lr.Namespaces if n.From != nil { switch *n.From { case k8s.NamespacesFromAll: return true case k8s.NamespacesFromSame: return localNamespace == parent.Namespace case k8s.NamespacesFromNone: return false default: // Unknown? return false } } if lr.Namespaces.Selector == nil { // Should never happen, invalid config return false } ls, err := metav1.LabelSelectorAsSelector(lr.Namespaces.Selector) if err != nil { return false } localNamespaceObject := lookupNamespace(localNamespace) if localNamespaceObject == nil { // Couldn't find the namespace return false } return ls.Matches(toNamespaceSet(localNamespaceObject.Name, localNamespaceObject.Labels)) } func humanReadableJoin(ss []string) string { switch len(ss) { case 0: return "" case 1: return ss[0] case 2: return ss[0] + " and " + ss[1] default: return strings.Join(ss[:len(ss)-1], ", ") + ", and " + ss[len(ss)-1] } } // NamespaceNameLabel represents that label added automatically to namespaces is newer Kubernetes clusters const NamespaceNameLabel = "kubernetes.io/metadata.name" // toNamespaceSet converts a set of namespace labels to a Set that can be used to select against. func toNamespaceSet(name string, labels map[string]string) klabels.Set { // If namespace label is not set, implicitly insert it to support older Kubernetes versions if labels[NamespaceNameLabel] == name { // Already set, avoid copies return labels } // First we need a copy to not modify the underlying object ret := make(map[string]string, len(labels)+1) for k, v := range labels { ret[k] = v } ret[NamespaceNameLabel] = name return ret } func GetCommonRouteInfo(spec any) ([]k8s.ParentReference, []k8s.Hostname, config.GroupVersionKind) { switch t := spec.(type) { case *k8salpha.TCPRoute: return t.Spec.ParentRefs, nil, gvk.TCPRoute case *k8salpha.TLSRoute: return t.Spec.ParentRefs, t.Spec.Hostnames, gvk.TLSRoute case *k8sbeta.HTTPRoute: return t.Spec.ParentRefs, t.Spec.Hostnames, gvk.HTTPRoute case *k8s.GRPCRoute: return t.Spec.ParentRefs, t.Spec.Hostnames, gvk.GRPCRoute default: log.Fatalf("unknown type %T", t) return nil, nil, config.GroupVersionKind{} } } func GetCommonRouteStateParents(spec any) []k8s.RouteParentStatus { switch t := spec.(type) { case *k8salpha.TCPRoute: return t.Status.Parents case *k8salpha.TLSRoute: return t.Status.Parents case *k8sbeta.HTTPRoute: return t.Status.Parents case *k8s.GRPCRoute: return t.Status.Parents default: log.Fatalf("unknown type %T", t) return nil } } // normalizeReference takes a generic Group/Kind (the API uses a few variations) and converts to a known GroupVersionKind. // Defaults for the group/kind are also passed. func normalizeReference[G ~string, K ~string](group *G, kind *K, def config.GroupVersionKind) config.GroupVersionKind { k := def.Kind if kind != nil { k = string(*kind) } g := def.Group if group != nil { g = string(*group) } gk := config.GroupVersionKind{ Group: g, Kind: k, } s, f := collections.All.FindByGroupKind(gk) if f { return s.GroupVersionKind() } return gk } func defaultString[T ~string](s *T, def string) string { if s == nil { return def } return string(*s) } func toRouteKind(g config.GroupVersionKind) k8s.RouteGroupKind { return k8s.RouteGroupKind{Group: (*k8s.Group)(&g.Group), Kind: k8s.Kind(g.Kind)} } func routeGroupKindEqual(rgk1, rgk2 k8s.RouteGroupKind) bool { return rgk1.Kind == rgk2.Kind && getGroup(rgk1) == getGroup(rgk2) } func getGroup(rgk k8s.RouteGroupKind) k8s.Group { return ptr.OrDefault(rgk.Group, k8s.Group(gvk.KubernetesGateway.Group)) } func GetStatus[I, IS any](spec I) IS { switch t := any(spec).(type) { case *k8salpha.TCPRoute: return any(t.Status).(IS) case *k8salpha.TLSRoute: return any(t.Status).(IS) case *k8sbeta.HTTPRoute: return any(t.Status).(IS) case *k8s.GRPCRoute: return any(t.Status).(IS) case *k8sbeta.Gateway: return any(t.Status).(IS) case *k8sbeta.GatewayClass: return any(t.Status).(IS) case *gatewayx.XBackendTrafficPolicy: return any(t.Status).(IS) case *k8s.BackendTLSPolicy: return any(t.Status).(IS) case *gatewayx.XListenerSet: return any(t.Status).(IS) case *inferencev1.InferencePool: return any(t.Status).(IS) default: log.Fatalf("unknown type %T", t) return ptr.Empty[IS]() } } func GetBackendRef[I any](spec I) (config.GroupVersionKind, *k8s.Namespace, k8s.ObjectName) { switch t := any(spec).(type) { case k8s.HTTPBackendRef: return normalizeReference(t.Group, t.Kind, gvk.Service), t.Namespace, t.Name case k8s.GRPCBackendRef: return normalizeReference(t.Group, t.Kind, gvk.Service), t.Namespace, t.Name case k8s.BackendRef: return normalizeReference(t.Group, t.Kind, gvk.Service), t.Namespace, t.Name default: log.Fatalf("unknown GetBackendRef type %T", t) return config.GroupVersionKind{}, nil, "" } } // Start - Added by Higress // isCatchAll returns true if HTTPMatchRequest is a catchall match otherwise // false. Note - this may not be exactly "catch all" as we don't know the full // class of possible inputs As such, this is used only for optimization. func isCatchAllMatch(m *istio.HTTPMatchRequest) bool { catchall := false if m.Uri != nil { switch m := m.Uri.MatchType.(type) { case *istio.StringMatch_Prefix: catchall = m.Prefix == "/" case *istio.StringMatch_Regex: catchall = m.Regex == "*" } } // A Match is catch all if and only if it has no match set // and URI has a prefix / or regex *. return catchall && len(m.Headers) == 0 && len(m.QueryParams) == 0 && len(m.SourceLabels) == 0 && len(m.WithoutHeaders) == 0 && len(m.Gateways) == 0 && m.Method == nil && m.Scheme == nil && m.Port == 0 && m.Authority == nil && m.SourceNamespace == "" } func equal(have *string, expected string) bool { return have != nil && *have == expected } func nilOrEqual(have *string, expected string) bool { return have == nil || *have == expected } func generateRouteName(obj config.Namer, routeType string) string { routeName := obj.GetName() if obj.GetNamespace() != higressconfig.PodNamespace { routeName = path.Join(obj.GetNamespace(), obj.GetName()) } if routeType != "HTTP" { routeName = path.Join(routeType, routeName) } return routeName } // End - Added by Higress