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 path from "path";
import fse from "fs-extra"; import fse from "fs-extra";
import type { Cluster } from "../cluster/cluster"; import type { Cluster } from "../cluster/cluster";
import { ClusterStore } from "../cluster-store/cluster-store"; import { ClusterStore } from "../cluster/store";
import { Console } from "console"; import { Console } from "console";
import { stdout, stderr } from "process"; import { stdout, stderr } from "process";
import getCustomKubeConfigDirectoryInjectable from "../app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable"; 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 clusterStoreInjectable from "../cluster/store.injectable";
import type { ClusterModel } from "../cluster-types"; import type { ClusterModel } from "../cluster/types";
import type { import type {
DiContainer, DiContainer,
} from "@ogre-tools/injectable"; } from "@ogre-tools/injectable";

View File

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

View File

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

View File

@ -121,14 +121,14 @@ export abstract class BaseStore<T> extends Singleton {
if (ipcMain) { if (ipcMain) {
this.syncDisposers.push(ipcMainOn(this.syncMainChannel, (event, model: T) => { 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); this.onSync(model);
})); }));
} }
if (ipcRenderer) { if (ipcRenderer) {
this.syncDisposers.push(ipcRendererOn(this.syncRendererChannel, (event, model: T) => { 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); this.onSyncFromMain(model);
})); }));
} }

View File

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

View File

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

View File

@ -3,7 +3,6 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * 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 { action, comparer, computed, makeObservable, observable, reaction, when } from "mobx";
import { broadcastMessage } from "../ipc"; import { broadcastMessage } from "../ipc";
import type { ContextHandler } from "../../main/context-handler/context-handler"; 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 type { KubeApiResource, KubeResource } from "../rbac";
import { apiResourceRecord, apiResources } from "../rbac"; import { apiResourceRecord, apiResources } from "../rbac";
import logger from "../../main/logger"; import logger from "../../main/logger";
import { VersionDetector } from "../../main/cluster-detectors/version-detector"; import { VersionDetector } from "../../main/cluster/detectors/version-detector";
import { DetectorRegistry } from "../../main/cluster-detectors/detector-registry"; import { DetectorRegistry } from "../../main/cluster/detectors/detector-registry";
import plimit from "p-limit"; import plimit from "p-limit";
import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, KubeAuthUpdate } from "../cluster-types"; import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, KubeAuthUpdate } from "./types";
import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus } from "../cluster-types"; import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus } from "./types";
import { disposer, toJS } from "../utils"; import { disposer, toJS } from "../utils";
import type { Response } from "request"; import type { Response } from "request";
import { clusterListNamespaceForbiddenChannel } from "../ipc/cluster"; import { clusterListNamespaceForbiddenChannel } from "../ipc/cluster";
import type { CanI } from "./authorization-review.injectable"; import type { CanI } from "./authorization-review.injectable";
import type { ListNamespaces } from "./list-namespaces.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 { export interface ClusterDependencies {
readonly directoryForKubeConfigs: string; readonly directoryForKubeConfigs: string;
createKubeconfigManager: (cluster: Cluster) => KubeconfigManager; createKubeconfigManager: CreateKubeConfigManager;
createContextHandler: (cluster: Cluster) => ContextHandler; createContextHandler: CreateContextHandler;
createKubectl: (clusterVersion: string) => Kubectl; createKubectl: (clusterVersion: string) => Kubectl;
createAuthorizationReview: (config: KubeConfig) => CanI; createAuthorizationReview: (config: KubeConfig) => CanI;
createListNamespaces: (config: KubeConfig) => ListNamespaces; createListNamespaces: (config: KubeConfig) => ListNamespaces;
@ -49,9 +50,9 @@ export class Cluster implements ClusterModel, ClusterState {
* *
* @internal * @internal
*/ */
public contextHandler: ContextHandler; public readonly contextHandler: ContextHandler | undefined;
protected proxyKubeconfigManager: KubeconfigManager; protected readonly proxyKubeconfigManager: KubeconfigManager | undefined;
protected eventsDisposer = disposer(); protected readonly eventsDisposer = disposer();
protected activated = false; protected activated = false;
private resourceAccessStatuses: Map<KubeApiResource, boolean> = new Map(); 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; this.apiUrl = config.getCluster(config.getContextObject(this.contextName).cluster).server;
if (ipcMain) { // for the time being, until renderer gets its own cluster type
// for the time being, until renderer gets its own cluster type this.contextHandler = this.dependencies.createContextHandler(this);
this.contextHandler = this.dependencies.createContextHandler(this); this.proxyKubeconfigManager = this.dependencies.createKubeconfigManager(this);
this.proxyKubeconfigManager = this.dependencies.createKubeconfigManager(this);
logger.debug(`[CLUSTER]: Cluster init success`, { logger.debug(`[CLUSTER]: Cluster init success`, {
id: this.id, id: this.id,
context: this.contextName, context: this.contextName,
apiUrl: this.apiUrl, apiUrl: this.apiUrl,
}); });
}
} }
/** /**
@ -571,7 +570,6 @@ export class Cluster implements ClusterModel, ClusterState {
* @param state cluster state * @param state cluster state
*/ */
pushState(state = this.getState()) { pushState(state = this.getState()) {
logger.silly(`[CLUSTER]: push-state`, state);
broadcastMessage("cluster:state", this.id, 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. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectionToken } from "@ogre-tools/injectable"; import { getInjectionToken } from "@ogre-tools/injectable";
import type { ClusterModel } from "../cluster-types"; import type { ClusterModel } from "./types";
import type { Cluster } from "./cluster"; import type { Cluster } from "./cluster";
export const createClusterInjectionToken = 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 { getInjectable } from "@ogre-tools/injectable";
import { getHostedClusterId } from "../utils"; import { getHostedClusterId } from "../utils";
import clusterStoreInjectable from "./cluster-store.injectable"; import getClusterByIdInjectable from "./get-by-id.injectable";
const hostedClusterInjectable = getInjectable({ const hostedClusterInjectable = getInjectable({
id: "hosted-cluster", id: "hosted-cluster",
instantiate: (di) => { instantiate: (di) => {
const hostedClusterId = getHostedClusterId(); 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. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { ClusterStore } from "./cluster-store"; import { createClusterInjectionToken } from "./create-cluster-injection-token";
import { createClusterInjectionToken } from "../cluster/create-cluster-injection-token"; import { ClusterStore } from "./store";
const clusterStoreInjectable = getInjectable({ const clusterStoreInjectable = getInjectable({
id: "cluster-store", id: "cluster-store",
instantiate: (di) => instantiate: (di) => {
ClusterStore.createInstance({ ClusterStore.resetInstance();
return ClusterStore.createInstance({
createCluster: di.inject(createClusterInjectionToken), createCluster: di.inject(createClusterInjectionToken),
}), });
},
causesSideEffects: true, causesSideEffects: true,
}); });

View File

@ -7,13 +7,13 @@
import { ipcMain, ipcRenderer, webFrame } from "electron"; import { ipcMain, ipcRenderer, webFrame } from "electron";
import { action, comparer, computed, makeObservable, observable, reaction } from "mobx"; import { action, comparer, computed, makeObservable, observable, reaction } from "mobx";
import { BaseStore } from "../base-store"; import { BaseStore } from "../base-store";
import { Cluster } from "../cluster/cluster"; import { Cluster } from "./cluster";
import migrations from "../../migrations/cluster-store"; import migrations from "../../migrations/cluster-store";
import logger from "../../main/logger"; import logger from "../../main/logger";
import { appEventBus } from "../app-event-bus/event-bus"; import { appEventBus } from "../app-event-bus/event-bus";
import { ipcMainHandle } from "../ipc"; import { ipcMainHandle } from "../ipc";
import { disposer, toJS } from "../utils"; import { disposer, toJS } from "../utils";
import type { ClusterModel, ClusterId, ClusterState } from "../cluster-types"; import type { ClusterModel, ClusterId, ClusterState } from "./types";
import { requestInitialClusterStates } from "../../renderer/ipc"; import { requestInitialClusterStates } from "../../renderer/ipc";
import { clusterStates } from "../ipc/cluster"; 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 { routeSpecificComponentInjectionToken } from "../../renderer/routes/route-specific-component-injection-token";
import { routeInjectionToken } from "./route-injection-token"; import { routeInjectionToken } from "./route-injection-token";
import { filter, map, matches } from "lodash/fp"; import { filter, map, matches } from "lodash/fp";
import clusterStoreInjectable from "../cluster-store/cluster-store.injectable"; import clusterStoreInjectable from "../cluster/store.injectable";
import type { ClusterStore } from "../cluster-store/cluster-store"; import type { ClusterStore } from "../cluster/store";
import { pipeline } from "@ogre-tools/fp"; import { pipeline } from "@ogre-tools/fp";
describe("verify-that-all-routes-have-component", () => { 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. // Send message to views.
try { 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); view.send(channel, ...args);
} catch (error) { } catch (error) {
logger.error(`[IPC]: failed to send IPC message "${channel}" to view "${viewType}=${view.id}"`, { 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. // Send message to subFrames of views.
for (const frameInfo of subFrames) { 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 { try {
view.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...args); 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 type { KubernetesCluster } from "../catalog-entities";
import logger from "../../main/logger"; import logger from "../../main/logger";
import { app } from "electron"; import { app } from "electron";
import { ClusterStore } from "../cluster-store/cluster-store"; import { ClusterStore } from "../cluster/store";
import yaml from "js-yaml"; import yaml from "js-yaml";
import { productName } from "../vars"; import { productName } from "../vars";
import { requestKubectlApplyAll, requestKubectlDeleteAll } from "../../renderer/ipc"; 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 { countBy } from "lodash";
import { iter } from "../utils"; import { iter } from "../utils";
import { pathToRegexp } from "path-to-regexp"; import { pathToRegexp } from "path-to-regexp";
import logger from "../../main/logger";
import type Url from "url-parse"; import type Url from "url-parse";
import { RoutingError, RoutingErrorType } from "./error"; import { RoutingError, RoutingErrorType } from "./error";
import type { ExtensionsStore } from "../../extensions/extensions-store/extensions-store"; 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 type { RouteHandler, RouteParams } from "../../extensions/registries/protocol-handler";
import { when } from "mobx"; import { when } from "mobx";
import { ipcRenderer } from "electron"; import { ipcRenderer } from "electron";
import type { Logger } from "../logger";
// IPC channel for protocol actions. Main broadcasts the open-url events to this channel. // IPC channel for protocol actions. Main broadcasts the open-url events to this channel.
export const ProtocolHandlerIpcPrefix = "protocol-handler"; export const ProtocolHandlerIpcPrefix = "protocol-handler";
@ -63,20 +63,19 @@ export function foldAttemptResults(mainAttempt: RouteAttempt, rendererAttempt: R
} }
} }
interface Dependencies { export interface LensProtocolRouterDependencies {
extensionLoader: ExtensionLoader; readonly extensionLoader: ExtensionLoader;
extensionsStore: ExtensionsStore; readonly extensionsStore: ExtensionsStore;
readonly logger: Logger;
} }
export abstract class LensProtocolRouter { export abstract class LensProtocolRouter {
// Map between path schemas and the handlers // Map between path schemas and the handlers
protected internalRoutes = new Map<string, RouteHandler>(); protected readonly internalRoutes = new Map<string, RouteHandler>();
public static readonly LoggingPrefix = "[PROTOCOL ROUTER]";
static readonly ExtensionUrlSchema = `/:${EXTENSION_PUBLISHER_MATCH}(@[A-Za-z0-9_]+)?/:${EXTENSION_NAME_MATCH}`; 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 * 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; data.extensionName = extensionName;
} }
logger.info(`${LensProtocolRouter.LoggingPrefix}: No handler found`, data); this.dependencies.logger.info(`No handler found`, data);
return RouteAttempt.MISSING; return RouteAttempt.MISSING;
} }
@ -183,9 +182,7 @@ export abstract class LensProtocolRouter {
timeout: 5_000, timeout: 5_000,
}); });
} catch (error) { } catch (error) {
logger.info( this.dependencies.logger.info(`Extension ${name} matched, but not installed (${error})`);
`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not installed (${error})`,
);
return name; return name;
} }
@ -193,18 +190,18 @@ export abstract class LensProtocolRouter {
const extension = extensionLoader.getInstanceByName(name); const extension = extensionLoader.getInstanceByName(name);
if (!extension) { 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; return name;
} }
if (!this.dependencies.extensionsStore.isEnabled(extension)) { 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; return name;
} }
logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched`); this.dependencies.logger.info(`Extension ${name} matched`);
return extension; return extension;
} }
@ -250,7 +247,7 @@ export abstract class LensProtocolRouter {
*/ */
public addInternalHandler(urlSchema: string, handler: RouteHandler): this { public addInternalHandler(urlSchema: string, handler: RouteHandler): this {
pathToRegexp(urlSchema); // verify now that the schema is valid 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); this.internalRoutes.set(urlSchema, handler);
return this; return this;

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * 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 * 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 { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { computed } from "mobx"; import { computed } from "mobx";
import allowedResourcesInjectable from "../cluster-store/allowed-resources.injectable"; import allowedResourcesInjectable from "../cluster/allowed-resources.injectable";
import type { KubeResource } from "../rbac"; import type { KubeResource } from "../rbac";
export type IsAllowedResource = (resource: KubeResource) => boolean; export type IsAllowedResource = (resource: KubeResource) => boolean;
@ -12,7 +12,7 @@ export type IsAllowedResource = (resource: KubeResource) => boolean;
const isAllowedResourceInjectable = getInjectable({ const isAllowedResourceInjectable = getInjectable({
id: "is-allowed-resource", id: "is-allowed-resource",
instantiate: (di, resourceName: KubeResource) => { instantiate: (di, resourceName) => {
const allowedResources = di.inject(allowedResourcesInjectable); const allowedResources = di.inject(allowedResourcesInjectable);
return computed(() => allowedResources.get().has(resourceName)); 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. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import type { Injectable } from "@ogre-tools/injectable"; 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 { asLegacyGlobalForExtensionApi } from "./as-legacy-global-object-for-extension-api";
import { getLegacyGlobalDiForExtensionApi } from "./legacy-global-di-for-extension-api"; import { getLegacyGlobalDiForExtensionApi } from "./legacy-global-di-for-extension-api";
import loggerInjectable from "../../common/logger.injectable";
export const asLegacyGlobalSingletonForExtensionApi = < export const asLegacyGlobalSingletonForExtensionApi = <
Instance, Instance,
@ -26,11 +26,9 @@ export const asLegacyGlobalSingletonForExtensionApi = <
resetInstance: () => { resetInstance: () => {
const di = getLegacyGlobalDiForExtensionApi(); const di = getLegacyGlobalDiForExtensionApi();
const logger = di.inject(loggerInjectable); const logger = di.inject(baseLoggerInjectionToken);
logger.warn( logger.warn(`resetInstance() for a legacy global singleton of "${injectable.id}" does nothing.`);
`resetInstance() for a legacy global singleton of "${injectable.id}" does nothing.`,
);
}, },
}; };
}; };

View File

@ -4,13 +4,14 @@
*/ */
import { LensExtension } from "./lens-extension"; import { LensExtension } from "./lens-extension";
import { WindowManager } from "../main/window-manager";
import { catalogEntityRegistry } from "../main/catalog"; import { catalogEntityRegistry } from "../main/catalog";
import type { CatalogEntity } from "../common/catalog"; import type { CatalogEntity } from "../common/catalog";
import type { IObservableArray } from "mobx"; import type { IObservableArray } from "mobx";
import type { MenuRegistration } from "../main/menu/menu-registration"; import type { MenuRegistration } from "../main/menu/menu-registration";
import type { TrayMenuRegistration } from "../main/tray/tray-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 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 { export class LensMainExtension extends LensExtension {
appMenus: MenuRegistration[] = []; 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 * 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> * (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 * @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 * 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. * @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 * @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 * does not modify the shell environment
*/ */
terminalShellEnvModifier?: ShellEnvModifier; terminalShellEnvModifier?: ShellEnvModifier;
async navigate(pageId?: string, params?: Record<string, any>, frameId?: number) { 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>) { addCatalogSource(id: string, source: IObservableArray<CatalogEntity>) {

View File

@ -3,8 +3,12 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * 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) { 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 type { Cluster } from "../../common/cluster/cluster";
import { Kubectl } from "../kubectl/kubectl"; import { Kubectl } from "../kubectl/kubectl";
import { getDiForUnitTesting } from "../getDiForUnitTesting"; 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 { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
import authorizationReviewInjectable from "../../common/cluster/authorization-review.injectable"; import authorizationReviewInjectable from "../../common/cluster/authorization-review.injectable";
import listNamespacesInjectable from "../../common/cluster/list-namespaces.injectable"; import listNamespacesInjectable from "../../common/cluster/list-namespaces.injectable";
import createContextHandlerInjectable from "../context-handler/create-context-handler.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 console = new Console(process.stdout, process.stderr); // fix mockFS
describe("create clusters", () => { describe("create clusters", () => {
let di: DiContainer;
let cluster: Cluster; let cluster: Cluster;
let createCluster: (model: ClusterModel) => Cluster; let createCluster: (model: ClusterModel) => Cluster;
beforeEach(async () => { beforeEach(async () => {
jest.clearAllMocks(); jest.clearAllMocks();
const di = getDiForUnitTesting({ doGeneralOverrides: true }); di = getDiForUnitTesting({ doGeneralOverrides: true });
mockFs({ mockFs({
"minikube-config.yml": JSON.stringify({ "minikube-config.yml": JSON.stringify({
@ -83,9 +87,11 @@ describe("create clusters", () => {
di.override(authorizationReviewInjectable, () => () => () => Promise.resolve(true)); di.override(authorizationReviewInjectable, () => () => () => Promise.resolve(true));
di.override(listNamespacesInjectable, () => () => () => Promise.resolve([ "default" ])); di.override(listNamespacesInjectable, () => () => () => Promise.resolve([ "default" ]));
di.override(createContextHandlerInjectable, () => () => { di.override(createContextHandlerInjectable, () => () => ({
throw new Error("you should never come here"); ensureServer: jest.fn(),
}); stopServer: jest.fn(),
}) as unknown as ContextHandler);
di.override(createKubeconfigManagerInjectable, () => () => undefined);
createCluster = di.inject(createClusterInjectionToken); createCluster = di.inject(createClusterInjectionToken);
@ -121,11 +127,6 @@ describe("create clusters", () => {
kubeConfigPath: "minikube-config.yml", kubeConfigPath: "minikube-config.yml",
}); });
cluster.contextHandler = {
ensureServer: jest.fn(),
stopServer: jest.fn(),
} as any;
jest.spyOn(cluster, "reconnect"); jest.spyOn(cluster, "reconnect");
jest.spyOn(cluster, "refreshConnectionStatus"); jest.spyOn(cluster, "refreshConnectionStatus");

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * 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", () => ({ jest.mock("winston", () => ({
format: { format: {

View File

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

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * 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", () => { describe("isLongRunningRequest", () => {
it("returns true on watches", () => { it("returns true on watches", () => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@
*/ */
import { BaseClusterDetector } from "./base-cluster-detector"; import { BaseClusterDetector } from "./base-cluster-detector";
import { ClusterMetadataKey } from "../../common/cluster-types"; import { ClusterMetadataKey } from "../../../common/cluster/types";
export class VersionDetector extends BaseClusterDetector { export class VersionDetector extends BaseClusterDetector {
key = ClusterMetadataKey.VERSION; 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. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import "../common/ipc/cluster"; import "../../common/ipc/cluster";
import type http from "http";
import { action, makeObservable, observable, observe, reaction, toJS } from "mobx"; import { action, makeObservable, observable, observe, reaction, toJS } from "mobx";
import type { Cluster } from "../common/cluster/cluster"; import type { Cluster } from "../../common/cluster/cluster";
import logger from "./logger"; import { catalogEntityRegistry } from "../catalog";
import { apiKubePrefix } from "../common/vars"; import type { KubernetesClusterPrometheusMetrics } from "../../common/catalog-entities/kubernetes-cluster";
import { getClusterIdFromHost, Singleton } from "../common/utils"; import { KubernetesCluster, LensKubernetesClusterStatus } from "../../common/catalog-entities/kubernetes-cluster";
import { catalogEntityRegistry } from "./catalog"; import { ipcMainOn } from "../../common/ipc";
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 { once } from "lodash";
import { ClusterStore } from "../common/cluster-store/cluster-store"; import type { ClusterId } from "../../common/cluster/types";
import type { ClusterId } from "../common/cluster-types"; import type { ClusterStore } from "../../common/cluster/store";
import type { Logger } from "../../common/logger";
const logPrefix = "[CLUSTER-MANAGER]:";
const lensSpecificClusterStatuses: Set<string> = new Set(Object.values(LensKubernetesClusterStatus)); const lensSpecificClusterStatuses: Set<string> = new Set(Object.values(LensKubernetesClusterStatus));
export class ClusterManager extends Singleton { interface Dependencies {
private store = ClusterStore.getInstance(); readonly clusterStore: ClusterStore;
deleting = observable.set<ClusterId>(); readonly logger: Logger;
}
export class ClusterManager {
readonly deleting = observable.set<ClusterId>();
@observable visibleCluster: ClusterId | undefined = undefined; @observable visibleCluster: ClusterId | undefined = undefined;
constructor() { constructor(protected readonly dependencies: Dependencies) {
super();
makeObservable(this); makeObservable(this);
} }
init = once(() => { init = once(() => {
// reacting to every cluster's state change and total amount of items // reacting to every cluster's state change and total amount of items
reaction( reaction(
() => this.store.clustersList.map(c => c.getState()), () => this.dependencies.clusterStore.clustersList.map(c => c.getState()),
() => this.updateCatalog(this.store.clustersList), () => this.updateCatalog(this.dependencies.clusterStore.clustersList),
{ fireImmediately: false }, { fireImmediately: false },
); );
// reacting to every cluster's preferences change and total amount of items // reacting to every cluster's preferences change and total amount of items
reaction( reaction(
() => this.store.clustersList.map(c => toJS(c.preferences)), () => this.dependencies.clusterStore.clustersList.map(c => toJS(c.preferences)),
() => this.updateCatalog(this.store.clustersList), () => this.updateCatalog(this.dependencies.clusterStore.clustersList),
{ fireImmediately: false }, { fireImmediately: false },
); );
@ -78,7 +76,7 @@ export class ClusterManager extends Singleton {
@action @action
protected updateCatalog(clusters: Cluster[]) { 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) { for (const cluster of clusters) {
this.updateEntityFromCluster(cluster); this.updateEntityFromCluster(cluster);
@ -170,7 +168,7 @@ export class ClusterManager extends Singleton {
@action @action
protected syncClustersFromCatalog(entities: KubernetesCluster[]) { protected syncClustersFromCatalog(entities: KubernetesCluster[]) {
for (const entity of entities) { for (const entity of entities) {
const cluster = this.store.getById(entity.getId()); const cluster = this.dependencies.clusterStore.getById(entity.getId());
if (!cluster) { if (!cluster) {
const model = { const model = {
@ -185,12 +183,12 @@ export class ClusterManager extends Singleton {
* Add the bare minimum of data to ClusterStore. And especially no * Add the bare minimum of data to ClusterStore. And especially no
* preferences, as those might be configured by the entity's source * preferences, as those might be configured by the entity's source
*/ */
this.store.addCluster(model); this.dependencies.clusterStore.addCluster(model);
} catch (error) { } catch (error) {
if (error.code === "ENOENT" && error.path === entity.spec.kubeconfigPath) { 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 { } else {
logger.error(`${logPrefix} failed to add cluster: ${error}`, model); this.dependencies.logger.error(`failed to add cluster: ${error}`, model);
} }
} }
} else { } else {
@ -223,8 +221,8 @@ export class ClusterManager extends Singleton {
} }
protected onNetworkOffline = () => { protected onNetworkOffline = () => {
logger.info(`${logPrefix} network is offline`); this.dependencies.logger.info(`network is offline`);
this.store.clustersList.forEach((cluster) => { this.dependencies.clusterStore.clustersList.forEach((cluster) => {
if (!cluster.disconnected) { if (!cluster.disconnected) {
cluster.online = false; cluster.online = false;
cluster.accessible = false; cluster.accessible = false;
@ -234,8 +232,8 @@ export class ClusterManager extends Singleton {
}; };
protected onNetworkOnline = () => { protected onNetworkOnline = () => {
logger.info(`${logPrefix} network is online`); this.dependencies.logger.info(`network is online`);
this.store.clustersList.forEach((cluster) => { this.dependencies.clusterStore.clustersList.forEach((cluster) => {
if (!cluster.disconnected) { if (!cluster.disconnected) {
cluster.refreshConnectionStatus().catch((e) => e); cluster.refreshConnectionStatus().catch((e) => e);
} }
@ -243,27 +241,10 @@ export class ClusterManager extends Singleton {
}; };
stop() { stop() {
this.store.clusters.forEach((cluster: Cluster) => { this.dependencies.clusterStore.clusters.forEach((cluster: Cluster) => {
cluster.disconnect(); 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) { export function catalogEntityFromCluster(cluster: Cluster) {

View File

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

View File

@ -3,27 +3,24 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import selfsigned from "selfsigned";
import type { Cluster } from "../../common/cluster/cluster"; import type { Cluster } from "../../common/cluster/cluster";
import type { ContextHandlerDependencies } from "./context-handler";
import { ContextHandler } from "./context-handler"; import { ContextHandler } from "./context-handler";
import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable"; import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
import { getKubeAuthProxyCertificate } from "../kube-auth-proxy/get-kube-auth-proxy-certificate"; import getKubeAuthProxyCertificateInjectable from "../kube-auth-proxy/get-proxy-cert.injectable";
import URLParse from "url-parse";
export type CreateContextHandler = (cluster: Cluster) => ContextHandler | undefined;
const createContextHandlerInjectable = getInjectable({ const createContextHandlerInjectable = getInjectable({
id: "create-context-handler", id: "create-context-handler",
instantiate: (di) => { instantiate: (di): CreateContextHandler => {
return (cluster: Cluster) => { const dependencies: ContextHandlerDependencies = {
const clusterUrl = new URLParse(cluster.apiUrl); createKubeAuthProxy: di.inject(createKubeAuthProxyInjectable),
getKubeAuthProxyCertificate: di.inject(getKubeAuthProxyCertificateInjectable),
const dependencies = {
createKubeAuthProxy: di.inject(createKubeAuthProxyInjectable),
authProxyCa: getKubeAuthProxyCertificate(clusterUrl.hostname, selfsigned.generate).cert,
};
return new ContextHandler(dependencies, cluster);
}; };
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 readJsonFileInjectable from "../common/fs/read-json-file.injectable";
import readFileInjectable from "../common/fs/read-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 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 spawnInjectable from "./child-process/spawn.injectable";
import extensionsStoreInjectable from "../extensions/extensions-store/extensions-store.injectable"; import extensionsStoreInjectable from "../extensions/extensions-store/extensions-store.injectable";
import type { ExtensionsStore } from "../extensions/extensions-store/extensions-store"; 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 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 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 clusterStoreInjectable from "../common/cluster/store.injectable";
import type { ClusterStore } from "../common/cluster-store/cluster-store"; import type { ClusterStore } from "../common/cluster/store";
import type { Cluster } from "../common/cluster/cluster"; import type { Cluster } from "../common/cluster/cluster";
import userStoreInjectable from "../common/user-store/user-store.injectable"; import userStoreInjectable from "../common/user-store/user-store.injectable";
import type { UserStore } from "../common/user-store"; 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 joinPathsInjectable from "../common/path/join-paths.injectable";
import { joinPathsFake } from "../common/test-utils/join-paths-fake"; import { joinPathsFake } from "../common/test-utils/join-paths-fake";
import hotbarStoreInjectable from "../common/hotbar-store.injectable"; import hotbarStoreInjectable from "../common/hotbar-store.injectable";
import loggerInjectable from "./logger/logger.injectable";
export const getDiForUnitTesting = ( export const getDiForUnitTesting = (
{ doGeneralOverrides } = { doGeneralOverrides: false }, { doGeneralOverrides } = { doGeneralOverrides: false },
@ -107,7 +107,7 @@ export const getDiForUnitTesting = (
di.override(loggerInjectable, () => ({ di.override(loggerInjectable, () => ({
warn: noop, warn: noop,
debug: noop, debug: noop,
error: (message: string, ...args: any) => console.error(message, ...args), error: noop,
info: noop, info: noop,
})); }));
} }

View File

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

View File

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

View File

@ -7,6 +7,10 @@ import directoryForLensLocalStorageInjectable from "../../../common/directory-fo
import { initIpcMainHandlers } from "./init-ipc-main-handlers"; import { initIpcMainHandlers } from "./init-ipc-main-handlers";
import getAbsolutePathInjectable from "../../../common/path/get-absolute-path.injectable"; import getAbsolutePathInjectable from "../../../common/path/get-absolute-path.injectable";
import applicationMenuItemsInjectable from "../../menu/application-menu-items.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({ const initIpcMainHandlersInjectable = getInjectable({
id: "init-ipc-main-handlers", id: "init-ipc-main-handlers",
@ -15,6 +19,10 @@ const initIpcMainHandlersInjectable = getInjectable({
applicationMenuItems: di.inject(applicationMenuItemsInjectable), applicationMenuItems: di.inject(applicationMenuItemsInjectable),
directoryForLensLocalStorage: di.inject(directoryForLensLocalStorageInjectable), directoryForLensLocalStorage: di.inject(directoryForLensLocalStorageInjectable),
getAbsolutePath: di.inject(getAbsolutePathInjectable), 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 { BrowserWindow, Menu } from "electron";
import { clusterFrameMap } from "../../../common/cluster-frames"; import { clusterFrameMap } from "../../../common/cluster-frames";
import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterDeleteHandler, clusterSetDeletingHandler, clusterClearDeletingHandler } from "../../../common/ipc/cluster"; import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterDeleteHandler, clusterSetDeletingHandler, clusterClearDeletingHandler } from "../../../common/ipc/cluster";
import type { ClusterId } from "../../../common/cluster-types"; import type { ClusterId } from "../../../common/cluster/types";
import { ClusterStore } from "../../../common/cluster-store/cluster-store"; import type { AppEventBus } from "../../../common/app-event-bus/event-bus";
import { appEventBus } from "../../../common/app-event-bus/event-bus";
import { broadcastMainChannel, broadcastMessage, ipcMainHandle, ipcMainOn } from "../../../common/ipc"; import { broadcastMainChannel, broadcastMessage, ipcMainHandle, ipcMainOn } from "../../../common/ipc";
import { catalogEntityRegistry } from "../../catalog"; import { catalogEntityRegistry } from "../../catalog";
import { pushCatalogToRenderer } from "../../catalog-pusher"; import { pushCatalogToRenderer } from "../../catalog-pusher";
import { ClusterManager } from "../../cluster-manager"; import type { ClusterManager } from "../../cluster/manager";
import { ResourceApplier } from "../../resource-applier"; import { ResourceApplier } from "../../resource-applier";
import { remove } from "fs-extra"; import { remove } from "fs-extra";
import { onLocationChange, handleWindowAction } from "../../ipc/window"; 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 { GetAbsolutePath } from "../../../common/path/get-absolute-path.injectable";
import type { IComputedValue } from "mobx"; import type { IComputedValue } from "mobx";
import type { MenuItemOpts } from "../../menu/application-menu-items.injectable"; 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 { interface Dependencies {
directoryForLensLocalStorage: string; directoryForLensLocalStorage: string;
getAbsolutePath: GetAbsolutePath; getAbsolutePath: GetAbsolutePath;
applicationMenuItems: IComputedValue<MenuItemOpts[]>; 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) => { ipcMainHandle(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => {
return ClusterStore.getInstance() return getClusterById(clusterId)
.getById(clusterId)
?.activate(force); ?.activate(force);
}); });
ipcMainHandle(clusterSetFrameIdHandler, (event: IpcMainInvokeEvent, clusterId: ClusterId) => { ipcMainHandle(clusterSetFrameIdHandler, (event: IpcMainInvokeEvent, clusterId: ClusterId) => {
const cluster = ClusterStore.getInstance().getById(clusterId); const cluster = getClusterById(clusterId);
if (cluster) { if (cluster) {
clusterFrameMap.set(cluster.id, { frameId: event.frameId, processId: event.processId }); clusterFrameMap.set(cluster.id, { frameId: event.frameId, processId: event.processId });
@ -51,18 +63,17 @@ export const initIpcMainHandlers = ({ applicationMenuItems, directoryForLensLoca
}); });
ipcMainOn(clusterVisibilityHandler, (event, clusterId?: ClusterId) => { ipcMainOn(clusterVisibilityHandler, (event, clusterId?: ClusterId) => {
ClusterManager.getInstance().visibleCluster = clusterId; clusterManager.visibleCluster = clusterId;
}); });
ipcMainHandle(clusterRefreshHandler, (event, clusterId: ClusterId) => { ipcMainHandle(clusterRefreshHandler, (event, clusterId: ClusterId) => {
return ClusterStore.getInstance() return getClusterById(clusterId)
.getById(clusterId)
?.refresh({ refreshMetadata: true }); ?.refresh({ refreshMetadata: true });
}); });
ipcMainHandle(clusterDisconnectHandler, (event, clusterId: ClusterId) => { ipcMainHandle(clusterDisconnectHandler, (event, clusterId: ClusterId) => {
appEventBus.emit({ name: "cluster", action: "stop" }); appEventBus.emit({ name: "cluster", action: "stop" });
const cluster = ClusterStore.getInstance().getById(clusterId); const cluster = getClusterById(clusterId);
if (cluster) { if (cluster) {
cluster.disconnect(); cluster.disconnect();
@ -73,8 +84,7 @@ export const initIpcMainHandlers = ({ applicationMenuItems, directoryForLensLoca
ipcMainHandle(clusterDeleteHandler, async (event, clusterId: ClusterId) => { ipcMainHandle(clusterDeleteHandler, async (event, clusterId: ClusterId) => {
appEventBus.emit({ name: "cluster", action: "remove" }); appEventBus.emit({ name: "cluster", action: "remove" });
const clusterStore = ClusterStore.getInstance(); const cluster = getClusterById(clusterId);
const cluster = clusterStore.getById(clusterId);
if (!cluster) { if (!cluster) {
return; return;
@ -97,16 +107,16 @@ export const initIpcMainHandlers = ({ applicationMenuItems, directoryForLensLoca
}); });
ipcMainHandle(clusterSetDeletingHandler, (event, clusterId: string) => { ipcMainHandle(clusterSetDeletingHandler, (event, clusterId: string) => {
ClusterManager.getInstance().deleting.add(clusterId); clusterManager.deleting.add(clusterId);
}); });
ipcMainHandle(clusterClearDeletingHandler, (event, clusterId: string) => { ipcMainHandle(clusterClearDeletingHandler, (event, clusterId: string) => {
ClusterManager.getInstance().deleting.delete(clusterId); clusterManager.deleting.delete(clusterId);
}); });
ipcMainHandle(clusterKubectlApplyAllHandler, async (event, clusterId: ClusterId, resources: string[], extraArgs: string[]) => { ipcMainHandle(clusterKubectlApplyAllHandler, async (event, clusterId: ClusterId, resources: string[], extraArgs: string[]) => {
appEventBus.emit({ name: "cluster", action: "kubectl-apply-all" }); appEventBus.emit({ name: "cluster", action: "kubectl-apply-all" });
const cluster = ClusterStore.getInstance().getById(clusterId); const cluster = getClusterById(clusterId);
if (cluster) { if (cluster) {
const applier = new ResourceApplier(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[]) => { ipcMainHandle(clusterKubectlDeleteAllHandler, async (event, clusterId: ClusterId, resources: string[], extraArgs: string[]) => {
appEventBus.emit({ name: "cluster", action: "kubectl-delete-all" }); appEventBus.emit({ name: "cluster", action: "kubectl-delete-all" });
const cluster = ClusterStore.getInstance().getById(clusterId); const cluster = getClusterById(clusterId);
if (cluster) { if (cluster) {
const applier = new ResourceApplier(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 request from "request-promise-native";
import { apiKubePrefix } from "../common/vars"; import { apiKubePrefix } from "../common/vars";
import type { IMetricsReqParams } from "../common/k8s-api/endpoints/metrics.api"; import type { IMetricsReqParams } from "../common/k8s-api/endpoints/metrics.api";
import { LensProxy } from "./lens-proxy";
import type { Cluster } from "../common/cluster/cluster"; 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> { 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.headers ??= {};
options.json ??= true; 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 { KubeAuthProxy } from "./kube-auth-proxy";
import type { Cluster } from "../../common/cluster/cluster"; import type { Cluster } from "../../common/cluster/cluster";
import path from "path"; import path from "path";
import selfsigned from "selfsigned";
import { getBinaryName } from "../../common/vars"; import { getBinaryName } from "../../common/vars";
import directoryForBundledBinariesInjectable from "../../common/app-paths/directory-for-bundled-binaries/directory-for-bundled-binaries.injectable"; import directoryForBundledBinariesInjectable from "../../common/app-paths/directory-for-bundled-binaries/directory-for-bundled-binaries.injectable";
import spawnInjectable from "../child-process/spawn.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; export type CreateKubeAuthProxy = (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => KubeAuthProxy;
@ -20,17 +19,13 @@ const createKubeAuthProxyInjectable = getInjectable({
instantiate: (di): CreateKubeAuthProxy => { instantiate: (di): CreateKubeAuthProxy => {
const binaryName = getBinaryName("lens-k8s-proxy"); const binaryName = getBinaryName("lens-k8s-proxy");
const dependencies: KubeAuthProxyDependencies = {
return (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => { proxyBinPath: path.join(di.inject(directoryForBundledBinariesInjectable), binaryName),
const clusterUrl = new URL(cluster.apiUrl); getKubeAuthProxyCertificate: di.inject(getKubeAuthProxyCertificateInjectable),
const dependencies: KubeAuthProxyDependencies = { spawn: di.inject(spawnInjectable),
proxyBinPath: path.join(di.inject(directoryForBundledBinariesInjectable), binaryName),
proxyCert: getKubeAuthProxyCertificate(clusterUrl.hostname, selfsigned.generate),
spawn: di.inject(spawnInjectable),
};
return new KubeAuthProxy(dependencies, cluster, environmentVariables);
}; };
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. * 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 { waitUntilUsed } from "tcp-port-used";
import { randomBytes } from "crypto"; import { randomBytes } from "crypto";
import type { Cluster } from "../../common/cluster/cluster"; import type { Cluster } from "../../common/cluster/cluster";
import logger from "../logger"; import logger from "../logger";
import { getPortFrom } from "../utils/get-port"; import { getPortFrom } from "../utils/get-port";
import { makeObservable, observable, when } from "mobx"; 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 type { SelfSignedCert } from "selfsigned";
import { URL } from "url";
const startingServeRegex = /starting to serve on (?<address>.+)/i; const startingServeRegex = /starting to serve on (?<address>.+)/i;
export interface KubeAuthProxyDependencies { export interface KubeAuthProxyDependencies {
proxyBinPath: string; readonly proxyBinPath: string;
proxyCert: SelfSignedCert; spawn: Spawn;
spawn: typeof spawn; getKubeAuthProxyCertificate: GetKubeAuthProxyCertificate;
} }
export class KubeAuthProxy { export class KubeAuthProxy {
@ -30,9 +33,12 @@ export class KubeAuthProxy {
protected _port: number; protected _port: number;
protected proxyProcess?: ChildProcess; protected proxyProcess?: ChildProcess;
@observable protected ready = false; @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); makeObservable(this);
this.proxyCert = this.dependencies.getKubeAuthProxyCertificate(new URL(cluster.apiUrl).hostname);
} }
get whenReady() { get whenReady() {
@ -45,7 +51,6 @@ export class KubeAuthProxy {
} }
const proxyBin = this.dependencies.proxyBinPath; const proxyBin = this.dependencies.proxyBinPath;
const cert = this.dependencies.proxyCert;
this.proxyProcess = this.dependencies.spawn(proxyBin, [], { this.proxyProcess = this.dependencies.spawn(proxyBin, [], {
env: { env: {
@ -53,8 +58,8 @@ export class KubeAuthProxy {
KUBECONFIG: this.cluster.kubeConfigPath, KUBECONFIG: this.cluster.kubeConfigPath,
KUBECONFIG_CONTEXT: this.cluster.contextName, KUBECONFIG_CONTEXT: this.cluster.contextName,
API_PREFIX: this.apiPrefix, API_PREFIX: this.apiPrefix,
PROXY_KEY: cert.private, PROXY_KEY: this.proxyCert.private,
PROXY_CERT: cert.cert, PROXY_CERT: this.proxyCert.cert,
}, },
}); });
this.proxyProcess.on("error", (error) => { this.proxyProcess.on("error", (error) => {

View File

@ -5,21 +5,26 @@
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import type { Cluster } from "../../common/cluster/cluster"; import type { Cluster } from "../../common/cluster/cluster";
import directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable"; 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 { KubeconfigManager } from "./kubeconfig-manager";
import lensProxyPortInjectable from "../lens-proxy/port.injectable";
export interface KubeConfigManagerInstantiationParameter { export interface KubeConfigManagerInstantiationParameter {
cluster: Cluster; cluster: Cluster;
} }
export type CreateKubeConfigManager = (cluster: Cluster) => KubeconfigManager | undefined;
const createKubeconfigManagerInjectable = getInjectable({ const createKubeconfigManagerInjectable = getInjectable({
id: "create-kubeconfig-manager", id: "create-kubeconfig-manager",
instantiate: (di) => { instantiate: (di): CreateKubeConfigManager => {
const dependencies = { const dependencies: KubeconfigManagerDependencies = {
directoryForTemp: di.inject(directoryForTempInjectable), 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 { KubeConfig } from "@kubernetes/client-node";
import type { Cluster } from "../../common/cluster/cluster"; import type { Cluster } from "../../common/cluster/cluster";
import type { ContextHandler } from "../context-handler/context-handler";
import path from "path"; import path from "path";
import fs from "fs-extra"; import fs from "fs-extra";
import { dumpConfigYaml } from "../../common/kube-helpers"; import { dumpConfigYaml } from "../../common/kube-helpers";
import logger from "../logger"; import logger from "../logger";
import { LensProxy } from "../lens-proxy"; import type { IObservableValue } from "mobx";
import { waitUntilSet } from "../../common/utils/wait-observable-value";
interface Dependencies { export interface KubeconfigManagerDependencies {
directoryForTemp: string; readonly directoryForTemp: string;
readonly proxyPort: IObservableValue<number | undefined>;
} }
export class KubeconfigManager { export class KubeconfigManager {
@ -26,11 +27,7 @@ export class KubeconfigManager {
*/ */
protected tempFilePath: string | null | undefined = null; protected tempFilePath: string | null | undefined = null;
protected contextHandler: ContextHandler; constructor(protected readonly dependencies: KubeconfigManagerDependencies, protected readonly cluster: Cluster) {}
constructor(private dependencies: Dependencies, protected cluster: Cluster) {
this.contextHandler = cluster.contextHandler;
}
/** /**
* *
@ -71,17 +68,13 @@ export class KubeconfigManager {
protected async ensureFile() { protected async ensureFile() {
try { try {
await this.contextHandler.ensureServer(); await this.cluster.contextHandler.ensureServer();
this.tempFilePath = await this.createProxyKubeconfig(); this.tempFilePath = await this.createProxyKubeconfig();
} catch (error) { } 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. * 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. * 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}`, `kubeconfig-${id}`,
); );
const kubeConfig = await cluster.getKubeconfig(); const kubeConfig = await cluster.getKubeconfig();
const proxyPort = await waitUntilSet(this.dependencies.proxyPort);
const proxyConfig: Partial<KubeConfig> = { const proxyConfig: Partial<KubeConfig> = {
currentContext: contextName, currentContext: contextName,
clusters: [ clusters: [
{ {
name: contextName, name: contextName,
server: this.resolveProxyUrl, server: `http://127.0.0.1:${proxyPort}/${this.cluster.id}`,
skipTLSVerify: undefined, skipTLSVerify: undefined,
}, },
], ],

View File

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

View File

@ -7,24 +7,25 @@ import net from "net";
import type http from "http"; import type http from "http";
import spdy from "spdy"; import spdy from "spdy";
import type httpProxy from "http-proxy"; import type httpProxy from "http-proxy";
import { apiPrefix, apiKubePrefix } from "../common/vars"; import { apiPrefix, apiKubePrefix } from "../../common/vars";
import type { Router } from "./router/router"; import type { Router } from "../router/router";
import type { ContextHandler } from "./context-handler/context-handler"; import type { ContextHandler } from "../context-handler/context-handler";
import logger from "./logger"; import { appEventBus } from "../../common/app-event-bus/event-bus";
import { Singleton } from "../common/utils"; import { getBoolean } from "../utils/parse-query";
import type { Cluster } from "../common/cluster/cluster"; import type { GetClusterForRequest } from "../cluster/get-cluster-for-request.injectable";
import type { ProxyApiRequestArgs } from "./proxy-functions"; import type { IObservableValue } from "mobx";
import { appEventBus } from "../common/app-event-bus/event-bus"; import type { Logger } from "../../common/logger";
import { getBoolean } from "./utils/parse-query"; import type { Cluster } from "../../common/cluster/cluster";
type GetClusterForRequest = (req: http.IncomingMessage) => Cluster | null; export interface ProxyApiRequestArgs {
req: http.IncomingMessage;
export interface LensProxyFunctions { socket: net.Socket;
getClusterForRequest: GetClusterForRequest; head: Buffer;
shellApiRequest: (args: ProxyApiRequestArgs) => void | Promise<void>; cluster: Cluster;
kubeApiUpgradeRequest: (args: ProxyApiRequestArgs) => void | Promise<void>;
} }
export type LensProxyRequestHandler = (args: ProxyApiRequestArgs) => void;
const watchParam = "watch"; const watchParam = "watch";
const followParam = "follow"; const followParam = "follow";
@ -52,22 +53,24 @@ const disallowedPorts = new Set([
10080, 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 origin: string;
protected proxyServer: http.Server; protected proxyServer: http.Server;
protected closed = false; protected closed = false;
protected retryCounters = new Map<string, number>(); protected readonly 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;
constructor(protected readonly dependencies: Dependencies) {
this.configureProxy(this.dependencies.proxy);
this.proxyServer = spdy.createServer({ this.proxyServer = spdy.createServer({
spdy: { spdy: {
plain: true, plain: true,
@ -79,17 +82,22 @@ export class LensProxy extends Singleton {
this.proxyServer this.proxyServer
.on("upgrade", (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => { .on("upgrade", (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => {
const cluster = getClusterForRequest(req); const cluster = this.dependencies.getClusterForRequest(req);
if (!cluster) { 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(); socket.destroy();
} else { } else {
const isInternal = req.url.startsWith(`${apiPrefix}?`); 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 }))() try {
.catch(error => logger.error("[LENS-PROXY]: failed to handle proxy upgrade", error)); 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; 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) => { 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 }}); appEventBus.emit({ name: "lens-proxy", action: "listen", params: { port }});
resolve(port); resolve(port);
}) })
.once("error", (error) => { .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); reject(error);
}); });
}); });
@ -144,7 +152,7 @@ export class LensProxy extends Singleton {
return; 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)) { if (seenPorts.has(port)) {
/** /**
@ -160,7 +168,7 @@ export class LensProxy extends Singleton {
} }
close() { close() {
logger.info("[LENS-PROXY]: Closing server"); this.dependencies.logger.info("Closing server");
this.proxyServer.close(); this.proxyServer.close();
this.closed = true; this.closed = true;
} }
@ -183,10 +191,10 @@ export class LensProxy extends Singleton {
return; 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) { 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)) { if (req.method === "GET" && (!res.statusCode || res.statusCode >= 500)) {
const reqId = this.getRequestId(req); const reqId = this.getRequestId(req);
@ -194,11 +202,11 @@ export class LensProxy extends Singleton {
const timeoutMs = retryCount * 250; const timeoutMs = retryCount * 250;
if (retryCount < 20) { if (retryCount < 20) {
logger.debug(`Retrying proxy request to url: ${reqId}`); this.dependencies.logger.debug(`Retrying proxy request to url: ${reqId}`);
setTimeout(() => { setTimeout(() => {
this.retryCounters.set(reqId, retryCount + 1); this.retryCounters.set(reqId, retryCount + 1);
this.handleRequest(req, res) 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); }, timeoutMs);
} }
} }
@ -207,7 +215,7 @@ export class LensProxy extends Singleton {
try { try {
res.writeHead(500).end(`Oops, something went wrong.\n${error}`); res.writeHead(500).end(`Oops, something went wrong.\n${error}`);
} catch (e) { } 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) { protected async handleRequest(req: http.IncomingMessage, res: http.ServerResponse) {
const cluster = this.getClusterForRequest(req); const cluster = this.dependencies.getClusterForRequest(req);
if (cluster) { if (cluster) {
const proxyTarget = await this.getProxyTarget(req, cluster.contextHandler); const proxyTarget = await this.getProxyTarget(req, cluster.contextHandler);
if (proxyTarget) { 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. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { ShellRequestAuthenticator } from "./shell-request-authenticator"; import { ShellRequestAuthenticator } from "./authenticator";
const shellRequestAuthenticatorInjectable = getInjectable({ const shellRequestAuthenticatorInjectable = getInjectable({
id: "shell-request-authenticator", 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 { getInjectable } from "@ogre-tools/injectable";
import { checkForUpdates } from "../app-updater"; import { checkForUpdates } from "../app-updater";
import { docsUrl, productName, supportUrl } from "../../common/vars"; import { docsUrl, productName, supportUrl } from "../../common/vars";
import { exitApp } from "../exit-app";
import { broadcastMessage } from "../../common/ipc"; import { broadcastMessage } from "../../common/ipc";
import { openBrowser } from "../../common/utils"; import { openBrowser } from "../../common/utils";
import { showAbout } from "./menu"; import { showAbout } from "./menu";
import windowManagerInjectable from "../window-manager.injectable"; import windowManagerInjectable from "../window/manager.injectable";
import type { import type { BrowserWindow, MenuItem, MenuItemConstructorOptions } from "electron";
BrowserWindow, import { webContents } from "electron";
MenuItem,
MenuItemConstructorOptions } from "electron";
import {
webContents,
} from "electron";
import loggerInjectable from "../../common/logger.injectable";
import appNameInjectable from "../app-paths/app-name/app-name.injectable"; import appNameInjectable from "../app-paths/app-name/app-name.injectable";
import electronMenuItemsInjectable from "./electron-menu-items.injectable"; import electronMenuItemsInjectable from "./electron-menu-items.injectable";
import isAutoUpdateEnabledInjectable from "../is-auto-update-enabled.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 navigateToAddClusterInjectable from "../../common/front-end-routing/routes/add-cluster/navigate-to-add-cluster.injectable";
import isMacInjectable from "../../common/vars/is-mac.injectable"; import isMacInjectable from "../../common/vars/is-mac.injectable";
import { computed } from "mobx"; import { computed } from "mobx";
import appMenuLoggerInjectable from "./logger.injectable";
import exitAppInjectable from "../utils/exit-app.injectable";
function ignoreIf(check: boolean, menuItems: MenuItemConstructorOptions[]) { function ignoreIf(check: boolean, menuItems: MenuItemConstructorOptions[]) {
return check ? [] : menuItems; return check ? [] : menuItems;
@ -41,26 +36,22 @@ const applicationMenuItemsInjectable = getInjectable({
id: "application-menu-items", id: "application-menu-items",
instantiate: (di) => { instantiate: (di) => {
const logger = di.inject(loggerInjectable); const logger = di.inject(appMenuLoggerInjectable);
const appName = di.inject(appNameInjectable); const appName = di.inject(appNameInjectable);
const isMac = di.inject(isMacInjectable); const isMac = di.inject(isMacInjectable);
const isAutoUpdateEnabled = di.inject(isAutoUpdateEnabledInjectable); const isAutoUpdateEnabled = di.inject(isAutoUpdateEnabledInjectable);
const electronMenuItems = di.inject(electronMenuItemsInjectable); 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[] => { return computed((): MenuItemOpts[] => {
logger.info(`auto update is ${autoUpdateDisabled ? "disabled" : "enabled"}`);
// 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}`);
const macAppMenu: MenuItemOpts = { const macAppMenu: MenuItemOpts = {
label: appName, label: appName,
@ -269,7 +260,7 @@ const applicationMenuItemsInjectable = getInjectable({
id: "documentation", id: "documentation",
click: async () => { click: async () => {
openBrowser(docsUrl).catch((error) => { 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", id: "support",
click: async () => { click: async () => {
openBrowser(supportUrl).catch((error) => { 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()) { for (const menuItem of electronMenuItems.get()) {
if (!appMenu.has(menuItem.parentId)) { if (!appMenu.has(menuItem.parentId)) {
logger.error( 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 }, { 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. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable } from "@ogre-tools/injectable"; 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"; import { navigateToUrlInjectionToken } from "../../common/front-end-routing/navigate-to-url-injection-token";
const navigateToUrlInjectable = getInjectable({ const navigateToUrlInjectable = getInjectable({

View File

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

View File

@ -3,4 +3,4 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * 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. * 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 URLParse from "url-parse";
import type { LensExtension } from "../../../extensions/lens-extension"; import type { LensExtension } from "../../extensions/lens-extension";
import { broadcastMessage } from "../../../common/ipc"; import { broadcastMessage } from "../../common/ipc";
import { observable, when, makeObservable } from "mobx"; import { observable, when, makeObservable } from "mobx";
import type { RouteAttempt } from "../../../common/protocol-handler"; import type { LensProtocolRouterDependencies, RouteAttempt } from "../../common/protocol-handler";
import { ProtocolHandlerInvalid } from "../../../common/protocol-handler"; import { ProtocolHandlerInvalid } from "../../common/protocol-handler";
import { disposer, noop } from "../../../common/utils"; import { disposer, noop } from "../../common/utils";
import { WindowManager } from "../../window-manager"; import type { EnsureMainWindow } from "../window/ensure-main.injectable";
import type { ExtensionLoader } from "../../../extensions/extension-loader";
import type { ExtensionsStore } from "../../../extensions/extensions-store/extensions-store";
export interface FallbackHandler { export interface FallbackHandler {
(name: string): Promise<boolean>; (name: string): Promise<boolean>;
@ -37,21 +34,19 @@ function checkHost<Query>(url: URLParse<Query>): boolean {
} }
} }
interface Dependencies { interface LensProtocolRouterMainDependencies extends LensProtocolRouterDependencies {
extensionLoader: ExtensionLoader; ensureMainWindow: EnsureMainWindow;
extensionsStore: ExtensionsStore;
} }
export class LensProtocolRouterMain extends proto.LensProtocolRouter { export class LensProtocolRouterMain extends proto.LensProtocolRouter {
private missingExtensionHandlers: FallbackHandler[] = []; private readonly missingExtensionHandlers: FallbackHandler[] = [];
@observable rendererLoaded = false; @observable rendererLoaded = false;
protected disposers = disposer(); protected readonly disposers = disposer();
constructor(protected dependencies: Dependencies) { constructor(protected readonly dependencies: LensProtocolRouterMainDependencies) {
super(dependencies); super(dependencies);
makeObservable(this); makeObservable(this);
} }
@ -73,10 +68,10 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter {
throw new proto.RoutingError(proto.RoutingErrorType.INVALID_PROTOCOL, url); throw new proto.RoutingError(proto.RoutingErrorType.INVALID_PROTOCOL, url);
} }
WindowManager.getInstance(false)?.ensureMainWindow().catch(noop); this.dependencies.ensureMainWindow().catch(noop);
const routeInternally = checkHost(url); const routeInternally = checkHost(url);
logger.info(`${proto.LensProtocolRouter.LoggingPrefix}: routing ${url.toString()}`); this.dependencies.logger.info(`routing ${url.toString()}`);
if (routeInternally) { if (routeInternally) {
this._routeToInternal(url); this._routeToInternal(url);
@ -87,9 +82,9 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter {
broadcastMessage(ProtocolHandlerInvalid, error.toString(), rawUrl); broadcastMessage(ProtocolHandlerInvalid, error.toString(), rawUrl);
if (error instanceof proto.RoutingError) { if (error instanceof proto.RoutingError) {
logger.error(`${proto.LensProtocolRouter.LoggingPrefix}: ${error}`, { url: error.url }); this.dependencies.logger.error(`${error}`, { url: error.url });
} else { } 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. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getOrInsertMap } from "../../../../common/utils"; 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 { ipcMainHandle } from "../../../../common/ipc";
import crypto from "crypto"; import crypto from "crypto";
import { promisify } from "util"; import { promisify } from "util";
@ -11,14 +11,13 @@ import { promisify } from "util";
const randomBytes = promisify(crypto.randomBytes); const randomBytes = promisify(crypto.randomBytes);
export class ShellRequestAuthenticator { export class ShellRequestAuthenticator {
private tokens = new Map<ClusterId, Map<string, Uint8Array>>(); private readonly tokens = new Map<ClusterId, Map<string, Uint8Array>>();
init() { init() {
ipcMainHandle("cluster:shell-api", async (event, clusterId, tabId) => { ipcMainHandle("cluster:shell-api", async (event, clusterId, tabId) => {
const authToken = Uint8Array.from(await randomBytes(128)); 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; 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 { apiPrefix } from "../../../common/vars";
import type { LensApiRequest, Route } from "../../router/router"; import type { LensApiRequest, Route } from "../../router/router";
import { routeInjectionToken } from "../../router/router.injectable"; import { routeInjectionToken } from "../../router/router.injectable";
import type { ClusterPrometheusMetadata } from "../../../common/cluster-types"; import type { ClusterPrometheusMetadata } from "../../../common/cluster/types";
import { ClusterMetadataKey } from "../../../common/cluster-types"; import { ClusterMetadataKey } from "../../../common/cluster/types";
import logger from "../../logger"; import logger from "../../logger";
import type { Cluster } from "../../../common/cluster/cluster"; import type { Cluster } from "../../../common/cluster/cluster";
import { getMetrics } from "../../k8s-request"; import { getMetrics } from "../../k8s-request";

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