import _ from 'lodash'; import CallToAction from './CallToAction.jsx'; import ConduitSpinner from "./ConduitSpinner.jsx"; import ErrorBanner from './ErrorBanner.jsx'; import ErrorModal from './ErrorModal.jsx'; import { incompleteMeshMessage } from './util/CopyUtils.jsx'; import Metric from './Metric.jsx'; import { numericSort } from './util/Utils.js'; import PageHeader from './PageHeader.jsx'; import Percentage from './util/Percentage.js'; import PropTypes from 'prop-types'; import React from 'react'; import StatusTable from './StatusTable.jsx'; import { withContext } from './util/AppContext.jsx'; import { Col, Row, Table, Tooltip } from 'antd'; import './../../css/service-mesh.css'; const serviceMeshDetailsColumns = [ { title: "Name", dataIndex: "name", key: "name" }, { title: "Value", dataIndex: "value", key: "value", className: "numeric" } ]; const getClassification = (meshedPodCount, failedPodCount) => { if (failedPodCount > 0) { return "poor"; } else if (meshedPodCount === 0) { return "neutral"; } else { return "good"; } }; const namespacesColumns = ConduitLink => [ { title: "Namespace", key: "namespace", defaultSortOrder: "ascend", sorter: (a, b) => (a.namespace || "").localeCompare(b.namespace), render: d => { return ( {d.namespace} { _.isEmpty(d.errors) ? null : } ); } }, { title: "Meshed pods", dataIndex: "meshedPodsStr", key: "meshedPodsStr", className: "numeric", sorter: (a, b) => numericSort(a.totalPods, b.totalPods), }, { title: "Meshed Status", key: "meshification", sorter: (a, b) => numericSort(a.meshedPercent.get(), b.meshedPercent.get()), render: row => { let containerWidth = 132; let percent = row.meshedPercent.get(); let barWidth = percent < 0 ? 0 : Math.round(percent * containerWidth); let barType = _.isEmpty(row.errors) ? getClassification(row.meshedPods, row.failedPods) : "poor"; return (
{`${row.meshedPods} out of ${row.totalPods} running or pending pods are in the mesh (${row.meshedPercent.prettyRate()})`}
{row.failedPods === 0 ? null :
{ `${row.failedPods} failed pods` }
} )}>
 
); } } ]; const componentNames = { "prometheus": "Prometheus", "grafana": "Grafana", "destination": "Destination", "proxy-api": "Proxy API", "public-api": "Public API", "tap": "Tap", "web": "Web UI" }; const componentDeploys = { "prometheus": "prometheus", "grafana": "grafana", "destination": "controller", "proxy-api": "controller", "public-api": "controller", "tap": "controller", "web": "web" }; class ServiceMesh extends React.Component { static propTypes = { api: PropTypes.shape({ cancelCurrentRequests: PropTypes.func.isRequired, ConduitLink: PropTypes.func.isRequired, fetchMetrics: PropTypes.func.isRequired, getCurrentPromises: PropTypes.func.isRequired, setCurrentRequests: PropTypes.func.isRequired, urlsForResource: PropTypes.func.isRequired, }).isRequired, controllerNamespace: PropTypes.string.isRequired, releaseVersion: PropTypes.string.isRequired, } constructor(props) { super(props); this.loadFromServer = this.loadFromServer.bind(this); this.handleApiError = this.handleApiError.bind(this); this.api = this.props.api; this.state = { pollingInterval: 2000, components: [], pendingRequests: false, loaded: false, error: '' }; } componentDidMount() { this.loadFromServer(); this.timerId = window.setInterval(this.loadFromServer, this.state.pollingInterval); } componentWillUnmount() { window.clearInterval(this.timerId); this.api.cancelCurrentRequests(); } getServiceMeshDetails() { return [ { key: 1, name: "Conduit version", value: this.props.releaseVersion }, { key: 2, name: "Conduit namespace", value: this.props.controllerNamespace }, { key: 3, name: "Control plane components", value: this.componentCount() }, { key: 4, name: "Data plane proxies", value: this.proxyCount() } ]; } extractNsStatuses(nsData) { let podsByNs = _.get(nsData, ["ok", "statTables", 0, "podGroup", "rows"], []); let dataPlaneNamepaces = _.map(podsByNs, ns => { let meshedPods = parseInt(ns.meshedPodCount, 10); let totalPods = parseInt(ns.runningPodCount, 10); let failedPods = parseInt(ns.failedPodCount, 10); return { namespace: ns.resource.name, meshedPodsStr: ns.meshedPodCount + "/" + ns.runningPodCount, meshedPercent: new Percentage(meshedPods, totalPods), meshedPods, totalPods, failedPods, errors: ns.errorsByPod }; }); return _.compact(dataPlaneNamepaces); } processComponents(conduitPods) { let pods = _.get(conduitPods, ["ok", "statTables", 0, "podGroup", "rows"], 0); return _.map(componentNames, (title, name) => { let deployName = componentDeploys[name]; let matchingPods = _.filter(pods, p => p.resource.name.split("-")[0] === deployName); return { name: title, pods: _.map(matchingPods, p => { return { name: p.resource.name, value: getClassification(parseInt(p.meshedPodCount, 10), parseInt(p.failedPodCount, 10)) }; }) }; }); } loadFromServer() { if (this.state.pendingRequests) { return; // don't make more requests if the ones we sent haven't completed } this.setState({ pendingRequests: true }); this.api.setCurrentRequests([ this.api.fetchMetrics(this.api.urlsForResource("pod", this.props.controllerNamespace)), this.api.fetchMetrics(this.api.urlsForResource("namespace")) ]); this.serverPromise = Promise.all(this.api.getCurrentPromises()) .then(([conduitPods, nsStats]) => { this.setState({ components: this.processComponents(conduitPods), nsStatuses: this.extractNsStatuses(nsStats), pendingRequests: false, loaded: true, error: '' }); }) .catch(this.handleApiError); } handleApiError(e) { if (e.isCanceled) { return; } this.setState({ pendingRequests: false, error: `Error getting data from server: ${e.message}` }); } componentCount() { return _.size(this.state.components); } proxyCount() { return _.sumBy(this.state.nsStatuses, d => { return d.namespace === this.props.controllerNamespace ? 0 : d.meshedPods; }); } renderControlPlaneDetails() { return (
Control plane
); } renderServiceMeshDetails() { return (
Service mesh details
); } renderAddResourcesMessage() { if (_.isEmpty(this.state.nsStatuses)) { return
No resources detected.
; } let meshedCount = _.countBy(this.state.nsStatuses, pod => { return pod.meshedPercent.get() > 0; }); let numUnadded = meshedCount["false"] || 0; if (numUnadded === 0) { return (
All namespaces have a conduit install.
); } else { return (
{numUnadded} {numUnadded === 1 ? "namespace has" : "namespaces have"} no meshed resources. {incompleteMeshMessage()}
); } } renderNamespaceStatusTable() { let rowCn = row => { return row.meshedPercent.get() > 0.9 ? "good" : "neutral"; }; return (
{this.renderAddResourcesMessage()} ); } render() { return (
{ !this.state.error ? null : } { !this.state.loaded ? : (
{this.proxyCount() === 0 ? : null}
{this.renderControlPlaneDetails()}{this.renderServiceMeshDetails()} {this.renderNamespaceStatusTable()} )} ); } } export default withContext(ServiceMesh);