Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions artifacts/flagger/crd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions charts/flagger/crds/crd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
35 changes: 35 additions & 0 deletions docs/gitbook/usage/deployment-strategies.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
22 changes: 22 additions & 0 deletions kustomize/base/flagger/crd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
54 changes: 53 additions & 1 deletion pkg/apis/flagger/v1beta1/canary.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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
}
6 changes: 2 additions & 4 deletions pkg/router/gateway_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
},
},
},
Expand Down
30 changes: 26 additions & 4 deletions pkg/router/gateway_api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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))
}
Expand Down
6 changes: 2 additions & 4 deletions pkg/router/gateway_api_v1beta1.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
},
},
},
Expand Down
30 changes: 26 additions & 4 deletions pkg/router/gateway_api_v1beta1_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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))
}
Expand Down
4 changes: 1 addition & 3 deletions pkg/router/istio.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading