mirror of
https://github.com/DI0IK/homepage-plus.git
synced 2025-07-14 00:40:30 +00:00
Merge pull request #448 from jameswynn/kubernetes
Support for Kubernetes and Longhorn
This commit is contained in:
commit
627ce179ef
33 changed files with 1789 additions and 43 deletions
110
src/pages/api/kubernetes/stats/[...service].js
Normal file
110
src/pages/api/kubernetes/stats/[...service].js
Normal file
|
@ -0,0 +1,110 @@
|
|||
import { CoreV1Api, Metrics } from "@kubernetes/client-node";
|
||||
|
||||
import getKubeConfig from "../../../../utils/config/kubernetes";
|
||||
import { parseCpu, parseMemory } from "../../../../utils/kubernetes/kubernetes-utils";
|
||||
import createLogger from "../../../../utils/logger";
|
||||
|
||||
const logger = createLogger("kubernetesStatsService");
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const APP_LABEL = "app.kubernetes.io/name";
|
||||
const { service, podSelector } = req.query;
|
||||
|
||||
const [namespace, appName] = service;
|
||||
if (!namespace && !appName) {
|
||||
res.status(400).send({
|
||||
error: "kubernetes query parameters are required"
|
||||
});
|
||||
return;
|
||||
}
|
||||
const labelSelector = podSelector !== undefined ? podSelector : `${APP_LABEL}=${appName}`;
|
||||
|
||||
try {
|
||||
const kc = getKubeConfig();
|
||||
if (!kc) {
|
||||
res.status(500).send({
|
||||
error: "No kubernetes configuration"
|
||||
});
|
||||
return;
|
||||
}
|
||||
const coreApi = kc.makeApiClient(CoreV1Api);
|
||||
const metricsApi = new Metrics(kc);
|
||||
const podsResponse = await coreApi.listNamespacedPod(namespace, null, null, null, null, labelSelector)
|
||||
.then((response) => response.body)
|
||||
.catch((err) => {
|
||||
logger.error("Error getting pods: %d %s %s", err.statusCode, err.body, err.response);
|
||||
return null;
|
||||
});
|
||||
if (!podsResponse) {
|
||||
res.status(500).send({
|
||||
error: "Error communicating with kubernetes"
|
||||
});
|
||||
return;
|
||||
}
|
||||
const pods = podsResponse.items;
|
||||
|
||||
if (pods.length === 0) {
|
||||
res.status(404).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 podStatsList = await Promise.all(pods.map(async (pod) => {
|
||||
let depMem = 0;
|
||||
let depCpu = 0;
|
||||
const podMetrics = await metricsApi.getPodMetrics(namespace, pod.metadata.name)
|
||||
.then((response) => response)
|
||||
.catch((err) => {
|
||||
// 404 generally means that the metrics have not been populated yet
|
||||
if (err.statusCode !== 404) {
|
||||
logger.error("Error getting pod metrics: %d %s %s", err.statusCode, err.body, err.response);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
if (podMetrics) {
|
||||
podMetrics.containers.forEach((container) => {
|
||||
depMem += parseMemory(container.usage.memory);
|
||||
depCpu += parseCpu(container.usage.cpu);
|
||||
});
|
||||
}
|
||||
return {
|
||||
mem: depMem,
|
||||
cpu: depCpu
|
||||
};
|
||||
}));
|
||||
const stats = {
|
||||
mem: 0,
|
||||
cpu: 0
|
||||
}
|
||||
podStatsList.forEach((podStat) => {
|
||||
stats.mem += podStat.mem;
|
||||
stats.cpu += podStat.cpu;
|
||||
});
|
||||
stats.cpuLimit = cpuLimit;
|
||||
stats.memLimit = memLimit;
|
||||
stats.cpuUsage = cpuLimit ? stats.cpu / cpuLimit : 0;
|
||||
stats.memUsage = memLimit ? stats.mem / memLimit : 0;
|
||||
res.status(200).json({
|
||||
stats
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
res.status(500).send({
|
||||
error: "unknown error"
|
||||
});
|
||||
}
|
||||
}
|
66
src/pages/api/kubernetes/status/[...service].js
Normal file
66
src/pages/api/kubernetes/status/[...service].js
Normal file
|
@ -0,0 +1,66 @@
|
|||
import { CoreV1Api } from "@kubernetes/client-node";
|
||||
|
||||
import getKubeConfig from "../../../../utils/config/kubernetes";
|
||||
import createLogger from "../../../../utils/logger";
|
||||
|
||||
const logger = createLogger("kubernetesStatusService");
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const APP_LABEL = "app.kubernetes.io/name";
|
||||
const { service, podSelector } = req.query;
|
||||
|
||||
const [namespace, appName] = service;
|
||||
if (!namespace && !appName) {
|
||||
res.status(400).send({
|
||||
error: "kubernetes query parameters are required",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const labelSelector = podSelector !== undefined ? podSelector : `${APP_LABEL}=${appName}`;
|
||||
try {
|
||||
const kc = getKubeConfig();
|
||||
if (!kc) {
|
||||
res.status(500).send({
|
||||
error: "No kubernetes configuration"
|
||||
});
|
||||
return;
|
||||
}
|
||||
const coreApi = kc.makeApiClient(CoreV1Api);
|
||||
const podsResponse = await coreApi.listNamespacedPod(namespace, null, null, null, null, labelSelector)
|
||||
.then((response) => response.body)
|
||||
.catch((err) => {
|
||||
logger.error("Error getting pods: %d %s %s", err.statusCode, err.body, err.response);
|
||||
return null;
|
||||
});
|
||||
if (!podsResponse) {
|
||||
res.status(500).send({
|
||||
error: "Error communicating with kubernetes"
|
||||
});
|
||||
return;
|
||||
}
|
||||
const pods = podsResponse.items;
|
||||
|
||||
if (pods.length === 0) {
|
||||
res.status(404).send({
|
||||
error: "not found",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const someReady = pods.find(pod => pod.status.phase === "Running");
|
||||
const allReady = pods.every((pod) => pod.status.phase === "Running");
|
||||
let status = "down";
|
||||
if (allReady) {
|
||||
status = "running";
|
||||
} else if (someReady) {
|
||||
status = "partial";
|
||||
}
|
||||
res.status(200).json({
|
||||
status
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
res.status(500).send({
|
||||
error: "unknown error",
|
||||
});
|
||||
}
|
||||
}
|
92
src/pages/api/widgets/kubernetes.js
Normal file
92
src/pages/api/widgets/kubernetes.js
Normal file
|
@ -0,0 +1,92 @@
|
|||
import { CoreV1Api, Metrics } from "@kubernetes/client-node";
|
||||
|
||||
import getKubeConfig from "../../../utils/config/kubernetes";
|
||||
import { parseCpu, parseMemory } from "../../../utils/kubernetes/kubernetes-utils";
|
||||
import createLogger from "../../../utils/logger";
|
||||
|
||||
const logger = createLogger("kubernetes-widget");
|
||||
|
||||
export default async function handler(req, res) {
|
||||
try {
|
||||
const kc = getKubeConfig();
|
||||
if (!kc) {
|
||||
return res.status(500).send({
|
||||
error: "No kubernetes configuration"
|
||||
});
|
||||
}
|
||||
const coreApi = kc.makeApiClient(CoreV1Api);
|
||||
const metricsApi = new Metrics(kc);
|
||||
|
||||
const nodes = await coreApi.listNode()
|
||||
.then((response) => response.body)
|
||||
.catch((error) => {
|
||||
logger.error("Error getting ingresses: %d %s %s", error.statusCode, error.body, error.response);
|
||||
return null;
|
||||
});
|
||||
if (!nodes) {
|
||||
return res.status(500).send({
|
||||
error: "unknown error"
|
||||
});
|
||||
}
|
||||
let cpuTotal = 0;
|
||||
let cpuUsage = 0;
|
||||
let memTotal = 0;
|
||||
let memUsage = 0;
|
||||
|
||||
const nodeMap = {};
|
||||
nodes.items.forEach((node) => {
|
||||
const cpu = Number.parseInt(node.status.capacity.cpu, 10);
|
||||
const mem = parseMemory(node.status.capacity.memory);
|
||||
const ready = node.status.conditions.filter(condition => condition.type === "Ready" && condition.status === "True").length > 0;
|
||||
nodeMap[node.metadata.name] = {
|
||||
name: node.metadata.name,
|
||||
ready,
|
||||
cpu: {
|
||||
total: cpu
|
||||
},
|
||||
memory: {
|
||||
total: mem
|
||||
}
|
||||
};
|
||||
cpuTotal += cpu;
|
||||
memTotal += mem;
|
||||
});
|
||||
|
||||
const nodeMetrics = await metricsApi.getNodeMetrics();
|
||||
nodeMetrics.items.forEach((nodeMetric) => {
|
||||
const cpu = parseCpu(nodeMetric.usage.cpu);
|
||||
const mem = parseMemory(nodeMetric.usage.memory);
|
||||
cpuUsage += cpu;
|
||||
memUsage += mem;
|
||||
nodeMap[nodeMetric.metadata.name].cpu.load = cpu;
|
||||
nodeMap[nodeMetric.metadata.name].cpu.percent = (cpu / nodeMap[nodeMetric.metadata.name].cpu.total) * 100;
|
||||
nodeMap[nodeMetric.metadata.name].memory.used = mem;
|
||||
nodeMap[nodeMetric.metadata.name].memory.free = nodeMap[nodeMetric.metadata.name].memory.total - mem;
|
||||
nodeMap[nodeMetric.metadata.name].memory.percent = (mem / nodeMap[nodeMetric.metadata.name].memory.total) * 100;
|
||||
});
|
||||
|
||||
const cluster = {
|
||||
cpu: {
|
||||
load: cpuUsage,
|
||||
total: cpuTotal,
|
||||
percent: (cpuUsage / cpuTotal) * 100
|
||||
},
|
||||
memory: {
|
||||
used: memUsage,
|
||||
total: memTotal,
|
||||
free: (memTotal - memUsage),
|
||||
percent: (memUsage / memTotal) * 100
|
||||
}
|
||||
};
|
||||
|
||||
return res.status(200).json({
|
||||
cluster,
|
||||
nodes: Object.entries(nodeMap).map(([name, node]) => ({ name, ...node }))
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error("exception %s", e);
|
||||
return res.status(500).send({
|
||||
error: "unknown error"
|
||||
});
|
||||
}
|
||||
}
|
84
src/pages/api/widgets/longhorn.js
Normal file
84
src/pages/api/widgets/longhorn.js
Normal file
|
@ -0,0 +1,84 @@
|
|||
import { httpProxy } from "../../../utils/proxy/http";
|
||||
import createLogger from "../../../utils/logger";
|
||||
import { getSettings } from "../../../utils/config/config";
|
||||
|
||||
const logger = createLogger("longhorn");
|
||||
|
||||
function parseLonghornData(data) {
|
||||
const json = JSON.parse(data);
|
||||
|
||||
if (!json) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nodes = json.data.map((node) => {
|
||||
let available = 0;
|
||||
let maximum = 0;
|
||||
let reserved = 0;
|
||||
let scheduled = 0;
|
||||
if (node.disks) {
|
||||
Object.keys(node.disks).forEach((diskKey) => {
|
||||
const disk = node.disks[diskKey];
|
||||
available += disk.storageAvailable;
|
||||
maximum += disk.storageMaximum;
|
||||
reserved += disk.storageReserved;
|
||||
scheduled += disk.storageScheduled;
|
||||
});
|
||||
}
|
||||
return {
|
||||
id: node.id,
|
||||
available,
|
||||
maximum,
|
||||
reserved,
|
||||
scheduled,
|
||||
};
|
||||
});
|
||||
const total = nodes.reduce((summary, node) => ({
|
||||
available: summary.available + node.available,
|
||||
maximum: summary.maximum + node.maximum,
|
||||
reserved: summary.reserved + node.reserved,
|
||||
scheduled: summary.scheduled + node.scheduled,
|
||||
}));
|
||||
total.id = "total";
|
||||
nodes.push(total);
|
||||
return nodes;
|
||||
}
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const settings = getSettings();
|
||||
const longhornSettings = settings?.providers?.longhorn;
|
||||
const {url, username, password} = longhornSettings;
|
||||
|
||||
if (!url) {
|
||||
const errorMessage = "Missing Longhorn URL";
|
||||
logger.error(errorMessage);
|
||||
return res.status(400).json({ error: errorMessage });
|
||||
}
|
||||
|
||||
const apiUrl = `${url}/v1/nodes`;
|
||||
const headers = {
|
||||
"Accept-Encoding": "application/json"
|
||||
};
|
||||
if (username && password) {
|
||||
headers.Authorization = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`
|
||||
}
|
||||
const params = { method: "GET", headers };
|
||||
|
||||
const [status, contentType, data] = await httpProxy(apiUrl, params);
|
||||
|
||||
if (status === 401) {
|
||||
logger.error("Authorization failure getting data from Longhorn API. Data: %s", data);
|
||||
}
|
||||
|
||||
if (status !== 200) {
|
||||
logger.error("HTTP %d getting data from Longhorn API. Data: %s", status, data);
|
||||
}
|
||||
|
||||
if (contentType) res.setHeader("Content-Type", contentType);
|
||||
|
||||
const nodes = parseLonghornData(data);
|
||||
|
||||
return res.status(200).json({
|
||||
nodes,
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue