Add Edges table to resource detail view of dashboard (#2965)

Adds an Edges table to the resource detail view that shows the source,
destination name and identity for proxied connections to and from the resource
shown.
This commit is contained in:
Carol A. Scott 2019-06-20 10:50:11 -07:00 committed by GitHub
parent 2252e44305
commit dce462acd9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 241 additions and 23 deletions

View File

@ -1,5 +1,6 @@
import CloseIcon from '@material-ui/icons/Close';
import FilterListIcon from '@material-ui/icons/FilterList';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import Hidden from '@material-ui/core/Hidden';
import Paper from '@material-ui/core/Paper';
import PropTypes from 'prop-types';
@ -19,6 +20,7 @@ import _get from 'lodash/get';
import _isNil from 'lodash/isNil';
import _orderBy from 'lodash/orderBy';
import classNames from 'classnames';
import { faQuestionCircle } from '@fortawesome/free-solid-svg-icons/faQuestionCircle';
import { withStyles } from '@material-ui/core/styles';
const styles = theme => ({
@ -149,7 +151,11 @@ class BaseTable extends React.Component {
<Typography
className={classes.title}
variant="h5">
{title}
{title} {this.props.showTitleTooltip &&
<Tooltip title={this.props.titleTooltipText}>
<FontAwesomeIcon icon={faQuestionCircle} />
</Tooltip>
}
</Typography>
{this.state.showFilter &&
<TextField
@ -223,6 +229,7 @@ BaseTable.propTypes = {
enableFilter: PropTypes.bool,
padding: PropTypes.string,
rowKey: PropTypes.func,
showTitleTooltip: PropTypes.bool,
tableClassName: PropTypes.string,
tableColumns: PropTypes.arrayOf(PropTypes.shape({
dataIndex: PropTypes.string,
@ -233,7 +240,8 @@ BaseTable.propTypes = {
title: PropTypes.string
})).isRequired,
tableRows: PropTypes.arrayOf(PropTypes.shape({})),
title: PropTypes.string
title: PropTypes.string,
titleTooltipText: PropTypes.string
};
BaseTable.defaultProps = {
@ -244,7 +252,9 @@ BaseTable.defaultProps = {
rowKey: null,
tableClassName: "",
tableRows: [],
title: ""
title: "",
showTitleTooltip: false,
titleTooltipText: ""
};
export default withStyles(styles)(BaseTable);

View File

@ -0,0 +1,113 @@
import BaseTable from './BaseTable.jsx';
import PropTypes from 'prop-types';
import React from 'react';
import { processedEdgesPropType } from './util/EdgesUtils.jsx';
import { withContext } from './util/AppContext.jsx';
const edgesColumnDefinitions = (PrefixedLink, namespace, type) => {
return [
{
title: "Source",
dataIndex: "source",
isNumeric: false,
filter: d => d.src.name,
render: d => {
// check that the source is a k8s resource with a name we can link to
if (namespace && type && d.src && d.src.name) {
return (
<PrefixedLink to={`/namespaces/${namespace}/${type}s/${d.src.name}`}>
{d.src.name}
</PrefixedLink>
);
} else {
return d.src.name;
}
},
sorter: d => d.src.name + d.dst.name
},
{
title: "Destination",
dataIndex: "destination",
isNumeric: false,
filter: d => d.dst.name,
render: d => {
// check that the destination is a k8s resource with a name we can link to
if (namespace && type && d.dst && d.dst.name) {
return (
<PrefixedLink to={`/namespaces/${namespace}/${type}s/${d.dst.name}`}>
{d.dst.name}
</PrefixedLink>
);
} else {
return d.dst.name;
}
},
sorter: d => d.dst.name + d.src.name
},
{
title: "Client",
dataIndex: "client",
isNumeric: false,
filter: d => d.clientId,
render: d => d.clientId.split('.')[0] + '.' + d.clientId.split('.')[1],
sorter: d => d.clientId
},
{
title: "Server",
dataIndex: "server",
isNumeric: false,
filter: d => d.serverId,
render: d => d.serverId.split('.')[0] + '.' + d.serverId.split('.')[1],
sorter: d => d.serverId
},
{
title: "Message",
dataIndex: "message",
isNumeric: false,
filter: d => d.noIdentityMsg,
render: d => d.noIdentityMsg,
sorter: d => d.noIdentityMsg
}
];
};
const tooltipText = `Edges show the source, destination name and identity
for proxied connections. If no identity is known, a message is displayed.`;
class EdgesTable extends React.Component {
static propTypes = {
api: PropTypes.shape({
prefixedUrl: PropTypes.func.isRequired,
}).isRequired,
edges: PropTypes.arrayOf(processedEdgesPropType),
namespace: PropTypes.string.isRequired,
title: PropTypes.string,
type: PropTypes.string.isRequired
};
static defaultProps = {
edges: [],
title: ""
};
render() {
const { edges, title, api, namespace, type } = this.props;
let edgesColumns = edgesColumnDefinitions(api.PrefixedLink, namespace, type);
return (
<BaseTable
defaultOrderBy="source"
enableFilter={true}
showTitleTooltip={true}
tableRows={edges}
tableColumns={edgesColumns}
tableClassName="metric-table"
title={title}
titleTooltipText={tooltipText}
padding="dense" />
);
}
}
export default withContext(EdgesTable);

View File

@ -2,6 +2,7 @@ import 'whatwg-fetch';
import { emptyMetric, processMultiResourceRollup, processSingleResourceRollup } from './util/MetricUtils.jsx';
import { resourceTypeToCamelCase, singularResource } from './util/Utils.js';
import AddResources from './AddResources.jsx';
import EdgesTable from './EdgesTable.jsx';
import ErrorBanner from './ErrorBanner.jsx';
import Grid from '@material-ui/core/Grid';
import MetricsTable from './MetricsTable.jsx';
@ -14,15 +15,19 @@ import TopRoutesTabs from './TopRoutesTabs.jsx';
import Typography from '@material-ui/core/Typography';
import _filter from 'lodash/filter';
import _get from 'lodash/get';
import _indexOf from 'lodash/indexOf';
import _isEmpty from 'lodash/isEmpty';
import _isEqual from 'lodash/isEqual';
import _isNil from 'lodash/isNil';
import _merge from 'lodash/merge';
import _reduce from 'lodash/reduce';
import { processEdges } from './util/EdgesUtils.jsx';
import { withContext } from './util/AppContext.jsx';
// if there has been no traffic for some time, show a warning
const showNoTrafficMsgDelayMs = 6000;
// resource types supported when querying API for edge data
const edgeDataAvailable = ["daemonset", "deployment", "job", "pod", "replicationcontroller", "statefulset"];
const getResourceFromUrl = (match, pathPrefix) => {
let resource = {
@ -124,7 +129,8 @@ export class ResourceDetailBase extends React.Component {
let { resource } = this.state;
this.api.setCurrentRequests([
let apiRequests =
[
// inbound stats for this resource
this.api.fetchMetrics(
`${this.api.urlsForResource(resource.type, resource.namespace, true)}&resource_name=${resource.name}`
@ -143,14 +149,23 @@ export class ResourceDetailBase extends React.Component {
this.api.fetchMetrics(
`${this.api.urlsForResource("all")}&from_name=${resource.name}&from_type=${resource.type}&from_namespace=${resource.namespace}`
)
];
if (_indexOf(edgeDataAvailable, resource.type) > 0) {
apiRequests = apiRequests.concat([
this.api.fetchEdges(resource.namespace, resource.type)
]);
}
this.api.setCurrentRequests(apiRequests);
Promise.all(this.api.getCurrentPromises())
.then(([resourceRsp, podListRsp, podMetricsRsp, upstreamRsp, downstreamRsp]) => {
.then(([resourceRsp, podListRsp, podMetricsRsp, upstreamRsp, downstreamRsp, edgesRsp]) => {
let resourceMetrics = processSingleResourceRollup(resourceRsp);
let podMetrics = processSingleResourceRollup(podMetricsRsp);
let upstreamMetrics = processMultiResourceRollup(upstreamRsp);
let downstreamMetrics = processMultiResourceRollup(downstreamRsp);
let edges = processEdges(edgesRsp, this.state.resource.name);
// INEFFICIENT: get metrics for all the pods belonging to this resource.
// Do this by querying for metrics for all pods in this namespace and then filtering
@ -208,6 +223,7 @@ export class ResourceDetailBase extends React.Component {
podMetrics: podMetricsForResource,
upstreamMetrics,
downstreamMetrics,
edges,
lastMetricReceivedTime,
isTcpOnly,
loaded: true,
@ -253,6 +269,7 @@ export class ResourceDetailBase extends React.Component {
resourceType,
namespace,
resourceMetrics,
edges,
unmeshedSources,
resourceIsMeshed,
lastMetricReceivedTime,
@ -347,6 +364,21 @@ export class ResourceDetailBase extends React.Component {
title="TCP"
isTcpTable={true}
metrics={this.state.podMetrics} />
{
_isEmpty(this.state.edges) ? null :
<Grid container direction="column" justify="center">
<Grid item>
<EdgesTable
api={this.api}
namespace={this.state.resource.namespace}
type={this.state.resource.type}
title="Edges"
edges={edges} />
</Grid>
</Grid>
}
</div>
);
}

View File

@ -59,6 +59,7 @@ const ApiHelpers = (pathPrefix, defaultMetricsWindow = '1m') => {
let metricsWindow = defaultMetricsWindow;
const podsPath = `/api/pods`;
const servicesPath = `/api/services`;
const edgesPath = `/api/edges`;
const validMetricsWindows = {
"10s": "10 minutes",
@ -110,6 +111,10 @@ const ApiHelpers = (pathPrefix, defaultMetricsWindow = '1m') => {
return apiFetch(servicesPath);
};
const fetchEdges = (namespace, resourceType) => {
return apiFetch(edgesPath + "?namespace=" + namespace + "&resource_type=" + resourceType);
};
const getMetricsWindow = () => metricsWindow;
const getMetricsWindowDisplayText = () => validMetricsWindows[metricsWindow];
@ -211,6 +216,7 @@ const ApiHelpers = (pathPrefix, defaultMetricsWindow = '1m') => {
fetchMetrics,
fetchPods,
fetchServices,
fetchEdges,
getMetricsWindow,
setMetricsWindow,
getValidMetricsWindows: () => Object.keys(validMetricsWindows),

View File

@ -0,0 +1,36 @@
import PropTypes from 'prop-types';
import _each from 'lodash/each';
import _isEmpty from 'lodash/isEmpty';
import _startsWith from 'lodash/startsWith';
export const processEdges = (rawEdges, resourceName) => {
let edges = [];
if (_isEmpty(rawEdges) || _isEmpty(rawEdges.ok) || _isEmpty(rawEdges.ok.edges)) {
return edges;
}
_each(rawEdges.ok.edges, edge => {
if (_isEmpty(edge)) {
return;
}
// check if any of the returned edges match the current resourceName
if (_startsWith(edge.src.name, resourceName) || _startsWith(edge.dst.name, resourceName)) {
edge.key = edge.src.name + edge.dst.name;
edges.push(edge);
}
});
return edges;
};
export const processedEdgesPropType = PropTypes.shape({
clientId: PropTypes.string,
dst: PropTypes.shape(edgeResourcePropType).isRequired,
key: PropTypes.string.isRequired,
noIdentityMsg: PropTypes.string,
serverId: PropTypes.string,
src: PropTypes.shape(edgeResourcePropType).isRequired,
});
const edgeResourcePropType = PropTypes.shape({
name: PropTypes.string,
type: PropTypes.string
});

View File

@ -251,3 +251,23 @@ func (h *handler) handleAPITap(w http.ResponseWriter, req *http.Request, p httpr
}
}
}
func (h *handler) handleAPIEdges(w http.ResponseWriter, req *http.Request, p httprouter.Params) {
requestParams := util.EdgesRequestParams{
Namespace: req.FormValue("namespace"),
ResourceType: req.FormValue("resource_type"),
}
edgesRequest, err := util.BuildEdgesRequest(requestParams)
if err != nil {
renderJSONError(w, err, http.StatusInternalServerError)
return
}
result, err := h.apiClient.Edges(req.Context(), edgesRequest)
if err != nil {
renderJSONError(w, err, http.StatusInternalServerError)
return
}
renderJSONPb(w, result)
}

View File

@ -124,6 +124,7 @@ func NewServer(
server.router.GET("/api/services", handler.handleAPIServices)
server.router.GET("/api/tap", handler.handleAPITap)
server.router.GET("/api/routes", handler.handleAPITopRoutes)
server.router.GET("/api/edges", handler.handleAPIEdges)
// grafana proxy
server.router.DELETE("/grafana/*grafanapath", handler.handleGrafana)