linkerd2/web/app/js/components/util/TapUtils.jsx

386 lines
10 KiB
JavaScript

import { podOwnerLookup, toShortResourceName } from './Utils.js';
import BaseTable from '../BaseTable.jsx';
import Grid from '@material-ui/core/Grid';
import { Link } from 'react-router-dom';
import OpenInNewIcon from '@material-ui/icons/OpenInNew';
import Popover from '../Popover.jsx';
import PropTypes from 'prop-types';
import React from 'react';
import TapLink from '../TapLink.jsx';
import Tooltip from '@material-ui/core/Tooltip';
import _each from 'lodash/each';
import _get from 'lodash/get';
import _has from 'lodash/has';
import _isEmpty from 'lodash/isEmpty';
import _isNil from 'lodash/isNil';
import _map from 'lodash/map';
import _merge from 'lodash/merge';
import _size from 'lodash/size';
import _take from 'lodash/take';
export const httpMethods = ["GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH"];
export const defaultMaxRps = "100.0";
export const setMaxRps = query => {
if (!_isEmpty(query.maxRps)) {
query.maxRps = parseFloat(query.maxRps);
} else {
query.maxRps = 0; // golang unset value for maxRps
}
};
// use a generator to get this object, to prevent it from being overwritten
export const emptyTapQuery = () => ({
resource: "",
namespace: "",
toResource: "",
toNamespace: "",
method: "",
path: "",
scheme: "",
authority: "",
maxRps: ""
});
export const tapQueryProps = {
resource: PropTypes.string,
namespace: PropTypes.string,
toResource: PropTypes.string,
toNamespace: PropTypes.string,
method: PropTypes.string,
path: PropTypes.string,
scheme: PropTypes.string,
authority: PropTypes.string,
maxRps: PropTypes.string
};
export const tapQueryPropType = PropTypes.shape(tapQueryProps);
// from https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent
export const wsCloseCodes = {
1000: "Normal Closure",
1001: "Going Away",
1002: "Protocol Error",
1003: "Unsupported Data",
1004: "Reserved",
1005: "No Status Recvd",
1006: "Abnormal Closure",
1007: "Invalid frame payload data",
1008: "Policy Violation",
1009: "Message too big",
1010: "Missing Extension",
1011: "Internal Error",
1012: "Service Restart",
1013: "Try Again Later",
1014: "Bad Gateway",
1015: "TLS Handshake"
};
/*
Use tap data to figure out a resource's unmeshed upstreams/downstreams
*/
export const processNeighborData = (source, labels, resourceAgg, resourceType) => {
if (_isEmpty(labels)) {
return resourceAgg;
}
let neighb = {};
if (_has(labels, resourceType)) {
neighb = {
type: resourceType,
name: labels[resourceType],
namespace: labels.namespace
};
} else if (_has(labels, "pod")) {
neighb = {
type: "pod",
name: labels.pod,
namespace: labels.namespace
};
} else {
neighb = {
type: "ip",
name: source.str
};
}
// keep track of pods under this resource to display the number of unmeshed source pods
neighb.pods = {};
if (labels.pod) {
neighb.pods[labels.pod] = true;
}
let key = neighb.type + "/" + neighb.name;
if (_has(labels, "control_plane_ns")) {
delete resourceAgg[key];
} else {
if (_has(resourceAgg, key)) {
_merge(neighb.pods, resourceAgg[key].pods);
}
resourceAgg[key] = neighb;
}
return resourceAgg;
};
export const processTapEvent = jsonString => {
let d = JSON.parse(jsonString);
d.source.str = publicAddressToString(_get(d, "source.ip.ipv4"));
d.source.pod = _get(d, "sourceMeta.labels.pod", null);
d.source.owner = extractPodOwner(d.sourceMeta.labels);
d.source.namespace = _get(d, "sourceMeta.labels.namespace", null);
d.destination.str = publicAddressToString(_get(d, "destination.ip.ipv4"));
d.destination.pod = _get(d, "destinationMeta.labels.pod", null);
d.destination.owner = extractPodOwner(d.destinationMeta.labels);
d.destination.namespace = _get(d, "destinationMeta.labels.namespace", null);
switch (d.proxyDirection) {
case "INBOUND":
d.tls = _get(d, "sourceMeta.labels.tls", "");
break;
case "OUTBOUND":
d.tls = _get(d, "destinationMeta.labels.tls", "");
break;
default:
// too old for TLS
}
if (_isNil(d.http)) {
this.setState({ error: "Undefined request type"});
} else {
if (!_isNil(d.http.requestInit)) {
d.eventType = "requestInit";
} else if (!_isNil(d.http.responseInit)) {
d.eventType = "responseInit";
} else if (!_isNil(d.http.responseEnd)) {
d.eventType = "responseEnd";
}
d.id = tapEventKey(d, d.eventType);
}
return d;
};
/*
Use this key to associate the corresponding response with the request
so that we can have one single event with reqInit, rspInit and rspEnd
current key: (src, dst, stream)
*/
const tapEventKey = (d, eventType) => {
return `${d.source.str},${d.destination.str},${_get(d, ["http", eventType, "id", "stream"])}`;
};
/*
produce octets given an ip address
*/
const decodeIPToOctets = ip => {
ip = parseInt(ip, 10);
return [
ip >> 24 & 255,
ip >> 16 & 255,
ip >> 8 & 255,
ip & 255
];
};
/*
converts an address to an ipv4 formatted host
*/
const publicAddressToString = ipv4 => {
let octets = decodeIPToOctets(ipv4);
return octets.join(".");
};
/*
display more human-readable information about source/destination
*/
const resourceShortLink = (resourceType, labels, ResourceLink) => (
<ResourceLink
key={`${labels[resourceType]}-${labels.namespace}`}
resource={{ type: resourceType, name: labels[resourceType], namespace: labels.namespace}}
linkText={toShortResourceName(resourceType) + "/" + labels[resourceType]} />
);
const displayLimit = 3; // how many upstreams/downstreams to display in the popover table
const popoverSrcDstColumns = [
{ title: "Source", dataIndex: "source" },
{ title: "", key: "arrow", render: () => <i className="fas fa-long-arrow-alt-right" /> },
{ title: "Destination", dataIndex: "destination" }
];
const getPodOwner = (labels, ResourceLink) => {
let podOwner = extractPodOwner(labels);
if (!podOwner) {
return null;
} else {
let [labelName] = podOwner.split("/");
return (
<div className="popover-td">
{ resourceShortLink(labelName, labels, ResourceLink) }
</div>
);
}
};
const getPodList = (endpoint, display, labels, ResourceLink) => {
let podList = "---";
if (!display) {
if (endpoint.pod) {
podList = resourceShortLink("pod", { pod: endpoint.pod, namespace: labels.namespace }, ResourceLink);
}
} else if (!_isEmpty(display.pods)) {
podList = (
<React.Fragment>
{
_map(display.pods, (namespace, pod, i) => {
if (i > displayLimit) {
return null;
} else {
return <div key={pod}>{resourceShortLink("pod", { pod, namespace }, ResourceLink)}</div>;
}
})
}
{ (_size(display.pods) > displayLimit ? "..." : "") }
</React.Fragment>
);
}
return <div className="popover-td">{podList}</div>;
};
// display consists of a list of ips and pods for aggregated displays (Top)
const getIpList = (endpoint, display) => {
let ipList = endpoint.str;
if (display) {
ipList = _take(Object.keys(display.ips), displayLimit).join(", ") +
(_size(display.ips) > displayLimit ? "..." : "");
}
return <div className="popover-td">{ipList}</div>;
};
const popoverResourceTable = (d, ResourceLink) => { // eslint-disable-line no-unused-vars
let tableData = [
{
source: getPodOwner(d.sourceLabels, ResourceLink),
destination: getPodOwner(d.destinationLabels, ResourceLink),
key: "podOwner"
},
{
source: getPodList(d.source, d.sourceDisplay, d.sourceLabels, ResourceLink),
destination: getPodList(d.destination, d.destinationDisplay, d.destinationLabels, ResourceLink),
key: "podList"
},
{
source: getIpList(d.source, d.sourceDisplay),
destination: getIpList(d.destination, d.destinationDisplay),
key: "ipList"
}
];
return (
<BaseTable
tableColumns={popoverSrcDstColumns}
tableRows={tableData}
tableClassName="metric-table" />
);
};
export const extractPodOwner = labels => {
let podOwner = "";
_each(labels, (labelVal, labelName) => {
if (_has(podOwnerLookup, labelName)) {
podOwner = labelName + "/" + labelVal;
}
});
return podOwner;
};
export const directionColumn = d => (
<Tooltip title={d} placement="right">
<span>{d === "INBOUND" ? "FROM" : "TO"}</span>
</Tooltip>
);
export const srcDstColumn = (d, resourceType, ResourceLink) => {
let display = {};
let labels = {};
if (d.direction === "INBOUND") {
display = d.source;
labels = d.sourceLabels;
} else {
display = d.destination;
labels = d.destinationLabels;
}
let link = (
!_isEmpty(labels[resourceType]) ?
resourceShortLink(resourceType, labels, ResourceLink) :
display.str
);
const linkFn = e => {
e.preventDefault();
};
let baseContent = (
<Link to="#" onClick={linkFn}>
<OpenInNewIcon fontSize="small" />
</Link>
);
return (
<Grid
container
direction="row"
alignItems="center"
spacing={8}>
<Grid item>
{link}
</Grid>
<Grid item>
<Popover
popoverContent={(popoverResourceTable(d, ResourceLink))}
baseContent={baseContent} />
</Grid>
</Grid>
);
};
export const tapLink = (d, resourceType, PrefixedLink) => {
let namespace = d.sourceLabels.namespace;
let resource = "";
if (_has(d.sourceLabels, resourceType)) {
resource = `${resourceType}/${d.sourceLabels[resourceType]}`;
} else if (_has(d.sourceLabels, "pod")) {
resource = `pod/${d.sourceLabels.pod}`;
} else {
return null; // can't tap a resource by IP from the web UI
}
let toNamespace = "";
let toResource = "";
if (_has(d.destinationLabels, resourceType)) {
toNamespace = d.destinationLabels.namespace,
toResource = `${resourceType}/${d.destinationLabels[resourceType]}`;
} else if (_has(d.destinationLabels, "pod")) {
toNamespace = d.destinationLabels.namespace,
toResource = `${resourceType}/${d.destinationLabels.pod}`;
}
return (
<TapLink
namespace={namespace}
resource={resource}
toNamespace={toNamespace}
toResource={toResource}
path={d.path}
PrefixedLink={PrefixedLink} />
);
};