From 9e48607893bd49f04010bccea0cd1525dcee839d Mon Sep 17 00:00:00 2001 From: "R.B. Boyer" <4903+rboyer@users.noreply.github.com> Date: Fri, 22 Sep 2023 16:13:24 -0500 Subject: [PATCH] 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 --- .../controllers/routes/controller_test.go | 171 +++++---- .../internal/controllers/routes/generate.go | 92 +++++ .../controllers/routes/generate_test.go | 114 +++--- .../mesh/internal/controllers/routes/util.go | 15 +- .../builder/destination_builder.go | 92 ++++- .../builder/destination_builder_test.go | 73 +++- .../sidecarproxy/builder/routes.go | 361 +++++++++++++++--- .../mixed-multi-destination.golden | 30 +- .../mesh/internal/types/computed_routes.go | 30 ++ .../internal/types/computed_routes_test.go | 5 + internal/mesh/internal/types/http_route.go | 12 + .../mesh/internal/types/http_route_test.go | 34 +- internal/mesh/internal/types/xroute.go | 18 +- internal/protoutil/protoutil.go | 23 ++ .../pbmesh/v2beta1/http_route_timeouts.pb.go | 111 ++---- .../pbmesh/v2beta1/http_route_timeouts.proto | 34 +- 16 files changed, 880 insertions(+), 335 deletions(-) create mode 100644 internal/protoutil/protoutil.go diff --git a/internal/mesh/internal/controllers/routes/controller_test.go b/internal/mesh/internal/controllers/routes/controller_test.go index 1e1aefba6d..e53b1fbec7 100644 --- a/internal/mesh/internal/controllers/routes/controller_test.go +++ b/internal/mesh/internal/controllers/routes/controller_test.go @@ -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" @@ -112,9 +111,10 @@ func (suite *controllerSuite) TestController() { Protocol: pbcatalog.Protocol_PROTOCOL_TCP, Targets: map[string]*pbmesh.BackendTargetDetails{ backendName("api", "tcp"): { - Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, - MeshPort: "mesh", - BackendRef: newBackendRef(apiServiceRef, "tcp", ""), + Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, + MeshPort: "mesh", + BackendRef: newBackendRef(apiServiceRef, "tcp", ""), + DestinationConfig: defaultDestConfig(), }, }, }, @@ -184,9 +184,10 @@ func (suite *controllerSuite) TestController() { Protocol: pbcatalog.Protocol_PROTOCOL_TCP, Targets: map[string]*pbmesh.BackendTargetDetails{ backendName("api", "tcp"): { - Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, - MeshPort: "mesh", - BackendRef: newBackendRef(apiServiceRef, "tcp", ""), + Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, + MeshPort: "mesh", + BackendRef: newBackendRef(apiServiceRef, "tcp", ""), + DestinationConfig: defaultDestConfig(), }, }, }, @@ -211,9 +212,10 @@ func (suite *controllerSuite) TestController() { Protocol: pbcatalog.Protocol_PROTOCOL_HTTP, Targets: map[string]*pbmesh.BackendTargetDetails{ backendName("api", "http"): { - Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, - MeshPort: "mesh", - BackendRef: newBackendRef(apiServiceRef, "http", ""), + Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, + MeshPort: "mesh", + BackendRef: newBackendRef(apiServiceRef, "http", ""), + DestinationConfig: defaultDestConfig(), }, }, }, @@ -238,9 +240,10 @@ func (suite *controllerSuite) TestController() { Protocol: pbcatalog.Protocol_PROTOCOL_HTTP2, Targets: map[string]*pbmesh.BackendTargetDetails{ backendName("api", "http2"): { - Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, - MeshPort: "mesh", - BackendRef: newBackendRef(apiServiceRef, "http2", ""), + Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, + MeshPort: "mesh", + BackendRef: newBackendRef(apiServiceRef, "http2", ""), + DestinationConfig: defaultDestConfig(), }, }, }, @@ -260,9 +263,10 @@ func (suite *controllerSuite) TestController() { Protocol: pbcatalog.Protocol_PROTOCOL_GRPC, Targets: map[string]*pbmesh.BackendTargetDetails{ backendName("api", "grpc"): { - Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, - MeshPort: "mesh", - BackendRef: newBackendRef(apiServiceRef, "grpc", ""), + Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, + MeshPort: "mesh", + BackendRef: newBackendRef(apiServiceRef, "grpc", ""), + DestinationConfig: defaultDestConfig(), }, }, }, @@ -347,9 +351,10 @@ func (suite *controllerSuite) TestController() { Protocol: pbcatalog.Protocol_PROTOCOL_TCP, Targets: map[string]*pbmesh.BackendTargetDetails{ backendName("foo", "tcp"): { - Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, - MeshPort: "mesh", - BackendRef: newBackendRef(fooServiceRef, "tcp", ""), + Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, + MeshPort: "mesh", + BackendRef: newBackendRef(fooServiceRef, "tcp", ""), + DestinationConfig: defaultDestConfig(), }, }, }, @@ -376,9 +381,10 @@ func (suite *controllerSuite) TestController() { Protocol: pbcatalog.Protocol_PROTOCOL_HTTP, Targets: map[string]*pbmesh.BackendTargetDetails{ backendName("foo", "http"): { - Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, - MeshPort: "mesh", - BackendRef: newBackendRef(fooServiceRef, "http", ""), + Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, + MeshPort: "mesh", + BackendRef: newBackendRef(fooServiceRef, "http", ""), + DestinationConfig: defaultDestConfig(), }, }, }, @@ -405,9 +411,10 @@ func (suite *controllerSuite) TestController() { Protocol: pbcatalog.Protocol_PROTOCOL_GRPC, Targets: map[string]*pbmesh.BackendTargetDetails{ backendName("foo", "grpc"): { - Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, - MeshPort: "mesh", - BackendRef: newBackendRef(fooServiceRef, "grpc", ""), + Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, + MeshPort: "mesh", + BackendRef: newBackendRef(fooServiceRef, "grpc", ""), + DestinationConfig: defaultDestConfig(), }, }, }, @@ -434,9 +441,10 @@ func (suite *controllerSuite) TestController() { Protocol: pbcatalog.Protocol_PROTOCOL_HTTP2, Targets: map[string]*pbmesh.BackendTargetDetails{ backendName("foo", "http2"): { - Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, - MeshPort: "mesh", - BackendRef: newBackendRef(fooServiceRef, "http2", ""), + Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, + MeshPort: "mesh", + BackendRef: newBackendRef(fooServiceRef, "http2", ""), + DestinationConfig: defaultDestConfig(), }, }, }, @@ -548,9 +556,10 @@ func (suite *controllerSuite) TestController() { Protocol: pbcatalog.Protocol_PROTOCOL_TCP, Targets: map[string]*pbmesh.BackendTargetDetails{ backendName("foo", "tcp"): { - Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, - MeshPort: "mesh", - BackendRef: newBackendRef(fooServiceRef, "tcp", ""), + Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, + MeshPort: "mesh", + BackendRef: newBackendRef(fooServiceRef, "tcp", ""), + DestinationConfig: defaultDestConfig(), }, }, }, @@ -588,9 +597,10 @@ func (suite *controllerSuite) TestController() { Protocol: pbcatalog.Protocol_PROTOCOL_HTTP, Targets: map[string]*pbmesh.BackendTargetDetails{ backendName("foo", "http"): { - Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, - MeshPort: "mesh", - BackendRef: newBackendRef(fooServiceRef, "http", ""), + Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, + MeshPort: "mesh", + BackendRef: newBackendRef(fooServiceRef, "http", ""), + DestinationConfig: defaultDestConfig(), }, }, }, @@ -629,9 +639,10 @@ func (suite *controllerSuite) TestController() { Protocol: pbcatalog.Protocol_PROTOCOL_GRPC, Targets: map[string]*pbmesh.BackendTargetDetails{ backendName("foo", "grpc"): { - Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, - MeshPort: "mesh", - BackendRef: newBackendRef(fooServiceRef, "grpc", ""), + Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, + MeshPort: "mesh", + BackendRef: newBackendRef(fooServiceRef, "grpc", ""), + DestinationConfig: defaultDestConfig(), }, }, }, @@ -669,9 +680,10 @@ func (suite *controllerSuite) TestController() { Protocol: pbcatalog.Protocol_PROTOCOL_HTTP2, Targets: map[string]*pbmesh.BackendTargetDetails{ backendName("foo", "http2"): { - Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, - MeshPort: "mesh", - BackendRef: newBackendRef(fooServiceRef, "http2", ""), + Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, + MeshPort: "mesh", + BackendRef: newBackendRef(fooServiceRef, "http2", ""), + DestinationConfig: defaultDestConfig(), }, }, }, @@ -786,9 +798,10 @@ func (suite *controllerSuite) TestController() { Protocol: pbcatalog.Protocol_PROTOCOL_TCP, Targets: map[string]*pbmesh.BackendTargetDetails{ backendName("foo", "tcp"): { - Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, - MeshPort: "mesh", - BackendRef: newBackendRef(fooServiceRef, "tcp", ""), + Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, + MeshPort: "mesh", + BackendRef: newBackendRef(fooServiceRef, "tcp", ""), + DestinationConfig: defaultDestConfig(), }, }, }, @@ -815,9 +828,10 @@ func (suite *controllerSuite) TestController() { Protocol: pbcatalog.Protocol_PROTOCOL_HTTP, Targets: map[string]*pbmesh.BackendTargetDetails{ backendName("foo", "http"): { - Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, - MeshPort: "mesh", - BackendRef: newBackendRef(fooServiceRef, "http", ""), + Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, + MeshPort: "mesh", + BackendRef: newBackendRef(fooServiceRef, "http", ""), + DestinationConfig: defaultDestConfig(), }, }, }, @@ -844,9 +858,10 @@ func (suite *controllerSuite) TestController() { Protocol: pbcatalog.Protocol_PROTOCOL_GRPC, Targets: map[string]*pbmesh.BackendTargetDetails{ backendName("foo", "grpc"): { - Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, - MeshPort: "mesh", - BackendRef: newBackendRef(fooServiceRef, "grpc", ""), + Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, + MeshPort: "mesh", + BackendRef: newBackendRef(fooServiceRef, "grpc", ""), + DestinationConfig: defaultDestConfig(), }, }, }, @@ -873,9 +888,10 @@ func (suite *controllerSuite) TestController() { Protocol: pbcatalog.Protocol_PROTOCOL_HTTP2, Targets: map[string]*pbmesh.BackendTargetDetails{ backendName("foo", "http2"): { - Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, - MeshPort: "mesh", - BackendRef: newBackendRef(fooServiceRef, "http2", ""), + Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, + MeshPort: "mesh", + BackendRef: newBackendRef(fooServiceRef, "http2", ""), + DestinationConfig: defaultDestConfig(), }, }, }, @@ -952,9 +968,10 @@ func (suite *controllerSuite) TestController() { Protocol: pbcatalog.Protocol_PROTOCOL_TCP, Targets: map[string]*pbmesh.BackendTargetDetails{ backendName("foo", "tcp"): { - Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, - MeshPort: "mesh", - BackendRef: newBackendRef(fooServiceRef, "tcp", ""), + Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, + MeshPort: "mesh", + BackendRef: newBackendRef(fooServiceRef, "tcp", ""), + DestinationConfig: defaultDestConfig(), }, }, }, @@ -981,9 +998,10 @@ func (suite *controllerSuite) TestController() { Protocol: pbcatalog.Protocol_PROTOCOL_HTTP, Targets: map[string]*pbmesh.BackendTargetDetails{ backendName("foo", "http"): { - Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, - MeshPort: "mesh", - BackendRef: newBackendRef(fooServiceRef, "http", ""), + Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, + MeshPort: "mesh", + BackendRef: newBackendRef(fooServiceRef, "http", ""), + DestinationConfig: defaultDestConfig(), }, }, }, @@ -1010,9 +1028,10 @@ func (suite *controllerSuite) TestController() { Protocol: pbcatalog.Protocol_PROTOCOL_GRPC, Targets: map[string]*pbmesh.BackendTargetDetails{ backendName("foo", "grpc"): { - Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, - MeshPort: "mesh", - BackendRef: newBackendRef(fooServiceRef, "grpc", ""), + Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, + MeshPort: "mesh", + BackendRef: newBackendRef(fooServiceRef, "grpc", ""), + DestinationConfig: defaultDestConfig(), }, }, }, @@ -1039,9 +1058,10 @@ func (suite *controllerSuite) TestController() { Protocol: pbcatalog.Protocol_PROTOCOL_HTTP2, Targets: map[string]*pbmesh.BackendTargetDetails{ backendName("foo", "http2"): { - Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, - MeshPort: "mesh", - BackendRef: newBackendRef(fooServiceRef, "http2", ""), + Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, + MeshPort: "mesh", + BackendRef: newBackendRef(fooServiceRef, "http2", ""), + DestinationConfig: defaultDestConfig(), }, }, }, @@ -1166,9 +1186,10 @@ func (suite *controllerSuite) TestController() { Protocol: pbcatalog.Protocol_PROTOCOL_HTTP, Targets: map[string]*pbmesh.BackendTargetDetails{ backendName("api", "http"): { - Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, - MeshPort: "mesh", - BackendRef: newBackendRef(apiServiceRef, "http", ""), + Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, + MeshPort: "mesh", + BackendRef: newBackendRef(apiServiceRef, "http", ""), + DestinationConfig: defaultDestConfig(), }, }, }, @@ -1204,9 +1225,10 @@ func (suite *controllerSuite) TestController() { Protocol: pbcatalog.Protocol_PROTOCOL_HTTP, Targets: map[string]*pbmesh.BackendTargetDetails{ backendName("api", "http"): { - Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, - MeshPort: "mesh", - BackendRef: newBackendRef(apiServiceRef, "http", ""), + Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, + MeshPort: "mesh", + BackendRef: newBackendRef(apiServiceRef, "http", ""), + DestinationConfig: defaultDestConfig(), }, }, }, @@ -1260,9 +1282,10 @@ func (suite *controllerSuite) TestController() { Protocol: pbcatalog.Protocol_PROTOCOL_HTTP, Targets: map[string]*pbmesh.BackendTargetDetails{ backendName("bar", "http"): { - Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, - MeshPort: "mesh", - BackendRef: newBackendRef(barServiceRef, "http", ""), + Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, + MeshPort: "mesh", + BackendRef: newBackendRef(barServiceRef, "http", ""), + DestinationConfig: defaultDestConfig(), }, }, }, diff --git a/internal/mesh/internal/controllers/routes/generate.go b/internal/mesh/internal/controllers/routes/generate.go index 35e72ac354..be8da854ed 100644 --- a/internal/mesh/internal/controllers/routes/generate.go +++ b/internal/mesh/internal/controllers/routes/generate.go @@ -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, diff --git a/internal/mesh/internal/controllers/routes/generate_test.go b/internal/mesh/internal/controllers/routes/generate_test.go index f8450220a2..094670da91 100644 --- a/internal/mesh/internal/controllers/routes/generate_test.go +++ b/internal/mesh/internal/controllers/routes/generate_test.go @@ -222,9 +222,10 @@ func TestGenerateComputedRoutes(t *testing.T) { Protocol: pbcatalog.Protocol_PROTOCOL_TCP, Targets: map[string]*pbmesh.BackendTargetDetails{ backendName("api", "tcp"): { - Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, - MeshPort: "mesh", - BackendRef: newBackendRef(apiServiceRef, "tcp", ""), + Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, + MeshPort: "mesh", + BackendRef: newBackendRef(apiServiceRef, "tcp", ""), + DestinationConfig: defaultDestConfig(), }, }, }, @@ -282,9 +283,10 @@ func TestGenerateComputedRoutes(t *testing.T) { Protocol: protocol, Targets: map[string]*pbmesh.BackendTargetDetails{ backendName("api", protoName): { - Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, - MeshPort: "mesh", - BackendRef: newBackendRef(apiServiceRef, protoName, ""), + Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, + MeshPort: "mesh", + BackendRef: newBackendRef(apiServiceRef, protoName, ""), + DestinationConfig: defaultDestConfig(), }, }, }, @@ -334,9 +336,10 @@ func TestGenerateComputedRoutes(t *testing.T) { Protocol: pbcatalog.Protocol_PROTOCOL_GRPC, Targets: map[string]*pbmesh.BackendTargetDetails{ backendName("api", "grpc"): { - Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, - MeshPort: "mesh", - BackendRef: newBackendRef(apiServiceRef, "grpc", ""), + Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, + MeshPort: "mesh", + BackendRef: newBackendRef(apiServiceRef, "grpc", ""), + DestinationConfig: defaultDestConfig(), }, }, }, @@ -443,9 +446,10 @@ func TestGenerateComputedRoutes(t *testing.T) { Protocol: pbcatalog.Protocol_PROTOCOL_TCP, Targets: map[string]*pbmesh.BackendTargetDetails{ backendName("foo", "tcp"): { - Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, - MeshPort: "mesh", - BackendRef: newBackendRef(fooServiceRef, "tcp", ""), + Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, + MeshPort: "mesh", + BackendRef: newBackendRef(fooServiceRef, "tcp", ""), + DestinationConfig: defaultDestConfig(), }, }, }, @@ -472,9 +476,10 @@ func TestGenerateComputedRoutes(t *testing.T) { Protocol: pbcatalog.Protocol_PROTOCOL_HTTP, Targets: map[string]*pbmesh.BackendTargetDetails{ backendName("foo", "http"): { - Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, - MeshPort: "mesh", - BackendRef: newBackendRef(fooServiceRef, "http", ""), + Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, + MeshPort: "mesh", + BackendRef: newBackendRef(fooServiceRef, "http", ""), + DestinationConfig: defaultDestConfig(), }, }, }, @@ -501,9 +506,10 @@ func TestGenerateComputedRoutes(t *testing.T) { Protocol: pbcatalog.Protocol_PROTOCOL_HTTP2, Targets: map[string]*pbmesh.BackendTargetDetails{ backendName("foo", "http2"): { - Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, - MeshPort: "mesh", - BackendRef: newBackendRef(fooServiceRef, "http2", ""), + Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, + MeshPort: "mesh", + BackendRef: newBackendRef(fooServiceRef, "http2", ""), + DestinationConfig: defaultDestConfig(), }, }, }, @@ -530,9 +536,10 @@ func TestGenerateComputedRoutes(t *testing.T) { Protocol: pbcatalog.Protocol_PROTOCOL_GRPC, Targets: map[string]*pbmesh.BackendTargetDetails{ backendName("foo", "grpc"): { - Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, - MeshPort: "mesh", - BackendRef: newBackendRef(fooServiceRef, "grpc", ""), + Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, + MeshPort: "mesh", + BackendRef: newBackendRef(fooServiceRef, "grpc", ""), + DestinationConfig: defaultDestConfig(), }, }, }, @@ -612,9 +619,10 @@ func TestGenerateComputedRoutes(t *testing.T) { Protocol: protocol, Targets: map[string]*pbmesh.BackendTargetDetails{ backendName("foo", portName): { - Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, - MeshPort: "mesh", - BackendRef: newBackendRef(fooServiceRef, portName, ""), + Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, + MeshPort: "mesh", + BackendRef: newBackendRef(fooServiceRef, portName, ""), + DestinationConfig: defaultDestConfig(), }, }, } @@ -721,9 +729,10 @@ func TestGenerateComputedRoutes(t *testing.T) { Protocol: pbcatalog.Protocol_PROTOCOL_HTTP, Targets: map[string]*pbmesh.BackendTargetDetails{ backendName("api", "http"): { - Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, - MeshPort: "mesh", - BackendRef: newBackendRef(apiServiceRef, "http", ""), + Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, + MeshPort: "mesh", + BackendRef: newBackendRef(apiServiceRef, "http", ""), + DestinationConfig: defaultDestConfig(), }, }, }, @@ -803,9 +812,10 @@ func TestGenerateComputedRoutes(t *testing.T) { Protocol: pbcatalog.Protocol_PROTOCOL_HTTP, Targets: map[string]*pbmesh.BackendTargetDetails{ backendName("api", "http"): { - Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, - MeshPort: "mesh", - BackendRef: newBackendRef(apiServiceRef, "http", ""), + Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, + MeshPort: "mesh", + BackendRef: newBackendRef(apiServiceRef, "http", ""), + DestinationConfig: defaultDestConfig(), }, }, }, @@ -910,9 +920,10 @@ func TestGenerateComputedRoutes(t *testing.T) { Protocol: pbcatalog.Protocol_PROTOCOL_HTTP, Targets: map[string]*pbmesh.BackendTargetDetails{ backendName("foo", "http"): { - Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, - MeshPort: "mesh", - BackendRef: newBackendRef(fooServiceRef, "http", ""), + Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, + MeshPort: "mesh", + BackendRef: newBackendRef(fooServiceRef, "http", ""), + DestinationConfig: defaultDestConfig(), }, }, }, @@ -1062,14 +1073,16 @@ func TestGenerateComputedRoutes(t *testing.T) { Protocol: pbcatalog.Protocol_PROTOCOL_HTTP, Targets: map[string]*pbmesh.BackendTargetDetails{ backendName("foo", "http"): { - Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, - MeshPort: "mesh", - BackendRef: newBackendRef(fooServiceRef, "http", ""), + 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", ""), + Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, + MeshPort: "mesh", + BackendRef: newBackendRef(barServiceRef, "http", ""), + DestinationConfig: defaultDestConfig(), }, }, }, @@ -1155,9 +1168,10 @@ func TestGenerateComputedRoutes(t *testing.T) { Protocol: pbcatalog.Protocol_PROTOCOL_HTTP, Targets: map[string]*pbmesh.BackendTargetDetails{ backendName("foo", "http"): { - Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, - MeshPort: "mesh", - BackendRef: newBackendRef(fooServiceRef, "http", ""), + Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, + MeshPort: "mesh", + BackendRef: newBackendRef(fooServiceRef, "http", ""), + DestinationConfig: defaultDestConfig(), }, }, }, @@ -1648,15 +1662,17 @@ func TestGenerateComputedRoutes(t *testing.T) { Protocol: pbcatalog.Protocol_PROTOCOL_HTTP, Targets: map[string]*pbmesh.BackendTargetDetails{ backendName("foo", "http"): { - Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, - MeshPort: "mesh", - BackendRef: newBackendRef(fooServiceRef, "http", ""), - FailoverConfig: portFailoverConfig, + Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_DIRECT, + 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", ""), + Type: pbmesh.BackendTargetDetailsType_BACKEND_TARGET_DETAILS_TYPE_INDIRECT, + MeshPort: "mesh", + BackendRef: newBackendRef(barServiceRef, "http", ""), + DestinationConfig: defaultDestConfig(), }, }, }, diff --git a/internal/mesh/internal/controllers/routes/util.go b/internal/mesh/internal/controllers/routes/util.go index 0abd5f095a..8f9fb87825 100644 --- a/internal/mesh/internal/controllers/routes/util.go +++ b/internal/mesh/internal/controllers/routes/util.go @@ -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) } diff --git a/internal/mesh/internal/controllers/sidecarproxy/builder/destination_builder.go b/internal/mesh/internal/controllers/sidecarproxy/builder/destination_builder.go index f714223860..7514b96884 100644 --- a/internal/mesh/internal/controllers/sidecarproxy/builder/destination_builder.go +++ b/internal/mesh/internal/controllers/sidecarproxy/builder/destination_builder.go @@ -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 + Match: routeMatch, + 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 + Match: routeMatch, + 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{ diff --git a/internal/mesh/internal/controllers/sidecarproxy/builder/destination_builder_test.go b/internal/mesh/internal/controllers/sidecarproxy/builder/destination_builder_test.go index 50633e674e..19e8e8d8cc 100644 --- a/internal/mesh/internal/controllers/sidecarproxy/builder/destination_builder_test.go +++ b/internal/mesh/internal/controllers/sidecarproxy/builder/destination_builder_test.go @@ -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,28 +153,56 @@ func TestBuildExplicitDestinations(t *testing.T) { Ref: resource.Reference(api1Service.Id, ""), Port: "http", }}, - Rules: []*pbmesh.HTTPRouteRule{{ - BackendRefs: []*pbmesh.HTTPBackendRef{ - { - BackendRef: &pbmesh.BackendReference{ - Ref: resource.Reference(api2Service.Id, ""), + Rules: []*pbmesh.HTTPRouteRule{ + { + Matches: []*pbmesh.HTTPRouteMatch{{ + Path: &pbmesh.HTTPPathMatch{ + Type: pbmesh.PathMatchType_PATH_MATCH_TYPE_PREFIX, + Value: "/split", + }, + }}, + BackendRefs: []*pbmesh.HTTPBackendRef{ + { + BackendRef: &pbmesh.BackendReference{ + Ref: resource.Reference(api2Service.Id, ""), + }, + Weight: 60, + }, + { + BackendRef: &pbmesh.BackendReference{ + Ref: resource.Reference(api1Service.Id, ""), + }, + Weight: 40, + }, + { + BackendRef: &pbmesh.BackendReference{ + Ref: resource.Reference(api3Service.Id, ""), + }, + Weight: 10, }, - Weight: 60, }, - { + }, + { + 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, ""), }, - Weight: 40, + }}, + Timeouts: &pbmesh.HTTPRouteTimeouts{ + Request: durationpb.New(606 * time.Second), // differnet than the 77s }, - { - BackendRef: &pbmesh.BackendReference{ - Ref: resource.Reference(api3Service.Id, ""), - }, - Weight: 10, + 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), diff --git a/internal/mesh/internal/controllers/sidecarproxy/builder/routes.go b/internal/mesh/internal/controllers/sidecarproxy/builder/routes.go index 5ca4fea896..b5c8fafebb 100644 --- a/internal/mesh/internal/controllers/sidecarproxy/builder/routes.go +++ b/internal/mesh/internal/controllers/sidecarproxy/builder/routes.go @@ -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 diff --git a/internal/mesh/internal/controllers/sidecarproxy/builder/testdata/destination/mixed-multi-destination.golden b/internal/mesh/internal/controllers/sidecarproxy/builder/testdata/destination/mixed-multi-destination.golden index a3f7f323cb..5ccf817640 100644 --- a/internal/mesh/internal/controllers/sidecarproxy/builder/testdata/destination/mixed-multi-destination.golden +++ b/internal/mesh/internal/controllers/sidecarproxy/builder/testdata/destination/mixed-multi-destination.golden @@ -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": "/" diff --git a/internal/mesh/internal/types/computed_routes.go b/internal/mesh/internal/types/computed_routes.go index 930f2e1d5d..b057fcb363 100644 --- a/internal/mesh/internal/types/computed_routes.go +++ b/internal/mesh/internal/types/computed_routes.go @@ -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", diff --git a/internal/mesh/internal/types/computed_routes_test.go b/internal/mesh/internal/types/computed_routes_test.go index 0240276390..40c13b3ac0 100644 --- a/internal/mesh/internal/types/computed_routes_test.go +++ b/internal/mesh/internal/types/computed_routes_test.go @@ -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), + }, }, }, }, diff --git a/internal/mesh/internal/types/http_route.go b/internal/mesh/internal/types/http_route.go index 374f4d7de5..d558747a0c 100644 --- a/internal/mesh/internal/types/http_route.go +++ b/internal/mesh/internal/types/http_route.go @@ -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{ diff --git a/internal/mesh/internal/types/http_route_test.go b/internal/mesh/internal/types/http_route_test.go index eec0b2e8c6..66d12bbbfa 100644 --- a/internal/mesh/internal/types/http_route_test.go +++ b/internal/mesh/internal/types/http_route_test.go @@ -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), @@ -1095,9 +1112,8 @@ 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), + Request: durationpb.New(1 * time.Second), + Idle: durationpb.New(3 * time.Second), }, }, } diff --git a/internal/mesh/internal/types/xroute.go b/internal/mesh/internal/types/xroute.go index aef6332b42..449e58b393 100644 --- a/internal/mesh/internal/types/xroute.go +++ b/internal/mesh/internal/types/xroute.go @@ -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), }) } } diff --git a/internal/protoutil/protoutil.go b/internal/protoutil/protoutil.go new file mode 100644 index 0000000000..21a16dd1f3 --- /dev/null +++ b/internal/protoutil/protoutil.go @@ -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 +} diff --git a/proto-public/pbmesh/v2beta1/http_route_timeouts.pb.go b/proto-public/pbmesh/v2beta1/http_route_timeouts.pb.go index d0ad6f8de4..85205f83e4 100644 --- a/proto-public/pbmesh/v2beta1/http_route_timeouts.pb.go +++ b/proto-public/pbmesh/v2beta1/http_route_timeouts.pb.go @@ -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() } diff --git a/proto-public/pbmesh/v2beta1/http_route_timeouts.proto b/proto-public/pbmesh/v2beta1/http_route_timeouts.proto index 31ef51e0fa..b17a522bae 100644 --- a/proto-public/pbmesh/v2beta1/http_route_timeouts.proto +++ b/proto-public/pbmesh/v2beta1/http_route_timeouts.proto @@ -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; }