package xds import ( "errors" "fmt" "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, token string) ([]proto.Message, error) { if cfgSnap == nil { return nil, errors.New("nil config given") } switch cfgSnap.Kind { case structs.ServiceKindConnectProxy: return routesFromSnapshotConnectProxy(cfgSnap, token) 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, token string) ([]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 { upstreamRoute, err := makeUpstreamRouteForDiscoveryChain(&u, chain, cfgSnap) if err != nil { return nil, err } if upstreamRoute != nil { resources = append(resources, upstreamRoute) } } } // TODO(rb): make sure we don't generate an empty result return resources, nil } func makeUpstreamRouteForDiscoveryChain( u *structs.Upstream, chain *structs.CompiledDiscoveryChain, cfgSnap *proxycfg.ConfigSnapshot, ) (*envoy.RouteConfiguration, error) { upstreamID := u.Identifier() routeName := upstreamID var routes []envoyroute.Route switch chain.Node.Type { case structs.DiscoveryGraphNodeTypeRouter: routes = make([]envoyroute.Route, 0, len(chain.Node.Routes)) for _, discoveryRoute := range chain.Node.Routes { routeMatch := makeRouteMatchForDiscoveryRoute(discoveryRoute, chain.Protocol) // TODO(rb): handle PrefixRewrite // TODO(rb): handle RequestTimeout // TODO(rb): handle Retries var ( routeAction *envoyroute.Route_Route err error ) next := discoveryRoute.DestinationNode if next.Type == structs.DiscoveryGraphNodeTypeSplitter { routeAction, err = makeRouteActionForSplitter(upstreamID, cfgSnap.Datacenter, next.Splits) if err != nil { return nil, err } } else if next.Type == structs.DiscoveryGraphNodeTypeGroupResolver { groupResolver := next.GroupResolver routeAction = makeRouteActionForSingleCluster(upstreamID, cfgSnap.Datacenter, groupResolver.Target) } else { return nil, fmt.Errorf("unexpected graph node after route %q", next.Type) } routes = append(routes, envoyroute.Route{ Match: routeMatch, Action: routeAction, }) } case structs.DiscoveryGraphNodeTypeSplitter: routeAction, err := makeRouteActionForSplitter(upstreamID, cfgSnap.Datacenter, chain.Node.Splits) if err != nil { return nil, err } defaultRoute := envoyroute.Route{ Match: makeDefaultRouteMatch(), Action: routeAction, } routes = []envoyroute.Route{defaultRoute} case structs.DiscoveryGraphNodeTypeGroupResolver: groupResolver := chain.Node.GroupResolver routeAction := makeRouteActionForSingleCluster(upstreamID, cfgSnap.Datacenter, groupResolver.Target) defaultRoute := envoyroute.Route{ Match: makeDefaultRouteMatch(), Action: routeAction, } routes = []envoyroute.Route{defaultRoute} default: panic("unknown top node in discovery chain of type: " + chain.Node.Type) } return &envoy.RouteConfiguration{ Name: routeName, VirtualHosts: []envoyroute.VirtualHost{ envoyroute.VirtualHost{ Name: routeName, Domains: []string{"*"}, Routes: routes, }, }, // 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), }, nil } func makeRouteMatchForDiscoveryRoute(discoveryRoute *structs.DiscoveryRoute, protocol string) envoyroute.RouteMatch { switch protocol { case "http", "http2": // The only match stanza is HTTP. default: return makeDefaultRouteMatch() } 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: "/"} case match.HTTP.PathPrefix != "": em.PathSpecifier = &envoyroute.RouteMatch_Prefix{Prefix: "/"} case match.HTTP.PathRegex != "": em.PathSpecifier = &envoyroute.RouteMatch_Regex{Regex: "/"} 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, } case hdr.Invert: // THIS HAS TO BE LAST eh.HeaderMatchSpecifier = &envoyroute.HeaderMatcher_PresentMatch{ // We set this to the misleading value of 'true' here // because we'll generically invert it next. PresentMatch: true, } default: continue // skip this impossible situation } if hdr.Invert { eh.InvertMatch = true } 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, Value: qm.Value, Regex: makeBoolValue(qm.Regex), } 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(upstreamID, currentDatacenter string, target structs.DiscoveryTarget) *envoyroute.Route_Route { clusterName := makeClusterName(upstreamID, target, currentDatacenter) return &envoyroute.Route_Route{ Route: &envoyroute.RouteAction{ ClusterSpecifier: &envoyroute.RouteAction_Cluster{ Cluster: clusterName, }, }, } } func makeRouteActionForSplitter(upstreamID, currentDatacenter string, splits []*structs.DiscoverySplit) (*envoyroute.Route_Route, error) { clusters := make([]*envoyroute.WeightedCluster_ClusterWeight, 0, len(splits)) for _, split := range splits { if split.Node.Type != structs.DiscoveryGraphNodeTypeGroupResolver { return nil, fmt.Errorf("unexpected splitter destination node type: %s", split.Node.Type) } groupResolver := split.Node.GroupResolver target := groupResolver.Target clusterName := makeClusterName(upstreamID, target, currentDatacenter) // TODO(rb): scale up by 100 and adjust total weight cw := &envoyroute.WeightedCluster_ClusterWeight{ Weight: makeUint32Value(int(split.Weight)), Name: clusterName, } clusters = append(clusters, cw) } return &envoyroute.Route_Route{ Route: &envoyroute.RouteAction{ ClusterSpecifier: &envoyroute.RouteAction_WeightedClusters{ WeightedClusters: &envoyroute.WeightedCluster{ Clusters: clusters, TotalWeight: makeUint32Value(100), }, }, }, }, nil }