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 60a4d01111..0000000000 --- a/src/common/cluster/request-api-resources.injectable.ts +++ /dev/null @@ -1,75 +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"; -import { pipeline } from "../utils/iter"; - -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); - - const requestApiVersions = async (cluster: Cluster) => (await k8sRequest(cluster, "/api") as V1APIVersions).versions; - const requestApisVersions = async (cluster: Cluster) => (await k8sRequest(cluster, "/apis") as V1APIGroupList).groups; - const limitingFor = (limit: plimit.Limit) => (fn: (...args: Args) => Res) => (...args: Args) => limit(() => fn(...args)); - const requestKubeApiResourcesFor = (cluster: Cluster) => async ({ group, path }: KubeResourceListGroup): Promise => { - const { resources } = await k8sRequest(cluster, path) as V1APIResourceList; - - return resources.map(resource => ({ - apiName: resource.name, - kind: resource.kind, - group, - namespaced: resource.namespaced, - })); - }; - - return async (cluster) => { - const requestKubeApiResources = requestKubeApiResourcesFor(cluster); - const withApiLimit = limitingFor(plimit(5)); - - try { - const resourceListGroups: KubeResourceListGroup[] = [ - ...(await requestApiVersions(cluster)) - .map(version => ({ - group: version, - path: `/api/${version}`, - })), - ...pipeline((await requestApisVersions(cluster)).values()) - .filterMap(group => group.preferredVersion?.groupVersion && ({ - group: group.name, - path: `/apis/${group.preferredVersion.groupVersion}`, - })), - ]; - - const resources = await Promise.all( - resourceListGroups - .map(withApiLimit(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/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/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..bace63a9c6 --- /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 { pipeline } 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 pipeline(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;