new status format, new podSelector field, more accurate pod stats

* renamed pod label prefix from "homepage" to "gethomepage.dev"
  which is more inline with typical kubernetes practices
pull/448/head
James Wynn 1 year ago
parent 174cb651b4
commit 09eb172079

@ -95,7 +95,7 @@ export default function Item({ service }) {
<button
type="button"
onClick={() => (statsOpen ? closeStats() : setStatsOpen(true))}
className="flex-shrink-0 flex items-center justify-center w-12 cursor-pointer"
className="flex-shrink-0 flex items-center justify-center cursor-pointer"
>
<KubernetesStatus service={service} />
<span className="sr-only">View container stats</span>
@ -121,7 +121,7 @@ export default function Item({ service }) {
"w-full overflow-hidden transition-all duration-300 ease-in-out"
)}
>
{statsOpen && <Kubernetes service={{ widget: { namespace: service.namespace, app: service.app } }} />}
{statsOpen && <Kubernetes service={{ widget: { namespace: service.namespace, app: service.app, podSelector: service.podSelector } }} />}
</div>
)}

@ -1,19 +1,35 @@
import useSWR from "swr";
import { t } from "i18next";
export default function KubernetesStatus({ service }) {
const { data, error } = useSWR(`/api/kubernetes/status/${service.namespace}/${service.app}`);
const podSelectorString = service.podSelector !== undefined ? `podSelector=${service.podSelector}` : "";
const { data, error } = useSWR(`/api/kubernetes/status/${service.namespace}/${service.app}?${podSelectorString}`);
if (error) {
return <div className="w-3 h-3 bg-rose-300 dark:bg-rose-500 rounded-full" />;
<div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden" title={data.status}>
<div className="text-[8px] font-bold text-rose-500/80 uppercase">{t("docker.error")}</div>
</div>
}
if (data && data.status === "running") {
return <div className="w-3 h-3 bg-emerald-300 dark:bg-emerald-500 rounded-full" />;
return (
<div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden" title={data.health ?? data.status}>
<div className="text-[8px] font-bold text-emerald-500/80 uppercase">{data.health ?? data.status}</div>
</div>
);
}
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" />;
if (data && (data.status === "not found" || data.status === "down" || data.status === "partial")) {
return (
<div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden" title={data.status}>
<div className="text-[8px] font-bold text-orange-400/50 dark:text-orange-400/80 uppercase">{data.status}</div>
</div>
);
}
return <div className="w-3 h-3 bg-black/20 dark:bg-white/40 rounded-full" />;
return (
<div className="w-auto px-1.5 py-0.5 text-center bg-theme-500/10 dark:bg-theme-900/50 rounded-b-[3px] overflow-hidden">
<div className="text-[8px] font-bold text-black/20 dark:text-white/40 uppercase">{t("docker.unknown")}</div>
</div>
);
}

@ -48,10 +48,10 @@ export default function Widget({ options }) {
<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">
{cluster.show &&
<Node type="cluster" options={options.cluster} data={defaultData} />
<Node type="cluster" key="cluster" options={options.cluster} data={defaultData} />
}
{nodes.show &&
<Node type="node" options={options.nodes} data={defaultData} />
<Node type="node" key="nodes" options={options.nodes} data={defaultData} />
}
</div>
</div>
@ -62,11 +62,11 @@ export default function Widget({ options }) {
<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">
{cluster.show &&
<Node type="cluster" options={options.cluster} data={data.cluster} />
<Node key="cluster" type="cluster" options={options.cluster} data={data.cluster} />
}
{nodes.show && data.nodes &&
data.nodes.map((node) =>
<Node key={node} type="node" options={options.nodes} data={node} />)
<Node key={node.name} type="node" options={options.nodes} data={node} />)
}
</div>
</div>

@ -9,7 +9,6 @@ import UsageBar from "./usage-bar";
export default function Node({ type, options, data }) {
const { t } = useTranslation();
console.log("Node", type, options, data);
function icon() {
if (type === "cluster") {

@ -8,7 +8,7 @@ const logger = createLogger("kubernetesStatsService");
export default async function handler(req, res) {
const APP_LABEL = "app.kubernetes.io/name";
const { service } = req.query;
const { service, podSelector } = req.query;
const [namespace, appName] = service;
if (!namespace && !appName) {
@ -17,7 +17,7 @@ export default async function handler(req, res) {
});
return;
}
const labelSelector = `${APP_LABEL}=${appName}`;
const labelSelector = podSelector !== undefined ? podSelector : `${APP_LABEL}=${appName}`;
try {
const kc = getKubeConfig();
@ -63,7 +63,7 @@ export default async function handler(req, res) {
});
});
const stats = await pods.map(async (pod) => {
const podStatsList = await Promise.all(pods.map(async (pod) => {
let depMem = 0;
let depCpu = 0;
const podMetrics = await metricsApi.getPodMetrics(namespace, pod.metadata.name)
@ -85,13 +85,15 @@ export default async function handler(req, res) {
mem: depMem,
cpu: depCpu
};
}).reduce(async (finalStats, podStatPromise) => {
const podStats = await podStatPromise;
return {
mem: finalStats.mem + podStats.mem,
cpu: finalStats.cpu + podStats.cpu
};
});
}));
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;

@ -7,7 +7,7 @@ const logger = createLogger("kubernetesStatusService");
export default async function handler(req, res) {
const APP_LABEL = "app.kubernetes.io/name";
const { service } = req.query;
const { service, podSelector } = req.query;
const [namespace, appName] = service;
if (!namespace && !appName) {
@ -16,8 +16,8 @@ export default async function handler(req, res) {
});
return;
}
const labelSelector = `${APP_LABEL}=${appName}`;
const labelSelector = podSelector !== undefined ? podSelector : `${APP_LABEL}=${appName}`;
logger.info("labelSelector %s/%s = %s", namespace, appName, labelSelector);
try {
const kc = getKubeConfig();
if (!kc) {
@ -47,10 +47,14 @@ export default async function handler(req, res) {
});
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";
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
});

@ -144,13 +144,14 @@ export async function servicesFromKubernetes() {
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'] || ''
name: ingress.metadata.annotations['gethomepage.dev/name'] || ingress.metadata.name,
group: ingress.metadata.annotations['gethomepage.dev/group'] || "Kubernetes",
icon: ingress.metadata.annotations['gethomepage.dev/icon'] || '',
description: ingress.metadata.annotations['gethomepage.dev/description'] || '',
podSelector: ingress.metadata.annotations['gethomepage.dev/pod-selector'] || ''
};
Object.keys(ingress.metadata.annotations).forEach((annotation) => {
if (annotation.startsWith("homepage/widget/")) {
if (annotation.startsWith("gethomepage.dev//widget/")) {
shvl.set(constructedService, annotation.replace("homepage/widget/", ""), ingress.metadata.annotations[annotation]);
}
});
@ -202,9 +203,10 @@ export function cleanServiceGroups(groups) {
container,
currency, // coinmarketcap widget
symbols,
defaultinterval
defaultinterval,
namespace, // kubernetes widget
app
app,
podSelector
} = cleanedService.widget;
cleanedService.widget = {
@ -225,6 +227,7 @@ export function cleanServiceGroups(groups) {
if (type === "kubernetes") {
if (namespace) cleanedService.widget.namespace = namespace;
if (app) cleanedService.widget.app = app;
if (podSelector) cleanedService.widget.podSelector = podSelector;
}
}
@ -267,4 +270,4 @@ export default async function getServiceWidget(group, service) {
}
return false;
}
}

@ -8,12 +8,13 @@ export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service;
const podSelectorString = service.podSelector !== undefined ? `podSelector=${widget.podSelector}` : "";
const { data: statusData, error: statusError } = useSWR(
`/api/kubernetes/status/${widget.namespace}/${widget.app}`);
`/api/kubernetes/status/${widget.namespace}/${widget.app}?${podSelectorString}`);
const { data: statsData, error: statsError } = useSWR(
`/api/kubernetes/stats/${widget.namespace}/${widget.app}`);
`/api/kubernetes/stats/${widget.namespace}/${widget.app}?${podSelectorString}`);
if (statsError || statusError) {
return <Container error={t("widget.api_error")} />;

Loading…
Cancel
Save