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,