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:
parent
bb30bdc750
commit
2ab9aeb83c
@ -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");
|
||||||
|
|||||||
@ -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> {
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user