linkerd2/controller/api/public/stat_summary_test.go

643 lines
17 KiB
Go

package public
import (
"context"
"errors"
"reflect"
"sort"
"testing"
"github.com/prometheus/common/model"
tap "github.com/runconduit/conduit/controller/gen/controller/tap"
pb "github.com/runconduit/conduit/controller/gen/public"
"github.com/runconduit/conduit/controller/k8s"
pkgK8s "github.com/runconduit/conduit/pkg/k8s"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes/fake"
"k8s.io/client-go/kubernetes/scheme"
)
type statSumExpected struct {
err error
k8sConfigs []string // k8s objects to seed the lister
mockPromResponse model.Value // mock out a prometheus query response
expectedPrometheusQueries []string // queries we expect public-api to issue to prometheus
req pb.StatSummaryRequest // the request we would like to test
expectedResponse pb.StatSummaryResponse // the stat response we expect
}
func prometheusMetric(resName string, resType string, resNs string, classification string) model.Vector {
return model.Vector{
genPromSample(resName, resType, resNs, classification),
}
}
func genPromSample(resName string, resType string, resNs string, classification string) *model.Sample {
return &model.Sample{
Metric: model.Metric{
model.LabelName(resType): model.LabelValue(resName),
"namespace": model.LabelValue(resNs),
"classification": model.LabelValue(classification),
},
Value: 123,
Timestamp: 456,
}
}
func genStatSummaryResponse(resName, resType, resNs string, meshedPods uint64, runningPods uint64) pb.StatSummaryResponse {
return pb.StatSummaryResponse{
Response: &pb.StatSummaryResponse_Ok_{ // https://github.com/golang/protobuf/issues/205
Ok: &pb.StatSummaryResponse_Ok{
StatTables: []*pb.StatTable{
&pb.StatTable{
Table: &pb.StatTable_PodGroup_{
PodGroup: &pb.StatTable_PodGroup{
Rows: []*pb.StatTable_PodGroup_Row{
&pb.StatTable_PodGroup_Row{
Resource: &pb.Resource{
Namespace: resNs,
Type: resType,
Name: resName,
},
Stats: &pb.BasicStats{
SuccessCount: 123,
FailureCount: 0,
LatencyMsP50: 123,
LatencyMsP95: 123,
LatencyMsP99: 123,
},
TimeWindow: "1m",
MeshedPodCount: meshedPods,
RunningPodCount: runningPods,
},
},
},
},
},
},
},
},
}
}
func genEmptyResponse() pb.StatSummaryResponse {
return pb.StatSummaryResponse{
Response: &pb.StatSummaryResponse_Ok_{ // https://github.com/golang/protobuf/issues/205
Ok: &pb.StatSummaryResponse_Ok{
StatTables: []*pb.StatTable{},
},
},
}
}
func createFakeLister(t *testing.T, k8sConfigs []string) *k8s.Lister {
k8sObjs := []runtime.Object{}
for _, res := range k8sConfigs {
decode := scheme.Codecs.UniversalDeserializer().Decode
obj, _, err := decode([]byte(res), nil, nil)
if err != nil {
t.Fatalf("could not decode yml: %s", err)
}
k8sObjs = append(k8sObjs, obj)
}
clientSet := fake.NewSimpleClientset(k8sObjs...)
return k8s.NewLister(clientSet)
}
func testStatSummary(t *testing.T, expectations []statSumExpected) {
for _, exp := range expectations {
lister := createFakeLister(t, exp.k8sConfigs)
mockProm := &MockProm{Res: exp.mockPromResponse}
fakeGrpcServer := newGrpcServer(
mockProm,
tap.NewTapClient(nil),
lister,
"conduit",
[]string{},
)
err := lister.Sync()
if err != nil {
t.Fatalf("timed out wait for caches to sync")
}
rsp, err := fakeGrpcServer.StatSummary(context.TODO(), &exp.req)
if err != exp.err {
t.Fatalf("Expected error: %s, Got: %s", exp.err, err)
}
if len(exp.expectedPrometheusQueries) > 0 {
sort.Strings(exp.expectedPrometheusQueries)
sort.Strings(mockProm.QueriesExecuted)
if !reflect.DeepEqual(exp.expectedPrometheusQueries, mockProm.QueriesExecuted) {
t.Fatalf("Prometheus queries incorrect. \nExpected: %+v \nGot: %+v",
exp.expectedPrometheusQueries, mockProm.QueriesExecuted)
}
}
if len(exp.expectedResponse.GetOk().StatTables) > 0 {
unsortedStatTables := rsp.GetOk().StatTables
sort.Sort(byStatResult(unsortedStatTables))
if !reflect.DeepEqual(exp.expectedResponse.GetOk().StatTables, unsortedStatTables) {
t.Fatalf("Expected: %+v\n Got: %+v", &exp.expectedResponse, rsp)
}
}
}
}
type byStatResult []*pb.StatTable
func (s byStatResult) Len() int {
return len(s)
}
func (s byStatResult) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s byStatResult) Less(i, j int) bool {
if len(s[i].GetPodGroup().Rows) == 0 {
return true
}
if len(s[j].GetPodGroup().Rows) == 0 {
return false
}
return s[i].GetPodGroup().Rows[0].Resource.Type < s[j].GetPodGroup().Rows[0].Resource.Type
}
func TestStatSummary(t *testing.T) {
t.Run("Successfully performs a query based on resource type", func(t *testing.T) {
expectations := []statSumExpected{
statSumExpected{
err: nil,
k8sConfigs: []string{`
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: emoji
namespace: emojivoto
spec:
selector:
matchLabels:
app: emoji-svc
strategy: {}
template:
spec:
containers:
- image: buoyantio/emojivoto-emoji-svc:v3
`, `
apiVersion: v1
kind: Pod
metadata:
name: emojivoto-meshed
namespace: emojivoto
labels:
app: emoji-svc
annotations:
conduit.io/proxy-version: testinjectversion
status:
phase: Running
`, `
apiVersion: v1
kind: Pod
metadata:
name: emojivoto-not-meshed
namespace: emojivoto
labels:
app: emoji-svc
status:
phase: Running
`, `
apiVersion: v1
kind: Pod
metadata:
name: emojivoto-meshed-not-running
namespace: emojivoto
labels:
app: emoji-svc
annotations:
conduit.io/proxy-version: testinjectversion
status:
phase: Completed
`,
},
mockPromResponse: prometheusMetric("emoji", "deployment", "emojivoto", "success"),
req: pb.StatSummaryRequest{
Selector: &pb.ResourceSelection{
Resource: &pb.Resource{
Namespace: "emojivoto",
Type: pkgK8s.Deployments,
},
},
TimeWindow: "1m",
},
expectedResponse: genStatSummaryResponse("emoji", "deployments", "emojivoto", 1, 2),
},
}
testStatSummary(t, expectations)
})
t.Run("Queries prometheus for a specific resource if name is specified", func(t *testing.T) {
expectations := []statSumExpected{
statSumExpected{
err: nil,
k8sConfigs: []string{`
apiVersion: v1
kind: Pod
metadata:
name: emojivoto-1
namespace: emojivoto
labels:
app: emoji-svc
annotations:
conduit.io/proxy-version: testinjectversion
status:
phase: Running
`,
},
mockPromResponse: prometheusMetric("emojivoto-1", "pod", "emojivoto", "success"),
req: pb.StatSummaryRequest{
Selector: &pb.ResourceSelection{
Resource: &pb.Resource{
Name: "emojivoto-1",
Namespace: "emojivoto",
Type: pkgK8s.Pods,
},
},
TimeWindow: "1m",
},
expectedPrometheusQueries: []string{
`histogram_quantile(0.5, sum(irate(response_latency_ms_bucket{direction="inbound", namespace="emojivoto", pod="emojivoto-1"}[1m])) by (le, namespace, pod))`,
`histogram_quantile(0.95, sum(irate(response_latency_ms_bucket{direction="inbound", namespace="emojivoto", pod="emojivoto-1"}[1m])) by (le, namespace, pod))`,
`histogram_quantile(0.99, sum(irate(response_latency_ms_bucket{direction="inbound", namespace="emojivoto", pod="emojivoto-1"}[1m])) by (le, namespace, pod))`,
`sum(increase(response_total{direction="inbound", namespace="emojivoto", pod="emojivoto-1"}[1m])) by (namespace, pod, classification)`,
},
expectedResponse: genStatSummaryResponse("emojivoto-1", "pods", "emojivoto", 1, 1),
},
}
testStatSummary(t, expectations)
})
t.Run("Queries prometheus for outbound metrics if from resource is specified, ignores resource name", func(t *testing.T) {
expectations := []statSumExpected{
statSumExpected{
err: nil,
k8sConfigs: []string{`
apiVersion: v1
kind: Pod
metadata:
name: emojivoto-1
namespace: emojivoto
labels:
app: emoji-svc
annotations:
conduit.io/proxy-version: testinjectversion
status:
phase: Running
`,
},
mockPromResponse: model.Vector{
genPromSample("emojivoto-2", "pod", "emojivoto", "success"),
},
req: pb.StatSummaryRequest{
Selector: &pb.ResourceSelection{
Resource: &pb.Resource{
Name: "emojivoto-1",
Namespace: "emojivoto",
Type: pkgK8s.Pods,
},
},
TimeWindow: "1m",
Outbound: &pb.StatSummaryRequest_FromResource{
FromResource: &pb.Resource{
Name: "emojivoto-2",
Namespace: "emojivoto",
Type: pkgK8s.Pods,
},
},
},
expectedPrometheusQueries: []string{
`histogram_quantile(0.5, sum(irate(response_latency_ms_bucket{direction="outbound", namespace="emojivoto", pod="emojivoto-2"}[1m])) by (le, dst_namespace, dst_pod))`,
`histogram_quantile(0.95, sum(irate(response_latency_ms_bucket{direction="outbound", namespace="emojivoto", pod="emojivoto-2"}[1m])) by (le, dst_namespace, dst_pod))`,
`histogram_quantile(0.99, sum(irate(response_latency_ms_bucket{direction="outbound", namespace="emojivoto", pod="emojivoto-2"}[1m])) by (le, dst_namespace, dst_pod))`,
`sum(increase(response_total{direction="outbound", namespace="emojivoto", pod="emojivoto-2"}[1m])) by (dst_namespace, dst_pod, classification)`,
},
expectedResponse: genEmptyResponse(),
},
}
testStatSummary(t, expectations)
})
t.Run("Successfully queries for resource type 'all'", func(t *testing.T) {
expectations := []statSumExpected{
statSumExpected{
err: nil,
k8sConfigs: []string{`
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: emoji-deploy
namespace: emojivoto
spec:
selector:
matchLabels:
app: emoji-svc
strategy: {}
template:
spec:
containers:
- image: buoyantio/emojivoto-emoji-svc:v3
`, `
apiVersion: v1
kind: Service
metadata:
name: emoji-svc
namespace: emojivoto
spec:
clusterIP: None
selector:
app: emoji-svc
`, `
apiVersion: v1
kind: Pod
metadata:
name: emojivoto-pod-1
namespace: not-right-emojivoto-namespace
labels:
app: emoji-svc
annotations:
conduit.io/proxy-version: testinjectversion
status:
phase: Running
`, `
apiVersion: v1
kind: Pod
metadata:
name: emojivoto-pod-2
namespace: emojivoto
labels:
app: emoji-svc
annotations:
conduit.io/proxy-version: testinjectversion
status:
phase: Running
`,
},
mockPromResponse: prometheusMetric("emoji-deploy", "deployment", "emojivoto", "success"),
req: pb.StatSummaryRequest{
Selector: &pb.ResourceSelection{
Resource: &pb.Resource{
Namespace: "emojivoto",
Type: pkgK8s.All,
},
},
TimeWindow: "1m",
},
expectedResponse: pb.StatSummaryResponse{
Response: &pb.StatSummaryResponse_Ok_{ // https://github.com/golang/protobuf/issues/205
Ok: &pb.StatSummaryResponse_Ok{
StatTables: []*pb.StatTable{
&pb.StatTable{
Table: &pb.StatTable_PodGroup_{
PodGroup: &pb.StatTable_PodGroup{
Rows: []*pb.StatTable_PodGroup_Row{},
},
},
},
&pb.StatTable{
Table: &pb.StatTable_PodGroup_{
PodGroup: &pb.StatTable_PodGroup{
Rows: []*pb.StatTable_PodGroup_Row{
&pb.StatTable_PodGroup_Row{
Resource: &pb.Resource{
Namespace: "emojivoto",
Type: "deployments",
Name: "emoji-deploy",
},
Stats: &pb.BasicStats{
SuccessCount: 123,
FailureCount: 0,
LatencyMsP50: 123,
LatencyMsP95: 123,
LatencyMsP99: 123,
},
TimeWindow: "1m",
MeshedPodCount: 1,
RunningPodCount: 1,
},
},
},
},
},
&pb.StatTable{
Table: &pb.StatTable_PodGroup_{
PodGroup: &pb.StatTable_PodGroup{
Rows: []*pb.StatTable_PodGroup_Row{
&pb.StatTable_PodGroup_Row{
Resource: &pb.Resource{
Namespace: "emojivoto",
Type: "pods",
Name: "emojivoto-pod-2",
},
TimeWindow: "1m",
MeshedPodCount: 1,
RunningPodCount: 1,
},
},
},
},
},
&pb.StatTable{
Table: &pb.StatTable_PodGroup_{
PodGroup: &pb.StatTable_PodGroup{
Rows: []*pb.StatTable_PodGroup_Row{
&pb.StatTable_PodGroup_Row{
Resource: &pb.Resource{
Namespace: "emojivoto",
Type: "services",
Name: "emoji-svc",
},
TimeWindow: "1m",
MeshedPodCount: 1,
RunningPodCount: 1,
},
},
},
},
},
},
},
},
},
},
}
testStatSummary(t, expectations)
})
t.Run("Given an invalid resource type, returns error", func(t *testing.T) {
expectations := []statSumExpected{
statSumExpected{
err: errors.New("rpc error: code = Unimplemented desc = unimplemented resource type: badtype"),
req: pb.StatSummaryRequest{
Selector: &pb.ResourceSelection{
Resource: &pb.Resource{
Type: "badtype",
},
},
},
},
statSumExpected{
err: errors.New("rpc error: code = Unimplemented desc = unimplemented resource type: deployment"),
req: pb.StatSummaryRequest{
Selector: &pb.ResourceSelection{
Resource: &pb.Resource{
Type: "deployment",
},
},
},
},
statSumExpected{
err: errors.New("rpc error: code = Unimplemented desc = unimplemented resource type: pod"),
req: pb.StatSummaryRequest{
Selector: &pb.ResourceSelection{
Resource: &pb.Resource{
Type: "pod",
},
},
},
},
}
for _, exp := range expectations {
clientSet := fake.NewSimpleClientset()
lister := k8s.NewLister(clientSet)
fakeGrpcServer := newGrpcServer(
&MockProm{Res: exp.mockPromResponse},
tap.NewTapClient(nil),
lister,
"conduit",
[]string{},
)
_, err := fakeGrpcServer.StatSummary(context.TODO(), &exp.req)
if err != nil || exp.err != nil {
if (err == nil && exp.err != nil) ||
(err != nil && exp.err == nil) ||
(err.Error() != exp.err.Error()) {
t.Fatalf("Unexpected error (Expected: %s, Got: %s)", exp.err, err)
}
}
}
})
t.Run("Validates service stat requests", func(t *testing.T) {
clientSet := fake.NewSimpleClientset()
lister := k8s.NewLister(clientSet)
fakeGrpcServer := newGrpcServer(
&MockProm{Res: model.Vector{}},
tap.NewTapClient(nil),
lister,
"conduit",
[]string{},
)
invalidRequests := []statSumExpected{
statSumExpected{
req: pb.StatSummaryRequest{},
},
statSumExpected{
req: pb.StatSummaryRequest{
Selector: &pb.ResourceSelection{
Resource: &pb.Resource{
Type: "services",
},
},
},
},
statSumExpected{
req: pb.StatSummaryRequest{
Selector: &pb.ResourceSelection{
Resource: &pb.Resource{
Type: "services",
},
},
Outbound: &pb.StatSummaryRequest_ToResource{
ToResource: &pb.Resource{
Type: "pods",
},
},
},
},
statSumExpected{
req: pb.StatSummaryRequest{
Selector: &pb.ResourceSelection{
Resource: &pb.Resource{
Type: "pods",
},
},
Outbound: &pb.StatSummaryRequest_FromResource{
FromResource: &pb.Resource{
Type: "services",
},
},
},
},
}
for _, invalid := range invalidRequests {
rsp, err := fakeGrpcServer.StatSummary(context.TODO(), &invalid.req)
if err != nil || rsp.GetError() == nil {
t.Fatalf("Expected validation error on StatSummaryResponse, got %v, %v", rsp, err)
}
}
validRequests := []statSumExpected{
statSumExpected{
req: pb.StatSummaryRequest{
Selector: &pb.ResourceSelection{
Resource: &pb.Resource{
Type: "pods",
},
},
Outbound: &pb.StatSummaryRequest_ToResource{
ToResource: &pb.Resource{
Type: "services",
},
},
},
},
statSumExpected{
req: pb.StatSummaryRequest{
Selector: &pb.ResourceSelection{
Resource: &pb.Resource{
Type: "services",
},
},
Outbound: &pb.StatSummaryRequest_FromResource{
FromResource: &pb.Resource{
Type: "pods",
},
},
},
},
}
for _, valid := range validRequests {
rsp, err := fakeGrpcServer.StatSummary(context.TODO(), &valid.req)
if err != nil || rsp.GetError() != nil {
t.Fatalf("Did not expect validation error on StatSummaryResponse, got %v, %v", rsp, err)
}
}
})
}