UNPKG

@kinvolk/headlamp-plugin

Version:

The needed infrastructure for building Headlamp plugins.

249 lines (248 loc) 11.6 kB
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; /* * Copyright 2025 The Kubernetes Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { InlineIcon } from '@iconify/react'; import Alert from '@mui/material/Alert'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import CircularProgress from '@mui/material/CircularProgress'; import { grey } from '@mui/material/colors'; import MuiLink from '@mui/material/Link'; import Tooltip from '@mui/material/Tooltip'; import Typography from '@mui/material/Typography'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { isDockerDesktop } from '../../../helpers/isDockerDesktop'; import { isElectron } from '../../../helpers/isElectron'; import { getCluster } from '../../../lib/cluster'; import { listPortForward, startPortForward, stopOrDeletePortForward, } from '../../../lib/k8s/apiProxy'; import Pod from '../../../lib/k8s/pod'; import ActionButton from '../ActionButton'; export const PORT_FORWARDS_STORAGE_KEY = 'portforwards'; export const PORT_FORWARD_STOP_STATUS = 'Stopped'; export const PORT_FORWARD_RUNNING_STATUS = 'Running'; function getPortNumberFromPortName(containers, namedPort) { let portNumber = 0; containers.every((container) => { container.ports?.find((port) => { if (port.name === namedPort) { portNumber = port.containerPort; return false; } }); return true; }); return portNumber; } function getPodsSelectorFilter(service) { if (!service) { return ''; } const selector = service?.jsonData.spec?.selector; if (selector) { return Object.keys(service?.jsonData.spec?.selector) .map(item => `${item}=${selector[item]}`) .join(','); } return ''; } function checkIfPodPortForwarding(portforwardParam) { const { item, namespace, name, cluster, numericContainerPort } = portforwardParam; return ((item.namespace === namespace || item.serviceNamespace === namespace) && (item.pod === name || item.service === name) && item.cluster === cluster && item.targetPort === numericContainerPort.toString()); } function PortForwardContent(props) { const { containerPort, resource } = props; const isPod = resource?.kind !== 'Service'; const service = !isPod ? resource : undefined; const namespace = resource?.metadata?.namespace || ''; const name = resource?.metadata?.name || ''; const [error, setError] = React.useState(null); const [portForward, setPortForward] = React.useState(null); const [loading, setLoading] = React.useState(false); const cluster = getCluster(); const { t } = useTranslation(['translation', 'resource']); const [pods, podsFetchError] = Pod.useList({ namespace, labelSelector: getPodsSelectorFilter(service), }); if (service && podsFetchError && !pods) { return null; } const numericContainerPort = typeof containerPort === 'string' && isNaN(parseInt(containerPort)) ? !pods || pods.length === 0 ? 0 : getPortNumberFromPortName(pods[0].spec.containers, containerPort) : containerPort; React.useEffect(() => { if (!cluster) { return; } listPortForward(cluster).then(result => { const portForwards = result || []; const serverAndStoragePortForwards = [...portForwards]; const portForwardsInStorage = localStorage.getItem(PORT_FORWARDS_STORAGE_KEY); const parsedPortForwards = JSON.parse(portForwardsInStorage || '[]'); parsedPortForwards.forEach((portforward) => { const isStoragePortForwardAvailableInServer = portForwards.find((pf) => pf.id === portforward.id); if (!isStoragePortForwardAvailableInServer) { portforward.status = PORT_FORWARD_STOP_STATUS; serverAndStoragePortForwards.push(portforward); } }); for (const item of serverAndStoragePortForwards) { if (checkIfPodPortForwarding({ item, namespace, name, cluster, numericContainerPort, })) { setPortForward(item); } } localStorage.setItem(PORT_FORWARDS_STORAGE_KEY, JSON.stringify(serverAndStoragePortForwards)); }); }, []); if (!isElectron()) { return null; } if (!isPod && podsFetchError) { return null; } if (!isPod && (!pods || pods.length === 0)) { return null; } function handlePortForward() { if (!namespace || !cluster || !pods) { return; } setError(null); const resourceName = name || ''; const podNamespace = isPod ? namespace : pods[0].metadata.namespace; const serviceNamespace = namespace; const serviceName = !isPod ? resourceName : ''; const podName = isPod ? resourceName : pods[0].metadata.name; var port = portForward?.port; let address = 'localhost'; // In case of docker desktop only a range of ports are open // so we need to generate a random port from that range // while making sure that it is not already in use if (isDockerDesktop()) { const validMinPort = 30000; const validMaxPort = 32000; // create a list of active ports const activePorts = []; const portForwardsInStorage = localStorage.getItem(PORT_FORWARDS_STORAGE_KEY); const parsedPortForwards = JSON.parse(portForwardsInStorage || '[]'); parsedPortForwards.forEach((pf) => { if (pf.status === PORT_FORWARD_RUNNING_STATUS) { activePorts.push(pf.port); } }); // generate random port till it is not in use while (true) { const randomPort = (Math.floor(Math.random() * (validMaxPort - validMinPort + 1)) + validMinPort).toString(); if (!activePorts.includes(randomPort)) { port = randomPort; break; } } address = '0.0.0.0'; } setLoading(true); startPortForward(cluster, podNamespace, podName, numericContainerPort, serviceName, serviceNamespace, port, address, portForward?.id) .then((data) => { setLoading(false); setPortForward(data); // append this new started portforward to storage const portForwardsInStorage = localStorage.getItem(PORT_FORWARDS_STORAGE_KEY); const parsedPortForwards = JSON.parse(portForwardsInStorage || '[]'); parsedPortForwards.push(data); localStorage.setItem(PORT_FORWARDS_STORAGE_KEY, JSON.stringify(parsedPortForwards)); }) .catch(() => { setPortForward(null); }); } function portForwardStopHandler() { if (!portForward || !cluster) { return; } setLoading(true); stopOrDeletePortForward(cluster, portForward.id, true) .then(() => { portForward.status = PORT_FORWARD_STOP_STATUS; setPortForward(portForward); }) .catch(() => { setPortForward(null); }) .finally(() => { setLoading(false); }); } function deletePortForwardHandler() { const id = portForward?.id; const cluster = getCluster(); setLoading(true); if (!cluster || !id) { return; } stopOrDeletePortForward(cluster, id, false).finally(() => { setLoading(false); // remove portforward from storage too const portforwardInStorage = localStorage.getItem(PORT_FORWARDS_STORAGE_KEY); const parsedPortForwards = JSON.parse(portforwardInStorage || '[]'); const index = parsedPortForwards.findIndex((pf) => pf.id === id); if (index !== -1) { parsedPortForwards.splice(index, 1); } localStorage.setItem(PORT_FORWARDS_STORAGE_KEY, JSON.stringify(parsedPortForwards)); setPortForward(null); }); } if (isPod && (!resource || resource.status.phase === 'Failed')) { return null; } const forwardBaseURL = 'http://127.0.0.1'; return !portForward ? (_jsxs(Box, { display: "flex", children: [loading ? (_jsx(CircularProgress, { size: 18 })) : (_jsxs(Button, { onClick: handlePortForward, "aria-label": t('translation|Start port forward'), color: "primary", variant: "outlined", style: { textTransform: 'none', }, disabled: loading, children: [_jsx(InlineIcon, { icon: "mdi:fast-forward", width: 20 }), _jsx(Typography, { children: t('translation|Forward port') })] })), error && (_jsx(Box, { children: _jsx(Alert, { severity: "error", onClose: () => { setError(null); }, children: _jsx(Tooltip, { title: "error", children: _jsx(Box, { style: { overflow: 'hidden', textOverflow: 'ellipsis' }, children: error }) }) }) }))] })) : (_jsx(Box, { children: portForward.status === PORT_FORWARD_STOP_STATUS ? (_jsxs(Box, { display: 'flex', alignItems: "center", children: [_jsx(Typography, { style: { color: grey[500], }, children: `${forwardBaseURL}:${portForward.port}` }), _jsx(ActionButton, { onClick: handlePortForward, description: t('translation|Start port forward'), color: "primary", icon: "mdi:fast-forward", iconButtonProps: { size: 'small', color: 'primary', disabled: loading, }, width: '25' }), _jsx(ActionButton, { onClick: deletePortForwardHandler, description: t('translation|Delete port forward'), color: "primary", icon: "mdi:delete-outline", iconButtonProps: { size: 'small', color: 'primary', disabled: loading, }, width: '25' })] })) : (_jsxs(_Fragment, { children: [_jsx(MuiLink, { href: `${forwardBaseURL}:${portForward.port}`, target: "_blank", color: "primary", children: `${forwardBaseURL}:${portForward.port}` }), _jsx(ActionButton, { onClick: portForwardStopHandler, description: t('translation|Stop port forward'), color: "primary", icon: "mdi:stop-circle-outline", iconButtonProps: { size: 'small', color: 'primary', disabled: loading, }, width: '25' })] })) })); } export default function PortForward(props) { if (!isElectron()) return null; return _jsx(PortForwardContent, { ...props }); }