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(" ");