linkerd2/controller/tap/server_test.go

626 lines
13 KiB
Go

package tap
import (
"context"
"fmt"
"net"
"reflect"
"strconv"
"testing"
proxy "github.com/linkerd/linkerd2-proxy-api/go/tap"
"github.com/linkerd/linkerd2/controller/api/util"
"github.com/linkerd/linkerd2/controller/gen/public"
"github.com/linkerd/linkerd2/controller/k8s"
"github.com/linkerd/linkerd2/pkg/addr"
pkgK8s "github.com/linkerd/linkerd2/pkg/k8s"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
type tapExpected struct {
err error
k8sRes []string
req public.TapByResourceRequest
requireID string
}
// mockTapByResourceServer satisfies controller.tap.Tap_TapByResourceServer
type mockTapByResourceServer struct {
util.MockServerStream
}
func (m *mockTapByResourceServer) Send(event *public.TapEvent) error {
return nil
}
// mockProxyTapServer satisfies proxy.tap.TapServer
type mockProxyTapServer struct {
mockControllerServer mockTapByResourceServer // for cancellation
ctx context.Context
}
func (m *mockProxyTapServer) Observe(req *proxy.ObserveRequest, obsSrv proxy.Tap_ObserveServer) error {
m.ctx = obsSrv.Context()
m.mockControllerServer.Cancel()
return nil
}
func TestTapByResource(t *testing.T) {
expectations := []tapExpected{
{
err: status.Error(codes.InvalidArgument, "TapByResource received nil target ResourceSelection"),
k8sRes: []string{},
req: public.TapByResourceRequest{},
},
{
err: status.Errorf(codes.Unimplemented, "unexpected match specified: any:<> "),
k8sRes: []string{`
apiVersion: v1
kind: Pod
metadata:
name: emojivoto-meshed
namespace: emojivoto
labels:
app: emoji-svc
linkerd.io/control-plane-ns: controller-ns
annotations:
linkerd.io/proxy-version: testinjectversion
status:
phase: Running
podIP: 127.0.0.1
`,
},
req: public.TapByResourceRequest{
Target: &public.ResourceSelection{
Resource: &public.Resource{
Namespace: "emojivoto",
Type: pkgK8s.Pod,
Name: "emojivoto-meshed",
},
},
Match: &public.TapByResourceRequest_Match{
Match: &public.TapByResourceRequest_Match_Any{
Any: &public.TapByResourceRequest_Match_Seq{},
},
},
},
},
{
err: status.Errorf(codes.NotFound, "no pods found for pod/emojivoto-not-meshed"),
k8sRes: []string{`
apiVersion: v1
kind: Pod
metadata:
name: emojivoto-not-meshed
namespace: emojivoto
labels:
app: emoji-svc
status:
phase: Running
podIP: 127.0.0.1
`,
},
req: public.TapByResourceRequest{
Target: &public.ResourceSelection{
Resource: &public.Resource{
Namespace: "emojivoto",
Type: pkgK8s.Pod,
Name: "emojivoto-not-meshed",
},
},
},
},
{
err: status.Errorf(codes.Unimplemented, "unimplemented resource type: bad-type"),
k8sRes: []string{},
req: public.TapByResourceRequest{
Target: &public.ResourceSelection{
Resource: &public.Resource{
Namespace: "emojivoto",
Type: "bad-type",
Name: "emojivoto-meshed-not-found",
},
},
},
},
{
err: status.Errorf(codes.NotFound, "pod \"emojivoto-meshed-not-found\" not found"),
k8sRes: []string{`
apiVersion: v1
kind: Pod
metadata:
name: emojivoto-meshed
namespace: emojivoto
labels:
app: emoji-svc
annotations:
linkerd.io/proxy-version: testinjectversion
status:
phase: Running
podIP: 127.0.0.1
`,
},
req: public.TapByResourceRequest{
Target: &public.ResourceSelection{
Resource: &public.Resource{
Namespace: "emojivoto",
Type: pkgK8s.Pod,
Name: "emojivoto-meshed-not-found",
},
},
},
},
{
err: status.Errorf(codes.NotFound, "no pods found for pod/emojivoto-meshed"),
k8sRes: []string{`
apiVersion: v1
kind: Pod
metadata:
name: emojivoto-meshed
namespace: emojivoto
labels:
app: emoji-svc
annotations:
linkerd.io/proxy-version: testinjectversion
status:
phase: Finished
podIP: 127.0.0.1
`,
},
req: public.TapByResourceRequest{
Target: &public.ResourceSelection{
Resource: &public.Resource{
Namespace: "emojivoto",
Type: pkgK8s.Pod,
Name: "emojivoto-meshed",
},
},
},
},
{
err: status.Errorf(codes.NotFound, "all pods found for pod/emojivoto-meshed-tap-disabled have tapping disabled"),
k8sRes: []string{`
apiVersion: v1
kind: Pod
metadata:
name: emojivoto-meshed-tap-disabled
namespace: emojivoto
labels:
app: emoji-svc
linkerd.io/control-plane-ns: controller-ns
annotations:
config.linkerd.io/disable-tap: "true"
linkerd.io/proxy-version: testinjectversion
status:
phase: Running
podIP: 127.0.0.1
`,
},
req: public.TapByResourceRequest{
Target: &public.ResourceSelection{
Resource: &public.Resource{
Namespace: "emojivoto",
Type: pkgK8s.Pod,
Name: "emojivoto-meshed-tap-disabled",
},
},
Match: &public.TapByResourceRequest_Match{
Match: &public.TapByResourceRequest_Match_All{
All: &public.TapByResourceRequest_Match_Seq{},
},
},
},
},
{
// success, underlying tap events tested in http_server_test.go
err: nil,
k8sRes: []string{`
apiVersion: v1
kind: Pod
metadata:
name: emojivoto-meshed
namespace: emojivoto
labels:
app: emoji-svc
linkerd.io/control-plane-ns: controller-ns
annotations:
linkerd.io/proxy-version: testinjectversion
status:
phase: Running
podIP: 127.0.0.1
`,
},
req: public.TapByResourceRequest{
Target: &public.ResourceSelection{
Resource: &public.Resource{
Namespace: "emojivoto",
Type: pkgK8s.Pod,
Name: "emojivoto-meshed",
},
},
Match: &public.TapByResourceRequest_Match{
Match: &public.TapByResourceRequest_Match_All{
All: &public.TapByResourceRequest_Match_Seq{},
},
},
},
requireID: ".emojivoto.serviceaccount.identity.controller-ns.cluster.local",
},
{
err: nil,
k8sRes: []string{`
apiVersion: v1
kind: Pod
metadata:
name: emojivoto-meshed
namespace: emojivoto
labels:
app: emoji-svc
linkerd.io/control-plane-ns: controller-ns
annotations:
linkerd.io/proxy-version: testinjectversion
spec:
serviceAccountName: emojivoto-meshed-sa
status:
phase: Running
podIP: 127.0.0.1
`,
},
req: public.TapByResourceRequest{
Target: &public.ResourceSelection{
Resource: &public.Resource{
Namespace: "emojivoto",
Type: pkgK8s.Pod,
Name: "emojivoto-meshed",
},
},
Match: &public.TapByResourceRequest_Match{
Match: &public.TapByResourceRequest_Match_All{
All: &public.TapByResourceRequest_Match_Seq{},
},
},
},
requireID: "emojivoto-meshed-sa.emojivoto.serviceaccount.identity.controller-ns.cluster.local",
},
{
err: nil,
k8sRes: []string{`
apiVersion: v1
kind: Namespace
metadata:
name: emojivoto
`, `
apiVersion: v1
kind: Pod
metadata:
name: emojivoto-meshed
namespace: emojivoto
labels:
app: emoji-svc
linkerd.io/control-plane-ns: controller-ns
annotations:
linkerd.io/proxy-version: testinjectversion
spec:
serviceAccountName: emojivoto-meshed-sa
status:
phase: Running
podIP: 127.0.0.1
`,
},
req: public.TapByResourceRequest{
Target: &public.ResourceSelection{
Resource: &public.Resource{
Namespace: "",
Type: pkgK8s.Namespace,
Name: "emojivoto",
},
},
Match: &public.TapByResourceRequest_Match{
Match: &public.TapByResourceRequest_Match_All{
All: &public.TapByResourceRequest_Match_Seq{},
},
},
},
requireID: "emojivoto-meshed-sa.emojivoto.serviceaccount.identity.controller-ns.cluster.local",
},
}
for i, exp := range expectations {
exp := exp // pin
t.Run(fmt.Sprintf("%d: Returns expected response", i), func(t *testing.T) {
k8sAPI, err := k8s.NewFakeAPI(exp.k8sRes...)
if err != nil {
t.Fatalf("NewFakeAPI returned an error: %s", err)
}
stream := mockTapByResourceServer{
MockServerStream: util.NewMockServerStream(),
}
s := grpc.NewServer()
mockProxyTapServer := mockProxyTapServer{
mockControllerServer: stream,
}
proxy.RegisterTapServer(s, &mockProxyTapServer)
lis, err := net.Listen("tcp", ":0")
if err != nil {
t.Fatalf("Failed to listen")
}
// TODO: mock out the underlying grpc tap events
errChan := make(chan error, 1)
go func() {
errChan <- s.Serve(lis)
}()
defer func() {
if err := <-errChan; err != nil {
t.Fatalf("Failed to serve on %+v: %s", lis, err)
}
}()
defer s.GracefulStop()
_, port, err := net.SplitHostPort(lis.Addr().String())
if err != nil {
t.Fatal(err.Error())
}
tapPort, err := strconv.ParseUint(port, 10, 32)
if err != nil {
t.Fatalf("Invalid port: %s", port)
}
fakeGrpcServer := newGRPCTapServer(uint(tapPort), "controller-ns", "cluster.local", k8sAPI)
k8sAPI.Sync(nil)
err = fakeGrpcServer.TapByResource(&exp.req, &stream)
if !reflect.DeepEqual(err, exp.err) {
t.Fatalf("TapByResource returned unexpected: [%s], expected: [%s]", err, exp.err)
}
if exp.requireID != "" {
md, ok := metadata.FromIncomingContext(mockProxyTapServer.ctx)
if !ok {
t.Fatalf("FromIncomingContext failed given: %+v", mockProxyTapServer.ctx)
}
if !reflect.DeepEqual(md.Get(requireIDHeader), []string{exp.requireID}) {
t.Fatalf("Unexpected l5d-require-id header [%+v] expected [%+v]", md.Get(requireIDHeader), []string{exp.requireID})
}
}
})
}
}
func TestHydrateIPLabels(t *testing.T) {
expectations := []struct {
k8sRes []string
requestedIP string
labels map[string]string
}{
{
// Requested IP that doesn't match node or any pod
k8sRes: []string{`
apiVersion: v1
kind: Node
metadata:
name: node1
status:
addresses:
- address: 1.2.3.4
type: InternalIP
`, `
apiVersion: v1
kind: Pod
metadata:
name: emojivoto-meshed
namespace: emojivoto
labels:
app: emoji-svc
status:
phase: Running
podIP: 5.6.7.8
`,
},
requestedIP: "10.20.30.40",
labels: map[string]string{},
},
{
// Requested IP that matches node only
k8sRes: []string{`
apiVersion: v1
kind: Node
metadata:
name: node1
status:
addresses:
- address: 1.2.3.4
type: InternalIP
`, `
apiVersion: v1
kind: Pod
metadata:
name: emojivoto-meshed
namespace: emojivoto
labels:
app: emoji-svc
status:
phase: Running
podIP: 5.6.7.8
`,
},
requestedIP: "1.2.3.4",
labels: map[string]string{"node": "node1"},
},
{
// Requested IP that matches node and pod
k8sRes: []string{`
apiVersion: v1
kind: Node
metadata:
name: node1
status:
addresses:
- address: 1.2.3.4
type: InternalIP
`, `
apiVersion: v1
kind: Pod
metadata:
name: emojivoto-meshed
namespace: emojivoto
labels:
app: emoji-svc
status:
phase: Running
podIP: 1.2.3.4
`,
},
requestedIP: "1.2.3.4",
labels: map[string]string{"node": "node1"},
},
{
// Requested IP that doesn't match node and matches exactly one pod
k8sRes: []string{`
apiVersion: v1
kind: Node
metadata:
name: node1
status:
addresses:
- address: 1.2.3.4
type: InternalIP
`, `
apiVersion: v1
kind: Pod
metadata:
name: emojivoto-meshed
namespace: emojivoto
labels:
app: emoji-svc
status:
phase: Running
podIP: 5.6.7.8
`,
},
requestedIP: "5.6.7.8",
labels: map[string]string{
"namespace": "emojivoto",
"pod": "emojivoto-meshed",
"serviceaccount": "default",
},
},
{
// Requested IP that doesn't match node and matches exactly one running pod and one finished pod
k8sRes: []string{`
apiVersion: v1
kind: Node
metadata:
name: node1
status:
addresses:
- address: 1.2.3.4
type: InternalIP
`, `
apiVersion: v1
kind: Pod
metadata:
name: emojivoto-meshed
namespace: emojivoto
labels:
app: emoji-svc
status:
phase: Running
podIP: 5.6.7.8
`, `
apiVersion: v1
kind: Pod
metadata:
name: emojivoto-meshed-2
namespace: emojivoto
labels:
app: emoji-svc
status:
phase: Finished
podIP: 5.6.7.8
`,
},
requestedIP: "5.6.7.8",
labels: map[string]string{
"namespace": "emojivoto",
"pod": "emojivoto-meshed",
"serviceaccount": "default",
},
},
{
// Requested IP that doesn't match node and matches two running pods
k8sRes: []string{`
apiVersion: v1
kind: Node
metadata:
name: node1
status:
addresses:
- address: 1.2.3.4
type: InternalIP
`, `
apiVersion: v1
kind: Pod
metadata:
name: emojivoto-meshed
namespace: emojivoto
labels:
app: emoji-svc
status:
phase: Running
podIP: 5.6.7.8
`, `
apiVersion: v1
kind: Pod
metadata:
name: emojivoto-meshed-2
namespace: emojivoto
labels:
app: emoji-svc
status:
phase: Running
podIP: 5.6.7.8
`,
},
requestedIP: "5.6.7.8",
labels: map[string]string{},
},
}
for i, exp := range expectations {
exp := exp // pin
t.Run(fmt.Sprintf("%d: Returns expected response", i), func(t *testing.T) {
k8sAPI, err := k8s.NewFakeAPI(exp.k8sRes...)
if err != nil {
t.Fatalf("NewFakeAPI returned an error: %s", err)
}
s := NewGrpcTapServer(4190, "controller-ns", "cluster.local", k8sAPI)
k8sAPI.Sync(nil)
labels := make(map[string]string)
ip, err := addr.ParsePublicIPV4(exp.requestedIP)
if err != nil {
t.Fatalf("Error parsing IP %s: %s", exp.requestedIP, err)
}
s.hydrateIPLabels(ip, labels)
if !reflect.DeepEqual(labels, exp.labels) {
t.Fatalf("Unexpected labels: [%#v], expected: [%#v]", labels, exp.labels)
}
})
}
}