Repository: linkerd/linkerd2
Pull request #5353 (2020-12-09 00:41:06)

Add jaeger uninstall command
Add a `linkerd jaeger uninstall` command which prints the linkerd-jaeger extension resources so that they can be deleted.  This is similar to the `linkerd uninstall` command.

```
> bin/linkerd jaeger uninstall | k delete -f -
clusterrole.rbac.authorization.k8s.io "linkerd-jaeger-linkerd-jaeger-proxy-mutator" deleted
clusterrolebinding.rbac.authorization.k8s.io "linkerd-jaeger-linkerd-jaeger-proxy-mutator" deleted
mutatingwebhookconfiguration.admissionregistration.k8s.io "linkerd-proxy-mutator-webhook-config" deleted
namespace "linkerd-jaeger" deleted
```


Patch:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 37c7949a6..2bea35bd4 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -11,7 +11,8 @@ jobs:
     runs-on: ubuntu-18.04
     strategy:
       matrix:
-        target: [proxy, controller, web, cni-plugin, debug, cli-bin, grafana]
+        # Keep in sync with integration_tests.yaml matrix build
+        target: [proxy, controller, web, cni-plugin, debug, cli-bin, grafana, jaeger-webhook]
     name: Docker build (${{ matrix.target }})
     timeout-minutes: 30
     steps:
diff --git a/.github/workflows/static_checks.yml b/.github/workflows/static_checks.yml
index 8d2ad93f8..2056c5367 100644
--- a/.github/workflows/static_checks.yml
+++ b/.github/workflows/static_checks.yml
@@ -39,6 +39,8 @@ jobs:
     container:
       image: golang:1.13.4
     steps:
+      - name: Prerequisites
+        run: apt-get update && DEBIAN_FRONTEND=noninteractive apt-get -y --no-install-recommends install unzip
       - name: Checkout code
         # actions/checkout@v2
         uses: actions/checkout@722adc6
diff --git a/CHANGES.md b/CHANGES.md
index a141bc4be..27e167c9f 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,6 +1,31 @@
 
 # Changes
 
+## edge-20.12.3
+
+This edge release is functionally the same as `edge-20.12.2`. It fixes an issue
+that prevented the release build from occurring.
+
+## edge-20.12.2
+
+* Fixed an issue where the `proxy-injector` and `sp-validator` did not refresh
+  their certs automatically when provided externally—like through cert-manager
+* Added support for overrides flags to the `jaeger install` command to allow
+  setting Helm values when installing the Linkerd-jaeger extension
+* Added missing Helm values to the multicluster chart (thanks @DaspawnW!)
+* Moved tracing functionality to the `linkerd-jaeger` extension
+* Fixed various issues in developer shell scripts (thanks @joakimr-axis!)
+* Fixed an issue where `install --ha` was only partially applying the high
+  availability config
+* Updated RBAC API versions in the CNI chart (thanks @glitchcrab!)
+* Fixed an issue where TLS credentials are changed during upgrades, but the
+  Linkerd webhooks would not restart, leaving them to use older credentials and
+  fail requests
+* Stopped publishing the multicluster link chart as its primary use case is in
+  the `multicluster link` command and not being installed through Helm
+* Added service mirror error logs for when the multicluster gateway's hostname
+  cannot be resolved.
+
 ## edge-20.12.1
 
 This edge release continues the work of decoupling non-core Linkerd components
diff --git a/bin/helm-build b/bin/helm-build
index be15a954e..e8ecab1d0 100755
--- a/bin/helm-build
+++ b/bin/helm-build
@@ -54,7 +54,6 @@ if [ "$1" = package ]; then
     "$bindir"/helm --version "$version" --app-version "$tag" -d "$rootdir"/target/helm package "$rootdir"/charts/linkerd2
     "$bindir"/helm --version "$version" --app-version "$tag" -d "$rootdir"/target/helm package "$rootdir"/charts/linkerd2-cni
     "$bindir"/helm --version "$version" --app-version "$tag" -d "$rootdir"/target/helm package "$rootdir"/multicluster/charts/linkerd2-multicluster
-    "$bindir"/helm --version "$version" --app-version "$tag" -d "$rootdir"/target/helm package "$rootdir"/multicluster/charts/linkerd2-multicluster-link
     "$bindir"/helm --version "$version" --app-version "$tag" -d "$rootdir"/target/helm package "$rootdir"/jaeger/charts/jaeger
     mv "$rootdir"/target/helm/index-pre.yaml "$rootdir"/target/helm/index-pre-"$version".yaml
     "$bindir"/helm repo index --url "https://helm.linkerd.io/$repo/" --merge "$rootdir"/target/helm/index-pre-"$version".yaml "$rootdir"/target/helm
diff --git a/bin/protoc b/bin/protoc
index 5efadcad5..0939a3f55 100755
--- a/bin/protoc
+++ b/bin/protoc
@@ -15,7 +15,16 @@ protocversion=3.6.0
 protocbin=$targetbin/protoc-$protocversion
 protocurl=https://github.com/google/protobuf/releases/download/v$protocversion/protoc-$protocversion-$os-$arch.zip
 
+require_from() {
+    if ! command -v "$1" >/dev/null 2>/dev/null ; then
+        echo "Please acquire $1 from $2" >&2
+        return 1
+    fi
+}
+
 if [ ! -f "$protocbin" ]; then
+  require_from curl 'https://curl.se/download.html'
+  require_from unzip 'http://infozip.sourceforge.net/UnZip.html#Downloads'
   tmp=$(mktemp -d -t protoc.XXX)
   mkdir -p "$targetbin"
   (
diff --git a/bin/protoc-diff b/bin/protoc-diff
index f0812dbd5..b0a33572a 100755
--- a/bin/protoc-diff
+++ b/bin/protoc-diff
@@ -2,14 +2,8 @@
 
 set -eu
 
-# Install unzip if not already installed (Linux only)
-system=$(uname -s)
-if [ "$system" = Linux ] && ! [ -x "$(command -v unzip)" ]; then
-  apt-get update
-  apt-get install unzip
-fi
-
-bin/protoc-go.sh
+bindir=$( cd "${0%/*}" && pwd )
+"$bindir"/protoc-go.sh
 
 dir_dirty=$(git diff HEAD)
 if [ -z "$dir_dirty" ]; then
diff --git a/bin/protoc-go.sh b/bin/protoc-go.sh
index ab960bd68..6879b35a1 100755
--- a/bin/protoc-go.sh
+++ b/bin/protoc-go.sh
@@ -2,9 +2,11 @@
 
 set -eu
 
+bindir=$( cd "${0%/*}" && pwd )
+
 gen() {
     for f in "$@"; do
-        bin/protoc -I proto --go_out=plugins=grpc,paths=source_relative:controller/gen "$f"
+        "$bindir"/protoc -I proto --go_out=plugins=grpc,paths=source_relative:controller/gen "$f"
     done
 }
 
diff --git a/charts/linkerd2-cni/templates/cni-plugin.yaml b/charts/linkerd2-cni/templates/cni-plugin.yaml
index d600f20b9..68e15a254 100644
--- a/charts/linkerd2-cni/templates/cni-plugin.yaml
+++ b/charts/linkerd2-cni/templates/cni-plugin.yaml
@@ -93,7 +93,7 @@ subjects:
   namespace: {{.Values.namespace}}
 ---
 kind: ClusterRole
-apiVersion: rbac.authorization.k8s.io/v1beta1
+apiVersion: rbac.authorization.k8s.io/v1
 metadata:
   name: linkerd-cni
   labels:
@@ -103,7 +103,7 @@ rules:
   resources: ["pods", "nodes", "namespaces"]
   verbs: ["list", "get", "watch"]
 ---
-apiVersion: rbac.authorization.k8s.io/v1beta1
+apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
 metadata:
   name: linkerd-cni
diff --git a/charts/linkerd2/templates/config.yaml b/charts/linkerd2/templates/config.yaml
index ef3eb0c1a..a49b162cf 100644
--- a/charts/linkerd2/templates/config.yaml
+++ b/charts/linkerd2/templates/config.yaml
@@ -30,4 +30,5 @@ data:
   {{- end -}}
   {{- $_ := unset $values "partials"}}
   {{- $_ := unset $values "configs"}}
+  {{- $_ := unset $values "stage"}}
   {{- toYaml $values | trim | nindent 4 }}
diff --git a/charts/linkerd2/templates/proxy-injector.yaml b/charts/linkerd2/templates/proxy-injector.yaml
index 640480a4f..21d0194d2 100644
--- a/charts/linkerd2/templates/proxy-injector.yaml
+++ b/charts/linkerd2/templates/proxy-injector.yaml
@@ -33,7 +33,7 @@ spec:
   template:
     metadata:
       annotations:
-        {{- if empty .Values.global.cliVersion }}
+        {{- if (or (empty .Values.global.cliVersion) (not (eq (.Values.stage | toString) "control-plane"))) }}
         checksum/config: {{ include (print $.Template.BasePath "/proxy-injector-rbac.yaml") . | sha256sum }}
         {{- end }}
         {{.Values.global.createdByAnnotation}}: {{default (printf "linkerd/helm %s" .Values.global.linkerdVersion) .Values.global.cliVersion}}
diff --git a/charts/linkerd2/templates/sp-validator.yaml b/charts/linkerd2/templates/sp-validator.yaml
index b584b7ff9..d3723822c 100644
--- a/charts/linkerd2/templates/sp-validator.yaml
+++ b/charts/linkerd2/templates/sp-validator.yaml
@@ -52,7 +52,7 @@ spec:
   template:
     metadata:
       annotations:
-        {{- if empty .Values.global.cliVersion }}
+        {{- if (or (empty .Values.global.cliVersion) (not (eq (.Values.stage | toString) "control-plane"))) }}
         checksum/config: {{ include (print $.Template.BasePath "/sp-validator-rbac.yaml") . | sha256sum }}
         {{- end }}
         {{.Values.global.createdByAnnotation}}: {{default (printf "linkerd/helm %s" .Values.global.linkerdVersion) .Values.global.cliVersion}}
diff --git a/charts/linkerd2/templates/tap.yaml b/charts/linkerd2/templates/tap.yaml
index 7e5926653..02bb2237f 100644
--- a/charts/linkerd2/templates/tap.yaml
+++ b/charts/linkerd2/templates/tap.yaml
@@ -57,7 +57,7 @@ spec:
   template:
     metadata:
       annotations:
-        {{- if empty .Values.global.cliVersion }}
+        {{- if (or (empty .Values.global.cliVersion) (not (eq (.Values.stage | toString) "control-plane"))) }}
         checksum/config: {{ include (print $.Template.BasePath "/tap-rbac.yaml") . | sha256sum }}
         {{- end }}
         {{.Values.global.createdByAnnotation}}: {{default (printf "linkerd/helm %s" .Values.global.linkerdVersion) .Values.global.cliVersion}}
diff --git a/cli/cmd/install.go b/cli/cmd/install.go
index 0df846379..d0337867f 100644
--- a/cli/cmd/install.go
+++ b/cli/cmd/install.go
@@ -300,6 +300,7 @@ func render(w io.Writer, values *l5dcharts.Values, stage string) error {
 
 	// Set any global flags if present, common with install and upgrade
 	values.GetGlobal().Namespace = controlPlaneNamespace
+	values.Stage = stage
 
 	// Render raw values and create chart config
 	rawValues, err := yaml.Marshal(values)
diff --git a/cli/cmd/options.go b/cli/cmd/options.go
index 586e72dbd..05801b941 100644
--- a/cli/cmd/options.go
+++ b/cli/cmd/options.go
@@ -42,18 +42,14 @@ func makeInstallUpgradeFlags(defaults *l5dcharts.Values) ([]flag.Flag, *pflag.Fl
 	}
 
 	flags := []flag.Flag{
-		flag.NewUintFlag(installUpgradeFlags, "controller-replicas", defaults.ControllerReplicas,
-			"Replicas of the controller to deploy", func(values *l5dcharts.Values, value uint) error {
-				values.ControllerReplicas = value
-				return nil
-			}),
-
 		flag.NewStringFlag(installUpgradeFlags, "controller-log-level", defaults.GetGlobal().ControllerLogLevel,
 			"Log level for the controller and web components", func(values *l5dcharts.Values, value string) error {
 				values.GetGlobal().ControllerLogLevel = value
 				return nil
 			}),
 
+		// The HA flag must be processed before flags that set these values individually so that the
+		// individual flags can override the HA defaults.
 		flag.NewBoolFlag(installUpgradeFlags, "ha", false, "Enable HA deployment config for the control plane (default false)",
 			func(values *l5dcharts.Values, value bool) error {
 				values.GetGlobal().HighAvailability = value
@@ -62,20 +58,20 @@ func makeInstallUpgradeFlags(defaults *l5dcharts.Values) ([]flag.Flag, *pflag.Fl
 					if err != nil {
 						return err
 					}
-					// The HA flag must be processed before flags that set these values individually so that the
-					// individual flags can override the HA defaults.  This means that the HA flag must appear
-					// before the individual flags in the slice passed to flags.ApplyIfSet.
-					values.ControllerReplicas = haValues.ControllerReplicas
-					values.GetGlobal().Proxy.Resources.CPU.Request = haValues.GetGlobal().Proxy.Resources.CPU.Request
-					values.GetGlobal().Proxy.Resources.Memory.Request = haValues.GetGlobal().Proxy.Resources.Memory.Request
-					// NOTE: CPU Limits are not currently set by default HA charts.
-					//values.Global.Proxy.Cores = haValues.Global.Proxy.Cores
-					//values.Global.Proxy.Resources.CPU.Limit = haValues.Global.Proxy.Resources.CPU.Limit
-					values.GetGlobal().Proxy.Resources.Memory.Limit = haValues.GetGlobal().Proxy.Resources.Memory.Limit
+					*values, err = values.Merge(*haValues)
+					if err != nil {
+						return err
+					}
 				}
 				return nil
 			}),
 
+		flag.NewUintFlag(installUpgradeFlags, "controller-replicas", defaults.ControllerReplicas,
+			"Replicas of the controller to deploy", func(values *l5dcharts.Values, value uint) error {
+				values.ControllerReplicas = value
+				return nil
+			}),
+
 		flag.NewInt64Flag(installUpgradeFlags, "controller-uid", defaults.ControllerUID,
 			"Run the control plane components under this user ID", func(values *l5dcharts.Values, value int64) error {
 				values.ControllerUID = value
diff --git a/cli/cmd/uninstall.go b/cli/cmd/uninstall.go
index 5c4338aa3..e98a48828 100644
--- a/cli/cmd/uninstall.go
+++ b/cli/cmd/uninstall.go
@@ -8,15 +8,7 @@ import (
 	"github.com/linkerd/linkerd2/pkg/k8s"
 	"github.com/linkerd/linkerd2/pkg/k8s/resource"
 	"github.com/spf13/cobra"
-	admissionRegistration "k8s.io/api/admissionregistration/v1beta1"
-	core "k8s.io/api/core/v1"
-	policy "k8s.io/api/policy/v1beta1"
-	rbac "k8s.io/api/rbac/v1"
-	apiextension "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
-	kerrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	apiRegistration "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
-	apiregistrationv1client "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/typed/apiregistration/v1"
 )
 
 const (
@@ -46,7 +38,9 @@ func uninstallRunE(ctx context.Context) error {
 		return err
 	}
 
-	resources, err := fetchKubernetesResources(ctx, k8sAPI)
+	resources, err := resource.FetchKubernetesResources(ctx, k8sAPI,
+		metav1.ListOptions{LabelSelector: k8s.ControllerNSLabel},
+	)
 	if err != nil {
 		return err
 	}
@@ -58,202 +52,3 @@ func uninstallRunE(ctx context.Context) error {
 	}
 	return nil
 }
-
-func fetchKubernetesResources(ctx context.Context, k *k8s.KubernetesAPI) ([]resource.Kubernetes, error) {
-	options := metav1.ListOptions{
-		LabelSelector: k8s.ControllerNSLabel,
-	}
-
-	resources := make([]resource.Kubernetes, 0)
-
-	clusterRoles, err := fetchClusterRoles(ctx, k, options)
-	if err != nil {
-		return nil, fmt.Errorf("could not fetch ClusterRole resources:%v", err)
-	}
-	resources = append(resources, clusterRoles...)
-
-	clusterRoleBindings, err := fetchClusterRoleBindings(ctx, k, options)
-	if err != nil {
-		return nil, fmt.Errorf("could not fetch ClusterRoleBinding resources:%v", err)
-	}
-	resources = append(resources, clusterRoleBindings...)
-
-	roleBindings, err := fetchKubeSystemRoleBindings(ctx, k, options)
-	if err != nil {
-		return nil, fmt.Errorf("could not fetch RoleBindings from kube-system namespace:%v", err)
-	}
-	resources = append(resources, roleBindings...)
-
-	crds, err := fetchCustomResourceDefinitions(ctx, k, options)
-	if err != nil {
-		return nil, fmt.Errorf("could not fetch CustomResourceDefinition resources:%v", err)
-	}
-	resources = append(resources, crds...)
-
-	apiCRDs, err := fetchAPIRegistrationResources(ctx, k, options)
-	if err != nil {
-		return nil, fmt.Errorf("could not fetch APIService CRDs:%v", err)
-	}
-	resources = append(resources, apiCRDs...)
-
-	psps, err := fetchPodSecurityPolicy(ctx, k, options)
-	if err != nil {
-		return nil, fmt.Errorf("could not fetch PodSecurityPolicy resources:%v", err)
-	}
-	resources = append(resources, psps...)
-
-	mutatinghooks, err := fetchMutatingWebhooksConfiguration(ctx, k, options)
-	if err != nil {
-		return nil, fmt.Errorf("could not fetch MutatingWebhookConfigurations:%v", err)
-	}
-	resources = append(resources, mutatinghooks...)
-
-	validationhooks, err := fetchValidatingWebhooksConfiguration(ctx, k, options)
-	if err != nil {
-		return nil, fmt.Errorf("could not fetch ValidatingWebhookConfiguration:%v", err)
-	}
-	resources = append(resources, validationhooks...)
-
-	namespace, err := fetchNamespaceResource(ctx, k)
-	if err != nil {
-		return nil, fmt.Errorf("could not fetch Namespace %s:%v", controlPlaneNamespace, err)
-	}
-
-	if namespace.Name != "" {
-		resources = append(resources, namespace)
-	}
-
-	return resources, nil
-}
-
-func fetchClusterRoles(ctx context.Context, k *k8s.KubernetesAPI, options metav1.ListOptions) ([]resource.Kubernetes, error) {
-	list, err := k.RbacV1().ClusterRoles().List(ctx, options)
-	if err != nil {
-		return nil, err
-	}
-
-	resources := make([]resource.Kubernetes, len(list.Items))
-	for i, item := range list.Items {
-		resources[i] = resource.New(rbac.SchemeGroupVersion.String(), "ClusterRole", item.Name)
-	}
-
-	return resources, nil
-}
-
-func fetchClusterRoleBindings(ctx context.Context, k *k8s.KubernetesAPI, options metav1.ListOptions) ([]resource.Kubernetes, error) {
-	list, err := k.RbacV1().ClusterRoleBindings().List(ctx, options)
-	if err != nil {
-		return nil, err
-	}
-
-	resources := make([]resource.Kubernetes, len(list.Items))
-	for i, item := range list.Items {
-		resources[i] = resource.New(rbac.SchemeGroupVersion.String(), "ClusterRoleBinding", item.Name)
-	}
-
-	return resources, nil
-}
-
-// Although role bindings are namespaced resources in nature
-// some admin role bindings are created and persisted in the kube-system namespace and will not be deleted
-// when the namespace is deleted
-func fetchKubeSystemRoleBindings(ctx context.Context, k *k8s.KubernetesAPI, options metav1.ListOptions) ([]resource.Kubernetes, error) {
-	list, err := k.RbacV1().RoleBindings("kube-system").List(ctx, options)
-	if err != nil {
-		return nil, err
-	}
-
-	resources := make([]resource.Kubernetes, len(list.Items))
-	for i, item := range list.Items {
-		r := resource.New(rbac.SchemeGroupVersion.String(), "RoleBinding", item.Name)
-		r.Namespace = item.Namespace
-		resources[i] = r
-	}
-	return resources, nil
-}
-
-func fetchCustomResourceDefinitions(ctx context.Context, k *k8s.KubernetesAPI, options metav1.ListOptions) ([]resource.Kubernetes, error) {
-	list, err := k.Apiextensions.ApiextensionsV1beta1().CustomResourceDefinitions().List(ctx, options)
-	if err != nil {
-		return nil, err
-	}
-
-	resources := make([]resource.Kubernetes, len(list.Items))
-	for i, item := range list.Items {
-		resources[i] = resource.New(apiextension.SchemeGroupVersion.String(), "CustomResourceDefinition", item.Name)
-	}
-
-	return resources, nil
-}
-
-func fetchNamespaceResource(ctx context.Context, k *k8s.KubernetesAPI) (resource.Kubernetes, error) {
-	obj, err := k.CoreV1().Namespaces().Get(ctx, controlPlaneNamespace, metav1.GetOptions{})
-	if err != nil {
-		if kerrors.IsNotFound(err) {
-			return resource.Kubernetes{}, nil
-		}
-		return resource.Kubernetes{}, err
-	}
-
-	return resource.New(core.SchemeGroupVersion.String(), "Namespace", obj.Name), nil
-}
-
-func fetchPodSecurityPolicy(ctx context.Context, k *k8s.KubernetesAPI, options metav1.ListOptions) ([]resource.Kubernetes, error) {
-	list, err := k.PolicyV1beta1().PodSecurityPolicies().List(ctx, options)
-	if err != nil {
-		return nil, err
-	}
-
-	resources := make([]resource.Kubernetes, len(list.Items))
-	for i, item := range list.Items {
-		resources[i] = resource.New(policy.SchemeGroupVersion.String(), "PodSecurityPolicy", item.Name)
-	}
-
-	return resources, nil
-}
-
-func fetchValidatingWebhooksConfiguration(ctx context.Context, k *k8s.KubernetesAPI, options metav1.ListOptions) ([]resource.Kubernetes, error) {
-	list, err := k.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().List(ctx, options)
-	if err != nil {
-		return nil, err
-	}
-
-	resources := make([]resource.Kubernetes, len(list.Items))
-	for i, item := range list.Items {
-		resources[i] = resource.New(admissionRegistration.SchemeGroupVersion.String(), "ValidatingWebhookConfiguration", item.Name)
-	}
-
-	return resources, nil
-}
-
-func fetchMutatingWebhooksConfiguration(ctx context.Context, k *k8s.KubernetesAPI, options metav1.ListOptions) ([]resource.Kubernetes, error) {
-	list, err := k.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().List(ctx, options)
-	if err != nil {
-		return nil, err
-	}
-
-	resources := make([]resource.Kubernetes, len(list.Items))
-	for i, item := range list.Items {
-		resources[i] = resource.New(admissionRegistration.SchemeGroupVersion.String(), "MutatingWebhookConfiguration", item.Name)
-	}
-
-	return resources, nil
-}
-func fetchAPIRegistrationResources(ctx context.Context, k *k8s.KubernetesAPI, options metav1.ListOptions) ([]resource.Kubernetes, error) {
-	apiClient, err := apiregistrationv1client.NewForConfig(k.Config)
-	if err != nil {
-		return nil, err
-	}
-
-	list, err := apiClient.APIServices().List(ctx, options)
-	if err != nil {
-		return nil, err
-	}
-
-	resources := make([]resource.Kubernetes, len(list.Items))
-	for i, item := range list.Items {
-		resources[i] = resource.New(apiRegistration.SchemeGroupVersion.String(), "APIService", item.Name)
-	}
-
-	return resources, nil
-}
diff --git a/jaeger/charts/jaeger/README.md b/jaeger/charts/jaeger/README.md
index c4f33b5b2..b5629d786 100644
--- a/jaeger/charts/jaeger/README.md
+++ b/jaeger/charts/jaeger/README.md
@@ -19,14 +19,14 @@ A Helm chart for the jaeger add-on in Linkerd
 | collector.image.name | string | `"omnition/opencensus-collector"` |  |
 | collector.image.pullPolicy | string | `"Always"` |  |
 | collector.image.version | string | `"0.1.11"` |  |
-| collectorSvcAccount | string | `"collector"` |  |
-| collectorSvcAddr | string | `"collector.linkerd-jaeger:55678"` |  |
 | jaeger.image.name | string | `"jaegertracing/all-in-one"` |  |
 | jaeger.image.pullPolicy | string | `"Always"` |  |
 | jaeger.image.version | string | `"1.19.2"` |  |
 | linkerdVersion | string | `"linkerdVersionValue"` |  |
 | namespace | string | `"linkerd-jaeger"` |  |
 | 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 |
 | webhook.crtPEM | string | `""` | if empty, Helm will auto-generate these fields |
 | webhook.externalSecret | bool | `false` |  |
 | webhook.failurePolicy | string | `"Ignore"` |  |
diff --git a/jaeger/charts/jaeger/templates/proxy-mutator.yaml b/jaeger/charts/jaeger/templates/proxy-mutator.yaml
index 6fe0eeef3..7dd4ded99 100644
--- a/jaeger/charts/jaeger/templates/proxy-mutator.yaml
+++ b/jaeger/charts/jaeger/templates/proxy-mutator.yaml
@@ -26,8 +26,8 @@ spec:
     spec:
       containers:
       - args:
-        - -collector-svc-addr={{.Values.collectorSvcAddr}}
-        - -collector-svc-account={{.Values.collectorSvcAccount}}
+        - -collector-svc-addr={{.Values.webhook.collectorSvcAddr}}
+        - -collector-svc-account={{.Values.webhook.collectorSvcAccount}}
         - -log-level={{.Values.webhook.logLevel}}
         image: {{.Values.webhook.image.name}}:{{default .Values.webhook.image.version .Values.cliVersion}}
         imagePullPolicy: {{.Values.webhook.image.pullPolicy}}
diff --git a/jaeger/charts/jaeger/templates/rbac.yaml b/jaeger/charts/jaeger/templates/rbac.yaml
index 303762ad1..ae613a10a 100644
--- a/jaeger/charts/jaeger/templates/rbac.yaml
+++ b/jaeger/charts/jaeger/templates/rbac.yaml
@@ -28,6 +28,8 @@ kind: ClusterRole
 apiVersion: rbac.authorization.k8s.io/v1
 metadata:
   name: linkerd-jaeger-{{.Values.namespace}}-proxy-mutator
+  labels:
+    linkerd.io/extension: linkerd-jaeger
 rules:
 - apiGroups: [""]
   resources: ["namespaces"]
@@ -37,6 +39,8 @@ kind: ClusterRoleBinding
 apiVersion: rbac.authorization.k8s.io/v1
 metadata:
   name: linkerd-jaeger-{{.Values.namespace}}-proxy-mutator
+  labels:
+    linkerd.io/extension: linkerd-jaeger
 subjects:
 - kind: ServiceAccount
   name: proxy-mutator
@@ -71,6 +75,8 @@ apiVersion: admissionregistration.k8s.io/v1beta1
 kind: MutatingWebhookConfiguration
 metadata:
   name: linkerd-proxy-mutator-webhook-config
+  labels:
+    linkerd.io/extension: linkerd-jaeger
 webhooks:
 - name: proxy-mutator.linkerd.io
   {{- if .Values.webhook.namespaceSelector }}
diff --git a/jaeger/charts/jaeger/values.yaml b/jaeger/charts/jaeger/values.yaml
index d9766081d..017547aaa 100644
--- a/jaeger/charts/jaeger/values.yaml
+++ b/jaeger/charts/jaeger/values.yaml
@@ -9,9 +9,6 @@ collector:
     pullPolicy: Always
   # resources:
 
-collectorSvcAddr: collector.linkerd-jaeger:55678
-collectorSvcAccount: collector
-
 jaeger:
   image: 
     name: jaegertracing/all-in-one
@@ -30,6 +27,12 @@ webhook:
   # -- if empty, Helm will auto-generate this field, unless externalSecret is set to true.
   caBundle: |
 
+  # -- collector service address for the proxies to send trace data.
+  # Points by default to the the linkerd-jaeger collector
+  collectorSvcAddr: collector.linkerd-jaeger:55678
+  # -- service account associated with the collector instance
+  collectorSvcAccount: collector
+
   failurePolicy: Ignore
   image: 
     name: ghcr.io/linkerd/jaeger-webhook
diff --git a/jaeger/cmd/root.go b/jaeger/cmd/root.go
index b8dd548a2..e8dadb79b 100644
--- a/jaeger/cmd/root.go
+++ b/jaeger/cmd/root.go
@@ -59,6 +59,7 @@ func NewCmdJaeger() *cobra.Command {
 	jaegerCmd.PersistentFlags().StringVar(&apiAddr, "api-addr", "", "Override kubeconfig and communicate directly with the control plane at host:port (mostly for testing)")
 	jaegerCmd.PersistentFlags().BoolVar(&verbose, "verbose", false, "Turn on debug logging")
 	jaegerCmd.AddCommand(newCmdInstall())
+	jaegerCmd.AddCommand(newCmdUninstall())
 	jaegerCmd.AddCommand(newCmdDashboard())
 
 	return jaegerCmd
diff --git a/jaeger/cmd/uninstall.go b/jaeger/cmd/uninstall.go
new file mode 100644
index 000000000..3a2efbba3
--- /dev/null
+++ b/jaeger/cmd/uninstall.go
@@ -0,0 +1,50 @@
+package cmd
+
+import (
+	"context"
+	"fmt"
+	"os"
+
+	"github.com/linkerd/linkerd2/pkg/k8s"
+	"github.com/linkerd/linkerd2/pkg/k8s/resource"
+	"github.com/spf13/cobra"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+func newCmdUninstall() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "uninstall",
+		Args:  cobra.NoArgs,
+		Short: "Output Kubernetes resources to uninstall the Linkerd-jaeger extension",
+		Long: `Output Kubernetes resources to uninstall the Linkerd-jaeger extension.
+
+This command provides all Kubernetes namespace-scoped and cluster-scoped resources (e.g services, deployments, RBACs, etc.) necessary to uninstall the Linkerd-jaeger extension.`,
+		Example: `linkerd uninstall | kubectl delete -f -`,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return uninstallRunE(cmd.Context())
+		},
+	}
+
+	return cmd
+}
+
+func uninstallRunE(ctx context.Context) error {
+	k8sAPI, err := k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, impersonateGroup, 0)
+	if err != nil {
+		return err
+	}
+
+	resources, err := resource.FetchKubernetesResources(ctx, k8sAPI,
+		metav1.ListOptions{LabelSelector: "linkerd.io/extension=linkerd-jaeger"},
+	)
+	if err != nil {
+		return err
+	}
+
+	for _, r := range resources {
+		if err := r.RenderResource(os.Stdout); err != nil {
+			return fmt.Errorf("error rendering Kubernetes resource: %v", err)
+		}
+	}
+	return nil
+}
diff --git a/jaeger/values/values.go b/jaeger/values/values.go
index e1def1f97..df8e16e44 100644
--- a/jaeger/values/values.go
+++ b/jaeger/values/values.go
@@ -15,14 +15,12 @@ import (
 
 // Values represents the values of jaeger template
 type Values struct {
-	Namespace           string    `json:"namespace"`
-	CliVersion          string    `json:"cliVersion"`
-	Collector           collector `json:"collector"`
-	CollectorSvcAddr    string    `json:"collectorSvcAddr"`
-	CollectorSvcAccount string    `json:"collectorSvcAccount"`
-	Jaeger              jaeger    `json:"jaeger"`
-	LinkerdVersion      string    `json:"linkerdVersion"`
-	Webhook             webhook   `json:"webhook"`
+	Namespace      string    `json:"namespace"`
+	CliVersion     string    `json:"cliVersion"`
+	Collector      collector `json:"collector"`
+	Jaeger         jaeger    `json:"jaeger"`
+	LinkerdVersion string    `json:"linkerdVersion"`
+	Webhook        webhook   `json:"webhook"`
 }
 
 type collector struct {
@@ -36,15 +34,17 @@ type jaeger struct {
 }
 
 type webhook struct {
-	ExternalSecret    bool                  `json:"externalSecret"`
-	CrtPEM            string                `json:"crtPEM"`
-	KeyPEM            string                `json:"keyPEM"`
-	CaBundle          string                `json:"caBundle"`
-	FailurePolicy     string                `json:"failurePolicy"`
-	Image             l5dcharts.Image       `json:"image"`
-	LogLevel          string                `json:"logLevel"`
-	NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector"`
-	ObjectSelector    *metav1.LabelSelector `json:"objectSelector"`
+	ExternalSecret      bool                  `json:"externalSecret"`
+	CrtPEM              string                `json:"crtPEM"`
+	KeyPEM              string                `json:"keyPEM"`
+	CaBundle            string                `json:"caBundle"`
+	CollectorSvcAddr    string                `json:"collectorSvcAddr"`
+	CollectorSvcAccount string                `json:"collectorSvcAccount"`
+	FailurePolicy       string                `json:"failurePolicy"`
+	Image               l5dcharts.Image       `json:"image"`
+	LogLevel            string                `json:"logLevel"`
+	NamespaceSelector   *metav1.LabelSelector `json:"namespaceSelector"`
+	ObjectSelector      *metav1.LabelSelector `json:"objectSelector"`
 }
 
 // NewValues returns a new instance of the Values type.
diff --git a/multicluster/charts/linkerd2-multicluster-link/Chart.yaml b/multicluster/charts/linkerd2-multicluster-link/Chart.yaml
index 3ab84bafd..0403c2edf 100644
--- a/multicluster/charts/linkerd2-multicluster-link/Chart.yaml
+++ b/multicluster/charts/linkerd2-multicluster-link/Chart.yaml
@@ -2,7 +2,13 @@ apiVersion: v1
 appVersion: edge-XX.X.X
 description: |
   A helm chart containing the resources to enable mirroring
-  of services from a remote cluster
+  of services from a remote cluster.
+
+  Warning: The purpose of this chart is just to support the `linkerd
+  multicluster link` CLI command, which also produces the
+  `cluster-credentials` secret and the Link CR, which are not found in this
+  chart. Therefore this chart is not a replacement for that command, and
+  shouldn't be used as-is unless you really know what you're doing ;-)
 kubeVersion: ">=1.13.0-0"
 icon: https://linkerd.io/images/logo-only-200h.png
 name: "linkerd2-multicluster-link"
diff --git a/multicluster/charts/linkerd2-multicluster-link/README.md b/multicluster/charts/linkerd2-multicluster-link/README.md
index 63f738595..cfc959862 100644
--- a/multicluster/charts/linkerd2-multicluster-link/README.md
+++ b/multicluster/charts/linkerd2-multicluster-link/README.md
@@ -1,7 +1,13 @@
 # linkerd2-multicluster-link
 
 A helm chart containing the resources to enable mirroring
-of services from a remote cluster
+of services from a remote cluster.
+
+Warning: The purpose of this chart is just to support the `linkerd
+multicluster link` CLI command, which also produces the
+`cluster-credentials` secret and the Link CR, which are not found in this
+chart. Therefore this chart is not a replacement for that command, and
+shouldn't be used as-is unless you really know what you're doing ;-)
 
 ![Version: 0.1.0](https://img.shields.io/badge/Version-0.1.0-informational?style=flat-square)
 
diff --git a/multicluster/service-mirror/cluster_watcher.go b/multicluster/service-mirror/cluster_watcher.go
index ba91b2774..961029413 100644
--- a/multicluster/service-mirror/cluster_watcher.go
+++ b/multicluster/service-mirror/cluster_watcher.go
@@ -339,11 +339,15 @@ func (rcsw *RemoteClusterServiceWatcher) handleRemoteServiceDeleted(ctx context.
 // new gateway being assigned or additional ports exposed. This method takes care of that.
 func (rcsw *RemoteClusterServiceWatcher) handleRemoteServiceUpdated(ctx context.Context, ev *RemoteServiceUpdated) error {
 	rcsw.log.Infof("Updating mirror service %s/%s", ev.localService.Namespace, ev.localService.Name)
+	gatewayAddresses, err := rcsw.resolveGatewayAddress()
+	if err != nil {
+		return err
+	}
 
 	copiedEndpoints := ev.localEndpoints.DeepCopy()
 	copiedEndpoints.Subsets = []corev1.EndpointSubset{
 		{
-			Addresses: rcsw.resolveGatewayAddress(),
+			Addresses: gatewayAddresses,
 			Ports:     rcsw.getEndpointsPorts(ev.remoteUpdate),
 		},
 	}
@@ -383,6 +387,11 @@ func remapRemoteServicePorts(ports []corev1.ServicePort) []corev1.ServicePort {
 }
 
 func (rcsw *RemoteClusterServiceWatcher) handleRemoteServiceCreated(ctx context.Context, ev *RemoteServiceCreated) error {
+	gatewayAddresses, err := rcsw.resolveGatewayAddress()
+	if err != nil {
+		return err
+	}
+
 	remoteService := ev.service.DeepCopy()
 	serviceInfo := fmt.Sprintf("%s/%s", remoteService.Namespace, remoteService.Name)
 	localServiceName := rcsw.mirroredResourceName(remoteService.Name)
@@ -417,14 +426,13 @@ func (rcsw *RemoteClusterServiceWatcher) handleRemoteServiceCreated(ctx context.
 		},
 	}
 
-	gatewayAddress := rcsw.resolveGatewayAddress()
 	// only if we resolve it, we are updating the endpoints addresses and ports
-	rcsw.log.Infof("Resolved gateway [%v:%d] for %s", gatewayAddress, rcsw.link.GatewayPort, serviceInfo)
+	rcsw.log.Infof("Resolved gateway [%v:%d] for %s", gatewayAddresses, rcsw.link.GatewayPort, serviceInfo)
 
-	if len(gatewayAddress) > 0 {
+	if len(gatewayAddresses) > 0 {
 		endpointsToCreate.Subsets = []corev1.EndpointSubset{
 			{
-				Addresses: gatewayAddress,
+				Addresses: gatewayAddresses,
 				Ports:     rcsw.getEndpointsPorts(ev.service),
 			},
 		}
@@ -564,7 +572,7 @@ func (rcsw *RemoteClusterServiceWatcher) processNextEvent(ctx context.Context) (
 	case *OrphanedServicesGcTriggered:
 		err = rcsw.cleanupOrphanedServices(ctx)
 	case *RepairEndpoints:
-		rcsw.repairEndpoints(ctx)
+		err = rcsw.repairEndpoints(ctx)
 	default:
 		if ev != nil || !done { // we get a nil in case we are shutting down...
 			rcsw.log.Warnf("Received unknown event: %v", ev)
@@ -673,22 +681,34 @@ func (rcsw *RemoteClusterServiceWatcher) Stop(cleanupState bool) {
 	rcsw.eventsQueue.ShutDown()
 }
 
-func (rcsw *RemoteClusterServiceWatcher) resolveGatewayAddress() []corev1.EndpointAddress {
+func (rcsw *RemoteClusterServiceWatcher) resolveGatewayAddress() ([]corev1.EndpointAddress, error) {
 	var gatewayEndpoints []corev1.EndpointAddress
+	var errors []error
 	for _, addr := range strings.Split(rcsw.link.GatewayAddress, ",") {
-		resolved := addr
 		ipAddr, err := net.ResolveIPAddr("ip", addr)
 		if err == nil {
-			resolved = ipAddr.String()
+			gatewayEndpoints = append(gatewayEndpoints, corev1.EndpointAddress{
+				IP: ipAddr.String(),
+			})
+		} else {
+			err = fmt.Errorf("Error resolving '%s': %s", addr, err)
+			rcsw.log.Warn(err)
+			errors = append(errors, err)
 		}
-		gatewayEndpoints = append(gatewayEndpoints, corev1.EndpointAddress{
-			IP: resolved,
-		})
 	}
-	return gatewayEndpoints
+	// one resolved address is enough
+	if len(gatewayEndpoints) > 0 {
+		return gatewayEndpoints, nil
+	}
+	return nil, RetryableError{errors}
 }
 
-func (rcsw *RemoteClusterServiceWatcher) repairEndpoints(ctx context.Context) {
+func (rcsw *RemoteClusterServiceWatcher) repairEndpoints(ctx context.Context) error {
+	gatewayAddresses, err := rcsw.resolveGatewayAddress()
+	if err != nil {
+		return err
+	}
+
 	endpointRepairCounter.With(prometheus.Labels{
 		gatewayClusterName: rcsw.link.TargetClusterName,
 	}).Inc()
@@ -709,7 +729,7 @@ func (rcsw *RemoteClusterServiceWatcher) repairEndpoints(ctx context.Context) {
 		},
 		Subsets: []corev1.EndpointSubset{
 			{
-				Addresses: rcsw.resolveGatewayAddress(),
+				Addresses: gatewayAddresses,
 				Ports: []corev1.EndpointPort{
 					{
 						Name:     "mc-probe",
@@ -721,7 +741,7 @@ func (rcsw *RemoteClusterServiceWatcher) repairEndpoints(ctx context.Context) {
 		},
 	}
 
-	err := rcsw.createOrUpdateEndpoints(ctx, gatewayMirrorEndpoints)
+	err = rcsw.createOrUpdateEndpoints(ctx, gatewayMirrorEndpoints)
 	if err != nil {
 		rcsw.log.Errorf("Failed to create/update gateway mirror endpoints: %s", err)
 	}
@@ -743,7 +763,7 @@ func (rcsw *RemoteClusterServiceWatcher) repairEndpoints(ctx context.Context) {
 		updatedEndpoints := endpoints.DeepCopy()
 		updatedEndpoints.Subsets = []corev1.EndpointSubset{
 			{
-				Addresses: rcsw.resolveGatewayAddress(),
+				Addresses: gatewayAddresses,
 				Ports:     rcsw.getEndpointsPorts(updatedService),
 			},
 		}
@@ -764,6 +784,8 @@ func (rcsw *RemoteClusterServiceWatcher) repairEndpoints(ctx context.Context) {
 			rcsw.log.Error(err)
 		}
 	}
+
+	return nil
 }
 
 func (rcsw *RemoteClusterServiceWatcher) createOrUpdateEndpoints(ctx context.Context, ep *corev1.Endpoints) error {
diff --git a/pkg/charts/linkerd2/values.go b/pkg/charts/linkerd2/values.go
index befb053d5..2f9a76b3e 100644
--- a/pkg/charts/linkerd2/values.go
+++ b/pkg/charts/linkerd2/values.go
@@ -45,6 +45,7 @@ type (
 		Tap                         *Tap              `json:"tap"`
 		NodeSelector                map[string]string `json:"nodeSelector"`
 		Tolerations                 []interface{}     `json:"tolerations"`
+		Stage                       string            `json:"stage"`
 
 		DestinationResources   *Resources `json:"destinationResources"`
 		HeartbeatResources     *Resources `json:"heartbeatResources"`
@@ -293,7 +294,7 @@ func readDefaults(chartDir string, ha bool) (*Values, error) {
 		}
 
 		var err error
-		values, err = values.merge(v)
+		values, err = values.Merge(v)
 		if err != nil {
 			return nil, err
 		}
@@ -302,10 +303,10 @@ func readDefaults(chartDir string, ha bool) (*Values, error) {
 	return &values, nil
 }
 
-// merge merges the non-empty properties of src into v.
+// Merge merges the non-empty properties of src into v.
 // A new Values instance is returned. Neither src nor v are mutated after
 // calling merge.
-func (v Values) merge(src Values) (Values, error) {
+func (v Values) Merge(src Values) (Values, error) {
 	// By default, mergo.Merge doesn't overwrite any existing non-empty values
 	// in its first argument. So in HA mode, we are merging values.yaml into
 	// values-ha.yaml, instead of the other way round (like Helm). This ensures
diff --git a/pkg/k8s/resource/resource.go b/pkg/k8s/resource/resource.go
index caf084d81..60e4e6233 100644
--- a/pkg/k8s/resource/resource.go
+++ b/pkg/k8s/resource/resource.go
@@ -1,10 +1,20 @@
 package resource
 
 import (
+	"context"
+	"fmt"
 	"io"
 
+	"github.com/linkerd/linkerd2/pkg/k8s"
+	admissionRegistration "k8s.io/api/admissionregistration/v1beta1"
+	core "k8s.io/api/core/v1"
+	policy "k8s.io/api/policy/v1beta1"
+	rbac "k8s.io/api/rbac/v1"
+	apiextension "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
+	apiRegistration "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
+	apiregistrationv1client "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/typed/apiregistration/v1"
 	"sigs.k8s.io/yaml"
 )
 
@@ -60,3 +70,201 @@ func (r Kubernetes) RenderResource(w io.Writer) error {
 	_, err = w.Write([]byte(yamlSep))
 	return err
 }
+
+// FetchKubernetesResources returns a slice of all cluster scoped kubernetes
+// resources which match the given ListOptions.
+func FetchKubernetesResources(ctx context.Context, k *k8s.KubernetesAPI, options metav1.ListOptions) ([]Kubernetes, error) {
+
+	resources := make([]Kubernetes, 0)
+
+	clusterRoles, err := fetchClusterRoles(ctx, k, options)
+	if err != nil {
+		return nil, fmt.Errorf("could not fetch ClusterRole resources:%v", err)
+	}
+	resources = append(resources, clusterRoles...)
+
+	clusterRoleBindings, err := fetchClusterRoleBindings(ctx, k, options)
+	if err != nil {
+		return nil, fmt.Errorf("could not fetch ClusterRoleBinding resources:%v", err)
+	}
+	resources = append(resources, clusterRoleBindings...)
+
+	roleBindings, err := fetchKubeSystemRoleBindings(ctx, k, options)
+	if err != nil {
+		return nil, fmt.Errorf("could not fetch RoleBindings from kube-system namespace:%v", err)
+	}
+	resources = append(resources, roleBindings...)
+
+	crds, err := fetchCustomResourceDefinitions(ctx, k, options)
+	if err != nil {
+		return nil, fmt.Errorf("could not fetch CustomResourceDefinition resources:%v", err)
+	}
+	resources = append(resources, crds...)
+
+	apiCRDs, err := fetchAPIRegistrationResources(ctx, k, options)
+	if err != nil {
+		return nil, fmt.Errorf("could not fetch APIService CRDs:%v", err)
+	}
+	resources = append(resources, apiCRDs...)
+
+	psps, err := fetchPodSecurityPolicy(ctx, k, options)
+	if err != nil {
+		return nil, fmt.Errorf("could not fetch PodSecurityPolicy resources:%v", err)
+	}
+	resources = append(resources, psps...)
+
+	mutatinghooks, err := fetchMutatingWebhooksConfiguration(ctx, k, options)
+	if err != nil {
+		return nil, fmt.Errorf("could not fetch MutatingWebhookConfigurations:%v", err)
+	}
+	resources = append(resources, mutatinghooks...)
+
+	validationhooks, err := fetchValidatingWebhooksConfiguration(ctx, k, options)
+	if err != nil {
+		return nil, fmt.Errorf("could not fetch ValidatingWebhookConfiguration:%v", err)
+	}
+	resources = append(resources, validationhooks...)
+
+	namespaces, err := fetchNamespace(ctx, k, options)
+	if err != nil {
+		return nil, fmt.Errorf("could not fetch Namespace:%v", err)
+	}
+	resources = append(resources, namespaces...)
+
+	return resources, nil
+}
+
+func fetchClusterRoles(ctx context.Context, k *k8s.KubernetesAPI, options metav1.ListOptions) ([]Kubernetes, error) {
+	list, err := k.RbacV1().ClusterRoles().List(ctx, options)
+	if err != nil {
+		return nil, err
+	}
+
+	resources := make([]Kubernetes, len(list.Items))
+	for i, item := range list.Items {
+		resources[i] = New(rbac.SchemeGroupVersion.String(), "ClusterRole", item.Name)
+	}
+
+	return resources, nil
+}
+
+func fetchClusterRoleBindings(ctx context.Context, k *k8s.KubernetesAPI, options metav1.ListOptions) ([]Kubernetes, error) {
+	list, err := k.RbacV1().ClusterRoleBindings().List(ctx, options)
+	if err != nil {
+		return nil, err
+	}
+
+	resources := make([]Kubernetes, len(list.Items))
+	for i, item := range list.Items {
+		resources[i] = New(rbac.SchemeGroupVersion.String(), "ClusterRoleBinding", item.Name)
+	}
+
+	return resources, nil
+}
+
+// Although role bindings are namespaced resources in nature
+// some admin role bindings are created and persisted in the kube-system namespace and will not be deleted
+// when the namespace is deleted
+func fetchKubeSystemRoleBindings(ctx context.Context, k *k8s.KubernetesAPI, options metav1.ListOptions) ([]Kubernetes, error) {
+	list, err := k.RbacV1().RoleBindings("kube-system").List(ctx, options)
+	if err != nil {
+		return nil, err
+	}
+
+	resources := make([]Kubernetes, len(list.Items))
+	for i, item := range list.Items {
+		r := New(rbac.SchemeGroupVersion.String(), "RoleBinding", item.Name)
+		r.Namespace = item.Namespace
+		resources[i] = r
+	}
+	return resources, nil
+}
+
+func fetchCustomResourceDefinitions(ctx context.Context, k *k8s.KubernetesAPI, options metav1.ListOptions) ([]Kubernetes, error) {
+	list, err := k.Apiextensions.ApiextensionsV1beta1().CustomResourceDefinitions().List(ctx, options)
+	if err != nil {
+		return nil, err
+	}
+
+	resources := make([]Kubernetes, len(list.Items))
+	for i, item := range list.Items {
+		resources[i] = New(apiextension.SchemeGroupVersion.String(), "CustomResourceDefinition", item.Name)
+	}
+
+	return resources, nil
+}
+
+func fetchNamespace(ctx context.Context, k *k8s.KubernetesAPI, options metav1.ListOptions) ([]Kubernetes, error) {
+	list, err := k.CoreV1().Namespaces().List(ctx, options)
+	if err != nil {
+		return nil, err
+	}
+
+	resources := make([]Kubernetes, len(list.Items))
+	for i, item := range list.Items {
+		r := New(core.SchemeGroupVersion.String(), "Namespace", item.Name)
+		r.Namespace = item.Namespace
+		resources[i] = r
+	}
+	return resources, nil
+}
+
+func fetchPodSecurityPolicy(ctx context.Context, k *k8s.KubernetesAPI, options metav1.ListOptions) ([]Kubernetes, error) {
+	list, err := k.PolicyV1beta1().PodSecurityPolicies().List(ctx, options)
+	if err != nil {
+		return nil, err
+	}
+
+	resources := make([]Kubernetes, len(list.Items))
+	for i, item := range list.Items {
+		resources[i] = New(policy.SchemeGroupVersion.String(), "PodSecurityPolicy", item.Name)
+	}
+
+	return resources, nil
+}
+
+func fetchValidatingWebhooksConfiguration(ctx context.Context, k *k8s.KubernetesAPI, options metav1.ListOptions) ([]Kubernetes, error) {
+	list, err := k.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().List(ctx, options)
+	if err != nil {
+		return nil, err
+	}
+
+	resources := make([]Kubernetes, len(list.Items))
+	for i, item := range list.Items {
+		resources[i] = New(admissionRegistration.SchemeGroupVersion.String(), "ValidatingWebhookConfiguration", item.Name)
+	}
+
+	return resources, nil
+}
+
+func fetchMutatingWebhooksConfiguration(ctx context.Context, k *k8s.KubernetesAPI, options metav1.ListOptions) ([]Kubernetes, error) {
+	list, err := k.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().List(ctx, options)
+	if err != nil {
+		return nil, err
+	}
+
+	resources := make([]Kubernetes, len(list.Items))
+	for i, item := range list.Items {
+		resources[i] = New(admissionRegistration.SchemeGroupVersion.String(), "MutatingWebhookConfiguration", item.Name)
+	}
+
+	return resources, nil
+}
+func fetchAPIRegistrationResources(ctx context.Context, k *k8s.KubernetesAPI, options metav1.ListOptions) ([]Kubernetes, error) {
+	apiClient, err := apiregistrationv1client.NewForConfig(k.Config)
+	if err != nil {
+		return nil, err
+	}
+
+	list, err := apiClient.APIServices().List(ctx, options)
+	if err != nil {
+		return nil, err
+	}
+
+	resources := make([]Kubernetes, len(list.Items))
+	for i, item := range list.Items {
+		resources[i] = New(apiRegistration.SchemeGroupVersion.String(), "APIService", item.Name)
+	}
+
+	return resources, nil
+}
diff --git a/web/app/yarn.lock b/web/app/yarn.lock
index aeb73c8a3..bc03e049c 100644
--- a/web/app/yarn.lock
+++ b/web/app/yarn.lock
@@ -6051,9 +6051,9 @@ inherits@2.0.3:
   integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
 
 ini@^1.3.4, ini@^1.3.5:
-  version "1.3.5"
-  resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
-  integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
+  version "1.3.7"
+  resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84"
+  integrity sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==
 
 inquirer@^6.3.1:
   version "6.5.2"


Test patch:
diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml
index 20128d60e..cd8112e27 100644
--- a/.github/workflows/integration_tests.yml
+++ b/.github/workflows/integration_tests.yml
@@ -15,6 +15,7 @@ jobs:
     runs-on: ubuntu-18.04
     strategy:
       matrix:
+        # Keep in sync with release.yaml matrix build
         target: [proxy, controller, web, cni-plugin, debug, cli-bin, grafana, jaeger-webhook]
     name: Docker build (${{ matrix.target }})
     timeout-minutes: 30
diff --git a/cli/cmd/testdata/install-cni-plugin_default.golden b/cli/cmd/testdata/install-cni-plugin_default.golden
index 66ce80b3b..6de67b927 100644
--- a/cli/cmd/testdata/install-cni-plugin_default.golden
+++ b/cli/cmd/testdata/install-cni-plugin_default.golden
@@ -68,7 +68,7 @@ subjects:
   namespace: linkerd-cni
 ---
 kind: ClusterRole
-apiVersion: rbac.authorization.k8s.io/v1beta1
+apiVersion: rbac.authorization.k8s.io/v1
 metadata:
   name: linkerd-cni
   labels:
@@ -78,7 +78,7 @@ rules:
   resources: ["pods", "nodes", "namespaces"]
   verbs: ["list", "get", "watch"]
 ---
-apiVersion: rbac.authorization.k8s.io/v1beta1
+apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
 metadata:
   name: linkerd-cni
diff --git a/cli/cmd/testdata/install-cni-plugin_fully_configured.golden b/cli/cmd/testdata/install-cni-plugin_fully_configured.golden
index 22e9b5e2a..333a1d637 100644
--- a/cli/cmd/testdata/install-cni-plugin_fully_configured.golden
+++ b/cli/cmd/testdata/install-cni-plugin_fully_configured.golden
@@ -68,7 +68,7 @@ subjects:
   namespace: other
 ---
 kind: ClusterRole
-apiVersion: rbac.authorization.k8s.io/v1beta1
+apiVersion: rbac.authorization.k8s.io/v1
 metadata:
   name: linkerd-cni
   labels:
@@ -78,7 +78,7 @@ rules:
   resources: ["pods", "nodes", "namespaces"]
   verbs: ["list", "get", "watch"]
 ---
-apiVersion: rbac.authorization.k8s.io/v1beta1
+apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
 metadata:
   name: linkerd-cni
diff --git a/cli/cmd/testdata/install-cni-plugin_fully_configured_equal_dsts.golden b/cli/cmd/testdata/install-cni-plugin_fully_configured_equal_dsts.golden
index 245d769a6..4d71f7110 100644
--- a/cli/cmd/testdata/install-cni-plugin_fully_configured_equal_dsts.golden
+++ b/cli/cmd/testdata/install-cni-plugin_fully_configured_equal_dsts.golden
@@ -68,7 +68,7 @@ subjects:
   namespace: other
 ---
 kind: ClusterRole
-apiVersion: rbac.authorization.k8s.io/v1beta1
+apiVersion: rbac.authorization.k8s.io/v1
 metadata:
   name: linkerd-cni
   labels:
@@ -78,7 +78,7 @@ rules:
   resources: ["pods", "nodes", "namespaces"]
   verbs: ["list", "get", "watch"]
 ---
-apiVersion: rbac.authorization.k8s.io/v1beta1
+apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
 metadata:
   name: linkerd-cni
diff --git a/cli/cmd/testdata/install-cni-plugin_fully_configured_no_namespace.golden b/cli/cmd/testdata/install-cni-plugin_fully_configured_no_namespace.golden
index 0e0801cef..61bab3967 100644
--- a/cli/cmd/testdata/install-cni-plugin_fully_configured_no_namespace.golden
+++ b/cli/cmd/testdata/install-cni-plugin_fully_configured_no_namespace.golden
@@ -58,7 +58,7 @@ subjects:
   namespace: other
 ---
 kind: ClusterRole
-apiVersion: rbac.authorization.k8s.io/v1beta1
+apiVersion: rbac.authorization.k8s.io/v1
 metadata:
   name: linkerd-cni
   labels:
@@ -68,7 +68,7 @@ rules:
   resources: ["pods", "nodes", "namespaces"]
   verbs: ["list", "get", "watch"]
 ---
-apiVersion: rbac.authorization.k8s.io/v1beta1
+apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
 metadata:
   name: linkerd-cni
diff --git a/cli/cmd/testdata/install-cni-plugin_skip_ports.golden b/cli/cmd/testdata/install-cni-plugin_skip_ports.golden
index f5d83da1f..c2f0e7a11 100644
--- a/cli/cmd/testdata/install-cni-plugin_skip_ports.golden
+++ b/cli/cmd/testdata/install-cni-plugin_skip_ports.golden
@@ -68,7 +68,7 @@ subjects:
   namespace: linkerd-cni
 ---
 kind: ClusterRole
-apiVersion: rbac.authorization.k8s.io/v1beta1
+apiVersion: rbac.authorization.k8s.io/v1
 metadata:
   name: linkerd-cni
   labels:
@@ -78,7 +78,7 @@ rules:
   resources: ["pods", "nodes", "namespaces"]
   verbs: ["list", "get", "watch"]
 ---
-apiVersion: rbac.authorization.k8s.io/v1beta1
+apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
 metadata:
   name: linkerd-cni
diff --git a/cli/cmd/testdata/install_cni_helm_default_output.golden b/cli/cmd/testdata/install_cni_helm_default_output.golden
index dfcbdf73b..bf967f1b5 100644
--- a/cli/cmd/testdata/install_cni_helm_default_output.golden
+++ b/cli/cmd/testdata/install_cni_helm_default_output.golden
@@ -70,7 +70,7 @@ subjects:
   namespace: linkerd-cni
 ---
 kind: ClusterRole
-apiVersion: rbac.authorization.k8s.io/v1beta1
+apiVersion: rbac.authorization.k8s.io/v1
 metadata:
   name: linkerd-cni
   labels:
@@ -80,7 +80,7 @@ rules:
   resources: ["pods", "nodes", "namespaces"]
   verbs: ["list", "get", "watch"]
 ---
-apiVersion: rbac.authorization.k8s.io/v1beta1
+apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
 metadata:
   name: linkerd-cni
diff --git a/cli/cmd/testdata/install_cni_helm_override_output.golden b/cli/cmd/testdata/install_cni_helm_override_output.golden
index 17bf55af9..0f72e1f66 100644
--- a/cli/cmd/testdata/install_cni_helm_override_output.golden
+++ b/cli/cmd/testdata/install_cni_helm_override_output.golden
@@ -70,7 +70,7 @@ subjects:
   namespace: linkerd-test
 ---
 kind: ClusterRole
-apiVersion: rbac.authorization.k8s.io/v1beta1
+apiVersion: rbac.authorization.k8s.io/v1
 metadata:
   name: linkerd-cni
   labels:
@@ -80,7 +80,7 @@ rules:
   resources: ["pods", "nodes", "namespaces"]
   verbs: ["list", "get", "watch"]
 ---
-apiVersion: rbac.authorization.k8s.io/v1beta1
+apiVersion: rbac.authorization.k8s.io/v1
 kind: ClusterRoleBinding
 metadata:
   name: linkerd-cni
diff --git a/cli/cmd/testdata/install_controlplane_tracing_output.golden b/cli/cmd/testdata/install_controlplane_tracing_output.golden
index a590e8970..7470327e9 100644
--- a/cli/cmd/testdata/install_controlplane_tracing_output.golden
+++ b/cli/cmd/testdata/install_controlplane_tracing_output.golden
@@ -2041,6 +2041,7 @@ spec:
   template:
     metadata:
       annotations:
+        checksum/config: 906e23f11c1c920abd60ae9cce09ca9f53673544c9c587ef390110c4a4bfe60d
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/identity-mode: default
         linkerd.io/proxy-version: install-proxy-version
@@ -2296,6 +2297,7 @@ spec:
   template:
     metadata:
       annotations:
+        checksum/config: 82e78e61c12a83a1c769a6a0b19b1567b0f73e5ebe240277970a23d83de6fe22
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/identity-mode: default
         linkerd.io/proxy-version: install-proxy-version
@@ -2532,6 +2534,7 @@ spec:
   template:
     metadata:
       annotations:
+        checksum/config: 51c6f0865aca4a1b4e25e619385ff0d9f95683e27c4d29c3072eac39b3fa7220
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/identity-mode: default
         linkerd.io/proxy-version: install-proxy-version
diff --git a/cli/cmd/testdata/install_custom_registry.golden b/cli/cmd/testdata/install_custom_registry.golden
index 7583b94ce..9d010353c 100644
--- a/cli/cmd/testdata/install_custom_registry.golden
+++ b/cli/cmd/testdata/install_custom_registry.golden
@@ -2037,6 +2037,7 @@ spec:
   template:
     metadata:
       annotations:
+        checksum/config: 906e23f11c1c920abd60ae9cce09ca9f53673544c9c587ef390110c4a4bfe60d
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/identity-mode: default
         linkerd.io/proxy-version: install-proxy-version
@@ -2292,6 +2293,7 @@ spec:
   template:
     metadata:
       annotations:
+        checksum/config: 82e78e61c12a83a1c769a6a0b19b1567b0f73e5ebe240277970a23d83de6fe22
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/identity-mode: default
         linkerd.io/proxy-version: install-proxy-version
@@ -2528,6 +2530,7 @@ spec:
   template:
     metadata:
       annotations:
+        checksum/config: 51c6f0865aca4a1b4e25e619385ff0d9f95683e27c4d29c3072eac39b3fa7220
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/identity-mode: default
         linkerd.io/proxy-version: install-proxy-version
diff --git a/cli/cmd/testdata/install_default.golden b/cli/cmd/testdata/install_default.golden
index 38a624564..3c2903f63 100644
--- a/cli/cmd/testdata/install_default.golden
+++ b/cli/cmd/testdata/install_default.golden
@@ -2037,6 +2037,7 @@ spec:
   template:
     metadata:
       annotations:
+        checksum/config: 906e23f11c1c920abd60ae9cce09ca9f53673544c9c587ef390110c4a4bfe60d
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/identity-mode: default
         linkerd.io/proxy-version: install-proxy-version
@@ -2292,6 +2293,7 @@ spec:
   template:
     metadata:
       annotations:
+        checksum/config: 82e78e61c12a83a1c769a6a0b19b1567b0f73e5ebe240277970a23d83de6fe22
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/identity-mode: default
         linkerd.io/proxy-version: install-proxy-version
@@ -2528,6 +2530,7 @@ spec:
   template:
     metadata:
       annotations:
+        checksum/config: 51c6f0865aca4a1b4e25e619385ff0d9f95683e27c4d29c3072eac39b3fa7220
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/identity-mode: default
         linkerd.io/proxy-version: install-proxy-version
diff --git a/cli/cmd/testdata/install_default_override_dst_get_nets.golden b/cli/cmd/testdata/install_default_override_dst_get_nets.golden
index dff1357e2..9e0606428 100644
--- a/cli/cmd/testdata/install_default_override_dst_get_nets.golden
+++ b/cli/cmd/testdata/install_default_override_dst_get_nets.golden
@@ -2037,6 +2037,7 @@ spec:
   template:
     metadata:
       annotations:
+        checksum/config: 906e23f11c1c920abd60ae9cce09ca9f53673544c9c587ef390110c4a4bfe60d
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/identity-mode: default
         linkerd.io/proxy-version: install-proxy-version
@@ -2292,6 +2293,7 @@ spec:
   template:
     metadata:
       annotations:
+        checksum/config: 82e78e61c12a83a1c769a6a0b19b1567b0f73e5ebe240277970a23d83de6fe22
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/identity-mode: default
         linkerd.io/proxy-version: install-proxy-version
@@ -2528,6 +2530,7 @@ spec:
   template:
     metadata:
       annotations:
+        checksum/config: 51c6f0865aca4a1b4e25e619385ff0d9f95683e27c4d29c3072eac39b3fa7220
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/identity-mode: default
         linkerd.io/proxy-version: install-proxy-version
diff --git a/cli/cmd/testdata/install_grafana_existing.golden b/cli/cmd/testdata/install_grafana_existing.golden
index df1920aa1..8d1e2db87 100644
--- a/cli/cmd/testdata/install_grafana_existing.golden
+++ b/cli/cmd/testdata/install_grafana_existing.golden
@@ -2034,6 +2034,7 @@ spec:
   template:
     metadata:
       annotations:
+        checksum/config: 906e23f11c1c920abd60ae9cce09ca9f53673544c9c587ef390110c4a4bfe60d
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/identity-mode: default
         linkerd.io/proxy-version: install-proxy-version
@@ -2289,6 +2290,7 @@ spec:
   template:
     metadata:
       annotations:
+        checksum/config: 82e78e61c12a83a1c769a6a0b19b1567b0f73e5ebe240277970a23d83de6fe22
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/identity-mode: default
         linkerd.io/proxy-version: install-proxy-version
@@ -2525,6 +2527,7 @@ spec:
   template:
     metadata:
       annotations:
+        checksum/config: 51c6f0865aca4a1b4e25e619385ff0d9f95683e27c4d29c3072eac39b3fa7220
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/identity-mode: default
         linkerd.io/proxy-version: install-proxy-version
diff --git a/cli/cmd/testdata/install_ha_output.golden b/cli/cmd/testdata/install_ha_output.golden
index 848b229c4..102f86219 100644
--- a/cli/cmd/testdata/install_ha_output.golden
+++ b/cli/cmd/testdata/install_ha_output.golden
@@ -2221,6 +2221,7 @@ spec:
   template:
     metadata:
       annotations:
+        checksum/config: 34434799a6867cce161426b50265dfd03620ec94a9f90a01ccc64bc7811af9a5
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/identity-mode: default
         linkerd.io/proxy-version: install-proxy-version
@@ -2510,6 +2511,7 @@ spec:
   template:
     metadata:
       annotations:
+        checksum/config: 89b4768cd03ce9a74f021b6b2cb026c1b25980b0fed5d2e15b100b4ef428a2bd
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/identity-mode: default
         linkerd.io/proxy-version: install-proxy-version
@@ -2780,6 +2782,7 @@ spec:
   template:
     metadata:
       annotations:
+        checksum/config: 51c6f0865aca4a1b4e25e619385ff0d9f95683e27c4d29c3072eac39b3fa7220
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/identity-mode: default
         linkerd.io/proxy-version: install-proxy-version
diff --git a/cli/cmd/testdata/install_ha_with_overrides_output.golden b/cli/cmd/testdata/install_ha_with_overrides_output.golden
index 5a38ed5a5..3b142d812 100644
--- a/cli/cmd/testdata/install_ha_with_overrides_output.golden
+++ b/cli/cmd/testdata/install_ha_with_overrides_output.golden
@@ -2221,6 +2221,7 @@ spec:
   template:
     metadata:
       annotations:
+        checksum/config: 34434799a6867cce161426b50265dfd03620ec94a9f90a01ccc64bc7811af9a5
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/identity-mode: default
         linkerd.io/proxy-version: install-proxy-version
@@ -2510,6 +2511,7 @@ spec:
   template:
     metadata:
       annotations:
+        checksum/config: 89b4768cd03ce9a74f021b6b2cb026c1b25980b0fed5d2e15b100b4ef428a2bd
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/identity-mode: default
         linkerd.io/proxy-version: install-proxy-version
@@ -2780,6 +2782,7 @@ spec:
   template:
     metadata:
       annotations:
+        checksum/config: 51c6f0865aca4a1b4e25e619385ff0d9f95683e27c4d29c3072eac39b3fa7220
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/identity-mode: default
         linkerd.io/proxy-version: install-proxy-version
diff --git a/cli/cmd/testdata/install_heartbeat_disabled_output.golden b/cli/cmd/testdata/install_heartbeat_disabled_output.golden
index 73d993614..f9b593d0b 100644
--- a/cli/cmd/testdata/install_heartbeat_disabled_output.golden
+++ b/cli/cmd/testdata/install_heartbeat_disabled_output.golden
@@ -1948,6 +1948,7 @@ spec:
   template:
     metadata:
       annotations:
+        checksum/config: 906e23f11c1c920abd60ae9cce09ca9f53673544c9c587ef390110c4a4bfe60d
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/identity-mode: default
         linkerd.io/proxy-version: install-proxy-version
@@ -2203,6 +2204,7 @@ spec:
   template:
     metadata:
       annotations:
+        checksum/config: 82e78e61c12a83a1c769a6a0b19b1567b0f73e5ebe240277970a23d83de6fe22
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/identity-mode: default
         linkerd.io/proxy-version: install-proxy-version
@@ -2439,6 +2441,7 @@ spec:
   template:
     metadata:
       annotations:
+        checksum/config: 51c6f0865aca4a1b4e25e619385ff0d9f95683e27c4d29c3072eac39b3fa7220
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/identity-mode: default
         linkerd.io/proxy-version: install-proxy-version
diff --git a/cli/cmd/testdata/install_no_init_container.golden b/cli/cmd/testdata/install_no_init_container.golden
index e1edc5342..d67d5f6b8 100644
--- a/cli/cmd/testdata/install_no_init_container.golden
+++ b/cli/cmd/testdata/install_no_init_container.golden
@@ -1882,6 +1882,7 @@ spec:
   template:
     metadata:
       annotations:
+        checksum/config: 906e23f11c1c920abd60ae9cce09ca9f53673544c9c587ef390110c4a4bfe60d
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/identity-mode: default
         linkerd.io/proxy-version: install-proxy-version
@@ -2099,6 +2100,7 @@ spec:
   template:
     metadata:
       annotations:
+        checksum/config: 82e78e61c12a83a1c769a6a0b19b1567b0f73e5ebe240277970a23d83de6fe22
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/identity-mode: default
         linkerd.io/proxy-version: install-proxy-version
@@ -2297,6 +2299,7 @@ spec:
   template:
     metadata:
       annotations:
+        checksum/config: 51c6f0865aca4a1b4e25e619385ff0d9f95683e27c4d29c3072eac39b3fa7220
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/identity-mode: default
         linkerd.io/proxy-version: install-proxy-version
diff --git a/cli/cmd/testdata/install_output.golden b/cli/cmd/testdata/install_output.golden
index 0bbb464ce..b381a9334 100644
--- a/cli/cmd/testdata/install_output.golden
+++ b/cli/cmd/testdata/install_output.golden
@@ -2046,6 +2046,7 @@ spec:
   template:
     metadata:
       annotations:
+        checksum/config: f43b1bcac5553936082cc54fea2701efae29eb7d43ca8d200fc4c90cc3a8f3d7
         CreatedByAnnotation: CliVersion
         linkerd.io/identity-mode: default
         linkerd.io/proxy-version: ProxyVersion
@@ -2303,6 +2304,7 @@ spec:
   template:
     metadata:
       annotations:
+        checksum/config: 46c3fa3a42fbd9a72f145d0e8e857eed03dff5231924f1b2e0b0889eb107f3a9
         CreatedByAnnotation: CliVersion
         linkerd.io/identity-mode: default
         linkerd.io/proxy-version: ProxyVersion
@@ -2541,6 +2543,7 @@ spec:
   template:
     metadata:
       annotations:
+        checksum/config: aef797109c168e77adfbdcb91b94ade00745023e5952a481af73b47727917289
         CreatedByAnnotation: CliVersion
         linkerd.io/identity-mode: default
         linkerd.io/proxy-version: ProxyVersion
diff --git a/cli/cmd/testdata/install_prometheus_overwrite.golden b/cli/cmd/testdata/install_prometheus_overwrite.golden
index b847e2ae1..6e76aa678 100644
--- a/cli/cmd/testdata/install_prometheus_overwrite.golden
+++ b/cli/cmd/testdata/install_prometheus_overwrite.golden
@@ -2094,6 +2094,7 @@ spec:
   template:
     metadata:
       annotations:
+        checksum/config: 906e23f11c1c920abd60ae9cce09ca9f53673544c9c587ef390110c4a4bfe60d
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/identity-mode: default
         linkerd.io/proxy-version: install-proxy-version
@@ -2349,6 +2350,7 @@ spec:
   template:
     metadata:
       annotations:
+        checksum/config: 82e78e61c12a83a1c769a6a0b19b1567b0f73e5ebe240277970a23d83de6fe22
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/identity-mode: default
         linkerd.io/proxy-version: install-proxy-version
@@ -2585,6 +2587,7 @@ spec:
   template:
     metadata:
       annotations:
+        checksum/config: 51c6f0865aca4a1b4e25e619385ff0d9f95683e27c4d29c3072eac39b3fa7220
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/identity-mode: default
         linkerd.io/proxy-version: install-proxy-version
diff --git a/cli/cmd/testdata/install_proxy_ignores.golden b/cli/cmd/testdata/install_proxy_ignores.golden
index d5b4a0073..e4d450875 100644
--- a/cli/cmd/testdata/install_proxy_ignores.golden
+++ b/cli/cmd/testdata/install_proxy_ignores.golden
@@ -2037,6 +2037,7 @@ spec:
   template:
     metadata:
       annotations:
+        checksum/config: 906e23f11c1c920abd60ae9cce09ca9f53673544c9c587ef390110c4a4bfe60d
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/identity-mode: default
         linkerd.io/proxy-version: install-proxy-version
@@ -2292,6 +2293,7 @@ spec:
   template:
     metadata:
       annotations:
+        checksum/config: 82e78e61c12a83a1c769a6a0b19b1567b0f73e5ebe240277970a23d83de6fe22
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/identity-mode: default
         linkerd.io/proxy-version: install-proxy-version
@@ -2528,6 +2530,7 @@ spec:
   template:
     metadata:
       annotations:
+        checksum/config: 51c6f0865aca4a1b4e25e619385ff0d9f95683e27c4d29c3072eac39b3fa7220
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/identity-mode: default
         linkerd.io/proxy-version: install-proxy-version
diff --git a/cli/cmd/testdata/install_restricted_dashboard.golden b/cli/cmd/testdata/install_restricted_dashboard.golden
index 361097747..1fc7e48da 100644
--- a/cli/cmd/testdata/install_restricted_dashboard.golden
+++ b/cli/cmd/testdata/install_restricted_dashboard.golden
@@ -1969,6 +1969,7 @@ spec:
   template:
     metadata:
       annotations:
+        checksum/config: 906e23f11c1c920abd60ae9cce09ca9f53673544c9c587ef390110c4a4bfe60d
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/identity-mode: default
         linkerd.io/proxy-version: install-proxy-version
@@ -2224,6 +2225,7 @@ spec:
   template:
     metadata:
       annotations:
+        checksum/config: 82e78e61c12a83a1c769a6a0b19b1567b0f73e5ebe240277970a23d83de6fe22
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/identity-mode: default
         linkerd.io/proxy-version: install-proxy-version
@@ -2460,6 +2462,7 @@ spec:
   template:
     metadata:
       annotations:
+        checksum/config: 51c6f0865aca4a1b4e25e619385ff0d9f95683e27c4d29c3072eac39b3fa7220
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/identity-mode: default
         linkerd.io/proxy-version: install-proxy-version
diff --git a/cli/cmd/testdata/install_tracing_overwrite.golden b/cli/cmd/testdata/install_tracing_overwrite.golden
index 6704af24a..6ef70965a 100644
--- a/cli/cmd/testdata/install_tracing_overwrite.golden
+++ b/cli/cmd/testdata/install_tracing_overwrite.golden
@@ -2047,6 +2047,7 @@ spec:
   template:
     metadata:
       annotations:
+        checksum/config: 906e23f11c1c920abd60ae9cce09ca9f53673544c9c587ef390110c4a4bfe60d
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/identity-mode: default
         linkerd.io/proxy-version: install-proxy-version
@@ -2302,6 +2303,7 @@ spec:
   template:
     metadata:
       annotations:
+        checksum/config: 82e78e61c12a83a1c769a6a0b19b1567b0f73e5ebe240277970a23d83de6fe22
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/identity-mode: default
         linkerd.io/proxy-version: install-proxy-version
@@ -2538,6 +2540,7 @@ spec:
   template:
     metadata:
       annotations:
+        checksum/config: 51c6f0865aca4a1b4e25e619385ff0d9f95683e27c4d29c3072eac39b3fa7220
         linkerd.io/created-by: linkerd/cli dev-undefined
         linkerd.io/identity-mode: default
         linkerd.io/proxy-version: install-proxy-version
diff --git a/cli/cmd/upgrade_test.go b/cli/cmd/upgrade_test.go
index 9baff129f..fc64476cf 100644
--- a/cli/cmd/upgrade_test.go
+++ b/cli/cmd/upgrade_test.go
@@ -770,6 +770,14 @@ func ignorableDiff(id string, diff diff) bool {
 		// caBundle to change.
 		return true
 	}
+
+	if (id == "Deployment/linkerd-sp-validator" || id == "Deployment/linkerd-proxy-injector" || id == "Deployment/linkerd-tap") &&
+		pathMatch(diff.path, []string{"spec", "template", "metadata", "annotations", "checksum/config"}) {
+		// APIService TLS chains are regenerated upon upgrade so we expect the
+		// caBundle to change.
+		return true
+	}
+
 	if id == "Secret/linkerd-proxy-injector-tls" || id == "Secret/linkerd-sp-validator-tls" ||
 		id == "Secret/linkerd-tap-tls" || id == "Secret/linkerd-sp-validator-k8s-tls" ||
 		id == "Secret/linkerd-proxy-injector-k8s-tls" || id == "Secret/linkerd-tap-k8s-tls" {
diff --git a/cli/cmd/uninstall_test.go b/pkg/k8s/resource/resource_test.go
similarity index 99%
rename from cli/cmd/uninstall_test.go
rename to pkg/k8s/resource/resource_test.go
index 264dd96bf..8b7425ca0 100644
--- a/cli/cmd/uninstall_test.go
+++ b/pkg/k8s/resource/resource_test.go
@@ -1,4 +1,4 @@
-package cmd
+package resource
 
 import (
 	"context"