feat(ingress gateway: support configuring limits in ingress-gateway c… (#14749)

* feat(ingress gateway: support configuring limits in ingress-gateway config entry

- a new Defaults field with max_connections, max_pending_connections, max_requests
  is added to ingress gateway config entry
- new field max_connections, max_pending_connections, max_requests in
  individual services to overwrite the value in Default
- added unit test and integration test
- updated doc

Co-authored-by: Chris S. Kim <ckim@hashicorp.com>
Co-authored-by: Jeff Boruszak <104028618+boruszak@users.noreply.github.com>
Co-authored-by: Dan Stough <dan.stough@hashicorp.com>
This commit is contained in:
cskh 2022-09-28 14:56:46 -04:00 committed by GitHub
parent 099f29387b
commit 4ece020bf1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 915 additions and 286 deletions

3
.changelog/14749.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:feature
config-entry(ingress-gateway): Added support for `max_connections` for upstream clusters
```

View File

@ -98,6 +98,9 @@ func (s *handlerIngressGateway) handleUpdate(ctx context.Context, u UpdateEvent,
snap.IngressGateway.GatewayConfigLoaded = true snap.IngressGateway.GatewayConfigLoaded = true
snap.IngressGateway.TLSConfig = gatewayConf.TLS snap.IngressGateway.TLSConfig = gatewayConf.TLS
if gatewayConf.Defaults != nil {
snap.IngressGateway.Defaults = *gatewayConf.Defaults
}
// Load each listener's config from the config entry so we don't have to // Load each listener's config from the config entry so we don't have to
// pass listener config through "upstreams" types as that grows. // pass listener config through "upstreams" types as that grows.

View File

@ -619,6 +619,9 @@ type configSnapshotIngressGateway struct {
// Listeners is the original listener config from the ingress-gateway config // Listeners is the original listener config from the ingress-gateway config
// entry to save us trying to pass fields through Upstreams // entry to save us trying to pass fields through Upstreams
Listeners map[IngressListenerKey]structs.IngressListener Listeners map[IngressListenerKey]structs.IngressListener
// Defaults is the default configuration for upstream service instances
Defaults structs.IngressServiceConfig
} }
// isEmpty is a test helper // isEmpty is a test helper

View File

@ -31,11 +31,20 @@ type IngressGatewayConfigEntry struct {
// what services to associated to those ports. // what services to associated to those ports.
Listeners []IngressListener Listeners []IngressListener
// Defaults contains default configuration for all upstream service instances
Defaults *IngressServiceConfig `json:",omitempty"`
Meta map[string]string `json:",omitempty"` Meta map[string]string `json:",omitempty"`
acl.EnterpriseMeta `hcl:",squash" mapstructure:",squash"` acl.EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
RaftIndex RaftIndex
} }
type IngressServiceConfig struct {
MaxConnections uint32
MaxPendingRequests uint32
MaxConcurrentRequests uint32
}
type IngressListener struct { type IngressListener struct {
// Port declares the port on which the ingress gateway should listen for traffic. // Port declares the port on which the ingress gateway should listen for traffic.
Port int Port int
@ -90,6 +99,10 @@ type IngressService struct {
RequestHeaders *HTTPHeaderModifiers `json:",omitempty" alias:"request_headers"` RequestHeaders *HTTPHeaderModifiers `json:",omitempty" alias:"request_headers"`
ResponseHeaders *HTTPHeaderModifiers `json:",omitempty" alias:"response_headers"` ResponseHeaders *HTTPHeaderModifiers `json:",omitempty" alias:"response_headers"`
MaxConnections uint32 `json:",omitempty" alias:"max_connections"`
MaxPendingRequests uint32 `json:",omitempty" alias:"max_pending_requests"`
MaxConcurrentRequests uint32 `json:",omitempty" alias:"max_concurrent_requests"`
Meta map[string]string `json:",omitempty"` Meta map[string]string `json:",omitempty"`
acl.EnterpriseMeta `hcl:",squash" mapstructure:",squash"` acl.EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
} }

View File

@ -630,7 +630,7 @@ func (s *ResourceGenerator) injectGatewayDestinationAddons(cfgSnap *proxycfg.Con
func (s *ResourceGenerator) clustersFromSnapshotIngressGateway(cfgSnap *proxycfg.ConfigSnapshot) ([]proto.Message, error) { func (s *ResourceGenerator) clustersFromSnapshotIngressGateway(cfgSnap *proxycfg.ConfigSnapshot) ([]proto.Message, error) {
var clusters []proto.Message var clusters []proto.Message
createdClusters := make(map[proxycfg.UpstreamID]bool) createdClusters := make(map[proxycfg.UpstreamID]bool)
for _, upstreams := range cfgSnap.IngressGateway.Upstreams { for listenerKey, upstreams := range cfgSnap.IngressGateway.Upstreams {
for _, u := range upstreams { for _, u := range upstreams {
uid := proxycfg.NewUpstreamID(&u) uid := proxycfg.NewUpstreamID(&u)
@ -658,6 +658,7 @@ func (s *ResourceGenerator) clustersFromSnapshotIngressGateway(cfgSnap *proxycfg
} }
for _, c := range upstreamClusters { for _, c := range upstreamClusters {
s.configIngressUpstreamCluster(c, cfgSnap, listenerKey, &u)
clusters = append(clusters, c) clusters = append(clusters, c)
} }
createdClusters[uid] = true createdClusters[uid] = true
@ -666,6 +667,50 @@ func (s *ResourceGenerator) clustersFromSnapshotIngressGateway(cfgSnap *proxycfg
return clusters, nil return clusters, nil
} }
func (s *ResourceGenerator) configIngressUpstreamCluster(c *envoy_cluster_v3.Cluster, cfgSnap *proxycfg.ConfigSnapshot, listenerKey proxycfg.IngressListenerKey, u *structs.Upstream) {
var threshold *envoy_cluster_v3.CircuitBreakers_Thresholds
setThresholdLimit := func(limitType string, limit int) {
if limit <= 0 {
return
}
if threshold == nil {
threshold = &envoy_cluster_v3.CircuitBreakers_Thresholds{}
}
switch limitType {
case "max_connections":
threshold.MaxConnections = makeUint32Value(limit)
case "max_pending_requests":
threshold.MaxPendingRequests = makeUint32Value(limit)
case "max_requests":
threshold.MaxRequests = makeUint32Value(limit)
}
}
setThresholdLimit("max_connections", int(cfgSnap.IngressGateway.Defaults.MaxConnections))
setThresholdLimit("max_pending_requests", int(cfgSnap.IngressGateway.Defaults.MaxPendingRequests))
setThresholdLimit("max_requests", int(cfgSnap.IngressGateway.Defaults.MaxConcurrentRequests))
// Adjust the limit for upstream service
// Lookup listener and service config details from ingress gateway
// definition.
var svc *structs.IngressService
if lCfg, ok := cfgSnap.IngressGateway.Listeners[listenerKey]; ok {
svc = findIngressServiceMatchingUpstream(lCfg, *u)
}
if svc != nil {
setThresholdLimit("max_connections", int(svc.MaxConnections))
setThresholdLimit("max_pending_requests", int(svc.MaxPendingRequests))
setThresholdLimit("max_requests", int(svc.MaxConcurrentRequests))
}
if threshold != nil {
c.CircuitBreakers.Thresholds = []*envoy_cluster_v3.CircuitBreakers_Thresholds{threshold}
}
}
func (s *ResourceGenerator) makeAppCluster(cfgSnap *proxycfg.ConfigSnapshot, name, pathProtocol string, port int) (*envoy_cluster_v3.Cluster, error) { func (s *ResourceGenerator) makeAppCluster(cfgSnap *proxycfg.ConfigSnapshot, name, pathProtocol string, port int) (*envoy_cluster_v3.Cluster, error) {
var c *envoy_cluster_v3.Cluster var c *envoy_cluster_v3.Cluster
var err error var err error

View File

@ -493,6 +493,45 @@ func TestClustersFromSnapshot(t *testing.T) {
"simple", nil, nil, nil) "simple", nil, nil, nil)
}, },
}, },
{
name: "ingress-with-service-max-connections",
create: func(t testinf.T) *proxycfg.ConfigSnapshot {
return proxycfg.TestConfigSnapshotIngressGateway(t, true, "tcp",
"simple", nil,
func(entry *structs.IngressGatewayConfigEntry) {
entry.Listeners[0].Services[0].MaxConnections = 4096
}, nil)
},
},
{
name: "ingress-with-defaults-service-max-connections",
create: func(t testinf.T) *proxycfg.ConfigSnapshot {
return proxycfg.TestConfigSnapshotIngressGateway(t, true, "tcp",
"simple", nil,
func(entry *structs.IngressGatewayConfigEntry) {
entry.Defaults = &structs.IngressServiceConfig{
MaxConnections: 2048,
MaxPendingRequests: 512,
MaxConcurrentRequests: 4096,
}
}, nil)
},
},
{
name: "ingress-with-overwrite-defaults-service-max-connections",
create: func(t testinf.T) *proxycfg.ConfigSnapshot {
return proxycfg.TestConfigSnapshotIngressGateway(t, true, "tcp",
"simple", nil,
func(entry *structs.IngressGatewayConfigEntry) {
entry.Defaults = &structs.IngressServiceConfig{
MaxConnections: 2048,
MaxPendingRequests: 512,
}
entry.Listeners[0].Services[0].MaxConnections = 4096
entry.Listeners[0].Services[0].MaxPendingRequests = 2048
}, nil)
},
},
{ {
name: "ingress-with-chain-external-sni", name: "ingress-with-chain-external-sni",
create: func(t testinf.T) *proxycfg.ConfigSnapshot { create: func(t testinf.T) *proxycfg.ConfigSnapshot {

View File

@ -0,0 +1,71 @@
{
"versionInfo": "00000001",
"resources": [
{
"@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster",
"name": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
"altStatName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
"type": "EDS",
"edsClusterConfig": {
"edsConfig": {
"ads": {
},
"resourceApiVersion": "V3"
}
},
"connectTimeout": "33s",
"circuitBreakers": {
"thresholds":[
{
"maxConnections": 2048,
"maxPendingRequests": 512,
"maxRequests": 4096
}
]
},
"outlierDetection": {
},
"commonLbConfig": {
"healthyPanicThreshold": {
}
},
"transportSocket": {
"name": "tls",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext",
"commonTlsContext": {
"tlsParams": {
},
"tlsCertificates": [
{
"certificateChain": {
"inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n"
},
"privateKey": {
"inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n"
}
}
],
"validationContext": {
"trustedCa": {
"inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n"
},
"matchSubjectAltNames": [
{
"exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/db"
}
]
}
},
"sni": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
}
}
],
"typeUrl": "type.googleapis.com/envoy.config.cluster.v3.Cluster",
"nonce": "00000001"
}

View File

@ -0,0 +1,70 @@
{
"versionInfo": "00000001",
"resources": [
{
"@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster",
"name": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
"altStatName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
"type": "EDS",
"edsClusterConfig": {
"edsConfig": {
"ads": {
},
"resourceApiVersion": "V3"
}
},
"connectTimeout": "33s",
"circuitBreakers": {
"thresholds":[
{
"maxConnections": 4096,
"maxPendingRequests": 2048
}
]
},
"outlierDetection": {
},
"commonLbConfig": {
"healthyPanicThreshold": {
}
},
"transportSocket": {
"name": "tls",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext",
"commonTlsContext": {
"tlsParams": {
},
"tlsCertificates": [
{
"certificateChain": {
"inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n"
},
"privateKey": {
"inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n"
}
}
],
"validationContext": {
"trustedCa": {
"inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n"
},
"matchSubjectAltNames": [
{
"exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/db"
}
]
}
},
"sni": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
}
}
],
"typeUrl": "type.googleapis.com/envoy.config.cluster.v3.Cluster",
"nonce": "00000001"
}

View File

@ -0,0 +1,69 @@
{
"versionInfo": "00000001",
"resources": [
{
"@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster",
"name": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
"altStatName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
"type": "EDS",
"edsClusterConfig": {
"edsConfig": {
"ads": {
},
"resourceApiVersion": "V3"
}
},
"connectTimeout": "33s",
"circuitBreakers": {
"thresholds":[
{
"maxConnections": 4096
}
]
},
"outlierDetection": {
},
"commonLbConfig": {
"healthyPanicThreshold": {
}
},
"transportSocket": {
"name": "tls",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext",
"commonTlsContext": {
"tlsParams": {
},
"tlsCertificates": [
{
"certificateChain": {
"inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n"
},
"privateKey": {
"inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n"
}
}
],
"validationContext": {
"trustedCa": {
"inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n"
},
"matchSubjectAltNames": [
{
"exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/db"
}
]
}
},
"sni": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
}
}
],
"typeUrl": "type.googleapis.com/envoy.config.cluster.v3.Cluster",
"nonce": "00000001"
}

View File

@ -27,6 +27,9 @@ type IngressGatewayConfigEntry struct {
Meta map[string]string `json:",omitempty"` Meta map[string]string `json:",omitempty"`
// Defaults is default configuration for all upstream services
Defaults *IngressServiceConfig `json:",omitempty"`
// CreateIndex is the Raft index this entry was created at. This is a // CreateIndex is the Raft index this entry was created at. This is a
// read-only field. // read-only field.
CreateIndex uint64 CreateIndex uint64
@ -37,6 +40,12 @@ type IngressGatewayConfigEntry struct {
ModifyIndex uint64 ModifyIndex uint64
} }
type IngressServiceConfig struct {
MaxConnections *uint32
MaxPendingRequests *uint32
MaxConcurrentRequests *uint32
}
type GatewayTLSConfig struct { type GatewayTLSConfig struct {
// Indicates that TLS should be enabled for this gateway service. // Indicates that TLS should be enabled for this gateway service.
Enabled bool Enabled bool
@ -124,6 +133,10 @@ type IngressService struct {
// Allow HTTP header manipulation to be configured. // Allow HTTP header manipulation to be configured.
RequestHeaders *HTTPHeaderModifiers `json:",omitempty" alias:"request_headers"` RequestHeaders *HTTPHeaderModifiers `json:",omitempty" alias:"request_headers"`
ResponseHeaders *HTTPHeaderModifiers `json:",omitempty" alias:"response_headers"` ResponseHeaders *HTTPHeaderModifiers `json:",omitempty" alias:"response_headers"`
MaxConnections *uint32 `json:",omitempty" alias:"max_connections"`
MaxPendingRequests *uint32 `json:",omitempty" alias:"max_pending_requests"`
MaxConcurrentRequests *uint32 `json:",omitempty" alias:"max_concurrent_requests"`
} }
func (i *IngressGatewayConfigEntry) GetKind() string { return i.Kind } func (i *IngressGatewayConfigEntry) GetKind() string { return i.Kind }

View File

@ -29,6 +29,10 @@ func TestAPI_ConfigEntries_IngressGateway(t *testing.T) {
Enabled: true, Enabled: true,
TLSMinVersion: "TLSv1_2", TLSMinVersion: "TLSv1_2",
}, },
Defaults: &IngressServiceConfig{
MaxConnections: uint32Pointer(2048),
MaxPendingRequests: uint32Pointer(4096),
},
} }
global := &ProxyConfigEntry{ global := &ProxyConfigEntry{
@ -93,6 +97,9 @@ func TestAPI_ConfigEntries_IngressGateway(t *testing.T) {
CertResource: "bar", CertResource: "bar",
}, },
}, },
MaxConnections: uint32Pointer(5120),
MaxPendingRequests: uint32Pointer(512),
MaxConcurrentRequests: uint32Pointer(2048),
}, },
}, },
TLS: &GatewayTLSConfig{ TLS: &GatewayTLSConfig{
@ -168,6 +175,9 @@ func TestAPI_ConfigEntries_IngressGateway(t *testing.T) {
require.True(t, ok) require.True(t, ok)
require.Equal(t, ingress2.Kind, readIngress.Kind) require.Equal(t, ingress2.Kind, readIngress.Kind)
require.Equal(t, ingress2.Name, readIngress.Name) require.Equal(t, ingress2.Name, readIngress.Name)
require.Equal(t, *ingress2.Defaults.MaxConnections, *readIngress.Defaults.MaxConnections)
require.Equal(t, uint32(4096), *readIngress.Defaults.MaxPendingRequests)
require.Equal(t, uint32(0), *readIngress.Defaults.MaxConcurrentRequests)
require.Len(t, readIngress.Listeners, 1) require.Len(t, readIngress.Listeners, 1)
require.Len(t, readIngress.Listeners[0].Services, 1) require.Len(t, readIngress.Listeners[0].Services, 1)
// Set namespace and partition to blank so that OSS and ent can utilize the same tests // Set namespace and partition to blank so that OSS and ent can utilize the same tests

View File

@ -141,6 +141,11 @@ func IngressGatewayToStructs(s *IngressGateway, t *structs.IngressGatewayConfigE
} }
} }
} }
if s.Defaults != nil {
var x structs.IngressServiceConfig
IngressServiceConfigToStructs(s.Defaults, &x)
t.Defaults = &x
}
t.Meta = s.Meta t.Meta = s.Meta
} }
func IngressGatewayFromStructs(t *structs.IngressGatewayConfigEntry, s *IngressGateway) { func IngressGatewayFromStructs(t *structs.IngressGatewayConfigEntry, s *IngressGateway) {
@ -162,6 +167,11 @@ func IngressGatewayFromStructs(t *structs.IngressGatewayConfigEntry, s *IngressG
} }
} }
} }
if t.Defaults != nil {
var x IngressServiceConfig
IngressServiceConfigFromStructs(t.Defaults, &x)
s.Defaults = &x
}
s.Meta = t.Meta s.Meta = t.Meta
} }
func IngressListenerToStructs(s *IngressListener, t *structs.IngressListener) { func IngressListenerToStructs(s *IngressListener, t *structs.IngressListener) {
@ -227,6 +237,9 @@ func IngressServiceToStructs(s *IngressService, t *structs.IngressService) {
HTTPHeaderModifiersToStructs(s.ResponseHeaders, &x) HTTPHeaderModifiersToStructs(s.ResponseHeaders, &x)
t.ResponseHeaders = &x t.ResponseHeaders = &x
} }
t.MaxConnections = s.MaxConnections
t.MaxPendingRequests = s.MaxPendingRequests
t.MaxConcurrentRequests = s.MaxConcurrentRequests
t.Meta = s.Meta t.Meta = s.Meta
t.EnterpriseMeta = enterpriseMetaToStructs(s.EnterpriseMeta) t.EnterpriseMeta = enterpriseMetaToStructs(s.EnterpriseMeta)
} }
@ -251,9 +264,28 @@ func IngressServiceFromStructs(t *structs.IngressService, s *IngressService) {
HTTPHeaderModifiersFromStructs(t.ResponseHeaders, &x) HTTPHeaderModifiersFromStructs(t.ResponseHeaders, &x)
s.ResponseHeaders = &x s.ResponseHeaders = &x
} }
s.MaxConnections = t.MaxConnections
s.MaxPendingRequests = t.MaxPendingRequests
s.MaxConcurrentRequests = t.MaxConcurrentRequests
s.Meta = t.Meta s.Meta = t.Meta
s.EnterpriseMeta = enterpriseMetaFromStructs(t.EnterpriseMeta) s.EnterpriseMeta = enterpriseMetaFromStructs(t.EnterpriseMeta)
} }
func IngressServiceConfigToStructs(s *IngressServiceConfig, t *structs.IngressServiceConfig) {
if s == nil {
return
}
t.MaxConnections = s.MaxConnections
t.MaxPendingRequests = s.MaxPendingRequests
t.MaxConcurrentRequests = s.MaxConcurrentRequests
}
func IngressServiceConfigFromStructs(t *structs.IngressServiceConfig, s *IngressServiceConfig) {
if s == nil {
return
}
s.MaxConnections = t.MaxConnections
s.MaxPendingRequests = t.MaxPendingRequests
s.MaxConcurrentRequests = t.MaxConcurrentRequests
}
func IntentionHTTPHeaderPermissionToStructs(s *IntentionHTTPHeaderPermission, t *structs.IntentionHTTPHeaderPermission) { func IntentionHTTPHeaderPermissionToStructs(s *IntentionHTTPHeaderPermission, t *structs.IntentionHTTPHeaderPermission) {
if s == nil { if s == nil {
return return

View File

@ -187,6 +187,16 @@ func (msg *IngressGateway) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg) return proto.Unmarshal(b, msg)
} }
// MarshalBinary implements encoding.BinaryMarshaler
func (msg *IngressServiceConfig) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg)
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler
func (msg *IngressServiceConfig) UnmarshalBinary(b []byte) error {
return proto.Unmarshal(b, msg)
}
// MarshalBinary implements encoding.BinaryMarshaler // MarshalBinary implements encoding.BinaryMarshaler
func (msg *GatewayTLSConfig) MarshalBinary() ([]byte, error) { func (msg *GatewayTLSConfig) MarshalBinary() ([]byte, error) {
return proto.Marshal(msg) return proto.Marshal(msg)

File diff suppressed because it is too large Load Diff

View File

@ -228,6 +228,18 @@ message IngressGateway {
GatewayTLSConfig TLS = 1; GatewayTLSConfig TLS = 1;
repeated IngressListener Listeners = 2; repeated IngressListener Listeners = 2;
map<string, string> Meta = 3; map<string, string> Meta = 3;
IngressServiceConfig Defaults = 4;
}
// mog annotation:
//
// target=github.com/hashicorp/consul/agent/structs.IngressServiceConfig
// output=config_entry.gen.go
// name=Structs
message IngressServiceConfig {
uint32 MaxConnections = 1;
uint32 MaxPendingRequests = 2;
uint32 MaxConcurrentRequests = 3;
} }
// mog annotation: // mog annotation:
@ -283,6 +295,9 @@ message IngressService {
map<string, string> Meta = 6; map<string, string> Meta = 6;
// mog: func-to=enterpriseMetaToStructs func-from=enterpriseMetaFromStructs // mog: func-to=enterpriseMetaToStructs func-from=enterpriseMetaFromStructs
common.EnterpriseMeta EnterpriseMeta = 7; common.EnterpriseMeta EnterpriseMeta = 7;
uint32 MaxConnections = 8;
uint32 MaxPendingRequests = 9;
uint32 MaxConcurrentRequests = 10;
} }
// mog annotation: // mog annotation:

View File

@ -11,6 +11,11 @@ config_entries {
kind = "ingress-gateway" kind = "ingress-gateway"
name = "ingress-gateway" name = "ingress-gateway"
Defaults {
MaxConnections = 10
MaxPendingRequests = 20
MaxConcurrentRequests = 30
}
listeners = [ listeners = [
{ {
port = 9999 port = 9999
@ -28,6 +33,9 @@ config_entries {
{ {
name = "s1" name = "s1"
hosts = ["test.example.com"] hosts = ["test.example.com"]
MaxConnections = 100
MaxPendingRequests = 200
MaxConcurrentRequests = 300
} }
] ]
} }

View File

@ -26,10 +26,44 @@ load helpers
assert_upstream_has_endpoints_in_status 127.0.0.1:20000 s1 HEALTHY 1 assert_upstream_has_endpoints_in_status 127.0.0.1:20000 s1 HEALTHY 1
} }
@test "s1 proxy should have been configured with max_connections in services" {
CLUSTER_THRESHOLD=$(get_envoy_cluster_config 127.0.0.1:20000 s1.default.primary | jq '.circuit_breakers.thresholds[0]')
echo $CLUSTER_THRESHOLD
MAX_CONNS=$(echo $CLUSTER_THRESHOLD | jq --raw-output '.max_connections')
MAX_PENDING_REQS=$(echo $CLUSTER_THRESHOLD | jq --raw-output '.max_pending_requests')
MAX_REQS=$(echo $CLUSTER_THRESHOLD | jq --raw-output '.max_requests')
echo "MAX_CONNS = $MAX_CONNS"
echo "MAX_PENDING_REQS = $MAX_PENDING_REQS"
echo "MAX_REQS = $MAX_REQS"
[ "$MAX_CONNS" = "100" ]
[ "$MAX_PENDING_REQS" = "200" ]
[ "$MAX_REQS" = "300" ]
}
@test "ingress-gateway should have healthy endpoints for s2" { @test "ingress-gateway should have healthy endpoints for s2" {
assert_upstream_has_endpoints_in_status 127.0.0.1:20000 s2 HEALTHY 1 assert_upstream_has_endpoints_in_status 127.0.0.1:20000 s2 HEALTHY 1
} }
@test "s2 proxy should have been configured with max_connections using defaults" {
CLUSTER_THRESHOLD=$(get_envoy_cluster_config 127.0.0.1:20000 s2.default.primary | jq '.circuit_breakers.thresholds[0]')
echo $CLUSTER_THRESHOLD
MAX_CONNS=$(echo $CLUSTER_THRESHOLD | jq --raw-output '.max_connections')
MAX_PENDING_REQS=$(echo $CLUSTER_THRESHOLD | jq --raw-output '.max_pending_requests')
MAX_REQS=$(echo $CLUSTER_THRESHOLD | jq --raw-output '.max_requests')
echo "MAX_CONNS = $MAX_CONNS"
echo "MAX_PENDING_REQS = $MAX_PENDING_REQS"
echo "MAX_REQS = $MAX_REQS"
[ "$MAX_CONNS" = "10" ]
[ "$MAX_PENDING_REQS" = "20" ]
[ "$MAX_REQS" = "30" ]
}
@test "ingress should be able to connect to s1 using Host header" { @test "ingress should be able to connect to s1 using Host header" {
assert_expected_fortio_name s1 s1.ingress.consul 9999 assert_expected_fortio_name s1 s1.ingress.consul 9999
} }

View File

@ -328,6 +328,7 @@ In the following example, two listeners are configured on an ingress gateway nam
- The first listener is configured to listen on port `8080` and uses a wildcard (`*`) to proxy traffic to all services in the datacenter. - The first listener is configured to listen on port `8080` and uses a wildcard (`*`) to proxy traffic to all services in the datacenter.
- The second listener exposes the `api` and `web` services on port `4567` at user-provided hosts. - The second listener exposes the `api` and `web` services on port `4567` at user-provided hosts.
- TLS is enabled on every listener. - TLS is enabled on every listener.
- The `max_connections` of the ingress gateway proxy to each upstream cluster is set to 4096.
The Consul Enterprise version implements the following additional configurations: The Consul Enterprise version implements the following additional configurations:
@ -346,6 +347,10 @@ TLS {
Enabled = true Enabled = true
} }
Defaults {
MaxConnections = 4096
}
Listeners = [ Listeners = [
{ {
Port = 8080 Port = 8080
@ -1041,6 +1046,38 @@ You can specify the following parameters to configure ingress gateway configurat
}, },
], ],
}, },
{
name: 'Defaults',
type: 'IngressServiceConfig: <optional>',
description: `Default configuration that applies to all upstreams.`,
children: [
{
name: 'MaxConnections',
type: 'int: 0',
description: `The maximum number of connections a service instance
will be allowed to establish against the given upstream. Use this to limit
HTTP/1.1 traffic, since HTTP/1.1 has a request per connection.
If not specified, it uses the default value. For example, 1024 for Envoy proxy.`,
},
{
name: 'MaxPendingRequests',
type: 'int: 0',
description: `The maximum number of requests that will be queued
while waiting for a connection to be established. For this configuration to
be respected, a L7 protocol must be defined in the \`protocol\` field.
If not specified, it uses the default value. For example, 1024 for Envoy proxy.`,
},
{
name: 'MaxConcurrentRequests',
type: 'int: 0',
description: `The maximum number of concurrent requests that
will be allowed at a single point in time. Use this to limit HTTP/2 traffic,
since HTTP/2 has many requests per connection. For this configuration to be
respected, a L7 protocol must be defined in the \`protocol\` field.
If not specified, it uses the default value. For example, 1024 for Envoy proxy.`,
},
],
},
{ {
name: 'Listeners', name: 'Listeners',
type: 'array<IngressListener>: <optional>)', type: 'array<IngressListener>: <optional>)',
@ -1156,6 +1193,21 @@ You can specify the following parameters to configure ingress gateway configurat
}, },
], ],
}, },
{
name: 'MaxConnections',
type: 'int: 0',
description: 'overrides for the [`Defaults` field](#available-fields)',
},
{
name: 'MaxPendingRequests',
type: 'int: 0',
description: 'overrides for the [`Defaults` field](#available-fields)',
},
{
name: 'MaxConcurrentRequests',
type: 'int: 0',
description: 'overrides for the [`Defaults` field](#available-fields)',
},
], ],
}, },
{ {