This commit is contained in:
Chris S. Kim 2023-08-25 12:47:20 -04:00 committed by GitHub
parent c8ef063523
commit ecdcde4309
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 1856 additions and 1188 deletions

3
.changelog/18583.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:feature
mesh: **(Enterprise only)** Adds rate limiting config to service-defaults
```

View File

@ -7,7 +7,7 @@ import (
"fmt"
"github.com/hashicorp/go-hclog"
memdb "github.com/hashicorp/go-memdb"
"github.com/hashicorp/go-memdb"
"github.com/imdario/mergo"
"github.com/mitchellh/copystructure"
@ -141,6 +141,10 @@ func MergeServiceConfig(defaults *structs.ServiceConfigResponse, service *struct
ns.Proxy.EnvoyExtensions = nsExtensions
}
if ratelimit := defaults.RateLimits.ToEnvoyExtension(); ratelimit != nil {
ns.Proxy.EnvoyExtensions = append(ns.Proxy.EnvoyExtensions, *ratelimit)
}
if ns.Proxy.MeshGateway.Mode == structs.MeshGatewayModeDefault {
ns.Proxy.MeshGateway.Mode = defaults.MeshGateway.Mode
}

View File

@ -972,3 +972,111 @@ func Test_MergeServiceConfig_UpstreamOverrides(t *testing.T) {
})
}
}
// Tests that RateLimit config is a no-op in non-enterprise.
// In practice, the ratelimit config would have been validated
// on write.
func Test_MergeServiceConfig_RateLimit(t *testing.T) {
rl := structs.RateLimits{
InstanceLevel: structs.InstanceLevelRateLimits{
RequestsPerSecond: 1234,
RequestsMaxBurst: 2345,
Routes: []structs.InstanceLevelRouteRateLimits{
{
PathExact: "/admin",
RequestsPerSecond: 3333,
RequestsMaxBurst: 4444,
},
},
},
}
tests := []struct {
name string
defaults *structs.ServiceConfigResponse
service *structs.NodeService
want *structs.NodeService
}{
{
name: "injects ratelimit extension",
defaults: &structs.ServiceConfigResponse{
RateLimits: rl,
},
service: &structs.NodeService{
ID: "foo-proxy",
Service: "foo-proxy",
Proxy: structs.ConnectProxyConfig{
DestinationServiceName: "foo",
DestinationServiceID: "foo",
},
},
want: &structs.NodeService{
ID: "foo-proxy",
Service: "foo-proxy",
Proxy: structs.ConnectProxyConfig{
DestinationServiceName: "foo",
DestinationServiceID: "foo",
EnvoyExtensions: func() []structs.EnvoyExtension {
if ext := rl.ToEnvoyExtension(); ext != nil {
return []structs.EnvoyExtension{*ext}
}
return nil
}(),
},
},
},
{
name: "injects ratelimit extension at the end",
defaults: &structs.ServiceConfigResponse{
RateLimits: rl,
EnvoyExtensions: []structs.EnvoyExtension{
{
Name: "existing-ext",
Required: true,
Arguments: map[string]interface{}{
"arg1": "val1",
},
},
},
},
service: &structs.NodeService{
ID: "foo-proxy",
Service: "foo-proxy",
Proxy: structs.ConnectProxyConfig{
DestinationServiceName: "foo",
DestinationServiceID: "foo",
},
},
want: &structs.NodeService{
ID: "foo-proxy",
Service: "foo-proxy",
Proxy: structs.ConnectProxyConfig{
DestinationServiceName: "foo",
DestinationServiceID: "foo",
EnvoyExtensions: func() []structs.EnvoyExtension {
existing := []structs.EnvoyExtension{
{
Name: "existing-ext",
Required: true,
Arguments: map[string]interface{}{
"arg1": "val1",
},
},
}
if ext := rl.ToEnvoyExtension(); ext != nil {
existing = append(existing, *ext)
}
return existing
}(),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := MergeServiceConfig(tt.defaults, tt.service)
require.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}

View File

@ -117,6 +117,9 @@ func ComputeResolvedServiceConfig(
if serviceConf.Destination != nil {
thisReply.Destination = *serviceConf.Destination
}
if serviceConf.RateLimits != nil {
thisReply.RateLimits = *serviceConf.RateLimits
}
// Populate values for the proxy config map
proxyConf := thisReply.ProxyConfig

View File

@ -165,6 +165,7 @@ type ServiceConfigEntry struct {
LocalConnectTimeoutMs int `json:",omitempty" alias:"local_connect_timeout_ms"`
LocalRequestTimeoutMs int `json:",omitempty" alias:"local_request_timeout_ms"`
BalanceInboundConnections string `json:",omitempty" alias:"balance_inbound_connections"`
RateLimits *RateLimits `json:",omitempty" alias:"rate_limits"`
EnvoyExtensions EnvoyExtensions `json:",omitempty" alias:"envoy_extensions"`
Meta map[string]string `json:",omitempty"`
@ -286,6 +287,10 @@ func (e *ServiceConfigEntry) Validate() error {
}
}
if err := validateRatelimit(e.RateLimits); err != nil {
validationErr = multierror.Append(validationErr, err)
}
if err := envoyextensions.ValidateExtensions(e.EnvoyExtensions.ToAPI()); err != nil {
validationErr = multierror.Append(validationErr, err)
}
@ -382,16 +387,52 @@ type DestinationConfig struct {
Port int `json:",omitempty"`
}
func IsHostname(address string) bool {
ip := net.ParseIP(address)
return ip == nil
}
func IsIP(address string) bool {
ip := net.ParseIP(address)
return ip != nil
}
// RateLimits is rate limiting configuration that is applied to
// inbound traffic for a service.
// Rate limiting is a Consul enterprise feature.
type RateLimits struct {
InstanceLevel InstanceLevelRateLimits `alias:"instance_level"`
}
// InstanceLevelRateLimits represents rate limit configuration
// that are applied per service instance.
type InstanceLevelRateLimits struct {
// RequestsPerSecond is the average number of requests per second that can be
// made without being throttled. This field is required if RequestsMaxBurst
// is set. The allowed number of requests may exceed RequestsPerSecond up to
// the value specified in RequestsMaxBurst.
//
// Internally, this is the refill rate of the token bucket used for rate limiting.
RequestsPerSecond int `alias:"requests_per_second"`
// RequestsMaxBurst is the maximum number of requests that can be sent
// in a burst. Should be equal to or greater than RequestsPerSecond.
// If unset, defaults to RequestsPerSecond.
//
// Internally, this is the maximum size of the token bucket used for rate limiting.
RequestsMaxBurst int `alias:"requests_max_burst"`
// Routes is a list of rate limits applied to specific routes.
// Overrides any top-level configuration.
Routes []InstanceLevelRouteRateLimits
}
// InstanceLevelRouteRateLimits represents rate limit configuration
// applied to a route matching one of PathExact/PathPrefix/PathRegex.
type InstanceLevelRouteRateLimits struct {
PathExact string `alias:"path_exact"`
PathPrefix string `alias:"path_prefix"`
PathRegex string `alias:"path_regex"`
RequestsPerSecond int `alias:"requests_per_second"`
RequestsMaxBurst int `alias:"requests_max_burst"`
}
// ProxyConfigEntry is the top-level struct for global proxy configuration defaults.
type ProxyConfigEntry struct {
Kind string
@ -1218,6 +1259,7 @@ type ServiceConfigResponse struct {
Mode ProxyMode `json:",omitempty"`
Destination DestinationConfig `json:",omitempty"`
AccessLogs AccessLogsConfig `json:",omitempty"`
RateLimits RateLimits `json:",omitempty"`
Meta map[string]string `json:",omitempty"`
EnvoyExtensions []EnvoyExtension `json:",omitempty"`
QueryMeta

View File

@ -53,3 +53,12 @@ func validateExportedServicesName(name string) error {
func makeEnterpriseConfigEntry(kind, name string) ConfigEntry {
return nil
}
func validateRatelimit(rl *RateLimits) error {
if rl != nil {
return fmt.Errorf("invalid rate_limits config. Rate limiting is a consul enterprise feature")
}
return nil
}
func (rl RateLimits) ToEnvoyExtension() *EnvoyExtension { return nil }

View File

@ -897,6 +897,14 @@ func (o *ServiceConfigEntry) DeepCopy() *ServiceConfigEntry {
copy(cp.Destination.Addresses, o.Destination.Addresses)
}
}
if o.RateLimits != nil {
cp.RateLimits = new(RateLimits)
*cp.RateLimits = *o.RateLimits
if o.RateLimits.InstanceLevel.Routes != nil {
cp.RateLimits.InstanceLevel.Routes = make([]InstanceLevelRouteRateLimits, len(o.RateLimits.InstanceLevel.Routes))
copy(cp.RateLimits.InstanceLevel.Routes, o.RateLimits.InstanceLevel.Routes)
}
}
if o.EnvoyExtensions != nil {
cp.EnvoyExtensions = make([]EnvoyExtension, len(o.EnvoyExtensions))
copy(cp.EnvoyExtensions, o.EnvoyExtensions)
@ -947,6 +955,10 @@ func (o *ServiceConfigResponse) DeepCopy() *ServiceConfigResponse {
cp.Destination.Addresses = make([]string, len(o.Destination.Addresses))
copy(cp.Destination.Addresses, o.Destination.Addresses)
}
if o.RateLimits.InstanceLevel.Routes != nil {
cp.RateLimits.InstanceLevel.Routes = make([]InstanceLevelRouteRateLimits, len(o.RateLimits.InstanceLevel.Routes))
copy(cp.RateLimits.InstanceLevel.Routes, o.RateLimits.InstanceLevel.Routes)
}
if o.Meta != nil {
cp.Meta = make(map[string]string, len(o.Meta))
for k2, v2 := range o.Meta {

View File

@ -314,6 +314,47 @@ type UpstreamLimits struct {
MaxConcurrentRequests *int `alias:"max_concurrent_requests"`
}
// RateLimits is rate limiting configuration that is applied to
// inbound traffic for a service.
// Rate limiting is a Consul enterprise feature.
type RateLimits struct {
InstanceLevel InstanceLevelRateLimits `alias:"instance_level"`
}
// InstanceLevelRateLimits represents rate limit configuration
// that are applied per service instance.
type InstanceLevelRateLimits struct {
// RequestsPerSecond is the average number of requests per second that can be
// made without being throttled. This field is required if RequestsMaxBurst
// is set. The allowed number of requests may exceed RequestsPerSecond up to
// the value specified in RequestsMaxBurst.
//
// Internally, this is the refill rate of the token bucket used for rate limiting.
RequestsPerSecond int `alias:"requests_per_second"`
// RequestsMaxBurst is the maximum number of requests that can be sent
// in a burst. Should be equal to or greater than RequestsPerSecond.
// If unset, defaults to RequestsPerSecond.
//
// Internally, this is the maximum size of the token bucket used for rate limiting.
RequestsMaxBurst int `alias:"requests_max_burst"`
// Routes is a list of rate limits applied to specific routes.
// Overrides any top-level configuration.
Routes []InstanceLevelRouteRateLimits
}
// InstanceLevelRouteRateLimits represents rate limit configuration
// applied to a route matching one of PathExact/PathPrefix/PathRegex.
type InstanceLevelRouteRateLimits struct {
PathExact string `alias:"path_exact"`
PathPrefix string `alias:"path_prefix"`
PathRegex string `alias:"path_regex"`
RequestsPerSecond int `alias:"requests_per_second"`
RequestsMaxBurst int `alias:"requests_max_burst"`
}
type ServiceConfigEntry struct {
Kind string
Name string
@ -332,6 +373,7 @@ type ServiceConfigEntry struct {
LocalConnectTimeoutMs int `json:",omitempty" alias:"local_connect_timeout_ms"`
LocalRequestTimeoutMs int `json:",omitempty" alias:"local_request_timeout_ms"`
BalanceInboundConnections string `json:",omitempty" alias:"balance_inbound_connections"`
RateLimits *RateLimits `json:",omitempty" alias:"rate_limits"`
EnvoyExtensions []EnvoyExtension `json:",omitempty" alias:"envoy_extensions"`
Meta map[string]string `json:",omitempty"`
CreateIndex uint64

View File

@ -938,6 +938,58 @@ func InlineCertificateFromStructs(t *structs.InlineCertificateConfigEntry, s *In
s.PrivateKey = t.PrivateKey
s.Meta = t.Meta
}
func InstanceLevelRateLimitsToStructs(s *InstanceLevelRateLimits, t *structs.InstanceLevelRateLimits) {
if s == nil {
return
}
t.RequestsPerSecond = int(s.RequestsPerSecond)
t.RequestsMaxBurst = int(s.RequestsMaxBurst)
{
t.Routes = make([]structs.InstanceLevelRouteRateLimits, len(s.Routes))
for i := range s.Routes {
if s.Routes[i] != nil {
InstanceLevelRouteRateLimitsToStructs(s.Routes[i], &t.Routes[i])
}
}
}
}
func InstanceLevelRateLimitsFromStructs(t *structs.InstanceLevelRateLimits, s *InstanceLevelRateLimits) {
if s == nil {
return
}
s.RequestsPerSecond = uint32(t.RequestsPerSecond)
s.RequestsMaxBurst = uint32(t.RequestsMaxBurst)
{
s.Routes = make([]*InstanceLevelRouteRateLimits, len(t.Routes))
for i := range t.Routes {
{
var x InstanceLevelRouteRateLimits
InstanceLevelRouteRateLimitsFromStructs(&t.Routes[i], &x)
s.Routes[i] = &x
}
}
}
}
func InstanceLevelRouteRateLimitsToStructs(s *InstanceLevelRouteRateLimits, t *structs.InstanceLevelRouteRateLimits) {
if s == nil {
return
}
t.PathExact = s.PathExact
t.PathPrefix = s.PathPrefix
t.PathRegex = s.PathRegex
t.RequestsPerSecond = int(s.RequestsPerSecond)
t.RequestsMaxBurst = int(s.RequestsMaxBurst)
}
func InstanceLevelRouteRateLimitsFromStructs(t *structs.InstanceLevelRouteRateLimits, s *InstanceLevelRouteRateLimits) {
if s == nil {
return
}
s.PathExact = t.PathExact
s.PathPrefix = t.PathPrefix
s.PathRegex = t.PathRegex
s.RequestsPerSecond = uint32(t.RequestsPerSecond)
s.RequestsMaxBurst = uint32(t.RequestsMaxBurst)
}
func IntentionHTTPHeaderPermissionToStructs(s *IntentionHTTPHeaderPermission, t *structs.IntentionHTTPHeaderPermission) {
if s == nil {
return
@ -1648,6 +1700,24 @@ func PeeringMeshConfigFromStructs(t *structs.PeeringMeshConfig, s *PeeringMeshCo
}
s.PeerThroughMeshGateways = t.PeerThroughMeshGateways
}
func RateLimitsToStructs(s *RateLimits, t *structs.RateLimits) {
if s == nil {
return
}
if s.InstanceLevel != nil {
InstanceLevelRateLimitsToStructs(s.InstanceLevel, &t.InstanceLevel)
}
}
func RateLimitsFromStructs(t *structs.RateLimits, s *RateLimits) {
if s == nil {
return
}
{
var x InstanceLevelRateLimits
InstanceLevelRateLimitsFromStructs(&t.InstanceLevel, &x)
s.InstanceLevel = &x
}
}
func RemoteJWKSToStructs(s *RemoteJWKS, t *structs.RemoteJWKS) {
if s == nil {
return
@ -1833,6 +1903,11 @@ func ServiceDefaultsToStructs(s *ServiceDefaults, t *structs.ServiceConfigEntry)
t.LocalConnectTimeoutMs = int(s.LocalConnectTimeoutMs)
t.LocalRequestTimeoutMs = int(s.LocalRequestTimeoutMs)
t.BalanceInboundConnections = s.BalanceInboundConnections
if s.RateLimits != nil {
var x structs.RateLimits
RateLimitsToStructs(s.RateLimits, &x)
t.RateLimits = &x
}
t.EnvoyExtensions = EnvoyExtensionsToStructs(s.EnvoyExtensions)
t.Meta = s.Meta
}
@ -1873,6 +1948,11 @@ func ServiceDefaultsFromStructs(t *structs.ServiceConfigEntry, s *ServiceDefault
s.LocalConnectTimeoutMs = int32(t.LocalConnectTimeoutMs)
s.LocalRequestTimeoutMs = int32(t.LocalRequestTimeoutMs)
s.BalanceInboundConnections = t.BalanceInboundConnections
if t.RateLimits != nil {
var x RateLimits
RateLimitsFromStructs(t.RateLimits, &x)
s.RateLimits = &x
}
s.EnvoyExtensions = EnvoyExtensionsFromStructs(t.EnvoyExtensions)
s.Meta = t.Meta
}

View File

@ -457,6 +457,36 @@ func (msg *DestinationConfig) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *RateLimits) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (msg *RateLimits) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *InstanceLevelRateLimits) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (msg *InstanceLevelRateLimits) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *InstanceLevelRouteRateLimits) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (msg *InstanceLevelRouteRateLimits) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *APIGateway) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)

File diff suppressed because it is too large Load Diff

View File

@ -507,6 +507,7 @@ message ServiceDefaults {
// mog: func-to=int func-from=int32
int32 LocalRequestTimeoutMs = 11;
string BalanceInboundConnections = 12;
RateLimits RateLimits = 16;
map<string, string> Meta = 13;
// mog: func-to=EnvoyExtensionsToStructs func-from=EnvoyExtensionsFromStructs
repeated hashicorp.consul.internal.common.EnvoyExtension EnvoyExtensions = 14;
@ -652,6 +653,43 @@ message DestinationConfig {
int32 Port = 2;
}
// mog annotation:
//
// target=github.com/hashicorp/consul/agent/structs.RateLimits
// output=config_entry.gen.go
// name=Structs
message RateLimits {
InstanceLevelRateLimits InstanceLevel = 1;
}
// mog annotation:
//
// target=github.com/hashicorp/consul/agent/structs.InstanceLevelRateLimits
// output=config_entry.gen.go
// name=Structs
message InstanceLevelRateLimits {
// mog: func-to=int func-from=uint32
uint32 RequestsPerSecond = 1;
// mog: func-to=int func-from=uint32
uint32 RequestsMaxBurst = 2;
repeated InstanceLevelRouteRateLimits Routes = 3;
}
// mog annotation:
//
// target=github.com/hashicorp/consul/agent/structs.InstanceLevelRouteRateLimits
// output=config_entry.gen.go
// name=Structs
message InstanceLevelRouteRateLimits {
string PathExact = 1;
string PathPrefix = 2;
string PathRegex = 3;
// mog: func-to=int func-from=uint32
uint32 RequestsPerSecond = 4;
// mog: func-to=int func-from=uint32
uint32 RequestsMaxBurst = 5;
}
// mog annotation:
//
// target=github.com/hashicorp/consul/agent/structs.APIGatewayConfigEntry