mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
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>
This commit is contained in:
parent
ef52e4e9e2
commit
ff7084ec81
@ -40,6 +40,7 @@ export * from "./network-policies";
|
||||
export * from "./network";
|
||||
export * from "./nodes";
|
||||
export * from "./pod-disruption-budgets";
|
||||
export * from "./port-forwards";
|
||||
export * from "./preferences";
|
||||
export * from "./releases";
|
||||
export * from "./resource-quotas";
|
||||
|
||||
@ -25,6 +25,7 @@ import { endpointRoute } from "./endpoints";
|
||||
import { ingressRoute } from "./ingresses";
|
||||
import { networkPoliciesRoute } from "./network-policies";
|
||||
import { servicesRoute, servicesURL } from "./services";
|
||||
import { portForwardsRoute } from "./port-forwards";
|
||||
|
||||
export const networkRoute: RouteProps = {
|
||||
path: [
|
||||
@ -32,6 +33,7 @@ export const networkRoute: RouteProps = {
|
||||
endpointRoute,
|
||||
ingressRoute,
|
||||
networkPoliciesRoute,
|
||||
portForwardsRoute,
|
||||
].map(route => route.path.toString())
|
||||
};
|
||||
|
||||
|
||||
33
src/common/routes/port-forwards.ts
Normal file
33
src/common/routes/port-forwards.ts
Normal file
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* 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/:forwardport?"
|
||||
};
|
||||
|
||||
export interface PortForwardsRouteParams {
|
||||
forwardport?: string;
|
||||
}
|
||||
|
||||
export const portForwardsURL = buildURL<PortForwardsRouteParams>(portForwardsRoute.path);
|
||||
@ -182,6 +182,7 @@ export class Router {
|
||||
// Port-forward API
|
||||
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: "get", path: `${apiPrefix}/port-forwards` }, PortForwardRoute.routeAllPortForwards);
|
||||
this.router.add({ method: "delete", path: `${apiPrefix}/pods/{namespace}/{resourceType}/{resourceName}/port-forward/{port}/{forwardPort}` }, PortForwardRoute.routeCurrentPortForwardStop);
|
||||
|
||||
// Helm API
|
||||
|
||||
@ -99,6 +99,8 @@ class PortForward {
|
||||
try {
|
||||
await tcpPortUsed.waitUntilUsed(this.internalPort, 500, 15000);
|
||||
|
||||
this.forwardPort = String(this.internalPort);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.process.kill();
|
||||
@ -134,6 +136,12 @@ export class PortForwardRoute {
|
||||
namespace, port, forwardPort,
|
||||
});
|
||||
|
||||
let thePort: string; // undefined
|
||||
const portNum = Number(forwardPort);
|
||||
if (portNum > 0 && portNum < 65536) {
|
||||
thePort = forwardPort;
|
||||
}
|
||||
|
||||
if (!portForward) {
|
||||
logger.info(`Creating a new port-forward ${namespace}/${resourceType}/${resourceName}:${port}`);
|
||||
portForward = new PortForward(await cluster.getProxyKubeconfigPath(), {
|
||||
@ -142,7 +150,7 @@ export class PortForwardRoute {
|
||||
namespace,
|
||||
name: resourceName,
|
||||
port,
|
||||
forwardPort
|
||||
forwardPort: thePort,
|
||||
});
|
||||
|
||||
const started = await portForward.start();
|
||||
@ -151,7 +159,7 @@ export class PortForwardRoute {
|
||||
logger.error("[PORT-FORWARD-ROUTE]: failed to start a port-forward", { namespace, port, resourceType, resourceName });
|
||||
|
||||
return respondJson(response, {
|
||||
message: `Failed to forward port ${port}`
|
||||
message: `Failed to forward port ${port} to ${thePort ? forwardPort : "random port"}`
|
||||
}, 400);
|
||||
}
|
||||
}
|
||||
@ -179,6 +187,26 @@ export class PortForwardRoute {
|
||||
respondJson(response, {port: portForward?.internalPort?? null});
|
||||
}
|
||||
|
||||
static async routeAllPortForwards(request: LensApiRequest) {
|
||||
const { response } = request;
|
||||
|
||||
const portForwards: PortForwardArgs[] = [];
|
||||
|
||||
PortForward.portForwards.forEach(f => {
|
||||
const pf: PortForwardArgs = {
|
||||
clusterId: f.clusterId,
|
||||
kind: f.kind,
|
||||
namespace: f.namespace,
|
||||
name: f.name,
|
||||
port: f.port,
|
||||
forwardPort: f.forwardPort,
|
||||
};
|
||||
portForwards.push(pf)
|
||||
});
|
||||
|
||||
respondJson(response, {portForwards});
|
||||
}
|
||||
|
||||
static async routeCurrentPortForwardStop(request: LensApiRequest) {
|
||||
const { params, response, cluster} = request;
|
||||
const { namespace, port, forwardPort, resourceType, resourceName } = params;
|
||||
|
||||
23
src/renderer/components/+network-port-forwards/index.ts
Normal file
23
src/renderer/components/+network-port-forwards/index.ts
Normal file
@ -0,0 +1,23 @@
|
||||
/**
|
||||
* 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";
|
||||
export * from "./port-forward-details";
|
||||
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.PortForwardDetails {
|
||||
.SubTitle {
|
||||
text-transform: none
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
/**
|
||||
* 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-details.scss";
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import type { PortForwardItem } from "./port-forward.store";
|
||||
|
||||
interface Props {
|
||||
portForward: PortForwardItem;
|
||||
hideDetails(): void;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class PortForwardDetails extends React.Component<Props> {
|
||||
render() {
|
||||
return (
|
||||
<div className="PortForwardDetails">
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,70 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,165 @@
|
||||
/**
|
||||
* 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 { computed, observable, makeObservable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { Dialog, DialogProps } from "../dialog";
|
||||
import { Wizard, WizardStep } from "../wizard";
|
||||
import { Icon } from "../icon";
|
||||
import { Input } from "../input";
|
||||
import { Notifications } from "../notifications";
|
||||
import { cssNames } from "../../utils";
|
||||
import { PortForwardItem, portForwardStore } from "./port-forward.store";
|
||||
import { isNumber } from "../input/input_validators";
|
||||
|
||||
interface Props extends Partial<DialogProps> {
|
||||
}
|
||||
|
||||
const dialogState = observable.object({
|
||||
isOpen: false,
|
||||
data: null as PortForwardItem,
|
||||
});
|
||||
|
||||
@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: PortForwardItem) {
|
||||
dialogState.isOpen = true;
|
||||
dialogState.data = portForward;
|
||||
}
|
||||
|
||||
static close() {
|
||||
dialogState.isOpen = false;
|
||||
}
|
||||
|
||||
get portForward() {
|
||||
return dialogState.data;
|
||||
}
|
||||
|
||||
close = () => {
|
||||
PortForwardDialog.close();
|
||||
};
|
||||
|
||||
@computed get scaleMax() {
|
||||
const { currentPort } = this;
|
||||
const defaultMax = 50;
|
||||
|
||||
return currentPort <= defaultMax
|
||||
? defaultMax * 2
|
||||
: currentPort * 2;
|
||||
}
|
||||
|
||||
onOpen = async () => {
|
||||
const { portForward } = this;
|
||||
|
||||
this.currentPort = +portForward.forwardPort;
|
||||
this.desiredPort = this.currentPort;
|
||||
this.ready = true;
|
||||
};
|
||||
|
||||
onClose = () => {
|
||||
this.ready = false;
|
||||
};
|
||||
|
||||
changePort = async () => {
|
||||
const { portForward } = this;
|
||||
const { currentPort, desiredPort, close } = this;
|
||||
|
||||
try {
|
||||
if (currentPort !== desiredPort) {
|
||||
await portForwardStore.modify(portForward, desiredPort);
|
||||
}
|
||||
close();
|
||||
} catch (err) {
|
||||
Notifications.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
renderContents() {
|
||||
return (
|
||||
<>
|
||||
<div className="flex gaps align-center">
|
||||
<div className="input-container flex align-center">
|
||||
<div className="current-port" data-testid="current-port">
|
||||
Current port: {this.currentPort}
|
||||
</div>
|
||||
<Input
|
||||
required autoFocus
|
||||
iconLeft="import_export"
|
||||
placeholder="Desired port"
|
||||
trim
|
||||
validators={isNumber}
|
||||
onChange={v => this.desiredPort = +v}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="warning" data-testid="warning">
|
||||
<Icon material="warning"/>
|
||||
Current port-forwarding will be removed and a new one will be started
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className, ...dialogProps } = this.props;
|
||||
const resourceName = this.portForward ? this.portForward.getName() : "";
|
||||
const header = (
|
||||
<h5>
|
||||
Change Port <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.changePort}
|
||||
nextLabel="Change Port"
|
||||
disabledNext={!this.ready}
|
||||
>
|
||||
{this.renderContents()}
|
||||
</WizardStep>
|
||||
</Wizard>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,94 @@
|
||||
/**
|
||||
* 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, openExternal } from "../../utils";
|
||||
import { PortForwardItem, portForwardStore } from "./port-forward.store";
|
||||
import { MenuActions, MenuActionsProps } from "../menu/menu-actions";
|
||||
import { MenuItem } from "../menu";
|
||||
import { Icon } from "../icon";
|
||||
import { Notifications } from "../notifications";
|
||||
import { PortForwardDialog } from "./port-forward-dialog";
|
||||
|
||||
interface Props extends MenuActionsProps {
|
||||
portForward: PortForwardItem;
|
||||
hideDetails?(): void;
|
||||
}
|
||||
|
||||
export class PortForwardMenu extends React.Component<Props> {
|
||||
@boundMethod
|
||||
remove() {
|
||||
return portForwardStore.remove(this.props.portForward);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
openInBrowser() {
|
||||
const { portForward } = this.props;
|
||||
const browseTo = `http://localhost:${portForward.forwardPort}`;
|
||||
|
||||
openExternal(browseTo)
|
||||
.catch(error => {
|
||||
console.error(`failed to open in browser: ${error}`, {
|
||||
clusterId: portForward.clusterId,
|
||||
port: portForward.port,
|
||||
kind: portForward.kind,
|
||||
namespace: portForward.namespace,
|
||||
name: portForward.name,
|
||||
});
|
||||
Notifications.error(`Failed to open ${browseTo} in browser`);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
const { portForward, toolbar } = this.props;
|
||||
|
||||
if (!portForward) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuItem onClick={this.openInBrowser}>
|
||||
<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,242 @@
|
||||
/**
|
||||
* 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 { computed, IReactionDisposer, makeObservable, observable, reaction } from "mobx";
|
||||
import { ItemObject, ItemStore } from "../../../common/item.store";
|
||||
import { autoBind, createStorage, delay } from "../../utils";
|
||||
import { apiBase } from "../../api";
|
||||
import { waitUntilFree } from "tcp-port-used";
|
||||
import { getHostedClusterId } from "../../utils";
|
||||
import { podsApi } from "../../../common/k8s-api/endpoints";
|
||||
|
||||
interface PortForwardResult {
|
||||
port: number;
|
||||
}
|
||||
|
||||
interface ForwardedPort {
|
||||
clusterId: string;
|
||||
kind: string;
|
||||
namespace: string;
|
||||
name: string;
|
||||
port: string;
|
||||
forwardPort: string;
|
||||
}
|
||||
|
||||
interface PortForwardsResult {
|
||||
portForwards: ForwardedPort[];
|
||||
}
|
||||
|
||||
export class PortForwardItem implements ItemObject {
|
||||
clusterId: string;
|
||||
kind: string;
|
||||
namespace: string;
|
||||
name: string;
|
||||
port: string;
|
||||
forwardPort: string;
|
||||
|
||||
interval: NodeJS.Timeout;
|
||||
|
||||
constructor() {
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
getNs() {
|
||||
return this.namespace;
|
||||
}
|
||||
|
||||
get id() {
|
||||
return this.forwardPort;
|
||||
}
|
||||
|
||||
getId() {
|
||||
return 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,
|
||||
];
|
||||
}
|
||||
|
||||
monitor() {
|
||||
// for now only support pods
|
||||
if (this.kind === "pod") {
|
||||
this.interval = setInterval( this.keepAlive, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
async keepAlive() {
|
||||
console.log("keepAlive() called");
|
||||
let pod = await podsApi.get({name: this.getName(), namespace: this.getNs()});
|
||||
if (!pod || pod.metadata.deletionTimestamp) {
|
||||
console.log("keepAlive() pod lost!");
|
||||
clearInterval(this.interval);
|
||||
this.interval = undefined;
|
||||
// wait for resource
|
||||
while (true) {
|
||||
await delay(1000);
|
||||
pod = await podsApi.get({name: this.getName(), namespace: this.getNs()});
|
||||
if (pod) {
|
||||
if (pod.getContainerStatuses()?.[0]?.ready) {
|
||||
console.log("keepAlive() pod found!");
|
||||
// restart port forward
|
||||
await portForwardStore.modify(this, +this.getForwardPort());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class PortForwardStore extends ItemStore<PortForwardItem> {
|
||||
private storage = createStorage<ForwardedPort[] | undefined>("port_forwards", undefined);
|
||||
|
||||
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)) {
|
||||
await Promise.all(savedPortForwards.map(pf => {
|
||||
const port = new PortForwardItem;
|
||||
port.clusterId = pf.clusterId;
|
||||
port.kind = pf.kind;
|
||||
port.namespace = pf.namespace;
|
||||
port.name = pf.name;
|
||||
port.port = pf.port;
|
||||
port.forwardPort = pf.forwardPort;
|
||||
return this.add(port);
|
||||
}));
|
||||
|
||||
this.portForwards = [];
|
||||
}
|
||||
}
|
||||
|
||||
@observable selectedItemId?: string;
|
||||
@observable portForwards: PortForwardItem[];
|
||||
|
||||
@computed get selectedItem() {
|
||||
return this.portForwards.find((e: ItemObject) => e.getId() === this.selectedItemId);
|
||||
}
|
||||
|
||||
watch() {
|
||||
const disposers: IReactionDisposer[] = [
|
||||
reaction(() => this.portForwards, () => this.loadAll()),
|
||||
];
|
||||
|
||||
return () => disposers.forEach((dispose) => dispose());
|
||||
}
|
||||
|
||||
loadAll() {
|
||||
return this.loadItems(async () => {
|
||||
const response = await apiBase.get<PortForwardsResult>(`/port-forwards`, {});
|
||||
|
||||
const portForwards = response.portForwards?.filter(pf => pf.clusterId == getHostedClusterId());
|
||||
this.storage.set(portForwards);
|
||||
|
||||
this.reset();
|
||||
portForwards?.forEach(pf => {
|
||||
const port = new PortForwardItem;
|
||||
port.clusterId = pf.clusterId;
|
||||
port.kind = pf.kind;
|
||||
port.namespace = pf.namespace;
|
||||
port.name = pf.name;
|
||||
port.port = pf.port;
|
||||
port.forwardPort = pf.forwardPort;
|
||||
port.monitor();
|
||||
this.portForwards.push(port);
|
||||
})
|
||||
return this.portForwards;
|
||||
});
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.portForwards?.forEach(pf => {
|
||||
if (pf.interval) clearInterval(pf.interval)
|
||||
});
|
||||
this.portForwards = [];
|
||||
}
|
||||
|
||||
async add(portForward: PortForwardItem) {
|
||||
await add(portForward);
|
||||
this.reset();
|
||||
}
|
||||
|
||||
async modify(portForward: PortForwardItem, desiredPort: number) {
|
||||
await remove(portForward);
|
||||
portForward.forwardPort = desiredPort.toString();
|
||||
await add(portForward);
|
||||
this.reset();
|
||||
}
|
||||
|
||||
async remove(portForward: PortForwardItem) {
|
||||
await remove(portForward);
|
||||
this.reset();
|
||||
}
|
||||
|
||||
async removeSelectedItems() {
|
||||
return Promise.all(this.selectedItems.map(this.remove));
|
||||
}
|
||||
}
|
||||
|
||||
async function add(portForward: PortForwardItem) {
|
||||
const response = await apiBase.post<PortForwardResult>(`/pods/${portForward.getNs()}/${portForward.getKind()}/${portForward.getName()}/port-forward/${portForward.getPort()}/${portForward.getForwardPort()}`, {});
|
||||
if (response?.port != +portForward.getForwardPort() ) {
|
||||
console.log(`specified ${portForward.getForwardPort()} got ${response.port}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(portForward: PortForwardItem) {
|
||||
await apiBase.del(`/pods/${portForward.getNs()}/${portForward.getKind()}/${portForward.getName()}/port-forward/${portForward.getPort()}/${portForward.forwardPort}`, {})
|
||||
await waitUntilFree(+portForward.getForwardPort(), 200, 1000);
|
||||
}
|
||||
|
||||
export const portForwardStore = new PortForwardStore();
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
136
src/renderer/components/+network-port-forwards/port-forwards.tsx
Normal file
136
src/renderer/components/+network-port-forwards/port-forwards.tsx
Normal file
@ -0,0 +1,136 @@
|
||||
/**
|
||||
* 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 type { RouteComponentProps } from "react-router-dom";
|
||||
import { ItemListLayout } from "../item-object-list/item-list-layout";
|
||||
import { PortForwardItem, portForwardStore } from "./port-forward.store";
|
||||
import { PortForwardsRouteParams, portForwardsURL } from "../../../common/routes";
|
||||
import { navigation } from "../../navigation";
|
||||
import { PortForwardDetails } from "./port-forward-details";
|
||||
import { PortForwardMenu } from "./port-forward-menu";
|
||||
|
||||
enum columnId {
|
||||
name = "name",
|
||||
namespace = "namespace",
|
||||
kind = "kind",
|
||||
port = "port",
|
||||
forwardPort = "forwardPort",
|
||||
}
|
||||
|
||||
interface Props extends RouteComponentProps<PortForwardsRouteParams> {
|
||||
}
|
||||
|
||||
@observer
|
||||
export class PortForwards extends React.Component<Props> {
|
||||
|
||||
componentDidMount() {
|
||||
disposeOnUnmount(this, [
|
||||
portForwardStore.watch(),
|
||||
]);
|
||||
}
|
||||
|
||||
get selectedPortForward() {
|
||||
const { match: { params: { forwardport } } } = this.props;
|
||||
|
||||
return portForwardStore.items.find(pf => {
|
||||
return pf.getForwardPort() == forwardport;
|
||||
});
|
||||
}
|
||||
|
||||
showDetails = (item: PortForwardItem) => {
|
||||
navigation.push(portForwardsURL({
|
||||
params: {
|
||||
forwardport: item.getForwardPort(),
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
hideDetails = () => {
|
||||
navigation.push(portForwardsURL());
|
||||
};
|
||||
|
||||
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="Forwarded Ports"
|
||||
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)
|
||||
})}
|
||||
detailsItem={this.selectedPortForward}
|
||||
onDetails={this.showDetails}
|
||||
/>
|
||||
<PortForwardDetails
|
||||
portForward={this.selectedPortForward}
|
||||
hideDetails={this.hideDetails}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -75,6 +75,10 @@ export class ServicePortComponent extends React.Component<Props> {
|
||||
|
||||
this.waiting = true;
|
||||
|
||||
if (!this.forwardPort) {
|
||||
this.forwardPort = 0;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiBase.post<PortForwardResult>(`/pods/${service.getNs()}/service/${service.getName()}/port-forward/${port.port}/${this.forwardPort}`, {});
|
||||
|
||||
@ -117,7 +121,8 @@ export class ServicePortComponent extends React.Component<Props> {
|
||||
<div className={cssNames("ServicePortComponent", { waiting: this.waiting })}>
|
||||
{port.toString()}
|
||||
{" "}
|
||||
<text>to</text>
|
||||
<Button onClick={() => portForwardAction()}> {this.isPortForwarded ? "Stop":"Forward"} </Button>
|
||||
<text> local port:</text>
|
||||
<Input className={"portInput"}
|
||||
type="number"
|
||||
min="0"
|
||||
@ -127,7 +132,6 @@ export class ServicePortComponent extends React.Component<Props> {
|
||||
placeholder={"Random"}
|
||||
onChange={(value) => this.forwardPort = Number(value)}
|
||||
/>
|
||||
<Button onClick={() => portForwardAction()}> {this.isPortForwarded ? "Stop":"Forward"} </Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -28,6 +28,7 @@ import { Services } from "../+network-services";
|
||||
import { Endpoints } from "../+network-endpoints";
|
||||
import { Ingresses } from "../+network-ingresses";
|
||||
import { NetworkPolicies } from "../+network-policies";
|
||||
import { PortForwards } from "../+network-port-forwards";
|
||||
import { isAllowedResource } from "../../../common/utils/allowed-resource";
|
||||
import * as routes from "../../../common/routes";
|
||||
|
||||
@ -72,6 +73,13 @@ export class Network extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
tabs.push({
|
||||
title: "Forwarded Ports",
|
||||
component: PortForwards,
|
||||
url: routes.portForwardsURL(),
|
||||
routePath: routes.portForwardsRoute.path.toString(),
|
||||
});
|
||||
|
||||
return tabs;
|
||||
}
|
||||
|
||||
|
||||
@ -79,6 +79,10 @@ export class PodContainerPort extends React.Component<Props> {
|
||||
|
||||
this.waiting = true;
|
||||
|
||||
if (!this.forwardPort) {
|
||||
this.forwardPort = 0;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiBase.post<PortForwardResult>(`/pods/${pod.getNs()}/pod/${pod.getName()}/port-forward/${port.containerPort}/${this.forwardPort}`, {});
|
||||
|
||||
@ -123,7 +127,8 @@ export class PodContainerPort extends React.Component<Props> {
|
||||
<div className={cssNames("PodContainerPort", { waiting: this.waiting })}>
|
||||
{text}
|
||||
{" "}
|
||||
<text>to</text>
|
||||
<Button onClick={() => portForwardAction()}> {this.isPortForwarded ? "Stop":"Forward"} </Button>
|
||||
<text> local port:</text>
|
||||
<Input className={"portInput"}
|
||||
type="number"
|
||||
min="0"
|
||||
@ -133,7 +138,6 @@ export class PodContainerPort extends React.Component<Props> {
|
||||
placeholder={"Random"}
|
||||
onChange={(value) => this.forwardPort = Number(value)}
|
||||
/>
|
||||
<Button onClick={() => portForwardAction()}> {this.isPortForwarded ? "Stop":"Forward"} </Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -74,6 +74,7 @@ import { ClusterStore } from "../../common/cluster-store";
|
||||
import type { ClusterId } from "../../common/cluster-types";
|
||||
import { watchHistoryState } from "../remote-helpers/history-updater";
|
||||
import { unmountComponentAtNode } from "react-dom";
|
||||
import { PortForwardDialog } from "./+network-port-forwards/port-forward-dialog";
|
||||
|
||||
@observer
|
||||
export class App extends React.Component {
|
||||
@ -231,6 +232,7 @@ export class App extends React.Component {
|
||||
<StatefulSetScaleDialog/>
|
||||
<ReplicaSetScaleDialog/>
|
||||
<CronJobTriggerDialog/>
|
||||
<PortForwardDialog/>
|
||||
<CommandContainer clusterId={App.clusterId}/>
|
||||
</ErrorBoundary>
|
||||
</Router>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user