Repository: linkerd/linkerd2
Pull request #4446 (2020-05-19 23:46:04)

Properly show the meshed count for non-selector services
When viewing the output of `linkerd stat` for services which do not have a selector (such as services created by the service-mirror, for example) the meshed count column shows the total number which exist, even though the service actually selects no pods at all.

We update the StatSummary implementation to account for services which have no selector.

Additionally, we update the logic of the `--unmeshed` flag.  When the `--unmeshed` flag is not set, we typically skip rows for unmeshed resources because those resources would have no stats.  This is not appropriate to do when the `--from` flag is also set because in this case, metrics are not collected on the target resource but are instead collected on the client-side.  This means that stats can be present, even for unmeshed resources and these resources should still be displayed, even if the `--unmeshed` flag is not set.

Signed-off-by: Alex Leong <alex@buoyant.io>

<!--  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 76cf13833..ba81f37c3 100644
--- a/.github/workflows/static_checks.yml
+++ b/.github/workflows/static_checks.yml
@@ -80,11 +80,18 @@ jobs:
       run: |
         find ./bin -type f \
         ! -name docker-build-proxy \
-        ! -name lint \
-        ! -name _log.sh \
         ! -name minikube-start-hyperv.bat \
         ! -name test-cleanup \
         ! -name _test-run.sh \
         ! -name *.nuspec \
         ! -name *.ps1 \
         | xargs -I {} bin/shellcheck -x -P ./bin {}
+  markdown_lint:
+    name: Markdown lint
+    runs-on: ubuntu-18.04
+    steps:
+    - name: Checkout code
+      # actions/checkout@v2
+      uses: actions/checkout@722adc6
+    - name: Markdown lint
+      run: bin/markdownlint-all
diff --git a/.gitignore b/.gitignore
index d6533786e..151f8553d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,4 @@ web/app/yarn-error.log
 **/*.gogen*
 **/*.swp
 charts/**/charts
+package-lock.json
diff --git a/.markdownlint.yaml b/.markdownlint.yaml
new file mode 100644
index 000000000..530389ae9
--- /dev/null
+++ b/.markdownlint.yaml
@@ -0,0 +1,6 @@
+default: true
+line_length: 
+  code_blocks: false
+  tables: false
+headings:
+  siblings_only: true
diff --git a/bin/_log.sh b/bin/_log.sh
index 9c9abe90e..72e9f53af 100644
--- a/bin/_log.sh
+++ b/bin/_log.sh
@@ -1,3 +1,4 @@
+#!/bin/sh
 set -eu
 
 # build debug logging is disabled by default; enable with BUILD_DEBUG=1
diff --git a/bin/lint b/bin/lint
index dbf1ff1cc..79837ac99 100755
--- a/bin/lint
+++ b/bin/lint
@@ -12,12 +12,8 @@ targetbin=$rootdir/target/bin
 
 cd "$rootdir"
 
-os=linux
 exe=
-if [ "$(uname -s)" = Darwin ]; then
-  os=darwin
-elif [ "$(uname -o)" = Msys ]; then
-  os=windows
+if [ "$(uname -o)" = Msys ]; then
   exe=.exe
 fi
 
diff --git a/bin/markdownlint b/bin/markdownlint
new file mode 100755
index 000000000..ed8ba6f72
--- /dev/null
+++ b/bin/markdownlint
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+set -eu
+
+bindir=$( cd "${BASH_SOURCE[0]%/*}" && pwd )
+rootdir=$( cd "$bindir"/.. && pwd )
+markdownlintbin="$rootdir/node_modules/.bin/markdownlint"
+
+markdownlint_version=0.23.1
+
+if [ ! -x "$markdownlintbin" ] || [ "$($markdownlintbin -V)" != $markdownlint_version ]; then
+    if ! [ -x "$(command -v npm)" ]; then
+        echo "Error: npm required to install markdownlint command"
+        exit 1
+    fi
+    npm install markdownlint-cli@$markdownlint_version
+fi
+
+"$markdownlintbin" "$@"
diff --git a/bin/markdownlint-all b/bin/markdownlint-all
new file mode 100755
index 000000000..e96b65a9c
--- /dev/null
+++ b/bin/markdownlint-all
@@ -0,0 +1,6 @@
+#!/bin/bash
+
+bindir=$( cd "${BASH_SOURCE[0]%/*}" && pwd )
+
+"$bindir"/markdownlint ./*.md
+"$bindir"/markdownlint ./**/*.md --ignore node_modules/
diff --git a/charts/add-ons/grafana/templates/grafana.yaml b/charts/add-ons/grafana/templates/grafana.yaml
index 931f685b2..721502447 100644
--- a/charts/add-ons/grafana/templates/grafana.yaml
+++ b/charts/add-ons/grafana/templates/grafana.yaml
@@ -93,7 +93,7 @@ metadata:
   labels:
     app.kubernetes.io/name: grafana
     app.kubernetes.io/part-of: Linkerd
-    app.kubernetes.io/version: {{default .Values.global.linkerdVersion .Values.controllerImageVersion}}
+    app.kubernetes.io/version: {{default .Values.global.linkerdVersion .Values.global.controllerImageVersion}}
     {{.Values.global.controllerComponentLabel}}: grafana
     {{.Values.global.controllerNamespaceLabel}}: {{.Values.global.namespace}}
   name: {{.Values.name}}
@@ -121,7 +121,7 @@ spec:
       - env:
         - name: GF_PATHS_DATA
           value: /data
-        image: {{.Values.image}}:{{default .Values.global.linkerdVersion .Values.controllerImageVersion}}
+        image: {{.Values.image.name}}:{{default .Values.global.controllerImageVersion .Values.image.version}}
         imagePullPolicy: {{.Values.global.imagePullPolicy}}
         livenessProbe:
           httpGet:
diff --git a/charts/add-ons/tracing/templates/tracing.yaml b/charts/add-ons/tracing/templates/tracing.yaml
index 5f929a131..ccba0f959 100644
--- a/charts/add-ons/tracing/templates/tracing.yaml
+++ b/charts/add-ons/tracing/templates/tracing.yaml
@@ -68,7 +68,7 @@ metadata:
   labels:
     app.kubernetes.io/name: {{.Values.collector.name}}
     app.kubernetes.io/part-of: Linkerd
-    app.kubernetes.io/version: {{default .Values.global.linkerdVersion .Values.controllerImageVersion}}
+    app.kubernetes.io/version: {{default .Values.global.linkerdVersion .Values.global.controllerImageVersion}}
     {{.Values.global.controllerComponentLabel}}: {{.Values.collector.name}}
     {{.Values.global.controllerNamespaceLabel}}: {{.Values.global.namespace}}
   name: {{.Values.collector.name}}
@@ -179,7 +179,7 @@ metadata:
   labels:
     app.kubernetes.io/name: {{.Values.jaeger.name}}
     app.kubernetes.io/part-of: Linkerd
-    app.kubernetes.io/version: {{default .Values.global.linkerdVersion .Values.controllerImageVersion}}
+    app.kubernetes.io/version: {{default .Values.global.linkerdVersion .Values.global.controllerImageVersion}}
     {{.Values.global.controllerComponentLabel}}: {{.Values.jaeger.name}}
     {{.Values.global.controllerNamespaceLabel}}: {{.Values.global.namespace}}
   name: {{.Values.jaeger.name}}
diff --git a/charts/linkerd2-multicluster-remote-setup/README.md b/charts/linkerd2-multicluster-remote-setup/README.md
index eba66e5ae..49155578c 100644
--- a/charts/linkerd2-multicluster-remote-setup/README.md
+++ b/charts/linkerd2-multicluster-remote-setup/README.md
@@ -15,7 +15,7 @@ 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`       |
+|`namespace`               | The namespace in which the gateway and SA will be created                                                       |`linkerd-multicluster`  |
 |`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                                                  |`4180`                    |
 |`linkerdNamespace`        | The namespace of the existing Linkerd installation                                                              |`linkerd`               |
@@ -25,4 +25,3 @@ linkerd2-multicluster-remote-setup chart and their default values.
 |`probePeriodSeconds`      | The interval (in seconds) between liveness probes                                                               |`3`                     |
 |`probePort`               | The port used for liveliness probing                                                                            |`4181`                    |
 |`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
index d06a75613..cc1dd473b 100644
--- a/charts/linkerd2-multicluster-remote-setup/templates/gateway.yaml
+++ b/charts/linkerd2-multicluster-remote-setup/templates/gateway.yaml
@@ -1,16 +1,11 @@
 ---
-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}}
+  namespace: {{.Values.namespace}}
 data:
   nginx.conf: |-
     events {
@@ -49,7 +44,7 @@ metadata:
     app.kubernetes.io/version: {{.Values.linkerdVersion}}
     app: {{.Values.gatewayName}}
   name: {{.Values.gatewayName}}
-  namespace: {{.Values.gatewayNamespace}}
+  namespace: {{.Values.namespace}}
 spec:
   replicas: 1
   selector:
@@ -97,9 +92,9 @@ apiVersion: v1
 kind: Service
 metadata:
   name: {{.Values.gatewayName}}
-  namespace: {{.Values.gatewayNamespace}}
+  namespace: {{.Values.namespace}}
   annotations: 
-    mirror.linkerd.io/gateway-identity: {{.Values.gatewayName}}.{{.Values.gatewayNamespace}}.serviceaccount.identity.{{.Values.linkerdNamespace}}.{{.Values.identityTrustDomain}}
+    mirror.linkerd.io/gateway-identity: {{.Values.gatewayName}}.{{.Values.namespace}}.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}}
@@ -120,5 +115,5 @@ kind: ServiceAccount
 apiVersion: v1
 metadata:
   name: {{.Values.gatewayName}}
-  namespace: {{.Values.gatewayNamespace}}
+  namespace: {{.Values.namespace}}
   
diff --git a/charts/linkerd2-multicluster-remote-setup/templates/namespace.yaml b/charts/linkerd2-multicluster-remote-setup/templates/namespace.yaml
new file mode 100644
index 000000000..48908101b
--- /dev/null
+++ b/charts/linkerd2-multicluster-remote-setup/templates/namespace.yaml
@@ -0,0 +1,5 @@
+kind: Namespace
+apiVersion: v1
+metadata:
+  name: {{ .Values.namespace }}
+
diff --git a/charts/linkerd2-multicluster-remote-setup/templates/service-mirror-rbac.yaml b/charts/linkerd2-multicluster-remote-setup/templates/service-mirror-rbac.yaml
index 5011cc92d..bac13b86c 100644
--- a/charts/linkerd2-multicluster-remote-setup/templates/service-mirror-rbac.yaml
+++ b/charts/linkerd2-multicluster-remote-setup/templates/service-mirror-rbac.yaml
@@ -1,14 +1,9 @@
 ---
-kind: Namespace
-apiVersion: v1
-metadata:
-  name: {{ .Values.serviceAccountNamespace }}
----
 apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRole
 metadata:
   name: {{.Values.serviceAccountName}}
-  namespace: {{.Values.serviceAccountNamespace}}
+  namespace: {{.Values.namespace}}
   annotations:
     {{.Values.createdByAnnotation}}: {{default (printf "linkerd/helm %s" .Values.linkerdVersion) .Values.cliVersion}}
 rules:
@@ -24,7 +19,7 @@ apiVersion: v1
 kind: ServiceAccount
 metadata:
   name: {{.Values.serviceAccountName}}
-  namespace: {{.Values.serviceAccountNamespace}}
+  namespace: {{.Values.namespace}}
   annotations:
     {{.Values.createdByAnnotation}}: {{default (printf "linkerd/helm %s" .Values.linkerdVersion) .Values.cliVersion}}
 ---
@@ -32,7 +27,7 @@ apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
 metadata:
   name: {{.Values.serviceAccountName}}
-  namespace: {{.Values.serviceAccountNamespace}}
+  namespace: {{.Values.namespace}}
   annotations:
     {{.Values.createdByAnnotation}}: {{default (printf "linkerd/helm %s" .Values.linkerdVersion) .Values.cliVersion}}
 roleRef:
@@ -42,5 +37,5 @@ roleRef:
 subjects:
 - kind: ServiceAccount
   name: {{.Values.serviceAccountName}}
-  namespace: {{.Values.serviceAccountNamespace}}
+  namespace: {{.Values.namespace}}
 
diff --git a/charts/linkerd2-multicluster-remote-setup/values.yaml b/charts/linkerd2-multicluster-remote-setup/values.yaml
index 861ceedcb..32ab96598 100644
--- a/charts/linkerd2-multicluster-remote-setup/values.yaml
+++ b/charts/linkerd2-multicluster-remote-setup/values.yaml
@@ -1,13 +1,12 @@
 gatewayName: linkerd-gateway
-gatewayNamespace: linkerd-gateway
+namespace: linkerd-multicluster
 identityTrustDomain: cluster.local
 incomingPort: 4180
 linkerdNamespace: linkerd
 probePath: /health
 probePeriodSeconds: 3
 probePort: 4181
-serviceAccountName: linkerd-service-mirror
-serviceAccountNamespace: linkerd-service-mirror
+serviceAccountName: linkerd-service-mirror-remote-access
 linkerdVersion: linkerdVersionValue
 createdByAnnotation: linkerd.io/created-by
 nginxImageVersion: 1.17
diff --git a/charts/linkerd2-service-mirror/templates/service-mirror.yaml b/charts/linkerd2-service-mirror/templates/service-mirror.yaml
index a4d41ce52..638465ae9 100644
--- a/charts/linkerd2-service-mirror/templates/service-mirror.yaml
+++ b/charts/linkerd2-service-mirror/templates/service-mirror.yaml
@@ -1,31 +1,66 @@
+---
+kind: Namespace
+apiVersion: v1
+metadata:
+  name: {{.Values.namespace}}
+---
 kind: ClusterRole
 apiVersion: rbac.authorization.k8s.io/v1
 metadata:
-  name: linkerd-service-mirror
-  namespace: {{.Values.namespace}}
+  name: linkerd-service-mirror-access-local-resources
   labels:
     {{.Values.controllerComponentLabel}}: linkerd-service-mirror
 rules:
 - apiGroups: [""]
-  resources: ["endpoints", "services", "secrets", "namespaces"]
+  resources: ["endpoints", "services"]
   verbs: ["list", "get", "watch", "create", "delete", "update"]
+- apiGroups: [""]
+  resources: ["namespaces"]
+  verbs: ["create","list", "get", "watch"]
 ---
 kind: ClusterRoleBinding
 apiVersion: rbac.authorization.k8s.io/v1
 metadata:
-  name: linkerd-service-mirror
-  namespace: {{.Values.namespace}}
+  name: linkerd-service-mirror-access-local-resources
   labels:
     {{.Values.controllerComponentLabel}}: linkerd-service-mirror
 roleRef:
   apiGroup: rbac.authorization.k8s.io
   kind: ClusterRole
-  name: linkerd-service-mirror
+  name: linkerd-service-mirror-access-local-resources
 subjects:
 - kind: ServiceAccount
   name: linkerd-service-mirror
   namespace: {{.Values.namespace}}
 ---
+kind: Role
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: linkerd-service-mirror-read-remote-creds
+  namespace: {{.Values.namespace}}
+  labels:
+      {{.Values.controllerComponentLabel}}: linkerd-service-mirror
+rules:
+  - apiGroups: [""]
+    resources: ["secrets"]
+    verbs: ["list", "get", "watch"]
+---
+kind: RoleBinding
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: linkerd-service-mirror-read-remote-creds
+  namespace: {{.Values.namespace}}
+  labels:
+      {{.Values.controllerComponentLabel}}: linkerd-service-mirror
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: Role
+  name: linkerd-service-mirror-read-remote-creds
+subjects:
+  - kind: ServiceAccount
+    name: linkerd-service-mirror
+    namespace: {{.Values.namespace}}
+---
 kind: ServiceAccount
 apiVersion: v1
 metadata:
@@ -58,6 +93,7 @@ spec:
         - service-mirror
         - -log-level={{.Values.logLevel}}
         - -event-requeue-limit={{.Values.eventRequeueLimit}}
+        - -namespace={{.Values.namespace}}
         image: {{.Values.controllerImage}}:{{.Values.controllerImageVersion}}
         name: service-mirror
         securityContext:
diff --git a/charts/linkerd2-service-mirror/values.yaml b/charts/linkerd2-service-mirror/values.yaml
index 7877e927d..a23433e5c 100644
--- a/charts/linkerd2-service-mirror/values.yaml
+++ b/charts/linkerd2-service-mirror/values.yaml
@@ -1,4 +1,4 @@
-namespace: default
+namespace: linkerd-multicluster
 serviceMirrorUID: 2103
 logLevel: info
 eventRequeueLimit: 3
diff --git a/charts/linkerd2/README.md b/charts/linkerd2/README.md
index 1a6a4ba3b..8142a9d84 100644
--- a/charts/linkerd2/README.md
+++ b/charts/linkerd2/README.md
@@ -172,7 +172,8 @@ The following table lists the configurable parameters for the Grafana Add-On.
 |:--------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------|
 | `grafana.enabled`                     | Flag to enable grafana instance to be installed                                                                                                                                                | `true`
 | `grafana.name`                | Name of the grafana instance Service                                                                                                                                                 | `linkerd-grafana`                             |
-| `grafana.image`                | Docker image for the grafana instance                                                                                                                                                 | `gcr.io/linkerd-io/grafana`                             |
+| `grafana.image.name`                | Docker image name for the grafana instance                                                                                                                                                 | `gcr.io/linkerd-io/grafana`                             |
+| `grafana.image.version`                | Tag for the grafana container Docker image                                                                                                                                                 | latest version                             |
 | `grafana.resources.cpu.limit`       | Maximum amount of CPU units that the grafana container can use                                                                                                                     ||
 | `grafana.resources.cpu.request`     | Amount of CPU units that the gafana container requests                                                                                                                            ||
 | `grafana.resources.memory.limit`    | Maximum amount of memory that grafana container can use                                                                                                                        ||
diff --git a/charts/linkerd2/templates/controller.yaml b/charts/linkerd2/templates/controller.yaml
index 0cbdefe70..8454b01c0 100644
--- a/charts/linkerd2/templates/controller.yaml
+++ b/charts/linkerd2/templates/controller.yaml
@@ -33,7 +33,7 @@ metadata:
   labels:
     app.kubernetes.io/name: controller
     app.kubernetes.io/part-of: Linkerd
-    app.kubernetes.io/version: {{default .Values.global.linkerdVersion .Values.controllerImageVersion}}
+    app.kubernetes.io/version: {{default .Values.global.linkerdVersion .Values.global.controllerImageVersion}}
     {{.Values.global.controllerComponentLabel}}: controller
     {{.Values.global.controllerNamespaceLabel}}: {{.Values.global.namespace}}
   name: linkerd-controller
@@ -74,7 +74,7 @@ spec:
         - -controller-namespace={{.Values.global.namespace}}
         - -log-level={{.Values.controllerLogLevel}}
         {{- include "partials.linkerd.trace" . | nindent 8 -}}
-        image: {{.Values.controllerImage}}:{{default .Values.global.linkerdVersion .Values.controllerImageVersion}}
+        image: {{.Values.controllerImage}}:{{default .Values.global.linkerdVersion .Values.global.controllerImageVersion}}
         imagePullPolicy: {{.Values.global.imagePullPolicy}}
         livenessProbe:
           httpGet:
diff --git a/charts/linkerd2/templates/destination.yaml b/charts/linkerd2/templates/destination.yaml
index 49d9e0944..82e97df5b 100644
--- a/charts/linkerd2/templates/destination.yaml
+++ b/charts/linkerd2/templates/destination.yaml
@@ -33,7 +33,7 @@ metadata:
   labels:
     app.kubernetes.io/name: destination
     app.kubernetes.io/part-of: Linkerd
-    app.kubernetes.io/version: {{default .Values.global.linkerdVersion .Values.controllerImageVersion}}
+    app.kubernetes.io/version: {{default .Values.global.linkerdVersion .Values.global.controllerImageVersion}}
     {{.Values.global.controllerComponentLabel}}: destination
     {{.Values.global.controllerNamespaceLabel}}: {{.Values.global.namespace}}
   name: linkerd-destination
@@ -74,7 +74,7 @@ spec:
         - -enable-h2-upgrade={{.Values.enableH2Upgrade}}
         - -log-level={{.Values.controllerLogLevel}}
         {{- include "partials.linkerd.trace" . | nindent 8 -}}
-        image: {{.Values.controllerImage}}:{{default .Values.global.linkerdVersion .Values.controllerImageVersion}}
+        image: {{.Values.controllerImage}}:{{default .Values.global.linkerdVersion .Values.global.controllerImageVersion}}
         imagePullPolicy: {{.Values.global.imagePullPolicy}}
         livenessProbe:
           httpGet:
diff --git a/charts/linkerd2/templates/heartbeat.yaml b/charts/linkerd2/templates/heartbeat.yaml
index e65400093..66d8906ef 100644
--- a/charts/linkerd2/templates/heartbeat.yaml
+++ b/charts/linkerd2/templates/heartbeat.yaml
@@ -12,7 +12,7 @@ metadata:
   labels:
     app.kubernetes.io/name: heartbeat
     app.kubernetes.io/part-of: Linkerd
-    app.kubernetes.io/version: {{default .Values.global.linkerdVersion .Values.controllerImageVersion}}
+    app.kubernetes.io/version: {{default .Values.global.linkerdVersion .Values.global.controllerImageVersion}}
     {{.Values.global.controllerComponentLabel}}: heartbeat
     {{.Values.global.controllerNamespaceLabel}}: {{.Values.global.namespace}}
   annotations:
@@ -35,7 +35,7 @@ spec:
           restartPolicy: Never
           containers:
           - name: heartbeat
-            image: {{.Values.controllerImage}}:{{default .Values.global.linkerdVersion .Values.controllerImageVersion}}
+            image: {{.Values.controllerImage}}:{{default .Values.global.linkerdVersion .Values.global.controllerImageVersion}}
             imagePullPolicy: {{.Values.global.imagePullPolicy}}
             args:
             - "heartbeat"
diff --git a/charts/linkerd2/templates/identity.yaml b/charts/linkerd2/templates/identity.yaml
index 4a9bcff3e..169c020d8 100644
--- a/charts/linkerd2/templates/identity.yaml
+++ b/charts/linkerd2/templates/identity.yaml
@@ -53,7 +53,7 @@ metadata:
   labels:
     app.kubernetes.io/name: identity
     app.kubernetes.io/part-of: Linkerd
-    app.kubernetes.io/version: {{default .Values.global.linkerdVersion .Values.controllerImageVersion}}
+    app.kubernetes.io/version: {{default .Values.global.linkerdVersion .Values.global.controllerImageVersion}}
     {{.Values.global.controllerComponentLabel}}: identity
     {{.Values.global.controllerNamespaceLabel}}: {{.Values.global.namespace}}
   name: linkerd-identity
@@ -91,7 +91,7 @@ spec:
         - identity
         - -log-level={{.Values.controllerLogLevel}}
         {{- include "partials.linkerd.trace" . | nindent 8 -}}
-        image: {{.Values.controllerImage}}:{{default .Values.global.linkerdVersion .Values.controllerImageVersion}}
+        image: {{.Values.controllerImage}}:{{default .Values.global.linkerdVersion .Values.global.controllerImageVersion}}
         imagePullPolicy: {{.Values.global.imagePullPolicy}}
         livenessProbe:
           httpGet:
diff --git a/charts/linkerd2/templates/prometheus.yaml b/charts/linkerd2/templates/prometheus.yaml
index b5b22db23..c1142d81c 100644
--- a/charts/linkerd2/templates/prometheus.yaml
+++ b/charts/linkerd2/templates/prometheus.yaml
@@ -187,7 +187,7 @@ metadata:
   labels:
     app.kubernetes.io/name: prometheus
     app.kubernetes.io/part-of: Linkerd
-    app.kubernetes.io/version: {{default .Values.global.linkerdVersion .Values.controllerImageVersion}}
+    app.kubernetes.io/version: {{default .Values.global.linkerdVersion .Values.global.controllerImageVersion}}
     {{.Values.global.controllerComponentLabel}}: prometheus
     {{.Values.global.controllerNamespaceLabel}}: {{.Values.global.namespace}}
   name: linkerd-prometheus
diff --git a/charts/linkerd2/templates/proxy-injector.yaml b/charts/linkerd2/templates/proxy-injector.yaml
index f12e7e87b..0f5e9e0f9 100644
--- a/charts/linkerd2/templates/proxy-injector.yaml
+++ b/charts/linkerd2/templates/proxy-injector.yaml
@@ -14,7 +14,7 @@ metadata:
   labels:
     app.kubernetes.io/name: proxy-injector
     app.kubernetes.io/part-of: Linkerd
-    app.kubernetes.io/version: {{default .Values.global.linkerdVersion .Values.controllerImageVersion}}
+    app.kubernetes.io/version: {{default .Values.global.linkerdVersion .Values.global.controllerImageVersion}}
     {{.Values.global.controllerComponentLabel}}: proxy-injector
     {{.Values.global.controllerNamespaceLabel}}: {{.Values.global.namespace}}
   name: linkerd-proxy-injector
@@ -52,7 +52,7 @@ spec:
       - args:
         - proxy-injector
         - -log-level={{.Values.controllerLogLevel}}
-        image: {{.Values.controllerImage}}:{{default .Values.global.linkerdVersion .Values.controllerImageVersion}}
+        image: {{.Values.controllerImage}}:{{default .Values.global.linkerdVersion .Values.global.controllerImageVersion}}
         imagePullPolicy: {{.Values.global.imagePullPolicy}}
         livenessProbe:
           httpGet:
diff --git a/charts/linkerd2/templates/smi-metrics.yaml b/charts/linkerd2/templates/smi-metrics.yaml
index 35048fda3..8fa7e2166 100644
--- a/charts/linkerd2/templates/smi-metrics.yaml
+++ b/charts/linkerd2/templates/smi-metrics.yaml
@@ -6,7 +6,7 @@ metadata:
   labels:
     app.kubernetes.io/name: smi-metrics
     app.kubernetes.io/part-of: Linkerd
-    app.kubernetes.io/version: {{default .Values.global.linkerdVersion .Values.controllerImageVersion}}
+    app.kubernetes.io/version: {{default .Values.global.linkerdVersion .Values.global.controllerImageVersion}}
     {{.Values.global.controllerComponentLabel}}: smi-metrics
     {{.Values.global.controllerNamespaceLabel}}: {{.Values.global.namespace}}
   name: linkerd-smi-metrics
@@ -24,7 +24,7 @@ metadata:
   labels:
     app.kubernetes.io/name: smi-metrics
     app.kubernetes.io/part-of: Linkerd
-    app.kubernetes.io/version: {{default .Values.global.linkerdVersion .Values.controllerImageVersion}}
+    app.kubernetes.io/version: {{default .Values.global.linkerdVersion .Values.global.controllerImageVersion}}
     {{.Values.global.controllerComponentLabel}}: smi-metrics
     {{.Values.global.controllerNamespaceLabel}}: {{.Values.global.namespace}}
 spec:
diff --git a/charts/linkerd2/templates/sp-validator.yaml b/charts/linkerd2/templates/sp-validator.yaml
index b6cf8a854..b19e6c243 100644
--- a/charts/linkerd2/templates/sp-validator.yaml
+++ b/charts/linkerd2/templates/sp-validator.yaml
@@ -33,7 +33,7 @@ metadata:
   labels:
     app.kubernetes.io/name: sp-validator
     app.kubernetes.io/part-of: Linkerd
-    app.kubernetes.io/version: {{default .Values.global.linkerdVersion .Values.controllerImageVersion}}
+    app.kubernetes.io/version: {{default .Values.global.linkerdVersion .Values.global.controllerImageVersion}}
     {{.Values.global.controllerComponentLabel}}: sp-validator
     {{.Values.global.controllerNamespaceLabel}}: {{.Values.global.namespace}}
   name: linkerd-sp-validator
@@ -71,7 +71,7 @@ spec:
       - args:
         - sp-validator
         - -log-level={{.Values.controllerLogLevel}}
-        image: {{.Values.controllerImage}}:{{default .Values.global.linkerdVersion .Values.controllerImageVersion}}
+        image: {{.Values.controllerImage}}:{{default .Values.global.linkerdVersion .Values.global.controllerImageVersion}}
         imagePullPolicy: {{.Values.global.imagePullPolicy}}
         livenessProbe:
           httpGet:
diff --git a/charts/linkerd2/templates/tap.yaml b/charts/linkerd2/templates/tap.yaml
index 8dc4ebf09..fc5c1d957 100644
--- a/charts/linkerd2/templates/tap.yaml
+++ b/charts/linkerd2/templates/tap.yaml
@@ -36,7 +36,7 @@ metadata:
   labels:
     app.kubernetes.io/name: tap
     app.kubernetes.io/part-of: Linkerd
-    app.kubernetes.io/version: {{default .Values.global.linkerdVersion .Values.controllerImageVersion}}
+    app.kubernetes.io/version: {{default .Values.global.linkerdVersion .Values.global.controllerImageVersion}}
     {{.Values.global.controllerComponentLabel}}: tap
     {{.Values.global.controllerNamespaceLabel}}: {{.Values.global.namespace}}
   name: linkerd-tap
@@ -78,7 +78,7 @@ spec:
         - -controller-namespace={{.Values.global.namespace}}
         - -log-level={{.Values.controllerLogLevel}}
         {{- include "partials.linkerd.trace" . | nindent 8 -}}
-        image: {{.Values.controllerImage}}:{{default .Values.global.linkerdVersion .Values.controllerImageVersion}}
+        image: {{.Values.controllerImage}}:{{default .Values.global.linkerdVersion .Values.global.controllerImageVersion}}
         imagePullPolicy: {{.Values.global.imagePullPolicy}}
         livenessProbe:
           httpGet:
diff --git a/charts/linkerd2/templates/web.yaml b/charts/linkerd2/templates/web.yaml
index f03367fca..8221695c3 100644
--- a/charts/linkerd2/templates/web.yaml
+++ b/charts/linkerd2/templates/web.yaml
@@ -36,7 +36,7 @@ metadata:
   labels:
     app.kubernetes.io/name: web
     app.kubernetes.io/part-of: Linkerd
-    app.kubernetes.io/version: {{default .Values.global.linkerdVersion .Values.controllerImageVersion}}
+    app.kubernetes.io/version: {{default .Values.global.linkerdVersion .Values.global.controllerImageVersion}}
     {{.Values.global.controllerComponentLabel}}: web
     {{.Values.global.controllerNamespaceLabel}}: {{.Values.global.namespace}}
   name: linkerd-web
@@ -81,7 +81,7 @@ spec:
         - -enforced-host=^(localhost|127\.0\.0\.1|{{ $hostFull }}|{{ $hostAbbrev }}|\[::1\])(:\d+)?$
         {{- end}}
         {{- include "partials.linkerd.trace" . | nindent 8 -}}
-        image: {{.Values.webImage}}:{{default .Values.global.linkerdVersion .Values.controllerImageVersion}}
+        image: {{.Values.webImage}}:{{default .Values.global.linkerdVersion .Values.global.controllerImageVersion}}
         imagePullPolicy: {{.Values.global.imagePullPolicy}}
         livenessProbe:
           httpGet:
diff --git a/charts/linkerd2/values.yaml b/charts/linkerd2/values.yaml
index d66b1abba..2d13bcf59 100644
--- a/charts/linkerd2/values.yaml
+++ b/charts/linkerd2/values.yaml
@@ -198,7 +198,9 @@ smiMetrics:
 grafana:
   enabled: true
   name: linkerd-grafana
-  image: gcr.io/linkerd-io/grafana
+  image:
+    name: gcr.io/linkerd-io/grafana
+    version: *linkerd_version
   # resources:
 
 tracing:
diff --git a/cli/cmd/cluster.go b/cli/cmd/cluster.go
index f742ead6e..648dc7b0f 100644
--- a/cli/cmd/cluster.go
+++ b/cli/cmd/cluster.go
@@ -33,15 +33,16 @@ import (
 )
 
 const (
+	defaultMulticlusterNamespace                 = "linkerd-multicluster"
 	helmMulticlusterRemoteSetuprDefaultChartName = "linkerd2-multicluster-remote-setup"
 	tokenKey                                     = "token"
-	defaultServiceAccountName                    = "linkerd-service-mirror"
-	defaultServiceAccountNs                      = "linkerd-service-mirror"
+	defaultServiceAccountName                    = "linkerd-service-mirror-remote-access"
 	defaultClusterName                           = "remote"
 )
 
 type (
 	getCredentialsOptions struct {
+		namespace               string
 		serviceAccountName      string
 		serviceAccountNamespace string
 		clusterName             string
@@ -49,16 +50,15 @@ type (
 	}
 
 	setupRemoteClusterOptions struct {
-		serviceAccountName      string
-		serviceAccountNamespace string
-		gatewayNamespace        string
-		gatewayName             string
-		probePort               uint32
-		incomingPort            uint32
-		probePeriodSeconds      uint32
-		probePath               string
-		nginxImageVersion       string
-		nginxImage              string
+		namespace          string
+		serviceAccountName string
+		gatewayName        string
+		probePort          uint32
+		incomingPort       uint32
+		probePeriodSeconds uint32
+		probePath          string
+		nginxImageVersion  string
+		nginxImage         string
 	}
 
 	exportServiceOptions struct {
@@ -80,16 +80,15 @@ func newSetupRemoteClusterOptionsWithDefault() (*setupRemoteClusterOptions, erro
 	}
 
 	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,
+		serviceAccountName: defaults.ServiceAccountName,
+		namespace:          defaults.Namespace,
+		gatewayName:        defaults.GatewayName,
+		probePort:          defaults.ProbePort,
+		incomingPort:       defaults.IncomingPort,
+		probePeriodSeconds: defaults.ProbePeriodSeconds,
+		probePath:          defaults.ProbePath,
+		nginxImageVersion:  defaults.NginxImageVersion,
+		nginxImage:         defaults.NginxImage,
 	}, nil
 
 }
@@ -128,7 +127,7 @@ func buildMulticlusterSetupValues(opts *setupRemoteClusterOptions) (*multicluste
 	}
 
 	defaults.GatewayName = opts.gatewayName
-	defaults.GatewayNamespace = opts.gatewayNamespace
+	defaults.Namespace = opts.namespace
 	defaults.IdentityTrustDomain = global.Global.IdentityContext.TrustDomain
 	defaults.IncomingPort = opts.incomingPort
 	defaults.LinkerdNamespace = controlPlaneNamespace
@@ -137,7 +136,6 @@ func buildMulticlusterSetupValues(opts *setupRemoteClusterOptions) (*multicluste
 	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
@@ -171,8 +169,8 @@ func newGatewaysCommand() *cobra.Command {
 		},
 	}
 
-	cmd.Flags().StringVar(&opts.clusterName, "cluster-name", "remote", "the name of the remote cluster")
-	cmd.Flags().StringVar(&opts.gatewayNamespace, "gateway-namespace", "linkerd-gateway", "the namespace in which the gateway resides on the remote cluster")
+	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.")
 
 	return cmd
@@ -194,6 +192,10 @@ func newSetupRemoteCommand() *cobra.Command {
 
 			values, err := buildMulticlusterSetupValues(options)
 
+			if options.namespace == controlPlaneNamespace {
+				return errors.New("you need to specify a namespace different than the one in which Linkerd is installed")
+			}
+
 			if err != nil {
 				return err
 			}
@@ -206,6 +208,7 @@ func newSetupRemoteCommand() *cobra.Command {
 
 			files := []*chartutil.BufferedFile{
 				{Name: chartutil.ChartfileName},
+				{Name: "templates/namespace.yaml"},
 				{Name: "templates/gateway.yaml"},
 				{Name: "templates/service-mirror-rbac.yaml"},
 			}
@@ -229,13 +232,12 @@ func newSetupRemoteCommand() *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().StringVar(&options.namespace, "namespace", options.namespace, "the namespace in which the gateway and service account 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")
 
@@ -333,7 +335,8 @@ func newGetCredentialsCommand() *cobra.Command {
 				Type:     k8s.MirrorSecretType,
 				TypeMeta: metav1.TypeMeta{Kind: "Secret", APIVersion: "v1"},
 				ObjectMeta: metav1.ObjectMeta{
-					Name: fmt.Sprintf("cluster-credentials-%s", opts.clusterName),
+					Name:      fmt.Sprintf("cluster-credentials-%s", opts.clusterName),
+					Namespace: opts.namespace,
 					Annotations: map[string]string{
 						k8s.RemoteClusterNameLabel:                  opts.clusterName,
 						k8s.RemoteClusterDomainAnnotation:           opts.remoteClusterDomain,
@@ -355,8 +358,9 @@ func newGetCredentialsCommand() *cobra.Command {
 		},
 	}
 
+	cmd.Flags().StringVar(&opts.namespace, "namespace", defaultMulticlusterNamespace, "the namespace in which the secret will be created")
 	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.serviceAccountNamespace, "service-account-namespace", defaultMulticlusterNamespace, "the namespace in which the svc account resides on the remote cluster")
 	cmd.Flags().StringVar(&opts.clusterName, "cluster-name", defaultClusterName, "cluster name")
 	cmd.Flags().StringVar(&opts.remoteClusterDomain, "remote-cluster-domain", defaultClusterDomain, "custom remote cluster domain")
 
@@ -575,7 +579,7 @@ func newExportServiceCommand() *cobra.Command {
 	}
 
 	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")
+	cmd.Flags().StringVar(&opts.gatewayNamespace, "gateway-namespace", defaultMulticlusterNamespace, "the namespace of the gateway")
 
 	return cmd
 }
diff --git a/cli/cmd/install-service-mirror.go b/cli/cmd/install-service-mirror.go
index a2eb371d2..9973393d5 100644
--- a/cli/cmd/install-service-mirror.go
+++ b/cli/cmd/install-service-mirror.go
@@ -11,6 +11,7 @@ import (
 	"github.com/linkerd/linkerd2/pkg/version"
 	log "github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
+	kerrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/helm/pkg/chartutil"
 	"sigs.k8s.io/yaml"
 )
@@ -84,6 +85,15 @@ func (options *installServiceMirrorOptions) buildValues() (*servicemirror.Values
 }
 
 func (options *installServiceMirrorOptions) validate() error {
+
+	_, err := getLinkerdConfigMap()
+	if err != nil {
+		if kerrors.IsNotFound(err) {
+			return errors.New("you need Linkerd to be installed in order to install the service mirroring component")
+		}
+		return err
+	}
+
 	if !alphaNumDashDot.MatchString(options.controlPlaneVersion) {
 		return fmt.Errorf("%s is not a valid version", options.controlPlaneVersion)
 	}
@@ -92,6 +102,10 @@ func (options *installServiceMirrorOptions) validate() error {
 		return errors.New("you need to specify a namespace")
 	}
 
+	if options.namespace == controlPlaneNamespace {
+		return errors.New("you need to install the service mirror component in a namespace different than the Linkerd one")
+	}
+
 	if _, err := log.ParseLevel(options.logLevel); err != nil {
 		return fmt.Errorf("--log-level must be one of: panic, fatal, error, warn, info, debug")
 	}
diff --git a/cli/cmd/install.go b/cli/cmd/install.go
index 93b720580..914be41b0 100644
--- a/cli/cmd/install.go
+++ b/cli/cmd/install.go
@@ -754,7 +754,7 @@ func (options *installOptions) buildValuesWithoutIdentity(configs *pb.All) (*l5d
 	installValues.Configs.Proxy = proxyJSON
 	installValues.Configs.Install = installJSON
 	installValues.ControllerImage = fmt.Sprintf("%s/controller", options.dockerRegistry)
-	installValues.ControllerImageVersion = configs.GetGlobal().GetVersion()
+	installValues.Global.ControllerImageVersion = configs.GetGlobal().GetVersion()
 	installValues.ControllerLogLevel = options.controllerLogLevel
 	installValues.ControllerReplicas = options.controllerReplicas
 	installValues.ControllerUID = options.controllerUID
@@ -763,7 +763,8 @@ func (options *installOptions) buildValuesWithoutIdentity(configs *pb.All) (*l5d
 	installValues.EnablePodAntiAffinity = options.highAvailability
 	installValues.Global.HighAvailability = options.highAvailability
 	installValues.Global.ImagePullPolicy = options.imagePullPolicy
-	installValues.Grafana["image"] = fmt.Sprintf("%s/grafana", options.dockerRegistry)
+	installValues.Grafana["image"].(map[string]interface{})["name"] = fmt.Sprintf("%s/grafana", options.dockerRegistry)
+	installValues.Grafana["image"].(map[string]interface{})["version"] = options.controlPlaneVersion
 	if options.prometheusImage != "" {
 		installValues.PrometheusImage = options.prometheusImage
 	}
diff --git a/cli/cmd/stat.go b/cli/cmd/stat.go
index b2d22b1ca..cc2d21e37 100644
--- a/cli/cmd/stat.go
+++ b/cli/cmd/stat.go
@@ -284,7 +284,15 @@ func writeStatsToBuffer(rows []*pb.StatTable_PodGroup_Row, w *tabwriter.Writer,
 	}
 
 	for _, r := range rows {
-		if isPodOwnerResource(r.Resource.Type) && !options.unmeshed && r.GetMeshedPodCount() == 0 {
+
+		// Skip unmeshed pods if the unmeshed option isn't enabled.
+		if !options.unmeshed && r.GetMeshedPodCount() == 0 &&
+			// Skip only if the resource can own pods
+			isPodOwnerResource(r.Resource.Type) &&
+			// Skip only if --from isn't specified (unmeshed resources can show
+			// stats in --from mode because metrics are collected on the client
+			// side).
+			options.fromResource == "" {
 			continue
 		}
 
diff --git a/cni-plugin/deployment/scripts/install-cni.sh b/cni-plugin/deployment/scripts/install-cni.sh
index 9083ac510..a31e608f8 100755
--- a/cni-plugin/deployment/scripts/install-cni.sh
+++ b/cni-plugin/deployment/scripts/install-cni.sh
@@ -65,7 +65,7 @@ cleanup() {
 
   if [ -e "${CNI_CONF_PATH}" ]; then
     echo "Removing linkerd-cni config: ${CNI_CONF_PATH}"
-    CNI_CONF_DATA=$(cat "${CNI_CONF_PATH}" | jq 'del( .plugins[]? | select( .type == "linkerd-cni" ))')
+    CNI_CONF_DATA=$(jq 'del( .plugins[]? | select( .type == "linkerd-cni" ))' "${CNI_CONF_PATH}")
     echo "${CNI_CONF_DATA}" > "${CNI_CONF_PATH}"
 
     if [ "${CNI_CONF_PATH}" = "${CONTAINER_MOUNT_PREFIX}${DEST_CNI_NET_DIR}/01-linkerd-cni.conf" ]; then
@@ -133,7 +133,7 @@ if [ -f "${SERVICE_ACCOUNT_PATH}/token" ]; then
   if [ "${SKIP_TLS_VERIFY}" = "true" ]; then
     TLS_CFG='insecure-skip-tls-verify: true'
   elif [ -f "${KUBE_CA_FILE}" ]; then
-    TLS_CFG="certificate-authority-data: $(cat "${KUBE_CA_FILE}" | base64 | tr -d '\n')"
+    TLS_CFG="certificate-authority-data: $(base64 "${KUBE_CA_FILE}" | tr -d '\n')"
   fi
 
   # Write a kubeconfig file for the CNI plugin. Do this
@@ -187,7 +187,7 @@ CNI_CONF_FILE="${CNI_CONF_PATH}"
 if [ -e "${CNI_CONF_FILE}" ]; then
   # Add the linkerd-cni plugin to the existing list
   CNI_TMP_CONF_DATA=$(cat "${TMP_CONF}")
-  CNI_CONF_DATA=$(cat "${CNI_CONF_FILE}" | jq --argjson CNI_TMP_CONF_DATA "$CNI_TMP_CONF_DATA" -f /linkerd/filter.jq)
+  CNI_CONF_DATA=$(jq --argjson CNI_TMP_CONF_DATA "$CNI_TMP_CONF_DATA" -f /linkerd/filter.jq "${CNI_CONF_FILE}")
   echo "${CNI_CONF_DATA}" > ${TMP_CONF}
 fi
 
diff --git a/controller/cmd/service-mirror/cluster_watcher.go b/controller/cmd/service-mirror/cluster_watcher.go
index 460f55cf5..9e5b0e27a 100644
--- a/controller/cmd/service-mirror/cluster_watcher.go
+++ b/controller/cmd/service-mirror/cluster_watcher.go
@@ -27,15 +27,16 @@ type (
 	// it can be requeued up to N times, to ensure that the failure is not due to some temporary network
 	// problems or general glitch in the Matrix.
 	RemoteClusterServiceWatcher struct {
-		clusterName     string
-		clusterDomain   string
-		remoteAPIClient *k8s.API
-		localAPIClient  *k8s.API
-		stopper         chan struct{}
-		log             *logging.Entry
-		eventsQueue     workqueue.RateLimitingInterface
-		requeueLimit    int
-		probeEventsSink ProbeEventSink
+		serviceMirrorNamespace string
+		clusterName            string
+		clusterDomain          string
+		remoteAPIClient        *k8s.API
+		localAPIClient         *k8s.API
+		stopper                chan struct{}
+		log                    *logging.Entry
+		eventsQueue            workqueue.RateLimitingInterface
+		requeueLimit           int
+		probeEventsSink        ProbeEventSink
 	}
 
 	// ProbeConfig describes the configured probe on particular gateway (if presents)
@@ -174,6 +175,7 @@ func (rcsw *RemoteClusterServiceWatcher) resolveGateway(metadata *gatewayMetadat
 
 // NewRemoteClusterServiceWatcher constructs a new cluster watcher
 func NewRemoteClusterServiceWatcher(
+	serviceMirrorNamespace string,
 	localAPI *k8s.API,
 	cfg *rest.Config,
 	clusterName string,
@@ -187,11 +189,12 @@ func NewRemoteClusterServiceWatcher(
 	}
 	stopper := make(chan struct{})
 	return &RemoteClusterServiceWatcher{
-		clusterName:     clusterName,
-		clusterDomain:   clusterDomain,
-		remoteAPIClient: remoteAPI,
-		localAPIClient:  localAPI,
-		stopper:         stopper,
+		serviceMirrorNamespace: serviceMirrorNamespace,
+		clusterName:            clusterName,
+		clusterDomain:          clusterDomain,
+		remoteAPIClient:        remoteAPI,
+		localAPIClient:         localAPI,
+		stopper:                stopper,
 		log: logging.WithFields(logging.Fields{
 			"cluster":    clusterName,
 			"apiAddress": cfg.Host,
@@ -497,6 +500,137 @@ func remapRemoteServicePorts(ports []corev1.ServicePort) []corev1.ServicePort {
 	return newPorts
 }
 
+func (rcsw *RemoteClusterServiceWatcher) updateGatewayMirrorService(spec *GatewaySpec) error {
+	localServiceName := rcsw.mirroredResourceName(spec.gatewayName)
+	service, err := rcsw.localAPIClient.Svc().Lister().Services(rcsw.serviceMirrorNamespace).Get(localServiceName)
+	if err != nil {
+		return err
+	}
+
+	if service.Annotations != nil && service.Annotations[consts.RemoteGatewayResourceVersionAnnotation] != spec.resourceVersion {
+		updatedService := service.DeepCopy()
+		if updatedService.Annotations != nil {
+			updatedService.Annotations[consts.RemoteGatewayResourceVersionAnnotation] = spec.resourceVersion
+		}
+
+		endpoints, err := rcsw.localAPIClient.Endpoint().Lister().Endpoints(rcsw.serviceMirrorNamespace).Get(localServiceName)
+		if err != nil {
+			return err
+		}
+
+		updatedEndpoints := endpoints.DeepCopy()
+		updatedEndpoints.Subsets = []corev1.EndpointSubset{
+			{
+				Addresses: spec.addresses,
+				Ports: []corev1.EndpointPort{
+					{
+						Protocol: "TCP",
+						Port:     int32(spec.ProbeConfig.port),
+					},
+				},
+			},
+		}
+
+		_, err = rcsw.localAPIClient.Client.CoreV1().Services(rcsw.serviceMirrorNamespace).Update(updatedService)
+		if err != nil {
+			return err
+		}
+
+		_, err = rcsw.localAPIClient.Client.CoreV1().Endpoints(rcsw.serviceMirrorNamespace).Update(updatedEndpoints)
+		if err != nil {
+			rcsw.localAPIClient.Client.CoreV1().Services(rcsw.serviceMirrorNamespace).Delete(updatedService.Name, &metav1.DeleteOptions{})
+			return err
+		}
+		rcsw.log.Debugf("%s gateway mirror updated", localServiceName)
+	}
+
+	return nil
+}
+
+// the logic here creates a mirror service for the gateway. The only port exposed there is the
+// probes port. This enables us to discover the gateways probe endpoints through the dst service
+// and apply proper identity
+func (rcsw *RemoteClusterServiceWatcher) createGatewayMirrorService(spec *GatewaySpec) error {
+	localServiceName := rcsw.mirroredResourceName(spec.gatewayName)
+	if spec.ProbeConfig == nil {
+		rcsw.log.Debugf("Skipping creation of gateway mirror as gateway does not specify probe config")
+		return nil
+	}
+
+	_, err := rcsw.localAPIClient.Svc().Lister().Services(rcsw.serviceMirrorNamespace).Get(localServiceName)
+	if err != nil {
+		if kerrors.IsNotFound(err) {
+			serviceToCreate := &corev1.Service{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      localServiceName,
+					Namespace: rcsw.serviceMirrorNamespace,
+					Annotations: map[string]string{
+						consts.RemoteGatewayResourceVersionAnnotation: spec.resourceVersion,
+					},
+					Labels: map[string]string{
+						consts.MirroredResourceLabel:  "true",
+						consts.RemoteClusterNameLabel: rcsw.clusterName,
+					},
+				},
+				Spec: corev1.ServiceSpec{
+					Ports: []corev1.ServicePort{
+						{
+							Protocol: "TCP",
+							Port:     int32(spec.ProbeConfig.port),
+						},
+					},
+				},
+			}
+
+			endpointsToCreate := &corev1.Endpoints{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      localServiceName,
+					Namespace: rcsw.serviceMirrorNamespace,
+					Labels: map[string]string{
+						consts.MirroredResourceLabel:  "true",
+						consts.RemoteClusterNameLabel: rcsw.clusterName,
+					},
+					Annotations: map[string]string{
+						consts.RemoteGatewayIdentity: spec.identity,
+					},
+				},
+				Subsets: []corev1.EndpointSubset{
+					{
+						Addresses: spec.addresses,
+						Ports: []corev1.EndpointPort{
+							{
+								Protocol: "TCP",
+								Port:     int32(spec.ProbeConfig.port),
+							},
+						},
+					},
+				},
+			}
+
+			rcsw.log.Debugf("Creating a new gateway mirror Service for %s", localServiceName)
+			if _, err := rcsw.localAPIClient.Client.CoreV1().Services(rcsw.serviceMirrorNamespace).Create(serviceToCreate); err != nil {
+				if !kerrors.IsAlreadyExists(err) {
+					// we might have created it during earlier attempt, if that is not the case, we retry
+					return RetryableError{[]error{err}}
+				}
+			}
+
+			rcsw.log.Debugf("Creating a new gateway mirror Endpoints for %s", localServiceName)
+			if _, err := rcsw.localAPIClient.Client.CoreV1().Endpoints(rcsw.serviceMirrorNamespace).Create(endpointsToCreate); err != nil {
+				// we clean up after ourselves
+				rcsw.localAPIClient.Client.CoreV1().Services(rcsw.serviceMirrorNamespace).Delete(spec.gatewayName, &metav1.DeleteOptions{})
+				// and retry
+				return RetryableError{[]error{err}}
+			}
+
+			return nil
+		}
+		return err
+	}
+	rcsw.log.Debugf("Skipping creation of gateway mirror as it already exists")
+	return nil
+}
+
 func (rcsw *RemoteClusterServiceWatcher) handleRemoteServiceCreated(ev *RemoteServiceCreated) error {
 	remoteService := ev.service.DeepCopy()
 	serviceInfo := fmt.Sprintf("%s/%s", remoteService.Namespace, remoteService.Name)
@@ -550,6 +684,10 @@ func (rcsw *RemoteClusterServiceWatcher) handleRemoteServiceCreated(ev *RemoteSe
 			endpointsToCreate.Annotations[consts.RemoteGatewayIdentity] = gatewaySpec.identity
 		}
 
+		if err := rcsw.createGatewayMirrorService(gatewaySpec); err != nil {
+			return err
+		}
+
 		rcsw.probeEventsSink.send(&MirroredServicePaired{
 			serviceName:      serviceToCreate.Name,
 			serviceNamespace: serviceToCreate.Namespace,
@@ -585,6 +723,11 @@ func (rcsw *RemoteClusterServiceWatcher) handleRemoteGatewayDeleted(ev *RemoteGa
 		gatewayNs:   ev.gatewayData.Namespace,
 		clusterName: rcsw.clusterName,
 	})
+
+	if err := rcsw.localAPIClient.Client.CoreV1().Services(rcsw.serviceMirrorNamespace).Delete(rcsw.mirroredResourceName(ev.gatewayData.Name), &metav1.DeleteOptions{}); err != nil {
+		rcsw.log.Errorf("Could not delete gateway mirror %s", err)
+	}
+
 	affectedEndpoints, err := rcsw.endpointsForGateway(&ev.gatewayData)
 	if err != nil {
 		// if we cannot find the endpoints, we can give up
@@ -619,7 +762,7 @@ func (rcsw *RemoteClusterServiceWatcher) handleRemoteGatewayUpdated(ev *RemoteGa
 	var errors []error
 	for _, svc := range ev.affectedServices {
 		updatedService := svc.DeepCopy()
-		if updatedService.Labels != nil {
+		if updatedService.Annotations != nil {
 			updatedService.Annotations[consts.RemoteGatewayResourceVersionAnnotation] = ev.gatewaySpec.resourceVersion
 		}
 		endpoints, err := rcsw.localAPIClient.Endpoint().Lister().Endpoints(svc.Namespace).Get(svc.Name)
@@ -663,6 +806,10 @@ func (rcsw *RemoteClusterServiceWatcher) handleRemoteGatewayUpdated(ev *RemoteGa
 		})
 	}
 
+	if err := rcsw.updateGatewayMirrorService(&ev.gatewaySpec); err != nil {
+		rcsw.log.Errorf("Could not update %s gateway proxy service: %s", ev.gatewaySpec.gatewayName, err)
+	}
+
 	rcsw.probeEventsSink.send(&GatewayUpdated{
 		GatewaySpec: ev.gatewaySpec,
 	})
@@ -945,7 +1092,7 @@ func (rcsw *RemoteClusterServiceWatcher) processEvents() {
 }
 
 // Start starts watching the remote cluster
-func (rcsw *RemoteClusterServiceWatcher) Start() {
+func (rcsw *RemoteClusterServiceWatcher) Start() error {
 	rcsw.remoteAPIClient.Sync(rcsw.stopper)
 	rcsw.eventsQueue.Add(&OprhanedServicesGcTriggered{})
 	rcsw.remoteAPIClient.Svc().Informer().AddEventHandler(
@@ -975,6 +1122,7 @@ func (rcsw *RemoteClusterServiceWatcher) Start() {
 		},
 	)
 	go rcsw.processEvents()
+	return nil
 }
 
 // Stop stops watching the cluster and cleans up all mirrored resources
diff --git a/controller/cmd/service-mirror/config_watcher.go b/controller/cmd/service-mirror/config_watcher.go
index 18573de21..46fc215e1 100644
--- a/controller/cmd/service-mirror/config_watcher.go
+++ b/controller/cmd/service-mirror/config_watcher.go
@@ -22,22 +22,24 @@ type ProbeEventSink interface {
 // RemoteClusterConfigWatcher watches for secrets of type MirrorSecretType
 // and upon the detection of such secret created starts a RemoteClusterServiceWatcher
 type RemoteClusterConfigWatcher struct {
-	k8sAPI          *k8s.API
-	clusterWatchers map[string]*RemoteClusterServiceWatcher
-	requeueLimit    int
+	serviceMirrorNamespace string
+	k8sAPI                 *k8s.API
+	clusterWatchers        map[string]*RemoteClusterServiceWatcher
+	requeueLimit           int
 	sync.RWMutex
 	probeEventsSink ProbeEventSink
 }
 
 // NewRemoteClusterConfigWatcher Creates a new config watcher
-func NewRemoteClusterConfigWatcher(k8sAPI *k8s.API, requeueLimit int, probeEventsSink ProbeEventSink) *RemoteClusterConfigWatcher {
+func NewRemoteClusterConfigWatcher(serviceMirrorNamespace string, secretsInformer cache.SharedIndexInformer, k8sAPI *k8s.API, requeueLimit int, probeEventsSink ProbeEventSink) *RemoteClusterConfigWatcher {
 	rcw := &RemoteClusterConfigWatcher{
-		k8sAPI:          k8sAPI,
-		clusterWatchers: map[string]*RemoteClusterServiceWatcher{},
-		requeueLimit:    requeueLimit,
-		probeEventsSink: probeEventsSink,
+		serviceMirrorNamespace: serviceMirrorNamespace,
+		k8sAPI:                 k8sAPI,
+		clusterWatchers:        map[string]*RemoteClusterServiceWatcher{},
+		requeueLimit:           requeueLimit,
+		probeEventsSink:        probeEventsSink,
 	}
-	k8sAPI.Secret().Informer().AddEventHandler(
+	secretsInformer.AddEventHandler(
 		cache.FilteringResourceEventHandler{
 			FilterFunc: func(obj interface{}) bool {
 				switch object := obj.(type) {
@@ -131,13 +133,15 @@ func (rcw *RemoteClusterConfigWatcher) registerRemoteCluster(secret *corev1.Secr
 		return fmt.Errorf("there is already a cluster with name %s being watcher. Please delete its config before attempting to register a new one", config.ClusterName)
 	}
 
-	watcher, err := NewRemoteClusterServiceWatcher(rcw.k8sAPI, clientConfig, config.ClusterName, rcw.requeueLimit, config.ClusterDomain, rcw.probeEventsSink)
+	watcher, err := NewRemoteClusterServiceWatcher(rcw.serviceMirrorNamespace, rcw.k8sAPI, clientConfig, config.ClusterName, rcw.requeueLimit, config.ClusterDomain, rcw.probeEventsSink)
 	if err != nil {
 		return err
 	}
 
 	rcw.clusterWatchers[config.ClusterName] = watcher
-	watcher.Start()
+	if err := watcher.Start(); err != nil {
+		return err
+	}
 	return nil
 
 }
diff --git a/controller/cmd/service-mirror/main.go b/controller/cmd/service-mirror/main.go
index 015c9a0d8..e59012592 100644
--- a/controller/cmd/service-mirror/main.go
+++ b/controller/cmd/service-mirror/main.go
@@ -1,15 +1,22 @@
 package servicemirror
 
 import (
+	"context"
+	"errors"
 	"flag"
 	"os"
 	"os/signal"
 	"syscall"
+	"time"
+
+	"k8s.io/client-go/informers"
+	"k8s.io/client-go/tools/cache"
 
 	"github.com/linkerd/linkerd2/controller/k8s"
 	"github.com/linkerd/linkerd2/pkg/admin"
 	"github.com/linkerd/linkerd2/pkg/flags"
 	log "github.com/sirupsen/logrus"
+	"k8s.io/client-go/kubernetes"
 )
 
 type chanProbeEventSink struct{ sender func(event interface{}) }
@@ -18,6 +25,24 @@ func (s *chanProbeEventSink) send(event interface{}) {
 	s.sender(event)
 }
 
+func initLocalSecretsInformer(api kubernetes.Interface, namespace string) (cache.SharedIndexInformer, error) {
+	sharedInformers := informers.NewSharedInformerFactoryWithOptions(api, 10*time.Minute, informers.WithNamespace(namespace))
+
+	informer := sharedInformers.Core().V1().Secrets().Informer()
+
+	sharedInformers.Start(nil)
+
+	ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
+	defer cancel()
+
+	log.Infof("waiting for local namespaced secrets informer caches to sync")
+	if !cache.WaitForCacheSync(ctx.Done(), informer.HasSynced) {
+		return nil, errors.New("failed to sync local namespaced secrets informer caches")
+	}
+	log.Infof("local namespaced secrets informer  caches synced")
+	return informer, nil
+}
+
 // Main executes the tap service-mirror
 func Main(args []string) {
 	cmd := flag.NewFlagSet("service-mirror", flag.ExitOnError)
@@ -25,6 +50,7 @@ func Main(args []string) {
 	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")
+	namespace := cmd.String("namespace", "", "address to serve scrapable metrics on")
 
 	flags.ConfigureAndParse(cmd, args)
 
@@ -34,21 +60,27 @@ func Main(args []string) {
 	k8sAPI, err := k8s.InitializeAPI(
 		*kubeConfigPath,
 		false,
-		k8s.Secret,
 		k8s.Svc,
 		k8s.NS,
 		k8s.Endpoint,
 	)
 
+	//TODO: Use can-i to check for required permissions
 	if err != nil {
 		log.Fatalf("Failed to initialize K8s API: %s", err)
 	}
 
+	secretsInformer, err := initLocalSecretsInformer(k8sAPI.Client, *namespace)
+
+	if err != nil {
+		log.Fatalf("Failed to initialize secrets informer: %s", err)
+	}
+
 	probeManager := NewProbeManager(k8sAPI)
 	probeManager.Start()
 
 	k8sAPI.Sync(nil)
-	watcher := NewRemoteClusterConfigWatcher(k8sAPI, *requeueLimit, &chanProbeEventSink{probeManager.enqueueEvent})
+	watcher := NewRemoteClusterConfigWatcher(*namespace, secretsInformer, k8sAPI, *requeueLimit, &chanProbeEventSink{probeManager.enqueueEvent})
 	log.Info("Started cluster config watcher")
 
 	go admin.StartServer(*metricsAddr)
diff --git a/controller/cmd/service-mirror/probe_manager.go b/controller/cmd/service-mirror/probe_manager.go
index 741f88261..9bf08782d 100644
--- a/controller/cmd/service-mirror/probe_manager.go
+++ b/controller/cmd/service-mirror/probe_manager.go
@@ -149,7 +149,8 @@ func (m *ProbeManager) handleMirroredServicePaired(event *MirroredServicePaired)
 		} else {
 			probeSpec := gatewayToProbeSpec(event.GatewaySpec)
 			if probeSpec != nil {
-				worker = NewProbeWorker(probeSpec, probeMetrics, probeKey)
+				localGatewayName := fmt.Sprintf("%s-%s", event.gatewayName, event.clusterName)
+				worker = NewProbeWorker(localGatewayName, probeSpec, probeMetrics, probeKey)
 				worker.PairService(event.serviceName, event.serviceNamespace)
 				m.probeWorkers[probeKey] = worker
 				worker.Start()
diff --git a/controller/cmd/service-mirror/probe_worker.go b/controller/cmd/service-mirror/probe_worker.go
index af70e4bc6..3886cbb6d 100644
--- a/controller/cmd/service-mirror/probe_worker.go
+++ b/controller/cmd/service-mirror/probe_worker.go
@@ -2,12 +2,10 @@ package servicemirror
 
 import (
 	"fmt"
-	"math/rand"
 	"net/http"
 	"sync"
 	"time"
 
-	consts "github.com/linkerd/linkerd2/pkg/k8s"
 	"github.com/prometheus/client_golang/prometheus"
 	logging "github.com/sirupsen/logrus"
 )
@@ -24,6 +22,7 @@ type probeSpec struct {
 
 // ProbeWorker is responsible for monitoring gateways using a probe specification
 type ProbeWorker struct {
+	localGatewayName string
 	*sync.RWMutex
 	probeSpec      *probeSpec
 	pairedServices map[string]struct{}
@@ -33,13 +32,14 @@ type ProbeWorker struct {
 }
 
 // NewProbeWorker creates a new probe worker associated with a particular gateway
-func NewProbeWorker(spec *probeSpec, metrics *probeMetrics, probekey string) *ProbeWorker {
+func NewProbeWorker(localGatewayName string, 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,
+		localGatewayName: localGatewayName,
+		RWMutex:          &sync.RWMutex{},
+		probeSpec:        spec,
+		pairedServices:   make(map[string]struct{}),
+		stopCh:           make(chan struct{}),
+		metrics:          metrics,
 		log: logging.WithFields(logging.Fields{
 			"probe-key": probekey,
 		}),
@@ -106,14 +106,6 @@ probeLoop:
 	}
 }
 
-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()
@@ -121,44 +113,37 @@ func (pw *ProbeWorker) doProbe() {
 	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,
-		}
-
-		req, err := http.NewRequest("GET", fmt.Sprintf("http://%s:%d/%s", ipToTry, pw.probeSpec.port, pw.probeSpec.path), nil)
-		if err != nil {
-			pw.log.Debugf("Could not create a GET request to gateway: %s", err)
-			return
-		}
+	client := http.Client{
+		Timeout: httpGatewayTimeoutMillis * time.Millisecond,
+	}
 
-		req.Header.Set(consts.RequireIDHeader, pw.probeSpec.gatewayIdentity)
-		start := time.Now()
-		resp, err := client.Do(req)
-		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()
-		}
+	req, err := http.NewRequest("GET", fmt.Sprintf("http://%s:%d/%s", pw.localGatewayName, pw.probeSpec.port, pw.probeSpec.path), nil)
+	if err != nil {
+		pw.log.Debugf("Could not create a GET request to gateway: %s", err)
+		return
+	}
 
-		if err := resp.Body.Close(); err != nil {
-			pw.log.Debugf("Failed to close response body %s", err)
-		}
+	start := time.Now()
+	resp, err := client.Do(req)
+	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/k8s/api.go b/controller/k8s/api.go
index 79a14abe2..57064b6f7 100644
--- a/controller/k8s/api.go
+++ b/controller/k8s/api.go
@@ -562,7 +562,11 @@ func (api *API) GetPodsFor(obj runtime.Object, includeFailed bool) ([]*corev1.Po
 			return []*corev1.Pod{}, nil
 		}
 		namespace = typed.Namespace
-		selector = labels.Set(typed.Spec.Selector).AsSelector()
+		if typed.Spec.Selector == nil {
+			selector = labels.Nothing()
+		} else {
+			selector = labels.Set(typed.Spec.Selector).AsSelector()
+		}
 
 	case *appsv1.StatefulSet:
 		namespace = typed.Namespace
diff --git a/pkg/charts/linkerd2/values.go b/pkg/charts/linkerd2/values.go
index f3050c31e..af065b402 100644
--- a/pkg/charts/linkerd2/values.go
+++ b/pkg/charts/linkerd2/values.go
@@ -21,7 +21,6 @@ type (
 	Values struct {
 		Stage                         string                         `json:"stage"`
 		ControllerImage               string                         `json:"controllerImage"`
-		ControllerImageVersion        string                         `json:"controllerImageVersion"`
 		WebImage                      string                         `json:"webImage"`
 		PrometheusImage               string                         `json:"prometheusImage"`
 		ControllerReplicas            uint                           `json:"controllerReplicas"`
@@ -72,6 +71,7 @@ type (
 		ImagePullPolicy          string `json:"imagePullPolicy"`
 		CliVersion               string `json:"cliVersion"`
 		ControllerComponentLabel string `json:"controllerComponentLabel"`
+		ControllerImageVersion   string `json:"controllerImageVersion"`
 		ControllerNamespaceLabel string `json:"controllerNamespaceLabel"`
 		WorkloadNamespaceLabel   string `json:"workloadNamespaceLabel"`
 		CreatedByAnnotation      string `json:"createdByAnnotation"`
diff --git a/pkg/charts/multicluster/values.go b/pkg/charts/multicluster/values.go
index 3043e23cc..81d81564f 100644
--- a/pkg/charts/multicluster/values.go
+++ b/pkg/charts/multicluster/values.go
@@ -13,24 +13,23 @@ 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"`
-	LocalProbePath          string `json:"localProbePath"`
-	LocalProbePort          uint32 `json:"localProbePort"`
+	CliVersion          string `json:"cliVersion"`
+	GatewayName         string `json:"gatewayName"`
+	Namespace           string `json:"namespace"`
+	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"`
+	NginxImageVersion   string `json:"nginxImageVersion"`
+	NginxImage          string `json:"nginxImage"`
+	LinkerdVersion      string `json:"linkerdVersion"`
+	CreatedByAnnotation string `json:"createdByAnnotation"`
+	LocalProbePath      string `json:"localProbePath"`
+	LocalProbePort      uint32 `json:"localProbePort"`
 }
 
 // NewValues returns a new instance of the Values type.
diff --git a/pkg/healthcheck/healthcheck.go b/pkg/healthcheck/healthcheck.go
index 1f1d674d6..7d542fca5 100644
--- a/pkg/healthcheck/healthcheck.go
+++ b/pkg/healthcheck/healthcheck.go
@@ -149,10 +149,12 @@ const (
 	// "" are equal, making "false" the default
 	LinkerdCNIResourceLabel = "linkerd.io/cni-resource"
 
-	linkerdCNIDisabledSkipReason      = "skipping check because CNI is not enabled"
-	linkerdCNIResourceName            = "linkerd-cni"
-	linkerdCNIConfigMapName           = "linkerd-cni-config"
-	linkerdServiceMirrorComponentName = "linkerd-service-mirror"
+	linkerdCNIDisabledSkipReason        = "skipping check because CNI is not enabled"
+	linkerdCNIResourceName              = "linkerd-cni"
+	linkerdCNIConfigMapName             = "linkerd-cni-config"
+	linkerdServiceMirrorComponentName   = "linkerd-service-mirror"
+	linkerdServiceMirrorClusterRoleName = "linkerd-service-mirror-access-local-resources"
+	linkerdServiceMirrorRoleName        = "linkerd-service-mirror-read-remote-creds"
 
 	// linkerdTapAPIServiceName is the name of the tap api service
 	// This key is passed to checkApiSercice method to check whether
@@ -196,12 +198,27 @@ var ExpectedServiceAccountNames = []string{
 	"linkerd-tap",
 }
 
-var expectedServiceMirrorPolicyVerbs = []string{
-	"list", "get", "watch", "create", "delete", "update",
+type expectedPolicy struct {
+	resources []string
+	verbs     []string
 }
 
-var expectedServiceMirrorPolicyResources = []string{
-	"endpoints", "services", "secrets", "namespaces",
+var expectedServiceMirrorClusterRolePolicies = []expectedPolicy{
+	{
+		resources: []string{"endpoints", "services"},
+		verbs:     []string{"list", "get", "watch", "create", "delete", "update"},
+	},
+	{
+		resources: []string{"namespaces"},
+		verbs:     []string{"create", "list", "get", "watch"},
+	},
+}
+
+var expectedServiceMirrorRolePolicies = []expectedPolicy{
+	{
+		resources: []string{"secrets"},
+		verbs:     []string{"list", "get", "watch"},
+	},
 }
 
 var expectedServiceMirrorRemoteClusterPolicyVerbs = []string{
@@ -1247,10 +1264,9 @@ func (hc *HealthChecker) allCategories() []category {
 				{
 					description: "service mirror controller ClusterRoles exist",
 					hintAnchor:  "l5d-smc-existence-cr",
-					fatal:       true,
 					check: func(context.Context) error {
 						if hc.Options.ShouldCheckMulticluster {
-							return hc.checkClusterRoles(true, []string{linkerdServiceMirrorComponentName}, hc.serviceMirrorComponentsSelector())
+							return hc.checkClusterRoles(true, []string{linkerdServiceMirrorClusterRoleName}, hc.serviceMirrorComponentsSelector())
 						}
 						return &SkipError{Reason: "not checking muticluster"}
 					},
@@ -1258,10 +1274,29 @@ func (hc *HealthChecker) allCategories() []category {
 				{
 					description: "service mirror controller ClusterRoleBindings exist",
 					hintAnchor:  "l5d-smc-existence-crb",
-					fatal:       true,
 					check: func(context.Context) error {
 						if hc.Options.ShouldCheckMulticluster {
-							return hc.checkClusterRoleBindings(true, []string{linkerdServiceMirrorComponentName}, hc.serviceMirrorComponentsSelector())
+							return hc.checkClusterRoleBindings(true, []string{linkerdServiceMirrorClusterRoleName}, hc.serviceMirrorComponentsSelector())
+						}
+						return &SkipError{Reason: "not checking muticluster"}
+					},
+				},
+				{
+					description: "service mirror controller Roles exist",
+					hintAnchor:  "l5d-smc-existence-r",
+					check: func(context.Context) error {
+						if hc.Options.ShouldCheckMulticluster {
+							return hc.checkRoles(true, hc.serviceMirrorNs, []string{linkerdServiceMirrorRoleName}, hc.serviceMirrorComponentsSelector())
+						}
+						return &SkipError{Reason: "not checking muticluster"}
+					},
+				},
+				{
+					description: "service mirror controller RoleBindings exist",
+					hintAnchor:  "l5d-smc-existence-rb",
+					check: func(context.Context) error {
+						if hc.Options.ShouldCheckMulticluster {
+							return hc.checkRoleBindings(true, hc.serviceMirrorNs, []string{linkerdServiceMirrorRoleName}, hc.serviceMirrorComponentsSelector())
 						}
 						return &SkipError{Reason: "not checking muticluster"}
 					},
@@ -1269,7 +1304,6 @@ func (hc *HealthChecker) allCategories() []category {
 				{
 					description: "service mirror controller ServiceAccounts exist",
 					hintAnchor:  "l5d-smc-existence-sa",
-					fatal:       true,
 					check: func(context.Context) error {
 						if hc.Options.ShouldCheckMulticluster {
 							return hc.checkServiceAccounts([]string{linkerdServiceMirrorComponentName}, hc.serviceMirrorNs, hc.serviceMirrorComponentsSelector())
@@ -1280,7 +1314,6 @@ func (hc *HealthChecker) allCategories() []category {
 				{
 					description: "service mirror controller has required permissions",
 					hintAnchor:  "l5d-smc-local-rbac-existence",
-					fatal:       true,
 					check: func(context.Context) error {
 						return hc.checkServiceMirrorLocalRBAC()
 					},
@@ -1288,7 +1321,6 @@ func (hc *HealthChecker) allCategories() []category {
 				{
 					description: "service mirror controller can access remote clusters",
 					hintAnchor:  "l5d-smc-remote-remote-clusters-access",
-					fatal:       true,
 					check: func(context.Context) error {
 						return hc.checkRemoteClusterConnectivity()
 					},
@@ -1296,7 +1328,6 @@ func (hc *HealthChecker) allCategories() []category {
 				{
 					description: "all remote cluster gateways are alive",
 					hintAnchor:  "l5d-remote-gateways-alive",
-					fatal:       true,
 					check: func(ctx context.Context) error {
 						return hc.checkRemoteClusterGatewaysHealth(ctx)
 					},
@@ -1304,7 +1335,6 @@ func (hc *HealthChecker) allCategories() []category {
 				{
 					description: "clusters share trust anchors",
 					hintAnchor:  "l5d-clusters-share-anchors",
-					fatal:       true,
 					check: func(ctx context.Context) error {
 						return hc.checkRemoteClusterAnchors()
 					},
@@ -1619,34 +1649,50 @@ func comparePermissions(expected, actual []string) error {
 	return nil
 }
 
+func verifyRule(expected expectedPolicy, actual []v1.PolicyRule) error {
+	for _, rule := range actual {
+		if err := comparePermissions(expected.resources, rule.Resources); err == nil {
+			if err := comparePermissions(expected.verbs, rule.Verbs); err != nil {
+				return fmt.Errorf("unexpected verbs %s", err)
+			}
+			return nil
+		}
+	}
+	return fmt.Errorf("could not fine rule for %s", strings.Join(expected.resources, ","))
+}
+
 func (hc *HealthChecker) checkServiceMirrorLocalRBAC() error {
 	if hc.Options.ShouldCheckMulticluster {
 		var errors []string
 
-		cr, err := hc.kubeAPI.RbacV1().ClusterRoles().Get(linkerdServiceMirrorComponentName, metav1.GetOptions{})
+		clusterRole, err := hc.kubeAPI.RbacV1().ClusterRoles().Get(linkerdServiceMirrorClusterRoleName, metav1.GetOptions{})
 		if err != nil {
 			return fmt.Errorf("Could not obtain service mirror ClusterRole: %s", err)
 		}
 
-		var policyRule *v1.PolicyRule
+		role, err := hc.kubeAPI.RbacV1().Roles(hc.serviceMirrorNs).Get(linkerdServiceMirrorRoleName, metav1.GetOptions{})
+		if err != nil {
+			return fmt.Errorf("Could not obtain service mirror Role: %s", err)
+		}
 
-		for _, r := range cr.Rules {
-			if len(r.APIGroups) == 1 && r.APIGroups[0] == "" {
-				rule := r
-				policyRule = &rule
-			}
+		if len(clusterRole.Rules) != len(expectedServiceMirrorClusterRolePolicies) {
+			return fmt.Errorf("Service mirror ClusterRole has %d policy rules, expected %d", len(clusterRole.Rules), len(expectedServiceMirrorClusterRolePolicies))
 		}
 
-		if policyRule == nil {
-			return fmt.Errorf("Service mirror ClusterRole is missing expected policy rule")
+		for _, rule := range expectedServiceMirrorClusterRolePolicies {
+			if err := verifyRule(rule, clusterRole.Rules); err != nil {
+				errors = append(errors, fmt.Sprintf("Service mirror ClusterRole: %s", err))
+			}
 		}
 
-		if err := comparePermissions(expectedServiceMirrorPolicyVerbs, policyRule.Verbs); err != nil {
-			errors = append(errors, fmt.Sprintf("Service mirror ClusterRole is missing verbs: %s", err))
+		if len(role.Rules) != len(expectedServiceMirrorRolePolicies) {
+			return fmt.Errorf("Service mirror Role has %d policy rules, expected %d", len(role.Rules), len(expectedServiceMirrorRolePolicies))
 		}
 
-		if err := comparePermissions(expectedServiceMirrorPolicyResources, policyRule.Resources); err != nil {
-			errors = append(errors, fmt.Sprintf("Service mirror ClusterRole is missing required resources: %s", err))
+		for _, rule := range expectedServiceMirrorRolePolicies {
+			if err := verifyRule(rule, role.Rules); err != nil {
+				errors = append(errors, fmt.Sprintf("Service mirror Role: %s", err))
+			}
 		}
 
 		if len(errors) > 0 {
@@ -1845,6 +1891,44 @@ func (hc *HealthChecker) expectedRBACNames() []string {
 	}
 }
 
+func (hc *HealthChecker) checkRoles(shouldExist bool, namespace string, expectedNames []string, labelSelector string) error {
+	options := metav1.ListOptions{
+		LabelSelector: labelSelector,
+	}
+	crList, err := hc.kubeAPI.RbacV1().Roles(namespace).List(options)
+	if err != nil {
+		return err
+	}
+
+	objects := []runtime.Object{}
+
+	for _, item := range crList.Items {
+		item := item // pin
+		objects = append(objects, &item)
+	}
+
+	return checkResources("Roles", objects, expectedNames, shouldExist)
+}
+
+func (hc *HealthChecker) checkRoleBindings(shouldExist bool, namespace string, expectedNames []string, labelSelector string) error {
+	options := metav1.ListOptions{
+		LabelSelector: labelSelector,
+	}
+	crbList, err := hc.kubeAPI.RbacV1().RoleBindings(namespace).List(options)
+	if err != nil {
+		return err
+	}
+
+	objects := []runtime.Object{}
+
+	for _, item := range crbList.Items {
+		item := item // pin
+		objects = append(objects, &item)
+	}
+
+	return checkResources("RoleBindings", objects, expectedNames, shouldExist)
+}
+
 func (hc *HealthChecker) checkClusterRoles(shouldExist bool, expectedNames []string, labelSelector string) error {
 	options := metav1.ListOptions{
 		LabelSelector: labelSelector,


Test patch:
diff --git a/cli/cmd/install_test.go b/cli/cmd/install_test.go
index 9ee7f98ea..15347db11 100644
--- a/cli/cmd/install_test.go
+++ b/cli/cmd/install_test.go
@@ -54,7 +54,6 @@ func TestRender(t *testing.T) {
 	metaConfig.Global.LinkerdNamespace = "Namespace"
 	metaValues := &charts.Values{
 		ControllerImage:             "ControllerImage",
-		ControllerImageVersion:      "ControllerImageVersion",
 		WebImage:                    "WebImage",
 		PrometheusImage:             "PrometheusImage",
 		ControllerLogLevel:          "ControllerLogLevel",
@@ -73,6 +72,7 @@ func TestRender(t *testing.T) {
 			ImagePullPolicy:          "ImagePullPolicy",
 			CliVersion:               "CliVersion",
 			ControllerComponentLabel: "ControllerComponentLabel",
+			ControllerImageVersion:   "ControllerImageVersion",
 			ControllerNamespaceLabel: "ControllerNamespaceLabel",
 			WorkloadNamespaceLabel:   "WorkloadNamespaceLabel",
 			CreatedByAnnotation:      "CreatedByAnnotation",
diff --git a/cli/cmd/testdata/grafana_overwrite.yaml b/cli/cmd/testdata/grafana_overwrite.yaml
index 955f4e8b5..c8547f72a 100644
--- a/cli/cmd/testdata/grafana_overwrite.yaml
+++ b/cli/cmd/testdata/grafana_overwrite.yaml
@@ -1,4 +1,5 @@
 grafana:
   enabled: true
   name: linkerd-grafana-overwrite
-  image: linkerd-image-overwrite
+  image:
+    name: linkerd-image-overwrite
diff --git a/cli/cmd/testdata/install_addon_control-plane.golden b/cli/cmd/testdata/install_addon_control-plane.golden
index abc25a4c8..3fb739015 100644
--- a/cli/cmd/testdata/install_addon_control-plane.golden
+++ b/cli/cmd/testdata/install_addon_control-plane.golden
@@ -2013,7 +2013,9 @@ data:
       grafanaUrl: 
     grafana:
       enabled: true
-      image: gcr.io/linkerd-io/grafana
+      image:
+        name: gcr.io/linkerd-io/grafana
+        version: install-control-plane-version
       name: linkerd-grafana
     tracing:
       collector:
diff --git a/cli/cmd/testdata/install_control-plane.golden b/cli/cmd/testdata/install_control-plane.golden
index 5a8d2507d..9625e8c9c 100644
--- a/cli/cmd/testdata/install_control-plane.golden
+++ b/cli/cmd/testdata/install_control-plane.golden
@@ -2012,7 +2012,9 @@ data:
       grafanaUrl: 
     grafana:
       enabled: true
-      image: gcr.io/linkerd-io/grafana
+      image:
+        name: gcr.io/linkerd-io/grafana
+        version: install-control-plane-version
       name: linkerd-grafana
     tracing:
       enabled: false
diff --git a/cli/cmd/testdata/install_controlplane_tracing_output.golden b/cli/cmd/testdata/install_controlplane_tracing_output.golden
index cab8bcdf0..e66ff27c4 100644
--- a/cli/cmd/testdata/install_controlplane_tracing_output.golden
+++ b/cli/cmd/testdata/install_controlplane_tracing_output.golden
@@ -2956,7 +2956,9 @@ data:
       grafanaUrl: 
     grafana:
       enabled: true
-      image: gcr.io/linkerd-io/grafana
+      image:
+        name: gcr.io/linkerd-io/grafana
+        version: install-control-plane-version
       name: linkerd-grafana
     tracing:
       enabled: false
diff --git a/cli/cmd/testdata/install_custom_registry.golden b/cli/cmd/testdata/install_custom_registry.golden
index 00df11a00..e3d1f2828 100644
--- a/cli/cmd/testdata/install_custom_registry.golden
+++ b/cli/cmd/testdata/install_custom_registry.golden
@@ -2839,7 +2839,9 @@ data:
       grafanaUrl: 
     grafana:
       enabled: true
-      image: my.custom.registry/linkerd-io/grafana
+      image:
+        name: my.custom.registry/linkerd-io/grafana
+        version: install-control-plane-version
       name: linkerd-grafana
     tracing:
       enabled: false
diff --git a/cli/cmd/testdata/install_default.golden b/cli/cmd/testdata/install_default.golden
index 7104c36ef..316a00be6 100644
--- a/cli/cmd/testdata/install_default.golden
+++ b/cli/cmd/testdata/install_default.golden
@@ -2839,7 +2839,9 @@ data:
       grafanaUrl: 
     grafana:
       enabled: true
-      image: gcr.io/linkerd-io/grafana
+      image:
+        name: gcr.io/linkerd-io/grafana
+        version: install-control-plane-version
       name: linkerd-grafana
     tracing:
       enabled: false
diff --git a/cli/cmd/testdata/install_ha_output.golden b/cli/cmd/testdata/install_ha_output.golden
index f042dd05e..3515bda6b 100644
--- a/cli/cmd/testdata/install_ha_output.golden
+++ b/cli/cmd/testdata/install_ha_output.golden
@@ -3088,7 +3088,9 @@ data:
       grafanaUrl: 
     grafana:
       enabled: true
-      image: gcr.io/linkerd-io/grafana
+      image:
+        name: gcr.io/linkerd-io/grafana
+        version: install-control-plane-version
       name: linkerd-grafana
       resources:
         cpu:
diff --git a/cli/cmd/testdata/install_ha_with_overrides_output.golden b/cli/cmd/testdata/install_ha_with_overrides_output.golden
index b2c0f47e2..a82a2dcfb 100644
--- a/cli/cmd/testdata/install_ha_with_overrides_output.golden
+++ b/cli/cmd/testdata/install_ha_with_overrides_output.golden
@@ -3088,7 +3088,9 @@ data:
       grafanaUrl: 
     grafana:
       enabled: true
-      image: gcr.io/linkerd-io/grafana
+      image:
+        name: gcr.io/linkerd-io/grafana
+        version: install-control-plane-version
       name: linkerd-grafana
       resources:
         cpu:
diff --git a/cli/cmd/testdata/install_heartbeat_disabled_output.golden b/cli/cmd/testdata/install_heartbeat_disabled_output.golden
index 7b065145b..9b4506d03 100644
--- a/cli/cmd/testdata/install_heartbeat_disabled_output.golden
+++ b/cli/cmd/testdata/install_heartbeat_disabled_output.golden
@@ -2750,7 +2750,9 @@ data:
       grafanaUrl: 
     grafana:
       enabled: true
-      image: gcr.io/linkerd-io/grafana
+      image:
+        name: gcr.io/linkerd-io/grafana
+        version: install-control-plane-version
       name: linkerd-grafana
     tracing:
       enabled: false
diff --git a/cli/cmd/testdata/install_helm_output.golden b/cli/cmd/testdata/install_helm_output.golden
index df759c77f..c1e1ecb66 100644
--- a/cli/cmd/testdata/install_helm_output.golden
+++ b/cli/cmd/testdata/install_helm_output.golden
@@ -2880,7 +2880,9 @@ data:
       grafanaUrl: 
     grafana:
       enabled: true
-      image: gcr.io/linkerd-io/grafana
+      image:
+        name: gcr.io/linkerd-io/grafana
+        version: linkerdVersionValue
       name: linkerd-grafana
     tracing:
       enabled: false
@@ -3023,7 +3025,7 @@ spec:
       - env:
         - name: GF_PATHS_DATA
           value: /data
-        image: gcr.io/linkerd-io/grafana:linkerd-version
+        image: gcr.io/linkerd-io/grafana:linkerdVersionValue
         imagePullPolicy: IfNotPresent
         livenessProbe:
           httpGet:
diff --git a/cli/cmd/testdata/install_helm_output_addons.golden b/cli/cmd/testdata/install_helm_output_addons.golden
index 72393975c..56b82cc28 100644
--- a/cli/cmd/testdata/install_helm_output_addons.golden
+++ b/cli/cmd/testdata/install_helm_output_addons.golden
@@ -2881,7 +2881,9 @@ data:
       grafanaUrl: 
     grafana:
       enabled: true
-      image: gcr.io/linkerd-io/grafana
+      image:
+        name: gcr.io/linkerd-io/grafana
+        version: linkerdVersionValue
       name: linkerd-grafana
     tracing:
       collector:
@@ -3493,7 +3495,7 @@ spec:
       - env:
         - name: GF_PATHS_DATA
           value: /data
-        image: gcr.io/linkerd-io/grafana:linkerd-version
+        image: gcr.io/linkerd-io/grafana:linkerdVersionValue
         imagePullPolicy: IfNotPresent
         livenessProbe:
           httpGet:
diff --git a/cli/cmd/testdata/install_helm_output_ha.golden b/cli/cmd/testdata/install_helm_output_ha.golden
index cf5b94575..7abd6759b 100644
--- a/cli/cmd/testdata/install_helm_output_ha.golden
+++ b/cli/cmd/testdata/install_helm_output_ha.golden
@@ -3129,7 +3129,9 @@ data:
       grafanaUrl: 
     grafana:
       enabled: true
-      image: gcr.io/linkerd-io/grafana
+      image:
+        name: gcr.io/linkerd-io/grafana
+        version: linkerdVersionValue
       name: linkerd-grafana
       resources:
         cpu:
@@ -3279,7 +3281,7 @@ spec:
       - env:
         - name: GF_PATHS_DATA
           value: /data
-        image: gcr.io/linkerd-io/grafana:linkerd-version
+        image: gcr.io/linkerd-io/grafana:linkerdVersionValue
         imagePullPolicy: IfNotPresent
         livenessProbe:
           httpGet:
diff --git a/cli/cmd/testdata/install_no_init_container.golden b/cli/cmd/testdata/install_no_init_container.golden
index 94891752a..50e9ed133 100644
--- a/cli/cmd/testdata/install_no_init_container.golden
+++ b/cli/cmd/testdata/install_no_init_container.golden
@@ -2572,7 +2572,9 @@ data:
       grafanaUrl: 
     grafana:
       enabled: true
-      image: gcr.io/linkerd-io/grafana
+      image:
+        name: gcr.io/linkerd-io/grafana
+        version: install-control-plane-version
       name: linkerd-grafana
     tracing:
       enabled: false
diff --git a/cli/cmd/testdata/install_output.golden b/cli/cmd/testdata/install_output.golden
index 814678252..a97b1d79c 100644
--- a/cli/cmd/testdata/install_output.golden
+++ b/cli/cmd/testdata/install_output.golden
@@ -2831,7 +2831,9 @@ data:
       grafanaUrl: 
     grafana:
       enabled: true
-      image: gcr.io/linkerd-io/grafana
+      image:
+        name: gcr.io/linkerd-io/grafana
+        version: install-control-plane-version
       name: linkerd-grafana
     tracing:
       enabled: false
@@ -2970,7 +2972,7 @@ spec:
       - env:
         - name: GF_PATHS_DATA
           value: /data
-        image: gcr.io/linkerd-io/grafana:ControllerImageVersion
+        image: gcr.io/linkerd-io/grafana:install-control-plane-version
         imagePullPolicy: ImagePullPolicy
         livenessProbe:
           httpGet:
diff --git a/cli/cmd/testdata/install_proxy_ignores.golden b/cli/cmd/testdata/install_proxy_ignores.golden
index 4944837db..d5406f02b 100644
--- a/cli/cmd/testdata/install_proxy_ignores.golden
+++ b/cli/cmd/testdata/install_proxy_ignores.golden
@@ -2839,7 +2839,9 @@ data:
       grafanaUrl: 
     grafana:
       enabled: true
-      image: gcr.io/linkerd-io/grafana
+      image:
+        name: gcr.io/linkerd-io/grafana
+        version: install-control-plane-version
       name: linkerd-grafana
     tracing:
       enabled: false
diff --git a/cli/cmd/testdata/install_restricted_dashboard.golden b/cli/cmd/testdata/install_restricted_dashboard.golden
index 199104f35..8dff0fb67 100644
--- a/cli/cmd/testdata/install_restricted_dashboard.golden
+++ b/cli/cmd/testdata/install_restricted_dashboard.golden
@@ -2774,7 +2774,9 @@ data:
       grafanaUrl: 
     grafana:
       enabled: true
-      image: gcr.io/linkerd-io/grafana
+      image:
+        name: gcr.io/linkerd-io/grafana
+        version: install-control-plane-version
       name: linkerd-grafana
     tracing:
       enabled: false
diff --git a/cli/cmd/testdata/install_tracing.golden b/cli/cmd/testdata/install_tracing.golden
index e8dd834bd..512200b10 100644
--- a/cli/cmd/testdata/install_tracing.golden
+++ b/cli/cmd/testdata/install_tracing.golden
@@ -2840,7 +2840,9 @@ data:
       grafanaUrl: 
     grafana:
       enabled: true
-      image: gcr.io/linkerd-io/grafana
+      image:
+        name: gcr.io/linkerd-io/grafana
+        version: install-control-plane-version
       name: linkerd-grafana
     tracing:
       collector:
diff --git a/cli/cmd/testdata/upgrade_add-on_controlplane.golden b/cli/cmd/testdata/upgrade_add-on_controlplane.golden
index 5f7dc8a4d..a65e46683 100644
--- a/cli/cmd/testdata/upgrade_add-on_controlplane.golden
+++ b/cli/cmd/testdata/upgrade_add-on_controlplane.golden
@@ -2029,7 +2029,9 @@ data:
       grafanaUrl: 
     grafana:
       enabled: true
-      image: gcr.io/linkerd-io/grafana
+      image:
+        name: gcr.io/linkerd-io/grafana
+        version: UPGRADE-CONTROL-PLANE-VERSION
       name: linkerd-grafana
     tracing:
       collector:
diff --git a/cli/cmd/testdata/upgrade_add-on_overwrite.golden b/cli/cmd/testdata/upgrade_add-on_overwrite.golden
index f40e15da3..77a4604a4 100644
--- a/cli/cmd/testdata/upgrade_add-on_overwrite.golden
+++ b/cli/cmd/testdata/upgrade_add-on_overwrite.golden
@@ -2856,7 +2856,9 @@ data:
       grafanaUrl: 
     grafana:
       enabled: true
-      image: gcr.io/linkerd-io/grafana
+      image:
+        name: gcr.io/linkerd-io/grafana
+        version: UPGRADE-CONTROL-PLANE-VERSION
       name: linkerd-grafana
     tracing:
       collector:
diff --git a/cli/cmd/testdata/upgrade_add_add-on.golden b/cli/cmd/testdata/upgrade_add_add-on.golden
index 3dfa047e0..5c73cd5ca 100644
--- a/cli/cmd/testdata/upgrade_add_add-on.golden
+++ b/cli/cmd/testdata/upgrade_add_add-on.golden
@@ -2856,7 +2856,9 @@ data:
       grafanaUrl: 
     grafana:
       enabled: true
-      image: gcr.io/linkerd-io/grafana
+      image:
+        name: gcr.io/linkerd-io/grafana
+        version: UPGRADE-CONTROL-PLANE-VERSION
       name: linkerd-grafana
     tracing:
       collector:
diff --git a/cli/cmd/testdata/upgrade_default.golden b/cli/cmd/testdata/upgrade_default.golden
index 1494f5a62..4f773c1af 100644
--- a/cli/cmd/testdata/upgrade_default.golden
+++ b/cli/cmd/testdata/upgrade_default.golden
@@ -2855,7 +2855,9 @@ data:
       grafanaUrl: 
     grafana:
       enabled: true
-      image: gcr.io/linkerd-io/grafana
+      image:
+        name: gcr.io/linkerd-io/grafana
+        version: UPGRADE-CONTROL-PLANE-VERSION
       name: linkerd-grafana
     tracing:
       enabled: false
diff --git a/cli/cmd/testdata/upgrade_external_issuer.golden b/cli/cmd/testdata/upgrade_external_issuer.golden
index 530cda1b1..78765f90c 100644
--- a/cli/cmd/testdata/upgrade_external_issuer.golden
+++ b/cli/cmd/testdata/upgrade_external_issuer.golden
@@ -2841,7 +2841,9 @@ data:
       grafanaUrl: 
     grafana:
       enabled: true
-      image: gcr.io/linkerd-io/grafana
+      image:
+        name: gcr.io/linkerd-io/grafana
+        version: UPGRADE-CONTROL-PLANE-VERSION
       name: linkerd-grafana
     tracing:
       enabled: false
diff --git a/cli/cmd/testdata/upgrade_grafana_enabled.yaml b/cli/cmd/testdata/upgrade_grafana_enabled.yaml
index 50379ae3c..cb882752b 100644
--- a/cli/cmd/testdata/upgrade_grafana_enabled.yaml
+++ b/cli/cmd/testdata/upgrade_grafana_enabled.yaml
@@ -2855,7 +2855,9 @@ data:
       grafanaUrl: 
     grafana:
       enabled: true
-      image: gcr.io/linkerd-io/grafana
+      image:
+        name: gcr.io/linkerd-io/grafana
+        version: UPGRADE-CONTROL-PLANE-VERSION
       name: linkerd-grafana
     tracing:
       enabled: false
diff --git a/cli/cmd/testdata/upgrade_grafana_overwrite.yaml b/cli/cmd/testdata/upgrade_grafana_overwrite.yaml
index 7382d594e..f32e13ce4 100644
--- a/cli/cmd/testdata/upgrade_grafana_overwrite.yaml
+++ b/cli/cmd/testdata/upgrade_grafana_overwrite.yaml
@@ -2855,7 +2855,9 @@ data:
       grafanaUrl: 
     grafana:
       enabled: true
-      image: linkerd-image-overwrite
+      image:
+        name: linkerd-image-overwrite
+        version: UPGRADE-CONTROL-PLANE-VERSION
       name: linkerd-grafana-overwrite
     tracing:
       enabled: false
diff --git a/cli/cmd/testdata/upgrade_ha.golden b/cli/cmd/testdata/upgrade_ha.golden
index 5851e2b88..bd14f532f 100644
--- a/cli/cmd/testdata/upgrade_ha.golden
+++ b/cli/cmd/testdata/upgrade_ha.golden
@@ -3104,7 +3104,9 @@ data:
       grafanaUrl: 
     grafana:
       enabled: true
-      image: gcr.io/linkerd-io/grafana
+      image:
+        name: gcr.io/linkerd-io/grafana
+        version: UPGRADE-CONTROL-PLANE-VERSION
       name: linkerd-grafana
       resources:
         cpu:
diff --git a/cli/cmd/testdata/upgrade_nothing_addon.yaml b/cli/cmd/testdata/upgrade_nothing_addon.yaml
index 50379ae3c..cb882752b 100644
--- a/cli/cmd/testdata/upgrade_nothing_addon.yaml
+++ b/cli/cmd/testdata/upgrade_nothing_addon.yaml
@@ -2855,7 +2855,9 @@ data:
       grafanaUrl: 
     grafana:
       enabled: true
-      image: gcr.io/linkerd-io/grafana
+      image:
+        name: gcr.io/linkerd-io/grafana
+        version: UPGRADE-CONTROL-PLANE-VERSION
       name: linkerd-grafana
     tracing:
       enabled: false
diff --git a/cli/cmd/testdata/upgrade_overwrite_issuer.golden b/cli/cmd/testdata/upgrade_overwrite_issuer.golden
index fcafda5f1..8ebed0b56 100644
--- a/cli/cmd/testdata/upgrade_overwrite_issuer.golden
+++ b/cli/cmd/testdata/upgrade_overwrite_issuer.golden
@@ -2839,7 +2839,9 @@ data:
       grafanaUrl: 
     grafana:
       enabled: true
-      image: gcr.io/linkerd-io/grafana
+      image:
+        name: gcr.io/linkerd-io/grafana
+        version: UPGRADE-CONTROL-PLANE-VERSION
       name: linkerd-grafana
     tracing:
       enabled: false
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 1388c6d56..19a40973e 100644
--- a/cli/cmd/testdata/upgrade_overwrite_trust_anchors-external-issuer.golden
+++ b/cli/cmd/testdata/upgrade_overwrite_trust_anchors-external-issuer.golden
@@ -2825,7 +2825,9 @@ data:
       grafanaUrl: 
     grafana:
       enabled: true
-      image: gcr.io/linkerd-io/grafana
+      image:
+        name: gcr.io/linkerd-io/grafana
+        version: UPGRADE-CONTROL-PLANE-VERSION
       name: linkerd-grafana
     tracing:
       enabled: false
diff --git a/cli/cmd/testdata/upgrade_overwrite_trust_anchors.golden b/cli/cmd/testdata/upgrade_overwrite_trust_anchors.golden
index fcafda5f1..8ebed0b56 100644
--- a/cli/cmd/testdata/upgrade_overwrite_trust_anchors.golden
+++ b/cli/cmd/testdata/upgrade_overwrite_trust_anchors.golden
@@ -2839,7 +2839,9 @@ data:
       grafanaUrl: 
     grafana:
       enabled: true
-      image: gcr.io/linkerd-io/grafana
+      image:
+        name: gcr.io/linkerd-io/grafana
+        version: UPGRADE-CONTROL-PLANE-VERSION
       name: linkerd-grafana
     tracing:
       enabled: false
diff --git a/pkg/charts/linkerd2/values_test.go b/pkg/charts/linkerd2/values_test.go
index d20af0369..357bd3b1f 100644
--- a/pkg/charts/linkerd2/values_test.go
+++ b/pkg/charts/linkerd2/values_test.go
@@ -16,7 +16,6 @@ func TestNewValues(t *testing.T) {
 	expected := &Values{
 		Stage:                         "",
 		ControllerImage:               "gcr.io/linkerd-io/controller",
-		ControllerImageVersion:        testVersion,
 		WebImage:                      "gcr.io/linkerd-io/web",
 		PrometheusImage:               "prom/prometheus:v2.15.2",
 		ControllerReplicas:            1,
@@ -40,6 +39,7 @@ func TestNewValues(t *testing.T) {
 			ImagePullPolicy:          "IfNotPresent",
 			CliVersion:               "linkerd/cli dev-undefined",
 			ControllerComponentLabel: "linkerd.io/control-plane-component",
+			ControllerImageVersion:   testVersion,
 			ControllerNamespaceLabel: "linkerd.io/control-plane-ns",
 			WorkloadNamespaceLabel:   "linkerd.io/workload-ns",
 			CreatedByAnnotation:      "linkerd.io/created-by",
@@ -134,17 +134,21 @@ func TestNewValues(t *testing.T) {
 		Grafana: Grafana{
 			"enabled": true,
 			"name":    "linkerd-grafana",
-			"image":   "gcr.io/linkerd-io/grafana",
+			"image": map[string]interface{}{
+				"name":    "gcr.io/linkerd-io/grafana",
+				"version": testVersion,
+			},
 		},
 	}
 
 	// pin the versions to ensure consistent test result.
 	// in non-test environment, the default versions are read from the
 	// values.yaml.
-	actual.ControllerImageVersion = testVersion
+	actual.Global.ControllerImageVersion = testVersion
 	actual.Global.Proxy.Image.Version = testVersion
 	actual.Global.ProxyInit.Image.Version = testVersion
 	actual.DebugContainer.Image.Version = testVersion
+	actual.Grafana["image"].(map[string]interface{})["version"] = testVersion
 
 	// Make Add-On Values nil to not have to check for their defaults
 	actual.Tracing = nil
@@ -190,7 +194,10 @@ func TestNewValues(t *testing.T) {
 		expected.Grafana = Grafana{
 			"enabled": true,
 			"name":    "linkerd-grafana",
-			"image":   "gcr.io/linkerd-io/grafana",
+			"image": map[string]interface{}{
+				"name":    "gcr.io/linkerd-io/grafana",
+				"version": testVersion,
+			},
 			"resources": map[string]interface{}{
 				"cpu": map[string]interface{}{
 					"limit":   controllerResources.CPU.Limit,
@@ -239,11 +246,11 @@ func TestNewValues(t *testing.T) {
 		// pin the versions to ensure consistent test result.
 		// in non-test environment, the default versions are read from the
 		// values.yaml.
-		actual.ControllerImageVersion = testVersion
+		actual.Global.ControllerImageVersion = testVersion
 		actual.Global.Proxy.Image.Version = testVersion
 		actual.Global.ProxyInit.Image.Version = testVersion
 		actual.DebugContainer.Image.Version = testVersion
-
+		actual.Grafana["image"].(map[string]interface{})["version"] = testVersion
 		// Make Add-On Values nil to not have to check for their defaults
 		actual.Tracing = nil
 
diff --git a/test/install_test.go b/test/install_test.go
index 1fda17d8f..094285c9c 100644
--- a/test/install_test.go
+++ b/test/install_test.go
@@ -350,6 +350,7 @@ func helmOverridesEdge(root *tls.CA) []string {
 		"--set", "identity.issuer.tls.crtPEM=" + root.Cred.Crt.EncodeCertificatePEM(),
 		"--set", "identity.issuer.tls.keyPEM=" + root.Cred.EncodePrivateKeyPEM(),
 		"--set", "identity.issuer.crtExpiry=" + root.Cred.Crt.Certificate.NotAfter.Format(time.RFC3339),
+		"--set", "grafana.image.version=" + TestHelper.GetVersion(),
 	}
 }