From 446eb5ca43811f90f3e13eb037b09e0d96cf52c8 Mon Sep 17 00:00:00 2001 From: Jim Ehrismann <40840436+jim-docker@users.noreply.github.com> Date: Mon, 10 Jan 2022 10:00:10 -0500 Subject: [PATCH] Keep port-forward objects around when not running (#4607) * adding disabled status to port forwards (WIP) Signed-off-by: Jim Ehrismann * more work Signed-off-by: Jim Ehrismann * almost working Signed-off-by: Jim Ehrismann * working Signed-off-by: Jim Ehrismann * refactoring and bug fixing, still issue with port-forward-dialog Signed-off-by: Jim Ehrismann * further refactoring and bug fixing Signed-off-by: Jim Ehrismann * fixed remaining issues around port-forward dialog, changed port-forward-item id to resource name, etc from local port Signed-off-by: Jim Ehrismann * documentation, more cleanup Signed-off-by: Jim Ehrismann * address review comments Signed-off-by: Jim Ehrismann --- src/main/router.ts | 1 - src/main/routes/port-forward-route.ts | 25 -- .../port-forward-menu.tsx | 45 ++- .../+network-port-forwards/port-forwards.tsx | 2 +- .../service-port-component.tsx | 60 ++-- .../components/+network/network-mixins.scss | 1 + .../+workloads-pods/pod-container-port.tsx | 58 ++-- .../port-forward/port-forward-dialog.tsx | 55 ++-- .../port-forward/port-forward-item.ts | 18 +- .../port-forward/port-forward-notify.tsx | 31 ++ .../port-forward/port-forward-utils.ts | 1 - .../port-forward/port-forward.store.ts | 270 +++++++++++++++--- 12 files changed, 405 insertions(+), 162 deletions(-) diff --git a/src/main/router.ts b/src/main/router.ts index 96ed7cdd02..082d710f68 100644 --- a/src/main/router.ts +++ b/src/main/router.ts @@ -182,7 +182,6 @@ export class Router { // Port-forward API (the container port and local forwarding port are obtained from the query parameters) this.router.add({ method: "post", path: `${apiPrefix}/pods/port-forward/{namespace}/{resourceType}/{resourceName}` }, PortForwardRoute.routePortForward); this.router.add({ method: "get", path: `${apiPrefix}/pods/port-forward/{namespace}/{resourceType}/{resourceName}` }, PortForwardRoute.routeCurrentPortForward); - this.router.add({ method: "get", path: `${apiPrefix}/pods/port-forwards` }, PortForwardRoute.routeAllPortForwards); this.router.add({ method: "delete", path: `${apiPrefix}/pods/port-forward/{namespace}/{resourceType}/{resourceName}` }, PortForwardRoute.routeCurrentPortForwardStop); // Helm API diff --git a/src/main/routes/port-forward-route.ts b/src/main/routes/port-forward-route.ts index 6392c70ed9..030ec8ff07 100644 --- a/src/main/routes/port-forward-route.ts +++ b/src/main/routes/port-forward-route.ts @@ -188,31 +188,6 @@ export class PortForwardRoute { respondJson(response, { port: portForward?.forwardPort ?? null }); } - static async routeAllPortForwards(request: LensApiRequest) { - const { query, response } = request; - const clusterId = query.get("clusterId"); - - let portForwards: PortForwardArgs[] = PortForward.portForwards.map(f => ( - { - clusterId: f.clusterId, - kind: f.kind, - namespace: f.namespace, - name: f.name, - port: f.port, - forwardPort: f.forwardPort, - protocol: f.protocol, - }), - ); - - if (clusterId) { - // filter out any not for this cluster - portForwards = portForwards.filter(pf => pf.clusterId == clusterId); - - } - - respondJson(response, { portForwards }); - } - static async routeCurrentPortForwardStop(request: LensApiRequest) { const { params, query, response, cluster } = request; const { namespace, resourceType, resourceName } = params; 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 864735f3b7..c4e24df266 100644 --- a/src/renderer/components/+network-port-forwards/port-forward-menu.tsx +++ b/src/renderer/components/+network-port-forwards/port-forward-menu.tsx @@ -21,7 +21,7 @@ import React from "react"; import { boundMethod, cssNames } from "../../utils"; -import { openPortForward, PortForwardItem, removePortForward, PortForwardDialog } from "../../port-forward"; +import { openPortForward, PortForwardItem, removePortForward, PortForwardDialog, startPortForward, stopPortForward } from "../../port-forward"; import { MenuActions, MenuActionsProps } from "../menu/menu-actions"; import { MenuItem } from "../menu"; import { Icon } from "../icon"; @@ -44,6 +44,38 @@ export class PortForwardMenu extends React.Component { } } + private startPortForwarding = async () => { + const { portForward } = this.props; + + const pf = await startPortForward(portForward); + + if (pf.status === "Disabled") { + const { name, kind, forwardPort } = portForward; + + Notifications.error(`Error occurred starting port-forward, the local port ${forwardPort} may not be available or the ${kind} ${name} may not be reachable`); + } + }; + + renderStartStopMenuItem() { + const { portForward, toolbar } = this.props; + + if (portForward.status === "Active") { + return ( + stopPortForward(portForward)}> + + Stop + + ); + } + + return ( + + + Start + + ); + } + renderContent() { const { portForward, toolbar } = this.props; @@ -51,14 +83,17 @@ export class PortForwardMenu extends React.Component { return ( <> - openPortForward(this.props.portForward)}> - - Open - + { portForward.status === "Active" && + openPortForward(portForward)}> + + Open + + } PortForwardDialog.open(portForward)}> Edit + {this.renderStartStopMenuItem()} ); } diff --git a/src/renderer/components/+network-port-forwards/port-forwards.tsx b/src/renderer/components/+network-port-forwards/port-forwards.tsx index a76588619d..b6d4fa5fbf 100644 --- a/src/renderer/components/+network-port-forwards/port-forwards.tsx +++ b/src/renderer/components/+network-port-forwards/port-forwards.tsx @@ -70,7 +70,7 @@ export class PortForwards extends React.Component { showDetails = (item: PortForwardItem) => { navigation.push(portForwardsURL({ params: { - forwardport: String(item.getForwardPort()), + forwardport: item.getId(), }, })); }; diff --git a/src/renderer/components/+network-services/service-port-component.tsx b/src/renderer/components/+network-services/service-port-component.tsx index 95355d0f0d..796c403cbd 100644 --- a/src/renderer/components/+network-services/service-port-component.tsx +++ b/src/renderer/components/+network-services/service-port-component.tsx @@ -24,13 +24,14 @@ import "./service-port-component.scss"; import React from "react"; import { disposeOnUnmount, observer } from "mobx-react"; import type { Service, ServicePort } from "../../../common/k8s-api/endpoints"; -import { observable, makeObservable, reaction } from "mobx"; +import { observable, makeObservable, reaction, action } from "mobx"; import { cssNames } from "../../utils"; import { Notifications } from "../notifications"; import { Button } from "../button"; -import { aboutPortForwarding, addPortForward, getPortForward, getPortForwards, openPortForward, PortForwardDialog, portForwardStore, predictProtocol, removePortForward } from "../../port-forward"; +import { aboutPortForwarding, addPortForward, getPortForward, getPortForwards, notifyErrorPortForwarding, openPortForward, PortForwardDialog, predictProtocol, removePortForward, startPortForward } from "../../port-forward"; import type { ForwardedPort } from "../../port-forward"; import { Spinner } from "../spinner"; +import logger from "../../../common/logger"; interface Props { service: Service; @@ -42,6 +43,7 @@ export class ServicePortComponent extends React.Component { @observable waiting = false; @observable forwardPort = 0; @observable isPortForwarded = false; + @observable isActive = false; constructor(props: Props) { super(props); @@ -51,13 +53,14 @@ export class ServicePortComponent extends React.Component { componentDidMount() { disposeOnUnmount(this, [ - reaction(() => [portForwardStore.portForwards, this.props.service], () => this.checkExistingPortForwarding()), + reaction(() => this.props.service, () => this.checkExistingPortForwarding()), ]); } + @action async checkExistingPortForwarding() { const { service, port } = this.props; - const portForward: ForwardedPort = { + let portForward: ForwardedPort = { kind: "service", name: service.getName(), namespace: service.getNs(), @@ -65,57 +68,66 @@ export class ServicePortComponent extends React.Component { forwardPort: this.forwardPort, }; - let activePort: number; - try { - activePort = await getPortForward(portForward) ?? 0; + portForward = await getPortForward(portForward); } catch (error) { this.isPortForwarded = false; + this.isActive = false; return; } - this.forwardPort = activePort; - this.isPortForwarded = activePort ? true : false; + this.forwardPort = portForward.forwardPort; + this.isPortForwarded = true; + this.isActive = portForward.status === "Active"; } + @action async portForward() { const { service, port } = this.props; - const portForward: ForwardedPort = { + let portForward: ForwardedPort = { kind: "service", name: service.getName(), namespace: service.getNs(), port: port.port, forwardPort: this.forwardPort, protocol: predictProtocol(port.name), + status: "Active", }; this.waiting = true; try { - // determine how many port-forwards are already active - const { length } = await getPortForwards(); + // determine how many port-forwards already exist + const { length } = getPortForwards(); - this.forwardPort = await addPortForward(portForward); + if (!this.isPortForwarded) { + portForward = await addPortForward(portForward); + } else if (!this.isActive) { + portForward = await startPortForward(portForward); + } - if (this.forwardPort) { - portForward.forwardPort = this.forwardPort; + this.forwardPort = portForward.forwardPort; + + if (portForward.status === "Active") { openPortForward(portForward); - this.isPortForwarded = true; // if this is the first port-forward show the about notification if (!length) { aboutPortForwarding(); } + } else { + notifyErrorPortForwarding(`Error occurred starting port-forward, the local port may not be available or the ${portForward.kind} ${portForward.name} may not be reachable`); } } catch (error) { - Notifications.error(`Error occurred starting port-forward, the local port may not be available or the ${portForward.kind} ${portForward.name} may not be reachable`); - this.checkExistingPortForwarding(); + logger.error("[SERVICE-PORT-COMPONENT]:", error, portForward); } finally { + this.checkExistingPortForwarding(); this.waiting = false; } } + @action async stopPortForward() { const { service, port } = this.props; const portForward: ForwardedPort = { @@ -130,11 +142,11 @@ export class ServicePortComponent extends React.Component { try { await removePortForward(portForward); - this.isPortForwarded = false; } catch (error) { Notifications.error(`Error occurred stopping the port-forward from port ${portForward.forwardPort}.`); - this.checkExistingPortForwarding(); } finally { + this.checkExistingPortForwarding(); + this.forwardPort = 0; this.waiting = false; } } @@ -142,7 +154,7 @@ export class ServicePortComponent extends React.Component { render() { const { port, service } = this.props; - const portForwardAction = async () => { + const portForwardAction = action(async () => { if (this.isPortForwarded) { await this.stopPortForward(); } else { @@ -155,16 +167,16 @@ export class ServicePortComponent extends React.Component { protocol: predictProtocol(port.name), }; - PortForwardDialog.open(portForward, { openInBrowser: true }); + PortForwardDialog.open(portForward, { openInBrowser: true, onClose: () => this.checkExistingPortForwarding() }); } - }; + }); return (
this.portForward()}> {port.toString()} - + {this.waiting && ( )} diff --git a/src/renderer/components/+network/network-mixins.scss b/src/renderer/components/+network/network-mixins.scss index 3fa05ac072..3b9ca4fbb9 100644 --- a/src/renderer/components/+network/network-mixins.scss +++ b/src/renderer/components/+network/network-mixins.scss @@ -34,6 +34,7 @@ $service-status-color-list: ( $port-forward-status-color-list: ( active: var(--colorOk), + disabled: var(--colorSoftError) ); @mixin port-forward-status-colors { diff --git a/src/renderer/components/+workloads-pods/pod-container-port.tsx b/src/renderer/components/+workloads-pods/pod-container-port.tsx index 50417ce9dd..768ea50220 100644 --- a/src/renderer/components/+workloads-pods/pod-container-port.tsx +++ b/src/renderer/components/+workloads-pods/pod-container-port.tsx @@ -24,13 +24,14 @@ import "./pod-container-port.scss"; import React from "react"; import { disposeOnUnmount, observer } from "mobx-react"; import type { Pod } from "../../../common/k8s-api/endpoints"; -import { observable, makeObservable, reaction } from "mobx"; +import { action, observable, makeObservable, reaction } from "mobx"; import { cssNames } from "../../utils"; import { Notifications } from "../notifications"; import { Button } from "../button"; -import { aboutPortForwarding, addPortForward, getPortForward, getPortForwards, openPortForward, PortForwardDialog, portForwardStore, predictProtocol, removePortForward } from "../../port-forward"; +import { aboutPortForwarding, addPortForward, getPortForward, getPortForwards, notifyErrorPortForwarding, openPortForward, PortForwardDialog, predictProtocol, removePortForward, startPortForward } from "../../port-forward"; import type { ForwardedPort } from "../../port-forward"; import { Spinner } from "../spinner"; +import logger from "../../../common/logger"; interface Props { pod: Pod; @@ -46,6 +47,7 @@ export class PodContainerPort extends React.Component { @observable waiting = false; @observable forwardPort = 0; @observable isPortForwarded = false; + @observable isActive = false; constructor(props: Props) { super(props); @@ -55,13 +57,14 @@ export class PodContainerPort extends React.Component { componentDidMount() { disposeOnUnmount(this, [ - reaction(() => [portForwardStore.portForwards, this.props.pod], () => this.checkExistingPortForwarding()), + reaction(() => this.props.pod, () => this.checkExistingPortForwarding()), ]); } + @action async checkExistingPortForwarding() { const { pod, port } = this.props; - const portForward: ForwardedPort = { + let portForward: ForwardedPort = { kind: "pod", name: pod.getName(), namespace: pod.getNs(), @@ -69,57 +72,64 @@ export class PodContainerPort extends React.Component { forwardPort: this.forwardPort, }; - let activePort: number; - try { - activePort = await getPortForward(portForward) ?? 0; + portForward = await getPortForward(portForward); } catch (error) { this.isPortForwarded = false; + this.isActive = false; return; } - this.forwardPort = activePort; - this.isPortForwarded = activePort ? true : false; + this.forwardPort = portForward.forwardPort; + this.isPortForwarded = true; + this.isActive = portForward.status === "Active"; } + @action async portForward() { const { pod, port } = this.props; - const portForward: ForwardedPort = { + let portForward: ForwardedPort = { kind: "pod", name: pod.getName(), namespace: pod.getNs(), port: port.containerPort, forwardPort: this.forwardPort, protocol: predictProtocol(port.name), + status: "Active", }; this.waiting = true; try { - // determine how many port-forwards are already active - const { length } = await getPortForwards(); + // determine how many port-forwards already exist + const { length } = getPortForwards(); - this.forwardPort = await addPortForward(portForward); + if (!this.isPortForwarded) { + portForward = await addPortForward(portForward); + } else if (!this.isActive) { + portForward = await startPortForward(portForward); + } - if (this.forwardPort) { - portForward.forwardPort = this.forwardPort; + if (portForward.status === "Active") { openPortForward(portForward); - this.isPortForwarded = true; // if this is the first port-forward show the about notification if (!length) { aboutPortForwarding(); } + } else { + notifyErrorPortForwarding(`Error occurred starting port-forward, the local port may not be available or the ${portForward.kind} ${portForward.name} may not be reachable`); } } catch (error) { - Notifications.error(`Error occurred starting port-forward, the local port may not be available or the ${portForward.kind} ${portForward.name} may not be reachable`); - this.checkExistingPortForwarding(); + logger.error("[POD-CONTAINER-PORT]:", error, portForward); } finally { + this.checkExistingPortForwarding(); this.waiting = false; } } + @action async stopPortForward() { const { pod, port } = this.props; const portForward: ForwardedPort = { @@ -134,11 +144,11 @@ export class PodContainerPort extends React.Component { try { await removePortForward(portForward); - this.isPortForwarded = false; } catch (error) { Notifications.error(`Error occurred stopping the port-forward from port ${portForward.forwardPort}.`); - this.checkExistingPortForwarding(); } finally { + this.checkExistingPortForwarding(); + this.forwardPort = 0; this.waiting = false; } } @@ -148,7 +158,7 @@ export class PodContainerPort extends React.Component { const { name, containerPort, protocol } = port; const text = `${name ? `${name}: ` : ""}${containerPort}/${protocol}`; - const portForwardAction = async () => { + const portForwardAction = action(async () => { if (this.isPortForwarded) { await this.stopPortForward(); } else { @@ -161,16 +171,16 @@ export class PodContainerPort extends React.Component { protocol: predictProtocol(port.name), }; - PortForwardDialog.open(portForward, { openInBrowser: true }); + PortForwardDialog.open(portForward, { openInBrowser: true, onClose: () => this.checkExistingPortForwarding() }); } - }; + }); return (
this.portForward()}> {text} - + {this.waiting && ( )} diff --git a/src/renderer/port-forward/port-forward-dialog.tsx b/src/renderer/port-forward/port-forward-dialog.tsx index b50dbae596..9db91f8993 100644 --- a/src/renderer/port-forward/port-forward-dialog.tsx +++ b/src/renderer/port-forward/port-forward-dialog.tsx @@ -27,19 +27,20 @@ import { observer } from "mobx-react"; 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 { cssNames, noop } from "../utils"; import { addPortForward, getPortForwards, modifyPortForward } from "./port-forward.store"; import type { ForwardedPort } from "./port-forward-item"; import { openPortForward } from "./port-forward-utils"; -import { aboutPortForwarding } from "./port-forward-notify"; +import { aboutPortForwarding, notifyErrorPortForwarding } from "./port-forward-notify"; import { Checkbox } from "../components/checkbox"; +import logger from "../../common/logger"; interface Props extends Partial { } interface PortForwardDialogOpenOptions { - openInBrowser: boolean + openInBrowser: boolean; + onClose: () => void; } const dialogState = observable.object({ @@ -47,6 +48,7 @@ const dialogState = observable.object({ data: null as ForwardedPort, useHttps: false, openInBrowser: false, + onClose: noop, }); @observer @@ -59,11 +61,12 @@ export class PortForwardDialog extends Component { makeObservable(this); } - static open(portForward: ForwardedPort, options: PortForwardDialogOpenOptions = { openInBrowser: false }) { + static open(portForward: ForwardedPort, options: PortForwardDialogOpenOptions = { openInBrowser: false, onClose: noop }) { dialogState.isOpen = true; dialogState.data = portForward; dialogState.useHttps = portForward.protocol === "https"; dialogState.openInBrowser = options.openInBrowser; + dialogState.onClose = options.onClose; } static close() { @@ -85,43 +88,47 @@ export class PortForwardDialog extends Component { this.desiredPort = this.currentPort; }; - onClose = () => { - }; - changePort = (value: string) => { this.desiredPort = Number(value); }; startPortForward = async () => { - const { portForward } = this; + let { portForward } = this; const { currentPort, desiredPort, close } = this; try { - // determine how many port-forwards are already active - const { length } = await getPortForwards(); - - let port: number; + // determine how many port-forwards already exist + const { length } = getPortForwards(); portForward.protocol = dialogState.useHttps ? "https" : "http"; if (currentPort) { - port = await modifyPortForward(portForward, desiredPort); + const wasRunning = portForward.status === "Active"; + + portForward = await modifyPortForward(portForward, desiredPort); + + if (wasRunning && portForward.status === "Disabled") { + notifyErrorPortForwarding(`Error occurred starting port-forward, the local port ${portForward.forwardPort} may not be available or the ${portForward.kind} ${portForward.name} may not be reachable`); + } } else { portForward.forwardPort = desiredPort; - port = await addPortForward(portForward); + portForward = await addPortForward(portForward); - // if this is the first port-forward show the about notification - if (!length) { - aboutPortForwarding(); + if (portForward.status === "Disabled") { + notifyErrorPortForwarding(`Error occurred starting port-forward, the local port ${portForward.forwardPort} may not be available or the ${portForward.kind} ${portForward.name} may not be reachable`); + } else { + // if this is the first port-forward show the about notification + if (!length) { + aboutPortForwarding(); + } } } - if (dialogState.openInBrowser) { - portForward.forwardPort = port; + if (portForward.status === "Active" && dialogState.openInBrowser) { openPortForward(portForward); } - } catch (err) { - Notifications.error(`Error occurred starting port-forward, the local port may not be available or the ${portForward.kind} ${portForward.name} may not be reachable`); + } catch (error) { + logger.error(`[PORT-FORWARD-DIALOG]: ${error}`, portForward); } finally { close(); } @@ -176,14 +183,14 @@ export class PortForwardDialog extends Component { isOpen={dialogState.isOpen} className={cssNames("PortForwardDialog", className)} onOpen={this.onOpen} - onClose={this.onClose} + onClose={dialogState.onClose} close={this.close} > {this.renderContents()} diff --git a/src/renderer/port-forward/port-forward-item.ts b/src/renderer/port-forward/port-forward-item.ts index 33126fafa5..90a9c7c218 100644 --- a/src/renderer/port-forward/port-forward-item.ts +++ b/src/renderer/port-forward/port-forward-item.ts @@ -23,33 +23,34 @@ import type { ItemObject } from "../../common/item.store"; import { autoBind } from "../../common/utils"; +export type ForwardedPortStatus = "Active" | "Disabled"; export interface ForwardedPort { - clusterId?: string; kind: string; namespace: string; name: string; port: number; forwardPort: number; protocol?: string; + status?: ForwardedPortStatus; } export class PortForwardItem implements ItemObject { - clusterId: string; kind: string; namespace: string; name: string; port: number; forwardPort: number; protocol: string; + status: ForwardedPortStatus; constructor(pf: ForwardedPort) { - this.clusterId = pf.clusterId; this.kind = pf.kind; this.namespace = pf.namespace; this.name = pf.name; this.port = pf.port; this.forwardPort = pf.forwardPort; this.protocol = pf.protocol ?? "http"; + this.status = pf.status ?? "Active"; autoBind(this); } @@ -62,12 +63,8 @@ export class PortForwardItem implements ItemObject { return this.namespace; } - get id() { - return this.forwardPort; - } - getId() { - return String(this.forwardPort); + return `${this.namespace}-${this.kind}-${this.name}:${this.port}`; } getKind() { @@ -87,16 +84,17 @@ export class PortForwardItem implements ItemObject { } getStatus() { - return "Active"; // to-do allow port-forward-items to be stopped (without removing them) + return this.status; } getSearchFields() { return [ this.name, - this.id, + this.namespace, this.kind, this.port, this.forwardPort, + this.status, ]; } } diff --git a/src/renderer/port-forward/port-forward-notify.tsx b/src/renderer/port-forward/port-forward-notify.tsx index 4b26fb976c..788a5b672d 100644 --- a/src/renderer/port-forward/port-forward-notify.tsx +++ b/src/renderer/port-forward/port-forward-notify.tsx @@ -56,3 +56,34 @@ export function aboutPortForwarding() { }, ); } + +export function notifyErrorPortForwarding(msg: string) { + const notificationId = `port-forward-error-notification-${getHostedClusterId()}`; + + Notifications.error( + ( +
+ Port Forwarding +

+ {msg} +

+
+
+
+ ), + { + id: notificationId, + timeout: 10_000, + }, + ); +} + diff --git a/src/renderer/port-forward/port-forward-utils.ts b/src/renderer/port-forward/port-forward-utils.ts index c88761f344..775a350e2b 100644 --- a/src/renderer/port-forward/port-forward-utils.ts +++ b/src/renderer/port-forward/port-forward-utils.ts @@ -35,7 +35,6 @@ export function openPortForward(portForward: ForwardedPort) { openExternal(browseTo) .catch(error => { logger.error(`failed to open in browser: ${error}`, { - clusterId: portForward.clusterId, port: portForward.port, kind: portForward.kind, namespace: portForward.namespace, diff --git a/src/renderer/port-forward/port-forward.store.ts b/src/renderer/port-forward/port-forward.store.ts index 57c9d8a1c4..e19371a65e 100644 --- a/src/renderer/port-forward/port-forward.store.ts +++ b/src/renderer/port-forward/port-forward.store.ts @@ -20,10 +20,11 @@ */ -import { makeObservable, observable, reaction } from "mobx"; +import { action, makeObservable, observable, reaction } from "mobx"; import { ItemStore } from "../../common/item.store"; -import { autoBind, createStorage, disposer, getHostedClusterId } from "../utils"; +import { autoBind, createStorage, disposer } from "../utils"; import { ForwardedPort, PortForwardItem } from "./port-forward-item"; +import { notifyErrorPortForwarding } from "./port-forward-notify"; import { apiBase } from "../api"; import { waitUntilFree } from "tcp-port-used"; import logger from "../../common/logger"; @@ -31,7 +32,7 @@ import logger from "../../common/logger"; export class PortForwardStore extends ItemStore { private storage = createStorage("port_forwards", undefined); - @observable portForwards: PortForwardItem[]; + @observable portForwards: PortForwardItem[] = []; constructor() { super(); @@ -48,33 +49,42 @@ export class PortForwardStore extends ItemStore { if (Array.isArray(savedPortForwards)) { logger.info("[PORT-FORWARD-STORE] starting saved port-forwards"); - await Promise.all(savedPortForwards.map(addPortForward)); + + // add the disabled ones + await Promise.all(savedPortForwards.filter(pf => pf.status === "Disabled").map(addPortForward)); + + // add the active ones and check if they started successfully + const results = await Promise.allSettled(savedPortForwards.filter(pf => pf.status === "Active").map(addPortForward)); + + for (const result of results) { + if (result.status === "rejected" || result.value.status === "Disabled") { + notifyErrorPortForwarding("One or more port-forwards could not be started"); + + return; + } + } } } watch() { return disposer( - reaction(() => this.portForwards, () => this.loadAll()), + reaction(() => portForwardStore.portForwards.slice(), () => portForwardStore.loadAll()), ); } loadAll() { - return this.loadItems(async () => { - const portForwards = await getPortForwards(getHostedClusterId()); + return this.loadItems(() => { + const portForwards = getPortForwards(); this.storage.set(portForwards); - this.reset(); + this.portForwards = []; portForwards.map(pf => this.portForwards.push(new PortForwardItem(pf))); return this.portForwards; }); } - reset() { - this.portForwards = []; - } - async removeSelectedItems() { return Promise.all(this.selectedItems.map(removePortForward)); } @@ -94,82 +104,248 @@ interface PortForwardResult { port: number; } -interface PortForwardsResult { - portForwards: ForwardedPort[]; +function portForwardsEqual(portForward: ForwardedPort) { + return (pf: ForwardedPort) => ( + pf.kind == portForward.kind && + pf.name == portForward.name && + pf.namespace == portForward.namespace && + pf.port == portForward.port + ); } -export async function addPortForward(portForward: ForwardedPort): Promise { - const { port, forwardPort } = portForward; +function findPortForward(portForward: ForwardedPort) { + return portForwardStore.portForwards.find(portForwardsEqual(portForward)); + +} + +const setPortForward = action((portForward: ForwardedPort) => { + const index = portForwardStore.portForwards.findIndex(portForwardsEqual(portForward)); + + if (index < 0 ) { + return; + } + + portForwardStore.portForwards[index] = new PortForwardItem(portForward); +}); + +/** + * start an existing port-forward + * @param portForward the port-forward to start. If the forwardPort field is 0 then an arbitrary port will be + * used + * + * @returns the port-forward with updated status ("Active" if successfully started, "Disabled" otherwise) and + * forwardPort + * + * @throws if the port-forward does not already exist in the store + */ +export const startPortForward = action( async (portForward: ForwardedPort): Promise => { + const pf = findPortForward(portForward); + + if (!pf) { + throw new Error("cannot start non-existent port-forward"); + } + + const { port, forwardPort } = pf; let response: PortForwardResult; try { - const protocol = portForward.protocol ?? "http"; + const protocol = pf.protocol ?? "http"; - response = await apiBase.post(`/pods/port-forward/${portForward.namespace}/${portForward.kind}/${portForward.name}`, { query: { port, forwardPort, protocol }}); + response = await apiBase.post(`/pods/port-forward/${pf.namespace}/${pf.kind}/${pf.name}`, { query: { port, forwardPort, protocol }}); // expecting the received port to be the specified port, unless the specified port is 0, which indicates any available port is suitable - if (portForward.forwardPort && response?.port && response.port != +portForward.forwardPort) { - logger.warn(`[PORT-FORWARD-STORE] specified ${portForward.forwardPort} got ${response.port}`); + if (pf.forwardPort && response?.port && response.port != +pf.forwardPort) { + logger.warn(`[PORT-FORWARD-STORE] specified ${pf.forwardPort}, got ${response.port}`); } + + pf.forwardPort = response.port; + pf.status = "Active"; + } catch (error) { - logger.warn("[PORT-FORWARD-STORE] Error adding port-forward:", error, portForward); - throw (error); + logger.warn(`[PORT-FORWARD-STORE] Error starting port-forward: ${error}`, pf); + pf.status = "Disabled"; } - portForwardStore.reset(); - return response?.port; -} + setPortForward(pf); -export async function getPortForward(portForward: ForwardedPort): Promise { + return pf as ForwardedPort; +}); + +/** + * add a port-forward to the store and optionally start it + * @param portForward the port-forward to add. If the port-forward already exists in the store it will be + * returned with its current state. If the forwardPort field is 0 then an arbitrary port will be + * used. If the status field is "Active" or not present then an attempt is made to start the port-forward. + * + * @returns the port-forward with updated status ("Active" if successfully started, "Disabled" otherwise) and + * forwardPort + */ +export const addPortForward = action(async (portForward: ForwardedPort): Promise => { + const pf = findPortForward(portForward); + + if (pf) { + return pf; + } + + portForwardStore.portForwards.push(new PortForwardItem(portForward)); + + if (!portForward.status) { + portForward.status = "Active"; + } + + if (portForward.status === "Active") { + portForward = await startPortForward(portForward); + } + + return portForward; +}); + +async function getActivePortForward(portForward: ForwardedPort): Promise { const { port, forwardPort, protocol } = portForward; let response: PortForwardResult; try { response = await apiBase.get(`/pods/port-forward/${portForward.namespace}/${portForward.kind}/${portForward.name}`, { query: { port, forwardPort, protocol }}); } catch (error) { - logger.warn("[PORT-FORWARD-STORE] Error getting port-forward:", error, portForward); - throw (error); + logger.warn(`[PORT-FORWARD-STORE] Error getting active port-forward: ${error}`, portForward); } - return response?.port; + portForward.status = response?.port ? "Active" : "Disabled"; + portForward.forwardPort = response?.port; + + return portForward; } -export async function modifyPortForward(portForward: ForwardedPort, desiredPort: number): Promise { - let port = 0; +/** + * get a port-forward from the store, with up-to-date status + * @param portForward the port-forward to get. + * + * @returns the port-forward with updated status ("Active" if running, "Disabled" if not) and + * forwardPort used. + * + * @throws if the port-forward does not exist in the store + */ +export async function getPortForward(portForward: ForwardedPort): Promise { + if (!findPortForward(portForward)) { + throw new Error("port-forward not found"); + } - await removePortForward(portForward); - portForward.forwardPort = desiredPort; - port = await addPortForward(portForward); + let pf: ForwardedPort; - portForwardStore.reset(); + try { + // check if the port-forward is active, and if so check if it has the same local port + pf = await getActivePortForward(portForward); - return port; + if (pf.forwardPort && pf.forwardPort !== portForward.forwardPort) { + logger.warn(`[PORT-FORWARD-STORE] local port, expected ${pf.forwardPort}, got ${portForward.forwardPort}`); + } + } catch (error) { + // port is not active + } + + return pf; } +/** + * modifies a port-forward in the store, including the forwardPort and protocol + * @param portForward the port-forward to modify. + * + * @returns the port-forward after being modified. + */ +export const modifyPortForward = action(async (portForward: ForwardedPort, desiredPort: number): Promise => { + const pf = findPortForward(portForward); + + if (!pf) { + throw new Error("port-forward not found"); + } + + if (pf.status === "Active") { + try { + await stopPortForward(pf); + } catch { + // ignore, assume it is stopped and proceed to restart it + } + + pf.forwardPort = desiredPort; + pf.protocol = portForward.protocol ?? "http"; + setPortForward(pf); + + return await startPortForward(pf); + } + + pf.forwardPort = desiredPort; + setPortForward(pf); + + return pf as ForwardedPort; +}); + + +/** + * stop an existing port-forward. Its status is set to "Disabled" after successfully stopped. + * @param portForward the port-forward to stop. + * + * @throws if the port-forward could not be stopped. Its status is unchanged + */ +export const stopPortForward = action(async (portForward: ForwardedPort) => { + const pf = findPortForward(portForward); + + if (!pf) { + logger.warn("[PORT-FORWARD-STORE] Error getting port-forward: port-forward not found", portForward); + + return; + } -export async function removePortForward(portForward: ForwardedPort) { const { port, forwardPort } = portForward; try { await apiBase.del(`/pods/port-forward/${portForward.namespace}/${portForward.kind}/${portForward.name}`, { query: { port, forwardPort }}); await waitUntilFree(+forwardPort, 200, 1000); } catch (error) { - logger.warn("[PORT-FORWARD-STORE] Error removing port-forward:", error, portForward); + logger.warn(`[PORT-FORWARD-STORE] Error stopping active port-forward: ${error}`, portForward); throw (error); } - portForwardStore.reset(); -} -export async function getPortForwards(clusterId?: string): Promise { - try { - const response = await apiBase.get("/pods/port-forwards", { query: { clusterId }}); + pf.status = "Disabled"; + setPortForward(pf); +}); - return response.portForwards; - } catch (error) { - logger.warn("[PORT-FORWARD-STORE] Error getting all port-forwards:", error); +/** + * remove and stop an existing port-forward. + * @param portForward the port-forward to remove. + */ +export const removePortForward = action(async (portForward: ForwardedPort) => { + const pf = findPortForward(portForward); - return []; + if (!pf) { + const error = new Error("port-forward not found"); + + logger.warn(`[PORT-FORWARD-STORE] Error getting port-forward: ${error}`, portForward); + + return; } + + try { + await stopPortForward(portForward); + } catch (error) { + if (pf.status === "Active") { + logger.warn(`[PORT-FORWARD-STORE] Error removing port-forward: ${error}`, portForward); + } + } + + const index = portForwardStore.portForwards.findIndex(portForwardsEqual(portForward)); + + if (index >= 0 ) { + portForwardStore.portForwards.splice(index, 1); + } +}); + +/** + * gets the list of port-forwards in the store + * + * @returns the port-forwards + */ +export function getPortForwards(): ForwardedPort[] { + return portForwardStore.portForwards; } export const portForwardStore = new PortForwardStore();