From ec00c4aa428abbf5ced88e15ac37967098aa17ed Mon Sep 17 00:00:00 2001 From: IIpragmaII <124467668+IIpragmaII@users.noreply.github.com> Date: Wed, 6 Nov 2024 16:04:04 +0100 Subject: [PATCH] Configurable path for sticky cookies --- .../dynamic-configuration/docker-labels.yml | 1 + .../reference/dynamic-configuration/file.toml | 2 + .../reference/dynamic-configuration/file.yaml | 2 + .../kubernetes-crd-definition-v1.yml | 48 ++++- .../reference/dynamic-configuration/kv-ref.md | 2 + .../traefik.io_ingressroutes.yaml | 8 +- .../traefik.io_middlewares.yaml | 8 +- .../traefik.io_traefikservices.yaml | 32 +++- .../routing/providers/consul-catalog.md | 8 + docs/content/routing/providers/docker.md | 8 + docs/content/routing/providers/ecs.md | 8 + .../routing/providers/kubernetes-crd.md | 5 +- .../routing/providers/kubernetes-ingress.md | 8 + docs/content/routing/providers/kv.md | 14 ++ docs/content/routing/providers/nomad.md | 8 + docs/content/routing/providers/swarm.md | 8 + integration/fixtures/k8s/01-traefik-crd.yml | 48 ++++- pkg/config/dynamic/http_config.go | 12 +- pkg/config/dynamic/zz_generated.deepcopy.go | 7 +- pkg/config/label/label_test.go | 4 + .../kubernetes/crd/fixtures/with_sticky.yml | 81 ++++++++ .../kubernetes/crd/kubernetes_http.go | 37 +++- .../kubernetes/crd/kubernetes_test.go | 173 ++++++++++++++++++ .../kubernetes/ingress/annotations_test.go | 8 +- .../kubernetes/ingress/kubernetes_test.go | 3 + pkg/provider/kv/kv_test.go | 4 + pkg/server/service/loadbalancer/wrr/wrr.go | 7 +- .../service/loadbalancer/wrr/wrr_test.go | 2 + 28 files changed, 530 insertions(+), 26 deletions(-) create mode 100644 pkg/provider/kubernetes/crd/fixtures/with_sticky.yml diff --git a/docs/content/reference/dynamic-configuration/docker-labels.yml b/docs/content/reference/dynamic-configuration/docker-labels.yml index 77fcdef87..b126ab96c 100644 --- a/docs/content/reference/dynamic-configuration/docker-labels.yml +++ b/docs/content/reference/dynamic-configuration/docker-labels.yml @@ -191,6 +191,7 @@ - "traefik.http.services.service02.loadbalancer.sticky.cookie.httponly=true" - "traefik.http.services.service02.loadbalancer.sticky.cookie.maxage=42" - "traefik.http.services.service02.loadbalancer.sticky.cookie.name=foobar" +- "traefik.http.services.service02.loadbalancer.sticky.cookie.path=foobar" - "traefik.http.services.service02.loadbalancer.sticky.cookie.samesite=foobar" - "traefik.http.services.service02.loadbalancer.sticky.cookie.secure=true" - "traefik.http.services.service02.loadbalancer.server.port=foobar" diff --git a/docs/content/reference/dynamic-configuration/file.toml b/docs/content/reference/dynamic-configuration/file.toml index 065d3ca16..e1a93d65f 100644 --- a/docs/content/reference/dynamic-configuration/file.toml +++ b/docs/content/reference/dynamic-configuration/file.toml @@ -55,6 +55,7 @@ httpOnly = true sameSite = "foobar" maxAge = 42 + path = "foobar" [[http.services.Service02.loadBalancer.servers]] url = "foobar" @@ -112,6 +113,7 @@ httpOnly = true sameSite = "foobar" maxAge = 42 + path = "foobar" [http.services.Service04.weighted.healthCheck] [http.middlewares] [http.middlewares.Middleware01] diff --git a/docs/content/reference/dynamic-configuration/file.yaml b/docs/content/reference/dynamic-configuration/file.yaml index 08cb9c385..4f2ae185d 100644 --- a/docs/content/reference/dynamic-configuration/file.yaml +++ b/docs/content/reference/dynamic-configuration/file.yaml @@ -63,6 +63,7 @@ http: httpOnly: true sameSite: foobar maxAge: 42 + path: foobar servers: - url: foobar weight: 42 @@ -113,6 +114,7 @@ http: httpOnly: true sameSite: foobar maxAge: 42 + path: foobar healthCheck: {} middlewares: Middleware01: diff --git a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml index 44ea6d964..8ba8377e1 100644 --- a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml +++ b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml @@ -242,13 +242,19 @@ spec: type: boolean maxAge: description: |- - MaxAge indicates the number of seconds until the cookie expires. + MaxAge defines the number of seconds until the cookie expires. When set to a negative number, the cookie expires immediately. When set to zero, the cookie never expires. type: integer name: description: Name defines the Cookie name. type: string + path: + description: |- + Path defines the path that must exist in the requested URL for the browser to send the Cookie header. + When not provided the cookie will be sent on every request to the domain. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#pathpath-value + type: string sameSite: description: |- SameSite defines the same site policy. @@ -1133,13 +1139,19 @@ spec: type: boolean maxAge: description: |- - MaxAge indicates the number of seconds until the cookie expires. + MaxAge defines the number of seconds until the cookie expires. When set to a negative number, the cookie expires immediately. When set to zero, the cookie never expires. type: integer name: description: Name defines the Cookie name. type: string + path: + description: |- + Path defines the path that must exist in the requested URL for the browser to send the Cookie header. + When not provided the cookie will be sent on every request to the domain. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#pathpath-value + type: string sameSite: description: |- SameSite defines the same site policy. @@ -2686,13 +2698,19 @@ spec: type: boolean maxAge: description: |- - MaxAge indicates the number of seconds until the cookie expires. + MaxAge defines the number of seconds until the cookie expires. When set to a negative number, the cookie expires immediately. When set to zero, the cookie never expires. type: integer name: description: Name defines the Cookie name. type: string + path: + description: |- + Path defines the path that must exist in the requested URL for the browser to send the Cookie header. + When not provided the cookie will be sent on every request to the domain. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#pathpath-value + type: string sameSite: description: |- SameSite defines the same site policy. @@ -2793,13 +2811,19 @@ spec: type: boolean maxAge: description: |- - MaxAge indicates the number of seconds until the cookie expires. + MaxAge defines the number of seconds until the cookie expires. When set to a negative number, the cookie expires immediately. When set to zero, the cookie never expires. type: integer name: description: Name defines the Cookie name. type: string + path: + description: |- + Path defines the path that must exist in the requested URL for the browser to send the Cookie header. + When not provided the cookie will be sent on every request to the domain. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#pathpath-value + type: string sameSite: description: |- SameSite defines the same site policy. @@ -2976,13 +3000,19 @@ spec: type: boolean maxAge: description: |- - MaxAge indicates the number of seconds until the cookie expires. + MaxAge defines the number of seconds until the cookie expires. When set to a negative number, the cookie expires immediately. When set to zero, the cookie never expires. type: integer name: description: Name defines the Cookie name. type: string + path: + description: |- + Path defines the path that must exist in the requested URL for the browser to send the Cookie header. + When not provided the cookie will be sent on every request to the domain. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#pathpath-value + type: string sameSite: description: |- SameSite defines the same site policy. @@ -3023,13 +3053,19 @@ spec: type: boolean maxAge: description: |- - MaxAge indicates the number of seconds until the cookie expires. + MaxAge defines the number of seconds until the cookie expires. When set to a negative number, the cookie expires immediately. When set to zero, the cookie never expires. type: integer name: description: Name defines the Cookie name. type: string + path: + description: |- + Path defines the path that must exist in the requested URL for the browser to send the Cookie header. + When not provided the cookie will be sent on every request to the domain. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#pathpath-value + type: string sameSite: description: |- SameSite defines the same site policy. diff --git a/docs/content/reference/dynamic-configuration/kv-ref.md b/docs/content/reference/dynamic-configuration/kv-ref.md index a2f24a805..21b23ecbe 100644 --- a/docs/content/reference/dynamic-configuration/kv-ref.md +++ b/docs/content/reference/dynamic-configuration/kv-ref.md @@ -266,6 +266,7 @@ THIS FILE MUST NOT BE EDITED BY HAND | `traefik/http/services/Service02/loadBalancer/sticky/cookie/httpOnly` | `true` | | `traefik/http/services/Service02/loadBalancer/sticky/cookie/maxAge` | `42` | | `traefik/http/services/Service02/loadBalancer/sticky/cookie/name` | `foobar` | +| `traefik/http/services/Service02/loadBalancer/sticky/cookie/path` | `foobar` | | `traefik/http/services/Service02/loadBalancer/sticky/cookie/sameSite` | `foobar` | | `traefik/http/services/Service02/loadBalancer/sticky/cookie/secure` | `true` | | `traefik/http/services/Service03/mirroring/healthCheck` | `` | @@ -284,6 +285,7 @@ THIS FILE MUST NOT BE EDITED BY HAND | `traefik/http/services/Service04/weighted/sticky/cookie/httpOnly` | `true` | | `traefik/http/services/Service04/weighted/sticky/cookie/maxAge` | `42` | | `traefik/http/services/Service04/weighted/sticky/cookie/name` | `foobar` | +| `traefik/http/services/Service04/weighted/sticky/cookie/path` | `foobar` | | `traefik/http/services/Service04/weighted/sticky/cookie/sameSite` | `foobar` | | `traefik/http/services/Service04/weighted/sticky/cookie/secure` | `true` | | `traefik/tcp/middlewares/TCPMiddleware01/ipAllowList/sourceRange/0` | `foobar` | diff --git a/docs/content/reference/dynamic-configuration/traefik.io_ingressroutes.yaml b/docs/content/reference/dynamic-configuration/traefik.io_ingressroutes.yaml index cc41ee18c..07a5ead8b 100644 --- a/docs/content/reference/dynamic-configuration/traefik.io_ingressroutes.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.io_ingressroutes.yaml @@ -242,13 +242,19 @@ spec: type: boolean maxAge: description: |- - MaxAge indicates the number of seconds until the cookie expires. + MaxAge defines the number of seconds until the cookie expires. When set to a negative number, the cookie expires immediately. When set to zero, the cookie never expires. type: integer name: description: Name defines the Cookie name. type: string + path: + description: |- + Path defines the path that must exist in the requested URL for the browser to send the Cookie header. + When not provided the cookie will be sent on every request to the domain. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#pathpath-value + type: string sameSite: description: |- SameSite defines the same site policy. diff --git a/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml b/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml index f3ea9fc58..fc9c48b96 100644 --- a/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml @@ -409,13 +409,19 @@ spec: type: boolean maxAge: description: |- - MaxAge indicates the number of seconds until the cookie expires. + MaxAge defines the number of seconds until the cookie expires. When set to a negative number, the cookie expires immediately. When set to zero, the cookie never expires. type: integer name: description: Name defines the Cookie name. type: string + path: + description: |- + Path defines the path that must exist in the requested URL for the browser to send the Cookie header. + When not provided the cookie will be sent on every request to the domain. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#pathpath-value + type: string sameSite: description: |- SameSite defines the same site policy. diff --git a/docs/content/reference/dynamic-configuration/traefik.io_traefikservices.yaml b/docs/content/reference/dynamic-configuration/traefik.io_traefikservices.yaml index 01e28fc5c..6178f7119 100644 --- a/docs/content/reference/dynamic-configuration/traefik.io_traefikservices.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.io_traefikservices.yaml @@ -279,13 +279,19 @@ spec: type: boolean maxAge: description: |- - MaxAge indicates the number of seconds until the cookie expires. + MaxAge defines the number of seconds until the cookie expires. When set to a negative number, the cookie expires immediately. When set to zero, the cookie never expires. type: integer name: description: Name defines the Cookie name. type: string + path: + description: |- + Path defines the path that must exist in the requested URL for the browser to send the Cookie header. + When not provided the cookie will be sent on every request to the domain. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#pathpath-value + type: string sameSite: description: |- SameSite defines the same site policy. @@ -386,13 +392,19 @@ spec: type: boolean maxAge: description: |- - MaxAge indicates the number of seconds until the cookie expires. + MaxAge defines the number of seconds until the cookie expires. When set to a negative number, the cookie expires immediately. When set to zero, the cookie never expires. type: integer name: description: Name defines the Cookie name. type: string + path: + description: |- + Path defines the path that must exist in the requested URL for the browser to send the Cookie header. + When not provided the cookie will be sent on every request to the domain. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#pathpath-value + type: string sameSite: description: |- SameSite defines the same site policy. @@ -569,13 +581,19 @@ spec: type: boolean maxAge: description: |- - MaxAge indicates the number of seconds until the cookie expires. + MaxAge defines the number of seconds until the cookie expires. When set to a negative number, the cookie expires immediately. When set to zero, the cookie never expires. type: integer name: description: Name defines the Cookie name. type: string + path: + description: |- + Path defines the path that must exist in the requested URL for the browser to send the Cookie header. + When not provided the cookie will be sent on every request to the domain. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#pathpath-value + type: string sameSite: description: |- SameSite defines the same site policy. @@ -616,13 +634,19 @@ spec: type: boolean maxAge: description: |- - MaxAge indicates the number of seconds until the cookie expires. + MaxAge defines the number of seconds until the cookie expires. When set to a negative number, the cookie expires immediately. When set to zero, the cookie never expires. type: integer name: description: Name defines the Cookie name. type: string + path: + description: |- + Path defines the path that must exist in the requested URL for the browser to send the Cookie header. + When not provided the cookie will be sent on every request to the domain. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#pathpath-value + type: string sameSite: description: |- SameSite defines the same site policy. diff --git a/docs/content/routing/providers/consul-catalog.md b/docs/content/routing/providers/consul-catalog.md index 82bf1542d..a2b0a7878 100644 --- a/docs/content/routing/providers/consul-catalog.md +++ b/docs/content/routing/providers/consul-catalog.md @@ -265,6 +265,14 @@ you'd add the tag `traefik.http.services.{name-of-your-choice}.loadbalancer.pass traefik.http.services.myservice.loadbalancer.sticky.cookie.name=foobar ``` +??? info "`traefik.http.services..loadbalancer.sticky.cookie.path`" + + See [sticky sessions](../services/index.md#sticky-sessions) for more information. + + ```yaml + traefik.http.services.myservice.loadbalancer.sticky.cookie.path=/foobar + ``` + ??? info "`traefik.http.services..loadbalancer.sticky.cookie.secure`" See [sticky sessions](../services/index.md#sticky-sessions) for more information. diff --git a/docs/content/routing/providers/docker.md b/docs/content/routing/providers/docker.md index 7b1f81408..84c3af25d 100644 --- a/docs/content/routing/providers/docker.md +++ b/docs/content/routing/providers/docker.md @@ -380,6 +380,14 @@ you'd add the label `traefik.http.services..loadbalancer.pa - "traefik.http.services.myservice.loadbalancer.sticky.cookie.name=foobar" ``` +??? info "`traefik.http.services..loadbalancer.sticky.cookie.path`" + + See [sticky sessions](../services/index.md#sticky-sessions) for more information. + + ```yaml + - "traefik.http.services.myservice.loadbalancer.sticky.cookie.path=/foobar" + ``` + ??? info "`traefik.http.services..loadbalancer.sticky.cookie.secure`" See [sticky sessions](../services/index.md#sticky-sessions) for more information. diff --git a/docs/content/routing/providers/ecs.md b/docs/content/routing/providers/ecs.md index fa29eb8a1..30c82fc9b 100644 --- a/docs/content/routing/providers/ecs.md +++ b/docs/content/routing/providers/ecs.md @@ -267,6 +267,14 @@ you'd add the label `traefik.http.services.{name-of-your-choice}.loadbalancer.pa traefik.http.services.myservice.loadbalancer.sticky.cookie.name=foobar ``` +??? info "`traefik.http.services..loadbalancer.sticky.cookie.path`" + + See [sticky sessions](../services/index.md#sticky-sessions) for more information. + + ```yaml + traefik.http.services.myservice.loadbalancer.sticky.cookie.path=/foobar + ``` + ??? info "`traefik.http.services..loadbalancer.sticky.cookie.secure`" See [sticky sessions](../services/index.md#sticky-sessions) for more information. diff --git a/docs/content/routing/providers/kubernetes-crd.md b/docs/content/routing/providers/kubernetes-crd.md index fca592beb..5d2873e5f 100644 --- a/docs/content/routing/providers/kubernetes-crd.md +++ b/docs/content/routing/providers/kubernetes-crd.md @@ -352,6 +352,7 @@ Register the `IngressRoute` [kind](../../reference/dynamic-configuration/kuberne secure: true sameSite: none maxAge: 42 + path: /foo strategy: RoundRobin weight: 10 nativeLB: true # [12] @@ -377,11 +378,11 @@ Register the `IngressRoute` [kind](../../reference/dynamic-configuration/kuberne | [4] | `routes[n].priority` | Defines the [priority](../routers/index.md#priority) to disambiguate rules of the same length, for route matching | | [5] | `routes[n].middlewares` | List of reference to [Middleware](#kind-middleware) | | [6] | `middlewares[n].name` | Defines the [Middleware](#kind-middleware) name | -| [7] | `middlewares[n].namespace` | Defines the [Middleware](#kind-middleware) namespace. It can be omitted when the Middleware is in the IngressRoute namespace. | +| [7] | `middlewares[n].namespace` | Defines the [Middleware](#kind-middleware) namespace. It can be omitted when the Middleware is in the IngressRoute namespace. | | [8] | `routes[n].services` | List of any combination of [TraefikService](#kind-traefikservice) and reference to a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) (See below for `ExternalName Service` setup) | | [9] | `services[n].port` | Defines the port of a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/). This can be a reference to a named port. | | [10] | `services[n].serversTransport` | Defines the reference to a [ServersTransport](#kind-serverstransport). The ServersTransport namespace is assumed to be the [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) namespace (see [ServersTransport reference](#serverstransport-reference)). | -| [11] | `services[n].healthCheck` | Defines the HealthCheck when service references a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type ExternalName. | +| [11] | `services[n].healthCheck` | Defines the HealthCheck when service references a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type ExternalName. | | [12] | `services[n].nativeLB` | Controls, when creating the load-balancer, whether the LB's children are directly the pods IPs or if the only child is the Kubernetes Service clusterIP. | | [13] | `services[n].nodePortLB` | Controls, when creating the load-balancer, whether the LB's children are directly the nodes internal IPs using the nodePort when the service type is NodePort. | | [14] | `tls` | Defines [TLS](../routers/index.md#tls) certificate configuration | diff --git a/docs/content/routing/providers/kubernetes-ingress.md b/docs/content/routing/providers/kubernetes-ingress.md index a5444f5d6..efbf3a0de 100644 --- a/docs/content/routing/providers/kubernetes-ingress.md +++ b/docs/content/routing/providers/kubernetes-ingress.md @@ -383,6 +383,14 @@ which in turn will create the resulting routers, services, handlers, etc. traefik.ingress.kubernetes.io/service.sticky.cookie.maxage: 42 ``` +??? info "`traefik.ingress.kubernetes.io/service.sticky.cookie.path`" + + See [sticky sessions](../services/index.md#sticky-sessions) for more information. + + ```yaml + traefik.ingress.kubernetes.io/service.sticky.cookie.path: /foobar + ``` + ## Path Types on Kubernetes 1.18+ If the Kubernetes cluster version is 1.18+, diff --git a/docs/content/routing/providers/kv.md b/docs/content/routing/providers/kv.md index fad639d19..ba440db3f 100644 --- a/docs/content/routing/providers/kv.md +++ b/docs/content/routing/providers/kv.md @@ -228,6 +228,14 @@ A Story of key & values |-------------------------------------------------------------------|----------| | `traefik/http/services/myservice/loadbalancer/sticky/cookie/name` | `foobar` | +??? info "`traefik/http/services//loadbalancer/sticky/cookie/path`" + + See [sticky sessions](../services/index.md#sticky-sessions) for more information. + + | Key (Path) | Value | + |-------------------------------------------------------------------|-----------| + | `traefik/http/services/myservice/loadbalancer/sticky/cookie/path` | `/foobar` | + ??? info "`traefik/http/services//loadbalancer/sticky/cookie/secure`" See [sticky sessions](../services/index.md#sticky-sessions) for more information. @@ -320,6 +328,12 @@ A Story of key & values |----------------------------------------------------------------------|-------| | `traefik/http/services//weighted/sticky/cookie/maxage` | `42` | +??? info "`traefik/http/services//weighted/sticky/cookie/path`" + + | Key (Path) | Value | + |----------------------------------------------------------------------|-----------| + | `traefik/http/services//weighted/sticky/cookie/path` | `/foobar` | + ### Middleware More information about available middlewares in the dedicated [middlewares section](../../middlewares/overview.md). diff --git a/docs/content/routing/providers/nomad.md b/docs/content/routing/providers/nomad.md index c1dc31f30..2fbdd8a4e 100644 --- a/docs/content/routing/providers/nomad.md +++ b/docs/content/routing/providers/nomad.md @@ -281,6 +281,14 @@ you'd add the tag `traefik.http.services.{name-of-your-choice}.loadbalancer.pass traefik.http.services.myservice.loadbalancer.sticky.cookie.maxage=42 ``` +??? info "`traefik.http.services..loadbalancer.sticky.cookie.path`" + + See [sticky sessions](../services/index.md#sticky-sessions) for more information. + + ```yaml + traefik.http.services.myservice.loadbalancer.sticky.cookie.path=/foobar + ``` + ??? info "`traefik.http.services..loadbalancer.responseforwarding.flushinterval`" See [response forwarding](../services/index.md#response-forwarding) for more information. diff --git a/docs/content/routing/providers/swarm.md b/docs/content/routing/providers/swarm.md index f6dcfb1ef..127336ba5 100644 --- a/docs/content/routing/providers/swarm.md +++ b/docs/content/routing/providers/swarm.md @@ -394,6 +394,14 @@ you'd add the label `traefik.http.services..loadbalancer.pa - "traefik.http.services.myservice.loadbalancer.sticky.cookie.name=foobar" ``` +??? info "`traefik.http.services..loadbalancer.sticky.cookie.path`" + + See [sticky sessions](../services/index.md#sticky-sessions) for more information. + + ```yaml + - "traefik.http.services.myservice.loadbalancer.sticky.cookie.path=/foobar" + ``` + ??? info "`traefik.http.services..loadbalancer.sticky.cookie.secure`" See [sticky sessions](../services/index.md#sticky-sessions) for more information. diff --git a/integration/fixtures/k8s/01-traefik-crd.yml b/integration/fixtures/k8s/01-traefik-crd.yml index 44ea6d964..8ba8377e1 100644 --- a/integration/fixtures/k8s/01-traefik-crd.yml +++ b/integration/fixtures/k8s/01-traefik-crd.yml @@ -242,13 +242,19 @@ spec: type: boolean maxAge: description: |- - MaxAge indicates the number of seconds until the cookie expires. + MaxAge defines the number of seconds until the cookie expires. When set to a negative number, the cookie expires immediately. When set to zero, the cookie never expires. type: integer name: description: Name defines the Cookie name. type: string + path: + description: |- + Path defines the path that must exist in the requested URL for the browser to send the Cookie header. + When not provided the cookie will be sent on every request to the domain. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#pathpath-value + type: string sameSite: description: |- SameSite defines the same site policy. @@ -1133,13 +1139,19 @@ spec: type: boolean maxAge: description: |- - MaxAge indicates the number of seconds until the cookie expires. + MaxAge defines the number of seconds until the cookie expires. When set to a negative number, the cookie expires immediately. When set to zero, the cookie never expires. type: integer name: description: Name defines the Cookie name. type: string + path: + description: |- + Path defines the path that must exist in the requested URL for the browser to send the Cookie header. + When not provided the cookie will be sent on every request to the domain. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#pathpath-value + type: string sameSite: description: |- SameSite defines the same site policy. @@ -2686,13 +2698,19 @@ spec: type: boolean maxAge: description: |- - MaxAge indicates the number of seconds until the cookie expires. + MaxAge defines the number of seconds until the cookie expires. When set to a negative number, the cookie expires immediately. When set to zero, the cookie never expires. type: integer name: description: Name defines the Cookie name. type: string + path: + description: |- + Path defines the path that must exist in the requested URL for the browser to send the Cookie header. + When not provided the cookie will be sent on every request to the domain. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#pathpath-value + type: string sameSite: description: |- SameSite defines the same site policy. @@ -2793,13 +2811,19 @@ spec: type: boolean maxAge: description: |- - MaxAge indicates the number of seconds until the cookie expires. + MaxAge defines the number of seconds until the cookie expires. When set to a negative number, the cookie expires immediately. When set to zero, the cookie never expires. type: integer name: description: Name defines the Cookie name. type: string + path: + description: |- + Path defines the path that must exist in the requested URL for the browser to send the Cookie header. + When not provided the cookie will be sent on every request to the domain. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#pathpath-value + type: string sameSite: description: |- SameSite defines the same site policy. @@ -2976,13 +3000,19 @@ spec: type: boolean maxAge: description: |- - MaxAge indicates the number of seconds until the cookie expires. + MaxAge defines the number of seconds until the cookie expires. When set to a negative number, the cookie expires immediately. When set to zero, the cookie never expires. type: integer name: description: Name defines the Cookie name. type: string + path: + description: |- + Path defines the path that must exist in the requested URL for the browser to send the Cookie header. + When not provided the cookie will be sent on every request to the domain. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#pathpath-value + type: string sameSite: description: |- SameSite defines the same site policy. @@ -3023,13 +3053,19 @@ spec: type: boolean maxAge: description: |- - MaxAge indicates the number of seconds until the cookie expires. + MaxAge defines the number of seconds until the cookie expires. When set to a negative number, the cookie expires immediately. When set to zero, the cookie never expires. type: integer name: description: Name defines the Cookie name. type: string + path: + description: |- + Path defines the path that must exist in the requested URL for the browser to send the Cookie header. + When not provided the cookie will be sent on every request to the domain. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#pathpath-value + type: string sameSite: description: |- SameSite defines the same site policy. diff --git a/pkg/config/dynamic/http_config.go b/pkg/config/dynamic/http_config.go index e29bdd30a..c92b1328a 100644 --- a/pkg/config/dynamic/http_config.go +++ b/pkg/config/dynamic/http_config.go @@ -175,10 +175,20 @@ type Cookie struct { // SameSite defines the same site policy. // More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite SameSite string `json:"sameSite,omitempty" toml:"sameSite,omitempty" yaml:"sameSite,omitempty" export:"true"` - // MaxAge indicates the number of seconds until the cookie expires. + // MaxAge defines the number of seconds until the cookie expires. // When set to a negative number, the cookie expires immediately. // When set to zero, the cookie never expires. MaxAge int `json:"maxAge,omitempty" toml:"maxAge,omitempty" yaml:"maxAge,omitempty" export:"true"` + // Path defines the path that must exist in the requested URL for the browser to send the Cookie header. + // When not provided the cookie will be sent on every request to the domain. + // More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#pathpath-value + Path *string `json:"path,omitempty" toml:"path,omitempty" yaml:"path,omitempty" export:"true"` +} + +// SetDefaults set the default values for a Cookie. +func (c *Cookie) SetDefaults() { + defaultPath := "/" + c.Path = &defaultPath } // +k8s:deepcopy-gen=true diff --git a/pkg/config/dynamic/zz_generated.deepcopy.go b/pkg/config/dynamic/zz_generated.deepcopy.go index ba0e96606..792ff4805 100644 --- a/pkg/config/dynamic/zz_generated.deepcopy.go +++ b/pkg/config/dynamic/zz_generated.deepcopy.go @@ -266,6 +266,11 @@ func (in *ContentType) DeepCopy() *ContentType { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Cookie) DeepCopyInto(out *Cookie) { *out = *in + if in.Path != nil { + in, out := &in.Path, &out.Path + *out = new(string) + **out = **in + } return } @@ -1515,7 +1520,7 @@ func (in *Sticky) DeepCopyInto(out *Sticky) { if in.Cookie != nil { in, out := &in.Cookie, &out.Cookie *out = new(Cookie) - **out = **in + (*in).DeepCopyInto(*out) } return } diff --git a/pkg/config/label/label_test.go b/pkg/config/label/label_test.go index be9c6f8cc..c9b8d3e35 100644 --- a/pkg/config/label/label_test.go +++ b/pkg/config/label/label_test.go @@ -174,6 +174,7 @@ func TestDecodeConfiguration(t *testing.T) { "traefik.http.services.Service0.loadbalancer.server.port": "8080", "traefik.http.services.Service0.loadbalancer.sticky.cookie.name": "foobar", "traefik.http.services.Service0.loadbalancer.sticky.cookie.secure": "true", + "traefik.http.services.Service0.loadbalancer.sticky.cookie.path": "/foobar", "traefik.http.services.Service0.loadbalancer.serversTransport": "foobar", "traefik.http.services.Service1.loadbalancer.healthcheck.headers.name0": "foobar", "traefik.http.services.Service1.loadbalancer.healthcheck.headers.name1": "foobar", @@ -674,6 +675,7 @@ func TestDecodeConfiguration(t *testing.T) { Name: "foobar", Secure: true, HTTPOnly: false, + Path: func(v string) *string { return &v }("/foobar"), }, }, Servers: []dynamic.Server{ @@ -1196,6 +1198,7 @@ func TestEncodeConfiguration(t *testing.T) { Cookie: &dynamic.Cookie{ Name: "foobar", HTTPOnly: true, + Path: func(v string) *string { return &v }("/foobar"), }, }, Servers: []dynamic.Server{ @@ -1433,6 +1436,7 @@ func TestEncodeConfiguration(t *testing.T) { "traefik.HTTP.Services.Service0.LoadBalancer.Sticky.Cookie.HTTPOnly": "true", "traefik.HTTP.Services.Service0.LoadBalancer.Sticky.Cookie.Secure": "false", "traefik.HTTP.Services.Service0.LoadBalancer.Sticky.Cookie.MaxAge": "0", + "traefik.HTTP.Services.Service0.LoadBalancer.Sticky.Cookie.Path": "/foobar", "traefik.HTTP.Services.Service0.LoadBalancer.ServersTransport": "foobar", "traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Headers.name0": "foobar", "traefik.HTTP.Services.Service1.LoadBalancer.HealthCheck.Headers.name1": "foobar", diff --git a/pkg/provider/kubernetes/crd/fixtures/with_sticky.yml b/pkg/provider/kubernetes/crd/fixtures/with_sticky.yml new file mode 100644 index 000000000..58ce4e199 --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/with_sticky.yml @@ -0,0 +1,81 @@ +--- +apiVersion: traefik.io/v1alpha1 +kind: TraefikService +metadata: + name: sticky-default + namespace: default +spec: + weighted: + sticky: + cookie: + httpOnly: true + name: cookie + secure: true + sameSite: none + maxAge: 42 + services: + - name: whoami3 + port: 8443 + +--- +apiVersion: traefik.io/v1alpha1 +kind: TraefikService +metadata: + name: sticky + namespace: default +spec: + weighted: + sticky: + cookie: + httpOnly: true + name: cookie + secure: true + sameSite: none + maxAge: 42 + path: /foo + services: + - name: whoami3 + port: 8443 + +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: test2.route + namespace: default + +spec: + entryPoints: + - web + + routes: + - match: Host(`traefik-service`) + kind: Rule + services: + - name: sticky + kind: TraefikService + - name: sticky-default + kind: TraefikService + + - match: Host(`k8s-service`) + kind: Rule + services: + - name: whoami + port: 80 + sticky: + cookie: + httpOnly: true + name: cookie + secure: true + sameSite: none + maxAge: 42 + path: /foo + - name: whoami2 + port: 8080 + sticky: + cookie: + httpOnly: true + name: cookie + secure: true + sameSite: none + maxAge: 42 diff --git a/pkg/provider/kubernetes/crd/kubernetes_http.go b/pkg/provider/kubernetes/crd/kubernetes_http.go index c0faaddbf..95a7063ed 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_http.go +++ b/pkg/provider/kubernetes/crd/kubernetes_http.go @@ -248,10 +248,28 @@ func (c configBuilder) buildServicesLB(ctx context.Context, namespace string, tS }) } + var sticky *dynamic.Sticky + if tService.Weighted.Sticky != nil && tService.Weighted.Sticky.Cookie != nil { + sticky = &dynamic.Sticky{ + Cookie: &dynamic.Cookie{ + Name: tService.Weighted.Sticky.Cookie.Name, + Secure: tService.Weighted.Sticky.Cookie.Secure, + HTTPOnly: tService.Weighted.Sticky.Cookie.HTTPOnly, + SameSite: tService.Weighted.Sticky.Cookie.SameSite, + MaxAge: tService.Weighted.Sticky.Cookie.MaxAge, + }, + } + sticky.Cookie.SetDefaults() + + if tService.Weighted.Sticky.Cookie.Path != nil { + sticky.Cookie.Path = tService.Weighted.Sticky.Cookie.Path + } + } + conf[id] = &dynamic.Service{ Weighted: &dynamic.WeightedRoundRobin{ Services: wrrServices, - Sticky: tService.Weighted.Sticky, + Sticky: sticky, }, } return nil @@ -353,7 +371,22 @@ func (c configBuilder) buildServersLB(namespace string, svc traefikv1alpha1.Load } } - lb.Sticky = svc.Sticky + if svc.Sticky != nil && svc.Sticky.Cookie != nil { + lb.Sticky = &dynamic.Sticky{ + Cookie: &dynamic.Cookie{ + Name: svc.Sticky.Cookie.Name, + Secure: svc.Sticky.Cookie.Secure, + HTTPOnly: svc.Sticky.Cookie.HTTPOnly, + SameSite: svc.Sticky.Cookie.SameSite, + MaxAge: svc.Sticky.Cookie.MaxAge, + }, + } + lb.Sticky.Cookie.SetDefaults() + + if svc.Sticky.Cookie.Path != nil { + lb.Sticky.Cookie.Path = svc.Sticky.Cookie.Path + } + } lb.ServersTransport, err = c.makeServersTransportKey(namespace, svc.ServersTransport) if err != nil { diff --git a/pkg/provider/kubernetes/crd/kubernetes_test.go b/pkg/provider/kubernetes/crd/kubernetes_test.go index e25357f51..3fce7f44a 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_test.go +++ b/pkg/provider/kubernetes/crd/kubernetes_test.go @@ -25,6 +25,7 @@ import ( "k8s.io/apimachinery/pkg/util/intstr" kubefake "k8s.io/client-go/kubernetes/fake" kscheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/utils/pointer" ) var _ provider.Provider = (*Provider)(nil) @@ -4903,6 +4904,178 @@ func TestLoadIngressRoutes(t *testing.T) { TLS: &dynamic.TLSConfiguration{}, }, }, + { + desc: "Simple Ingress Route with sticky", + allowCrossNamespace: true, + paths: []string{"services.yml", "with_sticky.yml"}, + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + TCP: &dynamic.TCPConfiguration{ + Routers: map[string]*dynamic.TCPRouter{}, + Middlewares: map[string]*dynamic.TCPMiddleware{}, + Services: map[string]*dynamic.TCPService{}, + ServersTransports: map[string]*dynamic.TCPServersTransport{}, + }, + HTTP: &dynamic.HTTPConfiguration{ + Routers: map[string]*dynamic.Router{ + "default-test2-route-840425136fbd5d85a4ad": { + EntryPoints: []string{"web"}, + Service: "default-test2-route-840425136fbd5d85a4ad", + Rule: "Host(`k8s-service`)", + }, + "default-test2-route-4f06607bbc69f34a4db5": { + EntryPoints: []string{"web"}, + Service: "default-test2-route-4f06607bbc69f34a4db5", + Rule: "Host(`traefik-service`)", + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "default-test2-route-840425136fbd5d85a4ad": { + Weighted: &dynamic.WeightedRoundRobin{ + Services: []dynamic.WRRService{ + { + Name: "default-whoami-80", + Weight: Int(1), + }, + { + Name: "default-whoami2-8080", + Weight: Int(1), + }, + }, + }, + }, + "default-test2-route-4f06607bbc69f34a4db5": { + Weighted: &dynamic.WeightedRoundRobin{ + Services: []dynamic.WRRService{ + { + Name: "default-sticky", + Weight: Int(1), + }, + { + Name: "default-sticky-default", + Weight: Int(1), + }, + }, + }, + }, + "default-sticky": { + Weighted: &dynamic.WeightedRoundRobin{ + Sticky: &dynamic.Sticky{ + Cookie: &dynamic.Cookie{ + Name: "cookie", + Secure: true, + HTTPOnly: true, + SameSite: "none", + MaxAge: 42, + Path: pointer.String("/foo"), + }, + }, + Services: []dynamic.WRRService{ + { + Name: "default-whoami3-8443", + Weight: Int(1), + }, + }, + }, + }, + "default-sticky-default": { + Weighted: &dynamic.WeightedRoundRobin{ + Sticky: &dynamic.Sticky{ + Cookie: &dynamic.Cookie{ + Name: "cookie", + Secure: true, + HTTPOnly: true, + SameSite: "none", + MaxAge: 42, + Path: pointer.String("/"), + }, + }, + Services: []dynamic.WRRService{ + { + Name: "default-whoami3-8443", + Weight: Int(1), + }, + }, + }, + }, + "default-whoami2-8080": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Sticky: &dynamic.Sticky{ + Cookie: &dynamic.Cookie{ + Name: "cookie", + Secure: true, + HTTPOnly: true, + SameSite: "none", + MaxAge: 42, + Path: pointer.String("/"), + }, + }, + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.3:8080", + }, + { + URL: "http://10.10.0.4:8080", + }, + }, + PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, + }, + }, + "default-whoami-80": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Sticky: &dynamic.Sticky{ + Cookie: &dynamic.Cookie{ + Name: "cookie", + Secure: true, + HTTPOnly: true, + SameSite: "none", + MaxAge: 42, + Path: pointer.String("/foo"), + }, + }, + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:80", + }, + { + URL: "http://10.10.0.2:80", + }, + }, + PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, + }, + }, + "default-whoami3-8443": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.7:8443", + }, + { + URL: "http://10.10.0.8:8443", + }, + }, + PassHostHeader: Bool(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + TLS: &dynamic.TLSConfiguration{}, + }, + }, } for _, test := range testCases { diff --git a/pkg/provider/kubernetes/ingress/annotations_test.go b/pkg/provider/kubernetes/ingress/annotations_test.go index 7746b2623..692e20dbc 100644 --- a/pkg/provider/kubernetes/ingress/annotations_test.go +++ b/pkg/provider/kubernetes/ingress/annotations_test.go @@ -113,6 +113,7 @@ func Test_parseServiceConfig(t *testing.T) { "traefik.ingress.kubernetes.io/service.sticky.cookie.name": "foobar", "traefik.ingress.kubernetes.io/service.sticky.cookie.secure": "true", "traefik.ingress.kubernetes.io/service.sticky.cookie.samesite": "none", + "traefik.ingress.kubernetes.io/service.sticky.cookie.path": "foobar", }, expected: &ServiceConfig{ Service: &ServiceIng{ @@ -122,6 +123,7 @@ func Test_parseServiceConfig(t *testing.T) { Secure: true, HTTPOnly: true, SameSite: "none", + Path: String("foobar"), }, }, ServersScheme: "protocol", @@ -138,7 +140,11 @@ func Test_parseServiceConfig(t *testing.T) { }, expected: &ServiceConfig{ Service: &ServiceIng{ - Sticky: &dynamic.Sticky{Cookie: &dynamic.Cookie{}}, + Sticky: &dynamic.Sticky{ + Cookie: &dynamic.Cookie{ + Path: String("/"), + }, + }, PassHostHeader: Bool(true), }, }, diff --git a/pkg/provider/kubernetes/ingress/kubernetes_test.go b/pkg/provider/kubernetes/ingress/kubernetes_test.go index 2af574c35..2d118f64d 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes_test.go +++ b/pkg/provider/kubernetes/ingress/kubernetes_test.go @@ -24,6 +24,8 @@ var _ provider.Provider = (*Provider)(nil) func Bool(v bool) *bool { return &v } +func String(v string) *string { return &v } + func TestLoadConfigurationFromIngresses(t *testing.T) { testCases := []struct { desc string @@ -126,6 +128,7 @@ func TestLoadConfigurationFromIngresses(t *testing.T) { Name: "foobar", Secure: true, HTTPOnly: true, + Path: String("/"), }, }, Servers: []dynamic.Server{ diff --git a/pkg/provider/kv/kv_test.go b/pkg/provider/kv/kv_test.go index 6cfcc7b61..ea147f0fa 100644 --- a/pkg/provider/kv/kv_test.go +++ b/pkg/provider/kv/kv_test.go @@ -58,6 +58,7 @@ func Test_buildConfiguration(t *testing.T) { "traefik/http/services/Service01/loadBalancer/sticky/cookie/name": "foobar", "traefik/http/services/Service01/loadBalancer/sticky/cookie/secure": "true", "traefik/http/services/Service01/loadBalancer/sticky/cookie/httpOnly": "true", + "traefik/http/services/Service01/loadBalancer/sticky/cookie/path": "foobar", "traefik/http/services/Service01/loadBalancer/servers/0/url": "foobar", "traefik/http/services/Service01/loadBalancer/servers/1/url": "foobar", "traefik/http/services/Service02/mirroring/service": "foobar", @@ -70,6 +71,7 @@ func Test_buildConfiguration(t *testing.T) { "traefik/http/services/Service03/weighted/sticky/cookie/name": "foobar", "traefik/http/services/Service03/weighted/sticky/cookie/secure": "true", "traefik/http/services/Service03/weighted/sticky/cookie/httpOnly": "true", + "traefik/http/services/Service03/weighted/sticky/cookie/path": "foobar", "traefik/http/services/Service03/weighted/services/0/name": "foobar", "traefik/http/services/Service03/weighted/services/0/weight": "42", "traefik/http/services/Service03/weighted/services/1/name": "foobar", @@ -642,6 +644,7 @@ func Test_buildConfiguration(t *testing.T) { Name: "foobar", Secure: true, HTTPOnly: true, + Path: func(v string) *string { return &v }("foobar"), }, }, Servers: []dynamic.Server{ @@ -708,6 +711,7 @@ func Test_buildConfiguration(t *testing.T) { Name: "foobar", Secure: true, HTTPOnly: true, + Path: func(v string) *string { return &v }("foobar"), }, }, }, diff --git a/pkg/server/service/loadbalancer/wrr/wrr.go b/pkg/server/service/loadbalancer/wrr/wrr.go index 6aa04c8ec..3d258877b 100644 --- a/pkg/server/service/loadbalancer/wrr/wrr.go +++ b/pkg/server/service/loadbalancer/wrr/wrr.go @@ -26,6 +26,7 @@ type stickyCookie struct { httpOnly bool sameSite string maxAge int + path string } func convertSameSite(sameSite string) http.SameSite { @@ -79,6 +80,10 @@ func New(sticky *dynamic.Sticky, wantHealthCheck bool) *Balancer { httpOnly: sticky.Cookie.HTTPOnly, sameSite: sticky.Cookie.SameSite, maxAge: sticky.Cookie.MaxAge, + path: "/", + } + if sticky.Cookie.Path != nil { + balancer.stickyCookie.path = *sticky.Cookie.Path } } @@ -236,7 +241,7 @@ func (b *Balancer) ServeHTTP(w http.ResponseWriter, req *http.Request) { cookie := &http.Cookie{ Name: b.stickyCookie.name, Value: hash(server.name), - Path: "/", + Path: b.stickyCookie.path, HttpOnly: b.stickyCookie.httpOnly, Secure: b.stickyCookie.secure, SameSite: convertSameSite(b.stickyCookie.sameSite), diff --git a/pkg/server/service/loadbalancer/wrr/wrr_test.go b/pkg/server/service/loadbalancer/wrr/wrr_test.go index 948e4df45..62b46ee41 100644 --- a/pkg/server/service/loadbalancer/wrr/wrr_test.go +++ b/pkg/server/service/loadbalancer/wrr/wrr_test.go @@ -226,6 +226,7 @@ func TestSticky(t *testing.T) { HTTPOnly: true, SameSite: "none", MaxAge: 42, + Path: func(v string) *string { return &v }("/foo"), }, }, false) @@ -263,6 +264,7 @@ func TestSticky(t *testing.T) { assert.True(t, recorder.cookies["test"].Secure) assert.Equal(t, http.SameSiteNoneMode, recorder.cookies["test"].SameSite) assert.Equal(t, 42, recorder.cookies["test"].MaxAge) + assert.Equal(t, "/foo", recorder.cookies["test"].Path) } func TestSticky_FallBack(t *testing.T) {