Repository: linkerd/linkerd2
Pull request #6217 (2021-06-03 01:44:09)

add resource aware completion for core linkerd cmd
Signed-off-by: Dennis Adjei-Baah <dennis@buoyant.io>




Patch:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 4486f3cf9..a6cd468f4 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -134,7 +134,7 @@ jobs:
         bin/docker-pull-binaries $TAG
         # Validate the CLI version matches the current build tag.
         [[ "$TAG" == "$($CMD version --short --client)" ]]
-        bin/tests --images skip --name ${{ matrix.integration_test }} "$CMD"
+        bin/tests --images preload --name ${{ matrix.integration_test }} "$CMD"
   arm64_integration_tests:
     name: ARM64 integration tests
     timeout-minutes: 60
@@ -175,7 +175,7 @@ jobs:
       #if: startsWith(github.ref, 'refs/tags/stable')
       env:
         RUN_ARM_TEST: 1
-      run: bin/tests --name deep --images skip --skip-cluster-create "$CMD"
+      run: bin/tests --name deep --images preload --skip-cluster-create "$CMD"
     - name: CNI tests
       #if: startsWith(github.ref, 'refs/tags/stable')
       run: |
diff --git a/CHANGES.md b/CHANGES.md
index 7d389b8c1..08667af88 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,35 @@
 # Changes
 
+## edge-21.6.1
+
+This release adds support for retrying HTTP/2 requests with small (<64KB)
+message bodies, allowing the proxy to properly buffer message bodies when
+responses are classified as a failure. Documentation on how to configure
+retries can be found [here](https://linkerd.io/2.10/tasks/configuring-retries/).
+
+This release also modifies the proxy's identity subsystem to instantiate a
+client on-demand so client connections are not retained continually. Also
+included in this release are various bug fixes and improvements as well as
+expanding support for resource-aware tab completion in the jaeger and
+multicluster CLI extensions.
+
+* Added support for specifying a `gateway-port` flag for the `multicluster link`
+  command (thanks @psmit!)
+* Added support for Kubernetes resource aware tab completion for `jaeger` and
+  `multicluster` commands
+* Fixed an issue where `viz`, `jaeger` and `multicluster` extensions could not
+  be installed on `PodSecurityPolicy`-enabled clusters
+* Fixed an issue where `linkerd check --proxy` could incorrectly report
+  out-of-date proxy versions caused by incorrect regex (thanks @aryan9600!)
+* Added support for the proxy to retry HTTP/2 requests with message bodies
+  <= 64KB
+* Modified the proxy's controller stack to create new client connections
+  on-demand
+* Fixed Viz's `uninstall` command to remove viz installations that used the
+  legacy `linkerd.io/extension: linkerd-viz` label (thanks @jsoref!)
+* Expanded the "linkerd-existence" health check to also check for the
+  destination pod readiness
+
 ## edge-21.5.3
 
 This edge release contains various improvements to the Viz and Jaeger install
diff --git a/bin/image-load b/bin/image-load
index aabf41d21..5fff54a57 100755
--- a/bin/image-load
+++ b/bin/image-load
@@ -4,61 +4,103 @@ set -eo pipefail
 
 kind=""
 k3d=""
+cluster=""
 archive=""
+preload=""
+images_default=(proxy controller web metrics-api grafana debug cni-plugin jaeger-webhook tap)
+images=()
+
+usage() {
+  printf "Load into KinD/k3d the referred Linkerd images. If no images are specified all of them are loaded (%s)\n" "${images_default[*]}"
+  echo ""
+  echo "Usage:"
+  echo "    bin/image-load [--kind] [--k3d] [--cluster name] [--preload] [--archive] [image] [image]..."
+  echo ""
+  echo "Examples:"
+  echo ""
+  echo "    # Load all the images from the local docker instance into KinD"
+  echo "    bin/image-load"
+  echo ""
+  echo "    # Load only the proxy and controller images into k3d"
+  echo "    bin/image-load --k3d proxy controller"
+  echo ""
+  echo "    # Load all the images from tar files located under the 'image-archives' directory into k3d"
+  echo "    bin/image-load --k3d --archive"
+  echo ""
+  echo "Available Commands:"
+  echo "    --cluster: target cluster name (defaults to 'k3s-default' under k3d, and 'kind' under KinD)."
+  echo "    --kind: use a KinD cluster (default)."
+  echo "    --k3d: use a k3d cluster."
+  echo "    --preload: pull the docker images from a public registry prior to loading them into the cluster, which appears to be faster than having k3d pulling them itself."
+  echo "    --archive: load the images from local .tar files in the current directory."
+}
 
 while :
 do
   case $1 in
     -h|--help)
-      echo "Load into KinD/k3d the images for Linkerd's proxy, controller, metrics-api, web, grafana, cli-bin, tap, debug and cni-plugin."
-      echo ""
-      echo "Usage:"
-      echo "    bin/image-load [--kind] [--k3d] [--archive]"
-      echo ""
-      echo "Examples:"
-      echo ""
-      echo "    # Load images from the local docker instance into KinD"
-      echo "    bin/image-load"
-      echo ""
-      echo "    # Load images from tar files located under the 'image-archives' directory into k3d"
-      echo "    bin/image-load --k3d --archive"
-      echo ""
-      echo "Available Commands:"
-      echo "    --kind: use a KinD cluster (default)."
-      echo "    --k3d: use a k3d cluster."
-      echo "    --archive: load the images from local .tar files in the current directory."
+      usage
       exit 0
       ;;
+    --cluster)
+      cluster=$2
+      if [ -z "$cluster" ]; then
+        echo 'Error: the argument for --cluster was not specified' >&2
+        usage
+        exit 1
+      fi
+      shift
+      ;;
     --kind)
       kind=1
       ;;
     --k3d)
       k3d=1
       ;;
+    --preload)
+      preload=1
+      ;;
     --archive)
       archive=1
       ;;
     *)
-      break
+      if [ -z "$1" ]; then
+        break
+      fi
+      if echo "$1" | grep -q '^-.*' ; then
+        echo "Unexpected flag: $1" >&2
+        usage
+        exit 1
+      fi
+      images+=("$1")
   esac
   shift
 done
 
+if [ ${#images[@]} -eq 0 ]; then
+  images=("${images_default[@]}")
+fi
+
 bindir=$( cd "${0%/*}" && pwd )
 
 if [ -n "$k3d" ]; then
   if [ -n "$kind" ]; then
     echo $k3d
     echo "Error: --kind and --k3d can't be used simultaneously" >&2
+    usage
     exit 1
   fi
-  cluster=${1:-"k3s-default"}
+  if [ -z "$cluster" ]; then
+    cluster="k3s-default"
+  fi
   bin="$bindir"/k3d
   image_sub_cmd=(image import -c "$cluster")
 else
   kind=1
-  cluster=${1:-"kind"}
   bin="$bindir"/kind
+  if [ -z "$cluster" ]; then
+    cluster="kind"
+  fi
   if [ $archive ]; then
     image_sub_cmd=(load image-archive --name "$cluster")
   else
@@ -79,19 +121,23 @@ fi
 "$bin" version
 
 rm -f load_fail
-for img in proxy controller web metrics-api grafana debug cni-plugin jaeger-webhook tap; do
-  printf 'Importing %s...\n' $img
+for i in "${!images[@]}"; do
   if [ $archive ]; then
-    param="image-archives/$img.tar"
+    param="image-archives/${images[$i]}.tar"
   else
-    param="$DOCKER_REGISTRY/$img:$TAG"
+    param="$DOCKER_REGISTRY/${images[$i]}:$TAG"
+    if [ $preload ]; then
+      "$bindir"/docker pull -q "$param" || (echo "Error pulling image $param"; touch load_fail) &
+    fi
   fi
 
   if [ -n "$kind" ]; then
+    printf 'Importing %s...\n' "${images[$i]}"
     "$bin" "${image_sub_cmd[@]}" "${param[@]}" || touch load_fail &
   else
     # "k3d image import" commands don't behave well when parallelized
-    "$bin" "${image_sub_cmd[@]}" "${param[@]}"
+    # but all the images can be loaded in a single invocation
+    images[$i]=$param
   fi
 done
 
@@ -101,3 +147,8 @@ if [ -f load_fail ]; then
   rm load_fail
   exit 1
 fi
+
+if [ -n "$k3d" ]; then
+  printf 'Importing %s...\n' "${images[@]}"
+  "$bin" "${image_sub_cmd[@]}" "${images[@]}"
+fi
diff --git a/bin/install-pr b/bin/install-pr
index 0fd289d72..b06a7f443 100755
--- a/bin/install-pr
+++ b/bin/install-pr
@@ -102,7 +102,7 @@ then
   then
     distro="kind"
   fi
-  "$bindir"/image-load --"$distro" --archive "$cluster"
+  "$bindir"/image-load --"$distro" --archive --cluster "$cluster"
 else
   for image in cni-plugin controller debug grafana proxy web
   do
diff --git a/cli/cmd/check.go b/cli/cmd/check.go
index c2edae1c1..defc6ae7a 100644
--- a/cli/cmd/check.go
+++ b/cli/cmd/check.go
@@ -11,6 +11,7 @@ import (
 
 	"github.com/linkerd/linkerd2/cli/flag"
 	charts "github.com/linkerd/linkerd2/pkg/charts/linkerd2"
+	pkgcmd "github.com/linkerd/linkerd2/pkg/cmd"
 	"github.com/linkerd/linkerd2/pkg/healthcheck"
 	"github.com/linkerd/linkerd2/pkg/k8s"
 	"github.com/linkerd/linkerd2/pkg/version"
@@ -139,6 +140,9 @@ non-zero exit code.`,
 
 	cmd.AddCommand(newCmdCheckConfig(options))
 
+	pkgcmd.ConfigureNamespaceFlagCompletion(cmd, []string{"namespace"},
+		kubeconfigPath, impersonate, impersonateGroup, kubeContext)
+
 	return cmd
 }
 
diff --git a/cli/cmd/endpoints.go b/cli/cmd/endpoints.go
index 09977f4bf..30cd5716f 100644
--- a/cli/cmd/endpoints.go
+++ b/cli/cmd/endpoints.go
@@ -17,6 +17,7 @@ import (
 	destinationPb "github.com/linkerd/linkerd2-proxy-api/go/destination"
 	netPb "github.com/linkerd/linkerd2-proxy-api/go/net"
 	"github.com/linkerd/linkerd2/controller/api/destination"
+	pkgcmd "github.com/linkerd/linkerd2/pkg/cmd"
 	"github.com/linkerd/linkerd2/pkg/k8s"
 	log "github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
@@ -116,6 +117,8 @@ destination.`,
 
 	cmd.PersistentFlags().StringVarP(&options.outputFormat, "output", "o", options.outputFormat, fmt.Sprintf("Output format; one of: \"%s\" or \"%s\"", tableOutput, jsonOutput))
 
+	pkgcmd.ConfigureOutputFlagCompletion(cmd)
+
 	return cmd
 }
 
diff --git a/cli/cmd/identity.go b/cli/cmd/identity.go
index 84a32750e..3b4724efd 100644
--- a/cli/cmd/identity.go
+++ b/cli/cmd/identity.go
@@ -47,16 +47,38 @@ func newCmdIdentity() *cobra.Command {
 		Use:   "identity [flags] (PODS)",
 		Short: "Display the certificate(s) of one or more selected pod(s)",
 		Long: `Display the certificate(s) of one or more selected pod(s).
-		
+
 This command initiates a port-forward to a given pod or a set of pods and fetches the TLS certificate.
 		`,
-		Example: ` 
+		Example: `
  # Get certificate from pod foo-bar in the default namespace.
  linkerd identity foo-bar
-		
+
  # Get certificate from all pods with the label name=nginx
  linkerd identity -l name=nginx
 		`,
+		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+			k8sAPI, err := k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, impersonateGroup, 0)
+			if err != nil {
+				return nil, cobra.ShellCompDirectiveError
+			}
+
+			if options.namespace == "" {
+				options.namespace = pkgcmd.GetDefaultNamespace(kubeconfigPath, kubeContext)
+			}
+
+			cc := k8s.NewCommandCompletion(k8sAPI, options.namespace)
+
+			// insert pod resource type as first argument to suggest
+			// pod resources only
+			args = append([]string{k8s.Pod}, args...)
+			results, err := cc.Complete(args, toComplete)
+			if err != nil {
+				return nil, cobra.ShellCompDirectiveError
+			}
+
+			return results, cobra.ShellCompDirectiveDefault
+		},
 		RunE: func(cmd *cobra.Command, args []string) error {
 			if options.namespace == "" {
 				options.namespace = pkgcmd.GetDefaultNamespace(kubeconfigPath, kubeContext)
@@ -104,6 +126,10 @@ This command initiates a port-forward to a given pod or a set of pods and fetche
 
 	cmd.PersistentFlags().StringVarP(&options.namespace, "namespace", "n", options.namespace, "Namespace of the pod")
 	cmd.PersistentFlags().StringVarP(&options.selector, "selector", "l", options.selector, "Selector (label query) to filter on, supports ‘=’, ‘==’, and ‘!=’ ")
+
+	pkgcmd.ConfigureNamespaceFlagCompletion(cmd, []string{"namespace"},
+		kubeconfigPath, impersonate, impersonateGroup, kubeContext)
+
 	return cmd
 }
 
diff --git a/cli/cmd/metrics.go b/cli/cmd/metrics.go
index 37f0609cd..362c9379e 100644
--- a/cli/cmd/metrics.go
+++ b/cli/cmd/metrics.go
@@ -108,6 +108,9 @@ func newCmdMetrics() *cobra.Command {
 
 	cmd.PersistentFlags().StringVarP(&options.namespace, "namespace", "n", options.namespace, "Namespace of resource")
 
+	pkgcmd.ConfigureNamespaceFlagCompletion(cmd, []string{"namespace"},
+		kubeconfigPath, impersonate, impersonateGroup, kubeContext)
+
 	return cmd
 }
 
diff --git a/cli/cmd/root.go b/cli/cmd/root.go
index 0f80c78c4..05f834170 100644
--- a/cli/cmd/root.go
+++ b/cli/cmd/root.go
@@ -10,6 +10,7 @@ import (
 	"github.com/linkerd/linkerd2/cli/flag"
 	jaeger "github.com/linkerd/linkerd2/jaeger/cmd"
 	multicluster "github.com/linkerd/linkerd2/multicluster/cmd"
+	pkgcmd "github.com/linkerd/linkerd2/pkg/cmd"
 	"github.com/linkerd/linkerd2/pkg/healthcheck"
 	viz "github.com/linkerd/linkerd2/viz/cmd"
 	log "github.com/sirupsen/logrus"
@@ -128,6 +129,13 @@ func init() {
 	RootCmd.AddCommand(deprecateCmd(viz.NewCmdStat()))
 	RootCmd.AddCommand(deprecateCmd(viz.NewCmdTap()))
 	RootCmd.AddCommand(deprecateCmd(viz.NewCmdTop()))
+
+	// resource-aware completion flag configurations
+	pkgcmd.ConfigureNamespaceFlagCompletion(
+		RootCmd, []string{"linkerd-namespace", "cni-namespace"},
+		kubeconfigPath, impersonate, impersonateGroup, kubeContext)
+
+	pkgcmd.ConfigureKubeContextFlagCompletion(RootCmd, kubeconfigPath)
 }
 
 func deprecateCmd(cmd *cobra.Command) *cobra.Command {
diff --git a/controller/api/destination/server.go b/controller/api/destination/server.go
index 535be5167..3418fdc7c 100644
--- a/controller/api/destination/server.go
+++ b/controller/api/destination/server.go
@@ -216,9 +216,6 @@ func (s *server) GetProfile(dest *pb.GetDestination, stream pb.Destination_GetPr
 					Namespace: pod.Namespace,
 					Name:      pod.Name,
 				}
-				if err != nil {
-					log.Errorf("failed to set opaque port annotation on pod: %s", err)
-				}
 				var ok bool
 				opaquePorts, ok, err = getPodOpaquePortsAnnotations(pod)
 				if err != nil {
diff --git a/jaeger/cmd/root.go b/jaeger/cmd/root.go
index d22cd8b29..d9b5d6305 100644
--- a/jaeger/cmd/root.go
+++ b/jaeger/cmd/root.go
@@ -6,6 +6,7 @@ import (
 	"regexp"
 
 	"github.com/fatih/color"
+	pkgcmd "github.com/linkerd/linkerd2/pkg/cmd"
 	"github.com/linkerd/linkerd2/pkg/healthcheck"
 	log "github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
@@ -69,6 +70,12 @@ func NewCmdJaeger() *cobra.Command {
 	jaegerCmd.AddCommand(newCmdList())
 	jaegerCmd.AddCommand(newCmdUninstall())
 
+	// resource-aware completion flag configurations
+	pkgcmd.ConfigureNamespaceFlagCompletion(
+		jaegerCmd, []string{"linkerd-namespace"},
+		kubeconfigPath, impersonate, impersonateGroup, kubeContext)
+
+	pkgcmd.ConfigureKubeContextFlagCompletion(jaegerCmd, kubeconfigPath)
 	return jaegerCmd
 }
 
diff --git a/multicluster/cmd/root.go b/multicluster/cmd/root.go
index 65026d5f1..24f7b5e7c 100644
--- a/multicluster/cmd/root.go
+++ b/multicluster/cmd/root.go
@@ -6,6 +6,7 @@ import (
 
 	"github.com/fatih/color"
 	"github.com/linkerd/linkerd2/pkg/charts/linkerd2"
+	pkgcmd "github.com/linkerd/linkerd2/pkg/cmd"
 	"github.com/linkerd/linkerd2/pkg/healthcheck"
 	"github.com/linkerd/linkerd2/pkg/k8s"
 	log "github.com/sirupsen/logrus"
@@ -85,6 +86,13 @@ components on a cluster, manage credentials and link clusters together.`,
 	multiclusterCmd.AddCommand(newMulticlusterUninstallCommand())
 	multiclusterCmd.AddCommand(newGatewaysCommand())
 	multiclusterCmd.AddCommand(newAllowCommand())
+
+	// resource-aware completion flag configurations
+	pkgcmd.ConfigureNamespaceFlagCompletion(
+		multiclusterCmd, []string{"linkerd-namespace"},
+		kubeconfigPath, impersonate, impersonateGroup, kubeContext)
+
+	pkgcmd.ConfigureKubeContextFlagCompletion(multiclusterCmd, kubeconfigPath)
 	return multiclusterCmd
 }
 
diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go
index ad9ab513b..e7862cdee 100644
--- a/pkg/cmd/cmd.go
+++ b/pkg/cmd/cmd.go
@@ -5,6 +5,7 @@ import (
 	"errors"
 	"fmt"
 	"os"
+	"strings"
 
 	"github.com/linkerd/linkerd2/pkg/k8s"
 	"github.com/linkerd/linkerd2/pkg/k8s/resource"
@@ -95,3 +96,32 @@ func ConfigureOutputFlagCompletion(cmd *cobra.Command) {
 			return []string{"basic", "json", "short", "table"}, cobra.ShellCompDirectiveDefault
 		})
 }
+
+// ConfigureKubeContextFlagCompletion sets up resource-aware completion for command
+// flags based off of a kubeconfig
+func ConfigureKubeContextFlagCompletion(cmd *cobra.Command, kubeconfigPath string) {
+	cmd.RegisterFlagCompletionFunc("context",
+		func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+			rules := clientcmd.NewDefaultClientConfigLoadingRules()
+			rules.ExplicitPath = kubeconfigPath
+			loader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, &clientcmd.ConfigOverrides{})
+			config, err := loader.RawConfig()
+			if err != nil {
+				return nil, cobra.ShellCompDirectiveError
+			}
+
+			suggestions := []string{}
+			uniqClusters := map[string]struct{}{}
+			for _, ctx := range config.Contexts {
+				clusterName := ctx.Cluster
+				if strings.HasPrefix(clusterName, toComplete) {
+					if _, ok := uniqClusters[clusterName]; !ok {
+						suggestions = append(suggestions, ctx.Cluster)
+						uniqClusters[clusterName] = struct{}{}
+					}
+				}
+			}
+
+			return suggestions, cobra.ShellCompDirectiveDefault
+		})
+}
diff --git a/pkg/healthcheck/healthcheck.go b/pkg/healthcheck/healthcheck.go
index 0664798f8..a94beccb7 100644
--- a/pkg/healthcheck/healthcheck.go
+++ b/pkg/healthcheck/healthcheck.go
@@ -1429,7 +1429,8 @@ func (hc *HealthChecker) CheckProxyVersionsUpToDate(pods []corev1.Pod) error {
 func CheckProxyVersionsUpToDate(pods []corev1.Pod, versions version.Channels) error {
 	outdatedPods := []string{}
 	for _, pod := range pods {
-		if containsProxy(pod) {
+		status := k8s.GetPodStatus(pod)
+		if status == string(corev1.PodRunning) && containsProxy(pod) {
 			proxyVersion := k8s.GetProxyVersion(pod)
 			if err := versions.Match(proxyVersion); err != nil {
 				outdatedPods = append(outdatedPods, fmt.Sprintf("\t* %s (%s)", pod.Name, proxyVersion))
@@ -2453,7 +2454,7 @@ func getPodStatuses(pods []corev1.Pod) map[string]map[string][]corev1.ContainerS
 func validateControlPlanePods(pods []corev1.Pod) error {
 	statuses := getPodStatuses(pods)
 
-	names := []string{"identity", "proxy-injector"}
+	names := []string{"destination", "identity", "proxy-injector"}
 
 	for _, name := range names {
 		pods, found := statuses[name]
@@ -2506,7 +2507,7 @@ func validateDataPlanePods(pods []corev1.Pod, targetNamespace string) error {
 			continue
 		}
 
-		if status != "Running" && status != "Evicted" {
+		if status != string(corev1.PodRunning) && status != "Evicted" {
 			return fmt.Errorf("The \"%s\" pod is not running", pod.Name)
 		}
 
@@ -2580,7 +2581,7 @@ func CheckPodsRunning(pods []corev1.Pod, podsNotFoundMsg string) error {
 		return fmt.Errorf(podsNotFoundMsg)
 	}
 	for _, pod := range pods {
-		if pod.Status.Phase != "Running" {
+		if pod.Status.Phase != corev1.PodRunning {
 			return fmt.Errorf("%s status is %s", pod.Name, pod.Status.Phase)
 		}
 
diff --git a/viz/cmd/root.go b/viz/cmd/root.go
index 59f24bc6b..94b4a3e83 100644
--- a/viz/cmd/root.go
+++ b/viz/cmd/root.go
@@ -5,6 +5,7 @@ import (
 	"regexp"
 
 	"github.com/fatih/color"
+	pkgcmd "github.com/linkerd/linkerd2/pkg/cmd"
 	"github.com/linkerd/linkerd2/pkg/healthcheck"
 	log "github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
@@ -14,6 +15,10 @@ const (
 	// ExtensionName is the value that the viz extension resources should be labeled with
 	ExtensionName = "viz"
 
+	// LegacyExtensionName is the value that the viz extension resources were labeled with
+	// until 15d1809bd043192bb21cacbc96112cce35bf384f
+	LegacyExtensionName = "linkerd-viz"
+
 	vizChartName            = "linkerd-viz"
 	defaultLinkerdNamespace = "linkerd"
 	maxRps                  = 100.0
@@ -83,5 +88,11 @@ func NewCmdViz() *cobra.Command {
 	vizCmd.AddCommand(NewCmdTop())
 	vizCmd.AddCommand(newCmdUninstall())
 
+	// resource-aware completion flag configurations
+	pkgcmd.ConfigureNamespaceFlagCompletion(
+		vizCmd, []string{"linkerd-namespace"},
+		kubeconfigPath, impersonate, impersonateGroup, kubeContext)
+
+	pkgcmd.ConfigureKubeContextFlagCompletion(vizCmd, kubeconfigPath)
 	return vizCmd
 }
diff --git a/viz/cmd/uninstall.go b/viz/cmd/uninstall.go
index a882b4f58..6a5fb8b7f 100644
--- a/viz/cmd/uninstall.go
+++ b/viz/cmd/uninstall.go
@@ -8,6 +8,8 @@ import (
 	pkgCmd "github.com/linkerd/linkerd2/pkg/cmd"
 	"github.com/linkerd/linkerd2/pkg/k8s"
 	"github.com/spf13/cobra"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/apimachinery/pkg/selection"
 )
 
 func newCmdUninstall() *cobra.Command {
@@ -38,5 +40,11 @@ func uninstallRunE(ctx context.Context) error {
 		return err
 	}
 
-	return pkgCmd.Uninstall(ctx, k8sAPI, fmt.Sprintf("%s=%s", k8s.LinkerdExtensionLabel, ExtensionName))
+	extensionReq, err := labels.NewRequirement(k8s.LinkerdExtensionLabel, selection.In, []string{ExtensionName, LegacyExtensionName})
+	if err != nil {
+		return err
+	}
+
+	selector := labels.NewSelector().Add(*extensionReq)
+	return pkgCmd.Uninstall(ctx, k8sAPI, selector.String())
 }


Test patch:
diff --git a/bin/_test-helpers.sh b/bin/_test-helpers.sh
index 2be4a4c10..49aca4763 100644
--- a/bin/_test-helpers.sh
+++ b/bin/_test-helpers.sh
@@ -9,6 +9,7 @@ set +e
 export default_test_names=(deep external-issuer external-prometheus-deep helm-deep helm-upgrade uninstall upgrade-edge upgrade-stable)
 export external_resource_test_names=(external-resources)
 export all_test_names=(cluster-domain cni-calico-deep multicluster "${default_test_names[*]}" "${external_resource_test_names[*]}")
+images_load_default=(proxy controller web metrics-api grafana tap)
 
 tests_usage() {
   progname="${0##*/}"
@@ -39,7 +40,10 @@ Available Commands:
     --name: the argument to this option is the specific test to run
     --skip-cluster-create: skip k3d cluster creation step and run tests in an existing cluster
     --skip-cluster-delete: if the tests succeed, don't delete the created resources nor the cluster
-    --images: by default load images into the cluster from the local docker cache (docker), or from tar files located under the 'image-archives' directory (archive), or completely skip image loading (skip)
+    --images: set to 'docker' (default) to load images into the cluster from the local docker cache;
+      set to 'preload' to also load them from the local docker cache, after having pulled them from
+      a public registry (appears to be faster than having k3d pulling them itself);
+      set to 'archive' to load the images from tar files located under the image-archives directory
     --cleanup-docker: delete the 'images-archive' directory and prune the docker cache"
 }
 
@@ -79,7 +83,7 @@ handle_tests_input() {
           tests_usage "$0" >&2
           exit 64
         fi
-        if [[ $images != "docker" && $images != "archive" && $images != "skip" ]]; then
+        if [[ $images != "docker" && $images != "archive" && $images != "preload" ]]; then
           echo 'Error: the argument for --images was invalid' >&2
           tests_usage "$0" >&2
           exit 64
@@ -275,13 +279,24 @@ Help:
 
 image_load() {
   cluster_name=$1
+  images_load=("${images_load_default[@]}")
+  if [[ "$cluster_name" = *deep ]]; then
+    images_load+=(jaeger-webhook)
+  fi
+  if [ "$cluster_name" = "cni-calico-deep" ]; then
+    images_load+=(cni-plugin)
+  fi
   case $images in
     docker)
-      "$bindir"/image-load --k3d "$cluster_name"
+      "$bindir"/image-load --k3d --cluster "$cluster_name" "${images_load[@]}"
+      exit_on_err "error calling '$bindir/image-load'"
+      ;;
+    preload)
+      "$bindir"/image-load --k3d --cluster "$cluster_name" --preload "${images_load[@]}"
       exit_on_err "error calling '$bindir/image-load'"
       ;;
     archive)
-      "$bindir"/image-load --k3d --archive "$cluster_name"
+      "$bindir"/image-load --k3d --archive --cluster "$cluster_name" "${images_load[@]}"
       exit_on_err "error calling '$bindir/image-load'"
       ;;
   esac
diff --git a/pkg/healthcheck/healthcheck_test.go b/pkg/healthcheck/healthcheck_test.go
index ee3c9ae8f..ec2566248 100644
--- a/pkg/healthcheck/healthcheck_test.go
+++ b/pkg/healthcheck/healthcheck_test.go
@@ -1323,6 +1323,7 @@ func TestValidateControlPlanePods(t *testing.T) {
 	t.Run("Returns an error if not all pods are running", func(t *testing.T) {
 		pods := []corev1.Pod{
 			pod("linkerd-grafana-5b7d796646-hh46d", corev1.PodRunning, true),
+			pod("linkerd-destination-9849948665-37082", corev1.PodFailed, true),
 			pod("linkerd-identity-6849948664-27982", corev1.PodFailed, true),
 		}
 
@@ -1330,7 +1331,7 @@ func TestValidateControlPlanePods(t *testing.T) {
 		if err == nil {
 			t.Fatal("Expected error, got nothing")
 		}
-		if err.Error() != "No running pods for \"linkerd-identity\"" {
+		if err.Error() != "No running pods for \"linkerd-destination\"" {
 			t.Fatalf("Unexpected error message: %s", err.Error())
 		}
 	})
@@ -1349,6 +1350,7 @@ func TestValidateControlPlanePods(t *testing.T) {
 
 	t.Run("Returns nil if all pods are running and all containers are ready", func(t *testing.T) {
 		pods := []corev1.Pod{
+			pod("linkerd-destination-9849948665-37082", corev1.PodRunning, true),
 			pod("linkerd-identity-6849948664-27982", corev1.PodRunning, true),
 			pod("linkerd-proxy-injector-5f79ff4844-", corev1.PodRunning, true),
 		}
@@ -1361,6 +1363,9 @@ func TestValidateControlPlanePods(t *testing.T) {
 
 	t.Run("Returns nil if, HA mode, at least one pod of each control plane component is ready", func(t *testing.T) {
 		pods := []corev1.Pod{
+			pod("linkerd-destination-9843948665-48082", corev1.PodRunning, true),
+			pod("linkerd-destination-9843948665-48083", corev1.PodRunning, false),
+			pod("linkerd-destination-9843948665-48084", corev1.PodFailed, false),
 			pod("linkerd-identity-6849948664-27982", corev1.PodRunning, true),
 			pod("linkerd-identity-6849948664-27983", corev1.PodRunning, false),
 			pod("linkerd-identity-6849948664-27984", corev1.PodFailed, false),
@@ -1375,6 +1380,7 @@ func TestValidateControlPlanePods(t *testing.T) {
 
 	t.Run("Returns nil if all linkerd pods are running and pod list includes non-linkerd pod", func(t *testing.T) {
 		pods := []corev1.Pod{
+			pod("linkerd-destination-9843948665-48082", corev1.PodRunning, true),
 			pod("linkerd-identity-6849948664-27982", corev1.PodRunning, true),
 			pod("linkerd-proxy-injector-5f79ff4844-", corev1.PodRunning, true),
 			pod("hello-43c25d", corev1.PodRunning, true),
diff --git a/test/integration/install_test.go b/test/integration/install_test.go
index dc6321823..31762b9e8 100644
--- a/test/integration/install_test.go
+++ b/test/integration/install_test.go
@@ -430,7 +430,7 @@ func TestInstallOrUpgradeCli(t *testing.T) {
 			"'kubectl apply' command failed\n%s", cmdOut)
 	}
 
-	TestHelper.WaitRollout(t)
+	TestHelper.WaitRollout(t, testutil.LinkerdDeployReplicasEdge)
 
 	if TestHelper.ExternalPrometheus() {
 
@@ -462,6 +462,17 @@ func TestInstallOrUpgradeCli(t *testing.T) {
 		testutil.AnnotatedFatalf(t, "'kubectl apply' command failed",
 			"'kubectl apply' command failed\n%s", out)
 	}
+
+	// It is necessary to clone LinkerdVizDeployReplicas so that we do not
+	// mutate it's original value.
+	expectedVizDeployments := make(map[string]testutil.DeploySpec)
+	for k, v := range testutil.LinkerdVizDeployReplicas {
+		expectedVizDeployments[k] = v
+	}
+	if TestHelper.ExternalPrometheus() {
+		delete(expectedVizDeployments, "prometheus")
+	}
+	TestHelper.WaitRollout(t, expectedVizDeployments)
 }
 
 // These need to be updated (if there are changes) once a new stable is released
@@ -535,13 +546,23 @@ func TestInstallHelm(t *testing.T) {
 		testutil.AnnotatedFatalf(t, "'helm install' command failed",
 			"'helm install' command failed\n%s\n%s", stdout, stderr)
 	}
-
-	TestHelper.WaitRollout(t)
+	TestHelper.WaitRollout(t, testutil.LinkerdDeployReplicasEdge)
 
 	if stdout, stderr, err := TestHelper.HelmCmdPlain("install", vizChartToInstall, "l5d-viz", vizArgs...); err != nil {
 		testutil.AnnotatedFatalf(t, "'helm install' command failed",
 			"'helm install' command failed\n%s\n%s", stdout, stderr)
 	}
+
+	// It is necessary to clone LinkerdVizDeployReplicas so that we do not
+	// mutate it's original value.
+	expectedVizDeployments := make(map[string]testutil.DeploySpec)
+	for k, v := range testutil.LinkerdVizDeployReplicas {
+		expectedVizDeployments[k] = v
+	}
+	if TestHelper.ExternalPrometheus() {
+		delete(expectedVizDeployments, "prometheus")
+	}
+	TestHelper.WaitRollout(t, expectedVizDeployments)
 }
 
 func TestControlPlaneResourcesPostInstall(t *testing.T) {
@@ -637,12 +658,25 @@ func TestUpgradeHelm(t *testing.T) {
 		testutil.AnnotatedFatalf(t, "'helm upgrade' command failed",
 			"'helm upgrade' command failed\n%s\n%s", stdout, stderr)
 	}
+	TestHelper.WaitRollout(t, testutil.LinkerdDeployReplicasEdge)
 
 	vizChart := TestHelper.GetLinkerdVizHelmChart()
 	if stdout, stderr, err := TestHelper.HelmCmdPlain("upgrade", vizChart, "l5d-viz", vizArgs...); err != nil {
 		testutil.AnnotatedFatalf(t, "'helm upgrade' command failed",
 			"'helm upgrade' command failed\n%s\n%s", stdout, stderr)
 	}
+
+	// It is necessary to clone LinkerdVizDeployReplicas so that we do not
+	// mutate it's original value.
+	expectedVizDeployments := make(map[string]testutil.DeploySpec)
+	for k, v := range testutil.LinkerdVizDeployReplicas {
+		expectedVizDeployments[k] = v
+	}
+	if TestHelper.ExternalPrometheus() {
+		delete(expectedVizDeployments, "prometheus")
+	}
+	TestHelper.WaitRollout(t, expectedVizDeployments)
+
 	TestHelper.AddInstalledExtension(vizExtensionName)
 }
 
@@ -943,12 +977,18 @@ func getCheckOutput(t *testing.T, goldenFile string, namespace string) string {
 		testutil.AnnotatedFatal(t, fmt.Sprintf("failed to retrieve pods: %s", err), err)
 	}
 
+	proxyVersionErr := ""
+	err = healthcheck.CheckProxyVersionsUpToDate(pods, version.Channels{})
+	if err != nil {
+		proxyVersionErr = err.Error()
+	}
+
 	tpl := template.Must(template.ParseFiles("testdata" + "/" + goldenFile))
 	vars := struct {
 		ProxyVersionErr string
 		HintURL         string
 	}{
-		healthcheck.CheckProxyVersionsUpToDate(pods, version.Channels{}).Error(),
+		proxyVersionErr,
 		healthcheck.HintBaseURL(TestHelper.GetVersion()),
 	}
 
diff --git a/testutil/kubernetes_helper.go b/testutil/kubernetes_helper.go
index 8953c7b04..64d518f53 100644
--- a/testutil/kubernetes_helper.go
+++ b/testutil/kubernetes_helper.go
@@ -320,17 +320,15 @@ func (h *KubernetesHelper) URLFor(ctx context.Context, namespace, deployName str
 	return pf.URLFor(""), nil
 }
 
-// WaitRollout blocks until all the deployments in the linkerd namespace have been
-// completely rolled out (and their pods are ready)
-func (h *KubernetesHelper) WaitRollout(t *testing.T) {
-	for deploy, deploySpec := range LinkerdDeployReplicasEdge {
-		if deploySpec.Namespace == "linkerd" {
-			o, err := h.Kubectl("", "--namespace=linkerd", "rollout", "status", "--timeout=60m", "deploy/"+deploy)
-			if err != nil {
-				AnnotatedFatalf(t,
-					fmt.Sprintf("failed to wait rollout of deploy/%s", deploy),
-					"failed to wait for rollout of deploy/%s: %s: %s", deploy, err, o)
-			}
+// WaitRollout blocks until all the given deployments have been completely
+// rolled out (and their pods are ready)
+func (h *KubernetesHelper) WaitRollout(t *testing.T, deploys map[string]DeploySpec) {
+	for deploy, deploySpec := range deploys {
+		o, err := h.Kubectl("", "--namespace="+deploySpec.Namespace, "rollout", "status", "--timeout=60m", "deploy/"+deploy)
+		if err != nil {
+			AnnotatedFatalf(t,
+				fmt.Sprintf("failed to wait rollout of deploy/%s", deploy),
+				"failed to wait for rollout of deploy/%s: %s: %s", deploy, err, o)
 		}
 	}
 }
diff --git a/testutil/test_helper.go b/testutil/test_helper.go
index 899be08cb..10073ff29 100644
--- a/testutil/test_helper.go
+++ b/testutil/test_helper.go
@@ -67,13 +67,10 @@ type Service struct {
 }
 
 // LinkerdDeployReplicasEdge is a map containing the number of replicas for each Deployment and the main
-// container name, in the current code-base
+// container name in the current core installation
 var LinkerdDeployReplicasEdge = map[string]DeploySpec{
 	"linkerd-destination":    {"linkerd", 1},
-	"tap":                    {"linkerd-viz", 1},
-	"grafana":                {"linkerd-viz", 1},
 	"linkerd-identity":       {"linkerd", 1},
-	"web":                    {"linkerd-viz", 1},
 	"linkerd-proxy-injector": {"linkerd", 1},
 }
 
@@ -81,6 +78,18 @@ var LinkerdDeployReplicasEdge = map[string]DeploySpec{
 // container name. Override whenever edge deviates from stable.
 var LinkerdDeployReplicasStable = LinkerdDeployReplicasEdge
 
+// LinkerdVizDeployReplicas is a map containing the number of replicas for
+// each Deployment and the main container name in the current linkerd-viz
+// installation
+var LinkerdVizDeployReplicas = map[string]DeploySpec{
+	"prometheus":   {"linkerd-viz", 1},
+	"metrics-api":  {"linkerd-viz", 1},
+	"grafana":      {"linkerd-viz", 1},
+	"tap":          {"linkerd-viz", 1},
+	"tap-injector": {"linkerd-viz", 1},
+	"web":          {"linkerd-viz", 1},
+}
+
 // NewGenericTestHelper returns a new *TestHelper from the options provided as function parameters.
 // This helper was created to be able to reuse this package without hard restrictions
 // as seen in `NewTestHelper()` which is primarily used with integration tests