Skip to content

Commit

Permalink
add configuration for ingress proxy body size and read and send timeo…
Browse files Browse the repository at this point in the history
…uts (#1204)
  • Loading branch information
nilsgstrabo authored Oct 7, 2024
1 parent 5ed2b4d commit ff8c8d2
Show file tree
Hide file tree
Showing 11 changed files with 305 additions and 61 deletions.
4 changes: 2 additions & 2 deletions charts/radix-operator/Chart.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
apiVersion: v2
name: radix-operator
version: 1.41.0
appVersion: 1.61.0
version: 1.42.0
appVersion: 1.62.0
kubeVersion: ">=1.24.0"
description: Radix Operator
keywords:
Expand Down
44 changes: 44 additions & 0 deletions charts/radix-operator/templates/radixapplication.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,28 @@ spec:
pattern: ^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\/([0-9]|[1-2][0-9]|3[0-2]))?$
type: string
type: array
proxyBodySize:
description: |-
Sets the maximum allowed size of the client request body.
Sizes can be specified in bytes, kilobytes (suffixes k and K), megabytes (suffixes m and M), or gigabytes (suffixes g and G) for example, "1024", "64k", "32m", "2g"
If the size in a request exceeds the configured value, the 413 (Request Entity Too Large) error is returned to the client.
Setting size to 0 disables checking of client request body size.
pattern: ^(?:0|[1-9][0-9]*[kKmMgG]?)$
type: string
proxyReadTimeout:
description: |-
Defines a timeout, in seconds, for reading a response from the proxied server.
The timeout is set only between two successive read operations, not for the transmission of the whole response.
If the proxied server does not transmit anything within this time, the connection is closed.
minimum: 0
type: integer
proxySendTimeout:
description: |-
Defines a timeout, in seconds, for transmitting a request to the proxied server.
The timeout is set only between two successive write operations, not for the transmission of the whole request.
If the proxied server does not receive anything within this time, the connection is closed.
minimum: 0
type: integer
type: object
type: object
type: object
Expand Down Expand Up @@ -1447,6 +1469,28 @@ spec:
pattern: ^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\/([0-9]|[1-2][0-9]|3[0-2]))?$
type: string
type: array
proxyBodySize:
description: |-
Sets the maximum allowed size of the client request body.
Sizes can be specified in bytes, kilobytes (suffixes k and K), megabytes (suffixes m and M), or gigabytes (suffixes g and G) for example, "1024", "64k", "32m", "2g"
If the size in a request exceeds the configured value, the 413 (Request Entity Too Large) error is returned to the client.
Setting size to 0 disables checking of client request body size.
pattern: ^(?:0|[1-9][0-9]*[kKmMgG]?)$
type: string
proxyReadTimeout:
description: |-
Defines a timeout, in seconds, for reading a response from the proxied server.
The timeout is set only between two successive read operations, not for the transmission of the whole response.
If the proxied server does not transmit anything within this time, the connection is closed.
minimum: 0
type: integer
proxySendTimeout:
description: |-
Defines a timeout, in seconds, for transmitting a request to the proxied server.
The timeout is set only between two successive write operations, not for the transmission of the whole request.
If the proxied server does not receive anything within this time, the connection is closed.
minimum: 0
type: integer
type: object
type: object
type: object
Expand Down
30 changes: 30 additions & 0 deletions json-schema/radixapplication.json
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,21 @@
"type": "string"
},
"type": "array"
},
"proxyBodySize": {
"description": "Sets the maximum allowed size of the client request body.\nSizes can be specified in bytes, kilobytes (suffixes k and K), megabytes (suffixes m and M), or gigabytes (suffixes g and G) for example, \"1024\", \"64k\", \"32m\", \"2g\"\nIf the size in a request exceeds the configured value, the 413 (Request Entity Too Large) error is returned to the client.\nSetting size to 0 disables checking of client request body size.",
"pattern": "^(?:0|[1-9][0-9]*[kKmMgG]?)$",
"type": "string"
},
"proxyReadTimeout": {
"description": "Defines a timeout, in seconds, for reading a response from the proxied server.\nThe timeout is set only between two successive read operations, not for the transmission of the whole response.\nIf the proxied server does not transmit anything within this time, the connection is closed.",
"minimum": 0,
"type": "integer"
},
"proxySendTimeout": {
"description": "Defines a timeout, in seconds, for transmitting a request to the proxied server.\nThe timeout is set only between two successive write operations, not for the transmission of the whole request.\nIf the proxied server does not receive anything within this time, the connection is closed.",
"minimum": 0,
"type": "integer"
}
},
"type": "object"
Expand Down Expand Up @@ -1438,6 +1453,21 @@
"type": "string"
},
"type": "array"
},
"proxyBodySize": {
"description": "Sets the maximum allowed size of the client request body.\nSizes can be specified in bytes, kilobytes (suffixes k and K), megabytes (suffixes m and M), or gigabytes (suffixes g and G) for example, \"1024\", \"64k\", \"32m\", \"2g\"\nIf the size in a request exceeds the configured value, the 413 (Request Entity Too Large) error is returned to the client.\nSetting size to 0 disables checking of client request body size.",
"pattern": "^(?:0|[1-9][0-9]*[kKmMgG]?)$",
"type": "string"
},
"proxyReadTimeout": {
"description": "Defines a timeout, in seconds, for reading a response from the proxied server.\nThe timeout is set only between two successive read operations, not for the transmission of the whole response.\nIf the proxied server does not transmit anything within this time, the connection is closed.",
"minimum": 0,
"type": "integer"
},
"proxySendTimeout": {
"description": "Defines a timeout, in seconds, for transmitting a request to the proxied server.\nThe timeout is set only between two successive write operations, not for the transmission of the whole request.\nIf the proxied server does not receive anything within this time, the connection is closed.",
"minimum": 0,
"type": "integer"
}
},
"type": "object"
Expand Down
2 changes: 1 addition & 1 deletion pkg/apis/deployment/radixcomponent.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ func getRadixComponentNetwork(component *radixv1.RadixComponent, environmentConf
if dst == nil {
dst = &radixv1.Network{}
}
if err := mergo.Merge(dst, environmentConfig.Network, mergo.WithOverride, mergo.WithOverrideEmptySlice); err != nil {
if err := mergo.Merge(dst, environmentConfig.Network, mergo.WithOverride, mergo.WithOverrideEmptySlice, mergo.WithTransformers(booleanPointerTransformer)); err != nil {
return nil, err
}
}
Expand Down
146 changes: 89 additions & 57 deletions pkg/apis/deployment/radixcomponent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package deployment
import (
"context"
"fmt"
"math"
"strings"
"testing"

Expand Down Expand Up @@ -1547,73 +1548,104 @@ func Test_GetRadixComponentsForEnv_Runtime_AlwaysUseFromDeployComponentImages(t
}
}

func Test_Test_GetRadixComponentsForEnv_NetworkIngressPublicAllow(t *testing.T) {
tests := map[string]struct {
commonConfig *radixv1.Network
envConfig *radixv1.Network
setEnv bool
expected *radixv1.Network
}{
"nil in common": {
setEnv: false,
expected: nil,
func Test_Test_GetRadixComponentsForEnv_NetworkIngressPublicConfig(t *testing.T) {
exp2 := func(n int) int {
return int(math.Exp2(float64(n)))
}

// Defines a list of functions that will set a single field in IngressPublic on component level (setCommonCfg)
// and in environmentConfig (setEnvCfg). Each field in IngressPublic should be represented in both listes.
// The corresponding field setter function in each list must set different values, or the tests won't be trustworthy.
// bool fields should have two functions, one for true and one for false value.
// All fields in IngressPublic should be represented in a function.
type setIngressFuncs []func(*radixv1.IngressPublic)
setCommonCfg := setIngressFuncs{
func(cfg *radixv1.IngressPublic) {
cfg.Allow = &[]radixv1.IPOrCIDR{radixv1.IPOrCIDR("10.10.10.10"), radixv1.IPOrCIDR("20.20.20.20")}
},
"empty in common, env not set": {
commonConfig: &radixv1.Network{},
setEnv: false,
expected: &radixv1.Network{},
func(cfg *radixv1.IngressPublic) {
cfg.ProxyBodySize = pointers.Ptr(radixv1.NginxSizeFormat("20m"))
},
"empty in common and nil in env": {
commonConfig: &radixv1.Network{},
setEnv: true,
expected: &radixv1.Network{},
func(cfg *radixv1.IngressPublic) {
cfg.ProxyReadTimeout = pointers.Ptr[uint](100)
},
"empty in common and env": {
commonConfig: &radixv1.Network{},
setEnv: true,
envConfig: &radixv1.Network{},
expected: &radixv1.Network{},
func(cfg *radixv1.IngressPublic) {
cfg.ProxySendTimeout = pointers.Ptr[uint](150)
},
"allow set in common, nil in env": {
commonConfig: &radixv1.Network{Ingress: &radixv1.Ingress{Public: &radixv1.IngressPublic{Allow: &[]radixv1.IPOrCIDR{radixv1.IPOrCIDR("commonip1"), radixv1.IPOrCIDR("commonip2")}}}},
setEnv: true,
envConfig: &radixv1.Network{Ingress: &radixv1.Ingress{Public: &radixv1.IngressPublic{}}},
expected: &radixv1.Network{Ingress: &radixv1.Ingress{Public: &radixv1.IngressPublic{Allow: &[]radixv1.IPOrCIDR{radixv1.IPOrCIDR("commonip1"), radixv1.IPOrCIDR("commonip2")}}}},
}
setEnvCfg := setIngressFuncs{
func(cfg *radixv1.IngressPublic) {
cfg.Allow = &[]radixv1.IPOrCIDR{radixv1.IPOrCIDR("1.1.1.1"), radixv1.IPOrCIDR("2.2.2.2")}
},
"allow set in common, empty in env": {
commonConfig: &radixv1.Network{Ingress: &radixv1.Ingress{Public: &radixv1.IngressPublic{Allow: &[]radixv1.IPOrCIDR{radixv1.IPOrCIDR("commonip1"), radixv1.IPOrCIDR("commonip2")}}}},
setEnv: true,
envConfig: &radixv1.Network{Ingress: &radixv1.Ingress{Public: &radixv1.IngressPublic{Allow: &[]radixv1.IPOrCIDR{}}}},
expected: &radixv1.Network{Ingress: &radixv1.Ingress{Public: &radixv1.IngressPublic{Allow: &[]radixv1.IPOrCIDR{}}}},
func(cfg *radixv1.IngressPublic) {
cfg.ProxyBodySize = pointers.Ptr(radixv1.NginxSizeFormat("10m"))
},
"allow set in common and env": {
commonConfig: &radixv1.Network{Ingress: &radixv1.Ingress{Public: &radixv1.IngressPublic{Allow: &[]radixv1.IPOrCIDR{radixv1.IPOrCIDR("commonip1"), radixv1.IPOrCIDR("commonip2")}}}},
setEnv: true,
envConfig: &radixv1.Network{Ingress: &radixv1.Ingress{Public: &radixv1.IngressPublic{Allow: &[]radixv1.IPOrCIDR{radixv1.IPOrCIDR("envip1"), radixv1.IPOrCIDR("envip2")}}}},
expected: &radixv1.Network{Ingress: &radixv1.Ingress{Public: &radixv1.IngressPublic{Allow: &[]radixv1.IPOrCIDR{radixv1.IPOrCIDR("envip1"), radixv1.IPOrCIDR("envip2")}}}},
func(cfg *radixv1.IngressPublic) {
cfg.ProxyReadTimeout = pointers.Ptr[uint](10)
},
"allow nil in common set in env": {
commonConfig: &radixv1.Network{Ingress: &radixv1.Ingress{Public: &radixv1.IngressPublic{}}},
setEnv: true,
envConfig: &radixv1.Network{Ingress: &radixv1.Ingress{Public: &radixv1.IngressPublic{Allow: &[]radixv1.IPOrCIDR{radixv1.IPOrCIDR("envip1"), radixv1.IPOrCIDR("envip2")}}}},
expected: &radixv1.Network{Ingress: &radixv1.Ingress{Public: &radixv1.IngressPublic{Allow: &[]radixv1.IPOrCIDR{radixv1.IPOrCIDR("envip1"), radixv1.IPOrCIDR("envip2")}}}},
func(cfg *radixv1.IngressPublic) {
cfg.ProxySendTimeout = pointers.Ptr[uint](15)
},
}

for testName, test := range tests {
t.Run(testName, func(t *testing.T) {
const envName = "anyenv"
component := utils.AnApplicationComponent().WithName("anycomponent").WithNetwork(test.commonConfig)
if test.setEnv {
component = component.WithEnvironmentConfigs(
utils.AnEnvironmentConfig().WithEnvironment(envName).WithNetwork(test.envConfig),
)
}
ra := utils.ARadixApplication().WithComponents(component).BuildRA()
components, err := GetRadixComponentsForEnv(context.Background(), ra, nil, envName, make(pipeline.DeployComponentImages), make(radixv1.EnvVarsMap), nil)
require.NoError(t, err)
assert.Equal(t, test.expected, components[0].Network)
})
/*
The tests will check every possible combination of component and environment specific configuration of the IngressPublic spec.
exp2 is used in the two for-loops to create a bitmap representation of each function in setCommonCfg and setEnvCfg.
The function is called with the corresponding config (common or env) and expectedCfg if the function's bit is set.
How it works:
We have 4 functions in each slice. To iterate over every possible combination of function call (call none, some or all),
we calculate 2 pow 4 = 16, and iterate from 0 to 15. This binary representation for each value will then be:
0: 0000 (no functions will be called)
1: 0001 (function with index 0 will be called)
2: 0010 (function with index 1 will be called)
3: 0011 (functions with indexes 0 and 1 will be called)
4: 0100 (function with index 2 will be called)
...
15: 1111 (all functions will be called)
It is imortant that the setCommonCfg functions are applied to expectedCfg first and setEnvCfg last,
since we excpect environment config to take precedence over common config if the field is non-nil.
*/
for c := range exp2(len(setCommonCfg)) {
for e := range exp2(len(setEnvCfg)) {
// Include bitmap representation of which functions in common and env config that must be called
// This makes it a bit easier to identity what fields are set in common and env config in case a test fails
testName := fmt.Sprintf("common bitmap: %.4b - env bitmap: %.4b", c, e)
t.Run(testName, func(t *testing.T) {
commonCfg := &radixv1.IngressPublic{}
envCfg := &radixv1.IngressPublic{}
expectedCfg := &radixv1.IngressPublic{}
for i := range len(setCommonCfg) {
if c&exp2(i) > 0 {
setCommonCfg[i](commonCfg)
setCommonCfg[i](expectedCfg)
}
}
for i := range len(setEnvCfg) {
if e&exp2(i) > 0 {
setEnvCfg[i](envCfg)
setEnvCfg[i](expectedCfg)
}
}

const envName = "anyenv"
ra := utils.ARadixApplication().
WithComponents(
utils.AnApplicationComponent().
WithName("anycomponent").
WithNetwork(&radixv1.Network{Ingress: &radixv1.Ingress{Public: commonCfg}}).
WithEnvironmentConfigs(
utils.AnEnvironmentConfig().
WithEnvironment(envName).
WithNetwork(&radixv1.Network{Ingress: &radixv1.Ingress{Public: envCfg}}),
),
).BuildRA()
components, err := GetRadixComponentsForEnv(context.Background(), ra, nil, envName, make(pipeline.DeployComponentImages), make(radixv1.EnvVarsMap), nil)
require.NoError(t, err)
assert.Equal(t, expectedCfg, components[0].Network.Ingress.Public)
})
}
}
}

Expand Down
1 change: 1 addition & 0 deletions pkg/apis/ingress/ingress.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,5 +116,6 @@ func GetAnnotationProvider(ingressConfiguration IngressConfiguration, certificat
NewClientCertificateAnnotationProvider(certificateNamespace),
NewOAuth2AnnotationProvider(oauth2DefaultConfig),
NewIngressPublicAllowListAnnotationProvider(),
NewIngressPublicConfigAnnotationProvider(),
}
}
34 changes: 34 additions & 0 deletions pkg/apis/ingress/ingressannotationprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package ingress

import (
"fmt"
"strconv"
"strings"

"github.com/equinor/radix-common/utils/slice"
Expand Down Expand Up @@ -149,3 +150,36 @@ func (*ingressPublicAllowListAnnotationProvider) GetAnnotations(component radixv
addressList := slice.Map(*component.GetNetwork().Ingress.Public.Allow, func(v radixv1.IPOrCIDR) string { return string(v) })
return map[string]string{"nginx.ingress.kubernetes.io/whitelist-source-range": strings.Join(addressList, ",")}, nil
}

// NewIngressPublicConfigAnnotationProvider provides Ingress annotations
// for fields in `Network.Ingress.Public`, except for `allow`
func NewIngressPublicConfigAnnotationProvider() AnnotationProvider {
return &ingressPublicConfigAnnotationProvider{}
}

type ingressPublicConfigAnnotationProvider struct{}

// GetAnnotations returns annotations for only allowing public ingress traffic
// for IPs or CIDRs defined in Network.Ingress.Public.Allow for a component
func (*ingressPublicConfigAnnotationProvider) GetAnnotations(component radixv1.RadixCommonDeployComponent, _ string) (map[string]string, error) {
if network := component.GetNetwork(); network == nil || network.Ingress == nil || network.Ingress.Public == nil {
return nil, nil
}

annotations := map[string]string{}
cfg := component.GetNetwork().Ingress.Public

if v := cfg.ProxyBodySize; v != nil {
annotations["nginx.ingress.kubernetes.io/proxy-body-size"] = string(*v)
}

if v := cfg.ProxyReadTimeout; v != nil {
annotations["nginx.ingress.kubernetes.io/proxy-read-timeout"] = strconv.FormatUint(uint64(*v), 10)
}

if v := cfg.ProxySendTimeout; v != nil {
annotations["nginx.ingress.kubernetes.io/proxy-send-timeout"] = strconv.FormatUint(uint64(*v), 10)
}

return annotations, nil
}
Loading

0 comments on commit ff8c8d2

Please sign in to comment.