...

Source file src/github.com/linkerd/linkerd2/viz/tap/api/grpc_server_test.go

Documentation: github.com/linkerd/linkerd2/viz/tap/api

     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  // mockTapByResourceServer satisfies controller.tap.Tap_TapByResourceServer
    32  type mockTapByResourceServer struct {
    33  	util.MockServerStream
    34  }
    35  
    36  func (m *mockTapByResourceServer) Send(event *tapPb.TapEvent) error {
    37  	return nil
    38  }
    39  
    40  // mockProxyTapServer satisfies proxy.tap.TapServer
    41  type mockProxyTapServer struct {
    42  	proxy.UnimplementedTapServer
    43  	mockControllerServer mockTapByResourceServer // for cancellation
    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  			// success, underlying tap events tested in http_server_test.go
   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 // pin
   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  			// TODO: mock out the underlying grpc tap events
   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  			// Requested IP that doesn't match node or any pod
   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  			// Requested IP that matches node only
   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  			// Requested IP that matches node and pod
   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  			// Requested IP that doesn't match node and matches exactly one pod
   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  			// Requested IP that doesn't match node and matches exactly one running pod and one finished pod
   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  			// Requested IP that doesn't match node and matches two running pods
   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 // pin
   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