Repository: linkerd/linkerd2
Pull request #6156 (2021-05-21 06:37:46)

destination: prefer `sp.dstOverrides` over `trafficsplit`
This updates the destination to prefer `serviceprofiles.dstOverrides`
over `trafficsplits`. This is useful as it is improtant for
ServiceProfile to take preference over TrafficSplits when both are
present.

This also makes integration testing the `smi-adaptor` easier.

This also adds unit tests in the `traffic_split_adaptor` to check
for the same.

Signed-off-by: Tarun Pothulapati <tarunpothulapati@outlook.com>


Pothulapati: This change was part of #6097, which is being moved into its own repo!

Patch:
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 000000000..d00ab9cf5
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,45 @@
+# See https://github.com/github/codeql-action/tree/v1 for more information.
+
+name: CodeQL
+
+on:
+  push:
+    branches: [ main, stable-* ]
+    paths-ignore:
+    - '*.md'
+    - '**/*.md'
+  pull_request:
+    # The branches below must be a subset of the branches above
+    branches: [ main, stable-* ]
+    paths-ignore:
+    - '*.md'
+    - '**/*.md'
+  schedule:
+    - cron: '40 5 * * 3'
+
+jobs:
+  analyze:
+    name: Analyze
+    runs-on: ubuntu-latest
+    permissions:
+      actions: read
+      contents: read
+      security-events: write
+
+    strategy:
+      fail-fast: false
+      matrix:
+        language: [ 'go', 'javascript' ]
+
+    steps:
+    - name: Checkout
+      uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
+
+    - name: Initialize
+      # Unpinned action version so that we automatically get analyzer updates.
+      uses: github/codeql-action/init@v1
+      with:
+        languages: ${{ matrix.language }}
+
+    - name: Analyze
+      uses: github/codeql-action/analyze@v1
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 1121980d5..33ed139d0 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -16,8 +16,7 @@ jobs:
     timeout-minutes: 30
     steps:
     - name: Checkout code
-      # actions/checkout@v2
-      uses: actions/checkout@722adc63f1aa60a57ec37892e133b1d319cae598
+      uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
     - name: Set environment variables from scripts
       run: |
         . bin/_tag.sh
@@ -77,8 +76,7 @@ jobs:
     needs: [docker_build]
     steps:
     - name: Checkout code
-      # actions/checkout@v2
-      uses: actions/checkout@722adc63f1aa60a57ec37892e133b1d319cae598
+      uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
     - name: Try to load cached Go modules
       # actions/cache@v1.1.2
       uses: actions/cache@70655ec8323daeeaa7ef06d7c56e1b9191396cbe
@@ -114,8 +112,7 @@ jobs:
     runs-on: ubuntu-20.04
     steps:
     - name: Checkout code
-      # actions/checkout@v2
-      uses: actions/checkout@722adc63f1aa60a57ec37892e133b1d319cae598
+      uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
     - name: Try to load cached Go modules
       # actions/cache@v1.1.2
       uses: actions/cache@70655ec8323daeeaa7ef06d7c56e1b9191396cbe
@@ -144,8 +141,7 @@ jobs:
     steps:
     - name: Checkout code
       #if: startsWith(github.ref, 'refs/tags/stable')
-      # actions/checkout@v2
-      uses: actions/checkout@722adc63f1aa60a57ec37892e133b1d319cae598
+      uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
     - name: Try to load cached Go modules
       #if: startsWith(github.ref, 'refs/tags/stable')
       # actions/cache@v1.1.2
@@ -197,8 +193,7 @@ jobs:
     steps:
     - name: Checkout code
       if: startsWith(github.ref, 'refs/tags/stable')
-      # actions/checkout@v2
-      uses: actions/checkout@722adc63f1aa60a57ec37892e133b1d319cae598
+      uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
     - name: Chocolatey - update nuspec
       if: startsWith(github.ref, 'refs/tags/stable')
       run: |
@@ -225,8 +220,7 @@ jobs:
     needs: [choco_pack]
     steps:
     - name: Checkout code
-      # actions/checkout@v2
-      uses: actions/checkout@722adc63f1aa60a57ec37892e133b1d319cae598
+      uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
     - name: Set environment variables from scripts
       run: |
         . bin/_tag.sh
@@ -286,8 +280,7 @@ jobs:
     needs: [website_publish]
     steps:
     - name: Checkout code
-      # actions/checkout@v2
-      uses: actions/checkout@722adc63f1aa60a57ec37892e133b1d319cae598
+      uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
     - name: Set environment variables from scripts
       run: |
         . bin/_tag.sh
@@ -316,8 +309,7 @@ jobs:
     needs: [gh_release]
     steps:
     - name: Checkout code
-      # actions/checkout@v2
-      uses: actions/checkout@722adc63f1aa60a57ec37892e133b1d319cae598
+      uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
     - name: Configure gsutils
       # linkerd/linkerd2-action-gcloud@v1.0.1
       uses: linkerd/linkerd2-action-gcloud@308c4df7314264c90d240e77e27d0d875d3c8ec1
diff --git a/.github/workflows/static_checks.yml b/.github/workflows/static_checks.yml
index f84593bbf..ebdc93056 100644
--- a/.github/workflows/static_checks.yml
+++ b/.github/workflows/static_checks.yml
@@ -1,10 +1,11 @@
 name: Static checks
 on:
-  pull_request: {}
+  pull_request:
+    paths-ignore:
+    - 'web/app/package.json'
   push:
     paths-ignore:
-    - '*.md'
-    - '**/*.md'
+    - 'web/app/package.json'
     branches:
     - main
 jobs:
@@ -13,11 +14,10 @@ jobs:
     timeout-minutes: 10
     runs-on: ubuntu-20.04
     container:
-      image: golang:1.16.2
+      image: golang:1.16.4
     steps:
     - name: Checkout code
-      # actions/checkout@v2
-      uses: actions/checkout@722adc63f1aa60a57ec37892e133b1d319cae598
+      uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
     - name: Go lint
       run: bin/lint --verbose
   go_format:
@@ -25,11 +25,10 @@ jobs:
     timeout-minutes: 10
     runs-on: ubuntu-20.04
     container:
-      image: golang:1.16.2
+      image: golang:1.16.4
     steps:
     - name: Checkout code
-      # actions/checkout@v2
-      uses: actions/checkout@722adc63f1aa60a57ec37892e133b1d319cae598
+      uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
     - name: Format
       run: bin/fmt
   proto_diff:
@@ -37,13 +36,12 @@ jobs:
     timeout-minutes: 10
     runs-on: ubuntu-20.04
     container:
-      image: golang:1.16.2
+      image: golang:1.16.4
     steps:
       - name: Prerequisites
         run: apt-get update && DEBIAN_FRONTEND=noninteractive apt-get -y --no-install-recommends install unzip
       - name: Checkout code
-        # actions/checkout@v2
-        uses: actions/checkout@722adc63f1aa60a57ec37892e133b1d319cae598
+        uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
       - name: Diff proto files
         run: bin/protoc-diff
   shellcheck:
@@ -52,8 +50,7 @@ jobs:
     runs-on: ubuntu-20.04
     steps:
     - name: Checkout code
-      # actions/checkout@v2
-      uses: actions/checkout@722adc63f1aa60a57ec37892e133b1d319cae598
+      uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
     - name: shellcheck
       # For more information on shellcheck failures:
       # https://github.com/koalaman/shellcheck/wiki/Checks
@@ -65,8 +62,7 @@ jobs:
     runs-on: ubuntu-20.04
     steps:
     - name: Checkout code
-      # actions/checkout@v2
-      uses: actions/checkout@722adc63f1aa60a57ec37892e133b1d319cae598
+      uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
     - name: Chocolatey - lint
       # devblackops/github-action-psscriptanalyzer@v2.3.0
       uses: devblackops/github-action-psscriptanalyzer@819c15c7d02dec0611fe3ded6f8e259b0b925754
@@ -82,8 +78,7 @@ jobs:
     runs-on: ubuntu-20.04
     steps:
     - name: Checkout code
-      # actions/checkout@v2
-      uses: actions/checkout@722adc63f1aa60a57ec37892e133b1d319cae598
+      uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
     - name: Markdown lint
       run: bin/markdownlint-all
   chart_docs_diff:
@@ -91,7 +86,6 @@ jobs:
     runs-on: ubuntu-20.04
     steps:
     - name: Checkout code
-      # actions/checkout@v2
-      uses: actions/checkout@722adc63f1aa60a57ec37892e133b1d319cae598
+      uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
     - name: Check docs for diff
       run: bin/helm-docs-diff
diff --git a/ADOPTERS.md b/ADOPTERS.md
index d5fcf111d..87340f747 100644
--- a/ADOPTERS.md
+++ b/ADOPTERS.md
@@ -35,6 +35,7 @@
 - [Mulligan Funding](https://www.mulliganfunding.com/)
 - [Mythical Games](https://mythicalgames.com/)
 - [National Information Solutions Cooperative (NISC)](https://nisc.coop/)
+- [Nexxiot](https://www.nexxiot.com)
 - [Nordstrom](https://nordstrom.com/)
 - [Novolabs](https://novolabs.com)
 - [Paybase](https://paybase.io/)
diff --git a/Dockerfile-proxy b/Dockerfile-proxy
index dddcadd08..8650b118a 100644
--- a/Dockerfile-proxy
+++ b/Dockerfile-proxy
@@ -2,7 +2,7 @@ ARG RUNTIME_IMAGE=debian:buster-20210208-slim
 ARG BUILDPLATFORM=linux/amd64
 
 # Precompile key slow-to-build dependencies
-FROM --platform=$BUILDPLATFORM golang:1.16.2-alpine as go-deps
+FROM --platform=$BUILDPLATFORM golang:1.16.4-alpine as go-deps
 WORKDIR /linkerd-build
 COPY go.mod go.sum ./
 COPY bin/install-deps bin/
diff --git a/cli/Dockerfile-bin b/cli/Dockerfile-bin
index d4eba7bb3..c372bdabf 100644
--- a/cli/Dockerfile-bin
+++ b/cli/Dockerfile-bin
@@ -2,7 +2,7 @@ ARG BUILDPLATFORM=linux/amd64
 ARG BUILD_STAGE=single-arch
 
 # Precompile key slow-to-build dependencies
-FROM --platform=$BUILDPLATFORM golang:1.16.2-alpine as go-deps
+FROM --platform=$BUILDPLATFORM golang:1.16.4-alpine as go-deps
 WORKDIR /linkerd-build
 COPY go.mod go.sum ./
 COPY bin/install-deps bin/
diff --git a/cli/cmd/diagnostics.go b/cli/cmd/diagnostics.go
index 9866f5a39..b19872e26 100644
--- a/cli/cmd/diagnostics.go
+++ b/cli/cmd/diagnostics.go
@@ -27,16 +27,12 @@ This command provides subcommands to diagnose the functionality of Linkerd.`,
  
   # Get the endpoints for authorities in Linkerd's control-plane itself
   linkerd diagnostics endpoints web.linkerd-viz.svc.cluster.local:8084
-
-  # Install service profiles for the control-plane components.
-  linkerd diagnostics install-sp
   `,
 	}
 
 	diagnosticsCmd.AddCommand(newCmdControllerMetrics())
 	diagnosticsCmd.AddCommand(newCmdEndpoints())
 	diagnosticsCmd.AddCommand(newCmdMetrics())
-	diagnosticsCmd.AddCommand(newCmdInstallSP())
 
 	return diagnosticsCmd
 }
diff --git a/cli/cmd/install-sp.go b/cli/cmd/install-sp.go
deleted file mode 100644
index 5713c1464..000000000
--- a/cli/cmd/install-sp.go
+++ /dev/null
@@ -1,62 +0,0 @@
-package cmd
-
-import (
-	"bytes"
-	"io"
-	"os"
-	"text/template"
-
-	"github.com/linkerd/linkerd2/cli/installsp"
-	"github.com/linkerd/linkerd2/pkg/healthcheck"
-	"github.com/linkerd/linkerd2/pkg/k8s"
-	"github.com/spf13/cobra"
-)
-
-func newCmdInstallSP() *cobra.Command {
-	cmd := &cobra.Command{
-		Use:   "install-sp [flags]",
-		Short: "Output Kubernetes configs to install Linkerd Service Profiles",
-		Long: `Output Kubernetes configs to install Linkerd Service Profiles.
-
-This command installs Service Profiles into the Linkerd control plane. A
-cluster-wide Linkerd control-plane is a prerequisite. To confirm Service Profile
-support, verify "kubectl api-versions" outputs "linkerd.io/v1alpha2".`,
-		RunE: func(cmd *cobra.Command, args []string) error {
-			k8sAPI, err := k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, impersonateGroup, 0)
-			if err != nil {
-				return err
-			}
-
-			_, values, err := healthcheck.FetchCurrentConfiguration(cmd.Context(), k8sAPI, controlPlaneNamespace)
-			if err != nil {
-				return err
-			}
-
-			clusterDomain := values.ClusterDomain
-			if clusterDomain == "" {
-				clusterDomain = defaultClusterDomain
-			}
-
-			return renderSP(os.Stdout, controlPlaneNamespace, clusterDomain)
-		},
-	}
-
-	return cmd
-}
-
-func renderSP(w io.Writer, namespace, clusterDomain string) error {
-	template, err := template.New("linkerd").Parse(installsp.Template)
-	if err != nil {
-		return err
-	}
-	buf := &bytes.Buffer{}
-	err = template.Execute(buf, map[string]string{"Namespace": namespace, "ClusterDomain": clusterDomain})
-	if err != nil {
-		return err
-	}
-
-	w.Write(buf.Bytes())
-	w.Write([]byte("---\n"))
-
-	return nil
-}
diff --git a/cli/cmd/upgrade.go b/cli/cmd/upgrade.go
index 4d6287bb3..79cf64f99 100644
--- a/cli/cmd/upgrade.go
+++ b/cli/cmd/upgrade.go
@@ -33,9 +33,8 @@ const (
 )
 
 var (
-	addOnOverwrite bool
-	manifests      string
-	force          bool
+	manifests string
+	force     bool
 )
 
 /* The upgrade commands all follow the same flow:
@@ -231,10 +230,6 @@ func makeUpgradeFlags() *pflag.FlagSet {
 		&force, "force", false,
 		"Force upgrade operation even when issuer certificate does not work with the trust anchors of all proxies",
 	)
-	upgradeFlags.BoolVar(
-		&addOnOverwrite, "addon-overwrite", false,
-		"Overwrite add-on configuration instead of loading the existing config (or reset to defaults if no new config is specified)",
-	)
 	return upgradeFlags
 }
 
diff --git a/cli/installsp/template.go b/cli/installsp/template.go
deleted file mode 100644
index 30914a32e..000000000
--- a/cli/installsp/template.go
+++ /dev/null
@@ -1,139 +0,0 @@
-package installsp
-
-// Template provides the base template for the `linkerd install-sp` command.
-const Template = `apiVersion: linkerd.io/v1alpha2
-kind: ServiceProfile
-metadata:
-  name: linkerd-controller-api.{{.Namespace}}.svc.{{.ClusterDomain}}
-  namespace: {{.Namespace}}
-spec:
-  routes:
-  - name: POST /api/v1/StatSummary
-    condition:
-      method: POST
-      pathRegex: /api/v1/StatSummary
-  - name: POST /api/v1/TopRoutes
-    condition:
-      method: POST
-      pathRegex: /api/v1/TopRoutes
-  - name: POST /api/v1/ListPods
-    condition:
-      method: POST
-      pathRegex: /api/v1/ListPods
-  - name: POST /api/v1/ListServices
-    condition:
-      method: POST
-      pathRegex: /api/v1/ListServices
-  - name: POST /api/v1/Version
-    condition:
-      method: POST
-      pathRegex: /api/v1/Version
-  - name: POST /api/v1/SelfCheck
-    condition:
-      method: POST
-      pathRegex: /api/v1/SelfCheck
----
-apiVersion: linkerd.io/v1alpha2
-kind: ServiceProfile
-metadata:
-  name: linkerd-dst.{{.Namespace}}.svc.{{.ClusterDomain}}
-  namespace: {{.Namespace}}
-spec:
-  routes:
-  - name: POST /io.linkerd.proxy.destination.Destination/Get
-    condition:
-      method: POST
-      pathRegex: /io\.linkerd\.proxy\.destination\.Destination/Get
-  - name: POST /io.linkerd.proxy.destination.Destination/GetProfile
-    condition:
-      method: POST
-      pathRegex: /io\.linkerd\.proxy\.destination\.Destination/GetProfile
----
-apiVersion: linkerd.io/v1alpha2
-kind: ServiceProfile
-metadata:
-  name: linkerd-dst-headless.{{.Namespace}}.svc.{{.ClusterDomain}}
-  namespace: {{.Namespace}}
-spec:
-  routes:
-  - name: POST /io.linkerd.proxy.destination.Destination/Get
-    condition:
-      method: POST
-      pathRegex: /io\.linkerd\.proxy\.destination\.Destination/Get
-  - name: POST /io.linkerd.proxy.destination.Destination/GetProfile
-    condition:
-      method: POST
-      pathRegex: /io\.linkerd\.proxy\.destination\.Destination/GetProfile
----
-apiVersion: linkerd.io/v1alpha2
-kind: ServiceProfile
-metadata:
-  name: linkerd-prometheus.{{.Namespace}}.svc.{{.ClusterDomain}}
-  namespace: {{.Namespace}}
-spec:
-  routes:
-  - name: POST /api/v1/query
-    condition:
-      method: POST
-      pathRegex: /api/v1/query
-  - name: GET /api/v1/query_range
-    condition:
-      method: GET
-      pathRegex: /api/v1/query_range
-  - name: GET /api/v1/series
-    condition:
-      method: GET
-      pathRegex: /api/v1/series
----
-apiVersion: linkerd.io/v1alpha2
-kind: ServiceProfile
-metadata:
-  name: linkerd-grafana.{{.Namespace}}.svc.{{.ClusterDomain}}
-  namespace: {{.Namespace}}
-spec:
-  routes:
-  - name: GET /api/annotations
-    condition:
-      method: GET
-      pathRegex: /api/annotations
-  - name: GET /api/dashboards/tags
-    condition:
-      method: GET
-      pathRegex: /api/dashboards/tags
-  - name: GET /api/dashboards/uid/{uid}
-    condition:
-      method: GET
-      pathRegex: /api/dashboards/uid/.*
-  - name: GET /api/dashboard/{dashboard}
-    condition:
-      method: GET
-      pathRegex: /api/dashboard/.*
-  - name: GET /api/datasources/proxy/1/api/v1/series
-    condition:
-      method: GET
-      pathRegex: /api/datasources/proxy/1/api/v1/series
-  - name: GET /api/datasources/proxy/1/api/v1/query_range
-    condition:
-      method: GET
-      pathRegex: /api/datasources/proxy/1/api/v1/query_range
-  - name: GET /api/search
-    condition:
-      method: GET
-      pathRegex: /api/search
-  - name: GET /d/{uid}/{dashboard-name}
-    condition:
-      method: GET
-      pathRegex: /d/[^/]*/.*
-  - name: GET /public/build/{style}.css
-    condition:
-      method: GET
-      pathRegex: /public/build/.*\.css
-  - name: GET /public/fonts/{font}
-    condition:
-      method: GET
-      pathRegex: /public/fonts/.*
-  - name: GET /public/img/{img}
-    condition:
-      method: GET
-      pathRegex: /public/img/.*
-`
diff --git a/cni-plugin/Dockerfile b/cni-plugin/Dockerfile
index 7e974d314..b8455e63c 100644
--- a/cni-plugin/Dockerfile
+++ b/cni-plugin/Dockerfile
@@ -1,7 +1,7 @@
 ARG BUILDPLATFORM=linux/amd64
 
 # Precompile key slow-to-build dependencies
-FROM --platform=$BUILDPLATFORM golang:1.16.2-alpine as go-deps
+FROM --platform=$BUILDPLATFORM golang:1.16.4-alpine as go-deps
 WORKDIR /linkerd-build
 COPY go.mod go.sum ./
 COPY bin/install-deps bin/
diff --git a/controller/Dockerfile b/controller/Dockerfile
index 373d776fb..2be516b13 100644
--- a/controller/Dockerfile
+++ b/controller/Dockerfile
@@ -1,7 +1,7 @@
 ARG BUILDPLATFORM=linux/amd64
 
 # Precompile key slow-to-build dependencies
-FROM --platform=$BUILDPLATFORM golang:1.16.2-alpine as go-deps
+FROM --platform=$BUILDPLATFORM golang:1.16.4-alpine as go-deps
 WORKDIR /linkerd-build
 COPY go.mod go.sum ./
 COPY bin/install-deps bin/
diff --git a/controller/api/destination/endpoint_translator.go b/controller/api/destination/endpoint_translator.go
index 2cd8af5ee..76d8f7064 100644
--- a/controller/api/destination/endpoint_translator.go
+++ b/controller/api/destination/endpoint_translator.go
@@ -329,12 +329,12 @@ func toWeightedAddr(address watcher.Address, opaquePorts, skippedInboundPorts ma
 	// If the pod is controlled by any Linkerd control plane, then it can be
 	// hinted that this destination knows H2 (and handles our orig-proto
 	// translation)
-	var hint *pb.ProtocolHint
-	if enableH2Upgrade && controllerNSLabel != "" && !isSkippedInboundPort {
-		hint = &pb.ProtocolHint{
-			Protocol: &pb.ProtocolHint_H2_{
+	hint := &pb.ProtocolHint{}
+	if controllerNSLabel != "" && !isSkippedInboundPort {
+		if enableH2Upgrade {
+			hint.Protocol = &pb.ProtocolHint_H2_{
 				H2: &pb.ProtocolHint_H2{},
-			},
+			}
 		}
 		if _, ok := opaquePorts[address.Port]; ok {
 			port, err := getInboundPort(&address.Pod.Spec)
diff --git a/controller/api/destination/server.go b/controller/api/destination/server.go
index c9be7f6b6..535be5167 100644
--- a/controller/api/destination/server.go
+++ b/controller/api/destination/server.go
@@ -532,7 +532,7 @@ func getHostAndPort(authority string) (string, watcher.Port, error) {
 	if len(hostPort) == 2 {
 		var err error
 		port, err = strconv.Atoi(hostPort[1])
-		if err != nil {
+		if err != nil || port <= 0 || port > 65535 {
 			return "", 0, fmt.Errorf("Invalid port %s", hostPort[1])
 		}
 	}
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/jaeger/charts/linkerd-jaeger/README.md b/jaeger/charts/linkerd-jaeger/README.md
index 437f46036..dad473aa5 100644
--- a/jaeger/charts/linkerd-jaeger/README.md
+++ b/jaeger/charts/linkerd-jaeger/README.md
@@ -71,6 +71,7 @@ Kubernetes: `>=1.16.0-0`
 
 | Key | Type | Default | Description |
 |-----|------|---------|-------------|
+| collector.enabled | bool | `true` | Set to false to exclude collector installation |
 | collector.image.name | string | `"omnition/opencensus-collector"` |  |
 | collector.image.pullPolicy | string | `"Always"` |  |
 | collector.image.version | string | `"0.1.11"` |  |
@@ -78,6 +79,7 @@ Kubernetes: `>=1.16.0-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. |
+| jaeger.enabled | bool | `true` | Set to false to exclude all-in-one Jaeger installation |
 | jaeger.image.name | string | `"jaegertracing/all-in-one"` |  |
 | jaeger.image.pullPolicy | string | `"Always"` |  |
 | jaeger.image.version | string | `"1.19.2"` |  |
diff --git a/jaeger/charts/linkerd-jaeger/templates/rbac.yaml b/jaeger/charts/linkerd-jaeger/templates/rbac.yaml
index 342ff9646..d4fd1d7ff 100644
--- a/jaeger/charts/linkerd-jaeger/templates/rbac.yaml
+++ b/jaeger/charts/linkerd-jaeger/templates/rbac.yaml
@@ -1,3 +1,4 @@
+{{- if .Values.collector.enabled -}}
 ---
 ###
 ### collector RBAC
@@ -7,16 +8,7 @@ apiVersion: v1
 metadata:
   name: collector
   namespace: {{.Values.namespace}}
----
-###
-### jaeger RBAC
-###
-kind: ServiceAccount
-apiVersion: v1
-metadata:
-  name: jaeger
-  namespace: {{.Values.namespace}}
-
+{{ end -}}
 ---
 ###
 ### Jaeger Injector RBAC
@@ -102,3 +94,14 @@ webhooks:
     apiVersions: ["v1"]
     resources: ["pods"]
   sideEffects: None
+{{- if .Values.jaeger.enabled }}
+---
+###
+### jaeger RBAC
+###
+kind: ServiceAccount
+apiVersion: v1
+metadata:
+  name: jaeger
+  namespace: {{.Values.namespace}}
+{{- end -}}
diff --git a/jaeger/charts/linkerd-jaeger/templates/tracing.yaml b/jaeger/charts/linkerd-jaeger/templates/tracing.yaml
index b16bcdc89..8e7a6101e 100644
--- a/jaeger/charts/linkerd-jaeger/templates/tracing.yaml
+++ b/jaeger/charts/linkerd-jaeger/templates/tracing.yaml
@@ -1,3 +1,4 @@
+{{- if .Values.collector.enabled }}
 ---
 ###
 ### Tracing Collector Service
@@ -119,6 +120,8 @@ spec:
             path: collector-config.yaml
           name: collector-config
         name: collector-config-val
+{{- end -}}
+{{- if .Values.jaeger.enabled }}
 ---
 ###
 ### Tracing Jaeger Service
@@ -186,3 +189,4 @@ spec:
         {{- end }}
       dnsPolicy: ClusterFirst
       serviceAccountName: jaeger
+{{- end }}
diff --git a/jaeger/charts/linkerd-jaeger/values.yaml b/jaeger/charts/linkerd-jaeger/values.yaml
index 0b8e48f43..904e5bca8 100644
--- a/jaeger/charts/linkerd-jaeger/values.yaml
+++ b/jaeger/charts/linkerd-jaeger/values.yaml
@@ -15,6 +15,8 @@ nodeSelector: &default_node_selector
 tolerations: &default_tolerations
 
 collector:
+  # -- Set to false to exclude collector installation
+  enabled: true
   image:
     name: omnition/opencensus-collector
     version: 0.1.11
@@ -32,6 +34,8 @@ collector:
   jaegerAddr:
 
 jaeger:
+  # -- Set to false to exclude all-in-one Jaeger installation
+  enabled: true
   image:
     name: jaegertracing/all-in-one
     version: 1.19.2
diff --git a/jaeger/injector/Dockerfile b/jaeger/injector/Dockerfile
index 67f484fe3..e536ac080 100644
--- a/jaeger/injector/Dockerfile
+++ b/jaeger/injector/Dockerfile
@@ -1,7 +1,7 @@
 ARG BUILDPLATFORM=linux/amd64
 
 # Precompile key slow-to-build dependencies
-FROM --platform=$BUILDPLATFORM golang:1.16.2-alpine as go-deps
+FROM --platform=$BUILDPLATFORM golang:1.16.4-alpine as go-deps
 WORKDIR /linkerd-build
 COPY go.mod go.sum ./
 COPY bin/install-deps bin/
diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go
index 0d18793ab..974997576 100644
--- a/pkg/cmd/cmd.go
+++ b/pkg/cmd/cmd.go
@@ -9,6 +9,7 @@ import (
 	"github.com/linkerd/linkerd2/pkg/k8s"
 	"github.com/linkerd/linkerd2/pkg/k8s/resource"
 	"github.com/prometheus/common/log"
+	"github.com/spf13/cobra"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/client-go/tools/clientcmd"
@@ -56,3 +57,32 @@ func Uninstall(ctx context.Context, k8sAPI *k8s.KubernetesAPI, selector string)
 	}
 	return nil
 }
+
+// ConfigureNamespaceFlagCompletion sets up resource-aware completion for command
+// flags that accept a namespace name
+func ConfigureNamespaceFlagCompletion(
+	cmd *cobra.Command,
+	flagNames []string,
+	kubeconfigPath string,
+	impersonate string,
+	impersonateGroup []string,
+	kubeContext string,
+) {
+	for _, flagName := range flagNames {
+		cmd.RegisterFlagCompletionFunc(flagName,
+			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, "")
+				results, err := cc.Complete([]string{k8s.Namespace}, toComplete)
+				if err != nil {
+					return nil, cobra.ShellCompDirectiveError
+				}
+
+				return results, cobra.ShellCompDirectiveDefault
+			})
+	}
+}
diff --git a/pkg/healthcheck/healthcheck.go b/pkg/healthcheck/healthcheck.go
index 3b53db57b..0664798f8 100644
--- a/pkg/healthcheck/healthcheck.go
+++ b/pkg/healthcheck/healthcheck.go
@@ -2500,6 +2500,12 @@ func validateDataPlanePods(pods []corev1.Pod, targetNamespace string) error {
 
 	for _, pod := range pods {
 		status := k8s.GetPodStatus(pod)
+		// Skip validating meshed pods that are in the `Completed` state
+		// as they do not have a running proxy
+		if status == "Completed" {
+			continue
+		}
+
 		if status != "Running" && status != "Evicted" {
 			return fmt.Errorf("The \"%s\" pod is not running", pod.Name)
 		}
diff --git a/pkg/k8s/completion.go b/pkg/k8s/completion.go
index 35ff5e4e3..f5f180ecd 100644
--- a/pkg/k8s/completion.go
+++ b/pkg/k8s/completion.go
@@ -60,6 +60,12 @@ func (c *CommandCompletion) Complete(args []string, toComplete string) ([]string
 		return suggestions, nil
 	}
 
+	// Similar to kubectl, we don't provide resource completion
+	// when the resource provided is in format <kind>/<resourceName>
+	if strings.Contains(args[0], "/") {
+		return []string{}, nil
+	}
+
 	resType, err := CanonicalResourceNameFromFriendlyName(args[0])
 	if err != nil {
 		return nil, fmt.Errorf("%s is not a valid resource name", args)
@@ -70,13 +76,6 @@ func (c *CommandCompletion) Complete(args []string, toComplete string) ([]string
 		c.namespace = ""
 	}
 
-	// Similar to kubectl's completion for bash and zsh only, we don't provide
-	// resource completion when the resource provided is in format
-	// <kind>/<resourceName>.
-	if strings.Contains(args[0], "/") {
-		return []string{}, nil
-	}
-
 	gvr, err := c.getGroupVersionKindForResource(resType)
 	if err != nil {
 		return nil, err
diff --git a/viz/charts/linkerd-viz/README.md b/viz/charts/linkerd-viz/README.md
index a53e47b7f..0e478f480 100644
--- a/viz/charts/linkerd-viz/README.md
+++ b/viz/charts/linkerd-viz/README.md
@@ -96,11 +96,13 @@ Kubernetes: `>=1.16.0-0`
 | grafana.image.pullPolicy | string | defaultImagePullPolicy | Pull policy for the grafana instance |
 | grafana.image.registry | string | defaultRegistry | Docker registry for the grafana instance |
 | grafana.image.tag | string | linkerdVersion | Docker image tag for the grafana instance |
+| grafana.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 |
 | grafana.proxy | string | `nil` |  |
 | grafana.resources.cpu.limit | string | `nil` | Maximum amount of CPU units that the grafana container can use |
 | grafana.resources.cpu.request | string | `nil` | Amount of CPU units that the grafana container requests |
 | grafana.resources.memory.limit | string | `nil` | Maximum amount of memory that grafana container can use |
 | grafana.resources.memory.request | string | `nil` | Amount of memory that the grafana container requests |
+| grafana.tolerations | string | `nil` | Tolerations section, See the [K8S documentation](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/) for more information |
 | grafanaUrl | string | `""` | url of external grafana instance with reverse proxy configured. |
 | identityTrustDomain | string | clusterDomain | Trust domain used for identity |
 | imagePullSecrets | list | `[]` | For Private docker registries, authentication is needed.  Registry secrets are applied to the respective service accounts |
@@ -114,14 +116,16 @@ Kubernetes: `>=1.16.0-0`
 | metricsAPI.image.registry | string | defaultRegistry | Docker registry for the metrics-api component |
 | metricsAPI.image.tag | string | linkerdVersion | Docker image tag for the metrics-api component |
 | metricsAPI.logLevel | string | defaultLogLevel | log level of the metrics-api component |
+| metricsAPI.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 |
 | metricsAPI.proxy | string | `nil` |  |
 | metricsAPI.replicas | int | `1` | number of replicas of the metrics-api component |
 | metricsAPI.resources.cpu.limit | string | `nil` | Maximum amount of CPU units that the metrics-api container can use |
 | metricsAPI.resources.cpu.request | string | `nil` | Amount of CPU units that the metrics-api container requests |
 | metricsAPI.resources.memory.limit | string | `nil` | Maximum amount of memory that metrics-api container can use |
 | metricsAPI.resources.memory.request | string | `nil` | Amount of memory that the metrics-api container requests |
+| metricsAPI.tolerations | string | `nil` | Tolerations section, See the [K8S documentation](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/) for more information |
 | namespace | string | `"linkerd-viz"` | Namespace in which the Linkerd Viz extension has to be installed |
-| 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 |
+| 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 |
 | prometheus.alertRelabelConfigs | string | `nil` | Alert relabeling is applied to alerts before they are sent to the Alertmanager. |
 | prometheus.alertmanagers | string | `nil` | Alertmanager instances the Prometheus server sends alerts to configured via the static_configs parameter. |
 | prometheus.args | object | `{"config.file":"/etc/prometheus/prometheus.yml","storage.tsdb.path":"/data","storage.tsdb.retention.time":"6h"}` | Command line options for Prometheus binary |
@@ -132,6 +136,7 @@ Kubernetes: `>=1.16.0-0`
 | prometheus.image.registry | string | `"prom"` | Docker registry for the prometheus instance |
 | prometheus.image.tag | string | `"v2.19.3"` | Docker image tag for the prometheus instance |
 | prometheus.logLevel | string | defaultLogLevel | log level of the prometheus instance |
+| prometheus.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 |
 | prometheus.proxy | string | `nil` |  |
 | prometheus.remoteWrite | string | `nil` | Allows transparently sending samples to an endpoint. Mostly used for long term storage. |
 | prometheus.resources.cpu.limit | string | `nil` | Maximum amount of CPU units that the prometheus container can use |
@@ -141,6 +146,7 @@ Kubernetes: `>=1.16.0-0`
 | prometheus.ruleConfigMapMounts | string | `nil` | Alerting/recording rule ConfigMap mounts (sub-path names must end in ´_rules.yml´ or ´_rules.yaml´) |
 | prometheus.scrapeConfigs | string | `nil` | A scrapeConfigs section specifies a set of targets and parameters describing how to scrape them. |
 | prometheus.sidecarContainers | string | `nil` | A sidecarContainers section specifies a list of secondary containers to run in the prometheus pod e.g. to export data to non-prometheus systems |
+| prometheus.tolerations | string | `nil` | Tolerations section, See the [K8S documentation](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/) for more information |
 | prometheusUrl | string | `""` | url of external prometheus instance |
 | tap.UID | string | `nil` | UID for the dashboard resource |
 | tap.caBundle | string | `""` | Bundle of CA certificates for Tap component. If not provided then Helm will use the certificate generated  for `tap.crtPEM`. If `tap.externalSecret` is set to true, this value must be set, as no certificate will be generated. |
@@ -177,7 +183,7 @@ Kubernetes: `>=1.16.0-0`
 | tapInjector.resources.cpu.request | string | `nil` | Amount of CPU units that the tapInjector container requests |
 | tapInjector.resources.memory.limit | string | `nil` | Maximum amount of memory that tapInjector container can use |
 | tapInjector.resources.memory.request | string | `nil` | Amount of memory that the tapInjector container requests |
-| tolerations | string | `nil` | Tolerations section, See the [K8S documentation](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/) for more information |
+| tolerations | string | `nil` | Default tolerations section, See the [K8S documentation](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/) for more information |
 
 ----------------------------------------------
 Autogenerated from chart metadata using [helm-docs v1.4.0](https://github.com/norwoodj/helm-docs/releases/v1.4.0)
diff --git a/viz/charts/linkerd-viz/templates/grafana.yaml b/viz/charts/linkerd-viz/templates/grafana.yaml
index 9d1ebeb4b..4f901b158 100644
--- a/viz/charts/linkerd-viz/templates/grafana.yaml
+++ b/viz/charts/linkerd-viz/templates/grafana.yaml
@@ -117,10 +117,10 @@ spec:
         namespace: {{.Values.namespace}}
         {{- with .Values.podLabels }}{{ toYaml . | trim | nindent 8 }}{{- end }}
     spec:
-      {{- if .Values.tolerations -}}
-      {{- include "linkerd.tolerations" . | nindent 6 }}
+      {{- if .Values.grafana.tolerations -}}
+      {{- include "linkerd.tolerations" (dict "Values" .Values.grafana) | nindent 6 }}
       {{- end -}}
-      {{- include "linkerd.node-selector" . | nindent 6 }}
+      {{- include "linkerd.node-selector" (dict "Values" .Values.grafana) | nindent 6 }}
       containers:
       - env:
         - name: GF_PATHS_DATA
diff --git a/viz/charts/linkerd-viz/templates/metrics-api.yaml b/viz/charts/linkerd-viz/templates/metrics-api.yaml
index 962c33b90..ca6aab7f5 100644
--- a/viz/charts/linkerd-viz/templates/metrics-api.yaml
+++ b/viz/charts/linkerd-viz/templates/metrics-api.yaml
@@ -57,10 +57,10 @@ spec:
         component: metrics-api
         {{- with .Values.podLabels }}{{ toYaml . | trim | nindent 8 }}{{- end }}
     spec:
-      {{- if .Values.tolerations -}}
-      {{- include "linkerd.tolerations" . | nindent 6 }}
+      {{- if .Values.metricsAPI.tolerations -}}
+      {{- include "linkerd.tolerations" (dict "Values" .Values.metricsAPI) | nindent 6 }}
       {{- end -}}
-      {{- include "linkerd.node-selector" . | nindent 6 }}
+      {{- include "linkerd.node-selector" (dict "Values" .Values.metricsAPI) | nindent 6 }}
       {{- if .Values.enablePodAntiAffinity -}}
       {{- $local := dict "component" "metrics-api" "label" "component" -}}
       {{- include "linkerd.pod-affinity" $local | nindent 6 -}}
diff --git a/viz/charts/linkerd-viz/templates/prometheus.yaml b/viz/charts/linkerd-viz/templates/prometheus.yaml
index 18cd008a2..111b5863b 100644
--- a/viz/charts/linkerd-viz/templates/prometheus.yaml
+++ b/viz/charts/linkerd-viz/templates/prometheus.yaml
@@ -224,17 +224,17 @@ spec:
         {{- with .Values.prometheus.proxy }}
         {{- include "partials.proxy.config.annotations" .resources | nindent 8 }}
         {{- end }}
-        {{- with .Values.podAnnotations }}{{  . | trim | nindent 8 }}{{- end }}
+        {{- with .Values.podAnnotations }}{{ toYaml . | trim | nindent 8 }}{{- end }}
       labels:
         linkerd.io/extension: viz
         component: prometheus
         namespace: {{.Values.namespace}}
         {{- with .Values.podLabels }}{{ toYaml . | trim | nindent 8 }}{{- end }}
     spec:
-      {{- if .Values.tolerations -}}
-      {{- include "linkerd.tolerations" . | nindent 6 }}
+      {{- if .Values.prometheus.tolerations -}}
+      {{- include "linkerd.tolerations" (dict "Values" .Values.prometheus) | nindent 6 }}
       {{- end -}}
-      {{- include "linkerd.node-selector" . | nindent 6 }}
+      {{- include "linkerd.node-selector" (dict "Values" .Values.prometheus) | nindent 6 }}
       securityContext:
         fsGroup: 65534
       containers:
diff --git a/viz/charts/linkerd-viz/values.yaml b/viz/charts/linkerd-viz/values.yaml
index 8c8ff694a..b6fd5421f 100644
--- a/viz/charts/linkerd-viz/values.yaml
+++ b/viz/charts/linkerd-viz/values.yaml
@@ -28,9 +28,9 @@ installNamespace: true
 # -- Namespace in which the Linkerd Viz extension has to be installed
 namespace: linkerd-viz
 
-# -- NodeSelector section, See the
+# -- Default nodeSelector section, See the
 # [K8S documentation](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector) for more information
-nodeSelector:
+nodeSelector: &default_node_selector
   beta.kubernetes.io/os: linux
 
 # -- For Private docker registries, authentication is needed.
@@ -38,10 +38,10 @@ nodeSelector:
 imagePullSecrets: []
 # - name: my-private-docker-registry-login-secret
 
-# -- Tolerations section, See the
+# -- Default tolerations section, See the
 # [K8S documentation](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/)
 # for more information
-tolerations:
+tolerations: &default_tolerations
 
 # -- Enables Pod Anti Affinity logic to balance the placement of replicas
 # across hosts and zones for High Availability.
@@ -98,6 +98,14 @@ metricsAPI:
   # -- UID for the metrics-api resource
   UID:
 
+  # -- NodeSelector section, See the
+  # [K8S documentation](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector) for more information
+  nodeSelector: *default_node_selector
+  # -- Tolerations section, See the
+  # [K8S documentation](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/)
+  # for more information
+  tolerations: *default_tolerations
+
 # tap configuration
 tap:
     # -- Number of tap component replicas
@@ -183,7 +191,7 @@ tapInjector:
     # matchLabels:
     #   foo: bar
   # -- UID for the tapInjector resource
-  UID: 
+  UID:
   failurePolicy: Ignore
   resources:
     cpu:
@@ -298,6 +306,14 @@ grafana:
     # into the grafana component
     # resources:
 
+  # -- NodeSelector section, See the
+  # [K8S documentation](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector) for more information
+  nodeSelector: *default_node_selector
+  # -- Tolerations section, See the
+  # [K8S documentation](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/)
+  # for more information
+  tolerations: *default_tolerations
+
 prometheus:
   # -- toggle field to enable or disable prometheus
   enabled: true
@@ -425,3 +441,11 @@ prometheus:
     # -- If set, overrides default proxy resources for the proxy injected
     # into the prometheus component
     # resources:
+
+  # -- NodeSelector section, See the
+  # [K8S documentation](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector) for more information
+  nodeSelector: *default_node_selector
+  # -- Tolerations section, See the
+  # [K8S documentation](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/)
+  # for more information
+  tolerations: *default_tolerations
diff --git a/viz/cmd/check.go b/viz/cmd/check.go
index 50a1d5527..ce9a930ae 100644
--- a/viz/cmd/check.go
+++ b/viz/cmd/check.go
@@ -6,6 +6,7 @@ import (
 	"os"
 	"time"
 
+	pkgcmd "github.com/linkerd/linkerd2/pkg/cmd"
 	"github.com/linkerd/linkerd2/pkg/healthcheck"
 	vizHealthCheck "github.com/linkerd/linkerd2/viz/pkg/healthcheck"
 	"github.com/spf13/cobra"
@@ -57,6 +58,10 @@ code.`,
 	cmd.Flags().BoolVar(&options.proxy, "proxy", options.proxy, "Also run data-plane checks, to determine if the data plane is healthy")
 	cmd.Flags().DurationVar(&options.wait, "wait", options.wait, "Maximum allowed time for all tests to pass")
 	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)
 	return cmd
 }
 
diff --git a/viz/cmd/edges.go b/viz/cmd/edges.go
index 9d4c6a541..d8a8fed97 100644
--- a/viz/cmd/edges.go
+++ b/viz/cmd/edges.go
@@ -14,11 +14,12 @@ import (
 	coreUtil "github.com/linkerd/linkerd2/controller/api/util"
 	pkgcmd "github.com/linkerd/linkerd2/pkg/cmd"
 	"github.com/linkerd/linkerd2/pkg/healthcheck"
+	"github.com/linkerd/linkerd2/pkg/k8s"
 	pb "github.com/linkerd/linkerd2/viz/metrics-api/gen/viz"
 	"github.com/linkerd/linkerd2/viz/metrics-api/util"
-	"github.com/linkerd/linkerd2/viz/pkg"
 	"github.com/linkerd/linkerd2/viz/pkg/api"
 	"github.com/spf13/cobra"
+	v1 "k8s.io/api/core/v1"
 )
 
 var (
@@ -83,8 +84,37 @@ func NewCmdEdges() *cobra.Command {
 
   # Get all edges between pods in all namespaces.
   linkerd viz edges po --all-namespaces`,
-		Args:      cobra.ExactArgs(1),
-		ValidArgs: pkg.ValidTargets,
+		Args: cobra.ExactArgs(1),
+		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+			// This command requires only one argument. If we already have
+			// one after requesting autocompletion i.e. [tab][tab]
+			// skip running validArgsFunction
+			if len(args) > 0 {
+				return []string{}, cobra.ShellCompDirectiveError
+			}
+
+			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)
+			}
+
+			if options.allNamespaces {
+				options.namespace = v1.NamespaceAll
+			}
+
+			cc := k8s.NewCommandCompletion(k8sAPI, options.namespace)
+
+			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)
@@ -138,6 +168,10 @@ func NewCmdEdges() *cobra.Command {
 	cmd.PersistentFlags().StringVarP(&options.namespace, "namespace", "n", options.namespace, "Namespace of the specified resource")
 	cmd.PersistentFlags().StringVarP(&options.outputFormat, "output", "o", options.outputFormat, "Output format; one of: \"table\" or \"json\" or \"wide\"")
 	cmd.PersistentFlags().BoolVarP(&options.allNamespaces, "all-namespaces", "A", options.allNamespaces, "If present, returns edges across all namespaces, ignoring the \"--namespace\" flag")
+
+	pkgcmd.ConfigureNamespaceFlagCompletion(
+		cmd, []string{"namespace"},
+		kubeconfigPath, impersonate, impersonateGroup, kubeContext)
 	return cmd
 }
 
diff --git a/viz/cmd/install.go b/viz/cmd/install.go
index a5ed3cb57..82d78f70a 100644
--- a/viz/cmd/install.go
+++ b/viz/cmd/install.go
@@ -21,7 +21,7 @@ import (
 )
 
 var (
-	templatesVIz = []string{
+	templatesViz = []string{
 		"templates/namespace.yaml",
 		"templates/metrics-api-rbac.yaml",
 		"templates/grafana-rbac.yaml",
@@ -36,6 +36,7 @@ var (
 		"templates/tap-injector-rbac.yaml",
 		"templates/tap-injector.yaml",
 		"templates/web.yaml",
+		"templates/service-profiles.yaml",
 	}
 )
 
@@ -116,7 +117,7 @@ func render(w io.Writer, valuesOverrides map[string]interface{}) error {
 		{Name: chartutil.ValuesfileName},
 	}
 
-	for _, template := range templatesVIz {
+	for _, template := range templatesViz {
 		files = append(files,
 			&loader.BufferedFile{Name: template},
 		)
diff --git a/viz/cmd/list.go b/viz/cmd/list.go
index 2abe868a4..fae8abd9b 100644
--- a/viz/cmd/list.go
+++ b/viz/cmd/list.go
@@ -5,7 +5,6 @@ import (
 	"os"
 
 	pkgcmd "github.com/linkerd/linkerd2/pkg/cmd"
-	"github.com/linkerd/linkerd2/pkg/k8s"
 	pkgK8s "github.com/linkerd/linkerd2/pkg/k8s"
 	vizLabels "github.com/linkerd/linkerd2/viz/pkg/labels"
 	"github.com/spf13/cobra"
@@ -26,7 +25,7 @@ func newCmdList() *cobra.Command {
 		Short: "Lists which pods can be tapped",
 		Args:  cobra.NoArgs,
 		RunE: func(cmd *cobra.Command, args []string) error {
-			k8sAPI, err := k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, impersonateGroup, 0)
+			k8sAPI, err := pkgK8s.NewAPI(kubeconfigPath, kubeContext, impersonate, impersonateGroup, 0)
 			if err != nil {
 				return err
 			}
@@ -90,5 +89,8 @@ 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/viz/cmd/profile.go b/viz/cmd/profile.go
index cfd66bbdd..2c583b182 100644
--- a/viz/cmd/profile.go
+++ b/viz/cmd/profile.go
@@ -73,6 +73,34 @@ func newCmdProfile() *cobra.Command {
   linkerd viz profile -n emojivoto web-svc --tap deploy/web --tap-duration 10s --tap-route-limit 5
 `,
 		Args: cobra.ExactArgs(1),
+		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+			// Skip providing suggestions if one or more arguments are provided
+			// We either have a suggestion selected or more multiple args are provided
+			// which is not allowed for this command.
+			if len(args) > 0 {
+				return nil, cobra.ShellCompDirectiveError
+			}
+
+			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)
+
+			// Profiles require completion for only services so prepend the service resource
+			// type to the list of args
+			results, err := cc.Complete([]string{k8s.Service}, toComplete)
+			if err != nil {
+				return nil, cobra.ShellCompDirectiveError
+			}
+
+			return results, cobra.ShellCompDirectiveDefault
+		},
 		RunE: func(cmd *cobra.Command, args []string) error {
 			api.CheckClientOrExit(healthcheck.Options{
 				ControlPlaneNamespace: controlPlaneNamespace,
@@ -110,6 +138,10 @@ func newCmdProfile() *cobra.Command {
 	cmd.PersistentFlags().DurationVar(&options.tapDuration, "tap-duration", options.tapDuration, "Duration over which tap data is collected (for example: \"10s\", \"1m\", \"10m\")")
 	cmd.PersistentFlags().UintVar(&options.tapRouteLimit, "tap-route-limit", options.tapRouteLimit, "Max number of routes to add to the profile")
 	cmd.PersistentFlags().StringVarP(&options.namespace, "namespace", "n", options.namespace, "Namespace of the service")
+
+	pkgcmd.ConfigureNamespaceFlagCompletion(
+		cmd, []string{"namespace"},
+		kubeconfigPath, impersonate, impersonateGroup, kubeContext)
 	return cmd
 }
 
diff --git a/viz/cmd/routes.go b/viz/cmd/routes.go
index 5d7698bbb..aebd8046d 100644
--- a/viz/cmd/routes.go
+++ b/viz/cmd/routes.go
@@ -105,6 +105,9 @@ This command will only display traffic which is sent to a service that has a Ser
 	cmd.PersistentFlags().StringVarP(&options.outputFormat, "output", "o", options.outputFormat, fmt.Sprintf("Output format; one of: \"%s\", \"%s\", or \"%s\"", tableOutput, wideOutput, jsonOutput))
 	cmd.PersistentFlags().StringVarP(&options.labelSelector, "selector", "l", options.labelSelector, "Selector (label query) to filter on, supports '=', '==', and '!='")
 
+	pkgcmd.ConfigureNamespaceFlagCompletion(
+		cmd, []string{"namespace", "to-namespace"},
+		kubeconfigPath, impersonate, impersonateGroup, kubeContext)
 	return cmd
 }
 
diff --git a/viz/cmd/stat.go b/viz/cmd/stat.go
index 9c80a59a1..36de57615 100644
--- a/viz/cmd/stat.go
+++ b/viz/cmd/stat.go
@@ -253,32 +253,12 @@ If no resource name is specified, displays stats about all resources of the spec
 	cmd.PersistentFlags().StringVarP(&options.labelSelector, "selector", "l", options.labelSelector, "Selector (label query) to filter on, supports '=', '==', and '!='")
 	cmd.PersistentFlags().BoolVar(&options.unmeshed, "unmeshed", options.unmeshed, "If present, include unmeshed resources in the output")
 
-	configureFlagCompletions(cmd)
+	pkgcmd.ConfigureNamespaceFlagCompletion(
+		cmd, []string{"namespace", "to-namespace", "from-namespace"},
+		kubeconfigPath, impersonate, impersonateGroup, kubeContext)
 	return cmd
 }
 
-func configureFlagCompletions(cmd *cobra.Command) {
-	flagNames := []string{"namespace", "to-namespace", "from-namespace"}
-	for _, flagName := range flagNames {
-		cmd.RegisterFlagCompletionFunc(flagName,
-			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, "")
-				results, err := cc.Complete([]string{"namespaces"}, toComplete)
-				if err != nil {
-					cobra.CompErrorln(err.Error())
-					return nil, cobra.ShellCompDirectiveError
-				}
-
-				return results, cobra.ShellCompDirectiveDefault
-			})
-	}
-}
-
 func respToRows(resp *pb.StatSummaryResponse) []*pb.StatTable_PodGroup_Row {
 	rows := make([]*pb.StatTable_PodGroup_Row, 0)
 	if resp != nil {
diff --git a/viz/cmd/tap.go b/viz/cmd/tap.go
index bda94f50a..26108c9b2 100644
--- a/viz/cmd/tap.go
+++ b/viz/cmd/tap.go
@@ -17,7 +17,6 @@ import (
 	"github.com/linkerd/linkerd2/pkg/k8s"
 	"github.com/linkerd/linkerd2/pkg/protohttp"
 	metricsPb "github.com/linkerd/linkerd2/viz/metrics-api/gen/viz"
-	vizpkg "github.com/linkerd/linkerd2/viz/pkg"
 	"github.com/linkerd/linkerd2/viz/pkg/api"
 	tapPb "github.com/linkerd/linkerd2/viz/tap/gen/tap"
 	"github.com/linkerd/linkerd2/viz/tap/pkg"
@@ -173,8 +172,33 @@ func NewCmdTap() *cobra.Command {
 
   # tap the test namespace, filter by request to prod namespace
   linkerd viz tap ns/test --to ns/prod`,
-		Args:      cobra.RangeArgs(1, 2),
-		ValidArgs: vizpkg.ValidTargets,
+		Args: cobra.RangeArgs(1, 2),
+		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+			// This command requires at most two arguments if we already have
+			// two after requesting autocompletion i.e. [tab][tab]
+			// skip running validArgsFunction
+			if len(args) > 1 {
+				return nil, cobra.ShellCompDirectiveError
+			}
+
+			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)
+
+			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)
@@ -251,6 +275,9 @@ func NewCmdTap() *cobra.Command {
 	cmd.PersistentFlags().StringVarP(&options.labelSelector, "selector", "l", options.labelSelector,
 		"Selector (label query) to filter on, supports '=', '==', and '!='")
 
+	pkgcmd.ConfigureNamespaceFlagCompletion(
+		cmd, []string{"namespace", "to-namespace"},
+		kubeconfigPath, impersonate, impersonateGroup, kubeContext)
 	return cmd
 }
 
diff --git a/viz/cmd/top.go b/viz/cmd/top.go
index 8c7643e01..dd7513bd9 100644
--- a/viz/cmd/top.go
+++ b/viz/cmd/top.go
@@ -18,7 +18,6 @@ import (
 	"github.com/linkerd/linkerd2/pkg/protohttp"
 	metricsAPI "github.com/linkerd/linkerd2/viz/metrics-api"
 	metricsPb "github.com/linkerd/linkerd2/viz/metrics-api/gen/viz"
-	vizpkg "github.com/linkerd/linkerd2/viz/pkg"
 	"github.com/linkerd/linkerd2/viz/pkg/api"
 	tapPb "github.com/linkerd/linkerd2/viz/tap/gen/tap"
 	"github.com/linkerd/linkerd2/viz/tap/pkg"
@@ -320,8 +319,33 @@ func NewCmdTop() *cobra.Command {
 
   # display traffic for the web-dlbvj pod in the default namespace
   linkerd viz top pod/web-dlbvj`,
-		Args:      cobra.RangeArgs(1, 2),
-		ValidArgs: vizpkg.ValidTargets,
+		Args: cobra.RangeArgs(1, 2),
+		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+			// This command requires at most two arguments if we already have
+			// two after requesting autocompletion i.e. [tab][tab]
+			// skip running validArgsFunction
+			if len(args) > 1 {
+				return nil, cobra.ShellCompDirectiveError
+			}
+
+			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)
+
+			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)
@@ -397,6 +421,9 @@ func NewCmdTop() *cobra.Command {
 	cmd.PersistentFlags().BoolVar(&options.routes, "routes", options.routes, "Display data per route instead of per path")
 	cmd.PersistentFlags().StringVarP(&options.labelSelector, "selector", "l", options.labelSelector, "Selector (label query) to filter on, supports '=', '==', and '!='")
 
+	pkgcmd.ConfigureNamespaceFlagCompletion(
+		cmd, []string{"namespace", "to-namespace"},
+		kubeconfigPath, impersonate, impersonateGroup, kubeContext)
 	return cmd
 }
 
diff --git a/viz/metrics-api/Dockerfile b/viz/metrics-api/Dockerfile
index ef66e7758..4ac473cb5 100644
--- a/viz/metrics-api/Dockerfile
+++ b/viz/metrics-api/Dockerfile
@@ -1,7 +1,7 @@
 ARG BUILDPLATFORM=linux/amd64
 
 # Precompile key slow-to-build dependencies
-FROM --platform=$BUILDPLATFORM golang:1.16.2-alpine as go-deps
+FROM --platform=$BUILDPLATFORM golang:1.16.4-alpine as go-deps
 WORKDIR /linkerd-build
 COPY go.mod go.sum ./
 COPY bin/install-deps bin/
diff --git a/viz/tap/Dockerfile b/viz/tap/Dockerfile
index 3e4402cbf..f58c68ba7 100644
--- a/viz/tap/Dockerfile
+++ b/viz/tap/Dockerfile
@@ -1,7 +1,7 @@
 ARG BUILDPLATFORM=linux/amd64
 
 # Precompile key slow-to-build dependencies
-FROM --platform=$BUILDPLATFORM golang:1.16.2-alpine as go-deps
+FROM --platform=$BUILDPLATFORM golang:1.16.4-alpine as go-deps
 WORKDIR /linkerd-build
 COPY go.mod go.sum ./
 COPY bin/install-deps bin/
diff --git a/viz/tap/api/handlers.go b/viz/tap/api/handlers.go
index 160c57d2a..4a9b3085a 100644
--- a/viz/tap/api/handlers.go
+++ b/viz/tap/api/handlers.go
@@ -120,8 +120,8 @@ func (h *handler) handleTap(w http.ResponseWriter, req *http.Request, p httprout
 		return
 	}
 
-	h.log.Debugf("SubjectAccessReview: namespace: %s, resource: %s, name: %s, user: %s, group: %s",
-		namespace, resource, name, req.Header.Get(h.usernameHeader), req.Header[h.groupHeader],
+	h.log.Debugf("SubjectAccessReview: namespace: %s, resource: %s, name: %s, user: <%s>, group: <%s>",
+		namespace, resource, name, h.usernameHeader, h.groupHeader,
 	)
 
 	// TODO: it's possible this SubjectAccessReview is redundant, consider
@@ -137,7 +137,7 @@ func (h *handler) handleTap(w http.ResponseWriter, req *http.Request, p httprout
 		"tap",
 		name,
 		req.Header.Get(h.usernameHeader),
-		req.Header[h.groupHeader],
+		req.Header.Values(h.groupHeader),
 	)
 	if err != nil {
 		err = fmt.Errorf("tap authorization failed (%s), visit %s for more information", err, pkg.TapRbacURL)
diff --git a/web/Dockerfile b/web/Dockerfile
index 228c6f203..acb4bf728 100644
--- a/web/Dockerfile
+++ b/web/Dockerfile
@@ -1,7 +1,7 @@
 ARG BUILDPLATFORM=linux/amd64
 
 # Precompile key slow-to-build dependencies
-FROM --platform=$BUILDPLATFORM golang:1.16.2-alpine as go-deps
+FROM --platform=$BUILDPLATFORM golang:1.16.4-alpine as go-deps
 WORKDIR /linkerd-build
 COPY go.mod go.sum ./
 COPY bin/install-deps bin/
diff --git a/web/app/package.json b/web/app/package.json
index b707873bf..26f063ffd 100644
--- a/web/app/package.json
+++ b/web/app/package.json
@@ -90,6 +90,7 @@
   },
   "resolutions": {
     "@lingui/**/**/minimist": ">=1.2.5",
+    "multicast-dns": "7.2.3",
     "webpack-dev-server/selfsigned/node-forge": ">=0.10.0"
   },
   "jest": {
diff --git a/web/app/yarn.lock b/web/app/yarn.lock
index 03265076a..4e996c226 100644
--- a/web/app/yarn.lock
+++ b/web/app/yarn.lock
@@ -2848,15 +2848,15 @@ browser-stdout@1.3.1:
   integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==
 
 browserslist@^4.14.5, browserslist@^4.16.3:
-  version "4.16.3"
-  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.3.tgz#340aa46940d7db878748567c5dea24a48ddf3717"
-  integrity sha512-vIyhWmIkULaq04Gt93txdh+j02yX/JzlyhLYbV3YQCn/zvES3JnY7TifHHvvr1w5hTDluNKMkV05cs4vy8Q7sw==
+  version "4.16.6"
+  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.6.tgz#d7901277a5a88e554ed305b183ec9b0c08f66fa2"
+  integrity sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==
   dependencies:
-    caniuse-lite "^1.0.30001181"
-    colorette "^1.2.1"
-    electron-to-chromium "^1.3.649"
+    caniuse-lite "^1.0.30001219"
+    colorette "^1.2.2"
+    electron-to-chromium "^1.3.723"
     escalade "^3.1.1"
-    node-releases "^1.1.70"
+    node-releases "^1.1.71"
 
 bser@2.1.1:
   version "2.1.1"
@@ -3010,10 +3010,10 @@ camelcase@^6.0.0, camelcase@^6.2.0:
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809"
   integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==
 
-caniuse-lite@^1.0.30001181:
-  version "1.0.30001204"
-  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001204.tgz#256c85709a348ec4d175e847a3b515c66e79f2aa"
-  integrity sha512-JUdjWpcxfJ9IPamy2f5JaRDCaqJOxDzOSKtbdx4rH9VivMd1vIzoPumsJa9LoMIi4Fx2BV2KZOxWhNkBjaYivQ==
+caniuse-lite@^1.0.30001219:
+  version "1.0.30001228"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001228.tgz#bfdc5942cd3326fa51ee0b42fbef4da9d492a7fa"
+  integrity sha512-QQmLOGJ3DEgokHbMSA8cj2a+geXqmnpyOFT0lhQV6P3/YOJvGDEwoedcwxEQ30gJIwIIunHIicunJ2rzK5gB2A==
 
 capture-exit@^2.0.0:
   version "2.0.0"
@@ -4034,13 +4034,12 @@ dns-equal@^1.0.0:
   resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d"
   integrity sha1-s55/HabrCnW6nBcySzR1PEfgZU0=
 
-dns-packet@^1.3.1:
-  version "1.3.1"
-  resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.1.tgz#12aa426981075be500b910eedcd0b47dd7deda5a"
-  integrity sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==
+dns-packet@^5.2.2:
+  version "5.2.3"
+  resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.2.3.tgz#ebd234118c40e59ec7097c50a4b2ee36fe9ae0c9"
+  integrity sha512-qrcTxpFwILBjWasLSVQmBXTI8XVLP9Tbmyv9Mvb41Exnl7yLqQ5UkXV7Jf+Ak14PBwoeOfCpMZZCzqUQVX40Zw==
   dependencies:
-    ip "^1.1.0"
-    safe-buffer "^5.0.1"
+    ip "^1.1.5"
 
 dns-txt@^2.0.2:
   version "2.0.2"
@@ -4196,10 +4195,10 @@ ejs@^3.0.1:
   dependencies:
     jake "^10.6.1"
 
-electron-to-chromium@^1.3.649:
-  version "1.3.700"
-  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.700.tgz#a6999a954c698dc7da5e84c369d65432dbe895be"
-  integrity sha512-wQtaxVZzpOeCjW1CGuC5W3bYjE2jglvk076LcTautBOB9UtHztty7wNzjVsndiMcSsdUsdMy5w76w5J1U7OPTQ==
+electron-to-chromium@^1.3.723:
+  version "1.3.737"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.737.tgz#196f2e9656f4f3c31930750e1899c091b72d36b5"
+  integrity sha512-P/B84AgUSQXaum7a8m11HUsYL8tj9h/Pt5f7Hg7Ty6bm5DxlFq+e5+ouHUoNQMsKDJ7u4yGfI8mOErCmSH9wyg==
 
 emittery@^0.7.1:
   version "0.7.2"
@@ -5921,7 +5920,7 @@ ip-regex@^4.1.0:
   resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.3.0.tgz#687275ab0f57fa76978ff8f4dddc8a23d5990db5"
   integrity sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==
 
-ip@^1.1.0, ip@^1.1.5:
+ip@^1.1.5:
   version "1.1.5"
   resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a"
   integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=
@@ -7826,12 +7825,12 @@ multicast-dns-service-types@^1.1.0:
   resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901"
   integrity sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=
 
-multicast-dns@^6.0.1:
-  version "6.2.3"
-  resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.2.3.tgz#a0ec7bd9055c4282f790c3c82f4e28db3b31b229"
-  integrity sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==
+multicast-dns@7.2.3, multicast-dns@^6.0.1:
+  version "7.2.3"
+  resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-7.2.3.tgz#cbd07571dda41807b36f71067681f19e85ccc2cd"
+  integrity sha512-TzxgGSLRLB7tqAlzjgd2x2ZE0cDsGFq4rs9W4yE5xp+7hlRXeUQGtXZsTGfGw2FwWB45rfe8DtXMYBpZGMLUng==
   dependencies:
-    dns-packet "^1.3.1"
+    dns-packet "^5.2.2"
     thunky "^1.0.2"
 
 mute-stream@0.0.7:
@@ -7955,10 +7954,10 @@ node-notifier@^8.0.0:
     uuid "^8.3.0"
     which "^2.0.2"
 
-node-releases@^1.1.70:
-  version "1.1.71"
-  resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.71.tgz#cb1334b179896b1c89ecfdd4b725fb7bbdfc7dbb"
-  integrity sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg==
+node-releases@^1.1.71:
+  version "1.1.72"
+  resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.72.tgz#14802ab6b1039a79a0c7d662b610a5bbd76eacbe"
+  integrity sha512-LLUo+PpH3dU6XizX3iVoubUNheF/owjXCZZ5yACDxNnPtgFuludV1ZL3ayK1kVep42Rmm0+R9/Y60NQbZ2bifw==
 
 normalize-package-data@^2.3.2, normalize-package-data@^2.5.0:
   version "2.5.0"


Test patch:
diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml
index a92621bed..44f9004d2 100644
--- a/.github/workflows/integration_tests.yml
+++ b/.github/workflows/integration_tests.yml
@@ -1,10 +1,15 @@
 name: Integration tests
 on:
-  pull_request: {}
+  pull_request:
+    paths-ignore:
+    - '*.md'
+    - '**/*.md'
+    - 'web/app/package.json'
   push:
     paths-ignore:
     - '*.md'
     - '**/*.md'
+    - 'web/app/package.json'
     branches:
     - main
 env:
@@ -20,8 +25,7 @@ jobs:
     timeout-minutes: 30
     steps:
     - name: Checkout code
-      # actions/checkout@v2
-      uses: actions/checkout@722adc63f1aa60a57ec37892e133b1d319cae598
+      uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
     - name: Set environment variables from scripts
       run: |
         . bin/_tag.sh
@@ -75,8 +79,7 @@ jobs:
     timeout-minutes: 30
     steps:
     - name: Checkout code
-      # actions/checkout@v2
-      uses: actions/checkout@722adc63f1aa60a57ec37892e133b1d319cae598
+      uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
     - name: Try to load cached Go modules
       # actions/cache@v1.1.2
       uses: actions/cache@70655ec8323daeeaa7ef06d7c56e1b9191396cbe
@@ -115,8 +118,7 @@ jobs:
     timeout-minutes: 60
     steps:
     - name: Checkout code
-      # actions/checkout@v2
-      uses: actions/checkout@722adc63f1aa60a57ec37892e133b1d319cae598
+      uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
     - name: Try to load cached Go modules
       # actions/cache@v1.1.2
       uses: actions/cache@70655ec8323daeeaa7ef06d7c56e1b9191396cbe
diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml
index 094e3d9d7..3237af44f 100644
--- a/.github/workflows/unit_tests.yml
+++ b/.github/workflows/unit_tests.yml
@@ -1,6 +1,9 @@
 name: Unit tests
 on:
-  pull_request: {}
+  pull_request:
+    paths-ignore:
+    - '*.md'
+    - '**/*.md'
   push:
     paths-ignore:
     - '*.md'
@@ -13,11 +16,10 @@ jobs:
     timeout-minutes: 30
     runs-on: ubuntu-20.04
     container:
-      image: golang:1.16.2
+      image: golang:1.16.4
     steps:
     - name: Checkout code
-      # actions/checkout@v2
-      uses: actions/checkout@722adc63f1aa60a57ec37892e133b1d319cae598
+      uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
     - name: Go unit tests
       run: |
         go get gotest.tools/gotestsum@v0.4.2
@@ -31,8 +33,7 @@ jobs:
       image: node:14-stretch
     steps:
     - name: Checkout code
-      # actions/checkout@v2
-      uses: actions/checkout@722adc63f1aa60a57ec37892e133b1d319cae598
+      uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
     - name: Yarn setup
       run: curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.21.1 --network-concurrency 1
     - name: JS unit tests
diff --git a/bin/test-scale b/bin/test-scale
index 40fc40b59..24d5cec8b 100755
--- a/bin/test-scale
+++ b/bin/test-scale
@@ -70,7 +70,6 @@ linkerd_namespace=${2:-l5d-scale}
 
 "$linkerd_path" -l "$linkerd_namespace" install | kubectl apply -f -
 "$linkerd_path" -l "$linkerd_namespace" check --expected-version="$linkerd_version"
-"$linkerd_path" -l "$linkerd_namespace" install-sp | kubectl apply -f -
 
 #
 # Deploy Books
diff --git a/cli/cmd/install-sp_test.go b/cli/cmd/install-sp_test.go
deleted file mode 100644
index edf94b505..000000000
--- a/cli/cmd/install-sp_test.go
+++ /dev/null
@@ -1,30 +0,0 @@
-package cmd
-
-import (
-	"bytes"
-	"fmt"
-	"testing"
-)
-
-func TestRenderSP(t *testing.T) {
-	testCases := []struct {
-		controlPlaneNamespace string
-		clusterDomain         string
-		goldenFileName        string
-	}{
-		{controlPlaneNamespace, "cluster.local", "install-sp_default.golden"},
-		{"NAMESPACE", "CLUSTERDOMAIN", "install-sp_output.golden"},
-	}
-
-	for i, tc := range testCases {
-		tc := tc // pin
-		t.Run(fmt.Sprintf("%d: %s", i, "testdata/"+tc.goldenFileName), func(t *testing.T) {
-			var buf bytes.Buffer
-			err := renderSP(&buf, tc.controlPlaneNamespace, tc.clusterDomain)
-			if err != nil {
-				t.Fatalf("Unexpected error: %v", err)
-			}
-			testDataDiffer.DiffTestdata(t, tc.goldenFileName, buf.String())
-		})
-	}
-}
diff --git a/cli/cmd/testdata/install-sp_output.golden b/cli/cmd/testdata/install-sp_output.golden
deleted file mode 100644
index 3f1db28c1..000000000
--- a/cli/cmd/testdata/install-sp_output.golden
+++ /dev/null
@@ -1,136 +0,0 @@
-apiVersion: linkerd.io/v1alpha2
-kind: ServiceProfile
-metadata:
-  name: linkerd-controller-api.NAMESPACE.svc.CLUSTERDOMAIN
-  namespace: NAMESPACE
-spec:
-  routes:
-  - name: POST /api/v1/StatSummary
-    condition:
-      method: POST
-      pathRegex: /api/v1/StatSummary
-  - name: POST /api/v1/TopRoutes
-    condition:
-      method: POST
-      pathRegex: /api/v1/TopRoutes
-  - name: POST /api/v1/ListPods
-    condition:
-      method: POST
-      pathRegex: /api/v1/ListPods
-  - name: POST /api/v1/ListServices
-    condition:
-      method: POST
-      pathRegex: /api/v1/ListServices
-  - name: POST /api/v1/Version
-    condition:
-      method: POST
-      pathRegex: /api/v1/Version
-  - name: POST /api/v1/SelfCheck
-    condition:
-      method: POST
-      pathRegex: /api/v1/SelfCheck
----
-apiVersion: linkerd.io/v1alpha2
-kind: ServiceProfile
-metadata:
-  name: linkerd-dst.NAMESPACE.svc.CLUSTERDOMAIN
-  namespace: NAMESPACE
-spec:
-  routes:
-  - name: POST /io.linkerd.proxy.destination.Destination/Get
-    condition:
-      method: POST
-      pathRegex: /io\.linkerd\.proxy\.destination\.Destination/Get
-  - name: POST /io.linkerd.proxy.destination.Destination/GetProfile
-    condition:
-      method: POST
-      pathRegex: /io\.linkerd\.proxy\.destination\.Destination/GetProfile
----
-apiVersion: linkerd.io/v1alpha2
-kind: ServiceProfile
-metadata:
-  name: linkerd-dst-headless.NAMESPACE.svc.CLUSTERDOMAIN
-  namespace: NAMESPACE
-spec:
-  routes:
-  - name: POST /io.linkerd.proxy.destination.Destination/Get
-    condition:
-      method: POST
-      pathRegex: /io\.linkerd\.proxy\.destination\.Destination/Get
-  - name: POST /io.linkerd.proxy.destination.Destination/GetProfile
-    condition:
-      method: POST
-      pathRegex: /io\.linkerd\.proxy\.destination\.Destination/GetProfile
----
-apiVersion: linkerd.io/v1alpha2
-kind: ServiceProfile
-metadata:
-  name: linkerd-prometheus.NAMESPACE.svc.CLUSTERDOMAIN
-  namespace: NAMESPACE
-spec:
-  routes:
-  - name: POST /api/v1/query
-    condition:
-      method: POST
-      pathRegex: /api/v1/query
-  - name: GET /api/v1/query_range
-    condition:
-      method: GET
-      pathRegex: /api/v1/query_range
-  - name: GET /api/v1/series
-    condition:
-      method: GET
-      pathRegex: /api/v1/series
----
-apiVersion: linkerd.io/v1alpha2
-kind: ServiceProfile
-metadata:
-  name: linkerd-grafana.NAMESPACE.svc.CLUSTERDOMAIN
-  namespace: NAMESPACE
-spec:
-  routes:
-  - name: GET /api/annotations
-    condition:
-      method: GET
-      pathRegex: /api/annotations
-  - name: GET /api/dashboards/tags
-    condition:
-      method: GET
-      pathRegex: /api/dashboards/tags
-  - name: GET /api/dashboards/uid/{uid}
-    condition:
-      method: GET
-      pathRegex: /api/dashboards/uid/.*
-  - name: GET /api/dashboard/{dashboard}
-    condition:
-      method: GET
-      pathRegex: /api/dashboard/.*
-  - name: GET /api/datasources/proxy/1/api/v1/series
-    condition:
-      method: GET
-      pathRegex: /api/datasources/proxy/1/api/v1/series
-  - name: GET /api/datasources/proxy/1/api/v1/query_range
-    condition:
-      method: GET
-      pathRegex: /api/datasources/proxy/1/api/v1/query_range
-  - name: GET /api/search
-    condition:
-      method: GET
-      pathRegex: /api/search
-  - name: GET /d/{uid}/{dashboard-name}
-    condition:
-      method: GET
-      pathRegex: /d/[^/]*/.*
-  - name: GET /public/build/{style}.css
-    condition:
-      method: GET
-      pathRegex: /public/build/.*\.css
-  - name: GET /public/fonts/{font}
-    condition:
-      method: GET
-      pathRegex: /public/fonts/.*
-  - name: GET /public/img/{img}
-    condition:
-      method: GET
-      pathRegex: /public/img/.*
----
diff --git a/controller/api/destination/server_test.go b/controller/api/destination/server_test.go
index 8ca9d6303..7bddbbe8e 100644
--- a/controller/api/destination/server_test.go
+++ b/controller/api/destination/server_test.go
@@ -413,12 +413,12 @@ func TestGet(t *testing.T) {
 			t.Fatalf("Expected len(addrs) to be > 0")
 		}
 
-		if addrs[0].ProtocolHint != nil {
+		if addrs[0].GetProtocolHint().GetProtocol() != nil || addrs[0].GetProtocolHint().GetOpaqueTransport() != nil {
 			t.Fatalf("Expected protocol hint for %s to be nil but got %+v", path, addrs[0].ProtocolHint)
 		}
 
 		if addrs[0].TlsIdentity != nil {
-			t.Fatalf("Expected protocol hint for %s to be nil but got %+v", path, addrs[0].TlsIdentity)
+			t.Fatalf("Expected TLS identity for %s to be nil but got %+v", path, addrs[0].TlsIdentity)
 		}
 	})
 }
@@ -682,10 +682,10 @@ func TestGetProfiles(t *testing.T) {
 		if !exists {
 			t.Fatalf("Expected 'namespace' metric label to exist but it did not")
 		}
-		if first.Endpoint.ProtocolHint == nil {
+		if first.GetEndpoint().GetProtocolHint() == nil {
 			t.Fatalf("Expected protocol hint but found none")
 		}
-		if first.Endpoint.ProtocolHint.GetOpaqueTransport() != nil {
+		if first.GetEndpoint().GetProtocolHint().GetOpaqueTransport() != nil {
 			t.Fatalf("Expected pod to not support opaque traffic on port %d", port)
 		}
 		if first.Endpoint.Addr.String() != epAddr.String() {
@@ -745,7 +745,7 @@ func TestGetProfiles(t *testing.T) {
 		if first.Endpoint == nil {
 			t.Fatalf("Expected response to have endpoint field")
 		}
-		if first.Endpoint.ProtocolHint != nil {
+		if first.Endpoint.GetProtocolHint().GetProtocol() != nil || first.Endpoint.GetProtocolHint().GetOpaqueTransport() != nil {
 			t.Fatalf("Expected no protocol hint but found one")
 		}
 	})
@@ -884,12 +884,12 @@ func TestGetProfiles(t *testing.T) {
 			t.Fatalf("Expected to not be nil")
 		}
 
-		if addr.ProtocolHint != nil {
+		if addr.GetProtocolHint().GetProtocol() != nil || addr.GetProtocolHint().GetOpaqueTransport() != nil {
 			t.Fatalf("Expected protocol hint for %s to be nil but got %+v", path, addr.ProtocolHint)
 		}
 
 		if addr.TlsIdentity != nil {
-			t.Fatalf("Expected protocol hint for %s to be nil but got %+v", path, addr.TlsIdentity)
+			t.Fatalf("Expected TLS identity for %s to be nil but got %+v", path, addr.TlsIdentity)
 		}
 	})
 }
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
new file mode 100644
index 000000000..5e2e0d4dd
--- /dev/null
+++ b/jaeger/cmd/install_test.go
@@ -0,0 +1,55 @@
+package cmd
+
+import (
+	"bytes"
+	"fmt"
+	"testing"
+
+	"github.com/linkerd/linkerd2/pkg/charts"
+)
+
+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",
+			"crtPEM":   "test-webhook-crt-pem",
+			"caBundle": "test-webhook-ca-bundle",
+		},
+	}
+
+	testCases := []struct {
+		values         map[string]interface{}
+		goldenFileName string
+	}{
+		{
+			nil,
+			"install_default.golden",
+		},
+		{
+			map[string]interface{}{
+				"jaeger": map[string]interface{}{"enabled": false},
+			},
+			"install_jaeger_disabled.golden",
+		},
+		{
+			map[string]interface{}{
+				"collector": map[string]interface{}{"enabled": false},
+			},
+			"install_collector_disabled.golden",
+		},
+	}
+
+	for i, tc := range testCases {
+		tc := tc // pin
+		t.Run(fmt.Sprintf("%d: %s", i, tc.goldenFileName), func(t *testing.T) {
+			var buf bytes.Buffer
+			// Merge overrides with default
+			if err := render(&buf, charts.MergeMaps(defaultValues, tc.values)); err != nil {
+				t.Fatalf("Failed to render templates: %v", err)
+			}
+			testDataDiffer.DiffTestdata(t, tc.goldenFileName, buf.String())
+		})
+	}
+}
diff --git a/jaeger/cmd/main_test.go b/jaeger/cmd/main_test.go
new file mode 100644
index 000000000..676a7e0af
--- /dev/null
+++ b/jaeger/cmd/main_test.go
@@ -0,0 +1,23 @@
+package cmd
+
+import (
+	"flag"
+	"os"
+	"testing"
+
+	"github.com/linkerd/linkerd2/testutil"
+)
+
+var (
+	testDataDiffer testutil.TestDataDiffer
+)
+
+// TestMain parses flags before running tests
+func TestMain(m *testing.M) {
+	flag.BoolVar(&testDataDiffer.UpdateFixtures, "update", false, "update text fixtures in place")
+	prettyDiff := os.Getenv("LINKERD_TEST_PRETTY_DIFF") != ""
+	flag.BoolVar(&testDataDiffer.PrettyDiff, "pretty-diff", prettyDiff, "display the full text when diffing")
+	flag.StringVar(&testDataDiffer.RejectPath, "reject-path", "", "write results for failed tests to this path (path is relative to the test location)")
+	flag.Parse()
+	os.Exit(m.Run())
+}
diff --git a/jaeger/cmd/testdata/install_collector_disabled.golden b/jaeger/cmd/testdata/install_collector_disabled.golden
new file mode 100644
index 000000000..a58eb8026
--- /dev/null
+++ b/jaeger/cmd/testdata/install_collector_disabled.golden
@@ -0,0 +1,232 @@
+---
+kind: Namespace
+apiVersion: v1
+metadata:
+  name: linkerd-jaeger
+  labels:
+    linkerd.io/extension: jaeger
+  annotations:
+    linkerd.io/inject: enabled
+    config.linkerd.io/proxy-await: "enabled"
+---
+###
+### Jaeger Injector
+###
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  labels:
+    linkerd.io/extension: jaeger
+    app.kubernetes.io/name: jaeger-injector
+    app.kubernetes.io/part-of: Linkerd
+    app.kubernetes.io/version: dev-undefined
+    component: jaeger-injector
+  name: jaeger-injector
+  namespace: linkerd-jaeger
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      linkerd.io/extension: jaeger
+      component: jaeger-injector
+  template:
+    metadata:
+      annotations:
+        checksum/config: 09b8c09754744aff9027048d3863d2401e4547f07d87f6e3dee643947adb0976
+      labels:
+        linkerd.io/extension: jaeger
+        component: jaeger-injector
+    spec:
+      nodeSelector:
+        beta.kubernetes.io/os: linux
+      containers:
+      - args:
+        - -collector-svc-addr=collector.linkerd-jaeger:55678
+        - -collector-svc-account=collector
+        - -log-level=info
+        image: cr.l5d.io/linkerd/jaeger-webhook:dev-undefined
+        imagePullPolicy: IfNotPresent
+        livenessProbe:
+          httpGet:
+            path: /ping
+            port: 9995
+          initialDelaySeconds: 10
+        name: jaeger-injector
+        ports:
+        - containerPort: 8443
+          name: jaeger-injector
+        - containerPort: 9995
+          name: admin-http
+        readinessProbe:
+          failureThreshold: 7
+          httpGet:
+            path: /ready
+            port: 9995
+        volumeMounts:
+        - mountPath: /var/run/linkerd/tls
+          name: tls
+          readOnly: true
+      serviceAccountName: jaeger-injector
+      volumes:
+      - name: tls
+        secret:
+          secretName: jaeger-injector-k8s-tls
+---
+kind: Service
+apiVersion: v1
+metadata:
+  name: jaeger-injector
+  namespace: linkerd-jaeger
+  labels:
+    linkerd.io/extension: jaeger
+    component: jaeger-injector
+spec:
+  type: ClusterIP
+  selector:
+    linkerd.io/extension: jaeger
+    component: jaeger-injector
+  ports:
+  - name: jaeger-injector
+    port: 443
+    targetPort: jaeger-injector
+---
+###
+### Jaeger Injector RBAC
+###
+kind: ClusterRole
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: linkerd-jaeger-injector
+  labels:
+    linkerd.io/extension: jaeger
+rules:
+- apiGroups: [""]
+  resources: ["namespaces"]
+  verbs: ["get", "list", "watch"]
+---
+kind: ClusterRoleBinding
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: linkerd-jaeger-injector
+  labels:
+    linkerd.io/extension: jaeger
+subjects:
+- kind: ServiceAccount
+  name: jaeger-injector
+  namespace: linkerd-jaeger
+  apiGroup: ""
+roleRef:
+  kind: ClusterRole
+  name: linkerd-jaeger-injector
+  apiGroup: rbac.authorization.k8s.io
+---
+kind: ServiceAccount
+apiVersion: v1
+metadata:
+  name: jaeger-injector
+  namespace: linkerd-jaeger
+---
+kind: Secret
+apiVersion: v1
+metadata:
+  name: jaeger-injector-k8s-tls
+  namespace: linkerd-jaeger
+type: kubernetes.io/tls
+data:
+  tls.crt: dGVzdC13ZWJob29rLWNydC1wZW0=
+  tls.key: dGVzdC13ZWJoaG9vay1rZXktcGVt
+---
+apiVersion: admissionregistration.k8s.io/v1
+kind: MutatingWebhookConfiguration
+metadata:
+  name: linkerd-jaeger-injector-webhook-config
+  labels:
+    linkerd.io/extension: jaeger
+webhooks:
+- name: jaeger-injector.linkerd.io
+  clientConfig:
+    service:
+      name: jaeger-injector
+      namespace: linkerd-jaeger
+      path: "/"
+    caBundle: dGVzdC13ZWJob29rLWNhLWJ1bmRsZQ==
+  failurePolicy: Ignore
+  admissionReviewVersions: ["v1", "v1beta1"]
+  reinvocationPolicy: IfNeeded
+  rules:
+  - operations: [ "CREATE" ]
+    apiGroups: [""]
+    apiVersions: ["v1"]
+    resources: ["pods"]
+  sideEffects: None
+---
+###
+### jaeger RBAC
+###
+kind: ServiceAccount
+apiVersion: v1
+metadata:
+  name: jaeger
+  namespace: linkerd-jaeger
+---
+###
+### Tracing Jaeger Service
+###
+apiVersion: v1
+kind: Service
+metadata:
+  name: jaeger
+  namespace: linkerd-jaeger
+  labels:
+    component: jaeger
+spec:
+  type: ClusterIP
+  selector:
+    component: jaeger
+  ports:
+    - name: collection
+      port: 14268
+    - name: ui
+      port: 16686
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  labels:
+    app.kubernetes.io/name: jaeger
+    app.kubernetes.io/part-of: Linkerd
+    component: jaeger
+  name: jaeger
+  namespace: linkerd-jaeger
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      component: jaeger
+  template:
+    metadata:
+      annotations:
+        config.linkerd.io/proxy-await: "enabled"
+        prometheus.io/path: /metrics
+        prometheus.io/port: "14269"
+        prometheus.io/scrape: "true"
+      labels:
+        component: jaeger
+    spec:
+      nodeSelector:
+        beta.kubernetes.io/os: linux
+      containers:
+      - args:
+        - --query.base-path=/jaeger
+        image: jaegertracing/all-in-one:1.19.2
+        imagePullPolicy: Always
+        name: jaeger
+        ports:
+        - containerPort: 14269
+          name: admin
+        - containerPort: 14268
+          name: collection
+        - containerPort: 16686
+          name: ui
+      dnsPolicy: ClusterFirst
+      serviceAccountName: jaeger
diff --git a/jaeger/cmd/testdata/install_default.golden b/jaeger/cmd/testdata/install_default.golden
new file mode 100644
index 000000000..3f4c56ece
--- /dev/null
+++ b/jaeger/cmd/testdata/install_default.golden
@@ -0,0 +1,357 @@
+---
+kind: Namespace
+apiVersion: v1
+metadata:
+  name: linkerd-jaeger
+  labels:
+    linkerd.io/extension: jaeger
+  annotations:
+    linkerd.io/inject: enabled
+    config.linkerd.io/proxy-await: "enabled"
+---
+###
+### Jaeger Injector
+###
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  labels:
+    linkerd.io/extension: jaeger
+    app.kubernetes.io/name: jaeger-injector
+    app.kubernetes.io/part-of: Linkerd
+    app.kubernetes.io/version: dev-undefined
+    component: jaeger-injector
+  name: jaeger-injector
+  namespace: linkerd-jaeger
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      linkerd.io/extension: jaeger
+      component: jaeger-injector
+  template:
+    metadata:
+      annotations:
+        checksum/config: 93ee231f45614b315429e63eb8c79ca948062c4f47b19c8c273ab943bd182ff3
+      labels:
+        linkerd.io/extension: jaeger
+        component: jaeger-injector
+    spec:
+      nodeSelector:
+        beta.kubernetes.io/os: linux
+      containers:
+      - args:
+        - -collector-svc-addr=collector.linkerd-jaeger:55678
+        - -collector-svc-account=collector
+        - -log-level=info
+        image: cr.l5d.io/linkerd/jaeger-webhook:dev-undefined
+        imagePullPolicy: IfNotPresent
+        livenessProbe:
+          httpGet:
+            path: /ping
+            port: 9995
+          initialDelaySeconds: 10
+        name: jaeger-injector
+        ports:
+        - containerPort: 8443
+          name: jaeger-injector
+        - containerPort: 9995
+          name: admin-http
+        readinessProbe:
+          failureThreshold: 7
+          httpGet:
+            path: /ready
+            port: 9995
+        volumeMounts:
+        - mountPath: /var/run/linkerd/tls
+          name: tls
+          readOnly: true
+      serviceAccountName: jaeger-injector
+      volumes:
+      - name: tls
+        secret:
+          secretName: jaeger-injector-k8s-tls
+---
+kind: Service
+apiVersion: v1
+metadata:
+  name: jaeger-injector
+  namespace: linkerd-jaeger
+  labels:
+    linkerd.io/extension: jaeger
+    component: jaeger-injector
+spec:
+  type: ClusterIP
+  selector:
+    linkerd.io/extension: jaeger
+    component: jaeger-injector
+  ports:
+  - name: jaeger-injector
+    port: 443
+    targetPort: jaeger-injector
+---
+###
+### collector RBAC
+###
+kind: ServiceAccount
+apiVersion: v1
+metadata:
+  name: collector
+  namespace: linkerd-jaeger
+---
+###
+### Jaeger Injector RBAC
+###
+kind: ClusterRole
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: linkerd-jaeger-injector
+  labels:
+    linkerd.io/extension: jaeger
+rules:
+- apiGroups: [""]
+  resources: ["namespaces"]
+  verbs: ["get", "list", "watch"]
+---
+kind: ClusterRoleBinding
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: linkerd-jaeger-injector
+  labels:
+    linkerd.io/extension: jaeger
+subjects:
+- kind: ServiceAccount
+  name: jaeger-injector
+  namespace: linkerd-jaeger
+  apiGroup: ""
+roleRef:
+  kind: ClusterRole
+  name: linkerd-jaeger-injector
+  apiGroup: rbac.authorization.k8s.io
+---
+kind: ServiceAccount
+apiVersion: v1
+metadata:
+  name: jaeger-injector
+  namespace: linkerd-jaeger
+---
+kind: Secret
+apiVersion: v1
+metadata:
+  name: jaeger-injector-k8s-tls
+  namespace: linkerd-jaeger
+type: kubernetes.io/tls
+data:
+  tls.crt: dGVzdC13ZWJob29rLWNydC1wZW0=
+  tls.key: dGVzdC13ZWJoaG9vay1rZXktcGVt
+---
+apiVersion: admissionregistration.k8s.io/v1
+kind: MutatingWebhookConfiguration
+metadata:
+  name: linkerd-jaeger-injector-webhook-config
+  labels:
+    linkerd.io/extension: jaeger
+webhooks:
+- name: jaeger-injector.linkerd.io
+  clientConfig:
+    service:
+      name: jaeger-injector
+      namespace: linkerd-jaeger
+      path: "/"
+    caBundle: dGVzdC13ZWJob29rLWNhLWJ1bmRsZQ==
+  failurePolicy: Ignore
+  admissionReviewVersions: ["v1", "v1beta1"]
+  reinvocationPolicy: IfNeeded
+  rules:
+  - operations: [ "CREATE" ]
+    apiGroups: [""]
+    apiVersions: ["v1"]
+    resources: ["pods"]
+  sideEffects: None
+---
+###
+### jaeger RBAC
+###
+kind: ServiceAccount
+apiVersion: v1
+metadata:
+  name: jaeger
+  namespace: linkerd-jaeger
+---
+###
+### Tracing Collector Service
+###
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: collector-config
+  namespace: linkerd-jaeger
+  labels:
+    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: http://jaeger.linkerd-jaeger:14268/api/traces
+          timeout: 5s
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: collector
+  namespace: linkerd-jaeger
+  labels:
+    component: collector
+spec:
+  type: ClusterIP
+  ports:
+  - name: opencensus
+    port: 55678
+    protocol: TCP
+    targetPort: 55678
+  - name: zipkin
+    port: 9411
+    protocol: TCP
+    targetPort: 9411
+  - name: jaeger
+    port: 14268
+    protocol: TCP
+    targetPort: 14268
+  selector:
+    component: collector
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  labels:
+    app.kubernetes.io/name: collector
+    app.kubernetes.io/part-of: Linkerd
+    component: collector
+  name: collector
+  namespace: linkerd-jaeger
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      component: collector
+  minReadySeconds: 5
+  progressDeadlineSeconds: 120
+  template:
+    metadata:
+      annotations:
+        prometheus.io/path: /metrics
+        prometheus.io/port: "8888"
+        prometheus.io/scrape: "true"
+      labels:
+        component: collector
+    spec:
+      nodeSelector:
+        beta.kubernetes.io/os: linux
+      containers:
+      - command:
+        - /occollector_linux
+        - --config=/conf/collector-config.yaml
+        env:
+        - name: GOGC
+          value: "80"
+        image: omnition/opencensus-collector:0.1.11
+        imagePullPolicy: Always
+        livenessProbe:
+          httpGet:
+            path: /
+            port: 13133
+        name: oc-collector
+        ports:
+        - containerPort: 55678
+        - containerPort: 9411
+        - containerPort: 14268
+        - containerPort: 8888
+        readinessProbe:
+          httpGet:
+            path: /
+            port: 13133
+        volumeMounts:
+        - mountPath: /conf
+          name: collector-config-val
+      serviceAccountName: collector
+      volumes:
+      - configMap:
+          items:
+          - key: collector-config
+            path: collector-config.yaml
+          name: collector-config
+        name: collector-config-val
+---
+###
+### Tracing Jaeger Service
+###
+apiVersion: v1
+kind: Service
+metadata:
+  name: jaeger
+  namespace: linkerd-jaeger
+  labels:
+    component: jaeger
+spec:
+  type: ClusterIP
+  selector:
+    component: jaeger
+  ports:
+    - name: collection
+      port: 14268
+    - name: ui
+      port: 16686
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  labels:
+    app.kubernetes.io/name: jaeger
+    app.kubernetes.io/part-of: Linkerd
+    component: jaeger
+  name: jaeger
+  namespace: linkerd-jaeger
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      component: jaeger
+  template:
+    metadata:
+      annotations:
+        config.linkerd.io/proxy-await: "enabled"
+        prometheus.io/path: /metrics
+        prometheus.io/port: "14269"
+        prometheus.io/scrape: "true"
+      labels:
+        component: jaeger
+    spec:
+      nodeSelector:
+        beta.kubernetes.io/os: linux
+      containers:
+      - args:
+        - --query.base-path=/jaeger
+        image: jaegertracing/all-in-one:1.19.2
+        imagePullPolicy: Always
+        name: jaeger
+        ports:
+        - containerPort: 14269
+          name: admin
+        - containerPort: 14268
+          name: collection
+        - containerPort: 16686
+          name: ui
+      dnsPolicy: ClusterFirst
+      serviceAccountName: jaeger
diff --git a/jaeger/cmd/testdata/install_jaeger_disabled.golden b/jaeger/cmd/testdata/install_jaeger_disabled.golden
new file mode 100644
index 000000000..698aaa1c7
--- /dev/null
+++ b/jaeger/cmd/testdata/install_jaeger_disabled.golden
@@ -0,0 +1,286 @@
+---
+kind: Namespace
+apiVersion: v1
+metadata:
+  name: linkerd-jaeger
+  labels:
+    linkerd.io/extension: jaeger
+  annotations:
+    linkerd.io/inject: enabled
+    config.linkerd.io/proxy-await: "enabled"
+---
+###
+### Jaeger Injector
+###
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  labels:
+    linkerd.io/extension: jaeger
+    app.kubernetes.io/name: jaeger-injector
+    app.kubernetes.io/part-of: Linkerd
+    app.kubernetes.io/version: dev-undefined
+    component: jaeger-injector
+  name: jaeger-injector
+  namespace: linkerd-jaeger
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      linkerd.io/extension: jaeger
+      component: jaeger-injector
+  template:
+    metadata:
+      annotations:
+        checksum/config: 83c7853cc6f0083c78ce1a1862e1f02c4eccb7d2fa90d3ffa924ac75d728ad95
+      labels:
+        linkerd.io/extension: jaeger
+        component: jaeger-injector
+    spec:
+      nodeSelector:
+        beta.kubernetes.io/os: linux
+      containers:
+      - args:
+        - -collector-svc-addr=collector.linkerd-jaeger:55678
+        - -collector-svc-account=collector
+        - -log-level=info
+        image: cr.l5d.io/linkerd/jaeger-webhook:dev-undefined
+        imagePullPolicy: IfNotPresent
+        livenessProbe:
+          httpGet:
+            path: /ping
+            port: 9995
+          initialDelaySeconds: 10
+        name: jaeger-injector
+        ports:
+        - containerPort: 8443
+          name: jaeger-injector
+        - containerPort: 9995
+          name: admin-http
+        readinessProbe:
+          failureThreshold: 7
+          httpGet:
+            path: /ready
+            port: 9995
+        volumeMounts:
+        - mountPath: /var/run/linkerd/tls
+          name: tls
+          readOnly: true
+      serviceAccountName: jaeger-injector
+      volumes:
+      - name: tls
+        secret:
+          secretName: jaeger-injector-k8s-tls
+---
+kind: Service
+apiVersion: v1
+metadata:
+  name: jaeger-injector
+  namespace: linkerd-jaeger
+  labels:
+    linkerd.io/extension: jaeger
+    component: jaeger-injector
+spec:
+  type: ClusterIP
+  selector:
+    linkerd.io/extension: jaeger
+    component: jaeger-injector
+  ports:
+  - name: jaeger-injector
+    port: 443
+    targetPort: jaeger-injector
+---
+###
+### collector RBAC
+###
+kind: ServiceAccount
+apiVersion: v1
+metadata:
+  name: collector
+  namespace: linkerd-jaeger
+---
+###
+### Jaeger Injector RBAC
+###
+kind: ClusterRole
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: linkerd-jaeger-injector
+  labels:
+    linkerd.io/extension: jaeger
+rules:
+- apiGroups: [""]
+  resources: ["namespaces"]
+  verbs: ["get", "list", "watch"]
+---
+kind: ClusterRoleBinding
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: linkerd-jaeger-injector
+  labels:
+    linkerd.io/extension: jaeger
+subjects:
+- kind: ServiceAccount
+  name: jaeger-injector
+  namespace: linkerd-jaeger
+  apiGroup: ""
+roleRef:
+  kind: ClusterRole
+  name: linkerd-jaeger-injector
+  apiGroup: rbac.authorization.k8s.io
+---
+kind: ServiceAccount
+apiVersion: v1
+metadata:
+  name: jaeger-injector
+  namespace: linkerd-jaeger
+---
+kind: Secret
+apiVersion: v1
+metadata:
+  name: jaeger-injector-k8s-tls
+  namespace: linkerd-jaeger
+type: kubernetes.io/tls
+data:
+  tls.crt: dGVzdC13ZWJob29rLWNydC1wZW0=
+  tls.key: dGVzdC13ZWJoaG9vay1rZXktcGVt
+---
+apiVersion: admissionregistration.k8s.io/v1
+kind: MutatingWebhookConfiguration
+metadata:
+  name: linkerd-jaeger-injector-webhook-config
+  labels:
+    linkerd.io/extension: jaeger
+webhooks:
+- name: jaeger-injector.linkerd.io
+  clientConfig:
+    service:
+      name: jaeger-injector
+      namespace: linkerd-jaeger
+      path: "/"
+    caBundle: dGVzdC13ZWJob29rLWNhLWJ1bmRsZQ==
+  failurePolicy: Ignore
+  admissionReviewVersions: ["v1", "v1beta1"]
+  reinvocationPolicy: IfNeeded
+  rules:
+  - operations: [ "CREATE" ]
+    apiGroups: [""]
+    apiVersions: ["v1"]
+    resources: ["pods"]
+  sideEffects: None
+---
+###
+### Tracing Collector Service
+###
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: collector-config
+  namespace: linkerd-jaeger
+  labels:
+    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: http://jaeger.linkerd-jaeger:14268/api/traces
+          timeout: 5s
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: collector
+  namespace: linkerd-jaeger
+  labels:
+    component: collector
+spec:
+  type: ClusterIP
+  ports:
+  - name: opencensus
+    port: 55678
+    protocol: TCP
+    targetPort: 55678
+  - name: zipkin
+    port: 9411
+    protocol: TCP
+    targetPort: 9411
+  - name: jaeger
+    port: 14268
+    protocol: TCP
+    targetPort: 14268
+  selector:
+    component: collector
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  labels:
+    app.kubernetes.io/name: collector
+    app.kubernetes.io/part-of: Linkerd
+    component: collector
+  name: collector
+  namespace: linkerd-jaeger
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      component: collector
+  minReadySeconds: 5
+  progressDeadlineSeconds: 120
+  template:
+    metadata:
+      annotations:
+        prometheus.io/path: /metrics
+        prometheus.io/port: "8888"
+        prometheus.io/scrape: "true"
+      labels:
+        component: collector
+    spec:
+      nodeSelector:
+        beta.kubernetes.io/os: linux
+      containers:
+      - command:
+        - /occollector_linux
+        - --config=/conf/collector-config.yaml
+        env:
+        - name: GOGC
+          value: "80"
+        image: omnition/opencensus-collector:0.1.11
+        imagePullPolicy: Always
+        livenessProbe:
+          httpGet:
+            path: /
+            port: 13133
+        name: oc-collector
+        ports:
+        - containerPort: 55678
+        - containerPort: 9411
+        - containerPort: 14268
+        - containerPort: 8888
+        readinessProbe:
+          httpGet:
+            path: /
+            port: 13133
+        volumeMounts:
+        - mountPath: /conf
+          name: collector-config-val
+      serviceAccountName: collector
+      volumes:
+      - configMap:
+          items:
+          - key: collector-config
+            path: collector-config.yaml
+          name: collector-config
+        name: collector-config-val
diff --git a/pkg/healthcheck/healthcheck_test.go b/pkg/healthcheck/healthcheck_test.go
index 49173eaaf..ee3c9ae8f 100644
--- a/pkg/healthcheck/healthcheck_test.go
+++ b/pkg/healthcheck/healthcheck_test.go
@@ -1645,6 +1645,67 @@ func TestValidateDataPlanePods(t *testing.T) {
 			t.Fatalf("Unexpected error: %s", err)
 		}
 	})
+
+	// This test relates to https://github.com/linkerd/linkerd2/issues/6128
+	t.Run("Returns nil if some pods are in the Succeeded phase and their proxies are no longer ready", func(t *testing.T) {
+		pods := []corev1.Pod{
+			{
+				ObjectMeta: metav1.ObjectMeta{Name: "emoji-d9c7866bb-7v74n"},
+				Status: corev1.PodStatus{
+					Phase:  "Succeeded",
+					Reason: "Completed",
+					ContainerStatuses: []corev1.ContainerStatus{
+						{
+							Name:  k8s.ProxyContainerName,
+							Ready: false,
+						},
+					},
+				},
+			},
+			{
+				ObjectMeta: metav1.ObjectMeta{Name: "vote-bot-644b8cb6b4-g8nlr"},
+				Status: corev1.PodStatus{
+					Phase: "Running",
+					ContainerStatuses: []corev1.ContainerStatus{
+						{
+							Name:  k8s.ProxyContainerName,
+							Ready: true,
+						},
+					},
+				},
+			},
+			{
+				ObjectMeta: metav1.ObjectMeta{Name: "voting-65b9fffd77-rlwsd"},
+				Status: corev1.PodStatus{
+					Phase: "Running",
+					ContainerStatuses: []corev1.ContainerStatus{
+						{
+							Name:  k8s.ProxyContainerName,
+							Ready: true,
+						},
+					},
+				},
+			},
+			{
+				ObjectMeta: metav1.ObjectMeta{Name: "web-6cfbccc48-5g8px"},
+				Status: corev1.PodStatus{
+					Phase:  "Succeeded",
+					Reason: "Completed",
+					ContainerStatuses: []corev1.ContainerStatus{
+						{
+							Name:  k8s.ProxyContainerName,
+							Ready: false,
+						},
+					},
+				},
+			},
+		}
+
+		err := validateDataPlanePods(pods, "emojivoto")
+		if err != nil {
+			t.Fatalf("Unexpected error: %s", err)
+		}
+	})
 }
 
 func TestDataPlanePodLabels(t *testing.T) {
diff --git a/test/integration/install_test.go b/test/integration/install_test.go
index 717044810..924dfce02 100644
--- a/test/integration/install_test.go
+++ b/test/integration/install_test.go
@@ -628,9 +628,10 @@ func TestUpgradeHelm(t *testing.T) {
 		"--set", "proxyInjectorProxyResources.cpu.limit=1060m",
 		"--set", "proxyInjectorProxyResources.memory.request=106Mi",
 		"--atomic",
+		"--timeout", "60m",
 		"--wait",
 	}
-	extraArgs, extraVizArgs := helmOverridesEdge(helmTLSCerts)
+	extraArgs, vizArgs := helmOverridesEdge(helmTLSCerts)
 	args = append(args, extraArgs...)
 	if stdout, stderr, err := TestHelper.HelmUpgrade(TestHelper.GetHelmChart(), args...); err != nil {
 		testutil.AnnotatedFatalf(t, "'helm upgrade' command failed",
@@ -638,7 +639,6 @@ func TestUpgradeHelm(t *testing.T) {
 	}
 
 	vizChart := TestHelper.GetLinkerdVizHelmChart()
-	vizArgs := append(extraVizArgs, "--wait")
 	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)
@@ -1025,21 +1025,6 @@ func TestUpgradeTestAppWorksAfterUpgrade(t *testing.T) {
 	}
 }
 
-func TestInstallSP(t *testing.T) {
-	cmd := []string{"diagnostics", "install-sp"}
-
-	out, err := TestHelper.LinkerdRun(cmd...)
-	if err != nil {
-		testutil.AnnotatedFatal(t, "'linkerd install-sp' command failed", err)
-	}
-
-	out, err = TestHelper.KubectlApply(out, TestHelper.GetLinkerdNamespace())
-	if err != nil {
-		testutil.AnnotatedFatalf(t, "'kubectl apply' command failed",
-			"'kubectl apply' command failed\n%s", out)
-	}
-}
-
 func TestDashboard(t *testing.T) {
 	dashboardPort := 52237
 	dashboardURL := fmt.Sprintf("http://localhost:%d", dashboardPort)
diff --git a/test/integration/routes/routes_test.go b/test/integration/routes/routes_test.go
index 167991540..6a03d4198 100644
--- a/test/integration/routes/routes_test.go
+++ b/test/integration/routes/routes_test.go
@@ -20,6 +20,11 @@ func TestMain(m *testing.M) {
 	os.Exit(m.Run())
 }
 
+type testCase struct {
+	s string
+	c int
+}
+
 //////////////////////
 /// TEST EXECUTION ///
 //////////////////////
@@ -35,16 +40,10 @@ func TestRoutes(t *testing.T) {
 		testutil.AnnotatedFatal(t, "'linkerd routes' command failed", err)
 	}
 
-	routeStrings := []struct {
-		s string
-		c int
-	}{
+	routeStrings := []testCase{
 		{"linkerd-destination", 1},
-		{"linkerd-dst", 6},
-		{"linkerd-dst-headless", 3},
 		{"linkerd-identity", 3},
-		{"linkerd-identity-headless", 1},
-		{"POST /io.linkerd.proxy.destination.Destination/Get", 4},
+		{"linkerd-proxy-injector", 2},
 	}
 
 	for _, r := range routeStrings {
@@ -55,6 +54,35 @@ func TestRoutes(t *testing.T) {
 		}
 	}
 
+	// viz routes
+	cmd = []string{"viz", "routes", "--namespace", TestHelper.GetVizNamespace(), "deploy"}
+	out, err = TestHelper.LinkerdRun(cmd...)
+	if err != nil {
+		testutil.AnnotatedFatal(t, "'linkerd routes' command failed", err)
+	}
+
+	vizRouteStrings := []testCase{
+		{"grafana", 13},
+		{"metrics-api", 9},
+		{"tap", 4},
+		{"tap-injector", 2},
+		{"web", 2},
+	}
+
+	if !TestHelper.ExternalPrometheus() {
+		vizRouteStrings = append(vizRouteStrings, testCase{
+			"prometheus",
+			5,
+		})
+	}
+	for _, r := range vizRouteStrings {
+		count := strings.Count(out, r.s)
+		if count != r.c {
+			testutil.AnnotatedFatalf(t, fmt.Sprintf("expected %d occurrences of \"%s\", got %d", r.c, r.s, count),
+				"expected %d occurrences of \"%s\", got %d\n%s", r.c, r.s, count, out)
+		}
+	}
+
 	// smoke test / bb routes
 	prefixedNs := TestHelper.GetTestNamespace("smoke-test")
 	cmd = []string{"viz", "routes", "--namespace", prefixedNs, "deploy"}
diff --git a/testutil/annotations_test.go b/testutil/annotations_test.go
index 359ecee38..9866d56fe 100644
--- a/testutil/annotations_test.go
+++ b/testutil/annotations_test.go
@@ -48,7 +48,7 @@ func TestError(t *testing.T) {
 	Error(&testing.T{}, msg)
 	out := restoreStdout(outC, origStdout)
 
-	if strings.TrimSpace(out) != "::error file=testutil/annotations_test.go,line=48:: - This is an error" {
+	if !strings.HasSuffix(strings.TrimSpace(out), "testutil/annotations_test.go,line=48:: - This is an error") {
 		t.Fatalf("unexpected stdout content: %s", out)
 	}
 }
@@ -63,7 +63,7 @@ func TestAnnotatedErrorf(t *testing.T) {
 	AnnotatedErrorf(&testing.T{}, msgDesc, msgFormat, str)
 	out := restoreStdout(outC, origStdout)
 
-	if strings.TrimSpace(out) != "::error file=testutil/annotations_test.go,line=63:: - This is a generic error" {
+	if !strings.HasSuffix(strings.TrimSpace(out), "testutil/annotations_test.go,line=63:: - This is a generic error") {
 		t.Fatalf("unexpected stdout content: %s", out)
 	}
 }
diff --git a/cli/cmd/testdata/install-sp_default.golden b/viz/charts/linkerd-viz/templates/service-profiles.yaml
similarity index 65%
rename from cli/cmd/testdata/install-sp_default.golden
rename to viz/charts/linkerd-viz/templates/service-profiles.yaml
index d44585000..868d2e0b0 100644
--- a/cli/cmd/testdata/install-sp_default.golden
+++ b/viz/charts/linkerd-viz/templates/service-profiles.yaml
@@ -1,8 +1,9 @@
+---
 apiVersion: linkerd.io/v1alpha2
 kind: ServiceProfile
 metadata:
-  name: linkerd-controller-api.linkerd.svc.cluster.local
-  namespace: linkerd
+  name: metrics-api.{{.Values.namespace}}.svc.{{.Values.clusterDomain}}
+  namespace: {{.Values.namespace}}
 spec:
   routes:
   - name: POST /api/v1/StatSummary
@@ -21,52 +22,25 @@ spec:
     condition:
       method: POST
       pathRegex: /api/v1/ListServices
-  - name: POST /api/v1/Version
-    condition:
-      method: POST
-      pathRegex: /api/v1/Version
   - name: POST /api/v1/SelfCheck
     condition:
       method: POST
       pathRegex: /api/v1/SelfCheck
----
-apiVersion: linkerd.io/v1alpha2
-kind: ServiceProfile
-metadata:
-  name: linkerd-dst.linkerd.svc.cluster.local
-  namespace: linkerd
-spec:
-  routes:
-  - name: POST /io.linkerd.proxy.destination.Destination/Get
-    condition:
-      method: POST
-      pathRegex: /io\.linkerd\.proxy\.destination\.Destination/Get
-  - name: POST /io.linkerd.proxy.destination.Destination/GetProfile
-    condition:
-      method: POST
-      pathRegex: /io\.linkerd\.proxy\.destination\.Destination/GetProfile
----
-apiVersion: linkerd.io/v1alpha2
-kind: ServiceProfile
-metadata:
-  name: linkerd-dst-headless.linkerd.svc.cluster.local
-  namespace: linkerd
-spec:
-  routes:
-  - name: POST /io.linkerd.proxy.destination.Destination/Get
+  - name: POST /api/v1/Gateways
     condition:
       method: POST
-      pathRegex: /io\.linkerd\.proxy\.destination\.Destination/Get
-  - name: POST /io.linkerd.proxy.destination.Destination/GetProfile
+      pathRegex: /api/v1/Gateways
+  - name: POST /api/v1/Edges
     condition:
       method: POST
-      pathRegex: /io\.linkerd\.proxy\.destination\.Destination/GetProfile
+      pathRegex: /api/v1/Edges
+{{ if .Values.prometheus.enabled -}}
 ---
 apiVersion: linkerd.io/v1alpha2
 kind: ServiceProfile
 metadata:
-  name: linkerd-prometheus.linkerd.svc.cluster.local
-  namespace: linkerd
+  name: prometheus.{{.Values.namespace}}.svc.{{.Values.clusterDomain}}
+  namespace: {{.Values.namespace}}
 spec:
   routes:
   - name: POST /api/v1/query
@@ -81,12 +55,14 @@ spec:
     condition:
       method: GET
       pathRegex: /api/v1/series
+{{ end -}}
+{{ if .Values.grafana.enabled -}}
 ---
 apiVersion: linkerd.io/v1alpha2
 kind: ServiceProfile
 metadata:
-  name: linkerd-grafana.linkerd.svc.cluster.local
-  namespace: linkerd
+  name: grafana.{{.Values.namespace}}.svc.{{.Values.clusterDomain}}
+  namespace: {{.Values.namespace}}
 spec:
   routes:
   - name: GET /api/annotations
@@ -133,4 +109,4 @@ spec:
     condition:
       method: GET
       pathRegex: /public/img/.*
----
+{{ end -}}
diff --git a/viz/cmd/testdata/install_default.golden b/viz/cmd/testdata/install_default.golden
index dc5ade487..9931e13a3 100644
--- a/viz/cmd/testdata/install_default.golden
+++ b/viz/cmd/testdata/install_default.golden
@@ -1250,3 +1250,111 @@ spec:
         securityContext:
           runAsUser: 2103
       serviceAccountName: web
+---
+apiVersion: linkerd.io/v1alpha2
+kind: ServiceProfile
+metadata:
+  name: metrics-api.linkerd-viz.svc.cluster.local
+  namespace: linkerd-viz
+spec:
+  routes:
+  - name: POST /api/v1/StatSummary
+    condition:
+      method: POST
+      pathRegex: /api/v1/StatSummary
+  - name: POST /api/v1/TopRoutes
+    condition:
+      method: POST
+      pathRegex: /api/v1/TopRoutes
+  - name: POST /api/v1/ListPods
+    condition:
+      method: POST
+      pathRegex: /api/v1/ListPods
+  - name: POST /api/v1/ListServices
+    condition:
+      method: POST
+      pathRegex: /api/v1/ListServices
+  - name: POST /api/v1/SelfCheck
+    condition:
+      method: POST
+      pathRegex: /api/v1/SelfCheck
+  - name: POST /api/v1/Gateways
+    condition:
+      method: POST
+      pathRegex: /api/v1/Gateways
+  - name: POST /api/v1/Edges
+    condition:
+      method: POST
+      pathRegex: /api/v1/Edges
+---
+apiVersion: linkerd.io/v1alpha2
+kind: ServiceProfile
+metadata:
+  name: prometheus.linkerd-viz.svc.cluster.local
+  namespace: linkerd-viz
+spec:
+  routes:
+  - name: POST /api/v1/query
+    condition:
+      method: POST
+      pathRegex: /api/v1/query
+  - name: GET /api/v1/query_range
+    condition:
+      method: GET
+      pathRegex: /api/v1/query_range
+  - name: GET /api/v1/series
+    condition:
+      method: GET
+      pathRegex: /api/v1/series
+---
+apiVersion: linkerd.io/v1alpha2
+kind: ServiceProfile
+metadata:
+  name: grafana.linkerd-viz.svc.cluster.local
+  namespace: linkerd-viz
+spec:
+  routes:
+  - name: GET /api/annotations
+    condition:
+      method: GET
+      pathRegex: /api/annotations
+  - name: GET /api/dashboards/tags
+    condition:
+      method: GET
+      pathRegex: /api/dashboards/tags
+  - name: GET /api/dashboards/uid/{uid}
+    condition:
+      method: GET
+      pathRegex: /api/dashboards/uid/.*
+  - name: GET /api/dashboard/{dashboard}
+    condition:
+      method: GET
+      pathRegex: /api/dashboard/.*
+  - name: GET /api/datasources/proxy/1/api/v1/series
+    condition:
+      method: GET
+      pathRegex: /api/datasources/proxy/1/api/v1/series
+  - name: GET /api/datasources/proxy/1/api/v1/query_range
+    condition:
+      method: GET
+      pathRegex: /api/datasources/proxy/1/api/v1/query_range
+  - name: GET /api/search
+    condition:
+      method: GET
+      pathRegex: /api/search
+  - name: GET /d/{uid}/{dashboard-name}
+    condition:
+      method: GET
+      pathRegex: /d/[^/]*/.*
+  - name: GET /public/build/{style}.css
+    condition:
+      method: GET
+      pathRegex: /public/build/.*\.css
+  - name: GET /public/fonts/{font}
+    condition:
+      method: GET
+      pathRegex: /public/fonts/.*
+  - name: GET /public/img/{img}
+    condition:
+      method: GET
+      pathRegex: /public/img/.*
diff --git a/viz/cmd/testdata/install_default_overrides.golden b/viz/cmd/testdata/install_default_overrides.golden
index 4686d572e..6d78faa1e 100644
--- a/viz/cmd/testdata/install_default_overrides.golden
+++ b/viz/cmd/testdata/install_default_overrides.golden
@@ -1250,3 +1250,111 @@ spec:
         securityContext:
           runAsUser: 1234
       serviceAccountName: web
+---
+apiVersion: linkerd.io/v1alpha2
+kind: ServiceProfile
+metadata:
+  name: metrics-api.linkerd-viz.svc.cluster.local
+  namespace: linkerd-viz
+spec:
+  routes:
+  - name: POST /api/v1/StatSummary
+    condition:
+      method: POST
+      pathRegex: /api/v1/StatSummary
+  - name: POST /api/v1/TopRoutes
+    condition:
+      method: POST
+      pathRegex: /api/v1/TopRoutes
+  - name: POST /api/v1/ListPods
+    condition:
+      method: POST
+      pathRegex: /api/v1/ListPods
+  - name: POST /api/v1/ListServices
+    condition:
+      method: POST
+      pathRegex: /api/v1/ListServices
+  - name: POST /api/v1/SelfCheck
+    condition:
+      method: POST
+      pathRegex: /api/v1/SelfCheck
+  - name: POST /api/v1/Gateways
+    condition:
+      method: POST
+      pathRegex: /api/v1/Gateways
+  - name: POST /api/v1/Edges
+    condition:
+      method: POST
+      pathRegex: /api/v1/Edges
+---
+apiVersion: linkerd.io/v1alpha2
+kind: ServiceProfile
+metadata:
+  name: prometheus.linkerd-viz.svc.cluster.local
+  namespace: linkerd-viz
+spec:
+  routes:
+  - name: POST /api/v1/query
+    condition:
+      method: POST
+      pathRegex: /api/v1/query
+  - name: GET /api/v1/query_range
+    condition:
+      method: GET
+      pathRegex: /api/v1/query_range
+  - name: GET /api/v1/series
+    condition:
+      method: GET
+      pathRegex: /api/v1/series
+---
+apiVersion: linkerd.io/v1alpha2
+kind: ServiceProfile
+metadata:
+  name: grafana.linkerd-viz.svc.cluster.local
+  namespace: linkerd-viz
+spec:
+  routes:
+  - name: GET /api/annotations
+    condition:
+      method: GET
+      pathRegex: /api/annotations
+  - name: GET /api/dashboards/tags
+    condition:
+      method: GET
+      pathRegex: /api/dashboards/tags
+  - name: GET /api/dashboards/uid/{uid}
+    condition:
+      method: GET
+      pathRegex: /api/dashboards/uid/.*
+  - name: GET /api/dashboard/{dashboard}
+    condition:
+      method: GET
+      pathRegex: /api/dashboard/.*
+  - name: GET /api/datasources/proxy/1/api/v1/series
+    condition:
+      method: GET
+      pathRegex: /api/datasources/proxy/1/api/v1/series
+  - name: GET /api/datasources/proxy/1/api/v1/query_range
+    condition:
+      method: GET
+      pathRegex: /api/datasources/proxy/1/api/v1/query_range
+  - name: GET /api/search
+    condition:
+      method: GET
+      pathRegex: /api/search
+  - name: GET /d/{uid}/{dashboard-name}
+    condition:
+      method: GET
+      pathRegex: /d/[^/]*/.*
+  - name: GET /public/build/{style}.css
+    condition:
+      method: GET
+      pathRegex: /public/build/.*\.css
+  - name: GET /public/fonts/{font}
+    condition:
+      method: GET
+      pathRegex: /public/fonts/.*
+  - name: GET /public/img/{img}
+    condition:
+      method: GET
+      pathRegex: /public/img/.*
diff --git a/viz/cmd/testdata/install_grafana_disabled.golden b/viz/cmd/testdata/install_grafana_disabled.golden
index 632182ae9..42b0fb037 100644
--- a/viz/cmd/testdata/install_grafana_disabled.golden
+++ b/viz/cmd/testdata/install_grafana_disabled.golden
@@ -1068,3 +1068,59 @@ spec:
         securityContext:
           runAsUser: 2103
       serviceAccountName: web
+---
+apiVersion: linkerd.io/v1alpha2
+kind: ServiceProfile
+metadata:
+  name: metrics-api.linkerd-viz.svc.cluster.local
+  namespace: linkerd-viz
+spec:
+  routes:
+  - name: POST /api/v1/StatSummary
+    condition:
+      method: POST
+      pathRegex: /api/v1/StatSummary
+  - name: POST /api/v1/TopRoutes
+    condition:
+      method: POST
+      pathRegex: /api/v1/TopRoutes
+  - name: POST /api/v1/ListPods
+    condition:
+      method: POST
+      pathRegex: /api/v1/ListPods
+  - name: POST /api/v1/ListServices
+    condition:
+      method: POST
+      pathRegex: /api/v1/ListServices
+  - name: POST /api/v1/SelfCheck
+    condition:
+      method: POST
+      pathRegex: /api/v1/SelfCheck
+  - name: POST /api/v1/Gateways
+    condition:
+      method: POST
+      pathRegex: /api/v1/Gateways
+  - name: POST /api/v1/Edges
+    condition:
+      method: POST
+      pathRegex: /api/v1/Edges
+---
+apiVersion: linkerd.io/v1alpha2
+kind: ServiceProfile
+metadata:
+  name: prometheus.linkerd-viz.svc.cluster.local
+  namespace: linkerd-viz
+spec:
+  routes:
+  - name: POST /api/v1/query
+    condition:
+      method: POST
+      pathRegex: /api/v1/query
+  - name: GET /api/v1/query_range
+    condition:
+      method: GET
+      pathRegex: /api/v1/query_range
+  - name: GET /api/v1/series
+    condition:
+      method: GET
+      pathRegex: /api/v1/series
diff --git a/viz/cmd/testdata/install_prometheus_disabled.golden b/viz/cmd/testdata/install_prometheus_disabled.golden
index 986363c8e..fab13bbc8 100644
--- a/viz/cmd/testdata/install_prometheus_disabled.golden
+++ b/viz/cmd/testdata/install_prometheus_disabled.golden
@@ -960,3 +960,91 @@ spec:
         securityContext:
           runAsUser: 2103
       serviceAccountName: web
+---
+apiVersion: linkerd.io/v1alpha2
+kind: ServiceProfile
+metadata:
+  name: metrics-api.linkerd-viz.svc.cluster.local
+  namespace: linkerd-viz
+spec:
+  routes:
+  - name: POST /api/v1/StatSummary
+    condition:
+      method: POST
+      pathRegex: /api/v1/StatSummary
+  - name: POST /api/v1/TopRoutes
+    condition:
+      method: POST
+      pathRegex: /api/v1/TopRoutes
+  - name: POST /api/v1/ListPods
+    condition:
+      method: POST
+      pathRegex: /api/v1/ListPods
+  - name: POST /api/v1/ListServices
+    condition:
+      method: POST
+      pathRegex: /api/v1/ListServices
+  - name: POST /api/v1/SelfCheck
+    condition:
+      method: POST
+      pathRegex: /api/v1/SelfCheck
+  - name: POST /api/v1/Gateways
+    condition:
+      method: POST
+      pathRegex: /api/v1/Gateways
+  - name: POST /api/v1/Edges
+    condition:
+      method: POST
+      pathRegex: /api/v1/Edges
+---
+apiVersion: linkerd.io/v1alpha2
+kind: ServiceProfile
+metadata:
+  name: grafana.linkerd-viz.svc.cluster.local
+  namespace: linkerd-viz
+spec:
+  routes:
+  - name: GET /api/annotations
+    condition:
+      method: GET
+      pathRegex: /api/annotations
+  - name: GET /api/dashboards/tags
+    condition:
+      method: GET
+      pathRegex: /api/dashboards/tags
+  - name: GET /api/dashboards/uid/{uid}
+    condition:
+      method: GET
+      pathRegex: /api/dashboards/uid/.*
+  - name: GET /api/dashboard/{dashboard}
+    condition:
+      method: GET
+      pathRegex: /api/dashboard/.*
+  - name: GET /api/datasources/proxy/1/api/v1/series
+    condition:
+      method: GET
+      pathRegex: /api/datasources/proxy/1/api/v1/series
+  - name: GET /api/datasources/proxy/1/api/v1/query_range
+    condition:
+      method: GET
+      pathRegex: /api/datasources/proxy/1/api/v1/query_range
+  - name: GET /api/search
+    condition:
+      method: GET
+      pathRegex: /api/search
+  - name: GET /d/{uid}/{dashboard-name}
+    condition:
+      method: GET
+      pathRegex: /d/[^/]*/.*
+  - name: GET /public/build/{style}.css
+    condition:
+      method: GET
+      pathRegex: /public/build/.*\.css
+  - name: GET /public/fonts/{font}
+    condition:
+      method: GET
+      pathRegex: /public/fonts/.*
+  - name: GET /public/img/{img}
+    condition:
+      method: GET
+      pathRegex: /public/img/.*
diff --git a/viz/cmd/testdata/install_prometheus_loglevel_from_args.golden b/viz/cmd/testdata/install_prometheus_loglevel_from_args.golden
index e6c8d7e73..011d21981 100644
--- a/viz/cmd/testdata/install_prometheus_loglevel_from_args.golden
+++ b/viz/cmd/testdata/install_prometheus_loglevel_from_args.golden
@@ -1250,3 +1250,111 @@ spec:
         securityContext:
           runAsUser: 2103
       serviceAccountName: web
+---
+apiVersion: linkerd.io/v1alpha2
+kind: ServiceProfile
+metadata:
+  name: metrics-api.linkerd-viz.svc.cluster.local
+  namespace: linkerd-viz
+spec:
+  routes:
+  - name: POST /api/v1/StatSummary
+    condition:
+      method: POST
+      pathRegex: /api/v1/StatSummary
+  - name: POST /api/v1/TopRoutes
+    condition:
+      method: POST
+      pathRegex: /api/v1/TopRoutes
+  - name: POST /api/v1/ListPods
+    condition:
+      method: POST
+      pathRegex: /api/v1/ListPods
+  - name: POST /api/v1/ListServices
+    condition:
+      method: POST
+      pathRegex: /api/v1/ListServices
+  - name: POST /api/v1/SelfCheck
+    condition:
+      method: POST
+      pathRegex: /api/v1/SelfCheck
+  - name: POST /api/v1/Gateways
+    condition:
+      method: POST
+      pathRegex: /api/v1/Gateways
+  - name: POST /api/v1/Edges
+    condition:
+      method: POST
+      pathRegex: /api/v1/Edges
+---
+apiVersion: linkerd.io/v1alpha2
+kind: ServiceProfile
+metadata:
+  name: prometheus.linkerd-viz.svc.cluster.local
+  namespace: linkerd-viz
+spec:
+  routes:
+  - name: POST /api/v1/query
+    condition:
+      method: POST
+      pathRegex: /api/v1/query
+  - name: GET /api/v1/query_range
+    condition:
+      method: GET
+      pathRegex: /api/v1/query_range
+  - name: GET /api/v1/series
+    condition:
+      method: GET
+      pathRegex: /api/v1/series
+---
+apiVersion: linkerd.io/v1alpha2
+kind: ServiceProfile
+metadata:
+  name: grafana.linkerd-viz.svc.cluster.local
+  namespace: linkerd-viz
+spec:
+  routes:
+  - name: GET /api/annotations
+    condition:
+      method: GET
+      pathRegex: /api/annotations
+  - name: GET /api/dashboards/tags
+    condition:
+      method: GET
+      pathRegex: /api/dashboards/tags
+  - name: GET /api/dashboards/uid/{uid}
+    condition:
+      method: GET
+      pathRegex: /api/dashboards/uid/.*
+  - name: GET /api/dashboard/{dashboard}
+    condition:
+      method: GET
+      pathRegex: /api/dashboard/.*
+  - name: GET /api/datasources/proxy/1/api/v1/series
+    condition:
+      method: GET
+      pathRegex: /api/datasources/proxy/1/api/v1/series
+  - name: GET /api/datasources/proxy/1/api/v1/query_range
+    condition:
+      method: GET
+      pathRegex: /api/datasources/proxy/1/api/v1/query_range
+  - name: GET /api/search
+    condition:
+      method: GET
+      pathRegex: /api/search
+  - name: GET /d/{uid}/{dashboard-name}
+    condition:
+      method: GET
+      pathRegex: /d/[^/]*/.*
+  - name: GET /public/build/{style}.css
+    condition:
+      method: GET
+      pathRegex: /public/build/.*\.css
+  - name: GET /public/fonts/{font}
+    condition:
+      method: GET
+      pathRegex: /public/fonts/.*
+  - name: GET /public/img/{img}
+    condition:
+      method: GET
+      pathRegex: /public/img/.*
diff --git a/viz/cmd/testdata/install_proxy_resources.golden b/viz/cmd/testdata/install_proxy_resources.golden
index b3583e71f..a05273913 100644
--- a/viz/cmd/testdata/install_proxy_resources.golden
+++ b/viz/cmd/testdata/install_proxy_resources.golden
@@ -1266,3 +1266,111 @@ spec:
         securityContext:
           runAsUser: 2103
       serviceAccountName: web
+---
+apiVersion: linkerd.io/v1alpha2
+kind: ServiceProfile
+metadata:
+  name: metrics-api.linkerd-viz.svc.cluster.local
+  namespace: linkerd-viz
+spec:
+  routes:
+  - name: POST /api/v1/StatSummary
+    condition:
+      method: POST
+      pathRegex: /api/v1/StatSummary
+  - name: POST /api/v1/TopRoutes
+    condition:
+      method: POST
+      pathRegex: /api/v1/TopRoutes
+  - name: POST /api/v1/ListPods
+    condition:
+      method: POST
+      pathRegex: /api/v1/ListPods
+  - name: POST /api/v1/ListServices
+    condition:
+      method: POST
+      pathRegex: /api/v1/ListServices
+  - name: POST /api/v1/SelfCheck
+    condition:
+      method: POST
+      pathRegex: /api/v1/SelfCheck
+  - name: POST /api/v1/Gateways
+    condition:
+      method: POST
+      pathRegex: /api/v1/Gateways
+  - name: POST /api/v1/Edges
+    condition:
+      method: POST
+      pathRegex: /api/v1/Edges
+---
+apiVersion: linkerd.io/v1alpha2
+kind: ServiceProfile
+metadata:
+  name: prometheus.linkerd-viz.svc.cluster.local
+  namespace: linkerd-viz
+spec:
+  routes:
+  - name: POST /api/v1/query
+    condition:
+      method: POST
+      pathRegex: /api/v1/query
+  - name: GET /api/v1/query_range
+    condition:
+      method: GET
+      pathRegex: /api/v1/query_range
+  - name: GET /api/v1/series
+    condition:
+      method: GET
+      pathRegex: /api/v1/series
+---
+apiVersion: linkerd.io/v1alpha2
+kind: ServiceProfile
+metadata:
+  name: grafana.linkerd-viz.svc.cluster.local
+  namespace: linkerd-viz
+spec:
+  routes:
+  - name: GET /api/annotations
+    condition:
+      method: GET
+      pathRegex: /api/annotations
+  - name: GET /api/dashboards/tags
+    condition:
+      method: GET
+      pathRegex: /api/dashboards/tags
+  - name: GET /api/dashboards/uid/{uid}
+    condition:
+      method: GET
+      pathRegex: /api/dashboards/uid/.*
+  - name: GET /api/dashboard/{dashboard}
+    condition:
+      method: GET
+      pathRegex: /api/dashboard/.*
+  - name: GET /api/datasources/proxy/1/api/v1/series
+    condition:
+      method: GET
+      pathRegex: /api/datasources/proxy/1/api/v1/series
+  - name: GET /api/datasources/proxy/1/api/v1/query_range
+    condition:
+      method: GET
+      pathRegex: /api/datasources/proxy/1/api/v1/query_range
+  - name: GET /api/search
+    condition:
+      method: GET
+      pathRegex: /api/search
+  - name: GET /d/{uid}/{dashboard-name}
+    condition:
+      method: GET
+      pathRegex: /d/[^/]*/.*
+  - name: GET /public/build/{style}.css
+    condition:
+      method: GET
+      pathRegex: /public/build/.*\.css
+  - name: GET /public/fonts/{font}
+    condition:
+      method: GET
+      pathRegex: /public/fonts/.*
+  - name: GET /public/img/{img}
+    condition:
+      method: GET
+      pathRegex: /public/img/.*