1 package api
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "sort"
8 "strings"
9 "testing"
10
11 "github.com/go-test/deep"
12 "github.com/golang/protobuf/ptypes/duration"
13 "github.com/linkerd/linkerd2/controller/k8s"
14 pkgK8s "github.com/linkerd/linkerd2/pkg/k8s"
15 "github.com/linkerd/linkerd2/pkg/prometheus"
16 pb "github.com/linkerd/linkerd2/viz/metrics-api/gen/viz"
17 "github.com/prometheus/common/model"
18 )
19
20 type listPodsExpected struct {
21 err error
22 k8sRes []string
23 promRes model.Value
24 req *pb.ListPodsRequest
25 res *pb.ListPodsResponse
26 promReqNamespace string
27 }
28
29 type listServicesExpected struct {
30 err error
31 k8sRes []string
32 res *pb.ListServicesResponse
33 }
34
35
36 type ByPod []*pb.Pod
37
38 func (bp ByPod) Len() int { return len(bp) }
39 func (bp ByPod) Swap(i, j int) { bp[i], bp[j] = bp[j], bp[i] }
40 func (bp ByPod) Less(i, j int) bool { return bp[i].Name <= bp[j].Name }
41
42
43 type ByService []*pb.Service
44
45 func (bs ByService) Len() int { return len(bs) }
46 func (bs ByService) Swap(i, j int) { bs[i], bs[j] = bs[j], bs[i] }
47 func (bs ByService) Less(i, j int) bool { return bs[i].Name <= bs[j].Name }
48
49 func listPodResponsesEqual(a *pb.ListPodsResponse, b *pb.ListPodsResponse) bool {
50 if a == nil || b == nil {
51 return a == b
52 }
53
54 if len(a.Pods) != len(b.Pods) {
55 return false
56 }
57
58 sort.Sort(ByPod(a.Pods))
59 sort.Sort(ByPod(b.Pods))
60
61 for i := 0; i < len(a.Pods); i++ {
62 aPod := a.Pods[i]
63 bPod := b.Pods[i]
64
65 if (aPod.Name != bPod.Name) ||
66 (aPod.Added != bPod.Added) ||
67 (aPod.Status != bPod.Status) ||
68 (aPod.PodIP != bPod.PodIP) ||
69 (aPod.GetDeployment() != bPod.GetDeployment()) {
70 return false
71 }
72
73 if (aPod.SinceLastReport == nil && bPod.SinceLastReport != nil) ||
74 (aPod.SinceLastReport != nil && bPod.SinceLastReport == nil) {
75 return false
76 }
77 }
78
79 return true
80 }
81
82 func TestListPods(t *testing.T) {
83 t.Run("Queries to the ListPods endpoint", func(t *testing.T) {
84 expectations := []listPodsExpected{
85 {
86 err: nil,
87 promRes: model.Vector{
88 &model.Sample{
89 Metric: model.Metric{"pod": "emojivoto-meshed"},
90 Timestamp: 456,
91 },
92 },
93 k8sRes: []string{`
94 apiVersion: v1
95 kind: Pod
96 metadata:
97 name: emojivoto-meshed
98 namespace: emojivoto
99 labels:
100 pod-template-hash: hash-meshed
101 ownerReferences:
102 - apiVersion: apps/v1
103 kind: ReplicaSet
104 name: rs-emojivoto-meshed
105 status:
106 phase: Running
107 podIP: 1.2.3.4
108 `, `
109 apiVersion: v1
110 kind: Pod
111 metadata:
112 name: emojivoto-not-meshed
113 namespace: emojivoto
114 labels:
115 pod-template-hash: hash-not-meshed
116 ownerReferences:
117 - apiVersion: apps/v1
118 kind: ReplicaSet
119 name: rs-emojivoto-not-meshed
120 status:
121 phase: Pending
122 podIP: 4.3.2.1
123 `, `
124 apiVersion: apps/v1
125 kind: ReplicaSet
126 metadata:
127 name: rs-emojivoto-meshed
128 namespace: emojivoto
129 ownerReferences:
130 - apiVersion: apps/v1
131 kind: Deployment
132 name: meshed-deployment
133 spec:
134 selector:
135 matchLabels:
136 pod-template-hash: hash-meshed
137 `, `
138 apiVersion: apps/v1
139 kind: ReplicaSet
140 metadata:
141 name: rs-emojivoto-not-meshed
142 namespace: emojivoto
143 ownerReferences:
144 - apiVersion: apps/v1
145 kind: Deployment
146 name: not-meshed-deployment
147 spec:
148 selector:
149 matchLabels:
150 pod-template-hash: hash-not-meshed
151 `,
152 },
153 req: &pb.ListPodsRequest{},
154 res: &pb.ListPodsResponse{
155 Pods: []*pb.Pod{
156 {
157 Name: "emojivoto/emojivoto-meshed",
158 Added: true,
159 SinceLastReport: &duration.Duration{},
160 Status: "Running",
161 PodIP: "1.2.3.4",
162 Owner: &pb.Pod_Deployment{Deployment: "emojivoto/meshed-deployment"},
163 },
164 {
165 Name: "emojivoto/emojivoto-not-meshed",
166 Status: "Pending",
167 PodIP: "4.3.2.1",
168 Owner: &pb.Pod_Deployment{Deployment: "emojivoto/not-meshed-deployment"},
169 },
170 },
171 },
172 },
173 {
174 err: nil,
175 promRes: model.Vector{
176 &model.Sample{
177 Metric: model.Metric{"pod": "emojivoto-meshed"},
178 Timestamp: 456,
179 },
180 },
181 k8sRes: []string{},
182 req: &pb.ListPodsRequest{
183 Selector: &pb.ResourceSelection{
184 Resource: &pb.Resource{
185 Namespace: "testnamespace",
186 },
187 },
188 },
189 res: &pb.ListPodsResponse{},
190 promReqNamespace: "testnamespace",
191 },
192 {
193 err: nil,
194 promRes: model.Vector{
195 &model.Sample{
196 Metric: model.Metric{"pod": "emojivoto-meshed"},
197 Timestamp: 456,
198 },
199 },
200 k8sRes: []string{},
201 req: &pb.ListPodsRequest{
202 Selector: &pb.ResourceSelection{
203 Resource: &pb.Resource{
204 Type: pkgK8s.Namespace,
205 Name: "testnamespace",
206 },
207 },
208 },
209 res: &pb.ListPodsResponse{},
210 promReqNamespace: "testnamespace",
211 },
212
213 {
214 err: nil,
215 promRes: model.Vector{
216 &model.Sample{
217 Metric: model.Metric{"pod": "emojivoto-meshed"},
218 Timestamp: 456,
219 },
220 },
221 k8sRes: []string{`
222 apiVersion: v1
223 kind: Pod
224 metadata:
225 name: emojivoto-meshed
226 namespace: emojivoto
227 labels:
228 pod-template-hash: hash-meshed
229 ownerReferences:
230 - apiVersion: apps/v1
231 kind: Deployment
232 name: meshed-deployment
233 status:
234 phase: Running
235 podIP: 1.2.3.4
236 `,
237 },
238 req: &pb.ListPodsRequest{
239 Selector: &pb.ResourceSelection{
240 Resource: &pb.Resource{
241 Type: pkgK8s.Pod,
242 Name: "non-existing-pod",
243 },
244 },
245 },
246 res: &pb.ListPodsResponse{},
247 },
248
249 {
250 err: nil,
251 promRes: model.Vector{
252 &model.Sample{
253 Metric: model.Metric{"pod": "emojivoto-meshed"},
254 Timestamp: 456,
255 },
256 },
257 k8sRes: []string{`
258 apiVersion: v1
259 kind: Pod
260 metadata:
261 name: emojivoto-meshed
262 namespace: emojivoto
263 labels:
264 pod-template-hash: hash-meshed
265 ownerReferences:
266 - apiVersion: apps/v1
267 kind: Deployment
268 name: meshed-deployment
269 status:
270 phase: Running
271 podIP: 1.2.3.4
272 `,
273 },
274 req: &pb.ListPodsRequest{
275 Selector: &pb.ResourceSelection{
276 Resource: &pb.Resource{
277 Type: pkgK8s.Deployment,
278 Name: "meshed-deployment",
279 },
280 },
281 },
282 res: &pb.ListPodsResponse{
283 Pods: []*pb.Pod{
284 {
285 Name: "emojivoto/emojivoto-meshed",
286 Added: true,
287 SinceLastReport: &duration.Duration{},
288 Status: "Running",
289 PodIP: "1.2.3.4",
290 Owner: &pb.Pod_Deployment{Deployment: "emojivoto/meshed-deployment"},
291 },
292 },
293 },
294 },
295
296 {
297 err: nil,
298 promRes: model.Vector{
299 &model.Sample{
300 Metric: model.Metric{"pod": "emojivoto-meshed"},
301 Timestamp: 456,
302 },
303 },
304 k8sRes: []string{`
305 apiVersion: v1
306 kind: Pod
307 metadata:
308 name: emojivoto-meshed
309 namespace: emojivoto
310 labels:
311 pod-template-hash: hash-meshed
312 ownerReferences:
313 - apiVersion: apps/v1
314 kind: Deployment
315 name: meshed-deployment
316 status:
317 phase: Running
318 podIP: 1.2.3.4
319 `,
320 },
321 req: &pb.ListPodsRequest{
322 Selector: &pb.ResourceSelection{
323 LabelSelector: "pod-template-hash=hash-meshed",
324 },
325 },
326 res: &pb.ListPodsResponse{
327 Pods: []*pb.Pod{
328 {
329 Name: "emojivoto/emojivoto-meshed",
330 Added: true,
331 SinceLastReport: &duration.Duration{},
332 Status: "Running",
333 PodIP: "1.2.3.4",
334 Owner: &pb.Pod_Deployment{Deployment: "emojivoto/meshed-deployment"},
335 },
336 },
337 },
338 },
339
340 {
341 err: nil,
342 promRes: model.Vector{
343 &model.Sample{
344 Metric: model.Metric{"pod": "emojivoto-meshed"},
345 Timestamp: 456,
346 },
347 },
348 k8sRes: []string{`
349 apiVersion: v1
350 kind: Pod
351 metadata:
352 name: emojivoto-meshed
353 namespace: emojivoto
354 labels:
355 pod-template-hash: hash-meshed
356 ownerReferences:
357 - apiVersion: apps/v1
358 kind: Deployment
359 name: meshed-deployment
360 status:
361 phase: Running
362 podIP: 1.2.3.4
363 `,
364 },
365 req: &pb.ListPodsRequest{
366 Selector: &pb.ResourceSelection{
367 LabelSelector: "non-existent-label=value",
368 },
369 },
370 res: &pb.ListPodsResponse{},
371 },
372 }
373
374 for _, exp := range expectations {
375 k8sAPI, err := k8s.NewFakeAPI(exp.k8sRes...)
376 if err != nil {
377 t.Fatalf("NewFakeAPI returned an error: %s", err)
378 }
379
380 mProm := prometheus.MockProm{Res: exp.promRes}
381
382 fakeGrpcServer := grpcServer{
383 prometheusAPI: &mProm,
384 k8sAPI: k8sAPI,
385 controllerNamespace: "linkerd",
386 clusterDomain: "mycluster.local",
387 ignoredNamespaces: []string{},
388 }
389
390 k8sAPI.Sync(nil)
391
392 rsp, err := fakeGrpcServer.ListPods(context.TODO(), exp.req)
393 if diff := deep.Equal(err, exp.err); diff != nil {
394 t.Fatalf("%+v", diff)
395 }
396
397 if !listPodResponsesEqual(exp.res, rsp) {
398 t.Fatalf("Expected: %+v, Got: %+v", exp.res, rsp)
399 }
400
401 if exp.promReqNamespace != "" {
402 err := verifyPromQueries(&mProm, exp.promReqNamespace)
403 if err != nil {
404 t.Fatalf("Expected prometheus query with namespace: %s, Got error: %s", exp.promReqNamespace, err)
405 }
406 }
407 }
408 })
409 }
410
411
412 func verifyPromQueries(mProm *prometheus.MockProm, namespace string) error {
413 namespaceSelector := fmt.Sprintf("namespace=\"%s\"", namespace)
414 for _, element := range mProm.QueriesExecuted {
415 if strings.Contains(element, namespaceSelector) {
416 return nil
417 }
418 }
419 return fmt.Errorf("Prometheus queries incorrect. \nExpected query containing:\n%s \nGot:\n%+v",
420 namespaceSelector, mProm.QueriesExecuted)
421 }
422
423 func listServiceResponsesEqual(a *pb.ListServicesResponse, b *pb.ListServicesResponse) bool {
424 if len(a.Services) != len(b.Services) {
425 return false
426 }
427
428 sort.Sort(ByService(a.Services))
429 sort.Sort(ByService(b.Services))
430
431 for i := 0; i < len(a.Services); i++ {
432 aSvc := a.Services[i]
433 bSvc := b.Services[i]
434
435 if aSvc.Name != bSvc.Name || aSvc.Namespace != bSvc.Namespace {
436 return false
437 }
438 }
439
440 return true
441 }
442
443 func TestListServices(t *testing.T) {
444 t.Run("Successfully queries for services", func(t *testing.T) {
445 expectations := []listServicesExpected{
446 {
447 err: nil,
448 k8sRes: []string{`
449 apiVersion: v1
450 kind: Service
451 metadata:
452 name: service-foo
453 namespace: emojivoto
454 `, `
455 apiVersion: v1
456 kind: Service
457 metadata:
458 name: service-bar
459 namespace: default
460 `,
461 },
462 res: &pb.ListServicesResponse{
463 Services: []*pb.Service{
464 {
465 Name: "service-foo",
466 Namespace: "emojivoto",
467 },
468 {
469 Name: "service-bar",
470 Namespace: "default",
471 },
472 },
473 },
474 },
475 }
476
477 for _, exp := range expectations {
478 k8sAPI, err := k8s.NewFakeAPI(exp.k8sRes...)
479 if err != nil {
480 t.Fatalf("NewFakeAPI returned an error: %s", err)
481 }
482
483 fakeGrpcServer := grpcServer{
484 prometheusAPI: &prometheus.MockProm{},
485 k8sAPI: k8sAPI,
486 controllerNamespace: "linkerd",
487 clusterDomain: "mycluster.local",
488 ignoredNamespaces: []string{},
489 }
490
491 k8sAPI.Sync(nil)
492
493 rsp, err := fakeGrpcServer.ListServices(context.TODO(), &pb.ListServicesRequest{})
494 if !errors.Is(err, exp.err) {
495 t.Fatalf("Expected error: %s, Got: %s", exp.err, err)
496 }
497
498 if !listServiceResponsesEqual(exp.res, rsp) {
499 t.Fatalf("Expected: %+v, Got: %+v", &exp.res, rsp)
500 }
501 }
502 })
503 }
504
View as plain text