1 package cmd
2
3 import (
4 "testing"
5
6 pkgcmd "github.com/linkerd/linkerd2/pkg/cmd"
7 "github.com/linkerd/linkerd2/pkg/k8s"
8 api "github.com/linkerd/linkerd2/viz/metrics-api"
9 )
10
11 type paramsExp struct {
12 counts *api.PodCounts
13 options *statOptions
14 resNs []string
15 file string
16 }
17
18 func TestStat(t *testing.T) {
19 options := newStatOptions()
20 t.Run("Returns namespace stats", func(t *testing.T) {
21 testStatCall(paramsExp{
22 counts: &api.PodCounts{
23 MeshedPods: 1,
24 RunningPods: 2,
25 FailedPods: 0,
26 },
27 options: options,
28 resNs: []string{"emojivoto1"},
29 file: "stat_one_output.golden",
30 }, k8s.Namespace, t)
31 })
32
33 t.Run("Returns pod stats", func(t *testing.T) {
34 testStatCall(paramsExp{
35 counts: &api.PodCounts{
36 Status: "Running",
37 MeshedPods: 1,
38 RunningPods: 1,
39 FailedPods: 0,
40 },
41 options: options,
42 resNs: []string{"emojivoto1"},
43 file: "stat_one_pod_output.golden",
44 }, k8s.Pod, t)
45 })
46
47 options.outputFormat = jsonOutput
48 t.Run("Returns namespace stats (json)", func(t *testing.T) {
49 testStatCall(paramsExp{
50 counts: &api.PodCounts{
51 MeshedPods: 1,
52 RunningPods: 2,
53 FailedPods: 0,
54 },
55 options: options,
56 resNs: []string{"emojivoto1"},
57 file: "stat_one_output_json.golden",
58 }, k8s.Namespace, t)
59 })
60
61 options = newStatOptions()
62 options.allNamespaces = true
63 t.Run("Returns all namespace stats", func(t *testing.T) {
64 testStatCall(paramsExp{
65 counts: &api.PodCounts{
66 MeshedPods: 1,
67 RunningPods: 2,
68 FailedPods: 0,
69 },
70 options: options,
71 resNs: []string{"emojivoto1", "emojivoto2"},
72 file: "stat_all_output.golden",
73 }, k8s.Namespace, t)
74 })
75
76 options.outputFormat = jsonOutput
77 t.Run("Returns all namespace stats (json)", func(t *testing.T) {
78 testStatCall(paramsExp{
79 counts: &api.PodCounts{
80 MeshedPods: 1,
81 RunningPods: 2,
82 FailedPods: 0,
83 },
84 options: options,
85 resNs: []string{"emojivoto1", "emojivoto2"},
86 file: "stat_all_output_json.golden",
87 }, k8s.Namespace, t)
88 })
89
90 options = newStatOptions()
91 options.outputFormat = "wide"
92 t.Run("Returns TCP stats", func(t *testing.T) {
93 testStatCall(paramsExp{
94 counts: &api.PodCounts{
95 MeshedPods: 1,
96 RunningPods: 2,
97 FailedPods: 0,
98 },
99 options: options,
100 resNs: []string{"emojivoto1"},
101 file: "stat_one_tcp_output.golden",
102 }, k8s.Namespace, t)
103 })
104
105 t.Run("Returns an error for named resource queries with the --all-namespaces flag", func(t *testing.T) {
106 options := newStatOptions()
107 options.allNamespaces = true
108 if options.namespace == "" {
109 options.namespace = pkgcmd.GetDefaultNamespace(kubeconfigPath, kubeContext)
110 }
111 args := []string{"po", "web"}
112 expectedError := "stats for a resource cannot be retrieved by name across all namespaces"
113
114 _, err := buildStatSummaryRequests(args, options)
115 if err == nil || err.Error() != expectedError {
116 t.Fatalf("Expected error [%s] instead got [%s]", expectedError, err)
117 }
118 })
119
120 t.Run("Rejects commands with both --to and --from flags", func(t *testing.T) {
121 options := newStatOptions()
122 if options.namespace == "" {
123 options.namespace = pkgcmd.GetDefaultNamespace(kubeconfigPath, kubeContext)
124 }
125 options.toResource = "deploy/foo"
126 options.fromResource = "deploy/bar"
127 args := []string{"ns", "test"}
128 expectedError := "--to and --from flags are mutually exclusive"
129
130 _, err := buildStatSummaryRequests(args, options)
131 if err == nil || err.Error() != expectedError {
132 t.Fatalf("Expected error [%s] instead got [%s]", expectedError, err)
133 }
134 })
135
136 t.Run("Rejects commands with both --to-namespace and --from-namespace flags", func(t *testing.T) {
137 options := newStatOptions()
138 if options.namespace == "" {
139 options.namespace = pkgcmd.GetDefaultNamespace(kubeconfigPath, kubeContext)
140 }
141 options.toNamespace = "foo"
142 options.fromNamespace = "bar"
143 args := []string{"po"}
144 expectedError := "--to-namespace and --from-namespace flags are mutually exclusive"
145
146 _, err := buildStatSummaryRequests(args, options)
147 if err == nil || err.Error() != expectedError {
148 t.Fatalf("Expected error [%s] instead got [%s]", expectedError, err)
149 }
150 })
151
152 t.Run("Rejects commands with both --all-namespaces and --namespace flags", func(t *testing.T) {
153 options := newStatOptions()
154 if options.namespace == "" {
155 options.namespace = pkgcmd.GetDefaultNamespace(kubeconfigPath, kubeContext)
156 }
157 options.allNamespaces = true
158 options.namespace = "ns"
159 args := []string{"po"}
160 expectedError := "--all-namespaces and --namespace flags are mutually exclusive"
161
162 _, err := buildStatSummaryRequests(args, options)
163 if err == nil || err.Error() != expectedError {
164 t.Fatalf("Expected error [%s] instead got [%s]", expectedError, err)
165 }
166 })
167
168 t.Run("Rejects --to-namespace flag when the target is a namespace", func(t *testing.T) {
169 options := newStatOptions()
170 if options.namespace == "" {
171 options.namespace = pkgcmd.GetDefaultNamespace(kubeconfigPath, kubeContext)
172 }
173 options.toNamespace = "bar"
174 args := []string{"ns", "foo"}
175 expectedError := "--to-namespace flag is incompatible with namespace resource type"
176
177 _, err := buildStatSummaryRequests(args, options)
178 if err == nil || err.Error() != expectedError {
179 t.Fatalf("Expected error [%s] instead got [%s]", expectedError, err)
180 }
181 })
182
183 t.Run("Rejects --from-namespace flag when the target is a namespace", func(t *testing.T) {
184 options := newStatOptions()
185 if options.namespace == "" {
186 options.namespace = pkgcmd.GetDefaultNamespace(kubeconfigPath, kubeContext)
187 }
188 options.fromNamespace = "foo"
189 args := []string{"ns/bar"}
190 expectedError := "--from-namespace flag is incompatible with namespace resource type"
191
192 _, err := buildStatSummaryRequests(args, options)
193 if err == nil || err.Error() != expectedError {
194 t.Fatalf("Expected error [%s] instead got [%s]", expectedError, err)
195 }
196 })
197
198 t.Run("Returns an error if --time-window is not more than 15s", func(t *testing.T) {
199 options := newStatOptions()
200 if options.namespace == "" {
201 options.namespace = pkgcmd.GetDefaultNamespace(kubeconfigPath, kubeContext)
202 }
203 options.timeWindow = "10s"
204 args := []string{"ns/bar"}
205 expectedError := "metrics time window needs to be at least 15s"
206
207 _, err := buildStatSummaryRequests(args, options)
208 if err == nil || err.Error() != expectedError {
209 t.Fatalf("Expected error [%s] instead got [%s]", expectedError, err)
210 }
211 })
212 }
213
214 func testStatCall(exp paramsExp, resourceType string, t *testing.T) {
215 mockClient := &api.MockAPIClient{}
216 response := api.GenStatSummaryResponse("emoji", resourceType, exp.resNs, exp.counts, true, true)
217
218 mockClient.StatSummaryResponseToReturn = response
219
220 if exp.options.namespace == "" {
221 exp.options.namespace = pkgcmd.GetDefaultNamespace(kubeconfigPath, kubeContext)
222 }
223 reqs, err := buildStatSummaryRequests([]string{"ns"}, exp.options)
224 if err != nil {
225 t.Fatalf("Unexpected error: %v", err)
226 }
227
228 resp, err := requestStatsFromAPI(mockClient, reqs[0])
229 if err != nil {
230 t.Fatalf("Unexpected error: %v", err)
231 }
232
233 rows := respToRows(resp)
234 output := renderStatStats(rows, exp.options)
235
236 testDataDiffer.DiffTestdata(t, exp.file, output)
237 }
238
View as plain text