Repository: linkerd/linkerd2
Pull request #4594 (2020-06-12 22:27:23)

Simplify CODEOWNERS constraints
My experience of our CODEOWNERS setup is that it frequently causes us to
require additional pro-forma reviews, but I think we can do a decent job
of getting the proper reviews informally without enforcing ownership.

I'd like to simplify this by relaxing the CODEOWNERS to add
@linkerd/maintainers by default. The project infrastructure docs should
remain locked-down, requiring a review from me; and I've updated the
CHANGES review requirement to be @adleong and I (practically, I'll
review most of the CHANGES, but Alex is a suitable fallback in most
cases).

Then, we leave the CNI ownership as-is (unless others want to volunteer
for those reviews ;).

<!--  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/master/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/master/CONTRIBUTING.md#committing
-->



Patch:
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 1a910a26e..2d3a780df 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1,27 +1,18 @@
-CHANGES.md @wmorgan @klingerf @olix0r @admc
-GOVERNANCE.md @klingerf @olix0r
-MAINTAINERS.md @klingerf @olix0r
-
-/.github/workflows/ @alpeb @kleimkuhler
-/charts @alpeb @zaharidichev
-
-/charts/linkerd2-cni @hemakl @zaharidichev
-/charts/linkerd-service-mirror @zaharidichev
-
-/cli @alpeb @adleong @zaharidichev
-
-/cni-plugin @hemakl @zaharidichev
-
-/controller @adleong @alpeb @kleimkuhler
-
-/pkg @adleong @alpeb
-
-Dockerfile* @adleong @alpeb @klingerf @olix0r @siggy
-
-# These do not have clear owners today
-# /grafana
-# /proto
-# /proxy-identity
-# /test
-# /testutil
-# /web
+# By default, the maintainers group is responsible for everything.
+*			@linkerd/maintainers
+
+# These documents have special review requirements:
+CHANGES.md		@adleong @olix0r
+CODE_OF_CONDUCT.md	@olix0r
+CONTRIBUTING.md		@olix0r
+DCO			@olix0r
+GOVERNANCE.md		@olix0r
+LICENSE			@olix0r
+MAINTAINERS.md		@olix0r
+README.md		@olix0r
+SECURITY.md		@olix0r
+SECURITY_AUDIT.pdf	@olix0r
+
+# Hema & Zahari understand the CNI best:
+/charts/linkerd2-cni	@hemakl @zaharidichev
+/cni-plugin		@hemakl @zaharidichev
diff --git a/Dockerfile-base b/Dockerfile-base
index 326704520..80deecde3 100644
--- a/Dockerfile-base
+++ b/Dockerfile-base
@@ -3,7 +3,7 @@
 # This means that all Linkerd containers share a common set of tools, and furthermore, they
 # are highly cacheable at runtime.
 
-FROM debian:stretch-20190812-slim
+FROM debian:buster-20200514-slim
 
 RUN apt-get update \
     && apt-get install -y --no-install-recommends \
@@ -13,3 +13,8 @@ RUN apt-get update \
         jq \
         nghttp2 \
     && rm -rf /var/lib/apt/lists/*
+
+# Switching base image from stretch to buster, we need to use iptables-legacy
+# if no changes to how iptables is invoked are done.
+RUN update-alternatives --set iptables /usr/sbin/iptables-legacy \
+    && update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy
diff --git a/Dockerfile-debug b/Dockerfile-debug
index 4192cba7a..6d7e9276e 100644
--- a/Dockerfile-debug
+++ b/Dockerfile-debug
@@ -1,4 +1,4 @@
-FROM gcr.io/linkerd-io/base:2019-09-04.01
+FROM gcr.io/linkerd-io/base:2020-06-08.01
 RUN apt-get update && apt-get install -y --no-install-recommends \
     tcpdump \
     iproute2 \
diff --git a/bin/docker-build-base b/bin/docker-build-base
index 3ab68bfcb..46f7be11f 100755
--- a/bin/docker-build-base
+++ b/bin/docker-build-base
@@ -15,7 +15,7 @@ rootdir=$( cd "$bindir"/.. && pwd )
 # shellcheck source=_docker.sh
 . "$bindir"/_docker.sh
 
-tag=2019-09-04.01
+tag=2020-06-08.01
 
 if (docker_pull base $tag); then
     echo "$(docker_repo base):$tag"
diff --git a/bin/docker-pull-deps b/bin/docker-pull-deps
index d64d3113a..9824fc9b4 100755
--- a/bin/docker-pull-deps
+++ b/bin/docker-pull-deps
@@ -9,5 +9,5 @@ bindir=$( cd "${BASH_SOURCE[0]%/*}" && pwd )
 # shellcheck source=_tag.sh
 . "$bindir"/_tag.sh
 
-docker_pull base       2019-09-04.01       || true
+docker_pull base       2020-06-08.01       || true
 docker_pull go-deps    "$(go_deps_sha)"    || true
diff --git a/bin/docker-push-deps b/bin/docker-push-deps
index 108215efc..770ac4844 100755
--- a/bin/docker-push-deps
+++ b/bin/docker-push-deps
@@ -9,5 +9,5 @@ bindir=$( cd "${BASH_SOURCE[0]%/*}" && pwd )
 # shellcheck source=_tag.sh
 . "$bindir"/_tag.sh
 
-docker_push base         2019-09-04.01
+docker_push base         2020-06-08.01
 docker_push go-deps      "$(go_deps_sha)"
diff --git a/charts/linkerd2/templates/web-rbac.yaml b/charts/linkerd2/templates/web-rbac.yaml
index e7cc2a2c4..666053579 100644
--- a/charts/linkerd2/templates/web-rbac.yaml
+++ b/charts/linkerd2/templates/web-rbac.yaml
@@ -69,6 +69,9 @@ rules:
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
+- apiGroups: ["apiregistration.k8s.io"]
+  resources: ["apiservices"]
+  verbs: ["get"]
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
diff --git a/cni-plugin/Dockerfile b/cni-plugin/Dockerfile
index 1fd771d7a..e1b270a22 100644
--- a/cni-plugin/Dockerfile
+++ b/cni-plugin/Dockerfile
@@ -6,7 +6,7 @@ COPY controller controller
 COPY cni-plugin cni-plugin
 RUN CGO_ENABLED=0 GOOS=linux go build -o /go/bin/linkerd-cni -v -mod=readonly ./cni-plugin/
 
-FROM gcr.io/linkerd-io/base:2019-09-04.01
+FROM gcr.io/linkerd-io/base:2020-06-08.01
 WORKDIR /linkerd
 RUN curl -kL -o $(which jq) https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64
 COPY --from=golang /go/bin/linkerd-cni /opt/cni/bin/
diff --git a/controller/cmd/service-mirror/cluster_watcher.go b/controller/cmd/service-mirror/cluster_watcher.go
index 12f0b33f6..883ab190e 100644
--- a/controller/cmd/service-mirror/cluster_watcher.go
+++ b/controller/cmd/service-mirror/cluster_watcher.go
@@ -3,11 +3,14 @@ package servicemirror
 import (
 	"errors"
 	"fmt"
+	"net"
 	"strconv"
 	"strings"
+	"time"
 
 	"github.com/linkerd/linkerd2/controller/k8s"
 	consts "github.com/linkerd/linkerd2/pkg/k8s"
+	"github.com/prometheus/client_golang/prometheus"
 	logging "github.com/sirupsen/logrus"
 	corev1 "k8s.io/api/core/v1"
 	kerrors "k8s.io/apimachinery/pkg/api/errors"
@@ -36,6 +39,7 @@ type (
 		log                    *logging.Entry
 		eventsQueue            workqueue.RateLimitingInterface
 		requeueLimit           int
+		repairPeriod           time.Duration
 	}
 
 	// ProbeConfig describes the configured probe on particular gateway (if presents)
@@ -136,6 +140,10 @@ type (
 		svc *corev1.Service
 	}
 
+	// RepairEndpoints is issued when the service mirror and mirror gateway
+	// endpoints should be resolved based on the remote gateway and updated.
+	RepairEndpoints struct{}
+
 	gatewayMetadata struct {
 		Name      string
 		Namespace string
@@ -171,6 +179,7 @@ func NewRemoteClusterServiceWatcher(
 	cfg *rest.Config,
 	clusterName string,
 	requeueLimit int,
+	repairPeriod time.Duration,
 	clusterDomain string,
 ) (*RemoteClusterServiceWatcher, error) {
 	remoteAPI, err := k8s.InitializeAPIForConfig(cfg, false, k8s.Svc)
@@ -191,6 +200,7 @@ func NewRemoteClusterServiceWatcher(
 		}),
 		eventsQueue:  workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()),
 		requeueLimit: requeueLimit,
+		repairPeriod: repairPeriod,
 	}, nil
 }
 
@@ -990,6 +1000,8 @@ func (rcsw *RemoteClusterServiceWatcher) processNextEvent() (bool, interface{},
 		err = rcsw.cleanupMirroredResources()
 	case *OprhanedServicesGcTriggered:
 		err = rcsw.cleanupOrphanedServices()
+	case *RepairEndpoints:
+		rcsw.repairEndpoints()
 	default:
 		if ev != nil || !done { // we get a nil in case we are shutting down...
 			rcsw.log.Warnf("Received unknown event: %v", ev)
@@ -1005,6 +1017,7 @@ func (rcsw *RemoteClusterServiceWatcher) processNextEvent() (bool, interface{},
 func (rcsw *RemoteClusterServiceWatcher) processEvents() {
 	for {
 		done, event, err := rcsw.processNextEvent()
+		rcsw.eventsQueue.Done(event)
 		// the logic here is that there might have been an API
 		// connectivity glitch or something. So its not a bad idea to requeue
 		// the event and try again up to a number of limits, just to ensure
@@ -1066,6 +1079,20 @@ func (rcsw *RemoteClusterServiceWatcher) Start() error {
 		},
 	)
 	go rcsw.processEvents()
+
+	go func() {
+		ticker := time.NewTicker(rcsw.repairPeriod)
+		for {
+			select {
+			case <-ticker.C:
+				ev := RepairEndpoints{}
+				rcsw.eventsQueue.Add(&ev)
+			case <-rcsw.stopper:
+				return
+			}
+		}
+	}()
+
 	return nil
 }
 
@@ -1121,9 +1148,16 @@ func (rcsw *RemoteClusterServiceWatcher) extractGatewaySpec(gateway *corev1.Serv
 
 	var gatewayEndpoints []corev1.EndpointAddress
 	for _, ingress := range gateway.Status.LoadBalancer.Ingress {
+		ip := ingress.IP
+		if ip == "" {
+			ipAddr, err := net.ResolveIPAddr("ip", ingress.Hostname)
+			if err != nil {
+				return nil, err
+			}
+			ip = ipAddr.String()
+		}
 		gatewayEndpoints = append(gatewayEndpoints, corev1.EndpointAddress{
-			IP:       ingress.IP,
-			Hostname: ingress.Hostname,
+			IP: ip,
 		})
 	}
 
@@ -1144,3 +1178,53 @@ func (rcsw *RemoteClusterServiceWatcher) extractGatewaySpec(gateway *corev1.Serv
 		ProbeConfig:      probeConfig,
 	}, nil
 }
+
+// repairEndpoints will look up all remote gateways and update the endpoints
+// of all local mirror services for those gateways. Note that we ignore resource
+// version and update ALL affected endpoints objects. This is because the
+// remote gateway may be exposed as a DNS hostname and we want to re-resolve
+// this DNS name in case its IP address has changed. By invoking repairEndpoints
+// frequently, we can pick up any DNS changes fairly quickly.
+// TODO: Replace this with a more robust solution that does not rely on
+// frequently repairing endpoints to pick up DNS updates.
+func (rcsw *RemoteClusterServiceWatcher) repairEndpoints() {
+	svcs, err := rcsw.remoteAPIClient.Svc().Lister().Services(metav1.NamespaceAll).List(labels.Everything())
+	if err != nil {
+		rcsw.log.Errorf("failed to list remote gateways: %s", err)
+		return
+	}
+	rcsw.log.Errorf("During repair, found %d remote services", len(svcs))
+	for _, svc := range svcs {
+		if isGateway(svc.Annotations) {
+
+			// We omit a resource version here because we want to get ALL mirror
+			// services for this gateway.
+			affectedServices, err := rcsw.affectedMirroredServicesForGatewayUpdate(&gatewayMetadata{
+				Name:      svc.Name,
+				Namespace: svc.Namespace,
+			}, "")
+			if err != nil {
+				rcsw.log.Errorf("failed to determine mirror services for gateway %s.%s: %s", svc.Name, svc.Namespace, err)
+				continue
+			}
+
+			spec, err := rcsw.extractGatewaySpec(svc)
+			if err != nil {
+				rcsw.log.Errorf("failed to extract spec for gateway %s.%s: %s", svc.Name, svc.Namespace, err)
+				continue
+			}
+
+			endpointRepairCounter.With(prometheus.Labels{
+				gatewayNameLabel:      svc.Name,
+				gatewayNamespaceLabel: svc.Namespace,
+				gatewayClusterName:    rcsw.clusterName,
+			}).Inc()
+
+			rcsw.log.Errorf("adding gateway update event %s with %d mirrro services", svc.Name, len(affectedServices))
+			rcsw.eventsQueue.Add(&RemoteGatewayUpdated{
+				gatewaySpec:      *spec,
+				affectedServices: affectedServices,
+			})
+		}
+	}
+}
diff --git a/controller/cmd/service-mirror/config_watcher.go b/controller/cmd/service-mirror/config_watcher.go
index 3d9f9283b..c71543aca 100644
--- a/controller/cmd/service-mirror/config_watcher.go
+++ b/controller/cmd/service-mirror/config_watcher.go
@@ -3,6 +3,7 @@ package servicemirror
 import (
 	"fmt"
 	"sync"
+	"time"
 
 	"github.com/linkerd/linkerd2/controller/k8s"
 	log "github.com/sirupsen/logrus"
@@ -21,16 +22,18 @@ type RemoteClusterConfigWatcher struct {
 	k8sAPI                 *k8s.API
 	clusterWatchers        map[string]*RemoteClusterServiceWatcher
 	requeueLimit           int
+	repairPeriod           time.Duration
 	sync.RWMutex
 }
 
 // NewRemoteClusterConfigWatcher Creates a new config watcher
-func NewRemoteClusterConfigWatcher(serviceMirrorNamespace string, secretsInformer cache.SharedIndexInformer, k8sAPI *k8s.API, requeueLimit int) *RemoteClusterConfigWatcher {
+func NewRemoteClusterConfigWatcher(serviceMirrorNamespace string, secretsInformer cache.SharedIndexInformer, k8sAPI *k8s.API, requeueLimit int, repairPeriod time.Duration) *RemoteClusterConfigWatcher {
 	rcw := &RemoteClusterConfigWatcher{
 		serviceMirrorNamespace: serviceMirrorNamespace,
 		k8sAPI:                 k8sAPI,
 		clusterWatchers:        map[string]*RemoteClusterServiceWatcher{},
 		requeueLimit:           requeueLimit,
+		repairPeriod:           repairPeriod,
 	}
 	secretsInformer.AddEventHandler(
 		cache.FilteringResourceEventHandler{
@@ -126,7 +129,7 @@ func (rcw *RemoteClusterConfigWatcher) registerRemoteCluster(secret *corev1.Secr
 		return fmt.Errorf("there is already a cluster with name %s being watcher. Please delete its config before attempting to register a new one", config.ClusterName)
 	}
 
-	watcher, err := NewRemoteClusterServiceWatcher(rcw.serviceMirrorNamespace, rcw.k8sAPI, clientConfig, config.ClusterName, rcw.requeueLimit, config.ClusterDomain)
+	watcher, err := NewRemoteClusterServiceWatcher(rcw.serviceMirrorNamespace, rcw.k8sAPI, clientConfig, config.ClusterName, rcw.requeueLimit, rcw.repairPeriod, config.ClusterDomain)
 	if err != nil {
 		return err
 	}
diff --git a/controller/cmd/service-mirror/events_formatting.go b/controller/cmd/service-mirror/events_formatting.go
index 6a2cba1bf..02058024c 100644
--- a/controller/cmd/service-mirror/events_formatting.go
+++ b/controller/cmd/service-mirror/events_formatting.go
@@ -113,6 +113,10 @@ func (od OnDeleteCalled) String() string {
 	return fmt.Sprintf("OnDeleteCalled: {svc: %s}", formatService(od.svc))
 }
 
+func (re RepairEndpoints) String() string {
+	return "RepairEndpoints"
+}
+
 //Events for probe manager
 
 func (ps probeSpec) String() string {
diff --git a/controller/cmd/service-mirror/main.go b/controller/cmd/service-mirror/main.go
index 831d8cf66..3fc6476e3 100644
--- a/controller/cmd/service-mirror/main.go
+++ b/controller/cmd/service-mirror/main.go
@@ -55,6 +55,7 @@ func Main(args []string) {
 	requeueLimit := cmd.Int("event-requeue-limit", 3, "requeue limit for events")
 	metricsAddr := cmd.String("metrics-addr", ":9999", "address to serve scrapable metrics on")
 	namespace := cmd.String("namespace", "", "address to serve scrapable metrics on")
+	repairPeriod := cmd.Duration("endpoint-refresh-period", 1*time.Minute, "frequency to refresh endpoint resolution")
 
 	flags.ConfigureAndParse(cmd, args)
 
@@ -88,7 +89,7 @@ func Main(args []string) {
 	probeManager.Start()
 
 	k8sAPI.Sync(nil)
-	watcher := NewRemoteClusterConfigWatcher(*namespace, secretsInformer, k8sAPI, *requeueLimit)
+	watcher := NewRemoteClusterConfigWatcher(*namespace, secretsInformer, k8sAPI, *requeueLimit, *repairPeriod)
 	log.Info("Started cluster config watcher")
 
 	go admin.StartServer(*metricsAddr)
diff --git a/controller/cmd/service-mirror/metrics.go b/controller/cmd/service-mirror/metrics.go
index a5b644678..97f027b5a 100644
--- a/controller/cmd/service-mirror/metrics.go
+++ b/controller/cmd/service-mirror/metrics.go
@@ -29,6 +29,18 @@ type probeMetrics struct {
 	unregister func()
 }
 
+var endpointRepairCounter *prometheus.CounterVec
+
+func init() {
+	endpointRepairCounter = promauto.NewCounterVec(
+		prometheus.CounterOpts{
+			Name: "service_mirror_endpoint_repairs",
+			Help: "Increments when the service mirror controller attempts to repair mirror endpoints",
+		},
+		[]string{gatewayNameLabel, gatewayNamespaceLabel, gatewayClusterName},
+	)
+}
+
 func newProbeMetricVecs() probeMetricVecs {
 	labelNames := []string{gatewayNameLabel, gatewayNamespaceLabel, gatewayClusterName}
 
diff --git a/web/Dockerfile b/web/Dockerfile
index e442dafad..3e4335a92 100644
--- a/web/Dockerfile
+++ b/web/Dockerfile
@@ -1,5 +1,5 @@
 ## bundle web assets
-FROM node:14-stretch as webpack-bundle
+FROM node:14-buster as webpack-bundle
 RUN curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.21.1 --network-concurrency 1
 
 ENV PATH /root/.yarn/bin:$PATH
@@ -32,7 +32,7 @@ COPY pkg pkg
 RUN CGO_ENABLED=0 GOOS=linux go build -mod=readonly -o web/web -ldflags "-s -w" ./web
 
 ## package it all up
-FROM debian:stretch-20190812-slim
+FROM debian:buster-20200514-slim
 WORKDIR /linkerd
 
 COPY LICENSE .


Test patch:
diff --git a/cli/cmd/testdata/inject_tap_deployment.input.yml b/cli/cmd/testdata/inject_tap_deployment.input.yml
index ce06ad797..b330a9271 100644
--- a/cli/cmd/testdata/inject_tap_deployment.input.yml
+++ b/cli/cmd/testdata/inject_tap_deployment.input.yml
@@ -205,7 +205,7 @@ spec:
         - 4190,4191
         - --outbound-ports-to-ignore
         - "443"
-        image: gcr.io/linkerd-io/proxy-init:v1.3.2
+        image: gcr.io/linkerd-io/proxy-init:v1.3.3
         imagePullPolicy: IfNotPresent
         name: linkerd-init
         resources:
diff --git a/cli/cmd/testdata/install_addon_config.golden b/cli/cmd/testdata/install_addon_config.golden
index 619b80c93..d426cf315 100644
--- a/cli/cmd/testdata/install_addon_config.golden
+++ b/cli/cmd/testdata/install_addon_config.golden
@@ -276,6 +276,9 @@ rules:
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
+- apiGroups: ["apiregistration.k8s.io"]
+  resources: ["apiservices"]
+  verbs: ["get"]
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
diff --git a/cli/cmd/testdata/install_config.golden b/cli/cmd/testdata/install_config.golden
index daa3258fe..b33dfcb6f 100644
--- a/cli/cmd/testdata/install_config.golden
+++ b/cli/cmd/testdata/install_config.golden
@@ -276,6 +276,9 @@ rules:
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
+- apiGroups: ["apiregistration.k8s.io"]
+  resources: ["apiservices"]
+  verbs: ["get"]
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
diff --git a/cli/cmd/testdata/install_controlplane_tracing_output.golden b/cli/cmd/testdata/install_controlplane_tracing_output.golden
index fb5c9b1e5..b634e059e 100644
--- a/cli/cmd/testdata/install_controlplane_tracing_output.golden
+++ b/cli/cmd/testdata/install_controlplane_tracing_output.golden
@@ -276,6 +276,9 @@ rules:
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
+- apiGroups: ["apiregistration.k8s.io"]
+  resources: ["apiservices"]
+  verbs: ["get"]
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
diff --git a/cli/cmd/testdata/install_custom_registry.golden b/cli/cmd/testdata/install_custom_registry.golden
index d5ec7ff4f..c11930743 100644
--- a/cli/cmd/testdata/install_custom_registry.golden
+++ b/cli/cmd/testdata/install_custom_registry.golden
@@ -276,6 +276,9 @@ rules:
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
+- apiGroups: ["apiregistration.k8s.io"]
+  resources: ["apiservices"]
+  verbs: ["get"]
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
diff --git a/cli/cmd/testdata/install_default.golden b/cli/cmd/testdata/install_default.golden
index 352bfcebb..a1e93eb3b 100644
--- a/cli/cmd/testdata/install_default.golden
+++ b/cli/cmd/testdata/install_default.golden
@@ -276,6 +276,9 @@ rules:
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
+- apiGroups: ["apiregistration.k8s.io"]
+  resources: ["apiservices"]
+  verbs: ["get"]
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
diff --git a/cli/cmd/testdata/install_grafana_existing.golden b/cli/cmd/testdata/install_grafana_existing.golden
index 698a37c4d..6f92f2083 100644
--- a/cli/cmd/testdata/install_grafana_existing.golden
+++ b/cli/cmd/testdata/install_grafana_existing.golden
@@ -276,6 +276,9 @@ rules:
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
+- apiGroups: ["apiregistration.k8s.io"]
+  resources: ["apiservices"]
+  verbs: ["get"]
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
diff --git a/cli/cmd/testdata/install_ha_output.golden b/cli/cmd/testdata/install_ha_output.golden
index 42529e962..41f8874f9 100644
--- a/cli/cmd/testdata/install_ha_output.golden
+++ b/cli/cmd/testdata/install_ha_output.golden
@@ -276,6 +276,9 @@ rules:
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
+- apiGroups: ["apiregistration.k8s.io"]
+  resources: ["apiservices"]
+  verbs: ["get"]
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
diff --git a/cli/cmd/testdata/install_ha_with_overrides_output.golden b/cli/cmd/testdata/install_ha_with_overrides_output.golden
index d4daaf2fc..0ffcaf205 100644
--- a/cli/cmd/testdata/install_ha_with_overrides_output.golden
+++ b/cli/cmd/testdata/install_ha_with_overrides_output.golden
@@ -276,6 +276,9 @@ rules:
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
+- apiGroups: ["apiregistration.k8s.io"]
+  resources: ["apiservices"]
+  verbs: ["get"]
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
diff --git a/cli/cmd/testdata/install_heartbeat_disabled_output.golden b/cli/cmd/testdata/install_heartbeat_disabled_output.golden
index 2d16715bc..5243a7beb 100644
--- a/cli/cmd/testdata/install_heartbeat_disabled_output.golden
+++ b/cli/cmd/testdata/install_heartbeat_disabled_output.golden
@@ -235,6 +235,9 @@ rules:
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
+- apiGroups: ["apiregistration.k8s.io"]
+  resources: ["apiservices"]
+  verbs: ["get"]
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
diff --git a/cli/cmd/testdata/install_helm_output.golden b/cli/cmd/testdata/install_helm_output.golden
index 5b0e8706a..822071723 100644
--- a/cli/cmd/testdata/install_helm_output.golden
+++ b/cli/cmd/testdata/install_helm_output.golden
@@ -288,6 +288,9 @@ rules:
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
+- apiGroups: ["apiregistration.k8s.io"]
+  resources: ["apiservices"]
+  verbs: ["get"]
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
diff --git a/cli/cmd/testdata/install_helm_output_addons.golden b/cli/cmd/testdata/install_helm_output_addons.golden
index 525c948a7..ec4c48c78 100644
--- a/cli/cmd/testdata/install_helm_output_addons.golden
+++ b/cli/cmd/testdata/install_helm_output_addons.golden
@@ -288,6 +288,9 @@ rules:
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
+- apiGroups: ["apiregistration.k8s.io"]
+  resources: ["apiservices"]
+  verbs: ["get"]
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
diff --git a/cli/cmd/testdata/install_helm_output_ha.golden b/cli/cmd/testdata/install_helm_output_ha.golden
index 7f1c63242..a2071f2ad 100644
--- a/cli/cmd/testdata/install_helm_output_ha.golden
+++ b/cli/cmd/testdata/install_helm_output_ha.golden
@@ -288,6 +288,9 @@ rules:
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
+- apiGroups: ["apiregistration.k8s.io"]
+  resources: ["apiservices"]
+  verbs: ["get"]
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
diff --git a/cli/cmd/testdata/install_no_init_container.golden b/cli/cmd/testdata/install_no_init_container.golden
index b12d0c349..8974fca9a 100644
--- a/cli/cmd/testdata/install_no_init_container.golden
+++ b/cli/cmd/testdata/install_no_init_container.golden
@@ -276,6 +276,9 @@ rules:
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
+- apiGroups: ["apiregistration.k8s.io"]
+  resources: ["apiservices"]
+  verbs: ["get"]
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
diff --git a/cli/cmd/testdata/install_output.golden b/cli/cmd/testdata/install_output.golden
index b11f32f62..7c6e5cdeb 100644
--- a/cli/cmd/testdata/install_output.golden
+++ b/cli/cmd/testdata/install_output.golden
@@ -276,6 +276,9 @@ rules:
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
+- apiGroups: ["apiregistration.k8s.io"]
+  resources: ["apiservices"]
+  verbs: ["get"]
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
diff --git a/cli/cmd/testdata/install_proxy_ignores.golden b/cli/cmd/testdata/install_proxy_ignores.golden
index c0ba00e3a..8707a8966 100644
--- a/cli/cmd/testdata/install_proxy_ignores.golden
+++ b/cli/cmd/testdata/install_proxy_ignores.golden
@@ -276,6 +276,9 @@ rules:
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
+- apiGroups: ["apiregistration.k8s.io"]
+  resources: ["apiservices"]
+  verbs: ["get"]
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
diff --git a/cli/cmd/testdata/install_tracing.golden b/cli/cmd/testdata/install_tracing.golden
index 8554b7296..12569fe91 100644
--- a/cli/cmd/testdata/install_tracing.golden
+++ b/cli/cmd/testdata/install_tracing.golden
@@ -276,6 +276,9 @@ rules:
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
+- apiGroups: ["apiregistration.k8s.io"]
+  resources: ["apiservices"]
+  verbs: ["get"]
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
diff --git a/cli/cmd/testdata/install_tracing_overwrite.golden b/cli/cmd/testdata/install_tracing_overwrite.golden
index 92ebd389f..26b36a4ce 100644
--- a/cli/cmd/testdata/install_tracing_overwrite.golden
+++ b/cli/cmd/testdata/install_tracing_overwrite.golden
@@ -276,6 +276,9 @@ rules:
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
+- apiGroups: ["apiregistration.k8s.io"]
+  resources: ["apiservices"]
+  verbs: ["get"]
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
diff --git a/cli/cmd/testdata/upgrade_add-on_config.golden b/cli/cmd/testdata/upgrade_add-on_config.golden
index d6268abf3..f3011ac37 100644
--- a/cli/cmd/testdata/upgrade_add-on_config.golden
+++ b/cli/cmd/testdata/upgrade_add-on_config.golden
@@ -276,6 +276,9 @@ rules:
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
+- apiGroups: ["apiregistration.k8s.io"]
+  resources: ["apiservices"]
+  verbs: ["get"]
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
diff --git a/cli/cmd/testdata/upgrade_add-on_overwrite.golden b/cli/cmd/testdata/upgrade_add-on_overwrite.golden
index b88488afe..f62cabfb0 100644
--- a/cli/cmd/testdata/upgrade_add-on_overwrite.golden
+++ b/cli/cmd/testdata/upgrade_add-on_overwrite.golden
@@ -276,6 +276,9 @@ rules:
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
+- apiGroups: ["apiregistration.k8s.io"]
+  resources: ["apiservices"]
+  verbs: ["get"]
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
diff --git a/cli/cmd/testdata/upgrade_add_add-on.golden b/cli/cmd/testdata/upgrade_add_add-on.golden
index aa9734cb9..4af364191 100644
--- a/cli/cmd/testdata/upgrade_add_add-on.golden
+++ b/cli/cmd/testdata/upgrade_add_add-on.golden
@@ -276,6 +276,9 @@ rules:
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
+- apiGroups: ["apiregistration.k8s.io"]
+  resources: ["apiservices"]
+  verbs: ["get"]
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
diff --git a/cli/cmd/testdata/upgrade_default.golden b/cli/cmd/testdata/upgrade_default.golden
index 78321a14e..191ea71d5 100644
--- a/cli/cmd/testdata/upgrade_default.golden
+++ b/cli/cmd/testdata/upgrade_default.golden
@@ -276,6 +276,9 @@ rules:
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
+- apiGroups: ["apiregistration.k8s.io"]
+  resources: ["apiservices"]
+  verbs: ["get"]
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
diff --git a/cli/cmd/testdata/upgrade_external_issuer.golden b/cli/cmd/testdata/upgrade_external_issuer.golden
index 89146ccd2..00324188b 100644
--- a/cli/cmd/testdata/upgrade_external_issuer.golden
+++ b/cli/cmd/testdata/upgrade_external_issuer.golden
@@ -276,6 +276,9 @@ rules:
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
+- apiGroups: ["apiregistration.k8s.io"]
+  resources: ["apiservices"]
+  verbs: ["get"]
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
diff --git a/cli/cmd/testdata/upgrade_grafana_addon_overwrite.yaml b/cli/cmd/testdata/upgrade_grafana_addon_overwrite.yaml
index 6bb5331f9..ad24c8adc 100644
--- a/cli/cmd/testdata/upgrade_grafana_addon_overwrite.yaml
+++ b/cli/cmd/testdata/upgrade_grafana_addon_overwrite.yaml
@@ -276,6 +276,9 @@ rules:
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
+- apiGroups: ["apiregistration.k8s.io"]
+  resources: ["apiservices"]
+  verbs: ["get"]
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
diff --git a/cli/cmd/testdata/upgrade_grafana_disabled.yaml b/cli/cmd/testdata/upgrade_grafana_disabled.yaml
index b26d60bba..329fbd6c8 100644
--- a/cli/cmd/testdata/upgrade_grafana_disabled.yaml
+++ b/cli/cmd/testdata/upgrade_grafana_disabled.yaml
@@ -276,6 +276,9 @@ rules:
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
+- apiGroups: ["apiregistration.k8s.io"]
+  resources: ["apiservices"]
+  verbs: ["get"]
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
diff --git a/cli/cmd/testdata/upgrade_grafana_enabled.yaml b/cli/cmd/testdata/upgrade_grafana_enabled.yaml
index 6bb5331f9..ad24c8adc 100644
--- a/cli/cmd/testdata/upgrade_grafana_enabled.yaml
+++ b/cli/cmd/testdata/upgrade_grafana_enabled.yaml
@@ -276,6 +276,9 @@ rules:
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
+- apiGroups: ["apiregistration.k8s.io"]
+  resources: ["apiservices"]
+  verbs: ["get"]
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
diff --git a/cli/cmd/testdata/upgrade_grafana_enabled_disabled.yaml b/cli/cmd/testdata/upgrade_grafana_enabled_disabled.yaml
index b26d60bba..329fbd6c8 100644
--- a/cli/cmd/testdata/upgrade_grafana_enabled_disabled.yaml
+++ b/cli/cmd/testdata/upgrade_grafana_enabled_disabled.yaml
@@ -276,6 +276,9 @@ rules:
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
+- apiGroups: ["apiregistration.k8s.io"]
+  resources: ["apiservices"]
+  verbs: ["get"]
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
diff --git a/cli/cmd/testdata/upgrade_grafana_overwrite.yaml b/cli/cmd/testdata/upgrade_grafana_overwrite.yaml
index f306255a2..bfebbeb6a 100644
--- a/cli/cmd/testdata/upgrade_grafana_overwrite.yaml
+++ b/cli/cmd/testdata/upgrade_grafana_overwrite.yaml
@@ -276,6 +276,9 @@ rules:
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
+- apiGroups: ["apiregistration.k8s.io"]
+  resources: ["apiservices"]
+  verbs: ["get"]
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
diff --git a/cli/cmd/testdata/upgrade_ha.golden b/cli/cmd/testdata/upgrade_ha.golden
index c615f0b81..99aabc523 100644
--- a/cli/cmd/testdata/upgrade_ha.golden
+++ b/cli/cmd/testdata/upgrade_ha.golden
@@ -276,6 +276,9 @@ rules:
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
+- apiGroups: ["apiregistration.k8s.io"]
+  resources: ["apiservices"]
+  verbs: ["get"]
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
diff --git a/cli/cmd/testdata/upgrade_ha_config.golden b/cli/cmd/testdata/upgrade_ha_config.golden
index 945b33b58..d0d1f39be 100644
--- a/cli/cmd/testdata/upgrade_ha_config.golden
+++ b/cli/cmd/testdata/upgrade_ha_config.golden
@@ -276,6 +276,9 @@ rules:
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
+- apiGroups: ["apiregistration.k8s.io"]
+  resources: ["apiservices"]
+  verbs: ["get"]
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
diff --git a/cli/cmd/testdata/upgrade_keep_webhook_cabundle.golden b/cli/cmd/testdata/upgrade_keep_webhook_cabundle.golden
index d60820304..e8ae1a04f 100644
--- a/cli/cmd/testdata/upgrade_keep_webhook_cabundle.golden
+++ b/cli/cmd/testdata/upgrade_keep_webhook_cabundle.golden
@@ -276,6 +276,9 @@ rules:
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
+- apiGroups: ["apiregistration.k8s.io"]
+  resources: ["apiservices"]
+  verbs: ["get"]
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
diff --git a/cli/cmd/testdata/upgrade_nothing_addon.yaml b/cli/cmd/testdata/upgrade_nothing_addon.yaml
index 6bb5331f9..ad24c8adc 100644
--- a/cli/cmd/testdata/upgrade_nothing_addon.yaml
+++ b/cli/cmd/testdata/upgrade_nothing_addon.yaml
@@ -276,6 +276,9 @@ rules:
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
+- apiGroups: ["apiregistration.k8s.io"]
+  resources: ["apiservices"]
+  verbs: ["get"]
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
diff --git a/cli/cmd/testdata/upgrade_overwrite_issuer.golden b/cli/cmd/testdata/upgrade_overwrite_issuer.golden
index 96edecd27..c30f3c87a 100644
--- a/cli/cmd/testdata/upgrade_overwrite_issuer.golden
+++ b/cli/cmd/testdata/upgrade_overwrite_issuer.golden
@@ -276,6 +276,9 @@ rules:
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
+- apiGroups: ["apiregistration.k8s.io"]
+  resources: ["apiservices"]
+  verbs: ["get"]
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
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 1ef0df2c1..53f187fde 100644
--- a/cli/cmd/testdata/upgrade_overwrite_trust_anchors-external-issuer.golden
+++ b/cli/cmd/testdata/upgrade_overwrite_trust_anchors-external-issuer.golden
@@ -276,6 +276,9 @@ rules:
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
+- apiGroups: ["apiregistration.k8s.io"]
+  resources: ["apiservices"]
+  verbs: ["get"]
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
diff --git a/cli/cmd/testdata/upgrade_overwrite_trust_anchors.golden b/cli/cmd/testdata/upgrade_overwrite_trust_anchors.golden
index 96edecd27..c30f3c87a 100644
--- a/cli/cmd/testdata/upgrade_overwrite_trust_anchors.golden
+++ b/cli/cmd/testdata/upgrade_overwrite_trust_anchors.golden
@@ -276,6 +276,9 @@ rules:
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
+- apiGroups: ["apiregistration.k8s.io"]
+  resources: ["apiservices"]
+  verbs: ["get"]
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
diff --git a/cli/cmd/testdata/upgrade_two_level_webhook_cert.golden b/cli/cmd/testdata/upgrade_two_level_webhook_cert.golden
index d0817cd7e..d2a7974b4 100644
--- a/cli/cmd/testdata/upgrade_two_level_webhook_cert.golden
+++ b/cli/cmd/testdata/upgrade_two_level_webhook_cert.golden
@@ -276,6 +276,9 @@ rules:
 - apiGroups: ["linkerd.io"]
   resources: ["serviceprofiles"]
   verbs: ["list"]
+- apiGroups: ["apiregistration.k8s.io"]
+  resources: ["apiservices"]
+  verbs: ["get"]
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
diff --git a/controller/cmd/service-mirror/cluster_watcher_mirroring_test.go b/controller/cmd/service-mirror/cluster_watcher_mirroring_test.go
index 468e18e0f..ce434f8e2 100644
--- a/controller/cmd/service-mirror/cluster_watcher_mirroring_test.go
+++ b/controller/cmd/service-mirror/cluster_watcher_mirroring_test.go
@@ -2,10 +2,12 @@ package servicemirror
 
 import (
 	"fmt"
+	"net"
 	"reflect"
 	"testing"
 
 	corev1 "k8s.io/api/core/v1"
+	v1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/client-go/util/workqueue"
 )
@@ -83,7 +85,7 @@ func (tc *mirroringTestCase) run(t *testing.T) {
 		for _, ev := range tc.expectedEventsInQueue {
 			evInQueue, _ := q.Get()
 			if !reflect.DeepEqual(ev, evInQueue) {
-				t.Fatalf("was expecting to see event %T but got %T", ev, evInQueue)
+				t.Fatalf("was expecting to see event %s but got %s", ev, evInQueue)
 			}
 		}
 	})
@@ -262,6 +264,12 @@ func TestRemoteServiceUpdatedMirroring(t *testing.T) {
 }
 
 func TestRemoteGatewayUpdatedMirroring(t *testing.T) {
+
+	localhostIP, err := net.ResolveIPAddr("ip", "localhost")
+	if err != nil {
+		t.Fatal(err)
+	}
+
 	for _, tt := range []mirroringTestCase{
 		{
 			description: "endpoints ports are updated on gateway change",
@@ -377,6 +385,28 @@ func TestRemoteGatewayUpdatedMirroring(t *testing.T) {
 						}}),
 			},
 		},
+		{
+			description: "gateway uses hostname address",
+			environment: remoteGatewayUpdatedWithHostnameAddress,
+			expectedEventsInQueue: []interface{}{
+				&RemoteGatewayUpdated{
+					gatewaySpec: GatewaySpec{
+						gatewayName:      "gateway",
+						gatewayNamespace: "gateway-ns",
+						clusterName:      "remote",
+						addresses:        []corev1.EndpointAddress{{IP: localhostIP.String()}},
+						incomingPort:     999,
+						resourceVersion:  "currentGatewayResVersion",
+						ProbeConfig: &ProbeConfig{
+							path:            defaultProbePath,
+							port:            defaultProbePort,
+							periodInSeconds: defaultProbePeriod,
+						},
+					},
+					affectedServices: []*v1.Service{},
+				},
+			},
+		},
 	} {
 		tc := tt // pin
 		tc.run(t)
diff --git a/controller/cmd/service-mirror/cluster_watcher_test_util.go b/controller/cmd/service-mirror/cluster_watcher_test_util.go
index c2605163f..b59dc0b61 100644
--- a/controller/cmd/service-mirror/cluster_watcher_test_util.go
+++ b/controller/cmd/service-mirror/cluster_watcher_test_util.go
@@ -104,7 +104,7 @@ var createServiceWrongGatewaySpec = &testEnvironment{
 		},
 	},
 	remoteResources: []string{
-		gatewayAsYaml("existing-gateway", "existing-namespace", "222", "192.0.2.127", "mc-wrong", 888, "", 111, "/path", 666),
+		gatewayAsYaml("existing-gateway", "existing-namespace", "222", "192.0.2.127", "", "mc-wrong", 888, "", 111, "/path", 666),
 	},
 }
 
@@ -130,7 +130,7 @@ var createServiceOkeGatewaySpec = &testEnvironment{
 		},
 	},
 	remoteResources: []string{
-		gatewayAsYaml("existing-gateway", "existing-namespace", "222", "192.0.2.127", "mc-gateway", 888, "gateway-identity", defaultProbePort, defaultProbePath, defaultProbePeriod),
+		gatewayAsYaml("existing-gateway", "existing-namespace", "222", "192.0.2.127", "", "mc-gateway", 888, "gateway-identity", defaultProbePort, defaultProbePath, defaultProbePeriod),
 	},
 }
 
@@ -193,7 +193,7 @@ var updateServiceToNewGateway = &testEnvironment{
 		},
 	},
 	remoteResources: []string{
-		gatewayAsYaml("gateway-new", "gateway-ns", "currentGatewayResVersion", "0.0.0.0", "mc-gateway", 999, "", defaultProbePort, defaultProbePath, defaultProbePeriod),
+		gatewayAsYaml("gateway-new", "gateway-ns", "currentGatewayResVersion", "0.0.0.0", "", "mc-gateway", 999, "", defaultProbePort, defaultProbePath, defaultProbePeriod),
 	},
 	localResources: []string{
 		mirroredServiceAsYaml("test-service-remote", "test-namespace", "gateway", "gateway-ns", "past", "pastGatewayResVersion", []corev1.ServicePort{
@@ -269,7 +269,7 @@ var updateServiceWithChangedPorts = &testEnvironment{
 		},
 	},
 	remoteResources: []string{
-		gatewayAsYaml("gateway", "gateway-ns", "currentGatewayResVersion", "192.0.2.127", "mc-gateway", 888, "", defaultProbePort, defaultProbePath, defaultProbePeriod),
+		gatewayAsYaml("gateway", "gateway-ns", "currentGatewayResVersion", "192.0.2.127", "", "mc-gateway", 888, "", defaultProbePort, defaultProbePath, defaultProbePeriod),
 	},
 	localResources: []string{
 		mirroredServiceAsYaml("test-service-remote", "test-namespace", "gateway", "gateway-ns", "past", "pastGatewayResVersion", []corev1.ServicePort{
@@ -378,6 +378,15 @@ var remoteGatewayUpdated = &testEnvironment{
 	},
 }
 
+var remoteGatewayUpdatedWithHostnameAddress = &testEnvironment{
+	events: []interface{}{
+		&RepairEndpoints{},
+	},
+	remoteResources: []string{
+		gatewayAsYaml("gateway", "gateway-ns", "currentGatewayResVersion", "", "localhost", "mc-gateway", 999, "", defaultProbePort, defaultProbePath, defaultProbePeriod),
+	},
+}
+
 var gatewayAddressChanged = &testEnvironment{
 	events: []interface{}{
 		&RemoteGatewayUpdated{
@@ -774,7 +783,7 @@ func mirroredServiceAsYaml(name, namespace, gtwName, gtwNs, resourceVersion, gat
 	return string(bytes)
 }
 
-func gateway(name, namespace, resourceVersion, ip, portName string, port int32, identity string, probePort int32, probePath string, probePeriod int) *corev1.Service {
+func gateway(name, namespace, resourceVersion, ip, hostname, portName string, port int32, identity string, probePort int32, probePath string, probePeriod int) *corev1.Service {
 	svc := corev1.Service{
 		TypeMeta: metav1.TypeMeta{
 			Kind:       "Service",
@@ -785,9 +794,10 @@ func gateway(name, namespace, resourceVersion, ip, portName string, port int32,
 			Namespace:       namespace,
 			ResourceVersion: resourceVersion,
 			Annotations: map[string]string{
-				consts.GatewayIdentity:    identity,
-				consts.GatewayProbePath:   probePath,
-				consts.GatewayProbePeriod: fmt.Sprint(probePeriod),
+				consts.GatewayIdentity:               identity,
+				consts.GatewayProbePath:              probePath,
+				consts.GatewayProbePeriod:            fmt.Sprint(probePeriod),
+				consts.MulticlusterGatewayAnnotation: "true",
 			},
 		},
 		Spec: corev1.ServiceSpec{
@@ -809,11 +819,14 @@ func gateway(name, namespace, resourceVersion, ip, portName string, port int32,
 	if ip != "" {
 		svc.Status.LoadBalancer.Ingress = append(svc.Status.LoadBalancer.Ingress, corev1.LoadBalancerIngress{IP: ip})
 	}
+	if hostname != "" {
+		svc.Status.LoadBalancer.Ingress = append(svc.Status.LoadBalancer.Ingress, corev1.LoadBalancerIngress{Hostname: hostname})
+	}
 	return &svc
 }
 
-func gatewayAsYaml(name, namespace, resourceVersion, ip, portName string, port int32, identity string, probePort int32, probePath string, probePeriod int) string {
-	gtw := gateway(name, namespace, resourceVersion, ip, portName, port, identity, probePort, probePath, probePeriod)
+func gatewayAsYaml(name, namespace, resourceVersion, ip, hostname, portName string, port int32, identity string, probePort int32, probePath string, probePeriod int) string {
+	gtw := gateway(name, namespace, resourceVersion, ip, hostname, portName, port, identity, probePort, probePath, probePeriod)
 
 	bytes, err := yaml.Marshal(gtw)
 	if err != nil {