Refactoring. Better charts. Fixed a bug parsing cpu usage. Using a better api for validating tokens. Added some server logging at startup to help track down issues.

This commit is contained in:
Eric Herbrandson
2019-04-03 22:17:08 -05:00
parent 18f5706ab9
commit 4526ecec02
10 changed files with 218 additions and 76 deletions
+3 -1
View File
@@ -10,7 +10,9 @@ const options = {
showLabel: false,
};
export default function Chart({used = 0, usedSuffix, available = 0, availableSuffix, pending = 0, decimals = 1}) { // eslint-disable-line max-len
export default function Chart(props) {
const {used = 0, usedSuffix, available = 0, availableSuffix, pending = 0, decimals = 1} = props;
const fixedUsed = _.round(used, decimals);
const fixedPending = _.round(pending, decimals);
const fixedAvailable = _.round(available, decimals);
+7 -2
View File
@@ -6,15 +6,20 @@ import {parseCpu, TO_ONE_CPU} from '../utils/unitHelpers';
export default function CpuChart({items, metrics}) {
const totals = getPodCpuTotals(items, metrics);
const decimals = totals && totals.used > 10 ? 1 : 2;
return (
<div className='charts_item'>
{totals ? (
<Chart decimals={2} used={totals.used} available={totals.available} />
<Chart
decimals={decimals}
used={totals.used}
available={totals.available}
/>
) : (
<LoadingChart />
)}
<div className='charts_itemLabel'>Requested Cpu Use</div>
<div className='charts_itemLabel'>Cores Used</div>
</div>
);
}
+68 -40
View File
@@ -7,6 +7,24 @@ import Sorter from './sorter';
import {parseRam, unparseRam, parseCpu, unparseCpu} from '../utils/unitHelpers';
export default class PodsPanel extends Base {
constructor(props) {
super(props);
this.sortByCpu = this.sortByCpu.bind(this);
this.sortByRam = this.sortByRam.bind(this);
}
sortByCpu(item) {
const actual = getCpuUsage(item, this.props.metrics);
const requested = getCpuRequest(item);
return actual / requested;
}
sortByRam(item) {
const actual = getRamUsage(item, this.props.metrics);
const requested = getRamRequest(item);
return actual / requested;
}
render() {
const {items, metrics, sort, filter, skipNamespace, skipNodeName} = this.props;
const col = 8 + !skipNamespace + !skipNodeName;
@@ -23,19 +41,21 @@ export default class PodsPanel extends Base {
<th><Sorter field='status.phase' sort={sort}>Status</Sorter></th>
<th><Sorter field={getContainerCount} sort={sort}>Containers</Sorter></th>
<th><Sorter field={getRestartCount} sort={sort}>Restarts</Sorter></th>
{/* TODO: support sorting by metrics */}
<th>
Cpu
<div className='podsPanel_label'>
actual vs. request
</div>
<Sorter field={this.sortByCpu} sort={sort}>
Cpu
<div className='podsPanel_label'>
actual vs. request
</div>
</Sorter>
</th>
<th>
Ram
<div className='podsPanel_label'>
actual vs. request
</div>
<Sorter field={this.sortByRam} sort={sort}>
Ram
<div className='podsPanel_label'>
actual vs. request
</div>
</Sorter>
</th>
</tr>
</thead>
@@ -71,47 +91,37 @@ function getRestartCount({status}) {
}
function Cpu({item, metrics}) {
if (!item || !metrics) return null;
const {containers} = metrics[item.metadata.name] || {};
const containers = getContainerMetrics(item, metrics);
if (!containers) return null;
const actual = _.sumBy(containers, x => parseCpu(x.usage.cpu));
const podContainers = item.spec.containers.filter(x => x.resources && x.resources.requests);
const requested = _.sumBy(podContainers, x => parseCpu(x.resources.requests.cpu));
const actual = getCpuUsage(item, metrics);
const requested = getCpuRequest(item);
return <Chart actual={actual} requested={requested} unparser={unparseCpu} />;
}
const isWarning = requested && (actual > requested);
const actualCpu = unparseCpu(actual);
const requestedCpu = unparseCpu(requested);
const percent = requested ? _.round(actual / requested * 100, 1) : 100;
function getCpuUsage(item, metrics) {
const containers = getContainerMetrics(item, metrics);
return _.sumBy(containers, x => parseCpu(x.usage.cpu));
}
return (
<div className={isWarning ? 'contentPanel_warn' : undefined}>
<div>{requested ? `${percent}%` : 'N/A'}</div>
<div className='podsPanel_label'>
<span>{actualCpu.value}{actualCpu.unit}</span>
<span> of </span>
<span>{requestedCpu.value}{requestedCpu.unit}</span>
</div>
</div>
);
function getCpuRequest(item) {
const podContainers = _.filter(item.spec.containers, x => x.resources && x.resources.requests);
return _.sumBy(podContainers, x => parseCpu(x.resources.requests.cpu));
}
function Ram({item, metrics}) {
if (!item || !metrics) return null;
const {containers} = metrics[item.metadata.name] || {};
const containers = getContainerMetrics(item, metrics);
if (!containers) return null;
const actual = _.sumBy(containers, x => parseRam(x.usage.memory));
const requested = _.sumBy(item.spec.containers, (x) => {
if (!x.resources.requests) return 0;
return parseRam(x.resources.requests.memory);
});
const actual = getRamUsage(item, metrics);
const requested = getRamRequest(item);
return <Chart actual={actual} requested={requested} unparser={unparseRam} />;
}
function Chart({actual, requested, unparser}) {
const isWarning = requested && (actual > requested);
const actualRam = unparseRam(actual);
const requestedRam = unparseRam(requested);
const actualRam = unparser(actual);
const requestedRam = unparser(requested);
const percent = requested ? _.round(actual / requested * 100, 1) : 100;
return (
@@ -125,3 +135,21 @@ function Ram({item, metrics}) {
</div>
);
}
function getRamUsage(item, metrics) {
const containers = getContainerMetrics(item, metrics);
return _.sumBy(containers, x => parseRam(x.usage.memory));
}
function getRamRequest(item) {
return _.sumBy(item.spec.containers, (x) => {
if (!x.resources.requests) return 0;
return parseRam(x.resources.requests.memory);
});
}
function getContainerMetrics(item, metrics) {
if (!item || !metrics) return null;
const metric = metrics[item.metadata.name] || {};
return metric.containers;
}
+9 -2
View File
@@ -6,15 +6,22 @@ import {parseRam, TO_GB} from '../utils/unitHelpers';
export default function RamChart({items, metrics}) {
const totals = getPodRamTotals(items, metrics);
const decimals = totals && totals.used > 10 ? 1 : 2;
return (
<div className='charts_item'>
{totals ? (
<Chart used={totals && totals.used} available={totals && totals.available} />
<Chart
decimals={decimals}
used={totals && totals.used}
usedSuffix='Gb'
available={totals && totals.available}
availableSuffix='Gb'
/>
) : (
<LoadingChart />
)}
<div className='charts_itemLabel'>Requested Ram Use</div>
<div className='charts_itemLabel'>Ram Used</div>
</div>
);
}
+4 -10
View File
@@ -1,5 +1,5 @@
import _ from 'lodash';
import {request, stream, streamResult, streamResults, getToken} from './apiProxy';
import {request, stream, streamResult, streamResults} from './apiProxy';
import log from '../utils/log';
const JSON_HEADERS = {Accept: 'application/json', 'Content-Type': 'application/json'};
@@ -40,15 +40,9 @@ const apis = {
roleBinding: apiFactoryWithNamespace('/apis/rbac.authorization.k8s.io/v1', 'rolebindings'),
};
function testAuth() {
const token = getToken();
const body = {
apiVerstion: 'authentication.k8s.io/v1',
kind: 'TokenReview',
spec: {token},
};
return post('/apis/authentication.k8s.io/v1/tokenreviews', body, false);
async function testAuth() {
const spec = {resourceAttributes: {}};
return post('apis/authorization.k8s.io/v1/selfsubjectaccessreviews', {spec}, false);
}
async function apply(body) {
+1
View File
@@ -52,6 +52,7 @@ export function parseCpu(value) {
const number = parseInt(value, 10);
if (value.endsWith('n')) return number;
if (value.endsWith('u')) return number * 1000;
if (value.endsWith('m')) return number * 1000 * 1000;
return number * 1000 * 1000 * 1000;
}
+14 -3
View File
@@ -14,6 +14,10 @@ const LOGIN_MESSAGE = 'Invalid credentials';
const ERROR_MESSAGE = 'Error occured attempting to login';
export default class Auth extends Base {
state = {
token: '',
};
async componentDidMount() {
const url = new URL(window.location.href);
const code = url.searchParams.get('code');
@@ -46,10 +50,10 @@ export default class Auth extends Base {
className='auth_input'
placeholder='Enter your auth token here...'
spellCheck='false'
defaultValue={token}
value={token}
onChange={x => this.setState({token: x.target.value})}
/>
<Button className='button auth_button' onClick={() => login(token)}>
<Button disabled={!token} className='button auth_button' onClick={() => login(token)}>
Go
</Button>
</>
@@ -95,7 +99,14 @@ async function oidcLogin(code, returnedState) {
async function login(token) {
try {
setToken(token);
await api.testAuth();
const result = await api.testAuth();
log.info('Auth', result);
if (!result || !result.status || !result.status.allowed) {
throw new Error('Invalid login');
}
window.location.reload();
} catch (err) {
log.error('Login Failed', err);
+37
View File
@@ -1,5 +1,6 @@
import _ from 'lodash';
import React from 'react';
import moment from 'moment';
import api from '../services/api';
import Base from '../components/base';
import Chart from '../components/chart';
@@ -11,6 +12,7 @@ import PodsPanel from '../components/podsPanel';
import {defaultSortInfo} from '../components/sorter';
import {parseCpu, parseRam, TO_GB, TO_ONE_M_CPU} from '../utils/unitHelpers';
import PodStatusChart from '../components/podStatusChart';
import Field from '../components/field';
export default class Node extends Base {
state = {
@@ -51,10 +53,45 @@ export default class Node extends Base {
{!item ? <Loading /> : (
<div>
<MetadataFields item={item} />
<Field name='Kernel Version' value={item.status.nodeInfo.kernelVersion} />
<Field name='OS Image' value={item.status.nodeInfo.osImage} />
<Field name='OS' value={item.status.nodeInfo.operatingSystem} />
<Field name='Architecture' value={item.status.nodeInfo.architecture} />
<Field name='Container Runtime' value={item.status.nodeInfo.containerRuntimeVersion} />
<Field name='Kubelet' value={item.status.nodeInfo.kubeletVersion} />
<Field name='Kube Proxy' value={item.status.nodeInfo.kubeProxyVersion} />
</div>
)}
</div>
<div className='contentPanel'>
{!item ? <Loading /> : (
<table>
<thead>
<tr>
<th>Condition</th>
<th>Status</th>
<th>Transition</th>
<th>Reason</th>
<th>Message</th>
</tr>
</thead>
<tbody>
{_.map(item.status.conditions, x => (
<tr key={x.type}>
<td>{x.type}</td>
<td>{x.status}</td>
<td>{moment(x.lastTransitionTime).fromNow()}</td>
<td>{x.reason}</td>
<td>{x.message}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
<PodsPanel
items={filteredPods}
sort={podsSort}
+53 -15
View File
@@ -58,10 +58,8 @@ export default class Nodes extends Base {
<th><Sorter field={getReadyStatus} sort={sort}>Ready</Sorter></th>
<th>Cpu</th>
<th>Ram</th>
{/* TODO: support sorting by cpu/ram
<th><Sorter field='' sort={sort}>Cpu</Sorter></th>
<th><Sorter field='' sort={sort}>Ram</Sorter></th> */}
{/* <th><Sorter field={sortByCpu} sort={sort}>Cpu</Sorter></th>
<th><Sorter field={sortByRam} sort={sort}>Ram</Sorter></th> */}
</tr>
</thead>
@@ -81,6 +79,23 @@ export default class Nodes extends Base {
}
}
// TODO: get sortying by cpu/ram working
// function sortByCpu(item) {
// const used = getCpuUsed(item, filteredMetrics);
// if (used == null) return null;
// const available = getCpuAvailable(item);
// return used / available;
// }
// function sortByRam(item) {
// const used = getRamUsed(item, filteredMetrics);
// if (used == null) return null;
// const available = getRamAvailable(item);
// return used / available;
// }
function getReadyStatus({status}) {
if (!status.conditions) return null;
@@ -117,31 +132,54 @@ function RamTotalsChart({items, metrics}) {
}
function CpuChart({item, metrics}) {
if (!item || !metrics) return null;
const used = getCpuUsed(item, metrics);
if (used == null) return null;
const {usage} = metrics[item.metadata.name] || {};
const available = getCpuAvailable(item);
return <Percent used={used} available={available} />;
}
function getCpuUsed(item, metrics) {
const usage = getUsage(item, metrics);
if (!usage) return null;
const totalUsed = parseCpu(usage.cpu) / 1000000;
const totalAvailable = parseCpu(item.status.capacity.cpu) / 1000000;
const percent = _.round(totalUsed / totalAvailable * 100, 1);
return parseCpu(usage.cpu) / 1000000;
}
return (<span>{`${percent}%`}</span>);
function getCpuAvailable(item) {
return parseCpu(item.status.capacity.cpu) / 1000000;
}
function RamChart({item, metrics}) {
if (!item || !metrics) return null;
const used = getRamUsed(item, metrics);
if (used == null) return null;
const {usage} = metrics[item.metadata.name] || {};
const available = getRamAvailable(item);
return <Percent used={used} available={available} />;
}
function getRamUsed(item, metrics) {
const usage = getUsage(item, metrics);
if (!usage) return null;
const totalUsed = parseRam(usage.memory);
const totalAvailable = parseRam(item.status.capacity.memory);
const percent = _.round(totalUsed / totalAvailable * 100, 1);
return parseRam(usage.memory);
}
function getRamAvailable(item) {
return parseRam(item.status.capacity.memory);
}
function Percent({used, available}) {
const percent = _.round(used / available * 100, 1);
return (<span>{`${percent}%`}</span>);
}
function getUsage(item, metrics) {
if (!item || !metrics) return null;
const result = metrics[item.metadata.name] || {};
return result && result.usage;
}
function getNodeMetrics(nodes, metrics) {
if (!nodes || !metrics) return null;
+22 -3
View File
@@ -27,9 +27,11 @@ const target = kc.getCurrentCluster().server;
const agent = new https.Agent({ca: opts.ca});
const proxySettings = {target, agent, secure: false, ws: true, onError};
logClusterInfo();
const app = express();
app.disable('x-powered-by'); // for security reasons, best not to tell attackers too much about our backend
app.use(cors());
app.use(cors()); // TODO: remove in production builds
app.use(logging);
app.use('/', express.static('public'));
@@ -79,11 +81,11 @@ function handleErrors(err, req, res, next) {
next();
}
async function getOidcEndpoint(redirectUri) { // TODO: gotta pass this in from
async function getOidcEndpoint() {
if (!OIDC_URL) return;
const provider = await getOidcProvider();
return provider.authorizationUrl({redirect_uri: redirectUri, scope: OIDC_SCOPES});
return provider.authorizationUrl({scope: OIDC_SCOPES});
}
async function oidcAuthenticate(code, redirectUri) {
@@ -96,3 +98,20 @@ async function getOidcProvider() {
const issuer = await Issuer.discover(OIDC_URL);
return new issuer.Client({client_id: OIDC_CLIENT_ID, client_secret: OIDC_SECRET});
}
async function logClusterInfo() {
try {
const versionClient = kc.makeApiClient(k8s.VersionApi);
const versionResponse = await versionClient.getCode();
const versionJson = JSON.stringify(versionResponse.body, null, 4);
console.log('Version Info: ', versionJson);
const apisClient = kc.makeApiClient(k8s.ApisApi);
const apisResponse = await apisClient.getAPIVersions();
const apis = apisResponse.body.groups.map(x => x.preferredVersion.groupVersion).sort();
const apisJson = JSON.stringify(apis, null, 4);
console.log('Available APIs:', apisJson);
} catch (err) {
console.error('Error getting cluster info', err);
}
}