2018-10-03 18:18:55 +00:00
package xds
import (
"errors"
2019-07-02 03:10:51 +00:00
"fmt"
2020-07-07 15:43:04 +00:00
"net"
2019-07-24 01:56:39 +00:00
"strings"
2018-10-03 18:18:55 +00:00
2019-07-02 03:10:51 +00:00
envoy "github.com/envoyproxy/go-control-plane/envoy/api/v2"
envoyroute "github.com/envoyproxy/go-control-plane/envoy/api/v2/route"
2020-07-09 22:04:51 +00:00
envoymatcher "github.com/envoyproxy/go-control-plane/envoy/type/matcher"
2020-06-23 20:19:56 +00:00
"github.com/golang/protobuf/proto"
"github.com/golang/protobuf/ptypes"
2018-10-03 18:18:55 +00:00
"github.com/hashicorp/consul/agent/proxycfg"
2019-07-02 03:10:51 +00:00
"github.com/hashicorp/consul/agent/structs"
2018-10-03 18:18:55 +00:00
)
2019-07-02 03:10:51 +00:00
// routesFromSnapshot returns the xDS API representation of the "routes" in the
// snapshot.
2020-07-09 22:04:51 +00:00
func routesFromSnapshot ( cInfo connectionInfo , cfgSnap * proxycfg . ConfigSnapshot ) ( [ ] proto . Message , error ) {
2018-10-03 18:18:55 +00:00
if cfgSnap == nil {
return nil , errors . New ( "nil config given" )
}
2019-07-02 03:10:51 +00:00
switch cfgSnap . Kind {
case structs . ServiceKindConnectProxy :
2020-07-09 22:04:51 +00:00
return routesFromSnapshotConnectProxy ( cInfo , cfgSnap )
2020-04-03 19:41:10 +00:00
case structs . ServiceKindIngressGateway :
2020-07-09 22:04:51 +00:00
return routesFromSnapshotIngressGateway ( cInfo , cfgSnap )
2019-07-02 03:10:51 +00:00
default :
return nil , fmt . Errorf ( "Invalid service kind: %v" , cfgSnap . Kind )
}
}
// routesFromSnapshotConnectProxy returns the xDS API representation of the
// "routes" in the snapshot.
2020-07-09 22:04:51 +00:00
func routesFromSnapshotConnectProxy ( cInfo connectionInfo , cfgSnap * proxycfg . ConfigSnapshot ) ( [ ] proto . Message , error ) {
2019-07-02 03:10:51 +00:00
if cfgSnap == nil {
return nil , errors . New ( "nil config given" )
}
var resources [ ] proto . Message
2020-04-16 23:24:11 +00:00
for _ , u := range cfgSnap . Proxy . Upstreams {
2019-07-02 03:10:51 +00:00
upstreamID := u . Identifier ( )
var chain * structs . CompiledDiscoveryChain
if u . DestinationType != structs . UpstreamDestTypePreparedQuery {
2020-04-16 23:24:11 +00:00
chain = cfgSnap . ConnectProxy . DiscoveryChain [ upstreamID ]
2019-07-02 03:10:51 +00:00
}
if chain == nil || chain . IsDefault ( ) {
// TODO(rb): make this do the old school stuff too
} else {
2020-07-09 22:04:51 +00:00
virtualHost , err := makeUpstreamRouteForDiscoveryChain ( cInfo , upstreamID , chain , [ ] string { "*" } )
2019-07-02 03:10:51 +00:00
if err != nil {
return nil , err
}
2020-04-16 23:24:11 +00:00
route := & envoy . RouteConfiguration {
2020-05-01 14:31:23 +00:00
Name : upstreamID ,
2020-06-23 20:19:56 +00:00
VirtualHosts : [ ] * envoyroute . VirtualHost { virtualHost } ,
2020-05-01 14:31:23 +00:00
// 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.
2020-04-16 23:24:11 +00:00
ValidateClusters : makeBoolValue ( true ) ,
2019-07-02 03:10:51 +00:00
}
2020-04-16 23:24:11 +00:00
resources = append ( resources , route )
2019-07-02 03:10:51 +00:00
}
}
// TODO(rb): make sure we don't generate an empty result
return resources , nil
}
2020-04-16 23:24:11 +00:00
// routesFromSnapshotIngressGateway returns the xDS API representation of the
// "routes" in the snapshot.
2020-07-09 22:04:51 +00:00
func routesFromSnapshotIngressGateway ( cInfo connectionInfo , cfgSnap * proxycfg . ConfigSnapshot ) ( [ ] proto . Message , error ) {
2020-04-16 23:24:11 +00:00
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 ]
2020-04-23 15:06:19 +00:00
if chain == nil {
continue
}
2020-07-07 15:43:04 +00:00
domains := generateUpstreamIngressDomains ( listenerKey , u )
2020-07-09 22:04:51 +00:00
virtualHost , err := makeUpstreamRouteForDiscoveryChain ( cInfo , upstreamID , chain , domains )
2020-04-23 15:06:19 +00:00
if err != nil {
return nil , err
2020-04-16 23:24:11 +00:00
}
2020-04-23 15:06:19 +00:00
upstreamRoute . VirtualHosts = append ( upstreamRoute . VirtualHosts , virtualHost )
2020-04-16 23:24:11 +00:00
}
result = append ( result , upstreamRoute )
}
return result , nil
}
2020-07-07 15:43:04 +00:00
func generateUpstreamIngressDomains ( listenerKey proxycfg . IngressListenerKey , u structs . Upstream ) [ ] string {
var domains [ ] string
domainsSet := make ( map [ string ] bool )
namespace := u . GetEnterpriseMeta ( ) . NamespaceOrDefault ( )
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.*" , u . DestinationName , namespace ) }
default :
domains = [ ] string { fmt . Sprintf ( "%s.ingress.*" , u . DestinationName ) }
}
for _ , h := range domains {
domainsSet [ h ] = true
}
// Host headers may contain port numbers in them, so we need to make sure
// we match on the host with and without the port number. Well-known
// ports like HTTP/HTTPS are stripped from Host headers, but other ports
// will be in the header.
for _ , h := range domains {
_ , _ , err := net . SplitHostPort ( h )
// Error message from Go's net/ipsock.go
// We check to see if a port is not missing, and ignore the
// error from SplitHostPort otherwise, since we have previously
// validated the Host values and should trust the user's input.
if err == nil || ! strings . Contains ( err . Error ( ) , "missing port in address" ) {
continue
}
domainWithPort := fmt . Sprintf ( "%s:%d" , h , listenerKey . Port )
// Do not add a duplicate domain if a hostname with port is already in the
// set
if ! domainsSet [ domainWithPort ] {
domains = append ( domains , domainWithPort )
}
}
return domains
}
2019-07-02 03:10:51 +00:00
func makeUpstreamRouteForDiscoveryChain (
2020-07-09 22:04:51 +00:00
cInfo connectionInfo ,
2020-04-16 23:24:11 +00:00
routeName string ,
2019-07-02 03:10:51 +00:00
chain * structs . CompiledDiscoveryChain ,
2020-04-23 15:06:19 +00:00
serviceDomains [ ] string ,
2020-06-23 20:19:56 +00:00
) ( * envoyroute . VirtualHost , error ) {
var routes [ ] * envoyroute . Route
2019-07-02 03:10:51 +00:00
2019-08-02 03:44:05 +00:00
startNode := chain . Nodes [ chain . StartNode ]
if startNode == nil {
2020-08-12 16:19:20 +00:00
return nil , fmt . Errorf ( "missing first node in compiled discovery chain for: %s" , chain . ServiceName )
2019-08-02 03:44:05 +00:00
}
switch startNode . Type {
2019-07-02 03:10:51 +00:00
case structs . DiscoveryGraphNodeTypeRouter :
2020-06-23 20:19:56 +00:00
routes = make ( [ ] * envoyroute . Route , 0 , len ( startNode . Routes ) )
2019-07-02 03:10:51 +00:00
2019-08-02 03:44:05 +00:00
for _ , discoveryRoute := range startNode . Routes {
2020-07-09 22:04:51 +00:00
routeMatch := makeRouteMatchForDiscoveryRoute ( cInfo , discoveryRoute )
2019-07-02 03:10:51 +00:00
var (
routeAction * envoyroute . Route_Route
err error
)
2019-08-02 03:44:05 +00:00
nextNode := chain . Nodes [ discoveryRoute . NextNode ]
switch nextNode . Type {
case structs . DiscoveryGraphNodeTypeSplitter :
2020-04-03 19:41:10 +00:00
routeAction , err = makeRouteActionForSplitter ( nextNode . Splits , chain )
2019-07-02 03:10:51 +00:00
if err != nil {
2020-06-23 20:19:56 +00:00
return nil , err
2019-07-02 03:10:51 +00:00
}
2019-08-02 03:44:05 +00:00
case structs . DiscoveryGraphNodeTypeResolver :
2020-04-03 19:41:10 +00:00
routeAction = makeRouteActionForSingleCluster ( nextNode . Resolver . Target , chain )
2019-07-02 03:10:51 +00:00
2019-08-02 03:44:05 +00:00
default :
2020-06-23 20:19:56 +00:00
return nil , fmt . Errorf ( "unexpected graph node after route %q" , nextNode . Type )
2019-07-02 03:10:51 +00:00
}
2019-07-12 19:16:21 +00:00
// 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 {
2020-06-23 20:19:56 +00:00
routeAction . Route . Timeout = ptypes . DurationProto ( destination . RequestTimeout )
2019-07-12 19:16:21 +00:00
}
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 != "" {
2020-02-18 15:25:47 +00:00
retryPolicy . RetryOn = retryPolicy . RetryOn + ",retriable-status-codes"
2019-07-12 19:16:21 +00:00
} else {
retryPolicy . RetryOn = "retriable-status-codes"
}
retryPolicy . RetriableStatusCodes = destination . RetryOnStatusCodes
}
routeAction . Route . RetryPolicy = retryPolicy
}
}
2020-06-23 20:19:56 +00:00
routes = append ( routes , & envoyroute . Route {
2019-07-02 03:10:51 +00:00
Match : routeMatch ,
Action : routeAction ,
} )
}
case structs . DiscoveryGraphNodeTypeSplitter :
2020-04-03 19:41:10 +00:00
routeAction , err := makeRouteActionForSplitter ( startNode . Splits , chain )
2019-07-02 03:10:51 +00:00
if err != nil {
2020-06-23 20:19:56 +00:00
return nil , err
2019-07-02 03:10:51 +00:00
}
2020-06-23 20:19:56 +00:00
defaultRoute := & envoyroute . Route {
2019-07-02 03:10:51 +00:00
Match : makeDefaultRouteMatch ( ) ,
Action : routeAction ,
}
2020-06-23 20:19:56 +00:00
routes = [ ] * envoyroute . Route { defaultRoute }
2019-07-02 03:10:51 +00:00
2019-08-02 03:44:05 +00:00
case structs . DiscoveryGraphNodeTypeResolver :
2020-04-03 19:41:10 +00:00
routeAction := makeRouteActionForSingleCluster ( startNode . Resolver . Target , chain )
2019-07-02 03:10:51 +00:00
2020-06-23 20:19:56 +00:00
defaultRoute := & envoyroute . Route {
2019-07-02 03:10:51 +00:00
Match : makeDefaultRouteMatch ( ) ,
Action : routeAction ,
}
2020-06-23 20:19:56 +00:00
routes = [ ] * envoyroute . Route { defaultRoute }
2019-07-02 03:10:51 +00:00
default :
2020-08-12 16:19:20 +00:00
return nil , fmt . Errorf ( "unknown first node in discovery chain of type: %s" , startNode . Type )
2019-07-02 03:10:51 +00:00
}
2020-06-23 20:19:56 +00:00
host := & envoyroute . VirtualHost {
2020-04-16 23:24:11 +00:00
Name : routeName ,
2020-04-23 15:06:19 +00:00
Domains : serviceDomains ,
2020-04-16 23:24:11 +00:00
Routes : routes ,
}
return host , nil
2019-07-02 03:10:51 +00:00
}
2020-07-31 20:52:49 +00:00
func makeRouteMatchForDiscoveryRoute ( _ connectionInfo , discoveryRoute * structs . DiscoveryRoute ) * envoyroute . RouteMatch {
2019-07-02 03:10:51 +00:00
match := discoveryRoute . Definition . Match
if match == nil || match . IsEmpty ( ) {
return makeDefaultRouteMatch ( )
}
2020-06-23 20:19:56 +00:00
em := & envoyroute . RouteMatch { }
2019-07-02 03:10:51 +00:00
switch {
case match . HTTP . PathExact != "" :
2019-07-12 19:16:21 +00:00
em . PathSpecifier = & envoyroute . RouteMatch_Path {
Path : match . HTTP . PathExact ,
}
2019-07-02 03:10:51 +00:00
case match . HTTP . PathPrefix != "" :
2019-07-12 19:16:21 +00:00
em . PathSpecifier = & envoyroute . RouteMatch_Prefix {
Prefix : match . HTTP . PathPrefix ,
}
2019-07-02 03:10:51 +00:00
case match . HTTP . PathRegex != "" :
2020-07-31 20:52:49 +00:00
em . PathSpecifier = & envoyroute . RouteMatch_SafeRegex {
SafeRegex : makeEnvoyRegexMatch ( match . HTTP . PathRegex ) ,
2019-07-12 19:16:21 +00:00
}
2019-07-02 03:10:51 +00:00
default :
2019-07-12 19:16:21 +00:00
em . PathSpecifier = & envoyroute . RouteMatch_Prefix {
Prefix : "/" ,
}
2019-07-02 03:10:51 +00:00
}
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 != "" :
2020-07-31 20:52:49 +00:00
eh . HeaderMatchSpecifier = & envoyroute . HeaderMatcher_SafeRegexMatch {
SafeRegexMatch : makeEnvoyRegexMatch ( hdr . Regex ) ,
2019-07-02 03:10:51 +00:00
}
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 )
}
}
2019-07-24 01:56:39 +00:00
if len ( match . HTTP . Methods ) > 0 {
methodHeaderRegex := strings . Join ( match . HTTP . Methods , "|" )
eh := & envoyroute . HeaderMatcher {
Name : ":method" ,
2020-07-31 20:52:49 +00:00
HeaderMatchSpecifier : & envoyroute . HeaderMatcher_SafeRegexMatch {
2020-07-09 22:04:51 +00:00
SafeRegexMatch : makeEnvoyRegexMatch ( methodHeaderRegex ) ,
2020-07-31 20:52:49 +00:00
} ,
2019-07-24 01:56:39 +00:00
}
2020-07-09 22:04:51 +00:00
2019-07-24 01:56:39 +00:00
em . Headers = append ( em . Headers , eh )
}
2019-07-02 03:10:51 +00:00
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 {
2019-07-24 01:55:26 +00:00
Name : qm . Name ,
}
switch {
case qm . Exact != "" :
2020-07-31 20:52:49 +00:00
eq . QueryParameterMatchSpecifier = & envoyroute . QueryParameterMatcher_StringMatch {
StringMatch : & envoymatcher . StringMatcher {
MatchPattern : & envoymatcher . StringMatcher_Exact {
Exact : qm . Exact ,
2020-07-09 22:04:51 +00:00
} ,
2020-07-31 20:52:49 +00:00
} ,
2020-07-09 22:04:51 +00:00
}
2019-07-24 01:55:26 +00:00
case qm . Regex != "" :
2020-07-31 20:52:49 +00:00
eq . QueryParameterMatchSpecifier = & envoyroute . QueryParameterMatcher_StringMatch {
StringMatch : & envoymatcher . StringMatcher {
MatchPattern : & envoymatcher . StringMatcher_SafeRegex {
SafeRegex : makeEnvoyRegexMatch ( qm . Regex ) ,
2020-07-09 22:04:51 +00:00
} ,
2020-07-31 20:52:49 +00:00
} ,
2020-07-09 22:04:51 +00:00
}
2019-07-24 01:55:26 +00:00
case qm . Present :
2020-07-31 20:52:49 +00:00
eq . QueryParameterMatchSpecifier = & envoyroute . QueryParameterMatcher_PresentMatch {
PresentMatch : true ,
2020-07-09 22:04:51 +00:00
}
2019-07-24 01:55:26 +00:00
default :
continue // skip this impossible situation
2019-07-02 03:10:51 +00:00
}
em . QueryParameters = append ( em . QueryParameters , eq )
}
}
return em
}
2020-06-23 20:19:56 +00:00
func makeDefaultRouteMatch ( ) * envoyroute . RouteMatch {
return & envoyroute . RouteMatch {
2019-07-02 03:10:51 +00:00
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.
}
}
2020-04-03 19:41:10 +00:00
func makeRouteActionForSingleCluster ( targetID string , chain * structs . CompiledDiscoveryChain ) * envoyroute . Route_Route {
2019-08-02 20:34:54 +00:00
target := chain . Targets [ targetID ]
2019-08-19 18:03:03 +00:00
clusterName := CustomizeClusterName ( target . Name , chain )
2019-07-02 03:10:51 +00:00
return & envoyroute . Route_Route {
Route : & envoyroute . RouteAction {
ClusterSpecifier : & envoyroute . RouteAction_Cluster {
Cluster : clusterName ,
} ,
} ,
}
}
2020-04-03 19:41:10 +00:00
func makeRouteActionForSplitter ( splits [ ] * structs . DiscoverySplit , chain * structs . CompiledDiscoveryChain ) ( * envoyroute . Route_Route , error ) {
2019-07-02 03:10:51 +00:00
clusters := make ( [ ] * envoyroute . WeightedCluster_ClusterWeight , 0 , len ( splits ) )
for _ , split := range splits {
2019-08-02 03:44:05 +00:00
nextNode := chain . Nodes [ split . NextNode ]
if nextNode . Type != structs . DiscoveryGraphNodeTypeResolver {
return nil , fmt . Errorf ( "unexpected splitter destination node type: %s" , nextNode . Type )
2019-07-02 03:10:51 +00:00
}
2019-08-02 20:34:54 +00:00
targetID := nextNode . Resolver . Target
target := chain . Targets [ targetID ]
2019-08-02 03:03:34 +00:00
2019-08-19 18:03:03 +00:00
clusterName := CustomizeClusterName ( target . Name , chain )
2019-07-02 03:10:51 +00:00
2019-07-12 19:16:21 +00:00
// The smallest representable weight is 1/10000 or .01% but envoy
// deals with integers so scale everything up by 100x.
2019-07-02 03:10:51 +00:00
cw := & envoyroute . WeightedCluster_ClusterWeight {
2019-07-12 19:16:21 +00:00
Weight : makeUint32Value ( int ( split . Weight * 100 ) ) ,
2019-07-02 03:10:51 +00:00
Name : clusterName ,
}
clusters = append ( clusters , cw )
}
return & envoyroute . Route_Route {
Route : & envoyroute . RouteAction {
ClusterSpecifier : & envoyroute . RouteAction_WeightedClusters {
WeightedClusters : & envoyroute . WeightedCluster {
Clusters : clusters ,
2019-07-12 19:16:21 +00:00
TotalWeight : makeUint32Value ( 10000 ) , // scaled up 100%
2019-07-02 03:10:51 +00:00
} ,
} ,
} ,
} , nil
2018-10-03 18:18:55 +00:00
}