mirror of
https://github.com/fluxcd/flagger.git
synced 2026-02-14 18:10:00 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4cc9bf616 | ||
|
|
ca3fd315cd | ||
|
|
d9d910b0cf | ||
|
|
9090802dcc | ||
|
|
70c4c528ed | ||
|
|
3ce87af077 | ||
|
|
1709b076e0 | ||
|
|
f3b240ca82 | ||
|
|
4821a687c1 | ||
|
|
931dd7fa6b | ||
|
|
8018353d54 | ||
|
|
ffddfd9c24 |
@@ -1250,6 +1250,9 @@ spec:
|
||||
sessionAffinityCookie:
|
||||
description: Session affinity cookie of the current canary run
|
||||
type: string
|
||||
primarySessionAffinityCookie:
|
||||
description: Primary session affinity cookie of the current canary run
|
||||
type: string
|
||||
previousSessionAffinityCookie:
|
||||
description: Session affinity cookie of the previous canary run
|
||||
type: string
|
||||
|
||||
@@ -1250,6 +1250,9 @@ spec:
|
||||
sessionAffinityCookie:
|
||||
description: Session affinity cookie of the current canary run
|
||||
type: string
|
||||
primarySessionAffinityCookie:
|
||||
description: Primary session affinity cookie of the current canary run
|
||||
type: string
|
||||
previousSessionAffinityCookie:
|
||||
description: Session affinity cookie of the previous canary run
|
||||
type: string
|
||||
|
||||
@@ -404,6 +404,9 @@ You can load `app.example.com` in your browser and refresh it until you see the
|
||||
All subsequent requests after that will be served by `podinfo:6.0.1` and not `podinfo:6.0.0` because of the session affinity
|
||||
configured by Flagger with Istio.
|
||||
|
||||
To configure stickiness for the Primary deployment to ensure fair weighted traffic routing, please
|
||||
checkout the [deployment strategies docs](../usage/deployment-strategies.md#canary-release-with-session-affinity).
|
||||
|
||||
## Traffic mirroring
|
||||
|
||||

|
||||
|
||||
@@ -474,7 +474,7 @@ can also configure stickiness for the Primary deployment. You can configure this
|
||||
primaryCookieName: primary-flagger-cookie
|
||||
```
|
||||
|
||||
> Note: This is only supported for the Gateway API provider for now.
|
||||
> Note: This is only supported for the Gateway API and Istio providers for now.
|
||||
|
||||
Let's understand what the above configuration does. All the session affinity stuff in the above section
|
||||
still occurs, but now the response header for requests routed to the primary deployment also include a
|
||||
|
||||
16
go.mod
16
go.mod
@@ -18,7 +18,7 @@ require (
|
||||
github.com/signalfx/signalfx-go v1.53.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/sync v0.17.0
|
||||
golang.org/x/sync v0.18.0
|
||||
google.golang.org/api v0.252.0
|
||||
google.golang.org/genproto v0.0.0-20250603155806-513f23925822
|
||||
google.golang.org/grpc v1.76.0
|
||||
@@ -83,15 +83,15 @@ require (
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.42.0 // indirect
|
||||
golang.org/x/mod v0.28.0 // indirect
|
||||
golang.org/x/net v0.44.0 // indirect
|
||||
golang.org/x/crypto v0.45.0 // indirect
|
||||
golang.org/x/mod v0.29.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/oauth2 v0.31.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/term v0.35.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/term v0.37.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
golang.org/x/time v0.13.0 // indirect
|
||||
golang.org/x/tools v0.37.0 // indirect
|
||||
golang.org/x/tools v0.38.0 // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797 // indirect
|
||||
|
||||
32
go.sum
32
go.sum
@@ -203,44 +203,44 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
||||
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
||||
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo=
|
||||
golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
|
||||
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
|
||||
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
||||
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
@@ -1250,6 +1250,9 @@ spec:
|
||||
sessionAffinityCookie:
|
||||
description: Session affinity cookie of the current canary run
|
||||
type: string
|
||||
primarySessionAffinityCookie:
|
||||
description: Primary session affinity cookie of the current canary run
|
||||
type: string
|
||||
previousSessionAffinityCookie:
|
||||
description: Session affinity cookie of the previous canary run
|
||||
type: string
|
||||
|
||||
@@ -706,9 +706,9 @@ func (c *Canary) DeploymentStrategy() string {
|
||||
}
|
||||
|
||||
// BuildCookie returns the cookie that should be used as the value of a Set-Cookie header
|
||||
func (s *SessionAffinity) BuildCookie(cookieName string) string {
|
||||
func (s *SessionAffinity) BuildCookie(cookieName string, maxAge int) string {
|
||||
cookie := fmt.Sprintf("%s; %s=%d", cookieName, "Max-Age",
|
||||
s.GetMaxAge(),
|
||||
maxAge,
|
||||
)
|
||||
|
||||
if s.Domain != "" {
|
||||
|
||||
@@ -78,6 +78,8 @@ type CanaryStatus struct {
|
||||
// +optional
|
||||
SessionAffinityCookie string `json:"sessionAffinityCookie,omitempty"`
|
||||
// +optional
|
||||
PrimarySessionAffinityCookie string `json:"primarySessionAffinityCookie,omitempty"`
|
||||
// +optional
|
||||
TrackedConfigs *map[string]string `json:"trackedConfigs,omitempty"`
|
||||
// +optional
|
||||
LastAppliedSpec string `json:"lastAppliedSpec,omitempty"`
|
||||
|
||||
@@ -131,7 +131,7 @@ func (p *DatadogProvider) RunQuery(query string) (float64, error) {
|
||||
}
|
||||
|
||||
if r.StatusCode != http.StatusOK {
|
||||
return 0, fmt.Errorf("error response: %s: %w", string(b), err)
|
||||
return 0, fmt.Errorf("error response: %s: %s", string(b), r.Status)
|
||||
}
|
||||
|
||||
var res datadogResponse
|
||||
|
||||
@@ -492,7 +492,10 @@ func (gwr *GatewayAPIRouter) getSessionAffinityRouteRules(canary *flaggerv1.Cana
|
||||
if canary.Status.SessionAffinityCookie == "" {
|
||||
canary.Status.SessionAffinityCookie = fmt.Sprintf("%s=%s", canary.Spec.Analysis.SessionAffinity.CookieName, randSeq())
|
||||
}
|
||||
primaryCookie := fmt.Sprintf("%s=%s", canary.Spec.Analysis.SessionAffinity.PrimaryCookieName, randSeq())
|
||||
// if the status doesn't have the primary cookie, then generate a new primary cookie.
|
||||
if canary.Status.PrimarySessionAffinityCookie == "" {
|
||||
canary.Status.PrimarySessionAffinityCookie = fmt.Sprintf("%s=%s", canary.Spec.Analysis.SessionAffinity.PrimaryCookieName, randSeq())
|
||||
}
|
||||
|
||||
// add response modifier to the canary backend ref in the rule that does weighted routing
|
||||
// to include the canary cookie.
|
||||
@@ -503,7 +506,7 @@ func (gwr *GatewayAPIRouter) getSessionAffinityRouteRules(canary *flaggerv1.Cana
|
||||
Add: []v1.HTTPHeader{
|
||||
{
|
||||
Name: setCookieHeader,
|
||||
Value: canary.Spec.Analysis.SessionAffinity.BuildCookie(canary.Status.SessionAffinityCookie),
|
||||
Value: canary.Spec.Analysis.SessionAffinity.BuildCookie(canary.Status.SessionAffinityCookie, canary.Spec.Analysis.SessionAffinity.GetMaxAge()),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -522,10 +525,8 @@ func (gwr *GatewayAPIRouter) getSessionAffinityRouteRules(canary *flaggerv1.Cana
|
||||
ResponseHeaderModifier: &v1.HTTPHeaderFilter{
|
||||
Add: []v1.HTTPHeader{
|
||||
{
|
||||
Name: setCookieHeader,
|
||||
Value: fmt.Sprintf("%s; %s=%d", primaryCookie, maxAgeAttr,
|
||||
int(interval.Seconds()),
|
||||
),
|
||||
Name: setCookieHeader,
|
||||
Value: canary.Spec.Analysis.SessionAffinity.BuildCookie(canary.Status.PrimarySessionAffinityCookie, int(interval.Seconds())),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -566,7 +567,7 @@ func (gwr *GatewayAPIRouter) getSessionAffinityRouteRules(canary *flaggerv1.Cana
|
||||
// primary cookie and send them to the primary backend, only if a primary cookie name has
|
||||
// been specified.
|
||||
if canary.Spec.Analysis.SessionAffinity.PrimaryCookieName != "" {
|
||||
cookieKeyAndVal = strings.Split(primaryCookie, "=")
|
||||
cookieKeyAndVal = strings.Split(canary.Status.PrimarySessionAffinityCookie, "=")
|
||||
regexMatchType = v1.HeaderMatchRegularExpression
|
||||
primaryCookieMatch := v1.HTTPRouteMatch{
|
||||
Headers: []v1.HTTPHeaderMatch{
|
||||
|
||||
@@ -424,7 +424,7 @@ func (gwr *GatewayAPIV1Beta1Router) getSessionAffinityRouteRules(canary *flagger
|
||||
Add: []v1beta1.HTTPHeader{
|
||||
{
|
||||
Name: setCookieHeader,
|
||||
Value: canary.Spec.Analysis.SessionAffinity.BuildCookie(canary.Status.SessionAffinityCookie),
|
||||
Value: canary.Spec.Analysis.SessionAffinity.BuildCookie(canary.Status.SessionAffinityCookie, canary.Spec.Analysis.SessionAffinity.GetMaxAge()),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -49,7 +49,6 @@ type IstioRouter struct {
|
||||
|
||||
const cookieHeader = "Cookie"
|
||||
const setCookieHeader = "Set-Cookie"
|
||||
const stickyRouteName = "sticky-route"
|
||||
const maxAgeAttr = "Max-Age"
|
||||
|
||||
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
@@ -182,8 +181,8 @@ func (ir *IstioRouter) reconcileVirtualService(canary *flaggerv1.Canary) error {
|
||||
|
||||
// create destinations with primary weight 100% and canary weight 0%
|
||||
canaryRoute := []istiov1beta1.HTTPRouteDestination{
|
||||
makeDestination(canary, primaryName, 100),
|
||||
makeDestination(canary, canaryName, 0),
|
||||
ir.makeDestination(canary, primaryName, 100),
|
||||
ir.makeDestination(canary, canaryName, 0),
|
||||
}
|
||||
|
||||
if canary.Spec.Service.Delegation {
|
||||
@@ -255,7 +254,7 @@ func (ir *IstioRouter) reconcileVirtualService(canary *flaggerv1.Canary) error {
|
||||
CorsPolicy: canary.Spec.Service.CorsPolicy,
|
||||
Headers: canary.Spec.Service.Headers,
|
||||
Route: []istiov1beta1.HTTPRouteDestination{
|
||||
makeDestination(canary, primaryName, 100),
|
||||
ir.makeDestination(canary, primaryName, 100),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -304,15 +303,21 @@ func (ir *IstioRouter) reconcileVirtualService(canary *flaggerv1.Canary) error {
|
||||
cmpopts.IgnoreFields(istiov1beta1.HTTPRoute{}, "Mirror", "MirrorPercentage"),
|
||||
}
|
||||
if canary.Spec.Analysis.SessionAffinity != nil {
|
||||
// We ignore this route as this does not do weighted routing and is handled exclusively
|
||||
// by SetRoutes().
|
||||
ignoreSlice := cmpopts.IgnoreSliceElements(func(t istiov1beta1.HTTPRoute) bool {
|
||||
if t.Name == stickyRouteName {
|
||||
return true
|
||||
ignoreCookieRouteFunc := func(name string) func(r istiov1beta1.HTTPRoute) bool {
|
||||
return func(r istiov1beta1.HTTPRoute) bool {
|
||||
// Ignore the rule that does sticky routing, i.e. matches against the `Cookie` header.
|
||||
for _, match := range r.Match {
|
||||
if strings.Contains(match.Headers[cookieHeader].Regex, name) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
return false
|
||||
})
|
||||
ignoreCmpOptions = append(ignoreCmpOptions, ignoreSlice)
|
||||
}
|
||||
ignoreCanaryRoute := cmpopts.IgnoreSliceElements(ignoreCookieRouteFunc(canary.Spec.Analysis.SessionAffinity.CookieName))
|
||||
ignorePrimaryRoute := cmpopts.IgnoreSliceElements(ignoreCookieRouteFunc(canary.Spec.Analysis.SessionAffinity.PrimaryCookieName))
|
||||
|
||||
ignoreCmpOptions = append(ignoreCmpOptions, ignoreCanaryRoute, ignorePrimaryRoute)
|
||||
ignoreCmpOptions = append(ignoreCmpOptions, cmpopts.IgnoreFields(istiov1beta1.HTTPRouteDestination{}, "Headers"))
|
||||
}
|
||||
if v, ok := virtualService.Annotations[kubectlAnnotation]; ok {
|
||||
@@ -481,8 +486,8 @@ func (ir *IstioRouter) SetRoutes(
|
||||
weightedRoute := istiov1beta1.TCPRoute{
|
||||
Match: canaryToL4Match(canary),
|
||||
Route: []istiov1beta1.HTTPRouteDestination{
|
||||
makeDestination(canary, primaryName, primaryWeight),
|
||||
makeDestination(canary, canaryName, canaryWeight),
|
||||
ir.makeDestination(canary, primaryName, primaryWeight),
|
||||
ir.makeDestination(canary, canaryName, canaryWeight),
|
||||
},
|
||||
}
|
||||
vsCopy.Spec.Tcp = []istiov1beta1.TCPRoute{
|
||||
@@ -497,7 +502,7 @@ func (ir *IstioRouter) SetRoutes(
|
||||
}
|
||||
|
||||
// weighted routing (progressive canary)
|
||||
weightedRoute := istiov1beta1.HTTPRoute{
|
||||
weightedRoute := &istiov1beta1.HTTPRoute{
|
||||
Match: canary.Spec.Service.Match,
|
||||
Rewrite: canary.Spec.Service.GetIstioRewrite(),
|
||||
Timeout: canary.Spec.Service.Timeout,
|
||||
@@ -505,94 +510,20 @@ func (ir *IstioRouter) SetRoutes(
|
||||
CorsPolicy: canary.Spec.Service.CorsPolicy,
|
||||
Headers: canary.Spec.Service.Headers,
|
||||
Route: []istiov1beta1.HTTPRouteDestination{
|
||||
makeDestination(canary, primaryName, primaryWeight),
|
||||
makeDestination(canary, canaryName, canaryWeight),
|
||||
ir.makeDestination(canary, primaryName, primaryWeight),
|
||||
ir.makeDestination(canary, canaryName, canaryWeight),
|
||||
},
|
||||
}
|
||||
vsCopy.Spec.Http = []istiov1beta1.HTTPRoute{
|
||||
weightedRoute,
|
||||
*weightedRoute,
|
||||
}
|
||||
|
||||
if canary.Spec.Analysis.SessionAffinity != nil {
|
||||
// If a canary run is active, we want all responses corresponding to requests hitting the canary deployment
|
||||
// (due to weighted routing) to include a `Set-Cookie` header. All requests that have the `Cookie` header
|
||||
// and match the value of the `Set-Cookie` header will be routed to the canary deployment.
|
||||
stickyRoute := weightedRoute
|
||||
stickyRoute.Name = stickyRouteName
|
||||
if canaryWeight != 0 {
|
||||
if canary.Status.SessionAffinityCookie == "" {
|
||||
canary.Status.SessionAffinityCookie = fmt.Sprintf("%s=%s", canary.Spec.Analysis.SessionAffinity.CookieName, randSeq())
|
||||
}
|
||||
|
||||
for i, routeDest := range weightedRoute.Route {
|
||||
if routeDest.Destination.Host == canaryName {
|
||||
if routeDest.Headers == nil {
|
||||
routeDest.Headers = &istiov1beta1.Headers{
|
||||
Response: &istiov1beta1.HeaderOperations{},
|
||||
}
|
||||
}
|
||||
routeDest.Headers.Response.Add = map[string]string{
|
||||
setCookieHeader: canary.Spec.Analysis.SessionAffinity.BuildCookie(canary.Status.SessionAffinityCookie),
|
||||
}
|
||||
}
|
||||
weightedRoute.Route[i] = routeDest
|
||||
}
|
||||
|
||||
cookieKeyAndVal := strings.Split(canary.Status.SessionAffinityCookie, "=")
|
||||
cookieMatch := istiov1beta1.HTTPMatchRequest{
|
||||
Headers: map[string]istiov1alpha1.StringMatch{
|
||||
cookieHeader: {
|
||||
Regex: fmt.Sprintf(".*%s.*%s.*", cookieKeyAndVal[0], cookieKeyAndVal[1]),
|
||||
},
|
||||
},
|
||||
}
|
||||
canaryMatch := mergeMatchConditions([]istiov1beta1.HTTPMatchRequest{cookieMatch}, canary.Spec.Service.Match)
|
||||
stickyRoute.Match = canaryMatch
|
||||
stickyRoute.Route = []istiov1beta1.HTTPRouteDestination{
|
||||
makeDestination(canary, primaryName, 0),
|
||||
makeDestination(canary, canaryName, 100),
|
||||
}
|
||||
} else {
|
||||
// If canary weight is 0 and SessionAffinityCookie is non-blank, then it belongs to a previous canary run.
|
||||
if canary.Status.SessionAffinityCookie != "" {
|
||||
canary.Status.PreviousSessionAffinityCookie = canary.Status.SessionAffinityCookie
|
||||
}
|
||||
previousCookie := canary.Status.PreviousSessionAffinityCookie
|
||||
|
||||
// Match against the previous session cookie and delete that cookie
|
||||
if previousCookie != "" {
|
||||
cookieKeyAndVal := strings.Split(previousCookie, "=")
|
||||
cookieMatch := istiov1beta1.HTTPMatchRequest{
|
||||
Headers: map[string]istiov1alpha1.StringMatch{
|
||||
cookieHeader: {
|
||||
Regex: fmt.Sprintf(".*%s.*%s.*", cookieKeyAndVal[0], cookieKeyAndVal[1]),
|
||||
},
|
||||
},
|
||||
}
|
||||
canaryMatch := mergeMatchConditions([]istiov1beta1.HTTPMatchRequest{cookieMatch}, canary.Spec.Service.Match)
|
||||
stickyRoute.Match = canaryMatch
|
||||
|
||||
if stickyRoute.Headers == nil {
|
||||
stickyRoute.Headers = &istiov1beta1.Headers{
|
||||
Response: &istiov1beta1.HeaderOperations{
|
||||
Add: map[string]string{},
|
||||
},
|
||||
}
|
||||
} else if stickyRoute.Headers.Response == nil {
|
||||
stickyRoute.Headers.Response = &istiov1beta1.HeaderOperations{
|
||||
Add: map[string]string{},
|
||||
}
|
||||
} else if stickyRoute.Headers.Response.Add == nil {
|
||||
stickyRoute.Headers.Response.Add = map[string]string{}
|
||||
}
|
||||
stickyRoute.Headers.Response.Add[setCookieHeader] = fmt.Sprintf("%s; %s=%d", previousCookie, maxAgeAttr, -1)
|
||||
}
|
||||
|
||||
canary.Status.SessionAffinityCookie = ""
|
||||
}
|
||||
vsCopy.Spec.Http = []istiov1beta1.HTTPRoute{
|
||||
stickyRoute, weightedRoute,
|
||||
rules, err := ir.getSessionAffinityRouteRules(canary, canaryWeight, weightedRoute)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
vsCopy.Spec.Http = rules
|
||||
}
|
||||
|
||||
if mirrored {
|
||||
@@ -618,8 +549,8 @@ func (ir *IstioRouter) SetRoutes(
|
||||
CorsPolicy: canary.Spec.Service.CorsPolicy,
|
||||
Headers: canary.Spec.Service.Headers,
|
||||
Route: []istiov1beta1.HTTPRouteDestination{
|
||||
makeDestination(canary, primaryName, primaryWeight),
|
||||
makeDestination(canary, canaryName, canaryWeight),
|
||||
ir.makeDestination(canary, primaryName, primaryWeight),
|
||||
ir.makeDestination(canary, canaryName, canaryWeight),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -630,7 +561,7 @@ func (ir *IstioRouter) SetRoutes(
|
||||
CorsPolicy: canary.Spec.Service.CorsPolicy,
|
||||
Headers: canary.Spec.Service.Headers,
|
||||
Route: []istiov1beta1.HTTPRouteDestination{
|
||||
makeDestination(canary, primaryName, primaryWeight),
|
||||
ir.makeDestination(canary, primaryName, primaryWeight),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -705,7 +636,7 @@ func mergeMatchConditions(canary, defaults []istiov1beta1.HTTPMatchRequest) []is
|
||||
}
|
||||
|
||||
// makeDestination returns a an destination weight for the specified host
|
||||
func makeDestination(canary *flaggerv1.Canary, host string, weight int) istiov1beta1.HTTPRouteDestination {
|
||||
func (ir *IstioRouter) makeDestination(canary *flaggerv1.Canary, host string, weight int) istiov1beta1.HTTPRouteDestination {
|
||||
dest := istiov1beta1.HTTPRouteDestination{
|
||||
Destination: istiov1beta1.Destination{
|
||||
Host: host,
|
||||
@@ -740,3 +671,144 @@ func randSeq() string {
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func getRouteByServiceName(rule *istiov1beta1.HTTPRoute, svcName string) *istiov1beta1.HTTPRouteDestination {
|
||||
for i, routeDest := range rule.Route {
|
||||
if routeDest.Destination.Host == svcName {
|
||||
return &rule.Route[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getSessionAffinityRouteRules returns the HTTPRoute objects required to perform
|
||||
// session affinity based Canary releases.
|
||||
func (ir *IstioRouter) getSessionAffinityRouteRules(canary *flaggerv1.Canary, canaryWeight int,
|
||||
weightedRoute *istiov1beta1.HTTPRoute) ([]istiov1beta1.HTTPRoute, error) {
|
||||
_, primaryName, canaryName := canary.GetServiceNames()
|
||||
stickyCanaryRoute := *weightedRoute
|
||||
stickyPrimaryRoute := *weightedRoute
|
||||
|
||||
// If a canary run is active, we want all responses corresponding to requests hitting the canary deployment
|
||||
// (due to weighted routing) to include a `Set-Cookie` header. All requests that have the `Cookie` header
|
||||
// and match the value of the `Set-Cookie` header will be routed to the canary deployment.
|
||||
if canaryWeight != 0 {
|
||||
// if the status doesn't have the canary cookie, then generate a new canary cookie.
|
||||
if canary.Status.SessionAffinityCookie == "" {
|
||||
canary.Status.SessionAffinityCookie = fmt.Sprintf("%s=%s", canary.Spec.Analysis.SessionAffinity.CookieName, randSeq())
|
||||
}
|
||||
// if the status doesn't have the primary cookie, then generate a new primary cookie.
|
||||
if canary.Status.PrimarySessionAffinityCookie == "" {
|
||||
canary.Status.PrimarySessionAffinityCookie = fmt.Sprintf("%s=%s", canary.Spec.Analysis.SessionAffinity.PrimaryCookieName, randSeq())
|
||||
}
|
||||
|
||||
// add response modifier to the canary backend route in the rule that does weighted routing
|
||||
// to include the canary cookie.
|
||||
canaryBackendRoute := getRouteByServiceName(weightedRoute, canaryName)
|
||||
if canaryBackendRoute.Headers == nil {
|
||||
canaryBackendRoute.Headers = &istiov1beta1.Headers{
|
||||
Response: &istiov1beta1.HeaderOperations{},
|
||||
}
|
||||
}
|
||||
canaryBackendRoute.Headers.Response.Add = map[string]string{
|
||||
setCookieHeader: canary.Spec.Analysis.SessionAffinity.BuildCookie(canary.Status.SessionAffinityCookie, canary.Spec.Analysis.SessionAffinity.GetMaxAge()),
|
||||
}
|
||||
|
||||
// add response modifier to the primary backend route in the rule that does weighted routing
|
||||
// to include the primary cookie, only if a primary cookie name has been specified.
|
||||
if canary.Spec.Analysis.SessionAffinity.PrimaryCookieName != "" {
|
||||
primaryBackendRoute := getRouteByServiceName(weightedRoute, primaryName)
|
||||
interval, err := time.ParseDuration(canary.Spec.Analysis.Interval)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse canary interval: %w", err)
|
||||
}
|
||||
if primaryBackendRoute.Headers == nil {
|
||||
primaryBackendRoute.Headers = &istiov1beta1.Headers{
|
||||
Response: &istiov1beta1.HeaderOperations{},
|
||||
}
|
||||
}
|
||||
primaryBackendRoute.Headers.Response.Add = map[string]string{
|
||||
setCookieHeader: canary.Spec.Analysis.SessionAffinity.BuildCookie(canary.Status.PrimarySessionAffinityCookie, int(interval.Seconds())),
|
||||
}
|
||||
}
|
||||
|
||||
// configure the sticky canary rule to match against requests that match against the
|
||||
// canary cookie and send them to the canary backend.
|
||||
cookieKeyAndVal := strings.Split(canary.Status.SessionAffinityCookie, "=")
|
||||
cookieMatch := istiov1beta1.HTTPMatchRequest{
|
||||
Headers: map[string]istiov1alpha1.StringMatch{
|
||||
cookieHeader: {
|
||||
Regex: fmt.Sprintf(".*%s.*%s.*", cookieKeyAndVal[0], cookieKeyAndVal[1]),
|
||||
},
|
||||
},
|
||||
}
|
||||
canaryMatch := mergeMatchConditions([]istiov1beta1.HTTPMatchRequest{cookieMatch}, canary.Spec.Service.Match)
|
||||
stickyCanaryRoute.Match = canaryMatch
|
||||
stickyCanaryRoute.Route = []istiov1beta1.HTTPRouteDestination{
|
||||
ir.makeDestination(canary, primaryName, 0),
|
||||
ir.makeDestination(canary, canaryName, 100),
|
||||
}
|
||||
|
||||
// configure the sticky primary rule to match against requests that match against the
|
||||
// primary cookie and send them to the primary backend, only if a primary cookie name has
|
||||
// been specified.
|
||||
if canary.Spec.Analysis.SessionAffinity.PrimaryCookieName != "" {
|
||||
cookieKeyAndVal := strings.Split(canary.Status.PrimarySessionAffinityCookie, "=")
|
||||
primaryCookieMatch := istiov1beta1.HTTPMatchRequest{
|
||||
Headers: map[string]istiov1alpha1.StringMatch{
|
||||
cookieHeader: {
|
||||
Regex: fmt.Sprintf(".*%s.*%s.*", cookieKeyAndVal[0], cookieKeyAndVal[1]),
|
||||
},
|
||||
},
|
||||
}
|
||||
primaryMatch := mergeMatchConditions([]istiov1beta1.HTTPMatchRequest{primaryCookieMatch}, canary.Spec.Service.Match)
|
||||
stickyPrimaryRoute.Match = primaryMatch
|
||||
stickyPrimaryRoute.Route = []istiov1beta1.HTTPRouteDestination{
|
||||
ir.makeDestination(canary, primaryName, 100),
|
||||
ir.makeDestination(canary, canaryName, 0),
|
||||
}
|
||||
|
||||
return []istiov1beta1.HTTPRoute{stickyCanaryRoute, stickyPrimaryRoute, *weightedRoute}, nil
|
||||
}
|
||||
|
||||
return []istiov1beta1.HTTPRoute{stickyCanaryRoute, *weightedRoute}, nil
|
||||
} else {
|
||||
// If canary weight is 0 and SessionAffinityCookie is non-blank, then it belongs to a previous canary run.
|
||||
if canary.Status.SessionAffinityCookie != "" {
|
||||
canary.Status.PreviousSessionAffinityCookie = canary.Status.SessionAffinityCookie
|
||||
}
|
||||
previousCookie := canary.Status.PreviousSessionAffinityCookie
|
||||
|
||||
// Match against the previous session cookie and delete that cookie
|
||||
if previousCookie != "" {
|
||||
cookieKeyAndVal := strings.Split(previousCookie, "=")
|
||||
cookieMatch := istiov1beta1.HTTPMatchRequest{
|
||||
Headers: map[string]istiov1alpha1.StringMatch{
|
||||
cookieHeader: {
|
||||
Regex: fmt.Sprintf(".*%s.*%s.*", cookieKeyAndVal[0], cookieKeyAndVal[1]),
|
||||
},
|
||||
},
|
||||
}
|
||||
canaryMatch := mergeMatchConditions([]istiov1beta1.HTTPMatchRequest{cookieMatch}, canary.Spec.Service.Match)
|
||||
stickyCanaryRoute.Match = canaryMatch
|
||||
|
||||
if stickyCanaryRoute.Headers == nil {
|
||||
stickyCanaryRoute.Headers = &istiov1beta1.Headers{
|
||||
Response: &istiov1beta1.HeaderOperations{
|
||||
Add: map[string]string{},
|
||||
},
|
||||
}
|
||||
} else if stickyCanaryRoute.Headers.Response == nil {
|
||||
stickyCanaryRoute.Headers.Response = &istiov1beta1.HeaderOperations{
|
||||
Add: map[string]string{},
|
||||
}
|
||||
} else if stickyCanaryRoute.Headers.Response.Add == nil {
|
||||
stickyCanaryRoute.Headers.Response.Add = map[string]string{}
|
||||
}
|
||||
stickyCanaryRoute.Headers.Response.Add[setCookieHeader] = fmt.Sprintf("%s; %s=%d", previousCookie, maxAgeAttr, -1)
|
||||
}
|
||||
|
||||
canary.Status.SessionAffinityCookie = ""
|
||||
return []istiov1beta1.HTTPRoute{stickyCanaryRoute, *weightedRoute}, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -404,6 +405,175 @@ func TestIstioRouter_SetRoutes(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestIstioRouter_getSessionAffinityRoutes(t *testing.T) {
|
||||
mocks := newFixture(nil)
|
||||
t.Run("without primary cookie", func(t *testing.T) {
|
||||
canary := mocks.canary.DeepCopy()
|
||||
cookieKey := "flagger-cookie"
|
||||
canary.Spec.Analysis.SessionAffinity = &flaggerv1.SessionAffinity{
|
||||
CookieName: cookieKey,
|
||||
MaxAge: 300,
|
||||
}
|
||||
|
||||
router := &IstioRouter{
|
||||
logger: mocks.logger,
|
||||
flaggerClient: mocks.flaggerClient,
|
||||
istioClient: mocks.meshClient,
|
||||
kubeClient: mocks.kubeClient,
|
||||
}
|
||||
|
||||
_, pSvcName, cSvcName := canary.GetServiceNames()
|
||||
weightedRoute := &istiov1beta1.HTTPRoute{
|
||||
Route: []istiov1beta1.HTTPRouteDestination{
|
||||
router.makeDestination(canary, pSvcName, 100),
|
||||
router.makeDestination(canary, cSvcName, 0),
|
||||
},
|
||||
}
|
||||
rules, err := router.getSessionAffinityRouteRules(canary, 10, weightedRoute)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, len(rules), 2)
|
||||
assert.True(t, strings.HasPrefix(canary.Status.SessionAffinityCookie, cookieKey))
|
||||
|
||||
stickyRule := rules[0]
|
||||
cookieMatch := stickyRule.Match[0].Headers[cookieHeader]
|
||||
assert.NotNil(t, cookieMatch.Regex)
|
||||
assert.Contains(t, cookieMatch.Regex, cookieKey)
|
||||
|
||||
assert.Equal(t, len(stickyRule.Route), 2)
|
||||
for _, route := range stickyRule.Route {
|
||||
if string(route.Destination.Host) == pSvcName {
|
||||
assert.Equal(t, route.Weight, int(0))
|
||||
}
|
||||
if string(route.Destination.Host) == cSvcName {
|
||||
assert.Equal(t, route.Weight, int(100))
|
||||
}
|
||||
}
|
||||
|
||||
weightedRule := rules[1]
|
||||
var found bool
|
||||
for _, route := range weightedRule.Route {
|
||||
if string(route.Destination.Host) == cSvcName {
|
||||
found = true
|
||||
|
||||
assert.NotNil(t, route.Headers.Response.Add)
|
||||
assert.Equal(t, route.Headers.Response.Add[setCookieHeader], fmt.Sprintf("%s; %s=%d", canary.Status.SessionAffinityCookie, maxAgeAttr, 300))
|
||||
}
|
||||
}
|
||||
assert.True(t, found)
|
||||
|
||||
rules, err = router.getSessionAffinityRouteRules(canary, 0, weightedRoute)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, canary.Status.SessionAffinityCookie)
|
||||
assert.Contains(t, canary.Status.PreviousSessionAffinityCookie, cookieKey)
|
||||
|
||||
stickyRule = rules[0]
|
||||
cookieMatch = stickyRule.Match[0].Headers[cookieHeader]
|
||||
assert.NotNil(t, cookieMatch.Regex)
|
||||
assert.Contains(t, cookieMatch.Regex, cookieKey)
|
||||
|
||||
assert.NotNil(t, stickyRule.Headers.Response.Add)
|
||||
assert.Equal(t, stickyRule.Headers.Response.Add[setCookieHeader], fmt.Sprintf("%s; %s=%d", canary.Status.PreviousSessionAffinityCookie, maxAgeAttr, -1))
|
||||
})
|
||||
|
||||
t.Run("with primary cookie", func(t *testing.T) {
|
||||
canary := mocks.canary.DeepCopy()
|
||||
mocks := newFixture(canary)
|
||||
canaryCookieKey := "canary-flagger-cookie"
|
||||
primaryCookieKey := "primary-flagger-cookie"
|
||||
canary.Spec.Analysis.Interval = "15s"
|
||||
canary.Spec.Analysis.SessionAffinity = &flaggerv1.SessionAffinity{
|
||||
CookieName: canaryCookieKey,
|
||||
PrimaryCookieName: primaryCookieKey,
|
||||
MaxAge: 300,
|
||||
}
|
||||
|
||||
router := &IstioRouter{
|
||||
logger: mocks.logger,
|
||||
flaggerClient: mocks.flaggerClient,
|
||||
istioClient: mocks.meshClient,
|
||||
kubeClient: mocks.kubeClient,
|
||||
}
|
||||
|
||||
_, pSvcName, cSvcName := canary.GetServiceNames()
|
||||
weightedRoute := &istiov1beta1.HTTPRoute{
|
||||
Route: []istiov1beta1.HTTPRouteDestination{
|
||||
router.makeDestination(canary, pSvcName, 100),
|
||||
router.makeDestination(canary, cSvcName, 0),
|
||||
},
|
||||
}
|
||||
rules, err := router.getSessionAffinityRouteRules(canary, 10, weightedRoute)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, len(rules), 3)
|
||||
assert.True(t, strings.HasPrefix(canary.Status.SessionAffinityCookie, canaryCookieKey))
|
||||
|
||||
canaryStickyRule := rules[0]
|
||||
cookieMatch := canaryStickyRule.Match[0].Headers[cookieHeader]
|
||||
assert.NotNil(t, cookieMatch.Regex)
|
||||
assert.Contains(t, cookieMatch.Regex, canaryCookieKey)
|
||||
|
||||
assert.Equal(t, len(canaryStickyRule.Route), 2)
|
||||
for _, route := range canaryStickyRule.Route {
|
||||
if string(route.Destination.Host) == pSvcName {
|
||||
assert.Equal(t, route.Weight, int(0))
|
||||
}
|
||||
if string(route.Destination.Host) == cSvcName {
|
||||
assert.Equal(t, route.Weight, int(100))
|
||||
}
|
||||
}
|
||||
|
||||
primaryStickyRule := rules[1]
|
||||
cookieMatch = primaryStickyRule.Match[0].Headers[cookieHeader]
|
||||
assert.NotNil(t, cookieMatch.Regex)
|
||||
assert.Contains(t, cookieMatch.Regex, primaryCookieKey)
|
||||
|
||||
assert.Equal(t, len(primaryStickyRule.Route), 2)
|
||||
for _, route := range primaryStickyRule.Route {
|
||||
if string(route.Destination.Host) == pSvcName {
|
||||
assert.Equal(t, route.Weight, int(100))
|
||||
}
|
||||
if string(route.Destination.Host) == cSvcName {
|
||||
assert.Equal(t, route.Weight, int(0))
|
||||
}
|
||||
}
|
||||
|
||||
weightedRule := rules[2]
|
||||
var c int
|
||||
for _, route := range weightedRule.Route {
|
||||
if string(route.Destination.Host) == cSvcName {
|
||||
c += 1
|
||||
|
||||
assert.NotNil(t, route.Headers.Response.Add)
|
||||
assert.Equal(t, route.Headers.Response.Add[setCookieHeader], fmt.Sprintf("%s; %s=%d", canary.Status.SessionAffinityCookie, maxAgeAttr, 300))
|
||||
}
|
||||
|
||||
if string(route.Destination.Host) == pSvcName {
|
||||
c += 1
|
||||
|
||||
assert.NotNil(t, route.Headers.Response.Add)
|
||||
assert.Contains(t, route.Headers.Response.Add[setCookieHeader], canary.Status.PrimarySessionAffinityCookie)
|
||||
|
||||
interval, err := time.ParseDuration(canary.Spec.Analysis.Interval)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, route.Headers.Response.Add[setCookieHeader], fmt.Sprintf("%s=%d", maxAgeAttr, int(interval.Seconds())))
|
||||
}
|
||||
}
|
||||
assert.Equal(t, 2, c)
|
||||
|
||||
rules, err = router.getSessionAffinityRouteRules(canary, 0, weightedRoute)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, canary.Status.SessionAffinityCookie)
|
||||
assert.Contains(t, canary.Status.PreviousSessionAffinityCookie, canaryCookieKey)
|
||||
|
||||
canaryStickyRule = rules[0]
|
||||
cookieMatch = canaryStickyRule.Match[0].Headers[cookieHeader]
|
||||
assert.NotNil(t, cookieMatch.Regex)
|
||||
assert.Contains(t, cookieMatch.Regex, canaryCookieKey)
|
||||
|
||||
assert.NotNil(t, canaryStickyRule.Headers.Response.Add)
|
||||
assert.Equal(t, canaryStickyRule.Headers.Response.Add[setCookieHeader], fmt.Sprintf("%s; %s=%d", canary.Status.PreviousSessionAffinityCookie, maxAgeAttr, -1))
|
||||
})
|
||||
}
|
||||
|
||||
func TestIstioRouter_GetRoutes(t *testing.T) {
|
||||
mocks := newFixture(nil)
|
||||
router := &IstioRouter{
|
||||
|
||||
@@ -107,7 +107,7 @@ done
|
||||
echo '>>> Verifying session affinity'
|
||||
if ! URL=http://localhost:8888 HOST=www.example.com CANARY_VERSION=6.1.0 \
|
||||
CANARY_COOKIE_NAME=canary-flagger-cookie PRIMARY_VERSION=6.0.4 PRIMARY_COOKIE_NAME=primary-flagger-cookie \
|
||||
go run ${REPO_ROOT}/test/gatewayapi/verify_session_affinity.go; then
|
||||
go run ${REPO_ROOT}/test/verify_session_affinity.go; then
|
||||
echo "failed to verify session affinity"
|
||||
exit $?
|
||||
fi
|
||||
|
||||
@@ -15,3 +15,6 @@ DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
"$REPO_ROOT"/test/workloads/init.sh
|
||||
"$DIR"/test-delegation.sh
|
||||
|
||||
"$REPO_ROOT"/test/workloads/init.sh
|
||||
"$DIR"/test-session-affinity.sh
|
||||
174
test/istio/test-session-affinity.sh
Executable file
174
test/istio/test-session-affinity.sh
Executable file
@@ -0,0 +1,174 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This script runs e2e tests for progressive traffic shifting with session affinity, Canary analysis and promotion
|
||||
# Prerequisites: Kubernetes Kind and Istio
|
||||
|
||||
set -o errexit
|
||||
|
||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||
|
||||
echo '>>> Initialising Gateway'
|
||||
cat <<EOF | kubectl apply -f -
|
||||
apiVersion: networking.istio.io/v1alpha3
|
||||
kind: Gateway
|
||||
metadata:
|
||||
name: istio-ingressgateway
|
||||
namespace: istio-system
|
||||
spec:
|
||||
selector:
|
||||
app: istio-ingressgateway
|
||||
istio: ingressgateway
|
||||
servers:
|
||||
- port:
|
||||
number: 80
|
||||
name: http
|
||||
protocol: HTTP
|
||||
hosts:
|
||||
- "*"
|
||||
EOF
|
||||
|
||||
echo '>>> Initialising canary for session affinity'
|
||||
cat <<EOF | kubectl apply -f -
|
||||
apiVersion: flagger.app/v1beta1
|
||||
kind: Canary
|
||||
metadata:
|
||||
name: podinfo
|
||||
namespace: test
|
||||
spec:
|
||||
targetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: podinfo
|
||||
progressDeadlineSeconds: 60
|
||||
service:
|
||||
port: 80
|
||||
targetPort: 9898
|
||||
gateways:
|
||||
- istio-system/istio-ingressgateway
|
||||
hosts:
|
||||
- "*"
|
||||
analysis:
|
||||
interval: 15s
|
||||
threshold: 15
|
||||
maxWeight: 30
|
||||
stepWeight: 10
|
||||
sessionAffinity:
|
||||
cookieName: canary-flagger-cookie
|
||||
primaryCookieName: primary-flagger-cookie
|
||||
webhooks:
|
||||
- name: load-test
|
||||
url: http://flagger-loadtester.test/
|
||||
timeout: 5s
|
||||
metadata:
|
||||
type: cmd
|
||||
cmd: "hey -z 10m -q 10 -c 2 http://istio-ingressgateway.istio-system/podinfo"
|
||||
logCmdOutput: "true"
|
||||
EOF
|
||||
|
||||
echo '>>> Waiting for primary to be ready'
|
||||
retries=50
|
||||
count=0
|
||||
ok=false
|
||||
until ${ok}; do
|
||||
kubectl -n test get canary/podinfo | grep 'Initialized' && ok=true || ok=false
|
||||
sleep 5
|
||||
count=$(($count + 1))
|
||||
if [[ ${count} -eq ${retries} ]]; then
|
||||
kubectl -n istio-system logs deployment/flagger
|
||||
echo "No more retries left"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo '✔ Canary initialization test passed'
|
||||
|
||||
echo '>>> Port forwarding load balancer'
|
||||
INGRESS_GATEWAY_POD=$(kubectl get pods -n istio-system -l app=istio-ingressgateway -o name | awk -F'/' '{print $2}')
|
||||
kubectl port-forward -n istio-system "$INGRESS_GATEWAY_POD" 8080:8080 2>&1 > /dev/null &
|
||||
pf_pid=$!
|
||||
|
||||
cleanup() {
|
||||
echo ">> Killing port forward process ${pf_pid}"
|
||||
kill -9 $pf_pid
|
||||
}
|
||||
trap "cleanup" EXIT SIGINT
|
||||
|
||||
echo '>>> Triggering canary deployment'
|
||||
kubectl -n test set image deployment/podinfo podinfod=ghcr.io/stefanprodan/podinfo:6.0.1
|
||||
|
||||
echo '>>> Waiting for initial traffic shift'
|
||||
retries=50
|
||||
count=0
|
||||
ok=false
|
||||
until ${ok}; do
|
||||
kubectl -n test get canary podinfo -o=jsonpath='{.status.canaryWeight}' | grep '10' && ok=true || ok=false
|
||||
sleep 5
|
||||
kubectl -n istio-system logs deployment/flagger --tail 1
|
||||
count=$(($count + 1))
|
||||
if [[ ${count} -eq ${retries} ]]; then
|
||||
kubectl -n istio-system logs deployment/flagger
|
||||
echo "No more retries left"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo '>>> Verifying session affinity'
|
||||
if ! URL=http://localhost:8080 HOST=localhost CANARY_VERSION=6.0.1 \
|
||||
CANARY_COOKIE_NAME=canary-flagger-cookie PRIMARY_VERSION=6.0.0 PRIMARY_COOKIE_NAME=primary-flagger-cookie \
|
||||
go run ${REPO_ROOT}/test/verify_session_affinity.go; then
|
||||
echo "failed to verify session affinity"
|
||||
exit $?
|
||||
fi
|
||||
|
||||
echo '>>> Waiting for canary promotion'
|
||||
retries=50
|
||||
count=0
|
||||
ok=false
|
||||
until ${ok}; do
|
||||
kubectl -n test describe deployment/podinfo-primary | grep '6.0.1' && ok=true || ok=false
|
||||
sleep 10
|
||||
kubectl -n istio-system logs deployment/flagger --tail 1
|
||||
count=$(($count + 1))
|
||||
if [[ ${count} -eq ${retries} ]]; then
|
||||
kubectl -n istio-system logs deployment/flagger
|
||||
echo "No more retries left"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo '>>> Waiting for canary finalization'
|
||||
retries=50
|
||||
count=0
|
||||
ok=false
|
||||
until ${ok}; do
|
||||
kubectl -n test get canary/podinfo | grep 'Succeeded' && ok=true || ok=false
|
||||
sleep 5
|
||||
count=$(($count + 1))
|
||||
if [[ ${count} -eq ${retries} ]]; then
|
||||
kubectl -n istio-system logs deployment/flagger
|
||||
echo "No more retries left"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo '>>> Verifying cookie cleanup'
|
||||
canary_cookie=$(kubectl -n test get canary podinfo -o=jsonpath='{.status.previousSessionAffinityCookie}' | xargs)
|
||||
response=$(curl -H "Cookie: $canary_cookie" -D - http://localhost:8080)
|
||||
|
||||
if [[ $response == *"$canary_cookie"* ]]; then
|
||||
echo "✔ Found previous cookie in response"
|
||||
else
|
||||
echo "⨯ Previous cookie ${canary_cookie} not found in response"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $response == *"Max-Age=-1"* ]]; then
|
||||
echo "✔ Found Max-Age attribute in cookie"
|
||||
else
|
||||
echo "⨯ Max-Age attribute not present in cookie"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo '✔ Canary release with session affinity promotion test passed'
|
||||
|
||||
kubectl delete -n test canary podinfo
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
set -o errexit
|
||||
|
||||
TRAEFIK_CHART_VERSION="34.4.1" # traefik 2.10.4
|
||||
TRAEFIK_CHART_VERSION="37.4.0" # traefik 3.6.2
|
||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||
|
||||
mkdir -p ${REPO_ROOT}/bin
|
||||
|
||||
Reference in New Issue
Block a user