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

Convert some non-store Singletons to normal injectables

- ClusterManager
- KubeconfigSyncManager
- LensProxy
- WindowManager

Other work:
- Convert logger to be an injection token, add createChildLogger to
  abstract away the need for logPrefix
- Fix tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2022-04-28 12:32:12 -04:00
parent dbdde19222
commit 50562f4eef
177 changed files with 2375 additions and 1745 deletions

View File

@ -8,12 +8,12 @@ import mockFs from "mock-fs";
import path from "path";
import fse from "fs-extra";
import type { Cluster } from "../cluster/cluster";
import { ClusterStore } from "../cluster-store/cluster-store";
import { ClusterStore } from "../cluster/store";
import { Console } from "console";
import { stdout, stderr } from "process";
import getCustomKubeConfigDirectoryInjectable from "../app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable";
import clusterStoreInjectable from "../cluster-store/cluster-store.injectable";
import type { ClusterModel } from "../cluster-types";
import clusterStoreInjectable from "../cluster/store.injectable";
import type { ClusterModel } from "../cluster/types";
import type {
DiContainer,
} from "@ogre-tools/injectable";

View File

@ -29,7 +29,7 @@ import { stdout, stderr } from "process";
import userStoreInjectable from "../user-store/user-store.injectable";
import type { DiContainer } from "@ogre-tools/injectable";
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
import type { ClusterStoreModel } from "../cluster-store/cluster-store";
import type { ClusterStoreModel } from "../cluster/store";
import { defaultTheme } from "../vars";
import writeFileInjectable from "../fs/write-file.injectable";
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";

View File

@ -12,4 +12,9 @@ export interface AppEvent {
params?: Record<string, any>;
}
export type AppEventBus = EventEmitter<[AppEvent]>;
/**
* @deprecated use `di.inject(appEventBusInjectable)` instead
*/
export const appEventBus = new EventEmitter<[AppEvent]>();

View File

@ -121,14 +121,14 @@ export abstract class BaseStore<T> extends Singleton {
if (ipcMain) {
this.syncDisposers.push(ipcMainOn(this.syncMainChannel, (event, model: T) => {
logger.silly(`[STORE]: SYNC ${this.name} from renderer`, { model });
logger.debug(`[STORE]: SYNC ${this.name} from renderer`, { model });
this.onSync(model);
}));
}
if (ipcRenderer) {
this.syncDisposers.push(ipcRendererOn(this.syncRendererChannel, (event, model: T) => {
logger.silly(`[STORE]: SYNC ${this.name} from main`, { model });
logger.debug(`[STORE]: SYNC ${this.name} from main`, { model });
this.onSyncFromMain(model);
}));
}

View File

@ -6,7 +6,7 @@
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
import type { CatalogEntityActionContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus, CatalogCategorySpec } from "../catalog";
import { CatalogEntity, CatalogCategory } from "../catalog";
import { ClusterStore } from "../cluster-store/cluster-store";
import { ClusterStore } from "../cluster/store";
import { broadcastMessage } from "../ipc";
import { app } from "electron";
import type { CatalogEntitySpec } from "../catalog/catalog-entity";

View File

@ -4,7 +4,7 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import { comparer, computed } from "mobx";
import hostedClusterInjectable from "./hosted-cluster.injectable";
import hostedClusterInjectable from "./hosted.injectable";
const allowedResourcesInjectable = getInjectable({
id: "allowed-resources",

View File

@ -3,7 +3,6 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { ipcMain } from "electron";
import { action, comparer, computed, makeObservable, observable, reaction, when } from "mobx";
import { broadcastMessage } from "../ipc";
import type { ContextHandler } from "../../main/context-handler/context-handler";
@ -15,21 +14,23 @@ import { loadConfigFromFile, loadConfigFromFileSync, validateKubeConfig } from "
import type { KubeApiResource, KubeResource } from "../rbac";
import { apiResourceRecord, apiResources } from "../rbac";
import logger from "../../main/logger";
import { VersionDetector } from "../../main/cluster-detectors/version-detector";
import { DetectorRegistry } from "../../main/cluster-detectors/detector-registry";
import { VersionDetector } from "../../main/cluster/detectors/version-detector";
import { DetectorRegistry } from "../../main/cluster/detectors/detector-registry";
import plimit from "p-limit";
import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, KubeAuthUpdate } from "../cluster-types";
import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus } from "../cluster-types";
import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, KubeAuthUpdate } from "./types";
import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus } from "./types";
import { disposer, toJS } from "../utils";
import type { Response } from "request";
import { clusterListNamespaceForbiddenChannel } from "../ipc/cluster";
import type { CanI } from "./authorization-review.injectable";
import type { ListNamespaces } from "./list-namespaces.injectable";
import type { CreateKubeConfigManager } from "../../main/kubeconfig-manager/create-kubeconfig-manager.injectable";
import type { CreateContextHandler } from "../../main/context-handler/create-context-handler.injectable";
export interface ClusterDependencies {
readonly directoryForKubeConfigs: string;
createKubeconfigManager: (cluster: Cluster) => KubeconfigManager;
createContextHandler: (cluster: Cluster) => ContextHandler;
createKubeconfigManager: CreateKubeConfigManager;
createContextHandler: CreateContextHandler;
createKubectl: (clusterVersion: string) => Kubectl;
createAuthorizationReview: (config: KubeConfig) => CanI;
createListNamespaces: (config: KubeConfig) => ListNamespaces;
@ -49,9 +50,9 @@ export class Cluster implements ClusterModel, ClusterState {
*
* @internal
*/
public contextHandler: ContextHandler;
protected proxyKubeconfigManager: KubeconfigManager;
protected eventsDisposer = disposer();
public readonly contextHandler: ContextHandler | undefined;
protected readonly proxyKubeconfigManager: KubeconfigManager | undefined;
protected readonly eventsDisposer = disposer();
protected activated = false;
private resourceAccessStatuses: Map<KubeApiResource, boolean> = new Map();
@ -233,17 +234,15 @@ export class Cluster implements ClusterModel, ClusterState {
this.apiUrl = config.getCluster(config.getContextObject(this.contextName).cluster).server;
if (ipcMain) {
// for the time being, until renderer gets its own cluster type
this.contextHandler = this.dependencies.createContextHandler(this);
this.proxyKubeconfigManager = this.dependencies.createKubeconfigManager(this);
// for the time being, until renderer gets its own cluster type
this.contextHandler = this.dependencies.createContextHandler(this);
this.proxyKubeconfigManager = this.dependencies.createKubeconfigManager(this);
logger.debug(`[CLUSTER]: Cluster init success`, {
id: this.id,
context: this.contextName,
apiUrl: this.apiUrl,
});
}
logger.debug(`[CLUSTER]: Cluster init success`, {
id: this.id,
context: this.contextName,
apiUrl: this.apiUrl,
});
}
/**
@ -571,7 +570,6 @@ export class Cluster implements ClusterModel, ClusterState {
* @param state cluster state
*/
pushState(state = this.getState()) {
logger.silly(`[CLUSTER]: push-state`, state);
broadcastMessage("cluster:state", this.id, state);
}

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
import type { ClusterModel } from "../cluster-types";
import type { ClusterModel } from "./types";
import type { Cluster } from "./cluster";
export const createClusterInjectionToken =

View File

@ -0,0 +1,21 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import clusterStoreInjectable from "./store.injectable";
import type { Cluster } from "./cluster";
import type { ClusterId } from "./types";
export type GetClusterById = (id: ClusterId) => Cluster | undefined;
const getClusterByIdInjectable = getInjectable({
id: "get-cluster-by-id",
instantiate: (di): GetClusterById => {
const store = di.inject(clusterStoreInjectable);
return (id) => store.getById(id);
},
});
export default getClusterByIdInjectable;

View File

@ -4,15 +4,16 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import { getHostedClusterId } from "../utils";
import clusterStoreInjectable from "./cluster-store.injectable";
import getClusterByIdInjectable from "./get-by-id.injectable";
const hostedClusterInjectable = getInjectable({
id: "hosted-cluster",
instantiate: (di) => {
const hostedClusterId = getHostedClusterId();
const getClusterById = di.inject(getClusterByIdInjectable);
return di.inject(clusterStoreInjectable).getById(hostedClusterId);
return getClusterById(hostedClusterId);
},
});

View File

@ -3,16 +3,19 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { ClusterStore } from "./cluster-store";
import { createClusterInjectionToken } from "../cluster/create-cluster-injection-token";
import { createClusterInjectionToken } from "./create-cluster-injection-token";
import { ClusterStore } from "./store";
const clusterStoreInjectable = getInjectable({
id: "cluster-store",
instantiate: (di) =>
ClusterStore.createInstance({
instantiate: (di) => {
ClusterStore.resetInstance();
return ClusterStore.createInstance({
createCluster: di.inject(createClusterInjectionToken),
}),
});
},
causesSideEffects: true,
});

View File

@ -7,13 +7,13 @@
import { ipcMain, ipcRenderer, webFrame } from "electron";
import { action, comparer, computed, makeObservable, observable, reaction } from "mobx";
import { BaseStore } from "../base-store";
import { Cluster } from "../cluster/cluster";
import { Cluster } from "./cluster";
import migrations from "../../migrations/cluster-store";
import logger from "../../main/logger";
import { appEventBus } from "../app-event-bus/event-bus";
import { ipcMainHandle } from "../ipc";
import { disposer, toJS } from "../utils";
import type { ClusterModel, ClusterId, ClusterState } from "../cluster-types";
import type { ClusterModel, ClusterId, ClusterState } from "./types";
import { requestInitialClusterStates } from "../../renderer/ipc";
import { clusterStates } from "../ipc/cluster";

View File

@ -6,8 +6,8 @@ import { getDiForUnitTesting } from "../../renderer/getDiForUnitTesting";
import { routeSpecificComponentInjectionToken } from "../../renderer/routes/route-specific-component-injection-token";
import { routeInjectionToken } from "./route-injection-token";
import { filter, map, matches } from "lodash/fp";
import clusterStoreInjectable from "../cluster-store/cluster-store.injectable";
import type { ClusterStore } from "../cluster-store/cluster-store";
import clusterStoreInjectable from "../cluster/store.injectable";
import type { ClusterStore } from "../cluster/store";
import { pipeline } from "@ogre-tools/fp";
describe("verify-that-all-routes-have-component", () => {

View File

@ -58,7 +58,7 @@ export async function broadcastMessage(channel: string, ...args: any[]): Promise
// Send message to views.
try {
logger.silly(`[IPC]: broadcasting "${channel}" to ${viewType}=${view.id}`, { args });
logger.debug(`[IPC]: broadcasting "${channel}" to ${viewType}=${view.id}`, { args });
view.send(channel, ...args);
} catch (error) {
logger.error(`[IPC]: failed to send IPC message "${channel}" to view "${viewType}=${view.id}"`, { error });
@ -66,7 +66,7 @@ export async function broadcastMessage(channel: string, ...args: any[]): Promise
// Send message to subFrames of views.
for (const frameInfo of subFrames) {
logger.silly(`[IPC]: broadcasting "${channel}" to subframe "frameInfo.processId"=${frameInfo.processId} "frameInfo.frameId"=${frameInfo.frameId}`, { args });
logger.debug(`[IPC]: broadcasting "${channel}" to subframe "frameInfo.processId"=${frameInfo.processId} "frameInfo.frameId"=${frameInfo.frameId}`, { args });
try {
view.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...args);

View File

@ -9,7 +9,7 @@ import { ResourceApplier } from "../../main/resource-applier";
import type { KubernetesCluster } from "../catalog-entities";
import logger from "../../main/logger";
import { app } from "electron";
import { ClusterStore } from "../cluster-store/cluster-store";
import { ClusterStore } from "../cluster/store";
import yaml from "js-yaml";
import { productName } from "../vars";
import { requestKubectlApplyAll, requestKubectlDeleteAll } from "../../renderer/ipc";

View File

@ -1,6 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export * from "./sentry";

View File

@ -1,80 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { app, ipcMain } from "electron";
import winston, { format } from "winston";
import type Transport from "winston-transport";
import { consoleFormat } from "winston-console-format";
import { isDebugging, isTestEnv } from "./vars";
import BrowserConsole from "winston-transport-browserconsole";
export interface Logger {
info: (message: string, ...args: any) => void;
error: (message: string, ...args: any) => void;
debug: (message: string, ...args: any) => void;
warn: (message: string, ...args: any) => void;
}
const logLevel = process.env.LOG_LEVEL
? process.env.LOG_LEVEL
: isDebugging
? "debug"
: isTestEnv
? "error"
: "info";
const transports: Transport[] = [];
if (ipcMain) {
transports.push(
new winston.transports.Console({
handleExceptions: false,
level: logLevel,
format: format.combine(
format.colorize({ level: true, message: false }),
format.padLevels(),
format.ms(),
consoleFormat({
showMeta: true,
inspectOptions: {
depth: 4,
colors: true,
maxArrayLength: 10,
breakLength: 120,
compact: Infinity,
},
}),
),
}),
);
if (!isTestEnv) {
transports.push(
new winston.transports.File({
handleExceptions: false,
level: logLevel,
filename: "lens.log",
/**
* SAFTEY: the `ipcMain` check above should mean that this is only
* called in the main process
*/
dirname: app.getPath("logs"),
maxsize: 16 * 1024,
maxFiles: 16,
tailable: true,
}),
);
}
} else {
transports.push(new BrowserConsole());
}
export default winston.createLogger({
format: format.combine(
format.splat(),
format.simple(),
),
transports,
});

View File

@ -0,0 +1,52 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import type { ClaimChildLogger } from "./claim-child-logger.injectable";
import claimChildLoggerInjectable from "./claim-child-logger.injectable";
import { baseLoggerInjectionToken } from "./logger.token";
import type { Logger } from "./type";
interface Dependencies {
baseLogger: Logger;
claimChildLogger: ClaimChildLogger;
}
export interface ChildLoggerArgs {
prefix: string;
defaultMeta?: Record<string, any>;
}
const createChildLogger = ({ baseLogger, claimChildLogger }: Dependencies, { prefix, defaultMeta = {}}: ChildLoggerArgs): Logger => {
const doDebugLogging = claimChildLogger(prefix);
const joinMeta = (meta: any): any => {
return {
...defaultMeta,
...meta,
};
};
return {
debug: (message, meta) => {
if (doDebugLogging.get()) {
baseLogger.debug(`[${prefix}]: ${message}`, joinMeta(meta));
}
},
info: (message, meta) => void baseLogger.info(`[${prefix}]: ${message}`, joinMeta(meta)),
error: (message, meta) => void baseLogger.error(`[${prefix}]: ${message}`, joinMeta(meta)),
warn: (message, meta) => void baseLogger.warn(`[${prefix}]: ${message}`, joinMeta(meta)),
};
};
const childLoggerInjectable = getInjectable({
id: "child-logger",
instantiate: (di, args: ChildLoggerArgs) => createChildLogger({
baseLogger: di.inject(baseLoggerInjectionToken),
claimChildLogger: di.inject(claimChildLoggerInjectable),
}, args),
lifecycle: lifecycleEnum.transient,
});
export default childLoggerInjectable;

View File

@ -0,0 +1,14 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { observable } from "mobx";
const childLoggersDebugStateInjectable = getInjectable({
id: "child-loggers",
instantiate: () => observable.map<string, boolean>(), // Whether debug logging is enabled for child loggers
});
export default childLoggersDebugStateInjectable;

View File

@ -0,0 +1,37 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { IComputedValue, ObservableMap } from "mobx";
import { runInAction, computed } from "mobx";
import childLoggersDebugStateInjectable from "./child-loggers-state.injectable";
export type ClaimChildLogger = (prefix: string) => IComputedValue<boolean>;
interface Dependencies {
state: ObservableMap<string, boolean>;
}
const claimChildLogger = ({ state }: Dependencies): ClaimChildLogger => (
(prefix) => {
if (state.has(prefix)) {
throw new Error(`Child logging prefix "${prefix}" has already been claimed`);
}
runInAction(() => {
state.set(prefix, false);
});
return computed(() => state.get(prefix));
}
);
const claimChildLoggerInjectable = getInjectable({
id: "claim-child-logger",
instantiate: (di) => claimChildLogger({
state: di.inject(childLoggersDebugStateInjectable),
}),
});
export default claimChildLoggerInjectable;

View File

@ -0,0 +1,35 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { Environments, getEnvironmentSpecificLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
import type { Logger } from "./type";
import { baseLoggerInjectionToken } from "./logger.token";
export type { Logger };
function legacyLog(level: keyof Logger, message: string, args: any[]) {
const di = getEnvironmentSpecificLegacyGlobalDiForExtensionApi(Environments.renderer)
?? getEnvironmentSpecificLegacyGlobalDiForExtensionApi(Environments.main);
if (!di) {
return;
}
const logger = di.inject(baseLoggerInjectionToken);
logger[level](message, ...args);
}
/**
* @deprecated create a new child logger with `di.inject(createChildLogger, { prefix: ... })`
*/
const logger: Logger = {
debug: (message, ...args) => legacyLog("debug", message, args),
info: (message, ...args) => legacyLog("info", message, args),
error: (message, ...args) => legacyLog("error", message, args),
warn: (message, ...args) => legacyLog("warn", message, args),
};
export default logger;

View File

@ -0,0 +1,10 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
import type { Logger } from "./type";
export const baseLoggerInjectionToken = getInjectionToken<Logger>({
id: "base-logger-token",
});

11
src/common/logger/type.ts Normal file
View File

@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export interface Logger {
info: (message: string, ...args: any) => void;
error: (message: string, ...args: any) => void;
debug: (message: string, ...args: any) => void;
warn: (message: string, ...args: any) => void;
}

View File

@ -0,0 +1,15 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import childLoggerInjectable from "../logger/child-logger.injectable";
const lensProtocolRouterLoggerInjectable = getInjectable({
id: "lens-protocol-handler-logger",
instantiate: (di) => di.inject(childLoggerInjectable, {
prefix: "LENS-PROTOCOL-ROUTER",
}),
});
export default lensProtocolRouterLoggerInjectable;

View File

@ -8,7 +8,6 @@ import { matchPath } from "react-router";
import { countBy } from "lodash";
import { iter } from "../utils";
import { pathToRegexp } from "path-to-regexp";
import logger from "../../main/logger";
import type Url from "url-parse";
import { RoutingError, RoutingErrorType } from "./error";
import type { ExtensionsStore } from "../../extensions/extensions-store/extensions-store";
@ -17,6 +16,7 @@ import type { LensExtension } from "../../extensions/lens-extension";
import type { RouteHandler, RouteParams } from "../../extensions/registries/protocol-handler";
import { when } from "mobx";
import { ipcRenderer } from "electron";
import type { Logger } from "../logger";
// IPC channel for protocol actions. Main broadcasts the open-url events to this channel.
export const ProtocolHandlerIpcPrefix = "protocol-handler";
@ -63,20 +63,19 @@ export function foldAttemptResults(mainAttempt: RouteAttempt, rendererAttempt: R
}
}
interface Dependencies {
extensionLoader: ExtensionLoader;
extensionsStore: ExtensionsStore;
export interface LensProtocolRouterDependencies {
readonly extensionLoader: ExtensionLoader;
readonly extensionsStore: ExtensionsStore;
readonly logger: Logger;
}
export abstract class LensProtocolRouter {
// Map between path schemas and the handlers
protected internalRoutes = new Map<string, RouteHandler>();
public static readonly LoggingPrefix = "[PROTOCOL ROUTER]";
protected readonly internalRoutes = new Map<string, RouteHandler>();
static readonly ExtensionUrlSchema = `/:${EXTENSION_PUBLISHER_MATCH}(@[A-Za-z0-9_]+)?/:${EXTENSION_NAME_MATCH}`;
constructor(protected dependencies: Dependencies) {}
constructor(protected readonly dependencies: LensProtocolRouterDependencies) {}
/**
* Attempts to route the given URL to all internal routes that have been registered
@ -130,7 +129,7 @@ export abstract class LensProtocolRouter {
data.extensionName = extensionName;
}
logger.info(`${LensProtocolRouter.LoggingPrefix}: No handler found`, data);
this.dependencies.logger.info(`No handler found`, data);
return RouteAttempt.MISSING;
}
@ -183,9 +182,7 @@ export abstract class LensProtocolRouter {
timeout: 5_000,
});
} catch (error) {
logger.info(
`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not installed (${error})`,
);
this.dependencies.logger.info(`Extension ${name} matched, but not installed (${error})`);
return name;
}
@ -193,18 +190,18 @@ export abstract class LensProtocolRouter {
const extension = extensionLoader.getInstanceByName(name);
if (!extension) {
logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but does not have a class for ${ipcRenderer ? "renderer" : "main"}`);
this.dependencies.logger.info(`Extension ${name} matched, but does not have a class for ${ipcRenderer ? "renderer" : "main"}`);
return name;
}
if (!this.dependencies.extensionsStore.isEnabled(extension)) {
logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not enabled`);
this.dependencies.logger.info(`Extension ${name} matched, but not enabled`);
return name;
}
logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched`);
this.dependencies.logger.info(`Extension ${name} matched`);
return extension;
}
@ -250,7 +247,7 @@ export abstract class LensProtocolRouter {
*/
public addInternalHandler(urlSchema: string, handler: RouteHandler): this {
pathToRegexp(urlSchema); // verify now that the schema is valid
logger.info(`${LensProtocolRouter.LoggingPrefix}: internal registering ${urlSchema}`);
this.dependencies.logger.info(`internal registering ${urlSchema}`);
this.internalRoutes.set(urlSchema, handler);
return this;

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ClusterId } from "../cluster-types";
import type { ClusterId } from "../cluster/types";
/**
* Grab the `ClusterId` out of a host that was generated by `getClusterFrameUrl`, or nothing

View File

@ -4,7 +4,7 @@
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { computed } from "mobx";
import allowedResourcesInjectable from "../cluster-store/allowed-resources.injectable";
import allowedResourcesInjectable from "../cluster/allowed-resources.injectable";
import type { KubeResource } from "../rbac";
export type IsAllowedResource = (resource: KubeResource) => boolean;
@ -12,7 +12,7 @@ export type IsAllowedResource = (resource: KubeResource) => boolean;
const isAllowedResourceInjectable = getInjectable({
id: "is-allowed-resource",
instantiate: (di, resourceName: KubeResource) => {
instantiate: (di, resourceName) => {
const allowedResources = di.inject(allowedResourcesInjectable);
return computed(() => allowedResources.get().has(resourceName));

View File

@ -0,0 +1,26 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { IObservableValue } from "mobx";
import { when } from "mobx";
/**
* This function waits for a box around a future supplied value to be set and then returns it
*/
export function waitUntilSet<T>(box: IObservableValue<T | undefined>): Promise<T> {
return new Promise<T>(resolve => {
when(() => {
const curVal = box.get();
if (curVal != null) {
resolve(curVal);
return true;
}
return false;
}, () => {});
});
}

View File

@ -3,9 +3,9 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Injectable } from "@ogre-tools/injectable";
import { baseLoggerInjectionToken } from "../../common/logger/logger.token";
import { asLegacyGlobalForExtensionApi } from "./as-legacy-global-object-for-extension-api";
import { getLegacyGlobalDiForExtensionApi } from "./legacy-global-di-for-extension-api";
import loggerInjectable from "../../common/logger.injectable";
export const asLegacyGlobalSingletonForExtensionApi = <
Instance,
@ -26,11 +26,9 @@ export const asLegacyGlobalSingletonForExtensionApi = <
resetInstance: () => {
const di = getLegacyGlobalDiForExtensionApi();
const logger = di.inject(loggerInjectable);
const logger = di.inject(baseLoggerInjectionToken);
logger.warn(
`resetInstance() for a legacy global singleton of "${injectable.id}" does nothing.`,
);
logger.warn(`resetInstance() for a legacy global singleton of "${injectable.id}" does nothing.`);
},
};
};

View File

@ -4,13 +4,14 @@
*/
import { LensExtension } from "./lens-extension";
import { WindowManager } from "../main/window-manager";
import { catalogEntityRegistry } from "../main/catalog";
import type { CatalogEntity } from "../common/catalog";
import type { IObservableArray } from "mobx";
import type { MenuRegistration } from "../main/menu/menu-registration";
import type { TrayMenuRegistration } from "../main/tray/tray-menu-registration";
import type { ShellEnvModifier } from "../main/shell-session/shell-env-modifier/shell-env-modifier-registration";
import { getLegacyGlobalDiForExtensionApi } from "./as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
import windowManagerInjectable from "../main/window/manager.injectable";
export class LensMainExtension extends LensExtension {
appMenus: MenuRegistration[] = [];
@ -18,21 +19,24 @@ export class LensMainExtension extends LensExtension {
/**
* implement this to modify the shell environment that Lens terminals are opened with. The ShellEnvModifier type has the signature
*
*
* (ctx: ShellEnvContext, env: Record<string, string | undefined>) => Record<string, string | undefined>
*
*
* @param ctx the shell environment context, specifically the relevant catalog entity for the terminal. This can be used, for example, to get
* cluster-specific information that can be made available in the shell environment by the implementation of terminalShellEnvModifier
*
*
* @param env the current shell environment that the terminal will be opened with. The implementation should modify this as desired.
*
*
* @returns the modified shell environment that the terminal will be opened with. The implementation must return env as passed in, if it
* does not modify the shell environment
*/
terminalShellEnvModifier?: ShellEnvModifier;
async navigate(pageId?: string, params?: Record<string, any>, frameId?: number) {
return WindowManager.getInstance().navigateExtension(this.id, pageId, params, frameId);
const di = getLegacyGlobalDiForExtensionApi();
const windowManager = di.inject(windowManagerInjectable);
return windowManager.navigateExtension(this.id, pageId, params, frameId);
}
addCatalogSource(id: string, source: IObservableArray<CatalogEntity>) {

View File

@ -3,8 +3,12 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { WindowManager } from "../../main/window-manager";
import windowManagerInjectable from "../../main/window/manager.injectable";
import { getLegacyGlobalDiForExtensionApi } from "../as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
export function navigate(url: string) {
return WindowManager.getInstance().navigate(url);
const di = getLegacyGlobalDiForExtensionApi();
const windowManager = di.inject(windowManagerInjectable);
return windowManager.navigate(url);
}

View File

@ -38,22 +38,26 @@ import mockFs from "mock-fs";
import type { Cluster } from "../../common/cluster/cluster";
import { Kubectl } from "../kubectl/kubectl";
import { getDiForUnitTesting } from "../getDiForUnitTesting";
import type { ClusterModel } from "../../common/cluster-types";
import type { ClusterModel } from "../../common/cluster/types";
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
import authorizationReviewInjectable from "../../common/cluster/authorization-review.injectable";
import listNamespacesInjectable from "../../common/cluster/list-namespaces.injectable";
import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable";
import type { DiContainer } from "@ogre-tools/injectable";
import type { ContextHandler } from "../context-handler/context-handler";
import createKubeconfigManagerInjectable from "../kubeconfig-manager/create-kubeconfig-manager.injectable";
console = new Console(process.stdout, process.stderr); // fix mockFS
describe("create clusters", () => {
let di: DiContainer;
let cluster: Cluster;
let createCluster: (model: ClusterModel) => Cluster;
beforeEach(async () => {
jest.clearAllMocks();
const di = getDiForUnitTesting({ doGeneralOverrides: true });
di = getDiForUnitTesting({ doGeneralOverrides: true });
mockFs({
"minikube-config.yml": JSON.stringify({
@ -83,9 +87,11 @@ describe("create clusters", () => {
di.override(authorizationReviewInjectable, () => () => () => Promise.resolve(true));
di.override(listNamespacesInjectable, () => () => () => Promise.resolve([ "default" ]));
di.override(createContextHandlerInjectable, () => () => {
throw new Error("you should never come here");
});
di.override(createContextHandlerInjectable, () => () => ({
ensureServer: jest.fn(),
stopServer: jest.fn(),
}) as unknown as ContextHandler);
di.override(createKubeconfigManagerInjectable, () => () => undefined);
createCluster = di.inject(createClusterInjectionToken);
@ -121,11 +127,6 @@ describe("create clusters", () => {
kubeConfigPath: "minikube-config.yml",
});
cluster.contextHandler = {
ensureServer: jest.fn(),
stopServer: jest.fn(),
} as any;
jest.spyOn(cluster, "reconnect");
jest.spyOn(cluster, "refreshConnectionStatus");

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ClusterModel } from "../../common/cluster-types";
import type { ClusterModel } from "../../common/cluster/types";
jest.mock("winston", () => ({
format: {

View File

@ -31,7 +31,7 @@ jest.mock("winston", () => ({
}));
import { getDiForUnitTesting } from "../getDiForUnitTesting";
import { KubeconfigManager } from "../kubeconfig-manager/kubeconfig-manager";
import type { KubeconfigManager } from "../kubeconfig-manager/kubeconfig-manager";
import mockFs from "mock-fs";
import type { Cluster } from "../../common/cluster/cluster";
import fse from "fs-extra";
@ -43,13 +43,15 @@ import { createClusterInjectionToken } from "../../common/cluster/create-cluster
import directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable";
import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable";
import type { DiContainer } from "@ogre-tools/injectable";
import type { ContextHandler } from "../context-handler/context-handler";
import lensProxyPortInjectable from "../lens-proxy/port.injectable";
console = new Console(process.stdout, process.stderr); // fix mockFS
describe("kubeconfig manager tests", () => {
let clusterFake: Cluster;
let createKubeconfigManager: (cluster: Cluster) => KubeconfigManager;
let di: DiContainer;
let di: DiContainer;
beforeEach(async () => {
di = getDiForUnitTesting({ doGeneralOverrides: true });
@ -82,10 +84,11 @@ describe("kubeconfig manager tests", () => {
await di.runSetups();
di.override(createContextHandlerInjectable, () => () => {
throw new Error("you should never come here");
});
di.override(createContextHandlerInjectable, () => () => ({
ensureServer: async () => {},
stopServer: jest.fn(),
}) as unknown as ContextHandler);
di.inject(lensProxyPortInjectable).set(9191);
const createCluster = di.inject(createClusterInjectionToken);
createKubeconfigManager = di.inject(createKubeconfigManagerInjectable);
@ -95,12 +98,6 @@ describe("kubeconfig manager tests", () => {
contextName: "minikube",
kubeConfigPath: "minikube-config.yml",
});
clusterFake.contextHandler = {
ensureServer: () => Promise.resolve(),
} as any;
jest.spyOn(KubeconfigManager.prototype, "resolveProxyUrl", "get").mockReturnValue("http://127.0.0.1:9191/foo");
});
afterEach(() => {

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { isLongRunningRequest } from "../lens-proxy";
import { isLongRunningRequest } from "../lens-proxy/lens-proxy";
describe("isLongRunningRequest", () => {
it("returns true on watches", () => {

View File

@ -7,19 +7,15 @@ import { ObservableMap } from "mobx";
import type { CatalogEntity } from "../../../common/catalog";
import { loadFromOptions } from "../../../common/kube-helpers";
import type { Cluster } from "../../../common/cluster/cluster";
import { computeDiff as computeDiffFor, configToModels } from "../kubeconfig-sync-manager/kubeconfig-sync-manager";
import mockFs from "mock-fs";
import fs from "fs";
import { ClusterManager } from "../../cluster-manager";
import clusterStoreInjectable from "../../../common/cluster-store/cluster-store.injectable";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import { createClusterInjectionToken } from "../../../common/cluster/create-cluster-injection-token";
import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import { ClusterStore } from "../../../common/cluster-store/cluster-store";
import getConfigurationFileModelInjectable
from "../../../common/get-configuration-file-model/get-configuration-file-model.injectable";
import appVersionInjectable
from "../../../common/get-configuration-file-model/app-version/app-version.injectable";
import type { ComputeDiff } from "../kubeconfig-sync/compute-diff.injectable";
import computeDiffInjectable from "../kubeconfig-sync/compute-diff.injectable";
import type { ConfigToModels } from "../kubeconfig-sync/config-to-models.injectable";
import configToModelsInjectable from "../kubeconfig-sync/config-to-models.injectable";
import clusterStoreInjectable from "../../../common/cluster/store.injectable";
import type { ClusterStore } from "../../../common/cluster/store";
import mockFs from "mock-fs";
import { writeFileSync } from "fs";
jest.mock("electron", () => ({
app: {
@ -38,7 +34,8 @@ jest.mock("electron", () => ({
}));
describe("kubeconfig-sync.source tests", () => {
let computeDiff: ReturnType<typeof computeDiffFor>;
let computeDiff: ComputeDiff;
let configToModels: ConfigToModels;
beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
@ -47,27 +44,18 @@ describe("kubeconfig-sync.source tests", () => {
await di.runSetups();
computeDiff = computeDiffFor({
directoryForKubeConfigs: di.inject(directoryForKubeConfigsInjectable),
createCluster: di.inject(createClusterInjectionToken),
});
di.override(clusterStoreInjectable, () => ({
getById: jest.fn(),
addCluster: jest.fn(),
clustersList: [],
} as unknown as ClusterStore));
di.override(clusterStoreInjectable, () =>
ClusterStore.createInstance({ createCluster: () => null }),
);
di.permitSideEffects(getConfigurationFileModelInjectable);
di.permitSideEffects(appVersionInjectable);
di.inject(clusterStoreInjectable);
ClusterManager.createInstance();
computeDiff = di.inject(computeDiffInjectable);
configToModels = di.inject(configToModelsInjectable);
});
afterEach(() => {
mockFs.restore();
ClusterManager.resetInstance();
ClusterStore.resetInstance();
});
describe("configsToModels", () => {
@ -86,7 +74,7 @@ describe("kubeconfig-sync.source tests", () => {
const config = loadFromOptions({
clusters: [{
name: "cluster-name",
server: "1.2.3.4",
server: "https://1.2.3.4",
skipTLSVerify: false,
}],
users: [{
@ -124,7 +112,7 @@ describe("kubeconfig-sync.source tests", () => {
clusters: [{
name: "cluster-name",
cluster: {
server: "1.2.3.4",
server: "https://1.2.3.4",
},
skipTLSVerify: false,
}],
@ -149,7 +137,7 @@ describe("kubeconfig-sync.source tests", () => {
const rootSource = new ObservableMap<string, [Cluster, CatalogEntity]>();
const filePath = "/bar";
fs.writeFileSync(filePath, contents);
writeFileSync(filePath, contents);
computeDiff(contents, rootSource, filePath);
@ -166,7 +154,7 @@ describe("kubeconfig-sync.source tests", () => {
clusters: [{
name: "cluster-name",
cluster: {
server: "1.2.3.4",
server: "https://1.2.3.4",
},
skipTLSVerify: false,
}],
@ -192,7 +180,7 @@ describe("kubeconfig-sync.source tests", () => {
const rootSource = new ObservableMap<string, [Cluster, CatalogEntity]>();
const filePath = "/bar";
fs.writeFileSync(filePath, contents);
writeFileSync(filePath, contents);
computeDiff(contents, rootSource, filePath);
@ -213,7 +201,7 @@ describe("kubeconfig-sync.source tests", () => {
clusters: [{
name: "cluster-name",
cluster: {
server: "1.2.3.4",
server: "https://1.2.3.4",
},
skipTLSVerify: false,
}],
@ -246,7 +234,7 @@ describe("kubeconfig-sync.source tests", () => {
const rootSource = new ObservableMap<string, [Cluster, CatalogEntity]>();
const filePath = "/bar";
fs.writeFileSync(filePath, contents);
writeFileSync(filePath, contents);
computeDiff(contents, rootSource, filePath);
@ -263,7 +251,7 @@ describe("kubeconfig-sync.source tests", () => {
clusters: [{
name: "cluster-name",
cluster: {
server: "1.2.3.4",
server: "https://1.2.3.4",
},
skipTLSVerify: false,
}],

View File

@ -1,379 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { IComputedValue, ObservableMap } from "mobx";
import { action, observable, computed, runInAction, makeObservable, observe } from "mobx";
import type { CatalogEntity } from "../../../common/catalog";
import { catalogEntityRegistry } from "../../catalog";
import type { FSWatcher } from "chokidar";
import { watch } from "chokidar";
import fs from "fs";
import path from "path";
import type stream from "stream";
import type { Disposer } from "../../../common/utils";
import { bytesToUnits, getOrInsertWith, iter, noop } from "../../../common/utils";
import logger from "../../logger";
import type { KubeConfig } from "@kubernetes/client-node";
import { loadConfigFromString, splitConfig } from "../../../common/kube-helpers";
import { catalogEntityFromCluster, ClusterManager } from "../../cluster-manager";
import { UserStore } from "../../../common/user-store";
import { ClusterStore } from "../../../common/cluster-store/cluster-store";
import { createHash } from "crypto";
import { homedir } from "os";
import globToRegExp from "glob-to-regexp";
import { inspect } from "util";
import type { ClusterModel, UpdateClusterModel } from "../../../common/cluster-types";
import type { Cluster } from "../../../common/cluster/cluster";
const logPrefix = "[KUBECONFIG-SYNC]:";
/**
* This is the list of globs of which files are ignored when under a folder sync
*/
const ignoreGlobs = [
"*.lock", // kubectl lock files
"*.swp", // vim swap files
".DS_Store", // macOS specific
].map(rawGlob => ({
rawGlob,
matcher: globToRegExp(rawGlob),
}));
/**
* This should be much larger than any kubeconfig text file
*
* Even if you have a cert-file, key-file, and client-cert files that is only
* 12kb of extra data (at 4096 bytes each) which allows for around 150 entries.
*/
const folderSyncMaxAllowedFileReadSize = 2 * 1024 * 1024; // 2 MiB
const fileSyncMaxAllowedFileReadSize = 16 * folderSyncMaxAllowedFileReadSize; // 32 MiB
interface Dependencies {
directoryForKubeConfigs: string;
createCluster: (model: ClusterModel) => Cluster;
}
const kubeConfigSyncName = "lens:kube-sync";
export class KubeconfigSyncManager {
protected sources = observable.map<string, [IComputedValue<CatalogEntity[]>, Disposer]>();
protected syncing = false;
protected syncListDisposer?: Disposer;
constructor(private dependencies: Dependencies) {
makeObservable(this);
}
@action
startSync(): void {
if (this.syncing) {
return;
}
this.syncing = true;
logger.info(`${logPrefix} starting requested syncs`);
catalogEntityRegistry.addComputedSource(kubeConfigSyncName, computed(() => (
Array.from(iter.flatMap(
this.sources.values(),
([entities]) => entities.get(),
))
)));
// This must be done so that c&p-ed clusters are visible
this.startNewSync(this.dependencies.directoryForKubeConfigs);
for (const filePath of UserStore.getInstance().syncKubeconfigEntries.keys()) {
this.startNewSync(filePath);
}
this.syncListDisposer = observe(UserStore.getInstance().syncKubeconfigEntries, change => {
switch (change.type) {
case "add":
this.startNewSync(change.name);
break;
case "delete":
this.stopOldSync(change.name);
break;
}
});
}
@action
stopSync() {
this.syncListDisposer?.();
for (const filePath of this.sources.keys()) {
this.stopOldSync(filePath);
}
catalogEntityRegistry.removeSource(kubeConfigSyncName);
this.syncing = false;
}
@action
protected startNewSync(filePath: string): void {
if (this.sources.has(filePath)) {
// don't start a new sync if we already have one
return void logger.debug(`${logPrefix} already syncing file/folder`, { filePath });
}
this.sources.set(
filePath,
watchFileChanges(filePath, this.dependencies),
);
logger.info(`${logPrefix} starting sync of file/folder`, { filePath });
logger.debug(`${logPrefix} ${this.sources.size} files/folders watched`, { files: Array.from(this.sources.keys()) });
}
@action
protected stopOldSync(filePath: string): void {
if (!this.sources.delete(filePath)) {
// already stopped
return void logger.debug(`${logPrefix} no syncing file/folder to stop`, { filePath });
}
logger.info(`${logPrefix} stopping sync of file/folder`, { filePath });
logger.debug(`${logPrefix} ${this.sources.size} files/folders watched`, { files: Array.from(this.sources.keys()) });
}
}
// exported for testing
export function configToModels(rootConfig: KubeConfig, filePath: string): UpdateClusterModel[] {
const validConfigs = [];
for (const { config, error } of splitConfig(rootConfig)) {
if (error) {
logger.debug(`${logPrefix} context failed validation: ${error}`, { context: config.currentContext, filePath });
} else {
validConfigs.push({
kubeConfigPath: filePath,
contextName: config.currentContext,
});
}
}
return validConfigs;
}
type RootSourceValue = [Cluster, CatalogEntity];
type RootSource = ObservableMap<string, RootSourceValue>;
// exported for testing
export const computeDiff = ({ directoryForKubeConfigs, createCluster }: Dependencies) => (contents: string, source: RootSource, filePath: string): void => {
runInAction(() => {
try {
const { config, error } = loadConfigFromString(contents);
if (error) {
logger.warn(`${logPrefix} encountered errors while loading config: ${error.message}`, { filePath, details: error.details });
}
const rawModels = configToModels(config, filePath);
const models = new Map(rawModels.map(m => [m.contextName, m]));
logger.debug(`${logPrefix} File now has ${models.size} entries`, { filePath });
for (const [contextName, value] of source) {
const model = models.get(contextName);
// remove and disconnect clusters that were removed from the config
if (!model) {
// remove from the deleting set, so that if a new context of the same name is added, it isn't marked as deleting
ClusterManager.getInstance().deleting.delete(value[0].id);
value[0].disconnect();
source.delete(contextName);
logger.debug(`${logPrefix} Removed old cluster from sync`, { filePath, contextName });
continue;
}
// TODO: For the update check we need to make sure that the config itself hasn't changed.
// Probably should make it so that cluster keeps a copy of the config in its memory and
// diff against that
// or update the model and mark it as not needed to be added
value[0].updateModel(model);
models.delete(contextName);
logger.debug(`${logPrefix} Updated old cluster from sync`, { filePath, contextName });
}
for (const [contextName, model] of models) {
// add new clusters to the source
try {
const clusterId = createHash("md5").update(`${filePath}:${contextName}`).digest("hex");
const cluster = ClusterStore.getInstance().getById(clusterId) || createCluster({ ...model, id: clusterId });
if (!cluster.apiUrl) {
throw new Error("Cluster constructor failed, see above error");
}
const entity = catalogEntityFromCluster(cluster);
if (!filePath.startsWith(directoryForKubeConfigs)) {
entity.metadata.labels.file = filePath.replace(homedir(), "~");
}
source.set(contextName, [cluster, entity]);
logger.debug(`${logPrefix} Added new cluster from sync`, { filePath, contextName });
} catch (error) {
logger.warn(`${logPrefix} Failed to create cluster from model: ${error}`, { filePath, contextName });
}
}
} catch (error) {
console.log(error);
logger.warn(`${logPrefix} Failed to compute diff: ${error}`, { filePath });
source.clear(); // clear source if we have failed so as to not show outdated information
}
});
};
interface DiffChangedConfigArgs {
filePath: string;
source: RootSource;
stats: fs.Stats;
maxAllowedFileReadSize: number;
}
const diffChangedConfigFor = (dependencies: Dependencies) => ({ filePath, source, stats, maxAllowedFileReadSize }: DiffChangedConfigArgs): Disposer => {
logger.debug(`${logPrefix} file changed`, { filePath });
if (stats.size >= maxAllowedFileReadSize) {
logger.warn(`${logPrefix} skipping ${filePath}: size=${bytesToUnits(stats.size)} is larger than maxSize=${bytesToUnits(maxAllowedFileReadSize)}`);
source.clear();
return noop;
}
// TODO: replace with an AbortController with fs.readFile when we upgrade to Node 16 (after it comes out)
const fileReader = fs.createReadStream(filePath, {
mode: fs.constants.O_RDONLY,
});
const readStream: stream.Readable = fileReader;
const decoder = new TextDecoder("utf-8", { fatal: true });
let fileString = "";
let closed = false;
const cleanup = () => {
closed = true;
fileReader.close(); // This may not close the stream.
// Artificially marking end-of-stream, as if the underlying resource had
// indicated end-of-file by itself, allows the stream to close.
// This does not cancel pending read operations, and if there is such an
// operation, the process may still not be able to exit successfully
// until it finishes.
fileReader.push(null);
fileReader.read(0);
readStream.removeAllListeners();
};
readStream
.on("data", (chunk: Buffer) => {
try {
fileString += decoder.decode(chunk, { stream: true });
} catch (error) {
logger.warn(`${logPrefix} skipping ${filePath}: ${error}`);
source.clear();
cleanup();
}
})
.on("close", () => cleanup())
.on("error", error => {
cleanup();
logger.warn(`${logPrefix} failed to read file: ${error}`, { filePath });
})
.on("end", () => {
if (!closed) {
computeDiff(dependencies)(fileString, source, filePath);
}
});
return cleanup;
};
const watchFileChanges = (filePath: string, dependencies: Dependencies): [IComputedValue<CatalogEntity[]>, Disposer] => {
const rootSource = observable.map<string, ObservableMap<string, RootSourceValue>>();
const derivedSource = computed(() => Array.from(iter.flatMap(rootSource.values(), from => iter.map(from.values(), child => child[1]))));
let watcher: FSWatcher;
(async () => {
try {
const stat = await fs.promises.stat(filePath);
const isFolderSync = stat.isDirectory();
const cleanupFns = new Map<string, Disposer>();
const maxAllowedFileReadSize = isFolderSync
? folderSyncMaxAllowedFileReadSize
: fileSyncMaxAllowedFileReadSize;
watcher = watch(filePath, {
followSymlinks: true,
depth: isFolderSync ? 0 : 1, // DIRs works with 0 but files need 1 (bug: https://github.com/paulmillr/chokidar/issues/1095)
disableGlobbing: true,
ignorePermissionErrors: true,
usePolling: false,
awaitWriteFinish: {
pollInterval: 100,
stabilityThreshold: 1000,
},
atomic: 150, // for "atomic writes"
});
const diffChangedConfig = diffChangedConfigFor(dependencies);
watcher
.on("change", (childFilePath, stats) => {
const cleanup = cleanupFns.get(childFilePath);
if (!cleanup) {
// file was previously ignored, do nothing
return void logger.debug(`${logPrefix} ${inspect(childFilePath)} that should have been previously ignored has changed. Doing nothing`);
}
cleanup();
cleanupFns.set(childFilePath, diffChangedConfig({
filePath: childFilePath,
source: getOrInsertWith(rootSource, childFilePath, observable.map),
stats,
maxAllowedFileReadSize,
}));
})
.on("add", (childFilePath, stats) => {
if (isFolderSync) {
const fileName = path.basename(childFilePath);
for (const ignoreGlob of ignoreGlobs) {
if (ignoreGlob.matcher.test(fileName)) {
return void logger.info(`${logPrefix} ignoring ${inspect(childFilePath)} due to ignore glob: ${ignoreGlob.rawGlob}`);
}
}
}
cleanupFns.set(childFilePath, diffChangedConfig({
filePath: childFilePath,
source: getOrInsertWith(rootSource, childFilePath, observable.map),
stats,
maxAllowedFileReadSize,
}));
})
.on("unlink", (childFilePath) => {
cleanupFns.get(childFilePath)?.();
cleanupFns.delete(childFilePath);
rootSource.delete(childFilePath);
})
.on("error", error => logger.error(`${logPrefix} watching file/folder failed: ${error}`, { filePath }));
} catch (error) {
console.log(error.stack);
logger.warn(`${logPrefix} failed to start watching changes: ${error}`);
}
})();
return [derivedSource, () => {
watcher?.close();
}];
};

View File

@ -0,0 +1,103 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { createHash } from "crypto";
import type { ObservableMap } from "mobx";
import { runInAction } from "mobx";
import { homedir } from "os";
import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import type { CatalogEntity } from "../../../common/catalog";
import type { Cluster } from "../../../common/cluster/cluster";
import getClusterByIdInjectable from "../../../common/cluster/get-by-id.injectable";
import { loadConfigFromString } from "../../../common/kube-helpers";
import { catalogEntityFromCluster } from "../../cluster/manager";
import clusterManagerInjectable from "../../cluster/manager.injectable";
import createClusterInjectable from "../../create-cluster/create-cluster.injectable";
import configToModelsInjectable from "./config-to-models.injectable";
import kubeconfigSyncLoggerInjectable from "./logger.injectable";
export type ComputeDiff = (contents: string, source: ObservableMap<string, [Cluster, CatalogEntity]>, filePath: string) => void;
const computeDiffInjectable = getInjectable({
id: "compute-diff",
instantiate: (di): ComputeDiff => {
const logger = di.inject(kubeconfigSyncLoggerInjectable);
const configToModels = di.inject(configToModelsInjectable);
const directoryForKubeConfigs = di.inject(directoryForKubeConfigsInjectable);
const createCluster = di.inject(createClusterInjectable);
const clusterManager = di.inject(clusterManagerInjectable);
const getClusterById = di.inject(getClusterByIdInjectable);
return (contents, source, filePath) => {
runInAction(() => {
try {
const { config, error } = loadConfigFromString(contents);
if (error) {
logger.warn(`encountered errors while loading config: ${error.message}`, { filePath, details: error.details });
}
const rawModels = configToModels(config, filePath);
const models = new Map(rawModels.map(m => [m.contextName, m]));
logger.debug(`File now has ${models.size} entries`, { filePath });
for (const [contextName, value] of source) {
const model = models.get(contextName);
// remove and disconnect clusters that were removed from the config
if (!model) {
// remove from the deleting set, so that if a new context of the same name is added, it isn't marked as deleting
clusterManager.deleting.delete(value[0].id);
value[0].disconnect();
source.delete(contextName);
logger.debug(`Removed old cluster from sync`, { filePath, contextName });
continue;
}
// TODO: For the update check we need to make sure that the config itself hasn't changed.
// Probably should make it so that cluster keeps a copy of the config in its memory and
// diff against that
// or update the model and mark it as not needed to be added
value[0].updateModel(model);
models.delete(contextName);
logger.debug(`Updated old cluster from sync`, { filePath, contextName });
}
for (const [contextName, model] of models) {
// add new clusters to the source
try {
const clusterId = createHash("md5").update(`${filePath}:${contextName}`).digest("hex");
const cluster = getClusterById(clusterId) || createCluster({ ...model, id: clusterId });
if (!cluster.apiUrl) {
throw new Error("Cluster constructor failed, see above error");
}
const entity = catalogEntityFromCluster(cluster);
if (!filePath.startsWith(directoryForKubeConfigs)) {
entity.metadata.labels.file = filePath.replace(homedir(), "~");
}
source.set(contextName, [cluster, entity]);
logger.debug(`Added new cluster from sync`, { filePath, contextName });
} catch (error) {
logger.warn(`Failed to create cluster from model: ${error}`, { filePath, contextName });
}
}
} catch (error) {
console.log(error);
logger.warn(`Failed to compute diff: ${error}`, { filePath });
source.clear(); // clear source if we have failed so as to not show outdated information
}
});
};
},
});
export default computeDiffInjectable;

View File

@ -0,0 +1,37 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { KubeConfig } from "@kubernetes/client-node";
import { getInjectable } from "@ogre-tools/injectable";
import kubeconfigSyncLoggerInjectable from "./logger.injectable";
import type { UpdateClusterModel } from "../../../common/cluster/types";
import { splitConfig } from "../../../common/kube-helpers";
export type ConfigToModels = (rootConfig: KubeConfig, filePath: string) => UpdateClusterModel[];
const configToModelsInjectable = getInjectable({
id: "config-to-models",
instantiate: (di): ConfigToModels => {
const logger = di.inject(kubeconfigSyncLoggerInjectable);
return (rootConfig, filePath) => {
const validConfigs = [];
for (const { config, error } of splitConfig(rootConfig)) {
if (error) {
logger.debug(`context failed validation: ${error}`, { context: config.currentContext, filePath });
} else {
validConfigs.push({
kubeConfigPath: filePath,
contextName: config.currentContext,
});
}
}
return validConfigs;
};
},
});
export default configToModelsInjectable;

View File

@ -0,0 +1,90 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { Stats } from "fs";
import { constants, createReadStream } from "fs";
import type { ObservableMap } from "mobx";
import type { CatalogEntity } from "../../../common/catalog";
import type { Cluster } from "../../../common/cluster/cluster";
import type { Disposer } from "../../../common/utils";
import { bytesToUnits, noop } from "../../../common/utils";
import kubeconfigSyncLoggerInjectable from "./logger.injectable";
import type stream from "stream";
import computeDiffInjectable from "./compute-diff.injectable";
export interface DiffChangedConfigArgs {
filePath: string;
source: ObservableMap<string, [Cluster, CatalogEntity]>;
stats: Stats;
maxAllowedFileReadSize: number;
}
export type DiffChangedConfig = (args: DiffChangedConfigArgs) => Disposer;
const diffChangedConfigInjectable = getInjectable({
id: "diff-changed-config",
instantiate: (di): DiffChangedConfig => {
const logger = di.inject(kubeconfigSyncLoggerInjectable);
const computeDiff = di.inject(computeDiffInjectable);
return ({ filePath, source, stats, maxAllowedFileReadSize }) => {
logger.debug(`file changed`, { filePath });
if (stats.size >= maxAllowedFileReadSize) {
logger.warn(`skipping ${filePath}: size=${bytesToUnits(stats.size)} is larger than maxSize=${bytesToUnits(maxAllowedFileReadSize)}`);
source.clear();
return noop;
}
// TODO: replace with an AbortController with fs.readFile when we upgrade to Node 16 (after it comes out)
const fileReader = createReadStream(filePath, {
mode: constants.O_RDONLY,
});
const readStream: stream.Readable = fileReader;
const decoder = new TextDecoder("utf-8", { fatal: true });
let fileString = "";
let closed = false;
const cleanup = () => {
closed = true;
fileReader.close(); // This may not close the stream.
// Artificially marking end-of-stream, as if the underlying resource had
// indicated end-of-file by itself, allows the stream to close.
// This does not cancel pending read operations, and if there is such an
// operation, the process may still not be able to exit successfully
// until it finishes.
fileReader.push(null);
fileReader.read(0);
readStream.removeAllListeners();
};
readStream
.on("data", (chunk: Buffer) => {
try {
fileString += decoder.decode(chunk, { stream: true });
} catch (error) {
logger.warn(`skipping ${filePath}: ${error}`);
source.clear();
cleanup();
}
})
.on("close", () => cleanup())
.on("error", error => {
cleanup();
logger.warn(`failed to read file: ${error}`, { filePath });
})
.on("end", () => {
if (!closed) {
computeDiff(fileString, source, filePath);
}
});
return cleanup;
};
},
});
export default diffChangedConfigInjectable;

View File

@ -0,0 +1,15 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import childLoggerInjectable from "../../../common/logger/child-logger.injectable";
const kubeconfigSyncLoggerInjectable = getInjectable({
id: "kubeconfig-sync-logger",
instantiate: (di) => di.inject(childLoggerInjectable, {
prefix: "KUBECONFIG-SYNC",
}),
});
export default kubeconfigSyncLoggerInjectable;

View File

@ -4,15 +4,17 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import { KubeconfigSyncManager } from "./kubeconfig-sync-manager";
import { createClusterInjectionToken } from "../../../common/cluster/create-cluster-injection-token";
import kubeconfigSyncLoggerInjectable from "./logger.injectable";
import { KubeconfigSyncManager } from "./manager";
import watchFileChangesInjectable from "./watch-file-changes.injectable";
const kubeconfigSyncManagerInjectable = getInjectable({
id: "kubeconfig-sync-manager",
instantiate: (di) => new KubeconfigSyncManager({
directoryForKubeConfigs: di.inject(directoryForKubeConfigsInjectable),
createCluster: di.inject(createClusterInjectionToken),
logger: di.inject(kubeconfigSyncLoggerInjectable),
watchFileChanges: di.inject(watchFileChangesInjectable),
}),
});

View File

@ -0,0 +1,105 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { action, observable, computed, makeObservable, observe } from "mobx";
import { catalogEntityRegistry } from "../../catalog";
import type { Disposer } from "../../../common/utils";
import { iter } from "../../../common/utils";
import { UserStore } from "../../../common/user-store";
import type { Logger } from "../../../common/logger";
import type { ChangesResult, WatchFileChanges } from "./watch-file-changes.injectable";
interface Dependencies {
readonly directoryForKubeConfigs: string;
readonly logger: Logger;
watchFileChanges: WatchFileChanges;
}
const kubeConfigSyncName = "lens:kube-sync";
export class KubeconfigSyncManager {
protected readonly sources = observable.map<string, ChangesResult>();
protected syncing = false;
protected syncListDisposer?: Disposer;
constructor(protected readonly dependencies: Dependencies) {
makeObservable(this);
}
@action
startSync(): void {
if (this.syncing) {
return;
}
this.syncing = true;
this.dependencies.logger.info(`starting requested syncs`);
catalogEntityRegistry.addComputedSource(kubeConfigSyncName, computed(() => (
Array.from(iter.flatMap(
this.sources.values(),
({ source }) => source.get(),
))
)));
// This must be done so that c&p-ed clusters are visible
this.startNewSync(this.dependencies.directoryForKubeConfigs);
for (const filePath of UserStore.getInstance().syncKubeconfigEntries.keys()) {
this.startNewSync(filePath);
}
this.syncListDisposer = observe(UserStore.getInstance().syncKubeconfigEntries, change => {
switch (change.type) {
case "add":
this.startNewSync(change.name);
break;
case "delete":
this.stopOldSync(change.name);
break;
}
});
}
@action
stopSync() {
this.syncListDisposer?.();
for (const filePath of this.sources.keys()) {
this.stopOldSync(filePath);
}
catalogEntityRegistry.removeSource(kubeConfigSyncName);
this.syncing = false;
}
@action
protected startNewSync(filePath: string): void {
if (this.sources.has(filePath)) {
// don't start a new sync if we already have one
return void this.dependencies.logger.debug(`already syncing file/folder`, { filePath });
}
this.sources.set(
filePath,
this.dependencies.watchFileChanges(filePath),
);
this.dependencies.logger.info(`starting sync of file/folder`, { filePath });
this.dependencies.logger.debug(`${this.sources.size} files/folders watched`, { files: Array.from(this.sources.keys()) });
}
@action
protected stopOldSync(filePath: string): void {
if (!this.sources.delete(filePath)) {
// already stopped
return void this.dependencies.logger.debug(`no syncing file/folder to stop`, { filePath });
}
this.dependencies.logger.info(`stopping sync of file/folder`, { filePath });
this.dependencies.logger.debug(`${this.sources.size} files/folders watched`, { files: Array.from(this.sources.keys()) });
}
}

View File

@ -0,0 +1,140 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Cluster } from "@kubernetes/client-node";
import { getInjectable } from "@ogre-tools/injectable";
import type { FSWatcher } from "chokidar";
import { watch } from "chokidar";
import GlobToRegExp from "glob-to-regexp";
import type { IComputedValue, ObservableMap } from "mobx";
import { computed, observable } from "mobx";
import path from "path";
import { inspect } from "util";
import type { CatalogEntity } from "../../../common/catalog";
import fsInjectable from "../../../common/fs/fs.injectable";
import { iter, type Disposer, getOrInsertWith } from "../../../common/utils";
import diffChangedConfigInjectable from "./diff-changed-config.injectable";
import kubeconfigSyncLoggerInjectable from "./logger.injectable";
/**
* This is the list of globs of which files are ignored when under a folder sync
*/
const ignoreGlobs = [
"*.lock", // kubectl lock files
"*.swp", // vim swap files
".DS_Store", // macOS specific
].map(rawGlob => ({
rawGlob,
matcher: GlobToRegExp(rawGlob),
}));
/**
* This should be much larger than any kubeconfig text file
*
* Even if you have a cert-file, key-file, and client-cert files that is only
* 12kb of extra data (at 4096 bytes each) which allows for around 150 entries.
*/
const folderSyncMaxAllowedFileReadSize = 2 * 1024 * 1024; // 2 MiB
const fileSyncMaxAllowedFileReadSize = 16 * folderSyncMaxAllowedFileReadSize; // 32 MiB
export interface ChangesResult {
stopWatching: () => void;
source: IComputedValue<CatalogEntity[]>;
}
export type WatchFileChanges = (filePath: string) => ChangesResult;
const watchFileChangesInjectable = getInjectable({
id: "watch-file-changes",
instantiate: (di): WatchFileChanges => {
const logger = di.inject(kubeconfigSyncLoggerInjectable);
const { stat } = di.inject(fsInjectable);
const diffChangedConfig = di.inject(diffChangedConfigInjectable);
return (filePath) => {
const rootSource = observable.map<string, ObservableMap<string, [Cluster, CatalogEntity]>>();
const derivedSource = computed(() => Array.from(iter.flatMap(rootSource.values(), from => iter.map(from.values(), child => child[1]))));
let watcher: FSWatcher;
(async () => {
try {
const stats = await stat(filePath);
const isFolderSync = stats.isDirectory();
const cleanupFns = new Map<string, Disposer>();
const maxAllowedFileReadSize = isFolderSync
? folderSyncMaxAllowedFileReadSize
: fileSyncMaxAllowedFileReadSize;
watcher = watch(filePath, {
followSymlinks: true,
depth: isFolderSync ? 0 : 1, // DIRs works with 0 but files need 1 (bug: https://github.com/paulmillr/chokidar/issues/1095)
disableGlobbing: true,
ignorePermissionErrors: true,
usePolling: false,
awaitWriteFinish: {
pollInterval: 100,
stabilityThreshold: 1000,
},
atomic: 150, // for "atomic writes"
});
watcher
.on("change", (childFilePath, stats) => {
const cleanup = cleanupFns.get(childFilePath);
if (!cleanup) {
// file was previously ignored, do nothing
return void logger.debug(`${inspect(childFilePath)} that should have been previously ignored has changed. Doing nothing`);
}
cleanup();
cleanupFns.set(childFilePath, diffChangedConfig({
filePath: childFilePath,
source: getOrInsertWith(rootSource, childFilePath, observable.map),
stats,
maxAllowedFileReadSize,
}));
})
.on("add", (childFilePath, stats) => {
if (isFolderSync) {
const fileName = path.basename(childFilePath);
for (const ignoreGlob of ignoreGlobs) {
if (ignoreGlob.matcher.test(fileName)) {
return void logger.info(`ignoring ${inspect(childFilePath)} due to ignore glob: ${ignoreGlob.rawGlob}`);
}
}
}
cleanupFns.set(childFilePath, diffChangedConfig({
filePath: childFilePath,
source: getOrInsertWith(rootSource, childFilePath, observable.map),
stats,
maxAllowedFileReadSize,
}));
})
.on("unlink", (childFilePath) => {
cleanupFns.get(childFilePath)?.();
cleanupFns.delete(childFilePath);
rootSource.delete(childFilePath);
})
.on("error", error => logger.error(`watching file/folder failed: ${error}`, { filePath }));
} catch (error) {
console.log(error.stack);
logger.warn(`failed to start watching changes: ${error}`);
}
})();
return {
source: derivedSource,
stopWatching: () => {
watcher?.close();
},
};
};
},
});
export default watchFileChangesInjectable;

View File

@ -5,12 +5,11 @@
import { getInjectable } from "@ogre-tools/injectable";
import { spawn } from "child_process";
export type Spawn = typeof spawn;
const spawnInjectable = getInjectable({
id: "spawn",
instantiate: () => {
return spawn;
},
instantiate: (): Spawn => spawn,
causesSideEffects: true,
});

View File

@ -4,8 +4,8 @@
*/
import type { RequestPromiseOptions } from "request-promise-native";
import type { Cluster } from "../../common/cluster/cluster";
import { k8sRequest } from "../k8s-request";
import type { Cluster } from "../../../common/cluster/cluster";
import { k8sRequest } from "../../k8s-request";
export interface ClusterDetectionResult {
value: string | number | boolean;

View File

@ -5,7 +5,7 @@
import { BaseClusterDetector } from "./base-cluster-detector";
import { createHash } from "crypto";
import { ClusterMetadataKey } from "../../common/cluster-types";
import { ClusterMetadataKey } from "../../../common/cluster/types";
export class ClusterIdDetector extends BaseClusterDetector {
key = ClusterMetadataKey.CLUSTER_ID;

View File

@ -4,9 +4,9 @@
*/
import { observable } from "mobx";
import type { ClusterMetadata } from "../../common/cluster-types";
import { Singleton } from "../../common/utils";
import type { Cluster } from "../../common/cluster/cluster";
import type { ClusterMetadata } from "../../../common/cluster/types";
import { Singleton } from "../../../common/utils";
import type { Cluster } from "../../../common/cluster/cluster";
import type { BaseClusterDetector, ClusterDetectionResult } from "./base-cluster-detector";
export class DetectorRegistry extends Singleton {

View File

@ -4,7 +4,7 @@
*/
import { BaseClusterDetector } from "./base-cluster-detector";
import { ClusterMetadataKey } from "../../common/cluster-types";
import { ClusterMetadataKey } from "../../../common/cluster/types";
export class DistributionDetector extends BaseClusterDetector {
key = ClusterMetadataKey.DISTRIBUTION;

View File

@ -4,7 +4,7 @@
*/
import { BaseClusterDetector } from "./base-cluster-detector";
import { ClusterMetadataKey } from "../../common/cluster-types";
import { ClusterMetadataKey } from "../../../common/cluster/types";
export class LastSeenDetector extends BaseClusterDetector {
key = ClusterMetadataKey.LAST_SEEN;

View File

@ -4,7 +4,7 @@
*/
import { BaseClusterDetector } from "./base-cluster-detector";
import { ClusterMetadataKey } from "../../common/cluster-types";
import { ClusterMetadataKey } from "../../../common/cluster/types";
export class NodesCountDetector extends BaseClusterDetector {
key = ClusterMetadataKey.NODES_COUNT;

View File

@ -4,7 +4,7 @@
*/
import { BaseClusterDetector } from "./base-cluster-detector";
import { ClusterMetadataKey } from "../../common/cluster-types";
import { ClusterMetadataKey } from "../../../common/cluster/types";
export class VersionDetector extends BaseClusterDetector {
key = ClusterMetadataKey.VERSION;

View File

@ -0,0 +1,38 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { IncomingMessage } from "http";
import clusterStoreInjectable from "../../common/cluster/store.injectable";
import type { Cluster } from "../../common/cluster/cluster";
import { getClusterIdFromHost } from "../../common/utils";
import { apiKubePrefix } from "../../common/vars";
export type GetClusterForRequest = (req: IncomingMessage) => Cluster | undefined;
const getClusterForRequestInjectable = getInjectable({
id: "get-cluster-for-request",
instantiate: (di): GetClusterForRequest => {
const clusterStore = di.inject(clusterStoreInjectable);
return (req) => {
// lens-server is connecting to 127.0.0.1:<port>/<uid>
if (req.headers.host.startsWith("127.0.0.1")) {
const clusterId = req.url.split("/")[1];
const cluster = clusterStore.getById(clusterId);
if (cluster) {
// we need to swap path prefix so that request is proxied to kube api
req.url = req.url.replace(`/${clusterId}`, apiKubePrefix);
}
return cluster;
}
return clusterStore.getById(getClusterIdFromHost(req.headers.host));
};
},
});
export default getClusterForRequestInjectable;

View File

@ -0,0 +1,15 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import childLoggerInjectable from "../../common/logger/child-logger.injectable";
const clusterManagerLoggerInjectable = getInjectable({
id: "cluster-manager-logger",
instantiate: (di) => di.inject(childLoggerInjectable, {
prefix: "CLUSTER-MANAGER",
}),
});
export default clusterManagerLoggerInjectable;

View File

@ -0,0 +1,18 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import clusterStoreInjectable from "../../common/cluster/store.injectable";
import { ClusterManager } from "./manager";
import clusterManagerLoggerInjectable from "./manager-logger.injectable";
const clusterManagerInjectable = getInjectable({
id: "cluster-manager",
instantiate: (di) => new ClusterManager({
clusterStore: di.inject(clusterStoreInjectable),
logger: di.inject(clusterManagerLoggerInjectable),
}),
});
export default clusterManagerInjectable;

View File

@ -3,48 +3,46 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import "../common/ipc/cluster";
import type http from "http";
import "../../common/ipc/cluster";
import { action, makeObservable, observable, observe, reaction, toJS } from "mobx";
import type { Cluster } from "../common/cluster/cluster";
import logger from "./logger";
import { apiKubePrefix } from "../common/vars";
import { getClusterIdFromHost, Singleton } from "../common/utils";
import { catalogEntityRegistry } from "./catalog";
import type { KubernetesClusterPrometheusMetrics } from "../common/catalog-entities/kubernetes-cluster";
import { KubernetesCluster, LensKubernetesClusterStatus } from "../common/catalog-entities/kubernetes-cluster";
import { ipcMainOn } from "../common/ipc";
import type { Cluster } from "../../common/cluster/cluster";
import { catalogEntityRegistry } from "../catalog";
import type { KubernetesClusterPrometheusMetrics } from "../../common/catalog-entities/kubernetes-cluster";
import { KubernetesCluster, LensKubernetesClusterStatus } from "../../common/catalog-entities/kubernetes-cluster";
import { ipcMainOn } from "../../common/ipc";
import { once } from "lodash";
import { ClusterStore } from "../common/cluster-store/cluster-store";
import type { ClusterId } from "../common/cluster-types";
const logPrefix = "[CLUSTER-MANAGER]:";
import type { ClusterId } from "../../common/cluster/types";
import type { ClusterStore } from "../../common/cluster/store";
import type { Logger } from "../../common/logger";
const lensSpecificClusterStatuses: Set<string> = new Set(Object.values(LensKubernetesClusterStatus));
export class ClusterManager extends Singleton {
private store = ClusterStore.getInstance();
deleting = observable.set<ClusterId>();
interface Dependencies {
readonly clusterStore: ClusterStore;
readonly logger: Logger;
}
export class ClusterManager {
readonly deleting = observable.set<ClusterId>();
@observable visibleCluster: ClusterId | undefined = undefined;
constructor() {
super();
constructor(protected readonly dependencies: Dependencies) {
makeObservable(this);
}
init = once(() => {
// reacting to every cluster's state change and total amount of items
reaction(
() => this.store.clustersList.map(c => c.getState()),
() => this.updateCatalog(this.store.clustersList),
() => this.dependencies.clusterStore.clustersList.map(c => c.getState()),
() => this.updateCatalog(this.dependencies.clusterStore.clustersList),
{ fireImmediately: false },
);
// reacting to every cluster's preferences change and total amount of items
reaction(
() => this.store.clustersList.map(c => toJS(c.preferences)),
() => this.updateCatalog(this.store.clustersList),
() => this.dependencies.clusterStore.clustersList.map(c => toJS(c.preferences)),
() => this.updateCatalog(this.dependencies.clusterStore.clustersList),
{ fireImmediately: false },
);
@ -78,7 +76,7 @@ export class ClusterManager extends Singleton {
@action
protected updateCatalog(clusters: Cluster[]) {
logger.debug("[CLUSTER-MANAGER]: updating catalog from cluster store");
this.dependencies.logger.debug("updating catalog from cluster store");
for (const cluster of clusters) {
this.updateEntityFromCluster(cluster);
@ -170,7 +168,7 @@ export class ClusterManager extends Singleton {
@action
protected syncClustersFromCatalog(entities: KubernetesCluster[]) {
for (const entity of entities) {
const cluster = this.store.getById(entity.getId());
const cluster = this.dependencies.clusterStore.getById(entity.getId());
if (!cluster) {
const model = {
@ -185,12 +183,12 @@ export class ClusterManager extends Singleton {
* Add the bare minimum of data to ClusterStore. And especially no
* preferences, as those might be configured by the entity's source
*/
this.store.addCluster(model);
this.dependencies.clusterStore.addCluster(model);
} catch (error) {
if (error.code === "ENOENT" && error.path === entity.spec.kubeconfigPath) {
logger.warn(`${logPrefix} kubeconfig file disappeared`, model);
this.dependencies.logger.warn(`kubeconfig file disappeared`, model);
} else {
logger.error(`${logPrefix} failed to add cluster: ${error}`, model);
this.dependencies.logger.error(`failed to add cluster: ${error}`, model);
}
}
} else {
@ -223,8 +221,8 @@ export class ClusterManager extends Singleton {
}
protected onNetworkOffline = () => {
logger.info(`${logPrefix} network is offline`);
this.store.clustersList.forEach((cluster) => {
this.dependencies.logger.info(`network is offline`);
this.dependencies.clusterStore.clustersList.forEach((cluster) => {
if (!cluster.disconnected) {
cluster.online = false;
cluster.accessible = false;
@ -234,8 +232,8 @@ export class ClusterManager extends Singleton {
};
protected onNetworkOnline = () => {
logger.info(`${logPrefix} network is online`);
this.store.clustersList.forEach((cluster) => {
this.dependencies.logger.info(`network is online`);
this.dependencies.clusterStore.clustersList.forEach((cluster) => {
if (!cluster.disconnected) {
cluster.refreshConnectionStatus().catch((e) => e);
}
@ -243,27 +241,10 @@ export class ClusterManager extends Singleton {
};
stop() {
this.store.clusters.forEach((cluster: Cluster) => {
this.dependencies.clusterStore.clusters.forEach((cluster: Cluster) => {
cluster.disconnect();
});
}
getClusterForRequest(req: http.IncomingMessage): Cluster {
// lens-server is connecting to 127.0.0.1:<port>/<uid>
if (req.headers.host.startsWith("127.0.0.1")) {
const clusterId = req.url.split("/")[1];
const cluster = this.store.getById(clusterId);
if (cluster) {
// we need to swap path prefix so that request is proxied to kube api
req.url = req.url.replace(`/${clusterId}`, apiKubePrefix);
}
return cluster;
}
return this.store.getById(getClusterIdFromHost(req.headers.host));
}
}
export function catalogEntityFromCluster(cluster: Cluster) {

View File

@ -5,7 +5,7 @@
import type { PrometheusProvider, PrometheusService } from "../prometheus/provider-registry";
import { PrometheusProviderRegistry } from "../prometheus/provider-registry";
import type { ClusterPrometheusPreferences } from "../../common/cluster-types";
import type { ClusterPrometheusPreferences } from "../../common/cluster/types";
import type { Cluster } from "../../common/cluster/cluster";
import type httpProxy from "http-proxy";
import type { UrlWithStringQuery } from "url";
@ -14,6 +14,8 @@ import { CoreV1Api } from "@kubernetes/client-node";
import logger from "../logger";
import type { KubeAuthProxy } from "../kube-auth-proxy/kube-auth-proxy";
import type { CreateKubeAuthProxy } from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
import type { GetKubeAuthProxyCertificate } from "../kube-auth-proxy/get-proxy-cert.injectable";
import type { SelfSignedCert } from "selfsigned";
export interface PrometheusDetails {
prometheusPath: string;
@ -27,21 +29,23 @@ interface PrometheusServicePreferences {
prefix: string;
}
interface Dependencies {
export interface ContextHandlerDependencies {
createKubeAuthProxy: CreateKubeAuthProxy;
authProxyCa: string;
getKubeAuthProxyCertificate: GetKubeAuthProxyCertificate;
}
export class ContextHandler {
public clusterUrl: UrlWithStringQuery;
public readonly clusterUrl: UrlWithStringQuery;
protected kubeAuthProxy?: KubeAuthProxy;
protected apiTarget?: httpProxy.ServerOptions;
protected prometheusProvider?: string;
protected prometheus?: PrometheusServicePreferences;
private readonly proxyCert: SelfSignedCert;
constructor(private dependencies: Dependencies, protected cluster: Cluster) {
constructor(protected readonly dependencies: ContextHandlerDependencies, protected readonly cluster: Cluster) {
this.clusterUrl = url.parse(cluster.apiUrl);
this.setupPrometheus(cluster.preferences);
this.proxyCert = this.dependencies.getKubeAuthProxyCertificate(new URL(cluster.apiUrl).hostname);
}
public setupPrometheus(preferences: ClusterPrometheusPreferences = {}) {
@ -125,7 +129,7 @@ export class ContextHandler {
}
resolveAuthProxyCa() {
return this.dependencies.authProxyCa;
return this.proxyCert.cert;
}
async getApiTarget(isLongRunningRequest = false): Promise<httpProxy.ServerOptions> {
@ -141,7 +145,6 @@ export class ContextHandler {
protected async newApiTarget(timeout: number): Promise<httpProxy.ServerOptions> {
await this.ensureServer();
const ca = this.dependencies.authProxyCa;
const clusterPath = this.clusterUrl.path !== "/" ? this.clusterUrl.path : "";
const apiPrefix = `${this.kubeAuthProxy.apiPrefix}${clusterPath}`;
@ -151,7 +154,7 @@ export class ContextHandler {
host: "127.0.0.1",
port: this.kubeAuthProxy.port,
path: apiPrefix,
ca,
ca: this.resolveAuthProxyCa(),
},
changeOrigin: true,
timeout,

View File

@ -3,27 +3,24 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import selfsigned from "selfsigned";
import type { Cluster } from "../../common/cluster/cluster";
import type { ContextHandlerDependencies } from "./context-handler";
import { ContextHandler } from "./context-handler";
import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
import { getKubeAuthProxyCertificate } from "../kube-auth-proxy/get-kube-auth-proxy-certificate";
import URLParse from "url-parse";
import getKubeAuthProxyCertificateInjectable from "../kube-auth-proxy/get-proxy-cert.injectable";
export type CreateContextHandler = (cluster: Cluster) => ContextHandler | undefined;
const createContextHandlerInjectable = getInjectable({
id: "create-context-handler",
instantiate: (di) => {
return (cluster: Cluster) => {
const clusterUrl = new URLParse(cluster.apiUrl);
const dependencies = {
createKubeAuthProxy: di.inject(createKubeAuthProxyInjectable),
authProxyCa: getKubeAuthProxyCertificate(clusterUrl.hostname, selfsigned.generate).cert,
};
return new ContextHandler(dependencies, cluster);
instantiate: (di): CreateContextHandler => {
const dependencies: ContextHandlerDependencies = {
createKubeAuthProxy: di.inject(createKubeAuthProxyInjectable),
getKubeAuthProxyCertificate: di.inject(getKubeAuthProxyCertificateInjectable),
};
return (cluster) => new ContextHandler(dependencies, cluster);
},
});

View File

@ -1,23 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { app } from "electron";
import { WindowManager } from "./window-manager";
import { appEventBus } from "../common/app-event-bus/event-bus";
import { ClusterManager } from "./cluster-manager";
import logger from "./logger";
export function exitApp() {
const windowManager = WindowManager.getInstance(false);
const clusterManager = ClusterManager.getInstance(false);
appEventBus.emit({ name: "service", action: "close" });
windowManager?.hide();
clusterManager?.stop();
logger.info("SERVICE:QUIT");
setTimeout(() => {
app.exit();
}, 1000);
}

View File

@ -16,14 +16,13 @@ import writeJsonFileInjectable from "../common/fs/write-json-file.injectable";
import readJsonFileInjectable from "../common/fs/read-json-file.injectable";
import readFileInjectable from "../common/fs/read-file.injectable";
import directoryForBundledBinariesInjectable from "../common/app-paths/directory-for-bundled-binaries/directory-for-bundled-binaries.injectable";
import loggerInjectable from "../common/logger.injectable";
import spawnInjectable from "./child-process/spawn.injectable";
import extensionsStoreInjectable from "../extensions/extensions-store/extensions-store.injectable";
import type { ExtensionsStore } from "../extensions/extensions-store/extensions-store";
import fileSystemProvisionerStoreInjectable from "../extensions/extension-loader/create-extension-instance/file-system-provisioner-store/file-system-provisioner-store.injectable";
import type { FileSystemProvisionerStore } from "../extensions/extension-loader/create-extension-instance/file-system-provisioner-store/file-system-provisioner-store";
import clusterStoreInjectable from "../common/cluster-store/cluster-store.injectable";
import type { ClusterStore } from "../common/cluster-store/cluster-store";
import clusterStoreInjectable from "../common/cluster/store.injectable";
import type { ClusterStore } from "../common/cluster/store";
import type { Cluster } from "../common/cluster/cluster";
import userStoreInjectable from "../common/user-store/user-store.injectable";
import type { UserStore } from "../common/user-store";
@ -35,6 +34,7 @@ import { getAbsolutePathFake } from "../common/test-utils/get-absolute-path-fake
import joinPathsInjectable from "../common/path/join-paths.injectable";
import { joinPathsFake } from "../common/test-utils/join-paths-fake";
import hotbarStoreInjectable from "../common/hotbar-store.injectable";
import loggerInjectable from "./logger/logger.injectable";
export const getDiForUnitTesting = (
{ doGeneralOverrides } = { doGeneralOverrides: false },
@ -107,7 +107,7 @@ export const getDiForUnitTesting = (
di.override(loggerInjectable, () => ({
warn: noop,
debug: noop,
error: (message: string, ...args: any) => console.error(message, ...args),
error: noop,
info: noop,
}));
}

View File

@ -7,18 +7,13 @@
import { injectSystemCAs } from "../common/system-ca";
import * as Mobx from "mobx";
import httpProxy from "http-proxy";
import * as LensExtensionsCommonApi from "../extensions/common-api";
import * as LensExtensionsMainApi from "../extensions/main-api";
import { app, autoUpdater, dialog, powerMonitor } from "electron";
import { appName, isIntegrationTesting, isMac, isWindows, productName, staticFilesDirectory } from "../common/vars";
import { LensProxy } from "./lens-proxy";
import { WindowManager } from "./window-manager";
import { ClusterManager } from "./cluster-manager";
import { shellSync } from "./shell-sync";
import { mangleProxyEnv } from "./proxy-env";
import { registerFileProtocol } from "../common/register-protocol";
import logger from "./logger";
import { appEventBus } from "../common/app-event-bus/event-bus";
import type { InstalledExtension } from "../extensions/extension-discovery/extension-discovery";
import type { LensExtensionId } from "../extensions/lens-extension";
@ -38,30 +33,29 @@ import { WeblinkStore } from "../common/weblink-store";
import { initializeSentryReporting } from "../common/sentry";
import { ensureDir } from "fs-extra";
import { initMenu } from "./menu/menu";
import { kubeApiUpgradeRequest } from "./proxy-functions";
import { initTray } from "./tray/tray";
import { ShellSession } from "./shell-session/shell-session";
import { getDi } from "./getDi";
import extensionLoaderInjectable from "../extensions/extension-loader/extension-loader.injectable";
import lensProtocolRouterMainInjectable from "./protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable";
import lensProtocolRouterMainInjectable from "./protocol-handler/router.injectable";
import extensionDiscoveryInjectable from "../extensions/extension-discovery/extension-discovery.injectable";
import directoryForExesInjectable from "../common/app-paths/directory-for-exes/directory-for-exes.injectable";
import initIpcMainHandlersInjectable from "./initializers/init-ipc-main-handlers/init-ipc-main-handlers.injectable";
import directoryForKubeConfigsInjectable from "../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import kubeconfigSyncManagerInjectable from "./catalog-sources/kubeconfig-sync-manager/kubeconfig-sync-manager.injectable";
import clusterStoreInjectable from "../common/cluster-store/cluster-store.injectable";
import routerInjectable from "./router/router.injectable";
import shellApiRequestInjectable from "./proxy-functions/shell-api-request/shell-api-request.injectable";
import kubeconfigSyncManagerInjectable from "./catalog-sources/kubeconfig-sync/manager.injectable";
import userStoreInjectable from "../common/user-store/user-store.injectable";
import trayMenuItemsInjectable from "./tray/tray-menu-items.injectable";
import { broadcastNativeThemeOnUpdate } from "./native-theme";
import windowManagerInjectable from "./window-manager.injectable";
import navigateToPreferencesInjectable from "../common/front-end-routing/routes/preferences/navigate-to-preferences.injectable";
import windowManagerInjectable from "./window/manager.injectable";
import syncGeneralCatalogEntitiesInjectable from "./catalog-sources/sync-general-catalog-entities.injectable";
import hotbarStoreInjectable from "../common/hotbar-store.injectable";
import applicationMenuItemsInjectable from "./menu/application-menu-items.injectable";
import type { DiContainer } from "@ogre-tools/injectable";
import { init } from "@sentry/electron/main";
import loggerInjectable from "./logger/logger.injectable";
import clusterManagerInjectable from "./cluster/manager.injectable";
import lensProxyInjectable from "./lens-proxy/proxy.injectable";
import lensProxyPortInjectable from "./lens-proxy/port.injectable";
import clusterStoreInjectable from "../common/cluster/store.injectable";
import initTrayInjectable from "./tray/init-tray.injectable";
async function main(di: DiContainer) {
app.setName(appName);
@ -77,6 +71,7 @@ async function main(di: DiContainer) {
const onCloseCleanup = disposer();
const onQuitCleanup = disposer();
const logger = di.inject(loggerInjectable);
logger.info(`📟 Setting ${productName} as protocol client for lens://`);
@ -120,6 +115,9 @@ async function main(di: DiContainer) {
broadcastNativeThemeOnUpdate();
logger.info("🖥️ Starting WindowManager");
const windowManager = di.inject(windowManagerInjectable);
app.on("second-instance", (event, argv) => {
logger.debug("second-instance message");
@ -129,14 +127,14 @@ async function main(di: DiContainer) {
}
}
WindowManager.getInstance(false)?.ensureMainWindow();
windowManager.ensureMainWindow();
});
app.on("activate", (event, hasVisibleWindows) => {
logger.info("APP:ACTIVATE", { hasVisibleWindows });
if (!hasVisibleWindows) {
WindowManager.getInstance(false)?.ensureMainWindow(false);
windowManager.ensureMainWindow(false);
}
});
@ -150,6 +148,8 @@ async function main(di: DiContainer) {
blockQuit = false;
});
const clusterManager = di.inject(clusterManagerInjectable);
app.on("will-quit", (event) => {
logger.debug("will-quit message");
@ -158,7 +158,7 @@ async function main(di: DiContainer) {
logger.info("APP:QUIT");
appEventBus.emit({ name: "app", action: "close" });
ClusterManager.getInstance(false)?.stop(); // close cluster connections
clusterManager.stop(); // close cluster connections
const kubeConfigSyncManager = di.inject(kubeconfigSyncManagerInjectable);
@ -232,17 +232,10 @@ async function main(di: DiContainer) {
HelmRepoManager.createInstance(); // create the instance
const router = di.inject(routerInjectable);
const shellApiRequest = di.inject(shellApiRequestInjectable);
const lensProxy = LensProxy.createInstance(router, httpProxy.createProxy(), {
getClusterForRequest: (req) => ClusterManager.getInstance().getClusterForRequest(req),
kubeApiUpgradeRequest,
shellApiRequest,
});
ClusterManager.createInstance().init();
const lensProxy = di.inject(lensProxyInjectable);
const proxyPort = di.inject(lensProxyPortInjectable);
clusterManager.init();
initializers.initClusterMetadataDetectors();
try {
@ -257,7 +250,7 @@ async function main(di: DiContainer) {
// test proxy connection
try {
logger.info("🔎 Testing LensProxy connection ...");
const versionFromProxy = await getAppVersionFromProxyServer(lensProxy.port);
const versionFromProxy = await getAppVersionFromProxyServer(proxyPort.get());
if (getAppVersion() !== versionFromProxy) {
logger.error("Proxy server responded with invalid response");
@ -296,16 +289,12 @@ async function main(di: DiContainer) {
// (On Windows and Linux, we get a flag. On MacOS, we get special API.)
const startHidden = process.argv.includes("--hidden") || (isMac && app.getLoginItemSettings().wasOpenedAsHidden);
logger.info("🖥️ Starting WindowManager");
const windowManager = di.inject(windowManagerInjectable);
const applicationMenuItems = di.inject(applicationMenuItemsInjectable);
const trayMenuItems = di.inject(trayMenuItemsInjectable);
const navigateToPreferences = di.inject(navigateToPreferencesInjectable);
const initTray = di.inject(initTrayInjectable);
onQuitCleanup.push(
initMenu(applicationMenuItems),
await initTray(windowManager, trayMenuItems, navigateToPreferences),
await initTray(),
() => ShellSession.cleanup(),
);

View File

@ -3,12 +3,12 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { ClusterIdDetector } from "../cluster-detectors/cluster-id-detector";
import { DetectorRegistry } from "../cluster-detectors/detector-registry";
import { DistributionDetector } from "../cluster-detectors/distribution-detector";
import { LastSeenDetector } from "../cluster-detectors/last-seen-detector";
import { NodesCountDetector } from "../cluster-detectors/nodes-count-detector";
import { VersionDetector } from "../cluster-detectors/version-detector";
import { ClusterIdDetector } from "../cluster/detectors/cluster-id-detector";
import { DetectorRegistry } from "../cluster/detectors/detector-registry";
import { DistributionDetector } from "../cluster/detectors/distribution-detector";
import { LastSeenDetector } from "../cluster/detectors/last-seen-detector";
import { NodesCountDetector } from "../cluster/detectors/nodes-count-detector";
import { VersionDetector } from "../cluster/detectors/version-detector";
export function initClusterMetadataDetectors() {
DetectorRegistry.createInstance()

View File

@ -7,6 +7,10 @@ import directoryForLensLocalStorageInjectable from "../../../common/directory-fo
import { initIpcMainHandlers } from "./init-ipc-main-handlers";
import getAbsolutePathInjectable from "../../../common/path/get-absolute-path.injectable";
import applicationMenuItemsInjectable from "../../menu/application-menu-items.injectable";
import appEventBusInjectable from "../../../common/app-event-bus/app-event-bus.injectable";
import clusterManagerInjectable from "../../cluster/manager.injectable";
import clusterStoreInjectable from "../../../common/cluster/store.injectable";
import getClusterByIdInjectable from "../../../common/cluster/get-by-id.injectable";
const initIpcMainHandlersInjectable = getInjectable({
id: "init-ipc-main-handlers",
@ -15,6 +19,10 @@ const initIpcMainHandlersInjectable = getInjectable({
applicationMenuItems: di.inject(applicationMenuItemsInjectable),
directoryForLensLocalStorage: di.inject(directoryForLensLocalStorageInjectable),
getAbsolutePath: di.inject(getAbsolutePathInjectable),
appEventBus: di.inject(appEventBusInjectable),
clusterManager: di.inject(clusterManagerInjectable),
clusterStore: di.inject(clusterStoreInjectable),
getClusterById: di.inject(getClusterByIdInjectable),
}),
});

View File

@ -7,13 +7,12 @@ import type { IpcMainInvokeEvent } from "electron";
import { BrowserWindow, Menu } from "electron";
import { clusterFrameMap } from "../../../common/cluster-frames";
import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterDeleteHandler, clusterSetDeletingHandler, clusterClearDeletingHandler } from "../../../common/ipc/cluster";
import type { ClusterId } from "../../../common/cluster-types";
import { ClusterStore } from "../../../common/cluster-store/cluster-store";
import { appEventBus } from "../../../common/app-event-bus/event-bus";
import type { ClusterId } from "../../../common/cluster/types";
import type { AppEventBus } from "../../../common/app-event-bus/event-bus";
import { broadcastMainChannel, broadcastMessage, ipcMainHandle, ipcMainOn } from "../../../common/ipc";
import { catalogEntityRegistry } from "../../catalog";
import { pushCatalogToRenderer } from "../../catalog-pusher";
import { ClusterManager } from "../../cluster-manager";
import type { ClusterManager } from "../../cluster/manager";
import { ResourceApplier } from "../../resource-applier";
import { remove } from "fs-extra";
import { onLocationChange, handleWindowAction } from "../../ipc/window";
@ -25,22 +24,35 @@ import { getNativeThemeChannel } from "../../../common/ipc/native-theme";
import type { GetAbsolutePath } from "../../../common/path/get-absolute-path.injectable";
import type { IComputedValue } from "mobx";
import type { MenuItemOpts } from "../../menu/application-menu-items.injectable";
import type { GetClusterById } from "../../../common/cluster/get-by-id.injectable";
import type { ClusterStore } from "../../../common/cluster/store";
interface Dependencies {
directoryForLensLocalStorage: string;
getAbsolutePath: GetAbsolutePath;
applicationMenuItems: IComputedValue<MenuItemOpts[]>;
clusterManager: ClusterManager;
getClusterById: GetClusterById;
appEventBus: AppEventBus;
clusterStore: ClusterStore;
}
export const initIpcMainHandlers = ({ applicationMenuItems, directoryForLensLocalStorage, getAbsolutePath }: Dependencies) => () => {
export const initIpcMainHandlers = ({
applicationMenuItems,
directoryForLensLocalStorage,
getAbsolutePath,
clusterManager,
getClusterById,
appEventBus,
clusterStore,
}: Dependencies) => () => {
ipcMainHandle(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => {
return ClusterStore.getInstance()
.getById(clusterId)
return getClusterById(clusterId)
?.activate(force);
});
ipcMainHandle(clusterSetFrameIdHandler, (event: IpcMainInvokeEvent, clusterId: ClusterId) => {
const cluster = ClusterStore.getInstance().getById(clusterId);
const cluster = getClusterById(clusterId);
if (cluster) {
clusterFrameMap.set(cluster.id, { frameId: event.frameId, processId: event.processId });
@ -51,18 +63,17 @@ export const initIpcMainHandlers = ({ applicationMenuItems, directoryForLensLoca
});
ipcMainOn(clusterVisibilityHandler, (event, clusterId?: ClusterId) => {
ClusterManager.getInstance().visibleCluster = clusterId;
clusterManager.visibleCluster = clusterId;
});
ipcMainHandle(clusterRefreshHandler, (event, clusterId: ClusterId) => {
return ClusterStore.getInstance()
.getById(clusterId)
return getClusterById(clusterId)
?.refresh({ refreshMetadata: true });
});
ipcMainHandle(clusterDisconnectHandler, (event, clusterId: ClusterId) => {
appEventBus.emit({ name: "cluster", action: "stop" });
const cluster = ClusterStore.getInstance().getById(clusterId);
const cluster = getClusterById(clusterId);
if (cluster) {
cluster.disconnect();
@ -73,8 +84,7 @@ export const initIpcMainHandlers = ({ applicationMenuItems, directoryForLensLoca
ipcMainHandle(clusterDeleteHandler, async (event, clusterId: ClusterId) => {
appEventBus.emit({ name: "cluster", action: "remove" });
const clusterStore = ClusterStore.getInstance();
const cluster = clusterStore.getById(clusterId);
const cluster = getClusterById(clusterId);
if (!cluster) {
return;
@ -97,16 +107,16 @@ export const initIpcMainHandlers = ({ applicationMenuItems, directoryForLensLoca
});
ipcMainHandle(clusterSetDeletingHandler, (event, clusterId: string) => {
ClusterManager.getInstance().deleting.add(clusterId);
clusterManager.deleting.add(clusterId);
});
ipcMainHandle(clusterClearDeletingHandler, (event, clusterId: string) => {
ClusterManager.getInstance().deleting.delete(clusterId);
clusterManager.deleting.delete(clusterId);
});
ipcMainHandle(clusterKubectlApplyAllHandler, async (event, clusterId: ClusterId, resources: string[], extraArgs: string[]) => {
appEventBus.emit({ name: "cluster", action: "kubectl-apply-all" });
const cluster = ClusterStore.getInstance().getById(clusterId);
const cluster = getClusterById(clusterId);
if (cluster) {
const applier = new ResourceApplier(cluster);
@ -125,7 +135,7 @@ export const initIpcMainHandlers = ({ applicationMenuItems, directoryForLensLoca
ipcMainHandle(clusterKubectlDeleteAllHandler, async (event, clusterId: ClusterId, resources: string[], extraArgs: string[]) => {
appEventBus.emit({ name: "cluster", action: "kubectl-delete-all" });
const cluster = ClusterStore.getInstance().getById(clusterId);
const cluster = getClusterById(clusterId);
if (cluster) {
const applier = new ResourceApplier(cluster);

View File

@ -7,11 +7,14 @@ import type { RequestPromiseOptions } from "request-promise-native";
import request from "request-promise-native";
import { apiKubePrefix } from "../common/vars";
import type { IMetricsReqParams } from "../common/k8s-api/endpoints/metrics.api";
import { LensProxy } from "./lens-proxy";
import type { Cluster } from "../common/cluster/cluster";
import { Environments, getEnvironmentSpecificLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
import lensProxyPortInjectable from "./lens-proxy/port.injectable";
export async function k8sRequest<T = any>(cluster: Cluster, path: string, options: RequestPromiseOptions = {}): Promise<T> {
const kubeProxyUrl = `http://localhost:${LensProxy.getInstance().port}${apiKubePrefix}`;
const di = getEnvironmentSpecificLegacyGlobalDiForExtensionApi(Environments.main);
const proxyPort = di.inject(lensProxyPortInjectable);
const kubeProxyUrl = `http://localhost:${proxyPort.get()}${apiKubePrefix}`;
options.headers ??= {};
options.json ??= true;

View File

@ -0,0 +1,13 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { SelfSignedCert } from "selfsigned";
const kubeAuthCertCacheInjectable = getInjectable({
id: "kube-auth-cert-cache",
instantiate: () => new Map<string, SelfSignedCert>(),
});
export default kubeAuthCertCacheInjectable;

View File

@ -7,11 +7,10 @@ import type { KubeAuthProxyDependencies } from "./kube-auth-proxy";
import { KubeAuthProxy } from "./kube-auth-proxy";
import type { Cluster } from "../../common/cluster/cluster";
import path from "path";
import selfsigned from "selfsigned";
import { getBinaryName } from "../../common/vars";
import directoryForBundledBinariesInjectable from "../../common/app-paths/directory-for-bundled-binaries/directory-for-bundled-binaries.injectable";
import spawnInjectable from "../child-process/spawn.injectable";
import { getKubeAuthProxyCertificate } from "./get-kube-auth-proxy-certificate";
import getKubeAuthProxyCertificateInjectable from "./get-proxy-cert.injectable";
export type CreateKubeAuthProxy = (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => KubeAuthProxy;
@ -20,17 +19,13 @@ const createKubeAuthProxyInjectable = getInjectable({
instantiate: (di): CreateKubeAuthProxy => {
const binaryName = getBinaryName("lens-k8s-proxy");
return (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => {
const clusterUrl = new URL(cluster.apiUrl);
const dependencies: KubeAuthProxyDependencies = {
proxyBinPath: path.join(di.inject(directoryForBundledBinariesInjectable), binaryName),
proxyCert: getKubeAuthProxyCertificate(clusterUrl.hostname, selfsigned.generate),
spawn: di.inject(spawnInjectable),
};
return new KubeAuthProxy(dependencies, cluster, environmentVariables);
const dependencies: KubeAuthProxyDependencies = {
proxyBinPath: path.join(di.inject(directoryForBundledBinariesInjectable), binaryName),
getKubeAuthProxyCertificate: di.inject(getKubeAuthProxyCertificateInjectable),
spawn: di.inject(spawnInjectable),
};
return (cluster, envVars) => new KubeAuthProxy(dependencies, cluster, envVars);
},
});

View File

@ -0,0 +1,13 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { generate } from "selfsigned";
const generateCertificateInjectable = getInjectable({
id: "generate-certificate",
instantiate: () => generate,
});
export default generateCertificateInjectable;

View File

@ -1,39 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type * as selfsigned from "selfsigned";
type SelfSignedGenerate = typeof selfsigned.generate;
const certCache: Map<string, selfsigned.SelfSignedCert> = new Map();
export function getKubeAuthProxyCertificate(hostname: string, generate: SelfSignedGenerate, useCache = true): selfsigned.SelfSignedCert {
if (useCache && certCache.has(hostname)) {
return certCache.get(hostname);
}
const opts = [
{ name: "commonName", value: "Lens Certificate Authority" },
{ name: "organizationName", value: "Lens" },
];
const cert = generate(opts, {
keySize: 2048,
algorithm: "sha256",
days: 365,
extensions: [
{ name: "basicConstraints", cA: true },
{ name: "subjectAltName", altNames: [
{ type: 2, value: hostname },
{ type: 2, value: "localhost" },
{ type: 7, ip: "127.0.0.1" },
] },
],
});
certCache.set(hostname, cert);
return cert;
}

View File

@ -0,0 +1,43 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { SelfSignedCert } from "selfsigned";
import { getOrInsertWith } from "../../common/utils";
import kubeAuthCertCacheInjectable from "./cert-cache.injectable";
import generateCertificateInjectable from "./generate-cert.injectable";
export type GetKubeAuthProxyCertificate = (hostname: string) => SelfSignedCert;
const getKubeAuthProxyCertificateInjectable = getInjectable({
id: "get-kube-auth-proxy-certificate",
instantiate: (di): GetKubeAuthProxyCertificate => {
const cache = di.inject(kubeAuthCertCacheInjectable);
const generate = di.inject(generateCertificateInjectable);
return (hostname) => getOrInsertWith(cache, hostname, () => (
generate(
[
{ name: "commonName", value: "Lens Certificate Authority" },
{ name: "organizationName", value: "Lens" },
],
{
keySize: 2048,
algorithm: "sha256",
days: 365,
extensions: [
{ name: "basicConstraints", cA: true },
{ name: "subjectAltName", altNames: [
{ type: 2, value: hostname },
{ type: 2, value: "localhost" },
{ type: 7, ip: "127.0.0.1" },
] },
],
},
)
));
},
});
export default getKubeAuthProxyCertificateInjectable;

View File

@ -3,21 +3,24 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ChildProcess, spawn } from "child_process";
import type { ChildProcess } from "child_process";
import { waitUntilUsed } from "tcp-port-used";
import { randomBytes } from "crypto";
import type { Cluster } from "../../common/cluster/cluster";
import logger from "../logger";
import { getPortFrom } from "../utils/get-port";
import { makeObservable, observable, when } from "mobx";
import type { Spawn } from "../child-process/spawn.injectable";
import type { GetKubeAuthProxyCertificate } from "./get-proxy-cert.injectable";
import type { SelfSignedCert } from "selfsigned";
import { URL } from "url";
const startingServeRegex = /starting to serve on (?<address>.+)/i;
export interface KubeAuthProxyDependencies {
proxyBinPath: string;
proxyCert: SelfSignedCert;
spawn: typeof spawn;
readonly proxyBinPath: string;
spawn: Spawn;
getKubeAuthProxyCertificate: GetKubeAuthProxyCertificate;
}
export class KubeAuthProxy {
@ -30,9 +33,12 @@ export class KubeAuthProxy {
protected _port: number;
protected proxyProcess?: ChildProcess;
@observable protected ready = false;
private readonly proxyCert: SelfSignedCert;
constructor(private dependencies: KubeAuthProxyDependencies, protected readonly cluster: Cluster, protected readonly env: NodeJS.ProcessEnv) {
constructor(protected readonly dependencies: KubeAuthProxyDependencies, protected readonly cluster: Cluster, protected readonly env: NodeJS.ProcessEnv) {
makeObservable(this);
this.proxyCert = this.dependencies.getKubeAuthProxyCertificate(new URL(cluster.apiUrl).hostname);
}
get whenReady() {
@ -45,7 +51,6 @@ export class KubeAuthProxy {
}
const proxyBin = this.dependencies.proxyBinPath;
const cert = this.dependencies.proxyCert;
this.proxyProcess = this.dependencies.spawn(proxyBin, [], {
env: {
@ -53,8 +58,8 @@ export class KubeAuthProxy {
KUBECONFIG: this.cluster.kubeConfigPath,
KUBECONFIG_CONTEXT: this.cluster.contextName,
API_PREFIX: this.apiPrefix,
PROXY_KEY: cert.private,
PROXY_CERT: cert.cert,
PROXY_KEY: this.proxyCert.private,
PROXY_CERT: this.proxyCert.cert,
},
});
this.proxyProcess.on("error", (error) => {

View File

@ -5,21 +5,26 @@
import { getInjectable } from "@ogre-tools/injectable";
import type { Cluster } from "../../common/cluster/cluster";
import directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable";
import type { KubeconfigManagerDependencies } from "./kubeconfig-manager";
import { KubeconfigManager } from "./kubeconfig-manager";
import lensProxyPortInjectable from "../lens-proxy/port.injectable";
export interface KubeConfigManagerInstantiationParameter {
cluster: Cluster;
}
export type CreateKubeConfigManager = (cluster: Cluster) => KubeconfigManager | undefined;
const createKubeconfigManagerInjectable = getInjectable({
id: "create-kubeconfig-manager",
instantiate: (di) => {
const dependencies = {
instantiate: (di): CreateKubeConfigManager => {
const dependencies: KubeconfigManagerDependencies = {
directoryForTemp: di.inject(directoryForTempInjectable),
proxyPort: di.inject(lensProxyPortInjectable),
};
return (cluster: Cluster) => new KubeconfigManager(dependencies, cluster);
return (cluster) => new KubeconfigManager(dependencies, cluster);
},
});

View File

@ -5,15 +5,16 @@
import type { KubeConfig } from "@kubernetes/client-node";
import type { Cluster } from "../../common/cluster/cluster";
import type { ContextHandler } from "../context-handler/context-handler";
import path from "path";
import fs from "fs-extra";
import { dumpConfigYaml } from "../../common/kube-helpers";
import logger from "../logger";
import { LensProxy } from "../lens-proxy";
import type { IObservableValue } from "mobx";
import { waitUntilSet } from "../../common/utils/wait-observable-value";
interface Dependencies {
directoryForTemp: string;
export interface KubeconfigManagerDependencies {
readonly directoryForTemp: string;
readonly proxyPort: IObservableValue<number | undefined>;
}
export class KubeconfigManager {
@ -26,11 +27,7 @@ export class KubeconfigManager {
*/
protected tempFilePath: string | null | undefined = null;
protected contextHandler: ContextHandler;
constructor(private dependencies: Dependencies, protected cluster: Cluster) {
this.contextHandler = cluster.contextHandler;
}
constructor(protected readonly dependencies: KubeconfigManagerDependencies, protected readonly cluster: Cluster) {}
/**
*
@ -71,17 +68,13 @@ export class KubeconfigManager {
protected async ensureFile() {
try {
await this.contextHandler.ensureServer();
await this.cluster.contextHandler.ensureServer();
this.tempFilePath = await this.createProxyKubeconfig();
} catch (error) {
throw Object.assign(new Error("Failed to creat temp config for auth-proxy"), { cause: error });
throw new Error(`Failed to creat temp config for auth-proxy: ${error}`);
}
}
get resolveProxyUrl() {
return `http://127.0.0.1:${LensProxy.getInstance().port}/${this.cluster.id}`;
}
/**
* Creates new "temporary" kubeconfig that point to the kubectl-proxy.
* This way any user of the config does not need to know anything about the auth etc. details.
@ -94,12 +87,13 @@ export class KubeconfigManager {
`kubeconfig-${id}`,
);
const kubeConfig = await cluster.getKubeconfig();
const proxyPort = await waitUntilSet(this.dependencies.proxyPort);
const proxyConfig: Partial<KubeConfig> = {
currentContext: contextName,
clusters: [
{
name: contextName,
server: this.resolveProxyUrl,
server: `http://127.0.0.1:${proxyPort}/${this.cluster.id}`,
skipTLSVerify: undefined,
},
],

View File

@ -7,17 +7,17 @@ import { chunk } from "lodash";
import tls from "tls";
import url from "url";
import { apiKubePrefix } from "../../common/vars";
import type { ProxyApiRequestArgs } from "./types";
import type { LensProxyRequestHandler } from "./lens-proxy";
const skipRawHeaders = new Set(["Host", "Authorization"]);
export async function kubeApiUpgradeRequest({ req, socket, head, cluster }: ProxyApiRequestArgs) {
export const kubeApiUpgradeRequest: LensProxyRequestHandler = async ({ req, socket, head, cluster }) => {
const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, "");
const proxyCa = await cluster.contextHandler.resolveAuthProxyCa();
const proxyCa = cluster.contextHandler.resolveAuthProxyCa();
const apiUrl = url.parse(cluster.apiUrl);
const pUrl = url.parse(proxyUrl);
const connectOpts = {
port: parseInt(pUrl.port),
const connectOpts = {
port: parseInt(pUrl.port),
host: pUrl.hostname,
ca: proxyCa,
};
@ -62,4 +62,4 @@ export async function kubeApiUpgradeRequest({ req, socket, head, cluster }: Prox
socket.on("error", function () {
proxySocket.end();
});
}
};

View File

@ -7,24 +7,25 @@ import net from "net";
import type http from "http";
import spdy from "spdy";
import type httpProxy from "http-proxy";
import { apiPrefix, apiKubePrefix } from "../common/vars";
import type { Router } from "./router/router";
import type { ContextHandler } from "./context-handler/context-handler";
import logger from "./logger";
import { Singleton } from "../common/utils";
import type { Cluster } from "../common/cluster/cluster";
import type { ProxyApiRequestArgs } from "./proxy-functions";
import { appEventBus } from "../common/app-event-bus/event-bus";
import { getBoolean } from "./utils/parse-query";
import { apiPrefix, apiKubePrefix } from "../../common/vars";
import type { Router } from "../router/router";
import type { ContextHandler } from "../context-handler/context-handler";
import { appEventBus } from "../../common/app-event-bus/event-bus";
import { getBoolean } from "../utils/parse-query";
import type { GetClusterForRequest } from "../cluster/get-cluster-for-request.injectable";
import type { IObservableValue } from "mobx";
import type { Logger } from "../../common/logger";
import type { Cluster } from "../../common/cluster/cluster";
type GetClusterForRequest = (req: http.IncomingMessage) => Cluster | null;
export interface LensProxyFunctions {
getClusterForRequest: GetClusterForRequest;
shellApiRequest: (args: ProxyApiRequestArgs) => void | Promise<void>;
kubeApiUpgradeRequest: (args: ProxyApiRequestArgs) => void | Promise<void>;
export interface ProxyApiRequestArgs {
req: http.IncomingMessage;
socket: net.Socket;
head: Buffer;
cluster: Cluster;
}
export type LensProxyRequestHandler = (args: ProxyApiRequestArgs) => void;
const watchParam = "watch";
const followParam = "follow";
@ -52,22 +53,24 @@ const disallowedPorts = new Set([
10080,
]);
export class LensProxy extends Singleton {
interface Dependencies {
readonly router: Router;
readonly proxy: httpProxy;
readonly proxyPort: IObservableValue<number>;
readonly logger: Logger;
getClusterForRequest: GetClusterForRequest;
shellApiRequest: LensProxyRequestHandler;
kubeApiUpgradeRequest: LensProxyRequestHandler;
}
export class LensProxy {
protected origin: string;
protected proxyServer: http.Server;
protected closed = false;
protected retryCounters = new Map<string, number>();
protected getClusterForRequest: GetClusterForRequest;
public port: number;
constructor(protected router: Router, protected proxy: httpProxy, { shellApiRequest, kubeApiUpgradeRequest, getClusterForRequest }: LensProxyFunctions) {
super();
this.configureProxy(proxy);
this.getClusterForRequest = getClusterForRequest;
protected readonly retryCounters = new Map<string, number>();
constructor(protected readonly dependencies: Dependencies) {
this.configureProxy(this.dependencies.proxy);
this.proxyServer = spdy.createServer({
spdy: {
plain: true,
@ -79,17 +82,22 @@ export class LensProxy extends Singleton {
this.proxyServer
.on("upgrade", (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => {
const cluster = getClusterForRequest(req);
const cluster = this.dependencies.getClusterForRequest(req);
if (!cluster) {
logger.error(`[LENS-PROXY]: Could not find cluster for upgrade request from url=${req.url}`);
this.dependencies.logger.error(`Could not find cluster for upgrade request from url=${req.url}`);
socket.destroy();
} else {
const isInternal = req.url.startsWith(`${apiPrefix}?`);
const reqHandler = isInternal ? shellApiRequest : kubeApiUpgradeRequest;
const reqHandler = isInternal
? this.dependencies.shellApiRequest
: this.dependencies.kubeApiUpgradeRequest;
(async () => reqHandler({ req, socket, head, cluster }))()
.catch(error => logger.error("[LENS-PROXY]: failed to handle proxy upgrade", error));
try {
reqHandler({ req, socket, head, cluster });
} catch (error) {
this.dependencies.logger.error("failed to handle proxy upgrade", error);
}
}
});
}
@ -110,18 +118,18 @@ export class LensProxy extends Singleton {
const { address, port } = this.proxyServer.address() as net.AddressInfo;
logger.info(`[LENS-PROXY]: Proxy server has started at ${address}:${port}`);
this.dependencies.logger.info(`Proxy server has started at ${address}:${port}`);
this.proxyServer.on("error", (error) => {
logger.info(`[LENS-PROXY]: Subsequent error: ${error}`);
this.dependencies.logger.info(`Subsequent error: ${error}`);
});
this.port = port;
this.dependencies.proxyPort.set(port);
appEventBus.emit({ name: "lens-proxy", action: "listen", params: { port }});
resolve(port);
})
.once("error", (error) => {
logger.info(`[LENS-PROXY]: Proxy server failed to start: ${error}`);
this.dependencies.logger.info(`Proxy server failed to start: ${error}`);
reject(error);
});
});
@ -144,7 +152,7 @@ export class LensProxy extends Singleton {
return;
}
logger.warn(`[LENS-PROXY]: Proxy server has with port known to be considered unsafe to connect to by chrome, restarting...`);
this.dependencies.logger.warn(`Proxy server has with port known to be considered unsafe to connect to by chrome, restarting...`);
if (seenPorts.has(port)) {
/**
@ -160,7 +168,7 @@ export class LensProxy extends Singleton {
}
close() {
logger.info("[LENS-PROXY]: Closing server");
this.dependencies.logger.info("Closing server");
this.proxyServer.close();
this.closed = true;
}
@ -183,10 +191,10 @@ export class LensProxy extends Singleton {
return;
}
logger.error(`[LENS-PROXY]: http proxy errored for cluster: ${error}`, { url: req.url });
this.dependencies.logger.error(`http proxy errored for cluster: ${error}`, { url: req.url });
if (target) {
logger.debug(`Failed proxy to target: ${JSON.stringify(target, null, 2)}`);
this.dependencies.logger.debug(`Failed proxy to target: ${JSON.stringify(target, null, 2)}`);
if (req.method === "GET" && (!res.statusCode || res.statusCode >= 500)) {
const reqId = this.getRequestId(req);
@ -194,11 +202,11 @@ export class LensProxy extends Singleton {
const timeoutMs = retryCount * 250;
if (retryCount < 20) {
logger.debug(`Retrying proxy request to url: ${reqId}`);
this.dependencies.logger.debug(`Retrying proxy request to url: ${reqId}`);
setTimeout(() => {
this.retryCounters.set(reqId, retryCount + 1);
this.handleRequest(req, res)
.catch(error => logger.error(`[LENS-PROXY]: failed to handle request on proxy error: ${error}`));
.catch(error => this.dependencies.logger.error(`failed to handle request on proxy error: ${error}`));
}, timeoutMs);
}
}
@ -207,7 +215,7 @@ export class LensProxy extends Singleton {
try {
res.writeHead(500).end(`Oops, something went wrong.\n${error}`);
} catch (e) {
logger.error(`[LENS-PROXY]: Failed to write headers: `, e);
this.dependencies.logger.error(`Failed to write headers: `, e);
}
});
@ -228,15 +236,16 @@ export class LensProxy extends Singleton {
}
protected async handleRequest(req: http.IncomingMessage, res: http.ServerResponse) {
const cluster = this.getClusterForRequest(req);
const cluster = this.dependencies.getClusterForRequest(req);
if (cluster) {
const proxyTarget = await this.getProxyTarget(req, cluster.contextHandler);
if (proxyTarget) {
return this.proxy.web(req, res, proxyTarget);
return this.dependencies.proxy.web(req, res, proxyTarget);
}
}
this.router.route(cluster, req, res);
this.dependencies.router.route(cluster, req, res);
}
}

View File

@ -0,0 +1,15 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import childLoggerInjectable from "../../common/logger/child-logger.injectable";
const lensProxyLoggerInjectable = getInjectable({
id: "lens-proxy-logger",
instantiate: (di) => di.inject(childLoggerInjectable, {
prefix: "LENS-PROXY",
}),
});
export default lensProxyLoggerInjectable;

View File

@ -0,0 +1,13 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { observable } from "mobx";
const lensProxyPortInjectable = getInjectable({
id: "lens-proxy-port",
instantiate: () => observable.box<number | undefined>(),
});
export default lensProxyPortInjectable;

View File

@ -0,0 +1,28 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import httpProxy from "http-proxy";
import getClusterForRequestInjectable from "../cluster/get-cluster-for-request.injectable";
import routerInjectable from "../router/router.injectable";
import { kubeApiUpgradeRequest } from "./kube-api-upgrade-request";
import { LensProxy } from "./lens-proxy";
import lensProxyLoggerInjectable from "./logger.injectable";
import lensProxyPortInjectable from "./port.injectable";
import shellApiRequestHandlerInjectable from "./shell-api/handler.injectable";
const lensProxyInjectable = getInjectable({
id: "lens-proxy",
instantiate: (di) => new LensProxy({
getClusterForRequest: di.inject(getClusterForRequestInjectable),
kubeApiUpgradeRequest,
logger: di.inject(lensProxyLoggerInjectable),
proxy: httpProxy.createProxy(),
proxyPort: di.inject(lensProxyPortInjectable),
router: di.inject(routerInjectable),
shellApiRequest: di.inject(shellApiRequestHandlerInjectable),
}),
});
export default lensProxyInjectable;

View File

@ -0,0 +1,42 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import URLParse from "url-parse";
import getClusterForRequestInjectable from "../../cluster/get-cluster-for-request.injectable";
import type { LensProxyRequestHandler } from "../lens-proxy";
import createShellSessionInjectable from "../../shell-session/create-shell-session.injectable";
import authenticateShellApiRequestInjectable from "./request/authenticate.injectable";
import { Server as WebSocketServer } from "ws";
import shellApiRequestLoggerInjectable from "./logger.injectable";
const shellApiRequestHandlerInjectable = getInjectable({
id: "shell-api-request-handler",
instantiate: (di): LensProxyRequestHandler => {
const createShellSession = di.inject(createShellSessionInjectable);
const authenticateShellApiRequest = di.inject(authenticateShellApiRequestInjectable);
const getClusterForRequest = di.inject(getClusterForRequestInjectable);
const logger = di.inject(shellApiRequestLoggerInjectable);
return ({ req, socket, head }) => {
const cluster = getClusterForRequest(req);
const { query: { node: nodeName, shellToken, id: tabId }} = new URLParse(req.url, true);
if (!cluster || !authenticateShellApiRequest(cluster.id, tabId, shellToken)) {
return void socket.end("Invalid shell request");
}
const ws = new WebSocketServer({ noServer: true });
ws.handleUpgrade(req, socket, head, (webSocket) => {
const shell = createShellSession({ webSocket, cluster, tabId, nodeName });
shell.open()
.catch(error => logger.error(`failed to open a ${nodeName ? "node" : "local"} shell`, error));
});
};
},
});
export default shellApiRequestHandlerInjectable;

View File

@ -0,0 +1,15 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import childLoggerInjectable from "../../../common/logger/child-logger.injectable";
const shellApiRequestLoggerInjectable = getInjectable({
id: "shell-api-request-logger",
instantiate: (di) => di.inject(childLoggerInjectable, {
prefix: "SHELL-API-REQUEST",
}),
});
export default shellApiRequestLoggerInjectable;

View File

@ -0,0 +1,20 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { ClusterId } from "../../../../common/cluster/types";
import shellRequestAuthenticatorInjectable from "./authenticator.injectable";
export type AuthenticateShellApiRequest = (clusterId: ClusterId, tabId: string, token: string) => boolean;
const authenticateShellApiRequestInjectable = getInjectable({
id: "authenticateShellApiRequest",
instantiate: (di): AuthenticateShellApiRequest => {
const authenticator = di.inject(shellRequestAuthenticatorInjectable);
return (clusterId, tabId, token) => authenticator.authenticate(clusterId, tabId, token);
},
});
export default authenticateShellApiRequestInjectable;

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { ShellRequestAuthenticator } from "./shell-request-authenticator";
import { ShellRequestAuthenticator } from "./authenticator";
const shellRequestAuthenticatorInjectable = getInjectable({
id: "shell-request-authenticator",

View File

@ -0,0 +1,52 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getOrInsertMap } from "../../../../common/utils";
import type { ClusterId } from "../../../../common/cluster/types";
import { ipcMainHandle } from "../../../../common/ipc";
import crypto from "crypto";
import { promisify } from "util";
const randomBytes = promisify(crypto.randomBytes);
export class ShellRequestAuthenticator {
private readonly tokens = new Map<ClusterId, Map<string, Uint8Array>>();
init() {
ipcMainHandle("cluster:shell-api", async (event, clusterId, tabId) => {
const authToken = Uint8Array.from(await randomBytes(128));
getOrInsertMap(this.tokens, clusterId).set(tabId, authToken);
return authToken;
});
}
/**
* Authenticates a single use token for creating a new shell
* @param clusterId The `ClusterId` for the shell
* @param tabId The ID for the shell
* @param token The value that is being presented as a one time authentication token
* @returns `true` if `token` was valid, false otherwise
*/
authenticate(clusterId: ClusterId, tabId: string, token: string): boolean {
const clusterTokens = this.tokens.get(clusterId);
if (!clusterTokens) {
return false;
}
const authToken = clusterTokens.get(tabId);
const buf = Uint8Array.from(Buffer.from(token, "base64"));
if (authToken instanceof Uint8Array && authToken.length === buf.length && crypto.timingSafeEqual(authToken, buf)) {
// remove the token because it is a single use token
clusterTokens.delete(tabId);
return true;
}
return false;
}
}

View File

@ -0,0 +1,67 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import winston, { format } from "winston";
import { consoleFormat } from "winston-console-format";
import type { Logger } from "../../common/logger";
import { baseLoggerInjectionToken } from "../../common/logger/logger.token";
import { isDebugging, isTestEnv } from "../../common/vars";
import appPathsInjectable from "../app-paths/app-paths.injectable";
const logLevel = process.env.LOG_LEVEL
? process.env.LOG_LEVEL
: isDebugging
? "debug"
: isTestEnv
? "error"
: "info";
const loggerInjectable = getInjectable({
id: "logger",
instantiate: (di): Logger => {
const logsPath = di.inject(appPathsInjectable).logs;
return winston.createLogger({
format: format.combine(
format.splat(),
format.simple(),
),
transports: [
new winston.transports.Console({
handleExceptions: false,
level: logLevel,
format: format.combine(
format.colorize({ level: true, message: false }),
format.padLevels(),
format.ms(),
consoleFormat({
showMeta: true,
inspectOptions: {
depth: 4,
colors: true,
maxArrayLength: 10,
breakLength: 120,
compact: Infinity,
},
}),
),
}),
new winston.transports.File({
handleExceptions: false,
level: logLevel,
filename: "lens.log",
dirname: logsPath,
maxsize: 16 * 1024,
maxFiles: 16,
tailable: true,
}),
],
});
},
injectionToken: baseLoggerInjectionToken,
});
export default loggerInjectable;

View File

@ -5,19 +5,12 @@
import { getInjectable } from "@ogre-tools/injectable";
import { checkForUpdates } from "../app-updater";
import { docsUrl, productName, supportUrl } from "../../common/vars";
import { exitApp } from "../exit-app";
import { broadcastMessage } from "../../common/ipc";
import { openBrowser } from "../../common/utils";
import { showAbout } from "./menu";
import windowManagerInjectable from "../window-manager.injectable";
import type {
BrowserWindow,
MenuItem,
MenuItemConstructorOptions } from "electron";
import {
webContents,
} from "electron";
import loggerInjectable from "../../common/logger.injectable";
import windowManagerInjectable from "../window/manager.injectable";
import type { BrowserWindow, MenuItem, MenuItemConstructorOptions } from "electron";
import { webContents } from "electron";
import appNameInjectable from "../app-paths/app-name/app-name.injectable";
import electronMenuItemsInjectable from "./electron-menu-items.injectable";
import isAutoUpdateEnabledInjectable from "../is-auto-update-enabled.injectable";
@ -28,6 +21,8 @@ import navigateToWelcomeInjectable from "../../common/front-end-routing/routes/w
import navigateToAddClusterInjectable from "../../common/front-end-routing/routes/add-cluster/navigate-to-add-cluster.injectable";
import isMacInjectable from "../../common/vars/is-mac.injectable";
import { computed } from "mobx";
import appMenuLoggerInjectable from "./logger.injectable";
import exitAppInjectable from "../utils/exit-app.injectable";
function ignoreIf(check: boolean, menuItems: MenuItemConstructorOptions[]) {
return check ? [] : menuItems;
@ -41,26 +36,22 @@ const applicationMenuItemsInjectable = getInjectable({
id: "application-menu-items",
instantiate: (di) => {
const logger = di.inject(loggerInjectable);
const logger = di.inject(appMenuLoggerInjectable);
const appName = di.inject(appNameInjectable);
const isMac = di.inject(isMacInjectable);
const isAutoUpdateEnabled = di.inject(isAutoUpdateEnabledInjectable);
const electronMenuItems = di.inject(electronMenuItemsInjectable);
const windowManager = di.inject(windowManagerInjectable);
const navigateToPreferences = di.inject(navigateToPreferencesInjectable);
const navigateToExtensions = di.inject(navigateToExtensionsInjectable);
const navigateToCatalog = di.inject(navigateToCatalogInjectable);
const navigateToWelcome = di.inject(navigateToWelcomeInjectable);
const navigateToAddCluster = di.inject(navigateToAddClusterInjectable);
const exitApp = di.inject(exitAppInjectable);
const autoUpdateDisabled = !isAutoUpdateEnabled();
return computed((): MenuItemOpts[] => {
// TODO: These injects should happen outside of the computed.
// TODO: Remove temporal dependencies in WindowManager to make sure timing is correct.
const windowManager = di.inject(windowManagerInjectable);
const navigateToPreferences = di.inject(navigateToPreferencesInjectable);
const navigateToExtensions = di.inject(navigateToExtensionsInjectable);
const navigateToCatalog = di.inject(navigateToCatalogInjectable);
const navigateToWelcome = di.inject(navigateToWelcomeInjectable);
const navigateToAddCluster = di.inject(navigateToAddClusterInjectable);
const autoUpdateDisabled = !isAutoUpdateEnabled();
logger.info(`[MENU]: autoUpdateDisabled=${autoUpdateDisabled}`);
logger.info(`auto update is ${autoUpdateDisabled ? "disabled" : "enabled"}`);
const macAppMenu: MenuItemOpts = {
label: appName,
@ -269,7 +260,7 @@ const applicationMenuItemsInjectable = getInjectable({
id: "documentation",
click: async () => {
openBrowser(docsUrl).catch((error) => {
logger.error("[MENU]: failed to open browser", { error });
logger.error("failed to open browser", { error });
});
},
},
@ -278,7 +269,7 @@ const applicationMenuItemsInjectable = getInjectable({
id: "support",
click: async () => {
openBrowser(supportUrl).catch((error) => {
logger.error("[MENU]: failed to open browser", { error });
logger.error("failed to open browser", { error });
});
},
},
@ -316,7 +307,7 @@ const applicationMenuItemsInjectable = getInjectable({
for (const menuItem of electronMenuItems.get()) {
if (!appMenu.has(menuItem.parentId)) {
logger.error(
`[MENU]: cannot register menu item for parentId=${menuItem.parentId}, parent item doesn't exist`,
`cannot register menu item for parentId=${menuItem.parentId}, parent item doesn't exist`,
{ menuItem },
);

View File

@ -0,0 +1,15 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import childLoggerInjectable from "../../common/logger/child-logger.injectable";
const appMenuLoggerInjectable = getInjectable({
id: "app-menu-logger",
instantiate: (di) => di.inject(childLoggerInjectable, {
prefix: "MENU",
}),
});
export default appMenuLoggerInjectable;

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import windowManagerInjectable from "../window-manager.injectable";
import windowManagerInjectable from "../window/manager.injectable";
import { navigateToUrlInjectionToken } from "../../common/front-end-routing/navigate-to-url-injection-token";
const navigateToUrlInjectable = getInjectable({

View File

@ -10,14 +10,15 @@ import { ProtocolHandlerExtension, ProtocolHandlerInternal } from "../../../comm
import { delay, noop } from "../../../common/utils";
import { LensExtension } from "../../../extensions/main-api";
import { ExtensionsStore } from "../../../extensions/extensions-store/extensions-store";
import type { LensProtocolRouterMain } from "../lens-protocol-router-main/lens-protocol-router-main";
import type { LensProtocolRouterMain } from "../router";
import mockFs from "mock-fs";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import extensionLoaderInjectable from "../../../extensions/extension-loader/extension-loader.injectable";
import lensProtocolRouterMainInjectable from "../lens-protocol-router-main/lens-protocol-router-main.injectable";
import lensProtocolRouterMainInjectable from "../router.injectable";
import extensionsStoreInjectable from "../../../extensions/extensions-store/extensions-store.injectable";
import getConfigurationFileModelInjectable from "../../../common/get-configuration-file-model/get-configuration-file-model.injectable";
import appVersionInjectable from "../../../common/get-configuration-file-model/app-version/app-version.injectable";
import ensureMainWindowInjectable from "../../window/ensure-main.injectable";
jest.mock("../../../common/ipc");
@ -48,10 +49,10 @@ describe("protocol router tests", () => {
await di.runSetups();
di.override(ensureMainWindowInjectable, () => async () => null);
extensionLoader = di.inject(extensionLoaderInjectable);
extensionsStore = di.inject(extensionsStoreInjectable);
lpr = di.inject(lensProtocolRouterMainInjectable);
lpr.rendererLoaded = true;

View File

@ -3,4 +3,4 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export * from "./lens-protocol-router-main/lens-protocol-router-main";
export * from "./router";

View File

@ -1,20 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import extensionLoaderInjectable from "../../../extensions/extension-loader/extension-loader.injectable";
import { LensProtocolRouterMain } from "./lens-protocol-router-main";
import extensionsStoreInjectable from "../../../extensions/extensions-store/extensions-store.injectable";
const lensProtocolRouterMainInjectable = getInjectable({
id: "lens-protocol-router-main",
instantiate: (di) =>
new LensProtocolRouterMain({
extensionLoader: di.inject(extensionLoaderInjectable),
extensionsStore: di.inject(extensionsStoreInjectable),
}),
});
export default lensProtocolRouterMainInjectable;

View File

@ -0,0 +1,23 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import extensionLoaderInjectable from "../../extensions/extension-loader/extension-loader.injectable";
import { LensProtocolRouterMain } from "./router";
import extensionsStoreInjectable from "../../extensions/extensions-store/extensions-store.injectable";
import ensureMainWindowInjectable from "../window/ensure-main.injectable";
import lensProtocolRouterLoggerInjectable from "../../common/protocol-handler/logger.injectable";
const lensProtocolRouterMainInjectable = getInjectable({
id: "lens-protocol-router-main",
instantiate: (di) => new LensProtocolRouterMain({
extensionLoader: di.inject(extensionLoaderInjectable),
extensionsStore: di.inject(extensionsStoreInjectable),
ensureMainWindow: di.inject(ensureMainWindowInjectable),
logger: di.inject(lensProtocolRouterLoggerInjectable),
}),
});
export default lensProtocolRouterMainInjectable;

View File

@ -3,18 +3,15 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import logger from "../../logger";
import * as proto from "../../../common/protocol-handler";
import * as proto from "../../common/protocol-handler";
import URLParse from "url-parse";
import type { LensExtension } from "../../../extensions/lens-extension";
import { broadcastMessage } from "../../../common/ipc";
import type { LensExtension } from "../../extensions/lens-extension";
import { broadcastMessage } from "../../common/ipc";
import { observable, when, makeObservable } from "mobx";
import type { RouteAttempt } from "../../../common/protocol-handler";
import { ProtocolHandlerInvalid } from "../../../common/protocol-handler";
import { disposer, noop } from "../../../common/utils";
import { WindowManager } from "../../window-manager";
import type { ExtensionLoader } from "../../../extensions/extension-loader";
import type { ExtensionsStore } from "../../../extensions/extensions-store/extensions-store";
import type { LensProtocolRouterDependencies, RouteAttempt } from "../../common/protocol-handler";
import { ProtocolHandlerInvalid } from "../../common/protocol-handler";
import { disposer, noop } from "../../common/utils";
import type { EnsureMainWindow } from "../window/ensure-main.injectable";
export interface FallbackHandler {
(name: string): Promise<boolean>;
@ -37,21 +34,19 @@ function checkHost<Query>(url: URLParse<Query>): boolean {
}
}
interface Dependencies {
extensionLoader: ExtensionLoader;
extensionsStore: ExtensionsStore;
interface LensProtocolRouterMainDependencies extends LensProtocolRouterDependencies {
ensureMainWindow: EnsureMainWindow;
}
export class LensProtocolRouterMain extends proto.LensProtocolRouter {
private missingExtensionHandlers: FallbackHandler[] = [];
private readonly missingExtensionHandlers: FallbackHandler[] = [];
@observable rendererLoaded = false;
protected disposers = disposer();
protected readonly disposers = disposer();
constructor(protected dependencies: Dependencies) {
constructor(protected readonly dependencies: LensProtocolRouterMainDependencies) {
super(dependencies);
makeObservable(this);
}
@ -73,10 +68,10 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter {
throw new proto.RoutingError(proto.RoutingErrorType.INVALID_PROTOCOL, url);
}
WindowManager.getInstance(false)?.ensureMainWindow().catch(noop);
this.dependencies.ensureMainWindow().catch(noop);
const routeInternally = checkHost(url);
logger.info(`${proto.LensProtocolRouter.LoggingPrefix}: routing ${url.toString()}`);
this.dependencies.logger.info(`routing ${url.toString()}`);
if (routeInternally) {
this._routeToInternal(url);
@ -87,9 +82,9 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter {
broadcastMessage(ProtocolHandlerInvalid, error.toString(), rawUrl);
if (error instanceof proto.RoutingError) {
logger.error(`${proto.LensProtocolRouter.LoggingPrefix}: ${error}`, { url: error.url });
this.dependencies.logger.error(`${error}`, { url: error.url });
} else {
logger.error(`${proto.LensProtocolRouter.LoggingPrefix}: ${error}`, { rawUrl });
this.dependencies.logger.error(`${error}`, { rawUrl });
}
}
}

View File

@ -1,6 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export * from "./kube-api-upgrade-request";
export * from "./types";

View File

@ -1,20 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { shellApiRequest } from "./shell-api-request";
import createShellSessionInjectable from "../../shell-session/create-shell-session.injectable";
import shellRequestAuthenticatorInjectable
from "./shell-request-authenticator/shell-request-authenticator.injectable";
const shellApiRequestInjectable = getInjectable({
id: "shell-api-request",
instantiate: (di) => shellApiRequest({
createShellSession: di.inject(createShellSessionInjectable),
authenticateRequest: di.inject(shellRequestAuthenticatorInjectable).authenticate,
}),
});
export default shellApiRequestInjectable;

View File

@ -1,44 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import logger from "../../logger";
import type WebSocket from "ws";
import { Server as WebSocketServer } from "ws";
import type { ProxyApiRequestArgs } from "../types";
import { ClusterManager } from "../../cluster-manager";
import URLParse from "url-parse";
import type { Cluster } from "../../../common/cluster/cluster";
import type { ClusterId } from "../../../common/cluster-types";
interface Dependencies {
authenticateRequest: (clusterId: ClusterId, tabId: string, shellToken: string) => boolean;
createShellSession: (args: {
webSocket: WebSocket;
cluster: Cluster;
tabId: string;
nodeName?: string;
}) => { open: () => Promise<void> };
}
export const shellApiRequest = ({ createShellSession, authenticateRequest }: Dependencies) => ({ req, socket, head }: ProxyApiRequestArgs): void => {
const cluster = ClusterManager.getInstance().getClusterForRequest(req);
const { query: { node: nodeName, shellToken, id: tabId }} = new URLParse(req.url, true);
if (!cluster || !authenticateRequest(cluster.id, tabId, shellToken)) {
socket.write("Invalid shell request");
return void socket.end();
}
const ws = new WebSocketServer({ noServer: true });
ws.handleUpgrade(req, socket, head, (webSocket) => {
const shell = createShellSession({ webSocket, cluster, tabId, nodeName });
shell.open()
.catch(error => logger.error(`[SHELL-SESSION]: failed to open a ${nodeName ? "node" : "local"} shell`, error));
});
};

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getOrInsertMap } from "../../../../common/utils";
import type { ClusterId } from "../../../../common/cluster-types";
import type { ClusterId } from "../../../../common/cluster/types";
import { ipcMainHandle } from "../../../../common/ipc";
import crypto from "crypto";
import { promisify } from "util";
@ -11,14 +11,13 @@ import { promisify } from "util";
const randomBytes = promisify(crypto.randomBytes);
export class ShellRequestAuthenticator {
private tokens = new Map<ClusterId, Map<string, Uint8Array>>();
private readonly tokens = new Map<ClusterId, Map<string, Uint8Array>>();
init() {
ipcMainHandle("cluster:shell-api", async (event, clusterId, tabId) => {
const authToken = Uint8Array.from(await randomBytes(128));
const forCluster = getOrInsertMap(this.tokens, clusterId);
forCluster.set(tabId, authToken);
getOrInsertMap(this.tokens, clusterId).set(tabId, authToken);
return authToken;
});

View File

@ -1,15 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type http from "http";
import type net from "net";
import type { Cluster } from "../../common/cluster/cluster";
export interface ProxyApiRequestArgs {
req: http.IncomingMessage;
socket: net.Socket;
head: Buffer;
cluster: Cluster;
}

View File

@ -7,8 +7,8 @@ import { getInjectable } from "@ogre-tools/injectable";
import { apiPrefix } from "../../../common/vars";
import type { LensApiRequest, Route } from "../../router/router";
import { routeInjectionToken } from "../../router/router.injectable";
import type { ClusterPrometheusMetadata } from "../../../common/cluster-types";
import { ClusterMetadataKey } from "../../../common/cluster-types";
import type { ClusterPrometheusMetadata } from "../../../common/cluster/types";
import { ClusterMetadataKey } from "../../../common/cluster/types";
import logger from "../../logger";
import type { Cluster } from "../../../common/cluster/cluster";
import { getMetrics } from "../../k8s-request";

Some files were not shown because too many files have changed in this diff Show More