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

Correctly handle promises and rejections around cluster connection (#4216)

This commit is contained in:
Sebastian Malton 2021-11-30 09:05:34 -05:00 committed by GitHub
parent bb30bdc750
commit 2ab9aeb83c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 78 additions and 94 deletions

View File

@ -142,9 +142,9 @@ describe("kubeconfig manager tests", () => {
const configPath = await kubeConfManager.getPath(); const configPath = await kubeConfManager.getPath();
expect(await fse.pathExists(configPath)).toBe(true); expect(await fse.pathExists(configPath)).toBe(true);
await kubeConfManager.unlink(); await kubeConfManager.clear();
expect(await fse.pathExists(configPath)).toBe(false); expect(await fse.pathExists(configPath)).toBe(false);
await kubeConfManager.unlink(); // doesn't throw await kubeConfManager.clear(); // doesn't throw
expect(async () => { expect(async () => {
await kubeConfManager.getPath(); await kubeConfManager.getPath();
}).rejects.toThrow("already unlinked"); }).rejects.toThrow("already unlinked");

View File

@ -34,7 +34,7 @@ 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, KubeAuthUpdate } 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 { disposer, storedKubeConfigFolder, toJS } from "../common/utils";
import type { Response } from "request"; import type { Response } from "request";
/** /**
@ -52,8 +52,8 @@ export class Cluster implements ClusterModel, ClusterState {
* @internal * @internal
*/ */
public contextHandler: ContextHandler; public contextHandler: ContextHandler;
protected kubeconfigManager: KubeconfigManager; protected proxyKubeconfigManager: KubeconfigManager;
protected eventDisposers: Function[] = []; protected eventsDisposer = disposer();
protected activated = false; protected activated = false;
private resourceAccessStatuses: Map<KubeApiResource, boolean> = new Map(); private resourceAccessStatuses: Map<KubeApiResource, boolean> = new Map();
@ -218,9 +218,7 @@ export class Cluster implements ClusterModel, ClusterState {
* @internal * @internal
*/ */
@computed get defaultNamespace(): string { @computed get defaultNamespace(): string {
const { defaultNamespace } = this.preferences; return this.preferences.defaultNamespace;
return defaultNamespace;
} }
constructor(model: ClusterModel) { constructor(model: ClusterModel) {
@ -240,7 +238,7 @@ export class Cluster implements ClusterModel, ClusterState {
if (ipcMain) { if (ipcMain) {
// for the time being, until renderer gets its own cluster type // for the time being, until renderer gets its own cluster type
this.contextHandler = new ContextHandler(this); this.contextHandler = new ContextHandler(this);
this.kubeconfigManager = new KubeconfigManager(this, this.contextHandler); this.proxyKubeconfigManager = new KubeconfigManager(this, this.contextHandler);
logger.debug(`[CLUSTER]: Cluster init success`, { logger.debug(`[CLUSTER]: Cluster init success`, {
id: this.id, id: this.id,
@ -297,40 +295,31 @@ export class Cluster implements ClusterModel, ClusterState {
const refreshTimer = setInterval(() => !this.disconnected && this.refresh(), 30000); // every 30s const refreshTimer = setInterval(() => !this.disconnected && this.refresh(), 30000); // every 30s
const refreshMetadataTimer = setInterval(() => !this.disconnected && this.refreshMetadata(), 900000); // every 15 minutes const refreshMetadataTimer = setInterval(() => !this.disconnected && this.refreshMetadata(), 900000); // every 15 minutes
if (ipcMain) { this.eventsDisposer.push(
this.eventDisposers.push( reaction(() => this.getState(), state => this.pushState(state)),
reaction(() => this.getState(), () => this.pushState()), reaction(
reaction(() => this.prometheusPreferences, (prefs) => this.contextHandler.setupPrometheus(prefs), { equals: comparer.structural }), () => this.prometheusPreferences,
() => { prefs => this.contextHandler.setupPrometheus(prefs),
clearInterval(refreshTimer); { equals: comparer.structural },
clearInterval(refreshMetadataTimer); ),
}, () => clearInterval(refreshTimer),
reaction(() => this.defaultNamespace, () => this.recreateProxyKubeconfig()), () => clearInterval(refreshMetadataTimer),
); reaction(() => this.defaultNamespace, () => this.recreateProxyKubeconfig()),
} );
} }
/** /**
* @internal * @internal
*/ */
async recreateProxyKubeconfig() { protected async recreateProxyKubeconfig() {
logger.info("Recreate proxy kubeconfig"); logger.info("[CLUSTER]: Recreating proxy kubeconfig");
try { try {
this.kubeconfigManager.clear(); await this.proxyKubeconfigManager.clear();
} catch { await this.getProxyKubeconfig();
// do nothing } catch (error) {
logger.error(`[CLUSTER]: failed to recreate proxy kubeconfig`, error);
} }
this.getProxyKubeconfig();
}
/**
* internal
*/
protected unbindEvents() {
logger.info(`[CLUSTER]: unbind events`, this.getMeta());
this.eventDisposers.forEach(dispose => dispose());
this.eventDisposers.length = 0;
} }
/** /**
@ -345,7 +334,7 @@ export class Cluster implements ClusterModel, ClusterState {
logger.info(`[CLUSTER]: activate`, this.getMeta()); logger.info(`[CLUSTER]: activate`, this.getMeta());
if (!this.eventDisposers.length) { if (!this.eventsDisposer.length) {
this.bindEvents(); this.bindEvents();
} }
@ -395,7 +384,8 @@ export class Cluster implements ClusterModel, ClusterState {
* @internal * @internal
*/ */
@action disconnect() { @action disconnect() {
this.unbindEvents(); logger.info(`[CLUSTER]: disconnecting`, { id: this.id });
this.eventsDisposer();
this.contextHandler?.stopServer(); this.contextHandler?.stopServer();
this.disconnected = true; this.disconnected = true;
this.online = false; this.online = false;
@ -405,7 +395,7 @@ export class Cluster implements ClusterModel, ClusterState {
this.allowedNamespaces = []; this.allowedNamespaces = [];
this.resourceAccessStatuses.clear(); this.resourceAccessStatuses.clear();
this.pushState(); this.pushState();
logger.info(`[CLUSTER]: disconnect`, this.getMeta()); logger.info(`[CLUSTER]: disconnected`, { id: this.id });
} }
/** /**
@ -481,7 +471,7 @@ export class Cluster implements ClusterModel, ClusterState {
* @internal * @internal
*/ */
async getProxyKubeconfigPath(): Promise<string> { async getProxyKubeconfigPath(): Promise<string> {
return this.kubeconfigManager.getPath(); return this.proxyKubeconfigManager.getPath();
} }
protected async getConnectionStatus(): Promise<ClusterStatus> { protected async getConnectionStatus(): Promise<ClusterStatus> {

View File

@ -32,30 +32,21 @@ import { makeObservable, observable, when } from "mobx";
const startingServeRegex = /^starting to serve on (?<address>.+)/i; const startingServeRegex = /^starting to serve on (?<address>.+)/i;
export class KubeAuthProxy { export class KubeAuthProxy {
public readonly apiPrefix: string; public readonly apiPrefix = `/${randomBytes(8).toString("hex")}`;
public get port(): number { public get port(): number {
return this._port; return this._port;
} }
protected _port?: number; protected _port: number;
protected cluster: Cluster; protected proxyProcess?: ChildProcess;
protected env: NodeJS.ProcessEnv = null; protected readonly acceptHosts: string;
protected proxyProcess: ChildProcess; @observable protected ready = false;
protected kubectl: Kubectl;
@observable protected ready: boolean;
constructor(cluster: Cluster, env: NodeJS.ProcessEnv) { constructor(protected readonly cluster: Cluster, protected readonly env: NodeJS.ProcessEnv) {
makeObservable(this); makeObservable(this);
this.ready = false;
this.env = env;
this.cluster = cluster;
this.kubectl = Kubectl.bundled();
this.apiPrefix = `/${randomBytes(8).toString("hex")}`;
}
get acceptHosts() { this.acceptHosts = url.parse(this.cluster.apiUrl).hostname;
return url.parse(this.cluster.apiUrl).hostname;
} }
get whenReady() { get whenReady() {
@ -67,7 +58,7 @@ export class KubeAuthProxy {
return this.whenReady; return this.whenReady;
} }
const proxyBin = await this.kubectl.getPath(); const proxyBin = await Kubectl.bundled().getPath();
const args = [ const args = [
"proxy", "proxy",
"-p", "0", "-p", "0",

View File

@ -30,57 +30,60 @@ import { LensProxy } from "./lens-proxy";
import { AppPaths } from "../common/app-paths"; import { AppPaths } from "../common/app-paths";
export class KubeconfigManager { export class KubeconfigManager {
protected configDir = AppPaths.get("temp"); /**
protected tempFile: string = null; * The path to the temp config file
*
* - if `string` then path
* - if `null` then not yet created
* - if `undefined` then unlinked by calling `clear()`
*/
protected tempFilePath: string | null | undefined = null;
constructor(protected cluster: Cluster, protected contextHandler: ContextHandler) { } constructor(protected cluster: Cluster, protected contextHandler: ContextHandler) { }
/**
*
* @returns The path to the temporary kubeconfig
*/
async getPath(): Promise<string> { async getPath(): Promise<string> {
if (this.tempFile === undefined) { if (this.tempFilePath === undefined) {
throw new Error("kubeconfig is already unlinked"); throw new Error("kubeconfig is already unlinked");
} }
if (!this.tempFile) { if (this.tempFilePath === null || !(await fs.pathExists(this.tempFilePath))) {
await this.init(); await this.ensureFile();
} }
// create proxy kubeconfig if it is removed without unlink called return this.tempFilePath;
if (!(await fs.pathExists(this.tempFile))) { }
try {
this.tempFile = await this.createProxyKubeconfig(); /**
} catch (err) { * Deletes the temporary kubeconfig file
logger.error(`[KUBECONFIG-MANAGER]: Failed to created temp config for auth-proxy`, { err }); */
async clear(): Promise<void> {
if (!this.tempFilePath) {
return;
}
logger.info(`[KUBECONFIG-MANAGER]: Deleting temporary kubeconfig: ${this.tempFilePath}`);
try {
await fs.unlink(this.tempFilePath);
} catch (error) {
if (error.code !== "ENOENT") {
throw error;
} }
} finally {
this.tempFilePath = undefined;
} }
return this.tempFile;
} }
async clear() { protected async ensureFile() {
if (!this.tempFile) {
return;
}
logger.info(`[KUBECONFIG-MANAGER]: Deleting temporary kubeconfig: ${this.tempFile}`);
await fs.unlink(this.tempFile);
}
async unlink() {
if (!this.tempFile) {
return;
}
logger.info(`[KUBECONFIG-MANAGER]: Deleting temporary kubeconfig: ${this.tempFile}`);
await fs.unlink(this.tempFile);
this.tempFile = undefined;
}
protected async init() {
try { try {
await this.contextHandler.ensureServer(); await this.contextHandler.ensureServer();
this.tempFile = await this.createProxyKubeconfig(); this.tempFilePath = await this.createProxyKubeconfig();
} catch (err) { } catch (error) {
logger.error(`[KUBECONFIG-MANAGER]: Failed to created temp config for auth-proxy`, err); throw Object.assign(new Error("Failed to creat temp config for auth-proxy"), { cause: error });
} }
} }
@ -93,9 +96,9 @@ export class KubeconfigManager {
* This way any user of the config does not need to know anything about the auth etc. details. * This way any user of the config does not need to know anything about the auth etc. details.
*/ */
protected async createProxyKubeconfig(): Promise<string> { protected async createProxyKubeconfig(): Promise<string> {
const { configDir, cluster } = this; const { cluster } = this;
const { contextName, id } = cluster; const { contextName, id } = cluster;
const tempFile = path.join(configDir, `kubeconfig-${id}`); const tempFile = path.join(AppPaths.get("temp"), `kubeconfig-${id}`);
const kubeConfig = await cluster.getKubeconfig(); const kubeConfig = await cluster.getKubeconfig();
const proxyConfig: Partial<KubeConfig> = { const proxyConfig: Partial<KubeConfig> = {
currentContext: contextName, currentContext: contextName,