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

refactoring

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman 2020-07-10 19:39:47 +03:00
parent 6df56b5471
commit 33d7036939
16 changed files with 182 additions and 160 deletions

View File

@ -15,7 +15,6 @@ export interface ClusterModel {
id: ClusterId; id: ClusterId;
contextName: string; contextName: string;
kubeConfigPath: string; kubeConfigPath: string;
port?: number;
kubeConfig?: string; kubeConfig?: string;
workspace?: string; workspace?: string;
preferences?: ClusterPreferences; preferences?: ClusterPreferences;
@ -56,6 +55,10 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
return Array.from(this.clusters.values()); return Array.from(this.clusters.values());
} }
@computed get inactiveClusters() {
return Array.from(this.clusters.values()).filter(cluster => !cluster.initialized);
}
getById(id: ClusterId): Cluster { getById(id: ClusterId): Cluster {
return this.clusters.get(id); return this.clusters.get(id);
} }
@ -97,8 +100,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
clusters.forEach(clusterModel => { clusters.forEach(clusterModel => {
let cluster = currentClusters.get(clusterModel.id); let cluster = currentClusters.get(clusterModel.id);
if (cluster) { if (cluster) {
Object.assign(cluster, clusterModel); cluster.updateModel(clusterModel);
cluster.mergeModel(clusterModel);
} else { } else {
cluster = new Cluster(clusterModel); cluster = new Cluster(clusterModel);
} }

View File

@ -2,10 +2,12 @@
// https://www.electronjs.org/docs/api/ipc-main // https://www.electronjs.org/docs/api/ipc-main
// https://www.electronjs.org/docs/api/ipc-renderer // https://www.electronjs.org/docs/api/ipc-renderer
import { ipcMain, ipcRenderer } from "electron" import { ipcMain, ipcRenderer, webContents } from "electron"
import logger from "../main/logger"; import logger from "../main/logger";
export interface IpcOptions { export type IpcChannel = string;
export interface IpcMessageOptions {
timeout?: number; timeout?: number;
} }
@ -13,12 +15,16 @@ export interface IpcMessageHandler {
(...args: any[]): any; (...args: any[]): any;
} }
export async function invokeMessage(channel: string, ...args: any[]) { export function sendMessageToRenderer(channel: IpcChannel, ...args: any[]) {
webContents.getFocusedWebContents().send(channel, ...args);
}
export async function invokeMessage(channel: IpcChannel, ...args: any[]) {
logger.debug(`[IPC]: invoke channel "${channel}"`, { args }); logger.debug(`[IPC]: invoke channel "${channel}"`, { args });
return ipcRenderer.invoke(channel, ...args); return ipcRenderer.invoke(channel, ...args);
} }
export function onMessage(channel: string, handler: IpcMessageHandler, options: IpcOptions = {}) { export function handleMessage(channel: IpcChannel, handler: IpcMessageHandler, options: IpcMessageOptions = {}) {
const { timeout = 0 } = options; const { timeout = 0 } = options;
ipcMain.handle(channel, async (event, ...args: any[]) => { ipcMain.handle(channel, async (event, ...args: any[]) => {
logger.debug(`[IPC]: handle "${channel}"`, { event, args }); logger.debug(`[IPC]: handle "${channel}"`, { event, args });
@ -41,8 +47,8 @@ export function onMessage(channel: string, handler: IpcMessageHandler, options:
}) })
} }
export function onMessages(messages: Record<string, IpcMessageHandler>, options?: IpcOptions) { export function handleMessages(messages: Record<string, IpcMessageHandler>, options?: IpcMessageOptions) {
Object.entries(messages).forEach(([channel, handler]) => { Object.entries(messages).forEach(([channel, handler]) => {
onMessage(channel, handler, options); handleMessage(channel, handler, options);
}) })
} }

View File

@ -6,6 +6,7 @@ export enum ClusterIpcMessage {
CLUSTER_REMOVE = "cluster-remove", CLUSTER_REMOVE = "cluster-remove",
CLUSTER_REMOVE_WORKSPACE = "cluster-remove-all-from-workspace", CLUSTER_REMOVE_WORKSPACE = "cluster-remove-all-from-workspace",
CLUSTER_EVENTS = "cluster-events-count", CLUSTER_EVENTS = "cluster-events-count",
CLUSTER_REFRESH = "cluster-refresh",
FEATURE_INSTALL = "cluster-feature-install", FEATURE_INSTALL = "cluster-feature-install",
FEATURE_UPGRADE = "cluster-feature-upgrade", FEATURE_UPGRADE = "cluster-feature-upgrade",
FEATURE_REMOVE = "cluster-feature-remove", FEATURE_REMOVE = "cluster-feature-remove",

View File

@ -1,12 +1,12 @@
import { app } from "electron" import { app } from "electron"
import { reaction } from "mobx"; import { autorun } from "mobx";
import path from "path" import path from "path"
import http from "http" import http from "http"
import { copyFile, ensureDir } from "fs-extra" import { copyFile, ensureDir } from "fs-extra"
import filenamify from "filenamify" import filenamify from "filenamify"
import { apiPrefix, appProto } from "../common/vars"; import { apiPrefix, appProto } from "../common/vars";
import { ClusterId, ClusterModel, clusterStore } from "../common/cluster-store" import { ClusterId, ClusterModel, clusterStore } from "../common/cluster-store"
import { onMessages } from "../common/ipc-helpers"; import { handleMessages } from "../common/ipc-helpers";
import { ClusterIpcMessage } from "../common/ipc-messages"; import { ClusterIpcMessage } from "../common/ipc-messages";
import { tracker } from "../common/tracker"; import { tracker } from "../common/tracker";
import { validateConfig } from "./k8s"; import { validateConfig } from "./k8s";
@ -25,20 +25,19 @@ export class ClusterManager {
return path.join(app.getPath("userData"), "icons"); return path.join(app.getPath("userData"), "icons");
} }
constructor(protected port: number) { constructor(public readonly proxyPort: number) {
// init clusters // auto-init fresh clusters
reaction(() => clusterStore.clusters.toJS(), clusters => { autorun(() => {
clusters.forEach(cluster => { clusterStore.inactiveClusters.forEach(cluster => {
if (!cluster.initialized) { cluster.init().then(() => cluster.refreshCluster());
cluster.init(this.port).then(() => cluster.refreshCluster()); });
}
})
}); });
// destroy clusters // auto-stop removed clusters
reaction(() => clusterStore.removedClusters.toJS(), removedClusters => { autorun(() => {
const removedClusters = clusterStore.removedClusters;
if (removedClusters.size > 0) { if (removedClusters.size > 0) {
removedClusters.forEach(cluster => cluster.stopServer()); removedClusters.forEach(cluster => cluster.stop());
clusterStore.removedClusters.clear(); removedClusters.clear();
} }
}); });
// listen ipc-events // listen ipc-events
@ -47,7 +46,7 @@ export class ClusterManager {
stop() { stop() {
clusterStore.clusters.forEach((cluster: Cluster) => { clusterStore.clusters.forEach((cluster: Cluster) => {
cluster.stopServer(); cluster.stop();
}) })
} }
@ -59,10 +58,7 @@ export class ClusterManager {
tracker.event("cluster", "add"); tracker.event("cluster", "add");
try { try {
await validateConfig(clusterModel.kubeConfigPath); await validateConfig(clusterModel.kubeConfigPath);
return clusterStore.addCluster({ return clusterStore.addCluster(clusterModel);
...clusterModel,
port: this.port,
});
} catch (error) { } catch (error) {
logger.error(`[CLUSTER-MANAGER]: add cluster error ${JSON.stringify(error)}`) logger.error(`[CLUSTER-MANAGER]: add cluster error ${JSON.stringify(error)}`)
throw error; throw error;
@ -71,7 +67,7 @@ export class ClusterManager {
protected stopCluster(clusterId: ClusterId) { protected stopCluster(clusterId: ClusterId) {
tracker.event("cluster", "stop"); tracker.event("cluster", "stop");
this.getCluster(clusterId)?.stopServer(); this.getCluster(clusterId)?.stop();
} }
protected removeAllByWorkspace(workspaceId: string) { protected removeAllByWorkspace(workspaceId: string) {
@ -86,7 +82,7 @@ export class ClusterManager {
tracker.event("cluster", "remove"); tracker.event("cluster", "remove");
const cluster = this.getCluster(clusterId); const cluster = this.getCluster(clusterId);
if (cluster) { if (cluster) {
cluster.stopServer() cluster.stop()
clusterStore.removeById(cluster.id); clusterStore.removeById(cluster.id);
return cluster; return cluster;
} }
@ -155,6 +151,10 @@ export class ClusterManager {
return await this.getCluster(clusterId)?.getEventCount() || 0; return await this.getCluster(clusterId)?.getEventCount() || 0;
} }
protected async refreshCluster(clusterId: ClusterId) {
await this.getCluster(clusterId)?.refreshCluster();
}
static ipcListen(clusterManager: ClusterManager) { static ipcListen(clusterManager: ClusterManager) {
const handlers = { const handlers = {
[ClusterIpcMessage.CLUSTER_ADD]: clusterManager.addCluster, [ClusterIpcMessage.CLUSTER_ADD]: clusterManager.addCluster,
@ -162,6 +162,7 @@ export class ClusterManager {
[ClusterIpcMessage.CLUSTER_REMOVE]: clusterManager.removeCluster, [ClusterIpcMessage.CLUSTER_REMOVE]: clusterManager.removeCluster,
[ClusterIpcMessage.CLUSTER_REMOVE_WORKSPACE]: clusterManager.removeAllByWorkspace, [ClusterIpcMessage.CLUSTER_REMOVE_WORKSPACE]: clusterManager.removeAllByWorkspace,
[ClusterIpcMessage.CLUSTER_EVENTS]: clusterManager.getEventsCount, [ClusterIpcMessage.CLUSTER_EVENTS]: clusterManager.getEventsCount,
[ClusterIpcMessage.CLUSTER_REFRESH]: clusterManager.refreshCluster,
[ClusterIpcMessage.FEATURE_INSTALL]: clusterManager.installFeature, [ClusterIpcMessage.FEATURE_INSTALL]: clusterManager.installFeature,
[ClusterIpcMessage.FEATURE_UPGRADE]: clusterManager.upgradeFeature, [ClusterIpcMessage.FEATURE_UPGRADE]: clusterManager.upgradeFeature,
[ClusterIpcMessage.FEATURE_REMOVE]: clusterManager.uninstallFeature, [ClusterIpcMessage.FEATURE_REMOVE]: clusterManager.uninstallFeature,
@ -171,8 +172,8 @@ export class ClusterManager {
Object.entries(handlers).forEach(([key, handler]) => { Object.entries(handlers).forEach(([key, handler]) => {
handlers[key as keyof typeof handlers] = handler.bind(clusterManager); handlers[key as keyof typeof handlers] = handler.bind(clusterManager);
}) })
onMessages(handlers, { handleMessages(handlers, {
timeout: 2000, timeout: 2000
}) })
} }
} }

View File

@ -1,8 +1,7 @@
import url, { UrlWithStringQuery } from "url" import url, { UrlWithStringQuery } from "url"
import type { ClusterId, ClusterModel, ClusterPreferences } from "../common/cluster-store" import type { ClusterId, ClusterModel, ClusterPreferences } from "../common/cluster-store"
import type { FeatureStatusMap } from "./feature" import type { FeatureStatusMap } from "./feature"
import { computed, observable, toJS } from "mobx"; import { action, observable, toJS } from "mobx";
import { apiPrefix } from "../common/vars";
import { ContextHandler } from "./context-handler" import { ContextHandler } from "./context-handler"
import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node" import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"
import { Kubectl } from "./kubectl"; import { Kubectl } from "./kubectl";
@ -20,7 +19,6 @@ enum ClusterStatus {
export interface ClusterState extends ClusterModel { export interface ClusterState extends ClusterModel {
url: string; url: string;
apiUrl: string;
online?: boolean; online?: boolean;
accessible?: boolean; accessible?: boolean;
failureReason?: string; failureReason?: string;
@ -40,10 +38,14 @@ export class Cluster implements ClusterModel {
@observable initialized = false; @observable initialized = false;
@observable id: ClusterId; @observable id: ClusterId;
@observable workspace: string; @observable workspace: string;
@observable kubeConfig?: string;
@observable kubeConfigPath: string; @observable kubeConfigPath: string;
@observable contextName: string; @observable contextName: string;
@observable url: string;
@observable port: number; @observable port: number;
@observable url: string;
@observable apiUrl: UrlWithStringQuery; // same as url, but parsed
@observable kubeAuthProxyUrl: string;
@observable webContentUrl: string;
@observable online: boolean; @observable online: boolean;
@observable accessible: boolean; @observable accessible: boolean;
@observable failureReason: string; @observable failureReason: string;
@ -56,43 +58,50 @@ export class Cluster implements ClusterModel {
@observable features: FeatureStatusMap = {}; @observable features: FeatureStatusMap = {};
constructor(model: ClusterModel) { constructor(model: ClusterModel) {
this.mergeModel(model); this.updateModel(model);
} }
mergeModel(model: ClusterModel) { updateModel(model: ClusterModel) {
Object.assign(this, model) Object.assign(this, model);
} }
// todo: use only api proxy url? @action
@computed get apiUrl(): UrlWithStringQuery { async init() {
return url.parse(`http://${this.id}.localhost:${this.port}`);
}
@computed get apiProxyUrl(): string {
return `http://127.0.0.1:${this.port}${apiPrefix.KUBE_BASE}`;
}
async init(port: number) {
try { try {
this.port = port;
this.contextHandler = new ContextHandler(this); this.contextHandler = new ContextHandler(this);
const proxyPort = await this.contextHandler.resolveProxyPort(); this.contextName = this.contextHandler.contextName;
this.kubeconfigManager = new KubeconfigManager(this, proxyPort); this.port = await this.contextHandler.resolveProxyPort(); // resolve port before KubeconfigManager
this.url = this.contextHandler.url; this.webContentUrl = `http://${this.id}.localhost:${this.port}`;
// this.apiUrl = kubeConfig.getCurrentCluster().server; this.kubeAuthProxyUrl = `http://127.0.0.1:${this.port}`;
this.kubeconfigManager = new KubeconfigManager(this);
this.url = this.kubeconfigManager.getCurrentClusterServer();
this.apiUrl = url.parse(this.url);
logger.info(`[CLUSTER]: INIT`, {
id: this.id,
port: this.port,
url: this.url,
webContentUrl: this.webContentUrl,
kubeAuthProxyUrl: this.kubeAuthProxyUrl,
});
this.initialized = true; this.initialized = true;
logger.debug(`[CLUSTER]: init done (id="${this.id}", context="${this.contextName}")`);
} catch (err) { } catch (err) {
logger.error(`[CLUSTER]: init failed (id="${this.id}")`, { logger.error(`[CLUSTER]: INIT FAILED`, {
contextName: this.contextName, id: this.id,
error: err error: err.stack,
}); });
} }
} }
// todo: auto-refresh when preferences changed? stop() {
if (!this.initialized) return;
this.contextHandler.stopServer();
this.kubeconfigManager.unlink();
}
// todo: auto-refresh when preferences changed + by timer?
@action
async refreshCluster() { async refreshCluster() {
this.contextHandler.setClusterPreferences(this.preferences) this.contextHandler.setClusterPreferences(this.preferences);
const connectionStatus = await this.getConnectionStatus() const connectionStatus = await this.getConnectionStatus()
this.accessible = connectionStatus == ClusterStatus.AccessGranted; this.accessible = connectionStatus == ClusterStatus.AccessGranted;
@ -143,12 +152,12 @@ export class Cluster implements ClusterModel {
} }
k8sRequest(path: string, options: RequestPromiseOptions = {}) { k8sRequest(path: string, options: RequestPromiseOptions = {}) {
return request(this.apiProxyUrl + path, { return request(this.kubeAuthProxyUrl + path, {
json: true, json: true,
timeout: 10000, timeout: 10000,
headers: { headers: {
...(options.headers || {}), ...(options.headers || {}),
host: this.apiUrl.host, host: `${this.id}.localhost:${this.port}`,
} }
}) })
} }
@ -160,7 +169,7 @@ export class Cluster implements ClusterModel {
this.failureReason = null this.failureReason = null
return ClusterStatus.AccessGranted; return ClusterStatus.AccessGranted;
} catch (error) { } catch (error) {
logger.error(`Failed to connect to cluster ${this.contextName}: ${JSON.stringify(error)}`) logger.error(`Failed to connect cluster "${this.contextName}": ${error.stack}`)
if (error.statusCode) { if (error.statusCode) {
if (error.statusCode >= 400 && error.statusCode < 500) { if (error.statusCode >= 400 && error.statusCode < 500) {
this.failureReason = "Invalid credentials"; this.failureReason = "Invalid credentials";
@ -207,13 +216,12 @@ export class Cluster implements ClusterModel {
} }
protected detectKubernetesDistribution(kubernetesVersion: string): string { protected detectKubernetesDistribution(kubernetesVersion: string): string {
const { apiUrl, contextName } = this
if (kubernetesVersion.includes("gke")) return "gke" if (kubernetesVersion.includes("gke")) return "gke"
if (kubernetesVersion.includes("eks")) return "eks" if (kubernetesVersion.includes("eks")) return "eks"
if (kubernetesVersion.includes("IKS")) return "iks" if (kubernetesVersion.includes("IKS")) return "iks"
if (apiUrl.href.endsWith("azmk8s.io")) return "aks" if (this.url.endsWith("azmk8s.io")) return "aks"
if (apiUrl.href.endsWith("k8s.ondigitalocean.com")) return "digitalocean" if (this.url.endsWith("k8s.ondigitalocean.com")) return "digitalocean"
if (contextName.startsWith("minikube")) return "minikube" if (this.contextName.startsWith("minikube")) return "minikube"
if (kubernetesVersion.includes("+")) return "custom" if (kubernetesVersion.includes("+")) return "custom"
return "vanilla" return "vanilla"
} }
@ -281,7 +289,6 @@ export class Cluster implements ClusterModel {
return toJS({ return toJS({
...storeModel, ...storeModel,
url: this.url, url: this.url,
apiUrl: this.apiUrl.href,
online: this.online, online: this.online,
accessible: this.accessible, accessible: this.accessible,
failureReason: this.failureReason, failureReason: this.failureReason,

View File

@ -23,20 +23,17 @@ export class ContextHandler {
protected clientKey: string protected clientKey: string
protected prometheusProvider: string protected prometheusProvider: string
protected prometheusPath: string protected prometheusPath: string
protected clusterName: string
constructor(protected cluster: Cluster) { constructor(protected cluster: Cluster) {
this.id = cluster.id this.id = cluster.id
this.url = cluster.apiUrl.href; this.url = cluster.url;
this.contextName = cluster.contextName; this.contextName = cluster.contextName || cluster.preferences.clusterName;
this.setClusterPreferences(cluster.preferences) this.setClusterPreferences(cluster.preferences)
} }
public setClusterPreferences(preferences: ClusterPreferences = {}) { public setClusterPreferences(preferences: ClusterPreferences = {}) {
this.clusterName = preferences.clusterName || this.contextName;
this.prometheusProvider = preferences.prometheusProvider?.type; this.prometheusProvider = preferences.prometheusProvider?.type;
this.prometheusPath = null; this.prometheusPath = null;
if (preferences.prometheus) { if (preferences.prometheus) {
const { namespace, service, port } = preferences.prometheus const { namespace, service, port } = preferences.prometheus
this.prometheusPath = `${namespace}/services/${service}:${port}` this.prometheusPath = `${namespace}/services/${service}:${port}`
@ -93,18 +90,17 @@ export class ContextHandler {
} }
protected async newApiTarget(timeout: number): Promise<ServerOptions> { protected async newApiTarget(timeout: number): Promise<ServerOptions> {
const clusterUrl = this.cluster.apiUrl;
return { return {
changeOrigin: true, changeOrigin: true,
timeout: timeout, timeout: timeout,
headers: { headers: {
"Host": clusterUrl.hostname "Host": this.cluster.apiUrl.hostname
}, },
target: { target: {
port: await this.resolveProxyPort(), port: await this.resolveProxyPort(),
protocol: "http://", protocol: "http://",
host: "localhost", host: "localhost",
path: clusterUrl.path, path: this.cluster.apiUrl.path,
}, },
} }
} }

View File

@ -6,7 +6,7 @@ import { app, dialog } from "electron"
import { appName, appProto, isMac, staticDir, staticProto } from "../common/vars"; import { appName, appProto, isMac, staticDir, staticProto } from "../common/vars";
import path from "path" import path from "path"
import initMenu from "./menu" import initMenu from "./menu"
import { LensProxy, listen } from "./lens-proxy" import { LensProxy } from "./lens-proxy"
import { WindowManager } from "./window-manager"; import { WindowManager } from "./window-manager";
import { ClusterManager } from "./cluster-manager"; import { ClusterManager } from "./cluster-manager";
import AppUpdater from "./app-updater" import AppUpdater from "./app-updater"
@ -46,9 +46,9 @@ async function main() {
registerFileProtocol(staticProto, staticDir); registerFileProtocol(staticProto, staticDir);
// find free port // find free port
let port: number let proxyPort: number
try { try {
port = await getFreePort() proxyPort = await getFreePort()
} catch (error) { } catch (error) {
logger.error(error) logger.error(error)
await dialog.showErrorBox("Lens Error", "Could not find a free port for the cluster proxy") await dialog.showErrorBox("Lens Error", "Could not find a free port for the cluster proxy")
@ -63,20 +63,20 @@ async function main() {
]); ]);
// create cluster manager // create cluster manager
clusterManager = new ClusterManager(port) clusterManager = new ClusterManager(proxyPort);
// run proxy // run proxy
try { try {
proxyServer = listen(port, clusterManager) proxyServer = LensProxy.create(clusterManager);
} catch (error) { } catch (error) {
logger.error(`Could not start proxy (127.0.0:${port}): ${error.message}`) logger.error(`Could not start proxy (127.0.0:${proxyPort}): ${error.message}`)
await dialog.showErrorBox("Lens Error", `Could not start proxy (127.0.0:${port}): ${error.message || "unknown error"}`) await dialog.showErrorBox("Lens Error", `Could not start proxy (127.0.0:${proxyPort}): ${error.message || "unknown error"}`)
app.quit(); app.quit();
} }
// create window manager and open app // create window manager and open app
windowManager = new WindowManager(); windowManager = new WindowManager();
windowManager.showSplash(); // windowManager.showSplash();
} }
// Events // Events
@ -88,12 +88,14 @@ app.on('window-all-closed', function () {
if (!isMac) { if (!isMac) {
app.quit(); app.quit();
} else { } else {
// todo: handle
// windowManager.destroy(); // windowManager.destroy();
// clusterManager.stop() // clusterManager.stop()
} }
}) })
app.on("activate", () => { app.on("activate", () => {
// todo: handle
logger.debug("app:activate"); logger.debug("app:activate");
}) })

View File

@ -1,4 +1,5 @@
import { KubeConfig, V1Node, V1Pod } from "@kubernetes/client-node" import { KubeConfig, V1Node, V1Pod } from "@kubernetes/client-node"
import path from "path"
import os from "os" import os from "os"
import yaml from "js-yaml" import yaml from "js-yaml"
import logger from "./logger"; import logger from "./logger";
@ -10,12 +11,12 @@ function resolveTilde(filePath: string) {
return filePath; return filePath;
} }
export function loadConfig(kubeConfigPath?: string): KubeConfig { export function loadKubeConfig(pathOrContent?: string): KubeConfig {
const kc = new KubeConfig() const kc = new KubeConfig();
if (kubeConfigPath) { if (path.isAbsolute(pathOrContent)) {
kc.loadFromFile(resolveTilde(kubeConfigPath)) kc.loadFromFile(resolveTilde(pathOrContent));
} else { } else {
kc.loadFromDefault(); kc.loadFromString(pathOrContent);
} }
return kc return kc
} }
@ -29,9 +30,8 @@ export function loadConfig(kubeConfigPath?: string): KubeConfig {
*/ */
export function validateConfig(config: KubeConfig | string): KubeConfig { export function validateConfig(config: KubeConfig | string): KubeConfig {
if (typeof config == "string") { if (typeof config == "string") {
config = loadConfig(config); config = loadKubeConfig(config);
} }
logger.debug(`validating kube config: ${JSON.stringify(config)}`) logger.debug(`validating kube config: ${JSON.stringify(config)}`)
if (!config.users || config.users.length == 0) { if (!config.users || config.users.length == 0) {
throw new Error("No users provided in config") throw new Error("No users provided in config")

View File

@ -1,10 +1,9 @@
import { spawn, ChildProcess } from "child_process" import { ChildProcess, spawn } from "child_process"
import { waitUntilUsed } from "tcp-port-used";
import { sendMessageToRenderer } from "../common/ipc-helpers";
import type { Cluster } from "./cluster"
import { bundledKubectl, Kubectl } from "./kubectl"
import logger from "./logger" import logger from "./logger"
import * as tcpPortUsed from "tcp-port-used"
import { Kubectl, bundledKubectl } from "./kubectl"
import { Cluster } from "./cluster"
import { PromiseIpc } from "electron-promise-ipc"
import { findMainWebContents } from "./webcontents"
export class KubeAuthProxy { export class KubeAuthProxy {
public lastError: string public lastError: string
@ -14,14 +13,12 @@ export class KubeAuthProxy {
protected proxyProcess: ChildProcess protected proxyProcess: ChildProcess
protected port: number protected port: number
protected kubectl: Kubectl protected kubectl: Kubectl
protected promiseIpc: any
constructor(cluster: Cluster, port: number, env: NodeJS.ProcessEnv) { constructor(cluster: Cluster, port: number, env: NodeJS.ProcessEnv) {
this.env = env this.env = env
this.port = port this.port = port
this.cluster = cluster this.cluster = cluster
this.kubectl = bundledKubectl this.kubectl = bundledKubectl
this.promiseIpc = new PromiseIpc({ timeout: 2000 })
} }
public async run(): Promise<void> { public async run(): Promise<void> {
@ -46,7 +43,7 @@ export class KubeAuthProxy {
}) })
this.proxyProcess.on("exit", (code) => { this.proxyProcess.on("exit", (code) => {
logger.error(`proxy ${this.cluster.contextName} exited with code ${code}`) logger.error(`proxy ${this.cluster.contextName} exited with code ${code}`)
this.sendIpcLogMessage( `proxy exited with code ${code}`, "stderr").catch((err: Error) => { this.sendIpcLogMessage(`proxy exited with code ${code}`, "stderr").catch((err: Error) => {
logger.debug("failed to send IPC log message: " + err.message) logger.debug("failed to send IPC log message: " + err.message)
}) })
this.proxyProcess = null this.proxyProcess = null
@ -65,7 +62,7 @@ export class KubeAuthProxy {
this.sendIpcLogMessage(data.toString(), "stderr") this.sendIpcLogMessage(data.toString(), "stderr")
}) })
return tcpPortUsed.waitUntilUsed(this.port, 500, 10000) return waitUntilUsed(this.port, 500, 10000)
} }
protected parseError(data: string) { protected parseError(data: string) {
@ -84,7 +81,10 @@ export class KubeAuthProxy {
} }
protected async sendIpcLogMessage(data: string, stream: string) { protected async sendIpcLogMessage(data: string, stream: string) {
await this.promiseIpc.send(`kube-auth:${this.cluster.id}`, findMainWebContents(), { data, stream }) const channel = `kube-auth:${this.cluster.id}`
const message = { data, stream };
logger.debug(channel, message);
sendMessageToRenderer(channel, message);
} }
public exit() { public exit() {

View File

@ -3,19 +3,30 @@ import { app } from "electron"
import fs from "fs" import fs from "fs"
import { KubeConfig } from "@kubernetes/client-node" import { KubeConfig } from "@kubernetes/client-node"
import { ensureDir, randomFileName } from "./file-helpers" import { ensureDir, randomFileName } from "./file-helpers"
import { dumpConfigYaml } from "./k8s" import { dumpConfigYaml, loadKubeConfig } from "./k8s"
import logger from "./logger" import logger from "./logger"
export class KubeconfigManager { export class KubeconfigManager {
public config: KubeConfig;
protected configDir = app.getPath("temp") protected configDir = app.getPath("temp")
protected tempFile: string protected tempFile: string
constructor(protected cluster: Cluster, protected proxyPort: number) { constructor(protected cluster: Cluster) {
this.tempFile = this.createTemporaryKubeconfig() this.tempFile = this.createTemporaryKubeconfig();
} }
public getPath() { getPath() {
return this.tempFile return this.tempFile;
}
getCurrentClusterServer() {
return this.config.getCurrentCluster().server;
}
protected loadConfig() {
const { kubeConfigPath, kubeConfig } = this.cluster;
this.config = loadKubeConfig(kubeConfigPath || kubeConfig);
return this.config;
} }
/** /**
@ -24,14 +35,13 @@ export class KubeconfigManager {
*/ */
protected createTemporaryKubeconfig(): string { protected createTemporaryKubeconfig(): string {
ensureDir(this.configDir); ensureDir(this.configDir);
const path = `${this.configDir}/${randomFileName("kubeconfig")}` const path = `${this.configDir}/${randomFileName("kubeconfig")}`;
const { contextName, kubeConfigPath } = this.cluster; const { contextName, kubeAuthProxyUrl } = this.cluster;
const kubeConfig = new KubeConfig() const kubeConfig = this.loadConfig();
kubeConfig.loadFromFile(kubeConfigPath)
kubeConfig.clusters = [ kubeConfig.clusters = [
{ {
name: contextName, name: contextName,
server: `http://127.0.0.1:${this.proxyPort}`, server: kubeAuthProxyUrl,
skipTLSVerify: true, skipTLSVerify: true,
} }
]; ];
@ -41,17 +51,17 @@ export class KubeconfigManager {
kubeConfig.currentContext = contextName; kubeConfig.currentContext = contextName;
kubeConfig.contexts = [ kubeConfig.contexts = [
{ {
user: "proxy",
name: contextName, name: contextName,
cluster: contextName, cluster: contextName,
namespace: kubeConfig.getContextObject(contextName).namespace,
user: "proxy"
} }
]; ];
fs.writeFileSync(path, dumpConfigYaml(kubeConfig)); fs.writeFileSync(path, dumpConfigYaml(kubeConfig));
return path logger.info(`Created temp kube-config file at "${path}"`);
return path;
} }
public unlink() { unlink() {
logger.debug('Deleting temporary kubeconfig: ' + this.tempFile) logger.debug('Deleting temporary kubeconfig: ' + this.tempFile)
fs.unlinkSync(this.tempFile) fs.unlinkSync(this.tempFile)
} }

View File

@ -11,23 +11,31 @@ import { apiPrefix } from "../common/vars";
import logger from "./logger" import logger from "./logger"
export class LensProxy { export class LensProxy {
protected clusterManager: ClusterManager
protected proxyServer: http.Server protected proxyServer: http.Server
protected router: Router protected router: Router
protected closed = false protected closed = false
protected retryCounters = new Map<string, number>() protected retryCounters = new Map<string, number>()
constructor(public port: number, protected clusterManager: ClusterManager) { static create(clusterManager: ClusterManager) {
return new LensProxy(clusterManager).listen();
}
private constructor(clusterManager: ClusterManager) {
this.clusterManager = clusterManager;
this.router = new Router(); this.router = new Router();
} }
public run() { listen(): this {
const proxyServer = this.buildProxyServer(); const proxyServer = this.buildProxyServer();
proxyServer.listen(this.port, "127.0.0.1") const { proxyPort } = this.clusterManager;
this.proxyServer = proxyServer this.proxyServer = proxyServer.listen(proxyPort, "127.0.0.1");
logger.info(`Lens proxy server started at ${proxyPort}`);
return this;
} }
public close() { close() {
logger.info(`Closing proxy server at port ${this.port}`); logger.info("Closing proxy server");
this.proxyServer.close() this.proxyServer.close()
this.closed = true this.closed = true
} }
@ -41,7 +49,7 @@ export class LensProxy {
this.handleWsUpgrade(req, socket, head) this.handleWsUpgrade(req, socket, head)
}); });
proxyServer.on("error", (err) => { proxyServer.on("error", (err) => {
logger.error(err) logger.error("proxy error", err)
}); });
return proxyServer; return proxyServer;
} }
@ -138,7 +146,7 @@ export class LensProxy {
if (proxyTarget) { if (proxyTarget) {
proxy.web(req, res, proxyTarget) proxy.web(req, res, proxyTarget)
} else { } else {
this.router.route(cluster, req, res) this.router.route(cluster, req, res); // todo: handle not-found route when isBoom==true?
} }
} }
@ -149,9 +157,3 @@ export class LensProxy {
}); });
} }
} }
export function listen(port: number, clusterManager: ClusterManager) {
const proxyServer = new LensProxy(port, clusterManager)
proxyServer.run();
return proxyServer;
}

View File

@ -46,19 +46,18 @@ export class Router {
this.addRoutes() this.addRoutes()
} }
public async route(cluster: Cluster, req: http.IncomingMessage, res: http.ServerResponse) { public async route(cluster: Cluster, req: http.IncomingMessage, res: http.ServerResponse): Promise<boolean> {
const reqUrl = new URL(req.url, "http://localhost") const reqUrl = new URL(req.url, "http://localhost");
const path = reqUrl.pathname const path = reqUrl.pathname
const method = req.method.toLowerCase() const method = req.method.toLowerCase()
const matchingRoute = this.router.route(method, path) const matchingRoute = this.router.route(method, path);
const routeExists = !matchingRoute.isBoom;
if (matchingRoute.isBoom !== true) { // route() returns error if route not found -> object.isBoom === true if (routeExists) {
const request = await this.getRequest({ req, res, cluster, url: reqUrl, params: matchingRoute.params }) const request = await this.getRequest({ req, res, cluster, url: reqUrl, params: matchingRoute.params })
await matchingRoute.route(request) await matchingRoute.route(request)
return true return true
} else {
return false
} }
return false;
} }
protected async getRequest(opts: { req: http.IncomingMessage; res: http.ServerResponse; cluster: Cluster; url: URL; params: RouteParams }) { protected async getRequest(opts: { req: http.IncomingMessage; res: http.ServerResponse; cluster: Cluster; url: URL; params: RouteParams }) {

View File

@ -12,7 +12,7 @@ function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster
{ {
'name': cluster.contextName, 'name': cluster.contextName,
'cluster': { 'cluster': {
'server': cluster.apiUrl.href, 'server': cluster.url,
'certificate-authority-data': secret.data["ca.crt"] 'certificate-authority-data': secret.data["ca.crt"]
} }
} }

View File

@ -13,7 +13,7 @@ class MetricsRoute extends LensApi {
const { response, cluster } = request const { response, cluster } = request
const query: IMetricsQuery = request.payload; const query: IMetricsQuery = request.payload;
const headers: Record<string, string> = { const headers: Record<string, string> = {
"Host": cluster.apiUrl.host, "Host": `${cluster.id}.localhost:${cluster.port}`,
"Content-type": "application/json", "Content-type": "application/json",
} }
const queryParams: IMetricsQuery = {} const queryParams: IMetricsQuery = {}
@ -25,7 +25,7 @@ class MetricsRoute extends LensApi {
let prometheusProvider: PrometheusProvider let prometheusProvider: PrometheusProvider
try { try {
const prometheusPath = await cluster.contextHandler.getPrometheusPath() const prometheusPath = await cluster.contextHandler.getPrometheusPath()
metricsUrl = `${cluster.apiProxyUrl}/api/v1/namespaces/${prometheusPath}/proxy${cluster.getPrometheusApiPrefix()}/api/v1/query_range` metricsUrl = `${cluster.kubeAuthProxyUrl}/api/v1/namespaces/${prometheusPath}/proxy${cluster.getPrometheusApiPrefix()}/api/v1/query_range`
prometheusProvider = await cluster.contextHandler.getPrometheusProvider() prometheusProvider = await cluster.contextHandler.getPrometheusProvider()
} catch { } catch {
this.respondJson(response, {}) this.respondJson(response, {})

View File

@ -1,7 +0,0 @@
import { webContents } from "electron"
/**
* Helper to find the correct web contents handle for main window
*/
export function findMainWebContents() {
return webContents.getAllWebContents().find(w => w.getType() === "window");
}

View File

@ -42,30 +42,33 @@ export class WindowManager {
this.splashWindow.hide(); this.splashWindow.hide();
} }
getView(clusterId: ClusterId) {
return this.views.get(clusterId);
}
async activateView(clusterId: ClusterId) { async activateView(clusterId: ClusterId) {
const cluster = clusterStore.getById(clusterId); const cluster = clusterStore.getById(clusterId);
if (!cluster) { if (!cluster) {
throw new Error(`Can't load lens for non-existing cluster="${clusterId}"`); throw new Error(`Can't load lens for non-existing cluster="${clusterId}"`);
} }
const currentView = this.activeView; const activeView = this.activeView;
const view = this.getView(clusterId); const isFresh = !this.getView(clusterId);
if (view !== currentView) { const view = this.initView(clusterId);
this.activeView = view; if (view !== activeView) {
const url = cluster.apiUrl.href; if (isFresh) {
const isLoaded = url === view.webContents.getURL(); await view.loadURL(cluster.webContentUrl);
if (!isLoaded) {
await view.loadURL(url);
} }
if (currentView) { if (activeView) {
view.setBounds(currentView.getBounds()); // refresh position for "invisible swap" view.setBounds(activeView.getBounds()); // refresh position for "invisible swap"
currentView.hide(); activeView.hide();
} }
view.show(); view.show();
this.activeView = view;
} }
} }
protected getView(clusterId: ClusterId) { protected initView(clusterId: ClusterId) {
let view = this.views.get(clusterId); let view = this.getView(clusterId);
if (!view) { if (!view) {
view = new BrowserWindow({ view = new BrowserWindow({
show: false, show: false,