Repository: linkerd/linkerd2
Pull request #4040 (2020-02-11 10:31:44)

cli: Add label selector flag for `stat`
First take on https://github.com/linkerd/linkerd2/issues/2734

#### Add `kubectl` like label selector for `linkerd stat`
As opposed to earlier, `linkerd stat` command now accepts a label selector via the `--selector` (or `-l` shorthand):
```bash
$ linkerd -n emojivoto stat deploy -l test.linkerd/test
```
The above command will return stats for all deployments with the label `test.linkerd/test` in the `emojivoto` namespace.

---
Follow up PRs shall be made for the following features (as a part of https://github.com/linkerd/linkerd2/issues/2734):
- Update `tap`, `top`, `routes` commands to accept resource lists much like `stat`
- Update `tap`, `top`, `routes` to accept label selectors

mayankshah1607: #### Need help :
From what has been implemented currently, it looks like `buildStatsSummaryRequests()` method returns a slice of `StatSummaryRequest` where each request has `Selector.LabelSelector` being read in correctly from the `-l` or `--selector` flag.
For eg:
```bash
$ linkerd stat deploy -n emojivoto -l test-label/test
```
returns a request object where `label_selector` correctly holds the value `test-label/test`.
However, the label is being ignored while fetching the stats from API - all deployments in the namespace `linkerd` are returned, and not just the ones with the specified label. I'm not really able to figure out whats happening at `requestStatsFromAPI` to diagnose this issue further.
Pothulapati: Hey, 

Hmmm, I don't think the `linkerd-controller` api server is actually using the label selectors.

If we look at the `StatSummary` method in the `linkerd-controller` api server, which calls `k8sResourceQuery` to imply a request for kubernetes resources query. This method at https://github.com/linkerd/linkerd2/blob/9b29a915d31f33914cdb94d71489d98f7e31c9a1/controller/api/public/stat_summary.go#L162 is called to retrieve the k8s resources, but I don't see the `selectors` being used anywhere in this path.

I also see that we are using the `labelSelectors` in the `ListPods` path at https://github.com/linkerd/linkerd2/blob/9b29a915d31f33914cdb94d71489d98f7e31c9a1/controller/api/public/grpc_server.go#L104, Thus making `ListPods` tests with `LabelSelectors` pass at  https://github.com/linkerd/linkerd2/blob/9b29a915d31f33914cdb94d71489d98f7e31c9a1/controller/api/public/grpc_server_test.go#L338

I think, using the same labelSelectors logic in the `ListPods` at the `getKubernetesObjectStats` function in the `StatSummary` path should fix this. 
mayankshah1607: thanks @Pothulapati ! That fixed the problem. I've updated the controller api server to use label selectors from requests. Had to do a small amount of refactoring, but it works as expected now :smile: 
As of now, I've only updated methods relevant to `stat` to make use of label selectors, but it looks like there's a lot of other places where label selectors are being ignored.
adleong: This looks like a good start!
mayankshah1607: @Pothulapati @adleong oops, tried to rebase against master and the integration tests are failing. Any tips on how to fix this?
mayankshah1607: @alpeb Thank you for pointing that out! :smile: I have updated the PR to address these changes. Also added a small helper to make things easier

Patch:
diff --git a/cli/cmd/root.go b/cli/cmd/root.go
index f54c868e4..e2881a899 100644
--- a/cli/cmd/root.go
+++ b/cli/cmd/root.go
@@ -92,7 +92,7 @@ var RootCmd = &cobra.Command{
 }
 
 func init() {
-	RootCmd.PersistentFlags().StringVarP(&controlPlaneNamespace, "linkerd-namespace", "l", defaultNamespace, "Namespace in which Linkerd is installed [$LINKERD_NAMESPACE]")
+	RootCmd.PersistentFlags().StringVarP(&controlPlaneNamespace, "linkerd-namespace", "L", defaultNamespace, "Namespace in which Linkerd is installed [$LINKERD_NAMESPACE]")
 	RootCmd.PersistentFlags().StringVarP(&cniNamespace, "cni-namespace", "", defaultCNINamespace, "Namespace in which the Linkerd CNI plugin is installed")
 	RootCmd.PersistentFlags().StringVar(&kubeconfigPath, "kubeconfig", "", "Path to the kubeconfig file to use for CLI requests")
 	RootCmd.PersistentFlags().StringVar(&kubeContext, "context", "", "Name of the kubeconfig context to use")
diff --git a/cli/cmd/stat.go b/cli/cmd/stat.go
index 15ce29e66..fdefa4729 100644
--- a/cli/cmd/stat.go
+++ b/cli/cmd/stat.go
@@ -25,6 +25,7 @@ type statOptions struct {
 	fromNamespace string
 	fromResource  string
 	allNamespaces bool
+	labelSelector string
 }
 
 type indexedResults struct {
@@ -41,6 +42,7 @@ func newStatOptions() *statOptions {
 		fromNamespace:   "",
 		fromResource:    "",
 		allNamespaces:   false,
+		labelSelector:   "",
 	}
 }
 
@@ -181,7 +183,7 @@ If no resource name is specified, displays stats about all resources of the spec
 	cmd.PersistentFlags().StringVar(&options.fromNamespace, "from-namespace", options.fromNamespace, "Sets the namespace used from lookup the \"--from\" resource; by default the current \"--namespace\" is used")
 	cmd.PersistentFlags().BoolVarP(&options.allNamespaces, "all-namespaces", "A", options.allNamespaces, "If present, returns stats across all namespaces, ignoring the \"--namespace\" flag")
 	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 '!='")
 	return cmd
 }
 
@@ -667,6 +669,7 @@ func buildStatSummaryRequests(resources []string, options *statOptions) ([]*pb.S
 			FromType:      fromRes.Type,
 			FromNamespace: options.fromNamespace,
 			TCPStats:      true,
+			LabelSelector: options.labelSelector,
 		}
 
 		req, err := util.BuildStatSummaryRequest(requestParams)
diff --git a/cli/cmd/top.go b/cli/cmd/top.go
index 1538c57a9..0708ee3c6 100644
--- a/cli/cmd/top.go
+++ b/cli/cmd/top.go
@@ -251,8 +251,9 @@ func newTopTable() *topTable {
 }
 
 const (
-	headerHeight  = 3
+	headerHeight  = 4
 	columnSpacing = 2
+	xOffset       = 5
 )
 
 func newTopOptions() *topOptions {
@@ -407,17 +408,17 @@ func getTrafficByResourceFromAPI(k8sAPI *k8s.KubernetesAPI, req *pb.TapByResourc
 	//       processEvents() && renderTable()
 	closing := make(chan struct{}, 1)
 	done := make(chan struct{})
+	horizontalScroll := make(chan int)
 
-	go pollInput(closing)
+	go pollInput(done, horizontalScroll)
 	go recvEvents(reader, eventCh, closing)
 	go processEvents(eventCh, requestCh, done)
 
 	go func() {
 		<-closing
-		close(done)
 	}()
 
-	renderTable(table, requestCh, done)
+	renderTable(table, requestCh, done, horizontalScroll)
 
 	return nil
 }
@@ -483,20 +484,29 @@ func processEvents(eventCh <-chan pb.TapEvent, requestCh chan<- topRequest, done
 	}
 }
 
-func pollInput(closing chan<- struct{}) {
+func pollInput(done chan<- struct{}, horizontalScroll chan int) {
 	for {
 		switch ev := termbox.PollEvent(); ev.Type {
 		case termbox.EventKey:
 			if ev.Ch == 'q' || ev.Key == termbox.KeyCtrlC {
-				closing <- struct{}{}
+				close(done)
 				return
 			}
+			if ev.Ch == 'a' || ev.Key == termbox.KeyArrowLeft {
+				horizontalScroll <- xOffset
+			}
+			if ev.Ch == 'd' || ev.Key == termbox.KeyArrowRight {
+				horizontalScroll <- -xOffset
+			}
 		}
 	}
 }
 
-func renderTable(table *topTable, requestCh <-chan topRequest, done <-chan struct{}) {
+func renderTable(table *topTable, requestCh <-chan topRequest, done <-chan struct{}, horizontalScroll chan int) {
+	scrollpos := 0
 	ticker := time.NewTicker(100 * time.Millisecond)
+	width, _ := termbox.Size()
+	tablewidth := table.tableWidthCalc()
 
 	for {
 		select {
@@ -506,10 +516,16 @@ func renderTable(table *topTable, requestCh <-chan topRequest, done <-chan struc
 			table.insert(req)
 		case <-ticker.C:
 			termbox.Clear(termbox.ColorDefault, termbox.ColorDefault)
+			width, _ = termbox.Size()
 			table.adjustColumnWidths()
-			table.renderHeaders()
-			table.renderBody()
+			tablewidth = table.tableWidthCalc()
+			table.renderHeaders(scrollpos)
+			table.renderBody(scrollpos)
 			termbox.Flush()
+		case offset := <-horizontalScroll:
+			if (offset > 0 && scrollpos < 0) || (offset < 0 && scrollpos > (width-tablewidth)) {
+				scrollpos = scrollpos + offset
+			}
 		}
 	}
 }
@@ -614,9 +630,10 @@ func stripPort(address string) string {
 	return strings.Split(address, ":")[0]
 }
 
-func (t *topTable) renderHeaders() {
+func (t *topTable) renderHeaders(scrollpos int) {
 	tbprint(0, 0, "(press q to quit)")
-	x := 0
+	tbprint(0, 1, "(press a/LeftArrowKey to scroll left, d/RightArrowKey to scroll right)")
+	x := scrollpos
 	for _, col := range t.columns {
 		if !col.display {
 			continue
@@ -630,6 +647,14 @@ func (t *topTable) renderHeaders() {
 	}
 }
 
+func (t *topTable) tableWidthCalc() int {
+	tablewidth := 0
+	for i := range t.columns {
+		tablewidth = tablewidth + t.columns[i].width + columnSpacing
+	}
+	return tablewidth - columnSpacing
+}
+
 func (t *topTable) adjustColumnWidths() {
 	for i, col := range t.columns {
 		if !col.flexible {
@@ -645,13 +670,13 @@ func (t *topTable) adjustColumnWidths() {
 	}
 }
 
-func (t *topTable) renderBody() {
+func (t *topTable) renderBody(scrollpos int) {
 	sort.SliceStable(t.rows, func(i, j int) bool {
 		return t.rows[i].count > t.rows[j].count
 	})
 
 	for i, row := range t.rows {
-		x := 0
+		x := scrollpos
 
 		for _, col := range t.columns {
 			if !col.display {
diff --git a/controller/api/public/stat_summary.go b/controller/api/public/stat_summary.go
index c0400f049..4bf9f60fb 100644
--- a/controller/api/public/stat_summary.go
+++ b/controller/api/public/stat_summary.go
@@ -159,7 +159,13 @@ func statSummaryError(req *pb.StatSummaryRequest, message string) *pb.StatSummar
 
 func (s *grpcServer) getKubernetesObjectStats(req *pb.StatSummaryRequest) (map[rKey]k8sStat, error) {
 	requestedResource := req.GetSelector().GetResource()
-	objects, err := s.k8sAPI.GetObjects(requestedResource.Namespace, requestedResource.Type, requestedResource.Name)
+
+	labelSelector, err := getLabelSelector(req)
+	if err != nil {
+		return nil, err
+	}
+
+	objects, err := s.k8sAPI.GetObjects(requestedResource.Namespace, requestedResource.Type, requestedResource.Name, labelSelector)
 	if err != nil {
 		return nil, err
 	}
@@ -258,14 +264,20 @@ func (s *grpcServer) k8sResourceQuery(ctx context.Context, req *pb.StatSummaryRe
 	return resourceResult{res: &rsp, err: nil}
 }
 
-func (s *grpcServer) getTrafficSplits(res *pb.Resource) ([]*v1alpha1.TrafficSplit, error) {
+func (s *grpcServer) getTrafficSplits(req *pb.StatSummaryRequest) ([]*v1alpha1.TrafficSplit, error) {
 	var err error
 	var trafficSplits []*v1alpha1.TrafficSplit
 
+	res := req.GetSelector().GetResource()
+	labelSelector, err := getLabelSelector(req)
+	if err != nil {
+		return nil, err
+	}
+
 	if res.GetNamespace() == "" {
-		trafficSplits, err = s.k8sAPI.TS().Lister().List(labels.Everything())
+		trafficSplits, err = s.k8sAPI.TS().Lister().List(labelSelector)
 	} else if res.GetName() == "" {
-		trafficSplits, err = s.k8sAPI.TS().Lister().TrafficSplits(res.GetNamespace()).List(labels.Everything())
+		trafficSplits, err = s.k8sAPI.TS().Lister().TrafficSplits(res.GetNamespace()).List(labelSelector)
 	} else {
 		var ts *v1alpha1.TrafficSplit
 		ts, err = s.k8sAPI.TS().Lister().TrafficSplits(res.GetNamespace()).Get(res.GetName())
@@ -276,7 +288,7 @@ func (s *grpcServer) getTrafficSplits(res *pb.Resource) ([]*v1alpha1.TrafficSpli
 }
 
 func (s *grpcServer) trafficSplitResourceQuery(ctx context.Context, req *pb.StatSummaryRequest) resourceResult {
-	tss, err := s.getTrafficSplits(req.GetSelector().GetResource())
+	tss, err := s.getTrafficSplits(req)
 
 	if err != nil {
 		return resourceResult{res: nil, err: err}
@@ -675,3 +687,15 @@ func checkContainerErrors(containerStatuses []corev1.ContainerStatus) []*pb.PodE
 	}
 	return errors
 }
+
+func getLabelSelector(req *pb.StatSummaryRequest) (labels.Selector, error) {
+	labelSelector := labels.Everything()
+	if s := req.GetSelector().GetLabelSelector(); s != "" {
+		var err error
+		labelSelector, err = labels.Parse(s)
+		if err != nil {
+			return nil, fmt.Errorf("invalid label selector \"%s\": %s", s, err)
+		}
+	}
+	return labelSelector, nil
+}
diff --git a/controller/api/public/top_routes.go b/controller/api/public/top_routes.go
index d8a11a765..dcd5e34f0 100644
--- a/controller/api/public/top_routes.go
+++ b/controller/api/public/top_routes.go
@@ -61,7 +61,7 @@ func (s *grpcServer) TopRoutes(ctx context.Context, req *pb.TopRoutesRequest) (*
 	}
 
 	// Non-authority resource
-	objects, err := s.k8sAPI.GetObjects(targetResource.Namespace, targetResource.Type, targetResource.Name)
+	objects, err := s.k8sAPI.GetObjects(targetResource.Namespace, targetResource.Type, targetResource.Name, labels.Everything())
 	if err != nil {
 		return nil, err
 	}
@@ -135,7 +135,7 @@ func (s *grpcServer) topRoutesFor(ctx context.Context, req *pb.TopRoutesRequest,
 	} else {
 		// Non-authority resource.
 		// Lookup individual resource objects.
-		objects, err := s.k8sAPI.GetObjects(requestedResource.Namespace, requestedResource.Type, requestedResource.Name)
+		objects, err := s.k8sAPI.GetObjects(requestedResource.Namespace, requestedResource.Type, requestedResource.Name, labels.Everything())
 		if err != nil {
 			return nil, err
 		}
diff --git a/controller/api/util/api_utils.go b/controller/api/util/api_utils.go
index 83f5ab920..67321497d 100644
--- a/controller/api/util/api_utils.go
+++ b/controller/api/util/api_utils.go
@@ -79,6 +79,7 @@ type StatsSummaryRequestParams struct {
 	FromName      string
 	SkipStats     bool
 	TCPStats      bool
+	LabelSelector string
 }
 
 // EdgesRequestParams contains parameters that are used to build
@@ -185,6 +186,7 @@ func BuildStatSummaryRequest(p StatsSummaryRequestParams) (*pb.StatSummaryReques
 				Name:      p.ResourceName,
 				Type:      resourceType,
 			},
+			LabelSelector: p.LabelSelector,
 		},
 		TimeWindow: window,
 		SkipStats:  p.SkipStats,
diff --git a/controller/k8s/api.go b/controller/k8s/api.go
index b0a74eff3..da9cdf25c 100644
--- a/controller/k8s/api.go
+++ b/controller/k8s/api.go
@@ -367,31 +367,32 @@ func (api *API) CJ() batchv1beta1informers.CronJobInformer {
 	return api.cj
 }
 
-// GetObjects returns a list of Kubernetes objects, given a namespace, type, and name.
+// GetObjects returns a list of Kubernetes objects, given a namespace, type, name and label selector.
 // If namespace is an empty string, match objects in all namespaces.
 // If name is an empty string, match all objects of the given type.
-func (api *API) GetObjects(namespace, restype, name string) ([]runtime.Object, error) {
+// If label selector is an empty string, match all labels.
+func (api *API) GetObjects(namespace, restype, name string, label labels.Selector) ([]runtime.Object, error) {
 	switch restype {
 	case k8s.Namespace:
-		return api.getNamespaces(name)
+		return api.getNamespaces(name, label)
 	case k8s.CronJob:
-		return api.getCronjobs(namespace, name)
+		return api.getCronjobs(namespace, name, label)
 	case k8s.DaemonSet:
-		return api.getDaemonsets(namespace, name)
+		return api.getDaemonsets(namespace, name, label)
 	case k8s.Deployment:
-		return api.getDeployments(namespace, name)
+		return api.getDeployments(namespace, name, label)
 	case k8s.Job:
-		return api.getJobs(namespace, name)
+		return api.getJobs(namespace, name, label)
 	case k8s.Pod:
-		return api.getPods(namespace, name)
+		return api.getPods(namespace, name, label)
 	case k8s.ReplicationController:
-		return api.getRCs(namespace, name)
+		return api.getRCs(namespace, name, label)
 	case k8s.ReplicaSet:
-		return api.getReplicasets(namespace, name)
+		return api.getReplicasets(namespace, name, label)
 	case k8s.Service:
 		return api.getServices(namespace, name)
 	case k8s.StatefulSet:
-		return api.getStatefulsets(namespace, name)
+		return api.getStatefulsets(namespace, name, label)
 	default:
 		return nil, status.Errorf(codes.Unimplemented, "unimplemented resource type: %s", restype)
 	}
@@ -632,12 +633,12 @@ func GetNamespaceOf(obj runtime.Object) (string, error) {
 
 // getNamespaces returns the namespace matching the specified name. If no name
 // is given, it returns all namespaces.
-func (api *API) getNamespaces(name string) ([]runtime.Object, error) {
+func (api *API) getNamespaces(name string, labelSelector labels.Selector) ([]runtime.Object, error) {
 	var namespaces []*corev1.Namespace
 
 	if name == "" {
 		var err error
-		namespaces, err = api.NS().Lister().List(labels.Everything())
+		namespaces, err = api.NS().Lister().List(labelSelector)
 		if err != nil {
 			return nil, err
 		}
@@ -657,14 +658,14 @@ func (api *API) getNamespaces(name string) ([]runtime.Object, error) {
 	return objects, nil
 }
 
-func (api *API) getDeployments(namespace, name string) ([]runtime.Object, error) {
+func (api *API) getDeployments(namespace, name string, labelSelector labels.Selector) ([]runtime.Object, error) {
 	var err error
 	var deploys []*appsv1.Deployment
 
 	if namespace == "" {
-		deploys, err = api.Deploy().Lister().List(labels.Everything())
+		deploys, err = api.Deploy().Lister().List(labelSelector)
 	} else if name == "" {
-		deploys, err = api.Deploy().Lister().Deployments(namespace).List(labels.Everything())
+		deploys, err = api.Deploy().Lister().Deployments(namespace).List(labelSelector)
 	} else {
 		var deploy *appsv1.Deployment
 		deploy, err = api.Deploy().Lister().Deployments(namespace).Get(name)
@@ -683,14 +684,14 @@ func (api *API) getDeployments(namespace, name string) ([]runtime.Object, error)
 	return objects, nil
 }
 
-func (api *API) getPods(namespace, name string) ([]runtime.Object, error) {
+func (api *API) getPods(namespace, name string, labelSelector labels.Selector) ([]runtime.Object, error) {
 	var err error
 	var pods []*corev1.Pod
 
 	if namespace == "" {
-		pods, err = api.Pod().Lister().List(labels.Everything())
+		pods, err = api.Pod().Lister().List(labelSelector)
 	} else if name == "" {
-		pods, err = api.Pod().Lister().Pods(namespace).List(labels.Everything())
+		pods, err = api.Pod().Lister().Pods(namespace).List(labelSelector)
 	} else {
 		var pod *corev1.Pod
 		pod, err = api.Pod().Lister().Pods(namespace).Get(name)
@@ -712,14 +713,14 @@ func (api *API) getPods(namespace, name string) ([]runtime.Object, error) {
 	return objects, nil
 }
 
-func (api *API) getRCs(namespace, name string) ([]runtime.Object, error) {
+func (api *API) getRCs(namespace, name string, labelSelector labels.Selector) ([]runtime.Object, error) {
 	var err error
 	var rcs []*corev1.ReplicationController
 
 	if namespace == "" {
-		rcs, err = api.RC().Lister().List(labels.Everything())
+		rcs, err = api.RC().Lister().List(labelSelector)
 	} else if name == "" {
-		rcs, err = api.RC().Lister().ReplicationControllers(namespace).List(labels.Everything())
+		rcs, err = api.RC().Lister().ReplicationControllers(namespace).List(labelSelector)
 	} else {
 		var rc *corev1.ReplicationController
 		rc, err = api.RC().Lister().ReplicationControllers(namespace).Get(name)
@@ -738,14 +739,14 @@ func (api *API) getRCs(namespace, name string) ([]runtime.Object, error) {
 	return objects, nil
 }
 
-func (api *API) getDaemonsets(namespace, name string) ([]runtime.Object, error) {
+func (api *API) getDaemonsets(namespace, name string, labelSelector labels.Selector) ([]runtime.Object, error) {
 	var err error
 	var daemonsets []*appsv1.DaemonSet
 
 	if namespace == "" {
-		daemonsets, err = api.DS().Lister().List(labels.Everything())
+		daemonsets, err = api.DS().Lister().List(labelSelector)
 	} else if name == "" {
-		daemonsets, err = api.DS().Lister().DaemonSets(namespace).List(labels.Everything())
+		daemonsets, err = api.DS().Lister().DaemonSets(namespace).List(labelSelector)
 	} else {
 		var ds *appsv1.DaemonSet
 		ds, err = api.DS().Lister().DaemonSets(namespace).Get(name)
@@ -764,14 +765,14 @@ func (api *API) getDaemonsets(namespace, name string) ([]runtime.Object, error)
 	return objects, nil
 }
 
-func (api *API) getStatefulsets(namespace, name string) ([]runtime.Object, error) {
+func (api *API) getStatefulsets(namespace, name string, labelSelector labels.Selector) ([]runtime.Object, error) {
 	var err error
 	var statefulsets []*appsv1.StatefulSet
 
 	if namespace == "" {
-		statefulsets, err = api.SS().Lister().List(labels.Everything())
+		statefulsets, err = api.SS().Lister().List(labelSelector)
 	} else if name == "" {
-		statefulsets, err = api.SS().Lister().StatefulSets(namespace).List(labels.Everything())
+		statefulsets, err = api.SS().Lister().StatefulSets(namespace).List(labelSelector)
 	} else {
 		var ss *appsv1.StatefulSet
 		ss, err = api.SS().Lister().StatefulSets(namespace).Get(name)
@@ -790,14 +791,14 @@ func (api *API) getStatefulsets(namespace, name string) ([]runtime.Object, error
 	return objects, nil
 }
 
-func (api *API) getJobs(namespace, name string) ([]runtime.Object, error) {
+func (api *API) getJobs(namespace, name string, labelSelector labels.Selector) ([]runtime.Object, error) {
 	var err error
 	var jobs []*batchv1.Job
 
 	if namespace == "" {
-		jobs, err = api.Job().Lister().List(labels.Everything())
+		jobs, err = api.Job().Lister().List(labelSelector)
 	} else if name == "" {
-		jobs, err = api.Job().Lister().Jobs(namespace).List(labels.Everything())
+		jobs, err = api.Job().Lister().Jobs(namespace).List(labelSelector)
 	} else {
 		var job *batchv1.Job
 		job, err = api.Job().Lister().Jobs(namespace).Get(name)
@@ -831,14 +832,14 @@ func (api *API) getServices(namespace, name string) ([]runtime.Object, error) {
 	return objects, nil
 }
 
-func (api *API) getCronjobs(namespace, name string) ([]runtime.Object, error) {
+func (api *API) getCronjobs(namespace, name string, labelSelector labels.Selector) ([]runtime.Object, error) {
 	var err error
 	var cronjobs []*batchv1beta1.CronJob
 
 	if namespace == "" {
-		cronjobs, err = api.CJ().Lister().List(labels.Everything())
+		cronjobs, err = api.CJ().Lister().List(labelSelector)
 	} else if name == "" {
-		cronjobs, err = api.CJ().Lister().CronJobs(namespace).List(labels.Everything())
+		cronjobs, err = api.CJ().Lister().CronJobs(namespace).List(labelSelector)
 	} else {
 		var cronjob *batchv1beta1.CronJob
 		cronjob, err = api.CJ().Lister().CronJobs(namespace).Get(name)
@@ -856,14 +857,14 @@ func (api *API) getCronjobs(namespace, name string) ([]runtime.Object, error) {
 	return objects, nil
 }
 
-func (api *API) getReplicasets(namespace, name string) ([]runtime.Object, error) {
+func (api *API) getReplicasets(namespace, name string, labelSelector labels.Selector) ([]runtime.Object, error) {
 	var err error
 	var replicasets []*appsv1.ReplicaSet
 
 	if namespace == "" {
-		replicasets, err = api.RS().Lister().List(labels.Everything())
+		replicasets, err = api.RS().Lister().List(labelSelector)
 	} else if name == "" {
-		replicasets, err = api.RS().Lister().ReplicaSets(namespace).List(labels.Everything())
+		replicasets, err = api.RS().Lister().ReplicaSets(namespace).List(labelSelector)
 	} else {
 		var replicaset *appsv1.ReplicaSet
 		replicaset, err = api.RS().Lister().ReplicaSets(namespace).Get(name)
diff --git a/controller/proxy-injector/webhook.go b/controller/proxy-injector/webhook.go
index b769a1e25..dc274862d 100644
--- a/controller/proxy-injector/webhook.go
+++ b/controller/proxy-injector/webhook.go
@@ -4,6 +4,8 @@ import (
 	"fmt"
 	"strings"
 
+	"k8s.io/apimachinery/pkg/labels"
+
 	pb "github.com/linkerd/linkerd2/controller/gen/config"
 	"github.com/linkerd/linkerd2/controller/k8s"
 	"github.com/linkerd/linkerd2/pkg/config"
@@ -67,7 +69,7 @@ func Inject(api *k8s.API,
 	var parent *runtime.Object
 	ownerKind := ""
 	if ownerRef := resourceConfig.GetOwnerRef(); ownerRef != nil {
-		objs, err := api.GetObjects(request.Namespace, ownerRef.Kind, ownerRef.Name)
+		objs, err := api.GetObjects(request.Namespace, ownerRef.Kind, ownerRef.Name, labels.Everything())
 		if err != nil {
 			log.Warnf("couldn't retrieve parent object %s-%s-%s; error: %s", request.Namespace, ownerRef.Kind, ownerRef.Name, err)
 		} else if len(objs) == 0 {
diff --git a/controller/tap/server.go b/controller/tap/server.go
index b1a26ffe7..dab120eaa 100644
--- a/controller/tap/server.go
+++ b/controller/tap/server.go
@@ -7,6 +7,8 @@ import (
 	"time"
 	"unicode/utf8"
 
+	"k8s.io/apimachinery/pkg/labels"
+
 	httpPb "github.com/linkerd/linkerd2-proxy-api/go/http_types"
 	proxy "github.com/linkerd/linkerd2-proxy-api/go/tap"
 	apiUtil "github.com/linkerd/linkerd2/controller/api/util"
@@ -64,7 +66,7 @@ func (s *GRPCTapServer) TapByResource(req *public.TapByResourceRequest, stream p
 		req.MaxRps = defaultMaxRps
 	}
 
-	objects, err := s.k8sAPI.GetObjects(res.GetNamespace(), res.GetType(), res.GetName())
+	objects, err := s.k8sAPI.GetObjects(res.GetNamespace(), res.GetType(), res.GetName(), labels.Everything())
 	if err != nil {
 		return apiUtil.GRPCError(err)
 	}


Test patch:
diff --git a/controller/k8s/api_test.go b/controller/k8s/api_test.go
index f81174e16..8a316969f 100644
--- a/controller/k8s/api_test.go
+++ b/controller/k8s/api_test.go
@@ -7,6 +7,8 @@ import (
 	"sort"
 	"testing"
 
+	"k8s.io/apimachinery/pkg/labels"
+
 	"github.com/linkerd/linkerd2/pkg/k8s"
 	"google.golang.org/grpc/codes"
 	"google.golang.org/grpc/status"
@@ -265,7 +267,7 @@ metadata:
 				t.Fatalf("newAPI error: %s", err)
 			}
 
-			pods, err := api.GetObjects(exp.namespace, exp.resType, exp.name)
+			pods, err := api.GetObjects(exp.namespace, exp.resType, exp.name, labels.Everything())
 			if err != nil || exp.err != nil {
 				if (err == nil && exp.err != nil) ||
 					(err != nil && exp.err == nil) ||
@@ -327,7 +329,7 @@ status:
 					t.Fatalf("newAPI error: %s", err)
 				}
 
-				pods, err := api.GetObjects(exp.namespace, exp.resType, exp.name)
+				pods, err := api.GetObjects(exp.namespace, exp.resType, exp.name, labels.Everything())
 				if err != nil {
 					t.Fatalf("api.GetObjects() unexpected error %s", err)
 				}
@@ -384,7 +386,7 @@ status:
 					t.Fatalf("newAPI error: %s", err)
 				}
 
-				pods, err := api.GetObjects(exp.namespace, exp.resType, exp.name)
+				pods, err := api.GetObjects(exp.namespace, exp.resType, exp.name, labels.Everything())
 				if err != nil {
 					t.Fatalf("api.GetObjects() unexpected error %s", err)
 				}