Kubernetes support
* 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 yet
This commit is contained in:
parent
b25ba09e18
commit
c4333fd2dc
18 changed files with 479 additions and 19 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -41,3 +41,6 @@ next-env.d.ts
|
|||
|
||||
# homepage
|
||||
/config
|
||||
|
||||
# idea
|
||||
.idea/
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.7.2",
|
||||
"@kubernetes/client-node": "^0.17.1",
|
||||
"classnames": "^2.3.2",
|
||||
"compare-versions": "^5.0.1",
|
||||
"dockerode": "^3.3.4",
|
||||
|
|
|
@ -3,8 +3,10 @@ import { useContext, useState } from "react";
|
|||
|
||||
import Status from "./status";
|
||||
import Widget from "./widget";
|
||||
import KubernetesStatus from "./kubernetes-status";
|
||||
|
||||
import Docker from "widgets/docker/component";
|
||||
import Kubernetes from "widgets/kubernetes/component";
|
||||
import { SettingsContext } from "utils/contexts/settings";
|
||||
import ResolvedIcon from "components/resolvedicon";
|
||||
|
||||
|
@ -80,6 +82,16 @@ export default function Item({ service }) {
|
|||
<span className="sr-only">View container stats</span>
|
||||
</button>
|
||||
)}
|
||||
{service.app && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => (statsOpen ? closeStats() : setStatsOpen(true))}
|
||||
className="flex-shrink-0 flex items-center justify-center w-12 cursor-pointer"
|
||||
>
|
||||
<KubernetesStatus service={service} />
|
||||
<span className="sr-only">View container stats</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{service.container && service.server && (
|
||||
|
@ -92,6 +104,16 @@ export default function Item({ service }) {
|
|||
{statsOpen && <Docker service={{ widget: { container: service.container, server: service.server } }} />}
|
||||
</div>
|
||||
)}
|
||||
{service.app && (
|
||||
<div
|
||||
className={classNames(
|
||||
statsOpen && !statsClosing ? "max-h-[55px] opacity-100" : " max-h-[0] opacity-0",
|
||||
"w-full overflow-hidden transition-all duration-300 ease-in-out"
|
||||
)}
|
||||
>
|
||||
{statsOpen && <Kubernetes service={{ widget: { namespace: service.namespace, app: service.app } }} />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{service.widget && <Widget service={service} />}
|
||||
</div>
|
||||
|
|
19
src/components/services/kubernetes-status.jsx
Normal file
19
src/components/services/kubernetes-status.jsx
Normal file
|
@ -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" />;
|
||||
}
|
|
@ -5,10 +5,10 @@ import { useTranslation } from "next-i18next";
|
|||
|
||||
import UsageBar from "./usage-bar";
|
||||
|
||||
export default function Cpu({ expanded }) {
|
||||
export default function Cpu({ expanded, backend }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data, error } = useSWR(`/api/widgets/resources?type=cpu`, {
|
||||
const { data, error } = useSWR(`/api/widgets/${backend || 'resources'}?type=cpu`, {
|
||||
refreshInterval: 1500,
|
||||
});
|
||||
|
||||
|
|
|
@ -5,10 +5,10 @@ import { useTranslation } from "next-i18next";
|
|||
|
||||
import UsageBar from "./usage-bar";
|
||||
|
||||
export default function Disk({ options, expanded }) {
|
||||
export default function Disk({ options, expanded, backend }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data, error } = useSWR(`/api/widgets/resources?type=disk&target=${options.disk}`, {
|
||||
const { data, error } = useSWR(`/api/widgets/${backend || 'resources'}?type=disk&target=${options.disk}`, {
|
||||
refreshInterval: 1500,
|
||||
});
|
||||
|
||||
|
|
|
@ -5,10 +5,10 @@ import { useTranslation } from "next-i18next";
|
|||
|
||||
import UsageBar from "./usage-bar";
|
||||
|
||||
export default function Memory({ expanded }) {
|
||||
export default function Memory({ expanded, backend }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data, error } = useSWR(`/api/widgets/resources?type=memory`, {
|
||||
const { data, error } = useSWR(`/api/widgets/${backend || 'resources'}?type=memory`, {
|
||||
refreshInterval: 1500,
|
||||
});
|
||||
|
||||
|
|
|
@ -3,15 +3,15 @@ import Cpu from "./cpu";
|
|||
import Memory from "./memory";
|
||||
|
||||
export default function Resources({ options }) {
|
||||
const { expanded } = options;
|
||||
const { expanded, backend } = options;
|
||||
return (
|
||||
<div className="flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap">
|
||||
<div className="flex flex-row self-center flex-wrap justify-between">
|
||||
{options.cpu && <Cpu expanded={expanded} />}
|
||||
{options.memory && <Memory expanded={expanded} />}
|
||||
{options.cpu && <Cpu expanded={expanded} backend={backend} />}
|
||||
{options.memory && <Memory expanded={expanded} backend={backend} />}
|
||||
{Array.isArray(options.disk)
|
||||
? options.disk.map((disk) => <Disk key={disk} options={{ disk }} expanded={expanded} />)
|
||||
: options.disk && <Disk options={options} expanded={expanded} />}
|
||||
? options.disk.map((disk) => <Disk key={disk} options={{ disk }} expanded={expanded} backend={backend} />)
|
||||
: options.disk && <Disk options={options} expanded={expanded} backend={backend} />}
|
||||
</div>
|
||||
{options.label && (
|
||||
<div className="ml-6 pt-1 text-center text-theme-800 dark:text-theme-200 text-xs">{options.label}</div>
|
||||
|
|
79
src/pages/api/kubernetes/stats/[...service].js
Normal file
79
src/pages/api/kubernetes/stats/[...service].js
Normal file
|
@ -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",
|
||||
});
|
||||
}
|
||||
}
|
42
src/pages/api/kubernetes/status/[...service].js
Normal file
42
src/pages/api/kubernetes/status/[...service].js
Normal file
|
@ -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",
|
||||
});
|
||||
}
|
||||
}
|
72
src/pages/api/widgets/kubernetes.js
Normal file
72
src/pages/api/widgets/kubernetes.js
Normal file
|
@ -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"
|
||||
});
|
||||
}
|
2
src/skeleton/kubernetes.yaml
Normal file
2
src/skeleton/kubernetes.yaml
Normal file
|
@ -0,0 +1,2 @@
|
|||
---
|
||||
# sample kubernetes config
|
|
@ -5,7 +5,12 @@ import path from "path";
|
|||
import yaml from "js-yaml";
|
||||
|
||||
import checkAndCopyConfig from "utils/config/config";
|
||||
import { servicesFromConfig, servicesFromDocker, cleanServiceGroups } from "utils/config/service-helpers";
|
||||
import {
|
||||
servicesFromConfig,
|
||||
servicesFromDocker,
|
||||
cleanServiceGroups,
|
||||
servicesFromKubernetes
|
||||
} from "utils/config/service-helpers";
|
||||
import { cleanWidgetGroups, widgetsFromConfig } from "utils/config/widget-helpers";
|
||||
|
||||
export async function bookmarksResponse() {
|
||||
|
@ -44,15 +49,24 @@ export async function widgetsResponse() {
|
|||
}
|
||||
|
||||
export async function servicesResponse() {
|
||||
let discoveredServices;
|
||||
let discoveredDockerServices;
|
||||
let discoveredKubernetesServices;
|
||||
let configuredServices;
|
||||
|
||||
try {
|
||||
discoveredServices = cleanServiceGroups(await servicesFromDocker());
|
||||
discoveredDockerServices = cleanServiceGroups(await servicesFromDocker());
|
||||
} catch (e) {
|
||||
console.error("Failed to discover services, please check docker.yaml for errors or remove example entries.");
|
||||
if (e) console.error(e);
|
||||
discoveredServices = [];
|
||||
discoveredDockerServices = [];
|
||||
}
|
||||
|
||||
try {
|
||||
discoveredKubernetesServices = cleanServiceGroups(await servicesFromKubernetes());
|
||||
} catch (e) {
|
||||
console.error("Failed to discover services, please check docker.yaml for errors or remove example entries.");
|
||||
if (e) console.error(e);
|
||||
discoveredKubernetesServices = [];
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -64,18 +78,27 @@ export async function servicesResponse() {
|
|||
}
|
||||
|
||||
const mergedGroupsNames = [
|
||||
...new Set([discoveredServices.map((group) => group.name), configuredServices.map((group) => group.name)].flat()),
|
||||
...new Set([
|
||||
discoveredDockerServices.map((group) => group.name),
|
||||
discoveredKubernetesServices.map((group) => group.name),
|
||||
configuredServices.map((group) => group.name),
|
||||
].flat()),
|
||||
];
|
||||
|
||||
const mergedGroups = [];
|
||||
|
||||
mergedGroupsNames.forEach((groupName) => {
|
||||
const discoveredGroup = discoveredServices.find((group) => group.name === groupName) || { services: [] };
|
||||
const discoveredDockerGroup = discoveredDockerServices.find((group) => group.name === groupName) || { services: [] };
|
||||
const discoveredKubernetesGroup = discoveredKubernetesServices.find((group) => group.name === groupName) || { services: [] };
|
||||
const configuredGroup = configuredServices.find((group) => group.name === groupName) || { services: [] };
|
||||
|
||||
const mergedGroup = {
|
||||
name: groupName,
|
||||
services: [...discoveredGroup.services, ...configuredGroup.services].filter((service) => service),
|
||||
services: [
|
||||
...discoveredDockerGroup.services,
|
||||
...discoveredKubernetesGroup.services,
|
||||
...configuredGroup.services
|
||||
].filter((service) => service),
|
||||
};
|
||||
|
||||
mergedGroups.push(mergedGroup);
|
||||
|
|
27
src/utils/config/kubernetes.js
Normal file
27
src/utils/config/kubernetes.js
Normal file
|
@ -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;
|
||||
}
|
|
@ -4,9 +4,11 @@ import path from "path";
|
|||
import yaml from "js-yaml";
|
||||
import Docker from "dockerode";
|
||||
import * as shvl from "shvl";
|
||||
import { NetworkingV1Api } from "@kubernetes/client-node";
|
||||
|
||||
import checkAndCopyConfig from "utils/config/config";
|
||||
import getDockerArguments from "utils/config/docker";
|
||||
import getKubeConfig from "utils/config/kubernetes";
|
||||
|
||||
export async function servicesFromConfig() {
|
||||
checkAndCopyConfig("services.yaml");
|
||||
|
@ -103,6 +105,56 @@ export async function servicesFromDocker() {
|
|||
return mappedServiceGroups;
|
||||
}
|
||||
|
||||
export async function servicesFromKubernetes() {
|
||||
checkAndCopyConfig("kubernetes.yaml");
|
||||
|
||||
const kc = getKubeConfig();
|
||||
const networking = kc.makeApiClient(NetworkingV1Api);
|
||||
|
||||
const ingressResponse = await networking.listIngressForAllNamespaces(null, null, null, "homepage/enabled=true");
|
||||
const services = ingressResponse.body.items.map((ingress) => {
|
||||
const constructedService = {
|
||||
app: ingress.metadata.name,
|
||||
namespace: ingress.metadata.namespace,
|
||||
href: `https://${ingress.spec.rules[0].host}`,
|
||||
name: ingress.metadata.annotations['homepage/name'],
|
||||
group: ingress.metadata.annotations['homepage/group'],
|
||||
icon: ingress.metadata.annotations['homepage/icon'],
|
||||
description: ingress.metadata.annotations['homepage/description']
|
||||
};
|
||||
Object.keys(ingress.metadata.labels).forEach((label) => {
|
||||
if (label.startsWith("homepage/widget/")) {
|
||||
shvl.set(constructedService, label.replace("homepage/widget/", ""), ingress.metadata.labels[label]);
|
||||
}
|
||||
});
|
||||
|
||||
return constructedService;
|
||||
});
|
||||
|
||||
const mappedServiceGroups = [];
|
||||
|
||||
services.forEach((serverService) => {
|
||||
let serverGroup = mappedServiceGroups.find((searchedGroup) => searchedGroup.name === serverService.group);
|
||||
if (!serverGroup) {
|
||||
mappedServiceGroups.push({
|
||||
name: serverService.group,
|
||||
services: [],
|
||||
});
|
||||
serverGroup = mappedServiceGroups[mappedServiceGroups.length - 1];
|
||||
}
|
||||
|
||||
const { name: serviceName, group: serverServiceGroup, ...pushedService } = serverService;
|
||||
const result = {
|
||||
name: serviceName,
|
||||
...pushedService,
|
||||
};
|
||||
|
||||
serverGroup.services.push(result);
|
||||
});
|
||||
|
||||
return mappedServiceGroups;
|
||||
}
|
||||
|
||||
export function cleanServiceGroups(groups) {
|
||||
return groups.map((serviceGroup) => ({
|
||||
name: serviceGroup.name,
|
||||
|
@ -118,6 +170,8 @@ export function cleanServiceGroups(groups) {
|
|||
container,
|
||||
currency, // coinmarketcap widget
|
||||
symbols,
|
||||
namespace, // kubernetes widget
|
||||
app
|
||||
} = cleanedService.widget;
|
||||
|
||||
cleanedService.widget = {
|
||||
|
@ -134,6 +188,10 @@ export function cleanServiceGroups(groups) {
|
|||
if (server) cleanedService.widget.server = server;
|
||||
if (container) cleanedService.widget.container = container;
|
||||
}
|
||||
if (type === "kubernetes") {
|
||||
if (namespace) cleanedService.widget.namespace = namespace;
|
||||
if (app) cleanedService.widget.app = app;
|
||||
}
|
||||
}
|
||||
|
||||
return cleanedService;
|
||||
|
@ -164,5 +222,15 @@ export default async function getServiceWidget(group, service) {
|
|||
}
|
||||
}
|
||||
|
||||
const kubernetesServices = await servicesFromKubernetes();
|
||||
const kubernetesServiceGroup = kubernetesServices.find((g) => g.name === group);
|
||||
if (kubernetesServiceGroup) {
|
||||
const kubernetesServiceEntry = kubernetesServiceGroup.services.find((s) => s.name === service);
|
||||
if (kubernetesServiceEntry) {
|
||||
const { widget } = kubernetesServiceEntry;
|
||||
return widget;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
47
src/utils/kubernetes/kubernetes-utils.js
Normal file
47
src/utils/kubernetes/kubernetes-utils.js
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ const components = {
|
|||
changedetectionio: dynamic(() => import("./changedetectionio/component")),
|
||||
coinmarketcap: dynamic(() => import("./coinmarketcap/component")),
|
||||
docker: dynamic(() => import("./docker/component")),
|
||||
kubernetes: dynamic(() => import("./kubernetes/component")),
|
||||
emby: dynamic(() => import("./emby/component")),
|
||||
gotify: dynamic(() => import("./gotify/component")),
|
||||
homebridge: dynamic(() => import("./homebridge/component")),
|
||||
|
|
54
src/widgets/kubernetes/component.jsx
Normal file
54
src/widgets/kubernetes/component.jsx
Normal file
|
@ -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…
Add table
Reference in a new issue