diff --git a/src/components/widgets/longhorn/longhorn.jsx b/src/components/widgets/longhorn/longhorn.jsx index 3ba421d08..9fcb21b4a 100644 --- a/src/components/widgets/longhorn/longhorn.jsx +++ b/src/components/widgets/longhorn/longhorn.jsx @@ -1,6 +1,6 @@ import useSWR from "swr"; import { BiError } from "react-icons/bi"; -import { i18n, useTranslation } from "next-i18next"; +import { useTranslation } from "next-i18next"; import Node from "./node"; diff --git a/src/pages/api/kubernetes/stats/[...service].js b/src/pages/api/kubernetes/stats/[...service].js index 05001908d..c4b5b931d 100644 --- a/src/pages/api/kubernetes/stats/[...service].js +++ b/src/pages/api/kubernetes/stats/[...service].js @@ -2,15 +2,18 @@ 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 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", + error: "kubernetes query parameters are required" }); return; } @@ -20,12 +23,23 @@ export default async function handler(req, res) { 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; + 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(200).send({ - error: "not found", + res.status(404).send({ + error: "not found" }); return; } @@ -46,34 +60,43 @@ export default async function handler(req, res) { 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); - }); + 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 - } - }).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.memLimit = memLimit; - stats.cpuUsage = stats.cpu / cpuLimit; - stats.memUsage = stats.mem / memLimit; - + stats.cpuUsage = cpuLimit ? stats.cpu / cpuLimit : 0; + stats.memUsage = memLimit ? stats.mem / memLimit : 0; res.status(200).json({ - stats, + stats }); } catch (e) { - console.log("error", e); + logger.error(e); res.status(500).send({ - error: "unknown error", + error: "unknown error" }); } } diff --git a/src/pages/api/kubernetes/status/[...service].js b/src/pages/api/kubernetes/status/[...service].js index dbe64f38b..aa325e9bb 100644 --- a/src/pages/api/kubernetes/status/[...service].js +++ b/src/pages/api/kubernetes/status/[...service].js @@ -1,6 +1,9 @@ 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"; @@ -18,11 +21,22 @@ export default async function handler(req, res) { 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; + 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(200).send({ + res.status(404).send({ error: "not found", }); return; @@ -34,7 +48,8 @@ export default async function handler(req, res) { res.status(200).json({ status }); - } catch { + } catch (e) { + logger.error(e); res.status(500).send({ error: "unknown error", }); diff --git a/src/pages/api/widgets/kubernetes.js b/src/pages/api/widgets/kubernetes.js index 350c93a22..7ca9f62b1 100644 --- a/src/pages/api/widgets/kubernetes.js +++ b/src/pages/api/widgets/kubernetes.js @@ -2,59 +2,79 @@ 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) { 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 - } + try { + const kc = getKubeConfig(); + 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" + }); + } + const nodeCapacity = new Map(); + let cpuTotal = 0; + let cpuUsage = 0; + let memTotal = 0; + let memUsage = 0; + + nodes.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); }); - } - 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 - } + 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); }); - } - return res.status(400).json({ - error: "invalid type" - }); + if (type === "cpu") { + 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" + }); + } } diff --git a/src/utils/config/api-response.js b/src/utils/config/api-response.js index aef1650c9..59f8e0828 100644 --- a/src/utils/config/api-response.js +++ b/src/utils/config/api-response.js @@ -64,7 +64,7 @@ export async function servicesResponse() { try { discoveredKubernetesServices = cleanServiceGroups(await servicesFromKubernetes()); } 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); discoveredKubernetesServices = []; } diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js index b1a368eee..efcab0e5b 100644 --- a/src/utils/config/service-helpers.js +++ b/src/utils/config/service-helpers.js @@ -6,10 +6,13 @@ import Docker from "dockerode"; import * as shvl from "shvl"; import { NetworkingV1Api } from "@kubernetes/client-node"; +import createLogger from "utils/logger"; import checkAndCopyConfig from "utils/config/config"; import getDockerArguments from "utils/config/docker"; import getKubeConfig from "utils/config/kubernetes"; +const logger = createLogger("service-helpers"); + export async function servicesFromConfig() { checkAndCopyConfig("services.yaml"); @@ -105,54 +108,80 @@ export async function servicesFromDocker() { 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() { 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]); - } + try { + const kc = getKubeConfig(); + const networking = kc.makeApiClient(NetworkingV1Api); + + const ingressList = await networking.listIngressForAllNamespaces(null, null, null, "homepage/enabled=true") + .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 = { + app: ingress.metadata.name, + 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) => { - 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, + }; - const { name: serviceName, group: serverServiceGroup, ...pushedService } = serverService; - const result = { - name: serviceName, - ...pushedService, - }; + serverGroup.services.push(result); + }); - serverGroup.services.push(result); - }); + return mappedServiceGroups; - return mappedServiceGroups; + } catch (e) { + logger.error(e); + throw e; + } } export function cleanServiceGroups(groups) { diff --git a/src/utils/kubernetes/kubernetes-utils.js b/src/utils/kubernetes/kubernetes-utils.js index 08bd53d34..eac42860f 100644 --- a/src/utils/kubernetes/kubernetes-utils.js +++ b/src/utils/kubernetes/kubernetes-utils.js @@ -2,7 +2,6 @@ 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': @@ -23,7 +22,6 @@ 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': diff --git a/src/widgets/kubernetes/component.jsx b/src/widgets/kubernetes/component.jsx index 9ed7627d2..ca3932d24 100644 --- a/src/widgets/kubernetes/component.jsx +++ b/src/widgets/kubernetes/component.jsx @@ -32,23 +32,19 @@ export default function Component({ service }) { - - ); } - const network = statsData.stats?.networks?.eth0 || statsData.stats?.networks?.network; return ( - - - {network && ( - <> - - - + {statsData.stats.cpuLimit && ( + + ) || ( + )} + ); }