consul/agent/xdsv2/route_resources.go

545 lines
20 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package xdsv2
import (
"fmt"
"strings"
envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
envoy_matcher_v3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
"google.golang.org/protobuf/types/known/wrapperspb"
"github.com/hashicorp/consul/agent/xds/response"
"github.com/hashicorp/consul/envoyextensions/xdscommon"
"github.com/hashicorp/consul/proto-public/pbmesh/v2beta1/pbproxystate"
envoy_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
"google.golang.org/protobuf/proto"
)
func (pr *ProxyResources) makeXDSRoutes() ([]proto.Message, error) {
routes := make([]proto.Message, 0)
for name, r := range pr.proxyState.Routes {
protoRoute := pr.makeEnvoyRouteConfigFromProxystateRoute(name, r)
// TODO: aggregate errors for routes and still return any properly formed routes.
routes = append(routes, protoRoute)
}
return routes, nil
}
func (pr *ProxyResources) makeEnvoyRoute(name string) (*envoy_route_v3.RouteConfiguration, error) {
var route *envoy_route_v3.RouteConfiguration
// TODO(proxystate): This will make routes in the future. This function should distinguish between static routes
// inlined into listeners and non-static routes that should be added as top level Envoy resources.
_, ok := pr.proxyState.Routes[name]
if !ok {
// This should not happen with a valid proxy state.
return nil, fmt.Errorf("could not find route in ProxyState: %s", name)
}
return route, nil
}
// makeEnvoyRouteConfigFromProxystateRoute converts the proxystate representation of a Route into Envoy proto message
// form. We don't throw any errors here, since the proxystate has already been validated.
func (pr *ProxyResources) makeEnvoyRouteConfigFromProxystateRoute(name string, psRoute *pbproxystate.Route) *envoy_route_v3.RouteConfiguration {
envoyRouteConfig := &envoy_route_v3.RouteConfiguration{
Name: name,
// ValidateClusters defaults to true when defined statically and false
// when done via RDS. Re-set the reasonable value of true to prevent
// null-routing traffic.
ValidateClusters: response.MakeBoolValue(true),
}
for _, vh := range psRoute.GetVirtualHosts() {
envoyRouteConfig.VirtualHosts = append(envoyRouteConfig.VirtualHosts, pr.makeEnvoyVHFromProxystateVH(vh))
}
return envoyRouteConfig
}
func (pr *ProxyResources) makeEnvoyVHFromProxystateVH(psVirtualHost *pbproxystate.VirtualHost) *envoy_route_v3.VirtualHost {
envoyVirtualHost := &envoy_route_v3.VirtualHost{
Name: psVirtualHost.Name,
Domains: psVirtualHost.GetDomains(),
}
for _, rr := range psVirtualHost.GetRouteRules() {
envoyVirtualHost.Routes = append(envoyVirtualHost.Routes, pr.makeEnvoyRouteFromProxystateRouteRule(rr))
}
for _, hm := range psVirtualHost.GetHeaderMutations() {
injectEnvoyVirtualHostWithProxystateHeaderMutation(envoyVirtualHost, hm)
}
return envoyVirtualHost
}
func (pr *ProxyResources) makeEnvoyRouteFromProxystateRouteRule(psRouteRule *pbproxystate.RouteRule) *envoy_route_v3.Route {
envoyRouteRule := &envoy_route_v3.Route{
Match: makeEnvoyRouteMatchFromProxystateRouteMatch(psRouteRule.GetMatch()),
Action: pr.makeEnvoyRouteActionFromProxystateRouteDestination(psRouteRule.GetDestination()),
}
for _, hm := range psRouteRule.GetHeaderMutations() {
injectEnvoyRouteRuleWithProxystateHeaderMutation(envoyRouteRule, hm)
}
return envoyRouteRule
}
func makeEnvoyRouteMatchFromProxystateRouteMatch(psRouteMatch *pbproxystate.RouteMatch) *envoy_route_v3.RouteMatch {
envoyRouteMatch := &envoy_route_v3.RouteMatch{}
switch psRouteMatch.PathMatch.GetPathMatch().(type) {
case *pbproxystate.PathMatch_Exact:
envoyRouteMatch.PathSpecifier = &envoy_route_v3.RouteMatch_Path{
Path: psRouteMatch.PathMatch.GetExact(),
}
case *pbproxystate.PathMatch_Prefix:
envoyRouteMatch.PathSpecifier = &envoy_route_v3.RouteMatch_Prefix{
Prefix: psRouteMatch.PathMatch.GetPrefix(),
}
case *pbproxystate.PathMatch_Regex:
envoyRouteMatch.PathSpecifier = &envoy_route_v3.RouteMatch_SafeRegex{
SafeRegex: makeEnvoyRegexMatch(psRouteMatch.PathMatch.GetRegex()),
}
default:
// This shouldn't be possible considering the types of PathMatch
return nil
}
if len(psRouteMatch.GetHeaderMatches()) > 0 {
envoyRouteMatch.Headers = make([]*envoy_route_v3.HeaderMatcher, 0, len(psRouteMatch.GetHeaderMatches()))
}
for _, psHM := range psRouteMatch.GetHeaderMatches() {
envoyRouteMatch.Headers = append(envoyRouteMatch.Headers, makeEnvoyHeaderMatcherFromProxystateHeaderMatch(psHM))
}
if len(psRouteMatch.MethodMatches) > 0 {
methodHeaderRegex := strings.Join(psRouteMatch.MethodMatches, "|")
eh := &envoy_route_v3.HeaderMatcher{
Name: ":method",
HeaderMatchSpecifier: &envoy_route_v3.HeaderMatcher_SafeRegexMatch{
SafeRegexMatch: makeEnvoyRegexMatch(methodHeaderRegex),
},
}
envoyRouteMatch.Headers = append(envoyRouteMatch.Headers, eh)
}
if len(psRouteMatch.GetQueryParameterMatches()) > 0 {
envoyRouteMatch.QueryParameters = make([]*envoy_route_v3.QueryParameterMatcher, 0, len(psRouteMatch.GetQueryParameterMatches()))
}
for _, psQM := range psRouteMatch.GetQueryParameterMatches() {
envoyRouteMatch.QueryParameters = append(envoyRouteMatch.QueryParameters, makeEnvoyQueryParamFromProxystateQueryMatch(psQM))
}
return envoyRouteMatch
}
func makeEnvoyRegexMatch(pattern string) *envoy_matcher_v3.RegexMatcher {
return &envoy_matcher_v3.RegexMatcher{
EngineType: &envoy_matcher_v3.RegexMatcher_GoogleRe2{
GoogleRe2: &envoy_matcher_v3.RegexMatcher_GoogleRE2{},
},
Regex: pattern,
}
}
func makeEnvoyHeaderMatcherFromProxystateHeaderMatch(psMatch *pbproxystate.HeaderMatch) *envoy_route_v3.HeaderMatcher {
envoyHeaderMatcher := &envoy_route_v3.HeaderMatcher{
Name: psMatch.Name,
}
switch psMatch.Match.(type) {
case *pbproxystate.HeaderMatch_Exact:
envoyHeaderMatcher.HeaderMatchSpecifier = &envoy_route_v3.HeaderMatcher_StringMatch{
StringMatch: &envoy_matcher_v3.StringMatcher{
MatchPattern: &envoy_matcher_v3.StringMatcher_Exact{
Exact: psMatch.GetExact(),
},
IgnoreCase: false,
},
}
case *pbproxystate.HeaderMatch_Regex:
envoyHeaderMatcher.HeaderMatchSpecifier = &envoy_route_v3.HeaderMatcher_StringMatch{
StringMatch: &envoy_matcher_v3.StringMatcher{
MatchPattern: &envoy_matcher_v3.StringMatcher_SafeRegex{
SafeRegex: response.MakeEnvoyRegexMatch(psMatch.GetRegex()),
},
IgnoreCase: false,
},
}
case *pbproxystate.HeaderMatch_Prefix:
envoyHeaderMatcher.HeaderMatchSpecifier = &envoy_route_v3.HeaderMatcher_StringMatch{
StringMatch: &envoy_matcher_v3.StringMatcher{
MatchPattern: &envoy_matcher_v3.StringMatcher_Prefix{
Prefix: psMatch.GetPrefix(),
},
IgnoreCase: false,
},
}
case *pbproxystate.HeaderMatch_Suffix:
envoyHeaderMatcher.HeaderMatchSpecifier = &envoy_route_v3.HeaderMatcher_StringMatch{
StringMatch: &envoy_matcher_v3.StringMatcher{
MatchPattern: &envoy_matcher_v3.StringMatcher_Suffix{
Suffix: psMatch.GetSuffix(),
},
IgnoreCase: false,
},
}
case *pbproxystate.HeaderMatch_Present:
envoyHeaderMatcher.HeaderMatchSpecifier = &envoy_route_v3.HeaderMatcher_PresentMatch{
PresentMatch: true,
}
default:
// This shouldn't be possible considering the types of HeaderMatch
return nil
}
if psMatch.GetInvertMatch() {
envoyHeaderMatcher.InvertMatch = true
}
return envoyHeaderMatcher
}
func makeEnvoyQueryParamFromProxystateQueryMatch(psMatch *pbproxystate.QueryParameterMatch) *envoy_route_v3.QueryParameterMatcher {
envoyQueryParamMatcher := &envoy_route_v3.QueryParameterMatcher{
Name: psMatch.Name,
}
switch psMatch.Match.(type) {
case *pbproxystate.QueryParameterMatch_Exact:
envoyQueryParamMatcher.QueryParameterMatchSpecifier = &envoy_route_v3.QueryParameterMatcher_StringMatch{
StringMatch: &envoy_matcher_v3.StringMatcher{
MatchPattern: &envoy_matcher_v3.StringMatcher_Exact{
Exact: psMatch.GetExact(),
},
},
}
case *pbproxystate.QueryParameterMatch_Regex:
envoyQueryParamMatcher.QueryParameterMatchSpecifier = &envoy_route_v3.QueryParameterMatcher_StringMatch{
StringMatch: &envoy_matcher_v3.StringMatcher{
MatchPattern: &envoy_matcher_v3.StringMatcher_SafeRegex{
SafeRegex: makeEnvoyRegexMatch(psMatch.GetRegex()),
},
},
}
case *pbproxystate.QueryParameterMatch_Present:
envoyQueryParamMatcher.QueryParameterMatchSpecifier = &envoy_route_v3.QueryParameterMatcher_PresentMatch{
PresentMatch: true,
}
default:
// This shouldn't be possible considering the types of QueryMatch
return nil
}
return envoyQueryParamMatcher
}
// TODO (dans): Will this always be envoy_route_v3.Route_Route?
// Definitely for connect proxies this is the only option.
func (pr *ProxyResources) makeEnvoyRouteActionFromProxystateRouteDestination(psRouteDestination *pbproxystate.RouteDestination) *envoy_route_v3.Route_Route {
envoyRouteRoute := &envoy_route_v3.Route_Route{
Route: &envoy_route_v3.RouteAction{},
}
switch psRouteDestination.Destination.(type) {
case *pbproxystate.RouteDestination_Cluster:
psCluster := psRouteDestination.GetCluster()
envoyRouteRoute.Route.ClusterSpecifier = &envoy_route_v3.RouteAction_Cluster{
Cluster: psCluster.GetName(),
}
clusters, _ := pr.makeClusters(psCluster.Name)
pr.envoyResources[xdscommon.ClusterType] = append(pr.envoyResources[xdscommon.ClusterType], clusters...)
case *pbproxystate.RouteDestination_WeightedClusters:
psWeightedClusters := psRouteDestination.GetWeightedClusters()
envoyClusters := make([]*envoy_route_v3.WeightedCluster_ClusterWeight, 0, len(psWeightedClusters.GetClusters()))
totalWeight := 0
for _, psCluster := range psWeightedClusters.GetClusters() {
clusters, _ := pr.makeClusters(psCluster.Name)
pr.envoyResources[xdscommon.ClusterType] = append(pr.envoyResources[xdscommon.ClusterType], clusters...)
totalWeight += int(psCluster.Weight.GetValue())
envoyClusters = append(envoyClusters, makeEnvoyClusterWeightFromProxystateWeightedCluster(psCluster))
}
var envoyWeightScale *wrapperspb.UInt32Value
if totalWeight == 10000 {
envoyWeightScale = response.MakeUint32Value(10000)
}
envoyRouteRoute.Route.ClusterSpecifier = &envoy_route_v3.RouteAction_WeightedClusters{
WeightedClusters: &envoy_route_v3.WeightedCluster{
Clusters: envoyClusters,
TotalWeight: envoyWeightScale,
},
}
default:
// This shouldn't be possible considering the types of Destination
return nil
}
injectEnvoyRouteActionWithProxystateDestinationConfig(envoyRouteRoute.Route, psRouteDestination.GetDestinationConfiguration())
if psRouteDestination.GetDestinationConfiguration() != nil {
config := psRouteDestination.GetDestinationConfiguration()
action := envoyRouteRoute.Route
action.PrefixRewrite = config.GetPrefixRewrite()
if config.GetTimeoutConfig().GetTimeout() != nil {
action.Timeout = config.GetTimeoutConfig().GetTimeout()
}
if config.GetTimeoutConfig().GetTimeout() != nil {
action.Timeout = config.GetTimeoutConfig().GetTimeout()
}
if config.GetTimeoutConfig().GetIdleTimeout() != nil {
action.IdleTimeout = config.GetTimeoutConfig().GetIdleTimeout()
}
if config.GetRetryPolicy() != nil {
action.RetryPolicy = makeEnvoyRetryPolicyFromProxystateRetryPolicy(config.GetRetryPolicy())
}
}
return envoyRouteRoute
}
func makeEnvoyClusterWeightFromProxystateWeightedCluster(cluster *pbproxystate.L7WeightedDestinationCluster) *envoy_route_v3.WeightedCluster_ClusterWeight {
envoyClusterWeight := &envoy_route_v3.WeightedCluster_ClusterWeight{
Name: cluster.GetName(),
Weight: cluster.GetWeight(),
}
for _, hm := range cluster.GetHeaderMutations() {
injectEnvoyClusterWeightWithProxystateHeaderMutation(envoyClusterWeight, hm)
}
return envoyClusterWeight
}
func injectEnvoyClusterWeightWithProxystateHeaderMutation(envoyClusterWeight *envoy_route_v3.WeightedCluster_ClusterWeight, mutation *pbproxystate.HeaderMutation) {
mutation.GetAction()
switch mutation.GetAction().(type) {
case *pbproxystate.HeaderMutation_RequestHeaderAdd:
action := mutation.GetRequestHeaderAdd()
header := action.GetHeader()
app := action.GetAppendAction() == pbproxystate.AppendAction_APPEND_ACTION_APPEND_IF_EXISTS_OR_ADD
hvo := &envoy_core_v3.HeaderValueOption{
Header: &envoy_core_v3.HeaderValue{
Key: header.GetKey(),
Value: header.GetValue(),
},
Append: response.MakeBoolValue(app),
}
envoyClusterWeight.RequestHeadersToAdd = append(envoyClusterWeight.RequestHeadersToAdd, hvo)
case *pbproxystate.HeaderMutation_RequestHeaderRemove:
action := mutation.GetRequestHeaderRemove()
envoyClusterWeight.RequestHeadersToRemove = append(envoyClusterWeight.RequestHeadersToRemove, action.GetHeaderKeys()...)
case *pbproxystate.HeaderMutation_ResponseHeaderAdd:
action := mutation.GetResponseHeaderAdd()
header := action.GetHeader()
app := action.GetAppendAction() == pbproxystate.AppendAction_APPEND_ACTION_APPEND_IF_EXISTS_OR_ADD
hvo := &envoy_core_v3.HeaderValueOption{
Header: &envoy_core_v3.HeaderValue{
Key: header.GetKey(),
Value: header.GetValue(),
},
Append: response.MakeBoolValue(app),
}
envoyClusterWeight.ResponseHeadersToAdd = append(envoyClusterWeight.ResponseHeadersToAdd, hvo)
case *pbproxystate.HeaderMutation_ResponseHeaderRemove:
action := mutation.GetResponseHeaderRemove()
envoyClusterWeight.ResponseHeadersToRemove = append(envoyClusterWeight.ResponseHeadersToRemove, action.GetHeaderKeys()...)
default:
// This shouldn't be possible considering the types of Destination
return
}
}
func injectEnvoyRouteActionWithProxystateDestinationConfig(envoyAction *envoy_route_v3.RouteAction, config *pbproxystate.DestinationConfiguration) {
if config == nil {
return
}
if len(config.GetHashPolicies()) > 0 {
envoyAction.HashPolicy = make([]*envoy_route_v3.RouteAction_HashPolicy, 0, len(config.GetHashPolicies()))
}
for _, policy := range config.GetHashPolicies() {
envoyPolicy := makeEnvoyHashPolicyFromProxystateLBHashPolicy(policy)
envoyAction.HashPolicy = append(envoyAction.HashPolicy, envoyPolicy)
}
if config.AutoHostRewrite != nil {
envoyAction.HostRewriteSpecifier = &envoy_route_v3.RouteAction_AutoHostRewrite{
AutoHostRewrite: config.AutoHostRewrite,
}
}
}
func makeEnvoyHashPolicyFromProxystateLBHashPolicy(psPolicy *pbproxystate.LoadBalancerHashPolicy) *envoy_route_v3.RouteAction_HashPolicy {
switch psPolicy.GetPolicy().(type) {
case *pbproxystate.LoadBalancerHashPolicy_ConnectionProperties:
return &envoy_route_v3.RouteAction_HashPolicy{
PolicySpecifier: &envoy_route_v3.RouteAction_HashPolicy_ConnectionProperties_{
ConnectionProperties: &envoy_route_v3.RouteAction_HashPolicy_ConnectionProperties{
SourceIp: true, // always true
},
},
Terminal: psPolicy.GetConnectionProperties().GetTerminal(),
}
case *pbproxystate.LoadBalancerHashPolicy_Header:
return &envoy_route_v3.RouteAction_HashPolicy{
PolicySpecifier: &envoy_route_v3.RouteAction_HashPolicy_Header_{
Header: &envoy_route_v3.RouteAction_HashPolicy_Header{
HeaderName: psPolicy.GetHeader().GetName(),
},
},
Terminal: psPolicy.GetHeader().GetTerminal(),
}
case *pbproxystate.LoadBalancerHashPolicy_Cookie:
cookie := &envoy_route_v3.RouteAction_HashPolicy_Cookie{
Name: psPolicy.GetCookie().GetName(),
Path: psPolicy.GetCookie().GetPath(),
Ttl: psPolicy.GetCookie().GetTtl(),
}
return &envoy_route_v3.RouteAction_HashPolicy{
PolicySpecifier: &envoy_route_v3.RouteAction_HashPolicy_Cookie_{
Cookie: cookie,
},
Terminal: psPolicy.GetCookie().GetTerminal(),
}
case *pbproxystate.LoadBalancerHashPolicy_QueryParameter:
return &envoy_route_v3.RouteAction_HashPolicy{
PolicySpecifier: &envoy_route_v3.RouteAction_HashPolicy_QueryParameter_{
QueryParameter: &envoy_route_v3.RouteAction_HashPolicy_QueryParameter{
Name: psPolicy.GetQueryParameter().GetName(),
},
},
Terminal: psPolicy.GetQueryParameter().GetTerminal(),
}
}
// This shouldn't be possible considering the types of LoadBalancerPolicy
return nil
}
func makeEnvoyRetryPolicyFromProxystateRetryPolicy(psRetryPolicy *pbproxystate.RetryPolicy) *envoy_route_v3.RetryPolicy {
return &envoy_route_v3.RetryPolicy{
NumRetries: psRetryPolicy.GetNumRetries(),
RetriableStatusCodes: psRetryPolicy.GetRetriableStatusCodes(),
RetryOn: psRetryPolicy.GetRetryOn(),
}
}
func injectEnvoyRouteRuleWithProxystateHeaderMutation(envoyRouteRule *envoy_route_v3.Route, mutation *pbproxystate.HeaderMutation) {
mutation.GetAction()
switch mutation.GetAction().(type) {
case *pbproxystate.HeaderMutation_RequestHeaderAdd:
action := mutation.GetRequestHeaderAdd()
header := action.GetHeader()
app := action.GetAppendAction() == pbproxystate.AppendAction_APPEND_ACTION_APPEND_IF_EXISTS_OR_ADD
hvo := &envoy_core_v3.HeaderValueOption{
Header: &envoy_core_v3.HeaderValue{
Key: header.GetKey(),
Value: header.GetValue(),
},
Append: response.MakeBoolValue(app),
}
envoyRouteRule.RequestHeadersToAdd = append(envoyRouteRule.RequestHeadersToAdd, hvo)
case *pbproxystate.HeaderMutation_RequestHeaderRemove:
action := mutation.GetRequestHeaderRemove()
envoyRouteRule.RequestHeadersToRemove = append(envoyRouteRule.RequestHeadersToRemove, action.GetHeaderKeys()...)
case *pbproxystate.HeaderMutation_ResponseHeaderAdd:
action := mutation.GetResponseHeaderAdd()
header := action.GetHeader()
app := action.GetAppendAction() == pbproxystate.AppendAction_APPEND_ACTION_APPEND_IF_EXISTS_OR_ADD
hvo := &envoy_core_v3.HeaderValueOption{
Header: &envoy_core_v3.HeaderValue{
Key: header.GetKey(),
Value: header.GetValue(),
},
Append: response.MakeBoolValue(app),
}
envoyRouteRule.ResponseHeadersToAdd = append(envoyRouteRule.ResponseHeadersToAdd, hvo)
case *pbproxystate.HeaderMutation_ResponseHeaderRemove:
action := mutation.GetResponseHeaderRemove()
envoyRouteRule.ResponseHeadersToRemove = append(envoyRouteRule.ResponseHeadersToRemove, action.GetHeaderKeys()...)
default:
// This shouldn't be possible considering the types of Destination
return
}
}
func injectEnvoyVirtualHostWithProxystateHeaderMutation(envoyVirtualHost *envoy_route_v3.VirtualHost, mutation *pbproxystate.HeaderMutation) {
mutation.GetAction()
switch mutation.GetAction().(type) {
case *pbproxystate.HeaderMutation_RequestHeaderAdd:
action := mutation.GetRequestHeaderAdd()
header := action.GetHeader()
app := action.GetAppendAction() == pbproxystate.AppendAction_APPEND_ACTION_APPEND_IF_EXISTS_OR_ADD
hvo := &envoy_core_v3.HeaderValueOption{
Header: &envoy_core_v3.HeaderValue{
Key: header.GetKey(),
Value: header.GetValue(),
},
Append: response.MakeBoolValue(app),
}
envoyVirtualHost.RequestHeadersToAdd = append(envoyVirtualHost.RequestHeadersToAdd, hvo)
case *pbproxystate.HeaderMutation_RequestHeaderRemove:
action := mutation.GetRequestHeaderRemove()
envoyVirtualHost.RequestHeadersToRemove = append(envoyVirtualHost.RequestHeadersToRemove, action.GetHeaderKeys()...)
case *pbproxystate.HeaderMutation_ResponseHeaderAdd:
action := mutation.GetResponseHeaderAdd()
header := action.GetHeader()
app := action.GetAppendAction() == pbproxystate.AppendAction_APPEND_ACTION_APPEND_IF_EXISTS_OR_ADD
hvo := &envoy_core_v3.HeaderValueOption{
Header: &envoy_core_v3.HeaderValue{
Key: header.GetKey(),
Value: header.GetValue(),
},
Append: response.MakeBoolValue(app),
}
envoyVirtualHost.ResponseHeadersToAdd = append(envoyVirtualHost.ResponseHeadersToAdd, hvo)
case *pbproxystate.HeaderMutation_ResponseHeaderRemove:
action := mutation.GetResponseHeaderRemove()
envoyVirtualHost.ResponseHeadersToRemove = append(envoyVirtualHost.ResponseHeadersToRemove, action.GetHeaderKeys()...)
default:
// This shouldn't be possible considering the types of Destination
return
}
}