UNPKG

@kinvolk/headlamp-plugin

Version:

The needed infrastructure for building Headlamp plugins.

285 lines (284 loc) 13.5 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/api/v1/portForward'; import Pod from '../../../lib/k8s/pod'; import ActionButton from '../ActionButton'; import PortForwardStartDialog from '../../portforward/PortForwardStartDialog'; export const PORT_FORWARDS_STORAGE_KEY = 'portforwards'; export const PORT_FORWARD_STOP_STATUS = 'Stopped'; export const PORT_FORWARD_RUNNING_STATUS = 'Running'; export const DOCKER_DESKTOP_MIN_PORT = 30000; export const DOCKER_DESKTOP_MAX_PORT = 32000; 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 [startDialogOpen, setStartDialogOpen] = React.useState(false); const { t } = useTranslation(['translation', 'resource']); const [pods, podsFetchError] = Pod.useList({ namespace, labelSelector: getPodsSelectorFilter(service), }); const cluster = React.useMemo(() => { if (!resource) { return ''; } if (!!resource?.cluster) { return resource.cluster; } return getCluster(); }, [resource]); const numericContainerPort = typeof containerPort === 'string' && isNaN(parseInt(containerPort)) ? !pods || pods.length === 0 ? 0 : getPortNumberFromPortName(pods[0].spec.containers, containerPort) : containerPort; const displayPodName = React.useMemo(() => { return isPod ? name : pods && pods.length > 0 ? pods[0].metadata.name : ''; }, [isPod, name, pods]); const shouldRender = isElectron() && !(!isPod && podsFetchError) && !(!isPod && (!pods || pods.length === 0)) && !(isPod && (!resource || resource.status.phase === 'Failed')); React.useEffect(() => { if (!cluster || !shouldRender) { return; } let cancelled = false; setError(null); listPortForward(cluster) .then(result => { if (cancelled) { return; } 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); } } if (!cancelled) { localStorage.setItem(PORT_FORWARDS_STORAGE_KEY, JSON.stringify(serverAndStoragePortForwards)); } }) .catch(err => { if (cancelled) { return; } console.error('Failed to list port forwards', err); setError(err?.message || 'Failed to list port forwards'); }); return () => { cancelled = true; }; }, [cluster, namespace, name, numericContainerPort, shouldRender]); if (!shouldRender) { return null; } function startPortForwardWithSelection(chosenPort) { 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; let port = chosenPort || portForward?.port; let address = 'localhost'; if (isDockerDesktop()) { address = '0.0.0.0'; if (!chosenPort && !portForward?.port) { 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); } }); const portRange = DOCKER_DESKTOP_MAX_PORT - DOCKER_DESKTOP_MIN_PORT + 1; const maxAttempts = portRange; let attempts = 0; while (attempts < maxAttempts) { const randomPort = (Math.floor(Math.random() * portRange) + DOCKER_DESKTOP_MIN_PORT).toString(); if (!activePorts.includes(randomPort)) { port = randomPort; break; } attempts++; } if (!port) { port = Math.floor(Math.random() * portRange + DOCKER_DESKTOP_MIN_PORT).toString(); } } } setLoading(true); startPortForward(cluster, podNamespace, podName, numericContainerPort, serviceName, serviceNamespace, port, address, portForward?.id) .then((data) => { setLoading(false); setPortForward(data); 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(error => { setError(error?.message ?? 'An unexpected error occurred.'); setLoading(false); setPortForward(null); }); } function openStartDialog() { setStartDialogOpen(true); } function closeStartDialog() { setStartDialogOpen(false); } function portForwardStopHandler() { if (!portForward || !cluster) { return; } setLoading(true); stopOrDeletePortForward(cluster, portForward.id, true) .then(() => { portForward.status = PORT_FORWARD_STOP_STATUS; setPortForward(portForward); }) .catch(error => { setError(error?.message); setPortForward(null); }) .finally(() => { setLoading(false); }); } function deletePortForwardHandler() { const id = portForward?.id; if (!cluster || !id) { return; } setLoading(true); stopOrDeletePortForward(cluster, id, false).finally(() => { setLoading(false); 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); }); } const forwardBaseURL = 'http://127.0.0.1'; return (_jsxs(Box, { children: [!portForward ? (_jsxs(_Fragment, { children: [loading ? (_jsx(CircularProgress, { size: 18 })) : (_jsxs(Button, { onClick: openStartDialog, "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, { mt: 1, children: _jsx(Alert, { severity: "error", onClose: () => { setError(null); }, children: _jsx(Tooltip, { title: "error", children: _jsx(Box, { style: { overflow: 'hidden', textOverflow: 'ellipsis' }, children: error }) }) }) }))] })) : (_jsx(_Fragment, { 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: openStartDialog, 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' })] })) })), _jsx(PortForwardStartDialog, { open: startDialogOpen, defaultPort: portForward?.port, podName: displayPodName, namespace: namespace, containerPort: numericContainerPort, isDockerDesktop: isDockerDesktop(), onCancel: closeStartDialog, onConfirm: portInput => { closeStartDialog(); startPortForwardWithSelection(portInput); } })] })); } export default function PortForward(props) { if (!isElectron()) return null; return _jsx(PortForwardContent, { ...props }); }