package api import ( "context" "fmt" "sort" "github.com/go-test/deep" "github.com/linkerd/linkerd2/controller/k8s" "github.com/linkerd/linkerd2/pkg/prometheus" pb "github.com/linkerd/linkerd2/viz/metrics-api/gen/viz" "github.com/prometheus/common/model" "google.golang.org/grpc" ) // MockAPIClient satisfies the metrics-api gRPC interfaces type MockAPIClient struct { ErrorToReturn error ListPodsResponseToReturn *pb.ListPodsResponse ListServicesResponseToReturn *pb.ListServicesResponse StatSummaryResponseToReturn *pb.StatSummaryResponse AuthzResponseToReturn *pb.AuthzResponse GatewaysResponseToReturn *pb.GatewaysResponse TopRoutesResponseToReturn *pb.TopRoutesResponse EdgesResponseToReturn *pb.EdgesResponse SelfCheckResponseToReturn *pb.SelfCheckResponse } // StatSummary provides a mock of a metrics-api method. func (c *MockAPIClient) StatSummary(ctx context.Context, in *pb.StatSummaryRequest, opts ...grpc.CallOption) (*pb.StatSummaryResponse, error) { return c.StatSummaryResponseToReturn, c.ErrorToReturn } // Authz provides a mock of a metrics-api method. func (c *MockAPIClient) Authz(ctx context.Context, in *pb.AuthzRequest, opts ...grpc.CallOption) (*pb.AuthzResponse, error) { return c.AuthzResponseToReturn, c.ErrorToReturn } // Gateways provides a mock of a metrics-api method. func (c *MockAPIClient) Gateways(ctx context.Context, in *pb.GatewaysRequest, opts ...grpc.CallOption) (*pb.GatewaysResponse, error) { return c.GatewaysResponseToReturn, c.ErrorToReturn } // TopRoutes provides a mock of a metrics-api method. func (c *MockAPIClient) TopRoutes(ctx context.Context, in *pb.TopRoutesRequest, opts ...grpc.CallOption) (*pb.TopRoutesResponse, error) { return c.TopRoutesResponseToReturn, c.ErrorToReturn } // Edges provides a mock of a metrics-api method. func (c *MockAPIClient) Edges(ctx context.Context, in *pb.EdgesRequest, opts ...grpc.CallOption) (*pb.EdgesResponse, error) { return c.EdgesResponseToReturn, c.ErrorToReturn } // ListPods provides a mock of a metrics-api method. func (c *MockAPIClient) ListPods(ctx context.Context, in *pb.ListPodsRequest, opts ...grpc.CallOption) (*pb.ListPodsResponse, error) { return c.ListPodsResponseToReturn, c.ErrorToReturn } // ListServices provides a mock of a metrics-api method. func (c *MockAPIClient) ListServices(ctx context.Context, in *pb.ListServicesRequest, opts ...grpc.CallOption) (*pb.ListServicesResponse, error) { return c.ListServicesResponseToReturn, c.ErrorToReturn } // SelfCheck provides a mock of a metrics-api method. func (c *MockAPIClient) SelfCheck(ctx context.Context, in *pb.SelfCheckRequest, _ ...grpc.CallOption) (*pb.SelfCheckResponse, error) { return c.SelfCheckResponseToReturn, c.ErrorToReturn } // PodCounts is a test helper struct that is used for representing data in a // StatTable.PodGroup.Row. type PodCounts struct { Status string MeshedPods uint64 RunningPods uint64 FailedPods uint64 Errors map[string]*pb.PodErrors } // GenStatSummaryResponse generates a mock metrics-api StatSummaryResponse // object. func GenStatSummaryResponse(resName, resType string, resNs []string, counts *PodCounts, basicStats bool, tcpStats bool) *pb.StatSummaryResponse { rows := []*pb.StatTable_PodGroup_Row{} for _, ns := range resNs { statTableRow := &pb.StatTable_PodGroup_Row{ Resource: &pb.Resource{ Namespace: ns, Type: resType, Name: resName, }, TimeWindow: "1m", } if basicStats { statTableRow.Stats = &pb.BasicStats{ SuccessCount: 123, FailureCount: 0, LatencyMsP50: 123, LatencyMsP95: 123, LatencyMsP99: 123, } } if tcpStats { statTableRow.TcpStats = &pb.TcpStats{ OpenConnections: 123, ReadBytesTotal: 123, WriteBytesTotal: 123, } } if counts != nil { statTableRow.MeshedPodCount = counts.MeshedPods statTableRow.RunningPodCount = counts.RunningPods statTableRow.FailedPodCount = counts.FailedPods statTableRow.Status = counts.Status statTableRow.ErrorsByPod = counts.Errors } rows = append(rows, statTableRow) } resp := &pb.StatSummaryResponse{ Response: &pb.StatSummaryResponse_Ok_{ // https://github.com/golang/protobuf/issues/205 Ok: &pb.StatSummaryResponse_Ok{ StatTables: []*pb.StatTable{ { Table: &pb.StatTable_PodGroup_{ PodGroup: &pb.StatTable_PodGroup{ Rows: rows, }, }, }, }, }, }, } return resp } type mockEdgeRow struct { resourceType string src string dst string srcNamespace string dstNamespace string clientID string serverID string msg string } // a slice of edge rows to generate mock results var emojivotoEdgeRows = []*mockEdgeRow{ { resourceType: "deployment", src: "web", dst: "voting", srcNamespace: "emojivoto", dstNamespace: "emojivoto", clientID: "web.emojivoto.serviceaccount.identity.linkerd.cluster.local", serverID: "voting.emojivoto.serviceaccount.identity.linkerd.cluster.local", msg: "", }, { resourceType: "deployment", src: "vote-bot", dst: "web", srcNamespace: "emojivoto", dstNamespace: "emojivoto", clientID: "default.emojivoto.serviceaccount.identity.linkerd.cluster.local", serverID: "web.emojivoto.serviceaccount.identity.linkerd.cluster.local", msg: "", }, { resourceType: "deployment", src: "web", dst: "emoji", srcNamespace: "emojivoto", dstNamespace: "emojivoto", clientID: "web.emojivoto.serviceaccount.identity.linkerd.cluster.local", serverID: "emoji.emojivoto.serviceaccount.identity.linkerd.cluster.local", msg: "", }, } // a slice of edge rows to generate mock results var linkerdEdgeRows = []*mockEdgeRow{ { resourceType: "deployment", src: "linkerd-identity", dst: "linkerd-prometheus", srcNamespace: "linkerd", dstNamespace: "linkerd", clientID: "linkerd-identity.linkerd.serviceaccount.identity.linkerd.cluster.local", serverID: "linkerd-prometheus.linkerd.serviceaccount.identity.linkerd.cluster.local", msg: "", }, } // GenEdgesResponse generates a mock metrics-api EdgesResponse // object. func GenEdgesResponse(resourceType string, edgeRowNamespace string) *pb.EdgesResponse { edgeRows := emojivotoEdgeRows if edgeRowNamespace == "linkerd" { edgeRows = linkerdEdgeRows } else if edgeRowNamespace == "all" { // combine emojivotoEdgeRows and linkerdEdgeRows edgeRows = append(edgeRows, linkerdEdgeRows...) } edges := []*pb.Edge{} for _, row := range edgeRows { edge := &pb.Edge{ Src: &pb.Resource{ Name: row.src, Namespace: row.srcNamespace, Type: row.resourceType, }, Dst: &pb.Resource{ Name: row.dst, Namespace: row.dstNamespace, Type: row.resourceType, }, ClientId: row.clientID, ServerId: row.serverID, NoIdentityMsg: row.msg, } edges = append(edges, edge) } // sorting to retain consistent order for tests edges = sortEdgeRows(edges) resp := &pb.EdgesResponse{ Response: &pb.EdgesResponse_Ok_{ Ok: &pb.EdgesResponse_Ok{ Edges: edges, }, }, } return resp } // GenTopRoutesResponse generates a mock metrics-api TopRoutesResponse object. func GenTopRoutesResponse(routes []string, counts []uint64, outbound bool, authority string) *pb.TopRoutesResponse { rows := []*pb.RouteTable_Row{} for i, route := range routes { row := &pb.RouteTable_Row{ Route: route, Authority: authority, Stats: &pb.BasicStats{ SuccessCount: counts[i], FailureCount: 0, LatencyMsP50: 123, LatencyMsP95: 123, LatencyMsP99: 123, }, TimeWindow: "1m", } if outbound { row.Stats.ActualSuccessCount = counts[i] } rows = append(rows, row) } defaultRow := &pb.RouteTable_Row{ Route: "[DEFAULT]", Authority: authority, Stats: &pb.BasicStats{ SuccessCount: counts[len(counts)-1], FailureCount: 0, LatencyMsP50: 123, LatencyMsP95: 123, LatencyMsP99: 123, }, TimeWindow: "1m", } if outbound { defaultRow.Stats.ActualSuccessCount = counts[len(counts)-1] } rows = append(rows, defaultRow) resp := &pb.TopRoutesResponse{ Response: &pb.TopRoutesResponse_Ok_{ Ok: &pb.TopRoutesResponse_Ok{ Routes: []*pb.RouteTable{ { Rows: rows, Resource: "deploy/foobar", }, }, }, }, } return resp } type expectedStatRPC struct { err error k8sConfigs []string // k8s objects to seed the API mockPromResponse model.Value // mock out a prometheus query response expectedPrometheusQueries []string // queries we expect metrics-api to issue to prometheus } func newMockGrpcServer(exp expectedStatRPC) (*prometheus.MockProm, *grpcServer, error) { k8sAPI, err := k8s.NewFakeAPI(exp.k8sConfigs...) if err != nil { return nil, nil, err } mockProm := &prometheus.MockProm{Res: exp.mockPromResponse} fakeGrpcServer := &grpcServer{ prometheusAPI: mockProm, k8sAPI: k8sAPI, controllerNamespace: "linkerd", clusterDomain: "cluster.local", ignoredNamespaces: []string{}, } k8sAPI.Sync(nil) return mockProm, fakeGrpcServer, nil } func (exp expectedStatRPC) verifyPromQueries(mockProm *prometheus.MockProm) error { // if exp.expectedPrometheusQueries is an empty slice we still want to check no queries were executed. if exp.expectedPrometheusQueries != nil { sort.Strings(exp.expectedPrometheusQueries) sort.Strings(mockProm.QueriesExecuted) // because reflect.DeepEqual([]string{}, nil) is false if len(exp.expectedPrometheusQueries) == 0 && len(mockProm.QueriesExecuted) == 0 { return nil } if diff := deep.Equal(exp.expectedPrometheusQueries, mockProm.QueriesExecuted); diff != nil { return fmt.Errorf("Prometheus queries incorrect: %+v", diff) } } return nil }