mesh: compute more of the xRoute features into ComputedRoutes (#18980)

Convert more of the xRoutes features that were skipped in an earlier PR into ComputedRoutes and make them work:

- DestinationPolicy defaults
- more timeouts
- load balancer policy
- request/response header mutations
- urlrewrite
- GRPCRoute matches
This commit is contained in:
R.B. Boyer 2023-09-22 16:13:24 -05:00 committed by GitHub
parent d3bb5ff21a
commit 9e48607893
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 880 additions and 335 deletions

View File

@ -11,8 +11,6 @@ import (
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v2beta1"
svctest "github.com/hashicorp/consul/agent/grpc-external/services/resource/testing"
"github.com/hashicorp/consul/internal/catalog"
"github.com/hashicorp/consul/internal/controller"
@ -20,6 +18,7 @@ import (
"github.com/hashicorp/consul/internal/resource"
rtest "github.com/hashicorp/consul/internal/resource/resourcetest"
pbcatalog "github.com/hashicorp/consul/proto-public/pbcatalog/v2beta1"
pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v2beta1"
"github.com/hashicorp/consul/proto-public/pbresource"
"github.com/hashicorp/consul/proto/private/prototest"
"github.com/hashicorp/consul/sdk/testutil"
@ -115,6 +114,7 @@ func (suite *controllerSuite) TestController() {
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
MeshPort: "mesh",
BackendRef: newBackendRef(apiServiceRef, "tcp", ""),
DestinationConfig: defaultDestConfig(),
},
},
},
@ -187,6 +187,7 @@ func (suite *controllerSuite) TestController() {
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
MeshPort: "mesh",
BackendRef: newBackendRef(apiServiceRef, "tcp", ""),
DestinationConfig: defaultDestConfig(),
},
},
},
@ -214,6 +215,7 @@ func (suite *controllerSuite) TestController() {
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
MeshPort: "mesh",
BackendRef: newBackendRef(apiServiceRef, "http", ""),
DestinationConfig: defaultDestConfig(),
},
},
},
@ -241,6 +243,7 @@ func (suite *controllerSuite) TestController() {
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
MeshPort: "mesh",
BackendRef: newBackendRef(apiServiceRef, "http2", ""),
DestinationConfig: defaultDestConfig(),
},
},
},
@ -263,6 +266,7 @@ func (suite *controllerSuite) TestController() {
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
MeshPort: "mesh",
BackendRef: newBackendRef(apiServiceRef, "grpc", ""),
DestinationConfig: defaultDestConfig(),
},
},
},
@ -350,6 +354,7 @@ func (suite *controllerSuite) TestController() {
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
MeshPort: "mesh",
BackendRef: newBackendRef(fooServiceRef, "tcp", ""),
DestinationConfig: defaultDestConfig(),
},
},
},
@ -379,6 +384,7 @@ func (suite *controllerSuite) TestController() {
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
MeshPort: "mesh",
BackendRef: newBackendRef(fooServiceRef, "http", ""),
DestinationConfig: defaultDestConfig(),
},
},
},
@ -408,6 +414,7 @@ func (suite *controllerSuite) TestController() {
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
MeshPort: "mesh",
BackendRef: newBackendRef(fooServiceRef, "grpc", ""),
DestinationConfig: defaultDestConfig(),
},
},
},
@ -437,6 +444,7 @@ func (suite *controllerSuite) TestController() {
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
MeshPort: "mesh",
BackendRef: newBackendRef(fooServiceRef, "http2", ""),
DestinationConfig: defaultDestConfig(),
},
},
},
@ -551,6 +559,7 @@ func (suite *controllerSuite) TestController() {
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
MeshPort: "mesh",
BackendRef: newBackendRef(fooServiceRef, "tcp", ""),
DestinationConfig: defaultDestConfig(),
},
},
},
@ -591,6 +600,7 @@ func (suite *controllerSuite) TestController() {
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
MeshPort: "mesh",
BackendRef: newBackendRef(fooServiceRef, "http", ""),
DestinationConfig: defaultDestConfig(),
},
},
},
@ -632,6 +642,7 @@ func (suite *controllerSuite) TestController() {
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
MeshPort: "mesh",
BackendRef: newBackendRef(fooServiceRef, "grpc", ""),
DestinationConfig: defaultDestConfig(),
},
},
},
@ -672,6 +683,7 @@ func (suite *controllerSuite) TestController() {
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
MeshPort: "mesh",
BackendRef: newBackendRef(fooServiceRef, "http2", ""),
DestinationConfig: defaultDestConfig(),
},
},
},
@ -789,6 +801,7 @@ func (suite *controllerSuite) TestController() {
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
MeshPort: "mesh",
BackendRef: newBackendRef(fooServiceRef, "tcp", ""),
DestinationConfig: defaultDestConfig(),
},
},
},
@ -818,6 +831,7 @@ func (suite *controllerSuite) TestController() {
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
MeshPort: "mesh",
BackendRef: newBackendRef(fooServiceRef, "http", ""),
DestinationConfig: defaultDestConfig(),
},
},
},
@ -847,6 +861,7 @@ func (suite *controllerSuite) TestController() {
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
MeshPort: "mesh",
BackendRef: newBackendRef(fooServiceRef, "grpc", ""),
DestinationConfig: defaultDestConfig(),
},
},
},
@ -876,6 +891,7 @@ func (suite *controllerSuite) TestController() {
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
MeshPort: "mesh",
BackendRef: newBackendRef(fooServiceRef, "http2", ""),
DestinationConfig: defaultDestConfig(),
},
},
},
@ -955,6 +971,7 @@ func (suite *controllerSuite) TestController() {
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
MeshPort: "mesh",
BackendRef: newBackendRef(fooServiceRef, "tcp", ""),
DestinationConfig: defaultDestConfig(),
},
},
},
@ -984,6 +1001,7 @@ func (suite *controllerSuite) TestController() {
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
MeshPort: "mesh",
BackendRef: newBackendRef(fooServiceRef, "http", ""),
DestinationConfig: defaultDestConfig(),
},
},
},
@ -1013,6 +1031,7 @@ func (suite *controllerSuite) TestController() {
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
MeshPort: "mesh",
BackendRef: newBackendRef(fooServiceRef, "grpc", ""),
DestinationConfig: defaultDestConfig(),
},
},
},
@ -1042,6 +1061,7 @@ func (suite *controllerSuite) TestController() {
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
MeshPort: "mesh",
BackendRef: newBackendRef(fooServiceRef, "http2", ""),
DestinationConfig: defaultDestConfig(),
},
},
},
@ -1169,6 +1189,7 @@ func (suite *controllerSuite) TestController() {
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
MeshPort: "mesh",
BackendRef: newBackendRef(apiServiceRef, "http", ""),
DestinationConfig: defaultDestConfig(),
},
},
},
@ -1207,6 +1228,7 @@ func (suite *controllerSuite) TestController() {
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
MeshPort: "mesh",
BackendRef: newBackendRef(apiServiceRef, "http", ""),
DestinationConfig: defaultDestConfig(),
},
},
},
@ -1263,6 +1285,7 @@ func (suite *controllerSuite) TestController() {
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
MeshPort: "mesh",
BackendRef: newBackendRef(barServiceRef, "http", ""),
DestinationConfig: defaultDestConfig(),
},
},
},

View File

@ -5,6 +5,10 @@ package routes
import (
"fmt"
"time"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/durationpb"
"github.com/hashicorp/consul/internal/catalog"
"github.com/hashicorp/consul/internal/mesh/internal/controllers/routes/loader"
@ -334,6 +338,72 @@ func compile(
details.DestinationConfig = portDestConfig
}
}
details.DestinationConfig = fillInDefaultDestConfig(details.DestinationConfig)
}
// Pull target information up to the level of the rules.
switch x := mc.Config.(type) {
case *pbmesh.ComputedPortRoutes_Http:
route := x.Http
for _, rule := range route.Rules {
// If there are multiple legs (split) then choose the first actually set value.
var requestTimeoutFallback *durationpb.Duration
for _, backendRef := range rule.BackendRefs {
if backendRef.BackendTarget == types.NullRouteBackend {
continue
}
details, ok := mc.Targets[backendRef.BackendTarget]
if !ok {
continue
}
if details.DestinationConfig.RequestTimeout != nil {
requestTimeoutFallback = details.DestinationConfig.RequestTimeout
break
}
}
if requestTimeoutFallback == nil {
continue // nothing to do
}
if rule.Timeouts == nil {
rule.Timeouts = &pbmesh.HTTPRouteTimeouts{}
}
if rule.Timeouts.Request == nil {
rule.Timeouts.Request = requestTimeoutFallback
}
}
case *pbmesh.ComputedPortRoutes_Grpc:
route := x.Grpc
for _, rule := range route.Rules {
// If there are multiple legs (split) then choose the first actually set value.
var requestTimeoutFallback *durationpb.Duration
for _, backendRef := range rule.BackendRefs {
if backendRef.BackendTarget == types.NullRouteBackend {
continue
}
details, ok := mc.Targets[backendRef.BackendTarget]
if !ok {
continue
}
if details.DestinationConfig.RequestTimeout != nil {
requestTimeoutFallback = details.DestinationConfig.RequestTimeout
break
}
}
if requestTimeoutFallback == nil {
continue // nothing to do
}
if rule.Timeouts == nil {
rule.Timeouts = &pbmesh.HTTPRouteTimeouts{}
}
if rule.Timeouts.Request == nil {
rule.Timeouts.Request = requestTimeoutFallback
}
}
case *pbmesh.ComputedPortRoutes_Tcp:
}
computedRoutes.PortedConfigs[port] = mc
@ -412,6 +482,28 @@ func compileFailoverConfig(
return cfc
}
func fillInDefaultDestConfig(target *pbmesh.DestinationConfig) *pbmesh.DestinationConfig {
base := defaultDestConfig()
if target == nil {
return proto.Clone(base).(*pbmesh.DestinationConfig)
}
out := proto.Clone(target).(*pbmesh.DestinationConfig)
if out.ConnectTimeout == nil {
out.ConnectTimeout = base.GetConnectTimeout()
}
return out
}
func defaultDestConfig() *pbmesh.DestinationConfig {
return &pbmesh.DestinationConfig{
ConnectTimeout: durationpb.New(5 * time.Second),
}
}
func compileHTTPRouteNode(
port string,
res *pbresource.Resource,

View File

@ -225,6 +225,7 @@ func TestGenerateComputedRoutes(t *testing.T) {
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
MeshPort: "mesh",
BackendRef: newBackendRef(apiServiceRef, "tcp", ""),
DestinationConfig: defaultDestConfig(),
},
},
},
@ -285,6 +286,7 @@ func TestGenerateComputedRoutes(t *testing.T) {
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
MeshPort: "mesh",
BackendRef: newBackendRef(apiServiceRef, protoName, ""),
DestinationConfig: defaultDestConfig(),
},
},
},
@ -337,6 +339,7 @@ func TestGenerateComputedRoutes(t *testing.T) {
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
MeshPort: "mesh",
BackendRef: newBackendRef(apiServiceRef, "grpc", ""),
DestinationConfig: defaultDestConfig(),
},
},
},
@ -446,6 +449,7 @@ func TestGenerateComputedRoutes(t *testing.T) {
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
MeshPort: "mesh",
BackendRef: newBackendRef(fooServiceRef, "tcp", ""),
DestinationConfig: defaultDestConfig(),
},
},
},
@ -475,6 +479,7 @@ func TestGenerateComputedRoutes(t *testing.T) {
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
MeshPort: "mesh",
BackendRef: newBackendRef(fooServiceRef, "http", ""),
DestinationConfig: defaultDestConfig(),
},
},
},
@ -504,6 +509,7 @@ func TestGenerateComputedRoutes(t *testing.T) {
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
MeshPort: "mesh",
BackendRef: newBackendRef(fooServiceRef, "http2", ""),
DestinationConfig: defaultDestConfig(),
},
},
},
@ -533,6 +539,7 @@ func TestGenerateComputedRoutes(t *testing.T) {
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
MeshPort: "mesh",
BackendRef: newBackendRef(fooServiceRef, "grpc", ""),
DestinationConfig: defaultDestConfig(),
},
},
},
@ -615,6 +622,7 @@ func TestGenerateComputedRoutes(t *testing.T) {
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
MeshPort: "mesh",
BackendRef: newBackendRef(fooServiceRef, portName, ""),
DestinationConfig: defaultDestConfig(),
},
},
}
@ -724,6 +732,7 @@ func TestGenerateComputedRoutes(t *testing.T) {
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
MeshPort: "mesh",
BackendRef: newBackendRef(apiServiceRef, "http", ""),
DestinationConfig: defaultDestConfig(),
},
},
},
@ -806,6 +815,7 @@ func TestGenerateComputedRoutes(t *testing.T) {
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
MeshPort: "mesh",
BackendRef: newBackendRef(apiServiceRef, "http", ""),
DestinationConfig: defaultDestConfig(),
},
},
},
@ -913,6 +923,7 @@ func TestGenerateComputedRoutes(t *testing.T) {
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
MeshPort: "mesh",
BackendRef: newBackendRef(fooServiceRef, "http", ""),
DestinationConfig: defaultDestConfig(),
},
},
},
@ -1065,11 +1076,13 @@ func TestGenerateComputedRoutes(t *testing.T) {
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
MeshPort: "mesh",
BackendRef: newBackendRef(fooServiceRef, "http", ""),
DestinationConfig: defaultDestConfig(),
},
backendName("bar", "http"): {
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
MeshPort: "mesh",
BackendRef: newBackendRef(barServiceRef, "http", ""),
DestinationConfig: defaultDestConfig(),
},
},
},
@ -1158,6 +1171,7 @@ func TestGenerateComputedRoutes(t *testing.T) {
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
MeshPort: "mesh",
BackendRef: newBackendRef(fooServiceRef, "http", ""),
DestinationConfig: defaultDestConfig(),
},
},
},
@ -1652,11 +1666,13 @@ func TestGenerateComputedRoutes(t *testing.T) {
MeshPort: "mesh",
BackendRef: newBackendRef(fooServiceRef, "http", ""),
FailoverConfig: portFailoverConfig,
DestinationConfig: defaultDestConfig(),
},
backendName("bar", "http"): {
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_INDIRECT,
MeshPort: "mesh",
BackendRef: newBackendRef(barServiceRef, "http", ""),
DestinationConfig: defaultDestConfig(),
},
},
},

View File

@ -5,19 +5,16 @@ package routes
import (
"google.golang.org/protobuf/proto"
"github.com/hashicorp/consul/internal/protoutil"
)
// Deprecated: see protoutil.Clone
func protoClone[T proto.Message](v T) T {
return proto.Clone(v).(T)
return protoutil.Clone(v)
}
// Deprecated: see protoutil.CloneSlice
func protoSliceClone[T proto.Message](in []T) []T {
if in == nil {
return nil
}
out := make([]T, 0, len(in))
for _, v := range in {
out = append(out, protoClone[T](v))
}
return out
return protoutil.CloneSlice(in)
}

View File

@ -7,7 +7,6 @@ import (
"fmt"
"time"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/wrapperspb"
@ -15,6 +14,7 @@ import (
"github.com/hashicorp/consul/envoyextensions/xdscommon"
"github.com/hashicorp/consul/internal/mesh/internal/types"
"github.com/hashicorp/consul/internal/mesh/internal/types/intermediate"
"github.com/hashicorp/consul/internal/protoutil"
pbcatalog "github.com/hashicorp/consul/proto-public/pbcatalog/v2beta1"
pbmesh "github.com/hashicorp/consul/proto-public/pbmesh/v2beta1"
"github.com/hashicorp/consul/proto-public/pbmesh/v2beta1/pbproxystate"
@ -109,6 +109,8 @@ func (b *Builder) buildDestination(
}
}
destConfig := b.makeDestinationConfiguration(routeRule.Timeouts, routeRule.Retries)
headerMutations := applyRouteFilters(destConfig, routeRule.Filters)
applyLoadBalancerPolicy(destConfig, cpr, routeRule.BackendRefs)
dest := b.makeHTTPRouteDestination(
routeRule.BackendRefs,
@ -117,17 +119,14 @@ func (b *Builder) buildDestination(
defaultDC,
)
// TODO(rb/v2): Filters []*HTTPRouteFilter
// TODO(rb/v2): BackendRefs []*ComputedHTTPBackendRef
// Explode out by matches
for _, match := range routeRule.Matches {
routeMatch := makeHTTPRouteMatch(match)
proxyRouteRules = append(proxyRouteRules, &pbproxystate.RouteRule{
Match: routeMatch,
Destination: proto.Clone(dest).(*pbproxystate.RouteDestination),
// TODO: mutations
Destination: protoutil.Clone(dest),
HeaderMutations: protoutil.CloneSlice(headerMutations),
})
}
}
@ -153,6 +152,8 @@ func (b *Builder) buildDestination(
}
}
destConfig := b.makeDestinationConfiguration(routeRule.Timeouts, routeRule.Retries)
headerMutations := applyRouteFilters(destConfig, routeRule.Filters)
applyLoadBalancerPolicy(destConfig, cpr, routeRule.BackendRefs)
// nolint:staticcheck
dest := b.makeGRPCRouteDestination(
@ -162,16 +163,14 @@ func (b *Builder) buildDestination(
defaultDC,
)
// TODO(rb/v2): Filters []*HTTPRouteFilter
// Explode out by matches
for _, match := range routeRule.Matches {
routeMatch := makeGRPCRouteMatch(match)
proxyRouteRules = append(proxyRouteRules, &pbproxystate.RouteRule{
Match: routeMatch,
Destination: proto.Clone(dest).(*pbproxystate.RouteDestination),
// TODO: mutations
Destination: protoutil.Clone(dest),
HeaderMutations: protoutil.CloneSlice(headerMutations),
})
}
}
@ -267,7 +266,8 @@ func (b *Builder) buildDestination(
continue
}
connectTimeout := durationpb.New(5 * time.Second)
connectTimeout := details.DestinationConfig.ConnectTimeout
loadBalancer := details.DestinationConfig.LoadBalancer
// NOTE: we collect both DIRECT and INDIRECT target information here.
dc := defaultDC(details.BackendRef.Datacenter)
@ -280,7 +280,7 @@ func (b *Builder) buildDestination(
)
clusterName := fmt.Sprintf("%s.%s", portName, sni)
egBase := b.newClusterEndpointGroup("", sni, portName, details.IdentityRefs, connectTimeout)
egBase := b.newClusterEndpointGroup("", sni, portName, details.IdentityRefs, connectTimeout, loadBalancer)
var endpointGroups []*pbproxystate.EndpointGroup
@ -301,6 +301,9 @@ func (b *Builder) buildDestination(
continue // not possible
}
destConnectTimeout := destDetails.DestinationConfig.ConnectTimeout
destLoadBalancer := destDetails.DestinationConfig.LoadBalancer
destDC := defaultDC(destDetails.BackendRef.Datacenter)
destPortName := destDetails.BackendRef.Port
@ -311,7 +314,7 @@ func (b *Builder) buildDestination(
)
destClusterName := fmt.Sprintf("%s%d~%s", xdscommon.FailoverClusterNamePrefix, i, clusterName)
egDest := b.newClusterEndpointGroup(destClusterName, destSNI, destPortName, destDetails.IdentityRefs, connectTimeout)
egDest := b.newClusterEndpointGroup(destClusterName, destSNI, destPortName, destDetails.IdentityRefs, destConnectTimeout, destLoadBalancer)
endpointGroups = append(endpointGroups, egDest)
b.addEndpointsRef(destClusterName, destDetails.ServiceEndpointsId, destDetails.MeshPort)
@ -389,7 +392,7 @@ func (b *ListenerBuilder) addL4RouterForSplit(
},
},
StatPrefix: statPrefix,
// TODO: can we use RDS for TCPRoute split?
// TODO(rb/v2): can we use RDS for TCPRoute split?
},
}
@ -565,24 +568,71 @@ func (b *Builder) newClusterEndpointGroup(
portName string,
destinationIdentities []*pbresource.Reference,
connectTimeout *durationpb.Duration,
loadBalancer *pbmesh.LoadBalancer,
) *pbproxystate.EndpointGroup {
var spiffeIDs []string
for _, identity := range destinationIdentities {
spiffeIDs = append(spiffeIDs, connect.SpiffeIDFromIdentityRef(b.trustDomain, identity))
}
// TODO(v2): DestinationPolicy: connect timeout, lb policy, cluster discovery type, circuit breakers, outlier detection
// TODO(v2): DestinationPolicy: circuit breakers, outlier detection
// TODO(v2): if http2/grpc then set http2protocol options
degConfig := &pbproxystate.DynamicEndpointGroupConfig{
DisablePanicThreshold: true,
ConnectTimeout: connectTimeout,
}
if loadBalancer != nil {
// enumcover:pbmesh.LoadBalancerPolicy
switch loadBalancer.Policy {
case pbmesh.LoadBalancerPolicy_LOAD_BALANCER_POLICY_RANDOM:
degConfig.LbPolicy = &pbproxystate.DynamicEndpointGroupConfig_Random{}
case pbmesh.LoadBalancerPolicy_LOAD_BALANCER_POLICY_ROUND_ROBIN:
degConfig.LbPolicy = &pbproxystate.DynamicEndpointGroupConfig_RoundRobin{}
case pbmesh.LoadBalancerPolicy_LOAD_BALANCER_POLICY_LEAST_REQUEST:
var choiceCount uint32
cfg, ok := loadBalancer.Config.(*pbmesh.LoadBalancer_LeastRequestConfig)
if ok {
choiceCount = cfg.LeastRequestConfig.GetChoiceCount()
}
degConfig.LbPolicy = &pbproxystate.DynamicEndpointGroupConfig_LeastRequest{
LeastRequest: &pbproxystate.LBPolicyLeastRequest{
ChoiceCount: wrapperspb.UInt32(choiceCount),
},
}
case pbmesh.LoadBalancerPolicy_LOAD_BALANCER_POLICY_MAGLEV:
degConfig.LbPolicy = &pbproxystate.DynamicEndpointGroupConfig_Maglev{}
case pbmesh.LoadBalancerPolicy_LOAD_BALANCER_POLICY_RING_HASH:
policy := &pbproxystate.DynamicEndpointGroupConfig_RingHash{}
cfg, ok := loadBalancer.Config.(*pbmesh.LoadBalancer_RingHashConfig)
if ok {
policy.RingHash = &pbproxystate.LBPolicyRingHash{
MinimumRingSize: wrapperspb.UInt64(cfg.RingHashConfig.MinimumRingSize),
MaximumRingSize: wrapperspb.UInt64(cfg.RingHashConfig.MaximumRingSize),
}
}
degConfig.LbPolicy = policy
case pbmesh.LoadBalancerPolicy_LOAD_BALANCER_POLICY_UNSPECIFIED:
// fallthrough to default
default:
// do nothing
}
}
return &pbproxystate.EndpointGroup{
Name: clusterName,
Group: &pbproxystate.EndpointGroup_Dynamic{
Dynamic: &pbproxystate.DynamicEndpointGroup{
Config: &pbproxystate.DynamicEndpointGroupConfig{
DisablePanicThreshold: true,
ConnectTimeout: connectTimeout,
},
Config: degConfig,
OutboundTls: &pbproxystate.TransportSocket{
ConnectionTls: &pbproxystate.TransportSocket_OutboundMesh{
OutboundMesh: &pbproxystate.OutboundMeshMTLS{

View File

@ -5,8 +5,11 @@ package builder
import (
"testing"
"time"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/wrapperspb"
"github.com/hashicorp/consul/internal/catalog"
"github.com/hashicorp/consul/internal/mesh/internal/controllers/routes/routestest"
@ -130,6 +133,19 @@ func TestBuildExplicitDestinations(t *testing.T) {
Tenancy: backup1Endpoints.Id.Tenancy,
}
api1DestPolicy := resourcetest.Resource(types.DestinationPolicyType, api1Service.Id.Name).
WithTenancy(api1Service.Id.GetTenancy()).
WithData(t, &pbmesh.DestinationPolicy{
PortConfigs: map[string]*pbmesh.DestinationConfig{
"http": {
ConnectTimeout: durationpb.New(55 * time.Second),
RequestTimeout: durationpb.New(77 * time.Second),
// LoadBalancer *LoadBalancer `protobuf:"bytes,3,opt,name=load_balancer,json=loadBalancer,proto3" json:"load_balancer,omitempty"`
},
},
}).
Build()
api1HTTPRoute := resourcetest.Resource(types.HTTPRouteType, "api-1-http-route").
WithTenancy(resource.DefaultNamespacedTenancy()).
WithData(t, &pbmesh.HTTPRoute{
@ -137,7 +153,14 @@ func TestBuildExplicitDestinations(t *testing.T) {
Ref: resource.Reference(api1Service.Id, ""),
Port: "http",
}},
Rules: []*pbmesh.HTTPRouteRule{{
Rules: []*pbmesh.HTTPRouteRule{
{
Matches: []*pbmesh.HTTPRouteMatch{{
Path: &pbmesh.HTTPPathMatch{
Type: pbmesh.PathMatchType_PATH_MATCH_TYPE_PREFIX,
Value: "/split",
},
}},
BackendRefs: []*pbmesh.HTTPBackendRef{
{
BackendRef: &pbmesh.BackendReference{
@ -158,7 +181,28 @@ func TestBuildExplicitDestinations(t *testing.T) {
Weight: 10,
},
},
},
{
Matches: []*pbmesh.HTTPRouteMatch{{
Path: &pbmesh.HTTPPathMatch{
Type: pbmesh.PathMatchType_PATH_MATCH_TYPE_PREFIX,
Value: "/",
},
}},
BackendRefs: []*pbmesh.HTTPBackendRef{{
BackendRef: &pbmesh.BackendReference{
Ref: resource.Reference(api1Service.Id, ""),
},
}},
Timeouts: &pbmesh.HTTPRouteTimeouts{
Request: durationpb.New(606 * time.Second), // differnet than the 77s
},
Retries: &pbmesh.HTTPRouteRetries{
Number: wrapperspb.UInt32(4),
OnConnectFailure: true,
},
},
},
}).
Build()
resourcetest.ValidateAndNormalize(t, registry, api1HTTPRoute)
@ -249,6 +293,7 @@ func TestBuildExplicitDestinations(t *testing.T) {
resourcetest.MustDecode[*pbcatalog.Service](t, api2Service),
resourcetest.MustDecode[*pbcatalog.Service](t, backup1Service),
// notably we do NOT include api3Service here so we trigger a null route to be generated
resourcetest.MustDecode[*pbmesh.DestinationPolicy](t, api1DestPolicy),
resourcetest.MustDecode[*pbmesh.HTTPRoute](t, api1HTTPRoute),
resourcetest.MustDecode[*pbmesh.TCPRoute](t, api1TCPRoute),
resourcetest.MustDecode[*pbcatalog.FailoverPolicy](t, api1FailoverPolicy),

View File

@ -6,7 +6,9 @@ package builder
import (
"fmt"
"strings"
"time"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/wrapperspb"
"github.com/hashicorp/consul/internal/mesh/internal/types"
@ -131,8 +133,6 @@ func (b *Builder) makeDestinationConfiguration(
timeouts *pbmesh.HTTPRouteTimeouts,
retries *pbmesh.HTTPRouteRetries,
) *pbproxystate.DestinationConfiguration {
// TODO: prefix rewrite, lb config
cfg := &pbproxystate.DestinationConfiguration{
TimeoutConfig: translateTimeouts(timeouts),
RetryPolicy: translateRetries(retries),
@ -144,8 +144,202 @@ func (b *Builder) makeDestinationConfiguration(
return cfg
}
func makeGRPCRouteMatch(match *pbmesh.GRPCRouteMatch) *pbproxystate.RouteMatch {
panic("TODO")
func applyRouteFilters[V interface {
GetRequestHeaderModifier() *pbmesh.HTTPHeaderFilter
GetResponseHeaderModifier() *pbmesh.HTTPHeaderFilter
GetUrlRewrite() *pbmesh.HTTPURLRewriteFilter
}](
psDestConfig *pbproxystate.DestinationConfiguration,
filters []V,
) []*pbproxystate.HeaderMutation {
var headerMutations []*pbproxystate.HeaderMutation
for _, filter := range filters {
switch {
case filter.GetRequestHeaderModifier() != nil:
mod := filter.GetRequestHeaderModifier()
for _, hdr := range mod.Set {
headerMutations = append(headerMutations, &pbproxystate.HeaderMutation{
Action: &pbproxystate.HeaderMutation_RequestHeaderAdd{
RequestHeaderAdd: &pbproxystate.RequestHeaderAdd{
Header: &pbproxystate.Header{
Key: hdr.Name,
Value: hdr.Value,
},
AppendAction: pbproxystate.AppendAction_APPEND_ACTION_OVERWRITE_IF_EXISTS_OR_ADD,
},
},
})
}
for _, hdr := range mod.Add {
headerMutations = append(headerMutations, &pbproxystate.HeaderMutation{
Action: &pbproxystate.HeaderMutation_RequestHeaderAdd{
RequestHeaderAdd: &pbproxystate.RequestHeaderAdd{
Header: &pbproxystate.Header{
Key: hdr.Name,
Value: hdr.Value,
},
AppendAction: pbproxystate.AppendAction_APPEND_ACTION_APPEND_IF_EXISTS_OR_ADD,
},
},
})
}
if len(mod.Remove) > 0 {
headerMutations = append(headerMutations, &pbproxystate.HeaderMutation{
Action: &pbproxystate.HeaderMutation_RequestHeaderRemove{
RequestHeaderRemove: &pbproxystate.RequestHeaderRemove{
HeaderKeys: mod.Remove,
},
},
})
}
case filter.GetResponseHeaderModifier() != nil:
mod := filter.GetResponseHeaderModifier()
for _, hdr := range mod.Set {
headerMutations = append(headerMutations, &pbproxystate.HeaderMutation{
Action: &pbproxystate.HeaderMutation_ResponseHeaderAdd{
ResponseHeaderAdd: &pbproxystate.ResponseHeaderAdd{
Header: &pbproxystate.Header{
Key: hdr.Name,
Value: hdr.Value,
},
AppendAction: pbproxystate.AppendAction_APPEND_ACTION_OVERWRITE_IF_EXISTS_OR_ADD,
},
},
})
}
for _, hdr := range mod.Add {
headerMutations = append(headerMutations, &pbproxystate.HeaderMutation{
Action: &pbproxystate.HeaderMutation_ResponseHeaderAdd{
ResponseHeaderAdd: &pbproxystate.ResponseHeaderAdd{
Header: &pbproxystate.Header{
Key: hdr.Name,
Value: hdr.Value,
},
AppendAction: pbproxystate.AppendAction_APPEND_ACTION_APPEND_IF_EXISTS_OR_ADD,
},
},
})
}
if len(mod.Remove) > 0 {
headerMutations = append(headerMutations, &pbproxystate.HeaderMutation{
Action: &pbproxystate.HeaderMutation_ResponseHeaderRemove{
ResponseHeaderRemove: &pbproxystate.ResponseHeaderRemove{
HeaderKeys: mod.Remove,
},
},
})
}
case filter.GetUrlRewrite() != nil:
prefix := filter.GetUrlRewrite().PathPrefix
if prefix != "" {
psDestConfig.PrefixRewrite = prefix
}
}
}
return headerMutations
}
func applyLoadBalancerPolicy[V interface {
GetBackendTarget() string
}](
psDestConfig *pbproxystate.DestinationConfiguration,
cpr *pbmesh.ComputedPortRoutes,
backendRefs []V,
) {
var lb *pbmesh.LoadBalancer
// If there are multiple targets, just pick the lb policy from
// the first one configured.
for _, backendRef := range backendRefs {
if backendRef.GetBackendTarget() == types.NullRouteBackend {
continue
}
details, ok := cpr.Targets[backendRef.GetBackendTarget()]
if !ok {
continue
}
thisLB := details.DestinationConfig.LoadBalancer
if thisLB != nil {
lb = thisLB
break
}
}
if lb == nil {
return
}
for _, policy := range lb.HashPolicies {
if policy.SourceIp {
psDestConfig.HashPolicies = append(psDestConfig.HashPolicies, &pbproxystate.LoadBalancerHashPolicy{
Policy: &pbproxystate.LoadBalancerHashPolicy_ConnectionProperties{
ConnectionProperties: &pbproxystate.ConnectionPropertiesPolicy{
SourceIp: true,
Terminal: policy.Terminal,
},
},
})
continue
}
// enumcover:pbmesh.HashPolicyField
switch policy.Field {
case pbmesh.HashPolicyField_HASH_POLICY_FIELD_HEADER:
psDestConfig.HashPolicies = append(psDestConfig.HashPolicies, &pbproxystate.LoadBalancerHashPolicy{
Policy: &pbproxystate.LoadBalancerHashPolicy_Header{
Header: &pbproxystate.HeaderPolicy{
Name: policy.FieldValue,
Terminal: policy.Terminal,
},
},
})
case pbmesh.HashPolicyField_HASH_POLICY_FIELD_COOKIE:
cookie := &pbproxystate.CookiePolicy{
Name: policy.FieldValue,
Terminal: policy.Terminal,
}
if policy.CookieConfig != nil {
cookie.Path = policy.CookieConfig.Path
if policy.CookieConfig.Ttl != nil {
if policy.CookieConfig.Ttl.AsDuration() != 0 {
cookie.Ttl = policy.CookieConfig.Ttl
}
}
// Envoy will generate a session cookie if the ttl is present and zero.
if policy.CookieConfig.Session {
cookie.Ttl = durationpb.New(0 * time.Second)
}
}
psDestConfig.HashPolicies = append(psDestConfig.HashPolicies, &pbproxystate.LoadBalancerHashPolicy{
Policy: &pbproxystate.LoadBalancerHashPolicy_Cookie{
Cookie: cookie,
},
})
case pbmesh.HashPolicyField_HASH_POLICY_FIELD_QUERY_PARAMETER:
psDestConfig.HashPolicies = append(psDestConfig.HashPolicies, &pbproxystate.LoadBalancerHashPolicy{
Policy: &pbproxystate.LoadBalancerHashPolicy_QueryParameter{
QueryParameter: &pbproxystate.QueryParameterPolicy{
Name: policy.FieldValue,
Terminal: policy.Terminal,
},
},
})
case pbmesh.HashPolicyField_HASH_POLICY_FIELD_UNSPECIFIED:
// fallthrough to default
default:
// not possible from validation
}
}
}
func makeHTTPRouteMatch(match *pbmesh.HTTPRouteMatch) *pbproxystate.RouteMatch {
@ -185,48 +379,7 @@ func makeHTTPRouteMatch(match *pbmesh.HTTPRouteMatch) *pbproxystate.RouteMatch {
}
}
if len(match.Headers) > 0 {
em.HeaderMatches = make([]*pbproxystate.HeaderMatch, 0, len(match.Headers))
for _, hdr := range match.Headers {
eh := &pbproxystate.HeaderMatch{
Name: hdr.Name,
}
// enumcover:pbmesh.HeaderMatchType
switch hdr.Type {
case pbmesh.HeaderMatchType_HEADER_MATCH_TYPE_EXACT:
eh.Match = &pbproxystate.HeaderMatch_Exact{
Exact: hdr.Value,
}
case pbmesh.HeaderMatchType_HEADER_MATCH_TYPE_REGEX:
eh.Match = &pbproxystate.HeaderMatch_Regex{
Regex: hdr.Value,
}
case pbmesh.HeaderMatchType_HEADER_MATCH_TYPE_PREFIX:
eh.Match = &pbproxystate.HeaderMatch_Prefix{
Prefix: hdr.Value,
}
case pbmesh.HeaderMatchType_HEADER_MATCH_TYPE_SUFFIX:
eh.Match = &pbproxystate.HeaderMatch_Suffix{
Suffix: hdr.Value,
}
case pbmesh.HeaderMatchType_HEADER_MATCH_TYPE_PRESENT:
eh.Match = &pbproxystate.HeaderMatch_Present{
Present: true,
}
case pbmesh.HeaderMatchType_HEADER_MATCH_TYPE_UNSPECIFIED:
fallthrough // to default
default:
panic(fmt.Sprintf("unknown header match type: %v", hdr.Type))
}
if hdr.Invert {
eh.InvertMatch = true
}
em.HeaderMatches = append(em.HeaderMatches, eh)
}
}
em.HeaderMatches = translateHeaderMatches(match.Headers, (*pbmesh.HTTPHeaderMatch).GetInvert)
if match.Method != "" {
em.MethodMatches = []string{match.Method}
@ -267,6 +420,122 @@ func makeHTTPRouteMatch(match *pbmesh.HTTPRouteMatch) *pbproxystate.RouteMatch {
return em
}
func makeGRPCRouteMatch(match *pbmesh.GRPCRouteMatch) *pbproxystate.RouteMatch {
em := &pbproxystate.RouteMatch{}
if match.Method != nil {
mm := match.Method
switch mm.Type {
case pbmesh.GRPCMethodMatchType_GRPC_METHOD_MATCH_TYPE_EXACT:
switch {
case mm.Method == "":
em.PathMatch = &pbproxystate.PathMatch{
PathMatch: &pbproxystate.PathMatch_Prefix{
Prefix: fmt.Sprintf("/%s/", mm.Service),
},
}
case mm.Service == "":
em.PathMatch = &pbproxystate.PathMatch{
PathMatch: &pbproxystate.PathMatch_Regex{
Regex: fmt.Sprintf("/[^/]+/%s", mm.Method),
},
}
default:
em.PathMatch = &pbproxystate.PathMatch{
PathMatch: &pbproxystate.PathMatch_Exact{
Exact: fmt.Sprintf("/%s/%s", mm.Service, mm.Method),
},
}
}
case pbmesh.GRPCMethodMatchType_GRPC_METHOD_MATCH_TYPE_REGEX:
switch {
case mm.Method == "":
em.PathMatch = &pbproxystate.PathMatch{
PathMatch: &pbproxystate.PathMatch_Regex{
Regex: fmt.Sprintf("/%s/.+", mm.Service),
},
}
case mm.Service == "":
em.PathMatch = &pbproxystate.PathMatch{
PathMatch: &pbproxystate.PathMatch_Regex{
Regex: fmt.Sprintf("/[^/]+/%s", mm.Method),
},
}
default:
em.PathMatch = &pbproxystate.PathMatch{
PathMatch: &pbproxystate.PathMatch_Regex{
Regex: fmt.Sprintf("/%s/%s", mm.Service, mm.Method),
},
}
}
case pbmesh.GRPCMethodMatchType_GRPC_METHOD_MATCH_TYPE_UNSPECIFIED:
fallthrough // to default
default:
panic(fmt.Sprintf("unknown method match type: %v", match.Method.Type))
}
}
em.HeaderMatches = translateHeaderMatches(match.Headers, nil)
return em
}
func translateHeaderMatches[V interface {
GetType() pbmesh.HeaderMatchType
GetName() string
GetValue() string
}](
headers []V,
getInvert func(v V) bool,
) []*pbproxystate.HeaderMatch {
if len(headers) == 0 {
return nil
}
var out []*pbproxystate.HeaderMatch
out = make([]*pbproxystate.HeaderMatch, 0, len(headers))
for _, hdr := range headers {
eh := &pbproxystate.HeaderMatch{
Name: hdr.GetName(),
}
// enumcover:pbmesh.HeaderMatchType
switch hdr.GetType() {
case pbmesh.HeaderMatchType_HEADER_MATCH_TYPE_EXACT:
eh.Match = &pbproxystate.HeaderMatch_Exact{
Exact: hdr.GetValue(),
}
case pbmesh.HeaderMatchType_HEADER_MATCH_TYPE_REGEX:
eh.Match = &pbproxystate.HeaderMatch_Regex{
Regex: hdr.GetValue(),
}
case pbmesh.HeaderMatchType_HEADER_MATCH_TYPE_PREFIX:
eh.Match = &pbproxystate.HeaderMatch_Prefix{
Prefix: hdr.GetValue(),
}
case pbmesh.HeaderMatchType_HEADER_MATCH_TYPE_SUFFIX:
eh.Match = &pbproxystate.HeaderMatch_Suffix{
Suffix: hdr.GetValue(),
}
case pbmesh.HeaderMatchType_HEADER_MATCH_TYPE_PRESENT:
eh.Match = &pbproxystate.HeaderMatch_Present{
Present: true,
}
case pbmesh.HeaderMatchType_HEADER_MATCH_TYPE_UNSPECIFIED:
fallthrough // to default
default:
panic(fmt.Sprintf("unknown header match type: %v", hdr.GetType()))
}
// HTTPHeaderMatch only
if getInvert != nil && getInvert(hdr) {
eh.InvertMatch = true
}
out = append(out, eh)
}
return out
}
func translateTimeouts(timeouts *pbmesh.HTTPRouteTimeouts) *pbproxystate.TimeoutConfig {
if timeouts == nil || (timeouts.Request == nil && timeouts.Idle == nil) {
return nil

View File

@ -5,14 +5,14 @@
"altStatName": "http.api-1.default.dc1.internal.foo.consul",
"failoverGroup": {
"config": {
"connectTimeout": "5s",
"connectTimeout": "55s",
"useAltStatName": true
},
"endpointGroups": [
{
"dynamic": {
"config": {
"connectTimeout": "5s",
"connectTimeout": "55s",
"disablePanicThreshold": true
},
"outboundTls": {
@ -235,6 +235,11 @@
"routeRules": [
{
"destination": {
"destinationConfiguration": {
"timeoutConfig": {
"timeout": "77s"
}
},
"weightedClusters": {
"clusters": [
{
@ -252,6 +257,27 @@
]
}
},
"match": {
"pathMatch": {
"prefix": "/split"
}
}
},
{
"destination": {
"cluster": {
"name": "http.api-1.default.dc1.internal.foo.consul"
},
"destinationConfiguration": {
"retryPolicy": {
"numRetries": 4,
"retryOn": "connect-failure"
},
"timeoutConfig": {
"timeout": "606s"
}
}
},
"match": {
"pathMatch": {
"prefix": "/"

View File

@ -111,6 +111,36 @@ func ValidateComputedRoutes(res *pbresource.Resource) error {
))
}
if target.DestinationConfig == nil {
merr = multierror.Append(merr, wrapTargetErr(resource.ErrInvalidField{
Name: "destination_config",
Wrapped: resource.ErrMissing,
}))
} else {
wrapDestConfigErr := func(err error) error {
return wrapTargetErr(resource.ErrInvalidField{
Name: "destination_config",
Wrapped: err,
})
}
destConfig := target.DestinationConfig
if destConfig.ConnectTimeout == nil {
merr = multierror.Append(merr, wrapDestConfigErr(resource.ErrInvalidField{
Name: "connect_timeout",
Wrapped: resource.ErrMissing,
}))
} else {
connectTimeout := destConfig.ConnectTimeout.AsDuration()
if connectTimeout < 0 {
merr = multierror.Append(merr, wrapDestConfigErr(resource.ErrInvalidField{
Name: "connect_timeout",
Wrapped: errTimeoutCannotBeNegative(connectTimeout),
}))
}
}
}
if target.MeshPort == "" {
merr = multierror.Append(merr, wrapTargetErr(resource.ErrInvalidField{
Name: "mesh_port",

View File

@ -5,8 +5,10 @@ package types
import (
"testing"
"time"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/durationpb"
"github.com/hashicorp/consul/internal/resource/resourcetest"
pbcatalog "github.com/hashicorp/consul/proto-public/pbcatalog/v2beta1"
@ -197,6 +199,9 @@ func TestValidateComputedRoutes(t *testing.T) {
"foo": {
Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT,
MeshPort: "mesh",
DestinationConfig: &pbmesh.DestinationConfig{
ConnectTimeout: durationpb.New(5 * time.Second),
},
},
},
},

View File

@ -249,6 +249,10 @@ func ValidateHTTPRoute(res *pbresource.Resource) error {
}
}
var (
hasReqMod bool
hasUrlRewrite bool
)
for j, filter := range rule.Filters {
wrapFilterErr := func(err error) error {
return wrapRuleErr(resource.ErrInvalidListElement{
@ -260,12 +264,14 @@ func ValidateHTTPRoute(res *pbresource.Resource) error {
set := 0
if filter.RequestHeaderModifier != nil {
set++
hasReqMod = true
}
if filter.ResponseHeaderModifier != nil {
set++
}
if filter.UrlRewrite != nil {
set++
hasUrlRewrite = true
if filter.UrlRewrite.PathPrefix == "" {
merr = multierror.Append(merr, wrapFilterErr(
resource.ErrInvalidField{
@ -285,6 +291,12 @@ func ValidateHTTPRoute(res *pbresource.Resource) error {
}
}
if hasReqMod && hasUrlRewrite {
merr = multierror.Append(merr, wrapRuleErr(
errors.New("exactly one of request_header_modifier or url_rewrite can be set at a time"),
))
}
if len(rule.BackendRefs) == 0 {
merr = multierror.Append(merr, wrapRuleErr(
resource.ErrInvalidField{

View File

@ -733,6 +733,29 @@ func TestValidateHTTPRoute(t *testing.T) {
},
expectErr: `invalid element at index 0 of list "rules": invalid element at index 0 of list "filters": exactly one of request_header_modifier, response_header_modifier, or url_rewrite`,
},
"filter req+rewrite on two rules is not allowed": {
route: &pbmesh.HTTPRoute{
ParentRefs: []*pbmesh.ParentReference{
newParentRef(catalog.ServiceType, "web", ""),
},
Rules: []*pbmesh.HTTPRouteRule{{
Filters: []*pbmesh.HTTPRouteFilter{
{
RequestHeaderModifier: &pbmesh.HTTPHeaderFilter{},
},
{
UrlRewrite: &pbmesh.HTTPURLRewriteFilter{
PathPrefix: "/blah",
},
},
},
BackendRefs: []*pbmesh.HTTPBackendRef{{
BackendRef: newBackendRef(catalog.ServiceType, "api", ""),
}},
}},
},
expectErr: `invalid element at index 0 of list "rules": exactly one of request_header_modifier or url_rewrite can be set at a time`,
},
"filter req+resp+rewrite header mod is bad": {
route: &pbmesh.HTTPRoute{
ParentRefs: []*pbmesh.ParentReference{
@ -1081,12 +1104,6 @@ func getXRouteTimeoutsTestCases() map[string]xRouteTimeoutsTestcase {
},
expectErr: `invalid element at index 0 of list "rules": invalid "timeouts" field: invalid "request" field: timeout cannot be negative: -1s`,
},
"bad backend request": {
timeouts: &pbmesh.HTTPRouteTimeouts{
BackendRequest: durationpb.New(-1 * time.Second),
},
expectErr: `invalid element at index 0 of list "rules": invalid "timeouts" field: invalid "backend_request" field: timeout cannot be negative: -1s`,
},
"bad idle": {
timeouts: &pbmesh.HTTPRouteTimeouts{
Idle: durationpb.New(-1 * time.Second),
@ -1096,7 +1113,6 @@ func getXRouteTimeoutsTestCases() map[string]xRouteTimeoutsTestcase {
"good all": {
timeouts: &pbmesh.HTTPRouteTimeouts{
Request: durationpb.New(1 * time.Second),
BackendRequest: durationpb.New(2 * time.Second),
Idle: durationpb.New(3 * time.Second),
},
},

View File

@ -6,6 +6,7 @@ package types
import (
"errors"
"fmt"
"time"
"github.com/hashicorp/go-multierror"
"google.golang.org/protobuf/proto"
@ -206,6 +207,10 @@ func validateHeaderMatchType(typ pbmesh.HeaderMatchType) error {
return nil
}
func errTimeoutCannotBeNegative(d time.Duration) error {
return fmt.Errorf("timeout cannot be negative: %v", d)
}
func validateHTTPTimeouts(timeouts *pbmesh.HTTPRouteTimeouts) []error {
if timeouts == nil {
return nil
@ -218,16 +223,7 @@ func validateHTTPTimeouts(timeouts *pbmesh.HTTPRouteTimeouts) []error {
if val < 0 {
errs = append(errs, resource.ErrInvalidField{
Name: "request",
Wrapped: fmt.Errorf("timeout cannot be negative: %v", val),
})
}
}
if timeouts.BackendRequest != nil {
val := timeouts.BackendRequest.AsDuration()
if val < 0 {
errs = append(errs, resource.ErrInvalidField{
Name: "backend_request",
Wrapped: fmt.Errorf("timeout cannot be negative: %v", val),
Wrapped: errTimeoutCannotBeNegative(val),
})
}
}
@ -236,7 +232,7 @@ func validateHTTPTimeouts(timeouts *pbmesh.HTTPRouteTimeouts) []error {
if val < 0 {
errs = append(errs, resource.ErrInvalidField{
Name: "idle",
Wrapped: fmt.Errorf("timeout cannot be negative: %v", val),
Wrapped: errTimeoutCannotBeNegative(val),
})
}
}

View File

@ -0,0 +1,23 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package protoutil
import (
"google.golang.org/protobuf/proto"
)
func Clone[T proto.Message](v T) T {
return proto.Clone(v).(T)
}
func CloneSlice[T proto.Message](in []T) []T {
if in == nil {
return nil
}
out := make([]T, 0, len(in))
for _, v := range in {
out = append(out, Clone[T](v))
}
return out
}

View File

@ -24,39 +24,18 @@ const (
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// HTTPRouteTimeouts defines timeouts that can be configured for an HTTPRoute.
// Timeout values are formatted like 1h/1m/1s/1ms as parsed by Golang time.ParseDuration
// and MUST BE >= 1ms.
//
// ALTERNATIVE: not using policy attachment semantics
// HTTPRouteTimeouts defines timeouts that can be configured for an HTTPRoute
// or GRPCRoute.
type HTTPRouteTimeouts struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Request specifies the duration for processing an HTTP client request after which the
// gateway will time out if unable to send a response.
// Whether the gateway starts the timeout before or after the entire client request stream
// has been received, is implementation-dependent.
//
// For example, setting the `rules.timeouts.request` field to the value `10s` in an
// `HTTPRoute` will cause a timeout if a client request is taking longer than 10 seconds
// to complete.
//
// When this field is unspecified, request timeout behavior is implementation-dependent.
// RequestTimeout is the total amount of time permitted for the entire
// downstream request (and retries) to be processed.
Request *durationpb.Duration `protobuf:"bytes,1,opt,name=request,proto3" json:"request,omitempty"`
// BackendRequest specifies a timeout for an individual request from the gateway
// to a backend service. Typically used in conjuction with retry configuration,
// if supported by an implementation.
//
// The value of BackendRequest defaults to and must be <= the value of Request timeout.
//
// Support: Extended
//
// TODO(rb): net-new feature
BackendRequest *durationpb.Duration `protobuf:"bytes,2,opt,name=backend_request,json=backendRequest,proto3" json:"backend_request,omitempty"`
// TODO(RB): this is a consul-only feature
Idle *durationpb.Duration `protobuf:"bytes,3,opt,name=idle,proto3" json:"idle,omitempty"`
// Idle specifies the total amount of time permitted for the request stream to be idle.
Idle *durationpb.Duration `protobuf:"bytes,2,opt,name=idle,proto3" json:"idle,omitempty"`
}
func (x *HTTPRouteTimeouts) Reset() {
@ -98,13 +77,6 @@ func (x *HTTPRouteTimeouts) GetRequest() *durationpb.Duration {
return nil
}
func (x *HTTPRouteTimeouts) GetBackendRequest() *durationpb.Duration {
if x != nil {
return x.BackendRequest
}
return nil
}
func (x *HTTPRouteTimeouts) GetIdle() *durationpb.Duration {
if x != nil {
return x.Idle
@ -121,37 +93,33 @@ var file_pbmesh_v2beta1_http_route_timeouts_proto_rawDesc = []byte{
0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x6d, 0x65, 0x73,
0x68, 0x2e, 0x76, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75, 0x72, 0x61, 0x74,
0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xbb, 0x01, 0x0a, 0x11, 0x48, 0x54,
0x54, 0x50, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x73, 0x12,
0x33, 0x0a, 0x07, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x72, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x12, 0x42, 0x0a, 0x0f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x5f,
0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e,
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0e, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e,
0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2d, 0x0a, 0x04, 0x69, 0x64, 0x6c, 0x65,
0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f,
0x6e, 0x52, 0x04, 0x69, 0x64, 0x6c, 0x65, 0x42, 0x97, 0x02, 0x0a, 0x21, 0x63, 0x6f, 0x6d, 0x2e,
0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c,
0x2e, 0x6d, 0x65, 0x73, 0x68, 0x2e, 0x76, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, 0x42, 0x16, 0x48,
0x74, 0x74, 0x70, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x73,
0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x43, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e,
0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x63, 0x6f,
0x6e, 0x73, 0x75, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2d, 0x70, 0x75, 0x62, 0x6c, 0x69,
0x63, 0x2f, 0x70, 0x62, 0x6d, 0x65, 0x73, 0x68, 0x2f, 0x76, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31,
0x3b, 0x6d, 0x65, 0x73, 0x68, 0x76, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, 0xa2, 0x02, 0x03, 0x48,
0x43, 0x4d, 0xaa, 0x02, 0x1d, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x43,
0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x4d, 0x65, 0x73, 0x68, 0x2e, 0x56, 0x32, 0x62, 0x65, 0x74,
0x61, 0x31, 0xca, 0x02, 0x1d, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x5c, 0x43,
0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x5c, 0x4d, 0x65, 0x73, 0x68, 0x5c, 0x56, 0x32, 0x62, 0x65, 0x74,
0x61, 0x31, 0xe2, 0x02, 0x29, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x5c, 0x43,
0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x5c, 0x4d, 0x65, 0x73, 0x68, 0x5c, 0x56, 0x32, 0x62, 0x65, 0x74,
0x61, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02,
0x20, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x3a, 0x3a, 0x43, 0x6f, 0x6e, 0x73,
0x75, 0x6c, 0x3a, 0x3a, 0x4d, 0x65, 0x73, 0x68, 0x3a, 0x3a, 0x56, 0x32, 0x62, 0x65, 0x74, 0x61,
0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x77, 0x0a, 0x11, 0x48, 0x54, 0x54,
0x50, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x73, 0x12, 0x33,
0x0a, 0x07, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x07, 0x72, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x12, 0x2d, 0x0a, 0x04, 0x69, 0x64, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x04, 0x69, 0x64,
0x6c, 0x65, 0x42, 0x97, 0x02, 0x0a, 0x21, 0x63, 0x6f, 0x6d, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69,
0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x6d, 0x65, 0x73, 0x68,
0x2e, 0x76, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, 0x42, 0x16, 0x48, 0x74, 0x74, 0x70, 0x52, 0x6f,
0x75, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f,
0x50, 0x01, 0x5a, 0x43, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68,
0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2f,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2d, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x2f, 0x70, 0x62, 0x6d,
0x65, 0x73, 0x68, 0x2f, 0x76, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, 0x3b, 0x6d, 0x65, 0x73, 0x68,
0x76, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, 0xa2, 0x02, 0x03, 0x48, 0x43, 0x4d, 0xaa, 0x02, 0x1d,
0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c,
0x2e, 0x4d, 0x65, 0x73, 0x68, 0x2e, 0x56, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, 0xca, 0x02, 0x1d,
0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x5c, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c,
0x5c, 0x4d, 0x65, 0x73, 0x68, 0x5c, 0x56, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, 0xe2, 0x02, 0x29,
0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x5c, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c,
0x5c, 0x4d, 0x65, 0x73, 0x68, 0x5c, 0x56, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, 0x5c, 0x47, 0x50,
0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x20, 0x48, 0x61, 0x73, 0x68,
0x69, 0x63, 0x6f, 0x72, 0x70, 0x3a, 0x3a, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x3a, 0x3a, 0x4d,
0x65, 0x73, 0x68, 0x3a, 0x3a, 0x56, 0x32, 0x62, 0x65, 0x74, 0x61, 0x31, 0x62, 0x06, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x33,
}
var (
@ -173,13 +141,12 @@ var file_pbmesh_v2beta1_http_route_timeouts_proto_goTypes = []interface{}{
}
var file_pbmesh_v2beta1_http_route_timeouts_proto_depIdxs = []int32{
1, // 0: hashicorp.consul.mesh.v2beta1.HTTPRouteTimeouts.request:type_name -> google.protobuf.Duration
1, // 1: hashicorp.consul.mesh.v2beta1.HTTPRouteTimeouts.backend_request:type_name -> google.protobuf.Duration
1, // 2: hashicorp.consul.mesh.v2beta1.HTTPRouteTimeouts.idle:type_name -> google.protobuf.Duration
3, // [3:3] is the sub-list for method output_type
3, // [3:3] is the sub-list for method input_type
3, // [3:3] is the sub-list for extension type_name
3, // [3:3] is the sub-list for extension extendee
0, // [0:3] is the sub-list for field type_name
1, // 1: hashicorp.consul.mesh.v2beta1.HTTPRouteTimeouts.idle:type_name -> google.protobuf.Duration
2, // [2:2] is the sub-list for method output_type
2, // [2:2] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
}
func init() { file_pbmesh_v2beta1_http_route_timeouts_proto_init() }

View File

@ -7,35 +7,13 @@ package hashicorp.consul.mesh.v2beta1;
import "google/protobuf/duration.proto";
// HTTPRouteTimeouts defines timeouts that can be configured for an HTTPRoute.
// Timeout values are formatted like 1h/1m/1s/1ms as parsed by Golang time.ParseDuration
// and MUST BE >= 1ms.
//
// ALTERNATIVE: not using policy attachment semantics
// HTTPRouteTimeouts defines timeouts that can be configured for an HTTPRoute
// or GRPCRoute.
message HTTPRouteTimeouts {
// Request specifies the duration for processing an HTTP client request after which the
// gateway will time out if unable to send a response.
// Whether the gateway starts the timeout before or after the entire client request stream
// has been received, is implementation-dependent.
//
// For example, setting the `rules.timeouts.request` field to the value `10s` in an
// `HTTPRoute` will cause a timeout if a client request is taking longer than 10 seconds
// to complete.
//
// When this field is unspecified, request timeout behavior is implementation-dependent.
// RequestTimeout is the total amount of time permitted for the entire
// downstream request (and retries) to be processed.
google.protobuf.Duration request = 1;
// BackendRequest specifies a timeout for an individual request from the gateway
// to a backend service. Typically used in conjuction with retry configuration,
// if supported by an implementation.
//
// The value of BackendRequest defaults to and must be <= the value of Request timeout.
//
// Support: Extended
//
// TODO(rb): net-new feature
google.protobuf.Duration backend_request = 2;
// TODO(RB): this is a consul-only feature
google.protobuf.Duration idle = 3;
// Idle specifies the total amount of time permitted for the request stream to be idle.
google.protobuf.Duration idle = 2;
}