Compare commits

..

25 Commits

Author SHA1 Message Date
dependabot[bot]
95af44c10f build(deps): bump the ci group across 1 directory with 9 updates
Bumps the ci group with 9 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [actions/checkout](https://github.com/actions/checkout) | `5` | `6` |
| [helm/kind-action](https://github.com/helm/kind-action) | `1.12.0` | `1.14.0` |
| [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer) | `3.10.0` | `4.0.0` |
| [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) | `3` | `4` |
| [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) | `3` | `4` |
| [docker/login-action](https://github.com/docker/login-action) | `3` | `4` |
| [docker/metadata-action](https://github.com/docker/metadata-action) | `5` | `6` |
| [docker/build-push-action](https://github.com/docker/build-push-action) | `6` | `7` |
| [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action) | `6` | `7` |



Updates `actions/checkout` from 5 to 6
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

Updates `helm/kind-action` from 1.12.0 to 1.14.0
- [Release notes](https://github.com/helm/kind-action/releases)
- [Commits](https://github.com/helm/kind-action/compare/v1.12.0...v1.14.0)

Updates `sigstore/cosign-installer` from 3.10.0 to 4.0.0
- [Release notes](https://github.com/sigstore/cosign-installer/releases)
- [Commits](https://github.com/sigstore/cosign-installer/compare/v3.10.0...v4.0.0)

Updates `docker/setup-qemu-action` from 3 to 4
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v3...v4)

Updates `docker/setup-buildx-action` from 3 to 4
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3...v4)

Updates `docker/login-action` from 3 to 4
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v3...v4)

Updates `docker/metadata-action` from 5 to 6
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](https://github.com/docker/metadata-action/compare/v5...v6)

Updates `docker/build-push-action` from 6 to 7
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6...v7)

Updates `goreleaser/goreleaser-action` from 6 to 7
- [Release notes](https://github.com/goreleaser/goreleaser-action/releases)
- [Commits](https://github.com/goreleaser/goreleaser-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: ci
- dependency-name: helm/kind-action
  dependency-version: 1.14.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: ci
- dependency-name: sigstore/cosign-installer
  dependency-version: 4.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: ci
- dependency-name: docker/setup-qemu-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: ci
- dependency-name: docker/setup-buildx-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: ci
- dependency-name: docker/login-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: ci
- dependency-name: docker/metadata-action
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: ci
- dependency-name: docker/build-push-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: ci
- dependency-name: goreleaser/goreleaser-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: ci
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-16 11:38:54 +00:00
Stefan Prodan
d29c60d2e6 Merge pull request #1885 from renatovassaomb/rv/drop-policy-v1beta1
Removes deprecated `policy/v1beta1` API from PodDisruptionBudget templates
2026-03-11 15:30:08 +02:00
Sanskar Jaiswal
481d9c3d1f Merge pull request #1811 from cdoble84-uk/prometheus-chart-fix
fix(chart): fixes a compatibility issue with the Helm Chart and Prometheus v3.0+
2026-03-09 20:03:47 +05:30
Chris Doble
5bf3cc5c95 Fixes a compatibility issue with the Helm Chart and Prometheus v3.0+
In the Flagger Helm Chart Prometheus config the storage retention duration is set via this flag --storage.tsdb.retention. This Flag was deprecated in a previous version of Prometheus and removed from Prometheus v3.0+ which means the current Flagger Helm Chart is incompatible with the newer Prometheus versions. This flag has now been updated to --storage.tsdb.retention.time which is backwards compatible down to Prometheus v2.51 at least.

Signed-off-by: Chris Doble <chrisdoble84@gmail.com>
2026-03-09 18:06:00 +05:30
Sanskar Jaiswal
d7166683c9 Merge pull request #1880 from nedal87/feat/helm-additional-volume-mounts
Add additionalVolumeMounts support for flagger chart
2026-03-08 09:14:41 +05:30
Nedal Eskaf
4850415854 add additionalVolumeMounts to chart
Signed-off-by: Nedal Eskaf <nedaleskaif87@gmail.com>

use list default for additionalVolumes

Signed-off-by: Nedal Eskaf <nedaleskaif87@gmail.com>

document additionalVolumes in chart readme

Signed-off-by: Nedal Eskaf <nedaleskaif87@gmail.com>
2026-03-08 08:37:52 +05:30
Sanskar Jaiswal
c9579da798 Merge pull request #1851 from sayap/fix-wrong-weight-when-promoting
Fix bug where CanaryWeight is reset to 0 during CanaryPhasePromoting
2026-03-08 08:34:16 +05:30
Yap Sok Ann
ceab3ec96b Fix bug where CanaryWeight is reset to 0 during CanaryPhasePromoting
Signed-off-by: Yap Sok Ann <sokann@gmail.com>
2026-03-07 15:02:09 +05:30
Renato Vassão
36a208835d Removes deprecated policy/v1beta1 api from PodDisruptionBudget templates
Signed-off-by: Renato Vassão <renato.vassao@mindbodyonline.com>
2026-03-06 10:21:45 -03:00
Sanskar Jaiswal
b10e7cfcb8 Merge pull request #1878 from rohansood10/fix/1434-prometheus-error-messages
Include HTTP status code in Prometheus error responses
2026-03-02 19:13:43 +05:30
Sanskar Jaiswal
6b7c86befd Merge pull request #1874 from mtfurlan/fix/mesh-router-warn-unknown
warn when mesh provider isn't valid
2026-02-27 12:46:16 +05:30
Mara Furland
fd26b1414b warn when mesh/metrics provider or canary target type isn't valid
fixes #1872

Signed-off-by: Mara Furland <mara@fur.land>
2026-02-20 11:40:39 -05:00
rohansood10
6a0096302f Include HTTP status code in Prometheus error responses
When Prometheus returns an HTTP error (4xx/5xx), the error message now
includes the status code and reason phrase. Previously, only the response
body was shown, which made it difficult to diagnose issues like HTTP 403
errors caused by missing authorization policies.

Fixes fluxcd/flagger#1434

Signed-off-by: rohansood10 <rohansood10@users.noreply.github.com>
2026-02-19 03:11:46 -08:00
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
39 changed files with 652 additions and 180 deletions

View File

@@ -19,7 +19,7 @@ jobs:
labels: ubuntu-latest-16-cores
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Setup Go
uses: actions/setup-go@v6
with:

View File

@@ -38,16 +38,16 @@ jobs:
- knative
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Setup Kubernetes
uses: helm/kind-action@v1.12.0
uses: helm/kind-action@v1.14.0
if: matrix.provider != 'skipper'
with:
version: v0.23.0
cluster_name: kind
node_image: kindest/node:v1.30.0@sha256:047357ac0cfea04663786a612ba1eaba9702bef25227a794b52890dd8bcd692e
- name: Setup Kubernetes for skipper
uses: helm/kind-action@v1.12.0
uses: helm/kind-action@v1.14.0
if: matrix.provider == 'skipper'
with:
version: v0.23.0

View File

@@ -12,7 +12,7 @@ jobs:
permissions:
contents: write
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Publish Helm charts
uses: stefanprodan/helm-gh-pages@v1.7.0
with:

View File

@@ -16,8 +16,8 @@ jobs:
id-token: write
packages: write
steps:
- uses: actions/checkout@v5
- uses: sigstore/cosign-installer@v3.10.0
- uses: actions/checkout@v6
- uses: sigstore/cosign-installer@v4.1.0
- name: Prepare
id: prep
run: |
@@ -25,19 +25,19 @@ jobs:
echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT
- name: Setup QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
- name: Setup Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ghcr.io
username: fluxcdbot
password: ${{ secrets.GHCR_TOKEN }}
- name: Generate image meta
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
with:
images: |
${{ env.IMAGE }}
@@ -45,7 +45,7 @@ jobs:
type=raw,value=${{ steps.prep.outputs.VERSION }}
- name: Publish image
id: build-push
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
push: true
builder: ${{ steps.buildx.outputs.name }}

View File

@@ -27,13 +27,13 @@ jobs:
id-token: write # needed for keyless signing
packages: write # needed for ghcr access
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: 1.25.x
- uses: fluxcd/flux2/action@main
- uses: sigstore/cosign-installer@v3.10.0
- uses: sigstore/cosign-installer@v4.1.0
- name: Prepare
id: prep
run: |
@@ -47,19 +47,19 @@ jobs:
echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT
- name: Setup QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
- name: Setup Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ghcr.io
username: fluxcdbot
password: ${{ secrets.GHCR_TOKEN }}
- name: Generate image meta
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
with:
images: |
${{ env.IMAGE }}
@@ -67,7 +67,7 @@ jobs:
type=raw,value=${{ steps.prep.outputs.VERSION }}
- name: Publish image
id: build-push
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
sbom: true
provenance: true
@@ -121,7 +121,7 @@ jobs:
- uses: anchore/sbom-action/download-syft@v0
- name: Create release and SBOM
id: run-goreleaser
uses: goreleaser/goreleaser-action@v6
uses: goreleaser/goreleaser-action@v7
if: startsWith(github.ref, 'refs/tags/v')
with:
version: latest

View File

@@ -17,7 +17,7 @@ jobs:
permissions:
security-events: write
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Run FOSSA scan and upload build data
uses: fossa-contrib/fossa-action@v3
with:
@@ -30,7 +30,7 @@ jobs:
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Setup Go
uses: actions/setup-go@v6
with:

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

@@ -186,6 +186,8 @@ The following tables lists the configurable parameters of the Flagger chart and
| `podDisruptionBudget.minAvailable` | The minimal number of available replicas that will be set in the PodDisruptionBudget | `1` |
| `noCrossNamespaceRefs` | If `true`, cross namespace references to custom resources will be disabled | `false` |
| `namespace` | When specified, Flagger will restrict itself to watching Canary objects from that namespace | `""` |
| `additionalVolumes` | Extra volumes to add to the Flagger pod | `[]` |
| `additionalVolumeMounts` | Extra volume mounts to add to the Flagger container | `[]` |
| `deploymentLabels` | Labels to add to Flagger deployment | `{}` |
| `podLabels` | Labels to add to pods of Flagger deployment | `{}` |

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

@@ -53,15 +53,17 @@ spec:
imagePullSecrets:
- name: {{ .Values.image.pullSecret }}
{{- end }}
{{- if .Values.controlplane.kubeconfig.secretName }}
{{- if or .Values.controlplane.kubeconfig.secretName .Values.additionalVolumes }}
volumes:
{{- if .Values.controlplane.kubeconfig.secretName }}
- name: kubeconfig
secret:
secretName: "{{ .Values.controlplane.kubeconfig.secretName }}"
{{- end }}
{{- if .Values.additionalVolumes }}
{{- toYaml .Values.additionalVolumes | nindent 8 -}}
{{- end }}
{{- if .Values.additionalVolumes }}
{{ toYaml .Values.additionalVolumes | nindent 8 }}
{{- end }}
{{- end }}
{{- if .Values.podPriorityClassName }}
priorityClassName: {{ .Values.podPriorityClassName }}
{{- end }}
@@ -71,11 +73,16 @@ spec:
securityContext:
{{ toYaml .Values.securityContext.context | indent 12 }}
{{- end }}
{{- if .Values.controlplane.kubeconfig.secretName }}
{{- if or .Values.controlplane.kubeconfig.secretName .Values.additionalVolumeMounts }}
volumeMounts:
{{- if .Values.controlplane.kubeconfig.secretName }}
- name: kubeconfig
mountPath: "/tmp/controlplane"
{{- end }}
{{- if .Values.additionalVolumeMounts }}
{{ toYaml .Values.additionalVolumeMounts | nindent 12 }}
{{- end }}
{{- end }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:

View File

@@ -1,9 +1,5 @@
{{- if .Values.podDisruptionBudget.enabled }}
{{- if .Capabilities.APIVersions.Has "policy/v1/PodDisruptionBudget" -}}
apiVersion: policy/v1
{{- else }}
apiVersion: policy/v1beta1
{{- end }}
kind: PodDisruptionBudget
metadata:
name: {{ template "flagger.name" . }}

View File

@@ -233,7 +233,7 @@ spec:
image: {{ .Values.prometheus.image }}
imagePullPolicy: IfNotPresent
args:
- '--storage.tsdb.retention={{ .Values.prometheus.retention }}'
- '--storage.tsdb.retention.time={{ .Values.prometheus.retention }}'
- '--config.file=/etc/prometheus/prometheus.yml'
ports:
- containerPort: 9090

View File

@@ -204,6 +204,11 @@ deploymentLabels: { }
noCrossNamespaceRefs: false
#Placeholder to supply additional volumes to the flagger pod
additionalVolumes: {}
additionalVolumes: []
# - name: tmpfs
# emptyDir: {}
# Placeholder to supply additional volume mounts to the flagger container
additionalVolumeMounts: []
# - name: tmpfs
# mountPath: /tmp

View File

@@ -1,9 +1,5 @@
{{- if .Values.podDisruptionBudget.enabled }}
{{- if .Capabilities.APIVersions.Has "policy/v1/PodDisruptionBudget" -}}
apiVersion: policy/v1
{{- else }}
apiVersion: policy/v1beta1
{{- end }}
kind: PodDisruptionBudget
metadata:
name: {{ include "loadtester.fullname" . }}

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

@@ -93,6 +93,7 @@ func (factory *Factory) Controller(obj v1beta1.LocalObjectReference) Controller
return serviceCtrl
}
default:
factory.logger.Warnf("unknown canary target '%s', assuming deployment", obj.Kind)
return deploymentCtrl
}
}
@@ -118,6 +119,7 @@ func (factory *Factory) ScalerReconciler(kind string) ScalerReconciler {
case "ScaledObject":
return soReconciler
default:
factory.logger.Errorf("unknown hpa kind '%s'")
return nil
}
}

View File

@@ -158,7 +158,7 @@ func setStatusPhase(flaggerClient clientset.Interface, cd *flaggerv1.Canary, pha
cdCopy.Status.Phase = phase
cdCopy.Status.LastTransitionTime = metav1.Now()
if phase != flaggerv1.CanaryPhaseProgressing && phase != flaggerv1.CanaryPhaseWaiting {
if phase != flaggerv1.CanaryPhaseProgressing && phase != flaggerv1.CanaryPhaseWaiting && phase != flaggerv1.CanaryPhasePromoting {
cdCopy.Status.CanaryWeight = 0
cdCopy.Status.Iterations = 0
if phase == flaggerv1.CanaryPhaseWaitingPromotion {

View File

@@ -179,27 +179,33 @@ func TestScheduler_DeploymentAnalysisPhases(t *testing.T) {
// detect changes
mocks.ctrl.advanceCanary("podinfo", "default")
require.NoError(t, assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhaseProgressing))
require.NoError(t, assertCanaryWeight(mocks.flaggerClient, "podinfo", 0))
mocks.makeCanaryReady(t)
// progressing
mocks.ctrl.advanceCanary("podinfo", "default")
require.NoError(t, assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhaseProgressing))
require.NoError(t, assertCanaryWeight(mocks.flaggerClient, "podinfo", 100))
// start promotion
mocks.ctrl.advanceCanary("podinfo", "default")
require.NoError(t, assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhasePromoting))
require.NoError(t, assertCanaryWeight(mocks.flaggerClient, "podinfo", 100))
// end promotion
mocks.ctrl.advanceCanary("podinfo", "default")
require.NoError(t, assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhasePromoting))
require.NoError(t, assertCanaryWeight(mocks.flaggerClient, "podinfo", 50))
// finalising
mocks.ctrl.advanceCanary("podinfo", "default")
require.NoError(t, assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhaseFinalising))
require.NoError(t, assertCanaryWeight(mocks.flaggerClient, "podinfo", 0))
// succeeded
mocks.ctrl.advanceCanary("podinfo", "default")
require.NoError(t, assertPhase(mocks.flaggerClient, "podinfo", flaggerv1.CanaryPhaseSucceeded))
require.NoError(t, assertCanaryWeight(mocks.flaggerClient, "podinfo", 0))
}
func TestScheduler_DeploymentBlueGreenAnalysisPhases(t *testing.T) {

View File

@@ -74,7 +74,7 @@ func (c *Controller) checkMetricProviderAvailability(canary *flaggerv1.Canary) e
credentials = secret.Data
}
factory := providers.Factory{}
factory := providers.NewFactory(c.logger)
provider, err := factory.Provider(metric.Interval, template.Spec.Provider, credentials, c.kubeConfig)
if err != nil {
return fmt.Errorf("metric template %s.%s provider %s error: %v",
@@ -292,7 +292,7 @@ func (c *Controller) runMetricChecks(canary *flaggerv1.Canary) bool {
credentials = secret.Data
}
factory := providers.Factory{}
factory := providers.NewFactory(c.logger)
provider, err := factory.Provider(metric.Interval, template.Spec.Provider, credentials, c.kubeConfig)
if err != nil {
c.recordEventErrorf(canary, "Metric template %s.%s provider %s error: %v",

View File

@@ -60,6 +60,19 @@ func assertPhase(flaggerClient clientset.Interface, canary string, phase flagger
return nil
}
func assertCanaryWeight(flaggerClient clientset.Interface, canary string, canaryWeight int) error {
c, err := flaggerClient.FlaggerV1beta1().Canaries("default").Get(context.TODO(), canary, metav1.GetOptions{})
if err != nil {
return err
}
if c.Status.CanaryWeight != canaryWeight {
return fmt.Errorf("got canary weight %d wanted %d", c.Status.CanaryWeight, canaryWeight)
}
return nil
}
func alwaysReady() bool {
return true
}

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

@@ -18,10 +18,19 @@ package providers
import (
flaggerv1 "github.com/fluxcd/flagger/pkg/apis/flagger/v1beta1"
"go.uber.org/zap"
rest "k8s.io/client-go/rest"
)
type Factory struct{}
type Factory struct {
logger *zap.SugaredLogger
}
func NewFactory(logger *zap.SugaredLogger) *Factory {
return &Factory{
logger: logger,
}
}
func (factory Factory) Provider(metricInterval string, provider flaggerv1.MetricTemplateProvider, credentials map[string][]byte, config *rest.Config) (Interface, error) {
switch provider.Type {
@@ -46,6 +55,7 @@ func (factory Factory) Provider(metricInterval string, provider flaggerv1.Metric
case "splunk":
return NewSplunkProvider(metricInterval, provider, credentials)
default:
factory.logger.Warnf("unknown metrics provider '%s', using prometheus", provider.Type)
return NewPrometheusProvider(provider, credentials)
}
}

View File

@@ -143,7 +143,7 @@ func (p *PrometheusProvider) RunQuery(query string) (float64, error) {
}
if 400 <= r.StatusCode {
return 0, fmt.Errorf("error response: %s", string(b))
return 0, fmt.Errorf("error response: Status %d %s: %s", r.StatusCode, http.StatusText(r.StatusCode), string(b))
}
var result prometheusResponse

View File

@@ -224,6 +224,7 @@ func (factory *Factory) MeshRouter(provider string, labelSelector string) Interf
case provider == flaggerv1.KubernetesProvider:
return &NopRouter{}
default:
factory.logger.Warnf("unknown mesh router provider '%s', using istio", provider)
return &IstioRouter{
logger: factory.logger,
flaggerClient: factory.flaggerClient,

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