Expand and improve edges API endpoint (#3007)

Updates functionality of `linkerd edges`, including a new `--all-namespaces`
flag and returning namespace information for SRC and DST resources.
This commit is contained in:
Carol A. Scott 2019-06-28 15:46:04 -07:00 committed by GitHub
parent f90a3c09ed
commit a504e8c2d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 109 additions and 48 deletions

View File

@ -16,14 +16,16 @@ import (
)
type edgesOptions struct {
namespace string
outputFormat string
namespace string
outputFormat string
allNamespaces bool
}
func newEdgesOptions() *edgesOptions {
return &edgesOptions{
namespace: "",
outputFormat: tableOutput,
namespace: "",
outputFormat: tableOutput,
allNamespaces: false,
}
}
@ -41,7 +43,7 @@ func newCmdEdges() *cobra.Command {
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. A namespace must be specified.
The RESOURCETYPE argument specifies the type of resource to display edges within.
Examples:
* deploy
@ -58,8 +60,14 @@ func newCmdEdges() *cobra.Command {
* pods
* replicationcontrollers
* statefulsets`,
Example: ` # Get all edges between pods in the test namespace.
linkerd edges po -n test`,
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 {
@ -101,6 +109,7 @@ func newCmdEdges() *cobra.Command {
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\"")
cmd.PersistentFlags().BoolVar(&options.allNamespaces, "all-namespaces", options.allNamespaces, "If present, returns edges across all namespaces, ignoring the \"--namespace\" flag")
return cmd
}
@ -139,8 +148,9 @@ func buildEdgesRequests(resources []string, options *edgesOptions) ([]*pb.EdgesR
requests := make([]*pb.EdgesRequest, 0)
for _, target := range targets {
requestParams := util.EdgesRequestParams{
ResourceType: target.Type,
Namespace: options.namespace,
ResourceType: target.Type,
Namespace: options.namespace,
AllNamespaces: options.allNamespaces,
}
req, err := util.BuildEdgesRequest(requestParams)

View File

@ -11,8 +11,8 @@ import (
)
const (
inboundIdentityQuery = "count(response_total%s) by (%s, client_id)"
outboundIdentityQuery = "count(response_total%s) by (%s, dst_%s, server_id, no_tls_reason)"
inboundIdentityQuery = "count(response_total%s) by (%s, client_id, namespace, no_tls_reason)"
outboundIdentityQuery = "count(response_total%s) by (%s, dst_%s, server_id, namespace, dst_namespace, no_tls_reason)"
)
var formatMsg = map[string]string{
@ -60,10 +60,10 @@ func (s *grpcServer) getEdges(ctx context.Context, req *pb.EdgesRequest) ([]*pb.
if len(labelNames) != 2 {
return nil, errors.New("unexpected resource selector")
}
selectedNamespace := req.Selector.Resource.Namespace
resourceType := string(labelNames[1]) // skipping first name which is always namespace
labels := promQueryLabels(req.Selector.Resource)
labelsOutbound := labels.Merge(promDirectionLabels("outbound"))
labelsInbound := labels.Merge(promDirectionLabels("inbound"))
labelsOutbound := promDirectionLabels("outbound")
labelsInbound := promDirectionLabels("inbound")
// checking that data for the specified resource type exists
labelsOutboundStr := generateLabelStringWithExclusion(labelsOutbound, resourceType)
@ -82,11 +82,11 @@ func (s *grpcServer) getEdges(ctx context.Context, req *pb.EdgesRequest) ([]*pb.
return nil, err
}
edge := processEdgeMetrics(inboundResult, outboundResult, resourceType)
edge := processEdgeMetrics(inboundResult, outboundResult, resourceType, selectedNamespace)
return edge, nil
}
func processEdgeMetrics(inbound, outbound model.Vector, resourceType string) []*pb.Edge {
func processEdgeMetrics(inbound, outbound model.Vector, resourceType, selectedNamespace string) []*pb.Edge {
edges := []*pb.Edge{}
dstIndex := map[model.LabelValue]model.Metric{}
srcIndex := map[model.LabelValue][]model.Metric{}
@ -94,10 +94,8 @@ func processEdgeMetrics(inbound, outbound model.Vector, resourceType string) []*
resourceReplacementOutbound := "dst_" + resourceType
for _, sample := range inbound {
// skip any inbound results that do not have a client_id, because this means
// the communication was one-sided (i.e. a probe or another instance where
// the src/dst are not both known) in future the edges command will support
// one-sided edges
// skip inbound results without a clientID because we cannot construct edge
// information
if _, ok := sample.Metric[model.LabelName("client_id")]; ok {
key := sample.Metric[model.LabelName(resourceReplacementInbound)]
dstIndex[key] = sample.Metric
@ -105,40 +103,78 @@ func processEdgeMetrics(inbound, outbound model.Vector, resourceType string) []*
}
for _, sample := range outbound {
// skip any outbound results that do not have a server_id for same reason as
// above section
if _, ok := sample.Metric[model.LabelName("server_id")]; ok {
key := sample.Metric[model.LabelName(resourceReplacementOutbound)]
if _, ok := srcIndex[key]; !ok {
srcIndex[key] = []model.Metric{}
}
srcIndex[key] = append(srcIndex[key], sample.Metric)
key := sample.Metric[model.LabelName(resourceReplacementOutbound)]
if _, ok := srcIndex[key]; !ok {
srcIndex[key] = []model.Metric{}
}
srcIndex[key] = append(srcIndex[key], sample.Metric)
}
for key, sources := range srcIndex {
for _, src := range sources {
srcNamespace := string(src[model.LabelName("namespace")])
dst, ok := dstIndex[key]
// if no destination, build edge entirely from source data
if !ok {
log.Errorf("missing resource in destination metrics: %s", key)
dstNamespace := string(src[model.LabelName("dst_namespace")])
// skip if selected namespace is given and neither the source nor
// destination is in the selected namespace
if selectedNamespace != "" && srcNamespace != selectedNamespace &&
dstNamespace != selectedNamespace {
continue
}
srcResource := string(src[model.LabelName(resourceType)])
dstResource := string(src[model.LabelName(resourceReplacementOutbound)])
// skip if source or destination resource is not present
if srcResource == "" || dstResource == "" {
continue
}
msg := formatMsg[string(src[model.LabelName("no_tls_reason")])]
edge := &pb.Edge{
Src: &pb.Resource{
Namespace: srcNamespace,
Name: srcResource,
Type: resourceType,
},
Dst: &pb.Resource{
Namespace: dstNamespace,
Name: dstResource,
Type: resourceType,
},
NoIdentityMsg: msg,
}
edges = append(edges, edge)
continue
}
msg := ""
if val, ok := src[model.LabelName("no_tls_reason")]; ok {
msg = formatMsg[string(val)]
dstNamespace := string(dst[model.LabelName("namespace")])
// skip if selected namespace is given and neither the source nor
// destination is in the selected namespace
if selectedNamespace != "" && srcNamespace != selectedNamespace &&
dstNamespace != selectedNamespace {
continue
}
edge := &pb.Edge{
Src: &pb.Resource{
Name: string(src[model.LabelName(resourceType)]),
Type: resourceType,
Namespace: srcNamespace,
Name: string(src[model.LabelName(resourceType)]),
Type: resourceType,
},
Dst: &pb.Resource{
Name: string(dst[model.LabelName(resourceType)]),
Type: resourceType,
Namespace: dstNamespace,
Name: string(dst[model.LabelName(resourceType)]),
Type: resourceType,
},
ClientId: string(dst[model.LabelName("client_id")]),
ServerId: string(src[model.LabelName("server_id")]),
NoIdentityMsg: msg,
ClientId: string(dst[model.LabelName("client_id")]),
ServerId: string(src[model.LabelName("server_id")]),
}
edges = append(edges, edge)
}

View File

@ -78,8 +78,9 @@ type StatsSummaryRequestParams struct {
// EdgesRequestParams contains parameters that are used to build
// Edges requests.
type EdgesRequestParams struct {
Namespace string
ResourceType string
Namespace string
ResourceType string
AllNamespaces bool
}
// TopRoutesRequestParams contains parameters that are used to build TopRoutes
@ -230,12 +231,10 @@ func BuildStatSummaryRequest(p StatsSummaryRequestParams) (*pb.StatSummaryReques
// BuildEdgesRequest builds a Public API EdgesRequest from a
// EdgesRequestParams.
func BuildEdgesRequest(p EdgesRequestParams) (*pb.EdgesRequest, error) {
namespace := p.Namespace
if p.Namespace == "" {
if namespace == "" && !p.AllNamespaces {
namespace = corev1.NamespaceDefault
}
resourceType, err := k8s.CanonicalResourceNameFromFriendlyName(p.ResourceType)
if err != nil {
return nil, err

View File

@ -24,6 +24,18 @@ const edgesColumnDefinitions = (PrefixedLink, namespace, type, classes) => {
dataIndex: "direction",
render: d => directionColumn(d.direction),
},
{
title: "Namespace",
dataIndex: "namespace",
isNumeric: false,
filter: d => d.namespace,
render: d => (
<PrefixedLink to={`/namespaces/${d.namespace}`}>
{d.namespace}
</PrefixedLink>
),
sorter: d => d.namespace
},
{
title: "Name",
dataIndex: "name",
@ -48,7 +60,7 @@ const edgesColumnDefinitions = (PrefixedLink, namespace, type, classes) => {
dataIndex: "identity",
isNumeric: false,
filter: d => d.identity,
render: d => d.identity ? `${d.identity.split('.')[0]}.${d.identity.split('.')[1]}` : null,
render: d => d.identity !== "" ? `${d.identity.split('.')[0]}.${d.identity.split('.')[1]}` : null,
sorter: d => d.identity
},
{
@ -74,8 +86,10 @@ const generateEdgesTableTitle = edges => {
let title = "Edges";
if (edges.length > 0) {
let identity = edges[0].direction === "INBOUND" ? edges[0].serverId : edges[0].clientId;
identity = identity.split('.')[0] + '.' + identity.split('.')[1];
title = `${title} (Identity: ${identity})`;
if (identity) {
identity = identity.split('.')[0] + '.' + identity.split('.')[1];
title = `${title} (Identity: ${identity})`;
}
}
return title;
};

View File

@ -112,7 +112,7 @@ const ApiHelpers = (pathPrefix, defaultMetricsWindow = '1m') => {
};
const fetchEdges = (namespace, resourceType) => {
return apiFetch(edgesPath + "?namespace=" + namespace + "&resource_type=" + resourceType);
return apiFetch(edgesPath + "?resource_type=" + resourceType + "&namespace=" + namespace);
};
const getMetricsWindow = () => metricsWindow;

View File

@ -18,6 +18,7 @@ export const processEdges = (rawEdges, resourceName) => {
edge.direction = "OUTBOUND";
edge.identity = edge.serverId;
edge.name = edge.dst.name;
edge.namespace = edge.dst.namespace;
edge.key = edge.dst.name + edge.src.name;
edges.push(edge);
} else if (_startsWith(edge.dst.name, resourceName)) {
@ -25,6 +26,7 @@ export const processEdges = (rawEdges, resourceName) => {
edge.direction = "INBOUND";
edge.identity = edge.clientId;
edge.name = edge.src.name;
edge.namespace = edge.src.namespace;
edge.key = edge.src.name + edge.dst.name;
edges.push(edge);
}