mirror of https://github.com/linkerd/linkerd2.git
387 lines
11 KiB
Go
387 lines
11 KiB
Go
package cmd
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
"text/tabwriter"
|
|
|
|
"github.com/linkerd/linkerd2/controller/api/util"
|
|
pb "github.com/linkerd/linkerd2/controller/gen/public"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
type edgesOptions struct {
|
|
namespace string
|
|
outputFormat string
|
|
allNamespaces bool
|
|
}
|
|
|
|
func newEdgesOptions() *edgesOptions {
|
|
return &edgesOptions{
|
|
namespace: "",
|
|
outputFormat: tableOutput,
|
|
allNamespaces: false,
|
|
}
|
|
}
|
|
|
|
type indexedEdgeResults struct {
|
|
ix int
|
|
rows []*pb.Edge
|
|
err error
|
|
}
|
|
|
|
func newCmdEdges() *cobra.Command {
|
|
options := newEdgesOptions()
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "edges [flags] (RESOURCETYPE)",
|
|
Short: "Display connections between resources, and Linkerd proxy identities",
|
|
Long: `Display connections between resources, and Linkerd proxy identities.
|
|
|
|
The RESOURCETYPE argument specifies the type of resource to display edges within.
|
|
|
|
Examples:
|
|
* deploy
|
|
* ds
|
|
* job
|
|
* po
|
|
* rc
|
|
* sts
|
|
|
|
Valid resource types include:
|
|
* daemonsets
|
|
* deployments
|
|
* jobs
|
|
* pods
|
|
* replicationcontrollers
|
|
* statefulsets`,
|
|
Example: ` # Get all edges between pods that either originate from or terminate in the demo namespace.
|
|
linkerd edges po -n test
|
|
|
|
# Get all edges between pods that either originate from or terminate in the default namespace.
|
|
linkerd edges po
|
|
|
|
# Get all edges between pods in all namespaces.
|
|
linkerd edges po --all-namespaces`,
|
|
Args: cobra.ExactArgs(1),
|
|
ValidArgs: util.ValidTargets,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
reqs, err := buildEdgesRequests(args, options)
|
|
if err != nil {
|
|
return fmt.Errorf("Error creating edges request: %s", err)
|
|
}
|
|
|
|
// The gRPC client is concurrency-safe, so we can reuse it in all the following goroutines
|
|
// https://github.com/grpc/grpc-go/issues/682
|
|
client := checkPublicAPIClientOrExit()
|
|
c := make(chan indexedEdgeResults, len(reqs))
|
|
for num, req := range reqs {
|
|
go func(num int, req *pb.EdgesRequest) {
|
|
resp, err := requestEdgesFromAPI(client, req)
|
|
rows := edgesRespToRows(resp)
|
|
c <- indexedEdgeResults{num, rows, err}
|
|
}(num, req)
|
|
}
|
|
|
|
totalRows := make([]*pb.Edge, 0)
|
|
i := 0
|
|
for res := range c {
|
|
if res.err != nil {
|
|
return res.err
|
|
}
|
|
totalRows = append(totalRows, res.rows...)
|
|
if i++; i == len(reqs) {
|
|
close(c)
|
|
}
|
|
}
|
|
|
|
output := renderEdgeStats(totalRows, options)
|
|
_, err = fmt.Print(output)
|
|
|
|
return err
|
|
},
|
|
}
|
|
|
|
cmd.PersistentFlags().StringVarP(&options.namespace, "namespace", "n", options.namespace, "Namespace of the specified resource")
|
|
cmd.PersistentFlags().StringVarP(&options.outputFormat, "output", "o", options.outputFormat, "Output format; one of: \"table\" or \"json\" or \"wide\"")
|
|
cmd.PersistentFlags().BoolVarP(&options.allNamespaces, "all-namespaces", "A", options.allNamespaces, "If present, returns edges across all namespaces, ignoring the \"--namespace\" flag")
|
|
return cmd
|
|
}
|
|
|
|
// validateEdgesRequestInputs ensures that the resource type and output format are both supported
|
|
// by the edges command, since the edges command does not support all k8s resource types.
|
|
func validateEdgesRequestInputs(targets []pb.Resource, options *edgesOptions) error {
|
|
for _, target := range targets {
|
|
if target.Name != "" {
|
|
return fmt.Errorf("Edges cannot be returned for a specific resource name; remove %s from query", target.Name)
|
|
}
|
|
switch target.Type {
|
|
case "authority", "service", "all":
|
|
return fmt.Errorf("Resource type is not supported: %s", target.Type)
|
|
}
|
|
}
|
|
|
|
switch options.outputFormat {
|
|
case tableOutput, jsonOutput, wideOutput:
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("--output supports %s, %s and %s", tableOutput, jsonOutput, wideOutput)
|
|
}
|
|
}
|
|
|
|
func buildEdgesRequests(resources []string, options *edgesOptions) ([]*pb.EdgesRequest, error) {
|
|
targets, err := util.BuildResources(options.namespace, resources)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = validateEdgesRequestInputs(targets, options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
requests := make([]*pb.EdgesRequest, 0)
|
|
for _, target := range targets {
|
|
requestParams := util.EdgesRequestParams{
|
|
ResourceType: target.Type,
|
|
Namespace: options.namespace,
|
|
AllNamespaces: options.allNamespaces,
|
|
}
|
|
|
|
req, err := util.BuildEdgesRequest(requestParams)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
requests = append(requests, req)
|
|
}
|
|
return requests, nil
|
|
}
|
|
|
|
func edgesRespToRows(resp *pb.EdgesResponse) []*pb.Edge {
|
|
rows := make([]*pb.Edge, 0)
|
|
if resp != nil {
|
|
rows = append(rows, resp.GetOk().Edges...)
|
|
}
|
|
return rows
|
|
}
|
|
|
|
func requestEdgesFromAPI(client pb.ApiClient, req *pb.EdgesRequest) (*pb.EdgesResponse, error) {
|
|
resp, err := client.Edges(context.Background(), req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Edges API error: %+v", err)
|
|
}
|
|
if e := resp.GetError(); e != nil {
|
|
return nil, fmt.Errorf("Edges API response error: %+v", e.Error)
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
func renderEdgeStats(rows []*pb.Edge, options *edgesOptions) string {
|
|
var buffer bytes.Buffer
|
|
w := tabwriter.NewWriter(&buffer, 0, 0, padding, ' ', tabwriter.AlignRight)
|
|
writeEdgesToBuffer(rows, w, options)
|
|
w.Flush()
|
|
|
|
return renderEdges(buffer, options)
|
|
}
|
|
|
|
type edgeRow struct {
|
|
src string
|
|
srcNamespace string
|
|
dst string
|
|
dstNamespace string
|
|
client string
|
|
server string
|
|
msg string
|
|
}
|
|
|
|
const (
|
|
srcHeader = "SRC"
|
|
dstHeader = "DST"
|
|
srcNamespaceHeader = "SRC_NS"
|
|
dstNamespaceHeader = "DST_NS"
|
|
clientHeader = "CLIENT_ID"
|
|
serverHeader = "SERVER_ID"
|
|
msgHeader = "SECURED"
|
|
)
|
|
|
|
func writeEdgesToBuffer(rows []*pb.Edge, w *tabwriter.Writer, options *edgesOptions) {
|
|
maxSrcLength := len(srcHeader)
|
|
maxDstLength := len(dstHeader)
|
|
maxSrcNamespaceLength := len(srcNamespaceHeader)
|
|
maxDstNamespaceLength := len(dstNamespaceHeader)
|
|
maxClientLength := len(clientHeader)
|
|
maxServerLength := len(serverHeader)
|
|
maxMsgLength := len(msgHeader)
|
|
|
|
edgeRows := []edgeRow{}
|
|
if len(rows) != 0 {
|
|
for _, r := range rows {
|
|
clientID := r.ClientId
|
|
serverID := r.ServerId
|
|
msg := r.NoIdentityMsg
|
|
if len(msg) == 0 && options.outputFormat != jsonOutput {
|
|
msg = okStatus
|
|
}
|
|
if len(clientID) > 0 {
|
|
parts := strings.Split(clientID, ".")
|
|
clientID = parts[0] + "." + parts[1]
|
|
}
|
|
if len(serverID) > 0 {
|
|
parts := strings.Split(serverID, ".")
|
|
serverID = parts[0] + "." + parts[1]
|
|
}
|
|
|
|
row := edgeRow{
|
|
client: clientID,
|
|
server: serverID,
|
|
msg: msg,
|
|
src: r.Src.Name,
|
|
srcNamespace: r.Src.Namespace,
|
|
dst: r.Dst.Name,
|
|
dstNamespace: r.Dst.Namespace,
|
|
}
|
|
|
|
edgeRows = append(edgeRows, row)
|
|
|
|
if len(r.Src.Name) > maxSrcLength {
|
|
maxSrcLength = len(r.Src.Name)
|
|
}
|
|
if len(r.Src.Namespace) > maxSrcNamespaceLength {
|
|
maxSrcNamespaceLength = len(r.Src.Namespace)
|
|
}
|
|
if len(r.Dst.Name) > maxDstLength {
|
|
maxDstLength = len(r.Dst.Name)
|
|
}
|
|
if len(r.Dst.Namespace) > maxDstNamespaceLength {
|
|
maxDstNamespaceLength = len(r.Dst.Namespace)
|
|
}
|
|
if len(clientID) > maxClientLength {
|
|
maxClientLength = len(clientID)
|
|
}
|
|
if len(serverID) > maxServerLength {
|
|
maxServerLength = len(serverID)
|
|
}
|
|
if len(msg) > maxMsgLength {
|
|
maxMsgLength = len(msg)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ordering the rows first by SRC/DST namespace, then by SRC/DST resource
|
|
sort.Slice(edgeRows, func(i, j int) bool {
|
|
keyI := edgeRows[i].srcNamespace + edgeRows[i].dstNamespace + edgeRows[i].src + edgeRows[i].dst
|
|
keyJ := edgeRows[j].srcNamespace + edgeRows[j].dstNamespace + edgeRows[j].src + edgeRows[j].dst
|
|
return keyI < keyJ
|
|
})
|
|
|
|
switch options.outputFormat {
|
|
case tableOutput, wideOutput:
|
|
if len(edgeRows) == 0 {
|
|
fmt.Fprintln(os.Stderr, "No edges found.")
|
|
os.Exit(0)
|
|
}
|
|
printEdgeTable(edgeRows, w, maxSrcLength, maxSrcNamespaceLength, maxDstLength, maxDstNamespaceLength, maxClientLength, maxServerLength, maxMsgLength, options.outputFormat)
|
|
case jsonOutput:
|
|
printEdgesJSON(edgeRows, w)
|
|
}
|
|
}
|
|
|
|
func printEdgeTable(edgeRows []edgeRow, w *tabwriter.Writer, maxSrcLength, maxSrcNamespaceLength, maxDstLength, maxDstNamespaceLength, maxClientLength, maxServerLength, maxMsgLength int, outputFormat string) {
|
|
srcTemplate := fmt.Sprintf("%%-%ds", maxSrcLength)
|
|
dstTemplate := fmt.Sprintf("%%-%ds", maxDstLength)
|
|
srcNamespaceTemplate := fmt.Sprintf("%%-%ds", maxSrcNamespaceLength)
|
|
dstNamespaceTemplate := fmt.Sprintf("%%-%ds", maxDstNamespaceLength)
|
|
msgTemplate := fmt.Sprintf("%%-%ds", maxMsgLength)
|
|
clientTemplate := fmt.Sprintf("%%-%ds", maxClientLength)
|
|
serverTemplate := fmt.Sprintf("%%-%ds", maxServerLength)
|
|
|
|
headers := []string{
|
|
fmt.Sprintf(srcTemplate, srcHeader),
|
|
fmt.Sprintf(dstTemplate, dstHeader),
|
|
fmt.Sprintf(srcNamespaceTemplate, srcNamespaceHeader),
|
|
fmt.Sprintf(dstNamespaceTemplate, dstNamespaceHeader),
|
|
}
|
|
|
|
if outputFormat == wideOutput {
|
|
headers = append(headers, fmt.Sprintf(clientTemplate, clientHeader), fmt.Sprintf(serverTemplate, serverHeader))
|
|
}
|
|
|
|
headers = append(headers, fmt.Sprintf(msgTemplate, msgHeader)+"\t")
|
|
|
|
fmt.Fprintln(w, strings.Join(headers, "\t"))
|
|
|
|
for _, row := range edgeRows {
|
|
values := []interface{}{
|
|
row.src,
|
|
row.dst,
|
|
row.srcNamespace,
|
|
row.dstNamespace,
|
|
}
|
|
templateString := fmt.Sprintf("%s\t%s\t%s\t%s\t", srcTemplate, dstTemplate, srcNamespaceTemplate, dstNamespaceTemplate)
|
|
|
|
if outputFormat == wideOutput {
|
|
templateString += fmt.Sprintf("%s\t%s\t", clientTemplate, serverTemplate)
|
|
values = append(values, row.client, row.server)
|
|
}
|
|
|
|
templateString += fmt.Sprintf("%s\t\n", msgTemplate)
|
|
values = append(values, row.msg)
|
|
|
|
fmt.Fprintf(w, templateString, values...)
|
|
}
|
|
}
|
|
|
|
func renderEdges(buffer bytes.Buffer, options *edgesOptions) string {
|
|
var out string
|
|
switch options.outputFormat {
|
|
case jsonOutput:
|
|
out = buffer.String()
|
|
default:
|
|
// strip left padding on the first column
|
|
out = string(buffer.Bytes()[padding:])
|
|
out = strings.Replace(out, "\n"+strings.Repeat(" ", padding), "\n", -1)
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
type edgesJSONStats struct {
|
|
Src string `json:"src"`
|
|
SrcNamespace string `json:"src_namespace"`
|
|
Dst string `json:"dst"`
|
|
DstNamespace string `json:"dst_namespace"`
|
|
Client string `json:"client_id"`
|
|
Server string `json:"server_id"`
|
|
Msg string `json:"no_tls_reason"`
|
|
}
|
|
|
|
func printEdgesJSON(edgeRows []edgeRow, w *tabwriter.Writer) {
|
|
// avoid nil initialization so that if there are not stats it gets marshalled as an empty array vs null
|
|
entries := []*edgesJSONStats{}
|
|
|
|
for _, row := range edgeRows {
|
|
entry := &edgesJSONStats{
|
|
Src: row.src,
|
|
SrcNamespace: row.srcNamespace,
|
|
Dst: row.dst,
|
|
DstNamespace: row.dstNamespace,
|
|
Client: row.client,
|
|
Server: row.server,
|
|
Msg: row.msg}
|
|
entries = append(entries, entry)
|
|
}
|
|
|
|
b, err := json.MarshalIndent(entries, "", " ")
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error marshalling JSON: %s\n", err)
|
|
return
|
|
}
|
|
fmt.Fprintf(w, "%s\n", b)
|
|
}
|