mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Feature/port forward dashboard (#3922)
* Added ability to add custom port on pod and service port forwarding (#3295) * Added ability to add custom port on pod and service port forwarding Signed-off-by: rdeepc <12953177+rdeepc@users.noreply.github.com> * Added ability to add custom port on pod and service port forwarding - pod-container-port fixed init async and active port checking - service-port-component fixed init async and active port checking - port-forward-route promise and response fix Signed-off-by: rdeepc <12953177+rdeepc@users.noreply.github.com> * Added ability to add custom port on pod and service port forwarding Signed-off-by: rdeepc <12953177+rdeepc@users.noreply.github.com> * - Added Custom Port Selection for port forwarding - Implemented Random Port if custom port is not provided Signed-off-by: rdeepc <12953177+rdeepc@users.noreply.github.com> Co-authored-by: Saumya Shovan Roy <saumyashovanroy@gmail.com> * cherry-pick of hackweek work plus merge conflicts/build errors added a route to get all port forwards Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> Added Forwarded Ports to cluster dashboard Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> working port-forward page (open, edit, remove) Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> added local storage to the port-forward store Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> automatically restore port-forward after pod is restarted Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> start port-forwards using random local port by default, rearranged pod and service port-forward UI Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * refactor Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * more refactoring, don't always open port-forwards in browser, refined reused port-forward dialog Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * removed unimplemented forwarded port details page,modified logging Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * addressed some review comments Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * made port and forwardPort query params and cleaned up port-forward routing paths Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * address more review comments and change dashboard tab name to 'Port Forwarding' Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * changed port and forwardPort fields to be Numbers Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * removed extraneous reset() call, reorder field declarations Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * port-forward now gets the bundled kubectl path without going through the 'ensureKubectl' hoops Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * more cleanup/tweaking Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> * fix bug where port-forward info did not update on pod details page when different pod (in same deployment?) is clicked Signed-off-by: Jim Ehrismann <jehrismann@mirantis.com> Co-authored-by: Saumya Shovan Roy (Deep) <12953177+rdeepc@users.noreply.github.com> Co-authored-by: Saumya Shovan Roy <saumyashovanroy@gmail.com>
This commit is contained in:
parent
e10c13cdf4
commit
07c8177a97
@ -40,6 +40,7 @@ export * from "./network-policies";
|
|||||||
export * from "./network";
|
export * from "./network";
|
||||||
export * from "./nodes";
|
export * from "./nodes";
|
||||||
export * from "./pod-disruption-budgets";
|
export * from "./pod-disruption-budgets";
|
||||||
|
export * from "./port-forwards";
|
||||||
export * from "./preferences";
|
export * from "./preferences";
|
||||||
export * from "./releases";
|
export * from "./releases";
|
||||||
export * from "./resource-quotas";
|
export * from "./resource-quotas";
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import { endpointRoute } from "./endpoints";
|
|||||||
import { ingressRoute } from "./ingresses";
|
import { ingressRoute } from "./ingresses";
|
||||||
import { networkPoliciesRoute } from "./network-policies";
|
import { networkPoliciesRoute } from "./network-policies";
|
||||||
import { servicesRoute, servicesURL } from "./services";
|
import { servicesRoute, servicesURL } from "./services";
|
||||||
|
import { portForwardsRoute } from "./port-forwards";
|
||||||
|
|
||||||
export const networkRoute: RouteProps = {
|
export const networkRoute: RouteProps = {
|
||||||
path: [
|
path: [
|
||||||
@ -32,6 +33,7 @@ export const networkRoute: RouteProps = {
|
|||||||
endpointRoute,
|
endpointRoute,
|
||||||
ingressRoute,
|
ingressRoute,
|
||||||
networkPoliciesRoute,
|
networkPoliciesRoute,
|
||||||
|
portForwardsRoute,
|
||||||
].map(route => route.path.toString())
|
].map(route => route.path.toString())
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
32
src/common/routes/port-forwards.ts
Normal file
32
src/common/routes/port-forwards.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { RouteProps } from "react-router";
|
||||||
|
import { buildURL } from "../utils/buildUrl";
|
||||||
|
|
||||||
|
export const portForwardsRoute: RouteProps = {
|
||||||
|
path: "/port-forwards"
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface PortForwardsRouteParams {
|
||||||
|
}
|
||||||
|
|
||||||
|
export const portForwardsURL = buildURL<PortForwardsRouteParams>(portForwardsRoute.path);
|
||||||
@ -179,8 +179,11 @@ export class Router {
|
|||||||
this.router.add({ method: "post", path: `${apiPrefix}/metrics` }, MetricsRoute.routeMetrics);
|
this.router.add({ method: "post", path: `${apiPrefix}/metrics` }, MetricsRoute.routeMetrics);
|
||||||
this.router.add({ method: "get", path: `${apiPrefix}/metrics/providers` }, MetricsRoute.routeMetricsProviders);
|
this.router.add({ method: "get", path: `${apiPrefix}/metrics/providers` }, MetricsRoute.routeMetricsProviders);
|
||||||
|
|
||||||
// Port-forward API
|
// Port-forward API (the container port and local forwarding port are obtained from the query parameters)
|
||||||
this.router.add({ method: "post", path: `${apiPrefix}/pods/{namespace}/{resourceType}/{resourceName}/port-forward/{port}` }, PortForwardRoute.routePortForward);
|
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
|
// Helm API
|
||||||
this.router.add({ method: "get", path: `${apiPrefix}/v2/charts` }, HelmApiRoute.listCharts);
|
this.router.add({ method: "get", path: `${apiPrefix}/v2/charts` }, HelmApiRoute.listCharts);
|
||||||
|
|||||||
@ -22,7 +22,6 @@
|
|||||||
import type { LensApiRequest } from "../router";
|
import type { LensApiRequest } from "../router";
|
||||||
import { spawn, ChildProcessWithoutNullStreams } from "child_process";
|
import { spawn, ChildProcessWithoutNullStreams } from "child_process";
|
||||||
import { Kubectl } from "../kubectl";
|
import { Kubectl } from "../kubectl";
|
||||||
import { shell } from "electron";
|
|
||||||
import * as tcpPortUsed from "tcp-port-used";
|
import * as tcpPortUsed from "tcp-port-used";
|
||||||
import logger from "../logger";
|
import logger from "../logger";
|
||||||
import { getPortFrom } from "../utils/get-port";
|
import { getPortFrom } from "../utils/get-port";
|
||||||
@ -33,7 +32,8 @@ interface PortForwardArgs {
|
|||||||
kind: string;
|
kind: string;
|
||||||
namespace: string;
|
namespace: string;
|
||||||
name: string;
|
name: string;
|
||||||
port: string;
|
port: number;
|
||||||
|
forwardPort: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const internalPortRegex = /^forwarding from (?<address>.+) ->/i;
|
const internalPortRegex = /^forwarding from (?<address>.+) ->/i;
|
||||||
@ -56,8 +56,8 @@ class PortForward {
|
|||||||
public kind: string;
|
public kind: string;
|
||||||
public namespace: string;
|
public namespace: string;
|
||||||
public name: string;
|
public name: string;
|
||||||
public port: string;
|
public port: number;
|
||||||
public internalPort?: number;
|
public forwardPort: number;
|
||||||
|
|
||||||
constructor(public kubeConfig: string, args: PortForwardArgs) {
|
constructor(public kubeConfig: string, args: PortForwardArgs) {
|
||||||
this.clusterId = args.clusterId;
|
this.clusterId = args.clusterId;
|
||||||
@ -65,16 +65,17 @@ class PortForward {
|
|||||||
this.namespace = args.namespace;
|
this.namespace = args.namespace;
|
||||||
this.name = args.name;
|
this.name = args.name;
|
||||||
this.port = args.port;
|
this.port = args.port;
|
||||||
|
this.forwardPort = args.forwardPort;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async start() {
|
public async start() {
|
||||||
const kubectlBin = await Kubectl.bundled().getPath();
|
const kubectlBin = await Kubectl.bundled().getPath(true);
|
||||||
const args = [
|
const args = [
|
||||||
"--kubeconfig", this.kubeConfig,
|
"--kubeconfig", this.kubeConfig,
|
||||||
"port-forward",
|
"port-forward",
|
||||||
"-n", this.namespace,
|
"-n", this.namespace,
|
||||||
`${this.kind}/${this.name}`,
|
`${this.kind}/${this.name}`,
|
||||||
`:${this.port}`
|
`${this.forwardPort ?? ""}:${this.port}`
|
||||||
];
|
];
|
||||||
|
|
||||||
this.process = spawn(kubectlBin, args, {
|
this.process = spawn(kubectlBin, args, {
|
||||||
@ -89,12 +90,15 @@ class PortForward {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.internalPort = await getPortFrom(this.process.stdout, {
|
const internalPort = await getPortFrom(this.process.stdout, {
|
||||||
lineRegex: internalPortRegex,
|
lineRegex: internalPortRegex,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await tcpPortUsed.waitUntilUsed(this.internalPort, 500, 15000);
|
await tcpPortUsed.waitUntilUsed(internalPort, 500, 15000);
|
||||||
|
|
||||||
|
// make sure this.forwardPort is set to the actual port used (if it was 0 then an available port is found by 'kubectl port-forward')
|
||||||
|
this.forwardPort = internalPort;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -104,58 +108,113 @@ class PortForward {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public open() {
|
public async stop() {
|
||||||
shell.openExternal(`http://localhost:${this.internalPort}`)
|
this.process.kill();
|
||||||
.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 {
|
export class PortForwardRoute {
|
||||||
static async routePortForward(request: LensApiRequest) {
|
static async routePortForward(request: LensApiRequest) {
|
||||||
const { params, response, cluster} = request;
|
const { params, query, response, cluster } = request;
|
||||||
const { namespace, port, resourceType, resourceName } = params;
|
const { namespace, resourceType, resourceName } = params;
|
||||||
let portForward = PortForward.getPortforward({
|
const port = Number(query.get("port"));
|
||||||
clusterId: cluster.id, kind: resourceType, name: resourceName,
|
const forwardPort = Number(query.get("forwardPort"));
|
||||||
namespace, port
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!portForward) {
|
try {
|
||||||
logger.info(`Creating a new port-forward ${namespace}/${resourceType}/${resourceName}:${port}`);
|
let portForward = PortForward.getPortforward({
|
||||||
portForward = new PortForward(await cluster.getProxyKubeconfigPath(), {
|
clusterId: cluster.id, kind: resourceType, name: resourceName,
|
||||||
clusterId: cluster.id,
|
namespace, port, forwardPort,
|
||||||
kind: resourceType,
|
|
||||||
namespace,
|
|
||||||
name: resourceName,
|
|
||||||
port,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
let thePort = 0;
|
||||||
|
|
||||||
|
if (forwardPort > 0 && forwardPort < 65536) {
|
||||||
|
thePort = forwardPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!portForward) {
|
||||||
|
logger.info(`Creating a new port-forward ${namespace}/${resourceType}/${resourceName}:${port}`);
|
||||||
|
portForward = new PortForward(await cluster.getProxyKubeconfigPath(), {
|
||||||
|
clusterId: cluster.id,
|
||||||
|
kind: resourceType,
|
||||||
|
namespace,
|
||||||
|
name: resourceName,
|
||||||
|
port,
|
||||||
|
forwardPort: thePort,
|
||||||
|
});
|
||||||
|
|
||||||
const started = await portForward.start();
|
const started = await portForward.start();
|
||||||
|
|
||||||
if (!started) {
|
if (!started) {
|
||||||
logger.warn("[PORT-FORWARD-ROUTE]: failed to start a port-forward", { namespace, port, resourceType, resourceName });
|
logger.error("[PORT-FORWARD-ROUTE]: failed to start a port-forward", { namespace, port, resourceType, resourceName });
|
||||||
|
|
||||||
return respondJson(response, {
|
return respondJson(response, {
|
||||||
message: "Failed to open port-forward"
|
message: `Failed to forward port ${port} to ${thePort ? forwardPort : "random port"}`
|
||||||
}, 400);
|
}, 400);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
logger.warn(`[PORT-FORWARD-ROUTE]: failed to open a port-forward: ${error}`, { namespace, port, resourceType, resourceName });
|
|
||||||
|
|
||||||
return respondJson(response, {
|
|
||||||
message: error?.toString() || "Failed to open port-forward",
|
|
||||||
}, 400);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
respondJson(response, { port: portForward.forwardPort });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[PORT-FORWARD-ROUTE]: failed to open a port-forward: ${error}`, { namespace, port, resourceType, resourceName });
|
||||||
|
|
||||||
|
return respondJson(response, {
|
||||||
|
message: `Failed to forward port ${port}`
|
||||||
|
}, 400);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
portForward.open();
|
static async routeCurrentPortForward(request: LensApiRequest) {
|
||||||
|
const { params, query, response, cluster } = request;
|
||||||
|
const { namespace, resourceType, resourceName } = params;
|
||||||
|
const port = Number(query.get("port"));
|
||||||
|
const forwardPort = Number(query.get("forwardPort"));
|
||||||
|
|
||||||
respondJson(response, {});
|
const portForward = PortForward.getPortforward({
|
||||||
|
clusterId: cluster.id, kind: resourceType, name: resourceName,
|
||||||
|
namespace, port, forwardPort
|
||||||
|
});
|
||||||
|
|
||||||
|
respondJson(response, { port: portForward?.forwardPort ?? null });
|
||||||
|
}
|
||||||
|
|
||||||
|
static async routeAllPortForwards(request: LensApiRequest) {
|
||||||
|
const { response } = request;
|
||||||
|
|
||||||
|
const portForwards: PortForwardArgs[] = PortForward.portForwards.map(f => (
|
||||||
|
{
|
||||||
|
clusterId: f.clusterId,
|
||||||
|
kind: f.kind,
|
||||||
|
namespace: f.namespace,
|
||||||
|
name: f.name,
|
||||||
|
port: f.port,
|
||||||
|
forwardPort: f.forwardPort,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
respondJson(response, { portForwards });
|
||||||
|
}
|
||||||
|
|
||||||
|
static async routeCurrentPortForwardStop(request: LensApiRequest) {
|
||||||
|
const { params, query, response, cluster } = request;
|
||||||
|
const { namespace, resourceType, resourceName } = params;
|
||||||
|
const port = Number(query.get("port"));
|
||||||
|
const forwardPort = Number(query.get("forwardPort"));
|
||||||
|
|
||||||
|
const portForward = PortForward.getPortforward({
|
||||||
|
clusterId: cluster.id, kind: resourceType, name: resourceName,
|
||||||
|
namespace, port, forwardPort,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await portForward.stop();
|
||||||
|
respondJson(response, { status: true });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("[PORT-FORWARD-ROUTE]: error stopping a port-forward", { namespace, port, forwardPort, resourceType, resourceName });
|
||||||
|
|
||||||
|
return respondJson(response, {
|
||||||
|
message: `error stopping a forward port ${port}`
|
||||||
|
}, 400);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
22
src/renderer/components/+network-port-forwards/index.ts
Normal file
22
src/renderer/components/+network-port-forwards/index.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from "./port-forwards";
|
||||||
@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
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 { PortForwardDialog } from "../../port-forward";
|
||||||
|
|
||||||
|
interface Props extends MenuActionsProps {
|
||||||
|
portForward: PortForwardItem;
|
||||||
|
hideDetails?(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PortForwardMenu extends React.Component<Props> {
|
||||||
|
@boundMethod
|
||||||
|
remove() {
|
||||||
|
return removePortForward(this.props.portForward);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderContent() {
|
||||||
|
const { portForward, toolbar } = this.props;
|
||||||
|
|
||||||
|
if (!portForward) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MenuItem onClick={() => openPortForward(this.props.portForward)}>
|
||||||
|
<Icon material="open_in_browser" interactive={toolbar} tooltip="Open in browser" />
|
||||||
|
<span className="title">Open</span>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={() => PortForwardDialog.open(portForward)}>
|
||||||
|
<Icon material="edit" tooltip="Change port" interactive={toolbar} />
|
||||||
|
<span className="title">Edit</span>
|
||||||
|
</MenuItem>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { className, ...menuProps } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuActions
|
||||||
|
{...menuProps}
|
||||||
|
className={cssNames("PortForwardMenu", className)}
|
||||||
|
removeAction={this.remove}
|
||||||
|
>
|
||||||
|
{this.renderContent()}
|
||||||
|
</MenuActions>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.PortForwards {
|
||||||
|
.TableCell {
|
||||||
|
&.warning {
|
||||||
|
@include table-cell-warning;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
103
src/renderer/components/+network-port-forwards/port-forwards.tsx
Normal file
103
src/renderer/components/+network-port-forwards/port-forwards.tsx
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "./port-forwards.scss";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
|
import { ItemListLayout } from "../item-object-list/item-list-layout";
|
||||||
|
import { PortForwardItem, portForwardStore } from "../../port-forward";
|
||||||
|
import { PortForwardMenu } from "./port-forward-menu";
|
||||||
|
|
||||||
|
enum columnId {
|
||||||
|
name = "name",
|
||||||
|
namespace = "namespace",
|
||||||
|
kind = "kind",
|
||||||
|
port = "port",
|
||||||
|
forwardPort = "forwardPort",
|
||||||
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class PortForwards extends React.Component {
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
disposeOnUnmount(this, [
|
||||||
|
portForwardStore.watch(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderRemoveDialogMessage(selectedItems: PortForwardItem[]) {
|
||||||
|
const forwardPorts = selectedItems.map(item => item.getForwardPort()).join(", ");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<>Stop forwarding from <b>{forwardPorts}</b>?</>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ItemListLayout
|
||||||
|
isConfigurable
|
||||||
|
tableId="port_forwards"
|
||||||
|
className="PortForwards" store={portForwardStore}
|
||||||
|
sortingCallbacks={{
|
||||||
|
[columnId.name]: item => item.getName(),
|
||||||
|
[columnId.namespace]: item => item.getNs(),
|
||||||
|
[columnId.kind]: item => item.getKind(),
|
||||||
|
[columnId.port]: item => item.getPort(),
|
||||||
|
[columnId.forwardPort]: item => item.getForwardPort(),
|
||||||
|
}}
|
||||||
|
searchFilters={[
|
||||||
|
item => item.getSearchFields(),
|
||||||
|
]}
|
||||||
|
renderHeaderTitle="Port Forwarding"
|
||||||
|
renderTableHeader={[
|
||||||
|
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
|
||||||
|
{ title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
|
||||||
|
{ title: "Kind", className: "kind", sortBy: columnId.kind, id: columnId.kind },
|
||||||
|
{ title: "Pod Port", className: "port", sortBy: columnId.port, id: columnId.port },
|
||||||
|
{ title: "Local Port", className: "forwardPort", sortBy: columnId.forwardPort, id: columnId.forwardPort },
|
||||||
|
]}
|
||||||
|
renderTableContents={item => [
|
||||||
|
item.getName(),
|
||||||
|
item.getNs(),
|
||||||
|
item.getKind(),
|
||||||
|
item.getPort(),
|
||||||
|
item.getForwardPort(),
|
||||||
|
]}
|
||||||
|
renderItemMenu={pf => (
|
||||||
|
<PortForwardMenu
|
||||||
|
portForward={pf}
|
||||||
|
removeConfirmationMessage={this.renderRemoveDialogMessage([pf])}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
customizeRemoveDialog={selectedItems => ({
|
||||||
|
message: this.renderRemoveDialogMessage(selectedItems)
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -32,6 +32,7 @@ import { ServicePortComponent } from "./service-port-component";
|
|||||||
import { endpointStore } from "../+network-endpoints/endpoints.store";
|
import { endpointStore } from "../+network-endpoints/endpoints.store";
|
||||||
import { ServiceDetailsEndpoint } from "./service-details-endpoint";
|
import { ServiceDetailsEndpoint } from "./service-details-endpoint";
|
||||||
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
|
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
|
||||||
|
import { portForwardStore } from "../../port-forward";
|
||||||
|
|
||||||
interface Props extends KubeObjectDetailsProps<Service> {
|
interface Props extends KubeObjectDetailsProps<Service> {
|
||||||
}
|
}
|
||||||
@ -46,6 +47,7 @@ export class ServiceDetails extends React.Component<Props> {
|
|||||||
preload: true,
|
preload: true,
|
||||||
namespaces: [service.getNs()],
|
namespaces: [service.getNs()],
|
||||||
}),
|
}),
|
||||||
|
portForwardStore.watch(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -33,11 +33,13 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: $primary;
|
color: $primary;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
padding-right: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Spinner {
|
.portInput {
|
||||||
--spinner-size: #{$unit * 2};
|
display: inline-block !important;
|
||||||
margin-left: $margin;
|
width: 70px;
|
||||||
position: absolute;
|
margin-left: 10px;
|
||||||
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,12 +22,15 @@
|
|||||||
import "./service-port-component.scss";
|
import "./service-port-component.scss";
|
||||||
|
|
||||||
import React from "react";
|
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 type { Service, ServicePort } from "../../../common/k8s-api/endpoints";
|
||||||
import { apiBase } from "../../api";
|
import { observable, makeObservable, reaction } from "mobx";
|
||||||
import { observable, makeObservable } from "mobx";
|
|
||||||
import { cssNames } from "../../utils";
|
import { cssNames } from "../../utils";
|
||||||
import { Notifications } from "../notifications";
|
import { Notifications } from "../notifications";
|
||||||
|
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";
|
import { Spinner } from "../spinner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -38,20 +41,86 @@ interface Props {
|
|||||||
@observer
|
@observer
|
||||||
export class ServicePortComponent extends React.Component<Props> {
|
export class ServicePortComponent extends React.Component<Props> {
|
||||||
@observable waiting = false;
|
@observable waiting = false;
|
||||||
|
@observable forwardPort = 0;
|
||||||
|
@observable isPortForwarded = false;
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
disposeOnUnmount(this, [
|
||||||
|
reaction(() => [ portForwardStore.portForwards, this.props.service ], () => this.init()),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.checkExistingPortForwarding().catch(error => {
|
||||||
|
logger.error(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkExistingPortForwarding() {
|
||||||
|
const { service, port } = this.props;
|
||||||
|
const portForward: ForwardedPort = {
|
||||||
|
kind: "service",
|
||||||
|
name: service.getName(),
|
||||||
|
namespace: service.getNs(),
|
||||||
|
port: port.port,
|
||||||
|
forwardPort: this.forwardPort,
|
||||||
|
};
|
||||||
|
const activePort = await getPortForward(portForward) ?? 0;
|
||||||
|
|
||||||
|
this.forwardPort = activePort;
|
||||||
|
this.isPortForwarded = activePort ? true : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async portForward() {
|
async portForward() {
|
||||||
const { service, port } = this.props;
|
const { service, port } = this.props;
|
||||||
|
const portForward: ForwardedPort = {
|
||||||
|
kind: "service",
|
||||||
|
name: service.getName(),
|
||||||
|
namespace: service.getNs(),
|
||||||
|
port: port.port,
|
||||||
|
forwardPort: this.forwardPort,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.waiting = true;
|
||||||
|
this.isPortForwarded = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.forwardPort = await addPortForward(portForward);
|
||||||
|
|
||||||
|
if (this.forwardPort) {
|
||||||
|
portForward.forwardPort = this.forwardPort;
|
||||||
|
openPortForward(portForward);
|
||||||
|
this.isPortForwarded = true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Notifications.error(error);
|
||||||
|
} finally {
|
||||||
|
this.waiting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async stopPortForward() {
|
||||||
|
const { service, port } = this.props;
|
||||||
|
const portForward: ForwardedPort = {
|
||||||
|
kind: "service",
|
||||||
|
name: service.getName(),
|
||||||
|
namespace: service.getNs(),
|
||||||
|
port: port.port,
|
||||||
|
forwardPort: this.forwardPort,
|
||||||
|
};
|
||||||
|
|
||||||
this.waiting = true;
|
this.waiting = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await apiBase.post(`/pods/${service.getNs()}/service/${service.getName()}/port-forward/${port.port}`, {});
|
await removePortForward(portForward);
|
||||||
} catch(error) {
|
this.isPortForwarded = false;
|
||||||
|
} catch (error) {
|
||||||
Notifications.error(error);
|
Notifications.error(error);
|
||||||
} finally {
|
} finally {
|
||||||
this.waiting = false;
|
this.waiting = false;
|
||||||
@ -59,16 +128,33 @@ export class ServicePortComponent extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { port } = this.props;
|
const { port, service } = this.props;
|
||||||
|
|
||||||
|
const portForwardAction = async () => {
|
||||||
|
if (this.isPortForwarded) {
|
||||||
|
await this.stopPortForward();
|
||||||
|
} else {
|
||||||
|
const portForward: ForwardedPort = {
|
||||||
|
kind: "service",
|
||||||
|
name: service.getName(),
|
||||||
|
namespace: service.getNs(),
|
||||||
|
port: port.port,
|
||||||
|
forwardPort: this.forwardPort,
|
||||||
|
};
|
||||||
|
|
||||||
|
PortForwardDialog.open(portForward, { openInBrowser: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cssNames("ServicePortComponent", { waiting: this.waiting })}>
|
<div className={cssNames("ServicePortComponent", { waiting: this.waiting })}>
|
||||||
<span title="Open in a browser" onClick={() => this.portForward() }>
|
<span title="Open in a browser" onClick={() => this.portForward()}>
|
||||||
{port.toString()}
|
{port.toString()}
|
||||||
{this.waiting && (
|
|
||||||
<Spinner />
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
|
<Button onClick={() => portForwardAction()}> {this.isPortForwarded ? "Stop" : "Forward..."} </Button>
|
||||||
|
{this.waiting && (
|
||||||
|
<Spinner />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,6 +28,7 @@ import { Services } from "../+network-services";
|
|||||||
import { Endpoints } from "../+network-endpoints";
|
import { Endpoints } from "../+network-endpoints";
|
||||||
import { Ingresses } from "../+network-ingresses";
|
import { Ingresses } from "../+network-ingresses";
|
||||||
import { NetworkPolicies } from "../+network-policies";
|
import { NetworkPolicies } from "../+network-policies";
|
||||||
|
import { PortForwards } from "../+network-port-forwards";
|
||||||
import { isAllowedResource } from "../../../common/utils/allowed-resource";
|
import { isAllowedResource } from "../../../common/utils/allowed-resource";
|
||||||
import * as routes from "../../../common/routes";
|
import * as routes from "../../../common/routes";
|
||||||
|
|
||||||
@ -72,6 +73,13 @@ export class Network extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tabs.push({
|
||||||
|
title: "Port Forwarding",
|
||||||
|
component: PortForwards,
|
||||||
|
url: routes.portForwardsURL(),
|
||||||
|
routePath: routes.portForwardsRoute.path.toString(),
|
||||||
|
});
|
||||||
|
|
||||||
return tabs;
|
return tabs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -34,11 +34,13 @@
|
|||||||
color: $primary;
|
color: $primary;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
padding-right: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Spinner {
|
.portInput {
|
||||||
--spinner-size: #{$unit * 2};
|
display: inline-block !important;
|
||||||
margin-left: $margin;
|
width: 70px;
|
||||||
position: absolute;
|
margin-left: 10px;
|
||||||
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,12 +22,15 @@
|
|||||||
import "./pod-container-port.scss";
|
import "./pod-container-port.scss";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import type { Pod } from "../../../common/k8s-api/endpoints";
|
import type { Pod } from "../../../common/k8s-api/endpoints";
|
||||||
import { apiBase } from "../../api";
|
import { observable, makeObservable, reaction } from "mobx";
|
||||||
import { observable, makeObservable } from "mobx";
|
|
||||||
import { cssNames } from "../../utils";
|
import { cssNames } from "../../utils";
|
||||||
import { Notifications } from "../notifications";
|
import { Notifications } from "../notifications";
|
||||||
|
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";
|
import { Spinner } from "../spinner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -42,20 +45,86 @@ interface Props {
|
|||||||
@observer
|
@observer
|
||||||
export class PodContainerPort extends React.Component<Props> {
|
export class PodContainerPort extends React.Component<Props> {
|
||||||
@observable waiting = false;
|
@observable waiting = false;
|
||||||
|
@observable forwardPort = 0;
|
||||||
|
@observable isPortForwarded = false;
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
disposeOnUnmount(this, [
|
||||||
|
reaction(() => [ portForwardStore.portForwards, this.props.pod ], () => this.init()),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.checkExistingPortForwarding().catch(error => {
|
||||||
|
logger.error(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkExistingPortForwarding() {
|
||||||
|
const { pod, port } = this.props;
|
||||||
|
const portForward: ForwardedPort = {
|
||||||
|
kind: "pod",
|
||||||
|
name: pod.getName(),
|
||||||
|
namespace: pod.getNs(),
|
||||||
|
port: port.containerPort,
|
||||||
|
forwardPort: this.forwardPort,
|
||||||
|
};
|
||||||
|
const activePort = await getPortForward(portForward) ?? 0;
|
||||||
|
|
||||||
|
this.forwardPort = activePort;
|
||||||
|
this.isPortForwarded = activePort ? true : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async portForward() {
|
async portForward() {
|
||||||
const { pod, port } = this.props;
|
const { pod, port } = this.props;
|
||||||
|
const portForward: ForwardedPort = {
|
||||||
|
kind: "pod",
|
||||||
|
name: pod.getName(),
|
||||||
|
namespace: pod.getNs(),
|
||||||
|
port: port.containerPort,
|
||||||
|
forwardPort: this.forwardPort,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.waiting = true;
|
||||||
|
this.isPortForwarded = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.forwardPort = await addPortForward(portForward);
|
||||||
|
|
||||||
|
if (this.forwardPort) {
|
||||||
|
portForward.forwardPort = this.forwardPort;
|
||||||
|
openPortForward(portForward);
|
||||||
|
this.isPortForwarded = true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Notifications.error(error);
|
||||||
|
} finally {
|
||||||
|
this.waiting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async stopPortForward() {
|
||||||
|
const { pod, port } = this.props;
|
||||||
|
const portForward: ForwardedPort = {
|
||||||
|
kind: "pod",
|
||||||
|
name: pod.getName(),
|
||||||
|
namespace: pod.getNs(),
|
||||||
|
port: port.containerPort,
|
||||||
|
forwardPort: this.forwardPort,
|
||||||
|
};
|
||||||
|
|
||||||
this.waiting = true;
|
this.waiting = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await apiBase.post(`/pods/${pod.getNs()}/pod/${pod.getName()}/port-forward/${port.containerPort}`, {});
|
await removePortForward(portForward);
|
||||||
} catch(error) {
|
this.isPortForwarded = false;
|
||||||
|
} catch (error) {
|
||||||
Notifications.error(error);
|
Notifications.error(error);
|
||||||
} finally {
|
} finally {
|
||||||
this.waiting = false;
|
this.waiting = false;
|
||||||
@ -63,18 +132,35 @@ export class PodContainerPort extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { port } = this.props;
|
const { pod, port } = this.props;
|
||||||
const { name, containerPort, protocol } = port;
|
const { name, containerPort, protocol } = port;
|
||||||
const text = `${name ? `${name}: ` : ""}${containerPort}/${protocol}`;
|
const text = `${name ? `${name}: ` : ""}${containerPort}/${protocol}`;
|
||||||
|
|
||||||
|
const portForwardAction = async () => {
|
||||||
|
if (this.isPortForwarded) {
|
||||||
|
await this.stopPortForward();
|
||||||
|
} else {
|
||||||
|
const portForward: ForwardedPort = {
|
||||||
|
kind: "pod",
|
||||||
|
name: pod.getName(),
|
||||||
|
namespace: pod.getNs(),
|
||||||
|
port: port.containerPort,
|
||||||
|
forwardPort: this.forwardPort,
|
||||||
|
};
|
||||||
|
|
||||||
|
PortForwardDialog.open(portForward, { openInBrowser: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cssNames("PodContainerPort", { waiting: this.waiting })}>
|
<div className={cssNames("PodContainerPort", { waiting: this.waiting })}>
|
||||||
<span title="Open in a browser" onClick={() => this.portForward() }>
|
<span title="Open in a browser" onClick={() => this.portForward()}>
|
||||||
{text}
|
{text}
|
||||||
{this.waiting && (
|
|
||||||
<Spinner />
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
|
<Button onClick={() => portForwardAction()}> {this.isPortForwarded ? "Stop" : "Forward..."} </Button>
|
||||||
|
{this.waiting && (
|
||||||
|
<Spinner />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,6 +35,8 @@ import { ContainerCharts } from "./container-charts";
|
|||||||
import { LocaleDate } from "../locale-date";
|
import { LocaleDate } from "../locale-date";
|
||||||
import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
|
import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
|
||||||
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
||||||
|
import { portForwardStore } from "../../port-forward/port-forward.store";
|
||||||
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
pod: Pod;
|
pod: Pod;
|
||||||
@ -42,8 +44,15 @@ interface Props {
|
|||||||
metrics?: { [key: string]: IMetrics };
|
metrics?: { [key: string]: IMetrics };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
export class PodDetailsContainer extends React.Component<Props> {
|
export class PodDetailsContainer extends React.Component<Props> {
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
disposeOnUnmount(this, [
|
||||||
|
portForwardStore.watch(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
renderStatus(state: string, status: IPodContainerStatus) {
|
renderStatus(state: string, status: IPodContainerStatus) {
|
||||||
const ready = status ? status.ready : "";
|
const ready = status ? status.ready : "";
|
||||||
|
|
||||||
|
|||||||
@ -74,6 +74,7 @@ import { ClusterStore } from "../../common/cluster-store";
|
|||||||
import type { ClusterId } from "../../common/cluster-types";
|
import type { ClusterId } from "../../common/cluster-types";
|
||||||
import { watchHistoryState } from "../remote-helpers/history-updater";
|
import { watchHistoryState } from "../remote-helpers/history-updater";
|
||||||
import { unmountComponentAtNode } from "react-dom";
|
import { unmountComponentAtNode } from "react-dom";
|
||||||
|
import { PortForwardDialog } from "../port-forward";
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class App extends React.Component {
|
export class App extends React.Component {
|
||||||
@ -231,6 +232,7 @@ export class App extends React.Component {
|
|||||||
<StatefulSetScaleDialog/>
|
<StatefulSetScaleDialog/>
|
||||||
<ReplicaSetScaleDialog/>
|
<ReplicaSetScaleDialog/>
|
||||||
<CronJobTriggerDialog/>
|
<CronJobTriggerDialog/>
|
||||||
|
<PortForwardDialog/>
|
||||||
<CommandContainer clusterId={App.clusterId}/>
|
<CommandContainer clusterId={App.clusterId}/>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</Router>
|
</Router>
|
||||||
|
|||||||
24
src/renderer/port-forward/index.ts
Normal file
24
src/renderer/port-forward/index.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from "./port-forward.store";
|
||||||
|
export * from "./port-forward-item";
|
||||||
|
export * from "./port-forward-dialog";
|
||||||
77
src/renderer/port-forward/port-forward-dialog.scss
Normal file
77
src/renderer/port-forward/port-forward-dialog.scss
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.PortForwardDialog {
|
||||||
|
.Wizard {
|
||||||
|
.header {
|
||||||
|
span {
|
||||||
|
color: #a0a0a0;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.WizardStep {
|
||||||
|
.step-content {
|
||||||
|
min-height: 90px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-scale {
|
||||||
|
font-weight: bold
|
||||||
|
}
|
||||||
|
|
||||||
|
.desired-scale {
|
||||||
|
flex: 1.1 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-container {
|
||||||
|
flex: 1 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plus-minus-container {
|
||||||
|
margin-left: $margin * 2;
|
||||||
|
.Icon {
|
||||||
|
--color-active: black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
color: $colorSoftError;
|
||||||
|
font-size: small;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.Icon {
|
||||||
|
margin: 0;
|
||||||
|
margin-right: $margin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.portInput {
|
||||||
|
display: inline-block !important;
|
||||||
|
width: 70px;
|
||||||
|
margin-left: 10px;
|
||||||
|
margin-right: 10px;
|
||||||
|
} }
|
||||||
|
|
||||||
|
}
|
||||||
173
src/renderer/port-forward/port-forward-dialog.tsx
Normal file
173
src/renderer/port-forward/port-forward-dialog.tsx
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "./port-forward-dialog.scss";
|
||||||
|
|
||||||
|
import React, { Component } from "react";
|
||||||
|
import { observable, makeObservable } from "mobx";
|
||||||
|
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 { addPortForward, modifyPortForward } from "./port-forward.store";
|
||||||
|
import type { ForwardedPort } from "./port-forward-item";
|
||||||
|
import { openPortForward } from ".";
|
||||||
|
|
||||||
|
interface Props extends Partial<DialogProps> {
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PortForwardDialogOpenOptions {
|
||||||
|
openInBrowser: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const dialogState = observable.object({
|
||||||
|
isOpen: false,
|
||||||
|
data: null as ForwardedPort,
|
||||||
|
openInBrowser: false
|
||||||
|
});
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class PortForwardDialog extends Component<Props> {
|
||||||
|
@observable ready = false;
|
||||||
|
@observable currentPort = 0;
|
||||||
|
@observable desiredPort = 0;
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
makeObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
static open(portForward: ForwardedPort, options : PortForwardDialogOpenOptions = { openInBrowser: false }) {
|
||||||
|
dialogState.isOpen = true;
|
||||||
|
dialogState.data = portForward;
|
||||||
|
dialogState.openInBrowser = options.openInBrowser;
|
||||||
|
}
|
||||||
|
|
||||||
|
static close() {
|
||||||
|
dialogState.isOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get portForward() {
|
||||||
|
return dialogState.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
close = () => {
|
||||||
|
PortForwardDialog.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
onOpen = async () => {
|
||||||
|
const { portForward } = this;
|
||||||
|
|
||||||
|
this.currentPort = +portForward.forwardPort;
|
||||||
|
this.desiredPort = this.currentPort;
|
||||||
|
this.ready = this.currentPort ? false : true;
|
||||||
|
};
|
||||||
|
|
||||||
|
onClose = () => {
|
||||||
|
this.ready = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 {
|
||||||
|
let port: number;
|
||||||
|
|
||||||
|
if (currentPort) {
|
||||||
|
port = await modifyPortForward(portForward, desiredPort);
|
||||||
|
} else {
|
||||||
|
portForward.forwardPort = desiredPort;
|
||||||
|
port = await addPortForward(portForward);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dialogState.openInBrowser) {
|
||||||
|
portForward.forwardPort = port;
|
||||||
|
openPortForward(portForward);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Notifications.error(err);
|
||||||
|
} finally {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
renderContents() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex gaps align-center">
|
||||||
|
<div className="input-container flex align-center">
|
||||||
|
<div className="current-port" data-testid="current-port">
|
||||||
|
Local port to forward from:
|
||||||
|
</div>
|
||||||
|
<Input className="portInput"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="65535"
|
||||||
|
value={this.desiredPort === 0 ? "" : String(this.desiredPort)}
|
||||||
|
placeholder={"Random"}
|
||||||
|
onChange={this.changePort}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { className, ...dialogProps } = this.props;
|
||||||
|
const resourceName = this.portForward?.name ?? "";
|
||||||
|
const header = (
|
||||||
|
<h5>
|
||||||
|
Port Forwarding for <span>{resourceName}</span>
|
||||||
|
</h5>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
{...dialogProps}
|
||||||
|
isOpen={dialogState.isOpen}
|
||||||
|
className={cssNames("PortForwardDialog", className)}
|
||||||
|
onOpen={this.onOpen}
|
||||||
|
onClose={this.onClose}
|
||||||
|
close={this.close}
|
||||||
|
>
|
||||||
|
<Wizard header={header} done={this.close}>
|
||||||
|
<WizardStep
|
||||||
|
contentClass="flex gaps column"
|
||||||
|
next={this.startPortForward}
|
||||||
|
nextLabel={this.currentPort === 0 ? "Start" : "Restart"}
|
||||||
|
disabledNext={!this.ready}
|
||||||
|
>
|
||||||
|
{this.renderContents()}
|
||||||
|
</WizardStep>
|
||||||
|
</Wizard>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
91
src/renderer/port-forward/port-forward-item.ts
Normal file
91
src/renderer/port-forward/port-forward-item.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
import type { ItemObject } from "../../common/item.store";
|
||||||
|
import { autoBind } from "../../common/utils";
|
||||||
|
|
||||||
|
export interface ForwardedPort {
|
||||||
|
clusterId?: string;
|
||||||
|
kind: string;
|
||||||
|
namespace: string;
|
||||||
|
name: string;
|
||||||
|
port: number;
|
||||||
|
forwardPort: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PortForwardItem implements ItemObject {
|
||||||
|
clusterId: string;
|
||||||
|
kind: string;
|
||||||
|
namespace: string;
|
||||||
|
name: string;
|
||||||
|
port: number;
|
||||||
|
forwardPort: number;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
autoBind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
getName() {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
getNs() {
|
||||||
|
return this.namespace;
|
||||||
|
}
|
||||||
|
|
||||||
|
get id() {
|
||||||
|
return this.forwardPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
getId() {
|
||||||
|
return String(this.forwardPort);
|
||||||
|
}
|
||||||
|
|
||||||
|
getKind() {
|
||||||
|
return this.kind;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPort() {
|
||||||
|
return this.port;
|
||||||
|
}
|
||||||
|
|
||||||
|
getForwardPort() {
|
||||||
|
return this.forwardPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSearchFields() {
|
||||||
|
return [
|
||||||
|
this.name,
|
||||||
|
this.id,
|
||||||
|
this.kind,
|
||||||
|
this.port,
|
||||||
|
this.forwardPort,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
179
src/renderer/port-forward/port-forward.store.ts
Normal file
179
src/renderer/port-forward/port-forward.store.ts
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
import { makeObservable, observable, reaction } from "mobx";
|
||||||
|
import { ItemStore } from "../../common/item.store";
|
||||||
|
import { autoBind, createStorage, disposer, getHostedClusterId, openExternal } from "../utils";
|
||||||
|
import { ForwardedPort, 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 class PortForwardStore extends ItemStore<PortForwardItem> {
|
||||||
|
private storage = createStorage<ForwardedPort[] | undefined>("port_forwards", undefined);
|
||||||
|
|
||||||
|
@observable portForwards: PortForwardItem[];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
makeObservable(this);
|
||||||
|
autoBind(this);
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async init() {
|
||||||
|
await this.storage.whenReady;
|
||||||
|
|
||||||
|
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(addPortForward));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch() {
|
||||||
|
return disposer(
|
||||||
|
reaction(() => this.portForwards, () => this.loadAll()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadAll() {
|
||||||
|
return this.loadItems(async () => {
|
||||||
|
let portForwards = await getPortForwards();
|
||||||
|
|
||||||
|
// filter out any not for this cluster
|
||||||
|
portForwards = portForwards.filter(pf => pf.clusterId == getHostedClusterId());
|
||||||
|
this.storage.set(portForwards);
|
||||||
|
|
||||||
|
this.reset();
|
||||||
|
portForwards.map(pf => this.portForwards.push(new PortForwardItem(pf)));
|
||||||
|
|
||||||
|
return this.portForwards;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.portForwards = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeSelectedItems() {
|
||||||
|
return Promise.all(this.selectedItems.map(removePortForward));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PortForwardResult {
|
||||||
|
port: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PortForwardsResult {
|
||||||
|
portForwards: ForwardedPort[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function addPortForward(portForward: ForwardedPort): Promise<number> {
|
||||||
|
let response: PortForwardResult;
|
||||||
|
|
||||||
|
try {
|
||||||
|
response = await apiBase.post<PortForwardResult>(`/pods/port-forward/${portForward.namespace}/${portForward.kind}/${portForward.name}?port=${portForward.port}&forwardPort=${portForward.forwardPort}`);
|
||||||
|
|
||||||
|
if (response?.port && response.port != +portForward.forwardPort) {
|
||||||
|
logger.warn(`specified ${portForward.forwardPort} got ${response.port}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(error); // don't care, caller must check
|
||||||
|
}
|
||||||
|
portForwardStore.reset();
|
||||||
|
|
||||||
|
return response?.port;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPortForward(portForward: ForwardedPort): Promise<number> {
|
||||||
|
let response: PortForwardResult;
|
||||||
|
|
||||||
|
try {
|
||||||
|
response = await apiBase.get<PortForwardResult>(`/pods/port-forward/${portForward.namespace}/${portForward.kind}/${portForward.name}?port=${portForward.port}&forwardPort=${portForward.forwardPort}`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(error); // don't care, caller must check
|
||||||
|
}
|
||||||
|
|
||||||
|
return response?.port;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function modifyPortForward(portForward: ForwardedPort, desiredPort: number): Promise<number> {
|
||||||
|
let port = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await removePortForward(portForward);
|
||||||
|
portForward.forwardPort = desiredPort;
|
||||||
|
port = await addPortForward(portForward);
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(error); // don't care, caller must check
|
||||||
|
}
|
||||||
|
portForwardStore.reset();
|
||||||
|
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function removePortForward(portForward: ForwardedPort) {
|
||||||
|
try {
|
||||||
|
await apiBase.del(`/pods/port-forward/${portForward.namespace}/${portForward.kind}/${portForward.name}?port=${portForward.port}&forwardPort=${portForward.forwardPort}`);
|
||||||
|
await waitUntilFree(+portForward.forwardPort, 200, 1000);
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(error); // don't care, caller must check
|
||||||
|
}
|
||||||
|
portForwardStore.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPortForwards(): Promise<ForwardedPort[]> {
|
||||||
|
try {
|
||||||
|
const response = await apiBase.get<PortForwardsResult>(`/pods/port-forwards`);
|
||||||
|
|
||||||
|
return response.portForwards;
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(error); // don't care, caller must check
|
||||||
|
|
||||||
|
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();
|
||||||
Loading…
Reference in New Issue
Block a user