Add unit test for edges API endpoint (#3306)

Fixes #3052.

Adds a unit test for the edges API endpoint. To maintain a consistent order for
testing, the returned rows in api/public/edges.go are now sorted.
This commit is contained in:
Carol A. Scott 2019-08-23 09:28:02 -07:00 committed by GitHub
parent 653ec8c5b7
commit 089836842a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 278 additions and 94 deletions

View File

@ -7,107 +7,38 @@ import (
)
type edgesParamsExp struct {
options *edgesOptions
resSrc []string
resSrcNamespace []string
resDst []string
resDstNamespace []string
resClient []string
resServer []string
resMsg []string
resourceType string
file string
options *edgesOptions
resourceType string
file string
}
func TestEdges(t *testing.T) {
// response content for SRC, DST, SRC_NS, DST_NS, CLIENT_ID, SERVER_ID and MSG
var (
resSrc = []string{
"web",
"vote-bot",
"web",
"linkerd-controller",
}
resDst = []string{
"voting",
"web",
"emoji",
"linkerd-prometheus",
}
resSrcNamespace = []string{
"emojivoto",
"emojivoto",
"emojivoto",
"linkerd",
}
resDstNamespace = []string{
"emojivoto",
"emojivoto",
"emojivoto",
"linkerd",
}
resClient = []string{
"web.emojivoto.serviceaccount.identity.linkerd.cluster.local",
"default.emojivoto.serviceaccount.identity.linkerd.cluster.local",
"web.emojivoto.serviceaccount.identity.linkerd.cluster.local",
"linkerd-controller.linkerd.identity.linkerd.cluster.local",
}
resServer = []string{
"voting.emojivoto.serviceaccount.identity.linkerd.cluster.local",
"web.emojivoto.serviceaccount.identity.linkerd.cluster.local",
"emoji.emojivoto.serviceaccount.identity.linkerd.cluster.local",
"linkerd-prometheus.linkerd.identity.linkerd.cluster.local",
}
resMsg = []string{"", "", "", ""}
)
options := newEdgesOptions()
options.outputFormat = tableOutput
options.allNamespaces = true
t.Run("Returns edges", func(t *testing.T) {
testEdgesCall(edgesParamsExp{
options: options,
resourceType: "deployment",
resSrc: resSrc,
resSrcNamespace: resSrcNamespace,
resDst: resDst,
resDstNamespace: resDstNamespace,
resClient: resClient,
resServer: resServer,
resMsg: resMsg,
file: "edges_one_output.golden",
options: options,
resourceType: "deployment",
file: "edges_one_output.golden",
}, t)
})
options.outputFormat = jsonOutput
t.Run("Returns edges (json)", func(t *testing.T) {
testEdgesCall(edgesParamsExp{
options: options,
resourceType: "deployment",
resSrc: resSrc,
resSrcNamespace: resSrcNamespace,
resDst: resDst,
resDstNamespace: resDstNamespace,
resClient: resClient,
resServer: resServer,
resMsg: resMsg,
file: "edges_one_output_json.golden",
options: options,
resourceType: "deployment",
file: "edges_one_output_json.golden",
}, t)
})
t.Run("Returns edges (wide)", func(t *testing.T) {
options.outputFormat = wideOutput
testEdgesCall(edgesParamsExp{
options: options,
resourceType: "deployment",
resSrc: resSrc,
resSrcNamespace: resSrcNamespace,
resDst: resDst,
resDstNamespace: resDstNamespace,
resClient: resClient,
resServer: resServer,
resMsg: resMsg,
file: "edges_wide_output.golden",
options: options,
resourceType: "deployment",
file: "edges_wide_output.golden",
}, t)
})
@ -169,7 +100,7 @@ func TestEdges(t *testing.T) {
func testEdgesCall(exp edgesParamsExp, t *testing.T) {
mockClient := &public.MockAPIClient{}
response := public.GenEdgesResponse(exp.resourceType, exp.resSrc, exp.resDst, exp.resSrcNamespace, exp.resDstNamespace, exp.resClient, exp.resServer, exp.resMsg)
response := public.GenEdgesResponse(exp.resourceType, "all")
mockClient.EdgesResponseToReturn = &response

View File

@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"sort"
"strings"
pb "github.com/linkerd/linkerd2/controller/gen/public"
@ -189,5 +190,17 @@ func processEdgeMetrics(inbound, outbound model.Vector, resourceType, selectedNa
}
}
// sort rows before returning in order to have a consistent order for tests
edges = sortEdgeRows(edges)
return edges
}
func sortEdgeRows(rows []*pb.Edge) []*pb.Edge {
sort.Slice(rows, func(i, j int) bool {
keyI := rows[i].GetSrc().GetNamespace() + rows[i].GetDst().GetNamespace() + rows[i].GetSrc().GetName() + rows[i].GetDst().GetName()
keyJ := rows[j].GetSrc().GetNamespace() + rows[j].GetDst().GetNamespace() + rows[j].GetSrc().GetName() + rows[j].GetDst().GetName()
return keyI < keyJ
})
return rows
}

View File

@ -0,0 +1,169 @@
package public
import (
"context"
"testing"
"github.com/golang/protobuf/proto"
pb "github.com/linkerd/linkerd2/controller/gen/public"
pkgK8s "github.com/linkerd/linkerd2/pkg/k8s"
"github.com/prometheus/common/model"
)
const (
clientIDLabel = model.LabelName("client_id")
serverIDLabel = model.LabelName("server_id")
resourceLabel = model.LabelName("deployment")
)
type edgesExpected struct {
expectedStatRPC
req pb.EdgesRequest // the request we would like to test
expectedResponse pb.EdgesResponse // the edges response we expect
}
func genInboundPromSample(resourceNamespace, resourceName, clientID string) *model.Sample {
return &model.Sample{
Metric: model.Metric{
resourceLabel: model.LabelValue(resourceName),
namespaceLabel: model.LabelValue(resourceNamespace),
clientIDLabel: model.LabelValue(clientID),
},
Value: 123,
Timestamp: 456,
}
}
func genOutboundPromSample(resourceNamespace, resourceName, resourceNameDst, resourceNamespaceDst, serverID string) *model.Sample {
dstResourceLabel := "dst_" + resourceLabel
return &model.Sample{
Metric: model.Metric{
resourceLabel: model.LabelValue(resourceName),
namespaceLabel: model.LabelValue(resourceNamespace),
dstNamespaceLabel: model.LabelValue(resourceNamespaceDst),
dstResourceLabel: model.LabelValue(resourceNameDst),
serverIDLabel: model.LabelValue(serverID),
},
Value: 123,
Timestamp: 456,
}
}
func testEdges(t *testing.T, expectations []edgesExpected) {
for _, exp := range expectations {
mockProm, fakeGrpcServer, err := newMockGrpcServer(exp.expectedStatRPC)
if err != nil {
t.Fatalf("Error creating mock grpc server: %s", err)
}
rsp, err := fakeGrpcServer.Edges(context.TODO(), &exp.req)
if err != exp.err {
t.Fatalf("Expected error: %s, Got: %s", exp.err, err)
}
err = exp.verifyPromQueries(mockProm)
if err != nil {
t.Fatal(err)
}
rspEdgeRows := rsp.GetOk().Edges
if len(rspEdgeRows) != len(exp.expectedResponse.GetOk().Edges) {
t.Fatalf(
"Expected [%d] edge rows, got [%d].\nExpected:\n%s\nGot:\n%s",
len(exp.expectedResponse.GetOk().Edges),
len(rspEdgeRows),
exp.expectedResponse.GetOk().Edges,
rspEdgeRows,
)
}
for i, st := range rspEdgeRows {
expected := exp.expectedResponse.GetOk().Edges[i]
if !proto.Equal(st, expected) {
t.Fatalf("Expected: %+v\n Got: %+v\n", expected, st)
}
}
if !proto.Equal(exp.expectedResponse.GetOk(), rsp.GetOk()) {
t.Fatalf("Expected edgesOkResp: %+v\n Got: %+v", &exp.expectedResponse, rsp)
}
}
}
func TestEdges(t *testing.T) {
mockPromResponse := model.Vector{
genInboundPromSample("emojivoto", "emoji", "web.emojivoto.serviceaccount.identity.linkerd.cluster.local"),
genInboundPromSample("emojivoto", "voting", "web.emojivoto.serviceaccount.identity.linkerd.cluster.local"),
genInboundPromSample("emojivoto", "web", "default.emojivoto.serviceaccount.identity.linkerd.cluster.local"),
genInboundPromSample("linkerd", "linkerd-prometheus", "linkerd-controller.linkerd.identity.linkerd.cluster.local"),
genOutboundPromSample("emojivoto", "web", "emoji", "emojivoto", "emoji.emojivoto.serviceaccount.identity.linkerd.cluster.local"),
genOutboundPromSample("emojivoto", "web", "voting", "emojivoto", "voting.emojivoto.serviceaccount.identity.linkerd.cluster.local"),
genOutboundPromSample("emojivoto", "vote-bot", "web", "emojivoto", "web.emojivoto.serviceaccount.identity.linkerd.cluster.local"),
genOutboundPromSample("linkerd", "linkerd-controller", "linkerd-prometheus", "linkerd", "linkerd-prometheus.linkerd.identity.linkerd.cluster.local"),
}
t.Run("Successfully returns edges for resource type Deployment and namespace emojivoto", func(t *testing.T) {
expectations := []edgesExpected{
{
expectedStatRPC: expectedStatRPC{
err: nil,
mockPromResponse: mockPromResponse,
},
req: pb.EdgesRequest{
Selector: &pb.ResourceSelection{
Resource: &pb.Resource{
Namespace: "emojivoto",
Type: pkgK8s.Deployment,
},
},
},
expectedResponse: GenEdgesResponse("deployment", "emojivoto"),
}}
testEdges(t, expectations)
})
t.Run("Successfully returns edges for resource type Deployment and namespace linkerd", func(t *testing.T) {
expectations := []edgesExpected{
{
expectedStatRPC: expectedStatRPC{
err: nil,
mockPromResponse: mockPromResponse,
},
req: pb.EdgesRequest{
Selector: &pb.ResourceSelection{
Resource: &pb.Resource{
Namespace: "linkerd",
Type: pkgK8s.Deployment,
},
},
},
expectedResponse: GenEdgesResponse("deployment", "linkerd"),
}}
testEdges(t, expectations)
})
t.Run("Successfully returns edges for resource type Deployment and all namespaces", func(t *testing.T) {
expectations := []edgesExpected{
{
expectedStatRPC: expectedStatRPC{
err: nil,
mockPromResponse: mockPromResponse,
},
req: pb.EdgesRequest{
Selector: &pb.ResourceSelection{
Resource: &pb.Resource{
Type: pkgK8s.Deployment,
},
},
},
expectedResponse: GenEdgesResponse("deployment", "all"),
}}
testEdges(t, expectations)
})
}

View File

@ -371,29 +371,100 @@ func GenStatTsResponse(resName, resType string, resNs []string, basicStats bool,
return resp
}
// GenEdgesResponse generates a mock Public API StatSummaryResponse
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-controller",
dst: "linkerd-prometheus",
srcNamespace: "linkerd",
dstNamespace: "linkerd",
clientID: "linkerd-controller.linkerd.identity.linkerd.cluster.local",
serverID: "linkerd-prometheus.linkerd.identity.linkerd.cluster.local",
msg: "",
},
}
// GenEdgesResponse generates a mock Public API EdgesResponse
// object.
func GenEdgesResponse(resourceType string, resSrc, resDst, resSrcNamespace, resDstNamespace, resClient, resServer, msg []string) pb.EdgesResponse {
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 i := range resSrc {
for _, row := range edgeRows {
edge := &pb.Edge{
Src: &pb.Resource{
Name: resSrc[i],
Namespace: resSrcNamespace[i],
Type: resourceType,
Name: row.src,
Namespace: row.srcNamespace,
Type: row.resourceType,
},
Dst: &pb.Resource{
Name: resDst[i],
Namespace: resDstNamespace[i],
Type: resourceType,
Name: row.dst,
Namespace: row.dstNamespace,
Type: row.resourceType,
},
ClientId: resClient[i],
ServerId: resServer[i],
NoIdentityMsg: msg[i],
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{