Repository: linkerd/linkerd2
Pull request #4725 (2020-07-07 06:59:55)

update helm render tests to consider child charts values.yaml
Helm installation by default, considers values.yaml for dependend charts
and uses them in rendering (which are empty as of now). This function is
being used for add-ons to keep the default template values, allowing
further override from the parent chart's i.e linkerd2 values.yaml 
or --addon-config through CLI as per #4682 

This PR updates the Helm tests to reflect the same behaviour i.e consider
values.yaml of chart dependencies if present.

Signed-off-by: Tarun Pothulapati <tarunpothulapati@outlook.com>

<!--  Thanks for sending a pull request!

If you already have a well-structured git commit message, chances are GitHub
set the title and description of this PR to the git commit message subject and
body, respectively. If so, you may delete these instructions and submit your PR.

If this is your first time, please read our contributor guide:
https://github.com/linkerd/linkerd2/blob/main/CONTRIBUTING.md

The title and description of your Pull Request should match the git commit
subject and body, respectively. Git commit messages are structured as follows:

```
Subject

Problem

Solution

Validation

Fixes #[GitHub issue ID]

DCO Sign off
```

Example git commit message:

```
Introduce Pull Request Template

GitHub's community guidelines recommend a pull request template, the repo was
lacking one.

Introduce a `PULL_REQUEST_TEMPLATE.md` file.

Once merged, the
[Community profile checklist](https://github.com/linkerd/linkerd2/community)
should indicate the repo now provides a pull request template.

Fixes #3321

Signed-off-by: Jane Smith <jane.smith@example.com>
```

Note the git commit message subject becomes the pull request title.

For more details around git commits, see the section on Committing in our
contributor guide:
https://github.com/linkerd/linkerd2/blob/main/CONTRIBUTING.md#committing
-->



Patch:
diff --git a/.github/workflows/kind_integration.yml b/.github/workflows/kind_integration.yml
index 5f2b6a663..72795065a 100644
--- a/.github/workflows/kind_integration.yml
+++ b/.github/workflows/kind_integration.yml
@@ -96,7 +96,7 @@ jobs:
   kind_integration_tests:
     strategy:
       matrix:
-        integration_test: [custom-domain, deep, external-issuer, helm, helm-upgrade, uninstall, upgrade-edge, upgrade-stable]
+        integration_test: [cluster-domain, deep, external-issuer, helm, helm-upgrade, uninstall, upgrade-edge, upgrade-stable]
     needs: [docker_build]
     name: Integration tests (${{ matrix.integration_test }})
     runs-on: ubuntu-18.04
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 56a926eb3..04836cf37 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -121,7 +121,7 @@ jobs:
   kind_integration_tests:
     strategy:
       matrix:
-        integration_test: [custom-domain, deep, external-issuer, helm, helm-upgrade, uninstall, upgrade-edge, upgrade-stable]
+        integration_test: [cluster-domain, deep, external-issuer, helm, helm-upgrade, uninstall, upgrade-edge, upgrade-stable]
     needs: [docker_build]
     name: Integration tests (${{ matrix.integration_test }})
     runs-on: ubuntu-18.04
diff --git a/bin/_release.sh b/bin/_release.sh
index 099d51c4a..9b16e1164 100644
--- a/bin/_release.sh
+++ b/bin/_release.sh
@@ -19,7 +19,7 @@ extract_release_notes() {
 
   # Save commit message into temporary file.
   #
-  # Match each occurence of the regex and increment `n` by 1. While n == 1
+  # Match each occurrence of the regex and increment `n` by 1. While n == 1
   # (which is true only for the first section) print that line of `CHANGES.md`.
   # This ends up being the first section of release changes.
   awk '/^## (edge|stable)-[0-9]+\.[0-9]+\.[0-9]+/{n++} n==1' "$rootdir"/CHANGES.md > "$tmp"
diff --git a/charts/add-ons/grafana/templates/grafana.yaml b/charts/add-ons/grafana/templates/grafana.yaml
index 3227cb2ba..5a526cdff 100644
--- a/charts/add-ons/grafana/templates/grafana.yaml
+++ b/charts/add-ons/grafana/templates/grafana.yaml
@@ -116,6 +116,9 @@ spec:
         {{.Values.global.workloadNamespaceLabel}}: {{.Values.global.namespace}}
         {{- include "partials.proxy.labels" .Values.global.proxy | nindent 8}}
     spec:
+      {{- if .Values.tolerations -}}
+      {{- include "linkerd.tolerations" . | nindent 6 }}
+      {{- end -}}
       {{- include "linkerd.node-selector" . | nindent 6 }}
       containers:
       - env:
diff --git a/charts/linkerd2/README.md b/charts/linkerd2/README.md
index d661ea10a..a5c0bc1cf 100644
--- a/charts/linkerd2/README.md
+++ b/charts/linkerd2/README.md
@@ -22,7 +22,7 @@ generate these automatically). You can provide your own, or follow [these
 instructions](https://linkerd.io/2/tasks/generate-certificates/) to generate new
 ones.
 
-Note that the provided certificates must be ECDSA certficates.
+Note that the provided certificates must be ECDSA certificates.
 
 ## Adding Linkerd's Helm repository
 
@@ -198,6 +198,8 @@ their default values.
 | `webResources`                              | CPU and Memory resources required by web UI (see `global.proxy.resources` for sub-fields)             |   |
 | `webProxyResources`                         | CPU and Memory resources required by proxy injected into web UI pod (see `global.proxy.resources` for sub-fields)             | values in `global.proxy.resources`   |
 | `enforcedHostRegexp`                        | Host header validation regex for the dashboard. See the [Linkerd documentation](https://linkerd.io/2/tasks/exposing-dashboard) for more information                                   | `""`                                 |
+| `nodeSelector`                        | NodeSelector section, See the [K8S documentation](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector) for more information                                   | `beta.kubernetes.io/os: linux`                                 |
+| `tolerations`                        | Tolerations section, See the [K8S documentation](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/) for more information                                   |                                  |
 
 ## Add-Ons Configuration
 
diff --git a/charts/linkerd2/templates/controller.yaml b/charts/linkerd2/templates/controller.yaml
index 540a5821b..ef5a04dae 100644
--- a/charts/linkerd2/templates/controller.yaml
+++ b/charts/linkerd2/templates/controller.yaml
@@ -61,6 +61,9 @@ spec:
         {{.Values.global.workloadNamespaceLabel}}: {{.Values.global.namespace}}
         {{- include "partials.proxy.labels" .Values.global.proxy | nindent 8}}
     spec:
+      {{- if .Values.tolerations -}}
+      {{- include "linkerd.tolerations" . | nindent 6 }}
+      {{- end -}}
       {{- include "linkerd.node-selector" . | nindent 6 }}
       {{- if .Values.enablePodAntiAffinity -}}
       {{- $local := dict "component" "controller" "label" .Values.global.controllerComponentLabel -}}
diff --git a/charts/linkerd2/templates/destination.yaml b/charts/linkerd2/templates/destination.yaml
index 84880e7e2..24771cb52 100644
--- a/charts/linkerd2/templates/destination.yaml
+++ b/charts/linkerd2/templates/destination.yaml
@@ -61,6 +61,9 @@ spec:
         {{.Values.global.workloadNamespaceLabel}}: {{.Values.global.namespace}}
         {{- include "partials.proxy.labels" .Values.global.proxy | nindent 8}}
     spec:
+      {{- if .Values.tolerations -}}
+      {{- include "linkerd.tolerations" . | nindent 6 }}
+      {{- end -}}
       {{- include "linkerd.node-selector" . | nindent 6 }}
       {{- if .Values.enablePodAntiAffinity -}}
       {{- $local := dict "component" "destination" "label" .Values.global.controllerComponentLabel -}}
diff --git a/charts/linkerd2/templates/heartbeat.yaml b/charts/linkerd2/templates/heartbeat.yaml
index 66d8906ef..91b268ff2 100644
--- a/charts/linkerd2/templates/heartbeat.yaml
+++ b/charts/linkerd2/templates/heartbeat.yaml
@@ -30,6 +30,9 @@ spec:
           annotations:
             {{.Values.global.createdByAnnotation}}: {{default (printf "linkerd/helm %s" .Values.global.linkerdVersion) .Values.global.cliVersion}}
         spec:
+          {{- if .Values.tolerations -}}
+          {{- include "linkerd.tolerations" . | nindent 10 }}
+          {{- end -}}
           {{- include "linkerd.node-selector" . | nindent 10 }}
           serviceAccountName: linkerd-heartbeat
           restartPolicy: Never
diff --git a/charts/linkerd2/templates/identity.yaml b/charts/linkerd2/templates/identity.yaml
index 08a42deb6..6374ab258 100644
--- a/charts/linkerd2/templates/identity.yaml
+++ b/charts/linkerd2/templates/identity.yaml
@@ -81,6 +81,9 @@ spec:
         {{.Values.global.workloadNamespaceLabel}}: {{.Values.global.namespace}}
         {{- include "partials.proxy.labels" .Values.global.proxy | nindent 8}}
     spec:
+      {{- if .Values.tolerations -}}
+      {{- include "linkerd.tolerations" . | nindent 6 }}
+      {{- end -}}
       {{- include "linkerd.node-selector" . | nindent 6 }}
       {{- if .Values.enablePodAntiAffinity -}}
       {{- $local := dict "component" "identity" "label" .Values.global.controllerComponentLabel -}}
diff --git a/charts/linkerd2/templates/prometheus.yaml b/charts/linkerd2/templates/prometheus.yaml
index f4d14a2a3..c3f3fbb2d 100644
--- a/charts/linkerd2/templates/prometheus.yaml
+++ b/charts/linkerd2/templates/prometheus.yaml
@@ -211,6 +211,9 @@ spec:
         {{.Values.global.workloadNamespaceLabel}}: {{.Values.global.namespace}}
         {{- include "partials.proxy.labels" .Values.global.proxy | nindent 8}}
     spec:
+      {{- if .Values.tolerations -}}
+      {{- include "linkerd.tolerations" . | nindent 6 }}
+      {{- end -}}
       {{- include "linkerd.node-selector" . | nindent 6 }}
       securityContext:
         fsGroup: 65534
diff --git a/charts/linkerd2/templates/proxy-injector.yaml b/charts/linkerd2/templates/proxy-injector.yaml
index a642ec98d..6583255ab 100644
--- a/charts/linkerd2/templates/proxy-injector.yaml
+++ b/charts/linkerd2/templates/proxy-injector.yaml
@@ -43,6 +43,9 @@ spec:
         {{.Values.global.workloadNamespaceLabel}}: {{.Values.global.namespace}}
         {{- include "partials.proxy.labels" .Values.global.proxy | nindent 8}}
     spec:
+      {{- if .Values.tolerations -}}
+      {{- include "linkerd.tolerations" . | nindent 6 }}
+      {{- end -}}
       {{- include "linkerd.node-selector" . | nindent 6 }}
       {{- if .Values.enablePodAntiAffinity -}}
       {{- $local := dict "component" "proxy-injector" "label" .Values.global.controllerComponentLabel -}}
diff --git a/charts/linkerd2/templates/smi-metrics.yaml b/charts/linkerd2/templates/smi-metrics.yaml
index 82ead8ae5..0f7f23298 100644
--- a/charts/linkerd2/templates/smi-metrics.yaml
+++ b/charts/linkerd2/templates/smi-metrics.yaml
@@ -48,6 +48,9 @@ spec:
         {{.Values.global.workloadNamespaceLabel}}: {{.Values.global.namespace}}
         {{- include "partials.proxy.labels" .Values.global.proxy | nindent 8}}
     spec:
+      {{- if .Values.tolerations -}}
+      {{- include "linkerd.tolerations" . | nindent 6 }}
+      {{- end -}}
       {{- include "linkerd.node-selector" . | nindent 6 }}
       {{- if .Values.enablePodAntiAffinity -}}
       {{- $local := dict "component" "smi-metrics" "label" .Values.global.controllerComponentLabel -}}
diff --git a/charts/linkerd2/templates/sp-validator.yaml b/charts/linkerd2/templates/sp-validator.yaml
index fea9d5121..217e694f7 100644
--- a/charts/linkerd2/templates/sp-validator.yaml
+++ b/charts/linkerd2/templates/sp-validator.yaml
@@ -62,6 +62,9 @@ spec:
         {{.Values.global.workloadNamespaceLabel}}: {{.Values.global.namespace}}
         {{- include "partials.proxy.labels" .Values.global.proxy | nindent 8}}
     spec:
+      {{- if .Values.tolerations -}}
+      {{- include "linkerd.tolerations" . | nindent 6 }}
+      {{- end -}}
       {{- include "linkerd.node-selector" . | nindent 6 }}
       {{- if .Values.enablePodAntiAffinity -}}
       {{- $local := dict "component" "sp-validator" "label" .Values.global.controllerComponentLabel -}}
diff --git a/charts/linkerd2/templates/tap.yaml b/charts/linkerd2/templates/tap.yaml
index 9d93938f3..f31f13eaf 100644
--- a/charts/linkerd2/templates/tap.yaml
+++ b/charts/linkerd2/templates/tap.yaml
@@ -67,6 +67,9 @@ spec:
         {{.Values.global.workloadNamespaceLabel}}: {{.Values.global.namespace}}
         {{- include "partials.proxy.labels" .Values.global.proxy | nindent 8}}
     spec:
+      {{- if .Values.tolerations -}}
+      {{- include "linkerd.tolerations" . | nindent 6 }}
+      {{- end -}}
       {{- include "linkerd.node-selector" . | nindent 6 }}
       {{- if .Values.enablePodAntiAffinity -}}
       {{- $local := dict "component" "tap" "label" .Values.global.controllerComponentLabel -}}
diff --git a/charts/linkerd2/templates/web.yaml b/charts/linkerd2/templates/web.yaml
index f0637c5a8..5737cde04 100644
--- a/charts/linkerd2/templates/web.yaml
+++ b/charts/linkerd2/templates/web.yaml
@@ -59,6 +59,9 @@ spec:
         {{.Values.global.workloadNamespaceLabel}}: {{.Values.global.namespace}}
         {{- include "partials.proxy.labels" .Values.global.proxy | nindent 8}}
     spec:
+      {{- if .Values.tolerations -}}
+      {{- include "linkerd.tolerations" . | nindent 6 }}
+      {{- end -}}
       {{- include "linkerd.node-selector" . | nindent 6 }}
       containers:
       - args:
diff --git a/charts/linkerd2/values.yaml b/charts/linkerd2/values.yaml
index e094c0121..1f2cf837e 100644
--- a/charts/linkerd2/values.yaml
+++ b/charts/linkerd2/values.yaml
@@ -251,6 +251,10 @@ installNamespace: true
 nodeSelector:
   beta.kubernetes.io/os: linux
 
+# Tolerations constraints for control-plane components
+# https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/
+#tolerations:
+
 smiMetrics:
   enabled: false
   externalSecret: false
diff --git a/charts/partials/templates/_tolerations.tpl b/charts/partials/templates/_tolerations.tpl
new file mode 100644
index 000000000..c2292b146
--- /dev/null
+++ b/charts/partials/templates/_tolerations.tpl
@@ -0,0 +1,4 @@
+{{- define "linkerd.tolerations" -}}
+tolerations:
+{{ toYaml .Values.tolerations | trim | indent 2 }}
+{{- end -}}
diff --git a/controller/api/destination/watcher/endpoints_watcher.go b/controller/api/destination/watcher/endpoints_watcher.go
index 7e8a4257d..32f71baa4 100644
--- a/controller/api/destination/watcher/endpoints_watcher.go
+++ b/controller/api/destination/watcher/endpoints_watcher.go
@@ -7,11 +7,13 @@ import (
 	"sync"
 
 	"github.com/linkerd/linkerd2/controller/k8s"
-	consts "github.com/linkerd/linkerd2/pkg/k8s"
+	pkgK8s "github.com/linkerd/linkerd2/pkg/k8s"
 	"github.com/prometheus/client_golang/prometheus"
 	logging "github.com/sirupsen/logrus"
 	corev1 "k8s.io/api/core/v1"
+	discovery "k8s.io/api/discovery/v1beta1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	k8slabels "k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apimachinery/pkg/util/intstr"
 	"k8s.io/client-go/tools/cache"
 )
@@ -122,7 +124,9 @@ type (
 var endpointsVecs = newEndpointsMetricsVecs()
 
 // NewEndpointsWatcher creates an EndpointsWatcher and begins watching the
-// k8sAPI for pod, service, and endpoint changes.
+// k8sAPI for pod, service, and endpoint changes. An EndpointsWatcher will
+// watch on Endpoints or EndpointSlice resources, depending on cluster configuration.
+//TODO: Allow EndpointSlice resources to be used once opt-in functionality is supported.
 func NewEndpointsWatcher(k8sAPI *k8s.API, log *logging.Entry) *EndpointsWatcher {
 	ew := &EndpointsWatcher{
 		publishers: make(map[ServiceID]*servicePublisher),
@@ -145,12 +149,22 @@ func NewEndpointsWatcher(k8sAPI *k8s.API, log *logging.Entry) *EndpointsWatcher
 		UpdateFunc: func(_, obj interface{}) { ew.addService(obj) },
 	})
 
-	k8sAPI.Endpoint().Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
-		AddFunc:    ew.addEndpoints,
-		DeleteFunc: ew.deleteEndpoints,
-		UpdateFunc: func(_, obj interface{}) { ew.addEndpoints(obj) },
-	})
-
+	if !pkgK8s.EndpointSliceAccess(k8sAPI.Client) {
+		// ew.log.Debugf("Cluster does not have EndpointSlice access:%v", err)
+		ew.log.Debugf("Watching Endpoints resources")
+		k8sAPI.Endpoint().Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
+			AddFunc:    ew.addEndpoints,
+			DeleteFunc: ew.deleteEndpoints,
+			UpdateFunc: func(_, obj interface{}) { ew.addEndpoints(obj) },
+		})
+	} else {
+		ew.log.Debugf("Watching EndpointSlice resources")
+		k8sAPI.ES().Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
+			AddFunc:    ew.addEndpointSlice,
+			DeleteFunc: ew.deleteEndpointSlice,
+			UpdateFunc: ew.updateEndpointSlice,
+		})
+	}
 	return ew
 }
 
@@ -240,17 +254,17 @@ func (ew *EndpointsWatcher) deleteService(obj interface{}) {
 }
 
 func (ew *EndpointsWatcher) addEndpoints(obj interface{}) {
-	endpoints := obj.(*corev1.Endpoints)
-	if endpoints.Namespace == kubeSystem {
+	endpoints, ok := obj.(*corev1.Endpoints)
+	if !ok {
+		ew.log.Errorf("error processing endpoints resource, got %#v expected *corev1.Endpoints", obj)
 		return
 	}
-	id := ServiceID{
-		Namespace: endpoints.Namespace,
-		Name:      endpoints.Name,
-	}
 
+	if endpoints.Namespace == kubeSystem {
+		return
+	}
+	id := ServiceID{endpoints.Namespace, endpoints.Name}
 	sp := ew.getOrNewServicePublisher(id)
-
 	sp.updateEndpoints(endpoints)
 }
 
@@ -283,6 +297,84 @@ func (ew *EndpointsWatcher) deleteEndpoints(obj interface{}) {
 	}
 }
 
+func (ew *EndpointsWatcher) addEndpointSlice(obj interface{}) {
+	newSlice, ok := obj.(*discovery.EndpointSlice)
+	if !ok {
+		ew.log.Errorf("error processing EndpointSlice resource, got %#v expected *discovery.EndpointSlice", obj)
+		return
+	}
+
+	if newSlice.Namespace == kubeSystem {
+		return
+	}
+
+	id, err := getEndpointSliceServiceID(newSlice)
+	if err != nil {
+		ew.log.Errorf("Could not fetch resource service name:%v", err)
+		return
+	}
+
+	sp := ew.getOrNewServicePublisher(id)
+	sp.addEndpointSlice(newSlice)
+}
+
+func (ew *EndpointsWatcher) updateEndpointSlice(oldObj interface{}, newObj interface{}) {
+	oldSlice, ok := oldObj.(*discovery.EndpointSlice)
+	if !ok {
+		ew.log.Errorf("error processing EndpointSlice resource, got %#v expected *discovery.EndpointSlice", oldObj)
+		return
+	}
+	newSlice, ok := newObj.(*discovery.EndpointSlice)
+	if !ok {
+		ew.log.Errorf("error processing EndpointSlice resource, got %#v expected *discovery.EndpointSlice", newObj)
+		return
+	}
+
+	if newSlice.Namespace == kubeSystem {
+		return
+	}
+
+	id, err := getEndpointSliceServiceID(newSlice)
+	if err != nil {
+		ew.log.Errorf("Could not fetch resource service name:%v", err)
+		return
+	}
+
+	sp, ok := ew.getServicePublisher(id)
+	if ok {
+		sp.updateEndpointSlice(oldSlice, newSlice)
+	}
+}
+
+func (ew *EndpointsWatcher) deleteEndpointSlice(obj interface{}) {
+	es, ok := obj.(*discovery.EndpointSlice)
+	if !ok {
+		tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
+		if !ok {
+			ew.log.Errorf("couldn't get object from DeletedFinalStateUnknown %#v", obj)
+		}
+		es, ok = tombstone.Obj.(*discovery.EndpointSlice)
+		if !ok {
+			ew.log.Errorf("DeletedFinalStateUnknown contained object that is not an EndpointSlice %#v", obj)
+			return
+		}
+	}
+
+	if es.Namespace == kubeSystem {
+		return
+	}
+
+	id, err := getEndpointSliceServiceID(es)
+	if err != nil {
+		ew.log.Errorf("Could not fetch resource service name:%v", err)
+	}
+
+	sp, ok := ew.getServicePublisher(id)
+	if ok {
+		sp.deleteEndpointSlice(es)
+	}
+}
+
 // Returns the servicePublisher for the given id if it exists.  Otherwise,
 // create a new one and return it.
 func (ew *EndpointsWatcher) getOrNewServicePublisher(id ServiceID) *servicePublisher {
@@ -323,7 +415,6 @@ func (sp *servicePublisher) updateEndpoints(newEndpoints *corev1.Endpoints) {
 	sp.Lock()
 	defer sp.Unlock()
 	sp.log.Debugf("Updating endpoints for %s", sp.id)
-
 	for _, port := range sp.ports {
 		port.updateEndpoints(newEndpoints)
 	}
@@ -333,12 +424,38 @@ func (sp *servicePublisher) deleteEndpoints() {
 	sp.Lock()
 	defer sp.Unlock()
 	sp.log.Debugf("Deleting endpoints for %s", sp.id)
-
 	for _, port := range sp.ports {
 		port.noEndpoints(false)
 	}
 }
 
+func (sp *servicePublisher) addEndpointSlice(newSlice *discovery.EndpointSlice) {
+	sp.Lock()
+	defer sp.Unlock()
+	sp.log.Debugf("Adding EndpointSlice for %s", sp.id)
+	for _, port := range sp.ports {
+		port.addEndpointSlice(newSlice)
+	}
+}
+
+func (sp *servicePublisher) updateEndpointSlice(oldSlice *discovery.EndpointSlice, newSlice *discovery.EndpointSlice) {
+	sp.Lock()
+	defer sp.Unlock()
+	sp.log.Debugf("Updating EndpointSlice for %s", sp.id)
+	for _, port := range sp.ports {
+		port.updateEndpointSlice(oldSlice, newSlice)
+	}
+}
+
+func (sp *servicePublisher) deleteEndpointSlice(es *discovery.EndpointSlice) {
+	sp.Lock()
+	defer sp.Unlock()
+	sp.log.Debugf("Deleting EndpointSlice for %s", sp.id)
+	for _, port := range sp.ports {
+		port.deleteEndpointSlice(es)
+	}
+}
+
 func (sp *servicePublisher) updateService(newService *corev1.Service) {
 	sp.Lock()
 	defer sp.Unlock()
@@ -411,12 +528,29 @@ func (sp *servicePublisher) newPortPublisher(srcPort Port, hostname string) *por
 		metrics:    endpointsVecs.newEndpointsMetrics(sp.metricsLabels(srcPort, hostname)),
 	}
 
-	endpoints, err := sp.k8sAPI.Endpoint().Lister().Endpoints(sp.id.Namespace).Get(sp.id.Name)
-	if err != nil && !apierrors.IsNotFound(err) {
-		sp.log.Errorf("error getting endpoints: %s", err)
-	}
-	if err == nil {
-		port.updateEndpoints(endpoints)
+	if !pkgK8s.EndpointSliceAccess(sp.k8sAPI.Client) {
+		// sp.log.Debugf("No EndpointSlice access, using endpoints:%v", err)
+		endpoints, err := sp.k8sAPI.Endpoint().Lister().Endpoints(sp.id.Namespace).Get(sp.id.Name)
+		if err != nil && !apierrors.IsNotFound(err) {
+			sp.log.Errorf("error getting endpoints: %s", err)
+		}
+		if err == nil {
+			port.updateEndpoints(endpoints)
+		}
+	} else {
+		sp.log.Debugf("Using EndpointSlice")
+		matchLabels := map[string]string{discovery.LabelServiceName: sp.id.Name}
+		selector := k8slabels.Set(matchLabels).AsSelector()
+
+		sliceList, err := sp.k8sAPI.ES().Lister().EndpointSlices(sp.id.Namespace).List(selector)
+		if err != nil && !apierrors.IsNotFound(err) {
+			sp.log.Errorf("error getting endpointSlice list: %s", err)
+		}
+		if err == nil {
+			for _, slice := range sliceList {
+				port.addEndpointSlice(slice)
+			}
+		}
 	}
 
 	return port
@@ -451,21 +585,92 @@ func (pp *portPublisher) updateEndpoints(endpoints *corev1.Endpoints) {
 			}
 		}
 	}
+	pp.addresses = newAddressSet
 	pp.exists = true
+	pp.metrics.incUpdates()
+	pp.metrics.setPods(len(pp.addresses.Addresses))
+	pp.metrics.setExists(true)
+}
+
+func (pp *portPublisher) addEndpointSlice(slice *discovery.EndpointSlice) {
+	newAddressSet := pp.endpointSliceToAddresses(slice)
+	for id, addr := range pp.addresses.Addresses {
+		newAddressSet.Addresses[id] = addr
+	}
+
+	add, _ := diffAddresses(pp.addresses, newAddressSet)
+	if len(add.Addresses) > 0 {
+		for _, listener := range pp.listeners {
+			listener.Add(add)
+		}
+	}
+
 	pp.addresses = newAddressSet
+	pp.exists = true
+	pp.metrics.incUpdates()
+	pp.metrics.setPods(len(pp.addresses.Addresses))
+	pp.metrics.setExists(true)
+}
+
+func (pp *portPublisher) updateEndpointSlice(oldSlice *discovery.EndpointSlice, newSlice *discovery.EndpointSlice) {
+	updatedAddressSet := AddressSet{
+		Addresses: make(map[ID]Address),
+		Labels:    pp.addresses.Labels,
+	}
+
+	for id, address := range pp.addresses.Addresses {
+		updatedAddressSet.Addresses[id] = address
+	}
+
+	oldAddressSet := pp.endpointSliceToAddresses(oldSlice)
+	for id := range oldAddressSet.Addresses {
+		delete(updatedAddressSet.Addresses, id)
+	}
+
+	newAddressSet := pp.endpointSliceToAddresses(newSlice)
+	for id, address := range newAddressSet.Addresses {
+		updatedAddressSet.Addresses[id] = address
+	}
+
+	add, remove := diffAddresses(pp.addresses, updatedAddressSet)
+	for _, listener := range pp.listeners {
+		if len(remove.Addresses) > 0 {
+			listener.Remove(remove)
+		}
+		if len(add.Addresses) > 0 {
+			listener.Add(add)
+		}
+	}
 
+	pp.addresses = updatedAddressSet
+	pp.exists = true
 	pp.metrics.incUpdates()
 	pp.metrics.setPods(len(pp.addresses.Addresses))
 	pp.metrics.setExists(true)
 }
 
-func metricLabels(endpoints *corev1.Endpoints) map[string]string {
-	labels := map[string]string{service: endpoints.Name, namespace: endpoints.Namespace}
+func metricLabels(resource interface{}) map[string]string {
+	var serviceName, ns string
+	var resLabels, resAnnotations map[string]string
+	switch res := resource.(type) {
+	case *corev1.Endpoints:
+		{
+			serviceName, ns = res.Name, res.Namespace
+			resLabels, resAnnotations = res.Labels, res.Annotations
+		}
+	case *discovery.EndpointSlice:
+		{
+			serviceName, ns = res.Labels[discovery.LabelServiceName], res.Namespace
+			resLabels, resAnnotations = res.Labels, res.Annotations
+		}
+	}
+
+	labels := map[string]string{service: serviceName, namespace: ns}
 
-	gateway, hasRemoteGateway := endpoints.Labels[consts.RemoteGatewayNameLabel]
-	gatewayNs, hasRemoteGatwayNs := endpoints.Labels[consts.RemoteGatewayNsLabel]
-	remoteClusterName, hasRemoteClusterName := endpoints.Labels[consts.RemoteClusterNameLabel]
-	serviceFqn, hasServiceFqn := endpoints.Annotations[consts.RemoteServiceFqName]
+	gateway, hasRemoteGateway := resLabels[pkgK8s.RemoteGatewayNameLabel]
+	gatewayNs, hasRemoteGatwayNs := resLabels[pkgK8s.RemoteGatewayNsLabel]
+	remoteClusterName, hasRemoteClusterName := resLabels[pkgK8s.RemoteClusterNameLabel]
+	serviceFqn, hasServiceFqn := resAnnotations[pkgK8s.RemoteServiceFqName]
 
 	if hasRemoteGateway && hasRemoteGatwayNs && hasRemoteClusterName && hasServiceFqn {
 		// this means we are looking at Endpoints created for the purpose of mirroring
@@ -483,6 +688,61 @@ func metricLabels(endpoints *corev1.Endpoints) map[string]string {
 	return labels
 }
 
+func (pp *portPublisher) endpointSliceToAddresses(es *discovery.EndpointSlice) AddressSet {
+	addresses := make(map[ID]Address)
+	resolvedPort := pp.resolveESTargetPort(es.Ports)
+	if resolvedPort == Port(0) {
+		return AddressSet{}
+	}
+	serviceID, err := getEndpointSliceServiceID(es)
+	if err != nil {
+		pp.log.Errorf("Could not fetch resource service name:%v", err)
+	}
+
+	for _, endpoint := range es.Endpoints {
+		if endpoint.Hostname != nil {
+			if pp.hostname != "" && pp.hostname != *endpoint.Hostname {
+				continue
+			}
+		}
+		if endpoint.Conditions.Ready != nil && !*endpoint.Conditions.Ready {
+			continue
+		}
+
+		if endpoint.TargetRef == nil {
+			for _, IPAddr := range endpoint.Addresses {
+				var authorityOverride string
+				if fqName, ok := es.Annotations[pkgK8s.RemoteServiceFqName]; ok {
+					authorityOverride = fmt.Sprintf("%s:%d", fqName, pp.srcPort)
+				}
+
+				identity := es.Annotations[pkgK8s.RemoteGatewayIdentity]
+				address, id := pp.newServiceRefAddress(resolvedPort, IPAddr, serviceID.Name, es.Namespace)
+				address.Identity, address.AuthorityOverride = authorityOverride, identity
+				addresses[id] = address
+			}
+
+			continue
+		}
+
+		if endpoint.TargetRef.Kind == "Pod" {
+			for _, IPAddr := range endpoint.Addresses {
+				address, id, err := pp.newPodRefAddress(resolvedPort, IPAddr, endpoint.TargetRef.Name, endpoint.TargetRef.Namespace)
+				if err != nil {
+					pp.log.Errorf("Unable to create new address:%v", err)
+					continue
+				}
+				addresses[id] = address
+			}
+		}
+
+	}
+	return AddressSet{
+		Addresses: addresses,
+		Labels:    metricLabels(es),
+	}
+}
+
 func (pp *portPublisher) endpointsToAddresses(endpoints *corev1.Endpoints) AddressSet {
 	addresses := make(map[ID]Address)
 	for _, subset := range endpoints.Subsets {
@@ -491,47 +751,28 @@ func (pp *portPublisher) endpointsToAddresses(endpoints *corev1.Endpoints) Addre
 			if pp.hostname != "" && pp.hostname != endpoint.Hostname {
 				continue
 			}
-			if endpoint.TargetRef == nil {
-				id := ServiceID{
-					Name: strings.Join([]string{
-						endpoints.ObjectMeta.Name,
-						endpoint.IP,
-						fmt.Sprint(resolvedPort),
-					}, "-"),
-					Namespace: endpoints.ObjectMeta.Namespace,
-				}
 
+			if endpoint.TargetRef == nil {
 				var authorityOverride string
-				if fqName, ok := endpoints.Annotations[consts.RemoteServiceFqName]; ok {
+				if fqName, ok := endpoints.Annotations[pkgK8s.RemoteServiceFqName]; ok {
 					authorityOverride = fmt.Sprintf("%s:%d", fqName, pp.srcPort)
 				}
 
-				addresses[id] = Address{
-					IP:                endpoint.IP,
-					Port:              resolvedPort,
-					Identity:          endpoints.Annotations[consts.RemoteGatewayIdentity],
-					AuthorityOverride: authorityOverride,
-				}
+				identity := endpoints.Annotations[pkgK8s.RemoteGatewayIdentity]
+				address, id := pp.newServiceRefAddress(resolvedPort, endpoint.IP, endpoints.Name, endpoints.Namespace)
+				address.Identity, address.AuthorityOverride = identity, authorityOverride
+
+				addresses[id] = address
 				continue
 			}
+
 			if endpoint.TargetRef.Kind == "Pod" {
-				id := PodID{
-					Name:      endpoint.TargetRef.Name,
-					Namespace: endpoint.TargetRef.Namespace,
-				}
-				pod, err := pp.k8sAPI.Pod().Lister().Pods(id.Namespace).Get(id.Name)
+				address, id, err := pp.newPodRefAddress(resolvedPort, endpoint.IP, endpoint.TargetRef.Name, endpoint.TargetRef.Namespace)
 				if err != nil {
-					pp.log.Errorf("Unable to fetch pod %v: %s", id, err)
+					pp.log.Errorf("Unable to create new address:%v", err)
 					continue
 				}
-				ownerKind, ownerName := pp.k8sAPI.GetOwnerKindAndName(pod, false)
-				addresses[id] = Address{
-					IP:        endpoint.IP,
-					Port:      resolvedPort,
-					Pod:       pod,
-					OwnerName: ownerName,
-					OwnerKind: ownerKind,
-				}
+				addresses[id] = address
 			}
 		}
 	}
@@ -541,6 +782,58 @@ func (pp *portPublisher) endpointsToAddresses(endpoints *corev1.Endpoints) Addre
 	}
 }
 
+func (pp *portPublisher) newServiceRefAddress(endpointPort Port, endpointIP, serviceName, serviceNamespace string) (Address, ServiceID) {
+	id := ServiceID{
+		Name: strings.Join([]string{
+			serviceName,
+			endpointIP,
+			fmt.Sprint(endpointPort),
+		}, "-"),
+		Namespace: serviceNamespace,
+	}
+
+	return Address{IP: endpointIP, Port: endpointPort}, id
+}
+
+func (pp *portPublisher) newPodRefAddress(endpointPort Port, endpointIP, podName, podNamespace string) (Address, PodID, error) {
+	id := PodID{
+		Name:      podName,
+		Namespace: podNamespace,
+	}
+	pod, err := pp.k8sAPI.Pod().Lister().Pods(id.Namespace).Get(id.Name)
+	if err != nil {
+		return Address{}, PodID{}, fmt.Errorf("unable to fetch pod %v:%v", id, err)
+	}
+	ownerKind, ownerName := pp.k8sAPI.GetOwnerKindAndName(pod, false)
+	addr := Address{
+		IP:        endpointIP,
+		Port:      endpointPort,
+		Pod:       pod,
+		OwnerName: ownerName,
+		OwnerKind: ownerKind,
+	}
+
+	return addr, id, nil
+}
+
+func (pp *portPublisher) resolveESTargetPort(slicePorts []discovery.EndpointPort) Port {
+	if slicePorts == nil {
+		return Port(0)
+	}
+
+	switch pp.targetPort.Type {
+	case intstr.Int:
+		return Port(pp.targetPort.IntVal)
+	case intstr.String:
+		for _, p := range slicePorts {
+			if *p.Name == pp.targetPort.StrVal {
+				return Port(*p.Port)
+			}
+		}
+	}
+	return Port(0)
+}
+
 func (pp *portPublisher) resolveTargetPort(subset corev1.EndpointSubset) Port {
 	switch pp.targetPort.Type {
 	case intstr.Int:
@@ -557,12 +850,42 @@ func (pp *portPublisher) resolveTargetPort(subset corev1.EndpointSubset) Port {
 
 func (pp *portPublisher) updatePort(targetPort namedPort) {
 	pp.targetPort = targetPort
-	endpoints, err := pp.k8sAPI.Endpoint().Lister().Endpoints(pp.id.Namespace).Get(pp.id.Name)
-	if err == nil {
-		pp.updateEndpoints(endpoints)
+
+	if !pkgK8s.EndpointSliceAccess(pp.k8sAPI.Client) {
+		endpoints, err := pp.k8sAPI.Endpoint().Lister().Endpoints(pp.id.Namespace).Get(pp.id.Name)
+		if err == nil {
+			pp.updateEndpoints(endpoints)
+		} else {
+			pp.log.Errorf("Unable to get endpoints during port update: %s", err)
+		}
 	} else {
-		pp.log.Errorf("Unable to get endpoints during port update: %s", err)
+		matchLabels := map[string]string{discovery.LabelServiceName: pp.id.Name}
+		selector := k8slabels.Set(matchLabels).AsSelector()
+
+		endpointSlices, err := pp.k8sAPI.ES().Lister().EndpointSlices(pp.id.Namespace).List(selector)
+		if err == nil {
+			pp.addresses = AddressSet{}
+			for _, slice := range endpointSlices {
+				pp.addEndpointSlice(slice)
+			}
+		} else {
+			pp.log.Errorf("Unable to get EndpointSlices during port update: %s", err)
+		}
+	}
+}
+
+func (pp *portPublisher) deleteEndpointSlice(es *discovery.EndpointSlice) {
+	addrSet := pp.endpointSliceToAddresses(es)
+	for id := range addrSet.Addresses {
+		delete(pp.addresses.Addresses, id)
 	}
+
+	for _, listener := range pp.listeners {
+		listener.Remove(addrSet)
+	}
+
+	svcExists := len(pp.addresses.Addresses) > 0
+	pp.noEndpoints(svcExists)
 }
 
 func (pp *portPublisher) noEndpoints(exists bool) {
@@ -627,6 +950,7 @@ func getTargetPort(service *corev1.Service, port Port) namedPort {
 	// port spec's name as the target port
 	for _, portSpec := range service.Spec.Ports {
 		if portSpec.Port == int32(port) {
+
 			return intstr.FromString(portSpec.Name)
 		}
 	}
@@ -655,16 +979,16 @@ func diffAddresses(oldAddresses, newAddresses AddressSet) (add, remove AddressSe
 	// TODO: this detects pods which have been added or removed, but does not
 	// detect addresses which have been modified.  A modified address should trigger
 	// an add of the new version.
-	addAddesses := make(map[ID]Address)
+	addAddresses := make(map[ID]Address)
 	removeAddresses := make(map[ID]Address)
 	for id, newAddress := range newAddresses.Addresses {
 		if oldAddress, ok := oldAddresses.Addresses[id]; ok {
 			if addressChanged(oldAddress, newAddress) {
-				addAddesses[id] = newAddress
+				addAddresses[id] = newAddress
 			}
 		} else {
 			// this is a new address, we need to add it
-			addAddesses[id] = newAddress
+			addAddresses[id] = newAddress
 		}
 	}
 	for id, address := range oldAddresses.Addresses {
@@ -673,7 +997,7 @@ func diffAddresses(oldAddresses, newAddresses AddressSet) (add, remove AddressSe
 		}
 	}
 	add = AddressSet{
-		Addresses: addAddesses,
+		Addresses: addAddresses,
 		Labels:    newAddresses.Labels,
 	}
 	remove = AddressSet{
@@ -681,3 +1005,32 @@ func diffAddresses(oldAddresses, newAddresses AddressSet) (add, remove AddressSe
 	}
 	return
 }
+
+func getEndpointSliceServiceID(es *discovery.EndpointSlice) (ServiceID, error) {
+	if !isValidSlice(es) {
+		return ServiceID{}, fmt.Errorf("EndpointSlice [%s/%s] is invalid", es.Namespace, es.Name)
+	}
+
+	if svc, ok := es.Labels[discovery.LabelServiceName]; ok {
+		return ServiceID{es.Namespace, svc}, nil
+	}
+
+	for _, ref := range es.OwnerReferences {
+		if ref.Kind == "Service" && ref.Name != "" {
+			return ServiceID{es.Namespace, ref.Name}, nil
+		}
+	}
+
+	return ServiceID{}, fmt.Errorf("EndpointSlice [%s/%s] is invalid", es.Namespace, es.Name)
+}
+
+func isValidSlice(es *discovery.EndpointSlice) bool {
+	serviceName, ok := es.Labels[discovery.LabelServiceName]
+	if !ok && len(es.OwnerReferences) == 0 {
+		return false
+	} else if len(es.OwnerReferences) == 0 && serviceName == "" {
+		return false
+	}
+
+	return true
+}
diff --git a/controller/cmd/destination/main.go b/controller/cmd/destination/main.go
index 5dbc3d1fe..cb7d0db4f 100644
--- a/controller/cmd/destination/main.go
+++ b/controller/cmd/destination/main.go
@@ -37,7 +37,7 @@ func Main(args []string) {
 
 	k8sAPI, err := k8s.InitializeAPI(
 		*kubeConfigPath, true,
-		k8s.Endpoint, k8s.Pod, k8s.RS, k8s.Svc, k8s.SP, k8s.TS, k8s.Job,
+		k8s.Endpoint, k8s.ES, k8s.Pod, k8s.RS, k8s.Svc, k8s.SP, k8s.TS, k8s.Job,
 	)
 	if err != nil {
 		log.Fatalf("Failed to initialize K8s API: %s", err)
diff --git a/controller/proxy-injector/webhook_ops.go b/controller/proxy-injector/webhook_ops.go
deleted file mode 100644
index e2e5b932f..000000000
--- a/controller/proxy-injector/webhook_ops.go
+++ /dev/null
@@ -1,53 +0,0 @@
-package injector
-
-import (
-	"bytes"
-
-	k8sPkg "github.com/linkerd/linkerd2/pkg/k8s"
-	log "github.com/sirupsen/logrus"
-	arv1beta1 "k8s.io/api/admissionregistration/v1beta1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	clientArv1beta1 "k8s.io/client-go/kubernetes/typed/admissionregistration/v1beta1"
-	"sigs.k8s.io/yaml"
-)
-
-// Ops satisfies the ConfigOps interface for managing MutatingWebhook configs
-type Ops struct{}
-
-// Create persists the Mutating webhook config and returns its SelfLink
-func (*Ops) Create(
-	client clientArv1beta1.AdmissionregistrationV1beta1Interface,
-	buf *bytes.Buffer,
-) (string, error) {
-	var config arv1beta1.MutatingWebhookConfiguration
-	if err := yaml.Unmarshal(buf.Bytes(), &config); err != nil {
-		log.Infof("failed to unmarshal mutating webhook configuration: %s\n%s\n", err, buf.String())
-		return "", err
-	}
-
-	obj, err := client.MutatingWebhookConfigurations().Create(&config)
-	if err != nil {
-		return "", err
-	}
-	return obj.ObjectMeta.SelfLink, nil
-}
-
-// Exists returns an error if the Mutating webhook doesn't exist
-func (o *Ops) Exists(client clientArv1beta1.AdmissionregistrationV1beta1Interface) error {
-	_, err := client.
-		MutatingWebhookConfigurations().
-		Get(o.Name(), metav1.GetOptions{})
-	return err
-}
-
-// Delete removes the Mutating webhook from the cluster
-func (o *Ops) Delete(client clientArv1beta1.AdmissionregistrationV1beta1Interface) error {
-	return client.
-		MutatingWebhookConfigurations().
-		Delete(o.Name(), &metav1.DeleteOptions{})
-}
-
-// Name returns name for this webhook configuration resource
-func (*Ops) Name() string {
-	return k8sPkg.ProxyInjectorWebhookConfigName
-}
diff --git a/controller/sp-validator/webhook_ops.go b/controller/sp-validator/webhook_ops.go
deleted file mode 100644
index f584a5805..000000000
--- a/controller/sp-validator/webhook_ops.go
+++ /dev/null
@@ -1,53 +0,0 @@
-package validator
-
-import (
-	"bytes"
-
-	k8sPkg "github.com/linkerd/linkerd2/pkg/k8s"
-	log "github.com/sirupsen/logrus"
-	arv1beta1 "k8s.io/api/admissionregistration/v1beta1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	clientArv1beta1 "k8s.io/client-go/kubernetes/typed/admissionregistration/v1beta1"
-	"sigs.k8s.io/yaml"
-)
-
-// Ops satisfies the ConfigOps interface for managing ValidatingWebhook configs
-type Ops struct{}
-
-// Create persists the Validating webhook config and returns its SelfLink
-func (*Ops) Create(
-	client clientArv1beta1.AdmissionregistrationV1beta1Interface,
-	buf *bytes.Buffer,
-) (string, error) {
-	var config arv1beta1.ValidatingWebhookConfiguration
-	if err := yaml.Unmarshal(buf.Bytes(), &config); err != nil {
-		log.Infof("failed to unmarshal validating webhook configuration: %s\n%s\n", err, buf.String())
-		return "", err
-	}
-
-	obj, err := client.ValidatingWebhookConfigurations().Create(&config)
-	if err != nil {
-		return "", err
-	}
-	return obj.ObjectMeta.SelfLink, nil
-}
-
-// Exists returns an error if the Validating webhook doesn't exist
-func (o *Ops) Exists(client clientArv1beta1.AdmissionregistrationV1beta1Interface) error {
-	_, err := client.
-		ValidatingWebhookConfigurations().
-		Get(o.Name(), metav1.GetOptions{})
-	return err
-}
-
-// Delete removes the Validating webhook from the cluster
-func (o *Ops) Delete(client clientArv1beta1.AdmissionregistrationV1beta1Interface) error {
-	return client.
-		ValidatingWebhookConfigurations().
-		Delete(o.Name(), &metav1.DeleteOptions{})
-}
-
-// Name returns name for this webhook configuration resource
-func (*Ops) Name() string {
-	return k8sPkg.SPValidatorWebhookConfigName
-}
diff --git a/grafana/dashboards/prometheus-benchmark.json b/grafana/dashboards/prometheus-benchmark.json
index c4911aeb5..22936304f 100644
--- a/grafana/dashboards/prometheus-benchmark.json
+++ b/grafana/dashboards/prometheus-benchmark.json
@@ -3261,7 +3261,7 @@
       "timeFrom": null,
       "timeRegions": [],
       "timeShift": null,
-      "title": "Rule group evaulation problems/s",
+      "title": "Rule group evaluation problems/s",
       "tooltip": {
         "msResolution": false,
         "shared": true,
diff --git a/pkg/charts/charts.go b/pkg/charts/charts.go
index ab8411d33..984ac1c26 100644
--- a/pkg/charts/charts.go
+++ b/pkg/charts/charts.go
@@ -84,6 +84,7 @@ func (chart *Chart) Render() (bytes.Buffer, error) {
 		{Name: "charts/partials/templates/_capabilities.tpl"},
 		{Name: "charts/partials/templates/_trace.tpl"},
 		{Name: "charts/partials/templates/_nodeselector.tpl"},
+		{Name: "charts/partials/templates/_tolerations.tpl"},
 		{Name: "charts/partials/templates/_affinity.tpl"},
 		{Name: "charts/partials/templates/_addons.tpl"},
 		{Name: "charts/partials/templates/_validate.tpl"},
diff --git a/pkg/charts/linkerd2/values.go b/pkg/charts/linkerd2/values.go
index 20dbcc7e8..094efdb25 100644
--- a/pkg/charts/linkerd2/values.go
+++ b/pkg/charts/linkerd2/values.go
@@ -48,6 +48,7 @@ type (
 		ProfileValidator              *ProfileValidator              `json:"profileValidator"`
 		Tap                           *Tap                           `json:"tap"`
 		NodeSelector                  map[string]string              `json:"nodeSelector"`
+		Tolerations                   []interface{}                  `json:"tolerations"`
 		SMIMetrics                    *SMIMetrics                    `json:"smiMetrics"`
 
 		DestinationResources   *Resources `json:"destinationResources"`
diff --git a/pkg/healthcheck/healthcheck.go b/pkg/healthcheck/healthcheck.go
index d7eec5ef4..40072bbf1 100644
--- a/pkg/healthcheck/healthcheck.go
+++ b/pkg/healthcheck/healthcheck.go
@@ -1445,7 +1445,7 @@ func (hc *HealthChecker) checkCertificatesConfig() (*tls.Cred, []*x509.Certifica
 		data, err = issuercerts.FetchIssuerData(hc.kubeAPI, idctx.TrustAnchorsPem, hc.ControlPlaneNamespace)
 	} else {
 		data, err = issuercerts.FetchExternalIssuerData(hc.kubeAPI, hc.ControlPlaneNamespace)
-		// ensure trust anchors in config matches whats in the secret
+		// ensure trust anchors in config matches what's in the secret
 		if data != nil && strings.TrimSpace(idctx.TrustAnchorsPem) != strings.TrimSpace(data.TrustAnchors) {
 			errFormat := "IdentityContext.TrustAnchorsPem does not match %s in %s"
 			err = fmt.Errorf(errFormat, k8s.IdentityIssuerTrustAnchorsNameExternal, k8s.IdentityIssuerSecretName)
diff --git a/pkg/inject/inject.go b/pkg/inject/inject.go
index 2b0e7bbda..ca4537fbf 100644
--- a/pkg/inject/inject.go
+++ b/pkg/inject/inject.go
@@ -620,7 +620,7 @@ func (conf *ResourceConfig) injectProxyInit(values *patch) {
 }
 
 func (conf *ResourceConfig) serviceAccountVolumeMount() *corev1.VolumeMount {
-	// Probably always true, but wanna be super-safe
+	// Probably always true, but want to be super-safe
 	if containers := conf.pod.spec.Containers; len(containers) > 0 {
 		for _, vm := range containers[0].VolumeMounts {
 			if vm.MountPath == k8s.MountPathServiceAccount {
diff --git a/pkg/k8s/labels.go b/pkg/k8s/labels.go
index cf0a95124..82d472514 100644
--- a/pkg/k8s/labels.go
+++ b/pkg/k8s/labels.go
@@ -132,7 +132,7 @@ const (
 	// config.
 	ProxyInitImageAnnotation = ProxyConfigAnnotationsPrefix + "/init-image"
 
-	// ProxyInitImageVersionAnnotation can be used to overrided the proxy-init image version
+	// ProxyInitImageVersionAnnotation can be used to override the proxy-init image version
 	ProxyInitImageVersionAnnotation = ProxyConfigAnnotationsPrefix + "/init-image-version"
 
 	// DebugImageAnnotation can be used to override the debugImage config.
diff --git a/proto/public.proto b/proto/public.proto
index 916f9f0af..9da357862 100644
--- a/proto/public.proto
+++ b/proto/public.proto
@@ -335,7 +335,7 @@ message ResourceSelection {
   // A string-formatted Kubernetes label selector as passed to `kubectl get
   // --selector`.
   //
-  // XXX in the future this may be superceded by a data structure that more
+  // XXX in the future this may be superseded by a data structure that more
   // richly describes a parsed label selector.
   string label_selector = 2;
 }
@@ -486,7 +486,7 @@ message RouteTable {
 
 message GatewaysTable {
   repeated Row rows = 1;
-  
+
   message Row {
     string namespace = 1;
     string name = 2;
@@ -520,7 +520,7 @@ service Api {
   rpc StatSummary(StatSummaryRequest) returns (StatSummaryResponse) {}
 
   rpc Edges(EdgesRequest) returns (EdgesResponse) {}
-  
+
   rpc Gateways(GatewaysRequest) returns (GatewaysResponse) {}
 
   rpc TopRoutes(TopRoutesRequest) returns (TopRoutesResponse) {}
@@ -529,10 +529,10 @@ service Api {
 
   rpc ListServices(ListServicesRequest) returns (ListServicesResponse) {}
 
-  // Superceded by `TapByResource`.
+  // Superseded by `TapByResource`.
   rpc Tap(TapRequest) returns (stream TapEvent) { option deprecated = true; }
 
-  // Superceded by tap APIServer.
+  // Superseded by tap APIServer.
   rpc TapByResource(TapByResourceRequest) returns (stream TapEvent) { option deprecated = true; }
 
   rpc Version(Empty) returns (VersionInfo) {}
diff --git a/web/app/js/components/ResourceDetail.jsx b/web/app/js/components/ResourceDetail.jsx
index d868fde6c..24fe481a4 100644
--- a/web/app/js/components/ResourceDetail.jsx
+++ b/web/app/js/components/ResourceDetail.jsx
@@ -69,7 +69,7 @@ export class ResourceDetailBase extends React.Component {
       resourceMetrics: [],
       podMetrics: [], // metrics for all pods whose owner is this resource
       upstreamMetrics: {}, // metrics for resources who send traffic to this resource
-      downstreamMetrics: {}, // metrics for resources who this resouce sends traffic to
+      downstreamMetrics: {}, // metrics for resources who this resource sends traffic to
       unmeshedSources: {},
       resourceIsMeshed: true,
       pendingRequests: false,


Test patch:
diff --git a/TEST.md b/TEST.md
index 012086c71..6492d3447 100644
--- a/TEST.md
+++ b/TEST.md
@@ -186,7 +186,7 @@ available to the cluster
 
 We use [WebdriverIO](https://webdriver.io/) to test how the web dashboard looks
 and operates locally in Chrome. For cross-browser testing, we use
-[SauceLabs](https://saucelabs.com/), which runs simulataneous tests on different
+[SauceLabs](https://saucelabs.com/), which runs simultaneous tests on different
 browsers in the cloud.
 
 If you're new to the repo, make sure you've installed web dependencies via
@@ -292,7 +292,7 @@ godoc github.com/linkerd/linkerd2/testutil | less
 The scale tests deploy a single Linkerd control-plane, and then scale up
 multiple sample apps across multiple replicas across multiple namespaces.
 
-Prequisites:
+Prerequisites:
 
 - a `linkerd` CLI binary
 - Linkerd Docker images associated with the `linkerd` CLI binary
diff --git a/bin/_test-helpers.sh b/bin/_test-helpers.sh
index e7c27c31e..654ebd995 100644
--- a/bin/_test-helpers.sh
+++ b/bin/_test-helpers.sh
@@ -7,7 +7,7 @@ set +e
 ##### Test setup helpers #####
 
 export default_test_names=(deep external-issuer helm helm-upgrade uninstall upgrade-edge upgrade-stable)
-export all_test_names=(custom-domain "${default_test_names[*]}")
+export all_test_names=(cluster-domain "${default_test_names[*]}")
 
 handle_input() {
   export images=''
@@ -23,7 +23,7 @@ handle_input() {
 
 Optionally specify a test with the --name flag: [${all_test_names[*]}]
 
-Note: The custom-domain test requires a cluster configuration with a custom cluster domain (see test/configs/cluster-domain.yaml)
+Note: The cluster-domain test requires a cluster configuration with a custom cluster domain (see test/configs/cluster-domain.yaml)
 
 Usage:
     ${0##*/} [--images] [--images-host ssh://linkerd-docker] [--name test-name] [--skip-kind-create] /path/to/linkerd
@@ -39,11 +39,11 @@ Examples:
     ${0##*/} --skip-kind-create /path/to/linkerd
 
     # Load images from tar files located under the 'image-archives' directory
-    # Note: This is primarly for CI
+    # Note: This is primarily for CI
     ${0##*/} --images /path/to/linkerd
 
     # Retrieve images from a remote docker instance and then load them into KinD
-    # Note: This is primarly for CI
+    # Note: This is primarily for CI
     ${0##*/} --images --images-host ssh://linkerd-docker /path/to/linkerd
 
 Available Commands:
@@ -140,6 +140,7 @@ check_cluster() {
 delete_cluster() {
   local name=$1
   "$bindir"/kind delete cluster --name "$name" 2>&1
+  exit_on_err 'error deleting cluster'
 }
 
 cleanup_cluster() {
@@ -183,6 +184,8 @@ start_test() {
   fi
   check_cluster
   run_"$name"_test
+  exit_on_err "error calling 'run_${name}_test'"
+
   if [ -z "$skip_kind_create" ]; then
     delete_cluster "$name"
   else
diff --git a/bin/test-scale b/bin/test-scale
index 8746b2b8b..40fc40b59 100755
--- a/bin/test-scale
+++ b/bin/test-scale
@@ -144,7 +144,7 @@ for ((i=1; i <= NAMESPACES; i++)); do
 done
 
 #
-# Lifecyle / bb
+# Lifecycle / bb
 #
 
 LIFECYCLE=$(curl -s https://raw.githubusercontent.com/linkerd/linkerd-examples/master/lifecycle/lifecycle.yml)
diff --git a/cli/cmd/get_test.go b/cli/cmd/get_test.go
index abee07cda..46b401ded 100644
--- a/cli/cmd/get_test.go
+++ b/cli/cmd/get_test.go
@@ -58,7 +58,7 @@ func TestGetPods(t *testing.T) {
 		}
 	})
 
-	t.Run("Returns error if cant find pods in API", func(t *testing.T) {
+	t.Run("Returns error if can't find pods in API", func(t *testing.T) {
 		mockClient := &public.MockAPIClient{}
 		mockClient.ErrorToReturn = errors.New("expected")
 
diff --git a/cli/cmd/install_helm_test.go b/cli/cmd/install_helm_test.go
index 90925655e..dc248acfc 100644
--- a/cli/cmd/install_helm_test.go
+++ b/cli/cmd/install_helm_test.go
@@ -6,6 +6,7 @@ import (
 	"path/filepath"
 	"testing"
 
+	"github.com/linkerd/linkerd2/pkg/charts"
 	l5dcharts "github.com/linkerd/linkerd2/pkg/charts/linkerd2"
 	"github.com/linkerd/linkerd2/pkg/k8s"
 	"k8s.io/helm/pkg/chartutil"
@@ -179,6 +180,7 @@ func chartControlPlane(t *testing.T, ha bool, addOnConfig string, ignoreOutbound
 		"templates/_affinity.tpl",
 		"templates/_addons.tpl",
 		"templates/_nodeselector.tpl",
+		"templates/_tolerations.tpl",
 		"templates/_validate.tpl",
 	}
 
@@ -229,6 +231,7 @@ func chartControlPlane(t *testing.T, ha bool, addOnConfig string, ignoreOutbound
 }
 
 func buildAddOnChart(t *testing.T, addon l5dcharts.AddOn, chartPartials *pb.Chart) *pb.Chart {
+	rawValues := readValuesFile(t, filepath.Join("add-ons", addon.Name()))
 	addOnChart := pb.Chart{
 		Metadata: &pb.Metadata{
 			Name: addon.Name(),
@@ -239,6 +242,9 @@ func buildAddOnChart(t *testing.T, addon l5dcharts.AddOn, chartPartials *pb.Char
 		Dependencies: []*pb.Chart{
 			chartPartials,
 		},
+		Values: &pb.Config{
+			Raw: string(rawValues),
+		},
 	}
 
 	for _, filepath := range append(addon.ConfigStageTemplates(), addon.ControlPlaneStageTemplates()...) {
@@ -290,3 +296,17 @@ func readTestValues(t *testing.T, ha bool, ignoreOutboundPorts string, ignoreInb
 
 	return yaml.Marshal(values)
 }
+
+// readValues reads values.yaml file from the given path
+func readValuesFile(t *testing.T, path string) []byte {
+
+	valuesFiles := []*chartutil.BufferedFile{
+		{Name: chartutil.ValuesfileName},
+	}
+
+	if err := charts.FilesReader(path+"/", valuesFiles); err != nil {
+		t.Fatal("Unexpected error", err)
+	}
+
+	return valuesFiles[0].Data
+}
diff --git a/cli/cmd/install_test.go b/cli/cmd/install_test.go
index 8d223c9bc..c0019cb7c 100644
--- a/cli/cmd/install_test.go
+++ b/cli/cmd/install_test.go
@@ -66,6 +66,7 @@ func TestRender(t *testing.T) {
 		InstallNamespace:            true,
 		Identity:                    defaultValues.Identity,
 		NodeSelector:                defaultValues.NodeSelector,
+		Tolerations:                 defaultValues.Tolerations,
 		Global: &charts.Global{
 			Namespace:                "Namespace",
 			ClusterDomain:            "cluster.local",
diff --git a/controller/api/destination/watcher/endpoints_watcher_test.go b/controller/api/destination/watcher/endpoints_watcher_test.go
index 3d2a5bb9c..2d06ca759 100644
--- a/controller/api/destination/watcher/endpoints_watcher_test.go
+++ b/controller/api/destination/watcher/endpoints_watcher_test.go
@@ -7,11 +7,11 @@ import (
 	"testing"
 
 	"github.com/linkerd/linkerd2/controller/k8s"
-	corev1 "k8s.io/api/core/v1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-
 	consts "github.com/linkerd/linkerd2/pkg/k8s"
 	logging "github.com/sirupsen/logrus"
+	corev1 "k8s.io/api/core/v1"
+	dv1beta1 "k8s.io/api/discovery/v1beta1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
 type bufferingEndpointListener struct {
@@ -140,6 +140,8 @@ func (bel *bufferingEndpointListenerWithResVersion) Remove(set AddressSet) {
 
 func (bel *bufferingEndpointListenerWithResVersion) NoEndpoints(exists bool) {}
 
+//TODO: Uncomment EndpointSlice related test cases
+// once EndpointSlices are opt-in and supported
 func TestEndpointsWatcher(t *testing.T) {
 	for _, tt := range []struct {
 		serviceType                      string
@@ -555,6 +557,583 @@ status:
 			expectedNoEndpoints:              false,
 			expectedNoEndpointsServiceExists: false,
 		},
+		{
+			serviceType: "local services with EndpointSlice",
+			k8sConfigs: []string{`
+kind: APIResourceList
+apiVersion: v1
+groupVersion: discovery.k8s.io/v1beta1
+resources:
+  - name: endpointslices
+    singularName: endpointslice
+    namespaced: true
+    kind: EndpointSlice
+    verbs:
+      - delete
+      - deletecollection
+      - get
+      - list
+      - patch
+      - create
+      - update
+      - watch
+`, `
+apiVersion: v1
+kind: Service
+metadata:
+  name: name-1
+  namespace: ns
+spec:
+  type: LoadBalancer
+  ports:
+  - port: 8989`,
+				`
+addressType: IPv4
+apiVersion: discovery.k8s.io/v1beta1
+endpoints:
+- addresses:
+  - 172.17.0.12
+  conditions:
+    ready: true
+  targetRef:
+    kind: Pod
+    name: name-1-1
+    namespace: ns
+  topology:
+    kubernetes.io/hostname: node-1
+- addresses:
+  - 172.17.0.19
+  conditions:
+    ready: true
+  targetRef:
+    kind: Pod
+    name: name-1-2
+    namespace: ns
+  topology:
+    kubernetes.io/hostname: node-1
+- addresses:
+  - 172.17.0.20
+  conditions:
+    ready: true
+  targetRef:
+    kind: Pod
+    name: name-1-3
+    namespace: ns
+  topology:
+    kubernetes.io/hostname: node-2
+- addresses:
+  - 172.17.0.21
+  conditions:
+    ready: true
+  topology:
+    kubernetes.io/hostname: node-2
+kind: EndpointSlice
+metadata:
+  labels:
+    kubernetes.io/service-name: name-1
+  name: name-1-bhnqh
+  namespace: ns
+ports:
+- name: ""
+  port: 8989`,
+				`
+apiVersion: v1
+kind: Pod
+metadata:
+  name: name-1-1
+  namespace: ns
+  ownerReferences:
+  - kind: ReplicaSet
+    name: rs-1
+status:
+  phase: Running
+  podIP: 172.17.0.12`,
+				`
+apiVersion: v1
+kind: Pod
+metadata:
+  name: name-1-2
+  namespace: ns
+  ownerReferences:
+  - kind: ReplicaSet
+    name: rs-1
+status:
+  phase: Running
+  podIP: 172.17.0.19`,
+				`
+apiVersion: v1
+kind: Pod
+metadata:
+  name: name-1-3
+  namespace: ns
+  ownerReferences:
+  - kind: ReplicaSet
+    name: rs-1
+status:
+  phase: Running
+  podIP: 172.17.0.20`,
+			},
+			id:   ServiceID{Name: "name-1", Namespace: "ns"},
+			port: 8989,
+			// expectedAddresses: []string{
+			// 	"172.17.0.12:8989",
+			// 	"172.17.0.19:8989",
+			// 	"172.17.0.20:8989",
+			// 	"172.17.0.21:8989",
+			// },
+			expectedAddresses: []string{},
+			//expectedNoEndpoints:              false,
+			expectedNoEndpoints: true,
+			//expectedNoEndpointsServiceExists: false,
+			expectedNoEndpointsServiceExists: true,
+			expectedError:                    false,
+		},
+		{
+			serviceType: "local services with missing addresses and EndpointSlice",
+			k8sConfigs: []string{`
+kind: APIResourceList
+apiVersion: v1
+groupVersion: discovery.k8s.io/v1beta1
+resources:
+  - name: endpointslices
+    singularName: endpointslice
+    namespaced: true
+    kind: EndpointSlice
+    verbs:
+      - delete
+      - deletecollection
+      - get
+      - list
+      - patch
+      - create
+      - update
+      - watch
+`, `
+apiVersion: v1
+kind: Service
+metadata:
+  name: name-1
+  namespace: ns
+spec:
+  type: LoadBalancer
+  ports:
+  - port: 8989`, `
+addressType: IPv4
+apiVersion: discovery.k8s.io/v1beta1
+endpoints:
+- addresses:
+  - 172.17.0.23
+  conditions:
+    ready: true
+  targetRef:
+    kind: Pod
+    name: name-1-1
+    namespace: ns
+  topology:
+    kubernetes.io/hostname: node-1
+- addresses:
+  - 172.17.0.24
+  conditions:
+    ready: true
+  targetRef:
+    kind: Pod
+    name: name-1-2
+    namespace: ns
+  topology:
+    kubernetes.io/hostname: node-1
+- addresses:
+  - 172.17.0.25
+  conditions:
+    ready: true
+  targetRef:
+    kind: Pod
+    name: name-1-3
+    namespace: ns
+  topology:
+    kubernetes.io/hostname: node-2
+kind: EndpointSlice
+metadata:
+  labels:
+    kubernetes.io/service-name: name-1
+  name: name1-f5fad
+  namespace: ns
+ports:
+- name: ""
+  port: 8989`, `
+apiVersion: v1
+kind: Pod
+metadata:
+  name: name-1-3
+  namespace: ns
+  ownerReferences:
+  - kind: ReplicaSet
+    name: rs-1
+status:
+  podIP: 172.17.0.25
+  phase: Running`,
+			},
+			id:   ServiceID{Name: "name-1", Namespace: "ns"},
+			port: 8989,
+			//expectedAddresses:                []string{"172.17.0.25:8989"},
+			expectedAddresses: []string{},
+			//expectedNoEndpoints:              false,
+			expectedNoEndpoints: true,
+			//expectedNoEndpointsServiceExists: false,
+			expectedNoEndpointsServiceExists: true,
+			expectedError:                    false,
+		},
+		{
+			serviceType: "local services with no EndpointSlices",
+			k8sConfigs: []string{`
+kind: APIResourceList
+apiVersion: v1
+groupVersion: discovery.k8s.io/v1beta1
+resources:
+  - name: endpointslices
+    singularName: endpointslice
+    namespaced: true
+    kind: EndpointSlice
+    verbs:
+      - delete
+      - deletecollection
+      - get
+      - list
+      - patch
+      - create
+      - update
+      - watch
+`, `
+apiVersion: v1
+kind: Service
+metadata:
+  name: name-2
+  namespace: ns
+spec:
+  type: LoadBalancer
+  ports:
+  - port: 7979`,
+			},
+			id:                               ServiceID{Name: "name-2", Namespace: "ns"},
+			port:                             7979,
+			expectedAddresses:                []string{},
+			expectedNoEndpoints:              true,
+			expectedNoEndpointsServiceExists: true,
+			expectedError:                    false,
+		},
+		{
+			serviceType: "external name services with EndpointSlices",
+			k8sConfigs: []string{`
+kind: APIResourceList
+apiVersion: v1
+groupVersion: discovery.k8s.io/v1beta1
+resources:
+  - name: endpointslices
+    singularName: endpointslice
+    namespaced: true
+    kind: EndpointSlice
+    verbs:
+      - delete
+      - deletecollection
+      - get
+      - list
+      - patch
+      - create
+      - update
+      - watch
+`, `
+apiVersion: v1
+kind: Service
+metadata:
+  name: name-3-external-svc
+  namespace: ns
+spec:
+  type: ExternalName
+  externalName: foo`,
+			},
+			id:                               ServiceID{Name: "name-3-external-svc", Namespace: "ns"},
+			port:                             7777,
+			expectedAddresses:                []string{},
+			expectedNoEndpoints:              false,
+			expectedNoEndpointsServiceExists: false,
+			expectedError:                    true,
+		},
+		{
+			serviceType:                      "services that do not exist",
+			k8sConfigs:                       []string{},
+			id:                               ServiceID{Name: "name-4-inexistent-svc", Namespace: "ns"},
+			port:                             5555,
+			expectedAddresses:                []string{},
+			expectedNoEndpoints:              true,
+			expectedNoEndpointsServiceExists: false,
+			expectedError:                    false,
+		},
+		{
+			serviceType: "stateful sets with EndpointSlices",
+			k8sConfigs: []string{`
+kind: APIResourceList
+apiVersion: v1
+groupVersion: discovery.k8s.io/v1beta1
+resources:
+  - name: endpointslices
+    singularName: endpointslice
+    namespaced: true
+    kind: EndpointSlice
+    verbs:
+      - delete
+      - deletecollection
+      - get
+      - list
+      - patch
+      - create
+      - update
+      - watch
+`, `
+apiVersion: v1
+kind: Service
+metadata:
+  name: name-1
+  namespace: ns
+spec:
+  type: LoadBalancer
+  ports:
+  - port: 8989`, `
+addressType: IPv4
+apiVersion: discovery.k8s.io/v1beta1
+endpoints:
+- addresses:
+  - 172.17.0.12
+  conditions:
+    ready: true
+  hostname: name-1-1
+  targetRef:
+    kind: Pod
+    name: name-1-1
+    namespace: ns
+  topology:
+    kubernetes.io/hostname: node-1
+- addresses:
+  - 172.17.0.19
+  hostname: name-1-2
+  conditions:
+    ready: true
+  targetRef:
+    kind: Pod
+    name: name-1-2
+    namespace: ns
+  topology:
+    kubernetes.io/hostname: node-1
+- addresses:
+  - 172.17.0.20
+  hostname: name-1-3
+  conditions:
+    ready: true
+  targetRef:
+    kind: Pod
+    name: name-1-3
+    namespace: ns
+  topology:
+    kubernetes.io/hostname: node-2
+kind: EndpointSlice
+metadata:
+  labels:
+    kubernetes.io/service-name: name-1
+  name: name-1-f5fad
+  namespace: ns
+ports:
+- name: ""
+  port: 8989`, `
+apiVersion: v1
+kind: Pod
+metadata:
+  name: name-1-1
+  namespace: ns
+  ownerReferences:
+  - kind: ReplicaSet
+    name: rs-1
+status:
+  phase: Running
+  podIP: 172.17.0.12`,
+				`
+apiVersion: v1
+kind: Pod
+metadata:
+  name: name-1-2
+  namespace: ns
+  ownerReferences:
+  - kind: ReplicaSet
+    name: rs-1
+status:
+  phase: Running
+  podIP: 172.17.0.19`,
+				`
+apiVersion: v1
+kind: Pod
+metadata:
+  name: name-1-3
+  namespace: ns
+  ownerReferences:
+  - kind: ReplicaSet
+    name: rs-1
+status:
+  phase: Running
+  podIP: 172.17.0.20`,
+			},
+			id:       ServiceID{Name: "name-1", Namespace: "ns"},
+			hostname: "name-1-3",
+			port:     6000,
+			//expectedAddresses:                []string{"172.17.0.20:6000"},
+			expectedAddresses: []string{},
+			//expectedNoEndpoints:              false,
+			expectedNoEndpoints: true,
+			//expectedNoEndpointsServiceExists: false,
+			expectedNoEndpointsServiceExists: true,
+			expectedError:                    false,
+		},
+		{
+			serviceType: "service with EndpointSlice without labels",
+			k8sConfigs: []string{`
+kind: APIResourceList
+apiVersion: v1
+groupVersion: discovery.k8s.io/v1beta1
+resources:
+  - name: endpointslices
+    singularName: endpointslice
+    namespaced: true
+    kind: EndpointSlice
+    verbs:
+      - delete
+      - deletecollection
+      - get
+      - list
+      - patch
+      - create
+      - update
+      - watch
+`, `
+apiVersion: v1
+kind: Service
+metadata:
+  name: name-5
+  namespace: ns
+spec:
+  type: LoadBalancer
+  ports:
+  - port: 8989`, `
+addressType: IPv4
+apiVersion: discovery.k8s.io/v1beta1
+endpoints:
+- addresses:
+  - 172.17.0.12
+  conditions:
+    ready: true
+  hostname: name-1-1
+  targetRef:
+    kind: Pod
+    name: name-1-1
+    namespace: ns
+  topology:
+    kubernetes.io/hostname: node-1
+kind: EndpointSlice
+metadata:
+  labels:
+  name: name-1-f5fad
+  namespace: ns
+ports:
+- name: ""
+  port: 8989`, `
+apiVersion: v1
+kind: Pod
+metadata:
+  name: name-1-1
+  namespace: ns
+  ownerReferences:
+  - kind: ReplicaSet
+    name: rs-1
+status:
+  phase: Running
+  podIP: 172.17.0.12`,
+			},
+			id:                               ServiceID{Name: "name-5", Namespace: "ns"},
+			port:                             8989,
+			expectedAddresses:                []string{},
+			expectedNoEndpoints:              true,
+			expectedNoEndpointsServiceExists: true,
+			expectedError:                    false,
+		},
+		{
+			serviceType: "service with IPv6 address type EndpointSlice",
+			k8sConfigs: []string{`
+kind: APIResourceList
+apiVersion: v1
+groupVersion: discovery.k8s.io/v1beta1
+resources:
+  - name: endpointslices
+    singularName: endpointslice
+    namespaced: true
+    kind: EndpointSlice
+    verbs:
+      - delete
+      - deletecollection
+      - get
+      - list
+      - patch
+      - create
+      - update
+      - watch
+`, `
+apiVersion: v1
+kind: Service
+metadata:
+  name: name-5
+  namespace: ns
+spec:
+  type: LoadBalancer
+  ports:
+  - port: 9000`, `
+addressType: IPv6
+apiVersion: discovery.k8s.io/v1beta1
+endpoints:
+- addresses:
+  - 0:0:0:0:0:0:0:1
+  conditions:
+    ready: true
+  targetRef:
+    kind: Pod
+    name: name-5-1
+    namespace: ns
+  topology:
+    kubernetes.io/hostname: node-1
+kind: EndpointSlice
+metadata:
+  labels:
+  name: name-5-f65dv
+  namespace: ns
+  ownerReferences:
+  - apiVersion: v1
+    kind: Service
+    name: name-5
+ports:
+- name: ""
+  port: 9000`, `
+apiVersion: v1
+kind: Pod
+metadata:
+  name: name-5-1
+  namespace: ns
+  ownerReferences:
+  - kind: ReplicaSet
+    name: rs-1
+status:
+  phase: Running
+  podIP: 0:0:0:0:0:0:0:1`,
+			},
+			id:                               ServiceID{Name: "name-5", Namespace: "ns"},
+			port:                             9000,
+			expectedAddresses:                []string{},
+			expectedNoEndpoints:              true,
+			expectedNoEndpointsServiceExists: true,
+			expectedError:                    false,
+		},
 	} {
 		tt := tt // pin
 		t.Run("subscribes listener to "+tt.serviceType, func(t *testing.T) {
@@ -621,6 +1200,65 @@ subsets:
 		`
 apiVersion: v1
 kind: Pod
+metadata:
+  name: name1-1
+  namespace: ns
+status:
+  phase: Running
+  podIP: 172.17.0.12`}
+
+	k8sConfigsWithES := []string{`
+kind: APIResourceList
+apiVersion: v1
+groupVersion: discovery.k8s.io/v1beta1
+resources:
+  - name: endpointslices
+    singularName: endpointslice
+    namespaced: true
+    kind: EndpointSlice
+    verbs:
+      - delete
+      - deletecollection
+      - get
+      - list
+      - patch
+      - create
+      - update
+      - watch
+`, `
+apiVersion: v1
+kind: Service
+metadata:
+  name: name1
+  namespace: ns
+spec:
+  type: LoadBalancer
+  ports:
+  - port: 8989`, `
+addressType: IPv4
+apiVersion: discovery.k8s.io/v1beta1
+endpoints:
+- addresses:
+  - 172.17.0.12
+  conditions:
+    ready: true
+  targetRef:
+    kind: Pod
+    name: name1-1
+    namespace: ns
+  topology:
+    kubernetes.io/hostname: node-1
+kind: EndpointSlice
+metadata:
+  labels:
+    kubernetes.io/service-name: name1
+  name: name1-del
+  namespace: ns
+ports:
+- name: ""
+  port: 8989`, `
+apiVersion: v1
+kind: Pod
 metadata:
   name: name1-1
   namespace: ns
@@ -636,6 +1274,7 @@ status:
 		port             Port
 		objectToDelete   interface{}
 		deletingServices bool
+		hasSliceAccess   bool
 	}{
 		{
 			serviceType:    "can delete endpoints",
@@ -671,6 +1310,24 @@ status:
 			objectToDelete:   &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: "name1", Namespace: "ns"}},
 			deletingServices: true,
 		},
+		{
+			serviceType:    "can delete EndpointSlices",
+			k8sConfigs:     k8sConfigsWithES,
+			id:             ServiceID{Name: "name1", Namespace: "ns"},
+			port:           8989,
+			hostname:       "name1-1",
+			objectToDelete: createTestEndpointSlice(),
+			hasSliceAccess: true,
+		},
+		{
+			serviceType:    "can delete EndpointSlices when wrapped in a DeletedFinalStateUnknown",
+			k8sConfigs:     k8sConfigsWithES,
+			id:             ServiceID{Name: "name1", Namespace: "ns"},
+			port:           8989,
+			hostname:       "name1-1",
+			objectToDelete: createTestEndpointSlice(),
+			hasSliceAccess: true,
+		},
 	} {
 
 		tt := tt // pin
@@ -693,7 +1350,8 @@ status:
 
 			if tt.deletingServices {
 				watcher.deleteService(tt.objectToDelete)
-
+			} else if tt.hasSliceAccess {
+				watcher.deleteEndpointSlice(tt.objectToDelete)
 			} else {
 				watcher.deleteEndpoints(tt.objectToDelete)
 			}
@@ -952,6 +1610,26 @@ func endpoints(identity string) *corev1.Endpoints {
 	}
 }
 
+func createTestEndpointSlice() *dv1beta1.EndpointSlice {
+	return &dv1beta1.EndpointSlice{
+		AddressType: "IPv4",
+		ObjectMeta:  metav1.ObjectMeta{Name: "name1-del", Namespace: "ns", Labels: map[string]string{dv1beta1.LabelServiceName: "name1"}},
+		Endpoints: []dv1beta1.Endpoint{
+			{
+				Addresses:  []string{"172.17.0.12"},
+				Conditions: dv1beta1.EndpointConditions{Ready: func(b bool) *bool { return &b }(true)},
+				TargetRef:  &corev1.ObjectReference{Name: "name1-1", Namespace: "ns", Kind: "Pod"},
+			},
+		},
+		Ports: []dv1beta1.EndpointPort{
+			{
+				Name: func(s string) *string { return &s }(""),
+				Port: func(i int32) *int32 { return &i }(8989),
+			},
+		},
+	}
+}
+
 func TestEndpointsChangeDetection(t *testing.T) {
 
 	k8sConfigs := []string{`
diff --git a/controller/api/public/test_helper.go b/controller/api/public/test_helper.go
index c4c94c494..beb5c932e 100644
--- a/controller/api/public/test_helper.go
+++ b/controller/api/public/test_helper.go
@@ -583,7 +583,7 @@ func newMockGrpcServer(exp expectedStatRPC) (*MockProm, *grpcServer, error) {
 }
 
 func (exp expectedStatRPC) verifyPromQueries(mockProm *MockProm) error {
-	// if exp.expectedPrometheusQueries is an empty slice we still wanna check no queries were executed.
+	// if exp.expectedPrometheusQueries is an empty slice we still want to check no queries were executed.
 	if exp.expectedPrometheusQueries != nil {
 		sort.Strings(exp.expectedPrometheusQueries)
 		sort.Strings(mockProm.QueriesExecuted)
diff --git a/pkg/addr/addr_test.go b/pkg/addr/addr_test.go
index 1ee576567..bbe75ed27 100644
--- a/pkg/addr/addr_test.go
+++ b/pkg/addr/addr_test.go
@@ -5,10 +5,141 @@ import (
 	"testing"
 
 	"github.com/golang/protobuf/proto"
+	pb "github.com/linkerd/linkerd2-proxy-api/go/net"
 	proxy "github.com/linkerd/linkerd2-proxy-api/go/net"
 	"github.com/linkerd/linkerd2/controller/gen/public"
 )
 
+func TestPublicAddressToString(t *testing.T) {
+	cases := []struct {
+		name     string
+		addr     *public.TcpAddress
+		expected string
+	}{
+		{
+			name: "ipv4",
+			addr: &public.TcpAddress{
+				Ip: &public.IPAddress{
+					Ip: &public.IPAddress_Ipv4{
+						Ipv4: 3232235521,
+					},
+				},
+				Port: 1234,
+			},
+			expected: "192.168.0.1:1234",
+		},
+		{
+			name: "ipv6",
+			addr: &public.TcpAddress{
+				Ip: &public.IPAddress{
+					Ip: &public.IPAddress_Ipv6{
+						Ipv6: &public.IPv6{
+							First: 49320,
+							Last:  1,
+						},
+					},
+				},
+				Port: 1234,
+			},
+			expected: "[::c0a8:0:0:0:1]:1234",
+		},
+		{
+			name:     "nil",
+			addr:     nil,
+			expected: "<nil>:0",
+		},
+	}
+
+	for _, c := range cases {
+		c := c
+		t.Run(c.name, func(t *testing.T) {
+			got := PublicAddressToString(c.addr)
+			if c.expected != got {
+				t.Errorf("expected: %v, got: %v", c.expected, got)
+			}
+		})
+	}
+}
+
+func TestProxyAddressesToString(t *testing.T) {
+	cases := []struct {
+		name     string
+		addrs    []pb.TcpAddress
+		expected string
+	}{
+		{
+			name: "ipv4",
+			addrs: []pb.TcpAddress{
+				{
+					Ip: &proxy.IPAddress{
+						Ip: &proxy.IPAddress_Ipv4{
+							Ipv4: 3232235521,
+						},
+					},
+					Port: 1234,
+				},
+				{
+					Ip: &proxy.IPAddress{
+						Ip: &proxy.IPAddress_Ipv4{
+							Ipv4: 3232235522,
+						},
+					},
+					Port: 1234,
+				},
+			},
+			expected: "[192.168.0.1:1234,192.168.0.2:1234]",
+		},
+		{
+			name:     "nil",
+			addrs:    nil,
+			expected: "[]",
+		},
+	}
+
+	for _, c := range cases {
+		c := c
+		t.Run(c.name, func(t *testing.T) {
+			got := ProxyAddressesToString(c.addrs)
+			if c.expected != got {
+				t.Errorf("expected: %v, got: %v", c.expected, got)
+			}
+		})
+	}
+}
+
+func TestProxyIPToString(t *testing.T) {
+	cases := []struct {
+		name     string
+		ip       *pb.IPAddress
+		expected string
+	}{
+		{
+			name: "ipv4",
+			ip: &pb.IPAddress{
+				Ip: &pb.IPAddress_Ipv4{
+					Ipv4: 3232235521,
+				},
+			},
+			expected: "192.168.0.1",
+		},
+		{
+			name:     "nil",
+			ip:       nil,
+			expected: "0.0.0.0",
+		},
+	}
+
+	for _, c := range cases {
+		c := c
+		t.Run(c.name, func(t *testing.T) {
+			got := ProxyIPToString(c.ip)
+			if c.expected != got {
+				t.Errorf("expected: %v, got: %v", c.expected, got)
+			}
+		})
+	}
+}
+
 func TestNetToPublic(t *testing.T) {
 
 	type addrExp struct {
diff --git a/pkg/charts/linkerd2/values_test.go b/pkg/charts/linkerd2/values_test.go
index 71cffb9c7..5fea753c8 100644
--- a/pkg/charts/linkerd2/values_test.go
+++ b/pkg/charts/linkerd2/values_test.go
@@ -115,7 +115,6 @@ func TestNewValues(t *testing.T) {
 		NodeSelector: map[string]string{
 			"beta.kubernetes.io/os": "linux",
 		},
-
 		Dashboard: &Dashboard{
 			Replicas: 1,
 		},
diff --git a/pkg/k8s/k8s_test.go b/pkg/k8s/k8s_test.go
index 8335194cb..f14329993 100644
--- a/pkg/k8s/k8s_test.go
+++ b/pkg/k8s/k8s_test.go
@@ -20,7 +20,7 @@ func TestGetConfig(t *testing.T) {
 	t.Run("Returns error if configuration cannot be found", func(t *testing.T) {
 		_, err := GetConfig("/this/doest./not/exist.config", "")
 		if err == nil {
-			t.Fatalf("Expecting error when config file doesnt exist, got nothing")
+			t.Fatalf("Expecting error when config file does not exist, got nothing")
 		}
 	})
 }
diff --git a/pkg/protohttp/protohttp_test.go b/pkg/protohttp/protohttp_test.go
index 67f54deba..1ffbbcf97 100644
--- a/pkg/protohttp/protohttp_test.go
+++ b/pkg/protohttp/protohttp_test.go
@@ -409,7 +409,7 @@ func TestNewStreamingWriter(t *testing.T) {
 		}
 	})
 
-	t.Run("Returns an error if writer doesnt support streaming", func(t *testing.T) {
+	t.Run("Returns an error if writer does not support streaming", func(t *testing.T) {
 		_, err := NewStreamingWriter(&nonStreamingResponseWriter{})
 		if err == nil {
 			t.Fatalf("Expecting error, got nothing")
diff --git a/test/integration/externalissuer/external_issuer_test.go b/test/integration/externalissuer/external_issuer_test.go
index 47402af72..911526d6e 100644
--- a/test/integration/externalissuer/external_issuer_test.go
+++ b/test/integration/externalissuer/external_issuer_test.go
@@ -21,7 +21,7 @@ const (
 func TestMain(m *testing.M) {
 	TestHelper = testutil.NewTestHelper()
 	if !TestHelper.ExternalIssuer() {
-		fmt.Fprintln(os.Stdout, "Skiping as --external-issuer=false")
+		fmt.Fprintln(os.Stdout, "Skipping as --external-issuer=false")
 		os.Exit(0)
 	}
 	os.Exit(testutil.Run(m, TestHelper))
@@ -83,7 +83,7 @@ func checkAppWoks(t *testing.T, timeout time.Duration) error {
 
 		stat := rowStats[TestAppBackendDeploymentName]
 		if stat.Success != "100.00%" {
-			t.Fatalf("Expected no errors in test app but got [%s] succes rate", stat.Success)
+			t.Fatalf("Expected no errors in test app but got [%s] success rate", stat.Success)
 		}
 		return nil
 	})
diff --git a/test/integration/inject/inject_test.go b/test/integration/inject/inject_test.go
index 06fa42327..0f0be58f0 100644
--- a/test/integration/inject/inject_test.go
+++ b/test/integration/inject/inject_test.go
@@ -147,12 +147,12 @@ func TestInjectAutoNamespaceOverrideAnnotations(t *testing.T) {
 
 	// Match the pod configuration with the namespace level overrides
 	if proxyContainer.Resources.Requests["memory"] != resource.MustParse(nsProxyMemReq) {
-		testutil.Fatalf(t, "proxy memory resource request falied to match with namespace level override")
+		testutil.Fatalf(t, "proxy memory resource request failed to match with namespace level override")
 	}
 
 	// Match with proxy level override
 	if proxyContainer.Resources.Requests["cpu"] != resource.MustParse(podProxyCPUReq) {
-		testutil.Fatalf(t, "proxy cpu resource request falied to match with pod level override")
+		testutil.Fatalf(t, "proxy cpu resource request failed to match with pod level override")
 	}
 }