diff --git a/config/crd/bases/k8s.nginx.org_virtualserverroutes.yaml b/config/crd/bases/k8s.nginx.org_virtualserverroutes.yaml index 9b3afbe076..22cc7c221a 100644 --- a/config/crd/bases/k8s.nginx.org_virtualserverroutes.yaml +++ b/config/crd/bases/k8s.nginx.org_virtualserverroutes.yaml @@ -872,6 +872,12 @@ spec: is set in the proxy-buffers ConfigMap key. type: string type: object + busy-buffers-size: + description: Sets the size of the buffers used for reading a + response from the upstream server when the proxy_buffering + is enabled. The default is set in the proxy-busy-buffers-size + ConfigMap key.' + type: string client-max-body-size: description: Sets the maximum allowed size of the client request body. The default is set in the client-max-body-size ConfigMap diff --git a/config/crd/bases/k8s.nginx.org_virtualservers.yaml b/config/crd/bases/k8s.nginx.org_virtualservers.yaml index 8e4cafe4e3..995786bfce 100644 --- a/config/crd/bases/k8s.nginx.org_virtualservers.yaml +++ b/config/crd/bases/k8s.nginx.org_virtualservers.yaml @@ -1061,6 +1061,12 @@ spec: is set in the proxy-buffers ConfigMap key. type: string type: object + busy-buffers-size: + description: Sets the size of the buffers used for reading a + response from the upstream server when the proxy_buffering + is enabled. The default is set in the proxy-busy-buffers-size + ConfigMap key.' + type: string client-max-body-size: description: Sets the maximum allowed size of the client request body. The default is set in the client-max-body-size ConfigMap diff --git a/deploy/crds.yaml b/deploy/crds.yaml index 5b80d9dff9..d1db1df275 100644 --- a/deploy/crds.yaml +++ b/deploy/crds.yaml @@ -1805,6 +1805,12 @@ spec: is set in the proxy-buffers ConfigMap key. type: string type: object + busy-buffers-size: + description: Sets the size of the buffers used for reading a + response from the upstream server when the proxy_buffering + is enabled. The default is set in the proxy-busy-buffers-size + ConfigMap key.' + type: string client-max-body-size: description: Sets the maximum allowed size of the client request body. The default is set in the client-max-body-size ConfigMap @@ -3217,6 +3223,12 @@ spec: is set in the proxy-buffers ConfigMap key. type: string type: object + busy-buffers-size: + description: Sets the size of the buffers used for reading a + response from the upstream server when the proxy_buffering + is enabled. The default is set in the proxy-busy-buffers-size + ConfigMap key.' + type: string client-max-body-size: description: Sets the maximum allowed size of the client request body. The default is set in the client-max-body-size ConfigMap diff --git a/docs/crd/k8s.nginx.org_virtualserverroutes.md b/docs/crd/k8s.nginx.org_virtualserverroutes.md index 93bc49f191..bcf4c2a925 100644 --- a/docs/crd/k8s.nginx.org_virtualserverroutes.md +++ b/docs/crd/k8s.nginx.org_virtualserverroutes.md @@ -168,6 +168,7 @@ The `.spec` object supports the following fields: | `upstreams[].buffers` | `object` | Configures the buffers used for reading a response from the upstream server for a single connection. | | `upstreams[].buffers.number` | `integer` | Configures the number of buffers. The default is set in the proxy-buffers ConfigMap key. | | `upstreams[].buffers.size` | `string` | Configures the size of a buffer. The default is set in the proxy-buffers ConfigMap key. | +| `upstreams[].busy-buffers-size` | `string` | Sets the size of the buffers used for reading a response from the upstream server when the proxy_buffering is enabled. The default is set in the proxy-busy-buffers-size ConfigMap key.' | | `upstreams[].client-max-body-size` | `string` | Sets the maximum allowed size of the client request body. The default is set in the client-max-body-size ConfigMap key. | | `upstreams[].connect-timeout` | `string` | The timeout for establishing a connection with an upstream server. The default is specified in the proxy-connect-timeout ConfigMap key. | | `upstreams[].fail-timeout` | `string` | The time during which the specified number of unsuccessful attempts to communicate with an upstream server should happen to consider the server unavailable. The default is set in the fail-timeout ConfigMap key. | diff --git a/docs/crd/k8s.nginx.org_virtualservers.md b/docs/crd/k8s.nginx.org_virtualservers.md index 154584e564..cc0268c773 100644 --- a/docs/crd/k8s.nginx.org_virtualservers.md +++ b/docs/crd/k8s.nginx.org_virtualservers.md @@ -203,6 +203,7 @@ The `.spec` object supports the following fields: | `upstreams[].buffers` | `object` | Configures the buffers used for reading a response from the upstream server for a single connection. | | `upstreams[].buffers.number` | `integer` | Configures the number of buffers. The default is set in the proxy-buffers ConfigMap key. | | `upstreams[].buffers.size` | `string` | Configures the size of a buffer. The default is set in the proxy-buffers ConfigMap key. | +| `upstreams[].busy-buffers-size` | `string` | Sets the size of the buffers used for reading a response from the upstream server when the proxy_buffering is enabled. The default is set in the proxy-busy-buffers-size ConfigMap key.' | | `upstreams[].client-max-body-size` | `string` | Sets the maximum allowed size of the client request body. The default is set in the client-max-body-size ConfigMap key. | | `upstreams[].connect-timeout` | `string` | The timeout for establishing a connection with an upstream server. The default is specified in the proxy-connect-timeout ConfigMap key. | | `upstreams[].fail-timeout` | `string` | The time during which the specified number of unsuccessful attempts to communicate with an upstream server should happen to consider the server unavailable. The default is set in the fail-timeout ConfigMap key. | diff --git a/internal/configs/annotations.go b/internal/configs/annotations.go index 8718dd151e..44f8f313a5 100644 --- a/internal/configs/annotations.go +++ b/internal/configs/annotations.go @@ -6,6 +6,7 @@ import ( "slices" nl "github.com/nginx/kubernetes-ingress/internal/logger" + "github.com/nginx/kubernetes-ingress/internal/validation" ) // JWTKeyAnnotation is the annotation where the Secret with a JWK is specified. @@ -301,7 +302,11 @@ func parseAnnotations(ingEx *IngressEx, baseCfgParams *ConfigParams, isPlus bool } if proxyBufferSize, exists := ingEx.Ingress.Annotations["nginx.org/proxy-buffer-size"]; exists { - cfgParams.ProxyBufferSize = proxyBufferSize + cfgParams.ProxyBufferSize = validation.NormalizeBufferSize(proxyBufferSize) + } + + if proxyBusyBuffersSize, exists := ingEx.Ingress.Annotations["nginx.org/proxy-busy-buffers-size"]; exists { + cfgParams.ProxyBusyBuffersSize = validation.NormalizeBufferSize(proxyBusyBuffersSize) } if upstreamZoneSize, exists := ingEx.Ingress.Annotations["nginx.org/upstream-zone-size"]; exists { diff --git a/internal/configs/commonhelpers/common_template_helpers.go b/internal/configs/commonhelpers/common_template_helpers.go index feb0e70e54..27dd570cfb 100644 --- a/internal/configs/commonhelpers/common_template_helpers.go +++ b/internal/configs/commonhelpers/common_template_helpers.go @@ -2,7 +2,11 @@ package commonhelpers import ( + "fmt" + "strconv" "strings" + + "github.com/nginx/kubernetes-ingress/internal/validation" ) // MakeSecretPath will return the path to the secret with the base secrets @@ -27,3 +31,192 @@ func MakeOnOffFromBool(b *bool) string { func BoolToPointerBool(b bool) *bool { return &b } + +// MakeProxyBuffers generates nginx proxy buffer configuration with validation +func MakeProxyBuffers(proxyBuffers, proxyBufferSize, proxyBusyBuffersSize string) string { + var parts []string + + proxyBufferSize = validation.NormalizeBufferSize(proxyBufferSize) + proxyBusyBuffersSize = validation.NormalizeBufferSize(proxyBusyBuffersSize) + + proxyBufferSize, _ = capBufferLimits(proxyBufferSize, 0) + + if proxyBufferSize != "" && proxyBuffers == "" { + count := 4 + if proxyBusyBuffersSize != "" { + bufferSizeBytes := validation.ParseSize(proxyBufferSize) + if bufferSizeBytes > 0 { + minBuffers := int((validation.ParseSize(proxyBusyBuffersSize) + bufferSizeBytes) / bufferSizeBytes) + if minBuffers > count { + count = minBuffers + } + } + } + proxyBuffers = fmt.Sprintf("%d %s", count, proxyBufferSize) + parts = append(parts, fmt.Sprintf("proxy_buffer_size %s", proxyBufferSize), fmt.Sprintf("proxy_buffers %s", proxyBuffers)) + } else if proxyBuffers != "" { + originalBuffers := proxyBuffers + proxyBuffers, proxyBufferSize = correctBufferConfig(proxyBuffers, proxyBufferSize) + parts = append(parts, fmt.Sprintf("proxy_buffers %s", proxyBuffers)) + if proxyBufferSize != "" { + parts = append(parts, fmt.Sprintf("proxy_buffer_size %s", proxyBufferSize)) + } + + if proxyBusyBuffersSize == "" && originalBuffers != proxyBuffers { + proxyBusyBuffersSize = defaultBusyBufferSize(proxyBuffers) + } + } + + parts = addBusyBufferSizeConfig(parts, proxyBuffers, proxyBufferSize, proxyBusyBuffersSize) + + if len(parts) == 0 { + return "" + } + return strings.Join(parts, ";\n\t\t") + ";" +} + +func defaultBusyBufferSize(proxyBuffers string) string { + fields := strings.Fields(proxyBuffers) + if len(fields) >= 2 { + // Return the individual buffer size as the default busy buffer size + return fields[1] + } + return "" +} + +// correctBufferConfig applies corrections to proxy buffer configuration +func correctBufferConfig(proxyBuffers, proxyBufferSize string) (string, string) { + fields := strings.Fields(strings.TrimSpace(proxyBuffers)) + if len(fields) < 2 { + return proxyBuffers, proxyBufferSize + } + + count, _ := strconv.Atoi(fields[0]) + + // Capping buffer count and individual buffer size + fields[1], count = capBufferLimits(fields[1], count) + proxyBuffers = fmt.Sprintf("%d %s", count, fields[1]) + + if proxyBufferSize == "" { + proxyBufferSize = fields[1] + } else { + // Validate user-defined proxy_buffer_size + bufferSizeBytes := validation.ParseSize(proxyBufferSize) + if bufferSizeBytes <= 0 { + // Invalid → fallback to individual buffer size + proxyBufferSize = fields[1] + } else { + // Valid → cap it if needed, but don't downgrade + proxyBufferSize, _ = capBufferLimits(proxyBufferSize, 0) + } + } + + return proxyBuffers, proxyBufferSize +} + +// capBufferLimits applies limits to buffer configurations to prevent issues +func capBufferLimits(bufferSize string, bufferCount int) (string, int) { + const maxReasonableBufferSize = 1000 * 1024 * 1024 // 1000MB in bytes + const maxReasonableBufferCount = 1024 + const minReasonableBufferCount = 2 + + cappedSize := bufferSize + cappedCount := bufferCount + + // Cap buffer size + if bufferSize != "" { + bufferSizeBytes := validation.ParseSize(bufferSize) + if bufferSizeBytes > maxReasonableBufferSize { + cappedSize = validation.FormatSize(maxReasonableBufferSize) + } + } + + // Cap buffer count + if bufferCount > maxReasonableBufferCount { + cappedCount = maxReasonableBufferCount + } else if bufferCount < minReasonableBufferCount { + cappedCount = minReasonableBufferCount + } + + return cappedSize, cappedCount +} + +// addBusyBufferSizeConfig manages busy buffer size configuration and adds it to the parts slice +func addBusyBufferSizeConfig(parts []string, proxyBuffers, proxyBufferSize, proxyBusyBuffersSize string) []string { + // Always ensure we have a valid busy buffer size when we have any buffer configuration + if len(parts) > 0 && proxyBuffers != "" { + if proxyBusyBuffersSize == "" { + proxyBusyBuffersSize = processBusyBufferSize(proxyBuffers, proxyBufferSize, "") + } else { + proxyBusyBuffersSize = processBusyBufferSize(proxyBuffers, proxyBufferSize, proxyBusyBuffersSize) + } + + if proxyBusyBuffersSize != "" { + parts = append(parts, fmt.Sprintf("proxy_busy_buffers_size %s", proxyBusyBuffersSize)) + } + } else if proxyBusyBuffersSize != "" { + // Handle case where only busy buffer size is provided without buffer configuration + parts = append(parts, fmt.Sprintf("proxy_busy_buffers_size %s", proxyBusyBuffersSize)) + } + + return parts +} + +// If proxyBusyBuffersSize is empty, it calculates a safe value +// If proxyBusyBuffersSize is provided, it validates and corrects it +func processBusyBufferSize(proxyBuffers, proxyBufferSize, proxyBusyBuffersSize string) string { + fields := strings.Fields(proxyBuffers) + if len(fields) < 2 { + if proxyBusyBuffersSize == "" { + return "" + } + return proxyBusyBuffersSize + } + + count, _ := strconv.Atoi(fields[0]) + individualSize := validation.ParseSize(fields[1]) + + if proxyBusyBuffersSize == "" { + proxyBufferSize, _ = capBufferLimits(proxyBufferSize, 0) + } + bufferSize := validation.ParseSize(proxyBufferSize) + + maxSize := int64(count)*individualSize - individualSize + minSize := individualSize + if bufferSize > minSize { + minSize = bufferSize + } + + if maxSize <= 0 || minSize >= maxSize { + safeSize := individualSize + if maxSize > individualSize { + safeSize = maxSize - 1024 // Leave 1k margin for safety + if safeSize < individualSize { + safeSize = individualSize + } + } + return validation.FormatSize(safeSize) + } + + // If no busy buffer size provided, calculate a safe one + if proxyBusyBuffersSize == "" { + // proxy_buffer_size + (2 * individual_buffer_size) + recommendedSize := minSize + 2*individualSize + if recommendedSize >= maxSize { + return validation.FormatSize(minSize) + } + return validation.FormatSize(recommendedSize) + } + + busySize := validation.ParseSize(proxyBusyBuffersSize) + + if busySize >= maxSize { + return validation.FormatSize(maxSize) + } + + if busySize < minSize { + return validation.FormatSize(minSize) + } + + return proxyBusyBuffersSize +} diff --git a/internal/configs/commonhelpers/common_template_helpers_test.go b/internal/configs/commonhelpers/common_template_helpers_test.go index 3d6c06cea9..7cf66ec4e9 100644 --- a/internal/configs/commonhelpers/common_template_helpers_test.go +++ b/internal/configs/commonhelpers/common_template_helpers_test.go @@ -61,3 +61,238 @@ func newMakeSecretPathTemplate(t *testing.T) *template.Template { } return tmpl } + +func TestMakeProxyBuffers(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + proxyBuffers string + proxyBufferSize string + proxyBusyBuffersSize string + expectedOutput string + }{ + { + name: "All empty", + expectedOutput: "", + }, + { + name: "buffer-size only", + proxyBufferSize: "4k", + expectedOutput: "proxy_buffer_size 4k;\n\t\tproxy_buffers 4 4k;\n\t\tproxy_busy_buffers_size 4k;", + }, + { + name: "buffers only", + proxyBuffers: "4 16k", + expectedOutput: "proxy_buffers 4 16k;\n\t\tproxy_buffer_size 16k;\n\t\tproxy_busy_buffers_size 16k;", + }, + { + name: "Both buffers and buffer-size set ", + proxyBuffers: "4 16k", + proxyBufferSize: "8k", + expectedOutput: "proxy_buffers 4 16k;\n\t\tproxy_buffer_size 8k;\n\t\tproxy_busy_buffers_size 16k;", + }, + { + name: "Invalid combination that should correct itself", + proxyBuffers: "8 1m", + proxyBufferSize: "5m", + expectedOutput: "proxy_buffers 8 1m;\n\t\tproxy_buffer_size 5m;\n\t\tproxy_busy_buffers_size 5m;", + }, + { + name: "Buffer-size smaller than individual buffer size", + proxyBuffers: "4 1m", + proxyBufferSize: "512k", + expectedOutput: "proxy_buffers 4 1m;\n\t\tproxy_buffer_size 512k;\n\t\tproxy_busy_buffers_size 1m;", + }, + { + name: "Minimum buffers configuration", + proxyBuffers: "2 4k", + proxyBufferSize: "4k", + expectedOutput: "proxy_buffers 2 4k;\n\t\tproxy_buffer_size 4k;\n\t\tproxy_busy_buffers_size 4k;", + }, + { + name: "All three parameters set", + proxyBuffers: "8 4k", + proxyBufferSize: "4k", + proxyBusyBuffersSize: "16k", + expectedOutput: "proxy_buffers 8 4k;\n\t\tproxy_buffer_size 4k;\n\t\tproxy_busy_buffers_size 16k;", + }, + { + name: "Busy buffer too large - reduces in size", + proxyBuffers: "4 8k", + proxyBufferSize: "8k", + proxyBusyBuffersSize: "40k", + expectedOutput: "proxy_buffers 4 8k;\n\t\tproxy_buffer_size 8k;\n\t\tproxy_busy_buffers_size 24k;", + }, + { + name: "Busy buffer wrong format", + proxyBuffers: "4 4k", + proxyBusyBuffersSize: "invalid", + expectedOutput: "proxy_buffers 4 4k;\n\t\tproxy_buffer_size 4k;\n\t\tproxy_busy_buffers_size 4k;", + }, + { + name: "Empty/zero values - corrected to minimum", + proxyBuffers: "0 4k", + expectedOutput: "proxy_buffers 2 4k;\n\t\tproxy_buffer_size 4k;\n\t\tproxy_busy_buffers_size 4k;", + }, + { + name: "Extreme values - autocorrect", + proxyBuffers: "1000000 1k", + proxyBufferSize: "999m", + expectedOutput: "proxy_buffers 1024 1k;\n\t\tproxy_buffer_size 999m;\n\t\tproxy_busy_buffers_size 1022k;", + }, + { + name: "Autocorrect buffer size and buffers", + proxyBuffers: "8 4k", + proxyBufferSize: "64k", + expectedOutput: "proxy_buffers 8 4k;\n\t\tproxy_buffer_size 64k;\n\t\tproxy_busy_buffers_size 27k;", + }, + { + name: "Buffer size with busy buffer calculates minimum buffers", + proxyBufferSize: "4k", + proxyBusyBuffersSize: "20k", + expectedOutput: "proxy_buffer_size 4k;\n\t\tproxy_buffers 6 4k;\n\t\tproxy_busy_buffers_size 20k;", + }, + { + name: "Single buffer corrected to minimum count", + proxyBuffers: "1 2k", + expectedOutput: "proxy_buffers 2 2k;\n\t\tproxy_buffer_size 2k;\n\t\tproxy_busy_buffers_size 2k;", + }, + { + name: "Single buffer with larger buffer size gets corrected", + proxyBuffers: "1 2k", + proxyBufferSize: "8k", + expectedOutput: "proxy_buffers 2 2k;\n\t\tproxy_buffer_size 8k;\n\t\tproxy_busy_buffers_size 2k;", + }, + { + name: "Zero buffers corrected to minimum 2", + proxyBuffers: "0 4k", + expectedOutput: "proxy_buffers 2 4k;\n\t\tproxy_buffer_size 4k;\n\t\tproxy_busy_buffers_size 4k;", + }, + { + name: "Large buffer count unchanged", + proxyBuffers: "16 1k", + expectedOutput: "proxy_buffers 16 1k;\n\t\tproxy_buffer_size 1k;\n\t\tproxy_busy_buffers_size 3k;", + }, + { + name: "Only busy buffer size set", + proxyBusyBuffersSize: "8k", + expectedOutput: "proxy_busy_buffers_size 8k;", + }, + { + name: "Very small buffers with large buffer size", + proxyBuffers: "2 1k", + proxyBufferSize: "2k", + expectedOutput: "proxy_buffers 2 1k;\n\t\tproxy_buffer_size 2k;\n\t\tproxy_busy_buffers_size 1k;", + }, + { + name: "Busy buffer exactly at limit", + proxyBuffers: "4 4k", + proxyBusyBuffersSize: "12k", + expectedOutput: "proxy_buffers 4 4k;\n\t\tproxy_buffer_size 4k;\n\t\tproxy_busy_buffers_size 12k;", + }, + { + name: "Busy buffer too small - gets adjusted", + proxyBuffers: "4 8k", + proxyBufferSize: "16k", + proxyBusyBuffersSize: "4k", + expectedOutput: "proxy_buffers 4 8k;\n\t\tproxy_buffer_size 16k;\n\t\tproxy_busy_buffers_size 16k;", + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + got := MakeProxyBuffers(tc.proxyBuffers, tc.proxyBufferSize, tc.proxyBusyBuffersSize) + + if got != tc.expectedOutput { + t.Errorf("Input: buffers=%q, bufferSize=%q, busyBufferSize=%q\nGot: %q\nExpected: %q", + tc.proxyBuffers, tc.proxyBufferSize, tc.proxyBusyBuffersSize, got, tc.expectedOutput) + } + }) + } +} + +func TestValidateBusyBufferSize(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + proxyBuffers string + proxyBufferSize string + proxyBusyBuffersSize string + expected string + }{ + { + name: "All empty", + expected: "", + }, + { + name: "No busy buffer size set", + proxyBuffers: "4 16k", + expected: "16k", + }, + { + name: "No busy buffer size with buffer size set", + proxyBuffers: "4 16k", + proxyBufferSize: "8k", + expected: "16k", + }, + { + name: "Valid busy buffer size within limits", + proxyBuffers: "4 16k", + proxyBusyBuffersSize: "32k", + expected: "32k", + }, + { + name: "Valid busy buffer size with buffer size", + proxyBuffers: "4 16k", + proxyBufferSize: "8k", + proxyBusyBuffersSize: "32k", + expected: "32k", + }, + { + name: "Valid configuration", + proxyBuffers: "8 4k", + proxyBufferSize: "4k", + proxyBusyBuffersSize: "16k", + expected: "16k", + }, + { + name: "Busy buffer too large, gets clamped", + proxyBuffers: "4 4k", + proxyBusyBuffersSize: "20k", + expected: "12k", + }, + { + name: "Busy buffer too small, gets adjusted", + proxyBuffers: "4 8k", + proxyBufferSize: "16k", + proxyBusyBuffersSize: "4k", + expected: "16k", + }, + { + name: "Buffer size larger than individual", + proxyBuffers: "4 8k", + proxyBufferSize: "16k", + proxyBusyBuffersSize: "12k", + expected: "16k", + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + got := processBusyBufferSize(tc.proxyBuffers, tc.proxyBufferSize, tc.proxyBusyBuffersSize) + + if got != tc.expected { + t.Errorf("Input: buffers=%q, bufferSize=%q, busyBufferSize=%q\nGot: %q\nExpected: %q", + tc.proxyBuffers, tc.proxyBufferSize, tc.proxyBusyBuffersSize, got, tc.expected) + } + }) + } +} diff --git a/internal/configs/config_params.go b/internal/configs/config_params.go index c73310056d..0e31282251 100644 --- a/internal/configs/config_params.go +++ b/internal/configs/config_params.go @@ -71,6 +71,7 @@ type ConfigParams struct { ProxyBuffering bool ProxyBuffers string ProxyBufferSize string + ProxyBusyBuffersSize string ProxyConnectTimeout string ProxyHideHeaders []string ProxyMaxTempFileSize string diff --git a/internal/configs/configmaps.go b/internal/configs/configmaps.go index 7a1c7cd001..1bc5415792 100644 --- a/internal/configs/configmaps.go +++ b/internal/configs/configmaps.go @@ -335,15 +335,32 @@ func ParseConfigMap(ctx context.Context, cfgm *v1.ConfigMap, nginxPlus bool, has } if proxyBuffers, exists := cfgm.Data["proxy-buffers"]; exists { - cfgParams.ProxyBuffers = proxyBuffers + fields := strings.Fields(strings.TrimSpace(proxyBuffers)) + if len(fields) == 2 { + normalizedSize := validation.NormalizeSize(fields[1]) + if normalizedSize != fields[1] { + correctedBuffers := fields[0] + " " + normalizedSize + nl.Info(l, fmt.Sprintf("Auto-corrected proxy-buffers from '%s' to '%s'", proxyBuffers, correctedBuffers)) + cfgParams.ProxyBuffers = correctedBuffers + } else { + cfgParams.ProxyBuffers = proxyBuffers + } + } else { + errorText := fmt.Sprintf("ConfigMap %s/%s: invalid value for 'proxy-buffers': %q, must be in format 'count size' (e.g. '4 8k'), ignoring", cfgm.GetNamespace(), cfgm.GetName(), proxyBuffers) + nl.Error(l, errorText) + eventLog.Event(cfgm, v1.EventTypeWarning, nl.EventReasonInvalidValue, errorText) + configOk = false + } } if proxyBufferSize, exists := cfgm.Data["proxy-buffer-size"]; exists { - cfgParams.ProxyBufferSize = proxyBufferSize + normalizedProxyBufferSize := validation.NormalizeBufferSize(proxyBufferSize) + cfgParams.ProxyBufferSize = normalizedProxyBufferSize } - if proxyMaxTempFileSize, exists := cfgm.Data["proxy-max-temp-file-size"]; exists { - cfgParams.ProxyMaxTempFileSize = proxyMaxTempFileSize + if proxyBusyBuffersSize, exists := cfgm.Data["proxy-busy-buffers-size"]; exists { + normalizedProxyBusyBuffersSize := validation.NormalizeBufferSize(proxyBusyBuffersSize) + cfgParams.ProxyBusyBuffersSize = normalizedProxyBusyBuffersSize } if mainMainSnippets, exists := GetMapKeyAsStringSlice(cfgm.Data, "main-snippets", cfgm, "\n"); exists { diff --git a/internal/configs/configmaps_test.go b/internal/configs/configmaps_test.go index b4afbd6613..220f0755f9 100644 --- a/internal/configs/configmaps_test.go +++ b/internal/configs/configmaps_test.go @@ -9,6 +9,7 @@ import ( "github.com/nginx/kubernetes-ingress/internal/configs/commonhelpers" v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/tools/record" ) @@ -1924,6 +1925,212 @@ func TestOpenTelemetryConfigurationInvalid(t *testing.T) { } } +func TestParseProxyBuffers(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + configMap *v1.ConfigMap + expectedProxyBuffers string + expectedProxyBufferSize string + expectedProxyBusyBuffersSize string + description string + }{ + { + name: "all proxy buffer settings provided", + configMap: &v1.ConfigMap{ + Data: map[string]string{ + "proxy-buffers": "8 4k", + "proxy-buffer-size": "8k", + "proxy-busy-buffers-size": "16k", + }, + }, + expectedProxyBuffers: "8 4k", + expectedProxyBufferSize: "8k", + expectedProxyBusyBuffersSize: "16k", + description: "should parse all proxy buffer settings correctly", + }, + { + name: "only proxy-buffers provided", + configMap: &v1.ConfigMap{ + Data: map[string]string{ + "proxy-buffers": "16 8k", + }, + }, + expectedProxyBuffers: "16 8k", + expectedProxyBufferSize: "", + expectedProxyBusyBuffersSize: "", + description: "should parse proxy-buffers only", + }, + { + name: "only proxy-buffer-size provided", + configMap: &v1.ConfigMap{ + Data: map[string]string{ + "proxy-buffer-size": "16k", + }, + }, + expectedProxyBuffers: "", + expectedProxyBufferSize: "16k", + expectedProxyBusyBuffersSize: "", + description: "should parse proxy-buffer-size only", + }, + { + name: "case insensitive units get normalized", + configMap: &v1.ConfigMap{ + Data: map[string]string{ + "proxy-buffers": "8 4K", + "proxy-buffer-size": "8K", + "proxy-busy-buffers-size": "16K", + }, + }, + expectedProxyBuffers: "8 4k", + expectedProxyBufferSize: "8k", + expectedProxyBusyBuffersSize: "16k", + description: "should normalize case insensitive units", + }, + { + name: "invalid units get normalized", + configMap: &v1.ConfigMap{ + Data: map[string]string{ + "proxy-buffers": "8 4g", + "proxy-buffer-size": "8x", + "proxy-busy-buffers-size": "16z", + }, + }, + expectedProxyBuffers: "8 4m", + expectedProxyBufferSize: "8m", + expectedProxyBusyBuffersSize: "16m", + description: "should normalize invalid units to 'm'", + }, + { + name: "empty configmap", + configMap: &v1.ConfigMap{ + Data: map[string]string{}, + }, + expectedProxyBuffers: "", + expectedProxyBufferSize: "", + expectedProxyBusyBuffersSize: "", + description: "should handle empty configmap gracefully", + }, + } + + nginxPlus := true + hasAppProtect := false + hasAppProtectDos := false + hasTLSPassthrough := false + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + eventRecorder := makeEventLogger() + result, configOk := ParseConfigMap(context.Background(), test.configMap, nginxPlus, hasAppProtect, hasAppProtectDos, hasTLSPassthrough, eventRecorder) + + if !configOk { + t.Errorf("%s: expected config to be valid but got invalid", test.description) + } + + if result.ProxyBuffers != test.expectedProxyBuffers { + t.Errorf("%s: ProxyBuffers = %q, want %q", test.description, result.ProxyBuffers, test.expectedProxyBuffers) + } + + if result.ProxyBufferSize != test.expectedProxyBufferSize { + t.Errorf("%s: ProxyBufferSize = %q, want %q", test.description, result.ProxyBufferSize, test.expectedProxyBufferSize) + } + + if result.ProxyBusyBuffersSize != test.expectedProxyBusyBuffersSize { + t.Errorf("%s: ProxyBusyBuffersSize = %q, want %q", test.description, result.ProxyBusyBuffersSize, test.expectedProxyBusyBuffersSize) + } + + fakeRecorder := eventRecorder.(*record.FakeRecorder) + if len(fakeRecorder.Events) > 0 { + t.Errorf("%s: unexpected warnings generated: %d events", test.description, len(fakeRecorder.Events)) + } + }) + } +} + +func TestParseProxyBuffersInvalidFormat(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + proxyBuffers string + expectValid bool + description string + }{ + { + name: "valid format", + proxyBuffers: "4 8k", + expectValid: true, + description: "should accept valid 'count size' format", + }, + { + name: "invalid - only size", + proxyBuffers: "1k", + expectValid: false, + description: "should reject format with only size", + }, + { + name: "invalid - only count", + proxyBuffers: "4", + expectValid: false, + description: "should reject format with only count", + }, + { + name: "invalid - three parts", + proxyBuffers: "4 8k extra", + expectValid: false, + description: "should reject format with too many parts", + }, + { + name: "invalid - empty", + proxyBuffers: "", + expectValid: false, + description: "should reject empty string", + }, + } + + nginxPlus := true + hasAppProtect := false + hasAppProtectDos := false + hasTLSPassthrough := false + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + cm := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-configmap", + Namespace: "default", + }, + Data: map[string]string{ + "proxy-buffers": test.proxyBuffers, + }, + } + + eventRecorder := makeEventLogger() + result, configOk := ParseConfigMap(context.Background(), cm, nginxPlus, hasAppProtect, hasAppProtectDos, hasTLSPassthrough, eventRecorder) + + if configOk != test.expectValid { + t.Errorf("%s: expected configOk=%v, got configOk=%v", test.description, test.expectValid, configOk) + } + + if test.expectValid { + if result.ProxyBuffers != test.proxyBuffers { + t.Errorf("%s: expected ProxyBuffers=%q, got %q", test.description, test.proxyBuffers, result.ProxyBuffers) + } + } else { + if result.ProxyBuffers != "" { + t.Errorf("%s: expected ProxyBuffers to be empty for invalid config, got %q", test.description, result.ProxyBuffers) + } + + fakeRecorder := eventRecorder.(*record.FakeRecorder) + if len(fakeRecorder.Events) == 0 { + t.Errorf("%s: expected error event to be generated for invalid config", test.description) + } + } + }) + } +} + func makeEventLogger() record.EventRecorder { return record.NewFakeRecorder(1024) } diff --git a/internal/configs/ingress.go b/internal/configs/ingress.go index 123bf02afa..f5bf9e6087 100644 --- a/internal/configs/ingress.go +++ b/internal/configs/ingress.go @@ -501,6 +501,7 @@ func createLocation(path string, upstream version1.Upstream, cfg *ConfigParams, ProxyBuffering: cfg.ProxyBuffering, ProxyBuffers: cfg.ProxyBuffers, ProxyBufferSize: cfg.ProxyBufferSize, + ProxyBusyBuffersSize: cfg.ProxyBusyBuffersSize, ProxyMaxTempFileSize: cfg.ProxyMaxTempFileSize, ProxySSLName: proxySSLName, LocationSnippets: cfg.LocationSnippets, diff --git a/internal/configs/version1/config.go b/internal/configs/version1/config.go index 106470b865..9091036dae 100644 --- a/internal/configs/version1/config.go +++ b/internal/configs/version1/config.go @@ -180,6 +180,7 @@ type Location struct { ProxyBuffering bool ProxyBuffers string ProxyBufferSize string + ProxyBusyBuffersSize string ProxyMaxTempFileSize string ProxySSLName string JWTAuth *JWTAuth diff --git a/internal/configs/version1/nginx-plus.ingress.tmpl b/internal/configs/version1/nginx-plus.ingress.tmpl index 94f30de87f..be9737d366 100644 --- a/internal/configs/version1/nginx-plus.ingress.tmpl +++ b/internal/configs/version1/nginx-plus.ingress.tmpl @@ -295,11 +295,8 @@ server { proxy_set_header X-Forwarded-Port $server_port; proxy_set_header X-Forwarded-Proto {{if $server.RedirectToHTTPS}}https{{else}}$scheme{{end}}; proxy_buffering {{if $location.ProxyBuffering}}on{{else}}off{{end}}; - {{- if $location.ProxyBuffers}} - proxy_buffers {{$location.ProxyBuffers}}; - {{- end}} - {{- if $location.ProxyBufferSize}} - proxy_buffer_size {{$location.ProxyBufferSize}}; + {{- if or $location.ProxyBuffers $location.ProxyBufferSize $location.ProxyBusyBuffersSize}} + {{makeProxyBuffers $location.ProxyBuffers $location.ProxyBufferSize $location.ProxyBusyBuffersSize}} {{- end}} {{- if $location.ProxyMaxTempFileSize}} proxy_max_temp_file_size {{$location.ProxyMaxTempFileSize}}; diff --git a/internal/configs/version1/nginx.ingress.tmpl b/internal/configs/version1/nginx.ingress.tmpl index 1d7ca8cd87..95c0456038 100644 --- a/internal/configs/version1/nginx.ingress.tmpl +++ b/internal/configs/version1/nginx.ingress.tmpl @@ -204,12 +204,8 @@ server { proxy_set_header X-Forwarded-Port $server_port; proxy_set_header X-Forwarded-Proto {{if $server.RedirectToHTTPS}}https{{else}}$scheme{{end}}; proxy_buffering {{if $location.ProxyBuffering}}on{{else}}off{{end}}; - - {{- if $location.ProxyBuffers}} - proxy_buffers {{$location.ProxyBuffers}}; - {{- end}} - {{- if $location.ProxyBufferSize}} - proxy_buffer_size {{$location.ProxyBufferSize}}; + {{- if or $location.ProxyBuffers $location.ProxyBufferSize $location.ProxyBusyBuffersSize}} + {{makeProxyBuffers $location.ProxyBuffers $location.ProxyBufferSize $location.ProxyBusyBuffersSize}} {{- end}} {{- if $location.ProxyMaxTempFileSize}} proxy_max_temp_file_size {{$location.ProxyMaxTempFileSize}}; diff --git a/internal/configs/version1/template_helper.go b/internal/configs/version1/template_helper.go index 203f2b4a2e..43959aaac9 100644 --- a/internal/configs/version1/template_helper.go +++ b/internal/configs/version1/template_helper.go @@ -202,6 +202,10 @@ func makeResolver(resolverAddresses []string, resolverValid string, resolverIPV6 return builder.String() } +func makeProxyBuffers(proxyBuffers, proxyBufferSize, proxyBusyBuffersSize string) string { + return commonhelpers.MakeProxyBuffers(proxyBuffers, proxyBufferSize, proxyBusyBuffersSize) +} + var helperFunctions = template.FuncMap{ "split": split, "trim": trim, @@ -217,4 +221,5 @@ var helperFunctions = template.FuncMap{ "generateProxySetHeaders": generateProxySetHeaders, "boolToPointerBool": commonhelpers.BoolToPointerBool, "makeResolver": makeResolver, + "makeProxyBuffers": makeProxyBuffers, } diff --git a/internal/configs/version2/__snapshots__/templates_test.snap b/internal/configs/version2/__snapshots__/templates_test.snap index 581fe23d00..2326c8aeb1 100644 --- a/internal/configs/version2/__snapshots__/templates_test.snap +++ b/internal/configs/version2/__snapshots__/templates_test.snap @@ -413,6 +413,7 @@ server { proxy_buffering on; proxy_buffers 8 4k; proxy_buffer_size 4k; + proxy_busy_buffers_size 8k; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $vs_connection_header; @@ -838,6 +839,7 @@ server { proxy_buffering on; proxy_buffers 8 4k; proxy_buffer_size 4k; + proxy_busy_buffers_size 8k; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $vs_connection_header; @@ -2810,6 +2812,7 @@ server { proxy_buffering on; proxy_buffers 8 4k; proxy_buffer_size 4k; + proxy_busy_buffers_size 8k; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $vs_connection_header; @@ -3212,6 +3215,7 @@ server { proxy_buffering on; proxy_buffers 8 4k; proxy_buffer_size 4k; + proxy_busy_buffers_size 8k; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $vs_connection_header; diff --git a/internal/configs/version2/http.go b/internal/configs/version2/http.go index f835656fb9..aa09b4fdc4 100644 --- a/internal/configs/version2/http.go +++ b/internal/configs/version2/http.go @@ -199,6 +199,7 @@ type Location struct { ProxyBuffering bool ProxyBuffers string ProxyBufferSize string + ProxyBusyBuffersSize string ProxyPass string ProxyNextUpstream string ProxyNextUpstreamTimeout string diff --git a/internal/configs/version2/nginx-plus.virtualserver.tmpl b/internal/configs/version2/nginx-plus.virtualserver.tmpl index 91c592cb79..770eb7bbac 100644 --- a/internal/configs/version2/nginx-plus.virtualserver.tmpl +++ b/internal/configs/version2/nginx-plus.virtualserver.tmpl @@ -610,11 +610,15 @@ server { {{- end }} proxy_buffering {{ if $l.ProxyBuffering }}on{{ else }}off{{ end }}; - {{- if $l.ProxyBuffers }} - proxy_buffers {{ $l.ProxyBuffers }}; - {{- end }} - {{- if $l.ProxyBufferSize }} - {{ $proxyOrGRPC }}_buffer_size {{ $l.ProxyBufferSize }}; + {{- if not $l.GRPCPass }} + {{- $proxyBuffersConfig := makeProxyBuffers $l.ProxyBuffers $l.ProxyBufferSize $l.ProxyBusyBuffersSize }} + {{- if $proxyBuffersConfig }} + {{ $proxyBuffersConfig }} + {{- end }} + {{- else }} + {{- if $l.ProxyBufferSize }} + grpc_buffer_size {{ $l.ProxyBufferSize }}; + {{- end }} {{- end }} {{- if not $l.GRPCPass }} proxy_http_version 1.1; diff --git a/internal/configs/version2/nginx.virtualserver.tmpl b/internal/configs/version2/nginx.virtualserver.tmpl index 4721b3c879..8ac56938ee 100644 --- a/internal/configs/version2/nginx.virtualserver.tmpl +++ b/internal/configs/version2/nginx.virtualserver.tmpl @@ -352,11 +352,15 @@ server { {{- end }} proxy_buffering {{ if $l.ProxyBuffering }}on{{ else }}off{{ end }}; - {{- if $l.ProxyBuffers }} - proxy_buffers {{ $l.ProxyBuffers }}; - {{- end }} - {{- if $l.ProxyBufferSize }} - {{ $proxyOrGRPC }}_buffer_size {{ $l.ProxyBufferSize }}; + {{- if not $l.GRPCPass }} + {{- $proxyBuffersConfig := makeProxyBuffers $l.ProxyBuffers $l.ProxyBufferSize $l.ProxyBusyBuffersSize }} + {{- if $proxyBuffersConfig }} + {{ $proxyBuffersConfig }} + {{- end }} + {{- else }} + {{- if $l.ProxyBufferSize }} + grpc_buffer_size {{ $l.ProxyBufferSize }}; + {{- end }} {{- end }} {{- if not $l.GRPCPass }} proxy_http_version 1.1; diff --git a/internal/configs/version2/template_helper.go b/internal/configs/version2/template_helper.go index a31003e928..393de1e7af 100644 --- a/internal/configs/version2/template_helper.go +++ b/internal/configs/version2/template_helper.go @@ -248,6 +248,10 @@ func boolToInteger(b bool) int { return i } +func makeProxyBuffers(proxyBuffers, proxyBufferSize, proxyBusyBuffersSize string) string { + return commonhelpers.MakeProxyBuffers(proxyBuffers, proxyBufferSize, proxyBusyBuffersSize) +} + var helperFunctions = template.FuncMap{ "headerListToCIMap": headerListToCIMap, "hasCIKey": hasCIKey, @@ -264,4 +268,5 @@ var helperFunctions = template.FuncMap{ "makeTransportListener": makeTransportListener, "makeServerName": makeServerName, "boolToInteger": boolToInteger, + "makeProxyBuffers": makeProxyBuffers, } diff --git a/internal/configs/version2/templates_test.go b/internal/configs/version2/templates_test.go index beaa511dfa..fce352eaa9 100644 --- a/internal/configs/version2/templates_test.go +++ b/internal/configs/version2/templates_test.go @@ -1088,6 +1088,7 @@ func vsConfig() VirtualServerConfig { ProxyBuffering: true, ProxyBuffers: "8 4k", ProxyBufferSize: "4k", + ProxyBusyBuffersSize: "8k", ProxyMaxTempFileSize: "1024m", ProxyPass: "http://test-upstream", ProxyNextUpstream: "error timeout", @@ -1451,6 +1452,7 @@ var ( ProxyBuffering: true, ProxyBuffers: "8 4k", ProxyBufferSize: "4k", + ProxyBusyBuffersSize: "8k", ProxyMaxTempFileSize: "1024m", ProxyPass: "http://test-upstream", ProxyNextUpstream: "error timeout", @@ -1800,6 +1802,7 @@ var ( ProxyBuffering: true, ProxyBuffers: "8 4k", ProxyBufferSize: "4k", + ProxyBusyBuffersSize: "8k", ProxyMaxTempFileSize: "1024m", ProxyPass: "http://test-upstream", ProxyNextUpstream: "error timeout", diff --git a/internal/configs/virtualserver.go b/internal/configs/virtualserver.go index 9cc56a1942..454a2642c9 100644 --- a/internal/configs/virtualserver.go +++ b/internal/configs/virtualserver.go @@ -2495,6 +2495,7 @@ func generateLocationForProxying(path string, upstreamName string, upstream conf ProxyBuffering: generateBool(upstream.ProxyBuffering, cfgParams.ProxyBuffering), ProxyBuffers: generateBuffers(upstream.ProxyBuffers, cfgParams.ProxyBuffers), ProxyBufferSize: generateString(upstream.ProxyBufferSize, cfgParams.ProxyBufferSize), + ProxyBusyBuffersSize: generateString(upstream.ProxyBusyBuffersSize, cfgParams.ProxyBusyBuffersSize), ProxyPass: generateProxyPass(upstream.TLS.Enable, upstreamName, internal, proxy), ProxyNextUpstream: generateString(upstream.ProxyNextUpstream, "error timeout"), ProxyNextUpstreamTimeout: generateTimeWithDefault(upstream.ProxyNextUpstreamTimeout, "0s"), diff --git a/internal/configs/virtualserver_test.go b/internal/configs/virtualserver_test.go index 106e4162e2..1ceaf46c01 100644 --- a/internal/configs/virtualserver_test.go +++ b/internal/configs/virtualserver_test.go @@ -15223,6 +15223,7 @@ func TestGenerateLocationForProxying(t *testing.T) { ProxyBuffering: true, ProxyBuffers: "8 4k", ProxyBufferSize: "4k", + ProxyBusyBuffersSize: "8k", LocationSnippets: []string{"# location snippet"}, } path := "/" @@ -15240,6 +15241,7 @@ func TestGenerateLocationForProxying(t *testing.T) { ProxyBuffering: true, ProxyBuffers: "8 4k", ProxyBufferSize: "4k", + ProxyBusyBuffersSize: "8k", ProxyPass: "http://test-upstream", ProxyNextUpstream: "error timeout", ProxyNextUpstreamTimeout: "0s", @@ -15270,6 +15272,7 @@ func TestGenerateLocationForGrpcProxying(t *testing.T) { ProxyBuffering: true, ProxyBuffers: "8 4k", ProxyBufferSize: "4k", + ProxyBusyBuffersSize: "8k", LocationSnippets: []string{"# location snippet"}, HTTP2: true, } @@ -15288,6 +15291,7 @@ func TestGenerateLocationForGrpcProxying(t *testing.T) { ProxyBuffering: true, ProxyBuffers: "8 4k", ProxyBufferSize: "4k", + ProxyBusyBuffersSize: "8k", ProxyPass: "http://test-upstream", ProxyNextUpstream: "error timeout", ProxyNextUpstreamTimeout: "0s", diff --git a/internal/validation/validation.go b/internal/validation/validation.go index e62086e469..f7171cdd48 100644 --- a/internal/validation/validation.go +++ b/internal/validation/validation.go @@ -178,3 +178,80 @@ func ValidateURI(uri string, options ...URIValidationOption) error { return nil } + +// NormalizeSize converts size strings to valid format +func NormalizeSize(sizeStr string) string { + bytes := ParseSize(sizeStr) + if bytes <= 0 { + return "" + } + return FormatSize(bytes) +} + +// ParseSize converts size strings to bytes, autocorrecting invalid units to 'm' +func ParseSize(sizeStr string) int64 { + sizeStr = strings.ToLower(strings.TrimSpace(sizeStr)) + if sizeStr == "" { + return 0 + } + + if num, err := strconv.ParseInt(sizeStr, 10, 64); err == nil { + if num <= 0 { + return 0 + } + return num + } + + if len(sizeStr) < 2 { + return 0 + } + + numStr := sizeStr[:len(sizeStr)-1] + unit := sizeStr[len(sizeStr)-1] + num, err := strconv.ParseInt(numStr, 10, 64) + if err != nil || num <= 0 { + return 0 + } + + // Autocorrect invalid units to 'm' + if unit != 'k' && unit != 'm' { + unit = 'm' + } + + switch unit { + case 'k': + return num << 10 + case 'm': + return num << 20 + default: + return num << 20 // Treat as MB + } +} + +// FormatSize converts bytes to human-readable size string +func FormatSize(bytes int64) string { + if bytes == 0 { + return "0" + } + + if bytes >= (1 << 20) { + return fmt.Sprintf("%dm", bytes/(1<<20)) + } + + if bytes >= (1 << 10) { + return fmt.Sprintf("%dk", bytes/(1<<10)) + } + + return fmt.Sprintf("%d", bytes) +} + +// NormalizeBufferSize handles buffer size values has the wrong format eg input "2 1k", returns "1k" +func NormalizeBufferSize(sizeStr string) string { + fields := strings.Fields(strings.TrimSpace(sizeStr)) + if len(fields) == 2 { + if _, err := strconv.Atoi(fields[0]); err == nil { + sizeStr = fields[1] + } + } + return NormalizeSize(sizeStr) +} diff --git a/internal/validation/validation_test.go b/internal/validation/validation_test.go index ac2869651d..e5eae8dcf0 100644 --- a/internal/validation/validation_test.go +++ b/internal/validation/validation_test.go @@ -205,3 +205,64 @@ func TestValidateURI(t *testing.T) { }) } } + +func TestParseSize(t *testing.T) { + t.Parallel() + + testCases := []struct { + input string + expected int64 + }{ + {"", 0}, + {"1024", 1024}, + {"4k", 4096}, + {"2m", 2097152}, + {"1g", 1048576}, // Now returns 1MB fallback instead of 1GB + {"4K", 4096}, // case insensitive + {"invalid", 0}, + {" 8k ", 8192}, // with whitespace + {"4kb", 0}, + {"8x", 8388608}, // Invalid unit returns same value as MB + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.input, func(t *testing.T) { + t.Parallel() + + got := ParseSize(tc.input) + if got != tc.expected { + t.Errorf("ParseSize(%q) = %d, expected %d", tc.input, got, tc.expected) + } + }) + } +} + +func TestFormatSize(t *testing.T) { + t.Parallel() + + testCases := []struct { + input int64 + expected string + }{ + {0, "0"}, + {1024, "1k"}, + {4096, "4k"}, + {2097152, "2m"}, + {1073741824, "1024m"}, // Now formats as 1024m instead of 1g (no g support) + {1536, "1k"}, // rounds down + {500, "500"}, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.expected, func(t *testing.T) { + t.Parallel() + + got := FormatSize(tc.input) + if got != tc.expected { + t.Errorf("FormatSize(%d) = %q, expected %q", tc.input, got, tc.expected) + } + }) + } +} diff --git a/pkg/apis/configuration/v1/types.go b/pkg/apis/configuration/v1/types.go index df32e367dc..26773cf696 100644 --- a/pkg/apis/configuration/v1/types.go +++ b/pkg/apis/configuration/v1/types.go @@ -152,6 +152,8 @@ type Upstream struct { ProxyBuffers *UpstreamBuffers `json:"buffers"` // Sets the size of the buffer used for reading the first part of a response received from the upstream server. The default is set in the proxy-buffer-size ConfigMap key. ProxyBufferSize string `json:"buffer-size"` + // Sets the size of the buffers used for reading a response from the upstream server when the proxy_buffering is enabled. The default is set in the proxy-busy-buffers-size ConfigMap key.' + ProxyBusyBuffersSize string `json:"busy-buffers-size"` // Sets the maximum allowed size of the client request body. The default is set in the client-max-body-size ConfigMap key. ClientMaxBodySize string `json:"client-max-body-size"` // The TLS configuration for the Upstream. diff --git a/pkg/apis/configuration/validation/virtualserver.go b/pkg/apis/configuration/validation/virtualserver.go index aa13c5061c..1c102fccd7 100644 --- a/pkg/apis/configuration/validation/virtualserver.go +++ b/pkg/apis/configuration/validation/virtualserver.go @@ -606,6 +606,7 @@ func (vsv *VirtualServerValidator) validateUpstreams(upstreams []v1.Upstream, fi allErrs = append(allErrs, validateTime(u.SlowStart, idxPath.Child("slow-start"))...) allErrs = append(allErrs, validateBuffer(u.ProxyBuffers, idxPath.Child("buffers"))...) allErrs = append(allErrs, validateSize(u.ProxyBufferSize, idxPath.Child("buffer-size"))...) + allErrs = append(allErrs, validateSize(u.ProxyBusyBuffersSize, idxPath.Child("busy-buffers-size"))...) allErrs = append(allErrs, validateQueue(u.Queue, idxPath.Child("queue"))...) allErrs = append(allErrs, validateSessionCookie(u.SessionCookie, idxPath.Child("sessionCookie"))...) allErrs = append(allErrs, validateUpstreamType(u.Type, idxPath.Child("type"))...)