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:
parent
dbdde19222
commit
50562f4eef
@ -8,12 +8,12 @@ import mockFs from "mock-fs";
|
||||
import path from "path";
|
||||
import fse from "fs-extra";
|
||||
import type { Cluster } from "../cluster/cluster";
|
||||
import { ClusterStore } from "../cluster-store/cluster-store";
|
||||
import { ClusterStore } from "../cluster/store";
|
||||
import { Console } from "console";
|
||||
import { stdout, stderr } from "process";
|
||||
import getCustomKubeConfigDirectoryInjectable from "../app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable";
|
||||
import clusterStoreInjectable from "../cluster-store/cluster-store.injectable";
|
||||
import type { ClusterModel } from "../cluster-types";
|
||||
import clusterStoreInjectable from "../cluster/store.injectable";
|
||||
import type { ClusterModel } from "../cluster/types";
|
||||
import type {
|
||||
DiContainer,
|
||||
} from "@ogre-tools/injectable";
|
||||
|
||||
@ -29,7 +29,7 @@ import { stdout, stderr } from "process";
|
||||
import userStoreInjectable from "../user-store/user-store.injectable";
|
||||
import type { DiContainer } from "@ogre-tools/injectable";
|
||||
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import type { ClusterStoreModel } from "../cluster-store/cluster-store";
|
||||
import type { ClusterStoreModel } from "../cluster/store";
|
||||
import { defaultTheme } from "../vars";
|
||||
import writeFileInjectable from "../fs/write-file.injectable";
|
||||
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
|
||||
|
||||
@ -12,4 +12,9 @@ export interface AppEvent {
|
||||
params?: Record<string, any>;
|
||||
}
|
||||
|
||||
export type AppEventBus = EventEmitter<[AppEvent]>;
|
||||
|
||||
/**
|
||||
* @deprecated use `di.inject(appEventBusInjectable)` instead
|
||||
*/
|
||||
export const appEventBus = new EventEmitter<[AppEvent]>();
|
||||
|
||||
@ -121,14 +121,14 @@ export abstract class BaseStore<T> extends Singleton {
|
||||
|
||||
if (ipcMain) {
|
||||
this.syncDisposers.push(ipcMainOn(this.syncMainChannel, (event, model: T) => {
|
||||
logger.silly(`[STORE]: SYNC ${this.name} from renderer`, { model });
|
||||
logger.debug(`[STORE]: SYNC ${this.name} from renderer`, { model });
|
||||
this.onSync(model);
|
||||
}));
|
||||
}
|
||||
|
||||
if (ipcRenderer) {
|
||||
this.syncDisposers.push(ipcRendererOn(this.syncRendererChannel, (event, model: T) => {
|
||||
logger.silly(`[STORE]: SYNC ${this.name} from main`, { model });
|
||||
logger.debug(`[STORE]: SYNC ${this.name} from main`, { model });
|
||||
this.onSyncFromMain(model);
|
||||
}));
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
|
||||
import type { CatalogEntityActionContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus, CatalogCategorySpec } from "../catalog";
|
||||
import { CatalogEntity, CatalogCategory } from "../catalog";
|
||||
import { ClusterStore } from "../cluster-store/cluster-store";
|
||||
import { ClusterStore } from "../cluster/store";
|
||||
import { broadcastMessage } from "../ipc";
|
||||
import { app } from "electron";
|
||||
import type { CatalogEntitySpec } from "../catalog/catalog-entity";
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { comparer, computed } from "mobx";
|
||||
import hostedClusterInjectable from "./hosted-cluster.injectable";
|
||||
import hostedClusterInjectable from "./hosted.injectable";
|
||||
|
||||
const allowedResourcesInjectable = getInjectable({
|
||||
id: "allowed-resources",
|
||||
@ -3,7 +3,6 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { ipcMain } from "electron";
|
||||
import { action, comparer, computed, makeObservable, observable, reaction, when } from "mobx";
|
||||
import { broadcastMessage } from "../ipc";
|
||||
import type { ContextHandler } from "../../main/context-handler/context-handler";
|
||||
@ -15,21 +14,23 @@ import { loadConfigFromFile, loadConfigFromFileSync, validateKubeConfig } from "
|
||||
import type { KubeApiResource, KubeResource } from "../rbac";
|
||||
import { apiResourceRecord, apiResources } from "../rbac";
|
||||
import logger from "../../main/logger";
|
||||
import { VersionDetector } from "../../main/cluster-detectors/version-detector";
|
||||
import { DetectorRegistry } from "../../main/cluster-detectors/detector-registry";
|
||||
import { VersionDetector } from "../../main/cluster/detectors/version-detector";
|
||||
import { DetectorRegistry } from "../../main/cluster/detectors/detector-registry";
|
||||
import plimit from "p-limit";
|
||||
import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, KubeAuthUpdate } from "../cluster-types";
|
||||
import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus } from "../cluster-types";
|
||||
import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, KubeAuthUpdate } from "./types";
|
||||
import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus } from "./types";
|
||||
import { disposer, toJS } from "../utils";
|
||||
import type { Response } from "request";
|
||||
import { clusterListNamespaceForbiddenChannel } from "../ipc/cluster";
|
||||
import type { CanI } from "./authorization-review.injectable";
|
||||
import type { ListNamespaces } from "./list-namespaces.injectable";
|
||||
import type { CreateKubeConfigManager } from "../../main/kubeconfig-manager/create-kubeconfig-manager.injectable";
|
||||
import type { CreateContextHandler } from "../../main/context-handler/create-context-handler.injectable";
|
||||
|
||||
export interface ClusterDependencies {
|
||||
readonly directoryForKubeConfigs: string;
|
||||
createKubeconfigManager: (cluster: Cluster) => KubeconfigManager;
|
||||
createContextHandler: (cluster: Cluster) => ContextHandler;
|
||||
createKubeconfigManager: CreateKubeConfigManager;
|
||||
createContextHandler: CreateContextHandler;
|
||||
createKubectl: (clusterVersion: string) => Kubectl;
|
||||
createAuthorizationReview: (config: KubeConfig) => CanI;
|
||||
createListNamespaces: (config: KubeConfig) => ListNamespaces;
|
||||
@ -49,9 +50,9 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public contextHandler: ContextHandler;
|
||||
protected proxyKubeconfigManager: KubeconfigManager;
|
||||
protected eventsDisposer = disposer();
|
||||
public readonly contextHandler: ContextHandler | undefined;
|
||||
protected readonly proxyKubeconfigManager: KubeconfigManager | undefined;
|
||||
protected readonly eventsDisposer = disposer();
|
||||
protected activated = false;
|
||||
private resourceAccessStatuses: Map<KubeApiResource, boolean> = new Map();
|
||||
|
||||
@ -233,17 +234,15 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
|
||||
this.apiUrl = config.getCluster(config.getContextObject(this.contextName).cluster).server;
|
||||
|
||||
if (ipcMain) {
|
||||
// for the time being, until renderer gets its own cluster type
|
||||
this.contextHandler = this.dependencies.createContextHandler(this);
|
||||
this.proxyKubeconfigManager = this.dependencies.createKubeconfigManager(this);
|
||||
// for the time being, until renderer gets its own cluster type
|
||||
this.contextHandler = this.dependencies.createContextHandler(this);
|
||||
this.proxyKubeconfigManager = this.dependencies.createKubeconfigManager(this);
|
||||
|
||||
logger.debug(`[CLUSTER]: Cluster init success`, {
|
||||
id: this.id,
|
||||
context: this.contextName,
|
||||
apiUrl: this.apiUrl,
|
||||
});
|
||||
}
|
||||
logger.debug(`[CLUSTER]: Cluster init success`, {
|
||||
id: this.id,
|
||||
context: this.contextName,
|
||||
apiUrl: this.apiUrl,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -571,7 +570,6 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
* @param state cluster state
|
||||
*/
|
||||
pushState(state = this.getState()) {
|
||||
logger.silly(`[CLUSTER]: push-state`, state);
|
||||
broadcastMessage("cluster:state", this.id, state);
|
||||
}
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectionToken } from "@ogre-tools/injectable";
|
||||
import type { ClusterModel } from "../cluster-types";
|
||||
import type { ClusterModel } from "./types";
|
||||
import type { Cluster } from "./cluster";
|
||||
|
||||
export const createClusterInjectionToken =
|
||||
|
||||
21
src/common/cluster/get-by-id.injectable.ts
Normal file
21
src/common/cluster/get-by-id.injectable.ts
Normal 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;
|
||||
@ -4,15 +4,16 @@
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { getHostedClusterId } from "../utils";
|
||||
import clusterStoreInjectable from "./cluster-store.injectable";
|
||||
import getClusterByIdInjectable from "./get-by-id.injectable";
|
||||
|
||||
const hostedClusterInjectable = getInjectable({
|
||||
id: "hosted-cluster",
|
||||
|
||||
instantiate: (di) => {
|
||||
const hostedClusterId = getHostedClusterId();
|
||||
const getClusterById = di.inject(getClusterByIdInjectable);
|
||||
|
||||
return di.inject(clusterStoreInjectable).getById(hostedClusterId);
|
||||
return getClusterById(hostedClusterId);
|
||||
},
|
||||
});
|
||||
|
||||
@ -3,16 +3,19 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { ClusterStore } from "./cluster-store";
|
||||
import { createClusterInjectionToken } from "../cluster/create-cluster-injection-token";
|
||||
import { createClusterInjectionToken } from "./create-cluster-injection-token";
|
||||
import { ClusterStore } from "./store";
|
||||
|
||||
const clusterStoreInjectable = getInjectable({
|
||||
id: "cluster-store",
|
||||
|
||||
instantiate: (di) =>
|
||||
ClusterStore.createInstance({
|
||||
instantiate: (di) => {
|
||||
ClusterStore.resetInstance();
|
||||
|
||||
return ClusterStore.createInstance({
|
||||
createCluster: di.inject(createClusterInjectionToken),
|
||||
}),
|
||||
});
|
||||
},
|
||||
|
||||
causesSideEffects: true,
|
||||
});
|
||||
@ -7,13 +7,13 @@
|
||||
import { ipcMain, ipcRenderer, webFrame } from "electron";
|
||||
import { action, comparer, computed, makeObservable, observable, reaction } from "mobx";
|
||||
import { BaseStore } from "../base-store";
|
||||
import { Cluster } from "../cluster/cluster";
|
||||
import { Cluster } from "./cluster";
|
||||
import migrations from "../../migrations/cluster-store";
|
||||
import logger from "../../main/logger";
|
||||
import { appEventBus } from "../app-event-bus/event-bus";
|
||||
import { ipcMainHandle } from "../ipc";
|
||||
import { disposer, toJS } from "../utils";
|
||||
import type { ClusterModel, ClusterId, ClusterState } from "../cluster-types";
|
||||
import type { ClusterModel, ClusterId, ClusterState } from "./types";
|
||||
import { requestInitialClusterStates } from "../../renderer/ipc";
|
||||
import { clusterStates } from "../ipc/cluster";
|
||||
|
||||
@ -6,8 +6,8 @@ import { getDiForUnitTesting } from "../../renderer/getDiForUnitTesting";
|
||||
import { routeSpecificComponentInjectionToken } from "../../renderer/routes/route-specific-component-injection-token";
|
||||
import { routeInjectionToken } from "./route-injection-token";
|
||||
import { filter, map, matches } from "lodash/fp";
|
||||
import clusterStoreInjectable from "../cluster-store/cluster-store.injectable";
|
||||
import type { ClusterStore } from "../cluster-store/cluster-store";
|
||||
import clusterStoreInjectable from "../cluster/store.injectable";
|
||||
import type { ClusterStore } from "../cluster/store";
|
||||
import { pipeline } from "@ogre-tools/fp";
|
||||
|
||||
describe("verify-that-all-routes-have-component", () => {
|
||||
|
||||
@ -58,7 +58,7 @@ export async function broadcastMessage(channel: string, ...args: any[]): Promise
|
||||
|
||||
// Send message to views.
|
||||
try {
|
||||
logger.silly(`[IPC]: broadcasting "${channel}" to ${viewType}=${view.id}`, { args });
|
||||
logger.debug(`[IPC]: broadcasting "${channel}" to ${viewType}=${view.id}`, { args });
|
||||
view.send(channel, ...args);
|
||||
} catch (error) {
|
||||
logger.error(`[IPC]: failed to send IPC message "${channel}" to view "${viewType}=${view.id}"`, { error });
|
||||
@ -66,7 +66,7 @@ export async function broadcastMessage(channel: string, ...args: any[]): Promise
|
||||
|
||||
// Send message to subFrames of views.
|
||||
for (const frameInfo of subFrames) {
|
||||
logger.silly(`[IPC]: broadcasting "${channel}" to subframe "frameInfo.processId"=${frameInfo.processId} "frameInfo.frameId"=${frameInfo.frameId}`, { args });
|
||||
logger.debug(`[IPC]: broadcasting "${channel}" to subframe "frameInfo.processId"=${frameInfo.processId} "frameInfo.frameId"=${frameInfo.frameId}`, { args });
|
||||
|
||||
try {
|
||||
view.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...args);
|
||||
|
||||
@ -9,7 +9,7 @@ import { ResourceApplier } from "../../main/resource-applier";
|
||||
import type { KubernetesCluster } from "../catalog-entities";
|
||||
import logger from "../../main/logger";
|
||||
import { app } from "electron";
|
||||
import { ClusterStore } from "../cluster-store/cluster-store";
|
||||
import { ClusterStore } from "../cluster/store";
|
||||
import yaml from "js-yaml";
|
||||
import { productName } from "../vars";
|
||||
import { requestKubectlApplyAll, requestKubectlDeleteAll } from "../../renderer/ipc";
|
||||
|
||||
@ -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";
|
||||
@ -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,
|
||||
});
|
||||
52
src/common/logger/child-logger.injectable.ts
Normal file
52
src/common/logger/child-logger.injectable.ts
Normal 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;
|
||||
14
src/common/logger/child-loggers-state.injectable.ts
Normal file
14
src/common/logger/child-loggers-state.injectable.ts
Normal 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;
|
||||
37
src/common/logger/claim-child-logger.injectable.ts
Normal file
37
src/common/logger/claim-child-logger.injectable.ts
Normal 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;
|
||||
35
src/common/logger/index.ts
Normal file
35
src/common/logger/index.ts
Normal 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;
|
||||
10
src/common/logger/logger.token.ts
Normal file
10
src/common/logger/logger.token.ts
Normal 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
11
src/common/logger/type.ts
Normal 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;
|
||||
}
|
||||
15
src/common/protocol-handler/logger.injectable.ts
Normal file
15
src/common/protocol-handler/logger.injectable.ts
Normal 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;
|
||||
@ -8,7 +8,6 @@ import { matchPath } from "react-router";
|
||||
import { countBy } from "lodash";
|
||||
import { iter } from "../utils";
|
||||
import { pathToRegexp } from "path-to-regexp";
|
||||
import logger from "../../main/logger";
|
||||
import type Url from "url-parse";
|
||||
import { RoutingError, RoutingErrorType } from "./error";
|
||||
import type { ExtensionsStore } from "../../extensions/extensions-store/extensions-store";
|
||||
@ -17,6 +16,7 @@ import type { LensExtension } from "../../extensions/lens-extension";
|
||||
import type { RouteHandler, RouteParams } from "../../extensions/registries/protocol-handler";
|
||||
import { when } from "mobx";
|
||||
import { ipcRenderer } from "electron";
|
||||
import type { Logger } from "../logger";
|
||||
|
||||
// IPC channel for protocol actions. Main broadcasts the open-url events to this channel.
|
||||
export const ProtocolHandlerIpcPrefix = "protocol-handler";
|
||||
@ -63,20 +63,19 @@ export function foldAttemptResults(mainAttempt: RouteAttempt, rendererAttempt: R
|
||||
}
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
extensionLoader: ExtensionLoader;
|
||||
extensionsStore: ExtensionsStore;
|
||||
export interface LensProtocolRouterDependencies {
|
||||
readonly extensionLoader: ExtensionLoader;
|
||||
readonly extensionsStore: ExtensionsStore;
|
||||
readonly logger: Logger;
|
||||
}
|
||||
|
||||
export abstract class LensProtocolRouter {
|
||||
// Map between path schemas and the handlers
|
||||
protected internalRoutes = new Map<string, RouteHandler>();
|
||||
|
||||
public static readonly LoggingPrefix = "[PROTOCOL ROUTER]";
|
||||
protected readonly internalRoutes = new Map<string, RouteHandler>();
|
||||
|
||||
static readonly ExtensionUrlSchema = `/:${EXTENSION_PUBLISHER_MATCH}(@[A-Za-z0-9_]+)?/:${EXTENSION_NAME_MATCH}`;
|
||||
|
||||
constructor(protected dependencies: Dependencies) {}
|
||||
constructor(protected readonly dependencies: LensProtocolRouterDependencies) {}
|
||||
|
||||
/**
|
||||
* Attempts to route the given URL to all internal routes that have been registered
|
||||
@ -130,7 +129,7 @@ export abstract class LensProtocolRouter {
|
||||
data.extensionName = extensionName;
|
||||
}
|
||||
|
||||
logger.info(`${LensProtocolRouter.LoggingPrefix}: No handler found`, data);
|
||||
this.dependencies.logger.info(`No handler found`, data);
|
||||
|
||||
return RouteAttempt.MISSING;
|
||||
}
|
||||
@ -183,9 +182,7 @@ export abstract class LensProtocolRouter {
|
||||
timeout: 5_000,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.info(
|
||||
`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not installed (${error})`,
|
||||
);
|
||||
this.dependencies.logger.info(`Extension ${name} matched, but not installed (${error})`);
|
||||
|
||||
return name;
|
||||
}
|
||||
@ -193,18 +190,18 @@ export abstract class LensProtocolRouter {
|
||||
const extension = extensionLoader.getInstanceByName(name);
|
||||
|
||||
if (!extension) {
|
||||
logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but does not have a class for ${ipcRenderer ? "renderer" : "main"}`);
|
||||
this.dependencies.logger.info(`Extension ${name} matched, but does not have a class for ${ipcRenderer ? "renderer" : "main"}`);
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
if (!this.dependencies.extensionsStore.isEnabled(extension)) {
|
||||
logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not enabled`);
|
||||
this.dependencies.logger.info(`Extension ${name} matched, but not enabled`);
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched`);
|
||||
this.dependencies.logger.info(`Extension ${name} matched`);
|
||||
|
||||
return extension;
|
||||
}
|
||||
@ -250,7 +247,7 @@ export abstract class LensProtocolRouter {
|
||||
*/
|
||||
public addInternalHandler(urlSchema: string, handler: RouteHandler): this {
|
||||
pathToRegexp(urlSchema); // verify now that the schema is valid
|
||||
logger.info(`${LensProtocolRouter.LoggingPrefix}: internal registering ${urlSchema}`);
|
||||
this.dependencies.logger.info(`internal registering ${urlSchema}`);
|
||||
this.internalRoutes.set(urlSchema, handler);
|
||||
|
||||
return this;
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { ClusterId } from "../cluster-types";
|
||||
import type { ClusterId } from "../cluster/types";
|
||||
|
||||
/**
|
||||
* Grab the `ClusterId` out of a host that was generated by `getClusterFrameUrl`, or nothing
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { computed } from "mobx";
|
||||
import allowedResourcesInjectable from "../cluster-store/allowed-resources.injectable";
|
||||
import allowedResourcesInjectable from "../cluster/allowed-resources.injectable";
|
||||
import type { KubeResource } from "../rbac";
|
||||
|
||||
export type IsAllowedResource = (resource: KubeResource) => boolean;
|
||||
@ -12,7 +12,7 @@ export type IsAllowedResource = (resource: KubeResource) => boolean;
|
||||
const isAllowedResourceInjectable = getInjectable({
|
||||
id: "is-allowed-resource",
|
||||
|
||||
instantiate: (di, resourceName: KubeResource) => {
|
||||
instantiate: (di, resourceName) => {
|
||||
const allowedResources = di.inject(allowedResourcesInjectable);
|
||||
|
||||
return computed(() => allowedResources.get().has(resourceName));
|
||||
|
||||
26
src/common/utils/wait-observable-value.ts
Normal file
26
src/common/utils/wait-observable-value.ts
Normal 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;
|
||||
}, () => {});
|
||||
});
|
||||
}
|
||||
@ -3,9 +3,9 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { Injectable } from "@ogre-tools/injectable";
|
||||
import { baseLoggerInjectionToken } from "../../common/logger/logger.token";
|
||||
import { asLegacyGlobalForExtensionApi } from "./as-legacy-global-object-for-extension-api";
|
||||
import { getLegacyGlobalDiForExtensionApi } from "./legacy-global-di-for-extension-api";
|
||||
import loggerInjectable from "../../common/logger.injectable";
|
||||
|
||||
export const asLegacyGlobalSingletonForExtensionApi = <
|
||||
Instance,
|
||||
@ -26,11 +26,9 @@ export const asLegacyGlobalSingletonForExtensionApi = <
|
||||
|
||||
resetInstance: () => {
|
||||
const di = getLegacyGlobalDiForExtensionApi();
|
||||
const logger = di.inject(loggerInjectable);
|
||||
const logger = di.inject(baseLoggerInjectionToken);
|
||||
|
||||
logger.warn(
|
||||
`resetInstance() for a legacy global singleton of "${injectable.id}" does nothing.`,
|
||||
);
|
||||
logger.warn(`resetInstance() for a legacy global singleton of "${injectable.id}" does nothing.`);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@ -4,13 +4,14 @@
|
||||
*/
|
||||
|
||||
import { LensExtension } from "./lens-extension";
|
||||
import { WindowManager } from "../main/window-manager";
|
||||
import { catalogEntityRegistry } from "../main/catalog";
|
||||
import type { CatalogEntity } from "../common/catalog";
|
||||
import type { IObservableArray } from "mobx";
|
||||
import type { MenuRegistration } from "../main/menu/menu-registration";
|
||||
import type { TrayMenuRegistration } from "../main/tray/tray-menu-registration";
|
||||
import type { ShellEnvModifier } from "../main/shell-session/shell-env-modifier/shell-env-modifier-registration";
|
||||
import { getLegacyGlobalDiForExtensionApi } from "./as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
|
||||
import windowManagerInjectable from "../main/window/manager.injectable";
|
||||
|
||||
export class LensMainExtension extends LensExtension {
|
||||
appMenus: MenuRegistration[] = [];
|
||||
@ -18,21 +19,24 @@ export class LensMainExtension extends LensExtension {
|
||||
|
||||
/**
|
||||
* implement this to modify the shell environment that Lens terminals are opened with. The ShellEnvModifier type has the signature
|
||||
*
|
||||
*
|
||||
* (ctx: ShellEnvContext, env: Record<string, string | undefined>) => Record<string, string | undefined>
|
||||
*
|
||||
*
|
||||
* @param ctx the shell environment context, specifically the relevant catalog entity for the terminal. This can be used, for example, to get
|
||||
* cluster-specific information that can be made available in the shell environment by the implementation of terminalShellEnvModifier
|
||||
*
|
||||
*
|
||||
* @param env the current shell environment that the terminal will be opened with. The implementation should modify this as desired.
|
||||
*
|
||||
*
|
||||
* @returns the modified shell environment that the terminal will be opened with. The implementation must return env as passed in, if it
|
||||
* does not modify the shell environment
|
||||
*/
|
||||
terminalShellEnvModifier?: ShellEnvModifier;
|
||||
|
||||
async navigate(pageId?: string, params?: Record<string, any>, frameId?: number) {
|
||||
return WindowManager.getInstance().navigateExtension(this.id, pageId, params, frameId);
|
||||
const di = getLegacyGlobalDiForExtensionApi();
|
||||
const windowManager = di.inject(windowManagerInjectable);
|
||||
|
||||
return windowManager.navigateExtension(this.id, pageId, params, frameId);
|
||||
}
|
||||
|
||||
addCatalogSource(id: string, source: IObservableArray<CatalogEntity>) {
|
||||
|
||||
@ -3,8 +3,12 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { WindowManager } from "../../main/window-manager";
|
||||
import windowManagerInjectable from "../../main/window/manager.injectable";
|
||||
import { getLegacyGlobalDiForExtensionApi } from "../as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
|
||||
|
||||
export function navigate(url: string) {
|
||||
return WindowManager.getInstance().navigate(url);
|
||||
const di = getLegacyGlobalDiForExtensionApi();
|
||||
const windowManager = di.inject(windowManagerInjectable);
|
||||
|
||||
return windowManager.navigate(url);
|
||||
}
|
||||
|
||||
@ -38,22 +38,26 @@ import mockFs from "mock-fs";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import { Kubectl } from "../kubectl/kubectl";
|
||||
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
||||
import type { ClusterModel } from "../../common/cluster-types";
|
||||
import type { ClusterModel } from "../../common/cluster/types";
|
||||
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
|
||||
import authorizationReviewInjectable from "../../common/cluster/authorization-review.injectable";
|
||||
import listNamespacesInjectable from "../../common/cluster/list-namespaces.injectable";
|
||||
import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable";
|
||||
import type { DiContainer } from "@ogre-tools/injectable";
|
||||
import type { ContextHandler } from "../context-handler/context-handler";
|
||||
import createKubeconfigManagerInjectable from "../kubeconfig-manager/create-kubeconfig-manager.injectable";
|
||||
|
||||
console = new Console(process.stdout, process.stderr); // fix mockFS
|
||||
|
||||
describe("create clusters", () => {
|
||||
let di: DiContainer;
|
||||
let cluster: Cluster;
|
||||
let createCluster: (model: ClusterModel) => Cluster;
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
mockFs({
|
||||
"minikube-config.yml": JSON.stringify({
|
||||
@ -83,9 +87,11 @@ describe("create clusters", () => {
|
||||
|
||||
di.override(authorizationReviewInjectable, () => () => () => Promise.resolve(true));
|
||||
di.override(listNamespacesInjectable, () => () => () => Promise.resolve([ "default" ]));
|
||||
di.override(createContextHandlerInjectable, () => () => {
|
||||
throw new Error("you should never come here");
|
||||
});
|
||||
di.override(createContextHandlerInjectable, () => () => ({
|
||||
ensureServer: jest.fn(),
|
||||
stopServer: jest.fn(),
|
||||
}) as unknown as ContextHandler);
|
||||
di.override(createKubeconfigManagerInjectable, () => () => undefined);
|
||||
|
||||
createCluster = di.inject(createClusterInjectionToken);
|
||||
|
||||
@ -121,11 +127,6 @@ describe("create clusters", () => {
|
||||
kubeConfigPath: "minikube-config.yml",
|
||||
});
|
||||
|
||||
cluster.contextHandler = {
|
||||
ensureServer: jest.fn(),
|
||||
stopServer: jest.fn(),
|
||||
} as any;
|
||||
|
||||
jest.spyOn(cluster, "reconnect");
|
||||
jest.spyOn(cluster, "refreshConnectionStatus");
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { ClusterModel } from "../../common/cluster-types";
|
||||
import type { ClusterModel } from "../../common/cluster/types";
|
||||
|
||||
jest.mock("winston", () => ({
|
||||
format: {
|
||||
|
||||
@ -31,7 +31,7 @@ jest.mock("winston", () => ({
|
||||
}));
|
||||
|
||||
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
||||
import { KubeconfigManager } from "../kubeconfig-manager/kubeconfig-manager";
|
||||
import type { KubeconfigManager } from "../kubeconfig-manager/kubeconfig-manager";
|
||||
import mockFs from "mock-fs";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import fse from "fs-extra";
|
||||
@ -43,13 +43,15 @@ import { createClusterInjectionToken } from "../../common/cluster/create-cluster
|
||||
import directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable";
|
||||
import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable";
|
||||
import type { DiContainer } from "@ogre-tools/injectable";
|
||||
import type { ContextHandler } from "../context-handler/context-handler";
|
||||
import lensProxyPortInjectable from "../lens-proxy/port.injectable";
|
||||
|
||||
console = new Console(process.stdout, process.stderr); // fix mockFS
|
||||
|
||||
describe("kubeconfig manager tests", () => {
|
||||
let clusterFake: Cluster;
|
||||
let createKubeconfigManager: (cluster: Cluster) => KubeconfigManager;
|
||||
let di: DiContainer;
|
||||
let di: DiContainer;
|
||||
|
||||
beforeEach(async () => {
|
||||
di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
@ -82,10 +84,11 @@ describe("kubeconfig manager tests", () => {
|
||||
|
||||
await di.runSetups();
|
||||
|
||||
di.override(createContextHandlerInjectable, () => () => {
|
||||
throw new Error("you should never come here");
|
||||
});
|
||||
|
||||
di.override(createContextHandlerInjectable, () => () => ({
|
||||
ensureServer: async () => {},
|
||||
stopServer: jest.fn(),
|
||||
}) as unknown as ContextHandler);
|
||||
di.inject(lensProxyPortInjectable).set(9191);
|
||||
const createCluster = di.inject(createClusterInjectionToken);
|
||||
|
||||
createKubeconfigManager = di.inject(createKubeconfigManagerInjectable);
|
||||
@ -95,12 +98,6 @@ describe("kubeconfig manager tests", () => {
|
||||
contextName: "minikube",
|
||||
kubeConfigPath: "minikube-config.yml",
|
||||
});
|
||||
|
||||
clusterFake.contextHandler = {
|
||||
ensureServer: () => Promise.resolve(),
|
||||
} as any;
|
||||
|
||||
jest.spyOn(KubeconfigManager.prototype, "resolveProxyUrl", "get").mockReturnValue("http://127.0.0.1:9191/foo");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { isLongRunningRequest } from "../lens-proxy";
|
||||
import { isLongRunningRequest } from "../lens-proxy/lens-proxy";
|
||||
|
||||
describe("isLongRunningRequest", () => {
|
||||
it("returns true on watches", () => {
|
||||
|
||||
@ -7,19 +7,15 @@ import { ObservableMap } from "mobx";
|
||||
import type { CatalogEntity } from "../../../common/catalog";
|
||||
import { loadFromOptions } from "../../../common/kube-helpers";
|
||||
import type { Cluster } from "../../../common/cluster/cluster";
|
||||
import { computeDiff as computeDiffFor, configToModels } from "../kubeconfig-sync-manager/kubeconfig-sync-manager";
|
||||
import mockFs from "mock-fs";
|
||||
import fs from "fs";
|
||||
import { ClusterManager } from "../../cluster-manager";
|
||||
import clusterStoreInjectable from "../../../common/cluster-store/cluster-store.injectable";
|
||||
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||
import { createClusterInjectionToken } from "../../../common/cluster/create-cluster-injection-token";
|
||||
import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
|
||||
import { ClusterStore } from "../../../common/cluster-store/cluster-store";
|
||||
import getConfigurationFileModelInjectable
|
||||
from "../../../common/get-configuration-file-model/get-configuration-file-model.injectable";
|
||||
import appVersionInjectable
|
||||
from "../../../common/get-configuration-file-model/app-version/app-version.injectable";
|
||||
import type { ComputeDiff } from "../kubeconfig-sync/compute-diff.injectable";
|
||||
import computeDiffInjectable from "../kubeconfig-sync/compute-diff.injectable";
|
||||
import type { ConfigToModels } from "../kubeconfig-sync/config-to-models.injectable";
|
||||
import configToModelsInjectable from "../kubeconfig-sync/config-to-models.injectable";
|
||||
import clusterStoreInjectable from "../../../common/cluster/store.injectable";
|
||||
import type { ClusterStore } from "../../../common/cluster/store";
|
||||
import mockFs from "mock-fs";
|
||||
import { writeFileSync } from "fs";
|
||||
|
||||
jest.mock("electron", () => ({
|
||||
app: {
|
||||
@ -38,7 +34,8 @@ jest.mock("electron", () => ({
|
||||
}));
|
||||
|
||||
describe("kubeconfig-sync.source tests", () => {
|
||||
let computeDiff: ReturnType<typeof computeDiffFor>;
|
||||
let computeDiff: ComputeDiff;
|
||||
let configToModels: ConfigToModels;
|
||||
|
||||
beforeEach(async () => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
@ -47,27 +44,18 @@ describe("kubeconfig-sync.source tests", () => {
|
||||
|
||||
await di.runSetups();
|
||||
|
||||
computeDiff = computeDiffFor({
|
||||
directoryForKubeConfigs: di.inject(directoryForKubeConfigsInjectable),
|
||||
createCluster: di.inject(createClusterInjectionToken),
|
||||
});
|
||||
di.override(clusterStoreInjectable, () => ({
|
||||
getById: jest.fn(),
|
||||
addCluster: jest.fn(),
|
||||
clustersList: [],
|
||||
} as unknown as ClusterStore));
|
||||
|
||||
di.override(clusterStoreInjectable, () =>
|
||||
ClusterStore.createInstance({ createCluster: () => null }),
|
||||
);
|
||||
|
||||
di.permitSideEffects(getConfigurationFileModelInjectable);
|
||||
di.permitSideEffects(appVersionInjectable);
|
||||
|
||||
di.inject(clusterStoreInjectable);
|
||||
|
||||
ClusterManager.createInstance();
|
||||
computeDiff = di.inject(computeDiffInjectable);
|
||||
configToModels = di.inject(configToModelsInjectable);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
ClusterManager.resetInstance();
|
||||
ClusterStore.resetInstance();
|
||||
});
|
||||
|
||||
describe("configsToModels", () => {
|
||||
@ -86,7 +74,7 @@ describe("kubeconfig-sync.source tests", () => {
|
||||
const config = loadFromOptions({
|
||||
clusters: [{
|
||||
name: "cluster-name",
|
||||
server: "1.2.3.4",
|
||||
server: "https://1.2.3.4",
|
||||
skipTLSVerify: false,
|
||||
}],
|
||||
users: [{
|
||||
@ -124,7 +112,7 @@ describe("kubeconfig-sync.source tests", () => {
|
||||
clusters: [{
|
||||
name: "cluster-name",
|
||||
cluster: {
|
||||
server: "1.2.3.4",
|
||||
server: "https://1.2.3.4",
|
||||
},
|
||||
skipTLSVerify: false,
|
||||
}],
|
||||
@ -149,7 +137,7 @@ describe("kubeconfig-sync.source tests", () => {
|
||||
const rootSource = new ObservableMap<string, [Cluster, CatalogEntity]>();
|
||||
const filePath = "/bar";
|
||||
|
||||
fs.writeFileSync(filePath, contents);
|
||||
writeFileSync(filePath, contents);
|
||||
|
||||
computeDiff(contents, rootSource, filePath);
|
||||
|
||||
@ -166,7 +154,7 @@ describe("kubeconfig-sync.source tests", () => {
|
||||
clusters: [{
|
||||
name: "cluster-name",
|
||||
cluster: {
|
||||
server: "1.2.3.4",
|
||||
server: "https://1.2.3.4",
|
||||
},
|
||||
skipTLSVerify: false,
|
||||
}],
|
||||
@ -192,7 +180,7 @@ describe("kubeconfig-sync.source tests", () => {
|
||||
const rootSource = new ObservableMap<string, [Cluster, CatalogEntity]>();
|
||||
const filePath = "/bar";
|
||||
|
||||
fs.writeFileSync(filePath, contents);
|
||||
writeFileSync(filePath, contents);
|
||||
|
||||
computeDiff(contents, rootSource, filePath);
|
||||
|
||||
@ -213,7 +201,7 @@ describe("kubeconfig-sync.source tests", () => {
|
||||
clusters: [{
|
||||
name: "cluster-name",
|
||||
cluster: {
|
||||
server: "1.2.3.4",
|
||||
server: "https://1.2.3.4",
|
||||
},
|
||||
skipTLSVerify: false,
|
||||
}],
|
||||
@ -246,7 +234,7 @@ describe("kubeconfig-sync.source tests", () => {
|
||||
const rootSource = new ObservableMap<string, [Cluster, CatalogEntity]>();
|
||||
const filePath = "/bar";
|
||||
|
||||
fs.writeFileSync(filePath, contents);
|
||||
writeFileSync(filePath, contents);
|
||||
|
||||
computeDiff(contents, rootSource, filePath);
|
||||
|
||||
@ -263,7 +251,7 @@ describe("kubeconfig-sync.source tests", () => {
|
||||
clusters: [{
|
||||
name: "cluster-name",
|
||||
cluster: {
|
||||
server: "1.2.3.4",
|
||||
server: "https://1.2.3.4",
|
||||
},
|
||||
skipTLSVerify: false,
|
||||
}],
|
||||
|
||||
@ -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();
|
||||
}];
|
||||
};
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -4,15 +4,17 @@
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
|
||||
import { KubeconfigSyncManager } from "./kubeconfig-sync-manager";
|
||||
import { createClusterInjectionToken } from "../../../common/cluster/create-cluster-injection-token";
|
||||
import kubeconfigSyncLoggerInjectable from "./logger.injectable";
|
||||
import { KubeconfigSyncManager } from "./manager";
|
||||
import watchFileChangesInjectable from "./watch-file-changes.injectable";
|
||||
|
||||
const kubeconfigSyncManagerInjectable = getInjectable({
|
||||
id: "kubeconfig-sync-manager",
|
||||
|
||||
instantiate: (di) => new KubeconfigSyncManager({
|
||||
directoryForKubeConfigs: di.inject(directoryForKubeConfigsInjectable),
|
||||
createCluster: di.inject(createClusterInjectionToken),
|
||||
logger: di.inject(kubeconfigSyncLoggerInjectable),
|
||||
watchFileChanges: di.inject(watchFileChangesInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
105
src/main/catalog-sources/kubeconfig-sync/manager.ts
Normal file
105
src/main/catalog-sources/kubeconfig-sync/manager.ts
Normal 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()) });
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
@ -5,12 +5,11 @@
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { spawn } from "child_process";
|
||||
|
||||
export type Spawn = typeof spawn;
|
||||
|
||||
const spawnInjectable = getInjectable({
|
||||
id: "spawn",
|
||||
|
||||
instantiate: () => {
|
||||
return spawn;
|
||||
},
|
||||
instantiate: (): Spawn => spawn,
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
|
||||
@ -4,8 +4,8 @@
|
||||
*/
|
||||
|
||||
import type { RequestPromiseOptions } from "request-promise-native";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import { k8sRequest } from "../k8s-request";
|
||||
import type { Cluster } from "../../../common/cluster/cluster";
|
||||
import { k8sRequest } from "../../k8s-request";
|
||||
|
||||
export interface ClusterDetectionResult {
|
||||
value: string | number | boolean;
|
||||
@ -5,7 +5,7 @@
|
||||
|
||||
import { BaseClusterDetector } from "./base-cluster-detector";
|
||||
import { createHash } from "crypto";
|
||||
import { ClusterMetadataKey } from "../../common/cluster-types";
|
||||
import { ClusterMetadataKey } from "../../../common/cluster/types";
|
||||
|
||||
export class ClusterIdDetector extends BaseClusterDetector {
|
||||
key = ClusterMetadataKey.CLUSTER_ID;
|
||||
@ -4,9 +4,9 @@
|
||||
*/
|
||||
|
||||
import { observable } from "mobx";
|
||||
import type { ClusterMetadata } from "../../common/cluster-types";
|
||||
import { Singleton } from "../../common/utils";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import type { ClusterMetadata } from "../../../common/cluster/types";
|
||||
import { Singleton } from "../../../common/utils";
|
||||
import type { Cluster } from "../../../common/cluster/cluster";
|
||||
import type { BaseClusterDetector, ClusterDetectionResult } from "./base-cluster-detector";
|
||||
|
||||
export class DetectorRegistry extends Singleton {
|
||||
@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { BaseClusterDetector } from "./base-cluster-detector";
|
||||
import { ClusterMetadataKey } from "../../common/cluster-types";
|
||||
import { ClusterMetadataKey } from "../../../common/cluster/types";
|
||||
|
||||
export class DistributionDetector extends BaseClusterDetector {
|
||||
key = ClusterMetadataKey.DISTRIBUTION;
|
||||
@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { BaseClusterDetector } from "./base-cluster-detector";
|
||||
import { ClusterMetadataKey } from "../../common/cluster-types";
|
||||
import { ClusterMetadataKey } from "../../../common/cluster/types";
|
||||
|
||||
export class LastSeenDetector extends BaseClusterDetector {
|
||||
key = ClusterMetadataKey.LAST_SEEN;
|
||||
@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { BaseClusterDetector } from "./base-cluster-detector";
|
||||
import { ClusterMetadataKey } from "../../common/cluster-types";
|
||||
import { ClusterMetadataKey } from "../../../common/cluster/types";
|
||||
|
||||
export class NodesCountDetector extends BaseClusterDetector {
|
||||
key = ClusterMetadataKey.NODES_COUNT;
|
||||
@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { BaseClusterDetector } from "./base-cluster-detector";
|
||||
import { ClusterMetadataKey } from "../../common/cluster-types";
|
||||
import { ClusterMetadataKey } from "../../../common/cluster/types";
|
||||
|
||||
export class VersionDetector extends BaseClusterDetector {
|
||||
key = ClusterMetadataKey.VERSION;
|
||||
38
src/main/cluster/get-cluster-for-request.injectable.ts
Normal file
38
src/main/cluster/get-cluster-for-request.injectable.ts
Normal 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;
|
||||
15
src/main/cluster/manager-logger.injectable.ts
Normal file
15
src/main/cluster/manager-logger.injectable.ts
Normal 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;
|
||||
18
src/main/cluster/manager.injectable.ts
Normal file
18
src/main/cluster/manager.injectable.ts
Normal 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;
|
||||
@ -3,48 +3,46 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import "../common/ipc/cluster";
|
||||
import type http from "http";
|
||||
import "../../common/ipc/cluster";
|
||||
import { action, makeObservable, observable, observe, reaction, toJS } from "mobx";
|
||||
import type { Cluster } from "../common/cluster/cluster";
|
||||
import logger from "./logger";
|
||||
import { apiKubePrefix } from "../common/vars";
|
||||
import { getClusterIdFromHost, Singleton } from "../common/utils";
|
||||
import { catalogEntityRegistry } from "./catalog";
|
||||
import type { KubernetesClusterPrometheusMetrics } from "../common/catalog-entities/kubernetes-cluster";
|
||||
import { KubernetesCluster, LensKubernetesClusterStatus } from "../common/catalog-entities/kubernetes-cluster";
|
||||
import { ipcMainOn } from "../common/ipc";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import { catalogEntityRegistry } from "../catalog";
|
||||
import type { KubernetesClusterPrometheusMetrics } from "../../common/catalog-entities/kubernetes-cluster";
|
||||
import { KubernetesCluster, LensKubernetesClusterStatus } from "../../common/catalog-entities/kubernetes-cluster";
|
||||
import { ipcMainOn } from "../../common/ipc";
|
||||
import { once } from "lodash";
|
||||
import { ClusterStore } from "../common/cluster-store/cluster-store";
|
||||
import type { ClusterId } from "../common/cluster-types";
|
||||
|
||||
const logPrefix = "[CLUSTER-MANAGER]:";
|
||||
import type { ClusterId } from "../../common/cluster/types";
|
||||
import type { ClusterStore } from "../../common/cluster/store";
|
||||
import type { Logger } from "../../common/logger";
|
||||
|
||||
const lensSpecificClusterStatuses: Set<string> = new Set(Object.values(LensKubernetesClusterStatus));
|
||||
|
||||
export class ClusterManager extends Singleton {
|
||||
private store = ClusterStore.getInstance();
|
||||
deleting = observable.set<ClusterId>();
|
||||
interface Dependencies {
|
||||
readonly clusterStore: ClusterStore;
|
||||
readonly logger: Logger;
|
||||
}
|
||||
|
||||
export class ClusterManager {
|
||||
readonly deleting = observable.set<ClusterId>();
|
||||
|
||||
@observable visibleCluster: ClusterId | undefined = undefined;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
constructor(protected readonly dependencies: Dependencies) {
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
init = once(() => {
|
||||
// reacting to every cluster's state change and total amount of items
|
||||
reaction(
|
||||
() => this.store.clustersList.map(c => c.getState()),
|
||||
() => this.updateCatalog(this.store.clustersList),
|
||||
() => this.dependencies.clusterStore.clustersList.map(c => c.getState()),
|
||||
() => this.updateCatalog(this.dependencies.clusterStore.clustersList),
|
||||
{ fireImmediately: false },
|
||||
);
|
||||
|
||||
// reacting to every cluster's preferences change and total amount of items
|
||||
reaction(
|
||||
() => this.store.clustersList.map(c => toJS(c.preferences)),
|
||||
() => this.updateCatalog(this.store.clustersList),
|
||||
() => this.dependencies.clusterStore.clustersList.map(c => toJS(c.preferences)),
|
||||
() => this.updateCatalog(this.dependencies.clusterStore.clustersList),
|
||||
{ fireImmediately: false },
|
||||
);
|
||||
|
||||
@ -78,7 +76,7 @@ export class ClusterManager extends Singleton {
|
||||
|
||||
@action
|
||||
protected updateCatalog(clusters: Cluster[]) {
|
||||
logger.debug("[CLUSTER-MANAGER]: updating catalog from cluster store");
|
||||
this.dependencies.logger.debug("updating catalog from cluster store");
|
||||
|
||||
for (const cluster of clusters) {
|
||||
this.updateEntityFromCluster(cluster);
|
||||
@ -170,7 +168,7 @@ export class ClusterManager extends Singleton {
|
||||
@action
|
||||
protected syncClustersFromCatalog(entities: KubernetesCluster[]) {
|
||||
for (const entity of entities) {
|
||||
const cluster = this.store.getById(entity.getId());
|
||||
const cluster = this.dependencies.clusterStore.getById(entity.getId());
|
||||
|
||||
if (!cluster) {
|
||||
const model = {
|
||||
@ -185,12 +183,12 @@ export class ClusterManager extends Singleton {
|
||||
* Add the bare minimum of data to ClusterStore. And especially no
|
||||
* preferences, as those might be configured by the entity's source
|
||||
*/
|
||||
this.store.addCluster(model);
|
||||
this.dependencies.clusterStore.addCluster(model);
|
||||
} catch (error) {
|
||||
if (error.code === "ENOENT" && error.path === entity.spec.kubeconfigPath) {
|
||||
logger.warn(`${logPrefix} kubeconfig file disappeared`, model);
|
||||
this.dependencies.logger.warn(`kubeconfig file disappeared`, model);
|
||||
} else {
|
||||
logger.error(`${logPrefix} failed to add cluster: ${error}`, model);
|
||||
this.dependencies.logger.error(`failed to add cluster: ${error}`, model);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -223,8 +221,8 @@ export class ClusterManager extends Singleton {
|
||||
}
|
||||
|
||||
protected onNetworkOffline = () => {
|
||||
logger.info(`${logPrefix} network is offline`);
|
||||
this.store.clustersList.forEach((cluster) => {
|
||||
this.dependencies.logger.info(`network is offline`);
|
||||
this.dependencies.clusterStore.clustersList.forEach((cluster) => {
|
||||
if (!cluster.disconnected) {
|
||||
cluster.online = false;
|
||||
cluster.accessible = false;
|
||||
@ -234,8 +232,8 @@ export class ClusterManager extends Singleton {
|
||||
};
|
||||
|
||||
protected onNetworkOnline = () => {
|
||||
logger.info(`${logPrefix} network is online`);
|
||||
this.store.clustersList.forEach((cluster) => {
|
||||
this.dependencies.logger.info(`network is online`);
|
||||
this.dependencies.clusterStore.clustersList.forEach((cluster) => {
|
||||
if (!cluster.disconnected) {
|
||||
cluster.refreshConnectionStatus().catch((e) => e);
|
||||
}
|
||||
@ -243,27 +241,10 @@ export class ClusterManager extends Singleton {
|
||||
};
|
||||
|
||||
stop() {
|
||||
this.store.clusters.forEach((cluster: Cluster) => {
|
||||
this.dependencies.clusterStore.clusters.forEach((cluster: Cluster) => {
|
||||
cluster.disconnect();
|
||||
});
|
||||
}
|
||||
|
||||
getClusterForRequest(req: http.IncomingMessage): Cluster {
|
||||
// lens-server is connecting to 127.0.0.1:<port>/<uid>
|
||||
if (req.headers.host.startsWith("127.0.0.1")) {
|
||||
const clusterId = req.url.split("/")[1];
|
||||
const cluster = this.store.getById(clusterId);
|
||||
|
||||
if (cluster) {
|
||||
// we need to swap path prefix so that request is proxied to kube api
|
||||
req.url = req.url.replace(`/${clusterId}`, apiKubePrefix);
|
||||
}
|
||||
|
||||
return cluster;
|
||||
}
|
||||
|
||||
return this.store.getById(getClusterIdFromHost(req.headers.host));
|
||||
}
|
||||
}
|
||||
|
||||
export function catalogEntityFromCluster(cluster: Cluster) {
|
||||
@ -5,7 +5,7 @@
|
||||
|
||||
import type { PrometheusProvider, PrometheusService } from "../prometheus/provider-registry";
|
||||
import { PrometheusProviderRegistry } from "../prometheus/provider-registry";
|
||||
import type { ClusterPrometheusPreferences } from "../../common/cluster-types";
|
||||
import type { ClusterPrometheusPreferences } from "../../common/cluster/types";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import type httpProxy from "http-proxy";
|
||||
import type { UrlWithStringQuery } from "url";
|
||||
@ -14,6 +14,8 @@ import { CoreV1Api } from "@kubernetes/client-node";
|
||||
import logger from "../logger";
|
||||
import type { KubeAuthProxy } from "../kube-auth-proxy/kube-auth-proxy";
|
||||
import type { CreateKubeAuthProxy } from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
|
||||
import type { GetKubeAuthProxyCertificate } from "../kube-auth-proxy/get-proxy-cert.injectable";
|
||||
import type { SelfSignedCert } from "selfsigned";
|
||||
|
||||
export interface PrometheusDetails {
|
||||
prometheusPath: string;
|
||||
@ -27,21 +29,23 @@ interface PrometheusServicePreferences {
|
||||
prefix: string;
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
export interface ContextHandlerDependencies {
|
||||
createKubeAuthProxy: CreateKubeAuthProxy;
|
||||
authProxyCa: string;
|
||||
getKubeAuthProxyCertificate: GetKubeAuthProxyCertificate;
|
||||
}
|
||||
|
||||
export class ContextHandler {
|
||||
public clusterUrl: UrlWithStringQuery;
|
||||
public readonly clusterUrl: UrlWithStringQuery;
|
||||
protected kubeAuthProxy?: KubeAuthProxy;
|
||||
protected apiTarget?: httpProxy.ServerOptions;
|
||||
protected prometheusProvider?: string;
|
||||
protected prometheus?: PrometheusServicePreferences;
|
||||
private readonly proxyCert: SelfSignedCert;
|
||||
|
||||
constructor(private dependencies: Dependencies, protected cluster: Cluster) {
|
||||
constructor(protected readonly dependencies: ContextHandlerDependencies, protected readonly cluster: Cluster) {
|
||||
this.clusterUrl = url.parse(cluster.apiUrl);
|
||||
this.setupPrometheus(cluster.preferences);
|
||||
this.proxyCert = this.dependencies.getKubeAuthProxyCertificate(new URL(cluster.apiUrl).hostname);
|
||||
}
|
||||
|
||||
public setupPrometheus(preferences: ClusterPrometheusPreferences = {}) {
|
||||
@ -125,7 +129,7 @@ export class ContextHandler {
|
||||
}
|
||||
|
||||
resolveAuthProxyCa() {
|
||||
return this.dependencies.authProxyCa;
|
||||
return this.proxyCert.cert;
|
||||
}
|
||||
|
||||
async getApiTarget(isLongRunningRequest = false): Promise<httpProxy.ServerOptions> {
|
||||
@ -141,7 +145,6 @@ export class ContextHandler {
|
||||
protected async newApiTarget(timeout: number): Promise<httpProxy.ServerOptions> {
|
||||
await this.ensureServer();
|
||||
|
||||
const ca = this.dependencies.authProxyCa;
|
||||
const clusterPath = this.clusterUrl.path !== "/" ? this.clusterUrl.path : "";
|
||||
const apiPrefix = `${this.kubeAuthProxy.apiPrefix}${clusterPath}`;
|
||||
|
||||
@ -151,7 +154,7 @@ export class ContextHandler {
|
||||
host: "127.0.0.1",
|
||||
port: this.kubeAuthProxy.port,
|
||||
path: apiPrefix,
|
||||
ca,
|
||||
ca: this.resolveAuthProxyCa(),
|
||||
},
|
||||
changeOrigin: true,
|
||||
timeout,
|
||||
|
||||
@ -3,27 +3,24 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import selfsigned from "selfsigned";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import type { ContextHandlerDependencies } from "./context-handler";
|
||||
import { ContextHandler } from "./context-handler";
|
||||
import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
|
||||
import { getKubeAuthProxyCertificate } from "../kube-auth-proxy/get-kube-auth-proxy-certificate";
|
||||
import URLParse from "url-parse";
|
||||
import getKubeAuthProxyCertificateInjectable from "../kube-auth-proxy/get-proxy-cert.injectable";
|
||||
|
||||
export type CreateContextHandler = (cluster: Cluster) => ContextHandler | undefined;
|
||||
|
||||
const createContextHandlerInjectable = getInjectable({
|
||||
id: "create-context-handler",
|
||||
|
||||
instantiate: (di) => {
|
||||
return (cluster: Cluster) => {
|
||||
const clusterUrl = new URLParse(cluster.apiUrl);
|
||||
|
||||
const dependencies = {
|
||||
createKubeAuthProxy: di.inject(createKubeAuthProxyInjectable),
|
||||
authProxyCa: getKubeAuthProxyCertificate(clusterUrl.hostname, selfsigned.generate).cert,
|
||||
};
|
||||
|
||||
return new ContextHandler(dependencies, cluster);
|
||||
instantiate: (di): CreateContextHandler => {
|
||||
const dependencies: ContextHandlerDependencies = {
|
||||
createKubeAuthProxy: di.inject(createKubeAuthProxyInjectable),
|
||||
getKubeAuthProxyCertificate: di.inject(getKubeAuthProxyCertificateInjectable),
|
||||
};
|
||||
|
||||
return (cluster) => new ContextHandler(dependencies, cluster);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -16,14 +16,13 @@ import writeJsonFileInjectable from "../common/fs/write-json-file.injectable";
|
||||
import readJsonFileInjectable from "../common/fs/read-json-file.injectable";
|
||||
import readFileInjectable from "../common/fs/read-file.injectable";
|
||||
import directoryForBundledBinariesInjectable from "../common/app-paths/directory-for-bundled-binaries/directory-for-bundled-binaries.injectable";
|
||||
import loggerInjectable from "../common/logger.injectable";
|
||||
import spawnInjectable from "./child-process/spawn.injectable";
|
||||
import extensionsStoreInjectable from "../extensions/extensions-store/extensions-store.injectable";
|
||||
import type { ExtensionsStore } from "../extensions/extensions-store/extensions-store";
|
||||
import fileSystemProvisionerStoreInjectable from "../extensions/extension-loader/create-extension-instance/file-system-provisioner-store/file-system-provisioner-store.injectable";
|
||||
import type { FileSystemProvisionerStore } from "../extensions/extension-loader/create-extension-instance/file-system-provisioner-store/file-system-provisioner-store";
|
||||
import clusterStoreInjectable from "../common/cluster-store/cluster-store.injectable";
|
||||
import type { ClusterStore } from "../common/cluster-store/cluster-store";
|
||||
import clusterStoreInjectable from "../common/cluster/store.injectable";
|
||||
import type { ClusterStore } from "../common/cluster/store";
|
||||
import type { Cluster } from "../common/cluster/cluster";
|
||||
import userStoreInjectable from "../common/user-store/user-store.injectable";
|
||||
import type { UserStore } from "../common/user-store";
|
||||
@ -35,6 +34,7 @@ import { getAbsolutePathFake } from "../common/test-utils/get-absolute-path-fake
|
||||
import joinPathsInjectable from "../common/path/join-paths.injectable";
|
||||
import { joinPathsFake } from "../common/test-utils/join-paths-fake";
|
||||
import hotbarStoreInjectable from "../common/hotbar-store.injectable";
|
||||
import loggerInjectable from "./logger/logger.injectable";
|
||||
|
||||
export const getDiForUnitTesting = (
|
||||
{ doGeneralOverrides } = { doGeneralOverrides: false },
|
||||
@ -107,7 +107,7 @@ export const getDiForUnitTesting = (
|
||||
di.override(loggerInjectable, () => ({
|
||||
warn: noop,
|
||||
debug: noop,
|
||||
error: (message: string, ...args: any) => console.error(message, ...args),
|
||||
error: noop,
|
||||
info: noop,
|
||||
}));
|
||||
}
|
||||
|
||||
@ -7,18 +7,13 @@
|
||||
|
||||
import { injectSystemCAs } from "../common/system-ca";
|
||||
import * as Mobx from "mobx";
|
||||
import httpProxy from "http-proxy";
|
||||
import * as LensExtensionsCommonApi from "../extensions/common-api";
|
||||
import * as LensExtensionsMainApi from "../extensions/main-api";
|
||||
import { app, autoUpdater, dialog, powerMonitor } from "electron";
|
||||
import { appName, isIntegrationTesting, isMac, isWindows, productName, staticFilesDirectory } from "../common/vars";
|
||||
import { LensProxy } from "./lens-proxy";
|
||||
import { WindowManager } from "./window-manager";
|
||||
import { ClusterManager } from "./cluster-manager";
|
||||
import { shellSync } from "./shell-sync";
|
||||
import { mangleProxyEnv } from "./proxy-env";
|
||||
import { registerFileProtocol } from "../common/register-protocol";
|
||||
import logger from "./logger";
|
||||
import { appEventBus } from "../common/app-event-bus/event-bus";
|
||||
import type { InstalledExtension } from "../extensions/extension-discovery/extension-discovery";
|
||||
import type { LensExtensionId } from "../extensions/lens-extension";
|
||||
@ -38,30 +33,29 @@ import { WeblinkStore } from "../common/weblink-store";
|
||||
import { initializeSentryReporting } from "../common/sentry";
|
||||
import { ensureDir } from "fs-extra";
|
||||
import { initMenu } from "./menu/menu";
|
||||
import { kubeApiUpgradeRequest } from "./proxy-functions";
|
||||
import { initTray } from "./tray/tray";
|
||||
import { ShellSession } from "./shell-session/shell-session";
|
||||
import { getDi } from "./getDi";
|
||||
import extensionLoaderInjectable from "../extensions/extension-loader/extension-loader.injectable";
|
||||
import lensProtocolRouterMainInjectable from "./protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable";
|
||||
import lensProtocolRouterMainInjectable from "./protocol-handler/router.injectable";
|
||||
import extensionDiscoveryInjectable from "../extensions/extension-discovery/extension-discovery.injectable";
|
||||
import directoryForExesInjectable from "../common/app-paths/directory-for-exes/directory-for-exes.injectable";
|
||||
import initIpcMainHandlersInjectable from "./initializers/init-ipc-main-handlers/init-ipc-main-handlers.injectable";
|
||||
import directoryForKubeConfigsInjectable from "../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
|
||||
import kubeconfigSyncManagerInjectable from "./catalog-sources/kubeconfig-sync-manager/kubeconfig-sync-manager.injectable";
|
||||
import clusterStoreInjectable from "../common/cluster-store/cluster-store.injectable";
|
||||
import routerInjectable from "./router/router.injectable";
|
||||
import shellApiRequestInjectable from "./proxy-functions/shell-api-request/shell-api-request.injectable";
|
||||
import kubeconfigSyncManagerInjectable from "./catalog-sources/kubeconfig-sync/manager.injectable";
|
||||
import userStoreInjectable from "../common/user-store/user-store.injectable";
|
||||
import trayMenuItemsInjectable from "./tray/tray-menu-items.injectable";
|
||||
import { broadcastNativeThemeOnUpdate } from "./native-theme";
|
||||
import windowManagerInjectable from "./window-manager.injectable";
|
||||
import navigateToPreferencesInjectable from "../common/front-end-routing/routes/preferences/navigate-to-preferences.injectable";
|
||||
import windowManagerInjectable from "./window/manager.injectable";
|
||||
import syncGeneralCatalogEntitiesInjectable from "./catalog-sources/sync-general-catalog-entities.injectable";
|
||||
import hotbarStoreInjectable from "../common/hotbar-store.injectable";
|
||||
import applicationMenuItemsInjectable from "./menu/application-menu-items.injectable";
|
||||
import type { DiContainer } from "@ogre-tools/injectable";
|
||||
import { init } from "@sentry/electron/main";
|
||||
import loggerInjectable from "./logger/logger.injectable";
|
||||
import clusterManagerInjectable from "./cluster/manager.injectable";
|
||||
import lensProxyInjectable from "./lens-proxy/proxy.injectable";
|
||||
import lensProxyPortInjectable from "./lens-proxy/port.injectable";
|
||||
import clusterStoreInjectable from "../common/cluster/store.injectable";
|
||||
import initTrayInjectable from "./tray/init-tray.injectable";
|
||||
|
||||
async function main(di: DiContainer) {
|
||||
app.setName(appName);
|
||||
@ -77,6 +71,7 @@ async function main(di: DiContainer) {
|
||||
|
||||
const onCloseCleanup = disposer();
|
||||
const onQuitCleanup = disposer();
|
||||
const logger = di.inject(loggerInjectable);
|
||||
|
||||
logger.info(`📟 Setting ${productName} as protocol client for lens://`);
|
||||
|
||||
@ -120,6 +115,9 @@ async function main(di: DiContainer) {
|
||||
|
||||
broadcastNativeThemeOnUpdate();
|
||||
|
||||
logger.info("🖥️ Starting WindowManager");
|
||||
const windowManager = di.inject(windowManagerInjectable);
|
||||
|
||||
app.on("second-instance", (event, argv) => {
|
||||
logger.debug("second-instance message");
|
||||
|
||||
@ -129,14 +127,14 @@ async function main(di: DiContainer) {
|
||||
}
|
||||
}
|
||||
|
||||
WindowManager.getInstance(false)?.ensureMainWindow();
|
||||
windowManager.ensureMainWindow();
|
||||
});
|
||||
|
||||
app.on("activate", (event, hasVisibleWindows) => {
|
||||
logger.info("APP:ACTIVATE", { hasVisibleWindows });
|
||||
|
||||
if (!hasVisibleWindows) {
|
||||
WindowManager.getInstance(false)?.ensureMainWindow(false);
|
||||
windowManager.ensureMainWindow(false);
|
||||
}
|
||||
});
|
||||
|
||||
@ -150,6 +148,8 @@ async function main(di: DiContainer) {
|
||||
blockQuit = false;
|
||||
});
|
||||
|
||||
const clusterManager = di.inject(clusterManagerInjectable);
|
||||
|
||||
app.on("will-quit", (event) => {
|
||||
logger.debug("will-quit message");
|
||||
|
||||
@ -158,7 +158,7 @@ async function main(di: DiContainer) {
|
||||
|
||||
logger.info("APP:QUIT");
|
||||
appEventBus.emit({ name: "app", action: "close" });
|
||||
ClusterManager.getInstance(false)?.stop(); // close cluster connections
|
||||
clusterManager.stop(); // close cluster connections
|
||||
|
||||
const kubeConfigSyncManager = di.inject(kubeconfigSyncManagerInjectable);
|
||||
|
||||
@ -232,17 +232,10 @@ async function main(di: DiContainer) {
|
||||
|
||||
HelmRepoManager.createInstance(); // create the instance
|
||||
|
||||
const router = di.inject(routerInjectable);
|
||||
const shellApiRequest = di.inject(shellApiRequestInjectable);
|
||||
|
||||
const lensProxy = LensProxy.createInstance(router, httpProxy.createProxy(), {
|
||||
getClusterForRequest: (req) => ClusterManager.getInstance().getClusterForRequest(req),
|
||||
kubeApiUpgradeRequest,
|
||||
shellApiRequest,
|
||||
});
|
||||
|
||||
ClusterManager.createInstance().init();
|
||||
const lensProxy = di.inject(lensProxyInjectable);
|
||||
const proxyPort = di.inject(lensProxyPortInjectable);
|
||||
|
||||
clusterManager.init();
|
||||
initializers.initClusterMetadataDetectors();
|
||||
|
||||
try {
|
||||
@ -257,7 +250,7 @@ async function main(di: DiContainer) {
|
||||
// test proxy connection
|
||||
try {
|
||||
logger.info("🔎 Testing LensProxy connection ...");
|
||||
const versionFromProxy = await getAppVersionFromProxyServer(lensProxy.port);
|
||||
const versionFromProxy = await getAppVersionFromProxyServer(proxyPort.get());
|
||||
|
||||
if (getAppVersion() !== versionFromProxy) {
|
||||
logger.error("Proxy server responded with invalid response");
|
||||
@ -296,16 +289,12 @@ async function main(di: DiContainer) {
|
||||
// (On Windows and Linux, we get a flag. On MacOS, we get special API.)
|
||||
const startHidden = process.argv.includes("--hidden") || (isMac && app.getLoginItemSettings().wasOpenedAsHidden);
|
||||
|
||||
logger.info("🖥️ Starting WindowManager");
|
||||
const windowManager = di.inject(windowManagerInjectable);
|
||||
|
||||
const applicationMenuItems = di.inject(applicationMenuItemsInjectable);
|
||||
const trayMenuItems = di.inject(trayMenuItemsInjectable);
|
||||
const navigateToPreferences = di.inject(navigateToPreferencesInjectable);
|
||||
const initTray = di.inject(initTrayInjectable);
|
||||
|
||||
onQuitCleanup.push(
|
||||
initMenu(applicationMenuItems),
|
||||
await initTray(windowManager, trayMenuItems, navigateToPreferences),
|
||||
await initTray(),
|
||||
() => ShellSession.cleanup(),
|
||||
);
|
||||
|
||||
|
||||
@ -3,12 +3,12 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { ClusterIdDetector } from "../cluster-detectors/cluster-id-detector";
|
||||
import { DetectorRegistry } from "../cluster-detectors/detector-registry";
|
||||
import { DistributionDetector } from "../cluster-detectors/distribution-detector";
|
||||
import { LastSeenDetector } from "../cluster-detectors/last-seen-detector";
|
||||
import { NodesCountDetector } from "../cluster-detectors/nodes-count-detector";
|
||||
import { VersionDetector } from "../cluster-detectors/version-detector";
|
||||
import { ClusterIdDetector } from "../cluster/detectors/cluster-id-detector";
|
||||
import { DetectorRegistry } from "../cluster/detectors/detector-registry";
|
||||
import { DistributionDetector } from "../cluster/detectors/distribution-detector";
|
||||
import { LastSeenDetector } from "../cluster/detectors/last-seen-detector";
|
||||
import { NodesCountDetector } from "../cluster/detectors/nodes-count-detector";
|
||||
import { VersionDetector } from "../cluster/detectors/version-detector";
|
||||
|
||||
export function initClusterMetadataDetectors() {
|
||||
DetectorRegistry.createInstance()
|
||||
|
||||
@ -7,6 +7,10 @@ import directoryForLensLocalStorageInjectable from "../../../common/directory-fo
|
||||
import { initIpcMainHandlers } from "./init-ipc-main-handlers";
|
||||
import getAbsolutePathInjectable from "../../../common/path/get-absolute-path.injectable";
|
||||
import applicationMenuItemsInjectable from "../../menu/application-menu-items.injectable";
|
||||
import appEventBusInjectable from "../../../common/app-event-bus/app-event-bus.injectable";
|
||||
import clusterManagerInjectable from "../../cluster/manager.injectable";
|
||||
import clusterStoreInjectable from "../../../common/cluster/store.injectable";
|
||||
import getClusterByIdInjectable from "../../../common/cluster/get-by-id.injectable";
|
||||
|
||||
const initIpcMainHandlersInjectable = getInjectable({
|
||||
id: "init-ipc-main-handlers",
|
||||
@ -15,6 +19,10 @@ const initIpcMainHandlersInjectable = getInjectable({
|
||||
applicationMenuItems: di.inject(applicationMenuItemsInjectable),
|
||||
directoryForLensLocalStorage: di.inject(directoryForLensLocalStorageInjectable),
|
||||
getAbsolutePath: di.inject(getAbsolutePathInjectable),
|
||||
appEventBus: di.inject(appEventBusInjectable),
|
||||
clusterManager: di.inject(clusterManagerInjectable),
|
||||
clusterStore: di.inject(clusterStoreInjectable),
|
||||
getClusterById: di.inject(getClusterByIdInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@ -7,13 +7,12 @@ import type { IpcMainInvokeEvent } from "electron";
|
||||
import { BrowserWindow, Menu } from "electron";
|
||||
import { clusterFrameMap } from "../../../common/cluster-frames";
|
||||
import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterDeleteHandler, clusterSetDeletingHandler, clusterClearDeletingHandler } from "../../../common/ipc/cluster";
|
||||
import type { ClusterId } from "../../../common/cluster-types";
|
||||
import { ClusterStore } from "../../../common/cluster-store/cluster-store";
|
||||
import { appEventBus } from "../../../common/app-event-bus/event-bus";
|
||||
import type { ClusterId } from "../../../common/cluster/types";
|
||||
import type { AppEventBus } from "../../../common/app-event-bus/event-bus";
|
||||
import { broadcastMainChannel, broadcastMessage, ipcMainHandle, ipcMainOn } from "../../../common/ipc";
|
||||
import { catalogEntityRegistry } from "../../catalog";
|
||||
import { pushCatalogToRenderer } from "../../catalog-pusher";
|
||||
import { ClusterManager } from "../../cluster-manager";
|
||||
import type { ClusterManager } from "../../cluster/manager";
|
||||
import { ResourceApplier } from "../../resource-applier";
|
||||
import { remove } from "fs-extra";
|
||||
import { onLocationChange, handleWindowAction } from "../../ipc/window";
|
||||
@ -25,22 +24,35 @@ import { getNativeThemeChannel } from "../../../common/ipc/native-theme";
|
||||
import type { GetAbsolutePath } from "../../../common/path/get-absolute-path.injectable";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import type { MenuItemOpts } from "../../menu/application-menu-items.injectable";
|
||||
import type { GetClusterById } from "../../../common/cluster/get-by-id.injectable";
|
||||
import type { ClusterStore } from "../../../common/cluster/store";
|
||||
|
||||
interface Dependencies {
|
||||
directoryForLensLocalStorage: string;
|
||||
getAbsolutePath: GetAbsolutePath;
|
||||
applicationMenuItems: IComputedValue<MenuItemOpts[]>;
|
||||
clusterManager: ClusterManager;
|
||||
getClusterById: GetClusterById;
|
||||
appEventBus: AppEventBus;
|
||||
clusterStore: ClusterStore;
|
||||
}
|
||||
|
||||
export const initIpcMainHandlers = ({ applicationMenuItems, directoryForLensLocalStorage, getAbsolutePath }: Dependencies) => () => {
|
||||
export const initIpcMainHandlers = ({
|
||||
applicationMenuItems,
|
||||
directoryForLensLocalStorage,
|
||||
getAbsolutePath,
|
||||
clusterManager,
|
||||
getClusterById,
|
||||
appEventBus,
|
||||
clusterStore,
|
||||
}: Dependencies) => () => {
|
||||
ipcMainHandle(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => {
|
||||
return ClusterStore.getInstance()
|
||||
.getById(clusterId)
|
||||
return getClusterById(clusterId)
|
||||
?.activate(force);
|
||||
});
|
||||
|
||||
ipcMainHandle(clusterSetFrameIdHandler, (event: IpcMainInvokeEvent, clusterId: ClusterId) => {
|
||||
const cluster = ClusterStore.getInstance().getById(clusterId);
|
||||
const cluster = getClusterById(clusterId);
|
||||
|
||||
if (cluster) {
|
||||
clusterFrameMap.set(cluster.id, { frameId: event.frameId, processId: event.processId });
|
||||
@ -51,18 +63,17 @@ export const initIpcMainHandlers = ({ applicationMenuItems, directoryForLensLoca
|
||||
});
|
||||
|
||||
ipcMainOn(clusterVisibilityHandler, (event, clusterId?: ClusterId) => {
|
||||
ClusterManager.getInstance().visibleCluster = clusterId;
|
||||
clusterManager.visibleCluster = clusterId;
|
||||
});
|
||||
|
||||
ipcMainHandle(clusterRefreshHandler, (event, clusterId: ClusterId) => {
|
||||
return ClusterStore.getInstance()
|
||||
.getById(clusterId)
|
||||
return getClusterById(clusterId)
|
||||
?.refresh({ refreshMetadata: true });
|
||||
});
|
||||
|
||||
ipcMainHandle(clusterDisconnectHandler, (event, clusterId: ClusterId) => {
|
||||
appEventBus.emit({ name: "cluster", action: "stop" });
|
||||
const cluster = ClusterStore.getInstance().getById(clusterId);
|
||||
const cluster = getClusterById(clusterId);
|
||||
|
||||
if (cluster) {
|
||||
cluster.disconnect();
|
||||
@ -73,8 +84,7 @@ export const initIpcMainHandlers = ({ applicationMenuItems, directoryForLensLoca
|
||||
ipcMainHandle(clusterDeleteHandler, async (event, clusterId: ClusterId) => {
|
||||
appEventBus.emit({ name: "cluster", action: "remove" });
|
||||
|
||||
const clusterStore = ClusterStore.getInstance();
|
||||
const cluster = clusterStore.getById(clusterId);
|
||||
const cluster = getClusterById(clusterId);
|
||||
|
||||
if (!cluster) {
|
||||
return;
|
||||
@ -97,16 +107,16 @@ export const initIpcMainHandlers = ({ applicationMenuItems, directoryForLensLoca
|
||||
});
|
||||
|
||||
ipcMainHandle(clusterSetDeletingHandler, (event, clusterId: string) => {
|
||||
ClusterManager.getInstance().deleting.add(clusterId);
|
||||
clusterManager.deleting.add(clusterId);
|
||||
});
|
||||
|
||||
ipcMainHandle(clusterClearDeletingHandler, (event, clusterId: string) => {
|
||||
ClusterManager.getInstance().deleting.delete(clusterId);
|
||||
clusterManager.deleting.delete(clusterId);
|
||||
});
|
||||
|
||||
ipcMainHandle(clusterKubectlApplyAllHandler, async (event, clusterId: ClusterId, resources: string[], extraArgs: string[]) => {
|
||||
appEventBus.emit({ name: "cluster", action: "kubectl-apply-all" });
|
||||
const cluster = ClusterStore.getInstance().getById(clusterId);
|
||||
const cluster = getClusterById(clusterId);
|
||||
|
||||
if (cluster) {
|
||||
const applier = new ResourceApplier(cluster);
|
||||
@ -125,7 +135,7 @@ export const initIpcMainHandlers = ({ applicationMenuItems, directoryForLensLoca
|
||||
|
||||
ipcMainHandle(clusterKubectlDeleteAllHandler, async (event, clusterId: ClusterId, resources: string[], extraArgs: string[]) => {
|
||||
appEventBus.emit({ name: "cluster", action: "kubectl-delete-all" });
|
||||
const cluster = ClusterStore.getInstance().getById(clusterId);
|
||||
const cluster = getClusterById(clusterId);
|
||||
|
||||
if (cluster) {
|
||||
const applier = new ResourceApplier(cluster);
|
||||
|
||||
@ -7,11 +7,14 @@ import type { RequestPromiseOptions } from "request-promise-native";
|
||||
import request from "request-promise-native";
|
||||
import { apiKubePrefix } from "../common/vars";
|
||||
import type { IMetricsReqParams } from "../common/k8s-api/endpoints/metrics.api";
|
||||
import { LensProxy } from "./lens-proxy";
|
||||
import type { Cluster } from "../common/cluster/cluster";
|
||||
import { Environments, getEnvironmentSpecificLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
|
||||
import lensProxyPortInjectable from "./lens-proxy/port.injectable";
|
||||
|
||||
export async function k8sRequest<T = any>(cluster: Cluster, path: string, options: RequestPromiseOptions = {}): Promise<T> {
|
||||
const kubeProxyUrl = `http://localhost:${LensProxy.getInstance().port}${apiKubePrefix}`;
|
||||
const di = getEnvironmentSpecificLegacyGlobalDiForExtensionApi(Environments.main);
|
||||
const proxyPort = di.inject(lensProxyPortInjectable);
|
||||
const kubeProxyUrl = `http://localhost:${proxyPort.get()}${apiKubePrefix}`;
|
||||
|
||||
options.headers ??= {};
|
||||
options.json ??= true;
|
||||
|
||||
13
src/main/kube-auth-proxy/cert-cache.injectable.ts
Normal file
13
src/main/kube-auth-proxy/cert-cache.injectable.ts
Normal 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;
|
||||
@ -7,11 +7,10 @@ import type { KubeAuthProxyDependencies } from "./kube-auth-proxy";
|
||||
import { KubeAuthProxy } from "./kube-auth-proxy";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import path from "path";
|
||||
import selfsigned from "selfsigned";
|
||||
import { getBinaryName } from "../../common/vars";
|
||||
import directoryForBundledBinariesInjectable from "../../common/app-paths/directory-for-bundled-binaries/directory-for-bundled-binaries.injectable";
|
||||
import spawnInjectable from "../child-process/spawn.injectable";
|
||||
import { getKubeAuthProxyCertificate } from "./get-kube-auth-proxy-certificate";
|
||||
import getKubeAuthProxyCertificateInjectable from "./get-proxy-cert.injectable";
|
||||
|
||||
export type CreateKubeAuthProxy = (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => KubeAuthProxy;
|
||||
|
||||
@ -20,17 +19,13 @@ const createKubeAuthProxyInjectable = getInjectable({
|
||||
|
||||
instantiate: (di): CreateKubeAuthProxy => {
|
||||
const binaryName = getBinaryName("lens-k8s-proxy");
|
||||
|
||||
return (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => {
|
||||
const clusterUrl = new URL(cluster.apiUrl);
|
||||
const dependencies: KubeAuthProxyDependencies = {
|
||||
proxyBinPath: path.join(di.inject(directoryForBundledBinariesInjectable), binaryName),
|
||||
proxyCert: getKubeAuthProxyCertificate(clusterUrl.hostname, selfsigned.generate),
|
||||
spawn: di.inject(spawnInjectable),
|
||||
};
|
||||
|
||||
return new KubeAuthProxy(dependencies, cluster, environmentVariables);
|
||||
const dependencies: KubeAuthProxyDependencies = {
|
||||
proxyBinPath: path.join(di.inject(directoryForBundledBinariesInjectable), binaryName),
|
||||
getKubeAuthProxyCertificate: di.inject(getKubeAuthProxyCertificateInjectable),
|
||||
spawn: di.inject(spawnInjectable),
|
||||
};
|
||||
|
||||
return (cluster, envVars) => new KubeAuthProxy(dependencies, cluster, envVars);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
13
src/main/kube-auth-proxy/generate-cert.injectable.ts
Normal file
13
src/main/kube-auth-proxy/generate-cert.injectable.ts
Normal 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;
|
||||
@ -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;
|
||||
}
|
||||
43
src/main/kube-auth-proxy/get-proxy-cert.injectable.ts
Normal file
43
src/main/kube-auth-proxy/get-proxy-cert.injectable.ts
Normal 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;
|
||||
@ -3,21 +3,24 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { ChildProcess, spawn } from "child_process";
|
||||
import type { ChildProcess } from "child_process";
|
||||
import { waitUntilUsed } from "tcp-port-used";
|
||||
import { randomBytes } from "crypto";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import logger from "../logger";
|
||||
import { getPortFrom } from "../utils/get-port";
|
||||
import { makeObservable, observable, when } from "mobx";
|
||||
import type { Spawn } from "../child-process/spawn.injectable";
|
||||
import type { GetKubeAuthProxyCertificate } from "./get-proxy-cert.injectable";
|
||||
import type { SelfSignedCert } from "selfsigned";
|
||||
import { URL } from "url";
|
||||
|
||||
const startingServeRegex = /starting to serve on (?<address>.+)/i;
|
||||
|
||||
export interface KubeAuthProxyDependencies {
|
||||
proxyBinPath: string;
|
||||
proxyCert: SelfSignedCert;
|
||||
spawn: typeof spawn;
|
||||
readonly proxyBinPath: string;
|
||||
spawn: Spawn;
|
||||
getKubeAuthProxyCertificate: GetKubeAuthProxyCertificate;
|
||||
}
|
||||
|
||||
export class KubeAuthProxy {
|
||||
@ -30,9 +33,12 @@ export class KubeAuthProxy {
|
||||
protected _port: number;
|
||||
protected proxyProcess?: ChildProcess;
|
||||
@observable protected ready = false;
|
||||
private readonly proxyCert: SelfSignedCert;
|
||||
|
||||
constructor(private dependencies: KubeAuthProxyDependencies, protected readonly cluster: Cluster, protected readonly env: NodeJS.ProcessEnv) {
|
||||
constructor(protected readonly dependencies: KubeAuthProxyDependencies, protected readonly cluster: Cluster, protected readonly env: NodeJS.ProcessEnv) {
|
||||
makeObservable(this);
|
||||
|
||||
this.proxyCert = this.dependencies.getKubeAuthProxyCertificate(new URL(cluster.apiUrl).hostname);
|
||||
}
|
||||
|
||||
get whenReady() {
|
||||
@ -45,7 +51,6 @@ export class KubeAuthProxy {
|
||||
}
|
||||
|
||||
const proxyBin = this.dependencies.proxyBinPath;
|
||||
const cert = this.dependencies.proxyCert;
|
||||
|
||||
this.proxyProcess = this.dependencies.spawn(proxyBin, [], {
|
||||
env: {
|
||||
@ -53,8 +58,8 @@ export class KubeAuthProxy {
|
||||
KUBECONFIG: this.cluster.kubeConfigPath,
|
||||
KUBECONFIG_CONTEXT: this.cluster.contextName,
|
||||
API_PREFIX: this.apiPrefix,
|
||||
PROXY_KEY: cert.private,
|
||||
PROXY_CERT: cert.cert,
|
||||
PROXY_KEY: this.proxyCert.private,
|
||||
PROXY_CERT: this.proxyCert.cert,
|
||||
},
|
||||
});
|
||||
this.proxyProcess.on("error", (error) => {
|
||||
|
||||
@ -5,21 +5,26 @@
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable";
|
||||
import type { KubeconfigManagerDependencies } from "./kubeconfig-manager";
|
||||
import { KubeconfigManager } from "./kubeconfig-manager";
|
||||
import lensProxyPortInjectable from "../lens-proxy/port.injectable";
|
||||
|
||||
export interface KubeConfigManagerInstantiationParameter {
|
||||
cluster: Cluster;
|
||||
}
|
||||
|
||||
export type CreateKubeConfigManager = (cluster: Cluster) => KubeconfigManager | undefined;
|
||||
|
||||
const createKubeconfigManagerInjectable = getInjectable({
|
||||
id: "create-kubeconfig-manager",
|
||||
|
||||
instantiate: (di) => {
|
||||
const dependencies = {
|
||||
instantiate: (di): CreateKubeConfigManager => {
|
||||
const dependencies: KubeconfigManagerDependencies = {
|
||||
directoryForTemp: di.inject(directoryForTempInjectable),
|
||||
proxyPort: di.inject(lensProxyPortInjectable),
|
||||
};
|
||||
|
||||
return (cluster: Cluster) => new KubeconfigManager(dependencies, cluster);
|
||||
return (cluster) => new KubeconfigManager(dependencies, cluster);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -5,15 +5,16 @@
|
||||
|
||||
import type { KubeConfig } from "@kubernetes/client-node";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import type { ContextHandler } from "../context-handler/context-handler";
|
||||
import path from "path";
|
||||
import fs from "fs-extra";
|
||||
import { dumpConfigYaml } from "../../common/kube-helpers";
|
||||
import logger from "../logger";
|
||||
import { LensProxy } from "../lens-proxy";
|
||||
import type { IObservableValue } from "mobx";
|
||||
import { waitUntilSet } from "../../common/utils/wait-observable-value";
|
||||
|
||||
interface Dependencies {
|
||||
directoryForTemp: string;
|
||||
export interface KubeconfigManagerDependencies {
|
||||
readonly directoryForTemp: string;
|
||||
readonly proxyPort: IObservableValue<number | undefined>;
|
||||
}
|
||||
|
||||
export class KubeconfigManager {
|
||||
@ -26,11 +27,7 @@ export class KubeconfigManager {
|
||||
*/
|
||||
protected tempFilePath: string | null | undefined = null;
|
||||
|
||||
protected contextHandler: ContextHandler;
|
||||
|
||||
constructor(private dependencies: Dependencies, protected cluster: Cluster) {
|
||||
this.contextHandler = cluster.contextHandler;
|
||||
}
|
||||
constructor(protected readonly dependencies: KubeconfigManagerDependencies, protected readonly cluster: Cluster) {}
|
||||
|
||||
/**
|
||||
*
|
||||
@ -71,17 +68,13 @@ export class KubeconfigManager {
|
||||
|
||||
protected async ensureFile() {
|
||||
try {
|
||||
await this.contextHandler.ensureServer();
|
||||
await this.cluster.contextHandler.ensureServer();
|
||||
this.tempFilePath = await this.createProxyKubeconfig();
|
||||
} catch (error) {
|
||||
throw Object.assign(new Error("Failed to creat temp config for auth-proxy"), { cause: error });
|
||||
throw new Error(`Failed to creat temp config for auth-proxy: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
get resolveProxyUrl() {
|
||||
return `http://127.0.0.1:${LensProxy.getInstance().port}/${this.cluster.id}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new "temporary" kubeconfig that point to the kubectl-proxy.
|
||||
* This way any user of the config does not need to know anything about the auth etc. details.
|
||||
@ -94,12 +87,13 @@ export class KubeconfigManager {
|
||||
`kubeconfig-${id}`,
|
||||
);
|
||||
const kubeConfig = await cluster.getKubeconfig();
|
||||
const proxyPort = await waitUntilSet(this.dependencies.proxyPort);
|
||||
const proxyConfig: Partial<KubeConfig> = {
|
||||
currentContext: contextName,
|
||||
clusters: [
|
||||
{
|
||||
name: contextName,
|
||||
server: this.resolveProxyUrl,
|
||||
server: `http://127.0.0.1:${proxyPort}/${this.cluster.id}`,
|
||||
skipTLSVerify: undefined,
|
||||
},
|
||||
],
|
||||
|
||||
@ -7,17 +7,17 @@ import { chunk } from "lodash";
|
||||
import tls from "tls";
|
||||
import url from "url";
|
||||
import { apiKubePrefix } from "../../common/vars";
|
||||
import type { ProxyApiRequestArgs } from "./types";
|
||||
import type { LensProxyRequestHandler } from "./lens-proxy";
|
||||
|
||||
const skipRawHeaders = new Set(["Host", "Authorization"]);
|
||||
|
||||
export async function kubeApiUpgradeRequest({ req, socket, head, cluster }: ProxyApiRequestArgs) {
|
||||
export const kubeApiUpgradeRequest: LensProxyRequestHandler = async ({ req, socket, head, cluster }) => {
|
||||
const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, "");
|
||||
const proxyCa = await cluster.contextHandler.resolveAuthProxyCa();
|
||||
const proxyCa = cluster.contextHandler.resolveAuthProxyCa();
|
||||
const apiUrl = url.parse(cluster.apiUrl);
|
||||
const pUrl = url.parse(proxyUrl);
|
||||
const connectOpts = {
|
||||
port: parseInt(pUrl.port),
|
||||
const connectOpts = {
|
||||
port: parseInt(pUrl.port),
|
||||
host: pUrl.hostname,
|
||||
ca: proxyCa,
|
||||
};
|
||||
@ -62,4 +62,4 @@ export async function kubeApiUpgradeRequest({ req, socket, head, cluster }: Prox
|
||||
socket.on("error", function () {
|
||||
proxySocket.end();
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -7,24 +7,25 @@ import net from "net";
|
||||
import type http from "http";
|
||||
import spdy from "spdy";
|
||||
import type httpProxy from "http-proxy";
|
||||
import { apiPrefix, apiKubePrefix } from "../common/vars";
|
||||
import type { Router } from "./router/router";
|
||||
import type { ContextHandler } from "./context-handler/context-handler";
|
||||
import logger from "./logger";
|
||||
import { Singleton } from "../common/utils";
|
||||
import type { Cluster } from "../common/cluster/cluster";
|
||||
import type { ProxyApiRequestArgs } from "./proxy-functions";
|
||||
import { appEventBus } from "../common/app-event-bus/event-bus";
|
||||
import { getBoolean } from "./utils/parse-query";
|
||||
import { apiPrefix, apiKubePrefix } from "../../common/vars";
|
||||
import type { Router } from "../router/router";
|
||||
import type { ContextHandler } from "../context-handler/context-handler";
|
||||
import { appEventBus } from "../../common/app-event-bus/event-bus";
|
||||
import { getBoolean } from "../utils/parse-query";
|
||||
import type { GetClusterForRequest } from "../cluster/get-cluster-for-request.injectable";
|
||||
import type { IObservableValue } from "mobx";
|
||||
import type { Logger } from "../../common/logger";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
|
||||
type GetClusterForRequest = (req: http.IncomingMessage) => Cluster | null;
|
||||
|
||||
export interface LensProxyFunctions {
|
||||
getClusterForRequest: GetClusterForRequest;
|
||||
shellApiRequest: (args: ProxyApiRequestArgs) => void | Promise<void>;
|
||||
kubeApiUpgradeRequest: (args: ProxyApiRequestArgs) => void | Promise<void>;
|
||||
export interface ProxyApiRequestArgs {
|
||||
req: http.IncomingMessage;
|
||||
socket: net.Socket;
|
||||
head: Buffer;
|
||||
cluster: Cluster;
|
||||
}
|
||||
|
||||
export type LensProxyRequestHandler = (args: ProxyApiRequestArgs) => void;
|
||||
|
||||
const watchParam = "watch";
|
||||
const followParam = "follow";
|
||||
|
||||
@ -52,22 +53,24 @@ const disallowedPorts = new Set([
|
||||
10080,
|
||||
]);
|
||||
|
||||
export class LensProxy extends Singleton {
|
||||
interface Dependencies {
|
||||
readonly router: Router;
|
||||
readonly proxy: httpProxy;
|
||||
readonly proxyPort: IObservableValue<number>;
|
||||
readonly logger: Logger;
|
||||
getClusterForRequest: GetClusterForRequest;
|
||||
shellApiRequest: LensProxyRequestHandler;
|
||||
kubeApiUpgradeRequest: LensProxyRequestHandler;
|
||||
}
|
||||
|
||||
export class LensProxy {
|
||||
protected origin: string;
|
||||
protected proxyServer: http.Server;
|
||||
protected closed = false;
|
||||
protected retryCounters = new Map<string, number>();
|
||||
protected getClusterForRequest: GetClusterForRequest;
|
||||
|
||||
public port: number;
|
||||
|
||||
constructor(protected router: Router, protected proxy: httpProxy, { shellApiRequest, kubeApiUpgradeRequest, getClusterForRequest }: LensProxyFunctions) {
|
||||
super();
|
||||
|
||||
this.configureProxy(proxy);
|
||||
|
||||
this.getClusterForRequest = getClusterForRequest;
|
||||
protected readonly retryCounters = new Map<string, number>();
|
||||
|
||||
constructor(protected readonly dependencies: Dependencies) {
|
||||
this.configureProxy(this.dependencies.proxy);
|
||||
this.proxyServer = spdy.createServer({
|
||||
spdy: {
|
||||
plain: true,
|
||||
@ -79,17 +82,22 @@ export class LensProxy extends Singleton {
|
||||
|
||||
this.proxyServer
|
||||
.on("upgrade", (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => {
|
||||
const cluster = getClusterForRequest(req);
|
||||
const cluster = this.dependencies.getClusterForRequest(req);
|
||||
|
||||
if (!cluster) {
|
||||
logger.error(`[LENS-PROXY]: Could not find cluster for upgrade request from url=${req.url}`);
|
||||
this.dependencies.logger.error(`Could not find cluster for upgrade request from url=${req.url}`);
|
||||
socket.destroy();
|
||||
} else {
|
||||
const isInternal = req.url.startsWith(`${apiPrefix}?`);
|
||||
const reqHandler = isInternal ? shellApiRequest : kubeApiUpgradeRequest;
|
||||
const reqHandler = isInternal
|
||||
? this.dependencies.shellApiRequest
|
||||
: this.dependencies.kubeApiUpgradeRequest;
|
||||
|
||||
(async () => reqHandler({ req, socket, head, cluster }))()
|
||||
.catch(error => logger.error("[LENS-PROXY]: failed to handle proxy upgrade", error));
|
||||
try {
|
||||
reqHandler({ req, socket, head, cluster });
|
||||
} catch (error) {
|
||||
this.dependencies.logger.error("failed to handle proxy upgrade", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -110,18 +118,18 @@ export class LensProxy extends Singleton {
|
||||
|
||||
const { address, port } = this.proxyServer.address() as net.AddressInfo;
|
||||
|
||||
logger.info(`[LENS-PROXY]: Proxy server has started at ${address}:${port}`);
|
||||
this.dependencies.logger.info(`Proxy server has started at ${address}:${port}`);
|
||||
|
||||
this.proxyServer.on("error", (error) => {
|
||||
logger.info(`[LENS-PROXY]: Subsequent error: ${error}`);
|
||||
this.dependencies.logger.info(`Subsequent error: ${error}`);
|
||||
});
|
||||
|
||||
this.port = port;
|
||||
this.dependencies.proxyPort.set(port);
|
||||
appEventBus.emit({ name: "lens-proxy", action: "listen", params: { port }});
|
||||
resolve(port);
|
||||
})
|
||||
.once("error", (error) => {
|
||||
logger.info(`[LENS-PROXY]: Proxy server failed to start: ${error}`);
|
||||
this.dependencies.logger.info(`Proxy server failed to start: ${error}`);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
@ -144,7 +152,7 @@ export class LensProxy extends Singleton {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.warn(`[LENS-PROXY]: Proxy server has with port known to be considered unsafe to connect to by chrome, restarting...`);
|
||||
this.dependencies.logger.warn(`Proxy server has with port known to be considered unsafe to connect to by chrome, restarting...`);
|
||||
|
||||
if (seenPorts.has(port)) {
|
||||
/**
|
||||
@ -160,7 +168,7 @@ export class LensProxy extends Singleton {
|
||||
}
|
||||
|
||||
close() {
|
||||
logger.info("[LENS-PROXY]: Closing server");
|
||||
this.dependencies.logger.info("Closing server");
|
||||
this.proxyServer.close();
|
||||
this.closed = true;
|
||||
}
|
||||
@ -183,10 +191,10 @@ export class LensProxy extends Singleton {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.error(`[LENS-PROXY]: http proxy errored for cluster: ${error}`, { url: req.url });
|
||||
this.dependencies.logger.error(`http proxy errored for cluster: ${error}`, { url: req.url });
|
||||
|
||||
if (target) {
|
||||
logger.debug(`Failed proxy to target: ${JSON.stringify(target, null, 2)}`);
|
||||
this.dependencies.logger.debug(`Failed proxy to target: ${JSON.stringify(target, null, 2)}`);
|
||||
|
||||
if (req.method === "GET" && (!res.statusCode || res.statusCode >= 500)) {
|
||||
const reqId = this.getRequestId(req);
|
||||
@ -194,11 +202,11 @@ export class LensProxy extends Singleton {
|
||||
const timeoutMs = retryCount * 250;
|
||||
|
||||
if (retryCount < 20) {
|
||||
logger.debug(`Retrying proxy request to url: ${reqId}`);
|
||||
this.dependencies.logger.debug(`Retrying proxy request to url: ${reqId}`);
|
||||
setTimeout(() => {
|
||||
this.retryCounters.set(reqId, retryCount + 1);
|
||||
this.handleRequest(req, res)
|
||||
.catch(error => logger.error(`[LENS-PROXY]: failed to handle request on proxy error: ${error}`));
|
||||
.catch(error => this.dependencies.logger.error(`failed to handle request on proxy error: ${error}`));
|
||||
}, timeoutMs);
|
||||
}
|
||||
}
|
||||
@ -207,7 +215,7 @@ export class LensProxy extends Singleton {
|
||||
try {
|
||||
res.writeHead(500).end(`Oops, something went wrong.\n${error}`);
|
||||
} catch (e) {
|
||||
logger.error(`[LENS-PROXY]: Failed to write headers: `, e);
|
||||
this.dependencies.logger.error(`Failed to write headers: `, e);
|
||||
}
|
||||
});
|
||||
|
||||
@ -228,15 +236,16 @@ export class LensProxy extends Singleton {
|
||||
}
|
||||
|
||||
protected async handleRequest(req: http.IncomingMessage, res: http.ServerResponse) {
|
||||
const cluster = this.getClusterForRequest(req);
|
||||
const cluster = this.dependencies.getClusterForRequest(req);
|
||||
|
||||
if (cluster) {
|
||||
const proxyTarget = await this.getProxyTarget(req, cluster.contextHandler);
|
||||
|
||||
if (proxyTarget) {
|
||||
return this.proxy.web(req, res, proxyTarget);
|
||||
return this.dependencies.proxy.web(req, res, proxyTarget);
|
||||
}
|
||||
}
|
||||
this.router.route(cluster, req, res);
|
||||
|
||||
this.dependencies.router.route(cluster, req, res);
|
||||
}
|
||||
}
|
||||
15
src/main/lens-proxy/logger.injectable.ts
Normal file
15
src/main/lens-proxy/logger.injectable.ts
Normal 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;
|
||||
13
src/main/lens-proxy/port.injectable.ts
Normal file
13
src/main/lens-proxy/port.injectable.ts
Normal 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;
|
||||
28
src/main/lens-proxy/proxy.injectable.ts
Normal file
28
src/main/lens-proxy/proxy.injectable.ts
Normal 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;
|
||||
42
src/main/lens-proxy/shell-api/handler.injectable.ts
Normal file
42
src/main/lens-proxy/shell-api/handler.injectable.ts
Normal 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;
|
||||
15
src/main/lens-proxy/shell-api/logger.injectable.ts
Normal file
15
src/main/lens-proxy/shell-api/logger.injectable.ts
Normal 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;
|
||||
@ -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;
|
||||
@ -3,7 +3,7 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { ShellRequestAuthenticator } from "./shell-request-authenticator";
|
||||
import { ShellRequestAuthenticator } from "./authenticator";
|
||||
|
||||
const shellRequestAuthenticatorInjectable = getInjectable({
|
||||
id: "shell-request-authenticator",
|
||||
52
src/main/lens-proxy/shell-api/request/authenticator.ts
Normal file
52
src/main/lens-proxy/shell-api/request/authenticator.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
67
src/main/logger/logger.injectable.ts
Normal file
67
src/main/logger/logger.injectable.ts
Normal 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;
|
||||
|
||||
@ -5,19 +5,12 @@
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { checkForUpdates } from "../app-updater";
|
||||
import { docsUrl, productName, supportUrl } from "../../common/vars";
|
||||
import { exitApp } from "../exit-app";
|
||||
import { broadcastMessage } from "../../common/ipc";
|
||||
import { openBrowser } from "../../common/utils";
|
||||
import { showAbout } from "./menu";
|
||||
import windowManagerInjectable from "../window-manager.injectable";
|
||||
import type {
|
||||
BrowserWindow,
|
||||
MenuItem,
|
||||
MenuItemConstructorOptions } from "electron";
|
||||
import {
|
||||
webContents,
|
||||
} from "electron";
|
||||
import loggerInjectable from "../../common/logger.injectable";
|
||||
import windowManagerInjectable from "../window/manager.injectable";
|
||||
import type { BrowserWindow, MenuItem, MenuItemConstructorOptions } from "electron";
|
||||
import { webContents } from "electron";
|
||||
import appNameInjectable from "../app-paths/app-name/app-name.injectable";
|
||||
import electronMenuItemsInjectable from "./electron-menu-items.injectable";
|
||||
import isAutoUpdateEnabledInjectable from "../is-auto-update-enabled.injectable";
|
||||
@ -28,6 +21,8 @@ import navigateToWelcomeInjectable from "../../common/front-end-routing/routes/w
|
||||
import navigateToAddClusterInjectable from "../../common/front-end-routing/routes/add-cluster/navigate-to-add-cluster.injectable";
|
||||
import isMacInjectable from "../../common/vars/is-mac.injectable";
|
||||
import { computed } from "mobx";
|
||||
import appMenuLoggerInjectable from "./logger.injectable";
|
||||
import exitAppInjectable from "../utils/exit-app.injectable";
|
||||
|
||||
function ignoreIf(check: boolean, menuItems: MenuItemConstructorOptions[]) {
|
||||
return check ? [] : menuItems;
|
||||
@ -41,26 +36,22 @@ const applicationMenuItemsInjectable = getInjectable({
|
||||
id: "application-menu-items",
|
||||
|
||||
instantiate: (di) => {
|
||||
const logger = di.inject(loggerInjectable);
|
||||
const logger = di.inject(appMenuLoggerInjectable);
|
||||
const appName = di.inject(appNameInjectable);
|
||||
const isMac = di.inject(isMacInjectable);
|
||||
const isAutoUpdateEnabled = di.inject(isAutoUpdateEnabledInjectable);
|
||||
const electronMenuItems = di.inject(electronMenuItemsInjectable);
|
||||
const windowManager = di.inject(windowManagerInjectable);
|
||||
const navigateToPreferences = di.inject(navigateToPreferencesInjectable);
|
||||
const navigateToExtensions = di.inject(navigateToExtensionsInjectable);
|
||||
const navigateToCatalog = di.inject(navigateToCatalogInjectable);
|
||||
const navigateToWelcome = di.inject(navigateToWelcomeInjectable);
|
||||
const navigateToAddCluster = di.inject(navigateToAddClusterInjectable);
|
||||
const exitApp = di.inject(exitAppInjectable);
|
||||
const autoUpdateDisabled = !isAutoUpdateEnabled();
|
||||
|
||||
return computed((): MenuItemOpts[] => {
|
||||
|
||||
// TODO: These injects should happen outside of the computed.
|
||||
// TODO: Remove temporal dependencies in WindowManager to make sure timing is correct.
|
||||
const windowManager = di.inject(windowManagerInjectable);
|
||||
const navigateToPreferences = di.inject(navigateToPreferencesInjectable);
|
||||
const navigateToExtensions = di.inject(navigateToExtensionsInjectable);
|
||||
const navigateToCatalog = di.inject(navigateToCatalogInjectable);
|
||||
const navigateToWelcome = di.inject(navigateToWelcomeInjectable);
|
||||
const navigateToAddCluster = di.inject(navigateToAddClusterInjectable);
|
||||
|
||||
const autoUpdateDisabled = !isAutoUpdateEnabled();
|
||||
|
||||
logger.info(`[MENU]: autoUpdateDisabled=${autoUpdateDisabled}`);
|
||||
logger.info(`auto update is ${autoUpdateDisabled ? "disabled" : "enabled"}`);
|
||||
|
||||
const macAppMenu: MenuItemOpts = {
|
||||
label: appName,
|
||||
@ -269,7 +260,7 @@ const applicationMenuItemsInjectable = getInjectable({
|
||||
id: "documentation",
|
||||
click: async () => {
|
||||
openBrowser(docsUrl).catch((error) => {
|
||||
logger.error("[MENU]: failed to open browser", { error });
|
||||
logger.error("failed to open browser", { error });
|
||||
});
|
||||
},
|
||||
},
|
||||
@ -278,7 +269,7 @@ const applicationMenuItemsInjectable = getInjectable({
|
||||
id: "support",
|
||||
click: async () => {
|
||||
openBrowser(supportUrl).catch((error) => {
|
||||
logger.error("[MENU]: failed to open browser", { error });
|
||||
logger.error("failed to open browser", { error });
|
||||
});
|
||||
},
|
||||
},
|
||||
@ -316,7 +307,7 @@ const applicationMenuItemsInjectable = getInjectable({
|
||||
for (const menuItem of electronMenuItems.get()) {
|
||||
if (!appMenu.has(menuItem.parentId)) {
|
||||
logger.error(
|
||||
`[MENU]: cannot register menu item for parentId=${menuItem.parentId}, parent item doesn't exist`,
|
||||
`cannot register menu item for parentId=${menuItem.parentId}, parent item doesn't exist`,
|
||||
{ menuItem },
|
||||
);
|
||||
|
||||
|
||||
15
src/main/menu/logger.injectable.ts
Normal file
15
src/main/menu/logger.injectable.ts
Normal 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;
|
||||
@ -3,7 +3,7 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import windowManagerInjectable from "../window-manager.injectable";
|
||||
import windowManagerInjectable from "../window/manager.injectable";
|
||||
import { navigateToUrlInjectionToken } from "../../common/front-end-routing/navigate-to-url-injection-token";
|
||||
|
||||
const navigateToUrlInjectable = getInjectable({
|
||||
|
||||
@ -10,14 +10,15 @@ import { ProtocolHandlerExtension, ProtocolHandlerInternal } from "../../../comm
|
||||
import { delay, noop } from "../../../common/utils";
|
||||
import { LensExtension } from "../../../extensions/main-api";
|
||||
import { ExtensionsStore } from "../../../extensions/extensions-store/extensions-store";
|
||||
import type { LensProtocolRouterMain } from "../lens-protocol-router-main/lens-protocol-router-main";
|
||||
import type { LensProtocolRouterMain } from "../router";
|
||||
import mockFs from "mock-fs";
|
||||
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||
import extensionLoaderInjectable from "../../../extensions/extension-loader/extension-loader.injectable";
|
||||
import lensProtocolRouterMainInjectable from "../lens-protocol-router-main/lens-protocol-router-main.injectable";
|
||||
import lensProtocolRouterMainInjectable from "../router.injectable";
|
||||
import extensionsStoreInjectable from "../../../extensions/extensions-store/extensions-store.injectable";
|
||||
import getConfigurationFileModelInjectable from "../../../common/get-configuration-file-model/get-configuration-file-model.injectable";
|
||||
import appVersionInjectable from "../../../common/get-configuration-file-model/app-version/app-version.injectable";
|
||||
import ensureMainWindowInjectable from "../../window/ensure-main.injectable";
|
||||
|
||||
jest.mock("../../../common/ipc");
|
||||
|
||||
@ -48,10 +49,10 @@ describe("protocol router tests", () => {
|
||||
|
||||
await di.runSetups();
|
||||
|
||||
di.override(ensureMainWindowInjectable, () => async () => null);
|
||||
|
||||
extensionLoader = di.inject(extensionLoaderInjectable);
|
||||
|
||||
extensionsStore = di.inject(extensionsStoreInjectable);
|
||||
|
||||
lpr = di.inject(lensProtocolRouterMainInjectable);
|
||||
|
||||
lpr.rendererLoaded = true;
|
||||
|
||||
@ -3,4 +3,4 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
export * from "./lens-protocol-router-main/lens-protocol-router-main";
|
||||
export * from "./router";
|
||||
|
||||
@ -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;
|
||||
23
src/main/protocol-handler/router.injectable.ts
Normal file
23
src/main/protocol-handler/router.injectable.ts
Normal 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;
|
||||
@ -3,18 +3,15 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import logger from "../../logger";
|
||||
import * as proto from "../../../common/protocol-handler";
|
||||
import * as proto from "../../common/protocol-handler";
|
||||
import URLParse from "url-parse";
|
||||
import type { LensExtension } from "../../../extensions/lens-extension";
|
||||
import { broadcastMessage } from "../../../common/ipc";
|
||||
import type { LensExtension } from "../../extensions/lens-extension";
|
||||
import { broadcastMessage } from "../../common/ipc";
|
||||
import { observable, when, makeObservable } from "mobx";
|
||||
import type { RouteAttempt } from "../../../common/protocol-handler";
|
||||
import { ProtocolHandlerInvalid } from "../../../common/protocol-handler";
|
||||
import { disposer, noop } from "../../../common/utils";
|
||||
import { WindowManager } from "../../window-manager";
|
||||
import type { ExtensionLoader } from "../../../extensions/extension-loader";
|
||||
import type { ExtensionsStore } from "../../../extensions/extensions-store/extensions-store";
|
||||
import type { LensProtocolRouterDependencies, RouteAttempt } from "../../common/protocol-handler";
|
||||
import { ProtocolHandlerInvalid } from "../../common/protocol-handler";
|
||||
import { disposer, noop } from "../../common/utils";
|
||||
import type { EnsureMainWindow } from "../window/ensure-main.injectable";
|
||||
|
||||
export interface FallbackHandler {
|
||||
(name: string): Promise<boolean>;
|
||||
@ -37,21 +34,19 @@ function checkHost<Query>(url: URLParse<Query>): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
extensionLoader: ExtensionLoader;
|
||||
extensionsStore: ExtensionsStore;
|
||||
interface LensProtocolRouterMainDependencies extends LensProtocolRouterDependencies {
|
||||
ensureMainWindow: EnsureMainWindow;
|
||||
}
|
||||
|
||||
export class LensProtocolRouterMain extends proto.LensProtocolRouter {
|
||||
private missingExtensionHandlers: FallbackHandler[] = [];
|
||||
private readonly missingExtensionHandlers: FallbackHandler[] = [];
|
||||
|
||||
@observable rendererLoaded = false;
|
||||
|
||||
protected disposers = disposer();
|
||||
protected readonly disposers = disposer();
|
||||
|
||||
constructor(protected dependencies: Dependencies) {
|
||||
constructor(protected readonly dependencies: LensProtocolRouterMainDependencies) {
|
||||
super(dependencies);
|
||||
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
@ -73,10 +68,10 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter {
|
||||
throw new proto.RoutingError(proto.RoutingErrorType.INVALID_PROTOCOL, url);
|
||||
}
|
||||
|
||||
WindowManager.getInstance(false)?.ensureMainWindow().catch(noop);
|
||||
this.dependencies.ensureMainWindow().catch(noop);
|
||||
const routeInternally = checkHost(url);
|
||||
|
||||
logger.info(`${proto.LensProtocolRouter.LoggingPrefix}: routing ${url.toString()}`);
|
||||
this.dependencies.logger.info(`routing ${url.toString()}`);
|
||||
|
||||
if (routeInternally) {
|
||||
this._routeToInternal(url);
|
||||
@ -87,9 +82,9 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter {
|
||||
broadcastMessage(ProtocolHandlerInvalid, error.toString(), rawUrl);
|
||||
|
||||
if (error instanceof proto.RoutingError) {
|
||||
logger.error(`${proto.LensProtocolRouter.LoggingPrefix}: ${error}`, { url: error.url });
|
||||
this.dependencies.logger.error(`${error}`, { url: error.url });
|
||||
} else {
|
||||
logger.error(`${proto.LensProtocolRouter.LoggingPrefix}: ${error}`, { rawUrl });
|
||||
this.dependencies.logger.error(`${error}`, { rawUrl });
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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";
|
||||
@ -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;
|
||||
@ -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));
|
||||
});
|
||||
};
|
||||
@ -3,7 +3,7 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getOrInsertMap } from "../../../../common/utils";
|
||||
import type { ClusterId } from "../../../../common/cluster-types";
|
||||
import type { ClusterId } from "../../../../common/cluster/types";
|
||||
import { ipcMainHandle } from "../../../../common/ipc";
|
||||
import crypto from "crypto";
|
||||
import { promisify } from "util";
|
||||
@ -11,14 +11,13 @@ import { promisify } from "util";
|
||||
const randomBytes = promisify(crypto.randomBytes);
|
||||
|
||||
export class ShellRequestAuthenticator {
|
||||
private tokens = new Map<ClusterId, Map<string, Uint8Array>>();
|
||||
private readonly tokens = new Map<ClusterId, Map<string, Uint8Array>>();
|
||||
|
||||
init() {
|
||||
ipcMainHandle("cluster:shell-api", async (event, clusterId, tabId) => {
|
||||
const authToken = Uint8Array.from(await randomBytes(128));
|
||||
const forCluster = getOrInsertMap(this.tokens, clusterId);
|
||||
|
||||
forCluster.set(tabId, authToken);
|
||||
getOrInsertMap(this.tokens, clusterId).set(tabId, authToken);
|
||||
|
||||
return authToken;
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
@ -7,8 +7,8 @@ import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { apiPrefix } from "../../../common/vars";
|
||||
import type { LensApiRequest, Route } from "../../router/router";
|
||||
import { routeInjectionToken } from "../../router/router.injectable";
|
||||
import type { ClusterPrometheusMetadata } from "../../../common/cluster-types";
|
||||
import { ClusterMetadataKey } from "../../../common/cluster-types";
|
||||
import type { ClusterPrometheusMetadata } from "../../../common/cluster/types";
|
||||
import { ClusterMetadataKey } from "../../../common/cluster/types";
|
||||
import logger from "../../logger";
|
||||
import type { Cluster } from "../../../common/cluster/cluster";
|
||||
import { getMetrics } from "../../k8s-request";
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user