mirror of https://github.com/linkerd/linkerd2.git
378 lines
9.0 KiB
Go
378 lines
9.0 KiB
Go
package tap
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"strings"
|
|
"time"
|
|
|
|
common "github.com/runconduit/conduit/controller/gen/common"
|
|
pb "github.com/runconduit/conduit/controller/gen/controller/tap"
|
|
proxy "github.com/runconduit/conduit/controller/gen/proxy/tap"
|
|
public "github.com/runconduit/conduit/controller/gen/public"
|
|
"github.com/runconduit/conduit/controller/k8s"
|
|
"github.com/runconduit/conduit/controller/util"
|
|
log "github.com/sirupsen/logrus"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
"k8s.io/api/core/v1"
|
|
)
|
|
|
|
var tapInterval = 10 * time.Second
|
|
|
|
type (
|
|
server struct {
|
|
tapPort uint
|
|
// We use the Kubernetes API to find the IP addresses of pods to tap
|
|
replicaSets *k8s.ReplicaSetStore
|
|
pods k8s.PodIndex
|
|
}
|
|
)
|
|
|
|
func (s *server) Tap(req *public.TapRequest, stream pb.Tap_TapServer) error {
|
|
|
|
// TODO: Allow a configurable aperture A.
|
|
// If the target contains more than A pods, select A of them at random.
|
|
var pods []*v1.Pod
|
|
var targetName string
|
|
switch target := req.Target.(type) {
|
|
case *public.TapRequest_Pod:
|
|
targetName = target.Pod
|
|
pod, err := s.pods.GetPod(target.Pod)
|
|
if err != nil {
|
|
return status.Errorf(codes.NotFound, err.Error())
|
|
}
|
|
pods = []*v1.Pod{pod}
|
|
case *public.TapRequest_Deployment:
|
|
targetName = target.Deployment
|
|
var err error
|
|
pods, err = s.pods.GetPodsByIndex(target.Deployment)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
log.Printf("Tapping %d pods for target %s", len(pods), targetName)
|
|
|
|
events := make(chan *common.TapEvent)
|
|
|
|
go func() { // Stop sending back events if the request is cancelled
|
|
<-stream.Context().Done()
|
|
close(events)
|
|
}()
|
|
|
|
// divide the rps evenly between all pods to tap
|
|
rpsPerPod := req.MaxRps / float32(len(pods))
|
|
if rpsPerPod < 1 {
|
|
rpsPerPod = 1
|
|
}
|
|
|
|
for _, pod := range pods {
|
|
// initiate a tap on the pod
|
|
match, err := makeMatch(req)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
go s.tapProxy(stream.Context(), rpsPerPod, match, pod.Status.PodIP, events)
|
|
}
|
|
|
|
// read events from the taps and send them back
|
|
for event := range events {
|
|
err := stream.Send(event)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validatePort(port uint32) error {
|
|
if port > 65535 {
|
|
return fmt.Errorf("Port number of range: %d", port)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func makeMatch(req *public.TapRequest) (*proxy.ObserveRequest_Match, error) {
|
|
matches := make([]*proxy.ObserveRequest_Match, 0)
|
|
if req.FromIP != "" {
|
|
ip, err := util.ParseIPV4(req.FromIP)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
matches = append(matches, &proxy.ObserveRequest_Match{
|
|
Match: &proxy.ObserveRequest_Match_Source{
|
|
Source: &proxy.ObserveRequest_Match_Tcp{
|
|
Match: &proxy.ObserveRequest_Match_Tcp_Netmask_{
|
|
Netmask: &proxy.ObserveRequest_Match_Tcp_Netmask{
|
|
Ip: ip,
|
|
Mask: 32,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
if req.FromPort != 0 {
|
|
if err := validatePort(req.FromPort); err != nil {
|
|
return nil, err
|
|
}
|
|
matches = append(matches, &proxy.ObserveRequest_Match{
|
|
Match: &proxy.ObserveRequest_Match_Source{
|
|
Source: &proxy.ObserveRequest_Match_Tcp{
|
|
Match: &proxy.ObserveRequest_Match_Tcp_Ports{
|
|
Ports: &proxy.ObserveRequest_Match_Tcp_PortRange{
|
|
Min: req.FromPort,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
if req.ToIP != "" {
|
|
ip, err := util.ParseIPV4(req.ToIP)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
matches = append(matches, &proxy.ObserveRequest_Match{
|
|
Match: &proxy.ObserveRequest_Match_Destination{
|
|
Destination: &proxy.ObserveRequest_Match_Tcp{
|
|
Match: &proxy.ObserveRequest_Match_Tcp_Netmask_{
|
|
Netmask: &proxy.ObserveRequest_Match_Tcp_Netmask{
|
|
Ip: ip,
|
|
Mask: 32,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
if req.ToPort != 0 {
|
|
if err := validatePort(req.ToPort); err != nil {
|
|
return nil, err
|
|
}
|
|
matches = append(matches, &proxy.ObserveRequest_Match{
|
|
Match: &proxy.ObserveRequest_Match_Destination{
|
|
Destination: &proxy.ObserveRequest_Match_Tcp{
|
|
Match: &proxy.ObserveRequest_Match_Tcp_Ports{
|
|
Ports: &proxy.ObserveRequest_Match_Tcp_PortRange{
|
|
Min: req.ToPort,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
if req.Scheme != "" {
|
|
matches = append(matches, &proxy.ObserveRequest_Match{
|
|
Match: &proxy.ObserveRequest_Match_Http_{
|
|
Http: &proxy.ObserveRequest_Match_Http{
|
|
Match: &proxy.ObserveRequest_Match_Http_Scheme{
|
|
Scheme: parseScheme(req.Scheme),
|
|
},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
if req.Method != "" {
|
|
matches = append(matches, &proxy.ObserveRequest_Match{
|
|
Match: &proxy.ObserveRequest_Match_Http_{
|
|
Http: &proxy.ObserveRequest_Match_Http{
|
|
Match: &proxy.ObserveRequest_Match_Http_Method{
|
|
Method: parseMethod(req.Method),
|
|
},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
// exact match
|
|
if req.Authority != "" {
|
|
matches = append(matches, &proxy.ObserveRequest_Match{
|
|
Match: &proxy.ObserveRequest_Match_Http_{
|
|
Http: &proxy.ObserveRequest_Match_Http{
|
|
Match: &proxy.ObserveRequest_Match_Http_Authority{
|
|
Authority: &proxy.ObserveRequest_Match_Http_StringMatch{
|
|
Match: &proxy.ObserveRequest_Match_Http_StringMatch_Exact{
|
|
Exact: req.Authority,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
// prefix match
|
|
if req.Path != "" {
|
|
matches = append(matches, &proxy.ObserveRequest_Match{
|
|
Match: &proxy.ObserveRequest_Match_Http_{
|
|
Http: &proxy.ObserveRequest_Match_Http{
|
|
Match: &proxy.ObserveRequest_Match_Http_Path{
|
|
Path: &proxy.ObserveRequest_Match_Http_StringMatch{
|
|
Match: &proxy.ObserveRequest_Match_Http_StringMatch_Prefix{
|
|
Prefix: req.Path,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
return &proxy.ObserveRequest_Match{
|
|
Match: &proxy.ObserveRequest_Match_All{
|
|
All: &proxy.ObserveRequest_Match_Seq{
|
|
Matches: matches,
|
|
},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// TODO: validate scheme
|
|
func parseScheme(scheme string) *common.Scheme {
|
|
value, ok := common.Scheme_Registered_value[strings.ToUpper(scheme)]
|
|
if ok {
|
|
return &common.Scheme{
|
|
Type: &common.Scheme_Registered_{
|
|
Registered: common.Scheme_Registered(value),
|
|
},
|
|
}
|
|
}
|
|
return &common.Scheme{
|
|
Type: &common.Scheme_Unregistered{
|
|
Unregistered: strings.ToUpper(scheme),
|
|
},
|
|
}
|
|
}
|
|
|
|
// TODO: validate method
|
|
func parseMethod(method string) *common.HttpMethod {
|
|
value, ok := common.HttpMethod_Registered_value[strings.ToUpper(method)]
|
|
if ok {
|
|
return &common.HttpMethod{
|
|
Type: &common.HttpMethod_Registered_{
|
|
Registered: common.HttpMethod_Registered(value),
|
|
},
|
|
}
|
|
}
|
|
return &common.HttpMethod{
|
|
Type: &common.HttpMethod_Unregistered{
|
|
Unregistered: strings.ToUpper(method),
|
|
},
|
|
}
|
|
}
|
|
|
|
// Tap a pod.
|
|
// This method will run continuously until an error is encountered or the
|
|
// request is cancelled via the context. Thus it should be called as a
|
|
// go-routine.
|
|
// To limit the rps to maxRps, this method calls Observe on the pod with a limit
|
|
// of maxRps * 10s at most once per 10s window. If this limit is reached in
|
|
// less than 10s, we sleep until the end of the window before calling Observe
|
|
// again.
|
|
func (s *server) tapProxy(ctx context.Context, maxRps float32, match *proxy.ObserveRequest_Match, addr string, events chan *common.TapEvent) {
|
|
tapAddr := fmt.Sprintf("%s:%d", addr, s.tapPort)
|
|
log.Printf("Establishing tap on %s", tapAddr)
|
|
conn, err := grpc.DialContext(ctx, tapAddr, grpc.WithInsecure())
|
|
if err != nil {
|
|
log.Println(err)
|
|
return
|
|
}
|
|
client := proxy.NewTapClient(conn)
|
|
|
|
req := &proxy.ObserveRequest{
|
|
Limit: uint32(maxRps * float32(tapInterval.Seconds())),
|
|
Match: match,
|
|
}
|
|
|
|
for { // Request loop
|
|
windowStart := time.Now()
|
|
windowEnd := windowStart.Add(tapInterval)
|
|
rsp, err := client.Observe(ctx, req)
|
|
if err != nil {
|
|
log.Println(err)
|
|
return
|
|
}
|
|
for { // Stream loop
|
|
event, err := rsp.Recv()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
log.Println(err)
|
|
return
|
|
}
|
|
events <- event
|
|
}
|
|
if time.Now().Before(windowEnd) {
|
|
time.Sleep(time.Until(windowEnd))
|
|
}
|
|
}
|
|
}
|
|
|
|
func NewServer(addr string, tapPort uint, kubeconfig string) (*grpc.Server, net.Listener, error) {
|
|
|
|
clientSet, err := k8s.NewClientSet(kubeconfig)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
replicaSets, err := k8s.NewReplicaSetStore(clientSet)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
err = replicaSets.Run()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// index pods by deployment
|
|
deploymentIndex := func(obj interface{}) ([]string, error) {
|
|
pod, ok := obj.(*v1.Pod)
|
|
if !ok {
|
|
return nil, fmt.Errorf("object is not a Pod")
|
|
}
|
|
deployment, err := replicaSets.GetDeploymentForPod(pod)
|
|
if err != nil {
|
|
log.Debugf("Cannot get deployment for pod %s: %s", pod.Name, err)
|
|
return []string{}, nil
|
|
}
|
|
return []string{deployment}, nil
|
|
}
|
|
|
|
pods, err := k8s.NewPodIndex(clientSet, deploymentIndex)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
err = pods.Run()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
lis, err := net.Listen("tcp", addr)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
s := util.NewGrpcServer()
|
|
srv := server{
|
|
tapPort: tapPort,
|
|
replicaSets: replicaSets,
|
|
pods: pods,
|
|
}
|
|
pb.RegisterTapServer(s, &srv)
|
|
|
|
// TODO: register shutdown hook to call pods.Stop() and replicatSets.Stop()
|
|
|
|
return s, lis, nil
|
|
}
|