diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index e7e665b09d..bfaa09c0e1 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store.ts @@ -15,7 +15,6 @@ export interface ClusterModel { id: ClusterId; contextName: string; kubeConfigPath: string; - port?: number; kubeConfig?: string; workspace?: string; preferences?: ClusterPreferences; @@ -56,6 +55,10 @@ export class ClusterStore extends BaseStore { return Array.from(this.clusters.values()); } + @computed get inactiveClusters() { + return Array.from(this.clusters.values()).filter(cluster => !cluster.initialized); + } + getById(id: ClusterId): Cluster { return this.clusters.get(id); } @@ -97,8 +100,7 @@ export class ClusterStore extends BaseStore { clusters.forEach(clusterModel => { let cluster = currentClusters.get(clusterModel.id); if (cluster) { - Object.assign(cluster, clusterModel); - cluster.mergeModel(clusterModel); + cluster.updateModel(clusterModel); } else { cluster = new Cluster(clusterModel); } diff --git a/src/common/ipc-helpers.ts b/src/common/ipc-helpers.ts index cd157e9bc0..259f20c3b5 100644 --- a/src/common/ipc-helpers.ts +++ b/src/common/ipc-helpers.ts @@ -2,10 +2,12 @@ // https://www.electronjs.org/docs/api/ipc-main // https://www.electronjs.org/docs/api/ipc-renderer -import { ipcMain, ipcRenderer } from "electron" +import { ipcMain, ipcRenderer, webContents } from "electron" import logger from "../main/logger"; -export interface IpcOptions { +export type IpcChannel = string; + +export interface IpcMessageOptions { timeout?: number; } @@ -13,12 +15,16 @@ export interface IpcMessageHandler { (...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 }); 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; ipcMain.handle(channel, async (event, ...args: any[]) => { logger.debug(`[IPC]: handle "${channel}"`, { event, args }); @@ -41,8 +47,8 @@ export function onMessage(channel: string, handler: IpcMessageHandler, options: }) } -export function onMessages(messages: Record, options?: IpcOptions) { +export function handleMessages(messages: Record, options?: IpcMessageOptions) { Object.entries(messages).forEach(([channel, handler]) => { - onMessage(channel, handler, options); + handleMessage(channel, handler, options); }) } diff --git a/src/common/ipc-messages.ts b/src/common/ipc-messages.ts index 856a4539bb..cff804b411 100644 --- a/src/common/ipc-messages.ts +++ b/src/common/ipc-messages.ts @@ -6,6 +6,7 @@ export enum ClusterIpcMessage { CLUSTER_REMOVE = "cluster-remove", CLUSTER_REMOVE_WORKSPACE = "cluster-remove-all-from-workspace", CLUSTER_EVENTS = "cluster-events-count", + CLUSTER_REFRESH = "cluster-refresh", FEATURE_INSTALL = "cluster-feature-install", FEATURE_UPGRADE = "cluster-feature-upgrade", FEATURE_REMOVE = "cluster-feature-remove", diff --git a/src/main/cluster-manager.ts b/src/main/cluster-manager.ts index b49715714e..095f022ea6 100644 --- a/src/main/cluster-manager.ts +++ b/src/main/cluster-manager.ts @@ -1,12 +1,12 @@ import { app } from "electron" -import { reaction } from "mobx"; +import { autorun } from "mobx"; import path from "path" import http from "http" import { copyFile, ensureDir } from "fs-extra" import filenamify from "filenamify" import { apiPrefix, appProto } from "../common/vars"; 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 { tracker } from "../common/tracker"; import { validateConfig } from "./k8s"; @@ -25,20 +25,19 @@ export class ClusterManager { return path.join(app.getPath("userData"), "icons"); } - constructor(protected port: number) { - // init clusters - reaction(() => clusterStore.clusters.toJS(), clusters => { - clusters.forEach(cluster => { - if (!cluster.initialized) { - cluster.init(this.port).then(() => cluster.refreshCluster()); - } - }) + constructor(public readonly proxyPort: number) { + // auto-init fresh clusters + autorun(() => { + clusterStore.inactiveClusters.forEach(cluster => { + cluster.init().then(() => cluster.refreshCluster()); + }); }); - // destroy clusters - reaction(() => clusterStore.removedClusters.toJS(), removedClusters => { + // auto-stop removed clusters + autorun(() => { + const removedClusters = clusterStore.removedClusters; if (removedClusters.size > 0) { - removedClusters.forEach(cluster => cluster.stopServer()); - clusterStore.removedClusters.clear(); + removedClusters.forEach(cluster => cluster.stop()); + removedClusters.clear(); } }); // listen ipc-events @@ -47,7 +46,7 @@ export class ClusterManager { stop() { clusterStore.clusters.forEach((cluster: Cluster) => { - cluster.stopServer(); + cluster.stop(); }) } @@ -59,10 +58,7 @@ export class ClusterManager { tracker.event("cluster", "add"); try { await validateConfig(clusterModel.kubeConfigPath); - return clusterStore.addCluster({ - ...clusterModel, - port: this.port, - }); + return clusterStore.addCluster(clusterModel); } catch (error) { logger.error(`[CLUSTER-MANAGER]: add cluster error ${JSON.stringify(error)}`) throw error; @@ -71,7 +67,7 @@ export class ClusterManager { protected stopCluster(clusterId: ClusterId) { tracker.event("cluster", "stop"); - this.getCluster(clusterId)?.stopServer(); + this.getCluster(clusterId)?.stop(); } protected removeAllByWorkspace(workspaceId: string) { @@ -86,7 +82,7 @@ export class ClusterManager { tracker.event("cluster", "remove"); const cluster = this.getCluster(clusterId); if (cluster) { - cluster.stopServer() + cluster.stop() clusterStore.removeById(cluster.id); return cluster; } @@ -155,6 +151,10 @@ export class ClusterManager { return await this.getCluster(clusterId)?.getEventCount() || 0; } + protected async refreshCluster(clusterId: ClusterId) { + await this.getCluster(clusterId)?.refreshCluster(); + } + static ipcListen(clusterManager: ClusterManager) { const handlers = { [ClusterIpcMessage.CLUSTER_ADD]: clusterManager.addCluster, @@ -162,6 +162,7 @@ export class ClusterManager { [ClusterIpcMessage.CLUSTER_REMOVE]: clusterManager.removeCluster, [ClusterIpcMessage.CLUSTER_REMOVE_WORKSPACE]: clusterManager.removeAllByWorkspace, [ClusterIpcMessage.CLUSTER_EVENTS]: clusterManager.getEventsCount, + [ClusterIpcMessage.CLUSTER_REFRESH]: clusterManager.refreshCluster, [ClusterIpcMessage.FEATURE_INSTALL]: clusterManager.installFeature, [ClusterIpcMessage.FEATURE_UPGRADE]: clusterManager.upgradeFeature, [ClusterIpcMessage.FEATURE_REMOVE]: clusterManager.uninstallFeature, @@ -171,8 +172,8 @@ export class ClusterManager { Object.entries(handlers).forEach(([key, handler]) => { handlers[key as keyof typeof handlers] = handler.bind(clusterManager); }) - onMessages(handlers, { - timeout: 2000, + handleMessages(handlers, { + timeout: 2000 }) } } diff --git a/src/main/cluster.ts b/src/main/cluster.ts index 875760c1f7..360d0ce5bd 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -1,8 +1,7 @@ import url, { UrlWithStringQuery } from "url" import type { ClusterId, ClusterModel, ClusterPreferences } from "../common/cluster-store" import type { FeatureStatusMap } from "./feature" -import { computed, observable, toJS } from "mobx"; -import { apiPrefix } from "../common/vars"; +import { action, observable, toJS } from "mobx"; import { ContextHandler } from "./context-handler" import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node" import { Kubectl } from "./kubectl"; @@ -20,7 +19,6 @@ enum ClusterStatus { export interface ClusterState extends ClusterModel { url: string; - apiUrl: string; online?: boolean; accessible?: boolean; failureReason?: string; @@ -40,10 +38,14 @@ export class Cluster implements ClusterModel { @observable initialized = false; @observable id: ClusterId; @observable workspace: string; + @observable kubeConfig?: string; @observable kubeConfigPath: string; @observable contextName: string; - @observable url: string; @observable port: number; + @observable url: string; + @observable apiUrl: UrlWithStringQuery; // same as url, but parsed + @observable kubeAuthProxyUrl: string; + @observable webContentUrl: string; @observable online: boolean; @observable accessible: boolean; @observable failureReason: string; @@ -56,43 +58,50 @@ export class Cluster implements ClusterModel { @observable features: FeatureStatusMap = {}; constructor(model: ClusterModel) { - this.mergeModel(model); + this.updateModel(model); } - mergeModel(model: ClusterModel) { - Object.assign(this, model) + updateModel(model: ClusterModel) { + Object.assign(this, model); } - // todo: use only api proxy url? - @computed get apiUrl(): UrlWithStringQuery { - 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) { + @action + async init() { try { - this.port = port; this.contextHandler = new ContextHandler(this); - const proxyPort = await this.contextHandler.resolveProxyPort(); - this.kubeconfigManager = new KubeconfigManager(this, proxyPort); - this.url = this.contextHandler.url; - // this.apiUrl = kubeConfig.getCurrentCluster().server; + this.contextName = this.contextHandler.contextName; + this.port = await this.contextHandler.resolveProxyPort(); // resolve port before KubeconfigManager + this.webContentUrl = `http://${this.id}.localhost:${this.port}`; + 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; - logger.debug(`[CLUSTER]: init done (id="${this.id}", context="${this.contextName}")`); } catch (err) { - logger.error(`[CLUSTER]: init failed (id="${this.id}")`, { - contextName: this.contextName, - error: err + logger.error(`[CLUSTER]: INIT FAILED`, { + id: this.id, + 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() { - this.contextHandler.setClusterPreferences(this.preferences) + this.contextHandler.setClusterPreferences(this.preferences); const connectionStatus = await this.getConnectionStatus() this.accessible = connectionStatus == ClusterStatus.AccessGranted; @@ -143,12 +152,12 @@ export class Cluster implements ClusterModel { } k8sRequest(path: string, options: RequestPromiseOptions = {}) { - return request(this.apiProxyUrl + path, { + return request(this.kubeAuthProxyUrl + path, { json: true, timeout: 10000, 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 return ClusterStatus.AccessGranted; } 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 >= 400 && error.statusCode < 500) { this.failureReason = "Invalid credentials"; @@ -207,13 +216,12 @@ export class Cluster implements ClusterModel { } protected detectKubernetesDistribution(kubernetesVersion: string): string { - const { apiUrl, contextName } = this if (kubernetesVersion.includes("gke")) return "gke" if (kubernetesVersion.includes("eks")) return "eks" if (kubernetesVersion.includes("IKS")) return "iks" - if (apiUrl.href.endsWith("azmk8s.io")) return "aks" - if (apiUrl.href.endsWith("k8s.ondigitalocean.com")) return "digitalocean" - if (contextName.startsWith("minikube")) return "minikube" + if (this.url.endsWith("azmk8s.io")) return "aks" + if (this.url.endsWith("k8s.ondigitalocean.com")) return "digitalocean" + if (this.contextName.startsWith("minikube")) return "minikube" if (kubernetesVersion.includes("+")) return "custom" return "vanilla" } @@ -281,7 +289,6 @@ export class Cluster implements ClusterModel { return toJS({ ...storeModel, url: this.url, - apiUrl: this.apiUrl.href, online: this.online, accessible: this.accessible, failureReason: this.failureReason, diff --git a/src/main/context-handler.ts b/src/main/context-handler.ts index a8ce88000b..a5d1474e52 100644 --- a/src/main/context-handler.ts +++ b/src/main/context-handler.ts @@ -23,20 +23,17 @@ export class ContextHandler { protected clientKey: string protected prometheusProvider: string protected prometheusPath: string - protected clusterName: string constructor(protected cluster: Cluster) { this.id = cluster.id - this.url = cluster.apiUrl.href; - this.contextName = cluster.contextName; + this.url = cluster.url; + this.contextName = cluster.contextName || cluster.preferences.clusterName; this.setClusterPreferences(cluster.preferences) } public setClusterPreferences(preferences: ClusterPreferences = {}) { - this.clusterName = preferences.clusterName || this.contextName; this.prometheusProvider = preferences.prometheusProvider?.type; this.prometheusPath = null; - if (preferences.prometheus) { const { namespace, service, port } = preferences.prometheus this.prometheusPath = `${namespace}/services/${service}:${port}` @@ -93,18 +90,17 @@ export class ContextHandler { } protected async newApiTarget(timeout: number): Promise { - const clusterUrl = this.cluster.apiUrl; return { changeOrigin: true, timeout: timeout, headers: { - "Host": clusterUrl.hostname + "Host": this.cluster.apiUrl.hostname }, target: { port: await this.resolveProxyPort(), protocol: "http://", host: "localhost", - path: clusterUrl.path, + path: this.cluster.apiUrl.path, }, } } diff --git a/src/main/index.ts b/src/main/index.ts index 2fa21da8aa..8f7a212239 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -6,7 +6,7 @@ import { app, dialog } from "electron" import { appName, appProto, isMac, staticDir, staticProto } from "../common/vars"; import path from "path" import initMenu from "./menu" -import { LensProxy, listen } from "./lens-proxy" +import { LensProxy } from "./lens-proxy" import { WindowManager } from "./window-manager"; import { ClusterManager } from "./cluster-manager"; import AppUpdater from "./app-updater" @@ -46,9 +46,9 @@ async function main() { registerFileProtocol(staticProto, staticDir); // find free port - let port: number + let proxyPort: number try { - port = await getFreePort() + proxyPort = await getFreePort() } catch (error) { logger.error(error) 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 - clusterManager = new ClusterManager(port) + clusterManager = new ClusterManager(proxyPort); // run proxy try { - proxyServer = listen(port, clusterManager) + proxyServer = LensProxy.create(clusterManager); } catch (error) { - logger.error(`Could not start proxy (127.0.0:${port}): ${error.message}`) - await dialog.showErrorBox("Lens Error", `Could not start proxy (127.0.0:${port}): ${error.message || "unknown error"}`) + logger.error(`Could not start proxy (127.0.0:${proxyPort}): ${error.message}`) + await dialog.showErrorBox("Lens Error", `Could not start proxy (127.0.0:${proxyPort}): ${error.message || "unknown error"}`) app.quit(); } // create window manager and open app windowManager = new WindowManager(); - windowManager.showSplash(); + // windowManager.showSplash(); } // Events @@ -88,12 +88,14 @@ app.on('window-all-closed', function () { if (!isMac) { app.quit(); } else { + // todo: handle // windowManager.destroy(); // clusterManager.stop() } }) app.on("activate", () => { + // todo: handle logger.debug("app:activate"); }) diff --git a/src/main/k8s.ts b/src/main/k8s.ts index 590f3a728f..ba65e22bd7 100644 --- a/src/main/k8s.ts +++ b/src/main/k8s.ts @@ -1,4 +1,5 @@ import { KubeConfig, V1Node, V1Pod } from "@kubernetes/client-node" +import path from "path" import os from "os" import yaml from "js-yaml" import logger from "./logger"; @@ -10,12 +11,12 @@ function resolveTilde(filePath: string) { return filePath; } -export function loadConfig(kubeConfigPath?: string): KubeConfig { - const kc = new KubeConfig() - if (kubeConfigPath) { - kc.loadFromFile(resolveTilde(kubeConfigPath)) +export function loadKubeConfig(pathOrContent?: string): KubeConfig { + const kc = new KubeConfig(); + if (path.isAbsolute(pathOrContent)) { + kc.loadFromFile(resolveTilde(pathOrContent)); } else { - kc.loadFromDefault(); + kc.loadFromString(pathOrContent); } return kc } @@ -29,9 +30,8 @@ export function loadConfig(kubeConfigPath?: string): KubeConfig { */ export function validateConfig(config: KubeConfig | string): KubeConfig { if (typeof config == "string") { - config = loadConfig(config); + config = loadKubeConfig(config); } - logger.debug(`validating kube config: ${JSON.stringify(config)}`) if (!config.users || config.users.length == 0) { throw new Error("No users provided in config") diff --git a/src/main/kube-auth-proxy.ts b/src/main/kube-auth-proxy.ts index 743bac88f5..9a16608201 100644 --- a/src/main/kube-auth-proxy.ts +++ b/src/main/kube-auth-proxy.ts @@ -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 * 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 { public lastError: string @@ -14,14 +13,12 @@ export class KubeAuthProxy { protected proxyProcess: ChildProcess protected port: number protected kubectl: Kubectl - protected promiseIpc: any constructor(cluster: Cluster, port: number, env: NodeJS.ProcessEnv) { this.env = env this.port = port this.cluster = cluster this.kubectl = bundledKubectl - this.promiseIpc = new PromiseIpc({ timeout: 2000 }) } public async run(): Promise { @@ -46,7 +43,7 @@ export class KubeAuthProxy { }) this.proxyProcess.on("exit", (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) }) this.proxyProcess = null @@ -65,7 +62,7 @@ export class KubeAuthProxy { this.sendIpcLogMessage(data.toString(), "stderr") }) - return tcpPortUsed.waitUntilUsed(this.port, 500, 10000) + return waitUntilUsed(this.port, 500, 10000) } protected parseError(data: string) { @@ -84,7 +81,10 @@ export class KubeAuthProxy { } 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() { diff --git a/src/main/kubeconfig-manager.ts b/src/main/kubeconfig-manager.ts index fd8430cebe..a0f2a7ec5d 100644 --- a/src/main/kubeconfig-manager.ts +++ b/src/main/kubeconfig-manager.ts @@ -3,19 +3,30 @@ import { app } from "electron" import fs from "fs" import { KubeConfig } from "@kubernetes/client-node" import { ensureDir, randomFileName } from "./file-helpers" -import { dumpConfigYaml } from "./k8s" +import { dumpConfigYaml, loadKubeConfig } from "./k8s" import logger from "./logger" export class KubeconfigManager { + public config: KubeConfig; protected configDir = app.getPath("temp") protected tempFile: string - constructor(protected cluster: Cluster, protected proxyPort: number) { - this.tempFile = this.createTemporaryKubeconfig() + constructor(protected cluster: Cluster) { + this.tempFile = this.createTemporaryKubeconfig(); } - public getPath() { - return this.tempFile + getPath() { + 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 { ensureDir(this.configDir); - const path = `${this.configDir}/${randomFileName("kubeconfig")}` - const { contextName, kubeConfigPath } = this.cluster; - const kubeConfig = new KubeConfig() - kubeConfig.loadFromFile(kubeConfigPath) + const path = `${this.configDir}/${randomFileName("kubeconfig")}`; + const { contextName, kubeAuthProxyUrl } = this.cluster; + const kubeConfig = this.loadConfig(); kubeConfig.clusters = [ { name: contextName, - server: `http://127.0.0.1:${this.proxyPort}`, + server: kubeAuthProxyUrl, skipTLSVerify: true, } ]; @@ -41,17 +51,17 @@ export class KubeconfigManager { kubeConfig.currentContext = contextName; kubeConfig.contexts = [ { + user: "proxy", name: contextName, cluster: contextName, - namespace: kubeConfig.getContextObject(contextName).namespace, - user: "proxy" } ]; 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) fs.unlinkSync(this.tempFile) } diff --git a/src/main/lens-proxy.ts b/src/main/lens-proxy.ts index 6cc9c14bcd..86a0386c2b 100644 --- a/src/main/lens-proxy.ts +++ b/src/main/lens-proxy.ts @@ -11,23 +11,31 @@ import { apiPrefix } from "../common/vars"; import logger from "./logger" export class LensProxy { + protected clusterManager: ClusterManager protected proxyServer: http.Server protected router: Router protected closed = false protected retryCounters = new Map() - 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(); } - public run() { + listen(): this { const proxyServer = this.buildProxyServer(); - proxyServer.listen(this.port, "127.0.0.1") - this.proxyServer = proxyServer + const { proxyPort } = this.clusterManager; + this.proxyServer = proxyServer.listen(proxyPort, "127.0.0.1"); + logger.info(`Lens proxy server started at ${proxyPort}`); + return this; } - public close() { - logger.info(`Closing proxy server at port ${this.port}`); + close() { + logger.info("Closing proxy server"); this.proxyServer.close() this.closed = true } @@ -41,7 +49,7 @@ export class LensProxy { this.handleWsUpgrade(req, socket, head) }); proxyServer.on("error", (err) => { - logger.error(err) + logger.error("proxy error", err) }); return proxyServer; } @@ -138,7 +146,7 @@ export class LensProxy { if (proxyTarget) { proxy.web(req, res, proxyTarget) } 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; -} diff --git a/src/main/router.ts b/src/main/router.ts index ace4a3cead..bed47fce2c 100644 --- a/src/main/router.ts +++ b/src/main/router.ts @@ -46,19 +46,18 @@ export class Router { this.addRoutes() } - public async route(cluster: Cluster, req: http.IncomingMessage, res: http.ServerResponse) { - const reqUrl = new URL(req.url, "http://localhost") + public async route(cluster: Cluster, req: http.IncomingMessage, res: http.ServerResponse): Promise { + const reqUrl = new URL(req.url, "http://localhost"); const path = reqUrl.pathname const method = req.method.toLowerCase() - const matchingRoute = this.router.route(method, path) - - if (matchingRoute.isBoom !== true) { // route() returns error if route not found -> object.isBoom === true + const matchingRoute = this.router.route(method, path); + const routeExists = !matchingRoute.isBoom; + if (routeExists) { const request = await this.getRequest({ req, res, cluster, url: reqUrl, params: matchingRoute.params }) await matchingRoute.route(request) return true - } else { - return false } + return false; } protected async getRequest(opts: { req: http.IncomingMessage; res: http.ServerResponse; cluster: Cluster; url: URL; params: RouteParams }) { diff --git a/src/main/routes/kubeconfig-route.ts b/src/main/routes/kubeconfig-route.ts index 2705c6e319..54255b1cb8 100644 --- a/src/main/routes/kubeconfig-route.ts +++ b/src/main/routes/kubeconfig-route.ts @@ -12,7 +12,7 @@ function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster { 'name': cluster.contextName, 'cluster': { - 'server': cluster.apiUrl.href, + 'server': cluster.url, 'certificate-authority-data': secret.data["ca.crt"] } } diff --git a/src/main/routes/metrics-route.ts b/src/main/routes/metrics-route.ts index 8bda7511d8..2005ef9fc2 100644 --- a/src/main/routes/metrics-route.ts +++ b/src/main/routes/metrics-route.ts @@ -13,7 +13,7 @@ class MetricsRoute extends LensApi { const { response, cluster } = request const query: IMetricsQuery = request.payload; const headers: Record = { - "Host": cluster.apiUrl.host, + "Host": `${cluster.id}.localhost:${cluster.port}`, "Content-type": "application/json", } const queryParams: IMetricsQuery = {} @@ -25,7 +25,7 @@ class MetricsRoute extends LensApi { let prometheusProvider: PrometheusProvider try { 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() } catch { this.respondJson(response, {}) diff --git a/src/main/webcontents.ts b/src/main/webcontents.ts deleted file mode 100644 index ffdb99b1d1..0000000000 --- a/src/main/webcontents.ts +++ /dev/null @@ -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"); -} diff --git a/src/main/window-manager.ts b/src/main/window-manager.ts index b781752751..ef4e3e926a 100644 --- a/src/main/window-manager.ts +++ b/src/main/window-manager.ts @@ -42,30 +42,33 @@ export class WindowManager { this.splashWindow.hide(); } + getView(clusterId: ClusterId) { + return this.views.get(clusterId); + } + async activateView(clusterId: ClusterId) { const cluster = clusterStore.getById(clusterId); if (!cluster) { throw new Error(`Can't load lens for non-existing cluster="${clusterId}"`); } - const currentView = this.activeView; - const view = this.getView(clusterId); - if (view !== currentView) { - this.activeView = view; - const url = cluster.apiUrl.href; - const isLoaded = url === view.webContents.getURL(); - if (!isLoaded) { - await view.loadURL(url); + const activeView = this.activeView; + const isFresh = !this.getView(clusterId); + const view = this.initView(clusterId); + if (view !== activeView) { + if (isFresh) { + await view.loadURL(cluster.webContentUrl); } - if (currentView) { - view.setBounds(currentView.getBounds()); // refresh position for "invisible swap" - currentView.hide(); + if (activeView) { + view.setBounds(activeView.getBounds()); // refresh position for "invisible swap" + activeView.hide(); } view.show(); + this.activeView = view; } } - protected getView(clusterId: ClusterId) { - let view = this.views.get(clusterId); + protected initView(clusterId: ClusterId) { + let view = this.getView(clusterId); if (!view) { view = new BrowserWindow({ show: false,