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
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
59
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
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}
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