From 13876f5aacbe395bc166e9bb2975df99182f42f1 Mon Sep 17 00:00:00 2001 From: Aliaksei Venski Date: Fri, 27 Mar 2026 14:48:28 +0100 Subject: [PATCH 1/3] update core chart to support sticky sessions --- charts/core/Chart.yaml | 2 +- charts/core/templates/CRD/ingressroute.yaml | 8 ++++++++ charts/core/values.yaml | 6 ++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/charts/core/Chart.yaml b/charts/core/Chart.yaml index 4c773f2..44dfd1d 100644 --- a/charts/core/Chart.yaml +++ b/charts/core/Chart.yaml @@ -2,4 +2,4 @@ apiVersion: v2 name: charts-core description: A Helm chart for Kubernetes type: application -version: 2.7.0 +version: 2.8.0 diff --git a/charts/core/templates/CRD/ingressroute.yaml b/charts/core/templates/CRD/ingressroute.yaml index 738fc95..7ed8df7 100644 --- a/charts/core/templates/CRD/ingressroute.yaml +++ b/charts/core/templates/CRD/ingressroute.yaml @@ -24,6 +24,14 @@ spec: name: {{ $rangeItem.serviceName | default ( include "charts-core.fullname" . ) }} namespace: {{ $rangeItem.serviceNamespace | default .Release.Namespace | quote}} port: {{ $rangeItem.servicePort | default 80 }} + {{- if and $rangeItem.sticky $rangeItem.sticky.enabled (not .Values.global.canary.enabled) }} + sticky: + cookie: + name: {{ $rangeItem.sticky.name | default (printf "%s-sticky" (include "charts-core.fullname" .)) }} + secure: {{ if kindIs "invalid" $rangeItem.sticky.secure }}true{{ else }}{{ $rangeItem.sticky.secure }} {{ end }} + httpOnly: {{ if kindIs "invalid" $rangeItem.sticky.httpOnly }}true{{ else }}{{ $rangeItem.sticky.httpOnly }} {{ end }} + sameSite: {{ $rangeItem.sticky.sameSite | default "strict" }} + {{- end }} {{- if or $rangeItem.isStripprefixEnabled $rangeItem.stripPrefixes (ne "false" ($rangeItem.isRetryEnabled | toString)) $rangeItem.isCircuitBreakerEnabled }} middlewares: {{- end }} diff --git a/charts/core/values.yaml b/charts/core/values.yaml index f230252..e508d94 100644 --- a/charts/core/values.yaml +++ b/charts/core/values.yaml @@ -98,6 +98,12 @@ global: # - /vadisservice #isRetryEnabled: true #isCircuitBreakerEnabled: true + #sticky: # Preview feature. Currently works only with canary deployments disabled. + #enabled: false + #name: cookie-name + #secure: true + #httpOnly: true + #sameSite: strict envVarsEnabled: true envVars: From 4c5776068034c2f1a9cb971fb731fa6a5c860235 Mon Sep 17 00:00:00 2001 From: Aliaksei Venski Date: Fri, 27 Mar 2026 15:01:26 +0100 Subject: [PATCH 2/3] add tests and update readme --- charts/core/README.md | 27 +- .../core/templates/tests/ingressroute_test.py | 231 ++++++++++++++++++ 2 files changed, 256 insertions(+), 2 deletions(-) diff --git a/charts/core/README.md b/charts/core/README.md index c377509..378866b 100644 --- a/charts/core/README.md +++ b/charts/core/README.md @@ -7,6 +7,7 @@ This chart supports integration with External Secrets Operator for syncing secre ### Prerequisites ESO requires the following secrets to be defined in `secEnvVars`: + - `AZURE_CLIENT_ID` - Azure Service Principal Client ID - `AZURE_CLIENT_SECRET` - Azure Service Principal Client Secret - `AZURE_TENANT_ID` - Azure Tenant ID @@ -34,17 +35,39 @@ global: ### Generated Resources When ESO is enabled, the chart creates: + - **ClusterSecretStore**: `-cluster-secret-store` - connects to Azure Key Vault - **ExternalSecret**: `-external-secret` - syncs secrets to `-secure-kv` The ESO will use the existing `-secure` secret (created from `secEnvVars`) for authentication to Azure Key Vault. -# How to test locally +## Sticky Sessions + +Enables cookie-based session affinity for Traefik IngressRoutes. **Mutually exclusive with canary deployments** — when `global.canary.enabled` is `true`, sticky config is ignored. Flagger officially doesn't support sticky sessions, as well as it breaks user weighted routing when rollout is happening. + +### Configuration + +```yaml +global: + ingressRoutes: + routes: + - ruleName: private + sticky: + enabled: true + name: my-cookie # default: -sticky + secure: false # default: true + httpOnly: true # default: true + sameSite: strict # default: strict +``` + +## How to test locally + 1. Install prerequisites as specified in tests requirements.txt 2. in charts\charts\core type "helm template ." make sure the template renders correctly 3. in charts\charts\core type "pytest" all tests should pass -# How to debug in VS code +## How to debug in VS code + https://code.visualstudio.com/docs/python/testing test discovery in subfolders is based on existence of __init__.py file to run tests succesfully you need to set test working directory go to File->Preferences->settings, search Tests, select Python and find "Optional working directory for tests." Set it to charts\core \ No newline at end of file diff --git a/charts/core/templates/tests/ingressroute_test.py b/charts/core/templates/tests/ingressroute_test.py index 034c584..0a23710 100644 --- a/charts/core/templates/tests/ingressroute_test.py +++ b/charts/core/templates/tests/ingressroute_test.py @@ -286,4 +286,235 @@ def test_host_rule_and_circuitbreaker_middleware_configured(self): "NetworkErrorRatio() > 0.10", jmespath.search( "spec.circuitBreaker.expression", docs[1]) + ) + + def test_sticky_session_rendered_when_enabled(self): + docs = render_chart( + values={ + "global": { + "ingressRoutes": { + "enabled": True, + "routes": [ + { + "ruleName": "http", + "isRetryEnabled": False, + "sticky": { + "enabled": True + } + } + ] + } + } + }, + name=".", + show_only=["templates/CRD/ingressroute.yaml"] + ) + self.assertIsNotNone(jmespath.search( + "spec.routes[0].services[0].sticky.cookie", docs[0])) + + def test_sticky_session_not_rendered_when_disabled(self): + docs = render_chart( + values={ + "global": { + "ingressRoutes": { + "enabled": True, + "routes": [ + { + "ruleName": "http", + "isRetryEnabled": False, + "sticky": { + "enabled": False + } + } + ] + } + } + }, + name=".", + show_only=["templates/CRD/ingressroute.yaml"] + ) + self.assertIsNone(jmespath.search( + "spec.routes[0].services[0].sticky", docs[0])) + + def test_sticky_session_not_rendered_when_canary_enabled(self): + docs = render_chart( + values={ + "global": { + "canary": { + "enabled": True + }, + "ingressRoutes": { + "enabled": True, + "routes": [ + { + "ruleName": "http", + "isRetryEnabled": False, + "sticky": { + "enabled": True + } + } + ] + } + } + }, + name=".", + show_only=["templates/CRD/ingressroute.yaml"] + ) + self.assertIsNone(jmespath.search( + "spec.routes[0].services[0].sticky", docs[0])) + + def test_sticky_session_custom_cookie_name(self): + docs = render_chart( + values={ + "global": { + "ingressRoutes": { + "enabled": True, + "routes": [ + { + "ruleName": "http", + "isRetryEnabled": False, + "sticky": { + "enabled": True, + "name": "my-cookie" + } + } + ] + } + } + }, + name=".", + show_only=["templates/CRD/ingressroute.yaml"] + ) + self.assertEqual( + "my-cookie", + jmespath.search( + "spec.routes[0].services[0].sticky.cookie.name", docs[0]) + ) + + def test_sticky_session_secure_defaults_to_true(self): + docs = render_chart( + values={ + "global": { + "ingressRoutes": { + "enabled": True, + "routes": [ + { + "ruleName": "http", + "isRetryEnabled": False, + "sticky": { + "enabled": True + } + } + ] + } + } + }, + name=".", + show_only=["templates/CRD/ingressroute.yaml"] + ) + self.assertTrue(jmespath.search( + "spec.routes[0].services[0].sticky.cookie.secure", docs[0])) + + def test_sticky_session_http_only_defaults_to_true(self): + docs = render_chart( + values={ + "global": { + "ingressRoutes": { + "enabled": True, + "routes": [ + { + "ruleName": "http", + "isRetryEnabled": False, + "sticky": { + "enabled": True + } + } + ] + } + } + }, + name=".", + show_only=["templates/CRD/ingressroute.yaml"] + ) + self.assertTrue(jmespath.search( + "spec.routes[0].services[0].sticky.cookie.httpOnly", docs[0])) + + def test_sticky_session_http_only_false_is_respected(self): + docs = render_chart( + values={ + "global": { + "ingressRoutes": { + "enabled": True, + "routes": [ + { + "ruleName": "http", + "isRetryEnabled": False, + "sticky": { + "enabled": True, + "httpOnly": False + } + } + ] + } + } + }, + name=".", + show_only=["templates/CRD/ingressroute.yaml"] + ) + self.assertFalse(jmespath.search( + "spec.routes[0].services[0].sticky.cookie.httpOnly", docs[0])) + + def test_sticky_session_same_site_defaults_to_strict(self): + docs = render_chart( + values={ + "global": { + "ingressRoutes": { + "enabled": True, + "routes": [ + { + "ruleName": "http", + "isRetryEnabled": False, + "sticky": { + "enabled": True + } + } + ] + } + } + }, + name=".", + show_only=["templates/CRD/ingressroute.yaml"] + ) + self.assertEqual( + "strict", + jmespath.search( + "spec.routes[0].services[0].sticky.cookie.sameSite", docs[0]) + ) + + def test_sticky_session_custom_same_site(self): + docs = render_chart( + values={ + "global": { + "ingressRoutes": { + "enabled": True, + "routes": [ + { + "ruleName": "http", + "isRetryEnabled": False, + "sticky": { + "enabled": True, + "sameSite": "lax" + } + } + ] + } + } + }, + name=".", + show_only=["templates/CRD/ingressroute.yaml"] + ) + self.assertEqual( + "lax", + jmespath.search( + "spec.routes[0].services[0].sticky.cookie.sameSite", docs[0]) ) \ No newline at end of file From c93efb17b1336f02df659ca5f5b2ab6052f11728 Mon Sep 17 00:00:00 2001 From: Aliaksei Venski Date: Mon, 30 Mar 2026 12:04:26 +0200 Subject: [PATCH 3/3] switch to dig function --- charts/core/templates/CRD/ingressroute.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/core/templates/CRD/ingressroute.yaml b/charts/core/templates/CRD/ingressroute.yaml index 7ed8df7..8c1fdad 100644 --- a/charts/core/templates/CRD/ingressroute.yaml +++ b/charts/core/templates/CRD/ingressroute.yaml @@ -24,7 +24,7 @@ spec: name: {{ $rangeItem.serviceName | default ( include "charts-core.fullname" . ) }} namespace: {{ $rangeItem.serviceNamespace | default .Release.Namespace | quote}} port: {{ $rangeItem.servicePort | default 80 }} - {{- if and $rangeItem.sticky $rangeItem.sticky.enabled (not .Values.global.canary.enabled) }} + {{- if and (dig "sticky" "enabled" false $rangeItem) (not .Values.global.canary.enabled) }} sticky: cookie: name: {{ $rangeItem.sticky.name | default (printf "%s-sticky" (include "charts-core.fullname" .)) }}