mirror of
https://github.com/skooner-k8s/skooner.git
synced 2026-08-24 04:16:18 +00:00
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:
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user