From 831fe9493c57a684fb62ac895f1786d294973da2 Mon Sep 17 00:00:00 2001 From: Haywood Shannon <5781935+haywoodsh@users.noreply.github.com> Date: Wed, 12 Nov 2025 17:04:21 +0000 Subject: [PATCH 01/16] extend cache policy for more configurable parameters Signed-off-by: Haywood Shannon <5781935+haywoodsh@users.noreply.github.com> --- config/crd/bases/k8s.nginx.org_policies.yaml | 127 +++++++++++++++++- deploy/crds.yaml | 127 +++++++++++++++++- docs/crd/k8s.nginx.org_policies.md | 20 +++ .../custom-resources/cache-policy/cache.yaml | 28 ++++ .../__snapshots__/templates_test.snap | 8 +- internal/configs/version2/http.go | 75 +++++++++-- .../version2/nginx-plus.virtualserver.tmpl | 64 ++++++++- .../configs/version2/nginx.virtualserver.tmpl | 64 ++++++++- internal/configs/version2/templates_test.go | 4 +- internal/configs/virtualserver.go | 52 ++++++- internal/configs/virtualserver_test.go | 75 ++++++++++- pkg/apis/configuration/v1/types.go | 111 ++++++++++++++- .../configuration/v1/zz_generated.deepcopy.go | 88 ++++++++++++ pkg/apis/configuration/validation/policy.go | 101 ++++++++++++++ .../configuration/validation/virtualserver.go | 4 + 15 files changed, 916 insertions(+), 32 deletions(-) diff --git a/config/crd/bases/k8s.nginx.org_policies.yaml b/config/crd/bases/k8s.nginx.org_policies.yaml index 0794caad39..54e418ad82 100644 --- a/config/crd/bases/k8s.nginx.org_policies.yaml +++ b/config/crd/bases/k8s.nginx.org_policies.yaml @@ -140,6 +140,24 @@ spec: x-kubernetes-validations: - message: 'allowed methods must be one of: GET, HEAD, POST' rule: self.all(method, method in ['GET', 'HEAD', 'POST']) + cacheBackgroundUpdate: + default: false + description: |- + CacheBackgroundUpdate allows starting a background subrequest to update an expired cache item (proxy_cache_background_update). + A stale cached response is returned to the client while the cache is being updated. + type: boolean + cacheKey: + description: |- + CacheKey defines a key for caching (proxy_cache_key). + By default, close to "$scheme$proxy_host$uri$is_args$args". + maxLength: 1024 + type: string + cacheMinUses: + description: CacheMinUses sets the number of requests after which + the response will be cached (proxy_cache_min_uses). + maximum: 2147483647 + minimum: 1 + type: integer cachePurgeAllow: description: |- CachePurgeAllow defines IP addresses or CIDR blocks allowed to purge cache. @@ -149,6 +167,20 @@ spec: items: type: string type: array + cacheRevalidate: + default: false + description: |- + CacheRevalidate enables revalidation of expired cache items using conditional requests (proxy_cache_revalidate). + Uses "If-Modified-Since" and "If-None-Match" header fields. + type: boolean + cacheUseStale: + description: |- + CacheUseStale determines in which cases a stale cached response can be used (proxy_cache_use_stale). + Valid parameters: error, timeout, invalid_header, updating, http_500, http_502, http_503, http_504, http_403, http_404, http_429, off. + items: + type: string + maxItems: 11 + type: array cacheZoneName: description: |- CacheZoneName defines the name of the cache zone. Must start with a lowercase letter, @@ -161,7 +193,32 @@ spec: CacheZoneSize defines the size of the cache zone. Must be a number followed by a size unit: 'k' for kilobytes, 'm' for megabytes, or 'g' for gigabytes. Examples: "10m", "1g", "512k". - pattern: ^[0-9]+[kmg]$ + pattern: ^[0-9]+[kmgKMG]$ + type: string + conditions: + description: Conditions defines when responses should not be cached + or taken from cache. + properties: + bypass: + description: |- + Bypass defines conditions under which the response will not be taken from a cache (proxy_cache_bypass). + If at least one value of the string parameters is not empty and is not equal to "0" then the response will not be taken from the cache. + items: + type: string + type: array + noCache: + description: |- + NoCache defines conditions under which the response will not be saved to a cache (proxy_no_cache). + If at least one value of the string parameters is not empty and is not equal to "0" then the response will not be saved. + items: + type: string + type: array + type: object + inactive: + description: |- + Inactive sets the time after which cached data that are not accessed get removed from the cache (inactive parameter). + By default, inactive is set to 10 minutes. + pattern: ^[0-9]+[smhd]$ type: string levels: description: |- @@ -172,6 +229,68 @@ spec: Invalid: "3:1", "1:3", "1:2:3". pattern: ^[12](?::[12]){0,2}$ type: string + lock: + description: Lock configures cache locking to prevent multiple + identical requests from populating the same cache element simultaneously. + properties: + age: + description: |- + Age sets the maximum time a cache lock can be held (proxy_cache_lock_age). + If the last request passed to the proxied server for populating a new cache element has not completed for the specified time, one more request may be passed. + pattern: ^[0-9]+[smhd]$ + type: string + enable: + default: false + description: |- + Enable sets whether cache locking is enabled (proxy_cache_lock). + When enabled, only one request at a time will be allowed to populate a new cache element according to the proxy_cache_key. + type: boolean + timeout: + description: |- + Timeout sets a timeout for proxy_cache_lock. + When the time expires, the request will be passed to the proxied server, however, the response will not be cached. + pattern: ^[0-9]+[smhd]$ + type: string + type: object + x-kubernetes-validations: + - message: timeout or age require enable=true + rule: (!has(self.timeout) && !has(self.age)) || self.enable + manager: + description: Manager configures the cache manager process parameters + (manager_files, manager_sleep, manager_threshold). + properties: + files: + description: |- + Files sets the maximum number of files that will be deleted in one iteration by the cache manager. + During one iteration no more than manager_files items are deleted (by default, 100). + maximum: 2147483647 + minimum: 1 + type: integer + sleep: + description: |- + Sleep sets the pause between cache manager iterations. + Between iterations, a pause configured by manager_sleep (by default, 50 milliseconds) is made. + pattern: ^[0-9]+[mu]?s$ + type: string + threshold: + description: |- + Threshold sets the maximum duration of one cache manager iteration. + The duration of one iteration is limited by manager_threshold (by default, 200 milliseconds). + pattern: ^[0-9]+[mu]?s$ + type: string + type: object + maxSize: + description: |- + MaxSize sets the maximum cache size (max_size parameter). + When the size is exceeded, the cache manager removes the least recently used data. + pattern: ^[0-9]+[kmgKMG]$ + type: string + minFree: + description: |- + MinFree sets the minimum amount of free space required on the file system with cache (min_free parameter). + When there is not enough free space, the cache manager removes the least recently used data. + pattern: ^[0-9]+[kmgKMG]$ + type: string overrideUpstreamCache: default: false description: |- @@ -188,6 +307,12 @@ spec: Examples: "30s", "5m", "1h", "2d". pattern: ^[0-9]+[smhd]$ type: string + useTempPath: + default: false + description: |- + UseTempPath controls whether temporary files and the cache are put on different file systems (use_temp_path parameter). + If set to off, temporary files will be put directly in the cache directory. + type: boolean required: - cacheZoneName - cacheZoneSize diff --git a/deploy/crds.yaml b/deploy/crds.yaml index fd080f2d59..088a6755dd 100644 --- a/deploy/crds.yaml +++ b/deploy/crds.yaml @@ -311,6 +311,24 @@ spec: x-kubernetes-validations: - message: 'allowed methods must be one of: GET, HEAD, POST' rule: self.all(method, method in ['GET', 'HEAD', 'POST']) + cacheBackgroundUpdate: + default: false + description: |- + CacheBackgroundUpdate allows starting a background subrequest to update an expired cache item (proxy_cache_background_update). + A stale cached response is returned to the client while the cache is being updated. + type: boolean + cacheKey: + description: |- + CacheKey defines a key for caching (proxy_cache_key). + By default, close to "$scheme$proxy_host$uri$is_args$args". + maxLength: 1024 + type: string + cacheMinUses: + description: CacheMinUses sets the number of requests after which + the response will be cached (proxy_cache_min_uses). + maximum: 2147483647 + minimum: 1 + type: integer cachePurgeAllow: description: |- CachePurgeAllow defines IP addresses or CIDR blocks allowed to purge cache. @@ -320,6 +338,20 @@ spec: items: type: string type: array + cacheRevalidate: + default: false + description: |- + CacheRevalidate enables revalidation of expired cache items using conditional requests (proxy_cache_revalidate). + Uses "If-Modified-Since" and "If-None-Match" header fields. + type: boolean + cacheUseStale: + description: |- + CacheUseStale determines in which cases a stale cached response can be used (proxy_cache_use_stale). + Valid parameters: error, timeout, invalid_header, updating, http_500, http_502, http_503, http_504, http_403, http_404, http_429, off. + items: + type: string + maxItems: 11 + type: array cacheZoneName: description: |- CacheZoneName defines the name of the cache zone. Must start with a lowercase letter, @@ -332,7 +364,32 @@ spec: CacheZoneSize defines the size of the cache zone. Must be a number followed by a size unit: 'k' for kilobytes, 'm' for megabytes, or 'g' for gigabytes. Examples: "10m", "1g", "512k". - pattern: ^[0-9]+[kmg]$ + pattern: ^[0-9]+[kmgKMG]$ + type: string + conditions: + description: Conditions defines when responses should not be cached + or taken from cache. + properties: + bypass: + description: |- + Bypass defines conditions under which the response will not be taken from a cache (proxy_cache_bypass). + If at least one value of the string parameters is not empty and is not equal to "0" then the response will not be taken from the cache. + items: + type: string + type: array + noCache: + description: |- + NoCache defines conditions under which the response will not be saved to a cache (proxy_no_cache). + If at least one value of the string parameters is not empty and is not equal to "0" then the response will not be saved. + items: + type: string + type: array + type: object + inactive: + description: |- + Inactive sets the time after which cached data that are not accessed get removed from the cache (inactive parameter). + By default, inactive is set to 10 minutes. + pattern: ^[0-9]+[smhd]$ type: string levels: description: |- @@ -343,6 +400,68 @@ spec: Invalid: "3:1", "1:3", "1:2:3". pattern: ^[12](?::[12]){0,2}$ type: string + lock: + description: Lock configures cache locking to prevent multiple + identical requests from populating the same cache element simultaneously. + properties: + age: + description: |- + Age sets the maximum time a cache lock can be held (proxy_cache_lock_age). + If the last request passed to the proxied server for populating a new cache element has not completed for the specified time, one more request may be passed. + pattern: ^[0-9]+[smhd]$ + type: string + enable: + default: false + description: |- + Enable sets whether cache locking is enabled (proxy_cache_lock). + When enabled, only one request at a time will be allowed to populate a new cache element according to the proxy_cache_key. + type: boolean + timeout: + description: |- + Timeout sets a timeout for proxy_cache_lock. + When the time expires, the request will be passed to the proxied server, however, the response will not be cached. + pattern: ^[0-9]+[smhd]$ + type: string + type: object + x-kubernetes-validations: + - message: timeout or age require enable=true + rule: (!has(self.timeout) && !has(self.age)) || self.enable + manager: + description: Manager configures the cache manager process parameters + (manager_files, manager_sleep, manager_threshold). + properties: + files: + description: |- + Files sets the maximum number of files that will be deleted in one iteration by the cache manager. + During one iteration no more than manager_files items are deleted (by default, 100). + maximum: 2147483647 + minimum: 1 + type: integer + sleep: + description: |- + Sleep sets the pause between cache manager iterations. + Between iterations, a pause configured by manager_sleep (by default, 50 milliseconds) is made. + pattern: ^[0-9]+[mu]?s$ + type: string + threshold: + description: |- + Threshold sets the maximum duration of one cache manager iteration. + The duration of one iteration is limited by manager_threshold (by default, 200 milliseconds). + pattern: ^[0-9]+[mu]?s$ + type: string + type: object + maxSize: + description: |- + MaxSize sets the maximum cache size (max_size parameter). + When the size is exceeded, the cache manager removes the least recently used data. + pattern: ^[0-9]+[kmgKMG]$ + type: string + minFree: + description: |- + MinFree sets the minimum amount of free space required on the file system with cache (min_free parameter). + When there is not enough free space, the cache manager removes the least recently used data. + pattern: ^[0-9]+[kmgKMG]$ + type: string overrideUpstreamCache: default: false description: |- @@ -359,6 +478,12 @@ spec: Examples: "30s", "5m", "1h", "2d". pattern: ^[0-9]+[smhd]$ type: string + useTempPath: + default: false + description: |- + UseTempPath controls whether temporary files and the cache are put on different file systems (use_temp_path parameter). + If set to off, temporary files will be put directly in the cache directory. + type: boolean required: - cacheZoneName - cacheZoneSize diff --git a/docs/crd/k8s.nginx.org_policies.md b/docs/crd/k8s.nginx.org_policies.md index c4cf5c8f66..6f6450f55c 100644 --- a/docs/crd/k8s.nginx.org_policies.md +++ b/docs/crd/k8s.nginx.org_policies.md @@ -29,12 +29,32 @@ The `.spec` object supports the following fields: | `cache` | `object` | The Cache Key defines a cache policy for proxy caching | | `cache.allowedCodes` | `array` | AllowedCodes defines which HTTP response codes should be cached. Accepts either: - The string "any" to cache all response codes (must be the only element) - A list of HTTP status codes as integers (100-599) Examples: ["any"], [200, 301, 404], [200]. Invalid: ["any", 200] (cannot mix "any" with specific codes). | | `cache.allowedMethods` | `array[string]` | AllowedMethods defines which HTTP methods should be cached. Only "GET", "HEAD", and "POST" are supported by NGINX proxy_cache_methods directive. GET and HEAD are always cached by default even if not specified. Maximum of 3 items allowed. Examples: ["GET"], ["GET", "HEAD", "POST"]. Invalid methods: PUT, DELETE, PATCH, etc. | +| `cache.cacheBackgroundUpdate` | `boolean` | CacheBackgroundUpdate allows starting a background subrequest to update an expired cache item (proxy_cache_background_update). A stale cached response is returned to the client while the cache is being updated. | +| `cache.cacheKey` | `string` | CacheKey defines a key for caching (proxy_cache_key). By default, close to "$scheme$proxy_host$uri$is_args$args". | +| `cache.cacheMinUses` | `integer` | CacheMinUses sets the number of requests after which the response will be cached (proxy_cache_min_uses). | | `cache.cachePurgeAllow` | `array[string]` | CachePurgeAllow defines IP addresses or CIDR blocks allowed to purge cache. This feature is only available in NGINX Plus. Examples: ["192.168.1.100", "10.0.0.0/8", "::1"]. Invalid in NGINX OSS (will be ignored). | +| `cache.cacheRevalidate` | `boolean` | CacheRevalidate enables revalidation of expired cache items using conditional requests (proxy_cache_revalidate). Uses "If-Modified-Since" and "If-None-Match" header fields. | +| `cache.cacheUseStale` | `array[string]` | CacheUseStale determines in which cases a stale cached response can be used (proxy_cache_use_stale). Valid parameters: error, timeout, invalid_header, updating, http_500, http_502, http_503, http_504, http_403, http_404, http_429, off. | | `cache.cacheZoneName` | `string` | CacheZoneName defines the name of the cache zone. Must start with a lowercase letter, followed by alphanumeric characters or underscores, and end with an alphanumeric character. Single lowercase letters are also allowed. Examples: "cache", "my_cache", "cache1". | | `cache.cacheZoneSize` | `string` | CacheZoneSize defines the size of the cache zone. Must be a number followed by a size unit: 'k' for kilobytes, 'm' for megabytes, or 'g' for gigabytes. Examples: "10m", "1g", "512k". | +| `cache.conditions` | `object` | Conditions defines when responses should not be cached or taken from cache. | +| `cache.conditions.bypass` | `array[string]` | Bypass defines conditions under which the response will not be taken from a cache (proxy_cache_bypass). If at least one value of the string parameters is not empty and is not equal to "0" then the response will not be taken from the cache. | +| `cache.conditions.noCache` | `array[string]` | NoCache defines conditions under which the response will not be saved to a cache (proxy_no_cache). If at least one value of the string parameters is not empty and is not equal to "0" then the response will not be saved. | +| `cache.inactive` | `string` | Inactive sets the time after which cached data that are not accessed get removed from the cache (inactive parameter). By default, inactive is set to 10 minutes. | | `cache.levels` | `string` | Levels defines the cache directory hierarchy levels for storing cached files. Must be in format "X:Y" or "X:Y:Z" where X, Y, Z are either 1 or 2. This controls the number of subdirectory levels and their name lengths. Examples: "1:2", "2:2", "1:2:2". Invalid: "3:1", "1:3", "1:2:3". | +| `cache.lock` | `object` | Lock configures cache locking to prevent multiple identical requests from populating the same cache element simultaneously. | +| `cache.lock.age` | `string` | Age sets the maximum time a cache lock can be held (proxy_cache_lock_age). If the last request passed to the proxied server for populating a new cache element has not completed for the specified time, one more request may be passed. | +| `cache.lock.enable` | `boolean` | Enable sets whether cache locking is enabled (proxy_cache_lock). When enabled, only one request at a time will be allowed to populate a new cache element according to the proxy_cache_key. | +| `cache.lock.timeout` | `string` | Timeout sets a timeout for proxy_cache_lock. When the time expires, the request will be passed to the proxied server, however, the response will not be cached. | +| `cache.manager` | `object` | Manager configures the cache manager process parameters (manager_files, manager_sleep, manager_threshold). | +| `cache.manager.files` | `integer` | Files sets the maximum number of files that will be deleted in one iteration by the cache manager. During one iteration no more than manager_files items are deleted (by default, 100). | +| `cache.manager.sleep` | `string` | Sleep sets the pause between cache manager iterations. Between iterations, a pause configured by manager_sleep (by default, 50 milliseconds) is made. | +| `cache.manager.threshold` | `string` | Threshold sets the maximum duration of one cache manager iteration. The duration of one iteration is limited by manager_threshold (by default, 200 milliseconds). | +| `cache.maxSize` | `string` | MaxSize sets the maximum cache size (max_size parameter). When the size is exceeded, the cache manager removes the least recently used data. | +| `cache.minFree` | `string` | MinFree sets the minimum amount of free space required on the file system with cache (min_free parameter). When there is not enough free space, the cache manager removes the least recently used data. | | `cache.overrideUpstreamCache` | `boolean` | OverrideUpstreamCache controls whether to override upstream cache headers (using proxy_ignore_headers directive). When true, NGINX will ignore cache-related headers from upstream servers like Cache-Control, Expires, etc. Default: false. | | `cache.time` | `string` | Time defines the default cache time. Required when allowedCodes is specified. Must be a number followed by a time unit: 's' for seconds, 'm' for minutes, 'h' for hours, 'd' for days. Examples: "30s", "5m", "1h", "2d". | +| `cache.useTempPath` | `boolean` | UseTempPath controls whether temporary files and the cache are put on different file systems (use_temp_path parameter). If set to off, temporary files will be put directly in the cache directory. | | `egressMTLS` | `object` | The EgressMTLS policy configures upstreams authentication and certificate verification. | | `egressMTLS.ciphers` | `string` | Specifies the enabled ciphers for requests to an upstream HTTPS server. The default is DEFAULT. | | `egressMTLS.protocols` | `string` | Specifies the protocols for requests to an upstream HTTPS server. The default is TLSv1 TLSv1.1 TLSv1.2. | diff --git a/examples/custom-resources/cache-policy/cache.yaml b/examples/custom-resources/cache-policy/cache.yaml index 7f3370dd5c..25ea7ee960 100644 --- a/examples/custom-resources/cache-policy/cache.yaml +++ b/examples/custom-resources/cache-policy/cache.yaml @@ -12,3 +12,31 @@ spec: overrideUpstreamCache: true # Optional, default is false # levels: "1:2" # Optional, default is "1:1" , see https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_cache_path for more details # cachePurgeAllow: [""] # Optional, If set allows cache purging from the specified IPs or CIDR ranges. Nginx Plus only. + inactive: "60m" # Optional - proxy_cache_path (inactive parameter) + useTempPath: false # Optional, false by default - proxy_cache_path (use_temp_path=off) + maxSize: "10g" # Optional - proxy_cache_path (max_size) + minFree: "1g" # Optional - proxy_cache_path (min_free) + manager: # Optional - proxy_cache_path manager settings + files: 100 # manager_files + sleep: "50ms" # manager_sleep + threshold: "200ms" # manager_threshold + + # Advanced cache behavior settings + cacheKey: "${scheme}${request_method}${host}${request_uri}" # Optional - proxy_cache_key (custom cache key) +# TODO: non support variables +# cacheKey: " ${scheme}${proxy_host}${uri}?&{args}" + cacheUseStale: [ "error", "timeout", "updating", "http_500" ] # Optional - proxy_cache_use_stale (serve stale conditions) + cacheRevalidate: true # Optional, false by default - proxy_cache_revalidate + cacheBackgroundUpdate: true # Optional, false by default - proxy_cache_background_update + cacheMinUses: 3 # Optional - proxy_cache_min_uses (requests before caching) + + # Cache locking settings + lock: # Optional - proxy_cache_lock settings + enable: true # proxy_cache_lock + timeout: "5s" # proxy_cache_lock_timeout + age: "30s" # proxy_cache_lock_age + + # Conditional caching settings + conditions: # Optional - cache condition settings + noCache: [ "$cookie_nocache", "$arg_nocache" ] # proxy_no_cache (skip caching conditions) + bypass: [ "$http_authorization" ] # proxy_cache_bypass (bypass cache conditions) diff --git a/internal/configs/version2/__snapshots__/templates_test.snap b/internal/configs/version2/__snapshots__/templates_test.snap index 50f2fe69b4..d32f17f651 100644 --- a/internal/configs/version2/__snapshots__/templates_test.snap +++ b/internal/configs/version2/__snapshots__/templates_test.snap @@ -1086,8 +1086,8 @@ upstream test-upstream { server 10.0.0.20:8001 max_fails=0 fail_timeout= max_conns=0; } -proxy_cache_path /var/cache/nginx/test_cache_full_advanced levels=2:2 keys_zone=test_cache_full_advanced:50m; -proxy_cache_path /var/cache/nginx/test_cache_location_location_cache keys_zone=test_cache_location_location_cache:20m; +proxy_cache_path /var/cache/nginx/test_cache_full_advanced levels=2:2 keys_zone=test_cache_full_advanced:50m use_temp_path=off; +proxy_cache_path /var/cache/nginx/test_cache_location_location_cache keys_zone=test_cache_location_location_cache:20m use_temp_path=off; geo $purge_allowed_test_cache_full_advanced { default 0; 127.0.0.1 1; @@ -1165,8 +1165,8 @@ upstream test-upstream {zone test-upstream ; server 10.0.0.20:8001 max_fails=0 fail_timeout= max_conns=0; } -proxy_cache_path /var/cache/nginx/test_cache_basic_cache levels=1:2 keys_zone=test_cache_basic_cache:10m; -proxy_cache_path /var/cache/nginx/test_cache_location_simple_cache keys_zone=test_cache_location_simple_cache:5m; +proxy_cache_path /var/cache/nginx/test_cache_basic_cache levels=1:2 keys_zone=test_cache_basic_cache:10m use_temp_path=off; +proxy_cache_path /var/cache/nginx/test_cache_location_simple_cache keys_zone=test_cache_location_simple_cache:5m use_temp_path=off; server { listen 80; listen [::]:80; diff --git a/internal/configs/version2/http.go b/internal/configs/version2/http.go index ea806bd563..cce2d25a0e 100644 --- a/internal/configs/version2/http.go +++ b/internal/configs/version2/http.go @@ -491,20 +491,71 @@ type Variable struct { // CacheZone defines a proxy cache zone configuration. type CacheZone struct { - Name string - Size string - Path string - Levels string // Optional. Directory hierarchy for cache files (e.g., "1:2", "2:2", "1:2:2") + Name string + Size string + Path string + Levels string // Optional. Directory hierarchy for cache files (e.g., "1:2", "2:2", "1:2:2") + Inactive string // Optional. Time after which inactive cached data is removed + UseTempPath bool // Optional. Whether to use temporary path (use_temp_path=off when false) + MaxSize string // Optional. Maximum size of the cache + MinFree string // Optional. Minimum free space required + ManagerFiles *int + ManagerSleep string + ManagerThreshold string } // Cache defines cache configuration for locations. type Cache struct { - ZoneName string - ZoneSize string - Time string - Valid map[string]string // map for codes to time - AllowedMethods []string // HTTP methods allowed for caching based on proxy_cache_methods - CachePurgeAllow []string // IPs/CIDRs allowed to purge cache - OverrideUpstreamCache bool // Controls whether to override upstream cache headers - Levels string // Optional. Directory hierarchy for cache files (e.g., "1:2", "2:2", "1:2:2") + // proxy_cache directive + ZoneName string // Required. The name of the cache zone + ZoneSize string // Required. The size of the cache zone + + // proxy_cache_path directive + Levels string // Optional. Directory hierarchy for cache files (e.g., "1:2", "2:2", "1:2:2") + Inactive string // Optional. Time after which inactive cached data is removed + UseTempPath bool // Optional. Whether to use temporary path (use_temp_path=off when false) + MaxSize string // Optional. Maximum size of the cache + MinFree string // Optional. Minimum free space required + ManagerFiles *int // Optional. Number of files manager can handle + ManagerSleep string // Optional. Sleep time between manager runs + ManagerThreshold string // Optional. Manager threshold for cache operations + + // proxy_cache_key directive + CacheKey string // Optional. Custom cache key + + // proxy_ignore_headers directive + OverrideUpstreamCache bool // Controls whether to override upstream cache headers + + // proxy_cache_valid directive + Time string // Optional. Default cache validity time + Valid map[string]string // Optional. Cache validity map for codes to time + + // proxy_cache_methods directive + AllowedMethods []string // Optional. HTTP methods to cache + + // proxy_cache_use_stale directive + CacheUseStale []string // Optional. Conditions under which stale cached responses may be served + + // proxy_cache_revalidate directive + CacheRevalidate bool // Optional. Enables revalidation of expired cache items + + // proxy_cache_background_update directive + CacheBackgroundUpdate bool // Optional. Enables background updating of expired items while serving stale content + + // proxy_cache_min_uses directive + CacheMinUses *int // Optional. Minimum number of uses before a response is cached + + // proxy_cache_purge directive + CachePurgeAllow []string // Optional. IPs/CIDRs allowed to purge cache (Plus only) + + // proxy_cache_lock directive + CacheLock bool // Optional. Whether to enable cache locking + CacheLockTimeout string // Optional. Timeout for cache lock + CacheLockAge string // Optional. Age for cache lock + + // proxy_no_cache directive + NoCacheConditions []string // Optional. Conditions under which responses should not be cached + + // proxy_cache_bypass directive + CacheBypassConditions []string // Optional. Conditions under which cache should be bypassed for requests } diff --git a/internal/configs/version2/nginx-plus.virtualserver.tmpl b/internal/configs/version2/nginx-plus.virtualserver.tmpl index 4886cc2463..9a6bb18aee 100644 --- a/internal/configs/version2/nginx-plus.virtualserver.tmpl +++ b/internal/configs/version2/nginx-plus.virtualserver.tmpl @@ -71,7 +71,7 @@ limit_req_zone {{ $z.Key }} zone={{ $z.ZoneName }}:{{ $z.ZoneSize }} rate={{ $z. {{- end }} {{- range $c := .CacheZones }} -proxy_cache_path {{ $c.Path }}{{ if $c.Levels }} levels={{ $c.Levels }}{{ end }} keys_zone={{ $c.Name }}:{{ $c.Size }}; +proxy_cache_path {{ $c.Path }}{{ if $c.Levels }} levels={{ $c.Levels }}{{ end }} keys_zone={{ $c.Name }}:{{ $c.Size }}{{ if $c.Inactive }} inactive={{ $c.Inactive }}{{ end }}{{ if $c.MaxSize }} max_size={{ $c.MaxSize }}{{ end }}{{ if $c.MinFree }} min_free={{ $c.MinFree }}{{ end }}{{ if $c.ManagerFiles }} manager_files={{ $c.ManagerFiles }}{{ end }}{{ if $c.ManagerSleep }} manager_sleep={{ $c.ManagerSleep }}{{ end }}{{ if $c.ManagerThreshold }} manager_threshold={{ $c.ManagerThreshold }}{{ end }}{{ if not $c.UseTempPath }} use_temp_path=off{{ end }}; {{- end }} {{- range $m := .StatusMatches }} @@ -230,7 +230,11 @@ server { {{- with $s.Cache }} # Server-level cache configuration proxy_cache {{ $s.Cache.ZoneName }}; + {{- if $s.Cache.CacheKey }} + proxy_cache_key {{ $s.Cache.CacheKey }}; + {{- else }} proxy_cache_key $scheme$proxy_host$request_uri; + {{- end }} {{- if $s.Cache.OverrideUpstreamCache }} proxy_ignore_headers Cache-Control Expires Set-Cookie Vary X-Accel-Expires; {{- end }} @@ -242,6 +246,33 @@ server { {{- end }} {{- if $s.Cache.AllowedMethods }} proxy_cache_methods{{ range $s.Cache.AllowedMethods }} {{ . }}{{ end }}; + {{- end }} + {{- if $s.Cache.CacheUseStale }} + proxy_cache_use_stale{{ range $s.Cache.CacheUseStale }} {{ . }}{{ end }}; + {{- end }} + {{- if $s.Cache.CacheRevalidate }} + proxy_cache_revalidate on; + {{- end }} + {{- if $s.Cache.CacheBackgroundUpdate }} + proxy_cache_background_update on; + {{- end }} + {{- if $s.Cache.CacheMinUses }} + proxy_cache_min_uses {{ $s.Cache.CacheMinUses }}; + {{- end }} + {{- if $s.Cache.CacheLock }} + proxy_cache_lock on; + {{- end }} + {{- if $s.Cache.CacheLockTimeout }} + proxy_cache_lock_timeout {{ $s.Cache.CacheLockTimeout }}; + {{- end }} + {{- if $s.Cache.CacheLockAge }} + proxy_cache_lock_age {{ $s.Cache.CacheLockAge }}; + {{- end }} + {{- if $s.Cache.NoCacheConditions }} + proxy_no_cache{{ range $s.Cache.NoCacheConditions }} {{ . }}{{ end }}; + {{- end }} + {{- if $s.Cache.CacheBypassConditions }} + proxy_cache_bypass{{ range $s.Cache.CacheBypassConditions }} {{ . }}{{ end }}; {{- end }} {{- if gt (len $s.Cache.CachePurgeAllow) 0 }} proxy_cache_purge $cache_purge_{{ replaceAll $s.Cache.ZoneName "-" "_" }}; @@ -752,7 +783,11 @@ server { {{- with $l.Cache }} proxy_cache {{ $l.Cache.ZoneName }}; + {{- if $l.Cache.CacheKey }} + proxy_cache_key {{ $l.Cache.CacheKey }}; + {{- else }} proxy_cache_key $scheme$proxy_host$request_uri; + {{- end }} {{- if $l.Cache.OverrideUpstreamCache }} proxy_ignore_headers Cache-Control Expires Set-Cookie Vary X-Accel-Expires; {{- end }} @@ -764,6 +799,33 @@ server { {{- end }} {{- if $l.Cache.AllowedMethods }} proxy_cache_methods{{ range $l.Cache.AllowedMethods }} {{ . }}{{ end }}; + {{- end }} + {{- if $l.Cache.CacheUseStale }} + proxy_cache_use_stale{{ range $l.Cache.CacheUseStale }} {{ . }}{{ end }}; + {{- end }} + {{- if $l.Cache.CacheRevalidate }} + proxy_cache_revalidate on; + {{- end }} + {{- if $l.Cache.CacheBackgroundUpdate }} + proxy_cache_background_update on; + {{- end }} + {{- if $l.Cache.CacheMinUses }} + proxy_cache_min_uses {{ $l.Cache.CacheMinUses }}; + {{- end }} + {{- if $l.Cache.CacheLock }} + proxy_cache_lock on; + {{- end }} + {{- if $l.Cache.CacheLockTimeout }} + proxy_cache_lock_timeout {{ $l.Cache.CacheLockTimeout }}; + {{- end }} + {{- if $l.Cache.CacheLockAge }} + proxy_cache_lock_age {{ $l.Cache.CacheLockAge }}; + {{- end }} + {{- if $l.Cache.NoCacheConditions }} + proxy_no_cache{{ range $l.Cache.NoCacheConditions }} {{ . }}{{ end }}; + {{- end }} + {{- if $l.Cache.CacheBypassConditions }} + proxy_cache_bypass{{ range $l.Cache.CacheBypassConditions }} {{ . }}{{ end }}; {{- end }} {{- if gt (len $l.Cache.CachePurgeAllow) 0 }} proxy_cache_purge $cache_purge_{{ replaceAll $l.Cache.ZoneName "-" "_" }}; diff --git a/internal/configs/version2/nginx.virtualserver.tmpl b/internal/configs/version2/nginx.virtualserver.tmpl index 13432de9d0..c1dd23bde9 100644 --- a/internal/configs/version2/nginx.virtualserver.tmpl +++ b/internal/configs/version2/nginx.virtualserver.tmpl @@ -41,7 +41,7 @@ limit_req_zone {{ $z.Key }} zone={{ $z.ZoneName }}:{{ $z.ZoneSize }} rate={{ $z. {{- end }} {{- range $c := .CacheZones }} -proxy_cache_path {{ $c.Path }}{{ if $c.Levels }} levels={{ $c.Levels }}{{ end }} keys_zone={{ $c.Name }}:{{ $c.Size }}; +proxy_cache_path {{ $c.Path }}{{ if $c.Levels }} levels={{ $c.Levels }}{{ end }} keys_zone={{ $c.Name }}:{{ $c.Size }}{{ if $c.Inactive }} inactive={{ $c.Inactive }}{{ end }}{{ if $c.MaxSize }} max_size={{ $c.MaxSize }}{{ end }}{{ if $c.MinFree }} min_free={{ $c.MinFree }}{{ end }}{{ if $c.ManagerFiles }} manager_files={{ $c.ManagerFiles }}{{ end }}{{ if $c.ManagerSleep }} manager_sleep={{ $c.ManagerSleep }}{{ end }}{{ if $c.ManagerThreshold }} manager_threshold={{ $c.ManagerThreshold }}{{ end }}{{ if not $c.UseTempPath }} use_temp_path=off{{ end }}; {{- end }} {{- $s := .Server }} @@ -121,7 +121,11 @@ server { {{- with $s.Cache }} # Server-level cache configuration proxy_cache {{ $s.Cache.ZoneName }}; + {{- if $s.Cache.CacheKey }} + proxy_cache_key {{ $s.Cache.CacheKey }}; + {{- else }} proxy_cache_key $scheme$proxy_host$request_uri; + {{- end }} {{- if $s.Cache.OverrideUpstreamCache }} proxy_ignore_headers Cache-Control Expires Set-Cookie Vary X-Accel-Expires; {{- end }} @@ -134,6 +138,33 @@ server { {{- if $s.Cache.AllowedMethods }} proxy_cache_methods{{ range $s.Cache.AllowedMethods }} {{ . }}{{ end }}; {{- end }} + {{- if $s.Cache.CacheUseStale }} + proxy_cache_use_stale{{ range $s.Cache.CacheUseStale }} {{ . }}{{ end }}; + {{- end }} + {{- if $s.Cache.CacheRevalidate }} + proxy_cache_revalidate on; + {{- end }} + {{- if $s.Cache.CacheBackgroundUpdate }} + proxy_cache_background_update on; + {{- end }} + {{- if $s.Cache.CacheMinUses }} + proxy_cache_min_uses {{ $s.Cache.CacheMinUses }}; + {{- end }} + {{- if $s.Cache.CacheLock }} + proxy_cache_lock on; + {{- end }} + {{- if $s.Cache.CacheLockTimeout }} + proxy_cache_lock_timeout {{ $s.Cache.CacheLockTimeout }}; + {{- end }} + {{- if $s.Cache.CacheLockAge }} + proxy_cache_lock_age {{ $s.Cache.CacheLockAge }}; + {{- end }} + {{- if $s.Cache.NoCacheConditions }} + proxy_no_cache{{ range $s.Cache.NoCacheConditions }} {{ . }}{{ end }}; + {{- end }} + {{- if $s.Cache.CacheBypassConditions }} + proxy_cache_bypass{{ range $s.Cache.CacheBypassConditions }} {{ . }}{{ end }}; + {{- end }} {{- end }} {{- range $allow := $s.Allow }} @@ -446,7 +477,11 @@ server { {{- with $l.Cache }} proxy_cache {{ $l.Cache.ZoneName }}; + {{- if $l.Cache.CacheKey }} + proxy_cache_key {{ $l.Cache.CacheKey }}; + {{- else }} proxy_cache_key $scheme$proxy_host$request_uri; + {{- end }} {{- if $l.Cache.OverrideUpstreamCache }} proxy_ignore_headers Cache-Control Expires Set-Cookie Vary X-Accel-Expires; {{- end }} @@ -459,6 +494,33 @@ server { {{- if $l.Cache.AllowedMethods }} proxy_cache_methods{{ range $l.Cache.AllowedMethods }} {{ . }}{{ end }}; {{- end }} + {{- if $l.Cache.CacheUseStale }} + proxy_cache_use_stale{{ range $l.Cache.CacheUseStale }} {{ . }}{{ end }}; + {{- end }} + {{- if $l.Cache.CacheRevalidate }} + proxy_cache_revalidate on; + {{- end }} + {{- if $l.Cache.CacheBackgroundUpdate }} + proxy_cache_background_update on; + {{- end }} + {{- if $l.Cache.CacheMinUses }} + proxy_cache_min_uses {{ $l.Cache.CacheMinUses }}; + {{- end }} + {{- if $l.Cache.CacheLock }} + proxy_cache_lock on; + {{- end }} + {{- if $l.Cache.CacheLockTimeout }} + proxy_cache_lock_timeout {{ $l.Cache.CacheLockTimeout }}; + {{- end }} + {{- if $l.Cache.CacheLockAge }} + proxy_cache_lock_age {{ $l.Cache.CacheLockAge }}; + {{- end }} + {{- if $l.Cache.NoCacheConditions }} + proxy_no_cache{{ range $l.Cache.NoCacheConditions }} {{ . }}{{ end }}; + {{- end }} + {{- if $l.Cache.CacheBypassConditions }} + proxy_cache_bypass{{ range $l.Cache.CacheBypassConditions }} {{ . }}{{ end }}; + {{- end }} {{- end }} {{- if $l.GRPCPass }} diff --git a/internal/configs/version2/templates_test.go b/internal/configs/version2/templates_test.go index 7b5253f92a..eac47d5c37 100644 --- a/internal/configs/version2/templates_test.go +++ b/internal/configs/version2/templates_test.go @@ -910,7 +910,7 @@ func TestExecuteVirtualServerTemplateWithCachePolicyNGINXPlus(t *testing.T) { } // Check cache zone declaration - expectedCacheZone := "proxy_cache_path /var/cache/nginx/test_cache_full_advanced levels=2:2 keys_zone=test_cache_full_advanced:50m;" + expectedCacheZone := "proxy_cache_path /var/cache/nginx/test_cache_full_advanced levels=2:2 keys_zone=test_cache_full_advanced:50m use_temp_path=off;" if !bytes.Contains(got, []byte(expectedCacheZone)) { t.Errorf("Expected cache zone declaration: %s", expectedCacheZone) } @@ -970,7 +970,7 @@ func TestExecuteVirtualServerTemplateWithCachePolicyOSS(t *testing.T) { } // Check cache zone declaration - expectedCacheZone := "proxy_cache_path /var/cache/nginx/test_cache_basic_cache levels=1:2 keys_zone=test_cache_basic_cache:10m;" + expectedCacheZone := "proxy_cache_path /var/cache/nginx/test_cache_basic_cache levels=1:2 keys_zone=test_cache_basic_cache:10m use_temp_path=off;" if !bytes.Contains(got, []byte(expectedCacheZone)) { t.Errorf("Expected cache zone declaration: %s", expectedCacheZone) } diff --git a/internal/configs/virtualserver.go b/internal/configs/virtualserver.go index 8dc42d007a..37c9cbae42 100644 --- a/internal/configs/virtualserver.go +++ b/internal/configs/virtualserver.go @@ -1990,6 +1990,14 @@ func generateCacheConfig(cache *conf_v1.Cache, vsNamespace, vsName, ownerNamespa uniqueZoneName = fmt.Sprintf("%s_%s_%s_%s_%s", vsNamespace, vsName, ownerNamespace, ownerName, cache.CacheZoneName) } + // Set cache key with default if not provided + var cacheKey string + if cache.CacheKey != "" { + cacheKey = cache.CacheKey + } else { + cacheKey = "${scheme}${proxy_host}${request_uri}" + } + cacheConfig := &version2.Cache{ ZoneName: uniqueZoneName, Time: cache.Time, @@ -1999,6 +2007,35 @@ func generateCacheConfig(cache *conf_v1.Cache, vsNamespace, vsName, ownerNamespa ZoneSize: cache.CacheZoneSize, OverrideUpstreamCache: cache.OverrideUpstreamCache, Levels: cache.Levels, // Pass Levels from Cache to CacheZone + Inactive: cache.Inactive, + UseTempPath: cache.UseTempPath, + MaxSize: cache.MaxSize, + MinFree: cache.MinFree, + CacheKey: cacheKey, + CacheUseStale: cache.CacheUseStale, + CacheRevalidate: cache.CacheRevalidate, + CacheBackgroundUpdate: cache.CacheBackgroundUpdate, + CacheMinUses: cache.CacheMinUses, + } + + // Map lock fields + if cache.Lock != nil { + cacheConfig.CacheLock = cache.Lock.Enable + cacheConfig.CacheLockTimeout = cache.Lock.Timeout + cacheConfig.CacheLockAge = cache.Lock.Age + } + + // Map manager fields + if cache.Manager != nil { + cacheConfig.ManagerFiles = cache.Manager.Files + cacheConfig.ManagerSleep = cache.Manager.Sleep + cacheConfig.ManagerThreshold = cache.Manager.Threshold + } + + // Map conditions + if cache.Conditions != nil { + cacheConfig.NoCacheConditions = cache.Conditions.NoCache + cacheConfig.CacheBypassConditions = cache.Conditions.Bypass } // Convert allowed codes to proxy_cache_valid entries @@ -2028,10 +2065,17 @@ func addCacheZone(cacheZones *[]version2.CacheZone, cache *version2.Cache) { } cacheZone := version2.CacheZone{ - Name: cache.ZoneName, - Size: zoneSize, - Path: fmt.Sprintf("/var/cache/nginx/%s", cache.ZoneName), - Levels: cache.Levels, // Pass Levels from Cache to CacheZone + Name: cache.ZoneName, + Size: zoneSize, + Path: fmt.Sprintf("/var/cache/nginx/%s", cache.ZoneName), + Levels: cache.Levels, // Pass Levels from Cache to CacheZone + Inactive: cache.Inactive, + UseTempPath: cache.UseTempPath, + MaxSize: cache.MaxSize, + MinFree: cache.MinFree, + ManagerFiles: cache.ManagerFiles, + ManagerSleep: cache.ManagerSleep, + ManagerThreshold: cache.ManagerThreshold, } // Check for duplicates diff --git a/internal/configs/virtualserver_test.go b/internal/configs/virtualserver_test.go index bf5f8d78d1..a8df0ba9eb 100644 --- a/internal/configs/virtualserver_test.go +++ b/internal/configs/virtualserver_test.go @@ -11691,6 +11691,7 @@ func TestGenerateVirtualServerConfigCache(t *testing.T) { CachePurgeAllow: nil, OverrideUpstreamCache: false, Levels: "", + CacheKey: "${scheme}${proxy_host}${request_uri}", }, Locations: []version2.Location{ { @@ -11857,6 +11858,7 @@ func TestGenerateVirtualServerConfigCache(t *testing.T) { CachePurgeAllow: nil, OverrideUpstreamCache: false, Levels: "", + CacheKey: "${scheme}${proxy_host}${request_uri}", }, }, { @@ -11954,6 +11956,15 @@ func TestGenerateVirtualServerConfigCache(t *testing.T) { Time: "2h", OverrideUpstreamCache: true, CachePurgeAllow: []string{"127.0.0.1"}, + CacheKey: "$scheme$proxy_host$request_uri$is_args$args", + CacheBackgroundUpdate: true, + CacheUseStale: []string{"error", "timeout", "http_503"}, + Levels: "2:2", + MinFree: "100m", + Conditions: &conf_v1.CacheConditions{ + NoCache: []string{"$http_pragma", "$http_authorization"}, + Bypass: []string{"$cookie_nocache", "$arg_nocache"}, + }, }, }, }, @@ -12019,10 +12030,11 @@ func TestGenerateVirtualServerConfigCache(t *testing.T) { LimitReqZones: []version2.LimitReqZone{}, CacheZones: []version2.CacheZone{ { - Name: "default_cafe_default_tea-vsr_vsr-cache", - Size: "20m", - Path: "/var/cache/nginx/default_cafe_default_tea-vsr_vsr-cache", - Levels: "", + Name: "default_cafe_default_tea-vsr_vsr-cache", + Size: "20m", + Path: "/var/cache/nginx/default_cafe_default_tea-vsr_vsr-cache", + Levels: "2:2", + MinFree: "100m", }, }, Server: version2.Server{ @@ -12053,7 +12065,13 @@ func TestGenerateVirtualServerConfigCache(t *testing.T) { AllowedMethods: nil, CachePurgeAllow: []string{"127.0.0.1"}, OverrideUpstreamCache: true, - Levels: "", + Levels: "2:2", + MinFree: "100m", + CacheKey: "$scheme$proxy_host$request_uri$is_args$args", + CacheBackgroundUpdate: true, + CacheUseStale: []string{"error", "timeout", "http_503"}, + NoCacheConditions: []string{"$http_pragma", "$http_authorization"}, + CacheBypassConditions: []string{"$cookie_nocache", "$arg_nocache"}, }, }, { @@ -13097,6 +13115,7 @@ func TestGeneratePolicies(t *testing.T) { ZoneName: "default_test_basic-cache", ZoneSize: "10m", Valid: map[string]string{}, + CacheKey: "${scheme}${proxy_host}${request_uri}", }, }, msg: "basic cache policy reference", @@ -13123,6 +13142,30 @@ func TestGeneratePolicies(t *testing.T) { Time: "1h", OverrideUpstreamCache: true, Levels: "1:2", + Inactive: "2d", + UseTempPath: false, + MaxSize: "5g", + MinFree: "500m", + Manager: &conf_v1.CacheManager{ + Files: &[]int{1000}[0], + Sleep: "50ms", + Threshold: "150ms", + }, + CacheKey: "$scheme$proxy_host$request_uri$is_args$args", + CacheUseStale: []string{"error", "timeout", "invalid_header", "updating", "http_500", "http_502", "http_503"}, + CacheRevalidate: true, + CacheBackgroundUpdate: true, + CacheMinUses: &[]int{3}[0], + Lock: &conf_v1.CacheLock{ + Enable: true, + Timeout: "10s", + Age: "30s", + }, + CachePurgeAllow: []string{"127.0.0.1", "10.0.0.0/8"}, + Conditions: &conf_v1.CacheConditions{ + NoCache: []string{"$http_pragma", "$http_authorization"}, + Bypass: []string{"$cookie_nocache", "$arg_nocache", "$arg_comment"}, + }, }, }, }, @@ -13137,6 +13180,24 @@ func TestGeneratePolicies(t *testing.T) { AllowedMethods: []string{"GET", "HEAD", "POST"}, OverrideUpstreamCache: true, Levels: "1:2", + Inactive: "2d", + UseTempPath: false, + MaxSize: "5g", + MinFree: "500m", + ManagerFiles: &[]int{1000}[0], + ManagerSleep: "50ms", + ManagerThreshold: "150ms", + CacheKey: "$scheme$proxy_host$request_uri$is_args$args", + CacheUseStale: []string{"error", "timeout", "invalid_header", "updating", "http_500", "http_502", "http_503"}, + CacheRevalidate: true, + CacheBackgroundUpdate: true, + CacheMinUses: &[]int{3}[0], + CacheLock: true, + CacheLockTimeout: "10s", + CacheLockAge: "30s", + CachePurgeAllow: []string{"127.0.0.1", "10.0.0.0/8"}, + NoCacheConditions: []string{"$http_pragma", "$http_authorization"}, + CacheBypassConditions: []string{"$cookie_nocache", "$arg_nocache", "$arg_comment"}, }, }, msg: "full cache policy with all options", @@ -13179,6 +13240,7 @@ func TestGeneratePolicies(t *testing.T) { "301": "30m", "404": "30m", }, + CacheKey: "${scheme}${proxy_host}${request_uri}", }, }, msg: "cache policy with specific status codes", @@ -13214,6 +13276,7 @@ func TestGeneratePolicies(t *testing.T) { Valid: map[string]string{}, AllowedMethods: []string{"GET", "HEAD"}, Levels: "2:2", + CacheKey: "${scheme}${proxy_host}${request_uri}", }, }, msg: "cache policy with allowed methods and levels", @@ -13247,6 +13310,7 @@ func TestGeneratePolicies(t *testing.T) { ZoneSize: "75m", Valid: map[string]string{}, CachePurgeAllow: []string{"192.168.1.0/24", "10.0.0.1"}, + CacheKey: "${scheme}${proxy_host}${request_uri}", }, }, msg: "cache policy with purge allow IPs", @@ -13279,6 +13343,7 @@ func TestGeneratePolicies(t *testing.T) { ZoneSize: "15m", Time: "45m", Valid: map[string]string{}, + CacheKey: "${scheme}${proxy_host}${request_uri}", }, }, msg: "implicit cache policy reference", diff --git a/pkg/apis/configuration/v1/types.go b/pkg/apis/configuration/v1/types.go index 1f3dd4a6b7..50dc7bb799 100644 --- a/pkg/apis/configuration/v1/types.go +++ b/pkg/apis/configuration/v1/types.go @@ -1019,6 +1019,61 @@ type SuppliedIn struct { Query []string `json:"query"` } +// CacheManager defines cache manager process parameters for controlling the cache manager process behavior. +// The cache manager monitors the maximum cache size and removes the least recently used data when the size is exceeded. +type CacheManager struct { + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=2147483647 + // Files sets the maximum number of files that will be deleted in one iteration by the cache manager. + // During one iteration no more than manager_files items are deleted (by default, 100). + Files *int `json:"files,omitempty"` + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Pattern=`^[0-9]+[mu]?s$` + // Sleep sets the pause between cache manager iterations. + // Between iterations, a pause configured by manager_sleep (by default, 50 milliseconds) is made. + Sleep string `json:"sleep,omitempty"` + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Pattern=`^[0-9]+[mu]?s$` + // Threshold sets the maximum duration of one cache manager iteration. + // The duration of one iteration is limited by manager_threshold (by default, 200 milliseconds). + Threshold string `json:"threshold,omitempty"` +} + +// CacheLock defines cache locking parameters. When enabled, only one request at a time will be allowed to populate a new cache element. +// Other requests of the same cache element will either wait for a response to appear in the cache or the cache lock for this element to be released. +// +kubebuilder:validation:XValidation:rule="(!has(self.timeout) && !has(self.age)) || self.enable",message="timeout or age require enable=true" +type CacheLock struct { + // +kubebuilder:validation:Optional + // +kubebuilder:default=false + // Enable sets whether cache locking is enabled (proxy_cache_lock). + // When enabled, only one request at a time will be allowed to populate a new cache element according to the proxy_cache_key. + Enable bool `json:"enable,omitempty"` + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Pattern=`^[0-9]+[smhd]$` + // Timeout sets a timeout for proxy_cache_lock. + // When the time expires, the request will be passed to the proxied server, however, the response will not be cached. + Timeout string `json:"timeout,omitempty"` + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Pattern=`^[0-9]+[smhd]$` + // Age sets the maximum time a cache lock can be held (proxy_cache_lock_age). + // If the last request passed to the proxied server for populating a new cache element has not completed for the specified time, one more request may be passed. + Age string `json:"age,omitempty"` +} + +// CacheConditions defines conditions for cache bypass and no-cache behavior. +// These use NGINX variables to make dynamic caching decisions based on request characteristics. +type CacheConditions struct { + // +kubebuilder:validation:Optional + // NoCache defines conditions under which the response will not be saved to a cache (proxy_no_cache). + // If at least one value of the string parameters is not empty and is not equal to "0" then the response will not be saved. + NoCache []string `json:"noCache,omitempty"` + // +kubebuilder:validation:Optional + // Bypass defines conditions under which the response will not be taken from a cache (proxy_cache_bypass). + // If at least one value of the string parameters is not empty and is not equal to "0" then the response will not be taken from the cache. + Bypass []string `json:"bypass,omitempty"` +} + // Cache defines a cache policy for proxy caching. // +kubebuilder:validation:XValidation:rule="!has(self.allowedCodes) || (has(self.allowedCodes) && has(self.time))",message="time is required when allowedCodes is specified" type Cache struct { @@ -1029,7 +1084,7 @@ type Cache struct { // Single lowercase letters are also allowed. Examples: "cache", "my_cache", "cache1". CacheZoneName string `json:"cacheZoneName"` // +kubebuilder:validation:Required - // +kubebuilder:validation:Pattern=`^[0-9]+[kmg]$` + // +kubebuilder:validation:Pattern=`^[0-9]+[kmgKMG]$` // CacheZoneSize defines the size of the cache zone. Must be a number followed by a size unit: // 'k' for kilobytes, 'm' for megabytes, or 'g' for gigabytes. // Examples: "10m", "1g", "512k". @@ -1079,4 +1134,58 @@ type Cache struct { // Examples: "1:2", "2:2", "1:2:2". // Invalid: "3:1", "1:3", "1:2:3". Levels string `json:"levels,omitempty"` + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Pattern=`^[0-9]+[smhd]$` + // Inactive sets the time after which cached data that are not accessed get removed from the cache (inactive parameter). + // By default, inactive is set to 10 minutes. + Inactive string `json:"inactive,omitempty"` + // +kubebuilder:validation:Optional + // +kubebuilder:default=false + // UseTempPath controls whether temporary files and the cache are put on different file systems (use_temp_path parameter). + // If set to off, temporary files will be put directly in the cache directory. + UseTempPath bool `json:"useTempPath,omitempty"` + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Pattern=`^[0-9]+[kmgKMG]$` + // MaxSize sets the maximum cache size (max_size parameter). + // When the size is exceeded, the cache manager removes the least recently used data. + MaxSize string `json:"maxSize,omitempty"` + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Pattern=`^[0-9]+[kmgKMG]$` + // MinFree sets the minimum amount of free space required on the file system with cache (min_free parameter). + // When there is not enough free space, the cache manager removes the least recently used data. + MinFree string `json:"minFree,omitempty"` + // +kubebuilder:validation:Optional + // Manager configures the cache manager process parameters (manager_files, manager_sleep, manager_threshold). + Manager *CacheManager `json:"manager,omitempty"` + // +kubebuilder:validation:Optional + // +kubebuilder:validation:MaxLength=1024 + // CacheKey defines a key for caching (proxy_cache_key). + // By default, close to "$scheme$proxy_host$uri$is_args$args". + CacheKey string `json:"cacheKey,omitempty"` + // +kubebuilder:validation:Optional + // +kubebuilder:validation:MaxItems=11 + // CacheUseStale determines in which cases a stale cached response can be used (proxy_cache_use_stale). + // Valid parameters: error, timeout, invalid_header, updating, http_500, http_502, http_503, http_504, http_403, http_404, http_429, off. + CacheUseStale []string `json:"cacheUseStale,omitempty"` + // +kubebuilder:validation:Optional + // +kubebuilder:default=false + // CacheRevalidate enables revalidation of expired cache items using conditional requests (proxy_cache_revalidate). + // Uses "If-Modified-Since" and "If-None-Match" header fields. + CacheRevalidate bool `json:"cacheRevalidate,omitempty"` + // +kubebuilder:validation:Optional + // +kubebuilder:default=false + // CacheBackgroundUpdate allows starting a background subrequest to update an expired cache item (proxy_cache_background_update). + // A stale cached response is returned to the client while the cache is being updated. + CacheBackgroundUpdate bool `json:"cacheBackgroundUpdate,omitempty"` + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=2147483647 + // CacheMinUses sets the number of requests after which the response will be cached (proxy_cache_min_uses). + CacheMinUses *int `json:"cacheMinUses,omitempty"` + // +kubebuilder:validation:Optional + // Lock configures cache locking to prevent multiple identical requests from populating the same cache element simultaneously. + Lock *CacheLock `json:"lock,omitempty"` + // +kubebuilder:validation:Optional + // Conditions defines when responses should not be cached or taken from cache. + Conditions *CacheConditions `json:"conditions,omitempty"` } diff --git a/pkg/apis/configuration/v1/zz_generated.deepcopy.go b/pkg/apis/configuration/v1/zz_generated.deepcopy.go index 9cc0b9da8a..e845ea505f 100644 --- a/pkg/apis/configuration/v1/zz_generated.deepcopy.go +++ b/pkg/apis/configuration/v1/zz_generated.deepcopy.go @@ -202,6 +202,31 @@ func (in *Cache) DeepCopyInto(out *Cache) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.Manager != nil { + in, out := &in.Manager, &out.Manager + *out = new(CacheManager) + (*in).DeepCopyInto(*out) + } + if in.CacheUseStale != nil { + in, out := &in.CacheUseStale, &out.CacheUseStale + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.CacheMinUses != nil { + in, out := &in.CacheMinUses, &out.CacheMinUses + *out = new(int) + **out = **in + } + if in.Lock != nil { + in, out := &in.Lock, &out.Lock + *out = new(CacheLock) + **out = **in + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = new(CacheConditions) + (*in).DeepCopyInto(*out) + } return } @@ -215,6 +240,69 @@ func (in *Cache) DeepCopy() *Cache { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CacheConditions) DeepCopyInto(out *CacheConditions) { + *out = *in + if in.NoCache != nil { + in, out := &in.NoCache, &out.NoCache + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Bypass != nil { + in, out := &in.Bypass, &out.Bypass + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CacheConditions. +func (in *CacheConditions) DeepCopy() *CacheConditions { + if in == nil { + return nil + } + out := new(CacheConditions) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CacheLock) DeepCopyInto(out *CacheLock) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CacheLock. +func (in *CacheLock) DeepCopy() *CacheLock { + if in == nil { + return nil + } + out := new(CacheLock) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CacheManager) DeepCopyInto(out *CacheManager) { + *out = *in + if in.Files != nil { + in, out := &in.Files, &out.Files + *out = new(int) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CacheManager. +func (in *CacheManager) DeepCopy() *CacheManager { + if in == nil { + return nil + } + out := new(CacheManager) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CertManager) DeepCopyInto(out *CertManager) { *out = *in diff --git a/pkg/apis/configuration/validation/policy.go b/pkg/apis/configuration/validation/policy.go index af0e2bc298..cea3b49c41 100644 --- a/pkg/apis/configuration/validation/policy.go +++ b/pkg/apis/configuration/validation/policy.go @@ -12,6 +12,7 @@ import ( validation2 "github.com/nginx/kubernetes-ingress/internal/validation" v1 "github.com/nginx/kubernetes-ingress/pkg/apis/configuration/v1" "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" ) @@ -438,8 +439,84 @@ func validateCache(cache *v1.Cache, fieldPath *field.Path, isPlus bool) field.Er allErrs = append(allErrs, validateCacheAllowedCodes(cache, fieldPath)...) + // Validate NGINX Plus features allErrs = append(allErrs, validateCachePlusFeatures(cache, fieldPath, isPlus)...) + // Validate time fields + if cache.Inactive != "" { + allErrs = append(allErrs, validateTime(cache.Inactive, fieldPath.Child("inactive"))...) + } + + // Validate size fields + // TODO: validate size with g|G properly + if cache.MaxSize != "" { + allErrs = append(allErrs, validateOffset(cache.MaxSize, fieldPath.Child("maxSize"))...) + } + if cache.MinFree != "" { + allErrs = append(allErrs, validateOffset(cache.MinFree, fieldPath.Child("minFree"))...) + } + + // Validate manager fields + if cache.Manager != nil { + managerPath := fieldPath.Child("manager") + if cache.Manager.Files != nil && *cache.Manager.Files <= 0 { + allErrs = append(allErrs, field.Invalid(managerPath.Child("files"), *cache.Manager.Files, "must be a positive integer")) + } + if cache.Manager.Sleep != "" { + allErrs = append(allErrs, validateTime(cache.Manager.Sleep, managerPath.Child("sleep"))...) + } + if cache.Manager.Threshold != "" { + allErrs = append(allErrs, validateTime(cache.Manager.Threshold, managerPath.Child("threshold"))...) + } + } + + // Validate cache min uses + if cache.CacheMinUses != nil && *cache.CacheMinUses <= 0 { + allErrs = append(allErrs, field.Invalid(fieldPath.Child("cacheMinUses"), *cache.CacheMinUses, "must be a positive integer")) + } + + // Validate lock fields + if cache.Lock != nil { + lockPath := fieldPath.Child("lock") + if cache.Lock.Timeout != "" { + allErrs = append(allErrs, validateTime(cache.Lock.Timeout, lockPath.Child("timeout"))...) + } + if cache.Lock.Age != "" { + allErrs = append(allErrs, validateTime(cache.Lock.Age, lockPath.Child("age"))...) + } + } + + // Validate cache key + if cache.CacheKey != "" { + if err := ValidateEscapedString(cache.CacheKey); err != nil { + allErrs = append(allErrs, field.Invalid(fieldPath.Child("cacheKey"), cache.CacheKey, err.Error())) + } + // Validate that cache key contains valid NGINX variables + allErrs = append(allErrs, validateStringWithVariables(cache.CacheKey, fieldPath.Child("cacheKey"), actionProxyHeaderSpecialVariables, actionProxyHeaderVariables, false)...) + } + + // Validate conditions + if cache.Conditions != nil { + conditionsPath := fieldPath.Child("conditions") + for i, condition := range cache.Conditions.NoCache { + if condition != "" { + if err := ValidateEscapedString(condition); err != nil { + allErrs = append(allErrs, field.Invalid(conditionsPath.Child("noCache").Index(i), condition, err.Error())) + } + } + } + for i, condition := range cache.Conditions.Bypass { + if condition != "" { + if err := ValidateEscapedString(condition); err != nil { + allErrs = append(allErrs, field.Invalid(conditionsPath.Child("bypass").Index(i), condition, err.Error())) + } + } + } + } + + // Validate use stale + allErrs = append(allErrs, validateCacheUseStale(cache, fieldPath)...) + return allErrs } @@ -512,6 +589,30 @@ func validateCachePlusFeatures(cache *v1.Cache, fieldPath *field.Path, isPlus bo return allErrs } +// validateCacheUseStale validates the cacheUseStale field values +// The directive's parameters match the parameters of the proxy_next_upstream directive plus "updating" +func validateCacheUseStale(cache *v1.Cache, fieldPath *field.Path) field.ErrorList { + if len(cache.CacheUseStale) == 0 { + return nil + } + + allErrs := field.ErrorList{} + allParams := sets.Set[string]{} + + for _, para := range cache.CacheUseStale { + // Check if parameter is valid (either from validNextUpstreamParams or "updating" which is specific to cache) + if !validNextUpstreamParams[para] && para != "updating" { + allErrs = append(allErrs, field.Invalid(fieldPath.Child("cacheUseStale"), para, "not a valid parameter")) + } + if allParams.Has(para) { + allErrs = append(allErrs, field.Invalid(fieldPath.Child("cacheUseStale"), para, "can not have duplicate parameters")) + } else { + allParams.Insert(para) + } + } + return allErrs +} + func validateLogConf(logConf *v1.SecurityLog, fieldPath *field.Path, bundleMode bool) field.ErrorList { allErrs := field.ErrorList{} diff --git a/pkg/apis/configuration/validation/virtualserver.go b/pkg/apis/configuration/validation/virtualserver.go index e5edfff294..911051e154 100644 --- a/pkg/apis/configuration/validation/virtualserver.go +++ b/pkg/apis/configuration/validation/virtualserver.go @@ -1171,6 +1171,7 @@ func validateActionProxyRewritePathForRegexp(rewritePath string, fieldPath *fiel return allErrs } +// TODO: what variable should be allowed for cache key and should this list be reused var actionProxyHeaderVariables = map[string]bool{ "request_uri": true, "request_method": true, @@ -1216,6 +1217,8 @@ var actionProxyHeaderVariables = map[string]bool{ "ssl_server_name": true, "ssl_session_id": true, "ssl_session_reused": true, + "uri": true, + "proxy_host": true, } var actionProxyHeaderSpecialVariables = []string{"arg_", "http_", "cookie_", "jwt_claim_", "jwt_header_"} @@ -1276,6 +1279,7 @@ func (vsv *VirtualServerValidator) validateActionProxyResponseHeaders(responseHe return allErrs } +// TODO: should it be customisable for cache var validIgnoreHeaders = map[string]bool{ "X-Accel-Redirect": true, "X-Accel-Expires": true, From 389d7f7c62ed821c5db497211bd1e78c49f76e11 Mon Sep 17 00:00:00 2001 From: Venktesh Shivam Patel Date: Wed, 26 Nov 2025 16:16:06 +0000 Subject: [PATCH 02/16] add validation on cacheKey and unit tests --- config/crd/bases/k8s.nginx.org_policies.yaml | 11 +- deploy/crds.yaml | 11 +- docs/crd/k8s.nginx.org_policies.md | 4 +- .../__snapshots__/templates_test.snap | 69 ++++++ internal/configs/version2/templates_test.go | 122 ++++++++++ internal/configs/virtualserver_test.go | 161 +++++++++++++ pkg/apis/configuration/v1/types.go | 11 +- .../configuration/validation/policy_test.go | 220 ++++++++++++++++++ .../configuration/validation/virtualserver.go | 1 - 9 files changed, 598 insertions(+), 12 deletions(-) diff --git a/config/crd/bases/k8s.nginx.org_policies.yaml b/config/crd/bases/k8s.nginx.org_policies.yaml index 54e418ad82..bc0d2bd8d0 100644 --- a/config/crd/bases/k8s.nginx.org_policies.yaml +++ b/config/crd/bases/k8s.nginx.org_policies.yaml @@ -150,12 +150,17 @@ spec: description: |- CacheKey defines a key for caching (proxy_cache_key). By default, close to "$scheme$proxy_host$uri$is_args$args". + Must not contain command execution patterns: $(, `, ;, &&, || maxLength: 1024 type: string + x-kubernetes-validations: + - message: 'cache key must not contain command execution patterns: + $(, `, ;, &&, ||' + rule: '!self.contains(''$('') && !self.contains(''`'') && !self.contains('';'') + && !self.contains(''&&'') && !self.contains(''||'')' cacheMinUses: description: CacheMinUses sets the number of requests after which the response will be cached (proxy_cache_min_uses). - maximum: 2147483647 minimum: 1 type: integer cachePurgeAllow: @@ -263,7 +268,6 @@ spec: description: |- Files sets the maximum number of files that will be deleted in one iteration by the cache manager. During one iteration no more than manager_files items are deleted (by default, 100). - maximum: 2147483647 minimum: 1 type: integer sleep: @@ -311,7 +315,8 @@ spec: default: false description: |- UseTempPath controls whether temporary files and the cache are put on different file systems (use_temp_path parameter). - If set to off, temporary files will be put directly in the cache directory. + If set to false, temporary files will be put directly in the cache directory (use_temp_path=off). + Default: false (use_temp_path=off, which puts temp files directly in cache directory for better performance). type: boolean required: - cacheZoneName diff --git a/deploy/crds.yaml b/deploy/crds.yaml index 088a6755dd..caad1c72a3 100644 --- a/deploy/crds.yaml +++ b/deploy/crds.yaml @@ -321,12 +321,17 @@ spec: description: |- CacheKey defines a key for caching (proxy_cache_key). By default, close to "$scheme$proxy_host$uri$is_args$args". + Must not contain command execution patterns: $(, `, ;, &&, || maxLength: 1024 type: string + x-kubernetes-validations: + - message: 'cache key must not contain command execution patterns: + $(, `, ;, &&, ||' + rule: '!self.contains(''$('') && !self.contains(''`'') && !self.contains('';'') + && !self.contains(''&&'') && !self.contains(''||'')' cacheMinUses: description: CacheMinUses sets the number of requests after which the response will be cached (proxy_cache_min_uses). - maximum: 2147483647 minimum: 1 type: integer cachePurgeAllow: @@ -434,7 +439,6 @@ spec: description: |- Files sets the maximum number of files that will be deleted in one iteration by the cache manager. During one iteration no more than manager_files items are deleted (by default, 100). - maximum: 2147483647 minimum: 1 type: integer sleep: @@ -482,7 +486,8 @@ spec: default: false description: |- UseTempPath controls whether temporary files and the cache are put on different file systems (use_temp_path parameter). - If set to off, temporary files will be put directly in the cache directory. + If set to false, temporary files will be put directly in the cache directory (use_temp_path=off). + Default: false (use_temp_path=off, which puts temp files directly in cache directory for better performance). type: boolean required: - cacheZoneName diff --git a/docs/crd/k8s.nginx.org_policies.md b/docs/crd/k8s.nginx.org_policies.md index 6f6450f55c..88418e31d5 100644 --- a/docs/crd/k8s.nginx.org_policies.md +++ b/docs/crd/k8s.nginx.org_policies.md @@ -30,7 +30,7 @@ The `.spec` object supports the following fields: | `cache.allowedCodes` | `array` | AllowedCodes defines which HTTP response codes should be cached. Accepts either: - The string "any" to cache all response codes (must be the only element) - A list of HTTP status codes as integers (100-599) Examples: ["any"], [200, 301, 404], [200]. Invalid: ["any", 200] (cannot mix "any" with specific codes). | | `cache.allowedMethods` | `array[string]` | AllowedMethods defines which HTTP methods should be cached. Only "GET", "HEAD", and "POST" are supported by NGINX proxy_cache_methods directive. GET and HEAD are always cached by default even if not specified. Maximum of 3 items allowed. Examples: ["GET"], ["GET", "HEAD", "POST"]. Invalid methods: PUT, DELETE, PATCH, etc. | | `cache.cacheBackgroundUpdate` | `boolean` | CacheBackgroundUpdate allows starting a background subrequest to update an expired cache item (proxy_cache_background_update). A stale cached response is returned to the client while the cache is being updated. | -| `cache.cacheKey` | `string` | CacheKey defines a key for caching (proxy_cache_key). By default, close to "$scheme$proxy_host$uri$is_args$args". | +| `cache.cacheKey` | `string` | CacheKey defines a key for caching (proxy_cache_key). By default, close to "$scheme$proxy_host$uri$is_args$args". Must not contain command execution patterns: $(, `, ;, &&, || | | `cache.cacheMinUses` | `integer` | CacheMinUses sets the number of requests after which the response will be cached (proxy_cache_min_uses). | | `cache.cachePurgeAllow` | `array[string]` | CachePurgeAllow defines IP addresses or CIDR blocks allowed to purge cache. This feature is only available in NGINX Plus. Examples: ["192.168.1.100", "10.0.0.0/8", "::1"]. Invalid in NGINX OSS (will be ignored). | | `cache.cacheRevalidate` | `boolean` | CacheRevalidate enables revalidation of expired cache items using conditional requests (proxy_cache_revalidate). Uses "If-Modified-Since" and "If-None-Match" header fields. | @@ -54,7 +54,7 @@ The `.spec` object supports the following fields: | `cache.minFree` | `string` | MinFree sets the minimum amount of free space required on the file system with cache (min_free parameter). When there is not enough free space, the cache manager removes the least recently used data. | | `cache.overrideUpstreamCache` | `boolean` | OverrideUpstreamCache controls whether to override upstream cache headers (using proxy_ignore_headers directive). When true, NGINX will ignore cache-related headers from upstream servers like Cache-Control, Expires, etc. Default: false. | | `cache.time` | `string` | Time defines the default cache time. Required when allowedCodes is specified. Must be a number followed by a time unit: 's' for seconds, 'm' for minutes, 'h' for hours, 'd' for days. Examples: "30s", "5m", "1h", "2d". | -| `cache.useTempPath` | `boolean` | UseTempPath controls whether temporary files and the cache are put on different file systems (use_temp_path parameter). If set to off, temporary files will be put directly in the cache directory. | +| `cache.useTempPath` | `boolean` | UseTempPath controls whether temporary files and the cache are put on different file systems (use_temp_path parameter). If set to false, temporary files will be put directly in the cache directory (use_temp_path=off). Default: false (use_temp_path=off, which puts temp files directly in cache directory for better performance). | | `egressMTLS` | `object` | The EgressMTLS policy configures upstreams authentication and certificate verification. | | `egressMTLS.ciphers` | `string` | Specifies the enabled ciphers for requests to an upstream HTTPS server. The default is DEFAULT. | | `egressMTLS.protocols` | `string` | Specifies the protocols for requests to an upstream HTTPS server. The default is TLSv1 TLSv1.1 TLSv1.2. | diff --git a/internal/configs/version2/__snapshots__/templates_test.snap b/internal/configs/version2/__snapshots__/templates_test.snap index d32f17f651..cddc5275ce 100644 --- a/internal/configs/version2/__snapshots__/templates_test.snap +++ b/internal/configs/version2/__snapshots__/templates_test.snap @@ -3734,3 +3734,72 @@ server { } --- + +[TestExecuteVirtualServerTemplateWithExtendedCachePolicy - 1] + +upstream extended-upstream { + zone extended-upstream ; + server 10.0.0.50:8001 max_fails=0 fail_timeout= max_conns=0; +} + +proxy_cache_path /var/cache/nginx/extended_cache_zone levels=2:2 keys_zone=extended_cache_zone:100m inactive=7d max_size=2g manager_files=500 manager_sleep=200ms manager_threshold=1s use_temp_path=off; + +server { + listen 80; + listen [::]:80; + + + server_name extended.example.com; + status_zone extended.example.com; + set $resource_type "virtualserver"; + set $resource_name ""; + set $resource_namespace ""; + + server_tokens "off"; + # Server-level cache configuration + proxy_cache extended_cache_zone; + proxy_cache_key $scheme$host$request_uri$args; + proxy_cache_valid 200 1h; + proxy_cache_valid 404 10m; + proxy_cache_valid any 5m; + proxy_cache_use_stale error timeout updating; + proxy_cache_revalidate on; + proxy_cache_background_update on; + proxy_cache_min_uses 3; + proxy_cache_lock on; + proxy_cache_lock_timeout 60s; + proxy_no_cache $cookie_admin; + proxy_cache_bypass $http_cache_control; + + + + + location /api { + set $service ""; + status_zone ""; + + + set $default_connection_header close; + proxy_connect_timeout ; + proxy_read_timeout ; + proxy_send_timeout ; + client_max_body_size ; + + proxy_buffering off; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $vs_connection_header; + proxy_pass_request_headers off; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_pass http://extended-upstream; + proxy_next_upstream ; + proxy_next_upstream_timeout ; + proxy_next_upstream_tries 0; + } +} + +--- diff --git a/internal/configs/version2/templates_test.go b/internal/configs/version2/templates_test.go index eac47d5c37..e650c1b0ba 100644 --- a/internal/configs/version2/templates_test.go +++ b/internal/configs/version2/templates_test.go @@ -961,6 +961,65 @@ func TestExecuteVirtualServerTemplateWithCachePolicyNGINXPlus(t *testing.T) { t.Log(string(got)) } +func TestExecuteVirtualServerTemplateWithExtendedCachePolicy(t *testing.T) { + t.Parallel() + executor := newTmplExecutorNGINXPlus(t) + got, err := executor.ExecuteVirtualServerTemplate(&virtualServerCfgWithExtendedCachePolicy) + if err != nil { + t.Error(err) + } + + // Check extended cache zone declaration with new fields + expectedCacheZone := "proxy_cache_path /var/cache/nginx/extended_cache_zone levels=2:2 keys_zone=extended_cache_zone:100m inactive=7d max_size=2g manager_files=500 manager_sleep=200ms manager_threshold=1s use_temp_path=off;" + if !bytes.Contains(got, []byte(expectedCacheZone)) { + t.Errorf("Expected extended cache zone declaration: %s", expectedCacheZone) + } + + // Check cache lock configuration + expectedCacheLock := "proxy_cache_lock on;" + if !bytes.Contains(got, []byte(expectedCacheLock)) { + t.Errorf("Expected cache lock directive: %s", expectedCacheLock) + } + + expectedCacheLockTimeout := "proxy_cache_lock_timeout 60s;" + if !bytes.Contains(got, []byte(expectedCacheLockTimeout)) { + t.Errorf("Expected cache lock timeout directive: %s", expectedCacheLockTimeout) + } + + // Check cache conditions + expectedNoCache := "proxy_no_cache $cookie_admin;" + if !bytes.Contains(got, []byte(expectedNoCache)) { + t.Errorf("Expected no-cache condition directive: %s", expectedNoCache) + } + + expectedCacheBypass := "proxy_cache_bypass $http_cache_control;" + if !bytes.Contains(got, []byte(expectedCacheBypass)) { + t.Errorf("Expected cache bypass condition directive: %s", expectedCacheBypass) + } + + // Check extended cache directives + expectedCacheDirectives := []string{ + "proxy_cache extended_cache_zone;", + "proxy_cache_key $scheme$host$request_uri$args;", + "proxy_cache_min_uses 3;", + "proxy_cache_valid 200 1h;", + "proxy_cache_valid 404 10m;", + "proxy_cache_valid any 5m;", + "proxy_cache_background_update on;", + "proxy_cache_use_stale error timeout updating;", + "proxy_cache_revalidate on;", + } + + for _, directive := range expectedCacheDirectives { + if !bytes.Contains(got, []byte(directive)) { + t.Errorf("Expected cache directive: %s", directive) + } + } + + snaps.MatchSnapshot(t, string(got)) + t.Log(string(got)) +} + func TestExecuteVirtualServerTemplateWithCachePolicyOSS(t *testing.T) { t.Parallel() executor := newTmplExecutorNGINX(t) @@ -2941,6 +3000,69 @@ var ( }, } + virtualServerCfgWithExtendedCachePolicy = VirtualServerConfig{ + CacheZones: []CacheZone{ + { + Name: "extended_cache_zone", + Size: "100m", + Path: "/var/cache/nginx/extended_cache_zone", + Levels: "2:2", + UseTempPath: false, + MaxSize: "2g", + Inactive: "7d", + ManagerFiles: createPointerFromInt(500), + ManagerSleep: "200ms", + ManagerThreshold: "1s", + }, + }, + Upstreams: []Upstream{ + { + Name: "extended-upstream", + Servers: []UpstreamServer{ + { + Address: "10.0.0.50:8001", + }, + }, + }, + }, + Server: Server{ + ServerName: "extended.example.com", + StatusZone: "extended.example.com", + ServerTokens: "off", + // Server-level cache policy with all extended options + Cache: &Cache{ + ZoneName: "extended_cache_zone", + ZoneSize: "100m", + Levels: "2:2", + Inactive: "7d", + UseTempPath: false, + MaxSize: "2g", + ManagerFiles: createPointerFromInt(500), + ManagerSleep: "200ms", + ManagerThreshold: "1s", + CacheKey: "$scheme$host$request_uri$args", + OverrideUpstreamCache: false, + Valid: map[string]string{"200": "1h", "404": "10m", "any": "5m"}, + AllowedMethods: nil, + CacheUseStale: []string{"error", "timeout", "updating"}, + CacheRevalidate: true, + CacheBackgroundUpdate: true, + CacheMinUses: createPointerFromInt(3), + CachePurgeAllow: nil, + CacheLock: true, + CacheLockTimeout: "60s", + NoCacheConditions: []string{"$cookie_admin"}, + CacheBypassConditions: []string{"$http_cache_control"}, + }, + Locations: []Location{ + { + Path: "/api", + ProxyPass: "http://extended-upstream", + }, + }, + }, + } + transportServerCfg = TransportServerConfig{ Upstreams: []StreamUpstream{ { diff --git a/internal/configs/virtualserver_test.go b/internal/configs/virtualserver_test.go index a8df0ba9eb..de2bc596a1 100644 --- a/internal/configs/virtualserver_test.go +++ b/internal/configs/virtualserver_test.go @@ -12092,6 +12092,167 @@ func TestGenerateVirtualServerConfigCache(t *testing.T) { }, }, }, + { + msg: "cache policy with extended fields", + virtualServerEx: VirtualServerEx{ + VirtualServer: &conf_v1.VirtualServer{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "extended-cache", + Namespace: "default", + }, + Spec: conf_v1.VirtualServerSpec{ + Host: "cache.example.com", + Policies: []conf_v1.PolicyReference{ + { + Name: "extended-cache-policy", + }, + }, + Upstreams: []conf_v1.Upstream{ + { + Name: "backend", + Service: "backend-svc", + Port: 80, + }, + }, + Routes: []conf_v1.Route{ + { + Path: "/api", + Action: &conf_v1.Action{ + Pass: "backend", + }, + }, + }, + }, + }, + Policies: map[string]*conf_v1.Policy{ + "default/extended-cache-policy": { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "extended-cache-policy", + Namespace: "default", + }, + Spec: conf_v1.PolicySpec{ + Cache: &conf_v1.Cache{ + CacheZoneName: "extended-cache", + CacheZoneSize: "100m", + CacheKey: "$scheme$host$request_uri$args", + CacheMinUses: createPointerFromInt(3), + UseTempPath: false, + MaxSize: "2g", + Inactive: "7d", + Manager: &conf_v1.CacheManager{ + Files: createPointerFromInt(500), + Sleep: "200ms", + Threshold: "1s", + }, + Lock: &conf_v1.CacheLock{ + Enable: true, + Timeout: "60s", + }, + Conditions: &conf_v1.CacheConditions{ + NoCache: []string{"$cookie_admin"}, + Bypass: []string{"$http_cache_control"}, + }, + AllowedCodes: []intstr.IntOrString{ + intstr.FromString("200"), + intstr.FromString("404"), + intstr.FromString("any"), + }, + Time: "1h", + CacheBackgroundUpdate: true, + CacheRevalidate: true, + }, + }, + }, + }, + Endpoints: map[string][]string{ + "default/backend-svc:80": { + "10.0.0.40:80", + }, + }, + }, + expected: version2.VirtualServerConfig{ + Upstreams: []version2.Upstream{ + { + UpstreamLabels: version2.UpstreamLabels{ + Service: "backend-svc", + ResourceType: "virtualserver", + ResourceName: "extended-cache", + ResourceNamespace: "default", + }, + Name: "vs_default_extended-cache_backend", + Servers: []version2.UpstreamServer{ + { + Address: "10.0.0.40:80", + }, + }, + }, + }, + HTTPSnippets: []string{}, + LimitReqZones: []version2.LimitReqZone{}, + CacheZones: []version2.CacheZone{ + { + Name: "default_extended-cache_extended-cache", + Size: "100m", + Path: "/var/cache/nginx/default_extended-cache_extended-cache", + Levels: "", + Inactive: "7d", + UseTempPath: false, + MaxSize: "2g", + MinFree: "", + ManagerFiles: createPointerFromInt(500), + ManagerSleep: "200ms", + ManagerThreshold: "1s", + }, + }, + Server: version2.Server{ + ServerName: "cache.example.com", + StatusZone: "cache.example.com", + ServerTokens: "off", + VSNamespace: "default", + VSName: "extended-cache", + Cache: &version2.Cache{ + ZoneName: "default_extended-cache_extended-cache", + ZoneSize: "100m", + Levels: "", + Inactive: "7d", + UseTempPath: false, + MaxSize: "2g", + MinFree: "", + ManagerFiles: createPointerFromInt(500), + ManagerSleep: "200ms", + ManagerThreshold: "1s", + CacheKey: "$scheme$host$request_uri$args", + OverrideUpstreamCache: false, + Time: "1h", + Valid: map[string]string{"200": "1h", "404": "1h", "any": "1h"}, + AllowedMethods: nil, + CacheUseStale: nil, + CacheRevalidate: true, + CacheBackgroundUpdate: true, + CacheMinUses: createPointerFromInt(3), + CachePurgeAllow: nil, + CacheLock: true, + CacheLockTimeout: "60s", + CacheLockAge: "", + NoCacheConditions: []string{"$cookie_admin"}, + CacheBypassConditions: []string{"$http_cache_control"}, + }, + Locations: []version2.Location{ + { + Path: "/api", + ProxyPass: "http://vs_default_extended-cache_backend", + ProxyNextUpstream: "error timeout", + ProxyNextUpstreamTimeout: "0s", + ProxyNextUpstreamTries: 0, + ProxySSLName: "backend-svc.default.svc", + ProxyPassRequestHeaders: true, + ProxySetHeaders: []version2.Header{{Name: "Host", Value: "$host"}}, + ServiceName: "backend-svc", + }, + }, + }, + }, + }, } baseCfgParams := ConfigParams{ diff --git a/pkg/apis/configuration/v1/types.go b/pkg/apis/configuration/v1/types.go index 50dc7bb799..7ece69fbbe 100644 --- a/pkg/apis/configuration/v1/types.go +++ b/pkg/apis/configuration/v1/types.go @@ -1024,7 +1024,6 @@ type SuppliedIn struct { type CacheManager struct { // +kubebuilder:validation:Optional // +kubebuilder:validation:Minimum=1 - // +kubebuilder:validation:Maximum=2147483647 // Files sets the maximum number of files that will be deleted in one iteration by the cache manager. // During one iteration no more than manager_files items are deleted (by default, 100). Files *int `json:"files,omitempty"` @@ -1065,12 +1064,16 @@ type CacheLock struct { // These use NGINX variables to make dynamic caching decisions based on request characteristics. type CacheConditions struct { // +kubebuilder:validation:Optional + // +kubebuilder:validation:XValidation:rule="self.all(item, !item.contains('$(') && !item.contains('`') && !item.contains(';') && !item.contains('&&') && !item.contains('||'))",message="cache conditions must not contain command execution patterns: $(, `, ;, &&, ||" // NoCache defines conditions under which the response will not be saved to a cache (proxy_no_cache). // If at least one value of the string parameters is not empty and is not equal to "0" then the response will not be saved. + // Must not contain command execution patterns: $(, `, ;, &&, || NoCache []string `json:"noCache,omitempty"` // +kubebuilder:validation:Optional + // +kubebuilder:validation:XValidation:rule="self.all(item, !item.contains('$(') && !item.contains('`') && !item.contains(';') && !item.contains('&&') && !item.contains('||'))",message="cache conditions must not contain command execution patterns: $(, `, ;, &&, ||" // Bypass defines conditions under which the response will not be taken from a cache (proxy_cache_bypass). // If at least one value of the string parameters is not empty and is not equal to "0" then the response will not be taken from the cache. + // Must not contain command execution patterns: $(, `, ;, &&, || Bypass []string `json:"bypass,omitempty"` } @@ -1142,7 +1145,8 @@ type Cache struct { // +kubebuilder:validation:Optional // +kubebuilder:default=false // UseTempPath controls whether temporary files and the cache are put on different file systems (use_temp_path parameter). - // If set to off, temporary files will be put directly in the cache directory. + // If set to false, temporary files will be put directly in the cache directory (use_temp_path=off). + // Default: false (use_temp_path=off, which puts temp files directly in cache directory for better performance). UseTempPath bool `json:"useTempPath,omitempty"` // +kubebuilder:validation:Optional // +kubebuilder:validation:Pattern=`^[0-9]+[kmgKMG]$` @@ -1159,8 +1163,10 @@ type Cache struct { Manager *CacheManager `json:"manager,omitempty"` // +kubebuilder:validation:Optional // +kubebuilder:validation:MaxLength=1024 + // +kubebuilder:validation:XValidation:rule="!self.contains('$(') && !self.contains('`') && !self.contains(';') && !self.contains('&&') && !self.contains('||')",message="cache key must not contain command execution patterns: $(, `, ;, &&, ||" // CacheKey defines a key for caching (proxy_cache_key). // By default, close to "$scheme$proxy_host$uri$is_args$args". + // Must not contain command execution patterns: $(, `, ;, &&, || CacheKey string `json:"cacheKey,omitempty"` // +kubebuilder:validation:Optional // +kubebuilder:validation:MaxItems=11 @@ -1179,7 +1185,6 @@ type Cache struct { CacheBackgroundUpdate bool `json:"cacheBackgroundUpdate,omitempty"` // +kubebuilder:validation:Optional // +kubebuilder:validation:Minimum=1 - // +kubebuilder:validation:Maximum=2147483647 // CacheMinUses sets the number of requests after which the response will be cached (proxy_cache_min_uses). CacheMinUses *int `json:"cacheMinUses,omitempty"` // +kubebuilder:validation:Optional diff --git a/pkg/apis/configuration/validation/policy_test.go b/pkg/apis/configuration/validation/policy_test.go index 0ba5667edb..1985de1712 100644 --- a/pkg/apis/configuration/validation/policy_test.go +++ b/pkg/apis/configuration/validation/policy_test.go @@ -2643,6 +2643,131 @@ func TestValidatePolicy_IsNotValidCachePolicy(t *testing.T) { }, isPlus: false, }, + // Note: Command execution pattern validation has been moved to CRD level in types.go + // This test case is no longer valid as the validation happens at Kubernetes API level + { + name: "cache policy with invalid minUses (zero)", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + Cache: &v1.Cache{ + CacheZoneName: "minuses", + CacheZoneSize: "10m", + CacheMinUses: intPtr(0), + }, + }, + }, + isPlus: false, + }, + { + name: "cache policy with invalid manager files (zero)", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + Cache: &v1.Cache{ + CacheZoneName: "managerbad", + CacheZoneSize: "10m", + Manager: &v1.CacheManager{ + Files: intPtr(0), + Sleep: "100ms", + Threshold: "500ms", + }, + }, + }, + }, + isPlus: false, + }, + { + name: "cache policy with invalid manager sleep format", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + Cache: &v1.Cache{ + CacheZoneName: "managersleep", + CacheZoneSize: "10m", + Manager: &v1.CacheManager{ + Files: intPtr(100), + Sleep: "invalid", + Threshold: "500ms", + }, + }, + }, + }, + isPlus: false, + }, + { + name: "cache policy with invalid manager threshold format", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + Cache: &v1.Cache{ + CacheZoneName: "managerthreshold", + CacheZoneSize: "10m", + Manager: &v1.CacheManager{ + Files: intPtr(100), + Sleep: "100ms", + Threshold: "bad-time", + }, + }, + }, + }, + isPlus: false, + }, + { + name: "cache policy with invalid lock timeout format", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + Cache: &v1.Cache{ + CacheZoneName: "locktimeout", + CacheZoneSize: "10m", + Lock: &v1.CacheLock{ + Enable: true, + Timeout: "invalid-timeout", + }, + }, + }, + }, + isPlus: false, + }, + // Note: Command execution pattern validation has been moved to CRD level in types.go + // This test case is no longer valid as the validation happens at Kubernetes API level + { + name: "cache policy with invalid inactive format", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + Cache: &v1.Cache{ + CacheZoneName: "inactive", + CacheZoneSize: "10m", + Inactive: "bad-duration", + }, + }, + }, + isPlus: false, + }, + { + name: "cache policy with invalid max size format", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + Cache: &v1.Cache{ + CacheZoneName: "maxsize", + CacheZoneSize: "10m", + MaxSize: "invalid-size", + }, + }, + }, + isPlus: false, + }, + // Note: Command execution pattern validation has been moved to CRD level in types.go + // These test cases are no longer valid as the validation happens at Kubernetes API level + { + name: "cache key with unmatched braces", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + Cache: &v1.Cache{ + CacheZoneName: "braces", + CacheZoneSize: "10m", + CacheKey: "${request_uri{malformed", + }, + }, + }, + isPlus: false, + }, } for _, tc := range tt { @@ -2772,6 +2897,101 @@ func TestValidatePolicy_IsValidCachePolicy(t *testing.T) { }, isPlus: false, }, + { + name: "cache policy with extended cache key configuration", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + Cache: &v1.Cache{ + CacheZoneName: "extended", + CacheZoneSize: "20m", + CacheKey: "${scheme}${host}${request_uri}${args}", + CacheMinUses: intPtr(5), + }, + }, + }, + isPlus: false, + }, + { + name: "cache policy with full manager configuration", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + Cache: &v1.Cache{ + CacheZoneName: "managercache", + CacheZoneSize: "30m", + Manager: &v1.CacheManager{ + Files: intPtr(200), + Sleep: "100ms", + Threshold: "500ms", + }, + }, + }, + }, + isPlus: false, + }, + { + name: "cache policy with lock configuration", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + Cache: &v1.Cache{ + CacheZoneName: "lockcache", + CacheZoneSize: "15m", + Lock: &v1.CacheLock{ + Enable: true, + Timeout: "30s", + }, + }, + }, + }, + isPlus: false, + }, + { + name: "cache policy with conditions configuration", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + Cache: &v1.Cache{ + CacheZoneName: "conditioncache", + CacheZoneSize: "25m", + Conditions: &v1.CacheConditions{ + NoCache: []string{"$cookie_nocache", "$arg_nocache"}, + Bypass: []string{"$http_pragma", "$http_authorization"}, + }, + }, + }, + }, + isPlus: false, + }, + { + name: "cache policy with all extended fields", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + Cache: &v1.Cache{ + CacheZoneName: "fullextended", + CacheZoneSize: "100m", + CacheKey: "${scheme}${host}${request_uri}", + CacheMinUses: intPtr(3), + UseTempPath: false, + MaxSize: "2g", + Inactive: "7d", + Manager: &v1.CacheManager{ + Files: intPtr(500), + Sleep: "200ms", + Threshold: "1s", + }, + Lock: &v1.CacheLock{ + Enable: true, + Timeout: "60s", + }, + Conditions: &v1.CacheConditions{ + NoCache: []string{"$cookie_admin"}, + Bypass: []string{"$http_cache_control"}, + }, + CacheBackgroundUpdate: true, + CacheRevalidate: true, + }, + }, + }, + isPlus: false, + }, } for _, tc := range tt { diff --git a/pkg/apis/configuration/validation/virtualserver.go b/pkg/apis/configuration/validation/virtualserver.go index 911051e154..4a5be0d53f 100644 --- a/pkg/apis/configuration/validation/virtualserver.go +++ b/pkg/apis/configuration/validation/virtualserver.go @@ -1171,7 +1171,6 @@ func validateActionProxyRewritePathForRegexp(rewritePath string, fieldPath *fiel return allErrs } -// TODO: what variable should be allowed for cache key and should this list be reused var actionProxyHeaderVariables = map[string]bool{ "request_uri": true, "request_method": true, From 394f019ca730a69fbbe18e05c25302410ae372c7 Mon Sep 17 00:00:00 2001 From: Venktesh Shivam Patel Date: Wed, 26 Nov 2025 16:24:05 +0000 Subject: [PATCH 03/16] remove redundant default assignment --- examples/custom-resources/cache-policy/cache.yaml | 2 -- internal/configs/virtualserver.go | 6 ------ 2 files changed, 8 deletions(-) diff --git a/examples/custom-resources/cache-policy/cache.yaml b/examples/custom-resources/cache-policy/cache.yaml index 25ea7ee960..aee1a286bb 100644 --- a/examples/custom-resources/cache-policy/cache.yaml +++ b/examples/custom-resources/cache-policy/cache.yaml @@ -23,8 +23,6 @@ spec: # Advanced cache behavior settings cacheKey: "${scheme}${request_method}${host}${request_uri}" # Optional - proxy_cache_key (custom cache key) -# TODO: non support variables -# cacheKey: " ${scheme}${proxy_host}${uri}?&{args}" cacheUseStale: [ "error", "timeout", "updating", "http_500" ] # Optional - proxy_cache_use_stale (serve stale conditions) cacheRevalidate: true # Optional, false by default - proxy_cache_revalidate cacheBackgroundUpdate: true # Optional, false by default - proxy_cache_background_update diff --git a/internal/configs/virtualserver.go b/internal/configs/virtualserver.go index 371aaf4007..5c656ecf69 100644 --- a/internal/configs/virtualserver.go +++ b/internal/configs/virtualserver.go @@ -1990,13 +1990,7 @@ func generateCacheConfig(cache *conf_v1.Cache, vsNamespace, vsName, ownerNamespa uniqueZoneName = fmt.Sprintf("%s_%s_%s_%s_%s", vsNamespace, vsName, ownerNamespace, ownerName, cache.CacheZoneName) } - // Set cache key with default if not provided var cacheKey string - if cache.CacheKey != "" { - cacheKey = cache.CacheKey - } else { - cacheKey = "${scheme}${proxy_host}${request_uri}" - } cacheConfig := &version2.Cache{ ZoneName: uniqueZoneName, From ba7e6d5c7cf7ba1d5b84dd2533e9233067d89c41 Mon Sep 17 00:00:00 2001 From: Venktesh Shivam Patel Date: Wed, 26 Nov 2025 16:38:37 +0000 Subject: [PATCH 04/16] add tests for cacheUseStale --- .../configuration/validation/policy_test.go | 73 +++++++++++++++++-- .../configuration/validation/virtualserver.go | 1 - 2 files changed, 68 insertions(+), 6 deletions(-) diff --git a/pkg/apis/configuration/validation/policy_test.go b/pkg/apis/configuration/validation/policy_test.go index 1985de1712..37e3457df2 100644 --- a/pkg/apis/configuration/validation/policy_test.go +++ b/pkg/apis/configuration/validation/policy_test.go @@ -2753,16 +2753,27 @@ func TestValidatePolicy_IsNotValidCachePolicy(t *testing.T) { }, isPlus: false, }, - // Note: Command execution pattern validation has been moved to CRD level in types.go - // These test cases are no longer valid as the validation happens at Kubernetes API level { - name: "cache key with unmatched braces", + name: "cache policy with invalid cacheUseStale parameter", policy: &v1.Policy{ Spec: v1.PolicySpec{ Cache: &v1.Cache{ - CacheZoneName: "braces", + CacheZoneName: "invalidstaleparameter", CacheZoneSize: "10m", - CacheKey: "${request_uri{malformed", + CacheUseStale: []string{"error", "invalid_param", "timeout"}, + }, + }, + }, + isPlus: false, + }, + { + name: "cache policy with duplicate cacheUseStale parameters", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + Cache: &v1.Cache{ + CacheZoneName: "duplicatestale", + CacheZoneSize: "10m", + CacheUseStale: []string{"error", "timeout", "error"}, }, }, }, @@ -2992,6 +3003,58 @@ func TestValidatePolicy_IsValidCachePolicy(t *testing.T) { }, isPlus: false, }, + { + name: "cache policy with valid cacheUseStale parameters", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + Cache: &v1.Cache{ + CacheZoneName: "validstale", + CacheZoneSize: "10m", + CacheUseStale: []string{"error", "timeout", "http_502"}, + }, + }, + }, + isPlus: false, + }, + { + name: "cache policy with updating parameter (cache specific)", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + Cache: &v1.Cache{ + CacheZoneName: "staleupdate", + CacheZoneSize: "10m", + CacheUseStale: []string{"error", "timeout", "updating"}, + }, + }, + }, + isPlus: false, + }, + { + name: "cache policy with all valid cacheUseStale parameters", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + Cache: &v1.Cache{ + CacheZoneName: "stallall", + CacheZoneSize: "10m", + CacheUseStale: []string{"error", "timeout", "invalid_header", "updating", "http_500", "http_502", "http_503", "http_504"}, + }, + }, + }, + isPlus: false, + }, + { + name: "cache policy with empty cacheUseStale (should be valid)", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + Cache: &v1.Cache{ + CacheZoneName: "emptystale", + CacheZoneSize: "10m", + CacheUseStale: []string{}, + }, + }, + }, + isPlus: false, + }, } for _, tc := range tt { diff --git a/pkg/apis/configuration/validation/virtualserver.go b/pkg/apis/configuration/validation/virtualserver.go index 4a5be0d53f..49c4ce0fc6 100644 --- a/pkg/apis/configuration/validation/virtualserver.go +++ b/pkg/apis/configuration/validation/virtualserver.go @@ -1278,7 +1278,6 @@ func (vsv *VirtualServerValidator) validateActionProxyResponseHeaders(responseHe return allErrs } -// TODO: should it be customisable for cache var validIgnoreHeaders = map[string]bool{ "X-Accel-Redirect": true, "X-Accel-Expires": true, From 59acac849d0065f35b5d8fb5e1af8ab5baca49a7 Mon Sep 17 00:00:00 2001 From: Venktesh Shivam Patel Date: Wed, 26 Nov 2025 17:07:47 +0000 Subject: [PATCH 05/16] move cacheKey default logic to VS --- .../configs/version2/nginx-plus.virtualserver.tmpl | 8 -------- internal/configs/version2/nginx.virtualserver.tmpl | 8 -------- internal/configs/version2/templates_test.go | 4 ++++ internal/configs/virtualserver.go | 6 ++++++ internal/configs/virtualserver_test.go | 14 +++++++------- pkg/apis/configuration/v1/types.go | 4 ---- 6 files changed, 17 insertions(+), 27 deletions(-) diff --git a/internal/configs/version2/nginx-plus.virtualserver.tmpl b/internal/configs/version2/nginx-plus.virtualserver.tmpl index d260fccec9..b406334be0 100644 --- a/internal/configs/version2/nginx-plus.virtualserver.tmpl +++ b/internal/configs/version2/nginx-plus.virtualserver.tmpl @@ -230,11 +230,7 @@ server { {{- with $s.Cache }} # Server-level cache configuration proxy_cache {{ $s.Cache.ZoneName }}; - {{- if $s.Cache.CacheKey }} proxy_cache_key {{ $s.Cache.CacheKey }}; - {{- else }} - proxy_cache_key $scheme$proxy_host$request_uri; - {{- end }} {{- if $s.Cache.OverrideUpstreamCache }} proxy_ignore_headers Cache-Control Expires Set-Cookie Vary X-Accel-Expires; {{- end }} @@ -786,11 +782,7 @@ server { {{- with $l.Cache }} proxy_cache {{ $l.Cache.ZoneName }}; - {{- if $l.Cache.CacheKey }} proxy_cache_key {{ $l.Cache.CacheKey }}; - {{- else }} - proxy_cache_key $scheme$proxy_host$request_uri; - {{- end }} {{- if $l.Cache.OverrideUpstreamCache }} proxy_ignore_headers Cache-Control Expires Set-Cookie Vary X-Accel-Expires; {{- end }} diff --git a/internal/configs/version2/nginx.virtualserver.tmpl b/internal/configs/version2/nginx.virtualserver.tmpl index 4c66ae40ab..42dcb691ac 100644 --- a/internal/configs/version2/nginx.virtualserver.tmpl +++ b/internal/configs/version2/nginx.virtualserver.tmpl @@ -121,11 +121,7 @@ server { {{- with $s.Cache }} # Server-level cache configuration proxy_cache {{ $s.Cache.ZoneName }}; - {{- if $s.Cache.CacheKey }} proxy_cache_key {{ $s.Cache.CacheKey }}; - {{- else }} - proxy_cache_key $scheme$proxy_host$request_uri; - {{- end }} {{- if $s.Cache.OverrideUpstreamCache }} proxy_ignore_headers Cache-Control Expires Set-Cookie Vary X-Accel-Expires; {{- end }} @@ -480,11 +476,7 @@ server { {{- with $l.Cache }} proxy_cache {{ $l.Cache.ZoneName }}; - {{- if $l.Cache.CacheKey }} proxy_cache_key {{ $l.Cache.CacheKey }}; - {{- else }} - proxy_cache_key $scheme$proxy_host$request_uri; - {{- end }} {{- if $l.Cache.OverrideUpstreamCache }} proxy_ignore_headers Cache-Control Expires Set-Cookie Vary X-Accel-Expires; {{- end }} diff --git a/internal/configs/version2/templates_test.go b/internal/configs/version2/templates_test.go index f47098a533..be42c06c3a 100644 --- a/internal/configs/version2/templates_test.go +++ b/internal/configs/version2/templates_test.go @@ -2948,6 +2948,7 @@ var ( CachePurgeAllow: []string{"127.0.0.1", "10.0.0.0/8", "192.168.1.0/24"}, OverrideUpstreamCache: true, Levels: "2:2", + CacheKey: "$scheme$proxy_host$request_uri", }, Locations: []Location{ { @@ -2963,6 +2964,7 @@ var ( CachePurgeAllow: nil, OverrideUpstreamCache: false, Levels: "", + CacheKey: "$scheme$proxy_host$request_uri", }, }, }, @@ -3008,6 +3010,7 @@ var ( CachePurgeAllow: []string{"127.0.0.1"}, // This should be ignored for OSS OverrideUpstreamCache: true, Levels: "1:2", + CacheKey: "$scheme$proxy_host$request_uri", }, Locations: []Location{ { @@ -3023,6 +3026,7 @@ var ( CachePurgeAllow: nil, OverrideUpstreamCache: false, Levels: "", + CacheKey: "$scheme$proxy_host$request_uri", }, }, }, diff --git a/internal/configs/virtualserver.go b/internal/configs/virtualserver.go index 5c656ecf69..929c472838 100644 --- a/internal/configs/virtualserver.go +++ b/internal/configs/virtualserver.go @@ -1990,7 +1990,13 @@ func generateCacheConfig(cache *conf_v1.Cache, vsNamespace, vsName, ownerNamespa uniqueZoneName = fmt.Sprintf("%s_%s_%s_%s_%s", vsNamespace, vsName, ownerNamespace, ownerName, cache.CacheZoneName) } + // Set cache key with default if not provided var cacheKey string + if cache.CacheKey != "" { + cacheKey = cache.CacheKey + } else { + cacheKey = "$scheme$proxy_host$request_uri" + } cacheConfig := &version2.Cache{ ZoneName: uniqueZoneName, diff --git a/internal/configs/virtualserver_test.go b/internal/configs/virtualserver_test.go index 4cd3e53e67..27580399a3 100644 --- a/internal/configs/virtualserver_test.go +++ b/internal/configs/virtualserver_test.go @@ -11691,7 +11691,7 @@ func TestGenerateVirtualServerConfigCache(t *testing.T) { CachePurgeAllow: nil, OverrideUpstreamCache: false, Levels: "", - CacheKey: "${scheme}${proxy_host}${request_uri}", + CacheKey: "$scheme$proxy_host$request_uri", }, Locations: []version2.Location{ { @@ -11858,7 +11858,7 @@ func TestGenerateVirtualServerConfigCache(t *testing.T) { CachePurgeAllow: nil, OverrideUpstreamCache: false, Levels: "", - CacheKey: "${scheme}${proxy_host}${request_uri}", + CacheKey: "$scheme$proxy_host$request_uri", }, }, { @@ -13276,7 +13276,7 @@ func TestGeneratePolicies(t *testing.T) { ZoneName: "default_test_basic-cache", ZoneSize: "10m", Valid: map[string]string{}, - CacheKey: "${scheme}${proxy_host}${request_uri}", + CacheKey: "$scheme$proxy_host$request_uri", }, }, msg: "basic cache policy reference", @@ -13401,7 +13401,7 @@ func TestGeneratePolicies(t *testing.T) { "301": "30m", "404": "30m", }, - CacheKey: "${scheme}${proxy_host}${request_uri}", + CacheKey: "$scheme$proxy_host$request_uri", }, }, msg: "cache policy with specific status codes", @@ -13437,7 +13437,7 @@ func TestGeneratePolicies(t *testing.T) { Valid: map[string]string{}, AllowedMethods: []string{"GET", "HEAD"}, Levels: "2:2", - CacheKey: "${scheme}${proxy_host}${request_uri}", + CacheKey: "$scheme$proxy_host$request_uri", }, }, msg: "cache policy with allowed methods and levels", @@ -13471,7 +13471,7 @@ func TestGeneratePolicies(t *testing.T) { ZoneSize: "75m", Valid: map[string]string{}, CachePurgeAllow: []string{"192.168.1.0/24", "10.0.0.1"}, - CacheKey: "${scheme}${proxy_host}${request_uri}", + CacheKey: "$scheme$proxy_host$request_uri", }, }, msg: "cache policy with purge allow IPs", @@ -13504,7 +13504,7 @@ func TestGeneratePolicies(t *testing.T) { ZoneSize: "15m", Time: "45m", Valid: map[string]string{}, - CacheKey: "${scheme}${proxy_host}${request_uri}", + CacheKey: "$scheme$proxy_host$request_uri", }, }, msg: "implicit cache policy reference", diff --git a/pkg/apis/configuration/v1/types.go b/pkg/apis/configuration/v1/types.go index 5449830c38..9932e9dbf6 100644 --- a/pkg/apis/configuration/v1/types.go +++ b/pkg/apis/configuration/v1/types.go @@ -1070,16 +1070,12 @@ type CacheLock struct { // These use NGINX variables to make dynamic caching decisions based on request characteristics. type CacheConditions struct { // +kubebuilder:validation:Optional - // +kubebuilder:validation:XValidation:rule="self.all(item, !item.contains('$(') && !item.contains('`') && !item.contains(';') && !item.contains('&&') && !item.contains('||'))",message="cache conditions must not contain command execution patterns: $(, `, ;, &&, ||" // NoCache defines conditions under which the response will not be saved to a cache (proxy_no_cache). // If at least one value of the string parameters is not empty and is not equal to "0" then the response will not be saved. - // Must not contain command execution patterns: $(, `, ;, &&, || NoCache []string `json:"noCache,omitempty"` // +kubebuilder:validation:Optional - // +kubebuilder:validation:XValidation:rule="self.all(item, !item.contains('$(') && !item.contains('`') && !item.contains(';') && !item.contains('&&') && !item.contains('||'))",message="cache conditions must not contain command execution patterns: $(, `, ;, &&, ||" // Bypass defines conditions under which the response will not be taken from a cache (proxy_cache_bypass). // If at least one value of the string parameters is not empty and is not equal to "0" then the response will not be taken from the cache. - // Must not contain command execution patterns: $(, `, ;, &&, || Bypass []string `json:"bypass,omitempty"` } From f21dd114e6b723095de5c5ef88c3a6d3d9d43f5f Mon Sep 17 00:00:00 2001 From: Venktesh Shivam Patel Date: Thu, 27 Nov 2025 11:44:26 +0000 Subject: [PATCH 06/16] remove vars from list --- pkg/apis/configuration/validation/policy_test.go | 4 ---- pkg/apis/configuration/validation/virtualserver.go | 2 -- 2 files changed, 6 deletions(-) diff --git a/pkg/apis/configuration/validation/policy_test.go b/pkg/apis/configuration/validation/policy_test.go index 37e3457df2..24356c48a4 100644 --- a/pkg/apis/configuration/validation/policy_test.go +++ b/pkg/apis/configuration/validation/policy_test.go @@ -2643,8 +2643,6 @@ func TestValidatePolicy_IsNotValidCachePolicy(t *testing.T) { }, isPlus: false, }, - // Note: Command execution pattern validation has been moved to CRD level in types.go - // This test case is no longer valid as the validation happens at Kubernetes API level { name: "cache policy with invalid minUses (zero)", policy: &v1.Policy{ @@ -2725,8 +2723,6 @@ func TestValidatePolicy_IsNotValidCachePolicy(t *testing.T) { }, isPlus: false, }, - // Note: Command execution pattern validation has been moved to CRD level in types.go - // This test case is no longer valid as the validation happens at Kubernetes API level { name: "cache policy with invalid inactive format", policy: &v1.Policy{ diff --git a/pkg/apis/configuration/validation/virtualserver.go b/pkg/apis/configuration/validation/virtualserver.go index 49c4ce0fc6..e5edfff294 100644 --- a/pkg/apis/configuration/validation/virtualserver.go +++ b/pkg/apis/configuration/validation/virtualserver.go @@ -1216,8 +1216,6 @@ var actionProxyHeaderVariables = map[string]bool{ "ssl_server_name": true, "ssl_session_id": true, "ssl_session_reused": true, - "uri": true, - "proxy_host": true, } var actionProxyHeaderSpecialVariables = []string{"arg_", "http_", "cookie_", "jwt_claim_", "jwt_header_"} From f159ca6271e823a424efc9e9544a337db42c2f06 Mon Sep 17 00:00:00 2001 From: Venktesh Shivam Patel Date: Thu, 27 Nov 2025 12:26:45 +0000 Subject: [PATCH 07/16] allow braced and unbraced vars in cacheKey --- pkg/apis/configuration/validation/policy.go | 7 +++- .../configuration/validation/policy_test.go | 41 +++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/pkg/apis/configuration/validation/policy.go b/pkg/apis/configuration/validation/policy.go index cea3b49c41..87bd1b272e 100644 --- a/pkg/apis/configuration/validation/policy.go +++ b/pkg/apis/configuration/validation/policy.go @@ -491,8 +491,11 @@ func validateCache(cache *v1.Cache, fieldPath *field.Path, isPlus bool) field.Er if err := ValidateEscapedString(cache.CacheKey); err != nil { allErrs = append(allErrs, field.Invalid(fieldPath.Child("cacheKey"), cache.CacheKey, err.Error())) } - // Validate that cache key contains valid NGINX variables - allErrs = append(allErrs, validateStringWithVariables(cache.CacheKey, fieldPath.Child("cacheKey"), actionProxyHeaderSpecialVariables, actionProxyHeaderVariables, false)...) + // Cache keys support both ${var} and $var NGINX syntax, so we skip variable braces validation + // but still validate basic syntax rules like not ending with $ + if strings.HasSuffix(cache.CacheKey, "$") { + allErrs = append(allErrs, field.Invalid(fieldPath.Child("cacheKey"), cache.CacheKey, "must not end with $")) + } } // Validate conditions diff --git a/pkg/apis/configuration/validation/policy_test.go b/pkg/apis/configuration/validation/policy_test.go index 24356c48a4..fc47d18c01 100644 --- a/pkg/apis/configuration/validation/policy_test.go +++ b/pkg/apis/configuration/validation/policy_test.go @@ -2775,6 +2775,19 @@ func TestValidatePolicy_IsNotValidCachePolicy(t *testing.T) { }, isPlus: false, }, + { + name: "cache policy with invalid cache key ending with $", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + Cache: &v1.Cache{ + CacheZoneName: "invalidkey", + CacheZoneSize: "10m", + CacheKey: "$scheme$host$request_uri$", // Invalid: ends with $ + }, + }, + }, + isPlus: false, + }, } for _, tc := range tt { @@ -3051,6 +3064,34 @@ func TestValidatePolicy_IsValidCachePolicy(t *testing.T) { }, isPlus: false, }, + { + name: "cache policy with unbraced cache key variables", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + Cache: &v1.Cache{ + CacheZoneName: "unbraced", + CacheZoneSize: "10m", + CacheKey: "$scheme$host$request_uri", // Test unbraced NGINX variable format + Time: "15m", + }, + }, + }, + isPlus: false, + }, + { + name: "cache policy with mixed braced and unbraced cache key variables", + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + Cache: &v1.Cache{ + CacheZoneName: "mixed", + CacheZoneSize: "10m", + CacheKey: "$scheme${host}$request_uri", // Test mixed format + Time: "20m", + }, + }, + }, + isPlus: false, + }, } for _, tc := range tt { From bfdf3600b6a9a09a8f8ca3aa768b24c31a39fd40 Mon Sep 17 00:00:00 2001 From: Venktesh Shivam Patel Date: Thu, 27 Nov 2025 12:43:47 +0000 Subject: [PATCH 08/16] update python test data to include extended spec --- .../custom-resources/cache-policy/cache.yaml | 4 ++-- .../policies/cache-policy-advanced.yaml | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/examples/custom-resources/cache-policy/cache.yaml b/examples/custom-resources/cache-policy/cache.yaml index aee1a286bb..499a181df4 100644 --- a/examples/custom-resources/cache-policy/cache.yaml +++ b/examples/custom-resources/cache-policy/cache.yaml @@ -9,7 +9,7 @@ spec: allowedCodes: ["any"] # Optional ["any"] or [200, 301, ...], "any" cannot be combined with specific codes allowedMethods: ["GET", "HEAD", "POST"] # Optional time: "30m" # Optional # e.g. "15m", "1h", "2d". Default is "10m" - overrideUpstreamCache: true # Optional, default is false + overrideUpstreamCache: true # Optional, default is false - whether to respect upstream cache-control headers (Cache-Control Expires Set-Cookie Vary X-Accel-Expires) # levels: "1:2" # Optional, default is "1:1" , see https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_cache_path for more details # cachePurgeAllow: [""] # Optional, If set allows cache purging from the specified IPs or CIDR ranges. Nginx Plus only. inactive: "60m" # Optional - proxy_cache_path (inactive parameter) @@ -22,7 +22,7 @@ spec: threshold: "200ms" # manager_threshold # Advanced cache behavior settings - cacheKey: "${scheme}${request_method}${host}${request_uri}" # Optional - proxy_cache_key (custom cache key) + cacheKey: "$scheme$host$request_uri$request_method" # Optional - proxy_cache_key (custom cache key) cacheUseStale: [ "error", "timeout", "updating", "http_500" ] # Optional - proxy_cache_use_stale (serve stale conditions) cacheRevalidate: true # Optional, false by default - proxy_cache_revalidate cacheBackgroundUpdate: true # Optional, false by default - proxy_cache_background_update diff --git a/tests/data/cache-policy/policies/cache-policy-advanced.yaml b/tests/data/cache-policy/policies/cache-policy-advanced.yaml index 2e7da75f9e..4d5193d0e2 100644 --- a/tests/data/cache-policy/policies/cache-policy-advanced.yaml +++ b/tests/data/cache-policy/policies/cache-policy-advanced.yaml @@ -11,3 +11,26 @@ spec: time: "2h" overrideUpstreamCache: true levels: "2:2" + inactive: "60m" + useTempPath: false + maxSize: "10g" + minFree: "1g" + manager: + files: 100 + sleep: "50ms" + threshold: "200ms" + + cacheKey: "$scheme$host$request_uri$request_method" + cacheUseStale: [ "error", "timeout", "updating", "http_500" ] + cacheRevalidate: true + cacheBackgroundUpdate: true + cacheMinUses: 1 + + lock: + enable: true + timeout: "5s" + age: "30s" + + conditions: + noCache: [ "$cookie_nocache", "$arg_nocache" ] + bypass: [ "$http_authorization" ] From d2dc3d94c085897fe03cd059912fb182a3bb338d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 12:44:47 +0000 Subject: [PATCH 09/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../policies/cache-policy-advanced.yaml | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/data/cache-policy/policies/cache-policy-advanced.yaml b/tests/data/cache-policy/policies/cache-policy-advanced.yaml index 4d5193d0e2..f67c9cf7da 100644 --- a/tests/data/cache-policy/policies/cache-policy-advanced.yaml +++ b/tests/data/cache-policy/policies/cache-policy-advanced.yaml @@ -11,26 +11,26 @@ spec: time: "2h" overrideUpstreamCache: true levels: "2:2" - inactive: "60m" - useTempPath: false + inactive: "60m" + useTempPath: false maxSize: "10g" minFree: "1g" - manager: + manager: files: 100 sleep: "50ms" threshold: "200ms" - cacheKey: "$scheme$host$request_uri$request_method" - cacheUseStale: [ "error", "timeout", "updating", "http_500" ] - cacheRevalidate: true - cacheBackgroundUpdate: true - cacheMinUses: 1 + cacheKey: "$scheme$host$request_uri$request_method" + cacheUseStale: [ "error", "timeout", "updating", "http_500" ] + cacheRevalidate: true + cacheBackgroundUpdate: true + cacheMinUses: 1 - lock: + lock: enable: true timeout: "5s" - age: "30s" + age: "30s" - conditions: + conditions: noCache: [ "$cookie_nocache", "$arg_nocache" ] bypass: [ "$http_authorization" ] From 5c899d82b00396456fa8b78ca0ea4a8c8f1e2331 Mon Sep 17 00:00:00 2001 From: Venktesh Shivam Patel Date: Thu, 27 Nov 2025 13:04:32 +0000 Subject: [PATCH 10/16] update python test assertion to account for in key --- tests/suite/test_cache_policies_vs.py | 9 +++++---- tests/suite/test_cache_policies_vsr.py | 9 +++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/suite/test_cache_policies_vs.py b/tests/suite/test_cache_policies_vs.py index 4ac7240265..10f867c934 100644 --- a/tests/suite/test_cache_policies_vs.py +++ b/tests/suite/test_cache_policies_vs.py @@ -179,11 +179,12 @@ def test_cache_policy_advanced( "Request ID:" in resp_1.text, "Request ID:" in resp_2.text, "Request ID:" in resp_3.text, - req_id_1.group(1) == req_id_2.group(1) == req_id_3.group(1), + req_id_1.group(1) == req_id_2.group(1), # GET requests should share cache + req_id_1.group(1) != req_id_3.group(1), # POST has different cache key due to $request_method cache_status_1 in ["MISS", "EXPIRED", None], - cache_status_2 == "HIT", - cache_status_3 == "HIT", - cache_status_4 == "HIT", + cache_status_2 == "HIT", # Second GET should be cache hit + cache_status_3 in ["MISS", "EXPIRED", None], # First POST should be cache miss (different key) + cache_status_4 == "HIT", # HEAD should be cache hit (shares key with GET) ] ) diff --git a/tests/suite/test_cache_policies_vsr.py b/tests/suite/test_cache_policies_vsr.py index 1947a00775..85827d4560 100644 --- a/tests/suite/test_cache_policies_vsr.py +++ b/tests/suite/test_cache_policies_vsr.py @@ -197,11 +197,12 @@ def test_cache_policy_vsr_advanced( "Request ID:" in resp_1.text, "Request ID:" in resp_2.text, "Request ID:" in resp_3.text, - req_id_1.group(1) == req_id_2.group(1) == req_id_3.group(1), + req_id_1.group(1) == req_id_2.group(1), # GET requests should share cache + req_id_1.group(1) != req_id_3.group(1), # POST has different cache key due to $request_method cache_status_1 in ["MISS", "EXPIRED", None], - cache_status_2 == "HIT", - cache_status_3 == "HIT", - cache_status_4 == "HIT", + cache_status_2 == "HIT", # Second GET should be cache hit + cache_status_3 in ["MISS", "EXPIRED", None], # First POST should be cache miss + cache_status_4 == "HIT", # HEAD should be cache hit ] ) From d0a6a754690b6f4569b4e406880ca0b1f399931e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 13:06:01 +0000 Subject: [PATCH 11/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/suite/test_cache_policies_vsr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/suite/test_cache_policies_vsr.py b/tests/suite/test_cache_policies_vsr.py index 85827d4560..54d701c94c 100644 --- a/tests/suite/test_cache_policies_vsr.py +++ b/tests/suite/test_cache_policies_vsr.py @@ -202,7 +202,7 @@ def test_cache_policy_vsr_advanced( cache_status_1 in ["MISS", "EXPIRED", None], cache_status_2 == "HIT", # Second GET should be cache hit cache_status_3 in ["MISS", "EXPIRED", None], # First POST should be cache miss - cache_status_4 == "HIT", # HEAD should be cache hit + cache_status_4 == "HIT", # HEAD should be cache hit ] ) From 6e60539bb4779922e40fc97de4c8212febcb70b2 Mon Sep 17 00:00:00 2001 From: Venktesh Shivam Patel Date: Thu, 27 Nov 2025 15:17:59 +0000 Subject: [PATCH 12/16] update python test assertion to account for request_method in cachekey --- tests/suite/test_cache_policies_vs.py | 8 ++++---- tests/suite/test_cache_policies_vsr.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/suite/test_cache_policies_vs.py b/tests/suite/test_cache_policies_vs.py index 10f867c934..37787323d3 100644 --- a/tests/suite/test_cache_policies_vs.py +++ b/tests/suite/test_cache_policies_vs.py @@ -157,11 +157,11 @@ def test_cache_policy_advanced( resp_2 = requests.get(virtual_server_setup.backend_1_url, headers={"host": virtual_server_setup.vs_host}) cache_status_2 = resp_2.headers.get("X-Cache-Status") - # Test cache behavior for POST requests + # Test cache behavior for POST requests (cached separately due to $request_method in cache key) resp_3 = requests.post(virtual_server_setup.backend_1_url, headers={"host": virtual_server_setup.vs_host}) cache_status_3 = resp_3.headers.get("X-Cache-Status") - # Test cache behavior for HEAD requests + # Test cache behavior for HEAD requests (cached separately due to $request_method in cache key) resp_4 = requests.head(virtual_server_setup.backend_1_url, headers={"host": virtual_server_setup.vs_host}) cache_status_4 = resp_4.headers.get("X-Cache-Status") @@ -180,11 +180,11 @@ def test_cache_policy_advanced( "Request ID:" in resp_2.text, "Request ID:" in resp_3.text, req_id_1.group(1) == req_id_2.group(1), # GET requests should share cache - req_id_1.group(1) != req_id_3.group(1), # POST has different cache key due to $request_method + req_id_1.group(1) != req_id_3.group(1), # POST should NOT share cache with GET due to $request_method in cacheKey cache_status_1 in ["MISS", "EXPIRED", None], cache_status_2 == "HIT", # Second GET should be cache hit cache_status_3 in ["MISS", "EXPIRED", None], # First POST should be cache miss (different key) - cache_status_4 == "HIT", # HEAD should be cache hit (shares key with GET) + cache_status_4 in ["MISS", "EXPIRED", None], # HEAD should also be cache miss (different method) ] ) diff --git a/tests/suite/test_cache_policies_vsr.py b/tests/suite/test_cache_policies_vsr.py index 54d701c94c..7b63431404 100644 --- a/tests/suite/test_cache_policies_vsr.py +++ b/tests/suite/test_cache_policies_vsr.py @@ -169,14 +169,14 @@ def test_cache_policy_vsr_advanced( cache_status_2 = resp_2.headers.get("X-Cache-Status") print(f"Cache status for second GET request: {cache_status_2}") - # Test cache behavior for POST requests (should be cached with advanced policy) + # Test cache behavior for POST requests (cached separately due to $request_method in cache key) resp_3 = requests.post( f"{req_url}{v_s_route_setup.route_m.paths[0]}", headers={"host": v_s_route_setup.vs_host} ) cache_status_3 = resp_3.headers.get("X-Cache-Status") print(f"Cache status for first POST request: {cache_status_3}") - # Test cache behavior for HEAD requests + # Test cache behavior for HEAD requests (cached separately due to $request_method in cache key) resp_4 = requests.head( f"{req_url}{v_s_route_setup.route_m.paths[0]}", headers={"host": v_s_route_setup.vs_host} ) @@ -202,7 +202,7 @@ def test_cache_policy_vsr_advanced( cache_status_1 in ["MISS", "EXPIRED", None], cache_status_2 == "HIT", # Second GET should be cache hit cache_status_3 in ["MISS", "EXPIRED", None], # First POST should be cache miss - cache_status_4 == "HIT", # HEAD should be cache hit + cache_status_4 in ["MISS", "EXPIRED", None], # HEAD should also be cache miss (different method) ] ) From 3daf6033cb8ce14b73e58c073b0f5fab8b1674a6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 15:18:30 +0000 Subject: [PATCH 13/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/suite/test_cache_policies_vs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/suite/test_cache_policies_vs.py b/tests/suite/test_cache_policies_vs.py index 37787323d3..ef5d7624cc 100644 --- a/tests/suite/test_cache_policies_vs.py +++ b/tests/suite/test_cache_policies_vs.py @@ -180,7 +180,8 @@ def test_cache_policy_advanced( "Request ID:" in resp_2.text, "Request ID:" in resp_3.text, req_id_1.group(1) == req_id_2.group(1), # GET requests should share cache - req_id_1.group(1) != req_id_3.group(1), # POST should NOT share cache with GET due to $request_method in cacheKey + req_id_1.group(1) + != req_id_3.group(1), # POST should NOT share cache with GET due to $request_method in cacheKey cache_status_1 in ["MISS", "EXPIRED", None], cache_status_2 == "HIT", # Second GET should be cache hit cache_status_3 in ["MISS", "EXPIRED", None], # First POST should be cache miss (different key) From eee7ce328f8e6c1dd66287c7777637df2e442ac1 Mon Sep 17 00:00:00 2001 From: Venktesh Shivam Patel Date: Thu, 27 Nov 2025 16:22:08 +0000 Subject: [PATCH 14/16] update CRD filed description --- config/crd/bases/k8s.nginx.org_policies.yaml | 2 +- deploy/crds.yaml | 2 +- docs/crd/k8s.nginx.org_policies.md | 2 +- pkg/apis/configuration/v1/types.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config/crd/bases/k8s.nginx.org_policies.yaml b/config/crd/bases/k8s.nginx.org_policies.yaml index df4fce6b2e..e55188b30e 100644 --- a/config/crd/bases/k8s.nginx.org_policies.yaml +++ b/config/crd/bases/k8s.nginx.org_policies.yaml @@ -196,7 +196,7 @@ spec: cacheZoneSize: description: |- CacheZoneSize defines the size of the cache zone. Must be a number followed by a size unit: - 'k' for kilobytes, 'm' for megabytes, or 'g' for gigabytes. + 'k' or 'K' for kilobytes, 'm' or 'M' for megabytes, or 'g' or 'G' for gigabytes. Examples: "10m", "1g", "512k". pattern: ^[0-9]+[kmgKMG]$ type: string diff --git a/deploy/crds.yaml b/deploy/crds.yaml index e280a491fc..93fc92efaa 100644 --- a/deploy/crds.yaml +++ b/deploy/crds.yaml @@ -367,7 +367,7 @@ spec: cacheZoneSize: description: |- CacheZoneSize defines the size of the cache zone. Must be a number followed by a size unit: - 'k' for kilobytes, 'm' for megabytes, or 'g' for gigabytes. + 'k' or 'K' for kilobytes, 'm' or 'M' for megabytes, or 'g' or 'G' for gigabytes. Examples: "10m", "1g", "512k". pattern: ^[0-9]+[kmgKMG]$ type: string diff --git a/docs/crd/k8s.nginx.org_policies.md b/docs/crd/k8s.nginx.org_policies.md index cb0c0e082b..80f846a0bc 100644 --- a/docs/crd/k8s.nginx.org_policies.md +++ b/docs/crd/k8s.nginx.org_policies.md @@ -36,7 +36,7 @@ The `.spec` object supports the following fields: | `cache.cacheRevalidate` | `boolean` | CacheRevalidate enables revalidation of expired cache items using conditional requests (proxy_cache_revalidate). Uses "If-Modified-Since" and "If-None-Match" header fields. | | `cache.cacheUseStale` | `array[string]` | CacheUseStale determines in which cases a stale cached response can be used (proxy_cache_use_stale). Valid parameters: error, timeout, invalid_header, updating, http_500, http_502, http_503, http_504, http_403, http_404, http_429, off. | | `cache.cacheZoneName` | `string` | CacheZoneName defines the name of the cache zone. Must start with a lowercase letter, followed by alphanumeric characters or underscores, and end with an alphanumeric character. Single lowercase letters are also allowed. Examples: "cache", "my_cache", "cache1". | -| `cache.cacheZoneSize` | `string` | CacheZoneSize defines the size of the cache zone. Must be a number followed by a size unit: 'k' for kilobytes, 'm' for megabytes, or 'g' for gigabytes. Examples: "10m", "1g", "512k". | +| `cache.cacheZoneSize` | `string` | CacheZoneSize defines the size of the cache zone. Must be a number followed by a size unit: 'k' or 'K' for kilobytes, 'm' or 'M' for megabytes, or 'g' or 'G' for gigabytes. Examples: "10m", "1g", "512k". | | `cache.conditions` | `object` | Conditions defines when responses should not be cached or taken from cache. | | `cache.conditions.bypass` | `array[string]` | Bypass defines conditions under which the response will not be taken from a cache (proxy_cache_bypass). If at least one value of the string parameters is not empty and is not equal to "0" then the response will not be taken from the cache. | | `cache.conditions.noCache` | `array[string]` | NoCache defines conditions under which the response will not be saved to a cache (proxy_no_cache). If at least one value of the string parameters is not empty and is not equal to "0" then the response will not be saved. | diff --git a/pkg/apis/configuration/v1/types.go b/pkg/apis/configuration/v1/types.go index 4e76a03a98..90376fe69b 100644 --- a/pkg/apis/configuration/v1/types.go +++ b/pkg/apis/configuration/v1/types.go @@ -1102,7 +1102,7 @@ type Cache struct { // +kubebuilder:validation:Required // +kubebuilder:validation:Pattern=`^[0-9]+[kmgKMG]$` // CacheZoneSize defines the size of the cache zone. Must be a number followed by a size unit: - // 'k' for kilobytes, 'm' for megabytes, or 'g' for gigabytes. + // 'k' or 'K' for kilobytes, 'm' or 'M' for megabytes, or 'g' or 'G' for gigabytes. // Examples: "10m", "1g", "512k". CacheZoneSize string `json:"cacheZoneSize"` // +kubebuilder:validation:Optional From 228b1257ab61812b323c27d1399a08e710858d0e Mon Sep 17 00:00:00 2001 From: Venktesh Shivam Patel Date: Fri, 28 Nov 2025 09:50:11 +0000 Subject: [PATCH 15/16] remove TODO --- pkg/apis/configuration/validation/policy.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/apis/configuration/validation/policy.go b/pkg/apis/configuration/validation/policy.go index 87bd1b272e..3e04f6037b 100644 --- a/pkg/apis/configuration/validation/policy.go +++ b/pkg/apis/configuration/validation/policy.go @@ -448,7 +448,6 @@ func validateCache(cache *v1.Cache, fieldPath *field.Path, isPlus bool) field.Er } // Validate size fields - // TODO: validate size with g|G properly if cache.MaxSize != "" { allErrs = append(allErrs, validateOffset(cache.MaxSize, fieldPath.Child("maxSize"))...) } From 36b8471e343589f9492bd3c05bf52ec1ead5134f Mon Sep 17 00:00:00 2001 From: Venktesh Shivam Patel Date: Fri, 28 Nov 2025 14:38:33 +0000 Subject: [PATCH 16/16] up Co-authored-by: Paul Abel <128620221+pdabelf5@users.noreply.github.com> Signed-off-by: Venktesh Shivam Patel --- internal/configs/virtualserver.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/internal/configs/virtualserver.go b/internal/configs/virtualserver.go index 830b3bb59c..be16b1f3fb 100644 --- a/internal/configs/virtualserver.go +++ b/internal/configs/virtualserver.go @@ -2037,11 +2037,9 @@ func generateCacheConfig(cache *conf_v1.Cache, vsNamespace, vsName, ownerNamespa } // Set cache key with default if not provided - var cacheKey string + cacheKey := "$scheme$proxy_host$request_uri" if cache.CacheKey != "" { cacheKey = cache.CacheKey - } else { - cacheKey = "$scheme$proxy_host$request_uri" } cacheConfig := &version2.Cache{