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
This commit is contained in:
parent
174cb651b4
commit
09eb172079
8 changed files with 65 additions and 40 deletions
|
@ -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…
Add table
Reference in a new issue