Improved kubernetes error handling

pull/448/head
James Wynn 2 years ago
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,15 +2,18 @@ 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";
const { service } = req.query; const { service } = req.query;
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,34 +60,43 @@ 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)
podMetrics.containers.forEach((container) => { .then((response) => response)
depMem += parseMemory(container.usage.memory); .catch((err) => {
depCpu += parseCpu(container.usage.cpu); // 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 { return {
mem: depMem, mem: depMem,
cpu: depCpu cpu: depCpu
}
}).reduce(async (finalStats, podStatPromise) => {
const podStats = await podStatPromise;
return {
mem: finalStats.mem + podStats.mem,
cpu: finalStats.cpu + podStats.cpu
}; };
}); }).reduce(async (finalStats, podStatPromise) => {
const podStats = await podStatPromise;
return {
mem: finalStats.mem + podStats.mem,
cpu: finalStats.cpu + podStats.cpu
};
});
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,59 +2,79 @@ 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;
const kc = getKubeConfig(); try {
const coreApi = kc.makeApiClient(CoreV1Api); const kc = getKubeConfig();
const metricsApi = new Metrics(kc); const coreApi = kc.makeApiClient(CoreV1Api);
const metricsApi = new Metrics(kc);
const nodes = await coreApi.listNode();
const nodeCapacity = new Map(); const nodes = await coreApi.listNode()
let cpuTotal = 0; .then((response) => response.body)
let cpuUsage = 0; .catch((error) => {
let memTotal = 0; logger.error("Error getting ingresses: %d %s %s", error.statusCode, error.body, error.response);
let memUsage = 0; return null;
});
nodes.body.items.forEach((node) => { if (!nodes) {
nodeCapacity.set(node.metadata.name, node.status.capacity); return res.status(500).send({
cpuTotal += Number.parseInt(node.status.capacity.cpu, 10); error: "unknown error"
memTotal += parseMemory(node.status.capacity.memory); });
}); }
const nodeCapacity = new Map();
const nodeMetrics = await metricsApi.getNodeMetrics(); let cpuTotal = 0;
const nodeUsage = new Map(); let cpuUsage = 0;
nodeMetrics.items.forEach((metrics) => { let memTotal = 0;
nodeUsage.set(metrics.metadata.name, metrics.usage); let memUsage = 0;
cpuUsage += parseCpu(metrics.usage.cpu);
memUsage += parseMemory(metrics.usage.memory); nodes.items.forEach((node) => {
}); nodeCapacity.set(node.metadata.name, node.status.capacity);
cpuTotal += Number.parseInt(node.status.capacity.cpu, 10);
if (type === "cpu") { memTotal += parseMemory(node.status.capacity.memory);
return res.status(200).json({
cpu: {
usage: (cpuUsage / cpuTotal) * 100,
load: cpuUsage
}
}); });
}
if (type === "memory") { const nodeMetrics = await metricsApi.getNodeMetrics();
const SCALE_MB = 1024 * 1024; const nodeUsage = new Map();
const usedMemMb = memUsage / SCALE_MB; nodeMetrics.items.forEach((metrics) => {
const totalMemMb = memTotal / SCALE_MB; nodeUsage.set(metrics.metadata.name, metrics.usage);
const freeMemMb = totalMemMb - usedMemMb; cpuUsage += parseCpu(metrics.usage.cpu);
return res.status(200).json({ memUsage += parseMemory(metrics.usage.memory);
memory: {
usedMemMb,
freeMemMb,
totalMemMb
}
}); });
}
return res.status(400).json({ if (type === "cpu") {
error: "invalid type" return res.status(200).json({
}); cpu: {
usage: (cpuUsage / cpuTotal) * 100,
load: cpuUsage
}
});
}
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"
});
} 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,54 +108,80 @@ 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");
const kc = getKubeConfig(); try {
const networking = kc.makeApiClient(NetworkingV1Api); 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 ingressList = await networking.listIngressForAllNamespaces(null, null, null, "homepage/enabled=true")
const constructedService = { .then((response) => response.body)
app: ingress.metadata.name, .catch((error) => {
namespace: ingress.metadata.namespace, logger.error("Error getting ingresses: %d %s %s", error.statusCode, error.body, error.response);
href: `https://${ingress.spec.rules[0].host}`, return null;
name: ingress.metadata.annotations['homepage/name'], });
group: ingress.metadata.annotations['homepage/group'], if (!ingressList) {
icon: ingress.metadata.annotations['homepage/icon'], return [];
description: ingress.metadata.annotations['homepage/description'] }
}; const services = ingressList.items.map((ingress) => {
Object.keys(ingress.metadata.labels).forEach((label) => { const constructedService = {
if (label.startsWith("homepage/widget/")) { app: ingress.metadata.name,
shvl.set(constructedService, label.replace("homepage/widget/", ""), ingress.metadata.labels[label]); namespace: ingress.metadata.namespace,
} href: getUrlFromIngress(ingress),
name: ingress.metadata.annotations['homepage/name'] || ingress.metadata.name,
group: ingress.metadata.annotations['homepage/group'] || "Kubernetes",
icon: ingress.metadata.annotations['homepage/icon'] || '',
description: ingress.metadata.annotations['homepage/description'] || ''
};
Object.keys(ingress.metadata.annotations).forEach((annotation) => {
if (annotation.startsWith("homepage/widget/")) {
shvl.set(constructedService, annotation.replace("homepage/widget/", ""), ingress.metadata.annotations[annotation]);
}
});
return constructedService;
}); });
return constructedService; const mappedServiceGroups = [];
});
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];
}
services.forEach((serverService) => { const { name: serviceName, group: serverServiceGroup, ...pushedService } = serverService;
let serverGroup = mappedServiceGroups.find((searchedGroup) => searchedGroup.name === serverService.group); const result = {
if (!serverGroup) { name: serviceName,
mappedServiceGroups.push({ ...pushedService,
name: serverService.group, };
services: [],
});
serverGroup = mappedServiceGroups[mappedServiceGroups.length - 1];
}
const { name: serviceName, group: serverServiceGroup, ...pushedService } = serverService; serverGroup.services.push(result);
const result = { });
name: serviceName,
...pushedService,
};
serverGroup.services.push(result); 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}>
<Block label="docker.cpu" value={t("common.percent", { value: statsData.stats.cpuUsage })} /> {statsData.stats.cpuLimit && (
<Block label="docker.mem" value={t("common.bytes", { value: statsData.stats.mem })} /> <Block label="docker.cpu" value={t("common.percent", { value: statsData.stats.cpuUsage })} />
{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…
Cancel
Save