Repository: linkerd/linkerd2
Pull request #6929 (2021-09-20 17:24:41)

build(deps): bump golang.org/x/tools from 0.1.5 to 0.1.6
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.1.5 to 0.1.6.
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/golang/tools/commit/2758b0478a86793b078ec87125f9c307198fe321"><code>2758b04</code></a> gopls/api-diff: create api-diff command for gopls api</li>
<li><a href="https://github.com/golang/tools/commit/aba0c5f15e84627297984f4b83e44fcc9dd8b0c3"><code>aba0c5f</code></a> go/callgraph/vta: optimize scc type initialization</li>
<li><a href="https://github.com/golang/tools/commit/1a7ca93429f83e087f7d44d35c0e9ea088fc722e"><code>1a7ca93</code></a> internal/lsp/source: update SuggestedFixFunc to accept source.Snapshot</li>
<li><a href="https://github.com/golang/tools/commit/76d4494227a68dc570dffd998008f03f3a9b6fa5"><code>76d4494</code></a> internal/lsp: fix panic in find references on Error</li>
<li><a href="https://github.com/golang/tools/commit/4ba3eff04f5bca6c4f3a5b9ac04387f1b008fd1e"><code>4ba3eff</code></a> gopls/doc: remove -tags=typeparams from generic build instructions</li>
<li><a href="https://github.com/golang/tools/commit/02e5238467d354d8d38d733360b36559b28ce782"><code>02e5238</code></a> go/internal/gcimporter: rename instType to instanceType</li>
<li><a href="https://github.com/golang/tools/commit/0cffec9e30d002e3af58010197795a3e089688b5"><code>0cffec9</code></a> go/internal/gcimporter: update iimport.go to support type parameters</li>
<li><a href="https://github.com/golang/tools/commit/5492d01d98d3be4e014a66fc31953c04cc38c8c8"><code>5492d01</code></a> internal/lsp/testdata: update inferred.go to use hoverdef</li>
<li><a href="https://github.com/golang/tools/commit/92077074d40106c4de535e3d6f3716dd54bedf5c"><code>9207707</code></a> go/internal/gcimporter: skip TestIExportData_stdlib on go1.18</li>
<li><a href="https://github.com/golang/tools/commit/cd7c003cd38984ebf592aa8e4755c832a8d632bf"><code>cd7c003</code></a> internal/lsp: add support for hovering runes</li>
<li>Additional commits viewable in <a href="https://github.com/golang/tools/compare/v0.1.5...v0.1.6">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=golang.org/x/tools&package-manager=go_modules&previous-version=0.1.5&new-version=0.1.6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 1c0582e7a..57c485e20 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1478,9 +1478,9 @@ dependencies = [
 
 [[package]]
 name = "schemars"
-version = "0.8.3"
+version = "0.8.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc6ab463ae35acccb5cba66c0084c985257b797d288b6050cc2f6ac1b266cb78"
+checksum = "b82485a532ef0af18878ad4281f73e58161cdba1db7918176e9294f0ca5498a5"
 dependencies = [
  "dyn-clone",
  "schemars_derive",
@@ -1490,9 +1490,9 @@ dependencies = [
 
 [[package]]
 name = "schemars_derive"
-version = "0.8.3"
+version = "0.8.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "902fdfbcf871ae8f653bddf4b2c05905ddaabc08f69d32a915787e3be0d31356"
+checksum = "791c2c848cff1abaeae34fef7e70da5f93171d9eea81ce0fe969a1df627a61a8"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1997,9 +1997,9 @@ checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6"
 
 [[package]]
 name = "tracing"
-version = "0.1.27"
+version = "0.1.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2ba9ab62b7d6497a8638dfda5e5c4fb3b2d5a7fca4118f2b96151c8ef1a437e"
+checksum = "84f96e095c0c82419687c20ddf5cb3eadb61f4e1405923c9dc8e53a1adacbda8"
 dependencies = [
  "cfg-if",
  "log",
@@ -2061,9 +2061,9 @@ dependencies = [
 
 [[package]]
 name = "tracing-subscriber"
-version = "0.2.23"
+version = "0.2.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "56c42e73a9d277d4d2b6a88389a137ccf3c58599660b17e8f5fc39305e490669"
+checksum = "fdd0568dbfe3baf7048b7908d2b32bca0d81cd56bec6d2a8f894b01d74f86be3"
 dependencies = [
  "lazy_static",
  "matchers",
diff --git a/charts/linkerd2/templates/destination-rbac.yaml b/charts/linkerd2/templates/destination-rbac.yaml
index 8117fcef2..736bc1bd9 100644
--- a/charts/linkerd2/templates/destination-rbac.yaml
+++ b/charts/linkerd2/templates/destination-rbac.yaml
@@ -99,7 +99,7 @@ webhooks:
   failurePolicy: {{.Values.webhookFailurePolicy}}
   admissionReviewVersions: ["v1", "v1beta1"]
   rules:
-  - operations: [ "CREATE" , "UPDATE" ]
+  - operations: ["CREATE", "UPDATE"]
     apiGroups: ["linkerd.io"]
     apiVersions: ["v1alpha1", "v1alpha2"]
     resources: ["serviceprofiles"]
@@ -149,10 +149,10 @@ webhooks:
   failurePolicy: {{.Values.webhookFailurePolicy}}
   admissionReviewVersions: ["v1", "v1beta1"]
   rules:
-  - operations: [ "CREATE" , "UPDATE" ]
+  - operations: ["CREATE", "UPDATE"]
     apiGroups: ["policy.linkerd.io"]
     apiVersions: ["v1alpha1"]
-    resources: ["serverauthorizations", "servers"]
+    resources: ["servers"]
   {{- if not .Values.omitWebhookSideEffects }}
   sideEffects: None
   {{- end }}
diff --git a/cli/cmd/authz.go b/cli/cmd/authz.go
index 81b02088c..560818cfe 100644
--- a/cli/cmd/authz.go
+++ b/cli/cmd/authz.go
@@ -1,36 +1,15 @@
 package cmd
 
 import (
-	"context"
 	"fmt"
 	"os"
-	"strings"
 
 	"github.com/linkerd/linkerd2/cli/table"
 	pkgcmd "github.com/linkerd/linkerd2/pkg/cmd"
 	"github.com/linkerd/linkerd2/pkg/k8s"
 	"github.com/spf13/cobra"
-	corev1 "k8s.io/api/core/v1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
-	"k8s.io/apimachinery/pkg/runtime/schema"
-	"k8s.io/client-go/kubernetes"
 )
 
-var sazGVR = schema.GroupVersionResource{
-	Group:    "policy.linkerd.io",
-	Version:  "v1alpha1",
-	Resource: "serverauthorizations",
-}
-
-var serverGVR = schema.GroupVersionResource{
-	Group:    "policy.linkerd.io",
-	Version:  "v1alpha1",
-	Resource: "servers",
-}
-
-type id struct{ name, namespace string }
-
 func newCmdAuthz() *cobra.Command {
 
 	var namespace string
@@ -78,58 +57,16 @@ func newCmdAuthz() *cobra.Command {
 				resource = args[0] + "/" + args[1]
 			}
 
-			pods, err := getPodsForResourceOrKind(cmd.Context(), k8sAPI, namespace, resource)
-			if err != nil {
-				return err
-			}
-			podSet := make(map[id]struct{})
-			for _, pod := range pods {
-				podSet[id{pod.Name, pod.Namespace}] = struct{}{}
-			}
-
 			rows := make([]table.Row, 0)
 
-			sazs, err := k8sAPI.DynamicClient.Resource(sazGVR).Namespace(namespace).List(cmd.Context(), metav1.ListOptions{})
+			authzs, err := k8s.ServerAuthorizationsForResource(cmd.Context(), k8sAPI, namespace, resource)
 			if err != nil {
 				fmt.Fprintf(os.Stderr, "Failed to get serverauthorization resources: %s\n", err)
 				os.Exit(1)
 			}
 
-			for _, saz := range sazs.Items {
-				var servers []unstructured.Unstructured
-
-				if name, found, _ := unstructured.NestedString(saz.UnstructuredContent(), "spec", "server", "name"); found {
-					server, err := k8sAPI.DynamicClient.Resource(serverGVR).Namespace(saz.GetNamespace()).Get(cmd.Context(), name, metav1.GetOptions{})
-					if err != nil {
-						fmt.Fprintf(os.Stderr, "Failed to get server %s: %s\n", name, err)
-						os.Exit(1)
-					}
-					servers = []unstructured.Unstructured{*server}
-				} else if sel, found, _ := unstructured.NestedMap(saz.UnstructuredContent(), "spec", "server", "selector"); found {
-					selector := selector(sel)
-					serverList, err := k8sAPI.DynamicClient.Resource(serverGVR).Namespace(saz.GetNamespace()).List(cmd.Context(), metav1.ListOptions{LabelSelector: metav1.FormatLabelSelector(&selector)})
-					if err != nil {
-						fmt.Fprintf(os.Stderr, "Failed to get servers: %s\n", err)
-						os.Exit(1)
-					}
-					servers = serverList.Items
-				}
-
-				for _, server := range servers {
-					if sel, found, _ := unstructured.NestedMap(server.UnstructuredContent(), "spec", "podSelector"); found {
-						selector := selector(sel)
-						selectedPods, err := k8sAPI.CoreV1().Pods(server.GetNamespace()).List(cmd.Context(), metav1.ListOptions{LabelSelector: metav1.FormatLabelSelector(&selector)})
-						if err != nil {
-							fmt.Fprintf(os.Stderr, "Failed to get pods: %s\n", err)
-							os.Exit(1)
-						}
-						if serverIncludesPod(server, selectedPods.Items, podSet) {
-							rows = append(rows, table.Row{server.GetName(), saz.GetName()})
-						}
-					}
-
-				}
-
+			for _, authz := range authzs {
+				rows = append(rows, table.Row{authz.Server, authz.ServerAuthorization})
 			}
 
 			cols := []table.Column{
@@ -150,202 +87,3 @@ func newCmdAuthz() *cobra.Command {
 		kubeconfigPath, impersonate, impersonateGroup, kubeContext)
 	return cmd
 }
-
-func selector(selector map[string]interface{}) metav1.LabelSelector {
-	if labels, found, err := unstructured.NestedStringMap(selector, "matchLabels"); found && err == nil {
-		return metav1.LabelSelector{MatchLabels: labels}
-	}
-	if expressions, found, err := unstructured.NestedSlice(selector, "matchExpressions"); found && err == nil {
-		exprs := make([]metav1.LabelSelectorRequirement, len(expressions))
-		for i, expr := range expressions {
-			exprs[i] = matchExpression(expr)
-		}
-		return metav1.LabelSelector{MatchExpressions: exprs}
-	}
-	return metav1.LabelSelector{}
-}
-
-func matchExpression(expr interface{}) metav1.LabelSelectorRequirement {
-	if exprMap, ok := expr.(map[string]interface{}); ok {
-		if key, found, err := unstructured.NestedString(exprMap, "key"); found && err == nil {
-			if op, found, err := unstructured.NestedString(exprMap, "operator"); found && err == nil {
-				if values, found, err := unstructured.NestedStringSlice(exprMap, "values"); found && err == nil {
-					return metav1.LabelSelectorRequirement{
-						Key:      key,
-						Operator: metav1.LabelSelectorOperator(op),
-						Values:   values,
-					}
-				}
-			}
-		}
-	}
-	return metav1.LabelSelectorRequirement{}
-}
-
-func serverIncludesPod(server unstructured.Unstructured, serverPods []corev1.Pod, podSet map[id]struct{}) bool {
-	for _, pod := range serverPods {
-		if _, ok := podSet[id{pod.Name, pod.Namespace}]; ok {
-			if port, found, err := unstructured.NestedInt64(server.UnstructuredContent(), "spec", "port"); found && err == nil {
-				for _, container := range pod.Spec.Containers {
-					for _, p := range container.Ports {
-						if int32(port) == p.ContainerPort {
-							return true
-						}
-					}
-				}
-			}
-			if port, found, err := unstructured.NestedString(server.UnstructuredContent(), "spec", "port"); found && err == nil {
-				for _, container := range pod.Spec.Containers {
-					for _, p := range container.Ports {
-						if port == p.Name {
-							return true
-						}
-					}
-				}
-			}
-		}
-	}
-	return false
-}
-
-// getPodsForResourceOrKind is similar to getPodsForResource, but also supports
-// querying for all resources of a given kind (i.e. when resource name is unspecified).
-func getPodsForResourceOrKind(ctx context.Context, k8sAPI kubernetes.Interface, namespace string, resource string) ([]corev1.Pod, error) {
-
-	elems := strings.Split(resource, "/")
-	if len(elems) > 2 {
-		return nil, fmt.Errorf("invalid resource: %s", resource)
-	}
-	if len(elems) == 2 {
-		pods, err := getPodsFor(ctx, k8sAPI, namespace, resource)
-		if err != nil {
-			fmt.Fprintf(os.Stderr, "failed to get pods: %s", err)
-			os.Exit(1)
-		}
-		return pods, nil
-	}
-	pods := []corev1.Pod{}
-
-	typ, err := k8s.CanonicalResourceNameFromFriendlyName(elems[0])
-	if err != nil {
-		return nil, fmt.Errorf("invalid resource: %s", resource)
-	}
-	switch typ {
-	case k8s.Pod:
-		ps, err := k8sAPI.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{})
-		if err != nil {
-			fmt.Fprintf(os.Stderr, "failed to get pods: %s", err)
-			os.Exit(1)
-		}
-		pods = append(pods, ps.Items...)
-
-	case k8s.CronJob:
-		jobs, err := k8sAPI.BatchV1().CronJobs(namespace).List(ctx, metav1.ListOptions{})
-		if err != nil {
-			fmt.Fprintf(os.Stderr, "failed to get cronjobs: %s", err)
-			os.Exit(1)
-		}
-		for _, job := range jobs.Items {
-			ps, err := getPodsFor(ctx, k8sAPI, namespace, fmt.Sprintf("%s/%s", k8s.CronJob, job.Name))
-			if err != nil {
-				fmt.Fprintf(os.Stderr, "failed to get pods: %s", err)
-				os.Exit(1)
-			}
-			pods = append(pods, ps...)
-		}
-
-	case k8s.DaemonSet:
-		dss, err := k8sAPI.AppsV1().DaemonSets(namespace).List(ctx, metav1.ListOptions{})
-		if err != nil {
-			fmt.Fprintf(os.Stderr, "failed to get demonsets: %s", err)
-			os.Exit(1)
-		}
-		for _, ds := range dss.Items {
-			ps, err := getPodsFor(ctx, k8sAPI, namespace, fmt.Sprintf("%s/%s", k8s.DaemonSet, ds.Name))
-			if err != nil {
-				fmt.Fprintf(os.Stderr, "failed to get pods: %s", err)
-				os.Exit(1)
-			}
-			pods = append(pods, ps...)
-		}
-
-	case k8s.Deployment:
-		deploys, err := k8sAPI.AppsV1().Deployments(namespace).List(ctx, metav1.ListOptions{})
-		if err != nil {
-			fmt.Fprintf(os.Stderr, "failed to get deployments: %s", err)
-			os.Exit(1)
-		}
-		for _, deploy := range deploys.Items {
-			ps, err := getPodsFor(ctx, k8sAPI, namespace, fmt.Sprintf("%s/%s", k8s.Deployment, deploy.Name))
-			if err != nil {
-				fmt.Fprintf(os.Stderr, "failed to get pods: %s", err)
-				os.Exit(1)
-			}
-			pods = append(pods, ps...)
-		}
-
-	case k8s.Job:
-		jobs, err := k8sAPI.BatchV1().Jobs(namespace).List(ctx, metav1.ListOptions{})
-		if err != nil {
-			fmt.Fprintf(os.Stderr, "failed to get jobs: %s", err)
-			os.Exit(1)
-		}
-		for _, job := range jobs.Items {
-			ps, err := getPodsFor(ctx, k8sAPI, namespace, fmt.Sprintf("%s/%s", k8s.Job, job.Name))
-			if err != nil {
-				fmt.Fprintf(os.Stderr, "failed to get pods: %s", err)
-				os.Exit(1)
-			}
-			pods = append(pods, ps...)
-		}
-
-	case k8s.ReplicaSet:
-		rss, err := k8sAPI.AppsV1().ReplicaSets(namespace).List(ctx, metav1.ListOptions{})
-		if err != nil {
-			fmt.Fprintf(os.Stderr, "failed to get replicasets: %s", err)
-			os.Exit(1)
-		}
-		for _, rs := range rss.Items {
-			ps, err := getPodsFor(ctx, k8sAPI, namespace, fmt.Sprintf("%s/%s", k8s.ReplicaSet, rs.Name))
-			if err != nil {
-				fmt.Fprintf(os.Stderr, "failed to get pods: %s", err)
-				os.Exit(1)
-			}
-			pods = append(pods, ps...)
-		}
-
-	case k8s.ReplicationController:
-		rcs, err := k8sAPI.CoreV1().ReplicationControllers(namespace).List(ctx, metav1.ListOptions{})
-		if err != nil {
-			fmt.Fprintf(os.Stderr, "failed to get replicationcontrollers: %s", err)
-			os.Exit(1)
-		}
-		for _, rc := range rcs.Items {
-			ps, err := getPodsFor(ctx, k8sAPI, namespace, fmt.Sprintf("%s/%s", k8s.ReplicationController, rc.Name))
-			if err != nil {
-				fmt.Fprintf(os.Stderr, "failed to get pods: %s", err)
-				os.Exit(1)
-			}
-			pods = append(pods, ps...)
-		}
-
-	case k8s.StatefulSet:
-		sss, err := k8sAPI.AppsV1().StatefulSets(namespace).List(ctx, metav1.ListOptions{})
-		if err != nil {
-			fmt.Fprintf(os.Stderr, "failed to get statefulsets: %s", err)
-			os.Exit(1)
-		}
-		for _, ss := range sss.Items {
-			ps, err := getPodsFor(ctx, k8sAPI, namespace, fmt.Sprintf("%s/%s", k8s.StatefulSet, ss.Name))
-			if err != nil {
-				fmt.Fprintf(os.Stderr, "failed to get pods: %s", err)
-				os.Exit(1)
-			}
-			pods = append(pods, ps...)
-		}
-
-	default:
-		return nil, fmt.Errorf("unsupported resource type: %s", typ)
-	}
-	return pods, nil
-}
diff --git a/cli/cmd/metrics.go b/cli/cmd/metrics.go
index 91b8ce3ca..a5c265ae7 100644
--- a/cli/cmd/metrics.go
+++ b/cli/cmd/metrics.go
@@ -2,20 +2,12 @@ package cmd
 
 import (
 	"bytes"
-	"context"
-	"errors"
 	"fmt"
-	"strings"
 	"time"
 
 	pkgcmd "github.com/linkerd/linkerd2/pkg/cmd"
 	"github.com/linkerd/linkerd2/pkg/k8s"
 	"github.com/spf13/cobra"
-	corev1 "k8s.io/api/core/v1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/labels"
-	"k8s.io/apimachinery/pkg/types"
-	"k8s.io/client-go/kubernetes"
 )
 
 type metricsOptions struct {
@@ -83,7 +75,7 @@ func newCmdMetrics() *cobra.Command {
 				return err
 			}
 
-			pods, err := getPodsFor(cmd.Context(), k8sAPI, options.namespace, args[0])
+			pods, err := k8s.GetPodsFor(cmd.Context(), k8sAPI, options.namespace, args[0])
 			if err != nil {
 				return err
 			}
@@ -113,163 +105,3 @@ func newCmdMetrics() *cobra.Command {
 
 	return cmd
 }
-
-// getPodsFor takes a resource string, queries the Kubernetes API, and returns a
-// list of pods belonging to that resource.
-// This could move into `pkg/k8s` if becomes more generally useful.
-func getPodsFor(ctx context.Context, clientset kubernetes.Interface, namespace string, resource string) ([]corev1.Pod, error) {
-	elems := strings.Split(resource, "/")
-
-	if len(elems) == 1 {
-		return nil, errors.New("no resource name provided")
-	}
-
-	if len(elems) != 2 {
-		return nil, fmt.Errorf("invalid resource string: %s", resource)
-	}
-
-	typ, err := k8s.CanonicalResourceNameFromFriendlyName(elems[0])
-	if err != nil {
-		return nil, err
-	}
-	name := elems[1]
-
-	// special case if a single pod was specified
-	if typ == k8s.Pod {
-		pod, err := clientset.CoreV1().Pods(namespace).Get(ctx, name, metav1.GetOptions{})
-		if err != nil {
-			return nil, err
-		}
-		return []corev1.Pod{*pod}, nil
-	}
-
-	var matchLabels map[string]string
-	var ownerUID types.UID
-	switch typ {
-	case k8s.CronJob:
-		jobs, err := clientset.BatchV1().Jobs(namespace).List(ctx, metav1.ListOptions{})
-		if err != nil {
-			return nil, err
-		}
-
-		var pods []corev1.Pod
-		for _, job := range jobs.Items {
-			if isOwner(job.GetUID(), job.GetOwnerReferences()) {
-				jobPods, err := getPodsFor(ctx, clientset, namespace, fmt.Sprintf("%s/%s", k8s.Job, job.GetName()))
-				if err != nil {
-					return nil, err
-				}
-				pods = append(pods, jobPods...)
-			}
-		}
-		return pods, nil
-
-	case k8s.DaemonSet:
-		ds, err := clientset.AppsV1().DaemonSets(namespace).Get(ctx, name, metav1.GetOptions{})
-		if err != nil {
-			return nil, err
-		}
-		matchLabels = ds.Spec.Selector.MatchLabels
-		ownerUID = ds.GetUID()
-
-	case k8s.Deployment:
-		deployment, err := clientset.AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{})
-		if err != nil {
-			return nil, err
-		}
-		matchLabels = deployment.Spec.Selector.MatchLabels
-		ownerUID = deployment.GetUID()
-
-		replicaSets, err := clientset.AppsV1().ReplicaSets(namespace).List(
-			ctx,
-			metav1.ListOptions{
-				LabelSelector: labels.Set(matchLabels).AsSelector().String(),
-			},
-		)
-		if err != nil {
-			return nil, err
-		}
-
-		var pods []corev1.Pod
-		for _, rs := range replicaSets.Items {
-			if isOwner(ownerUID, rs.GetOwnerReferences()) {
-				podsRS, err := getPodsFor(ctx, clientset, namespace, fmt.Sprintf("%s/%s", k8s.ReplicaSet, rs.GetName()))
-				if err != nil {
-					return nil, err
-				}
-				pods = append(pods, podsRS...)
-			}
-		}
-		return pods, nil
-
-	case k8s.Job:
-		job, err := clientset.BatchV1().Jobs(namespace).Get(ctx, name, metav1.GetOptions{})
-		if err != nil {
-			return nil, err
-		}
-		matchLabels = job.Spec.Selector.MatchLabels
-		ownerUID = job.GetUID()
-
-	case k8s.ReplicaSet:
-		rs, err := clientset.AppsV1().ReplicaSets(namespace).Get(ctx, name, metav1.GetOptions{})
-		if err != nil {
-			return nil, err
-		}
-		matchLabels = rs.Spec.Selector.MatchLabels
-		ownerUID = rs.GetUID()
-
-	case k8s.ReplicationController:
-		rc, err := clientset.CoreV1().ReplicationControllers(namespace).Get(ctx, name, metav1.GetOptions{})
-		if err != nil {
-			return nil, err
-		}
-		matchLabels = rc.Spec.Selector
-		ownerUID = rc.GetUID()
-
-	case k8s.StatefulSet:
-		ss, err := clientset.AppsV1().StatefulSets(namespace).Get(ctx, name, metav1.GetOptions{})
-		if err != nil {
-			return nil, err
-		}
-		matchLabels = ss.Spec.Selector.MatchLabels
-		ownerUID = ss.GetUID()
-
-	default:
-		return nil, fmt.Errorf("unsupported resource type: %s", name)
-	}
-
-	podList, err := clientset.
-		CoreV1().
-		Pods(namespace).
-		List(
-			ctx,
-			metav1.ListOptions{
-				LabelSelector: labels.Set(matchLabels).AsSelector().String(),
-			},
-		)
-	if err != nil {
-		return nil, err
-	}
-
-	if ownerUID == "" {
-		return podList.Items, nil
-	}
-
-	pods := []corev1.Pod{}
-	for _, pod := range podList.Items {
-		if isOwner(ownerUID, pod.GetOwnerReferences()) {
-			pods = append(pods, pod)
-		}
-	}
-
-	return pods, nil
-}
-
-func isOwner(u types.UID, ownerRefs []metav1.OwnerReference) bool {
-	for _, or := range ownerRefs {
-		if u == or.UID {
-			return true
-		}
-	}
-	return false
-}
diff --git a/cli/table/table.go b/cli/table/table.go
index d405c2d68..ba585f721 100644
--- a/cli/table/table.go
+++ b/cli/table/table.go
@@ -43,6 +43,21 @@ func NewTable(cols []Column, data []Row) Table {
 	}
 }
 
+// NewColumn creates a new flexible column with the given name.
+func NewColumn(header string) Column {
+	return Column{
+		Header:   header,
+		Flexible: true,
+		Width:    len(header),
+	}
+}
+
+// WithLeftAlign turns on the left align of this column and returns it.
+func (c Column) WithLeftAlign() Column {
+	c.LeftAlign = true
+	return c
+}
+
 // Render writes the full table to the given Writer.
 func (t *Table) Render(w io.Writer) {
 	columnWidths := t.columnWidths()
diff --git a/go.mod b/go.mod
index 4d92b894f..872dc6a05 100644
--- a/go.mod
+++ b/go.mod
@@ -38,8 +38,8 @@ require (
 	github.com/spf13/cobra v1.2.1
 	github.com/spf13/pflag v1.0.5
 	go.opencensus.io v0.23.0
-	golang.org/x/net v0.0.0-20210525063256-abc453219eb5
-	golang.org/x/tools v0.1.5
+	golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d
+	golang.org/x/tools v0.1.6
 	google.golang.org/grpc v1.40.0
 	google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0
 	google.golang.org/protobuf v1.27.1
diff --git a/go.sum b/go.sum
index 5bb90938e..b443ba545 100644
--- a/go.sum
+++ b/go.sum
@@ -866,6 +866,7 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
 github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 h1:+lm10QQTNSBd8DVTNGHx7o/IKu9HYDvLMffDhbyLccI=
 github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs=
 github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 h1:hlE8//ciYMztlGpl/VA+Zm1AcTPHYkHJPbHqE6WJUXE=
@@ -1019,6 +1020,8 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/net v0.0.0-20210525063256-abc453219eb5 h1:wjuX4b5yYQnEQHzd+CBcrcC6OVR2J1CN6mUy0oSxIPo=
 golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d h1:20cMwl2fHAzkJMEA+8J4JgqBQcQGzbisXo31MIeenXI=
+golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1118,6 +1121,8 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e h1:WUoyKPm6nCo1BnNUvPGnFG3T5DUVem42yDJZZ4CNxMA=
+golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE=
@@ -1205,6 +1210,8 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
 golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA=
 golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.6 h1:SIasE1FVIQOWz2GEAHFOmoW7xchJcqlucjSULTL0Ag4=
+golang.org/x/tools v0.1.6/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/pkg/k8s/api.go b/pkg/k8s/api.go
index 967ca55b7..0ae270d9c 100644
--- a/pkg/k8s/api.go
+++ b/pkg/k8s/api.go
@@ -2,6 +2,7 @@ package k8s
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"net/http"
 	"strings"
@@ -12,8 +13,10 @@ import (
 	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
 	apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
-	"k8s.io/apimachinery/pkg/api/errors"
+	kerrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/apimachinery/pkg/version"
 	"k8s.io/client-go/dynamic"
 	"k8s.io/client-go/kubernetes"
@@ -137,7 +140,7 @@ func (kubeAPI *KubernetesAPI) CheckVersion(versionInfo *version.Info) error {
 // NamespaceExists validates whether a given namespace exists.
 func (kubeAPI *KubernetesAPI) NamespaceExists(ctx context.Context, namespace string) (bool, error) {
 	ns, err := kubeAPI.CoreV1().Namespaces().Get(ctx, namespace, metav1.GetOptions{})
-	if errors.IsNotFound(err) {
+	if kerrors.IsNotFound(err) {
 		return false, nil
 	}
 	if err != nil {
@@ -197,7 +200,7 @@ func (kubeAPI *KubernetesAPI) GetNamespaceWithExtensionLabel(ctx context.Context
 			return &ns, err
 		}
 	}
-	return nil, errors.NewNotFound(corev1.Resource("namespace"), value)
+	return nil, kerrors.NewNotFound(corev1.Resource("namespace"), value)
 }
 
 // GetPodStatus receives a pod and returns the pod status, based on `kubectl` logic.
@@ -285,3 +288,162 @@ func GetProxyVersion(pod corev1.Pod) string {
 	}
 	return ""
 }
+
+// GetPodsFor takes a resource string, queries the Kubernetes API, and returns a
+// list of pods belonging to that resource.
+func GetPodsFor(ctx context.Context, clientset kubernetes.Interface, namespace string, resource string) ([]corev1.Pod, error) {
+	elems := strings.Split(resource, "/")
+
+	if len(elems) == 1 {
+		return nil, errors.New("no resource name provided")
+	}
+
+	if len(elems) != 2 {
+		return nil, fmt.Errorf("invalid resource string: %s", resource)
+	}
+
+	typ, err := CanonicalResourceNameFromFriendlyName(elems[0])
+	if err != nil {
+		return nil, err
+	}
+	name := elems[1]
+
+	// special case if a single pod was specified
+	if typ == Pod {
+		pod, err := clientset.CoreV1().Pods(namespace).Get(ctx, name, metav1.GetOptions{})
+		if err != nil {
+			return nil, err
+		}
+		return []corev1.Pod{*pod}, nil
+	}
+
+	var matchLabels map[string]string
+	var ownerUID types.UID
+	switch typ {
+	case CronJob:
+		jobs, err := clientset.BatchV1().Jobs(namespace).List(ctx, metav1.ListOptions{})
+		if err != nil {
+			return nil, err
+		}
+
+		var pods []corev1.Pod
+		for _, job := range jobs.Items {
+			if isOwner(job.GetUID(), job.GetOwnerReferences()) {
+				jobPods, err := GetPodsFor(ctx, clientset, namespace, fmt.Sprintf("%s/%s", Job, job.GetName()))
+				if err != nil {
+					return nil, err
+				}
+				pods = append(pods, jobPods...)
+			}
+		}
+		return pods, nil
+
+	case DaemonSet:
+		ds, err := clientset.AppsV1().DaemonSets(namespace).Get(ctx, name, metav1.GetOptions{})
+		if err != nil {
+			return nil, err
+		}
+		matchLabels = ds.Spec.Selector.MatchLabels
+		ownerUID = ds.GetUID()
+
+	case Deployment:
+		deployment, err := clientset.AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{})
+		if err != nil {
+			return nil, err
+		}
+		matchLabels = deployment.Spec.Selector.MatchLabels
+		ownerUID = deployment.GetUID()
+
+		replicaSets, err := clientset.AppsV1().ReplicaSets(namespace).List(
+			ctx,
+			metav1.ListOptions{
+				LabelSelector: labels.Set(matchLabels).AsSelector().String(),
+			},
+		)
+		if err != nil {
+			return nil, err
+		}
+
+		var pods []corev1.Pod
+		for _, rs := range replicaSets.Items {
+			if isOwner(ownerUID, rs.GetOwnerReferences()) {
+				podsRS, err := GetPodsFor(ctx, clientset, namespace, fmt.Sprintf("%s/%s", ReplicaSet, rs.GetName()))
+				if err != nil {
+					return nil, err
+				}
+				pods = append(pods, podsRS...)
+			}
+		}
+		return pods, nil
+
+	case Job:
+		job, err := clientset.BatchV1().Jobs(namespace).Get(ctx, name, metav1.GetOptions{})
+		if err != nil {
+			return nil, err
+		}
+		matchLabels = job.Spec.Selector.MatchLabels
+		ownerUID = job.GetUID()
+
+	case ReplicaSet:
+		rs, err := clientset.AppsV1().ReplicaSets(namespace).Get(ctx, name, metav1.GetOptions{})
+		if err != nil {
+			return nil, err
+		}
+		matchLabels = rs.Spec.Selector.MatchLabels
+		ownerUID = rs.GetUID()
+
+	case ReplicationController:
+		rc, err := clientset.CoreV1().ReplicationControllers(namespace).Get(ctx, name, metav1.GetOptions{})
+		if err != nil {
+			return nil, err
+		}
+		matchLabels = rc.Spec.Selector
+		ownerUID = rc.GetUID()
+
+	case StatefulSet:
+		ss, err := clientset.AppsV1().StatefulSets(namespace).Get(ctx, name, metav1.GetOptions{})
+		if err != nil {
+			return nil, err
+		}
+		matchLabels = ss.Spec.Selector.MatchLabels
+		ownerUID = ss.GetUID()
+
+	default:
+		return nil, fmt.Errorf("unsupported resource type: %s", name)
+	}
+
+	podList, err := clientset.
+		CoreV1().
+		Pods(namespace).
+		List(
+			ctx,
+			metav1.ListOptions{
+				LabelSelector: labels.Set(matchLabels).AsSelector().String(),
+			},
+		)
+	if err != nil {
+		return nil, err
+	}
+
+	if ownerUID == "" {
+		return podList.Items, nil
+	}
+
+	pods := []corev1.Pod{}
+	for _, pod := range podList.Items {
+		if isOwner(ownerUID, pod.GetOwnerReferences()) {
+			pods = append(pods, pod)
+		}
+	}
+
+	return pods, nil
+}
+
+func isOwner(u types.UID, ownerRefs []metav1.OwnerReference) bool {
+	for _, or := range ownerRefs {
+		if u == or.UID {
+			return true
+		}
+	}
+	return false
+}
diff --git a/pkg/k8s/policy.go b/pkg/k8s/policy.go
new file mode 100644
index 000000000..d1f8f152c
--- /dev/null
+++ b/pkg/k8s/policy.go
@@ -0,0 +1,374 @@
+package k8s
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"strings"
+
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/client-go/kubernetes"
+)
+
+// ServerAndAuthorization holds a server name and serverauthorization name.
+type ServerAndAuthorization struct {
+	Server              string
+	ServerAuthorization string
+}
+
+type id struct{ name, namespace string }
+
+// SazGVR is the GroupVersionResource for the ServerAuthorization resource.
+var SazGVR = schema.GroupVersionResource{
+	Group:    "policy.linkerd.io",
+	Version:  "v1alpha1",
+	Resource: "serverauthorizations",
+}
+
+// ServerGVR is the GroupVersionResource for the Server resource.
+var ServerGVR = schema.GroupVersionResource{
+	Group:    "policy.linkerd.io",
+	Version:  "v1alpha1",
+	Resource: "servers",
+}
+
+// ServerAuthorizationsForResource returns a list of Server-ServerAuthorization
+// pairs which select pods belonging to the given resource.
+func ServerAuthorizationsForResource(ctx context.Context, k8sAPI *KubernetesAPI, namespace string, resource string) ([]ServerAndAuthorization, error) {
+	pods, err := getPodsForResourceOrKind(ctx, k8sAPI, namespace, resource, "")
+	if err != nil {
+		return nil, err
+	}
+	podSet := make(map[id]struct{})
+	for _, pod := range pods {
+		podSet[id{pod.Name, pod.Namespace}] = struct{}{}
+	}
+
+	results := make([]ServerAndAuthorization, 0)
+
+	sazs, err := k8sAPI.DynamicClient.Resource(SazGVR).Namespace(namespace).List(ctx, metav1.ListOptions{})
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "Failed to get serverauthorization resources: %s\n", err)
+		os.Exit(1)
+	}
+
+	for _, saz := range sazs.Items {
+		var servers []unstructured.Unstructured
+
+		if name, found, _ := unstructured.NestedString(saz.UnstructuredContent(), "spec", "server", "name"); found {
+			server, err := k8sAPI.DynamicClient.Resource(ServerGVR).Namespace(saz.GetNamespace()).Get(ctx, name, metav1.GetOptions{})
+			if err != nil {
+				fmt.Fprintf(os.Stderr, "Failed to get server %s: %s\n", name, err)
+				os.Exit(1)
+			}
+			servers = []unstructured.Unstructured{*server}
+		} else if sel, found, _ := unstructured.NestedMap(saz.UnstructuredContent(), "spec", "server", "selector"); found {
+			selector := selector(sel)
+			serverList, err := k8sAPI.DynamicClient.Resource(ServerGVR).Namespace(saz.GetNamespace()).List(ctx, metav1.ListOptions{LabelSelector: metav1.FormatLabelSelector(&selector)})
+			if err != nil {
+				fmt.Fprintf(os.Stderr, "Failed to get servers: %s\n", err)
+				os.Exit(1)
+			}
+			servers = serverList.Items
+		}
+
+		for _, server := range servers {
+			if sel, found, _ := unstructured.NestedMap(server.UnstructuredContent(), "spec", "podSelector"); found {
+				selector := selector(sel)
+				selectedPods, err := k8sAPI.CoreV1().Pods(server.GetNamespace()).List(ctx, metav1.ListOptions{LabelSelector: metav1.FormatLabelSelector(&selector)})
+				if err != nil {
+					fmt.Fprintf(os.Stderr, "Failed to get pods: %s\n", err)
+					os.Exit(1)
+				}
+				if serverIncludesPod(server, selectedPods.Items, podSet) {
+					results = append(results, ServerAndAuthorization{server.GetName(), saz.GetName()})
+				}
+			}
+
+		}
+	}
+	return results, nil
+}
+
+// ServersForResource returns a list of Server names of Servers which select pods
+// belonging to the given resource.
+func ServersForResource(ctx context.Context, k8sAPI *KubernetesAPI, namespace string, resource string, labelSelector string) ([]string, error) {
+	pods, err := getPodsForResourceOrKind(ctx, k8sAPI, namespace, resource, labelSelector)
+	if err != nil {
+		return nil, err
+	}
+	podSet := make(map[id]struct{})
+	for _, pod := range pods {
+		podSet[id{pod.Name, pod.Namespace}] = struct{}{}
+	}
+
+	results := make([]string, 0)
+
+	servers, err := k8sAPI.DynamicClient.Resource(ServerGVR).Namespace(namespace).List(ctx, metav1.ListOptions{})
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "Failed to get serverauthorization resources: %s\n", err)
+		os.Exit(1)
+	}
+
+	for _, server := range servers.Items {
+		if sel, found, _ := unstructured.NestedMap(server.UnstructuredContent(), "spec", "podSelector"); found {
+			selector := selector(sel)
+			selectedPods, err := k8sAPI.CoreV1().Pods(server.GetNamespace()).List(ctx, metav1.ListOptions{LabelSelector: metav1.FormatLabelSelector(&selector)})
+			if err != nil {
+				fmt.Fprintf(os.Stderr, "Failed to get pods: %s\n", err)
+				os.Exit(1)
+			}
+			if serverIncludesPod(server, selectedPods.Items, podSet) {
+				results = append(results, server.GetName())
+			}
+		}
+
+	}
+	return results, nil
+}
+
+// ServerAuthorizationsForServer returns a list of ServerAuthorization names of
+// ServerAuthorizations which select the given Server.
+func ServerAuthorizationsForServer(ctx context.Context, k8sAPI *KubernetesAPI, namespace string, server string) ([]string, error) {
+	results := make([]string, 0)
+
+	sazs, err := k8sAPI.DynamicClient.Resource(SazGVR).Namespace(namespace).List(ctx, metav1.ListOptions{})
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "Failed to get serverauthorization resources: %s\n", err)
+		os.Exit(1)
+	}
+
+	for _, saz := range sazs.Items {
+		if name, found, _ := unstructured.NestedString(saz.UnstructuredContent(), "spec", "server", "name"); found {
+			s, err := k8sAPI.DynamicClient.Resource(ServerGVR).Namespace(saz.GetNamespace()).Get(ctx, name, metav1.GetOptions{})
+			if err != nil {
+				fmt.Fprintf(os.Stderr, "Failed to get server %s: %s\n", name, err)
+				os.Exit(1)
+			}
+			if s.GetName() == server {
+				results = append(results, saz.GetName())
+			}
+		} else if sel, found, _ := unstructured.NestedMap(saz.UnstructuredContent(), "spec", "server", "selector"); found {
+			selector := selector(sel)
+			serverList, err := k8sAPI.DynamicClient.Resource(ServerGVR).Namespace(saz.GetNamespace()).List(ctx, metav1.ListOptions{LabelSelector: metav1.FormatLabelSelector(&selector)})
+			if err != nil {
+				fmt.Fprintf(os.Stderr, "Failed to get servers: %s\n", err)
+				os.Exit(1)
+			}
+			for _, s := range serverList.Items {
+				if s.GetName() == server {
+					results = append(results, saz.GetName())
+					break
+				}
+			}
+		}
+	}
+	return results, nil
+}
+
+func selector(selector map[string]interface{}) metav1.LabelSelector {
+	if labels, found, err := unstructured.NestedStringMap(selector, "matchLabels"); found && err == nil {
+		return metav1.LabelSelector{MatchLabels: labels}
+	}
+	if expressions, found, err := unstructured.NestedSlice(selector, "matchExpressions"); found && err == nil {
+		exprs := make([]metav1.LabelSelectorRequirement, len(expressions))
+		for i, expr := range expressions {
+			exprs[i] = matchExpression(expr)
+		}
+		return metav1.LabelSelector{MatchExpressions: exprs}
+	}
+	return metav1.LabelSelector{}
+}
+
+func matchExpression(expr interface{}) metav1.LabelSelectorRequirement {
+	if exprMap, ok := expr.(map[string]interface{}); ok {
+		if key, found, err := unstructured.NestedString(exprMap, "key"); found && err == nil {
+			if op, found, err := unstructured.NestedString(exprMap, "operator"); found && err == nil {
+				if values, found, err := unstructured.NestedStringSlice(exprMap, "values"); found && err == nil {
+					return metav1.LabelSelectorRequirement{
+						Key:      key,
+						Operator: metav1.LabelSelectorOperator(op),
+						Values:   values,
+					}
+				}
+			}
+		}
+	}
+	return metav1.LabelSelectorRequirement{}
+}
+
+func serverIncludesPod(server unstructured.Unstructured, serverPods []corev1.Pod, podSet map[id]struct{}) bool {
+	for _, pod := range serverPods {
+		if _, ok := podSet[id{pod.Name, pod.Namespace}]; ok {
+			if port, found, err := unstructured.NestedInt64(server.UnstructuredContent(), "spec", "port"); found && err == nil {
+				for _, container := range pod.Spec.Containers {
+					for _, p := range container.Ports {
+						if int32(port) == p.ContainerPort {
+							return true
+						}
+					}
+				}
+			}
+			if port, found, err := unstructured.NestedString(server.UnstructuredContent(), "spec", "port"); found && err == nil {
+				for _, container := range pod.Spec.Containers {
+					for _, p := range container.Ports {
+						if port == p.Name {
+							return true
+						}
+					}
+				}
+			}
+		}
+	}
+	return false
+}
+
+// getPodsForResourceOrKind is similar to getPodsForResource, but also supports
+// querying for all resources of a given kind (i.e. when resource name is unspecified).
+func getPodsForResourceOrKind(ctx context.Context, k8sAPI kubernetes.Interface, namespace string, resource string, labelSelector string) ([]corev1.Pod, error) {
+
+	elems := strings.Split(resource, "/")
+	if len(elems) > 2 {
+		return nil, fmt.Errorf("invalid resource: %s", resource)
+	}
+	if len(elems) == 2 {
+		pods, err := GetPodsFor(ctx, k8sAPI, namespace, resource)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "failed to get pods: %s", err)
+			os.Exit(1)
+		}
+		return pods, nil
+	}
+	pods := []corev1.Pod{}
+
+	typ, err := CanonicalResourceNameFromFriendlyName(elems[0])
+	if err != nil {
+		return nil, fmt.Errorf("invalid resource: %s", resource)
+	}
+
+	selector := metav1.ListOptions{
+		LabelSelector: labelSelector,
+	}
+
+	switch typ {
+	case Pod:
+		ps, err := k8sAPI.CoreV1().Pods(namespace).List(ctx, selector)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "failed to get pods: %s", err)
+			os.Exit(1)
+		}
+		pods = append(pods, ps.Items...)
+
+	case CronJob:
+		jobs, err := k8sAPI.BatchV1().CronJobs(namespace).List(ctx, selector)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "failed to get cronjobs: %s", err)
+			os.Exit(1)
+		}
+		for _, job := range jobs.Items {
+			ps, err := GetPodsFor(ctx, k8sAPI, namespace, fmt.Sprintf("%s/%s", CronJob, job.Name))
+			if err != nil {
+				fmt.Fprintf(os.Stderr, "failed to get pods: %s", err)
+				os.Exit(1)
+			}
+			pods = append(pods, ps...)
+		}
+
+	case DaemonSet:
+		dss, err := k8sAPI.AppsV1().DaemonSets(namespace).List(ctx, selector)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "failed to get demonsets: %s", err)
+			os.Exit(1)
+		}
+		for _, ds := range dss.Items {
+			ps, err := GetPodsFor(ctx, k8sAPI, namespace, fmt.Sprintf("%s/%s", DaemonSet, ds.Name))
+			if err != nil {
+				fmt.Fprintf(os.Stderr, "failed to get pods: %s", err)
+				os.Exit(1)
+			}
+			pods = append(pods, ps...)
+		}
+
+	case Deployment:
+		deploys, err := k8sAPI.AppsV1().Deployments(namespace).List(ctx, selector)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "failed to get deployments: %s", err)
+			os.Exit(1)
+		}
+		for _, deploy := range deploys.Items {
+			ps, err := GetPodsFor(ctx, k8sAPI, namespace, fmt.Sprintf("%s/%s", Deployment, deploy.Name))
+			if err != nil {
+				fmt.Fprintf(os.Stderr, "failed to get pods: %s", err)
+				os.Exit(1)
+			}
+			pods = append(pods, ps...)
+		}
+
+	case Job:
+		jobs, err := k8sAPI.BatchV1().Jobs(namespace).List(ctx, selector)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "failed to get jobs: %s", err)
+			os.Exit(1)
+		}
+		for _, job := range jobs.Items {
+			ps, err := GetPodsFor(ctx, k8sAPI, namespace, fmt.Sprintf("%s/%s", Job, job.Name))
+			if err != nil {
+				fmt.Fprintf(os.Stderr, "failed to get pods: %s", err)
+				os.Exit(1)
+			}
+			pods = append(pods, ps...)
+		}
+
+	case ReplicaSet:
+		rss, err := k8sAPI.AppsV1().ReplicaSets(namespace).List(ctx, selector)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "failed to get replicasets: %s", err)
+			os.Exit(1)
+		}
+		for _, rs := range rss.Items {
+			ps, err := GetPodsFor(ctx, k8sAPI, namespace, fmt.Sprintf("%s/%s", ReplicaSet, rs.Name))
+			if err != nil {
+				fmt.Fprintf(os.Stderr, "failed to get pods: %s", err)
+				os.Exit(1)
+			}
+			pods = append(pods, ps...)
+		}
+
+	case ReplicationController:
+		rcs, err := k8sAPI.CoreV1().ReplicationControllers(namespace).List(ctx, selector)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "failed to get replicationcontrollers: %s", err)
+			os.Exit(1)
+		}
+		for _, rc := range rcs.Items {
+			ps, err := GetPodsFor(ctx, k8sAPI, namespace, fmt.Sprintf("%s/%s", ReplicationController, rc.Name))
+			if err != nil {
+				fmt.Fprintf(os.Stderr, "failed to get pods: %s", err)
+				os.Exit(1)
+			}
+			pods = append(pods, ps...)
+		}
+
+	case StatefulSet:
+		sss, err := k8sAPI.AppsV1().StatefulSets(namespace).List(ctx, selector)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "failed to get statefulsets: %s", err)
+			os.Exit(1)
+		}
+		for _, ss := range sss.Items {
+			ps, err := GetPodsFor(ctx, k8sAPI, namespace, fmt.Sprintf("%s/%s", StatefulSet, ss.Name))
+			if err != nil {
+				fmt.Fprintf(os.Stderr, "failed to get pods: %s", err)
+				os.Exit(1)
+			}
+			pods = append(pods, ps...)
+		}
+
+	default:
+		return nil, fmt.Errorf("unsupported resource type: %s", typ)
+	}
+	return pods, nil
+}
diff --git a/viz/cmd/authz.go b/viz/cmd/authz.go
new file mode 100644
index 000000000..3093111da
--- /dev/null
+++ b/viz/cmd/authz.go
@@ -0,0 +1,196 @@
+package cmd
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/linkerd/linkerd2/cli/table"
+	pkgcmd "github.com/linkerd/linkerd2/pkg/cmd"
+	"github.com/linkerd/linkerd2/pkg/healthcheck"
+	"github.com/linkerd/linkerd2/pkg/k8s"
+	"github.com/linkerd/linkerd2/viz/metrics-api/util"
+	"github.com/linkerd/linkerd2/viz/pkg/api"
+	"github.com/spf13/cobra"
+)
+
+// NewCmdAuthz creates a new cobra command `authz`
+func NewCmdAuthz() *cobra.Command {
+	options := newStatOptions()
+
+	cmd := &cobra.Command{
+		Use:   "authz [flags] resource",
+		Short: "Display stats for server authorizations for a resource",
+		Long:  "Display stats for server authorizations for a resource.",
+		Args:  cobra.MinimumNArgs(1),
+		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+
+			k8sAPI, err := k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, impersonateGroup, 0)
+			if err != nil {
+				return nil, cobra.ShellCompDirectiveError
+			}
+
+			if options.namespace == "" {
+				options.namespace = pkgcmd.GetDefaultNamespace(kubeconfigPath, kubeContext)
+			}
+
+			cc := k8s.NewCommandCompletion(k8sAPI, options.namespace)
+
+			results, err := cc.Complete(args, toComplete)
+			if err != nil {
+				return nil, cobra.ShellCompDirectiveError
+			}
+
+			return results, cobra.ShellCompDirectiveDefault
+		},
+		RunE: func(cmd *cobra.Command, args []string) error {
+			if options.namespace == "" {
+				options.namespace = pkgcmd.GetDefaultNamespace(kubeconfigPath, kubeContext)
+			}
+
+			k8sAPI, err := k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, impersonateGroup, 0)
+			if err != nil {
+				return err
+			}
+			// The gRPC client is concurrency-safe, so we can reuse it in all the following goroutines
+			// https://github.com/grpc/grpc-go/issues/682
+			client := api.CheckClientOrExit(healthcheck.Options{
+				ControlPlaneNamespace: controlPlaneNamespace,
+				KubeConfig:            kubeconfigPath,
+				Impersonate:           impersonate,
+				ImpersonateGroup:      impersonateGroup,
+				KubeContext:           kubeContext,
+				APIAddr:               apiAddr,
+			})
+
+			var resource string
+			if len(args) == 1 {
+				resource = args[0]
+			} else if len(args) == 2 {
+				resource = args[0] + "/" + args[1]
+			}
+
+			cols := []table.Column{
+				table.NewColumn("SERVER").WithLeftAlign(),
+				table.NewColumn("AUTHZ").WithLeftAlign(),
+				table.NewColumn("SUCCESS"),
+				table.NewColumn("RPS"),
+				table.NewColumn("LATENCY_P50"),
+				table.NewColumn("LATENCY_P95"),
+				table.NewColumn("LATENCY_P99"),
+			}
+			rows := []table.Row{}
+
+			servers, err := k8s.ServersForResource(cmd.Context(), k8sAPI, options.namespace, resource, options.labelSelector)
+			if err != nil {
+				fmt.Fprint(os.Stderr, err.Error())
+				os.Exit(1)
+			}
+			for _, server := range servers {
+				sazs, err := k8s.ServerAuthorizationsForServer(cmd.Context(), k8sAPI, options.namespace, server)
+				if err != nil {
+					fmt.Fprint(os.Stderr, err.Error())
+					os.Exit(1)
+				}
+				for _, saz := range sazs {
+					requestParams := util.StatsSummaryRequestParams{
+						StatsBaseRequestParams: util.StatsBaseRequestParams{
+							TimeWindow:    options.timeWindow,
+							ResourceName:  saz,
+							ResourceType:  k8s.ServerAuthorization,
+							Namespace:     options.namespace,
+							AllNamespaces: false,
+						},
+						ToNamespace: options.namespace,
+					}
+					requestParams.ToName = server
+					requestParams.ToType = k8s.Server
+
+					req, err := util.BuildStatSummaryRequest(requestParams)
+					if err != nil {
+						return err
+					}
+					resp, err := requestStatsFromAPI(client, req)
+					if err != nil {
+						fmt.Fprint(os.Stderr, err.Error())
+						os.Exit(1)
+					}
+
+					for _, row := range respToRows(resp) {
+						if row.Stats == nil {
+							rows = append(rows, table.Row{
+								server,
+								saz,
+								"-",
+								"-",
+								"-",
+								"-",
+								"-",
+							})
+						} else {
+							rows = append(rows, table.Row{
+								server,
+								saz,
+								fmt.Sprintf("%.2f%%", getSuccessRate(row.Stats.GetSuccessCount(), row.Stats.GetFailureCount())*100),
+								fmt.Sprintf("%.1frps", getRequestRate(row.Stats.GetSuccessCount(), row.Stats.GetFailureCount(), row.TimeWindow)),
+								fmt.Sprintf("%dms", row.Stats.LatencyMsP50),
+								fmt.Sprintf("%dms", row.Stats.LatencyMsP95),
+								fmt.Sprintf("%dms", row.Stats.LatencyMsP99),
+							})
+						}
+					}
+				}
+
+				// Unauthorized
+				requestParams := util.StatsSummaryRequestParams{
+					StatsBaseRequestParams: util.StatsBaseRequestParams{
+						TimeWindow:    options.timeWindow,
+						ResourceName:  server,
+						ResourceType:  k8s.Server,
+						Namespace:     options.namespace,
+						AllNamespaces: false,
+					},
+					ToNamespace:   options.namespace,
+					LabelSelector: options.labelSelector,
+				}
+
+				req, err := util.BuildStatSummaryRequest(requestParams)
+				if err != nil {
+					return err
+				}
+				resp, err := requestStatsFromAPI(client, req)
+				if err != nil {
+					fmt.Fprint(os.Stderr, err.Error())
+					os.Exit(1)
+				}
+				for _, row := range respToRows(resp) {
+					if row.SrvStats != nil && row.SrvStats.DeniedCount > 0 {
+						rows = append(rows, table.Row{
+							server,
+							"[UNAUTHORIZED]",
+							"-",
+							fmt.Sprintf("%.1frps", getRequestRate(row.SrvStats.DeniedCount, 0, row.TimeWindow)),
+							"-",
+							"-",
+							"-",
+						})
+					}
+				}
+			}
+
+			data := table.NewTable(cols, rows)
+			data.Render(os.Stdout)
+
+			return nil
+		},
+	}
+
+	cmd.PersistentFlags().StringVarP(&options.namespace, "namespace", "n", options.namespace, "Namespace of the specified resource")
+	cmd.PersistentFlags().StringVarP(&options.timeWindow, "time-window", "t", options.timeWindow, "Stat window (for example: \"15s\", \"1m\", \"10m\", \"1h\"). Needs to be at least 15s.")
+	cmd.PersistentFlags().StringVarP(&options.outputFormat, "output", "o", options.outputFormat, "Output format; one of: \"table\" or \"json\" or \"wide\"")
+	cmd.PersistentFlags().StringVarP(&options.labelSelector, "selector", "l", options.labelSelector, "Selector (label query) to filter on, supports '=', '==', and '!='")
+
+	pkgcmd.ConfigureNamespaceFlagCompletion(
+		cmd, []string{"namespace"},
+		kubeconfigPath, impersonate, impersonateGroup, kubeContext)
+	return cmd
+}
diff --git a/viz/cmd/root.go b/viz/cmd/root.go
index e832da7ae..9578c0c8f 100644
--- a/viz/cmd/root.go
+++ b/viz/cmd/root.go
@@ -76,6 +76,7 @@ func NewCmdViz() *cobra.Command {
 	vizCmd.PersistentFlags().StringArrayVar(&impersonateGroup, "as-group", []string{}, "Group to impersonate for Kubernetes operations")
 	vizCmd.PersistentFlags().StringVar(&apiAddr, "api-addr", "", "Override kubeconfig and communicate directly with the control plane at host:port (mostly for testing)")
 	vizCmd.PersistentFlags().BoolVar(&verbose, "verbose", false, "Turn on debug logging")
+	vizCmd.AddCommand(NewCmdAuthz())
 	vizCmd.AddCommand(NewCmdCheck())
 	vizCmd.AddCommand(NewCmdDashboard())
 	vizCmd.AddCommand(NewCmdEdges())
diff --git a/viz/metrics-api/prometheus.go b/viz/metrics-api/prometheus.go
index a901e5e3c..0f05b7a09 100644
--- a/viz/metrics-api/prometheus.go
+++ b/viz/metrics-api/prometheus.go
@@ -118,8 +118,14 @@ func promDstGroupByLabelNames(resource *pb.Resource) model.LabelNames {
 func promQueryLabels(resource *pb.Resource) model.LabelSet {
 	set := model.LabelSet{}
 	if resource != nil {
-		if resource.Name != "" && resource.GetType() != k8s.Service {
-			set[promResourceType(resource)] = model.LabelValue(resource.Name)
+		if resource.Name != "" {
+			if resource.GetType() == k8s.Server {
+				set[serverLabel] = model.LabelValue(resource.GetName())
+			} else if resource.GetType() == k8s.ServerAuthorization {
+				set[serverAuthorizationLabel] = model.LabelValue(resource.GetName())
+			} else if resource.GetType() != k8s.Service {
+				set[promResourceType(resource)] = model.LabelValue(resource.Name)
+			}
 		}
 		if shouldAddNamespaceLabel(resource) {
 			set[namespaceLabel] = model.LabelValue(resource.Namespace)
diff --git a/viz/metrics-api/stat_summary.go b/viz/metrics-api/stat_summary.go
index a26348716..eba146cb0 100644
--- a/viz/metrics-api/stat_summary.go
+++ b/viz/metrics-api/stat_summary.go
@@ -24,20 +24,6 @@ import (
 	"k8s.io/apimachinery/pkg/runtime/schema"
 )
 
-var (
-	serverGroupVersionResource = schema.GroupVersionResource{
-		Group:    "policy.linkerd.io",
-		Version:  "v1alpha1",
-		Resource: "servers",
-	}
-
-	serverAuthorizationGroupVersionResource = schema.GroupVersionResource{
-		Group:    "policy.linkerd.io",
-		Version:  "v1alpha1",
-		Resource: "serverauthorizations",
-	}
-)
-
 type resourceResult struct {
 	res *pb.StatTable
 	err error
@@ -406,9 +392,9 @@ func (s *grpcServer) getPolicyResourceKeys(req *pb.StatSummaryRequest) ([]rKey,
 
 	var gvr schema.GroupVersionResource
 	if req.GetSelector().Resource.GetType() == k8s.Server {
-		gvr = serverGroupVersionResource
+		gvr = k8s.ServerGVR
 	} else if req.GetSelector().Resource.GetType() == k8s.ServerAuthorization {
-		gvr = serverAuthorizationGroupVersionResource
+		gvr = k8s.SazGVR
 	}
 
 	res := req.GetSelector().GetResource()


Test patch:
diff --git a/cli/cmd/metrics_test.go b/cli/cmd/metrics_test.go
deleted file mode 100644
index d28cda687..000000000
--- a/cli/cmd/metrics_test.go
+++ /dev/null
@@ -1,130 +0,0 @@
-package cmd
-
-import (
-	"context"
-	"testing"
-
-	"github.com/linkerd/linkerd2/pkg/k8s"
-)
-
-func TestGetPodsFor(t *testing.T) {
-
-	configs := []string{
-		// pod-1
-		`apiVersion: v1
-kind: Pod
-metadata:
-  name: pod-1
-  namespace: ns
-  uid: pod-1
-  labels:
-    app: foo
-  ownerReferences:
-  - apiVersion: apps/v1
-    controller: true
-    kind: ReplicaSet
-    name: rs-1
-    uid: rs-1
-`,
-		// rs-1
-		`apiVersion: apps/v1
-kind: ReplicaSet
-metadata:
-  name: rs-1
-  namespace: ns
-  uid: rs-1
-  labels:
-    app: foo
-  ownerReferences:
-  - apiVersion: apps/v1
-    controller: true
-    kind: Deployment
-    name: deploy-1
-    uid: deploy-1
-spec:
-  selector:
-    matchLabels:
-      app: foo
-`,
-		// deploy-1
-		`apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: deploy-1
-  namespace: ns
-  uid: deploy-1
-spec:
-  selector:
-    matchLabels:
-      app: foo
-`,
-		// pod-2
-		`apiVersion: v1
-kind: Pod
-metadata:
-  name: pod-2
-  namespace: ns
-  uid: pod-2
-  labels:
-    app: foo
-  ownerReferences:
-  - apiVersion: apps/v1
-    controller: true
-    kind: ReplicaSet
-    name: rs-2
-    uid: rs-2
-`,
-		// rs-2
-		`apiVersion: apps/v1
-kind: ReplicaSet
-metadata:
-  name: rs-2
-  namespace: ns
-  uid: rs-2
-  labels:
-    app: foo
-  ownerReferences:
-  - apiVersion: apps/v1
-    controller: true
-    kind: Deployment
-    name: deploy-2
-    uid: deploy-2
-spec:
-  selector:
-    matchLabels:
-     app: foo
-`,
-		// deploy-2
-		`apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: deploy-2
-  namespace: ns
-  uid: deploy-2
-spec:
-  selector:
-    matchLabels:
-      app: foo
-`}
-
-	k8sClient, err := k8s.NewFakeAPI(configs...)
-	if err != nil {
-		t.Fatalf("Unexpected error %s", err)
-	}
-
-	// Both pod-1 and pod-2 have labels which match deploy-1's selector.
-	// However, only pod-1 is owned by deploy-1 according to the owner references.
-	// Owner references should be considered authoritative to resolve ambiguity
-	// when deployments have overlapping seletors.
-	pods, err := getPodsFor(context.Background(), k8sClient, "ns", "deploy/deploy-1")
-	if err != nil {
-		t.Fatalf("Unexpected error %s", err)
-	}
-
-	if len(pods) != 1 {
-		for _, p := range pods {
-			t.Logf("%s/%s", p.Namespace, p.Name)
-		}
-		t.Fatalf("Expected 1 pod, got %d", len(pods))
-	}
-}
diff --git a/cli/cmd/testdata/install_controlplane_tracing_output.golden b/cli/cmd/testdata/install_controlplane_tracing_output.golden
index b0e9e13b6..a9bb36947 100644
--- a/cli/cmd/testdata/install_controlplane_tracing_output.golden
+++ b/cli/cmd/testdata/install_controlplane_tracing_output.golden
@@ -150,7 +150,7 @@ webhooks:
   failurePolicy: Ignore
   admissionReviewVersions: ["v1", "v1beta1"]
   rules:
-  - operations: [ "CREATE" , "UPDATE" ]
+  - operations: ["CREATE", "UPDATE"]
     apiGroups: ["linkerd.io"]
     apiVersions: ["v1alpha1", "v1alpha2"]
     resources: ["serviceprofiles"]
@@ -195,10 +195,10 @@ webhooks:
   failurePolicy: Ignore
   admissionReviewVersions: ["v1", "v1beta1"]
   rules:
-  - operations: [ "CREATE" , "UPDATE" ]
+  - operations: ["CREATE", "UPDATE"]
     apiGroups: ["policy.linkerd.io"]
     apiVersions: ["v1alpha1"]
-    resources: ["serverauthorizations", "servers"]
+    resources: ["servers"]
   sideEffects: None
 ---
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/install_custom_domain.golden b/cli/cmd/testdata/install_custom_domain.golden
index 15e9dc507..4e4d330f7 100644
--- a/cli/cmd/testdata/install_custom_domain.golden
+++ b/cli/cmd/testdata/install_custom_domain.golden
@@ -150,7 +150,7 @@ webhooks:
   failurePolicy: Ignore
   admissionReviewVersions: ["v1", "v1beta1"]
   rules:
-  - operations: [ "CREATE" , "UPDATE" ]
+  - operations: ["CREATE", "UPDATE"]
     apiGroups: ["linkerd.io"]
     apiVersions: ["v1alpha1", "v1alpha2"]
     resources: ["serviceprofiles"]
@@ -195,10 +195,10 @@ webhooks:
   failurePolicy: Ignore
   admissionReviewVersions: ["v1", "v1beta1"]
   rules:
-  - operations: [ "CREATE" , "UPDATE" ]
+  - operations: ["CREATE", "UPDATE"]
     apiGroups: ["policy.linkerd.io"]
     apiVersions: ["v1alpha1"]
-    resources: ["serverauthorizations", "servers"]
+    resources: ["servers"]
   sideEffects: None
 ---
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/install_custom_registry.golden b/cli/cmd/testdata/install_custom_registry.golden
index f887cfbc2..ec00bec75 100644
--- a/cli/cmd/testdata/install_custom_registry.golden
+++ b/cli/cmd/testdata/install_custom_registry.golden
@@ -150,7 +150,7 @@ webhooks:
   failurePolicy: Ignore
   admissionReviewVersions: ["v1", "v1beta1"]
   rules:
-  - operations: [ "CREATE" , "UPDATE" ]
+  - operations: ["CREATE", "UPDATE"]
     apiGroups: ["linkerd.io"]
     apiVersions: ["v1alpha1", "v1alpha2"]
     resources: ["serviceprofiles"]
@@ -195,10 +195,10 @@ webhooks:
   failurePolicy: Ignore
   admissionReviewVersions: ["v1", "v1beta1"]
   rules:
-  - operations: [ "CREATE" , "UPDATE" ]
+  - operations: ["CREATE", "UPDATE"]
     apiGroups: ["policy.linkerd.io"]
     apiVersions: ["v1alpha1"]
-    resources: ["serverauthorizations", "servers"]
+    resources: ["servers"]
   sideEffects: None
 ---
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/install_default.golden b/cli/cmd/testdata/install_default.golden
index 0bdebdde0..de2769aac 100644
--- a/cli/cmd/testdata/install_default.golden
+++ b/cli/cmd/testdata/install_default.golden
@@ -150,7 +150,7 @@ webhooks:
   failurePolicy: Ignore
   admissionReviewVersions: ["v1", "v1beta1"]
   rules:
-  - operations: [ "CREATE" , "UPDATE" ]
+  - operations: ["CREATE", "UPDATE"]
     apiGroups: ["linkerd.io"]
     apiVersions: ["v1alpha1", "v1alpha2"]
     resources: ["serviceprofiles"]
@@ -195,10 +195,10 @@ webhooks:
   failurePolicy: Ignore
   admissionReviewVersions: ["v1", "v1beta1"]
   rules:
-  - operations: [ "CREATE" , "UPDATE" ]
+  - operations: ["CREATE", "UPDATE"]
     apiGroups: ["policy.linkerd.io"]
     apiVersions: ["v1alpha1"]
-    resources: ["serverauthorizations", "servers"]
+    resources: ["servers"]
   sideEffects: None
 ---
 apiVersion: rbac.authorization.k8s.io/v1
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 b06979d36..7582395d9 100644
--- a/cli/cmd/testdata/install_default_override_dst_get_nets.golden
+++ b/cli/cmd/testdata/install_default_override_dst_get_nets.golden
@@ -150,7 +150,7 @@ webhooks:
   failurePolicy: Ignore
   admissionReviewVersions: ["v1", "v1beta1"]
   rules:
-  - operations: [ "CREATE" , "UPDATE" ]
+  - operations: ["CREATE", "UPDATE"]
     apiGroups: ["linkerd.io"]
     apiVersions: ["v1alpha1", "v1alpha2"]
     resources: ["serviceprofiles"]
@@ -195,10 +195,10 @@ webhooks:
   failurePolicy: Ignore
   admissionReviewVersions: ["v1", "v1beta1"]
   rules:
-  - operations: [ "CREATE" , "UPDATE" ]
+  - operations: ["CREATE", "UPDATE"]
     apiGroups: ["policy.linkerd.io"]
     apiVersions: ["v1alpha1"]
-    resources: ["serverauthorizations", "servers"]
+    resources: ["servers"]
   sideEffects: None
 ---
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/install_ha_output.golden b/cli/cmd/testdata/install_ha_output.golden
index e32c6c3f1..91a7136d5 100644
--- a/cli/cmd/testdata/install_ha_output.golden
+++ b/cli/cmd/testdata/install_ha_output.golden
@@ -150,7 +150,7 @@ webhooks:
   failurePolicy: Fail
   admissionReviewVersions: ["v1", "v1beta1"]
   rules:
-  - operations: [ "CREATE" , "UPDATE" ]
+  - operations: ["CREATE", "UPDATE"]
     apiGroups: ["linkerd.io"]
     apiVersions: ["v1alpha1", "v1alpha2"]
     resources: ["serviceprofiles"]
@@ -195,10 +195,10 @@ webhooks:
   failurePolicy: Fail
   admissionReviewVersions: ["v1", "v1beta1"]
   rules:
-  - operations: [ "CREATE" , "UPDATE" ]
+  - operations: ["CREATE", "UPDATE"]
     apiGroups: ["policy.linkerd.io"]
     apiVersions: ["v1alpha1"]
-    resources: ["serverauthorizations", "servers"]
+    resources: ["servers"]
   sideEffects: None
 ---
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/install_ha_with_overrides_output.golden b/cli/cmd/testdata/install_ha_with_overrides_output.golden
index ed0bb1334..9a529b4bc 100644
--- a/cli/cmd/testdata/install_ha_with_overrides_output.golden
+++ b/cli/cmd/testdata/install_ha_with_overrides_output.golden
@@ -150,7 +150,7 @@ webhooks:
   failurePolicy: Fail
   admissionReviewVersions: ["v1", "v1beta1"]
   rules:
-  - operations: [ "CREATE" , "UPDATE" ]
+  - operations: ["CREATE", "UPDATE"]
     apiGroups: ["linkerd.io"]
     apiVersions: ["v1alpha1", "v1alpha2"]
     resources: ["serviceprofiles"]
@@ -195,10 +195,10 @@ webhooks:
   failurePolicy: Fail
   admissionReviewVersions: ["v1", "v1beta1"]
   rules:
-  - operations: [ "CREATE" , "UPDATE" ]
+  - operations: ["CREATE", "UPDATE"]
     apiGroups: ["policy.linkerd.io"]
     apiVersions: ["v1alpha1"]
-    resources: ["serverauthorizations", "servers"]
+    resources: ["servers"]
   sideEffects: None
 ---
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/install_heartbeat_disabled_output.golden b/cli/cmd/testdata/install_heartbeat_disabled_output.golden
index a35fe149c..9709c3dcf 100644
--- a/cli/cmd/testdata/install_heartbeat_disabled_output.golden
+++ b/cli/cmd/testdata/install_heartbeat_disabled_output.golden
@@ -150,7 +150,7 @@ webhooks:
   failurePolicy: Ignore
   admissionReviewVersions: ["v1", "v1beta1"]
   rules:
-  - operations: [ "CREATE" , "UPDATE" ]
+  - operations: ["CREATE", "UPDATE"]
     apiGroups: ["linkerd.io"]
     apiVersions: ["v1alpha1", "v1alpha2"]
     resources: ["serviceprofiles"]
@@ -195,10 +195,10 @@ webhooks:
   failurePolicy: Ignore
   admissionReviewVersions: ["v1", "v1beta1"]
   rules:
-  - operations: [ "CREATE" , "UPDATE" ]
+  - operations: ["CREATE", "UPDATE"]
     apiGroups: ["policy.linkerd.io"]
     apiVersions: ["v1alpha1"]
-    resources: ["serverauthorizations", "servers"]
+    resources: ["servers"]
   sideEffects: None
 ---
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/install_helm_output.golden b/cli/cmd/testdata/install_helm_output.golden
index 043d6d916..41d9b82f6 100644
--- a/cli/cmd/testdata/install_helm_output.golden
+++ b/cli/cmd/testdata/install_helm_output.golden
@@ -156,7 +156,7 @@ webhooks:
   failurePolicy: Ignore
   admissionReviewVersions: ["v1", "v1beta1"]
   rules:
-  - operations: [ "CREATE" , "UPDATE" ]
+  - operations: ["CREATE", "UPDATE"]
     apiGroups: ["linkerd.io"]
     apiVersions: ["v1alpha1", "v1alpha2"]
     resources: ["serviceprofiles"]
@@ -201,10 +201,10 @@ webhooks:
   failurePolicy: Ignore
   admissionReviewVersions: ["v1", "v1beta1"]
   rules:
-  - operations: [ "CREATE" , "UPDATE" ]
+  - operations: ["CREATE", "UPDATE"]
     apiGroups: ["policy.linkerd.io"]
     apiVersions: ["v1alpha1"]
-    resources: ["serverauthorizations", "servers"]
+    resources: ["servers"]
   sideEffects: None
 ---
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/install_helm_output_ha.golden b/cli/cmd/testdata/install_helm_output_ha.golden
index 710ada695..71384fb9c 100644
--- a/cli/cmd/testdata/install_helm_output_ha.golden
+++ b/cli/cmd/testdata/install_helm_output_ha.golden
@@ -156,7 +156,7 @@ webhooks:
   failurePolicy: Fail
   admissionReviewVersions: ["v1", "v1beta1"]
   rules:
-  - operations: [ "CREATE" , "UPDATE" ]
+  - operations: ["CREATE", "UPDATE"]
     apiGroups: ["linkerd.io"]
     apiVersions: ["v1alpha1", "v1alpha2"]
     resources: ["serviceprofiles"]
@@ -201,10 +201,10 @@ webhooks:
   failurePolicy: Fail
   admissionReviewVersions: ["v1", "v1beta1"]
   rules:
-  - operations: [ "CREATE" , "UPDATE" ]
+  - operations: ["CREATE", "UPDATE"]
     apiGroups: ["policy.linkerd.io"]
     apiVersions: ["v1alpha1"]
-    resources: ["serverauthorizations", "servers"]
+    resources: ["servers"]
   sideEffects: None
 ---
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/install_helm_output_ha_labels.golden b/cli/cmd/testdata/install_helm_output_ha_labels.golden
index 621d92ece..71b169711 100644
--- a/cli/cmd/testdata/install_helm_output_ha_labels.golden
+++ b/cli/cmd/testdata/install_helm_output_ha_labels.golden
@@ -156,7 +156,7 @@ webhooks:
   failurePolicy: Fail
   admissionReviewVersions: ["v1", "v1beta1"]
   rules:
-  - operations: [ "CREATE" , "UPDATE" ]
+  - operations: ["CREATE", "UPDATE"]
     apiGroups: ["linkerd.io"]
     apiVersions: ["v1alpha1", "v1alpha2"]
     resources: ["serviceprofiles"]
@@ -201,10 +201,10 @@ webhooks:
   failurePolicy: Fail
   admissionReviewVersions: ["v1", "v1beta1"]
   rules:
-  - operations: [ "CREATE" , "UPDATE" ]
+  - operations: ["CREATE", "UPDATE"]
     apiGroups: ["policy.linkerd.io"]
     apiVersions: ["v1alpha1"]
-    resources: ["serverauthorizations", "servers"]
+    resources: ["servers"]
   sideEffects: None
 ---
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/install_helm_output_ha_namespace_selector.golden b/cli/cmd/testdata/install_helm_output_ha_namespace_selector.golden
index 7d9648994..b173a8d64 100644
--- a/cli/cmd/testdata/install_helm_output_ha_namespace_selector.golden
+++ b/cli/cmd/testdata/install_helm_output_ha_namespace_selector.golden
@@ -156,7 +156,7 @@ webhooks:
   failurePolicy: Fail
   admissionReviewVersions: ["v1", "v1beta1"]
   rules:
-  - operations: [ "CREATE" , "UPDATE" ]
+  - operations: ["CREATE", "UPDATE"]
     apiGroups: ["linkerd.io"]
     apiVersions: ["v1alpha1", "v1alpha2"]
     resources: ["serviceprofiles"]
@@ -201,10 +201,10 @@ webhooks:
   failurePolicy: Fail
   admissionReviewVersions: ["v1", "v1beta1"]
   rules:
-  - operations: [ "CREATE" , "UPDATE" ]
+  - operations: ["CREATE", "UPDATE"]
     apiGroups: ["policy.linkerd.io"]
     apiVersions: ["v1alpha1"]
-    resources: ["serverauthorizations", "servers"]
+    resources: ["servers"]
   sideEffects: None
 ---
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/install_no_init_container.golden b/cli/cmd/testdata/install_no_init_container.golden
index 9af4d5a31..f0e712376 100644
--- a/cli/cmd/testdata/install_no_init_container.golden
+++ b/cli/cmd/testdata/install_no_init_container.golden
@@ -150,7 +150,7 @@ webhooks:
   failurePolicy: Ignore
   admissionReviewVersions: ["v1", "v1beta1"]
   rules:
-  - operations: [ "CREATE" , "UPDATE" ]
+  - operations: ["CREATE", "UPDATE"]
     apiGroups: ["linkerd.io"]
     apiVersions: ["v1alpha1", "v1alpha2"]
     resources: ["serviceprofiles"]
@@ -195,10 +195,10 @@ webhooks:
   failurePolicy: Ignore
   admissionReviewVersions: ["v1", "v1beta1"]
   rules:
-  - operations: [ "CREATE" , "UPDATE" ]
+  - operations: ["CREATE", "UPDATE"]
     apiGroups: ["policy.linkerd.io"]
     apiVersions: ["v1alpha1"]
-    resources: ["serverauthorizations", "servers"]
+    resources: ["servers"]
   sideEffects: None
 ---
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/install_output.golden b/cli/cmd/testdata/install_output.golden
index b1d212420..52a2bbb78 100644
--- a/cli/cmd/testdata/install_output.golden
+++ b/cli/cmd/testdata/install_output.golden
@@ -150,7 +150,7 @@ webhooks:
   failurePolicy: WebhookFailurePolicy
   admissionReviewVersions: ["v1", "v1beta1"]
   rules:
-  - operations: [ "CREATE" , "UPDATE" ]
+  - operations: ["CREATE", "UPDATE"]
     apiGroups: ["linkerd.io"]
     apiVersions: ["v1alpha1", "v1alpha2"]
     resources: ["serviceprofiles"]
@@ -195,10 +195,10 @@ webhooks:
   failurePolicy: WebhookFailurePolicy
   admissionReviewVersions: ["v1", "v1beta1"]
   rules:
-  - operations: [ "CREATE" , "UPDATE" ]
+  - operations: ["CREATE", "UPDATE"]
     apiGroups: ["policy.linkerd.io"]
     apiVersions: ["v1alpha1"]
-    resources: ["serverauthorizations", "servers"]
+    resources: ["servers"]
   sideEffects: None
 ---
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/install_proxy_ignores.golden b/cli/cmd/testdata/install_proxy_ignores.golden
index f68db4694..492d26883 100644
--- a/cli/cmd/testdata/install_proxy_ignores.golden
+++ b/cli/cmd/testdata/install_proxy_ignores.golden
@@ -150,7 +150,7 @@ webhooks:
   failurePolicy: Ignore
   admissionReviewVersions: ["v1", "v1beta1"]
   rules:
-  - operations: [ "CREATE" , "UPDATE" ]
+  - operations: ["CREATE", "UPDATE"]
     apiGroups: ["linkerd.io"]
     apiVersions: ["v1alpha1", "v1alpha2"]
     resources: ["serviceprofiles"]
@@ -195,10 +195,10 @@ webhooks:
   failurePolicy: Ignore
   admissionReviewVersions: ["v1", "v1beta1"]
   rules:
-  - operations: [ "CREATE" , "UPDATE" ]
+  - operations: ["CREATE", "UPDATE"]
     apiGroups: ["policy.linkerd.io"]
     apiVersions: ["v1alpha1"]
-    resources: ["serverauthorizations", "servers"]
+    resources: ["servers"]
   sideEffects: None
 ---
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/cli/cmd/testdata/install_values_file.golden b/cli/cmd/testdata/install_values_file.golden
index 74b80aa0c..f4ca67d5a 100644
--- a/cli/cmd/testdata/install_values_file.golden
+++ b/cli/cmd/testdata/install_values_file.golden
@@ -136,7 +136,7 @@ webhooks:
   failurePolicy: Ignore
   admissionReviewVersions: ["v1", "v1beta1"]
   rules:
-  - operations: [ "CREATE" , "UPDATE" ]
+  - operations: ["CREATE", "UPDATE"]
     apiGroups: ["linkerd.io"]
     apiVersions: ["v1alpha1", "v1alpha2"]
     resources: ["serviceprofiles"]
@@ -181,10 +181,10 @@ webhooks:
   failurePolicy: Ignore
   admissionReviewVersions: ["v1", "v1beta1"]
   rules:
-  - operations: [ "CREATE" , "UPDATE" ]
+  - operations: ["CREATE", "UPDATE"]
     apiGroups: ["policy.linkerd.io"]
     apiVersions: ["v1alpha1"]
-    resources: ["serverauthorizations", "servers"]
+    resources: ["servers"]
   sideEffects: None
 ---
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/pkg/k8s/api_test.go b/pkg/k8s/api_test.go
index 8f04a88a9..b0d78f24e 100644
--- a/pkg/k8s/api_test.go
+++ b/pkg/k8s/api_test.go
@@ -1,6 +1,7 @@
 package k8s
 
 import (
+	"context"
 	"testing"
 
 	corev1 "k8s.io/api/core/v1"
@@ -303,3 +304,125 @@ status:
 		}
 	}
 }
+
+func TestGetPodsFor(t *testing.T) {
+
+	configs := []string{
+		// pod-1
+		`apiVersion: v1
+kind: Pod
+metadata:
+  name: pod-1
+  namespace: ns
+  uid: pod-1
+  labels:
+    app: foo
+  ownerReferences:
+  - apiVersion: apps/v1
+    controller: true
+    kind: ReplicaSet
+    name: rs-1
+    uid: rs-1
+`,
+		// rs-1
+		`apiVersion: apps/v1
+kind: ReplicaSet
+metadata:
+  name: rs-1
+  namespace: ns
+  uid: rs-1
+  labels:
+    app: foo
+  ownerReferences:
+  - apiVersion: apps/v1
+    controller: true
+    kind: Deployment
+    name: deploy-1
+    uid: deploy-1
+spec:
+  selector:
+    matchLabels:
+      app: foo
+`,
+		// deploy-1
+		`apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: deploy-1
+  namespace: ns
+  uid: deploy-1
+spec:
+  selector:
+    matchLabels:
+      app: foo
+`,
+		// pod-2
+		`apiVersion: v1
+kind: Pod
+metadata:
+  name: pod-2
+  namespace: ns
+  uid: pod-2
+  labels:
+    app: foo
+  ownerReferences:
+  - apiVersion: apps/v1
+    controller: true
+    kind: ReplicaSet
+    name: rs-2
+    uid: rs-2
+`,
+		// rs-2
+		`apiVersion: apps/v1
+kind: ReplicaSet
+metadata:
+  name: rs-2
+  namespace: ns
+  uid: rs-2
+  labels:
+    app: foo
+  ownerReferences:
+  - apiVersion: apps/v1
+    controller: true
+    kind: Deployment
+    name: deploy-2
+    uid: deploy-2
+spec:
+  selector:
+    matchLabels:
+     app: foo
+`,
+		// deploy-2
+		`apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: deploy-2
+  namespace: ns
+  uid: deploy-2
+spec:
+  selector:
+    matchLabels:
+      app: foo
+`}
+
+	k8sClient, err := NewFakeAPI(configs...)
+	if err != nil {
+		t.Fatalf("Unexpected error %s", err)
+	}
+
+	// Both pod-1 and pod-2 have labels which match deploy-1's selector.
+	// However, only pod-1 is owned by deploy-1 according to the owner references.
+	// Owner references should be considered authoritative to resolve ambiguity
+	// when deployments have overlapping seletors.
+	pods, err := GetPodsFor(context.Background(), k8sClient, "ns", "deploy/deploy-1")
+	if err != nil {
+		t.Fatalf("Unexpected error %s", err)
+	}
+
+	if len(pods) != 1 {
+		for _, p := range pods {
+			t.Logf("%s/%s", p.Namespace, p.Name)
+		}
+		t.Fatalf("Expected 1 pod, got %d", len(pods))
+	}
+}