diff --git a/src/main/routes/port-forward-route.ts b/src/main/routes/port-forward-route.ts index 9a6535bdd5..641e8af843 100644 --- a/src/main/routes/port-forward-route.ts +++ b/src/main/routes/port-forward-route.ts @@ -22,7 +22,6 @@ import type { LensApiRequest } from "../router"; import { spawn, ChildProcessWithoutNullStreams } from "child_process"; import { Kubectl } from "../kubectl"; -import { shell } from "electron"; import * as tcpPortUsed from "tcp-port-used"; import logger from "../logger"; import { getPortFrom } from "../utils/get-port"; @@ -112,17 +111,6 @@ class PortForward { public async stop() { this.process.kill(); } - - public open() { - shell.openExternal(`http://localhost:${this.internalPort}`) - .catch(error => logger.error(`[PORT-FORWARD]: failed to open external shell: ${error}`, { - clusterId: this.clusterId, - port: this.port, - kind: this.kind, - namespace: this.namespace, - name: this.name, - })); - } } export class PortForwardRoute { @@ -165,7 +153,6 @@ export class PortForwardRoute { } } - portForward.open(); respondJson(response, {port: portForward.internalPort}); } catch (error) { logger.error(`[PORT-FORWARD-ROUTE]: failed to open a port-forward: ${error}`, { namespace, port, resourceType, resourceName }); diff --git a/src/renderer/components/+network-port-forwards/port-forward-menu.tsx b/src/renderer/components/+network-port-forwards/port-forward-menu.tsx index 3750468922..7b0f35ef43 100644 --- a/src/renderer/components/+network-port-forwards/port-forward-menu.tsx +++ b/src/renderer/components/+network-port-forwards/port-forward-menu.tsx @@ -20,13 +20,12 @@ */ import React from "react"; -import { boundMethod, cssNames, openExternal } from "../../utils"; -import { PortForwardItem, removePortForward } from "../../port-forward"; +import { boundMethod, cssNames } from "../../utils"; +import { openPortForward, PortForwardItem, removePortForward } from "../../port-forward"; import { MenuActions, MenuActionsProps } from "../menu/menu-actions"; import { MenuItem } from "../menu"; import { Icon } from "../icon"; -import { Notifications } from "../notifications"; -import { PortForwardDialog } from "./port-forward-dialog"; +import { PortForwardDialog } from "../../port-forward"; interface Props extends MenuActionsProps { portForward: PortForwardItem; @@ -39,25 +38,6 @@ export class PortForwardMenu extends React.Component { return removePortForward(this.props.portForward); } - @boundMethod - openInBrowser() { - const { portForward } = this.props; - const browseTo = `http://localhost:${portForward.forwardPort}`; - - openExternal(browseTo) - .catch(error => { - console.error(`failed to open in browser: ${error}`, { - clusterId: portForward.clusterId, - port: portForward.port, - kind: portForward.kind, - namespace: portForward.namespace, - name: portForward.name, - }); - Notifications.error(`Failed to open ${browseTo} in browser`); - } - ); - } - renderContent() { const { portForward, toolbar } = this.props; @@ -65,7 +45,7 @@ export class PortForwardMenu extends React.Component { return ( <> - + openPortForward(this.props.portForward)}> Open diff --git a/src/renderer/components/+network-services/service-details.tsx b/src/renderer/components/+network-services/service-details.tsx index 66a09672a1..38ed8d6c68 100644 --- a/src/renderer/components/+network-services/service-details.tsx +++ b/src/renderer/components/+network-services/service-details.tsx @@ -32,6 +32,7 @@ import { ServicePortComponent } from "./service-port-component"; import { endpointStore } from "../+network-endpoints/endpoints.store"; import { ServiceDetailsEndpoint } from "./service-details-endpoint"; import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api"; +import { portForwardStore } from "../../port-forward"; interface Props extends KubeObjectDetailsProps { } @@ -46,6 +47,7 @@ export class ServiceDetails extends React.Component { preload: true, namespaces: [service.getNs()], }), + portForwardStore.watch(), ]); } diff --git a/src/renderer/components/+network-services/service-port-component.tsx b/src/renderer/components/+network-services/service-port-component.tsx index fe41462808..0fffcd09f9 100644 --- a/src/renderer/components/+network-services/service-port-component.tsx +++ b/src/renderer/components/+network-services/service-port-component.tsx @@ -22,24 +22,22 @@ import "./service-port-component.scss"; import React from "react"; -import { observer } from "mobx-react"; +import { disposeOnUnmount, observer } from "mobx-react"; import type { Service, ServicePort } from "../../../common/k8s-api/endpoints"; -import { apiBase } from "../../api"; -import { observable, makeObservable } from "mobx"; +import { observable, makeObservable, IReactionDisposer, reaction } from "mobx"; import { cssNames } from "../../utils"; import { Notifications } from "../notifications"; -import { Input } from "../input"; import { Button } from "../button"; +import { addPortForward, getPortForward, openPortForward, PortForwardDialog, portForwardStore, removePortForward } from "../../port-forward"; +import type { ForwardedPort } from "../../port-forward"; +import logger from "../../../common/logger"; +import { Spinner } from "../spinner"; interface Props { service: Service; port: ServicePort; } -interface PortForwardResult { - port: number; -} - @observer export class ServicePortComponent extends React.Component { @observable waiting = false; @@ -52,26 +50,54 @@ export class ServicePortComponent extends React.Component { this.init(); } + componentDidMount() { + disposeOnUnmount(this, [ + this.watch(), + ]); + } + + watch() { + const disposers: IReactionDisposer[] = [ + reaction(() => portForwardStore.portForwards, () => this.init()), + ]; + + return () => disposers.forEach((dispose) => dispose()); + } + init() { this.checkExistingPortForwarding().catch(error => { - console.error(error); + logger.error(error); }); } async checkExistingPortForwarding() { const { service, port } = this.props; - const response = await apiBase.get(`/pods/${service.getNs()}/service/${service.getName()}/port-forward/${port.port}/${this.forwardPort}`, {}); + const portForward: ForwardedPort = { + kind: "service", + name: service.getName(), + namespace: service.getNs(), + port: port.port.toString(), + forwardPort: this.forwardPort.toString() + }; + let activePort = await getPortForward(portForward); - const activePort = response.port; - - if (activePort) { - this.forwardPort = activePort; - this.isPortForwarded = true; + if (!activePort) { + activePort = 0; } + + this.forwardPort = activePort; + this.isPortForwarded = activePort ? true : false; } async portForward() { const { service, port } = this.props; + const portForward: ForwardedPort = { + kind: "service", + name: service.getName(), + namespace: service.getNs(), + port: port.port.toString(), + forwardPort: this.forwardPort.toString() + }; this.waiting = true; @@ -80,10 +106,13 @@ export class ServicePortComponent extends React.Component { } try { - const response = await apiBase.post(`/pods/${service.getNs()}/service/${service.getName()}/port-forward/${port.port}/${this.forwardPort}`, {}); + this.forwardPort = await addPortForward(portForward); - this.forwardPort = response.port; - this.isPortForwarded = true; + if (this.forwardPort) { + portForward.forwardPort = this.forwardPort.toString(); + openPortForward(portForward); + this.isPortForwarded = true; + } } catch(error) { Notifications.error(error); } finally { @@ -93,11 +122,18 @@ export class ServicePortComponent extends React.Component { async stopPortForward() { const { service, port } = this.props; + const portForward: ForwardedPort = { + kind: "service", + name: service.getName(), + namespace: service.getNs(), + port: port.port.toString(), + forwardPort: this.forwardPort.toString() + }; this.waiting = true; try { - await apiBase.del(`/pods/${service.getNs()}/service/${service.getName()}/port-forward/${port.port}/${this.forwardPort}`, {}); + await removePortForward(portForward); this.isPortForwarded = false; } catch(error) { Notifications.error(error); @@ -107,31 +143,34 @@ export class ServicePortComponent extends React.Component { } render() { - const { port } = this.props; + const { port, service } = this.props; const portForwardAction = async () => { if (this.isPortForwarded) { await this.stopPortForward(); }else { - await this.portForward(); + const portForward: ForwardedPort = { + kind: "service", + name: service.getName(), + namespace: service.getNs(), + port: port.port.toString(), + forwardPort: this.forwardPort.toString() + }; + + PortForwardDialog.open(portForward, true); } }; return (
- {port.toString()} + this.portForward() }> + {port.toString()} + {this.waiting && ( + + )} + {" "} - - local port: - this.forwardPort = Number(value)} - /> +
); } diff --git a/src/renderer/components/+workloads-pods/pod-container-port.tsx b/src/renderer/components/+workloads-pods/pod-container-port.tsx index ff2a745e45..2a0b98a68a 100644 --- a/src/renderer/components/+workloads-pods/pod-container-port.tsx +++ b/src/renderer/components/+workloads-pods/pod-container-port.tsx @@ -22,15 +22,16 @@ import "./pod-container-port.scss"; import React from "react"; -import { observer } from "mobx-react"; +import { disposeOnUnmount, observer } from "mobx-react"; import type { Pod } from "../../../common/k8s-api/endpoints"; -import { apiBase } from "../../api"; -import { observable, makeObservable } from "mobx"; +import { observable, makeObservable, reaction, IReactionDisposer } from "mobx"; import { cssNames } from "../../utils"; import { Notifications } from "../notifications"; import { Button } from "../button"; -import { Input } from "../input"; -import { portForwardStore } from "../../port-forward/port-forward.store"; +import { addPortForward, getPortForward, openPortForward, PortForwardDialog, portForwardStore, removePortForward } from "../../port-forward"; +import type { ForwardedPort } from "../../port-forward"; +import logger from "../../../common/logger"; +import { Spinner } from "../spinner"; interface Props { pod: Pod; @@ -41,10 +42,6 @@ interface Props { } } -interface PortForwardResult { - port: number; -} - @observer export class PodContainerPort extends React.Component { @observable waiting = false; @@ -57,26 +54,54 @@ export class PodContainerPort extends React.Component { this.init(); } + componentDidMount() { + disposeOnUnmount(this, [ + this.watch(), + ]); + } + + watch() { + const disposers: IReactionDisposer[] = [ + reaction(() => portForwardStore.portForwards, () => this.init()), + ]; + + return () => disposers.forEach((dispose) => dispose()); + } + init() { this.checkExistingPortForwarding().catch(error => { - console.error(error); + logger.error(error); }); } async checkExistingPortForwarding() { const { pod, port } = this.props; - const response = await apiBase.get(`/pods/${pod.getNs()}/pod/${pod.getName()}/port-forward/${port.containerPort}/${this.forwardPort}`, {}); + const portForward: ForwardedPort = { + kind: "pod", + name: pod.getName(), + namespace: pod.getNs(), + port: port.containerPort.toString(), + forwardPort: this.forwardPort.toString() + }; + let activePort = await getPortForward(portForward); - const activePort = response.port; - - if (activePort) { - this.forwardPort = activePort; - this.isPortForwarded = true; + if (!activePort) { + activePort = 0; } + + this.forwardPort = activePort; + this.isPortForwarded = activePort ? true : false; } async portForward() { const { pod, port } = this.props; + const portForward: ForwardedPort = { + kind: "pod", + name: pod.getName(), + namespace: pod.getNs(), + port: port.containerPort.toString(), + forwardPort: this.forwardPort.toString() + }; this.waiting = true; @@ -85,11 +110,13 @@ export class PodContainerPort extends React.Component { } try { - const response = await apiBase.post(`/pods/${pod.getNs()}/pod/${pod.getName()}/port-forward/${port.containerPort}/${this.forwardPort}`, {}); - - this.forwardPort = response.port; - this.isPortForwarded = true; - portForwardStore.reset(); + this.forwardPort = await addPortForward(portForward); + + if (this.forwardPort) { + portForward.forwardPort = this.forwardPort.toString(); + openPortForward(portForward); + this.isPortForwarded = true; + } } catch(error) { Notifications.error(error); } finally { @@ -99,13 +126,19 @@ export class PodContainerPort extends React.Component { async stopPortForward() { const { pod, port } = this.props; + const portForward: ForwardedPort = { + kind: "pod", + name: pod.getName(), + namespace: pod.getNs(), + port: port.containerPort.toString(), + forwardPort: this.forwardPort.toString() + }; this.waiting = true; try { - await apiBase.del(`/pods/${pod.getNs()}/pod/${pod.getName()}/port-forward/${port.containerPort}/${this.forwardPort}`, {}); + await removePortForward(portForward); this.isPortForwarded = false; - portForwardStore.reset(); } catch(error) { Notifications.error(error); } finally { @@ -114,7 +147,7 @@ export class PodContainerPort extends React.Component { } render() { - const { port } = this.props; + const { pod, port } = this.props; const { name, containerPort, protocol } = port; const text = `${name ? `${name}: ` : ""}${containerPort}/${protocol}`; @@ -122,25 +155,28 @@ export class PodContainerPort extends React.Component { if (this.isPortForwarded) { await this.stopPortForward(); } else { - await this.portForward(); + const portForward: ForwardedPort = { + kind: "pod", + name: pod.getName(), + namespace: pod.getNs(), + port: port.containerPort.toString(), + forwardPort: this.forwardPort.toString() + }; + + PortForwardDialog.open(portForward, true); } }; return (
- {text} + this.portForward() }> + {text} + {this.waiting && ( + + )} + {" "} - - local port: - this.forwardPort = Number(value)} - /> +
); } diff --git a/src/renderer/components/+workloads-pods/pod-details-container.tsx b/src/renderer/components/+workloads-pods/pod-details-container.tsx index cc41d9a183..b4a4c5eaf2 100644 --- a/src/renderer/components/+workloads-pods/pod-details-container.tsx +++ b/src/renderer/components/+workloads-pods/pod-details-container.tsx @@ -36,7 +36,7 @@ import { LocaleDate } from "../locale-date"; import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; import { ClusterMetricsResourceType } from "../../../common/cluster-types"; import { portForwardStore } from "../../port-forward/port-forward.store"; -import { disposeOnUnmount } from "mobx-react"; +import { disposeOnUnmount, observer } from "mobx-react"; interface Props { pod: Pod; @@ -44,6 +44,7 @@ interface Props { metrics?: { [key: string]: IMetrics }; } +@observer export class PodDetailsContainer extends React.Component { componentDidMount() { diff --git a/src/renderer/components/app.tsx b/src/renderer/components/app.tsx index a7efbe41e3..769c9fe9c4 100755 --- a/src/renderer/components/app.tsx +++ b/src/renderer/components/app.tsx @@ -74,7 +74,7 @@ import { ClusterStore } from "../../common/cluster-store"; import type { ClusterId } from "../../common/cluster-types"; import { watchHistoryState } from "../remote-helpers/history-updater"; import { unmountComponentAtNode } from "react-dom"; -import { PortForwardDialog } from "./+network-port-forwards/port-forward-dialog"; +import { PortForwardDialog } from "../port-forward"; @observer export class App extends React.Component { diff --git a/src/renderer/port-forward/index.ts b/src/renderer/port-forward/index.ts index 9b1a2b0d1e..5be9fb5aed 100644 --- a/src/renderer/port-forward/index.ts +++ b/src/renderer/port-forward/index.ts @@ -21,3 +21,4 @@ export * from "./port-forward.store"; export * from "./port-forward-item"; +export * from "./port-forward-dialog"; diff --git a/src/renderer/components/+network-port-forwards/port-forward-dialog.scss b/src/renderer/port-forward/port-forward-dialog.scss similarity index 93% rename from src/renderer/components/+network-port-forwards/port-forward-dialog.scss rename to src/renderer/port-forward/port-forward-dialog.scss index 6fcfe9ed0a..6d5e07176b 100644 --- a/src/renderer/components/+network-port-forwards/port-forward-dialog.scss +++ b/src/renderer/port-forward/port-forward-dialog.scss @@ -66,5 +66,12 @@ margin-right: $margin; } } - } + + .portInput { + display: inline-block !important; + width: 70px; + margin-left: 10px; + margin-right: 10px; + } } + } diff --git a/src/renderer/components/+network-port-forwards/port-forward-dialog.tsx b/src/renderer/port-forward/port-forward-dialog.tsx similarity index 65% rename from src/renderer/components/+network-port-forwards/port-forward-dialog.tsx rename to src/renderer/port-forward/port-forward-dialog.tsx index f06d72ccff..2a3949d5d6 100644 --- a/src/renderer/components/+network-port-forwards/port-forward-dialog.tsx +++ b/src/renderer/port-forward/port-forward-dialog.tsx @@ -24,21 +24,22 @@ import "./port-forward-dialog.scss"; import React, { Component } from "react"; import { computed, observable, makeObservable } from "mobx"; import { observer } from "mobx-react"; -import { Dialog, DialogProps } from "../dialog"; -import { Wizard, WizardStep } from "../wizard"; -import { Icon } from "../icon"; -import { Input } from "../input"; -import { Notifications } from "../notifications"; -import { cssNames } from "../../utils"; -import { modifyPortForward, PortForwardItem } from "../../port-forward"; -import { isNumber } from "../input/input_validators"; +import { Dialog, DialogProps } from "../components/dialog"; +import { Wizard, WizardStep } from "../components/wizard"; +import { Input } from "../components/input"; +import { Notifications } from "../components/notifications"; +import { cssNames } from "../utils"; +import { addPortForward, modifyPortForward } from "./port-forward.store"; +import type { ForwardedPort } from "./port-forward.store"; +import { openPortForward } from "."; interface Props extends Partial { } const dialogState = observable.object({ isOpen: false, - data: null as PortForwardItem, + data: null as ForwardedPort, + openInBrowser: false }); @observer @@ -52,9 +53,10 @@ export class PortForwardDialog extends Component { makeObservable(this); } - static open(portForward: PortForwardItem) { + static open(portForward: ForwardedPort, openInBrowser = false) { dialogState.isOpen = true; dialogState.data = portForward; + dialogState.openInBrowser = openInBrowser; } static close() { @@ -83,24 +85,40 @@ export class PortForwardDialog extends Component { this.currentPort = +portForward.forwardPort; this.desiredPort = this.currentPort; - this.ready = true; + this.ready = this.currentPort ? false : true; }; onClose = () => { this.ready = false; }; - changePort = async () => { + changePort = (value: string) => { + this.desiredPort = Number(value); + this.ready = Boolean(this.desiredPort == 0 || this.currentPort !== this.desiredPort); + }; + + startPortForward = async () => { const { portForward } = this; const { currentPort, desiredPort, close } = this; try { - if (currentPort !== desiredPort) { - await modifyPortForward(portForward, desiredPort); + let port: number; + + if (currentPort) { + port = await modifyPortForward(portForward, desiredPort); + } else { + portForward.forwardPort = String(desiredPort); + port = await addPortForward(portForward); + } + + if (dialogState.openInBrowser) { + portForward.forwardPort = String(port); + openPortForward(portForward); } - close(); } catch (err) { Notifications.error(err); + } finally { + close(); } }; @@ -110,32 +128,28 @@ export class PortForwardDialog extends Component {
- Current port: {this.currentPort} + Local port to forward from:
- this.desiredPort = +v} +
-
- - Current port-forwarding will be removed and a new one will be started -
); } render() { const { className, ...dialogProps } = this.props; - const resourceName = this.portForward ? this.portForward.getName() : ""; + const resourceName = this.portForward ? this.portForward.name : ""; const header = (
- Change Port {resourceName} + Port Forwarding for {resourceName}
); @@ -151,8 +165,8 @@ export class PortForwardDialog extends Component { {this.renderContents()} diff --git a/src/renderer/port-forward/port-forward.store.ts b/src/renderer/port-forward/port-forward.store.ts index f298d581cb..3336b8802b 100644 --- a/src/renderer/port-forward/port-forward.store.ts +++ b/src/renderer/port-forward/port-forward.store.ts @@ -22,14 +22,15 @@ import { computed, IReactionDisposer, makeObservable, observable, reaction } from "mobx"; import { ItemObject, ItemStore } from "../../common/item.store"; -import { autoBind, createStorage } from "../utils"; -import { getHostedClusterId } from "../utils"; +import { autoBind, createStorage, getHostedClusterId, openExternal } from "../utils"; import { PortForwardItem } from "./port-forward-item"; import { apiBase } from "../api"; import { waitUntilFree } from "tcp-port-used"; +import { Notifications } from "../components/notifications"; +import logger from "../../common/logger"; export interface ForwardedPort { - clusterId: string; + clusterId?: string; kind: string; namespace: string; name: string; @@ -54,6 +55,7 @@ export class PortForwardStore extends ItemStore { const savedPortForwards = this.storage.get(); // undefined on first load if (Array.isArray(savedPortForwards)) { + logger.info("[PORT_FORWARD] starting saved port-forwards"); await Promise.all(savedPortForwards.map(pf => { const port = new PortForwardItem; @@ -128,41 +130,57 @@ interface PortForwardsResult { portForwards: ForwardedPort[]; } -export async function addPortForward(portForward: PortForwardItem): Promise { - let response; +export async function addPortForward(portForward: ForwardedPort): Promise { + let response: PortForwardResult; try { - response = await apiBase.post(`/pods/${portForward.getNs()}/${portForward.getKind()}/${portForward.getName()}/port-forward/${portForward.getPort()}/${portForward.getForwardPort()}`, {}); + response = await apiBase.post(`/pods/${portForward.namespace}/${portForward.kind}/${portForward.name}/port-forward/${portForward.port}/${portForward.forwardPort}`, {}); - if (response?.port != +portForward.getForwardPort()) { - console.log(`specified ${portForward.getForwardPort()} got ${response.port}`); + if (response?.port != +portForward.forwardPort) { + logger.info(`specified ${portForward.forwardPort} got ${response.port}`); } } catch (error) { - console.error(error); + logger.error(error); } portForwardStore.reset(); return response?.port; } -export async function modifyPortForward(portForward: PortForwardItem, desiredPort: number) { +export async function getPortForward(portForward: ForwardedPort): Promise { + let response: PortForwardResult; + + try { + response = await apiBase.get(`/pods/${portForward.namespace}/${portForward.kind}/${portForward.name}/port-forward/${portForward.port}/${portForward.forwardPort}`, {}); + } catch (error) { + logger.error(error); + } + + return response?.port; +} + +export async function modifyPortForward(portForward: ForwardedPort, desiredPort: number): Promise { + let port = 0; + try { await removePortForward(portForward); portForward.forwardPort = desiredPort.toString(); - await addPortForward(portForward); + port = await addPortForward(portForward); } catch (error) { - console.error(error); + logger.error(error); } portForwardStore.reset(); + + return port; } -export async function removePortForward(portForward: PortForwardItem) { +export async function removePortForward(portForward: ForwardedPort) { try { - await apiBase.del(`/pods/${portForward.getNs()}/${portForward.getKind()}/${portForward.getName()}/port-forward/${portForward.getPort()}/${portForward.forwardPort}`, {}); - await waitUntilFree(+portForward.getForwardPort(), 200, 1000); + await apiBase.del(`/pods/${portForward.namespace}/${portForward.kind}/${portForward.name}/port-forward/${portForward.port}/${portForward.forwardPort}`, {}); + await waitUntilFree(+portForward.forwardPort, 200, 1000); } catch (error) { - console.error(error); + logger.error(error); } portForwardStore.reset(); } @@ -173,10 +191,28 @@ export async function getPortForwards(): Promise { return response.portForwards; } catch (error) { - console.error(error); + logger.error(error); return []; } } +export function openPortForward(portForward: ForwardedPort) { + const browseTo = `http://localhost:${portForward.forwardPort}`; + + openExternal(browseTo) + .catch(error => { + logger.error(`failed to open in browser: ${error}`, { + clusterId: portForward.clusterId, + port: portForward.port, + kind: portForward.kind, + namespace: portForward.namespace, + name: portForward.name, + }); + Notifications.error(`Failed to open ${browseTo} in browser`); + } + ); + +} + export const portForwardStore = new PortForwardStore();