From 5f5b5c6e341c30204bb784c4c8dc0e239c15ced9 Mon Sep 17 00:00:00 2001 From: Sudheer Obbu Date: Fri, 1 May 2026 13:20:26 -0400 Subject: [PATCH] cli: add --selector flag to proxy-metrics and multi-resource to routes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #2734 Two additions toward full kubectl-style resource selector support: 1. viz routes — multi-resource support Change Args from ExactArgs(1) to MinimumNArgs(1) so that multiple resources can be specified: linkerd viz routes svc/webapp svc/api -n test The API client is constructed once before the loop; each resource produces one TopRoutesRequest and the outputs are concatenated. 2. diagnostics proxy-metrics — --selector / -l flag Add a labelSelector field to metricsOptions and register the --selector/-l flag. When no positional argument is given but a selector is provided, GetPodsBySelector (new helper in pkg/k8s) is called to list matching pods directly: linkerd diagnostics proxy-metrics -n emojivoto -l app=web GetPodsBySelector wraps a plain CoreV1().Pods().List() call with the label-selector string, consistent with the existing getPods() helper in cli/cmd/identity.go. A positional argument is still accepted and takes precedence when both are given; omitting both is an error. Signed-off-by: Sudheer Obbu --- cli/cmd/metrics.go | 37 ++++++++++++++++++++++------- pkg/k8s/api.go | 12 ++++++++++ viz/cmd/routes.go | 59 +++++++++++++++++++++++++--------------------- 3 files changed, 72 insertions(+), 36 deletions(-) diff --git a/cli/cmd/metrics.go b/cli/cmd/metrics.go index e81fcce76ab65..7ce9e7a0aeca4 100644 --- a/cli/cmd/metrics.go +++ b/cli/cmd/metrics.go @@ -2,24 +2,28 @@ package cmd import ( "bytes" + "errors" "fmt" "time" pkgcmd "github.com/linkerd/linkerd2/pkg/cmd" "github.com/linkerd/linkerd2/pkg/k8s" "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" ) type metricsOptions struct { - namespace string - pod string - obfuscate bool + namespace string + pod string + obfuscate bool + labelSelector string } func newMetricsOptions() *metricsOptions { return &metricsOptions{ - pod: "", - obfuscate: false, + pod: "", + obfuscate: false, + labelSelector: "", } } @@ -27,7 +31,7 @@ func newCmdMetrics() *cobra.Command { options := newMetricsOptions() cmd := &cobra.Command{ - Use: "proxy-metrics [flags] (RESOURCE)", + Use: "proxy-metrics [flags] [(RESOURCE)]", Short: "Fetch metrics directly from Linkerd proxies", Long: `Fetch metrics directly from Linkerd proxies. @@ -35,7 +39,8 @@ func newCmdMetrics() *cobra.Command { queries the /metrics endpoint on the Linkerd proxies. The RESOURCE argument specifies the target resource to query metrics for: - (TYPE/NAME) + (TYPE/NAME). Alternatively, use --selector (-l) to select pods by label + without specifying a resource name. Examples: * cronjob/my-cronjob @@ -61,14 +66,21 @@ func newCmdMetrics() *cobra.Command { # Get metrics from the web deployment in the emojivoto namespace. linkerd diagnostics proxy-metrics -n emojivoto deploy/web + # Get metrics from all pods with a given label in the emojivoto namespace. + linkerd diagnostics proxy-metrics -n emojivoto -l app=web + # Get metrics from the linkerd-destination pod in the linkerd namespace. linkerd diagnostics proxy-metrics -n linkerd $( kubectl --namespace linkerd get pod \ --selector linkerd.io/control-plane-component=destination \ --output name )`, - Args: cobra.ExactArgs(1), + Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 && options.labelSelector == "" { + return errors.New("must specify a resource or --selector") + } + if options.namespace == "" { options.namespace = pkgcmd.GetDefaultNamespace(kubeconfigPath, kubeContext) } @@ -77,7 +89,13 @@ func newCmdMetrics() *cobra.Command { return err } - pods, err := k8s.GetPodsFor(cmd.Context(), k8sAPI, options.namespace, args[0]) + var pods []corev1.Pod + if len(args) == 0 { + // selector-only mode: list pods matching the label selector + pods, err = k8s.GetPodsBySelector(cmd.Context(), k8sAPI, options.namespace, options.labelSelector) + } else { + pods, err = k8s.GetPodsFor(cmd.Context(), k8sAPI, options.namespace, args[0]) + } if err != nil { return err } @@ -111,6 +129,7 @@ func newCmdMetrics() *cobra.Command { cmd.PersistentFlags().StringVarP(&options.namespace, "namespace", "n", options.namespace, "Namespace of resource") cmd.PersistentFlags().BoolVar(&options.obfuscate, "obfuscate", options.obfuscate, "Obfuscate sensitive information") + 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) diff --git a/pkg/k8s/api.go b/pkg/k8s/api.go index 84ef1690d21fb..dbe666af1e01e 100644 --- a/pkg/k8s/api.go +++ b/pkg/k8s/api.go @@ -490,6 +490,18 @@ func GetPodsFor(ctx context.Context, clientset kubernetes.Interface, namespace s return pods, nil } +// GetPodsBySelector queries the Kubernetes API and returns all pods in the +// given namespace matching the provided label selector string. If selector is +// empty, all pods in the namespace are returned. +func GetPodsBySelector(ctx context.Context, clientset kubernetes.Interface, namespace string, selector string) ([]corev1.Pod, error) { + podList, err := clientset.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{ + LabelSelector: selector, + }) + if err != nil { + return nil, err + } + return podList.Items, nil +} func isOwner(u types.UID, ownerRefs []metav1.OwnerReference) bool { for _, or := range ownerRefs { diff --git a/viz/cmd/routes.go b/viz/cmd/routes.go index 75ee0ea799e92..51b0ab3e29079 100644 --- a/viz/cmd/routes.go +++ b/viz/cmd/routes.go @@ -54,7 +54,7 @@ func NewCmdRoutes() *cobra.Command { options := newRoutesOptions() cmd := &cobra.Command{ - Use: "routes [flags] (RESOURCES)", + Use: "routes [flags] (RESOURCE) [RESOURCE...]", Short: "Display route stats", Long: `Display route stats. @@ -62,42 +62,47 @@ This command will only display traffic which is sent to a service that has a Ser Example: ` # Routes for the webapp service in the test namespace. linkerd viz routes service/webapp -n test + # Routes for multiple services in the test namespace. + linkerd viz routes service/webapp service/api -n test + # Routes for calls from the traffic deployment to the webapp service in the test namespace. linkerd viz routes deploy/traffic -n test --to svc/webapp`, - Args: cobra.ExactArgs(1), + Args: cobra.MinimumNArgs(1), ValidArgs: pkgUtil.ValidTargets, RunE: func(cmd *cobra.Command, args []string) error { if options.namespace == "" { options.namespace = pkgcmd.GetDefaultNamespace(kubeconfigPath, kubeContext) } - req, err := buildTopRoutesRequest(args[0], options) - if err != nil { - return fmt.Errorf("error creating metrics request while making routes request: %w", err) - } - - output, err := requestRouteStatsFromAPI( - api.CheckClientOrExit(hc.VizOptions{ - Options: &healthcheck.Options{ - ControlPlaneNamespace: controlPlaneNamespace, - KubeConfig: kubeconfigPath, - Impersonate: impersonate, - ImpersonateGroup: impersonateGroup, - KubeContext: kubeContext, - APIAddr: apiAddr, - }, - VizNamespaceOverride: vizNamespace, - }), - req, - options, - ) - if err != nil { - fmt.Fprint(os.Stderr, err.Error()) - os.Exit(1) + client := api.CheckClientOrExit(hc.VizOptions{ + Options: &healthcheck.Options{ + ControlPlaneNamespace: controlPlaneNamespace, + KubeConfig: kubeconfigPath, + Impersonate: impersonate, + ImpersonateGroup: impersonateGroup, + KubeContext: kubeContext, + APIAddr: apiAddr, + }, + VizNamespaceOverride: vizNamespace, + }) + + var buf bytes.Buffer + for _, arg := range args { + req, err := buildTopRoutesRequest(arg, options) + if err != nil { + return fmt.Errorf("error creating metrics request while making routes request: %w", err) + } + + output, err := requestRouteStatsFromAPI(client, req, options) + if err != nil { + fmt.Fprint(os.Stderr, err.Error()) + os.Exit(1) + } + buf.WriteString(output) } - _, err = fmt.Print(output) + _, printErr := fmt.Print(buf.String()) - return err + return printErr }, }