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:
parent
6df56b5471
commit
33d7036939
@ -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<ClusterStoreModel> {
|
||||
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<ClusterStoreModel> {
|
||||
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);
|
||||
}
|
||||
|
||||
@ -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<string, IpcMessageHandler>, options?: IpcOptions) {
|
||||
export function handleMessages(messages: Record<string, IpcMessageHandler>, options?: IpcMessageOptions) {
|
||||
Object.entries(messages).forEach(([channel, handler]) => {
|
||||
onMessage(channel, handler, options);
|
||||
handleMessage(channel, handler, options);
|
||||
})
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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<ServerOptions> {
|
||||
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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -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");
|
||||
})
|
||||
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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<void> {
|
||||
@ -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() {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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<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();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@ -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<boolean> {
|
||||
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 }) {
|
||||
|
||||
@ -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"]
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ class MetricsRoute extends LensApi {
|
||||
const { response, cluster } = request
|
||||
const query: IMetricsQuery = request.payload;
|
||||
const headers: Record<string, string> = {
|
||||
"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, {})
|
||||
|
||||
@ -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");
|
||||
}
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user