...

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

Documentation: github.com/linkerd/linkerd2/viz/metrics-api

     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  // sort Pods in ListPodResponses for easier comparison
    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  // sort Services in ListServiceResponses for easier comparison
    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  			// non-matching owner type -> no pod in the result
   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  			// matching owner type -> pod is part of the result
   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  			// matching label in request -> pod is in the response
   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  			// NOT matching label in request -> pod is NOT in the response
   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  // TODO: consider refactoring with expectedStatRPC.verifyPromQueries
   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