Repository: Kong/kubernetes-ingress-controller (go)

Issue:
Check Kong version for generating sticky session algorithm for upstreams
### Is there an existing issue for this?

- [x] I have searched the existing issues

### Does this enhancement require public documentation?

- [x] I have added an Acceptance Criteria item for adding and/or adjusting public documentation (if applicable)

### Problem Statement

KIC did not check version of Kong gateway before generating the `sticky-sessions` algorithm in translated upstreams. Since the `sticky-sessions` loadbalancing algorithm is supported from Kong gateway 3.11, it may cause incompatibility for Kong gateway below version 3.11. We need to check the Kong version before generating such configuration.

### Proposed Solution

- Check Kong version before we translate the `KongUpstreamPolicy` to Kong upstream entity
   - Emit `error` log and generate a `KongConfigurationTranslationFailed` event attached to the `KongUpstreamPolicy` when Kong version does not support
   - Do not add the terminating pods to translated Kong targets when Kong version does not support sticky sessions

### Additional information

_No response_

### Acceptance Criteria

- [ ] When Kong version is below 3.11, KIC should generate `KongConfigurationTranslationFailed` event for`KongUpstreamPolicy` if it specified `sticky-sessions` algorithm and do not translate them
- [ ]  When Kong version is below 3.11, KIC should not add terminating pods to the target list

Pull request description:
feat(translator): Generate KongConfigurationTranslationFailed event when algorithm=sticky-sessions and kong version < 3.11

<!-- Thanks for sending a pull request! Here are some tips for you:
1. If this is your first time, read our contributor guidelines https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#the-pull-request-submit-process and developer guide https://github.com/kubernetes/community/blob/master/contributors/devel/development.md
2. If you want *faster* PR reviews, read how: https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#best-practices-for-faster-reviews
3. Follow the instructions for writing a release note: https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md and ensure your changes are being reflected in CHANGELOG.md for the next upcoming release
-->

**What this PR does / why we need it**:

<!-- Please describe why this particular PR is necessary or why you see it as a nice addition -->
When `KongUpstreamPolicy` set algorithm to `sticky-sessions` and  Kong gateway version < 3.11, generate a `KongConfigurationTranslationFailed` event for services using the `KongUpstreamPolicy` and do not override translated `upstream` by `KongUpstreamPolicy`.

**Which issue this PR fixes**:

closes #7537

<!--
Here you can add any links to issues that this PR is relevant for.
You can use Github keywords (like: closes, fixes or resolves) to auto-resolve
the linked issue(s) when this PR gets merged.

For example: fixes #<issue number>
-->

**Special notes for your reviewer**:

Generation of event for Kong Gateway `< 3.11.0` is covered by dedicated envtest `TestStickySessionsNotSupportedEventGeneration`. Big general envtest `TestConfigErrorEventGenerationInMemoryMode` to pass has to detect this event too, because `3.11.0` is not released yet, so there is no support. But `3.11.0` is around the corner, so when it is updated, this test will fail, and someone needs to adjust it as described in the TODO to cover the situation of not generating this event. There is rather no point in introducing some machinery for testing it now. Proper behavior has been tested manually with the release candidate of 3.11.0, so the update will go smoothly

<!-- Here you can add any open questions or notes that you might have for reviewers -->

**PR Readiness Checklist**:

Complete these before marking the PR as `ready to review`:

- [x] the `CHANGELOG.md` release notes have been updated to reflect any significant (and particularly user-facing) changes introduced by this PR


Patch:
diff --git a/.tools_versions.yaml b/.tools_versions.yaml
index 17ec5e1e7..6e90ede5b 100644
--- a/.tools_versions.yaml
+++ b/.tools_versions.yaml
@@ -1,7 +1,7 @@
 # renovate: datasource=github-releases depName=kubernetes-sigs/controller-tools
 controller-tools: "0.18.0"
 # renovate: datasource=github-releases depName=kubernetes-sigs/kustomize
-kustomize: "5.6.0"
+kustomize: "5.7.0"
 # renovate: datasource=github-releases depName=golangci/golangci-lint
 golangci-lint: "2.1.6"
 # renovate: datasource=github-releases depName=GoogleContainerTools/skaffold
@@ -19,7 +19,7 @@ gotestsum: "1.12.3"
 # renovate: datasource=github-releases depName=dominikh/go-tools
 staticcheck: "2025.1.1"
 # renovate: datasource=github-releases depName=go-delve/delve
-delve: "1.24.2"
+delve: "1.25.0"
 # renovate: datasource=github-releases depName=rhysd/actionlint
 actionlint: "1.7.7"
 # renovate: datasource=github-releases depName=koalaman/shellcheck
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7d7b85e04..78a5c04d0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -116,6 +116,7 @@ Adding a new version? You'll need three changes:
    [#7466](https://github.com/Kong/kubernetes-ingress-controller/pull/7466)
    [#7512](https://github.com/Kong/kubernetes-ingress-controller/pull/7512)
    [#7533](https://github.com/Kong/kubernetes-ingress-controller/pull/7533)
+   [#7538](https://github.com/Kong/kubernetes-ingress-controller/pull/7538)
  - Store the license fetched from Konnect to the `Secret` named `konnect-license-<cpID>`
    in the same namespace where KIC runs. The `cpID` is the ID of the Konnect
    control plane. If the `Secret` does not exist, KIC will create it. However,
diff --git a/Dockerfile.debug b/Dockerfile.debug
index 6a1f53c31..509c7ccb8 100644
--- a/Dockerfile.debug
+++ b/Dockerfile.debug
@@ -48,7 +48,7 @@ RUN --mount=type=cache,target=$GOPATH/pkg/mod \
 # Create an image that runs a debug build with Delve installed
 FROM --platform=$BUILDPLATFORM golang:1.24.4@sha256:dc3de886034b72c0d916022e36986435b0692f3e7d14aa69e2c4d3f8d6b58a03 AS debug
 # renovate: datasource=github-releases depName=go-delve/delve
-RUN go install github.com/go-delve/delve/cmd/dlv@v1.24.2
+RUN go install github.com/go-delve/delve/cmd/dlv@v1.25.0
 # We want all source so Delve file location operations work
 COPY --from=builder /workspace/bin/manager-debug /
 USER 65532:65532
diff --git a/config/crd/incubator/kustomization.yaml b/config/crd/incubator/kustomization.yaml
index 9bdb126fb..aef459bcc 100644
--- a/config/crd/incubator/kustomization.yaml
+++ b/config/crd/incubator/kustomization.yaml
@@ -2,4 +2,4 @@
 apiVersion: kustomize.config.k8s.io/v1beta1
 kind: Kustomization
 resources:
-  - https://github.com/kong/kubernetes-configuration/config/crd/ingress-controller-incubator?ref=403ef6b96f0b190acc5d94c3b6d3f08dc3e405ed # Version is auto-updated by the generating script.
+  - https://github.com/kong/kubernetes-configuration/config/crd/ingress-controller-incubator?ref=d5352741776e75f537fc30220e375fa40fbabc3c # Version is auto-updated by the generating script.
diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml
index affb1e04c..8c7bb1190 100644
--- a/config/crd/kustomization.yaml
+++ b/config/crd/kustomization.yaml
@@ -2,4 +2,4 @@
 apiVersion: kustomize.config.k8s.io/v1beta1
 kind: Kustomization
 resources:
-  - https://github.com/kong/kubernetes-configuration/config/crd/ingress-controller?ref=403ef6b96f0b190acc5d94c3b6d3f08dc3e405ed # Version is auto-updated by the generating script.
+  - https://github.com/kong/kubernetes-configuration/config/crd/ingress-controller?ref=d5352741776e75f537fc30220e375fa40fbabc3c # Version is auto-updated by the generating script.
diff --git a/go.mod b/go.mod
index ce0acf0c9..c5258c74f 100644
--- a/go.mod
+++ b/go.mod
@@ -17,7 +17,7 @@ go 1.24.3
 
 require (
 	cloud.google.com/go/container v1.43.0
-	github.com/Kong/sdk-konnect-go v0.3.1
+	github.com/Kong/sdk-konnect-go v0.4.0
 	github.com/Masterminds/sprig/v3 v3.3.0
 	github.com/avast/retry-go/v4 v4.6.1
 	github.com/blang/semver/v4 v4.0.0
@@ -32,7 +32,7 @@ require (
 	github.com/jpillora/backoff v1.0.0
 	github.com/kong/go-database-reconciler v1.24.2
 	github.com/kong/go-kong v0.67.0
-	github.com/kong/kubernetes-configuration v1.4.1-0.20250623142530-403ef6b96f0b
+	github.com/kong/kubernetes-configuration v1.4.1-0.20250626121935-d5352741776e
 	github.com/kong/kubernetes-telemetry v0.1.10
 	github.com/kong/kubernetes-testing-framework v0.47.2
 	github.com/lithammer/dedent v1.1.0
@@ -116,7 +116,7 @@ require (
 	github.com/cpuguy83/dockercfg v0.3.2 // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/distribution/reference v0.6.0 // indirect
-	github.com/docker/docker v28.2.2+incompatible
+	github.com/docker/docker v28.3.0+incompatible
 	github.com/docker/go-connections v0.5.0
 	github.com/docker/go-units v0.5.0 // indirect
 	github.com/emicklei/go-restful/v3 v3.12.0 // indirect
diff --git a/go.sum b/go.sum
index 4135e1bf7..aa5280f4a 100644
--- a/go.sum
+++ b/go.sum
@@ -20,8 +20,8 @@ github.com/Kong/go-diff v1.2.2 h1:KKKaqHc8IxuguFVIZMNt3bi6YuC/t9r7BGD8bOOpSgM=
 github.com/Kong/go-diff v1.2.2/go.mod h1:nlvdwVZQk3Rm+tbI0cDmKFrOjghtcZTrZBp+UruvvA8=
 github.com/Kong/gojsondiff v1.3.2 h1:qIOVq2mUXt+NXy8Be5gRUee9TP3Ve0MbQSafg9bXKZE=
 github.com/Kong/gojsondiff v1.3.2/go.mod h1:DiIxtU59q4alK7ecP+7k56C5UjgOviJ5gQVR2esEhYw=
-github.com/Kong/sdk-konnect-go v0.3.1 h1:L5/6wQVUpCarBJePaZGGjm5doTKermtAWnIDqPvL5KM=
-github.com/Kong/sdk-konnect-go v0.3.1/go.mod h1:xsmTIkBbmVyUh1nRFjQMOhxYIPDl+sMfmRmPuZHtwLE=
+github.com/Kong/sdk-konnect-go v0.4.0 h1:0mda6v+mmJVMvEC3R5ReL4u5e8kbDhbSCc69yj89sIg=
+github.com/Kong/sdk-konnect-go v0.4.0/go.mod h1:lDQ6eNibkcSYhqZd+A4qhoLbNCrJGtvGOKttJFJX0OQ=
 github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
 github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
 github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
@@ -73,8 +73,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
 github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
-github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw=
-github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/docker v28.3.0+incompatible h1:ffS62aKWupCWdvcee7nBU9fhnmknOqDPaJAMtfK0ImQ=
+github.com/docker/docker v28.3.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
 github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
 github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
 github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@@ -230,8 +230,8 @@ github.com/kong/go-database-reconciler v1.24.2 h1:TNY4dy+uJcmSlWAElye65eVl1Wd1xg
 github.com/kong/go-database-reconciler v1.24.2/go.mod h1:fWjwkA2MDQu1QKLV8qeUCagGmOU4wNhPAQkl5LBfYd0=
 github.com/kong/go-kong v0.67.0 h1:54zXKc58IZpZdlJCv8p95SJjejTxT+cwbWXw97icCak=
 github.com/kong/go-kong v0.67.0/go.mod h1:J0vGB3wsZ2i99zly1zTRe3v7rOKpkhQZRwbcTFP76qM=
-github.com/kong/kubernetes-configuration v1.4.1-0.20250623142530-403ef6b96f0b h1:IbKRfbeP3bchAoPI9LsOwNRjkKLUF8qKGXykN2UqvR4=
-github.com/kong/kubernetes-configuration v1.4.1-0.20250623142530-403ef6b96f0b/go.mod h1:Pqm6hhgk4CvHpw+OIhlVqdpfrNo0B6Z4zkiI9O7V1jM=
+github.com/kong/kubernetes-configuration v1.4.1-0.20250626121935-d5352741776e h1:9wRdBxG8j0G1/BQDApcFaetPdrLtUfUiJ0now8uyVp4=
+github.com/kong/kubernetes-configuration v1.4.1-0.20250626121935-d5352741776e/go.mod h1:Pqm6hhgk4CvHpw+OIhlVqdpfrNo0B6Z4zkiI9O7V1jM=
 github.com/kong/kubernetes-telemetry v0.1.10 h1:V+/Lco8VFY/CzoELwOPcGTyg0zbj/HAvoFkH50UsKYs=
 github.com/kong/kubernetes-telemetry v0.1.10/go.mod h1:/r/FevTOGegCqaxXCJyGkbE1E3IcYUGJd7xX7D73s2Y=
 github.com/kong/kubernetes-testing-framework v0.47.2 h1:+2Z9anTpbV/hwNeN+NFQz53BMU+g3QJydkweBp3tULo=
diff --git a/internal/dataplane/kongstate/kongstate.go b/internal/dataplane/kongstate/kongstate.go
index a0ab33ce1..0c77f431b 100644
--- a/internal/dataplane/kongstate/kongstate.go
+++ b/internal/dataplane/kongstate/kongstate.go
@@ -8,6 +8,7 @@ import (
 	"strconv"
 	"strings"
 
+	"github.com/blang/semver/v4"
 	"github.com/go-logr/logr"
 	"github.com/kong/go-kong/kong"
 	"github.com/samber/lo"
@@ -30,6 +31,7 @@ import (
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/store"
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/util"
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/util/rels"
+	"github.com/kong/kubernetes-ingress-controller/v3/internal/versions"
 )
 
 // KongState holds the configuration that should be applied to Kong.
@@ -250,6 +252,7 @@ func (ks *KongState) FillOverrides(
 	logger logr.Logger,
 	s store.Storer,
 	failuresCollector *failures.ResourceFailuresCollector,
+	kongVersion semver.Version,
 ) {
 	for i := 0; i < len(ks.Services); i++ {
 		// Services
@@ -271,13 +274,14 @@ func (ks *KongState) FillOverrides(
 		}
 	}
 
-	ks.FillUpstreamOverrides(s, logger, failuresCollector)
+	ks.FillUpstreamOverrides(s, logger, failuresCollector, kongVersion)
 }
 
 func (ks *KongState) FillUpstreamOverrides(
 	s store.Storer,
 	logger logr.Logger,
 	failuresCollector *failures.ResourceFailuresCollector,
+	kongVersion semver.Version,
 ) {
 	for i := 0; i < len(ks.Upstreams); i++ {
 		servicesGroup := lo.Values(ks.Upstreams[i].Service.K8sServices)
@@ -298,6 +302,17 @@ func (ks *KongState) FillUpstreamOverrides(
 		if err != nil {
 			failuresCollector.PushResourceFailure(err.Error(), lo.Map(servicesGroup, servicesAsObjects)...)
 		} else if kongUpstreamPolicy != nil {
+			if kongUpstreamPolicy.Spec.Algorithm != nil && *kongUpstreamPolicy.Spec.Algorithm == "sticky-sessions" &&
+				kongVersion.LT(versions.KongStickySessionsCutoff) {
+				failuresCollector.PushResourceFailure(
+					fmt.Sprintf(
+						"sticky sessions algorithm specified in KongUpstreamPolicy '%s' is not supported with Kong Gateway versions < %s",
+						kongUpstreamPolicy.Name, versions.KongStickySessionsCutoff,
+					),
+					lo.Map(servicesGroup, servicesAsObjects)...,
+				)
+				continue
+			}
 			ks.Upstreams[i].overrideByKongUpstreamPolicy(kongUpstreamPolicy)
 		}
 	}
diff --git a/internal/dataplane/translator/translate_upstreams.go b/internal/dataplane/translator/translate_upstreams.go
index e7c014eed..83c8c11ff 100644
--- a/internal/dataplane/translator/translate_upstreams.go
+++ b/internal/dataplane/translator/translate_upstreams.go
@@ -78,7 +78,12 @@ func (t *Translator) getUpstreams(serviceMap map[string]kongstate.Service) ([]ko
 				serviceMap[serviceName] = service
 
 				// get the new targets for this backend service
-				newTargets := getServiceEndpoints(t.logger, t.storer, k8sService, port, t.clusterDomain, t.enableDrainSupport)
+				newTargets := getServiceEndpoints(
+					t.logger, t.storer,
+					k8sService, port,
+					t.clusterDomain,
+					t.enableDrainSupport,
+				)
 
 				if len(newTargets) == 0 {
 					t.logger.V(logging.InfoLevel).Info("No targets could be found for kubernetes service",
diff --git a/internal/dataplane/translator/translator.go b/internal/dataplane/translator/translator.go
index 677e6c00c..785571785 100644
--- a/internal/dataplane/translator/translator.go
+++ b/internal/dataplane/translator/translator.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"errors"
 
+	"github.com/blang/semver/v4"
 	"github.com/go-logr/logr"
 	"github.com/kong/go-kong/kong"
 	"sigs.k8s.io/controller-runtime/pkg/client"
@@ -97,6 +98,7 @@ type Translator struct {
 	storer        store.Storer
 	workspace     string
 	licenseGetter license.Getter
+	kongVersion   semver.Version
 	featureFlags  FeatureFlags
 
 	// schemaServiceProvider provides the schema service required for fetching schemas of custom entities.
@@ -125,6 +127,7 @@ func NewTranslator(
 	logger logr.Logger,
 	storer store.Storer,
 	workspace string,
+	kongVersion semver.Version,
 	featureFlags FeatureFlags,
 	schemaServiceProvider SchemaServiceProvider,
 	config Config,
@@ -141,6 +144,7 @@ func NewTranslator(
 		logger:                     logger,
 		storer:                     storer,
 		workspace:                  workspace,
+		kongVersion:                kongVersion,
 		featureFlags:               featureFlags,
 		schemaServiceProvider:      schemaServiceProvider,
 		failuresCollector:          failuresCollector,
@@ -211,7 +215,7 @@ func (t *Translator) BuildKongConfig() KongConfigBuildingResult {
 	}
 
 	// merge KongIngress with Routes, Services and Upstream
-	result.FillOverrides(t.logger, t.storer, t.failuresCollector)
+	result.FillOverrides(t.logger, t.storer, t.failuresCollector, t.kongVersion)
 
 	// generate consumers and credentials
 	result.FillConsumersAndCredentials(t.logger, t.storer, t.failuresCollector)
diff --git a/internal/manager/run.go b/internal/manager/run.go
index f0c4ada5a..6c58d331f 100644
--- a/internal/manager/run.go
+++ b/internal/manager/run.go
@@ -216,7 +216,7 @@ func New(
 	cache := store.NewCacheStores()
 	storer := store.New(cache, c.IngressClassName, logger)
 
-	configTranslator, err := translator.NewTranslator(logger, storer, c.KongWorkspace, translatorFeatureFlags, NewSchemaServiceGetter(clientsManager),
+	configTranslator, err := translator.NewTranslator(logger, storer, c.KongWorkspace, kongSemVersion, translatorFeatureFlags, NewSchemaServiceGetter(clientsManager),
 		translator.Config{
 			ClusterDomain:      c.ClusterDomain,
 			EnableDrainSupport: c.EnableDrainSupport,
diff --git a/internal/versions/versions.go b/internal/versions/versions.go
index babcb4f1f..0f0d8e373 100644
--- a/internal/versions/versions.go
+++ b/internal/versions/versions.go
@@ -10,5 +10,8 @@ var KICv3VersionCutoff = semver.Version{Major: 3, Minor: 4, Patch: 1}
 // KongRedirectPluginCutoff is the lowest version of Kong Gateway that supports `redirect` plugin.
 var KongRedirectPluginCutoff = semver.Version{Major: 3, Minor: 9, Patch: 0}
 
+// KongStickySessionsCutoff is the lowest version of Kong Gateway that supports `sticky_sessions` loadbalancing algorithm.
+var KongStickySessionsCutoff = semver.Version{Major: 3, Minor: 11, Patch: 0}
+
 // DeckFileFormatVersion is the version of the decK file format used by KIC everywhere.
 const DeckFileFormatVersion = "3.0"


Test patch:
diff --git a/.github/test_dependencies.yaml b/.github/test_dependencies.yaml
index 6bb9054a1..9eaf760c1 100644
--- a/.github/test_dependencies.yaml
+++ b/.github/test_dependencies.yaml
@@ -57,3 +57,6 @@ kongintegration:
 envtests:
   # renovate: datasource=docker depName=kong/kong-gateway versioning=docker
   kong-ee: '3.9.1.1'
+  # This is a version of Kong Gateway that does not support sticky sessions,
+  # all versions >= 3.11.0 support sticky sessions.
+  kong-without-sticky-sessions: '3.9.1'
diff --git a/internal/dataplane/kong_client_golden_test.go b/internal/dataplane/kong_client_golden_test.go
index e92d3f128..4616955c6 100644
--- a/internal/dataplane/kong_client_golden_test.go
+++ b/internal/dataplane/kong_client_golden_test.go
@@ -13,6 +13,7 @@ import (
 	"testing"
 	"time"
 
+	"github.com/blang/semver/v4"
 	"github.com/go-logr/zapr"
 	"github.com/kong/go-kong/kong"
 	"github.com/samber/lo"
@@ -259,7 +260,7 @@ func runKongClientGoldenTest(t *testing.T, tc kongClientGoldenTestCase) {
 	// Create the translator.
 	logger := zapr.NewLogger(zap.NewNop())
 	s := store.New(cacheStores, "kong", logger)
-	p, err := translator.NewTranslator(logger, s, "", tc.featureFlags, fakeSchemaServiceProvier{},
+	p, err := translator.NewTranslator(logger, s, "", semver.MustParse("3.9.1"), tc.featureFlags, fakeSchemaServiceProvier{},
 		translator.Config{
 			ClusterDomain:      consts.DefaultClusterDomain,
 			EnableDrainSupport: consts.DefaultEnableDrainSupport,
diff --git a/internal/dataplane/kongstate/kongstate_test.go b/internal/dataplane/kongstate/kongstate_test.go
index 54040144b..c0bf4bbc3 100644
--- a/internal/dataplane/kongstate/kongstate_test.go
+++ b/internal/dataplane/kongstate/kongstate_test.go
@@ -9,6 +9,7 @@ import (
 	"testing"
 	"time"
 
+	"github.com/blang/semver/v4"
 	"github.com/go-logr/logr"
 	"github.com/go-logr/logr/testr"
 	"github.com/go-logr/zapr"
@@ -37,6 +38,7 @@ import (
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/store"
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/util"
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/util/rels"
+	"github.com/kong/kubernetes-ingress-controller/v3/internal/versions"
 )
 
 var kongConsumerTypeMeta = metav1.TypeMeta{
@@ -49,6 +51,8 @@ var serviceTypeMeta = metav1.TypeMeta{
 	Kind:       "Service",
 }
 
+var defaultTestKongVersion = semver.MustParse("3.9.1")
+
 func TestKongState_SanitizedCopy(t *testing.T) {
 	testedFields := sets.New[string]()
 	for _, tt := range []struct {
@@ -1550,13 +1554,15 @@ func TestKongState_FillUpstreamOverrides(t *testing.T) {
 	testCases := []struct {
 		name                 string
 		upstream             Upstream
+		kongVersion          semver.Version
 		kongUpstreamPolicies []*configurationv1beta1.KongUpstreamPolicy
 		kongIngresses        []*configurationv1.KongIngress
 		expectedUpstream     kong.Upstream
 		expectedFailures     []failures.ResourceFailure
 	}{
 		{
-			name: "upstream with no overrides",
+			name:        "upstream with no overrides",
+			kongVersion: defaultTestKongVersion,
 			upstream: Upstream{
 				Upstream: kong.Upstream{
 					Name: kong.String("foo-upstream"),
@@ -1567,7 +1573,8 @@ func TestKongState_FillUpstreamOverrides(t *testing.T) {
 			},
 		},
 		{
-			name: "upstream backed by service annotated with KongUpstreamPolicy",
+			name:        "upstream backed by service annotated with KongUpstreamPolicy",
+			kongVersion: defaultTestKongVersion,
 			upstream: Upstream{
 				Upstream: kong.Upstream{
 					Name: kong.String("foo-upstream"),
@@ -1593,7 +1600,8 @@ func TestKongState_FillUpstreamOverrides(t *testing.T) {
 			},
 		},
 		{
-			name: "upstream backed by service annotated with KongUpstreamPolicy that doesn't exist",
+			name:        "upstream backed by service annotated with KongUpstreamPolicy that doesn't exist",
+			kongVersion: defaultTestKongVersion,
 			upstream: Upstream{
 				Upstream: kong.Upstream{
 					Name: kong.String("foo-upstream"),
@@ -1613,7 +1621,8 @@ func TestKongState_FillUpstreamOverrides(t *testing.T) {
 			},
 		},
 		{
-			name: "KongUpstreamPolicy is applied even if KongIngress is not found",
+			name:        "KongUpstreamPolicy is applied even if KongIngress is not found",
+			kongVersion: defaultTestKongVersion,
 			upstream: Upstream{
 				Upstream: kong.Upstream{
 					Name: kong.String("foo-upstream"),
@@ -1645,7 +1654,8 @@ func TestKongState_FillUpstreamOverrides(t *testing.T) {
 			},
 		},
 		{
-			name: "KongUpstreamPolicy overwrites KongIngress",
+			name:        "KongUpstreamPolicy overwrites KongIngress",
+			kongVersion: defaultTestKongVersion,
 			upstream: Upstream{
 				Upstream: kong.Upstream{
 					Name: kong.String("foo-upstream"),
@@ -1681,6 +1691,65 @@ func TestKongState_FillUpstreamOverrides(t *testing.T) {
 				Algorithm: kong.String("least-connections"),
 			},
 		},
+		{
+			name:        "KongUpstreamPolicy with algorithm sticky-sessions is not supported for Kong < 3.11",
+			kongVersion: defaultTestKongVersion,
+			upstream: Upstream{
+				Upstream: kong.Upstream{
+					Name: kong.String("foo-upstream"),
+				},
+				Service: Service{
+					K8sServices: map[string]*corev1.Service{"": serviceAnnotatedWithKongUpstreamPolicy()},
+				},
+			},
+			kongUpstreamPolicies: []*configurationv1beta1.KongUpstreamPolicy{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      kongUpstreamPolicyName,
+						Namespace: "default",
+					},
+					Spec: configurationv1beta1.KongUpstreamPolicySpec{
+						Algorithm: lo.ToPtr("sticky-sessions"),
+					},
+				},
+			},
+			expectedUpstream: kong.Upstream{
+				Name: kong.String("foo-upstream"),
+			},
+			expectedFailures: []failures.ResourceFailure{
+				lo.Must(failures.NewResourceFailure(
+					"sticky sessions algorithm specified in KongUpstreamPolicy 'policy' is not supported with Kong Gateway versions < 3.11.0",
+					serviceAnnotatedWithKongUpstreamPolicy(),
+				)),
+			},
+		},
+		{
+			name:        "KongUpstreamPolicy with algorithm sticky-sessions is supported for Kong >= 3.11",
+			kongVersion: versions.KongStickySessionsCutoff,
+			upstream: Upstream{
+				Upstream: kong.Upstream{
+					Name: kong.String("foo-upstream"),
+				},
+				Service: Service{
+					K8sServices: map[string]*corev1.Service{"": serviceAnnotatedWithKongUpstreamPolicy()},
+				},
+			},
+			kongUpstreamPolicies: []*configurationv1beta1.KongUpstreamPolicy{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      kongUpstreamPolicyName,
+						Namespace: "default",
+					},
+					Spec: configurationv1beta1.KongUpstreamPolicySpec{
+						Algorithm: lo.ToPtr("sticky-sessions"),
+					},
+				},
+			},
+			expectedUpstream: kong.Upstream{
+				Name:      kong.String("foo-upstream"),
+				Algorithm: kong.String("sticky-sessions"),
+			},
+		},
 	}
 
 	for _, tc := range testCases {
@@ -1693,7 +1762,7 @@ func TestKongState_FillUpstreamOverrides(t *testing.T) {
 			failuresCollector := failures.NewResourceFailuresCollector(logr.Discard())
 
 			kongState := KongState{Upstreams: []Upstream{tc.upstream}}
-			kongState.FillUpstreamOverrides(s, logr.Discard(), failuresCollector)
+			kongState.FillUpstreamOverrides(s, logr.Discard(), failuresCollector, tc.kongVersion)
 			require.Equal(t, tc.expectedUpstream, kongState.Upstreams[0].Upstream)
 			require.ElementsMatch(t, tc.expectedFailures, failuresCollector.PopResourceFailures())
 		})
@@ -1988,7 +2057,7 @@ func TestFillOverrides_ServiceFailures(t *testing.T) {
 			require.NoError(t, err)
 			logger := zapr.NewLogger(zap.NewNop())
 			failuresCollector := failures.NewResourceFailuresCollector(logger)
-			tt.state.FillOverrides(logger, store, failuresCollector)
+			tt.state.FillOverrides(logger, store, failuresCollector, defaultTestKongVersion)
 			if len(tt.expectedTranslationFailureMessages) > 0 {
 				translationFailures := failuresCollector.PopResourceFailures()
 				for nsName, expectedMessage := range tt.expectedTranslationFailureMessages {
diff --git a/internal/dataplane/translator/translator_test.go b/internal/dataplane/translator/translator_test.go
index c960c24c1..78a2c14c6 100644
--- a/internal/dataplane/translator/translator_test.go
+++ b/internal/dataplane/translator/translator_test.go
@@ -7,6 +7,7 @@ import (
 	"testing"
 	"time"
 
+	"github.com/blang/semver/v4"
 	"github.com/go-logr/zapr"
 	"github.com/kong/go-kong/kong"
 	"github.com/samber/lo"
@@ -5430,12 +5431,14 @@ func (p fakeSchemaServiceProvier) GetSchemaService() kong.AbstractSchemaService
 func mustNewTranslator(t *testing.T, storer store.Storer) *Translator {
 	logger := zapr.NewLogger(zap.NewNop())
 	p, err := NewTranslator(logger, storer, "",
+		semver.MustParse("3.9.1"),
 		FeatureFlags{
 			// We'll assume these are true for all tests.
 			FillIDs:                           true,
 			ReportConfiguredKubernetesObjects: true,
 			KongServiceFacade:                 true,
 		},
+
 		fakeSchemaServiceProvier{},
 		Config{
 			EnableDrainSupport: consts.DefaultEnableDrainSupport,
diff --git a/test/e2e/manifests/all-in-one-dbless-k4k8s-enterprise.yaml b/test/e2e/manifests/all-in-one-dbless-k4k8s-enterprise.yaml
index 6a0d02b6f..da4c3aaae 100644
--- a/test/e2e/manifests/all-in-one-dbless-k4k8s-enterprise.yaml
+++ b/test/e2e/manifests/all-in-one-dbless-k4k8s-enterprise.yaml
@@ -2865,6 +2865,10 @@ spec:
             is set.
           rule: 'has(self.spec.stickySessions) ? has(self.spec.stickySessions.cookie)
             : true'
+        - message: spec.algorithm must be set to 'sticky-sessions' when spec.stickySessions
+            is set.
+          rule: 'has(self.spec.stickySessions) ? (has(self.spec.algorithm) && self.spec.algorithm
+            == "sticky-sessions") : true'
     served: true
     storage: true
     subresources:
diff --git a/test/e2e/manifests/all-in-one-dbless-konnect-enterprise.yaml b/test/e2e/manifests/all-in-one-dbless-konnect-enterprise.yaml
index 3afa365ac..64243a9fa 100644
--- a/test/e2e/manifests/all-in-one-dbless-konnect-enterprise.yaml
+++ b/test/e2e/manifests/all-in-one-dbless-konnect-enterprise.yaml
@@ -2865,6 +2865,10 @@ spec:
             is set.
           rule: 'has(self.spec.stickySessions) ? has(self.spec.stickySessions.cookie)
             : true'
+        - message: spec.algorithm must be set to 'sticky-sessions' when spec.stickySessions
+            is set.
+          rule: 'has(self.spec.stickySessions) ? (has(self.spec.algorithm) && self.spec.algorithm
+            == "sticky-sessions") : true'
     served: true
     storage: true
     subresources:
diff --git a/test/e2e/manifests/all-in-one-dbless-konnect.yaml b/test/e2e/manifests/all-in-one-dbless-konnect.yaml
index 490a290a4..bfbba2742 100644
--- a/test/e2e/manifests/all-in-one-dbless-konnect.yaml
+++ b/test/e2e/manifests/all-in-one-dbless-konnect.yaml
@@ -2865,6 +2865,10 @@ spec:
             is set.
           rule: 'has(self.spec.stickySessions) ? has(self.spec.stickySessions.cookie)
             : true'
+        - message: spec.algorithm must be set to 'sticky-sessions' when spec.stickySessions
+            is set.
+          rule: 'has(self.spec.stickySessions) ? (has(self.spec.algorithm) && self.spec.algorithm
+            == "sticky-sessions") : true'
     served: true
     storage: true
     subresources:
diff --git a/test/e2e/manifests/all-in-one-dbless.yaml b/test/e2e/manifests/all-in-one-dbless.yaml
index c4f560613..036fdc1e7 100644
--- a/test/e2e/manifests/all-in-one-dbless.yaml
+++ b/test/e2e/manifests/all-in-one-dbless.yaml
@@ -2865,6 +2865,10 @@ spec:
             is set.
           rule: 'has(self.spec.stickySessions) ? has(self.spec.stickySessions.cookie)
             : true'
+        - message: spec.algorithm must be set to 'sticky-sessions' when spec.stickySessions
+            is set.
+          rule: 'has(self.spec.stickySessions) ? (has(self.spec.algorithm) && self.spec.algorithm
+            == "sticky-sessions") : true'
     served: true
     storage: true
     subresources:
diff --git a/test/e2e/manifests/all-in-one-postgres-enterprise.yaml b/test/e2e/manifests/all-in-one-postgres-enterprise.yaml
index 174e97b46..06ce09565 100644
--- a/test/e2e/manifests/all-in-one-postgres-enterprise.yaml
+++ b/test/e2e/manifests/all-in-one-postgres-enterprise.yaml
@@ -2865,6 +2865,10 @@ spec:
             is set.
           rule: 'has(self.spec.stickySessions) ? has(self.spec.stickySessions.cookie)
             : true'
+        - message: spec.algorithm must be set to 'sticky-sessions' when spec.stickySessions
+            is set.
+          rule: 'has(self.spec.stickySessions) ? (has(self.spec.algorithm) && self.spec.algorithm
+            == "sticky-sessions") : true'
     served: true
     storage: true
     subresources:
diff --git a/test/e2e/manifests/all-in-one-postgres-multiple-gateways.yaml b/test/e2e/manifests/all-in-one-postgres-multiple-gateways.yaml
index 49eccdc37..efc330f56 100644
--- a/test/e2e/manifests/all-in-one-postgres-multiple-gateways.yaml
+++ b/test/e2e/manifests/all-in-one-postgres-multiple-gateways.yaml
@@ -2865,6 +2865,10 @@ spec:
             is set.
           rule: 'has(self.spec.stickySessions) ? has(self.spec.stickySessions.cookie)
             : true'
+        - message: spec.algorithm must be set to 'sticky-sessions' when spec.stickySessions
+            is set.
+          rule: 'has(self.spec.stickySessions) ? (has(self.spec.algorithm) && self.spec.algorithm
+            == "sticky-sessions") : true'
     served: true
     storage: true
     subresources:
diff --git a/test/e2e/manifests/all-in-one-postgres.yaml b/test/e2e/manifests/all-in-one-postgres.yaml
index 429a6480c..31fb37267 100644
--- a/test/e2e/manifests/all-in-one-postgres.yaml
+++ b/test/e2e/manifests/all-in-one-postgres.yaml
@@ -2865,6 +2865,10 @@ spec:
             is set.
           rule: 'has(self.spec.stickySessions) ? has(self.spec.stickySessions.cookie)
             : true'
+        - message: spec.algorithm must be set to 'sticky-sessions' when spec.stickySessions
+            is set.
+          rule: 'has(self.spec.stickySessions) ? (has(self.spec.algorithm) && self.spec.algorithm
+            == "sticky-sessions") : true'
     served: true
     storage: true
     subresources:
diff --git a/test/envtest/configerrorevent_envtest_test.go b/test/envtest/configerrorevent_envtest_test.go
index 24099887c..215cb9c2d 100644
--- a/test/envtest/configerrorevent_envtest_test.go
+++ b/test/envtest/configerrorevent_envtest_test.go
@@ -20,10 +20,10 @@ import (
 	netv1 "k8s.io/api/networking/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/client-go/kubernetes/scheme"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	configurationv1 "github.com/kong/kubernetes-configuration/api/configuration/v1"
+	configurationv1beta1 "github.com/kong/kubernetes-configuration/api/configuration/v1beta1"
 
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/annotations"
 	"github.com/kong/kubernetes-ingress-controller/v3/internal/dataplane"
@@ -38,8 +38,10 @@ func TestConfigErrorEventGenerationInMemoryMode(t *testing.T) {
 	ctx, cancel := context.WithCancel(t.Context())
 	defer cancel()
 
-	restConfig := Setup(t, scheme.Scheme)
-	ctrlClient := NewControllerClient(t, scheme.Scheme, restConfig)
+	scheme := Scheme(t, WithKong, WithGatewayAPI)
+
+	restConfig := Setup(t, scheme)
+	ctrlClient := NewControllerClient(t, scheme, restConfig)
 
 	ns := CreateNamespace(ctx, t, ctrlClient)
 	ingressClassName := "kongenvtest"
@@ -55,12 +57,36 @@ func TestConfigErrorEventGenerationInMemoryMode(t *testing.T) {
 	deployment.Namespace = ns.Name
 	require.NoError(t, ctrlClient.Create(ctx, deployment))
 
+	t.Log("creating a KongUpstreamPolicy with sticky sessions configuration")
+	upstreamPolicy := &configurationv1beta1.KongUpstreamPolicy{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "echo-drain-policy",
+			Namespace: ns.Name,
+			Annotations: map[string]string{
+				annotations.IngressClassKey: ingressClassName,
+			},
+		},
+		Spec: configurationv1beta1.KongUpstreamPolicySpec{
+			Algorithm: lo.ToPtr("sticky-sessions"),
+			Slots:     lo.ToPtr(100),
+			HashOn: &configurationv1beta1.KongUpstreamHash{
+				Input: lo.ToPtr(configurationv1beta1.HashInput("none")),
+			},
+			StickySessions: &configurationv1beta1.KongUpstreamStickySessions{
+				Cookie:     "session-id",
+				CookiePath: lo.ToPtr("/"),
+			},
+		},
+	}
+	require.NoError(t, ctrlClient.Create(ctx, upstreamPolicy))
+
 	t.Logf("exposing deployment %s via service", deployment.Name)
 	service := generators.NewServiceForDeployment(deployment, corev1.ServiceTypeLoadBalancer)
 	service.Annotations = map[string]string{
 		// TCP services cannot have paths, and we don't catch this as a translation error
-		"konghq.com/protocol": "tcp",
-		"konghq.com/path":     "/aitmatov",
+		"konghq.com/protocol":        "tcp",
+		"konghq.com/path":            "/aitmatov",
+		"konghq.com/upstream-policy": upstreamPolicy.Name,
 		// Referencing non-existent KongPlugins.
 		"konghq.com/plugins": "foo,bar,n1:p1",
 	}
@@ -86,10 +112,11 @@ func TestConfigErrorEventGenerationInMemoryMode(t *testing.T) {
 		),
 		WithPublishService(ns.Name),
 		WithIngressClass(ingressClassName),
+		WithKongUpstreamPolicyEnabled(),
 		WithProxySyncSeconds(0.1),
 	)
 
-	const numberOfExpectedEvents = 12
+	const numberOfExpectedEvents = 13
 	collectedEvents := collectGeneratedEvents(
 		ctx, t, ctrlClient, ns, t.Name(), numberOfExpectedEvents,
 	)
@@ -107,6 +134,9 @@ func TestConfigErrorEventGenerationInMemoryMode(t *testing.T) {
 		predicate(corev1.EventTypeWarning, dataplane.KongConfigurationTranslationFailedEventReason, "Service", service.Name, `^no grant found to referenced "n1:p1" plugin in the requested remote KongPlugin bind$`),
 		predicate(corev1.EventTypeWarning, dataplane.FallbackKongConfigurationApplyFailedEventReason, "Service", service.Name, `^invalid service:httpbin\.httpbin\.80: failed conditional validation given value of field 'protocol'$`),
 		predicate(corev1.EventTypeWarning, dataplane.FallbackKongConfigurationApplyFailedEventReason, "Pod", podName, `failed to apply fallback Kong configuration to http://[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+:[0-9]+: HTTP status 400 \(message: "failed posting new config to /config"\)`),
+		// TODO: Remove this event once we start using Kong Gateway >= 3.11.0, because sticky sessions type is supported there.
+		// Adjust also numberOfExpectedEvents constant above.
+		predicate(corev1.EventTypeWarning, dataplane.KongConfigurationTranslationFailedEventReason, "Service", service.Name, `^sticky sessions algorithm specified in KongUpstreamPolicy 'echo-drain-policy' is not supported with Kong Gateway versions < 3\.11\.0$`),
 	}
 
 	assertExpectedEvents(t, predicatesToCheck, collectedEvents)
@@ -176,6 +206,92 @@ func TestConfigErrorEventGenerationDBMode(t *testing.T) {
 	assertExpectedEvents(t, predicatesToCheck, collectedEvents)
 }
 
+func TestStickySessionsNotSupportedEventGeneration(t *testing.T) {
+	// Can't be run in parallel because we're using t.Setenv() below which doesn't allow it.
+
+	ctx, cancel := context.WithCancel(t.Context())
+	defer cancel()
+
+	scheme := Scheme(t, WithKong)
+	restConfig := Setup(t, scheme)
+	ctrlClientGlobal := NewControllerClient(t, scheme, restConfig)
+	ns := CreateNamespace(ctx, t, ctrlClientGlobal)
+	ctrlClient := client.NewNamespacedClient(ctrlClientGlobal, ns.Name)
+
+	ingressClassName := "kongenvtest"
+	deployIngressClass(ctx, t, ingressClassName, ctrlClient)
+
+	const podName = "kong-ingress-controller-tyjh1"
+	t.Setenv("POD_NAMESPACE", ns.Name)
+	t.Setenv("POD_NAME", podName)
+
+	t.Log("deploying a minimal HTTP container deployment to test Ingress routes")
+	container := generators.NewContainer("httpbin", test.HTTPBinImage, test.HTTPBinPort)
+	deployment := generators.NewDeploymentForContainer(container)
+	deployment.Namespace = ns.Name
+	require.NoError(t, ctrlClient.Create(ctx, deployment))
+
+	t.Log("creating a KongUpstreamPolicy with sticky sessions configuration")
+	upstreamPolicy := &configurationv1beta1.KongUpstreamPolicy{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "echo-drain-policy",
+			Namespace: ns.Name,
+			Annotations: map[string]string{
+				annotations.IngressClassKey: ingressClassName,
+			},
+		},
+		Spec: configurationv1beta1.KongUpstreamPolicySpec{
+			Algorithm: lo.ToPtr("sticky-sessions"),
+			Slots:     lo.ToPtr(100),
+			HashOn: &configurationv1beta1.KongUpstreamHash{
+				Input: lo.ToPtr(configurationv1beta1.HashInput("none")),
+			},
+			StickySessions: &configurationv1beta1.KongUpstreamStickySessions{
+				Cookie:     "session-id",
+				CookiePath: lo.ToPtr("/"),
+			},
+		},
+	}
+	require.NoError(t, ctrlClient.Create(ctx, upstreamPolicy))
+
+	t.Logf("exposing deployment %s via service", deployment.Name)
+	service := generators.NewServiceForDeployment(deployment, corev1.ServiceTypeLoadBalancer)
+	service.Annotations = map[string]string{
+		"konghq.com/upstream-policy": upstreamPolicy.Name,
+	}
+	service.Namespace = ns.Name
+	require.NoError(t, ctrlClient.Create(ctx, service))
+
+	t.Logf("creating an ingress for service %s with invalid configuration", service.Name)
+	ingress := generators.NewIngressForService("/bar", nil, service)
+	ingress.Spec.IngressClassName = lo.ToPtr(ingressClassName)
+	ingress.Namespace = ns.Name
+	t.Logf("deploying ingress %s", ingress.Name)
+	require.NoError(t, ctrlClient.Create(ctx, ingress))
+
+	kongContainer := runKongGatewayWithoutStickySessionsSupport(ctx, t)
+	RunManager(ctx, t, restConfig,
+		AdminAPIOptFns(),
+		WithPublishService(ns.Name),
+		WithIngressClass(ingressClassName),
+		WithKongServiceFacadeFeatureEnabled(),
+		WithProxySyncSeconds(0.1),
+		WithKongAdminURLs(kongContainer.AdminURL(ctx, t)),
+	)
+
+	const numberOfExpectedEvents = 2
+	collectedEvents := collectGeneratedEvents(
+		ctx, t, ctrlClient, ns, t.Name(), numberOfExpectedEvents,
+	)
+
+	predicatesToCheck := []func(e corev1.Event) bool{
+		predicate(corev1.EventTypeNormal, dataplane.KongConfigurationApplySucceededEventReason, "Pod", podName, `successfully applied Kong configuration to http://([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+:[0-9]+|localhost:[0-9]+)`),
+		predicate(corev1.EventTypeWarning, dataplane.KongConfigurationTranslationFailedEventReason, "Service", service.Name, `^sticky sessions algorithm specified in KongUpstreamPolicy 'echo-drain-policy' is not supported with Kong Gateway versions < 3\.11\.0$`),
+	}
+
+	assertExpectedEvents(t, predicatesToCheck, collectedEvents)
+}
+
 func predicate(eventType, eventReason, invObjKind, invObjName, msgToMatch string) func(e corev1.Event) bool {
 	return func(e corev1.Event) bool {
 		ok, err := regexp.MatchString(msgToMatch, e.Message)
@@ -204,12 +320,13 @@ func collectGeneratedEvents(
 		collectedEvents = lo.Filter(events.Items, func(e corev1.Event, _ int) bool {
 			return e.Annotations[consts.InstanceIDAnnotationKey] == expectedInstanceID
 		})
-		require.Len(c, collectedEvents, numberOfExpectedEvents)
+		require.Len(c, collectedEvents, numberOfExpectedEvents, "number of events mismatch")
 	}, waitTime, tickTime)
 	return collectedEvents
 }
 
 func assertExpectedEvents(t *testing.T, predicatesToCheck []func(e corev1.Event) bool, collectedEvents []corev1.Event) {
+	t.Helper()
 	for pi, predicate := range predicatesToCheck {
 		lenBefore := len(collectedEvents)
 		collectedEvents = lo.Reject(collectedEvents, func(e corev1.Event, _ int) bool {
@@ -223,7 +340,7 @@ func assertExpectedEvents(t *testing.T, predicatesToCheck []func(e corev1.Event)
 	if !assert.Equal(t, 0, len(collectedEvents), "expected all warning events to match test predicates, but some were left") {
 		t.Logf("remaining events %d:", len(collectedEvents))
 		for _, e := range collectedEvents {
-			t.Logf("  - %s %s %s: %s", e.InvolvedObject.Kind, e.InvolvedObject.Name, e.Reason, e.Message)
+			t.Logf("  - %s | %s | %s | %s | %s", e.Type, e.Reason, e.InvolvedObject.Kind, e.InvolvedObject.Name, e.Message)
 		}
 	}
 }
@@ -370,5 +487,5 @@ func formatDBRootResponse(version string) []byte {
 			]
 		}
 	}`
-	return []byte(fmt.Sprintf(defaultDBLessRootResponse, version))
+	return fmt.Appendf(nil, defaultDBLessRootResponse, version)
 }
diff --git a/test/envtest/kong.go b/test/envtest/kong.go
index dd0ace958..702803789 100644
--- a/test/envtest/kong.go
+++ b/test/envtest/kong.go
@@ -25,3 +25,18 @@ func runKongEnterprise(ctx context.Context, t *testing.T) containers.Kong {
 
 	return containers.NewKong(ctx, t, withEnvtestsVersion)
 }
+
+// runKongGatewayWithoutStickySessionsSupport runs a Kong Gateway container that does
+// not support sticky sessions, since every version >= 3.11.0 supports sticky sessions.
+func runKongGatewayWithoutStickySessionsSupport(ctx context.Context, t *testing.T) containers.Kong {
+	// Get the Kong Gateway version to use for the test from `test_dependencies.yaml` file.
+	gatewayTag, err := testenv.GetDependencyVersion("envtests.kong-without-sticky-sessions")
+	require.NoError(t, err)
+
+	// Prepare the container config modifier to set the Kong Gateway version.
+	withEnvtestsVersion := func(request *testcontainers.ContainerRequest) {
+		request.Image = fmt.Sprintf("kong/kong:%s", gatewayTag)
+	}
+
+	return containers.NewKong(ctx, t, withEnvtestsVersion)
+}
diff --git a/test/envtest/run.go b/test/envtest/run.go
index 43c6d2417..f282ed5d8 100644
--- a/test/envtest/run.go
+++ b/test/envtest/run.go
@@ -159,6 +159,12 @@ func WithKongServiceFacadeFeatureEnabled() func(cfg *managercfg.Config) {
 	}
 }
 
+func WithKongUpstreamPolicyEnabled() func(cfg *managercfg.Config) {
+	return func(cfg *managercfg.Config) {
+		cfg.KongUpstreamPolicyEnabled = true
+	}
+}
+
 func WithKongAdminURLs(urls ...string) func(cfg *managercfg.Config) {
 	return func(cfg *managercfg.Config) {
 		cfg.KongAdminURLs = urls