1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Added ability to add custom port on pod and service port forwarding

This commit is contained in:
rdeepc 2021-07-05 23:03:06 -04:00
parent 8ac3203924
commit 6f86615a91
6 changed files with 219 additions and 47 deletions

View File

@ -180,7 +180,9 @@ export class Router {
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
this.router.add({ method: "post", path: `${apiPrefix}/pods/{namespace}/{resourceType}/{resourceName}/port-forward/{port}` }, PortForwardRoute.routePortForward); this.router.add({ method: "post", path: `${apiPrefix}/pods/{namespace}/{resourceType}/{resourceName}/port-forward/{port}/{forwardPort}` }, PortForwardRoute.routePortForward);
this.router.add({ method: "get", path: `${apiPrefix}/pods/{namespace}/{resourceType}/{resourceName}/port-forward/{port}/{forwardPort}` }, PortForwardRoute.routeCurrentPortForward);
this.router.add({ method: "delete", path: `${apiPrefix}/pods/{namespace}/{resourceType}/{resourceName}/port-forward/{port}/{forwardPort}` }, 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);

View File

@ -34,6 +34,7 @@ interface PortForwardArgs {
namespace: string; namespace: string;
name: string; name: string;
port: string; port: string;
forwardPort: string;
} }
const internalPortRegex = /^forwarding from (?<address>.+) ->/i; const internalPortRegex = /^forwarding from (?<address>.+) ->/i;
@ -58,6 +59,7 @@ class PortForward {
public name: string; public name: string;
public port: string; public port: string;
public internalPort?: number; public internalPort?: number;
public forwardPort: string;
constructor(public kubeConfig: string, args: PortForwardArgs) { constructor(public kubeConfig: string, args: PortForwardArgs) {
this.clusterId = args.clusterId; this.clusterId = args.clusterId;
@ -65,6 +67,7 @@ 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() {
@ -74,7 +77,7 @@ class PortForward {
"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, {
@ -104,6 +107,10 @@ class PortForward {
} }
} }
public async stop() {
this.process.kill();
}
public open() { public open() {
shell.openExternal(`http://localhost:${this.internalPort}`) shell.openExternal(`http://localhost:${this.internalPort}`)
.catch(error => logger.error(`[PORT-FORWARD]: failed to open external shell: ${error}`, { .catch(error => logger.error(`[PORT-FORWARD]: failed to open external shell: ${error}`, {
@ -118,33 +125,73 @@ class PortForward {
export class PortForwardRoute { export class PortForwardRoute {
static async routePortForward(request: LensApiRequest) { static async routePortForward(request: LensApiRequest) {
const { params, response, cluster } = request;
const { namespace, port, forwardPort, resourceType, resourceName } = params;
try {
let portForward = PortForward.getPortforward({
clusterId: cluster.id, kind: resourceType, name: resourceName,
namespace, port, 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
});
const started = await portForward.start();
if (!started) {
return respondJson(response, {
message: `Failed to forward port ${port} to ${forwardPort}`
}, 400);
}
}
portForward.open();
respondJson(response, {port: portForward.forwardPort});
} catch (e) {
return respondJson(response, {
message: `Failed to forward port ${port} to ${forwardPort}`
}, 400);
}
}
static async routeCurrentPortForward(request: LensApiRequest) {
const { params, response, cluster} = request; const { params, response, cluster} = request;
const { namespace, port, resourceType, resourceName } = params; const { namespace, port, forwardPort, resourceType, resourceName } = params;
let portForward = PortForward.getPortforward({
const portForward = PortForward.getPortforward({
clusterId: cluster.id, kind: resourceType, name: resourceName, clusterId: cluster.id, kind: resourceType, name: resourceName,
namespace, port namespace, port, forwardPort
}); });
if (!portForward) { let forwardedPort = -1;
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,
});
const started = await portForward.start();
if (!started) { if (portForward) {
return respondJson(response, { forwardedPort = Number(portForward.forwardPort);
message: "Failed to open port-forward"
}, 400);
}
} }
portForward.open(); respondJson(response, {port: forwardedPort});
}
respondJson(response, {}); static async routeCurrentPortForwardStop(request: LensApiRequest) {
const { params, response, cluster} = request;
const { namespace, port, forwardPort, resourceType, resourceName } = params;
const portForward = PortForward.getPortforward({
clusterId: cluster.id, kind: resourceType, name: resourceName,
namespace, port, forwardPort,
});
portForward.stop().then( () => {
respondJson(response, {status: true});
}).catch(error => logger.error(error));
} }
} }

View File

@ -35,9 +35,10 @@
text-decoration: underline; text-decoration: underline;
} }
.Spinner { .portInput {
--spinner-size: #{$unit * 2}; display: inline-block !important;
margin-left: $margin; width: 65px;
position: absolute; margin-left: 10px;
margin-right: 10px;
} }
} }

View File

@ -28,20 +28,44 @@ import { apiBase } from "../../api";
import { observable, makeObservable } from "mobx"; import { observable, makeObservable } from "mobx";
import { cssNames } from "../../utils"; import { cssNames } from "../../utils";
import { Notifications } from "../notifications"; import { Notifications } from "../notifications";
import { Spinner } from "../spinner"; import { Input } from "../input";
import { Button } from "../button";
interface Props { interface Props {
service: Service; service: Service;
port: ServicePort; port: ServicePort;
} }
interface PortForwardResult {
port: number;
}
@observer @observer
export class ServicePortComponent extends React.Component<Props> { export class ServicePortComponent extends React.Component<Props> {
@observable waiting = false; @observable waiting = false;
@observable forwardPort = -1;
@observable isPortForwarded = false;
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
makeObservable(this); makeObservable(this);
this.init();
}
init() {
this.checkExistingPortForwarding().then();
}
async checkExistingPortForwarding() {
const { service, port } = this.props;
const response = await apiBase.get<PortForwardResult>(`/pods/${service.getNs()}/service/${service.getName()}/port-forward/${port.port}/${this.forwardPort}`, {});
const activePort = response.port;
if (activePort && activePort != -1) {
this.forwardPort = activePort;
this.isPortForwarded = true;
}
} }
async portForward() { async portForward() {
@ -50,7 +74,26 @@ export class ServicePortComponent extends React.Component<Props> {
this.waiting = true; this.waiting = true;
try { try {
await apiBase.post(`/pods/${service.getNs()}/service/${service.getName()}/port-forward/${port.port}`, {}); const response = await apiBase.post<PortForwardResult>(`/pods/${service.getNs()}/service/${service.getName()}/port-forward/${port.port}/${this.forwardPort}`, {});
this.forwardPort = response.port;
this.isPortForwarded = true;
} catch(error) {
Notifications.error(error);
} finally {
this.waiting = false;
}
}
async stopPortForward() {
const { service, port } = this.props;
this.waiting = true;
try {
await apiBase.del(`/pods/${service.getNs()}/service/${service.getName()}/port-forward/${port.port}/${this.forwardPort}`, {});
this.isPortForwarded = false;
} catch(error) { } catch(error) {
Notifications.error(error); Notifications.error(error);
} finally { } finally {
@ -61,14 +104,32 @@ export class ServicePortComponent extends React.Component<Props> {
render() { render() {
const { port } = this.props; const { port } = this.props;
if (this.forwardPort == -1) {
this.forwardPort = port.port;
}
const portForwardAction = async () => {
if (this.isPortForwarded) {
await this.stopPortForward();
}else {
await this.portForward();
}
};
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() }> {port.toString()}
{port.toString()} {" "}
{this.waiting && ( <text>to</text>
<Spinner /> <Input className={"portInput"}
)} type="number"
</span> min="0"
max="65535"
value= {String(this.forwardPort)}
disabled={this.isPortForwarded}
onChange={(value) => this.forwardPort = Number(value)}
/>
<Button onClick={() => portForwardAction()}> {this.isPortForwarded ? "Stop":"Forward"} </Button>
</div> </div>
); );
} }

View File

@ -36,9 +36,10 @@
position: relative; position: relative;
} }
.Spinner { .portInput {
--spinner-size: #{$unit * 2}; display: inline-block !important;
margin-left: $margin; width: 65px;
position: absolute; margin-left: 10px;
margin-right: 10px;
} }
} }

View File

@ -28,7 +28,8 @@ import { apiBase } from "../../api";
import { observable, makeObservable } from "mobx"; import { observable, makeObservable } from "mobx";
import { cssNames } from "../../utils"; import { cssNames } from "../../utils";
import { Notifications } from "../notifications"; import { Notifications } from "../notifications";
import { Spinner } from "../spinner"; import { Button } from "../button";
import { Input } from "../input";
interface Props { interface Props {
pod: Pod; pod: Pod;
@ -39,13 +40,36 @@ interface Props {
} }
} }
interface PortForwardResult {
port: number;
}
@observer @observer
export class PodContainerPort extends React.Component<Props> { export class PodContainerPort extends React.Component<Props> {
@observable waiting = false; @observable waiting = false;
@observable forwardPort = -1;
@observable isPortForwarded = false;
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
makeObservable(this); makeObservable(this);
this.init();
}
init() {
this.checkExistingPortForwarding().then();
}
async checkExistingPortForwarding() {
const { pod, port } = this.props;
const response = await apiBase.get<PortForwardResult>(`/pods/${pod.getNs()}/pod/${pod.getName()}/port-forward/${port.containerPort}/${this.forwardPort}`, {});
const activePort = response.port;
if (activePort && activePort != -1) {
this.forwardPort = activePort;
this.isPortForwarded = true;
}
} }
async portForward() { async portForward() {
@ -54,7 +78,25 @@ export class PodContainerPort extends React.Component<Props> {
this.waiting = true; this.waiting = true;
try { try {
await apiBase.post(`/pods/${pod.getNs()}/pod/${pod.getName()}/port-forward/${port.containerPort}`, {}); const response = await apiBase.post<PortForwardResult>(`/pods/${pod.getNs()}/pod/${pod.getName()}/port-forward/${port.containerPort}/${this.forwardPort}`, {});
this.forwardPort = response.port;
this.isPortForwarded = true;
} catch(error) {
Notifications.error(error);
} finally {
this.waiting = false;
}
}
async stopPortForward() {
const { pod, port } = this.props;
this.waiting = true;
try {
await apiBase.del(`/pods/${pod.getNs()}/pod/${pod.getName()}/port-forward/${port.containerPort}/${this.forwardPort}`, {});
this.isPortForwarded = false;
} catch(error) { } catch(error) {
Notifications.error(error); Notifications.error(error);
} finally { } finally {
@ -67,14 +109,32 @@ export class PodContainerPort extends React.Component<Props> {
const { name, containerPort, protocol } = port; const { name, containerPort, protocol } = port;
const text = `${name ? `${name}: ` : ""}${containerPort}/${protocol}`; const text = `${name ? `${name}: ` : ""}${containerPort}/${protocol}`;
if (this.forwardPort == -1) {
this.forwardPort = containerPort;
}
const portForwardAction = async () => {
if (this.isPortForwarded) {
await this.stopPortForward();
}else {
await this.portForward();
}
};
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() }> {text}
{text} {" "}
{this.waiting && ( <text>to</text>
<Spinner /> <Input className={"portInput"}
)} type="number"
</span> min="0"
max="65535"
value= {String(this.forwardPort)}
disabled={this.isPortForwarded}
onChange={(value) => this.forwardPort = Number(value)}
/>
<Button onClick={() => portForwardAction()}> {this.isPortForwarded ? "Stop":"Forward"} </Button>
</div> </div>
); );
} }