mirror of
https://github.com/karl0ss/homepage.git
synced 2025-04-29 12:03:41 +01:00
Improved kubernetes error handling
This commit is contained in:
parent
8887fcc3ee
commit
4fc6db49ca
@ -1,6 +1,6 @@
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { BiError } from "react-icons/bi";
|
import { BiError } from "react-icons/bi";
|
||||||
import { i18n, useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
|
|
||||||
import Node from "./node";
|
import Node from "./node";
|
||||||
|
|
||||||
|
@ -2,6 +2,9 @@ import { CoreV1Api, Metrics } from "@kubernetes/client-node";
|
|||||||
|
|
||||||
import getKubeConfig from "../../../../utils/config/kubernetes";
|
import getKubeConfig from "../../../../utils/config/kubernetes";
|
||||||
import { parseCpu, parseMemory } from "../../../../utils/kubernetes/kubernetes-utils";
|
import { parseCpu, parseMemory } from "../../../../utils/kubernetes/kubernetes-utils";
|
||||||
|
import createLogger from "../../../../utils/logger";
|
||||||
|
|
||||||
|
const logger = createLogger("kubernetesStatsService");
|
||||||
|
|
||||||
export default async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
const APP_LABEL = "app.kubernetes.io/name";
|
const APP_LABEL = "app.kubernetes.io/name";
|
||||||
@ -10,7 +13,7 @@ export default async function handler(req, res) {
|
|||||||
const [namespace, appName] = service;
|
const [namespace, appName] = service;
|
||||||
if (!namespace && !appName) {
|
if (!namespace && !appName) {
|
||||||
res.status(400).send({
|
res.status(400).send({
|
||||||
error: "kubernetes query parameters are required",
|
error: "kubernetes query parameters are required"
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -20,12 +23,23 @@ export default async function handler(req, res) {
|
|||||||
const kc = getKubeConfig();
|
const kc = getKubeConfig();
|
||||||
const coreApi = kc.makeApiClient(CoreV1Api);
|
const coreApi = kc.makeApiClient(CoreV1Api);
|
||||||
const metricsApi = new Metrics(kc);
|
const metricsApi = new Metrics(kc);
|
||||||
const podsResponse = await coreApi.listNamespacedPod(namespace, null, null, null, null, labelSelector);
|
const podsResponse = await coreApi.listNamespacedPod(namespace, null, null, null, null, labelSelector)
|
||||||
const pods = podsResponse.body.items;
|
.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) {
|
if (pods.length === 0) {
|
||||||
res.status(200).send({
|
res.status(404).send({
|
||||||
error: "not found",
|
error: "not found"
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -46,15 +60,25 @@ export default async function handler(req, res) {
|
|||||||
const stats = await pods.map(async (pod) => {
|
const stats = await pods.map(async (pod) => {
|
||||||
let depMem = 0;
|
let depMem = 0;
|
||||||
let depCpu = 0;
|
let depCpu = 0;
|
||||||
const podMetrics = await metricsApi.getPodMetrics(namespace, pod.metadata.name);
|
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) => {
|
podMetrics.containers.forEach((container) => {
|
||||||
depMem += parseMemory(container.usage.memory);
|
depMem += parseMemory(container.usage.memory);
|
||||||
depCpu += parseCpu(container.usage.cpu);
|
depCpu += parseCpu(container.usage.cpu);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
mem: depMem,
|
mem: depMem,
|
||||||
cpu: depCpu
|
cpu: depCpu
|
||||||
}
|
};
|
||||||
}).reduce(async (finalStats, podStatPromise) => {
|
}).reduce(async (finalStats, podStatPromise) => {
|
||||||
const podStats = await podStatPromise;
|
const podStats = await podStatPromise;
|
||||||
return {
|
return {
|
||||||
@ -64,16 +88,15 @@ export default async function handler(req, res) {
|
|||||||
});
|
});
|
||||||
stats.cpuLimit = cpuLimit;
|
stats.cpuLimit = cpuLimit;
|
||||||
stats.memLimit = memLimit;
|
stats.memLimit = memLimit;
|
||||||
stats.cpuUsage = stats.cpu / cpuLimit;
|
stats.cpuUsage = cpuLimit ? stats.cpu / cpuLimit : 0;
|
||||||
stats.memUsage = stats.mem / memLimit;
|
stats.memUsage = memLimit ? stats.mem / memLimit : 0;
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
stats,
|
stats
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("error", e);
|
logger.error(e);
|
||||||
res.status(500).send({
|
res.status(500).send({
|
||||||
error: "unknown error",
|
error: "unknown error"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import { CoreV1Api } from "@kubernetes/client-node";
|
import { CoreV1Api } from "@kubernetes/client-node";
|
||||||
|
|
||||||
import getKubeConfig from "../../../../utils/config/kubernetes";
|
import getKubeConfig from "../../../../utils/config/kubernetes";
|
||||||
|
import createLogger from "../../../../utils/logger";
|
||||||
|
|
||||||
|
const logger = createLogger("kubernetesStatusService");
|
||||||
|
|
||||||
export default async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
const APP_LABEL = "app.kubernetes.io/name";
|
const APP_LABEL = "app.kubernetes.io/name";
|
||||||
@ -18,11 +21,22 @@ export default async function handler(req, res) {
|
|||||||
try {
|
try {
|
||||||
const kc = getKubeConfig();
|
const kc = getKubeConfig();
|
||||||
const coreApi = kc.makeApiClient(CoreV1Api);
|
const coreApi = kc.makeApiClient(CoreV1Api);
|
||||||
const podsResponse = await coreApi.listNamespacedPod(namespace, null, null, null, null, labelSelector);
|
const podsResponse = await coreApi.listNamespacedPod(namespace, null, null, null, null, labelSelector)
|
||||||
const pods = podsResponse.body.items;
|
.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) {
|
if (pods.length === 0) {
|
||||||
res.status(200).send({
|
res.status(404).send({
|
||||||
error: "not found",
|
error: "not found",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@ -34,7 +48,8 @@ export default async function handler(req, res) {
|
|||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
status
|
status
|
||||||
});
|
});
|
||||||
} catch {
|
} catch (e) {
|
||||||
|
logger.error(e);
|
||||||
res.status(500).send({
|
res.status(500).send({
|
||||||
error: "unknown error",
|
error: "unknown error",
|
||||||
});
|
});
|
||||||
|
@ -2,22 +2,36 @@ import { CoreV1Api, Metrics } from "@kubernetes/client-node";
|
|||||||
|
|
||||||
import getKubeConfig from "../../../utils/config/kubernetes";
|
import getKubeConfig from "../../../utils/config/kubernetes";
|
||||||
import { parseCpu, parseMemory } from "../../../utils/kubernetes/kubernetes-utils";
|
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) {
|
export default async function handler(req, res) {
|
||||||
const { type } = req.query;
|
const { type } = req.query;
|
||||||
|
|
||||||
|
try {
|
||||||
const kc = getKubeConfig();
|
const kc = getKubeConfig();
|
||||||
const coreApi = kc.makeApiClient(CoreV1Api);
|
const coreApi = kc.makeApiClient(CoreV1Api);
|
||||||
const metricsApi = new Metrics(kc);
|
const metricsApi = new Metrics(kc);
|
||||||
|
|
||||||
const nodes = await coreApi.listNode();
|
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"
|
||||||
|
});
|
||||||
|
}
|
||||||
const nodeCapacity = new Map();
|
const nodeCapacity = new Map();
|
||||||
let cpuTotal = 0;
|
let cpuTotal = 0;
|
||||||
let cpuUsage = 0;
|
let cpuUsage = 0;
|
||||||
let memTotal = 0;
|
let memTotal = 0;
|
||||||
let memUsage = 0;
|
let memUsage = 0;
|
||||||
|
|
||||||
nodes.body.items.forEach((node) => {
|
nodes.items.forEach((node) => {
|
||||||
nodeCapacity.set(node.metadata.name, node.status.capacity);
|
nodeCapacity.set(node.metadata.name, node.status.capacity);
|
||||||
cpuTotal += Number.parseInt(node.status.capacity.cpu, 10);
|
cpuTotal += Number.parseInt(node.status.capacity.cpu, 10);
|
||||||
memTotal += parseMemory(node.status.capacity.memory);
|
memTotal += parseMemory(node.status.capacity.memory);
|
||||||
@ -57,4 +71,10 @@ export default async function handler(req, res) {
|
|||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
error: "invalid type"
|
error: "invalid type"
|
||||||
});
|
});
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("exception %s", e);
|
||||||
|
return res.status(500).send({
|
||||||
|
error: "unknown error"
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -64,7 +64,7 @@ export async function servicesResponse() {
|
|||||||
try {
|
try {
|
||||||
discoveredKubernetesServices = cleanServiceGroups(await servicesFromKubernetes());
|
discoveredKubernetesServices = cleanServiceGroups(await servicesFromKubernetes());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to discover services, please check docker.yaml for errors or remove example entries.");
|
console.error("Failed to discover services, please check kubernetes.yaml for errors or remove example entries.");
|
||||||
if (e) console.error(e);
|
if (e) console.error(e);
|
||||||
discoveredKubernetesServices = [];
|
discoveredKubernetesServices = [];
|
||||||
}
|
}
|
||||||
|
@ -6,10 +6,13 @@ import Docker from "dockerode";
|
|||||||
import * as shvl from "shvl";
|
import * as shvl from "shvl";
|
||||||
import { NetworkingV1Api } from "@kubernetes/client-node";
|
import { NetworkingV1Api } from "@kubernetes/client-node";
|
||||||
|
|
||||||
|
import createLogger from "utils/logger";
|
||||||
import checkAndCopyConfig from "utils/config/config";
|
import checkAndCopyConfig from "utils/config/config";
|
||||||
import getDockerArguments from "utils/config/docker";
|
import getDockerArguments from "utils/config/docker";
|
||||||
import getKubeConfig from "utils/config/kubernetes";
|
import getKubeConfig from "utils/config/kubernetes";
|
||||||
|
|
||||||
|
const logger = createLogger("service-helpers");
|
||||||
|
|
||||||
export async function servicesFromConfig() {
|
export async function servicesFromConfig() {
|
||||||
checkAndCopyConfig("services.yaml");
|
checkAndCopyConfig("services.yaml");
|
||||||
|
|
||||||
@ -105,26 +108,47 @@ export async function servicesFromDocker() {
|
|||||||
return mappedServiceGroups;
|
return mappedServiceGroups;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getUrlFromIngress(ingress) {
|
||||||
|
let url = ingress.metadata.annotations['homepage/url'];
|
||||||
|
if(!url) {
|
||||||
|
const host = ingress.spec.rules[0].host;
|
||||||
|
const path = ingress.spec.rules[0].http.paths[0].path;
|
||||||
|
const schema = ingress.spec.tls ? 'https' : 'http';
|
||||||
|
|
||||||
|
url = `${schema}://${host}${path}`;
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
export async function servicesFromKubernetes() {
|
export async function servicesFromKubernetes() {
|
||||||
checkAndCopyConfig("kubernetes.yaml");
|
checkAndCopyConfig("kubernetes.yaml");
|
||||||
|
|
||||||
|
try {
|
||||||
const kc = getKubeConfig();
|
const kc = getKubeConfig();
|
||||||
const networking = kc.makeApiClient(NetworkingV1Api);
|
const networking = kc.makeApiClient(NetworkingV1Api);
|
||||||
|
|
||||||
const ingressResponse = await networking.listIngressForAllNamespaces(null, null, null, "homepage/enabled=true");
|
const ingressList = await networking.listIngressForAllNamespaces(null, null, null, "homepage/enabled=true")
|
||||||
const services = ingressResponse.body.items.map((ingress) => {
|
.then((response) => response.body)
|
||||||
|
.catch((error) => {
|
||||||
|
logger.error("Error getting ingresses: %d %s %s", error.statusCode, error.body, error.response);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
if (!ingressList) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const services = ingressList.items.map((ingress) => {
|
||||||
const constructedService = {
|
const constructedService = {
|
||||||
app: ingress.metadata.name,
|
app: ingress.metadata.name,
|
||||||
namespace: ingress.metadata.namespace,
|
namespace: ingress.metadata.namespace,
|
||||||
href: `https://${ingress.spec.rules[0].host}`,
|
href: getUrlFromIngress(ingress),
|
||||||
name: ingress.metadata.annotations['homepage/name'],
|
name: ingress.metadata.annotations['homepage/name'] || ingress.metadata.name,
|
||||||
group: ingress.metadata.annotations['homepage/group'],
|
group: ingress.metadata.annotations['homepage/group'] || "Kubernetes",
|
||||||
icon: ingress.metadata.annotations['homepage/icon'],
|
icon: ingress.metadata.annotations['homepage/icon'] || '',
|
||||||
description: ingress.metadata.annotations['homepage/description']
|
description: ingress.metadata.annotations['homepage/description'] || ''
|
||||||
};
|
};
|
||||||
Object.keys(ingress.metadata.labels).forEach((label) => {
|
Object.keys(ingress.metadata.annotations).forEach((annotation) => {
|
||||||
if (label.startsWith("homepage/widget/")) {
|
if (annotation.startsWith("homepage/widget/")) {
|
||||||
shvl.set(constructedService, label.replace("homepage/widget/", ""), ingress.metadata.labels[label]);
|
shvl.set(constructedService, annotation.replace("homepage/widget/", ""), ingress.metadata.annotations[annotation]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -153,6 +177,11 @@ export async function servicesFromKubernetes() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return mappedServiceGroups;
|
return mappedServiceGroups;
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function cleanServiceGroups(groups) {
|
export function cleanServiceGroups(groups) {
|
||||||
|
@ -2,7 +2,6 @@ export function parseCpu(cpuStr) {
|
|||||||
const unitLength = 1;
|
const unitLength = 1;
|
||||||
const base = Number.parseInt(cpuStr, 10);
|
const base = Number.parseInt(cpuStr, 10);
|
||||||
const units = cpuStr.substring(cpuStr.length - unitLength);
|
const units = cpuStr.substring(cpuStr.length - unitLength);
|
||||||
// console.log(Number.isNaN(Number(units)), cpuStr, base, units);
|
|
||||||
if (Number.isNaN(Number(units))) {
|
if (Number.isNaN(Number(units))) {
|
||||||
switch (units) {
|
switch (units) {
|
||||||
case 'n':
|
case 'n':
|
||||||
@ -23,7 +22,6 @@ export function parseMemory(memStr) {
|
|||||||
const unitLength = (memStr.substring(memStr.length - 1) === 'i' ? 2 : 1);
|
const unitLength = (memStr.substring(memStr.length - 1) === 'i' ? 2 : 1);
|
||||||
const base = Number.parseInt(memStr, 10);
|
const base = Number.parseInt(memStr, 10);
|
||||||
const units = memStr.substring(memStr.length - unitLength);
|
const units = memStr.substring(memStr.length - unitLength);
|
||||||
// console.log(Number.isNaN(Number(units)), memStr, base, units);
|
|
||||||
if (Number.isNaN(Number(units))) {
|
if (Number.isNaN(Number(units))) {
|
||||||
switch (units) {
|
switch (units) {
|
||||||
case 'Ki':
|
case 'Ki':
|
||||||
|
@ -32,23 +32,19 @@ export default function Component({ service }) {
|
|||||||
<Container service={service}>
|
<Container service={service}>
|
||||||
<Block label="docker.cpu" />
|
<Block label="docker.cpu" />
|
||||||
<Block label="docker.mem" />
|
<Block label="docker.mem" />
|
||||||
<Block label="docker.rx" />
|
|
||||||
<Block label="docker.tx" />
|
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const network = statsData.stats?.networks?.eth0 || statsData.stats?.networks?.network;
|
|
||||||
return (
|
return (
|
||||||
<Container service={service}>
|
<Container service={service}>
|
||||||
|
{statsData.stats.cpuLimit && (
|
||||||
<Block label="docker.cpu" value={t("common.percent", { value: statsData.stats.cpuUsage })} />
|
<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.cpu" value={t("common.number", { value: statsData.stats.cpu, maximumFractionDigits: 4 })}
|
||||||
<>
|
/>
|
||||||
<Block label="docker.rx" value={t("common.bytes", { value: network.rx_bytes })} />
|
|
||||||
<Block label="docker.tx" value={t("common.bytes", { value: network.tx_bytes })} />
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
<Block label="docker.mem" value={t("common.bytes", { value: statsData.stats.mem })} />
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user