1 package api
2
3 import (
4 "context"
5 "fmt"
6 "net"
7 "strconv"
8 "testing"
9
10 "github.com/go-test/deep"
11 proxy "github.com/linkerd/linkerd2-proxy-api/go/tap"
12 "github.com/linkerd/linkerd2/controller/api/util"
13 "github.com/linkerd/linkerd2/controller/k8s"
14 "github.com/linkerd/linkerd2/pkg/addr"
15 pkgK8s "github.com/linkerd/linkerd2/pkg/k8s"
16 metricsPb "github.com/linkerd/linkerd2/viz/metrics-api/gen/viz"
17 tapPb "github.com/linkerd/linkerd2/viz/tap/gen/tap"
18 "google.golang.org/grpc"
19 "google.golang.org/grpc/codes"
20 "google.golang.org/grpc/metadata"
21 "google.golang.org/grpc/status"
22 )
23
24 type tapExpected struct {
25 err error
26 k8sRes []string
27 req *tapPb.TapByResourceRequest
28 requireID string
29 }
30
31
32 type mockTapByResourceServer struct {
33 util.MockServerStream
34 }
35
36 func (m *mockTapByResourceServer) Send(event *tapPb.TapEvent) error {
37 return nil
38 }
39
40
41 type mockProxyTapServer struct {
42 proxy.UnimplementedTapServer
43 mockControllerServer mockTapByResourceServer
44 ctx context.Context
45 }
46
47 func (m *mockProxyTapServer) Observe(req *proxy.ObserveRequest, obsSrv proxy.Tap_ObserveServer) error {
48 m.ctx = obsSrv.Context()
49 m.mockControllerServer.Cancel()
50 return nil
51 }
52
53 func TestTapByResource(t *testing.T) {
54 expectations := []tapExpected{
55 {
56 err: status.Error(codes.InvalidArgument, "TapByResource received nil target ResourceSelection"),
57 k8sRes: []string{},
58 req: &tapPb.TapByResourceRequest{},
59 },
60 {
61 err: status.Errorf(codes.Unimplemented, "unexpected match specified: any:{}"),
62 k8sRes: []string{`
63 apiVersion: v1
64 kind: Pod
65 metadata:
66 name: emojivoto-meshed
67 namespace: emojivoto
68 labels:
69 app: emoji-svc
70 linkerd.io/control-plane-ns: controller-ns
71 annotations:
72 viz.linkerd.io/tap-enabled: "true"
73 linkerd.io/proxy-version: testinjectversion
74 status:
75 phase: Running
76 podIP: 127.0.0.1
77 `,
78 },
79 req: &tapPb.TapByResourceRequest{
80 Target: &metricsPb.ResourceSelection{
81 Resource: &metricsPb.Resource{
82 Namespace: "emojivoto",
83 Type: pkgK8s.Pod,
84 Name: "emojivoto-meshed",
85 },
86 },
87 Match: &tapPb.TapByResourceRequest_Match{
88 Match: &tapPb.TapByResourceRequest_Match_Any{
89 Any: &tapPb.TapByResourceRequest_Match_Seq{},
90 },
91 },
92 },
93 },
94 {
95 err: status.Errorf(codes.NotFound, "no pods to tap for type=\"pod\" name=\"emojivoto-not-meshed\"\n"),
96 k8sRes: []string{`
97 apiVersion: v1
98 kind: Pod
99 metadata:
100 name: emojivoto-not-meshed
101 namespace: emojivoto
102 labels:
103 app: emoji-svc
104 status:
105 phase: Running
106 podIP: 127.0.0.1
107 `,
108 },
109 req: &tapPb.TapByResourceRequest{
110 Target: &metricsPb.ResourceSelection{
111 Resource: &metricsPb.Resource{
112 Namespace: "emojivoto",
113 Type: pkgK8s.Pod,
114 Name: "emojivoto-not-meshed",
115 },
116 },
117 },
118 },
119 {
120 err: status.Errorf(codes.Unimplemented, "unimplemented resource type: bad-type"),
121 k8sRes: []string{},
122 req: &tapPb.TapByResourceRequest{
123 Target: &metricsPb.ResourceSelection{
124 Resource: &metricsPb.Resource{
125 Namespace: "emojivoto",
126 Type: "bad-type",
127 Name: "emojivoto-meshed-not-found",
128 },
129 },
130 },
131 },
132 {
133 err: status.Errorf(codes.NotFound, "pod \"emojivoto-meshed-not-found\" not found"),
134 k8sRes: []string{`
135 apiVersion: v1
136 kind: Pod
137 metadata:
138 name: emojivoto-meshed
139 namespace: emojivoto
140 labels:
141 app: emoji-svc
142 annotations:
143 viz.linkerd.io/tap-enabled: "true"
144 linkerd.io/proxy-version: testinjectversion
145 status:
146 phase: Running
147 podIP: 127.0.0.1
148 `,
149 },
150 req: &tapPb.TapByResourceRequest{
151 Target: &metricsPb.ResourceSelection{
152 Resource: &metricsPb.Resource{
153 Namespace: "emojivoto",
154 Type: pkgK8s.Pod,
155 Name: "emojivoto-meshed-not-found",
156 },
157 },
158 },
159 },
160 {
161 err: status.Errorf(codes.NotFound, "no pods to tap for type=\"pod\" name=\"emojivoto-meshed\"\n"),
162 k8sRes: []string{`
163 apiVersion: v1
164 kind: Pod
165 metadata:
166 name: emojivoto-meshed
167 namespace: emojivoto
168 labels:
169 app: emoji-svc
170 annotations:
171 viz.linkerd.io/tap-enabled: "true"
172 linkerd.io/proxy-version: testinjectversion
173 status:
174 phase: Finished
175 podIP: 127.0.0.1
176 `,
177 },
178 req: &tapPb.TapByResourceRequest{
179 Target: &metricsPb.ResourceSelection{
180 Resource: &metricsPb.Resource{
181 Namespace: "emojivoto",
182 Type: pkgK8s.Pod,
183 Name: "emojivoto-meshed",
184 },
185 },
186 },
187 },
188 {
189 err: status.Errorf(codes.NotFound, `no pods to tap for type="pod" name="emojivoto-meshed-tap-disabled"
190 1 pods found with tap disabled via the viz.linkerd.io/disable-tap annotation:
191 * emojivoto-meshed-tap-disabled
192 remove this annotation to make these pods valid tap targets
193 `),
194 k8sRes: []string{`
195 apiVersion: v1
196 kind: Pod
197 metadata:
198 name: emojivoto-meshed-tap-disabled
199 namespace: emojivoto
200 labels:
201 app: emoji-svc
202 linkerd.io/control-plane-ns: controller-ns
203 annotations:
204 viz.linkerd.io/disable-tap: "true"
205 linkerd.io/proxy-version: testinjectversion
206 status:
207 phase: Running
208 podIP: 127.0.0.1
209 `,
210 },
211 req: &tapPb.TapByResourceRequest{
212 Target: &metricsPb.ResourceSelection{
213 Resource: &metricsPb.Resource{
214 Namespace: "emojivoto",
215 Type: pkgK8s.Pod,
216 Name: "emojivoto-meshed-tap-disabled",
217 },
218 },
219 Match: &tapPb.TapByResourceRequest_Match{
220 Match: &tapPb.TapByResourceRequest_Match_All{
221 All: &tapPb.TapByResourceRequest_Match_Seq{},
222 },
223 },
224 },
225 },
226 {
227 err: status.Errorf(codes.NotFound, `no pods to tap for type="pod" name="emojivoto-meshed-tap-not-enabled"
228 1 pods found with tap not enabled:
229 * emojivoto-meshed-tap-not-enabled
230 restart these pods to enable tap and make them valid tap targets
231 `),
232 k8sRes: []string{`
233 apiVersion: v1
234 kind: Pod
235 metadata:
236 name: emojivoto-meshed-tap-not-enabled
237 namespace: emojivoto
238 labels:
239 app: emoji-svc
240 linkerd.io/control-plane-ns: controller-ns
241 annotations:
242 linkerd.io/proxy-version: testinjectversion
243 status:
244 phase: Running
245 podIP: 127.0.0.1
246 `,
247 },
248 req: &tapPb.TapByResourceRequest{
249 Target: &metricsPb.ResourceSelection{
250 Resource: &metricsPb.Resource{
251 Namespace: "emojivoto",
252 Type: pkgK8s.Pod,
253 Name: "emojivoto-meshed-tap-not-enabled",
254 },
255 },
256 Match: &tapPb.TapByResourceRequest_Match{
257 Match: &tapPb.TapByResourceRequest_Match_All{
258 All: &tapPb.TapByResourceRequest_Match_Seq{},
259 },
260 },
261 },
262 },
263 {
264
265 err: nil,
266 k8sRes: []string{`
267 apiVersion: v1
268 kind: Pod
269 metadata:
270 name: emojivoto-meshed
271 namespace: emojivoto
272 labels:
273 app: emoji-svc
274 linkerd.io/control-plane-ns: controller-ns
275 annotations:
276 viz.linkerd.io/tap-enabled: "true"
277 linkerd.io/proxy-version: testinjectversion
278 status:
279 phase: Running
280 podIP: 127.0.0.1
281 `,
282 },
283 req: &tapPb.TapByResourceRequest{
284 Target: &metricsPb.ResourceSelection{
285 Resource: &metricsPb.Resource{
286 Namespace: "emojivoto",
287 Type: pkgK8s.Pod,
288 Name: "emojivoto-meshed",
289 },
290 },
291 Match: &tapPb.TapByResourceRequest_Match{
292 Match: &tapPb.TapByResourceRequest_Match_All{
293 All: &tapPb.TapByResourceRequest_Match_Seq{},
294 },
295 },
296 },
297 requireID: ".emojivoto.serviceaccount.identity.controller-ns.cluster.local",
298 },
299 {
300 err: nil,
301 k8sRes: []string{`
302 apiVersion: v1
303 kind: Pod
304 metadata:
305 name: emojivoto-meshed
306 namespace: emojivoto
307 labels:
308 app: emoji-svc
309 linkerd.io/control-plane-ns: controller-ns
310 annotations:
311 viz.linkerd.io/tap-enabled: "true"
312 linkerd.io/proxy-version: testinjectversion
313 spec:
314 serviceAccountName: emojivoto-meshed-sa
315 status:
316 phase: Running
317 podIP: 127.0.0.1
318 `,
319 },
320 req: &tapPb.TapByResourceRequest{
321 Target: &metricsPb.ResourceSelection{
322 Resource: &metricsPb.Resource{
323 Namespace: "emojivoto",
324 Type: pkgK8s.Pod,
325 Name: "emojivoto-meshed",
326 },
327 },
328 Match: &tapPb.TapByResourceRequest_Match{
329 Match: &tapPb.TapByResourceRequest_Match_All{
330 All: &tapPb.TapByResourceRequest_Match_Seq{},
331 },
332 },
333 },
334 requireID: "emojivoto-meshed-sa.emojivoto.serviceaccount.identity.controller-ns.cluster.local",
335 },
336 {
337 err: nil,
338 k8sRes: []string{`
339 apiVersion: v1
340 kind: Namespace
341 metadata:
342 name: emojivoto
343 `, `
344 apiVersion: v1
345 kind: Pod
346 metadata:
347 name: emojivoto-meshed
348 namespace: emojivoto
349 labels:
350 app: emoji-svc
351 linkerd.io/control-plane-ns: controller-ns
352 annotations:
353 viz.linkerd.io/tap-enabled: "true"
354 linkerd.io/proxy-version: testinjectversion
355 spec:
356 serviceAccountName: emojivoto-meshed-sa
357 status:
358 phase: Running
359 podIP: 127.0.0.1
360 `,
361 },
362 req: &tapPb.TapByResourceRequest{
363 Target: &metricsPb.ResourceSelection{
364 Resource: &metricsPb.Resource{
365 Namespace: "",
366 Type: pkgK8s.Namespace,
367 Name: "emojivoto",
368 },
369 },
370 Match: &tapPb.TapByResourceRequest_Match{
371 Match: &tapPb.TapByResourceRequest_Match_All{
372 All: &tapPb.TapByResourceRequest_Match_Seq{},
373 },
374 },
375 },
376 requireID: "emojivoto-meshed-sa.emojivoto.serviceaccount.identity.controller-ns.cluster.local",
377 },
378 }
379
380 for i, exp := range expectations {
381 exp := exp
382 t.Run(fmt.Sprintf("%d: Returns expected response", i), func(t *testing.T) {
383 k8sAPI, err := k8s.NewFakeAPI(exp.k8sRes...)
384 if err != nil {
385 t.Fatalf("NewFakeAPI returned an error: %s", err)
386 }
387
388 stream := mockTapByResourceServer{
389 MockServerStream: util.NewMockServerStream(),
390 }
391
392 s := grpc.NewServer()
393
394 mockProxyTapServer := mockProxyTapServer{
395 mockControllerServer: stream,
396 }
397 proxy.RegisterTapServer(s, &mockProxyTapServer)
398
399 lis, err := net.Listen("tcp", "localhost:0")
400 if err != nil {
401 t.Fatalf("Failed to listen")
402 }
403
404
405 errChan := make(chan error, 1)
406 go func() {
407 errChan <- s.Serve(lis)
408 }()
409
410 defer func() {
411 if err := <-errChan; err != nil {
412 t.Fatalf("Failed to serve on %+v: %s", lis, err)
413 }
414 }()
415
416 defer s.GracefulStop()
417
418 _, port, err := net.SplitHostPort(lis.Addr().String())
419 if err != nil {
420 t.Fatal(err.Error())
421 }
422
423 tapPort, err := strconv.ParseUint(port, 10, 32)
424 if err != nil {
425 t.Fatalf("Invalid port: %s", port)
426 }
427
428 fakeGrpcServer := newGRPCTapServer(uint(tapPort), "controller-ns", "cluster.local", k8sAPI, nil)
429
430 k8sAPI.Sync(nil)
431
432 err = fakeGrpcServer.TapByResource(exp.req, &stream)
433 if err != nil || exp.err != nil {
434 code := status.Code(err)
435 expCode := status.Code(exp.err)
436 if code != expCode {
437 t.Fatalf("TapByResource returned unexpected error code: [%s], expected: [%s]", code, expCode)
438 }
439 if err.Error() != exp.err.Error() {
440 t.Fatalf("TapByResource returned unexpected error message: [%s], expected: [%s]", err.Error(), exp.err.Error())
441 }
442 }
443
444 if exp.requireID != "" {
445 md, ok := metadata.FromIncomingContext(mockProxyTapServer.ctx)
446 if !ok {
447 t.Fatalf("FromIncomingContext failed given: %+v", mockProxyTapServer.ctx)
448 }
449 if diff := deep.Equal(md.Get(pkgK8s.RequireIDHeader), []string{exp.requireID}); diff != nil {
450 t.Fatalf("Unexpected l5d-require-id header: %+v", diff)
451 }
452 }
453
454 })
455 }
456 }
457
458 func TestHydrateIPLabels(t *testing.T) {
459 expectations := []struct {
460 k8sRes []string
461 requestedIP string
462 labels map[string]string
463 }{
464 {
465
466 k8sRes: []string{`
467 apiVersion: v1
468 kind: Node
469 metadata:
470 name: node1
471 status:
472 addresses:
473 - address: 1.2.3.4
474 type: InternalIP
475 `, `
476 apiVersion: v1
477 kind: Pod
478 metadata:
479 name: emojivoto-meshed
480 namespace: emojivoto
481 labels:
482 app: emoji-svc
483 status:
484 phase: Running
485 podIP: 5.6.7.8
486 `,
487 },
488 requestedIP: "10.20.30.40",
489 labels: map[string]string{},
490 },
491 {
492
493 k8sRes: []string{`
494 apiVersion: v1
495 kind: Node
496 metadata:
497 name: node1
498 status:
499 addresses:
500 - address: 1.2.3.4
501 type: InternalIP
502 `, `
503 apiVersion: v1
504 kind: Pod
505 metadata:
506 name: emojivoto-meshed
507 namespace: emojivoto
508 labels:
509 app: emoji-svc
510 status:
511 phase: Running
512 podIP: 5.6.7.8
513 `,
514 },
515 requestedIP: "1.2.3.4",
516 labels: map[string]string{"node": "node1"},
517 },
518 {
519
520 k8sRes: []string{`
521 apiVersion: v1
522 kind: Node
523 metadata:
524 name: node1
525 status:
526 addresses:
527 - address: 1.2.3.4
528 type: InternalIP
529 `, `
530 apiVersion: v1
531 kind: Pod
532 metadata:
533 name: emojivoto-meshed
534 namespace: emojivoto
535 labels:
536 app: emoji-svc
537 status:
538 phase: Running
539 podIP: 1.2.3.4
540 `,
541 },
542 requestedIP: "1.2.3.4",
543 labels: map[string]string{"node": "node1"},
544 },
545 {
546
547 k8sRes: []string{`
548 apiVersion: v1
549 kind: Node
550 metadata:
551 name: node1
552 status:
553 addresses:
554 - address: 1.2.3.4
555 type: InternalIP
556 `, `
557 apiVersion: v1
558 kind: Pod
559 metadata:
560 name: emojivoto-meshed
561 namespace: emojivoto
562 labels:
563 app: emoji-svc
564 status:
565 phase: Running
566 podIP: 5.6.7.8
567 `,
568 },
569 requestedIP: "5.6.7.8",
570 labels: map[string]string{
571 "namespace": "emojivoto",
572 "pod": "emojivoto-meshed",
573 "serviceaccount": "default",
574 },
575 },
576 {
577
578 k8sRes: []string{`
579 apiVersion: v1
580 kind: Node
581 metadata:
582 name: node1
583 status:
584 addresses:
585 - address: 1.2.3.4
586 type: InternalIP
587 `, `
588 apiVersion: v1
589 kind: Pod
590 metadata:
591 name: emojivoto-meshed
592 namespace: emojivoto
593 labels:
594 app: emoji-svc
595 status:
596 phase: Running
597 podIP: 5.6.7.8
598 `, `
599 apiVersion: v1
600 kind: Pod
601 metadata:
602 name: emojivoto-meshed-2
603 namespace: emojivoto
604 labels:
605 app: emoji-svc
606 status:
607 phase: Finished
608 podIP: 5.6.7.8
609 `,
610 },
611 requestedIP: "5.6.7.8",
612 labels: map[string]string{
613 "namespace": "emojivoto",
614 "pod": "emojivoto-meshed",
615 "serviceaccount": "default",
616 },
617 },
618 {
619
620 k8sRes: []string{`
621 apiVersion: v1
622 kind: Node
623 metadata:
624 name: node1
625 status:
626 addresses:
627 - address: 1.2.3.4
628 type: InternalIP
629 `, `
630 apiVersion: v1
631 kind: Pod
632 metadata:
633 name: emojivoto-meshed
634 namespace: emojivoto
635 labels:
636 app: emoji-svc
637 status:
638 phase: Running
639 podIP: 5.6.7.8
640 `, `
641 apiVersion: v1
642 kind: Pod
643 metadata:
644 name: emojivoto-meshed-2
645 namespace: emojivoto
646 labels:
647 app: emoji-svc
648 status:
649 phase: Running
650 podIP: 5.6.7.8
651 `,
652 },
653 requestedIP: "5.6.7.8",
654 labels: map[string]string{},
655 },
656 }
657
658 ctx := context.Background()
659 for i, exp := range expectations {
660 exp := exp
661 t.Run(fmt.Sprintf("%d: Returns expected response", i), func(t *testing.T) {
662 k8sAPI, err := k8s.NewFakeAPI(exp.k8sRes...)
663 if err != nil {
664 t.Fatalf("NewFakeAPI returned an error: %s", err)
665 }
666 s, _ := NewGrpcTapServer(4190, "controller-ns", "cluster.local", k8sAPI, nil)
667 k8sAPI.Sync(nil)
668
669 labels := make(map[string]string)
670 ip, err := addr.ParsePublicIP(exp.requestedIP)
671 if err != nil {
672 t.Fatalf("Error parsing IP %s: %s", exp.requestedIP, err)
673 }
674 s.hydrateIPLabels(ctx, ip, labels)
675 if diff := deep.Equal(labels, exp.labels); diff != nil {
676 t.Fatalf("Unexpected labels: %+v", diff)
677 }
678 })
679 }
680 }
681
View as plain text