From 323a4c141ea4f390c6e144c452b1d735b4f90362 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 20 Jul 2020 13:17:39 +0300 Subject: [PATCH] cluster-status -- part 2 Signed-off-by: Roman --- locales/en/messages.po | 37 ++-- locales/fi/messages.po | 37 ++-- locales/ru/messages.po | 37 ++-- src/common/base-store.ts | 4 +- src/common/ipc.ts | 22 +-- src/main/cluster-manager.ts | 85 +++++++--- src/main/cluster.ts | 64 +++---- src/main/context-handler.ts | 32 ++-- src/main/kube-auth-proxy.ts | 8 +- src/main/lens-proxy.ts | 21 +-- src/main/window-manager.ts | 21 +-- src/renderer/_vue/components/ClusterPage.vue | 159 ------------------ src/renderer/components/app.tsx | 45 +++-- .../cluster-manager/cluster-manager.tsx | 34 +++- .../cluster-manager/cluster-status.scss | 16 ++ .../cluster-manager/cluster-status.tsx | 67 +++++--- .../cluster-manager/clusters-menu.tsx | 2 +- src/renderer/index.tsx | 7 +- 18 files changed, 319 insertions(+), 379 deletions(-) delete mode 100644 src/renderer/_vue/components/ClusterPage.vue diff --git a/locales/en/messages.po b/locales/en/messages.po index e879875424..c967c5901a 100644 --- a/locales/en/messages.po +++ b/locales/en/messages.po @@ -29,6 +29,10 @@ msgstr "(as a percentage of request)" msgid "(empty) (Allowing the specific traffic to all pods in this namespace)" msgstr "(empty) (Allowing the specific traffic to all pods in this namespace)" +#: src/renderer/components/+add-cluster/add-cluster.tsx:104 +msgid "(new)" +msgstr "(new)" + #: src/renderer/components/item-object-list/item-list-layout.tsx:224 msgid "<0>Filtered: {itemsCount} / {allItemsCount}" msgstr "<0>Filtered: {itemsCount} / {allItemsCount}" @@ -41,7 +45,7 @@ msgstr "<0>Your browser does not support all Lens features. Please consider msgid "<0>{0} successfully created" msgstr "<0>{0} successfully created" -#: src/renderer/components/+add-cluster/add-cluster.tsx:52 +#: src/renderer/components/+add-cluster/add-cluster.tsx:126 msgid "A HTTP proxy server URL (format: http://
:)" msgstr "A HTTP proxy server URL (format: http://
:)" @@ -67,7 +71,8 @@ msgstr "Account Name" msgid "Active" msgstr "Active" -#: src/renderer/components/+add-cluster/add-cluster.tsx:44 +#: src/renderer/components/+add-cluster/add-cluster.tsx:118 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:97 msgid "Add Cluster" msgstr "Add Cluster" @@ -83,7 +88,7 @@ msgstr "Add RoleBinding" msgid "Add bindings to {name}" msgstr "Add bindings to {name}" -#: src/renderer/components/+add-cluster/add-cluster.tsx:62 +#: src/renderer/components/+add-cluster/add-cluster.tsx:137 msgid "Add cluster" msgstr "Add cluster" @@ -223,7 +228,7 @@ msgstr "Are you sure you want to drain <0>{nodeName}?" msgid "Arguments" msgstr "Arguments" -#: src/renderer/components/cluster-manager/clusters-menu.tsx:74 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:84 msgid "Associate clusters and choose the ones you want to access via quick launch menu by clicking the + button." msgstr "Associate clusters and choose the ones you want to access via quick launch menu by clicking the + button." @@ -634,7 +639,7 @@ msgstr "Currently applied filters:" msgid "Custom Resources" msgstr "Custom Resources" -#: src/renderer/components/+add-cluster/add-cluster.tsx:36 +#: src/renderer/components/+add-cluster/add-cluster.tsx:110 msgid "Custom.." msgstr "Custom.." @@ -698,7 +703,7 @@ msgstr "Description" msgid "Desired number of replicas" msgstr "Desired number of replicas" -#: src/renderer/components/cluster-manager/clusters-menu.tsx:49 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:51 msgid "Disconnect" msgstr "Disconnect" @@ -882,7 +887,7 @@ msgstr "Groups" msgid "HPA" msgstr "HPA" -#: src/renderer/components/+add-cluster/add-cluster.tsx:54 +#: src/renderer/components/+add-cluster/add-cluster.tsx:128 msgid "HTTP Proxy server. Used for communicating with Kubernetes API." msgstr "HTTP Proxy server. Used for communicating with Kubernetes API." @@ -1570,6 +1575,10 @@ msgstr "Persistent Volume Claims" msgid "Persistent Volumes" msgstr "Persistent Volumes" +#: src/renderer/components/+add-cluster/add-cluster.tsx:51 +msgid "Please select kubeconfig" +msgstr "Please select kubeconfig" + #: src/renderer/components/+workloads-pods/pod-menu.tsx:50 msgid "Pod" msgstr "Pod" @@ -1651,7 +1660,7 @@ msgstr "Privileged" msgid "Provisioner" msgstr "Provisioner" -#: src/renderer/components/+add-cluster/add-cluster.tsx:48 +#: src/renderer/components/+add-cluster/add-cluster.tsx:122 msgid "Proxy settings" msgstr "Proxy settings" @@ -1701,6 +1710,10 @@ msgstr "Receive" msgid "Reclaim Policy" msgstr "Reclaim Policy" +#: src/renderer/components/cluster-manager/cluster-status.tsx:52 +msgid "Reconnect" +msgstr "Reconnect" + #: src/renderer/components/+config-autoscalers/hpa-details.tsx:70 #: src/renderer/components/+user-management-roles-bindings/role-binding-details.tsx:75 msgid "Reference" @@ -1728,6 +1741,8 @@ msgid "Releases" msgstr "Releases" #: src/renderer/components/+user-management-roles-bindings/role-binding-details.tsx:60 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:58 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:62 #: src/renderer/components/item-object-list/item-list-layout.tsx:179 #: src/renderer/components/menu/menu-actions.tsx:49 #: src/renderer/components/menu/menu-actions.tsx:85 @@ -2015,7 +2030,7 @@ msgstr "Secrets" msgid "Select a quota.." msgstr "Select a quota.." -#: src/renderer/components/+add-cluster/add-cluster.tsx:45 +#: src/renderer/components/+add-cluster/add-cluster.tsx:119 msgid "Select kubeconfig" msgstr "Select kubeconfig" @@ -2070,7 +2085,7 @@ msgstr "Set" msgid "Set quota" msgstr "Set quota" -#: src/renderer/components/cluster-manager/clusters-menu.tsx:43 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:46 msgid "Settings" msgstr "Settings" @@ -2244,7 +2259,7 @@ msgstr "This field is required" msgid "This field must contain only lowercase latin characters, numbers and dash." msgstr "This field must contain only lowercase latin characters, numbers and dash." -#: src/renderer/components/cluster-manager/clusters-menu.tsx:72 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:82 msgid "This is the quick launch menu." msgstr "This is the quick launch menu." diff --git a/locales/fi/messages.po b/locales/fi/messages.po index cd0bb6feec..cd6b540f90 100644 --- a/locales/fi/messages.po +++ b/locales/fi/messages.po @@ -29,6 +29,10 @@ msgstr "" msgid "(empty) (Allowing the specific traffic to all pods in this namespace)" msgstr "" +#: src/renderer/components/+add-cluster/add-cluster.tsx:104 +msgid "(new)" +msgstr "" + #: src/renderer/components/item-object-list/item-list-layout.tsx:224 msgid "<0>Filtered: {itemsCount} / {allItemsCount}" msgstr "" @@ -41,7 +45,7 @@ msgstr "" msgid "<0>{0} successfully created" msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:52 +#: src/renderer/components/+add-cluster/add-cluster.tsx:126 msgid "A HTTP proxy server URL (format: http://
:)" msgstr "" @@ -67,7 +71,8 @@ msgstr "" msgid "Active" msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:44 +#: src/renderer/components/+add-cluster/add-cluster.tsx:118 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:97 msgid "Add Cluster" msgstr "" @@ -83,7 +88,7 @@ msgstr "" msgid "Add bindings to {name}" msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:62 +#: src/renderer/components/+add-cluster/add-cluster.tsx:137 msgid "Add cluster" msgstr "" @@ -223,7 +228,7 @@ msgstr "" msgid "Arguments" msgstr "" -#: src/renderer/components/cluster-manager/clusters-menu.tsx:74 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:84 msgid "Associate clusters and choose the ones you want to access via quick launch menu by clicking the + button." msgstr "" @@ -630,7 +635,7 @@ msgstr "" msgid "Custom Resources" msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:36 +#: src/renderer/components/+add-cluster/add-cluster.tsx:110 msgid "Custom.." msgstr "" @@ -694,7 +699,7 @@ msgstr "" msgid "Desired number of replicas" msgstr "" -#: src/renderer/components/cluster-manager/clusters-menu.tsx:49 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:51 msgid "Disconnect" msgstr "" @@ -873,7 +878,7 @@ msgstr "" msgid "HPA" msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:54 +#: src/renderer/components/+add-cluster/add-cluster.tsx:128 msgid "HTTP Proxy server. Used for communicating with Kubernetes API." msgstr "" @@ -1553,6 +1558,10 @@ msgstr "" msgid "Persistent Volumes" msgstr "" +#: src/renderer/components/+add-cluster/add-cluster.tsx:51 +msgid "Please select kubeconfig" +msgstr "" + #: src/renderer/components/+workloads-pods/pod-menu.tsx:50 msgid "Pod" msgstr "" @@ -1634,7 +1643,7 @@ msgstr "" msgid "Provisioner" msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:48 +#: src/renderer/components/+add-cluster/add-cluster.tsx:122 msgid "Proxy settings" msgstr "" @@ -1684,6 +1693,10 @@ msgstr "" msgid "Reclaim Policy" msgstr "" +#: src/renderer/components/cluster-manager/cluster-status.tsx:52 +msgid "Reconnect" +msgstr "" + #: src/renderer/components/+config-autoscalers/hpa-details.tsx:70 #: src/renderer/components/+user-management-roles-bindings/role-binding-details.tsx:75 msgid "Reference" @@ -1711,6 +1724,8 @@ msgid "Releases" msgstr "" #: src/renderer/components/+user-management-roles-bindings/role-binding-details.tsx:60 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:58 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:62 #: src/renderer/components/item-object-list/item-list-layout.tsx:179 #: src/renderer/components/menu/menu-actions.tsx:49 #: src/renderer/components/menu/menu-actions.tsx:85 @@ -1998,7 +2013,7 @@ msgstr "" msgid "Select a quota.." msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:45 +#: src/renderer/components/+add-cluster/add-cluster.tsx:119 msgid "Select kubeconfig" msgstr "" @@ -2053,7 +2068,7 @@ msgstr "" msgid "Set quota" msgstr "" -#: src/renderer/components/cluster-manager/clusters-menu.tsx:43 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:46 msgid "Settings" msgstr "" @@ -2227,7 +2242,7 @@ msgstr "" msgid "This field must contain only lowercase latin characters, numbers and dash." msgstr "" -#: src/renderer/components/cluster-manager/clusters-menu.tsx:72 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:82 msgid "This is the quick launch menu." msgstr "" diff --git a/locales/ru/messages.po b/locales/ru/messages.po index 1787dbc5c9..fc4b01d90c 100644 --- a/locales/ru/messages.po +++ b/locales/ru/messages.po @@ -30,6 +30,10 @@ msgstr "" msgid "(empty) (Allowing the specific traffic to all pods in this namespace)" msgstr "(Пусто) (Допускается трафик ко всем подам в данной области имен)" +#: src/renderer/components/+add-cluster/add-cluster.tsx:104 +msgid "(new)" +msgstr "" + #: src/renderer/components/item-object-list/item-list-layout.tsx:224 msgid "<0>Filtered: {itemsCount} / {allItemsCount}" msgstr "<0>Отфильтровано: {itemsCount} / {allItemsCount}" @@ -42,7 +46,7 @@ msgstr "<0>Ваш браузер не поддерживает все возмо msgid "<0>{0} successfully created" msgstr "" -#: src/renderer/components/+add-cluster/add-cluster.tsx:52 +#: src/renderer/components/+add-cluster/add-cluster.tsx:126 msgid "A HTTP proxy server URL (format: http://
:)" msgstr "" @@ -68,7 +72,8 @@ msgstr "Название аккаунта" msgid "Active" msgstr "Активный" -#: src/renderer/components/+add-cluster/add-cluster.tsx:44 +#: src/renderer/components/+add-cluster/add-cluster.tsx:118 +#: src/renderer/components/cluster-manager/clusters-menu.tsx:97 msgid "Add Cluster" msgstr "" @@ -84,7 +89,7 @@ msgstr "Добавить привязку ролей" msgid "Add bindings to {name}" msgstr "Добавить привязки к {name}" -#: src/renderer/components/+add-cluster/add-cluster.tsx:62 +#: src/renderer/components/+add-cluster/add-cluster.tsx:137 msgid "Add cluster" msgstr "" @@ -224,7 +229,7 @@ msgstr "Выполнить команду drain для ноды <0>{nodeName} extends ConfOptions { @@ -118,7 +118,7 @@ export class BaseStore extends Singleton { protected async onModelChange(model: T) { if (ipcMain) { this.save(model); // save config file - sendMessage({ channel: this.syncChannel, args: [model] }); // broadcast to renderer views + broadcastIpc({ channel: this.syncChannel, args: [model] }); // broadcast to renderer views } // send "update-request" to main-process if (ipcRenderer) { diff --git a/src/common/ipc.ts b/src/common/ipc.ts index 704ce9a9ba..e5e8101994 100644 --- a/src/common/ipc.ts +++ b/src/common/ipc.ts @@ -19,11 +19,11 @@ export interface IpcMessageOpts { channel: IpcChannel webContentId?: number; // sends to single webContents view filter?: (webContent: WebContents) => boolean - timeout?: number; // fixme: support + timeout?: number; // fixme: add support args?: A; } -export function sendMessage({ channel, webContentId, filter, args = [] }: IpcMessageOpts) { +export function broadcastIpc({ channel, webContentId, filter, args = [] }: IpcMessageOpts) { const singleView = webContentId ? webContents.fromId(webContentId) : null; let views = singleView ? [singleView] : webContents.getAllWebContents(); if (filter) { @@ -37,16 +37,16 @@ export function sendMessage({ channel, webContentId, filter, args = [] }: IpcMes } // todo: support timeout + merge with sendMessage? -export async function invokeMessage(channel: IpcChannel, ...args: T): Promise { - logger.debug(`[IPC]: invoke channel "${channel}"`, { args }); +export async function invokeIpc(channel: IpcChannel, ...args: any[]): Promise { + logger.info(`[IPC]: invoke channel "${channel}"`, { args }); return ipcRenderer.invoke(channel, ...args); } // todo: make isomorphic api -export function handleMessage(channel: IpcChannel, handler: IpcMessageHandler, options: IpcHandleOpts = {}) { +export function handleIpc(channel: IpcChannel, handler: IpcMessageHandler, options: IpcHandleOpts = {}) { const { timeout = 0 } = options; ipcMain.handle(channel, async (event, ...args: T) => { - logger.debug(`[IPC]: handle "${channel}"`, { args }); + logger.info(`[IPC]: handle "${channel}"`, { args }); return new Promise(async (resolve, reject) => { let timerId; if (timeout) { @@ -57,17 +57,11 @@ export function handleMessage(channel: IpcChannel, handler: Ipc } try { const result = await handler(...args); // todo: maybe exec in separate thread/worker + resolve(result); clearTimeout(timerId); - return result; } catch (err) { - logger.debug(`[IPC]: handling "${channel}" error`, { err }); + reject(err); } }) }) } - -export function handleMessages(messages: Record, options?: IpcHandleOpts) { - Object.entries(messages).forEach(([channel, handler]) => { - handleMessage(channel, handler, options); - }) -} diff --git a/src/main/cluster-manager.ts b/src/main/cluster-manager.ts index 53b8dbc35a..f569012c3a 100644 --- a/src/main/cluster-manager.ts +++ b/src/main/cluster-manager.ts @@ -1,61 +1,92 @@ import type http from "http" -import { autorun } from "mobx"; +import { autorun, reaction } from "mobx"; import { apiKubePrefix } from "../common/vars"; import { ClusterId, clusterStore } from "../common/cluster-store" -import { handleMessage } from "../common/ipc"; -import { tracker } from "../common/tracker"; -import { Cluster, ClusterIpcEvent } from "./cluster" +import { handleIpc } from "../common/ipc"; +import { Cluster, ClusterIpcChannel } from "./cluster" import logger from "./logger"; +import { tracker } from "../common/tracker"; export class ClusterManager { + protected activeClusterId: ClusterId; + constructor(public readonly port: number) { + this.activeClusterId = clusterStore.activeClusterId; + // auto-init clusters autorun(() => { clusterStore.clusters.forEach(cluster => { - if (cluster.initialized) return; - cluster.init(port); - logger.info(`[CLUSTER-MANAGER]: initializing cluster`, cluster.getMeta()); + if (!cluster.initialized) { + logger.info(`[CLUSTER-MANAGER]: initializing cluster`, cluster.getMeta()); + cluster.init(port); + } }); }); + // auto-bind events for active cluster + reaction(() => clusterStore.activeCluster, activeCluster => { + const prevCluster = clusterStore.getById(this.activeClusterId); + if (prevCluster) { + prevCluster.unbindEvents(); + } + if (activeCluster) { + this.activeClusterId = activeCluster.id; + activeCluster.bindEvents(); + activeCluster.refreshStatus(); + } + }, { + fireImmediately: true + }); + // auto-stop removed clusters autorun(() => { const { removedClusters } = clusterStore; - const meta = Array.from(removedClusters.values()).map(cluster => cluster.getMeta()); - logger.info(`[CLUSTER-MANAGER]: removing clusters`, meta); - removedClusters.forEach(cluster => cluster.destroy()); - removedClusters.clear(); + if (removedClusters.size > 0) { + const meta = Array.from(removedClusters.values()).map(cluster => cluster.getMeta()); + logger.info(`[CLUSTER-MANAGER]: removing clusters`, meta); + removedClusters.forEach(cluster => cluster.disconnect()); + removedClusters.clear(); + } }, { delay: 250 }); - // listen for ipc-events that must be handled *only* in main-process (nodeIntegration=true) - handleMessage(ClusterIpcEvent.STOP, this.stopCluster.bind(this)); + // listen for ipc-events that must/can be handled *only* in main-process (nodeIntegration=true) + handleIpc(ClusterIpcChannel.INIT, this.onClusterInit); + handleIpc(ClusterIpcChannel.DISCONNECT, this.onClusterDisconnect); + handleIpc(ClusterIpcChannel.RECONNECT, this.onClusterReconnect); } stop() { clusterStore.clusters.forEach((cluster: Cluster) => { - cluster.stop(); + cluster.disconnect(); }) } + protected onClusterInit = async (id = clusterStore.activeClusterId) => { + const cluster = this.getCluster(id); + if (cluster) { + logger.info(`[CLUSTER-MANAGER]: init cluster`, cluster.getMeta()); + tracker.event("cluster", "activate"); + await cluster.refreshStatus(); + cluster.pushState(); + } + } + + protected onClusterDisconnect = (id: ClusterId) => { + tracker.event("cluster", "stop"); + this.getCluster(id)?.disconnect(); + } + + protected onClusterReconnect = (id: ClusterId) => { + tracker.event("cluster", "reconnect"); + this.getCluster(id)?.reconnect(); + } + protected getCluster(id: ClusterId) { return clusterStore.getById(id); } - protected stopCluster(clusterId: ClusterId) { - tracker.event("cluster", "stop"); - this.getCluster(clusterId)?.destroy(); - } - - // todo - protected reconnectCluster(clusterId: ClusterId) { - tracker.event("cluster", "reconnect"); - logger.info(`[CLUSTER-MANAGER]: reconnect cluster`, { - meta: this.getCluster(clusterId)?.getMeta() - }); - } - getClusterForRequest(req: http.IncomingMessage): Cluster { let cluster: Cluster = null diff --git a/src/main/cluster.ts b/src/main/cluster.ts index c5fa80fb36..88b8e9ac93 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -1,9 +1,9 @@ import type { ClusterId, ClusterModel, ClusterPreferences } from "../common/cluster-store" import type { FeatureStatusMap } from "./feature" import type { WorkspaceId } from "../common/workspace-store"; -import { action, observable, reaction, toJS, when } from "mobx"; +import { action, computed, observable, reaction, toJS, when } from "mobx"; import { apiKubePrefix } from "../common/vars"; -import { sendMessage } from "../common/ipc"; +import { broadcastIpc } from "../common/ipc"; import { ContextHandler } from "./context-handler" import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node" import { Kubectl } from "./kubectl"; @@ -13,8 +13,9 @@ import { getFeatures, installFeature, uninstallFeature, upgradeFeature } from ". import request, { RequestPromiseOptions } from "request-promise-native" import logger from "./logger" -export enum ClusterIpcEvent { - STOP = "cluster:stop", +export enum ClusterIpcChannel { + INIT = "cluster:init", + DISCONNECT = "cluster:disconnect", RECONNECT = "cluster:reconnect", } @@ -43,8 +44,6 @@ export class Cluster implements ClusterModel { public kubeCtl: Kubectl public contextHandler: ContextHandler; protected kubeconfigManager: KubeconfigManager; - - public whenReady = when(() => this.initialized); protected disposers: Function[] = []; @observable initialized = false; @@ -69,6 +68,10 @@ export class Cluster implements ClusterModel { this.updateModel(model); } + @computed get isReady() { + return this.initialized && this.accessible === true; + } + @action updateModel(model: ClusterModel) { Object.assign(this, model); @@ -98,8 +101,7 @@ export class Cluster implements ClusterModel { } } - bindEvents(viewId: number) { - if (!this.initialized) return; + bindEvents() { logger.info(`[CLUSTER]: bind events`, this.getMeta()); const refreshStatusTimer = setInterval(() => this.refreshStatus(), 30000); // every 30s const refreshEventsTimer = setInterval(() => this.refreshEvents(), 3000); // every 3s @@ -107,43 +109,35 @@ export class Cluster implements ClusterModel { this.disposers.push( () => clearInterval(refreshStatusTimer), () => clearInterval(refreshEventsTimer), - - reaction(() => this.getState(), clusterState => { - sendMessage({ - channel: "cluster:state", - webContentId: viewId, - args: [clusterState], - }) - }, { + reaction(() => this.getState(), this.pushState, { fireImmediately: true }) ); } unbindEvents() { - if (!this.initialized) return; logger.info(`[CLUSTER]: unbind events`, this.getMeta()); this.disposers.forEach(dispose => dispose()); this.disposers.length = 0; } - stop() { - this.contextHandler.stopServer(); + // fixme: possibly doesn't work as expected + async reconnect() { + logger.info(`[CLUSTER]: reconnect`, this.getMeta()); + await this.contextHandler.stopServer(); + await this.contextHandler.ensureServer(); } - destroy() { - try { - this.stop(); - this.unbindEvents(); - this.kubeconfigManager.unlink(); - } catch (err) { - logger.error(`[CLUSTER]: destroy() throws: ${err}`, this.getMeta()); - } + disconnect() { + logger.info(`[CLUSTER]: disconnect`, this.getMeta()); + this.contextHandler.stopServer(); + this.unbindEvents(); } @action async refreshStatus() { - await this.whenReady; + await when(() => this.initialized); + logger.info(`[CLUSTER]: refreshing status`, this.getMeta()); const connectionStatus = await this.getConnectionStatus(); this.online = connectionStatus > ClusterStatus.Offline; this.accessible = connectionStatus == ClusterStatus.AccessGranted; @@ -191,9 +185,8 @@ export class Cluster implements ClusterModel { return this.preferences.prometheus?.prefix || "" } - protected k8sRequest(path: string, options: RequestPromiseOptions = {}) { + protected async k8sRequest(path: string, options: RequestPromiseOptions = {}) { const apiUrl = this.kubeProxyUrl + path; - logger.debug(`[CLUSTER]: getting request to: ${apiUrl}`); return request(apiUrl, { json: true, timeout: 10000, @@ -211,7 +204,7 @@ export class Cluster implements ClusterModel { this.failureReason = null return ClusterStatus.AccessGranted; } catch (error) { - logger.error(`Failed to connect cluster "${this.contextName}": ${error.stack}`) + logger.error(`Failed to connect cluster "${this.contextName}": ${error}`) if (error.statusCode) { if (error.statusCode >= 400 && error.statusCode < 500) { this.failureReason = "Invalid credentials"; @@ -347,6 +340,15 @@ export class Cluster implements ClusterModel { }) } + pushState = (clusterState = this.getState()) => { + logger.info(`[CLUSTER]: push-state`, clusterState); + broadcastIpc({ + // webContentId: viewId, // todo: send to cluster-view only + channel: "cluster:state", + args: [clusterState], + }) + } + // get cluster system meta, e.g. use in "logger" getMeta() { return { diff --git a/src/main/context-handler.ts b/src/main/context-handler.ts index 228721887c..32e933d8d4 100644 --- a/src/main/context-handler.ts +++ b/src/main/context-handler.ts @@ -12,7 +12,7 @@ import { KubeAuthProxy } from "./kube-auth-proxy" export class ContextHandler { public proxyPort: number; public clusterUrl: UrlWithStringQuery; - protected proxyServer: KubeAuthProxy + protected kubeAuthProxy: KubeAuthProxy protected apiTarget: httpProxy.ServerOptions protected prometheusProvider: string protected prometheusPath: string @@ -36,7 +36,7 @@ export class ContextHandler { return `${namespace}/services/${service}:${port}` } - public async getPrometheusProvider() { + async getPrometheusProvider() { if (!this.prometheusProvider) { const service = await this.getPrometheusService() logger.info(`using ${service.id} as prometheus provider`) @@ -45,7 +45,7 @@ export class ContextHandler { return prometheusProviders.find(p => p.id === this.prometheusProvider) } - public async getPrometheusService(): Promise { + async getPrometheusService(): Promise { const providers = this.prometheusProvider ? prometheusProviders.filter(provider => provider.id == this.prometheusProvider) : prometheusProviders; const prometheusPromises: Promise[] = providers.map(async (provider: PrometheusProvider): Promise => { const apiClient = this.cluster.getProxyKubeconfig().makeApiClient(CoreV1Api) @@ -61,19 +61,19 @@ export class ContextHandler { } } - public async getPrometheusPath(): Promise { + async getPrometheusPath(): Promise { if (!this.prometheusPath) { this.prometheusPath = await this.resolvePrometheusPath() } return this.prometheusPath; } - public async resolveAuthProxyUrl() { + async resolveAuthProxyUrl() { const proxyPort = await this.ensurePort(); return `http://127.0.0.1:${proxyPort}`; } - public async getApiTarget(isWatchRequest = false): Promise { + async getApiTarget(isWatchRequest = false): Promise { if (this.apiTarget && !isWatchRequest) { return this.apiTarget } @@ -104,26 +104,26 @@ export class ContextHandler { return this.proxyPort } - public async ensureServer() { - if (!this.proxyServer) { + async ensureServer() { + if (!this.kubeAuthProxy) { await this.ensurePort(); const proxyEnv = Object.assign({}, process.env) if (this.cluster.preferences.httpsProxy) { proxyEnv.HTTPS_PROXY = this.cluster.preferences.httpsProxy } - this.proxyServer = new KubeAuthProxy(this.cluster, this.proxyPort, proxyEnv) - await this.proxyServer.run() + this.kubeAuthProxy = new KubeAuthProxy(this.cluster, this.proxyPort, proxyEnv) + await this.kubeAuthProxy.run() } } - public stopServer() { - if (this.proxyServer) { - this.proxyServer.exit() - this.proxyServer = null + stopServer() { + if (this.kubeAuthProxy) { + this.kubeAuthProxy.exit() + this.kubeAuthProxy = null } } - public proxyServerError(): string { - return this.proxyServer?.lastError || "" + get proxyLastError(): string { + return this.kubeAuthProxy?.lastError || "" } } diff --git a/src/main/kube-auth-proxy.ts b/src/main/kube-auth-proxy.ts index d793128c34..21e9bd8a7d 100644 --- a/src/main/kube-auth-proxy.ts +++ b/src/main/kube-auth-proxy.ts @@ -1,6 +1,6 @@ import { ChildProcess, spawn } from "child_process" import { waitUntilUsed } from "tcp-port-used"; -import { sendMessage } from "../common/ipc"; +import { broadcastIpc } from "../common/ipc"; import type { Cluster } from "./cluster" import { bundledKubectl, Kubectl } from "./kubectl" import logger from "./logger" @@ -85,8 +85,8 @@ export class KubeAuthProxy { protected async sendIpcLogMessage(res: KubeAuthProxyResponse) { const channel = `kube-auth:${this.cluster.id}` - logger.debug(`[KUBE-AUTH]: output for ${channel}`, { ...res, meta: this.cluster.getMeta() }); - sendMessage({ + logger.info(`[KUBE-AUTH]: out-channel "${channel}"`, { ...res, meta: this.cluster.getMeta() }); + broadcastIpc({ // webContentId: null, // todo: send a message only to single cluster's window channel: channel, args: [res], @@ -95,7 +95,7 @@ export class KubeAuthProxy { public exit() { if (this.proxyProcess) { - logger.debug(`Stopping local proxy: ${this.cluster.contextName}`) + logger.debug("[KUBE-AUTH]: stopping local proxy", this.cluster.getMeta()) this.proxyProcess.kill() } } diff --git a/src/main/lens-proxy.ts b/src/main/lens-proxy.ts index 8179b9d8d9..9c6690f45c 100644 --- a/src/main/lens-proxy.ts +++ b/src/main/lens-proxy.ts @@ -52,21 +52,17 @@ export class LensProxy { protected createProxy(): httpProxy { const proxy = httpProxy.createProxyServer(); - proxy.on("proxyRes", (proxyRes, req, res) => { + if (req.method !== "GET") { + return; + } if (proxyRes.statusCode === 502) { const cluster = this.clusterManager.getClusterForRequest(req) - if (cluster && cluster.contextHandler.proxyServerError()) { - res.writeHead(proxyRes.statusCode, { - "Content-Type": "text/plain" - }) - res.end(cluster.contextHandler.proxyServerError()) - return + const proxyError = cluster?.contextHandler.proxyLastError; + if (proxyError) { + return res.writeHead(502).end(proxyError); } } - if (req.method !== "GET") { - return - } const reqId = this.getRequestId(req); if (this.retryCounters.has(reqId)) { logger.debug(`Resetting proxy retry cache for url: ${reqId}`); @@ -92,10 +88,7 @@ export class LensProxy { } } } - res.writeHead(500, { - 'Content-Type': 'text/plain' - }) - res.end('Oops, something went wrong.') + res.writeHead(500).end("Oops, something went wrong.") }) return proxy; diff --git a/src/main/window-manager.ts b/src/main/window-manager.ts index 828fd66e8f..a634f4f082 100644 --- a/src/main/window-manager.ts +++ b/src/main/window-manager.ts @@ -1,4 +1,4 @@ -import { autorun, reaction } from "mobx"; +import { autorun, reaction, when } from "mobx"; import { BrowserWindow, shell } from "electron" import windowStateKeeper from "electron-window-state" import type { ClusterId } from "../common/cluster-store"; @@ -9,7 +9,6 @@ import logger from "./logger"; // fixme: remove switching view delay on first load export class WindowManager { - protected activeClusterId: ClusterId; protected activeView: BrowserWindow; protected views = new Map(); protected disposers: CallableFunction[] = []; @@ -36,21 +35,7 @@ export class WindowManager { // Manage reactive state this.disposers.push( // auto-show active cluster window and subscribe for push-events - reaction(() => clusterStore.activeCluster, async activeCluster => { - if (this.activeClusterId) { - const prevCluster = clusterStore.getById(this.activeClusterId); - if (prevCluster) prevCluster.unbindEvents(); - this.activeClusterId = null; - } - if (activeCluster) { - this.activeClusterId = activeCluster.id; - const viewId = await this.activateView(activeCluster.id); - if (viewId) { - await activeCluster.refreshStatus(); - activeCluster.bindEvents(viewId); - } - } - }, { + reaction(() => clusterStore.activeClusterId, clusterId => this.activateView(clusterId), { fireImmediately: true, }), @@ -108,7 +93,7 @@ export class WindowManager { if (activeView !== view) { this.activeView = view; if (!isLoadedBefore) { - await cluster.whenReady; + await when(() => cluster.initialized); await view.loadURL(cluster.webContentUrl); this.hideSplash(); } diff --git a/src/renderer/_vue/components/ClusterPage.vue b/src/renderer/_vue/components/ClusterPage.vue deleted file mode 100644 index 29235b1095..0000000000 --- a/src/renderer/_vue/components/ClusterPage.vue +++ /dev/null @@ -1,159 +0,0 @@ - - - - diff --git a/src/renderer/components/app.tsx b/src/renderer/components/app.tsx index 9ef5a9ad35..8992ea6fe2 100755 --- a/src/renderer/components/app.tsx +++ b/src/renderer/components/app.tsx @@ -1,5 +1,5 @@ import "./app.scss"; -import React, { Fragment } from "react"; +import React from "react"; import { observer } from "mobx-react"; import { i18nStore } from "../i18n"; import { configStore } from "../config.store"; @@ -34,11 +34,10 @@ import { LandingPage, landingRoute, landingURL } from "./+landing-page"; import { clusterStore } from "../../common/cluster-store"; import { ClusterSettings, clusterSettingsRoute } from "./+cluster-settings"; import { Workspaces, workspacesRoute } from "./+workspaces"; +import { ErrorBoundary } from "./error-boundary"; @observer export class App extends React.Component { - static rootElem = document.getElementById('app'); - static async init() { await i18nStore.init(); await configStore.init(); @@ -57,27 +56,25 @@ export class App extends React.Component { render() { return ( - + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + @@ -86,7 +83,7 @@ export class App extends React.Component { - + ) } } diff --git a/src/renderer/components/cluster-manager/cluster-manager.tsx b/src/renderer/components/cluster-manager/cluster-manager.tsx index 6b6ebc6200..50bff19daa 100644 --- a/src/renderer/components/cluster-manager/cluster-manager.tsx +++ b/src/renderer/components/cluster-manager/cluster-manager.tsx @@ -1,16 +1,40 @@ import "./cluster-manager.scss" import React from "react"; +import { observer } from "mobx-react"; +import { computed } from "mobx"; +import { App } from "../app"; +import { ClusterStatus } from "./cluster-status"; import { ClustersMenu } from "./clusters-menu"; import { BottomBar } from "./bottom-bar"; -import { App } from "../app"; +import { cssNames, IClassName } from "../../utils"; +import { invokeIpc } from "../../../common/ipc"; +import { ClusterIpcChannel } from "../../../main/cluster"; +import { clusterStore } from "../../../common/cluster-store"; + +interface Props { + className?: IClassName; + contentClass?: IClassName; +} + +@observer +export class ClusterManager extends React.Component { + @computed get isReady() { + return clusterStore.activeCluster?.isReady + } + + async componentDidMount() { + await invokeIpc(ClusterIpcChannel.INIT) + await App.init(); + } -export class ClusterManager extends React.Component { render() { + const { className, contentClass } = this.props; return ( -
+
-
- +
+ {this.isReady && } + {!this.isReady && }
diff --git a/src/renderer/components/cluster-manager/cluster-status.scss b/src/renderer/components/cluster-manager/cluster-status.scss index ad1fc1d11e..f60b79d8ac 100644 --- a/src/renderer/components/cluster-manager/cluster-status.scss +++ b/src/renderer/components/cluster-manager/cluster-status.scss @@ -1,3 +1,19 @@ .ClusterStatus { + --flex-gap: #{$padding * 2}; + min-width: 350px; + margin: auto; + text-align: center; + + pre { + @include custom-scrollbar; + max-width: 70vw; + max-height: 40vh; + //text-align: left; + } + + .Icon { + --size: 70px; + margin: auto; + } } \ No newline at end of file diff --git a/src/renderer/components/cluster-manager/cluster-status.tsx b/src/renderer/components/cluster-manager/cluster-status.tsx index 6863d855ba..d1d5239f4c 100644 --- a/src/renderer/components/cluster-manager/cluster-status.tsx +++ b/src/renderer/components/cluster-manager/cluster-status.tsx @@ -1,29 +1,32 @@ -import "./cluster-manager.scss" -import type { KubeAuthProxyResponse } from "../../../main/kube-auth-proxy"; -import { Cluster, ClusterIpcEvent } from "../../../main/cluster"; +import "./cluster-status.scss" import React from "react"; +import type { KubeAuthProxyResponse } from "../../../main/kube-auth-proxy"; +import { ClusterIpcChannel } from "../../../main/cluster"; +import { invokeIpc } from "../../../common/ipc"; +import { clusterStore } from "../../../common/cluster-store"; import { ipcRenderer } from "electron"; -import { computed, observable } from "mobx"; +import { observable } from "mobx"; import { observer } from "mobx-react"; import { Icon } from "../icon"; import { Button } from "../button"; -import { Trans } from "@lingui/macro"; - -interface Props { - cluster: Cluster; -} +import { cssNames } from "../../utils"; @observer -export class ClusterStatus extends React.Component { - @observable authProxyOutput = "Connecting ...\n" +export class ClusterStatus extends React.Component { + @observable authOutput: string[] = []; - @computed get clusterId() { - return this.props.cluster.id; + get cluster() { + return clusterStore.activeCluster; + } + + get clusterId() { + return clusterStore.activeClusterId; } componentDidMount() { - ipcRenderer.on(`kube-auth:${this.clusterId}`, (evt, authResponse: KubeAuthProxyResponse) => { - this.authProxyOutput += authResponse.data; + this.authOutput = ["Connecting ...\n"]; + ipcRenderer.on(`kube-auth:${this.clusterId}`, (evt, { data, stream }: KubeAuthProxyResponse) => { + this.authOutput.push(`[${stream}]: ${data}`); }) } @@ -31,22 +34,32 @@ export class ClusterStatus extends React.Component { ipcRenderer.removeAllListeners(`kube-auth:${this.clusterId}`); } - reconnectCluster = () => { - ipcRenderer.send(ClusterIpcEvent.RECONNECT, this.clusterId); + reconnect = () => { + this.authOutput = ["Reconnecting ...\n"]; + invokeIpc(ClusterIpcChannel.RECONNECT, this.clusterId); } render() { - const { authProxyOutput } = this; - const { contextName, online } = this.props.cluster; + const { authOutput, cluster } = this; + const isError = cluster?.accessible === false; return ( -
- -

{contextName}

-
{authProxyOutput}
-
) } diff --git a/src/renderer/components/cluster-manager/clusters-menu.tsx b/src/renderer/components/cluster-manager/clusters-menu.tsx index c3c66b697b..132f39e5cd 100644 --- a/src/renderer/components/cluster-manager/clusters-menu.tsx +++ b/src/renderer/components/cluster-manager/clusters-menu.tsx @@ -51,7 +51,7 @@ export class ClustersMenu extends React.Component { label: _i18n._(t`Settings`), click: () => navigate(clusterSettingsURL()) })); - if (cluster.initialized) { + if (cluster.online) { menu.append(new MenuItem({ label: _i18n._(t`Disconnect`), click: () => { diff --git a/src/renderer/index.tsx b/src/renderer/index.tsx index 86c24632d1..c68cc004ac 100644 --- a/src/renderer/index.tsx +++ b/src/renderer/index.tsx @@ -10,7 +10,6 @@ import { I18nProvider } from "@lingui/react"; import { browserHistory } from "./navigation"; import { isMac } from "../common/vars"; import { _i18n } from "./i18n"; -import { App } from "./components/app"; import { ClusterManager } from "./components/cluster-manager"; import { ErrorBoundary } from "./components/error-boundary"; import { WhatsNew, whatsNewRoute } from "./components/+whats-new"; @@ -19,14 +18,14 @@ import { Preferences, preferencesRoute } from "./components/+preferences"; @observer class LensApp extends React.Component { static async init() { - App.rootElem.classList.toggle("is-mac", isMac); await Promise.all([ userStore.load(), workspaceStore.load(), clusterStore.load(), ]); - await App.init(); - render(, App.rootElem); + const elem = document.getElementById("app"); + elem.classList.toggle("is-mac", isMac); + render(, elem); } render() {