From 50562f4eefff70d521203dfab0f471ab591c7280 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Thu, 28 Apr 2022 12:32:12 -0400 Subject: [PATCH] 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 --- src/common/__tests__/cluster-store.test.ts | 6 +- src/common/__tests__/user-store.test.ts | 2 +- src/common/app-event-bus/event-bus.ts | 5 + src/common/base-store.ts | 4 +- .../catalog-entities/kubernetes-cluster.ts | 2 +- .../allowed-resources.injectable.ts | 2 +- src/common/cluster/cluster.ts | 40 +- .../cluster/create-cluster-injection-token.ts | 2 +- src/common/cluster/get-by-id.injectable.ts | 21 + .../hosted.injectable.ts} | 5 +- .../store.injectable.ts} | 13 +- .../cluster-store.ts => cluster/store.ts} | 4 +- .../{cluster-types.ts => cluster/types.ts} | 0 ...at-all-routes-have-route-component.test.ts | 4 +- src/common/ipc/ipc.ts | 4 +- src/common/k8s/resource-stack.ts | 2 +- src/common/logger-transports/index.ts | 6 - src/common/logger.ts | 80 ---- src/common/logger/child-logger.injectable.ts | 52 +++ .../logger/child-loggers-state.injectable.ts | 14 + .../logger/claim-child-logger.injectable.ts | 37 ++ src/common/logger/index.ts | 35 ++ src/common/logger/logger.token.ts | 10 + .../transports}/sentry.ts | 0 src/common/logger/type.ts | 11 + .../protocol-handler/logger.injectable.ts | 15 + src/common/protocol-handler/router.ts | 29 +- src/common/utils/cluster-id-url-parsing.ts | 2 +- .../utils/is-allowed-resource.injectable.ts | 4 +- src/common/utils/wait-observable-value.ts | 26 ++ ...obal-singleton-object-for-extension-api.ts | 8 +- src/extensions/lens-main-extension.ts | 16 +- src/extensions/main-api/navigation.ts | 8 +- src/main/__test__/cluster.test.ts | 21 +- src/main/__test__/kube-auth-proxy.test.ts | 2 +- src/main/__test__/kubeconfig-manager.test.ts | 21 +- src/main/__test__/lens-proxy.test.ts | 2 +- .../__test__/kubeconfig-sync.test.ts | 62 ++- .../kubeconfig-sync-manager.ts | 379 ------------------ .../compute-diff.injectable.ts | 103 +++++ .../config-to-models.injectable.ts | 37 ++ .../diff-changed-config.injectable.ts | 90 +++++ .../kubeconfig-sync/logger.injectable.ts | 15 + .../manager.injectable.ts} | 8 +- .../kubeconfig-sync/manager.ts | 105 +++++ .../watch-file-changes.injectable.ts | 140 +++++++ src/main/child-process/spawn.injectable.ts | 7 +- .../detectors}/base-cluster-detector.ts | 4 +- .../detectors}/cluster-id-detector.ts | 2 +- .../detectors}/detector-registry.ts | 6 +- .../detectors}/distribution-detector.ts | 2 +- .../detectors}/last-seen-detector.ts | 2 +- .../detectors}/nodes-count-detector.ts | 2 +- .../detectors}/version-detector.ts | 2 +- .../get-cluster-for-request.injectable.ts | 38 ++ src/main/cluster/manager-logger.injectable.ts | 15 + src/main/cluster/manager.injectable.ts | 18 + .../manager.ts} | 81 ++-- src/main/context-handler/context-handler.ts | 19 +- .../create-context-handler.injectable.ts | 23 +- src/main/exit-app.ts | 23 -- src/main/getDiForUnitTesting.ts | 8 +- src/main/index.ts | 59 ++- .../cluster-metadata-detectors.ts | 12 +- .../init-ipc-main-handlers.injectable.ts | 8 + .../init-ipc-main-handlers.ts | 46 ++- src/main/k8s-request.ts | 7 +- .../kube-auth-proxy/cert-cache.injectable.ts | 13 + .../create-kube-auth-proxy.injectable.ts | 19 +- .../generate-cert.injectable.ts | 13 + .../get-kube-auth-proxy-certificate.ts | 39 -- .../get-proxy-cert.injectable.ts | 43 ++ src/main/kube-auth-proxy/kube-auth-proxy.ts | 21 +- .../create-kubeconfig-manager.injectable.ts | 11 +- .../kubeconfig-manager/kubeconfig-manager.ts | 26 +- .../kube-api-upgrade-request.ts | 12 +- src/main/{ => lens-proxy}/lens-proxy.ts | 101 ++--- src/main/lens-proxy/logger.injectable.ts | 15 + src/main/lens-proxy/port.injectable.ts | 13 + src/main/lens-proxy/proxy.injectable.ts | 28 ++ .../shell-api/handler.injectable.ts | 42 ++ .../lens-proxy/shell-api/logger.injectable.ts | 15 + .../request/authenticate.injectable.ts | 20 + .../request/authenticator.injectable.ts} | 2 +- .../shell-api/request/authenticator.ts | 52 +++ src/main/logger/logger.injectable.ts | 67 ++++ .../menu/application-menu-items.injectable.ts | 45 +-- src/main/menu/logger.injectable.ts | 15 + .../navigate-to-url.injectable.ts | 2 +- .../protocol-handler/__test__/router.test.ts | 9 +- src/main/protocol-handler/index.ts | 2 +- .../lens-protocol-router-main.injectable.ts | 20 - .../protocol-handler/router.injectable.ts | 23 ++ ...lens-protocol-router-main.ts => router.ts} | 37 +- src/main/proxy-functions/index.ts | 6 - .../shell-api-request.injectable.ts | 20 - .../shell-api-request/shell-api-request.ts | 44 -- .../shell-request-authenticator.ts | 7 +- src/main/proxy-functions/types.ts | 15 - .../metrics/add-metrics-route.injectable.ts | 4 +- .../local-shell-session.ts | 2 +- .../terminal-shell-env-modifiers.ts | 8 +- src/main/shell-session/shell-session.ts | 6 +- src/main/tray/icon.ts | 60 +++ src/main/tray/init-tray.injectable.ts | 58 +++ src/main/tray/logger.injectable.ts | 15 + src/main/tray/tray-menu-items.injectable.ts | 9 +- src/main/tray/tray-menu-items.test.ts | 4 +- src/main/tray/tray-menu.injectable.ts | 90 +++++ src/main/tray/tray.ts | 181 --------- src/main/utils/exit-app.injectable.ts | 36 ++ src/main/window-manager.injectable.ts | 20 - src/main/window/ensure-main.injectable.ts | 19 + src/main/window/logger.injectable.ts | 15 + src/main/window/manager.injectable.ts | 18 + .../{window-manager.ts => window/manager.ts} | 65 +-- src/migrations/cluster-store/3.6.0-beta.1.ts | 2 +- src/migrations/cluster-store/5.0.0-beta.10.ts | 2 +- src/migrations/cluster-store/5.0.0-beta.13.ts | 2 +- src/migrations/cluster-store/snap.ts | 2 +- src/migrations/hotbar-store/5.0.0-beta.10.ts | 2 +- src/migrations/user-store/5.0.3-beta.1.ts | 2 +- src/renderer/api/catalog-entity-registry.ts | 2 +- src/renderer/bootstrap.tsx | 2 +- .../cluster-frame-context.injectable.ts | 3 +- .../cluster-frame-context.ts | 4 +- .../+cluster/cluster-no-metrics.tsx | 2 +- .../components/+cluster/cluster-overview.tsx | 4 +- .../+namespaces/namespace-details.tsx | 5 +- .../+network-ingresses/ingress-details.tsx | 2 +- src/renderer/components/+nodes/details.tsx | 5 +- .../get-all-entries.injectable.ts | 20 + .../kubeconfig-syncs/get-entry.injectable.ts | 44 ++ .../kubeconfig-syncs/logger.injectable.ts | 15 + .../view.tsx} | 92 ++--- .../components/+preferences/kubernetes.tsx | 2 +- .../volume-claim-details.tsx | 2 +- .../daemonset-details.tsx | 5 +- .../deployment-details.tsx | 5 +- .../+workloads-jobs/job-details.tsx | 5 +- .../+workloads-pods/pod-details-container.tsx | 2 +- .../+workloads-pods/pod-details.tsx | 2 +- .../replicaset-details.tsx | 5 +- .../statefulset-details.tsx | 5 +- .../cluster-manager/cluster-status.tsx | 2 +- .../cluster-manager/cluster-view.tsx | 2 +- .../components/cluster-manager/lens-views.ts | 4 +- .../cluster-settings/cluster-settings.tsx | 2 +- .../components/cluster-metrics-setting.tsx | 2 +- .../components/cluster-node-shell-setting.tsx | 2 +- .../command-palette/command-container.tsx | 8 +- .../__tests__/delete-cluster-dialog.test.tsx | 4 +- .../test-utils/get-application-builder.tsx | 62 +-- .../create-cluster.injectable.ts | 6 +- .../init-cluster-frame.injectable.ts | 2 +- .../init-root-frame.injectable.ts | 33 +- src/renderer/getDiForUnitTesting.tsx | 8 +- .../add-sync-entries.injectable.tsx | 9 +- src/renderer/initializers/catalog.tsx | 2 +- ...gister-ipc-channel-listeners.injectable.ts | 8 +- src/renderer/ipc/index.ts | 2 +- ...amespaces-forbidden-handler.injectable.tsx | 2 +- .../logger}/logger.injectable.ts | 6 +- ...protocol-add-route-handlers.injectable.tsx | 157 ++++++++ ...-protocol-add-route-handlers.injectable.ts | 36 -- .../bind-protocol-add-route-handlers.tsx | 129 ------ src/renderer/protocol-handler/index.ts | 3 +- ...ens-protocol-router-renderer.injectable.ts | 21 - ...avigate-to-preference-tab-id.injectable.ts | 4 +- .../protocol-handler/router.injectable.ts | 21 + ...rotocol-router-renderer.tsx => router.tsx} | 17 +- .../create-storage.injectable.ts | 30 +- .../utils/create-storage/create-storage.ts | 19 +- .../utils/create-storage/logger.injectable.ts | 15 + .../window/event-listener.injectable.ts | 2 + src/test-utils/override-ipc-bridge.ts | 4 +- types/selfsigned.d.ts | 6 +- 177 files changed, 2375 insertions(+), 1745 deletions(-) rename src/common/{cluster-store => cluster}/allowed-resources.injectable.ts (91%) create mode 100644 src/common/cluster/get-by-id.injectable.ts rename src/common/{cluster-store/hosted-cluster.injectable.ts => cluster/hosted.injectable.ts} (72%) rename src/common/{cluster-store/cluster-store.injectable.ts => cluster/store.injectable.ts} (63%) rename src/common/{cluster-store/cluster-store.ts => cluster/store.ts} (96%) rename src/common/{cluster-types.ts => cluster/types.ts} (100%) delete mode 100644 src/common/logger-transports/index.ts delete mode 100644 src/common/logger.ts create mode 100644 src/common/logger/child-logger.injectable.ts create mode 100644 src/common/logger/child-loggers-state.injectable.ts create mode 100644 src/common/logger/claim-child-logger.injectable.ts create mode 100644 src/common/logger/index.ts create mode 100644 src/common/logger/logger.token.ts rename src/common/{logger-transports => logger/transports}/sentry.ts (100%) create mode 100644 src/common/logger/type.ts create mode 100644 src/common/protocol-handler/logger.injectable.ts create mode 100644 src/common/utils/wait-observable-value.ts delete mode 100644 src/main/catalog-sources/kubeconfig-sync-manager/kubeconfig-sync-manager.ts create mode 100644 src/main/catalog-sources/kubeconfig-sync/compute-diff.injectable.ts create mode 100644 src/main/catalog-sources/kubeconfig-sync/config-to-models.injectable.ts create mode 100644 src/main/catalog-sources/kubeconfig-sync/diff-changed-config.injectable.ts create mode 100644 src/main/catalog-sources/kubeconfig-sync/logger.injectable.ts rename src/main/catalog-sources/{kubeconfig-sync-manager/kubeconfig-sync-manager.injectable.ts => kubeconfig-sync/manager.injectable.ts} (66%) create mode 100644 src/main/catalog-sources/kubeconfig-sync/manager.ts create mode 100644 src/main/catalog-sources/kubeconfig-sync/watch-file-changes.injectable.ts rename src/main/{cluster-detectors => cluster/detectors}/base-cluster-detector.ts (85%) rename src/main/{cluster-detectors => cluster/detectors}/cluster-id-detector.ts (91%) rename src/main/{cluster-detectors => cluster/detectors}/detector-registry.ts (87%) rename src/main/{cluster-detectors => cluster/detectors}/distribution-detector.ts (98%) rename src/main/{cluster-detectors => cluster/detectors}/last-seen-detector.ts (87%) rename src/main/{cluster-detectors => cluster/detectors}/nodes-count-detector.ts (90%) rename src/main/{cluster-detectors => cluster/detectors}/version-detector.ts (89%) create mode 100644 src/main/cluster/get-cluster-for-request.injectable.ts create mode 100644 src/main/cluster/manager-logger.injectable.ts create mode 100644 src/main/cluster/manager.injectable.ts rename src/main/{cluster-manager.ts => cluster/manager.ts} (75%) delete mode 100644 src/main/exit-app.ts create mode 100644 src/main/kube-auth-proxy/cert-cache.injectable.ts create mode 100644 src/main/kube-auth-proxy/generate-cert.injectable.ts delete mode 100644 src/main/kube-auth-proxy/get-kube-auth-proxy-certificate.ts create mode 100644 src/main/kube-auth-proxy/get-proxy-cert.injectable.ts rename src/main/{proxy-functions => lens-proxy}/kube-api-upgrade-request.ts (84%) rename src/main/{ => lens-proxy}/lens-proxy.ts (66%) create mode 100644 src/main/lens-proxy/logger.injectable.ts create mode 100644 src/main/lens-proxy/port.injectable.ts create mode 100644 src/main/lens-proxy/proxy.injectable.ts create mode 100644 src/main/lens-proxy/shell-api/handler.injectable.ts create mode 100644 src/main/lens-proxy/shell-api/logger.injectable.ts create mode 100644 src/main/lens-proxy/shell-api/request/authenticate.injectable.ts rename src/main/{proxy-functions/shell-api-request/shell-request-authenticator/shell-request-authenticator.injectable.ts => lens-proxy/shell-api/request/authenticator.injectable.ts} (86%) create mode 100644 src/main/lens-proxy/shell-api/request/authenticator.ts create mode 100644 src/main/logger/logger.injectable.ts create mode 100644 src/main/menu/logger.injectable.ts delete mode 100644 src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable.ts create mode 100644 src/main/protocol-handler/router.injectable.ts rename src/main/protocol-handler/{lens-protocol-router-main/lens-protocol-router-main.ts => router.ts} (75%) delete mode 100644 src/main/proxy-functions/index.ts delete mode 100644 src/main/proxy-functions/shell-api-request/shell-api-request.injectable.ts delete mode 100644 src/main/proxy-functions/shell-api-request/shell-api-request.ts rename src/main/proxy-functions/{shell-api-request => shell-api}/shell-request-authenticator/shell-request-authenticator.ts (86%) delete mode 100644 src/main/proxy-functions/types.ts create mode 100644 src/main/tray/icon.ts create mode 100644 src/main/tray/init-tray.injectable.ts create mode 100644 src/main/tray/logger.injectable.ts create mode 100644 src/main/tray/tray-menu.injectable.ts delete mode 100644 src/main/tray/tray.ts create mode 100644 src/main/utils/exit-app.injectable.ts delete mode 100644 src/main/window-manager.injectable.ts create mode 100644 src/main/window/ensure-main.injectable.ts create mode 100644 src/main/window/logger.injectable.ts create mode 100644 src/main/window/manager.injectable.ts rename src/main/{window-manager.ts => window/manager.ts} (79%) create mode 100644 src/renderer/components/+preferences/kubeconfig-syncs/get-all-entries.injectable.ts create mode 100644 src/renderer/components/+preferences/kubeconfig-syncs/get-entry.injectable.ts create mode 100644 src/renderer/components/+preferences/kubeconfig-syncs/logger.injectable.ts rename src/renderer/components/+preferences/{kubeconfig-syncs.tsx => kubeconfig-syncs/view.tsx} (60%) rename src/{common => renderer/logger}/logger.injectable.ts (66%) create mode 100644 src/renderer/protocol-handler/bind-protocol-add-route-handlers.injectable.tsx delete mode 100644 src/renderer/protocol-handler/bind-protocol-add-route-handlers/bind-protocol-add-route-handlers.injectable.ts delete mode 100644 src/renderer/protocol-handler/bind-protocol-add-route-handlers/bind-protocol-add-route-handlers.tsx delete mode 100644 src/renderer/protocol-handler/lens-protocol-router-renderer/lens-protocol-router-renderer.injectable.ts rename src/renderer/protocol-handler/{bind-protocol-add-route-handlers => }/navigate-to-preference-tab-id.injectable.ts (74%) create mode 100644 src/renderer/protocol-handler/router.injectable.ts rename src/renderer/protocol-handler/{lens-protocol-router-renderer/lens-protocol-router-renderer.tsx => router.tsx} (84%) create mode 100644 src/renderer/utils/create-storage/logger.injectable.ts diff --git a/src/common/__tests__/cluster-store.test.ts b/src/common/__tests__/cluster-store.test.ts index ccc2d897b9..9704ada1b6 100644 --- a/src/common/__tests__/cluster-store.test.ts +++ b/src/common/__tests__/cluster-store.test.ts @@ -8,12 +8,12 @@ import mockFs from "mock-fs"; import path from "path"; import fse from "fs-extra"; import type { Cluster } from "../cluster/cluster"; -import { ClusterStore } from "../cluster-store/cluster-store"; +import { ClusterStore } from "../cluster/store"; import { Console } from "console"; import { stdout, stderr } from "process"; import getCustomKubeConfigDirectoryInjectable from "../app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable"; -import clusterStoreInjectable from "../cluster-store/cluster-store.injectable"; -import type { ClusterModel } from "../cluster-types"; +import clusterStoreInjectable from "../cluster/store.injectable"; +import type { ClusterModel } from "../cluster/types"; import type { DiContainer, } from "@ogre-tools/injectable"; diff --git a/src/common/__tests__/user-store.test.ts b/src/common/__tests__/user-store.test.ts index 9fb3713be1..2365b9d6ab 100644 --- a/src/common/__tests__/user-store.test.ts +++ b/src/common/__tests__/user-store.test.ts @@ -29,7 +29,7 @@ import { stdout, stderr } from "process"; import userStoreInjectable from "../user-store/user-store.injectable"; import type { DiContainer } from "@ogre-tools/injectable"; import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable"; -import type { ClusterStoreModel } from "../cluster-store/cluster-store"; +import type { ClusterStoreModel } from "../cluster/store"; import { defaultTheme } from "../vars"; import writeFileInjectable from "../fs/write-file.injectable"; import { getDiForUnitTesting } from "../../main/getDiForUnitTesting"; diff --git a/src/common/app-event-bus/event-bus.ts b/src/common/app-event-bus/event-bus.ts index 67d23587c7..edf94fb531 100644 --- a/src/common/app-event-bus/event-bus.ts +++ b/src/common/app-event-bus/event-bus.ts @@ -12,4 +12,9 @@ export interface AppEvent { params?: Record; } +export type AppEventBus = EventEmitter<[AppEvent]>; + +/** + * @deprecated use `di.inject(appEventBusInjectable)` instead + */ export const appEventBus = new EventEmitter<[AppEvent]>(); diff --git a/src/common/base-store.ts b/src/common/base-store.ts index 4671e377e8..380ff69bf8 100644 --- a/src/common/base-store.ts +++ b/src/common/base-store.ts @@ -121,14 +121,14 @@ export abstract class BaseStore extends Singleton { if (ipcMain) { this.syncDisposers.push(ipcMainOn(this.syncMainChannel, (event, model: T) => { - logger.silly(`[STORE]: SYNC ${this.name} from renderer`, { model }); + logger.debug(`[STORE]: SYNC ${this.name} from renderer`, { model }); this.onSync(model); })); } if (ipcRenderer) { this.syncDisposers.push(ipcRendererOn(this.syncRendererChannel, (event, model: T) => { - logger.silly(`[STORE]: SYNC ${this.name} from main`, { model }); + logger.debug(`[STORE]: SYNC ${this.name} from main`, { model }); this.onSyncFromMain(model); })); } diff --git a/src/common/catalog-entities/kubernetes-cluster.ts b/src/common/catalog-entities/kubernetes-cluster.ts index 83a596fcc8..88b25a26c3 100644 --- a/src/common/catalog-entities/kubernetes-cluster.ts +++ b/src/common/catalog-entities/kubernetes-cluster.ts @@ -6,7 +6,7 @@ import { catalogCategoryRegistry } from "../catalog/catalog-category-registry"; import type { CatalogEntityActionContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus, CatalogCategorySpec } from "../catalog"; import { CatalogEntity, CatalogCategory } from "../catalog"; -import { ClusterStore } from "../cluster-store/cluster-store"; +import { ClusterStore } from "../cluster/store"; import { broadcastMessage } from "../ipc"; import { app } from "electron"; import type { CatalogEntitySpec } from "../catalog/catalog-entity"; diff --git a/src/common/cluster-store/allowed-resources.injectable.ts b/src/common/cluster/allowed-resources.injectable.ts similarity index 91% rename from src/common/cluster-store/allowed-resources.injectable.ts rename to src/common/cluster/allowed-resources.injectable.ts index 2277befbd6..42d4ee2bc4 100644 --- a/src/common/cluster-store/allowed-resources.injectable.ts +++ b/src/common/cluster/allowed-resources.injectable.ts @@ -4,7 +4,7 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import { comparer, computed } from "mobx"; -import hostedClusterInjectable from "./hosted-cluster.injectable"; +import hostedClusterInjectable from "./hosted.injectable"; const allowedResourcesInjectable = getInjectable({ id: "allowed-resources", diff --git a/src/common/cluster/cluster.ts b/src/common/cluster/cluster.ts index fbe7880f3c..08a77fb6aa 100644 --- a/src/common/cluster/cluster.ts +++ b/src/common/cluster/cluster.ts @@ -3,7 +3,6 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { ipcMain } from "electron"; import { action, comparer, computed, makeObservable, observable, reaction, when } from "mobx"; import { broadcastMessage } from "../ipc"; import type { ContextHandler } from "../../main/context-handler/context-handler"; @@ -15,21 +14,23 @@ import { loadConfigFromFile, loadConfigFromFileSync, validateKubeConfig } from " import type { KubeApiResource, KubeResource } from "../rbac"; import { apiResourceRecord, apiResources } from "../rbac"; import logger from "../../main/logger"; -import { VersionDetector } from "../../main/cluster-detectors/version-detector"; -import { DetectorRegistry } from "../../main/cluster-detectors/detector-registry"; +import { VersionDetector } from "../../main/cluster/detectors/version-detector"; +import { DetectorRegistry } from "../../main/cluster/detectors/detector-registry"; import plimit from "p-limit"; -import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, KubeAuthUpdate } from "../cluster-types"; -import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus } from "../cluster-types"; +import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, KubeAuthUpdate } from "./types"; +import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus } from "./types"; import { disposer, toJS } from "../utils"; import type { Response } from "request"; import { clusterListNamespaceForbiddenChannel } from "../ipc/cluster"; import type { CanI } from "./authorization-review.injectable"; import type { ListNamespaces } from "./list-namespaces.injectable"; +import type { CreateKubeConfigManager } from "../../main/kubeconfig-manager/create-kubeconfig-manager.injectable"; +import type { CreateContextHandler } from "../../main/context-handler/create-context-handler.injectable"; export interface ClusterDependencies { readonly directoryForKubeConfigs: string; - createKubeconfigManager: (cluster: Cluster) => KubeconfigManager; - createContextHandler: (cluster: Cluster) => ContextHandler; + createKubeconfigManager: CreateKubeConfigManager; + createContextHandler: CreateContextHandler; createKubectl: (clusterVersion: string) => Kubectl; createAuthorizationReview: (config: KubeConfig) => CanI; createListNamespaces: (config: KubeConfig) => ListNamespaces; @@ -49,9 +50,9 @@ export class Cluster implements ClusterModel, ClusterState { * * @internal */ - public contextHandler: ContextHandler; - protected proxyKubeconfigManager: KubeconfigManager; - protected eventsDisposer = disposer(); + public readonly contextHandler: ContextHandler | undefined; + protected readonly proxyKubeconfigManager: KubeconfigManager | undefined; + protected readonly eventsDisposer = disposer(); protected activated = false; private resourceAccessStatuses: Map = new Map(); @@ -233,17 +234,15 @@ export class Cluster implements ClusterModel, ClusterState { this.apiUrl = config.getCluster(config.getContextObject(this.contextName).cluster).server; - if (ipcMain) { - // for the time being, until renderer gets its own cluster type - this.contextHandler = this.dependencies.createContextHandler(this); - this.proxyKubeconfigManager = this.dependencies.createKubeconfigManager(this); + // for the time being, until renderer gets its own cluster type + this.contextHandler = this.dependencies.createContextHandler(this); + this.proxyKubeconfigManager = this.dependencies.createKubeconfigManager(this); - logger.debug(`[CLUSTER]: Cluster init success`, { - id: this.id, - context: this.contextName, - apiUrl: this.apiUrl, - }); - } + logger.debug(`[CLUSTER]: Cluster init success`, { + id: this.id, + context: this.contextName, + apiUrl: this.apiUrl, + }); } /** @@ -571,7 +570,6 @@ export class Cluster implements ClusterModel, ClusterState { * @param state cluster state */ pushState(state = this.getState()) { - logger.silly(`[CLUSTER]: push-state`, state); broadcastMessage("cluster:state", this.id, state); } diff --git a/src/common/cluster/create-cluster-injection-token.ts b/src/common/cluster/create-cluster-injection-token.ts index 0508e7b325..54d6e1d19d 100644 --- a/src/common/cluster/create-cluster-injection-token.ts +++ b/src/common/cluster/create-cluster-injection-token.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectionToken } from "@ogre-tools/injectable"; -import type { ClusterModel } from "../cluster-types"; +import type { ClusterModel } from "./types"; import type { Cluster } from "./cluster"; export const createClusterInjectionToken = diff --git a/src/common/cluster/get-by-id.injectable.ts b/src/common/cluster/get-by-id.injectable.ts new file mode 100644 index 0000000000..2809b1a861 --- /dev/null +++ b/src/common/cluster/get-by-id.injectable.ts @@ -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; diff --git a/src/common/cluster-store/hosted-cluster.injectable.ts b/src/common/cluster/hosted.injectable.ts similarity index 72% rename from src/common/cluster-store/hosted-cluster.injectable.ts rename to src/common/cluster/hosted.injectable.ts index 1bcafc9238..4f25eba418 100644 --- a/src/common/cluster-store/hosted-cluster.injectable.ts +++ b/src/common/cluster/hosted.injectable.ts @@ -4,15 +4,16 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import { getHostedClusterId } from "../utils"; -import clusterStoreInjectable from "./cluster-store.injectable"; +import getClusterByIdInjectable from "./get-by-id.injectable"; const hostedClusterInjectable = getInjectable({ id: "hosted-cluster", instantiate: (di) => { const hostedClusterId = getHostedClusterId(); + const getClusterById = di.inject(getClusterByIdInjectable); - return di.inject(clusterStoreInjectable).getById(hostedClusterId); + return getClusterById(hostedClusterId); }, }); diff --git a/src/common/cluster-store/cluster-store.injectable.ts b/src/common/cluster/store.injectable.ts similarity index 63% rename from src/common/cluster-store/cluster-store.injectable.ts rename to src/common/cluster/store.injectable.ts index a47978376a..7e65f9d08c 100644 --- a/src/common/cluster-store/cluster-store.injectable.ts +++ b/src/common/cluster/store.injectable.ts @@ -3,16 +3,19 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import { ClusterStore } from "./cluster-store"; -import { createClusterInjectionToken } from "../cluster/create-cluster-injection-token"; +import { createClusterInjectionToken } from "./create-cluster-injection-token"; +import { ClusterStore } from "./store"; const clusterStoreInjectable = getInjectable({ id: "cluster-store", - instantiate: (di) => - ClusterStore.createInstance({ + instantiate: (di) => { + ClusterStore.resetInstance(); + + return ClusterStore.createInstance({ createCluster: di.inject(createClusterInjectionToken), - }), + }); + }, causesSideEffects: true, }); diff --git a/src/common/cluster-store/cluster-store.ts b/src/common/cluster/store.ts similarity index 96% rename from src/common/cluster-store/cluster-store.ts rename to src/common/cluster/store.ts index 901f340bda..2ba8ef6c1e 100644 --- a/src/common/cluster-store/cluster-store.ts +++ b/src/common/cluster/store.ts @@ -7,13 +7,13 @@ import { ipcMain, ipcRenderer, webFrame } from "electron"; import { action, comparer, computed, makeObservable, observable, reaction } from "mobx"; import { BaseStore } from "../base-store"; -import { Cluster } from "../cluster/cluster"; +import { Cluster } from "./cluster"; import migrations from "../../migrations/cluster-store"; import logger from "../../main/logger"; import { appEventBus } from "../app-event-bus/event-bus"; import { ipcMainHandle } from "../ipc"; import { disposer, toJS } from "../utils"; -import type { ClusterModel, ClusterId, ClusterState } from "../cluster-types"; +import type { ClusterModel, ClusterId, ClusterState } from "./types"; import { requestInitialClusterStates } from "../../renderer/ipc"; import { clusterStates } from "../ipc/cluster"; diff --git a/src/common/cluster-types.ts b/src/common/cluster/types.ts similarity index 100% rename from src/common/cluster-types.ts rename to src/common/cluster/types.ts diff --git a/src/common/front-end-routing/verify-that-all-routes-have-route-component.test.ts b/src/common/front-end-routing/verify-that-all-routes-have-route-component.test.ts index 7cf871f9b9..3e72b477ef 100644 --- a/src/common/front-end-routing/verify-that-all-routes-have-route-component.test.ts +++ b/src/common/front-end-routing/verify-that-all-routes-have-route-component.test.ts @@ -6,8 +6,8 @@ import { getDiForUnitTesting } from "../../renderer/getDiForUnitTesting"; import { routeSpecificComponentInjectionToken } from "../../renderer/routes/route-specific-component-injection-token"; import { routeInjectionToken } from "./route-injection-token"; import { filter, map, matches } from "lodash/fp"; -import clusterStoreInjectable from "../cluster-store/cluster-store.injectable"; -import type { ClusterStore } from "../cluster-store/cluster-store"; +import clusterStoreInjectable from "../cluster/store.injectable"; +import type { ClusterStore } from "../cluster/store"; import { pipeline } from "@ogre-tools/fp"; describe("verify-that-all-routes-have-component", () => { diff --git a/src/common/ipc/ipc.ts b/src/common/ipc/ipc.ts index bddeb0ff3c..9d136cce7b 100644 --- a/src/common/ipc/ipc.ts +++ b/src/common/ipc/ipc.ts @@ -58,7 +58,7 @@ export async function broadcastMessage(channel: string, ...args: any[]): Promise // Send message to views. try { - logger.silly(`[IPC]: broadcasting "${channel}" to ${viewType}=${view.id}`, { args }); + logger.debug(`[IPC]: broadcasting "${channel}" to ${viewType}=${view.id}`, { args }); view.send(channel, ...args); } catch (error) { logger.error(`[IPC]: failed to send IPC message "${channel}" to view "${viewType}=${view.id}"`, { error }); @@ -66,7 +66,7 @@ export async function broadcastMessage(channel: string, ...args: any[]): Promise // Send message to subFrames of views. for (const frameInfo of subFrames) { - logger.silly(`[IPC]: broadcasting "${channel}" to subframe "frameInfo.processId"=${frameInfo.processId} "frameInfo.frameId"=${frameInfo.frameId}`, { args }); + logger.debug(`[IPC]: broadcasting "${channel}" to subframe "frameInfo.processId"=${frameInfo.processId} "frameInfo.frameId"=${frameInfo.frameId}`, { args }); try { view.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...args); diff --git a/src/common/k8s/resource-stack.ts b/src/common/k8s/resource-stack.ts index 29ade4b84a..a0a4319439 100644 --- a/src/common/k8s/resource-stack.ts +++ b/src/common/k8s/resource-stack.ts @@ -9,7 +9,7 @@ import { ResourceApplier } from "../../main/resource-applier"; import type { KubernetesCluster } from "../catalog-entities"; import logger from "../../main/logger"; import { app } from "electron"; -import { ClusterStore } from "../cluster-store/cluster-store"; +import { ClusterStore } from "../cluster/store"; import yaml from "js-yaml"; import { productName } from "../vars"; import { requestKubectlApplyAll, requestKubectlDeleteAll } from "../../renderer/ipc"; diff --git a/src/common/logger-transports/index.ts b/src/common/logger-transports/index.ts deleted file mode 100644 index 20205c616d..0000000000 --- a/src/common/logger-transports/index.ts +++ /dev/null @@ -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"; diff --git a/src/common/logger.ts b/src/common/logger.ts deleted file mode 100644 index 172f3e2fe8..0000000000 --- a/src/common/logger.ts +++ /dev/null @@ -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, -}); diff --git a/src/common/logger/child-logger.injectable.ts b/src/common/logger/child-logger.injectable.ts new file mode 100644 index 0000000000..8edf86ffcc --- /dev/null +++ b/src/common/logger/child-logger.injectable.ts @@ -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; +} + +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; diff --git a/src/common/logger/child-loggers-state.injectable.ts b/src/common/logger/child-loggers-state.injectable.ts new file mode 100644 index 0000000000..85668df15a --- /dev/null +++ b/src/common/logger/child-loggers-state.injectable.ts @@ -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(), // Whether debug logging is enabled for child loggers +}); + +export default childLoggersDebugStateInjectable; diff --git a/src/common/logger/claim-child-logger.injectable.ts b/src/common/logger/claim-child-logger.injectable.ts new file mode 100644 index 0000000000..32bb418745 --- /dev/null +++ b/src/common/logger/claim-child-logger.injectable.ts @@ -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; + +interface Dependencies { + state: ObservableMap; +} + +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; diff --git a/src/common/logger/index.ts b/src/common/logger/index.ts new file mode 100644 index 0000000000..8c74de51e7 --- /dev/null +++ b/src/common/logger/index.ts @@ -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; diff --git a/src/common/logger/logger.token.ts b/src/common/logger/logger.token.ts new file mode 100644 index 0000000000..9b7fe822c0 --- /dev/null +++ b/src/common/logger/logger.token.ts @@ -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({ + id: "base-logger-token", +}); diff --git a/src/common/logger-transports/sentry.ts b/src/common/logger/transports/sentry.ts similarity index 100% rename from src/common/logger-transports/sentry.ts rename to src/common/logger/transports/sentry.ts diff --git a/src/common/logger/type.ts b/src/common/logger/type.ts new file mode 100644 index 0000000000..46219b457e --- /dev/null +++ b/src/common/logger/type.ts @@ -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; +} diff --git a/src/common/protocol-handler/logger.injectable.ts b/src/common/protocol-handler/logger.injectable.ts new file mode 100644 index 0000000000..39adb39145 --- /dev/null +++ b/src/common/protocol-handler/logger.injectable.ts @@ -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; diff --git a/src/common/protocol-handler/router.ts b/src/common/protocol-handler/router.ts index 1740071cfe..17e8c4e9b0 100644 --- a/src/common/protocol-handler/router.ts +++ b/src/common/protocol-handler/router.ts @@ -8,7 +8,6 @@ import { matchPath } from "react-router"; import { countBy } from "lodash"; import { iter } from "../utils"; import { pathToRegexp } from "path-to-regexp"; -import logger from "../../main/logger"; import type Url from "url-parse"; import { RoutingError, RoutingErrorType } from "./error"; import type { ExtensionsStore } from "../../extensions/extensions-store/extensions-store"; @@ -17,6 +16,7 @@ import type { LensExtension } from "../../extensions/lens-extension"; import type { RouteHandler, RouteParams } from "../../extensions/registries/protocol-handler"; import { when } from "mobx"; import { ipcRenderer } from "electron"; +import type { Logger } from "../logger"; // IPC channel for protocol actions. Main broadcasts the open-url events to this channel. export const ProtocolHandlerIpcPrefix = "protocol-handler"; @@ -63,20 +63,19 @@ export function foldAttemptResults(mainAttempt: RouteAttempt, rendererAttempt: R } } -interface Dependencies { - extensionLoader: ExtensionLoader; - extensionsStore: ExtensionsStore; +export interface LensProtocolRouterDependencies { + readonly extensionLoader: ExtensionLoader; + readonly extensionsStore: ExtensionsStore; + readonly logger: Logger; } export abstract class LensProtocolRouter { // Map between path schemas and the handlers - protected internalRoutes = new Map(); - - public static readonly LoggingPrefix = "[PROTOCOL ROUTER]"; + protected readonly internalRoutes = new Map(); static readonly ExtensionUrlSchema = `/:${EXTENSION_PUBLISHER_MATCH}(@[A-Za-z0-9_]+)?/:${EXTENSION_NAME_MATCH}`; - constructor(protected dependencies: Dependencies) {} + constructor(protected readonly dependencies: LensProtocolRouterDependencies) {} /** * Attempts to route the given URL to all internal routes that have been registered @@ -130,7 +129,7 @@ export abstract class LensProtocolRouter { data.extensionName = extensionName; } - logger.info(`${LensProtocolRouter.LoggingPrefix}: No handler found`, data); + this.dependencies.logger.info(`No handler found`, data); return RouteAttempt.MISSING; } @@ -183,9 +182,7 @@ export abstract class LensProtocolRouter { timeout: 5_000, }); } catch (error) { - logger.info( - `${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not installed (${error})`, - ); + this.dependencies.logger.info(`Extension ${name} matched, but not installed (${error})`); return name; } @@ -193,18 +190,18 @@ export abstract class LensProtocolRouter { const extension = extensionLoader.getInstanceByName(name); if (!extension) { - logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but does not have a class for ${ipcRenderer ? "renderer" : "main"}`); + this.dependencies.logger.info(`Extension ${name} matched, but does not have a class for ${ipcRenderer ? "renderer" : "main"}`); return name; } if (!this.dependencies.extensionsStore.isEnabled(extension)) { - logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not enabled`); + this.dependencies.logger.info(`Extension ${name} matched, but not enabled`); return name; } - logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched`); + this.dependencies.logger.info(`Extension ${name} matched`); return extension; } @@ -250,7 +247,7 @@ export abstract class LensProtocolRouter { */ public addInternalHandler(urlSchema: string, handler: RouteHandler): this { pathToRegexp(urlSchema); // verify now that the schema is valid - logger.info(`${LensProtocolRouter.LoggingPrefix}: internal registering ${urlSchema}`); + this.dependencies.logger.info(`internal registering ${urlSchema}`); this.internalRoutes.set(urlSchema, handler); return this; diff --git a/src/common/utils/cluster-id-url-parsing.ts b/src/common/utils/cluster-id-url-parsing.ts index ded15d0472..8c5c9fbe69 100644 --- a/src/common/utils/cluster-id-url-parsing.ts +++ b/src/common/utils/cluster-id-url-parsing.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { ClusterId } from "../cluster-types"; +import type { ClusterId } from "../cluster/types"; /** * Grab the `ClusterId` out of a host that was generated by `getClusterFrameUrl`, or nothing diff --git a/src/common/utils/is-allowed-resource.injectable.ts b/src/common/utils/is-allowed-resource.injectable.ts index f66029c018..40ad31305a 100644 --- a/src/common/utils/is-allowed-resource.injectable.ts +++ b/src/common/utils/is-allowed-resource.injectable.ts @@ -4,7 +4,7 @@ */ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { computed } from "mobx"; -import allowedResourcesInjectable from "../cluster-store/allowed-resources.injectable"; +import allowedResourcesInjectable from "../cluster/allowed-resources.injectable"; import type { KubeResource } from "../rbac"; export type IsAllowedResource = (resource: KubeResource) => boolean; @@ -12,7 +12,7 @@ export type IsAllowedResource = (resource: KubeResource) => boolean; const isAllowedResourceInjectable = getInjectable({ id: "is-allowed-resource", - instantiate: (di, resourceName: KubeResource) => { + instantiate: (di, resourceName) => { const allowedResources = di.inject(allowedResourcesInjectable); return computed(() => allowedResources.get().has(resourceName)); diff --git a/src/common/utils/wait-observable-value.ts b/src/common/utils/wait-observable-value.ts new file mode 100644 index 0000000000..7a602106f6 --- /dev/null +++ b/src/common/utils/wait-observable-value.ts @@ -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(box: IObservableValue): Promise { + return new Promise(resolve => { + when(() => { + const curVal = box.get(); + + if (curVal != null) { + resolve(curVal); + + return true; + } + + return false; + }, () => {}); + }); +} diff --git a/src/extensions/as-legacy-globals-for-extension-api/as-legacy-global-singleton-object-for-extension-api.ts b/src/extensions/as-legacy-globals-for-extension-api/as-legacy-global-singleton-object-for-extension-api.ts index b9ed0f826e..63b754cf57 100644 --- a/src/extensions/as-legacy-globals-for-extension-api/as-legacy-global-singleton-object-for-extension-api.ts +++ b/src/extensions/as-legacy-globals-for-extension-api/as-legacy-global-singleton-object-for-extension-api.ts @@ -3,9 +3,9 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import type { Injectable } from "@ogre-tools/injectable"; +import { baseLoggerInjectionToken } from "../../common/logger/logger.token"; import { asLegacyGlobalForExtensionApi } from "./as-legacy-global-object-for-extension-api"; import { getLegacyGlobalDiForExtensionApi } from "./legacy-global-di-for-extension-api"; -import loggerInjectable from "../../common/logger.injectable"; export const asLegacyGlobalSingletonForExtensionApi = < Instance, @@ -26,11 +26,9 @@ export const asLegacyGlobalSingletonForExtensionApi = < resetInstance: () => { const di = getLegacyGlobalDiForExtensionApi(); - const logger = di.inject(loggerInjectable); + const logger = di.inject(baseLoggerInjectionToken); - logger.warn( - `resetInstance() for a legacy global singleton of "${injectable.id}" does nothing.`, - ); + logger.warn(`resetInstance() for a legacy global singleton of "${injectable.id}" does nothing.`); }, }; }; diff --git a/src/extensions/lens-main-extension.ts b/src/extensions/lens-main-extension.ts index bda6024522..746c0ccead 100644 --- a/src/extensions/lens-main-extension.ts +++ b/src/extensions/lens-main-extension.ts @@ -4,13 +4,14 @@ */ import { LensExtension } from "./lens-extension"; -import { WindowManager } from "../main/window-manager"; import { catalogEntityRegistry } from "../main/catalog"; import type { CatalogEntity } from "../common/catalog"; import type { IObservableArray } from "mobx"; import type { MenuRegistration } from "../main/menu/menu-registration"; import type { TrayMenuRegistration } from "../main/tray/tray-menu-registration"; import type { ShellEnvModifier } from "../main/shell-session/shell-env-modifier/shell-env-modifier-registration"; +import { getLegacyGlobalDiForExtensionApi } from "./as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; +import windowManagerInjectable from "../main/window/manager.injectable"; export class LensMainExtension extends LensExtension { appMenus: MenuRegistration[] = []; @@ -18,21 +19,24 @@ export class LensMainExtension extends LensExtension { /** * implement this to modify the shell environment that Lens terminals are opened with. The ShellEnvModifier type has the signature - * + * * (ctx: ShellEnvContext, env: Record) => Record - * + * * @param ctx the shell environment context, specifically the relevant catalog entity for the terminal. This can be used, for example, to get * cluster-specific information that can be made available in the shell environment by the implementation of terminalShellEnvModifier - * + * * @param env the current shell environment that the terminal will be opened with. The implementation should modify this as desired. - * + * * @returns the modified shell environment that the terminal will be opened with. The implementation must return env as passed in, if it * does not modify the shell environment */ terminalShellEnvModifier?: ShellEnvModifier; async navigate(pageId?: string, params?: Record, 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) { diff --git a/src/extensions/main-api/navigation.ts b/src/extensions/main-api/navigation.ts index 178f3d6d16..3f76bbd2c1 100644 --- a/src/extensions/main-api/navigation.ts +++ b/src/extensions/main-api/navigation.ts @@ -3,8 +3,12 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { WindowManager } from "../../main/window-manager"; +import windowManagerInjectable from "../../main/window/manager.injectable"; +import { getLegacyGlobalDiForExtensionApi } from "../as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; export function navigate(url: string) { - return WindowManager.getInstance().navigate(url); + const di = getLegacyGlobalDiForExtensionApi(); + const windowManager = di.inject(windowManagerInjectable); + + return windowManager.navigate(url); } diff --git a/src/main/__test__/cluster.test.ts b/src/main/__test__/cluster.test.ts index d570919f3e..871695088a 100644 --- a/src/main/__test__/cluster.test.ts +++ b/src/main/__test__/cluster.test.ts @@ -38,22 +38,26 @@ import mockFs from "mock-fs"; import type { Cluster } from "../../common/cluster/cluster"; import { Kubectl } from "../kubectl/kubectl"; import { getDiForUnitTesting } from "../getDiForUnitTesting"; -import type { ClusterModel } from "../../common/cluster-types"; +import type { ClusterModel } from "../../common/cluster/types"; import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token"; import authorizationReviewInjectable from "../../common/cluster/authorization-review.injectable"; import listNamespacesInjectable from "../../common/cluster/list-namespaces.injectable"; import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable"; +import type { DiContainer } from "@ogre-tools/injectable"; +import type { ContextHandler } from "../context-handler/context-handler"; +import createKubeconfigManagerInjectable from "../kubeconfig-manager/create-kubeconfig-manager.injectable"; console = new Console(process.stdout, process.stderr); // fix mockFS describe("create clusters", () => { + let di: DiContainer; let cluster: Cluster; let createCluster: (model: ClusterModel) => Cluster; beforeEach(async () => { jest.clearAllMocks(); - const di = getDiForUnitTesting({ doGeneralOverrides: true }); + di = getDiForUnitTesting({ doGeneralOverrides: true }); mockFs({ "minikube-config.yml": JSON.stringify({ @@ -83,9 +87,11 @@ describe("create clusters", () => { di.override(authorizationReviewInjectable, () => () => () => Promise.resolve(true)); di.override(listNamespacesInjectable, () => () => () => Promise.resolve([ "default" ])); - di.override(createContextHandlerInjectable, () => () => { - throw new Error("you should never come here"); - }); + di.override(createContextHandlerInjectable, () => () => ({ + ensureServer: jest.fn(), + stopServer: jest.fn(), + }) as unknown as ContextHandler); + di.override(createKubeconfigManagerInjectable, () => () => undefined); createCluster = di.inject(createClusterInjectionToken); @@ -121,11 +127,6 @@ describe("create clusters", () => { kubeConfigPath: "minikube-config.yml", }); - cluster.contextHandler = { - ensureServer: jest.fn(), - stopServer: jest.fn(), - } as any; - jest.spyOn(cluster, "reconnect"); jest.spyOn(cluster, "refreshConnectionStatus"); diff --git a/src/main/__test__/kube-auth-proxy.test.ts b/src/main/__test__/kube-auth-proxy.test.ts index 36af031ecf..1a5c508b72 100644 --- a/src/main/__test__/kube-auth-proxy.test.ts +++ b/src/main/__test__/kube-auth-proxy.test.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { ClusterModel } from "../../common/cluster-types"; +import type { ClusterModel } from "../../common/cluster/types"; jest.mock("winston", () => ({ format: { diff --git a/src/main/__test__/kubeconfig-manager.test.ts b/src/main/__test__/kubeconfig-manager.test.ts index 91792bfaf8..8bcaec7a44 100644 --- a/src/main/__test__/kubeconfig-manager.test.ts +++ b/src/main/__test__/kubeconfig-manager.test.ts @@ -31,7 +31,7 @@ jest.mock("winston", () => ({ })); import { getDiForUnitTesting } from "../getDiForUnitTesting"; -import { KubeconfigManager } from "../kubeconfig-manager/kubeconfig-manager"; +import type { KubeconfigManager } from "../kubeconfig-manager/kubeconfig-manager"; import mockFs from "mock-fs"; import type { Cluster } from "../../common/cluster/cluster"; import fse from "fs-extra"; @@ -43,13 +43,15 @@ import { createClusterInjectionToken } from "../../common/cluster/create-cluster import directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable"; import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable"; import type { DiContainer } from "@ogre-tools/injectable"; +import type { ContextHandler } from "../context-handler/context-handler"; +import lensProxyPortInjectable from "../lens-proxy/port.injectable"; console = new Console(process.stdout, process.stderr); // fix mockFS describe("kubeconfig manager tests", () => { let clusterFake: Cluster; let createKubeconfigManager: (cluster: Cluster) => KubeconfigManager; - let di: DiContainer; + let di: DiContainer; beforeEach(async () => { di = getDiForUnitTesting({ doGeneralOverrides: true }); @@ -82,10 +84,11 @@ describe("kubeconfig manager tests", () => { await di.runSetups(); - di.override(createContextHandlerInjectable, () => () => { - throw new Error("you should never come here"); - }); - + di.override(createContextHandlerInjectable, () => () => ({ + ensureServer: async () => {}, + stopServer: jest.fn(), + }) as unknown as ContextHandler); + di.inject(lensProxyPortInjectable).set(9191); const createCluster = di.inject(createClusterInjectionToken); createKubeconfigManager = di.inject(createKubeconfigManagerInjectable); @@ -95,12 +98,6 @@ describe("kubeconfig manager tests", () => { contextName: "minikube", kubeConfigPath: "minikube-config.yml", }); - - clusterFake.contextHandler = { - ensureServer: () => Promise.resolve(), - } as any; - - jest.spyOn(KubeconfigManager.prototype, "resolveProxyUrl", "get").mockReturnValue("http://127.0.0.1:9191/foo"); }); afterEach(() => { diff --git a/src/main/__test__/lens-proxy.test.ts b/src/main/__test__/lens-proxy.test.ts index a80d983dec..20af162f17 100644 --- a/src/main/__test__/lens-proxy.test.ts +++ b/src/main/__test__/lens-proxy.test.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { isLongRunningRequest } from "../lens-proxy"; +import { isLongRunningRequest } from "../lens-proxy/lens-proxy"; describe("isLongRunningRequest", () => { it("returns true on watches", () => { diff --git a/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts b/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts index 782c6e5950..d29a447253 100644 --- a/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts +++ b/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts @@ -7,19 +7,15 @@ import { ObservableMap } from "mobx"; import type { CatalogEntity } from "../../../common/catalog"; import { loadFromOptions } from "../../../common/kube-helpers"; import type { Cluster } from "../../../common/cluster/cluster"; -import { computeDiff as computeDiffFor, configToModels } from "../kubeconfig-sync-manager/kubeconfig-sync-manager"; -import mockFs from "mock-fs"; -import fs from "fs"; -import { ClusterManager } from "../../cluster-manager"; -import clusterStoreInjectable from "../../../common/cluster-store/cluster-store.injectable"; import { getDiForUnitTesting } from "../../getDiForUnitTesting"; -import { createClusterInjectionToken } from "../../../common/cluster/create-cluster-injection-token"; -import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; -import { ClusterStore } from "../../../common/cluster-store/cluster-store"; -import getConfigurationFileModelInjectable - from "../../../common/get-configuration-file-model/get-configuration-file-model.injectable"; -import appVersionInjectable - from "../../../common/get-configuration-file-model/app-version/app-version.injectable"; +import type { ComputeDiff } from "../kubeconfig-sync/compute-diff.injectable"; +import computeDiffInjectable from "../kubeconfig-sync/compute-diff.injectable"; +import type { ConfigToModels } from "../kubeconfig-sync/config-to-models.injectable"; +import configToModelsInjectable from "../kubeconfig-sync/config-to-models.injectable"; +import clusterStoreInjectable from "../../../common/cluster/store.injectable"; +import type { ClusterStore } from "../../../common/cluster/store"; +import mockFs from "mock-fs"; +import { writeFileSync } from "fs"; jest.mock("electron", () => ({ app: { @@ -38,7 +34,8 @@ jest.mock("electron", () => ({ })); describe("kubeconfig-sync.source tests", () => { - let computeDiff: ReturnType; + let computeDiff: ComputeDiff; + let configToModels: ConfigToModels; beforeEach(async () => { const di = getDiForUnitTesting({ doGeneralOverrides: true }); @@ -47,27 +44,18 @@ describe("kubeconfig-sync.source tests", () => { await di.runSetups(); - computeDiff = computeDiffFor({ - directoryForKubeConfigs: di.inject(directoryForKubeConfigsInjectable), - createCluster: di.inject(createClusterInjectionToken), - }); + di.override(clusterStoreInjectable, () => ({ + getById: jest.fn(), + addCluster: jest.fn(), + clustersList: [], + } as unknown as ClusterStore)); - di.override(clusterStoreInjectable, () => - ClusterStore.createInstance({ createCluster: () => null }), - ); - - di.permitSideEffects(getConfigurationFileModelInjectable); - di.permitSideEffects(appVersionInjectable); - - di.inject(clusterStoreInjectable); - - ClusterManager.createInstance(); + computeDiff = di.inject(computeDiffInjectable); + configToModels = di.inject(configToModelsInjectable); }); afterEach(() => { mockFs.restore(); - ClusterManager.resetInstance(); - ClusterStore.resetInstance(); }); describe("configsToModels", () => { @@ -86,7 +74,7 @@ describe("kubeconfig-sync.source tests", () => { const config = loadFromOptions({ clusters: [{ name: "cluster-name", - server: "1.2.3.4", + server: "https://1.2.3.4", skipTLSVerify: false, }], users: [{ @@ -124,7 +112,7 @@ describe("kubeconfig-sync.source tests", () => { clusters: [{ name: "cluster-name", cluster: { - server: "1.2.3.4", + server: "https://1.2.3.4", }, skipTLSVerify: false, }], @@ -149,7 +137,7 @@ describe("kubeconfig-sync.source tests", () => { const rootSource = new ObservableMap(); const filePath = "/bar"; - fs.writeFileSync(filePath, contents); + writeFileSync(filePath, contents); computeDiff(contents, rootSource, filePath); @@ -166,7 +154,7 @@ describe("kubeconfig-sync.source tests", () => { clusters: [{ name: "cluster-name", cluster: { - server: "1.2.3.4", + server: "https://1.2.3.4", }, skipTLSVerify: false, }], @@ -192,7 +180,7 @@ describe("kubeconfig-sync.source tests", () => { const rootSource = new ObservableMap(); const filePath = "/bar"; - fs.writeFileSync(filePath, contents); + writeFileSync(filePath, contents); computeDiff(contents, rootSource, filePath); @@ -213,7 +201,7 @@ describe("kubeconfig-sync.source tests", () => { clusters: [{ name: "cluster-name", cluster: { - server: "1.2.3.4", + server: "https://1.2.3.4", }, skipTLSVerify: false, }], @@ -246,7 +234,7 @@ describe("kubeconfig-sync.source tests", () => { const rootSource = new ObservableMap(); const filePath = "/bar"; - fs.writeFileSync(filePath, contents); + writeFileSync(filePath, contents); computeDiff(contents, rootSource, filePath); @@ -263,7 +251,7 @@ describe("kubeconfig-sync.source tests", () => { clusters: [{ name: "cluster-name", cluster: { - server: "1.2.3.4", + server: "https://1.2.3.4", }, skipTLSVerify: false, }], diff --git a/src/main/catalog-sources/kubeconfig-sync-manager/kubeconfig-sync-manager.ts b/src/main/catalog-sources/kubeconfig-sync-manager/kubeconfig-sync-manager.ts deleted file mode 100644 index a9e1038f53..0000000000 --- a/src/main/catalog-sources/kubeconfig-sync-manager/kubeconfig-sync-manager.ts +++ /dev/null @@ -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, 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; - -// 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, Disposer] => { - const rootSource = observable.map>(); - 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(); - 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(); - }]; -}; diff --git a/src/main/catalog-sources/kubeconfig-sync/compute-diff.injectable.ts b/src/main/catalog-sources/kubeconfig-sync/compute-diff.injectable.ts new file mode 100644 index 0000000000..4717a332bd --- /dev/null +++ b/src/main/catalog-sources/kubeconfig-sync/compute-diff.injectable.ts @@ -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, 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; diff --git a/src/main/catalog-sources/kubeconfig-sync/config-to-models.injectable.ts b/src/main/catalog-sources/kubeconfig-sync/config-to-models.injectable.ts new file mode 100644 index 0000000000..bc8f3b5494 --- /dev/null +++ b/src/main/catalog-sources/kubeconfig-sync/config-to-models.injectable.ts @@ -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; diff --git a/src/main/catalog-sources/kubeconfig-sync/diff-changed-config.injectable.ts b/src/main/catalog-sources/kubeconfig-sync/diff-changed-config.injectable.ts new file mode 100644 index 0000000000..7259cc901d --- /dev/null +++ b/src/main/catalog-sources/kubeconfig-sync/diff-changed-config.injectable.ts @@ -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; + 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; diff --git a/src/main/catalog-sources/kubeconfig-sync/logger.injectable.ts b/src/main/catalog-sources/kubeconfig-sync/logger.injectable.ts new file mode 100644 index 0000000000..e5dfc0728b --- /dev/null +++ b/src/main/catalog-sources/kubeconfig-sync/logger.injectable.ts @@ -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; diff --git a/src/main/catalog-sources/kubeconfig-sync-manager/kubeconfig-sync-manager.injectable.ts b/src/main/catalog-sources/kubeconfig-sync/manager.injectable.ts similarity index 66% rename from src/main/catalog-sources/kubeconfig-sync-manager/kubeconfig-sync-manager.injectable.ts rename to src/main/catalog-sources/kubeconfig-sync/manager.injectable.ts index 7c258cabd7..97ba9fba13 100644 --- a/src/main/catalog-sources/kubeconfig-sync-manager/kubeconfig-sync-manager.injectable.ts +++ b/src/main/catalog-sources/kubeconfig-sync/manager.injectable.ts @@ -4,15 +4,17 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; -import { KubeconfigSyncManager } from "./kubeconfig-sync-manager"; -import { createClusterInjectionToken } from "../../../common/cluster/create-cluster-injection-token"; +import kubeconfigSyncLoggerInjectable from "./logger.injectable"; +import { KubeconfigSyncManager } from "./manager"; +import watchFileChangesInjectable from "./watch-file-changes.injectable"; const kubeconfigSyncManagerInjectable = getInjectable({ id: "kubeconfig-sync-manager", instantiate: (di) => new KubeconfigSyncManager({ directoryForKubeConfigs: di.inject(directoryForKubeConfigsInjectable), - createCluster: di.inject(createClusterInjectionToken), + logger: di.inject(kubeconfigSyncLoggerInjectable), + watchFileChanges: di.inject(watchFileChangesInjectable), }), }); diff --git a/src/main/catalog-sources/kubeconfig-sync/manager.ts b/src/main/catalog-sources/kubeconfig-sync/manager.ts new file mode 100644 index 0000000000..18c987f3d4 --- /dev/null +++ b/src/main/catalog-sources/kubeconfig-sync/manager.ts @@ -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(); + 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()) }); + } +} diff --git a/src/main/catalog-sources/kubeconfig-sync/watch-file-changes.injectable.ts b/src/main/catalog-sources/kubeconfig-sync/watch-file-changes.injectable.ts new file mode 100644 index 0000000000..674ad55f75 --- /dev/null +++ b/src/main/catalog-sources/kubeconfig-sync/watch-file-changes.injectable.ts @@ -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; +} + +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>(); + 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(); + 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; diff --git a/src/main/child-process/spawn.injectable.ts b/src/main/child-process/spawn.injectable.ts index d313e19d95..d896af5e1a 100644 --- a/src/main/child-process/spawn.injectable.ts +++ b/src/main/child-process/spawn.injectable.ts @@ -5,12 +5,11 @@ import { getInjectable } from "@ogre-tools/injectable"; import { spawn } from "child_process"; +export type Spawn = typeof spawn; + const spawnInjectable = getInjectable({ id: "spawn", - - instantiate: () => { - return spawn; - }, + instantiate: (): Spawn => spawn, causesSideEffects: true, }); diff --git a/src/main/cluster-detectors/base-cluster-detector.ts b/src/main/cluster/detectors/base-cluster-detector.ts similarity index 85% rename from src/main/cluster-detectors/base-cluster-detector.ts rename to src/main/cluster/detectors/base-cluster-detector.ts index a5a2564ab4..5a5d73e1e3 100644 --- a/src/main/cluster-detectors/base-cluster-detector.ts +++ b/src/main/cluster/detectors/base-cluster-detector.ts @@ -4,8 +4,8 @@ */ import type { RequestPromiseOptions } from "request-promise-native"; -import type { Cluster } from "../../common/cluster/cluster"; -import { k8sRequest } from "../k8s-request"; +import type { Cluster } from "../../../common/cluster/cluster"; +import { k8sRequest } from "../../k8s-request"; export interface ClusterDetectionResult { value: string | number | boolean; diff --git a/src/main/cluster-detectors/cluster-id-detector.ts b/src/main/cluster/detectors/cluster-id-detector.ts similarity index 91% rename from src/main/cluster-detectors/cluster-id-detector.ts rename to src/main/cluster/detectors/cluster-id-detector.ts index 8f1486fcb1..585a7e1307 100644 --- a/src/main/cluster-detectors/cluster-id-detector.ts +++ b/src/main/cluster/detectors/cluster-id-detector.ts @@ -5,7 +5,7 @@ import { BaseClusterDetector } from "./base-cluster-detector"; import { createHash } from "crypto"; -import { ClusterMetadataKey } from "../../common/cluster-types"; +import { ClusterMetadataKey } from "../../../common/cluster/types"; export class ClusterIdDetector extends BaseClusterDetector { key = ClusterMetadataKey.CLUSTER_ID; diff --git a/src/main/cluster-detectors/detector-registry.ts b/src/main/cluster/detectors/detector-registry.ts similarity index 87% rename from src/main/cluster-detectors/detector-registry.ts rename to src/main/cluster/detectors/detector-registry.ts index e2741c1353..77c636f59c 100644 --- a/src/main/cluster-detectors/detector-registry.ts +++ b/src/main/cluster/detectors/detector-registry.ts @@ -4,9 +4,9 @@ */ import { observable } from "mobx"; -import type { ClusterMetadata } from "../../common/cluster-types"; -import { Singleton } from "../../common/utils"; -import type { Cluster } from "../../common/cluster/cluster"; +import type { ClusterMetadata } from "../../../common/cluster/types"; +import { Singleton } from "../../../common/utils"; +import type { Cluster } from "../../../common/cluster/cluster"; import type { BaseClusterDetector, ClusterDetectionResult } from "./base-cluster-detector"; export class DetectorRegistry extends Singleton { diff --git a/src/main/cluster-detectors/distribution-detector.ts b/src/main/cluster/detectors/distribution-detector.ts similarity index 98% rename from src/main/cluster-detectors/distribution-detector.ts rename to src/main/cluster/detectors/distribution-detector.ts index 83610fdaa0..419c0bb59f 100644 --- a/src/main/cluster-detectors/distribution-detector.ts +++ b/src/main/cluster/detectors/distribution-detector.ts @@ -4,7 +4,7 @@ */ import { BaseClusterDetector } from "./base-cluster-detector"; -import { ClusterMetadataKey } from "../../common/cluster-types"; +import { ClusterMetadataKey } from "../../../common/cluster/types"; export class DistributionDetector extends BaseClusterDetector { key = ClusterMetadataKey.DISTRIBUTION; diff --git a/src/main/cluster-detectors/last-seen-detector.ts b/src/main/cluster/detectors/last-seen-detector.ts similarity index 87% rename from src/main/cluster-detectors/last-seen-detector.ts rename to src/main/cluster/detectors/last-seen-detector.ts index 087557f083..5c47c4bf38 100644 --- a/src/main/cluster-detectors/last-seen-detector.ts +++ b/src/main/cluster/detectors/last-seen-detector.ts @@ -4,7 +4,7 @@ */ import { BaseClusterDetector } from "./base-cluster-detector"; -import { ClusterMetadataKey } from "../../common/cluster-types"; +import { ClusterMetadataKey } from "../../../common/cluster/types"; export class LastSeenDetector extends BaseClusterDetector { key = ClusterMetadataKey.LAST_SEEN; diff --git a/src/main/cluster-detectors/nodes-count-detector.ts b/src/main/cluster/detectors/nodes-count-detector.ts similarity index 90% rename from src/main/cluster-detectors/nodes-count-detector.ts rename to src/main/cluster/detectors/nodes-count-detector.ts index f9b3c02dbf..8b395dc5ba 100644 --- a/src/main/cluster-detectors/nodes-count-detector.ts +++ b/src/main/cluster/detectors/nodes-count-detector.ts @@ -4,7 +4,7 @@ */ import { BaseClusterDetector } from "./base-cluster-detector"; -import { ClusterMetadataKey } from "../../common/cluster-types"; +import { ClusterMetadataKey } from "../../../common/cluster/types"; export class NodesCountDetector extends BaseClusterDetector { key = ClusterMetadataKey.NODES_COUNT; diff --git a/src/main/cluster-detectors/version-detector.ts b/src/main/cluster/detectors/version-detector.ts similarity index 89% rename from src/main/cluster-detectors/version-detector.ts rename to src/main/cluster/detectors/version-detector.ts index 50b55b433d..a84c623fdf 100644 --- a/src/main/cluster-detectors/version-detector.ts +++ b/src/main/cluster/detectors/version-detector.ts @@ -4,7 +4,7 @@ */ import { BaseClusterDetector } from "./base-cluster-detector"; -import { ClusterMetadataKey } from "../../common/cluster-types"; +import { ClusterMetadataKey } from "../../../common/cluster/types"; export class VersionDetector extends BaseClusterDetector { key = ClusterMetadataKey.VERSION; diff --git a/src/main/cluster/get-cluster-for-request.injectable.ts b/src/main/cluster/get-cluster-for-request.injectable.ts new file mode 100644 index 0000000000..da0eebbf12 --- /dev/null +++ b/src/main/cluster/get-cluster-for-request.injectable.ts @@ -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:/ + 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; diff --git a/src/main/cluster/manager-logger.injectable.ts b/src/main/cluster/manager-logger.injectable.ts new file mode 100644 index 0000000000..a18ec83e99 --- /dev/null +++ b/src/main/cluster/manager-logger.injectable.ts @@ -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; diff --git a/src/main/cluster/manager.injectable.ts b/src/main/cluster/manager.injectable.ts new file mode 100644 index 0000000000..818c7124a9 --- /dev/null +++ b/src/main/cluster/manager.injectable.ts @@ -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; diff --git a/src/main/cluster-manager.ts b/src/main/cluster/manager.ts similarity index 75% rename from src/main/cluster-manager.ts rename to src/main/cluster/manager.ts index 1991ce8ea1..c96c6ade97 100644 --- a/src/main/cluster-manager.ts +++ b/src/main/cluster/manager.ts @@ -3,48 +3,46 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import "../common/ipc/cluster"; -import type http from "http"; +import "../../common/ipc/cluster"; import { action, makeObservable, observable, observe, reaction, toJS } from "mobx"; -import type { Cluster } from "../common/cluster/cluster"; -import logger from "./logger"; -import { apiKubePrefix } from "../common/vars"; -import { getClusterIdFromHost, Singleton } from "../common/utils"; -import { catalogEntityRegistry } from "./catalog"; -import type { KubernetesClusterPrometheusMetrics } from "../common/catalog-entities/kubernetes-cluster"; -import { KubernetesCluster, LensKubernetesClusterStatus } from "../common/catalog-entities/kubernetes-cluster"; -import { ipcMainOn } from "../common/ipc"; +import type { Cluster } from "../../common/cluster/cluster"; +import { catalogEntityRegistry } from "../catalog"; +import type { KubernetesClusterPrometheusMetrics } from "../../common/catalog-entities/kubernetes-cluster"; +import { KubernetesCluster, LensKubernetesClusterStatus } from "../../common/catalog-entities/kubernetes-cluster"; +import { ipcMainOn } from "../../common/ipc"; import { once } from "lodash"; -import { ClusterStore } from "../common/cluster-store/cluster-store"; -import type { ClusterId } from "../common/cluster-types"; - -const logPrefix = "[CLUSTER-MANAGER]:"; +import type { ClusterId } from "../../common/cluster/types"; +import type { ClusterStore } from "../../common/cluster/store"; +import type { Logger } from "../../common/logger"; const lensSpecificClusterStatuses: Set = new Set(Object.values(LensKubernetesClusterStatus)); -export class ClusterManager extends Singleton { - private store = ClusterStore.getInstance(); - deleting = observable.set(); +interface Dependencies { + readonly clusterStore: ClusterStore; + readonly logger: Logger; +} + +export class ClusterManager { + readonly deleting = observable.set(); @observable visibleCluster: ClusterId | undefined = undefined; - constructor() { - super(); + constructor(protected readonly dependencies: Dependencies) { makeObservable(this); } init = once(() => { // reacting to every cluster's state change and total amount of items reaction( - () => this.store.clustersList.map(c => c.getState()), - () => this.updateCatalog(this.store.clustersList), + () => this.dependencies.clusterStore.clustersList.map(c => c.getState()), + () => this.updateCatalog(this.dependencies.clusterStore.clustersList), { fireImmediately: false }, ); // reacting to every cluster's preferences change and total amount of items reaction( - () => this.store.clustersList.map(c => toJS(c.preferences)), - () => this.updateCatalog(this.store.clustersList), + () => this.dependencies.clusterStore.clustersList.map(c => toJS(c.preferences)), + () => this.updateCatalog(this.dependencies.clusterStore.clustersList), { fireImmediately: false }, ); @@ -78,7 +76,7 @@ export class ClusterManager extends Singleton { @action protected updateCatalog(clusters: Cluster[]) { - logger.debug("[CLUSTER-MANAGER]: updating catalog from cluster store"); + this.dependencies.logger.debug("updating catalog from cluster store"); for (const cluster of clusters) { this.updateEntityFromCluster(cluster); @@ -170,7 +168,7 @@ export class ClusterManager extends Singleton { @action protected syncClustersFromCatalog(entities: KubernetesCluster[]) { for (const entity of entities) { - const cluster = this.store.getById(entity.getId()); + const cluster = this.dependencies.clusterStore.getById(entity.getId()); if (!cluster) { const model = { @@ -185,12 +183,12 @@ export class ClusterManager extends Singleton { * Add the bare minimum of data to ClusterStore. And especially no * preferences, as those might be configured by the entity's source */ - this.store.addCluster(model); + this.dependencies.clusterStore.addCluster(model); } catch (error) { if (error.code === "ENOENT" && error.path === entity.spec.kubeconfigPath) { - logger.warn(`${logPrefix} kubeconfig file disappeared`, model); + this.dependencies.logger.warn(`kubeconfig file disappeared`, model); } else { - logger.error(`${logPrefix} failed to add cluster: ${error}`, model); + this.dependencies.logger.error(`failed to add cluster: ${error}`, model); } } } else { @@ -223,8 +221,8 @@ export class ClusterManager extends Singleton { } protected onNetworkOffline = () => { - logger.info(`${logPrefix} network is offline`); - this.store.clustersList.forEach((cluster) => { + this.dependencies.logger.info(`network is offline`); + this.dependencies.clusterStore.clustersList.forEach((cluster) => { if (!cluster.disconnected) { cluster.online = false; cluster.accessible = false; @@ -234,8 +232,8 @@ export class ClusterManager extends Singleton { }; protected onNetworkOnline = () => { - logger.info(`${logPrefix} network is online`); - this.store.clustersList.forEach((cluster) => { + this.dependencies.logger.info(`network is online`); + this.dependencies.clusterStore.clustersList.forEach((cluster) => { if (!cluster.disconnected) { cluster.refreshConnectionStatus().catch((e) => e); } @@ -243,27 +241,10 @@ export class ClusterManager extends Singleton { }; stop() { - this.store.clusters.forEach((cluster: Cluster) => { + this.dependencies.clusterStore.clusters.forEach((cluster: Cluster) => { cluster.disconnect(); }); } - - getClusterForRequest(req: http.IncomingMessage): Cluster { - // lens-server is connecting to 127.0.0.1:/ - 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) { diff --git a/src/main/context-handler/context-handler.ts b/src/main/context-handler/context-handler.ts index 46e100c07e..b67d5adb21 100644 --- a/src/main/context-handler/context-handler.ts +++ b/src/main/context-handler/context-handler.ts @@ -5,7 +5,7 @@ import type { PrometheusProvider, PrometheusService } from "../prometheus/provider-registry"; import { PrometheusProviderRegistry } from "../prometheus/provider-registry"; -import type { ClusterPrometheusPreferences } from "../../common/cluster-types"; +import type { ClusterPrometheusPreferences } from "../../common/cluster/types"; import type { Cluster } from "../../common/cluster/cluster"; import type httpProxy from "http-proxy"; import type { UrlWithStringQuery } from "url"; @@ -14,6 +14,8 @@ import { CoreV1Api } from "@kubernetes/client-node"; import logger from "../logger"; import type { KubeAuthProxy } from "../kube-auth-proxy/kube-auth-proxy"; import type { CreateKubeAuthProxy } from "../kube-auth-proxy/create-kube-auth-proxy.injectable"; +import type { GetKubeAuthProxyCertificate } from "../kube-auth-proxy/get-proxy-cert.injectable"; +import type { SelfSignedCert } from "selfsigned"; export interface PrometheusDetails { prometheusPath: string; @@ -27,21 +29,23 @@ interface PrometheusServicePreferences { prefix: string; } -interface Dependencies { +export interface ContextHandlerDependencies { createKubeAuthProxy: CreateKubeAuthProxy; - authProxyCa: string; + getKubeAuthProxyCertificate: GetKubeAuthProxyCertificate; } export class ContextHandler { - public clusterUrl: UrlWithStringQuery; + public readonly clusterUrl: UrlWithStringQuery; protected kubeAuthProxy?: KubeAuthProxy; protected apiTarget?: httpProxy.ServerOptions; protected prometheusProvider?: string; protected prometheus?: PrometheusServicePreferences; + private readonly proxyCert: SelfSignedCert; - constructor(private dependencies: Dependencies, protected cluster: Cluster) { + constructor(protected readonly dependencies: ContextHandlerDependencies, protected readonly cluster: Cluster) { this.clusterUrl = url.parse(cluster.apiUrl); this.setupPrometheus(cluster.preferences); + this.proxyCert = this.dependencies.getKubeAuthProxyCertificate(new URL(cluster.apiUrl).hostname); } public setupPrometheus(preferences: ClusterPrometheusPreferences = {}) { @@ -125,7 +129,7 @@ export class ContextHandler { } resolveAuthProxyCa() { - return this.dependencies.authProxyCa; + return this.proxyCert.cert; } async getApiTarget(isLongRunningRequest = false): Promise { @@ -141,7 +145,6 @@ export class ContextHandler { protected async newApiTarget(timeout: number): Promise { await this.ensureServer(); - const ca = this.dependencies.authProxyCa; const clusterPath = this.clusterUrl.path !== "/" ? this.clusterUrl.path : ""; const apiPrefix = `${this.kubeAuthProxy.apiPrefix}${clusterPath}`; @@ -151,7 +154,7 @@ export class ContextHandler { host: "127.0.0.1", port: this.kubeAuthProxy.port, path: apiPrefix, - ca, + ca: this.resolveAuthProxyCa(), }, changeOrigin: true, timeout, diff --git a/src/main/context-handler/create-context-handler.injectable.ts b/src/main/context-handler/create-context-handler.injectable.ts index 86867ccdfb..8905cadd23 100644 --- a/src/main/context-handler/create-context-handler.injectable.ts +++ b/src/main/context-handler/create-context-handler.injectable.ts @@ -3,27 +3,24 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import selfsigned from "selfsigned"; import type { Cluster } from "../../common/cluster/cluster"; +import type { ContextHandlerDependencies } from "./context-handler"; import { ContextHandler } from "./context-handler"; import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable"; -import { getKubeAuthProxyCertificate } from "../kube-auth-proxy/get-kube-auth-proxy-certificate"; -import URLParse from "url-parse"; +import getKubeAuthProxyCertificateInjectable from "../kube-auth-proxy/get-proxy-cert.injectable"; + +export type CreateContextHandler = (cluster: Cluster) => ContextHandler | undefined; const createContextHandlerInjectable = getInjectable({ id: "create-context-handler", - instantiate: (di) => { - return (cluster: Cluster) => { - const clusterUrl = new URLParse(cluster.apiUrl); - - const dependencies = { - createKubeAuthProxy: di.inject(createKubeAuthProxyInjectable), - authProxyCa: getKubeAuthProxyCertificate(clusterUrl.hostname, selfsigned.generate).cert, - }; - - return new ContextHandler(dependencies, cluster); + instantiate: (di): CreateContextHandler => { + const dependencies: ContextHandlerDependencies = { + createKubeAuthProxy: di.inject(createKubeAuthProxyInjectable), + getKubeAuthProxyCertificate: di.inject(getKubeAuthProxyCertificateInjectable), }; + + return (cluster) => new ContextHandler(dependencies, cluster); }, }); diff --git a/src/main/exit-app.ts b/src/main/exit-app.ts deleted file mode 100644 index c9a5bc5bf1..0000000000 --- a/src/main/exit-app.ts +++ /dev/null @@ -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); -} diff --git a/src/main/getDiForUnitTesting.ts b/src/main/getDiForUnitTesting.ts index b1306457c5..58d73878aa 100644 --- a/src/main/getDiForUnitTesting.ts +++ b/src/main/getDiForUnitTesting.ts @@ -16,14 +16,13 @@ import writeJsonFileInjectable from "../common/fs/write-json-file.injectable"; import readJsonFileInjectable from "../common/fs/read-json-file.injectable"; import readFileInjectable from "../common/fs/read-file.injectable"; import directoryForBundledBinariesInjectable from "../common/app-paths/directory-for-bundled-binaries/directory-for-bundled-binaries.injectable"; -import loggerInjectable from "../common/logger.injectable"; import spawnInjectable from "./child-process/spawn.injectable"; import extensionsStoreInjectable from "../extensions/extensions-store/extensions-store.injectable"; import type { ExtensionsStore } from "../extensions/extensions-store/extensions-store"; import fileSystemProvisionerStoreInjectable from "../extensions/extension-loader/create-extension-instance/file-system-provisioner-store/file-system-provisioner-store.injectable"; import type { FileSystemProvisionerStore } from "../extensions/extension-loader/create-extension-instance/file-system-provisioner-store/file-system-provisioner-store"; -import clusterStoreInjectable from "../common/cluster-store/cluster-store.injectable"; -import type { ClusterStore } from "../common/cluster-store/cluster-store"; +import clusterStoreInjectable from "../common/cluster/store.injectable"; +import type { ClusterStore } from "../common/cluster/store"; import type { Cluster } from "../common/cluster/cluster"; import userStoreInjectable from "../common/user-store/user-store.injectable"; import type { UserStore } from "../common/user-store"; @@ -35,6 +34,7 @@ import { getAbsolutePathFake } from "../common/test-utils/get-absolute-path-fake import joinPathsInjectable from "../common/path/join-paths.injectable"; import { joinPathsFake } from "../common/test-utils/join-paths-fake"; import hotbarStoreInjectable from "../common/hotbar-store.injectable"; +import loggerInjectable from "./logger/logger.injectable"; export const getDiForUnitTesting = ( { doGeneralOverrides } = { doGeneralOverrides: false }, @@ -107,7 +107,7 @@ export const getDiForUnitTesting = ( di.override(loggerInjectable, () => ({ warn: noop, debug: noop, - error: (message: string, ...args: any) => console.error(message, ...args), + error: noop, info: noop, })); } diff --git a/src/main/index.ts b/src/main/index.ts index 5b4f965a93..ec85628d5d 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -7,18 +7,13 @@ import { injectSystemCAs } from "../common/system-ca"; import * as Mobx from "mobx"; -import httpProxy from "http-proxy"; import * as LensExtensionsCommonApi from "../extensions/common-api"; import * as LensExtensionsMainApi from "../extensions/main-api"; import { app, autoUpdater, dialog, powerMonitor } from "electron"; import { appName, isIntegrationTesting, isMac, isWindows, productName, staticFilesDirectory } from "../common/vars"; -import { LensProxy } from "./lens-proxy"; -import { WindowManager } from "./window-manager"; -import { ClusterManager } from "./cluster-manager"; import { shellSync } from "./shell-sync"; import { mangleProxyEnv } from "./proxy-env"; import { registerFileProtocol } from "../common/register-protocol"; -import logger from "./logger"; import { appEventBus } from "../common/app-event-bus/event-bus"; import type { InstalledExtension } from "../extensions/extension-discovery/extension-discovery"; import type { LensExtensionId } from "../extensions/lens-extension"; @@ -38,30 +33,29 @@ import { WeblinkStore } from "../common/weblink-store"; import { initializeSentryReporting } from "../common/sentry"; import { ensureDir } from "fs-extra"; import { initMenu } from "./menu/menu"; -import { kubeApiUpgradeRequest } from "./proxy-functions"; -import { initTray } from "./tray/tray"; import { ShellSession } from "./shell-session/shell-session"; import { getDi } from "./getDi"; import extensionLoaderInjectable from "../extensions/extension-loader/extension-loader.injectable"; -import lensProtocolRouterMainInjectable from "./protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable"; +import lensProtocolRouterMainInjectable from "./protocol-handler/router.injectable"; import extensionDiscoveryInjectable from "../extensions/extension-discovery/extension-discovery.injectable"; import directoryForExesInjectable from "../common/app-paths/directory-for-exes/directory-for-exes.injectable"; import initIpcMainHandlersInjectable from "./initializers/init-ipc-main-handlers/init-ipc-main-handlers.injectable"; import directoryForKubeConfigsInjectable from "../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; -import kubeconfigSyncManagerInjectable from "./catalog-sources/kubeconfig-sync-manager/kubeconfig-sync-manager.injectable"; -import clusterStoreInjectable from "../common/cluster-store/cluster-store.injectable"; -import routerInjectable from "./router/router.injectable"; -import shellApiRequestInjectable from "./proxy-functions/shell-api-request/shell-api-request.injectable"; +import kubeconfigSyncManagerInjectable from "./catalog-sources/kubeconfig-sync/manager.injectable"; import userStoreInjectable from "../common/user-store/user-store.injectable"; -import trayMenuItemsInjectable from "./tray/tray-menu-items.injectable"; import { broadcastNativeThemeOnUpdate } from "./native-theme"; -import windowManagerInjectable from "./window-manager.injectable"; -import navigateToPreferencesInjectable from "../common/front-end-routing/routes/preferences/navigate-to-preferences.injectable"; +import windowManagerInjectable from "./window/manager.injectable"; import syncGeneralCatalogEntitiesInjectable from "./catalog-sources/sync-general-catalog-entities.injectable"; import hotbarStoreInjectable from "../common/hotbar-store.injectable"; import applicationMenuItemsInjectable from "./menu/application-menu-items.injectable"; import type { DiContainer } from "@ogre-tools/injectable"; import { init } from "@sentry/electron/main"; +import loggerInjectable from "./logger/logger.injectable"; +import clusterManagerInjectable from "./cluster/manager.injectable"; +import lensProxyInjectable from "./lens-proxy/proxy.injectable"; +import lensProxyPortInjectable from "./lens-proxy/port.injectable"; +import clusterStoreInjectable from "../common/cluster/store.injectable"; +import initTrayInjectable from "./tray/init-tray.injectable"; async function main(di: DiContainer) { app.setName(appName); @@ -77,6 +71,7 @@ async function main(di: DiContainer) { const onCloseCleanup = disposer(); const onQuitCleanup = disposer(); + const logger = di.inject(loggerInjectable); logger.info(`📟 Setting ${productName} as protocol client for lens://`); @@ -120,6 +115,9 @@ async function main(di: DiContainer) { broadcastNativeThemeOnUpdate(); + logger.info("🖥️ Starting WindowManager"); + const windowManager = di.inject(windowManagerInjectable); + app.on("second-instance", (event, argv) => { logger.debug("second-instance message"); @@ -129,14 +127,14 @@ async function main(di: DiContainer) { } } - WindowManager.getInstance(false)?.ensureMainWindow(); + windowManager.ensureMainWindow(); }); app.on("activate", (event, hasVisibleWindows) => { logger.info("APP:ACTIVATE", { hasVisibleWindows }); if (!hasVisibleWindows) { - WindowManager.getInstance(false)?.ensureMainWindow(false); + windowManager.ensureMainWindow(false); } }); @@ -150,6 +148,8 @@ async function main(di: DiContainer) { blockQuit = false; }); + const clusterManager = di.inject(clusterManagerInjectable); + app.on("will-quit", (event) => { logger.debug("will-quit message"); @@ -158,7 +158,7 @@ async function main(di: DiContainer) { logger.info("APP:QUIT"); appEventBus.emit({ name: "app", action: "close" }); - ClusterManager.getInstance(false)?.stop(); // close cluster connections + clusterManager.stop(); // close cluster connections const kubeConfigSyncManager = di.inject(kubeconfigSyncManagerInjectable); @@ -232,17 +232,10 @@ async function main(di: DiContainer) { HelmRepoManager.createInstance(); // create the instance - const router = di.inject(routerInjectable); - const shellApiRequest = di.inject(shellApiRequestInjectable); - - const lensProxy = LensProxy.createInstance(router, httpProxy.createProxy(), { - getClusterForRequest: (req) => ClusterManager.getInstance().getClusterForRequest(req), - kubeApiUpgradeRequest, - shellApiRequest, - }); - - ClusterManager.createInstance().init(); + const lensProxy = di.inject(lensProxyInjectable); + const proxyPort = di.inject(lensProxyPortInjectable); + clusterManager.init(); initializers.initClusterMetadataDetectors(); try { @@ -257,7 +250,7 @@ async function main(di: DiContainer) { // test proxy connection try { logger.info("🔎 Testing LensProxy connection ..."); - const versionFromProxy = await getAppVersionFromProxyServer(lensProxy.port); + const versionFromProxy = await getAppVersionFromProxyServer(proxyPort.get()); if (getAppVersion() !== versionFromProxy) { logger.error("Proxy server responded with invalid response"); @@ -296,16 +289,12 @@ async function main(di: DiContainer) { // (On Windows and Linux, we get a flag. On MacOS, we get special API.) const startHidden = process.argv.includes("--hidden") || (isMac && app.getLoginItemSettings().wasOpenedAsHidden); - logger.info("🖥️ Starting WindowManager"); - const windowManager = di.inject(windowManagerInjectable); - const applicationMenuItems = di.inject(applicationMenuItemsInjectable); - const trayMenuItems = di.inject(trayMenuItemsInjectable); - const navigateToPreferences = di.inject(navigateToPreferencesInjectable); + const initTray = di.inject(initTrayInjectable); onQuitCleanup.push( initMenu(applicationMenuItems), - await initTray(windowManager, trayMenuItems, navigateToPreferences), + await initTray(), () => ShellSession.cleanup(), ); diff --git a/src/main/initializers/cluster-metadata-detectors.ts b/src/main/initializers/cluster-metadata-detectors.ts index 95d724bf3f..fc34c86d65 100644 --- a/src/main/initializers/cluster-metadata-detectors.ts +++ b/src/main/initializers/cluster-metadata-detectors.ts @@ -3,12 +3,12 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { ClusterIdDetector } from "../cluster-detectors/cluster-id-detector"; -import { DetectorRegistry } from "../cluster-detectors/detector-registry"; -import { DistributionDetector } from "../cluster-detectors/distribution-detector"; -import { LastSeenDetector } from "../cluster-detectors/last-seen-detector"; -import { NodesCountDetector } from "../cluster-detectors/nodes-count-detector"; -import { VersionDetector } from "../cluster-detectors/version-detector"; +import { ClusterIdDetector } from "../cluster/detectors/cluster-id-detector"; +import { DetectorRegistry } from "../cluster/detectors/detector-registry"; +import { DistributionDetector } from "../cluster/detectors/distribution-detector"; +import { LastSeenDetector } from "../cluster/detectors/last-seen-detector"; +import { NodesCountDetector } from "../cluster/detectors/nodes-count-detector"; +import { VersionDetector } from "../cluster/detectors/version-detector"; export function initClusterMetadataDetectors() { DetectorRegistry.createInstance() diff --git a/src/main/initializers/init-ipc-main-handlers/init-ipc-main-handlers.injectable.ts b/src/main/initializers/init-ipc-main-handlers/init-ipc-main-handlers.injectable.ts index d92897bb50..740c64f7de 100644 --- a/src/main/initializers/init-ipc-main-handlers/init-ipc-main-handlers.injectable.ts +++ b/src/main/initializers/init-ipc-main-handlers/init-ipc-main-handlers.injectable.ts @@ -7,6 +7,10 @@ import directoryForLensLocalStorageInjectable from "../../../common/directory-fo import { initIpcMainHandlers } from "./init-ipc-main-handlers"; import getAbsolutePathInjectable from "../../../common/path/get-absolute-path.injectable"; import applicationMenuItemsInjectable from "../../menu/application-menu-items.injectable"; +import appEventBusInjectable from "../../../common/app-event-bus/app-event-bus.injectable"; +import clusterManagerInjectable from "../../cluster/manager.injectable"; +import clusterStoreInjectable from "../../../common/cluster/store.injectable"; +import getClusterByIdInjectable from "../../../common/cluster/get-by-id.injectable"; const initIpcMainHandlersInjectable = getInjectable({ id: "init-ipc-main-handlers", @@ -15,6 +19,10 @@ const initIpcMainHandlersInjectable = getInjectable({ applicationMenuItems: di.inject(applicationMenuItemsInjectable), directoryForLensLocalStorage: di.inject(directoryForLensLocalStorageInjectable), getAbsolutePath: di.inject(getAbsolutePathInjectable), + appEventBus: di.inject(appEventBusInjectable), + clusterManager: di.inject(clusterManagerInjectable), + clusterStore: di.inject(clusterStoreInjectable), + getClusterById: di.inject(getClusterByIdInjectable), }), }); diff --git a/src/main/initializers/init-ipc-main-handlers/init-ipc-main-handlers.ts b/src/main/initializers/init-ipc-main-handlers/init-ipc-main-handlers.ts index 1a657b78be..2ef432ed4f 100644 --- a/src/main/initializers/init-ipc-main-handlers/init-ipc-main-handlers.ts +++ b/src/main/initializers/init-ipc-main-handlers/init-ipc-main-handlers.ts @@ -7,13 +7,12 @@ import type { IpcMainInvokeEvent } from "electron"; import { BrowserWindow, Menu } from "electron"; import { clusterFrameMap } from "../../../common/cluster-frames"; import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterDeleteHandler, clusterSetDeletingHandler, clusterClearDeletingHandler } from "../../../common/ipc/cluster"; -import type { ClusterId } from "../../../common/cluster-types"; -import { ClusterStore } from "../../../common/cluster-store/cluster-store"; -import { appEventBus } from "../../../common/app-event-bus/event-bus"; +import type { ClusterId } from "../../../common/cluster/types"; +import type { AppEventBus } from "../../../common/app-event-bus/event-bus"; import { broadcastMainChannel, broadcastMessage, ipcMainHandle, ipcMainOn } from "../../../common/ipc"; import { catalogEntityRegistry } from "../../catalog"; import { pushCatalogToRenderer } from "../../catalog-pusher"; -import { ClusterManager } from "../../cluster-manager"; +import type { ClusterManager } from "../../cluster/manager"; import { ResourceApplier } from "../../resource-applier"; import { remove } from "fs-extra"; import { onLocationChange, handleWindowAction } from "../../ipc/window"; @@ -25,22 +24,35 @@ import { getNativeThemeChannel } from "../../../common/ipc/native-theme"; import type { GetAbsolutePath } from "../../../common/path/get-absolute-path.injectable"; import type { IComputedValue } from "mobx"; import type { MenuItemOpts } from "../../menu/application-menu-items.injectable"; +import type { GetClusterById } from "../../../common/cluster/get-by-id.injectable"; +import type { ClusterStore } from "../../../common/cluster/store"; interface Dependencies { directoryForLensLocalStorage: string; getAbsolutePath: GetAbsolutePath; applicationMenuItems: IComputedValue; + clusterManager: ClusterManager; + getClusterById: GetClusterById; + appEventBus: AppEventBus; + clusterStore: ClusterStore; } -export const initIpcMainHandlers = ({ applicationMenuItems, directoryForLensLocalStorage, getAbsolutePath }: Dependencies) => () => { +export const initIpcMainHandlers = ({ + applicationMenuItems, + directoryForLensLocalStorage, + getAbsolutePath, + clusterManager, + getClusterById, + appEventBus, + clusterStore, +}: Dependencies) => () => { ipcMainHandle(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => { - return ClusterStore.getInstance() - .getById(clusterId) + return getClusterById(clusterId) ?.activate(force); }); ipcMainHandle(clusterSetFrameIdHandler, (event: IpcMainInvokeEvent, clusterId: ClusterId) => { - const cluster = ClusterStore.getInstance().getById(clusterId); + const cluster = getClusterById(clusterId); if (cluster) { clusterFrameMap.set(cluster.id, { frameId: event.frameId, processId: event.processId }); @@ -51,18 +63,17 @@ export const initIpcMainHandlers = ({ applicationMenuItems, directoryForLensLoca }); ipcMainOn(clusterVisibilityHandler, (event, clusterId?: ClusterId) => { - ClusterManager.getInstance().visibleCluster = clusterId; + clusterManager.visibleCluster = clusterId; }); ipcMainHandle(clusterRefreshHandler, (event, clusterId: ClusterId) => { - return ClusterStore.getInstance() - .getById(clusterId) + return getClusterById(clusterId) ?.refresh({ refreshMetadata: true }); }); ipcMainHandle(clusterDisconnectHandler, (event, clusterId: ClusterId) => { appEventBus.emit({ name: "cluster", action: "stop" }); - const cluster = ClusterStore.getInstance().getById(clusterId); + const cluster = getClusterById(clusterId); if (cluster) { cluster.disconnect(); @@ -73,8 +84,7 @@ export const initIpcMainHandlers = ({ applicationMenuItems, directoryForLensLoca ipcMainHandle(clusterDeleteHandler, async (event, clusterId: ClusterId) => { appEventBus.emit({ name: "cluster", action: "remove" }); - const clusterStore = ClusterStore.getInstance(); - const cluster = clusterStore.getById(clusterId); + const cluster = getClusterById(clusterId); if (!cluster) { return; @@ -97,16 +107,16 @@ export const initIpcMainHandlers = ({ applicationMenuItems, directoryForLensLoca }); ipcMainHandle(clusterSetDeletingHandler, (event, clusterId: string) => { - ClusterManager.getInstance().deleting.add(clusterId); + clusterManager.deleting.add(clusterId); }); ipcMainHandle(clusterClearDeletingHandler, (event, clusterId: string) => { - ClusterManager.getInstance().deleting.delete(clusterId); + clusterManager.deleting.delete(clusterId); }); ipcMainHandle(clusterKubectlApplyAllHandler, async (event, clusterId: ClusterId, resources: string[], extraArgs: string[]) => { appEventBus.emit({ name: "cluster", action: "kubectl-apply-all" }); - const cluster = ClusterStore.getInstance().getById(clusterId); + const cluster = getClusterById(clusterId); if (cluster) { const applier = new ResourceApplier(cluster); @@ -125,7 +135,7 @@ export const initIpcMainHandlers = ({ applicationMenuItems, directoryForLensLoca ipcMainHandle(clusterKubectlDeleteAllHandler, async (event, clusterId: ClusterId, resources: string[], extraArgs: string[]) => { appEventBus.emit({ name: "cluster", action: "kubectl-delete-all" }); - const cluster = ClusterStore.getInstance().getById(clusterId); + const cluster = getClusterById(clusterId); if (cluster) { const applier = new ResourceApplier(cluster); diff --git a/src/main/k8s-request.ts b/src/main/k8s-request.ts index 8531e9abb7..f223827fd1 100644 --- a/src/main/k8s-request.ts +++ b/src/main/k8s-request.ts @@ -7,11 +7,14 @@ import type { RequestPromiseOptions } from "request-promise-native"; import request from "request-promise-native"; import { apiKubePrefix } from "../common/vars"; import type { IMetricsReqParams } from "../common/k8s-api/endpoints/metrics.api"; -import { LensProxy } from "./lens-proxy"; import type { Cluster } from "../common/cluster/cluster"; +import { Environments, getEnvironmentSpecificLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; +import lensProxyPortInjectable from "./lens-proxy/port.injectable"; export async function k8sRequest(cluster: Cluster, path: string, options: RequestPromiseOptions = {}): Promise { - const kubeProxyUrl = `http://localhost:${LensProxy.getInstance().port}${apiKubePrefix}`; + const di = getEnvironmentSpecificLegacyGlobalDiForExtensionApi(Environments.main); + const proxyPort = di.inject(lensProxyPortInjectable); + const kubeProxyUrl = `http://localhost:${proxyPort.get()}${apiKubePrefix}`; options.headers ??= {}; options.json ??= true; diff --git a/src/main/kube-auth-proxy/cert-cache.injectable.ts b/src/main/kube-auth-proxy/cert-cache.injectable.ts new file mode 100644 index 0000000000..98380aec64 --- /dev/null +++ b/src/main/kube-auth-proxy/cert-cache.injectable.ts @@ -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(), +}); + +export default kubeAuthCertCacheInjectable; diff --git a/src/main/kube-auth-proxy/create-kube-auth-proxy.injectable.ts b/src/main/kube-auth-proxy/create-kube-auth-proxy.injectable.ts index 9d62a04cef..1548b58f34 100644 --- a/src/main/kube-auth-proxy/create-kube-auth-proxy.injectable.ts +++ b/src/main/kube-auth-proxy/create-kube-auth-proxy.injectable.ts @@ -7,11 +7,10 @@ import type { KubeAuthProxyDependencies } from "./kube-auth-proxy"; import { KubeAuthProxy } from "./kube-auth-proxy"; import type { Cluster } from "../../common/cluster/cluster"; import path from "path"; -import selfsigned from "selfsigned"; import { getBinaryName } from "../../common/vars"; import directoryForBundledBinariesInjectable from "../../common/app-paths/directory-for-bundled-binaries/directory-for-bundled-binaries.injectable"; import spawnInjectable from "../child-process/spawn.injectable"; -import { getKubeAuthProxyCertificate } from "./get-kube-auth-proxy-certificate"; +import getKubeAuthProxyCertificateInjectable from "./get-proxy-cert.injectable"; export type CreateKubeAuthProxy = (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => KubeAuthProxy; @@ -20,17 +19,13 @@ const createKubeAuthProxyInjectable = getInjectable({ instantiate: (di): CreateKubeAuthProxy => { const binaryName = getBinaryName("lens-k8s-proxy"); - - return (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => { - const clusterUrl = new URL(cluster.apiUrl); - const dependencies: KubeAuthProxyDependencies = { - proxyBinPath: path.join(di.inject(directoryForBundledBinariesInjectable), binaryName), - proxyCert: getKubeAuthProxyCertificate(clusterUrl.hostname, selfsigned.generate), - spawn: di.inject(spawnInjectable), - }; - - return new KubeAuthProxy(dependencies, cluster, environmentVariables); + const dependencies: KubeAuthProxyDependencies = { + proxyBinPath: path.join(di.inject(directoryForBundledBinariesInjectable), binaryName), + getKubeAuthProxyCertificate: di.inject(getKubeAuthProxyCertificateInjectable), + spawn: di.inject(spawnInjectable), }; + + return (cluster, envVars) => new KubeAuthProxy(dependencies, cluster, envVars); }, }); diff --git a/src/main/kube-auth-proxy/generate-cert.injectable.ts b/src/main/kube-auth-proxy/generate-cert.injectable.ts new file mode 100644 index 0000000000..00ddf36d02 --- /dev/null +++ b/src/main/kube-auth-proxy/generate-cert.injectable.ts @@ -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; diff --git a/src/main/kube-auth-proxy/get-kube-auth-proxy-certificate.ts b/src/main/kube-auth-proxy/get-kube-auth-proxy-certificate.ts deleted file mode 100644 index fb4c13da60..0000000000 --- a/src/main/kube-auth-proxy/get-kube-auth-proxy-certificate.ts +++ /dev/null @@ -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 = 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; -} diff --git a/src/main/kube-auth-proxy/get-proxy-cert.injectable.ts b/src/main/kube-auth-proxy/get-proxy-cert.injectable.ts new file mode 100644 index 0000000000..a245c023b4 --- /dev/null +++ b/src/main/kube-auth-proxy/get-proxy-cert.injectable.ts @@ -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; diff --git a/src/main/kube-auth-proxy/kube-auth-proxy.ts b/src/main/kube-auth-proxy/kube-auth-proxy.ts index dfeff29c98..946c96a0fd 100644 --- a/src/main/kube-auth-proxy/kube-auth-proxy.ts +++ b/src/main/kube-auth-proxy/kube-auth-proxy.ts @@ -3,21 +3,24 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { ChildProcess, spawn } from "child_process"; +import type { ChildProcess } from "child_process"; import { waitUntilUsed } from "tcp-port-used"; import { randomBytes } from "crypto"; import type { Cluster } from "../../common/cluster/cluster"; import logger from "../logger"; import { getPortFrom } from "../utils/get-port"; import { makeObservable, observable, when } from "mobx"; +import type { Spawn } from "../child-process/spawn.injectable"; +import type { GetKubeAuthProxyCertificate } from "./get-proxy-cert.injectable"; import type { SelfSignedCert } from "selfsigned"; +import { URL } from "url"; const startingServeRegex = /starting to serve on (?
.+)/i; export interface KubeAuthProxyDependencies { - proxyBinPath: string; - proxyCert: SelfSignedCert; - spawn: typeof spawn; + readonly proxyBinPath: string; + spawn: Spawn; + getKubeAuthProxyCertificate: GetKubeAuthProxyCertificate; } export class KubeAuthProxy { @@ -30,9 +33,12 @@ export class KubeAuthProxy { protected _port: number; protected proxyProcess?: ChildProcess; @observable protected ready = false; + private readonly proxyCert: SelfSignedCert; - constructor(private dependencies: KubeAuthProxyDependencies, protected readonly cluster: Cluster, protected readonly env: NodeJS.ProcessEnv) { + constructor(protected readonly dependencies: KubeAuthProxyDependencies, protected readonly cluster: Cluster, protected readonly env: NodeJS.ProcessEnv) { makeObservable(this); + + this.proxyCert = this.dependencies.getKubeAuthProxyCertificate(new URL(cluster.apiUrl).hostname); } get whenReady() { @@ -45,7 +51,6 @@ export class KubeAuthProxy { } const proxyBin = this.dependencies.proxyBinPath; - const cert = this.dependencies.proxyCert; this.proxyProcess = this.dependencies.spawn(proxyBin, [], { env: { @@ -53,8 +58,8 @@ export class KubeAuthProxy { KUBECONFIG: this.cluster.kubeConfigPath, KUBECONFIG_CONTEXT: this.cluster.contextName, API_PREFIX: this.apiPrefix, - PROXY_KEY: cert.private, - PROXY_CERT: cert.cert, + PROXY_KEY: this.proxyCert.private, + PROXY_CERT: this.proxyCert.cert, }, }); this.proxyProcess.on("error", (error) => { diff --git a/src/main/kubeconfig-manager/create-kubeconfig-manager.injectable.ts b/src/main/kubeconfig-manager/create-kubeconfig-manager.injectable.ts index fd75737555..9a2aac35b2 100644 --- a/src/main/kubeconfig-manager/create-kubeconfig-manager.injectable.ts +++ b/src/main/kubeconfig-manager/create-kubeconfig-manager.injectable.ts @@ -5,21 +5,26 @@ import { getInjectable } from "@ogre-tools/injectable"; import type { Cluster } from "../../common/cluster/cluster"; import directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable"; +import type { KubeconfigManagerDependencies } from "./kubeconfig-manager"; import { KubeconfigManager } from "./kubeconfig-manager"; +import lensProxyPortInjectable from "../lens-proxy/port.injectable"; export interface KubeConfigManagerInstantiationParameter { cluster: Cluster; } +export type CreateKubeConfigManager = (cluster: Cluster) => KubeconfigManager | undefined; + const createKubeconfigManagerInjectable = getInjectable({ id: "create-kubeconfig-manager", - instantiate: (di) => { - const dependencies = { + instantiate: (di): CreateKubeConfigManager => { + const dependencies: KubeconfigManagerDependencies = { directoryForTemp: di.inject(directoryForTempInjectable), + proxyPort: di.inject(lensProxyPortInjectable), }; - return (cluster: Cluster) => new KubeconfigManager(dependencies, cluster); + return (cluster) => new KubeconfigManager(dependencies, cluster); }, }); diff --git a/src/main/kubeconfig-manager/kubeconfig-manager.ts b/src/main/kubeconfig-manager/kubeconfig-manager.ts index fb72b5ebac..599b518c6a 100644 --- a/src/main/kubeconfig-manager/kubeconfig-manager.ts +++ b/src/main/kubeconfig-manager/kubeconfig-manager.ts @@ -5,15 +5,16 @@ import type { KubeConfig } from "@kubernetes/client-node"; import type { Cluster } from "../../common/cluster/cluster"; -import type { ContextHandler } from "../context-handler/context-handler"; import path from "path"; import fs from "fs-extra"; import { dumpConfigYaml } from "../../common/kube-helpers"; import logger from "../logger"; -import { LensProxy } from "../lens-proxy"; +import type { IObservableValue } from "mobx"; +import { waitUntilSet } from "../../common/utils/wait-observable-value"; -interface Dependencies { - directoryForTemp: string; +export interface KubeconfigManagerDependencies { + readonly directoryForTemp: string; + readonly proxyPort: IObservableValue; } export class KubeconfigManager { @@ -26,11 +27,7 @@ export class KubeconfigManager { */ protected tempFilePath: string | null | undefined = null; - protected contextHandler: ContextHandler; - - constructor(private dependencies: Dependencies, protected cluster: Cluster) { - this.contextHandler = cluster.contextHandler; - } + constructor(protected readonly dependencies: KubeconfigManagerDependencies, protected readonly cluster: Cluster) {} /** * @@ -71,17 +68,13 @@ export class KubeconfigManager { protected async ensureFile() { try { - await this.contextHandler.ensureServer(); + await this.cluster.contextHandler.ensureServer(); this.tempFilePath = await this.createProxyKubeconfig(); } catch (error) { - throw Object.assign(new Error("Failed to creat temp config for auth-proxy"), { cause: error }); + throw new Error(`Failed to creat temp config for auth-proxy: ${error}`); } } - get resolveProxyUrl() { - return `http://127.0.0.1:${LensProxy.getInstance().port}/${this.cluster.id}`; - } - /** * Creates new "temporary" kubeconfig that point to the kubectl-proxy. * This way any user of the config does not need to know anything about the auth etc. details. @@ -94,12 +87,13 @@ export class KubeconfigManager { `kubeconfig-${id}`, ); const kubeConfig = await cluster.getKubeconfig(); + const proxyPort = await waitUntilSet(this.dependencies.proxyPort); const proxyConfig: Partial = { currentContext: contextName, clusters: [ { name: contextName, - server: this.resolveProxyUrl, + server: `http://127.0.0.1:${proxyPort}/${this.cluster.id}`, skipTLSVerify: undefined, }, ], diff --git a/src/main/proxy-functions/kube-api-upgrade-request.ts b/src/main/lens-proxy/kube-api-upgrade-request.ts similarity index 84% rename from src/main/proxy-functions/kube-api-upgrade-request.ts rename to src/main/lens-proxy/kube-api-upgrade-request.ts index 899d22164e..59e753fbca 100644 --- a/src/main/proxy-functions/kube-api-upgrade-request.ts +++ b/src/main/lens-proxy/kube-api-upgrade-request.ts @@ -7,17 +7,17 @@ import { chunk } from "lodash"; import tls from "tls"; import url from "url"; import { apiKubePrefix } from "../../common/vars"; -import type { ProxyApiRequestArgs } from "./types"; +import type { LensProxyRequestHandler } from "./lens-proxy"; const skipRawHeaders = new Set(["Host", "Authorization"]); -export async function kubeApiUpgradeRequest({ req, socket, head, cluster }: ProxyApiRequestArgs) { +export const kubeApiUpgradeRequest: LensProxyRequestHandler = async ({ req, socket, head, cluster }) => { const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, ""); - const proxyCa = await cluster.contextHandler.resolveAuthProxyCa(); + const proxyCa = cluster.contextHandler.resolveAuthProxyCa(); const apiUrl = url.parse(cluster.apiUrl); const pUrl = url.parse(proxyUrl); - const connectOpts = { - port: parseInt(pUrl.port), + const connectOpts = { + port: parseInt(pUrl.port), host: pUrl.hostname, ca: proxyCa, }; @@ -62,4 +62,4 @@ export async function kubeApiUpgradeRequest({ req, socket, head, cluster }: Prox socket.on("error", function () { proxySocket.end(); }); -} +}; diff --git a/src/main/lens-proxy.ts b/src/main/lens-proxy/lens-proxy.ts similarity index 66% rename from src/main/lens-proxy.ts rename to src/main/lens-proxy/lens-proxy.ts index 945c7269e8..1c20e5053d 100644 --- a/src/main/lens-proxy.ts +++ b/src/main/lens-proxy/lens-proxy.ts @@ -7,24 +7,25 @@ import net from "net"; import type http from "http"; import spdy from "spdy"; import type httpProxy from "http-proxy"; -import { apiPrefix, apiKubePrefix } from "../common/vars"; -import type { Router } from "./router/router"; -import type { ContextHandler } from "./context-handler/context-handler"; -import logger from "./logger"; -import { Singleton } from "../common/utils"; -import type { Cluster } from "../common/cluster/cluster"; -import type { ProxyApiRequestArgs } from "./proxy-functions"; -import { appEventBus } from "../common/app-event-bus/event-bus"; -import { getBoolean } from "./utils/parse-query"; +import { apiPrefix, apiKubePrefix } from "../../common/vars"; +import type { Router } from "../router/router"; +import type { ContextHandler } from "../context-handler/context-handler"; +import { appEventBus } from "../../common/app-event-bus/event-bus"; +import { getBoolean } from "../utils/parse-query"; +import type { GetClusterForRequest } from "../cluster/get-cluster-for-request.injectable"; +import type { IObservableValue } from "mobx"; +import type { Logger } from "../../common/logger"; +import type { Cluster } from "../../common/cluster/cluster"; -type GetClusterForRequest = (req: http.IncomingMessage) => Cluster | null; - -export interface LensProxyFunctions { - getClusterForRequest: GetClusterForRequest; - shellApiRequest: (args: ProxyApiRequestArgs) => void | Promise; - kubeApiUpgradeRequest: (args: ProxyApiRequestArgs) => void | Promise; +export interface ProxyApiRequestArgs { + req: http.IncomingMessage; + socket: net.Socket; + head: Buffer; + cluster: Cluster; } +export type LensProxyRequestHandler = (args: ProxyApiRequestArgs) => void; + const watchParam = "watch"; const followParam = "follow"; @@ -52,22 +53,24 @@ const disallowedPorts = new Set([ 10080, ]); -export class LensProxy extends Singleton { +interface Dependencies { + readonly router: Router; + readonly proxy: httpProxy; + readonly proxyPort: IObservableValue; + readonly logger: Logger; + getClusterForRequest: GetClusterForRequest; + shellApiRequest: LensProxyRequestHandler; + kubeApiUpgradeRequest: LensProxyRequestHandler; +} + +export class LensProxy { protected origin: string; protected proxyServer: http.Server; protected closed = false; - protected retryCounters = new Map(); - protected getClusterForRequest: GetClusterForRequest; - - public port: number; - - constructor(protected router: Router, protected proxy: httpProxy, { shellApiRequest, kubeApiUpgradeRequest, getClusterForRequest }: LensProxyFunctions) { - super(); - - this.configureProxy(proxy); - - this.getClusterForRequest = getClusterForRequest; + protected readonly retryCounters = new Map(); + constructor(protected readonly dependencies: Dependencies) { + this.configureProxy(this.dependencies.proxy); this.proxyServer = spdy.createServer({ spdy: { plain: true, @@ -79,17 +82,22 @@ export class LensProxy extends Singleton { this.proxyServer .on("upgrade", (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => { - const cluster = getClusterForRequest(req); + const cluster = this.dependencies.getClusterForRequest(req); if (!cluster) { - logger.error(`[LENS-PROXY]: Could not find cluster for upgrade request from url=${req.url}`); + this.dependencies.logger.error(`Could not find cluster for upgrade request from url=${req.url}`); socket.destroy(); } else { const isInternal = req.url.startsWith(`${apiPrefix}?`); - const reqHandler = isInternal ? shellApiRequest : kubeApiUpgradeRequest; + const reqHandler = isInternal + ? this.dependencies.shellApiRequest + : this.dependencies.kubeApiUpgradeRequest; - (async () => reqHandler({ req, socket, head, cluster }))() - .catch(error => logger.error("[LENS-PROXY]: failed to handle proxy upgrade", error)); + try { + reqHandler({ req, socket, head, cluster }); + } catch (error) { + this.dependencies.logger.error("failed to handle proxy upgrade", error); + } } }); } @@ -110,18 +118,18 @@ export class LensProxy extends Singleton { const { address, port } = this.proxyServer.address() as net.AddressInfo; - logger.info(`[LENS-PROXY]: Proxy server has started at ${address}:${port}`); + this.dependencies.logger.info(`Proxy server has started at ${address}:${port}`); this.proxyServer.on("error", (error) => { - logger.info(`[LENS-PROXY]: Subsequent error: ${error}`); + this.dependencies.logger.info(`Subsequent error: ${error}`); }); - this.port = port; + this.dependencies.proxyPort.set(port); appEventBus.emit({ name: "lens-proxy", action: "listen", params: { port }}); resolve(port); }) .once("error", (error) => { - logger.info(`[LENS-PROXY]: Proxy server failed to start: ${error}`); + this.dependencies.logger.info(`Proxy server failed to start: ${error}`); reject(error); }); }); @@ -144,7 +152,7 @@ export class LensProxy extends Singleton { return; } - logger.warn(`[LENS-PROXY]: Proxy server has with port known to be considered unsafe to connect to by chrome, restarting...`); + this.dependencies.logger.warn(`Proxy server has with port known to be considered unsafe to connect to by chrome, restarting...`); if (seenPorts.has(port)) { /** @@ -160,7 +168,7 @@ export class LensProxy extends Singleton { } close() { - logger.info("[LENS-PROXY]: Closing server"); + this.dependencies.logger.info("Closing server"); this.proxyServer.close(); this.closed = true; } @@ -183,10 +191,10 @@ export class LensProxy extends Singleton { return; } - logger.error(`[LENS-PROXY]: http proxy errored for cluster: ${error}`, { url: req.url }); + this.dependencies.logger.error(`http proxy errored for cluster: ${error}`, { url: req.url }); if (target) { - logger.debug(`Failed proxy to target: ${JSON.stringify(target, null, 2)}`); + this.dependencies.logger.debug(`Failed proxy to target: ${JSON.stringify(target, null, 2)}`); if (req.method === "GET" && (!res.statusCode || res.statusCode >= 500)) { const reqId = this.getRequestId(req); @@ -194,11 +202,11 @@ export class LensProxy extends Singleton { const timeoutMs = retryCount * 250; if (retryCount < 20) { - logger.debug(`Retrying proxy request to url: ${reqId}`); + this.dependencies.logger.debug(`Retrying proxy request to url: ${reqId}`); setTimeout(() => { this.retryCounters.set(reqId, retryCount + 1); this.handleRequest(req, res) - .catch(error => logger.error(`[LENS-PROXY]: failed to handle request on proxy error: ${error}`)); + .catch(error => this.dependencies.logger.error(`failed to handle request on proxy error: ${error}`)); }, timeoutMs); } } @@ -207,7 +215,7 @@ export class LensProxy extends Singleton { try { res.writeHead(500).end(`Oops, something went wrong.\n${error}`); } catch (e) { - logger.error(`[LENS-PROXY]: Failed to write headers: `, e); + this.dependencies.logger.error(`Failed to write headers: `, e); } }); @@ -228,15 +236,16 @@ export class LensProxy extends Singleton { } protected async handleRequest(req: http.IncomingMessage, res: http.ServerResponse) { - const cluster = this.getClusterForRequest(req); + const cluster = this.dependencies.getClusterForRequest(req); if (cluster) { const proxyTarget = await this.getProxyTarget(req, cluster.contextHandler); if (proxyTarget) { - return this.proxy.web(req, res, proxyTarget); + return this.dependencies.proxy.web(req, res, proxyTarget); } } - this.router.route(cluster, req, res); + + this.dependencies.router.route(cluster, req, res); } } diff --git a/src/main/lens-proxy/logger.injectable.ts b/src/main/lens-proxy/logger.injectable.ts new file mode 100644 index 0000000000..56909eb5b9 --- /dev/null +++ b/src/main/lens-proxy/logger.injectable.ts @@ -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; diff --git a/src/main/lens-proxy/port.injectable.ts b/src/main/lens-proxy/port.injectable.ts new file mode 100644 index 0000000000..a91770660d --- /dev/null +++ b/src/main/lens-proxy/port.injectable.ts @@ -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(), +}); + +export default lensProxyPortInjectable; diff --git a/src/main/lens-proxy/proxy.injectable.ts b/src/main/lens-proxy/proxy.injectable.ts new file mode 100644 index 0000000000..430f66e59f --- /dev/null +++ b/src/main/lens-proxy/proxy.injectable.ts @@ -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; diff --git a/src/main/lens-proxy/shell-api/handler.injectable.ts b/src/main/lens-proxy/shell-api/handler.injectable.ts new file mode 100644 index 0000000000..c88e84482d --- /dev/null +++ b/src/main/lens-proxy/shell-api/handler.injectable.ts @@ -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; diff --git a/src/main/lens-proxy/shell-api/logger.injectable.ts b/src/main/lens-proxy/shell-api/logger.injectable.ts new file mode 100644 index 0000000000..84b5e76e26 --- /dev/null +++ b/src/main/lens-proxy/shell-api/logger.injectable.ts @@ -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; diff --git a/src/main/lens-proxy/shell-api/request/authenticate.injectable.ts b/src/main/lens-proxy/shell-api/request/authenticate.injectable.ts new file mode 100644 index 0000000000..04eeb9912d --- /dev/null +++ b/src/main/lens-proxy/shell-api/request/authenticate.injectable.ts @@ -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; diff --git a/src/main/proxy-functions/shell-api-request/shell-request-authenticator/shell-request-authenticator.injectable.ts b/src/main/lens-proxy/shell-api/request/authenticator.injectable.ts similarity index 86% rename from src/main/proxy-functions/shell-api-request/shell-request-authenticator/shell-request-authenticator.injectable.ts rename to src/main/lens-proxy/shell-api/request/authenticator.injectable.ts index c273f105d0..cac05bbfb3 100644 --- a/src/main/proxy-functions/shell-api-request/shell-request-authenticator/shell-request-authenticator.injectable.ts +++ b/src/main/lens-proxy/shell-api/request/authenticator.injectable.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import { ShellRequestAuthenticator } from "./shell-request-authenticator"; +import { ShellRequestAuthenticator } from "./authenticator"; const shellRequestAuthenticatorInjectable = getInjectable({ id: "shell-request-authenticator", diff --git a/src/main/lens-proxy/shell-api/request/authenticator.ts b/src/main/lens-proxy/shell-api/request/authenticator.ts new file mode 100644 index 0000000000..71bb563ef0 --- /dev/null +++ b/src/main/lens-proxy/shell-api/request/authenticator.ts @@ -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>(); + + 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; + } +} diff --git a/src/main/logger/logger.injectable.ts b/src/main/logger/logger.injectable.ts new file mode 100644 index 0000000000..c669bdcac4 --- /dev/null +++ b/src/main/logger/logger.injectable.ts @@ -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; + diff --git a/src/main/menu/application-menu-items.injectable.ts b/src/main/menu/application-menu-items.injectable.ts index 9e95414838..b2ce758362 100644 --- a/src/main/menu/application-menu-items.injectable.ts +++ b/src/main/menu/application-menu-items.injectable.ts @@ -5,19 +5,12 @@ import { getInjectable } from "@ogre-tools/injectable"; import { checkForUpdates } from "../app-updater"; import { docsUrl, productName, supportUrl } from "../../common/vars"; -import { exitApp } from "../exit-app"; import { broadcastMessage } from "../../common/ipc"; import { openBrowser } from "../../common/utils"; import { showAbout } from "./menu"; -import windowManagerInjectable from "../window-manager.injectable"; -import type { - BrowserWindow, - MenuItem, - MenuItemConstructorOptions } from "electron"; -import { - webContents, -} from "electron"; -import loggerInjectable from "../../common/logger.injectable"; +import windowManagerInjectable from "../window/manager.injectable"; +import type { BrowserWindow, MenuItem, MenuItemConstructorOptions } from "electron"; +import { webContents } from "electron"; import appNameInjectable from "../app-paths/app-name/app-name.injectable"; import electronMenuItemsInjectable from "./electron-menu-items.injectable"; import isAutoUpdateEnabledInjectable from "../is-auto-update-enabled.injectable"; @@ -28,6 +21,8 @@ import navigateToWelcomeInjectable from "../../common/front-end-routing/routes/w import navigateToAddClusterInjectable from "../../common/front-end-routing/routes/add-cluster/navigate-to-add-cluster.injectable"; import isMacInjectable from "../../common/vars/is-mac.injectable"; import { computed } from "mobx"; +import appMenuLoggerInjectable from "./logger.injectable"; +import exitAppInjectable from "../utils/exit-app.injectable"; function ignoreIf(check: boolean, menuItems: MenuItemConstructorOptions[]) { return check ? [] : menuItems; @@ -41,26 +36,22 @@ const applicationMenuItemsInjectable = getInjectable({ id: "application-menu-items", instantiate: (di) => { - const logger = di.inject(loggerInjectable); + const logger = di.inject(appMenuLoggerInjectable); const appName = di.inject(appNameInjectable); const isMac = di.inject(isMacInjectable); const isAutoUpdateEnabled = di.inject(isAutoUpdateEnabledInjectable); const electronMenuItems = di.inject(electronMenuItemsInjectable); + const windowManager = di.inject(windowManagerInjectable); + const navigateToPreferences = di.inject(navigateToPreferencesInjectable); + const navigateToExtensions = di.inject(navigateToExtensionsInjectable); + const navigateToCatalog = di.inject(navigateToCatalogInjectable); + const navigateToWelcome = di.inject(navigateToWelcomeInjectable); + const navigateToAddCluster = di.inject(navigateToAddClusterInjectable); + const exitApp = di.inject(exitAppInjectable); + const autoUpdateDisabled = !isAutoUpdateEnabled(); return computed((): MenuItemOpts[] => { - - // TODO: These injects should happen outside of the computed. - // TODO: Remove temporal dependencies in WindowManager to make sure timing is correct. - const windowManager = di.inject(windowManagerInjectable); - const navigateToPreferences = di.inject(navigateToPreferencesInjectable); - const navigateToExtensions = di.inject(navigateToExtensionsInjectable); - const navigateToCatalog = di.inject(navigateToCatalogInjectable); - const navigateToWelcome = di.inject(navigateToWelcomeInjectable); - const navigateToAddCluster = di.inject(navigateToAddClusterInjectable); - - const autoUpdateDisabled = !isAutoUpdateEnabled(); - - logger.info(`[MENU]: autoUpdateDisabled=${autoUpdateDisabled}`); + logger.info(`auto update is ${autoUpdateDisabled ? "disabled" : "enabled"}`); const macAppMenu: MenuItemOpts = { label: appName, @@ -269,7 +260,7 @@ const applicationMenuItemsInjectable = getInjectable({ id: "documentation", click: async () => { openBrowser(docsUrl).catch((error) => { - logger.error("[MENU]: failed to open browser", { error }); + logger.error("failed to open browser", { error }); }); }, }, @@ -278,7 +269,7 @@ const applicationMenuItemsInjectable = getInjectable({ id: "support", click: async () => { openBrowser(supportUrl).catch((error) => { - logger.error("[MENU]: failed to open browser", { error }); + logger.error("failed to open browser", { error }); }); }, }, @@ -316,7 +307,7 @@ const applicationMenuItemsInjectable = getInjectable({ for (const menuItem of electronMenuItems.get()) { if (!appMenu.has(menuItem.parentId)) { logger.error( - `[MENU]: cannot register menu item for parentId=${menuItem.parentId}, parent item doesn't exist`, + `cannot register menu item for parentId=${menuItem.parentId}, parent item doesn't exist`, { menuItem }, ); diff --git a/src/main/menu/logger.injectable.ts b/src/main/menu/logger.injectable.ts new file mode 100644 index 0000000000..d76f0b1b7a --- /dev/null +++ b/src/main/menu/logger.injectable.ts @@ -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; diff --git a/src/main/navigate-to-url/navigate-to-url.injectable.ts b/src/main/navigate-to-url/navigate-to-url.injectable.ts index 5a3cd2bf49..dc6f79e3f3 100644 --- a/src/main/navigate-to-url/navigate-to-url.injectable.ts +++ b/src/main/navigate-to-url/navigate-to-url.injectable.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import windowManagerInjectable from "../window-manager.injectable"; +import windowManagerInjectable from "../window/manager.injectable"; import { navigateToUrlInjectionToken } from "../../common/front-end-routing/navigate-to-url-injection-token"; const navigateToUrlInjectable = getInjectable({ diff --git a/src/main/protocol-handler/__test__/router.test.ts b/src/main/protocol-handler/__test__/router.test.ts index d8c88f7f22..55ed283e92 100644 --- a/src/main/protocol-handler/__test__/router.test.ts +++ b/src/main/protocol-handler/__test__/router.test.ts @@ -10,14 +10,15 @@ import { ProtocolHandlerExtension, ProtocolHandlerInternal } from "../../../comm import { delay, noop } from "../../../common/utils"; import { LensExtension } from "../../../extensions/main-api"; import { ExtensionsStore } from "../../../extensions/extensions-store/extensions-store"; -import type { LensProtocolRouterMain } from "../lens-protocol-router-main/lens-protocol-router-main"; +import type { LensProtocolRouterMain } from "../router"; import mockFs from "mock-fs"; import { getDiForUnitTesting } from "../../getDiForUnitTesting"; import extensionLoaderInjectable from "../../../extensions/extension-loader/extension-loader.injectable"; -import lensProtocolRouterMainInjectable from "../lens-protocol-router-main/lens-protocol-router-main.injectable"; +import lensProtocolRouterMainInjectable from "../router.injectable"; import extensionsStoreInjectable from "../../../extensions/extensions-store/extensions-store.injectable"; import getConfigurationFileModelInjectable from "../../../common/get-configuration-file-model/get-configuration-file-model.injectable"; import appVersionInjectable from "../../../common/get-configuration-file-model/app-version/app-version.injectable"; +import ensureMainWindowInjectable from "../../window/ensure-main.injectable"; jest.mock("../../../common/ipc"); @@ -48,10 +49,10 @@ describe("protocol router tests", () => { await di.runSetups(); + di.override(ensureMainWindowInjectable, () => async () => null); + extensionLoader = di.inject(extensionLoaderInjectable); - extensionsStore = di.inject(extensionsStoreInjectable); - lpr = di.inject(lensProtocolRouterMainInjectable); lpr.rendererLoaded = true; diff --git a/src/main/protocol-handler/index.ts b/src/main/protocol-handler/index.ts index 973d27c28c..286687198d 100644 --- a/src/main/protocol-handler/index.ts +++ b/src/main/protocol-handler/index.ts @@ -3,4 +3,4 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -export * from "./lens-protocol-router-main/lens-protocol-router-main"; +export * from "./router"; diff --git a/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable.ts b/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable.ts deleted file mode 100644 index f09a7c2b8a..0000000000 --- a/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable.ts +++ /dev/null @@ -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; diff --git a/src/main/protocol-handler/router.injectable.ts b/src/main/protocol-handler/router.injectable.ts new file mode 100644 index 0000000000..cbc071be86 --- /dev/null +++ b/src/main/protocol-handler/router.injectable.ts @@ -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; diff --git a/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.ts b/src/main/protocol-handler/router.ts similarity index 75% rename from src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.ts rename to src/main/protocol-handler/router.ts index e40ba5fa11..04a772ef70 100644 --- a/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.ts +++ b/src/main/protocol-handler/router.ts @@ -3,18 +3,15 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import logger from "../../logger"; -import * as proto from "../../../common/protocol-handler"; +import * as proto from "../../common/protocol-handler"; import URLParse from "url-parse"; -import type { LensExtension } from "../../../extensions/lens-extension"; -import { broadcastMessage } from "../../../common/ipc"; +import type { LensExtension } from "../../extensions/lens-extension"; +import { broadcastMessage } from "../../common/ipc"; import { observable, when, makeObservable } from "mobx"; -import type { RouteAttempt } from "../../../common/protocol-handler"; -import { ProtocolHandlerInvalid } from "../../../common/protocol-handler"; -import { disposer, noop } from "../../../common/utils"; -import { WindowManager } from "../../window-manager"; -import type { ExtensionLoader } from "../../../extensions/extension-loader"; -import type { ExtensionsStore } from "../../../extensions/extensions-store/extensions-store"; +import type { LensProtocolRouterDependencies, RouteAttempt } from "../../common/protocol-handler"; +import { ProtocolHandlerInvalid } from "../../common/protocol-handler"; +import { disposer, noop } from "../../common/utils"; +import type { EnsureMainWindow } from "../window/ensure-main.injectable"; export interface FallbackHandler { (name: string): Promise; @@ -37,21 +34,19 @@ function checkHost(url: URLParse): boolean { } } -interface Dependencies { - extensionLoader: ExtensionLoader; - extensionsStore: ExtensionsStore; +interface LensProtocolRouterMainDependencies extends LensProtocolRouterDependencies { + ensureMainWindow: EnsureMainWindow; } export class LensProtocolRouterMain extends proto.LensProtocolRouter { - private missingExtensionHandlers: FallbackHandler[] = []; + private readonly missingExtensionHandlers: FallbackHandler[] = []; @observable rendererLoaded = false; - protected disposers = disposer(); + protected readonly disposers = disposer(); - constructor(protected dependencies: Dependencies) { + constructor(protected readonly dependencies: LensProtocolRouterMainDependencies) { super(dependencies); - makeObservable(this); } @@ -73,10 +68,10 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter { throw new proto.RoutingError(proto.RoutingErrorType.INVALID_PROTOCOL, url); } - WindowManager.getInstance(false)?.ensureMainWindow().catch(noop); + this.dependencies.ensureMainWindow().catch(noop); const routeInternally = checkHost(url); - logger.info(`${proto.LensProtocolRouter.LoggingPrefix}: routing ${url.toString()}`); + this.dependencies.logger.info(`routing ${url.toString()}`); if (routeInternally) { this._routeToInternal(url); @@ -87,9 +82,9 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter { broadcastMessage(ProtocolHandlerInvalid, error.toString(), rawUrl); if (error instanceof proto.RoutingError) { - logger.error(`${proto.LensProtocolRouter.LoggingPrefix}: ${error}`, { url: error.url }); + this.dependencies.logger.error(`${error}`, { url: error.url }); } else { - logger.error(`${proto.LensProtocolRouter.LoggingPrefix}: ${error}`, { rawUrl }); + this.dependencies.logger.error(`${error}`, { rawUrl }); } } } diff --git a/src/main/proxy-functions/index.ts b/src/main/proxy-functions/index.ts deleted file mode 100644 index 5d374825ee..0000000000 --- a/src/main/proxy-functions/index.ts +++ /dev/null @@ -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"; diff --git a/src/main/proxy-functions/shell-api-request/shell-api-request.injectable.ts b/src/main/proxy-functions/shell-api-request/shell-api-request.injectable.ts deleted file mode 100644 index 73477d1889..0000000000 --- a/src/main/proxy-functions/shell-api-request/shell-api-request.injectable.ts +++ /dev/null @@ -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; diff --git a/src/main/proxy-functions/shell-api-request/shell-api-request.ts b/src/main/proxy-functions/shell-api-request/shell-api-request.ts deleted file mode 100644 index 8a797f5f68..0000000000 --- a/src/main/proxy-functions/shell-api-request/shell-api-request.ts +++ /dev/null @@ -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 }; -} - -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)); - }); -}; diff --git a/src/main/proxy-functions/shell-api-request/shell-request-authenticator/shell-request-authenticator.ts b/src/main/proxy-functions/shell-api/shell-request-authenticator/shell-request-authenticator.ts similarity index 86% rename from src/main/proxy-functions/shell-api-request/shell-request-authenticator/shell-request-authenticator.ts rename to src/main/proxy-functions/shell-api/shell-request-authenticator/shell-request-authenticator.ts index bafd073c14..27d575f8e2 100644 --- a/src/main/proxy-functions/shell-api-request/shell-request-authenticator/shell-request-authenticator.ts +++ b/src/main/proxy-functions/shell-api/shell-request-authenticator/shell-request-authenticator.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getOrInsertMap } from "../../../../common/utils"; -import type { ClusterId } from "../../../../common/cluster-types"; +import type { ClusterId } from "../../../../common/cluster/types"; import { ipcMainHandle } from "../../../../common/ipc"; import crypto from "crypto"; import { promisify } from "util"; @@ -11,14 +11,13 @@ import { promisify } from "util"; const randomBytes = promisify(crypto.randomBytes); export class ShellRequestAuthenticator { - private tokens = new Map>(); + private readonly tokens = new Map>(); init() { ipcMainHandle("cluster:shell-api", async (event, clusterId, tabId) => { const authToken = Uint8Array.from(await randomBytes(128)); - const forCluster = getOrInsertMap(this.tokens, clusterId); - forCluster.set(tabId, authToken); + getOrInsertMap(this.tokens, clusterId).set(tabId, authToken); return authToken; }); diff --git a/src/main/proxy-functions/types.ts b/src/main/proxy-functions/types.ts deleted file mode 100644 index 4d7e3a684e..0000000000 --- a/src/main/proxy-functions/types.ts +++ /dev/null @@ -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; -} diff --git a/src/main/routes/metrics/add-metrics-route.injectable.ts b/src/main/routes/metrics/add-metrics-route.injectable.ts index 470fecd56c..dd8398da5c 100644 --- a/src/main/routes/metrics/add-metrics-route.injectable.ts +++ b/src/main/routes/metrics/add-metrics-route.injectable.ts @@ -7,8 +7,8 @@ import { getInjectable } from "@ogre-tools/injectable"; import { apiPrefix } from "../../../common/vars"; import type { LensApiRequest, Route } from "../../router/router"; import { routeInjectionToken } from "../../router/router.injectable"; -import type { ClusterPrometheusMetadata } from "../../../common/cluster-types"; -import { ClusterMetadataKey } from "../../../common/cluster-types"; +import type { ClusterPrometheusMetadata } from "../../../common/cluster/types"; +import { ClusterMetadataKey } from "../../../common/cluster/types"; import logger from "../../logger"; import type { Cluster } from "../../../common/cluster/cluster"; import { getMetrics } from "../../k8s-request"; diff --git a/src/main/shell-session/local-shell-session/local-shell-session.ts b/src/main/shell-session/local-shell-session/local-shell-session.ts index 314f8700ec..6f627a46f1 100644 --- a/src/main/shell-session/local-shell-session/local-shell-session.ts +++ b/src/main/shell-session/local-shell-session/local-shell-session.ts @@ -7,7 +7,7 @@ import type WebSocket from "ws"; import path from "path"; import { UserStore } from "../../../common/user-store"; import type { Cluster } from "../../../common/cluster/cluster"; -import type { ClusterId } from "../../../common/cluster-types"; +import type { ClusterId } from "../../../common/cluster/types"; import { ShellSession } from "../shell-session"; import type { Kubectl } from "../../kubectl/kubectl"; import { baseBinariesDir } from "../../../common/vars"; diff --git a/src/main/shell-session/shell-env-modifier/terminal-shell-env-modifiers.ts b/src/main/shell-session/shell-env-modifier/terminal-shell-env-modifiers.ts index f60631e554..b438d7917d 100644 --- a/src/main/shell-session/shell-env-modifier/terminal-shell-env-modifiers.ts +++ b/src/main/shell-session/shell-env-modifier/terminal-shell-env-modifiers.ts @@ -5,15 +5,15 @@ import type { IComputedValue } from "mobx"; import { computed } from "mobx"; -import type { ClusterId } from "../../../common/cluster-types"; +import type { ClusterId } from "../../../common/cluster/types"; import type { LensMainExtension } from "../../../extensions/lens-main-extension"; import { catalogEntityRegistry } from "../../catalog"; - + interface Dependencies { extensions: IComputedValue; } - -export const terminalShellEnvModify = ({ extensions }: Dependencies) => + +export const terminalShellEnvModify = ({ extensions }: Dependencies) => (clusterId: ClusterId, env: Record) => { const terminalShellEnvModifiers = computed(() => ( extensions.get() diff --git a/src/main/shell-session/shell-session.ts b/src/main/shell-session/shell-session.ts index 831c24bc02..db231b94da 100644 --- a/src/main/shell-session/shell-session.ts +++ b/src/main/shell-session/shell-session.ts @@ -108,8 +108,8 @@ export enum WebSocketCloseEvent { export abstract class ShellSession { abstract ShellType: string; - private static shellEnvs = new Map>(); - private static processes = new Map(); + private static readonly shellEnvs = new Map>(); + private static readonly processes = new Map(); /** * Kill all remaining shell backing processes. Should be called when about to @@ -240,7 +240,7 @@ export abstract class ShellSession { } if (typeof data === "string") { - return void logger.silly(`[SHELL-SESSION]: Received message from ${this.terminalId}`, { data }); + return void logger.debug(`[SHELL-SESSION]: Received message from ${this.terminalId}`, { data }); } try { diff --git a/src/main/tray/icon.ts b/src/main/tray/icon.ts new file mode 100644 index 0000000000..5324378f64 --- /dev/null +++ b/src/main/tray/icon.ts @@ -0,0 +1,60 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { NativeImage, Tray } from "electron"; +import { nativeImage, nativeTheme } from "electron"; +import { JSDOM } from "jsdom"; +import sharp from "sharp"; +import { getOrInsertWithAsync, base64, type Disposer } from "../../common/utils"; +import LogoLens from "../../renderer/components/icon/logo-lens.svg"; + +export interface CreateTrayIconArgs { + shouldUseDarkColors: boolean; + size: number; + sourceSvg: string; +} + +const trayIcons = new Map(); + +async function createTrayIcon({ shouldUseDarkColors, size, sourceSvg }: CreateTrayIconArgs): Promise { + return getOrInsertWithAsync(trayIcons, shouldUseDarkColors, async () => { + const trayIconColor = shouldUseDarkColors ? "white" : "black"; // Invert to show contrast + const parsedSvg = base64.decode(sourceSvg.split("base64,")[1]); + const svgDom = new JSDOM(`${parsedSvg}`); + const svgRoot = svgDom.window.document.body.getElementsByTagName("svg")[0]; + + svgRoot.innerHTML += ``; + + const iconBuffer = await sharp(Buffer.from(svgRoot.outerHTML)) + .resize({ width: size, height: size }) + .png() + .toBuffer(); + + return nativeImage.createFromBuffer(iconBuffer); + }); +} + +export function createCurrentTrayIcon() { + return createTrayIcon({ + shouldUseDarkColors: nativeTheme.shouldUseDarkColors, + size: 16, + sourceSvg: LogoLens, + }); +} + +export function watchShouldUseDarkColors(tray: Tray): Disposer { + let prevShouldUseDarkColors = nativeTheme.shouldUseDarkColors; + const onUpdated = () => { + if (prevShouldUseDarkColors !== nativeTheme.shouldUseDarkColors) { + prevShouldUseDarkColors = nativeTheme.shouldUseDarkColors; + createCurrentTrayIcon() + .then(img => tray.setImage(img)); + } + }; + + nativeTheme.on("updated", onUpdated); + + return () => nativeTheme.off("updated", onUpdated); +} diff --git a/src/main/tray/init-tray.injectable.ts b/src/main/tray/init-tray.injectable.ts new file mode 100644 index 0000000000..8360a795fc --- /dev/null +++ b/src/main/tray/init-tray.injectable.ts @@ -0,0 +1,58 @@ +/** + * 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 { Tray } from "electron"; +import { autorun } from "mobx"; +import { disposer } from "../../common/utils"; +import { isWindows } from "../../common/vars"; +import ensureMainWindowInjectable from "../window/ensure-main.injectable"; +import { createCurrentTrayIcon, watchShouldUseDarkColors } from "./icon"; +import trayMenuInjectable from "./tray-menu.injectable"; +import packageInfo from "../../../package.json"; +import trayLoggerInjectable from "./logger.injectable"; + +const initTrayInjectable = getInjectable({ + id: "init-tray", + instantiate: (di) => { + const ensureMainWindow = di.inject(ensureMainWindowInjectable); + const trayMenu = di.inject(trayMenuInjectable); + const logger = di.inject(trayLoggerInjectable); + + return async () => { + const icon = await createCurrentTrayIcon(); + const dispose = disposer(); + const tray = new Tray(icon); + + tray.setToolTip(packageInfo.description); + tray.setIgnoreDoubleClickEvents(true); + + dispose.push(watchShouldUseDarkColors(tray)); + + if (isWindows) { + tray.on("click", () => { + ensureMainWindow() + .catch(error => logger.error(`Failed to open lens`, { error })); + }); + } + + dispose.push( + autorun(() => { + try { + tray.setContextMenu(trayMenu.get()); + } catch (error) { + logger.error(`building failed`, { error }); + } + }), + () => { + tray.destroy(); + }, + ); + + return dispose; + }; + }, +}); + +export default initTrayInjectable; diff --git a/src/main/tray/logger.injectable.ts b/src/main/tray/logger.injectable.ts new file mode 100644 index 0000000000..80089ef4fe --- /dev/null +++ b/src/main/tray/logger.injectable.ts @@ -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 trayLoggerInjectable = getInjectable({ + id: "tray-logger", + instantiate: (di) => di.inject(childLoggerInjectable, { + prefix: "TRAY", + }), +}); + +export default trayLoggerInjectable; diff --git a/src/main/tray/tray-menu-items.injectable.ts b/src/main/tray/tray-menu-items.injectable.ts index 8ee9d25e5e..6efb470d2b 100644 --- a/src/main/tray/tray-menu-items.injectable.ts +++ b/src/main/tray/tray-menu-items.injectable.ts @@ -6,15 +6,14 @@ import { getInjectable } from "@ogre-tools/injectable"; import { computed } from "mobx"; import mainExtensionsInjectable from "../../extensions/main-extensions.injectable"; -const trayItemsInjectable = getInjectable({ - id: "tray-items", +const extensionTrayItemsInjectable = getInjectable({ + id: "extension-tray-items", instantiate: (di) => { const extensions = di.inject(mainExtensionsInjectable); - return computed(() => - extensions.get().flatMap(extension => extension.trayMenus)); + return computed(() => extensions.get().flatMap(extension => extension.trayMenus)); }, }); -export default trayItemsInjectable; +export default extensionTrayItemsInjectable; diff --git a/src/main/tray/tray-menu-items.test.ts b/src/main/tray/tray-menu-items.test.ts index da9f2f2745..a20767534c 100644 --- a/src/main/tray/tray-menu-items.test.ts +++ b/src/main/tray/tray-menu-items.test.ts @@ -4,7 +4,7 @@ */ import type { DiContainer } from "@ogre-tools/injectable"; import { LensMainExtension } from "../../extensions/lens-main-extension"; -import trayItemsInjectable from "./tray-menu-items.injectable"; +import extensionTrayItemsInjectable from "./tray-menu-items.injectable"; import type { IComputedValue } from "mobx"; import { computed, ObservableMap, runInAction } from "mobx"; import { getDiForUnitTesting } from "../getDiForUnitTesting"; @@ -28,7 +28,7 @@ describe("tray-menu-items", () => { () => computed(() => [...extensionsStub.values()]), ); - trayMenuItems = di.inject(trayItemsInjectable); + trayMenuItems = di.inject(extensionTrayItemsInjectable); }); it("does not have any items yet", () => { diff --git a/src/main/tray/tray-menu.injectable.ts b/src/main/tray/tray-menu.injectable.ts new file mode 100644 index 0000000000..c698a58e51 --- /dev/null +++ b/src/main/tray/tray-menu.injectable.ts @@ -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 { Menu } from "electron"; +import { computed } from "mobx"; +import navigateToPreferencesInjectable from "../../common/front-end-routing/routes/preferences/navigate-to-preferences.injectable"; +import { productName } from "../../common/vars"; +import { checkForUpdates } from "../app-updater"; +import isAutoUpdateEnabledInjectable from "../is-auto-update-enabled.injectable"; +import { showAbout } from "../menu/menu"; +import exitAppInjectable from "../utils/exit-app.injectable"; +import ensureMainWindowInjectable from "../window/ensure-main.injectable"; +import trayLoggerInjectable from "./logger.injectable"; +import extensionTrayItemsInjectable from "./tray-menu-items.injectable"; +import type { TrayMenuRegistration } from "./tray-menu-registration"; + +function getMenuItemConstructorOptions(trayItem: TrayMenuRegistration): Electron.MenuItemConstructorOptions { + return { + ...trayItem, + submenu: trayItem.submenu?.map(getMenuItemConstructorOptions), + click: trayItem.click + ? () => { + trayItem.click(trayItem); + } + : undefined, + }; +} + +const trayMenuInjectable = getInjectable({ + id: "tray-menu", + instantiate: (di) => { + const extensionTrayItems = di.inject(extensionTrayItemsInjectable); + const navigateToPreferences = di.inject(navigateToPreferencesInjectable); + const ensureMainWindow = di.inject(ensureMainWindowInjectable); + const logger = di.inject(trayLoggerInjectable); + const isAutoUpdateEnabled = di.inject(isAutoUpdateEnabledInjectable); + const exitApp = di.inject(exitAppInjectable); + + return computed(() => Menu.buildFromTemplate([ + { + label: `Open ${productName}`, + click() { + ensureMainWindow() + .catch(error => logger.error(`Failed to open lens`, { error })); + }, + }, + { + label: "Preferences", + click() { + navigateToPreferences(); + }, + }, + ...( + isAutoUpdateEnabled() + ? [ + { + label: "Check for updates", + click() { + checkForUpdates() + .then(() => ensureMainWindow()); + }, + }, + ] + : [] + ), + ...extensionTrayItems + .get() + .map(getMenuItemConstructorOptions), + { + label: `About ${productName}`, + click() { + ensureMainWindow() + .then(showAbout) + .catch(error => logger.error(`Failed to show Lens About view`, { error })); + }, + }, + { type: "separator" }, + { + label: "Quit App", + click() { + exitApp(); + }, + }, + ])); + }, +}); + +export default trayMenuInjectable; diff --git a/src/main/tray/tray.ts b/src/main/tray/tray.ts deleted file mode 100644 index af129ac27a..0000000000 --- a/src/main/tray/tray.ts +++ /dev/null @@ -1,181 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import packageInfo from "../../../package.json"; -import type { NativeImage } from "electron"; -import { Menu, nativeImage, nativeTheme, Tray } from "electron"; -import type { IComputedValue } from "mobx"; -import { autorun } from "mobx"; -import { showAbout } from "../menu/menu"; -import { checkForUpdates, isAutoUpdateEnabled } from "../app-updater"; -import type { WindowManager } from "../window-manager"; -import logger from "../logger"; -import { isWindows, productName } from "../../common/vars"; -import { exitApp } from "../exit-app"; -import type { Disposer } from "../../common/utils"; -import { base64, disposer, getOrInsertWithAsync, toJS } from "../../common/utils"; -import type { TrayMenuRegistration } from "./tray-menu-registration"; -import sharp from "sharp"; -import LogoLens from "../../renderer/components/icon/logo-lens.svg"; -import { JSDOM } from "jsdom"; - - -const TRAY_LOG_PREFIX = "[TRAY]"; - -// note: instance of Tray should be saved somewhere, otherwise it disappears -export let tray: Tray; - -interface CreateTrayIconArgs { - shouldUseDarkColors: boolean; - size: number; - sourceSvg: string; -} - -const trayIcons = new Map(); - -async function createTrayIcon({ shouldUseDarkColors, size, sourceSvg }: CreateTrayIconArgs): Promise { - return getOrInsertWithAsync(trayIcons, shouldUseDarkColors, async () => { - const trayIconColor = shouldUseDarkColors ? "white" : "black"; // Invert to show contrast - const parsedSvg = base64.decode(sourceSvg.split("base64,")[1]); - const svgDom = new JSDOM(`${parsedSvg}`); - const svgRoot = svgDom.window.document.body.getElementsByTagName("svg")[0]; - - svgRoot.innerHTML += ``; - - const iconBuffer = await sharp(Buffer.from(svgRoot.outerHTML)) - .resize({ width: size, height: size }) - .png() - .toBuffer(); - - return nativeImage.createFromBuffer(iconBuffer); - }); -} - -function createCurrentTrayIcon() { - return createTrayIcon({ - shouldUseDarkColors: nativeTheme.shouldUseDarkColors, - size: 16, - sourceSvg: LogoLens, - }); -} - -function watchShouldUseDarkColors(tray: Tray): Disposer { - let prevShouldUseDarkColors = nativeTheme.shouldUseDarkColors; - const onUpdated = () => { - if (prevShouldUseDarkColors !== nativeTheme.shouldUseDarkColors) { - prevShouldUseDarkColors = nativeTheme.shouldUseDarkColors; - createCurrentTrayIcon() - .then(img => tray.setImage(img)); - } - }; - - nativeTheme.on("updated", onUpdated); - - return () => nativeTheme.off("updated", onUpdated); -} - -export async function initTray( - windowManager: WindowManager, - trayMenuItems: IComputedValue, - navigateToPreferences: () => void, -): Promise { - const icon = await createCurrentTrayIcon(); - const dispose = disposer(); - - tray = new Tray(icon); - tray.setToolTip(packageInfo.description); - tray.setIgnoreDoubleClickEvents(true); - - dispose.push(watchShouldUseDarkColors(tray)); - - if (isWindows) { - tray.on("click", () => { - windowManager - .ensureMainWindow() - .catch(error => logger.error(`${TRAY_LOG_PREFIX}: Failed to open lens`, { error })); - }); - } - - dispose.push( - autorun(() => { - try { - const menu = createTrayMenu(windowManager, toJS(trayMenuItems.get()), navigateToPreferences); - - tray.setContextMenu(menu); - } catch (error) { - logger.error(`${TRAY_LOG_PREFIX}: building failed`, { error }); - } - }), - () => { - tray?.destroy(); - tray = null; - }, - ); - - return dispose; -} - -function getMenuItemConstructorOptions(trayItem: TrayMenuRegistration): Electron.MenuItemConstructorOptions { - return { - ...trayItem, - submenu: trayItem.submenu ? trayItem.submenu.map(getMenuItemConstructorOptions) : undefined, - click: trayItem.click ? () => { - trayItem.click(trayItem); - } : undefined, - }; -} - -function createTrayMenu( - windowManager: WindowManager, - extensionTrayItems: TrayMenuRegistration[], - navigateToPreferences: () => void, -): Menu { - let template: Electron.MenuItemConstructorOptions[] = [ - { - label: `Open ${productName}`, - click() { - windowManager - .ensureMainWindow() - .catch(error => logger.error(`${TRAY_LOG_PREFIX}: Failed to open lens`, { error })); - }, - }, - { - label: "Preferences", - click() { - navigateToPreferences(); - }, - }, - ]; - - if (isAutoUpdateEnabled()) { - template.push({ - label: "Check for updates", - click() { - checkForUpdates() - .then(() => windowManager.ensureMainWindow()); - }, - }); - } - - template = template.concat(extensionTrayItems.map(getMenuItemConstructorOptions)); - - return Menu.buildFromTemplate(template.concat([ - { - label: `About ${productName}`, - click() { - windowManager.ensureMainWindow() - .then(showAbout) - .catch(error => logger.error(`${TRAY_LOG_PREFIX}: Failed to show Lens About view`, { error })); - }, - }, - { type: "separator" }, - { - label: "Quit App", - click() { - exitApp(); - }, - }, - ])); -} diff --git a/src/main/utils/exit-app.injectable.ts b/src/main/utils/exit-app.injectable.ts new file mode 100644 index 0000000000..59747b64ed --- /dev/null +++ b/src/main/utils/exit-app.injectable.ts @@ -0,0 +1,36 @@ +/** + * 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 loggerInjectable from "../logger/logger.injectable"; +import windowManagerInjectable from "../window/manager.injectable"; +import clusterManagerInjectable from "../cluster/manager.injectable"; +import electronAppInjectable from "../app-paths/get-electron-app-path/electron-app/electron-app.injectable"; +import appEventBusInjectable from "../../common/app-event-bus/app-event-bus.injectable"; + +export type ExitApp = () => void; + +const exitAppInjectable = getInjectable({ + id: "exit-app", + instantiate: (di): ExitApp => { + const logger = di.inject(loggerInjectable); + const windowManager = di.inject(windowManagerInjectable); + const clusterManager = di.inject(clusterManagerInjectable); + const app = di.inject(electronAppInjectable); + const appEventBus = di.inject(appEventBusInjectable); + + return () => { + appEventBus.emit({ name: "service", action: "close" }); + windowManager.hide(); + clusterManager.stop(); + logger.info("SERVICE:QUIT"); + setTimeout(() => { + app.exit(); + }, 1000); + }; + }, +}); + +export default exitAppInjectable; diff --git a/src/main/window-manager.injectable.ts b/src/main/window-manager.injectable.ts deleted file mode 100644 index 6a1f54a660..0000000000 --- a/src/main/window-manager.injectable.ts +++ /dev/null @@ -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 { WindowManager } from "./window-manager"; - -const windowManagerInjectable = getInjectable({ - id: "window-manager", - - instantiate: () => { - WindowManager.resetInstance(); - - return WindowManager.createInstance(); - }, - - causesSideEffects: true, -}); - -export default windowManagerInjectable; diff --git a/src/main/window/ensure-main.injectable.ts b/src/main/window/ensure-main.injectable.ts new file mode 100644 index 0000000000..44cb2d9195 --- /dev/null +++ b/src/main/window/ensure-main.injectable.ts @@ -0,0 +1,19 @@ +/** + * 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 windowManagerInjectable from "./manager.injectable"; + +export type EnsureMainWindow = (showSplash?: boolean) => Promise; + +const ensureMainWindowInjectable = getInjectable({ + id: "ensure-main-window", + instantiate: (di): EnsureMainWindow => { + const windowManager = di.inject(windowManagerInjectable); + + return (showSplash) => windowManager.ensureMainWindow(showSplash); + }, +}); + +export default ensureMainWindowInjectable; diff --git a/src/main/window/logger.injectable.ts b/src/main/window/logger.injectable.ts new file mode 100644 index 0000000000..5d05aa6e8d --- /dev/null +++ b/src/main/window/logger.injectable.ts @@ -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 windowManagerLoggerInjectable = getInjectable({ + id: "window-manager-logger", + instantiate: (di) => di.inject(childLoggerInjectable, { + prefix: "WINDOW-MANAGER", + }), +}); + +export default windowManagerLoggerInjectable; diff --git a/src/main/window/manager.injectable.ts b/src/main/window/manager.injectable.ts new file mode 100644 index 0000000000..edeb5060d6 --- /dev/null +++ b/src/main/window/manager.injectable.ts @@ -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 lensProxyPortInjectable from "../lens-proxy/port.injectable"; +import windowManagerLoggerInjectable from "./logger.injectable"; +import { WindowManager } from "./manager"; + +const windowManagerInjectable = getInjectable({ + id: "window-manager", + instantiate: (di) => new WindowManager({ + logger: di.inject(windowManagerLoggerInjectable), + proxyPort: di.inject(lensProxyPortInjectable), + }), +}); + +export default windowManagerInjectable; diff --git a/src/main/window-manager.ts b/src/main/window/manager.ts similarity index 79% rename from src/main/window-manager.ts rename to src/main/window/manager.ts index 260a82ccc0..edc8655211 100644 --- a/src/main/window-manager.ts +++ b/src/main/window/manager.ts @@ -3,20 +3,21 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { ClusterId } from "../common/cluster-types"; +import type { ClusterId } from "../../common/cluster/types"; +import type { IObservableValue } from "mobx"; import { makeObservable, observable } from "mobx"; import { app, BrowserWindow, dialog, ipcMain, webContents } from "electron"; import windowStateKeeper from "electron-window-state"; -import { appEventBus } from "../common/app-event-bus/event-bus"; -import { ipcMainOn } from "../common/ipc"; -import { delay, iter, Singleton, openBrowser } from "../common/utils"; -import type { ClusterFrameInfo } from "../common/cluster-frames"; -import { clusterFrameMap } from "../common/cluster-frames"; -import { IpcRendererNavigationEvents } from "../renderer/navigation/events"; -import logger from "./logger"; -import { isMac, productName } from "../common/vars"; -import { LensProxy } from "./lens-proxy"; -import { bundledExtensionsLoaded } from "../common/ipc/extension-handling"; +import { appEventBus } from "../../common/app-event-bus/event-bus"; +import { ipcMainOn } from "../../common/ipc"; +import { delay, iter, openBrowser } from "../../common/utils"; +import type { ClusterFrameInfo } from "../../common/cluster-frames"; +import { clusterFrameMap } from "../../common/cluster-frames"; +import { IpcRendererNavigationEvents } from "../../renderer/navigation/events"; +import { isMac, productName } from "../../common/vars"; +import { bundledExtensionsLoaded } from "../../common/ipc/extension-handling"; +import type { Logger } from "../../common/logger"; +import { waitUntilSet } from "../../common/utils/wait-observable-value"; function isHideable(window: BrowserWindow | null): boolean { return Boolean(window && !window.isDestroyed()); @@ -28,23 +29,27 @@ export interface SendToViewArgs { data?: any[]; } -export class WindowManager extends Singleton { - public mainContentUrl = `http://localhost:${LensProxy.getInstance().port}`; +interface Dependencies { + readonly proxyPort: IObservableValue; + readonly logger: Logger; +} +export class WindowManager { protected mainWindow: BrowserWindow; protected splashWindow: BrowserWindow; protected windowState: windowStateKeeper.State; - protected disposers: Record = {}; @observable activeClusterId: ClusterId; - constructor() { - super(); + constructor(protected readonly dependencies: Dependencies) { makeObservable(this); this.bindEvents(); } private async initMainWindow(showSplash: boolean) { + const proxyPort = await waitUntilSet(this.dependencies.proxyPort); + const mainContentUrl = `http://localhost:${proxyPort}`; + // Manage main window size and position with state persistence if (!this.windowState) { this.windowState = windowStateKeeper({ @@ -98,30 +103,30 @@ export class WindowManager extends Singleton { appEventBus.emit({ name: "app", action: "dom-ready" }); }) .on("did-fail-load", (_event, code, desc) => { - logger.error(`[WINDOW-MANAGER]: Failed to load Main window`, { code, desc }); + this.dependencies.logger.error(`Failed to load Main window`, { code, desc }); }) .on("did-finish-load", () => { - logger.info("[WINDOW-MANAGER]: Main window loaded"); + this.dependencies.logger.info("Main window loaded"); }) .on("will-attach-webview", (event, webPreferences, params) => { - logger.debug("[WINDOW-MANAGER]: Attaching webview"); + this.dependencies.logger.debug("Attaching webview"); // Following is security recommendations because we allow webview tag (webviewTag: true) // suggested by https://www.electronjs.org/docs/tutorial/security#11-verify-webview-options-before-creation // and https://www.electronjs.org/docs/tutorial/security#10-do-not-use-allowpopups if (webPreferences.preload) { - logger.warn("[WINDOW-MANAGER]: Strip away preload scripts of webview"); + this.dependencies.logger.warn("Strip away preload scripts of webview"); delete webPreferences.preload; } // @ts-expect-error some electron version uses webPreferences.preloadURL/webPreferences.preload if (webPreferences.preloadURL) { - logger.warn("[WINDOW-MANAGER]: Strip away preload scripts of webview"); + this.dependencies.logger.warn("Strip away preload scripts of webview"); delete webPreferences.preload; } if (params.allowpopups) { - logger.warn("[WINDOW-MANAGER]: We do not allow allowpopups props, stop webview from renderer"); + this.dependencies.logger.warn("We do not allow allowpopups props, stop webview from renderer"); // event.preventDefault() will destroy the guest page. event.preventDefault(); @@ -134,7 +139,7 @@ export class WindowManager extends Singleton { }) .setWindowOpenHandler((details) => { openBrowser(details.url).catch(error => { - logger.error("[WINDOW-MANAGER]: failed to open browser", { error }); + this.dependencies.logger.error("failed to open browser", { error }); }); return { action: "deny" }; @@ -143,10 +148,10 @@ export class WindowManager extends Singleton { try { if (showSplash) await this.showSplash(); - logger.info(`[WINDOW-MANAGER]: Loading Main window from url: ${this.mainContentUrl} ...`); - await this.mainWindow.loadURL(this.mainContentUrl); + this.dependencies.logger.info(`Loading Main window from url: ${mainContentUrl} ...`); + await this.mainWindow.loadURL(mainContentUrl); } catch (error) { - logger.error("Loading main window failed", { error }); + this.dependencies.logger.error("Loading main window failed", { error }); dialog.showErrorBox("ERROR!", error.toString()); } } @@ -172,7 +177,7 @@ export class WindowManager extends Singleton { try { await viewHasLoaded; await delay(50); // wait just a bit longer to let the first round of rendering happen - logger.info("[WINDOW-MANAGER]: Main window has reported that it has loaded"); + this.dependencies.logger.info("Main window has reported that it has loaded"); this.mainWindow.show(); this.splashWindow?.close(); @@ -181,7 +186,7 @@ export class WindowManager extends Singleton { appEventBus.emit({ name: "app", action: "start" }); }, 1000); } catch (error) { - logger.error(`Showing main window failed: ${error.stack || error}`); + this.dependencies.logger.error(`Showing main window failed: ${error.stack || error}`); dialog.showErrorBox("ERROR!", error.toString()); } @@ -277,9 +282,5 @@ export class WindowManager extends Singleton { this.splashWindow.destroy(); this.mainWindow = null; this.splashWindow = null; - Object.entries(this.disposers).forEach(([name, dispose]) => { - dispose(); - delete this.disposers[name]; - }); } } diff --git a/src/migrations/cluster-store/3.6.0-beta.1.ts b/src/migrations/cluster-store/3.6.0-beta.1.ts index d399c9f324..e208d80aed 100644 --- a/src/migrations/cluster-store/3.6.0-beta.1.ts +++ b/src/migrations/cluster-store/3.6.0-beta.1.ts @@ -11,7 +11,7 @@ import fse from "fs-extra"; import { loadConfigFromFileSync } from "../../common/kube-helpers"; import type { MigrationDeclaration } from "../helpers"; import { migrationLog } from "../helpers"; -import type { ClusterModel } from "../../common/cluster-types"; +import type { ClusterModel } from "../../common/cluster/types"; import { getLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; diff --git a/src/migrations/cluster-store/5.0.0-beta.10.ts b/src/migrations/cluster-store/5.0.0-beta.10.ts index a9fc759ee4..ed0babf6fe 100644 --- a/src/migrations/cluster-store/5.0.0-beta.10.ts +++ b/src/migrations/cluster-store/5.0.0-beta.10.ts @@ -5,7 +5,7 @@ import path from "path"; import fse from "fs-extra"; -import type { ClusterModel } from "../../common/cluster-types"; +import type { ClusterModel } from "../../common/cluster/types"; import type { MigrationDeclaration } from "../helpers"; import { getLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; diff --git a/src/migrations/cluster-store/5.0.0-beta.13.ts b/src/migrations/cluster-store/5.0.0-beta.13.ts index d386d006d2..2e4f1d252d 100644 --- a/src/migrations/cluster-store/5.0.0-beta.13.ts +++ b/src/migrations/cluster-store/5.0.0-beta.13.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { ClusterModel, ClusterPreferences, ClusterPrometheusPreferences } from "../../common/cluster-types"; +import type { ClusterModel, ClusterPreferences, ClusterPrometheusPreferences } from "../../common/cluster/types"; import type { MigrationDeclaration } from "../helpers"; import { migrationLog } from "../helpers"; import { generateNewIdFor } from "../utils"; diff --git a/src/migrations/cluster-store/snap.ts b/src/migrations/cluster-store/snap.ts index 9128dca82a..c0e511e5af 100644 --- a/src/migrations/cluster-store/snap.ts +++ b/src/migrations/cluster-store/snap.ts @@ -5,7 +5,7 @@ // Fix embedded kubeconfig paths under snap config -import type { ClusterModel } from "../../common/cluster-types"; +import type { ClusterModel } from "../../common/cluster/types"; import { getAppVersion } from "../../common/utils/app-version"; import fs from "fs"; import type { MigrationDeclaration } from "../helpers"; diff --git a/src/migrations/hotbar-store/5.0.0-beta.10.ts b/src/migrations/hotbar-store/5.0.0-beta.10.ts index 15466b993e..75fddb096d 100644 --- a/src/migrations/hotbar-store/5.0.0-beta.10.ts +++ b/src/migrations/hotbar-store/5.0.0-beta.10.ts @@ -7,7 +7,7 @@ import fse from "fs-extra"; import { isNull } from "lodash"; import path from "path"; import * as uuid from "uuid"; -import type { ClusterStoreModel } from "../../common/cluster-store/cluster-store"; +import type { ClusterStoreModel } from "../../common/cluster/store"; import type { Hotbar, HotbarItem } from "../../common/hotbar-types"; import { defaultHotbarCells, getEmptyHotbar } from "../../common/hotbar-types"; import type { MigrationDeclaration } from "../helpers"; diff --git a/src/migrations/user-store/5.0.3-beta.1.ts b/src/migrations/user-store/5.0.3-beta.1.ts index f46949942c..001b680dee 100644 --- a/src/migrations/user-store/5.0.3-beta.1.ts +++ b/src/migrations/user-store/5.0.3-beta.1.ts @@ -6,7 +6,7 @@ import { existsSync, readFileSync } from "fs"; import path from "path"; import os from "os"; -import type { ClusterStoreModel } from "../../common/cluster-store/cluster-store"; +import type { ClusterStoreModel } from "../../common/cluster/store"; import type { KubeconfigSyncEntry, UserPreferencesModel } from "../../common/user-store"; import type { MigrationDeclaration } from "../helpers"; import { migrationLog } from "../helpers"; diff --git a/src/renderer/api/catalog-entity-registry.ts b/src/renderer/api/catalog-entity-registry.ts index 1cc3f24a0d..3e6a70137d 100644 --- a/src/renderer/api/catalog-entity-registry.ts +++ b/src/renderer/api/catalog-entity-registry.ts @@ -9,7 +9,7 @@ import type { CatalogCategory, CatalogEntity, CatalogEntityData, CatalogCategory import { catalogCategoryRegistry } from "../../common/catalog"; import "../../common/catalog-entities"; import type { Cluster } from "../../common/cluster/cluster"; -import { ClusterStore } from "../../common/cluster-store/cluster-store"; +import { ClusterStore } from "../../common/cluster/store"; import { iter } from "../utils"; import type { Disposer } from "../utils"; import { once } from "lodash"; diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index 207ae3fd8f..39c6a0a6a0 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -30,7 +30,7 @@ import type { DiContainer } from "@ogre-tools/injectable"; import extensionLoaderInjectable from "../extensions/extension-loader/extension-loader.injectable"; import extensionDiscoveryInjectable from "../extensions/extension-discovery/extension-discovery.injectable"; import extensionInstallationStateStoreInjectable from "../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; -import clusterStoreInjectable from "../common/cluster-store/cluster-store.injectable"; +import clusterStoreInjectable from "../common/cluster/store.injectable"; import userStoreInjectable from "../common/user-store/user-store.injectable"; import initRootFrameInjectable from "./frames/root-frame/init-root-frame/init-root-frame.injectable"; import initClusterFrameInjectable from "./frames/cluster-frame/init-cluster-frame/init-cluster-frame.injectable"; diff --git a/src/renderer/cluster-frame-context/cluster-frame-context.injectable.ts b/src/renderer/cluster-frame-context/cluster-frame-context.injectable.ts index ad71a3dd17..071178b379 100644 --- a/src/renderer/cluster-frame-context/cluster-frame-context.injectable.ts +++ b/src/renderer/cluster-frame-context/cluster-frame-context.injectable.ts @@ -5,7 +5,7 @@ import { getInjectable } from "@ogre-tools/injectable"; import { ClusterFrameContext } from "./cluster-frame-context"; import namespaceStoreInjectable from "../components/+namespaces/namespace-store/namespace-store.injectable"; -import hostedClusterInjectable from "../../common/cluster-store/hosted-cluster.injectable"; +import hostedClusterInjectable from "../../common/cluster/hosted.injectable"; const clusterFrameContextInjectable = getInjectable({ id: "cluster-frame-context", @@ -15,7 +15,6 @@ const clusterFrameContextInjectable = getInjectable({ return new ClusterFrameContext( cluster, - { namespaceStore: di.inject(namespaceStoreInjectable), }, diff --git a/src/renderer/cluster-frame-context/cluster-frame-context.ts b/src/renderer/cluster-frame-context/cluster-frame-context.ts index 68ae6fa61d..000bec5b7c 100755 --- a/src/renderer/cluster-frame-context/cluster-frame-context.ts +++ b/src/renderer/cluster-frame-context/cluster-frame-context.ts @@ -9,11 +9,11 @@ import type { ClusterContext } from "../../common/k8s-api/cluster-context"; import { computed, makeObservable } from "mobx"; interface Dependencies { - namespaceStore: NamespaceStore; + readonly namespaceStore: NamespaceStore; } export class ClusterFrameContext implements ClusterContext { - constructor(public cluster: Cluster, private dependencies: Dependencies) { + constructor(public readonly cluster: Cluster, protected readonly dependencies: Dependencies) { makeObservable(this); } diff --git a/src/renderer/components/+cluster/cluster-no-metrics.tsx b/src/renderer/components/+cluster/cluster-no-metrics.tsx index 5c981008be..9148abfef5 100644 --- a/src/renderer/components/+cluster/cluster-no-metrics.tsx +++ b/src/renderer/components/+cluster/cluster-no-metrics.tsx @@ -11,7 +11,7 @@ import { cssNames } from "../../utils"; import type { NavigateToEntitySettings } from "../../../common/front-end-routing/routes/entity-settings/navigate-to-entity-settings.injectable"; import { withInjectables } from "@ogre-tools/injectable-react"; import navigateToEntitySettingsInjectable from "../../../common/front-end-routing/routes/entity-settings/navigate-to-entity-settings.injectable"; -import hostedClusterInjectable from "../../../common/cluster-store/hosted-cluster.injectable"; +import hostedClusterInjectable from "../../../common/cluster/hosted.injectable"; export interface ClusterNoMetricsProps { className: string; diff --git a/src/renderer/components/+cluster/cluster-overview.tsx b/src/renderer/components/+cluster/cluster-overview.tsx index 36590df34b..60ede3fcc1 100644 --- a/src/renderer/components/+cluster/cluster-overview.tsx +++ b/src/renderer/components/+cluster/cluster-overview.tsx @@ -19,8 +19,8 @@ import { ClusterMetrics } from "./cluster-metrics"; import type { ClusterOverviewStore } from "./cluster-overview-store/cluster-overview-store"; import { ClusterPieCharts } from "./cluster-pie-charts"; import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; -import { ClusterMetricsResourceType } from "../../../common/cluster-types"; -import { ClusterStore } from "../../../common/cluster-store/cluster-store"; +import { ClusterMetricsResourceType } from "../../../common/cluster/types"; +import { ClusterStore } from "../../../common/cluster/store"; import { eventStore } from "../+events/event.store"; import { withInjectables } from "@ogre-tools/injectable-react"; import kubeWatchApiInjectable from "../../kube-watch-api/kube-watch-api.injectable"; diff --git a/src/renderer/components/+namespaces/namespace-details.tsx b/src/renderer/components/+namespaces/namespace-details.tsx index 4d94e8990d..0853e62fcf 100644 --- a/src/renderer/components/+namespaces/namespace-details.tsx +++ b/src/renderer/components/+namespaces/namespace-details.tsx @@ -20,15 +20,14 @@ import { KubeObjectMeta } from "../kube-object-meta"; import { limitRangeStore } from "../+config-limit-ranges/limit-ranges.store"; import { ResourceMetrics } from "../resource-metrics"; import { PodCharts, podMetricTabs } from "../+workloads-pods/pod-charts"; -import { ClusterMetricsResourceType } from "../../../common/cluster-types"; +import { ClusterMetricsResourceType } from "../../../common/cluster/types"; import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; import { getDetailsUrl } from "../kube-detail-params"; import logger from "../../../common/logger"; import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; import type { KubeObject } from "../../../common/k8s-api/kube-object"; import { withInjectables } from "@ogre-tools/injectable-react"; -import kubeWatchApiInjectable - from "../../kube-watch-api/kube-watch-api.injectable"; +import kubeWatchApiInjectable from "../../kube-watch-api/kube-watch-api.injectable"; export interface NamespaceDetailsProps extends KubeObjectDetailsProps { } diff --git a/src/renderer/components/+network-ingresses/ingress-details.tsx b/src/renderer/components/+network-ingresses/ingress-details.tsx index bb088be389..5af88bedad 100644 --- a/src/renderer/components/+network-ingresses/ingress-details.tsx +++ b/src/renderer/components/+network-ingresses/ingress-details.tsx @@ -18,7 +18,7 @@ import { IngressCharts } from "./ingress-charts"; import { KubeObjectMeta } from "../kube-object-meta"; import { computeRuleDeclarations, getMetricsForIngress, type IIngressMetrics } from "../../../common/k8s-api/endpoints/ingress.api"; import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; -import { ClusterMetricsResourceType } from "../../../common/cluster-types"; +import { ClusterMetricsResourceType } from "../../../common/cluster/types"; import logger from "../../../common/logger"; export interface IngressDetailsProps extends KubeObjectDetailsProps { diff --git a/src/renderer/components/+nodes/details.tsx b/src/renderer/components/+nodes/details.tsx index 9fe1af6675..7e749193ba 100644 --- a/src/renderer/components/+nodes/details.tsx +++ b/src/renderer/components/+nodes/details.tsx @@ -21,7 +21,7 @@ import { makeObservable, observable, reaction } from "mobx"; import { PodDetailsList } from "../+workloads-pods/pod-details-list"; import { KubeObjectMeta } from "../kube-object-meta"; import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; -import { ClusterMetricsResourceType } from "../../../common/cluster-types"; +import { ClusterMetricsResourceType } from "../../../common/cluster/types"; import { NodeDetailsResources } from "./details-resources"; import { DrawerTitle } from "../drawer/drawer-title"; import type { Disposer } from "../../utils"; @@ -29,8 +29,7 @@ import logger from "../../../common/logger"; import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; import type { KubeObject } from "../../../common/k8s-api/kube-object"; import { withInjectables } from "@ogre-tools/injectable-react"; -import kubeWatchApiInjectable - from "../../kube-watch-api/kube-watch-api.injectable"; +import kubeWatchApiInjectable from "../../kube-watch-api/kube-watch-api.injectable"; export interface NodeDetailsProps extends KubeObjectDetailsProps { } diff --git a/src/renderer/components/+preferences/kubeconfig-syncs/get-all-entries.injectable.ts b/src/renderer/components/+preferences/kubeconfig-syncs/get-all-entries.injectable.ts new file mode 100644 index 0000000000..adbc284504 --- /dev/null +++ b/src/renderer/components/+preferences/kubeconfig-syncs/get-all-entries.injectable.ts @@ -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 getSyncEntryInjectable from "./get-entry.injectable"; +import type { SyncValue } from "./view"; + +export type GetAllSyncEntries = (filePaths: string[]) => Promise<[string, SyncValue][]>; + +const getAllSyncEntriesInjectable = getInjectable({ + id: "get-all-sync-entries", + instantiate: (di): GetAllSyncEntries => { + const getSyncEntry = di.inject(getSyncEntryInjectable); + + return (filePaths) => Promise.all(filePaths.map(filePath => getSyncEntry({ filePath }))); + }, +}); + +export default getAllSyncEntriesInjectable; diff --git a/src/renderer/components/+preferences/kubeconfig-syncs/get-entry.injectable.ts b/src/renderer/components/+preferences/kubeconfig-syncs/get-entry.injectable.ts new file mode 100644 index 0000000000..cc1f5bc135 --- /dev/null +++ b/src/renderer/components/+preferences/kubeconfig-syncs/get-entry.injectable.ts @@ -0,0 +1,44 @@ +/** + * 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 fsInjectable from "../../../../common/fs/fs.injectable"; +import type { KubeconfigSyncEntry } from "../../../../common/user-store"; +import kubeconfigSyncsPreferencesLoggerInjectable from "./logger.injectable"; +import type { SyncValue } from "./view"; + +export type GetSyncEntry = ({ filePath, ...data }: KubeconfigSyncEntry) => Promise<[string, SyncValue]>; + +const getSyncEntryInjectable = getInjectable({ + id: "get-sync-entry", + instantiate: (di): GetSyncEntry => { + const logger = di.inject(kubeconfigSyncsPreferencesLoggerInjectable); + const { stat } = di.inject(fsInjectable); + + return async ({ filePath, ...data }) => { + try { + // stat follows the stat(2) linux syscall spec, namely it follows symlinks + const stats = await stat(filePath); + + if (stats.isFile()) { + return [filePath, { info: { type: "file" }, data }]; + } + + if (stats.isDirectory()) { + return [filePath, { info: { type: "folder" }, data }]; + } + + logger.warn("unknown stat entry", { stats }); + + return [filePath, { info: { type: "unknown" }, data }]; + } catch (error) { + logger.warn("failed to stat entry", { error }); + + return [filePath, { info: { type: "unknown" }, data }]; + } + }; + }, +}); + +export default getSyncEntryInjectable; diff --git a/src/renderer/components/+preferences/kubeconfig-syncs/logger.injectable.ts b/src/renderer/components/+preferences/kubeconfig-syncs/logger.injectable.ts new file mode 100644 index 0000000000..ff063022e5 --- /dev/null +++ b/src/renderer/components/+preferences/kubeconfig-syncs/logger.injectable.ts @@ -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 kubeconfigSyncsPreferencesLoggerInjectable = getInjectable({ + id: "kubeconfig-syncs-preferences-logger", + instantiate: (di) => di.inject(childLoggerInjectable, { + prefix: "KUBECONFIG-SYNCS-PREFERENCES", + }), +}); + +export default kubeconfigSyncsPreferencesLoggerInjectable; diff --git a/src/renderer/components/+preferences/kubeconfig-syncs.tsx b/src/renderer/components/+preferences/kubeconfig-syncs/view.tsx similarity index 60% rename from src/renderer/components/+preferences/kubeconfig-syncs.tsx rename to src/renderer/components/+preferences/kubeconfig-syncs/view.tsx index a6a6fc1d10..11a2a053b6 100644 --- a/src/renderer/components/+preferences/kubeconfig-syncs.tsx +++ b/src/renderer/components/+preferences/kubeconfig-syncs/view.tsx @@ -3,71 +3,46 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { withInjectables } from "@ogre-tools/injectable-react"; -import fse from "fs-extra"; import { computed, makeObservable, observable, reaction } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; import React from "react"; -import { Notice } from "../+extensions/notice"; -import type { KubeconfigSyncEntry, KubeconfigSyncValue, UserStore } from "../../../common/user-store"; -import { iter, tuple } from "../../utils"; -import { SubTitle } from "../layout/sub-title"; -import { PathPicker } from "../path-picker/path-picker"; -import { Spinner } from "../spinner"; -import { RemovableItem } from "./removable-item"; -import userStoreInjectable from "../../../common/user-store/user-store.injectable"; -import isWindowsInjectable from "../../../common/vars/is-windows.injectable"; -import loggerInjectable from "../../../common/logger.injectable"; -import type { Logger } from "../../../common/logger"; +import { Notice } from "../../+extensions/notice"; +import type { KubeconfigSyncValue, UserStore } from "../../../../common/user-store"; +import { iter, tuple } from "../../../utils"; +import { SubTitle } from "../../layout/sub-title"; +import { PathPicker } from "../../path-picker/path-picker"; +import { Spinner } from "../../spinner"; +import { RemovableItem } from "../removable-item"; +import userStoreInjectable from "../../../../common/user-store/user-store.injectable"; +import isWindowsInjectable from "../../../../common/vars/is-windows.injectable"; +import type { GetSyncEntry } from "./get-entry.injectable"; +import type { GetAllSyncEntries } from "./get-all-entries.injectable"; +import getAllSyncEntriesInjectable from "./get-all-entries.injectable"; +import getSyncEntryInjectable from "./get-entry.injectable"; -interface SyncInfo { +export interface SyncInfo { type: "file" | "folder" | "unknown"; } -interface Entry extends Value { +interface SyncEntry extends SyncValue { filePath: string; } -interface Value { +export interface SyncValue { data: KubeconfigSyncValue; info: SyncInfo; } -async function getMapEntry({ filePath, ...data }: KubeconfigSyncEntry, logger: Logger): Promise<[string, Value]> { - try { - // stat follows the stat(2) linux syscall spec, namely it follows symlinks - const stats = await fse.stat(filePath); - - if (stats.isFile()) { - return [filePath, { info: { type: "file" }, data }]; - } - - if (stats.isDirectory()) { - return [filePath, { info: { type: "folder" }, data }]; - } - - logger.warn("[KubeconfigSyncs]: unknown stat entry", { stats }); - - return [filePath, { info: { type: "unknown" }, data }]; - } catch (error) { - logger.warn(`[KubeconfigSyncs]: failed to stat entry: ${error}`, { error }); - - return [filePath, { info: { type: "unknown" }, data }]; - } -} - -export async function getAllEntries(filePaths: string[], logger: Logger): Promise<[string, Value][]> { - return Promise.all(filePaths.map(filePath => getMapEntry({ filePath }, logger))); -} - interface Dependencies { userStore: UserStore; isWindows: boolean; - logger: Logger; + getSyncEntry: GetSyncEntry; + getAllSyncEntries: GetAllSyncEntries; } @observer class NonInjectedKubeconfigSyncs extends React.Component { - syncs = observable.map(); + syncs = observable.map(); @observable loaded = false; constructor(props: Dependencies) { @@ -79,7 +54,7 @@ class NonInjectedKubeconfigSyncs extends React.Component { const mapEntries = await Promise.all( iter.map( this.props.userStore.syncKubeconfigEntries, - ([filePath, ...value]) => getMapEntry({ filePath, ...value }, this.props.logger), + ([filePath, ...value]) => this.props.getSyncEntry({ filePath, ...value }), ), ); @@ -96,7 +71,7 @@ class NonInjectedKubeconfigSyncs extends React.Component { ]); } - @computed get syncsList(): Entry[] | undefined { + @computed get syncsList(): SyncEntry[] | undefined { if (!this.loaded) { return undefined; } @@ -105,10 +80,10 @@ class NonInjectedKubeconfigSyncs extends React.Component { } onPick = async (filePaths: string[]) => { - this.syncs.merge(await getAllEntries(filePaths, this.props.logger)); + this.syncs.merge(await this.props.getAllSyncEntries(filePaths)); }; - getIconName(entry: Entry) { + getIconName(entry: SyncEntry) { switch (entry.info.type) { case "file": return "description"; @@ -119,7 +94,7 @@ class NonInjectedKubeconfigSyncs extends React.Component { } } - renderEntry = (entry: Entry) => { + renderEntry = (entry: SyncEntry) => { return ( { } } -export const KubeconfigSyncs = withInjectables( - NonInjectedKubeconfigSyncs, - - { - getProps: (di) => ({ - userStore: di.inject(userStoreInjectable), - isWindows: di.inject(isWindowsInjectable), - logger: di.inject(loggerInjectable), - }), - }, -); +export const KubeconfigSyncs = withInjectables(NonInjectedKubeconfigSyncs, { + getProps: (di) => ({ + userStore: di.inject(userStoreInjectable), + isWindows: di.inject(isWindowsInjectable), + getAllSyncEntries: di.inject(getAllSyncEntriesInjectable), + getSyncEntry: di.inject(getSyncEntryInjectable), + }), +}); diff --git a/src/renderer/components/+preferences/kubernetes.tsx b/src/renderer/components/+preferences/kubernetes.tsx index aaaffab0aa..edbb162e8a 100644 --- a/src/renderer/components/+preferences/kubernetes.tsx +++ b/src/renderer/components/+preferences/kubernetes.tsx @@ -6,7 +6,7 @@ import { observer } from "mobx-react"; import React from "react"; import { HelmCharts } from "./helm-charts"; -import { KubeconfigSyncs } from "./kubeconfig-syncs"; +import { KubeconfigSyncs } from "./kubeconfig-syncs/view"; import { KubectlBinaries } from "./kubectl-binaries"; import { Preferences } from "./preferences"; diff --git a/src/renderer/components/+storage-volume-claims/volume-claim-details.tsx b/src/renderer/components/+storage-volume-claims/volume-claim-details.tsx index d5ca96b19b..7fdf5290e0 100644 --- a/src/renderer/components/+storage-volume-claims/volume-claim-details.tsx +++ b/src/renderer/components/+storage-volume-claims/volume-claim-details.tsx @@ -17,7 +17,7 @@ import { VolumeClaimDiskChart } from "./volume-claim-disk-chart"; import type { KubeObjectDetailsProps } from "../kube-object-details"; import { getMetricsForPvc, type IPvcMetrics, PersistentVolumeClaim } from "../../../common/k8s-api/endpoints"; import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; -import { ClusterMetricsResourceType } from "../../../common/cluster-types"; +import { ClusterMetricsResourceType } from "../../../common/cluster/types"; import { KubeObjectMeta } from "../kube-object-meta"; import { getDetailsUrl } from "../kube-detail-params"; import logger from "../../../common/logger"; diff --git a/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx b/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx index 92084e9312..bbccef10e5 100644 --- a/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx +++ b/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx @@ -22,14 +22,13 @@ import { makeObservable, observable, reaction } from "mobx"; import { PodDetailsList } from "../+workloads-pods/pod-details-list"; import { KubeObjectMeta } from "../kube-object-meta"; import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; -import { ClusterMetricsResourceType } from "../../../common/cluster-types"; +import { ClusterMetricsResourceType } from "../../../common/cluster/types"; import type { Disposer } from "../../utils"; import logger from "../../../common/logger"; import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; import type { KubeObject } from "../../../common/k8s-api/kube-object"; import { withInjectables } from "@ogre-tools/injectable-react"; -import kubeWatchApiInjectable - from "../../kube-watch-api/kube-watch-api.injectable"; +import kubeWatchApiInjectable from "../../kube-watch-api/kube-watch-api.injectable"; export interface DaemonSetDetailsProps extends KubeObjectDetailsProps { } diff --git a/src/renderer/components/+workloads-deployments/deployment-details.tsx b/src/renderer/components/+workloads-deployments/deployment-details.tsx index 2c143989c0..a16d3408c0 100644 --- a/src/renderer/components/+workloads-deployments/deployment-details.tsx +++ b/src/renderer/components/+workloads-deployments/deployment-details.tsx @@ -24,14 +24,13 @@ import { KubeObjectMeta } from "../kube-object-meta"; import { replicaSetStore } from "../+workloads-replicasets/replicasets.store"; import { DeploymentReplicaSets } from "./deployment-replicasets"; import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; -import { ClusterMetricsResourceType } from "../../../common/cluster-types"; +import { ClusterMetricsResourceType } from "../../../common/cluster/types"; import type { Disposer } from "../../utils"; import logger from "../../../common/logger"; import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; import type { KubeObject } from "../../../common/k8s-api/kube-object"; import { withInjectables } from "@ogre-tools/injectable-react"; -import kubeWatchApiInjectable - from "../../kube-watch-api/kube-watch-api.injectable"; +import kubeWatchApiInjectable from "../../kube-watch-api/kube-watch-api.injectable"; export interface DeploymentDetailsProps extends KubeObjectDetailsProps { } diff --git a/src/renderer/components/+workloads-jobs/job-details.tsx b/src/renderer/components/+workloads-jobs/job-details.tsx index c77af55cca..784fd1488e 100644 --- a/src/renderer/components/+workloads-jobs/job-details.tsx +++ b/src/renderer/components/+workloads-jobs/job-details.tsx @@ -22,7 +22,7 @@ import { PodDetailsList } from "../+workloads-pods/pod-details-list"; import { KubeObjectMeta } from "../kube-object-meta"; import { makeObservable, observable, reaction } from "mobx"; import { podMetricTabs, PodCharts } from "../+workloads-pods/pod-charts"; -import { ClusterMetricsResourceType } from "../../../common/cluster-types"; +import { ClusterMetricsResourceType } from "../../../common/cluster/types"; import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; import { ResourceMetrics } from "../resource-metrics"; import { getDetailsUrl } from "../kube-detail-params"; @@ -32,8 +32,7 @@ import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store" import type { KubeObject } from "../../../common/k8s-api/kube-object"; import type { Disposer } from "../../../common/utils"; import { withInjectables } from "@ogre-tools/injectable-react"; -import kubeWatchApiInjectable - from "../../kube-watch-api/kube-watch-api.injectable"; +import kubeWatchApiInjectable from "../../kube-watch-api/kube-watch-api.injectable"; export interface JobDetailsProps extends KubeObjectDetailsProps { } diff --git a/src/renderer/components/+workloads-pods/pod-details-container.tsx b/src/renderer/components/+workloads-pods/pod-details-container.tsx index d6af5ba59a..4659da1fc8 100644 --- a/src/renderer/components/+workloads-pods/pod-details-container.tsx +++ b/src/renderer/components/+workloads-pods/pod-details-container.tsx @@ -18,7 +18,7 @@ import type { IMetrics } from "../../../common/k8s-api/endpoints/metrics.api"; import { ContainerCharts } from "./container-charts"; import { LocaleDate } from "../locale-date"; import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; -import { ClusterMetricsResourceType } from "../../../common/cluster-types"; +import { ClusterMetricsResourceType } from "../../../common/cluster/types"; import type { PortForwardStore } from "../../port-forward"; import { disposeOnUnmount, observer } from "mobx-react"; import { withInjectables } from "@ogre-tools/injectable-react"; diff --git a/src/renderer/components/+workloads-pods/pod-details.tsx b/src/renderer/components/+workloads-pods/pod-details.tsx index 0eda7209fe..2b29e8826f 100644 --- a/src/renderer/components/+workloads-pods/pod-details.tsx +++ b/src/renderer/components/+workloads-pods/pod-details.tsx @@ -24,7 +24,7 @@ import { getItemMetrics } from "../../../common/k8s-api/endpoints/metrics.api"; import { PodCharts, podMetricTabs } from "./pod-charts"; import { KubeObjectMeta } from "../kube-object-meta"; import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; -import { ClusterMetricsResourceType } from "../../../common/cluster-types"; +import { ClusterMetricsResourceType } from "../../../common/cluster/types"; import { getDetailsUrl } from "../kube-detail-params"; import logger from "../../../common/logger"; import { PodVolumes } from "./details/volumes/view"; diff --git a/src/renderer/components/+workloads-replicasets/replicaset-details.tsx b/src/renderer/components/+workloads-replicasets/replicaset-details.tsx index 2ab2c4480f..0ea19d8cb8 100644 --- a/src/renderer/components/+workloads-replicasets/replicaset-details.tsx +++ b/src/renderer/components/+workloads-replicasets/replicaset-details.tsx @@ -21,14 +21,13 @@ import { PodCharts, podMetricTabs } from "../+workloads-pods/pod-charts"; import { PodDetailsList } from "../+workloads-pods/pod-details-list"; import { KubeObjectMeta } from "../kube-object-meta"; import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; -import { ClusterMetricsResourceType } from "../../../common/cluster-types"; +import { ClusterMetricsResourceType } from "../../../common/cluster/types"; import type { Disposer } from "../../utils"; import logger from "../../../common/logger"; import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; import type { KubeObject } from "../../../common/k8s-api/kube-object"; import { withInjectables } from "@ogre-tools/injectable-react"; -import kubeWatchApiInjectable - from "../../kube-watch-api/kube-watch-api.injectable"; +import kubeWatchApiInjectable from "../../kube-watch-api/kube-watch-api.injectable"; export interface ReplicaSetDetailsProps extends KubeObjectDetailsProps { } diff --git a/src/renderer/components/+workloads-statefulsets/statefulset-details.tsx b/src/renderer/components/+workloads-statefulsets/statefulset-details.tsx index e024973e2a..f1a0a805b1 100644 --- a/src/renderer/components/+workloads-statefulsets/statefulset-details.tsx +++ b/src/renderer/components/+workloads-statefulsets/statefulset-details.tsx @@ -22,14 +22,13 @@ import { PodCharts, podMetricTabs } from "../+workloads-pods/pod-charts"; import { PodDetailsList } from "../+workloads-pods/pod-details-list"; import { KubeObjectMeta } from "../kube-object-meta"; import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; -import { ClusterMetricsResourceType } from "../../../common/cluster-types"; +import { ClusterMetricsResourceType } from "../../../common/cluster/types"; import type { Disposer } from "../../utils"; import logger from "../../../common/logger"; import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; import type { KubeObject } from "../../../common/k8s-api/kube-object"; import { withInjectables } from "@ogre-tools/injectable-react"; -import kubeWatchApiInjectable - from "../../kube-watch-api/kube-watch-api.injectable"; +import kubeWatchApiInjectable from "../../kube-watch-api/kube-watch-api.injectable"; export interface StatefulSetDetailsProps extends KubeObjectDetailsProps { } diff --git a/src/renderer/components/cluster-manager/cluster-status.tsx b/src/renderer/components/cluster-manager/cluster-status.tsx index 25a9b7408b..03b87a9d47 100644 --- a/src/renderer/components/cluster-manager/cluster-status.tsx +++ b/src/renderer/components/cluster-manager/cluster-status.tsx @@ -15,7 +15,7 @@ import { cssNames } from "../../utils"; import { Button } from "../button"; import { Icon } from "../icon"; import { Spinner } from "../spinner"; -import type { KubeAuthUpdate } from "../../../common/cluster-types"; +import type { KubeAuthUpdate } from "../../../common/cluster/types"; import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; import { requestClusterActivation } from "../../ipc"; import type { NavigateToEntitySettings } from "../../../common/front-end-routing/routes/entity-settings/navigate-to-entity-settings.injectable"; diff --git a/src/renderer/components/cluster-manager/cluster-view.tsx b/src/renderer/components/cluster-manager/cluster-view.tsx index b4f36d5529..3cf4f660bc 100644 --- a/src/renderer/components/cluster-manager/cluster-view.tsx +++ b/src/renderer/components/cluster-manager/cluster-view.tsx @@ -11,7 +11,7 @@ import { disposeOnUnmount, observer } from "mobx-react"; import { ClusterStatus } from "./cluster-status"; import { ClusterFrameHandler } from "./lens-views"; import type { Cluster } from "../../../common/cluster/cluster"; -import { ClusterStore } from "../../../common/cluster-store/cluster-store"; +import { ClusterStore } from "../../../common/cluster/store"; import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; import { requestClusterActivation } from "../../ipc"; import { withInjectables } from "@ogre-tools/injectable-react"; diff --git a/src/renderer/components/cluster-manager/lens-views.ts b/src/renderer/components/cluster-manager/lens-views.ts index a8fcd12257..5c46087004 100644 --- a/src/renderer/components/cluster-manager/lens-views.ts +++ b/src/renderer/components/cluster-manager/lens-views.ts @@ -7,8 +7,8 @@ import type { IReactionDisposer } from "mobx"; import { action, makeObservable, observable, when } from "mobx"; import logger from "../../../main/logger"; import { clusterVisibilityHandler } from "../../../common/ipc/cluster"; -import { ClusterStore } from "../../../common/cluster-store/cluster-store"; -import type { ClusterId } from "../../../common/cluster-types"; +import { ClusterStore } from "../../../common/cluster/store"; +import type { ClusterId } from "../../../common/cluster/types"; import { getClusterFrameUrl, Singleton } from "../../utils"; import { ipcRenderer } from "electron"; diff --git a/src/renderer/components/cluster-settings/cluster-settings.tsx b/src/renderer/components/cluster-settings/cluster-settings.tsx index e89a600095..b2080d1660 100644 --- a/src/renderer/components/cluster-settings/cluster-settings.tsx +++ b/src/renderer/components/cluster-settings/cluster-settings.tsx @@ -5,7 +5,7 @@ import React from "react"; import type { KubernetesCluster } from "../../../common/catalog-entities"; -import { ClusterStore } from "../../../common/cluster-store/cluster-store"; +import { ClusterStore } from "../../../common/cluster/store"; import type { EntitySettingViewProps } from "../../../extensions/registries"; import type { CatalogEntity } from "../../api/catalog-entity"; import * as components from "./components"; diff --git a/src/renderer/components/cluster-settings/components/cluster-metrics-setting.tsx b/src/renderer/components/cluster-settings/components/cluster-metrics-setting.tsx index 58c6e57388..37541e98fa 100644 --- a/src/renderer/components/cluster-settings/components/cluster-metrics-setting.tsx +++ b/src/renderer/components/cluster-settings/components/cluster-metrics-setting.tsx @@ -12,7 +12,7 @@ import { Button } from "../../button/button"; import { SubTitle } from "../../layout/sub-title"; import type { Cluster } from "../../../../common/cluster/cluster"; import { observable, reaction, makeObservable } from "mobx"; -import { ClusterMetricsResourceType } from "../../../../common/cluster-types"; +import { ClusterMetricsResourceType } from "../../../../common/cluster/types"; export interface ClusterMetricsSettingProps { cluster: Cluster; diff --git a/src/renderer/components/cluster-settings/components/cluster-node-shell-setting.tsx b/src/renderer/components/cluster-settings/components/cluster-node-shell-setting.tsx index f21b725226..424f4c485f 100644 --- a/src/renderer/components/cluster-settings/components/cluster-node-shell-setting.tsx +++ b/src/renderer/components/cluster-settings/components/cluster-node-shell-setting.tsx @@ -10,7 +10,7 @@ import React from "react"; import { Input } from "../../input/input"; import { observer } from "mobx-react"; import { Icon } from "../../icon/icon"; -import { initialNodeShellImage } from "../../../../common/cluster-types"; +import { initialNodeShellImage } from "../../../../common/cluster/types"; export interface ClusterNodeShellSettingProps { cluster: Cluster; diff --git a/src/renderer/components/command-palette/command-container.tsx b/src/renderer/components/command-palette/command-container.tsx index c6a390b6b7..3673fe8131 100644 --- a/src/renderer/components/command-palette/command-container.tsx +++ b/src/renderer/components/command-palette/command-container.tsx @@ -9,21 +9,21 @@ import { disposeOnUnmount, observer } from "mobx-react"; import React from "react"; import { Dialog } from "../dialog"; import { CommandDialog } from "./command-dialog"; -import type { ClusterId } from "../../../common/cluster-types"; +import type { ClusterId } from "../../../common/cluster/types"; import type { CommandOverlay } from "./command-overlay.injectable"; import commandOverlayInjectable from "./command-overlay.injectable"; import { isMac } from "../../../common/vars"; import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; import { broadcastMessage, ipcRendererOn } from "../../../common/ipc"; -import type { Disposer } from "../../utils"; import { withInjectables } from "@ogre-tools/injectable-react"; +import type { AddWindowEventListener } from "../../window/event-listener.injectable"; import windowAddEventListenerInjectable from "../../window/event-listener.injectable"; -import hostedClusterInjectable from "../../../common/cluster-store/hosted-cluster.injectable"; +import hostedClusterInjectable from "../../../common/cluster/hosted.injectable"; import type { IComputedValue } from "mobx"; import matchedClusterIdInjectable from "../../navigation/matched-cluster-id.injectable"; interface Dependencies { - addWindowEventListener: (type: K, listener: (this: Window, ev: WindowEventMap[K]) => any, options?: boolean | AddEventListenerOptions) => Disposer; + addWindowEventListener: AddWindowEventListener; commandOverlay: CommandOverlay; clusterId?: ClusterId; matchedClusterId: IComputedValue; diff --git a/src/renderer/components/delete-cluster-dialog/__tests__/delete-cluster-dialog.test.tsx b/src/renderer/components/delete-cluster-dialog/__tests__/delete-cluster-dialog.test.tsx index 1fe1d6e339..577b3578a6 100644 --- a/src/renderer/components/delete-cluster-dialog/__tests__/delete-cluster-dialog.test.tsx +++ b/src/renderer/components/delete-cluster-dialog/__tests__/delete-cluster-dialog.test.tsx @@ -8,11 +8,9 @@ import { fireEvent } from "@testing-library/react"; import mockFs from "mock-fs"; import React from "react"; import * as selectEvent from "react-select-event"; - import type { Cluster } from "../../../../common/cluster/cluster"; import { DeleteClusterDialog } from "../delete-cluster-dialog"; - -import type { ClusterModel } from "../../../../common/cluster-types"; +import type { ClusterModel } from "../../../../common/cluster/types"; import { getDisForUnitTesting } from "../../../../test-utils/get-dis-for-unit-testing"; import { createClusterInjectionToken } from "../../../../common/cluster/create-cluster-injection-token"; import createContextHandlerInjectable from "../../../../main/context-handler/create-context-handler.injectable"; diff --git a/src/renderer/components/test-utils/get-application-builder.tsx b/src/renderer/components/test-utils/get-application-builder.tsx index deaa51eb0b..042dfc6835 100644 --- a/src/renderer/components/test-utils/get-application-builder.tsx +++ b/src/renderer/components/test-utils/get-application-builder.tsx @@ -14,7 +14,7 @@ import React from "react"; import { Router } from "react-router"; import { Observer } from "mobx-react"; import subscribeStoresInjectable from "../../kube-watch-api/subscribe-stores.injectable"; -import allowedResourcesInjectable from "../../../common/cluster-store/allowed-resources.injectable"; +import allowedResourcesInjectable from "../../../common/cluster/allowed-resources.injectable"; import type { RenderResult } from "@testing-library/react"; import { fireEvent } from "@testing-library/react"; import type { KubeResource } from "../../../common/rbac"; @@ -22,8 +22,8 @@ import directoryForLensLocalStorageInjectable from "../../../common/directory-fo import { Sidebar } from "../layout/sidebar"; import { getDisForUnitTesting } from "../../../test-utils/get-dis-for-unit-testing"; import type { DiContainer } from "@ogre-tools/injectable"; -import clusterStoreInjectable from "../../../common/cluster-store/cluster-store.injectable"; -import type { ClusterStore } from "../../../common/cluster-store/cluster-store"; +import clusterStoreInjectable from "../../../common/cluster/store.injectable"; +import type { ClusterStore } from "../../../common/cluster/store"; import mainExtensionsInjectable from "../../../extensions/main-extensions.injectable"; import type { LensMainExtension } from "../../../extensions/lens-main-extension"; import currentRouteComponentInjectable from "../../routes/current-route-component.injectable"; @@ -33,29 +33,37 @@ import preferenceNavigationItemsInjectable from "../+preferences/preferences-nav import navigateToPreferencesInjectable from "../../../common/front-end-routing/routes/preferences/navigate-to-preferences.injectable"; import type { MenuItemOpts } from "../../../main/menu/application-menu-items.injectable"; import applicationMenuItemsInjectable from "../../../main/menu/application-menu-items.injectable"; +import exitAppInjectable from "../../../main/utils/exit-app.injectable"; type Callback = (dis: DiContainers) => void | Promise; +export interface ApplicationMenu { + click: (path: string) => void; +} + +export interface ApplicationPreferences { + close: () => void; + navigate: () => void; + navigation: { + click: (id: string) => void; + }; +} + +export interface ApplicationActions { + exit: () => void; +} + export interface ApplicationBuilder { - dis: DiContainers; + readonly dis: DiContainers; setEnvironmentToClusterFrame: () => ApplicationBuilder; addExtensions: (...extensions: LensRendererExtension[]) => Promise; allowKubeResource: (resourceName: KubeResource) => ApplicationBuilder; beforeSetups: (callback: Callback) => ApplicationBuilder; beforeRender: (callback: Callback) => ApplicationBuilder; render: () => Promise; - - applicationMenu: { - click: (path: string) => void; - }; - - preferences: { - close: () => void; - navigate: () => void; - navigation: { - click: (id: string) => void; - }; - }; + readonly applicationMenu: ApplicationMenu; + readonly preferences: ApplicationPreferences; + readonly actions: ApplicationActions; } interface DiContainers { @@ -106,27 +114,23 @@ export const getApplicationBuilder = () => { }, }; + const exit = jest.fn(); + let environment = environments.application; - rendererDi.override( - currentlyInClusterFrameInjectable, - () => environment === environments.clusterFrame, - ); - - rendererDi.override(rendererExtensionsInjectable, () => - computed(() => extensionsState), - ); - - mainDi.override(mainExtensionsInjectable, () => - computed((): LensMainExtension[] => []), - ); + rendererDi.override( currentlyInClusterFrameInjectable, () => environment === environments.clusterFrame); + rendererDi.override(rendererExtensionsInjectable, () => computed(() => extensionsState)); + mainDi.override(mainExtensionsInjectable, () => computed((): LensMainExtension[] => [])); + mainDi.override(exitAppInjectable, () => exit); let allowedResourcesState: IObservableArray; let rendered: RenderResult; const builder: ApplicationBuilder = { dis, - + actions: { + exit, + }, applicationMenu: { click: (path: string) => { const applicationMenuItems = mainDi.inject( diff --git a/src/renderer/create-cluster/create-cluster.injectable.ts b/src/renderer/create-cluster/create-cluster.injectable.ts index a1c244568c..8a4422f381 100644 --- a/src/renderer/create-cluster/create-cluster.injectable.ts +++ b/src/renderer/create-cluster/create-cluster.injectable.ts @@ -14,9 +14,9 @@ const createClusterInjectable = getInjectable({ instantiate: (di) => { const dependencies: ClusterDependencies = { directoryForKubeConfigs: di.inject(directoryForKubeConfigsInjectable), - createKubeconfigManager: () => { throw new Error("Tried to access back-end feature in front-end."); }, - createKubectl: () => { throw new Error("Tried to access back-end feature in front-end.");}, - createContextHandler: () => { throw new Error("Tried to access back-end feature in front-end."); }, + createKubeconfigManager: () => undefined, + createContextHandler: () => undefined, + createKubectl: () => { throw new Error("Tried to access back-end feature in front-end."); }, createAuthorizationReview: () => { throw new Error("Tried to access back-end feature in front-end."); }, createListNamespaces: () => { throw new Error("Tried to access back-end feature in front-end."); }, }; diff --git a/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.injectable.ts b/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.injectable.ts index cc1140ca89..19b5730e3e 100644 --- a/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.injectable.ts +++ b/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.injectable.ts @@ -7,7 +7,7 @@ import { initClusterFrame } from "./init-cluster-frame"; import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable"; import catalogEntityRegistryInjectable from "../../../api/catalog-entity-registry/catalog-entity-registry.injectable"; import frameRoutingIdInjectable from "./frame-routing-id/frame-routing-id.injectable"; -import hostedClusterInjectable from "../../../../common/cluster-store/hosted-cluster.injectable"; +import hostedClusterInjectable from "../../../../common/cluster/hosted.injectable"; import appEventBusInjectable from "../../../../common/app-event-bus/app-event-bus.injectable"; import clusterFrameContextInjectable from "../../../cluster-frame-context/cluster-frame-context.injectable"; diff --git a/src/renderer/frames/root-frame/init-root-frame/init-root-frame.injectable.ts b/src/renderer/frames/root-frame/init-root-frame/init-root-frame.injectable.ts index 03ab72ddb6..5affff3d5b 100644 --- a/src/renderer/frames/root-frame/init-root-frame/init-root-frame.injectable.ts +++ b/src/renderer/frames/root-frame/init-root-frame/init-root-frame.injectable.ts @@ -6,35 +6,22 @@ import { getInjectable } from "@ogre-tools/injectable"; import { initRootFrame } from "./init-root-frame"; import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable"; import ipcRendererInjectable from "../../../app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable"; -import bindProtocolAddRouteHandlersInjectable from "../../../protocol-handler/bind-protocol-add-route-handlers/bind-protocol-add-route-handlers.injectable"; -import lensProtocolRouterRendererInjectable from "../../../protocol-handler/lens-protocol-router-renderer/lens-protocol-router-renderer.injectable"; +import bindProtocolAddRouteHandlersInjectable from "../../../protocol-handler/bind-protocol-add-route-handlers.injectable"; +import lensProtocolRouterRendererInjectable from "../../../protocol-handler/router.injectable"; import catalogEntityRegistryInjectable from "../../../api/catalog-entity-registry/catalog-entity-registry.injectable"; import registerIpcListenersInjectable from "../../../ipc/register-ipc-listeners.injectable"; const initRootFrameInjectable = getInjectable({ id: "init-root-frame", - instantiate: (di) => { - const extensionLoader = di.inject(extensionLoaderInjectable); - - return initRootFrame({ - loadExtensions: extensionLoader.loadOnClusterManagerRenderer, - - registerIpcListeners: di.inject(registerIpcListenersInjectable), - - ipcRenderer: di.inject(ipcRendererInjectable), - - bindProtocolAddRouteHandlers: di.inject( - bindProtocolAddRouteHandlersInjectable, - ), - - lensProtocolRouterRenderer: di.inject( - lensProtocolRouterRendererInjectable, - ), - - catalogEntityRegistry: di.inject(catalogEntityRegistryInjectable), - }); - }, + instantiate: (di) => initRootFrame({ + loadExtensions: di.inject(extensionLoaderInjectable).loadOnClusterManagerRenderer, + registerIpcListeners: di.inject(registerIpcListenersInjectable), + ipcRenderer: di.inject(ipcRendererInjectable), + bindProtocolAddRouteHandlers: di.inject(bindProtocolAddRouteHandlersInjectable), + lensProtocolRouterRenderer: di.inject(lensProtocolRouterRendererInjectable), + catalogEntityRegistry: di.inject(catalogEntityRegistryInjectable), + }), }); export default initRootFrameInjectable; diff --git a/src/renderer/getDiForUnitTesting.tsx b/src/renderer/getDiForUnitTesting.tsx index 6a752dfd85..9cf8526f61 100644 --- a/src/renderer/getDiForUnitTesting.tsx +++ b/src/renderer/getDiForUnitTesting.tsx @@ -8,7 +8,6 @@ import { memoize, noop } from "lodash/fp"; import { createContainer } from "@ogre-tools/injectable"; import { Environments, setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; import getValueFromRegisteredChannelInjectable from "./app-paths/get-value-from-registered-channel/get-value-from-registered-channel.injectable"; -import loggerInjectable from "../common/logger.injectable"; import { overrideFsWithFakes } from "../test-utils/override-fs-with-fakes"; import observableHistoryInjectable from "./navigation/observable-history.injectable"; import { searchParamsOptions } from "./navigation"; @@ -20,8 +19,8 @@ import extensionsStoreInjectable from "../extensions/extensions-store/extensions import type { ExtensionsStore } from "../extensions/extensions-store/extensions-store"; import fileSystemProvisionerStoreInjectable from "../extensions/extension-loader/create-extension-instance/file-system-provisioner-store/file-system-provisioner-store.injectable"; import type { FileSystemProvisionerStore } from "../extensions/extension-loader/create-extension-instance/file-system-provisioner-store/file-system-provisioner-store"; -import clusterStoreInjectable from "../common/cluster-store/cluster-store.injectable"; -import type { ClusterStore } from "../common/cluster-store/cluster-store"; +import clusterStoreInjectable from "../common/cluster/store.injectable"; +import type { ClusterStore } from "../common/cluster/store"; import type { Cluster } from "../common/cluster/cluster"; import userStoreInjectable from "../common/user-store/user-store.injectable"; import type { UserStore } from "../common/user-store"; @@ -33,6 +32,7 @@ import { getAbsolutePathFake } from "../common/test-utils/get-absolute-path-fake import joinPathsInjectable from "../common/path/join-paths.injectable"; import { joinPathsFake } from "../common/test-utils/join-paths-fake"; import hotbarStoreInjectable from "../common/hotbar-store.injectable"; +import loggerInjectable from "./logger/logger.injectable"; export const getDiForUnitTesting = ( { doGeneralOverrides } = { doGeneralOverrides: false }, @@ -89,7 +89,7 @@ export const getDiForUnitTesting = ( di.override(loggerInjectable, () => ({ warn: noop, debug: noop, - error: (message: string, ...args: any) => console.error(message, ...args), + error: noop, info: noop, })); } diff --git a/src/renderer/initializers/add-sync-entries.injectable.tsx b/src/renderer/initializers/add-sync-entries.injectable.tsx index 418759bfe7..161977a1c1 100644 --- a/src/renderer/initializers/add-sync-entries.injectable.tsx +++ b/src/renderer/initializers/add-sync-entries.injectable.tsx @@ -2,13 +2,12 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { getAllEntries } from "../components/+preferences/kubeconfig-syncs"; import { Notifications } from "../components/notifications"; import { getInjectable } from "@ogre-tools/injectable"; import userStoreInjectable from "../../common/user-store/user-store.injectable"; import React from "react"; import navigateToKubernetesPreferencesInjectable from "../../common/front-end-routing/routes/preferences/kubernetes/navigate-to-kubernetes-preferences.injectable"; -import loggerInjectable from "../../common/logger.injectable"; +import getAllSyncEntriesInjectable from "../components/+preferences/kubeconfig-syncs/get-all-entries.injectable"; const addSyncEntriesInjectable = getInjectable({ id: "add-sync-entries", @@ -16,12 +15,10 @@ const addSyncEntriesInjectable = getInjectable({ instantiate: (di) => { const userStore = di.inject(userStoreInjectable); const navigateToKubernetesPreferences = di.inject(navigateToKubernetesPreferencesInjectable); - const logger = di.inject(loggerInjectable); + const getAllSyncEntries = di.inject(getAllSyncEntriesInjectable); return async (filePaths: string[]) => { - userStore.syncKubeconfigEntries.merge( - await getAllEntries(filePaths, logger), - ); + userStore.syncKubeconfigEntries.merge(await getAllSyncEntries(filePaths)); Notifications.ok(
diff --git a/src/renderer/initializers/catalog.tsx b/src/renderer/initializers/catalog.tsx index f287aa8360..469b1ce6c7 100644 --- a/src/renderer/initializers/catalog.tsx +++ b/src/renderer/initializers/catalog.tsx @@ -6,7 +6,7 @@ import React from "react"; import fs from "fs"; import "../../common/catalog-entities/kubernetes-cluster"; -import { ClusterStore } from "../../common/cluster-store/cluster-store"; +import { ClusterStore } from "../../common/cluster/store"; import { catalogCategoryRegistry } from "../api/catalog-category-registry"; import { WeblinkAddCommand } from "../components/catalog-entities/weblink-add-command"; import { loadConfigFromString } from "../../common/kube-helpers"; diff --git a/src/renderer/ipc-channel-listeners/register-ipc-channel-listeners.injectable.ts b/src/renderer/ipc-channel-listeners/register-ipc-channel-listeners.injectable.ts index 9ce36a787b..0cc0f44475 100644 --- a/src/renderer/ipc-channel-listeners/register-ipc-channel-listeners.injectable.ts +++ b/src/renderer/ipc-channel-listeners/register-ipc-channel-listeners.injectable.ts @@ -5,20 +5,16 @@ import { getInjectable } from "@ogre-tools/injectable"; import { noop } from "lodash/fp"; import { ipcChannelListenerInjectionToken } from "./ipc-channel-listener-injection-token"; -import registerIpcChannelListenerInjectable - from "../app-paths/get-value-from-registered-channel/register-ipc-channel-listener.injectable"; +import registerIpcChannelListenerInjectable from "../app-paths/get-value-from-registered-channel/register-ipc-channel-listener.injectable"; const registerIpcChannelListenersInjectable = getInjectable({ id: "register-ipc-channel-listeners", setup: async di => { const registerIpcChannelListener = await di.inject(registerIpcChannelListenerInjectable); - const listeners = await di.injectMany(ipcChannelListenerInjectionToken); - listeners.forEach(listener => { - registerIpcChannelListener(listener); - }); + listeners.forEach(registerIpcChannelListener); }, instantiate: () => noop, diff --git a/src/renderer/ipc/index.ts b/src/renderer/ipc/index.ts index 5e204ef98b..5c447b6ed9 100644 --- a/src/renderer/ipc/index.ts +++ b/src/renderer/ipc/index.ts @@ -6,7 +6,7 @@ import type { OpenDialogOptions } from "electron"; import { ipcRenderer } from "electron"; import { clusterActivateHandler, clusterClearDeletingHandler, clusterDeleteHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterSetDeletingHandler, clusterSetFrameIdHandler, clusterStates } from "../../common/ipc/cluster"; -import type { ClusterId, ClusterState } from "../../common/cluster-types"; +import type { ClusterId, ClusterState } from "../../common/cluster/types"; import { windowActionHandleChannel, windowLocationChangedChannel, windowOpenAppMenuAsContextMenuChannel, type WindowAction } from "../../common/ipc/window"; import { openFilePickingDialogChannel } from "../../common/ipc/dialog"; import { extensionDiscoveryStateChannel, extensionLoaderFromMainChannel } from "../../common/ipc/extension-handling"; diff --git a/src/renderer/ipc/list-namespaces-forbidden-handler.injectable.tsx b/src/renderer/ipc/list-namespaces-forbidden-handler.injectable.tsx index 43b0eb8701..13166f3cb5 100644 --- a/src/renderer/ipc/list-namespaces-forbidden-handler.injectable.tsx +++ b/src/renderer/ipc/list-namespaces-forbidden-handler.injectable.tsx @@ -6,7 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable"; import navigateToEntitySettingsInjectable from "../../common/front-end-routing/routes/entity-settings/navigate-to-entity-settings.injectable"; import type { ListNamespaceForbiddenArgs } from "../../common/ipc/cluster"; import { Notifications, notificationsStore } from "../components/notifications"; -import { ClusterStore } from "../../common/cluster-store/cluster-store"; +import { ClusterStore } from "../../common/cluster/store"; import { Button } from "../components/button"; import type { IpcRendererEvent } from "electron"; import React from "react"; diff --git a/src/common/logger.injectable.ts b/src/renderer/logger/logger.injectable.ts similarity index 66% rename from src/common/logger.injectable.ts rename to src/renderer/logger/logger.injectable.ts index 55e2f26b2a..a0f53713f0 100644 --- a/src/common/logger.injectable.ts +++ b/src/renderer/logger/logger.injectable.ts @@ -3,12 +3,12 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import type { Logger } from "./logger"; -import logger from "./logger"; +import { baseLoggerInjectionToken } from "../../common/logger/logger.token"; const loggerInjectable = getInjectable({ id: "logger", - instantiate: (): Logger => logger, + instantiate: () => console, + injectionToken: baseLoggerInjectionToken, }); export default loggerInjectable; diff --git a/src/renderer/protocol-handler/bind-protocol-add-route-handlers.injectable.tsx b/src/renderer/protocol-handler/bind-protocol-add-route-handlers.injectable.tsx new file mode 100644 index 0000000000..61dee46e7d --- /dev/null +++ b/src/renderer/protocol-handler/bind-protocol-add-route-handlers.injectable.tsx @@ -0,0 +1,157 @@ +/** + * 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 attemptInstallByInfoInjectable from "../components/+extensions/attempt-install-by-info.injectable"; +import lensProtocolRouterRendererInjectable from "./router.injectable"; +import navigateToCatalogInjectable from "../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable"; +import navigateToAddClusterInjectable from "../../common/front-end-routing/routes/add-cluster/navigate-to-add-cluster.injectable"; +import navigateToExtensionsInjectable from "../../common/front-end-routing/routes/extensions/navigate-to-extensions.injectable"; +import navigateToEntitySettingsInjectable from "../../common/front-end-routing/routes/entity-settings/navigate-to-entity-settings.injectable"; +import navigateToClusterViewInjectable from "../../common/front-end-routing/routes/cluster-view/navigate-to-cluster-view.injectable"; +import navigateToPreferenceTabIdInjectable from "./navigate-to-preference-tab-id.injectable"; +import React from "react"; +import type { LensProtocolRouterRenderer } from "./router"; +import { catalogEntityRegistry } from "../api/catalog-entity-registry"; +import { ClusterStore } from "../../common/cluster/store"; +import { EXTENSION_NAME_MATCH, EXTENSION_PUBLISHER_MATCH, LensProtocolRouter } from "../../common/protocol-handler"; +import { Notifications } from "../components/notifications"; +import type { NavigateToCatalog } from "../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable"; +import type { NavigateToEntitySettings } from "../../common/front-end-routing/routes/entity-settings/navigate-to-entity-settings.injectable"; +import type { NavigateToClusterView } from "../../common/front-end-routing/routes/cluster-view/navigate-to-cluster-view.injectable"; +import type { AttemptInstallByInfo } from "../components/+extensions/attempt-install-by-info.injectable"; + +interface Dependencies { + attemptInstallByInfo: AttemptInstallByInfo; + lensProtocolRouterRenderer: LensProtocolRouterRenderer; + navigateToCatalog: NavigateToCatalog; + navigateToAddCluster: () => void; + navigateToExtensions: () => void; + navigateToEntitySettings: NavigateToEntitySettings; + navigateToClusterView: NavigateToClusterView; + navigateToPreferenceTabId: (tabId: string) => void; +} + +const bindProtocolAddRouteHandlers = ({ + attemptInstallByInfo, + lensProtocolRouterRenderer, + navigateToCatalog, + navigateToAddCluster, + navigateToExtensions, + navigateToEntitySettings, + navigateToClusterView, + navigateToPreferenceTabId, +}: Dependencies) => () => { + lensProtocolRouterRenderer + .addInternalHandler("/preferences", ({ search: { highlight: tabId }}) => { + navigateToPreferenceTabId(tabId); + }) + .addInternalHandler("/", ({ tail }) => { + if (tail) { + Notifications.shortInfo( +

+ Unknown Action for lens://app/{tail}. Are you on the + latest version? +

, + ); + } + + navigateToCatalog(); + }) + .addInternalHandler("/landing", () => { + navigateToCatalog(); + }) + .addInternalHandler( + "/landing/view/:group/:kind", + ({ pathname: { group, kind }}) => { + navigateToCatalog({ group, kind }); + }, + ) + .addInternalHandler("/cluster", () => { + navigateToAddCluster(); + }) + .addInternalHandler( + "/entity/:entityId/settings", + ({ pathname: { entityId }}) => { + const entity = catalogEntityRegistry.getById(entityId); + + if (entity) { + navigateToEntitySettings(entityId); + } else { + Notifications.shortInfo( +

+ Unknown catalog entity {entityId}. +

, + ); + } + }, + ) + // Handlers below are deprecated and only kept for backward compact purposes + .addInternalHandler( + "/cluster/:clusterId", + ({ pathname: { clusterId }}) => { + const cluster = ClusterStore.getInstance().getById(clusterId); + + if (cluster) { + navigateToClusterView(clusterId); + } else { + Notifications.shortInfo( +

+ Unknown catalog entity {clusterId}. +

, + ); + } + }, + ) + .addInternalHandler( + "/cluster/:clusterId/settings", + ({ pathname: { clusterId }}) => { + const cluster = ClusterStore.getInstance().getById(clusterId); + + if (cluster) { + navigateToEntitySettings(clusterId); + } else { + Notifications.shortInfo( +

+ Unknown catalog entity {clusterId}. +

, + ); + } + }, + ) + .addInternalHandler("/extensions", () => { + navigateToExtensions(); + }) + .addInternalHandler( + `/extensions/install${LensProtocolRouter.ExtensionUrlSchema}`, + ({ pathname, search: { version }}) => { + const name = [ + pathname[EXTENSION_PUBLISHER_MATCH], + pathname[EXTENSION_NAME_MATCH], + ] + .filter(Boolean) + .join("/"); + + navigateToExtensions(); + attemptInstallByInfo({ name, version, requireConfirmation: true }); + }, + ); +}; + +const bindProtocolAddRouteHandlersInjectable = getInjectable({ + id: "bind-protocol-add-route-handlers", + + instantiate: (di) => bindProtocolAddRouteHandlers({ + attemptInstallByInfo: di.inject(attemptInstallByInfoInjectable), + lensProtocolRouterRenderer: di.inject(lensProtocolRouterRendererInjectable), + navigateToCatalog: di.inject(navigateToCatalogInjectable), + navigateToAddCluster: di.inject(navigateToAddClusterInjectable), + navigateToExtensions: di.inject(navigateToExtensionsInjectable), + navigateToEntitySettings: di.inject(navigateToEntitySettingsInjectable), + navigateToClusterView: di.inject(navigateToClusterViewInjectable), + navigateToPreferenceTabId: di.inject(navigateToPreferenceTabIdInjectable), + }), +}); + +export default bindProtocolAddRouteHandlersInjectable; diff --git a/src/renderer/protocol-handler/bind-protocol-add-route-handlers/bind-protocol-add-route-handlers.injectable.ts b/src/renderer/protocol-handler/bind-protocol-add-route-handlers/bind-protocol-add-route-handlers.injectable.ts deleted file mode 100644 index 2aa92a8b22..0000000000 --- a/src/renderer/protocol-handler/bind-protocol-add-route-handlers/bind-protocol-add-route-handlers.injectable.ts +++ /dev/null @@ -1,36 +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 attemptInstallByInfoInjectable from "../../components/+extensions/attempt-install-by-info.injectable"; -import { bindProtocolAddRouteHandlers } from "./bind-protocol-add-route-handlers"; -import lensProtocolRouterRendererInjectable from "../lens-protocol-router-renderer/lens-protocol-router-renderer.injectable"; -import navigateToCatalogInjectable from "../../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable"; -import navigateToAddClusterInjectable from "../../../common/front-end-routing/routes/add-cluster/navigate-to-add-cluster.injectable"; -import navigateToExtensionsInjectable from "../../../common/front-end-routing/routes/extensions/navigate-to-extensions.injectable"; -import navigateToEntitySettingsInjectable from "../../../common/front-end-routing/routes/entity-settings/navigate-to-entity-settings.injectable"; -import navigateToClusterViewInjectable from "../../../common/front-end-routing/routes/cluster-view/navigate-to-cluster-view.injectable"; -import navigateToPreferenceTabIdInjectable from "./navigate-to-preference-tab-id.injectable"; - -const bindProtocolAddRouteHandlersInjectable = getInjectable({ - id: "bind-protocol-add-route-handlers", - - instantiate: (di) => - bindProtocolAddRouteHandlers({ - attemptInstallByInfo: di.inject(attemptInstallByInfoInjectable), - - lensProtocolRouterRenderer: di.inject( - lensProtocolRouterRendererInjectable, - ), - - navigateToCatalog: di.inject(navigateToCatalogInjectable), - navigateToAddCluster: di.inject(navigateToAddClusterInjectable), - navigateToExtensions: di.inject(navigateToExtensionsInjectable), - navigateToEntitySettings: di.inject(navigateToEntitySettingsInjectable), - navigateToClusterView: di.inject(navigateToClusterViewInjectable), - navigateToPreferenceTabId: di.inject(navigateToPreferenceTabIdInjectable), - }), -}); - -export default bindProtocolAddRouteHandlersInjectable; diff --git a/src/renderer/protocol-handler/bind-protocol-add-route-handlers/bind-protocol-add-route-handlers.tsx b/src/renderer/protocol-handler/bind-protocol-add-route-handlers/bind-protocol-add-route-handlers.tsx deleted file mode 100644 index 2b9038172d..0000000000 --- a/src/renderer/protocol-handler/bind-protocol-add-route-handlers/bind-protocol-add-route-handlers.tsx +++ /dev/null @@ -1,129 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import React from "react"; -import type { LensProtocolRouterRenderer } from "../lens-protocol-router-renderer/lens-protocol-router-renderer"; -import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; -import { ClusterStore } from "../../../common/cluster-store/cluster-store"; -import { - EXTENSION_NAME_MATCH, - EXTENSION_PUBLISHER_MATCH, - LensProtocolRouter, -} from "../../../common/protocol-handler"; -import { Notifications } from "../../components/notifications"; -import type { NavigateToCatalog } from "../../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable"; -import type { NavigateToEntitySettings } from "../../../common/front-end-routing/routes/entity-settings/navigate-to-entity-settings.injectable"; -import type { NavigateToClusterView } from "../../../common/front-end-routing/routes/cluster-view/navigate-to-cluster-view.injectable"; -import type { AttemptInstallByInfo } from "../../components/+extensions/attempt-install-by-info.injectable"; - -interface Dependencies { - attemptInstallByInfo: AttemptInstallByInfo; - lensProtocolRouterRenderer: LensProtocolRouterRenderer; - navigateToCatalog: NavigateToCatalog; - navigateToAddCluster: () => void; - navigateToExtensions: () => void; - navigateToEntitySettings: NavigateToEntitySettings; - navigateToClusterView: NavigateToClusterView; - navigateToPreferenceTabId: (tabId: string) => void; -} - -export const bindProtocolAddRouteHandlers = - ({ attemptInstallByInfo, lensProtocolRouterRenderer, navigateToCatalog, navigateToAddCluster, navigateToExtensions, navigateToEntitySettings, navigateToClusterView, navigateToPreferenceTabId }: Dependencies) => - () => { - lensProtocolRouterRenderer - .addInternalHandler("/preferences", ({ search: { highlight: tabId }}) => { - navigateToPreferenceTabId(tabId); - }) - .addInternalHandler("/", ({ tail }) => { - if (tail) { - Notifications.shortInfo( -

- Unknown Action for lens://app/{tail}. Are you on the - latest version? -

, - ); - } - - navigateToCatalog(); - }) - .addInternalHandler("/landing", () => { - navigateToCatalog(); - }) - .addInternalHandler( - "/landing/view/:group/:kind", - ({ pathname: { group, kind }}) => { - navigateToCatalog({ group, kind }); - }, - ) - .addInternalHandler("/cluster", () => { - navigateToAddCluster(); - }) - .addInternalHandler( - "/entity/:entityId/settings", - ({ pathname: { entityId }}) => { - const entity = catalogEntityRegistry.getById(entityId); - - if (entity) { - navigateToEntitySettings(entityId); - } else { - Notifications.shortInfo( -

- Unknown catalog entity {entityId}. -

, - ); - } - }, - ) - // Handlers below are deprecated and only kept for backward compact purposes - .addInternalHandler( - "/cluster/:clusterId", - ({ pathname: { clusterId }}) => { - const cluster = ClusterStore.getInstance().getById(clusterId); - - if (cluster) { - navigateToClusterView(clusterId); - } else { - Notifications.shortInfo( -

- Unknown catalog entity {clusterId}. -

, - ); - } - }, - ) - .addInternalHandler( - "/cluster/:clusterId/settings", - ({ pathname: { clusterId }}) => { - const cluster = ClusterStore.getInstance().getById(clusterId); - - if (cluster) { - navigateToEntitySettings(clusterId); - } else { - Notifications.shortInfo( -

- Unknown catalog entity {clusterId}. -

, - ); - } - }, - ) - .addInternalHandler("/extensions", () => { - navigateToExtensions(); - }) - .addInternalHandler( - `/extensions/install${LensProtocolRouter.ExtensionUrlSchema}`, - ({ pathname, search: { version }}) => { - const name = [ - pathname[EXTENSION_PUBLISHER_MATCH], - pathname[EXTENSION_NAME_MATCH], - ] - .filter(Boolean) - .join("/"); - - navigateToExtensions(); - attemptInstallByInfo({ name, version, requireConfirmation: true }); - }, - ); - }; diff --git a/src/renderer/protocol-handler/index.ts b/src/renderer/protocol-handler/index.ts index c338f50114..7a32d45595 100644 --- a/src/renderer/protocol-handler/index.ts +++ b/src/renderer/protocol-handler/index.ts @@ -3,5 +3,4 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -export { LensProtocolRouterRenderer } from "./lens-protocol-router-renderer/lens-protocol-router-renderer"; -export { bindProtocolAddRouteHandlers } from "./bind-protocol-add-route-handlers/bind-protocol-add-route-handlers"; +export { LensProtocolRouterRenderer } from "./router"; diff --git a/src/renderer/protocol-handler/lens-protocol-router-renderer/lens-protocol-router-renderer.injectable.ts b/src/renderer/protocol-handler/lens-protocol-router-renderer/lens-protocol-router-renderer.injectable.ts deleted file mode 100644 index 9aa3df8a0a..0000000000 --- a/src/renderer/protocol-handler/lens-protocol-router-renderer/lens-protocol-router-renderer.injectable.ts +++ /dev/null @@ -1,21 +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 { LensProtocolRouterRenderer } from "./lens-protocol-router-renderer"; -import extensionsStoreInjectable - from "../../../extensions/extensions-store/extensions-store.injectable"; - -const lensProtocolRouterRendererInjectable = getInjectable({ - id: "lens-protocol-router-renderer", - - instantiate: (di) => - new LensProtocolRouterRenderer({ - extensionLoader: di.inject(extensionLoaderInjectable), - extensionsStore: di.inject(extensionsStoreInjectable), - }), -}); - -export default lensProtocolRouterRendererInjectable; diff --git a/src/renderer/protocol-handler/bind-protocol-add-route-handlers/navigate-to-preference-tab-id.injectable.ts b/src/renderer/protocol-handler/navigate-to-preference-tab-id.injectable.ts similarity index 74% rename from src/renderer/protocol-handler/bind-protocol-add-route-handlers/navigate-to-preference-tab-id.injectable.ts rename to src/renderer/protocol-handler/navigate-to-preference-tab-id.injectable.ts index 5c6aa2177e..55f5c9159c 100644 --- a/src/renderer/protocol-handler/bind-protocol-add-route-handlers/navigate-to-preference-tab-id.injectable.ts +++ b/src/renderer/protocol-handler/navigate-to-preference-tab-id.injectable.ts @@ -3,8 +3,8 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import preferenceNavigationItemsInjectable from "../../components/+preferences/preferences-navigation/preference-navigation-items.injectable"; -import navigateToPreferencesInjectable from "../../../common/front-end-routing/routes/preferences/navigate-to-preferences.injectable"; +import preferenceNavigationItemsInjectable from "../components/+preferences/preferences-navigation/preference-navigation-items.injectable"; +import navigateToPreferencesInjectable from "../../common/front-end-routing/routes/preferences/navigate-to-preferences.injectable"; const navigateToPreferenceTabIdInjectable = getInjectable({ id: "navigate-to-preference-tab-id", diff --git a/src/renderer/protocol-handler/router.injectable.ts b/src/renderer/protocol-handler/router.injectable.ts new file mode 100644 index 0000000000..42a1e45c3c --- /dev/null +++ b/src/renderer/protocol-handler/router.injectable.ts @@ -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 extensionLoaderInjectable from "../../extensions/extension-loader/extension-loader.injectable"; +import { LensProtocolRouterRenderer } from "./router"; +import extensionsStoreInjectable from "../../extensions/extensions-store/extensions-store.injectable"; +import lensProtocolRouterLoggerInjectable from "../../common/protocol-handler/logger.injectable"; + +const lensProtocolRouterRendererInjectable = getInjectable({ + id: "lens-protocol-router-renderer", + + instantiate: (di) => new LensProtocolRouterRenderer({ + extensionLoader: di.inject(extensionLoaderInjectable), + extensionsStore: di.inject(extensionsStoreInjectable), + logger: di.inject(lensProtocolRouterLoggerInjectable), + }), +}); + +export default lensProtocolRouterRendererInjectable; diff --git a/src/renderer/protocol-handler/lens-protocol-router-renderer/lens-protocol-router-renderer.tsx b/src/renderer/protocol-handler/router.tsx similarity index 84% rename from src/renderer/protocol-handler/lens-protocol-router-renderer/lens-protocol-router-renderer.tsx rename to src/renderer/protocol-handler/router.tsx index e5b9bb300d..ee3a82d785 100644 --- a/src/renderer/protocol-handler/lens-protocol-router-renderer/lens-protocol-router-renderer.tsx +++ b/src/renderer/protocol-handler/router.tsx @@ -5,13 +5,12 @@ import React from "react"; import { ipcRenderer } from "electron"; -import * as proto from "../../../common/protocol-handler"; +import * as proto from "../../common/protocol-handler"; import Url from "url-parse"; -import { onCorrect } from "../../../common/ipc"; -import { foldAttemptResults, ProtocolHandlerInvalid, RouteAttempt } from "../../../common/protocol-handler"; -import { Notifications } from "../../components/notifications"; -import type { ExtensionLoader } from "../../../extensions/extension-loader"; -import type { ExtensionsStore } from "../../../extensions/extensions-store/extensions-store"; +import { onCorrect } from "../../common/ipc"; +import type { LensProtocolRouterDependencies } from "../../common/protocol-handler"; +import { foldAttemptResults, ProtocolHandlerInvalid, RouteAttempt } from "../../common/protocol-handler"; +import { Notifications } from "../components/notifications"; function verifyIpcArgs(args: unknown[]): args is [string, RouteAttempt] { if (args.length !== 2) { @@ -32,14 +31,12 @@ function verifyIpcArgs(args: unknown[]): args is [string, RouteAttempt] { } } -interface Dependencies { - extensionLoader: ExtensionLoader; - extensionsStore: ExtensionsStore; +export interface LensProtocolRouterRendererDependencies extends LensProtocolRouterDependencies { } export class LensProtocolRouterRenderer extends proto.LensProtocolRouter { - constructor(protected dependencies: Dependencies) { + constructor(protected readonly dependencies: LensProtocolRouterRendererDependencies) { super(dependencies); } diff --git a/src/renderer/utils/create-storage/create-storage.injectable.ts b/src/renderer/utils/create-storage/create-storage.injectable.ts index f9df0315a9..3c0e668eda 100644 --- a/src/renderer/utils/create-storage/create-storage.injectable.ts +++ b/src/renderer/utils/create-storage/create-storage.injectable.ts @@ -8,30 +8,24 @@ import { createStorage } from "./create-storage"; import readJsonFileInjectable from "../../../common/fs/read-json-file.injectable"; import writeJsonFileInjectable from "../../../common/fs/write-json-file.injectable"; import { observable } from "mobx"; -import loggerInjectable from "../../../common/logger.injectable"; import getAbsolutePathInjectable from "../../../common/path/get-absolute-path.injectable"; +import createStorageLoggerInjectable from "./logger.injectable"; const createStorageInjectable = getInjectable({ id: "create-storage", - instantiate: (di) => - createStorage({ - storage: observable({ - initialized: false, - loaded: false, - data: {} as Record, // json-serializable - }), - - readJsonFile: di.inject(readJsonFileInjectable), - writeJsonFile: di.inject(writeJsonFileInjectable), - logger: di.inject(loggerInjectable), - - directoryForLensLocalStorage: di.inject( - directoryForLensLocalStorageInjectable, - ), - - getAbsolutePath: di.inject(getAbsolutePathInjectable), + instantiate: (di) => createStorage({ + storage: observable({ + initialized: false, + loaded: false, + data: {} as Record, // json-serializable }), + readJsonFile: di.inject(readJsonFileInjectable), + writeJsonFile: di.inject(writeJsonFileInjectable), + logger: di.inject(createStorageLoggerInjectable), + directoryForLensLocalStorage: di.inject(directoryForLensLocalStorageInjectable), + getAbsolutePath: di.inject(getAbsolutePathInjectable), + }), }); export default createStorageInjectable; diff --git a/src/renderer/utils/create-storage/create-storage.ts b/src/renderer/utils/create-storage/create-storage.ts index 295e88fda7..fd27f7013d 100755 --- a/src/renderer/utils/create-storage/create-storage.ts +++ b/src/renderer/utils/create-storage/create-storage.ts @@ -26,9 +26,14 @@ interface Dependencies { /** * Creates a helper for saving data under the "key" intended for window.localStorage */ -export const createStorage = ({ storage, getAbsolutePath, logger, directoryForLensLocalStorage, readJsonFile, writeJsonFile }: Dependencies) => (key: string, defaultValue: T) => { - const { logPrefix } = StorageHelper; - +export const createStorage = ({ + storage, + getAbsolutePath, + logger, + directoryForLensLocalStorage, + readJsonFile, + writeJsonFile, +}: Dependencies) => (key: string, defaultValue: T) => { if (!storage.initialized) { storage.initialized = true; @@ -44,7 +49,7 @@ export const createStorage = ({ storage, getAbsolutePath, logger, directoryForLe finally { if (!isTestEnv) { - logger.info(`${logPrefix} loading finished for ${filePath}`); + logger.info(`loading finished for ${filePath}`); } storage.loaded = true; @@ -57,18 +62,18 @@ export const createStorage = ({ storage, getAbsolutePath, logger, directoryForLe }); async function saveFile(state: Record = {}) { - logger.info(`${logPrefix} saving ${filePath}`); + logger.info(`saving ${filePath}`); try { await writeJsonFile(filePath, state); } catch (error) { - logger.error(`${logPrefix} saving failed: ${error}`, { + logger.error(`saving failed: ${error}`, { json: state, jsonFilePath: filePath, }); } } })() - .catch(error => logger.error(`${logPrefix} Failed to initialize storage: ${error}`)); + .catch(error => logger.error(`Failed to initialize storage: ${error}`)); } return new StorageHelper(key, { diff --git a/src/renderer/utils/create-storage/logger.injectable.ts b/src/renderer/utils/create-storage/logger.injectable.ts new file mode 100644 index 0000000000..dea76445ba --- /dev/null +++ b/src/renderer/utils/create-storage/logger.injectable.ts @@ -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 createStorageLoggerInjectable = getInjectable({ + id: "create-storage-logger", + instantiate: (di) => di.inject(childLoggerInjectable, { + prefix: "CREATE-STORAGE", + }), +}); + +export default createStorageLoggerInjectable; diff --git a/src/renderer/window/event-listener.injectable.ts b/src/renderer/window/event-listener.injectable.ts index 5f682d956d..84803cbd86 100644 --- a/src/renderer/window/event-listener.injectable.ts +++ b/src/renderer/window/event-listener.injectable.ts @@ -6,6 +6,8 @@ import { getInjectable } from "@ogre-tools/injectable"; import type { Disposer } from "../utils"; +export type AddWindowEventListener = typeof addWindowEventListener; + function addWindowEventListener(type: K, listener: (this: Window, ev: WindowEventMap[K]) => any, options?: boolean | AddEventListenerOptions): Disposer { window.addEventListener(type, listener, options); diff --git a/src/test-utils/override-ipc-bridge.ts b/src/test-utils/override-ipc-bridge.ts index efcb48a115..6aba381243 100644 --- a/src/test-utils/override-ipc-bridge.ts +++ b/src/test-utils/override-ipc-bridge.ts @@ -8,8 +8,8 @@ import getValueFromRegisteredChannelInjectable from "../renderer/app-paths/get-v import registerChannelInjectable from "../main/app-paths/register-channel/register-channel.injectable"; import asyncFn from "@async-fn/jest"; import registerIpcChannelListenerInjectable from "../renderer/app-paths/get-value-from-registered-channel/register-ipc-channel-listener.injectable"; -import windowManagerInjectable from "../main/window-manager.injectable"; -import type { SendToViewArgs, WindowManager } from "../main/window-manager"; +import windowManagerInjectable from "../main/window/manager.injectable"; +import type { SendToViewArgs, WindowManager } from "../main/window/manager"; import { appNavigationIpcChannel } from "../common/front-end-routing/navigation-ipc-channel"; export const overrideIpcBridge = ({ diff --git a/types/selfsigned.d.ts b/types/selfsigned.d.ts index 89615f1c01..810d095414 100644 --- a/types/selfsigned.d.ts +++ b/types/selfsigned.d.ts @@ -10,9 +10,9 @@ cert: string; } - type GenerateAttributes = Array; + export type GenerateAttributes = Array; - interface GenerateOptions { + export interface GenerateOptions { keySize?: number; days?: number; algorithm?: "sha1" | "sha256"; @@ -22,5 +22,5 @@ clientCertificateCN?: string; } - export function generate(GenerateAttributes, GenerateOptions): SelfSignedCert; + export function generate(attrs: GenerateAttributes, opts: GenerateOptions): SelfSignedCert; }