diff --git a/docs/examples/affinity/cookie/README.md b/docs/examples/affinity/cookie/README.md index e66503dd751..4d963c0fd47 100644 --- a/docs/examples/affinity/cookie/README.md +++ b/docs/examples/affinity/cookie/README.md @@ -6,20 +6,21 @@ This example demonstrates how to achieve session affinity using cookies. Session affinity can be configured using the following annotations: -|Name|Description|Value| -| --- | --- | --- | -|nginx.ingress.kubernetes.io/affinity|Type of the affinity, set this to `cookie` to enable session affinity|string (NGINX only supports `cookie`)| -|nginx.ingress.kubernetes.io/affinity-mode|The affinity mode defines how sticky a session is. Use `balanced` to redistribute some sessions when scaling pods or `persistent` for maximum stickiness.|`balanced` (default) or `persistent`| -|nginx.ingress.kubernetes.io/affinity-canary-behavior|Defines session affinity behavior of canaries. By default the behavior is `sticky`, and canaries respect session affinity configuration. Set this to `legacy` to restore original canary behavior, when session affinity parameters were not respected.|`sticky` (default) or `legacy`| -|nginx.ingress.kubernetes.io/session-cookie-name|Name of the cookie that will be created|string (defaults to `INGRESSCOOKIE`)| -|nginx.ingress.kubernetes.io/session-cookie-secure|Set the cookie as secure regardless the protocol of the incoming request|`"true"` or `"false"`| -|nginx.ingress.kubernetes.io/session-cookie-path|Path that will be set on the cookie (required if your [Ingress paths][ingress-paths] use regular expressions)|string (defaults to the currently [matched path][ingress-paths])| -|nginx.ingress.kubernetes.io/session-cookie-domain|Domain that will be set on the cookie|string| -|nginx.ingress.kubernetes.io/session-cookie-samesite|`SameSite` attribute to apply to the cookie|Browser accepted values are `None`, `Lax`, and `Strict`| -|nginx.ingress.kubernetes.io/session-cookie-conditional-samesite-none|Will omit `SameSite=None` attribute for older browsers which reject the more-recently defined `SameSite=None` value|`"true"` or `"false"` -|nginx.ingress.kubernetes.io/session-cookie-max-age|Time until the cookie expires, corresponds to the `Max-Age` cookie directive|number of seconds| -|nginx.ingress.kubernetes.io/session-cookie-expires|Legacy version of the previous annotation for compatibility with older browsers, generates an `Expires` cookie directive by adding the seconds to the current date|number of seconds| -|nginx.ingress.kubernetes.io/session-cookie-change-on-failure|When set to `false` nginx ingress will send request to upstream pointed by sticky cookie even if previous attempt failed. When set to `true` and previous attempt failed, sticky cookie will be changed to point to another upstream.|`true` or `false` (defaults to `false`)| +| Name | Description |Value| +|----------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| --- | +| nginx.ingress.kubernetes.io/affinity | Type of the affinity, set this to `cookie` to enable session affinity |string (NGINX only supports `cookie`)| +| nginx.ingress.kubernetes.io/affinity-mode | The affinity mode defines how sticky a session is. Use `balanced` to redistribute some sessions when scaling pods or `persistent` for maximum stickiness. |`balanced` (default) or `persistent`| +| nginx.ingress.kubernetes.io/affinity-canary-behavior | Defines session affinity behavior of canaries. By default the behavior is `sticky`, and canaries respect session affinity configuration. Set this to `legacy` to restore original canary behavior, when session affinity parameters were not respected. |`sticky` (default) or `legacy`| +| nginx.ingress.kubernetes.io/session-cookie-name | Name of the cookie that will be created |string (defaults to `INGRESSCOOKIE`)| +| nginx.ingress.kubernetes.io/session-cookie-secure | Set the cookie as secure regardless the protocol of the incoming request |`"true"` or `"false"`| +| nginx.ingress.kubernetes.io/session-cookie-path | Path that will be set on the cookie (required if your [Ingress paths][ingress-paths] use regular expressions) |string (defaults to the currently [matched path][ingress-paths])| +| nginx.ingress.kubernetes.io/session-cookie-domain | Domain that will be set on the cookie |string| +| nginx.ingress.kubernetes.io/session-cookie-samesite | `SameSite` attribute to apply to the cookie |Browser accepted values are `None`, `Lax`, and `Strict`| +| nginx.ingress.kubernetes.io/session-cookie-conditional-samesite-none | Will omit `SameSite=None` attribute for older browsers which reject the more-recently defined `SameSite=None` value |`"true"` or `"false"`| +| nginx.ingress.kubernetes.io/session-cookie-partitioned | Will set `Partitioned` attribute on the cookie |`"true"` or `"false"` (defaults to false)| +| nginx.ingress.kubernetes.io/session-cookie-max-age | Time until the cookie expires, corresponds to the `Max-Age` cookie directive |number of seconds| +| nginx.ingress.kubernetes.io/session-cookie-expires | Legacy version of the previous annotation for compatibility with older browsers, generates an `Expires` cookie directive by adding the seconds to the current date |number of seconds| +| nginx.ingress.kubernetes.io/session-cookie-change-on-failure | When set to `false` nginx ingress will send request to upstream pointed by sticky cookie even if previous attempt failed. When set to `true` and previous attempt failed, sticky cookie will be changed to point to another upstream. |`true` or `false` (defaults to `false`)| You can create the [session affinity example Ingress](ingress.yaml) to test this: diff --git a/docs/examples/affinity/cookie/ingress-partitioned.yaml b/docs/examples/affinity/cookie/ingress-partitioned.yaml new file mode 100644 index 00000000000..837b6b6ef07 --- /dev/null +++ b/docs/examples/affinity/cookie/ingress-partitioned.yaml @@ -0,0 +1,49 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: cookie-partitioned + annotations: + nginx.ingress.kubernetes.io/affinity: "cookie" + nginx.ingress.kubernetes.io/session-cookie-name: "PARTITIONEDCOOKIENAME" + nginx.ingress.kubernetes.io/session-cookie-secure: "true" + nginx.ingress.kubernetes.io/session-cookie-expires: "172800" + nginx.ingress.kubernetes.io/session-cookie-max-age: "172800" + nginx.ingress.kubernetes.io/session-cookie-partitioned: "true" +spec: + ingressClassName: nginx + rules: + - host: stickyingress-partitioned.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: http-svc + port: + number: 80 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: cookie-partitioned-false + annotations: + nginx.ingress.kubernetes.io/affinity: "cookie" + nginx.ingress.kubernetes.io/session-cookie-name: "PARTITIONEDFALSECOOKIENAME" + nginx.ingress.kubernetes.io/session-cookie-secure: "true" + nginx.ingress.kubernetes.io/session-cookie-expires: "172800" + nginx.ingress.kubernetes.io/session-cookie-max-age: "172800" + nginx.ingress.kubernetes.io/session-cookie-partitioned: "false" +spec: + ingressClassName: nginx + rules: + - host: stickyingress-partitioned-false.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: http-svc + port: + number: 80 diff --git a/docs/user-guide/nginx-configuration/annotations.md b/docs/user-guide/nginx-configuration/annotations.md index 184c4993bed..7f75a7433a2 100755 --- a/docs/user-guide/nginx-configuration/annotations.md +++ b/docs/user-guide/nginx-configuration/annotations.md @@ -106,6 +106,7 @@ You can add these Kubernetes annotations to specific Ingress objects to customiz |[nginx.ingress.kubernetes.io/session-cookie-path](#cookie-affinity)|string| |[nginx.ingress.kubernetes.io/session-cookie-samesite](#cookie-affinity)|string|"None", "Lax" or "Strict"| |[nginx.ingress.kubernetes.io/session-cookie-secure](#cookie-affinity)|string| +|[nginx.ingress.kubernetes.io/session-cookie-partitioned](#cookie-affinity)|"true" or "false"| |[nginx.ingress.kubernetes.io/ssl-redirect](#server-side-https-enforcement-through-redirect)|"true" or "false"| |[nginx.ingress.kubernetes.io/ssl-passthrough](#ssl-passthrough)|"true" or "false"| |[nginx.ingress.kubernetes.io/stream-snippet](#stream-snippet)|string| @@ -196,6 +197,8 @@ Use `nginx.ingress.kubernetes.io/session-cookie-domain` to set the `Domain` attr Use `nginx.ingress.kubernetes.io/session-cookie-samesite` to apply a `SameSite` attribute to the sticky cookie. Browser accepted values are `None`, `Lax`, and `Strict`. Some browsers reject cookies with `SameSite=None`, including those created before the `SameSite=None` specification (e.g. Chrome 5X). Other browsers mistakenly treat `SameSite=None` cookies as `SameSite=Strict` (e.g. Safari running on OSX 14). To omit `SameSite=None` from browsers with these incompatibilities, add the annotation `nginx.ingress.kubernetes.io/session-cookie-conditional-samesite-none: "true"`. +Use `nginx.ingress.kubernetes.io/session-cookie-partitioned` to apply a `Partitioned` attribute to the sticky cookie. + Use `nginx.ingress.kubernetes.io/session-cookie-expires` to control the cookie expires, its value is a number of seconds until the cookie expires. Use `nginx.ingress.kubernetes.io/session-cookie-path` to control the cookie path when use-regex is set to true. diff --git a/images/nginx/rootfs/build.sh b/images/nginx/rootfs/build.sh index 98bb346fb48..f2a8d5ddecb 100755 --- a/images/nginx/rootfs/build.sh +++ b/images/nginx/rootfs/build.sh @@ -94,6 +94,7 @@ export LUA_RESTY_CORE=0.1.27 # Check for recent changes: https://github.com/utix/lua-resty-cookie/compare/9533f47...master export LUA_RESTY_COOKIE_VERSION=9533f479371663107b515590fc9daf00d61ebf11 +# TODO avif: wait for https://github.com/utix/lua-resty-cookie/issues/2 support # Check for recent changes: https://github.com/openresty/lua-resty-dns/compare/v0.22...master export LUA_RESTY_DNS=0.22 @@ -287,6 +288,7 @@ get_src a77b9de160d81712f2f442e1de8b78a5a7ef0d08f13430ff619f79235db974d4 \ get_src a404c790553617424d743b82a9f01feccd0d2930b306b370c665ca3b7c09ccb6 \ "https://github.com/utix/lua-resty-cookie/archive/$LUA_RESTY_COOKIE_VERSION.tar.gz" +# TODO avif: wait for https://github.com/utix/lua-resty-cookie/issues/2 support get_src 573184006b98ccee2594b0d134fa4d05e5d2afd5141cbad315051ccf7e9b6403 \ "https://github.com/openresty/lua-resty-lrucache/archive/v$LUA_RESTY_CACHE.tar.gz" diff --git a/internal/ingress/annotations/sessionaffinity/main.go b/internal/ingress/annotations/sessionaffinity/main.go index bee4a209432..67f72e8ffbb 100644 --- a/internal/ingress/annotations/sessionaffinity/main.go +++ b/internal/ingress/annotations/sessionaffinity/main.go @@ -61,6 +61,9 @@ const ( // This is used to control whether SameSite=None should be conditionally applied based on the User-Agent annotationAffinityCookieConditionalSameSiteNone = "session-cookie-conditional-samesite-none" + // This is used to set the Partitioned flag on the cookie + annotationAffinityCookiePartitioned = "session-cookie-partitioned" + // This is used to control the cookie change after request failure annotationAffinityCookieChangeOnFailure = "session-cookie-change-on-failure" @@ -141,6 +144,12 @@ var sessionAffinityAnnotations = parser.Annotation{ Risk: parser.AnnotationRiskLow, Documentation: `This annotation is used to omit SameSite=None from browsers with SameSite attribute incompatibilities`, }, + annotationAffinityCookiePartitioned: { + Validator: parser.ValidateBool, + Scope: parser.AnnotationScopeIngress, + Risk: parser.AnnotationRiskLow, + Documentation: `This annotation sets the cookie as Partitioned`, + }, annotationAffinityCookieChangeOnFailure: { Validator: parser.ValidateBool, Scope: parser.AnnotationScopeIngress, @@ -184,6 +193,8 @@ type Cookie struct { SameSite string `json:"samesite"` // Flag that conditionally applies SameSite=None attribute on cookie if user agent accepts it. ConditionalSameSiteNone bool `json:"conditional-samesite-none"` + // Partitioned flag to be set + Partitioned bool `json:"partitioned"` } type affinity struct { @@ -241,6 +252,11 @@ func (a affinity) cookieAffinityParse(ing *networking.Ingress) *Cookie { klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookieConditionalSameSiteNone) } + cookie.Partitioned, err = parser.GetBoolAnnotation(annotationAffinityCookiePartitioned, ing, a.annotationConfig.Annotations) + if err != nil { + klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookiePartitioned) + } + cookie.ChangeOnFailure, err = parser.GetBoolAnnotation(annotationAffinityCookieChangeOnFailure, ing, a.annotationConfig.Annotations) if err != nil { klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookieChangeOnFailure) diff --git a/internal/ingress/annotations/sessionaffinity/main_test.go b/internal/ingress/annotations/sessionaffinity/main_test.go index 4b7ea5e619a..09c38b42d17 100644 --- a/internal/ingress/annotations/sessionaffinity/main_test.go +++ b/internal/ingress/annotations/sessionaffinity/main_test.go @@ -82,6 +82,7 @@ func TestIngressAffinityCookieConfig(t *testing.T) { data[parser.GetAnnotationWithPrefix(annotationAffinityCookieSameSite)] = "Strict" data[parser.GetAnnotationWithPrefix(annotationAffinityCookieChangeOnFailure)] = "true" data[parser.GetAnnotationWithPrefix(annotationAffinityCookieSecure)] = "true" + data[parser.GetAnnotationWithPrefix(annotationAffinityCookiePartitioned)] = "true" ing.SetAnnotations(data) affin, err := NewParser(&resolver.Mock{}).Parse(ing) @@ -133,4 +134,8 @@ func TestIngressAffinityCookieConfig(t *testing.T) { if !nginxAffinity.Cookie.Secure { t.Errorf("expected secure parameter set to true but returned %v", nginxAffinity.Cookie.Secure) } + + if !nginxAffinity.Cookie.Partitioned { + t.Errorf("expected partitioned parameter set to true but returned %v", nginxAffinity.Cookie.Partitioned) + } } diff --git a/internal/ingress/controller/controller.go b/internal/ingress/controller/controller.go index db786a15c95..db301f9a797 100644 --- a/internal/ingress/controller/controller.go +++ b/internal/ingress/controller/controller.go @@ -870,6 +870,7 @@ func (n *NGINXController) getBackendServers(ingresses []*ingress.Ingress) ([]*in ups.SessionAffinity.CookieSessionAffinity.Domain = anns.SessionAffinity.Cookie.Domain ups.SessionAffinity.CookieSessionAffinity.SameSite = anns.SessionAffinity.Cookie.SameSite ups.SessionAffinity.CookieSessionAffinity.ConditionalSameSiteNone = anns.SessionAffinity.Cookie.ConditionalSameSiteNone + ups.SessionAffinity.CookieSessionAffinity.Partitioned = anns.SessionAffinity.Cookie.Partitioned ups.SessionAffinity.CookieSessionAffinity.ChangeOnFailure = anns.SessionAffinity.Cookie.ChangeOnFailure locs := ups.SessionAffinity.CookieSessionAffinity.Locations diff --git a/pkg/apis/ingress/types.go b/pkg/apis/ingress/types.go index 2ad17ec3dd0..52b03d356f7 100644 --- a/pkg/apis/ingress/types.go +++ b/pkg/apis/ingress/types.go @@ -162,6 +162,7 @@ type CookieSessionAffinity struct { Domain string `json:"domain,omitempty"` SameSite string `json:"samesite,omitempty"` ConditionalSameSiteNone bool `json:"conditional_samesite_none,omitempty"` + Partitioned bool `json:"partitioned,omitempty"` ChangeOnFailure bool `json:"change_on_failure,omitempty"` } diff --git a/pkg/apis/ingress/types_equals.go b/pkg/apis/ingress/types_equals.go index eeed9a06e43..9f918b87bc1 100644 --- a/pkg/apis/ingress/types_equals.go +++ b/pkg/apis/ingress/types_equals.go @@ -187,6 +187,9 @@ func (csa1 *CookieSessionAffinity) Equal(csa2 *CookieSessionAffinity) bool { if csa1.ConditionalSameSiteNone != csa2.ConditionalSameSiteNone { return false } + if csa1.Partitioned != csa2.Partitioned { + return false + } return true } diff --git a/rootfs/etc/nginx/lua/balancer/sticky.lua b/rootfs/etc/nginx/lua/balancer/sticky.lua index 9d0a5411698..c0499c62891 100644 --- a/rootfs/etc/nginx/lua/balancer/sticky.lua +++ b/rootfs/etc/nginx/lua/balancer/sticky.lua @@ -99,6 +99,8 @@ function _M.set_cookie(self, value) httponly = true, samesite = cookie_samesite, secure = cookie_secure, +-- TODO avif: wait for https://github.com/utix/lua-resty-cookie/issues/2 support + partitioned = self.cookie_session_affinity.partitioned, } if self.cookie_session_affinity.expires and self.cookie_session_affinity.expires ~= "" then diff --git a/rootfs/etc/nginx/lua/test/balancer/sticky_test.lua b/rootfs/etc/nginx/lua/test/balancer/sticky_test.lua index 80d0c0d0ec2..095609674b5 100644 --- a/rootfs/etc/nginx/lua/test/balancer/sticky_test.lua +++ b/rootfs/etc/nginx/lua/test/balancer/sticky_test.lua @@ -503,6 +503,56 @@ describe("Sticky", function() end) end) + describe("Partitioned settings", function() + local mocked_cookie_new = cookie.new + + before_each(function() + reset_sticky_balancer() + end) + + after_each(function() + cookie.new = mocked_cookie_new + end) + + local function test_set_cookie_with(sticky_balancer_type, expected_path, partitioned, expected_partitioned) + local s = {} + cookie.new = function(self) + local cookie_instance = { + set = function(self, payload) + assert.equal(payload.key, test_backend.sessionAffinityConfig.cookieSessionAffinity.name) + assert.equal(payload.path, expected_path) + assert.equal(payload.domain, nil) + assert.equal(payload.httponly, true) + assert.equal(payload.secure, true) + assert.equal(payload.partitioned, expected_partitioned) + return true, nil + end, + get = function(k) return false end, + } + s = spy.on(cookie_instance, "set") + return cookie_instance, false + end + local b = get_test_backend() + b.sessionAffinityConfig.cookieSessionAffinity.locations = {} + b.sessionAffinityConfig.cookieSessionAffinity.locations["test.com"] = {"/"} + b.sessionAffinityConfig.cookieSessionAffinity.secure = true + b.sessionAffinityConfig.cookieSessionAffinity.partitioned = partitioned + local sticky_balancer_instance = sticky_balancer_type:new(b) + assert.has_no.errors(function() sticky_balancer_instance:balance() end) + assert.spy(s).was_called() + end + + it("returns a secure cookie with Partitioned when user specifies partitioned=true", function() + test_set_cookie_with(sticky_balanced, "/", true, true) + end) + it("returns a secure cookie with without Partitioned when user specifies partitioned=false", function() + test_set_cookie_with(sticky_balanced, "/", false, false) + end) + it("returns a secure cookie with without Partitioned when user does not specify partitioned", function() + test_set_cookie_with(sticky_balanced, "/", nil, false) + end) + end) + describe("get_cookie()", function() describe("legacy cookie value", function()