Repository: linkerd/linkerd2
Pull request #4700 (2020-07-02 09:12:58)

Fix spelling mistakes using codespell
Using following command the wrong spelling were found and later on
fixed:

```
codespell --skip CHANGES.md,.git,go.sum,\
    controller/cmd/service-mirror/events_formatting.go,\
    controller/cmd/service-mirror/cluster_watcher_test_util.go,\
    SECURITY_AUDIT.pdf,.gcp.json.enc,web/app/img/favicon.png \
    --ignore-words-list=aks,uint,ans,files\' --check-filenames \
    --check-hidden
```


surajssd: @alpeb

> I'm wondering, do you know what's the benefit of using codespell over some simple spell checker? Maybe it knows to look only into comments, or something like that?

I am not really aware of what it does differently compared to the normal spell checker. But this is from the wiki:

```
codespell is designed to find and fix common misspellings in text files. It is designed primarily for checking misspelled words in source code, but it can be used with other files as well.
```

src: https://manpages.debian.org/testing/codespell/codespell.1.en.html


> It'd be cool to integrate this into our CI pipeline wink wink

Yep we do that in our project and that's how I caught some errors and wanted to fix them here.

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/ADOPTERS.md b/ADOPTERS.md
index b4c266fb9..38b3ca466 100644
--- a/ADOPTERS.md
+++ b/ADOPTERS.md
@@ -6,16 +6,19 @@
 - [Attest](https://www.askattest.com)
 - [Buoyant](https://buoyant.io)
 - [Candide](https://candidegardening.com)
-- [Commonbond](https://www.commonbond.co/)
+- [Celonis](https://celonis.com)
 - [CloverHealth](https://www.cloverhealth.com/)
+- [Commonbond](https://www.commonbond.co/)
 - [Expedia](https://www.expedia.com)
 - [Facelift](https://www.facelift-bbt.com/en)
 - [finleap connect](https://connect.finleap.com/)
+- [HomeChoice](https://www.homechoice.co.za/)
 - [In Loco](https://inloco.com.br/en/)
 - [Just Football](https://justfootball.io)
 - [Kairos](https://kairos.com)
 - [Kurio](https://kurio.id)
 - [LeadIQ](https://leadiq.com)
+- [MattePaint](https://www.mattepaint.com/)
 - [Mulligan Funding](https://www.mulliganfunding.com/)
 - [National Information Solutions Cooperative (NISC)](https://nisc.coop/)
 - [Nordstrom](https://nordstrom.com/)
@@ -23,9 +26,10 @@
 - [PlayStudios Asia](https://www.playstudios.asia)
 - [Projector](https://projector.com)
 - [Studyo](https://studyo.co)
+- [Sue](https://sue.nl)
 - [Transit](https://transit.app)
 - [Vernacular.ai](https://vernacular.ai/)
-- [HomeChoice](https://www.homechoice.co.za/)
+- [YouMail](https://www.youmail.com)
 
 If you're using Linkerd 2.x and aren't on this list, please [submit a pull
-request](https://github.com/linkerd/linkerd2/pulls)!
+request](https://github.com/linkerd/linkerd2/edit/main/ADOPTERS.md)!
diff --git a/CHANGES.md b/CHANGES.md
index c0ce0d243..075138a2d 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,27 @@
 # Changes
 
+## edge-20.7.1
+
+This edge release features the option to persist prometheus data to a volume
+instead of memory, so that historical metrics are available when prometheus is
+restarted. Additional changes are outlined in the bullet points below.
+
+* Some commands like `linkerd stat` would fail if any control plane components
+  were unhealthy, even when other replicas are healthy. The check conditions
+  for these commands have been improved
+* The helm chart can now configure persistent storage for Prometheus
+  (thanks @naseemkullah!)
+* The proxy log output format can now be configured to `plain` or `json` using
+  the `config.linkerd.io/proxy-log-format` annotation or the
+  `global.proxy.logFormat` value in the helm chart
+  (thanks again @naseemkullah!)
+* `linkerd install --addon-config=` now supports URLs in addition to local
+  files
+* The CNI Helm chart used the incorrect variable name to determine the `createdBy`
+  version tag. This is now controlled by `cniPluginVersion` in the helm chart
+* The proxy's default buffer size has been increased, which reduces latency when
+  the proxy has many concurrent clients
+
 ## edge-20.6.4
 
 This edge release moves the proxy onto a new version of the Tokio runtime. This
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/linkerd2/README.md b/charts/linkerd2/README.md
index d661ea10a..007c2f597 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
 
diff --git a/charts/linkerd2/templates/destination-rbac.yaml b/charts/linkerd2/templates/destination-rbac.yaml
index 18b977b99..29d396037 100644
--- a/charts/linkerd2/templates/destination-rbac.yaml
+++ b/charts/linkerd2/templates/destination-rbac.yaml
@@ -26,6 +26,9 @@ rules:
 - apiGroups: ["split.smi-spec.io"]
   resources: ["trafficsplits"]
   verbs: ["list", "get", "watch"]
+- apiGroups: ["discovery.k8s.io"]
+  resources: ["endpointslices"]
+  verbs: ["list", "get", "watch"]
 ---
 kind: ClusterRoleBinding
 apiVersion: rbac.authorization.k8s.io/v1
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/k8s/api.go b/controller/k8s/api.go
index 57064b6f7..baa6a8750 100644
--- a/controller/k8s/api.go
+++ b/controller/k8s/api.go
@@ -34,6 +34,7 @@ import (
 	batchv1informers "k8s.io/client-go/informers/batch/v1"
 	batchv1beta1informers "k8s.io/client-go/informers/batch/v1beta1"
 	coreinformers "k8s.io/client-go/informers/core/v1"
+	discoveryinformers "k8s.io/client-go/informers/discovery/v1beta1"
 	"k8s.io/client-go/kubernetes"
 	"k8s.io/client-go/tools/cache"
 )
@@ -61,6 +62,7 @@ const (
 	TS
 	Node
 	Secret
+	ES // EndpointSlice resource
 )
 
 // API provides shared informers for all Kubernetes objects
@@ -72,6 +74,7 @@ type API struct {
 	deploy   appv1informers.DeploymentInformer
 	ds       appv1informers.DaemonSetInformer
 	endpoint coreinformers.EndpointsInformer
+	es       discoveryinformers.EndpointSliceInformer
 	job      batchv1informers.JobInformer
 	mwc      arinformers.MutatingWebhookConfigurationInformer
 	ns       coreinformers.NamespaceInformer
@@ -204,6 +207,9 @@ func NewAPI(
 		case Endpoint:
 			api.endpoint = sharedInformers.Core().V1().Endpoints()
 			api.syncChecks = append(api.syncChecks, api.endpoint.Informer().HasSynced)
+		case ES:
+			api.es = sharedInformers.Discovery().V1beta1().EndpointSlices()
+			api.syncChecks = append(api.syncChecks, api.es.Informer().HasSynced)
 		case Job:
 			api.job = sharedInformers.Batch().V1().Jobs()
 			api.syncChecks = append(api.syncChecks, api.job.Informer().HasSynced)
@@ -334,6 +340,14 @@ func (api *API) Endpoint() coreinformers.EndpointsInformer {
 	return api.endpoint
 }
 
+// ES provides access to a shared informer and lister for EndpointSlices
+func (api *API) ES() discoveryinformers.EndpointSliceInformer {
+	if api.es == nil {
+		panic("EndpointSlices informer not configured")
+	}
+	return api.es
+}
+
 // CM provides access to a shared informer and lister for ConfigMaps.
 func (api *API) CM() coreinformers.ConfigMapInformer {
 	if api.cm == nil {
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/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/authz.go b/pkg/k8s/authz.go
index 6617ebf7c..3f274e04b 100644
--- a/pkg/k8s/authz.go
+++ b/pkg/k8s/authz.go
@@ -105,6 +105,43 @@ func ServiceProfilesAccess(k8sClient kubernetes.Interface) error {
 	return errors.New("ServiceProfile CRD not found")
 }
 
+// EndpointSliceAccess verifies whether the K8s cluster has
+// access to EndpointSlice resources.
+//TODO: Uncomment function and change return type once EndpointSlices
+// are supported and made opt-in through install flag
+func EndpointSliceAccess(k8sClient kubernetes.Interface) bool {
+	// gv := discovery.SchemeGroupVersion.String()
+	// res, err := k8sClient.Discovery().ServerResourcesForGroupVersion(gv)
+	// if err != nil {
+	// 	return err
+	// }
+
+	// if res.GroupVersion == gv {
+	// 	for _, apiRes := range res.APIResources {
+	// 		if apiRes.Kind == "EndpointSlice" {
+	// 			return checkEndpointSlicesExist(k8sClient)
+	// 		}
+	// 	}
+	// }
+
+	// return errors.New("EndpointSlice resource not found")
+	return false
+}
+
+//TODO: Uncomment function once EndpointSlices are supported and opt-in
+//func checkEndpointSlicesExist(k8sclient kubernetes.Interface) error {
+//	sliceList, err := k8sclient.DiscoveryV1beta1().EndpointSlices("").List(metav1.ListOptions{})
+//	if err != nil {
+//		return err
+//	}
+//
+//	if len(sliceList.Items) > 0 {
+//		return nil
+//	}
+//
+//	return errors.New("no EndpointSlice resources exist in the cluster")
+//}
+
 // ClusterAccess verifies whether k8sClient is authorized to access all pods in
 // all namespaces in the cluster.
 func ClusterAccess(k8sClient kubernetes.Interface) error {
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/testdata/install_addon_config.golden b/cli/cmd/testdata/install_addon_config.golden
index d426cf315..8c4946b5b 100644
--- a/cli/cmd/testdata/install_addon_config.golden
+++ b/cli/cmd/testdata/install_addon_config.golden
@@ -141,6 +141,9 @@ rules:
 - apiGroups: ["split.smi-spec.io"]
   resources: ["trafficsplits"]
   verbs: ["list", "get", "watch"]
+- apiGroups: ["discovery.k8s.io"]
+  resources: ["endpointslices"]
+  verbs: ["list", "get", "watch"]
 ---
 kind: ClusterRoleBinding
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/install_config.golden b/cli/cmd/testdata/install_config.golden
index b33dfcb6f..c44c7b5a1 100644
--- a/cli/cmd/testdata/install_config.golden
+++ b/cli/cmd/testdata/install_config.golden
@@ -141,6 +141,9 @@ rules:
 - apiGroups: ["split.smi-spec.io"]
   resources: ["trafficsplits"]
   verbs: ["list", "get", "watch"]
+- apiGroups: ["discovery.k8s.io"]
+  resources: ["endpointslices"]
+  verbs: ["list", "get", "watch"]
 ---
 kind: ClusterRoleBinding
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/install_controlplane_tracing_output.golden b/cli/cmd/testdata/install_controlplane_tracing_output.golden
index a1b08250b..9d8dd470d 100644
--- a/cli/cmd/testdata/install_controlplane_tracing_output.golden
+++ b/cli/cmd/testdata/install_controlplane_tracing_output.golden
@@ -141,6 +141,9 @@ rules:
 - apiGroups: ["split.smi-spec.io"]
   resources: ["trafficsplits"]
   verbs: ["list", "get", "watch"]
+- apiGroups: ["discovery.k8s.io"]
+  resources: ["endpointslices"]
+  verbs: ["list", "get", "watch"]
 ---
 kind: ClusterRoleBinding
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/install_custom_registry.golden b/cli/cmd/testdata/install_custom_registry.golden
index 6b6bb354b..8392e93e1 100644
--- a/cli/cmd/testdata/install_custom_registry.golden
+++ b/cli/cmd/testdata/install_custom_registry.golden
@@ -141,6 +141,9 @@ rules:
 - apiGroups: ["split.smi-spec.io"]
   resources: ["trafficsplits"]
   verbs: ["list", "get", "watch"]
+- apiGroups: ["discovery.k8s.io"]
+  resources: ["endpointslices"]
+  verbs: ["list", "get", "watch"]
 ---
 kind: ClusterRoleBinding
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/install_default.golden b/cli/cmd/testdata/install_default.golden
index 8a58bc10b..8c93b1264 100644
--- a/cli/cmd/testdata/install_default.golden
+++ b/cli/cmd/testdata/install_default.golden
@@ -141,6 +141,9 @@ rules:
 - apiGroups: ["split.smi-spec.io"]
   resources: ["trafficsplits"]
   verbs: ["list", "get", "watch"]
+- apiGroups: ["discovery.k8s.io"]
+  resources: ["endpointslices"]
+  verbs: ["list", "get", "watch"]
 ---
 kind: ClusterRoleBinding
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/install_default_override_dst_get_nets.golden b/cli/cmd/testdata/install_default_override_dst_get_nets.golden
index 1ca738168..0f2181977 100644
--- a/cli/cmd/testdata/install_default_override_dst_get_nets.golden
+++ b/cli/cmd/testdata/install_default_override_dst_get_nets.golden
@@ -141,6 +141,9 @@ rules:
 - apiGroups: ["split.smi-spec.io"]
   resources: ["trafficsplits"]
   verbs: ["list", "get", "watch"]
+- apiGroups: ["discovery.k8s.io"]
+  resources: ["endpointslices"]
+  verbs: ["list", "get", "watch"]
 ---
 kind: ClusterRoleBinding
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/install_grafana_existing.golden b/cli/cmd/testdata/install_grafana_existing.golden
index d44bb1411..6b0a8b466 100644
--- a/cli/cmd/testdata/install_grafana_existing.golden
+++ b/cli/cmd/testdata/install_grafana_existing.golden
@@ -141,6 +141,9 @@ rules:
 - apiGroups: ["split.smi-spec.io"]
   resources: ["trafficsplits"]
   verbs: ["list", "get", "watch"]
+- apiGroups: ["discovery.k8s.io"]
+  resources: ["endpointslices"]
+  verbs: ["list", "get", "watch"]
 ---
 kind: ClusterRoleBinding
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/install_ha_output.golden b/cli/cmd/testdata/install_ha_output.golden
index 14c682b4e..f4e569f77 100644
--- a/cli/cmd/testdata/install_ha_output.golden
+++ b/cli/cmd/testdata/install_ha_output.golden
@@ -141,6 +141,9 @@ rules:
 - apiGroups: ["split.smi-spec.io"]
   resources: ["trafficsplits"]
   verbs: ["list", "get", "watch"]
+- apiGroups: ["discovery.k8s.io"]
+  resources: ["endpointslices"]
+  verbs: ["list", "get", "watch"]
 ---
 kind: ClusterRoleBinding
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/install_ha_with_overrides_output.golden b/cli/cmd/testdata/install_ha_with_overrides_output.golden
index 2438e5cc1..0aee6f370 100644
--- a/cli/cmd/testdata/install_ha_with_overrides_output.golden
+++ b/cli/cmd/testdata/install_ha_with_overrides_output.golden
@@ -141,6 +141,9 @@ rules:
 - apiGroups: ["split.smi-spec.io"]
   resources: ["trafficsplits"]
   verbs: ["list", "get", "watch"]
+- apiGroups: ["discovery.k8s.io"]
+  resources: ["endpointslices"]
+  verbs: ["list", "get", "watch"]
 ---
 kind: ClusterRoleBinding
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/install_heartbeat_disabled_output.golden b/cli/cmd/testdata/install_heartbeat_disabled_output.golden
index bbb6dea75..9b590c724 100644
--- a/cli/cmd/testdata/install_heartbeat_disabled_output.golden
+++ b/cli/cmd/testdata/install_heartbeat_disabled_output.golden
@@ -141,6 +141,9 @@ rules:
 - apiGroups: ["split.smi-spec.io"]
   resources: ["trafficsplits"]
   verbs: ["list", "get", "watch"]
+- apiGroups: ["discovery.k8s.io"]
+  resources: ["endpointslices"]
+  verbs: ["list", "get", "watch"]
 ---
 kind: ClusterRoleBinding
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/install_helm_output.golden b/cli/cmd/testdata/install_helm_output.golden
index 14e76f3f7..953b2a6c4 100644
--- a/cli/cmd/testdata/install_helm_output.golden
+++ b/cli/cmd/testdata/install_helm_output.golden
@@ -149,6 +149,9 @@ rules:
 - apiGroups: ["split.smi-spec.io"]
   resources: ["trafficsplits"]
   verbs: ["list", "get", "watch"]
+- apiGroups: ["discovery.k8s.io"]
+  resources: ["endpointslices"]
+  verbs: ["list", "get", "watch"]
 ---
 kind: ClusterRoleBinding
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/install_helm_output_addons.golden b/cli/cmd/testdata/install_helm_output_addons.golden
index 3f397893d..a8253d435 100644
--- a/cli/cmd/testdata/install_helm_output_addons.golden
+++ b/cli/cmd/testdata/install_helm_output_addons.golden
@@ -149,6 +149,9 @@ rules:
 - apiGroups: ["split.smi-spec.io"]
   resources: ["trafficsplits"]
   verbs: ["list", "get", "watch"]
+- apiGroups: ["discovery.k8s.io"]
+  resources: ["endpointslices"]
+  verbs: ["list", "get", "watch"]
 ---
 kind: ClusterRoleBinding
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/install_helm_output_ha.golden b/cli/cmd/testdata/install_helm_output_ha.golden
index 3ca2c0cb4..8afbbbbc6 100644
--- a/cli/cmd/testdata/install_helm_output_ha.golden
+++ b/cli/cmd/testdata/install_helm_output_ha.golden
@@ -149,6 +149,9 @@ rules:
 - apiGroups: ["split.smi-spec.io"]
   resources: ["trafficsplits"]
   verbs: ["list", "get", "watch"]
+- apiGroups: ["discovery.k8s.io"]
+  resources: ["endpointslices"]
+  verbs: ["list", "get", "watch"]
 ---
 kind: ClusterRoleBinding
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/install_no_init_container.golden b/cli/cmd/testdata/install_no_init_container.golden
index 9e492d379..a68a13822 100644
--- a/cli/cmd/testdata/install_no_init_container.golden
+++ b/cli/cmd/testdata/install_no_init_container.golden
@@ -141,6 +141,9 @@ rules:
 - apiGroups: ["split.smi-spec.io"]
   resources: ["trafficsplits"]
   verbs: ["list", "get", "watch"]
+- apiGroups: ["discovery.k8s.io"]
+  resources: ["endpointslices"]
+  verbs: ["list", "get", "watch"]
 ---
 kind: ClusterRoleBinding
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/install_output.golden b/cli/cmd/testdata/install_output.golden
index ae7eb45fa..c643e3c04 100644
--- a/cli/cmd/testdata/install_output.golden
+++ b/cli/cmd/testdata/install_output.golden
@@ -141,6 +141,9 @@ rules:
 - apiGroups: ["split.smi-spec.io"]
   resources: ["trafficsplits"]
   verbs: ["list", "get", "watch"]
+- apiGroups: ["discovery.k8s.io"]
+  resources: ["endpointslices"]
+  verbs: ["list", "get", "watch"]
 ---
 kind: ClusterRoleBinding
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/install_proxy_ignores.golden b/cli/cmd/testdata/install_proxy_ignores.golden
index 5c73c8d54..0cc49332a 100644
--- a/cli/cmd/testdata/install_proxy_ignores.golden
+++ b/cli/cmd/testdata/install_proxy_ignores.golden
@@ -141,6 +141,9 @@ rules:
 - apiGroups: ["split.smi-spec.io"]
   resources: ["trafficsplits"]
   verbs: ["list", "get", "watch"]
+- apiGroups: ["discovery.k8s.io"]
+  resources: ["endpointslices"]
+  verbs: ["list", "get", "watch"]
 ---
 kind: ClusterRoleBinding
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/install_restricted_dashboard.golden b/cli/cmd/testdata/install_restricted_dashboard.golden
index 9b5338852..f0c6c44e2 100644
--- a/cli/cmd/testdata/install_restricted_dashboard.golden
+++ b/cli/cmd/testdata/install_restricted_dashboard.golden
@@ -141,6 +141,9 @@ rules:
 - apiGroups: ["split.smi-spec.io"]
   resources: ["trafficsplits"]
   verbs: ["list", "get", "watch"]
+- apiGroups: ["discovery.k8s.io"]
+  resources: ["endpointslices"]
+  verbs: ["list", "get", "watch"]
 ---
 kind: ClusterRoleBinding
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/install_tracing.golden b/cli/cmd/testdata/install_tracing.golden
index 6e4c41831..257f1b6d7 100644
--- a/cli/cmd/testdata/install_tracing.golden
+++ b/cli/cmd/testdata/install_tracing.golden
@@ -141,6 +141,9 @@ rules:
 - apiGroups: ["split.smi-spec.io"]
   resources: ["trafficsplits"]
   verbs: ["list", "get", "watch"]
+- apiGroups: ["discovery.k8s.io"]
+  resources: ["endpointslices"]
+  verbs: ["list", "get", "watch"]
 ---
 kind: ClusterRoleBinding
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/install_tracing_overwrite.golden b/cli/cmd/testdata/install_tracing_overwrite.golden
index 4a9d1b9ac..a105ca587 100644
--- a/cli/cmd/testdata/install_tracing_overwrite.golden
+++ b/cli/cmd/testdata/install_tracing_overwrite.golden
@@ -141,6 +141,9 @@ rules:
 - apiGroups: ["split.smi-spec.io"]
   resources: ["trafficsplits"]
   verbs: ["list", "get", "watch"]
+- apiGroups: ["discovery.k8s.io"]
+  resources: ["endpointslices"]
+  verbs: ["list", "get", "watch"]
 ---
 kind: ClusterRoleBinding
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/upgrade_add-on_config.golden b/cli/cmd/testdata/upgrade_add-on_config.golden
index f3011ac37..159dbd65b 100644
--- a/cli/cmd/testdata/upgrade_add-on_config.golden
+++ b/cli/cmd/testdata/upgrade_add-on_config.golden
@@ -141,6 +141,9 @@ rules:
 - apiGroups: ["split.smi-spec.io"]
   resources: ["trafficsplits"]
   verbs: ["list", "get", "watch"]
+- apiGroups: ["discovery.k8s.io"]
+  resources: ["endpointslices"]
+  verbs: ["list", "get", "watch"]
 ---
 kind: ClusterRoleBinding
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/upgrade_add-on_overwrite.golden b/cli/cmd/testdata/upgrade_add-on_overwrite.golden
index 170cb49d2..327328269 100644
--- a/cli/cmd/testdata/upgrade_add-on_overwrite.golden
+++ b/cli/cmd/testdata/upgrade_add-on_overwrite.golden
@@ -141,6 +141,9 @@ rules:
 - apiGroups: ["split.smi-spec.io"]
   resources: ["trafficsplits"]
   verbs: ["list", "get", "watch"]
+- apiGroups: ["discovery.k8s.io"]
+  resources: ["endpointslices"]
+  verbs: ["list", "get", "watch"]
 ---
 kind: ClusterRoleBinding
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/upgrade_add_add-on.golden b/cli/cmd/testdata/upgrade_add_add-on.golden
index cbc655cf0..659150fcb 100644
--- a/cli/cmd/testdata/upgrade_add_add-on.golden
+++ b/cli/cmd/testdata/upgrade_add_add-on.golden
@@ -141,6 +141,9 @@ rules:
 - apiGroups: ["split.smi-spec.io"]
   resources: ["trafficsplits"]
   verbs: ["list", "get", "watch"]
+- apiGroups: ["discovery.k8s.io"]
+  resources: ["endpointslices"]
+  verbs: ["list", "get", "watch"]
 ---
 kind: ClusterRoleBinding
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/upgrade_default.golden b/cli/cmd/testdata/upgrade_default.golden
index eb49d03b9..223f2f10a 100644
--- a/cli/cmd/testdata/upgrade_default.golden
+++ b/cli/cmd/testdata/upgrade_default.golden
@@ -141,6 +141,9 @@ rules:
 - apiGroups: ["split.smi-spec.io"]
   resources: ["trafficsplits"]
   verbs: ["list", "get", "watch"]
+- apiGroups: ["discovery.k8s.io"]
+  resources: ["endpointslices"]
+  verbs: ["list", "get", "watch"]
 ---
 kind: ClusterRoleBinding
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/upgrade_external_issuer.golden b/cli/cmd/testdata/upgrade_external_issuer.golden
index 3e6a533df..e0850d458 100644
--- a/cli/cmd/testdata/upgrade_external_issuer.golden
+++ b/cli/cmd/testdata/upgrade_external_issuer.golden
@@ -141,6 +141,9 @@ rules:
 - apiGroups: ["split.smi-spec.io"]
   resources: ["trafficsplits"]
   verbs: ["list", "get", "watch"]
+- apiGroups: ["discovery.k8s.io"]
+  resources: ["endpointslices"]
+  verbs: ["list", "get", "watch"]
 ---
 kind: ClusterRoleBinding
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/upgrade_grafana_addon_overwrite.yaml b/cli/cmd/testdata/upgrade_grafana_addon_overwrite.yaml
index ffd279bd9..3703dc116 100644
--- a/cli/cmd/testdata/upgrade_grafana_addon_overwrite.yaml
+++ b/cli/cmd/testdata/upgrade_grafana_addon_overwrite.yaml
@@ -141,6 +141,9 @@ rules:
 - apiGroups: ["split.smi-spec.io"]
   resources: ["trafficsplits"]
   verbs: ["list", "get", "watch"]
+- apiGroups: ["discovery.k8s.io"]
+  resources: ["endpointslices"]
+  verbs: ["list", "get", "watch"]
 ---
 kind: ClusterRoleBinding
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/upgrade_grafana_disabled.yaml b/cli/cmd/testdata/upgrade_grafana_disabled.yaml
index d1306919a..06562335a 100644
--- a/cli/cmd/testdata/upgrade_grafana_disabled.yaml
+++ b/cli/cmd/testdata/upgrade_grafana_disabled.yaml
@@ -141,6 +141,9 @@ rules:
 - apiGroups: ["split.smi-spec.io"]
   resources: ["trafficsplits"]
   verbs: ["list", "get", "watch"]
+- apiGroups: ["discovery.k8s.io"]
+  resources: ["endpointslices"]
+  verbs: ["list", "get", "watch"]
 ---
 kind: ClusterRoleBinding
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/upgrade_grafana_enabled.yaml b/cli/cmd/testdata/upgrade_grafana_enabled.yaml
index ffd279bd9..3703dc116 100644
--- a/cli/cmd/testdata/upgrade_grafana_enabled.yaml
+++ b/cli/cmd/testdata/upgrade_grafana_enabled.yaml
@@ -141,6 +141,9 @@ rules:
 - apiGroups: ["split.smi-spec.io"]
   resources: ["trafficsplits"]
   verbs: ["list", "get", "watch"]
+- apiGroups: ["discovery.k8s.io"]
+  resources: ["endpointslices"]
+  verbs: ["list", "get", "watch"]
 ---
 kind: ClusterRoleBinding
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/upgrade_grafana_enabled_disabled.yaml b/cli/cmd/testdata/upgrade_grafana_enabled_disabled.yaml
index d1306919a..06562335a 100644
--- a/cli/cmd/testdata/upgrade_grafana_enabled_disabled.yaml
+++ b/cli/cmd/testdata/upgrade_grafana_enabled_disabled.yaml
@@ -141,6 +141,9 @@ rules:
 - apiGroups: ["split.smi-spec.io"]
   resources: ["trafficsplits"]
   verbs: ["list", "get", "watch"]
+- apiGroups: ["discovery.k8s.io"]
+  resources: ["endpointslices"]
+  verbs: ["list", "get", "watch"]
 ---
 kind: ClusterRoleBinding
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/upgrade_grafana_overwrite.yaml b/cli/cmd/testdata/upgrade_grafana_overwrite.yaml
index 134366b0f..10415aba3 100644
--- a/cli/cmd/testdata/upgrade_grafana_overwrite.yaml
+++ b/cli/cmd/testdata/upgrade_grafana_overwrite.yaml
@@ -141,6 +141,9 @@ rules:
 - apiGroups: ["split.smi-spec.io"]
   resources: ["trafficsplits"]
   verbs: ["list", "get", "watch"]
+- apiGroups: ["discovery.k8s.io"]
+  resources: ["endpointslices"]
+  verbs: ["list", "get", "watch"]
 ---
 kind: ClusterRoleBinding
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/upgrade_ha.golden b/cli/cmd/testdata/upgrade_ha.golden
index b9c9a3631..76595750a 100644
--- a/cli/cmd/testdata/upgrade_ha.golden
+++ b/cli/cmd/testdata/upgrade_ha.golden
@@ -141,6 +141,9 @@ rules:
 - apiGroups: ["split.smi-spec.io"]
   resources: ["trafficsplits"]
   verbs: ["list", "get", "watch"]
+- apiGroups: ["discovery.k8s.io"]
+  resources: ["endpointslices"]
+  verbs: ["list", "get", "watch"]
 ---
 kind: ClusterRoleBinding
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/upgrade_ha_config.golden b/cli/cmd/testdata/upgrade_ha_config.golden
index d0d1f39be..f9caa1e7f 100644
--- a/cli/cmd/testdata/upgrade_ha_config.golden
+++ b/cli/cmd/testdata/upgrade_ha_config.golden
@@ -141,6 +141,9 @@ rules:
 - apiGroups: ["split.smi-spec.io"]
   resources: ["trafficsplits"]
   verbs: ["list", "get", "watch"]
+- apiGroups: ["discovery.k8s.io"]
+  resources: ["endpointslices"]
+  verbs: ["list", "get", "watch"]
 ---
 kind: ClusterRoleBinding
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/upgrade_keep_webhook_cabundle.golden b/cli/cmd/testdata/upgrade_keep_webhook_cabundle.golden
index 8d003c807..12fe5818d 100644
--- a/cli/cmd/testdata/upgrade_keep_webhook_cabundle.golden
+++ b/cli/cmd/testdata/upgrade_keep_webhook_cabundle.golden
@@ -141,6 +141,9 @@ rules:
 - apiGroups: ["split.smi-spec.io"]
   resources: ["trafficsplits"]
   verbs: ["list", "get", "watch"]
+- apiGroups: ["discovery.k8s.io"]
+  resources: ["endpointslices"]
+  verbs: ["list", "get", "watch"]
 ---
 kind: ClusterRoleBinding
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/upgrade_nothing_addon.yaml b/cli/cmd/testdata/upgrade_nothing_addon.yaml
index ffd279bd9..3703dc116 100644
--- a/cli/cmd/testdata/upgrade_nothing_addon.yaml
+++ b/cli/cmd/testdata/upgrade_nothing_addon.yaml
@@ -141,6 +141,9 @@ rules:
 - apiGroups: ["split.smi-spec.io"]
   resources: ["trafficsplits"]
   verbs: ["list", "get", "watch"]
+- apiGroups: ["discovery.k8s.io"]
+  resources: ["endpointslices"]
+  verbs: ["list", "get", "watch"]
 ---
 kind: ClusterRoleBinding
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/upgrade_overwrite_issuer.golden b/cli/cmd/testdata/upgrade_overwrite_issuer.golden
index 9010423b8..37aeebeea 100644
--- a/cli/cmd/testdata/upgrade_overwrite_issuer.golden
+++ b/cli/cmd/testdata/upgrade_overwrite_issuer.golden
@@ -141,6 +141,9 @@ rules:
 - apiGroups: ["split.smi-spec.io"]
   resources: ["trafficsplits"]
   verbs: ["list", "get", "watch"]
+- apiGroups: ["discovery.k8s.io"]
+  resources: ["endpointslices"]
+  verbs: ["list", "get", "watch"]
 ---
 kind: ClusterRoleBinding
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/upgrade_overwrite_trust_anchors-external-issuer.golden b/cli/cmd/testdata/upgrade_overwrite_trust_anchors-external-issuer.golden
index 86593741f..66cdd077c 100644
--- a/cli/cmd/testdata/upgrade_overwrite_trust_anchors-external-issuer.golden
+++ b/cli/cmd/testdata/upgrade_overwrite_trust_anchors-external-issuer.golden
@@ -141,6 +141,9 @@ rules:
 - apiGroups: ["split.smi-spec.io"]
   resources: ["trafficsplits"]
   verbs: ["list", "get", "watch"]
+- apiGroups: ["discovery.k8s.io"]
+  resources: ["endpointslices"]
+  verbs: ["list", "get", "watch"]
 ---
 kind: ClusterRoleBinding
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/upgrade_overwrite_trust_anchors.golden b/cli/cmd/testdata/upgrade_overwrite_trust_anchors.golden
index 9010423b8..37aeebeea 100644
--- a/cli/cmd/testdata/upgrade_overwrite_trust_anchors.golden
+++ b/cli/cmd/testdata/upgrade_overwrite_trust_anchors.golden
@@ -141,6 +141,9 @@ rules:
 - apiGroups: ["split.smi-spec.io"]
   resources: ["trafficsplits"]
   verbs: ["list", "get", "watch"]
+- apiGroups: ["discovery.k8s.io"]
+  resources: ["endpointslices"]
+  verbs: ["list", "get", "watch"]
 ---
 kind: ClusterRoleBinding
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/upgrade_two_level_webhook_cert.golden b/cli/cmd/testdata/upgrade_two_level_webhook_cert.golden
index f9b52d497..d9e8c53b7 100644
--- a/cli/cmd/testdata/upgrade_two_level_webhook_cert.golden
+++ b/cli/cmd/testdata/upgrade_two_level_webhook_cert.golden
@@ -141,6 +141,9 @@ rules:
 - apiGroups: ["split.smi-spec.io"]
   resources: ["trafficsplits"]
   verbs: ["list", "get", "watch"]
+- apiGroups: ["discovery.k8s.io"]
+  resources: ["endpointslices"]
+  verbs: ["list", "get", "watch"]
 ---
 kind: ClusterRoleBinding
 apiVersion: rbac.authorization.k8s.io/v1
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/controller/k8s/test_helper.go b/controller/k8s/test_helper.go
index f81182217..5a017951b 100644
--- a/controller/k8s/test_helper.go
+++ b/controller/k8s/test_helper.go
@@ -32,6 +32,7 @@ func NewFakeAPI(configs ...string) (*API, error) {
 		Svc,
 		TS,
 		Node,
+		ES,
 	), nil
 }
 
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/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")
 	}
 }