Compare commits

...

12 Commits

Author SHA1 Message Date
Sanskar Jaiswal
d4cc9bf616 Merge pull request #1862 from fluxcd/dependabot/go_modules/golang.org/x/crypto-0.45.0
build(deps): bump golang.org/x/crypto from 0.42.0 to 0.45.0
2025-12-29 18:38:24 +05:30
dependabot[bot]
ca3fd315cd build(deps): bump golang.org/x/crypto from 0.42.0 to 0.45.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.42.0 to 0.45.0.
- [Commits](https://github.com/golang/crypto/compare/v0.42.0...v0.45.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.45.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-29 12:14:13 +00:00
Stefan Prodan
d9d910b0cf Merge pull request #1861 from renatovassaomb/rv/istio-primary-cookie-name-support
Feat: Add support for stickiness for primary deployment in Istio
2025-12-18 09:11:10 +02:00
Stefan Prodan
9090802dcc Merge pull request #1858 from erikmiller-gusto/main
fix: datadog metrics should provide http status code on error if non-2xx response
2025-12-18 09:09:51 +02:00
Renato Vassão
70c4c528ed Updates docs with new support for primary cookie name in istio router
Signed-off-by: Renato Vassão <renato.vassao@mindbodyonline.com>
2025-12-12 10:55:37 -03:00
Stefan Prodan
3ce87af077 Merge pull request #1868 from darkweaver87/chore/update-traefik
chore(ci): update Traefik to 3.6.2 in E2E tests
2025-12-11 13:22:30 +02:00
Rémi BUISSON
1709b076e0 chore(ci): update traefik for E2E tests
Signed-off-by: Rémi BUISSON <remi.buisson@traefik.io>
2025-12-11 11:36:53 +01:00
Renato Vassão
f3b240ca82 Adds integration tests for session affinity in Istio router
Signed-off-by: Renato Vassão <renato.vassao@mindbodyonline.com>
2025-11-18 15:16:42 -03:00
Renato Vassão
4821a687c1 Adds support for setting primary cookie name in istio router
Signed-off-by: Renato Vassão <renato.vassao@mindbodyonline.com>
2025-11-18 15:08:25 -03:00
Renato Vassão
931dd7fa6b Adds usage of PrimarySessionAffinityCookie in Gateway API router
Signed-off-by: Renato Vassão <renato.vassao@mindbodyonline.com>
2025-11-18 15:07:19 -03:00
Renato Vassão
8018353d54 Adds primarySessionAffinityCookie field to sessionAffinity
Signed-off-by: Renato Vassão <renato.vassao@mindbodyonline.com>
2025-11-18 15:04:04 -03:00
Erik Miller
ffddfd9c24 fix: datadog metrics should provide http status code on error if non-2xx response
currently the log line exposes the error, however that's always going to be nil
based on the check just above it.  This provides better visibility into the failure reason

Signed-off-by: Erik Miller <erik.miller@gusto.com>
2025-11-13 12:59:08 -08:00
19 changed files with 572 additions and 138 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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
![Flagger Canary Traffic Shadowing](https://raw.githubusercontent.com/fluxcd/flagger/main/docs/diagrams/flagger-canary-traffic-mirroring.png)

View File

@@ -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
View File

@@ -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
View File

@@ -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=

View File

@@ -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

View File

@@ -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 != "" {

View File

@@ -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"`

View File

@@ -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

View File

@@ -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{

View File

@@ -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()),
},
},
},

View File

@@ -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
}
}

View File

@@ -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{

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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