5d0cb00ec3
Previously, we did not require the 'service-name.*' host header value when on a single http service was exposed. However, this allows a user to get into a situation where, if they add another service to the listener, suddenly the previous service's traffic might not be routed correctly. Thus, we always require the Host header, even if there is only 1 service. Also, we add the make the default domain matching more restrictive by matching "service-name.ingress.*" by default. This lines up better with the namespace case and more accurately matches the Consul DNS value we expect people to use in this case.
410 lines
12 KiB
Go
410 lines
12 KiB
Go
package xds
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/gogo/protobuf/proto"
|
|
|
|
envoy "github.com/envoyproxy/go-control-plane/envoy/api/v2"
|
|
envoyroute "github.com/envoyproxy/go-control-plane/envoy/api/v2/route"
|
|
"github.com/hashicorp/consul/agent/proxycfg"
|
|
"github.com/hashicorp/consul/agent/structs"
|
|
)
|
|
|
|
// routesFromSnapshot returns the xDS API representation of the "routes" in the
|
|
// snapshot.
|
|
func routesFromSnapshot(cfgSnap *proxycfg.ConfigSnapshot, _ string) ([]proto.Message, error) {
|
|
if cfgSnap == nil {
|
|
return nil, errors.New("nil config given")
|
|
}
|
|
|
|
switch cfgSnap.Kind {
|
|
case structs.ServiceKindConnectProxy:
|
|
return routesFromSnapshotConnectProxy(cfgSnap)
|
|
case structs.ServiceKindIngressGateway:
|
|
return routesFromSnapshotIngressGateway(cfgSnap)
|
|
default:
|
|
return nil, fmt.Errorf("Invalid service kind: %v", cfgSnap.Kind)
|
|
}
|
|
}
|
|
|
|
// routesFromSnapshotConnectProxy returns the xDS API representation of the
|
|
// "routes" in the snapshot.
|
|
func routesFromSnapshotConnectProxy(cfgSnap *proxycfg.ConfigSnapshot) ([]proto.Message, error) {
|
|
if cfgSnap == nil {
|
|
return nil, errors.New("nil config given")
|
|
}
|
|
|
|
var resources []proto.Message
|
|
for _, u := range cfgSnap.Proxy.Upstreams {
|
|
upstreamID := u.Identifier()
|
|
|
|
var chain *structs.CompiledDiscoveryChain
|
|
if u.DestinationType != structs.UpstreamDestTypePreparedQuery {
|
|
chain = cfgSnap.ConnectProxy.DiscoveryChain[upstreamID]
|
|
}
|
|
|
|
if chain == nil || chain.IsDefault() {
|
|
// TODO(rb): make this do the old school stuff too
|
|
} else {
|
|
virtualHost, err := makeUpstreamRouteForDiscoveryChain(upstreamID, chain, []string{"*"})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
route := &envoy.RouteConfiguration{
|
|
Name: upstreamID,
|
|
VirtualHosts: []envoyroute.VirtualHost{virtualHost},
|
|
// ValidateClusters defaults to true when defined statically and false
|
|
// when done via RDS. Re-set the sane value of true to prevent
|
|
// null-routing traffic.
|
|
ValidateClusters: makeBoolValue(true),
|
|
}
|
|
resources = append(resources, route)
|
|
}
|
|
}
|
|
|
|
// TODO(rb): make sure we don't generate an empty result
|
|
return resources, nil
|
|
}
|
|
|
|
// routesFromSnapshotIngressGateway returns the xDS API representation of the
|
|
// "routes" in the snapshot.
|
|
func routesFromSnapshotIngressGateway(cfgSnap *proxycfg.ConfigSnapshot) ([]proto.Message, error) {
|
|
if cfgSnap == nil {
|
|
return nil, errors.New("nil config given")
|
|
}
|
|
|
|
var result []proto.Message
|
|
for listenerKey, upstreams := range cfgSnap.IngressGateway.Upstreams {
|
|
// Do not create any route configuration for TCP listeners
|
|
if listenerKey.Protocol == "tcp" {
|
|
continue
|
|
}
|
|
|
|
upstreamRoute := &envoy.RouteConfiguration{
|
|
Name: listenerKey.RouteName(),
|
|
// ValidateClusters defaults to true when defined statically and false
|
|
// when done via RDS. Re-set the sane value of true to prevent
|
|
// null-routing traffic.
|
|
ValidateClusters: makeBoolValue(true),
|
|
}
|
|
for _, u := range upstreams {
|
|
upstreamID := u.Identifier()
|
|
chain := cfgSnap.IngressGateway.DiscoveryChain[upstreamID]
|
|
if chain == nil {
|
|
continue
|
|
}
|
|
|
|
namespace := u.GetEnterpriseMeta().NamespaceOrDefault()
|
|
var domains []string
|
|
switch {
|
|
case len(u.IngressHosts) > 0:
|
|
// If a user has specified hosts, do not add the default
|
|
// "<service-name>.ingress.*" prefixes
|
|
domains = u.IngressHosts
|
|
case namespace != structs.IntentionDefaultNamespace:
|
|
domains = []string{fmt.Sprintf("%s.ingress.%s.*", chain.ServiceName, namespace)}
|
|
default:
|
|
domains = []string{fmt.Sprintf("%s.ingress.*", chain.ServiceName)}
|
|
}
|
|
|
|
virtualHost, err := makeUpstreamRouteForDiscoveryChain(upstreamID, chain, domains)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
upstreamRoute.VirtualHosts = append(upstreamRoute.VirtualHosts, virtualHost)
|
|
}
|
|
|
|
result = append(result, upstreamRoute)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func makeUpstreamRouteForDiscoveryChain(
|
|
routeName string,
|
|
chain *structs.CompiledDiscoveryChain,
|
|
serviceDomains []string,
|
|
) (envoyroute.VirtualHost, error) {
|
|
var routes []envoyroute.Route
|
|
|
|
startNode := chain.Nodes[chain.StartNode]
|
|
if startNode == nil {
|
|
panic("missing first node in compiled discovery chain for: " + chain.ServiceName)
|
|
}
|
|
|
|
switch startNode.Type {
|
|
case structs.DiscoveryGraphNodeTypeRouter:
|
|
routes = make([]envoyroute.Route, 0, len(startNode.Routes))
|
|
|
|
for _, discoveryRoute := range startNode.Routes {
|
|
routeMatch := makeRouteMatchForDiscoveryRoute(discoveryRoute, chain.Protocol)
|
|
|
|
var (
|
|
routeAction *envoyroute.Route_Route
|
|
err error
|
|
)
|
|
|
|
nextNode := chain.Nodes[discoveryRoute.NextNode]
|
|
switch nextNode.Type {
|
|
case structs.DiscoveryGraphNodeTypeSplitter:
|
|
routeAction, err = makeRouteActionForSplitter(nextNode.Splits, chain)
|
|
if err != nil {
|
|
return envoyroute.VirtualHost{}, err
|
|
}
|
|
|
|
case structs.DiscoveryGraphNodeTypeResolver:
|
|
routeAction = makeRouteActionForSingleCluster(nextNode.Resolver.Target, chain)
|
|
|
|
default:
|
|
return envoyroute.VirtualHost{}, fmt.Errorf("unexpected graph node after route %q", nextNode.Type)
|
|
}
|
|
|
|
// TODO(rb): Better help handle the envoy case where you need (prefix=/foo/,rewrite=/) and (exact=/foo,rewrite=/) to do a full rewrite
|
|
|
|
destination := discoveryRoute.Definition.Destination
|
|
if destination != nil {
|
|
if destination.PrefixRewrite != "" {
|
|
routeAction.Route.PrefixRewrite = destination.PrefixRewrite
|
|
}
|
|
|
|
if destination.RequestTimeout > 0 {
|
|
routeAction.Route.Timeout = &destination.RequestTimeout
|
|
}
|
|
|
|
if destination.HasRetryFeatures() {
|
|
retryPolicy := &envoyroute.RetryPolicy{}
|
|
if destination.NumRetries > 0 {
|
|
retryPolicy.NumRetries = makeUint32Value(int(destination.NumRetries))
|
|
}
|
|
|
|
// The RetryOn magic values come from: https://www.envoyproxy.io/docs/envoy/v1.10.0/configuration/http_filters/router_filter#config-http-filters-router-x-envoy-retry-on
|
|
if destination.RetryOnConnectFailure {
|
|
retryPolicy.RetryOn = "connect-failure"
|
|
}
|
|
if len(destination.RetryOnStatusCodes) > 0 {
|
|
if retryPolicy.RetryOn != "" {
|
|
retryPolicy.RetryOn = retryPolicy.RetryOn + ",retriable-status-codes"
|
|
} else {
|
|
retryPolicy.RetryOn = "retriable-status-codes"
|
|
}
|
|
retryPolicy.RetriableStatusCodes = destination.RetryOnStatusCodes
|
|
}
|
|
|
|
routeAction.Route.RetryPolicy = retryPolicy
|
|
}
|
|
}
|
|
|
|
routes = append(routes, envoyroute.Route{
|
|
Match: routeMatch,
|
|
Action: routeAction,
|
|
})
|
|
}
|
|
|
|
case structs.DiscoveryGraphNodeTypeSplitter:
|
|
routeAction, err := makeRouteActionForSplitter(startNode.Splits, chain)
|
|
if err != nil {
|
|
return envoyroute.VirtualHost{}, err
|
|
}
|
|
|
|
defaultRoute := envoyroute.Route{
|
|
Match: makeDefaultRouteMatch(),
|
|
Action: routeAction,
|
|
}
|
|
|
|
routes = []envoyroute.Route{defaultRoute}
|
|
|
|
case structs.DiscoveryGraphNodeTypeResolver:
|
|
routeAction := makeRouteActionForSingleCluster(startNode.Resolver.Target, chain)
|
|
|
|
defaultRoute := envoyroute.Route{
|
|
Match: makeDefaultRouteMatch(),
|
|
Action: routeAction,
|
|
}
|
|
|
|
routes = []envoyroute.Route{defaultRoute}
|
|
|
|
default:
|
|
panic("unknown first node in discovery chain of type: " + startNode.Type)
|
|
}
|
|
|
|
host := envoyroute.VirtualHost{
|
|
Name: routeName,
|
|
Domains: serviceDomains,
|
|
Routes: routes,
|
|
}
|
|
|
|
return host, nil
|
|
}
|
|
|
|
func makeRouteMatchForDiscoveryRoute(discoveryRoute *structs.DiscoveryRoute, protocol string) envoyroute.RouteMatch {
|
|
match := discoveryRoute.Definition.Match
|
|
if match == nil || match.IsEmpty() {
|
|
return makeDefaultRouteMatch()
|
|
}
|
|
|
|
em := envoyroute.RouteMatch{}
|
|
|
|
switch {
|
|
case match.HTTP.PathExact != "":
|
|
em.PathSpecifier = &envoyroute.RouteMatch_Path{
|
|
Path: match.HTTP.PathExact,
|
|
}
|
|
case match.HTTP.PathPrefix != "":
|
|
em.PathSpecifier = &envoyroute.RouteMatch_Prefix{
|
|
Prefix: match.HTTP.PathPrefix,
|
|
}
|
|
case match.HTTP.PathRegex != "":
|
|
em.PathSpecifier = &envoyroute.RouteMatch_Regex{
|
|
Regex: match.HTTP.PathRegex,
|
|
}
|
|
default:
|
|
em.PathSpecifier = &envoyroute.RouteMatch_Prefix{
|
|
Prefix: "/",
|
|
}
|
|
}
|
|
|
|
if len(match.HTTP.Header) > 0 {
|
|
em.Headers = make([]*envoyroute.HeaderMatcher, 0, len(match.HTTP.Header))
|
|
for _, hdr := range match.HTTP.Header {
|
|
eh := &envoyroute.HeaderMatcher{
|
|
Name: hdr.Name,
|
|
}
|
|
|
|
switch {
|
|
case hdr.Exact != "":
|
|
eh.HeaderMatchSpecifier = &envoyroute.HeaderMatcher_ExactMatch{
|
|
ExactMatch: hdr.Exact,
|
|
}
|
|
case hdr.Regex != "":
|
|
eh.HeaderMatchSpecifier = &envoyroute.HeaderMatcher_RegexMatch{
|
|
RegexMatch: hdr.Regex,
|
|
}
|
|
case hdr.Prefix != "":
|
|
eh.HeaderMatchSpecifier = &envoyroute.HeaderMatcher_PrefixMatch{
|
|
PrefixMatch: hdr.Prefix,
|
|
}
|
|
case hdr.Suffix != "":
|
|
eh.HeaderMatchSpecifier = &envoyroute.HeaderMatcher_SuffixMatch{
|
|
SuffixMatch: hdr.Suffix,
|
|
}
|
|
case hdr.Present:
|
|
eh.HeaderMatchSpecifier = &envoyroute.HeaderMatcher_PresentMatch{
|
|
PresentMatch: true,
|
|
}
|
|
default:
|
|
continue // skip this impossible situation
|
|
}
|
|
|
|
if hdr.Invert {
|
|
eh.InvertMatch = true
|
|
}
|
|
|
|
em.Headers = append(em.Headers, eh)
|
|
}
|
|
}
|
|
|
|
if len(match.HTTP.Methods) > 0 {
|
|
methodHeaderRegex := strings.Join(match.HTTP.Methods, "|")
|
|
|
|
eh := &envoyroute.HeaderMatcher{
|
|
Name: ":method",
|
|
HeaderMatchSpecifier: &envoyroute.HeaderMatcher_RegexMatch{
|
|
RegexMatch: methodHeaderRegex,
|
|
},
|
|
}
|
|
em.Headers = append(em.Headers, eh)
|
|
}
|
|
|
|
if len(match.HTTP.QueryParam) > 0 {
|
|
em.QueryParameters = make([]*envoyroute.QueryParameterMatcher, 0, len(match.HTTP.QueryParam))
|
|
for _, qm := range match.HTTP.QueryParam {
|
|
eq := &envoyroute.QueryParameterMatcher{
|
|
Name: qm.Name,
|
|
}
|
|
|
|
switch {
|
|
case qm.Exact != "":
|
|
eq.Value = qm.Exact
|
|
case qm.Regex != "":
|
|
eq.Value = qm.Regex
|
|
eq.Regex = makeBoolValue(true)
|
|
case qm.Present:
|
|
eq.Value = ""
|
|
default:
|
|
continue // skip this impossible situation
|
|
}
|
|
|
|
em.QueryParameters = append(em.QueryParameters, eq)
|
|
}
|
|
}
|
|
|
|
return em
|
|
}
|
|
|
|
func makeDefaultRouteMatch() envoyroute.RouteMatch {
|
|
return envoyroute.RouteMatch{
|
|
PathSpecifier: &envoyroute.RouteMatch_Prefix{
|
|
Prefix: "/",
|
|
},
|
|
// TODO(banks) Envoy supports matching only valid GRPC
|
|
// requests which might be nice to add here for gRPC services
|
|
// but it's not supported in our current envoy SDK version
|
|
// although docs say it was supported by 1.8.0. Going to defer
|
|
// that until we've updated the deps.
|
|
}
|
|
}
|
|
|
|
func makeRouteActionForSingleCluster(targetID string, chain *structs.CompiledDiscoveryChain) *envoyroute.Route_Route {
|
|
target := chain.Targets[targetID]
|
|
|
|
clusterName := CustomizeClusterName(target.Name, chain)
|
|
|
|
return &envoyroute.Route_Route{
|
|
Route: &envoyroute.RouteAction{
|
|
ClusterSpecifier: &envoyroute.RouteAction_Cluster{
|
|
Cluster: clusterName,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func makeRouteActionForSplitter(splits []*structs.DiscoverySplit, chain *structs.CompiledDiscoveryChain) (*envoyroute.Route_Route, error) {
|
|
clusters := make([]*envoyroute.WeightedCluster_ClusterWeight, 0, len(splits))
|
|
for _, split := range splits {
|
|
nextNode := chain.Nodes[split.NextNode]
|
|
|
|
if nextNode.Type != structs.DiscoveryGraphNodeTypeResolver {
|
|
return nil, fmt.Errorf("unexpected splitter destination node type: %s", nextNode.Type)
|
|
}
|
|
targetID := nextNode.Resolver.Target
|
|
|
|
target := chain.Targets[targetID]
|
|
|
|
clusterName := CustomizeClusterName(target.Name, chain)
|
|
|
|
// The smallest representable weight is 1/10000 or .01% but envoy
|
|
// deals with integers so scale everything up by 100x.
|
|
cw := &envoyroute.WeightedCluster_ClusterWeight{
|
|
Weight: makeUint32Value(int(split.Weight * 100)),
|
|
Name: clusterName,
|
|
}
|
|
|
|
clusters = append(clusters, cw)
|
|
}
|
|
|
|
return &envoyroute.Route_Route{
|
|
Route: &envoyroute.RouteAction{
|
|
ClusterSpecifier: &envoyroute.RouteAction_WeightedClusters{
|
|
WeightedClusters: &envoyroute.WeightedCluster{
|
|
Clusters: clusters,
|
|
TotalWeight: makeUint32Value(10000), // scaled up 100%
|
|
},
|
|
},
|
|
},
|
|
}, nil
|
|
}
|