* Total CPU and Memory usage for the entire cluster * Total CPU and Memory usage for kubernetes pods * Service discovery via annotations on ingress * No storage stats yet * No network stats yetpull/448/head
parent
b25ba09e18
commit
c4333fd2dc
@ -0,0 +1,19 @@
|
||||
import useSWR from "swr";
|
||||
|
||||
export default function KubernetesStatus({ service }) {
|
||||
const { data, error } = useSWR(`/api/kubernetes/status/${service.namespace}/${service.app}`);
|
||||
|
||||
if (error) {
|
||||
return <div className="w-3 h-3 bg-rose-300 dark:bg-rose-500 rounded-full" />;
|
||||
}
|
||||
|
||||
if (data && data.status === "running") {
|
||||
return <div className="w-3 h-3 bg-emerald-300 dark:bg-emerald-500 rounded-full" />;
|
||||
}
|
||||
|
||||
if (data && data.status === "not found") {
|
||||
return <div className="h-2.5 w-2.5 bg-orange-400/50 dark:bg-yellow-200/40 -rotate-45" />;
|
||||
}
|
||||
|
||||
return <div className="w-3 h-3 bg-black/20 dark:bg-white/40 rounded-full" />;
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
import { CoreV1Api, Metrics } from "@kubernetes/client-node";
|
||||
|
||||
import getKubeConfig from "../../../../utils/config/kubernetes";
|
||||
import { parseCpu, parseMemory } from "../../../../utils/kubernetes/kubernetes-utils";
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const APP_LABEL = "app.kubernetes.io/name";
|
||||
const { service } = req.query;
|
||||
|
||||
const [namespace, appName] = service;
|
||||
if (!namespace && !appName) {
|
||||
res.status(400).send({
|
||||
error: "kubernetes query parameters are required",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const labelSelector = `${APP_LABEL}=${appName}`;
|
||||
|
||||
try {
|
||||
const kc = getKubeConfig();
|
||||
const coreApi = kc.makeApiClient(CoreV1Api);
|
||||
const metricsApi = new Metrics(kc);
|
||||
const podsResponse = await coreApi.listNamespacedPod(namespace, null, null, null, null, labelSelector);
|
||||
const pods = podsResponse.body.items;
|
||||
|
||||
if (pods.length === 0) {
|
||||
res.status(200).send({
|
||||
error: "not found",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let cpuLimit = 0;
|
||||
let memLimit = 0;
|
||||
pods.forEach((pod) => {
|
||||
pod.spec.containers.forEach((container) => {
|
||||
if (container?.resources?.limits?.cpu) {
|
||||
cpuLimit += parseCpu(container?.resources?.limits?.cpu);
|
||||
}
|
||||
if (container?.resources?.limits?.memory) {
|
||||
memLimit += parseMemory(container?.resources?.limits?.memory);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const stats = await pods.map(async (pod) => {
|
||||
let depMem = 0;
|
||||
let depCpu = 0;
|
||||
const podMetrics = await metricsApi.getPodMetrics(namespace, pod.metadata.name);
|
||||
podMetrics.containers.forEach((container) => {
|
||||
depMem += parseMemory(container.usage.memory);
|
||||
depCpu += parseCpu(container.usage.cpu);
|
||||
});
|
||||
return {
|
||||
mem: depMem,
|
||||
cpu: depCpu
|
||||
}
|
||||
}).reduce(async (finalStats, podStatPromise) => {
|
||||
const podStats = await podStatPromise;
|
||||
return {
|
||||
mem: finalStats.mem + podStats.mem,
|
||||
cpu: finalStats.cpu + podStats.cpu
|
||||
};
|
||||
});
|
||||
stats.cpuLimit = cpuLimit;
|
||||
stats.memLimit = memLimit;
|
||||
stats.cpuUsage = stats.cpu / cpuLimit;
|
||||
stats.memUsage = stats.mem / memLimit;
|
||||
|
||||
res.status(200).json({
|
||||
stats,
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("error", e);
|
||||
res.status(500).send({
|
||||
error: "unknown error",
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
import { CoreV1Api } from "@kubernetes/client-node";
|
||||
|
||||
import getKubeConfig from "../../../../utils/config/kubernetes";
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const APP_LABEL = "app.kubernetes.io/name";
|
||||
const { service } = req.query;
|
||||
|
||||
const [namespace, appName] = service;
|
||||
if (!namespace && !appName) {
|
||||
res.status(400).send({
|
||||
error: "kubernetes query parameters are required",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const labelSelector = `${APP_LABEL}=${appName}`;
|
||||
|
||||
try {
|
||||
const kc = getKubeConfig();
|
||||
const coreApi = kc.makeApiClient(CoreV1Api);
|
||||
const podsResponse = await coreApi.listNamespacedPod(namespace, null, null, null, null, labelSelector);
|
||||
const pods = podsResponse.body.items;
|
||||
|
||||
if (pods.length === 0) {
|
||||
res.status(200).send({
|
||||
error: "not found",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// at least one pod must be in the "Running" phase, otherwise its "down"
|
||||
const runningPod = pods.find(pod => pod.status.phase === "Running");
|
||||
const status = runningPod ? "running" : "down";
|
||||
res.status(200).json({
|
||||
status
|
||||
});
|
||||
} catch {
|
||||
res.status(500).send({
|
||||
error: "unknown error",
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
import { CoreV1Api, Metrics } from "@kubernetes/client-node";
|
||||
|
||||
import getKubeConfig from "../../../utils/config/kubernetes";
|
||||
import { parseCpu, parseMemory } from "../../../utils/kubernetes/kubernetes-utils";
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const { type } = req.query;
|
||||
|
||||
const kc = getKubeConfig();
|
||||
const coreApi = kc.makeApiClient(CoreV1Api);
|
||||
const metricsApi = new Metrics(kc);
|
||||
|
||||
const nodes = await coreApi.listNode();
|
||||
const nodeCapacity = new Map();
|
||||
let cpuTotal = 0;
|
||||
let cpuUsage = 0;
|
||||
let memTotal = 0;
|
||||
let memUsage = 0;
|
||||
|
||||
nodes.body.items.forEach((node) => {
|
||||
nodeCapacity.set(node.metadata.name, node.status.capacity);
|
||||
cpuTotal += Number.parseInt(node.status.capacity.cpu, 10);
|
||||
memTotal += parseMemory(node.status.capacity.memory);
|
||||
});
|
||||
|
||||
const nodeMetrics = await metricsApi.getNodeMetrics();
|
||||
const nodeUsage = new Map();
|
||||
nodeMetrics.items.forEach((metrics) => {
|
||||
nodeUsage.set(metrics.metadata.name, metrics.usage);
|
||||
cpuUsage += parseCpu(metrics.usage.cpu);
|
||||
memUsage += parseMemory(metrics.usage.memory);
|
||||
});
|
||||
|
||||
if (type === "cpu") {
|
||||
return res.status(200).json({
|
||||
cpu: {
|
||||
usage: (cpuUsage / cpuTotal) * 100,
|
||||
load: cpuUsage
|
||||
}
|
||||
});
|
||||
}
|
||||
// Maybe Storage CSI can provide this information
|
||||
// if (type === "disk") {
|
||||
// if (!existsSync(target)) {
|
||||
// return res.status(404).json({
|
||||
// error: "Target not found",
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// return res.status(200).json({
|
||||
// drive: await drive.info(target || "/"),
|
||||
// });
|
||||
// }
|
||||
//
|
||||
if (type === "memory") {
|
||||
const SCALE_MB = 1024 * 1024;
|
||||
const usedMemMb = memUsage / SCALE_MB;
|
||||
const totalMemMb = memTotal / SCALE_MB;
|
||||
const freeMemMb = totalMemMb - usedMemMb;
|
||||
return res.status(200).json({
|
||||
memory: {
|
||||
usedMemMb,
|
||||
freeMemMb,
|
||||
totalMemMb
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(400).json({
|
||||
error: "invalid type"
|
||||
});
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
---
|
||||
# sample kubernetes config
|
@ -0,0 +1,27 @@
|
||||
import path from "path";
|
||||
import { readFileSync } from "fs";
|
||||
|
||||
import yaml from "js-yaml";
|
||||
import { KubeConfig } from "@kubernetes/client-node";
|
||||
|
||||
import checkAndCopyConfig from "utils/config/config";
|
||||
|
||||
export default function getKubeConfig() {
|
||||
checkAndCopyConfig("kubernetes.yaml");
|
||||
|
||||
const configFile = path.join(process.cwd(), "config", "kubernetes.yaml");
|
||||
const configData = readFileSync(configFile, "utf8");
|
||||
const config = yaml.load(configData);
|
||||
const kc = new KubeConfig();
|
||||
|
||||
switch (config?.mode) {
|
||||
case 'cluster':
|
||||
kc.loadFromCluster();
|
||||
break;
|
||||
case 'default':
|
||||
default:
|
||||
kc.loadFromDefault();
|
||||
}
|
||||
|
||||
return kc;
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
export function parseCpu(cpuStr) {
|
||||
const unitLength = 1;
|
||||
const base = Number.parseInt(cpuStr, 10);
|
||||
const units = cpuStr.substring(cpuStr.length - unitLength);
|
||||
// console.log(Number.isNaN(Number(units)), cpuStr, base, units);
|
||||
if (Number.isNaN(Number(units))) {
|
||||
switch (units) {
|
||||
case 'n':
|
||||
return base / 1000000000;
|
||||
case 'u':
|
||||
return base / 1000000;
|
||||
case 'm':
|
||||
return base / 1000;
|
||||
default:
|
||||
return base;
|
||||
}
|
||||
} else {
|
||||
return Number.parseInt(cpuStr, 10);
|
||||
}
|
||||
}
|
||||
|
||||
export function parseMemory(memStr) {
|
||||
const unitLength = (memStr.substring(memStr.length - 1) === 'i' ? 2 : 1);
|
||||
const base = Number.parseInt(memStr, 10);
|
||||
const units = memStr.substring(memStr.length - unitLength);
|
||||
// console.log(Number.isNaN(Number(units)), memStr, base, units);
|
||||
if (Number.isNaN(Number(units))) {
|
||||
switch (units) {
|
||||
case 'Ki':
|
||||
return base * 1000;
|
||||
case 'K':
|
||||
return base * 1024;
|
||||
case 'Mi':
|
||||
return base * 1000000;
|
||||
case 'M':
|
||||
return base * 1024 * 1024;
|
||||
case 'Gi':
|
||||
return base * 1000000000;
|
||||
case 'G':
|
||||
return base * 1024 * 1024 * 1024;
|
||||
default:
|
||||
return base;
|
||||
}
|
||||
} else {
|
||||
return Number.parseInt(memStr, 10);
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
import useSWR from "swr";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import Container from "components/services/widget/container";
|
||||
import Block from "components/services/widget/block";
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { widget } = service;
|
||||
|
||||
const { data: statusData, error: statusError } = useSWR(
|
||||
`/api/kubernetes/status/${widget.namespace}/${widget.app}`);
|
||||
|
||||
const { data: statsData, error: statsError } = useSWR(
|
||||
`/api/kubernetes/stats/${widget.namespace}/${widget.app}`);
|
||||
|
||||
if (statsError || statusError) {
|
||||
return <Container error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (statusData && statusData.status !== "running") {
|
||||
return (
|
||||
<Container>
|
||||
<Block label={t("widget.status")} value={t("docker.offline")} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
if (!statsData || !statusData) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="docker.cpu" />
|
||||
<Block label="docker.mem" />
|
||||
<Block label="docker.rx" />
|
||||
<Block label="docker.tx" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const network = statsData.stats?.networks?.eth0 || statsData.stats?.networks?.network;
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="docker.cpu" value={t("common.percent", { value: statsData.stats.cpuUsage })} />
|
||||
<Block label="docker.mem" value={t("common.bytes", { value: statsData.stats.mem })} />
|
||||
{network && (
|
||||
<>
|
||||
<Block label="docker.rx" value={t("common.bytes", { value: network.rx_bytes })} />
|
||||
<Block label="docker.tx" value={t("common.bytes", { value: network.tx_bytes })} />
|
||||
</>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
Loading…
Reference in new issue