Просмотр исходного кода

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
James Wynn 2 лет назад
Родитель
Сommit
09eb172079

+ 2 - 2
src/components/services/item.jsx

@@ -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>
         )}
 

+ 22 - 6
src/components/services/kubernetes-status.jsx

@@ -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>
+  );
 }

+ 4 - 4
src/components/widgets/kubernetes/kubernetes.jsx

@@ -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>

+ 0 - 1
src/components/widgets/kubernetes/node.jsx

@@ -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") {

+ 12 - 10
src/pages/api/kubernetes/stats/[...service].js

@@ -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;

+ 11 - 7
src/pages/api/kubernetes/status/[...service].js

@@ -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
     });

+ 11 - 8
src/utils/config/service-helpers.js

@@ -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;
-}
+}

+ 3 - 2
src/widgets/kubernetes/component.jsx

@@ -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")} />;