blob: 820fe25e19ecf347d5a0ea9366b857edcb29b1aa [file] [log] [blame]
// 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
}