Repository: linkerd/linkerd2
Pull request #6188 (2021-05-27 10:32:46)

Introduce OpenTelemetry collector instead of OpenCensus
Introduce OpenTelemetry collector instead of OpenCensus.

Implements: #6159

Replaces: #6167

Signed-off-by: aatarasoff <aatarasoff@gmail.com>


Patch:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 33ed139d0..a6cd468f4 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -3,6 +3,8 @@ on:
   push:
     tags:
     - "*"
+permissions: 
+  contents: read
 env:
   GH_ANNOTATION: true
 jobs:
@@ -132,7 +134,7 @@ jobs:
         bin/docker-pull-binaries $TAG
         # Validate the CLI version matches the current build tag.
         [[ "$TAG" == "$($CMD version --short --client)" ]]
-        bin/tests --images skip --name ${{ matrix.integration_test }} "$CMD"
+        bin/tests --images preload --name ${{ matrix.integration_test }} "$CMD"
   arm64_integration_tests:
     name: ARM64 integration tests
     timeout-minutes: 60
@@ -173,7 +175,7 @@ jobs:
       #if: startsWith(github.ref, 'refs/tags/stable')
       env:
         RUN_ARM_TEST: 1
-      run: bin/tests --name deep --images skip --skip-cluster-create "$CMD"
+      run: bin/tests --name deep --images preload --skip-cluster-create "$CMD"
     - name: CNI tests
       #if: startsWith(github.ref, 'refs/tags/stable')
       run: |
@@ -218,6 +220,8 @@ jobs:
     if: startsWith(github.ref, 'refs/tags/stable') || startsWith(github.ref, 'refs/tags/edge')
     runs-on: ubuntu-20.04
     needs: [choco_pack]
+    permissions:
+      contents: write
     steps:
     - name: Checkout code
       uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
@@ -264,6 +268,8 @@ jobs:
     if: startsWith(github.ref, 'refs/tags/stable') || startsWith(github.ref, 'refs/tags/edge')
     runs-on: ubuntu-20.04
     needs: [gh_release]
+    permissions:
+      contents: write
     steps:
     - name: Create linkerd/website repository dispatch event
       # peter-evans/repository-dispatch@v1
diff --git a/.github/workflows/static_checks.yml b/.github/workflows/static_checks.yml
index ebdc93056..faf53a86e 100644
--- a/.github/workflows/static_checks.yml
+++ b/.github/workflows/static_checks.yml
@@ -8,6 +8,8 @@ on:
     - 'web/app/package.json'
     branches:
     - main
+permissions: 
+  contents: read
 jobs:
   go_lint:
     name: Go lint
diff --git a/.proxy-version b/.proxy-version
index fb741f89d..54ba55334 100644
--- a/.proxy-version
+++ b/.proxy-version
@@ -1,1 +1,1 @@
-v2.144.0
\ No newline at end of file
+v2.147.0
diff --git a/ADOPTERS.md b/ADOPTERS.md
index 87340f747..5f4d64408 100644
--- a/ADOPTERS.md
+++ b/ADOPTERS.md
@@ -8,10 +8,12 @@
 - [AppddictionStudio](https://appddicitonstudio.com)
 - [Attest](https://www.askattest.com)
 - [Bink](https://bink.com)
+- [Boomin](https://www.boomin.com)
 - [Buoyant](https://buoyant.io)
 - [Candide](https://candidegardening.com)
 - [Celonis](https://celonis.com)
 - [CloverHealth](https://www.cloverhealth.com/)
+- [Cohere](https://cohere.ai/)
 - [Commonbond](https://www.commonbond.co/)
 - [Doji](https://www.doji.cn/)
 - [Expedia](https://www.expedia.com)
diff --git a/BUILD.md b/BUILD.md
index 0259d872f..5eac2ab36 100644
--- a/BUILD.md
+++ b/BUILD.md
@@ -28,7 +28,7 @@ about testing from source can be found in the [TEST.md](TEST.md) guide.
     - [First time setup](#first-time-setup)
     - [Run web standalone](#run-web-standalone)
     - [Webpack dev server](#webpack-dev-server)
-    - [Javascript dependencies](#javascript-dependencies)
+    - [JavaScript dependencies](#javascript-dependencies)
     - [Translations](#translations)
   - [Rust](#rust)
     - [Docker](#docker)
@@ -73,7 +73,7 @@ its extensions are written in Go. The dashboard UI is a React application.
   - [`web`](web): Provides a UI dashboard to view and drive the control plane.
 - [`multicluster extension`](multicluster)
   - [`linkerd-gateway`]: Accepts requests from other clusters and forwards them
-    to the appropriate destionation in the local cluster.
+    to the appropriate destination in the local cluster.
   - [`linkerd-service-mirror-xxx`](multicluster/service-mirror): Controller
     observing the labeling of exported services in the target cluster, each one
     for which it will create a mirrored service in the local cluster.
@@ -375,13 +375,13 @@ To develop with a webpack dev server:
       on :8185
     - `grafana` is port-forwarded from the Kubernetes cluster via `kubectl` on
       :3000
-    - `metrics-api` is port-forwarded from the Kubernets cluster via `kubectl`
+    - `metrics-api` is port-forwarded from the Kubernetes cluster via `kubectl`
       on :8085
 
 2. Go to [http://localhost:7777](http://localhost:7777) to see everything
    running.
 
-#### Javascript dependencies
+#### JavaScript dependencies
 
 To add a JS dependency:
 
diff --git a/CHANGES.md b/CHANGES.md
index d5ea14f6e..155f21eb5 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,97 @@
 # Changes
 
+## edge-21.6.2
+
+This release fixes a problem with the HTTP body buffering that was added
+to support gRPC retries. Now, only requests with a retry configuration
+are buffered (and only when their bodies are less than 64KB).
+
+Additionally, an issue with the outbound ingress-mode proxy where forwarded
+HTTP clients could fail to detect when the target pod was deleted, causing
+connections to retry forever has been fixed. This only impacted traffic
+forwarded directly to pod IPs and not load balanced services.
+
+Finally, this release also includes some fixes in the CLI and dashboard.
+
+* Added a new check that verifies if the opaque ports annotation is
+  misconfigured on services or pods (thanks @migue!)
+* Added support for resource aware completion for core linkerd command
+* Fixed an issue where `namespace` resource was erroneously being shown
+  in the dashboard's topology graph
+* Added uninstall command support for legacy extension installs
+* Updated the proxy to only buffer request bodies when a request can be retried
+* Updated the proxy to prevent buffering indefinitely on requests
+  when endpoints are updated in ingress mode
+* Fixed spelling mistakes across various files in the project
+  (thanks @jsoref!)
+
+## edge-21.6.1
+
+This release adds support for retrying HTTP/2 requests with small (<64KB)
+message bodies, allowing the proxy to properly buffer message bodies when
+responses are classified as a failure. Documentation on how to configure
+retries can be found [here](https://linkerd.io/2.10/tasks/configuring-retries/).
+
+This release also modifies the proxy's identity subsystem to instantiate a
+client on-demand so client connections are not retained continually. Also
+included in this release are various bug fixes and improvements as well as
+expanding support for resource-aware tab completion in the jaeger and
+multicluster CLI extensions.
+
+* Added support for specifying a `gateway-port` flag for the `multicluster link`
+  command (thanks @psmit!)
+* Added support for Kubernetes resource aware tab completion for `jaeger` and
+  `multicluster` commands
+* Fixed an issue where `viz`, `jaeger` and `multicluster` extensions could not
+  be installed on `PodSecurityPolicy`-enabled clusters
+* Fixed an issue where `linkerd check --proxy` could incorrectly report
+  out-of-date proxy versions caused by incorrect regex (thanks @aryan9600!)
+* Added support for the proxy to retry HTTP/2 requests with message bodies
+  <= 64KB
+* Modified the proxy's controller stack to create new client connections
+  on-demand
+* Fixed Viz's `uninstall` command to remove viz installations that used the
+  legacy `linkerd.io/extension: linkerd-viz` label (thanks @jsoref!)
+* Expanded the "linkerd-existence" health check to also check for the
+  destination pod readiness
+
+## edge-21.5.3
+
+This edge release contains various improvements to the Viz and Jaeger install
+charts, along with bug fixes in the CLI, and destination. This release also
+adds kubernetes aware autocompletion to all viz commands, along with
+ServiceProfiles to be part of the default `viz install`.
+
+Finally, the proxy has been updated to continue supporting requests without
+`l5d-dst-override` in ingress-mode proxies, to no longer include query parameters
+in the OpenCensus trace spans, and to prevent timeouts with controller clients
+of components with more than one replica.
+
+* Separated protocol hint setting from H2 upgrades in destination profile
+  response, thus preventing `hint.OpaqueTransport` field from not being set when
+  H2 upgrades are disabled
+* Updated OpenCensus trace spans for HTTP requests to no longer include query
+  parameters (thanks @aatarasoff!)
+* Reverted [linkerd/linkerd2-proxy#992](https://github.com/linkerd/linkerd2-proxy/pull/992)
+  to support requests without `l5d-dst-override` in ingress-mode proxies
+* Fixed an issue in the proxy to prevent timeouts with controller clients
+  of components with more than one replica
+* Fixed `linkerd check --proxy` failure with pods that are part of Jobs
+* Updated `viz install` to also include ServiceProfiles of its components.
+  As a side-effect, `linkerd diagnostics install-sp` cmd has been removed
+* Added support for Kubernetes resource aware tab completion for all
+  viz commands
+* Updated destination to prefer `ServiceProfile.dstOverrides` over
+  `TrafficSplit` when both are present for a service
+* Added toggle flags for `collector` and `jaeger` components in the
+  jaeger extension (thanks @tarvip!)
+* Added support for setting `nodeselector`, `toleration` fields for components
+  in the Viz extension (thanks @aatarasoff!)
+* Fixed a templating issue in Viz, making `podAnnotations` field
+  work with prometheus
+* Updated Golang version to 1.16.4
+* Removed unnecessary `--addon-overwrite` flag in `linkerd upgrade`
+
 ## edge-21.5.2
 
 This edge release updates the proxy-init container to check whether the iptables
@@ -193,7 +285,7 @@ if you are upgrading from `2.9.x` or below versions.
     longer draggable.
   * Updated dashboard build to use webpack v5
   * Added CA certs to the Viz extension's metrics-api container so
-    that it can validate the certifcate of an external Prometheus
+    that it can validate the certificate of an external Prometheus
   * Removed components from the control plane dashboard that now
     are part of the Viz extension
   * Changed web's base image from debian to scratch
@@ -253,13 +345,13 @@ Thanks to all our 2.10 users who helped discover these issues!
 * Modified the proxy-injector to add the opaque ports annotation to pods if
   their namespace has it set
 * Added CA certs to the Viz extension's `metrics-api` container so that it can
-  validate the certifcate of an external Prometheus
+  validate the certificate of an external Prometheus
 * Fixed an issue where inbound TLS detection from non-meshed workloads could
   break
 * Fixed an issue where the admin server's HTTP detection would fail and not
   recover; these are now handled gracefully and without logging warnings
 * Aligned the Helm installation heartbeat schedule to match that of the CLI
-* Fixed an issue with Multicluster's serivce mirror where it's endpoint repair
+* Fixed an issue with Multicluster's service mirror where it's endpoint repair
   retries were not properly rate limited
 * Removed components from the control plane dashboard that now are part of the
   Viz extension
@@ -637,7 +729,7 @@ the robustness of the opaque transport.
 * Changed opaque-port transport to be advertised via ALPN so that new proxies
   will not initiate opaque-transport connections to proxies from prior edge
   releases
-* Added inbound proxy transport metrics with `tls="passhtru"` when forwarding
+* Added inbound proxy transport metrics with `tls="passthru"` when forwarding
   non-mesh TLS connections
 * Thanks to @hs0210 for adding new unit tests!
 
@@ -786,7 +878,7 @@ async runtime.
   `linkerd-jaeger` extension is working correctly
 * Added new `linkerd jaeger uninstall` CLI command to print the `linkerd-jaeger`
   extension's resources so that they can be piped into `kubectl delete`
-* Fixed an issue where the `linkerd-cni` daemgitonset may not be installed on all
+* Fixed an issue where the `linkerd-cni` daemonset may not be installed on all
   intended nodes, due to missing tolerations to the `linkerd-cni` Helm chart
   (thanks @rish-onesignal!)
 * Fixed an issue where the `tap` APIServer would not refresh its certs
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 947cfb1d2..3f4b621aa 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -127,7 +127,7 @@ Describe any implementation changes which are particularly complex or
 unintuitive.
 
 List any follow-up work that will need to be done in a future PR and link to any
-relevant Github issues.
+relevant GitHub issues.
 
 #### Validation ####
 
diff --git a/Dockerfile-proxy b/Dockerfile-proxy
index 8650b118a..ae7a944ba 100644
--- a/Dockerfile-proxy
+++ b/Dockerfile-proxy
@@ -1,4 +1,4 @@
-ARG RUNTIME_IMAGE=debian:buster-20210208-slim
+ARG RUNTIME_IMAGE=gcr.io/distroless/cc:nonroot
 ARG BUILDPLATFORM=linux/amd64
 
 # Precompile key slow-to-build dependencies
@@ -10,8 +10,7 @@ RUN go mod download
 ARG TARGETARCH
 RUN ./bin/install-deps $TARGETARCH
 
-FROM --platform=$BUILDPLATFORM $RUNTIME_IMAGE as fetch
-RUN apt-get update && apt-get install -y ca-certificates curl jq
+FROM --platform=$BUILDPLATFORM curlimages/curl:7.76.1 as fetch
 WORKDIR /build
 COPY bin/fetch-proxy bin/fetch-proxy
 COPY .proxy-version proxy-version
@@ -38,9 +37,8 @@ COPY --from=fetch /build/proxy-version /usr/lib/linkerd/linkerd2-proxy-version.t
 COPY --from=fetch /build/linkerd2-proxy /usr/lib/linkerd/linkerd2-proxy
 COPY --from=fetch /build/linkerd-await /usr/lib/linkerd/linkerd-await
 COPY --from=golang /out/proxy-identity /usr/lib/linkerd/linkerd2-proxy-identity
-COPY proxy-identity/run-proxy.sh /usr/bin/linkerd2-proxy-run
 ARG LINKERD_VERSION
 ENV LINKERD_CONTAINER_VERSION_OVERRIDE=${LINKERD_VERSION}
 ENV LINKERD2_PROXY_LOG=warn,linkerd=info
 ENV LINKERD2_PROXY_LOG_FORMAT=plain
-ENTRYPOINT ["/usr/bin/linkerd2-proxy-run"]
+ENTRYPOINT ["/usr/lib/linkerd/linkerd2-proxy-identity"]
diff --git a/bin/helm-build b/bin/helm-build
index bfeb90dc8..a8f2fce8e 100755
--- a/bin/helm-build
+++ b/bin/helm-build
@@ -71,6 +71,8 @@ if [ "$1" = package ]; then
     mv "$rootdir"/target/helm/index-pre.yaml "$rootdir"/target/helm/index-pre-"$version".yaml
     "$bindir"/helm repo index --url "https://helm.linkerd.io/$repo/" --merge "$rootdir"/target/helm/index-pre-"$version".yaml "$rootdir"/target/helm
 
+    cp "$rootdir"/charts/linkerd2/artifacthub-repo.yml "$rootdir"/target/helm/
+
     # restore version in Values files
     setValues "$fullVersion" "linkerdVersionValue"
 fi
diff --git a/bin/image-load b/bin/image-load
index aabf41d21..6d49af209 100755
--- a/bin/image-load
+++ b/bin/image-load
@@ -4,61 +4,98 @@ set -eo pipefail
 
 kind=""
 k3d=""
+cluster=""
 archive=""
+preload=""
+images_default=(proxy controller web metrics-api grafana debug cni-plugin jaeger-webhook tap)
+images=()
+
+usage() {
+  printf "Load into KinD/k3d the referred Linkerd images. If no images are specified all of them are loaded (%s)\n" "${images_default[*]}"
+  echo ""
+  echo "Usage:"
+  echo "    bin/image-load [--kind] [--k3d] [--cluster name] [--preload] [--archive] [image] [image]..."
+  echo ""
+  echo "Examples:"
+  echo ""
+  echo "    # Load all the images from the local docker instance into KinD"
+  echo "    bin/image-load"
+  echo ""
+  echo "    # Load only the proxy and controller images into k3d"
+  echo "    bin/image-load --k3d proxy controller"
+  echo ""
+  echo "    # Load all the images from tar files located under the 'image-archives' directory into k3d"
+  echo "    bin/image-load --k3d --archive"
+  echo ""
+  echo "Available Commands:"
+  echo "    --cluster: target cluster name (defaults to 'k3s-default' under k3d, and 'kind' under KinD)."
+  echo "    --kind: use a KinD cluster (default)."
+  echo "    --k3d: use a k3d cluster."
+  echo "    --preload: pull the docker images from a public registry prior to loading them into the cluster, which appears to be faster than having k3d pulling them itself."
+  echo "    --archive: load the images from local .tar files in the current directory."
+}
 
 while :
 do
   case $1 in
     -h|--help)
-      echo "Load into KinD/k3d the images for Linkerd's proxy, controller, metrics-api, web, grafana, cli-bin, tap, debug and cni-plugin."
-      echo ""
-      echo "Usage:"
-      echo "    bin/image-load [--kind] [--k3d] [--archive]"
-      echo ""
-      echo "Examples:"
-      echo ""
-      echo "    # Load images from the local docker instance into KinD"
-      echo "    bin/image-load"
-      echo ""
-      echo "    # Load images from tar files located under the 'image-archives' directory into k3d"
-      echo "    bin/image-load --k3d --archive"
-      echo ""
-      echo "Available Commands:"
-      echo "    --kind: use a KinD cluster (default)."
-      echo "    --k3d: use a k3d cluster."
-      echo "    --archive: load the images from local .tar files in the current directory."
+      usage
       exit 0
       ;;
+    --cluster)
+      cluster=$2
+      shift
+      ;;
     --kind)
       kind=1
       ;;
     --k3d)
       k3d=1
       ;;
+    --preload)
+      preload=1
+      ;;
     --archive)
       archive=1
       ;;
     *)
-      break
+      if [ -z "$1" ]; then
+        break
+      fi
+      if echo "$1" | grep -q '^-.*' ; then
+        echo "Unexpected flag: $1" >&2
+        usage
+        exit 1
+      fi
+      images+=("$1")
   esac
   shift
 done
 
+if [ ${#images[@]} -eq 0 ]; then
+  images=("${images_default[@]}")
+fi
+
 bindir=$( cd "${0%/*}" && pwd )
 
 if [ -n "$k3d" ]; then
   if [ -n "$kind" ]; then
     echo $k3d
     echo "Error: --kind and --k3d can't be used simultaneously" >&2
+    usage
     exit 1
   fi
-  cluster=${1:-"k3s-default"}
+  if [ -z "$cluster" ]; then
+    cluster="k3s-default"
+  fi
   bin="$bindir"/k3d
   image_sub_cmd=(image import -c "$cluster")
 else
   kind=1
-  cluster=${1:-"kind"}
   bin="$bindir"/kind
+  if [ -z "$cluster" ]; then
+    cluster="kind"
+  fi
   if [ $archive ]; then
     image_sub_cmd=(load image-archive --name "$cluster")
   else
@@ -79,19 +116,23 @@ fi
 "$bin" version
 
 rm -f load_fail
-for img in proxy controller web metrics-api grafana debug cni-plugin jaeger-webhook tap; do
-  printf 'Importing %s...\n' $img
+for i in "${!images[@]}"; do
   if [ $archive ]; then
-    param="image-archives/$img.tar"
+    param="image-archives/${images[$i]}.tar"
   else
-    param="$DOCKER_REGISTRY/$img:$TAG"
+    param="$DOCKER_REGISTRY/${images[$i]}:$TAG"
+    if [ $preload ]; then
+      "$bindir"/docker pull -q "$param" || (echo "Error pulling image $param"; touch load_fail) &
+    fi
   fi
 
   if [ -n "$kind" ]; then
+    printf 'Importing %s...\n' "${images[$i]}"
     "$bin" "${image_sub_cmd[@]}" "${param[@]}" || touch load_fail &
   else
     # "k3d image import" commands don't behave well when parallelized
-    "$bin" "${image_sub_cmd[@]}" "${param[@]}"
+    # but all the images can be loaded in a single invocation
+    images[$i]=$param
   fi
 done
 
@@ -101,3 +142,8 @@ if [ -f load_fail ]; then
   rm load_fail
   exit 1
 fi
+
+if [ -n "$k3d" ]; then
+  printf 'Importing %s...\n' "${images[@]}"
+  "$bin" "${image_sub_cmd[@]}" "${images[@]}"
+fi
diff --git a/bin/install-pr b/bin/install-pr
index 0fd289d72..b06a7f443 100755
--- a/bin/install-pr
+++ b/bin/install-pr
@@ -102,7 +102,7 @@ then
   then
     distro="kind"
   fi
-  "$bindir"/image-load --"$distro" --archive "$cluster"
+  "$bindir"/image-load --"$distro" --archive --cluster "$cluster"
 else
   for image in cni-plugin controller debug grafana proxy web
   do
diff --git a/charts/linkerd2-cni/OWNERS b/charts/linkerd2-cni/OWNERS
deleted file mode 100644
index 4efc13328..000000000
--- a/charts/linkerd2-cni/OWNERS
+++ /dev/null
@@ -1,8 +0,0 @@
-approvers:
-- grampelberg
-- alpeb
-- ihcsim
-reviewers:
-- grampelberg
-- alpeb
-- ihcsim
\ No newline at end of file
diff --git a/charts/linkerd2/OWNERS b/charts/linkerd2/OWNERS
deleted file mode 100644
index 4efc13328..000000000
--- a/charts/linkerd2/OWNERS
+++ /dev/null
@@ -1,8 +0,0 @@
-approvers:
-- grampelberg
-- alpeb
-- ihcsim
-reviewers:
-- grampelberg
-- alpeb
-- ihcsim
\ No newline at end of file
diff --git a/charts/linkerd2/README.md b/charts/linkerd2/README.md
index b799c893e..f7b7cfacc 100644
--- a/charts/linkerd2/README.md
+++ b/charts/linkerd2/README.md
@@ -84,7 +84,7 @@ helm install \
 
 ## Get involved
 
-* Check out Linkerd's source code at [Github][linkerd2].
+* Check out Linkerd's source code at [GitHub][linkerd2].
 * Join Linkerd's [user mailing list][linkerd-users], [developer mailing
   list][linkerd-dev], and [announcements mailing list][linkerd-announce].
 * Follow [@linkerd][twitter] on Twitter.
@@ -188,8 +188,8 @@ Kubernetes: `>=1.16.0-0`
 | proxy.uid | int | `2102` | User id under which the proxy runs |
 | proxy.waitBeforeExitSeconds | int | `0` | If set the proxy sidecar will stay alive for at least the given period before receiving SIGTERM signal from Kubernetes but no longer than pod's `terminationGracePeriodSeconds`. See [Lifecycle hooks](https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks) for more info on container lifecycle hooks. |
 | proxyInit.closeWaitTimeoutSecs | int | `0` |  |
-| proxyInit.ignoreInboundPorts | string | `""` | Default set of inbound ports to skip via itpables |
-| proxyInit.ignoreOutboundPorts | string | `""` | Default set of outbound ports to skip via itpables |
+| proxyInit.ignoreInboundPorts | string | `""` | Default set of inbound ports to skip via iptables |
+| proxyInit.ignoreOutboundPorts | string | `""` | Default set of outbound ports to skip via iptables |
 | proxyInit.image.name | string | `"cr.l5d.io/linkerd/proxy-init"` | Docker image for the proxy-init container |
 | proxyInit.image.pullPolicy | string | imagePullPolicy | Pull policy for the proxy-init container Docker image |
 | proxyInit.image.version | string | `"v1.3.12"` | Tag for the proxy-init container Docker image |
diff --git a/charts/linkerd2/README.md.gotmpl b/charts/linkerd2/README.md.gotmpl
index 2107fb906..e90d4c370 100644
--- a/charts/linkerd2/README.md.gotmpl
+++ b/charts/linkerd2/README.md.gotmpl
@@ -82,7 +82,7 @@ helm install \
 
 ## Get involved
 
-* Check out Linkerd's source code at [Github][linkerd2].
+* Check out Linkerd's source code at [GitHub][linkerd2].
 * Join Linkerd's [user mailing list][linkerd-users], [developer mailing
   list][linkerd-dev], and [announcements mailing list][linkerd-announce].
 * Follow [@linkerd][twitter] on Twitter.
diff --git a/charts/linkerd2/artifacthub-repo.yml b/charts/linkerd2/artifacthub-repo.yml
new file mode 100644
index 000000000..9bcacc411
--- /dev/null
+++ b/charts/linkerd2/artifacthub-repo.yml
@@ -0,0 +1,14 @@
+#repositoryID: The ID of the Artifact Hub repository where the packages will be published to (optional, but it enables verified publisher)
+owners:
+  - name: olix0r
+    email: ver@buoyant.io
+  - name: alpeb
+    email: alejandro@buoyant.io
+  - name: adleong
+    email: alex@buoyant.io
+  - name: hawkw
+    email: eliza@buoyant.io
+  - name: kleimkuhler
+    email: kevinl@buoyant.io
+  - name: pothulapati
+    email: tarun@buoyant.io
diff --git a/charts/linkerd2/values.yaml b/charts/linkerd2/values.yaml
index 08ba652be..16fde230b 100644
--- a/charts/linkerd2/values.yaml
+++ b/charts/linkerd2/values.yaml
@@ -111,9 +111,9 @@ proxy:
 
 # proxy-init configuration
 proxyInit:
-  # -- Default set of inbound ports to skip via itpables
+  # -- Default set of inbound ports to skip via iptables
   ignoreInboundPorts: ""
-  # -- Default set of outbound ports to skip via itpables
+  # -- Default set of outbound ports to skip via iptables
   ignoreOutboundPorts: ""
   image:
     # -- Docker image for the proxy-init container
diff --git a/cli/cmd/check.go b/cli/cmd/check.go
index c2edae1c1..a1eb3c3ca 100644
--- a/cli/cmd/check.go
+++ b/cli/cmd/check.go
@@ -11,6 +11,7 @@ import (
 
 	"github.com/linkerd/linkerd2/cli/flag"
 	charts "github.com/linkerd/linkerd2/pkg/charts/linkerd2"
+	pkgcmd "github.com/linkerd/linkerd2/pkg/cmd"
 	"github.com/linkerd/linkerd2/pkg/healthcheck"
 	"github.com/linkerd/linkerd2/pkg/k8s"
 	"github.com/linkerd/linkerd2/pkg/version"
@@ -139,6 +140,9 @@ non-zero exit code.`,
 
 	cmd.AddCommand(newCmdCheckConfig(options))
 
+	pkgcmd.ConfigureNamespaceFlagCompletion(cmd, []string{"namespace"},
+		kubeconfigPath, impersonate, impersonateGroup, kubeContext)
+
 	return cmd
 }
 
@@ -182,12 +186,12 @@ func configureAndRunChecks(cmd *cobra.Command, wout io.Writer, werr io.Writer, s
 			if options.dataPlaneOnly {
 				checks = append(checks, healthcheck.LinkerdDataPlaneChecks)
 				checks = append(checks, healthcheck.LinkerdIdentityDataPlane)
+				checks = append(checks, healthcheck.LinkerdOpaquePortsDefinitionChecks)
 			} else {
 				checks = append(checks, healthcheck.LinkerdControlPlaneVersionChecks)
 			}
 			checks = append(checks, healthcheck.LinkerdCNIPluginChecks)
 			checks = append(checks, healthcheck.LinkerdHAChecks)
-
 		}
 	}
 
diff --git a/cli/cmd/endpoints.go b/cli/cmd/endpoints.go
index 09977f4bf..30cd5716f 100644
--- a/cli/cmd/endpoints.go
+++ b/cli/cmd/endpoints.go
@@ -17,6 +17,7 @@ import (
 	destinationPb "github.com/linkerd/linkerd2-proxy-api/go/destination"
 	netPb "github.com/linkerd/linkerd2-proxy-api/go/net"
 	"github.com/linkerd/linkerd2/controller/api/destination"
+	pkgcmd "github.com/linkerd/linkerd2/pkg/cmd"
 	"github.com/linkerd/linkerd2/pkg/k8s"
 	log "github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
@@ -116,6 +117,8 @@ destination.`,
 
 	cmd.PersistentFlags().StringVarP(&options.outputFormat, "output", "o", options.outputFormat, fmt.Sprintf("Output format; one of: \"%s\" or \"%s\"", tableOutput, jsonOutput))
 
+	pkgcmd.ConfigureOutputFlagCompletion(cmd)
+
 	return cmd
 }
 
diff --git a/cli/cmd/identity.go b/cli/cmd/identity.go
index 84a32750e..3b4724efd 100644
--- a/cli/cmd/identity.go
+++ b/cli/cmd/identity.go
@@ -47,16 +47,38 @@ func newCmdIdentity() *cobra.Command {
 		Use:   "identity [flags] (PODS)",
 		Short: "Display the certificate(s) of one or more selected pod(s)",
 		Long: `Display the certificate(s) of one or more selected pod(s).
-		
+
 This command initiates a port-forward to a given pod or a set of pods and fetches the TLS certificate.
 		`,
-		Example: ` 
+		Example: `
  # Get certificate from pod foo-bar in the default namespace.
  linkerd identity foo-bar
-		
+
  # Get certificate from all pods with the label name=nginx
  linkerd identity -l name=nginx
 		`,
+		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+			k8sAPI, err := k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, impersonateGroup, 0)
+			if err != nil {
+				return nil, cobra.ShellCompDirectiveError
+			}
+
+			if options.namespace == "" {
+				options.namespace = pkgcmd.GetDefaultNamespace(kubeconfigPath, kubeContext)
+			}
+
+			cc := k8s.NewCommandCompletion(k8sAPI, options.namespace)
+
+			// insert pod resource type as first argument to suggest
+			// pod resources only
+			args = append([]string{k8s.Pod}, args...)
+			results, err := cc.Complete(args, toComplete)
+			if err != nil {
+				return nil, cobra.ShellCompDirectiveError
+			}
+
+			return results, cobra.ShellCompDirectiveDefault
+		},
 		RunE: func(cmd *cobra.Command, args []string) error {
 			if options.namespace == "" {
 				options.namespace = pkgcmd.GetDefaultNamespace(kubeconfigPath, kubeContext)
@@ -104,6 +126,10 @@ This command initiates a port-forward to a given pod or a set of pods and fetche
 
 	cmd.PersistentFlags().StringVarP(&options.namespace, "namespace", "n", options.namespace, "Namespace of the pod")
 	cmd.PersistentFlags().StringVarP(&options.selector, "selector", "l", options.selector, "Selector (label query) to filter on, supports ‘=’, ‘==’, and ‘!=’ ")
+
+	pkgcmd.ConfigureNamespaceFlagCompletion(cmd, []string{"namespace"},
+		kubeconfigPath, impersonate, impersonateGroup, kubeContext)
+
 	return cmd
 }
 
diff --git a/cli/cmd/inject.go b/cli/cmd/inject.go
index 46bf14a5f..fa50cc675 100644
--- a/cli/cmd/inject.go
+++ b/cli/cmd/inject.go
@@ -198,7 +198,7 @@ func (rt resourceTransformerInject) transform(bytes []byte) ([]byte, []inject.Re
 
 	if rt.injectProxy {
 		// delete the inject annotation if present as its not needed in the manual case
-		// prevents injector from taking a different code path in the ignress mode
+		// prevents injector from taking a different code path in the ingress mode
 		delete(rt.overrideAnnotations, k8s.ProxyInjectAnnotation)
 		conf.AppendPodAnnotation(k8s.CreatedByAnnotation, k8s.CreatedByAnnotationValue())
 	} else if !rt.values.Proxy.IsIngress { // Add enabled annotation only if its not ingress mode to prevent overriding the annotation
diff --git a/cli/cmd/metrics.go b/cli/cmd/metrics.go
index 37f0609cd..362c9379e 100644
--- a/cli/cmd/metrics.go
+++ b/cli/cmd/metrics.go
@@ -108,6 +108,9 @@ func newCmdMetrics() *cobra.Command {
 
 	cmd.PersistentFlags().StringVarP(&options.namespace, "namespace", "n", options.namespace, "Namespace of resource")
 
+	pkgcmd.ConfigureNamespaceFlagCompletion(cmd, []string{"namespace"},
+		kubeconfigPath, impersonate, impersonateGroup, kubeContext)
+
 	return cmd
 }
 
diff --git a/cli/cmd/repair.go b/cli/cmd/repair.go
index d5adff9fc..932a6918f 100644
--- a/cli/cmd/repair.go
+++ b/cli/cmd/repair.go
@@ -24,7 +24,7 @@ var (
 	// 2.8 and below
 	repairNotApplicableVersionRegex = regexp.MustCompile(`^stable-2\.[0-8]\.([0-9]+)$`)
 
-	// repairApplicableVersionRegex matches versions 2.9 versions upto 2.9.4
+	// repairApplicableVersionRegex matches versions 2.9 versions up to 2.9.4
 	repairApplicableVersionRegex = regexp.MustCompile(`^stable-2\.9\.[0-4]$`)
 )
 
@@ -87,7 +87,7 @@ func repair(ctx context.Context, forced bool) error {
 			return fmt.Errorf("repair command is only applicable to 2.9 control-plane versions. Please try upgrading to the latest supported versions of Linkerd i.e 2.9.4 and above")
 		}
 
-		// Suggest 2.9.4 CLI version for all 2.9 server versions upto 2.9.4
+		// Suggest 2.9.4 CLI version for all 2.9 server versions up to 2.9.4
 		if repairApplicableVersionRegex.Match([]byte(serverVersion)) {
 			return fmt.Errorf("Please run the repair command with a `stable-2.9.4` CLI.\nRun `curl -sL https://run.linkerd.io/install | LINKERD2_VERSION=\"stable-2.9.4\" sh` to install the server version of the CLI")
 		}
diff --git a/cli/cmd/root.go b/cli/cmd/root.go
index 0f80c78c4..05f834170 100644
--- a/cli/cmd/root.go
+++ b/cli/cmd/root.go
@@ -10,6 +10,7 @@ import (
 	"github.com/linkerd/linkerd2/cli/flag"
 	jaeger "github.com/linkerd/linkerd2/jaeger/cmd"
 	multicluster "github.com/linkerd/linkerd2/multicluster/cmd"
+	pkgcmd "github.com/linkerd/linkerd2/pkg/cmd"
 	"github.com/linkerd/linkerd2/pkg/healthcheck"
 	viz "github.com/linkerd/linkerd2/viz/cmd"
 	log "github.com/sirupsen/logrus"
@@ -128,6 +129,13 @@ func init() {
 	RootCmd.AddCommand(deprecateCmd(viz.NewCmdStat()))
 	RootCmd.AddCommand(deprecateCmd(viz.NewCmdTap()))
 	RootCmd.AddCommand(deprecateCmd(viz.NewCmdTop()))
+
+	// resource-aware completion flag configurations
+	pkgcmd.ConfigureNamespaceFlagCompletion(
+		RootCmd, []string{"linkerd-namespace", "cni-namespace"},
+		kubeconfigPath, impersonate, impersonateGroup, kubeContext)
+
+	pkgcmd.ConfigureKubeContextFlagCompletion(RootCmd, kubeconfigPath)
 }
 
 func deprecateCmd(cmd *cobra.Command) *cobra.Command {
diff --git a/cli/cmd/uninstall.go b/cli/cmd/uninstall.go
index 8469136b7..53d53b80d 100644
--- a/cli/cmd/uninstall.go
+++ b/cli/cmd/uninstall.go
@@ -35,7 +35,7 @@ This command provides all Kubernetes namespace-scoped and cluster-scoped resourc
 			if !force {
 
 				var fail bool
-				// Retrtieve any installed extensions
+				// Retrieve any installed extensions
 				extensionNamespaces, err := k8sAPI.GetAllNamespacesWithExtensionLabel(cmd.Context())
 				if err != nil {
 					return err
@@ -82,7 +82,12 @@ This command provides all Kubernetes namespace-scoped and cluster-scoped resourc
 				}
 			}
 
-			err = pkgCmd.Uninstall(cmd.Context(), k8sAPI, k8s.ControllerNSLabel)
+			selector, err := pkgCmd.GetLabelSelector(k8s.ControllerNSLabel)
+			if err != nil {
+				return err
+			}
+
+			err = pkgCmd.Uninstall(cmd.Context(), k8sAPI, selector)
 			if err != nil {
 				fmt.Fprintln(os.Stderr, err)
 			}
diff --git a/cli/cmd/upgrade.go b/cli/cmd/upgrade.go
index 79cf64f99..d14d95cb2 100644
--- a/cli/cmd/upgrade.go
+++ b/cli/cmd/upgrade.go
@@ -260,7 +260,7 @@ func upgrade(ctx context.Context, k *k8s.KubernetesAPI, flags []flag.Flag, stage
 		return bytes.Buffer{}, err
 	}
 	// If there is no linkerd-config-overrides secret, assume we are upgrading
-	// from a verion of Linkerd prior to the introduction of this secret.  In
+	// from a version of Linkerd prior to the introduction of this secret.  In
 	// this case we load the values from the legacy linkerd-config configmap.
 	if values == nil {
 		values, err = loadStoredValuesLegacy(ctx, k)
diff --git a/controller/api/destination/endpoint_translator.go b/controller/api/destination/endpoint_translator.go
index 76d8f7064..c40a487ff 100644
--- a/controller/api/destination/endpoint_translator.go
+++ b/controller/api/destination/endpoint_translator.go
@@ -228,7 +228,7 @@ func (et *endpointTranslator) sendClientAdd(set watcher.AddressSet) {
 
 			skippedInboundPorts, skippedErr := getPodSkippedInboundPortsAnnotations(address.Pod)
 			if skippedErr != nil {
-				et.log.Errorf("failed getting ignored inbound ports annoatation for pod: %s", err)
+				et.log.Errorf("failed getting ignored inbound ports annotation for pod: %s", err)
 			}
 
 			wa, err = toWeightedAddr(address, opaquePorts, skippedInboundPorts, et.enableH2Upgrade, et.identityTrustDomain, et.controllerNS, et.log)
diff --git a/controller/api/destination/server.go b/controller/api/destination/server.go
index 535be5167..3418fdc7c 100644
--- a/controller/api/destination/server.go
+++ b/controller/api/destination/server.go
@@ -216,9 +216,6 @@ func (s *server) GetProfile(dest *pb.GetDestination, stream pb.Destination_GetPr
 					Namespace: pod.Namespace,
 					Name:      pod.Name,
 				}
-				if err != nil {
-					log.Errorf("failed to set opaque port annotation on pod: %s", err)
-				}
 				var ok bool
 				opaquePorts, ok, err = getPodOpaquePortsAnnotations(pod)
 				if err != nil {
diff --git a/controller/api/destination/traffic_split_adaptor.go b/controller/api/destination/traffic_split_adaptor.go
index 111facbee..0eee04967 100644
--- a/controller/api/destination/traffic_split_adaptor.go
+++ b/controller/api/destination/traffic_split_adaptor.go
@@ -53,7 +53,10 @@ func (tsa *trafficSplitAdaptor) publish() {
 	if tsa.profile != nil {
 		merged = *tsa.profile
 	}
-	if tsa.split != nil {
+
+	// Update only if `dstOverrides` is empty, to give preference
+	// to serviceprofiles over trafficsplits
+	if tsa.split != nil && len(merged.Spec.DstOverrides) == 0 {
 		overrides := []*sp.WeightedDst{}
 		for _, backend := range tsa.split.Spec.Backends {
 			dst := &sp.WeightedDst{
diff --git a/controller/api/destination/watcher/opaque_ports_watcher.go b/controller/api/destination/watcher/opaque_ports_watcher.go
index 02b828085..47b85ade0 100644
--- a/controller/api/destination/watcher/opaque_ports_watcher.go
+++ b/controller/api/destination/watcher/opaque_ports_watcher.go
@@ -83,7 +83,7 @@ func (opw *OpaquePortsWatcher) Subscribe(id ServiceID, listener OpaquePortsUpdat
 	return nil
 }
 
-// Unsubscribe unsubscries a listener from service.
+// Unsubscribe unsubscribes a listener from service.
 func (opw *OpaquePortsWatcher) Unsubscribe(id ServiceID, listener OpaquePortsUpdateListener) {
 	opw.Lock()
 	defer opw.Unlock()
diff --git a/jaeger/charts/linkerd-jaeger/README.md b/jaeger/charts/linkerd-jaeger/README.md
index dad473aa5..229f02f46 100644
--- a/jaeger/charts/linkerd-jaeger/README.md
+++ b/jaeger/charts/linkerd-jaeger/README.md
@@ -43,7 +43,7 @@ helm install linkerd/linkerd-jaeger
 
 ## Get involved
 
-* Check out Linkerd's source code at [Github][linkerd2].
+* Check out Linkerd's source code at [GitHub][linkerd2].
 * Join Linkerd's [user mailing list][linkerd-users], [developer mailing
   list][linkerd-dev], and [announcements mailing list][linkerd-announce].
 * Follow [@linkerd][twitter] on Twitter.
@@ -71,11 +71,11 @@ Kubernetes: `>=1.16.0-0`
 
 | Key | Type | Default | Description |
 |-----|------|---------|-------------|
+| collector.config | string | see `value.yaml` for actual configuration | OpenTelemetry Collector config, See the [Configuration docs](https://opentelemetry.io/docs/collector/configuration/) for more information |
 | collector.enabled | bool | `true` | Set to false to exclude collector installation |
-| collector.image.name | string | `"omnition/opencensus-collector"` |  |
+| collector.image.name | string | `"otel/opentelemetry-collector"` |  |
 | collector.image.pullPolicy | string | `"Always"` |  |
-| collector.image.version | string | `"0.1.11"` |  |
-| collector.jaegerAddr | string | `nil` | address of the jaeger backend to send traces to |
+| collector.image.version | string | `"0.27.0"` |  |
 | collector.nodeSelector | object | `{"beta.kubernetes.io/os":"linux"}` | NodeSelector section, See the [K8S documentation](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector) for more information |
 | collector.tolerations | string | `nil` | Tolerations section, See the [K8S documentation](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/) for more information |
 | installNamespace | bool | `true` | Set to false when installing in a custom namespace. |
@@ -85,6 +85,7 @@ Kubernetes: `>=1.16.0-0`
 | jaeger.image.version | string | `"1.19.2"` |  |
 | jaeger.nodeSelector | object | `{"beta.kubernetes.io/os":"linux"}` | NodeSelector section, See the [K8S documentation](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector) for more information |
 | jaeger.tolerations | string | `nil` | Tolerations section, See the [K8S documentation](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/) for more information |
+| linkerdNamespace | string | `"linkerd"` | Namespace of the Linkerd core control-plane install |
 | linkerdVersion | string | `"linkerdVersionValue"` |  |
 | namespace | string | `"linkerd-jaeger"` |  |
 | nodeSelector | object | `{"beta.kubernetes.io/os":"linux"}` | Default nodeSelector section, See the [K8S documentation](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector) for more information |
diff --git a/jaeger/charts/linkerd-jaeger/README.md.gotmpl b/jaeger/charts/linkerd-jaeger/README.md.gotmpl
index 3e1a15ff7..13cbfa572 100644
--- a/jaeger/charts/linkerd-jaeger/README.md.gotmpl
+++ b/jaeger/charts/linkerd-jaeger/README.md.gotmpl
@@ -41,7 +41,7 @@ helm install linkerd/linkerd-jaeger
 
 ## Get involved
 
-* Check out Linkerd's source code at [Github][linkerd2].
+* Check out Linkerd's source code at [GitHub][linkerd2].
 * Join Linkerd's [user mailing list][linkerd-users], [developer mailing
   list][linkerd-dev], and [announcements mailing list][linkerd-announce].
 * Follow [@linkerd][twitter] on Twitter.
diff --git a/jaeger/charts/linkerd-jaeger/templates/psp.yaml b/jaeger/charts/linkerd-jaeger/templates/psp.yaml
new file mode 100644
index 000000000..6f375c8a2
--- /dev/null
+++ b/jaeger/charts/linkerd-jaeger/templates/psp.yaml
@@ -0,0 +1,40 @@
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  name: psp
+  namespace: {{.Values.namespace}}
+  labels:
+    linkerd.io/extension: jaeger
+rules:
+- apiGroups: ['policy', 'extensions']
+  resources: ['podsecuritypolicies']
+  verbs: ['use']
+  resourceNames:
+  - linkerd-{{.Values.linkerdNamespace}}-control-plane
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+  name: jaeger-psp
+  namespace: {{.Values.namespace}}
+  labels:
+    linkerd.io/extension: jaeger
+roleRef:
+  kind: Role
+  name: psp
+  apiGroup: rbac.authorization.k8s.io
+subjects:
+{{ if .Values.collector.enabled -}}
+- kind: ServiceAccount
+  name: collector
+  namespace: {{.Values.namespace}}
+{{ end -}}
+- kind: ServiceAccount
+  name: jaeger-injector
+  namespace: {{.Values.namespace}}
+{{ if .Values.jaeger.enabled -}}
+- kind: ServiceAccount
+  name: jaeger
+  namespace: {{.Values.namespace}}
+{{ end -}}
diff --git a/jaeger/charts/linkerd-jaeger/templates/rbac.yaml b/jaeger/charts/linkerd-jaeger/templates/rbac.yaml
index d4fd1d7ff..98a0d5c5d 100644
--- a/jaeger/charts/linkerd-jaeger/templates/rbac.yaml
+++ b/jaeger/charts/linkerd-jaeger/templates/rbac.yaml
@@ -69,11 +69,11 @@ metadata:
 webhooks:
 - name: jaeger-injector.linkerd.io
   {{- if .Values.webhook.namespaceSelector }}
-  namespaceSelector: 
+  namespaceSelector:
 {{ toYaml .Values.webhook.namespaceSelector | trim | indent 4 -}}
   {{- end }}
   {{- if .Values.webhook.objectSelector }}
-  objectSelector: 
+  objectSelector:
 {{ toYaml .Values.webhook.objectSelector | trim | indent 4 -}}
   {{- end }}
   clientConfig:
@@ -82,7 +82,7 @@ webhooks:
       namespace: {{ .Values.namespace }}
       path: "/"
 {{- if and (.Values.webhook.externalSecret) (empty .Values.webhook.caBundle) }}
-  {{- fail "If webhook.externalSecret is true then you need to provide webook.caBundle" }}
+  {{- fail "If webhook.externalSecret is true then you need to provide webhook.caBundle" }}
 {{- end }}
     caBundle: {{ ternary (b64enc (trim $ca.Cert)) (b64enc (trim .Values.webhook.caBundle)) (empty .Values.webhook.caBundle) }}
   failurePolicy: {{.Values.webhook.failurePolicy}}
@@ -94,7 +94,7 @@ webhooks:
     apiVersions: ["v1"]
     resources: ["pods"]
   sideEffects: None
-{{- if .Values.jaeger.enabled }}
+{{ if .Values.jaeger.enabled -}}
 ---
 ###
 ### jaeger RBAC
@@ -104,4 +104,4 @@ apiVersion: v1
 metadata:
   name: jaeger
   namespace: {{.Values.namespace}}
-{{- end -}}
+{{ end -}}
diff --git a/jaeger/charts/linkerd-jaeger/templates/tracing.yaml b/jaeger/charts/linkerd-jaeger/templates/tracing.yaml
index 8e7a6101e..5a5a32226 100644
--- a/jaeger/charts/linkerd-jaeger/templates/tracing.yaml
+++ b/jaeger/charts/linkerd-jaeger/templates/tracing.yaml
@@ -1,4 +1,4 @@
-{{- if .Values.collector.enabled }}
+{{ if .Values.collector.enabled -}}
 ---
 ###
 ### Tracing Collector Service
@@ -12,22 +12,7 @@ metadata:
     component: collector
 data:
   collector-config: |
-    receivers:
-      opencensus:
-        port: 55678
-      zipkin:
-        port: 9411
-      jaeger:
-        jaeger-thrift-http-port: 14268
-    queued-exporters:
-      jaeger-all-in-one:
-        num-workers: 4
-        queue-size: 100
-        retry-on-failure: true
-        sender-type: jaeger-thrift-http
-        jaeger-thrift-http:
-          collector-endpoint: {{.Values.collector.jaegerAddr | default (printf "http://%s.%s:14268/api/traces" "jaeger" .Values.namespace) }}
-          timeout: 5s
+    {{- .Values.collector.config | trim | nindent 4 }}
 ---
 apiVersion: v1
 kind: Service
@@ -39,6 +24,10 @@ metadata:
 spec:
   type: ClusterIP
   ports:
+  - name: otlp
+    port: 4317
+    protocol: TCP
+    targetPort: 4317
   - name: opencensus
     port: 55678
     protocol: TCP
@@ -47,10 +36,14 @@ spec:
     port: 9411
     protocol: TCP
     targetPort: 9411
-  - name: jaeger
+  - name: jaeger-thrift
     port: 14268
     protocol: TCP
     targetPort: 14268
+  - name: jaeger-grpc
+    port: 14250
+    protocol: TCP
+    targetPort: 14250
   selector:
     component: collector
 ---
@@ -85,22 +78,29 @@ spec:
       {{- include "linkerd.node-selector" (dict "Values" .Values.collector) | nindent 6 }}
       containers:
       - command:
-        - /occollector_linux
+        - /otelcol
         - --config=/conf/collector-config.yaml
         env:
         - name: GOGC
           value: "80"
+        - name: POD_NAMESPACE
+          valueFrom:
+            fieldRef:
+              apiVersion: v1
+              fieldPath: metadata.namespace
         image: {{.Values.collector.image.name}}:{{.Values.collector.image.version}}
         imagePullPolicy: {{.Values.collector.image.pullPolicy}}
         livenessProbe:
           httpGet:
             path: /
             port: 13133
-        name: oc-collector
+        name: ot-collector
         ports:
+        - containerPort: 4317
         - containerPort: 55678
         - containerPort: 9411
         - containerPort: 14268
+        - containerPort: 14250
         - containerPort: 8888
         readinessProbe:
           httpGet:
@@ -120,8 +120,8 @@ spec:
             path: collector-config.yaml
           name: collector-config
         name: collector-config-val
-{{- end -}}
-{{- if .Values.jaeger.enabled }}
+{{ end -}}
+{{ if .Values.jaeger.enabled -}}
 ---
 ###
 ### Tracing Jaeger Service
@@ -140,6 +140,8 @@ spec:
   ports:
     - name: collection
       port: 14268
+    - name: grpc
+      port: 14250
     - name: ui
       port: 16686
 ---
@@ -182,6 +184,8 @@ spec:
           name: admin
         - containerPort: 14268
           name: collection
+        - containerPort: 14250
+          name: grpc
         - containerPort: 16686
           name: ui
         {{- if .Values.jaeger.resources -}}
@@ -189,4 +193,4 @@ spec:
         {{- end }}
       dnsPolicy: ClusterFirst
       serviceAccountName: jaeger
-{{- end }}
+{{ end -}}
diff --git a/jaeger/charts/linkerd-jaeger/values.yaml b/jaeger/charts/linkerd-jaeger/values.yaml
index 904e5bca8..1375a43ff 100644
--- a/jaeger/charts/linkerd-jaeger/values.yaml
+++ b/jaeger/charts/linkerd-jaeger/values.yaml
@@ -4,6 +4,9 @@ installNamespace: true
 
 namespace: linkerd-jaeger
 
+# -- Namespace of the Linkerd core control-plane install
+linkerdNamespace: linkerd
+
 # -- Default nodeSelector section, See the
 # [K8S documentation](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector) for more information
 nodeSelector: &default_node_selector
@@ -18,8 +21,8 @@ collector:
   # -- Set to false to exclude collector installation
   enabled: true
   image:
-    name: omnition/opencensus-collector
-    version: 0.1.11
+    name: otel/opentelemetry-collector
+    version: 0.27.0
     pullPolicy: Always
 
   # -- NodeSelector section, See the
@@ -30,8 +33,39 @@ collector:
   # for more information
   tolerations: *default_tolerations
 
-  # -- address of the jaeger backend to send traces to
-  jaegerAddr:
+  # -- OpenTelemetry Collector config, See the
+  # [Configuration docs](https://opentelemetry.io/docs/collector/configuration/)
+  # for more information
+  # @default -- see `value.yaml` for actual configuration
+  config: |
+    receivers:
+      otlp:
+        protocols:
+          grpc:
+          http:
+      opencensus:
+      zipkin:
+      jaeger:
+        protocols:
+          grpc:
+          thrift_http:
+          thrift_compact:
+          thrift_binary:
+    processors:
+      batch:
+    extensions:
+      health_check:
+    exporters:
+      jaeger:
+        endpoint: jaeger.${POD_NAMESPACE}:14250
+        insecure: true
+    service:
+      extensions: [health_check]
+      pipelines:
+        traces:
+          receivers: [otlp,opencensus,zipkin,jaeger]
+          processors: [batch]
+          exporters: [jaeger]
 
 jaeger:
   # -- Set to false to exclude all-in-one Jaeger installation
@@ -40,6 +74,7 @@ jaeger:
     name: jaegertracing/all-in-one
     version: 1.19.2
     pullPolicy: Always
+
   # -- NodeSelector section, See the
   # [K8S documentation](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector) for more information
   nodeSelector: *default_node_selector
diff --git a/jaeger/cmd/check.go b/jaeger/cmd/check.go
index 98cb79cb5..6e54bd254 100644
--- a/jaeger/cmd/check.go
+++ b/jaeger/cmd/check.go
@@ -7,6 +7,7 @@ import (
 	"os"
 	"time"
 
+	pkgcmd "github.com/linkerd/linkerd2/pkg/cmd"
 	"github.com/linkerd/linkerd2/pkg/healthcheck"
 	"github.com/linkerd/linkerd2/pkg/version"
 	"github.com/spf13/cobra"
@@ -18,6 +19,10 @@ const (
 	// JaegerExtensionName is the name of jaeger extension
 	JaegerExtensionName = "jaeger"
 
+	// JaegerLegacyExtension is the name of the jaeger extension prior to
+	// stable-2.10.0 when the linkerd prefix was removed.
+	JaegerLegacyExtension = "linkerd-jaeger"
+
 	// linkerdJaegerExtensionCheck adds checks related to the jaeger extension
 	linkerdJaegerExtensionCheck healthcheck.CategoryID = "linkerd-jaeger"
 )
@@ -201,6 +206,11 @@ code.`,
 	cmd.Flags().BoolVar(&options.proxy, "proxy", options.proxy, "Also run data-plane checks, to determine if the data plane is healthy")
 	cmd.Flags().StringVarP(&options.namespace, "namespace", "n", options.namespace, "Namespace to use for --proxy checks (default: all namespaces)")
 
+	pkgcmd.ConfigureNamespaceFlagCompletion(
+		cmd, []string{"namespace"},
+		kubeconfigPath, impersonate, impersonateGroup, kubeContext)
+	pkgcmd.ConfigureOutputFlagCompletion(cmd)
+
 	return cmd
 }
 
diff --git a/jaeger/cmd/install.go b/jaeger/cmd/install.go
index c453f45d6..231d4ddb2 100644
--- a/jaeger/cmd/install.go
+++ b/jaeger/cmd/install.go
@@ -25,6 +25,7 @@ var (
 		"templates/namespace.yaml",
 		"templates/jaeger-injector.yaml",
 		"templates/rbac.yaml",
+		"templates/psp.yaml",
 		"templates/tracing.yaml",
 	}
 )
@@ -43,7 +44,7 @@ func newCmdInstall() *cobra.Command {
   linkerd jaeger install | kubectl apply -f -
   # Install Jaeger extension into a non-default namespace.
   linkerd jaeger install --namespace custom | kubectl apply -f -
-  
+
 The installation can be configured by using the --set, --values, --set-string and --set-file flags.
 A full list of configurable values can be found at https://www.github.com/linkerd/linkerd2/tree/main/jaeger/charts/linkerd-jaeger/README.md
   `,
diff --git a/jaeger/cmd/list.go b/jaeger/cmd/list.go
index 1f2d964a6..697abef55 100644
--- a/jaeger/cmd/list.go
+++ b/jaeger/cmd/list.go
@@ -81,5 +81,9 @@ func newCmdList() *cobra.Command {
 	cmd.Flags().StringVarP(&options.namespace, "namespace", "n", options.namespace, "The namespace to list pods in")
 	cmd.Flags().BoolVarP(&options.allNamespaces, "all-namespaces", "A", options.allNamespaces, "If present, list pods across all namespaces")
 
+	pkgcmd.ConfigureNamespaceFlagCompletion(
+		cmd, []string{"namespace"},
+		kubeconfigPath, impersonate, impersonateGroup, kubeContext)
+
 	return cmd
 }
diff --git a/jaeger/cmd/root.go b/jaeger/cmd/root.go
index d22cd8b29..ebfb4b551 100644
--- a/jaeger/cmd/root.go
+++ b/jaeger/cmd/root.go
@@ -6,6 +6,7 @@ import (
 	"regexp"
 
 	"github.com/fatih/color"
+	pkgcmd "github.com/linkerd/linkerd2/pkg/cmd"
 	"github.com/linkerd/linkerd2/pkg/healthcheck"
 	log "github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
@@ -34,7 +35,7 @@ var (
 	alphaNumDash = regexp.MustCompile(`^[a-zA-Z0-9-]+$`)
 )
 
-// NewCmdJaeger returns a new jeager command
+// NewCmdJaeger returns a new jaeger command
 func NewCmdJaeger() *cobra.Command {
 	jaegerCmd := &cobra.Command{
 		Use:   "jaeger",
@@ -69,6 +70,12 @@ func NewCmdJaeger() *cobra.Command {
 	jaegerCmd.AddCommand(newCmdList())
 	jaegerCmd.AddCommand(newCmdUninstall())
 
+	// resource-aware completion flag configurations
+	pkgcmd.ConfigureNamespaceFlagCompletion(
+		jaegerCmd, []string{"linkerd-namespace"},
+		kubeconfigPath, impersonate, impersonateGroup, kubeContext)
+
+	pkgcmd.ConfigureKubeContextFlagCompletion(jaegerCmd, kubeconfigPath)
 	return jaegerCmd
 }
 
diff --git a/jaeger/cmd/uninstall.go b/jaeger/cmd/uninstall.go
index f03592e00..1764730a4 100644
--- a/jaeger/cmd/uninstall.go
+++ b/jaeger/cmd/uninstall.go
@@ -38,5 +38,10 @@ func uninstallRunE(ctx context.Context) error {
 		return err
 	}
 
-	return pkgCmd.Uninstall(ctx, k8sAPI, fmt.Sprintf("%s=%s", k8s.LinkerdExtensionLabel, JaegerExtensionName))
+	selector, err := pkgCmd.GetLabelSelector(k8s.LinkerdExtensionLabel, JaegerExtensionName, JaegerLegacyExtension)
+	if err != nil {
+		return err
+	}
+
+	return pkgCmd.Uninstall(ctx, k8sAPI, selector)
 }
diff --git a/jaeger/injector/mutator/webhook.go b/jaeger/injector/mutator/webhook.go
index 8475aca3c..1f5b786de 100644
--- a/jaeger/injector/mutator/webhook.go
+++ b/jaeger/injector/mutator/webhook.go
@@ -10,7 +10,7 @@ import (
 	"github.com/linkerd/linkerd2/controller/k8s"
 	"github.com/linkerd/linkerd2/controller/webhook"
 	"github.com/linkerd/linkerd2/jaeger/pkg/labels"
-	l5dLables "github.com/linkerd/linkerd2/pkg/k8s"
+	l5dLabels "github.com/linkerd/linkerd2/pkg/k8s"
 	"github.com/prometheus/common/log"
 	admissionv1beta1 "k8s.io/api/admission/v1beta1"
 	corev1 "k8s.io/api/core/v1"
@@ -19,8 +19,8 @@ import (
 )
 
 const (
-	collectorSvcAddrAnnotation    = l5dLables.ProxyConfigAnnotationsPrefix + "/trace-collector"
-	collectorSvcAccountAnnotation = l5dLables.ProxyConfigAnnotationsPrefixAlpha +
+	collectorSvcAddrAnnotation    = l5dLabels.ProxyConfigAnnotationsPrefix + "/trace-collector"
+	collectorSvcAccountAnnotation = l5dLabels.ProxyConfigAnnotationsPrefixAlpha +
 		"/trace-collector-service-account"
 )
 
diff --git a/multicluster/charts/linkerd-multicluster-link/templates/psp.yaml b/multicluster/charts/linkerd-multicluster-link/templates/psp.yaml
new file mode 100644
index 000000000..d18e8d1ed
--- /dev/null
+++ b/multicluster/charts/linkerd-multicluster-link/templates/psp.yaml
@@ -0,0 +1,17 @@
+---
+kind: RoleBinding
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: multicluster-link-psp
+  namespace: {{.Values.namespace}}
+  labels:
+    linkerd.io/extension: multicluster
+    namespace: {{.Values.namespace}}
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: Role
+  name: psp
+subjects:
+- kind: ServiceAccount
+  name: linkerd-service-mirror-{{.Values.targetClusterName}}
+  namespace: {{.Values.namespace}}
diff --git a/multicluster/charts/linkerd-multicluster/README.md b/multicluster/charts/linkerd-multicluster/README.md
index 6c78298bf..5423ac587 100644
--- a/multicluster/charts/linkerd-multicluster/README.md
+++ b/multicluster/charts/linkerd-multicluster/README.md
@@ -43,7 +43,7 @@ helm install linkerd/linkerd-multicluster
 
 ## Get involved
 
-* Check out Linkerd's source code at [Github][linkerd2].
+* Check out Linkerd's source code at [GitHub][linkerd2].
 * Join Linkerd's [user mailing list][linkerd-users], [developer mailing
   list][linkerd-dev], and [announcements mailing list][linkerd-announce].
 * Follow [@linkerd][twitter] on Twitter.
diff --git a/multicluster/charts/linkerd-multicluster/README.md.gotmpl b/multicluster/charts/linkerd-multicluster/README.md.gotmpl
index 555a09215..f3ac69798 100644
--- a/multicluster/charts/linkerd-multicluster/README.md.gotmpl
+++ b/multicluster/charts/linkerd-multicluster/README.md.gotmpl
@@ -41,7 +41,7 @@ helm install linkerd/linkerd-multicluster
 
 ## Get involved
 
-* Check out Linkerd's source code at [Github][linkerd2].
+* Check out Linkerd's source code at [GitHub][linkerd2].
 * Join Linkerd's [user mailing list][linkerd-users], [developer mailing
   list][linkerd-dev], and [announcements mailing list][linkerd-announce].
 * Follow [@linkerd][twitter] on Twitter.
diff --git a/multicluster/charts/linkerd-multicluster/templates/psp.yaml b/multicluster/charts/linkerd-multicluster/templates/psp.yaml
new file mode 100644
index 000000000..3725dd86e
--- /dev/null
+++ b/multicluster/charts/linkerd-multicluster/templates/psp.yaml
@@ -0,0 +1,31 @@
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  name: psp
+  namespace: {{.Values.namespace}}
+  labels:
+    linkerd.io/extension: multicluster
+rules:
+- apiGroups: ['policy', 'extensions']
+  resources: ['podsecuritypolicies']
+  verbs: ['use']
+  resourceNames:
+  - linkerd-{{.Values.linkerdNamespace}}-control-plane
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+  name: multicluster-psp
+  namespace: {{.Values.namespace}}
+  labels:
+    linkerd.io/extension: multicluster
+    namespace: {{.Values.namespace}}
+roleRef:
+  kind: Role
+  name: psp
+  apiGroup: rbac.authorization.k8s.io
+subjects:
+- kind: ServiceAccount
+  name: {{.Values.gateway.name}}
+  namespace: {{.Values.namespace}}
diff --git a/multicluster/cmd/allow.go b/multicluster/cmd/allow.go
index a021431dc..4e342752b 100644
--- a/multicluster/cmd/allow.go
+++ b/multicluster/cmd/allow.go
@@ -8,6 +8,7 @@ import (
 	"github.com/linkerd/linkerd2/multicluster/static"
 	mccharts "github.com/linkerd/linkerd2/multicluster/values"
 	"github.com/linkerd/linkerd2/pkg/charts"
+	pkgcmd "github.com/linkerd/linkerd2/pkg/cmd"
 	"github.com/linkerd/linkerd2/pkg/k8s"
 	"github.com/linkerd/linkerd2/pkg/version"
 	"github.com/spf13/cobra"
@@ -79,6 +80,10 @@ func newAllowCommand() *cobra.Command {
 	cmd.Flags().BoolVar(&opts.ignoreCluster, "ignore-cluster", false, "Ignore cluster configuration")
 	cmd.Flags().StringVar(&opts.serviceAccountName, "service-account-name", "", "The name of the multicluster access service account")
 
+	pkgcmd.ConfigureNamespaceFlagCompletion(
+		cmd, []string{"namespace"},
+		kubeconfigPath, impersonate, impersonateGroup, kubeContext)
+
 	return cmd
 }
 
diff --git a/multicluster/cmd/check.go b/multicluster/cmd/check.go
index 1526ccdbd..5235f761b 100644
--- a/multicluster/cmd/check.go
+++ b/multicluster/cmd/check.go
@@ -10,6 +10,7 @@ import (
 	"strings"
 	"time"
 
+	pkgcmd "github.com/linkerd/linkerd2/pkg/cmd"
 	"github.com/linkerd/linkerd2/pkg/healthcheck"
 	"github.com/linkerd/linkerd2/pkg/k8s"
 	"github.com/linkerd/linkerd2/pkg/multicluster"
@@ -29,6 +30,10 @@ const (
 	// MulticlusterExtensionName is the name of the multicluster extension
 	MulticlusterExtensionName = "multicluster"
 
+	// MulticlusterLegacyExtension is the name of the multicluster extension
+	// prior to stable-2.10.0 when the linkerd prefix was removed.
+	MulticlusterLegacyExtension = "linkerd-multicluster"
+
 	// linkerdMulticlusterExtensionCheck adds checks related to the multicluster extension
 	linkerdMulticlusterExtensionCheck healthcheck.CategoryID = "linkerd-multicluster"
 
@@ -102,6 +107,12 @@ non-zero exit code.`,
 	cmd.Flags().MarkHidden("proxy")
 	cmd.Flags().StringP("namespace", "n", "", "")
 	cmd.Flags().MarkHidden("namespace")
+
+	pkgcmd.ConfigureNamespaceFlagCompletion(
+		cmd, []string{"namespace"},
+		kubeconfigPath, impersonate, impersonateGroup, kubeContext)
+	pkgcmd.ConfigureOutputFlagCompletion(cmd)
+
 	return cmd
 }
 
diff --git a/multicluster/cmd/install.go b/multicluster/cmd/install.go
index 371472ae6..2c6520391 100644
--- a/multicluster/cmd/install.go
+++ b/multicluster/cmd/install.go
@@ -85,6 +85,7 @@ A full list of configurable values can be found at https://github.com/linkerd/li
 				{Name: chartutil.ChartfileName},
 				{Name: "templates/namespace.yaml"},
 				{Name: "templates/gateway.yaml"},
+				{Name: "templates/psp.yaml"},
 				{Name: "templates/remote-access-service-mirror-rbac.yaml"},
 				{Name: "templates/link-crd.yaml"},
 			}
diff --git a/multicluster/cmd/link.go b/multicluster/cmd/link.go
index c91ade648..55edae876 100644
--- a/multicluster/cmd/link.go
+++ b/multicluster/cmd/link.go
@@ -11,6 +11,7 @@ import (
 	"github.com/linkerd/linkerd2/multicluster/static"
 	multicluster "github.com/linkerd/linkerd2/multicluster/values"
 	"github.com/linkerd/linkerd2/pkg/charts"
+	pkgcmd "github.com/linkerd/linkerd2/pkg/cmd"
 	"github.com/linkerd/linkerd2/pkg/flags"
 	"github.com/linkerd/linkerd2/pkg/k8s"
 	mc "github.com/linkerd/linkerd2/pkg/multicluster"
@@ -43,6 +44,7 @@ type (
 		dockerRegistry          string
 		selector                string
 		gatewayAddresses        string
+		gatewayPort             uint32
 	}
 )
 
@@ -209,6 +211,11 @@ A full list of configurable values can be found at https://github.com/linkerd/li
 				return err
 			}
 
+			// Override with user provided gateway port if present
+			if opts.gatewayPort != 0 {
+				gatewayPort = opts.gatewayPort
+			}
+
 			selector, err := metav1.ParseToLabelSelector(opts.selector)
 			if err != nil {
 				return err
@@ -252,6 +259,7 @@ A full list of configurable values can be found at https://github.com/linkerd/li
 			files := []*chartloader.BufferedFile{
 				{Name: chartutil.ChartfileName},
 				{Name: "templates/service-mirror.yaml"},
+				{Name: "templates/psp.yaml"},
 				{Name: "templates/gateway-mirror.yaml"},
 			}
 
@@ -321,8 +329,12 @@ A full list of configurable values can be found at https://github.com/linkerd/li
 	cmd.Flags().StringVar(&opts.logLevel, "log-level", opts.logLevel, "Log level for the Multicluster components")
 	cmd.Flags().StringVar(&opts.dockerRegistry, "registry", opts.dockerRegistry, "Docker registry to pull service mirror controller image from")
 	cmd.Flags().StringVarP(&opts.selector, "selector", "l", opts.selector, "Selector (label query) to filter which services in the target cluster to mirror")
-	cmd.Flags().StringVar(&opts.gatewayAddresses, "gateway-addresses", opts.gatewayAddresses, "If specified overwrites gateway addresses when gateway service is not type LoadBalancer (comma separated list)")
+	cmd.Flags().StringVar(&opts.gatewayAddresses, "gateway-addresses", opts.gatewayAddresses, "If specified, overwrites gateway addresses when gateway service is not type LoadBalancer (comma separated list)")
+	cmd.Flags().Uint32Var(&opts.gatewayPort, "gateway-port", opts.gatewayPort, "If specified, overwrites gateway port when gateway service is not type LoadBalancer")
 
+	pkgcmd.ConfigureNamespaceFlagCompletion(
+		cmd, []string{"namespace", "gateway-namespace"},
+		kubeconfigPath, impersonate, impersonateGroup, kubeContext)
 	return cmd
 }
 
@@ -340,6 +352,7 @@ func newLinkOptionsWithDefault() (*linkOptions, error) {
 		logLevel:                defaults.LogLevel,
 		selector:                k8s.DefaultExportedServiceSelector,
 		gatewayAddresses:        "",
+		gatewayPort:             0,
 	}, nil
 }
 
diff --git a/multicluster/cmd/root.go b/multicluster/cmd/root.go
index 65026d5f1..24f7b5e7c 100644
--- a/multicluster/cmd/root.go
+++ b/multicluster/cmd/root.go
@@ -6,6 +6,7 @@ import (
 
 	"github.com/fatih/color"
 	"github.com/linkerd/linkerd2/pkg/charts/linkerd2"
+	pkgcmd "github.com/linkerd/linkerd2/pkg/cmd"
 	"github.com/linkerd/linkerd2/pkg/healthcheck"
 	"github.com/linkerd/linkerd2/pkg/k8s"
 	log "github.com/sirupsen/logrus"
@@ -85,6 +86,13 @@ components on a cluster, manage credentials and link clusters together.`,
 	multiclusterCmd.AddCommand(newMulticlusterUninstallCommand())
 	multiclusterCmd.AddCommand(newGatewaysCommand())
 	multiclusterCmd.AddCommand(newAllowCommand())
+
+	// resource-aware completion flag configurations
+	pkgcmd.ConfigureNamespaceFlagCompletion(
+		multiclusterCmd, []string{"linkerd-namespace"},
+		kubeconfigPath, impersonate, impersonateGroup, kubeContext)
+
+	pkgcmd.ConfigureKubeContextFlagCompletion(multiclusterCmd, kubeconfigPath)
 	return multiclusterCmd
 }
 
diff --git a/multicluster/cmd/uninstall.go b/multicluster/cmd/uninstall.go
index 1cef31317..f68a16923 100644
--- a/multicluster/cmd/uninstall.go
+++ b/multicluster/cmd/uninstall.go
@@ -53,7 +53,12 @@ func newMulticlusterUninstallCommand() *cobra.Command {
 				return errors.New(strings.Join(err, "\n"))
 			}
 
-			err = pkgCmd.Uninstall(cmd.Context(), k8sAPI, fmt.Sprintf("%s=%s", k8s.LinkerdExtensionLabel, MulticlusterExtensionName))
+			selector, err := pkgCmd.GetLabelSelector(k8s.LinkerdExtensionLabel, MulticlusterExtensionName, MulticlusterLegacyExtension)
+			if err != nil {
+				return err
+			}
+
+			err = pkgCmd.Uninstall(cmd.Context(), k8sAPI, selector)
 			if err != nil {
 				fmt.Fprintln(os.Stderr, err)
 				os.Exit(1)
diff --git a/multicluster/cmd/unlink.go b/multicluster/cmd/unlink.go
index 5165f6f69..cc5826cc5 100644
--- a/multicluster/cmd/unlink.go
+++ b/multicluster/cmd/unlink.go
@@ -4,7 +4,9 @@ import (
 	"errors"
 	"fmt"
 	"os"
+	"strings"
 
+	pkgcmd "github.com/linkerd/linkerd2/pkg/cmd"
 	"github.com/linkerd/linkerd2/pkg/k8s"
 	"github.com/linkerd/linkerd2/pkg/k8s/resource"
 	mc "github.com/linkerd/linkerd2/pkg/multicluster"
@@ -95,5 +97,28 @@ func newUnlinkCommand() *cobra.Command {
 	cmd.Flags().StringVar(&opts.namespace, "namespace", defaultMulticlusterNamespace, "The namespace for the service account")
 	cmd.Flags().StringVar(&opts.clusterName, "cluster-name", "", "Cluster name")
 
+	pkgcmd.ConfigureNamespaceFlagCompletion(
+		cmd, []string{"namespace"},
+		kubeconfigPath, impersonate, impersonateGroup, kubeContext)
+
+	configureClusterNameFlagCompletion(cmd)
 	return cmd
 }
+
+func configureClusterNameFlagCompletion(cmd *cobra.Command) {
+	cmd.RegisterFlagCompletionFunc("cluster-name",
+		func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+			k8sAPI, err := k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, impersonateGroup, 0)
+			if err != nil {
+				return nil, cobra.ShellCompDirectiveError
+			}
+
+			cc := k8s.NewCommandCompletion(k8sAPI, corev1.NamespaceAll)
+			results, err := cc.Complete([]string{strings.ToLower(k8s.LinkKind)}, toComplete)
+			if err != nil {
+				return nil, cobra.ShellCompDirectiveError
+			}
+
+			return results, cobra.ShellCompDirectiveDefault
+		})
+}
diff --git a/multicluster/values/values.go b/multicluster/values/values.go
index 79dbac31c..151c17e1e 100644
--- a/multicluster/values/values.go
+++ b/multicluster/values/values.go
@@ -37,7 +37,7 @@ type Values struct {
 	TargetClusterName              string   `json:"targetClusterName"`
 }
 
-// Gateway contains all opttions related to the Gateway Service
+// Gateway contains all options related to the Gateway Service
 type Gateway struct {
 	Enabled            bool              `json:"enabled"`
 	Name               string            `json:"name"`
diff --git a/pkg/charts/charts.go b/pkg/charts/charts.go
index b7b6d7d07..712a1cba7 100644
--- a/pkg/charts/charts.go
+++ b/pkg/charts/charts.go
@@ -196,7 +196,7 @@ func OverrideFromFile(values map[string]interface{}, fs http.FileSystem, chartNa
 }
 
 // MergeMaps returns the resultant map after merging given two maps of type map[string]interface{}
-// The inputs are not mutated and the second map i.e b's values take predence during merge.
+// The inputs are not mutated and the second map i.e b's values take precedence during merge.
 // This gives semantically correct merge compared with `mergo.Merge` (with boolean values).
 // See https://github.com/imdario/mergo/issues/129
 func MergeMaps(a, b map[string]interface{}) map[string]interface{} {
diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go
index 974997576..542bc2be1 100644
--- a/pkg/cmd/cmd.go
+++ b/pkg/cmd/cmd.go
@@ -5,6 +5,7 @@ import (
 	"errors"
 	"fmt"
 	"os"
+	"strings"
 
 	"github.com/linkerd/linkerd2/pkg/k8s"
 	"github.com/linkerd/linkerd2/pkg/k8s/resource"
@@ -12,6 +13,8 @@ import (
 	"github.com/spf13/cobra"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/apimachinery/pkg/selection"
 	"k8s.io/client-go/tools/clientcmd"
 )
 
@@ -86,3 +89,60 @@ func ConfigureNamespaceFlagCompletion(
 			})
 	}
 }
+
+// ConfigureOutputFlagCompletion sets up resource-aware completion for command
+// flags that accept an output name.
+func ConfigureOutputFlagCompletion(cmd *cobra.Command) {
+	cmd.RegisterFlagCompletionFunc("output",
+		func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+			return []string{"basic", "json", "short", "table"}, cobra.ShellCompDirectiveDefault
+		})
+}
+
+// ConfigureKubeContextFlagCompletion sets up resource-aware completion for command
+// flags based off of a kubeconfig
+func ConfigureKubeContextFlagCompletion(cmd *cobra.Command, kubeconfigPath string) {
+	cmd.RegisterFlagCompletionFunc("context",
+		func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+			rules := clientcmd.NewDefaultClientConfigLoadingRules()
+			rules.ExplicitPath = kubeconfigPath
+			loader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, &clientcmd.ConfigOverrides{})
+			config, err := loader.RawConfig()
+			if err != nil {
+				return nil, cobra.ShellCompDirectiveError
+			}
+
+			suggestions := []string{}
+			uniqClusters := map[string]struct{}{}
+			for _, ctx := range config.Contexts {
+				clusterName := ctx.Cluster
+				if strings.HasPrefix(clusterName, toComplete) {
+					if _, ok := uniqClusters[clusterName]; !ok {
+						suggestions = append(suggestions, ctx.Cluster)
+						uniqClusters[clusterName] = struct{}{}
+					}
+				}
+			}
+
+			return suggestions, cobra.ShellCompDirectiveDefault
+		})
+}
+
+// GetLabelSelector creates a label selector as a string based on a label key
+// whose value may be in the set provided as an argument to the function. If the
+// value set is empty then the selector will match resources where the label key
+// exists regardless of value.
+func GetLabelSelector(labelKey string, labelValues ...string) (string, error) {
+	selectionOp := selection.In
+	if len(labelValues) < 1 {
+		selectionOp = selection.Exists
+	}
+
+	labelRequirement, err := labels.NewRequirement(labelKey, selectionOp, labelValues)
+	if err != nil {
+		return "", err
+	}
+
+	selector := labels.NewSelector().Add(*labelRequirement)
+	return selector.String(), nil
+}
diff --git a/pkg/healthcheck/healthcheck.go b/pkg/healthcheck/healthcheck.go
index 0664798f8..ee9854f9e 100644
--- a/pkg/healthcheck/healthcheck.go
+++ b/pkg/healthcheck/healthcheck.go
@@ -7,6 +7,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"net"
 	"sort"
 	"strings"
 	"time"
@@ -136,6 +137,11 @@ const (
 	/// plugin is installed and ready
 	LinkerdCNIPluginChecks CategoryID = "linkerd-cni-plugin"
 
+	// LinkerdOpaquePortsDefinitionChecks adds checks to validate that the
+	// "opaque ports" annotation has been defined both in the service and the
+	// corresponding pods
+	LinkerdOpaquePortsDefinitionChecks CategoryID = "linkerd-opaque-ports-definition"
+
 	// LinkerdCNIResourceLabel is the label key that is used to identify
 	// whether a Kubernetes resource is related to the install-cni command
 	// The value is expected to be "true", "false" or "", where "false" and
@@ -758,6 +764,19 @@ func (hc *HealthChecker) allCategories() []*Category {
 						return validateControlPlanePods(hc.controlPlanePods)
 					},
 				},
+				{
+					description: "cluster networks contains all node podCIDRs",
+					hintAnchor:  "l5d-cluster-networks-cidr",
+					check: func(ctx context.Context) error {
+						// We explicitly initialize the config here so that we dont rely on the "l5d-existence-linkerd-config"
+						// check to set the clusterNetworks value, since `linkerd check config` will skip that check.
+						err := hc.InitializeLinkerdGlobalConfig(ctx)
+						if err != nil {
+							return err
+						}
+						return hc.checkClusterNetworks(ctx)
+					},
+				},
 			},
 			false,
 		),
@@ -1379,6 +1398,13 @@ func (hc *HealthChecker) allCategories() []*Category {
 						return checkMisconfiguredServiceAnnotations(services)
 					},
 				},
+				{
+					description: "opaque ports are properly annotated",
+					hintAnchor:  "linkerd-opaque-ports-definition",
+					check: func(ctx context.Context) error {
+						return hc.checkMisconfiguredOpaquePortAnnotations(ctx)
+					},
+				},
 			},
 			false,
 		),
@@ -1429,7 +1455,8 @@ func (hc *HealthChecker) CheckProxyVersionsUpToDate(pods []corev1.Pod) error {
 func CheckProxyVersionsUpToDate(pods []corev1.Pod, versions version.Channels) error {
 	outdatedPods := []string{}
 	for _, pod := range pods {
-		if containsProxy(pod) {
+		status := k8s.GetPodStatus(pod)
+		if status == string(corev1.PodRunning) && containsProxy(pod) {
 			proxyVersion := k8s.GetProxyVersion(pod)
 			if err := versions.Match(proxyVersion); err != nil {
 				outdatedPods = append(outdatedPods, fmt.Sprintf("\t* %s (%s)", pod.Name, proxyVersion))
@@ -1791,7 +1818,7 @@ func (hc *HealthChecker) FetchCredsFromSecret(ctx context.Context, namespace str
 	return cred, nil
 }
 
-// FetchCredsFromOldSecret function can be removed in later versions, once eihter all webhook secrets are recreated for each update
+// FetchCredsFromOldSecret function can be removed in later versions, once either all webhook secrets are recreated for each update
 // (see https://github.com/linkerd/linkerd2/issues/4813)
 // or later releases are only expected to update from the new names.
 func (hc *HealthChecker) FetchCredsFromOldSecret(ctx context.Context, namespace string, secretName string) (*tls.Cred, error) {
@@ -1854,6 +1881,48 @@ func (hc *HealthChecker) CheckNamespace(ctx context.Context, namespace string, s
 	return nil
 }
 
+func (hc *HealthChecker) checkClusterNetworks(ctx context.Context) error {
+	nodes, err := hc.kubeAPI.GetNodes(ctx)
+	if err != nil {
+		return err
+	}
+	clusterNetworks := strings.Split(hc.linkerdConfig.ClusterNetworks, ",")
+	clusterIPNets := make([]*net.IPNet, len(clusterNetworks))
+	for i, clusterNetwork := range clusterNetworks {
+		_, clusterIPNets[i], err = net.ParseCIDR(clusterNetwork)
+		if err != nil {
+			return err
+		}
+	}
+	var badPodCIDRS []string
+	for _, node := range nodes {
+		podCIDR := node.Spec.PodCIDR
+		podIP, podIPNet, err := net.ParseCIDR(podCIDR)
+		if err != nil {
+			return err
+		}
+		exists := cluterNetworksContainCIDR(clusterIPNets, podIPNet, podIP)
+		if !exists {
+			badPodCIDRS = append(badPodCIDRS, podCIDR)
+		}
+	}
+	if len(badPodCIDRS) > 0 {
+		return fmt.Errorf("node has podCIDR(s) %v which are not contained in the Linkerd clusterNetworks.\n\tTry installing linkerd via --set clusterNetworks=%s", badPodCIDRS, strings.Join(badPodCIDRS, ","))
+	}
+	return nil
+}
+
+func cluterNetworksContainCIDR(clusterIPNets []*net.IPNet, podIPNet *net.IPNet, podIP net.IP) bool {
+	for _, clusterIPNet := range clusterIPNets {
+		clusterIPMaskOnes, _ := clusterIPNet.Mask.Size()
+		podCIDRMaskOnes, _ := podIPNet.Mask.Size()
+		if clusterIPNet.Contains(podIP) && podCIDRMaskOnes >= clusterIPMaskOnes {
+			return true
+		}
+	}
+	return false
+}
+
 func (hc *HealthChecker) expectedRBACNames() []string {
 	return []string{
 		fmt.Sprintf("linkerd-%s-identity", hc.ControlPlaneNamespace),
@@ -1941,7 +2010,7 @@ func (hc *HealthChecker) checkServiceAccounts(ctx context.Context, saNames []str
 	return CheckServiceAccounts(ctx, hc.kubeAPI, saNames, ns, labelSelector)
 }
 
-// CheckServiceAccounts check for serivceaccounts
+// CheckServiceAccounts check for serviceaccounts
 func CheckServiceAccounts(ctx context.Context, api *k8s.KubernetesAPI, saNames []string, ns, labelSelector string) error {
 	options := metav1.ListOptions{
 		LabelSelector: labelSelector,
@@ -2191,6 +2260,87 @@ func checkResources(resourceName string, objects []runtime.Object, expectedNames
 	return nil
 }
 
+// Check if there's a pod with the "opaque ports" annotation defined but a
+// service selecting the aforementioned pod doesn't define it
+func (hc *HealthChecker) checkMisconfiguredOpaquePortAnnotations(ctx context.Context) error {
+	services, err := hc.GetServices(ctx)
+	if err != nil {
+		return err
+	}
+
+	var errStrings []string
+
+	for _, service := range services {
+		if service.Spec.ClusterIP == "None" {
+			// skip headless services; they're handled differently
+			continue
+		}
+
+		endpoint, err := hc.kubeAPI.CoreV1().Endpoints(service.Namespace).Get(ctx, service.Name, metav1.GetOptions{})
+
+		if err != nil {
+			return err
+		}
+
+		pods := make([]*corev1.Pod, 0)
+		for _, subset := range endpoint.Subsets {
+			for _, addr := range subset.Addresses {
+				if addr.TargetRef != nil && addr.TargetRef.Kind == "Pod" {
+					pod, err := hc.kubeAPI.CoreV1().Pods(service.Namespace).Get(ctx, addr.TargetRef.Name, metav1.GetOptions{})
+					if err != nil {
+						return err
+					}
+					pods = append(pods, pod)
+				}
+			}
+		}
+
+		if mismatch := misconfiguredOpaquePortAnnotationsInService(service, pods); mismatch != nil {
+			errStrings = append(
+				errStrings,
+				fmt.Sprintf("\t* %s", mismatch.Error()),
+			)
+		}
+	}
+
+	if len(errStrings) >= 1 {
+		return fmt.Errorf(strings.Join(errStrings, "\n    "))
+	}
+
+	return nil
+}
+
+func misconfiguredOpaquePortAnnotationsInService(service corev1.Service, pods []*corev1.Pod) error {
+	for _, pod := range pods {
+		if err := misconfiguredOpaqueAnnotation(service, pod); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func misconfiguredOpaqueAnnotation(service corev1.Service, pod *corev1.Pod) error {
+	svcAnnotation, svcAnnotationOk := service.Annotations[k8s.ProxyOpaquePortsAnnotation]
+	podAnnotation, podAnnotationOk := pod.Annotations[k8s.ProxyOpaquePortsAnnotation]
+
+	if svcAnnotationOk && podAnnotationOk {
+		if svcAnnotation != podAnnotation {
+			return fmt.Errorf("pod/%s and service/%s have the annotation %s but values don't match", pod.Name, service.Name, k8s.ProxyOpaquePortsAnnotation)
+		}
+
+		return nil
+	}
+
+	if svcAnnotationOk {
+		return fmt.Errorf("service/%s has the annotation %s but pod/%s doesn't", service.Name, k8s.ProxyOpaquePortsAnnotation, pod.Name)
+	}
+	if podAnnotationOk {
+		return fmt.Errorf("pod/%s has the annotation %s but service/%s doesn't", pod.Name, k8s.ProxyOpaquePortsAnnotation, service.Name)
+	}
+
+	return nil
+}
+
 // GetDataPlanePods returns all the pods with data plane
 func (hc *HealthChecker) GetDataPlanePods(ctx context.Context) ([]corev1.Pod, error) {
 	selector := fmt.Sprintf("%s=%s", k8s.ControllerNSLabel, hc.ControlPlaneNamespace)
@@ -2453,7 +2603,7 @@ func getPodStatuses(pods []corev1.Pod) map[string]map[string][]corev1.ContainerS
 func validateControlPlanePods(pods []corev1.Pod) error {
 	statuses := getPodStatuses(pods)
 
-	names := []string{"identity", "proxy-injector"}
+	names := []string{"destination", "identity", "proxy-injector"}
 
 	for _, name := range names {
 		pods, found := statuses[name]
@@ -2506,7 +2656,7 @@ func validateDataPlanePods(pods []corev1.Pod, targetNamespace string) error {
 			continue
 		}
 
-		if status != "Running" && status != "Evicted" {
+		if status != string(corev1.PodRunning) && status != "Evicted" {
 			return fmt.Errorf("The \"%s\" pod is not running", pod.Name)
 		}
 
@@ -2580,7 +2730,7 @@ func CheckPodsRunning(pods []corev1.Pod, podsNotFoundMsg string) error {
 		return fmt.Errorf(podsNotFoundMsg)
 	}
 	for _, pod := range pods {
-		if pod.Status.Phase != "Running" {
+		if pod.Status.Phase != corev1.PodRunning {
 			return fmt.Errorf("%s status is %s", pod.Name, pod.Status.Phase)
 		}
 
diff --git a/pkg/inject/inject.go b/pkg/inject/inject.go
index 8cadd4208..fd59697a0 100644
--- a/pkg/inject/inject.go
+++ b/pkg/inject/inject.go
@@ -121,7 +121,7 @@ type ResourceConfig struct {
 		meta *metav1.ObjectMeta
 		// This fields hold labels and annotations which are to be added to the
 		// injected resource. This is different from meta.Labels and
-		// meta.Annotationswhich are the labels and annotations on the original
+		// meta.Annotations which are the labels and annotations on the original
 		// resource before injection.
 		labels      map[string]string
 		annotations map[string]string
@@ -248,7 +248,7 @@ func (conf *ResourceConfig) ParseMetaAndYAML(bytes []byte) (*Report, error) {
 }
 
 // GetOverriddenValues returns the final Values struct which is created
-// by overiding annoatated configuration on top of default Values
+// by overriding annotated configuration on top of default Values
 func (conf *ResourceConfig) GetOverriddenValues() (*linkerd2.Values, error) {
 	// Make a copy of Values and mutate that
 	copyValues, err := conf.values.DeepCopy()
diff --git a/pkg/k8s/api.go b/pkg/k8s/api.go
index 45dd08d2e..55e1014c2 100644
--- a/pkg/k8s/api.go
+++ b/pkg/k8s/api.go
@@ -147,6 +147,15 @@ func (kubeAPI *KubernetesAPI) NamespaceExists(ctx context.Context, namespace str
 	return ns != nil, nil
 }
 
+// GetNodes returs all the nodes in a cluster.
+func (kubeAPI *KubernetesAPI) GetNodes(ctx context.Context) ([]corev1.Node, error) {
+	nodes, err := kubeAPI.CoreV1().Nodes().List(ctx, metav1.ListOptions{})
+	if err != nil {
+		return nil, err
+	}
+	return nodes.Items, nil
+}
+
 // GetPodsByNamespace returns all pods in a given namespace
 func (kubeAPI *KubernetesAPI) GetPodsByNamespace(ctx context.Context, namespace string) ([]corev1.Pod, error) {
 	podList, err := kubeAPI.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{})
@@ -271,7 +280,7 @@ func GetProxyVersion(pod corev1.Pod) string {
 	for _, container := range pod.Spec.Containers {
 		if container.Name == ProxyContainerName {
 			parts := strings.Split(container.Image, ":")
-			return parts[1]
+			return parts[len(parts)-1]
 		}
 	}
 	return ""
diff --git a/pkg/k8s/completion.go b/pkg/k8s/completion.go
index f5f180ecd..f326719e5 100644
--- a/pkg/k8s/completion.go
+++ b/pkg/k8s/completion.go
@@ -29,7 +29,7 @@ func NewCommandCompletion(
 }
 
 // Complete accepts a list of arguments and a substring to generate CLI suggestions.
-// `args` represent a list of arguments a user has already enterd in the CLI. These
+// `args` represent a list of arguments a user has already entered in the CLI. These
 // arguments are used for determining what resource type we'd like to receive
 // suggestions for as well as a list of resources names that have already provided.
 // `toComplete` represents the string prefix of a resource name that we'd like to
@@ -48,11 +48,11 @@ func (c *CommandCompletion) Complete(args []string, toComplete string) ([]string
 
 	suggestions := []string{}
 	if len(args) == 0 && toComplete == "" {
-		return StatAllResourceTypes, nil
+		return CompletionResourceTypes, nil
 	}
 
 	if len(args) == 0 && toComplete != "" {
-		for _, t := range StatAllResourceTypes {
+		for _, t := range CompletionResourceTypes {
 			if strings.HasPrefix(t, toComplete) {
 				suggestions = append(suggestions, t)
 			}
diff --git a/pkg/k8s/k8s.go b/pkg/k8s/k8s.go
index aa989b3c5..f64f77693 100644
--- a/pkg/k8s/k8s.go
+++ b/pkg/k8s/k8s.go
@@ -62,6 +62,21 @@ var AllResources = []string{
 
 // StatAllResourceTypes represents the resources to query in StatSummary when Resource.Type is "all"
 var StatAllResourceTypes = []string{
+	DaemonSet,
+	StatefulSet,
+	Job,
+	Deployment,
+	ReplicationController,
+	Pod,
+	Service,
+	TrafficSplit,
+	Authority,
+	CronJob,
+	ReplicaSet,
+}
+
+// CompletionResourceTypes represents resources the CLI's uses for autocompleting resource type names
+var CompletionResourceTypes = []string{
 	Namespace,
 	DaemonSet,
 	StatefulSet,
@@ -90,6 +105,7 @@ var resourceNames = []resourceName{
 	{"sp", "serviceprofile", "serviceprofiles"},
 	{"sts", "statefulset", "statefulsets"},
 	{"ts", "trafficsplit", "trafficsplits"},
+	{"ln", "link", "links"},
 	{"all", "all", "all"},
 }
 
diff --git a/pkg/k8s/labels.go b/pkg/k8s/labels.go
index ac58ea65f..4bf60d724 100644
--- a/pkg/k8s/labels.go
+++ b/pkg/k8s/labels.go
@@ -84,7 +84,7 @@ const (
 	// Enabled is used by annotations whose valid values include "enabled".
 	Enabled = "enabled"
 
-	// Disabled is used by annotations whose valid values incluce "disabled".
+	// Disabled is used by annotations whose valid values include "disabled".
 	Disabled = "disabled"
 
 	/*
diff --git a/pkg/k8s/resource/resource.go b/pkg/k8s/resource/resource.go
index 798c66511..ecb2559d4 100644
--- a/pkg/k8s/resource/resource.go
+++ b/pkg/k8s/resource/resource.go
@@ -55,7 +55,7 @@ func NewNamespaced(apiVersion, kind, name, namespace string) Kubernetes {
 	}
 }
 
-// RenderResource renders a kuberetes object as a yaml object
+// RenderResource renders a kubernetes object as a yaml object
 func (r Kubernetes) RenderResource(w io.Writer) error {
 	b, err := yaml.Marshal(r)
 	if err != nil {
diff --git a/pkg/tree/tree.go b/pkg/tree/tree.go
index 67bbd5d6e..35576b283 100644
--- a/pkg/tree/tree.go
+++ b/pkg/tree/tree.go
@@ -169,7 +169,7 @@ func BytesToTree(bytes []byte) (Tree, error) {
 	return tree, nil
 }
 
-// Diff mashals two objects into their yaml representations and then performs
+// Diff marshals two objects into their yaml representations and then performs
 // a diff on those Trees.  It returns a Tree which represents all of the fields
 // in y which differ from x.
 func Diff(x interface{}, y interface{}) (Tree, error) {
diff --git a/proxy-identity/main.go b/proxy-identity/main.go
index b522a27e4..e6d7ba7eb 100644
--- a/proxy-identity/main.go
+++ b/proxy-identity/main.go
@@ -6,36 +6,33 @@ import (
 	"crypto/x509"
 	"crypto/x509/pkix"
 	"errors"
-	"flag"
 	"fmt"
 	"io/ioutil"
 	"os"
 	"path/filepath"
+	"syscall"
 
-	"github.com/linkerd/linkerd2/pkg/flags"
 	"github.com/linkerd/linkerd2/pkg/tls"
 	log "github.com/sirupsen/logrus"
 )
 
 const (
+	envDir          = "LINKERD2_PROXY_IDENTITY_DIR"
 	envDisabled     = "LINKERD2_PROXY_IDENTITY_DISABLED"
+	envLocalName    = "LINKERD2_PROXY_IDENTITY_LOCAL_NAME"
 	envTrustAnchors = "LINKERD2_PROXY_IDENTITY_TRUST_ANCHORS"
 )
 
 func main() {
-	cmd := flag.NewFlagSet("public-api", flag.ExitOnError)
-
-	name := cmd.String("name", "", "identity name")
-	dir := cmd.String("dir", "", "directory under which credentials are written")
-
-	flags.ConfigureAndParse(cmd, os.Args[1:])
+	defer runProxy()
 
 	if os.Getenv(envDisabled) != "" {
 		log.Debug("Identity disabled.")
-		os.Exit(0)
+		return
 	}
 
-	keyPath, csrPath, err := checkEndEntityDir(*dir)
+	dir := os.Getenv(envDir)
+	keyPath, csrPath, err := checkEndEntityDir(dir)
 	if err != nil {
 		log.Fatalf("Invalid end-entity directory: %s", err)
 	}
@@ -49,7 +46,8 @@ func main() {
 		log.Fatal(err.Error())
 	}
 
-	if _, err := generateAndStoreCSR(csrPath, *name, key); err != nil {
+	name := os.Getenv(envLocalName)
+	if _, err := generateAndStoreCSR(csrPath, name, key); err != nil {
 		log.Fatal(err.Error())
 	}
 }
@@ -146,3 +144,10 @@ func generateAndStoreCSR(p, id string, key *ecdsa.PrivateKey) ([]byte, error) {
 
 	return csrb, nil
 }
+
+func runProxy() {
+	err := syscall.Exec("/usr/lib/linkerd/linkerd2-proxy", []string{}, os.Environ())
+	if err != nil {
+		log.Fatalf("Failed to run proxy: %s", err)
+	}
+}
diff --git a/proxy-identity/run-proxy.sh b/proxy-identity/run-proxy.sh
deleted file mode 100755
index 44f4e8a38..000000000
--- a/proxy-identity/run-proxy.sh
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/usr/bin/env sh
-set -eu
-
-if [ -z "${LINKERD2_PROXY_IDENTITY_DISABLED:-}" ]; then
-    /usr/lib/linkerd/linkerd2-proxy-identity \
-        -dir "$LINKERD2_PROXY_IDENTITY_DIR" \
-        -name "$LINKERD2_PROXY_IDENTITY_LOCAL_NAME"
-fi
-
-exec /usr/lib/linkerd/linkerd2-proxy
diff --git a/viz/charts/linkerd-viz/README.md b/viz/charts/linkerd-viz/README.md
index 0e478f480..3d2b3610f 100644
--- a/viz/charts/linkerd-viz/README.md
+++ b/viz/charts/linkerd-viz/README.md
@@ -43,7 +43,7 @@ helm install linkerd/linkerd-viz
 
 ## Get involved
 
-* Check out Linkerd's source code at [Github][linkerd2].
+* Check out Linkerd's source code at [GitHub][linkerd2].
 * Join Linkerd's [user mailing list][linkerd-users], [developer mailing
   list][linkerd-dev], and [announcements mailing list][linkerd-announce].
 * Follow [@linkerd][twitter] on Twitter.
diff --git a/viz/charts/linkerd-viz/README.md.gotmpl b/viz/charts/linkerd-viz/README.md.gotmpl
index 2d395fb13..a19aab08f 100644
--- a/viz/charts/linkerd-viz/README.md.gotmpl
+++ b/viz/charts/linkerd-viz/README.md.gotmpl
@@ -41,7 +41,7 @@ helm install linkerd/linkerd-viz
 
 ## Get involved
 
-* Check out Linkerd's source code at [Github][linkerd2].
+* Check out Linkerd's source code at [GitHub][linkerd2].
 * Join Linkerd's [user mailing list][linkerd-users], [developer mailing
   list][linkerd-dev], and [announcements mailing list][linkerd-announce].
 * Follow [@linkerd][twitter] on Twitter.
diff --git a/viz/charts/linkerd-viz/templates/psp.yaml b/viz/charts/linkerd-viz/templates/psp.yaml
index 5134a6414..444c7e9a1 100644
--- a/viz/charts/linkerd-viz/templates/psp.yaml
+++ b/viz/charts/linkerd-viz/templates/psp.yaml
@@ -1,5 +1,19 @@
 ---
 apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  name: psp
+  namespace: {{.Values.namespace}}
+  labels:
+    linkerd.io/extension: viz
+rules:
+- apiGroups: ['policy', 'extensions']
+  resources: ['podsecuritypolicies']
+  verbs: ['use']
+  resourceNames:
+  - linkerd-{{.Values.linkerdNamespace}}-control-plane
+---
+apiVersion: rbac.authorization.k8s.io/v1
 kind: RoleBinding
 metadata:
   name: viz-psp
@@ -28,3 +42,9 @@ subjects:
   name: prometheus
   namespace: {{.Values.namespace}}
 {{ end -}}
+- kind: ServiceAccount
+  name: metrics-api
+  namespace: {{.Values.namespace}}
+- kind: ServiceAccount
+  name: tap-injector
+  namespace: {{.Values.namespace}}
diff --git a/viz/cmd/root.go b/viz/cmd/root.go
index 59f24bc6b..e832da7ae 100644
--- a/viz/cmd/root.go
+++ b/viz/cmd/root.go
@@ -5,6 +5,7 @@ import (
 	"regexp"
 
 	"github.com/fatih/color"
+	pkgcmd "github.com/linkerd/linkerd2/pkg/cmd"
 	"github.com/linkerd/linkerd2/pkg/healthcheck"
 	log "github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
@@ -14,6 +15,10 @@ const (
 	// ExtensionName is the value that the viz extension resources should be labeled with
 	ExtensionName = "viz"
 
+	// LegacyExtensionName is the value that the viz extension resources were labeled with
+	// until 15d1809bd043192bb21cacbc96112cce35bf384f
+	LegacyExtensionName = "linkerd-viz"
+
 	vizChartName            = "linkerd-viz"
 	defaultLinkerdNamespace = "linkerd"
 	maxRps                  = 100.0
@@ -42,7 +47,7 @@ var (
 	alphaNumDash = regexp.MustCompile(`^[a-zA-Z0-9-]+$`)
 )
 
-// NewCmdViz returns a new jeager command
+// NewCmdViz returns a new jaeger command
 func NewCmdViz() *cobra.Command {
 	vizCmd := &cobra.Command{
 		Use:   "viz",
@@ -83,5 +88,11 @@ func NewCmdViz() *cobra.Command {
 	vizCmd.AddCommand(NewCmdTop())
 	vizCmd.AddCommand(newCmdUninstall())
 
+	// resource-aware completion flag configurations
+	pkgcmd.ConfigureNamespaceFlagCompletion(
+		vizCmd, []string{"linkerd-namespace"},
+		kubeconfigPath, impersonate, impersonateGroup, kubeContext)
+
+	pkgcmd.ConfigureKubeContextFlagCompletion(vizCmd, kubeconfigPath)
 	return vizCmd
 }
diff --git a/viz/cmd/uninstall.go b/viz/cmd/uninstall.go
index a882b4f58..e1caeaa90 100644
--- a/viz/cmd/uninstall.go
+++ b/viz/cmd/uninstall.go
@@ -38,5 +38,10 @@ func uninstallRunE(ctx context.Context) error {
 		return err
 	}
 
-	return pkgCmd.Uninstall(ctx, k8sAPI, fmt.Sprintf("%s=%s", k8s.LinkerdExtensionLabel, ExtensionName))
+	selector, err := pkgCmd.GetLabelSelector(k8s.LinkerdExtensionLabel, ExtensionName, LegacyExtensionName)
+	if err != nil {
+		return err
+	}
+
+	return pkgCmd.Uninstall(ctx, k8sAPI, selector)
 }
diff --git a/web/app/yarn.lock b/web/app/yarn.lock
index 4e996c226..a77f822a2 100644
--- a/web/app/yarn.lock
+++ b/web/app/yarn.lock
@@ -7982,9 +7982,9 @@ normalize-path@^3.0.0, normalize-path@~3.0.0:
   integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
 
 normalize-url@^4.1.0:
-  version "4.5.0"
-  resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129"
-  integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==
+  version "4.5.1"
+  resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a"
+  integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==
 
 npm-run-path@^2.0.0:
   version "2.0.2"
@@ -11156,9 +11156,9 @@ write-file-atomic@^3.0.0:
     typedarray-to-buffer "^3.1.5"
 
 ws@^5.2.0:
-  version "5.2.2"
-  resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f"
-  integrity sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==
+  version "5.2.3"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.3.tgz#05541053414921bc29c63bee14b8b0dd50b07b3d"
+  integrity sha512-jZArVERrMsKUatIdnLzqvcfydI85dvd/Fp1u/VOpfdDWQ4c9qWXe+VIeAbQ5FrDwciAkr+lzofXLz3Kuf26AOA==
   dependencies:
     async-limiter "~1.0.0"
 


Test patch:
diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml
index 44f9004d2..76533f028 100644
--- a/.github/workflows/integration_tests.yml
+++ b/.github/workflows/integration_tests.yml
@@ -1,10 +1,6 @@
 name: Integration tests
 on:
-  pull_request:
-    paths-ignore:
-    - '*.md'
-    - '**/*.md'
-    - 'web/app/package.json'
+  pull_request: {}
   push:
     paths-ignore:
     - '*.md'
@@ -12,6 +8,8 @@ on:
     - 'web/app/package.json'
     branches:
     - main
+permissions:
+  contents: read
 env:
   GH_ANNOTATION: true
 jobs:
@@ -150,11 +148,5 @@ jobs:
         # Validate the CLI version matches the current build tag.
         [[ "$TAG" == "$($HOME/.linkerd version --short --client)" ]]
     - name: Run integration tests
-      if: ${{ !endsWith(matrix.integration_test, 'deep') }}
-      run: |
-        bin/tests --images archive --name ${{ matrix.integration_test }} "$HOME/.linkerd"
-    - name: Run integration tests (deep)
-      if: ${{ endsWith(matrix.integration_test, 'deep') }}
       run: |
-        # deep tests consume a lot of disk
         bin/tests --images archive --cleanup-docker --name ${{ matrix.integration_test }} "$HOME/.linkerd"
diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml
index 3237af44f..c01c59061 100644
--- a/.github/workflows/unit_tests.yml
+++ b/.github/workflows/unit_tests.yml
@@ -1,15 +1,14 @@
 name: Unit tests
 on:
-  pull_request:
-    paths-ignore:
-    - '*.md'
-    - '**/*.md'
+  pull_request: {}
   push:
     paths-ignore:
     - '*.md'
     - '**/*.md'
     branches:
     - main
+permissions: 
+  contents: read
 jobs:
   go_unit_tests:
     name: Go unit tests
diff --git a/TEST.md b/TEST.md
index a45dfdfd5..7628dd959 100644
--- a/TEST.md
+++ b/TEST.md
@@ -12,7 +12,7 @@ of this repo, unless otherwise indicated by a `cd` command.
 
 - [Unit tests](#unit-tests)
   - [Go](#go)
-  - [Javascript](#javascript)
+  - [JavaScript](#javascript)
   - [Shell](#shell)
 - [Integration tests](#integration-tests)
   - [Prerequisites](#prerequisites)
@@ -54,9 +54,9 @@ automatically regenerated with the command:
 go test ./cli/cmd/... --update
 ```
 
-### Javascript
+### JavaScript
 
-Javascript dependencies are managed via [yarn](https://yarnpkg.com/) and
+JavaScript dependencies are managed via [yarn](https://yarnpkg.com/) and
 [webpack](https://webpack.js.org/). We use
 [jest](https://facebook.github.io/jest) as our test runner.
 
diff --git a/bin/_test-helpers.sh b/bin/_test-helpers.sh
index d9c0828b9..94cc7611f 100644
--- a/bin/_test-helpers.sh
+++ b/bin/_test-helpers.sh
@@ -9,6 +9,7 @@ set +e
 export default_test_names=(deep external-issuer external-prometheus-deep helm-deep helm-upgrade uninstall upgrade-edge upgrade-stable)
 export external_resource_test_names=(external-resources)
 export all_test_names=(cluster-domain cni-calico-deep multicluster "${default_test_names[*]}" "${external_resource_test_names[*]}")
+images_load_default=(proxy controller web metrics-api grafana tap)
 
 tests_usage() {
   progname="${0##*/}"
@@ -39,7 +40,10 @@ Available Commands:
     --name: the argument to this option is the specific test to run
     --skip-cluster-create: skip k3d cluster creation step and run tests in an existing cluster
     --skip-cluster-delete: if the tests succeed, don't delete the created resources nor the cluster
-    --images: by default load images into the cluster from the local docker cache (docker), or from tar files located under the 'image-archives' directory (archive), or completely skip image loading (skip)
+    --images: set to 'docker' (default) to load images into the cluster from the local docker cache;
+      set to 'preload' to also load them from the local docker cache, after having pulled them from
+      a public registry (appears to be faster than having k3d pulling them itself);
+      set to 'archive' to load the images from tar files located under the image-archives directory
     --cleanup-docker: delete the 'images-archive' directory and prune the docker cache"
 }
 
@@ -79,7 +83,7 @@ handle_tests_input() {
           tests_usage "$0" >&2
           exit 64
         fi
-        if [[ $images != "docker" && $images != "archive" && $images != "skip" ]]; then
+        if [[ $images != "docker" && $images != "archive" && $images != "preload" ]]; then
           echo 'Error: the argument for --images was invalid' >&2
           tests_usage "$0" >&2
           exit 64
@@ -116,7 +120,7 @@ handle_tests_input() {
           exit 64
         fi
         if [ -n "$linkerd_path" ]; then
-          echo "Multliple linkerd paths specified:" >&2
+          echo "Multiple linkerd paths specified:" >&2
           echo "  $linkerd_path" >&2
           echo "  $1" >&2
           tests_usage "$0" >&2
@@ -163,7 +167,7 @@ handle_cleanup_input() {
           exit 64
         fi
         if [ -n "$linkerd_path" ]; then
-          echo "Multliple linkerd paths specified:" >&2
+          echo "Multiple linkerd paths specified:" >&2
           echo "  $linkerd_path" >&2
           echo "  $1" >&2
           cleanup_usage "$0" >&2
@@ -209,10 +213,6 @@ check_linkerd_binary() {
 
 ##### Cluster helpers #####
 
-create_cluster() {
-  "$bindir"/k3d cluster create "$@"
-}
-
 check_cluster() {
   check_if_k8s_reachable
   check_if_l5d_exists
@@ -235,12 +235,8 @@ setup_cluster() {
 
   test_setup
   if [ -z "$skip_cluster_create" ]; then
-    create_cluster "$@"
+    "$bindir"/k3d cluster create "$@"
     image_load "$name"
-    if [ -n "$cleanup_docker" ]; then
-      rm -rf image-archives
-      docker system prune --force --all
-    fi
   fi
   check_cluster
 }
@@ -283,13 +279,24 @@ Help:
 
 image_load() {
   cluster_name=$1
+  images_load=("${images_load_default[@]}")
+  if [[ "$cluster_name" = *deep ]]; then
+    images_load+=(jaeger-webhook)
+  fi
+  if [ "$cluster_name" = "cni-calico-deep" ]; then
+    images_load+=(cni-plugin)
+  fi
   case $images in
     docker)
-      "$bindir"/image-load --k3d "$cluster_name"
+      "$bindir"/image-load --k3d --cluster "$cluster_name" "${images_load[@]}"
+      exit_on_err "error calling '$bindir/image-load'"
+      ;;
+    preload)
+      "$bindir"/image-load --k3d --cluster "$cluster_name" --preload "${images_load[@]}"
       exit_on_err "error calling '$bindir/image-load'"
       ;;
     archive)
-      "$bindir"/image-load --k3d --archive "$cluster_name"
+      "$bindir"/image-load --k3d --archive --cluster "$cluster_name" "${images_load[@]}"
       exit_on_err "error calling '$bindir/image-load'"
       ;;
   esac
@@ -297,19 +304,20 @@ image_load() {
 
 start_test() {
   local name=$1
+  local config=(--no-hostip --k3s-server-arg '--disable=local-storage,metrics-server')
 
   case $name in
     cluster-domain)
-      config=("$name" --k3s-server-arg --cluster-domain=custom.domain)
+      config=("$name" "${config[@]}" --no-lb --k3s-server-arg --cluster-domain=custom.domain --k3s-server-arg '--disable=servicelb,traefik')
       ;;
     cni-calico-deep)
-      config=("$name" --k3s-server-arg --write-kubeconfig-mode=644 --k3s-server-arg --flannel-backend=none --k3s-server-arg --cluster-cidr=192.168.0.0/16 --k3s-server-arg --disable=traefik)
+      config=("$name" "${config[@]}" --no-lb --k3s-server-arg --write-kubeconfig-mode=644 --k3s-server-arg --flannel-backend=none --k3s-server-arg --cluster-cidr=192.168.0.0/16 --k3s-server-arg '--disable=servicelb,traefik')
       ;;
     multicluster)
-      config=(--network multicluster-test)
+      config=("${config[@]}" --network multicluster-test)
       ;;
     *)
-      config=("$name")
+      config=("$name" "${config[@]}" --no-lb --k3s-server-arg '--disable=servicelb,traefik')
       ;;
   esac
 
@@ -323,6 +331,10 @@ start_test() {
 start_single_test() {
   name=$1
   setup_cluster "$@"
+  if [ -n "$cleanup_docker" ]; then
+    rm -rf image-archives
+    docker system prune --force --all
+  fi
   run_"$name"_test
   exit_on_err "error calling 'run_${name}_test'"
   finish "$name"
@@ -331,6 +343,10 @@ start_single_test() {
 start_multicluster_test() {
   setup_cluster source "$@"
   setup_cluster target "$@"
+  if [ -n "$cleanup_docker" ]; then
+    rm -rf image-archives
+    docker system prune --force --all
+  fi
   run_multicluster_test
   exit_on_err "error calling 'run_multicluster_test'"
   finish source
@@ -562,7 +578,7 @@ run_external-resources_test(){
 }
 
 # exit_on_err should be called right after a command to check the result status
-# and eventually generate a Github error annotation. Do not use after calls to
+# and eventually generate a GitHub error annotation. Do not use after calls to
 # `go test` as that generates its own annotations. Note this should be called
 # outside subshells in order for the script to terminate.
 exit_on_err() {
diff --git a/controller/api/destination/traffic_split_adaptor_test.go b/controller/api/destination/traffic_split_adaptor_test.go
index 7e24178bb..6a04ce654 100644
--- a/controller/api/destination/traffic_split_adaptor_test.go
+++ b/controller/api/destination/traffic_split_adaptor_test.go
@@ -77,11 +77,20 @@ func TestTrafficSplitAdaptor(t *testing.T) {
 		testCompare(t, expected.Spec, listener.Profiles[0].Spec)
 	})
 
-	t.Run("Profile merged with traffic split", func(t *testing.T) {
+	t.Run("Profile merged with traffic split when `dstOverrides` is empty", func(t *testing.T) {
 		listener := watcher.NewBufferingProfileListener()
 		adaptor := newTrafficSplitAdaptor(listener, watcher.ServiceID{Name: "foo", Namespace: "ns"}, watcher.Port(80), "cluster.local")
 
-		adaptor.Update(profile)
+		adaptor.Update(&sp.ServiceProfile{
+			Spec: sp.ServiceProfileSpec{
+				Routes: []*sp.RouteSpec{
+					{
+						Name: "route",
+					},
+				},
+				DstOverrides: []*sp.WeightedDst{},
+			},
+		})
 		adaptor.UpdateTrafficSplit(split)
 
 		if len(listener.Profiles) != 2 {
@@ -106,6 +115,36 @@ func TestTrafficSplitAdaptor(t *testing.T) {
 
 		testCompare(t, expected.Spec, listener.Profiles[1].Spec)
 	})
+
+	t.Run("Profile taking priority over traffic split when `dstOverrides` is not empty", func(t *testing.T) {
+		listener := watcher.NewBufferingProfileListener()
+		adaptor := newTrafficSplitAdaptor(listener, watcher.ServiceID{Name: "foo", Namespace: "ns"}, watcher.Port(80), "cluster.local")
+
+		adaptor.Update(profile)
+		adaptor.UpdateTrafficSplit(split)
+
+		if len(listener.Profiles) != 2 {
+			t.Fatalf("Expected two profile updated, got %d", len(listener.Profiles))
+		}
+
+		expected := &sp.ServiceProfile{
+			Spec: sp.ServiceProfileSpec{
+				Routes: []*sp.RouteSpec{
+					{
+						Name: "route",
+					},
+				},
+				DstOverrides: []*sp.WeightedDst{
+					{
+						Authority: "foo.ns.svc.cluster.local.:80",
+						Weight:    resource.MustParse("1"),
+					},
+				},
+			},
+		}
+
+		testCompare(t, expected.Spec, listener.Profiles[1].Spec)
+	})
 }
 
 func testCompare(t *testing.T, expected interface{}, actual interface{}) {
diff --git a/jaeger/cmd/install_test.go b/jaeger/cmd/install_test.go
index 5e2e0d4dd..e54daed8a 100644
--- a/jaeger/cmd/install_test.go
+++ b/jaeger/cmd/install_test.go
@@ -13,7 +13,7 @@ func TestRender(t *testing.T) {
 	// pin values that are changed by render functions on each test run
 	defaultValues := map[string]interface{}{
 		"webhook": map[string]interface{}{
-			"keyPEM":   "test-webhhook-key-pem",
+			"keyPEM":   "test-webhook-key-pem",
 			"crtPEM":   "test-webhook-crt-pem",
 			"caBundle": "test-webhook-ca-bundle",
 		},
diff --git a/jaeger/cmd/testdata/install_collector_disabled.golden b/jaeger/cmd/testdata/install_collector_disabled.golden
index a58eb8026..6b1e8033f 100644
--- a/jaeger/cmd/testdata/install_collector_disabled.golden
+++ b/jaeger/cmd/testdata/install_collector_disabled.golden
@@ -32,7 +32,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: 09b8c09754744aff9027048d3863d2401e4547f07d87f6e3dee643947adb0976
+        checksum/config: 7cd13a5efac387ff6fffe14bdce45a64b0b462af3e55e8b0532c65a3140914d7
       labels:
         linkerd.io/extension: jaeger
         component: jaeger-injector
@@ -134,7 +134,7 @@ metadata:
 type: kubernetes.io/tls
 data:
   tls.crt: dGVzdC13ZWJob29rLWNydC1wZW0=
-  tls.key: dGVzdC13ZWJoaG9vay1rZXktcGVt
+  tls.key: dGVzdC13ZWJob29rLWtleS1wZW0=
 ---
 apiVersion: admissionregistration.k8s.io/v1
 kind: MutatingWebhookConfiguration
@@ -169,6 +169,39 @@ metadata:
   name: jaeger
   namespace: linkerd-jaeger
 ---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  name: psp
+  namespace: linkerd-jaeger
+  labels:
+    linkerd.io/extension: jaeger
+rules:
+- apiGroups: ['policy', 'extensions']
+  resources: ['podsecuritypolicies']
+  verbs: ['use']
+  resourceNames:
+  - linkerd-linkerd-control-plane
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+  name: jaeger-psp
+  namespace: linkerd-jaeger
+  labels:
+    linkerd.io/extension: jaeger
+roleRef:
+  kind: Role
+  name: psp
+  apiGroup: rbac.authorization.k8s.io
+subjects:
+- kind: ServiceAccount
+  name: jaeger-injector
+  namespace: linkerd-jaeger
+- kind: ServiceAccount
+  name: jaeger
+  namespace: linkerd-jaeger
+---
 ###
 ### Tracing Jaeger Service
 ###
@@ -186,6 +219,8 @@ spec:
   ports:
     - name: collection
       port: 14268
+    - name: grpc
+      port: 14250
     - name: ui
       port: 16686
 ---
@@ -226,6 +261,8 @@ spec:
           name: admin
         - containerPort: 14268
           name: collection
+        - containerPort: 14250
+          name: grpc
         - containerPort: 16686
           name: ui
       dnsPolicy: ClusterFirst
diff --git a/jaeger/cmd/testdata/install_default.golden b/jaeger/cmd/testdata/install_default.golden
index 3f4c56ece..0f79ed75f 100644
--- a/jaeger/cmd/testdata/install_default.golden
+++ b/jaeger/cmd/testdata/install_default.golden
@@ -32,7 +32,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: 93ee231f45614b315429e63eb8c79ca948062c4f47b19c8c273ab943bd182ff3
+        checksum/config: 1388bf30cdbc3236d7a52e0a115ee8ae1095140cb5b89c7b97fd30fdffad07f9
       labels:
         linkerd.io/extension: jaeger
         component: jaeger-injector
@@ -143,7 +143,7 @@ metadata:
 type: kubernetes.io/tls
 data:
   tls.crt: dGVzdC13ZWJob29rLWNydC1wZW0=
-  tls.key: dGVzdC13ZWJoaG9vay1rZXktcGVt
+  tls.key: dGVzdC13ZWJob29rLWtleS1wZW0=
 ---
 apiVersion: admissionregistration.k8s.io/v1
 kind: MutatingWebhookConfiguration
@@ -178,6 +178,42 @@ metadata:
   name: jaeger
   namespace: linkerd-jaeger
 ---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  name: psp
+  namespace: linkerd-jaeger
+  labels:
+    linkerd.io/extension: jaeger
+rules:
+- apiGroups: ['policy', 'extensions']
+  resources: ['podsecuritypolicies']
+  verbs: ['use']
+  resourceNames:
+  - linkerd-linkerd-control-plane
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+  name: jaeger-psp
+  namespace: linkerd-jaeger
+  labels:
+    linkerd.io/extension: jaeger
+roleRef:
+  kind: Role
+  name: psp
+  apiGroup: rbac.authorization.k8s.io
+subjects:
+- kind: ServiceAccount
+  name: collector
+  namespace: linkerd-jaeger
+- kind: ServiceAccount
+  name: jaeger-injector
+  namespace: linkerd-jaeger
+- kind: ServiceAccount
+  name: jaeger
+  namespace: linkerd-jaeger
+---
 ###
 ### Tracing Collector Service
 ###
@@ -191,21 +227,33 @@ metadata:
 data:
   collector-config: |
     receivers:
+      otlp:
+        protocols:
+          grpc:
+          http:
       opencensus:
-        port: 55678
       zipkin:
-        port: 9411
       jaeger:
-        jaeger-thrift-http-port: 14268
-    queued-exporters:
-      jaeger-all-in-one:
-        num-workers: 4
-        queue-size: 100
-        retry-on-failure: true
-        sender-type: jaeger-thrift-http
-        jaeger-thrift-http:
-          collector-endpoint: http://jaeger.linkerd-jaeger:14268/api/traces
-          timeout: 5s
+        protocols:
+          grpc:
+          thrift_http:
+          thrift_compact:
+          thrift_binary:
+    processors:
+      batch:
+    extensions:
+      health_check:
+    exporters:
+      jaeger:
+        endpoint: jaeger.${POD_NAMESPACE}:14250
+        insecure: true
+    service:
+      extensions: [health_check]
+      pipelines:
+        traces:
+          receivers: [otlp,opencensus,zipkin,jaeger]
+          processors: [batch]
+          exporters: [jaeger]
 ---
 apiVersion: v1
 kind: Service
@@ -217,6 +265,10 @@ metadata:
 spec:
   type: ClusterIP
   ports:
+  - name: otlp
+    port: 4317
+    protocol: TCP
+    targetPort: 4317
   - name: opencensus
     port: 55678
     protocol: TCP
@@ -225,10 +277,14 @@ spec:
     port: 9411
     protocol: TCP
     targetPort: 9411
-  - name: jaeger
+  - name: jaeger-thrift
     port: 14268
     protocol: TCP
     targetPort: 14268
+  - name: jaeger-grpc
+    port: 14250
+    protocol: TCP
+    targetPort: 14250
   selector:
     component: collector
 ---
@@ -261,22 +317,29 @@ spec:
         beta.kubernetes.io/os: linux
       containers:
       - command:
-        - /occollector_linux
+        - /otelcol
         - --config=/conf/collector-config.yaml
         env:
         - name: GOGC
           value: "80"
-        image: omnition/opencensus-collector:0.1.11
+        - name: POD_NAMESPACE
+          valueFrom:
+            fieldRef:
+              apiVersion: v1
+              fieldPath: metadata.namespace
+        image: otel/opentelemetry-collector:0.27.0
         imagePullPolicy: Always
         livenessProbe:
           httpGet:
             path: /
             port: 13133
-        name: oc-collector
+        name: ot-collector
         ports:
+        - containerPort: 4317
         - containerPort: 55678
         - containerPort: 9411
         - containerPort: 14268
+        - containerPort: 14250
         - containerPort: 8888
         readinessProbe:
           httpGet:
@@ -311,6 +374,8 @@ spec:
   ports:
     - name: collection
       port: 14268
+    - name: grpc
+      port: 14250
     - name: ui
       port: 16686
 ---
@@ -351,6 +416,8 @@ spec:
           name: admin
         - containerPort: 14268
           name: collection
+        - containerPort: 14250
+          name: grpc
         - containerPort: 16686
           name: ui
       dnsPolicy: ClusterFirst
diff --git a/jaeger/cmd/testdata/install_jaeger_disabled.golden b/jaeger/cmd/testdata/install_jaeger_disabled.golden
index 698aaa1c7..137d840a7 100644
--- a/jaeger/cmd/testdata/install_jaeger_disabled.golden
+++ b/jaeger/cmd/testdata/install_jaeger_disabled.golden
@@ -32,7 +32,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/config: 83c7853cc6f0083c78ce1a1862e1f02c4eccb7d2fa90d3ffa924ac75d728ad95
+        checksum/config: 0cf88aabfdb59ab401cb17b59e26b57277ca6c039ec4837e2523759802565c1b
       labels:
         linkerd.io/extension: jaeger
         component: jaeger-injector
@@ -143,7 +143,7 @@ metadata:
 type: kubernetes.io/tls
 data:
   tls.crt: dGVzdC13ZWJob29rLWNydC1wZW0=
-  tls.key: dGVzdC13ZWJoaG9vay1rZXktcGVt
+  tls.key: dGVzdC13ZWJob29rLWtleS1wZW0=
 ---
 apiVersion: admissionregistration.k8s.io/v1
 kind: MutatingWebhookConfiguration
@@ -169,6 +169,39 @@ webhooks:
     resources: ["pods"]
   sideEffects: None
 ---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  name: psp
+  namespace: linkerd-jaeger
+  labels:
+    linkerd.io/extension: jaeger
+rules:
+- apiGroups: ['policy', 'extensions']
+  resources: ['podsecuritypolicies']
+  verbs: ['use']
+  resourceNames:
+  - linkerd-linkerd-control-plane
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+  name: jaeger-psp
+  namespace: linkerd-jaeger
+  labels:
+    linkerd.io/extension: jaeger
+roleRef:
+  kind: Role
+  name: psp
+  apiGroup: rbac.authorization.k8s.io
+subjects:
+- kind: ServiceAccount
+  name: collector
+  namespace: linkerd-jaeger
+- kind: ServiceAccount
+  name: jaeger-injector
+  namespace: linkerd-jaeger
+---
 ###
 ### Tracing Collector Service
 ###
@@ -182,21 +215,33 @@ metadata:
 data:
   collector-config: |
     receivers:
+      otlp:
+        protocols:
+          grpc:
+          http:
       opencensus:
-        port: 55678
       zipkin:
-        port: 9411
       jaeger:
-        jaeger-thrift-http-port: 14268
-    queued-exporters:
-      jaeger-all-in-one:
-        num-workers: 4
-        queue-size: 100
-        retry-on-failure: true
-        sender-type: jaeger-thrift-http
-        jaeger-thrift-http:
-          collector-endpoint: http://jaeger.linkerd-jaeger:14268/api/traces
-          timeout: 5s
+        protocols:
+          grpc:
+          thrift_http:
+          thrift_compact:
+          thrift_binary:
+    processors:
+      batch:
+    extensions:
+      health_check:
+    exporters:
+      jaeger:
+        endpoint: jaeger.${POD_NAMESPACE}:14250
+        insecure: true
+    service:
+      extensions: [health_check]
+      pipelines:
+        traces:
+          receivers: [otlp,opencensus,zipkin,jaeger]
+          processors: [batch]
+          exporters: [jaeger]
 ---
 apiVersion: v1
 kind: Service
@@ -208,6 +253,10 @@ metadata:
 spec:
   type: ClusterIP
   ports:
+  - name: otlp
+    port: 4317
+    protocol: TCP
+    targetPort: 4317
   - name: opencensus
     port: 55678
     protocol: TCP
@@ -216,10 +265,14 @@ spec:
     port: 9411
     protocol: TCP
     targetPort: 9411
-  - name: jaeger
+  - name: jaeger-thrift
     port: 14268
     protocol: TCP
     targetPort: 14268
+  - name: jaeger-grpc
+    port: 14250
+    protocol: TCP
+    targetPort: 14250
   selector:
     component: collector
 ---
@@ -252,22 +305,29 @@ spec:
         beta.kubernetes.io/os: linux
       containers:
       - command:
-        - /occollector_linux
+        - /otelcol
         - --config=/conf/collector-config.yaml
         env:
         - name: GOGC
           value: "80"
-        image: omnition/opencensus-collector:0.1.11
+        - name: POD_NAMESPACE
+          valueFrom:
+            fieldRef:
+              apiVersion: v1
+              fieldPath: metadata.namespace
+        image: otel/opentelemetry-collector:0.27.0
         imagePullPolicy: Always
         livenessProbe:
           httpGet:
             path: /
             port: 13133
-        name: oc-collector
+        name: ot-collector
         ports:
+        - containerPort: 4317
         - containerPort: 55678
         - containerPort: 9411
         - containerPort: 14268
+        - containerPort: 14250
         - containerPort: 8888
         readinessProbe:
           httpGet:
diff --git a/pkg/healthcheck/healthcheck_test.go b/pkg/healthcheck/healthcheck_test.go
index ee3c9ae8f..772133e18 100644
--- a/pkg/healthcheck/healthcheck_test.go
+++ b/pkg/healthcheck/healthcheck_test.go
@@ -1204,6 +1204,107 @@ data:
 	}
 }
 
+func TestCheckClusterNetworks(t *testing.T) {
+	var testCases = []struct {
+		checkDescription string
+		k8sConfigs       []string
+		expected         []string
+	}{
+		{
+			checkDescription: "cluster networks contains all node podCIDRs",
+			k8sConfigs: []string{`
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: test-ns
+`,
+				`
+apiVersion: v1
+kind: Node
+metadata:
+  name: linkerd-test-ns-identity
+spec:
+  podCIDR: 90.10.90.24/24
+`,
+				`
+kind: ConfigMap
+apiVersion: v1
+metadata:
+  name: linkerd-config
+  namespace: test-ns
+  labels:
+    linkerd.io/control-plane-ns: test-ns
+data:
+  values: |
+    clusterNetworks: "10.0.0.0/8,100.64.0.0/10,172.16.0.0/12,192.168.0.0/16"
+`,
+			},
+			expected: []string{
+				"linkerd-existence cluster networks contains all node podCIDRs: node has podCIDR(s) [90.10.90.24/24] which are not contained in the Linkerd clusterNetworks.\n\tTry installing linkerd via --set clusterNetworks=90.10.90.24/24",
+			},
+		},
+		{
+			checkDescription: "cluster networks contains all node podCIDRs",
+			k8sConfigs: []string{`
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: test-ns
+`,
+				`
+apiVersion: v1
+kind: Node
+metadata:
+  name: linkerd-test-ns-identity
+spec:
+  podCIDR: 10.0.0.24/24
+`,
+				`
+kind: ConfigMap
+apiVersion: v1
+metadata:
+  name: linkerd-config
+  namespace: test-ns
+  labels:
+    linkerd.io/control-plane-ns: test-ns
+data:
+  values: |
+    clusterNetworks: "10.0.0.0/8,100.64.0.0/10,172.16.0.0/12,192.168.0.0/16"
+`,
+			},
+			expected: []string{
+				"linkerd-existence cluster networks contains all node podCIDRs",
+			},
+		},
+	}
+
+	for i, tc := range testCases {
+		tc := tc // pin
+		t.Run(fmt.Sprintf("%d: returns expected config result", i), func(t *testing.T) {
+			hc := NewHealthChecker(
+				[]CategoryID{},
+				&Options{
+					ControlPlaneNamespace: "test-ns",
+				},
+			)
+
+			var err error
+			hc.kubeAPI, err = k8s.NewFakeAPI(tc.k8sConfigs...)
+			if err != nil {
+				t.Fatalf("Unexpected error: %s", err)
+			}
+
+			obs := newObserver()
+			hc.addCheckAsCategory("linkerd-existence", LinkerdControlPlaneExistenceChecks,
+				tc.checkDescription)
+			hc.RunChecks(obs.resultFn)
+			if !reflect.DeepEqual(obs.results, tc.expected) {
+				t.Fatalf("Expected results\n%s,\nbut got:\n%s", strings.Join(tc.expected, "\n"), strings.Join(obs.results, "\n"))
+			}
+		})
+	}
+}
+
 func proxiesWithCertificates(certificates ...string) []string {
 	result := []string{}
 	for i, certificate := range certificates {
@@ -1323,6 +1424,7 @@ func TestValidateControlPlanePods(t *testing.T) {
 	t.Run("Returns an error if not all pods are running", func(t *testing.T) {
 		pods := []corev1.Pod{
 			pod("linkerd-grafana-5b7d796646-hh46d", corev1.PodRunning, true),
+			pod("linkerd-destination-9849948665-37082", corev1.PodFailed, true),
 			pod("linkerd-identity-6849948664-27982", corev1.PodFailed, true),
 		}
 
@@ -1330,7 +1432,7 @@ func TestValidateControlPlanePods(t *testing.T) {
 		if err == nil {
 			t.Fatal("Expected error, got nothing")
 		}
-		if err.Error() != "No running pods for \"linkerd-identity\"" {
+		if err.Error() != "No running pods for \"linkerd-destination\"" {
 			t.Fatalf("Unexpected error message: %s", err.Error())
 		}
 	})
@@ -1349,6 +1451,7 @@ func TestValidateControlPlanePods(t *testing.T) {
 
 	t.Run("Returns nil if all pods are running and all containers are ready", func(t *testing.T) {
 		pods := []corev1.Pod{
+			pod("linkerd-destination-9849948665-37082", corev1.PodRunning, true),
 			pod("linkerd-identity-6849948664-27982", corev1.PodRunning, true),
 			pod("linkerd-proxy-injector-5f79ff4844-", corev1.PodRunning, true),
 		}
@@ -1361,6 +1464,9 @@ func TestValidateControlPlanePods(t *testing.T) {
 
 	t.Run("Returns nil if, HA mode, at least one pod of each control plane component is ready", func(t *testing.T) {
 		pods := []corev1.Pod{
+			pod("linkerd-destination-9843948665-48082", corev1.PodRunning, true),
+			pod("linkerd-destination-9843948665-48083", corev1.PodRunning, false),
+			pod("linkerd-destination-9843948665-48084", corev1.PodFailed, false),
 			pod("linkerd-identity-6849948664-27982", corev1.PodRunning, true),
 			pod("linkerd-identity-6849948664-27983", corev1.PodRunning, false),
 			pod("linkerd-identity-6849948664-27984", corev1.PodFailed, false),
@@ -1375,6 +1481,7 @@ func TestValidateControlPlanePods(t *testing.T) {
 
 	t.Run("Returns nil if all linkerd pods are running and pod list includes non-linkerd pod", func(t *testing.T) {
 		pods := []corev1.Pod{
+			pod("linkerd-destination-9843948665-48082", corev1.PodRunning, true),
 			pod("linkerd-identity-6849948664-27982", corev1.PodRunning, true),
 			pod("linkerd-proxy-injector-5f79ff4844-", corev1.PodRunning, true),
 			pod("hello-43c25d", corev1.PodRunning, true),
@@ -1773,12 +1880,11 @@ func TestDataPlanePodLabels(t *testing.T) {
 			tc := tc //pin
 			t.Run(tc.description, func(t *testing.T) {
 				err := checkMisconfiguredPodsLabels(tc.pods)
-				fmt.Println(err.Error())
 
 				if err == nil {
 					t.Fatal("Expected error, got nothing")
 				}
-				fmt.Println(err.Error())
+
 				if err.Error() != tc.expectedErrorMsg {
 					t.Fatalf("Unexpected error message: %s", err.Error())
 				}
@@ -3297,6 +3403,348 @@ func TestMinReplicaCheck(t *testing.T) {
 	}
 }
 
+func TestCheckOpaquePortAnnotations(t *testing.T) {
+	hc := NewHealthChecker(
+		[]CategoryID{LinkerdOpaquePortsDefinitionChecks},
+		&Options{
+			DataPlaneNamespace: "test-ns",
+		},
+	)
+
+	var err error
+
+	var testCases = []struct {
+		resources []string
+		expected  error
+	}{
+		{
+			resources: []string{`
+apiVersion: v1
+kind: Service
+metadata:
+  name: test-service-1
+  namespace: test-ns
+spec:
+  ports:
+    - name: elasticsearch
+      port: 9200
+      protocol: TCP
+      targetPort: 9200
+  selector:
+    service: service-1
+`,
+				`
+apiVersion: v1
+kind: Pod
+metadata:
+  name: my-service-deployment
+  namespace: test-ns
+  labels:
+    service: service-1
+spec:
+  containers:
+    - name: test
+      image: "test-service"
+`,
+				`
+apiVersion: v1
+kind: Endpoints
+metadata:
+  annotations:
+    endpoints.kubernetes.io/last-change-trigger-time: "2021-06-08T08:38:16Z"
+  creationTimestamp: "2021-06-08T08:38:03Z"
+  labels:
+    service: test-service-1
+  name: test-service-1
+  namespace: test-ns
+subsets:
+- addresses:
+  - ip: 10.244.3.12
+    nodeName: nodename-1
+    targetRef:
+      kind: Pod
+      name: my-service-deployment
+      namespace: test-ns
+      resourceVersion: "20661"
+      uid: b37782aa-1458-4153-8399-dabc2b29aaae
+  ports:
+  - name: http-port
+    port: 8080
+    protocol: TCP
+`,
+			},
+			expected: nil,
+		},
+		{
+			resources: []string{`
+apiVersion: v1
+kind: Service
+metadata:
+  name: test-service-1
+  namespace: test-ns
+  annotations:
+    config.linkerd.io/opaque-ports: "9200"
+spec:
+  ports:
+    - name: elasticsearch
+      port: 9200
+      protocol: TCP
+      targetPort: 9200
+  selector:
+    service: service-1
+`,
+				`
+apiVersion: v1
+kind: Pod
+metadata:
+  name: my-service-deployment
+  namespace: test-ns
+  service: service-1
+  labels:
+    service: service-1
+  annotations:
+    config.linkerd.io/opaque-ports: "9200"
+spec:
+  containers:
+    - name: test
+      image: "test-service"
+`,
+				`
+apiVersion: v1
+kind: Endpoints
+metadata:
+  annotations:
+    endpoints.kubernetes.io/last-change-trigger-time: "2021-06-08T08:38:16Z"
+  creationTimestamp: "2021-06-08T08:38:03Z"
+  labels:
+    service: test-service-1
+  name: test-service-1
+  namespace: test-ns
+subsets:
+- addresses:
+  - ip: 10.244.3.12
+    nodeName: nodename-1
+    targetRef:
+      kind: Pod
+      name: my-service-deployment
+      namespace: test-ns
+      resourceVersion: "20661"
+      uid: b37782aa-1458-4153-8399-dabc2b29aaae
+  ports:
+  - name: http-port
+    port: 8080
+    protocol: TCP			
+`,
+			},
+			expected: nil,
+		},
+		{
+			resources: []string{`
+apiVersion: v1
+kind: Service
+metadata:
+  name: test-service-1
+  namespace: test-ns
+spec:
+  ports:
+    - name: http
+      port: 9200
+      protocol: TCP
+      targetPort: 9200
+  selector:
+    service: service-1
+`,
+				`
+apiVersion: v1
+kind: Pod
+metadata:
+  name: my-service-deployment
+  namespace: test-ns
+  service: service-1
+  labels:
+    service: service-1
+  annotations:
+    config.linkerd.io/opaque-ports: "9200"
+spec:
+  containers:
+    - name: test
+      image: "test-service"
+`,
+				`
+apiVersion: v1
+kind: Endpoints
+metadata:
+  annotations:
+    endpoints.kubernetes.io/last-change-trigger-time: "2021-06-08T08:38:16Z"
+  creationTimestamp: "2021-06-08T08:38:03Z"
+  labels:
+    service: test-service-1
+  name: test-service-1
+  namespace: test-ns
+subsets:
+- addresses:
+  - ip: 10.244.3.12
+    nodeName: nodename-1
+    targetRef:
+      kind: Pod
+      name: my-service-deployment
+      namespace: test-ns
+      resourceVersion: "20661"
+      uid: b37782aa-1458-4153-8399-dabc2b29aaae
+  ports:
+  - name: http-port
+    port: 8080
+    protocol: TCP
+`,
+			},
+			expected: fmt.Errorf("\t* pod/my-service-deployment has the annotation %s but service/test-service-1 doesn't", k8s.ProxyOpaquePortsAnnotation),
+		},
+		{
+			resources: []string{`
+apiVersion: v1
+kind: Service
+metadata:
+  name: test-service-1
+  namespace: test-ns
+  annotations:
+    config.linkerd.io/opaque-ports: "9200"
+spec:
+  ports:
+    - name: http
+      port: 9200
+      protocol: TCP
+      targetPort: 9200
+  selector:
+    service: service-1
+`,
+				`
+apiVersion: v1
+kind: Pod
+metadata:
+  name: my-service-deployment
+  namespace: test-ns
+  service: service-1
+  labels:
+    service: service-1
+spec:
+  containers:
+    - name: test
+      image: "test-service"
+`,
+				`
+apiVersion: v1
+kind: Endpoints
+metadata:
+  annotations:
+    endpoints.kubernetes.io/last-change-trigger-time: "2021-06-08T08:38:16Z"
+  creationTimestamp: "2021-06-08T08:38:03Z"
+  labels:
+    service: test-service-1
+  name: test-service-1
+  namespace: test-ns
+subsets:
+- addresses:
+  - ip: 10.244.3.12
+    nodeName: nodename-1
+    targetRef:
+      kind: Pod
+      name: my-service-deployment
+      namespace: test-ns
+      resourceVersion: "20661"
+      uid: b37782aa-1458-4153-8399-dabc2b29aaae
+  ports:
+  - name: http-port
+    port: 8080
+    protocol: TCP		
+`,
+			},
+			expected: fmt.Errorf("\t* service/test-service-1 has the annotation %s but pod/my-service-deployment doesn't", k8s.ProxyOpaquePortsAnnotation),
+		},
+		{
+			resources: []string{`
+apiVersion: v1
+kind: Service
+metadata:
+  name: test-service-1
+  namespace: test-ns
+  annotations:
+    config.linkerd.io/opaque-ports: "9200"
+spec:
+  ports:
+    - name: elasticsearch
+      port: 9200
+      protocol: TCP
+      targetPort: 9200
+  selector:
+    service: service-1
+`,
+				`
+apiVersion: v1
+kind: Pod
+metadata:
+  name: my-service-deployment
+  namespace: test-ns
+  service: service-1
+  labels:
+    service: service-1
+  annotations:
+    config.linkerd.io/opaque-ports: "9300"
+spec:
+  containers:
+    - name: test
+      image: "test-service"
+`,
+				`
+apiVersion: v1
+kind: Endpoints
+metadata:
+  annotations:
+    endpoints.kubernetes.io/last-change-trigger-time: "2021-06-08T08:38:16Z"
+  creationTimestamp: "2021-06-08T08:38:03Z"
+  labels:
+    service: test-service-1
+  name: test-service-1
+  namespace: test-ns
+subsets:
+- addresses:
+  - ip: 10.244.3.12
+    nodeName: nodename-1
+    targetRef:
+      kind: Pod
+      name: my-service-deployment
+      namespace: test-ns
+      resourceVersion: "20661"
+      uid: b37782aa-1458-4153-8399-dabc2b29aaae
+  ports:
+  - name: http-port
+    port: 8080
+    protocol: TCP
+`,
+			},
+			expected: fmt.Errorf("\t* pod/my-service-deployment and service/test-service-1 have the annotation %s but values don't match", k8s.ProxyOpaquePortsAnnotation),
+		},
+	}
+
+	for i, tc := range testCases {
+		tc := tc //pin
+		t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+			hc.kubeAPI, err = k8s.NewFakeAPI(tc.resources...)
+			if err != nil {
+				t.Fatalf("unexpected error: %s", err)
+			}
+			err = hc.checkMisconfiguredOpaquePortAnnotations(context.Background())
+			if err == nil && tc.expected != nil {
+				t.Fatalf("Expected check to be successful, got: %s", err)
+			}
+			if err != nil {
+				if err.Error() != tc.expected.Error() {
+					t.Fatalf("Expected error: %s, received: %s", tc.expected, err)
+				}
+			}
+		})
+	}
+}
+
 type controlPlaneReplicaOptions struct {
 	destination   int
 	identity      int
diff --git a/pkg/tree/tree_test.go b/pkg/tree/tree_test.go
index 2db866541..241cb9fdd 100644
--- a/pkg/tree/tree_test.go
+++ b/pkg/tree/tree_test.go
@@ -34,7 +34,7 @@ func TestTreeGetString(t *testing.T) {
 		},
 		{
 			values,
-			[]string{"global", "namespac"},
+			[]string{"global", "namespace"},
 			"",
 			fmt.Errorf("could not find node global"),
 		},
diff --git a/test/integration/install_test.go b/test/integration/install_test.go
index 924dfce02..ee92599c8 100644
--- a/test/integration/install_test.go
+++ b/test/integration/install_test.go
@@ -430,7 +430,7 @@ func TestInstallOrUpgradeCli(t *testing.T) {
 			"'kubectl apply' command failed\n%s", cmdOut)
 	}
 
-	TestHelper.WaitRollout(t)
+	TestHelper.WaitRollout(t, testutil.LinkerdDeployReplicasEdge)
 
 	if TestHelper.ExternalPrometheus() {
 
@@ -446,7 +446,7 @@ func TestInstallOrUpgradeCli(t *testing.T) {
 				"kubectl apply command failed\n%s", out)
 		}
 
-		// Update args to use external proemtheus
+		// Update args to use external prometheus
 		vizArgs = append(vizArgs, "--set", "prometheusUrl=http://prometheus.external-prometheus.svc.cluster.local:9090", "--set", "prometheus.enabled=false")
 	}
 
@@ -462,6 +462,17 @@ func TestInstallOrUpgradeCli(t *testing.T) {
 		testutil.AnnotatedFatalf(t, "'kubectl apply' command failed",
 			"'kubectl apply' command failed\n%s", out)
 	}
+
+	// It is necessary to clone LinkerdVizDeployReplicas so that we do not
+	// mutate it's original value.
+	expectedVizDeployments := make(map[string]testutil.DeploySpec)
+	for k, v := range testutil.LinkerdVizDeployReplicas {
+		expectedVizDeployments[k] = v
+	}
+	if TestHelper.ExternalPrometheus() {
+		delete(expectedVizDeployments, "prometheus")
+	}
+	TestHelper.WaitRollout(t, expectedVizDeployments)
 }
 
 // These need to be updated (if there are changes) once a new stable is released
@@ -535,13 +546,23 @@ func TestInstallHelm(t *testing.T) {
 		testutil.AnnotatedFatalf(t, "'helm install' command failed",
 			"'helm install' command failed\n%s\n%s", stdout, stderr)
 	}
-
-	TestHelper.WaitRollout(t)
+	TestHelper.WaitRollout(t, testutil.LinkerdDeployReplicasEdge)
 
 	if stdout, stderr, err := TestHelper.HelmCmdPlain("install", vizChartToInstall, "l5d-viz", vizArgs...); err != nil {
 		testutil.AnnotatedFatalf(t, "'helm install' command failed",
 			"'helm install' command failed\n%s\n%s", stdout, stderr)
 	}
+
+	// It is necessary to clone LinkerdVizDeployReplicas so that we do not
+	// mutate it's original value.
+	expectedVizDeployments := make(map[string]testutil.DeploySpec)
+	for k, v := range testutil.LinkerdVizDeployReplicas {
+		expectedVizDeployments[k] = v
+	}
+	if TestHelper.ExternalPrometheus() {
+		delete(expectedVizDeployments, "prometheus")
+	}
+	TestHelper.WaitRollout(t, expectedVizDeployments)
 }
 
 func TestControlPlaneResourcesPostInstall(t *testing.T) {
@@ -637,12 +658,25 @@ func TestUpgradeHelm(t *testing.T) {
 		testutil.AnnotatedFatalf(t, "'helm upgrade' command failed",
 			"'helm upgrade' command failed\n%s\n%s", stdout, stderr)
 	}
+	TestHelper.WaitRollout(t, testutil.LinkerdDeployReplicasEdge)
 
 	vizChart := TestHelper.GetLinkerdVizHelmChart()
 	if stdout, stderr, err := TestHelper.HelmCmdPlain("upgrade", vizChart, "l5d-viz", vizArgs...); err != nil {
 		testutil.AnnotatedFatalf(t, "'helm upgrade' command failed",
 			"'helm upgrade' command failed\n%s\n%s", stdout, stderr)
 	}
+
+	// It is necessary to clone LinkerdVizDeployReplicas so that we do not
+	// mutate it's original value.
+	expectedVizDeployments := make(map[string]testutil.DeploySpec)
+	for k, v := range testutil.LinkerdVizDeployReplicas {
+		expectedVizDeployments[k] = v
+	}
+	if TestHelper.ExternalPrometheus() {
+		delete(expectedVizDeployments, "prometheus")
+	}
+	TestHelper.WaitRollout(t, expectedVizDeployments)
+
 	TestHelper.AddInstalledExtension(vizExtensionName)
 }
 
@@ -943,12 +977,18 @@ func getCheckOutput(t *testing.T, goldenFile string, namespace string) string {
 		testutil.AnnotatedFatal(t, fmt.Sprintf("failed to retrieve pods: %s", err), err)
 	}
 
+	proxyVersionErr := ""
+	err = healthcheck.CheckProxyVersionsUpToDate(pods, version.Channels{})
+	if err != nil {
+		proxyVersionErr = err.Error()
+	}
+
 	tpl := template.Must(template.ParseFiles("testdata" + "/" + goldenFile))
 	vars := struct {
 		ProxyVersionErr string
 		HintURL         string
 	}{
-		healthcheck.CheckProxyVersionsUpToDate(pods, version.Channels{}).Error(),
+		proxyVersionErr,
 		healthcheck.HintBaseURL(TestHelper.GetVersion()),
 	}
 
@@ -995,7 +1035,7 @@ func TestCheckViz(t *testing.T) {
 		testutil.AnnotatedFatal(t, fmt.Sprintf("failed to parse check.viz.golden template: %s", err), err)
 	}
 
-	timeout := time.Minute
+	timeout := 5 * time.Minute
 	err = TestHelper.RetryFor(timeout, func() error {
 		out, err := TestHelper.LinkerdRun(cmd...)
 		if err != nil {
diff --git a/test/integration/opaqueports/opaque_ports_test.go b/test/integration/opaqueports/opaque_ports_test.go
index b56b465a2..ae310f89a 100644
--- a/test/integration/opaqueports/opaque_ports_test.go
+++ b/test/integration/opaqueports/opaque_ports_test.go
@@ -94,7 +94,7 @@ func TestOpaquePorts(t *testing.T) {
 	// Wait for slow-cookers to start sending requests
 	time.Sleep(20 * time.Second)
 
-	t.Run("expect absent HTTP outbound requests for opaque-pod slow clooker", func(t *testing.T) {
+	t.Run("expect absent HTTP outbound requests for opaque-pod slow cooker", func(t *testing.T) {
 		// Check the slow cooker metrics
 		pods, err := TestHelper.GetPods(ctx, opaquePortsNs, map[string]string{"app": opaquePodSC})
 		if err != nil {
diff --git a/test/integration/smimetrics/smi-metrics_test.go b/test/integration/smimetrics/smi-metrics_test.go
index b17171344..0607ecf5c 100644
--- a/test/integration/smimetrics/smi-metrics_test.go
+++ b/test/integration/smimetrics/smi-metrics_test.go
@@ -148,7 +148,7 @@ func checkTrafficMetrics(metrics v1alpha1.TrafficMetrics, name string) error {
 	if metrics.Name == name {
 		return nil
 	}
-	return fmt.Errorf("excpected %s, but got %s", name, metrics.Name)
+	return fmt.Errorf("expected %s, but got %s", name, metrics.Name)
 
 }
 
@@ -156,5 +156,5 @@ func checkTrafficMetricsList(metrics v1alpha1.TrafficMetricsList, name string, n
 	if metrics.Resource.Name == name && len(metrics.Items) == numberOfEdges {
 		return nil
 	}
-	return fmt.Errorf("excpected %s with %d edges, but got %s with %d edges", name, numberOfEdges, metrics.Resource.Name, len(metrics.Items))
+	return fmt.Errorf("expected %s with %d edges, but got %s with %d edges", name, numberOfEdges, metrics.Resource.Name, len(metrics.Items))
 }
diff --git a/test/integration/testdata/check.cni.golden b/test/integration/testdata/check.cni.golden
index c7c95e380..bcd102a9a 100644
--- a/test/integration/testdata/check.cni.golden
+++ b/test/integration/testdata/check.cni.golden
@@ -18,6 +18,7 @@ linkerd-existence
 √ control plane replica sets are ready
 √ no unschedulable pods
 √ control plane pods are ready
+√ cluster networks contains all node podCIDRs
 
 linkerd-config
 --------------
diff --git a/test/integration/testdata/check.cni.proxy.golden b/test/integration/testdata/check.cni.proxy.golden
index ac9d79baa..185b2c44a 100644
--- a/test/integration/testdata/check.cni.proxy.golden
+++ b/test/integration/testdata/check.cni.proxy.golden
@@ -18,6 +18,7 @@ linkerd-existence
 √ control plane replica sets are ready
 √ no unschedulable pods
 √ control plane pods are ready
+√ cluster networks contains all node podCIDRs
 
 linkerd-config
 --------------
@@ -84,5 +85,6 @@ linkerd-data-plane
 √ data plane pod labels are configured correctly
 √ data plane service labels are configured correctly
 √ data plane service annotations are configured correctly
+√ opaque ports are properly annotated
 
 Status check results are √
diff --git a/test/integration/testdata/check.golden b/test/integration/testdata/check.golden
index ba54a3a85..44dd0e3f9 100644
--- a/test/integration/testdata/check.golden
+++ b/test/integration/testdata/check.golden
@@ -18,6 +18,7 @@ linkerd-existence
 √ control plane replica sets are ready
 √ no unschedulable pods
 √ control plane pods are ready
+√ cluster networks contains all node podCIDRs
 
 linkerd-config
 --------------
diff --git a/test/integration/testdata/check.multicluster.proxy.golden b/test/integration/testdata/check.multicluster.proxy.golden
index 9fa3a576e..9e512eba2 100644
--- a/test/integration/testdata/check.multicluster.proxy.golden
+++ b/test/integration/testdata/check.multicluster.proxy.golden
@@ -18,6 +18,7 @@ linkerd-existence
 √ control plane replica sets are ready
 √ no unschedulable pods
 √ control plane pods are ready
+√ cluster networks contains all node podCIDRs
 
 linkerd-config
 --------------
diff --git a/test/integration/testdata/check.proxy.golden b/test/integration/testdata/check.proxy.golden
index f206a3613..58ab20b93 100644
--- a/test/integration/testdata/check.proxy.golden
+++ b/test/integration/testdata/check.proxy.golden
@@ -18,6 +18,7 @@ linkerd-existence
 √ control plane replica sets are ready
 √ no unschedulable pods
 √ control plane pods are ready
+√ cluster networks contains all node podCIDRs
 
 linkerd-config
 --------------
@@ -72,5 +73,6 @@ linkerd-data-plane
 √ data plane pod labels are configured correctly
 √ data plane service labels are configured correctly
 √ data plane service annotations are configured correctly
+√ opaque ports are properly annotated
 
 Status check results are √
diff --git a/testutil/annotations.go b/testutil/annotations.go
index 7fe8660c9..786bbe46a 100644
--- a/testutil/annotations.go
+++ b/testutil/annotations.go
@@ -58,7 +58,7 @@ func echoAnnotationWarn(t *testing.T, args ...interface{}) {
 
 // Error is a wrapper around t.Error()
 // args are passed to t.Error(args) and each arg will be sent to stdout formatted
-// as a Github annotation when the envFlag environment variable is set
+// as a GitHub annotation when the envFlag environment variable is set
 func Error(t *testing.T, args ...interface{}) {
 	t.Helper()
 	echoAnnotationErr(t, args...)
@@ -66,7 +66,7 @@ func Error(t *testing.T, args ...interface{}) {
 }
 
 // AnnotatedError is similar to Error() but it also admits a msg string that
-// will be used as the Github annotation
+// will be used as the GitHub annotation
 func AnnotatedError(t *testing.T, msg string, args ...interface{}) {
 	t.Helper()
 	echoAnnotationErr(t, msg)
@@ -75,7 +75,7 @@ func AnnotatedError(t *testing.T, msg string, args ...interface{}) {
 
 // Errorf is a wrapper around t.Errorf()
 // format and args are passed to t.Errorf(format, args) and the formatted
-// message will be sent to stdout as a Github annotation when the envFlag
+// message will be sent to stdout as a GitHub annotation when the envFlag
 // environment variable is set
 func Errorf(t *testing.T, format string, args ...interface{}) {
 	t.Helper()
@@ -84,7 +84,7 @@ func Errorf(t *testing.T, format string, args ...interface{}) {
 }
 
 // AnnotatedErrorf is similar to Errorf() but it also admits a msg string that
-// will be used as the Github annotation
+// will be used as the GitHub annotation
 func AnnotatedErrorf(t *testing.T, msg, format string, args ...interface{}) {
 	t.Helper()
 	echoAnnotationErr(t, msg)
@@ -93,7 +93,7 @@ func AnnotatedErrorf(t *testing.T, msg, format string, args ...interface{}) {
 
 // Fatal is a wrapper around t.Fatal()
 // args are passed to t.Fatal(args) and each arg will be sent to stdout formatted
-// as a Github annotation when the envFlag environment variable is set
+// as a GitHub annotation when the envFlag environment variable is set
 func Fatal(t *testing.T, args ...interface{}) {
 	t.Helper()
 	echoAnnotationErr(t, args)
@@ -101,7 +101,7 @@ func Fatal(t *testing.T, args ...interface{}) {
 }
 
 // AnnotatedFatal is similar to Fatal() but it also admits a msg string that
-// will be used as the Github annotation
+// will be used as the GitHub annotation
 func AnnotatedFatal(t *testing.T, msg string, args ...interface{}) {
 	t.Helper()
 	echoAnnotationErr(t, msg)
@@ -110,7 +110,7 @@ func AnnotatedFatal(t *testing.T, msg string, args ...interface{}) {
 
 // Fatalf is a wrapper around t.Errorf()
 // format and args are passed to t.Fatalf(format, args) and the formatted
-// message will be sent to stdout as a Github annotation when the envFlag
+// message will be sent to stdout as a GitHub annotation when the envFlag
 // environment variable is set
 func Fatalf(t *testing.T, format string, args ...interface{}) {
 	t.Helper()
@@ -119,7 +119,7 @@ func Fatalf(t *testing.T, format string, args ...interface{}) {
 }
 
 // AnnotatedFatalf is similar to Fatalf() but it also admits a msg string that
-// will be used as the Github annotation
+// will be used as the GitHub annotation
 func AnnotatedFatalf(t *testing.T, msg, format string, args ...interface{}) {
 	t.Helper()
 	echoAnnotationErr(t, msg)
@@ -127,7 +127,7 @@ func AnnotatedFatalf(t *testing.T, msg, format string, args ...interface{}) {
 }
 
 // AnnotatedWarn is a wrapper around t.Log() but it also admits a msg string that
-// will be used as the Github warning annotation
+// will be used as the GitHub warning annotation
 func AnnotatedWarn(t *testing.T, msg string, args ...interface{}) {
 	t.Helper()
 	echoAnnotationWarn(t, msg)
diff --git a/testutil/annotations_test.go b/testutil/annotations_test.go
index 9866d56fe..030827591 100644
--- a/testutil/annotations_test.go
+++ b/testutil/annotations_test.go
@@ -43,7 +43,7 @@ func restoreStdout(outC chan string, origStdout *os.File) string {
 func TestError(t *testing.T) {
 	msg := "This is an error"
 
-	// redirect stdout temporarily to catch the Github annotation output
+	// redirect stdout temporarily to catch the GitHub annotation output
 	origStdout, outC := redirectStdout(t)
 	Error(&testing.T{}, msg)
 	out := restoreStdout(outC, origStdout)
@@ -58,7 +58,7 @@ func TestAnnotatedErrorf(t *testing.T) {
 	str := "foobar"
 	msgDesc := "This is a generic error"
 
-	// redirect stdout temporarily to catch the Github annotation output
+	// redirect stdout temporarily to catch the GitHub annotation output
 	origStdout, outC := redirectStdout(t)
 	AnnotatedErrorf(&testing.T{}, msgDesc, msgFormat, str)
 	out := restoreStdout(outC, origStdout)
diff --git a/testutil/kubernetes_helper.go b/testutil/kubernetes_helper.go
index 8953c7b04..64d518f53 100644
--- a/testutil/kubernetes_helper.go
+++ b/testutil/kubernetes_helper.go
@@ -320,17 +320,15 @@ func (h *KubernetesHelper) URLFor(ctx context.Context, namespace, deployName str
 	return pf.URLFor(""), nil
 }
 
-// WaitRollout blocks until all the deployments in the linkerd namespace have been
-// completely rolled out (and their pods are ready)
-func (h *KubernetesHelper) WaitRollout(t *testing.T) {
-	for deploy, deploySpec := range LinkerdDeployReplicasEdge {
-		if deploySpec.Namespace == "linkerd" {
-			o, err := h.Kubectl("", "--namespace=linkerd", "rollout", "status", "--timeout=60m", "deploy/"+deploy)
-			if err != nil {
-				AnnotatedFatalf(t,
-					fmt.Sprintf("failed to wait rollout of deploy/%s", deploy),
-					"failed to wait for rollout of deploy/%s: %s: %s", deploy, err, o)
-			}
+// WaitRollout blocks until all the given deployments have been completely
+// rolled out (and their pods are ready)
+func (h *KubernetesHelper) WaitRollout(t *testing.T, deploys map[string]DeploySpec) {
+	for deploy, deploySpec := range deploys {
+		o, err := h.Kubectl("", "--namespace="+deploySpec.Namespace, "rollout", "status", "--timeout=60m", "deploy/"+deploy)
+		if err != nil {
+			AnnotatedFatalf(t,
+				fmt.Sprintf("failed to wait rollout of deploy/%s", deploy),
+				"failed to wait for rollout of deploy/%s: %s: %s", deploy, err, o)
 		}
 	}
 }
diff --git a/testutil/test_helper.go b/testutil/test_helper.go
index 899be08cb..10073ff29 100644
--- a/testutil/test_helper.go
+++ b/testutil/test_helper.go
@@ -67,13 +67,10 @@ type Service struct {
 }
 
 // LinkerdDeployReplicasEdge is a map containing the number of replicas for each Deployment and the main
-// container name, in the current code-base
+// container name in the current core installation
 var LinkerdDeployReplicasEdge = map[string]DeploySpec{
 	"linkerd-destination":    {"linkerd", 1},
-	"tap":                    {"linkerd-viz", 1},
-	"grafana":                {"linkerd-viz", 1},
 	"linkerd-identity":       {"linkerd", 1},
-	"web":                    {"linkerd-viz", 1},
 	"linkerd-proxy-injector": {"linkerd", 1},
 }
 
@@ -81,6 +78,18 @@ var LinkerdDeployReplicasEdge = map[string]DeploySpec{
 // container name. Override whenever edge deviates from stable.
 var LinkerdDeployReplicasStable = LinkerdDeployReplicasEdge
 
+// LinkerdVizDeployReplicas is a map containing the number of replicas for
+// each Deployment and the main container name in the current linkerd-viz
+// installation
+var LinkerdVizDeployReplicas = map[string]DeploySpec{
+	"prometheus":   {"linkerd-viz", 1},
+	"metrics-api":  {"linkerd-viz", 1},
+	"grafana":      {"linkerd-viz", 1},
+	"tap":          {"linkerd-viz", 1},
+	"tap-injector": {"linkerd-viz", 1},
+	"web":          {"linkerd-viz", 1},
+}
+
 // NewGenericTestHelper returns a new *TestHelper from the options provided as function parameters.
 // This helper was created to be able to reuse this package without hard restrictions
 // as seen in `NewTestHelper()` which is primarily used with integration tests
diff --git a/viz/cmd/testdata/install_default.golden b/viz/cmd/testdata/install_default.golden
index 9931e13a3..befee925d 100644
--- a/viz/cmd/testdata/install_default.golden
+++ b/viz/cmd/testdata/install_default.golden
@@ -390,6 +390,20 @@ metadata:
     namespace: linkerd-viz
 ---
 apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  name: psp
+  namespace: linkerd-viz
+  labels:
+    linkerd.io/extension: viz
+rules:
+- apiGroups: ['policy', 'extensions']
+  resources: ['podsecuritypolicies']
+  verbs: ['use']
+  resourceNames:
+  - linkerd-linkerd-control-plane
+---
+apiVersion: rbac.authorization.k8s.io/v1
 kind: RoleBinding
 metadata:
   name: viz-psp
@@ -414,6 +428,12 @@ subjects:
 - kind: ServiceAccount
   name: prometheus
   namespace: linkerd-viz
+- kind: ServiceAccount
+  name: metrics-api
+  namespace: linkerd-viz
+- kind: ServiceAccount
+  name: tap-injector
+  namespace: linkerd-viz
 ---
 ###
 ### Metrics API
diff --git a/viz/cmd/testdata/install_default_overrides.golden b/viz/cmd/testdata/install_default_overrides.golden
index 6d78faa1e..84607d6c9 100644
--- a/viz/cmd/testdata/install_default_overrides.golden
+++ b/viz/cmd/testdata/install_default_overrides.golden
@@ -390,6 +390,20 @@ metadata:
     namespace: linkerd-viz
 ---
 apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  name: psp
+  namespace: linkerd-viz
+  labels:
+    linkerd.io/extension: viz
+rules:
+- apiGroups: ['policy', 'extensions']
+  resources: ['podsecuritypolicies']
+  verbs: ['use']
+  resourceNames:
+  - linkerd-linkerd-control-plane
+---
+apiVersion: rbac.authorization.k8s.io/v1
 kind: RoleBinding
 metadata:
   name: viz-psp
@@ -414,6 +428,12 @@ subjects:
 - kind: ServiceAccount
   name: prometheus
   namespace: linkerd-viz
+- kind: ServiceAccount
+  name: metrics-api
+  namespace: linkerd-viz
+- kind: ServiceAccount
+  name: tap-injector
+  namespace: linkerd-viz
 ---
 ###
 ### Metrics API
diff --git a/viz/cmd/testdata/install_grafana_disabled.golden b/viz/cmd/testdata/install_grafana_disabled.golden
index 42b0fb037..501b3bc1f 100644
--- a/viz/cmd/testdata/install_grafana_disabled.golden
+++ b/viz/cmd/testdata/install_grafana_disabled.golden
@@ -377,6 +377,20 @@ metadata:
     namespace: linkerd-viz
 ---
 apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  name: psp
+  namespace: linkerd-viz
+  labels:
+    linkerd.io/extension: viz
+rules:
+- apiGroups: ['policy', 'extensions']
+  resources: ['podsecuritypolicies']
+  verbs: ['use']
+  resourceNames:
+  - linkerd-linkerd-control-plane
+---
+apiVersion: rbac.authorization.k8s.io/v1
 kind: RoleBinding
 metadata:
   name: viz-psp
@@ -398,6 +412,12 @@ subjects:
 - kind: ServiceAccount
   name: prometheus
   namespace: linkerd-viz
+- kind: ServiceAccount
+  name: metrics-api
+  namespace: linkerd-viz
+- kind: ServiceAccount
+  name: tap-injector
+  namespace: linkerd-viz
 ---
 ###
 ### Metrics API
diff --git a/viz/cmd/testdata/install_prometheus_disabled.golden b/viz/cmd/testdata/install_prometheus_disabled.golden
index fab13bbc8..8d20b8125 100644
--- a/viz/cmd/testdata/install_prometheus_disabled.golden
+++ b/viz/cmd/testdata/install_prometheus_disabled.golden
@@ -350,6 +350,20 @@ metadata:
     namespace: linkerd-viz
 ---
 apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  name: psp
+  namespace: linkerd-viz
+  labels:
+    linkerd.io/extension: viz
+rules:
+- apiGroups: ['policy', 'extensions']
+  resources: ['podsecuritypolicies']
+  verbs: ['use']
+  resourceNames:
+  - linkerd-linkerd-control-plane
+---
+apiVersion: rbac.authorization.k8s.io/v1
 kind: RoleBinding
 metadata:
   name: viz-psp
@@ -371,6 +385,12 @@ subjects:
 - kind: ServiceAccount
   name: grafana
   namespace: linkerd-viz
+- kind: ServiceAccount
+  name: metrics-api
+  namespace: linkerd-viz
+- kind: ServiceAccount
+  name: tap-injector
+  namespace: linkerd-viz
 ---
 ###
 ### Metrics API
diff --git a/viz/cmd/testdata/install_prometheus_loglevel_from_args.golden b/viz/cmd/testdata/install_prometheus_loglevel_from_args.golden
index 011d21981..4916ba39b 100644
--- a/viz/cmd/testdata/install_prometheus_loglevel_from_args.golden
+++ b/viz/cmd/testdata/install_prometheus_loglevel_from_args.golden
@@ -390,6 +390,20 @@ metadata:
     namespace: linkerd-viz
 ---
 apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  name: psp
+  namespace: linkerd-viz
+  labels:
+    linkerd.io/extension: viz
+rules:
+- apiGroups: ['policy', 'extensions']
+  resources: ['podsecuritypolicies']
+  verbs: ['use']
+  resourceNames:
+  - linkerd-linkerd-control-plane
+---
+apiVersion: rbac.authorization.k8s.io/v1
 kind: RoleBinding
 metadata:
   name: viz-psp
@@ -414,6 +428,12 @@ subjects:
 - kind: ServiceAccount
   name: prometheus
   namespace: linkerd-viz
+- kind: ServiceAccount
+  name: metrics-api
+  namespace: linkerd-viz
+- kind: ServiceAccount
+  name: tap-injector
+  namespace: linkerd-viz
 ---
 ###
 ### Metrics API
diff --git a/viz/cmd/testdata/install_proxy_resources.golden b/viz/cmd/testdata/install_proxy_resources.golden
index a05273913..d7a37b4c5 100644
--- a/viz/cmd/testdata/install_proxy_resources.golden
+++ b/viz/cmd/testdata/install_proxy_resources.golden
@@ -390,6 +390,20 @@ metadata:
     namespace: linkerd-viz
 ---
 apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  name: psp
+  namespace: linkerd-viz
+  labels:
+    linkerd.io/extension: viz
+rules:
+- apiGroups: ['policy', 'extensions']
+  resources: ['podsecuritypolicies']
+  verbs: ['use']
+  resourceNames:
+  - linkerd-linkerd-control-plane
+---
+apiVersion: rbac.authorization.k8s.io/v1
 kind: RoleBinding
 metadata:
   name: viz-psp
@@ -414,6 +428,12 @@ subjects:
 - kind: ServiceAccount
   name: prometheus
   namespace: linkerd-viz
+- kind: ServiceAccount
+  name: metrics-api
+  namespace: linkerd-viz
+- kind: ServiceAccount
+  name: tap-injector
+  namespace: linkerd-viz
 ---
 ###
 ### Metrics API
diff --git a/viz/metrics-api/stat_summary_test.go b/viz/metrics-api/stat_summary_test.go
index e3a4ee59a..755e04a79 100644
--- a/viz/metrics-api/stat_summary_test.go
+++ b/viz/metrics-api/stat_summary_test.go
@@ -1316,13 +1316,6 @@ status:
 										},
 									},
 								},
-								{
-									Table: &pb.StatTable_PodGroup_{
-										PodGroup: &pb.StatTable_PodGroup{
-											Rows: []*pb.StatTable_PodGroup_Row{},
-										},
-									},
-								},
 								{
 									Table: &pb.StatTable_PodGroup_{
 										PodGroup: &pb.StatTable_PodGroup{