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

Improve cluster connection visibility (#4266)

This commit is contained in:
Sebastian Malton 2021-11-12 08:49:21 -05:00 committed by GitHub
parent 23d5d40d62
commit 3522485c6f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 229 additions and 207 deletions

View File

@ -26,7 +26,7 @@ import { Cluster } from "../main/cluster";
import migrations from "../migrations/cluster-store"; import migrations from "../migrations/cluster-store";
import logger from "../main/logger"; import logger from "../main/logger";
import { appEventBus } from "./event-bus"; import { appEventBus } from "./event-bus";
import { ipcMainHandle, ipcMainOn, ipcRendererOn, requestMain } from "./ipc"; import { ipcMainHandle, requestMain } from "./ipc";
import { disposer, toJS } from "./utils"; import { disposer, toJS } from "./utils";
import type { ClusterModel, ClusterId, ClusterState } from "./cluster-types"; import type { ClusterModel, ClusterId, ClusterState } from "./cluster-types";
@ -37,8 +37,6 @@ export interface ClusterStoreModel {
const initialStates = "cluster:states"; const initialStates = "cluster:states";
export class ClusterStore extends BaseStore<ClusterStoreModel> { export class ClusterStore extends BaseStore<ClusterStoreModel> {
private static StateChannel = "cluster:state";
clusters = observable.map<ClusterId, Cluster>(); clusters = observable.map<ClusterId, Cluster>();
protected disposer = disposer(); protected disposer = disposer();
@ -83,21 +81,13 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
} }
} }
handleStateChange = (event: any, clusterId: string, state: ClusterState) => {
logger.silly(`[CLUSTER-STORE]: received push-state at ${location.host} (${webFrame.routingId})`, clusterId, state);
this.getById(clusterId)?.setState(state);
};
registerIpcListener() { registerIpcListener() {
logger.info(`[CLUSTER-STORE] start to listen (${webFrame.routingId})`); logger.info(`[CLUSTER-STORE] start to listen (${webFrame.routingId})`);
const ipc = ipcMain ?? ipcRenderer;
if (ipcMain) { ipc?.on("cluster:state", (event, clusterId: ClusterId, state: ClusterState) => {
this.disposer.push(ipcMainOn(ClusterStore.StateChannel, this.handleStateChange)); this.getById(clusterId)?.setState(state);
} });
if (ipcRenderer) {
this.disposer.push(ipcRendererOn(ClusterStore.StateChannel, this.handleStateChange));
}
} }
unregisterIpcListener() { unregisterIpcListener() {

View File

@ -122,6 +122,14 @@ export enum ClusterStatus {
Offline = 0, Offline = 0,
} }
/**
* The message format for the "cluster:<cluster-id>:connection-update" channels
*/
export interface KubeAuthUpdate {
message: string;
isError: boolean;
}
/** /**
* The OpenLens known static metadata keys * The OpenLens known static metadata keys
*/ */
@ -173,7 +181,6 @@ export interface ClusterState {
disconnected: boolean; disconnected: boolean;
accessible: boolean; accessible: boolean;
ready: boolean; ready: boolean;
failureReason: string;
isAdmin: boolean; isAdmin: boolean;
allowedNamespaces: string[] allowedNamespaces: string[]
allowedResources: string[] allowedResources: string[]

View File

@ -55,7 +55,7 @@ import { ChildProcess, spawn } from "child_process";
import { bundledKubectlPath, Kubectl } from "../kubectl"; import { bundledKubectlPath, Kubectl } from "../kubectl";
import { mock, MockProxy } from "jest-mock-extended"; import { mock, MockProxy } from "jest-mock-extended";
import { waitUntilUsed } from "tcp-port-used"; import { waitUntilUsed } from "tcp-port-used";
import type { Readable } from "stream"; import { EventEmitter, Readable } from "stream";
import { UserStore } from "../../common/user-store"; import { UserStore } from "../../common/user-store";
import { Console } from "console"; import { Console } from "console";
import { stdout, stderr } from "process"; import { stdout, stderr } from "process";
@ -134,30 +134,73 @@ describe("kube auth proxy tests", () => {
describe("spawn tests", () => { describe("spawn tests", () => {
let mockedCP: MockProxy<ChildProcess>; let mockedCP: MockProxy<ChildProcess>;
let listeners: Record<string, (...args: any[]) => void>; let listeners: EventEmitter;
let proxy: KubeAuthProxy; let proxy: KubeAuthProxy;
beforeEach(async () => { beforeEach(async () => {
mockedCP = mock<ChildProcess>(); mockedCP = mock<ChildProcess>();
listeners = {}; listeners = new EventEmitter();
jest.spyOn(Kubectl.prototype, "checkBinary").mockReturnValueOnce(Promise.resolve(true)); jest.spyOn(Kubectl.prototype, "checkBinary").mockReturnValueOnce(Promise.resolve(true));
jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValueOnce(Promise.resolve(false)); jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValueOnce(Promise.resolve(false));
mockedCP.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): ChildProcess => { mockedCP.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): ChildProcess => {
listeners[event] = listener; listeners.on(event, listener);
return mockedCP; return mockedCP;
}); });
mockedCP.stderr = mock<Readable>(); mockedCP.stderr = mock<Readable>();
mockedCP.stderr.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): Readable => { mockedCP.stderr.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): Readable => {
listeners[`stderr/${event}`] = listener; listeners.on(`stderr/${event}`, listener);
return mockedCP.stderr;
});
mockedCP.stderr.off.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): Readable => {
listeners.off(`stderr/${event}`, listener);
return mockedCP.stderr;
});
mockedCP.stderr.removeListener.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): Readable => {
listeners.off(`stderr/${event}`, listener);
return mockedCP.stderr;
});
mockedCP.stderr.once.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): Readable => {
listeners.once(`stderr/${event}`, listener);
return mockedCP.stderr;
});
mockedCP.stderr.removeAllListeners.mockImplementation((event?: string): Readable => {
listeners.removeAllListeners(event ?? `stderr/${event}`);
return mockedCP.stderr; return mockedCP.stderr;
}); });
mockedCP.stdout = mock<Readable>(); mockedCP.stdout = mock<Readable>();
mockedCP.stdout.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): Readable => { mockedCP.stdout.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): Readable => {
listeners[`stdout/${event}`] = listener; listeners.on(`stdout/${event}`, listener);
listeners[`stdout/${event}`]("Starting to serve on 127.0.0.1:9191");
if (event === "data") {
listeners.emit("stdout/data", "Starting to serve on 127.0.0.1:9191");
}
return mockedCP.stdout;
});
mockedCP.stdout.once.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): Readable => {
listeners.once(`stdout/${event}`, listener);
return mockedCP.stdout;
});
mockedCP.stdout.off.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): Readable => {
listeners.off(`stdout/${event}`, listener);
return mockedCP.stdout;
});
mockedCP.stdout.removeListener.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): Readable => {
listeners.off(`stdout/${event}`, listener);
return mockedCP.stdout;
});
mockedCP.stdout.removeAllListeners.mockImplementation((event?: string): Readable => {
listeners.removeAllListeners(event ?? `stdout/${event}`);
return mockedCP.stdout; return mockedCP.stdout;
}); });
@ -175,36 +218,36 @@ describe("kube auth proxy tests", () => {
it("should call spawn and broadcast errors", async () => { it("should call spawn and broadcast errors", async () => {
await proxy.run(); await proxy.run();
listeners["error"]({ message: "foobarbat" }); listeners.emit("error", { message: "foobarbat" });
expect(mockBroadcastIpc).toBeCalledWith("kube-auth:foobar", { data: "foobarbat", error: true }); expect(mockBroadcastIpc).toBeCalledWith("cluster:foobar:connection-update", { message: "foobarbat", isError: true });
}); });
it("should call spawn and broadcast exit", async () => { it("should call spawn and broadcast exit", async () => {
await proxy.run(); await proxy.run();
listeners["exit"](0); listeners.emit("exit", 0);
expect(mockBroadcastIpc).toBeCalledWith("kube-auth:foobar", { data: "proxy exited with code: 0", error: false }); expect(mockBroadcastIpc).toBeCalledWith("cluster:foobar:connection-update", { message: "proxy exited with code: 0", isError: false });
}); });
it("should call spawn and broadcast errors from stderr", async () => { it("should call spawn and broadcast errors from stderr", async () => {
await proxy.run(); await proxy.run();
listeners["stderr/data"]("an error"); listeners.emit("stderr/data", "an error");
expect(mockBroadcastIpc).toBeCalledWith("kube-auth:foobar", { data: "an error", error: true }); expect(mockBroadcastIpc).toBeCalledWith("cluster:foobar:connection-update", { message: "an error", isError: true });
}); });
it("should call spawn and broadcast stdout serving info", async () => { it("should call spawn and broadcast stdout serving info", async () => {
await proxy.run(); await proxy.run();
expect(mockBroadcastIpc).toBeCalledWith("kube-auth:foobar", { data: "Authentication proxy started\n" }); expect(mockBroadcastIpc).toBeCalledWith("cluster:foobar:connection-update", { message: "Authentication proxy started", isError: false });
}); });
it("should call spawn and broadcast stdout other info", async () => { it("should call spawn and broadcast stdout other info", async () => {
await proxy.run(); await proxy.run();
listeners["stdout/data"]("some info"); listeners.emit("stdout/data", "some info");
expect(mockBroadcastIpc).toBeCalledWith("kube-auth:foobar", { data: "some info" }); expect(mockBroadcastIpc).toBeCalledWith("cluster:foobar:connection-update", { message: "some info", isError: false });
}); });
}); });
}); });

View File

@ -32,7 +32,7 @@ import logger from "./logger";
import { VersionDetector } from "./cluster-detectors/version-detector"; import { VersionDetector } from "./cluster-detectors/version-detector";
import { DetectorRegistry } from "./cluster-detectors/detector-registry"; import { DetectorRegistry } from "./cluster-detectors/detector-registry";
import plimit from "p-limit"; import plimit from "p-limit";
import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel } from "../common/cluster-types"; import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, KubeAuthUpdate } from "../common/cluster-types";
import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus } from "../common/cluster-types"; import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus } from "../common/cluster-types";
import { storedKubeConfigFolder, toJS } from "../common/utils"; import { storedKubeConfigFolder, toJS } from "../common/utils";
import type { Response } from "request"; import type { Response } from "request";
@ -117,12 +117,6 @@ export class Cluster implements ClusterModel, ClusterState {
* @observable * @observable
*/ */
@observable disconnected = true; @observable disconnected = true;
/**
* Connection failure reason
*
* @observable
*/
@observable failureReason: string;
/** /**
* Does user have admin like access * Does user have admin like access
* *
@ -358,14 +352,20 @@ export class Cluster implements ClusterModel, ClusterState {
if (this.disconnected || !this.accessible) { if (this.disconnected || !this.accessible) {
await this.reconnect(); await this.reconnect();
} }
this.broadcastConnectUpdate("Refreshing connection status ...");
await this.refreshConnectionStatus(); await this.refreshConnectionStatus();
if (this.accessible) { if (this.accessible) {
this.broadcastConnectUpdate("Refreshing cluster accessibility ...");
await this.refreshAccessibility(); await this.refreshAccessibility();
this.ensureKubectl(); // download kubectl in background, so it's not blocking dashboard // download kubectl in background, so it's not blocking dashboard
this.ensureKubectl()
.catch(error => logger.warn(`[CLUSTER]: failed to download kubectl for clusterId=${this.id}`, error));
this.broadcastConnectUpdate("Connected, waiting for view to load ...");
} }
this.activated = true;
this.activated = true;
this.pushState(); this.pushState();
} }
@ -445,9 +445,8 @@ export class Cluster implements ClusterModel, ClusterState {
private async refreshAccessibility(): Promise<void> { private async refreshAccessibility(): Promise<void> {
this.isAdmin = await this.isClusterAdmin(); this.isAdmin = await this.isClusterAdmin();
this.isGlobalWatchEnabled = await this.canUseWatchApi({ resource: "*" }); this.isGlobalWatchEnabled = await this.canUseWatchApi({ resource: "*" });
this.allowedNamespaces = await this.getAllowedNamespaces();
await this.refreshAllowedResources(); this.allowedResources = await this.getAllowedResources();
this.ready = true; this.ready = true;
} }
@ -462,15 +461,6 @@ export class Cluster implements ClusterModel, ClusterState {
this.accessible = connectionStatus == ClusterStatus.AccessGranted; this.accessible = connectionStatus == ClusterStatus.AccessGranted;
} }
/**
* @internal
*/
@action
async refreshAllowedResources() {
this.allowedNamespaces = await this.getAllowedNamespaces();
this.allowedResources = await this.getAllowedResources();
}
async getKubeconfig(): Promise<KubeConfig> { async getKubeconfig(): Promise<KubeConfig> {
const { config } = await loadConfigFromFile(this.kubeConfigPath); const { config } = await loadConfigFromFile(this.kubeConfigPath);
@ -501,34 +491,35 @@ export class Cluster implements ClusterModel, ClusterState {
this.metadata.version = versionData.value; this.metadata.version = versionData.value;
this.failureReason = null;
return ClusterStatus.AccessGranted; return ClusterStatus.AccessGranted;
} catch (error) { } catch (error) {
logger.error(`Failed to connect cluster "${this.contextName}": ${error}`); logger.error(`[CLUSTER]: Failed to connect to "${this.contextName}": ${error}`);
if (error.statusCode) { if (error.statusCode) {
if (error.statusCode >= 400 && error.statusCode < 500) { if (error.statusCode >= 400 && error.statusCode < 500) {
this.failureReason = "Invalid credentials"; this.broadcastConnectUpdate("Invalid credentials", true);
return ClusterStatus.AccessDenied;
} else {
this.failureReason = error.error || error.message;
return ClusterStatus.Offline;
}
} else if (error.failed === true) {
if (error.timedOut === true) {
this.failureReason = "Connection timed out";
return ClusterStatus.Offline;
} else {
this.failureReason = "Failed to fetch credentials";
return ClusterStatus.AccessDenied; return ClusterStatus.AccessDenied;
} }
this.broadcastConnectUpdate(error.error || error.message, true);
return ClusterStatus.Offline;
} }
this.failureReason = error.message;
if (error.failed === true) {
if (error.timedOut === true) {
this.broadcastConnectUpdate("Connection timed out", true);
return ClusterStatus.Offline;
}
this.broadcastConnectUpdate("Failed to fetch credentials", true);
return ClusterStatus.AccessDenied;
}
this.broadcastConnectUpdate(error.message, true);
return ClusterStatus.Offline; return ClusterStatus.Offline;
} }
@ -579,7 +570,7 @@ export class Cluster implements ClusterModel, ClusterState {
} }
toJSON(): ClusterModel { toJSON(): ClusterModel {
const model: ClusterModel = { return toJS({
id: this.id, id: this.id,
contextName: this.contextName, contextName: this.contextName,
kubeConfigPath: this.kubeConfigPath, kubeConfigPath: this.kubeConfigPath,
@ -589,29 +580,24 @@ export class Cluster implements ClusterModel, ClusterState {
metadata: this.metadata, metadata: this.metadata,
accessibleNamespaces: this.accessibleNamespaces, accessibleNamespaces: this.accessibleNamespaces,
labels: this.labels, labels: this.labels,
}; });
return toJS(model);
} }
/** /**
* Serializable cluster-state used for sync btw main <-> renderer * Serializable cluster-state used for sync btw main <-> renderer
*/ */
getState(): ClusterState { getState(): ClusterState {
const state: ClusterState = { return toJS({
apiUrl: this.apiUrl, apiUrl: this.apiUrl,
online: this.online, online: this.online,
ready: this.ready, ready: this.ready,
disconnected: this.disconnected, disconnected: this.disconnected,
accessible: this.accessible, accessible: this.accessible,
failureReason: this.failureReason,
isAdmin: this.isAdmin, isAdmin: this.isAdmin,
allowedNamespaces: this.allowedNamespaces, allowedNamespaces: this.allowedNamespaces,
allowedResources: this.allowedResources, allowedResources: this.allowedResources,
isGlobalWatchEnabled: this.isGlobalWatchEnabled, isGlobalWatchEnabled: this.isGlobalWatchEnabled,
}; });
return toJS(state);
} }
/** /**
@ -643,6 +629,17 @@ export class Cluster implements ClusterModel, ClusterState {
}; };
} }
/**
* broadcast an authentication update concerning this cluster
* @internal
*/
broadcastConnectUpdate(message: string, isError = false): void {
const update: KubeAuthUpdate = { message, isError };
logger.debug(`[CLUSTER]: broadcasting connection update`, { ...update, meta: this.getMeta() });
broadcastMessage(`cluster:${this.id}:connection-update`, update);
}
protected async getAllowedNamespaces() { protected async getAllowedNamespaces() {
if (this.accessibleNamespaces.length) { if (this.accessibleNamespaces.length) {
return this.accessibleNamespaces; return this.accessibleNamespaces;

View File

@ -146,7 +146,7 @@ export class ContextHandler {
proxyEnv.HTTPS_PROXY = this.cluster.preferences.httpsProxy; proxyEnv.HTTPS_PROXY = this.cluster.preferences.httpsProxy;
} }
this.kubeAuthProxy = new KubeAuthProxy(this.cluster, proxyEnv); this.kubeAuthProxy = new KubeAuthProxy(this.cluster, proxyEnv);
this.kubeAuthProxy.run(); await this.kubeAuthProxy.run();
} }
await this.kubeAuthProxy.whenReady; await this.kubeAuthProxy.whenReady;
@ -157,8 +157,4 @@ export class ContextHandler {
this.kubeAuthProxy = undefined; this.kubeAuthProxy = undefined;
this.apiTarget = undefined; this.apiTarget = undefined;
} }
get proxyLastError(): string {
return this.kubeAuthProxy?.lastError || "";
}
} }

View File

@ -22,7 +22,6 @@
import { ChildProcess, spawn } from "child_process"; import { ChildProcess, spawn } from "child_process";
import { waitUntilUsed } from "tcp-port-used"; import { waitUntilUsed } from "tcp-port-used";
import { randomBytes } from "crypto"; import { randomBytes } from "crypto";
import { broadcastMessage } from "../common/ipc";
import type { Cluster } from "./cluster"; import type { Cluster } from "./cluster";
import { Kubectl } from "./kubectl"; import { Kubectl } from "./kubectl";
import logger from "./logger"; import logger from "./logger";
@ -30,22 +29,16 @@ import * as url from "url";
import { getPortFrom } from "./utils/get-port"; import { getPortFrom } from "./utils/get-port";
import { makeObservable, observable, when } from "mobx"; import { makeObservable, observable, when } from "mobx";
export interface KubeAuthProxyLog {
data: string;
error?: boolean; // stream=stderr
}
const startingServeRegex = /^starting to serve on (?<address>.+)/i; const startingServeRegex = /^starting to serve on (?<address>.+)/i;
export class KubeAuthProxy { export class KubeAuthProxy {
public lastError: string;
public readonly apiPrefix: string; public readonly apiPrefix: string;
public get port(): number { public get port(): number {
return this._port; return this._port;
} }
protected _port: number; protected _port?: number;
protected cluster: Cluster; protected cluster: Cluster;
protected env: NodeJS.ProcessEnv = null; protected env: NodeJS.ProcessEnv = null;
protected proxyProcess: ChildProcess; protected proxyProcess: ChildProcess;
@ -92,67 +85,56 @@ export class KubeAuthProxy {
this.proxyProcess = spawn(proxyBin, args, { env: this.env }); this.proxyProcess = spawn(proxyBin, args, { env: this.env });
this.proxyProcess.on("error", (error) => { this.proxyProcess.on("error", (error) => {
this.sendIpcLogMessage({ data: error.message, error: true }); this.cluster.broadcastConnectUpdate(error.message, true);
this.exit(); this.exit();
}); });
this.proxyProcess.on("exit", (code) => { this.proxyProcess.on("exit", (code) => {
this.sendIpcLogMessage({ data: `proxy exited with code: ${code}`, error: code > 0 }); this.cluster.broadcastConnectUpdate(`proxy exited with code: ${code}`, code > 0);
this.exit();
});
this.proxyProcess.on("disconnect", () => {
this.cluster.broadcastConnectUpdate("Proxy disconnected communications", true );
this.exit(); this.exit();
}); });
this.proxyProcess.stderr.on("data", (data) => { this.proxyProcess.stderr.on("data", (data) => {
this.lastError = this.parseError(data.toString()); this.cluster.broadcastConnectUpdate(data.toString(), true);
this.sendIpcLogMessage({ data: data.toString(), error: true }); });
this.proxyProcess.stdout.on("data", (data: any) => {
if (typeof this._port === "number") {
this.cluster.broadcastConnectUpdate(data.toString());
}
}); });
this._port = await getPortFrom(this.proxyProcess.stdout, { this._port = await getPortFrom(this.proxyProcess.stdout, {
lineRegex: startingServeRegex, lineRegex: startingServeRegex,
onFind: () => this.sendIpcLogMessage({ data: "Authentication proxy started\n" }), onFind: () => this.cluster.broadcastConnectUpdate("Authentication proxy started"),
}); });
this.proxyProcess.stdout.on("data", (data: any) => { try {
this.sendIpcLogMessage({ data: data.toString() }); await waitUntilUsed(this.port, 500, 10000);
}); this.ready = true;
} catch (error) {
this.cluster.broadcastConnectUpdate("Proxy port failed to be used within timelimit, restarting...", true);
this.exit();
await waitUntilUsed(this.port, 500, 10000); return this.run();
this.ready = true;
}
protected parseError(data: string) {
const error = data.split("http: proxy error:").slice(1).join("").trim();
let errorMsg = error;
const jsonError = error.split("Response: ")[1];
if (jsonError) {
try {
const parsedError = JSON.parse(jsonError);
errorMsg = parsedError.error_description || parsedError.error || jsonError;
} catch (_) {
errorMsg = jsonError.trim();
}
} }
return errorMsg;
}
protected sendIpcLogMessage(res: KubeAuthProxyLog) {
const channel = `kube-auth:${this.cluster.id}`;
logger.info(`[KUBE-AUTH]: out-channel "${channel}"`, { ...res, meta: this.cluster.getMeta() });
broadcastMessage(channel, res);
} }
public exit() { public exit() {
this.ready = false; this.ready = false;
if (!this.proxyProcess) return;
logger.debug("[KUBE-AUTH]: stopping local proxy", this.cluster.getMeta()); if (this.proxyProcess) {
this.proxyProcess.kill(); logger.debug("[KUBE-AUTH]: stopping local proxy", this.cluster.getMeta());
this.proxyProcess.removeAllListeners(); this.proxyProcess.removeAllListeners();
this.proxyProcess.stderr.removeAllListeners(); this.proxyProcess.stderr.removeAllListeners();
this.proxyProcess.stdout.removeAllListeners(); this.proxyProcess.stdout.removeAllListeners();
this.proxyProcess = null; this.proxyProcess.kill();
this.proxyProcess = null;
}
} }
} }

View File

@ -49,7 +49,7 @@ export class KubeconfigManager {
try { try {
this.tempFile = await this.createProxyKubeconfig(); this.tempFile = await this.createProxyKubeconfig();
} catch (err) { } catch (err) {
logger.error(`Failed to created temp config for auth-proxy`, { err }); logger.error(`[KUBECONFIG-MANAGER]: Failed to created temp config for auth-proxy`, { err });
} }
} }
@ -61,7 +61,7 @@ export class KubeconfigManager {
return; return;
} }
logger.info(`Deleting temporary kubeconfig: ${this.tempFile}`); logger.info(`[KUBECONFIG-MANAGER]: Deleting temporary kubeconfig: ${this.tempFile}`);
await fs.unlink(this.tempFile); await fs.unlink(this.tempFile);
} }
@ -70,7 +70,7 @@ export class KubeconfigManager {
return; return;
} }
logger.info(`Deleting temporary kubeconfig: ${this.tempFile}`); logger.info(`[KUBECONFIG-MANAGER]: Deleting temporary kubeconfig: ${this.tempFile}`);
await fs.unlink(this.tempFile); await fs.unlink(this.tempFile);
this.tempFile = undefined; this.tempFile = undefined;
} }
@ -80,8 +80,7 @@ export class KubeconfigManager {
await this.contextHandler.ensureServer(); await this.contextHandler.ensureServer();
this.tempFile = await this.createProxyKubeconfig(); this.tempFile = await this.createProxyKubeconfig();
} catch (err) { } catch (err) {
console.log(err); logger.error(`[KUBECONFIG-MANAGER]: Failed to created temp config for auth-proxy`, err);
logger.error(`Failed to created temp config for auth-proxy`, { err });
} }
} }
@ -124,7 +123,7 @@ export class KubeconfigManager {
await fs.ensureDir(path.dirname(tempFile)); await fs.ensureDir(path.dirname(tempFile));
await fs.writeFile(tempFile, configYaml, { mode: 0o600 }); await fs.writeFile(tempFile, configYaml, { mode: 0o600 });
logger.debug(`Created temp kubeconfig "${contextName}" at "${tempFile}": \n${configYaml}`); logger.debug(`[KUBECONFIG-MANAGER]: Created temp kubeconfig "${contextName}" at "${tempFile}": \n${configYaml}`);
return tempFile; return tempFile;
} }

View File

@ -119,7 +119,7 @@ export class WindowManager extends Singleton {
logger.info("[WINDOW-MANAGER]: Main window loaded"); logger.info("[WINDOW-MANAGER]: Main window loaded");
}) })
.on("will-attach-webview", (event, webPreferences, params) => { .on("will-attach-webview", (event, webPreferences, params) => {
logger.info("[WINDOW-MANAGER]: Attaching webview"); logger.debug("[WINDOW-MANAGER]: Attaching webview");
// Following is security recommendations because we allow webview tag (webviewTag: true) // Following is security recommendations because we allow webview tag (webviewTag: true)
// suggested by https://www.electronjs.org/docs/tutorial/security#11-verify-webview-options-before-creation // suggested by https://www.electronjs.org/docs/tutorial/security#11-verify-webview-options-before-creation
// and https://www.electronjs.org/docs/tutorial/security#10-do-not-use-allowpopups // and https://www.electronjs.org/docs/tutorial/security#10-do-not-use-allowpopups

View File

@ -21,9 +21,8 @@
import styles from "./cluster-status.module.css"; import styles from "./cluster-status.module.css";
import { ipcRenderer } from "electron";
import { computed, observable, makeObservable } from "mobx"; import { computed, observable, makeObservable } from "mobx";
import { observer } from "mobx-react"; import { disposeOnUnmount, observer } from "mobx-react";
import React from "react"; import React from "react";
import { clusterActivateHandler } from "../../../common/cluster-ipc"; import { clusterActivateHandler } from "../../../common/cluster-ipc";
import { ClusterStore } from "../../../common/cluster-store"; import { ClusterStore } from "../../../common/cluster-store";
@ -33,10 +32,9 @@ import { cssNames, IClassName } from "../../utils";
import { Button } from "../button"; import { Button } from "../button";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { Spinner } from "../spinner"; import { Spinner } from "../spinner";
import type { KubeAuthProxyLog } from "../../../main/kube-auth-proxy";
import { navigate } from "../../navigation"; import { navigate } from "../../navigation";
import { entitySettingsURL } from "../../../common/routes"; import { entitySettingsURL } from "../../../common/routes";
import type { ClusterId } from "../../../common/cluster-types"; import type { ClusterId, KubeAuthUpdate } from "../../../common/cluster-types";
interface Props { interface Props {
className?: IClassName; className?: IClassName;
@ -45,7 +43,7 @@ interface Props {
@observer @observer
export class ClusterStatus extends React.Component<Props> { export class ClusterStatus extends React.Component<Props> {
@observable authOutput: KubeAuthProxyLog[] = []; @observable authOutput: KubeAuthUpdate[] = [];
@observable isReconnecting = false; @observable isReconnecting = false;
constructor(props: Props) { constructor(props: Props) {
@ -58,31 +56,31 @@ export class ClusterStatus extends React.Component<Props> {
} }
@computed get hasErrors(): boolean { @computed get hasErrors(): boolean {
return this.authOutput.some(({ error }) => error) || !!this.cluster.failureReason; return this.authOutput.some(({ isError }) => isError);
} }
async componentDidMount() { componentDidMount() {
ipcRendererOn(`kube-auth:${this.cluster.id}`, (evt, res: KubeAuthProxyLog) => { disposeOnUnmount(this, [
this.authOutput.push({ ipcRendererOn(`cluster:${this.cluster.id}:connection-update`, (evt, res: KubeAuthUpdate) => {
data: res.data.trimRight(), this.authOutput.push(res);
error: res.error, }),
}); ]);
});
} }
componentWillUnmount() {
ipcRenderer.removeAllListeners(`kube-auth:${this.props.clusterId}`);
}
activateCluster = async (force = false) => {
await requestMain(clusterActivateHandler, this.props.clusterId, force);
};
reconnect = async () => { reconnect = async () => {
this.authOutput = []; this.authOutput = [];
this.isReconnecting = true; this.isReconnecting = true;
await this.activateCluster(true);
this.isReconnecting = false; try {
await requestMain(clusterActivateHandler, this.props.clusterId, true);
} catch (error) {
this.authOutput.push({
message: error.toString(),
isError: true,
});
} finally {
this.isReconnecting = false;
}
}; };
manageProxySettings = () => { manageProxySettings = () => {
@ -94,58 +92,68 @@ export class ClusterStatus extends React.Component<Props> {
})); }));
}; };
renderContent() { renderAuthenticationOutput() {
const { authOutput, cluster, hasErrors } = this; return (
const failureReason = cluster.failureReason; <pre>
{
this.authOutput.map(({ message, isError }, index) => (
<p key={index} className={cssNames({ error: isError })}>
{message.trim()}
</p>
))
}
</pre>
);
}
if (!hasErrors || this.isReconnecting) { renderStatusIcon() {
return ( if (this.hasErrors) {
<div className="flex items-center column gaps"> return <Icon material="cloud_off" className={styles.icon} />;
<Spinner singleColor={false} className={styles.spinner} />
<pre className="kube-auth-out">
<p>{this.isReconnecting ? "Reconnecting" : "Connecting"}&hellip;</p>
{authOutput.map(({ data, error }, index) => {
return <p key={index} className={cssNames({ error })}>{data}</p>;
})}
</pre>
</div>
);
} }
return ( return (
<div className="flex items-center column gaps"> <>
<Icon material="cloud_off" className={styles.icon} /> <Spinner singleColor={false} className={styles.spinner} />
<h2> <pre className="kube-auth-out">
{cluster.preferences.clusterName} <p>{this.isReconnecting ? "Reconnecting" : "Connecting"}&hellip;</p>
</h2>
<pre>
{authOutput.map(({ data, error }, index) => {
return <p key={index} className={cssNames({ error })}>{data}</p>;
})}
</pre> </pre>
{failureReason && ( </>
<div className="error">{failureReason}</div>
)}
<Button
primary
label="Reconnect"
className="box center"
onClick={this.reconnect}
waiting={this.isReconnecting}
/>
<a
className="box center interactive"
onClick={this.manageProxySettings}>
Manage Proxy Settings
</a>
</div>
); );
} }
renderReconnectionHelp() {
if (this.hasErrors && !this.isReconnecting) {
return (
<>
<Button
primary
label="Reconnect"
className="box center"
onClick={this.reconnect}
waiting={this.isReconnecting}
/>
<a
className="box center interactive"
onClick={this.manageProxySettings}
>
Manage Proxy Settings
</a>
</>
);
}
return undefined;
}
render() { render() {
return ( return (
<div className={cssNames(styles.status, "flex column box center align-center justify-center", this.props.className)}> <div className={cssNames(styles.status, "flex column box center align-center justify-center", this.props.className)}>
{this.renderContent()} <div className="flex items-center column gaps">
<h2>{this.cluster.preferences.clusterName}</h2>
{this.renderStatusIcon()}
{this.renderAuthenticationOutput()}
{this.renderReconnectionHelp()}
</div>
</div> </div>
); );
} }