mirror of https://github.com/linkerd/linkerd2.git
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:
parent
f90a3c09ed
commit
a504e8c2d8
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue