Skip to content

Commit

Permalink
Enable Tap from the Web UI (#1356)
Browse files Browse the repository at this point in the history
Adds a tap endpoint in the web api that communicates with the dashboard 
via websockets.
I've moved a bunch of code from the cli tap.go into utils so that the code 
can be shared between web and CLI. I think we should consider making the 
display more suited to web, but in the short term, reusing the CLI's 
rendering of tap events works.

Adds a Tap page in the Web UI that you can use to make tap requests. 
The form currently only allows you to enter a resource and namespace, 
other filters coming in a follow-up branch.
  • Loading branch information
Risha Mars authored Jul 24, 2018
1 parent dcb9c55 commit ec3c861
Show file tree
Hide file tree
Showing 15 changed files with 501 additions and 223 deletions.
8 changes: 7 additions & 1 deletion Gopkg.lock
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,12 @@
]
revision = "104e2578924bb3b211150c19414d0144b82165bb"

[[projects]]
name = "github.com/gorilla/websocket"
packages = ["."]
revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b"
version = "v1.2.0"

[[projects]]
name = "github.com/grpc-ecosystem/go-grpc-prometheus"
packages = ["."]
Expand Down Expand Up @@ -636,6 +642,6 @@
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "54e0dd7c4513ef8ceddf3cbb7e4d20a7b868c4a850b8c5efce81065181e2b745"
inputs-digest = "51bf169451e7cef474b4402438cb4a0cf0cf43159a08fc5bdf29498c46031dc1"
solver-name = "gps-cdcl"
solver-version = 1
3 changes: 3 additions & 0 deletions Gopkg.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ required = ["github.com/golang/protobuf/protoc-gen-go"]
name = "github.com/prometheus/client_golang"
revision = "9bb6ab929dcbe1c8393cd9ef70387cb69811bd1c"

[[constraint]]
name = "github.com/gorilla/websocket"
version = "1.2.0"
#
# k8s.io/kubernetes dependency fixes
# taken from https://github.com/kubernetes/kubernetes/blob/master/Godeps/Godeps.json
Expand Down
2 changes: 1 addition & 1 deletion cli/Dockerfile-bin
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
## compile binaries
FROM gcr.io/linkerd-io/go-deps:baf323e4 as golang
FROM gcr.io/linkerd-io/go-deps:766a0983 as golang
WORKDIR /go/src/github.com/linkerd/linkerd2
COPY cli cli
COPY controller/k8s controller/k8s
Expand Down
197 changes: 15 additions & 182 deletions cli/cmd/tap.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@ import (
"fmt"
"io"
"os"
"strings"
"text/tabwriter"

"github.com/linkerd/linkerd2/controller/api/util"
pb "github.com/linkerd/linkerd2/controller/gen/public"
"github.com/linkerd/linkerd2/pkg/addr"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"google.golang.org/grpc/codes"
)

type tapOptions struct {
Expand Down Expand Up @@ -74,7 +73,19 @@ func newCmdTap() *cobra.Command {
Args: cobra.RangeArgs(1, 2),
ValidArgs: util.ValidTargets,
RunE: func(cmd *cobra.Command, args []string) error {
req, err := buildTapByResourceRequest(args, options)
requestParams := util.TapRequestParams{
Resource: strings.Join(args, "/"),
Namespace: options.namespace,
ToResource: options.toResource,
ToNamespace: options.toNamespace,
MaxRps: options.maxRps,
Scheme: options.scheme,
Method: options.method,
Authority: options.authority,
Path: options.path,
}

req, err := util.BuildTapByResourceRequest(requestParams)
if err != nil {
return err
}
Expand Down Expand Up @@ -108,103 +119,11 @@ func newCmdTap() *cobra.Command {
return cmd
}

func buildTapByResourceRequest(
resource []string,
options *tapOptions,
) (*pb.TapByResourceRequest, error) {

target, err := util.BuildResource(options.namespace, resource...)
if err != nil {
return nil, fmt.Errorf("target resource invalid: %s", err)
}
if !contains(util.ValidTargets, target.Type) {
return nil, fmt.Errorf("unsupported resource type [%s]", target.Type)
}

matches := []*pb.TapByResourceRequest_Match{}

if options.toResource != "" {
destination, err := util.BuildResource(options.toNamespace, options.toResource)
if err != nil {
return nil, fmt.Errorf("destination resource invalid: %s", err)
}
if !contains(util.ValidDestinations, destination.Type) {
return nil, fmt.Errorf("unsupported resource type [%s]", target.Type)
}

match := pb.TapByResourceRequest_Match{
Match: &pb.TapByResourceRequest_Match_Destinations{
Destinations: &pb.ResourceSelection{
Resource: &destination,
},
},
}
matches = append(matches, &match)
}

if options.scheme != "" {
match := buildMatchHTTP(&pb.TapByResourceRequest_Match_Http{
Match: &pb.TapByResourceRequest_Match_Http_Scheme{Scheme: options.scheme},
})
matches = append(matches, &match)
}
if options.method != "" {
match := buildMatchHTTP(&pb.TapByResourceRequest_Match_Http{
Match: &pb.TapByResourceRequest_Match_Http_Method{Method: options.method},
})
matches = append(matches, &match)
}
if options.authority != "" {
match := buildMatchHTTP(&pb.TapByResourceRequest_Match_Http{
Match: &pb.TapByResourceRequest_Match_Http_Authority{Authority: options.authority},
})
matches = append(matches, &match)
}
if options.path != "" {
match := buildMatchHTTP(&pb.TapByResourceRequest_Match_Http{
Match: &pb.TapByResourceRequest_Match_Http_Path{Path: options.path},
})
matches = append(matches, &match)
}

return &pb.TapByResourceRequest{
Target: &pb.ResourceSelection{
Resource: &target,
},
MaxRps: options.maxRps,
Match: &pb.TapByResourceRequest_Match{
Match: &pb.TapByResourceRequest_Match_All{
All: &pb.TapByResourceRequest_Match_Seq{
Matches: matches,
},
},
},
}, nil
}

func contains(list []string, s string) bool {
for _, elem := range list {
if s == elem {
return true
}
}
return false
}

func buildMatchHTTP(match *pb.TapByResourceRequest_Match_Http) pb.TapByResourceRequest_Match {
return pb.TapByResourceRequest_Match{
Match: &pb.TapByResourceRequest_Match_Http_{
Http: match,
},
}
}

func requestTapByResourceFromAPI(w io.Writer, client pb.ApiClient, req *pb.TapByResourceRequest) error {
rsp, err := client.TapByResource(context.Background(), req)
if err != nil {
return err
}

return renderTap(w, rsp)
}

Expand All @@ -230,97 +149,11 @@ func writeTapEventsToBuffer(tapClient pb.Api_TapByResourceClient, w *tabwriter.W
fmt.Fprintln(os.Stderr, err)
break
}
_, err = fmt.Fprintln(w, renderTapEvent(event))
_, err = fmt.Fprintln(w, util.RenderTapEvent(event))
if err != nil {
return err
}
}

return nil
}

func renderTapEvent(event *pb.TapEvent) string {
dstLabels := event.GetDestinationMeta().GetLabels()

dst := addr.PublicAddressToString(event.GetDestination())
if pod := dstLabels["pod"]; pod != "" {
dst = fmt.Sprintf("%s:%d", pod, event.GetDestination().GetPort())
}

proxy := "???"
tls := ""
switch event.GetProxyDirection() {
case pb.TapEvent_INBOUND:
proxy = "in " // A space is added so it aligns with `out`.
srcLabels := event.GetSourceMeta().GetLabels()
tls = srcLabels["tls"]
case pb.TapEvent_OUTBOUND:
proxy = "out"
tls = dstLabels["tls"]
default:
// Too old for TLS.
}

flow := fmt.Sprintf("proxy=%s src=%s dst=%s tls=%s",
proxy,
addr.PublicAddressToString(event.GetSource()),
dst,
tls,
)

switch ev := event.GetHttp().GetEvent().(type) {
case *pb.TapEvent_Http_RequestInit_:
return fmt.Sprintf("req id=%d:%d %s :method=%s :authority=%s :path=%s",
ev.RequestInit.GetId().GetBase(),
ev.RequestInit.GetId().GetStream(),
flow,
ev.RequestInit.GetMethod().GetRegistered().String(),
ev.RequestInit.GetAuthority(),
ev.RequestInit.GetPath(),
)

case *pb.TapEvent_Http_ResponseInit_:
return fmt.Sprintf("rsp id=%d:%d %s :status=%d latency=%dµs",
ev.ResponseInit.GetId().GetBase(),
ev.ResponseInit.GetId().GetStream(),
flow,
ev.ResponseInit.GetHttpStatus(),
ev.ResponseInit.GetSinceRequestInit().GetNanos()/1000,
)

case *pb.TapEvent_Http_ResponseEnd_:
switch eos := ev.ResponseEnd.GetEos().GetEnd().(type) {
case *pb.Eos_GrpcStatusCode:
return fmt.Sprintf("end id=%d:%d %s grpc-status=%s duration=%dµs response-length=%dB",
ev.ResponseEnd.GetId().GetBase(),
ev.ResponseEnd.GetId().GetStream(),
flow,
codes.Code(eos.GrpcStatusCode),
ev.ResponseEnd.GetSinceResponseInit().GetNanos()/1000,
ev.ResponseEnd.GetResponseBytes(),
)

case *pb.Eos_ResetErrorCode:
return fmt.Sprintf("end id=%d:%d %s reset-error=%+v duration=%dµs response-length=%dB",
ev.ResponseEnd.GetId().GetBase(),
ev.ResponseEnd.GetId().GetStream(),
flow,
eos.ResetErrorCode,
ev.ResponseEnd.GetSinceResponseInit().GetNanos()/1000,
ev.ResponseEnd.GetResponseBytes(),
)

default:
return fmt.Sprintf("end id=%d:%d %s duration=%dµs response-length=%dB",
ev.ResponseEnd.GetId().GetBase(),
ev.ResponseEnd.GetId().GetStream(),
flow,
ev.ResponseEnd.GetSinceResponseInit().GetNanos()/1000,
ev.ResponseEnd.GetResponseBytes(),
)
}

default:
return fmt.Sprintf("unknown %s", flow)
}
}
Loading

0 comments on commit ec3c861

Please sign in to comment.