Repository: Kong/kubernetes-ingress-controller
Pull request #2506 (2022-05-20 04:26:19)

fix: Refactors the function return for `store` pkg
Signed-off-by: afzal442 <afzal442@gmail.com>

<!--  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**:

**Which issue this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close that issue when PR gets merged)*: fixes #

**Special notes for your reviewer**:

**PR Readiness Checklist**:

Complete these before marking the PR as `ready to review`:

- [ ] the `CHANGELOG.md` release notes have been updated to reflect any significant (and particularly user-facing) changes introduced by this PR


CLAassistant: [![CLA assistant check](https://cla-assistant.io/pull/badge/not_signed)](https://cla-assistant.io/Kong/kubernetes-ingress-controller?pullRequest=2506) <br/>Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our [Contributor License Agreement](https://cla-assistant.io/Kong/kubernetes-ingress-controller?pullRequest=2506) before we can accept your contribution.<br/><sub>You have signed the CLA already but the status is still pending? Let us [recheck](https://cla-assistant.io/check/Kong/kubernetes-ingress-controller?pullRequest=2506) it.</sub>
afzal442: hi @rainest, could you review this plz? Thanks.
shaneutt: @afzal442 we had a CI failure in `main` over the last few days due to our CI testing suite picking up the new kubernetes `v1.24.0` and it having some backwards incompatible changes, it looks like you've inherited that problem as well. Please rebase against the current `main` branch and this should take care of the troubles.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1564d03aa..e724f415f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -53,6 +53,23 @@
 
 #### Added
 
+- A new gated feature called `CombinedRoutes` has been added. Historically
+  a `kong.Route` would be created for _each path_ on an `Ingress` resource
+  in the phase where Kubernetes resources are translated to Kong Admin API
+  configuration. This new feature changes how `Ingress` resources are
+  translated so that a single route can be created for any unique combination
+  of ingress object, hostname, service and port which has multiple paths.
+  This option is helpful for end-users who are making near constant changes
+  to their configs (e.g. constantly adding, updating, and removing `Ingress`
+  resources) at scale, and users that have enormous numbers of paths all
+  pointing to a single Kubernetes `Service` as it can significantly reduce
+  the overall size of the dataplane configuration that is pushed to the Kong
+  Admin API. This feature is expected to be disruptive (routes may be dropped
+  briefly in postgres mode when switching to this mode) so for the moment it
+  is behind a feature gate while we continue to iterate on it and evaluate it
+  and seek a point where it would become the default behavior. Enable it with
+  the controller argument `--feature-gates=CombinedRoutes`.
+  [#2490](https://github.com/Kong/kubernetes-ingress-controller/issues/2490)
 - `UDPRoute` resources now support multiple backendRefs for load-balancing.
   [#2405](https://github.com/Kong/kubernetes-ingress-controller/issues/2405)
 - `TCPRoute` resources now support multiple backendRefs for load-balancing.
@@ -83,6 +100,12 @@
   [#1766](https://github.com/Kong/kubernetes-ingress-controller/issues/1766)
 - Added support for `TLSRoute` resources.
   [#2476](https://github.com/Kong/kubernetes-ingress-controller/issues/2476)
+- Added `--term-delay` flag to support setting a time delay before processing
+  `SIGTERM` and `SIGINT` signals. This was added to specifically help in
+  situations where the Kong Gateway has a load-balancer in front of it to help
+  stagger and stabilize the shutdown procedure when the load-balancer is
+  draining or otherwise needs to remove the Gateway from it's rotation.
+  [#2494](https://github.com/Kong/kubernetes-ingress-controller/pull/2494)
 
 #### Fixed
 
diff --git a/FEATURE_GATES.md b/FEATURE_GATES.md
index 33cf04ab0..eb25d9bbb 100644
--- a/FEATURE_GATES.md
+++ b/FEATURE_GATES.md
@@ -60,9 +60,10 @@ Features that reach GA and over time become stable will be removed from this tab
 
 {{< table caption="Feature gates for features in Alpha or Beta states" >}}
 
-| Feature | Default | Stage | Since | Until |
-|---------|---------|-------|-------|-------|
-| Knative | `true`  | Alpha | 0.8.0 | TBD   |
-| Gateway | `false` | Alpha | TBD   | TBD   |
+| Feature        | Default | Stage | Since | Until |
+|---------       |---------|-------|-------|-------|
+| Knative        | `true`  | Alpha | 0.8.0 | TBD   |
+| Gateway        | `false` | Alpha | 2.2.0 | TBD   |
+| CombinedRoutes | `false` | Alpha | 2.4.0 | TBD   |
 
 {{< /table > }}
diff --git a/Makefile b/Makefile
index 523b828f8..6a37f005c 100644
--- a/Makefile
+++ b/Makefile
@@ -189,8 +189,9 @@ debug-container:
 NCPU ?= $(shell getconf _NPROCESSORS_ONLN)
 PKG_LIST = ./pkg/...,./internal/...
 KIND_CLUSTER_NAME ?= "integration-tests"
-INTEGRATION_TEST_TIMEOUT ?= "25m"
-E2E_TEST_TIMEOUT ?= "30m"
+INTEGRATION_TEST_TIMEOUT ?= "45m"
+E2E_TEST_TIMEOUT ?= "45m"
+KONG_CONTROLLER_FEATURE_GATES ?= Gateway=true
 
 .PHONY: test
 test: test.unit
@@ -208,7 +209,10 @@ test.conformance:
 		./test/conformance
 
 .PHONY: test.integration
-test.integration: test.integration.enterprise.postgres  test.integration.dbless test.integration.postgres
+test.integration: test.integration.dbless test.integration.postgres
+
+.PHONY: test.integration.enterprise
+test.integration.enterprise: test.integration.enterprise.postgres
 
 .PHONY: test.unit
 test.unit:
@@ -222,7 +226,8 @@ test.unit:
 .PHONY: test.integration.dbless
 test.integration.dbless:
 	@./scripts/check-container-environment.sh
-	@TEST_DATABASE_MODE="off" GOFLAGS="-tags=integration_tests" go test -v -race \
+	@TEST_DATABASE_MODE="off" GOFLAGS="-tags=integration_tests" KONG_CONTROLLER_FEATURE_GATES=$(KONG_CONTROLLER_FEATURE_GATES) \
+		go test -v -race \
 		-timeout $(INTEGRATION_TEST_TIMEOUT) \
 		-parallel $(NCPU) \
 		-race \
@@ -234,7 +239,8 @@ test.integration.dbless:
 .PHONY: test.integration.postgres
 test.integration.postgres:
 	@./scripts/check-container-environment.sh
-	@TEST_DATABASE_MODE="postgres" GOFLAGS="-tags=integration_tests" go test -v \
+	@TEST_DATABASE_MODE="postgres" GOFLAGS="-tags=integration_tests" KONG_CONTROLLER_FEATURE_GATES=$(KONG_CONTROLLER_FEATURE_GATES) \
+		go test -v \
 		-timeout $(INTEGRATION_TEST_TIMEOUT) \
 		-parallel $(NCPU) \
 		-race \
@@ -246,7 +252,8 @@ test.integration.postgres:
 .PHONY: test.integration.enterprise.postgres
 test.integration.enterprise.postgres:
 	@./scripts/check-container-environment.sh
-	@TEST_DATABASE_MODE="postgres" TEST_KONG_ENTERPRISE="true" GOFLAGS="-tags=integration_tests" go test -v \
+	@TEST_DATABASE_MODE="postgres" TEST_KONG_ENTERPRISE="true" GOFLAGS="-tags=integration_tests" KONG_CONTROLLER_FEATURE_GATES=$(KONG_CONTROLLER_FEATURE_GATES) \
+		go test -v \
 		-timeout $(INTEGRATION_TEST_TIMEOUT) \
 		-parallel $(NCPU) \
 		-race \
@@ -255,10 +262,6 @@ test.integration.enterprise.postgres:
 		-coverprofile=coverage.enterprisepostgres.out \
 		./test/integration
 
-.PHONY: test.integration.legacy
-test.integration.legacy: container
-	KIC_IMAGE="${IMAGE}:${TAG}" KUBE_VERSION=${KUBE_VERSION} ./hack/legacy/test/test.sh
-
 .PHONY: test.e2e
 test.e2e:
 	GOFLAGS="-tags=e2e_tests" go test -v \
@@ -295,14 +298,14 @@ debug: install
 		--kong-admin-url $(KONG_ADMIN_URL) \
 		--publish-service $(KONG_NAMESPACE)/$(KONG_PROXY_SERVICE) \
 		--kubeconfig $(KUBECONFIG) \
-		--feature-gates=Gateway=true
+		--feature-gates=$(KONG_CONTROLLER_FEATURE_GATES)
 
 run: install
 	go run ./internal/cmd/main.go \
 		--kong-admin-url $(KONG_ADMIN_URL) \
 		--publish-service $(KONG_NAMESPACE)/$(KONG_PROXY_SERVICE) \
 		--kubeconfig $(KUBECONFIG) \
-		--feature-gates=Gateway=true
+		--feature-gates=$(KONG_CONTROLLER_FEATURE_GATES)
 
 install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config.
 	$(KUSTOMIZE) build config/crd | kubectl apply -f -
diff --git a/examples/ingress.yaml b/examples/ingress.yaml
index 58203a143..1b79e6934 100644
--- a/examples/ingress.yaml
+++ b/examples/ingress.yaml
@@ -11,7 +11,7 @@ metadata:
   labels:
     app: httpbin
 spec:
-  replicas: 3
+  replicas: 1
   selector:
     matchLabels:
       app: httpbin
diff --git a/go.mod b/go.mod
index 81b1a1da2..b53dc2858 100644
--- a/go.mod
+++ b/go.mod
@@ -27,9 +27,9 @@ require (
 	github.com/spf13/pflag v1.0.5
 	github.com/stretchr/testify v1.7.1
 	github.com/tidwall/gjson v1.14.1
-	golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4
-	google.golang.org/api v0.80.0
-	google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3
+	golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2
+	google.golang.org/api v0.81.0
+	google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd
 	k8s.io/api v0.24.0
 	k8s.io/apiextensions-apiserver v0.24.0
 	k8s.io/apimachinery v0.24.0
@@ -77,7 +77,7 @@ require (
 	github.com/google/go-querystring v1.1.0 // indirect
 	github.com/google/gofuzz v1.2.0 // indirect
 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
-	github.com/googleapis/gax-go/v2 v2.3.0 // indirect
+	github.com/googleapis/gax-go/v2 v2.4.0 // indirect
 	github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
 	github.com/hashicorp/go-memdb v1.3.3 // indirect
 	github.com/hashicorp/golang-lru v0.5.4 // indirect
@@ -125,16 +125,16 @@ require (
 	golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect
 	golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
 	golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 // indirect
-	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
-	golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 // indirect
+	golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 // indirect
+	golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
 	golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
 	golang.org/x/text v0.3.7 // indirect
 	golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
 	golang.org/x/tools v0.1.10-0.20220218145154-897bd77cd717 // indirect
-	golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f // indirect
+	golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect
 	gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
-	google.golang.org/grpc v1.46.0 // indirect
+	google.golang.org/grpc v1.46.2 // indirect
 	google.golang.org/protobuf v1.28.0 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
diff --git a/go.sum b/go.sum
index 16756d18c..eb161273f 100644
--- a/go.sum
+++ b/go.sum
@@ -470,8 +470,9 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m
 github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
 github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
 github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM=
-github.com/googleapis/gax-go/v2 v2.3.0 h1:nRJtk3y8Fm770D42QV6T90ZnvFZyk7agSo3Q+Z9p3WI=
 github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM=
+github.com/googleapis/gax-go/v2 v2.4.0 h1:dS9eYAjhrE2RjmzYw2XAPvcXfmcQLtFEQWn0CR82awk=
+github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c=
 github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU=
 github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA=
 github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=
@@ -1201,8 +1202,9 @@ golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su
 golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA=
 golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 h1:NWy5+hlRbC7HK+PmcXVUmW1IMyFce7to56IUvhUFm7Y=
+golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1236,8 +1238,9 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/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.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 h1:w8s32wxx3sY+OjLlv9qltkLU5yvJzxjjgiHWLjdIcw4=
+golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1337,8 +1340,9 @@ golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 h1:nonptSpoQ4vQjyraW20DXPAglgQfVnM9ZC6MmNLMR60=
-golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -1476,8 +1480,9 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
 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=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f h1:GGU+dLjvlC3qDwqYgL6UgRmHXhOOgns0bZu2Ty5mm6U=
 golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df h1:5Pf6pFKu98ODmgnpvkJ3kFUOQGGLIzLIkbzUHp47618=
+golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
 gomodules.xyz/jsonpatch/v2 v2.2.0 h1:4pT439QV83L+G9FkcCriY6EkpcK6r6bK+A5FBUMI7qY=
 gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY=
 google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
@@ -1520,8 +1525,9 @@ google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/S
 google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8=
 google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs=
 google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=
-google.golang.org/api v0.80.0 h1:IQWaGVCYnsm4MO3hh+WtSXMzMzuyFx/fuR8qkN3A0Qo=
-google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg=
+google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw=
+google.golang.org/api v0.81.0 h1:o8WF5AvfidafWbFjsRyupxyEQJNUWxLZJCK5NXrxZZ8=
+google.golang.org/api v0.81.0/go.mod h1:FA6Mb/bZxj706H2j+j2d6mHEEaHBmbbWnkfvmorOCko=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -1616,8 +1622,10 @@ google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX
 google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
 google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
 google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
-google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3 h1:q1kiSVscqoDeqTF27eQ2NnLLDmqF0I373qQNXYMy0fo=
+google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
 google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
+google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd h1:e0TwkXOdbnH/1x5rc5MZ/VYyiZ4v+RdVfrGMqEwT68I=
+google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
 google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
@@ -1653,8 +1661,9 @@ google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzI
 google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
 google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
 google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
-google.golang.org/grpc v1.46.0 h1:oCjezcn6g6A75TGoKYBPgKmVBLexhYLM6MebdrPApP8=
 google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
+google.golang.org/grpc v1.46.2 h1:u+MLGgVf7vRdjEYZ8wDFhAVNmhkbJ5hmrA1LMWK1CAQ=
+google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
diff --git a/internal/cmd/fips/main.go b/internal/cmd/fips/main.go
index 5119ac57a..9463e212a 100644
--- a/internal/cmd/fips/main.go
+++ b/internal/cmd/fips/main.go
@@ -6,13 +6,11 @@ package main
 import (
 	_ "crypto/tls/fipsonly"
 
-	ctrl "sigs.k8s.io/controller-runtime"
-
 	"github.com/kong/kubernetes-ingress-controller/v2/internal/cmd/rootcmd"
 )
 
 //go:generate go run github.com/kong/kubernetes-ingress-controller/v2/hack/generators/controllers/networking
 
 func main() {
-	rootcmd.Execute(ctrl.SetupSignalHandler())
+	rootcmd.Execute()
 }
diff --git a/internal/cmd/main.go b/internal/cmd/main.go
index 73018ff83..56c3d100c 100644
--- a/internal/cmd/main.go
+++ b/internal/cmd/main.go
@@ -1,13 +1,11 @@
 package main
 
 import (
-	ctrl "sigs.k8s.io/controller-runtime"
-
 	"github.com/kong/kubernetes-ingress-controller/v2/internal/cmd/rootcmd"
 )
 
 //go:generate go run github.com/kong/kubernetes-ingress-controller/v2/hack/generators/controllers/networking
 
 func main() {
-	rootcmd.Execute(ctrl.SetupSignalHandler())
+	rootcmd.Execute()
 }
diff --git a/internal/cmd/rootcmd/rootcmd.go b/internal/cmd/rootcmd/rootcmd.go
index b3874ccb8..e838d96c0 100644
--- a/internal/cmd/rootcmd/rootcmd.go
+++ b/internal/cmd/rootcmd/rootcmd.go
@@ -2,8 +2,6 @@
 package rootcmd
 
 import (
-	"context"
-
 	"github.com/spf13/cobra"
 
 	"github.com/kong/kubernetes-ingress-controller/v2/internal/manager"
@@ -18,12 +16,12 @@ func init() {
 var rootCmd = &cobra.Command{
 	PersistentPreRunE: bindEnvVars,
 	RunE: func(cmd *cobra.Command, args []string) error {
-		return Run(cmd.Context(), &cfg)
+		return Run(&cfg)
 	},
 	SilenceUsage: true,
 }
 
 // Execute is the entry point to the controller manager.
-func Execute(ctx context.Context) {
-	cobra.CheckErr(rootCmd.ExecuteContext(ctx))
+func Execute() {
+	cobra.CheckErr(rootCmd.Execute())
 }
diff --git a/internal/cmd/rootcmd/run.go b/internal/cmd/rootcmd/run.go
index 5522f78ca..aadf0c642 100644
--- a/internal/cmd/rootcmd/run.go
+++ b/internal/cmd/rootcmd/run.go
@@ -1,13 +1,17 @@
 package rootcmd
 
 import (
-	"context"
 	"fmt"
 
 	"github.com/kong/kubernetes-ingress-controller/v2/internal/manager"
 )
 
-func Run(ctx context.Context, c *manager.Config) error {
+func Run(c *manager.Config) error {
+	ctx, err := SetupSignalHandler(c)
+	if err != nil {
+		return fmt.Errorf("failed to setup signal handler: %w", err)
+	}
+
 	diag, err := StartDiagnosticsServer(ctx, manager.DiagnosticsPort, c)
 	if err != nil {
 		return fmt.Errorf("failed to start diagnostics server: %w", err)
diff --git a/internal/cmd/rootcmd/signal.go b/internal/cmd/rootcmd/signal.go
new file mode 100644
index 000000000..3cd1c0ef5
--- /dev/null
+++ b/internal/cmd/rootcmd/signal.go
@@ -0,0 +1,61 @@
+package rootcmd
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"os"
+	"os/signal"
+	"sync"
+	"syscall"
+	"time"
+
+	"github.com/bombsimon/logrusr/v2"
+
+	"github.com/kong/kubernetes-ingress-controller/v2/internal/manager"
+	"github.com/kong/kubernetes-ingress-controller/v2/internal/util"
+)
+
+var mutex sync.Mutex
+var shutdownSignals = []os.Signal{os.Interrupt, syscall.SIGTERM}
+
+// SetupSignalHandler registers for SIGTERM and SIGINT. A context is returned
+// which is canceled on one of these signals. If a second signal is not caught, the program
+// will delay for the configured period of time before terminating. If a second signal is caught,
+// the program is terminated with exit code 1.
+func SetupSignalHandler(cfg *manager.Config) (context.Context, error) {
+
+	// This will prevent multiple signal handlers from being created
+	if ok := mutex.TryLock(); !ok {
+		return nil, errors.New("signal handler can only be setup once")
+	}
+
+	deprecatedLogger, err := util.MakeLogger(cfg.LogLevel, cfg.LogFormat)
+	if err != nil {
+		return nil, err
+	}
+	logger := logrusr.New(deprecatedLogger)
+
+	ctx, cancel := context.WithCancel(context.Background())
+
+	c := make(chan os.Signal, 2)
+	signal.Notify(c, shutdownSignals...)
+	go func() {
+		<-c
+		logger.Info("Signal received, shutting down", "timeout", fmt.Sprint(cfg.TermDelay))
+
+		select {
+		case <-time.After(cfg.TermDelay):
+			cancel()
+		case <-c:
+			logger.Info("Signal received during termination delay, exiting immediately")
+			os.Exit(1) // second signal. Exit directly.
+		}
+
+		<-c
+		logger.Info("Signal received during graceful shutdown, exiting immediately")
+		os.Exit(1) // second signal. Exit directly.
+	}()
+
+	return ctx, nil
+}
diff --git a/internal/dataplane/kong_client.go b/internal/dataplane/kong_client.go
index 37fc6fe9b..df5a5c010 100644
--- a/internal/dataplane/kong_client.go
+++ b/internal/dataplane/kong_client.go
@@ -41,6 +41,12 @@ type KongClient struct {
 	// updates to the data-plane.
 	enableReverseSync bool
 
+	// enableCombinedServiceRoutes indicates that when translating Kubernetes
+	// ingress objects into Kong Admin API configuration we should disable the
+	// legacy logic which would create a single route per path and instead use
+	// the newer logic which combines them.
+	enableCombinedServiceRoutes bool
+
 	// skipCACertificates disables CA certificates, to avoid fighting over configuration in multi-workspace
 	// environments. See https://github.com/Kong/deck/pull/617
 	skipCACertificates bool
@@ -77,6 +83,10 @@ type KongClient struct {
 	// kubernetes object reporting functionality.
 	kubernetesObjectReportLock sync.RWMutex
 
+	// additionalFeaturesLock is a mutex to enable thread-safety of enabling or
+	// disabling various features.
+	additionalFeaturesLock sync.RWMutex
+
 	// kubernetesObjectStatusQueue is a queue that needs to be messaged whenever
 	// a Kubernetes object has had configuration for itself successfully applied
 	// to the data-plane: messages will trigger reconciliation in the control plane
@@ -238,6 +248,30 @@ func (c *KongClient) KubernetesObjectIsConfigured(obj client.Object) bool {
 	return c.kubernetesObjectReportsFilter.Has(obj)
 }
 
+// -----------------------------------------------------------------------------
+// Dataplane Client - Kong - Optional Features
+// -----------------------------------------------------------------------------
+
+// EnableCombinedServiceRoutes turns on the combined service routes feature for
+// the Kong Dataplane client.
+func (c *KongClient) EnableCombinedServiceRoutes() {
+	c.additionalFeaturesLock.Lock()
+	defer c.additionalFeaturesLock.Unlock()
+	c.enableCombinedServiceRoutes = true
+}
+
+// AreCombinedServiceRoutesEnabled determines whether the combined service
+// routes translation mode has been enabled, or if the legacy logic is being
+// used. When enabled this changes the logic to try and combine multiple paths
+// into single routes, but it also changes the names of existing routes and so
+// it should be considered disruptive as it will temporarily drop routes when
+// it's first enabled.
+func (c *KongClient) AreCombinedServiceRoutesEnabled() bool {
+	c.additionalFeaturesLock.RLock()
+	defer c.additionalFeaturesLock.RUnlock()
+	return c.enableCombinedServiceRoutes
+}
+
 // -----------------------------------------------------------------------------
 // Dataplane Client - Kong - Interface Implementation
 // -----------------------------------------------------------------------------
@@ -265,6 +299,9 @@ func (c *KongClient) Update(ctx context.Context) error {
 	if c.AreKubernetesObjectReportsEnabled() {
 		p.EnableKubernetesObjectReports()
 	}
+	if c.AreCombinedServiceRoutesEnabled() {
+		p.EnableCombinedServiceRoutes()
+	}
 
 	// parse the Kubernetes objects from the storer into Kong configuration
 	kongstate, err := p.Build()
diff --git a/internal/dataplane/kongstate/service.go b/internal/dataplane/kongstate/service.go
index 05ba96842..b2b69b759 100644
--- a/internal/dataplane/kongstate/service.go
+++ b/internal/dataplane/kongstate/service.go
@@ -11,6 +11,33 @@ import (
 	configurationv1 "github.com/kong/kubernetes-ingress-controller/v2/pkg/apis/configuration/v1"
 )
 
+// Services is a list of kongstate.Service objects with sorting enabled based
+// on a lexographical comparison of the underlying kong.Service names which are
+// always expected to be unique.
+type Services []*Service
+
+func (s Services) Len() int {
+	return len(s)
+}
+
+func (s Services) Swap(i, j int) {
+	s[i], s[j] = s[j], s[i]
+}
+
+func (s Services) Less(i, j int) bool {
+	a := ""
+	if s[i].Service.Name != nil {
+		a = *s[i].Service.Name
+	}
+
+	b := ""
+	if s[j].Service.Name != nil {
+		b = *s[j].Service.Name
+	}
+
+	return strings.Compare(a, b) == -1
+}
+
 // Service represents a service in Kong and holds routes associated with the
 // service and other k8s metadata.
 type Service struct {
diff --git a/internal/dataplane/parser/parser.go b/internal/dataplane/parser/parser.go
index f0dc33771..cacd3ea11 100644
--- a/internal/dataplane/parser/parser.go
+++ b/internal/dataplane/parser/parser.go
@@ -34,10 +34,12 @@ import (
 // equivalent Kong objects and configurations, producing a complete
 // state configuration for the Kong Admin API.
 type Parser struct {
-	logger                            logrus.FieldLogger
-	storer                            store.Storer
-	reportConfiguredKubernetesObjects bool
-	configuredKubernetesObjects       []client.Object
+	logger                      logrus.FieldLogger
+	storer                      store.Storer
+	configuredKubernetesObjects []client.Object
+
+	featureEnabledReportConfiguredKubernetesObjects bool
+	featureEnabledCombinedServiceRoutes             bool
 }
 
 // NewParser produces a new Parser object provided a logging mechanism
@@ -119,14 +121,14 @@ func (p *Parser) Build() (*kongstate.KongState, error) {
 // were successfully parsed. Objects tracked this way can be retrieved by
 // calling GenerateKubernetesObjectReport().
 func (p *Parser) EnableKubernetesObjectReports() {
-	p.reportConfiguredKubernetesObjects = true
+	p.featureEnabledReportConfiguredKubernetesObjects = true
 }
 
 // ReportKubernetesObjectUpdate reports an update to a Kubernetes object if
 // updates have been requested. If the parser has not been configured to
 // report Kubernetes object updates this is a no-op.
 func (p *Parser) ReportKubernetesObjectUpdate(obj client.Object) {
-	if p.reportConfiguredKubernetesObjects {
+	if p.featureEnabledReportConfiguredKubernetesObjects {
 		p.configuredKubernetesObjects = append(p.configuredKubernetesObjects, obj)
 	}
 }
@@ -141,6 +143,18 @@ func (p *Parser) GenerateKubernetesObjectReport() []client.Object {
 	return report
 }
 
+// -----------------------------------------------------------------------------
+// Parser - Public Methods - Other Optional Features
+// -----------------------------------------------------------------------------
+
+// EnableCombinedServiceRoutes changes the translation logic from the legacy
+// mode which would create a kong.Route object per each individual path on
+// an Ingress object to a mode that can combine routes for paths where the
+// service name, host and port match for those paths.
+func (p *Parser) EnableCombinedServiceRoutes() {
+	p.featureEnabledCombinedServiceRoutes = true
+}
+
 // -----------------------------------------------------------------------------
 // Parser - Private Methods
 // -----------------------------------------------------------------------------
diff --git a/internal/dataplane/parser/translate_ingress.go b/internal/dataplane/parser/translate_ingress.go
index ba1a3a19a..af1b12e93 100644
--- a/internal/dataplane/parser/translate_ingress.go
+++ b/internal/dataplane/parser/translate_ingress.go
@@ -11,6 +11,7 @@ import (
 	networkingv1beta1 "k8s.io/api/networking/v1beta1"
 
 	"github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/kongstate"
+	"github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/parser/translators"
 	"github.com/kong/kubernetes-ingress-controller/v2/internal/util"
 )
 
@@ -187,72 +188,80 @@ func (p *Parser) ingressRulesFromIngressV1() ingressRules {
 		result.SecretNameToSNIs.addFromIngressV1TLS(ingressSpec.TLS, ingress.Namespace)
 
 		var objectSuccessfullyParsed bool
-		for i, rule := range ingressSpec.Rules {
-			if rule.HTTP == nil {
-				continue
+
+		if p.featureEnabledCombinedServiceRoutes {
+			for _, kongStateService := range translators.TranslateIngress(ingress) {
+				result.ServiceNameToServices[*kongStateService.Service.Name] = *kongStateService
 			}
-			for j, rulePath := range rule.HTTP.Paths {
-				if strings.Contains(rulePath.Path, "//") {
-					log.Errorf("rule skipped: invalid path: '%v'", rulePath.Path)
+			objectSuccessfullyParsed = true
+		} else {
+			for i, rule := range ingressSpec.Rules {
+				if rule.HTTP == nil {
 					continue
 				}
+				for j, rulePath := range rule.HTTP.Paths {
+					if strings.Contains(rulePath.Path, "//") {
+						log.Errorf("rule skipped: invalid path: '%v'", rulePath.Path)
+						continue
+					}
 
-				pathType := networkingv1.PathTypeImplementationSpecific
-				if rulePath.PathType != nil {
-					pathType = *rulePath.PathType
-				}
-
-				paths, err := pathsFromK8s(rulePath.Path, pathType)
-				if err != nil {
-					log.WithError(err).Error("rule skipped: pathsFromK8s")
-					continue
-				}
+					pathType := networkingv1.PathTypeImplementationSpecific
+					if rulePath.PathType != nil {
+						pathType = *rulePath.PathType
+					}
 
-				r := kongstate.Route{
-					Ingress: util.FromK8sObject(ingress),
-					Route: kong.Route{
-						Name:              kong.String(fmt.Sprintf("%s.%s.%d%d", ingress.Namespace, ingress.Name, i, j)),
-						Paths:             paths,
-						StripPath:         kong.Bool(false),
-						PreserveHost:      kong.Bool(true),
-						Protocols:         kong.StringSlice("http", "https"),
-						RegexPriority:     kong.Int(priorityForPath[pathType]),
-						RequestBuffering:  kong.Bool(true),
-						ResponseBuffering: kong.Bool(true),
-					},
-				}
-				if rule.Host != "" {
-					r.Hosts = kong.StringSlice(rule.Host)
-				}
+					paths, err := pathsFromK8s(rulePath.Path, pathType)
+					if err != nil {
+						log.WithError(err).Error("rule skipped: pathsFromK8s")
+						continue
+					}
 
-				port := PortDefFromServiceBackendPort(&rulePath.Backend.Service.Port)
-				serviceName := fmt.Sprintf("%s.%s.%s", ingress.Namespace, rulePath.Backend.Service.Name,
-					serviceBackendPortToStr(rulePath.Backend.Service.Port))
-				service, ok := result.ServiceNameToServices[serviceName]
-				if !ok {
-					service = kongstate.Service{
-						Service: kong.Service{
-							Name: kong.String(serviceName),
-							Host: kong.String(fmt.Sprintf("%s.%s.%s.svc", rulePath.Backend.Service.Name, ingress.Namespace,
-								port.CanonicalString())),
-							Port:           kong.Int(DefaultHTTPPort),
-							Protocol:       kong.String("http"),
-							Path:           kong.String("/"),
-							ConnectTimeout: kong.Int(DefaultServiceTimeout),
-							ReadTimeout:    kong.Int(DefaultServiceTimeout),
-							WriteTimeout:   kong.Int(DefaultServiceTimeout),
-							Retries:        kong.Int(DefaultRetries),
+					r := kongstate.Route{
+						Ingress: util.FromK8sObject(ingress),
+						Route: kong.Route{
+							Name:              kong.String(fmt.Sprintf("%s.%s.%d%d", ingress.Namespace, ingress.Name, i, j)),
+							Paths:             paths,
+							StripPath:         kong.Bool(false),
+							PreserveHost:      kong.Bool(true),
+							Protocols:         kong.StringSlice("http", "https"),
+							RegexPriority:     kong.Int(priorityForPath[pathType]),
+							RequestBuffering:  kong.Bool(true),
+							ResponseBuffering: kong.Bool(true),
 						},
-						Namespace: ingress.Namespace,
-						Backends: []kongstate.ServiceBackend{{
-							Name:    rulePath.Backend.Service.Name,
-							PortDef: port,
-						}},
 					}
+					if rule.Host != "" {
+						r.Hosts = kong.StringSlice(rule.Host)
+					}
+
+					port := PortDefFromServiceBackendPort(&rulePath.Backend.Service.Port)
+					serviceName := fmt.Sprintf("%s.%s.%s", ingress.Namespace, rulePath.Backend.Service.Name,
+						serviceBackendPortToStr(rulePath.Backend.Service.Port))
+					service, ok := result.ServiceNameToServices[serviceName]
+					if !ok {
+						service = kongstate.Service{
+							Service: kong.Service{
+								Name: kong.String(serviceName),
+								Host: kong.String(fmt.Sprintf("%s.%s.%s.svc", rulePath.Backend.Service.Name, ingress.Namespace,
+									port.CanonicalString())),
+								Port:           kong.Int(DefaultHTTPPort),
+								Protocol:       kong.String("http"),
+								Path:           kong.String("/"),
+								ConnectTimeout: kong.Int(DefaultServiceTimeout),
+								ReadTimeout:    kong.Int(DefaultServiceTimeout),
+								WriteTimeout:   kong.Int(DefaultServiceTimeout),
+								Retries:        kong.Int(DefaultRetries),
+							},
+							Namespace: ingress.Namespace,
+							Backends: []kongstate.ServiceBackend{{
+								Name:    rulePath.Backend.Service.Name,
+								PortDef: port,
+							}},
+						}
+					}
+					service.Routes = append(service.Routes, r)
+					result.ServiceNameToServices[serviceName] = service
+					objectSuccessfullyParsed = true
 				}
-				service.Routes = append(service.Routes, r)
-				result.ServiceNameToServices[serviceName] = service
-				objectSuccessfullyParsed = true
 			}
 		}
 
diff --git a/internal/dataplane/parser/translators/ingress.go b/internal/dataplane/parser/translators/ingress.go
new file mode 100644
index 000000000..899060d86
--- /dev/null
+++ b/internal/dataplane/parser/translators/ingress.go
@@ -0,0 +1,248 @@
+package translators
+
+import (
+	"fmt"
+	"sort"
+	"strings"
+	"time"
+
+	"github.com/kong/go-kong/kong"
+	networkingv1 "k8s.io/api/networking/v1"
+
+	"github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/kongstate"
+	"github.com/kong/kubernetes-ingress-controller/v2/internal/util"
+)
+
+// -----------------------------------------------------------------------------
+// Ingress Translation - Public Functions
+// -----------------------------------------------------------------------------
+
+// TranslateIngress receives a Kubernetes ingress object and from it will
+// produce a translated set of kong.Services and kong.Routes which will come
+// wrapped in a kongstate.Service object.
+func TranslateIngress(ingress *networkingv1.Ingress) []*kongstate.Service {
+	index := &ingressTranslationIndex{cache: make(map[string]*ingressTranslationMeta)}
+	index.add(ingress)
+	kongStateServices := kongstate.Services(index.translate())
+	sort.Sort(kongStateServices)
+	return kongStateServices
+}
+
+// -----------------------------------------------------------------------------
+// Ingress Translation - Private Consts & Vars
+// -----------------------------------------------------------------------------
+
+var defaultHTTPIngressPathType = networkingv1.PathTypePrefix
+
+const (
+	defaultHTTPPort = 80
+	defaultRetries  = 5
+
+	// defaultServiceTimeout indicates the amount of time by default that we wait
+	// for connections to an underlying Kubernetes service to complete in the
+	// data-plane. The current value is based on a historical default that started
+	// in version 0 of the ingress controller.
+	defaultServiceTimeout = time.Second * 60
+)
+
+// -----------------------------------------------------------------------------
+// Ingress Translation - Private - Index
+// -----------------------------------------------------------------------------
+
+// ingressTranslationIndex is a de-duplicating index of the contents of a
+// Kubernetes Ingress resource, where the key is a combination of data from
+// that resource which makes it unique for the purpose of translating it into
+// kong.Services and kong.Routes and the value is a combination of various
+// metadata needed to configure and name those kong.Services and kong.Routes
+// plus the URL paths which make those routes actionable. This index is used
+// to enable compiling a minimal set of kong.Routes when translating into
+// Kong resources for each rule in the ingress spec, where the combination of:
+//
+// - ingress.Namespace
+// - ingress.Name
+// - host for the Ingress rule
+// - Kubernetes Service for the Ingress rule
+// - the port for the Kubernetes Service
+//
+// are unique. For ingress spec rules which are not unique along those
+// data-points, a separate kong.Service and separate kong.Routes will be created
+// for each unique combination.
+type ingressTranslationIndex struct {
+	cache map[string]*ingressTranslationMeta
+}
+
+func (i *ingressTranslationIndex) add(ingress *networkingv1.Ingress) {
+	for _, ingressRule := range ingress.Spec.Rules {
+		if ingressRule.HTTP == nil || len(ingressRule.HTTP.Paths) < 1 {
+			continue
+		}
+
+		for _, httpIngressPath := range ingressRule.HTTP.Paths {
+			httpIngressPath.Path = flattenMultipleSlashes(httpIngressPath.Path)
+
+			if httpIngressPath.Path == "" {
+				httpIngressPath.Path = "/"
+			}
+
+			if httpIngressPath.PathType == nil {
+				httpIngressPath.PathType = &defaultHTTPIngressPathType
+			}
+
+			serviceName := httpIngressPath.Backend.Service.Name
+			servicePort := httpIngressPath.Backend.Service.Port.Number
+
+			cacheKey := fmt.Sprintf("%s%s%s%s%d", ingress.Namespace, ingress.Name, ingressRule.Host, serviceName, servicePort)
+			meta, ok := i.cache[cacheKey]
+			if !ok {
+				meta = &ingressTranslationMeta{
+					ingressNamespace: ingress.Namespace,
+					ingressName:      ingress.Name,
+					ingressHost:      ingressRule.Host,
+					serviceName:      serviceName,
+					servicePort:      servicePort,
+				}
+			}
+
+			meta.paths = append(meta.paths, httpIngressPath)
+			meta.ingressAnnotations = ingress.Annotations
+			i.cache[cacheKey] = meta
+		}
+	}
+}
+
+func (i *ingressTranslationIndex) translate() []*kongstate.Service {
+	kongStateServiceCache := make(map[string]*kongstate.Service)
+	for _, meta := range i.cache {
+		portDef := kongstate.PortDef{
+			Number: meta.servicePort,
+			Mode:   kongstate.PortModeByNumber,
+		}
+
+		kongServiceName := fmt.Sprintf("%s.%s.%s.%s", meta.ingressNamespace, meta.ingressName, meta.serviceName, portDef.CanonicalString())
+		kongStateService, ok := kongStateServiceCache[kongServiceName]
+		if !ok {
+			kongStateService = meta.translateIntoKongStateService(kongServiceName, portDef)
+		}
+
+		route := meta.translateIntoKongRoutes()
+		kongStateService.Routes = append(kongStateService.Routes, *route)
+
+		kongStateServiceCache[kongServiceName] = kongStateService
+	}
+
+	kongStateServices := make([]*kongstate.Service, 0, len(kongStateServiceCache))
+	for _, kongStateService := range kongStateServiceCache {
+		kongStateServices = append(kongStateServices, kongStateService)
+	}
+
+	return kongStateServices
+}
+
+// -----------------------------------------------------------------------------
+// Ingress Translation - Private - Metadata
+// -----------------------------------------------------------------------------
+
+type ingressTranslationMeta struct {
+	ingressAnnotations map[string]string
+	ingressNamespace   string
+	ingressName        string
+	ingressHost        string
+	serviceName        string
+	servicePort        int32
+	paths              []networkingv1.HTTPIngressPath
+}
+
+func (m *ingressTranslationMeta) translateIntoKongStateService(kongServiceName string, portDef kongstate.PortDef) *kongstate.Service {
+	return &kongstate.Service{
+		Namespace: m.ingressNamespace,
+		Service: kong.Service{
+			Name:           kong.String(kongServiceName),
+			Host:           kong.String(fmt.Sprintf("%s.%s.%d.svc", m.serviceName, m.ingressNamespace, portDef.Number)),
+			Port:           kong.Int(defaultHTTPPort),
+			Protocol:       kong.String("http"),
+			Path:           kong.String("/"),
+			ConnectTimeout: kong.Int(int(defaultServiceTimeout.Milliseconds())),
+			ReadTimeout:    kong.Int(int(defaultServiceTimeout.Milliseconds())),
+			WriteTimeout:   kong.Int(int(defaultServiceTimeout.Milliseconds())),
+			Retries:        kong.Int(defaultRetries),
+		},
+		Backends: []kongstate.ServiceBackend{{
+			Name:      m.serviceName,
+			Namespace: m.ingressNamespace,
+			PortDef:   portDef,
+		}},
+	}
+}
+
+func (m *ingressTranslationMeta) translateIntoKongRoutes() *kongstate.Route {
+	routeName := fmt.Sprintf("%s.%s.%s.%s.%d", m.ingressNamespace, m.ingressName, m.serviceName, m.ingressHost, m.servicePort)
+	route := &kongstate.Route{
+		Ingress: util.K8sObjectInfo{
+			Namespace:   m.ingressNamespace,
+			Name:        m.ingressName,
+			Annotations: m.ingressAnnotations,
+		},
+		Route: kong.Route{
+			Name:              kong.String(routeName),
+			StripPath:         kong.Bool(false),
+			PreserveHost:      kong.Bool(true),
+			Protocols:         kong.StringSlice("http", "https"),
+			RegexPriority:     kong.Int(0),
+			RequestBuffering:  kong.Bool(true),
+			ResponseBuffering: kong.Bool(true),
+		},
+	}
+
+	if m.ingressHost != "" {
+		route.Route.Hosts = append(route.Route.Hosts, kong.String(m.ingressHost))
+	}
+
+	for _, httpIngressPath := range m.paths {
+		paths := pathsFromIngressPaths(httpIngressPath)
+		route.Paths = append(route.Paths, paths...)
+	}
+
+	return route
+}
+
+// -----------------------------------------------------------------------------
+// Ingress Translation - Private - Helper Functions
+// -----------------------------------------------------------------------------
+
+func pathsFromIngressPaths(httpIngressPath networkingv1.HTTPIngressPath) []*string {
+	switch *httpIngressPath.PathType { //nolint:exhaustive
+	case networkingv1.PathTypeExact:
+		relative := strings.TrimLeft(httpIngressPath.Path, "/")
+		if httpIngressPath.Path == "" {
+			return kong.StringSlice("/")
+		}
+		return kong.StringSlice("/" + relative + "$")
+	case networkingv1.PathTypeImplementationSpecific:
+		return kong.StringSlice(httpIngressPath.Path)
+	default:
+		// path type is prefix
+		base := strings.Trim(httpIngressPath.Path, "/")
+		if base == "" {
+			return kong.StringSlice("/")
+		}
+		return kong.StringSlice(
+			"/"+base+"$",
+			"/"+base+"/",
+		)
+	}
+}
+
+func flattenMultipleSlashes(path string) string {
+	var out []rune
+	in := []rune(path)
+	for i := 0; i < len(in); i++ {
+		c := in[i]
+		if c == '/' {
+			for i < len(in)-1 && in[i+1] == '/' {
+				i++
+			}
+		}
+		out = append(out, c)
+	}
+	return string(out)
+}
diff --git a/internal/manager/config.go b/internal/manager/config.go
index 49bc08f37..502dacd96 100644
--- a/internal/manager/config.go
+++ b/internal/manager/config.go
@@ -92,6 +92,12 @@ type Config struct {
 
 	// Feature Gates
 	FeatureGates map[string]bool
+
+	// TermDelay is the time.Duration which the controller manager will wait
+	// after receiving SIGTERM or SIGINT before shutting down. This can be
+	// helpful for advanced cases with load-balancers so that the ingress
+	// controller can be gracefully removed/drained from their rotation.
+	TermDelay time.Duration
 }
 
 // -----------------------------------------------------------------------------
@@ -203,6 +209,9 @@ func (c *Config) FlagSet() *pflag.FlagSet {
 	flagSet.Var(cliflag.NewMapStringBool(&c.FeatureGates), "feature-gates", "A set of key=value pairs that describe feature gates for alpha/beta/experimental features. "+
 		fmt.Sprintf("See the Feature Gates documentation for information and available options: %s", featureGatesDocsURL))
 
+	// SIGTERM or SIGINT signal delay
+	flagSet.DurationVar(&c.TermDelay, "term-delay", time.Second*0, "The time delay to sleep before SIGTERM or SIGINT will shut down the Ingress Controller")
+
 	// Deprecated (to be removed in future releases)
 	flagSet.Float32Var(&c.ProxySyncSeconds, "sync-rate-limit", dataplane.DefaultSyncSeconds,
 		"Define the rate (in seconds) in which configuration updates will be applied to the Kong Admin API (DEPRECATED, use --proxy-sync-seconds instead)",
diff --git a/internal/manager/feature_gates.go b/internal/manager/feature_gates.go
index 1426b80a7..bdf32b4a9 100644
--- a/internal/manager/feature_gates.go
+++ b/internal/manager/feature_gates.go
@@ -17,6 +17,11 @@ const (
 	// gatewayFeature is the name of the feature-gate for enabling/disabling Gateway APIs
 	gatewayFeature = "Gateway"
 
+	// combinedRoutesFeature is the name of the feature-gate for the newer object
+	// translation logic that will combine routes for kong services when translating
+	// objects like Ingress instead of creating a route per path.
+	combinedRoutesFeature = "CombinedRoutes"
+
 	// featureGatesDocsURL provides a link to the documentation for feature gates in the KIC repository
 	featureGatesDocsURL = "https://github.com/Kong/kubernetes-ingress-controller/blob/main/FEATURE_GATES.md"
 )
@@ -46,7 +51,8 @@ func setupFeatureGates(setupLog logr.Logger, c *Config) (map[string]bool, error)
 // NOTE: if you're adding a new feature gate, it needs to be added here.
 func getFeatureGatesDefaults() map[string]bool {
 	return map[string]bool{
-		knativeFeature: false,
-		gatewayFeature: false,
+		knativeFeature:        false,
+		gatewayFeature:        false,
+		combinedRoutesFeature: false,
 	}
 }
diff --git a/internal/manager/run.go b/internal/manager/run.go
index 8672af82f..a6cb486be 100644
--- a/internal/manager/run.go
+++ b/internal/manager/run.go
@@ -139,6 +139,11 @@ func Run(ctx context.Context, c *Config, diagnostic util.ConfigDumpDiagnostic) e
 		return fmt.Errorf("unable to initialize dataplane synchronizer: %w", err)
 	}
 
+	if enabled, ok := featureGates[combinedRoutesFeature]; ok && enabled {
+		dataplaneClient.EnableCombinedServiceRoutes()
+		setupLog.Info("combined routes mode has been enabled")
+	}
+
 	var kubernetesStatusQueue *status.Queue
 	if c.UpdateStatus {
 		setupLog.Info("Starting Status Updater")
diff --git a/internal/store/store.go b/internal/store/store.go
index 8403ced98..fc8151c9c 100644
--- a/internal/store/store.go
+++ b/internal/store/store.go
@@ -148,27 +148,28 @@ type CacheStores struct {
 }
 
 // NewCacheStores is a convenience function for CacheStores to initialize all attributes with new cache stores
-func NewCacheStores() (c CacheStores) {
-	c.ClusterPlugin = cache.NewStore(clusterResourceKeyFunc)
-	c.Consumer = cache.NewStore(keyFunc)
-	c.Endpoint = cache.NewStore(keyFunc)
-	c.IngressV1 = cache.NewStore(keyFunc)
-	c.IngressClassV1 = cache.NewStore(clusterResourceKeyFunc)
-	c.IngressV1beta1 = cache.NewStore(keyFunc)
-	c.HTTPRoute = cache.NewStore(keyFunc)
-	c.UDPRoute = cache.NewStore(keyFunc)
-	c.TCPRoute = cache.NewStore(keyFunc)
-	c.TLSRoute = cache.NewStore(keyFunc)
-	c.ReferencePolicy = cache.NewStore(keyFunc)
-	c.KnativeIngress = cache.NewStore(keyFunc)
-	c.Plugin = cache.NewStore(keyFunc)
-	c.Secret = cache.NewStore(keyFunc)
-	c.Service = cache.NewStore(keyFunc)
-	c.TCPIngress = cache.NewStore(keyFunc)
-	c.UDPIngress = cache.NewStore(keyFunc)
-	c.KongIngress = cache.NewStore(keyFunc)
-	c.l = &sync.RWMutex{}
-	return
+func NewCacheStores() CacheStores {
+	return CacheStores{
+		IngressV1beta1:  cache.NewStore(keyFunc),
+		IngressV1:       cache.NewStore(keyFunc),
+		IngressClassV1:  cache.NewStore(clusterResourceKeyFunc),
+		Service:         cache.NewStore(keyFunc),
+		Secret:          cache.NewStore(keyFunc),
+		Endpoint:        cache.NewStore(keyFunc),
+		HTTPRoute:       cache.NewStore(keyFunc),
+		UDPRoute:        cache.NewStore(keyFunc),
+		TCPRoute:        cache.NewStore(keyFunc),
+		TLSRoute:        cache.NewStore(keyFunc),
+		ReferencePolicy: cache.NewStore(keyFunc),
+		Plugin:          cache.NewStore(keyFunc),
+		ClusterPlugin:   cache.NewStore(clusterResourceKeyFunc),
+		Consumer:        cache.NewStore(keyFunc),
+		KongIngress:     cache.NewStore(keyFunc),
+		TCPIngress:      cache.NewStore(keyFunc),
+		UDPIngress:      cache.NewStore(keyFunc),
+		KnativeIngress:  cache.NewStore(keyFunc),
+		l:               &sync.RWMutex{},
+	}
 }
 
 // NewCacheStoresFromObjYAML provides a new CacheStores object given any number of byte arrays containing


Test patch:
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 96cb4e2ec..a6a2bcf0c 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -214,6 +214,39 @@ jobs:
         name: coverage
         path: coverage.postgres.out
 
+  integration-tests-feature-gates:
+    runs-on: ubuntu-latest
+    steps:
+
+    - name: setup golang
+      uses: actions/setup-go@v3
+      with:
+        go-version: '^1.18'
+
+    - name: cache go modules
+      uses: actions/cache@v3
+      with:
+        path: ~/go/pkg/mod
+        key: ${{ runner.os }}-build-codegen-${{ hashFiles('**/go.sum') }}
+        restore-keys: |
+          ${{ runner.os }}-build-codegen-
+
+    - name: checkout repository
+      uses: actions/checkout@v3
+      with:
+        fetch-depth: 0
+
+    - name: run integration tests
+      run: make test.integration.dbless
+      env:
+        KONG_CONTROLLER_FEATURE_GATES: "Gateway=true,CombinedRoutes=true"
+
+    - name: collect test coverage
+      uses: actions/upload-artifact@v3
+      with:
+        name: coverage
+        path: coverage.featuregates.out
+
   conformance-tests:
     runs-on: ubuntu-latest
     steps:
@@ -246,6 +279,7 @@ jobs:
       - "integration-tests-dbless"
       - "integration-tests-postgres"
       - "integration-tests-enterprise-postgres"
+      - "integration-tests-feature-gates"
     runs-on: ubuntu-latest
     steps:
 
diff --git a/internal/dataplane/parser/translators/ingress_test.go b/internal/dataplane/parser/translators/ingress_test.go
new file mode 100644
index 000000000..79834d962
--- /dev/null
+++ b/internal/dataplane/parser/translators/ingress_test.go
@@ -0,0 +1,848 @@
+package translators
+
+import (
+	"testing"
+
+	"github.com/kong/go-kong/kong"
+	"github.com/stretchr/testify/assert"
+	corev1 "k8s.io/api/core/v1"
+	networkingv1 "k8s.io/api/networking/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/kongstate"
+	"github.com/kong/kubernetes-ingress-controller/v2/internal/util"
+)
+
+var (
+	pathTypeExact                  = networkingv1.PathTypeExact
+	pathTypeImplementationSpecific = networkingv1.PathTypeImplementationSpecific
+	pathTypePrefix                 = networkingv1.PathTypePrefix
+)
+
+func TestTranslateIngress(t *testing.T) {
+	tts := []struct {
+		name     string
+		ingress  *networkingv1.Ingress
+		expected []*kongstate.Service
+	}{
+		{
+			name: "a basic ingress resource with a single rule, and only one path results in a single kong service and route",
+			ingress: &networkingv1.Ingress{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-ingress",
+					Namespace: corev1.NamespaceDefault,
+				},
+				Spec: networkingv1.IngressSpec{
+					Rules: []networkingv1.IngressRule{{
+						Host: "konghq.com",
+						IngressRuleValue: networkingv1.IngressRuleValue{
+							HTTP: &networkingv1.HTTPIngressRuleValue{
+								Paths: []networkingv1.HTTPIngressPath{{
+									Path: "/api",
+									Backend: networkingv1.IngressBackend{
+										Service: &networkingv1.IngressServiceBackend{
+											Name: "test-service",
+											Port: networkingv1.ServiceBackendPort{
+												Name:   "http",
+												Number: 80,
+											}}}}}}}}}}},
+			expected: []*kongstate.Service{{
+				Namespace: corev1.NamespaceDefault,
+				Service: kong.Service{
+					Name:           kong.String("default.test-ingress.test-service.80"),
+					Host:           kong.String("test-service.default.80.svc"),
+					ConnectTimeout: kong.Int(int(defaultServiceTimeout.Milliseconds())),
+					Path:           kong.String("/"),
+					Port:           kong.Int(80),
+					Protocol:       kong.String("http"),
+					Retries:        kong.Int(defaultRetries),
+					ReadTimeout:    kong.Int(int(defaultServiceTimeout.Milliseconds())),
+					WriteTimeout:   kong.Int(int(defaultServiceTimeout.Milliseconds())),
+				},
+				Routes: []kongstate.Route{{
+					Ingress: util.K8sObjectInfo{
+						Name:      "test-ingress",
+						Namespace: corev1.NamespaceDefault,
+					},
+					Route: kong.Route{
+						Name:              kong.String("default.test-ingress.test-service.konghq.com.80"),
+						Hosts:             kong.StringSlice("konghq.com"),
+						Paths:             kong.StringSlice("/api$", "/api/"), // Prefix pathing is the default behavior when no pathtype is defined
+						PreserveHost:      kong.Bool(true),
+						Protocols:         kong.StringSlice("http", "https"),
+						RegexPriority:     kong.Int(0),
+						StripPath:         kong.Bool(false),
+						ResponseBuffering: kong.Bool(true),
+						RequestBuffering:  kong.Bool(true),
+					},
+				}},
+				Backends: []kongstate.ServiceBackend{{
+					Name:      "test-service",
+					Namespace: corev1.NamespaceDefault,
+					PortDef: kongstate.PortDef{
+						Mode:   kongstate.PortModeByNumber,
+						Number: 80,
+					},
+				}},
+			}},
+		},
+
+		{
+			name: "an ingress with path type exact gets a kong route with an exact path match",
+			ingress: &networkingv1.Ingress{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-ingress",
+					Namespace: corev1.NamespaceDefault,
+				},
+				Spec: networkingv1.IngressSpec{
+					Rules: []networkingv1.IngressRule{{
+						Host: "konghq.com",
+						IngressRuleValue: networkingv1.IngressRuleValue{
+							HTTP: &networkingv1.HTTPIngressRuleValue{
+								Paths: []networkingv1.HTTPIngressPath{{
+									Path:     "/api",
+									PathType: &pathTypeExact,
+									Backend: networkingv1.IngressBackend{
+										Service: &networkingv1.IngressServiceBackend{
+											Name: "test-service",
+											Port: networkingv1.ServiceBackendPort{
+												Name:   "http",
+												Number: 80,
+											}}}}}}}}}}},
+			expected: []*kongstate.Service{{
+				Namespace: corev1.NamespaceDefault,
+				Service: kong.Service{
+					Name:           kong.String("default.test-ingress.test-service.80"),
+					Host:           kong.String("test-service.default.80.svc"),
+					ConnectTimeout: kong.Int(int(defaultServiceTimeout.Milliseconds())),
+					Path:           kong.String("/"),
+					Port:           kong.Int(80),
+					Protocol:       kong.String("http"),
+					Retries:        kong.Int(defaultRetries),
+					ReadTimeout:    kong.Int(int(defaultServiceTimeout.Milliseconds())),
+					WriteTimeout:   kong.Int(int(defaultServiceTimeout.Milliseconds())),
+				},
+				Routes: []kongstate.Route{{
+					Ingress: util.K8sObjectInfo{
+						Name:      "test-ingress",
+						Namespace: corev1.NamespaceDefault,
+					},
+					Route: kong.Route{
+						Name:              kong.String("default.test-ingress.test-service.konghq.com.80"),
+						Hosts:             kong.StringSlice("konghq.com"),
+						Paths:             kong.StringSlice("/api$"), // No Prefix Pathing
+						PreserveHost:      kong.Bool(true),
+						Protocols:         kong.StringSlice("http", "https"),
+						RegexPriority:     kong.Int(0),
+						StripPath:         kong.Bool(false),
+						ResponseBuffering: kong.Bool(true),
+						RequestBuffering:  kong.Bool(true),
+					},
+				}},
+				Backends: []kongstate.ServiceBackend{{
+					Name:      "test-service",
+					Namespace: corev1.NamespaceDefault,
+					PortDef: kongstate.PortDef{
+						Mode:   kongstate.PortModeByNumber,
+						Number: 80,
+					},
+				}},
+			}},
+		},
+
+		{
+			name: "an Ingress resource with implementation specific path type doesn't modify the path",
+			ingress: &networkingv1.Ingress{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-ingress",
+					Namespace: corev1.NamespaceDefault,
+				},
+				Spec: networkingv1.IngressSpec{
+					Rules: []networkingv1.IngressRule{{
+						Host: "konghq.com",
+						IngressRuleValue: networkingv1.IngressRuleValue{
+							HTTP: &networkingv1.HTTPIngressRuleValue{
+								Paths: []networkingv1.HTTPIngressPath{{
+									Path:     "/api",
+									PathType: &pathTypeImplementationSpecific,
+									Backend: networkingv1.IngressBackend{
+										Service: &networkingv1.IngressServiceBackend{
+											Name: "test-service",
+											Port: networkingv1.ServiceBackendPort{
+												Name:   "http",
+												Number: 80,
+											}}}}}}}}}}},
+			expected: []*kongstate.Service{{
+				Namespace: corev1.NamespaceDefault,
+				Service: kong.Service{
+					Name:           kong.String("default.test-ingress.test-service.80"),
+					Host:           kong.String("test-service.default.80.svc"),
+					ConnectTimeout: kong.Int(int(defaultServiceTimeout.Milliseconds())),
+					Path:           kong.String("/"),
+					Port:           kong.Int(80),
+					Protocol:       kong.String("http"),
+					Retries:        kong.Int(defaultRetries),
+					ReadTimeout:    kong.Int(int(defaultServiceTimeout.Milliseconds())),
+					WriteTimeout:   kong.Int(int(defaultServiceTimeout.Milliseconds())),
+				},
+				Routes: []kongstate.Route{{
+					Ingress: util.K8sObjectInfo{
+						Name:      "test-ingress",
+						Namespace: corev1.NamespaceDefault,
+					},
+					Route: kong.Route{
+						Name:              kong.String("default.test-ingress.test-service.konghq.com.80"),
+						Hosts:             kong.StringSlice("konghq.com"),
+						Paths:             kong.StringSlice("/api"), // No path mods
+						PreserveHost:      kong.Bool(true),
+						Protocols:         kong.StringSlice("http", "https"),
+						RegexPriority:     kong.Int(0),
+						StripPath:         kong.Bool(false),
+						ResponseBuffering: kong.Bool(true),
+						RequestBuffering:  kong.Bool(true),
+					},
+				}},
+				Backends: []kongstate.ServiceBackend{{
+					Name:      "test-service",
+					Namespace: corev1.NamespaceDefault,
+					PortDef: kongstate.PortDef{
+						Mode:   kongstate.PortModeByNumber,
+						Number: 80,
+					},
+				}},
+			}},
+		},
+
+		{
+			name: "an Ingress resource with paths with double /'s gets flattened",
+			ingress: &networkingv1.Ingress{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-ingress",
+					Namespace: corev1.NamespaceDefault,
+				},
+				Spec: networkingv1.IngressSpec{
+					Rules: []networkingv1.IngressRule{{
+						Host: "konghq.com",
+						IngressRuleValue: networkingv1.IngressRuleValue{
+							HTTP: &networkingv1.HTTPIngressRuleValue{
+								Paths: []networkingv1.HTTPIngressPath{{
+									Path: "/v1//api///",
+									Backend: networkingv1.IngressBackend{
+										Service: &networkingv1.IngressServiceBackend{
+											Name: "test-service",
+											Port: networkingv1.ServiceBackendPort{
+												Name:   "http",
+												Number: 80,
+											}}}}}}}}}}},
+			expected: []*kongstate.Service{{
+				Namespace: corev1.NamespaceDefault,
+				Service: kong.Service{
+					Name:           kong.String("default.test-ingress.test-service.80"),
+					Host:           kong.String("test-service.default.80.svc"),
+					ConnectTimeout: kong.Int(int(defaultServiceTimeout.Milliseconds())),
+					Path:           kong.String("/"),
+					Port:           kong.Int(80),
+					Protocol:       kong.String("http"),
+					Retries:        kong.Int(defaultRetries),
+					ReadTimeout:    kong.Int(int(defaultServiceTimeout.Milliseconds())),
+					WriteTimeout:   kong.Int(int(defaultServiceTimeout.Milliseconds())),
+				},
+				Routes: []kongstate.Route{{
+					Ingress: util.K8sObjectInfo{
+						Name:      "test-ingress",
+						Namespace: corev1.NamespaceDefault,
+					},
+					Route: kong.Route{
+						Name:              kong.String("default.test-ingress.test-service.konghq.com.80"),
+						Hosts:             kong.StringSlice("konghq.com"),
+						Paths:             kong.StringSlice("/v1/api$", "/v1/api/"),
+						PreserveHost:      kong.Bool(true),
+						Protocols:         kong.StringSlice("http", "https"),
+						RegexPriority:     kong.Int(0),
+						StripPath:         kong.Bool(false),
+						ResponseBuffering: kong.Bool(true),
+						RequestBuffering:  kong.Bool(true),
+					},
+				}},
+				Backends: []kongstate.ServiceBackend{{
+					Name:      "test-service",
+					Namespace: corev1.NamespaceDefault,
+					PortDef: kongstate.PortDef{
+						Mode:   kongstate.PortModeByNumber,
+						Number: 80,
+					},
+				}},
+			}},
+		},
+
+		{
+			name: "empty paths get treated as '/'",
+			ingress: &networkingv1.Ingress{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-ingress",
+					Namespace: corev1.NamespaceDefault,
+				},
+				Spec: networkingv1.IngressSpec{
+					Rules: []networkingv1.IngressRule{{
+						Host: "konghq.com",
+						IngressRuleValue: networkingv1.IngressRuleValue{
+							HTTP: &networkingv1.HTTPIngressRuleValue{
+								Paths: []networkingv1.HTTPIngressPath{{
+									Path: "",
+									Backend: networkingv1.IngressBackend{
+										Service: &networkingv1.IngressServiceBackend{
+											Name: "test-service",
+											Port: networkingv1.ServiceBackendPort{
+												Name:   "http",
+												Number: 80,
+											}}}}}}}}}}},
+			expected: []*kongstate.Service{{
+				Namespace: corev1.NamespaceDefault,
+				Service: kong.Service{
+					Name:           kong.String("default.test-ingress.test-service.80"),
+					Host:           kong.String("test-service.default.80.svc"),
+					ConnectTimeout: kong.Int(int(defaultServiceTimeout.Milliseconds())),
+					Path:           kong.String("/"),
+					Port:           kong.Int(80),
+					Protocol:       kong.String("http"),
+					Retries:        kong.Int(defaultRetries),
+					ReadTimeout:    kong.Int(int(defaultServiceTimeout.Milliseconds())),
+					WriteTimeout:   kong.Int(int(defaultServiceTimeout.Milliseconds())),
+				},
+				Routes: []kongstate.Route{{
+					Ingress: util.K8sObjectInfo{
+						Name:      "test-ingress",
+						Namespace: corev1.NamespaceDefault,
+					},
+					Route: kong.Route{
+						Name:              kong.String("default.test-ingress.test-service.konghq.com.80"),
+						Hosts:             kong.StringSlice("konghq.com"),
+						Paths:             kong.StringSlice("/"),
+						PreserveHost:      kong.Bool(true),
+						Protocols:         kong.StringSlice("http", "https"),
+						RegexPriority:     kong.Int(0),
+						StripPath:         kong.Bool(false),
+						ResponseBuffering: kong.Bool(true),
+						RequestBuffering:  kong.Bool(true),
+					},
+				}},
+				Backends: []kongstate.ServiceBackend{{
+					Name:      "test-service",
+					Namespace: corev1.NamespaceDefault,
+					PortDef: kongstate.PortDef{
+						Mode:   kongstate.PortModeByNumber,
+						Number: 80,
+					},
+				}},
+			}},
+		},
+
+		{
+			name: "multiple and various paths get compiled together properly",
+			ingress: &networkingv1.Ingress{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-ingress",
+					Namespace: corev1.NamespaceDefault,
+				},
+				Spec: networkingv1.IngressSpec{
+					Rules: []networkingv1.IngressRule{{
+						Host: "konghq.com",
+						IngressRuleValue: networkingv1.IngressRuleValue{
+							HTTP: &networkingv1.HTTPIngressRuleValue{
+								Paths: []networkingv1.HTTPIngressPath{
+									{
+										Path: "/v1/api",
+										Backend: networkingv1.IngressBackend{
+											Service: &networkingv1.IngressServiceBackend{
+												Name: "test-service",
+												Port: networkingv1.ServiceBackendPort{
+													Name:   "http",
+													Number: 80,
+												}}},
+									},
+									{
+										Path: "/v2/api",
+										Backend: networkingv1.IngressBackend{
+											Service: &networkingv1.IngressServiceBackend{
+												Name: "test-service",
+												Port: networkingv1.ServiceBackendPort{
+													Name:   "http",
+													Number: 80,
+												}}},
+									},
+									{
+										Path: "/v3/api",
+										Backend: networkingv1.IngressBackend{
+											Service: &networkingv1.IngressServiceBackend{
+												Name: "test-service",
+												Port: networkingv1.ServiceBackendPort{
+													Name:   "http",
+													Number: 80,
+												}}},
+									},
+									{
+										Path:     "/other/path/1",
+										PathType: &pathTypeExact,
+										Backend: networkingv1.IngressBackend{
+											Service: &networkingv1.IngressServiceBackend{
+												Name: "test-service",
+												Port: networkingv1.ServiceBackendPort{
+													Name:   "http",
+													Number: 80,
+												}}},
+									},
+									{
+										Path:     "/other/path/2",
+										PathType: &pathTypeImplementationSpecific,
+										Backend: networkingv1.IngressBackend{
+											Service: &networkingv1.IngressServiceBackend{
+												Name: "test-service",
+												Port: networkingv1.ServiceBackendPort{
+													Name:   "http",
+													Number: 80,
+												}}},
+									},
+									{
+										Path:     "",
+										PathType: &pathTypeImplementationSpecific,
+										Backend: networkingv1.IngressBackend{
+											Service: &networkingv1.IngressServiceBackend{
+												Name: "test-service",
+												Port: networkingv1.ServiceBackendPort{
+													Name:   "http",
+													Number: 80,
+												}}},
+									},
+								}}}}}}},
+			expected: []*kongstate.Service{{
+				Namespace: corev1.NamespaceDefault,
+				Service: kong.Service{
+					Name:           kong.String("default.test-ingress.test-service.80"),
+					Host:           kong.String("test-service.default.80.svc"),
+					ConnectTimeout: kong.Int(int(defaultServiceTimeout.Milliseconds())),
+					Path:           kong.String("/"),
+					Port:           kong.Int(80),
+					Protocol:       kong.String("http"),
+					Retries:        kong.Int(defaultRetries),
+					ReadTimeout:    kong.Int(int(defaultServiceTimeout.Milliseconds())),
+					WriteTimeout:   kong.Int(int(defaultServiceTimeout.Milliseconds())),
+				},
+				Routes: []kongstate.Route{{
+					Ingress: util.K8sObjectInfo{
+						Name:      "test-ingress",
+						Namespace: corev1.NamespaceDefault,
+					},
+					Route: kong.Route{
+						Name:  kong.String("default.test-ingress.test-service.konghq.com.80"),
+						Hosts: kong.StringSlice("konghq.com"),
+						Paths: kong.StringSlice(
+							"/v1/api$", "/v1/api/",
+							"/v2/api$", "/v2/api/",
+							"/v3/api$", "/v3/api/",
+							"/other/path/1$",
+							"/other/path/2",
+							"/",
+						),
+						PreserveHost:      kong.Bool(true),
+						Protocols:         kong.StringSlice("http", "https"),
+						RegexPriority:     kong.Int(0),
+						StripPath:         kong.Bool(false),
+						ResponseBuffering: kong.Bool(true),
+						RequestBuffering:  kong.Bool(true),
+					},
+				}},
+				Backends: []kongstate.ServiceBackend{{
+					Name:      "test-service",
+					Namespace: corev1.NamespaceDefault,
+					PortDef: kongstate.PortDef{
+						Mode:   kongstate.PortModeByNumber,
+						Number: 80,
+					},
+				}},
+			}},
+		},
+
+		{
+			name: "when no host is provided, all hosts are matched",
+			ingress: &networkingv1.Ingress{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-ingress",
+					Namespace: corev1.NamespaceDefault,
+				},
+				Spec: networkingv1.IngressSpec{
+					Rules: []networkingv1.IngressRule{{
+						IngressRuleValue: networkingv1.IngressRuleValue{
+							HTTP: &networkingv1.HTTPIngressRuleValue{
+								Paths: []networkingv1.HTTPIngressPath{
+									{
+										Path: "/v1/api",
+										Backend: networkingv1.IngressBackend{
+											Service: &networkingv1.IngressServiceBackend{
+												Name: "test-service",
+												Port: networkingv1.ServiceBackendPort{
+													Name:   "http",
+													Number: 80,
+												}}},
+									},
+									{
+										Path: "/v2/api",
+										Backend: networkingv1.IngressBackend{
+											Service: &networkingv1.IngressServiceBackend{
+												Name: "test-service",
+												Port: networkingv1.ServiceBackendPort{
+													Name:   "http",
+													Number: 80,
+												}}},
+									},
+									{
+										Path: "/v3/api",
+										Backend: networkingv1.IngressBackend{
+											Service: &networkingv1.IngressServiceBackend{
+												Name: "test-service",
+												Port: networkingv1.ServiceBackendPort{
+													Name:   "http",
+													Number: 80,
+												}}},
+									},
+									{
+										Path:     "/other/path/1",
+										PathType: &pathTypeExact,
+										Backend: networkingv1.IngressBackend{
+											Service: &networkingv1.IngressServiceBackend{
+												Name: "test-service",
+												Port: networkingv1.ServiceBackendPort{
+													Name:   "http",
+													Number: 80,
+												}}},
+									},
+									{
+										Path:     "/other/path/2",
+										PathType: &pathTypeImplementationSpecific,
+										Backend: networkingv1.IngressBackend{
+											Service: &networkingv1.IngressServiceBackend{
+												Name: "test-service",
+												Port: networkingv1.ServiceBackendPort{
+													Name:   "http",
+													Number: 80,
+												}}},
+									},
+									{
+										Path:     "",
+										PathType: &pathTypeImplementationSpecific,
+										Backend: networkingv1.IngressBackend{
+											Service: &networkingv1.IngressServiceBackend{
+												Name: "test-service",
+												Port: networkingv1.ServiceBackendPort{
+													Name:   "http",
+													Number: 80,
+												}}},
+									},
+								}}}}}}},
+			expected: []*kongstate.Service{{
+				Namespace: corev1.NamespaceDefault,
+				Service: kong.Service{
+					Name:           kong.String("default.test-ingress.test-service.80"),
+					Host:           kong.String("test-service.default.80.svc"),
+					ConnectTimeout: kong.Int(int(defaultServiceTimeout.Milliseconds())),
+					Path:           kong.String("/"),
+					Port:           kong.Int(80),
+					Protocol:       kong.String("http"),
+					Retries:        kong.Int(defaultRetries),
+					ReadTimeout:    kong.Int(int(defaultServiceTimeout.Milliseconds())),
+					WriteTimeout:   kong.Int(int(defaultServiceTimeout.Milliseconds())),
+				},
+				Routes: []kongstate.Route{{
+					Ingress: util.K8sObjectInfo{
+						Name:      "test-ingress",
+						Namespace: corev1.NamespaceDefault,
+					},
+					Route: kong.Route{
+						Name: kong.String("default.test-ingress.test-service..80"),
+						Paths: kong.StringSlice(
+							"/v1/api$", "/v1/api/",
+							"/v2/api$", "/v2/api/",
+							"/v3/api$", "/v3/api/",
+							"/other/path/1$",
+							"/other/path/2",
+							"/",
+						),
+						PreserveHost:      kong.Bool(true),
+						Protocols:         kong.StringSlice("http", "https"),
+						RegexPriority:     kong.Int(0),
+						StripPath:         kong.Bool(false),
+						ResponseBuffering: kong.Bool(true),
+						RequestBuffering:  kong.Bool(true),
+					},
+				}},
+				Backends: []kongstate.ServiceBackend{{
+					Name:      "test-service",
+					Namespace: corev1.NamespaceDefault,
+					PortDef: kongstate.PortDef{
+						Mode:   kongstate.PortModeByNumber,
+						Number: 80,
+					},
+				}},
+			}},
+		},
+
+		{
+			name: "when there are multiple backends services, paths wont be combined and separate kong services will be provided",
+			ingress: &networkingv1.Ingress{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-ingress",
+					Namespace: corev1.NamespaceDefault,
+				},
+				Spec: networkingv1.IngressSpec{
+					Rules: []networkingv1.IngressRule{{
+						Host: "konghq.com",
+						IngressRuleValue: networkingv1.IngressRuleValue{
+							HTTP: &networkingv1.HTTPIngressRuleValue{
+								Paths: []networkingv1.HTTPIngressPath{
+									{
+										Path: "/v1/api",
+										Backend: networkingv1.IngressBackend{
+											Service: &networkingv1.IngressServiceBackend{
+												Name: "test-service1",
+												Port: networkingv1.ServiceBackendPort{
+													Name:   "http",
+													Number: 80,
+												}}},
+									},
+									{
+										Path: "/v2/api",
+										Backend: networkingv1.IngressBackend{
+											Service: &networkingv1.IngressServiceBackend{
+												Name: "test-service2",
+												Port: networkingv1.ServiceBackendPort{
+													Name:   "http",
+													Number: 80,
+												}}},
+									},
+								}}}}}}},
+			expected: []*kongstate.Service{
+				{
+					Namespace: corev1.NamespaceDefault,
+					Service: kong.Service{
+						Name:           kong.String("default.test-ingress.test-service1.80"),
+						Host:           kong.String("test-service1.default.80.svc"),
+						ConnectTimeout: kong.Int(int(defaultServiceTimeout.Milliseconds())),
+						Path:           kong.String("/"),
+						Port:           kong.Int(80),
+						Protocol:       kong.String("http"),
+						Retries:        kong.Int(defaultRetries),
+						ReadTimeout:    kong.Int(int(defaultServiceTimeout.Milliseconds())),
+						WriteTimeout:   kong.Int(int(defaultServiceTimeout.Milliseconds())),
+					},
+					Routes: []kongstate.Route{
+						{
+							Ingress: util.K8sObjectInfo{
+								Name:      "test-ingress",
+								Namespace: corev1.NamespaceDefault,
+							},
+							Route: kong.Route{
+								Name:              kong.String("default.test-ingress.test-service1.konghq.com.80"),
+								Hosts:             kong.StringSlice("konghq.com"),
+								Paths:             kong.StringSlice("/v1/api$", "/v1/api/"),
+								PreserveHost:      kong.Bool(true),
+								Protocols:         kong.StringSlice("http", "https"),
+								RegexPriority:     kong.Int(0),
+								StripPath:         kong.Bool(false),
+								ResponseBuffering: kong.Bool(true),
+								RequestBuffering:  kong.Bool(true),
+							},
+						},
+					},
+					Backends: []kongstate.ServiceBackend{{
+						Name:      "test-service1",
+						Namespace: corev1.NamespaceDefault,
+						PortDef: kongstate.PortDef{
+							Mode:   kongstate.PortModeByNumber,
+							Number: 80,
+						},
+					}},
+				},
+
+				{
+					Namespace: corev1.NamespaceDefault,
+					Service: kong.Service{
+						Name:           kong.String("default.test-ingress.test-service2.80"),
+						Host:           kong.String("test-service2.default.80.svc"),
+						ConnectTimeout: kong.Int(int(defaultServiceTimeout.Milliseconds())),
+						Path:           kong.String("/"),
+						Port:           kong.Int(80),
+						Protocol:       kong.String("http"),
+						Retries:        kong.Int(defaultRetries),
+						ReadTimeout:    kong.Int(int(defaultServiceTimeout.Milliseconds())),
+						WriteTimeout:   kong.Int(int(defaultServiceTimeout.Milliseconds())),
+					},
+					Routes: []kongstate.Route{
+						{
+							Ingress: util.K8sObjectInfo{
+								Name:      "test-ingress",
+								Namespace: corev1.NamespaceDefault,
+							},
+							Route: kong.Route{
+								Name:              kong.String("default.test-ingress.test-service2.konghq.com.80"),
+								Hosts:             kong.StringSlice("konghq.com"),
+								Paths:             kong.StringSlice("/v2/api$", "/v2/api/"),
+								PreserveHost:      kong.Bool(true),
+								Protocols:         kong.StringSlice("http", "https"),
+								RegexPriority:     kong.Int(0),
+								StripPath:         kong.Bool(false),
+								ResponseBuffering: kong.Bool(true),
+								RequestBuffering:  kong.Bool(true),
+							},
+						},
+					},
+					Backends: []kongstate.ServiceBackend{{
+						Name:      "test-service2",
+						Namespace: corev1.NamespaceDefault,
+						PortDef: kongstate.PortDef{
+							Mode:   kongstate.PortModeByNumber,
+							Number: 80,
+						},
+					}},
+				},
+			},
+		},
+	}
+
+	for _, tt := range tts {
+		t.Run(tt.name, func(t *testing.T) {
+			assert.Equal(t, TranslateIngress(tt.ingress), tt.expected)
+		})
+	}
+}
+
+func Test_pathsFromIngressPaths(t *testing.T) {
+	for _, tt := range []struct {
+		name string
+		in   networkingv1.HTTPIngressPath
+		out  []*string
+	}{
+		{
+			name: "path type prefix will expand the match to a trailing slash if not provided",
+			in: networkingv1.HTTPIngressPath{
+				Path:     "/v1/api/packages",
+				PathType: &pathTypePrefix,
+			},
+			out: kong.StringSlice(
+				"/v1/api/packages$",
+				"/v1/api/packages/",
+			),
+		},
+		{
+			name: "path type prefix will expand the match with a literal match if a slash is provided",
+			in: networkingv1.HTTPIngressPath{
+				Path:     "/v1/api/packages/",
+				PathType: &pathTypePrefix,
+			},
+			out: kong.StringSlice(
+				"/v1/api/packages$",
+				"/v1/api/packages/",
+			),
+		},
+		{
+			name: "path type prefix will provide a default when no path is provided",
+			in: networkingv1.HTTPIngressPath{
+				Path:     "",
+				PathType: &pathTypePrefix,
+			},
+			out: kong.StringSlice("/"),
+		},
+		{
+			name: "path type exact will cause an exact matching path on a regular path",
+			in: networkingv1.HTTPIngressPath{
+				Path:     "/v1/api/packages",
+				PathType: &pathTypeExact,
+			},
+			out: kong.StringSlice("/v1/api/packages$"),
+		},
+		{
+			name: "path type exact will cause an exact matching path on a regular path with a / suffix",
+			in: networkingv1.HTTPIngressPath{
+				Path:     "/v1/api/packages/",
+				PathType: &pathTypeExact,
+			},
+			out: kong.StringSlice("/v1/api/packages/$"),
+		},
+		{
+			name: "path type exact will supply a default if no path is provided",
+			in: networkingv1.HTTPIngressPath{
+				Path:     "",
+				PathType: &pathTypeExact,
+			},
+			out: kong.StringSlice("/"),
+		},
+		{
+			name: "path type implementation-specific will leave the path alone",
+			in: networkingv1.HTTPIngressPath{
+				Path:     "/asdfasd9jhf09432$",
+				PathType: &pathTypeImplementationSpecific,
+			},
+			out: kong.StringSlice("/asdfasd9jhf09432$"),
+		},
+	} {
+		t.Run(tt.name, func(t *testing.T) {
+			assert.Equal(t, tt.out, pathsFromIngressPaths(tt.in))
+		})
+	}
+}
+
+func Test_flattenMultipleSlashes(t *testing.T) {
+	for _, tt := range []struct {
+		name string
+		in   string
+		out  string
+	}{
+		{
+			name: "a path with no slashes gets left alone",
+			in:   "api",
+			out:  "api",
+		},
+		{
+			name: "a normal path gets left alone",
+			in:   "/v1/api/packages/",
+			out:  "/v1/api/packages/",
+		},
+		{
+			name: "a path with two starting slashes gets flattened",
+			in:   "//",
+			out:  "/",
+		},
+		{
+			name: "a path with many slashes gets flattened",
+			in:   "/////////////////",
+			out:  "/",
+		},
+		{
+			name: "a path with multiple groups of double slashes gets flattened",
+			in:   "//api//packages//",
+			out:  "/api/packages/",
+		},
+		{
+			name: "a path with multiple groups of various sized groups of slashes gets flattened",
+			in:   "////////v1////api//packages///and/stuff//////////////////",
+			out:  "/v1/api/packages/and/stuff/",
+		},
+		{
+			name: "a path with multiple slashes but none at the end gets flattened",
+			in:   "////////v1////api//packages///and/stuff",
+			out:  "/v1/api/packages/and/stuff",
+		},
+		{
+			name: "a path with multiple slashes but none at the beginning gets flattened",
+			in:   "v1/////api//packages///and/stuff//////",
+			out:  "v1/api/packages/and/stuff/",
+		},
+		{
+			name: "a path with multiple slashes but none at the beginning or end gets flattened",
+			in:   "v1/////api//packages//////////////////////////and/stuff",
+			out:  "v1/api/packages/and/stuff",
+		},
+	} {
+		t.Run(tt.name, func(t *testing.T) {
+			assert.Equal(t, tt.out, flattenMultipleSlashes(tt.in))
+		})
+	}
+}
diff --git a/internal/util/test/controller_manager.go b/internal/util/test/controller_manager.go
index 2a2cbcea2..1e9a5528d 100644
--- a/internal/util/test/controller_manager.go
+++ b/internal/util/test/controller_manager.go
@@ -79,7 +79,7 @@ func DeployControllerManagerForCluster(ctx context.Context, cluster clusters.Clu
 	go func() {
 		defer os.Remove(kubeconfig.Name())
 		fmt.Fprintf(os.Stderr, "INFO: Starting Controller Manager for Cluster %s with Configuration: %+v\n", cluster.Name(), config)
-		if err := rootcmd.Run(ctx, &config); err != nil {
+		if err := rootcmd.Run(&config); err != nil {
 			panic(err)
 		}
 	}()
diff --git a/test/integration/httproute_test.go b/test/integration/httproute_test.go
index 9953d7e2d..59dac38c7 100644
--- a/test/integration/httproute_test.go
+++ b/test/integration/httproute_test.go
@@ -150,7 +150,7 @@ func TestHTTPRouteEssentials(t *testing.T) {
 	}
 	require.NoError(t, err)
 	pluginClient, err := clientset.NewForConfig(env.Cluster().Config())
-	kongplugin, err = pluginClient.ConfigurationV1().KongPlugins(ns.Name).Create(ctx, kongplugin, metav1.CreateOptions{})
+	_, err = pluginClient.ConfigurationV1().KongPlugins(ns.Name).Create(ctx, kongplugin, metav1.CreateOptions{})
 
 	t.Logf("creating an httproute to access deployment %s via kong", deployment1.Name)
 	httpPort := gatewayv1alpha2.PortNumber(80)
@@ -240,11 +240,16 @@ func TestHTTPRouteEssentials(t *testing.T) {
 
 	require.Eventually(t, func() bool {
 		req, err := http.NewRequest("GET", fmt.Sprintf("%s/%s", proxyURL, "httpbin"), nil)
+		if err != nil {
+			t.Logf("WARNING: failed to create HTTP request: %v", err)
+			return false
+		}
 		resp, err := httpc.Do(req)
 		if err != nil {
 			t.Logf("WARNING: http request failed for GET %s/%s: %v", proxyURL, "httpbin", err)
 			return false
 		}
+		defer resp.Body.Close()
 		if _, ok := resp.Header["Reqid"]; ok {
 			return true
 		}
diff --git a/test/integration/suite_test.go b/test/integration/suite_test.go
index e1dcc1266..51346d8bf 100644
--- a/test/integration/suite_test.go
+++ b/test/integration/suite_test.go
@@ -143,6 +143,11 @@ func TestMain(m *testing.M) {
 				exitOnErr(err)
 			}
 		}
+		fmt.Println("INFO: configuring feature gates")
+		if controllerFeatureGates == "" {
+			controllerFeatureGates = defaultFeatureGates
+		}
+		fmt.Printf("INFO: feature gates enabled: %s\n", controllerFeatureGates)
 		fmt.Println("INFO: starting the controller manager")
 		standardControllerArgs := []string{
 			fmt.Sprintf("--ingress-class=%s", ingressClass),
@@ -153,7 +158,7 @@ func TestMain(m *testing.M) {
 			"--dump-config",
 			"--log-level=trace",
 			"--debug-log-reduce-redundancy",
-			"--feature-gates=Gateway=true",
+			fmt.Sprintf("--feature-gates=%s", controllerFeatureGates),
 			fmt.Sprintf("--election-namespace=%s", kongAddon.Namespace()),
 		}
 		allControllerArgs := append(standardControllerArgs, extraControllerArgs...)
diff --git a/test/integration/tcproute_test.go b/test/integration/tcproute_test.go
index a89f3570c..6e0f13ba7 100644
--- a/test/integration/tcproute_test.go
+++ b/test/integration/tcproute_test.go
@@ -689,7 +689,7 @@ func TestTCPRouteReferencePolicy(t *testing.T) {
 	t.Logf("testing incorrect name does not match")
 	blueguyName := gatewayv1alpha2.ObjectName("blueguy")
 	policy.Spec.To[1].Name = &blueguyName
-	policy, err = c.GatewayV1alpha2().ReferencePolicies(otherNs.Name).Update(ctx, policy, metav1.UpdateOptions{})
+	_, err = c.GatewayV1alpha2().ReferencePolicies(otherNs.Name).Update(ctx, policy, metav1.UpdateOptions{})
 	require.NoError(t, err)
 
 	require.Eventually(t, func() bool {
diff --git a/test/integration/utils_test.go b/test/integration/utils_test.go
index d305deade..6c9b9003a 100644
--- a/test/integration/utils_test.go
+++ b/test/integration/utils_test.go
@@ -104,6 +104,14 @@ var (
 	clusterVersion semver.Version
 )
 
+const (
+	// defaultFeatureGates is the default feature gates setting that should be
+	// provided if none are provided by the user. This generally includes features
+	// that are innocuous, or otherwise don't actually get triggered unless the
+	// user takes further action.
+	defaultFeatureGates = "Gateway=true"
+)
+
 // -----------------------------------------------------------------------------
 // Testing Variables - Environment Overrides
 // -----------------------------------------------------------------------------
@@ -126,6 +134,10 @@ var (
 
 	// kongEnterpriseEnabled enables Enterprise-specific tests when set to "true"
 	kongEnterpriseEnabled = os.Getenv("TEST_KONG_ENTERPRISE")
+
+	// controllerFeatureGates contains the feature gates that should be enabled
+	// for test runs.
+	controllerFeatureGates = os.Getenv("KONG_CONTROLLER_FEATURE_GATES")
 )
 
 // -----------------------------------------------------------------------------