From c4333fd2dc6a87a08b747f96d4e075a34c858cec Mon Sep 17 00:00:00 2001 From: James Wynn Date: Mon, 24 Oct 2022 17:03:35 -0500 Subject: [PATCH 01/20] Kubernetes support * Total CPU and Memory usage for the entire cluster * Total CPU and Memory usage for kubernetes pods * Service discovery via annotations on ingress * No storage stats yet * No network stats yet --- .gitignore | 3 + package.json | 1 + src/components/services/item.jsx | 22 ++++++ src/components/services/kubernetes-status.jsx | 19 +++++ src/components/widgets/resources/cpu.jsx | 4 +- src/components/widgets/resources/disk.jsx | 4 +- src/components/widgets/resources/memory.jsx | 4 +- .../widgets/resources/resources.jsx | 10 +-- .../api/kubernetes/stats/[...service].js | 79 +++++++++++++++++++ .../api/kubernetes/status/[...service].js | 42 ++++++++++ src/pages/api/widgets/kubernetes.js | 72 +++++++++++++++++ src/skeleton/kubernetes.yaml | 2 + src/utils/config/api-response.js | 37 +++++++-- src/utils/config/kubernetes.js | 27 +++++++ src/utils/config/service-helpers.js | 70 +++++++++++++++- src/utils/kubernetes/kubernetes-utils.js | 47 +++++++++++ src/widgets/components.js | 1 + src/widgets/kubernetes/component.jsx | 54 +++++++++++++ 18 files changed, 479 insertions(+), 19 deletions(-) create mode 100644 src/components/services/kubernetes-status.jsx create mode 100644 src/pages/api/kubernetes/stats/[...service].js create mode 100644 src/pages/api/kubernetes/status/[...service].js create mode 100644 src/pages/api/widgets/kubernetes.js create mode 100644 src/skeleton/kubernetes.yaml create mode 100644 src/utils/config/kubernetes.js create mode 100644 src/utils/kubernetes/kubernetes-utils.js create mode 100644 src/widgets/kubernetes/component.jsx diff --git a/.gitignore b/.gitignore index 7ab221f97..9bd31081c 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,6 @@ next-env.d.ts # homepage /config + +# idea +.idea/ diff --git a/package.json b/package.json index 75d02e7fd..2dddf7250 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@headlessui/react": "^1.7.2", + "@kubernetes/client-node": "^0.17.1", "classnames": "^2.3.2", "compare-versions": "^5.0.1", "dockerode": "^3.3.4", diff --git a/src/components/services/item.jsx b/src/components/services/item.jsx index 56ed2b4b1..be6cf6c52 100644 --- a/src/components/services/item.jsx +++ b/src/components/services/item.jsx @@ -3,8 +3,10 @@ import { useContext, useState } from "react"; import Status from "./status"; import Widget from "./widget"; +import KubernetesStatus from "./kubernetes-status"; import Docker from "widgets/docker/component"; +import Kubernetes from "widgets/kubernetes/component"; import { SettingsContext } from "utils/contexts/settings"; import ResolvedIcon from "components/resolvedicon"; @@ -80,6 +82,16 @@ export default function Item({ service }) { View container stats )} + {service.app && ( + + )} {service.container && service.server && ( @@ -92,6 +104,16 @@ export default function Item({ service }) { {statsOpen && } )} + {service.app && ( +
+ {statsOpen && } +
+ )} {service.widget && } diff --git a/src/components/services/kubernetes-status.jsx b/src/components/services/kubernetes-status.jsx new file mode 100644 index 000000000..4b2c57236 --- /dev/null +++ b/src/components/services/kubernetes-status.jsx @@ -0,0 +1,19 @@ +import useSWR from "swr"; + +export default function KubernetesStatus({ service }) { + const { data, error } = useSWR(`/api/kubernetes/status/${service.namespace}/${service.app}`); + + if (error) { + return
; + } + + if (data && data.status === "running") { + return
; + } + + if (data && data.status === "not found") { + return
; + } + + return
; +} diff --git a/src/components/widgets/resources/cpu.jsx b/src/components/widgets/resources/cpu.jsx index 6b021193b..9564d99a1 100644 --- a/src/components/widgets/resources/cpu.jsx +++ b/src/components/widgets/resources/cpu.jsx @@ -5,10 +5,10 @@ import { useTranslation } from "next-i18next"; import UsageBar from "./usage-bar"; -export default function Cpu({ expanded }) { +export default function Cpu({ expanded, backend }) { const { t } = useTranslation(); - const { data, error } = useSWR(`/api/widgets/resources?type=cpu`, { + const { data, error } = useSWR(`/api/widgets/${backend || 'resources'}?type=cpu`, { refreshInterval: 1500, }); diff --git a/src/components/widgets/resources/disk.jsx b/src/components/widgets/resources/disk.jsx index 69f560f62..eea84162e 100644 --- a/src/components/widgets/resources/disk.jsx +++ b/src/components/widgets/resources/disk.jsx @@ -5,10 +5,10 @@ import { useTranslation } from "next-i18next"; import UsageBar from "./usage-bar"; -export default function Disk({ options, expanded }) { +export default function Disk({ options, expanded, backend }) { const { t } = useTranslation(); - const { data, error } = useSWR(`/api/widgets/resources?type=disk&target=${options.disk}`, { + const { data, error } = useSWR(`/api/widgets/${backend || 'resources'}?type=disk&target=${options.disk}`, { refreshInterval: 1500, }); diff --git a/src/components/widgets/resources/memory.jsx b/src/components/widgets/resources/memory.jsx index 452634565..db5be4b9b 100644 --- a/src/components/widgets/resources/memory.jsx +++ b/src/components/widgets/resources/memory.jsx @@ -5,10 +5,10 @@ import { useTranslation } from "next-i18next"; import UsageBar from "./usage-bar"; -export default function Memory({ expanded }) { +export default function Memory({ expanded, backend }) { const { t } = useTranslation(); - const { data, error } = useSWR(`/api/widgets/resources?type=memory`, { + const { data, error } = useSWR(`/api/widgets/${backend || 'resources'}?type=memory`, { refreshInterval: 1500, }); diff --git a/src/components/widgets/resources/resources.jsx b/src/components/widgets/resources/resources.jsx index 0524e39af..c4072c7a1 100644 --- a/src/components/widgets/resources/resources.jsx +++ b/src/components/widgets/resources/resources.jsx @@ -3,15 +3,15 @@ import Cpu from "./cpu"; import Memory from "./memory"; export default function Resources({ options }) { - const { expanded } = options; + const { expanded, backend } = options; return (
- {options.cpu && } - {options.memory && } + {options.cpu && } + {options.memory && } {Array.isArray(options.disk) - ? options.disk.map((disk) => ) - : options.disk && } + ? options.disk.map((disk) => ) + : options.disk && }
{options.label && (
{options.label}
diff --git a/src/pages/api/kubernetes/stats/[...service].js b/src/pages/api/kubernetes/stats/[...service].js new file mode 100644 index 000000000..05001908d --- /dev/null +++ b/src/pages/api/kubernetes/stats/[...service].js @@ -0,0 +1,79 @@ +import { CoreV1Api, Metrics } from "@kubernetes/client-node"; + +import getKubeConfig from "../../../../utils/config/kubernetes"; +import { parseCpu, parseMemory } from "../../../../utils/kubernetes/kubernetes-utils"; + +export default async function handler(req, res) { + 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", + }); + return; + } + const labelSelector = `${APP_LABEL}=${appName}`; + + try { + 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; + + if (pods.length === 0) { + res.status(200).send({ + error: "not found", + }); + return; + } + + let cpuLimit = 0; + let memLimit = 0; + pods.forEach((pod) => { + pod.spec.containers.forEach((container) => { + if (container?.resources?.limits?.cpu) { + cpuLimit += parseCpu(container?.resources?.limits?.cpu); + } + if (container?.resources?.limits?.memory) { + memLimit += parseMemory(container?.resources?.limits?.memory); + } + }); + }); + + 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); + }); + return { + mem: depMem, + cpu: depCpu + } + }).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; + + res.status(200).json({ + stats, + }); + } catch (e) { + console.log("error", e); + res.status(500).send({ + error: "unknown error", + }); + } +} diff --git a/src/pages/api/kubernetes/status/[...service].js b/src/pages/api/kubernetes/status/[...service].js new file mode 100644 index 000000000..dbe64f38b --- /dev/null +++ b/src/pages/api/kubernetes/status/[...service].js @@ -0,0 +1,42 @@ +import { CoreV1Api } from "@kubernetes/client-node"; + +import getKubeConfig from "../../../../utils/config/kubernetes"; + +export default async function handler(req, res) { + 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", + }); + return; + } + const labelSelector = `${APP_LABEL}=${appName}`; + + 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; + + if (pods.length === 0) { + res.status(200).send({ + error: "not found", + }); + 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"; + res.status(200).json({ + status + }); + } catch { + res.status(500).send({ + error: "unknown error", + }); + } +} diff --git a/src/pages/api/widgets/kubernetes.js b/src/pages/api/widgets/kubernetes.js new file mode 100644 index 000000000..a740df903 --- /dev/null +++ b/src/pages/api/widgets/kubernetes.js @@ -0,0 +1,72 @@ +import { CoreV1Api, Metrics } from "@kubernetes/client-node"; + +import getKubeConfig from "../../../utils/config/kubernetes"; +import { parseCpu, parseMemory } from "../../../utils/kubernetes/kubernetes-utils"; + +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 + } + }); + } + // Maybe Storage CSI can provide this information + // if (type === "disk") { + // if (!existsSync(target)) { + // return res.status(404).json({ + // error: "Target not found", + // }); + // } + // + // return res.status(200).json({ + // drive: await drive.info(target || "/"), + // }); + // } + // + 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" + }); +} diff --git a/src/skeleton/kubernetes.yaml b/src/skeleton/kubernetes.yaml new file mode 100644 index 000000000..aca6e8213 --- /dev/null +++ b/src/skeleton/kubernetes.yaml @@ -0,0 +1,2 @@ +--- +# sample kubernetes config diff --git a/src/utils/config/api-response.js b/src/utils/config/api-response.js index 5cc1127e3..aef1650c9 100644 --- a/src/utils/config/api-response.js +++ b/src/utils/config/api-response.js @@ -5,7 +5,12 @@ import path from "path"; import yaml from "js-yaml"; import checkAndCopyConfig from "utils/config/config"; -import { servicesFromConfig, servicesFromDocker, cleanServiceGroups } from "utils/config/service-helpers"; +import { + servicesFromConfig, + servicesFromDocker, + cleanServiceGroups, + servicesFromKubernetes +} from "utils/config/service-helpers"; import { cleanWidgetGroups, widgetsFromConfig } from "utils/config/widget-helpers"; export async function bookmarksResponse() { @@ -44,15 +49,24 @@ export async function widgetsResponse() { } export async function servicesResponse() { - let discoveredServices; + let discoveredDockerServices; + let discoveredKubernetesServices; let configuredServices; try { - discoveredServices = cleanServiceGroups(await servicesFromDocker()); + discoveredDockerServices = cleanServiceGroups(await servicesFromDocker()); } catch (e) { console.error("Failed to discover services, please check docker.yaml for errors or remove example entries."); if (e) console.error(e); - discoveredServices = []; + discoveredDockerServices = []; + } + + try { + discoveredKubernetesServices = cleanServiceGroups(await servicesFromKubernetes()); + } catch (e) { + console.error("Failed to discover services, please check docker.yaml for errors or remove example entries."); + if (e) console.error(e); + discoveredKubernetesServices = []; } try { @@ -64,18 +78,27 @@ export async function servicesResponse() { } const mergedGroupsNames = [ - ...new Set([discoveredServices.map((group) => group.name), configuredServices.map((group) => group.name)].flat()), + ...new Set([ + discoveredDockerServices.map((group) => group.name), + discoveredKubernetesServices.map((group) => group.name), + configuredServices.map((group) => group.name), + ].flat()), ]; const mergedGroups = []; mergedGroupsNames.forEach((groupName) => { - const discoveredGroup = discoveredServices.find((group) => group.name === groupName) || { services: [] }; + const discoveredDockerGroup = discoveredDockerServices.find((group) => group.name === groupName) || { services: [] }; + const discoveredKubernetesGroup = discoveredKubernetesServices.find((group) => group.name === groupName) || { services: [] }; const configuredGroup = configuredServices.find((group) => group.name === groupName) || { services: [] }; const mergedGroup = { name: groupName, - services: [...discoveredGroup.services, ...configuredGroup.services].filter((service) => service), + services: [ + ...discoveredDockerGroup.services, + ...discoveredKubernetesGroup.services, + ...configuredGroup.services + ].filter((service) => service), }; mergedGroups.push(mergedGroup); diff --git a/src/utils/config/kubernetes.js b/src/utils/config/kubernetes.js new file mode 100644 index 000000000..b6b1adc7f --- /dev/null +++ b/src/utils/config/kubernetes.js @@ -0,0 +1,27 @@ +import path from "path"; +import { readFileSync } from "fs"; + +import yaml from "js-yaml"; +import { KubeConfig } from "@kubernetes/client-node"; + +import checkAndCopyConfig from "utils/config/config"; + +export default function getKubeConfig() { + checkAndCopyConfig("kubernetes.yaml"); + + const configFile = path.join(process.cwd(), "config", "kubernetes.yaml"); + const configData = readFileSync(configFile, "utf8"); + const config = yaml.load(configData); + const kc = new KubeConfig(); + + switch (config?.mode) { + case 'cluster': + kc.loadFromCluster(); + break; + case 'default': + default: + kc.loadFromDefault(); + } + + return kc; +} diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js index 15740d22b..b1a368eee 100644 --- a/src/utils/config/service-helpers.js +++ b/src/utils/config/service-helpers.js @@ -4,9 +4,11 @@ import path from "path"; import yaml from "js-yaml"; import Docker from "dockerode"; import * as shvl from "shvl"; +import { NetworkingV1Api } from "@kubernetes/client-node"; import checkAndCopyConfig from "utils/config/config"; import getDockerArguments from "utils/config/docker"; +import getKubeConfig from "utils/config/kubernetes"; export async function servicesFromConfig() { checkAndCopyConfig("services.yaml"); @@ -103,6 +105,56 @@ export async function servicesFromDocker() { return mappedServiceGroups; } +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]); + } + }); + + return constructedService; + }); + + 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]; + } + + const { name: serviceName, group: serverServiceGroup, ...pushedService } = serverService; + const result = { + name: serviceName, + ...pushedService, + }; + + serverGroup.services.push(result); + }); + + return mappedServiceGroups; +} + export function cleanServiceGroups(groups) { return groups.map((serviceGroup) => ({ name: serviceGroup.name, @@ -118,6 +170,8 @@ export function cleanServiceGroups(groups) { container, currency, // coinmarketcap widget symbols, + namespace, // kubernetes widget + app } = cleanedService.widget; cleanedService.widget = { @@ -134,6 +188,10 @@ export function cleanServiceGroups(groups) { if (server) cleanedService.widget.server = server; if (container) cleanedService.widget.container = container; } + if (type === "kubernetes") { + if (namespace) cleanedService.widget.namespace = namespace; + if (app) cleanedService.widget.app = app; + } } return cleanedService; @@ -164,5 +222,15 @@ export default async function getServiceWidget(group, service) { } } + const kubernetesServices = await servicesFromKubernetes(); + const kubernetesServiceGroup = kubernetesServices.find((g) => g.name === group); + if (kubernetesServiceGroup) { + const kubernetesServiceEntry = kubernetesServiceGroup.services.find((s) => s.name === service); + if (kubernetesServiceEntry) { + const { widget } = kubernetesServiceEntry; + return widget; + } + } + return false; -} \ No newline at end of file +} diff --git a/src/utils/kubernetes/kubernetes-utils.js b/src/utils/kubernetes/kubernetes-utils.js new file mode 100644 index 000000000..08bd53d34 --- /dev/null +++ b/src/utils/kubernetes/kubernetes-utils.js @@ -0,0 +1,47 @@ +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': + return base / 1000000000; + case 'u': + return base / 1000000; + case 'm': + return base / 1000; + default: + return base; + } + } else { + return Number.parseInt(cpuStr, 10); + } +} + +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': + return base * 1000; + case 'K': + return base * 1024; + case 'Mi': + return base * 1000000; + case 'M': + return base * 1024 * 1024; + case 'Gi': + return base * 1000000000; + case 'G': + return base * 1024 * 1024 * 1024; + default: + return base; + } + } else { + return Number.parseInt(memStr, 10); + } +} diff --git a/src/widgets/components.js b/src/widgets/components.js index 33d09eac3..026f9dade 100644 --- a/src/widgets/components.js +++ b/src/widgets/components.js @@ -8,6 +8,7 @@ const components = { changedetectionio: dynamic(() => import("./changedetectionio/component")), coinmarketcap: dynamic(() => import("./coinmarketcap/component")), docker: dynamic(() => import("./docker/component")), + kubernetes: dynamic(() => import("./kubernetes/component")), emby: dynamic(() => import("./emby/component")), gotify: dynamic(() => import("./gotify/component")), homebridge: dynamic(() => import("./homebridge/component")), diff --git a/src/widgets/kubernetes/component.jsx b/src/widgets/kubernetes/component.jsx new file mode 100644 index 000000000..9ed7627d2 --- /dev/null +++ b/src/widgets/kubernetes/component.jsx @@ -0,0 +1,54 @@ +import useSWR from "swr"; +import { useTranslation } from "next-i18next"; + +import Container from "components/services/widget/container"; +import Block from "components/services/widget/block"; + +export default function Component({ service }) { + const { t } = useTranslation(); + + const { widget } = service; + + const { data: statusData, error: statusError } = useSWR( + `/api/kubernetes/status/${widget.namespace}/${widget.app}`); + + const { data: statsData, error: statsError } = useSWR( + `/api/kubernetes/stats/${widget.namespace}/${widget.app}`); + + if (statsError || statusError) { + return ; + } + + if (statusData && statusData.status !== "running") { + return ( + + + + ); + } + + if (!statsData || !statusData) { + return ( + + + + + + + ); + } + + const network = statsData.stats?.networks?.eth0 || statsData.stats?.networks?.network; + return ( + + + + {network && ( + <> + + + + )} + + ); +} From 8887fcc3ee24ec2bdde739726a20d72d9c465387 Mon Sep 17 00:00:00 2001 From: James Wynn Date: Tue, 25 Oct 2022 13:35:38 -0500 Subject: [PATCH 02/20] longhorn support * longhorn widget for showing storage stats as "disks" --- src/components/widgets/longhorn/longhorn.jsx | 57 ++++++++++++++ src/components/widgets/longhorn/node.jsx | 32 ++++++++ src/components/widgets/widget.jsx | 1 + src/pages/api/widgets/kubernetes.js | 14 +--- src/pages/api/widgets/longhorn.js | 82 ++++++++++++++++++++ 5 files changed, 173 insertions(+), 13 deletions(-) create mode 100644 src/components/widgets/longhorn/longhorn.jsx create mode 100644 src/components/widgets/longhorn/node.jsx create mode 100644 src/pages/api/widgets/longhorn.js diff --git a/src/components/widgets/longhorn/longhorn.jsx b/src/components/widgets/longhorn/longhorn.jsx new file mode 100644 index 000000000..3ba421d08 --- /dev/null +++ b/src/components/widgets/longhorn/longhorn.jsx @@ -0,0 +1,57 @@ +import useSWR from "swr"; +import { BiError } from "react-icons/bi"; +import { i18n, useTranslation } from "next-i18next"; + +import Node from "./node"; + +export default function Longhorn({ options }) { + const { expanded, total, labels, include, nodes } = options; + const { t } = useTranslation(); + const { data, error } = useSWR(`/api/widgets/longhorn`, { + refreshInterval: 1500 + }); + + if (error || data?.error) { + return ( +
+ +
+ {t("widget.api_error")} +
+
+ ); + } + + if (!data) { + return ( +
+
+
+ ); + } + + return ( +
+
+ {data.nodes + .filter((node) => { + if (node.id === 'total' && total) { + return true; + } + if (!nodes) { + return false; + } + if (include && !include.includes(node.id)) { + return false; + } + return true; + }) + .map((node) => +
+ +
+ )} +
+
+ ); +} diff --git a/src/components/widgets/longhorn/node.jsx b/src/components/widgets/longhorn/node.jsx new file mode 100644 index 000000000..44b96b1c2 --- /dev/null +++ b/src/components/widgets/longhorn/node.jsx @@ -0,0 +1,32 @@ +import { FiHardDrive } from "react-icons/fi"; +import { useTranslation } from "next-i18next"; + +import UsageBar from "../resources/usage-bar"; + +export default function Node({ data, expanded, labels }) { + const { t } = useTranslation(); + + return ( + <> +
+ +
+ +
{t("common.bytes", { value: data.node.available })}
+
{t("resources.free")}
+
+ {expanded && ( + +
{t("common.bytes", { value: data.node.maximum })}
+
{t("resources.total")}
+
+ )} + +
+
+ {labels && ( +
{data.node.id}
+ )} + + ); +} diff --git a/src/components/widgets/widget.jsx b/src/components/widgets/widget.jsx index 86f79dfeb..29e7a1803 100644 --- a/src/components/widgets/widget.jsx +++ b/src/components/widgets/widget.jsx @@ -13,6 +13,7 @@ const widgetMappings = { unifi_console: dynamic(() => import("components/widgets/unifi_console/unifi_console")), glances: dynamic(() => import("components/widgets/glances/glances")), openmeteo: dynamic(() => import("components/widgets/openmeteo/openmeteo")), + longhorn: dynamic(() => import("components/widgets/longhorn/longhorn")), }; export default function Widget({ widget }) { diff --git a/src/pages/api/widgets/kubernetes.js b/src/pages/api/widgets/kubernetes.js index a740df903..350c93a22 100644 --- a/src/pages/api/widgets/kubernetes.js +++ b/src/pages/api/widgets/kubernetes.js @@ -39,19 +39,7 @@ export default async function handler(req, res) { } }); } - // Maybe Storage CSI can provide this information - // if (type === "disk") { - // if (!existsSync(target)) { - // return res.status(404).json({ - // error: "Target not found", - // }); - // } - // - // return res.status(200).json({ - // drive: await drive.info(target || "/"), - // }); - // } - // + if (type === "memory") { const SCALE_MB = 1024 * 1024; const usedMemMb = memUsage / SCALE_MB; diff --git a/src/pages/api/widgets/longhorn.js b/src/pages/api/widgets/longhorn.js new file mode 100644 index 000000000..cb9ed24bd --- /dev/null +++ b/src/pages/api/widgets/longhorn.js @@ -0,0 +1,82 @@ +import { httpProxy } from "../../../utils/proxy/http"; +import createLogger from "../../../utils/logger"; +import { getSettings } from "../../../utils/config/config"; + +const logger = createLogger("longhorn"); + +function parseLonghornData(data) { + const json = JSON.parse(data); + + if (!json) { + return null; + } + + const nodes = json.data.map((node) => { + let available = 0; + let maximum = 0; + let reserved = 0; + let scheduled = 0; + Object.keys(node.disks).forEach((diskKey) => { + const disk = node.disks[diskKey]; + available += disk.storageAvailable; + maximum += disk.storageMaximum; + reserved += disk.storageReserved; + scheduled += disk.storageScheduled; + }); + return { + id: node.id, + available, + maximum, + reserved, + scheduled, + }; + }); + const total = nodes.reduce((summary, node) => ({ + available: summary.available + node.available, + maximum: summary.maximum + node.maximum, + reserved: summary.reserved + node.reserved, + scheduled: summary.scheduled + node.scheduled, + })); + total.id = "total"; + nodes.push(total); + return nodes; +} + +export default async function handler(req, res) { + const settings = getSettings(); + const longhornSettings = settings?.providers?.longhorn; + const {url, username, password} = longhornSettings; + + if (!url) { + const errorMessage = "Missing Longhorn URL"; + logger.error(errorMessage); + return res.status(400).json({ error: errorMessage }); + } + + const apiUrl = `${url}/v1/nodes`; + const headers = { + "Accept-Encoding": "application/json" + }; + if (username && password) { + headers.Authorization = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}` + } + const params = { method: "GET", headers }; + + const [status, contentType, data] = await httpProxy(apiUrl, params); + + if (status === 401) { + logger.error("Authorization failure getting data from Longhorn API. Data: %s", data); + } + + if (status !== 200) { + logger.error("HTTP %d getting data from Longhorn API. Data: %s", status, data); + } + + if (contentType) res.setHeader("Content-Type", contentType); + + const nodes = parseLonghornData(data); + + return res.status(200).json({ + nodes, + }); +} From 4fc6db49ca66f770e556ac87311e2a63cae448da Mon Sep 17 00:00:00 2001 From: James Wynn Date: Wed, 26 Oct 2022 10:15:25 -0500 Subject: [PATCH 03/20] Improved kubernetes error handling --- src/components/widgets/longhorn/longhorn.jsx | 2 +- .../api/kubernetes/stats/[...service].js | 71 +++++++---- .../api/kubernetes/status/[...service].js | 23 +++- src/pages/api/widgets/kubernetes.js | 114 ++++++++++-------- src/utils/config/api-response.js | 2 +- src/utils/config/service-helpers.js | 105 ++++++++++------ src/utils/kubernetes/kubernetes-utils.js | 2 - src/widgets/kubernetes/component.jsx | 16 +-- 8 files changed, 208 insertions(+), 127 deletions(-) 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 && ( + + ) || ( + )} + ); } From 0c6f7dbee1381f13fe20eae7a779958bd9ff0056 Mon Sep 17 00:00:00 2001 From: James Wynn Date: Wed, 26 Oct 2022 10:25:27 -0500 Subject: [PATCH 04/20] Cleaned up some variable names --- src/utils/config/service-helpers.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js index efcab0e5b..d37423be5 100644 --- a/src/utils/config/service-helpers.js +++ b/src/utils/config/service-helpers.js @@ -111,11 +111,11 @@ export async function servicesFromDocker() { 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'; + const urlHost = ingress.spec.rules[0].host; + const urlPath = ingress.spec.rules[0].http.paths[0].path; + const urlSchema = ingress.spec.tls ? 'https' : 'http'; - url = `${schema}://${host}${path}`; + url = `${urlSchema}://${urlHost}${urlPath}`; } return url; } From 056e26dfd3b499ac32abbb866fddc388d3dca0e9 Mon Sep 17 00:00:00 2001 From: James Wynn Date: Thu, 27 Oct 2022 16:53:54 -0500 Subject: [PATCH 05/20] Improved handling of empty or disabled kubernetes configuration --- src/pages/api/kubernetes/stats/[...service].js | 6 ++++++ src/pages/api/kubernetes/status/[...service].js | 6 ++++++ src/pages/api/widgets/kubernetes.js | 5 +++++ src/utils/config/kubernetes.js | 5 ++++- src/utils/config/service-helpers.js | 3 +++ 5 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/pages/api/kubernetes/stats/[...service].js b/src/pages/api/kubernetes/stats/[...service].js index c4b5b931d..2ab9ba584 100644 --- a/src/pages/api/kubernetes/stats/[...service].js +++ b/src/pages/api/kubernetes/stats/[...service].js @@ -21,6 +21,12 @@ export default async function handler(req, res) { try { const kc = getKubeConfig(); + if (!kc) { + res.status(500).send({ + error: "No kubernetes configuration" + }); + return; + } const coreApi = kc.makeApiClient(CoreV1Api); const metricsApi = new Metrics(kc); const podsResponse = await coreApi.listNamespacedPod(namespace, null, null, null, null, labelSelector) diff --git a/src/pages/api/kubernetes/status/[...service].js b/src/pages/api/kubernetes/status/[...service].js index aa325e9bb..e76d2d7d8 100644 --- a/src/pages/api/kubernetes/status/[...service].js +++ b/src/pages/api/kubernetes/status/[...service].js @@ -20,6 +20,12 @@ export default async function handler(req, res) { try { const kc = getKubeConfig(); + if (!kc) { + res.status(500).send({ + error: "No kubernetes configuration" + }); + return; + } const coreApi = kc.makeApiClient(CoreV1Api); const podsResponse = await coreApi.listNamespacedPod(namespace, null, null, null, null, labelSelector) .then((response) => response.body) diff --git a/src/pages/api/widgets/kubernetes.js b/src/pages/api/widgets/kubernetes.js index 7ca9f62b1..f589b2b94 100644 --- a/src/pages/api/widgets/kubernetes.js +++ b/src/pages/api/widgets/kubernetes.js @@ -11,6 +11,11 @@ export default async function handler(req, res) { try { const kc = getKubeConfig(); + if (!kc) { + return res.status(500).send({ + error: "No kubernetes configuration" + }); + } const coreApi = kc.makeApiClient(CoreV1Api); const metricsApi = new Metrics(kc); diff --git a/src/utils/config/kubernetes.js b/src/utils/config/kubernetes.js index b6b1adc7f..d44b75f3a 100644 --- a/src/utils/config/kubernetes.js +++ b/src/utils/config/kubernetes.js @@ -19,8 +19,11 @@ export default function getKubeConfig() { kc.loadFromCluster(); break; case 'default': - default: kc.loadFromDefault(); + break; + case 'disabled': + default: + return null; } return kc; diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js index d37423be5..d705b5377 100644 --- a/src/utils/config/service-helpers.js +++ b/src/utils/config/service-helpers.js @@ -125,6 +125,9 @@ export async function servicesFromKubernetes() { try { const kc = getKubeConfig(); + if (!kc) { + return []; + } const networking = kc.makeApiClient(NetworkingV1Api); const ingressList = await networking.listIngressForAllNamespaces(null, null, null, "homepage/enabled=true") From fdb143304fd63faeeaedaa7660f6fe8f546f823c Mon Sep 17 00:00:00 2001 From: James Wynn Date: Fri, 18 Nov 2022 18:02:23 -0600 Subject: [PATCH 06/20] Separated kubernetes widgets from resources widgets --- .../widgets/kubernetes/kubernetes.jsx | 73 ++++++++++++++++++ src/components/widgets/kubernetes/node.jsx | 61 +++++++++++++++ .../widgets/kubernetes/usage-bar.jsx | 12 +++ src/components/widgets/resources/cpu.jsx | 4 +- src/components/widgets/resources/disk.jsx | 4 +- src/components/widgets/resources/memory.jsx | 4 +- .../widgets/resources/resources.jsx | 10 +-- src/components/widgets/widget.jsx | 1 + src/pages/api/widgets/kubernetes.js | 77 ++++++++++--------- 9 files changed, 200 insertions(+), 46 deletions(-) create mode 100644 src/components/widgets/kubernetes/kubernetes.jsx create mode 100644 src/components/widgets/kubernetes/node.jsx create mode 100644 src/components/widgets/kubernetes/usage-bar.jsx diff --git a/src/components/widgets/kubernetes/kubernetes.jsx b/src/components/widgets/kubernetes/kubernetes.jsx new file mode 100644 index 000000000..a769db8f2 --- /dev/null +++ b/src/components/widgets/kubernetes/kubernetes.jsx @@ -0,0 +1,73 @@ +import useSWR from "swr"; +import { BiError } from "react-icons/bi"; +import { useTranslation } from "next-i18next"; +import Node from "./node"; + +export default function Widget({ options }) { + const { cluster, nodes } = options; + const { t, i18n } = useTranslation(); + + const defaultData = { + cpu: { + load: 0, + total: 0, + percent: 0 + }, + memory: { + used: 0, + total: 0, + free: 0, + precent: 0 + } + }; + + const { data, error } = useSWR( + `/api/widgets/kubernetes?${new URLSearchParams({ lang: i18n.language }).toString()}`, { + refreshInterval: 1500 + } + ); + + if (error || data?.error) { + return ( +
+
+
+ +
+ {t("widget.api_error")} +
+
+
+
+ ); + } + + if (!data) { + return ( +
+
+ {cluster.show && + + } + {nodes.show && + + } +
+
+ ); + } + + return ( +
+
+ {cluster.show && + + } + {nodes.show && data.nodes && + data.nodes.map((node) => + ) + } +
+
+ ); +} diff --git a/src/components/widgets/kubernetes/node.jsx b/src/components/widgets/kubernetes/node.jsx new file mode 100644 index 000000000..0d0bde4d1 --- /dev/null +++ b/src/components/widgets/kubernetes/node.jsx @@ -0,0 +1,61 @@ +import { FaMemory } from "react-icons/fa"; +import { FiAlertTriangle, FiCpu, FiServer } from "react-icons/fi"; +import { SiKubernetes } from "react-icons/si"; +import { useTranslation } from "next-i18next"; + +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") { + return ; + } + if (data.ready) { + return ; + } + return ; + } + + return ( +
+
+
+ {icon()} +
+
+
+ {t("common.number", { + value: data.cpu.percent, + style: "unit", + unit: "percent", + maximumFractionDigits: 0 + })} +
+ +
+ +
+
+ {t("common.bytes", { + value: data.memory.free, + maximumFractionDigits: 0, + binary: true + })} +
+ +
+ + {options.showLabel && ( +
{type === "cluster" ? options.label : data.name}
+ )} +
+
+
+
+ ); +} diff --git a/src/components/widgets/kubernetes/usage-bar.jsx b/src/components/widgets/kubernetes/usage-bar.jsx new file mode 100644 index 000000000..c817db4c4 --- /dev/null +++ b/src/components/widgets/kubernetes/usage-bar.jsx @@ -0,0 +1,12 @@ +export default function UsageBar({ percent }) { + return ( +
+
+
+ ); +} diff --git a/src/components/widgets/resources/cpu.jsx b/src/components/widgets/resources/cpu.jsx index 9564d99a1..6b021193b 100644 --- a/src/components/widgets/resources/cpu.jsx +++ b/src/components/widgets/resources/cpu.jsx @@ -5,10 +5,10 @@ import { useTranslation } from "next-i18next"; import UsageBar from "./usage-bar"; -export default function Cpu({ expanded, backend }) { +export default function Cpu({ expanded }) { const { t } = useTranslation(); - const { data, error } = useSWR(`/api/widgets/${backend || 'resources'}?type=cpu`, { + const { data, error } = useSWR(`/api/widgets/resources?type=cpu`, { refreshInterval: 1500, }); diff --git a/src/components/widgets/resources/disk.jsx b/src/components/widgets/resources/disk.jsx index eea84162e..69f560f62 100644 --- a/src/components/widgets/resources/disk.jsx +++ b/src/components/widgets/resources/disk.jsx @@ -5,10 +5,10 @@ import { useTranslation } from "next-i18next"; import UsageBar from "./usage-bar"; -export default function Disk({ options, expanded, backend }) { +export default function Disk({ options, expanded }) { const { t } = useTranslation(); - const { data, error } = useSWR(`/api/widgets/${backend || 'resources'}?type=disk&target=${options.disk}`, { + const { data, error } = useSWR(`/api/widgets/resources?type=disk&target=${options.disk}`, { refreshInterval: 1500, }); diff --git a/src/components/widgets/resources/memory.jsx b/src/components/widgets/resources/memory.jsx index db5be4b9b..452634565 100644 --- a/src/components/widgets/resources/memory.jsx +++ b/src/components/widgets/resources/memory.jsx @@ -5,10 +5,10 @@ import { useTranslation } from "next-i18next"; import UsageBar from "./usage-bar"; -export default function Memory({ expanded, backend }) { +export default function Memory({ expanded }) { const { t } = useTranslation(); - const { data, error } = useSWR(`/api/widgets/${backend || 'resources'}?type=memory`, { + const { data, error } = useSWR(`/api/widgets/resources?type=memory`, { refreshInterval: 1500, }); diff --git a/src/components/widgets/resources/resources.jsx b/src/components/widgets/resources/resources.jsx index c4072c7a1..0524e39af 100644 --- a/src/components/widgets/resources/resources.jsx +++ b/src/components/widgets/resources/resources.jsx @@ -3,15 +3,15 @@ import Cpu from "./cpu"; import Memory from "./memory"; export default function Resources({ options }) { - const { expanded, backend } = options; + const { expanded } = options; return (
- {options.cpu && } - {options.memory && } + {options.cpu && } + {options.memory && } {Array.isArray(options.disk) - ? options.disk.map((disk) => ) - : options.disk && } + ? options.disk.map((disk) => ) + : options.disk && }
{options.label && (
{options.label}
diff --git a/src/components/widgets/widget.jsx b/src/components/widgets/widget.jsx index 29e7a1803..471418872 100644 --- a/src/components/widgets/widget.jsx +++ b/src/components/widgets/widget.jsx @@ -14,6 +14,7 @@ const widgetMappings = { glances: dynamic(() => import("components/widgets/glances/glances")), openmeteo: dynamic(() => import("components/widgets/openmeteo/openmeteo")), longhorn: dynamic(() => import("components/widgets/longhorn/longhorn")), + kubernetes: dynamic(() => import("components/widgets/kubernetes/kubernetes")), }; export default function Widget({ widget }) { diff --git a/src/pages/api/widgets/kubernetes.js b/src/pages/api/widgets/kubernetes.js index f589b2b94..b0d7f5531 100644 --- a/src/pages/api/widgets/kubernetes.js +++ b/src/pages/api/widgets/kubernetes.js @@ -7,8 +7,6 @@ import createLogger from "../../../utils/logger"; const logger = createLogger("kubernetes-widget"); export default async function handler(req, res) { - const { type } = req.query; - try { const kc = getKubeConfig(); if (!kc) { @@ -30,51 +28,60 @@ export default async function handler(req, res) { error: "unknown error" }); } - const nodeCapacity = new Map(); let cpuTotal = 0; let cpuUsage = 0; let memTotal = 0; let memUsage = 0; + const nodeMap = {}; 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); + const cpu = Number.parseInt(node.status.capacity.cpu, 10); + const mem = parseMemory(node.status.capacity.memory); + const ready = node.status.conditions.filter(condition => condition.type === "Ready" && condition.status === "True").length > 0; + nodeMap[node.metadata.name] = { + name: node.metadata.name, + ready, + cpu: { + total: cpu + }, + memory: { + total: mem + } + }; + cpuTotal += cpu; + memTotal += mem; }); 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); + nodeMetrics.items.forEach((nodeMetric) => { + const cpu = parseCpu(nodeMetric.usage.cpu); + const mem = parseMemory(nodeMetric.usage.memory); + cpuUsage += cpu; + memUsage += mem; + nodeMap[nodeMetric.metadata.name].cpu.load = cpu; + nodeMap[nodeMetric.metadata.name].cpu.percent = (cpu / nodeMap[nodeMetric.metadata.name].cpu.total) * 100; + nodeMap[nodeMetric.metadata.name].memory.used = mem; + nodeMap[nodeMetric.metadata.name].memory.free = nodeMap[nodeMetric.metadata.name].memory.total - mem; + nodeMap[nodeMetric.metadata.name].memory.percent = (mem / nodeMap[nodeMetric.metadata.name].memory.total) * 100; }); - 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 - } - }); - } + const cluster = { + cpu: { + load: cpuUsage, + total: cpuTotal, + percent: (cpuUsage / cpuTotal) * 100 + }, + memory: { + used: memUsage, + total: memTotal, + free: (memTotal - memUsage), + percent: (memUsage / memTotal) * 100 + } + }; - return res.status(400).json({ - error: "invalid type" + return res.status(200).json({ + cluster, + nodes: Object.entries(nodeMap).map(([name, node]) => ({ name, ...node })) }); } catch (e) { logger.error("exception %s", e); From c54374068d6ef271a7fe5757a588e60a2f8b1f3a Mon Sep 17 00:00:00 2001 From: James Wynn Date: Fri, 25 Nov 2022 10:21:51 -0600 Subject: [PATCH 07/20] fixed a formatting error and longhorn's usage bar --- src/components/widgets/kubernetes/kubernetes.jsx | 1 + src/components/widgets/longhorn/node.jsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/widgets/kubernetes/kubernetes.jsx b/src/components/widgets/kubernetes/kubernetes.jsx index a769db8f2..edd298639 100644 --- a/src/components/widgets/kubernetes/kubernetes.jsx +++ b/src/components/widgets/kubernetes/kubernetes.jsx @@ -1,6 +1,7 @@ import useSWR from "swr"; import { BiError } from "react-icons/bi"; import { useTranslation } from "next-i18next"; + import Node from "./node"; export default function Widget({ options }) { diff --git a/src/components/widgets/longhorn/node.jsx b/src/components/widgets/longhorn/node.jsx index 44b96b1c2..e0ee69afb 100644 --- a/src/components/widgets/longhorn/node.jsx +++ b/src/components/widgets/longhorn/node.jsx @@ -21,7 +21,7 @@ export default function Node({ data, expanded, labels }) {
{t("resources.total")}
)} - +
{labels && ( From 09eb172079f147a1919e092beba8601c61b29c45 Mon Sep 17 00:00:00 2001 From: James Wynn Date: Thu, 8 Dec 2022 16:03:29 -0600 Subject: [PATCH 08/20] 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 --- src/components/services/item.jsx | 4 +-- src/components/services/kubernetes-status.jsx | 28 +++++++++++++++---- .../widgets/kubernetes/kubernetes.jsx | 8 +++--- src/components/widgets/kubernetes/node.jsx | 1 - .../api/kubernetes/stats/[...service].js | 22 ++++++++------- .../api/kubernetes/status/[...service].js | 18 +++++++----- src/utils/config/service-helpers.js | 19 +++++++------ src/widgets/kubernetes/component.jsx | 5 ++-- 8 files changed, 65 insertions(+), 40 deletions(-) diff --git a/src/components/services/item.jsx b/src/components/services/item.jsx index 913b294fa..4c48c486d 100644 --- a/src/components/services/item.jsx +++ b/src/components/services/item.jsx @@ -95,7 +95,7 @@ export default function Item({ service }) {
)} diff --git a/src/components/services/kubernetes-status.jsx b/src/components/services/kubernetes-status.jsx index 4b2c57236..06dde6faa 100644 --- a/src/components/services/kubernetes-status.jsx +++ b/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
; +
+
{t("docker.error")}
+
} if (data && data.status === "running") { - return
; + return ( +
+
{data.health ?? data.status}
+
+ ); } - if (data && data.status === "not found") { - return
; + if (data && (data.status === "not found" || data.status === "down" || data.status === "partial")) { + return ( +
+
{data.status}
+
+ ); } - return
; + return ( +
+
{t("docker.unknown")}
+
+ ); } diff --git a/src/components/widgets/kubernetes/kubernetes.jsx b/src/components/widgets/kubernetes/kubernetes.jsx index edd298639..78c4caaf9 100644 --- a/src/components/widgets/kubernetes/kubernetes.jsx +++ b/src/components/widgets/kubernetes/kubernetes.jsx @@ -48,10 +48,10 @@ export default function Widget({ options }) {
{cluster.show && - + } {nodes.show && - + }
@@ -62,11 +62,11 @@ export default function Widget({ options }) {
{cluster.show && - + } {nodes.show && data.nodes && data.nodes.map((node) => - ) + ) }
diff --git a/src/components/widgets/kubernetes/node.jsx b/src/components/widgets/kubernetes/node.jsx index 0d0bde4d1..7a7c322d4 100644 --- a/src/components/widgets/kubernetes/node.jsx +++ b/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") { diff --git a/src/pages/api/kubernetes/stats/[...service].js b/src/pages/api/kubernetes/stats/[...service].js index 2ab9ba584..52ddcffa7 100644 --- a/src/pages/api/kubernetes/stats/[...service].js +++ b/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; diff --git a/src/pages/api/kubernetes/status/[...service].js b/src/pages/api/kubernetes/status/[...service].js index e76d2d7d8..34e4798c6 100644 --- a/src/pages/api/kubernetes/status/[...service].js +++ b/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 }); diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js index 2392033fe..1f5f7966f 100644 --- a/src/utils/config/service-helpers.js +++ b/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; -} \ No newline at end of file +} diff --git a/src/widgets/kubernetes/component.jsx b/src/widgets/kubernetes/component.jsx index ca3932d24..ce4dd490d 100644 --- a/src/widgets/kubernetes/component.jsx +++ b/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 ; From 27d067dc4cf15b640b0f44e1d5c5abeaef379bce Mon Sep 17 00:00:00 2001 From: James Wynn Date: Thu, 8 Dec 2022 18:31:51 -0600 Subject: [PATCH 09/20] Typo in kubernetes component --- src/widgets/kubernetes/component.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/widgets/kubernetes/component.jsx b/src/widgets/kubernetes/component.jsx index ce4dd490d..1615ebd5a 100644 --- a/src/widgets/kubernetes/component.jsx +++ b/src/widgets/kubernetes/component.jsx @@ -8,8 +8,7 @@ export default function Component({ service }) { const { t } = useTranslation(); const { widget } = service; - const podSelectorString = service.podSelector !== undefined ? `podSelector=${widget.podSelector}` : ""; - + const podSelectorString = widget.podSelector !== undefined ? `podSelector=${widget.podSelector}` : ""; const { data: statusData, error: statusError } = useSWR( `/api/kubernetes/status/${widget.namespace}/${widget.app}?${podSelectorString}`); From 8543118607e4bad75a2ac71cd70b72db5419c322 Mon Sep 17 00:00:00 2001 From: James Wynn Date: Fri, 9 Dec 2022 07:43:52 -0600 Subject: [PATCH 10/20] updated ingress selector label, added href override annotation --- src/utils/config/service-helpers.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js index 1f5f7966f..3f484772f 100644 --- a/src/utils/config/service-helpers.js +++ b/src/utils/config/service-helpers.js @@ -130,7 +130,7 @@ export async function servicesFromKubernetes() { } const networking = kc.makeApiClient(NetworkingV1Api); - const ingressList = await networking.listIngressForAllNamespaces(null, null, null, "homepage/enabled=true") + const ingressList = await networking.listIngressForAllNamespaces(null, null, null, "gethomepage.dev/enabled=true") .then((response) => response.body) .catch((error) => { logger.error("Error getting ingresses: %d %s %s", error.statusCode, error.body, error.response); @@ -143,7 +143,7 @@ export async function servicesFromKubernetes() { const constructedService = { app: ingress.metadata.name, namespace: ingress.metadata.namespace, - href: getUrlFromIngress(ingress), + href: ingress.metadata.annotations['gethomepage.dev/href'] || getUrlFromIngress(ingress), 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'] || '', From a146c13c4f86b99a565c5d2bc1bfadf9730667d3 Mon Sep 17 00:00:00 2001 From: James Wynn Date: Fri, 9 Dec 2022 07:52:32 -0600 Subject: [PATCH 11/20] fixed unintentional blank default podSelector from discovery --- src/utils/config/service-helpers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js index 3f484772f..9c4677bd1 100644 --- a/src/utils/config/service-helpers.js +++ b/src/utils/config/service-helpers.js @@ -148,7 +148,7 @@ export async function servicesFromKubernetes() { 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'] || '' + podSelector: ingress.metadata.annotations['gethomepage.dev/pod-selector'] || null }; Object.keys(ingress.metadata.annotations).forEach((annotation) => { if (annotation.startsWith("gethomepage.dev//widget/")) { From ec08535204820bd01d79c6b154ac53d4d2083845 Mon Sep 17 00:00:00 2001 From: James Wynn Date: Fri, 9 Dec 2022 07:56:51 -0600 Subject: [PATCH 12/20] fixed podSelector discovery --- src/utils/config/service-helpers.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js index 9c4677bd1..d34c07f30 100644 --- a/src/utils/config/service-helpers.js +++ b/src/utils/config/service-helpers.js @@ -148,8 +148,10 @@ export async function servicesFromKubernetes() { 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'] || null }; + if (ingress.metadata.annotations['gethomepage.dev/pod-selector']) { + constructedService.podSelector = ingress.metadata.annotations['gethomepage.dev/pod-selector']; + } Object.keys(ingress.metadata.annotations).forEach((annotation) => { if (annotation.startsWith("gethomepage.dev//widget/")) { shvl.set(constructedService, annotation.replace("homepage/widget/", ""), ingress.metadata.annotations[annotation]); From 51ff424d9822279e3948b85edb573d017e93f0a6 Mon Sep 17 00:00:00 2001 From: James Wynn Date: Fri, 9 Dec 2022 17:00:05 -0600 Subject: [PATCH 13/20] added check for nodes without disks --- src/pages/api/widgets/longhorn.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/pages/api/widgets/longhorn.js b/src/pages/api/widgets/longhorn.js index cb9ed24bd..a6b6781c8 100644 --- a/src/pages/api/widgets/longhorn.js +++ b/src/pages/api/widgets/longhorn.js @@ -16,13 +16,15 @@ function parseLonghornData(data) { let maximum = 0; let reserved = 0; let scheduled = 0; - Object.keys(node.disks).forEach((diskKey) => { - const disk = node.disks[diskKey]; - available += disk.storageAvailable; - maximum += disk.storageMaximum; - reserved += disk.storageReserved; - scheduled += disk.storageScheduled; - }); + if (node.disks) { + Object.keys(node.disks).forEach((diskKey) => { + const disk = node.disks[diskKey]; + available += disk.storageAvailable; + maximum += disk.storageMaximum; + reserved += disk.storageReserved; + scheduled += disk.storageScheduled; + }); + } return { id: node.id, available, From 7ac862be752aec4f25a8ca1252ec6d77d8fb44fa Mon Sep 17 00:00:00 2001 From: James Wynn Date: Sat, 31 Dec 2022 11:13:52 -0600 Subject: [PATCH 14/20] removed overly verbose logging message --- src/pages/api/kubernetes/status/[...service].js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/api/kubernetes/status/[...service].js b/src/pages/api/kubernetes/status/[...service].js index 34e4798c6..b52a05d0a 100644 --- a/src/pages/api/kubernetes/status/[...service].js +++ b/src/pages/api/kubernetes/status/[...service].js @@ -17,7 +17,6 @@ export default async function handler(req, res) { return; } const labelSelector = podSelector !== undefined ? podSelector : `${APP_LABEL}=${appName}`; - logger.info("labelSelector %s/%s = %s", namespace, appName, labelSelector); try { const kc = getKubeConfig(); if (!kc) { From 36ed1022e37bcdd022f9e4efa2539b226e3fafed Mon Sep 17 00:00:00 2001 From: James Wynn Date: Tue, 3 Jan 2023 16:15:08 -0600 Subject: [PATCH 15/20] detection now uses annotation "gethomepage.dev/enabled" instead of label --- src/utils/config/service-helpers.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js index e83c76f17..9fdedec02 100644 --- a/src/utils/config/service-helpers.js +++ b/src/utils/config/service-helpers.js @@ -135,7 +135,7 @@ export async function servicesFromKubernetes() { } const networking = kc.makeApiClient(NetworkingV1Api); - const ingressList = await networking.listIngressForAllNamespaces(null, null, null, "gethomepage.dev/enabled=true") + const ingressList = await networking.listIngressForAllNamespaces(null, null, null, null) .then((response) => response.body) .catch((error) => { logger.error("Error getting ingresses: %d %s %s", error.statusCode, error.body, error.response); @@ -144,7 +144,9 @@ export async function servicesFromKubernetes() { if (!ingressList) { return []; } - const services = ingressList.items.map((ingress) => { + const services = ingressList.items + .filter((ingress) => ingress.metadata.annotations && ingress.metadata.annotations['gethomepage.dev/enabled'] === 'true') + .map((ingress) => { const constructedService = { app: ingress.metadata.name, namespace: ingress.metadata.namespace, @@ -158,7 +160,7 @@ export async function servicesFromKubernetes() { constructedService.podSelector = ingress.metadata.annotations['gethomepage.dev/pod-selector']; } Object.keys(ingress.metadata.annotations).forEach((annotation) => { - if (annotation.startsWith("gethomepage.dev//widget/")) { + if (annotation.startsWith("gethomepage.dev/widget/")) { shvl.set(constructedService, annotation.replace("homepage/widget/", ""), ingress.metadata.annotations[annotation]); } }); From 9a072cdddee54af43dc20da30a6c01dd36441249 Mon Sep 17 00:00:00 2001 From: James Wynn Date: Tue, 3 Jan 2023 16:50:24 -0600 Subject: [PATCH 16/20] added documentation --- kubernetes.md | 143 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 kubernetes.md diff --git a/kubernetes.md b/kubernetes.md new file mode 100644 index 000000000..f01f54cf0 --- /dev/null +++ b/kubernetes.md @@ -0,0 +1,143 @@ +# Kubernetes Support + +## Requirements + +* Kubernetes 1.19+ +* Metrics service +* An Ingress controller + +## Deployment + +Use the unofficial helm chart: https://github.com/jameswynn/helm-charts/tree/main/charts/homepage + +```sh +helm repo add jameswynn https://jameswynn.github.io/helm-charts +helm install my-release jameswynn/homepage +``` + +### Configuration + +Set the `mode` in the `kubernetes.yaml` to `cluster`. + +```yaml +mode: default +``` + +## Widgets + +The Kubernetes widget can show a high-level overview of the cluster, +individual nodes, or both. + +```yaml +- kubernetes: + cluster: + # Shows the cluster node + show: true + # Shows the aggregate CPU stats + cpu: true + # Shows the aggregate memory stats + memory: true + # Shows a custom label + showLabel: true + label: "cluster" + nodes: + # Shows the clusters + show: true + # Shows the CPU for each node + cpu: true + # Shows the memory for each node + memory: true + # Shows the label, which is always the node name + showLabel: true +``` + +## Service Discovery + +Sample yaml: + +```yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: homepage + annotations: + gethomepage.dev/enabled: "true" + gethomepage.dev/description: Dynamically Detected Homepage + gethomepage.dev/group: Operations + gethomepage.dev/icon: homepage.png + gethomepage.dev/name: Homepage +spec: + rules: + - host: homepage.example.com + http: + paths: + - backend: + service: + name: homepage + port: + number: 3000 + path: / + pathType: Prefix +``` + +## Service Widgets + +To manually configure a Service Widget the `namespace` and `app` fields must +be configured on the service entry. + +```yaml +- Home Automation + - Home-Assistant: + icon: home-assistant.png + href: https://home.example.com + description: Home Automation + app: home-assistant + namespace: home +``` + +This works by creating a label selector `app.kubernetes.io/name=home-assistant`, +which typically will be the same both for the ingress and the deployment. However, +some deployments can be complex and will not conform to this rule. In such +cases the `podSelector` variable can bridge the gap. Any field selector can +be used in it which allows for some powerful selection capabilities. + +For instance, it can be utilized to roll multiple underlying deployments under +one application to see a high-level aggregate: + +```yaml +- Comms + - Element Chat: + icon: matrix-light.png + href: https://chat.example.com + description: Matrix Synapse Powered Chat + app: matrix-element + namespace: comms + podSelector: >- + app.kubernetes.io/instance in ( + matrix-element, + matrix-media-repo, + matrix-media-repo-postgresql, + matrix-synapse + ) +``` + +## Longhorn Widget + +There is a widget for showing storage stats from [Longhorn](https://longhorn.io). +Configure it from the `widgets.yaml`. + +```yaml +- longhorn: + # Show the expanded + expanded: true + # Shows a node representing the aggregate values + total: true + # Shows the node names as labels + labels: true + # Show the nodes + nodes: true + # An explicit list of nodes to show. All are shown by default if "nodes" is true + include: + - node1 + - node2 +``` From 4d6ce1f7e2a8dfa99225877d74a506c5ab5f4bd1 Mon Sep 17 00:00:00 2001 From: James Wynn Date: Mon, 9 Jan 2023 08:30:50 -0600 Subject: [PATCH 17/20] Widgets in discovered services now work correctly --- src/utils/config/service-helpers.js | 37 ++++++++++++++--------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js index 9fdedec02..28f4d76d4 100644 --- a/src/utils/config/service-helpers.js +++ b/src/utils/config/service-helpers.js @@ -114,18 +114,17 @@ export async function servicesFromDocker() { } function getUrlFromIngress(ingress) { - let url = ingress.metadata.annotations['homepage/url']; - if(!url) { - const urlHost = ingress.spec.rules[0].host; - const urlPath = ingress.spec.rules[0].http.paths[0].path; - const urlSchema = ingress.spec.tls ? 'https' : 'http'; - - url = `${urlSchema}://${urlHost}${urlPath}`; - } - return url; + const urlHost = ingress.spec.rules[0].host; + const urlPath = ingress.spec.rules[0].http.paths[0].path; + const urlSchema = ingress.spec.tls ? 'https' : 'http'; + return `${urlSchema}://${urlHost}${urlPath}`; } export async function servicesFromKubernetes() { + const ANNOTATION_BASE = 'gethomepage.dev'; + const ANNOTATION_WIDGET_BASE = `${ANNOTATION_BASE}/widget.`; + const ANNOTATION_POD_SELECTOR = `${ANNOTATION_BASE}/pod-selector`; + checkAndCopyConfig("kubernetes.yaml"); try { @@ -145,23 +144,23 @@ export async function servicesFromKubernetes() { return []; } const services = ingressList.items - .filter((ingress) => ingress.metadata.annotations && ingress.metadata.annotations['gethomepage.dev/enabled'] === 'true') + .filter((ingress) => ingress.metadata.annotations && ingress.metadata.annotations[`${ANNOTATION_BASE}/enabled`] === 'true') .map((ingress) => { const constructedService = { app: ingress.metadata.name, namespace: ingress.metadata.namespace, - href: ingress.metadata.annotations['gethomepage.dev/href'] || getUrlFromIngress(ingress), - 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'] || '', + href: ingress.metadata.annotations[`${ANNOTATION_BASE}/href`] || getUrlFromIngress(ingress), + name: ingress.metadata.annotations[`${ANNOTATION_BASE}/name`] || ingress.metadata.name, + group: ingress.metadata.annotations[`${ANNOTATION_BASE}/group`] || "Kubernetes", + icon: ingress.metadata.annotations[`${ANNOTATION_BASE}/icon`] || '', + description: ingress.metadata.annotations[`${ANNOTATION_BASE}/description`] || '', }; - if (ingress.metadata.annotations['gethomepage.dev/pod-selector']) { - constructedService.podSelector = ingress.metadata.annotations['gethomepage.dev/pod-selector']; + if (ingress.metadata.annotations[ANNOTATION_POD_SELECTOR]) { + constructedService.podSelector = ingress.metadata.annotations[ANNOTATION_POD_SELECTOR]; } Object.keys(ingress.metadata.annotations).forEach((annotation) => { - if (annotation.startsWith("gethomepage.dev/widget/")) { - shvl.set(constructedService, annotation.replace("homepage/widget/", ""), ingress.metadata.annotations[annotation]); + if (annotation.startsWith(ANNOTATION_WIDGET_BASE)) { + shvl.set(constructedService, annotation.replace(`${ANNOTATION_BASE}/`, ""), ingress.metadata.annotations[annotation]); } }); From 98ce0e8c2e93c787df7a4af15ffd7c19c9262465 Mon Sep 17 00:00:00 2001 From: James Wynn Date: Mon, 9 Jan 2023 10:41:06 -0600 Subject: [PATCH 18/20] Updated package lock with kubernetes deps to resolve offline builds --- pnpm-lock.yaml | 480 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 447 insertions(+), 33 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 73e30e541..c2984e5df 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2,6 +2,7 @@ lockfileVersion: 5.4 specifiers: '@headlessui/react': ^1.7.2 + '@kubernetes/client-node': ^0.17.1 '@tailwindcss/forms': ^0.5.3 autoprefixer: ^10.4.12 classnames: ^2.3.2 @@ -44,6 +45,7 @@ specifiers: dependencies: '@headlessui/react': 1.7.2_biqbaboplfbrettd7655fr4n2y + '@kubernetes/client-node': 0.17.1 classnames: 2.3.2 compare-versions: 5.0.1 dockerode: 3.3.4 @@ -171,6 +173,30 @@ packages: resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} dev: true + /@kubernetes/client-node/0.17.1: + resolution: {integrity: sha512-qXANjukuTq/drb1hq1NCYZafpdRTvbyTzbliWO6RwW7eEb2b9qwINbw0DiVHpBQg3e9DeQd8+brI1sR1Fck5kQ==} + dependencies: + byline: 5.0.0 + execa: 5.0.0 + isomorphic-ws: 4.0.1_ws@7.5.9 + js-yaml: 4.1.0 + jsonpath-plus: 0.19.0 + request: 2.88.2 + rfc4648: 1.5.2 + shelljs: 0.8.5 + stream-buffers: 3.0.2 + tar: 6.1.13 + tmp-promise: 3.0.3 + tslib: 1.14.1 + underscore: 1.13.6 + ws: 7.5.9 + optionalDependencies: + openid-client: 5.3.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: false + /@next/env/12.3.1: resolution: {integrity: sha512-9P9THmRFVKGKt9DYqeC2aKIxm8rlvkK38V1P1sRE7qyoPBIs8l9oo79QoSdPtOWfzkbDAVUqvbQGgTMsb8BtJg==} dev: false @@ -467,7 +493,6 @@ packages: fast-json-stable-stringify: 2.1.0 json-schema-traverse: 0.4.1 uri-js: 4.4.1 - dev: true /ansi-regex/5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} @@ -546,6 +571,11 @@ packages: safer-buffer: 2.1.2 dev: false + /assert-plus/1.0.0: + resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} + engines: {node: '>=0.8'} + dev: false + /ast-types-flow/0.0.7: resolution: {integrity: sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==} dev: true @@ -574,6 +604,14 @@ packages: postcss-value-parser: 4.2.0 dev: true + /aws-sign2/0.7.0: + resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==} + dev: false + + /aws4/1.12.0: + resolution: {integrity: sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==} + dev: false + /axe-core/4.4.3: resolution: {integrity: sha512-32+ub6kkdhhWick/UjvEwRchgoetXqTK14INLqbGm5U2TzBkBNF3nQtLYm8ovxSkQWArjEQvftCKryjZaATu3w==} engines: {node: '>=4'} @@ -585,7 +623,6 @@ packages: /balanced-match/1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - dev: true /base64-js/1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -615,7 +652,6 @@ packages: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - dev: true /braces/3.0.2: resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} @@ -648,6 +684,11 @@ packages: dev: false optional: true + /byline/5.0.0: + resolution: {integrity: sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==} + engines: {node: '>=0.10.0'} + dev: false + /bytes/3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -673,6 +714,10 @@ packages: /caniuse-lite/1.0.30001410: resolution: {integrity: sha512-QoblBnuE+rG0lc3Ur9ltP5q47lbguipa/ncNMyyGuqPk44FxbScWAeEO+k5fSQ8WekdAK4mWqNs1rADDAiN5xQ==} + /caseless/0.12.0: + resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} + dev: false + /chalk/4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -700,6 +745,11 @@ packages: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} dev: false + /chownr/2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + dev: false + /classnames/2.3.2: resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==} dev: false @@ -758,7 +808,6 @@ packages: /concat-map/0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - dev: true /confusing-browser-globals/1.0.11: resolution: {integrity: sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==} @@ -774,13 +823,17 @@ packages: requiresBuild: true dev: false + /core-util-is/1.0.2: + resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} + dev: false + /cpu-features/0.0.4: resolution: {integrity: sha512-fKiZ/zp1mUwQbnzb9IghXtHtDoTMtNeb8oYGx6kX2SYfhnG0HNdBEBIzB9b5KlXu5DQPhfy3mInbBxFcgwAr3A==} engines: {node: '>=10.0.0'} requiresBuild: true dependencies: buildcheck: 0.0.3 - nan: 2.16.0 + nan: 2.17.0 dev: false optional: true @@ -791,7 +844,6 @@ packages: path-key: 3.1.1 shebang-command: 2.0.0 which: 2.0.2 - dev: true /cssesc/3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} @@ -807,6 +859,13 @@ packages: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} dev: true + /dashdash/1.14.1: + resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==} + engines: {node: '>=0.10'} + dependencies: + assert-plus: 1.0.0 + dev: false + /debug/2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -928,6 +987,13 @@ packages: esutils: 2.0.3 dev: true + /ecc-jsbn/0.1.2: + resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==} + dependencies: + jsbn: 0.1.1 + safer-buffer: 2.1.2 + dev: false + /electron-to-chromium/1.4.261: resolution: {integrity: sha512-fVXliNUGJ7XUVJSAasPseBbVgJIeyw5M1xIkgXdTSRjlmCqBbiSTsEdLOCJS31Fc8B7CaloQ/BFAg8By3ODLdg==} dev: true @@ -1396,9 +1462,32 @@ packages: engines: {node: '>=0.10.0'} dev: true + /execa/5.0.0: + resolution: {integrity: sha512-ov6w/2LCiuyO4RLYGdpFGjkcs0wMTgGE8PrkTHikeUy5iJekXyPIKUjifk5CsE0pt7sMCrMZ3YNqoCj6idQOnQ==} + engines: {node: '>=10'} + dependencies: + cross-spawn: 7.0.3 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + dev: false + + /extend/3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + dev: false + + /extsprintf/1.3.0: + resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==} + engines: {'0': node >=0.6.0} + dev: false + /fast-deep-equal/3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - dev: true /fast-diff/1.2.0: resolution: {integrity: sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==} @@ -1417,7 +1506,6 @@ packages: /fast-json-stable-stringify/2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - dev: true /fast-levenshtein/2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} @@ -1481,6 +1569,19 @@ packages: optional: true dev: false + /forever-agent/0.6.1: + resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} + dev: false + + /form-data/2.3.3: + resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==} + engines: {node: '>= 0.12'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: false + /form-data/3.0.1: resolution: {integrity: sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==} engines: {node: '>= 6'} @@ -1498,9 +1599,15 @@ packages: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} dev: false + /fs-minipass/2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + dependencies: + minipass: 3.3.6 + dev: false + /fs.realpath/1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - dev: true /fsevents/2.3.2: resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} @@ -1512,7 +1619,6 @@ packages: /function-bind/1.1.1: resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} - dev: true /function.prototype.name/1.1.5: resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==} @@ -1536,6 +1642,11 @@ packages: has-symbols: 1.0.3 dev: true + /get-stream/6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + dev: false + /get-symbol-description/1.0.0: resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} engines: {node: '>= 0.4'} @@ -1544,6 +1655,12 @@ packages: get-intrinsic: 1.1.3 dev: true + /getpass/0.1.7: + resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==} + dependencies: + assert-plus: 1.0.0 + dev: false + /glob-parent/5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -1578,7 +1695,6 @@ packages: minimatch: 3.1.2 once: 1.4.0 path-is-absolute: 1.0.1 - dev: true /globals/13.17.0: resolution: {integrity: sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==} @@ -1603,6 +1719,20 @@ packages: resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} dev: true + /har-schema/2.0.0: + resolution: {integrity: sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==} + engines: {node: '>=4'} + dev: false + + /har-validator/5.1.5: + resolution: {integrity: sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==} + engines: {node: '>=6'} + deprecated: this library is no longer supported + dependencies: + ajv: 6.12.6 + har-schema: 2.0.0 + dev: false + /has-bigints/1.0.2: resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} dev: true @@ -1635,7 +1765,6 @@ packages: engines: {node: '>= 0.4.0'} dependencies: function-bind: 1.1.1 - dev: true /hoist-non-react-statics/3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} @@ -1660,6 +1789,20 @@ packages: toidentifier: 1.0.1 dev: false + /http-signature/1.2.0: + resolution: {integrity: sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==} + engines: {node: '>=0.8', npm: '>=1.3.7'} + dependencies: + assert-plus: 1.0.0 + jsprim: 1.4.2 + sshpk: 1.17.0 + dev: false + + /human-signals/2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + dev: false + /i18next-fs-backend/1.1.5: resolution: {integrity: sha512-raTel3EfshiUXxR0gvmIoqp75jhkj8+7R1LjB006VZKPTFBbXyx6TlUVhb8Z9+7ahgpFbcQg1QWVOdf/iNzI5A==} dev: false @@ -1704,7 +1847,6 @@ packages: dependencies: once: 1.4.0 wrappy: 1.0.2 - dev: true /inherits/2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -1718,6 +1860,11 @@ packages: side-channel: 1.0.4 dev: true + /interpret/1.4.0: + resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} + engines: {node: '>= 0.10'} + dev: false + /is-arrayish/0.3.2: resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} dev: false @@ -1752,7 +1899,6 @@ packages: resolution: {integrity: sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==} dependencies: has: 1.0.3 - dev: true /is-date-object/1.0.5: resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} @@ -1823,6 +1969,10 @@ packages: has-symbols: 1.0.3 dev: true + /is-typedarray/1.0.0: + resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} + dev: false + /is-weakref/1.0.2: resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} dependencies: @@ -1831,7 +1981,23 @@ packages: /isexe/2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - dev: true + + /isomorphic-ws/4.0.1_ws@7.5.9: + resolution: {integrity: sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==} + peerDependencies: + ws: '*' + dependencies: + ws: 7.5.9 + dev: false + + /isstream/0.1.2: + resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==} + dev: false + + /jose/4.11.2: + resolution: {integrity: sha512-njj0VL2TsIxCtgzhO+9RRobBvws4oYyCM8TpvoUQwl/MbIM3NFJRR9+e6x0sS5xXaP1t6OCBkaBME98OV9zU5A==} + dev: false + optional: true /js-sdsl/4.1.4: resolution: {integrity: sha512-Y2/yD55y5jteOAmY50JbUZYwk3CP3wnLPEZnlR1w9oKhITrBEtAxwuWKebFf8hMrPMgbYwFoWK/lH2sBkErELw==} @@ -1846,18 +2012,29 @@ packages: dependencies: argparse: 2.0.1 + /jsbn/0.1.1: + resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==} + dev: false + /json-rpc-2.0/1.4.1: resolution: {integrity: sha512-OX1NJhpIfuK4GjDnJ/gKtZy1HOYo0l4eL0a4rb0rNeQheX1xlyQ9+JMmPzs/sFNthpS/TXKPWlGo09X7B5l81A==} dev: false /json-schema-traverse/0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - dev: true + + /json-schema/0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + dev: false /json-stable-stringify-without-jsonify/1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} dev: true + /json-stringify-safe/5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + dev: false + /json5/1.0.1: resolution: {integrity: sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==} hasBin: true @@ -1865,6 +2042,21 @@ packages: minimist: 1.2.6 dev: true + /jsonpath-plus/0.19.0: + resolution: {integrity: sha512-GSVwsrzW9LsA5lzsqe4CkuZ9wp+kxBb2GwNniaWzI2YFn5Ig42rSW8ZxVpWXaAfakXNrx5pgY5AbQq7kzX29kg==} + engines: {node: '>=6.0'} + dev: false + + /jsprim/1.4.2: + resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==} + engines: {node: '>=0.6.0'} + dependencies: + assert-plus: 1.0.0 + extsprintf: 1.3.0 + json-schema: 0.4.0 + verror: 1.10.0 + dev: false + /jsx-ast-utils/3.3.3: resolution: {integrity: sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==} engines: {node: '>=4.0'} @@ -1932,12 +2124,15 @@ packages: engines: {node: '>=10'} dependencies: yallist: 4.0.0 - dev: true /memory-cache/0.2.0: resolution: {integrity: sha512-OcjA+jzjOYzKmKS6IQVALHLVz+rNTMPoJvCztFaZxwG14wtAW7VRZjwTQu06vKCYOxh4jVnik7ya0SXTB0W+xA==} dev: false + /merge-stream/2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + dev: false + /merge2/1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1963,6 +2158,11 @@ packages: mime-db: 1.52.0 dev: false + /mimic-fn/2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + dev: false + /mini-svg-data-uri/1.4.4: resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} hasBin: true @@ -1972,16 +2172,43 @@ packages: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} dependencies: brace-expansion: 1.1.11 - dev: true /minimist/1.2.6: resolution: {integrity: sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==} dev: true + /minipass/3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + dependencies: + yallist: 4.0.0 + dev: false + + /minipass/4.0.0: + resolution: {integrity: sha512-g2Uuh2jEKoht+zvO6vJqXmYpflPqzRBT+Th2h01DKh5z7wbY/AZ2gCQ78cP70YoHPyFdY30YBV5WxgLOEwOykw==} + engines: {node: '>=8'} + dependencies: + yallist: 4.0.0 + dev: false + + /minizlib/2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + dev: false + /mkdirp-classic/0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} dev: false + /mkdirp/1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + dev: false + /ms/2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} dev: true @@ -1992,8 +2219,8 @@ packages: /ms/2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - /nan/2.16.0: - resolution: {integrity: sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA==} + /nan/2.17.0: + resolution: {integrity: sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==} dev: false optional: true @@ -2102,11 +2329,28 @@ packages: engines: {node: '>=0.10.0'} dev: true + /npm-run-path/4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + dependencies: + path-key: 3.1.1 + dev: false + + /oauth-sign/0.9.0: + resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==} + dev: false + /object-assign/4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} dev: true + /object-hash/2.2.0: + resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==} + engines: {node: '>= 6'} + dev: false + optional: true + /object-hash/3.0.0: resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} engines: {node: '>= 6'} @@ -2165,6 +2409,12 @@ packages: es-abstract: 1.20.3 dev: true + /oidc-token-hash/5.0.1: + resolution: {integrity: sha512-EvoOtz6FIEBzE+9q253HsLCVRiK/0doEJ2HCvvqMQb3dHZrP3WlJKYtJ55CRTw4jmYomzH4wkPuCj/I3ZvpKxQ==} + engines: {node: ^10.13.0 || >=12.0.0} + dev: false + optional: true + /once/1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: @@ -2176,6 +2426,24 @@ packages: fn.name: 1.1.0 dev: false + /onetime/5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + dependencies: + mimic-fn: 2.1.0 + dev: false + + /openid-client/5.3.1: + resolution: {integrity: sha512-RLfehQiHch9N6tRWNx68cicf3b1WR0x74bJWHRc25uYIbSRwjxYcTFaRnzbbpls5jroLAaB/bFIodTgA5LJMvw==} + requiresBuild: true + dependencies: + jose: 4.11.2 + lru-cache: 6.0.0 + object-hash: 2.2.0 + oidc-token-hash: 5.0.1 + dev: false + optional: true + /optionator/0.9.1: resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==} engines: {node: '>= 0.8.0'} @@ -2217,22 +2485,23 @@ packages: /path-is-absolute/1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} - dev: true /path-key/3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} - dev: true /path-parse/1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - dev: true /path-type/4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} dev: true + /performance-now/2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + dev: false + /picocolors/1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} @@ -2371,6 +2640,11 @@ packages: resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==} engines: {node: '>=6'} + /qs/6.5.3: + resolution: {integrity: sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==} + engines: {node: '>=0.6'} + dev: false + /querystringify/2.2.0: resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} dev: false @@ -2464,6 +2738,13 @@ packages: picomatch: 2.3.1 dev: true + /rechoir/0.6.2: + resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} + engines: {node: '>= 0.10'} + dependencies: + resolve: 1.22.1 + dev: false + /regenerator-runtime/0.13.9: resolution: {integrity: sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==} @@ -2481,6 +2762,33 @@ packages: engines: {node: '>=8'} dev: true + /request/2.88.2: + resolution: {integrity: sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==} + engines: {node: '>= 6'} + deprecated: request has been deprecated, see https://github.com/request/request/issues/3142 + dependencies: + aws-sign2: 0.7.0 + aws4: 1.12.0 + caseless: 0.12.0 + combined-stream: 1.0.8 + extend: 3.0.2 + forever-agent: 0.6.1 + form-data: 2.3.3 + har-validator: 5.1.5 + http-signature: 1.2.0 + is-typedarray: 1.0.0 + isstream: 0.1.2 + json-stringify-safe: 5.0.1 + mime-types: 2.1.35 + oauth-sign: 0.9.0 + performance-now: 2.1.0 + qs: 6.5.3 + safe-buffer: 5.2.1 + tough-cookie: 2.5.0 + tunnel-agent: 0.6.0 + uuid: 3.4.0 + dev: false + /requires-port/1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} dev: false @@ -2497,7 +2805,6 @@ packages: is-core-module: 2.10.0 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - dev: true /resolve/2.0.0-next.4: resolution: {integrity: sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==} @@ -2513,12 +2820,15 @@ packages: engines: {iojs: '>=1.0.0', node: '>=0.10.0'} dev: true + /rfc4648/1.5.2: + resolution: {integrity: sha512-tLOizhR6YGovrEBLatX1sdcuhoSCXddw3mqNVAcKxGJ+J0hFeJ+SjeWCv5UPA/WU3YzWPPuCVYgXBKZUPGpKtg==} + dev: false + /rimraf/3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} hasBin: true dependencies: glob: 7.2.3 - dev: true /run-parallel/1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -2588,12 +2898,20 @@ packages: engines: {node: '>=8'} dependencies: shebang-regex: 3.0.0 - dev: true /shebang-regex/3.0.0: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - dev: true + + /shelljs/0.8.5: + resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==} + engines: {node: '>=4'} + hasBin: true + dependencies: + glob: 7.2.3 + interpret: 1.4.0 + rechoir: 0.6.2 + dev: false /shvl/3.0.0: resolution: {integrity: sha512-5IomAM3ykE/g9K9L6lhODc+TpCuN03rrhlboegeKyyfh66DDdpRD5JN37DYhNHH+RaYjiIDx64K/Ms/xQYOR5w==} @@ -2607,6 +2925,10 @@ packages: object-inspect: 1.12.2 dev: true + /signal-exit/3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + dev: false + /simple-swizzle/0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} dependencies: @@ -2635,7 +2957,23 @@ packages: bcrypt-pbkdf: 1.0.2 optionalDependencies: cpu-features: 0.0.4 - nan: 2.16.0 + nan: 2.17.0 + dev: false + + /sshpk/1.17.0: + resolution: {integrity: sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==} + engines: {node: '>=0.10.0'} + hasBin: true + dependencies: + asn1: 0.2.6 + assert-plus: 1.0.0 + bcrypt-pbkdf: 1.0.2 + dashdash: 1.14.1 + ecc-jsbn: 0.1.2 + getpass: 0.1.7 + jsbn: 0.1.1 + safer-buffer: 2.1.2 + tweetnacl: 0.14.5 dev: false /stack-trace/0.0.10: @@ -2647,6 +2985,11 @@ packages: engines: {node: '>= 0.8'} dev: false + /stream-buffers/3.0.2: + resolution: {integrity: sha512-DQi1h8VEBA/lURbSwFtEHnSTb9s2/pwLEaFuNhXwy1Dx3Sa0lOuYT2yNUr4/j2fs8oCAMANtrZ5OrPZtyVs3MQ==} + engines: {node: '>= 0.10.0'} + dev: false + /string.prototype.matchall/4.0.7: resolution: {integrity: sha512-f48okCX7JiwVi1NXCVWcFnZgADDC/n2vePlQ/KUCNqCikLLilQvwjMO8+BHVKvgzH0JB0J9LEPgxOGT02RoETg==} dependencies: @@ -2694,6 +3037,11 @@ packages: engines: {node: '>=4'} dev: true + /strip-final-newline/2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + dev: false + /strip-json-comments/3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -2725,7 +3073,6 @@ packages: /supports-preserve-symlinks-flag/1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - dev: true /swr/1.3.0_react@18.2.0: resolution: {integrity: sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw==} @@ -2797,6 +3144,18 @@ packages: readable-stream: 3.6.0 dev: false + /tar/6.1.13: + resolution: {integrity: sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw==} + engines: {node: '>=10'} + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 4.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + dev: false + /text-hex/1.0.0: resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} dev: false @@ -2805,6 +3164,19 @@ packages: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: true + /tmp-promise/3.0.3: + resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==} + dependencies: + tmp: 0.2.1 + dev: false + + /tmp/0.2.1: + resolution: {integrity: sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==} + engines: {node: '>=8.17.0'} + dependencies: + rimraf: 3.0.2 + dev: false + /to-regex-range/5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -2817,6 +3189,14 @@ packages: engines: {node: '>=0.6'} dev: false + /tough-cookie/2.5.0: + resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==} + engines: {node: '>=0.8'} + dependencies: + psl: 1.9.0 + punycode: 2.1.1 + dev: false + /tough-cookie/4.1.2: resolution: {integrity: sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==} engines: {node: '>=6'} @@ -2846,7 +3226,6 @@ packages: /tslib/1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} - dev: true /tslib/2.4.0: resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==} @@ -2862,6 +3241,12 @@ packages: typescript: 4.8.3 dev: true + /tunnel-agent/0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + dependencies: + safe-buffer: 5.2.1 + dev: false + /tweetnacl/0.14.5: resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} dev: false @@ -2893,6 +3278,10 @@ packages: which-boxed-primitive: 1.0.2 dev: true + /underscore/1.13.6: + resolution: {integrity: sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==} + dev: false + /universalify/0.2.0: resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} engines: {node: '>= 4.0.0'} @@ -2918,7 +3307,6 @@ packages: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} dependencies: punycode: 2.1.1 - dev: true /url-parse/1.5.10: resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} @@ -2938,6 +3326,21 @@ packages: /util-deprecate/1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + /uuid/3.4.0: + resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} + deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. + hasBin: true + dev: false + + /verror/1.10.0: + resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} + engines: {'0': node >=0.6.0} + dependencies: + assert-plus: 1.0.0 + core-util-is: 1.0.2 + extsprintf: 1.3.0 + dev: false + /void-elements/3.1.0: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} engines: {node: '>=0.10.0'} @@ -2970,7 +3373,6 @@ packages: hasBin: true dependencies: isexe: 2.0.0 - dev: true /winston-transport/4.5.0: resolution: {integrity: sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==} @@ -3006,6 +3408,19 @@ packages: /wrappy/1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + /ws/7.5.9: + resolution: {integrity: sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + /xml-js/1.6.11: resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==} hasBin: true @@ -3020,7 +3435,6 @@ packages: /yallist/4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - dev: true /yaml/1.10.2: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} From b724f520cd5b8bf126a1f93bd9b67fb166bfdb3b Mon Sep 17 00:00:00 2001 From: James Wynn Date: Wed, 11 Jan 2023 09:47:34 -0600 Subject: [PATCH 19/20] added k3d test scripts --- .dockerignore | 2 ++ Dockerfile-tilt | 22 ++++++++++++ k3d/.envrc | 2 ++ k3d/.gitignore | 2 ++ k3d/README.md | 64 +++++++++++++++++++++++++++++++++ k3d/Tiltfile | 24 +++++++++++++ k3d/k3d-deploy.sh | 14 ++++++++ k3d/k3d-down.sh | 4 +++ k3d/k3d-helm-values.yaml | 77 ++++++++++++++++++++++++++++++++++++++++ k3d/k3d-up.sh | 9 +++++ k3d/k3d.yaml | 59 ++++++++++++++++++++++++++++++ kubernetes.md | 4 +++ 12 files changed, 283 insertions(+) create mode 100644 Dockerfile-tilt create mode 100644 k3d/.envrc create mode 100644 k3d/.gitignore create mode 100644 k3d/README.md create mode 100644 k3d/Tiltfile create mode 100755 k3d/k3d-deploy.sh create mode 100755 k3d/k3d-down.sh create mode 100644 k3d/k3d-helm-values.yaml create mode 100755 k3d/k3d-up.sh create mode 100644 k3d/k3d.yaml diff --git a/.dockerignore b/.dockerignore index 402dc84b0..edbf85256 100644 --- a/.dockerignore +++ b/.dockerignore @@ -20,5 +20,7 @@ **/obj **/secrets.dev.yaml **/values.dev.yaml +**/.next README.md config/ +k3d/ diff --git a/Dockerfile-tilt b/Dockerfile-tilt new file mode 100644 index 000000000..431ef2a66 --- /dev/null +++ b/Dockerfile-tilt @@ -0,0 +1,22 @@ +# syntax = docker/dockerfile:latest +FROM docker.io/node:18-alpine + +WORKDIR /app + +COPY --link package.json pnpm-lock.yaml* ./ + +RUN < /dev/null; then + helm repo add $HELM_REPO_NAME $HELM_REPO_URL + helm repo update +fi + +helm upgrade --install homepage jameswynn/homepage -f k3d-helm-values.yaml diff --git a/k3d/k3d-down.sh b/k3d/k3d-down.sh new file mode 100755 index 000000000..668643d94 --- /dev/null +++ b/k3d/k3d-down.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +k3d cluster delete homepage +rm kubeconfig diff --git a/k3d/k3d-helm-values.yaml b/k3d/k3d-helm-values.yaml new file mode 100644 index 000000000..1fe61bf1b --- /dev/null +++ b/k3d/k3d-helm-values.yaml @@ -0,0 +1,77 @@ +image: + repository: k3d-registry.localhost:55000/homepage + tag: local + pullPolicy: IfNotPresent + +config: + bookmarks: + - Developer: + - Github: + - abbr: GH + href: https://github.com/ + services: + - My First Group: + - My First Service: + href: http://localhost/ + description: Homepage is awesome + + - My Second Group: + - My Second Service: + href: http://localhost/ + description: Homepage is the best + + - My Third Group: + - My Third Service: + href: http://localhost/ + description: Homepage is 😎 + widgets: + # show the kubernetes widget, with the cluster summary and individual nodes + - kubernetes: + cluster: + show: true + cpu: true + memory: true + showLabel: true + label: "cluster" + nodes: + show: true + cpu: true + memory: true + showLabel: true + - search: + provider: duckduckgo + target: _blank + kubernetes: + mode: cluster + docker: + settings: + +serviceAccount: + create: true + name: homepage + +enableRbac: true + +ingress: + main: + enabled: true + annotations: + gethomepage.dev/enabled: "true" + gethomepage.dev/name: "Homepage" + gethomepage.dev/description: "Dynamically Detected Homepage" + gethomepage.dev/group: "Dynamic" + gethomepage.dev/icon: "homepage.png" + hosts: + - host: homepage.k3d.localhost + paths: + - path: / + pathType: Prefix + +persistence: + # this persists the .next directory which greatly improves successive pod startup times + dotnext: + enabled: true + type: pvc + accessMode: ReadWriteOnce + size: 1Gi + mountPath: /app/.next diff --git a/k3d/k3d-up.sh b/k3d/k3d-up.sh new file mode 100755 index 000000000..c90af8f04 --- /dev/null +++ b/k3d/k3d-up.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +k3d cluster create --config k3d.yaml --wait +k3d kubeconfig get homepage > kubeconfig +chmod 600 kubeconfig +export KUBECONFIG=$(pwd)/kubeconfig + +echo "Waiting for traefik install job to complete (CTRL+C is safe if you're impatient)" +kubectl wait jobs/helm-install-traefik -n kube-system --for condition=complete --timeout 90s && echo "Completed" || echo "Timed out" diff --git a/k3d/k3d.yaml b/k3d/k3d.yaml new file mode 100644 index 000000000..e976c5c34 --- /dev/null +++ b/k3d/k3d.yaml @@ -0,0 +1,59 @@ +kind: Simple +apiVersion: k3d.io/v1alpha3 +name: homepage +servers: 1 +agents: 2 +kubeAPI: + hostIP: 0.0.0.0 + hostPort: "6443" +image: rancher/k3s:v1.25.5-k3s1 +volumes: + - volume: /tmp:/tmp/k3d-homepage + nodeFilters: + - all +ports: + - port: 8080:80 + nodeFilters: + - loadbalancer + - port: 0.0.0.0:8443:443 + nodeFilters: + - loadbalancer +options: + k3d: + wait: true + timeout: 6m0s + disableLoadbalancer: false + disableImageVolume: false + disableRollback: false + k3s: + extraArgs: + - arg: --tls-san=127.0.0.1 + nodeFilters: + - server:* + nodeLabels: [] + kubeconfig: + updateDefaultKubeconfig: false + switchCurrentContext: false + runtime: + gpuRequest: "" + serversMemory: "1024Mi" + agentsMemory: "1024Mi" + labels: + - label: foo=bar + nodeFilters: + - server:0 + - loadbalancer +env: + - envVar: bar=baz + nodeFilters: + - all +registries: + create: + name: k3d-registry +# host: 0.0.0.0 + hostPort: "55000" + config: | + mirrors: + "k3d-registry.localhost:55000": + endpoint: + - http://k3d-registry:5000 diff --git a/kubernetes.md b/kubernetes.md index f01f54cf0..0035ddd2e 100644 --- a/kubernetes.md +++ b/kubernetes.md @@ -141,3 +141,7 @@ Configure it from the `widgets.yaml`. - node1 - node2 ``` + +## Testing + +Refer to the [k3d readme](k3d/README.md). From 725189a7b0bb6d69331b0dca5beb805025d5f7ca Mon Sep 17 00:00:00 2001 From: James Wynn Date: Wed, 18 Jan 2023 10:05:12 -0600 Subject: [PATCH 20/20] Issue with dotnext PVC preventing normal deployments * fixed k3d-deploy.sh directory reference --- k3d/Tiltfile | 3 ++- k3d/k3d-deploy.sh | 2 +- k3d/k3d-helm-values.yaml | 7 ++++--- k3d/k3d-up.sh | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/k3d/Tiltfile b/k3d/Tiltfile index 52c2687bb..84c7f1544 100644 --- a/k3d/Tiltfile +++ b/k3d/Tiltfile @@ -19,6 +19,7 @@ helm_resource('homepage', 'jameswynn/homepage', ], # image_selector= "k3d-registry.localhost:55000/homepage:local", flags=[ - "-f", "k3d-helm-values.yaml" + "-f", "k3d-helm-values.yaml", + "--set", "persistence.dotnext.enabled=true" ] ) diff --git a/k3d/k3d-deploy.sh b/k3d/k3d-deploy.sh index 3379db8f1..ef00c6ff4 100755 --- a/k3d/k3d-deploy.sh +++ b/k3d/k3d-deploy.sh @@ -1,6 +1,6 @@ #!/bin/bash -DOCKER_BUILDKIT=1 docker build -t k3d-registry.localhost:55000/homepage:local . +DOCKER_BUILDKIT=1 docker build -t k3d-registry.localhost:55000/homepage:local .. docker push k3d-registry.localhost:55000/homepage:local HELM_REPO_NAME=jameswynn diff --git a/k3d/k3d-helm-values.yaml b/k3d/k3d-helm-values.yaml index 1fe61bf1b..60b6fe381 100644 --- a/k3d/k3d-helm-values.yaml +++ b/k3d/k3d-helm-values.yaml @@ -1,7 +1,7 @@ image: repository: k3d-registry.localhost:55000/homepage tag: local - pullPolicy: IfNotPresent + pullPolicy: Always config: bookmarks: @@ -68,9 +68,10 @@ ingress: pathType: Prefix persistence: - # this persists the .next directory which greatly improves successive pod startup times + # this persists the .next directory which greatly improves successive pod startup times in Tilt, + # but it breaks normal deployments, so it is disabled by default dotnext: - enabled: true + enabled: false type: pvc accessMode: ReadWriteOnce size: 1Gi diff --git a/k3d/k3d-up.sh b/k3d/k3d-up.sh index c90af8f04..e16104012 100755 --- a/k3d/k3d-up.sh +++ b/k3d/k3d-up.sh @@ -6,4 +6,4 @@ chmod 600 kubeconfig export KUBECONFIG=$(pwd)/kubeconfig echo "Waiting for traefik install job to complete (CTRL+C is safe if you're impatient)" -kubectl wait jobs/helm-install-traefik -n kube-system --for condition=complete --timeout 90s && echo "Completed" || echo "Timed out" +kubectl wait jobs/helm-install-traefik -n kube-system --for condition=complete --timeout 90s && echo "Completed" || echo "Timed out (but it should still come up eventually)"