Repository: linkerd/linkerd2
Pull request #6091 (2021-05-03 21:35:53)

Add support for server resource aware completion
Linkerd's CLI offers basic shell suggestions on most of its subcommands.
These suggestions are based on hardcoded suggestion lists, for example such
`viz stat` auto suggests a list of all resources types supported by that
command located in `k8s.go`. Although this provides basic suggestions
for k8s manifest resources, prior to this change, there currently is no
way to get auto suggested resources from the k8s cluster linkerd is
installed in.

This change adds a new `CommandCompletion` module that reads arguments
and a substring, and queries the k8s API to determine what suggestions
to provide to the user. The current implementation makes the module
generic enough to query most Kubernetes resourcesand can be used for all
subcommands in the CLI.

This change only applies this behavior to the `stat` command as first
step. Adding auto completion for other commands will be done in a number
of follow up PRs.

To test out the change on this branch:
- Build the CLI binaries on this branch
- install the completion scripts for your shell environment. Running
`linkerd completion -h` should give you more info on how to do that.
- If not installed already, install `linkerd viz`
```
linkerd viz install | k apply -f -
```
- test out completion by typing
```
linkerd viz stat [tab][tab]
```

Part of #5981 

Signed-off-by: Dennis Adjei-Baah <dennis@buoyant.io>


Patch:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 951116263..1121980d5 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -110,7 +110,7 @@ jobs:
         - cni-calico-deep
     needs: [docker_build]
     name: Integration tests (${{ matrix.integration_test }})
-    timeout-minutes: 30
+    timeout-minutes: 60
     runs-on: ubuntu-20.04
     steps:
     - name: Checkout code
diff --git a/ADOPTERS.md b/ADOPTERS.md
index c347b2f0a..d5fcf111d 100644
--- a/ADOPTERS.md
+++ b/ADOPTERS.md
@@ -7,6 +7,7 @@
 - [Apester](https://apester.com)
 - [AppddictionStudio](https://appddicitonstudio.com)
 - [Attest](https://www.askattest.com)
+- [Bink](https://bink.com)
 - [Buoyant](https://buoyant.io)
 - [Candide](https://candidegardening.com)
 - [Celonis](https://celonis.com)
diff --git a/CHANGES.md b/CHANGES.md
index 3a1e53f34..44906445d 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,34 @@
 # Changes
 
+## edge-21.5.1
+
+This edge release adds support for versioned hint URLs in `linkerd check` and
+support for traffic splitting through ServiceProfiles, among other fixes and
+improvements. Additionally, more options have been added to the
+linkerd-multicluster and linkerd-jaeger helm charts.
+
+* Added support for traffic splitting through a ServiceProfile's `dstOverrides`
+  field.
+* Added `nodePorts` option to the multicluster helm chart (thanks @psmit!).
+* Added `nodeSelector` and toleration options to the linkerd-jaeger helm chart
+  (thanks @aatarasoff!).
+* Added versioned hint URLs to the CLI `check` command when encountering an
+  error; each major CLI version will now point to that version's relevant
+  section in the Linkerd troubleshooting page.
+* Fixed an issue in the CLI `check` command where error messages for
+  healthchecks that were being retried would be outputted repeatedly instead of
+  just once.
+* Fixed an issue in the proxy injector where a namespace annotated with opaque
+  ports would overwrite all service annotations.
+* Fixed a regression in the proxy that caused all logs to be output with ANSI
+  control characters, by default logs are output in plaintext now.
+* Simplified proxy internals in order to distinguish endpoint-forwarding logic
+  from the handling of load balanced services.
+* Simplified the ingress-mode outbound proxy by requiring the
+  `l5d-dst-override` header and by failing non-HTTP communication. Proxies
+  running in ingress-mode will not unexpectedly revert to insecure
+  communication as a result.
+
 ## edge-21.4.5
 
 This edge release adds a new output format `short` for `linkerd check` to show a
diff --git a/EXTENSIONS.md b/EXTENSIONS.md
index 333b04221..f1ebd07bc 100644
--- a/EXTENSIONS.md
+++ b/EXTENSIONS.md
@@ -106,7 +106,7 @@ instead of the output format described above.  E.g.
         },
         {
           "description": "linkerd-viz Namespace exists",
-          "hint": "https://linkerd.io/checks/#l5d-viz-ns-exists",
+          "hint": "https://linkerd.io/2/checks/#l5d-viz-ns-exists",
           "error": "could not find the linkerd-viz extension",
           "result": "error"
         }
diff --git a/cli/cmd/completion.go b/cli/cmd/completion.go
index 28fb3f9ca..58d6511a5 100644
--- a/cli/cmd/completion.go
+++ b/cli/cmd/completion.go
@@ -12,11 +12,13 @@ import (
 // enabling linkerd auto completion
 func newCmdCompletion() *cobra.Command {
 	example := `  # bash <= 3.2:
+  # To load shell completion into your current shell session
   source /dev/stdin <<< "$(linkerd completion bash)"
 
   # bash >= 4.0:
   source <(linkerd completion bash)
 
+  # To load shell completion for every shell session
   # bash <= 3.2 on osx:
   brew install bash-completion # ensure you have bash-completion 1.3+
   linkerd completion bash > $(brew --prefix)/etc/bash_completion.d/linkerd
@@ -25,14 +27,22 @@ func newCmdCompletion() *cobra.Command {
   brew install bash-completion@2
   linkerd completion bash > $(brew --prefix)/etc/bash_completion.d/linkerd
 
+  # bash >= 4.0 on linux:
+  linkerd completion bash > /etc/bash_completion.d/linkerd
+
+  # You will need to start a new shell for this setup to take effect.
+
   # zsh:
   # If shell completion is not already enabled in your environment you will need
   # to enable it.  You can execute the following once:
 
-  echo "autoload -U compinit; compinit" >> ~/.zshrc
+  echo "autoload -U compinit && compinit" >> ~/.zshrc
+
+  # create a linkerd 'plugins' folder and add it to your $fpath
+  mkdir $ZSH/plugins/linkerd && echo "fpath=($ZSH/plugins/linkerd $fpath)" >> ~/.zshrc
 
   # To load completions for each session, execute once:
-  linkerd completion zsh > "${fpath[1]}/_linkerd"
+  linkerd completion zsh > "${fpath[1]}/_linkerd" && exec $SHELL
 
   # You will need to start a new shell for this setup to take effect.
 
diff --git a/go.mod b/go.mod
index 310a63b1c..58bbfbf2b 100644
--- a/go.mod
+++ b/go.mod
@@ -16,7 +16,7 @@ require (
 	github.com/ghodss/yaml v1.0.0
 	github.com/go-openapi/spec v0.19.3
 	github.com/golang/protobuf v1.4.3
-	github.com/gorilla/websocket v1.4.0
+	github.com/gorilla/websocket v1.4.2
 	github.com/grantae/certinfo v0.0.0-20170412194111-59d56a35515b
 	github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
 	github.com/imdario/mergo v0.3.8
@@ -36,7 +36,7 @@ require (
 	github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 // indirect
 	github.com/shurcooL/vfsgen v0.0.0-20181202132449-6a9ea43bcacd
 	github.com/sirupsen/logrus v1.7.0
-	github.com/spf13/cobra v1.0.0
+	github.com/spf13/cobra v1.1.3
 	github.com/spf13/pflag v1.0.5
 	go.opencensus.io v0.22.2
 	golang.org/x/net v0.0.0-20200707034311-ab3426394381
diff --git a/go.sum b/go.sum
index eef3ec7d9..33363ca9b 100644
--- a/go.sum
+++ b/go.sum
@@ -10,6 +10,7 @@ cloud.google.com/go v0.51.0 h1:PvKAVQWCtlGUSlZkGW3QLelKaWq7KYv/MW1EboG8bfM=
 cloud.google.com/go v0.51.0/go.mod h1:hWtGJ6gnXH+KgDv+V0zFGDvpi07n3z8ZNj3T1RW0Gcw=
 cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
 cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
+cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
 cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
 cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
 contrib.go.opencensus.io/exporter/ocagent v0.6.0 h1:Z1n6UAyr0QwM284yUuh5Zd8JlvxUGAhFZcgMJkMPrGM=
@@ -103,6 +104,7 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
 github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
+github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
 github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
 github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
@@ -146,6 +148,7 @@ github.com/containernetworking/cni v0.6.1-0.20180218032124-142cde0c766c h1:X6Gxg
 github.com/containernetworking/cni v0.6.1-0.20180218032124-142cde0c766c/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY=
 github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
 github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
 github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
 github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
 github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
@@ -228,6 +231,7 @@ github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
 github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
+github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
@@ -367,8 +371,9 @@ github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z
 github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
 github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
 github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
-github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
 github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
+github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo=
 github.com/grantae/certinfo v0.0.0-20170412194111-59d56a35515b h1:NGgE5ELokSf2tZ/bydyDUKrvd/jP8lrAoPNeBuMOTOk=
 github.com/grantae/certinfo v0.0.0-20170412194111-59d56a35515b/go.mod h1:zT/uzhdQGTqlwTq7Lpbj3JoJQWfPfIJ1tE0OidAmih8=
@@ -380,7 +385,9 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92Bcuy
 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
 github.com/grpc-ecosystem/grpc-gateway v1.14.8 h1:hXClj+iFpmLM8i3lkO6i4Psli4P2qObQuQReiII26U8=
 github.com/grpc-ecosystem/grpc-gateway v1.14.8/go.mod h1:NZE8t6vs6TnwLL/ITkaK8W3ecMLGAbh2jXTclvpiwYo=
+github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
 github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
+github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
 github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
 github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -463,6 +470,7 @@ github.com/linkerd/linkerd2-proxy-init v1.3.11/go.mod h1:iYv3JOgCBpOr6T5qjfE1IeO
 github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc=
 github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
 github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
 github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
 github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
 github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
@@ -667,8 +675,9 @@ github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU
 github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
 github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
 github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
-github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=
 github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
+github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M=
+github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo=
 github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
 github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
@@ -678,6 +687,7 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
 github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
+github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
 github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
 github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
 github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
@@ -690,6 +700,7 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
 github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
 github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
@@ -762,6 +773,7 @@ golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
 golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
+golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
 golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
@@ -907,6 +919,7 @@ golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3/go.mod h1:b+2E5dAYhXwXZwtn
 golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
@@ -923,6 +936,7 @@ google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEt
 google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
 google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
 google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
 google.golang.org/api v0.15.0 h1:yzlyyDW/J0w8yNFJIhiAJy4kq74S+1DOLdawELNxFMA=
 google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
@@ -943,6 +957,7 @@ google.golang.org/genproto v0.0.0-20190716160619-c506a9f90610/go.mod h1:DMBHOl98
 google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
+google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
 google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
 google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY=
@@ -991,6 +1006,7 @@ gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKW
 gopkg.in/gorp.v1 v1.7.2/go.mod h1:Wo3h+DBQZIxATwftsglhdD/62zRFPhGhTiu5jUJmCaw=
 gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
 gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
 gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
@@ -1001,8 +1017,9 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
 gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
diff --git a/jaeger/charts/linkerd-jaeger/README.md b/jaeger/charts/linkerd-jaeger/README.md
index bc456a063..437f46036 100644
--- a/jaeger/charts/linkerd-jaeger/README.md
+++ b/jaeger/charts/linkerd-jaeger/README.md
@@ -75,12 +75,18 @@ Kubernetes: `>=1.16.0-0`
 | collector.image.pullPolicy | string | `"Always"` |  |
 | collector.image.version | string | `"0.1.11"` |  |
 | collector.jaegerAddr | string | `nil` | address of the jaeger backend to send traces to |
+| collector.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.image.name | string | `"jaegertracing/all-in-one"` |  |
 | jaeger.image.pullPolicy | string | `"Always"` |  |
 | jaeger.image.version | string | `"1.19.2"` |  |
+| jaeger.nodeSelector | object | `{"beta.kubernetes.io/os":"linux"}` | NodeSelector section, See the [K8S documentation](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector) for more information |
+| jaeger.tolerations | string | `nil` | Tolerations section, See the [K8S documentation](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/) for more information |
 | linkerdVersion | string | `"linkerdVersionValue"` |  |
 | namespace | string | `"linkerd-jaeger"` |  |
+| nodeSelector | object | `{"beta.kubernetes.io/os":"linux"}` | Default nodeSelector section, See the [K8S documentation](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector) for more information |
+| tolerations | string | `nil` | Default tolerations section, See the [K8S documentation](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/) for more information |
 | webhook.caBundle | string | `""` | if empty, Helm will auto-generate this field, unless externalSecret is set to true. |
 | webhook.collectorSvcAccount | string | `"collector"` | service account associated with the collector instance |
 | webhook.collectorSvcAddr | string | `"collector.linkerd-jaeger:55678"` | collector service address for the proxies to send trace data. Points by default to the the linkerd-jaeger collector |
@@ -93,7 +99,9 @@ Kubernetes: `>=1.16.0-0`
 | webhook.keyPEM | string | `""` |  |
 | webhook.logLevel | string | `"info"` |  |
 | webhook.namespaceSelector | string | `nil` |  |
+| webhook.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 |
 | webhook.objectSelector | string | `nil` |  |
+| webhook.tolerations | string | `nil` | 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/jaeger/charts/linkerd-jaeger/templates/jaeger-injector.yaml b/jaeger/charts/linkerd-jaeger/templates/jaeger-injector.yaml
index ae12db910..9f9e0f393 100644
--- a/jaeger/charts/linkerd-jaeger/templates/jaeger-injector.yaml
+++ b/jaeger/charts/linkerd-jaeger/templates/jaeger-injector.yaml
@@ -27,6 +27,10 @@ spec:
         linkerd.io/extension: jaeger
         component: jaeger-injector
     spec:
+      {{- if .Values.webhook.tolerations -}}
+      {{- include "linkerd.tolerations" (dict "Values" .Values.webhook) | nindent 6 }}
+      {{- end -}}
+      {{- include "linkerd.node-selector" (dict "Values" .Values.webhook) | nindent 6 }}
       containers:
       - args:
         - -collector-svc-addr={{.Values.webhook.collectorSvcAddr}}
diff --git a/jaeger/charts/linkerd-jaeger/templates/tracing.yaml b/jaeger/charts/linkerd-jaeger/templates/tracing.yaml
index cdfb4e506..b16bcdc89 100644
--- a/jaeger/charts/linkerd-jaeger/templates/tracing.yaml
+++ b/jaeger/charts/linkerd-jaeger/templates/tracing.yaml
@@ -78,6 +78,10 @@ spec:
       labels:
         component: collector
     spec:
+      {{- if .Values.collector.tolerations -}}
+      {{- include "linkerd.tolerations" (dict "Values" .Values.collector) | nindent 6 }}
+      {{- end -}}
+      {{- include "linkerd.node-selector" (dict "Values" .Values.collector) | nindent 6 }}
       containers:
       - command:
         - /occollector_linux
@@ -160,6 +164,10 @@ spec:
       labels:
         component: jaeger
     spec:
+      {{- if .Values.jaeger.tolerations -}}
+      {{- include "linkerd.tolerations" (dict "Values" .Values.jaeger) | nindent 6 }}
+      {{- end -}}
+      {{- include "linkerd.node-selector" (dict "Values" .Values.jaeger) | nindent 6 }}
       containers:
       - args:
         - --query.base-path=/jaeger
diff --git a/jaeger/charts/linkerd-jaeger/values.yaml b/jaeger/charts/linkerd-jaeger/values.yaml
index 3b9b7a73c..0b8e48f43 100644
--- a/jaeger/charts/linkerd-jaeger/values.yaml
+++ b/jaeger/charts/linkerd-jaeger/values.yaml
@@ -4,21 +4,45 @@ installNamespace: true
 
 namespace: linkerd-jaeger
 
+# -- Default nodeSelector section, See the
+# [K8S documentation](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector) for more information
+nodeSelector: &default_node_selector
+  beta.kubernetes.io/os: linux
+
+# -- Default tolerations section, See the
+# [K8S documentation](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/)
+# for more information
+tolerations: &default_tolerations
+
 collector:
-  image: 
+  image:
     name: omnition/opencensus-collector
     version: 0.1.11
     pullPolicy: Always
-  # 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
 
   # -- address of the jaeger backend to send traces to
   jaegerAddr:
 
 jaeger:
-  image: 
+  image:
     name: jaegertracing/all-in-one
     version: 1.19.2
     pullPolicy: Always
+  # -- NodeSelector section, See the
+  # [K8S documentation](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector) for more information
+  nodeSelector: *default_node_selector
+  # -- Tolerations section, See the
+  # [K8S documentation](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/)
+  # for more information
+  tolerations: *default_tolerations
 
 linkerdVersion: &linkerd_version linkerdVersionValue
 
@@ -39,7 +63,7 @@ webhook:
   collectorSvcAccount: collector
 
   failurePolicy: Ignore
-  image: 
+  image:
     name: cr.l5d.io/linkerd/jaeger-webhook
     version: *linkerd_version
     pullPolicy: IfNotPresent
@@ -53,3 +77,11 @@ webhook:
   objectSelector:
     #matchLabels:
     #  foo: bar
+
+  # -- 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/multicluster/charts/linkerd-multicluster-link/README.md b/multicluster/charts/linkerd-multicluster-link/README.md
index 1b46aca68..b74c864b8 100644
--- a/multicluster/charts/linkerd-multicluster-link/README.md
+++ b/multicluster/charts/linkerd-multicluster-link/README.md
@@ -23,11 +23,11 @@ Kubernetes: `>=1.16.0-0`
 |-----|------|---------|-------------|
 | controllerImage | string | `"cr.l5d.io/linkerd/controller"` | Docker image for the Service mirror component (uses the Linkerd controller image) |
 | controllerImageVersion | string | `"linkerdVersionValue"` | Tag for the Service Mirror container Docker image |
-| gatewayProbePort | int | `4191` | The port used for liveliness probing  |
-| logLevel | string | `"info"` | Log level for the Multicluster components  |
-| namespace | string | `"linkerd-multicluster"` | Service Mirror component namespace  |
+| gateway.probe.port | int | `4191` | The port used for liveliness probing |
+| logLevel | string | `"info"` | Log level for the Multicluster components |
+| namespace | string | `"linkerd-multicluster"` | Service Mirror component namespace |
 | serviceMirrorRetryLimit | int | `3` | Number of times update from the remote cluster is allowed to be requeued (retried) |
-| serviceMirrorUID | int | `2103` | User id under which the Service Mirror shall be ran  |
+| serviceMirrorUID | int | `2103` | User id under which the Service Mirror shall be ran |
 
 ----------------------------------------------
 Autogenerated from chart metadata using [helm-docs v1.4.0](https://github.com/norwoodj/helm-docs/releases/v1.4.0)
diff --git a/multicluster/charts/linkerd-multicluster-link/templates/gateway-mirror.yaml b/multicluster/charts/linkerd-multicluster-link/templates/gateway-mirror.yaml
index 7806d3e47..3aa6b6f1e 100644
--- a/multicluster/charts/linkerd-multicluster-link/templates/gateway-mirror.yaml
+++ b/multicluster/charts/linkerd-multicluster-link/templates/gateway-mirror.yaml
@@ -10,5 +10,5 @@ metadata:
 spec:
   ports:
   - name: mc-probe
-    port: {{.Values.gatewayProbePort}}
+    port: {{.Values.gateway.probe.port}}
     protocol: TCP
diff --git a/multicluster/charts/linkerd-multicluster-link/values.yaml b/multicluster/charts/linkerd-multicluster-link/values.yaml
index 654bd884c..bb6b5e105 100644
--- a/multicluster/charts/linkerd-multicluster-link/values.yaml
+++ b/multicluster/charts/linkerd-multicluster-link/values.yaml
@@ -3,14 +3,16 @@
 controllerImage: cr.l5d.io/linkerd/controller
 # -- Tag for the Service Mirror container Docker image
 controllerImageVersion: linkerdVersionValue
-# -- The port used for liveliness probing 
-gatewayProbePort: 4191
-# -- Service Mirror component namespace 
+gateway:
+  probe:
+    # -- The port used for liveliness probing
+    port: 4191
+# -- Service Mirror component namespace
 namespace: linkerd-multicluster
-# -- Log level for the Multicluster components 
+# -- Log level for the Multicluster components
 logLevel: info
 # -- Number of times update from the remote cluster is allowed to be requeued
 # (retried)
 serviceMirrorRetryLimit: 3
-# -- User id under which the Service Mirror shall be ran 
+# -- User id under which the Service Mirror shall be ran
 serviceMirrorUID: 2103
diff --git a/multicluster/charts/linkerd-multicluster/README.md b/multicluster/charts/linkerd-multicluster/README.md
index 5904aadd3..6c78298bf 100644
--- a/multicluster/charts/linkerd-multicluster/README.md
+++ b/multicluster/charts/linkerd-multicluster/README.md
@@ -71,13 +71,15 @@ Kubernetes: `>=1.16.0-0`
 
 | Key | Type | Default | Description |
 |-----|------|---------|-------------|
-| gateway | bool | `true` | If the gateway component should be installed |
-| gatewayName | string | `"linkerd-gateway"` | The name of the gateway that will be installed |
-| gatewayPort | int | `4143` | The port on which all the gateway will accept incoming traffic |
-| gatewayProbePath | string | `"/ready"` | The path that will be used by remote clusters for determining whether the gateway is alive |
-| gatewayProbePort | int | `4191` | The port used for liveliness probing |
-| gatewayProbeSeconds | int | `3` | The interval (in seconds) between liveness probes |
-| gatewayServiceType | string | `"LoadBalancer"` | Service Type of gateway Service |
+| gateway.enabled | bool | `true` | If the gateway component should be installed |
+| gateway.loadBalancerIP | string | `""` | Set loadBalancerIP on gateway service |
+| gateway.name | string | `"linkerd-gateway"` | The name of the gateway that will be installed |
+| gateway.port | int | `4143` | The port on which all the gateway will accept incoming traffic |
+| gateway.probe.path | string | `"/ready"` | The path that will be used by remote clusters for determining whether the gateway is alive |
+| gateway.probe.port | int | `4191` | The port used for liveliness probing |
+| gateway.probe.seconds | int | `3` |  |
+| gateway.serviceAnnotations | object | `{}` | Annotations to add to the gateway service |
+| gateway.serviceType | string | `"LoadBalancer"` | Service Type of gateway Service |
 | identityTrustDomain | string | `"cluster.local"` | Identity Trust Domain of the certificate authority |
 | installNamespace | bool | `true` | If the namespace should be installed |
 | linkerdNamespace | string | `"linkerd"` | Namespace of linkerd installation |
diff --git a/multicluster/charts/linkerd-multicluster/templates/gateway.yaml b/multicluster/charts/linkerd-multicluster/templates/gateway.yaml
index da9fb62fe..17395d87f 100644
--- a/multicluster/charts/linkerd-multicluster/templates/gateway.yaml
+++ b/multicluster/charts/linkerd-multicluster/templates/gateway.yaml
@@ -1,4 +1,4 @@
-{{if .Values.gateway -}}
+{{if .Values.gateway.enabled -}}
 ---
 apiVersion: apps/v1
 kind: Deployment
@@ -10,64 +10,71 @@ metadata:
     app.kubernetes.io/part-of: Linkerd
     app.kubernetes.io/version: {{.Values.linkerdVersion}}
     linkerd.io/control-plane-component: gateway
-    app: {{.Values.gatewayName}}
+    app: {{.Values.gateway.name}}
     linkerd.io/extension: multicluster
-  name: {{.Values.gatewayName}}
+  name: {{.Values.gateway.name}}
   namespace: {{.Values.namespace}}
 spec:
   replicas: 1
   selector:
     matchLabels:
-      app: {{.Values.gatewayName}}
+      app: {{.Values.gateway.name}}
   template:
     metadata:
       annotations:
         {{ include "partials.annotations.created-by" . }}
         linkerd.io/inject: enabled
-        config.linkerd.io/proxy-require-identity-inbound-ports: "{{.Values.gatewayProbePort}},{{.Values.gatewayPort}}"
+        config.linkerd.io/proxy-require-identity-inbound-ports: "{{.Values.gateway.probe.port}},{{.Values.gateway.port}}"
         config.linkerd.io/enable-gateway: "true"
       labels:
-        app: {{.Values.gatewayName}}
+        app: {{.Values.gateway.name}}
     spec:
       containers:
         - name: pause
           image: gcr.io/google_containers/pause
-      serviceAccountName: {{.Values.gatewayName}}
+      serviceAccountName: {{.Values.gateway.name}}
 ---
 apiVersion: v1
 kind: Service
 metadata:
-  name: {{.Values.gatewayName}}
+  name: {{.Values.gateway.name}}
   namespace: {{.Values.namespace}}
   labels:
     linkerd.io/extension: multicluster
   annotations:
-    mirror.linkerd.io/gateway-identity: {{.Values.gatewayName}}.{{.Values.namespace}}.serviceaccount.identity.{{.Values.linkerdNamespace}}.{{.Values.identityTrustDomain}}
-    mirror.linkerd.io/probe-period: "{{.Values.gatewayProbeSeconds}}"
-    mirror.linkerd.io/probe-path: {{.Values.gatewayProbePath}}
+    mirror.linkerd.io/gateway-identity: {{.Values.gateway.name}}.{{.Values.namespace}}.serviceaccount.identity.{{.Values.linkerdNamespace}}.{{.Values.identityTrustDomain}}
+    mirror.linkerd.io/probe-period: "{{.Values.gateway.probe.seconds}}"
+    mirror.linkerd.io/probe-path: {{.Values.gateway.probe.path}}
     mirror.linkerd.io/multicluster-gateway: "true"
     linkerd.io/control-plane-component: gateway
     {{ include "partials.annotations.created-by" . }}
-    {{- with .Values.gatewayServiceAnnotations }}{{ toYaml . | trim | nindent 4 }}{{- end }}
+    {{- with .Values.gateway.serviceAnnotations }}{{ toYaml . | trim | nindent 4 }}{{- end }}
 spec:
   ports:
+  {{- $setNodePorts := (or (eq .Values.gateway.serviceType "NodePort") (eq .Values.gateway.serviceType "LoadBalancer")) }}
   - name: mc-gateway
-    port: {{.Values.gatewayPort}}
+    port: {{.Values.gateway.port}}
     protocol: TCP
+  {{- if (and $setNodePorts .Values.gateway.nodePort) }}
+    nodePort: {{ .Values.gateway.nodePort }}
+  {{- end }}
   - name: mc-probe
-    port: {{.Values.gatewayProbePort}}
+    port: {{.Values.gateway.probe.port}}
     protocol: TCP
+  {{- if (and $setNodePorts .Values.gateway.probe.nodePort) }}
+    nodePort: {{ .Values.gateway.probe.nodePort }}
+  {{- end }}
   selector:
-    app: {{.Values.gatewayName}}
-  type: {{ .Values.gatewayServiceType }}
-{{- if .Values.loadBalancerIP }}
-  loadBalancerIP: {{ .Values.loadBalancerIP }}
+    app: {{.Values.gateway.name}}
+  type: {{ .Values.gateway.serviceType }}
+{{- if .Values.gateway.loadBalancerIP }}
+  loadBalancerIP: {{ .Values.gateway.loadBalancerIP }}
 {{- end }}
 ---
 kind: ServiceAccount
 apiVersion: v1
 metadata:
-  name: {{.Values.gatewayName}}
+  name: {{.Values.gateway.name}}
   namespace: {{.Values.namespace}}
   labels:
     linkerd.io/extension: multicluster
diff --git a/multicluster/charts/linkerd-multicluster/values.yaml b/multicluster/charts/linkerd-multicluster/values.yaml
index 5b5a37faa..2a8835853 100644
--- a/multicluster/charts/linkerd-multicluster/values.yaml
+++ b/multicluster/charts/linkerd-multicluster/values.yaml
@@ -1,16 +1,29 @@
-# -- If the gateway component should be installed
-gateway: true
-# -- The name of the gateway that will be installed
-gatewayName: linkerd-gateway
-# -- The port on which all the gateway will accept incoming traffic
-gatewayPort: 4143
-# -- The path that will be used by remote clusters for determining whether the
-# gateway is alive
-gatewayProbePath: /ready
-# -- The port used for liveliness probing
-gatewayProbePort: 4191
-# -- The interval (in seconds) between liveness probes
-gatewayProbeSeconds: 3
+gateway:
+  # -- If the gateway component should be installed
+  enabled: true
+  # -- The name of the gateway that will be installed
+  name: linkerd-gateway
+  # -- The port on which all the gateway will accept incoming traffic
+  port: 4143
+  # -- Service Type of gateway Service
+  serviceType: LoadBalancer
+  # nodePort -- Set the gateway nodePort (for LoadBalancer or NodePort) to a specific value
+  # nodePort:
+  probe:
+    # -- The path that will be used by remote clusters for determining whether the
+    # gateway is alive
+    path: /ready
+    # -- The port used for liveliness probing
+    port: 4191
+    # nodePort -- Set the probe nodePort (for LoadBalancer or NodePort) to a specific value
+    # nodePort:
+    # -- The interval (in seconds) between liveness probes
+    seconds: 3
+  # -- Annotations to add to the gateway service
+  serviceAnnotations: {}
+  # -- Set loadBalancerIP on gateway service
+  loadBalancerIP: ""
+
 # -- If the namespace should be installed
 installNamespace: true
 # -- Control plane version
@@ -24,8 +37,6 @@ remoteMirrorServiceAccount: true
 # -- The name of the service account used to allow remote clusters to mirror
 # local services
 remoteMirrorServiceAccountName: linkerd-service-mirror-remote-access-default
-# -- Service Type of gateway Service
-gatewayServiceType: LoadBalancer
 # -- Namespace of linkerd installation
 linkerdNamespace: linkerd
 # -- Identity Trust Domain of the certificate authority
diff --git a/multicluster/cmd/allow.go b/multicluster/cmd/allow.go
index 41506d611..a021431dc 100644
--- a/multicluster/cmd/allow.go
+++ b/multicluster/cmd/allow.go
@@ -108,7 +108,7 @@ func buildMulticlusterAllowValues(ctx context.Context, opts *allowOptions) (*mcc
 
 	defaults.Namespace = opts.namespace
 	defaults.LinkerdVersion = version.Version
-	defaults.Gateway = false
+	defaults.Gateway.Enabled = false
 	defaults.ServiceMirror = false
 	defaults.RemoteMirrorServiceAccount = true
 	defaults.RemoteMirrorServiceAccountName = opts.serviceAccountName
diff --git a/multicluster/cmd/gateways.go b/multicluster/cmd/gateways.go
index 4002bb687..e7f373c08 100644
--- a/multicluster/cmd/gateways.go
+++ b/multicluster/cmd/gateways.go
@@ -166,6 +166,9 @@ func gatewaysRowToTableRow(row *pb.GatewaysTable_Row) []string {
 func extractGatewayPort(gateway *corev1.Service) (uint32, error) {
 	for _, port := range gateway.Spec.Ports {
 		if port.Name == k8s.GatewayPortName {
+			if gateway.Spec.Type == "NodePort" {
+				return uint32(port.NodePort), nil
+			}
 			return uint32(port.Port), nil
 		}
 	}
diff --git a/multicluster/cmd/install.go b/multicluster/cmd/install.go
index 0893a161d..371472ae6 100644
--- a/multicluster/cmd/install.go
+++ b/multicluster/cmd/install.go
@@ -30,13 +30,9 @@ import (
 
 type (
 	multiclusterInstallOptions struct {
-		gateway                 bool
-		gatewayPort             uint32
-		gatewayProbeSeconds     uint32
-		gatewayProbePort        uint32
+		gateway                 multicluster.Gateway
 		namespace               string
 		remoteMirrorCredentials bool
-		gatewayServiceType      string
 	}
 )
 
@@ -56,7 +52,7 @@ func newMulticlusterInstallCommand() *cobra.Command {
 		Args:  cobra.NoArgs,
 		Example: `  # Default install.
   linkerd multicluster install | kubectl apply -f -
-  
+
 The installation can be configured by using the --set, --values, --set-string and --set-file flags.
 A full list of configurable values can be found at https://github.com/linkerd/linkerd2/blob/main/multicluster/charts/linkerd-multicluster/README.md
   `,
@@ -156,12 +152,12 @@ A full list of configurable values can be found at https://github.com/linkerd/li
 
 	flags.AddValueOptionsFlags(cmd.Flags(), &valuesOptions)
 	cmd.Flags().StringVar(&options.namespace, "namespace", options.namespace, "The namespace in which the multicluster add-on is to be installed. Must not be the control plane namespace. ")
-	cmd.Flags().BoolVar(&options.gateway, "gateway", options.gateway, "If the gateway component should be installed")
-	cmd.Flags().Uint32Var(&options.gatewayPort, "gateway-port", options.gatewayPort, "The port on the gateway used for all incoming traffic")
-	cmd.Flags().Uint32Var(&options.gatewayProbeSeconds, "gateway-probe-seconds", options.gatewayProbeSeconds, "The interval at which the gateway will be checked for being alive in seconds")
-	cmd.Flags().Uint32Var(&options.gatewayProbePort, "gateway-probe-port", options.gatewayProbePort, "The liveness check port of the gateway")
+	cmd.Flags().BoolVar(&options.gateway.Enabled, "gateway", options.gateway.Enabled, "If the gateway component should be installed")
+	cmd.Flags().Uint32Var(&options.gateway.Port, "gateway-port", options.gateway.Port, "The port on the gateway used for all incoming traffic")
+	cmd.Flags().Uint32Var(&options.gateway.Probe.Seconds, "gateway-probe-seconds", options.gateway.Probe.Seconds, "The interval at which the gateway will be checked for being alive in seconds")
+	cmd.Flags().Uint32Var(&options.gateway.Probe.Port, "gateway-probe-port", options.gateway.Probe.Port, "The liveness check port of the gateway")
 	cmd.Flags().BoolVar(&options.remoteMirrorCredentials, "service-mirror-credentials", options.remoteMirrorCredentials, "Whether to install the service account which can be used by service mirror components in source clusters to discover exported services")
-	cmd.Flags().StringVar(&options.gatewayServiceType, "gateway-service-type", options.gatewayServiceType, "Overwrite Service type for gateway service")
+	cmd.Flags().StringVar(&options.gateway.ServiceType, "gateway-service-type", options.gateway.ServiceType, "Overwrite Service type for gateway service")
 	cmd.Flags().DurationVar(&wait, "wait", 300*time.Second, "Wait for core control-plane components to be available")
 
 	// Hide developer focused flags in release builds.
@@ -185,13 +181,9 @@ func newMulticlusterInstallOptionsWithDefault() (*multiclusterInstallOptions, er
 	}
 
 	return &multiclusterInstallOptions{
-		gateway:                 defaults.Gateway,
-		gatewayPort:             defaults.GatewayPort,
-		gatewayProbeSeconds:     defaults.GatewayProbeSeconds,
-		gatewayProbePort:        defaults.GatewayProbePort,
+		gateway:                 *defaults.Gateway,
 		namespace:               defaults.Namespace,
 		remoteMirrorCredentials: true,
-		gatewayServiceType:      defaults.GatewayServiceType,
 	}, nil
 }
 
@@ -219,16 +211,16 @@ func buildMulticlusterInstallValues(ctx context.Context, opts *multiclusterInsta
 	}
 
 	defaults.Namespace = opts.namespace
-	defaults.Gateway = opts.gateway
-	defaults.GatewayPort = opts.gatewayPort
-	defaults.GatewayProbeSeconds = opts.gatewayProbeSeconds
-	defaults.GatewayProbePort = opts.gatewayProbePort
+	defaults.Gateway.Enabled = opts.gateway.Enabled
+	defaults.Gateway.Port = opts.gateway.Port
+	defaults.Gateway.Probe.Seconds = opts.gateway.Probe.Seconds
+	defaults.Gateway.Probe.Port = opts.gateway.Probe.Port
 	defaults.IdentityTrustDomain = values.IdentityTrustDomain
 	defaults.LinkerdNamespace = controlPlaneNamespace
 	defaults.ProxyOutboundPort = uint32(values.Proxy.Ports.Outbound)
 	defaults.LinkerdVersion = version.Version
 	defaults.RemoteMirrorServiceAccount = opts.remoteMirrorCredentials
-	defaults.GatewayServiceType = opts.gatewayServiceType
+	defaults.Gateway.ServiceType = opts.gateway.ServiceType
 
 	return defaults, nil
 }
diff --git a/multicluster/values/values.go b/multicluster/values/values.go
index 22d40c1d3..79dbac31c 100644
--- a/multicluster/values/values.go
+++ b/multicluster/values/values.go
@@ -18,29 +18,43 @@ const (
 
 // Values contains the top-level elements in the Helm charts
 type Values struct {
-	CliVersion                     string `json:"cliVersion"`
-	ControllerImage                string `json:"controllerImage"`
-	ControllerImageVersion         string `json:"controllerImageVersion"`
-	Gateway                        bool   `json:"gateway"`
-	GatewayName                    string `json:"gatewayName"`
-	GatewayPort                    uint32 `json:"gatewayPort"`
-	GatewayProbePath               string `json:"gatewayProbePath"`
-	GatewayProbePort               uint32 `json:"gatewayProbePort"`
-	GatewayProbeSeconds            uint32 `json:"gatewayProbeSeconds"`
-	IdentityTrustDomain            string `json:"identityTrustDomain"`
-	InstallNamespace               bool   `json:"installNamespace"`
-	LinkerdNamespace               string `json:"linkerdNamespace"`
-	LinkerdVersion                 string `json:"linkerdVersion"`
-	Namespace                      string `json:"namespace"`
-	ProxyOutboundPort              uint32 `json:"proxyOutboundPort"`
-	ServiceMirror                  bool   `json:"serviceMirror"`
-	LogLevel                       string `json:"logLevel"`
-	ServiceMirrorRetryLimit        uint32 `json:"serviceMirrorRetryLimit"`
-	ServiceMirrorUID               int64  `json:"serviceMirrorUID"`
-	RemoteMirrorServiceAccount     bool   `json:"remoteMirrorServiceAccount"`
-	RemoteMirrorServiceAccountName string `json:"remoteMirrorServiceAccountName"`
-	TargetClusterName              string `json:"targetClusterName"`
-	GatewayServiceType             string `json:"gatewayServiceType"`
+	CliVersion                     string   `json:"cliVersion"`
+	ControllerImage                string   `json:"controllerImage"`
+	ControllerImageVersion         string   `json:"controllerImageVersion"`
+	Gateway                        *Gateway `json:"gateway"`
+	IdentityTrustDomain            string   `json:"identityTrustDomain"`
+	InstallNamespace               bool     `json:"installNamespace"`
+	LinkerdNamespace               string   `json:"linkerdNamespace"`
+	LinkerdVersion                 string   `json:"linkerdVersion"`
+	Namespace                      string   `json:"namespace"`
+	ProxyOutboundPort              uint32   `json:"proxyOutboundPort"`
+	ServiceMirror                  bool     `json:"serviceMirror"`
+	LogLevel                       string   `json:"logLevel"`
+	ServiceMirrorRetryLimit        uint32   `json:"serviceMirrorRetryLimit"`
+	ServiceMirrorUID               int64    `json:"serviceMirrorUID"`
+	RemoteMirrorServiceAccount     bool     `json:"remoteMirrorServiceAccount"`
+	RemoteMirrorServiceAccountName string   `json:"remoteMirrorServiceAccountName"`
+	TargetClusterName              string   `json:"targetClusterName"`
+}
+
+// Gateway contains all opttions related to the Gateway Service
+type Gateway struct {
+	Enabled            bool              `json:"enabled"`
+	Name               string            `json:"name"`
+	Port               uint32            `json:"port"`
+	NodePort           uint32            `json:"nodePort"`
+	ServiceType        string            `json:"serviceType"`
+	Probe              *Probe            `json:"probe"`
+	ServiceAnnotations map[string]string `json:"serviceAnnotations"`
+	LoadBalancerIP     string            `json:"loadBalancerIP"`
+}
+
+// Probe contains all options for the Probe Service
+type Probe struct {
+	Path     string `json:"path"`
+	Port     uint32 `json:"port"`
+	NodePort uint32 `json:"nodePort"`
+	Seconds  uint32 `json:"seconds"`
 }
 
 // NewInstallValues returns a new instance of the Values type.
diff --git a/pkg/healthcheck/healthcheck.go b/pkg/healthcheck/healthcheck.go
index d9a71b975..3b53db57b 100644
--- a/pkg/healthcheck/healthcheck.go
+++ b/pkg/healthcheck/healthcheck.go
@@ -156,11 +156,6 @@ const (
 	keyKeyName                    = "tls.key"
 )
 
-// DefaultHintBaseURL is the base URL on the linkerd.io website that all check hints
-// point to. Each check adds its own `hintAnchor` to specify a location on the
-// page.
-const DefaultHintBaseURL = "https://linkerd.io/checks/#"
-
 // AllowedClockSkew sets the allowed skew in clock synchronization
 // between the system running inject command and the node(s), being
 // based on assumed node's heartbeat interval (5 minutes) plus default TLS
@@ -367,13 +362,12 @@ type Category struct {
 }
 
 // NewCategory returns an instance of Category with the specified data
-// and the DefaultHintBaseURL
 func NewCategory(id CategoryID, checkers []Checker, enabled bool) *Category {
 	return &Category{
 		ID:          id,
 		checkers:    checkers,
 		enabled:     enabled,
-		hintBaseURL: DefaultHintBaseURL,
+		hintBaseURL: HintBaseURL(version.Version),
 	}
 }
 
@@ -496,7 +490,7 @@ func (hc *HealthChecker) GetCategories() []*Category {
 //
 // Note that all checks should include a `hintAnchor` with a corresponding section
 // in the linkerd check faq:
-// https://linkerd.io/checks/#
+// https://linkerd.io/{major-version}/checks/#
 func (hc *HealthChecker) allCategories() []*Category {
 	return []*Category{
 		NewCategory(
diff --git a/pkg/healthcheck/healthcheck_output.go b/pkg/healthcheck/healthcheck_output.go
index 3e7ffa38b..4b22ef191 100644
--- a/pkg/healthcheck/healthcheck_output.go
+++ b/pkg/healthcheck/healthcheck_output.go
@@ -8,11 +8,13 @@ import (
 	"io"
 	"os"
 	"os/exec"
+	"regexp"
 	"strings"
 	"time"
 
 	"github.com/briandowns/spinner"
 	"github.com/fatih/color"
+	"github.com/linkerd/linkerd2/pkg/version"
 	"github.com/mattn/go-isatty"
 )
 
@@ -25,12 +27,19 @@ const (
 	WideOutput = "wide"
 	// ShortOutput is used to specify the short output format
 	ShortOutput = "short"
+
+	// DefaultHintBaseURL is the default base URL on the linkerd.io website
+	// that all check hints for the latest linkerd version point to. Each
+	// check adds its own `hintAnchor` to specify a location on the page.
+	DefaultHintBaseURL = "https://linkerd.io/2/checks/#"
 )
 
 var (
 	okStatus   = color.New(color.FgGreen, color.Bold).SprintFunc()("\u221A")  // √
 	warnStatus = color.New(color.FgYellow, color.Bold).SprintFunc()("\u203C") // ‼
 	failStatus = color.New(color.FgRed, color.Bold).SprintFunc()("\u00D7")    // ×
+
+	reStableVersion = regexp.MustCompile(`stable-(\d\.\d+)\.`)
 )
 
 // CheckResults contains a slice of CheckResult structs.
@@ -101,7 +110,7 @@ func RunExtensionsChecks(wout io.Writer, werr io.Writer, extensions []string, fl
 					Category:    CategoryID(extensionCmd),
 					Description: fmt.Sprintf("Linkerd extension command %s exists", extensionCmd),
 					Err:         err,
-					HintURL:     DefaultHintBaseURL + "extensions",
+					HintURL:     HintBaseURL(version.Version) + "extensions",
 					Warning:     true,
 				},
 			}
@@ -130,7 +139,7 @@ func RunExtensionsChecks(wout io.Writer, werr io.Writer, extensions []string, fl
 					Category:    CategoryID(extensionCmd),
 					Description: fmt.Sprintf("Running: %s", command),
 					Err:         err,
-					HintURL:     DefaultHintBaseURL + "extensions",
+					HintURL:     HintBaseURL(version.Version) + "extensions",
 				})
 				success = false
 			} else {
@@ -401,3 +410,13 @@ func printCategory(wout io.Writer, lastCategory CategoryID, result *CheckResult)
 
 	return result.Category
 }
+
+// HintBaseURL returns the base URL on the linkerd.io website that check hints
+// point to, depending on the version
+func HintBaseURL(ver string) string {
+	stableVersion := reStableVersion.FindStringSubmatch(ver)
+	if stableVersion == nil {
+		return DefaultHintBaseURL
+	}
+	return fmt.Sprintf("https://linkerd.io/%s/checks/#", stableVersion[1])
+}
diff --git a/pkg/inject/inject.go b/pkg/inject/inject.go
index 8974a2d82..8cadd4208 100644
--- a/pkg/inject/inject.go
+++ b/pkg/inject/inject.go
@@ -361,7 +361,13 @@ func (conf *ResourceConfig) GetConfigAnnotation(annotationKey string) (string, b
 // CreateAnnotationPatch returns a json patch which adds the opaque ports
 // annotation with the `opaquePorts` value.
 func (conf *ResourceConfig) CreateAnnotationPatch(opaquePorts string) ([]byte, error) {
-	addRootAnnotations := len(conf.pod.meta.Annotations) == 0
+	addRootAnnotations := false
+	if conf.IsPod() {
+		addRootAnnotations = len(conf.pod.meta.Annotations) == 0
+	} else {
+		addRootAnnotations = len(conf.workload.Meta.Annotations) == 0
+	}
+
 	patch := &annotationPatch{
 		AddRootAnnotations: addRootAnnotations,
 		OpaquePorts:        opaquePorts,
diff --git a/pkg/k8s/completion.go b/pkg/k8s/completion.go
new file mode 100644
index 000000000..35ff5e4e3
--- /dev/null
+++ b/pkg/k8s/completion.go
@@ -0,0 +1,180 @@
+package k8s
+
+import (
+	"context"
+	"fmt"
+	"strings"
+	"time"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+)
+
+// CommandCompletion generates CLI suggestions from resources in a given cluster
+// It uses a list of arguments and a substring from the CLI to filter suggestions.
+type CommandCompletion struct {
+	k8sAPI    *KubernetesAPI
+	namespace string
+}
+
+// NewCommandCompletion creates a command completion module
+func NewCommandCompletion(
+	k8sAPI *KubernetesAPI,
+	namespace string,
+) *CommandCompletion {
+	return &CommandCompletion{
+		k8sAPI:    k8sAPI,
+		namespace: namespace,
+	}
+}
+
+// Complete accepts a list of arguments and a substring to generate CLI suggestions.
+// `args` represent a list of arguments a user has already enterd in the CLI. These
+// arguments are used for determining what resource type we'd like to receive
+// suggestions for as well as a list of resources names that have already provided.
+// `toComplete` represents the string prefix of a resource name that we'd like to
+// use to search for suggestions
+//
+// If `args` is empty, send back a list of all resource types support by the CLI.
+// If `args` has at least one or more items, assume that the first item in `args`
+// is the resource type we are trying to get suggestions for e.g. Deployment, StatefulSets.
+//
+// Complete is generic enough so that it can find suggestions for any type of resource
+// in a Kubernetes cluster. It does this by first querying what GroupVersion a resource
+// belongs to and then does a dynamic `List` query to get resources under that GroupVersion
+func (c *CommandCompletion) Complete(args []string, toComplete string) ([]string, error) {
+	ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancelFn()
+
+	suggestions := []string{}
+	if len(args) == 0 && toComplete == "" {
+		return StatAllResourceTypes, nil
+	}
+
+	if len(args) == 0 && toComplete != "" {
+		for _, t := range StatAllResourceTypes {
+			if strings.HasPrefix(t, toComplete) {
+				suggestions = append(suggestions, t)
+			}
+		}
+		return suggestions, nil
+	}
+
+	resType, err := CanonicalResourceNameFromFriendlyName(args[0])
+	if err != nil {
+		return nil, fmt.Errorf("%s is not a valid resource name", args)
+	}
+
+	// if we are looking for namespace suggestions clear namespace selector
+	if resType == "namespace" {
+		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
+	}
+
+	uList, err := c.k8sAPI.DynamicClient.
+		Resource(*gvr).
+		Namespace(c.namespace).
+		List(ctx, metav1.ListOptions{})
+	if err != nil {
+		return nil, err
+	}
+
+	otherResources := args[1:]
+	for _, u := range uList.Items {
+		name := u.GetName()
+
+		// Filter out the list of resource items returned from k8s API to
+		// only include items that have the prefix `toComplete` and items
+		// that aren't in the list of resources already provided in the
+		// list of arguments.
+		//
+		// This is useful so that we avoid duplicate suggestions if they
+		// are already in the list of args
+		if strings.HasPrefix(name, toComplete) &&
+			!containsResource(name, otherResources) {
+			suggestions = append(suggestions, name)
+		}
+	}
+
+	return suggestions, nil
+}
+
+func (c *CommandCompletion) getGroupVersionKindForResource(resourceName string) (*schema.GroupVersionResource, error) {
+	_, apiResourceList, err := c.k8sAPI.Discovery().ServerGroupsAndResources()
+	if err != nil {
+		return nil, err
+	}
+
+	// find the plural name to ensure the resource we are searching for is not a subresource
+	// i.e. deployment/scale
+	pluralResourceName, err := PluralResourceNameFromFriendlyName(resourceName)
+	if err != nil {
+		return nil, fmt.Errorf("%s not a valid resource name", resourceName)
+	}
+
+	gvr, err := findGroupVersionResource(resourceName, pluralResourceName, apiResourceList)
+	if err != nil {
+		return nil, fmt.Errorf("could not find GroupVersionResource for %s", resourceName)
+	}
+
+	return gvr, nil
+}
+
+func findGroupVersionResource(singularName string, pluralName string, apiResourceList []*metav1.APIResourceList) (*schema.GroupVersionResource, error) {
+	err := fmt.Errorf("could not find the requested resource")
+	for _, res := range apiResourceList {
+		for _, r := range res.APIResources {
+
+			// Make sure we get a resource type where its Kind matches the
+			// singularName passed into this function and its Name (which is always
+			// the pluralName of an api resource) matches the pluralName passed
+			// into this function. Skip further processing of this APIResource
+			// if this is not the case.
+			if strings.ToLower(r.Kind) != singularName || r.Name != pluralName {
+				continue
+			}
+
+			gv := strings.Split(res.GroupVersion, "/")
+
+			if len(gv) == 1 && gv[0] == "v1" {
+				return &schema.GroupVersionResource{
+					Version:  gv[0],
+					Resource: r.Name,
+				}, nil
+			}
+
+			if len(gv) != 2 {
+				return nil, err
+			}
+
+			return &schema.GroupVersionResource{
+				Group:    gv[0],
+				Version:  gv[1],
+				Resource: r.Name,
+			}, nil
+		}
+	}
+
+	return nil, err
+}
+
+func containsResource(resource string, otherResources []string) bool {
+	for _, r := range otherResources {
+		if r == resource {
+			return true
+		}
+	}
+
+	return false
+}
diff --git a/pkg/k8s/k8s.go b/pkg/k8s/k8s.go
index e480d3375..aa989b3c5 100644
--- a/pkg/k8s/k8s.go
+++ b/pkg/k8s/k8s.go
@@ -62,7 +62,7 @@ var AllResources = []string{
 
 // StatAllResourceTypes represents the resources to query in StatSummary when Resource.Type is "all"
 var StatAllResourceTypes = []string{
-	// TODO: add Namespace here to decrease queries from the web process
+	Namespace,
 	DaemonSet,
 	StatefulSet,
 	Job,
diff --git a/pkg/multicluster/link.go b/pkg/multicluster/link.go
index 5676115e1..824f31c3e 100644
--- a/pkg/multicluster/link.go
+++ b/pkg/multicluster/link.go
@@ -200,7 +200,7 @@ func ExtractProbeSpec(gateway *corev1.Service) (ProbeSpec, error) {
 		return ProbeSpec{}, errors.New("probe path is empty")
 	}
 
-	port, err := extractPort(gateway.Spec.Ports, consts.ProbePortName)
+	port, err := extractPort(gateway.Spec, consts.ProbePortName)
 	if err != nil {
 		return ProbeSpec{}, err
 	}
@@ -248,9 +248,12 @@ func GetLink(ctx context.Context, client dynamic.Interface, namespace, name stri
 	return NewLink(*unstructured)
 }
 
-func extractPort(port []corev1.ServicePort, portName string) (uint32, error) {
-	for _, p := range port {
+func extractPort(spec corev1.ServiceSpec, portName string) (uint32, error) {
+	for _, p := range spec.Ports {
 		if p.Name == portName {
+			if spec.Type == "NodePort" {
+				return uint32(p.NodePort), nil
+			}
 			return uint32(p.Port), nil
 		}
 	}
diff --git a/viz/cmd/stat.go b/viz/cmd/stat.go
index 05039a661..9c80a59a1 100644
--- a/viz/cmd/stat.go
+++ b/viz/cmd/stat.go
@@ -18,10 +18,10 @@ import (
 	"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"
 	log "github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
+	v1 "k8s.io/api/core/v1"
 )
 
 type statOptions struct {
@@ -167,8 +167,31 @@ If no resource name is specified, displays stats about all resources of the spec
 
   # Get all inbound stats to the test namespace.
   linkerd viz stat ns/test`,
-		Args:      cobra.MinimumNArgs(1),
-		ValidArgs: pkg.ValidTargets,
+		Args: cobra.MinimumNArgs(1),
+		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+
+			k8sAPI, err := k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, impersonateGroup, 0)
+			if err != nil {
+				return nil, cobra.ShellCompDirectiveError
+			}
+
+			if options.namespace == "" {
+				options.namespace = pkgcmd.GetDefaultNamespace(kubeconfigPath, kubeContext)
+			}
+
+			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)
@@ -229,9 +252,33 @@ If no resource name is specified, displays stats about all resources of the spec
 	cmd.PersistentFlags().StringVarP(&options.outputFormat, "output", "o", options.outputFormat, "Output format; one of: \"table\" or \"json\" or \"wide\"")
 	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)
 	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/web/app/yarn.lock b/web/app/yarn.lock
index 64da7cb14..03265076a 100644
--- a/web/app/yarn.lock
+++ b/web/app/yarn.lock
@@ -5542,9 +5542,9 @@ hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.2:
     react-is "^16.7.0"
 
 hosted-git-info@^2.1.4:
-  version "2.8.8"
-  resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
-  integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==
+  version "2.8.9"
+  resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
+  integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
 
 hpack.js@^2.1.6:
   version "2.1.6"
@@ -7849,10 +7849,10 @@ nan@^2.12.1:
   resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19"
   integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==
 
-nanoid@^3.1.20:
-  version "3.1.22"
-  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.22.tgz#b35f8fb7d151990a8aebd5aa5015c03cf726f844"
-  integrity sha512-/2ZUaJX2ANuLtTvqTlgqBQNJoQO398KyJgZloL0PZkC0dpysjncRUPsFe3DUPzz/y3h+u7C46np8RMuvF3jsSQ==
+nanoid@^3.1.23:
+  version "3.1.23"
+  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81"
+  integrity sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==
 
 nanomatch@^1.2.9:
   version "1.2.13"
@@ -8654,12 +8654,12 @@ postcss-value-parser@^4.1.0:
   integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==
 
 postcss@^8.2.8:
-  version "8.2.8"
-  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.8.tgz#0b90f9382efda424c4f0f69a2ead6f6830d08ece"
-  integrity sha512-1F0Xb2T21xET7oQV9eKuctbM9S7BC0fetoHCc4H13z0PT6haiRLP4T0ZY4XWh7iLP0usgqykT6p9B2RtOf4FPw==
+  version "8.2.15"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.15.tgz#9e66ccf07292817d226fc315cbbf9bc148fbca65"
+  integrity sha512-2zO3b26eJD/8rb106Qu2o7Qgg52ND5HPjcyQiK2B98O388h43A448LCslC0dI2P97wCAQRJsFvwTRcXxTKds+Q==
   dependencies:
     colorette "^1.2.2"
-    nanoid "^3.1.20"
+    nanoid "^3.1.23"
     source-map "^0.6.1"
 
 prelude-ls@^1.2.1:


Test patch:
diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml
index 2096eb970..a92621bed 100644
--- a/.github/workflows/integration_tests.yml
+++ b/.github/workflows/integration_tests.yml
@@ -112,7 +112,7 @@ jobs:
     needs: [docker_build]
     name: Integration tests (${{ matrix.integration_test }})
     runs-on: ubuntu-20.04
-    timeout-minutes: 30
+    timeout-minutes: 60
     steps:
     - name: Checkout code
       # actions/checkout@v2
diff --git a/bin/_test-helpers.sh b/bin/_test-helpers.sh
index 0d7d84fa4..d9c0828b9 100644
--- a/bin/_test-helpers.sh
+++ b/bin/_test-helpers.sh
@@ -348,7 +348,7 @@ run_test(){
 
   printf 'Test script: [%s] Params: [%s]\n' "${filename##*/}" "$*"
   # Exit on failure here
-  GO111MODULE=on go test --failfast --mod=readonly "$filename" --linkerd="$linkerd_path" --helm-path="$helm_path" --k8s-context="$context" --integration-tests "$@" || exit 1
+  GO111MODULE=on go test -test.timeout=60m --failfast --mod=readonly "$filename" --linkerd="$linkerd_path" --helm-path="$helm_path" --k8s-context="$context" --integration-tests "$@" || exit 1
 }
 
 # Returns the latest version for the release channel
@@ -386,7 +386,7 @@ install_version() {
 
     (
         set -x
-        "$linkerd_path" check 2>&1
+        "$linkerd_path" check --wait 60m 2>&1
     )
     exit_on_err 'install_version() - linkerd check failed'
 
diff --git a/cli/cmd/testdata/check_output.golden b/cli/cmd/testdata/check_output.golden
index 39c9efb0b..db4ea9a1a 100644
--- a/cli/cmd/testdata/check_output.golden
+++ b/cli/cmd/testdata/check_output.golden
@@ -3,6 +3,6 @@ category
 √ check1
 × check2
     This should contain instructions for fail
-    see https://linkerd.io/checks/#hint-anchor for hints
+    see https://linkerd.io/2/checks/#hint-anchor for hints
 
 Status check results are ×
diff --git a/cli/cmd/testdata/check_output_json.golden b/cli/cmd/testdata/check_output_json.golden
index b87ac212e..38cc72a72 100644
--- a/cli/cmd/testdata/check_output_json.golden
+++ b/cli/cmd/testdata/check_output_json.golden
@@ -10,7 +10,7 @@
         },
         {
           "description": "check2",
-          "hint": "https://linkerd.io/checks/#hint-anchor",
+          "hint": "https://linkerd.io/2/checks/#hint-anchor",
           "error": "This should contain instructions for fail",
           "result": "error"
         }
diff --git a/test/integration/install_test.go b/test/integration/install_test.go
index ef0588ec8..717044810 100644
--- a/test/integration/install_test.go
+++ b/test/integration/install_test.go
@@ -234,7 +234,7 @@ func TestInstallCNIPlugin(t *testing.T) {
 	// perform a linkerd check with --linkerd-cni-enabled
 	timeout := time.Minute
 	err = TestHelper.RetryFor(timeout, func() error {
-		out, err = TestHelper.LinkerdRun("check", "--pre", "--linkerd-cni-enabled", "--wait=0")
+		out, err = TestHelper.LinkerdRun("check", "--pre", "--linkerd-cni-enabled", "--wait=60m")
 		if err != nil {
 			return err
 		}
@@ -861,7 +861,7 @@ func testCheckCommand(t *testing.T, stage, expectedVersion, namespace, cliVersio
 	var golden string
 	proxyStage := "proxy"
 	if stage == proxyStage {
-		cmd = []string{"check", "--proxy", "--expected-version", expectedVersion, "--namespace", namespace, "--wait=0"}
+		cmd = []string{"check", "--proxy", "--expected-version", expectedVersion, "--namespace", namespace, "--wait=60m"}
 		// if TestHelper.GetMulticlusterHelmReleaseName() != "" || TestHelper.Multicluster() {
 		// golden = "check.multicluster.proxy.golden"
 		// } else if TestHelper.CNI() {
@@ -871,10 +871,10 @@ func testCheckCommand(t *testing.T, stage, expectedVersion, namespace, cliVersio
 			golden = "check.proxy.golden"
 		}
 	} else if stage == "config" {
-		cmd = []string{"check", "config", "--expected-version", expectedVersion, "--wait=0"}
+		cmd = []string{"check", "config", "--expected-version", expectedVersion, "--wait=60m"}
 		golden = "check.config.golden"
 	} else {
-		cmd = []string{"check", "--expected-version", expectedVersion, "--wait=0"}
+		cmd = []string{"check", "--expected-version", expectedVersion, "--wait=60m"}
 		// if TestHelper.GetMulticlusterHelmReleaseName() != "" || TestHelper.Multicluster() {
 		// golden = "check.multicluster.golden"
 		// } else if TestHelper.CNI() {
@@ -946,8 +946,10 @@ func getCheckOutput(t *testing.T, goldenFile string, namespace string) string {
 	tpl := template.Must(template.ParseFiles("testdata" + "/" + goldenFile))
 	vars := struct {
 		ProxyVersionErr string
+		HintURL         string
 	}{
 		healthcheck.CheckProxyVersionsUpToDate(pods, version.Channels{}).Error(),
+		healthcheck.HintBaseURL(TestHelper.GetVersion()),
 	}
 
 	var expected bytes.Buffer
@@ -968,7 +970,7 @@ func TestCheckPostInstall(t *testing.T) {
 }
 
 func TestCheckViz(t *testing.T) {
-	cmd := []string{"viz", "check", "--wait=0"}
+	cmd := []string{"viz", "check", "--wait=60m"}
 	golden := "check.viz.golden"
 	if TestHelper.ExternalPrometheus() {
 		golden = "check.viz.external-prometheus.golden"
@@ -982,8 +984,10 @@ func TestCheckViz(t *testing.T) {
 	tpl := template.Must(template.ParseFiles("testdata" + "/" + golden))
 	vars := struct {
 		ProxyVersionErr string
+		HintURL         string
 	}{
 		healthcheck.CheckProxyVersionsUpToDate(pods, version.Channels{}).Error(),
+		healthcheck.HintBaseURL(TestHelper.GetVersion()),
 	}
 
 	var expected bytes.Buffer
@@ -1209,7 +1213,7 @@ func TestRestarts(t *testing.T) {
 
 func TestCheckMulticluster(t *testing.T) {
 	if TestHelper.GetMulticlusterHelmReleaseName() != "" || TestHelper.Multicluster() {
-		cmd := []string{"multicluster", "check", "--wait=0"}
+		cmd := []string{"multicluster", "check", "--wait=60m"}
 		golden := "check.multicluster.golden"
 		timeout := time.Minute
 		err := TestHelper.RetryFor(timeout, func() error {
diff --git a/test/integration/testdata/check.viz.external-prometheus.golden b/test/integration/testdata/check.viz.external-prometheus.golden
index f8989f74b..727dd2b66 100644
--- a/test/integration/testdata/check.viz.external-prometheus.golden
+++ b/test/integration/testdata/check.viz.external-prometheus.golden
@@ -11,7 +11,7 @@ linkerd-viz
 √ viz extension proxies are healthy
 ‼ viz extension proxies are up-to-date
     {{.ProxyVersionErr}}
-    see https://linkerd.io/checks/#l5d-viz-proxy-cp-version for hints
+    see https://linkerd.io/2/checks/#l5d-viz-proxy-cp-version for hints
 √ viz extension proxies and cli versions match
 √ can initialize the client
 √ viz extension self-check
diff --git a/test/integration/testdata/check.viz.golden b/test/integration/testdata/check.viz.golden
index af33cd21b..99f74c4e4 100644
--- a/test/integration/testdata/check.viz.golden
+++ b/test/integration/testdata/check.viz.golden
@@ -11,7 +11,7 @@ linkerd-viz
 √ viz extension proxies are healthy
 ‼ viz extension proxies are up-to-date
     {{.ProxyVersionErr}}
-    see https://linkerd.io/checks/#l5d-viz-proxy-cp-version for hints
+    see {{.HintURL}}l5d-viz-proxy-cp-version for hints
 √ viz extension proxies and cli versions match
 √ prometheus is installed and configured correctly
 √ can initialize the client
diff --git a/test/integration/testdata/check.viz.proxy.golden b/test/integration/testdata/check.viz.proxy.golden
index d62947f61..3be3d6aac 100644
--- a/test/integration/testdata/check.viz.proxy.golden
+++ b/test/integration/testdata/check.viz.proxy.golden
@@ -11,7 +11,7 @@ linkerd-viz
 √ viz extension proxies are healthy
 ‼ viz extension proxies are up-to-date
     {{.ProxyVersionErr}}
-    see https://linkerd.io/checks/#l5d-viz-proxy-cp-version for hints
+    see {{.HintURL}}l5d-viz-proxy-cp-version for hints
 √ viz extension proxies and cli versions match
 √ prometheus is installed and configured correctly
 √ can initialize the client
diff --git a/test/integration/tracing/testdata/check.jaeger.golden b/test/integration/tracing/testdata/check.jaeger.golden
index 41c125b5e..5d4503107 100644
--- a/test/integration/tracing/testdata/check.jaeger.golden
+++ b/test/integration/tracing/testdata/check.jaeger.golden
@@ -8,7 +8,7 @@ linkerd-jaeger
 √ jaeger extension proxies are healthy
 ‼ jaeger extension proxies are up-to-date
     {{.ProxyVersionErr}}
-    see https://linkerd.io/checks/#l5d-jaeger-proxy-cp-version for hints
+    see {{.HintURL}}l5d-jaeger-proxy-cp-version for hints
 √ jaeger extension proxies and cli versions match
 
 Status check results are √
diff --git a/test/integration/tracing/tracing_test.go b/test/integration/tracing/tracing_test.go
index 1885f7b8b..076e38f6b 100644
--- a/test/integration/tracing/tracing_test.go
+++ b/test/integration/tracing/tracing_test.go
@@ -92,8 +92,10 @@ func TestTracing(t *testing.T) {
 		tpl := template.Must(template.ParseFiles("testdata" + "/" + golden))
 		vars := struct {
 			ProxyVersionErr string
+			HintURL         string
 		}{
 			healthcheck.CheckProxyVersionsUpToDate(pods, version.Channels{}).Error(),
+			healthcheck.HintBaseURL(TestHelper.GetVersion()),
 		}
 
 		var expected bytes.Buffer
diff --git a/test/integration/uninstall/uninstall_test.go b/test/integration/uninstall/uninstall_test.go
index f90f1fead..02489945d 100644
--- a/test/integration/uninstall/uninstall_test.go
+++ b/test/integration/uninstall/uninstall_test.go
@@ -42,7 +42,7 @@ func TestInstall(t *testing.T) {
 	var (
 		vizCmd  = []string{"viz", "install"}
 		vizArgs = []string{
-			"--set", fmt.Sprintf("namespace=%s", TestHelper.GetVizNamespace())}
+			"--set", fmt.Sprintf("namespace=%s", TestHelper.GetVizNamespace()), "--wait", "60m"}
 	)
 
 	// Install Linkerd Viz Extension
diff --git a/testutil/kubernetes_helper.go b/testutil/kubernetes_helper.go
index 81fe57aa2..8953c7b04 100644
--- a/testutil/kubernetes_helper.go
+++ b/testutil/kubernetes_helper.go
@@ -182,7 +182,7 @@ func (h *KubernetesHelper) GetResources(ctx context.Context, containerName, depl
 func (h *KubernetesHelper) CheckPods(ctx context.Context, namespace string, deploymentName string, replicas int) error {
 	var checkedPods []corev1.Pod
 
-	err := h.retryFor(6*time.Minute, func() error {
+	err := h.retryFor(60*time.Minute, func() error {
 		checkedPods = []corev1.Pod{}
 		pods, err := h.GetPodsForDeployment(ctx, namespace, deploymentName)
 		if err != nil {
@@ -325,7 +325,7 @@ func (h *KubernetesHelper) URLFor(ctx context.Context, namespace, deployName str
 func (h *KubernetesHelper) WaitRollout(t *testing.T) {
 	for deploy, deploySpec := range LinkerdDeployReplicasEdge {
 		if deploySpec.Namespace == "linkerd" {
-			o, err := h.Kubectl("", "--namespace=linkerd", "rollout", "status", "--timeout=120s", "deploy/"+deploy)
+			o, err := h.Kubectl("", "--namespace=linkerd", "rollout", "status", "--timeout=60m", "deploy/"+deploy)
 			if err != nil {
 				AnnotatedFatalf(t,
 					fmt.Sprintf("failed to wait rollout of deploy/%s", deploy),
diff --git a/testutil/test_helper.go b/testutil/test_helper.go
index 7b690d853..899be08cb 100644
--- a/testutil/test_helper.go
+++ b/testutil/test_helper.go
@@ -477,6 +477,8 @@ func (h *TestHelper) HelmUpgrade(chart string, arg ...string) (string, string, e
 		h.helm.releaseName,
 		"--kube-context", h.k8sContext,
 		"--set", "namespace=" + h.namespace,
+		"--timeout", "60m",
+		"--wait",
 		chart,
 	}, arg...)
 	return combinedOutput("", h.helm.path, withParams...)
@@ -490,6 +492,8 @@ func (h *TestHelper) HelmInstall(chart string, arg ...string) (string, string, e
 		chart,
 		"--kube-context", h.k8sContext,
 		"--set", "namespace=" + h.namespace,
+		"--timeout", "60m",
+		"--wait",
 	}, arg...)
 	return combinedOutput("", h.helm.path, withParams...)
 }
diff --git a/viz/metrics-api/stat_summary_test.go b/viz/metrics-api/stat_summary_test.go
index 755e04a79..e3a4ee59a 100644
--- a/viz/metrics-api/stat_summary_test.go
+++ b/viz/metrics-api/stat_summary_test.go
@@ -1316,6 +1316,13 @@ status:
 										},
 									},
 								},
+								{
+									Table: &pb.StatTable_PodGroup_{
+										PodGroup: &pb.StatTable_PodGroup{
+											Rows: []*pb.StatTable_PodGroup_Row{},
+										},
+									},
+								},
 								{
 									Table: &pb.StatTable_PodGroup_{
 										PodGroup: &pb.StatTable_PodGroup{
diff --git a/web/srv/testdata/api_check_output.json b/web/srv/testdata/api_check_output.json
index b7539ed8c..70f331e50 100644
--- a/web/srv/testdata/api_check_output.json
+++ b/web/srv/testdata/api_check_output.json
@@ -13,7 +13,7 @@
             "Warning": true,
             "Err": {},
             "ErrMsg": "check2-error",
-            "HintURL": "https://linkerd.io/checks/#check2-hint-anchor"
+            "HintURL": "https://linkerd.io/2/checks/#check2-hint-anchor"
         }],
         "linkerd-config": [{
             "Category": "linkerd-config",
@@ -24,4 +24,4 @@
         }]
     },
     "success": true
-}
\ No newline at end of file
+}