| // 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 route |
| |
| import ( |
| "fmt" |
| "regexp" |
| "sort" |
| "strconv" |
| "strings" |
| ) |
| |
| import ( |
| core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" |
| route "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" |
| xdsfault "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/common/fault/v3" |
| xdshttpfault "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/fault/v3" |
| matcher "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3" |
| xdstype "github.com/envoyproxy/go-control-plane/envoy/type/v3" |
| "github.com/envoyproxy/go-control-plane/pkg/wellknown" |
| any "google.golang.org/protobuf/types/known/anypb" |
| "google.golang.org/protobuf/types/known/durationpb" |
| wrappers "google.golang.org/protobuf/types/known/wrapperspb" |
| meshconfig "istio.io/api/mesh/v1alpha1" |
| networking "istio.io/api/networking/v1alpha3" |
| "istio.io/pkg/log" |
| ) |
| |
| import ( |
| "github.com/apache/dubbo-go-pixiu/pilot/pkg/features" |
| "github.com/apache/dubbo-go-pixiu/pilot/pkg/model" |
| "github.com/apache/dubbo-go-pixiu/pilot/pkg/networking/core/v1alpha3/route/retry" |
| "github.com/apache/dubbo-go-pixiu/pilot/pkg/networking/telemetry" |
| "github.com/apache/dubbo-go-pixiu/pilot/pkg/networking/util" |
| authz "github.com/apache/dubbo-go-pixiu/pilot/pkg/security/authz/model" |
| "github.com/apache/dubbo-go-pixiu/pilot/pkg/util/constant" |
| "github.com/apache/dubbo-go-pixiu/pkg/config" |
| "github.com/apache/dubbo-go-pixiu/pkg/config/constants" |
| "github.com/apache/dubbo-go-pixiu/pkg/config/host" |
| "github.com/apache/dubbo-go-pixiu/pkg/config/labels" |
| "github.com/apache/dubbo-go-pixiu/pkg/proto" |
| ) |
| |
| // Headers with special meaning in Envoy |
| const ( |
| HeaderMethod = ":method" |
| HeaderAuthority = ":authority" |
| HeaderScheme = ":scheme" |
| ) |
| |
| // DefaultRouteName is the name assigned to a route generated by default in absence of a virtual service. |
| const DefaultRouteName = "default" |
| |
| // prefixMatchRegex optionally matches "/..." at the end of a path. |
| // regex taken from https://github.com/projectcontour/contour/blob/2b3376449bedfea7b8cea5fbade99fb64009c0f6/internal/envoy/v3/route.go#L59 |
| const prefixMatchRegex = `((\/).*)?` |
| |
| // VirtualHostWrapper is a context-dependent virtual host entry with guarded routes. |
| // Note: Currently we are not fully utilizing this structure. We could invoke this logic |
| // once for all sidecars in the cluster to compute all RDS for inside the mesh and arrange |
| // it by listener port. However to properly use such an optimization, we need to have an |
| // eventing subsystem to invalidate the computed routes if any service changes/virtual Services change. |
| type VirtualHostWrapper struct { |
| // Port is the listener port for outbound sidecar (e.g. service port) |
| Port int |
| |
| // Services are the Services from the registry. Each service |
| // in this list should have a virtual host entry |
| Services []*model.Service |
| |
| // VirtualServiceHosts is a list of hosts defined in the virtual service |
| // if virtual service hostname is same as a the service registry host, then |
| // the host would appear in Services as we need to generate all variants of the |
| // service's hostname within a platform (e.g., foo, foo.default, foo.default.svc, etc.) |
| VirtualServiceHosts []string |
| |
| // Routes in the virtual host |
| Routes []*route.Route |
| } |
| |
| // BuildSidecarVirtualHostWrapper creates virtual hosts from |
| // the given set of virtual Services and a list of Services from the |
| // service registry. Services are indexed by FQDN hostnames. |
| // The list of Services is also passed to allow maintaining consistent ordering. |
| func BuildSidecarVirtualHostWrapper(routeCache *Cache, node *model.Proxy, push *model.PushContext, serviceRegistry map[host.Name]*model.Service, |
| virtualServices []config.Config, listenPort int, |
| ) []VirtualHostWrapper { |
| out := make([]VirtualHostWrapper, 0) |
| |
| // dependentDestinationRules includes all the destinationrules referenced by the virtualservices, which have consistent hash policy. |
| dependentDestinationRules := []*config.Config{} |
| // consistent hash policies for the http route destinations |
| hashByDestination := map[*networking.HTTPRouteDestination]*networking.LoadBalancerSettings_ConsistentHashLB{} |
| for _, virtualService := range virtualServices { |
| for _, httpRoute := range virtualService.Spec.(*networking.VirtualService).Http { |
| for _, destination := range httpRoute.Route { |
| hostName := destination.Destination.Host |
| var configNamespace string |
| if serviceRegistry[host.Name(hostName)] != nil { |
| configNamespace = serviceRegistry[host.Name(hostName)].Attributes.Namespace |
| } else { |
| configNamespace = virtualService.Namespace |
| } |
| hash, destinationRule := GetHashForHTTPDestination(push, node, destination, configNamespace) |
| if hash != nil { |
| hashByDestination[destination] = hash |
| dependentDestinationRules = append(dependentDestinationRules, destinationRule) |
| } |
| } |
| } |
| } |
| |
| // translate all virtual service configs into virtual hosts |
| for _, virtualService := range virtualServices { |
| wrappers := buildSidecarVirtualHostsForVirtualService(node, virtualService, serviceRegistry, hashByDestination, listenPort, push.Mesh) |
| out = append(out, wrappers...) |
| } |
| |
| // compute Services missing virtual service configs |
| for _, wrapper := range out { |
| for _, service := range wrapper.Services { |
| delete(serviceRegistry, service.Hostname) |
| } |
| } |
| |
| hashByService := map[host.Name]map[int]*networking.LoadBalancerSettings_ConsistentHashLB{} |
| for _, svc := range serviceRegistry { |
| for _, port := range svc.Ports { |
| if port.Protocol.IsHTTP() || util.IsProtocolSniffingEnabledForPort(port) { |
| hash, destinationRule := getHashForService(node, push, svc, port) |
| if hash != nil { |
| if _, ok := hashByService[svc.Hostname]; !ok { |
| hashByService[svc.Hostname] = map[int]*networking.LoadBalancerSettings_ConsistentHashLB{} |
| } |
| hashByService[svc.Hostname][port.Port] = hash |
| dependentDestinationRules = append(dependentDestinationRules, destinationRule) |
| } |
| } |
| } |
| } |
| |
| if routeCache != nil { |
| routeCache.DestinationRules = dependentDestinationRules |
| } |
| |
| // append default hosts for the service missing virtual Services |
| out = append(out, buildSidecarVirtualHostsForService(serviceRegistry, hashByService, push.Mesh)...) |
| return out |
| } |
| |
| // separateVSHostsAndServices splits the virtual service hosts into Services (if they are found in the registry) and |
| // plain non-registry hostnames |
| func separateVSHostsAndServices(virtualService config.Config, |
| serviceRegistry map[host.Name]*model.Service, |
| ) ([]string, []*model.Service) { |
| rule := virtualService.Spec.(*networking.VirtualService) |
| hosts := make([]string, 0) |
| servicesInVirtualService := make([]*model.Service, 0) |
| wchosts := make([]host.Name, 0) |
| |
| // As a performance optimization, process non wildcard hosts first, so that they can be |
| // looked up directly in the service registry map. |
| for _, hostname := range rule.Hosts { |
| vshost := host.Name(hostname) |
| if !vshost.IsWildCarded() { |
| if svc, exists := serviceRegistry[vshost]; exists { |
| servicesInVirtualService = append(servicesInVirtualService, svc) |
| } else { |
| hosts = append(hosts, hostname) |
| } |
| } else { |
| // Add it to the wildcard hosts so that they can be processed later. |
| wchosts = append(wchosts, vshost) |
| } |
| } |
| |
| // Now process wild card hosts as they need to follow the slow path of looping through all Services in the registry. |
| for _, hostname := range wchosts { |
| if model.UseGatewaySemantics(virtualService) { |
| hosts = append(hosts, string(hostname)) |
| continue |
| } |
| // Say host is *.global |
| foundSvcMatch := false |
| // Say we have Services *.foo.global, *.bar.global |
| for svcHost, svc := range serviceRegistry { |
| // *.foo.global matches *.global |
| if svcHost.Matches(hostname) { |
| servicesInVirtualService = append(servicesInVirtualService, svc) |
| foundSvcMatch = true |
| } |
| } |
| if !foundSvcMatch { |
| hosts = append(hosts, string(hostname)) |
| } |
| } |
| |
| return hosts, servicesInVirtualService |
| } |
| |
| // buildSidecarVirtualHostsForVirtualService creates virtual hosts corresponding to a virtual service. |
| // Called for each port to determine the list of vhosts on the given port. |
| // It may return an empty list if no VirtualService rule has a matching service. |
| func buildSidecarVirtualHostsForVirtualService( |
| node *model.Proxy, |
| virtualService config.Config, |
| serviceRegistry map[host.Name]*model.Service, |
| hashByDestination map[*networking.HTTPRouteDestination]*networking.LoadBalancerSettings_ConsistentHashLB, |
| listenPort int, |
| mesh *meshconfig.MeshConfig, |
| ) []VirtualHostWrapper { |
| meshGateway := map[string]bool{constants.IstioMeshGateway: true} |
| routes, err := BuildHTTPRoutesForVirtualService(node, virtualService, serviceRegistry, hashByDestination, |
| listenPort, meshGateway, false /* isH3DiscoveryNeeded */, mesh) |
| if err != nil || len(routes) == 0 { |
| return nil |
| } |
| |
| hosts, servicesInVirtualService := separateVSHostsAndServices(virtualService, serviceRegistry) |
| |
| // Now group these Services by port so that we can infer the destination.port if the user |
| // doesn't specify any port for a multiport service. We need to know the destination port in |
| // order to build the cluster name (outbound|<port>|<subset>|<serviceFQDN>) |
| // If the destination service is being accessed on port X, we set that as the default |
| // destination port |
| serviceByPort := make(map[int][]*model.Service) |
| for _, svc := range servicesInVirtualService { |
| for _, port := range svc.Ports { |
| if port.Protocol.IsHTTP() || util.IsProtocolSniffingEnabledForPort(port) { |
| serviceByPort[port.Port] = append(serviceByPort[port.Port], svc) |
| } |
| } |
| } |
| |
| if len(serviceByPort) == 0 { |
| if listenPort == 80 { |
| // TODO: This is a gross HACK. Fix me. Its a much bigger surgery though, due to the way |
| // the current code is written. |
| serviceByPort[80] = nil |
| } |
| } |
| |
| out := make([]VirtualHostWrapper, 0, len(serviceByPort)) |
| for port, services := range serviceByPort { |
| out = append(out, VirtualHostWrapper{ |
| Port: port, |
| Services: services, |
| VirtualServiceHosts: hosts, |
| Routes: routes, |
| }) |
| } |
| |
| return out |
| } |
| |
| func buildSidecarVirtualHostsForService( |
| serviceRegistry map[host.Name]*model.Service, |
| hashByService map[host.Name]map[int]*networking.LoadBalancerSettings_ConsistentHashLB, |
| mesh *meshconfig.MeshConfig, |
| ) []VirtualHostWrapper { |
| out := make([]VirtualHostWrapper, 0) |
| for _, svc := range serviceRegistry { |
| for _, port := range svc.Ports { |
| if port.Protocol.IsHTTP() || util.IsProtocolSniffingEnabledForPort(port) { |
| cluster := model.BuildSubsetKey(model.TrafficDirectionOutbound, "", svc.Hostname, port.Port) |
| traceOperation := telemetry.TraceOperation(string(svc.Hostname), port.Port) |
| httpRoute := BuildDefaultHTTPOutboundRoute(cluster, traceOperation, mesh) |
| |
| // if this host has no virtualservice, the consistentHash on its destinationRule will be useless |
| if hashByPort, ok := hashByService[svc.Hostname]; ok { |
| hashPolicy := consistentHashToHashPolicy(hashByPort[port.Port]) |
| if hashPolicy != nil { |
| httpRoute.GetRoute().HashPolicy = []*route.RouteAction_HashPolicy{hashPolicy} |
| } |
| } |
| out = append(out, VirtualHostWrapper{ |
| Port: port.Port, |
| Services: []*model.Service{svc}, |
| Routes: []*route.Route{httpRoute}, |
| }) |
| } |
| } |
| } |
| return out |
| } |
| |
| // GetDestinationCluster generates a cluster name for the route, or error if no cluster |
| // can be found. Called by translateRule to determine if |
| func GetDestinationCluster(destination *networking.Destination, service *model.Service, listenerPort int) string { |
| port := listenerPort |
| if destination.GetPort() != nil { |
| port = int(destination.GetPort().GetNumber()) |
| } else if service != nil && len(service.Ports) == 1 { |
| // if service only has one port defined, use that as the port, otherwise use default listenerPort |
| port = service.Ports[0].Port |
| |
| // Do not return blackhole cluster for service==nil case as there is a legitimate use case for |
| // calling this function with nil service: to route to a pre-defined statically configured cluster |
| // declared as part of the bootstrap. |
| // If blackhole cluster is needed, do the check on the caller side. See gateway and tls.go for examples. |
| } |
| |
| return model.BuildSubsetKey(model.TrafficDirectionOutbound, destination.Subset, host.Name(destination.Host), port) |
| } |
| |
| // BuildHTTPRoutesForVirtualService creates data plane HTTP routes from the virtual service spec. |
| // The rule should be adapted to destination names (outbound clusters). |
| // Each rule is guarded by source labels. |
| // |
| // This is called for each port to compute virtual hosts. |
| // Each VirtualService is tried, with a list of Services that listen on the port. |
| // Error indicates the given virtualService can't be used on the port. |
| // This function is used by both the gateway and the sidecar |
| func BuildHTTPRoutesForVirtualService( |
| node *model.Proxy, |
| virtualService config.Config, |
| serviceRegistry map[host.Name]*model.Service, |
| hashByDestination map[*networking.HTTPRouteDestination]*networking.LoadBalancerSettings_ConsistentHashLB, |
| listenPort int, |
| gatewayNames map[string]bool, |
| isHTTP3AltSvcHeaderNeeded bool, |
| mesh *meshconfig.MeshConfig, |
| ) ([]*route.Route, error) { |
| vs, ok := virtualService.Spec.(*networking.VirtualService) |
| if !ok { // should never happen |
| return nil, fmt.Errorf("in not a virtual service: %#v", virtualService) |
| } |
| |
| out := make([]*route.Route, 0, len(vs.Http)) |
| |
| catchall := false |
| for _, http := range vs.Http { |
| if len(http.Match) == 0 { |
| if r := translateRoute(node, http, nil, listenPort, virtualService, serviceRegistry, |
| hashByDestination, gatewayNames, isHTTP3AltSvcHeaderNeeded, mesh); r != nil { |
| out = append(out, r) |
| } |
| catchall = true |
| } else { |
| for _, match := range http.Match { |
| if r := translateRoute(node, http, match, listenPort, virtualService, serviceRegistry, |
| hashByDestination, gatewayNames, isHTTP3AltSvcHeaderNeeded, mesh); r != nil { |
| out = append(out, r) |
| // This is a catch all path. Routes are matched in order, so we will never go beyond this match |
| // As an optimization, we can just top sending any more routes here. |
| if isCatchAllRoute(r) { |
| catchall = true |
| break |
| } |
| } |
| } |
| } |
| if catchall { |
| break |
| } |
| } |
| |
| if len(out) == 0 { |
| return nil, fmt.Errorf("no routes matched") |
| } |
| return out, nil |
| } |
| |
| // sourceMatchHttp checks if the sourceLabels or the gateways in a match condition match with the |
| // labels for the proxy or the gateway name for which we are generating a route |
| func sourceMatchHTTP(match *networking.HTTPMatchRequest, proxyLabels labels.Instance, gatewayNames map[string]bool, proxyNamespace string) bool { |
| if match == nil { |
| return true |
| } |
| |
| // Trim by source labels or mesh gateway |
| if len(match.Gateways) > 0 { |
| for _, g := range match.Gateways { |
| if gatewayNames[g] { |
| return true |
| } |
| } |
| } else if labels.Instance(match.GetSourceLabels()).SubsetOf(proxyLabels) { |
| return match.SourceNamespace == "" || match.SourceNamespace == proxyNamespace |
| } |
| |
| return false |
| } |
| |
| // translateRoute translates HTTP routes |
| func translateRoute( |
| node *model.Proxy, |
| in *networking.HTTPRoute, |
| match *networking.HTTPMatchRequest, |
| listenPort int, |
| virtualService config.Config, |
| serviceRegistry map[host.Name]*model.Service, |
| hashByDestination map[*networking.HTTPRouteDestination]*networking.LoadBalancerSettings_ConsistentHashLB, |
| gatewayNames map[string]bool, |
| isHTTP3AltSvcHeaderNeeded bool, |
| mesh *meshconfig.MeshConfig, |
| ) *route.Route { |
| // When building routes, it's okay if the target cluster cannot be |
| // resolved Traffic to such clusters will blackhole. |
| |
| // Match by the destination port specified in the match condition |
| if match != nil && match.Port != 0 && match.Port != uint32(listenPort) { |
| return nil |
| } |
| // Match by source labels/gateway names inside the match condition |
| if !sourceMatchHTTP(match, node.Metadata.Labels, gatewayNames, node.Metadata.Namespace) { |
| return nil |
| } |
| |
| routeName := in.Name |
| if match != nil && match.Name != "" { |
| routeName = routeName + "." + match.Name |
| } |
| |
| out := &route.Route{ |
| Name: routeName, |
| Match: translateRouteMatch(node, virtualService, match), |
| Metadata: util.BuildConfigInfoMetadata(virtualService.Meta), |
| } |
| authority := "" |
| if in.Headers != nil { |
| operations := translateHeadersOperations(in.Headers) |
| out.RequestHeadersToAdd = operations.requestHeadersToAdd |
| out.ResponseHeadersToAdd = operations.responseHeadersToAdd |
| out.RequestHeadersToRemove = operations.requestHeadersToRemove |
| out.ResponseHeadersToRemove = operations.responseHeadersToRemove |
| authority = operations.authority |
| } |
| |
| if in.Redirect != nil { |
| applyRedirect(out, in.Redirect, listenPort) |
| } else { |
| applyHTTPRouteDestination(out, node, in, mesh, authority, serviceRegistry, listenPort, hashByDestination) |
| } |
| |
| out.Decorator = &route.Decorator{ |
| Operation: getRouteOperation(out, virtualService.Name, listenPort), |
| } |
| if in.Fault != nil { |
| out.TypedPerFilterConfig = make(map[string]*any.Any) |
| out.TypedPerFilterConfig[wellknown.Fault] = util.MessageToAny(translateFault(in.Fault)) |
| } |
| |
| if isHTTP3AltSvcHeaderNeeded { |
| http3AltSvcHeader := buildHTTP3AltSvcHeader(listenPort, util.ALPNHttp3OverQUIC) |
| if out.ResponseHeadersToAdd == nil { |
| out.ResponseHeadersToAdd = make([]*core.HeaderValueOption, 0) |
| } |
| out.ResponseHeadersToAdd = append(out.ResponseHeadersToAdd, http3AltSvcHeader) |
| } |
| |
| return out |
| } |
| |
| func applyHTTPRouteDestination( |
| out *route.Route, |
| node *model.Proxy, |
| in *networking.HTTPRoute, |
| mesh *meshconfig.MeshConfig, |
| authority string, |
| serviceRegistry map[host.Name]*model.Service, |
| listenerPort int, |
| hashByDestination map[*networking.HTTPRouteDestination]*networking.LoadBalancerSettings_ConsistentHashLB, |
| ) { |
| policy := in.Retries |
| if policy == nil { |
| // No VS policy set, use mesh defaults |
| policy = mesh.GetDefaultHttpRetryPolicy() |
| } |
| action := &route.RouteAction{ |
| Cors: translateCORSPolicy(in.CorsPolicy), |
| RetryPolicy: retry.ConvertPolicy(policy), |
| } |
| |
| // Configure timeouts specified by Virtual Service if they are provided, otherwise set it to defaults. |
| action.Timeout = features.DefaultRequestTimeout |
| if in.Timeout != nil { |
| action.Timeout = in.Timeout |
| } |
| if node.IsProxylessGrpc() { |
| // TODO(stevenctl) merge these paths; grpc's xDS impl will not read the deprecated value |
| action.MaxStreamDuration = &route.RouteAction_MaxStreamDuration{MaxStreamDuration: action.Timeout} |
| } else { |
| // Use deprecated value for now as the replacement MaxStreamDuration has some regressions. |
| // nolint: staticcheck |
| action.MaxGrpcTimeout = action.Timeout |
| } |
| |
| out.Action = &route.Route_Route{Route: action} |
| |
| if in.Rewrite != nil { |
| action.PrefixRewrite = in.Rewrite.GetUri() |
| if in.Rewrite.GetAuthority() != "" { |
| authority = in.Rewrite.GetAuthority() |
| } |
| } |
| if authority != "" { |
| action.HostRewriteSpecifier = &route.RouteAction_HostRewriteLiteral{ |
| HostRewriteLiteral: authority, |
| } |
| } |
| |
| if in.Mirror != nil { |
| if mp := mirrorPercent(in); mp != nil { |
| action.RequestMirrorPolicies = []*route.RouteAction_RequestMirrorPolicy{{ |
| Cluster: GetDestinationCluster(in.Mirror, serviceRegistry[host.Name(in.Mirror.Host)], listenerPort), |
| RuntimeFraction: mp, |
| TraceSampled: &wrappers.BoolValue{Value: false}, |
| }} |
| } |
| } |
| |
| // TODO: eliminate this logic and use the total_weight option in envoy route |
| weighted := make([]*route.WeightedCluster_ClusterWeight, 0) |
| for _, dst := range in.Route { |
| weight := &wrappers.UInt32Value{Value: uint32(dst.Weight)} |
| if dst.Weight == 0 { |
| // Ignore 0 weighted clusters if there are other clusters in the route. |
| // But if this is the only cluster in the route, then add it as a cluster with weight 100 |
| if len(in.Route) == 1 { |
| weight.Value = uint32(100) |
| } else { |
| continue |
| } |
| } |
| hostname := host.Name(dst.GetDestination().GetHost()) |
| n := GetDestinationCluster(dst.Destination, serviceRegistry[hostname], listenerPort) |
| clusterWeight := &route.WeightedCluster_ClusterWeight{ |
| Name: n, |
| Weight: weight, |
| } |
| if dst.Headers != nil { |
| operations := translateHeadersOperations(dst.Headers) |
| clusterWeight.RequestHeadersToAdd = operations.requestHeadersToAdd |
| clusterWeight.RequestHeadersToRemove = operations.requestHeadersToRemove |
| clusterWeight.ResponseHeadersToAdd = operations.responseHeadersToAdd |
| clusterWeight.ResponseHeadersToRemove = operations.responseHeadersToRemove |
| if operations.authority != "" { |
| clusterWeight.HostRewriteSpecifier = &route.WeightedCluster_ClusterWeight_HostRewriteLiteral{ |
| HostRewriteLiteral: operations.authority, |
| } |
| } |
| } |
| |
| weighted = append(weighted, clusterWeight) |
| hash := hashByDestination[dst] |
| hashPolicy := consistentHashToHashPolicy(hash) |
| if hashPolicy != nil { |
| action.HashPolicy = append(action.HashPolicy, hashPolicy) |
| } |
| } |
| |
| // rewrite to a single cluster if there is only weighted cluster |
| if len(weighted) == 1 { |
| action.ClusterSpecifier = &route.RouteAction_Cluster{Cluster: weighted[0].Name} |
| out.RequestHeadersToAdd = append(out.RequestHeadersToAdd, weighted[0].RequestHeadersToAdd...) |
| out.RequestHeadersToRemove = append(out.RequestHeadersToRemove, weighted[0].RequestHeadersToRemove...) |
| out.ResponseHeadersToAdd = append(out.ResponseHeadersToAdd, weighted[0].ResponseHeadersToAdd...) |
| out.ResponseHeadersToRemove = append(out.ResponseHeadersToRemove, weighted[0].ResponseHeadersToRemove...) |
| if weighted[0].HostRewriteSpecifier != nil && action.HostRewriteSpecifier == nil { |
| // Ideally, if the weighted cluster overwrites authority, it has precedence. This mirrors behavior of headers, |
| // because for headers we append the weighted last which allows it to Set and wipe out previous Adds. |
| // However, Envoy behavior is different when we set at both cluster level and route level, and we want |
| // behavior to be consistent with a single cluster and multiple clusters. |
| // As a result, we only override if the top level rewrite is not set |
| action.HostRewriteSpecifier = &route.RouteAction_HostRewriteLiteral{ |
| HostRewriteLiteral: weighted[0].GetHostRewriteLiteral(), |
| } |
| } |
| } else { |
| action.ClusterSpecifier = &route.RouteAction_WeightedClusters{ |
| WeightedClusters: &route.WeightedCluster{ |
| Clusters: weighted, |
| }, |
| } |
| } |
| } |
| |
| func applyRedirect(out *route.Route, redirect *networking.HTTPRedirect, port int) { |
| action := &route.Route_Redirect{ |
| Redirect: &route.RedirectAction{ |
| HostRedirect: redirect.Authority, |
| PathRewriteSpecifier: &route.RedirectAction_PathRedirect{ |
| PathRedirect: redirect.Uri, |
| }, |
| }, |
| } |
| |
| if redirect.Scheme != "" { |
| action.Redirect.SchemeRewriteSpecifier = &route.RedirectAction_SchemeRedirect{SchemeRedirect: redirect.Scheme} |
| } |
| |
| if redirect.RedirectPort != nil { |
| switch rp := redirect.RedirectPort.(type) { |
| case *networking.HTTPRedirect_DerivePort: |
| if rp.DerivePort == networking.HTTPRedirect_FROM_REQUEST_PORT { |
| // Envoy doesn't actually support deriving the port from the request dynamically. However, |
| // we always generate routes in the context of a specific request port. As a result, we can just |
| // use that port |
| action.Redirect.PortRedirect = uint32(port) |
| } |
| // Otherwise, no port needed; HTTPRedirect_FROM_PROTOCOL_DEFAULT is Envoy's default behavior |
| case *networking.HTTPRedirect_Port: |
| action.Redirect.PortRedirect = rp.Port |
| } |
| } |
| |
| switch redirect.RedirectCode { |
| case 0, 301: |
| action.Redirect.ResponseCode = route.RedirectAction_MOVED_PERMANENTLY |
| case 302: |
| action.Redirect.ResponseCode = route.RedirectAction_FOUND |
| case 303: |
| action.Redirect.ResponseCode = route.RedirectAction_SEE_OTHER |
| case 307: |
| action.Redirect.ResponseCode = route.RedirectAction_TEMPORARY_REDIRECT |
| case 308: |
| action.Redirect.ResponseCode = route.RedirectAction_PERMANENT_REDIRECT |
| default: |
| log.Warnf("Redirect Code %d is not yet supported", redirect.RedirectCode) |
| action = nil |
| } |
| |
| out.Action = action |
| } |
| |
| func buildHTTP3AltSvcHeader(port int, h3Alpns []string) *core.HeaderValueOption { |
| // For example, www.cloudflare.com returns the following |
| // alt-svc: h3-27=":443"; ma=86400, h3-28=":443"; ma=86400, h3-29=":443"; ma=86400, h3=":443"; ma=86400 |
| valParts := make([]string, 0, len(h3Alpns)) |
| for _, alpn := range h3Alpns { |
| // Max-age is hardcoded to 1 day for now. |
| valParts = append(valParts, fmt.Sprintf(`%s=":%d"; ma=86400`, alpn, port)) |
| } |
| headerVal := strings.Join(valParts, ", ") |
| return &core.HeaderValueOption{ |
| Append: proto.BoolTrue, |
| Header: &core.HeaderValue{ |
| Key: util.AltSvcHeader, |
| Value: headerVal, |
| }, |
| } |
| } |
| |
| // SortHeaderValueOption type and the functions below (Len, Less and Swap) are for sort.Stable for type HeaderValueOption |
| type SortHeaderValueOption []*core.HeaderValueOption |
| |
| // mirrorPercent computes the mirror percent to be used based on "Mirror" data in route. |
| func mirrorPercent(in *networking.HTTPRoute) *core.RuntimeFractionalPercent { |
| switch { |
| case in.MirrorPercentage != nil: |
| if in.MirrorPercentage.GetValue() > 0 { |
| return &core.RuntimeFractionalPercent{ |
| DefaultValue: translatePercentToFractionalPercent(in.MirrorPercentage), |
| } |
| } |
| // If zero percent is provided explicitly, we should not mirror. |
| return nil |
| // nolint: staticcheck |
| case in.MirrorPercent != nil: |
| if in.MirrorPercent.GetValue() > 0 { |
| return &core.RuntimeFractionalPercent{ |
| DefaultValue: translateIntegerToFractionalPercent((int32(in.MirrorPercent.GetValue()))), |
| } |
| } |
| // If zero percent is provided explicitly, we should not mirror. |
| return nil |
| default: |
| // Default to 100 percent if percent is not given. |
| return &core.RuntimeFractionalPercent{ |
| DefaultValue: translateIntegerToFractionalPercent(100), |
| } |
| } |
| } |
| |
| // Len is i the sort.Interface for SortHeaderValueOption |
| func (b SortHeaderValueOption) Len() int { |
| return len(b) |
| } |
| |
| // Less is in the sort.Interface for SortHeaderValueOption |
| func (b SortHeaderValueOption) Less(i, j int) bool { |
| if b[i] == nil || b[i].Header == nil { |
| return false |
| } else if b[j] == nil || b[j].Header == nil { |
| return true |
| } |
| return strings.Compare(b[i].Header.Key, b[j].Header.Key) < 0 |
| } |
| |
| // Swap is in the sort.Interface for SortHeaderValueOption |
| func (b SortHeaderValueOption) Swap(i, j int) { |
| b[i], b[j] = b[j], b[i] |
| } |
| |
| // translateAppendHeaders translates headers |
| func translateAppendHeaders(headers map[string]string, appendFlag bool) ([]*core.HeaderValueOption, string) { |
| if len(headers) == 0 { |
| return nil, "" |
| } |
| authority := "" |
| headerValueOptionList := make([]*core.HeaderValueOption, 0, len(headers)) |
| for key, value := range headers { |
| if isAuthorityHeader(key) { |
| // If there are multiple, last one wins; validation will reject |
| authority = value |
| } |
| if isInternalHeader(key) { |
| continue |
| } |
| headerValueOptionList = append(headerValueOptionList, &core.HeaderValueOption{ |
| Header: &core.HeaderValue{ |
| Key: key, |
| Value: value, |
| }, |
| Append: &wrappers.BoolValue{Value: appendFlag}, |
| }) |
| } |
| sort.Stable(SortHeaderValueOption(headerValueOptionList)) |
| return headerValueOptionList, authority |
| } |
| |
| type headersOperations struct { |
| requestHeadersToAdd []*core.HeaderValueOption |
| responseHeadersToAdd []*core.HeaderValueOption |
| requestHeadersToRemove []string |
| responseHeadersToRemove []string |
| authority string |
| } |
| |
| // isInternalHeader returns true if a header refers to an internal value that cannot be modified by Envoy |
| func isInternalHeader(headerKey string) bool { |
| return strings.HasPrefix(headerKey, ":") || strings.EqualFold(headerKey, "host") |
| } |
| |
| // isAuthorityHeader returns true if a header refers to the authority header |
| func isAuthorityHeader(headerKey string) bool { |
| return strings.EqualFold(headerKey, ":authority") || strings.EqualFold(headerKey, "host") |
| } |
| |
| func dropInternal(keys []string) []string { |
| result := make([]string, 0, len(keys)) |
| for _, k := range keys { |
| if isInternalHeader(k) { |
| continue |
| } |
| result = append(result, k) |
| } |
| return result |
| } |
| |
| // translateHeadersOperations translates headers operations |
| func translateHeadersOperations(headers *networking.Headers) headersOperations { |
| req := headers.GetRequest() |
| resp := headers.GetResponse() |
| |
| requestHeadersToAdd, setAuthority := translateAppendHeaders(req.GetSet(), false) |
| reqAdd, addAuthority := translateAppendHeaders(req.GetAdd(), true) |
| requestHeadersToAdd = append(requestHeadersToAdd, reqAdd...) |
| |
| responseHeadersToAdd, _ := translateAppendHeaders(resp.GetSet(), false) |
| respAdd, _ := translateAppendHeaders(resp.GetAdd(), true) |
| responseHeadersToAdd = append(responseHeadersToAdd, respAdd...) |
| |
| auth := addAuthority |
| if setAuthority != "" { |
| // If authority is set in 'add' and 'set', pick the one from 'set' |
| auth = setAuthority |
| } |
| return headersOperations{ |
| requestHeadersToAdd: requestHeadersToAdd, |
| responseHeadersToAdd: responseHeadersToAdd, |
| requestHeadersToRemove: dropInternal(req.GetRemove()), |
| responseHeadersToRemove: dropInternal(resp.GetRemove()), |
| authority: auth, |
| } |
| } |
| |
| // translateRouteMatch translates match condition |
| func translateRouteMatch(node *model.Proxy, vs config.Config, in *networking.HTTPMatchRequest) *route.RouteMatch { |
| out := &route.RouteMatch{PathSpecifier: &route.RouteMatch_Prefix{Prefix: "/"}} |
| if in == nil { |
| return out |
| } |
| |
| for name, stringMatch := range in.Headers { |
| // The metadata matcher takes precedence over the header matcher. |
| if metadataMatcher := translateMetadataMatch(name, stringMatch); metadataMatcher != nil { |
| out.DynamicMetadata = append(out.DynamicMetadata, metadataMatcher) |
| } else { |
| matcher := translateHeaderMatch(name, stringMatch) |
| out.Headers = append(out.Headers, matcher) |
| } |
| } |
| |
| for name, stringMatch := range in.WithoutHeaders { |
| if metadataMatcher := translateMetadataMatch(name, stringMatch); metadataMatcher != nil { |
| metadataMatcher.Invert = true |
| out.DynamicMetadata = append(out.DynamicMetadata, metadataMatcher) |
| } else { |
| matcher := translateHeaderMatch(name, stringMatch) |
| matcher.InvertMatch = true |
| out.Headers = append(out.Headers, matcher) |
| } |
| } |
| |
| // guarantee ordering of headers |
| sort.Slice(out.Headers, func(i, j int) bool { |
| return out.Headers[i].Name < out.Headers[j].Name |
| }) |
| |
| if in.Uri != nil { |
| switch m := in.Uri.MatchType.(type) { |
| case *networking.StringMatch_Exact: |
| out.PathSpecifier = &route.RouteMatch_Path{Path: m.Exact} |
| case *networking.StringMatch_Prefix: |
| if (model.UseIngressSemantics(vs) || model.UseGatewaySemantics(vs)) && m.Prefix != "/" { |
| path := strings.TrimSuffix(m.Prefix, "/") |
| if util.IsIstioVersionGE114(node.IstioVersion) { |
| out.PathSpecifier = &route.RouteMatch_PathSeparatedPrefix{PathSeparatedPrefix: path} |
| } else { |
| // For older versions, we have to use the regex hack. |
| // From the spec: /foo/bar matches /foo/bar/baz, but does not match /foo/barbaz |
| // and if the prefix is /foo/bar/ we must match /foo/bar and /foo/bar/baz. We cannot simply strip the |
| // trailing "/" and do a prefix match since we'll match unwanted continuations and we cannot add |
| // a "/" if not present since we won't match the prefix without trailing "/". Must be smarter and |
| // use regex. |
| out.PathSpecifier = &route.RouteMatch_SafeRegex{ |
| SafeRegex: &matcher.RegexMatcher{ |
| EngineType: util.RegexEngine, |
| Regex: regexp.QuoteMeta(path) + prefixMatchRegex, |
| }, |
| } |
| } |
| } else { |
| out.PathSpecifier = &route.RouteMatch_Prefix{Prefix: m.Prefix} |
| } |
| case *networking.StringMatch_Regex: |
| out.PathSpecifier = &route.RouteMatch_SafeRegex{ |
| SafeRegex: &matcher.RegexMatcher{ |
| EngineType: util.RegexEngine, |
| Regex: m.Regex, |
| }, |
| } |
| } |
| } |
| |
| out.CaseSensitive = &wrappers.BoolValue{Value: !in.IgnoreUriCase} |
| |
| if in.Method != nil { |
| matcher := translateHeaderMatch(HeaderMethod, in.Method) |
| out.Headers = append(out.Headers, matcher) |
| } |
| |
| if in.Authority != nil { |
| matcher := translateHeaderMatch(HeaderAuthority, in.Authority) |
| out.Headers = append(out.Headers, matcher) |
| } |
| |
| if in.Scheme != nil { |
| matcher := translateHeaderMatch(HeaderScheme, in.Scheme) |
| out.Headers = append(out.Headers, matcher) |
| } |
| |
| for name, stringMatch := range in.QueryParams { |
| matcher := translateQueryParamMatch(name, stringMatch) |
| out.QueryParameters = append(out.QueryParameters, matcher) |
| } |
| |
| return out |
| } |
| |
| // translateQueryParamMatch translates a StringMatch to a QueryParameterMatcher. |
| func translateQueryParamMatch(name string, in *networking.StringMatch) *route.QueryParameterMatcher { |
| out := &route.QueryParameterMatcher{ |
| Name: name, |
| } |
| |
| switch m := in.MatchType.(type) { |
| case *networking.StringMatch_Exact: |
| out.QueryParameterMatchSpecifier = &route.QueryParameterMatcher_StringMatch{ |
| StringMatch: &matcher.StringMatcher{MatchPattern: &matcher.StringMatcher_Exact{Exact: m.Exact}}, |
| } |
| case *networking.StringMatch_Regex: |
| out.QueryParameterMatchSpecifier = &route.QueryParameterMatcher_StringMatch{ |
| StringMatch: &matcher.StringMatcher{ |
| MatchPattern: &matcher.StringMatcher_SafeRegex{ |
| SafeRegex: &matcher.RegexMatcher{ |
| EngineType: util.RegexEngine, |
| Regex: m.Regex, |
| }, |
| }, |
| }, |
| } |
| } |
| |
| return out |
| } |
| |
| // isCatchAllHeaderMatch determines if the given header is matched with all strings or not. |
| // Currently, if the regex has "*" value, it returns true |
| func isCatchAllHeaderMatch(in *networking.StringMatch) bool { |
| if in == nil { |
| return true |
| } |
| |
| catchall := false |
| |
| switch m := in.MatchType.(type) { |
| case *networking.StringMatch_Regex: |
| catchall = m.Regex == "*" |
| } |
| |
| return catchall |
| } |
| |
| // translateMetadataMatch translates a header match to dynamic metadata matcher. Returns nil if the header is not supported |
| // or the header format is invalid for generating metadata matcher. |
| // |
| // The currently only supported header is @request.auth.claims for JWT claims matching. Claims of type string or list of string |
| // are supported and nested claims are also supported using `.` as a separator for claim names. |
| // Examples: |
| // - `@request.auth.claims.admin` matches the claim "admin". |
| // - `@request.auth.claims.group.id` matches the nested claims "group" and "id". |
| func translateMetadataMatch(name string, in *networking.StringMatch) *matcher.MetadataMatcher { |
| if !strings.HasPrefix(strings.ToLower(name), constant.HeaderJWTClaim) { |
| return nil |
| } |
| claims := strings.Split(name[len(constant.HeaderJWTClaim):], ".") |
| return authz.MetadataMatcherForJWTClaims(claims, util.ConvertToEnvoyMatch(in)) |
| } |
| |
| // translateHeaderMatch translates to HeaderMatcher |
| func translateHeaderMatch(name string, in *networking.StringMatch) *route.HeaderMatcher { |
| out := &route.HeaderMatcher{ |
| Name: name, |
| } |
| |
| if isCatchAllHeaderMatch(in) { |
| out.HeaderMatchSpecifier = &route.HeaderMatcher_PresentMatch{PresentMatch: true} |
| return out |
| } |
| |
| if em := util.ConvertToEnvoyMatch(in); em != nil { |
| out.HeaderMatchSpecifier = &route.HeaderMatcher_StringMatch{ |
| StringMatch: em, |
| } |
| } |
| |
| return out |
| } |
| |
| // translateCORSPolicy translates CORS policy |
| func translateCORSPolicy(in *networking.CorsPolicy) *route.CorsPolicy { |
| if in == nil { |
| return nil |
| } |
| |
| // CORS filter is enabled by default |
| out := route.CorsPolicy{} |
| // nolint: staticcheck |
| if in.AllowOrigins != nil { |
| out.AllowOriginStringMatch = util.ConvertToEnvoyMatches(in.AllowOrigins) |
| } else if in.AllowOrigin != nil { |
| out.AllowOriginStringMatch = util.StringToExactMatch(in.AllowOrigin) |
| } |
| |
| out.EnabledSpecifier = &route.CorsPolicy_FilterEnabled{ |
| FilterEnabled: &core.RuntimeFractionalPercent{ |
| DefaultValue: &xdstype.FractionalPercent{ |
| Numerator: 100, |
| Denominator: xdstype.FractionalPercent_HUNDRED, |
| }, |
| }, |
| } |
| |
| out.AllowCredentials = in.AllowCredentials |
| out.AllowHeaders = strings.Join(in.AllowHeaders, ",") |
| out.AllowMethods = strings.Join(in.AllowMethods, ",") |
| out.ExposeHeaders = strings.Join(in.ExposeHeaders, ",") |
| if in.MaxAge != nil { |
| out.MaxAge = strconv.FormatInt(in.MaxAge.GetSeconds(), 10) |
| } |
| return &out |
| } |
| |
| // getRouteOperation returns readable route description for trace. |
| func getRouteOperation(in *route.Route, vsName string, port int) string { |
| path := "/*" |
| m := in.GetMatch() |
| ps := m.GetPathSpecifier() |
| if ps != nil { |
| switch ps.(type) { |
| case *route.RouteMatch_Prefix: |
| path = m.GetPrefix() + "*" |
| case *route.RouteMatch_Path: |
| path = m.GetPath() |
| case *route.RouteMatch_SafeRegex: |
| path = m.GetSafeRegex().GetRegex() |
| } |
| } |
| |
| // If there is only one destination cluster in route, return host:port/uri as description of route. |
| // Otherwise there are multiple destination clusters and destination host is not clear. For that case |
| // return virtual serivce name:port/uri as substitute. |
| if c := in.GetRoute().GetCluster(); model.IsValidSubsetKey(c) { |
| // Parse host and port from cluster name. |
| _, _, h, p := model.ParseSubsetKey(c) |
| return string(h) + ":" + strconv.Itoa(p) + path |
| } |
| return vsName + ":" + strconv.Itoa(port) + path |
| } |
| |
| // BuildDefaultHTTPInboundRoute builds a default inbound route. |
| func BuildDefaultHTTPInboundRoute(clusterName string, operation string) *route.Route { |
| notimeout := durationpb.New(0) |
| routeAction := &route.RouteAction{ |
| ClusterSpecifier: &route.RouteAction_Cluster{Cluster: clusterName}, |
| Timeout: notimeout, |
| } |
| routeAction.MaxStreamDuration = &route.RouteAction_MaxStreamDuration{ |
| MaxStreamDuration: notimeout, |
| // If not configured at all, the grpc-timeout header is not used and |
| // gRPC requests time out like any other requests using timeout or its default. |
| GrpcTimeoutHeaderMax: notimeout, |
| } |
| val := &route.Route{ |
| Match: translateRouteMatch(nil, config.Config{}, nil), |
| Decorator: &route.Decorator{ |
| Operation: operation, |
| }, |
| Action: &route.Route_Route{ |
| Route: routeAction, |
| }, |
| } |
| |
| val.Name = DefaultRouteName |
| return val |
| } |
| |
| // BuildDefaultHTTPOutboundRoute builds a default outbound route, including a retry policy. |
| func BuildDefaultHTTPOutboundRoute(clusterName string, operation string, mesh *meshconfig.MeshConfig) *route.Route { |
| // Start with the same configuration as for inbound. |
| out := BuildDefaultHTTPInboundRoute(clusterName, operation) |
| |
| // Add a default retry policy for outbound routes. |
| out.GetRoute().RetryPolicy = retry.ConvertPolicy(mesh.GetDefaultHttpRetryPolicy()) |
| return out |
| } |
| |
| // translatePercentToFractionalPercent translates an v1alpha3 Percent instance |
| // to an envoy.type.FractionalPercent instance. |
| func translatePercentToFractionalPercent(p *networking.Percent) *xdstype.FractionalPercent { |
| return &xdstype.FractionalPercent{ |
| Numerator: uint32(p.Value * 10000), |
| Denominator: xdstype.FractionalPercent_MILLION, |
| } |
| } |
| |
| // translateIntegerToFractionalPercent translates an int32 instance to an |
| // envoy.type.FractionalPercent instance. |
| func translateIntegerToFractionalPercent(p int32) *xdstype.FractionalPercent { |
| return &xdstype.FractionalPercent{ |
| Numerator: uint32(p), |
| Denominator: xdstype.FractionalPercent_HUNDRED, |
| } |
| } |
| |
| // translateFault translates networking.HTTPFaultInjection into Envoy's HTTPFault |
| func translateFault(in *networking.HTTPFaultInjection) *xdshttpfault.HTTPFault { |
| if in == nil { |
| return nil |
| } |
| |
| out := xdshttpfault.HTTPFault{} |
| if in.Delay != nil { |
| out.Delay = &xdsfault.FaultDelay{} |
| if in.Delay.Percentage != nil { |
| out.Delay.Percentage = translatePercentToFractionalPercent(in.Delay.Percentage) |
| } else { |
| out.Delay.Percentage = translateIntegerToFractionalPercent(in.Delay.Percent) // nolint: staticcheck |
| } |
| switch d := in.Delay.HttpDelayType.(type) { |
| case *networking.HTTPFaultInjection_Delay_FixedDelay: |
| out.Delay.FaultDelaySecifier = &xdsfault.FaultDelay_FixedDelay{ |
| FixedDelay: d.FixedDelay, |
| } |
| default: |
| log.Warnf("Exponential faults are not yet supported") |
| out.Delay = nil |
| } |
| } |
| |
| if in.Abort != nil { |
| out.Abort = &xdshttpfault.FaultAbort{} |
| if in.Abort.Percentage != nil { |
| out.Abort.Percentage = translatePercentToFractionalPercent(in.Abort.Percentage) |
| } |
| switch a := in.Abort.ErrorType.(type) { |
| case *networking.HTTPFaultInjection_Abort_HttpStatus: |
| out.Abort.ErrorType = &xdshttpfault.FaultAbort_HttpStatus{ |
| HttpStatus: uint32(a.HttpStatus), |
| } |
| default: |
| log.Warnf("Non-HTTP type abort faults are not yet supported") |
| out.Abort = nil |
| } |
| } |
| |
| if out.Delay == nil && out.Abort == nil { |
| return nil |
| } |
| |
| return &out |
| } |
| |
| func portLevelSettingsConsistentHash(dst *networking.Destination, |
| pls []*networking.TrafficPolicy_PortTrafficPolicy, |
| ) *networking.LoadBalancerSettings_ConsistentHashLB { |
| if dst.Port != nil { |
| portNumber := dst.GetPort().GetNumber() |
| for _, setting := range pls { |
| number := setting.GetPort().GetNumber() |
| if number == portNumber { |
| return setting.GetLoadBalancer().GetConsistentHash() |
| } |
| } |
| } |
| |
| return nil |
| } |
| |
| func consistentHashToHashPolicy(consistentHash *networking.LoadBalancerSettings_ConsistentHashLB) *route.RouteAction_HashPolicy { |
| switch consistentHash.GetHashKey().(type) { |
| case *networking.LoadBalancerSettings_ConsistentHashLB_HttpHeaderName: |
| return &route.RouteAction_HashPolicy{ |
| PolicySpecifier: &route.RouteAction_HashPolicy_Header_{ |
| Header: &route.RouteAction_HashPolicy_Header{ |
| HeaderName: consistentHash.GetHttpHeaderName(), |
| }, |
| }, |
| } |
| case *networking.LoadBalancerSettings_ConsistentHashLB_HttpCookie: |
| cookie := consistentHash.GetHttpCookie() |
| var ttl *durationpb.Duration |
| if cookie.GetTtl() != nil { |
| ttl = cookie.GetTtl() |
| } |
| return &route.RouteAction_HashPolicy{ |
| PolicySpecifier: &route.RouteAction_HashPolicy_Cookie_{ |
| Cookie: &route.RouteAction_HashPolicy_Cookie{ |
| Name: cookie.GetName(), |
| Ttl: ttl, |
| Path: cookie.GetPath(), |
| }, |
| }, |
| } |
| case *networking.LoadBalancerSettings_ConsistentHashLB_UseSourceIp: |
| return &route.RouteAction_HashPolicy{ |
| PolicySpecifier: &route.RouteAction_HashPolicy_ConnectionProperties_{ |
| ConnectionProperties: &route.RouteAction_HashPolicy_ConnectionProperties{ |
| SourceIp: consistentHash.GetUseSourceIp(), |
| }, |
| }, |
| } |
| case *networking.LoadBalancerSettings_ConsistentHashLB_HttpQueryParameterName: |
| return &route.RouteAction_HashPolicy{ |
| PolicySpecifier: &route.RouteAction_HashPolicy_QueryParameter_{ |
| QueryParameter: &route.RouteAction_HashPolicy_QueryParameter{ |
| Name: consistentHash.GetHttpQueryParameterName(), |
| }, |
| }, |
| } |
| } |
| return nil |
| } |
| |
| func getHashForService(node *model.Proxy, push *model.PushContext, |
| svc *model.Service, port *model.Port, |
| ) (*networking.LoadBalancerSettings_ConsistentHashLB, *config.Config) { |
| if push == nil { |
| return nil, nil |
| } |
| destinationRule := node.SidecarScope.DestinationRule(model.TrafficDirectionOutbound, node, svc.Hostname) |
| if destinationRule == nil { |
| return nil, nil |
| } |
| rule := destinationRule.Spec.(*networking.DestinationRule) |
| consistentHash := rule.GetTrafficPolicy().GetLoadBalancer().GetConsistentHash() |
| portLevelSettings := rule.GetTrafficPolicy().GetPortLevelSettings() |
| for _, setting := range portLevelSettings { |
| number := setting.GetPort().GetNumber() |
| if int(number) == port.Port { |
| if setting.GetLoadBalancer().GetConsistentHash() != nil { |
| consistentHash = setting.GetLoadBalancer().GetConsistentHash() |
| } |
| break |
| } |
| } |
| |
| return consistentHash, destinationRule |
| } |
| |
| func GetConsistentHashForVirtualService(push *model.PushContext, node *model.Proxy, |
| virtualService config.Config, |
| serviceRegistry map[host.Name]*model.Service, |
| ) map[*networking.HTTPRouteDestination]*networking.LoadBalancerSettings_ConsistentHashLB { |
| hashByDestination := map[*networking.HTTPRouteDestination]*networking.LoadBalancerSettings_ConsistentHashLB{} |
| for _, httpRoute := range virtualService.Spec.(*networking.VirtualService).Http { |
| for _, destination := range httpRoute.Route { |
| hostName := destination.Destination.Host |
| var configNamespace string |
| if serviceRegistry[host.Name(hostName)] != nil { |
| configNamespace = serviceRegistry[host.Name(hostName)].Attributes.Namespace |
| } else { |
| configNamespace = virtualService.Namespace |
| } |
| hash, _ := GetHashForHTTPDestination(push, node, destination, configNamespace) |
| if hash != nil { |
| hashByDestination[destination] = hash |
| } |
| } |
| } |
| |
| return hashByDestination |
| } |
| |
| // GetHashForHTTPDestination return the ConsistentHashLB and the DestinationRule associated with HTTP route destination. |
| func GetHashForHTTPDestination(push *model.PushContext, node *model.Proxy, dst *networking.HTTPRouteDestination, |
| configNamespace string, |
| ) (*networking.LoadBalancerSettings_ConsistentHashLB, *config.Config) { |
| if push == nil { |
| return nil, nil |
| } |
| |
| destination := dst.GetDestination() |
| destinationRule := node.SidecarScope.DestinationRule(model.TrafficDirectionOutbound, node, host.Name(destination.Host)) |
| if destinationRule == nil { |
| return nil, nil |
| } |
| |
| rule := destinationRule.Spec.(*networking.DestinationRule) |
| |
| consistentHash := rule.GetTrafficPolicy().GetLoadBalancer().GetConsistentHash() |
| portLevelSettings := rule.GetTrafficPolicy().GetPortLevelSettings() |
| plsHash := portLevelSettingsConsistentHash(destination, portLevelSettings) |
| |
| var subsetHash, subsetPLSHash *networking.LoadBalancerSettings_ConsistentHashLB |
| for _, subset := range rule.GetSubsets() { |
| if subset.GetName() == destination.GetSubset() { |
| subsetPortLevelSettings := subset.GetTrafficPolicy().GetPortLevelSettings() |
| subsetHash = subset.GetTrafficPolicy().GetLoadBalancer().GetConsistentHash() |
| subsetPLSHash = portLevelSettingsConsistentHash(destination, subsetPortLevelSettings) |
| break |
| } |
| } |
| |
| switch { |
| case subsetPLSHash != nil: |
| consistentHash = subsetPLSHash |
| case subsetHash != nil: |
| consistentHash = subsetHash |
| case plsHash != nil: |
| consistentHash = plsHash |
| } |
| return consistentHash, destinationRule |
| } |
| |
| // 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 *networking.HTTPMatchRequest) bool { |
| catchall := false |
| if m.Uri != nil { |
| switch m := m.Uri.MatchType.(type) { |
| case *networking.StringMatch_Prefix: |
| catchall = m.Prefix == "/" |
| case *networking.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 == "" |
| } |
| |
| // CombineVHostRoutes semi concatenates Vhost's routes into a single route set. |
| // Moves the catch all routes alone to the end, while retaining |
| // the relative order of other routes in the concatenated route. |
| // Assumes that the virtual Services that generated first and second are ordered by |
| // time. |
| func CombineVHostRoutes(routeSets ...[]*route.Route) []*route.Route { |
| l := 0 |
| for _, rs := range routeSets { |
| l += len(rs) |
| } |
| allroutes := make([]*route.Route, 0, l) |
| catchAllRoutes := make([]*route.Route, 0) |
| for _, routes := range routeSets { |
| for _, r := range routes { |
| if isCatchAllRoute(r) { |
| catchAllRoutes = append(catchAllRoutes, r) |
| } else { |
| allroutes = append(allroutes, r) |
| } |
| } |
| } |
| return append(allroutes, catchAllRoutes...) |
| } |
| |
| // isCatchAllRoute returns true if an Envoy route is a catchall route otherwise false. |
| func isCatchAllRoute(r *route.Route) bool { |
| catchall := false |
| switch ir := r.Match.PathSpecifier.(type) { |
| case *route.RouteMatch_Prefix: |
| catchall = ir.Prefix == "/" |
| case *route.RouteMatch_PathSeparatedPrefix: |
| catchall = ir.PathSeparatedPrefix == "/" |
| case *route.RouteMatch_SafeRegex: |
| catchall = ir.SafeRegex.GetRegex() == "*" |
| } |
| // A Match is catch all if and only if it has no header/query param match |
| // and URI has a prefix / or regex *. |
| return catchall && len(r.Match.Headers) == 0 && len(r.Match.QueryParameters) == 0 && len(r.Match.DynamicMetadata) == 0 |
| } |