From 73aa2018a8ff68dd828687b946aaccc5a9fd7105 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albin=20M=C3=A9doc?= Date: Fri, 13 Jun 2025 09:17:20 +0200 Subject: [PATCH] Enhancement: add kubernetes support to portainer widget (#5414) Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com> --- docs/widgets/services/portainer.md | 6 ++- src/utils/config/service-helpers.js | 6 +++ src/widgets/portainer/component.jsx | 67 +++++++++++++++++++++++++---- src/widgets/portainer/widget.js | 15 +++++-- 4 files changed, 81 insertions(+), 13 deletions(-) diff --git a/docs/widgets/services/portainer.md b/docs/widgets/services/portainer.md index f18d4eec4..d5e6380e3 100644 --- a/docs/widgets/services/portainer.md +++ b/docs/widgets/services/portainer.md @@ -7,12 +7,16 @@ Learn more about [Portainer](https://github.com/portainer/portainer). You'll need to make sure you have the correct environment set for the integration to work properly. From the Environments section inside of Portainer, click the one you'd like to connect to and observe the ID at the end of the URL (should be), something like `#!/endpoints/1`, here `1` is the value to set as the `env` value. In order to generate an API key, please follow the steps outlined here https://docs.portainer.io/api/access. -Allowed fields: `["running", "stopped", "total"]`. +Allowed fields: + +- For Docker mode (default): `["running", "stopped", "total"]` +- For Kubernetes mode (`kubernetes: true`): `["applications", "services", "namespaces"]` ```yaml widget: type: portainer url: https://portainer.host.or.ip:9443 env: 1 + kubernetes: true # optional, defaults to false key: ptr_accesskeyaccesskeyaccesskeyaccesskey ``` diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js index d46c86057..68de39449 100644 --- a/src/utils/config/service-helpers.js +++ b/src/utils/config/service-helpers.js @@ -367,6 +367,9 @@ export function cleanServiceGroups(groups) { // opnsense, pfsense wan, + // portainer + kubernetes, + // prometheusmetric metrics, @@ -448,6 +451,9 @@ export function cleanServiceGroups(groups) { if (type === "unifi") { if (site) widget.site = site; } + if (type === "portainer") { + if (kubernetes) widget.kubernetes = !!JSON.parse(kubernetes); + } if (type === "proxmox") { if (node) widget.node = node; } diff --git a/src/widgets/portainer/component.jsx b/src/widgets/portainer/component.jsx index f8a89507d..d141bc306 100644 --- a/src/widgets/portainer/component.jsx +++ b/src/widgets/portainer/component.jsx @@ -6,15 +6,64 @@ import useWidgetAPI from "utils/proxy/use-widget-api"; export default function Component({ service }) { const { widget } = service; - const { data: containersData, error: containersError } = useWidgetAPI(widget, "docker/containers/json", { - all: 1, - }); + if (!widget.fields) { + widget.fields = widget.kubernetes ? ["applications", "services", "namespaces"] : ["running", "stopped", "total"]; + } + + const { data: containersCount, error: containersError } = useWidgetAPI( + widget, + widget.kubernetes ? "" : "docker/containers", + { + all: 1, + }, + ); + + const { data: applicationsCount, error: applicationsError } = useWidgetAPI( + widget, + widget.kubernetes ? "kubernetes/applications" : "", + ); + + const { data: servicesCount, error: servicesError } = useWidgetAPI( + widget, + widget.kubernetes ? "kubernetes/services" : "", + ); + + const { data: namespacesCount, error: namespacesError } = useWidgetAPI( + widget, + widget.kubernetes ? "kubernetes/namespaces" : "", + ); + + if (widget.kubernetes) { + // count can be an error object + const error = applicationsError ?? servicesError ?? namespacesError ?? applicationsCount; + if (error) { + return ; + } + + if (applicationsCount == undefined || servicesCount == undefined || namespacesCount == undefined) { + return ( + + + + + + ); + } + + return ( + + + + + + ); + } if (containersError) { return ; } - if (!containersData) { + if (!containersCount) { return ( @@ -24,14 +73,14 @@ export default function Component({ service }) { ); } - if (containersData.error || containersData.message) { + if (containersCount.error || containersCount.message) { // containersData can be itself an error object e.g. if environment fails - return ; + return ; } - const running = containersData.filter((c) => c.State === "running").length; - const stopped = containersData.filter((c) => c.State === "exited").length; - const total = containersData.length; + const running = containersCount.filter((c) => c.State === "running").length; + const stopped = containersCount.filter((c) => c.State === "exited").length; + const total = containersCount.length; return ( diff --git a/src/widgets/portainer/widget.js b/src/widgets/portainer/widget.js index ca3d5bb0b..c47c88377 100644 --- a/src/widgets/portainer/widget.js +++ b/src/widgets/portainer/widget.js @@ -1,14 +1,23 @@ import credentialedProxyHandler from "utils/proxy/handlers/credentialed"; const widget = { - api: "{url}/api/endpoints/{env}/{endpoint}", + api: "{url}/api/{endpoint}", proxyHandler: credentialedProxyHandler, mappings: { - "docker/containers/json": { - endpoint: "docker/containers/json", + "docker/containers": { + endpoint: "endpoints/{env}/docker/containers/json", params: ["all"], }, + "kubernetes/applications": { + endpoint: "kubernetes/{env}/applications/count", + }, + "kubernetes/services": { + endpoint: "kubernetes/{env}/services/count", + }, + "kubernetes/namespaces": { + endpoint: "kubernetes/{env}/namespaces/count", + }, }, };