...

Source file src/github.com/linkerd/linkerd2/viz/cmd/authz.go

Documentation: github.com/linkerd/linkerd2/viz/cmd

     1  package cmd
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"strings"
    10  
    11  	"github.com/linkerd/linkerd2/cli/table"
    12  	pkgcmd "github.com/linkerd/linkerd2/pkg/cmd"
    13  	"github.com/linkerd/linkerd2/pkg/healthcheck"
    14  	"github.com/linkerd/linkerd2/pkg/k8s"
    15  	pb "github.com/linkerd/linkerd2/viz/metrics-api/gen/viz"
    16  	"github.com/linkerd/linkerd2/viz/metrics-api/util"
    17  	"github.com/linkerd/linkerd2/viz/pkg/api"
    18  	hc "github.com/linkerd/linkerd2/viz/pkg/healthcheck"
    19  	pkgUtil "github.com/linkerd/linkerd2/viz/pkg/util"
    20  	"github.com/spf13/cobra"
    21  	corev1 "k8s.io/api/core/v1"
    22  )
    23  
    24  // NewCmdAuthz creates a new cobra command `authz`
    25  func NewCmdAuthz() *cobra.Command {
    26  	options := newStatOptions()
    27  
    28  	cmd := &cobra.Command{
    29  		Use:   "authz [flags] resource",
    30  		Short: "Display stats for authorizations for a resource",
    31  		Long:  "Display stats for authorizations for a resource.",
    32  		Args:  cobra.MinimumNArgs(1),
    33  		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
    34  
    35  			k8sAPI, err := k8s.NewAPI(kubeconfigPath, kubeContext, impersonate, impersonateGroup, 0)
    36  			if err != nil {
    37  				return nil, cobra.ShellCompDirectiveError
    38  			}
    39  
    40  			if options.namespace == "" {
    41  				options.namespace = pkgcmd.GetDefaultNamespace(kubeconfigPath, kubeContext)
    42  			}
    43  
    44  			cc := k8s.NewCommandCompletion(k8sAPI, options.namespace)
    45  
    46  			results, err := cc.Complete(args, toComplete)
    47  			if err != nil {
    48  				return nil, cobra.ShellCompDirectiveError
    49  			}
    50  
    51  			return results, cobra.ShellCompDirectiveDefault
    52  		},
    53  		RunE: func(cmd *cobra.Command, args []string) error {
    54  			if options.namespace == "" {
    55  				options.namespace = pkgcmd.GetDefaultNamespace(kubeconfigPath, kubeContext)
    56  			}
    57  
    58  			// The gRPC client is concurrency-safe, so we can reuse it in all the following goroutines
    59  			// https://github.com/grpc/grpc-go/issues/682
    60  			client := api.CheckClientOrExit(hc.VizOptions{
    61  				Options: &healthcheck.Options{
    62  					ControlPlaneNamespace: controlPlaneNamespace,
    63  					KubeConfig:            kubeconfigPath,
    64  					Impersonate:           impersonate,
    65  					ImpersonateGroup:      impersonateGroup,
    66  					KubeContext:           kubeContext,
    67  					APIAddr:               apiAddr,
    68  				},
    69  				VizNamespaceOverride: vizNamespace,
    70  			})
    71  
    72  			var resource string
    73  			if len(args) == 1 {
    74  				resource = args[0]
    75  			} else if len(args) == 2 {
    76  				resource = args[0] + "/" + args[1]
    77  			}
    78  
    79  			cols := []table.Column{
    80  				table.NewColumn("ROUTE").WithLeftAlign(),
    81  				table.NewColumn("SERVER").WithLeftAlign(),
    82  				table.NewColumn("AUTHORIZATION").WithLeftAlign(),
    83  				table.NewColumn("UNAUTHORIZED"),
    84  				table.NewColumn("SUCCESS"),
    85  				table.NewColumn("RPS"),
    86  				table.NewColumn("LATENCY_P50"),
    87  				table.NewColumn("LATENCY_P95"),
    88  				table.NewColumn("LATENCY_P99"),
    89  			}
    90  			rows := []table.Row{}
    91  
    92  			req := pb.AuthzRequest{}
    93  			window, err := util.ValidateTimeWindow(options.timeWindow)
    94  			if err != nil {
    95  				return err
    96  			}
    97  			req.TimeWindow = window
    98  
    99  			target, err := pkgUtil.BuildResource(options.namespace, resource)
   100  			if err != nil {
   101  				return err
   102  			}
   103  
   104  			if options.allNamespaces && target.Name != "" {
   105  				return errors.New("stats for a resource cannot be retrieved by name across all namespaces")
   106  			}
   107  
   108  			if options.allNamespaces {
   109  				target.Namespace = ""
   110  			} else if target.Namespace == "" {
   111  				target.Namespace = corev1.NamespaceDefault
   112  			}
   113  
   114  			req.Resource = target
   115  
   116  			resp, err := client.Authz(cmd.Context(), &req)
   117  			if err != nil {
   118  				fmt.Fprintf(os.Stderr, "Authz API error: %s", err)
   119  				os.Exit(1)
   120  			}
   121  			if e := resp.GetError(); e != nil {
   122  				fmt.Fprintf(os.Stderr, "Authz API error: %s", e.Error)
   123  				os.Exit(1)
   124  			}
   125  
   126  			for _, row := range resp.GetOk().GetStatTable().GetPodGroup().GetRows() {
   127  				server := row.GetSrvStats().GetSrv().GetName()
   128  				if row.GetSrvStats().GetSrv().GetType() == "default" {
   129  					server = fmt.Sprintf("%s:%s", row.GetSrvStats().GetSrv().GetType(), row.GetSrvStats().GetSrv().GetName())
   130  				}
   131  				authz := fmt.Sprintf("%s/%s", row.GetSrvStats().GetAuthz().GetType(), row.GetSrvStats().GetAuthz().GetName())
   132  				if row.GetSrvStats().GetAuthz().GetType() == "" {
   133  					authz = ""
   134  				}
   135  				if row.GetStats().GetSuccessCount()+row.GetStats().GetFailureCount()+row.GetSrvStats().GetDeniedCount() > 0 {
   136  					rows = append(rows, table.Row{
   137  						row.GetSrvStats().GetRoute().GetName(),
   138  						server,
   139  						authz,
   140  						fmt.Sprintf("%.1frps", getRequestRate(row.GetSrvStats().GetDeniedCount(), 0, window)),
   141  						fmt.Sprintf("%.2f%%", getSuccessRate(row.Stats.GetSuccessCount(), row.Stats.GetFailureCount())*100),
   142  						fmt.Sprintf("%.1frps", getRequestRate(row.Stats.GetSuccessCount(), row.Stats.GetFailureCount(), window)),
   143  						fmt.Sprintf("%dms", row.Stats.LatencyMsP50),
   144  						fmt.Sprintf("%dms", row.Stats.LatencyMsP95),
   145  						fmt.Sprintf("%dms", row.Stats.LatencyMsP99),
   146  					})
   147  				} else {
   148  					if row.GetSrvStats().GetAuthz().GetType() == "" || row.GetSrvStats().GetAuthz().GetType() == "default" {
   149  						// Skip showing the default or unauthorized entries if there are no requests for them.
   150  						continue
   151  					}
   152  					rows = append(rows, table.Row{
   153  						row.GetSrvStats().GetRoute().GetName(),
   154  						server,
   155  						authz,
   156  						"-",
   157  						"-",
   158  						"-",
   159  						"-",
   160  						"-",
   161  						"-",
   162  					})
   163  				}
   164  			}
   165  
   166  			data := table.NewTable(cols, rows)
   167  			data.Sort = []int{1, 0, 2} // Sort by Server, then Route, then Authorization
   168  			if options.outputFormat == "json" {
   169  				err = renderJSON(data, os.Stdout)
   170  				if err != nil {
   171  					fmt.Fprint(os.Stderr, err.Error())
   172  					os.Exit(1)
   173  				}
   174  			} else {
   175  				data.Render(os.Stdout)
   176  			}
   177  
   178  			return nil
   179  		},
   180  	}
   181  
   182  	cmd.PersistentFlags().StringVarP(&options.namespace, "namespace", "n", options.namespace, "Namespace of the specified resource")
   183  	cmd.PersistentFlags().StringVarP(&options.timeWindow, "time-window", "t", options.timeWindow, "Stat window (for example: \"15s\", \"1m\", \"10m\", \"1h\"). Needs to be at least 15s.")
   184  	cmd.PersistentFlags().StringVarP(&options.outputFormat, "output", "o", options.outputFormat, "Output format; one of: \"table\" or \"json\" or \"wide\"")
   185  	cmd.PersistentFlags().StringVarP(&options.labelSelector, "selector", "l", options.labelSelector, "Selector (label query) to filter on, supports '=', '==', and '!='")
   186  
   187  	pkgcmd.ConfigureNamespaceFlagCompletion(
   188  		cmd, []string{"namespace"},
   189  		kubeconfigPath, impersonate, impersonateGroup, kubeContext)
   190  	return cmd
   191  }
   192  
   193  func renderJSON(t table.Table, w io.Writer) error {
   194  	rows := make([]map[string]interface{}, len(t.Data))
   195  	for i, data := range t.Data {
   196  		rows[i] = make(map[string]interface{})
   197  		for j, col := range t.Columns {
   198  			if data[j] == "-" {
   199  				continue
   200  			}
   201  			field := strings.ToLower(col.Header)
   202  			var percentile string
   203  
   204  			if n, _ := fmt.Sscanf(field, "latency_%s", &percentile); n == 1 {
   205  				var latency int
   206  				n, _ := fmt.Sscanf(data[j], "%dms", &latency)
   207  				if n == 1 {
   208  					rows[i]["latency_ms_"+percentile] = latency
   209  				} else {
   210  					rows[i]["latency_ms_"+percentile] = data[j]
   211  				}
   212  			} else if field == "rps" || field == "unauthorized" {
   213  				var rps float32
   214  				if n, _ := fmt.Sscanf(data[j], "%frps", &rps); n == 1 {
   215  					rows[i][field] = rps
   216  				} else {
   217  					rows[i][field] = data[j]
   218  				}
   219  			} else if field == "success" {
   220  				var success float32
   221  				if n, _ := fmt.Sscanf(data[j], "%f%%", &success); n == 1 {
   222  					rows[i][field] = success / 100.0
   223  				} else {
   224  					rows[i][field] = data[j]
   225  				}
   226  			} else {
   227  				rows[i][field] = data[j]
   228  			}
   229  		}
   230  	}
   231  	out, err := json.MarshalIndent(rows, "", "  ")
   232  	if err != nil {
   233  		return err
   234  	}
   235  	_, err = w.Write(out)
   236  	return err
   237  }
   238  

View as plain text