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