Repository: linkerd/linkerd2
Pull request #4297 (2020-04-27 05:51:08)

Refactor TestRenderHelm to not need add-ons list
As we already have `l5dcharts.parseAddOnValues` which takes a `values` and builds a list of enabled add-ons. We can use it, instead of passing a list of add-ons for each test.

**changes include**
- update `chartControlPlane` to take `addOnConfig` to build add-on list based of it.

Signed-off-by: Tarun Pothulapati <tarunpothulapati@outlook.com>

<!--  Thanks for sending a pull request!

If you already have a well-structured git commit message, chances are GitHub
set the title and description of this PR to the git commit message subject and
body, respectively. If so, you may delete these instructions and submit your PR.

If this is your first time, please read our contributor guide:
https://github.com/linkerd/linkerd2/blob/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/workflows/static_checks.yml b/.github/workflows/static_checks.yml
index c62280d02..8fda3d078 100644
--- a/.github/workflows/static_checks.yml
+++ b/.github/workflows/static_checks.yml
@@ -113,4 +113,6 @@ jobs:
         ! -name test-scale \
         ! -name update-go-deps-shas \
         ! -name web \
+        ! -name *.nuspec \
+        ! -name *.ps1 \
         | xargs -I {} bin/shellcheck -x {}
diff --git a/bin/_tag.sh b/bin/_tag.sh
index 8764433a9..07ed6ef4f 100644
--- a/bin/_tag.sh
+++ b/bin/_tag.sh
@@ -17,7 +17,9 @@ clean_head() {
 }
 
 named_tag() {
-    echo "$(git name-rev --tags --name-only $(git_sha_head))"
+    tag="$(git name-rev --tags --name-only $(git_sha_head))"
+    tag=${tag%"^0"}
+    echo "${tag}"
 }
 
 head_root_tag() {
diff --git a/bin/helm-build b/bin/helm-build
index bd0ababc2..d1190bfc9 100755
--- a/bin/helm-build
+++ b/bin/helm-build
@@ -6,6 +6,7 @@ setValues() {
     sed -i "s/$1/$2/" charts/linkerd2/values.yaml
     sed -i "s/$1/$2/" charts/linkerd2-cni/values.yaml
     sed -i "s/$1/$2/" charts/linkerd2-service-mirror/values.yaml
+    sed -i "s/$1/$2/" charts/linkerd2-multicluster-remote-setup/values.yaml
 }
 
 showErr() {
@@ -20,6 +21,7 @@ bindir=$( cd "${BASH_SOURCE[0]%/*}" && pwd )
 rootdir=$( cd "$bindir"/.. && pwd )
 
 "$bindir"/helm init --client-only
+"$bindir"/helm lint "$rootdir"/charts/linkerd2-multicluster-remote-setup
 "$bindir"/helm lint "$rootdir"/charts/linkerd2-service-mirror
 "$bindir"/helm lint "$rootdir"/charts/partials
 "$bindir"/helm dep up "$rootdir"/charts/linkerd2-cni
@@ -52,6 +54,7 @@ if [ "$1" = package ]; then
     "$bindir"/helm --version "$version" --app-version "$tag" -d "$rootdir"/target/helm package "$rootdir"/charts/linkerd2-cni
     # TODO: When ready to publish, uncomment
     #"$bindir"/helm --version $version --app-version $tag -d "$rootdir"/target/helm package "$rootdir"/charts/linkerd2-service-mirror
+    #"$bindir"/helm --version $version --app-version $tag -d "$rootdir"/target/helm package "$rootdir"/charts/linkerd2-multicluster-remote-setup
     mv "$rootdir"/target/helm/index-pre.yaml "$rootdir"/target/helm/index-pre-"$version".yaml
     "$bindir"/helm repo index --url "https://helm.linkerd.io/$repo/" --merge "$rootdir"/target/helm/index-pre-"$version".yaml "$rootdir"/target/helm
 
diff --git a/bin/web b/bin/web
index 7fa729c8f..171a2c2e5 100755
--- a/bin/web
+++ b/bin/web
@@ -103,7 +103,7 @@ setup() {
 
 function test {
   cd "$ROOT"/web/app
-  yarn jest "$*"
+  yarn jest $*
 }
 
 integration() {
diff --git a/bin/win/linkerd.nuspec b/bin/win/linkerd.nuspec
new file mode 100644
index 000000000..1cbc73963
--- /dev/null
+++ b/bin/win/linkerd.nuspec
@@ -0,0 +1,32 @@
+﻿<?xml version="1.0" encoding="utf-8"?>
+<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
+  <metadata>
+    <id>linkerd</id>
+    <version>${LINKERD_VERSION}</version>
+    
+    <title>Linkerd</title>
+    <authors>Linkerd</authors>
+    <summary>Ultralight service mesh for Kubernetes and beyond</summary>
+    <description>
+    Linkerd is an ultralight service mesh for Kubernetes. 
+    It gives you observability, reliability, and security without requiring any code changes.
+
+    Package Parameters
+    - `path`: Specify path for linkerd-cli installation. If not specified, uses environment variable `linkerdPath`. Uses default chocolatey install path if none provided. 
+    - `checksum`: Specify the checksum for linkerd-cli. If not specified, uses environment variable `linkerdCheckSum`. Defaults to `null` if no checksum provided and skips verification. Do ensure to use the correct checksum for the version of install. 
+    </description>
+
+    <projectUrl>https://github.com/linkerd/linkerd2</projectUrl>
+    <licenseUrl>https://github.com/linkerd/linkerd2/blob/master/LICENSE</licenseUrl>
+    <releaseNotes>https://github.com/linkerd/linkerd2/releases</releaseNotes>
+    <docsUrl>https://linkerd.io/docs</docsUrl>
+
+    <dependencies>
+      <dependency id="chocolatey-core.extension" version="1.1.0" />
+    </dependencies>
+  </metadata>
+
+  <files>
+    <file src="bin\win\tools\**" target="tools" />
+    </files>
+</package>
\ No newline at end of file
diff --git a/bin/win/tools/chocolateyinstall.ps1 b/bin/win/tools/chocolateyinstall.ps1
new file mode 100644
index 000000000..6c05e3924
--- /dev/null
+++ b/bin/win/tools/chocolateyinstall.ps1
@@ -0,0 +1,33 @@
+﻿$ErrorActionPreference = 'Stop';
+$toolsPath = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)"
+
+$version = $env:chocolateyPackageVersion
+$pp = Get-PackageParameters
+
+if ($null -ne $pp['path']){
+	$lpath = $pp['path']
+}
+elseif (Test-Path env:linkerdPath) {
+	$lpath = $env:linkerdPath
+}
+else {
+	$lpath = $toolsPath
+}
+
+if ($null -ne $pp['checksum']){
+	$checksum = $pp['checksum']
+}
+else{
+	$checksum = $env:linkerdCheckSum
+}
+
+$packageArgs = @{
+  packageName    = 'linkerd'
+  fileFullPath   = "$lpath\linkerd.exe"
+  url64          = "https://github.com/linkerd/linkerd2/releases/download/stable-$version/linkerd2-cli-stable-$version-windows.exe"
+  checksum       = $checksum
+  checksumType   = 'sha256'
+}
+
+Get-ChocolateyWebFile @packageArgs
+Install-ChocolateyPath $packageArgs.fileFullPath 'User'
diff --git a/charts/linkerd2-multicluster-remote-setup/.helmignore b/charts/linkerd2-multicluster-remote-setup/.helmignore
new file mode 100644
index 000000000..79c90a806
--- /dev/null
+++ b/charts/linkerd2-multicluster-remote-setup/.helmignore
@@ -0,0 +1,22 @@
+# Patterns to ignore when building packages.
+# This supports shell glob matching, relative path matching, and
+# negation (prefixed with !). Only one pattern per line.
+.DS_Store
+OWNERS
+# Common VCS dirs
+.git/
+.gitignore
+.bzr/
+.bzrignore
+.hg/
+.hgignore
+.svn/
+# Common backup files
+*.swp
+*.bak
+*.tmp
+*~
+# Various IDEs
+.project
+.idea/
+*.tmproj
diff --git a/charts/linkerd2-multicluster-remote-setup/Chart.yaml b/charts/linkerd2-multicluster-remote-setup/Chart.yaml
new file mode 100644
index 000000000..ba826153e
--- /dev/null
+++ b/charts/linkerd2-multicluster-remote-setup/Chart.yaml
@@ -0,0 +1,7 @@
+apiVersion: v1
+appVersion: edge-XX.X.X
+description: A helm chart containing the resources to enable mirroring of services on remote clusters
+kubeVersion: ">=1.13.0-0"
+icon: https://linkerd.io/images/logo-only-200h.png
+name: "linkerd2-multicluster-remote-setup"
+version: 0.1.0
\ No newline at end of file
diff --git a/charts/linkerd2-multicluster-remote-setup/README.md b/charts/linkerd2-multicluster-remote-setup/README.md
new file mode 100644
index 000000000..3eb9a30d2
--- /dev/null
+++ b/charts/linkerd2-multicluster-remote-setup/README.md
@@ -0,0 +1,27 @@
+
+# Linkerd2-multicluster-remote-setup Helm Chart
+
+Linkerd is a *service mesh*, designed to give platform-wide observability,
+reliability, and security without requiring configuration or code changes.
+This chart provides a reference cluster gateway implementation, which coupled
+with Linkerd and the Service Mirror component can enable multicluster 
+communication and service discovery
+
+## Configuration
+
+The following table lists the configurable parameters of the linkerd2-multicluster-remote-setup chart and their default values.
+
+| Parameter                | Description                                                                                                     | Default                |
+|--------------------------|-----------------------------------------------------------------------------------------------------------------|------------------------|
+|`gatewayName`             | The name of the gateway that will be installed                                                                  | `linkerd-gateway`      |
+|`gatewayNamespace`        | The namespace in which the gateway will be created                                                              |`linkerd-gateway`       |
+|`identityTrustDomain`     | Trust domain used for identity of the existing linkerd installation                                             |`cluster.local`         |
+|`incomingPort`            | The port on which all the gateway will accept incoming traffic                                                  |`80`                    |
+|`linkerdNamespace`        | The namespace of the existing Linkerd installation                                                              |`linkerd`               |
+|`nginxImage`              | The Nginx image                                                                                                 |`nginx`                 |
+|`nginxImageVersion`       | The version of the Nginx image                                                                                  |`1.17`                  |
+|`probePath`               | The path that will be used by remote clusters for determining whether the gateway is alive                  |`/health`               |
+|`probePeriodSeconds`      | The interval (in seconds) between liveness probes                                                               |`3`                     |
+|`probePort`               | The port used for liveliness probing                                                                            |`81`                    |
+|`serviceAccountName`      | The name of the service account that will be created and used by remote clusters, attempting to mirror services |`linkerd-service-mirror`|
+|`serviceAccountNamespace` | The namespace in which the service account will be created                                                      |`linkerd-service-mirror`|
diff --git a/charts/linkerd2-multicluster-remote-setup/templates/gateway.yaml b/charts/linkerd2-multicluster-remote-setup/templates/gateway.yaml
new file mode 100644
index 000000000..16173bb2f
--- /dev/null
+++ b/charts/linkerd2-multicluster-remote-setup/templates/gateway.yaml
@@ -0,0 +1,114 @@
+---
+kind: Namespace
+apiVersion: v1
+metadata:
+  name: {{ .Values.gatewayNamespace }}
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: nginx-configuration
+  annotations:
+    {{.Values.createdByAnnotation}}: {{default (printf "linkerd/helm %s" .Values.linkerdVersion) .Values.cliVersion}}
+  namespace: {{.Values.gatewayNamespace}}
+data:
+  nginx.conf: |-
+    events {
+    }
+    stream {
+      server {
+          listen     {{.Values.incomingPort}};
+          proxy_pass 127.0.0.1:{{.Values.proxyOutboundPort}};
+      }
+    }
+    http {
+      server {
+          listen     {{.Values.probePort}};
+          location {{.Values.probePath}} {
+            access_log off;
+            return 200 "healthy\n";
+          }
+      }
+    }
+---    
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  annotations:
+    {{.Values.createdByAnnotation}}: {{default (printf "linkerd/helm %s" .Values.linkerdVersion) .Values.cliVersion}}
+  labels:
+    app.kubernetes.io/name: gateway
+    app.kubernetes.io/part-of: Linkerd
+    app.kubernetes.io/version: {{.Values.linkerdVersion}}
+    app: {{.Values.gatewayName}}
+  name: {{.Values.gatewayName}}
+  namespace: {{.Values.gatewayNamespace}}
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: {{.Values.gatewayName}}
+  template:
+    metadata:
+      annotations:
+        {{.Values.createdByAnnotation}}: {{default (printf "linkerd/helm %s" .Values.linkerdVersion) .Values.cliVersion}}        
+        linkerd.io/inject: enabled
+      labels:
+        app: {{.Values.gatewayName}}
+    spec:
+      volumes:
+        - name: config
+          configMap:
+            name: nginx-configuration   
+      containers:
+        - name: nginx
+          readinessProbe:
+            failureThreshold: 7
+            httpGet:
+              path: {{.Values.probePath}}
+              port: {{.Values.probePort}}
+          livenessProbe:
+            httpGet:
+              path: {{.Values.probePath}}
+              port: {{.Values.probePort}}
+            initialDelaySeconds: 10  
+          image: {{.Values.nginxImage}}:{{.Values.nginxImageVersion}}
+          ports:
+            - name: incoming-port
+              containerPort: {{.Values.incomingPort}}
+            - name: probe-port
+              containerPort: {{.Values.probePort}}             
+          volumeMounts:
+            - name: config
+              mountPath: /etc/nginx
+      serviceAccountName: {{.Values.gatewayName}}
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: {{.Values.gatewayName}}
+  namespace: {{.Values.gatewayNamespace}}
+  annotations: 
+    mirror.linkerd.io/gateway-identity: {{.Values.gatewayName}}.{{.Values.gatewayNamespace}}.serviceaccount.identity.{{.Values.linkerdNamespace}}.{{.Values.identityTrustDomain}}
+    mirror.linkerd.io/probe-port: "{{.Values.probePort}}"
+    mirror.linkerd.io/probe-period: "{{.Values.probePeriodSeconds}}"
+    mirror.linkerd.io/probe-path: {{.Values.probePath}}
+    {{.Values.createdByAnnotation}}: {{default (printf "linkerd/helm %s" .Values.linkerdVersion) .Values.cliVersion}}
+spec:
+  ports:
+  - name: incoming-port
+    port: {{.Values.incomingPort}}    
+    protocol: TCP
+  - name: probe-port
+    port: {{.Values.probePort}}    
+    protocol: TCP
+  selector:
+    app: {{.Values.gatewayName}}
+  type: LoadBalancer
+---
+kind: ServiceAccount
+apiVersion: v1
+metadata:
+  name: {{.Values.gatewayName}}
+  namespace: {{.Values.gatewayNamespace}}
+  
diff --git a/charts/linkerd2-multicluster-remote-setup/templates/service-mirror-rbac.yaml b/charts/linkerd2-multicluster-remote-setup/templates/service-mirror-rbac.yaml
new file mode 100644
index 000000000..adf8f764b
--- /dev/null
+++ b/charts/linkerd2-multicluster-remote-setup/templates/service-mirror-rbac.yaml
@@ -0,0 +1,47 @@
+---
+kind: Namespace
+apiVersion: v1
+metadata:
+  name: {{ .Values.serviceAccountNamespace }}
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  name: {{.Values.serviceAccountName}}
+  namespace: {{.Values.serviceAccountNamespace}}
+  annotations:
+    {{.Values.createdByAnnotation}}: {{default (printf "linkerd/helm %s" .Values.linkerdVersion) .Values.cliVersion}}
+rules:
+- apiGroups:
+  - ""
+  resources:
+  - services
+  verbs:
+  - list
+  - get
+  - watch
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: {{.Values.serviceAccountName}}
+  namespace: {{.Values.serviceAccountNamespace}}
+  annotations:
+    {{.Values.createdByAnnotation}}: {{default (printf "linkerd/helm %s" .Values.linkerdVersion) .Values.cliVersion}}
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  name: {{.Values.serviceAccountName}}
+  namespace: {{.Values.serviceAccountNamespace}}
+  annotations:
+    {{.Values.createdByAnnotation}}: {{default (printf "linkerd/helm %s" .Values.linkerdVersion) .Values.cliVersion}}
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: {{.Values.serviceAccountName}}
+subjects:
+- kind: ServiceAccount
+  name: {{.Values.serviceAccountName}}
+  namespace: {{.Values.serviceAccountNamespace}}
+
diff --git a/charts/linkerd2-multicluster-remote-setup/values.yaml b/charts/linkerd2-multicluster-remote-setup/values.yaml
new file mode 100644
index 000000000..85abfd267
--- /dev/null
+++ b/charts/linkerd2-multicluster-remote-setup/values.yaml
@@ -0,0 +1,15 @@
+gatewayName: linkerd-gateway
+gatewayNamespace: linkerd-gateway
+identityTrustDomain: cluster.local
+incomingPort: 80
+linkerdNamespace: linkerd
+probePath: /health
+probePeriodSeconds: 3
+probePort: 81
+serviceAccountName: linkerd-service-mirror
+serviceAccountNamespace: linkerd-service-mirror
+linkerdVersion: {version}
+createdByAnnotation: linkerd.io/created-by
+nginxImageVersion: 1.17
+nginxImage: nginx
+proxyOutboundPort: 4140
\ No newline at end of file
diff --git a/charts/linkerd2-service-mirror/README.md b/charts/linkerd2-service-mirror/README.md
index 4450fdf8c..4fcdd751a 100644
--- a/charts/linkerd2-service-mirror/README.md
+++ b/charts/linkerd2-service-mirror/README.md
@@ -13,6 +13,7 @@ The following table lists the configurable parameters of the linkerd2-service-mi
 
 | Parameter                            | Description                                                                       | Default                       |
 |--------------------------------------|-----------------------------------------------------------------------------------|-------------------------------|
+|`controllerComponentLabel`            | Control plane label. Do not edit                                                  | `linkerd.io/control-plane-component`|
 |`controllerImage`                     | Docker image for the Service mirror component (uses the Linkerd controller image) |`gcr.io/linkerd-io/controller`|
 |`controllerImageVersion`              | Tag for the Service Mirror container Docker image                                 |latest version|
 |`namespace`                           | Service Mirror component namespace                                                |`linkerd-service-mirror`|
diff --git a/charts/linkerd2-service-mirror/templates/service-mirror.yaml b/charts/linkerd2-service-mirror/templates/service-mirror.yaml
index 0f66a81a1..abf10511b 100644
--- a/charts/linkerd2-service-mirror/templates/service-mirror.yaml
+++ b/charts/linkerd2-service-mirror/templates/service-mirror.yaml
@@ -4,7 +4,7 @@ metadata:
   name: linkerd-service-mirror
   namespace: {{.Values.namespace}}
   labels:
-    ControllerComponentLabel: service-mirror
+    {{.Values.controllerComponentLabel}}: linkerd-service-mirror
 rules:
 - apiGroups: [""]
   resources: ["endpoints", "services", "secrets", "namespaces"]
@@ -16,7 +16,7 @@ metadata:
   name: linkerd-service-mirror
   namespace: {{.Values.namespace}}
   labels:
-    ControllerComponentLabel: service-mirror
+    {{.Values.controllerComponentLabel}}: linkerd-service-mirror
 roleRef:
   apiGroup: rbac.authorization.k8s.io
   kind: ClusterRole
@@ -32,24 +32,24 @@ metadata:
   name: linkerd-service-mirror
   namespace: {{.Values.namespace}}
   labels:
-    ControllerComponentLabel: service-mirror
+    {{.Values.controllerComponentLabel}}: linkerd-service-mirror
 ---
 apiVersion: apps/v1
 kind: Deployment
 metadata:
   labels:
-    ControllerComponentLabel: service-mirror
+    {{.Values.controllerComponentLabel}}: linkerd-service-mirror
   name: linkerd-service-mirror
   namespace: {{.Values.namespace}}
 spec:
   replicas: 1
   selector:
     matchLabels:
-      ControllerComponentLabel: service-mirror
+      {{.Values.controllerComponentLabel}}: linkerd-service-mirror
   template:
     metadata:
       labels:
-        ControllerComponentLabel: service-mirror
+        {{.Values.controllerComponentLabel}}: linkerd-service-mirror
     spec:
       containers:
       - args:
@@ -60,4 +60,7 @@ spec:
         name: service-mirror
         securityContext:
           runAsUser: {{.Values.serviceMirrorUID}}
+        ports:
+        - containerPort: 9999
+          name: admin-http
       serviceAccountName: linkerd-service-mirror
diff --git a/charts/linkerd2-service-mirror/values.yaml b/charts/linkerd2-service-mirror/values.yaml
index 6aff9f3fa..652cc785d 100644
--- a/charts/linkerd2-service-mirror/values.yaml
+++ b/charts/linkerd2-service-mirror/values.yaml
@@ -4,3 +4,4 @@ logLevel: info
 eventRequeueLimit: 3
 controllerImage: gcr.io/linkerd-io/controller
 controllerImageVersion: {version}
+controllerComponentLabel: linkerd.io/control-plane-component
diff --git a/charts/linkerd2/templates/prometheus.yaml b/charts/linkerd2/templates/prometheus.yaml
index 060f412fe..59176fe67 100644
--- a/charts/linkerd2/templates/prometheus.yaml
+++ b/charts/linkerd2/templates/prometheus.yaml
@@ -81,6 +81,19 @@ data:
         action: replace
         target_label: component
 
+    - job_name: 'linkerd-service-mirror'
+      kubernetes_sd_configs:
+      - role: pod
+      relabel_configs:
+      - source_labels:
+        - __meta_kubernetes_pod_label_linkerd_io_control_plane_component
+        - __meta_kubernetes_pod_container_port_name
+        action: keep
+        regex: linkerd-service-mirror;admin-http$
+      - source_labels: [__meta_kubernetes_pod_container_name]
+        action: replace
+        target_label: component
+
     - job_name: 'linkerd-proxy'
       kubernetes_sd_configs:
       - role: pod
diff --git a/cli/cmd/cluster.go b/cli/cmd/cluster.go
index 1b35566bd..394305f0c 100644
--- a/cli/cmd/cluster.go
+++ b/cli/cmd/cluster.go
@@ -1,41 +1,58 @@
 package cmd
 
 import (
+	"context"
+	"errors"
 	"fmt"
+	"io"
+	"os"
 	"strings"
 
-	v1 "k8s.io/api/rbac/v1"
-
+	"github.com/linkerd/linkerd2/cli/table"
+	pb "github.com/linkerd/linkerd2/controller/gen/public"
+	"github.com/linkerd/linkerd2/pkg/charts"
+	"github.com/linkerd/linkerd2/pkg/charts/multicluster"
+	mccharts "github.com/linkerd/linkerd2/pkg/charts/multicluster"
+	"github.com/linkerd/linkerd2/pkg/healthcheck"
 	"github.com/linkerd/linkerd2/pkg/k8s"
+	"github.com/linkerd/linkerd2/pkg/version"
 	"github.com/spf13/cobra"
 	corev1 "k8s.io/api/core/v1"
-	rbacv1 "k8s.io/api/rbac/v1"
-
+	kerrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"sigs.k8s.io/yaml"
-
 	"k8s.io/client-go/tools/clientcmd"
 	"k8s.io/client-go/tools/clientcmd/api"
+	"k8s.io/helm/pkg/chartutil"
+	"sigs.k8s.io/yaml"
 )
 
 const (
-	tokenKey                  = "token"
-	defaultServiceAccountName = "linkerd-service-mirror"
-	defaultServiceAccountNs   = "default"
-	defaultClusterName        = "remote"
+	helmMulticlusterRemoteSetuprDefaultChartName = "linkerd2-multicluster-remote-setup"
+	tokenKey                                     = "token"
+	defaultServiceAccountName                    = "linkerd-service-mirror"
+	defaultServiceAccountNs                      = "linkerd-service-mirror"
+	defaultClusterName                           = "remote"
 )
 
 type (
 	getCredentialsOptions struct {
-		namespace           string
-		serviceAccount      string
-		clusterName         string
-		remoteClusterDomain string
+		serviceAccountName      string
+		serviceAccountNamespace string
+		clusterName             string
+		remoteClusterDomain     string
 	}
 
-	createOptions struct {
-		namespace      string
-		serviceAccount string
+	setupRemoteClusterOptions struct {
+		serviceAccountName      string
+		serviceAccountNamespace string
+		gatewayNamespace        string
+		gatewayName             string
+		probePort               uint32
+		incomingPort            uint32
+		probePeriodSeconds      uint32
+		probePath               string
+		nginxImageVersion       string
+		nginxImage              string
 	}
 
 	exportServiceOptions struct {
@@ -44,93 +61,174 @@ type (
 		gatewayNamespace string
 		gatewayName      string
 	}
+
+	gatewaysOptions struct {
+		gatewayNamespace string
+		clusterName      string
+		timeWindow       string
+	}
 )
 
-func newCmdCluster() *cobra.Command {
+func newSetupRemoteClusterOptionsWithDefault() (*setupRemoteClusterOptions, error) {
+	defaults, err := mccharts.NewValues()
+	if err != nil {
+		return nil, err
+	}
 
-	getOpts := getCredentialsOptions{}
-	createOpts := createOptions{}
-	exportOpts := exportServiceOptions{}
-	clusterCmd := &cobra.Command{
+	return &setupRemoteClusterOptions{
+		serviceAccountName:      defaults.ServiceAccountName,
+		serviceAccountNamespace: defaults.ServiceAccountNamespace,
+		gatewayNamespace:        defaults.GatewayNamespace,
+		gatewayName:             defaults.GatewayName,
+		probePort:               defaults.ProbePort,
+		incomingPort:            defaults.IncomingPort,
+		probePeriodSeconds:      defaults.ProbePeriodSeconds,
+		probePath:               defaults.ProbePath,
+		nginxImageVersion:       defaults.NginxImageVersion,
+		nginxImage:              defaults.NginxImage,
+	}, nil
 
-		Hidden: true,
-		Use:    "cluster [flags]",
-		Args:   cobra.NoArgs,
-		Short:  "Manages the multicluster setup for Linkerd",
-		Long: `Manages the multicluster setup for Linkerd.
+}
 
-This command provides subcommands to manage the multicluster support
-functionality of Linkerd. You can use it to deploy credentials to
-remote clusters, extract them as well as export remote services to be
-available across clusters.`,
-		Example: `  # Create remote cluster credentials.
-  linkerd --context=cluster-a cluster create-credentials | kubectl --context=cluster-a apply -f -
+func buildMulticlusterSetupValues(opts *setupRemoteClusterOptions) (*multicluster.Values, error) {
 
-  # Extract mirroring cluster credentials from cluster A and install them on cluster B
-  linkerd --context=cluster-a cluster get-credentials --cluster-name=remote | kubectl apply --context=cluster-b -f -
+	kubeAPI, err := k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, impersonateGroup, 0)
+	if err != nil {
+		return nil, err
+	}
 
-  # Export service from cluster A to be available to other clusters
-  linkerd --context=cluster-a cluster export-service --service-name=backend-svc --service-namespace=default --gateway-name=linkerd-gateway --gateway-ns=default`,
+	_, global, err := healthcheck.FetchLinkerdConfigMap(kubeAPI, controlPlaneNamespace)
+	if err != nil {
+		if kerrors.IsNotFound(err) {
+			return nil, errors.New("you need Linkerd to be installed in order to setup a remote cluster")
+		}
+		return nil, err
+	}
+
+	defaults, err := mccharts.NewValues()
+	if err != nil {
+		return nil, err
 	}
 
-	createCredentialsCommand := &cobra.Command{
+	defaults.GatewayName = opts.gatewayName
+	defaults.GatewayNamespace = opts.gatewayNamespace
+	defaults.IdentityTrustDomain = global.Global.IdentityContext.TrustDomain
+	defaults.IncomingPort = opts.incomingPort
+	defaults.LinkerdNamespace = controlPlaneNamespace
+	defaults.ProbePath = opts.probePath
+	defaults.ProbePeriodSeconds = opts.probePeriodSeconds
+	defaults.ProbePort = opts.probePort
+	defaults.ProxyOutboundPort = global.Proxy.OutboundPort.Port
+	defaults.ServiceAccountName = opts.serviceAccountName
+	defaults.ServiceAccountNamespace = opts.serviceAccountNamespace
+	defaults.NginxImageVersion = opts.nginxImageVersion
+	defaults.NginxImage = opts.nginxImage
+	defaults.LinkerdVersion = version.Version
+
+	return defaults, nil
+}
+
+func newGatewaysCommand() *cobra.Command {
+
+	opts := gatewaysOptions{}
+
+	cmd := &cobra.Command{
 		Hidden: false,
-		Use:    "create-credentials",
-		Short:  "Create the necessary credentials for service mirroring",
+		Use:    "gateways",
+		Short:  "Display stats information about the remote gateways",
 		Args:   cobra.NoArgs,
 		RunE: func(cmd *cobra.Command, args []string) error {
-
-			labels := map[string]string{
-				k8s.ControllerComponentLabel: k8s.ServiceMirrorLabel,
-				k8s.ControllerNSLabel:        controlPlaneNamespace,
+			req := &pb.GatewaysRequest{
+				RemoteClusterName: opts.clusterName,
+				GatewayNamespace:  opts.gatewayNamespace,
+				TimeWindow:        opts.timeWindow,
 			}
 
-			clusterRole := v1.ClusterRole{
-				ObjectMeta: metav1.ObjectMeta{Name: createOpts.serviceAccount, Namespace: createOpts.namespace, Labels: labels},
-				TypeMeta:   metav1.TypeMeta{Kind: "ClusterRole", APIVersion: "rbac.authorization.k8s.io/v1"},
-				Rules: []rbacv1.PolicyRule{
-					{
-						APIGroups: []string{""},
-						Resources: []string{"services"},
-						Verbs:     []string{"list", "get", "watch"},
-					},
-				},
+			client := checkPublicAPIClientOrExit()
+			resp, err := requestGatewaysFromAPI(client, req)
+			if err != nil {
+				return err
 			}
+			renderGateways(resp.GetOk().GatewaysTable.Rows, stdout)
+			return nil
+		},
+	}
 
-			svcAccount := corev1.ServiceAccount{
-				ObjectMeta: metav1.ObjectMeta{Name: createOpts.serviceAccount, Namespace: createOpts.namespace, Labels: labels},
-				TypeMeta:   metav1.TypeMeta{Kind: v1.ServiceAccountKind, APIVersion: "v1"},
-			}
+	cmd.Flags().StringVar(&opts.clusterName, "cluster-name", "", "the name of the remote cluster")
+	cmd.Flags().StringVar(&opts.gatewayNamespace, "gateway-namespace", "", "the namespace in which the gateway resides on the remote cluster")
+	cmd.Flags().StringVarP(&opts.timeWindow, "time-window", "t", "1m", "Time window (for example: \"15s\", \"1m\", \"10m\", \"1h\"). Needs to be at least 15s.")
 
-			clusterRoleBinding := v1.ClusterRoleBinding{
-				TypeMeta:   metav1.TypeMeta{Kind: "ClusterRoleBinding", APIVersion: "rbac.authorization.k8s.io/v1"},
-				ObjectMeta: metav1.ObjectMeta{Name: createOpts.serviceAccount, Namespace: createOpts.namespace, Labels: labels},
+	return cmd
+}
 
-				Subjects: []v1.Subject{
-					v1.Subject{Kind: v1.ServiceAccountKind, Name: createOpts.serviceAccount, Namespace: createOpts.namespace},
-				},
-				RoleRef: rbacv1.RoleRef{Kind: "ClusterRole", APIGroup: "rbac.authorization.k8s.io", Name: createOpts.serviceAccount},
-			}
+func newSetupRemoteCommand() *cobra.Command {
+	options, err := newSetupRemoteClusterOptionsWithDefault()
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "%s", err)
+		os.Exit(1)
+	}
+
+	cmd := &cobra.Command{
+		Hidden: false,
+		Use:    "setup-remote",
+		Short:  "Sets up the remote cluster by creating the gateway and necessary credentials",
+		Args:   cobra.NoArgs,
+		RunE: func(cmd *cobra.Command, args []string) error {
+
+			values, err := buildMulticlusterSetupValues(options)
 
-			crOut, err := yaml.Marshal(clusterRole)
 			if err != nil {
 				return err
 			}
 
-			saOut, err := yaml.Marshal(svcAccount)
+			// Render raw values and create chart config
+			rawValues, err := yaml.Marshal(values)
 			if err != nil {
 				return err
 			}
-			crbOut, err := yaml.Marshal(clusterRoleBinding)
+
+			files := []*chartutil.BufferedFile{
+				{Name: chartutil.ChartfileName},
+				{Name: "templates/gateway.yaml"},
+				{Name: "templates/service-mirror-rbac.yaml"},
+			}
+
+			chart := &charts.Chart{
+				Name:      helmMulticlusterRemoteSetuprDefaultChartName,
+				Dir:       helmMulticlusterRemoteSetuprDefaultChartName,
+				Namespace: controlPlaneNamespace,
+				RawValues: rawValues,
+				Files:     files,
+			}
+			buf, err := chart.RenderNoPartials()
 			if err != nil {
 				return err
 			}
-			fmt.Println(fmt.Sprintf("---\n%s---\n%s---\n%s", crOut, saOut, crbOut))
+			stdout.Write(buf.Bytes())
+			stdout.Write([]byte("---\n"))
+
 			return nil
 		},
 	}
 
-	getCredentialsCmd := &cobra.Command{
+	cmd.Flags().StringVar(&options.gatewayName, "gateway-name", options.gatewayName, "the name of the gateway")
+	cmd.Flags().StringVar(&options.gatewayNamespace, "gateway-namespace", options.gatewayNamespace, "the namespace in which the gateway will be installed")
+	cmd.Flags().Uint32Var(&options.probePort, "probe-port", options.probePort, "the liveness check port of the gateway")
+	cmd.Flags().Uint32Var(&options.incomingPort, "incoming-port", options.incomingPort, "the port on the gateway used for all incomming traffic")
+	cmd.Flags().StringVar(&options.probePath, "probe-path", options.probePath, "the path that will be exercised by the liveness checks")
+	cmd.Flags().Uint32Var(&options.probePeriodSeconds, "probe-period", options.probePeriodSeconds, "the interval at which the gateway will be checked for being alive in seconds")
+	cmd.Flags().StringVar(&options.serviceAccountName, "service-account-name", options.serviceAccountName, "the name of the service account")
+	cmd.Flags().StringVar(&options.serviceAccountNamespace, "service-account-namespace", options.serviceAccountNamespace, "the namespace in which the service account will be created")
+	cmd.Flags().StringVar(&options.nginxImageVersion, "nginx-image-version", options.nginxImageVersion, "the version of nginx to be used")
+	cmd.Flags().StringVar(&options.nginxImage, "nginx-image", options.nginxImage, "the nginx image to be used")
+
+	return cmd
+}
+
+func newGetCredentialsCommand() *cobra.Command {
+	opts := getCredentialsOptions{}
+
+	cmd := &cobra.Command{
 		Hidden: false,
 		Use:    "get-credentials",
 		Short:  "Get cluster credentials as a secret",
@@ -153,7 +251,7 @@ available across clusters.`,
 				return err
 			}
 
-			sa, err := k.CoreV1().ServiceAccounts(getOpts.namespace).Get(getOpts.serviceAccount, metav1.GetOptions{})
+			sa, err := k.CoreV1().ServiceAccounts(opts.serviceAccountNamespace).Get(opts.serviceAccountName, metav1.GetOptions{})
 			if err != nil {
 				return err
 			}
@@ -169,7 +267,7 @@ available across clusters.`,
 				return fmt.Errorf("could not find service account token secret for %s", sa.Name)
 			}
 
-			secret, err := k.CoreV1().Secrets(getOpts.namespace).Get(secretName, metav1.GetOptions{})
+			secret, err := k.CoreV1().Secrets(opts.serviceAccountNamespace).Get(secretName, metav1.GetOptions{})
 			if err != nil {
 				return err
 			}
@@ -184,12 +282,12 @@ available across clusters.`,
 				return fmt.Errorf("could not extract current context from config")
 			}
 
-			context.AuthInfo = getOpts.serviceAccount
+			context.AuthInfo = opts.serviceAccountName
 			config.Contexts = map[string]*api.Context{
 				config.CurrentContext: context,
 			}
 			config.AuthInfos = map[string]*api.AuthInfo{
-				getOpts.serviceAccount: {
+				opts.serviceAccountName: {
 					Token: string(token),
 				},
 			}
@@ -209,10 +307,10 @@ available across clusters.`,
 				Type:     k8s.MirrorSecretType,
 				TypeMeta: metav1.TypeMeta{Kind: "Secret", APIVersion: "v1"},
 				ObjectMeta: metav1.ObjectMeta{
-					Name: fmt.Sprintf("cluster-credentials-%s", getOpts.clusterName),
+					Name: fmt.Sprintf("cluster-credentials-%s", opts.clusterName),
 					Annotations: map[string]string{
-						k8s.RemoteClusterNameLabel:        getOpts.clusterName,
-						k8s.RemoteClusterDomainAnnotation: getOpts.remoteClusterDomain,
+						k8s.RemoteClusterNameLabel:        opts.clusterName,
+						k8s.RemoteClusterDomainAnnotation: opts.remoteClusterDomain,
 					},
 				},
 				Data: map[string][]byte{
@@ -230,12 +328,40 @@ available across clusters.`,
 		},
 	}
 
-	exportServiceCmd := &cobra.Command{
+	cmd.Flags().StringVar(&opts.serviceAccountName, "service-account-name", defaultServiceAccountName, "the name of the service account")
+	cmd.Flags().StringVar(&opts.serviceAccountNamespace, "service-account-namespace", defaultServiceAccountNs, "the namespace in which the service account will be created")
+	cmd.Flags().StringVar(&opts.clusterName, "cluster-name", defaultClusterName, "cluster name")
+	cmd.Flags().StringVar(&opts.remoteClusterDomain, "remote-cluster-domain", defaultClusterDomain, "custom remote cluster domain")
+
+	return cmd
+}
+
+func newExportServiceCommand() *cobra.Command {
+	opts := exportServiceOptions{}
+
+	cmd := &cobra.Command{
 		Hidden: false,
 		Use:    "export-service",
 		Short:  "Exposes a remote service to be mirrored",
 		Args:   cobra.NoArgs,
 		RunE: func(cmd *cobra.Command, args []string) error {
+
+			if opts.service == "" {
+				return errors.New("The --service-name flag needs to be set")
+			}
+
+			if opts.namespace == "" {
+				return errors.New("The --service-name flag needs to be set")
+			}
+
+			if opts.gatewayName == "" {
+				return errors.New("The --gateway-name flag needs to be set")
+			}
+
+			if opts.gatewayNamespace == "" {
+				return errors.New("The --gateway-namespace flag needs to be set")
+			}
+
 			rules := clientcmd.NewDefaultClientConfigLoadingRules()
 			rules.ExplicitPath = kubeconfigPath
 			loader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, &clientcmd.ConfigOverrides{})
@@ -253,7 +379,7 @@ available across clusters.`,
 				return err
 			}
 
-			svc, err := k.CoreV1().Services(exportOpts.namespace).Get(exportOpts.service, metav1.GetOptions{})
+			svc, err := k.CoreV1().Services(opts.namespace).Get(opts.service, metav1.GetOptions{})
 			if err != nil {
 				return err
 			}
@@ -265,8 +391,8 @@ available across clusters.`,
 				return fmt.Errorf("service %s/%s has already been exported", svc.Namespace, svc.Name)
 			}
 
-			svc.Annotations[k8s.GatewayNameAnnotation] = exportOpts.gatewayName
-			svc.Annotations[k8s.GatewayNsAnnotation] = exportOpts.gatewayNamespace
+			svc.Annotations[k8s.GatewayNameAnnotation] = opts.gatewayName
+			svc.Annotations[k8s.GatewayNsAnnotation] = opts.gatewayNamespace
 
 			_, err = k.CoreV1().Services(svc.Namespace).Update(svc)
 			if err != nil {
@@ -277,22 +403,149 @@ available across clusters.`,
 			return nil
 		},
 	}
-	getCredentialsCmd.Flags().StringVar(&getOpts.serviceAccount, "service-account-name", defaultServiceAccountName, "the name of the service account")
-	getCredentialsCmd.Flags().StringVar(&getOpts.namespace, "service-account-namespace", defaultServiceAccountNs, "the namespace in which the service account will be created")
-	getCredentialsCmd.Flags().StringVar(&getOpts.clusterName, "cluster-name", defaultClusterName, "cluster name")
-	getCredentialsCmd.Flags().StringVar(&getOpts.remoteClusterDomain, "remote-cluster-domain", defaultClusterDomain, "custom remote cluster domain")
 
-	createCredentialsCommand.Flags().StringVar(&createOpts.serviceAccount, "service-account-name", defaultServiceAccountName, "the name of the service account used")
-	createCredentialsCommand.Flags().StringVar(&createOpts.namespace, "service-account-namespace", defaultServiceAccountNs, "the namespace in which the service account can be found")
+	cmd.Flags().StringVar(&opts.service, "service-name", "", "the name of the service to be exported")
+	cmd.Flags().StringVar(&opts.namespace, "service-namespace", "", "the namespace in which the service to be exported resides")
+	cmd.Flags().StringVar(&opts.gatewayName, "gateway-name", "linkerd-gateway", "the name of the gateway")
+	cmd.Flags().StringVar(&opts.gatewayNamespace, "gateway-namespace", "linkerd-gateway", "the namespace of the gateway")
+
+	return cmd
+}
 
-	exportServiceCmd.Flags().StringVar(&exportOpts.service, "service-name", "", "the name of the service to be exported")
-	exportServiceCmd.Flags().StringVar(&exportOpts.namespace, "service-namespace", "", "the namespace in which the service to be exported resides")
-	exportServiceCmd.Flags().StringVar(&exportOpts.gatewayName, "gateway-name", "", "the name of the gateway")
-	exportServiceCmd.Flags().StringVar(&exportOpts.gatewayNamespace, "gateway-ns", "", "the ns of the gateway")
+func newCmdCluster() *cobra.Command {
 
-	clusterCmd.AddCommand(getCredentialsCmd)
-	clusterCmd.AddCommand(createCredentialsCommand)
-	clusterCmd.AddCommand(exportServiceCmd)
+	clusterCmd := &cobra.Command{
+
+		Hidden: true,
+		Use:    "cluster [flags]",
+		Args:   cobra.NoArgs,
+		Short:  "Manages the multicluster setup for Linkerd",
+		Long: `Manages the multicluster setup for Linkerd.
+
+This command provides subcommands to manage the multicluster support
+functionality of Linkerd. You can use it to deploy credentials to
+remote clusters, extract them as well as export remote services to be
+available across clusters.`,
+		Example: `  # Setup remote cluster.
+  linkerd --context=cluster-a cluster setup-remote | kubectl --context=cluster-a apply -f -
+
+  # Extract mirroring cluster credentials from cluster A and install them on cluster B
+  linkerd --context=cluster-a cluster get-credentials --cluster-name=remote | kubectl apply --context=cluster-b -f -
+
+  # Export service from cluster A to be available to other clusters
+  linkerd --context=cluster-a cluster export-service --service-name=backend-svc --service-namespace=default --gateway-name=linkerd-gateway --gateway-ns=default`,
+	}
+
+	clusterCmd.AddCommand(newGetCredentialsCommand())
+	clusterCmd.AddCommand(newSetupRemoteCommand())
+	clusterCmd.AddCommand(newExportServiceCommand())
+	clusterCmd.AddCommand(newGatewaysCommand())
 
 	return clusterCmd
 }
+
+func requestGatewaysFromAPI(client pb.ApiClient, req *pb.GatewaysRequest) (*pb.GatewaysResponse, error) {
+	resp, err := client.Gateways(context.Background(), req)
+	if err != nil {
+		return nil, fmt.Errorf("Gateways API error: %v", err)
+	}
+	if e := resp.GetError(); e != nil {
+		return nil, fmt.Errorf("Gateways API response error: %v", e.Error)
+	}
+	return resp, nil
+}
+
+func renderGateways(rows []*pb.GatewaysTable_Row, w io.Writer) {
+	t := buildGatewaysTable()
+	t.Data = []table.Row{}
+	for _, row := range rows {
+		row := row // Copy to satisfy golint.
+		t.Data = append(t.Data, gatewaysRowToTableRow(row))
+	}
+	t.Render(w)
+}
+
+var (
+	gatewayNameHeader      = "NAME"
+	gatewayNamespaceHeader = "NAMESPACE"
+	clusterNameHeader      = "CLUSTER"
+	aliveHeader            = "ALIVE"
+	pairedServicesHeader   = "NUM_SVC"
+	latencyP50Header       = "LATENCY_P50"
+	latencyP95Header       = "LATENCY_P95"
+	latencyP99Header       = "LATENCY_P99"
+)
+
+func buildGatewaysTable() table.Table {
+	columns := []table.Column{
+		table.Column{
+			Header:    clusterNameHeader,
+			Width:     7,
+			Flexible:  true,
+			LeftAlign: true,
+		},
+		table.Column{
+			Header:    gatewayNamespaceHeader,
+			Width:     9,
+			Flexible:  true,
+			LeftAlign: true,
+		},
+		table.Column{
+			Header:    gatewayNameHeader,
+			Width:     4,
+			Flexible:  true,
+			LeftAlign: true,
+		},
+		table.Column{
+			Header:    aliveHeader,
+			Width:     5,
+			Flexible:  true,
+			LeftAlign: true,
+		},
+		table.Column{
+			Header: pairedServicesHeader,
+			Width:  9,
+		},
+		table.Column{
+			Header: latencyP50Header,
+			Width:  11,
+		},
+		table.Column{
+			Header: latencyP95Header,
+			Width:  11,
+		},
+		table.Column{
+			Header: latencyP99Header,
+			Width:  11,
+		},
+	}
+	t := table.NewTable(columns, []table.Row{})
+	t.Sort = []int{0, 1} // Sort by namespace, then name.
+	return t
+}
+
+func gatewaysRowToTableRow(row *pb.GatewaysTable_Row) []string {
+	valueOrPlaceholder := func(value string) string {
+		if row.Alive {
+			return value
+		}
+		return "-"
+	}
+
+	alive := "False"
+
+	if row.Alive {
+		alive = "True"
+	}
+	return []string{
+		row.ClusterName,
+		row.Namespace,
+		row.Name,
+		alive,
+		fmt.Sprint(row.PairedServices),
+		valueOrPlaceholder(fmt.Sprintf("%dms", row.LatencyMsP50)),
+		valueOrPlaceholder(fmt.Sprintf("%dms", row.LatencyMsP95)),
+		valueOrPlaceholder(fmt.Sprintf("%dms", row.LatencyMsP99)),
+	}
+
+}
diff --git a/cli/cmd/install-service-mirror.go b/cli/cmd/install-service-mirror.go
index 3721edd8c..a2eb371d2 100644
--- a/cli/cmd/install-service-mirror.go
+++ b/cli/cmd/install-service-mirror.go
@@ -127,7 +127,7 @@ func renderServiceMirror(w io.Writer, config *installServiceMirrorOptions) error
 		RawValues: rawValues,
 		Files:     files,
 	}
-	buf, err := chart.RenderServiceMirror()
+	buf, err := chart.RenderNoPartials()
 	if err != nil {
 		return err
 	}
diff --git a/controller/api/public/client.go b/controller/api/public/client.go
index ba9019a0e..79f2c59eb 100644
--- a/controller/api/public/client.go
+++ b/controller/api/public/client.go
@@ -61,6 +61,12 @@ func (c *grpcOverHTTPClient) TopRoutes(ctx context.Context, req *pb.TopRoutesReq
 	return &msg, err
 }
 
+func (c *grpcOverHTTPClient) Gateways(ctx context.Context, req *pb.GatewaysRequest, _ ...grpc.CallOption) (*pb.GatewaysResponse, error) {
+	var msg pb.GatewaysResponse
+	err := c.apiRequest(ctx, "Gateways", req, &msg)
+	return &msg, err
+}
+
 func (c *grpcOverHTTPClient) Version(ctx context.Context, req *pb.Empty, _ ...grpc.CallOption) (*pb.VersionInfo, error) {
 	var msg pb.VersionInfo
 	err := c.apiRequest(ctx, "Version", req, &msg)
diff --git a/controller/api/public/gateways.go b/controller/api/public/gateways.go
new file mode 100644
index 000000000..d836ab674
--- /dev/null
+++ b/controller/api/public/gateways.go
@@ -0,0 +1,127 @@
+package public
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/prometheus/common/model"
+
+	pb "github.com/linkerd/linkerd2/controller/gen/public"
+)
+
+const (
+	gatewayAliveQuery           = "sum(gateway_alive%s) by (%s)"
+	numMirroredServicesQuery    = "sum(num_mirrored_services%s) by (%s)"
+	gatewayLatencyQuantileQuery = "histogram_quantile(%s, sum(irate(gateway_probe_latency_ms_bucket%s[%s])) by (le, %s))"
+)
+
+func (s *grpcServer) Gateways(ctx context.Context, req *pb.GatewaysRequest) (*pb.GatewaysResponse, error) {
+	array := []*pb.GatewaysTable_Row{}
+	metrics, err := s.getGatewaysMetrics(ctx, req, req.TimeWindow)
+
+	if err != nil {
+		return nil, err
+	}
+
+	for _, v := range metrics {
+		array = append(array, v)
+	}
+	return &pb.GatewaysResponse{
+		Response: &pb.GatewaysResponse_Ok_{
+			Ok: &pb.GatewaysResponse_Ok{
+				GatewaysTable: &pb.GatewaysTable{
+					Rows: array,
+				},
+			},
+		},
+	}, nil
+}
+
+func buildGatewaysRequestLabels(req *pb.GatewaysRequest) (labels model.LabelSet, labelNames model.LabelNames) {
+	labels = model.LabelSet{}
+
+	if req.GatewayNamespace != "" {
+		labels[gatewayNamespaceLabel] = model.LabelValue(req.GatewayNamespace)
+	}
+
+	if req.RemoteClusterName != "" {
+		labels[remoteClusterNameLabel] = model.LabelValue(req.RemoteClusterName)
+	}
+
+	groupBy := model.LabelNames{gatewayNamespaceLabel, remoteClusterNameLabel, gatewayNameLabel}
+
+	return labels, groupBy
+}
+
+func processPrometheusResult(results []promResult) map[string]*pb.GatewaysTable_Row {
+	rows := make(map[string]*pb.GatewaysTable_Row)
+
+	for _, result := range results {
+		for _, sample := range result.vec {
+
+			clusterName := sample.Metric[remoteClusterNameLabel]
+			gatewayName := sample.Metric[gatewayNameLabel]
+			gatewayNamespace := sample.Metric[gatewayNamespaceLabel]
+
+			key := fmt.Sprintf("%s-%s-%s", clusterName, gatewayNamespace, gatewayName)
+
+			addRow := func() {
+				if rows[key] == nil {
+					rows[key] = &pb.GatewaysTable_Row{}
+					rows[key].ClusterName = string(clusterName)
+					rows[key].Name = string(gatewayName)
+					rows[key].Namespace = string(gatewayNamespace)
+
+				}
+			}
+
+			value := extractSampleValue(sample)
+
+			switch result.prom {
+			case promGatewayAlive:
+				addRow()
+				if value == 0 {
+					rows[key].Alive = false
+				} else {
+					rows[key].Alive = true
+				}
+
+			case promNumMirroredServices:
+				addRow()
+				rows[key].PairedServices = value
+			case promLatencyP50:
+				addRow()
+				rows[key].LatencyMsP50 = value
+			case promLatencyP95:
+				addRow()
+				rows[key].LatencyMsP95 = value
+			case promLatencyP99:
+				addRow()
+				rows[key].LatencyMsP99 = value
+			}
+		}
+	}
+
+	return rows
+}
+
+func (s *grpcServer) getGatewaysMetrics(ctx context.Context, req *pb.GatewaysRequest, timeWindow string) (map[string]*pb.GatewaysTable_Row, error) {
+	labels, groupBy := buildGatewaysRequestLabels(req)
+
+	reqLabels := generateLabelStringWithExclusion(labels, string(gatewayNameLabel))
+
+	promQueries := map[promType]string{
+		promGatewayAlive:        gatewayAliveQuery,
+		promNumMirroredServices: numMirroredServicesQuery,
+	}
+
+	metricsResp, err := s.getPrometheusMetrics(ctx, promQueries, gatewayLatencyQuantileQuery, reqLabels, timeWindow, groupBy.String())
+
+	if err != nil {
+		return nil, err
+	}
+
+	rowsMap := processPrometheusResult(metricsResp)
+
+	return rowsMap, nil
+}
diff --git a/controller/api/public/http_server.go b/controller/api/public/http_server.go
index 7ed47d2e0..66f69ec98 100644
--- a/controller/api/public/http_server.go
+++ b/controller/api/public/http_server.go
@@ -19,6 +19,7 @@ import (
 )
 
 var (
+	gatewaysPath     = fullURLPathFor("Gateways")
 	statSummaryPath  = fullURLPathFor("StatSummary")
 	topRoutesPath    = fullURLPathFor("TopRoutes")
 	versionPath      = fullURLPathFor("Version")
@@ -46,6 +47,8 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 
 	// Serve request
 	switch req.URL.Path {
+	case gatewaysPath:
+		h.handleGateways(w, req)
 	case statSummaryPath:
 		h.handleStatSummary(w, req)
 	case topRoutesPath:
@@ -70,6 +73,27 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 
 }
 
+func (h *handler) handleGateways(w http.ResponseWriter, req *http.Request) {
+	var protoRequest pb.GatewaysRequest
+
+	err := protohttp.HTTPRequestToProto(req, &protoRequest)
+	if err != nil {
+		protohttp.WriteErrorToHTTPResponse(w, err)
+		return
+	}
+
+	rsp, err := h.grpcServer.Gateways(req.Context(), &protoRequest)
+	if err != nil {
+		protohttp.WriteErrorToHTTPResponse(w, err)
+		return
+	}
+	err = protohttp.WriteProtoToHTTPResponse(w, rsp)
+	if err != nil {
+		protohttp.WriteErrorToHTTPResponse(w, err)
+		return
+	}
+}
+
 func (h *handler) handleStatSummary(w http.ResponseWriter, req *http.Request) {
 	var protoRequest pb.StatSummaryRequest
 
diff --git a/controller/api/public/prometheus.go b/controller/api/public/prometheus.go
index 9046ce452..c16d80fee 100644
--- a/controller/api/public/prometheus.go
+++ b/controller/api/public/prometheus.go
@@ -23,17 +23,22 @@ type promResult struct {
 }
 
 const (
-	promRequests       = promType("QUERY_REQUESTS")
-	promActualRequests = promType("QUERY_ACTUAL_REQUESTS")
-	promTCPConnections = promType("QUERY_TCP_CONNECTIONS")
-	promTCPReadBytes   = promType("QUERY_TCP_READ_BYTES")
-	promTCPWriteBytes  = promType("QUERY_TCP_WRITE_BYTES")
-	promLatencyP50     = promType("0.5")
-	promLatencyP95     = promType("0.95")
-	promLatencyP99     = promType("0.99")
-
-	namespaceLabel    = model.LabelName("namespace")
-	dstNamespaceLabel = model.LabelName("dst_namespace")
+	promGatewayAlive        = promType("QUERY_GATEWAY_ALIVE")
+	promNumMirroredServices = promType("QUERY_NUM_MIRRORED_SERVICES")
+	promRequests            = promType("QUERY_REQUESTS")
+	promActualRequests      = promType("QUERY_ACTUAL_REQUESTS")
+	promTCPConnections      = promType("QUERY_TCP_CONNECTIONS")
+	promTCPReadBytes        = promType("QUERY_TCP_READ_BYTES")
+	promTCPWriteBytes       = promType("QUERY_TCP_WRITE_BYTES")
+	promLatencyP50          = promType("0.5")
+	promLatencyP95          = promType("0.95")
+	promLatencyP99          = promType("0.99")
+
+	namespaceLabel         = model.LabelName("namespace")
+	dstNamespaceLabel      = model.LabelName("dst_namespace")
+	gatewayNameLabel       = model.LabelName("gateway_name")
+	gatewayNamespaceLabel  = model.LabelName("gateway_namespace")
+	remoteClusterNameLabel = model.LabelName("remote_cluster_name")
 )
 
 func extractSampleValue(sample *model.Sample) uint64 {
@@ -176,7 +181,7 @@ func (s *grpcServer) getPrometheusMetrics(ctx context.Context, requestQueryTempl
 	// kick off asynchronous queries: request count queries + 3 latency queries
 	for pt, requestQueryTemplate := range requestQueryTemplates {
 		var query string
-		if pt == promTCPConnections {
+		if pt == promTCPConnections || pt == promGatewayAlive || pt == promNumMirroredServices {
 			query = fmt.Sprintf(requestQueryTemplate, labels, groupBy)
 		} else {
 			query = fmt.Sprintf(requestQueryTemplate, labels, timeWindow, groupBy)
diff --git a/controller/cmd/service-mirror/cluster_watcher.go b/controller/cmd/service-mirror/cluster_watcher.go
index ffedf91c1..f4fc0be70 100644
--- a/controller/cmd/service-mirror/cluster_watcher.go
+++ b/controller/cmd/service-mirror/cluster_watcher.go
@@ -3,6 +3,7 @@ package servicemirror
 import (
 	"errors"
 	"fmt"
+	"strconv"
 	"strings"
 
 	"github.com/linkerd/linkerd2/controller/k8s"
@@ -34,6 +35,26 @@ type (
 		log             *logging.Entry
 		eventsQueue     workqueue.RateLimitingInterface
 		requeueLimit    int
+		probeEventsSink ProbeEventSink
+	}
+
+	// ProbeConfig describes the configured probe on particular gateway (if presents)
+	ProbeConfig struct {
+		path            string
+		port            uint32
+		periodInSeconds uint32
+	}
+
+	// GatewaySpec contains essential data about the gateway
+	GatewaySpec struct {
+		gatewayName      string
+		gatewayNamespace string
+		clusterName      string
+		addresses        []corev1.EndpointAddress
+		incomingPort     uint32
+		resourceVersion  string
+		identity         string
+		*ProbeConfig
 	}
 
 	// RemoteServiceCreated is generated whenever a remote service is created Observing
@@ -51,32 +72,29 @@ type (
 		localService   *corev1.Service
 		localEndpoints *corev1.Endpoints
 		remoteUpdate   *corev1.Service
-		gatewayData    *gatewayMetadata
+		gatewayData    gatewayMetadata
 	}
 
 	// RemoteServiceDeleted when a remote service is going away or it is not
 	// considered mirrored anymore
 	RemoteServiceDeleted struct {
-		Name      string
-		Namespace string
+		Name        string
+		Namespace   string
+		GatewayData gatewayMetadata
 	}
 
 	// RemoteGatewayDeleted is observed when a service that is a gateway to at least
 	// one already mirrored service is deleted
 	RemoteGatewayDeleted struct {
-		gatewayData *gatewayMetadata
+		gatewayData gatewayMetadata
 	}
 
 	// RemoteGatewayUpdated happens when a service that is a gateway to at least
 	// one already mirrored service is updated. This might mean an IP change,
 	// incoming port change, etc...
 	RemoteGatewayUpdated struct {
-		newPort              int32
-		newEndpointAddresses []corev1.EndpointAddress
-		gatewayData          *gatewayMetadata
-		newResourceVersion   string
-		affectedServices     []*corev1.Service
-		identity             string
+		gatewaySpec      GatewaySpec
+		affectedServices []*corev1.Service
 	}
 
 	// ConsiderGatewayUpdateDispatch is issued when we are receiving an update for a
@@ -143,51 +161,26 @@ func (re RetryableError) Error() string {
 	return fmt.Sprintf("Inner errors:\n\t%s", strings.Join(errorStrings, "\n\t"))
 }
 
-func (rcsw *RemoteClusterServiceWatcher) extractGatewayInfo(gateway *corev1.Service) ([]corev1.EndpointAddress, int32, string, string, error) {
-	if len(gateway.Status.LoadBalancer.Ingress) == 0 {
-		return nil, 0, "", "", errors.New("expected gateway to have at lest 1 external Ip address but it has none")
-	}
-
-	var foundPort = false
-	var port int32
-	for _, p := range gateway.Spec.Ports {
-		if p.Name == consts.GatewayPortName {
-			foundPort = true
-			port = p.Port
-			break
-		}
-	}
-
-	if !foundPort {
-		return nil, 0, "", "", fmt.Errorf("cannot find  port named %s on gateway", consts.GatewayPortName)
-	}
-
-	var gatewayEndpoints []corev1.EndpointAddress
-	for _, ingress := range gateway.Status.LoadBalancer.Ingress {
-		gatewayEndpoints = append(gatewayEndpoints, corev1.EndpointAddress{
-			IP:       ingress.IP,
-			Hostname: ingress.Hostname,
-		})
-	}
-
-	gatewayIdentity := gateway.Annotations[consts.GatewayIdentity]
-
-	return gatewayEndpoints, port, gateway.ResourceVersion, gatewayIdentity, nil
-}
-
 // When the gateway is resolved we need to produce a set of endpoint addresses that that
 // contain the external IPs that this gateway exposes. Therefore we return the IP addresses
 // as well as a single port on which the gateway is accessible.
-func (rcsw *RemoteClusterServiceWatcher) resolveGateway(metadata *gatewayMetadata) ([]corev1.EndpointAddress, int32, string, string, error) {
+func (rcsw *RemoteClusterServiceWatcher) resolveGateway(metadata *gatewayMetadata) (*GatewaySpec, error) {
 	gateway, err := rcsw.remoteAPIClient.Svc().Lister().Services(metadata.Namespace).Get(metadata.Name)
 	if err != nil {
-		return nil, 0, "", "", err
+		return nil, err
 	}
-	return rcsw.extractGatewayInfo(gateway)
+	return rcsw.extractGatewaySpec(gateway)
 }
 
 // NewRemoteClusterServiceWatcher constructs a new cluster watcher
-func NewRemoteClusterServiceWatcher(localAPI *k8s.API, cfg *rest.Config, clusterName string, requeueLimit int, clusterDomain string) (*RemoteClusterServiceWatcher, error) {
+func NewRemoteClusterServiceWatcher(
+	localAPI *k8s.API,
+	cfg *rest.Config,
+	clusterName string,
+	requeueLimit int,
+	clusterDomain string,
+	probeEventsSink ProbeEventSink,
+) (*RemoteClusterServiceWatcher, error) {
 	remoteAPI, err := k8s.InitializeAPIForConfig(cfg, false, k8s.Svc)
 	if err != nil {
 		return nil, fmt.Errorf("cannot initialize remote api for cluster %s: %s", clusterName, err)
@@ -203,8 +196,9 @@ func NewRemoteClusterServiceWatcher(localAPI *k8s.API, cfg *rest.Config, cluster
 			"cluster":    clusterName,
 			"apiAddress": cfg.Host,
 		}),
-		eventsQueue:  workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()),
-		requeueLimit: requeueLimit,
+		eventsQueue:     workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()),
+		requeueLimit:    requeueLimit,
+		probeEventsSink: probeEventsSink,
 	}, nil
 }
 
@@ -309,11 +303,25 @@ func (rcsw *RemoteClusterServiceWatcher) cleanupOrphanedServices() error {
 				// something went wrong getting the service, we can retry
 				errors = append(errors, err)
 			}
+		} else {
+			if gtwData := getGatewayMetadata(srv.Annotations); gtwData != nil {
+				gatewaySpec, err := rcsw.resolveGateway(gtwData)
+				if gatewaySpec != nil && err == nil {
+					rcsw.probeEventsSink.send(&MirroredServicePaired{
+						serviceName:      srv.Name,
+						serviceNamespace: srv.Namespace,
+						GatewaySpec:      *gatewaySpec,
+					})
+				} else {
+					rcsw.log.Debugf("Could not resolve gateway for %s/%s: %s", srv.Namespace, srv.Name, err)
+				}
+			}
 		}
 	}
 	if len(errors) > 0 {
 		return RetryableError{errors}
 	}
+
 	return nil
 }
 
@@ -396,6 +404,13 @@ func (rcsw *RemoteClusterServiceWatcher) handleRemoteServiceDeleted(ev *RemoteSe
 	}
 
 	rcsw.log.Debugf("Successfully deleted Service: %s/%s", ev.Namespace, localServiceName)
+	rcsw.probeEventsSink.send(&MirroredServiceUnpaired{
+		serviceName:      localServiceName,
+		serviceNamespace: ev.Namespace,
+		gatewayName:      ev.GatewayData.Name,
+		gatewayNs:        ev.GatewayData.Namespace,
+		clusterName:      rcsw.clusterName,
+	})
 	return nil
 }
 
@@ -405,24 +420,44 @@ func (rcsw *RemoteClusterServiceWatcher) handleRemoteServiceUpdated(ev *RemoteSe
 	serviceInfo := fmt.Sprintf("%s/%s", ev.remoteUpdate.Namespace, ev.remoteUpdate.Name)
 	rcsw.log.Debugf("Updating remote mirrored service %s/%s", ev.localService.Namespace, ev.localService.Name)
 
-	gatewayEndpoints, gatewayPort, resVersion, gatewayIdentity, err := rcsw.resolveGateway(ev.gatewayData)
+	gatewayChanged := false
+	if ev.localEndpoints.Labels[consts.RemoteGatewayNameLabel] != ev.gatewayData.Name || ev.localEndpoints.Labels[consts.RemoteGatewayNsLabel] != ev.gatewayData.Namespace {
+		gatewayChanged = true
+		rcsw.probeEventsSink.send(&MirroredServiceUnpaired{
+			serviceName:      ev.localService.Name,
+			serviceNamespace: ev.localService.Namespace,
+			gatewayName:      ev.localEndpoints.Labels[consts.RemoteGatewayNameLabel],
+			gatewayNs:        ev.localEndpoints.Labels[consts.RemoteGatewayNsLabel],
+			clusterName:      rcsw.clusterName,
+		})
+	}
+
+	gatewaySpec, err := rcsw.resolveGateway(&ev.gatewayData)
 	if err == nil {
 		ev.localEndpoints.Subsets = []corev1.EndpointSubset{
 			{
-				Addresses: gatewayEndpoints,
-				Ports:     rcsw.getEndpointsPorts(ev.remoteUpdate, gatewayPort),
+				Addresses: gatewaySpec.addresses,
+				Ports:     rcsw.getEndpointsPorts(ev.remoteUpdate, int32(gatewaySpec.incomingPort)),
 			},
 		}
 
 		ev.localEndpoints.Labels[consts.RemoteGatewayNameLabel] = ev.gatewayData.Name
 		ev.localEndpoints.Labels[consts.RemoteGatewayNsLabel] = ev.gatewayData.Namespace
 
-		if gatewayIdentity != "" {
-			ev.localEndpoints.Annotations[consts.RemoteGatewayIdentity] = gatewayIdentity
+		if gatewaySpec.identity != "" {
+			ev.localEndpoints.Annotations[consts.RemoteGatewayIdentity] = gatewaySpec.identity
 		} else {
 			delete(ev.localEndpoints.Annotations, consts.RemoteGatewayIdentity)
 		}
 
+		if gatewayChanged {
+			rcsw.probeEventsSink.send(&MirroredServicePaired{
+				serviceName:      ev.localService.Name,
+				serviceNamespace: ev.localService.Namespace,
+				GatewaySpec:      *gatewaySpec,
+			})
+		}
+
 	} else {
 		rcsw.log.Warnf("Could not resolve gateway for %s: %s, nulling endpoints", serviceInfo, err)
 		ev.localEndpoints.Subsets = nil
@@ -432,9 +467,9 @@ func (rcsw *RemoteClusterServiceWatcher) handleRemoteServiceUpdated(ev *RemoteSe
 		return RetryableError{[]error{err}}
 	}
 
-	ev.localService.Labels = rcsw.getMirroredServiceLabels(ev.gatewayData)
+	ev.localService.Labels = rcsw.getMirroredServiceLabels(&ev.gatewayData)
 	ev.localService.Annotations = rcsw.getMirroredServiceAnnotations(ev.remoteUpdate)
-	ev.localService.Annotations[consts.RemoteGatewayResourceVersionAnnotation] = resVersion
+	ev.localService.Annotations[consts.RemoteGatewayResourceVersionAnnotation] = gatewaySpec.resourceVersion
 	ev.localService.Spec.Ports = remapRemoteServicePorts(ev.remoteUpdate.Spec.Ports)
 
 	if _, err := rcsw.localAPIClient.Client.CoreV1().Services(ev.localService.Namespace).Update(ev.localService); err != nil {
@@ -496,20 +531,27 @@ func (rcsw *RemoteClusterServiceWatcher) handleRemoteServiceCreated(ev *RemoteSe
 	}
 
 	// Now we try to resolve the remote gateway
-	gatewayEndpoints, gatewayPort, resVersion, gatewayIdentity, err := rcsw.resolveGateway(ev.gatewayData)
+	gatewaySpec, err := rcsw.resolveGateway(ev.gatewayData)
 	if err == nil {
 		// only if we resolve it, we are updating the endpoints addresses and ports
-		rcsw.log.Debugf("Resolved remote gateway [%v:%d] for %s", gatewayEndpoints, gatewayPort, serviceInfo)
+		rcsw.log.Debugf("Resolved remote gateway [%v:%d] for %s", gatewaySpec.addresses, gatewaySpec.incomingPort, serviceInfo)
 		endpointsToCreate.Subsets = []corev1.EndpointSubset{
 			{
-				Addresses: gatewayEndpoints,
-				Ports:     rcsw.getEndpointsPorts(ev.service, gatewayPort),
+				Addresses: gatewaySpec.addresses,
+				Ports:     rcsw.getEndpointsPorts(ev.service, int32(gatewaySpec.incomingPort)),
 			},
 		}
-		serviceToCreate.Annotations[consts.RemoteGatewayResourceVersionAnnotation] = resVersion
-		if gatewayIdentity != "" {
-			endpointsToCreate.Annotations[consts.RemoteGatewayIdentity] = gatewayIdentity
+		serviceToCreate.Annotations[consts.RemoteGatewayResourceVersionAnnotation] = gatewaySpec.resourceVersion
+		if gatewaySpec.identity != "" {
+			endpointsToCreate.Annotations[consts.RemoteGatewayIdentity] = gatewaySpec.identity
 		}
+
+		rcsw.probeEventsSink.send(&MirroredServicePaired{
+			serviceName:      serviceToCreate.Name,
+			serviceNamespace: serviceToCreate.Namespace,
+			GatewaySpec:      *gatewaySpec,
+		})
+
 	} else {
 		rcsw.log.Warnf("Could not resolve gateway for %s: %s, skipping subsets", serviceInfo, err)
 		endpointsToCreate.Subsets = nil
@@ -534,7 +576,7 @@ func (rcsw *RemoteClusterServiceWatcher) handleRemoteServiceCreated(ev *RemoteSe
 }
 
 func (rcsw *RemoteClusterServiceWatcher) handleRemoteGatewayDeleted(ev *RemoteGatewayDeleted) error {
-	affectedEndpoints, err := rcsw.endpointsForGateway(ev.gatewayData)
+	affectedEndpoints, err := rcsw.endpointsForGateway(&ev.gatewayData)
 	if err != nil {
 		// if we cannot find the endpoints, we can give up
 		if kerrors.IsNotFound(err) {
@@ -563,13 +605,17 @@ func (rcsw *RemoteClusterServiceWatcher) handleRemoteGatewayDeleted(ev *RemoteGa
 }
 
 func (rcsw *RemoteClusterServiceWatcher) handleRemoteGatewayUpdated(ev *RemoteGatewayUpdated) error {
-	rcsw.log.Debugf("Updating %d services due to remote gateway [%s/%s] update", len(ev.affectedServices), ev.gatewayData.Namespace, ev.gatewayData.Name)
+	rcsw.log.Debugf("Updating %d services due to remote gateway [%s/%s] update", len(ev.affectedServices), ev.gatewaySpec.gatewayNamespace, ev.gatewaySpec.gatewayName)
+
+	rcsw.probeEventsSink.send(&GatewayUpdated{
+		GatewaySpec: ev.gatewaySpec,
+	})
 
 	var errors []error
 	for _, svc := range ev.affectedServices {
 		updatedService := svc.DeepCopy()
 		if updatedService.Labels != nil {
-			updatedService.Annotations[consts.RemoteGatewayResourceVersionAnnotation] = ev.newResourceVersion
+			updatedService.Annotations[consts.RemoteGatewayResourceVersionAnnotation] = ev.gatewaySpec.resourceVersion
 		}
 		endpoints, err := rcsw.localAPIClient.Endpoint().Lister().Endpoints(svc.Namespace).Get(svc.Name)
 		if err != nil {
@@ -580,13 +626,13 @@ func (rcsw *RemoteClusterServiceWatcher) handleRemoteGatewayUpdated(ev *RemoteGa
 		updatedEndpoints := endpoints.DeepCopy()
 		updatedEndpoints.Subsets = []corev1.EndpointSubset{
 			{
-				Addresses: ev.newEndpointAddresses,
-				Ports:     rcsw.getEndpointsPorts(updatedService, ev.newPort),
+				Addresses: ev.gatewaySpec.addresses,
+				Ports:     rcsw.getEndpointsPorts(updatedService, int32(ev.gatewaySpec.incomingPort)),
 			},
 		}
 
-		if ev.identity != "" {
-			updatedEndpoints.Annotations[consts.RemoteGatewayIdentity] = ev.identity
+		if ev.gatewaySpec.identity != "" {
+			updatedEndpoints.Annotations[consts.RemoteGatewayIdentity] = ev.gatewaySpec.identity
 		} else {
 			delete(updatedEndpoints.Annotations, consts.RemoteGatewayIdentity)
 		}
@@ -641,11 +687,11 @@ func (rcsw *RemoteClusterServiceWatcher) handleConsiderGatewayUpdateDispatch(eve
 	}
 
 	if len(services) > 0 {
-		gatewayMeta := &gatewayMetadata{
+		gatewayMeta := gatewayMetadata{
 			Name:      event.maybeGateway.Name,
 			Namespace: event.maybeGateway.Namespace,
 		}
-		if endpoints, port, resVersion, identity, err := rcsw.extractGatewayInfo(event.maybeGateway); err != nil {
+		if gatewaySpec, err := rcsw.extractGatewaySpec(event.maybeGateway); err != nil {
 			rcsw.log.Warnf("Gateway [%s/%s] is not a compliant gateway anymore, dispatching GatewayDeleted event: %s", event.maybeGateway.Namespace, event.maybeGateway.Name, err)
 			// in case something changed about this gateway and it is not really a gateway anymore,
 			// simply dispatch deletion event so all endpoints are nulled
@@ -662,12 +708,8 @@ func (rcsw *RemoteClusterServiceWatcher) handleConsiderGatewayUpdateDispatch(eve
 
 			if len(affectedServices) > 0 {
 				rcsw.eventsQueue.Add(&RemoteGatewayUpdated{
-					newPort:              port,
-					newEndpointAddresses: endpoints,
-					gatewayData:          gatewayMeta,
-					newResourceVersion:   resVersion,
-					affectedServices:     affectedServices,
-					identity:             identity,
+					gatewaySpec:      *gatewaySpec,
+					affectedServices: affectedServices,
 				})
 			}
 
@@ -718,7 +760,7 @@ func (rcsw *RemoteClusterServiceWatcher) createOrUpdateService(service *corev1.S
 						localService:   localService,
 						localEndpoints: endpoints,
 						remoteUpdate:   service,
-						gatewayData:    gtwData,
+						gatewayData:    *gtwData,
 					})
 				} else {
 					return RetryableError{[]error{err}}
@@ -785,12 +827,13 @@ func (rcsw *RemoteClusterServiceWatcher) endpointsForGateway(gatewayData *gatewa
 func (rcsw *RemoteClusterServiceWatcher) handleOnDelete(service *corev1.Service) {
 	if gtwData := getGatewayMetadata(service.Annotations); gtwData != nil {
 		rcsw.eventsQueue.Add(&RemoteServiceDeleted{
-			Name:      service.Name,
-			Namespace: service.Namespace,
+			Name:        service.Name,
+			Namespace:   service.Namespace,
+			GatewayData: *gtwData,
 		})
 	} else {
 		rcsw.eventsQueue.Add(&RemoteGatewayDeleted{
-			gatewayData: &gatewayMetadata{
+			gatewayData: gatewayMetadata{
 				Name:      service.Name,
 				Namespace: service.Namespace,
 			}})
@@ -904,9 +947,81 @@ func (rcsw *RemoteClusterServiceWatcher) Start() {
 
 // Stop stops watching the cluster and cleans up all mirrored resources
 func (rcsw *RemoteClusterServiceWatcher) Stop(cleanupState bool) {
+	rcsw.probeEventsSink.send(&ClusterNotRegistered{
+		clusterName: rcsw.clusterName,
+	})
 	close(rcsw.stopper)
 	if cleanupState {
 		rcsw.eventsQueue.Add(&ClusterUnregistered{})
 	}
 	rcsw.eventsQueue.ShutDown()
 }
+
+func parseProbeConfig(data map[string]string) (*ProbeConfig, error) {
+	probePath := data[consts.GatewayProbePath]
+	probePort, err := strconv.ParseUint(data[consts.GatewayProbePort], 10, 32)
+	if err != nil {
+		return nil, err
+	}
+
+	probePeriod, err := strconv.ParseUint(data[consts.GatewayProbePeriod], 10, 32)
+	if err != nil {
+		return nil, err
+	}
+
+	if probePath == "" {
+		return nil, errors.New("probe path is empty")
+	}
+
+	return &ProbeConfig{
+		path:            probePath,
+		port:            uint32(probePort),
+		periodInSeconds: uint32(probePeriod),
+	}, nil
+}
+
+func (rcsw *RemoteClusterServiceWatcher) extractGatewaySpec(gateway *corev1.Service) (*GatewaySpec, error) {
+	if len(gateway.Status.LoadBalancer.Ingress) == 0 {
+		return nil, fmt.Errorf("expected gateway %s/%s to have at lest 1 external Ip address but it has none", gateway.Namespace, gateway.Name)
+	}
+
+	var foundPort = false
+	var port uint32
+	for _, p := range gateway.Spec.Ports {
+		if p.Name == consts.GatewayPortName {
+			foundPort = true
+			port = uint32(p.Port)
+			break
+		}
+	}
+
+	if !foundPort {
+		return nil, fmt.Errorf("cannot find  port named %s on gateway", consts.GatewayPortName)
+	}
+
+	var gatewayEndpoints []corev1.EndpointAddress
+	for _, ingress := range gateway.Status.LoadBalancer.Ingress {
+		gatewayEndpoints = append(gatewayEndpoints, corev1.EndpointAddress{
+			IP:       ingress.IP,
+			Hostname: ingress.Hostname,
+		})
+	}
+
+	gatewayIdentity := gateway.Annotations[consts.GatewayIdentity]
+	probeConfig, err := parseProbeConfig(gateway.Annotations)
+
+	if err != nil {
+		rcsw.log.Debugf("could not parse probe config for gateway: %s/%s: %s", gateway.Namespace, gateway.Name, err)
+	}
+
+	return &GatewaySpec{
+		clusterName:      rcsw.clusterName,
+		gatewayName:      gateway.Name,
+		gatewayNamespace: gateway.Namespace,
+		addresses:        gatewayEndpoints,
+		incomingPort:     port,
+		resourceVersion:  gateway.ResourceVersion,
+		identity:         gatewayIdentity,
+		ProbeConfig:      probeConfig,
+	}, nil
+}
diff --git a/controller/cmd/service-mirror/config_watcher.go b/controller/cmd/service-mirror/config_watcher.go
index 32b9a2a27..a92df5f59 100644
--- a/controller/cmd/service-mirror/config_watcher.go
+++ b/controller/cmd/service-mirror/config_watcher.go
@@ -13,6 +13,11 @@ import (
 	consts "github.com/linkerd/linkerd2/pkg/k8s"
 )
 
+// ProbeEventSink is an interface for a type that can send events to the probe manager
+type ProbeEventSink interface {
+	send(event interface{})
+}
+
 // RemoteClusterConfigWatcher watches for secrets of type MirrorSecretType
 // and upon the detection of such secret created starts a RemoteClusterServiceWatcher
 type RemoteClusterConfigWatcher struct {
@@ -20,14 +25,16 @@ type RemoteClusterConfigWatcher struct {
 	clusterWatchers map[string]*RemoteClusterServiceWatcher
 	requeueLimit    int
 	sync.RWMutex
+	probeEventsSink ProbeEventSink
 }
 
 // NewRemoteClusterConfigWatcher Creates a new config watcher
-func NewRemoteClusterConfigWatcher(k8sAPI *k8s.API, requeueLimit int) *RemoteClusterConfigWatcher {
+func NewRemoteClusterConfigWatcher(k8sAPI *k8s.API, requeueLimit int, probeEventsSink ProbeEventSink) *RemoteClusterConfigWatcher {
 	rcw := &RemoteClusterConfigWatcher{
 		k8sAPI:          k8sAPI,
 		clusterWatchers: map[string]*RemoteClusterServiceWatcher{},
 		requeueLimit:    requeueLimit,
+		probeEventsSink: probeEventsSink,
 	}
 	k8sAPI.Secret().Informer().AddEventHandler(
 		cache.FilteringResourceEventHandler{
@@ -105,12 +112,12 @@ func (rcw *RemoteClusterConfigWatcher) Stop() {
 }
 
 func (rcw *RemoteClusterConfigWatcher) registerRemoteCluster(secret *corev1.Secret) error {
-	config, name, domain, err := parseRemoteClusterSecret(secret)
+	config, err := parseRemoteClusterSecret(secret)
 	if err != nil {
 		return err
 	}
 
-	clientConfig, err := clientcmd.RESTConfigFromKubeConfig(config)
+	clientConfig, err := clientcmd.RESTConfigFromKubeConfig(config.apiConfig)
 	if err != nil {
 		return fmt.Errorf("unable to parse kube config: %s", err)
 	}
@@ -118,50 +125,61 @@ func (rcw *RemoteClusterConfigWatcher) registerRemoteCluster(secret *corev1.Secr
 	rcw.Lock()
 	defer rcw.Unlock()
 
-	if _, ok := rcw.clusterWatchers[name]; ok {
-		return fmt.Errorf("there is already a cluster with name %s being watcher. Please delete its config before attempting to register a new one", name)
+	if _, ok := rcw.clusterWatchers[config.clusterName]; ok {
+		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.k8sAPI, clientConfig, name, rcw.requeueLimit, domain)
+	watcher, err := NewRemoteClusterServiceWatcher(rcw.k8sAPI, clientConfig, config.clusterName, rcw.requeueLimit, config.clusterDomain, rcw.probeEventsSink)
 	if err != nil {
 		return err
 	}
 
-	rcw.clusterWatchers[name] = watcher
+	rcw.clusterWatchers[config.clusterName] = watcher
 	watcher.Start()
 	return nil
 
 }
 
 func (rcw *RemoteClusterConfigWatcher) unregisterRemoteCluster(secret *corev1.Secret, cleanState bool) error {
-	_, name, _, err := parseRemoteClusterSecret(secret)
+	config, err := parseRemoteClusterSecret(secret)
 	if err != nil {
 		return err
 	}
 	rcw.Lock()
 	defer rcw.Unlock()
-	if watcher, ok := rcw.clusterWatchers[name]; ok {
+	if watcher, ok := rcw.clusterWatchers[config.clusterName]; ok {
 		watcher.Stop(cleanState)
 	} else {
-		return fmt.Errorf("cannot find watcher for cluser: %s", name)
+		return fmt.Errorf("cannot find watcher for cluser: %s", config.clusterName)
 	}
-	delete(rcw.clusterWatchers, name)
+	delete(rcw.clusterWatchers, config.clusterName)
 
 	return nil
 }
 
-func parseRemoteClusterSecret(secret *corev1.Secret) ([]byte, string, string, error) {
+type watchedClusterConfig struct {
+	apiConfig     []byte
+	clusterName   string
+	clusterDomain string
+}
+
+func parseRemoteClusterSecret(secret *corev1.Secret) (*watchedClusterConfig, error) {
 	clusterName, hasClusterName := secret.Annotations[consts.RemoteClusterNameLabel]
 	config, hasConfig := secret.Data[consts.ConfigKeyName]
 	domain, hasDomain := secret.Annotations[consts.RemoteClusterDomainAnnotation]
 	if !hasClusterName {
-		return nil, "", "", fmt.Errorf("secret of type %s should contain key %s", consts.MirrorSecretType, consts.ConfigKeyName)
+		return nil, fmt.Errorf("secret of type %s should contain key %s", consts.MirrorSecretType, consts.ConfigKeyName)
 	}
 	if !hasConfig {
-		return nil, "", "", fmt.Errorf("secret should contain remote cluster name as annotation %s", consts.RemoteClusterNameLabel)
+		return nil, fmt.Errorf("secret should contain remote cluster name as annotation %s", consts.RemoteClusterNameLabel)
 	}
 	if !hasDomain {
-		return nil, "", "", fmt.Errorf("secret should contain remote cluster domain as annotation %s", consts.RemoteClusterDomainAnnotation)
+		return nil, fmt.Errorf("secret should contain remote cluster domain as annotation %s", consts.RemoteClusterDomainAnnotation)
 	}
-	return config, clusterName, domain, nil
+
+	return &watchedClusterConfig{
+		apiConfig:     config,
+		clusterName:   clusterName,
+		clusterDomain: domain,
+	}, nil
 }
diff --git a/controller/cmd/service-mirror/jittered_ticker.go b/controller/cmd/service-mirror/jittered_ticker.go
new file mode 100644
index 000000000..b57c0edd3
--- /dev/null
+++ b/controller/cmd/service-mirror/jittered_ticker.go
@@ -0,0 +1,77 @@
+// Copyright (c) 2016-2017 Tigera, Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//NB: This file was copied from: https://github.com/projectcalico/felix/blob/master/jitter/jittered_ticker.go
+
+package servicemirror
+
+import (
+	"math/rand"
+	"time"
+
+	log "github.com/sirupsen/logrus"
+)
+
+// Ticker tries to emit events on channel C at minDuration intervals plus up to maxJitter.
+type Ticker struct {
+	C           <-chan time.Time
+	stop        chan bool
+	MinDuration time.Duration
+	MaxJitter   time.Duration
+}
+
+// NewTicker creates a new Ticker
+func NewTicker(minDuration time.Duration, maxJitter time.Duration) *Ticker {
+	if minDuration < 0 {
+		log.WithField("duration", minDuration).Panic("Negative duration")
+	}
+	if maxJitter < 0 {
+		log.WithField("jitter", minDuration).Panic("Negative jitter")
+	}
+	c := make(chan time.Time, 1)
+	ticker := &Ticker{
+		C:           c,
+		stop:        make(chan bool),
+		MinDuration: minDuration,
+		MaxJitter:   maxJitter,
+	}
+	go ticker.loop(c)
+	return ticker
+}
+
+func (t *Ticker) loop(c chan time.Time) {
+tickLoop:
+	for {
+		time.Sleep(t.calculateDelay())
+		// Send best-effort then go back to sleep.
+		select {
+		case <-t.stop:
+			log.Info("Stopping jittered ticker")
+			close(c)
+			break tickLoop
+		case c <- time.Now():
+		default:
+		}
+	}
+}
+
+func (t *Ticker) calculateDelay() time.Duration {
+	jitter := time.Duration(rand.Int63n(int64(t.MaxJitter)))
+	delay := t.MinDuration + jitter
+	return delay
+}
+
+// Stop the ticker
+func (t *Ticker) Stop() {
+	t.stop <- true
+}
diff --git a/controller/cmd/service-mirror/main.go b/controller/cmd/service-mirror/main.go
index 97265f48b..015c9a0d8 100644
--- a/controller/cmd/service-mirror/main.go
+++ b/controller/cmd/service-mirror/main.go
@@ -7,16 +7,24 @@ import (
 	"syscall"
 
 	"github.com/linkerd/linkerd2/controller/k8s"
+	"github.com/linkerd/linkerd2/pkg/admin"
 	"github.com/linkerd/linkerd2/pkg/flags"
 	log "github.com/sirupsen/logrus"
 )
 
+type chanProbeEventSink struct{ sender func(event interface{}) }
+
+func (s *chanProbeEventSink) send(event interface{}) {
+	s.sender(event)
+}
+
 // Main executes the tap service-mirror
 func Main(args []string) {
 	cmd := flag.NewFlagSet("service-mirror", flag.ExitOnError)
 
 	kubeConfigPath := cmd.String("kubeconfig", "", "path to the local kube config")
 	requeueLimit := cmd.Int("event-requeue-limit", 3, "requeue limit for events")
+	metricsAddr := cmd.String("metrics-addr", ":9999", "address to serve scrapable metrics on")
 
 	flags.ConfigureAndParse(cmd, args)
 
@@ -36,12 +44,17 @@ func Main(args []string) {
 		log.Fatalf("Failed to initialize K8s API: %s", err)
 	}
 
+	probeManager := NewProbeManager(k8sAPI)
+	probeManager.Start()
+
 	k8sAPI.Sync(nil)
-	watcher := NewRemoteClusterConfigWatcher(k8sAPI, *requeueLimit)
+	watcher := NewRemoteClusterConfigWatcher(k8sAPI, *requeueLimit, &chanProbeEventSink{probeManager.enqueueEvent})
 	log.Info("Started cluster config watcher")
 
-	<-stop
+	go admin.StartServer(*metricsAddr)
 
+	<-stop
 	log.Info("Stopping cluster config watcher")
 	watcher.Stop()
+	probeManager.Stop()
 }
diff --git a/controller/cmd/service-mirror/metrics.go b/controller/cmd/service-mirror/metrics.go
new file mode 100644
index 000000000..ec2fdf31f
--- /dev/null
+++ b/controller/cmd/service-mirror/metrics.go
@@ -0,0 +1,116 @@
+package servicemirror
+
+import (
+	"github.com/prometheus/client_golang/prometheus"
+	"github.com/prometheus/client_golang/prometheus/promauto"
+)
+
+const (
+	gatewayNameLabel      = "gateway_name"
+	gatewayNamespaceLabel = "gateway_namespace"
+	gatewayClusterName    = "remote_cluster_name"
+	eventTypeLabelName    = "event_type"
+	probeSuccessfulLabel  = "probe_successful"
+)
+
+type probeMetricVecs struct {
+	services  *prometheus.GaugeVec
+	alive     *prometheus.GaugeVec
+	latencies *prometheus.HistogramVec
+	enqueues  *prometheus.CounterVec
+	dequeues  *prometheus.CounterVec
+	probes    *prometheus.CounterVec
+}
+
+type probeMetrics struct {
+	services  prometheus.Gauge
+	alive     prometheus.Gauge
+	latencies prometheus.Observer
+	probes    *prometheus.CounterVec
+}
+
+func newProbeMetricVecs() probeMetricVecs {
+	labelNames := []string{gatewayNameLabel, gatewayNamespaceLabel, gatewayClusterName}
+
+	probes := promauto.NewCounterVec(
+		prometheus.CounterOpts{
+			Name: "gateway_probes",
+			Help: "A counter for the number of actual performed probes to a gateway",
+		},
+		[]string{gatewayNameLabel, gatewayNamespaceLabel, gatewayClusterName, probeSuccessfulLabel},
+	)
+
+	enqueues := promauto.NewCounterVec(
+		prometheus.CounterOpts{
+			Name: "probe_manager_event_enqueues",
+			Help: "A counter for the number of enqueued events to the probe manager",
+		},
+		[]string{eventTypeLabelName},
+	)
+
+	dequeues := promauto.NewCounterVec(
+		prometheus.CounterOpts{
+			Name: "probe_manager_event_dequeues",
+			Help: "A counter for the number of dequeued events to the probe manager",
+		},
+		[]string{eventTypeLabelName},
+	)
+
+	services := promauto.NewGaugeVec(
+		prometheus.GaugeOpts{
+			Name: "num_mirrored_services",
+			Help: "A gauge for the current number of mirrored services associated with a gateway",
+		},
+		labelNames,
+	)
+
+	alive := promauto.NewGaugeVec(
+		prometheus.GaugeOpts{
+			Name: "gateway_alive",
+			Help: "A gauge which is 1 if the gateway is alive and 0 if it is not.",
+		},
+		labelNames,
+	)
+
+	latencies := promauto.NewHistogramVec(
+		prometheus.HistogramOpts{
+			Name: "gateway_probe_latency_ms",
+			Help: "A histogram of latencies to a remote gateway.",
+			Buckets: []float64{
+				1, 2, 3, 4, 5,
+				10, 20, 30, 40, 50,
+				100, 200, 300, 400, 500,
+				1000, 2000, 3000, 4000, 5000,
+				10000, 20000, 30000, 40000, 50000,
+			},
+		},
+		labelNames)
+
+	return probeMetricVecs{
+		services:  services,
+		alive:     alive,
+		latencies: latencies,
+		enqueues:  enqueues,
+		dequeues:  dequeues,
+		probes:    probes,
+	}
+}
+func (mv probeMetricVecs) newWorkerMetrics(gatewayNamespace, gatewayName, remoteClusterName string) (*probeMetrics, error) {
+
+	labels := prometheus.Labels{
+		gatewayNameLabel:      gatewayName,
+		gatewayNamespaceLabel: gatewayNamespace,
+		gatewayClusterName:    remoteClusterName,
+	}
+
+	curriedProbes, err := mv.probes.CurryWith(labels)
+	if err != nil {
+		return nil, err
+	}
+	return &probeMetrics{
+		services:  mv.services.With(labels),
+		alive:     mv.alive.With(labels),
+		latencies: mv.latencies.With(labels),
+		probes:    curriedProbes,
+	}, nil
+}
diff --git a/controller/cmd/service-mirror/probe_manager.go b/controller/cmd/service-mirror/probe_manager.go
new file mode 100644
index 000000000..0febad05d
--- /dev/null
+++ b/controller/cmd/service-mirror/probe_manager.go
@@ -0,0 +1,239 @@
+package servicemirror
+
+import (
+	"fmt"
+
+	"github.com/prometheus/client_golang/prometheus"
+
+	"github.com/linkerd/linkerd2/controller/k8s"
+	consts "github.com/linkerd/linkerd2/pkg/k8s"
+	log "github.com/sirupsen/logrus"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/labels"
+)
+
+const probeChanBufferSize = 500
+
+// ProbeManager takes care of managing the lifecycle of probe workers
+type ProbeManager struct {
+	k8sAPI       *k8s.API
+	probeWorkers map[string]*ProbeWorker
+
+	events     chan interface{}
+	metricVecs *probeMetricVecs
+	done       chan struct{}
+}
+
+// MirroredServiceUnpaired is emitted when a service is no longer mirrored
+type MirroredServiceUnpaired struct {
+	serviceName      string
+	serviceNamespace string
+	gatewayName      string
+	gatewayNs        string
+	clusterName      string
+}
+
+// MirroredServicePaired is emitted when a new mirrored service is created
+type MirroredServicePaired struct {
+	serviceName      string
+	serviceNamespace string
+	GatewaySpec
+}
+
+// ClusterNotRegistered is is emitted when the cluster is not monitored anymore
+type ClusterNotRegistered struct {
+	clusterName string
+}
+
+// GatewayUpdated is emitted when something about the gateway is updated (i.e. its external IP(s))
+type GatewayUpdated struct {
+	GatewaySpec
+}
+
+// NewProbeManager creates a new probe manager
+func NewProbeManager(k8sAPI *k8s.API) *ProbeManager {
+	metricVecs := newProbeMetricVecs()
+	return &ProbeManager{
+		k8sAPI:       k8sAPI,
+		probeWorkers: make(map[string]*ProbeWorker),
+		events:       make(chan interface{}, probeChanBufferSize),
+		metricVecs:   &metricVecs,
+		done:         make(chan struct{}),
+	}
+}
+
+func eventTypeString(ev interface{}) string {
+	switch ev.(type) {
+	case *MirroredServicePaired:
+		return "MirroredServicePaired"
+	case *MirroredServiceUnpaired:
+		return "MirroredServiceUnpaired"
+	case *GatewayUpdated:
+		return "GatewayUpdated"
+	case *ClusterNotRegistered:
+		return "ClusterNotRegistered"
+	default:
+		return "Unknown"
+	}
+}
+
+func (m *ProbeManager) enqueueEvent(event interface{}) {
+	m.metricVecs.enqueues.With(prometheus.Labels{eventTypeLabelName: eventTypeString(event)}).Inc()
+	m.events <- event
+}
+
+func probeKey(gatewayNamespace string, gatewayName string, clusterName string) string {
+	return fmt.Sprintf("%s-%s-%s", gatewayNamespace, gatewayName, clusterName)
+}
+
+func (m *ProbeManager) handleEvent(ev interface{}) {
+	switch ev := ev.(type) {
+	case *MirroredServicePaired:
+		m.handleMirroredServicePaired(ev)
+	case *MirroredServiceUnpaired:
+		m.handleMirroredServiceUnpaired(ev)
+	case *GatewayUpdated:
+		m.handleGatewayUpdated(ev)
+	case *ClusterNotRegistered:
+		m.handleClusterNotRegistered(ev)
+	default:
+		log.Errorf("Received unknown event: %v", ev)
+	}
+}
+
+func endpointAddressesToIps(addrs []corev1.EndpointAddress) []string {
+	result := []string{}
+
+	for _, a := range addrs {
+
+		result = append(result, a.IP)
+	}
+
+	return result
+}
+
+func gatewayToProbeSpec(gatewaySpec GatewaySpec) *probeSpec {
+
+	if gatewaySpec.ProbeConfig == nil {
+		return nil
+	}
+	return &probeSpec{
+		ips:             endpointAddressesToIps(gatewaySpec.addresses),
+		path:            gatewaySpec.path,
+		port:            gatewaySpec.port,
+		periodInSeconds: gatewaySpec.periodInSeconds,
+	}
+}
+
+func (m *ProbeManager) handleMirroredServicePaired(event *MirroredServicePaired) {
+	probeKey := probeKey(event.gatewayNamespace, event.gatewayName, event.clusterName)
+	worker, ok := m.probeWorkers[probeKey]
+	if ok {
+		log.Debugf("Probe worker %s already exists", probeKey)
+		worker.PairService(event.serviceName, event.serviceNamespace)
+	} else {
+		log.Debugf("Creating probe worker %s", probeKey)
+		probeMetrics, err := m.metricVecs.newWorkerMetrics(event.gatewayNamespace, event.gatewayName, event.clusterName)
+		if err != nil {
+			log.Errorf("Could not crete probe metrics: %s", err)
+		} else {
+			probeSpec := gatewayToProbeSpec(event.GatewaySpec)
+			if probeSpec != nil {
+				worker = NewProbeWorker(probeSpec, probeMetrics, probeKey)
+				worker.PairService(event.serviceName, event.serviceNamespace)
+				m.probeWorkers[probeKey] = worker
+				worker.Start()
+			} else {
+				log.Debugf("No probe spec for: %s", probeKey)
+			}
+		}
+	}
+}
+
+func (m *ProbeManager) handleMirroredServiceUnpaired(event *MirroredServiceUnpaired) {
+	probeKey := probeKey(event.gatewayNs, event.gatewayName, event.clusterName)
+	worker, ok := m.probeWorkers[probeKey]
+	if ok {
+		worker.UnPairService(event.serviceName, event.serviceNamespace)
+		if worker.NumPairedServices() < 1 {
+			log.Debugf("Probe worker's %s associated services dropped to 0, cleaning up", probeKey)
+			worker.Stop()
+			delete(m.probeWorkers, probeKey)
+		}
+	} else {
+		log.Debugf("Could not find a worker for %s while handling MirroredServiceUnpaired event", probeKey)
+	}
+}
+
+func (m *ProbeManager) handleGatewayUpdated(event *GatewayUpdated) {
+	probeKey := probeKey(event.gatewayNamespace, event.gatewayName, event.clusterName)
+	worker, ok := m.probeWorkers[probeKey]
+	if ok {
+		probeSpec := gatewayToProbeSpec(event.GatewaySpec)
+		if probeSpec != nil {
+			worker.UpdateProbeSpec(probeSpec)
+		} else {
+			log.Debugf("No probe spec for: %s", probeKey)
+		}
+	} else {
+		log.Debugf("Could not find a worker for %s while handling GatewayUpdated event", probeKey)
+	}
+}
+
+func (m *ProbeManager) stopProbe(key string) {
+	if worker, ok := m.probeWorkers[key]; ok {
+		worker.Stop()
+		delete(m.probeWorkers, key)
+	} else {
+		log.Infof("Could not find probe worker with key %s", key)
+	}
+}
+
+func (m *ProbeManager) handleClusterNotRegistered(event *ClusterNotRegistered) {
+	matchLabels := map[string]string{
+		consts.MirroredResourceLabel:  "true",
+		consts.RemoteClusterNameLabel: event.clusterName,
+	}
+
+	services, err := m.k8sAPI.Svc().Lister().List(labels.Set(matchLabels).AsSelector())
+	if err != nil {
+		log.Errorf("Was not able to unregister cluster %s, %s", event.clusterName, err)
+	}
+
+	stopped := make(map[string]bool)
+	for _, svc := range services {
+		probeKey := probeKey(svc.Labels[consts.RemoteGatewayNameLabel], svc.Labels[consts.RemoteGatewayNameLabel], event.clusterName)
+
+		if _, ok := stopped[probeKey]; !ok {
+			m.stopProbe(probeKey)
+			stopped[probeKey] = true
+		}
+	}
+}
+
+func (m *ProbeManager) run() {
+	for {
+		select {
+		case event := <-m.events:
+			log.Debugf("Received event: %v", event)
+			m.metricVecs.dequeues.With(prometheus.Labels{eventTypeLabelName: eventTypeString(event)}).Inc()
+			m.handleEvent(event)
+		case <-m.done:
+			log.Debug("Shutting down ProbeManager")
+			for key := range m.probeWorkers {
+				m.stopProbe(key)
+			}
+			return
+		}
+	}
+}
+
+// Start starts the probe manager
+func (m *ProbeManager) Start() {
+	go m.run()
+}
+
+// Stop stops the probe manager
+func (m *ProbeManager) Stop() {
+	close(m.done)
+}
diff --git a/controller/cmd/service-mirror/probe_worker.go b/controller/cmd/service-mirror/probe_worker.go
new file mode 100644
index 000000000..b5ffb0bd3
--- /dev/null
+++ b/controller/cmd/service-mirror/probe_worker.go
@@ -0,0 +1,154 @@
+package servicemirror
+
+import (
+	"fmt"
+	"math/rand"
+	"net/http"
+	"sync"
+	"time"
+
+	"github.com/prometheus/client_golang/prometheus"
+
+	logging "github.com/sirupsen/logrus"
+)
+
+const httpGatewayTimeoutMillis = 50000
+
+type probeSpec struct {
+	ips             []string
+	path            string
+	port            uint32
+	periodInSeconds uint32
+}
+
+// ProbeWorker is responsible for monitoring gateways using a probe specification
+type ProbeWorker struct {
+	*sync.RWMutex
+	probeSpec      *probeSpec
+	pairedServices map[string]struct{}
+	stopCh         chan struct{}
+	metrics        *probeMetrics
+	log            *logging.Entry
+}
+
+// NewProbeWorker creates a new probe worker associated with a particular gateway
+func NewProbeWorker(spec *probeSpec, metrics *probeMetrics, probekey string) *ProbeWorker {
+	return &ProbeWorker{
+		RWMutex:        &sync.RWMutex{},
+		probeSpec:      spec,
+		pairedServices: make(map[string]struct{}),
+		stopCh:         make(chan struct{}),
+		metrics:        metrics,
+		log: logging.WithFields(logging.Fields{
+			"probe-key": probekey,
+		}),
+	}
+}
+
+// NumPairedServices returns the number of paired services for this probe worker
+func (pw *ProbeWorker) NumPairedServices() int {
+	return len(pw.pairedServices)
+}
+
+// PairService increments the number of services that are routed by the gateway
+func (pw *ProbeWorker) PairService(serviceName, serviceNamespace string) {
+	svcKey := fmt.Sprintf("%s-%s", serviceNamespace, serviceName)
+	if _, ok := pw.pairedServices[svcKey]; !ok {
+		pw.pairedServices[svcKey] = struct{}{}
+		pw.metrics.services.Set(float64(len(pw.pairedServices)))
+	}
+}
+
+// UnPairService decrements the number of services that are routed by the gateway
+func (pw *ProbeWorker) UnPairService(serviceName, serviceNamespace string) {
+	svcKey := fmt.Sprintf("%s-%s", serviceNamespace, serviceName)
+	if _, ok := pw.pairedServices[svcKey]; ok {
+		delete(pw.pairedServices, svcKey)
+		pw.metrics.services.Set(float64(len(pw.pairedServices)))
+	}
+}
+
+// UpdateProbeSpec is used to update the probe specification when something about the gateway changes
+func (pw *ProbeWorker) UpdateProbeSpec(spec *probeSpec) {
+	pw.Lock()
+	pw.probeSpec = spec
+	pw.Unlock()
+}
+
+// Stop this probe worker
+func (pw *ProbeWorker) Stop() {
+	pw.log.Debug("Stopping probe worker")
+	close(pw.stopCh)
+}
+
+// Start this probe worker
+func (pw *ProbeWorker) Start() {
+	pw.log.Debug("Starting probe worker")
+	go pw.run()
+}
+
+func (pw *ProbeWorker) run() {
+	periodInMillis := pw.probeSpec.periodInSeconds * 1000
+	probeTickerPeriod := time.Duration(periodInMillis) * time.Millisecond
+	maxJitter := time.Duration(periodInMillis/10) * time.Millisecond // max jitter is 10% of period
+	probeTicker := NewTicker(probeTickerPeriod, maxJitter)
+
+probeLoop:
+	for {
+		select {
+		case <-pw.stopCh:
+			break probeLoop
+		case <-probeTicker.C:
+			pw.doProbe()
+		}
+	}
+}
+
+func (pw *ProbeWorker) pickAnIP() string {
+	numIps := len(pw.probeSpec.ips)
+	if numIps == 0 {
+		return ""
+	}
+	return pw.probeSpec.ips[rand.Int()%numIps]
+}
+
+func (pw *ProbeWorker) doProbe() {
+	pw.RLock()
+	defer pw.RUnlock()
+
+	successLabel := prometheus.Labels{probeSuccessfulLabel: "true"}
+	notSuccessLabel := prometheus.Labels{probeSuccessfulLabel: "false"}
+
+	ipToTry := pw.pickAnIP()
+	if ipToTry == "" {
+		pw.log.Debug("No ips. Marking as unhealthy")
+		pw.metrics.alive.Set(0)
+	} else {
+		client := http.Client{
+			Timeout: httpGatewayTimeoutMillis * time.Millisecond,
+		}
+		start := time.Now()
+		resp, err := client.Get(fmt.Sprintf("http://%s:%d/%s", ipToTry, pw.probeSpec.port, pw.probeSpec.path))
+		end := time.Since(start)
+		if err != nil {
+			pw.log.Errorf("Problem connecting with gateway. Marking as unhealthy %s", err)
+			pw.metrics.alive.Set(0)
+			pw.metrics.probes.With(notSuccessLabel).Inc()
+			return
+		} else if resp.StatusCode != 200 {
+			pw.log.Debugf("Gateway returned unexpected status %d. Marking as unhealthy", resp.StatusCode)
+			pw.metrics.alive.Set(0)
+			pw.metrics.probes.With(notSuccessLabel).Inc()
+		} else {
+			pw.log.Debug("Gateway is healthy")
+			pw.metrics.alive.Set(1)
+			pw.metrics.latencies.Observe(float64(end.Milliseconds()))
+			pw.metrics.probes.With(successLabel).Inc()
+		}
+
+		if err := resp.Body.Close(); err != nil {
+			pw.log.Debugf("Failed to close response body %s", err)
+		}
+
+	}
+}
diff --git a/controller/gen/public/public.pb.go b/controller/gen/public/public.pb.go
index 7bf053d5e..b905bba3b 100644
--- a/controller/gen/public/public.pb.go
+++ b/controller/gen/public/public.pb.go
@@ -4009,6 +4009,314 @@ func (m *RouteTable_Row) GetStats() *BasicStats {
 	return nil
 }
 
+type GatewaysTable struct {
+	Rows                 []*GatewaysTable_Row `protobuf:"bytes,1,rep,name=rows,proto3" json:"rows,omitempty"`
+	XXX_NoUnkeyedLiteral struct{}             `json:"-"`
+	XXX_unrecognized     []byte               `json:"-"`
+	XXX_sizecache        int32                `json:"-"`
+}
+
+func (m *GatewaysTable) Reset()         { *m = GatewaysTable{} }
+func (m *GatewaysTable) String() string { return proto.CompactTextString(m) }
+func (*GatewaysTable) ProtoMessage()    {}
+func (*GatewaysTable) Descriptor() ([]byte, []int) {
+	return fileDescriptor_413a91106d7bcce8, []int{35}
+}
+
+func (m *GatewaysTable) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_GatewaysTable.Unmarshal(m, b)
+}
+func (m *GatewaysTable) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_GatewaysTable.Marshal(b, m, deterministic)
+}
+func (m *GatewaysTable) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_GatewaysTable.Merge(m, src)
+}
+func (m *GatewaysTable) XXX_Size() int {
+	return xxx_messageInfo_GatewaysTable.Size(m)
+}
+func (m *GatewaysTable) XXX_DiscardUnknown() {
+	xxx_messageInfo_GatewaysTable.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_GatewaysTable proto.InternalMessageInfo
+
+func (m *GatewaysTable) GetRows() []*GatewaysTable_Row {
+	if m != nil {
+		return m.Rows
+	}
+	return nil
+}
+
+type GatewaysTable_Row struct {
+	Namespace            string   `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"`
+	Name                 string   `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
+	ClusterName          string   `protobuf:"bytes,3,opt,name=cluster_name,json=clusterName,proto3" json:"cluster_name,omitempty"`
+	PairedServices       uint64   `protobuf:"varint,4,opt,name=paired_services,json=pairedServices,proto3" json:"paired_services,omitempty"`
+	Alive                bool     `protobuf:"varint,5,opt,name=alive,proto3" json:"alive,omitempty"`
+	LatencyMsP50         uint64   `protobuf:"varint,6,opt,name=latency_ms_p50,json=latencyMsP50,proto3" json:"latency_ms_p50,omitempty"`
+	LatencyMsP95         uint64   `protobuf:"varint,7,opt,name=latency_ms_p95,json=latencyMsP95,proto3" json:"latency_ms_p95,omitempty"`
+	LatencyMsP99         uint64   `protobuf:"varint,8,opt,name=latency_ms_p99,json=latencyMsP99,proto3" json:"latency_ms_p99,omitempty"`
+	XXX_NoUnkeyedLiteral struct{} `json:"-"`
+	XXX_unrecognized     []byte   `json:"-"`
+	XXX_sizecache        int32    `json:"-"`
+}
+
+func (m *GatewaysTable_Row) Reset()         { *m = GatewaysTable_Row{} }
+func (m *GatewaysTable_Row) String() string { return proto.CompactTextString(m) }
+func (*GatewaysTable_Row) ProtoMessage()    {}
+func (*GatewaysTable_Row) Descriptor() ([]byte, []int) {
+	return fileDescriptor_413a91106d7bcce8, []int{35, 0}
+}
+
+func (m *GatewaysTable_Row) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_GatewaysTable_Row.Unmarshal(m, b)
+}
+func (m *GatewaysTable_Row) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_GatewaysTable_Row.Marshal(b, m, deterministic)
+}
+func (m *GatewaysTable_Row) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_GatewaysTable_Row.Merge(m, src)
+}
+func (m *GatewaysTable_Row) XXX_Size() int {
+	return xxx_messageInfo_GatewaysTable_Row.Size(m)
+}
+func (m *GatewaysTable_Row) XXX_DiscardUnknown() {
+	xxx_messageInfo_GatewaysTable_Row.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_GatewaysTable_Row proto.InternalMessageInfo
+
+func (m *GatewaysTable_Row) GetNamespace() string {
+	if m != nil {
+		return m.Namespace
+	}
+	return ""
+}
+
+func (m *GatewaysTable_Row) GetName() string {
+	if m != nil {
+		return m.Name
+	}
+	return ""
+}
+
+func (m *GatewaysTable_Row) GetClusterName() string {
+	if m != nil {
+		return m.ClusterName
+	}
+	return ""
+}
+
+func (m *GatewaysTable_Row) GetPairedServices() uint64 {
+	if m != nil {
+		return m.PairedServices
+	}
+	return 0
+}
+
+func (m *GatewaysTable_Row) GetAlive() bool {
+	if m != nil {
+		return m.Alive
+	}
+	return false
+}
+
+func (m *GatewaysTable_Row) GetLatencyMsP50() uint64 {
+	if m != nil {
+		return m.LatencyMsP50
+	}
+	return 0
+}
+
+func (m *GatewaysTable_Row) GetLatencyMsP95() uint64 {
+	if m != nil {
+		return m.LatencyMsP95
+	}
+	return 0
+}
+
+func (m *GatewaysTable_Row) GetLatencyMsP99() uint64 {
+	if m != nil {
+		return m.LatencyMsP99
+	}
+	return 0
+}
+
+type GatewaysRequest struct {
+	RemoteClusterName    string   `protobuf:"bytes,1,opt,name=remote_cluster_name,json=remoteClusterName,proto3" json:"remote_cluster_name,omitempty"`
+	GatewayNamespace     string   `protobuf:"bytes,2,opt,name=gateway_namespace,json=gatewayNamespace,proto3" json:"gateway_namespace,omitempty"`
+	TimeWindow           string   `protobuf:"bytes,3,opt,name=time_window,json=timeWindow,proto3" json:"time_window,omitempty"`
+	XXX_NoUnkeyedLiteral struct{} `json:"-"`
+	XXX_unrecognized     []byte   `json:"-"`
+	XXX_sizecache        int32    `json:"-"`
+}
+
+func (m *GatewaysRequest) Reset()         { *m = GatewaysRequest{} }
+func (m *GatewaysRequest) String() string { return proto.CompactTextString(m) }
+func (*GatewaysRequest) ProtoMessage()    {}
+func (*GatewaysRequest) Descriptor() ([]byte, []int) {
+	return fileDescriptor_413a91106d7bcce8, []int{36}
+}
+
+func (m *GatewaysRequest) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_GatewaysRequest.Unmarshal(m, b)
+}
+func (m *GatewaysRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_GatewaysRequest.Marshal(b, m, deterministic)
+}
+func (m *GatewaysRequest) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_GatewaysRequest.Merge(m, src)
+}
+func (m *GatewaysRequest) XXX_Size() int {
+	return xxx_messageInfo_GatewaysRequest.Size(m)
+}
+func (m *GatewaysRequest) XXX_DiscardUnknown() {
+	xxx_messageInfo_GatewaysRequest.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_GatewaysRequest proto.InternalMessageInfo
+
+func (m *GatewaysRequest) GetRemoteClusterName() string {
+	if m != nil {
+		return m.RemoteClusterName
+	}
+	return ""
+}
+
+func (m *GatewaysRequest) GetGatewayNamespace() string {
+	if m != nil {
+		return m.GatewayNamespace
+	}
+	return ""
+}
+
+func (m *GatewaysRequest) GetTimeWindow() string {
+	if m != nil {
+		return m.TimeWindow
+	}
+	return ""
+}
+
+type GatewaysResponse struct {
+	// Types that are valid to be assigned to Response:
+	//	*GatewaysResponse_Ok_
+	//	*GatewaysResponse_Error
+	Response             isGatewaysResponse_Response `protobuf_oneof:"response"`
+	XXX_NoUnkeyedLiteral struct{}                    `json:"-"`
+	XXX_unrecognized     []byte                      `json:"-"`
+	XXX_sizecache        int32                       `json:"-"`
+}
+
+func (m *GatewaysResponse) Reset()         { *m = GatewaysResponse{} }
+func (m *GatewaysResponse) String() string { return proto.CompactTextString(m) }
+func (*GatewaysResponse) ProtoMessage()    {}
+func (*GatewaysResponse) Descriptor() ([]byte, []int) {
+	return fileDescriptor_413a91106d7bcce8, []int{37}
+}
+
+func (m *GatewaysResponse) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_GatewaysResponse.Unmarshal(m, b)
+}
+func (m *GatewaysResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_GatewaysResponse.Marshal(b, m, deterministic)
+}
+func (m *GatewaysResponse) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_GatewaysResponse.Merge(m, src)
+}
+func (m *GatewaysResponse) XXX_Size() int {
+	return xxx_messageInfo_GatewaysResponse.Size(m)
+}
+func (m *GatewaysResponse) XXX_DiscardUnknown() {
+	xxx_messageInfo_GatewaysResponse.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_GatewaysResponse proto.InternalMessageInfo
+
+type isGatewaysResponse_Response interface {
+	isGatewaysResponse_Response()
+}
+
+type GatewaysResponse_Ok_ struct {
+	Ok *GatewaysResponse_Ok `protobuf:"bytes,1,opt,name=ok,proto3,oneof"`
+}
+
+type GatewaysResponse_Error struct {
+	Error *ResourceError `protobuf:"bytes,2,opt,name=error,proto3,oneof"`
+}
+
+func (*GatewaysResponse_Ok_) isGatewaysResponse_Response() {}
+
+func (*GatewaysResponse_Error) isGatewaysResponse_Response() {}
+
+func (m *GatewaysResponse) GetResponse() isGatewaysResponse_Response {
+	if m != nil {
+		return m.Response
+	}
+	return nil
+}
+
+func (m *GatewaysResponse) GetOk() *GatewaysResponse_Ok {
+	if x, ok := m.GetResponse().(*GatewaysResponse_Ok_); ok {
+		return x.Ok
+	}
+	return nil
+}
+
+func (m *GatewaysResponse) GetError() *ResourceError {
+	if x, ok := m.GetResponse().(*GatewaysResponse_Error); ok {
+		return x.Error
+	}
+	return nil
+}
+
+// XXX_OneofWrappers is for the internal use of the proto package.
+func (*GatewaysResponse) XXX_OneofWrappers() []interface{} {
+	return []interface{}{
+		(*GatewaysResponse_Ok_)(nil),
+		(*GatewaysResponse_Error)(nil),
+	}
+}
+
+type GatewaysResponse_Ok struct {
+	GatewaysTable        *GatewaysTable `protobuf:"bytes,1,opt,name=gateways_table,json=gatewaysTable,proto3" json:"gateways_table,omitempty"`
+	XXX_NoUnkeyedLiteral struct{}       `json:"-"`
+	XXX_unrecognized     []byte         `json:"-"`
+	XXX_sizecache        int32          `json:"-"`
+}
+
+func (m *GatewaysResponse_Ok) Reset()         { *m = GatewaysResponse_Ok{} }
+func (m *GatewaysResponse_Ok) String() string { return proto.CompactTextString(m) }
+func (*GatewaysResponse_Ok) ProtoMessage()    {}
+func (*GatewaysResponse_Ok) Descriptor() ([]byte, []int) {
+	return fileDescriptor_413a91106d7bcce8, []int{37, 0}
+}
+
+func (m *GatewaysResponse_Ok) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_GatewaysResponse_Ok.Unmarshal(m, b)
+}
+func (m *GatewaysResponse_Ok) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_GatewaysResponse_Ok.Marshal(b, m, deterministic)
+}
+func (m *GatewaysResponse_Ok) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_GatewaysResponse_Ok.Merge(m, src)
+}
+func (m *GatewaysResponse_Ok) XXX_Size() int {
+	return xxx_messageInfo_GatewaysResponse_Ok.Size(m)
+}
+func (m *GatewaysResponse_Ok) XXX_DiscardUnknown() {
+	xxx_messageInfo_GatewaysResponse_Ok.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_GatewaysResponse_Ok proto.InternalMessageInfo
+
+func (m *GatewaysResponse_Ok) GetGatewaysTable() *GatewaysTable {
+	if m != nil {
+		return m.GatewaysTable
+	}
+	return nil
+}
+
 func init() {
 	proto.RegisterEnum("linkerd2.public.HttpMethod_Registered", HttpMethod_Registered_name, HttpMethod_Registered_value)
 	proto.RegisterEnum("linkerd2.public.Scheme_Registered", Scheme_Registered_name, Scheme_Registered_value)
@@ -4073,219 +4381,237 @@ func init() {
 	proto.RegisterType((*TopRoutesResponse_Ok)(nil), "linkerd2.public.TopRoutesResponse.Ok")
 	proto.RegisterType((*RouteTable)(nil), "linkerd2.public.RouteTable")
 	proto.RegisterType((*RouteTable_Row)(nil), "linkerd2.public.RouteTable.Row")
+	proto.RegisterType((*GatewaysTable)(nil), "linkerd2.public.GatewaysTable")
+	proto.RegisterType((*GatewaysTable_Row)(nil), "linkerd2.public.GatewaysTable.Row")
+	proto.RegisterType((*GatewaysRequest)(nil), "linkerd2.public.GatewaysRequest")
+	proto.RegisterType((*GatewaysResponse)(nil), "linkerd2.public.GatewaysResponse")
+	proto.RegisterType((*GatewaysResponse_Ok)(nil), "linkerd2.public.GatewaysResponse.Ok")
 }
 
 func init() { proto.RegisterFile("public.proto", fileDescriptor_413a91106d7bcce8) }
 
 var fileDescriptor_413a91106d7bcce8 = []byte{
-	// 3301 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xd4, 0x1a, 0x4d, 0x6f, 0x23, 0x49,
-	0xd5, 0xdf, 0x1f, 0xcf, 0x4e, 0xe2, 0xa9, 0xc9, 0x0e, 0x5e, 0xef, 0xee, 0x7c, 0xf4, 0x7c, 0x6c,
-	0x98, 0x01, 0x27, 0x93, 0xf9, 0xd8, 0xc9, 0xcc, 0x2e, 0x10, 0x27, 0xde, 0x71, 0x60, 0x26, 0xf1,
-	0xb6, 0x3d, 0xbb, 0x68, 0xb5, 0xc8, 0xea, 0x74, 0x57, 0x9c, 0x26, 0xed, 0xae, 0x9e, 0xee, 0x72,
-	0x32, 0xfe, 0x03, 0x08, 0x09, 0x21, 0x24, 0x24, 0x6e, 0x48, 0x1c, 0x38, 0xb1, 0xe2, 0xcc, 0x05,
-	0x89, 0x03, 0x57, 0xae, 0x48, 0x88, 0xd3, 0x9e, 0x38, 0xad, 0x38, 0xc1, 0x89, 0x03, 0x42, 0xf5,
-	0xd5, 0xee, 0x8e, 0xed, 0x7c, 0xcc, 0x72, 0x80, 0x93, 0xeb, 0xbd, 0x7a, 0xef, 0xd5, 0xab, 0x57,
-	0xef, 0xab, 0xca, 0x0d, 0x65, 0x6f, 0xb8, 0xeb, 0xd8, 0x66, 0xdd, 0xf3, 0x09, 0x25, 0x68, 0xc1,
-	0xb1, 0xdd, 0x03, 0xec, 0x5b, 0xab, 0x75, 0x81, 0xae, 0x5d, 0xee, 0x13, 0xd2, 0x77, 0xf0, 0x32,
-	0x9f, 0xde, 0x1d, 0xee, 0x2d, 0x5b, 0x43, 0xdf, 0xa0, 0x36, 0x71, 0x05, 0x43, 0xad, 0x6a, 0x92,
-	0xc1, 0x80, 0xb8, 0xcb, 0xfb, 0xd8, 0x70, 0xe8, 0xbe, 0xb9, 0x8f, 0xcd, 0x03, 0x39, 0x73, 0xd1,
-	0x24, 0xee, 0x9e, 0xdd, 0x5f, 0x16, 0x3f, 0x02, 0xa9, 0xe5, 0x21, 0xdb, 0x1c, 0x78, 0x74, 0xa4,
-	0xbd, 0x84, 0xd2, 0xc7, 0xd8, 0x0f, 0x6c, 0xe2, 0x6e, 0xb9, 0x7b, 0x04, 0xbd, 0x0d, 0xc5, 0x3e,
-	0x91, 0x88, 0x6a, 0xf2, 0x6a, 0x72, 0xa9, 0xa8, 0x8f, 0x11, 0x6c, 0x76, 0x77, 0x68, 0x3b, 0xd6,
-	0xa6, 0x41, 0x71, 0x35, 0x25, 0x66, 0x43, 0x04, 0xba, 0x05, 0xf3, 0x3e, 0x76, 0xb0, 0x11, 0x60,
-	0x25, 0x20, 0xcd, 0x49, 0x8e, 0x61, 0xb5, 0x7b, 0x70, 0xf1, 0x99, 0x1d, 0xd0, 0x0e, 0xf6, 0x0f,
-	0x6d, 0x13, 0x07, 0x3a, 0x7e, 0x39, 0xc4, 0x01, 0x65, 0xc2, 0x5d, 0x63, 0x80, 0x03, 0xcf, 0x30,
-	0xb1, 0x5a, 0x3a, 0x44, 0x68, 0xcf, 0x60, 0x31, 0xce, 0x14, 0x78, 0xc4, 0x0d, 0x30, 0xba, 0x0f,
-	0x85, 0x40, 0xe2, 0xaa, 0xc9, 0xab, 0xe9, 0xa5, 0xd2, 0x6a, 0xb5, 0x7e, 0xcc, 0x76, 0x75, 0xc9,
-	0xa4, 0x87, 0x94, 0xda, 0x13, 0xc8, 0x4b, 0x24, 0x42, 0x90, 0x61, 0xab, 0xc8, 0x15, 0xf9, 0x38,
-	0xae, 0x4a, 0xea, 0xb8, 0x2a, 0x01, 0x2c, 0x30, 0x55, 0xda, 0xc4, 0x0a, 0x75, 0xbf, 0x3a, 0xa1,
-	0x7b, 0x23, 0x55, 0x4d, 0x46, 0x98, 0xd0, 0xb7, 0x98, 0x9e, 0x0e, 0x36, 0x29, 0xf1, 0xb9, 0xc4,
-	0xd2, 0xaa, 0x36, 0xa1, 0xa7, 0x8e, 0x03, 0x32, 0xf4, 0x4d, 0xdc, 0xe1, 0x84, 0x36, 0x71, 0xf5,
-	0x90, 0x47, 0x7b, 0x1f, 0x2a, 0xe3, 0x45, 0xe5, 0xde, 0x97, 0x20, 0xe3, 0x11, 0x4b, 0xed, 0x7b,
-	0x71, 0x42, 0x5e, 0x9b, 0x58, 0x3a, 0xa7, 0xd0, 0xfe, 0x95, 0x81, 0x74, 0x9b, 0x58, 0x53, 0x37,
-	0xbb, 0x08, 0x59, 0x8f, 0x58, 0x5b, 0x6d, 0xb9, 0x51, 0x01, 0xa0, 0xab, 0x00, 0x16, 0xf6, 0x1c,
-	0x32, 0x1a, 0x60, 0x97, 0x8a, 0x83, 0x6c, 0x25, 0xf4, 0x08, 0x0e, 0x5d, 0x83, 0x92, 0x8f, 0x3d,
-	0xc7, 0x36, 0x8d, 0x5e, 0x80, 0x69, 0x15, 0x14, 0x89, 0x44, 0x76, 0x30, 0x45, 0xef, 0xc1, 0x25,
-	0x09, 0xb1, 0xdd, 0xf4, 0x4c, 0xe2, 0x52, 0x9f, 0x38, 0x0e, 0xf6, 0xab, 0x25, 0x49, 0xfd, 0x46,
-	0x64, 0x7e, 0x23, 0x9c, 0x46, 0xd7, 0xa1, 0x1c, 0x50, 0x83, 0xe2, 0xbd, 0xa1, 0xc3, 0x85, 0x97,
-	0x25, 0x79, 0x49, 0x61, 0x99, 0xf4, 0x2b, 0x00, 0x96, 0x81, 0x07, 0xc4, 0xe5, 0x24, 0x73, 0x92,
-	0xa4, 0x28, 0x70, 0x8c, 0x00, 0x41, 0xfa, 0x87, 0x64, 0xb7, 0x3a, 0x2f, 0x67, 0x18, 0x80, 0x2e,
-	0x41, 0x8e, 0xc9, 0x18, 0x06, 0xd5, 0x0c, 0xdf, 0xae, 0x84, 0x98, 0x15, 0x0c, 0xcb, 0xc2, 0x56,
-	0x35, 0x7b, 0x35, 0xb9, 0x54, 0xd0, 0x05, 0x80, 0x36, 0x60, 0x21, 0xb0, 0x5d, 0x13, 0x3f, 0x33,
-	0x02, 0xaa, 0x63, 0x8f, 0xf8, 0xb4, 0x9a, 0xe3, 0x87, 0xf7, 0x66, 0x5d, 0xc4, 0x63, 0x5d, 0xc5,
-	0x63, 0x7d, 0x53, 0xc6, 0xa3, 0x7e, 0x9c, 0x03, 0xad, 0xc0, 0xc5, 0xf1, 0xce, 0xb7, 0x43, 0x37,
-	0xc9, 0xf3, 0xf5, 0xa7, 0x4d, 0x21, 0x0d, 0xca, 0x12, 0xdd, 0x76, 0x0c, 0x17, 0x57, 0x0b, 0x5c,
-	0xa7, 0x18, 0x0e, 0xdd, 0x85, 0xdc, 0xd0, 0xa3, 0xf6, 0x00, 0x57, 0x8b, 0xa7, 0x69, 0x24, 0x09,
-	0xd1, 0x65, 0x00, 0xcf, 0x27, 0xaf, 0x46, 0x3a, 0x36, 0xac, 0x51, 0x75, 0x81, 0x0b, 0x8d, 0x60,
-	0xd8, 0xb2, 0x1c, 0x52, 0xe1, 0x5b, 0xe1, 0x1a, 0xc6, 0x70, 0x68, 0x09, 0x16, 0x7c, 0xe9, 0xa6,
-	0x8a, 0xec, 0x02, 0x27, 0x3b, 0x8e, 0x6e, 0xe4, 0x21, 0x4b, 0x8e, 0x5c, 0xec, 0x6b, 0x9f, 0xa7,
-	0x00, 0xba, 0x86, 0xa7, 0x62, 0x05, 0x41, 0xda, 0x23, 0x96, 0x70, 0x41, 0x76, 0x2a, 0x1e, 0xb1,
-	0x8e, 0x79, 0x5b, 0x6a, 0x8a, 0xb7, 0x5d, 0x82, 0xdc, 0xc0, 0x78, 0xa5, 0x7b, 0x01, 0xf7, 0xc5,
-	0x94, 0x2e, 0x21, 0x86, 0xa7, 0xa4, 0xcd, 0x0e, 0x86, 0x9d, 0xe7, 0x9c, 0x2e, 0x21, 0xe6, 0xe9,
-	0x94, 0x6c, 0xb5, 0xf9, 0x71, 0x16, 0x75, 0x3e, 0x46, 0x35, 0x28, 0xec, 0xf9, 0x64, 0xd0, 0x56,
-	0xc7, 0x38, 0xa7, 0x87, 0x30, 0x93, 0xc3, 0xc6, 0x5b, 0x6d, 0x79, 0x2e, 0x12, 0xe2, 0xfe, 0x62,
-	0xee, 0xe3, 0x81, 0x38, 0x04, 0xe6, 0x2f, 0x1c, 0xe2, 0xfa, 0x60, 0xba, 0x4f, 0x2c, 0x6e, 0xfe,
-	0xa2, 0x2e, 0x21, 0x96, 0x3a, 0x8c, 0x21, 0xdd, 0x27, 0xbe, 0x4d, 0x47, 0x22, 0x26, 0xf4, 0x31,
-	0x82, 0x69, 0xe5, 0x19, 0x74, 0x5f, 0xb8, 0xbf, 0xce, 0xc7, 0x8f, 0x53, 0xd5, 0x64, 0xa3, 0x00,
-	0x39, 0x6a, 0xf8, 0x7d, 0x4c, 0xb5, 0x1f, 0x15, 0x60, 0xb1, 0x6b, 0x78, 0x8d, 0x91, 0x4a, 0x06,
-	0xca, 0x6c, 0x8f, 0x15, 0x09, 0xb7, 0xdc, 0xd9, 0xd2, 0x87, 0xe4, 0x40, 0xeb, 0x90, 0x1d, 0x18,
-	0xd4, 0xdc, 0x97, 0x99, 0xe7, 0xce, 0x04, 0xeb, 0xb4, 0x15, 0xeb, 0xcf, 0x19, 0x8b, 0x2e, 0x38,
-	0x67, 0xda, 0xff, 0x29, 0xe4, 0xf1, 0x2b, 0xea, 0x1b, 0xa6, 0x38, 0x80, 0xd2, 0xea, 0x37, 0xcf,
-	0x26, 0xbc, 0x29, 0x98, 0x74, 0xc5, 0x5d, 0xfb, 0x5d, 0x06, 0xb2, 0x7c, 0x45, 0xb4, 0x01, 0x69,
-	0xc3, 0x71, 0xe4, 0x36, 0x97, 0xcf, 0xa1, 0x6b, 0xbd, 0x83, 0x5f, 0x32, 0x8f, 0x32, 0x1c, 0x87,
-	0x0b, 0x71, 0x47, 0x72, 0xc3, 0xaf, 0x25, 0xc4, 0x1d, 0xa1, 0x6f, 0x43, 0xda, 0x25, 0x22, 0xfb,
-	0x9d, 0xcf, 0x6a, 0x4c, 0x80, 0x4b, 0x28, 0x6a, 0x41, 0xd9, 0xc2, 0x01, 0xb5, 0x5d, 0x1e, 0x88,
-	0x81, 0x34, 0xd1, 0x19, 0x8e, 0xae, 0x95, 0xd0, 0x63, 0x9c, 0xe8, 0x43, 0xc8, 0xec, 0x53, 0xea,
-	0x71, 0x7f, 0x2e, 0xad, 0xae, 0x9c, 0x67, 0x43, 0x2d, 0x4a, 0xbd, 0x56, 0x42, 0xe7, 0xfc, 0xb5,
-	0x67, 0x90, 0xee, 0xe0, 0x97, 0xa8, 0x09, 0x79, 0x7e, 0xae, 0x61, 0xd5, 0x3c, 0x97, 0x4f, 0x28,
-	0xde, 0xda, 0x08, 0x32, 0x4c, 0x3a, 0xaa, 0x86, 0x51, 0xa2, 0xc2, 0x5a, 0xc5, 0x49, 0x35, 0x8c,
-	0x13, 0x15, 0xd5, 0x2a, 0x52, 0x2e, 0x47, 0x23, 0x45, 0x15, 0x98, 0x48, 0xac, 0x2c, 0xca, 0x58,
-	0xc9, 0xc8, 0x29, 0x0e, 0xb1, 0xac, 0xc2, 0x17, 0x0f, 0x07, 0xb5, 0x3f, 0x27, 0x21, 0x2f, 0xbd,
-	0x09, 0xb5, 0xa4, 0x95, 0x84, 0xef, 0xac, 0x9e, 0xcb, 0x15, 0xe3, 0x76, 0xa2, 0x72, 0x67, 0x1f,
-	0x43, 0x7e, 0x1f, 0x1b, 0x16, 0xf6, 0x03, 0x29, 0xf4, 0xf1, 0xf9, 0x85, 0xd6, 0x5b, 0x42, 0x42,
-	0x2b, 0xa1, 0x2b, 0x61, 0xb5, 0x22, 0xe4, 0x25, 0xb6, 0x51, 0x0c, 0x43, 0x28, 0x32, 0xd4, 0xfe,
-	0x99, 0x04, 0x60, 0xcc, 0xcf, 0x85, 0xb5, 0x5a, 0x00, 0x3e, 0xee, 0xdb, 0x01, 0xc5, 0x3e, 0x16,
-	0xc9, 0x73, 0x7e, 0xf5, 0xd6, 0x84, 0x2a, 0x63, 0x86, 0xba, 0x1e, 0x52, 0x8b, 0xa2, 0xac, 0x20,
-	0x74, 0x03, 0xca, 0x43, 0x37, 0x22, 0x4b, 0x9d, 0x4b, 0x0c, 0xab, 0xb9, 0x00, 0x63, 0x09, 0x28,
-	0x0f, 0xe9, 0xa7, 0xcd, 0x6e, 0x25, 0x81, 0x0a, 0x90, 0x69, 0xef, 0x74, 0xba, 0x95, 0x24, 0x43,
-	0xb5, 0x5f, 0x74, 0x2b, 0x29, 0x04, 0x90, 0xdb, 0x6c, 0x3e, 0x6b, 0x76, 0x9b, 0x95, 0x34, 0x2a,
-	0x42, 0xb6, 0xbd, 0xde, 0xdd, 0x68, 0x55, 0x32, 0xa8, 0x04, 0xf9, 0x9d, 0x76, 0x77, 0x6b, 0x67,
-	0xbb, 0x53, 0xc9, 0x32, 0x60, 0x63, 0x67, 0x7b, 0xbb, 0xb9, 0xd1, 0xad, 0xe4, 0x98, 0x8c, 0x56,
-	0x73, 0x7d, 0xb3, 0x92, 0x67, 0xe4, 0x5d, 0x7d, 0x7d, 0xa3, 0x59, 0x29, 0x34, 0x72, 0x90, 0xa1,
-	0x23, 0x0f, 0x6b, 0xbf, 0x4a, 0x42, 0xae, 0x23, 0x5c, 0x67, 0x73, 0xca, 0x96, 0x27, 0x43, 0x47,
-	0x10, 0x7f, 0xd5, 0xed, 0x5e, 0x8b, 0x6d, 0x97, 0x69, 0xd8, 0xed, 0xb6, 0x2b, 0x09, 0xa6, 0x21,
-	0x1b, 0x75, 0x2a, 0xc9, 0x50, 0xc3, 0xdf, 0x24, 0xc3, 0xa3, 0x43, 0x6b, 0x51, 0xef, 0x60, 0x61,
-	0x74, 0x65, 0xf2, 0x48, 0xc4, 0xbc, 0xfc, 0x1d, 0x3b, 0x80, 0x09, 0x39, 0x81, 0x9a, 0xda, 0x94,
-	0xbd, 0x03, 0xc5, 0x43, 0xc3, 0x19, 0xe2, 0x5e, 0x40, 0xfd, 0x50, 0xe5, 0x02, 0x47, 0x75, 0xa8,
-	0x3f, 0x9e, 0xde, 0xb5, 0x45, 0x97, 0x5d, 0x0e, 0xa7, 0x1b, 0x36, 0x2f, 0xbd, 0x7c, 0xac, 0x75,
-	0xa1, 0xb8, 0xd5, 0x5e, 0xb7, 0x2c, 0x1f, 0x07, 0xac, 0xc5, 0xc9, 0xd8, 0xde, 0xe1, 0x7d, 0xbe,
-	0x4e, 0x9e, 0x39, 0x3a, 0x83, 0xd0, 0x1d, 0x8e, 0x7d, 0x28, 0x33, 0xe5, 0x1b, 0x13, 0xfa, 0x6f,
-	0xb5, 0x0f, 0x1f, 0x4a, 0xe2, 0x87, 0x8d, 0x0c, 0xa4, 0x6c, 0x4f, 0x5b, 0x81, 0x0c, 0xc3, 0xb2,
-	0x9e, 0x69, 0xcf, 0xf6, 0x03, 0x51, 0x91, 0x72, 0xba, 0x00, 0xd8, 0x76, 0x1c, 0x23, 0x10, 0x55,
-	0x3c, 0xa7, 0xf3, 0xb1, 0xf6, 0x0c, 0xa0, 0x6b, 0x7a, 0x4a, 0x91, 0xdb, 0x4c, 0x8a, 0x0c, 0xa7,
-	0xda, 0x94, 0x05, 0x25, 0x9d, 0x9e, 0xb2, 0x3d, 0x5e, 0x31, 0x59, 0xbd, 0x4e, 0xf1, 0x7a, 0xcd,
-	0xc7, 0x9a, 0x05, 0xe9, 0x26, 0x61, 0x62, 0x2a, 0x7d, 0xdf, 0x33, 0x7b, 0xa2, 0x83, 0xeb, 0x99,
-	0xc4, 0x12, 0x36, 0x9c, 0x6b, 0x25, 0xf4, 0x79, 0x36, 0xd3, 0xe1, 0x13, 0x1b, 0xc4, 0xc2, 0x8c,
-	0xd6, 0xc7, 0x01, 0xa6, 0x3d, 0xec, 0xfb, 0xc4, 0x17, 0xb4, 0x29, 0x45, 0xcb, 0x67, 0x9a, 0x6c,
-	0x82, 0xd1, 0x36, 0xb2, 0x90, 0xc6, 0xae, 0xa5, 0xfd, 0x63, 0x01, 0x0a, 0x5d, 0xc3, 0x6b, 0x1e,
-	0xb2, 0xf6, 0xe3, 0x1e, 0xe4, 0x44, 0x7c, 0x4b, 0xb5, 0xdf, 0x9a, 0xcc, 0x02, 0xe1, 0xfe, 0x74,
-	0x49, 0x8a, 0x9e, 0x42, 0x49, 0x8c, 0x7a, 0x03, 0x4c, 0x0d, 0x99, 0xba, 0x6f, 0x4d, 0xcb, 0x1f,
-	0x7c, 0x91, 0x7a, 0xd3, 0xb5, 0x3c, 0x62, 0xbb, 0xf4, 0x39, 0xa6, 0x86, 0x0e, 0x82, 0x95, 0x8d,
-	0xd1, 0x07, 0x50, 0x8a, 0x14, 0x03, 0x79, 0x54, 0x27, 0xaa, 0x10, 0xa5, 0x47, 0x1f, 0x41, 0x25,
-	0x02, 0x0a, 0x65, 0x32, 0xe7, 0x52, 0x66, 0x21, 0xc2, 0xcf, 0x35, 0x6a, 0x00, 0xf8, 0x64, 0x48,
-	0xe5, 0xce, 0xf2, 0x5c, 0xd8, 0xf5, 0xd9, 0xc2, 0x74, 0x46, 0xcb, 0x25, 0x15, 0x7d, 0x35, 0x44,
-	0x1f, 0xc1, 0x02, 0x6f, 0x2d, 0x7b, 0x96, 0xed, 0x8b, 0xaa, 0xc7, 0xbb, 0xb2, 0xf9, 0xd5, 0xa5,
-	0xd9, 0x82, 0xda, 0x8c, 0x61, 0x53, 0xd1, 0xeb, 0xf3, 0x5e, 0x0c, 0x46, 0xf7, 0x65, 0xfe, 0x17,
-	0x15, 0xfb, 0xf2, 0x6c, 0x39, 0xb1, 0x5c, 0xff, 0x8b, 0x24, 0x94, 0xa3, 0xdb, 0x45, 0xdf, 0x85,
-	0x9c, 0x63, 0xec, 0x62, 0x47, 0x45, 0xf5, 0xea, 0xd9, 0xcc, 0x54, 0x7f, 0xc6, 0x99, 0x9a, 0x2e,
-	0xf5, 0x47, 0xba, 0x94, 0x50, 0x5b, 0x83, 0x52, 0x04, 0x8d, 0x2a, 0x90, 0x3e, 0xc0, 0x23, 0x19,
-	0xeb, 0x6c, 0xc8, 0xa2, 0x88, 0x07, 0xab, 0xba, 0x7f, 0x71, 0xe0, 0x71, 0xea, 0x51, 0xb2, 0xf6,
-	0xb3, 0x24, 0x14, 0x43, 0xcb, 0xa1, 0xa7, 0xc7, 0x94, 0x5a, 0x3e, 0x83, 0xb9, 0xff, 0xdb, 0x1a,
-	0xfd, 0xb2, 0x28, 0xcb, 0xe2, 0x0e, 0x94, 0x7d, 0x51, 0xe9, 0x7a, 0xb6, 0x6b, 0xab, 0x9e, 0xf4,
-	0xf6, 0xc9, 0x06, 0xaf, 0xcb, 0xe2, 0xb8, 0xe5, 0xda, 0x94, 0x5d, 0xe6, 0xfc, 0x31, 0x88, 0x74,
-	0x98, 0xf3, 0xe5, 0xbd, 0x56, 0x48, 0x3c, 0xa1, 0x55, 0x8d, 0x49, 0x14, 0x3c, 0x52, 0x64, 0xd9,
-	0x8f, 0xc0, 0x42, 0x49, 0x29, 0x13, 0xbb, 0x96, 0xf4, 0x8a, 0xdb, 0x67, 0x14, 0xd9, 0x74, 0x2d,
-	0xa1, 0x64, 0x08, 0xd6, 0x1e, 0x42, 0xa1, 0x43, 0x7d, 0x6c, 0x0c, 0xb6, 0xf8, 0x55, 0x7a, 0xd7,
-	0x08, 0x64, 0xc6, 0xd1, 0xf9, 0x58, 0x5c, 0x2e, 0xd9, 0x3c, 0xd7, 0x3e, 0xa3, 0x4b, 0xa8, 0xf6,
-	0xf3, 0x14, 0x94, 0x22, 0x7b, 0x47, 0xef, 0x41, 0xca, 0xb6, 0xa4, 0xcd, 0xde, 0x3d, 0x45, 0x1d,
-	0xb5, 0xa0, 0x9e, 0xb2, 0x2d, 0x96, 0x86, 0x22, 0xdd, 0xd4, 0xb4, 0x1c, 0x30, 0xee, 0x00, 0xc2,
-	0x46, 0x6b, 0x39, 0x6c, 0xce, 0x84, 0x01, 0xbe, 0x36, 0xa3, 0x86, 0x86, 0x3d, 0x5b, 0xec, 0x0e,
-	0x93, 0x99, 0x75, 0x87, 0xc9, 0x8e, 0xef, 0x30, 0x68, 0x75, 0x5c, 0x07, 0xc5, 0xfd, 0xb8, 0x3a,
-	0xab, 0x0e, 0x8e, 0x0b, 0xe0, 0xdf, 0x92, 0x50, 0x8e, 0x1e, 0xdf, 0xeb, 0x5b, 0xe5, 0x29, 0x20,
-	0x7e, 0xe7, 0xee, 0xc5, 0x5c, 0x32, 0x75, 0xda, 0xb5, 0xb8, 0xc2, 0x99, 0xa2, 0xe7, 0x72, 0x05,
-	0x4a, 0x2c, 0x21, 0xc8, 0x8a, 0xc2, 0xcd, 0x35, 0xa7, 0x03, 0x43, 0x89, 0x52, 0x12, 0xdd, 0x67,
-	0xe6, 0xac, 0xfb, 0xfc, 0x82, 0x1f, 0x7e, 0xe8, 0x44, 0xff, 0x03, 0xdb, 0xdc, 0x82, 0x8b, 0x4a,
-	0x50, 0x34, 0xe2, 0xd2, 0xa7, 0x49, 0xba, 0x20, 0x25, 0x45, 0xce, 0xec, 0x26, 0xcc, 0x87, 0x42,
-	0x76, 0x47, 0x14, 0x0b, 0xbb, 0x64, 0xf4, 0x30, 0x98, 0x1b, 0x0c, 0x89, 0x6e, 0x41, 0x1a, 0x93,
-	0x40, 0x56, 0xc0, 0xc9, 0x87, 0xaa, 0x26, 0x09, 0x74, 0x46, 0x80, 0xee, 0x43, 0x81, 0xfa, 0x86,
-	0xed, 0x9c, 0xc5, 0x91, 0x42, 0x4a, 0xd6, 0xee, 0x60, 0x66, 0x33, 0xed, 0x11, 0xcc, 0xc7, 0x0b,
-	0x04, 0x6b, 0x3c, 0x5f, 0x6c, 0x7f, 0x6f, 0x7b, 0xe7, 0x93, 0xed, 0x4a, 0x82, 0x01, 0x5b, 0xdb,
-	0x8d, 0x9d, 0x17, 0xdb, 0x9b, 0x95, 0x24, 0x2a, 0x43, 0x61, 0xe7, 0x45, 0x57, 0x40, 0xa9, 0xb1,
-	0x88, 0xab, 0x50, 0x58, 0xf7, 0x6c, 0xde, 0x0c, 0xb0, 0x3c, 0xc8, 0xdb, 0x05, 0x99, 0x1b, 0x05,
-	0xa0, 0x7d, 0x9e, 0x82, 0x62, 0x9b, 0x58, 0x9c, 0x24, 0x40, 0x4f, 0x20, 0xc7, 0xd1, 0x2a, 0x2b,
-	0x5f, 0x9f, 0xf6, 0x0a, 0x27, 0x68, 0xc3, 0x91, 0x2e, 0x59, 0x6a, 0x5f, 0x24, 0xa1, 0xa0, 0x90,
-	0x48, 0x87, 0xa2, 0x49, 0x5c, 0x6a, 0xd8, 0x2e, 0xf6, 0x67, 0x5e, 0x60, 0x26, 0x85, 0xd5, 0x37,
-	0x14, 0x13, 0x07, 0xd9, 0x1d, 0x2a, 0x14, 0x53, 0x3b, 0x84, 0xf9, 0xf8, 0x34, 0xaa, 0x42, 0x7e,
-	0x80, 0x83, 0xc0, 0xe8, 0xab, 0x7e, 0x53, 0x81, 0x2c, 0xea, 0xc7, 0xeb, 0xcb, 0x47, 0xcf, 0x10,
-	0xc1, 0x6c, 0x61, 0x0f, 0x18, 0x97, 0x78, 0xd3, 0x15, 0x00, 0x4b, 0x78, 0x3e, 0x36, 0x02, 0xe2,
-	0xaa, 0xd7, 0x34, 0x01, 0x71, 0x73, 0x72, 0x63, 0xb5, 0xa1, 0xa0, 0x6e, 0x46, 0x27, 0x3f, 0xf0,
-	0xf2, 0x07, 0x9b, 0x91, 0xa7, 0x6a, 0x0e, 0x1f, 0x87, 0x9d, 0x71, 0x7a, 0xdc, 0x19, 0x6b, 0x2f,
-	0xe1, 0xc2, 0xc4, 0x6d, 0x19, 0x3d, 0x80, 0x82, 0x7a, 0x7e, 0x92, 0xa6, 0x7b, 0x73, 0xe6, 0x1d,
-	0x5b, 0x0f, 0x49, 0x99, 0xf7, 0xf2, 0x9a, 0xd8, 0x8b, 0x3d, 0xcd, 0x16, 0xf5, 0x39, 0x8e, 0xed,
-	0xa8, 0xb7, 0xd7, 0xcf, 0x60, 0x4e, 0x31, 0x0b, 0x23, 0xbe, 0xe6, 0x72, 0xa1, 0x3f, 0xa5, 0xa2,
-	0xfe, 0xf4, 0x65, 0x0a, 0x10, 0x4b, 0x2f, 0x9d, 0xe1, 0x60, 0x60, 0xf8, 0x23, 0xf5, 0xde, 0x13,
-	0x7d, 0x30, 0x4e, 0x9e, 0xff, 0xc1, 0x98, 0xe5, 0x32, 0x6a, 0x0f, 0x70, 0xef, 0xc8, 0x76, 0x2d,
-	0x72, 0x24, 0x97, 0x04, 0x86, 0xfa, 0x84, 0x63, 0xd0, 0x37, 0x20, 0xe3, 0x12, 0x57, 0x15, 0x85,
-	0x4b, 0x93, 0x41, 0x39, 0xf0, 0xe8, 0x88, 0xf5, 0x48, 0x8c, 0x0a, 0xbd, 0x0f, 0x25, 0x4a, 0x7a,
-	0xe1, 0xae, 0x33, 0xa7, 0xec, 0x9a, 0x5d, 0xc2, 0x28, 0x09, 0x8f, 0xfe, 0x3b, 0x30, 0xb7, 0xe7,
-	0x93, 0xc1, 0x98, 0x3f, 0x7b, 0x3a, 0x7f, 0x99, 0x71, 0x84, 0x12, 0xde, 0x01, 0x08, 0x0e, 0x6c,
-	0x91, 0x9a, 0x45, 0x6e, 0x28, 0xe8, 0x45, 0x86, 0x61, 0xa6, 0x0b, 0xd0, 0x5b, 0x50, 0xa4, 0xa6,
-	0x9a, 0xcd, 0xf3, 0xd9, 0x02, 0x35, 0xc5, 0x64, 0x03, 0xa0, 0x40, 0x86, 0x74, 0x97, 0x0c, 0x5d,
-	0x4b, 0xfb, 0x4b, 0x12, 0x2e, 0xc6, 0xac, 0x2d, 0xdf, 0xd2, 0xd7, 0x20, 0x45, 0x0e, 0x66, 0x66,
-	0xe5, 0x29, 0x1c, 0xf5, 0x9d, 0x83, 0x56, 0x42, 0x4f, 0x91, 0x03, 0xf4, 0x30, 0x7a, 0xac, 0xd3,
-	0xba, 0xce, 0x98, 0xf3, 0xb4, 0x12, 0xf2, 0xe0, 0x6b, 0xeb, 0x90, 0xda, 0x39, 0x40, 0x4f, 0x80,
-	0x3f, 0x6a, 0xf7, 0xa8, 0xb1, 0xeb, 0x84, 0xaf, 0x31, 0xb5, 0xa9, 0x1a, 0x74, 0x19, 0x89, 0x0e,
-	0x81, 0x1a, 0xf2, 0x9d, 0xa9, 0x44, 0xab, 0xfd, 0x36, 0x05, 0xd0, 0x30, 0x02, 0xdb, 0x14, 0x16,
-	0xb9, 0x0e, 0x73, 0xc1, 0xd0, 0x34, 0x71, 0xc0, 0x6e, 0x46, 0x43, 0x57, 0xb4, 0x68, 0x19, 0xbd,
-	0x2c, 0x91, 0x1b, 0x0c, 0xc7, 0x88, 0xf6, 0x0c, 0xdb, 0x19, 0xfa, 0x58, 0x12, 0x89, 0xbe, 0xa5,
-	0x2c, 0x91, 0x82, 0xe8, 0x06, 0x8b, 0x12, 0x8a, 0x5d, 0x73, 0xd4, 0x1b, 0x04, 0x3d, 0xef, 0xc1,
-	0x0a, 0x77, 0x99, 0x8c, 0x5e, 0x96, 0xd8, 0xe7, 0x41, 0xfb, 0xc1, 0xca, 0x71, 0xaa, 0xb5, 0x07,
-	0xb2, 0x12, 0x44, 0xa8, 0xd6, 0x1e, 0x4c, 0x50, 0xad, 0x71, 0x4f, 0x88, 0x53, 0xad, 0xa1, 0x15,
-	0x58, 0x34, 0x4c, 0x3a, 0x34, 0x9c, 0x5e, 0x7c, 0x0b, 0x39, 0x4e, 0x8b, 0xc4, 0x5c, 0x27, 0xba,
-	0x91, 0x31, 0x47, 0x7c, 0x3f, 0xf9, 0x28, 0xc7, 0x87, 0x91, 0x5d, 0x69, 0x3f, 0x49, 0x42, 0xa1,
-	0x2b, 0x3d, 0x04, 0x7d, 0x1d, 0x2a, 0xc4, 0xc3, 0xfc, 0x1f, 0x0a, 0x57, 0x44, 0x52, 0x20, 0xed,
-	0xb5, 0xc0, 0xf0, 0x1b, 0x63, 0x34, 0x5a, 0x62, 0x37, 0x49, 0xc3, 0x12, 0xd5, 0xae, 0x47, 0x09,
-	0x35, 0x1c, 0x69, 0xb5, 0x79, 0x86, 0xe7, 0xf5, 0xae, 0xcb, 0xb0, 0xe8, 0x36, 0x5c, 0x38, 0xf2,
-	0x6d, 0x8a, 0x63, 0xa4, 0xc2, 0x74, 0x0b, 0x7c, 0x62, 0x4c, 0xab, 0x75, 0xe0, 0x42, 0xd7, 0x37,
-	0xf6, 0xf6, 0x6c, 0xb3, 0xe3, 0x39, 0x36, 0x15, 0x5a, 0x21, 0xc8, 0x18, 0x1e, 0x7e, 0xa5, 0x52,
-	0x22, 0x1b, 0xf3, 0xdb, 0x35, 0x36, 0xf6, 0x54, 0x4a, 0x64, 0x63, 0x96, 0x85, 0x8f, 0xb0, 0xdd,
-	0xdf, 0xa7, 0x2a, 0x0b, 0x0b, 0x48, 0xfb, 0x77, 0x16, 0x8a, 0xa1, 0xdf, 0xa0, 0x06, 0x14, 0x3d,
-	0x62, 0xf5, 0xfa, 0x3e, 0x19, 0xaa, 0xcb, 0xf7, 0xf5, 0xd9, 0x6e, 0xc6, 0xea, 0xcb, 0x53, 0x46,
-	0xda, 0x4a, 0xe8, 0x05, 0x4f, 0x8e, 0x6b, 0xbf, 0xce, 0xf2, 0x82, 0xc5, 0x01, 0xf4, 0x04, 0x32,
-	0x3e, 0x39, 0x52, 0x2e, 0xfb, 0xee, 0x19, 0x64, 0xd5, 0x75, 0x72, 0xa4, 0x73, 0xa6, 0xda, 0x5f,
-	0x33, 0x90, 0xd6, 0xc9, 0xd1, 0xeb, 0xa6, 0xd2, 0x53, 0xb3, 0xdb, 0xf8, 0x7f, 0x9e, 0x62, 0xec,
-	0x7f, 0x9e, 0x25, 0xa8, 0x0c, 0x70, 0xb0, 0x8f, 0xad, 0x1e, 0x33, 0x86, 0x70, 0x12, 0x71, 0x26,
-	0xf3, 0x02, 0xdf, 0x26, 0x96, 0x70, 0xa9, 0xdb, 0x70, 0xc1, 0x1f, 0xba, 0xae, 0xed, 0xf6, 0x23,
-	0xa4, 0xc2, 0xa7, 0x17, 0xe4, 0x44, 0x48, 0xbb, 0x04, 0x15, 0xe6, 0x77, 0x31, 0xa9, 0xc2, 0x59,
-	0xe7, 0x05, 0x3e, 0xa4, 0xbc, 0x0b, 0x59, 0x91, 0xa4, 0xb2, 0x33, 0x1a, 0xf8, 0x71, 0x08, 0xeb,
-	0x82, 0x12, 0x3d, 0x8c, 0xe6, 0xb6, 0xc2, 0x0c, 0x1b, 0x29, 0x57, 0x1e, 0xa7, 0x3d, 0xf4, 0x01,
-	0x14, 0x68, 0x20, 0xd9, 0x60, 0x46, 0x05, 0x99, 0x70, 0x3a, 0x3d, 0x4f, 0x03, 0xc1, 0xfe, 0x19,
-	0xcc, 0x89, 0x36, 0xa5, 0xb7, 0x3b, 0x62, 0xdb, 0xaa, 0xe6, 0xf9, 0x39, 0x3f, 0x3a, 0xe3, 0x39,
-	0xd7, 0x45, 0x9f, 0xd2, 0x18, 0xb1, 0x46, 0x85, 0xdf, 0x3f, 0x4b, 0x78, 0x8c, 0xa9, 0x7d, 0x0a,
-	0x95, 0xe3, 0x04, 0x53, 0x6e, 0xa2, 0x2b, 0xd1, 0x9b, 0xe8, 0xb4, 0xb4, 0x18, 0xf6, 0x43, 0x91,
-	0x5b, 0x2a, 0xeb, 0x3e, 0x78, 0x36, 0xd5, 0xb6, 0xa1, 0xdc, 0xb4, 0xfa, 0xe3, 0xbf, 0x98, 0xbf,
-	0x62, 0x4d, 0xd5, 0x7e, 0x9f, 0x84, 0x39, 0x29, 0x50, 0x96, 0x8d, 0x7b, 0x91, 0xb2, 0x71, 0x6d,
-	0xb2, 0x84, 0x46, 0x69, 0xbf, 0x7a, 0xc1, 0xb8, 0xcb, 0x0b, 0xc6, 0x1d, 0xc8, 0x62, 0x26, 0x57,
-	0xc6, 0xdd, 0x1b, 0x53, 0x57, 0xd5, 0x05, 0x4d, 0xac, 0x40, 0xfc, 0x31, 0x09, 0x19, 0x36, 0x87,
-	0xee, 0x40, 0x3a, 0xf0, 0xcd, 0xd3, 0xc3, 0x8d, 0x51, 0x31, 0x62, 0x2b, 0x18, 0x5f, 0x33, 0x66,
-	0x13, 0x5b, 0x01, 0x65, 0x65, 0xd8, 0x74, 0x6c, 0xec, 0xd2, 0x9e, 0x6d, 0xc9, 0x14, 0x55, 0x10,
-	0x88, 0x2d, 0x8b, 0x4d, 0x06, 0xd8, 0x3f, 0xc4, 0x3e, 0x9b, 0x14, 0x99, 0xaa, 0x20, 0x10, 0x5b,
-	0x16, 0xba, 0x05, 0x0b, 0x2e, 0xe9, 0xd9, 0x16, 0x76, 0xa9, 0x4d, 0x59, 0x71, 0xe8, 0xcb, 0x0b,
-	0xe6, 0x9c, 0x4b, 0xb6, 0x24, 0xf6, 0x79, 0xd0, 0xd7, 0xbe, 0x4c, 0x42, 0xa5, 0x4b, 0x3c, 0xfe,
-	0xc2, 0x11, 0xfc, 0x7f, 0xf4, 0x4a, 0xf9, 0x73, 0xf5, 0x4a, 0xb1, 0x6e, 0xe5, 0x4f, 0x49, 0xb8,
-	0x10, 0xd9, 0xad, 0x74, 0xba, 0xd7, 0xf4, 0x1f, 0x76, 0xf3, 0x24, 0x07, 0x72, 0x0f, 0x37, 0x27,
-	0x53, 0xc1, 0xf1, 0x75, 0x42, 0x87, 0xad, 0xad, 0x71, 0xc7, 0xbb, 0x07, 0x39, 0xfe, 0x78, 0xa7,
-	0x3c, 0x6f, 0x32, 0x77, 0x71, 0x7e, 0xd1, 0xa5, 0x48, 0xd2, 0x98, 0x03, 0xfe, 0x3d, 0x09, 0x30,
-	0x26, 0x41, 0xf7, 0x62, 0xf5, 0xe3, 0xca, 0x09, 0xd2, 0xc6, 0x75, 0x03, 0xd5, 0x22, 0xf5, 0x42,
-	0x9c, 0x53, 0x08, 0xd7, 0x7e, 0x9a, 0x14, 0x35, 0x65, 0x11, 0xb2, 0x7c, 0x75, 0x75, 0x6f, 0xe3,
-	0xc0, 0xe9, 0x87, 0x1c, 0x7b, 0xf6, 0xc8, 0x1d, 0x7f, 0xf6, 0x38, 0x7f, 0xe2, 0x5e, 0xfd, 0x43,
-	0x0e, 0xd2, 0xeb, 0x9e, 0x8d, 0x3e, 0x85, 0x52, 0xa4, 0x81, 0x44, 0xd7, 0x4f, 0x6e, 0x2f, 0xb9,
-	0x4b, 0xd7, 0x6e, 0x9c, 0xa5, 0x07, 0xd5, 0x12, 0xa8, 0x05, 0x59, 0x9e, 0x65, 0xd0, 0x3b, 0xb3,
-	0xb2, 0x8f, 0x90, 0x77, 0xf9, 0xe4, 0xe4, 0xa4, 0x25, 0x50, 0x17, 0x8a, 0xa1, 0x0b, 0xa0, 0x6b,
-	0x27, 0xb9, 0x87, 0x90, 0xa8, 0x9d, 0xee, 0x41, 0x5a, 0x02, 0x7d, 0x04, 0x05, 0xf5, 0xdd, 0x0a,
-	0xba, 0x3a, 0xc1, 0x71, 0xec, 0x3b, 0x9a, 0xda, 0xb5, 0x13, 0x28, 0x42, 0x91, 0x3f, 0x80, 0x72,
-	0xf4, 0x53, 0x20, 0x74, 0x63, 0x2a, 0xd3, 0xb1, 0xcf, 0x8b, 0x6a, 0x37, 0x4f, 0xa1, 0x0a, 0xc5,
-	0x6f, 0x42, 0xba, 0x6b, 0x78, 0xe8, 0xad, 0x69, 0x4f, 0x33, 0x4a, 0xd8, 0x9b, 0x33, 0xdf, 0x6d,
-	0xb4, 0xf4, 0x8f, 0x53, 0xc9, 0x95, 0x24, 0xfa, 0x3e, 0xcc, 0xc5, 0xfe, 0x17, 0x44, 0x37, 0xcf,
-	0xf4, 0xbf, 0xe1, 0x19, 0x24, 0xaf, 0x43, 0x5e, 0x7d, 0x8c, 0x31, 0x23, 0x11, 0xd5, 0xde, 0x9e,
-	0xc0, 0x47, 0xbe, 0xf1, 0xd2, 0x12, 0xc8, 0x81, 0x62, 0x07, 0x3b, 0x7b, 0x1b, 0xfb, 0xd8, 0x3c,
-	0x40, 0x91, 0x3f, 0xec, 0xc5, 0x37, 0x64, 0xf5, 0xe8, 0x37, 0x64, 0x21, 0x9d, 0x52, 0xb0, 0x7e,
-	0x56, 0xf2, 0xd0, 0xa0, 0x8f, 0x20, 0xb7, 0xc1, 0xbf, 0x3d, 0x9b, 0xa9, 0xef, 0x62, 0x54, 0x26,
-	0xff, 0x4a, 0x6d, 0xdd, 0x71, 0xb4, 0x44, 0xe3, 0xde, 0xa7, 0x77, 0xfb, 0x36, 0xdd, 0x1f, 0xee,
-	0xb2, 0xa5, 0x96, 0x25, 0x8d, 0xfa, 0x5d, 0x5d, 0x1e, 0x7f, 0x3a, 0xb3, 0xdc, 0xc7, 0xee, 0xb2,
-	0x10, 0xb9, 0x9b, 0xe3, 0x0f, 0x57, 0xf7, 0xfe, 0x13, 0x00, 0x00, 0xff, 0xff, 0x49, 0xb3, 0x10,
-	0x28, 0x51, 0x27, 0x00, 0x00,
+	// 3508 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xd4, 0x3a, 0x4d, 0x73, 0x23, 0x49,
+	0x56, 0xfa, 0xfe, 0x78, 0x92, 0x6c, 0x39, 0xdb, 0x33, 0x68, 0x34, 0x3b, 0x3d, 0xdd, 0xd5, 0x3d,
+	0x3d, 0xa6, 0x1b, 0x64, 0x8f, 0x7b, 0xba, 0x77, 0x3c, 0xb3, 0x0b, 0x58, 0xb6, 0xb6, 0x65, 0xb6,
+	0xdb, 0xd6, 0x94, 0xd4, 0xbb, 0xc4, 0xc4, 0x12, 0x8a, 0x72, 0x55, 0x5a, 0x2e, 0x5c, 0xaa, 0xac,
+	0xae, 0x4a, 0xd9, 0xad, 0x3f, 0x40, 0x10, 0xb1, 0x41, 0x6c, 0x40, 0x04, 0x37, 0x22, 0x38, 0x70,
+	0x62, 0x83, 0x33, 0x17, 0x6e, 0x5c, 0xb9, 0x42, 0x10, 0x04, 0x87, 0x3d, 0x71, 0xda, 0xe0, 0x04,
+	0x27, 0x0e, 0x04, 0x91, 0x5f, 0xf5, 0xa1, 0x0f, 0x5b, 0xee, 0x99, 0x03, 0x9c, 0x94, 0xf9, 0xf2,
+	0xbd, 0x97, 0xef, 0xbd, 0x7c, 0x5f, 0x99, 0x2a, 0xa8, 0x7a, 0x93, 0x53, 0xc7, 0x36, 0x5b, 0x9e,
+	0x4f, 0x28, 0x41, 0xeb, 0x8e, 0xed, 0x5e, 0x60, 0xdf, 0xda, 0x6d, 0x09, 0x70, 0xf3, 0xee, 0x88,
+	0x90, 0x91, 0x83, 0xb7, 0xf9, 0xf2, 0xe9, 0xe4, 0x6c, 0xdb, 0x9a, 0xf8, 0x06, 0xb5, 0x89, 0x2b,
+	0x08, 0x9a, 0x0d, 0x93, 0x8c, 0xc7, 0xc4, 0xdd, 0x3e, 0xc7, 0x86, 0x43, 0xcf, 0xcd, 0x73, 0x6c,
+	0x5e, 0xc8, 0x95, 0x3b, 0x26, 0x71, 0xcf, 0xec, 0xd1, 0xb6, 0xf8, 0x11, 0x40, 0xad, 0x08, 0xf9,
+	0xce, 0xd8, 0xa3, 0x53, 0xed, 0x0d, 0x54, 0x7e, 0x82, 0xfd, 0xc0, 0x26, 0xee, 0x91, 0x7b, 0x46,
+	0xd0, 0xf7, 0xa0, 0x3c, 0x22, 0x12, 0xd0, 0x48, 0xdf, 0x4b, 0x6f, 0x95, 0xf5, 0x08, 0xc0, 0x56,
+	0x4f, 0x27, 0xb6, 0x63, 0x1d, 0x1a, 0x14, 0x37, 0x32, 0x62, 0x35, 0x04, 0xa0, 0x47, 0xb0, 0xe6,
+	0x63, 0x07, 0x1b, 0x01, 0x56, 0x0c, 0xb2, 0x1c, 0x65, 0x06, 0xaa, 0x3d, 0x85, 0x3b, 0x2f, 0xed,
+	0x80, 0xf6, 0xb1, 0x7f, 0x69, 0x9b, 0x38, 0xd0, 0xf1, 0x9b, 0x09, 0x0e, 0x28, 0x63, 0xee, 0x1a,
+	0x63, 0x1c, 0x78, 0x86, 0x89, 0xd5, 0xd6, 0x21, 0x40, 0x7b, 0x09, 0x9b, 0x49, 0xa2, 0xc0, 0x23,
+	0x6e, 0x80, 0xd1, 0xe7, 0x50, 0x0a, 0x24, 0xac, 0x91, 0xbe, 0x97, 0xdd, 0xaa, 0xec, 0x36, 0x5a,
+	0x33, 0xb6, 0x6b, 0x49, 0x22, 0x3d, 0xc4, 0xd4, 0xbe, 0x82, 0xa2, 0x04, 0x22, 0x04, 0x39, 0xb6,
+	0x8b, 0xdc, 0x91, 0x8f, 0x93, 0xa2, 0x64, 0x66, 0x45, 0x09, 0x60, 0x9d, 0x89, 0xd2, 0x23, 0x56,
+	0x28, 0xfb, 0xbd, 0x39, 0xd9, 0xdb, 0x99, 0x46, 0x3a, 0x46, 0x84, 0x7e, 0x87, 0xc9, 0xe9, 0x60,
+	0x93, 0x12, 0x9f, 0x73, 0xac, 0xec, 0x6a, 0x73, 0x72, 0xea, 0x38, 0x20, 0x13, 0xdf, 0xc4, 0x7d,
+	0x8e, 0x68, 0x13, 0x57, 0x0f, 0x69, 0xb4, 0x1f, 0x40, 0x3d, 0xda, 0x54, 0xea, 0xbe, 0x05, 0x39,
+	0x8f, 0x58, 0x4a, 0xef, 0xcd, 0x39, 0x7e, 0x3d, 0x62, 0xe9, 0x1c, 0x43, 0xfb, 0xef, 0x1c, 0x64,
+	0x7b, 0xc4, 0x5a, 0xa8, 0xec, 0x26, 0xe4, 0x3d, 0x62, 0x1d, 0xf5, 0xa4, 0xa2, 0x62, 0x82, 0xee,
+	0x01, 0x58, 0xd8, 0x73, 0xc8, 0x74, 0x8c, 0x5d, 0x2a, 0x0e, 0xb2, 0x9b, 0xd2, 0x63, 0x30, 0x74,
+	0x1f, 0x2a, 0x3e, 0xf6, 0x1c, 0xdb, 0x34, 0x86, 0x01, 0xa6, 0x0d, 0x50, 0x28, 0x12, 0xd8, 0xc7,
+	0x14, 0x7d, 0x1f, 0xde, 0x97, 0x33, 0xa6, 0xcd, 0xd0, 0x24, 0x2e, 0xf5, 0x89, 0xe3, 0x60, 0xbf,
+	0x51, 0x91, 0xd8, 0xef, 0xc5, 0xd6, 0x0f, 0xc2, 0x65, 0xf4, 0x00, 0xaa, 0x01, 0x35, 0x28, 0x3e,
+	0x9b, 0x38, 0x9c, 0x79, 0x55, 0xa2, 0x57, 0x14, 0x94, 0x71, 0xff, 0x18, 0xc0, 0x32, 0xf0, 0x98,
+	0xb8, 0x1c, 0xa5, 0x26, 0x51, 0xca, 0x02, 0xc6, 0x10, 0x10, 0x64, 0xff, 0x88, 0x9c, 0x36, 0xd6,
+	0xe4, 0x0a, 0x9b, 0xa0, 0xf7, 0xa1, 0xc0, 0x78, 0x4c, 0x82, 0x46, 0x8e, 0xab, 0x2b, 0x67, 0xcc,
+	0x0a, 0x86, 0x65, 0x61, 0xab, 0x91, 0xbf, 0x97, 0xde, 0x2a, 0xe9, 0x62, 0x82, 0x0e, 0x60, 0x3d,
+	0xb0, 0x5d, 0x13, 0xbf, 0x34, 0x02, 0xaa, 0x63, 0x8f, 0xf8, 0xb4, 0x51, 0xe0, 0x87, 0xf7, 0x41,
+	0x4b, 0xc4, 0x63, 0x4b, 0xc5, 0x63, 0xeb, 0x50, 0xc6, 0xa3, 0x3e, 0x4b, 0x81, 0x76, 0xe0, 0x4e,
+	0xa4, 0xf9, 0x71, 0xe8, 0x26, 0x45, 0xbe, 0xff, 0xa2, 0x25, 0xa4, 0x41, 0x55, 0x82, 0x7b, 0x8e,
+	0xe1, 0xe2, 0x46, 0x89, 0xcb, 0x94, 0x80, 0xa1, 0xcf, 0xa0, 0x30, 0xf1, 0xa8, 0x3d, 0xc6, 0x8d,
+	0xf2, 0x4d, 0x12, 0x49, 0x44, 0x74, 0x17, 0xc0, 0xf3, 0xc9, 0xdb, 0xa9, 0x8e, 0x0d, 0x6b, 0xda,
+	0x58, 0xe7, 0x4c, 0x63, 0x10, 0xb6, 0x2d, 0x9f, 0xa9, 0xf0, 0xad, 0x73, 0x09, 0x13, 0x30, 0xb4,
+	0x05, 0xeb, 0xbe, 0x74, 0x53, 0x85, 0xb6, 0xc1, 0xd1, 0x66, 0xc1, 0xed, 0x22, 0xe4, 0xc9, 0x95,
+	0x8b, 0x7d, 0xed, 0x97, 0x19, 0x80, 0x81, 0xe1, 0xa9, 0x58, 0x41, 0x90, 0xf5, 0x88, 0x25, 0x5c,
+	0x90, 0x9d, 0x8a, 0x47, 0xac, 0x19, 0x6f, 0xcb, 0x2c, 0xf0, 0xb6, 0xf7, 0xa1, 0x30, 0x36, 0xde,
+	0xea, 0x5e, 0xc0, 0x7d, 0x31, 0xa3, 0xcb, 0x19, 0x83, 0x53, 0xd2, 0x63, 0x07, 0xc3, 0xce, 0xb3,
+	0xa6, 0xcb, 0x19, 0xf3, 0x74, 0x4a, 0x8e, 0x7a, 0xfc, 0x38, 0xcb, 0x3a, 0x1f, 0xa3, 0x26, 0x94,
+	0xce, 0x7c, 0x32, 0xee, 0xa9, 0x63, 0xac, 0xe9, 0xe1, 0x9c, 0xf1, 0x61, 0xe3, 0xa3, 0x9e, 0x3c,
+	0x17, 0x39, 0xe3, 0xfe, 0x62, 0x9e, 0xe3, 0xb1, 0x38, 0x04, 0xe6, 0x2f, 0x7c, 0xc6, 0xe5, 0xc1,
+	0xf4, 0x9c, 0x58, 0xdc, 0xfc, 0x65, 0x5d, 0xce, 0x58, 0xea, 0x30, 0x26, 0xf4, 0x9c, 0xf8, 0x36,
+	0x9d, 0x8a, 0x98, 0xd0, 0x23, 0x00, 0x93, 0xca, 0x33, 0xe8, 0xb9, 0x70, 0x7f, 0x9d, 0x8f, 0xbf,
+	0xcc, 0x34, 0xd2, 0xed, 0x12, 0x14, 0xa8, 0xe1, 0x8f, 0x30, 0xd5, 0xfe, 0xb8, 0x04, 0x9b, 0x03,
+	0xc3, 0x6b, 0x4f, 0x55, 0x32, 0x50, 0x66, 0xfb, 0x52, 0xa1, 0x70, 0xcb, 0xad, 0x96, 0x3e, 0x24,
+	0x05, 0xda, 0x87, 0xfc, 0xd8, 0xa0, 0xe6, 0xb9, 0xcc, 0x3c, 0x4f, 0xe6, 0x48, 0x17, 0xed, 0xd8,
+	0x7a, 0xc5, 0x48, 0x74, 0x41, 0xb9, 0xd4, 0xfe, 0x2f, 0xa0, 0x88, 0xdf, 0x52, 0xdf, 0x30, 0xc5,
+	0x01, 0x54, 0x76, 0x7f, 0x7b, 0x35, 0xe6, 0x1d, 0x41, 0xa4, 0x2b, 0xea, 0xe6, 0xdf, 0xe5, 0x20,
+	0xcf, 0x77, 0x44, 0x07, 0x90, 0x35, 0x1c, 0x47, 0xaa, 0xb9, 0x7d, 0x0b, 0x59, 0x5b, 0x7d, 0xfc,
+	0x86, 0x79, 0x94, 0xe1, 0x38, 0x9c, 0x89, 0x3b, 0x95, 0x0a, 0xbf, 0x13, 0x13, 0x77, 0x8a, 0x7e,
+	0x17, 0xb2, 0x2e, 0x11, 0xd9, 0xef, 0x76, 0x56, 0x63, 0x0c, 0x5c, 0x42, 0x51, 0x17, 0xaa, 0x16,
+	0x0e, 0xa8, 0xed, 0xf2, 0x40, 0x0c, 0xa4, 0x89, 0x56, 0x38, 0xba, 0x6e, 0x4a, 0x4f, 0x50, 0xa2,
+	0x1f, 0x41, 0xee, 0x9c, 0x52, 0x8f, 0xfb, 0x73, 0x65, 0x77, 0xe7, 0x36, 0x0a, 0x75, 0x29, 0xf5,
+	0xba, 0x29, 0x9d, 0xd3, 0x37, 0x5f, 0x42, 0xb6, 0x8f, 0xdf, 0xa0, 0x0e, 0x14, 0xf9, 0xb9, 0x86,
+	0x55, 0xf3, 0x56, 0x3e, 0xa1, 0x68, 0x9b, 0x53, 0xc8, 0x31, 0xee, 0xa8, 0x11, 0x46, 0x89, 0x0a,
+	0x6b, 0x15, 0x27, 0x8d, 0x30, 0x4e, 0x54, 0x54, 0xab, 0x48, 0xb9, 0x1b, 0x8f, 0x14, 0x55, 0x60,
+	0x62, 0xb1, 0xb2, 0x29, 0x63, 0x25, 0x27, 0x97, 0xf8, 0x8c, 0x65, 0x15, 0xbe, 0x79, 0x38, 0x68,
+	0xfe, 0x53, 0x1a, 0x8a, 0xd2, 0x9b, 0x50, 0x57, 0x5a, 0x49, 0xf8, 0xce, 0xee, 0xad, 0x5c, 0x31,
+	0x69, 0x27, 0x2a, 0x35, 0xfb, 0x09, 0x14, 0xcf, 0xb1, 0x61, 0x61, 0x3f, 0x90, 0x4c, 0xbf, 0xbc,
+	0x3d, 0xd3, 0x56, 0x57, 0x70, 0xe8, 0xa6, 0x74, 0xc5, 0xac, 0x59, 0x86, 0xa2, 0x84, 0xb6, 0xcb,
+	0x61, 0x08, 0xc5, 0x86, 0xda, 0x7f, 0xa5, 0x01, 0x18, 0xf1, 0x2b, 0x61, 0xad, 0x2e, 0x80, 0x8f,
+	0x47, 0x76, 0x40, 0xb1, 0x8f, 0x45, 0xf2, 0x5c, 0xdb, 0x7d, 0x34, 0x27, 0x4a, 0x44, 0xd0, 0xd2,
+	0x43, 0x6c, 0x51, 0x94, 0xd5, 0x0c, 0x3d, 0x84, 0xea, 0xc4, 0x8d, 0xf1, 0x52, 0xe7, 0x92, 0x80,
+	0x6a, 0x2e, 0x40, 0xc4, 0x01, 0x15, 0x21, 0xfb, 0xa2, 0x33, 0xa8, 0xa7, 0x50, 0x09, 0x72, 0xbd,
+	0x93, 0xfe, 0xa0, 0x9e, 0x66, 0xa0, 0xde, 0xeb, 0x41, 0x3d, 0x83, 0x00, 0x0a, 0x87, 0x9d, 0x97,
+	0x9d, 0x41, 0xa7, 0x9e, 0x45, 0x65, 0xc8, 0xf7, 0xf6, 0x07, 0x07, 0xdd, 0x7a, 0x0e, 0x55, 0xa0,
+	0x78, 0xd2, 0x1b, 0x1c, 0x9d, 0x1c, 0xf7, 0xeb, 0x79, 0x36, 0x39, 0x38, 0x39, 0x3e, 0xee, 0x1c,
+	0x0c, 0xea, 0x05, 0xc6, 0xa3, 0xdb, 0xd9, 0x3f, 0xac, 0x17, 0x19, 0xfa, 0x40, 0xdf, 0x3f, 0xe8,
+	0xd4, 0x4b, 0xed, 0x02, 0xe4, 0xe8, 0xd4, 0xc3, 0xda, 0x5f, 0xa5, 0xa1, 0xd0, 0x17, 0xae, 0x73,
+	0xb8, 0x40, 0xe5, 0xf9, 0xd0, 0x11, 0xc8, 0xdf, 0x56, 0xdd, 0xfb, 0x09, 0x75, 0x99, 0x84, 0x83,
+	0x41, 0xaf, 0x9e, 0x62, 0x12, 0xb2, 0x51, 0xbf, 0x9e, 0x0e, 0x25, 0xfc, 0x9b, 0x74, 0x78, 0x74,
+	0x68, 0x2f, 0xee, 0x1d, 0x2c, 0x8c, 0x3e, 0x9e, 0x3f, 0x12, 0xb1, 0x2e, 0x7f, 0x23, 0x07, 0x30,
+	0xa1, 0x20, 0x40, 0x0b, 0x9b, 0xb2, 0x8f, 0xa0, 0x7c, 0x69, 0x38, 0x13, 0x3c, 0x0c, 0xa8, 0x1f,
+	0x8a, 0x5c, 0xe2, 0xa0, 0x3e, 0xf5, 0xa3, 0xe5, 0x53, 0x5b, 0x74, 0xd9, 0xd5, 0x70, 0xb9, 0x6d,
+	0xf3, 0xd2, 0xcb, 0xc7, 0xda, 0x00, 0xca, 0x47, 0xbd, 0x7d, 0xcb, 0xf2, 0x71, 0xc0, 0x5a, 0x9c,
+	0x9c, 0xed, 0x5d, 0x7e, 0xce, 0xf7, 0x29, 0x32, 0x47, 0x67, 0x33, 0xf4, 0x84, 0x43, 0x9f, 0xcb,
+	0x4c, 0xf9, 0xde, 0x9c, 0xfc, 0x47, 0xbd, 0xcb, 0xe7, 0x12, 0xf9, 0x79, 0x3b, 0x07, 0x19, 0xdb,
+	0xd3, 0x76, 0x20, 0xc7, 0xa0, 0xac, 0x67, 0x3a, 0xb3, 0xfd, 0x40, 0x54, 0xa4, 0x82, 0x2e, 0x26,
+	0x4c, 0x1d, 0xc7, 0x08, 0x44, 0x15, 0x2f, 0xe8, 0x7c, 0xac, 0xbd, 0x04, 0x18, 0x98, 0x9e, 0x12,
+	0xe4, 0x31, 0xe3, 0x22, 0xc3, 0xa9, 0xb9, 0x60, 0x43, 0x89, 0xa7, 0x67, 0x6c, 0x8f, 0x57, 0x4c,
+	0x56, 0xaf, 0x33, 0xbc, 0x5e, 0xf3, 0xb1, 0x66, 0x41, 0xb6, 0x43, 0x18, 0x9b, 0xfa, 0xc8, 0xf7,
+	0xcc, 0xa1, 0xe8, 0xe0, 0x86, 0x26, 0xb1, 0x84, 0x0d, 0x6b, 0xdd, 0x94, 0xbe, 0xc6, 0x56, 0xfa,
+	0x7c, 0xe1, 0x80, 0x58, 0x98, 0xe1, 0xfa, 0x38, 0xc0, 0x74, 0x88, 0x7d, 0x9f, 0xf8, 0x02, 0x37,
+	0xa3, 0x70, 0xf9, 0x4a, 0x87, 0x2d, 0x30, 0xdc, 0x76, 0x1e, 0xb2, 0xd8, 0xb5, 0xb4, 0xff, 0x5c,
+	0x87, 0xd2, 0xc0, 0xf0, 0x3a, 0x97, 0xac, 0xfd, 0x78, 0x0a, 0x05, 0x11, 0xdf, 0x52, 0xec, 0x0f,
+	0xe7, 0xb3, 0x40, 0xa8, 0x9f, 0x2e, 0x51, 0xd1, 0x0b, 0xa8, 0x88, 0xd1, 0x70, 0x8c, 0xa9, 0x21,
+	0x53, 0xf7, 0xa3, 0x45, 0xf9, 0x83, 0x6f, 0xd2, 0xea, 0xb8, 0x96, 0x47, 0x6c, 0x97, 0xbe, 0xc2,
+	0xd4, 0xd0, 0x41, 0x90, 0xb2, 0x31, 0xfa, 0x21, 0x54, 0x62, 0xc5, 0x40, 0x1e, 0xd5, 0xb5, 0x22,
+	0xc4, 0xf1, 0xd1, 0xd7, 0x50, 0x8f, 0x4d, 0x85, 0x30, 0xb9, 0x5b, 0x09, 0xb3, 0x1e, 0xa3, 0xe7,
+	0x12, 0xb5, 0x01, 0x7c, 0x32, 0xa1, 0x52, 0xb3, 0x22, 0x67, 0xf6, 0x60, 0x39, 0x33, 0x9d, 0xe1,
+	0x72, 0x4e, 0x65, 0x5f, 0x0d, 0xd1, 0xd7, 0xb0, 0xce, 0x5b, 0xcb, 0xa1, 0x65, 0xfb, 0xa2, 0xea,
+	0xf1, 0xae, 0x6c, 0x6d, 0x77, 0x6b, 0x39, 0xa3, 0x1e, 0x23, 0x38, 0x54, 0xf8, 0xfa, 0x9a, 0x97,
+	0x98, 0xa3, 0xcf, 0x65, 0xfe, 0x17, 0x15, 0xfb, 0xee, 0x72, 0x3e, 0x89, 0x5c, 0xff, 0x17, 0x69,
+	0xa8, 0xc6, 0xd5, 0x45, 0xbf, 0x0f, 0x05, 0xc7, 0x38, 0xc5, 0x8e, 0x8a, 0xea, 0xdd, 0xd5, 0xcc,
+	0xd4, 0x7a, 0xc9, 0x89, 0x3a, 0x2e, 0xf5, 0xa7, 0xba, 0xe4, 0xd0, 0xdc, 0x83, 0x4a, 0x0c, 0x8c,
+	0xea, 0x90, 0xbd, 0xc0, 0x53, 0x19, 0xeb, 0x6c, 0xc8, 0xa2, 0x88, 0x07, 0xab, 0xba, 0x7f, 0xf1,
+	0xc9, 0x97, 0x99, 0x2f, 0xd2, 0xcd, 0x5f, 0xa4, 0xa1, 0x1c, 0x5a, 0x0e, 0xbd, 0x98, 0x11, 0x6a,
+	0x7b, 0x05, 0x73, 0x7f, 0xd7, 0x12, 0xfd, 0x65, 0x59, 0x96, 0xc5, 0x13, 0xa8, 0xfa, 0xa2, 0xd2,
+	0x0d, 0x6d, 0xd7, 0x56, 0x3d, 0xe9, 0xe3, 0xeb, 0x0d, 0xde, 0x92, 0xc5, 0xf1, 0xc8, 0xb5, 0x29,
+	0xbb, 0xcc, 0xf9, 0xd1, 0x14, 0xe9, 0x50, 0xf3, 0xe5, 0xbd, 0x56, 0x70, 0xbc, 0xa6, 0x55, 0x4d,
+	0x70, 0x14, 0x34, 0x92, 0x65, 0xd5, 0x8f, 0xcd, 0x85, 0x90, 0x92, 0x27, 0x76, 0x2d, 0xe9, 0x15,
+	0x8f, 0x57, 0x64, 0xd9, 0x71, 0x2d, 0x21, 0x64, 0x38, 0x6d, 0x3e, 0x87, 0x52, 0x9f, 0xfa, 0xd8,
+	0x18, 0x1f, 0xf1, 0xab, 0xf4, 0xa9, 0x11, 0xc8, 0x8c, 0xa3, 0xf3, 0xb1, 0xb8, 0x5c, 0xb2, 0x75,
+	0x2e, 0x7d, 0x4e, 0x97, 0xb3, 0xe6, 0x9f, 0x67, 0xa0, 0x12, 0xd3, 0x1d, 0x7d, 0x1f, 0x32, 0xb6,
+	0x25, 0x6d, 0xf6, 0xe9, 0x0d, 0xe2, 0xa8, 0x0d, 0xf5, 0x8c, 0x6d, 0xb1, 0x34, 0x14, 0xeb, 0xa6,
+	0x16, 0xe5, 0x80, 0xa8, 0x03, 0x08, 0x1b, 0xad, 0xed, 0xb0, 0x39, 0x13, 0x06, 0xf8, 0x8d, 0x25,
+	0x35, 0x34, 0xec, 0xd9, 0x12, 0x77, 0x98, 0xdc, 0xb2, 0x3b, 0x4c, 0x3e, 0xba, 0xc3, 0xa0, 0xdd,
+	0xa8, 0x0e, 0x8a, 0xfb, 0x71, 0x63, 0x59, 0x1d, 0x8c, 0x0a, 0xe0, 0xbf, 0xa7, 0xa1, 0x1a, 0x3f,
+	0xbe, 0x77, 0xb7, 0xca, 0x0b, 0x40, 0xfc, 0xce, 0x3d, 0x4c, 0xb8, 0x64, 0xe6, 0xa6, 0x6b, 0x71,
+	0x9d, 0x13, 0xc5, 0xcf, 0xe5, 0x63, 0xa8, 0xb0, 0x84, 0x20, 0x2b, 0x0a, 0x37, 0x57, 0x4d, 0x07,
+	0x06, 0x12, 0xa5, 0x24, 0xae, 0x67, 0x6e, 0x55, 0x3d, 0x7f, 0xc5, 0x0f, 0x3f, 0x74, 0xa2, 0xff,
+	0x03, 0x6a, 0x1e, 0xc1, 0x1d, 0xc5, 0x28, 0x1e, 0x71, 0xd9, 0x9b, 0x38, 0x6d, 0x48, 0x4e, 0xb1,
+	0x33, 0xfb, 0x04, 0xd6, 0x42, 0x26, 0xa7, 0x53, 0x8a, 0x85, 0x5d, 0x72, 0x7a, 0x18, 0xcc, 0x6d,
+	0x06, 0x44, 0x8f, 0x20, 0x8b, 0x49, 0x20, 0x2b, 0xe0, 0xfc, 0x43, 0x55, 0x87, 0x04, 0x3a, 0x43,
+	0x40, 0x9f, 0x43, 0x89, 0xfa, 0x86, 0xed, 0xac, 0xe2, 0x48, 0x21, 0x26, 0x6b, 0x77, 0x30, 0xb3,
+	0x99, 0xf6, 0x05, 0xac, 0x25, 0x0b, 0x04, 0x6b, 0x3c, 0x5f, 0x1f, 0xff, 0xf8, 0xf8, 0xe4, 0xa7,
+	0xc7, 0xf5, 0x14, 0x9b, 0x1c, 0x1d, 0xb7, 0x4f, 0x5e, 0x1f, 0x1f, 0xd6, 0xd3, 0xa8, 0x0a, 0xa5,
+	0x93, 0xd7, 0x03, 0x31, 0xcb, 0x44, 0x2c, 0xee, 0x41, 0x69, 0xdf, 0xb3, 0x79, 0x33, 0xc0, 0xf2,
+	0x20, 0x6f, 0x17, 0x64, 0x6e, 0x14, 0x13, 0xed, 0x97, 0x19, 0x28, 0xf7, 0x88, 0xc5, 0x51, 0x02,
+	0xf4, 0x15, 0x14, 0x38, 0x58, 0x65, 0xe5, 0x07, 0x8b, 0x5e, 0xe1, 0x04, 0x6e, 0x38, 0xd2, 0x25,
+	0x49, 0xf3, 0x57, 0x69, 0x28, 0x29, 0x20, 0xd2, 0xa1, 0x6c, 0x12, 0x97, 0x1a, 0xb6, 0x8b, 0xfd,
+	0xa5, 0x17, 0x98, 0x79, 0x66, 0xad, 0x03, 0x45, 0xc4, 0xa7, 0xec, 0x0e, 0x15, 0xb2, 0x69, 0x5e,
+	0xc2, 0x5a, 0x72, 0x19, 0x35, 0xa0, 0x38, 0xc6, 0x41, 0x60, 0x8c, 0x54, 0xbf, 0xa9, 0xa6, 0x2c,
+	0xea, 0xa3, 0xfd, 0xe5, 0xa3, 0x67, 0x08, 0x60, 0xb6, 0xb0, 0xc7, 0x8c, 0x4a, 0xbc, 0xe9, 0x8a,
+	0x09, 0x4b, 0x78, 0x3e, 0x36, 0x02, 0xe2, 0xaa, 0xd7, 0x34, 0x31, 0xe3, 0xe6, 0xe4, 0xc6, 0xea,
+	0x41, 0x49, 0xdd, 0x8c, 0xae, 0x7f, 0xe0, 0xe5, 0x0f, 0x36, 0x53, 0x4f, 0xd5, 0x1c, 0x3e, 0x0e,
+	0x3b, 0xe3, 0x6c, 0xd4, 0x19, 0x6b, 0x6f, 0x60, 0x63, 0xee, 0xb6, 0x8c, 0x9e, 0x41, 0x49, 0x3d,
+	0x3f, 0x49, 0xd3, 0x7d, 0xb0, 0xf4, 0x8e, 0xad, 0x87, 0xa8, 0xcc, 0x7b, 0x79, 0x4d, 0x1c, 0x26,
+	0x9e, 0x66, 0xcb, 0x7a, 0x8d, 0x43, 0xfb, 0xea, 0xed, 0xf5, 0x67, 0x50, 0x53, 0xc4, 0xc2, 0x88,
+	0xef, 0xb8, 0x5d, 0xe8, 0x4f, 0x99, 0xb8, 0x3f, 0xfd, 0x3a, 0x03, 0x88, 0xa5, 0x97, 0xfe, 0x64,
+	0x3c, 0x36, 0xfc, 0xa9, 0x7a, 0xef, 0x89, 0x3f, 0x18, 0xa7, 0x6f, 0xff, 0x60, 0xcc, 0x72, 0x19,
+	0xb5, 0xc7, 0x78, 0x78, 0x65, 0xbb, 0x16, 0xb9, 0x92, 0x5b, 0x02, 0x03, 0xfd, 0x94, 0x43, 0xd0,
+	0x6f, 0x41, 0xce, 0x25, 0xae, 0x2a, 0x0a, 0xef, 0xcf, 0x07, 0xe5, 0xd8, 0xa3, 0x53, 0xd6, 0x23,
+	0x31, 0x2c, 0xf4, 0x03, 0xa8, 0x50, 0x32, 0x0c, 0xb5, 0xce, 0xdd, 0xa0, 0x35, 0xbb, 0x84, 0x51,
+	0x12, 0x1e, 0xfd, 0xef, 0x41, 0xed, 0xcc, 0x27, 0xe3, 0x88, 0x3e, 0x7f, 0x33, 0x7d, 0x95, 0x51,
+	0x84, 0x1c, 0x3e, 0x02, 0x08, 0x2e, 0x6c, 0x91, 0x9a, 0x45, 0x6e, 0x28, 0xe9, 0x65, 0x06, 0x61,
+	0xa6, 0x0b, 0xd0, 0x87, 0x50, 0xa6, 0xa6, 0x5a, 0x2d, 0xf2, 0xd5, 0x12, 0x35, 0xc5, 0x62, 0x1b,
+	0xa0, 0x44, 0x26, 0xf4, 0x94, 0x4c, 0x5c, 0x4b, 0xfb, 0x97, 0x34, 0xdc, 0x49, 0x58, 0x5b, 0xbe,
+	0xa5, 0xef, 0x41, 0x86, 0x5c, 0x2c, 0xcd, 0xca, 0x0b, 0x28, 0x5a, 0x27, 0x17, 0xdd, 0x94, 0x9e,
+	0x21, 0x17, 0xe8, 0x79, 0xfc, 0x58, 0x17, 0x75, 0x9d, 0x09, 0xe7, 0xe9, 0xa6, 0xe4, 0xc1, 0x37,
+	0xf7, 0x21, 0x73, 0x72, 0x81, 0xbe, 0x02, 0xfe, 0xa8, 0x3d, 0xa4, 0xc6, 0xa9, 0x13, 0xbe, 0xc6,
+	0x34, 0x17, 0x4a, 0x30, 0x60, 0x28, 0x3a, 0x04, 0x6a, 0xc8, 0x35, 0x53, 0x89, 0x56, 0xfb, 0xdb,
+	0x0c, 0x40, 0xdb, 0x08, 0x6c, 0x53, 0x58, 0xe4, 0x01, 0xd4, 0x82, 0x89, 0x69, 0xe2, 0x80, 0xdd,
+	0x8c, 0x26, 0xae, 0x68, 0xd1, 0x72, 0x7a, 0x55, 0x02, 0x0f, 0x18, 0x8c, 0x21, 0x9d, 0x19, 0xb6,
+	0x33, 0xf1, 0xb1, 0x44, 0x12, 0x7d, 0x4b, 0x55, 0x02, 0x05, 0xd2, 0x43, 0x16, 0x25, 0x14, 0xbb,
+	0xe6, 0x74, 0x38, 0x0e, 0x86, 0xde, 0xb3, 0x1d, 0xee, 0x32, 0x39, 0xbd, 0x2a, 0xa1, 0xaf, 0x82,
+	0xde, 0xb3, 0x9d, 0x59, 0xac, 0xbd, 0x67, 0xb2, 0x12, 0xc4, 0xb0, 0xf6, 0x9e, 0xcd, 0x61, 0xed,
+	0x71, 0x4f, 0x48, 0x62, 0xed, 0xa1, 0x1d, 0xd8, 0x34, 0x4c, 0x3a, 0x31, 0x9c, 0x61, 0x52, 0x85,
+	0x02, 0xc7, 0x45, 0x62, 0xad, 0x1f, 0x57, 0x24, 0xa2, 0x48, 0xea, 0x53, 0x8c, 0x53, 0xfc, 0x28,
+	0xa6, 0x95, 0xf6, 0xf3, 0x34, 0x94, 0x06, 0xd2, 0x43, 0xd0, 0x6f, 0x42, 0x9d, 0x78, 0x98, 0xff,
+	0x43, 0xe1, 0x8a, 0x48, 0x0a, 0xa4, 0xbd, 0xd6, 0x19, 0xfc, 0x20, 0x02, 0xa3, 0x2d, 0x76, 0x93,
+	0x34, 0x2c, 0x51, 0xed, 0x86, 0x94, 0x50, 0xc3, 0x91, 0x56, 0x5b, 0x63, 0x70, 0x5e, 0xef, 0x06,
+	0x0c, 0x8a, 0x1e, 0xc3, 0xc6, 0x95, 0x6f, 0x53, 0x9c, 0x40, 0x15, 0xa6, 0x5b, 0xe7, 0x0b, 0x11,
+	0xae, 0xd6, 0x87, 0x8d, 0x81, 0x6f, 0x9c, 0x9d, 0xd9, 0x66, 0xdf, 0x73, 0x6c, 0x2a, 0xa4, 0x42,
+	0x90, 0x33, 0x3c, 0xfc, 0x56, 0xa5, 0x44, 0x36, 0xe6, 0xb7, 0x6b, 0x6c, 0x9c, 0xa9, 0x94, 0xc8,
+	0xc6, 0x2c, 0x0b, 0x5f, 0x61, 0x7b, 0x74, 0x4e, 0x55, 0x16, 0x16, 0x33, 0xed, 0x7f, 0xf2, 0x50,
+	0x0e, 0xfd, 0x06, 0xb5, 0xa1, 0xec, 0x11, 0x6b, 0x38, 0xf2, 0xc9, 0x44, 0x5d, 0xbe, 0x1f, 0x2c,
+	0x77, 0x33, 0x56, 0x5f, 0x5e, 0x30, 0xd4, 0x6e, 0x4a, 0x2f, 0x79, 0x72, 0xdc, 0xfc, 0xeb, 0x3c,
+	0x2f, 0x58, 0x7c, 0x82, 0xbe, 0x82, 0x9c, 0x4f, 0xae, 0x94, 0xcb, 0x7e, 0xba, 0x02, 0xaf, 0x96,
+	0x4e, 0xae, 0x74, 0x4e, 0xd4, 0xfc, 0xd7, 0x1c, 0x64, 0x75, 0x72, 0xf5, 0xae, 0xa9, 0xf4, 0xc6,
+	0xec, 0x16, 0xfd, 0xcf, 0x53, 0x4e, 0xfc, 0xcf, 0xb3, 0x05, 0xf5, 0x31, 0x0e, 0xce, 0xb1, 0x35,
+	0x64, 0xc6, 0x10, 0x4e, 0x22, 0xce, 0x64, 0x4d, 0xc0, 0x7b, 0xc4, 0x12, 0x2e, 0xf5, 0x18, 0x36,
+	0xfc, 0x89, 0xeb, 0xda, 0xee, 0x28, 0x86, 0x2a, 0x7c, 0x7a, 0x5d, 0x2e, 0x84, 0xb8, 0x5b, 0x50,
+	0x67, 0x7e, 0x97, 0xe0, 0x2a, 0x9c, 0x75, 0x4d, 0xc0, 0x43, 0xcc, 0xcf, 0x20, 0x2f, 0x92, 0x54,
+	0x7e, 0x49, 0x03, 0x1f, 0x85, 0xb0, 0x2e, 0x30, 0xd1, 0xf3, 0x78, 0x6e, 0x2b, 0x2d, 0xb1, 0x91,
+	0x72, 0xe5, 0x28, 0xed, 0xa1, 0x1f, 0x42, 0x89, 0x06, 0x92, 0x0c, 0x96, 0x54, 0x90, 0x39, 0xa7,
+	0xd3, 0x8b, 0x34, 0x10, 0xe4, 0x3f, 0x83, 0x9a, 0x68, 0x53, 0x86, 0xa7, 0x53, 0xa6, 0x56, 0xa3,
+	0xc8, 0xcf, 0xf9, 0x8b, 0x15, 0xcf, 0xb9, 0x25, 0xfa, 0x94, 0xf6, 0x94, 0x35, 0x2a, 0xfc, 0xfe,
+	0x59, 0xc1, 0x11, 0xa4, 0xf9, 0x0d, 0xd4, 0x67, 0x11, 0x16, 0xdc, 0x44, 0x77, 0xe2, 0x37, 0xd1,
+	0x45, 0x69, 0x31, 0xec, 0x87, 0x62, 0xb7, 0x54, 0xd6, 0x7d, 0xf0, 0x6c, 0xaa, 0x1d, 0x43, 0xb5,
+	0x63, 0x8d, 0xa2, 0xbf, 0x98, 0xbf, 0x65, 0x4d, 0xd5, 0xfe, 0x3e, 0x0d, 0x35, 0xc9, 0x50, 0x96,
+	0x8d, 0xa7, 0xb1, 0xb2, 0x71, 0x7f, 0xbe, 0x84, 0xc6, 0x71, 0xbf, 0x7d, 0xc1, 0xf8, 0x8c, 0x17,
+	0x8c, 0x27, 0x90, 0xc7, 0x8c, 0xaf, 0x8c, 0xbb, 0xf7, 0x16, 0xee, 0xaa, 0x0b, 0x9c, 0x44, 0x81,
+	0xf8, 0x87, 0x34, 0xe4, 0xd8, 0x1a, 0x7a, 0x02, 0xd9, 0xc0, 0x37, 0x6f, 0x0e, 0x37, 0x86, 0xc5,
+	0x90, 0xad, 0x20, 0xba, 0x66, 0x2c, 0x47, 0xb6, 0x02, 0xca, 0xca, 0xb0, 0xe9, 0xd8, 0xd8, 0xa5,
+	0x43, 0xdb, 0x92, 0x29, 0xaa, 0x24, 0x00, 0x47, 0x16, 0x5b, 0x0c, 0xb0, 0x7f, 0x89, 0x7d, 0xb6,
+	0x28, 0x32, 0x55, 0x49, 0x00, 0x8e, 0x2c, 0xf4, 0x08, 0xd6, 0x5d, 0x32, 0xb4, 0x2d, 0xec, 0x52,
+	0x9b, 0xb2, 0xe2, 0x30, 0x92, 0x17, 0xcc, 0x9a, 0x4b, 0x8e, 0x24, 0xf4, 0x55, 0x30, 0xd2, 0x7e,
+	0x9d, 0x86, 0xfa, 0x80, 0x78, 0xfc, 0x85, 0x23, 0xf8, 0xff, 0xd1, 0x2b, 0x15, 0x6f, 0xd5, 0x2b,
+	0x25, 0xba, 0x95, 0x7f, 0x4c, 0xc3, 0x46, 0x4c, 0x5b, 0xe9, 0x74, 0xef, 0xe8, 0x3f, 0xec, 0xe6,
+	0x49, 0x2e, 0xa4, 0x0e, 0x9f, 0xcc, 0xa7, 0x82, 0xd9, 0x7d, 0x42, 0x87, 0x6d, 0xee, 0x71, 0xc7,
+	0x7b, 0x0a, 0x05, 0xfe, 0x78, 0xa7, 0x3c, 0x6f, 0x3e, 0x77, 0x71, 0x7a, 0xd1, 0xa5, 0x48, 0xd4,
+	0x84, 0x03, 0xfe, 0x47, 0x1a, 0x20, 0x42, 0x41, 0x4f, 0x13, 0xf5, 0xe3, 0xe3, 0x6b, 0xb8, 0x45,
+	0x75, 0x03, 0x35, 0x63, 0xf5, 0x42, 0x9c, 0x53, 0x38, 0x6f, 0xfe, 0x69, 0x5a, 0xd4, 0x94, 0x4d,
+	0xc8, 0xf3, 0xdd, 0xd5, 0xbd, 0x8d, 0x4f, 0x6e, 0x3e, 0xe4, 0xc4, 0xb3, 0x47, 0x61, 0xf6, 0xd9,
+	0xe3, 0xf6, 0x89, 0x5b, 0xfb, 0xe7, 0x0c, 0xd4, 0x5e, 0x18, 0x14, 0x5f, 0x19, 0xd3, 0x40, 0xa8,
+	0xfc, 0x3c, 0xa1, 0xf2, 0xbc, 0x93, 0x26, 0xb0, 0x63, 0xd5, 0xf2, 0xe7, 0x19, 0xa1, 0xd9, 0x8d,
+	0x57, 0x28, 0x7e, 0x5d, 0xca, 0xc4, 0xfe, 0x48, 0xb8, 0x0f, 0x55, 0xd3, 0x99, 0x04, 0x14, 0xfb,
+	0xc3, 0xd8, 0x55, 0xaa, 0x22, 0x61, 0xc7, 0x0c, 0xe5, 0x53, 0x58, 0xf7, 0x0c, 0xdb, 0xc7, 0xd6,
+	0x30, 0xfc, 0x92, 0x46, 0x94, 0xb9, 0x35, 0x01, 0x56, 0xdf, 0xdc, 0xf0, 0x6f, 0x24, 0x1c, 0xfb,
+	0x12, 0x87, 0xdf, 0x48, 0xb0, 0xc9, 0x82, 0xf6, 0xb0, 0xb0, 0x52, 0x7b, 0x58, 0x5c, 0xa9, 0x3d,
+	0x2c, 0xcd, 0xb7, 0x87, 0xda, 0x2f, 0xd2, 0xb0, 0xae, 0x2c, 0xa5, 0x52, 0x40, 0x0b, 0xee, 0xf8,
+	0x78, 0x4c, 0x28, 0x1e, 0x26, 0xd4, 0x15, 0x36, 0xda, 0x10, 0x4b, 0x07, 0x31, 0xa5, 0x9f, 0xc0,
+	0xc6, 0x48, 0xb0, 0x18, 0xce, 0x7e, 0xea, 0x53, 0x97, 0x0b, 0xd1, 0xf7, 0x18, 0x33, 0xae, 0x93,
+	0x9d, 0x75, 0x1d, 0xed, 0xdf, 0xd2, 0x50, 0x8f, 0x24, 0x0a, 0xc3, 0x34, 0xaa, 0x0d, 0x0f, 0x97,
+	0x1e, 0xf5, 0x77, 0x57, 0x1e, 0x7e, 0xcc, 0xa3, 0xb4, 0x03, 0x6b, 0x52, 0xfe, 0x40, 0xdc, 0x29,
+	0xa4, 0x04, 0x77, 0xaf, 0x77, 0x36, 0xbd, 0x36, 0x8a, 0x4f, 0xe3, 0x71, 0xbb, 0xfb, 0x67, 0x45,
+	0xc8, 0xee, 0x7b, 0x36, 0xfa, 0x06, 0x2a, 0xb1, 0x8b, 0x10, 0x7a, 0x70, 0xfd, 0x35, 0x89, 0x9f,
+	0x4b, 0xf3, 0xe1, 0x2a, 0x77, 0x29, 0x2d, 0x85, 0xba, 0x90, 0xe7, 0xd5, 0x12, 0x7d, 0xb4, 0xac,
+	0x8a, 0x0a, 0x7e, 0x77, 0xaf, 0x2f, 0xb2, 0x5a, 0x0a, 0x7d, 0x0d, 0x25, 0xa5, 0x19, 0xba, 0x77,
+	0x8d, 0xd9, 0x05, 0xbf, 0xfb, 0x37, 0x1e, 0x8c, 0x96, 0x42, 0x03, 0x28, 0x87, 0xd9, 0x11, 0xdd,
+	0xbf, 0x2e, 0x73, 0x0a, 0xa6, 0xda, 0xcd, 0xc9, 0x55, 0x08, 0xaa, 0x3e, 0xe9, 0x5a, 0x20, 0xe8,
+	0xcc, 0x27, 0x66, 0x0b, 0x04, 0x9d, 0xfd, 0x1e, 0x4c, 0x4b, 0xa1, 0x3f, 0x84, 0x6a, 0xfc, 0x2b,
+	0x39, 0xf4, 0x70, 0x21, 0xd1, 0xcc, 0x97, 0x77, 0xcd, 0x4f, 0x6e, 0xc0, 0x0a, 0xd9, 0x1f, 0x42,
+	0x76, 0x60, 0x78, 0xe8, 0xc3, 0x45, 0xaf, 0x96, 0x8a, 0xd9, 0x07, 0x4b, 0x9f, 0x34, 0xb5, 0xec,
+	0x9f, 0x64, 0xd2, 0x3b, 0x69, 0xf4, 0x07, 0x50, 0x4b, 0xfc, 0x65, 0x8e, 0x3e, 0x59, 0xe9, 0x2f,
+	0xf5, 0x15, 0x38, 0xef, 0x43, 0x51, 0x7d, 0xa7, 0xb4, 0xa4, 0x46, 0x37, 0xbf, 0x37, 0x07, 0x8f,
+	0x7d, 0xfe, 0xa8, 0xa5, 0x90, 0x03, 0xe5, 0x3e, 0x76, 0xce, 0x0e, 0xce, 0xb1, 0x79, 0x81, 0x62,
+	0xdf, 0xb2, 0x88, 0xcf, 0x2b, 0x5b, 0xf1, 0xcf, 0x2b, 0x43, 0x3c, 0x25, 0x60, 0x6b, 0x55, 0xf4,
+	0xd0, 0xa0, 0x5f, 0x40, 0xe1, 0x80, 0x7f, 0x96, 0xb9, 0x54, 0xde, 0xcd, 0x38, 0x4f, 0xfe, 0x01,
+	0xe7, 0xbe, 0xe3, 0x68, 0xa9, 0xf6, 0xd3, 0x6f, 0x3e, 0x1b, 0xd9, 0xf4, 0x7c, 0x72, 0xca, 0xb6,
+	0xda, 0x96, 0x38, 0xea, 0x77, 0x77, 0x3b, 0xfa, 0xaa, 0x6c, 0x7b, 0x84, 0xdd, 0x6d, 0xc1, 0xf2,
+	0xb4, 0xc0, 0xdf, 0x74, 0x9f, 0xfe, 0x6f, 0x00, 0x00, 0x00, 0xff, 0xff, 0xcf, 0x75, 0xc9, 0xb9,
+	0x6c, 0x2a, 0x00, 0x00,
 }
 
 // Reference imports to suppress errors if they are not otherwise used.
@@ -4302,6 +4628,7 @@ const _ = grpc.SupportPackageIsVersion4
 type ApiClient interface {
 	StatSummary(ctx context.Context, in *StatSummaryRequest, opts ...grpc.CallOption) (*StatSummaryResponse, error)
 	Edges(ctx context.Context, in *EdgesRequest, opts ...grpc.CallOption) (*EdgesResponse, error)
+	Gateways(ctx context.Context, in *GatewaysRequest, opts ...grpc.CallOption) (*GatewaysResponse, error)
 	TopRoutes(ctx context.Context, in *TopRoutesRequest, opts ...grpc.CallOption) (*TopRoutesResponse, error)
 	ListPods(ctx context.Context, in *ListPodsRequest, opts ...grpc.CallOption) (*ListPodsResponse, error)
 	ListServices(ctx context.Context, in *ListServicesRequest, opts ...grpc.CallOption) (*ListServicesResponse, error)
@@ -4340,6 +4667,15 @@ func (c *apiClient) Edges(ctx context.Context, in *EdgesRequest, opts ...grpc.Ca
 	return out, nil
 }
 
+func (c *apiClient) Gateways(ctx context.Context, in *GatewaysRequest, opts ...grpc.CallOption) (*GatewaysResponse, error) {
+	out := new(GatewaysResponse)
+	err := c.cc.Invoke(ctx, "/linkerd2.public.Api/Gateways", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
 func (c *apiClient) TopRoutes(ctx context.Context, in *TopRoutesRequest, opts ...grpc.CallOption) (*TopRoutesResponse, error) {
 	out := new(TopRoutesResponse)
 	err := c.cc.Invoke(ctx, "/linkerd2.public.Api/TopRoutes", in, out, opts...)
@@ -4464,6 +4800,7 @@ func (c *apiClient) Config(ctx context.Context, in *Empty, opts ...grpc.CallOpti
 type ApiServer interface {
 	StatSummary(context.Context, *StatSummaryRequest) (*StatSummaryResponse, error)
 	Edges(context.Context, *EdgesRequest) (*EdgesResponse, error)
+	Gateways(context.Context, *GatewaysRequest) (*GatewaysResponse, error)
 	TopRoutes(context.Context, *TopRoutesRequest) (*TopRoutesResponse, error)
 	ListPods(context.Context, *ListPodsRequest) (*ListPodsResponse, error)
 	ListServices(context.Context, *ListServicesRequest) (*ListServicesResponse, error)
@@ -4486,6 +4823,9 @@ func (*UnimplementedApiServer) StatSummary(ctx context.Context, req *StatSummary
 func (*UnimplementedApiServer) Edges(ctx context.Context, req *EdgesRequest) (*EdgesResponse, error) {
 	return nil, status.Errorf(codes.Unimplemented, "method Edges not implemented")
 }
+func (*UnimplementedApiServer) Gateways(ctx context.Context, req *GatewaysRequest) (*GatewaysResponse, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method Gateways not implemented")
+}
 func (*UnimplementedApiServer) TopRoutes(ctx context.Context, req *TopRoutesRequest) (*TopRoutesResponse, error) {
 	return nil, status.Errorf(codes.Unimplemented, "method TopRoutes not implemented")
 }
@@ -4551,6 +4891,24 @@ func _Api_Edges_Handler(srv interface{}, ctx context.Context, dec func(interface
 	return interceptor(ctx, in, info, handler)
 }
 
+func _Api_Gateways_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(GatewaysRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(ApiServer).Gateways(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/linkerd2.public.Api/Gateways",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(ApiServer).Gateways(ctx, req.(*GatewaysRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
 func _Api_TopRoutes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
 	in := new(TopRoutesRequest)
 	if err := dec(in); err != nil {
@@ -4713,6 +5071,10 @@ var _Api_serviceDesc = grpc.ServiceDesc{
 			MethodName: "Edges",
 			Handler:    _Api_Edges_Handler,
 		},
+		{
+			MethodName: "Gateways",
+			Handler:    _Api_Gateways_Handler,
+		},
 		{
 			MethodName: "TopRoutes",
 			Handler:    _Api_TopRoutes_Handler,
diff --git a/pkg/charts/charts.go b/pkg/charts/charts.go
index 9784f83e0..429001d22 100644
--- a/pkg/charts/charts.go
+++ b/pkg/charts/charts.go
@@ -99,8 +99,8 @@ func (chart *Chart) RenderCNI() (bytes.Buffer, error) {
 	return chart.render(cniPartials)
 }
 
-// RenderServiceMirror returns a bytes buffer with the result of rendering a Helm chart
-func (chart *Chart) RenderServiceMirror() (bytes.Buffer, error) {
+// RenderNoPartials returns a bytes buffer with the result of rendering a Helm chart with no partials
+func (chart *Chart) RenderNoPartials() (bytes.Buffer, error) {
 	return chart.render([]*chartutil.BufferedFile{})
 }
 
diff --git a/pkg/charts/multicluster/values.go b/pkg/charts/multicluster/values.go
new file mode 100644
index 000000000..3c12177a1
--- /dev/null
+++ b/pkg/charts/multicluster/values.go
@@ -0,0 +1,60 @@
+package multicluster
+
+import (
+	"fmt"
+
+	"github.com/linkerd/linkerd2/pkg/charts"
+	"github.com/linkerd/linkerd2/pkg/k8s"
+	"k8s.io/helm/pkg/chartutil"
+	"sigs.k8s.io/yaml"
+)
+
+const helmDefaultChartDir = "linkerd2-multicluster-remote-setup"
+
+// Values contains the top-level elements in the Helm charts
+type Values struct {
+	CliVersion              string `json:"cliVersion"`
+	GatewayName             string `json:"gatewayName"`
+	GatewayNamespace        string `json:"gatewayNamespace"`
+	IdentityTrustDomain     string `json:"identityTrustDomain"`
+	IncomingPort            uint32 `json:"incomingPort"`
+	LinkerdNamespace        string `json:"linkerdNamespace"`
+	ProbePath               string `json:"probePath"`
+	ProbePeriodSeconds      uint32 `json:"probePeriodSeconds"`
+	ProbePort               uint32 `json:"probePort"`
+	ProxyOutboundPort       uint32 `json:"proxyOutboundPort"`
+	ServiceAccountName      string `json:"serviceAccountName"`
+	ServiceAccountNamespace string `json:"serviceAccountNamespace"`
+	NginxImageVersion       string `json:"nginxImageVersion"`
+	NginxImage              string `json:"nginxImage"`
+	LinkerdVersion          string `json:"linkerdVersion"`
+	CreatedByAnnotation     string `json:"createdByAnnotation"`
+}
+
+// NewValues returns a new instance of the Values type.
+func NewValues() (*Values, error) {
+	chartDir := fmt.Sprintf("%s/", helmDefaultChartDir)
+	v, err := readDefaults(chartDir)
+	if err != nil {
+		return nil, err
+	}
+
+	v.CliVersion = k8s.CreatedByAnnotationValue()
+	return v, nil
+}
+
+// readDefaults read all the default variables from the values.yaml file.
+// chartDir is the root directory of the Helm chart where values.yaml is.
+func readDefaults(chartDir string) (*Values, error) {
+	file := &chartutil.BufferedFile{
+		Name: chartutil.ValuesfileName,
+	}
+	if err := charts.ReadFile(chartDir, file); err != nil {
+		return nil, err
+	}
+	values := Values{}
+	if err := yaml.Unmarshal(charts.InsertVersion(file.Data), &values); err != nil {
+		return nil, err
+	}
+	return &values, nil
+}
diff --git a/pkg/charts/servicemirror/values.go b/pkg/charts/servicemirror/values.go
index c84d5ec7b..203c3ead8 100644
--- a/pkg/charts/servicemirror/values.go
+++ b/pkg/charts/servicemirror/values.go
@@ -14,12 +14,13 @@ const (
 
 // Values contains the top-level elements in the Helm charts
 type Values struct {
-	Namespace              string `json:"namespace"`
-	ControllerImage        string `json:"controllerImage"`
-	ControllerImageVersion string `json:"controllerImageVersion"`
-	ServiceMirrorUID       int64  `json:"serviceMirrorUID"`
-	LogLevel               string `json:"logLevel"`
-	EventRequeueLimit      int32  `json:"eventRequeueLimit"`
+	Namespace                string `json:"namespace"`
+	ControllerImage          string `json:"controllerImage"`
+	ControllerImageVersion   string `json:"controllerImageVersion"`
+	ControllerComponentLabel string `json:"controllerComponentLabel"`
+	ServiceMirrorUID         int64  `json:"serviceMirrorUID"`
+	LogLevel                 string `json:"logLevel"`
+	EventRequeueLimit        int32  `json:"eventRequeueLimit"`
 }
 
 // NewValues returns a new instance of the Values type.
diff --git a/pkg/k8s/labels.go b/pkg/k8s/labels.go
index 3dfbaffea..dd5380169 100644
--- a/pkg/k8s/labels.go
+++ b/pkg/k8s/labels.go
@@ -388,6 +388,15 @@ const (
 	// GatewayIdentity can be found on the remote gateway service
 	GatewayIdentity = SvcMirrorPrefix + "/gateway-identity"
 
+	// GatewayProbePort the port on which the gateway can be probed
+	GatewayProbePort = SvcMirrorPrefix + "/probe-port"
+
+	// GatewayProbePeriod the interval at which the health of the gateway should be probed
+	GatewayProbePeriod = SvcMirrorPrefix + "/probe-period"
+
+	// GatewayProbePath the path at which the health of the gateway should be probed
+	GatewayProbePath = SvcMirrorPrefix + "/probe-path"
+
 	// ConfigKeyName is the key in the secret that stores the kubeconfig needed to connect
 	// to a remote cluster
 	ConfigKeyName = "kubeconfig"
diff --git a/proto/public.proto b/proto/public.proto
index a4af1f4f9..916f9f0af 100644
--- a/proto/public.proto
+++ b/proto/public.proto
@@ -483,10 +483,45 @@ message RouteTable {
   }
 }
 
+
+message GatewaysTable {
+  repeated Row rows = 1;
+  
+  message Row {
+    string namespace = 1;
+    string name = 2;
+    string cluster_name = 3;
+    uint64 paired_services = 4;
+    bool alive = 5;
+    uint64 latency_ms_p50 = 6;
+    uint64 latency_ms_p95 = 7;
+    uint64 latency_ms_p99 = 8;
+  }
+}
+
+message GatewaysRequest {
+  string remote_cluster_name = 1;
+  string gateway_namespace = 2;
+  string time_window = 3;
+}
+
+message GatewaysResponse {
+  oneof response {
+    Ok ok = 1;
+    ResourceError error = 2;
+  }
+
+  message Ok {
+    GatewaysTable gateways_table = 1;
+  }
+}
+
 service Api {
   rpc StatSummary(StatSummaryRequest) returns (StatSummaryResponse) {}
 
   rpc Edges(EdgesRequest) returns (EdgesResponse) {}
+  
+  rpc Gateways(GatewaysRequest) returns (GatewaysResponse) {}
 
   rpc TopRoutes(TopRoutesRequest) returns (TopRoutesResponse) {}
 
diff --git a/web/app/gh_ann_reporter.js b/web/app/gh_ann_reporter.js
new file mode 100644
index 000000000..0a1ba4ff7
--- /dev/null
+++ b/web/app/gh_ann_reporter.js
@@ -0,0 +1,26 @@
+class GhAnnReporter {
+  /* eslint-disable class-methods-use-this */
+  onRunComplete(contexts, results) {
+    // only pick the first line of the error
+    const msgRe = /^(.+)$/m;
+    const pathRe = /^.+\/linkerd2\/(.+)$/;
+    results.testResults.forEach(item => {
+      if (item.numFailingTests > 0) {
+        const msgArr = msgRe.exec(item.failureMessage);
+        let msg = 'Unknown';
+        if (msgArr.length > 0) {
+          msg = msgArr[1];
+        }
+        const pathArr = pathRe.exec(item.testFilePath);
+        let path = 'Unknown';
+        if (pathArr.length > 0) {
+          path = pathArr[1];
+        }
+        /* eslint-disable no-console */
+        console.log(`::error file=${path}::${msg}`);
+      }
+    });
+  }
+}
+
+module.exports = GhAnnReporter;


Test patch:
diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml
index f6a537aeb..2b169afe6 100644
--- a/.github/workflows/unit_tests.yml
+++ b/.github/workflows/unit_tests.yml
@@ -40,4 +40,4 @@ jobs:
         export PATH="$HOME/.yarn/bin:$PATH"
         export NODE_ENV=test
         bin/web
-        bin/web test --reporters=jest-dot-reporter
+        bin/web test --reporters="jest-dot-reporter" --reporters="./gh_ann_reporter.js"
diff --git a/cli/cmd/install_helm_test.go b/cli/cmd/install_helm_test.go
index 082484f45..7011e7802 100644
--- a/cli/cmd/install_helm_test.go
+++ b/cli/cmd/install_helm_test.go
@@ -23,14 +23,14 @@ func TestRenderHelm(t *testing.T) {
 
 	t.Run("Non-HA mode", func(t *testing.T) {
 		ha := false
-		chartControlPlane := chartControlPlane(t, ha, nil, "111", "222")
-		testRenderHelm(t, chartControlPlane, "", "install_helm_output.golden")
+		chartControlPlane := chartControlPlane(t, ha, "", "111", "222")
+		testRenderHelm(t, chartControlPlane, "install_helm_output.golden")
 	})
 
 	t.Run("HA mode", func(t *testing.T) {
 		ha := true
-		chartControlPlane := chartControlPlane(t, ha, nil, "111", "222")
-		testRenderHelm(t, chartControlPlane, "", "install_helm_output_ha.golden")
+		chartControlPlane := chartControlPlane(t, ha, "", "111", "222")
+		testRenderHelm(t, chartControlPlane, "install_helm_output_ha.golden")
 	})
 
 	t.Run("Non-HA with add-ons mode", func(t *testing.T) {
@@ -39,12 +39,12 @@ func TestRenderHelm(t *testing.T) {
 tracing:
   enabled: true
 `
-		chartControlPlane := chartControlPlane(t, ha, []l5dcharts.AddOn{l5dcharts.Tracing{}}, "111", "222")
-		testRenderHelm(t, chartControlPlane, addOnConfig, "install_helm_output_addons.golden")
+		chartControlPlane := chartControlPlane(t, ha, addOnConfig, "111", "222")
+		testRenderHelm(t, chartControlPlane, "install_helm_output_addons.golden")
 	})
 }
 
-func testRenderHelm(t *testing.T, chart *pb.Chart, addOnConfig string, goldenFileName string) {
+func testRenderHelm(t *testing.T, chart *pb.Chart, goldenFileName string) {
 	var (
 		chartName = "linkerd2"
 		namespace = "linkerd-dev"
@@ -102,14 +102,6 @@ func testRenderHelm(t *testing.T, chart *pb.Chart, addOnConfig string, goldenFil
   }
 }`
 
-	if addOnConfig != "" {
-		mergedConfig, err := mergeRaw([]byte(overrideJSON), []byte(addOnConfig))
-		if err != nil {
-			t.Fatal("Unexpected error", err)
-		}
-		overrideJSON = string(mergedConfig)
-	}
-
 	overrideConfig := &pb.Config{
 		Raw: fmt.Sprintf(overrideJSON, k8s.IdentityIssuerExpiryAnnotation),
 	}
@@ -156,12 +148,20 @@ func testRenderHelm(t *testing.T, chart *pb.Chart, addOnConfig string, goldenFil
 	diffTestdata(t, goldenFileName, buf.String())
 }
 
-func chartControlPlane(t *testing.T, ha bool, addons []l5dcharts.AddOn, ignoreOutboundPorts string, ignoreInboundPorts string) *pb.Chart {
-	values, err := readTestValues(t, ha, ignoreOutboundPorts, ignoreInboundPorts)
+func chartControlPlane(t *testing.T, ha bool, addOnConfig string, ignoreOutboundPorts string, ignoreInboundPorts string) *pb.Chart {
+	rawValues, err := readTestValues(t, ha, ignoreOutboundPorts, ignoreInboundPorts)
 	if err != nil {
 		t.Fatal("Unexpected error", err)
 	}
 
+	if addOnConfig != "" {
+		mergedConfig, err := mergeRaw(rawValues, []byte(addOnConfig))
+		if err != nil {
+			t.Fatal("Unexpected error", err)
+		}
+		rawValues = mergedConfig
+	}
+
 	partialPaths := []string{
 		"templates/_proxy.tpl",
 		"templates/_proxy-init.tpl",
@@ -190,10 +190,21 @@ func chartControlPlane(t *testing.T, ha bool, addons []l5dcharts.AddOn, ignoreOu
 			chartPartials,
 		},
 		Values: &pb.Config{
-			Raw: string(values),
+			Raw: string(rawValues),
 		},
 	}
 
+	var values l5dcharts.Values
+	err = yaml.Unmarshal(rawValues, &values)
+	if err != nil {
+		t.Fatal("Unexpected error", err)
+	}
+
+	addons, err := l5dcharts.ParseAddOnValues(&values)
+	if err != nil {
+		t.Fatal("Unexpected error", err)
+	}
+
 	for _, addon := range addons {
 		chart.Dependencies = append(chart.Dependencies, buildAddOnChart(t, addon, chartPartials))
 	}
diff --git a/cli/cmd/testdata/install_addon_control-plane.golden b/cli/cmd/testdata/install_addon_control-plane.golden
index cc072326d..e454ca184 100644
--- a/cli/cmd/testdata/install_addon_control-plane.golden
+++ b/cli/cmd/testdata/install_addon_control-plane.golden
@@ -1033,6 +1033,19 @@ data:
         action: replace
         target_label: component
 
+    - job_name: 'linkerd-service-mirror'
+      kubernetes_sd_configs:
+      - role: pod
+      relabel_configs:
+      - source_labels:
+        - __meta_kubernetes_pod_label_linkerd_io_control_plane_component
+        - __meta_kubernetes_pod_container_port_name
+        action: keep
+        regex: linkerd-service-mirror;admin-http$
+      - source_labels: [__meta_kubernetes_pod_container_name]
+        action: replace
+        target_label: component
+
     - job_name: 'linkerd-proxy'
       kubernetes_sd_configs:
       - role: pod
diff --git a/cli/cmd/testdata/install_control-plane.golden b/cli/cmd/testdata/install_control-plane.golden
index addd5f19e..4a885918d 100644
--- a/cli/cmd/testdata/install_control-plane.golden
+++ b/cli/cmd/testdata/install_control-plane.golden
@@ -1033,6 +1033,19 @@ data:
         action: replace
         target_label: component
 
+    - job_name: 'linkerd-service-mirror'
+      kubernetes_sd_configs:
+      - role: pod
+      relabel_configs:
+      - source_labels:
+        - __meta_kubernetes_pod_label_linkerd_io_control_plane_component
+        - __meta_kubernetes_pod_container_port_name
+        action: keep
+        regex: linkerd-service-mirror;admin-http$
+      - source_labels: [__meta_kubernetes_pod_container_name]
+        action: replace
+        target_label: component
+
     - job_name: 'linkerd-proxy'
       kubernetes_sd_configs:
       - role: pod
diff --git a/cli/cmd/testdata/install_controlplane_tracing_output.golden b/cli/cmd/testdata/install_controlplane_tracing_output.golden
index 5a9f6cc53..da483a581 100644
--- a/cli/cmd/testdata/install_controlplane_tracing_output.golden
+++ b/cli/cmd/testdata/install_controlplane_tracing_output.golden
@@ -1933,6 +1933,19 @@ data:
         action: replace
         target_label: component
 
+    - job_name: 'linkerd-service-mirror'
+      kubernetes_sd_configs:
+      - role: pod
+      relabel_configs:
+      - source_labels:
+        - __meta_kubernetes_pod_label_linkerd_io_control_plane_component
+        - __meta_kubernetes_pod_container_port_name
+        action: keep
+        regex: linkerd-service-mirror;admin-http$
+      - source_labels: [__meta_kubernetes_pod_container_name]
+        action: replace
+        target_label: component
+
     - job_name: 'linkerd-proxy'
       kubernetes_sd_configs:
       - role: pod
diff --git a/cli/cmd/testdata/install_custom_registry.golden b/cli/cmd/testdata/install_custom_registry.golden
index 4e3a9ef07..a53bd3b26 100644
--- a/cli/cmd/testdata/install_custom_registry.golden
+++ b/cli/cmd/testdata/install_custom_registry.golden
@@ -1873,6 +1873,19 @@ data:
         action: replace
         target_label: component
 
+    - job_name: 'linkerd-service-mirror'
+      kubernetes_sd_configs:
+      - role: pod
+      relabel_configs:
+      - source_labels:
+        - __meta_kubernetes_pod_label_linkerd_io_control_plane_component
+        - __meta_kubernetes_pod_container_port_name
+        action: keep
+        regex: linkerd-service-mirror;admin-http$
+      - source_labels: [__meta_kubernetes_pod_container_name]
+        action: replace
+        target_label: component
+
     - job_name: 'linkerd-proxy'
       kubernetes_sd_configs:
       - role: pod
diff --git a/cli/cmd/testdata/install_default.golden b/cli/cmd/testdata/install_default.golden
index c02a1db05..030bc1e21 100644
--- a/cli/cmd/testdata/install_default.golden
+++ b/cli/cmd/testdata/install_default.golden
@@ -1873,6 +1873,19 @@ data:
         action: replace
         target_label: component
 
+    - job_name: 'linkerd-service-mirror'
+      kubernetes_sd_configs:
+      - role: pod
+      relabel_configs:
+      - source_labels:
+        - __meta_kubernetes_pod_label_linkerd_io_control_plane_component
+        - __meta_kubernetes_pod_container_port_name
+        action: keep
+        regex: linkerd-service-mirror;admin-http$
+      - source_labels: [__meta_kubernetes_pod_container_name]
+        action: replace
+        target_label: component
+
     - job_name: 'linkerd-proxy'
       kubernetes_sd_configs:
       - role: pod
diff --git a/cli/cmd/testdata/install_ha_output.golden b/cli/cmd/testdata/install_ha_output.golden
index 4f5a8926b..ce4b2ab6d 100644
--- a/cli/cmd/testdata/install_ha_output.golden
+++ b/cli/cmd/testdata/install_ha_output.golden
@@ -2001,6 +2001,19 @@ data:
         action: replace
         target_label: component
 
+    - job_name: 'linkerd-service-mirror'
+      kubernetes_sd_configs:
+      - role: pod
+      relabel_configs:
+      - source_labels:
+        - __meta_kubernetes_pod_label_linkerd_io_control_plane_component
+        - __meta_kubernetes_pod_container_port_name
+        action: keep
+        regex: linkerd-service-mirror;admin-http$
+      - source_labels: [__meta_kubernetes_pod_container_name]
+        action: replace
+        target_label: component
+
     - job_name: 'linkerd-proxy'
       kubernetes_sd_configs:
       - role: pod
diff --git a/cli/cmd/testdata/install_ha_with_overrides_output.golden b/cli/cmd/testdata/install_ha_with_overrides_output.golden
index a8b7efdea..81345d6dd 100644
--- a/cli/cmd/testdata/install_ha_with_overrides_output.golden
+++ b/cli/cmd/testdata/install_ha_with_overrides_output.golden
@@ -2001,6 +2001,19 @@ data:
         action: replace
         target_label: component
 
+    - job_name: 'linkerd-service-mirror'
+      kubernetes_sd_configs:
+      - role: pod
+      relabel_configs:
+      - source_labels:
+        - __meta_kubernetes_pod_label_linkerd_io_control_plane_component
+        - __meta_kubernetes_pod_container_port_name
+        action: keep
+        regex: linkerd-service-mirror;admin-http$
+      - source_labels: [__meta_kubernetes_pod_container_name]
+        action: replace
+        target_label: component
+
     - job_name: 'linkerd-proxy'
       kubernetes_sd_configs:
       - role: pod
diff --git a/cli/cmd/testdata/install_heartbeat_disabled_output.golden b/cli/cmd/testdata/install_heartbeat_disabled_output.golden
index ccbd78259..d625f3f5a 100644
--- a/cli/cmd/testdata/install_heartbeat_disabled_output.golden
+++ b/cli/cmd/testdata/install_heartbeat_disabled_output.golden
@@ -1784,6 +1784,19 @@ data:
         action: replace
         target_label: component
 
+    - job_name: 'linkerd-service-mirror'
+      kubernetes_sd_configs:
+      - role: pod
+      relabel_configs:
+      - source_labels:
+        - __meta_kubernetes_pod_label_linkerd_io_control_plane_component
+        - __meta_kubernetes_pod_container_port_name
+        action: keep
+        regex: linkerd-service-mirror;admin-http$
+      - source_labels: [__meta_kubernetes_pod_container_name]
+        action: replace
+        target_label: component
+
     - job_name: 'linkerd-proxy'
       kubernetes_sd_configs:
       - role: pod
diff --git a/cli/cmd/testdata/install_helm_output.golden b/cli/cmd/testdata/install_helm_output.golden
index 31c8c275e..5aa40e4a0 100644
--- a/cli/cmd/testdata/install_helm_output.golden
+++ b/cli/cmd/testdata/install_helm_output.golden
@@ -1939,6 +1939,19 @@ data:
         action: replace
         target_label: component
 
+    - job_name: 'linkerd-service-mirror'
+      kubernetes_sd_configs:
+      - role: pod
+      relabel_configs:
+      - source_labels:
+        - __meta_kubernetes_pod_label_linkerd_io_control_plane_component
+        - __meta_kubernetes_pod_container_port_name
+        action: keep
+        regex: linkerd-service-mirror;admin-http$
+      - source_labels: [__meta_kubernetes_pod_container_name]
+        action: replace
+        target_label: component
+
     - job_name: 'linkerd-proxy'
       kubernetes_sd_configs:
       - role: pod
diff --git a/cli/cmd/testdata/install_helm_output_addons.golden b/cli/cmd/testdata/install_helm_output_addons.golden
index 6db21f19a..cca17f2d3 100644
--- a/cli/cmd/testdata/install_helm_output_addons.golden
+++ b/cli/cmd/testdata/install_helm_output_addons.golden
@@ -1939,6 +1939,19 @@ data:
         action: replace
         target_label: component
 
+    - job_name: 'linkerd-service-mirror'
+      kubernetes_sd_configs:
+      - role: pod
+      relabel_configs:
+      - source_labels:
+        - __meta_kubernetes_pod_label_linkerd_io_control_plane_component
+        - __meta_kubernetes_pod_container_port_name
+        action: keep
+        regex: linkerd-service-mirror;admin-http$
+      - source_labels: [__meta_kubernetes_pod_container_name]
+        action: replace
+        target_label: component
+
     - job_name: 'linkerd-proxy'
       kubernetes_sd_configs:
       - role: pod
diff --git a/cli/cmd/testdata/install_helm_output_ha.golden b/cli/cmd/testdata/install_helm_output_ha.golden
index f1cd9c85e..8222a0261 100644
--- a/cli/cmd/testdata/install_helm_output_ha.golden
+++ b/cli/cmd/testdata/install_helm_output_ha.golden
@@ -2067,6 +2067,19 @@ data:
         action: replace
         target_label: component
 
+    - job_name: 'linkerd-service-mirror'
+      kubernetes_sd_configs:
+      - role: pod
+      relabel_configs:
+      - source_labels:
+        - __meta_kubernetes_pod_label_linkerd_io_control_plane_component
+        - __meta_kubernetes_pod_container_port_name
+        action: keep
+        regex: linkerd-service-mirror;admin-http$
+      - source_labels: [__meta_kubernetes_pod_container_name]
+        action: replace
+        target_label: component
+
     - job_name: 'linkerd-proxy'
       kubernetes_sd_configs:
       - role: pod
diff --git a/cli/cmd/testdata/install_no_init_container.golden b/cli/cmd/testdata/install_no_init_container.golden
index 63619164b..0c44685a1 100644
--- a/cli/cmd/testdata/install_no_init_container.golden
+++ b/cli/cmd/testdata/install_no_init_container.golden
@@ -1738,6 +1738,19 @@ data:
         action: replace
         target_label: component
 
+    - job_name: 'linkerd-service-mirror'
+      kubernetes_sd_configs:
+      - role: pod
+      relabel_configs:
+      - source_labels:
+        - __meta_kubernetes_pod_label_linkerd_io_control_plane_component
+        - __meta_kubernetes_pod_container_port_name
+        action: keep
+        regex: linkerd-service-mirror;admin-http$
+      - source_labels: [__meta_kubernetes_pod_container_name]
+        action: replace
+        target_label: component
+
     - job_name: 'linkerd-proxy'
       kubernetes_sd_configs:
       - role: pod
diff --git a/cli/cmd/testdata/install_output.golden b/cli/cmd/testdata/install_output.golden
index 714e882e9..f3e0cbdac 100644
--- a/cli/cmd/testdata/install_output.golden
+++ b/cli/cmd/testdata/install_output.golden
@@ -1869,6 +1869,19 @@ data:
         action: replace
         target_label: component
 
+    - job_name: 'linkerd-service-mirror'
+      kubernetes_sd_configs:
+      - role: pod
+      relabel_configs:
+      - source_labels:
+        - __meta_kubernetes_pod_label_linkerd_io_control_plane_component
+        - __meta_kubernetes_pod_container_port_name
+        action: keep
+        regex: linkerd-service-mirror;admin-http$
+      - source_labels: [__meta_kubernetes_pod_container_name]
+        action: replace
+        target_label: component
+
     - job_name: 'linkerd-proxy'
       kubernetes_sd_configs:
       - role: pod
diff --git a/cli/cmd/testdata/install_proxy_ignores.golden b/cli/cmd/testdata/install_proxy_ignores.golden
index a1c1898d0..b0f730399 100644
--- a/cli/cmd/testdata/install_proxy_ignores.golden
+++ b/cli/cmd/testdata/install_proxy_ignores.golden
@@ -1873,6 +1873,19 @@ data:
         action: replace
         target_label: component
 
+    - job_name: 'linkerd-service-mirror'
+      kubernetes_sd_configs:
+      - role: pod
+      relabel_configs:
+      - source_labels:
+        - __meta_kubernetes_pod_label_linkerd_io_control_plane_component
+        - __meta_kubernetes_pod_container_port_name
+        action: keep
+        regex: linkerd-service-mirror;admin-http$
+      - source_labels: [__meta_kubernetes_pod_container_name]
+        action: replace
+        target_label: component
+
     - job_name: 'linkerd-proxy'
       kubernetes_sd_configs:
       - role: pod
diff --git a/cli/cmd/testdata/install_restricted_dashboard.golden b/cli/cmd/testdata/install_restricted_dashboard.golden
index 0ff8a4030..8d4afdc65 100644
--- a/cli/cmd/testdata/install_restricted_dashboard.golden
+++ b/cli/cmd/testdata/install_restricted_dashboard.golden
@@ -1808,6 +1808,19 @@ data:
         action: replace
         target_label: component
 
+    - job_name: 'linkerd-service-mirror'
+      kubernetes_sd_configs:
+      - role: pod
+      relabel_configs:
+      - source_labels:
+        - __meta_kubernetes_pod_label_linkerd_io_control_plane_component
+        - __meta_kubernetes_pod_container_port_name
+        action: keep
+        regex: linkerd-service-mirror;admin-http$
+      - source_labels: [__meta_kubernetes_pod_container_name]
+        action: replace
+        target_label: component
+
     - job_name: 'linkerd-proxy'
       kubernetes_sd_configs:
       - role: pod
diff --git a/cli/cmd/testdata/install_tracing.golden b/cli/cmd/testdata/install_tracing.golden
index d2cf23fb6..36bf5563e 100644
--- a/cli/cmd/testdata/install_tracing.golden
+++ b/cli/cmd/testdata/install_tracing.golden
@@ -1873,6 +1873,19 @@ data:
         action: replace
         target_label: component
 
+    - job_name: 'linkerd-service-mirror'
+      kubernetes_sd_configs:
+      - role: pod
+      relabel_configs:
+      - source_labels:
+        - __meta_kubernetes_pod_label_linkerd_io_control_plane_component
+        - __meta_kubernetes_pod_container_port_name
+        action: keep
+        regex: linkerd-service-mirror;admin-http$
+      - source_labels: [__meta_kubernetes_pod_container_name]
+        action: replace
+        target_label: component
+
     - job_name: 'linkerd-proxy'
       kubernetes_sd_configs:
       - role: pod
diff --git a/cli/cmd/testdata/upgrade_add-on_controlplane.golden b/cli/cmd/testdata/upgrade_add-on_controlplane.golden
index 1e8226931..609979c4e 100644
--- a/cli/cmd/testdata/upgrade_add-on_controlplane.golden
+++ b/cli/cmd/testdata/upgrade_add-on_controlplane.golden
@@ -1041,6 +1041,19 @@ data:
         action: replace
         target_label: component
 
+    - job_name: 'linkerd-service-mirror'
+      kubernetes_sd_configs:
+      - role: pod
+      relabel_configs:
+      - source_labels:
+        - __meta_kubernetes_pod_label_linkerd_io_control_plane_component
+        - __meta_kubernetes_pod_container_port_name
+        action: keep
+        regex: linkerd-service-mirror;admin-http$
+      - source_labels: [__meta_kubernetes_pod_container_name]
+        action: replace
+        target_label: component
+
     - job_name: 'linkerd-proxy'
       kubernetes_sd_configs:
       - role: pod
diff --git a/cli/cmd/testdata/upgrade_add-on_overwrite.golden b/cli/cmd/testdata/upgrade_add-on_overwrite.golden
index bfa3066d9..3eaa1b550 100644
--- a/cli/cmd/testdata/upgrade_add-on_overwrite.golden
+++ b/cli/cmd/testdata/upgrade_add-on_overwrite.golden
@@ -1881,6 +1881,19 @@ data:
         action: replace
         target_label: component
 
+    - job_name: 'linkerd-service-mirror'
+      kubernetes_sd_configs:
+      - role: pod
+      relabel_configs:
+      - source_labels:
+        - __meta_kubernetes_pod_label_linkerd_io_control_plane_component
+        - __meta_kubernetes_pod_container_port_name
+        action: keep
+        regex: linkerd-service-mirror;admin-http$
+      - source_labels: [__meta_kubernetes_pod_container_name]
+        action: replace
+        target_label: component
+
     - job_name: 'linkerd-proxy'
       kubernetes_sd_configs:
       - role: pod
diff --git a/cli/cmd/testdata/upgrade_add_add-on.golden b/cli/cmd/testdata/upgrade_add_add-on.golden
index 8864449dc..1c12fbacf 100644
--- a/cli/cmd/testdata/upgrade_add_add-on.golden
+++ b/cli/cmd/testdata/upgrade_add_add-on.golden
@@ -1881,6 +1881,19 @@ data:
         action: replace
         target_label: component
 
+    - job_name: 'linkerd-service-mirror'
+      kubernetes_sd_configs:
+      - role: pod
+      relabel_configs:
+      - source_labels:
+        - __meta_kubernetes_pod_label_linkerd_io_control_plane_component
+        - __meta_kubernetes_pod_container_port_name
+        action: keep
+        regex: linkerd-service-mirror;admin-http$
+      - source_labels: [__meta_kubernetes_pod_container_name]
+        action: replace
+        target_label: component
+
     - job_name: 'linkerd-proxy'
       kubernetes_sd_configs:
       - role: pod
diff --git a/cli/cmd/testdata/upgrade_default.golden b/cli/cmd/testdata/upgrade_default.golden
index ea29509c5..fc2d4361b 100644
--- a/cli/cmd/testdata/upgrade_default.golden
+++ b/cli/cmd/testdata/upgrade_default.golden
@@ -1881,6 +1881,19 @@ data:
         action: replace
         target_label: component
 
+    - job_name: 'linkerd-service-mirror'
+      kubernetes_sd_configs:
+      - role: pod
+      relabel_configs:
+      - source_labels:
+        - __meta_kubernetes_pod_label_linkerd_io_control_plane_component
+        - __meta_kubernetes_pod_container_port_name
+        action: keep
+        regex: linkerd-service-mirror;admin-http$
+      - source_labels: [__meta_kubernetes_pod_container_name]
+        action: replace
+        target_label: component
+
     - job_name: 'linkerd-proxy'
       kubernetes_sd_configs:
       - role: pod
diff --git a/cli/cmd/testdata/upgrade_external_issuer.golden b/cli/cmd/testdata/upgrade_external_issuer.golden
index 24869ad0d..424312deb 100644
--- a/cli/cmd/testdata/upgrade_external_issuer.golden
+++ b/cli/cmd/testdata/upgrade_external_issuer.golden
@@ -1867,6 +1867,19 @@ data:
         action: replace
         target_label: component
 
+    - job_name: 'linkerd-service-mirror'
+      kubernetes_sd_configs:
+      - role: pod
+      relabel_configs:
+      - source_labels:
+        - __meta_kubernetes_pod_label_linkerd_io_control_plane_component
+        - __meta_kubernetes_pod_container_port_name
+        action: keep
+        regex: linkerd-service-mirror;admin-http$
+      - source_labels: [__meta_kubernetes_pod_container_name]
+        action: replace
+        target_label: component
+
     - job_name: 'linkerd-proxy'
       kubernetes_sd_configs:
       - role: pod
diff --git a/cli/cmd/testdata/upgrade_ha.golden b/cli/cmd/testdata/upgrade_ha.golden
index 657de0a1f..8ed0064c1 100644
--- a/cli/cmd/testdata/upgrade_ha.golden
+++ b/cli/cmd/testdata/upgrade_ha.golden
@@ -2009,6 +2009,19 @@ data:
         action: replace
         target_label: component
 
+    - job_name: 'linkerd-service-mirror'
+      kubernetes_sd_configs:
+      - role: pod
+      relabel_configs:
+      - source_labels:
+        - __meta_kubernetes_pod_label_linkerd_io_control_plane_component
+        - __meta_kubernetes_pod_container_port_name
+        action: keep
+        regex: linkerd-service-mirror;admin-http$
+      - source_labels: [__meta_kubernetes_pod_container_name]
+        action: replace
+        target_label: component
+
     - job_name: 'linkerd-proxy'
       kubernetes_sd_configs:
       - role: pod
diff --git a/cli/cmd/testdata/upgrade_overwrite_issuer.golden b/cli/cmd/testdata/upgrade_overwrite_issuer.golden
index 64f150ab5..2f840137b 100644
--- a/cli/cmd/testdata/upgrade_overwrite_issuer.golden
+++ b/cli/cmd/testdata/upgrade_overwrite_issuer.golden
@@ -1873,6 +1873,19 @@ data:
         action: replace
         target_label: component
 
+    - job_name: 'linkerd-service-mirror'
+      kubernetes_sd_configs:
+      - role: pod
+      relabel_configs:
+      - source_labels:
+        - __meta_kubernetes_pod_label_linkerd_io_control_plane_component
+        - __meta_kubernetes_pod_container_port_name
+        action: keep
+        regex: linkerd-service-mirror;admin-http$
+      - source_labels: [__meta_kubernetes_pod_container_name]
+        action: replace
+        target_label: component
+
     - job_name: 'linkerd-proxy'
       kubernetes_sd_configs:
       - role: pod
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 81ef8639f..ea04f4aa3 100644
--- a/cli/cmd/testdata/upgrade_overwrite_trust_anchors-external-issuer.golden
+++ b/cli/cmd/testdata/upgrade_overwrite_trust_anchors-external-issuer.golden
@@ -1859,6 +1859,19 @@ data:
         action: replace
         target_label: component
 
+    - job_name: 'linkerd-service-mirror'
+      kubernetes_sd_configs:
+      - role: pod
+      relabel_configs:
+      - source_labels:
+        - __meta_kubernetes_pod_label_linkerd_io_control_plane_component
+        - __meta_kubernetes_pod_container_port_name
+        action: keep
+        regex: linkerd-service-mirror;admin-http$
+      - source_labels: [__meta_kubernetes_pod_container_name]
+        action: replace
+        target_label: component
+
     - job_name: 'linkerd-proxy'
       kubernetes_sd_configs:
       - role: pod
diff --git a/cli/cmd/testdata/upgrade_overwrite_trust_anchors.golden b/cli/cmd/testdata/upgrade_overwrite_trust_anchors.golden
index 64f150ab5..2f840137b 100644
--- a/cli/cmd/testdata/upgrade_overwrite_trust_anchors.golden
+++ b/cli/cmd/testdata/upgrade_overwrite_trust_anchors.golden
@@ -1873,6 +1873,19 @@ data:
         action: replace
         target_label: component
 
+    - job_name: 'linkerd-service-mirror'
+      kubernetes_sd_configs:
+      - role: pod
+      relabel_configs:
+      - source_labels:
+        - __meta_kubernetes_pod_label_linkerd_io_control_plane_component
+        - __meta_kubernetes_pod_container_port_name
+        action: keep
+        regex: linkerd-service-mirror;admin-http$
+      - source_labels: [__meta_kubernetes_pod_container_name]
+        action: replace
+        target_label: component
+
     - job_name: 'linkerd-proxy'
       kubernetes_sd_configs:
       - role: pod
diff --git a/controller/api/public/http_server_test.go b/controller/api/public/http_server_test.go
index 52bb08829..b6cb12b4c 100644
--- a/controller/api/public/http_server_test.go
+++ b/controller/api/public/http_server_test.go
@@ -31,6 +31,11 @@ func (m *mockGrpcServer) StatSummary(ctx context.Context, req *pb.StatSummaryReq
 	return m.ResponseToReturn.(*pb.StatSummaryResponse), m.ErrorToReturn
 }
 
+func (m *mockGrpcServer) Gateways(ctx context.Context, req *pb.GatewaysRequest) (*pb.GatewaysResponse, error) {
+	m.LastRequestReceived = req
+	return m.ResponseToReturn.(*pb.GatewaysResponse), m.ErrorToReturn
+}
+
 func (m *mockGrpcServer) TopRoutes(ctx context.Context, req *pb.TopRoutesRequest) (*pb.TopRoutesResponse, error) {
 	m.LastRequestReceived = req
 	return m.ResponseToReturn.(*pb.TopRoutesResponse), m.ErrorToReturn
diff --git a/controller/api/public/test_helper.go b/controller/api/public/test_helper.go
index aed815f96..edffd2624 100644
--- a/controller/api/public/test_helper.go
+++ b/controller/api/public/test_helper.go
@@ -29,6 +29,7 @@ type MockAPIClient struct {
 	ListPodsResponseToReturn       *pb.ListPodsResponse
 	ListServicesResponseToReturn   *pb.ListServicesResponse
 	StatSummaryResponseToReturn    *pb.StatSummaryResponse
+	GatewaysResponseToReturn       *pb.GatewaysResponse
 	TopRoutesResponseToReturn      *pb.TopRoutesResponse
 	EdgesResponseToReturn          *pb.EdgesResponse
 	SelfCheckResponseToReturn      *healthcheckPb.SelfCheckResponse
@@ -43,6 +44,11 @@ func (c *MockAPIClient) StatSummary(ctx context.Context, in *pb.StatSummaryReque
 	return c.StatSummaryResponseToReturn, c.ErrorToReturn
 }
 
+// Gateways provides a mock of a Public API method.
+func (c *MockAPIClient) Gateways(ctx context.Context, in *pb.GatewaysRequest, opts ...grpc.CallOption) (*pb.GatewaysResponse, error) {
+	return c.GatewaysResponseToReturn, c.ErrorToReturn
+}
+
 // TopRoutes provides a mock of a Public API method.
 func (c *MockAPIClient) TopRoutes(ctx context.Context, in *pb.TopRoutesRequest, opts ...grpc.CallOption) (*pb.TopRoutesResponse, error) {
 	return c.TopRoutesResponseToReturn, c.ErrorToReturn
diff --git a/controller/cmd/service-mirror/cluster_watcher_test.go b/controller/cmd/service-mirror/cluster_watcher_test.go
index e2189ed27..1637258e2 100644
--- a/controller/cmd/service-mirror/cluster_watcher_test.go
+++ b/controller/cmd/service-mirror/cluster_watcher_test.go
@@ -27,6 +27,10 @@ type testCase struct {
 	expectedEventsInQueue  []interface{}
 }
 
+type NoOpProbeEventSink struct{}
+
+func (s *NoOpProbeEventSink) send(event interface{}) {}
+
 func runTestCase(tc *testCase, t *testing.T) {
 	t.Run(tc.testDescription, func(t *testing.T) {
 		remoteAPI, err := k8s.NewFakeAPI(tc.remoteResources...)
@@ -53,6 +57,7 @@ func runTestCase(tc *testCase, t *testing.T) {
 			log:             logging.WithFields(logging.Fields{"cluster": clusterName}),
 			eventsQueue:     q,
 			requeueLimit:    0,
+			probeEventsSink: &NoOpProbeEventSink{},
 		}
 
 		for _, ev := range tc.events {
@@ -270,6 +275,10 @@ func TestRemoteServiceDeleted(t *testing.T) {
 				&RemoteServiceDeleted{
 					Name:      "test-service-remote-to-delete",
 					Namespace: "test-namespace-to-delete",
+					GatewayData: gatewayMetadata{
+						Name:      "gateway",
+						Namespace: "gateway-ns",
+					},
 				},
 			},
 
@@ -326,7 +335,7 @@ func TestRemoteServiceUpdated(t *testing.T) {
 							Protocol: "TCP",
 						},
 					}),
-					gatewayData: &gatewayMetadata{
+					gatewayData: gatewayMetadata{
 						Name:      "gateway-new",
 						Namespace: "gateway-ns",
 					},
@@ -437,7 +446,7 @@ func TestRemoteServiceUpdated(t *testing.T) {
 							Protocol: "TCP",
 						},
 					}),
-					gatewayData: &gatewayMetadata{
+					gatewayData: gatewayMetadata{
 						Name:      "gateway",
 						Namespace: "gateway-ns",
 					},
@@ -525,13 +534,16 @@ func TestRemoteGatewayUpdated(t *testing.T) {
 			testDescription: "endpoints ports are updated on gateway change",
 			events: []interface{}{
 				&RemoteGatewayUpdated{
-					newPort:              999,
-					newEndpointAddresses: []corev1.EndpointAddress{{IP: "0.0.0.0"}},
-					gatewayData: &gatewayMetadata{
-						Name:      "gateway",
-						Namespace: "gateway-ns",
+
+					gatewaySpec: GatewaySpec{
+						gatewayName:      "gateway",
+						gatewayNamespace: "gateway-ns",
+						clusterName:      "remote",
+						addresses:        []corev1.EndpointAddress{{IP: "0.0.0.0"}},
+						incomingPort:     999,
+						resourceVersion:  "currentGatewayResVersion",
+						ProbeConfig:      nil,
 					},
-					newResourceVersion: "currentGatewayResVersion",
 					affectedServices: []*corev1.Service{
 						mirroredService("test-service-1-remote", "test-namespace", "gateway", "gateway-ns", "", "pastGatewayResVersion",
 							[]corev1.ServicePort{
@@ -624,13 +636,14 @@ func TestRemoteGatewayUpdated(t *testing.T) {
 			testDescription: "endpoints addresses are updated on gateway change",
 			events: []interface{}{
 				&RemoteGatewayUpdated{
-					newPort:              888,
-					newEndpointAddresses: []corev1.EndpointAddress{{IP: "0.0.0.1"}},
-					gatewayData: &gatewayMetadata{
-						Name:      "gateway",
-						Namespace: "gateway-ns",
+					gatewaySpec: GatewaySpec{
+						gatewayName:      "gateway",
+						gatewayNamespace: "gateway-ns",
+						clusterName:      "remote",
+						addresses:        []corev1.EndpointAddress{{IP: "0.0.0.1"}},
+						incomingPort:     888,
+						resourceVersion:  "currentGatewayResVersion",
 					},
-					newResourceVersion: "currentGatewayResVersion",
 					affectedServices: []*corev1.Service{
 						mirroredService("test-service-1-remote", "test-namespace", "gateway", "gateway-ns", "", "pastGatewayResVersion",
 							[]corev1.ServicePort{
@@ -721,14 +734,16 @@ func TestRemoteGatewayUpdated(t *testing.T) {
 			testDescription: "identity is updated on gateway change",
 			events: []interface{}{
 				&RemoteGatewayUpdated{
-					identity:             "new-identity",
-					newPort:              888,
-					newEndpointAddresses: []corev1.EndpointAddress{{IP: "0.0.0.0"}},
-					gatewayData: &gatewayMetadata{
-						Name:      "gateway",
-						Namespace: "gateway-ns",
+					gatewaySpec: GatewaySpec{
+						gatewayName:      "gateway",
+						gatewayNamespace: "gateway-ns",
+						clusterName:      "",
+						addresses:        []corev1.EndpointAddress{{IP: "0.0.0.0"}},
+						incomingPort:     888,
+						resourceVersion:  "currentGatewayResVersion",
+						identity:         "new-identity",
+						ProbeConfig:      nil,
 					},
-					newResourceVersion: "currentGatewayResVersion",
 					affectedServices: []*corev1.Service{
 						mirroredService("test-service-1-remote", "test-namespace", "gateway", "gateway-ns", "", "pastGatewayResVersion",
 							[]corev1.ServicePort{
@@ -825,7 +840,7 @@ func TestRemoteGatewayDeleted(t *testing.T) {
 			testDescription: "removes endpoint subsets when gateway is deleted",
 			events: []interface{}{
 				&RemoteGatewayDeleted{
-					gatewayData: &gatewayMetadata{
+					gatewayData: gatewayMetadata{
 						Name:      "gateway",
 						Namespace: "gateway-ns",
 					},
@@ -995,7 +1010,7 @@ func onAddOrUpdateTestCases(t *testing.T, isAdd bool) []testCase {
 				localService:   mirroredService("test-service-remote", "test-namespace", "gateway", "gateway-ns", "pastResourceVersion", "gatewayResVersion", nil),
 				localEndpoints: endpoints("test-service-remote", "test-namespace", "gateway", "gateway-ns", "0.0.0.0", "", nil),
 				remoteUpdate:   remoteService("test-service", "test-namespace", "gateway", "gateway-ns", "currentResVersion", nil),
-				gatewayData: &gatewayMetadata{
+				gatewayData: gatewayMetadata{
 					Name:      "gateway",
 					Namespace: "gateway-ns",
 				},
@@ -1075,6 +1090,10 @@ func TestOnDelete(t *testing.T) {
 				&RemoteServiceDeleted{
 					Name:      "test-service",
 					Namespace: "test-namespace",
+					GatewayData: gatewayMetadata{
+						Name:      "gateway",
+						Namespace: "gateway-ns",
+					},
 				},
 			},
 		},
@@ -1087,7 +1106,7 @@ func TestOnDelete(t *testing.T) {
 			},
 			expectedEventsInQueue: []interface{}{
 				&RemoteGatewayDeleted{
-					gatewayData: &gatewayMetadata{
+					gatewayData: gatewayMetadata{
 						Name:      "gateway",
 						Namespace: "test-namespace",
 					},