Merge branch 'master' into show-cluster-on-workspace-switch

This commit is contained in:
Alex Andreev committed 2020-11-19 12:48:21 +03:00
commit 7ea7ee7a85
15 files changed
+187 -29

No files matched your search

+9 -10
View File
@@ -58,6 +58,7 @@ describe("kube auth proxy tests", () => {
let port: number
let mockedCP: MockProxy<ChildProcess>
let listeners: Record<string, (...args: any[]) => void>
let proxy: KubeAuthProxy
beforeEach(async () => {
port = await getFreePort()
@@ -85,43 +86,41 @@ describe("kube auth proxy tests", () => {
return mockedCP
})
mockWaitUntilUsed.mockReturnValueOnce(Promise.resolve())
const cluster = new Cluster({ id: "foobar", kubeConfigPath: "fake-path.yml" })
jest.spyOn(cluster, "apiUrl", "get").mockReturnValue("https://fake.k8s.internal")
proxy = new KubeAuthProxy(cluster, port, {})
})
it("should call spawn and broadcast errors", async () => {
const kap = new KubeAuthProxy(new Cluster({ id: "foobar", kubeConfigPath: "fake-path.yml" }), port, {})
await kap.run()
await proxy.run()
listeners["error"]({ message: "foobarbat" })
expect(mockBroadcastIpc).toBeCalledWith({ channel: "kube-auth:foobar", args: [{ data: "foobarbat", error: true }] })
})
it("should call spawn and broadcast exit", async () => {
const kap = new KubeAuthProxy(new Cluster({ id: "foobar", kubeConfigPath: "fake-path.yml" }), port, {})
await kap.run()
await proxy.run()
listeners["exit"](0)
expect(mockBroadcastIpc).toBeCalledWith({ channel: "kube-auth:foobar", args: [{ data: "proxy exited with code: 0", error: false }] })
})
it("should call spawn and broadcast errors from stderr", async () => {
const kap = new KubeAuthProxy(new Cluster({ id: "foobar", kubeConfigPath: "fake-path.yml" }), port, {})
await kap.run()
await proxy.run()
listeners["stderr/data"]("an error")
expect(mockBroadcastIpc).toBeCalledWith({ channel: "kube-auth:foobar", args: [{ data: "an error", error: true }] })
})
it("should call spawn and broadcast stdout serving info", async () => {
const kap = new KubeAuthProxy(new Cluster({ id: "foobar", kubeConfigPath: "fake-path.yml" }), port, {})
await kap.run()
await proxy.run()
listeners["stdout/data"]("Starting to serve on")
expect(mockBroadcastIpc).toBeCalledWith({ channel: "kube-auth:foobar", args: [{ data: "Authentication proxy started\n" }] })
})
it("should call spawn and broadcast stdout other info", async () => {
const kap = new KubeAuthProxy(new Cluster({ id: "foobar", kubeConfigPath: "fake-path.yml" }), port, {})
await kap.run()
await proxy.run()
listeners["stdout/data"]("some info")
expect(mockBroadcastIpc).toBeCalledWith({ channel: "kube-auth:foobar", args: [{ data: "some info" }] })
+3 -1
View File
@@ -6,9 +6,11 @@ import { clusterStore, getClusterIdFromHost } from "../common/cluster-store"
import { Cluster } from "./cluster"
import logger from "./logger";
import { apiKubePrefix } from "../common/vars";
import { Singleton } from "../common/utils";
export class ClusterManager {
export class ClusterManager extends Singleton {
constructor(public readonly port: number) {
super()
// auto-init clusters
autorun(() => {
clusterStore.enabledClustersList.forEach(cluster => {
+4
View File
@@ -86,6 +86,10 @@ export class Cluster implements ClusterModel, ClusterState {
return this.accessible && !this.disconnected;
}
@computed get name() {
return this.preferences.clusterName || this.contextName
}
get version(): string {
return String(this.metadata?.version) || ""
}
+18
View File
@@ -0,0 +1,18 @@
import { app } from "electron";
import { WindowManager } from "./window-manager";
import { appEventBus } from "../common/event-bus";
import { ClusterManager } from "./cluster-manager";
import logger from "./logger";
export function exitApp() {
const windowManager = WindowManager.getInstance<WindowManager>()
const clusterManager = ClusterManager.getInstance<ClusterManager>()
appEventBus.emit({ name: "service", action: "close" })
windowManager.hide();
clusterManager.stop();
logger.info('SERVICE:QUIT');
setTimeout(() => {
app.exit()
}, 1000)
}
+3 -2
View File
@@ -67,7 +67,7 @@ app.on("ready", async () => {
}
// create cluster manager
clusterManager = new ClusterManager(proxyPort);
clusterManager = ClusterManager.getInstance<ClusterManager>(proxyPort);
// run proxy
try {
@@ -82,7 +82,7 @@ app.on("ready", async () => {
extensionLoader.init(await extensionManager.load()); // call after windowManager to see splash earlier
setTimeout(() => {
appEventBus.emit({ name: "app", action: "start" })
appEventBus.emit({ name: "service", action: "start" })
}, 1000)
});
@@ -96,6 +96,7 @@ app.on("activate", (event, hasVisibleWindows) => {
// Quit app on Cmd+Q (MacOS)
app.on("will-quit", (event) => {
logger.info('APP:QUIT');
appEventBus.emit({name: "app", action: "close"})
event.preventDefault(); // prevent app's default shutdown (e.g. required for telemetry, etc.)
clusterManager?.stop(); // close cluster connections
return; // skip exit to make tray work, to quit go to app's global menu or tray's menu
+7 -1
View File
@@ -4,6 +4,7 @@ import { broadcastIpc } from "../common/ipc";
import type { Cluster } from "./cluster"
import { Kubectl } from "./kubectl"
import logger from "./logger"
import * as url from "url"
export interface KubeAuthProxyLog {
data: string;
@@ -26,17 +27,22 @@ export class KubeAuthProxy {
this.kubectl = Kubectl.bundled()
}
get acceptHosts() {
return url.parse(this.cluster.apiUrl).hostname;
}
public async run(): Promise<void> {
if (this.proxyProcess) {
return;
}
const proxyBin = await this.kubectl.getPath()
const args = [
"proxy",
"-p", `${this.port}`,
"--kubeconfig", `${this.cluster.kubeConfigPath}`,
"--context", `${this.cluster.contextName}`,
"--accept-hosts", ".*",
"--accept-hosts", this.acceptHosts,
"--reject-paths", "^[^/]"
]
if (process.env.DEBUG_PROXY === "true") {
+5 -1
View File
@@ -131,7 +131,11 @@ export class LensProxy {
}
}
}
res.writeHead(500).end("Oops, something went wrong.")
try {
res.writeHead(500).end("Oops, something went wrong.")
} catch (e) {
logger.error(`[LENS-PROXY]: Failed to write headers: `, e)
}
})
return proxy;
+18 -6
View File
@@ -9,6 +9,7 @@ import { clusterSettingsURL } from "../renderer/components/+cluster-settings/clu
import { extensionsURL } from "../renderer/components/+extensions/extensions.route";
import { menuRegistry } from "../extensions/registries/menu-registry";
import logger from "./logger";
import { exitApp } from "./exit-app";
export type MenuTopId = "mac" | "file" | "edit" | "view" | "help"
@@ -89,7 +90,7 @@ export function buildMenu(windowManager: WindowManager) {
label: 'Quit',
accelerator: 'Cmd+Q',
click() {
app.exit(); // force quit since might be blocked within app.on("will-quit")
exitApp()
}
}
]
@@ -133,12 +134,23 @@ export function buildMenu(windowManager: WindowManager) {
click() {
navigate(extensionsURL())
}
},
{ type: 'separator' },
{ role: 'quit' }
}
]),
{ type: 'separator' },
{ role: 'close' } // close current window
{
role: 'close',
label: "Close Window"
},
...ignoreOnMac([
{ type: 'separator' },
{
label: 'Exit',
accelerator: 'Alt+F4',
click() {
exitApp()
}
}
])
]
};
@@ -259,7 +271,7 @@ export function buildMenu(windowManager: WindowManager) {
}
menu = menuItem.submenu;
}
const menuPath: string = parentLabels.join(" -> ")
if (!menuItem) {
logger.info(`[MENU:test-menu-item-click] Cannot find menu item ${menuPath}`);
+4 -3
View File
@@ -1,6 +1,6 @@
import path from "path"
import packageInfo from "../../package.json"
import { app, dialog, Menu, NativeImage, nativeTheme, Tray } from "electron"
import { dialog, Menu, NativeImage, nativeTheme, Tray } from "electron"
import { autorun } from "mobx";
import { showAbout } from "./menu";
import { AppUpdater } from "./app-updater";
@@ -11,6 +11,7 @@ import { preferencesURL } from "../renderer/components/+preferences/preferences.
import { clusterViewURL } from "../renderer/components/cluster-manager/cluster-view.route";
import logger from "./logger";
import { isDevelopment } from "../common/vars";
import { exitApp } from "./exit-app";
// note: instance of Tray should be saved somewhere, otherwise it disappears
export let tray: Tray;
@@ -88,7 +89,7 @@ export function createTrayMenu(windowManager: WindowManager): Menu {
label: workspace.name,
toolTip: workspace.description,
submenu: clusters.map(cluster => {
const { id: clusterId, preferences: { clusterName: label }, online, workspace } = cluster;
const { id: clusterId, name: label, online, workspace } = cluster;
return {
label: `${online ? '✓' : '\x20'.repeat(3)/*offset*/}${label}`,
toolTip: clusterId,
@@ -118,7 +119,7 @@ export function createTrayMenu(windowManager: WindowManager): Menu {
{
label: 'Quit App',
click() {
app.exit();
exitApp()
}
}
]);
+6
View File
@@ -85,6 +85,7 @@ export class WindowManager extends Singleton {
await this.mainWindow.loadURL(this.mainUrl);
this.mainWindow.show();
this.splashWindow?.close();
appEventBus.emit({ name: "app", action: "start" })
} catch (err) {
dialog.showErrorBox("ERROR!", err.toString())
}
@@ -156,6 +157,11 @@ export class WindowManager extends Singleton {
this.splashWindow.show();
}
hide() {
if (!this.mainWindow?.isDestroyed()) this.mainWindow.hide();
if (!this.splashWindow.isDestroyed()) this.splashWindow.hide();
}
destroy() {
this.mainWindow.destroy();
this.splashWindow.destroy();
@@ -34,8 +34,8 @@ export class ClusterIcon extends React.Component<Props> {
cluster, showErrors, showTooltip, errorClass, options, interactive, isActive,
children, ...elemProps
} = this.props;
const { isAdmin, eventCount, preferences, id: clusterId } = cluster;
const { clusterName, icon } = preferences;
const { isAdmin, name, eventCount, preferences, id: clusterId } = cluster;
const { icon } = preferences;
const clusterIconId = `cluster-icon-${clusterId}`;
const className = cssNames("ClusterIcon flex inline", this.props.className, {
interactive: interactive !== undefined ? interactive : !!this.props.onClick,
@@ -44,9 +44,9 @@ export class ClusterIcon extends React.Component<Props> {
return (
<div {...elemProps} className={className} id={showTooltip ? clusterIconId : null}>
{showTooltip && (
<Tooltip targetId={clusterIconId}>{clusterName}</Tooltip>
<Tooltip targetId={clusterIconId}>{name}</Tooltip>
)}
{icon && <img src={icon} alt={clusterName}/>}
{icon && <img src={icon} alt={name}/>}
{!icon && <Hashicon value={clusterId} options={options}/>}
{showErrors && isAdmin && eventCount > 0 && (
<Badge
@@ -65,7 +65,7 @@ export class MainLayout extends React.Component<MainLayoutProps> {
return (
<div className={cssNames("MainLayout", className)} style={this.getSidebarSize() as any}>
<header className={cssNames("flex gaps align-center", headerClass)}>
<span className="cluster">{cluster.preferences.clusterName || cluster.contextName}</span>
<span className="cluster">{cluster.name}</span>
</header>
<aside className={cssNames("flex column", { pinned: this.isPinned, accessible: this.isAccessible })}>