diff --git a/charts/consul/templates/crd-ingressgateways.yaml b/charts/consul/templates/crd-ingressgateways.yaml index f14789e83d..1a241a9cfb 100644 --- a/charts/consul/templates/crd-ingressgateways.yaml +++ b/charts/consul/templates/crd-ingressgateways.yaml @@ -57,6 +57,19 @@ spec: spec: description: IngressGatewaySpec defines the desired state of IngressGateway. properties: + defaults: + description: Defaults is default configuration for all upstream services + properties: + maxConcurrentRequests: + format: int32 + type: integer + maxConnections: + format: int32 + type: integer + maxPendingRequests: + format: int32 + type: integer + type: object listeners: description: Listeners declares what ports the ingress gateway should listen on, and what services to associated to those ports. @@ -98,6 +111,15 @@ spec: items: type: string type: array + maxConcurrentRequests: + format: int32 + type: integer + maxConnections: + format: int32 + type: integer + maxPendingRequests: + format: int32 + type: integer name: description: "Name declares the service to which traffic should be forwarded. \n This can either be a specific diff --git a/control-plane/api/v1alpha1/ingressgateway_types.go b/control-plane/api/v1alpha1/ingressgateway_types.go index 7251608223..d213b7eb84 100644 --- a/control-plane/api/v1alpha1/ingressgateway_types.go +++ b/control-plane/api/v1alpha1/ingressgateway_types.go @@ -57,6 +57,15 @@ type IngressGatewaySpec struct { // Listeners declares what ports the ingress gateway should listen on, and // what services to associated to those ports. Listeners []IngressListener `json:"listeners,omitempty"` + + // Defaults is default configuration for all upstream services + Defaults *IngressServiceConfig `json:"defaults,omitempty"` +} + +type IngressServiceConfig struct { + MaxConnections *uint32 `json:"maxConnections,omitempty"` + MaxPendingRequests *uint32 `json:"maxPendingRequests,omitempty"` + MaxConcurrentRequests *uint32 `json:"maxConcurrentRequests,omitempty"` } type GatewayTLSConfig struct { @@ -144,6 +153,10 @@ type IngressService struct { // Allow HTTP header manipulation to be configured. RequestHeaders *HTTPHeaderModifiers `json:"requestHeaders,omitempty"` ResponseHeaders *HTTPHeaderModifiers `json:"responseHeaders,omitempty"` + + MaxConnections *uint32 `json:"maxConnections,omitempty"` + MaxPendingRequests *uint32 `json:"maxPendingRequests,omitempty"` + MaxConcurrentRequests *uint32 `json:"maxConcurrentRequests,omitempty"` } func (in *IngressGateway) GetObjectMeta() metav1.ObjectMeta { @@ -235,6 +248,7 @@ func (in *IngressGateway) ToConsul(datacenter string) capi.ConfigEntry { TLS: *in.Spec.TLS.toConsul(), Listeners: listeners, Meta: meta(datacenter), + Defaults: in.Spec.Defaults.toConsul(), } } @@ -257,6 +271,8 @@ func (in *IngressGateway) Validate(consulMeta common.ConsulMeta) error { errs = append(errs, v.validate(path.Child("listeners").Index(i), consulMeta)...) } + errs = append(errs, in.Spec.Defaults.validate(path.Child("defaults"))...) + if len(errs) > 0 { return apierrors.NewInvalid( schema.GroupKind{Group: ConsulHashicorpGroup, Kind: ingressGatewayKubeKind}, @@ -329,13 +345,16 @@ func (in IngressListener) toConsul() capi.IngressListener { func (in IngressService) toConsul() capi.IngressService { return capi.IngressService{ - Name: in.Name, - Hosts: in.Hosts, - Namespace: in.Namespace, - Partition: in.Partition, - TLS: in.TLS.toConsul(), - RequestHeaders: in.RequestHeaders.toConsul(), - ResponseHeaders: in.ResponseHeaders.toConsul(), + Name: in.Name, + Hosts: in.Hosts, + Namespace: in.Namespace, + Partition: in.Partition, + TLS: in.TLS.toConsul(), + RequestHeaders: in.RequestHeaders.toConsul(), + ResponseHeaders: in.ResponseHeaders.toConsul(), + MaxConnections: in.MaxConnections, + MaxPendingRequests: in.MaxPendingRequests, + MaxConcurrentRequests: in.MaxConcurrentRequests, } } @@ -406,6 +425,49 @@ func (in IngressListener) validate(path *field.Path, consulMeta common.ConsulMet string(asJSON), "hosts must be empty if protocol is \"tcp\"")) } + + if svc.MaxConnections != nil && *svc.MaxConnections <= 0 { + errs = append(errs, field.Invalid(path.Child("maxconnections"), *svc.MaxConnections, "MaxConnections must be > 0")) + } + + if svc.MaxConcurrentRequests != nil && *svc.MaxConcurrentRequests <= 0 { + errs = append(errs, field.Invalid(path.Child("maxconcurrentrequests"), *svc.MaxConcurrentRequests, "MaxConcurrentRequests must be > 0")) + } + + if svc.MaxPendingRequests != nil && *svc.MaxPendingRequests <= 0 { + errs = append(errs, field.Invalid(path.Child("maxpendingrequests"), *svc.MaxPendingRequests, "MaxPendingRequests must be > 0")) + } + } + return errs +} + +func (in *IngressServiceConfig) validate(path *field.Path) field.ErrorList { + if in == nil { + return nil + } + var errs field.ErrorList + + if in.MaxConnections != nil && *in.MaxConnections <= 0 { + errs = append(errs, field.Invalid(path.Child("maxconnections"), *in.MaxConnections, "MaxConnections must be > 0")) + } + + if in.MaxConcurrentRequests != nil && *in.MaxConcurrentRequests <= 0 { + errs = append(errs, field.Invalid(path.Child("maxconcurrentrequests"), *in.MaxConcurrentRequests, "MaxConcurrentRequests must be > 0")) + } + + if in.MaxPendingRequests != nil && *in.MaxPendingRequests <= 0 { + errs = append(errs, field.Invalid(path.Child("maxpendingrequests"), *in.MaxPendingRequests, "MaxPendingRequests must be > 0")) } return errs } + +func (in *IngressServiceConfig) toConsul() *capi.IngressServiceConfig { + if in == nil { + return nil + } + return &capi.IngressServiceConfig{ + MaxConnections: in.MaxConnections, + MaxPendingRequests: in.MaxPendingRequests, + MaxConcurrentRequests: in.MaxConcurrentRequests, + } +} diff --git a/control-plane/api/v1alpha1/ingressgateway_types_test.go b/control-plane/api/v1alpha1/ingressgateway_types_test.go index 2585614519..c60f487954 100644 --- a/control-plane/api/v1alpha1/ingressgateway_types_test.go +++ b/control-plane/api/v1alpha1/ingressgateway_types_test.go @@ -253,6 +253,15 @@ func TestIngressGateway_MatchesConsul(t *testing.T) { } func TestIngressGateway_ToConsul(t *testing.T) { + + defaultMaxConnections := uint32(100) + defaultMaxPendingRequests := uint32(101) + defaultMaxConcurrentRequests := uint32(102) + + maxConnections := uint32(200) + maxPendingRequests := uint32(201) + maxConcurrentRequests := uint32(202) + cases := map[string]struct { Ours IngressGateway Exp *capi.IngressGatewayConfigEntry @@ -289,6 +298,11 @@ func TestIngressGateway_ToConsul(t *testing.T) { TLSMaxVersion: "TLSv1_1", CipherSuites: []string{"ECDHE-ECDSA-AES128-GCM-SHA256", "AES128-SHA"}, }, + Defaults: &IngressServiceConfig{ + MaxConnections: &defaultMaxConnections, + MaxPendingRequests: &defaultMaxPendingRequests, + MaxConcurrentRequests: &defaultMaxConcurrentRequests, + }, Listeners: []IngressListener{ { Port: 8888, @@ -305,10 +319,13 @@ func TestIngressGateway_ToConsul(t *testing.T) { }, Services: []IngressService{ { - Name: "name1", - Hosts: []string{"host1_1", "host1_2"}, - Namespace: "ns1", - Partition: "default", + Name: "name1", + Hosts: []string{"host1_1", "host1_2"}, + Namespace: "ns1", + Partition: "default", + MaxConnections: &maxConnections, + MaxPendingRequests: &maxPendingRequests, + MaxConcurrentRequests: &maxConcurrentRequests, TLS: &GatewayServiceTLSConfig{ SDS: &GatewayTLSSDSConfig{ ClusterName: "cluster1", @@ -378,6 +395,11 @@ func TestIngressGateway_ToConsul(t *testing.T) { TLSMaxVersion: "TLSv1_1", CipherSuites: []string{"ECDHE-ECDSA-AES128-GCM-SHA256", "AES128-SHA"}, }, + Defaults: &capi.IngressServiceConfig{ + MaxConnections: &defaultMaxConnections, + MaxPendingRequests: &defaultMaxPendingRequests, + MaxConcurrentRequests: &defaultMaxConcurrentRequests, + }, Listeners: []capi.IngressListener{ { Port: 8888, @@ -394,10 +416,13 @@ func TestIngressGateway_ToConsul(t *testing.T) { }, Services: []capi.IngressService{ { - Name: "name1", - Hosts: []string{"host1_1", "host1_2"}, - Namespace: "ns1", - Partition: "default", + Name: "name1", + Hosts: []string{"host1_1", "host1_2"}, + Namespace: "ns1", + Partition: "default", + MaxConnections: &maxConnections, + MaxPendingRequests: &maxPendingRequests, + MaxConcurrentRequests: &maxConcurrentRequests, TLS: &capi.GatewayServiceTLSConfig{ SDS: &capi.GatewayTLSSDSConfig{ ClusterName: "cluster1", @@ -471,6 +496,8 @@ func TestIngressGateway_ToConsul(t *testing.T) { } func TestIngressGateway_Validate(t *testing.T) { + zero := uint32(0) + cases := map[string]struct { input *IngressGateway namespacesEnabled bool @@ -785,6 +812,121 @@ func TestIngressGateway_Validate(t *testing.T) { }, partitionEnabled: true, }, + "defaults.maxConnections invalid": { + input: &IngressGateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: IngressGatewaySpec{ + Defaults: &IngressServiceConfig{ + MaxConnections: &zero, + }, + }, + }, + expectedErrMsgs: []string{ + `spec.defaults.maxconnections: Invalid`, + }, + }, + "defaults.maxPendingRequests invalid": { + input: &IngressGateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: IngressGatewaySpec{ + Defaults: &IngressServiceConfig{ + MaxPendingRequests: &zero, + }, + }, + }, + expectedErrMsgs: []string{ + `spec.defaults.maxpendingrequests: Invalid`, + }, + }, + "defaults.maxConcurrentRequests invalid": { + input: &IngressGateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: IngressGatewaySpec{ + Defaults: &IngressServiceConfig{ + MaxConcurrentRequests: &zero, + }, + }, + }, + expectedErrMsgs: []string{ + `spec.defaults.maxconcurrentrequests: Invalid`, + }, + }, + "service.maxConnections invalid": { + input: &IngressGateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: IngressGatewaySpec{ + Listeners: []IngressListener{ + { + Protocol: "http", + Services: []IngressService{ + { + Name: "svc1", + MaxConnections: &zero, + }, + }, + }, + }, + }, + }, + expectedErrMsgs: []string{ + `spec.listeners[0].maxconnections: Invalid`, + }, + }, + "service.maxConcurrentRequests invalid": { + input: &IngressGateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: IngressGatewaySpec{ + Listeners: []IngressListener{ + { + Protocol: "http", + Services: []IngressService{ + { + Name: "svc1", + MaxConcurrentRequests: &zero, + }, + }, + }, + }, + }, + }, + expectedErrMsgs: []string{ + `spec.listeners[0].maxconcurrentrequests: Invalid`, + }, + }, + "service.maxPendingRequests invalid": { + input: &IngressGateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: IngressGatewaySpec{ + Listeners: []IngressListener{ + { + Protocol: "http", + Services: []IngressService{ + { + Name: "svc1", + MaxPendingRequests: &zero, + }, + }, + }, + }, + }, + }, + expectedErrMsgs: []string{ + `spec.listeners[0].maxpendingrequests: Invalid`, + }, + }, + "multiple errors": { input: &IngressGateway{ ObjectMeta: metav1.ObjectMeta{ diff --git a/control-plane/api/v1alpha1/zz_generated.deepcopy.go b/control-plane/api/v1alpha1/zz_generated.deepcopy.go index 85d64c3c85..af7ec840a1 100644 --- a/control-plane/api/v1alpha1/zz_generated.deepcopy.go +++ b/control-plane/api/v1alpha1/zz_generated.deepcopy.go @@ -383,6 +383,11 @@ func (in *IngressGatewaySpec) DeepCopyInto(out *IngressGatewaySpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.Defaults != nil { + in, out := &in.Defaults, &out.Defaults + *out = new(IngressServiceConfig) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressGatewaySpec. @@ -445,6 +450,21 @@ func (in *IngressService) DeepCopyInto(out *IngressService) { *out = new(HTTPHeaderModifiers) (*in).DeepCopyInto(*out) } + if in.MaxConnections != nil { + in, out := &in.MaxConnections, &out.MaxConnections + *out = new(uint32) + **out = **in + } + if in.MaxPendingRequests != nil { + in, out := &in.MaxPendingRequests, &out.MaxPendingRequests + *out = new(uint32) + **out = **in + } + if in.MaxConcurrentRequests != nil { + in, out := &in.MaxConcurrentRequests, &out.MaxConcurrentRequests + *out = new(uint32) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressService. @@ -457,6 +477,36 @@ func (in *IngressService) DeepCopy() *IngressService { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IngressServiceConfig) DeepCopyInto(out *IngressServiceConfig) { + *out = *in + if in.MaxConnections != nil { + in, out := &in.MaxConnections, &out.MaxConnections + *out = new(uint32) + **out = **in + } + if in.MaxPendingRequests != nil { + in, out := &in.MaxPendingRequests, &out.MaxPendingRequests + *out = new(uint32) + **out = **in + } + if in.MaxConcurrentRequests != nil { + in, out := &in.MaxConcurrentRequests, &out.MaxConcurrentRequests + *out = new(uint32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressServiceConfig. +func (in *IngressServiceConfig) DeepCopy() *IngressServiceConfig { + if in == nil { + return nil + } + out := new(IngressServiceConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IntentionDestination) DeepCopyInto(out *IntentionDestination) { *out = *in diff --git a/control-plane/config/crd/bases/consul.hashicorp.com_ingressgateways.yaml b/control-plane/config/crd/bases/consul.hashicorp.com_ingressgateways.yaml index 6378ee4213..9934e9c0d9 100644 --- a/control-plane/config/crd/bases/consul.hashicorp.com_ingressgateways.yaml +++ b/control-plane/config/crd/bases/consul.hashicorp.com_ingressgateways.yaml @@ -50,6 +50,19 @@ spec: spec: description: IngressGatewaySpec defines the desired state of IngressGateway. properties: + defaults: + description: Defaults is default configuration for all upstream services + properties: + maxConcurrentRequests: + format: int32 + type: integer + maxConnections: + format: int32 + type: integer + maxPendingRequests: + format: int32 + type: integer + type: object listeners: description: Listeners declares what ports the ingress gateway should listen on, and what services to associated to those ports. @@ -91,6 +104,15 @@ spec: items: type: string type: array + maxConcurrentRequests: + format: int32 + type: integer + maxConnections: + format: int32 + type: integer + maxPendingRequests: + format: int32 + type: integer name: description: "Name declares the service to which traffic should be forwarded. \n This can either be a specific