mirror of
https://github.com/fluxcd/flagger.git
synced 2026-02-14 09:59:58 +00:00
Merge pull request #1826 from renatovassaomb/rv/add-cookie-attributes
Feat: Add Support for Cookie Attributes in Session Affinity
This commit is contained in:
@@ -1158,10 +1158,32 @@ spec:
|
||||
primaryCookieName:
|
||||
description: CookieName is the key that will be used for the session affinity cookie.
|
||||
type: string
|
||||
domain:
|
||||
description: Domain defines the host to which the cookie will be sent.
|
||||
type: string
|
||||
httpOnly:
|
||||
description: HttpOnly forbids JavaScript from accessing the cookie, for example, through the Document.cookie property.
|
||||
type: boolean
|
||||
maxAge:
|
||||
description: MaxAge indicates the number of seconds until the session affinity cookie will expire.
|
||||
default: 86400
|
||||
type: number
|
||||
partitioned:
|
||||
description: Partitioned indicates that the cookie should be stored using partitioned storage.
|
||||
type: boolean
|
||||
path:
|
||||
description: Path indicates the path that must exist in the requested URL for the browser to send the Cookie header.
|
||||
type: string
|
||||
sameSite:
|
||||
description: SameSite controls whether or not a cookie is sent with cross-site requests.
|
||||
type: string
|
||||
enum:
|
||||
- Strict
|
||||
- Lax
|
||||
- None
|
||||
secure:
|
||||
description: "Secure indicates that the cookie is sent to the server only when a request is made with the https: scheme (except on localhost)"
|
||||
type: boolean
|
||||
status:
|
||||
description: CanaryStatus defines the observed state of a canary.
|
||||
type: object
|
||||
|
||||
@@ -1158,10 +1158,32 @@ spec:
|
||||
primaryCookieName:
|
||||
description: CookieName is the key that will be used for the session affinity cookie.
|
||||
type: string
|
||||
domain:
|
||||
description: Domain defines the host to which the cookie will be sent.
|
||||
type: string
|
||||
httpOnly:
|
||||
description: HttpOnly forbids JavaScript from accessing the cookie, for example, through the Document.cookie property.
|
||||
type: boolean
|
||||
maxAge:
|
||||
description: MaxAge indicates the number of seconds until the session affinity cookie will expire.
|
||||
default: 86400
|
||||
type: number
|
||||
partitioned:
|
||||
description: Partitioned indicates that the cookie should be stored using partitioned storage.
|
||||
type: boolean
|
||||
path:
|
||||
description: Path indicates the path that must exist in the requested URL for the browser to send the Cookie header.
|
||||
type: string
|
||||
sameSite:
|
||||
description: SameSite controls whether or not a cookie is sent with cross-site requests.
|
||||
type: string
|
||||
enum:
|
||||
- Strict
|
||||
- Lax
|
||||
- None
|
||||
secure:
|
||||
description: "Secure indicates that the cookie is sent to the server only when a request is made with the https: scheme (except on localhost)"
|
||||
type: boolean
|
||||
status:
|
||||
description: CanaryStatus defines the observed state of a canary.
|
||||
type: object
|
||||
|
||||
@@ -494,3 +494,38 @@ then all subsequent requests will be routed to the same until the next step star
|
||||
value is generated which is then included in the headers of responses from the primary workload. This allows for
|
||||
weighted traffic routing to happen while ensuring that users don't ever switch back to the primary deployment from
|
||||
the canary deployment during a Canary analysis.
|
||||
|
||||
### Configuring additional cookie attributes
|
||||
|
||||
Depending on your use case, you may neet to set additional [cookie attributes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#attributes) in order for your application to route requests correctly.
|
||||
You may set the following attributes:
|
||||
|
||||
```yaml
|
||||
analysis:
|
||||
# schedule interval (default 60s)
|
||||
interval: 1m
|
||||
sessionAffinity:
|
||||
# name of the cookie used
|
||||
cookieName: flagger-cookie
|
||||
# max age of the cookie (in seconds)
|
||||
# optional; defaults to 86400
|
||||
maxAge: 21600
|
||||
# defines the host to which the cookie will be sent.
|
||||
# optional
|
||||
domain: fluxcd.io
|
||||
# forbids JavaScript from accessing the cookie, for example, through the Document.cookie property.
|
||||
# optional
|
||||
httpOnly: true
|
||||
# indicates that the cookie should be stored using partitioned storage.
|
||||
# optional
|
||||
partitioned: true
|
||||
# indicates the path that must exist in the requested URL for the browser to send the Cookie header.
|
||||
# optional
|
||||
path: /flagger
|
||||
# controls whether or not a cookie is sent with cross-site requests.
|
||||
# optional; valid values are Strict, Lax or None
|
||||
sameSite: Strict
|
||||
# indicates that the cookie is sent to the server only when a request is made with the https: scheme (except on localhost)
|
||||
# optional
|
||||
secure: true
|
||||
```
|
||||
|
||||
@@ -1158,10 +1158,32 @@ spec:
|
||||
primaryCookieName:
|
||||
description: CookieName is the key that will be used for the session affinity cookie.
|
||||
type: string
|
||||
domain:
|
||||
description: Domain defines the host to which the cookie will be sent.
|
||||
type: string
|
||||
httpOnly:
|
||||
description: HttpOnly forbids JavaScript from accessing the cookie, for example, through the Document.cookie property.
|
||||
type: boolean
|
||||
maxAge:
|
||||
description: MaxAge indicates the number of seconds until the session affinity cookie will expire.
|
||||
default: 86400
|
||||
type: number
|
||||
partitioned:
|
||||
description: Partitioned indicates that the cookie should be stored using partitioned storage.
|
||||
type: boolean
|
||||
path:
|
||||
description: Path indicates the path that must exist in the requested URL for the browser to send the Cookie header.
|
||||
type: string
|
||||
sameSite:
|
||||
description: SameSite controls whether or not a cookie is sent with cross-site requests.
|
||||
type: string
|
||||
enum:
|
||||
- Strict
|
||||
- Lax
|
||||
- None
|
||||
secure:
|
||||
description: "Secure indicates that the cookie is sent to the server only when a request is made with the https: scheme (except on localhost)"
|
||||
type: boolean
|
||||
status:
|
||||
description: CanaryStatus defines the observed state of a canary.
|
||||
type: object
|
||||
|
||||
@@ -297,11 +297,30 @@ type CanaryAnalysis struct {
|
||||
type SessionAffinity struct {
|
||||
// CookieName is the key that will be used for the session affinity cookie.
|
||||
CookieName string `json:"cookieName,omitempty"`
|
||||
// MaxAge indicates the number of seconds until the session affinity cookie will expire.
|
||||
// ref: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#attributes
|
||||
// Domain defines the host to which the cookie will be sent.
|
||||
// +optional
|
||||
Domain string `json:"domain,omitempty"`
|
||||
// HttpOnly forbids JavaScript from accessing the cookie, for example, through the Document.cookie property.
|
||||
// +optional
|
||||
HttpOnly bool `json:"httpOnly,omitempty"`
|
||||
// MaxAge indicates the number of seconds until the session affinity cookie will expire.
|
||||
// The default value is 86,400 seconds, i.e. a day.
|
||||
// +optional
|
||||
MaxAge int `json:"maxAge,omitempty"`
|
||||
// Partitioned indicates that the cookie should be stored using partitioned storage.
|
||||
// +optional
|
||||
Partitioned bool `json:"partitioned,omitempty"`
|
||||
// Path indicates the path that must exist in the requested URL for the browser to send the Cookie header.
|
||||
// +optional
|
||||
Path string `json:"path,omitempty"`
|
||||
// SameSite controls whether or not a cookie is sent with cross-site requests.
|
||||
// +optional
|
||||
// +kubebuilder:validation:Enum=Strict;Lax;None
|
||||
SameSite string `json:"sameSite,omitempty"`
|
||||
// Secure indicates that the cookie is sent to the server only when a request is made with the https: scheme (except on localhost)
|
||||
// +optional
|
||||
Secure bool `json:"secure,omitempty"`
|
||||
// PrimaryCookieName is the key that will be used for the primary session affinity cookie.
|
||||
// +optional
|
||||
PrimaryCookieName string `json:"primaryCookieName,omitempty"`
|
||||
@@ -668,3 +687,36 @@ func (c *Canary) DeploymentStrategy() string {
|
||||
// Canary Release: default (has maxWeight, stepWeight, or stepWeights)
|
||||
return DeploymentStrategyCanary
|
||||
}
|
||||
|
||||
// BuildCookie returns the cookie that should be used as the value of a Set-Cookie header
|
||||
func (s *SessionAffinity) BuildCookie(cookieName string) string {
|
||||
cookie := fmt.Sprintf("%s; %s=%d", cookieName, "Max-Age",
|
||||
s.GetMaxAge(),
|
||||
)
|
||||
|
||||
if s.Domain != "" {
|
||||
cookie += fmt.Sprintf("; %s=%s", "Domain", s.Domain)
|
||||
}
|
||||
|
||||
if s.HttpOnly {
|
||||
cookie += fmt.Sprintf("; %s", "HttpOnly")
|
||||
}
|
||||
|
||||
if s.Partitioned {
|
||||
cookie += fmt.Sprintf("; %s", "Partitioned")
|
||||
}
|
||||
|
||||
if s.Path != "" {
|
||||
cookie += fmt.Sprintf("; %s=%s", "Path", s.Path)
|
||||
}
|
||||
|
||||
if s.SameSite != "" {
|
||||
cookie += fmt.Sprintf("; %s=%s", "SameSite", s.SameSite)
|
||||
}
|
||||
|
||||
if s.Secure {
|
||||
cookie += fmt.Sprintf("; %s", "Secure")
|
||||
}
|
||||
|
||||
return cookie
|
||||
}
|
||||
|
||||
@@ -482,10 +482,8 @@ func (gwr *GatewayAPIRouter) getSessionAffinityRouteRules(canary *flaggerv1.Cana
|
||||
ResponseHeaderModifier: &v1.HTTPHeaderFilter{
|
||||
Add: []v1.HTTPHeader{
|
||||
{
|
||||
Name: setCookieHeader,
|
||||
Value: fmt.Sprintf("%s; %s=%d", canary.Status.SessionAffinityCookie, maxAgeAttr,
|
||||
canary.Spec.Analysis.SessionAffinity.GetMaxAge(),
|
||||
),
|
||||
Name: setCookieHeader,
|
||||
Value: canary.Spec.Analysis.SessionAffinity.BuildCookie(canary.Status.SessionAffinityCookie),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -99,8 +99,14 @@ func TestGatewayAPIRouter_Routes(t *testing.T) {
|
||||
cookieKey := "flagger-cookie"
|
||||
// enable session affinity and start canary run
|
||||
canary.Spec.Analysis.SessionAffinity = &flaggerv1.SessionAffinity{
|
||||
CookieName: cookieKey,
|
||||
MaxAge: 300,
|
||||
CookieName: cookieKey,
|
||||
Domain: "flagger.app",
|
||||
HttpOnly: true,
|
||||
MaxAge: 300,
|
||||
Partitioned: true,
|
||||
Path: "/app",
|
||||
SameSite: "Strict",
|
||||
Secure: true,
|
||||
}
|
||||
_, pSvcName, cSvcName := canary.GetServiceNames()
|
||||
|
||||
@@ -137,10 +143,18 @@ func TestGatewayAPIRouter_Routes(t *testing.T) {
|
||||
if string(backendRef.Name) == cSvcName {
|
||||
found = true
|
||||
filter := backendRef.Filters[0]
|
||||
val := filter.ResponseHeaderModifier.Add[0].Value
|
||||
assert.Equal(t, filter.Type, v1.HTTPRouteFilterResponseHeaderModifier)
|
||||
assert.NotNil(t, filter.ResponseHeaderModifier)
|
||||
assert.Equal(t, string(filter.ResponseHeaderModifier.Add[0].Name), setCookieHeader)
|
||||
assert.Equal(t, filter.ResponseHeaderModifier.Add[0].Value, fmt.Sprintf("%s; %s=%d", canary.Status.SessionAffinityCookie, maxAgeAttr, 300))
|
||||
assert.True(t, strings.HasPrefix(val, cookieKey))
|
||||
assert.True(t, strings.Contains(val, "Domain=flagger.app"))
|
||||
assert.True(t, strings.Contains(val, "HttpOnly"))
|
||||
assert.True(t, strings.Contains(val, "Max-Age=300"))
|
||||
assert.True(t, strings.Contains(val, "Partitioned"))
|
||||
assert.True(t, strings.Contains(val, "Path=/app"))
|
||||
assert.True(t, strings.Contains(val, "SameSite=Strict"))
|
||||
assert.True(t, strings.Contains(val, "Secure"))
|
||||
assert.Equal(t, *backendRef.Weight, int32(10))
|
||||
}
|
||||
if string(backendRef.Name) == pSvcName {
|
||||
@@ -193,10 +207,18 @@ func TestGatewayAPIRouter_Routes(t *testing.T) {
|
||||
if string(backendRef.Name) == cSvcName {
|
||||
found = true
|
||||
filter := backendRef.Filters[0]
|
||||
val := filter.ResponseHeaderModifier.Add[0].Value
|
||||
assert.Equal(t, filter.Type, v1.HTTPRouteFilterResponseHeaderModifier)
|
||||
assert.NotNil(t, filter.ResponseHeaderModifier)
|
||||
assert.Equal(t, string(filter.ResponseHeaderModifier.Add[0].Name), setCookieHeader)
|
||||
assert.Equal(t, filter.ResponseHeaderModifier.Add[0].Value, fmt.Sprintf("%s; %s=%d", canary.Status.SessionAffinityCookie, maxAgeAttr, 300))
|
||||
assert.True(t, strings.HasPrefix(val, cookieKey))
|
||||
assert.True(t, strings.Contains(val, "Domain=flagger.app"))
|
||||
assert.True(t, strings.Contains(val, "HttpOnly"))
|
||||
assert.True(t, strings.Contains(val, "Max-Age=300"))
|
||||
assert.True(t, strings.Contains(val, "Partitioned"))
|
||||
assert.True(t, strings.Contains(val, "Path=/app"))
|
||||
assert.True(t, strings.Contains(val, "SameSite=Strict"))
|
||||
assert.True(t, strings.Contains(val, "Secure"))
|
||||
|
||||
assert.Equal(t, *backendRef.Weight, int32(50))
|
||||
}
|
||||
|
||||
@@ -423,10 +423,8 @@ func (gwr *GatewayAPIV1Beta1Router) getSessionAffinityRouteRules(canary *flagger
|
||||
ResponseHeaderModifier: &v1beta1.HTTPHeaderFilter{
|
||||
Add: []v1beta1.HTTPHeader{
|
||||
{
|
||||
Name: setCookieHeader,
|
||||
Value: fmt.Sprintf("%s; %s=%d", canary.Status.SessionAffinityCookie, maxAgeAttr,
|
||||
canary.Spec.Analysis.SessionAffinity.GetMaxAge(),
|
||||
),
|
||||
Name: setCookieHeader,
|
||||
Value: canary.Spec.Analysis.SessionAffinity.BuildCookie(canary.Status.SessionAffinityCookie),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -96,8 +96,14 @@ func TestGatewayAPIV1Beta1Router_Routes(t *testing.T) {
|
||||
cookieKey := "flagger-cookie"
|
||||
// enable session affinity and start canary run
|
||||
canary.Spec.Analysis.SessionAffinity = &flaggerv1.SessionAffinity{
|
||||
CookieName: cookieKey,
|
||||
MaxAge: 300,
|
||||
CookieName: cookieKey,
|
||||
Domain: "flagger.app",
|
||||
HttpOnly: true,
|
||||
MaxAge: 300,
|
||||
Partitioned: true,
|
||||
Path: "/app",
|
||||
SameSite: "Strict",
|
||||
Secure: true,
|
||||
}
|
||||
_, pSvcName, cSvcName := canary.GetServiceNames()
|
||||
|
||||
@@ -133,10 +139,18 @@ func TestGatewayAPIV1Beta1Router_Routes(t *testing.T) {
|
||||
if string(backendRef.Name) == cSvcName {
|
||||
found = true
|
||||
filter := backendRef.Filters[0]
|
||||
val := filter.ResponseHeaderModifier.Add[0].Value
|
||||
assert.Equal(t, filter.Type, v1beta1.HTTPRouteFilterResponseHeaderModifier)
|
||||
assert.NotNil(t, filter.ResponseHeaderModifier)
|
||||
assert.Equal(t, string(filter.ResponseHeaderModifier.Add[0].Name), setCookieHeader)
|
||||
assert.Equal(t, filter.ResponseHeaderModifier.Add[0].Value, fmt.Sprintf("%s; %s=%d", canary.Status.SessionAffinityCookie, maxAgeAttr, 300))
|
||||
assert.True(t, strings.HasPrefix(val, cookieKey))
|
||||
assert.True(t, strings.Contains(val, "Domain=flagger.app"))
|
||||
assert.True(t, strings.Contains(val, "HttpOnly"))
|
||||
assert.True(t, strings.Contains(val, "Max-Age=300"))
|
||||
assert.True(t, strings.Contains(val, "Partitioned"))
|
||||
assert.True(t, strings.Contains(val, "Path=/app"))
|
||||
assert.True(t, strings.Contains(val, "SameSite=Strict"))
|
||||
assert.True(t, strings.Contains(val, "Secure"))
|
||||
assert.Equal(t, *backendRef.Weight, int32(10))
|
||||
}
|
||||
if string(backendRef.Name) == pSvcName {
|
||||
@@ -189,10 +203,18 @@ func TestGatewayAPIV1Beta1Router_Routes(t *testing.T) {
|
||||
if string(backendRef.Name) == cSvcName {
|
||||
found = true
|
||||
filter := backendRef.Filters[0]
|
||||
val := filter.ResponseHeaderModifier.Add[0].Value
|
||||
assert.Equal(t, filter.Type, v1beta1.HTTPRouteFilterResponseHeaderModifier)
|
||||
assert.NotNil(t, filter.ResponseHeaderModifier)
|
||||
assert.Equal(t, string(filter.ResponseHeaderModifier.Add[0].Name), setCookieHeader)
|
||||
assert.Equal(t, filter.ResponseHeaderModifier.Add[0].Value, fmt.Sprintf("%s; %s=%d", canary.Status.SessionAffinityCookie, maxAgeAttr, 300))
|
||||
assert.True(t, strings.HasPrefix(val, cookieKey))
|
||||
assert.True(t, strings.Contains(val, "Domain=flagger.app"))
|
||||
assert.True(t, strings.Contains(val, "HttpOnly"))
|
||||
assert.True(t, strings.Contains(val, "Max-Age=300"))
|
||||
assert.True(t, strings.Contains(val, "Partitioned"))
|
||||
assert.True(t, strings.Contains(val, "Path=/app"))
|
||||
assert.True(t, strings.Contains(val, "SameSite=Strict"))
|
||||
assert.True(t, strings.Contains(val, "Secure"))
|
||||
|
||||
assert.Equal(t, *backendRef.Weight, int32(50))
|
||||
}
|
||||
|
||||
@@ -532,9 +532,7 @@ func (ir *IstioRouter) SetRoutes(
|
||||
}
|
||||
}
|
||||
routeDest.Headers.Response.Add = map[string]string{
|
||||
setCookieHeader: fmt.Sprintf("%s; %s=%d", canary.Status.SessionAffinityCookie, maxAgeAttr,
|
||||
canary.Spec.Analysis.SessionAffinity.GetMaxAge(),
|
||||
),
|
||||
setCookieHeader: canary.Spec.Analysis.SessionAffinity.BuildCookie(canary.Status.SessionAffinityCookie),
|
||||
}
|
||||
}
|
||||
weightedRoute.Route[i] = routeDest
|
||||
|
||||
@@ -190,8 +190,14 @@ func TestIstioRouter_SetRoutes(t *testing.T) {
|
||||
cookieKey := "flagger-cookie"
|
||||
// enable session affinity and start canary run
|
||||
canary.Spec.Analysis.SessionAffinity = &v1beta1.SessionAffinity{
|
||||
CookieName: cookieKey,
|
||||
MaxAge: 300,
|
||||
CookieName: cookieKey,
|
||||
Domain: "flagger.app",
|
||||
HttpOnly: true,
|
||||
MaxAge: 300,
|
||||
Partitioned: true,
|
||||
Path: "/app",
|
||||
SameSite: "Strict",
|
||||
Secure: true,
|
||||
}
|
||||
err := router.SetRoutes(canary, 0, 10, false)
|
||||
|
||||
@@ -231,7 +237,13 @@ func TestIstioRouter_SetRoutes(t *testing.T) {
|
||||
val, ok := routeDest.Headers.Response.Add[setCookieHeader]
|
||||
assert.True(t, ok)
|
||||
assert.True(t, strings.HasPrefix(val, cookieKey))
|
||||
assert.True(t, strings.Contains(val, "Domain=flagger.app"))
|
||||
assert.True(t, strings.Contains(val, "HttpOnly"))
|
||||
assert.True(t, strings.Contains(val, "Max-Age=300"))
|
||||
assert.True(t, strings.Contains(val, "Partitioned"))
|
||||
assert.True(t, strings.Contains(val, "Path=/app"))
|
||||
assert.True(t, strings.Contains(val, "SameSite=Strict"))
|
||||
assert.True(t, strings.Contains(val, "Secure"))
|
||||
}
|
||||
}
|
||||
assert.True(t, strings.HasPrefix(canary.Status.SessionAffinityCookie, cookieKey))
|
||||
@@ -286,7 +298,13 @@ func TestIstioRouter_SetRoutes(t *testing.T) {
|
||||
val, ok := routeDest.Headers.Response.Add[setCookieHeader]
|
||||
assert.True(t, ok)
|
||||
assert.True(t, strings.HasPrefix(val, cookieKey))
|
||||
assert.True(t, strings.Contains(val, "Domain=flagger.app"))
|
||||
assert.True(t, strings.Contains(val, "HttpOnly"))
|
||||
assert.True(t, strings.Contains(val, "Max-Age=300"))
|
||||
assert.True(t, strings.Contains(val, "Partitioned"))
|
||||
assert.True(t, strings.Contains(val, "Path=/app"))
|
||||
assert.True(t, strings.Contains(val, "SameSite=Strict"))
|
||||
assert.True(t, strings.Contains(val, "Secure"))
|
||||
}
|
||||
}
|
||||
assert.True(t, strings.HasPrefix(canary.Status.SessionAffinityCookie, cookieKey))
|
||||
|
||||
Reference in New Issue
Block a user