mirror of https://github.com/linkerd/linkerd2.git
626 lines
13 KiB
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)
|
|
}
|
|
})
|
|
}
|
|
}
|