fix reconnect, better handling no-clusters state

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman committed 2020-07-24 14:17:14 +03:00
1 parent 0490dc1d12
commit 408f64c184
12 files changed
+145 -138

No files matched your search

+3 -8
View File
@@ -12,9 +12,8 @@ export class ClusterManager {
autorun(() => {
clusterStore.clusters.forEach(cluster => {
if (!cluster.initialized) {
logger.info(`[CLUSTER-MANAGER]: initializing cluster`, cluster.getMeta());
cluster.init(port); // connect to kube-auth-proxy, context handling
cluster.bindEvents(); // send push-updates to renderer
logger.info(`[CLUSTER-MANAGER]: init cluster`, cluster.getMeta());
cluster.init(port);
}
});
});
@@ -25,10 +24,7 @@ export class ClusterManager {
if (removedClusters.length > 0) {
const meta = removedClusters.map(cluster => cluster.getMeta());
logger.info(`[CLUSTER-MANAGER]: removing clusters`, meta);
removedClusters.forEach(cluster => {
cluster.disconnect();
cluster.unbindEvents();
});
removedClusters.forEach(cluster => cluster.disconnect());
clusterStore.removedClusters.clear();
}
}, {
@@ -38,7 +34,6 @@ export class ClusterManager {
// listen for ipc-events that must/can be handled *only* in main-process (nodeIntegration=true)
clusterIpc.activate.handleInMain();
clusterIpc.disconnect.handleInMain();
clusterIpc.reconnect.handleInMain();
}
stop() {
+27 -20
View File
@@ -1,7 +1,7 @@
import type { ClusterId, ClusterModel, ClusterPreferences } from "../common/cluster-store"
import type { FeatureStatusMap } from "./feature"
import type { WorkspaceId } from "../common/workspace-store";
import { action, computed, observable, reaction, toJS } from "mobx";
import { action, observable, reaction, toJS, when } from "mobx";
import { apiKubePrefix } from "../common/vars";
import { broadcastIpc } from "../common/ipc";
import { ContextHandler } from "./context-handler"
@@ -23,6 +23,7 @@ export interface ClusterState extends ClusterModel {
initialized: boolean;
apiUrl: string;
online: boolean;
disconnected: boolean;
accessible: boolean;
failureReason: string;
nodes: number;
@@ -40,7 +41,9 @@ export class Cluster implements ClusterModel {
public kubeCtl: Kubectl
public contextHandler: ContextHandler;
protected kubeconfigManager: KubeconfigManager;
protected disposers: Function[] = [];
protected eventDisposers: Function[] = [];
whenInitialized = when(() => this.initialized);
@observable initialized = false;
@observable contextName: string;
@@ -67,10 +70,6 @@ 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);
@@ -103,47 +102,53 @@ export class Cluster implements ClusterModel {
}
}
bindEvents() {
protected bindEvents() {
logger.info(`[CLUSTER]: bind events`, this.getMeta());
const refreshTimer = setInterval(() => this.online && this.refresh(), 30000); // every 30s
const refreshEventsTimer = setInterval(() => this.online && this.refreshEvents(), 3000); // every 3s
this.disposers.push(
this.eventDisposers.push(
reaction(this.getState, this.pushState),
() => clearInterval(refreshTimer),
() => clearInterval(refreshEventsTimer),
reaction(this.getState, this.pushState, {
fireImmediately: true
})
);
}
unbindEvents() {
protected unbindEvents() {
logger.info(`[CLUSTER]: unbind events`, this.getMeta());
this.disposers.forEach(dispose => dispose());
this.disposers.length = 0;
this.eventDisposers.forEach(dispose => dispose());
this.eventDisposers.length = 0;
}
async activate() {
if (this.disconnected) await this.reconnect();
logger.info(`[CLUSTER]: activate`, this.getMeta());
await this.whenInitialized;
if (!this.eventDisposers.length) {
this.bindEvents();
}
if (this.disconnected) {
await this.reconnect();
}
await this.refresh();
return this.pushState();
}
// todo: check, possibly doesn't work as expected
async reconnect() {
logger.info(`[CLUSTER]: reconnect`, this.getMeta());
this.disconnected = false;
await this.contextHandler.stopServer();
await this.contextHandler.ensureServer();
this.disconnected = false;
}
@action
disconnect() {
logger.info(`[CLUSTER]: disconnect`, this.getMeta());
this.unbindEvents();
this.contextHandler.stopServer();
this.disconnected = true;
this.online = false;
this.accessible = false;
this.contextHandler.stopServer();
this.pushState();
}
@action
@@ -344,13 +349,14 @@ export class Cluster implements ClusterModel {
})
}
// serializable cluster-state (mostly used for push-notifications)
// serializable cluster-state used for sync btw main <-> renderer
getState = (): ClusterState => {
const state: ClusterState = {
...this.toJSON(),
initialized: this.initialized,
apiUrl: this.apiUrl,
online: this.online,
disconnected: this.disconnected,
accessible: this.accessible,
failureReason: this.failureReason,
nodes: this.nodes,
@@ -383,8 +389,9 @@ export class Cluster implements ClusterModel {
id: this.id,
name: this.contextName,
initialized: this.initialized,
accessible: this.accessible,
online: this.online,
accessible: this.accessible,
disconnected: this.disconnected,
}
}
-1
View File
@@ -74,7 +74,6 @@ async function main() {
// create window manager and open app
windowManager = new WindowManager(proxyPort);
windowManager.showSplash();
initMenu(windowManager);
}
+6 -8
View File
@@ -6,8 +6,8 @@ import { bundledKubectl, Kubectl } from "./kubectl"
import logger from "./logger"
export interface KubeAuthProxyResponse {
data: string;
stream: "stderr" | "stdout";
data: string; // stream=stdout
error?: boolean; // stream=stderr
}
export class KubeAuthProxy {
@@ -47,10 +47,7 @@ export class KubeAuthProxy {
env: this.env
})
this.proxyProcess.on("exit", (code) => {
if (code) {
logger.error(`[KUBE-AUTH]: proxying ${this.cluster.contextName} exited with code ${code}`, this.cluster.getMeta());
}
this.sendIpcLogMessage({ data: `proxy exited with code ${code}`, stream: "stderr" })
this.sendIpcLogMessage({ data: `proxy exited with code: ${code}`, error: code > 0 })
this.proxyProcess = null
})
this.proxyProcess.stdout.on('data', (data) => {
@@ -58,11 +55,11 @@ export class KubeAuthProxy {
if (logItem.startsWith("Starting to serve on")) {
logItem = "Authentication proxy started\n"
}
this.sendIpcLogMessage({ data: logItem, stream: "stdout" })
this.sendIpcLogMessage({ data: logItem })
})
this.proxyProcess.stderr.on('data', (data) => {
this.lastError = this.parseError(data.toString())
this.sendIpcLogMessage({ data: data.toString(), stream: "stderr" })
this.sendIpcLogMessage({ data: data.toString(), error: true })
})
return waitUntilUsed(this.port, 500, 10000)
@@ -97,6 +94,7 @@ export class KubeAuthProxy {
if (this.proxyProcess) {
logger.debug("[KUBE-AUTH]: stopping local proxy", this.cluster.getMeta())
this.proxyProcess.kill()
this.proxyProcess = null;
}
}
}
+1 -1
View File
@@ -13,7 +13,7 @@ export function initMenu(windowManager: WindowManager) {
function navigate(url: string) {
const activeClusterId = clusterStore.activeClusterId;
const view = windowManager.getView(activeClusterId);
const view = windowManager.getClusterView(activeClusterId);
if (view) {
broadcastIpc({
channel: "menu:navigate",
+1 -1
View File
@@ -4,7 +4,7 @@ import { Cluster } from "../cluster"
import { CoreV1Api, V1Secret } from "@kubernetes/client-node"
function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster) {
const tokenData = new Buffer(secret.data["token"], "base64")
const tokenData = Buffer.from(secret.data["token"], "base64")
return {
'apiVersion': 'v1',
'kind': 'Config',
+38 -39
View File
@@ -1,4 +1,4 @@
import { autorun, reaction, when } from "mobx";
import { autorun, reaction } from "mobx";
import { BrowserWindow, shell } from "electron"
import windowStateKeeper from "electron-window-state"
import type { ClusterId } from "../common/cluster-store";
@@ -15,25 +15,23 @@ export class WindowManager {
protected disposers: CallableFunction[] = [];
protected windowState: windowStateKeeper.State;
constructor(protected proxyPort: number) {
this.splashWindow = new BrowserWindow({
width: 500,
height: 300,
backgroundColor: "#1e2124",
center: true,
frame: false,
resizable: false,
show: false,
});
constructor(protected proxyPort: number, showSplash = true) {
// Manage main window size and position with state persistence
this.windowState = windowStateKeeper({
defaultHeight: 900,
defaultWidth: 1440,
});
// Show while app not ready
if (showSplash) {
this.showSplash();
}
// Manage reactive state
this.disposers.push(
// show and hide "no-clusters" window when necessary
autorun(this.handleNoClustersView),
// auto-show active cluster window and subscribe for push-events
reaction(() => clusterStore.activeClusterId, this.activateView, {
fireImmediately: true,
@@ -42,33 +40,37 @@ export class WindowManager {
// auto-destroy views for removed clusters
reaction(() => clusterStore.removedClusters.toJS(), removedClusters => {
removedClusters.forEach(cluster => {
this.destroyView(cluster.id);
this.destroyClusterView(cluster.id);
});
}, {
delay: 25, // fix: destroy later and allow to use view's state in next activateView()
}),
// handle no-clusters view
autorun(() => {
if (!clusterStore.hasClusters()) {
this.handleNoClustersView();
}
})
);
}
protected async handleNoClustersView() {
if (!this.noClustersWindow) {
this.noClustersWindow = this.initView(undefined);
await this.noClustersWindow.loadURL(`http://no-clusters.localhost:${this.proxyPort}`);
protected handleNoClustersView = async () => {
this.noClustersWindow = this.initClusterView(null);
await this.noClustersWindow.loadURL(`http://no-clusters.localhost:${this.proxyPort}`).catch(Function);
if (!clusterStore.hasClusters()) {
this.activeView = this.noClustersWindow;
this.noClustersWindow.show();
this.hideSplash();
}
this.activeView = this.noClustersWindow;
this.noClustersWindow.show();
this.hideSplash();
}
async showSplash() {
await this.splashWindow.loadURL("static://splash.html").catch(() => null)
if (!this.splashWindow) {
this.splashWindow = new BrowserWindow({
width: 500,
height: 300,
backgroundColor: "#1e2124",
center: true,
frame: false,
resizable: false,
show: false,
});
}
await this.splashWindow.loadURL("static://splash.html").catch(Function)
this.splashWindow.show();
}
@@ -76,19 +78,17 @@ export class WindowManager {
this.splashWindow.hide();
}
getView(clusterId: ClusterId) {
getClusterView(clusterId: ClusterId): BrowserWindow {
return this.views.get(clusterId);
}
activateView = async (clusterId: ClusterId): Promise<number> => {
const cluster = clusterStore.getById(clusterId);
if (!cluster) {
return;
}
if (!cluster) return;
try {
const prevActiveView = this.activeView;
const isLoadedBefore = !!this.getView(clusterId);
const view = this.initView(clusterId);
const isLoadedBefore = !!this.getClusterView(clusterId);
const view = this.initClusterView(clusterId);
logger.info(`[WINDOW-MANAGER]: activating cluster view`, {
id: view.id,
clusterId: cluster.id,
@@ -97,9 +97,8 @@ export class WindowManager {
});
if (prevActiveView !== view) {
this.activeView = view;
cluster.activate(); // refresh + reconnect when required
if (!isLoadedBefore) {
await when(() => cluster.initialized);
await cluster.whenInitialized; // wait for url
await view.loadURL(cluster.webContentUrl);
this.hideSplash();
}
@@ -119,8 +118,8 @@ export class WindowManager {
}
}
protected initView(clusterId: ClusterId): BrowserWindow {
let view = this.getView(clusterId);
protected initClusterView(clusterId: ClusterId): BrowserWindow {
let view = this.getClusterView(clusterId);
if (!view) {
const { width, height, x, y } = this.windowState;
view = new BrowserWindow({
@@ -146,7 +145,7 @@ export class WindowManager {
return view;
}
protected destroyView(clusterId: ClusterId) {
protected destroyClusterView(clusterId: ClusterId) {
const view = this.views.get(clusterId);
if (view) {
view.destroy();