Repository: kubernetes/ingress-nginx
Pull request #9505 (2023-01-12 22:01:00)

feat(configmap): expose gzip-disable
## What this PR does / why we need it:

This PR expose the settings `gzip_disable` of the NGINX [`gzip`](http://nginx.org/en/docs/http/ngx_http_gzip_module.html#gzip_disable) module.

## Types of changes

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] CVE Report (Scanner found CVE and adding report)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Documentation only

## How Has This Been Tested?

Unit & E2E tests have been added to cover the feature.

## Checklist:

- [x] My change requires a change to the documentation.
- [x] I have updated the documentation accordingly.
- [x] I've read the [CONTRIBUTION](https://github.com/kubernetes/ingress-nginx/blob/main/CONTRIBUTING.md) guide
- [x] I have added unit and/or e2e tests to cover my changes.
- [x] All new and existing tests passed.

```release-note
Adds gzip-disable configuration
```

linux-foundation-easycla[bot]:  <a href="https://api.easycla.lfx.linuxfoundation.org/v2/repository-provider/github/sign/18706487/72891330/9505/#/?version=2"><img src="https://s3.amazonaws.com/cla-project-logo-prod/cla-not-signed.svg" alt="CLA Not Signed" align="left" height="28" width="328"></a><br/><br /><ul><li><a href='https://api.easycla.lfx.linuxfoundation.org/v2/repository-provider/github/sign/18706487/72891330/9505/#/?version=2' target='_blank'>:x:</a> - login: @samouss / name: Samuel Vaillant . The commit (a4c549631167df6b143d4afd8e81766e6c49f58c, 6a47016456ea22dbdcd5e73fdac16ca7d5d61a73, 1a90b32358f9e9e15d3016d432959af4a1f06d6b) is not authorized under a signed CLA. <a href='https://api.easycla.lfx.linuxfoundation.org/v2/repository-provider/github/sign/18706487/72891330/9505/#/?version=2' target='_blank'>Please click here to be authorized</a>. For further assistance with EasyCLA, <a href='https://jira.linuxfoundation.org/servicedesk/customer/portal/4' target='_blank'>please submit a support request ticket</a>.</li></ul>
k8s-ci-robot: Welcome @samouss! <br><br>It looks like this is your first PR to <a href='https://github.com/kubernetes/ingress-nginx'>kubernetes/ingress-nginx</a> 🎉. Please refer to our [pull request process documentation](https://git.k8s.io/community/contributors/guide/pull-requests.md) to help your PR have a smooth ride to approval. <br><br>You will be prompted by a bot to use commands during the review process. Do not be afraid to follow the prompts! It is okay to experiment. [Here is the bot commands documentation](https://go.k8s.io/bot-commands). <br><br>You can also check if kubernetes/ingress-nginx has [its own contribution guidelines](https://github.com/kubernetes/ingress-nginx/tree/master/CONTRIBUTING.md). <br><br>You may want to refer to our [testing guide](https://git.k8s.io/community/contributors/devel/sig-testing/testing.md) if you run into trouble with your tests not passing. <br><br>If you are having difficulty getting your pull request seen, please follow the [recommended escalation practices](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#why-is-my-pull-request-not-getting-reviewed). Also, for tips and tricks in the contribution process you may want to read the [Kubernetes contributor cheat sheet](https://git.k8s.io/community/contributors/guide/contributor-cheatsheet/README.md). We want to make sure your contribution gets all the attention it needs! <br><br>Thank you, and welcome to Kubernetes. :smiley:
k8s-ci-robot: @samouss: This issue is currently awaiting triage.

If Ingress contributors determines this is a relevant issue, they will accept it by applying the `triage/accepted` label and provide further guidance.

The `triage/accepted` label can be added by org members by writing `/triage accepted` in a comment.


<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
k8s-ci-robot: Hi @samouss. Thanks for your PR.

I'm waiting for a [kubernetes](https://github.com/orgs/kubernetes/people) member to verify that this patch is reasonable to test. If it is, they should reply with `/ok-to-test` on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work. Regular contributors should [join the org](https://git.k8s.io/community/community-membership.md#member) to skip this step.

Once the patch is verified, the new status will be reflected by the `ok-to-test` label.

I understand the commands that are listed [here](https://go.k8s.io/bot-commands?repo=kubernetes%2Fingress-nginx).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>

k8s-ci-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes/ingress-nginx/pull/9505#" title="Author self-approved">samouss</a>*
**Once this PR has been reviewed and has the lgtm label**, please assign strongjz for approval by writing `/assign @strongjz` in a comment. For more information see [the Kubernetes Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes%2Fingress-nginx).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/kubernetes/ingress-nginx/blob/main/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["strongjz"]} -->

Patch:
diff --git a/charts/ingress-nginx/README.md b/charts/ingress-nginx/README.md
index 00d85156e..65236e08c 100644
--- a/charts/ingress-nginx/README.md
+++ b/charts/ingress-nginx/README.md
@@ -315,6 +315,7 @@ Kubernetes: `>=1.20.0-0`
 | controller.dnsPolicy | string | `"ClusterFirst"` | Optionally change this to ClusterFirstWithHostNet in case you have 'hostNetwork: true'. By default, while using host network, name resolution uses the host's DNS. If you wish nginx-controller to keep resolving names inside the k8s network, use ClusterFirstWithHostNet. |
 | controller.electionID | string | `""` | Election ID to use for status update, by default it uses the controller name combined with a suffix of 'leader' |
 | controller.enableMimalloc | bool | `true` | Enable mimalloc as a drop-in replacement for malloc. # ref: https://github.com/microsoft/mimalloc # |
+| controller.enableTopologyAwareRouting | bool | `false` | This configuration enables Topology Aware Routing feature, used together with service annotation service.kubernetes.io/topology-aware-hints="auto" Defaults to false |
 | controller.existingPsp | string | `""` | Use an existing PSP instead of creating one |
 | controller.extraArgs | object | `{}` | Additional command line arguments to pass to nginx-ingress-controller E.g. to specify the default SSL certificate you can use |
 | controller.extraContainers | list | `[]` | Additional containers to be added to the controller pod. See https://github.com/lemonldap-ng-controller/lemonldap-ng-controller as example. |
@@ -376,6 +377,7 @@ Kubernetes: `>=1.20.0-0`
 | controller.metrics.prometheusRule.rules | list | `[]` |  |
 | controller.metrics.service.annotations | object | `{}` |  |
 | controller.metrics.service.externalIPs | list | `[]` | List of IP addresses at which the stats-exporter service is available # Ref: https://kubernetes.io/docs/user-guide/services/#external-ips # |
+| controller.metrics.service.labels | object | `{}` | Labels to be added to the metrics service resource |
 | controller.metrics.service.loadBalancerSourceRanges | list | `[]` |  |
 | controller.metrics.service.servicePort | int | `10254` |  |
 | controller.metrics.service.type | string | `"ClusterIP"` |  |
diff --git a/charts/ingress-nginx/templates/_params.tpl b/charts/ingress-nginx/templates/_params.tpl
index 66c581fa6..a1aef01ae 100644
--- a/charts/ingress-nginx/templates/_params.tpl
+++ b/charts/ingress-nginx/templates/_params.tpl
@@ -51,6 +51,9 @@
 {{- if .Values.controller.watchIngressWithoutClass }}
 - --watch-ingress-without-class=true
 {{- end }}
+{{- if .Values.controller.enableTopologyAwareRouting }}
+- --enable-topology-aware-routing=true
+{{- end }}
 {{- range $key, $value := .Values.controller.extraArgs }}
 {{- /* Accept keys without values or with false as value */}}
 {{- if eq ($value | quote | len) 2 }}
diff --git a/charts/ingress-nginx/values.yaml b/charts/ingress-nginx/values.yaml
index a7fab0fe9..215ce509d 100644
--- a/charts/ingress-nginx/values.yaml
+++ b/charts/ingress-nginx/values.yaml
@@ -77,6 +77,10 @@ controller:
   # -- Process IngressClass per name (additionally as per spec.controller).
   ingressClassByName: false
 
+  # -- This configuration enables Topology Aware Routing feature, used together with service annotation service.kubernetes.io/topology-aware-hints="auto"
+  # Defaults to false
+  enableTopologyAwareRouting: false
+
   # -- This configuration defines if Ingress Controller should allow users to set
   # their own *-snippet annotations, otherwise this is forbidden / dropped
   # when users add those annotations.
@@ -700,6 +704,8 @@ controller:
       annotations: {}
       # prometheus.io/scrape: "true"
       # prometheus.io/port: "10254"
+      # -- Labels to be added to the metrics service resource
+      labels: {}
 
       # clusterIP: ""
 
diff --git a/docs/user-guide/cli-arguments.md b/docs/user-guide/cli-arguments.md
index 4379d0b34..febc6f762 100644
--- a/docs/user-guide/cli-arguments.md
+++ b/docs/user-guide/cli-arguments.md
@@ -24,6 +24,7 @@ They are set in the container spec of the `ingress-nginx-controller` Deployment
 | `--enable-metrics`                 | Enables the collection of NGINX metrics. (default true) |
 | `--enable-ssl-chain-completion`    | Autocomplete SSL certificate chains with missing intermediate CA certificates. Certificates uploaded to Kubernetes must have the "Authority Information Access" X.509 v3 extension for this to succeed. (default false)|
 | `--enable-ssl-passthrough`         | Enable SSL Passthrough. (default false) |
+| `--enable-topology-aware-routing`  | Enable topology aware hints feature, needs service object annotation service.kubernetes.io/topology-aware-hints sets to auto. (default false) |
 | `--health-check-path`              | URL path of the health check endpoint. Configured inside the NGINX status server. All requests received on the port defined by the healthz-port parameter are forwarded internally to this path. (default "/healthz") |
 | `--health-check-timeout`           | Time limit, in seconds, for a probe to health-check-path to succeed. (default 10) |
 | `--healthz-port`                   | Port to use for the healthz endpoint. (default 10254) |
diff --git a/docs/user-guide/monitoring.md b/docs/user-guide/monitoring.md
index cc9fb91ec..807d91afc 100644
--- a/docs/user-guide/monitoring.md
+++ b/docs/user-guide/monitoring.md
@@ -355,56 +355,40 @@ Prometheus metrics are exposed on port 10254.
 
 ### Request metrics
 
-* `nginx_ingress_controller_request_duration_seconds` Histogram
-
-  The request processing time in seconds (affected by client speed)
-
+* `nginx_ingress_controller_request_duration_seconds` Histogram\
+  The request processing (time elapsed between the first bytes were read from the client and the log write after the last bytes were sent to the client) time in seconds (affected by client speed).\
   nginx var: `request_time`
 
-* `nginx_ingress_controller_response_duration_seconds` Histogram
-
-  The time spent on receiving the response from the upstream server (affected by client speed)
-
+* `nginx_ingress_controller_response_duration_seconds` Histogram\
+  The time spent on receiving the response from the upstream server in seconds (affected by client speed when the response is bigger than proxy buffers).\
+  Note: can be up to several millis bigger than the `nginx_ingress_controller_request_duration_seconds` because of the different measuring method.
   nginx var: `upstream_response_time`
 
-* `nginx_ingress_controller_header_duration_seconds` Histogram
-
-  The time spent on receiving first header from the upstream server
-
+* `nginx_ingress_controller_header_duration_seconds` Histogram\
+  The time spent on receiving first header from the upstream server\
   nginx var: `upstream_header_time`
 
-* `nginx_ingress_controller_connect_duration_seconds` Histogram
-
-  The time spent on establishing a connection with the upstream server
-
+* `nginx_ingress_controller_connect_duration_seconds` Histogram\
+  The time spent on establishing a connection with the upstream server\
   nginx var: `upstream_connect_time`
 
-* `nginx_ingress_controller_response_size` Histogram
-
-  The response length (including request line, header, and request body)
-
+* `nginx_ingress_controller_response_size` Histogram\
+  The response length (including request line, header, and request body)\
   nginx var: `bytes_sent`
 
-* `nginx_ingress_controller_request_size` Histogram
-
-  The request length (including request line, header, and request body)
-
+* `nginx_ingress_controller_request_size` Histogram\
+  The request length (including request line, header, and request body)\
   nginx var: `request_length`
 
-* `nginx_ingress_controller_requests` Counter
-
+* `nginx_ingress_controller_requests` Counter\
   The total number of client requests
 
-* `nginx_ingress_controller_bytes_sent` Histogram
-
-  The number of bytes sent to a client. **Deprecated**, use `nginx_ingress_controller_response_size`
-
+* `nginx_ingress_controller_bytes_sent` Histogram\
+  The number of bytes sent to a client. **Deprecated**, use `nginx_ingress_controller_response_size`\
   nginx var: `bytes_sent`
 
-* `nginx_ingress_controller_ingress_upstream_latency_seconds` Summary
-
-  Upstream service latency per Ingress. **Deprecated**, use `nginx_ingress_controller_connect_duration_seconds`
-
+* `nginx_ingress_controller_ingress_upstream_latency_seconds` Summary\
+  Upstream service latency per Ingress. **Deprecated**, use `nginx_ingress_controller_connect_duration_seconds`\
   nginx var: `upstream_connect_time`
 
 ```
@@ -469,6 +453,8 @@ Prometheus metrics are exposed on port 10254.
 # TYPE nginx_ingress_controller_ssl_certificate_info gauge
 # HELP nginx_ingress_controller_success Cumulative number of Ingress controller reload operations
 # TYPE nginx_ingress_controller_success counter
+# HELP nginx_ingress_controller_orphan_ingress Gauge reporting status of ingress orphanity, 1 indicates orphaned ingress. 'namespace' is the string used to identify namespace of ingress, 'ingress' for ingress name and 'type' for 'no-service' or 'no-endpoint' of orphanity
+# TYPE nginx_ingress_controller_orphan_ingress gauge
 ```
 
 ### Admission metrics
diff --git a/docs/user-guide/nginx-configuration/configmap.md b/docs/user-guide/nginx-configuration/configmap.md
index 77cee0507..2a3369044 100755
--- a/docs/user-guide/nginx-configuration/configmap.md
+++ b/docs/user-guide/nginx-configuration/configmap.md
@@ -103,7 +103,9 @@ The following table shows a configuration option's name, type, and the default v
 |[brotli-min-length](#brotli-min-length)|int|20|
 |[brotli-types](#brotli-types)|string|"application/xml+rss application/atom+xml application/javascript application/x-javascript application/json application/rss+xml application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/svg+xml image/x-icon text/css text/javascript text/plain text/x-component"|
 |[use-http2](#use-http2)|bool|"true"|
+|[gzip-disable](#gzip-disable)|string|""|
 |[gzip-level](#gzip-level)|int|1|
+|[gzip-min-length](#gzip-min-length)|int|256|
 |[gzip-types](#gzip-types)|string|"application/atom+xml application/javascript application/x-javascript application/json application/rss+xml application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/svg+xml image/x-icon text/css text/javascript text/plain text/x-component"|
 |[worker-processes](#worker-processes)|string|`<Number of CPUs>`|
 |[worker-cpu-affinity](#worker-cpu-affinity)|string|""|
@@ -693,7 +695,8 @@ _**default:**_ false
 ## enable-brotli
 
 Enables or disables compression of HTTP responses using the ["brotli" module](https://github.com/google/ngx_brotli).
-The default mime type list to compress is: `application/xml+rss application/atom+xml application/javascript application/x-javascript application/json application/rss+xml application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/svg+xml image/x-icon text/css text/plain text/x-component`. _**default:**_ is disabled
+The default mime type list to compress is: `application/xml+rss application/atom+xml application/javascript application/x-javascript application/json application/rss+xml application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/svg+xml image/x-icon text/css text/plain text/x-component`. 
+_**default:**_ false
 
 > __Note:__ Brotli does not works in Safari < 11. For more information see [https://caniuse.com/#feat=brotli](https://caniuse.com/#feat=brotli)
 
@@ -714,6 +717,10 @@ _**default:**_ `application/xml+rss application/atom+xml application/javascript
 
 Enables or disables [HTTP/2](https://nginx.org/en/docs/http/ngx_http_v2_module.html) support in secure connections.
 
+## gzip-disable
+
+Disables [gzipping](http://nginx.org/en/docs/http/ngx_http_gzip_module.html#gzip_disable) of responses for requests with "User-Agent" header fields matching any of the specified regular expressions.
+
 ## gzip-level
 
 Sets the gzip Compression Level that will be used. _**default:**_ 1
diff --git a/internal/ingress/controller/config/config.go b/internal/ingress/controller/config/config.go
index f064dad53..a4a4c5a0b 100644
--- a/internal/ingress/controller/config/config.go
+++ b/internal/ingress/controller/config/config.go
@@ -438,6 +438,11 @@ type Configuration struct {
 	// Default: true
 	UseHTTP2 bool `json:"use-http2,omitempty"`
 
+	// Disables gzipping of responses for requests with "User-Agent" header fields matching any of
+	// the specified regular expressions.
+	// http://nginx.org/en/docs/http/ngx_http_gzip_module.html#gzip_disable
+	GzipDisable string `json:"gzip-disable,omitempty"`
+
 	// gzip Compression Level that will be used
 	GzipLevel int `json:"gzip-level,omitempty"`
 
@@ -794,7 +799,6 @@ func NewDefault() Configuration {
 	defGlobalExternalAuth := GlobalExternalAuth{"", "", "", "", "", append(defResponseHeaders, ""), "", "", "", []string{}, map[string]string{}, false}
 
 	cfg := Configuration{
-
 		AllowSnippetAnnotations:          true,
 		AllowBackendServerHeader:         false,
 		AnnotationValueWordBlocklist:     "",
diff --git a/internal/ingress/controller/controller.go b/internal/ingress/controller/controller.go
index 010eba162..dab507cdf 100644
--- a/internal/ingress/controller/controller.go
+++ b/internal/ingress/controller/controller.go
@@ -50,9 +50,12 @@ import (
 )
 
 const (
-	defUpstreamName = "upstream-default-backend"
-	defServerName   = "_"
-	rootLocation    = "/"
+	defUpstreamName             = "upstream-default-backend"
+	defServerName               = "_"
+	rootLocation                = "/"
+	emptyZone                   = ""
+	orphanMetricLabelNoService  = "no-service"
+	orphanMetricLabelNoEndpoint = "no-endpoint"
 )
 
 // Configuration contains all the settings required by an Ingress controller
@@ -131,6 +134,21 @@ type Configuration struct {
 	DynamicConfigurationRetries int
 
 	DisableSyncEvents bool
+
+	EnableTopologyAwareRouting bool
+}
+
+func getIngressPodZone(svc *apiv1.Service) string {
+	svcKey := k8s.MetaNamespaceKey(svc)
+	if svcZoneAnnotation, ok := svc.ObjectMeta.GetAnnotations()[apiv1.AnnotationTopologyAwareHints]; ok {
+		if strings.ToLower(svcZoneAnnotation) == "auto" {
+			if foundZone, ok := k8s.IngressNodeDetails.GetLabels()[apiv1.LabelTopologyZone]; ok {
+				klog.V(3).Infof("Svc has topology aware annotation enabled, try to use zone %q where controller pod is running for Service %q ", foundZone, svcKey)
+				return foundZone
+			}
+		}
+	}
+	return emptyZone
 }
 
 // GetPublishService returns the Service used to set the load-balancer status of Ingresses.
@@ -429,6 +447,13 @@ func (n *NGINXController) getStreamServices(configmapName string, proto apiv1.Pr
 		var endps []ingress.Endpoint
 		/* #nosec */
 		targetPort, err := strconv.Atoi(svcPort) // #nosec
+		var zone string
+		if n.cfg.EnableTopologyAwareRouting {
+			zone = getIngressPodZone(svc)
+		} else {
+			zone = emptyZone
+		}
+
 		if err != nil {
 			// not a port number, fall back to using port name
 			klog.V(3).Infof("Searching Endpoints with %v port name %q for Service %q", proto, svcPort, nsName)
@@ -436,7 +461,7 @@ func (n *NGINXController) getStreamServices(configmapName string, proto apiv1.Pr
 				sp := svc.Spec.Ports[i]
 				if sp.Name == svcPort {
 					if sp.Protocol == proto {
-						endps = getEndpointsFromSlices(svc, &sp, proto, n.store.GetServiceEndpointsSlices)
+						endps = getEndpointsFromSlices(svc, &sp, proto, zone, n.store.GetServiceEndpointsSlices)
 						break
 					}
 				}
@@ -447,7 +472,7 @@ func (n *NGINXController) getStreamServices(configmapName string, proto apiv1.Pr
 				sp := svc.Spec.Ports[i]
 				if sp.Port == int32(targetPort) {
 					if sp.Protocol == proto {
-						endps = getEndpointsFromSlices(svc, &sp, proto, n.store.GetServiceEndpointsSlices)
+						endps = getEndpointsFromSlices(svc, &sp, proto, zone, n.store.GetServiceEndpointsSlices)
 						break
 					}
 				}
@@ -498,8 +523,13 @@ func (n *NGINXController) getDefaultUpstream() *ingress.Backend {
 		upstream.Endpoints = append(upstream.Endpoints, n.DefaultEndpoint())
 		return upstream
 	}
-
-	endps := getEndpointsFromSlices(svc, &svc.Spec.Ports[0], apiv1.ProtocolTCP, n.store.GetServiceEndpointsSlices)
+	var zone string
+	if n.cfg.EnableTopologyAwareRouting {
+		zone = getIngressPodZone(svc)
+	} else {
+		zone = emptyZone
+	}
+	endps := getEndpointsFromSlices(svc, &svc.Spec.Ports[0], apiv1.ProtocolTCP, zone, n.store.GetServiceEndpointsSlices)
 	if len(endps) == 0 {
 		klog.Warningf("Service %q does not have any active Endpoint", svcKey)
 		endps = []ingress.Endpoint{n.DefaultEndpoint()}
@@ -827,7 +857,13 @@ func (n *NGINXController) getBackendServers(ingresses []*ingress.Ingress) ([]*in
 				}
 
 				sp := location.DefaultBackend.Spec.Ports[0]
-				endps := getEndpointsFromSlices(location.DefaultBackend, &sp, apiv1.ProtocolTCP, n.store.GetServiceEndpointsSlices)
+				var zone string
+				if n.cfg.EnableTopologyAwareRouting {
+					zone = getIngressPodZone(location.DefaultBackend)
+				} else {
+					zone = emptyZone
+				}
+				endps := getEndpointsFromSlices(location.DefaultBackend, &sp, apiv1.ProtocolTCP, zone, n.store.GetServiceEndpointsSlices)
 				// custom backend is valid only if contains at least one endpoint
 				if len(endps) > 0 {
 					name := fmt.Sprintf("custom-default-backend-%v-%v", location.DefaultBackend.GetNamespace(), location.DefaultBackend.GetName())
@@ -1018,8 +1054,16 @@ func (n *NGINXController) createUpstreams(data []*ingress.Ingress, du *ingress.B
 					endp, err := n.serviceEndpoints(svcKey, port.String())
 					if err != nil {
 						klog.Warningf("Error obtaining Endpoints for Service %q: %v", svcKey, err)
+						n.metricCollector.IncOrphanIngress(ing.Namespace, ing.Name, orphanMetricLabelNoService)
 						continue
 					}
+					n.metricCollector.DecOrphanIngress(ing.Namespace, ing.Name, orphanMetricLabelNoService)
+
+					if len(endp) == 0 {
+						n.metricCollector.IncOrphanIngress(ing.Namespace, ing.Name, orphanMetricLabelNoEndpoint)
+					} else {
+						n.metricCollector.DecOrphanIngress(ing.Namespace, ing.Name, orphanMetricLabelNoEndpoint)
+					}
 					upstreams[name].Endpoints = endp
 				}
 
@@ -1083,7 +1127,12 @@ func (n *NGINXController) serviceEndpoints(svcKey, backendPort string) ([]ingres
 	if err != nil {
 		return upstreams, err
 	}
-
+	var zone string
+	if n.cfg.EnableTopologyAwareRouting {
+		zone = getIngressPodZone(svc)
+	} else {
+		zone = emptyZone
+	}
 	klog.V(3).Infof("Obtaining ports information for Service %q", svcKey)
 	// Ingress with an ExternalName Service and no port defined for that Service
 	if svc.Spec.Type == apiv1.ServiceTypeExternalName {
@@ -1092,7 +1141,7 @@ func (n *NGINXController) serviceEndpoints(svcKey, backendPort string) ([]ingres
 			return upstreams, nil
 		}
 		servicePort := externalNamePorts(backendPort, svc)
-		endps := getEndpointsFromSlices(svc, servicePort, apiv1.ProtocolTCP, n.store.GetServiceEndpointsSlices)
+		endps := getEndpointsFromSlices(svc, servicePort, apiv1.ProtocolTCP, zone, n.store.GetServiceEndpointsSlices)
 		if len(endps) == 0 {
 			klog.Warningf("Service %q does not have any active Endpoint.", svcKey)
 			return upstreams, nil
@@ -1109,7 +1158,7 @@ func (n *NGINXController) serviceEndpoints(svcKey, backendPort string) ([]ingres
 			servicePort.TargetPort.String() == backendPort ||
 			servicePort.Name == backendPort {
 
-			endps := getEndpointsFromSlices(svc, &servicePort, apiv1.ProtocolTCP, n.store.GetServiceEndpointsSlices)
+			endps := getEndpointsFromSlices(svc, &servicePort, apiv1.ProtocolTCP, zone, n.store.GetServiceEndpointsSlices)
 			if len(endps) == 0 {
 				klog.Warningf("Service %q does not have any active Endpoint.", svcKey)
 			}
diff --git a/internal/ingress/controller/endpointslices.go b/internal/ingress/controller/endpointslices.go
index 5a24c3880..34d5266dd 100644
--- a/internal/ingress/controller/endpointslices.go
+++ b/internal/ingress/controller/endpointslices.go
@@ -35,7 +35,7 @@ import (
 )
 
 // getEndpoints returns a list of Endpoint structs for a given service/target port combination.
-func getEndpointsFromSlices(s *corev1.Service, port *corev1.ServicePort, proto corev1.Protocol,
+func getEndpointsFromSlices(s *corev1.Service, port *corev1.ServicePort, proto corev1.Protocol, zoneForHints string,
 	getServiceEndpointsSlices func(string) ([]*discoveryv1.EndpointSlice, error)) []ingress.Endpoint {
 
 	upsServers := []ingress.Endpoint{}
@@ -49,6 +49,7 @@ func getEndpointsFromSlices(s *corev1.Service, port *corev1.ServicePort, proto c
 	processedUpstreamServers := make(map[string]struct{})
 
 	svcKey := k8s.MetaNamespaceKey(s)
+	var useTopologyHints bool
 
 	// ExternalName services
 	if s.Spec.Type == corev1.ServiceTypeExternalName {
@@ -111,12 +112,38 @@ func getEndpointsFromSlices(s *corev1.Service, port *corev1.ServicePort, proto c
 				ports = append(ports, targetPort)
 			}
 		}
+		useTopologyHints = false
+		if zoneForHints != emptyZone {
+			useTopologyHints = true
+			// check if all endpointslices has zone hints
+			for _, ep := range eps.Endpoints {
+				if ep.Hints == nil || len(ep.Hints.ForZones) == 0 {
+					useTopologyHints = false
+					break
+				}
+			}
+			if useTopologyHints {
+				klog.V(3).Infof("All endpoint slices has zone hint, using zone %q for Service %q", zoneForHints, svcKey)
+			}
+		}
+
 		for _, ep := range eps.Endpoints {
 			if !(*ep.Conditions.Ready) {
 				continue
 			}
+			epHasZone := false
+			if useTopologyHints {
+				for _, epzone := range ep.Hints.ForZones {
+					if epzone.Name == zoneForHints {
+						epHasZone = true
+						break
+					}
+				}
+			}
 
-			// ep.Hints
+			if useTopologyHints && !epHasZone {
+				continue
+			}
 
 			for _, epPort := range ports {
 				for _, epAddress := range ep.Addresses {
diff --git a/internal/ingress/metric/collectors/controller.go b/internal/ingress/metric/collectors/controller.go
index 5822f0d57..3a65a1a99 100644
--- a/internal/ingress/metric/collectors/controller.go
+++ b/internal/ingress/metric/collectors/controller.go
@@ -32,6 +32,7 @@ var (
 	ingressOperation = []string{"controller_namespace", "controller_class", "controller_pod", "namespace", "ingress"}
 	sslLabelHost     = []string{"namespace", "class", "host", "secret_name"}
 	sslInfoLabels    = []string{"namespace", "class", "host", "secret_name", "identifier", "issuer_organization", "issuer_common_name", "serial_number", "public_key_algorithm"}
+	orphanityLabels  = []string{"controller_namespace", "controller_class", "controller_pod", "namespace", "ingress", "type"}
 )
 
 // Controller defines base metrics about the ingress controller
@@ -48,6 +49,7 @@ type Controller struct {
 	checkIngressOperationErrors *prometheus.CounterVec
 	sslExpireTime               *prometheus.GaugeVec
 	sslInfo                     *prometheus.GaugeVec
+	OrphanIngress               *prometheus.GaugeVec
 
 	constLabels prometheus.Labels
 	labels      prometheus.Labels
@@ -171,6 +173,15 @@ func NewController(pod, namespace, class string) *Controller {
 			},
 			[]string{"name"},
 		),
+		OrphanIngress: prometheus.NewGaugeVec(
+			prometheus.GaugeOpts{
+				Namespace: PrometheusNamespace,
+				Name:      "orphan_ingress",
+				Help: `Gauge reporting status of ingress orphanity, 1 indicates orphaned ingress.
+			'namespace' is the string used to identify namespace of ingress, 'ingress' for ingress name and 'type' for 'no-service' or 'no-endpoint' of orphanity`,
+			},
+			orphanityLabels,
+		),
 	}
 
 	return cm
@@ -214,6 +225,26 @@ func (cm *Controller) IncCheckErrorCount(namespace, name string) {
 	cm.checkIngressOperationErrors.MustCurryWith(cm.constLabels).With(labels).Inc()
 }
 
+// IncOrphanIngress sets the the orphaned ingress gauge to one
+func (cm *Controller) IncOrphanIngress(namespace string, name string, orphanityType string) {
+	labels := prometheus.Labels{
+		"namespace": namespace,
+		"ingress":   name,
+		"type":      orphanityType,
+	}
+	cm.OrphanIngress.MustCurryWith(cm.constLabels).With(labels).Set(1.0)
+}
+
+// DecOrphanIngress sets the the orphaned ingress gauge to zero (all services has their endpoints)
+func (cm *Controller) DecOrphanIngress(namespace string, name string, orphanityType string) {
+	labels := prometheus.Labels{
+		"namespace": namespace,
+		"ingress":   name,
+		"type":      orphanityType,
+	}
+	cm.OrphanIngress.MustCurryWith(cm.constLabels).With(labels).Set(0.0)
+}
+
 // ConfigSuccess set a boolean flag according to the output of the controller configuration reload
 func (cm *Controller) ConfigSuccess(hash uint64, success bool) {
 	if success {
@@ -242,6 +273,7 @@ func (cm Controller) Describe(ch chan<- *prometheus.Desc) {
 	cm.sslInfo.Describe(ch)
 	cm.leaderElection.Describe(ch)
 	cm.buildInfo.Describe(ch)
+	cm.OrphanIngress.Describe(ch)
 }
 
 // Collect implements the prometheus.Collector interface.
@@ -257,6 +289,7 @@ func (cm Controller) Collect(ch chan<- prometheus.Metric) {
 	cm.sslInfo.Collect(ch)
 	cm.leaderElection.Collect(ch)
 	cm.buildInfo.Collect(ch)
+	cm.OrphanIngress.Collect(ch)
 }
 
 // SetSSLExpireTime sets the expiration time of SSL Certificates
diff --git a/internal/ingress/metric/dummy.go b/internal/ingress/metric/dummy.go
index 4a6366b84..e34b5ecbd 100644
--- a/internal/ingress/metric/dummy.go
+++ b/internal/ingress/metric/dummy.go
@@ -41,6 +41,12 @@ func (dc DummyCollector) IncReloadCount() {}
 // IncReloadErrorCount ...
 func (dc DummyCollector) IncReloadErrorCount() {}
 
+// IncOrphanIngress ...
+func (dc DummyCollector) IncOrphanIngress(string, string, string) {}
+
+// DecOrphanIngress ...
+func (dc DummyCollector) DecOrphanIngress(string, string, string) {}
+
 // IncCheckCount ...
 func (dc DummyCollector) IncCheckCount(string, string) {}
 
diff --git a/internal/ingress/metric/main.go b/internal/ingress/metric/main.go
index b3323c7fe..892c8e8db 100644
--- a/internal/ingress/metric/main.go
+++ b/internal/ingress/metric/main.go
@@ -43,6 +43,8 @@ type Collector interface {
 
 	IncCheckCount(string, string)
 	IncCheckErrorCount(string, string)
+	IncOrphanIngress(string, string, string)
+	DecOrphanIngress(string, string, string)
 
 	RemoveMetrics(ingresses, endpoints, certificates []string)
 
@@ -181,6 +183,14 @@ func (c *collector) SetSSLInfo(servers []*ingress.Server) {
 	c.ingressController.SetSSLInfo(servers)
 }
 
+func (c *collector) IncOrphanIngress(namespace string, name string, orphanityType string) {
+	c.ingressController.IncOrphanIngress(namespace, name, orphanityType)
+}
+
+func (c *collector) DecOrphanIngress(namespace string, name string, orphanityType string) {
+	c.ingressController.DecOrphanIngress(namespace, name, orphanityType)
+}
+
 func (c *collector) SetHosts(hosts sets.String) {
 	c.socket.SetHosts(hosts)
 }
diff --git a/internal/k8s/main.go b/internal/k8s/main.go
index 5332631a7..e0d2a1660 100644
--- a/internal/k8s/main.go
+++ b/internal/k8s/main.go
@@ -78,6 +78,8 @@ func GetNodeIPOrName(kubeClient clientset.Interface, name string, useInternalIP
 var (
 	// IngressPodDetails hold information about the ingress-nginx pod
 	IngressPodDetails *PodInfo
+	// IngressNodeDetails old information about the node running ingress-nginx pod
+	IngressNodeDetails *NodeInfo
 )
 
 // PodInfo contains runtime information about the pod running the Ingres controller
@@ -87,6 +89,12 @@ type PodInfo struct {
 	metav1.ObjectMeta
 }
 
+// NodeInfo contains runtime information about the node pod running the Ingres controller, eg. zone where pod is running
+type NodeInfo struct {
+	metav1.TypeMeta
+	metav1.ObjectMeta
+}
+
 // GetIngressPod load the ingress-nginx pod
 func GetIngressPod(kubeClient clientset.Interface) error {
 	podName := os.Getenv("POD_NAME")
@@ -108,6 +116,18 @@ func GetIngressPod(kubeClient clientset.Interface) error {
 	pod.ObjectMeta.DeepCopyInto(&IngressPodDetails.ObjectMeta)
 	IngressPodDetails.SetLabels(pod.GetLabels())
 
+	IngressNodeDetails = &NodeInfo{
+		TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Node"},
+	}
+	// Try to get node info/labels to determine topology zone where pod is running
+	node, err := kubeClient.CoreV1().Nodes().Get(context.TODO(), pod.Spec.NodeName, metav1.GetOptions{})
+	if err != nil {
+		klog.Warningf("Unable to get NODE information: %v", err)
+	} else {
+		node.ObjectMeta.DeepCopyInto(&IngressNodeDetails.ObjectMeta)
+		IngressNodeDetails.SetLabels(node.GetLabels())
+	}
+
 	return nil
 }
 
diff --git a/pkg/flags/flags.go b/pkg/flags/flags.go
index 6e183289c..911ab775c 100644
--- a/pkg/flags/flags.go
+++ b/pkg/flags/flags.go
@@ -216,6 +216,8 @@ Takes the form "<host>:port". If not provided, no admission controller is starte
 		dynamicConfigurationRetries = flags.Int("dynamic-configuration-retries", 15, "Number of times to retry failed dynamic configuration before failing to sync an ingress.")
 
 		disableSyncEvents = flags.Bool("disable-sync-events", false, "Disables the creation of 'Sync' event resources")
+
+		enableTopologyAwareRouting = flags.Bool("enable-topology-aware-routing", false, "Enable topology aware hints feature, needs service object annotation service.kubernetes.io/topology-aware-hints sets to auto.")
 	)
 
 	flags.StringVar(&nginx.MaxmindMirror, "maxmind-mirror", "", `Maxmind mirror url (example: http://geoip.local/databases.`)
@@ -348,6 +350,7 @@ https://blog.maxmind.com/2019/12/18/significant-changes-to-accessing-and-using-g
 		SyncRateLimit:               *syncRateLimit,
 		HealthCheckHost:             *healthzHost,
 		DynamicConfigurationRetries: *dynamicConfigurationRetries,
+		EnableTopologyAwareRouting:  *enableTopologyAwareRouting,
 		ListenPorts: &ngx_config.ListenPorts{
 			Default:  *defServerPort,
 			Health:   *healthzPort,
diff --git a/rootfs/etc/nginx/template/nginx.tmpl b/rootfs/etc/nginx/template/nginx.tmpl
index fc48de912..911cf75ea 100755
--- a/rootfs/etc/nginx/template/nginx.tmpl
+++ b/rootfs/etc/nginx/template/nginx.tmpl
@@ -336,6 +336,9 @@ http {
     {{ if $cfg.UseGzip }}
     gzip on;
     gzip_comp_level {{ $cfg.GzipLevel }};
+    {{- if $cfg.GzipDisable }}
+    gzip_disable "{{ $cfg.GzipDisable }}";
+    {{- end }}
     gzip_http_version 1.1;
     gzip_min_length {{ $cfg.GzipMinLength}};
     gzip_types {{ $cfg.GzipTypes }};


Test patch:
diff --git a/internal/ingress/controller/controller_test.go b/internal/ingress/controller/controller_test.go
index 796012cd3..8cca10385 100644
--- a/internal/ingress/controller/controller_test.go
+++ b/internal/ingress/controller/controller_test.go
@@ -2481,8 +2481,9 @@ func newDynamicNginxController(t *testing.T, setConfigMap func(string) *v1.Confi
 	}
 
 	return &NGINXController{
-		store:   storer,
-		cfg:     config,
-		command: NewNginxCommand(),
+		store:           storer,
+		cfg:             config,
+		command:         NewNginxCommand(),
+		metricCollector: metric.DummyCollector{},
 	}
 }
diff --git a/internal/ingress/controller/endpointslices_test.go b/internal/ingress/controller/endpointslices_test.go
index e404c4949..b61e9a4f3 100644
--- a/internal/ingress/controller/endpointslices_test.go
+++ b/internal/ingress/controller/endpointslices_test.go
@@ -33,6 +33,7 @@ func TestGetEndpointsFromSlices(t *testing.T) {
 		svc    *corev1.Service
 		port   *corev1.ServicePort
 		proto  corev1.Protocol
+		zone   string
 		fn     func(string) ([]*discoveryv1.EndpointSlice, error)
 		result []ingress.Endpoint
 	}{
@@ -41,6 +42,7 @@ func TestGetEndpointsFromSlices(t *testing.T) {
 			nil,
 			nil,
 			corev1.ProtocolTCP,
+			"",
 			func(string) ([]*discoveryv1.EndpointSlice, error) {
 				return nil, nil
 			},
@@ -51,6 +53,7 @@ func TestGetEndpointsFromSlices(t *testing.T) {
 			&corev1.Service{},
 			nil,
 			corev1.ProtocolTCP,
+			"",
 			func(string) ([]*discoveryv1.EndpointSlice, error) {
 				return nil, nil
 			},
@@ -61,6 +64,7 @@ func TestGetEndpointsFromSlices(t *testing.T) {
 			&corev1.Service{},
 			&corev1.ServicePort{Name: "default"},
 			corev1.ProtocolTCP,
+			"",
 			func(string) ([]*discoveryv1.EndpointSlice, error) {
 				return []*discoveryv1.EndpointSlice{}, nil
 			},
@@ -75,6 +79,7 @@ func TestGetEndpointsFromSlices(t *testing.T) {
 			},
 			&corev1.ServicePort{Name: "default"},
 			corev1.ProtocolTCP,
+			"",
 			func(string) ([]*discoveryv1.EndpointSlice, error) {
 				return []*discoveryv1.EndpointSlice{}, nil
 			},
@@ -99,6 +104,7 @@ func TestGetEndpointsFromSlices(t *testing.T) {
 				TargetPort: intstr.FromInt(80),
 			},
 			corev1.ProtocolTCP,
+			"",
 			func(string) ([]*discoveryv1.EndpointSlice, error) {
 				return []*discoveryv1.EndpointSlice{}, nil
 			},
@@ -123,6 +129,7 @@ func TestGetEndpointsFromSlices(t *testing.T) {
 				TargetPort: intstr.FromInt(80),
 			},
 			corev1.ProtocolTCP,
+			"",
 			func(string) ([]*discoveryv1.EndpointSlice, error) {
 				return []*discoveryv1.EndpointSlice{}, nil
 			},
@@ -147,6 +154,7 @@ func TestGetEndpointsFromSlices(t *testing.T) {
 				TargetPort: intstr.FromInt(80),
 			},
 			corev1.ProtocolTCP,
+			"",
 			func(string) ([]*discoveryv1.EndpointSlice, error) {
 				return []*discoveryv1.EndpointSlice{}, nil
 			},
@@ -176,6 +184,7 @@ func TestGetEndpointsFromSlices(t *testing.T) {
 				TargetPort: intstr.FromInt(80),
 			},
 			corev1.ProtocolTCP,
+			"",
 			func(string) ([]*discoveryv1.EndpointSlice, error) {
 				return []*discoveryv1.EndpointSlice{}, nil
 			},
@@ -205,6 +214,7 @@ func TestGetEndpointsFromSlices(t *testing.T) {
 				TargetPort: intstr.FromInt(80),
 			},
 			corev1.ProtocolTCP,
+			"",
 			func(string) ([]*discoveryv1.EndpointSlice, error) {
 				return []*discoveryv1.EndpointSlice{}, nil
 			},
@@ -229,6 +239,7 @@ func TestGetEndpointsFromSlices(t *testing.T) {
 				TargetPort: intstr.FromInt(80),
 			},
 			corev1.ProtocolTCP,
+			"",
 			func(string) ([]*discoveryv1.EndpointSlice, error) {
 				return nil, fmt.Errorf("unexpected error")
 			},
@@ -253,6 +264,7 @@ func TestGetEndpointsFromSlices(t *testing.T) {
 				TargetPort: intstr.FromInt(80),
 			},
 			corev1.ProtocolTCP,
+			"",
 			func(string) ([]*discoveryv1.EndpointSlice, error) {
 				return []*discoveryv1.EndpointSlice{{
 					ObjectMeta: metav1.ObjectMeta{
@@ -296,6 +308,7 @@ func TestGetEndpointsFromSlices(t *testing.T) {
 				TargetPort: intstr.FromInt(80),
 			},
 			corev1.ProtocolTCP,
+			"",
 			func(string) ([]*discoveryv1.EndpointSlice, error) {
 				return []*discoveryv1.EndpointSlice{{
 					ObjectMeta: metav1.ObjectMeta{
@@ -339,6 +352,7 @@ func TestGetEndpointsFromSlices(t *testing.T) {
 				TargetPort: intstr.FromString("port-1"),
 			},
 			corev1.ProtocolTCP,
+			"",
 			func(string) ([]*discoveryv1.EndpointSlice, error) {
 				return []*discoveryv1.EndpointSlice{{
 					ObjectMeta: metav1.ObjectMeta{
@@ -382,6 +396,7 @@ func TestGetEndpointsFromSlices(t *testing.T) {
 				TargetPort: intstr.FromInt(80),
 			},
 			corev1.ProtocolTCP,
+			"",
 			func(string) ([]*discoveryv1.EndpointSlice, error) {
 				return []*discoveryv1.EndpointSlice{{
 					ObjectMeta: metav1.ObjectMeta{
@@ -430,6 +445,7 @@ func TestGetEndpointsFromSlices(t *testing.T) {
 				TargetPort: intstr.FromInt(80),
 			},
 			corev1.ProtocolTCP,
+			"",
 			func(string) ([]*discoveryv1.EndpointSlice, error) {
 				return []*discoveryv1.EndpointSlice{{
 					ObjectMeta: metav1.ObjectMeta{
@@ -478,6 +494,7 @@ func TestGetEndpointsFromSlices(t *testing.T) {
 				TargetPort: intstr.FromString("port-1"),
 			},
 			corev1.ProtocolTCP,
+			"",
 			func(string) ([]*discoveryv1.EndpointSlice, error) {
 				return []*discoveryv1.EndpointSlice{
 					{
@@ -552,6 +569,7 @@ func TestGetEndpointsFromSlices(t *testing.T) {
 				TargetPort: intstr.FromString("port-1"),
 			},
 			corev1.ProtocolTCP,
+			"",
 			func(string) ([]*discoveryv1.EndpointSlice, error) {
 				return []*discoveryv1.EndpointSlice{
 					{
@@ -622,6 +640,7 @@ func TestGetEndpointsFromSlices(t *testing.T) {
 				TargetPort: intstr.FromString("port-1"),
 			},
 			corev1.ProtocolTCP,
+			"",
 			func(string) ([]*discoveryv1.EndpointSlice, error) {
 				return []*discoveryv1.EndpointSlice{{
 					ObjectMeta: metav1.ObjectMeta{
@@ -656,11 +675,251 @@ func TestGetEndpointsFromSlices(t *testing.T) {
 				},
 			},
 		},
+		{
+			"should return one endpoint which belongs to zone",
+			&corev1.Service{
+				Spec: corev1.ServiceSpec{
+					Type:      corev1.ServiceTypeClusterIP,
+					ClusterIP: "1.1.1.1",
+					Ports: []corev1.ServicePort{
+						{
+							Name:       "default",
+							TargetPort: intstr.FromString("port-1"),
+						},
+					},
+				},
+			},
+			&corev1.ServicePort{
+				Name:       "port-1",
+				TargetPort: intstr.FromString("port-1"),
+			},
+			corev1.ProtocolTCP,
+			"eu-west-1b",
+			func(string) ([]*discoveryv1.EndpointSlice, error) {
+				return []*discoveryv1.EndpointSlice{{
+					ObjectMeta: metav1.ObjectMeta{
+						Labels: map[string]string{discoveryv1.LabelServiceName: "default"},
+					},
+					Endpoints: []discoveryv1.Endpoint{
+						{
+							Addresses: []string{"1.1.1.1"},
+							Conditions: discoveryv1.EndpointConditions{
+								Ready: &[]bool{true}[0],
+							},
+							Hints: &[]discoveryv1.EndpointHints{{
+								ForZones: []discoveryv1.ForZone{{
+									Name: "eu-west-1b",
+								}},
+							}}[0],
+						},
+						{
+							Addresses: []string{"1.1.1.2"},
+							Conditions: discoveryv1.EndpointConditions{
+								Ready: &[]bool{true}[0],
+							},
+							Hints: &[]discoveryv1.EndpointHints{{
+								ForZones: []discoveryv1.ForZone{{
+									Name: "eu-west-1a",
+								}},
+							}}[0],
+						},
+						{
+							Addresses: []string{"1.1.1.3"},
+							Conditions: discoveryv1.EndpointConditions{
+								Ready: &[]bool{true}[0],
+							},
+							Hints: &[]discoveryv1.EndpointHints{{
+								ForZones: []discoveryv1.ForZone{{
+									Name: "eu-west-1c",
+								}},
+							}}[0],
+						},
+					},
+					Ports: []discoveryv1.EndpointPort{
+						{
+							Protocol: &[]corev1.Protocol{corev1.ProtocolTCP}[0],
+							Port:     &[]int32{80}[0],
+							Name:     &[]string{"port-1"}[0],
+						},
+					},
+				}}, nil
+			},
+			[]ingress.Endpoint{
+				{
+					Address: "1.1.1.1",
+					Port:    "80",
+				},
+			},
+		},
+		{
+			"should return all endpoints because one is missing zone hint",
+			&corev1.Service{
+				Spec: corev1.ServiceSpec{
+					Type:      corev1.ServiceTypeClusterIP,
+					ClusterIP: "1.1.1.1",
+					Ports: []corev1.ServicePort{
+						{
+							Name:       "default",
+							TargetPort: intstr.FromString("port-1"),
+						},
+					},
+				},
+			},
+			&corev1.ServicePort{
+				Name:       "port-1",
+				TargetPort: intstr.FromString("port-1"),
+			},
+			corev1.ProtocolTCP,
+			"eu-west-1b",
+			func(string) ([]*discoveryv1.EndpointSlice, error) {
+				return []*discoveryv1.EndpointSlice{{
+					ObjectMeta: metav1.ObjectMeta{
+						Labels: map[string]string{discoveryv1.LabelServiceName: "default"},
+					},
+					Endpoints: []discoveryv1.Endpoint{
+						{
+							Addresses: []string{"1.1.1.1"},
+							Conditions: discoveryv1.EndpointConditions{
+								Ready: &[]bool{true}[0],
+							},
+							Hints: &[]discoveryv1.EndpointHints{{
+								ForZones: []discoveryv1.ForZone{{
+									Name: "eu-west-1b",
+								}},
+							}}[0],
+						},
+						{
+							Addresses: []string{"1.1.1.2"},
+							Conditions: discoveryv1.EndpointConditions{
+								Ready: &[]bool{true}[0],
+							},
+							Hints: &[]discoveryv1.EndpointHints{{
+								ForZones: []discoveryv1.ForZone{{
+									Name: "eu-west-1b",
+								}},
+							}}[0],
+						},
+						{
+							Addresses: []string{"1.1.1.3"},
+							Conditions: discoveryv1.EndpointConditions{
+								Ready: &[]bool{true}[0],
+							},
+							Hints: &[]discoveryv1.EndpointHints{{}}[0],
+						},
+					},
+					Ports: []discoveryv1.EndpointPort{
+						{
+							Protocol: &[]corev1.Protocol{corev1.ProtocolTCP}[0],
+							Port:     &[]int32{80}[0],
+							Name:     &[]string{"port-1"}[0],
+						},
+					},
+				}}, nil
+			},
+			[]ingress.Endpoint{
+				{
+					Address: "1.1.1.1",
+					Port:    "80",
+				},
+				{
+					Address: "1.1.1.2",
+					Port:    "80",
+				},
+				{
+					Address: "1.1.1.3",
+					Port:    "80",
+				},
+			},
+		},
+		{
+			"should return all endpoints because no zone from controller node",
+			&corev1.Service{
+				Spec: corev1.ServiceSpec{
+					Type:      corev1.ServiceTypeClusterIP,
+					ClusterIP: "1.1.1.1",
+					Ports: []corev1.ServicePort{
+						{
+							Name:       "default",
+							TargetPort: intstr.FromString("port-1"),
+						},
+					},
+				},
+			},
+			&corev1.ServicePort{
+				Name:       "port-1",
+				TargetPort: intstr.FromString("port-1"),
+			},
+			corev1.ProtocolTCP,
+			"",
+			func(string) ([]*discoveryv1.EndpointSlice, error) {
+				return []*discoveryv1.EndpointSlice{{
+					ObjectMeta: metav1.ObjectMeta{
+						Labels: map[string]string{discoveryv1.LabelServiceName: "default"},
+					},
+					Endpoints: []discoveryv1.Endpoint{
+						{
+							Addresses: []string{"1.1.1.1"},
+							Conditions: discoveryv1.EndpointConditions{
+								Ready: &[]bool{true}[0],
+							},
+							Hints: &[]discoveryv1.EndpointHints{{
+								ForZones: []discoveryv1.ForZone{{
+									Name: "eu-west-1a",
+								}},
+							}}[0],
+						},
+						{
+							Addresses: []string{"1.1.1.2"},
+							Conditions: discoveryv1.EndpointConditions{
+								Ready: &[]bool{true}[0],
+							},
+							Hints: &[]discoveryv1.EndpointHints{{
+								ForZones: []discoveryv1.ForZone{{
+									Name: "eu-west-1b",
+								}},
+							}}[0],
+						},
+						{
+							Addresses: []string{"1.1.1.3"},
+							Conditions: discoveryv1.EndpointConditions{
+								Ready: &[]bool{true}[0],
+							},
+							Hints: &[]discoveryv1.EndpointHints{{
+								ForZones: []discoveryv1.ForZone{{
+									Name: "eu-west-1c",
+								}},
+							}}[0],
+						},
+					},
+					Ports: []discoveryv1.EndpointPort{
+						{
+							Protocol: &[]corev1.Protocol{corev1.ProtocolTCP}[0],
+							Port:     &[]int32{80}[0],
+							Name:     &[]string{"port-1"}[0],
+						},
+					},
+				}}, nil
+			},
+			[]ingress.Endpoint{
+				{
+					Address: "1.1.1.1",
+					Port:    "80",
+				},
+				{
+					Address: "1.1.1.2",
+					Port:    "80",
+				},
+				{
+					Address: "1.1.1.3",
+					Port:    "80",
+				},
+			},
+		},
 	}
 
 	for _, testCase := range tests {
 		t.Run(testCase.name, func(t *testing.T) {
-			result := getEndpointsFromSlices(testCase.svc, testCase.port, testCase.proto, testCase.fn)
+			result := getEndpointsFromSlices(testCase.svc, testCase.port, testCase.proto, testCase.zone, testCase.fn)
 			if len(testCase.result) != len(result) {
 				t.Errorf("Expected %d Endpoints but got %d", len(testCase.result), len(result))
 			}
diff --git a/internal/ingress/controller/template/configmap_test.go b/internal/ingress/controller/template/configmap_test.go
index c662e0bc7..ebe55d192 100644
--- a/internal/ingress/controller/template/configmap_test.go
+++ b/internal/ingress/controller/template/configmap_test.go
@@ -64,6 +64,7 @@ func TestMergeConfigMapToStruct(t *testing.T) {
 		"access-log-path":               "/var/log/test/access.log",
 		"error-log-path":                "/var/log/test/error.log",
 		"use-gzip":                      "false",
+		"gzip-disable":                  "msie6",
 		"gzip-level":                    "9",
 		"gzip-min-length":               "1024",
 		"gzip-types":                    "text/html",
@@ -87,6 +88,7 @@ func TestMergeConfigMapToStruct(t *testing.T) {
 	def.ProxyReadTimeout = 1
 	def.ProxySendTimeout = 2
 	def.UseProxyProtocol = true
+	def.GzipDisable = "msie6"
 	def.GzipLevel = 9
 	def.GzipMinLength = 1024
 	def.GzipTypes = "text/html"
diff --git a/test/e2e-image/e2e.sh b/test/e2e-image/e2e.sh
index 7b4d56f10..ed13926fb 100755
--- a/test/e2e-image/e2e.sh
+++ b/test/e2e-image/e2e.sh
@@ -41,12 +41,22 @@ reportFileNamePrefix="report-e2e-test-suite"
 echo -e "${BGREEN}Running e2e test suite (FOCUS=${FOCUS})...${NC}"
 ginkgo "${ginkgo_args[@]}"               \
   -focus="${FOCUS}"                  \
-  -skip="\[Serial\]|\[MemoryLeak\]"  \
+  -skip="\[Serial\]|\[MemoryLeak\]|\[TopologyHints\]"  \
   -nodes="${E2E_NODES}" \
   --junit-report=$reportFileNamePrefix.xml \
   /e2e.test
 # Create configMap out of a compressed report file for extraction later
 
+# Must be isolated, there is a collision if multiple helms tries to install same clusterRole at same time
+echo -e "${BGREEN}Running e2e test for topology aware hints...${NC}"
+ginkgo "${ginkgo_args[@]}" \
+  -focus="\[TopologyHints\]" \
+  -skip="\[Serial\]|\[MemoryLeak\]]" \
+  -nodes="${E2E_NODES}" \
+  --junit-report=$reportFileNamePrefix-topology.xml \
+  /e2e.test
+# Create configMap out of a compressed report file for extraction later
+
 echo -e "${BGREEN}Running e2e test suite with tests that require serial execution...${NC}"
 ginkgo "${ginkgo_args[@]}"   \
   -focus="\[Serial\]"    \
diff --git a/test/e2e-image/namespace-overlays/topology/values.yaml b/test/e2e-image/namespace-overlays/topology/values.yaml
new file mode 100644
index 000000000..28b1cad19
--- /dev/null
+++ b/test/e2e-image/namespace-overlays/topology/values.yaml
@@ -0,0 +1,51 @@
+# TODO: remove the need to use fullnameOverride
+fullnameOverride: nginx-ingress
+controller:
+  image:
+    repository: ingress-controller/controller
+    chroot: true
+    tag: 1.0.0-dev
+    digest:
+    digestChroot:
+  scope:
+    enabled: false
+  config:
+    worker-processes: "1"
+  readinessProbe:
+    initialDelaySeconds: 3
+    periodSeconds: 1
+  livenessProbe:
+    initialDelaySeconds: 3
+    periodSeconds: 1
+  service:
+    type: NodePort
+  electionID: ingress-controller-leader
+  ingressClassResource:
+    # We will create and remove each IC/ClusterRole/ClusterRoleBinding per test so there's no conflict
+    enabled: false
+  extraArgs:
+    tcp-services-configmap: $NAMESPACE/tcp-services
+    # e2e tests do not require information about ingress status
+    update-status: "false"
+  terminationGracePeriodSeconds: 1
+  admissionWebhooks:
+    enabled: false
+
+  enableTopologyAwareRouting: true
+
+  # ulimit -c unlimited
+  # mkdir -p /tmp/coredump
+  # chmod a+rwx /tmp/coredump
+  # echo "/tmp/coredump/core.%e.%p.%h.%t" > /proc/sys/kernel/core_pattern
+  extraVolumeMounts:
+    - name: coredump
+      mountPath: /tmp/coredump
+
+  extraVolumes:
+    - name: coredump
+      hostPath:
+        path: /tmp/coredump
+
+rbac:
+  create: true
+  scope: false
diff --git a/test/e2e/endpointslices/topology.go b/test/e2e/endpointslices/topology.go
new file mode 100644
index 000000000..ce913e966
--- /dev/null
+++ b/test/e2e/endpointslices/topology.go
@@ -0,0 +1,112 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package endpointslices
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"os/exec"
+	"strings"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"github.com/onsi/ginkgo/v2"
+	"github.com/stretchr/testify/assert"
+
+	"k8s.io/ingress-nginx/internal/nginx"
+	"k8s.io/ingress-nginx/test/e2e/framework"
+)
+
+var _ = framework.IngressNginxDescribe("[TopologyHints] topology aware routing", func() {
+	f := framework.NewDefaultFramework("topology")
+	host := "topology-svc.foo.com"
+
+	ginkgo.BeforeEach(func() {
+		f.NewEchoDeployment(framework.WithDeploymentReplicas(2), framework.WithSvcTopologyAnnotations())
+	})
+
+	ginkgo.AfterEach(func() {
+		// we need to uninstall chart because of clusterRole which is not destroyed with namespace
+		err := uninstallChart(f)
+		assert.Nil(ginkgo.GinkgoT(), err, "uninstalling helm chart")
+	})
+
+	ginkgo.It("should return 200 when service has topology hints", func() {
+
+		annotations := make(map[string]string)
+		ing := framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, annotations)
+		f.EnsureIngress(ing)
+
+		f.WaitForNginxServer(host, func(server string) bool {
+			return strings.Contains(server, fmt.Sprintf("server_name %s", host))
+		})
+
+		ginkgo.By("checking if the service is reached")
+		f.HTTPTestClient().
+			GET("/").
+			WithHeader("Host", host).
+			Expect().
+			Status(http.StatusOK)
+
+		slices, err := f.KubeClientSet.DiscoveryV1().EndpointSlices(f.Namespace).List(context.TODO(), metav1.ListOptions{
+			LabelSelector: "kubernetes.io/service-name=echo",
+			Limit:         1,
+		})
+		assert.Nil(ginkgo.GinkgoT(), err)
+
+		// check if we have hints, really depends on k8s endpoint slice controller
+		gotHints := true
+		for _, ep := range slices.Items[0].Endpoints {
+			if ep.Hints == nil || len(ep.Hints.ForZones) == 0 {
+				gotHints = false
+				break
+			}
+		}
+
+		curlCmd := fmt.Sprintf("curl --fail --silent http://localhost:%v/configuration/backends", nginx.StatusPort)
+		status, err := f.ExecIngressPod(curlCmd)
+		assert.Nil(ginkgo.GinkgoT(), err)
+		var backends []map[string]interface{}
+		json.Unmarshal([]byte(status), &backends)
+		gotBackends := 0
+		for _, bck := range backends {
+			if strings.Contains(bck["name"].(string), "topology") {
+				gotBackends = len(bck["endpoints"].([]interface{}))
+			}
+		}
+
+		if gotHints {
+			//we have 2 replics, if there is just one backend it means that we are routing according slices hints to same zone as controller is
+			assert.Equal(ginkgo.GinkgoT(), 1, gotBackends)
+		} else {
+			// two replicas should have two endpoints without topology hints
+			assert.Equal(ginkgo.GinkgoT(), 2, gotBackends)
+		}
+	})
+})
+
+func uninstallChart(f *framework.Framework) error {
+	cmd := exec.Command("helm", "uninstall", "--namespace", f.Namespace, "nginx-ingress")
+	_, err := cmd.CombinedOutput()
+	if err != nil {
+		return fmt.Errorf("unexpected error uninstalling ingress-nginx release: %v", err)
+	}
+
+	return nil
+}
diff --git a/test/e2e/framework/deployment.go b/test/e2e/framework/deployment.go
index 3cfe8f360..8115fd12e 100644
--- a/test/e2e/framework/deployment.go
+++ b/test/e2e/framework/deployment.go
@@ -40,10 +40,10 @@ const SlowEchoService = "slow-echo"
 const HTTPBinService = "httpbin"
 
 type deploymentOptions struct {
-	namespace string
-	name      string
-	replicas  int
-	image     string
+	namespace      string
+	name           string
+	replicas       int
+	svcAnnotations map[string]string
 }
 
 // WithDeploymentNamespace allows configuring the deployment's namespace
@@ -53,6 +53,15 @@ func WithDeploymentNamespace(n string) func(*deploymentOptions) {
 	}
 }
 
+// WithSvcTopologyAnnotations create svc with topology aware hints sets to auto
+func WithSvcTopologyAnnotations() func(*deploymentOptions) {
+	return func(o *deploymentOptions) {
+		o.svcAnnotations = map[string]string{
+			"service.kubernetes.io/topology-aware-hints": "auto",
+		}
+	}
+}
+
 // WithDeploymentName allows configuring the deployment's names
 func WithDeploymentName(n string) func(*deploymentOptions) {
 	return func(o *deploymentOptions) {
@@ -95,8 +104,9 @@ func (f *Framework) NewEchoDeployment(opts ...func(*deploymentOptions)) {
 
 	service := &corev1.Service{
 		ObjectMeta: metav1.ObjectMeta{
-			Name:      options.name,
-			Namespace: options.namespace,
+			Name:        options.name,
+			Namespace:   options.namespace,
+			Annotations: options.svcAnnotations,
 		},
 		Spec: corev1.ServiceSpec{
 			Ports: []corev1.ServicePort{
diff --git a/test/e2e/kind.yaml b/test/e2e/kind.yaml
index 97dc7082d..07a56dae8 100644
--- a/test/e2e/kind.yaml
+++ b/test/e2e/kind.yaml
@@ -2,8 +2,14 @@ kind: Cluster
 apiVersion: kind.x-k8s.io/v1alpha4
 nodes:
 - role: control-plane
+  labels:
+    topology.kubernetes.io/zone: zone-1
 - role: worker
+  labels:
+    topology.kubernetes.io/zone: zone-1
 - role: worker
+  labels:
+    topology.kubernetes.io/zone: zone-2
 kubeadmConfigPatches:
 - |
   kind: ClusterConfiguration
diff --git a/test/e2e/settings/gzip.go b/test/e2e/settings/gzip.go
new file mode 100644
index 000000000..68e80d3a0
--- /dev/null
+++ b/test/e2e/settings/gzip.go
@@ -0,0 +1,99 @@
+/*
+Copyright 2023 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package settings
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/onsi/ginkgo/v2"
+
+	"k8s.io/ingress-nginx/internal/ingress/controller/config"
+	"k8s.io/ingress-nginx/test/e2e/framework"
+)
+
+var _ = framework.DescribeSetting("gzip", func() {
+	f := framework.NewDefaultFramework("gzip")
+
+	ginkgo.It("should be disabled by default", func() {
+		f.WaitForNginxConfiguration(
+			func(cfg string) bool {
+				return !strings.Contains(cfg, "gzip on;")
+			})
+	})
+
+	ginkgo.It("should be enabled with default settings", func() {
+		f.UpdateNginxConfigMapData("use-gzip", "true")
+
+		f.WaitForNginxConfiguration(
+			func(cfg string) bool {
+				defaultCfg := config.NewDefault()
+				return strings.Contains(cfg, "gzip on;") &&
+					strings.Contains(cfg, fmt.Sprintf("gzip_comp_level %d;", defaultCfg.GzipLevel)) &&
+					!strings.Contains(cfg, "gzip_disable") &&
+					strings.Contains(cfg, "gzip_http_version 1.1;") &&
+					strings.Contains(cfg, fmt.Sprintf("gzip_min_length %d;", defaultCfg.GzipMinLength)) &&
+					strings.Contains(cfg, fmt.Sprintf("gzip_types %s;", defaultCfg.GzipTypes)) &&
+					strings.Contains(cfg, "gzip_proxied any;") &&
+					strings.Contains(cfg, "gzip_vary on;")
+			})
+	})
+
+	ginkgo.It("should set gzip_comp_level to 4", func() {
+		f.UpdateNginxConfigMapData("use-gzip", "true")
+		f.UpdateNginxConfigMapData("gzip-level", "4")
+
+		f.WaitForNginxConfiguration(
+			func(cfg string) bool {
+				return strings.Contains(cfg, "gzip on;") &&
+					strings.Contains(cfg, "gzip_comp_level 4;")
+			})
+	})
+
+	ginkgo.It("should set gzip_disable to msie6", func() {
+		f.UpdateNginxConfigMapData("use-gzip", "true")
+		f.UpdateNginxConfigMapData("gzip-disable", "msie6")
+
+		f.WaitForNginxConfiguration(
+			func(cfg string) bool {
+				return strings.Contains(cfg, "gzip on;") &&
+					strings.Contains(cfg, `gzip_disable "msie6";`)
+			})
+	})
+
+	ginkgo.It("should set gzip_min_length to 100", func() {
+		f.UpdateNginxConfigMapData("use-gzip", "true")
+		f.UpdateNginxConfigMapData("gzip-min-length", "100")
+
+		f.WaitForNginxConfiguration(
+			func(cfg string) bool {
+				return strings.Contains(cfg, "gzip on;") &&
+					strings.Contains(cfg, "gzip_min_length 100;")
+			})
+	})
+
+	ginkgo.It("should set gzip_types to application/javascript", func() {
+		f.UpdateNginxConfigMapData("use-gzip", "true")
+		f.UpdateNginxConfigMapData("gzip-types", "application/javascript")
+
+		f.WaitForNginxConfiguration(
+			func(cfg string) bool {
+				return strings.Contains(cfg, "gzip on;") &&
+					strings.Contains(cfg, "gzip_types application/javascript;")
+			})
+	})
+})