Repository: linkerd/linkerd2
Pull request #4447 (2020-05-20 07:51:25)

markdownlint: Use /bin/sh instead of /bin/bash
The nice and clean markdownlint scripts use no bash-specific
functionality. Hence they could be run with /bin/sh instead. On e.g.
Debian-based systems /bin/sh is dash which has 1/10 of bash's footprint.

Signed-off-by: Joakim Roubert <joakimr@axis.com>



Patch:
diff --git a/.github/workflows/kind_integration.yml b/.github/workflows/kind_integration.yml
index a51b9aa7c..4af9d4f29 100644
--- a/.github/workflows/kind_integration.yml
+++ b/.github/workflows/kind_integration.yml
@@ -130,14 +130,14 @@ jobs:
       # engineerd/setup-kind@v0.3.0
       uses: engineerd/setup-kind@d0e9be1
       with:
-        version: "v0.7.0"
+        version: "v0.8.1"
     - name: Setup custom_domain KinD cluster
       if: matrix.integration_test == 'custom_domain'
       # engineerd/setup-kind@v0.3.0
       uses: engineerd/setup-kind@d0e9be1
       with:
         config: test/testdata/custom_cluster_domain_config.yaml
-        version: "v0.7.0"
+        version: "v0.8.1"
     - name: Load image archives into the local KinD cluster
       if: github.event_name == 'push' || !github.event.pull_request.head.repo.fork
       env:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 907f54908..26acd70b4 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -129,14 +129,14 @@ jobs:
       # engineerd/setup-kind@v0.3.0
       uses: engineerd/setup-kind@d0e9be1
       with:
-        version: "v0.7.0"
+        version: "v0.8.1"
     - name: Setup custom_domain KinD cluster
       if: matrix.integration_test == 'custom_domain'
       # engineerd/setup-kind@v0.3.0
       uses: engineerd/setup-kind@d0e9be1
       with:
         config: test/testdata/custom_cluster_domain_config.yaml
-        version: "v0.7.0"
+        version: "v0.8.1"
     - name: Load image archives into the local KinD cluster
       env:
         PROXY_INIT_IMAGE_NAME: gcr.io/linkerd-io/proxy-init:v1.3.1
diff --git a/.github/workflows/static_checks.yml b/.github/workflows/static_checks.yml
index c01ccf083..e03d358cc 100644
--- a/.github/workflows/static_checks.yml
+++ b/.github/workflows/static_checks.yml
@@ -68,25 +68,10 @@ jobs:
     - name: Checkout code
       uses: actions/checkout@v2
     - name: shellcheck
-      # TODO: Each file listed here is excluded from shellcheck because it
-      # fails. As we fix files we can remove them from this list.
-      #
-      # Once we have this list paired down signficantly, we can switch to
-      # disabling specific checks across all files, for example:
-      # bin/shellcheck bin/* --exclude=SC1000
-      #
       # For more information on shellcheck failures:
       # https://github.com/koalaman/shellcheck/wiki/Checks
       run: |
-        find ./bin -type f \
-        ! -name docker-build-proxy \
-        ! -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 {}
+        bin/shellcheck-all
   markdown_lint:
     name: Markdown lint
     runs-on: ubuntu-18.04
@@ -95,4 +80,4 @@ jobs:
       # actions/checkout@v2
       uses: actions/checkout@722adc6
     - name: Markdown lint
-      run: bin/markdownlint-all
\ No newline at end of file
+      run: bin/markdownlint-all
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/create-release-tag b/bin/create-release-tag
index 0a36b8406..d8e382122 100755
--- a/bin/create-release-tag
+++ b/bin/create-release-tag
@@ -85,7 +85,7 @@ if [ $# -ne 1 ]; then
 fi
 
 bindir=$( cd "${BASH_SOURCE[0]%/*}" && pwd )
-# shellcheck source=bin/_release.sh
+# shellcheck source=_release.sh
 tmp=$(. "$bindir"/_release.sh; extract_release_notes)
 
 # Create a signed tag with the commit message.
diff --git a/bin/helm-build b/bin/helm-build
index 41f6537fa..dc0a0e7b2 100755
--- a/bin/helm-build
+++ b/bin/helm-build
@@ -32,7 +32,7 @@ rootdir=$( cd "$bindir"/.. && pwd )
 
 # `bin/helm-build package` assumes the presence of "$rootdir"/target/helm/index-pre.yaml which is downloaded in the chart_deploy CI job
 if [ "$1" = package ]; then
-    # shellcheck source=bin/_tag.sh
+    # shellcheck source=_tag.sh
     . "$bindir"/_tag.sh
     tag=$(named_tag)
     clean_head || { echo 'There are uncommitted changes'; exit 1; }
diff --git a/bin/kind b/bin/kind
index 0e4f6d13f..9ac710ee3 100755
--- a/bin/kind
+++ b/bin/kind
@@ -2,7 +2,7 @@
 
 set -eu
 
-kindversion=v0.7.0
+kindversion=v0.8.1
 
 bindir=$( cd "${0%/*}" && pwd )
 targetbin=$( cd "$bindir"/.. && pwd )/target/bin
diff --git a/bin/kind-load b/bin/kind-load
index f175f646d..1df3b7478 100755
--- a/bin/kind-load
+++ b/bin/kind-load
@@ -58,9 +58,9 @@ cluster=${1:-"kind"}
 
 bindir=$( cd "${0%/*}" && pwd )
 
-# shellcheck source=bin/_tag.sh
+# shellcheck source=_tag.sh
 . "$bindir"/_tag.sh
-# shellcheck source=bin/_docker.sh
+# shellcheck source=_docker.sh
 . "$bindir"/_docker.sh
 
 TAG=${TAG:-$(head_root_tag)}
diff --git a/bin/markdownlint b/bin/markdownlint
index ed8ba6f72..708677990 100755
--- a/bin/markdownlint
+++ b/bin/markdownlint
@@ -1,8 +1,6 @@
-#!/bin/bash
+#!/bin/sh -eu
 
-set -eu
-
-bindir=$( cd "${BASH_SOURCE[0]%/*}" && pwd )
+bindir=$( cd "${0%/*}" && pwd )
 rootdir=$( cd "$bindir"/.. && pwd )
 markdownlintbin="$rootdir/node_modules/.bin/markdownlint"
 
@@ -10,7 +8,7 @@ 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"
+        echo 'Error: npm required to install markdownlint command'
         exit 1
     fi
     npm install markdownlint-cli@$markdownlint_version
diff --git a/bin/markdownlint-all b/bin/markdownlint-all
index e96b65a9c..e1d83ee65 100755
--- a/bin/markdownlint-all
+++ b/bin/markdownlint-all
@@ -1,6 +1,6 @@
-#!/bin/bash
+#!/bin/sh -eu
 
-bindir=$( cd "${BASH_SOURCE[0]%/*}" && pwd )
+bindir=$( cd "${0%/*}" && pwd )
 
 "$bindir"/markdownlint ./*.md
 "$bindir"/markdownlint ./**/*.md --ignore node_modules/
diff --git a/bin/shellcheck-all b/bin/shellcheck-all
new file mode 100755
index 000000000..cec07eaf0
--- /dev/null
+++ b/bin/shellcheck-all
@@ -0,0 +1,22 @@
+#!/bin/sh -eu
+
+bindir=$( cd "${0%/*}" && pwd )
+rootdir=$( cd "$bindir"/.. && pwd )
+
+# TODO: Each file excluded from the shell script search result below is
+# excluded from shellcheck because it fails. As we fix files we can remove them
+# from this exclusion list. And this comment when all files pass shellcheck.
+
+# For more information on shellcheck failures:
+# https://github.com/koalaman/shellcheck/wiki/Checks
+
+# We want the word splitting for the shellcheck arguments
+# shellcheck disable=SC2046
+"$bindir"/shellcheck -x -P "$bindir" $(find "$rootdir" -type f \
+		! -path "$bindir"/docker-build-proxy \
+		! -path "$bindir"/_log.sh \
+		! -path "$bindir"/test-cleanup \
+		! -path "$bindir"/_test-run.sh \
+		! -path "$rootdir"/cni-plugin/deployment/scripts/install-cni.sh \
+		! -path "$rootdir"/.git/hooks/\*.sample \
+		| while read -r f; do [ "$(file -b --mime-type "$f")" = 'text/x-shellscript' ] && printf '%s\0' "$f"; done | xargs -0)
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..0f51d8e10 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{
@@ -1237,8 +1254,8 @@ func (hc *HealthChecker) allCategories() []category {
 			id: LinkerdMulticlusterChecks,
 			checkers: []checker{
 				{
-					description: "service mirror controller exists",
-					hintAnchor:  "l5d-smc-existence",
+					description: "service mirror controller is running",
+					hintAnchor:  "l5d-multicluster-service-mirror-running",
 					fatal:       true,
 					check: func(context.Context) error {
 						return hc.checkServiceMirrorController()
@@ -1246,30 +1263,47 @@ func (hc *HealthChecker) allCategories() []category {
 				},
 				{
 					description: "service mirror controller ClusterRoles exist",
-					hintAnchor:  "l5d-smc-existence-cr",
-					fatal:       true,
+					hintAnchor:  "l5d-multicluster-cluster-role-exist",
 					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"}
 					},
 				},
 				{
 					description: "service mirror controller ClusterRoleBindings exist",
-					hintAnchor:  "l5d-smc-existence-crb",
-					fatal:       true,
+					hintAnchor:  "l5d-multicluster-cluster-role-binding-exist",
 					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-multicluster-role-exist",
+					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-multicluster-role-binding-exist",
+					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"}
 					},
 				},
 				{
 					description: "service mirror controller ServiceAccounts exist",
-					hintAnchor:  "l5d-smc-existence-sa",
-					fatal:       true,
+					hintAnchor:  "l5d-multicluster-service-account-exist",
 					check: func(context.Context) error {
 						if hc.Options.ShouldCheckMulticluster {
 							return hc.checkServiceAccounts([]string{linkerdServiceMirrorComponentName}, hc.serviceMirrorNs, hc.serviceMirrorComponentsSelector())
@@ -1279,8 +1313,7 @@ func (hc *HealthChecker) allCategories() []category {
 				},
 				{
 					description: "service mirror controller has required permissions",
-					hintAnchor:  "l5d-smc-local-rbac-existence",
-					fatal:       true,
+					hintAnchor:  "l5d-multicluster-local-rbac-correct",
 					check: func(context.Context) error {
 						return hc.checkServiceMirrorLocalRBAC()
 					},
@@ -1288,23 +1321,20 @@ 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()
 					},
 				},
 				{
 					description: "all remote cluster gateways are alive",
-					hintAnchor:  "l5d-remote-gateways-alive",
-					fatal:       true,
+					hintAnchor:  "l5d-multicluster-remote-gateways-alive",
 					check: func(ctx context.Context) error {
 						return hc.checkRemoteClusterGatewaysHealth(ctx)
 					},
 				},
 				{
 					description: "clusters share trust anchors",
-					hintAnchor:  "l5d-clusters-share-anchors",
-					fatal:       true,
+					hintAnchor:  "l5d-multicluster-clusters-share-anchors",
 					check: func(ctx context.Context) error {
 						return hc.checkRemoteClusterAnchors()
 					},
@@ -1619,38 +1649,54 @@ 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)
+			return fmt.Errorf("Could not obtain service mirror ClusterRole %s: %s", linkerdServiceMirrorClusterRoleName, 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 : %s", linkerdServiceMirrorRoleName, 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 %s has %d policy rules, expected %d", clusterRole.Name, 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: %s", clusterRole.Name, 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 %s has %d policy rules, expected %d", role.Name, 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: %s", role.Name, err))
+			}
 		}
 
 		if len(errors) > 0 {
-			return fmt.Errorf(strings.Join(errors, "\n\t"))
+			return fmt.Errorf(strings.Join(errors, "\n"))
 		}
 
 		return nil
@@ -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(),
 	}
 }