Repository: linkerd/linkerd2
Pull request #4153 (2020-03-09 01:04:53)

update helm chart to use proxyInit version v1.3.1
I noticed the helm chart install uses an old version of the proxyInit container while the linkerd install command uses the latest version

lundbird: Error from integration test:
```Checking the linkerd binary...[ok]
Checking if there is a Kubernetes cluster available...[ok]
Test script: [install_test.go] Params: [--linkerd-namespace=l5d-integration]
--- FAIL: TestLogs (37.53s)
    --- FAIL: TestLogs/proxy-injector/proxy-injector (2.50s)
        install_test.go:720: Found unexpected controller error in proxy-injector/proxy-injector log: linkerd-proxy-injector-587bd8466f-zrtl8 proxy-injector time="2020-03-09T01:23:35Z" level=warning msg="couldn't retrieve parent object l5d-integration-smoke-test-deployment-smoke-test-gateway; error: deployment.apps \"smoke-test-gateway\" not found"
FAIL
FAIL	command-line-arguments	138.281s
##[error]Process completed with exit code 1.```
It doesn't seem to be related to helm as the integration test hasnt progressed to that stage yet. How can we rerun?
lundbird: It doesnt seem to be related to the change as the integration test hasnt progressed to the helm stage yet. How can we rerun?

Patch:
diff --git a/.github/workflows/kind_integration.yml b/.github/workflows/kind_integration.yml
index 8810bbf39..a8fea9b52 100644
--- a/.github/workflows/kind_integration.yml
+++ b/.github/workflows/kind_integration.yml
@@ -38,8 +38,7 @@ jobs:
       run: |
         export PATH="`pwd`/bin:$PATH"
         bin/docker-build
-    - name: Create artifact with CLI and image archives (Forked repositories)
-      if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork
+    - name: Create artifact with CLI and image archives
       env:
         ARCHIVES: /home/runner/archives
       run: |
@@ -57,8 +56,7 @@ jobs:
     # absolute path is used here.
     #
     # https://github.com/actions/upload-artifact/issues/8
-    - name: Upload artifact (Forked repositories)
-      if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork
+    - name: Upload artifact
       # actions/upload-artifact@v1
       uses: actions/upload-artifact@3446296
       with:
diff --git a/.proxy-version b/.proxy-version
index f9993ec65..4b2170c01 100644
--- a/.proxy-version
+++ b/.proxy-version
@@ -1,1 +1,1 @@
-v2.88.0
+v2.89.0
diff --git a/bin/create-release-tag b/bin/create-release-tag
index 50f5ba9ae..ab95ff9c4 100755
--- a/bin/create-release-tag
+++ b/bin/create-release-tag
@@ -3,41 +3,98 @@
 set -o errexit
 set -o nounset
 
-if [[ $# -ne 1 ]]; then
-    echo "usage: ${0##*/} (edge|stable)-xx.xx.xx" >&2
+# Assert at least one and at most two arguments were passed.
+if [[ $# -lt 1 || $# -gt 2 ]]; then
+    echo "Error: ${0##*/} accepts 1 or 2 arguments" >&2
+    echo "Usage:" >&2
+    echo "    ${0##*/} edge" >&2
+    echo "    ${0##*/} stable x.x.x" >&2
     exit 1
 fi
 
-tag="$1"
-
-# Verify the tag format
-tag_format="^(edge|stable)-([0-9]+)\.([0-9]+)\.([0-9]+)$"
-if [[ $tag =~ $tag_format ]]; then
-    # todo: Use these values to verify the tag version increment.
-    # release_channel="${BASH_REMATCH[1]}"
-    # release_major="${BASH_REMATCH[2]}"
-    # release_minor="${BASH_REMATCH[3]}"
-    # release_patch="${BASH_REMATCH[4]}"
-    :
-else
-    echo "tag format incorrect; expected: $tag_format"
-    echo "example: edge-20.12.2, stable-2.10.1"
+release_channel="$1"
+
+# Verify the release channel.
+release_channel_regex="(edge|stable)"
+if [[ ! $release_channel =~ $release_channel_regex ]]; then
+    echo "Error: valid release channels: edge, stable"
+    echo "Usage:"
+    echo "    bin/create-release-tag edge"
+    echo "    bin/create-release-tag stable 2.4.8"
     exit 1
 fi
 
-# todo: Verify the tag version increment.
+if [ "$release_channel" == "stable" ]; then
+    if [ $# -ne 2 ]; then
+        echo "Error: accepts 2 arguments"
+        echo "Usage:"
+        echo "    bin/create-release-tag stable 2.4.8"
+        exit 1
+    fi
+
+    stable_tag="$2"
+
+    # Verify the stable tag format.
+    stable_tag_regex="^([0-9]+)\.([0-9]+)\.([0-9]+)$"
+    if [[ ! $stable_tag =~ $stable_tag_regex ]]; then
+        echo "Error: version reference incorrect"
+        echo "Usage:"
+        echo "    bin/create-release-tag stable 2.4.8"
+        exit 1
+    fi
+
+    # Set the script global `release_tag` variable.
+    release_tag="stable-$stable_tag"
+fi
+
+if [ "$release_channel" == "edge" ]; then
+if [ $# -ne 1 ]; then
+        echo "Error: accepts 1 argument"
+        echo "Usage:"
+        echo "    bin/create-release-tag edge"
+        exit 1
+    fi
+
+    # Some awks on Linux do no support quantifiers in regex, so this regex is
+    # explicit.
+    edge_tag_regex="(edge)-([0-9][0-9])\.([0-9]|[0-9][0-9])\.([0-9]+)"
+    url="https://run.linkerd.io/install-edge"
+
+    # Get the current edge version.
+    current_edge=$(curl -sL $url | awk -v tag_format="$edge_tag_regex" '$0 ~ tag_format')
+
+    # Get the third and fourth groups of the regex; they are the month and
+    # month minor values for the current edge version.
+    current_mm=$(echo "$current_edge" | sed -n -E "s/.*$edge_tag_regex}$/\3/p")
+    current_xx=$(echo "$current_edge" | sed -n -E "s/.*$edge_tag_regex}$/\4/p")
+
+    yy=$(date +"%y")
+    
+    # Strip leading zero
+    new_mm=$(date +"%-m")
+
+    # If this is a new month, `new_xx` should be 1; otherwise increment it.
+    if [ ! "$new_mm" == "$current_mm" ]; then
+        new_xx="1"
+    else
+        new_xx=$((current_xx+1))
+    fi
+
+    # Set the script global `release_tag` variable.
+    release_tag="edge-$yy.$new_mm.$new_xx"
+fi
 
 bindir=$( cd "${BASH_SOURCE[0]%/*}" && pwd )
 # shellcheck source=bin/_release.sh
 tmp=$(. "$bindir"/_release.sh; extract_release_notes)
 
-# Create an unsigned tag with the commit message.
-git tag -s -F "$tmp" "$tag"
+# Create a signed tag with the commit message.
+git tag -s -F "$tmp" "$release_tag"
 
 # Success message
-echo "tag created and signed."
+echo "$release_tag tag created and signed."
 echo ""
-echo "tag: $tag"
+echo "tag: $release_tag"
 echo ""
 echo "To push tag, run:"
-echo "    git push origin $tag"
+echo "    git push origin $release_tag"
diff --git a/bin/install-pr b/bin/install-pr
new file mode 100755
index 000000000..40445c27c
--- /dev/null
+++ b/bin/install-pr
@@ -0,0 +1,71 @@
+#!/bin/bash
+
+### Install PR ###
+#
+# This script takes a Github pull request number as an argument, downloads the
+# docker images from the pull request's artifacts, pushes them, and installs
+# them on your Kubernetes cluster.  Requires a Github personal access token
+# in the $GITHUB_TOKEN environment variable.
+
+set -eo pipefail
+
+pr=$1
+
+if [ -z "$pr" ]
+then
+  echo "usage: ${0##*/} (PR number)" >&2
+  exit 1
+fi
+
+if [ -z "$GITHUB_TOKEN" ]
+then
+  # shellcheck disable=SC2016
+  echo 'Generate a personal access token at https://github.com/settings/tokens and set it in the $GITHUB_TOKEN env var'
+  exit 1
+fi
+
+auth="Authorization: token $GITHUB_TOKEN"
+
+branch=$(curl -s -H "$auth" "https://api.github.com/repos/linkerd/linkerd2/pulls/$pr" | jq -r '.head.ref')
+
+artifacts=$(curl -s -H "$auth" "https://api.github.com/repos/linkerd/linkerd2/actions/workflows/kind_integration.yml/runs?branch=$branch" | jq -r '.workflow_runs[0].artifacts_url')
+
+archive_url=$(curl -s -H "$auth" "$artifacts" | jq -r '.artifacts[0].archive_download_url')
+
+dir=$(mktemp -d -t "linkerd-pr-$pr.XXXXXXXXXX")
+
+cd "$dir" || exit
+
+echo "### Downloading images ###"
+
+curl -L -o archive.zip -H "$auth" "$archive_url"
+
+unzip -o archive.zip
+
+echo "### Pushing images ###"
+
+for archive in cli-bin cni-plugin controller debug grafana proxy web
+do 
+  image=$(docker load -i "$archive.tar" | sed "s/Loaded image: //")
+  docker push "$image"
+  tag=$(echo "$image" | cut -f 2 -d ":")
+done
+
+cd -
+
+rm -rf "$dir"
+
+linkerd=$(bin/docker-pull-binaries "$tag" | head -n 1)
+
+echo "### Pre checks ###"
+
+$linkerd check --pre
+
+echo "### Installing $tag ###"
+
+$linkerd install | kubectl apply -f -
+
+$linkerd check
+
+echo "### Linkerd installed. CLI available: "
+echo "$linkerd"
diff --git a/bin/shellcheck b/bin/shellcheck
index 622b4bc4f..b05d64e80 100755
--- a/bin/shellcheck
+++ b/bin/shellcheck
@@ -22,7 +22,7 @@ if [ ! -f "$scbin" ]; then
   fi
 
   mkdir -p "$targetbin"
-  wget -qO- "https://storage.googleapis.com/shellcheck/shellcheck-${scversion?}.$file" | tar -OxJv "shellcheck-${scversion}/shellcheck" > "$scbin"
+  curl -sLf "https://storage.googleapis.com/shellcheck/shellcheck-${scversion?}.$file" | tar -OxJv "shellcheck-${scversion}/shellcheck" > "$scbin"
   chmod +x "$scbin"
 fi
 
diff --git a/charts/linkerd2/README.md b/charts/linkerd2/README.md
index d5e1a3400..24d158d3b 100644
--- a/charts/linkerd2/README.md
+++ b/charts/linkerd2/README.md
@@ -85,6 +85,7 @@ The following table lists the configurable parameters of the Linkerd2 chart and
 | Parameter                             | Description                                                                                                                                                                           | Default                              |
 |:--------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------|
 | `controllerImage`                     | Docker image for the controller, tap and identity components                                                                                                                          | `gcr.io/linkerd-io/controller`       |
+| `controllerImageVersion`              | Tag for the controller container docker image                                                                                                                                         | latest version                       |
 | `controllerLogLevel`                  | Log level for the control plane components                                                                                                                                            | `info`                               |
 | `controllerReplicas`                  | Number of replicas for each control plane pod                                                                                                                                         | `1`                                  |
 | `controllerUID`                       | User ID for the control plane components                                                                                                                                              | `2103`                               |
@@ -154,6 +155,7 @@ The following table lists the configurable parameters of the Linkerd2 chart and
 | `tap.keyPEM`                          | Certificate key for Tap component. If not provided then Helm will generate one.                                                                                                                                             ||
 | `webhookFailurePolicy`                | Failure policy for the proxy injector                                                                                                                                                 | `Ignore`                             |
 | `webImage`                            | Docker image for the web container                                                                                                                                                    | `gcr.io/linkerd-io/web`              |
+| `enforcedHostRegexp`                  | Host header validation regex for the dashboard. See the [Linkerd documentation](https://linkerd.io/2/tasks/exposing-dashboard) for more information                                   | `""`              |
 
 ## Add-Ons Configuration
 
diff --git a/charts/linkerd2/values.yaml b/charts/linkerd2/values.yaml
index a8a79454b..256efe04f 100644
--- a/charts/linkerd2/values.yaml
+++ b/charts/linkerd2/values.yaml
@@ -56,7 +56,7 @@ global:
     image:
       name: gcr.io/linkerd-io/proxy-init
       pullPolicy: *image_pull_policy
-      version: v1.1.0
+      version: v1.3.1
     resources:
       cpu:
         limit: 100m
@@ -193,4 +193,3 @@ tracing:
     name: linkerd-jaeger
     image: jaegertracing/all-in-one:1.8
     resources:
-
diff --git a/cli/cmd/alpha_stat.go b/cli/cmd/alpha_stat.go
index e046ed103..41f20e679 100644
--- a/cli/cmd/alpha_stat.go
+++ b/cli/cmd/alpha_stat.go
@@ -14,6 +14,7 @@ import (
 	log "github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
 
+	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/resource"
 )
 
@@ -30,8 +31,9 @@ var allowedKinds = map[string]struct{}{
 }
 
 type alphaStatOptions struct {
-	namespace  string
-	toResource string
+	namespace     string
+	toResource    string
+	allNamespaces bool
 }
 
 func newCmdAlphaStat() *cobra.Command {
@@ -84,10 +86,19 @@ Examples:
 				return err
 			}
 
-			target, err := util.BuildResource(options.namespace, args[0])
+			namespace := options.namespace
+			if options.allNamespaces {
+				namespace = corev1.NamespaceAll
+			}
+			target, err := util.BuildResource(namespace, args[0])
 			if err != nil {
 				return err
 			}
+			if target.GetName() != "" && options.allNamespaces {
+				// Getting a named resource from all-namespaces is not supported.
+				return errors.New("cannot use --all-namespaces flag with a named target resource")
+			}
+
 			if _, ok := allowedKinds[target.GetType()]; !ok {
 				return fmt.Errorf("%s is not a supported resource type", target.GetType())
 			}
@@ -96,7 +107,7 @@ Examples:
 				return err
 			}
 			name := target.GetName()
-			toResource := buildToResource(options.namespace, options.toResource)
+			toResource := buildToResource("", options.toResource)
 			// TODO: Lift this requirement once the API supports it.
 			if toResource != nil && toResource.GetType() != target.GetType() {
 				return errors.New("the --to resource must have the same kind as the target resource")
@@ -114,7 +125,7 @@ Examples:
 					if err != nil {
 						return err
 					}
-					renderTrafficMetrics(metrics, stdout)
+					renderTrafficMetrics(metrics, options.allNamespaces, stdout)
 				}
 			} else {
 				if toResource != nil {
@@ -124,7 +135,7 @@ Examples:
 				if err != nil {
 					return err
 				}
-				renderTrafficMetricsList(metrics, stdout)
+				renderTrafficMetricsList(metrics, options.allNamespaces, stdout)
 			}
 
 			return nil
@@ -133,6 +144,7 @@ Examples:
 
 	statCmd.PersistentFlags().StringVarP(&options.namespace, "namespace", "n", options.namespace, "Namespace of the specified resource")
 	statCmd.PersistentFlags().StringVar(&options.toResource, "to", options.toResource, "If present, restricts outbound stats to the specified resource name")
+	statCmd.PersistentFlags().BoolVarP(&options.allNamespaces, "all-namespaces", "A", options.allNamespaces, "Ignore the --namespace flag and fetches data from all namespaces")
 
 	return statCmd
 }
@@ -148,14 +160,14 @@ func buildToResource(namespace, to string) *public.Resource {
 	return &toResource
 }
 
-func renderTrafficMetrics(metrics *smimetrics.TrafficMetrics, w io.Writer) {
-	t := buildTable(false)
+func renderTrafficMetrics(metrics *smimetrics.TrafficMetrics, allNamespaces bool, w io.Writer) {
+	t := buildTable(false, allNamespaces)
 	t.Data = []table.Row{metricsToRow(metrics, false)}
 	t.Render(w)
 }
 
-func renderTrafficMetricsList(metrics *smimetrics.TrafficMetricsList, w io.Writer) {
-	t := buildTable(false)
+func renderTrafficMetricsList(metrics *smimetrics.TrafficMetricsList, allNamespaces bool, w io.Writer) {
+	t := buildTable(false, allNamespaces)
 	t.Data = []table.Row{}
 	for _, row := range metrics.Items {
 		row := row // Copy to satisfy golint.
@@ -165,20 +177,24 @@ func renderTrafficMetricsList(metrics *smimetrics.TrafficMetricsList, w io.Write
 }
 
 func renderTrafficMetricsEdgesList(metrics *smimetrics.TrafficMetricsList, w io.Writer, toResource *public.Resource) {
-	outbound := toResource != nil
-	t := buildTable(outbound)
+	t := buildTable(true, false)
 	t.Data = []table.Row{}
 	for _, row := range metrics.Items {
 		row := row // Copy to satisfy golint.
 		if row.Edge.Direction != "to" {
 			continue
 		}
-		if toResource != nil && toResource.GetName() != "" &&
-			(row.Edge.Resource.Name != toResource.GetName() || row.Edge.Resource.Namespace != toResource.GetNamespace()) {
-			log.Debugf("Skipping edge %v", row.Edge.Resource)
-			continue
+		if toResource != nil {
+			if toResource.GetName() != "" && row.Edge.Resource.Name != toResource.GetName() {
+				log.Debugf("Skipping edge %v", row.Edge.Resource)
+				continue
+			}
+			if toResource.GetNamespace() != "" && row.Edge.Resource.Namespace != toResource.GetNamespace() {
+				log.Debugf("Skipping edge %v", row.Edge.Resource)
+				continue
+			}
 		}
-		t.Data = append(t.Data, metricsToRow(&row, outbound))
+		t.Data = append(t.Data, metricsToRow(&row, true))
 	}
 	t.Render(w)
 }
@@ -227,10 +243,11 @@ func metricsToRow(metrics *smimetrics.TrafficMetrics, outbound bool) []string {
 
 	var to string
 	if outbound {
-		to = metrics.Edge.Resource.Name
+		to = metrics.Edge.Resource.String()
 	}
 
 	return []string{
+		metrics.Resource.Namespace,
 		metrics.Resource.Name, // Name
 		metrics.Resource.Name, // From
 		to,                    // To
@@ -242,8 +259,15 @@ func metricsToRow(metrics *smimetrics.TrafficMetrics, outbound bool) []string {
 	}
 }
 
-func buildTable(outbound bool) table.Table {
+func buildTable(outbound, allNamespaces bool) table.Table {
 	columns := []table.Column{
+		table.Column{
+			Header:    "NAMESPACE",
+			Width:     9,
+			Hide:      !allNamespaces,
+			Flexible:  true,
+			LeftAlign: true,
+		},
 		table.Column{
 			Header:    "NAME",
 			Width:     4,
@@ -286,5 +310,7 @@ func buildTable(outbound bool) table.Table {
 			Width:  11,
 		},
 	}
-	return table.NewTable(columns, []table.Row{})
+	t := table.NewTable(columns, []table.Row{})
+	t.Sort = []int{0, 1} // Sort by namespace, then name.
+	return t
 }
diff --git a/controller/api/destination/endpoint_translator.go b/controller/api/destination/endpoint_translator.go
index d8e30d737..c4acdbc7c 100644
--- a/controller/api/destination/endpoint_translator.go
+++ b/controller/api/destination/endpoint_translator.go
@@ -39,9 +39,9 @@ func newEndpointTranslator(
 	return &endpointTranslator{controllerNS, identityTrustDomain, enableH2Upgrade, stream, log}
 }
 
-func (et *endpointTranslator) Add(set watcher.PodSet) {
+func (et *endpointTranslator) Add(set watcher.AddressSet) {
 	addrs := []*pb.WeightedAddr{}
-	for _, address := range set.Pods {
+	for _, address := range set.Addresses {
 		var (
 			wa  *pb.WeightedAddr
 			err error
@@ -49,14 +49,31 @@ func (et *endpointTranslator) Add(set watcher.PodSet) {
 		if address.Pod != nil {
 			wa, err = et.toWeightedAddr(address)
 		} else {
-			// handling address with no associated pod, such as those
-			// belonging to headless services
+			// handling address with no associated pod
 			var addr *net.TcpAddress
 			addr, err = et.toAddr(address)
 			wa = &pb.WeightedAddr{
 				Addr:   addr,
 				Weight: defaultWeight,
 			}
+
+			if address.Identity != "" {
+				wa.TlsIdentity = &pb.TlsIdentity{
+					Strategy: &pb.TlsIdentity_DnsLikeIdentity_{
+						DnsLikeIdentity: &pb.TlsIdentity_DnsLikeIdentity{
+							Name: address.Identity,
+						},
+					},
+				}
+				// in this case we most likely have a proxy on the side, so set protocol hint as well.
+				if et.enableH2Upgrade {
+					wa.ProtocolHint = &pb.ProtocolHint{
+						Protocol: &pb.ProtocolHint_H2_{
+							H2: &pb.ProtocolHint_H2{},
+						},
+					}
+				}
+			}
 		}
 		if err != nil {
 			et.log.Errorf("Failed to translate endpoints to weighted addr: %s", err)
@@ -78,9 +95,9 @@ func (et *endpointTranslator) Add(set watcher.PodSet) {
 	}
 }
 
-func (et *endpointTranslator) Remove(set watcher.PodSet) {
+func (et *endpointTranslator) Remove(set watcher.AddressSet) {
 	addrs := []*net.TcpAddress{}
-	for _, address := range set.Pods {
+	for _, address := range set.Addresses {
 		tcpAddr, err := et.toAddr(address)
 		if err != nil {
 			et.log.Errorf("Failed to translate endpoints to addr: %s", err)
diff --git a/controller/api/destination/watcher/endpoints_watcher.go b/controller/api/destination/watcher/endpoints_watcher.go
index 875c99370..a1ecdfe99 100644
--- a/controller/api/destination/watcher/endpoints_watcher.go
+++ b/controller/api/destination/watcher/endpoints_watcher.go
@@ -7,6 +7,7 @@ import (
 	"sync"
 
 	"github.com/linkerd/linkerd2/controller/k8s"
+	consts "github.com/linkerd/linkerd2/pkg/k8s"
 	"github.com/prometheus/client_golang/prometheus"
 	logging "github.com/sirupsen/logrus"
 	corev1 "k8s.io/api/core/v1"
@@ -24,19 +25,24 @@ const (
 // https://github.com/linkerd/linkerd2/issues/2204
 
 type (
-	// Address represents an individual port on an specific pod.
+	// Address represents an individual port on a specific endpoint.
+	// This endpoint might be the result of a the existence of a pod
+	// that is targeted by this service; alternatively it can be the
+	// case that this endpoint is not associated with a pod and maps
+	// to some other IP (i.e. a remote service gateway)
 	Address struct {
 		IP        string
 		Port      Port
 		Pod       *corev1.Pod
 		OwnerName string
 		OwnerKind string
+		Identity  string
 	}
 
-	// PodSet is a set of pods, indexed by IP.
-	PodSet struct {
-		Pods   map[PodID]Address
-		Labels map[string]string
+	// AddressSet is a set of Address, indexed by ID.
+	AddressSet struct {
+		Addresses map[ID]Address
+		Labels    map[string]string
 	}
 
 	portAndHostname struct {
@@ -89,15 +95,15 @@ type (
 		k8sAPI     *k8s.API
 
 		exists    bool
-		pods      PodSet
+		addresses AddressSet
 		listeners []EndpointUpdateListener
 		metrics   endpointsMetrics
 	}
 
 	// EndpointUpdateListener is the interface that subscribers must implement.
 	EndpointUpdateListener interface {
-		Add(set PodSet)
-		Remove(set PodSet)
+		Add(set AddressSet)
+		Remove(set AddressSet)
 		NoEndpoints(exists bool)
 	}
 )
@@ -417,32 +423,32 @@ func (sp *servicePublisher) metricsLabels(port Port, hostname string) prometheus
 // portPublisher.
 
 func (pp *portPublisher) updateEndpoints(endpoints *corev1.Endpoints) {
-	newPods := pp.endpointsToAddresses(endpoints)
-	if len(newPods.Pods) == 0 {
+	newAddressSet := pp.endpointsToAddresses(endpoints)
+	if len(newAddressSet.Addresses) == 0 {
 		for _, listener := range pp.listeners {
 			listener.NoEndpoints(true)
 		}
 	} else {
-		add, remove := diffPods(pp.pods, newPods)
+		add, remove := diffAddresses(pp.addresses, newAddressSet)
 		for _, listener := range pp.listeners {
-			if len(remove.Pods) > 0 {
+			if len(remove.Addresses) > 0 {
 				listener.Remove(remove)
 			}
-			if len(add.Pods) > 0 {
+			if len(add.Addresses) > 0 {
 				listener.Add(add)
 			}
 		}
 	}
 	pp.exists = true
-	pp.pods = newPods
+	pp.addresses = newAddressSet
 
 	pp.metrics.incUpdates()
-	pp.metrics.setPods(len(pp.pods.Pods))
+	pp.metrics.setPods(len(pp.addresses.Addresses))
 	pp.metrics.setExists(true)
 }
 
-func (pp *portPublisher) endpointsToAddresses(endpoints *corev1.Endpoints) PodSet {
-	pods := make(map[PodID]Address)
+func (pp *portPublisher) endpointsToAddresses(endpoints *corev1.Endpoints) AddressSet {
+	addresses := make(map[ID]Address)
 	for _, subset := range endpoints.Subsets {
 		resolvedPort := pp.resolveTargetPort(subset)
 		for _, endpoint := range subset.Addresses {
@@ -458,9 +464,11 @@ func (pp *portPublisher) endpointsToAddresses(endpoints *corev1.Endpoints) PodSe
 					}, "-"),
 					Namespace: endpoints.ObjectMeta.Namespace,
 				}
-				pods[id] = Address{
-					IP:   endpoint.IP,
-					Port: resolvedPort,
+
+				addresses[id] = Address{
+					IP:       endpoint.IP,
+					Port:     resolvedPort,
+					Identity: endpoints.Annotations[consts.RemoteGatewayIdentity],
 				}
 				continue
 			}
@@ -475,7 +483,7 @@ func (pp *portPublisher) endpointsToAddresses(endpoints *corev1.Endpoints) PodSe
 					continue
 				}
 				ownerKind, ownerName := pp.k8sAPI.GetOwnerKindAndName(pod, false)
-				pods[id] = Address{
+				addresses[id] = Address{
 					IP:        endpoint.IP,
 					Port:      resolvedPort,
 					Pod:       pod,
@@ -485,9 +493,9 @@ func (pp *portPublisher) endpointsToAddresses(endpoints *corev1.Endpoints) PodSe
 			}
 		}
 	}
-	return PodSet{
-		Pods:   pods,
-		Labels: map[string]string{"service": endpoints.Name, "namespace": endpoints.Namespace},
+	return AddressSet{
+		Addresses: addresses,
+		Labels:    map[string]string{"service": endpoints.Name, "namespace": endpoints.Namespace},
 	}
 }
 
@@ -517,7 +525,7 @@ func (pp *portPublisher) updatePort(targetPort namedPort) {
 
 func (pp *portPublisher) noEndpoints(exists bool) {
 	pp.exists = exists
-	pp.pods = PodSet{}
+	pp.addresses = AddressSet{}
 	for _, listener := range pp.listeners {
 		listener.NoEndpoints(exists)
 	}
@@ -529,8 +537,8 @@ func (pp *portPublisher) noEndpoints(exists bool) {
 
 func (pp *portPublisher) subscribe(listener EndpointUpdateListener) {
 	if pp.exists {
-		if len(pp.pods.Pods) > 0 {
-			listener.Add(pp.pods)
+		if len(pp.addresses.Addresses) > 0 {
+			listener.Add(pp.addresses)
 		} else {
 			listener.NoEndpoints(true)
 		}
@@ -584,28 +592,28 @@ func getTargetPort(service *corev1.Service, port Port) namedPort {
 	return targetPort
 }
 
-func diffPods(oldPods, newPods PodSet) (add, remove PodSet) {
+func diffAddresses(oldAddresses, newAddresses AddressSet) (add, remove AddressSet) {
 	// TODO: this detects pods which have been added or removed, but does not
-	// detect pods which have been modified.  A modified pod should trigger
+	// detect addresses which have been modified.  A modified address should trigger
 	// an add of the new version.
-	addPods := make(map[PodID]Address)
-	removePods := make(map[PodID]Address)
-	for id, pod := range newPods.Pods {
-		if _, ok := oldPods.Pods[id]; !ok {
-			addPods[id] = pod
+	addAddesses := make(map[ID]Address)
+	removeAddresses := make(map[ID]Address)
+	for id, address := range newAddresses.Addresses {
+		if _, ok := oldAddresses.Addresses[id]; !ok {
+			addAddesses[id] = address
 		}
 	}
-	for id, pod := range oldPods.Pods {
-		if _, ok := newPods.Pods[id]; !ok {
-			removePods[id] = pod
+	for id, address := range oldAddresses.Addresses {
+		if _, ok := newAddresses.Addresses[id]; !ok {
+			removeAddresses[id] = address
 		}
 	}
-	add = PodSet{
-		Pods:   addPods,
-		Labels: newPods.Labels,
+	add = AddressSet{
+		Addresses: addAddesses,
+		Labels:    newAddresses.Labels,
 	}
-	remove = PodSet{
-		Pods: removePods,
+	remove = AddressSet{
+		Addresses: removeAddresses,
 	}
 	return
 }
diff --git a/controller/api/destination/watcher/ip_watcher.go b/controller/api/destination/watcher/ip_watcher.go
index a38aa1330..b0a45d017 100644
--- a/controller/api/destination/watcher/ip_watcher.go
+++ b/controller/api/destination/watcher/ip_watcher.go
@@ -29,7 +29,7 @@ type (
 
 		// At most one of service or pod may be non-zero.
 		service ServiceID
-		pod     PodSet
+		pod     AddressSet
 
 		listeners map[EndpointUpdateListener]Port
 		endpoints *EndpointsWatcher
@@ -150,7 +150,7 @@ func (iw *IPWatcher) addPod(obj interface{}) {
 		return
 	}
 	ss := iw.getOrNewServiceSubscriptions(pod.Status.PodIP)
-	ss.updatePod(iw.podToPodSet(pod))
+	ss.updatePod(iw.podToAddressSet(pod))
 }
 
 func (iw *IPWatcher) deletePod(obj interface{}) {
@@ -228,7 +228,7 @@ func (iw *IPWatcher) getOrNewServiceSubscriptions(clusterIP string) *serviceSubs
 				iw.log.Errorf("Pod IP conflict: %v, %v", objs[0], objs[1])
 			}
 			if len(pods) == 1 {
-				ss.pod = iw.podToPodSet(pods[0])
+				ss.pod = iw.podToAddressSet(pods[0])
 			}
 		}
 
@@ -244,14 +244,14 @@ func (iw *IPWatcher) getServiceSubscriptions(clusterIP string) (ss *serviceSubsc
 	return
 }
 
-func (iw *IPWatcher) podToPodSet(pod *corev1.Pod) PodSet {
+func (iw *IPWatcher) podToAddressSet(pod *corev1.Pod) AddressSet {
 	if pod.Spec.HostNetwork {
-		return PodSet{}
+		return AddressSet{}
 	}
 
 	ownerKind, ownerName := iw.k8sAPI.GetOwnerKindAndName(pod, true)
-	return PodSet{
-		Pods: map[PodID]Address{
+	return AddressSet{
+		Addresses: map[PodID]Address{
 			PodID{
 				Name:      pod.Name,
 				Namespace: pod.Namespace,
@@ -292,7 +292,7 @@ func (ss *serviceSubscriptions) updateService(service *corev1.Service) {
 			}
 		}
 		ss.service = id
-		ss.pod = PodSet{}
+		ss.pod = AddressSet{}
 	}
 }
 
@@ -309,13 +309,13 @@ func (ss *serviceSubscriptions) deleteService() {
 	ss.service = ServiceID{}
 }
 
-func (ss *serviceSubscriptions) updatePod(podSet PodSet) {
+func (ss *serviceSubscriptions) updatePod(podSet AddressSet) {
 	ss.Lock()
 	defer ss.Unlock()
 
 	for listener, port := range ss.listeners {
 		listener.NoEndpoints(true) // Clear out previous endpoints.
-		if len(podSet.Pods) != 0 {
+		if len(podSet.Addresses) != 0 {
 			podSetWithPort := withPort(podSet, port)
 			listener.Add(podSetWithPort)
 		}
@@ -330,7 +330,7 @@ func (ss *serviceSubscriptions) deletePod() {
 		listener.NoEndpoints(true) // Clear out previous endpoints.
 		listener.Add(singletonAddress(ss.clusterIP, port))
 	}
-	ss.pod = PodSet{}
+	ss.pod = AddressSet{}
 }
 
 func (ss *serviceSubscriptions) subscribe(port Port, listener EndpointUpdateListener) error {
@@ -342,7 +342,7 @@ func (ss *serviceSubscriptions) subscribe(port Port, listener EndpointUpdateList
 		if err != nil {
 			return err
 		}
-	} else if len(ss.pod.Pods) != 0 {
+	} else if len(ss.pod.Addresses) != 0 {
 		podSetWithPort := withPort(ss.pod, port)
 		listener.Add(podSetWithPort)
 	} else {
@@ -362,21 +362,21 @@ func (ss *serviceSubscriptions) unsubscribe(port Port, listener EndpointUpdateLi
 	delete(ss.listeners, listener)
 }
 
-func withPort(pods PodSet, port Port) PodSet {
-	wp := PodSet{
-		Pods:   map[PodID]Address{},
-		Labels: pods.Labels,
+func withPort(pods AddressSet, port Port) AddressSet {
+	wp := AddressSet{
+		Addresses: map[PodID]Address{},
+		Labels:    pods.Labels,
 	}
-	for id, pod := range pods.Pods {
-		pod.Port = port
-		wp.Pods[id] = pod
+	for id, addr := range pods.Addresses {
+		addr.Port = port
+		wp.Addresses[id] = addr
 	}
 	return wp
 }
 
-func singletonAddress(ip string, port Port) PodSet {
-	return PodSet{
-		Pods: map[PodID]Address{
+func singletonAddress(ip string, port Port) AddressSet {
+	return AddressSet{
+		Addresses: map[PodID]Address{
 			PodID{}: Address{
 				IP:   ip,
 				Port: port,
diff --git a/grafana/dashboards/authority.json b/grafana/dashboards/authority.json
index 08b6caf7f..9e8adedc6 100644
--- a/grafana/dashboards/authority.json
+++ b/grafana/dashboards/authority.json
@@ -1180,7 +1180,7 @@
   "templating": {
     "list": [
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
@@ -1202,7 +1202,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
diff --git a/grafana/dashboards/cronjob.json b/grafana/dashboards/cronjob.json
index 8d9a97eb9..0f7268e1c 100644
--- a/grafana/dashboards/cronjob.json
+++ b/grafana/dashboards/cronjob.json
@@ -2174,7 +2174,7 @@
     "templating": {
       "list": [
         {
-          "allValue": null,
+          "allValue": ".*",
           "current": {},
           "datasource": "prometheus",
           "definition": "",
@@ -2196,7 +2196,7 @@
           "useTags": false
         },
         {
-          "allValue": null,
+          "allValue": ".*",
           "current": {},
           "datasource": "prometheus",
           "definition": "",
@@ -2218,7 +2218,7 @@
           "useTags": false
         },
         {
-          "allValue": null,
+          "allValue": ".*",
           "current": {},
           "datasource": "prometheus",
           "definition": "",
@@ -2240,7 +2240,7 @@
           "useTags": false
         },
         {
-          "allValue": null,
+          "allValue": ".*",
           "current": {},
           "datasource": "prometheus",
           "definition": "",
diff --git a/grafana/dashboards/daemonset.json b/grafana/dashboards/daemonset.json
index c11e634d0..0a86b717c 100644
--- a/grafana/dashboards/daemonset.json
+++ b/grafana/dashboards/daemonset.json
@@ -2174,7 +2174,7 @@
   "templating": {
     "list": [
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
@@ -2196,7 +2196,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
@@ -2218,7 +2218,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
@@ -2240,7 +2240,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
diff --git a/grafana/dashboards/deployment.json b/grafana/dashboards/deployment.json
index 32185b3f0..08c5d83d7 100644
--- a/grafana/dashboards/deployment.json
+++ b/grafana/dashboards/deployment.json
@@ -2174,7 +2174,7 @@
   "templating": {
     "list": [
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
@@ -2196,7 +2196,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
@@ -2218,7 +2218,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
@@ -2240,7 +2240,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
diff --git a/grafana/dashboards/health.json b/grafana/dashboards/health.json
index 203faf5a6..c613141db 100644
--- a/grafana/dashboards/health.json
+++ b/grafana/dashboards/health.json
@@ -2233,7 +2233,7 @@
   "templating": {
     "list": [
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {
           "text": "All",
           "value": "$__all"
@@ -2258,7 +2258,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {
           "text": "All",
           "value": "$__all"
@@ -2283,7 +2283,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
diff --git a/grafana/dashboards/job.json b/grafana/dashboards/job.json
index 69bdabd5d..e2e268c07 100644
--- a/grafana/dashboards/job.json
+++ b/grafana/dashboards/job.json
@@ -2174,7 +2174,7 @@
   "templating": {
     "list": [
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
@@ -2196,7 +2196,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
@@ -2218,7 +2218,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
@@ -2240,7 +2240,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
diff --git a/grafana/dashboards/namespace.json b/grafana/dashboards/namespace.json
index 04b82c2dc..3d6a459ab 100644
--- a/grafana/dashboards/namespace.json
+++ b/grafana/dashboards/namespace.json
@@ -893,7 +893,7 @@
   "templating": {
     "list": [
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "label_values(process_start_time_seconds{deployment!=\"\"}, namespace)",
@@ -915,7 +915,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "label_values(process_start_time_seconds{namespace=\"$namespace\"}, deployment)",
diff --git a/grafana/dashboards/pod.json b/grafana/dashboards/pod.json
index 93ffa54b0..1f99773c3 100644
--- a/grafana/dashboards/pod.json
+++ b/grafana/dashboards/pod.json
@@ -2121,7 +2121,7 @@
   "templating": {
     "list": [
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
@@ -2143,7 +2143,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
@@ -2165,7 +2165,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "label_values(process_start_time_seconds{namespace=\"$namespace\"}, deployment)",
@@ -2187,7 +2187,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {
           "text": "All",
           "value": "$__all"
@@ -2212,7 +2212,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {
           "text": "All",
           "value": "$__all"
diff --git a/grafana/dashboards/prometheus-benchmark.json b/grafana/dashboards/prometheus-benchmark.json
index 1a3d36423..c4911aeb5 100644
--- a/grafana/dashboards/prometheus-benchmark.json
+++ b/grafana/dashboards/prometheus-benchmark.json
@@ -3641,7 +3641,7 @@
   "templating": {
     "list": [
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
@@ -3663,7 +3663,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
diff --git a/grafana/dashboards/replicaset.json b/grafana/dashboards/replicaset.json
index 59b56bb9b..9564224fc 100644
--- a/grafana/dashboards/replicaset.json
+++ b/grafana/dashboards/replicaset.json
@@ -2208,7 +2208,7 @@
     "templating": {
       "list": [
         {
-          "allValue": null,
+          "allValue": ".*",
           "current": {
             "text": "default",
             "value": "default"
@@ -2233,7 +2233,7 @@
           "useTags": false
         },
         {
-          "allValue": null,
+          "allValue": ".*",
           "current": {
             "text": "rs1",
             "value": "rs1"
@@ -2258,7 +2258,7 @@
           "useTags": false
         },
         {
-          "allValue": null,
+          "allValue": ".*",
           "current": {
             "text": "All",
             "value": "$__all"
@@ -2283,7 +2283,7 @@
           "useTags": false
         },
         {
-          "allValue": null,
+          "allValue": ".*",
           "current": {
             "text": "All",
             "value": "$__all"
diff --git a/grafana/dashboards/replicationcontroller.json b/grafana/dashboards/replicationcontroller.json
index 5b4389a69..36c271145 100644
--- a/grafana/dashboards/replicationcontroller.json
+++ b/grafana/dashboards/replicationcontroller.json
@@ -2174,7 +2174,7 @@
   "templating": {
     "list": [
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
@@ -2196,7 +2196,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
@@ -2218,7 +2218,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
@@ -2240,7 +2240,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
diff --git a/grafana/dashboards/service.json b/grafana/dashboards/service.json
index dea9edc23..db517460f 100644
--- a/grafana/dashboards/service.json
+++ b/grafana/dashboards/service.json
@@ -1180,7 +1180,7 @@
   "templating": {
     "list": [
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
@@ -1202,7 +1202,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
@@ -1224,7 +1224,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {
           "text": "All",
           "value": "$__all"
@@ -1249,7 +1249,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {
           "text": "All",
           "value": "$__all"
diff --git a/grafana/dashboards/statefulset.json b/grafana/dashboards/statefulset.json
index b9491a996..b6df517ec 100644
--- a/grafana/dashboards/statefulset.json
+++ b/grafana/dashboards/statefulset.json
@@ -2174,7 +2174,7 @@
   "templating": {
     "list": [
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
@@ -2196,7 +2196,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
@@ -2218,7 +2218,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
@@ -2240,7 +2240,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {},
         "datasource": "prometheus",
         "definition": "",
diff --git a/grafana/dashboards/top-line.json b/grafana/dashboards/top-line.json
index 6483a4773..d9181a2b6 100644
--- a/grafana/dashboards/top-line.json
+++ b/grafana/dashboards/top-line.json
@@ -999,7 +999,7 @@
   "templating": {
     "list": [
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {
           "text": "All",
           "value": "$__all"
@@ -1024,7 +1024,7 @@
         "useTags": false
       },
       {
-        "allValue": null,
+        "allValue": ".*",
         "current": {
           "text": "All",
           "value": "$__all"
diff --git a/pkg/k8s/labels.go b/pkg/k8s/labels.go
index fbff494de..4a59499bb 100644
--- a/pkg/k8s/labels.go
+++ b/pkg/k8s/labels.go
@@ -374,6 +374,9 @@ const (
 	// in cases we detect a change in a remote gateway
 	RemoteGatewayResourceVersionAnnotation = SvcMirrorPrefix + "/remote-gateway-resource-version"
 
+	// RemoteGatewayIdentity follows the same kind of logic as RemoteGatewayNameLabel
+	RemoteGatewayIdentity = SvcMirrorPrefix + "/remote-gateway-identity"
+
 	// ConfigKeyName is the key in the secret that stores the kubeconfig needed to connect
 	// to a remote cluster
 	ConfigKeyName = "kubeconfig"
diff --git a/pkg/profiles/openapi.go b/pkg/profiles/openapi.go
index 661e6c57d..88ad65bd1 100644
--- a/pkg/profiles/openapi.go
+++ b/pkg/profiles/openapi.go
@@ -17,6 +17,7 @@ import (
 
 const (
 	xLinkerdRetryable = "x-linkerd-retryable"
+	xLinkerdTimeout   = "x-linkerd-timeout"
 )
 
 var pathParamRegex = regexp.MustCompile(`\\{[^\}]*\\}`)
@@ -110,9 +111,11 @@ func swaggerToServiceProfile(swagger spec.Swagger, namespace, name, clusterDomai
 
 func mkRouteSpec(path, pathRegex string, method string, operation *spec.Operation) *sp.RouteSpec {
 	retryable := false
+	timeout := ""
 	var responses *spec.Responses
 	if operation != nil {
 		retryable, _ = operation.VendorExtensible.Extensions.GetBool(xLinkerdRetryable)
+		timeout, _ = operation.VendorExtensible.Extensions.GetString(xLinkerdTimeout)
 		responses = operation.Responses
 	}
 	return &sp.RouteSpec{
@@ -120,6 +123,7 @@ func mkRouteSpec(path, pathRegex string, method string, operation *spec.Operatio
 		Condition:       toReqMatch(pathRegex, method),
 		ResponseClasses: toRspClasses(responses),
 		IsRetryable:     retryable,
+		Timeout:         timeout,
 	}
 }
 
diff --git a/pkg/smimetrics/client.go b/pkg/smimetrics/client.go
index 841744288..b4c0ae367 100644
--- a/pkg/smimetrics/client.go
+++ b/pkg/smimetrics/client.go
@@ -60,6 +60,10 @@ type (
 
 const apiBase = "/apis/metrics.smi-spec.io/v1alpha1"
 
+func (r Resource) String() string {
+	return fmt.Sprintf("%s.%s", r.Name, r.Namespace)
+}
+
 // GetTrafficMetrics returns the inbound traffic metrics for a specific named
 // resource.
 func GetTrafficMetrics(k8sAPI *k8s.KubernetesAPI, namespace, kind, name string, params map[string]string) (*TrafficMetrics, error) {


Test patch:
diff --git a/controller/api/destination/endpoint_translator_test.go b/controller/api/destination/endpoint_translator_test.go
index 7c57b1e45..3f5e22b38 100644
--- a/controller/api/destination/endpoint_translator_test.go
+++ b/controller/api/destination/endpoint_translator_test.go
@@ -1,8 +1,10 @@
 package destination
 
 import (
+	"fmt"
 	"reflect"
 	"sort"
+	"strings"
 	"testing"
 
 	pb "github.com/linkerd/linkerd2-proxy-api/go/destination"
@@ -92,6 +94,17 @@ var (
 			},
 		},
 	}
+
+	remoteGatewayWithNoTLS = watcher.Address{
+		IP:   "1.1.1.1",
+		Port: 1,
+	}
+
+	remoteGatewayWithTLS = watcher.Address{
+		IP:       "1.1.1.2",
+		Port:     2,
+		Identity: "some-identity",
+	}
 )
 
 func makeEndpointTranslator(t *testing.T) (*mockDestinationGetServer, *endpointTranslator) {
@@ -99,7 +112,7 @@ func makeEndpointTranslator(t *testing.T) (*mockDestinationGetServer, *endpointT
 	translator := newEndpointTranslator(
 		"linkerd",
 		"trust.domain",
-		false,
+		true,
 		"service-name.service-ns",
 		mockGetServer,
 		logging.WithField("test", t.Name()),
@@ -107,12 +120,77 @@ func makeEndpointTranslator(t *testing.T) (*mockDestinationGetServer, *endpointT
 	return mockGetServer, translator
 }
 
-func TestEndpointTranslator(t *testing.T) {
+func TestEndpointTranslatorForRemoteGateways(t *testing.T) {
+	t.Run("Sends one update for add and another for remove", func(t *testing.T) {
+		mockGetServer, translator := makeEndpointTranslator(t)
+
+		translator.Add(mkAddressSetForServices(remoteGatewayWithNoTLS))
+		translator.Remove(mkAddressSetForServices(remoteGatewayWithTLS))
+
+		expectedNumUpdates := 2
+		actualNumUpdates := len(mockGetServer.updatesReceived)
+		if actualNumUpdates != expectedNumUpdates {
+			t.Fatalf("Expecting [%d] updates, got [%d]. Updates: %v", expectedNumUpdates, actualNumUpdates, mockGetServer.updatesReceived)
+		}
+	})
+
+	t.Run("Sends TlsIdentity when enabled", func(t *testing.T) {
+		expectedTLSIdentity := &pb.TlsIdentity_DnsLikeIdentity{
+			Name: "some-identity",
+		}
+
+		expectedProtocolHint := &pb.ProtocolHint{
+			Protocol: &pb.ProtocolHint_H2_{
+				H2: &pb.ProtocolHint_H2{},
+			},
+		}
+
+		mockGetServer, translator := makeEndpointTranslator(t)
+
+		translator.Add(mkAddressSetForServices(remoteGatewayWithTLS))
+
+		addrs := mockGetServer.updatesReceived[0].GetAdd().GetAddrs()
+		if len(addrs) != 1 {
+			t.Fatalf("Expected [1] address returned, got %v", addrs)
+		}
+
+		actualTLSIdentity := addrs[0].GetTlsIdentity().GetDnsLikeIdentity()
+		if !reflect.DeepEqual(actualTLSIdentity, expectedTLSIdentity) {
+			t.Fatalf("Expected TlsIdentity to be [%v] but was [%v]", expectedTLSIdentity, actualTLSIdentity)
+		}
+
+		actualProtocolHint := addrs[0].GetProtocolHint()
+		if !reflect.DeepEqual(actualProtocolHint, expectedProtocolHint) {
+			t.Fatalf("Expected ProtoclHint to be [%v] but was [%v]", expectedProtocolHint, actualProtocolHint)
+		}
+	})
+
+	t.Run("Does not send TlsIdentity when not present", func(t *testing.T) {
+		mockGetServer, translator := makeEndpointTranslator(t)
+
+		translator.Add(mkAddressSetForServices(remoteGatewayWithNoTLS))
+
+		addrs := mockGetServer.updatesReceived[0].GetAdd().GetAddrs()
+		if len(addrs) != 1 {
+			t.Fatalf("Expected [1] address returned, got %v", addrs)
+		}
+
+		if addrs[0].TlsIdentity != nil {
+			t.Fatalf("Expected no TlsIdentity to be sent, but got [%v]", addrs[0].TlsIdentity)
+		}
+		if addrs[0].ProtocolHint != nil {
+			t.Fatalf("Expected no ProtocolHint to be sent, but got [%v]", addrs[0].TlsIdentity)
+		}
+	})
+
+}
+
+func TestEndpointTranslatorForPods(t *testing.T) {
 	t.Run("Sends one update for add and another for remove", func(t *testing.T) {
 		mockGetServer, translator := makeEndpointTranslator(t)
 
-		translator.Add(mkPodSet(normalPod))
-		translator.Remove(mkPodSet(tlsOptionalPod))
+		translator.Add(mkAddressSetForPods(normalPod))
+		translator.Remove(mkAddressSetForPods(tlsOptionalPod))
 
 		expectedNumUpdates := 2
 		actualNumUpdates := len(mockGetServer.updatesReceived)
@@ -124,8 +202,8 @@ func TestEndpointTranslator(t *testing.T) {
 	t.Run("Sends addresses as removed or added", func(t *testing.T) {
 		mockGetServer, translator := makeEndpointTranslator(t)
 
-		translator.Add(mkPodSet(normalPod, tlsOptionalPod))
-		translator.Remove(mkPodSet(tlsDisabledPod))
+		translator.Add(mkAddressSetForPods(normalPod, tlsOptionalPod))
+		translator.Remove(mkAddressSetForPods(tlsDisabledPod))
 
 		addressesAdded := mockGetServer.updatesReceived[0].GetAdd().Addrs
 		actualNumberOfAdded := len(addressesAdded)
@@ -152,7 +230,7 @@ func TestEndpointTranslator(t *testing.T) {
 	t.Run("Sends metric labels with added addresses", func(t *testing.T) {
 		mockGetServer, translator := makeEndpointTranslator(t)
 
-		translator.Add(mkPodSet(normalPod))
+		translator.Add(mkAddressSetForPods(normalPod))
 
 		actualGlobalMetricLabels := mockGetServer.updatesReceived[0].GetAdd().MetricLabels
 		expectedGlobalMetricLabels := map[string]string{"namespace": "service-ns", "service": "service-name"}
@@ -179,7 +257,7 @@ func TestEndpointTranslator(t *testing.T) {
 
 		mockGetServer, translator := makeEndpointTranslator(t)
 
-		translator.Add(mkPodSet(normalPod))
+		translator.Add(mkAddressSetForPods(normalPod))
 
 		addrs := mockGetServer.updatesReceived[0].GetAdd().GetAddrs()
 		if len(addrs) != 1 {
@@ -195,7 +273,7 @@ func TestEndpointTranslator(t *testing.T) {
 	t.Run("Does not send TlsIdentity for non-default identity-modes", func(t *testing.T) {
 		mockGetServer, translator := makeEndpointTranslator(t)
 
-		translator.Add(mkPodSet(tlsOptionalPod))
+		translator.Add(mkAddressSetForPods(tlsOptionalPod))
 
 		addrs := mockGetServer.updatesReceived[0].GetAdd().GetAddrs()
 		if len(addrs) != 1 {
@@ -210,7 +288,7 @@ func TestEndpointTranslator(t *testing.T) {
 	t.Run("Does not send TlsIdentity for other meshes", func(t *testing.T) {
 		mockGetServer, translator := makeEndpointTranslator(t)
 
-		translator.Add(mkPodSet(otherMeshPod))
+		translator.Add(mkAddressSetForPods(otherMeshPod))
 
 		addrs := mockGetServer.updatesReceived[0].GetAdd().GetAddrs()
 		if len(addrs) != 1 {
@@ -225,7 +303,7 @@ func TestEndpointTranslator(t *testing.T) {
 	t.Run("Does not send TlsIdentity when not enabled", func(t *testing.T) {
 		mockGetServer, translator := makeEndpointTranslator(t)
 
-		translator.Add(mkPodSet(tlsDisabledPod))
+		translator.Add(mkAddressSetForPods(tlsDisabledPod))
 
 		addrs := mockGetServer.updatesReceived[0].GetAdd().GetAddrs()
 		if len(addrs) != 1 {
@@ -238,14 +316,33 @@ func TestEndpointTranslator(t *testing.T) {
 	})
 }
 
-func mkPodSet(pods ...watcher.Address) watcher.PodSet {
-	set := watcher.PodSet{
-		Pods:   make(map[watcher.PodID]watcher.Address),
-		Labels: map[string]string{"service": "service-name", "namespace": "service-ns"},
+func mkAddressSetForServices(gatewayAddresses ...watcher.Address) watcher.AddressSet {
+	set := watcher.AddressSet{
+		Addresses: make(map[watcher.ServiceID]watcher.Address),
+		Labels:    map[string]string{"service": "service-name", "namespace": "service-ns"},
+	}
+	for _, a := range gatewayAddresses {
+		a := a // pin
+
+		id := watcher.ServiceID{
+			Name: strings.Join([]string{
+				a.IP,
+				fmt.Sprint(a.Port),
+			}, "-"),
+		}
+		set.Addresses[id] = a
+	}
+	return set
+}
+
+func mkAddressSetForPods(podAddresses ...watcher.Address) watcher.AddressSet {
+	set := watcher.AddressSet{
+		Addresses: make(map[watcher.PodID]watcher.Address),
+		Labels:    map[string]string{"service": "service-name", "namespace": "service-ns"},
 	}
-	for _, p := range pods {
+	for _, p := range podAddresses {
 		id := watcher.PodID{Name: p.Pod.Name, Namespace: p.Pod.Namespace}
-		set.Pods[id] = p
+		set.Addresses[id] = p
 	}
 	return set
 }
diff --git a/controller/api/destination/watcher/endpoints_watcher_test.go b/controller/api/destination/watcher/endpoints_watcher_test.go
index 6798ca2ca..07d566848 100644
--- a/controller/api/destination/watcher/endpoints_watcher_test.go
+++ b/controller/api/destination/watcher/endpoints_watcher_test.go
@@ -27,17 +27,21 @@ func newBufferingEndpointListener() *bufferingEndpointListener {
 }
 
 func addressString(address Address) string {
-	return fmt.Sprintf("%s:%d", address.IP, address.Port)
+	addressString := fmt.Sprintf("%s:%d", address.IP, address.Port)
+	if address.Identity != "" {
+		addressString = fmt.Sprintf("%s/%s", addressString, address.Identity)
+	}
+	return addressString
 }
 
-func (bel *bufferingEndpointListener) Add(set PodSet) {
-	for _, address := range set.Pods {
+func (bel *bufferingEndpointListener) Add(set AddressSet) {
+	for _, address := range set.Addresses {
 		bel.added = append(bel.added, addressString(address))
 	}
 }
 
-func (bel *bufferingEndpointListener) Remove(set PodSet) {
-	for _, address := range set.Pods {
+func (bel *bufferingEndpointListener) Remove(set AddressSet) {
+	for _, address := range set.Addresses {
 		bel.removed = append(bel.removed, addressString(address))
 	}
 }
@@ -271,7 +275,7 @@ status:
 			expectedError:                    false,
 		},
 		{
-			serviceType: "local services with missing pods",
+			serviceType: "local services with missing addresses",
 			k8sConfigs: []string{`
 apiVersion: v1
 kind: Service
@@ -615,5 +619,165 @@ status:
 		})
 
 	}
+}
+
+func TestEndpointsWatcherServiceMirrors(t *testing.T) {
+	for _, tt := range []struct {
+		serviceType                      string
+		k8sConfigs                       []string
+		id                               ServiceID
+		hostname                         string
+		port                             Port
+		expectedAddresses                []string
+		expectedNoEndpoints              bool
+		expectedNoEndpointsServiceExists bool
+	}{
+		{
+			k8sConfigs: []string{`
+apiVersion: v1
+kind: Service
+metadata:
+  name: name1-remote
+  namespace: ns
+spec:
+  type: LoadBalancer
+  ports:
+  - port: 8989`,
+				`
+apiVersion: v1
+kind: Endpoints
+metadata:
+  name: name1-remote
+  namespace: ns
+  annotations:
+    mirror.linkerd.io/remote-gateway-identity: "gateway-identity-1"
+    mirror.linkerd.io/remote-svc-fq-name: "name1-remote-fq"
+  labels:
+    mirror.linkerd.io/mirrored-service: "true"
+subsets:
+- addresses:
+  - ip: 172.17.0.12
+  ports:
+  - port: 8989`,
+			},
+			serviceType: "mirrored service with identity",
+			id:          ServiceID{Name: "name1-remote", Namespace: "ns"},
+			port:        8989,
+			expectedAddresses: []string{
+				"172.17.0.12:8989/gateway-identity-1",
+			},
+			expectedNoEndpoints:              false,
+			expectedNoEndpointsServiceExists: false,
+		},
+		{
+			k8sConfigs: []string{`
+apiVersion: v1
+kind: Service
+metadata:
+  name: name1-remote
+  namespace: ns
+spec:
+  type: LoadBalancer
+  ports:
+  - port: 8989`,
+				`
+apiVersion: v1
+kind: Endpoints
+metadata:
+  name: name1-remote
+  namespace: ns
+  annotations:
+    mirror.linkerd.io/remote-svc-fq-name: "name1-remote-fq"
+  labels:
+    mirror.linkerd.io/mirrored-service: "true"
+subsets:
+- addresses:
+  - ip: 172.17.0.12
+  ports:
+  - port: 8989`,
+			},
+			serviceType: "mirrored service without identity",
+			id:          ServiceID{Name: "name1-remote", Namespace: "ns"},
+			port:        8989,
+			expectedAddresses: []string{
+				"172.17.0.12:8989",
+			},
+			expectedNoEndpoints:              false,
+			expectedNoEndpointsServiceExists: false,
+		},
+
+		{
+			k8sConfigs: []string{`
+apiVersion: v1
+kind: Service
+metadata:
+  name: name1-remote
+  namespace: ns
+spec:
+  type: LoadBalancer
+  ports:
+  - port: 8989`,
+				`
+apiVersion: v1
+kind: Endpoints
+metadata:
+  name: name1-remote
+  namespace: ns
+  annotations:
+    mirror.linkerd.io/remote-gateway-identity: "gateway-identity-1"
+    mirror.linkerd.io/remote-svc-fq-name: "name1-remote-fq"
+  labels:
+    mirror.linkerd.io/mirrored-service: "true"
+subsets:
+- addresses:
+  - ip: 172.17.0.12
+  ports:
+  - port: 9999`,
+			},
+			serviceType: "mirrored service with remapped port in endpoints",
+			id:          ServiceID{Name: "name1-remote", Namespace: "ns"},
+			port:        9999,
+			expectedAddresses: []string{
+				"172.17.0.12:9999/gateway-identity-1",
+			},
+			expectedNoEndpoints:              false,
+			expectedNoEndpointsServiceExists: false,
+		},
+	} {
+		tt := tt // pin
+		t.Run("subscribes listener to "+tt.serviceType, func(t *testing.T) {
+			k8sAPI, err := k8s.NewFakeAPI(tt.k8sConfigs...)
+			if err != nil {
+				t.Fatalf("NewFakeAPI returned an error: %s", err)
+			}
+
+			watcher := NewEndpointsWatcher(k8sAPI, logging.WithField("test", t.Name()))
+
+			k8sAPI.Sync(nil)
+
+			listener := newBufferingEndpointListener()
+
+			err = watcher.Subscribe(tt.id, tt.port, tt.hostname, listener)
+
+			if err != nil {
+				t.Fatalf("NewFakeAPI returned an error: %s", err)
+			}
+
+			actualAddresses := make([]string, 0)
+			actualAddresses = append(actualAddresses, listener.added...)
+			sort.Strings(actualAddresses)
+
+			testCompare(t, tt.expectedAddresses, actualAddresses)
+
+			if listener.noEndpointsCalled != tt.expectedNoEndpoints {
+				t.Fatalf("Expected noEndpointsCalled to be [%t], got [%t]",
+					tt.expectedNoEndpoints, listener.noEndpointsCalled)
+			}
 
+			if listener.noEndpointsExists != tt.expectedNoEndpointsServiceExists {
+				t.Fatalf("Expected noEndpointsExists to be [%t], got [%t]",
+					tt.expectedNoEndpointsServiceExists, listener.noEndpointsExists)
+			}
+		})
+	}
 }
diff --git a/controller/api/destination/watcher/ip_watcher_test.go b/controller/api/destination/watcher/ip_watcher_test.go
index 2e4218088..17207a2b9 100644
--- a/controller/api/destination/watcher/ip_watcher_test.go
+++ b/controller/api/destination/watcher/ip_watcher_test.go
@@ -531,7 +531,7 @@ status:
 		deletingPod    bool
 	}{
 		{
-			description:    "can delete pods",
+			description:    "can delete addresses",
 			k8sConfigs:     podK8sConfig,
 			host:           "172.17.0.12",
 			port:           8989,
@@ -539,7 +539,7 @@ status:
 			deletingPod:    true,
 		},
 		{
-			description:    "can delete pods wrapped in a DeletedFinalStateUnknown",
+			description:    "can delete addresses wrapped in a DeletedFinalStateUnknown",
 			k8sConfigs:     podK8sConfig,
 			host:           "172.17.0.12",
 			port:           8989,
diff --git a/pkg/profiles/openapi_test.go b/pkg/profiles/openapi_test.go
index 72dbe3aa8..39f419b51 100644
--- a/pkg/profiles/openapi_test.go
+++ b/pkg/profiles/openapi_test.go
@@ -30,7 +30,7 @@ func TestSwaggerToServiceProfile(t *testing.T) {
 									},
 								},
 								VendorExtensible: spec.VendorExtensible{
-									Extensions: spec.Extensions{xLinkerdRetryable: true},
+									Extensions: spec.Extensions{xLinkerdRetryable: true, xLinkerdTimeout: "60s"},
 								},
 							},
 						},
@@ -66,6 +66,7 @@ func TestSwaggerToServiceProfile(t *testing.T) {
 						},
 					},
 					IsRetryable: true,
+					Timeout:     "60s",
 				},
 			},
 		},