From a83259b70a0d01fdaa6c6be8ec4c0563383ab922 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Wed, 21 Dec 2022 06:31:31 -0800 Subject: [PATCH 1/4] Make request-api-resources flatter in implementation (#6802) * Make request-api-resources flatter in implementation Signed-off-by: Sebastian Malton * More improvements to requestApiResources - Also move files to better places Signed-off-by: Sebastian Malton * Rename iter.pipeline as iter.chain Signed-off-by: Sebastian Malton Signed-off-by: Sebastian Malton --- src/common/cluster/cluster.ts | 2 +- .../request-api-resources.injectable.ts | 83 ------------------- src/common/utils/iter.ts | 13 +-- src/common/utils/with-concurrency-limit.ts | 13 +++ .../kubeconfig-sync/manager.ts | 2 +- .../request-api-resources.injectable.ts | 49 +++++++++++ src/main/cluster/request-api-versions.ts | 18 ++++ .../request-core-api-versions.injectable.ts | 27 ++++++ ...quest-kube-api-resources-for.injectable.ts | 34 ++++++++ ...equest-non-core-api-versions.injectable.ts | 30 +++++++ .../create-cluster.injectable.ts | 2 +- src/main/k8s-request.injectable.ts | 4 +- .../+config-secrets/add-secret-dialog.tsx | 2 +- .../command-palette/command-dialog.tsx | 2 +- src/renderer/utils/cssNames.ts | 2 +- 15 files changed, 186 insertions(+), 97 deletions(-) delete mode 100644 src/common/cluster/request-api-resources.injectable.ts create mode 100644 src/common/utils/with-concurrency-limit.ts create mode 100644 src/main/cluster/request-api-resources.injectable.ts create mode 100644 src/main/cluster/request-api-versions.ts create mode 100644 src/main/cluster/request-core-api-versions.injectable.ts create mode 100644 src/main/cluster/request-kube-api-resources-for.injectable.ts create mode 100644 src/main/cluster/request-non-core-api-versions.injectable.ts diff --git a/src/common/cluster/cluster.ts b/src/common/cluster/cluster.ts index 303ee89361..c8e7e69e90 100644 --- a/src/common/cluster/cluster.ts +++ b/src/common/cluster/cluster.ts @@ -26,7 +26,7 @@ import type { Logger } from "../logger"; import type { BroadcastMessage } from "../ipc/broadcast-message.injectable"; import type { LoadConfigfromFile } from "../kube-helpers/load-config-from-file.injectable"; import type { CanListResource, RequestNamespaceListPermissions, RequestNamespaceListPermissionsFor } from "./request-namespace-list-permissions.injectable"; -import type { RequestApiResources } from "./request-api-resources.injectable"; +import type { RequestApiResources } from "../../main/cluster/request-api-resources.injectable"; export interface ClusterDependencies { readonly directoryForKubeConfigs: string; diff --git a/src/common/cluster/request-api-resources.injectable.ts b/src/common/cluster/request-api-resources.injectable.ts deleted file mode 100644 index 3e1a621f39..0000000000 --- a/src/common/cluster/request-api-resources.injectable.ts +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { V1APIGroupList, V1APIResourceList, V1APIVersions } from "@kubernetes/client-node"; -import { getInjectable } from "@ogre-tools/injectable"; -import k8SRequestInjectable from "../../main/k8s-request.injectable"; -import loggerInjectable from "../logger.injectable"; -import type { KubeApiResource } from "../rbac"; -import type { Cluster } from "./cluster"; -import plimit from "p-limit"; - -export type RequestApiResources = (cluster: Cluster) => Promise; - -interface KubeResourceListGroup { - group: string; - path: string; -} - -const requestApiResourcesInjectable = getInjectable({ - id: "request-api-resources", - instantiate: (di): RequestApiResources => { - const k8sRequest = di.inject(k8SRequestInjectable); - const logger = di.inject(loggerInjectable); - - return async (cluster) => { - const apiLimit = plimit(5); - const kubeApiResources: KubeApiResource[] = []; - const resourceListGroups: KubeResourceListGroup[] = []; - - try { - await Promise.all([ - (async () => { - const { versions } = await k8sRequest(cluster, "/api") as V1APIVersions; - - for (const version of versions) { - resourceListGroups.push({ - group: version, - path: `/api/${version}`, - }); - } - })(), - (async () => { - const { groups } = await k8sRequest(cluster, "/apis") as V1APIGroupList; - - for (const { preferredVersion, name } of groups) { - const { groupVersion } = preferredVersion ?? {}; - - if (groupVersion) { - resourceListGroups.push({ - group: name, - path: `/apis/${groupVersion}`, - }); - } - } - })(), - ]); - - await Promise.all( - resourceListGroups.map(({ group, path }) => apiLimit(async () => { - const { resources } = await k8sRequest(cluster, path) as V1APIResourceList; - - for (const resource of resources) { - kubeApiResources.push({ - apiName: resource.name, - kind: resource.kind, - group, - namespaced: resource.namespaced, - }); - } - })), - ); - } catch (error) { - logger.error(`[LIST-API-RESOURCES]: failed to list api resources: ${error}`); - } - - return kubeApiResources; - }; - }, -}); - -export default requestApiResourcesInjectable; diff --git a/src/common/utils/iter.ts b/src/common/utils/iter.ts index 9752e56dc5..5b5593d2d4 100644 --- a/src/common/utils/iter.ts +++ b/src/common/utils/iter.ts @@ -5,7 +5,7 @@ export type Falsey = false | 0 | "" | null | undefined; -interface Iterator { +interface Iterator extends Iterable { filter(fn: (val: T) => unknown): Iterator; filterMap(fn: (val: T) => Falsey | U): Iterator; find(fn: (val: T) => unknown): T | undefined; @@ -15,15 +15,16 @@ interface Iterator { join(sep?: string): string; } -export function pipeline(src: IterableIterator): Iterator { +export function chain(src: IterableIterator): Iterator { return { - filter: (fn) => pipeline(filter(src, fn)), - filterMap: (fn) => pipeline(filterMap(src, fn)), - map: (fn) => pipeline(map(src, fn)), - flatMap: (fn) => pipeline(flatMap(src, fn)), + filter: (fn) => chain(filter(src, fn)), + filterMap: (fn) => chain(filterMap(src, fn)), + map: (fn) => chain(map(src, fn)), + flatMap: (fn) => chain(flatMap(src, fn)), find: (fn) => find(src, fn), join: (sep) => join(src, sep), collect: (fn) => fn(src), + [Symbol.iterator]: () => src, }; } diff --git a/src/common/utils/with-concurrency-limit.ts b/src/common/utils/with-concurrency-limit.ts new file mode 100644 index 0000000000..284bb334e1 --- /dev/null +++ b/src/common/utils/with-concurrency-limit.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 plimit from "p-limit"; + +export type ConcurrencyLimiter = (fn: (...args: Args) => Res) => (...args: Args) => Promise; + +export function withConcurrencyLimit(limit: number): ConcurrencyLimiter { + const limiter = plimit(limit); + + return fn => (...args) => limiter(() => fn(...args)); +} diff --git a/src/main/catalog-sources/kubeconfig-sync/manager.ts b/src/main/catalog-sources/kubeconfig-sync/manager.ts index 305f25a909..cf05577ae7 100644 --- a/src/main/catalog-sources/kubeconfig-sync/manager.ts +++ b/src/main/catalog-sources/kubeconfig-sync/manager.ts @@ -34,7 +34,7 @@ export class KubeconfigSyncManager { const seenIds = new Set(); return ( - iter.pipeline(this.sources.values()) + iter.chain(this.sources.values()) .flatMap(([entities]) => entities.get()) .filter(entity => { const alreadySeen = seenIds.has(entity.getId()); diff --git a/src/main/cluster/request-api-resources.injectable.ts b/src/main/cluster/request-api-resources.injectable.ts new file mode 100644 index 0000000000..bc996add60 --- /dev/null +++ b/src/main/cluster/request-api-resources.injectable.ts @@ -0,0 +1,49 @@ +/** + * 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 "../../common/logger.injectable"; +import type { KubeApiResource } from "../../common/rbac"; +import type { Cluster } from "../../common/cluster/cluster"; +import { requestApiVersionsInjectionToken } from "./request-api-versions"; +import { withConcurrencyLimit } from "../../common/utils/with-concurrency-limit"; +import requestKubeApiResourcesForInjectable from "./request-kube-api-resources-for.injectable"; + +export type RequestApiResources = (cluster: Cluster) => Promise; + +export interface KubeResourceListGroup { + group: string; + path: string; +} + +const requestApiResourcesInjectable = getInjectable({ + id: "request-api-resources", + instantiate: (di): RequestApiResources => { + const logger = di.inject(loggerInjectable); + const apiVersionRequesters = di.injectMany(requestApiVersionsInjectionToken); + const requestKubeApiResourcesFor = di.inject(requestKubeApiResourcesForInjectable); + + return async (cluster) => { + const requestKubeApiResources = withConcurrencyLimit(5)(requestKubeApiResourcesFor(cluster)); + + try { + const requests = await Promise.all(apiVersionRequesters.map(fn => fn(cluster))); + const resources = await Promise.all(( + requests + .flat() + .map(requestKubeApiResources) + )); + + return resources.flat(); + } catch (error) { + logger.error(`[LIST-API-RESOURCES]: failed to list api resources: ${error}`); + + return []; + } + }; + }, +}); + +export default requestApiResourcesInjectable; diff --git a/src/main/cluster/request-api-versions.ts b/src/main/cluster/request-api-versions.ts new file mode 100644 index 0000000000..af7bc7f232 --- /dev/null +++ b/src/main/cluster/request-api-versions.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 { getInjectionToken } from "@ogre-tools/injectable"; +import type { Cluster } from "../../common/cluster/cluster"; + +export interface KubeResourceListGroup { + group: string; + path: string; +} + +export type RequestApiVersions = (cluster: Cluster) => Promise; + +export const requestApiVersionsInjectionToken = getInjectionToken({ + id: "request-api-versions-token", +}); diff --git a/src/main/cluster/request-core-api-versions.injectable.ts b/src/main/cluster/request-core-api-versions.injectable.ts new file mode 100644 index 0000000000..e92def914e --- /dev/null +++ b/src/main/cluster/request-core-api-versions.injectable.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { V1APIVersions } from "@kubernetes/client-node"; +import { getInjectable } from "@ogre-tools/injectable"; +import k8sRequestInjectable from "../k8s-request.injectable"; +import { requestApiVersionsInjectionToken } from "./request-api-versions"; + +const requestCoreApiVersionsInjectable = getInjectable({ + id: "request-core-api-versions", + instantiate: (di) => { + const k8sRequest = di.inject(k8sRequestInjectable); + + return async (cluster) => { + const { versions } = await k8sRequest(cluster, "/api") as V1APIVersions; + + return versions.map(version => ({ + group: version, + path: `/api/${version}`, + })); + }; + }, + injectionToken: requestApiVersionsInjectionToken, +}); + +export default requestCoreApiVersionsInjectable; diff --git a/src/main/cluster/request-kube-api-resources-for.injectable.ts b/src/main/cluster/request-kube-api-resources-for.injectable.ts new file mode 100644 index 0000000000..dfe1345db1 --- /dev/null +++ b/src/main/cluster/request-kube-api-resources-for.injectable.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { V1APIResourceList } from "@kubernetes/client-node"; +import { getInjectable } from "@ogre-tools/injectable"; +import type { Cluster } from "../../common/cluster/cluster"; +import type { KubeApiResource } from "../../common/rbac"; +import k8sRequestInjectable from "../k8s-request.injectable"; +import type { KubeResourceListGroup } from "./request-api-versions"; + +export type RequestKubeApiResources = (grouping: KubeResourceListGroup) => Promise; + +export type RequestKubeApiResourcesFor = (cluster: Cluster) => RequestKubeApiResources; + +const requestKubeApiResourcesForInjectable = getInjectable({ + id: "request-kube-api-resources-for", + instantiate: (di): RequestKubeApiResourcesFor => { + const k8sRequest = di.inject(k8sRequestInjectable); + + return (cluster) => async ({ group, path }) => { + const { resources } = await k8sRequest(cluster, path) as V1APIResourceList; + + return resources.map(resource => ({ + apiName: resource.name, + kind: resource.kind, + group, + namespaced: resource.namespaced, + })); + }; + }, +}); + +export default requestKubeApiResourcesForInjectable; diff --git a/src/main/cluster/request-non-core-api-versions.injectable.ts b/src/main/cluster/request-non-core-api-versions.injectable.ts new file mode 100644 index 0000000000..5ca9e1495b --- /dev/null +++ b/src/main/cluster/request-non-core-api-versions.injectable.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { V1APIGroupList } from "@kubernetes/client-node"; +import { getInjectable } from "@ogre-tools/injectable"; +import { chain } from "../../common/utils/iter"; +import k8sRequestInjectable from "../k8s-request.injectable"; +import { requestApiVersionsInjectionToken } from "./request-api-versions"; + +const requestNonCoreApiVersionsInjectable = getInjectable({ + id: "request-non-core-api-versions", + instantiate: (di) => { + const k8sRequest = di.inject(k8sRequestInjectable); + + return async (cluster) => { + const { groups } = await k8sRequest(cluster, "/apis") as V1APIGroupList; + + return chain(groups.values()) + .filterMap(group => group.preferredVersion?.groupVersion && ({ + group: group.name, + path: `/apis/${group.preferredVersion.groupVersion}`, + })) + .collect(v => [...v]); + }; + }, + injectionToken: requestApiVersionsInjectionToken, +}); + +export default requestNonCoreApiVersionsInjectable; diff --git a/src/main/create-cluster/create-cluster.injectable.ts b/src/main/create-cluster/create-cluster.injectable.ts index f5b302300c..760e8e2e75 100644 --- a/src/main/create-cluster/create-cluster.injectable.ts +++ b/src/main/create-cluster/create-cluster.injectable.ts @@ -12,7 +12,7 @@ import createContextHandlerInjectable from "../context-handler/create-context-ha 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 createListApiResourcesInjectable from "../../common/cluster/request-api-resources.injectable"; +import createListApiResourcesInjectable from "../cluster/request-api-resources.injectable"; import loggerInjectable from "../../common/logger.injectable"; import detectorRegistryInjectable from "../cluster-detectors/detector-registry.injectable"; import createVersionDetectorInjectable from "../cluster-detectors/create-version-detector.injectable"; diff --git a/src/main/k8s-request.injectable.ts b/src/main/k8s-request.injectable.ts index a556cf62f2..a1a56be7dd 100644 --- a/src/main/k8s-request.injectable.ts +++ b/src/main/k8s-request.injectable.ts @@ -11,7 +11,7 @@ import lensProxyPortInjectable from "./lens-proxy/lens-proxy-port.injectable"; export type K8sRequest = (cluster: Cluster, path: string, options?: RequestPromiseOptions) => Promise; -const k8SRequestInjectable = getInjectable({ +const k8sRequestInjectable = getInjectable({ id: "k8s-request", instantiate: (di) => { @@ -34,4 +34,4 @@ const k8SRequestInjectable = getInjectable({ }, }); -export default k8SRequestInjectable; +export default k8sRequestInjectable; diff --git a/src/renderer/components/+config-secrets/add-secret-dialog.tsx b/src/renderer/components/+config-secrets/add-secret-dialog.tsx index 2d35a79160..c839025f02 100644 --- a/src/renderer/components/+config-secrets/add-secret-dialog.tsx +++ b/src/renderer/components/+config-secrets/add-secret-dialog.tsx @@ -86,7 +86,7 @@ export class AddSecretDialog extends React.Component { }; private getDataFromFields = (fields: SecretTemplateField[] = [], processValue: (val: string) => string = (val => val)) => { - return iter.pipeline(fields.values()) + return iter.chain(fields.values()) .filterMap(({ key, value }) => ( value ? [key, processValue(value)] as const diff --git a/src/renderer/components/command-palette/command-dialog.tsx b/src/renderer/components/command-palette/command-dialog.tsx index 06ed612b93..db33bde1cd 100644 --- a/src/renderer/components/command-palette/command-dialog.tsx +++ b/src/renderer/components/command-palette/command-dialog.tsx @@ -67,7 +67,7 @@ const NonInjectedCommandDialog = observer(({ } }; - const activeCommands = iter.pipeline(commands.get().values()) + const activeCommands = iter.chain(commands.get().values()) .filter(command => { try { return command.isActive(context); diff --git a/src/renderer/utils/cssNames.ts b/src/renderer/utils/cssNames.ts index 68fc8c1749..cd1fa37a4a 100755 --- a/src/renderer/utils/cssNames.ts +++ b/src/renderer/utils/cssNames.ts @@ -27,7 +27,7 @@ export function cssNames(...classNames: IClassName[]): string { } } - return iter.pipeline(classNamesEnabled.entries()) + return iter.chain(classNamesEnabled.entries()) .filter(([, isActive]) => !!isActive) .filterMap(([className]) => className.trim()) .join(" "); From 2000d9b32e4069d8256b83fdd2570a38b1ec6ca6 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Wed, 21 Dec 2022 06:46:51 -0800 Subject: [PATCH 2/4] Massively simplify bundled extension loading (#6787) * Switch bundled extension declarations to injection token Signed-off-by: Sebastian Malton * Change how bundled extensions are loaded Signed-off-by: Sebastian Malton * Fix token file name Signed-off-by: Sebastian Malton * Fix spelling Signed-off-by: Sebastian Malton * Improve interface name Signed-off-by: Sebastian Malton Signed-off-by: Sebastian Malton --- package.json | 3 +- .../bundled-extension-token.ts | 17 +++ .../extension-discovery.injectable.ts | 2 - .../extension-discovery.ts | 34 +---- .../extension-loader/entry-point-name.ts | 10 ++ .../extension-loader.injectable.ts | 4 + .../extension-loader/extension-loader.ts | 142 +++++++++++++----- .../entry-point-name.injectable.ts | 14 ++ .../entry-point-name.injectable.ts | 14 ++ 9 files changed, 164 insertions(+), 76 deletions(-) create mode 100644 src/extensions/extension-discovery/bundled-extension-token.ts create mode 100644 src/extensions/extension-loader/entry-point-name.ts create mode 100644 src/main/extension-loader/entry-point-name.injectable.ts create mode 100644 src/renderer/extension-loader/entry-point-name.injectable.ts diff --git a/package.json b/package.json index b0b341bd64..8eeef8787d 100644 --- a/package.json +++ b/package.json @@ -58,8 +58,7 @@ "bundledHelmVersion": "3.7.2", "sentryDsn": "", "contentSecurityPolicy": "script-src 'unsafe-eval' 'self'; frame-src http://*.localhost:*/; img-src * data:", - "welcomeRoute": "/welcome", - "extensions": [] + "welcomeRoute": "/welcome" }, "engines": { "node": ">=16 <17" diff --git a/src/extensions/extension-discovery/bundled-extension-token.ts b/src/extensions/extension-discovery/bundled-extension-token.ts new file mode 100644 index 0000000000..1a1a40f9fa --- /dev/null +++ b/src/extensions/extension-discovery/bundled-extension-token.ts @@ -0,0 +1,17 @@ +/** + * 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 { LensExtensionConstructor, LensExtensionManifest } from "../lens-extension"; + +export interface BundledExtension { + readonly manifest: LensExtensionManifest; + main: () => LensExtensionConstructor | null; + renderer: () => LensExtensionConstructor | null; +} + +export const bundledExtensionInjectionToken = getInjectionToken({ + id: "bundled-extension-path", +}); diff --git a/src/extensions/extension-discovery/extension-discovery.injectable.ts b/src/extensions/extension-discovery/extension-discovery.injectable.ts index 971850c585..378f519bb7 100644 --- a/src/extensions/extension-discovery/extension-discovery.injectable.ts +++ b/src/extensions/extension-discovery/extension-discovery.injectable.ts @@ -27,7 +27,6 @@ import getRelativePathInjectable from "../../common/path/get-relative-path.injec import joinPathsInjectable from "../../common/path/join-paths.injectable"; import removePathInjectable from "../../common/fs/remove.injectable"; import homeDirectoryPathInjectable from "../../common/os/home-directory-path.injectable"; -import applicationInformationInjectable from "../../common/vars/application-information.injectable"; import lensResourcesDirInjectable from "../../common/vars/lens-resources-dir.injectable"; const extensionDiscoveryInjectable = getInjectable({ @@ -58,7 +57,6 @@ const extensionDiscoveryInjectable = getInjectable({ getRelativePath: di.inject(getRelativePathInjectable), joinPaths: di.inject(joinPathsInjectable), homeDirectoryPath: di.inject(homeDirectoryPathInjectable), - applicationInformation: di.inject(applicationInformationInjectable), }), }); diff --git a/src/extensions/extension-discovery/extension-discovery.ts b/src/extensions/extension-discovery/extension-discovery.ts index 1d14c427d0..c9646a1c6c 100644 --- a/src/extensions/extension-discovery/extension-discovery.ts +++ b/src/extensions/extension-discovery/extension-discovery.ts @@ -30,7 +30,6 @@ import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable" import type { GetRelativePath } from "../../common/path/get-relative-path.injectable"; import type { RemovePath } from "../../common/fs/remove.injectable"; import type TypedEventEmitter from "typed-emitter"; -import type { ApplicationInformation } from "../../common/vars/application-information.injectable"; interface Dependencies { readonly extensionLoader: ExtensionLoader; @@ -42,7 +41,6 @@ interface Dependencies { readonly isProduction: boolean; readonly fileSystemSeparator: string; readonly homeDirectoryPath: string; - readonly applicationInformation: ApplicationInformation; isCompatibleExtension: (manifest: LensExtensionManifest) => boolean; installExtension: (name: string) => Promise; readJsonFile: ReadJson; @@ -384,42 +382,16 @@ export class ExtensionDiscovery { } async ensureExtensions(): Promise> { - const bundledExtensions = await this.loadBundledExtensions(); - const userExtensions = await this.loadFromFolder(this.localFolderPath, bundledExtensions.map((extension) => extension.manifest.name)); - const extensions = bundledExtensions.concat(userExtensions); + const userExtensions = await this.loadFromFolder(this.localFolderPath); - return this.extensions = new Map(extensions.map(extension => [extension.id, extension])); + return this.extensions = new Map(userExtensions.map(extension => [extension.id, extension])); } - async loadBundledExtensions(): Promise { - const extensions: InstalledExtension[] = []; - const extensionNames = this.dependencies.applicationInformation.config.extensions || []; - - for (const dirName of extensionNames) { - const absPath = this.dependencies.joinPaths(__dirname, "..", "..", "node_modules", dirName); - const extension = await this.loadExtensionFromFolder(absPath, { isBundled: true }); - - if (!extension) { - throw new Error(`Couldn't load bundled extension: ${dirName}`); - } - - extensions.push(extension); - } - this.dependencies.logger.debug(`${logModule}: ${extensions.length} extensions loaded`, { extensions }); - - return extensions; - } - - async loadFromFolder(folderPath: string, bundledExtensions: string[]): Promise { + async loadFromFolder(folderPath: string): Promise { const extensions: InstalledExtension[] = []; const paths = await this.dependencies.readDirectory(folderPath); for (const fileName of paths) { - // do not allow to override bundled extensions - if (bundledExtensions.includes(fileName)) { - continue; - } - const absPath = this.dependencies.joinPaths(folderPath, fileName); try { diff --git a/src/extensions/extension-loader/entry-point-name.ts b/src/extensions/extension-loader/entry-point-name.ts new file mode 100644 index 0000000000..390da4a34a --- /dev/null +++ b/src/extensions/extension-loader/entry-point-name.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"; + +export const extensionEntryPointNameInjectionToken = getInjectionToken<"main" | "renderer">({ + id: "extension-entry-point-name-token", +}); diff --git a/src/extensions/extension-loader/extension-loader.injectable.ts b/src/extensions/extension-loader/extension-loader.injectable.ts index 48edce4446..67f8434043 100644 --- a/src/extensions/extension-loader/extension-loader.injectable.ts +++ b/src/extensions/extension-loader/extension-loader.injectable.ts @@ -12,6 +12,8 @@ import extensionInjectable from "./extension/extension.injectable"; import loggerInjectable from "../../common/logger.injectable"; import joinPathsInjectable from "../../common/path/join-paths.injectable"; import getDirnameOfPathInjectable from "../../common/path/get-dirname.injectable"; +import { bundledExtensionInjectionToken } from "../extension-discovery/bundled-extension-token"; +import { extensionEntryPointNameInjectionToken } from "./entry-point-name"; const extensionLoaderInjectable = getInjectable({ id: "extension-loader", @@ -21,6 +23,8 @@ const extensionLoaderInjectable = getInjectable({ createExtensionInstance: di.inject(createExtensionInstanceInjectionToken), extensionInstances: di.inject(extensionInstancesInjectable), getExtension: (instance: LensExtension) => di.inject(extensionInjectable, instance), + bundledExtensions: di.injectMany(bundledExtensionInjectionToken), + extensionEntryPointName: di.inject(extensionEntryPointNameInjectionToken), logger: di.inject(loggerInjectable), joinPaths: di.inject(joinPathsInjectable), getDirnameOfPath: di.inject(getDirnameOfPathInjectable), diff --git a/src/extensions/extension-loader/extension-loader.ts b/src/extensions/extension-loader/extension-loader.ts index c00414e88d..2c186ff38f 100644 --- a/src/extensions/extension-loader/extension-loader.ts +++ b/src/extensions/extension-loader/extension-loader.ts @@ -21,12 +21,15 @@ import type { Extension } from "./extension/extension.injectable"; import type { Logger } from "../../common/logger"; import type { JoinPaths } from "../../common/path/join-paths.injectable"; import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable"; +import type { BundledExtension } from "../extension-discovery/bundled-extension-token"; const logModule = "[EXTENSIONS-LOADER]"; interface Dependencies { readonly extensionInstances: ObservableMap; + readonly bundledExtensions: BundledExtension[]; readonly logger: Logger; + readonly extensionEntryPointName: "main" | "renderer"; updateExtensionsState: (extensionsState: Record) => void; createExtensionInstance: CreateExtensionInstance; getExtension: (instance: LensExtension) => Extension; @@ -34,6 +37,12 @@ interface Dependencies { getDirnameOfPath: GetDirnameOfPath; } +interface ExtensionBeingActivated { + instance: LensExtension; + installedExtension: InstalledExtension; + activated: Promise; +} + export interface ExtensionLoading { isBundled: boolean; loaded: Promise; @@ -248,14 +257,84 @@ export class ExtensionLoader { }); } - protected async loadExtensions(installedExtensions: Map) { + protected async loadBundledExtensions() { + return this.dependencies.bundledExtensions + .map(extension => { + try { + const LensExtensionClass = extension[this.dependencies.extensionEntryPointName](); + + if (!LensExtensionClass) { + return null; + } + + const installedExtension: InstalledExtension = { + absolutePath: "irrelevant", + id: extension.manifest.name, + isBundled: true, + isCompatible: true, + isEnabled: true, + manifest: extension.manifest, + manifestPath: "irrelevant", + }; + const instance = this.dependencies.createExtensionInstance( + LensExtensionClass, + installedExtension, + ); + + this.dependencies.extensionInstances.set(extension.manifest.name, instance); + + return { + instance, + installedExtension, + activated: instance.activate(), + } as ExtensionBeingActivated; + } catch (err) { + this.dependencies.logger.error(`${logModule}: error loading extension`, { ext: extension, err }); + + return null; + } + }) + .filter(isDefined); + } + + protected async loadExtensions(extensions: ExtensionBeingActivated[]): Promise { + // We first need to wait until each extension's `onActivate` is resolved or rejected, + // as this might register new catalog categories. Afterwards we can safely .enable the extension. + await Promise.all( + extensions.map(extension => + // If extension activation fails, log error + extension.activated.catch((error) => { + this.dependencies.logger.error(`${logModule}: activation extension error`, { ext: extension.installedExtension, error }); + }), + ), + ); + + extensions.forEach(({ instance }) => { + const extension = this.dependencies.getExtension(instance); + + extension.register(); + }); + + return extensions.map(extension => { + const loaded = extension.instance.enable().catch((err) => { + this.dependencies.logger.error(`${logModule}: failed to enable`, { ext: extension, err }); + }); + + return { + isBundled: extension.installedExtension.isBundled, + loaded, + }; + }); + } + + protected async loadUserExtensions(installedExtensions: Map) { // Steps of the function: // 1. require and call .activate for each Extension // 2. Wait until every extension's onActivate has been resolved // 3. Call .enable for each extension // 4. Return ExtensionLoading[] - const extensions = [...installedExtensions.entries()] + return [...installedExtensions.entries()] .map(([extId, extension]) => { const alreadyInit = this.dependencies.extensionInstances.has(extId) || this.nonInstancesByName.has(extension.manifest.name); @@ -280,7 +359,7 @@ export class ExtensionLoader { instance, installedExtension: extension, activated: instance.activate(), - }; + } as ExtensionBeingActivated; } catch (err) { this.dependencies.logger.error(`${logModule}: error loading extension`, { ext: extension, err }); } @@ -290,52 +369,33 @@ export class ExtensionLoader { return null; }) - // Remove null values .filter(isDefined); - - // We first need to wait until each extension's `onActivate` is resolved or rejected, - // as this might register new catalog categories. Afterwards we can safely .enable the extension. - await Promise.all( - extensions.map(extension => - // If extension activation fails, log error - extension.activated.catch((error) => { - this.dependencies.logger.error(`${logModule}: activation extension error`, { ext: extension.installedExtension, error }); - }), - ), - ); - - extensions.forEach(({ instance }) => { - const extension = this.dependencies.getExtension(instance); - - extension.register(); - }); - - // Return ExtensionLoading[] - return extensions.map(extension => { - const loaded = extension.instance.enable().catch((err) => { - this.dependencies.logger.error(`${logModule}: failed to enable`, { ext: extension, err }); - }); - - return { - isBundled: extension.installedExtension.isBundled, - loaded, - }; - }); } - autoInitExtensions() { + async autoInitExtensions() { this.dependencies.logger.info(`${logModule}: auto initializing extensions`); - // Setup reaction to load extensions on JSON changes - reaction(() => this.toJSON(), installedExtensions => this.loadExtensions(installedExtensions)); + const bundledExtensions = await this.loadBundledExtensions(); + const userExtensions = await this.loadUserExtensions(this.toJSON()); + const loadedExtensions = await this.loadExtensions([ + ...bundledExtensions, + ...userExtensions, + ]); - // Load initial extensions - return this.loadExtensions(this.toJSON()); + // Setup reaction to load extensions on JSON changes + reaction(() => this.toJSON(), installedExtensions => { + void (async () => { + const userExtensions = await this.loadUserExtensions(installedExtensions); + + await this.loadExtensions(userExtensions); + })(); + }); + + return loadedExtensions; } protected requireExtension(extension: InstalledExtension): LensExtensionConstructor | null { - const entryPointName = ipcRenderer ? "renderer" : "main"; - const extRelativePath = extension.manifest[entryPointName]; + const extRelativePath = extension.manifest[this.dependencies.extensionEntryPointName]; if (!extRelativePath) { return null; @@ -348,7 +408,7 @@ export class ExtensionLoader { } catch (error) { const message = (error instanceof Error ? error.stack : undefined) || error; - this.dependencies.logger.error(`${logModule}: can't load ${entryPointName} for "${extension.manifest.name}": ${message}`, { extension }); + this.dependencies.logger.error(`${logModule}: can't load ${this.dependencies.extensionEntryPointName} for "${extension.manifest.name}": ${message}`, { extension }); } return null; diff --git a/src/main/extension-loader/entry-point-name.injectable.ts b/src/main/extension-loader/entry-point-name.injectable.ts new file mode 100644 index 0000000000..0d209211f2 --- /dev/null +++ b/src/main/extension-loader/entry-point-name.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 { extensionEntryPointNameInjectionToken } from "../../extensions/extension-loader/entry-point-name"; + +const extensionEntryPointNameInjectable = getInjectable({ + id: "extension-entry-point-name", + instantiate: () => "main" as const, + injectionToken: extensionEntryPointNameInjectionToken, +}); + +export default extensionEntryPointNameInjectable; diff --git a/src/renderer/extension-loader/entry-point-name.injectable.ts b/src/renderer/extension-loader/entry-point-name.injectable.ts new file mode 100644 index 0000000000..55315d6d73 --- /dev/null +++ b/src/renderer/extension-loader/entry-point-name.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 { extensionEntryPointNameInjectionToken } from "../../extensions/extension-loader/entry-point-name"; + +const extensionEntryPointNameInjectable = getInjectable({ + id: "extension-entry-point-name", + instantiate: () => "renderer" as const, + injectionToken: extensionEntryPointNameInjectionToken, +}); + +export default extensionEntryPointNameInjectable; From 6aee907dc5dcc1c68e802f3a9147df2727299b77 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Wed, 21 Dec 2022 09:57:53 -0800 Subject: [PATCH 3/4] Release 6.3.0 (#6804) Signed-off-by: Sebastian Malton Signed-off-by: Sebastian Malton --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8eeef8787d..9a02b8bbea 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "productName": "OpenLens", "description": "OpenLens - Open Source IDE for Kubernetes", "homepage": "https://github.com/lensapp/lens", - "version": "6.3.0-alpha.0", + "version": "6.3.0", "main": "static/build/main.js", "copyright": "© 2022 OpenLens Authors", "license": "MIT", From 6781b4657f1245545598a9a5b62a78871e047cc5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 21 Dec 2022 15:03:48 -0500 Subject: [PATCH 4/4] Update version to next preminor (#6810) Signed-off-by: GitHub Signed-off-by: GitHub Co-authored-by: Nokel81 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9a02b8bbea..225bc82cbe 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "productName": "OpenLens", "description": "OpenLens - Open Source IDE for Kubernetes", "homepage": "https://github.com/lensapp/lens", - "version": "6.3.0", + "version": "6.4.0-alpha.0", "main": "static/build/main.js", "copyright": "© 2022 OpenLens Authors", "license": "MIT",