@kinvolk/headlamp-plugin
Version:
The needed infrastructure for building Headlamp plugins.
285 lines (284 loc) • 13.5 kB
JavaScript
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 });
}