From 8e65a0acd616bcf0769109b3b1c6c7e30ace8ab6 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Thu, 19 Jan 2023 06:34:31 -0800 Subject: [PATCH] Improve the injectability of cluster metadata detection (#6910) * Improve the injectability of cluster metadata detection - Remove unnecessary and complex base class Signed-off-by: Sebastian Malton * Remove dead code Signed-off-by: Sebastian Malton * Remove dead code Signed-off-by: Sebastian Malton Signed-off-by: Sebastian Malton --- src/common/cluster/cluster.ts | 92 +++++---- src/common/utils/type-narrowing.ts | 2 +- .../base-cluster-detector.ts | 25 --- ...luster-distribution-detector.injectable.ts | 141 +++++++++++++ .../cluster-id-detector.injectable.ts | 43 ++++ .../cluster-detectors/cluster-id-detector.ts | 31 --- .../cluster-last-seen-detector.injectable.ts | 32 +++ ...cluster-nodes-count-detector.injectable.ts | 40 ++++ .../cluster-version-detector.injectable.ts | 28 +++ .../create-version-detector.injectable.ts | 21 -- .../detect-cluster-metadata.injectable.ts | 51 +++++ .../detector-registry.injectable.ts | 16 -- .../cluster-detectors/detector-registry.ts | 55 ----- .../distribution-detector.ts | 189 ------------------ .../cluster-detectors/last-seen-detector.ts | 19 -- .../cluster-detectors/nodes-count-detector.ts | 24 --- .../request-cluster-version.injectable.ts | 22 ++ src/main/cluster-detectors/token.ts | 26 +++ .../cluster-detectors/version-detector.ts | 23 --- .../create-cluster.injectable.ts | 10 +- .../setup-detector-registry.injectable.ts | 36 ---- .../cluster/create-cluster.injectable.ts | 9 +- 22 files changed, 443 insertions(+), 492 deletions(-) delete mode 100644 src/main/cluster-detectors/base-cluster-detector.ts create mode 100644 src/main/cluster-detectors/cluster-distribution-detector.injectable.ts create mode 100644 src/main/cluster-detectors/cluster-id-detector.injectable.ts delete mode 100644 src/main/cluster-detectors/cluster-id-detector.ts create mode 100644 src/main/cluster-detectors/cluster-last-seen-detector.injectable.ts create mode 100644 src/main/cluster-detectors/cluster-nodes-count-detector.injectable.ts create mode 100644 src/main/cluster-detectors/cluster-version-detector.injectable.ts delete mode 100644 src/main/cluster-detectors/create-version-detector.injectable.ts create mode 100644 src/main/cluster-detectors/detect-cluster-metadata.injectable.ts delete mode 100644 src/main/cluster-detectors/detector-registry.injectable.ts delete mode 100644 src/main/cluster-detectors/detector-registry.ts delete mode 100644 src/main/cluster-detectors/distribution-detector.ts delete mode 100644 src/main/cluster-detectors/last-seen-detector.ts delete mode 100644 src/main/cluster-detectors/nodes-count-detector.ts create mode 100644 src/main/cluster-detectors/request-cluster-version.injectable.ts create mode 100644 src/main/cluster-detectors/token.ts delete mode 100644 src/main/cluster-detectors/version-detector.ts delete mode 100644 src/main/start-main-application/runnables/setup-detector-registry.injectable.ts diff --git a/src/common/cluster/cluster.ts b/src/common/cluster/cluster.ts index c68fd41c8a..c769865086 100644 --- a/src/common/cluster/cluster.ts +++ b/src/common/cluster/cluster.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { action, comparer, computed, makeObservable, observable, reaction, when } from "mobx"; +import { action, comparer, computed, makeObservable, observable, reaction, runInAction, when } from "mobx"; import type { ClusterContextHandler } from "../../main/context-handler/context-handler"; import type { KubeConfig } from "@kubernetes/client-node"; import { HttpError } from "@kubernetes/client-node"; @@ -11,8 +11,6 @@ import type { Kubectl } from "../../main/kubectl/kubectl"; import type { KubeconfigManager } from "../../main/kubeconfig-manager/kubeconfig-manager"; import type { KubeApiResource, KubeApiResourceDescriptor } from "../rbac"; import { formatKubeApiResource } from "../rbac"; -import type { VersionDetector } from "../../main/cluster-detectors/version-detector"; -import type { DetectorRegistry } from "../../main/cluster-detectors/detector-registry"; import plimit from "p-limit"; import type { ClusterState, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, KubeAuthUpdate, ClusterConfigData } from "../cluster-types"; import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus, clusterModelIdChecker, updateClusterModelChecker } from "../cluster-types"; @@ -27,11 +25,14 @@ 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 "../../main/cluster/request-api-resources.injectable"; +import type { DetectClusterMetadata } from "../../main/cluster-detectors/detect-cluster-metadata.injectable"; +import type { FalibleOnlyClusterMetadataDetector } from "../../main/cluster-detectors/token"; export interface ClusterDependencies { readonly directoryForKubeConfigs: string; readonly logger: Logger; - readonly detectorRegistry: DetectorRegistry; + readonly clusterVersionDetector: FalibleOnlyClusterMetadataDetector; + detectClusterMetadata: DetectClusterMetadata; createKubeconfigManager: (cluster: Cluster) => KubeconfigManager; createContextHandler: (cluster: Cluster) => ClusterContextHandler; createKubectl: (clusterVersion: string) => Kubectl; @@ -39,7 +40,6 @@ export interface ClusterDependencies { requestApiResources: RequestApiResources; requestNamespaceListPermissionsFor: RequestNamespaceListPermissionsFor; createListNamespaces: (config: KubeConfig) => ListNamespaces; - createVersionDetector: (cluster: Cluster) => VersionDetector; broadcastMessage: BroadcastMessage; loadConfigfromFile: LoadConfigfromFile; } @@ -444,57 +444,62 @@ export class Cluster implements ClusterModel { /** * @internal */ - @action + @action async refreshAccessibilityAndMetadata() { await this.refreshAccessibility(); await this.refreshMetadata(); } - /** - * @internal - */ - async refreshMetadata() { - this.dependencies.logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta()); - const metadata = await this.dependencies.detectorRegistry.detectForCluster(this); - const existingMetadata = this.metadata; + /** + * @internal + */ + async refreshMetadata() { + this.dependencies.logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta()); - this.metadata = Object.assign(existingMetadata, metadata); - } + const newMetadata = await this.dependencies.detectClusterMetadata(this); - /** - * @internal - */ - private async refreshAccessibility(): Promise { - this.dependencies.logger.info(`[CLUSTER]: refreshAccessibility`, this.getMeta()); - const proxyConfig = await this.getProxyKubeconfig(); - const canI = this.dependencies.createAuthorizationReview(proxyConfig); - const requestNamespaceListPermissions = this.dependencies.requestNamespaceListPermissionsFor(proxyConfig); + runInAction(() => { + this.metadata = { + ...this.metadata, + ...newMetadata, + }; + }); + } - this.isAdmin = await canI({ - namespace: "kube-system", - resource: "*", - verb: "create", - }); - this.isGlobalWatchEnabled = await canI({ - verb: "watch", - resource: "*", - }); - this.allowedNamespaces.replace(await this.requestAllowedNamespaces(proxyConfig)); - this.knownResources.replace(await this.dependencies.requestApiResources(this)); - this.allowedResources.replace(await this.getAllowedResources(requestNamespaceListPermissions)); - this.ready = true; - } + /** + * @internal + */ + private async refreshAccessibility(): Promise { + this.dependencies.logger.info(`[CLUSTER]: refreshAccessibility`, this.getMeta()); + const proxyConfig = await this.getProxyKubeconfig(); + const canI = this.dependencies.createAuthorizationReview(proxyConfig); + const requestNamespaceListPermissions = this.dependencies.requestNamespaceListPermissionsFor(proxyConfig); + + this.isAdmin = await canI({ + namespace: "kube-system", + resource: "*", + verb: "create", + }); + this.isGlobalWatchEnabled = await canI({ + verb: "watch", + resource: "*", + }); + this.allowedNamespaces.replace(await this.requestAllowedNamespaces(proxyConfig)); + this.knownResources.replace(await this.dependencies.requestApiResources(this)); + this.allowedResources.replace(await this.getAllowedResources(requestNamespaceListPermissions)); + this.ready = true; + } /** * @internal */ @action - async refreshConnectionStatus() { - const connectionStatus = await this.getConnectionStatus(); + async refreshConnectionStatus() { + const connectionStatus = await this.getConnectionStatus(); - this.online = connectionStatus > ClusterStatus.Offline; - this.accessible = connectionStatus == ClusterStatus.AccessGranted; - } + this.online = connectionStatus > ClusterStatus.Offline; + this.accessible = connectionStatus == ClusterStatus.AccessGranted; + } async getKubeconfig(): Promise { const { config } = await this.dependencies.loadConfigfromFile(this.kubeConfigPath); @@ -521,8 +526,7 @@ export class Cluster implements ClusterModel { protected async getConnectionStatus(): Promise { try { - const versionDetector = this.dependencies.createVersionDetector(this); - const versionData = await versionDetector.detect(); + const versionData = await this.dependencies.clusterVersionDetector.detect(this); this.metadata.version = versionData.value; diff --git a/src/common/utils/type-narrowing.ts b/src/common/utils/type-narrowing.ts index 41552c3136..68d5265237 100644 --- a/src/common/utils/type-narrowing.ts +++ b/src/common/utils/type-narrowing.ts @@ -130,7 +130,7 @@ export function isFunction(val: unknown): val is (...args: unknown[]) => unknown /** * Checks if the value in the second position is non-nullable */ -export function hasDefinedTupleValue(pair: [K, V | undefined | null]): pair is [K, V] { +export function hasDefinedTupleValue(pair: readonly [K, V | undefined | null]): pair is [K, V] { return pair[1] != null; } diff --git a/src/main/cluster-detectors/base-cluster-detector.ts b/src/main/cluster-detectors/base-cluster-detector.ts deleted file mode 100644 index 1aca321dfe..0000000000 --- a/src/main/cluster-detectors/base-cluster-detector.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { RequestPromiseOptions } from "request-promise-native"; -import type { Cluster } from "../../common/cluster/cluster"; -import type { K8sRequest } from "../k8s-request.injectable"; - -export interface ClusterDetectionResult { - value: string | number | boolean; - accuracy: number; -} - -export abstract class BaseClusterDetector { - abstract readonly key: string; - - constructor(public readonly cluster: Cluster, private _k8sRequest: K8sRequest) {} - - abstract detect(): Promise; - - protected async k8sRequest(path: string, options: RequestPromiseOptions = {}): Promise { - return this._k8sRequest(this.cluster, path, options); - } -} diff --git a/src/main/cluster-detectors/cluster-distribution-detector.injectable.ts b/src/main/cluster-detectors/cluster-distribution-detector.injectable.ts new file mode 100644 index 0000000000..7a13c62515 --- /dev/null +++ b/src/main/cluster-detectors/cluster-distribution-detector.injectable.ts @@ -0,0 +1,141 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { clusterMetadataDetectorInjectionToken } from "./token"; +import { ClusterMetadataKey } from "../../common/cluster-types"; +import { getInjectable } from "@ogre-tools/injectable"; +import k8SRequestInjectable from "../k8s-request.injectable"; +import type { Cluster } from "../../common/cluster/cluster"; +import requestClusterVersionInjectable from "./request-cluster-version.injectable"; + +const isGKE = (version: string) => version.includes("gke"); +const isEKS = (version: string) => version.includes("eks"); +const isIKS = (version: string) => version.includes("IKS"); +const isAKS = (cluster: Cluster) => cluster.apiUrl.includes("azmk8s.io"); +const isMirantis = (version: string) => version.includes("-mirantis-") || version.includes("-docker-"); +const isDigitalOcean = (cluster: Cluster) => cluster.apiUrl.endsWith("k8s.ondigitalocean.com"); +const isMinikube = (cluster: Cluster) => cluster.contextName.startsWith("minikube"); +const isMicrok8s = (cluster: Cluster) => cluster.contextName.startsWith("microk8s"); +const isKind = (cluster: Cluster) => cluster.contextName.startsWith("kubernetes-admin@kind-"); +const isDockerDesktop = (cluster: Cluster) => cluster.contextName === "docker-desktop"; +const isTke = (version: string) => version.includes("-tke."); +const isCustom = (version: string) => version.includes("+"); +const isVMWare = (version: string) => version.includes("+vmware"); +const isRke = (version: string) => version.includes("-rancher"); +const isRancherDesktop = (cluster: Cluster) => cluster.contextName === "rancher-desktop"; +const isK3s = (version: string) => version.includes("+k3s"); +const isK0s = (version: string) => version.includes("-k0s") || version.includes("+k0s"); +const isAlibaba = (version: string) => version.includes("-aliyun"); +const isHuawei = (version: string) => version.includes("-CCE"); + +const clusterDistributionDetectorInjectable = getInjectable({ + id: "cluster-distribution-detector", + instantiate: (di) => { + const k8sRequest = di.inject(k8SRequestInjectable); + const requestClusterVersion = di.inject(requestClusterVersionInjectable); + const isOpenshift = async (cluster: Cluster) => { + try { + const { paths = [] } = await k8sRequest(cluster, "") as { paths?: string[] }; + + return paths.includes("/apis/project.openshift.io"); + } catch (e) { + return false; + } + }; + + return { + key: ClusterMetadataKey.DISTRIBUTION, + detect: async (cluster) => { + const version = await requestClusterVersion(cluster); + + if (isRke(version)) { + return { value: "rke", accuracy: 80 }; + } + + if (isRancherDesktop(cluster)) { + return { value: "rancher-desktop", accuracy: 80 }; + } + + if (isK3s(version)) { + return { value: "k3s", accuracy: 80 }; + } + + if (isGKE(version)) { + return { value: "gke", accuracy: 80 }; + } + + if (isEKS(version)) { + return { value: "eks", accuracy: 80 }; + } + + if (isIKS(version)) { + return { value: "iks", accuracy: 80 }; + } + + if (isAKS(cluster)) { + return { value: "aks", accuracy: 80 }; + } + + if (isDigitalOcean(cluster)) { + return { value: "digitalocean", accuracy: 90 }; + } + + if (isK0s(version)) { + return { value: "k0s", accuracy: 80 }; + } + + if (isVMWare(version)) { + return { value: "vmware", accuracy: 90 }; + } + + if (isMirantis(version)) { + return { value: "mirantis", accuracy: 90 }; + } + + if (isAlibaba(version)) { + return { value: "alibaba", accuracy: 90 }; + } + + if (isHuawei(version)) { + return { value: "huawei", accuracy: 90 }; + } + + if (isTke(version)) { + return { value: "tencent", accuracy: 90 }; + } + + if (isMinikube(cluster)) { + return { value: "minikube", accuracy: 80 }; + } + + if (isMicrok8s(cluster)) { + return { value: "microk8s", accuracy: 80 }; + } + + if (isKind(cluster)) { + return { value: "kind", accuracy: 70 }; + } + + if (isDockerDesktop(cluster)) { + return { value: "docker-desktop", accuracy: 80 }; + } + + if (isCustom(version)) { + if (await isOpenshift(cluster)) { + return { value: "openshift", accuracy: 90 }; + } + + return { value: "custom", accuracy: 10 }; + } + + return { value: "unknown", accuracy: 10 }; + }, + }; + }, + injectionToken: clusterMetadataDetectorInjectionToken, +}); + +export default clusterDistributionDetectorInjectable; + diff --git a/src/main/cluster-detectors/cluster-id-detector.injectable.ts b/src/main/cluster-detectors/cluster-id-detector.injectable.ts new file mode 100644 index 0000000000..8d7a90fedc --- /dev/null +++ b/src/main/cluster-detectors/cluster-id-detector.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 { clusterMetadataDetectorInjectionToken } from "./token"; +import { createHash } from "crypto"; +import { ClusterMetadataKey } from "../../common/cluster-types"; +import { getInjectable } from "@ogre-tools/injectable"; +import k8SRequestInjectable from "../k8s-request.injectable"; +import type { Cluster } from "../../common/cluster/cluster"; + +const clusterIdDetectorFactoryInjectable = getInjectable({ + id: "cluster-id-detector-factory", + instantiate: (di) => { + const k8sRequest = di.inject(k8SRequestInjectable); + const getDefaultNamespaceId = async (cluster: Cluster) => { + const { metadata } = await k8sRequest(cluster, "/api/v1/namespaces/default") as { metadata: { uid: string }}; + + return metadata.uid; + }; + + return { + key: ClusterMetadataKey.CLUSTER_ID, + detect: async (cluster) => { + let id: string; + + try { + id = await getDefaultNamespaceId(cluster); + } catch(_) { + id = cluster.apiUrl; + } + const value = createHash("sha256").update(id).digest("hex"); + + return { value, accuracy: 100 }; + }, + }; + }, + injectionToken: clusterMetadataDetectorInjectionToken, +}); + +export default clusterIdDetectorFactoryInjectable; + diff --git a/src/main/cluster-detectors/cluster-id-detector.ts b/src/main/cluster-detectors/cluster-id-detector.ts deleted file mode 100644 index 8f1486fcb1..0000000000 --- a/src/main/cluster-detectors/cluster-id-detector.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { BaseClusterDetector } from "./base-cluster-detector"; -import { createHash } from "crypto"; -import { ClusterMetadataKey } from "../../common/cluster-types"; - -export class ClusterIdDetector extends BaseClusterDetector { - key = ClusterMetadataKey.CLUSTER_ID; - - public async detect() { - let id: string; - - try { - id = await this.getDefaultNamespaceId(); - } catch(_) { - id = this.cluster.apiUrl; - } - const value = createHash("sha256").update(id).digest("hex"); - - return { value, accuracy: 100 }; - } - - protected async getDefaultNamespaceId() { - const response = await this.k8sRequest("/api/v1/namespaces/default"); - - return response.metadata.uid; - } -} diff --git a/src/main/cluster-detectors/cluster-last-seen-detector.injectable.ts b/src/main/cluster-detectors/cluster-last-seen-detector.injectable.ts new file mode 100644 index 0000000000..64b05b22ab --- /dev/null +++ b/src/main/cluster-detectors/cluster-last-seen-detector.injectable.ts @@ -0,0 +1,32 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { clusterMetadataDetectorInjectionToken } from "./token"; +import { ClusterMetadataKey } from "../../common/cluster-types"; +import { getInjectable } from "@ogre-tools/injectable"; +import requestClusterVersionInjectable from "./request-cluster-version.injectable"; + +const clusterLastSeenDetectorInjectable = getInjectable({ + id: "cluster-last-seen-detector", + instantiate: (di) => { + const requestClusterVersion = di.inject(requestClusterVersionInjectable); + + return { + key: ClusterMetadataKey.LAST_SEEN, + detect: async (cluster) => { + try { + await requestClusterVersion(cluster); + + return { value: new Date().toJSON(), accuracy: 100 }; + } catch { + return null; + } + }, + }; + }, + injectionToken: clusterMetadataDetectorInjectionToken, +}); + +export default clusterLastSeenDetectorInjectable; diff --git a/src/main/cluster-detectors/cluster-nodes-count-detector.injectable.ts b/src/main/cluster-detectors/cluster-nodes-count-detector.injectable.ts new file mode 100644 index 0000000000..e9f0f91e65 --- /dev/null +++ b/src/main/cluster-detectors/cluster-nodes-count-detector.injectable.ts @@ -0,0 +1,40 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { clusterMetadataDetectorInjectionToken } from "./token"; +import { ClusterMetadataKey } from "../../common/cluster-types"; +import { getInjectable } from "@ogre-tools/injectable"; +import k8SRequestInjectable from "../k8s-request.injectable"; +import type { Cluster } from "../../common/cluster/cluster"; + +const clusterNodeCountDetectorInjectable = getInjectable({ + id: "cluster-node-count-detector", + instantiate: (di) => { + const k8sRequest = di.inject(k8SRequestInjectable); + const requestNodeCount = async (cluster: Cluster) => { + const { items } = await k8sRequest(cluster, "/api/v1/nodes") as { items: unknown[] }; + + return items.length; + }; + + return { + key: ClusterMetadataKey.NODES_COUNT, + detect: async (cluster) => { + try { + return { + value: await requestNodeCount(cluster), + accuracy: 1000, + }; + } catch { + return null; + } + }, + }; + }, + injectionToken: clusterMetadataDetectorInjectionToken, +}); + +export default clusterNodeCountDetectorInjectable; + diff --git a/src/main/cluster-detectors/cluster-version-detector.injectable.ts b/src/main/cluster-detectors/cluster-version-detector.injectable.ts new file mode 100644 index 0000000000..82d8c18fdc --- /dev/null +++ b/src/main/cluster-detectors/cluster-version-detector.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 { clusterMetadataDetectorInjectionToken } from "./token"; +import { ClusterMetadataKey } from "../../common/cluster-types"; +import { getInjectable } from "@ogre-tools/injectable"; +import requestClusterVersionInjectable from "./request-cluster-version.injectable"; + +const clusterVersionDetectorInjectable = getInjectable({ + id: "cluster-version-detector", + instantiate: (di) => { + const requestClusterVersion = di.inject(requestClusterVersionInjectable); + + return { + key: ClusterMetadataKey.VERSION, + detect: async (cluster) => ({ + value: await requestClusterVersion(cluster), + accuracy: 100, + }), + }; + }, + injectionToken: clusterMetadataDetectorInjectionToken, +}); + +export default clusterVersionDetectorInjectable; + diff --git a/src/main/cluster-detectors/create-version-detector.injectable.ts b/src/main/cluster-detectors/create-version-detector.injectable.ts deleted file mode 100644 index efd59dcfc3..0000000000 --- a/src/main/cluster-detectors/create-version-detector.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 { VersionDetector } from "./version-detector"; -import k8sRequestInjectable from "../k8s-request.injectable"; -import type { Cluster } from "../../common/cluster/cluster"; - -const createVersionDetectorInjectable = getInjectable({ - id: "create-version-detector", - - instantiate: (di) => { - const k8sRequest = di.inject(k8sRequestInjectable); - - return (cluster: Cluster) => - new VersionDetector(cluster, k8sRequest); - }, -}); - -export default createVersionDetectorInjectable; diff --git a/src/main/cluster-detectors/detect-cluster-metadata.injectable.ts b/src/main/cluster-detectors/detect-cluster-metadata.injectable.ts new file mode 100644 index 0000000000..3665f74f52 --- /dev/null +++ b/src/main/cluster-detectors/detect-cluster-metadata.injectable.ts @@ -0,0 +1,51 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { pipeline } from "@ogre-tools/fp"; +import { getInjectable } from "@ogre-tools/injectable"; +import { groupBy, reduce } from "lodash"; +import { filter, map } from "lodash/fp"; +import type { ClusterMetadata } from "../../common/cluster-types"; +import type { Cluster } from "../../common/cluster/cluster"; +import { hasDefinedTupleValue, isDefined, object } from "../../common/utils"; +import type { ClusterDetectionResult, ClusterMetadataDetector } from "./token"; +import { clusterMetadataDetectorInjectionToken } from "./token"; + +export type DetectClusterMetadata = (cluster: Cluster) => Promise; + +const pickHighestAccuracy = (prev: ClusterDetectionResult, curr: ClusterDetectionResult) => ( + prev.accuracy > curr.accuracy + ? prev + : curr +); + +const detectMetadataWithFor = (cluster: Cluster) => async (clusterMetadataDetector: ClusterMetadataDetector) => { + try { + return await clusterMetadataDetector.detect(cluster); + } catch { + return null; + } +}; + +const detectClusterMetadataInjectable = getInjectable({ + id: "detect-cluster-metadata", + instantiate: (di): DetectClusterMetadata => { + const clusterMetadataDetectors = di.injectMany(clusterMetadataDetectorInjectionToken); + + return async (cluster) => { + const entries = pipeline( + await Promise.all(clusterMetadataDetectors.map(detectMetadataWithFor(cluster))), + filter(isDefined), + (arg) => groupBy(arg, "key"), + (arg) => object.entries(arg), + map(([ key, results ]) => [key, reduce(results, pickHighestAccuracy)] as const), + filter(hasDefinedTupleValue), + ); + + return object.fromEntries(entries); + }; + }, +}); + +export default detectClusterMetadataInjectable; diff --git a/src/main/cluster-detectors/detector-registry.injectable.ts b/src/main/cluster-detectors/detector-registry.injectable.ts deleted file mode 100644 index 344e82003f..0000000000 --- a/src/main/cluster-detectors/detector-registry.injectable.ts +++ /dev/null @@ -1,16 +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 { DetectorRegistry } from "./detector-registry"; -import k8sRequestInjectable from "../k8s-request.injectable"; - -const detectorRegistryInjectable = getInjectable({ - id: "detector-registry", - - instantiate: (di) => - new DetectorRegistry({ k8sRequest: di.inject(k8sRequestInjectable) }), -}); - -export default detectorRegistryInjectable; diff --git a/src/main/cluster-detectors/detector-registry.ts b/src/main/cluster-detectors/detector-registry.ts deleted file mode 100644 index 395d819850..0000000000 --- a/src/main/cluster-detectors/detector-registry.ts +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { observable } from "mobx"; -import type { ClusterMetadata } from "../../common/cluster-types"; -import type { Cluster } from "../../common/cluster/cluster"; -import type { BaseClusterDetector, ClusterDetectionResult } from "./base-cluster-detector"; -import type { K8sRequest } from "../k8s-request.injectable"; - -interface Dependencies { - k8sRequest: K8sRequest; -} - -export type DetectorConstructor = new (cluster: Cluster, k8sRequest: K8sRequest) => BaseClusterDetector; - -export class DetectorRegistry { - constructor(private dependencies: Dependencies) {} - - registry = observable.array([], { deep: false }); - - add(detectorClass: DetectorConstructor): this { - this.registry.push(detectorClass); - - return this; - } - - async detectForCluster(cluster: Cluster): Promise { - const results: { [key: string]: ClusterDetectionResult } = {}; - - for (const detectorClass of this.registry) { - const detector = new detectorClass(cluster, this.dependencies.k8sRequest); - - try { - const data = await detector.detect(); - - if (!data) continue; - const existingValue = results[detector.key]; - - if (existingValue && existingValue.accuracy > data.accuracy) continue; // previous value exists and is more accurate - results[detector.key] = data; - } catch (e) { - // detector raised error, do nothing - } - } - const metadata: ClusterMetadata = {}; - - for (const [key, result] of Object.entries(results)) { - metadata[key] = result.value; - } - - return metadata; - } -} diff --git a/src/main/cluster-detectors/distribution-detector.ts b/src/main/cluster-detectors/distribution-detector.ts deleted file mode 100644 index dcdacfb253..0000000000 --- a/src/main/cluster-detectors/distribution-detector.ts +++ /dev/null @@ -1,189 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { BaseClusterDetector } from "./base-cluster-detector"; -import { ClusterMetadataKey } from "../../common/cluster-types"; - -export class DistributionDetector extends BaseClusterDetector { - key = ClusterMetadataKey.DISTRIBUTION; - - public async detect() { - const version = await this.getKubernetesVersion(); - - if (this.isRke(version)) { - return { value: "rke", accuracy: 80 }; - } - - if (this.isRancherDesktop()) { - return { value: "rancher-desktop", accuracy: 80 }; - } - - if (this.isK3s(version)) { - return { value: "k3s", accuracy: 80 }; - } - - if (this.isGKE(version)) { - return { value: "gke", accuracy: 80 }; - } - - if (this.isEKS(version)) { - return { value: "eks", accuracy: 80 }; - } - - if (this.isIKS(version)) { - return { value: "iks", accuracy: 80 }; - } - - if (this.isAKS()) { - return { value: "aks", accuracy: 80 }; - } - - if (this.isDigitalOcean()) { - return { value: "digitalocean", accuracy: 90 }; - } - - if (this.isK0s(version)) { - return { value: "k0s", accuracy: 80 }; - } - - if (this.isVMWare(version)) { - return { value: "vmware", accuracy: 90 }; - } - - if (this.isMirantis(version)) { - return { value: "mirantis", accuracy: 90 }; - } - - if (this.isAlibaba(version)) { - return { value: "alibaba", accuracy: 90 }; - } - - if (this.isHuawei(version)) { - return { value: "huawei", accuracy: 90 }; - } - - if (this.isTke(version)) { - return { value: "tencent", accuracy: 90 }; - } - - if (this.isMinikube()) { - return { value: "minikube", accuracy: 80 }; - } - - if (this.isMicrok8s()) { - return { value: "microk8s", accuracy: 80 }; - } - - if (this.isKind()) { - return { value: "kind", accuracy: 70 }; - } - - if (this.isDockerDesktop()) { - return { value: "docker-desktop", accuracy: 80 }; - } - - if (this.isCustom(version) && await this.isOpenshift()) { - return { value: "openshift", accuracy: 90 }; - } - - if (this.isCustom(version)) { - return { value: "custom", accuracy: 10 }; - } - - return { value: "unknown", accuracy: 10 }; - } - - public async getKubernetesVersion() { - const response = await this.k8sRequest("/version"); - - return response.gitVersion; - } - - protected isGKE(version: string) { - return version.includes("gke"); - } - - protected isEKS(version: string) { - return version.includes("eks"); - } - - protected isIKS(version: string) { - return version.includes("IKS"); - } - - protected isAKS() { - return this.cluster.apiUrl.includes("azmk8s.io"); - } - - protected isMirantis(version: string) { - return version.includes("-mirantis-") || version.includes("-docker-"); - } - - protected isDigitalOcean() { - return this.cluster.apiUrl.endsWith("k8s.ondigitalocean.com"); - } - - protected isMinikube() { - return this.cluster.contextName.startsWith("minikube"); - } - - protected isMicrok8s() { - return this.cluster.contextName.startsWith("microk8s"); - } - - protected isKind() { - return this.cluster.contextName.startsWith("kubernetes-admin@kind-"); - } - - protected isDockerDesktop() { - return this.cluster.contextName === "docker-desktop"; - } - - protected isTke(version: string) { - return version.includes("-tke."); - } - - protected isCustom(version: string) { - return version.includes("+"); - } - - protected isVMWare(version: string) { - return version.includes("+vmware"); - } - - protected isRke(version: string) { - return version.includes("-rancher"); - } - - protected isRancherDesktop() { - return this.cluster.contextName === "rancher-desktop"; - } - - protected isK3s(version: string) { - return version.includes("+k3s"); - } - - protected isK0s(version: string) { - return version.includes("-k0s") || version.includes("+k0s"); - } - - protected isAlibaba(version: string) { - return version.includes("-aliyun"); - } - - protected isHuawei(version: string) { - return version.includes("-CCE"); - } - - protected async isOpenshift() { - try { - const response = await this.k8sRequest(""); - - return response.paths?.includes("/apis/project.openshift.io"); - } catch (e) { - return false; - } - } -} diff --git a/src/main/cluster-detectors/last-seen-detector.ts b/src/main/cluster-detectors/last-seen-detector.ts deleted file mode 100644 index 087557f083..0000000000 --- a/src/main/cluster-detectors/last-seen-detector.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { BaseClusterDetector } from "./base-cluster-detector"; -import { ClusterMetadataKey } from "../../common/cluster-types"; - -export class LastSeenDetector extends BaseClusterDetector { - key = ClusterMetadataKey.LAST_SEEN; - - public async detect() { - if (!this.cluster.accessible) return null; - - await this.k8sRequest("/version"); - - return { value: new Date().toJSON(), accuracy: 100 }; - } -} diff --git a/src/main/cluster-detectors/nodes-count-detector.ts b/src/main/cluster-detectors/nodes-count-detector.ts deleted file mode 100644 index f9b3c02dbf..0000000000 --- a/src/main/cluster-detectors/nodes-count-detector.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { BaseClusterDetector } from "./base-cluster-detector"; -import { ClusterMetadataKey } from "../../common/cluster-types"; - -export class NodesCountDetector extends BaseClusterDetector { - key = ClusterMetadataKey.NODES_COUNT; - - public async detect() { - if (!this.cluster.accessible) return null; - const nodeCount = await this.getNodeCount(); - - return { value: nodeCount, accuracy: 100 }; - } - - protected async getNodeCount(): Promise { - const response = await this.k8sRequest("/api/v1/nodes"); - - return response.items.length; - } -} diff --git a/src/main/cluster-detectors/request-cluster-version.injectable.ts b/src/main/cluster-detectors/request-cluster-version.injectable.ts new file mode 100644 index 0000000000..3863e75a0c --- /dev/null +++ b/src/main/cluster-detectors/request-cluster-version.injectable.ts @@ -0,0 +1,22 @@ +/** + * 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 { Cluster } from "../../common/cluster/cluster"; +import k8SRequestInjectable from "../k8s-request.injectable"; + +const requestClusterVersionInjectable = getInjectable({ + id: "request-cluster-version", + instantiate: (di) => { + const k8sRequest = di.inject(k8SRequestInjectable); + + return async (cluster: Cluster) => { + const { gitVersion } = await k8sRequest(cluster, "/version") as { gitVersion: string }; + + return gitVersion; + }; + }, +}); + +export default requestClusterVersionInjectable; diff --git a/src/main/cluster-detectors/token.ts b/src/main/cluster-detectors/token.ts new file mode 100644 index 0000000000..fc9031c815 --- /dev/null +++ b/src/main/cluster-detectors/token.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 { getInjectionToken } from "@ogre-tools/injectable"; +import type { Cluster } from "../../common/cluster/cluster"; + +export interface ClusterDetectionResult { + value: string | number | boolean; + accuracy: number; +} + +export interface FalibleOnlyClusterMetadataDetector { + readonly key: string; + detect(cluster: Cluster): Promise; +} + +export interface ClusterMetadataDetector { + readonly key: string; + detect(cluster: Cluster): Promise; +} + +export const clusterMetadataDetectorInjectionToken = getInjectionToken({ + id: "cluster-metadata-detector-token", +}); diff --git a/src/main/cluster-detectors/version-detector.ts b/src/main/cluster-detectors/version-detector.ts deleted file mode 100644 index cc228734c5..0000000000 --- a/src/main/cluster-detectors/version-detector.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 { BaseClusterDetector } from "./base-cluster-detector"; -import { ClusterMetadataKey } from "../../common/cluster-types"; - -export class VersionDetector extends BaseClusterDetector { - key = ClusterMetadataKey.VERSION; - - public async detect() { - const version = await this.getKubernetesVersion(); - - return { value: version, accuracy: 100 }; - } - - public async getKubernetesVersion() { - const response = await this.k8sRequest("/version"); - - return response.gitVersion; - } -} diff --git a/src/main/create-cluster/create-cluster.injectable.ts b/src/main/create-cluster/create-cluster.injectable.ts index 760e8e2e75..79eee5a151 100644 --- a/src/main/create-cluster/create-cluster.injectable.ts +++ b/src/main/create-cluster/create-cluster.injectable.ts @@ -14,11 +14,11 @@ import authorizationReviewInjectable from "../../common/cluster/authorization-re import listNamespacesInjectable from "../../common/cluster/list-namespaces.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"; import broadcastMessageInjectable from "../../common/ipc/broadcast-message.injectable"; import loadConfigfromFileInjectable from "../../common/kube-helpers/load-config-from-file.injectable"; import requestNamespaceListPermissionsForInjectable from "../../common/cluster/request-namespace-list-permissions.injectable"; +import detectClusterMetadataInjectable from "../cluster-detectors/detect-cluster-metadata.injectable"; +import clusterVersionDetectorInjectable from "../cluster-detectors/cluster-version-detector.injectable"; const createClusterInjectable = getInjectable({ id: "create-cluster", @@ -26,6 +26,8 @@ const createClusterInjectable = getInjectable({ instantiate: (di) => { const dependencies: ClusterDependencies = { directoryForKubeConfigs: di.inject(directoryForKubeConfigsInjectable), + logger: di.inject(loggerInjectable), + clusterVersionDetector: di.inject(clusterVersionDetectorInjectable), createKubeconfigManager: di.inject(createKubeconfigManagerInjectable), createKubectl: di.inject(createKubectlInjectable), createContextHandler: di.inject(createContextHandlerInjectable), @@ -33,11 +35,9 @@ const createClusterInjectable = getInjectable({ requestNamespaceListPermissionsFor: di.inject(requestNamespaceListPermissionsForInjectable), requestApiResources: di.inject(createListApiResourcesInjectable), createListNamespaces: di.inject(listNamespacesInjectable), - logger: di.inject(loggerInjectable), - detectorRegistry: di.inject(detectorRegistryInjectable), - createVersionDetector: di.inject(createVersionDetectorInjectable), broadcastMessage: di.inject(broadcastMessageInjectable), loadConfigfromFile: di.inject(loadConfigfromFileInjectable), + detectClusterMetadata: di.inject(detectClusterMetadataInjectable), }; return (model, configData) => new Cluster(dependencies, model, configData); diff --git a/src/main/start-main-application/runnables/setup-detector-registry.injectable.ts b/src/main/start-main-application/runnables/setup-detector-registry.injectable.ts deleted file mode 100644 index 0e8b7ffba2..0000000000 --- a/src/main/start-main-application/runnables/setup-detector-registry.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 { ClusterIdDetector } from "../../cluster-detectors/cluster-id-detector"; -import { LastSeenDetector } from "../../cluster-detectors/last-seen-detector"; -import { VersionDetector } from "../../cluster-detectors/version-detector"; -import { DistributionDetector } from "../../cluster-detectors/distribution-detector"; -import { NodesCountDetector } from "../../cluster-detectors/nodes-count-detector"; -import detectorRegistryInjectable from "../../cluster-detectors/detector-registry.injectable"; -import { onLoadOfApplicationInjectionToken } from "../runnable-tokens/on-load-of-application-injection-token"; - -const setupDetectorRegistryInjectable = getInjectable({ - id: "setup-detector-registry", - - instantiate: (di) => { - const detectorRegistry = di.inject(detectorRegistryInjectable); - - return { - id: "setup-detector-registry", - run: () => { - detectorRegistry - .add(ClusterIdDetector) - .add(LastSeenDetector) - .add(VersionDetector) - .add(DistributionDetector) - .add(NodesCountDetector); - }, - }; - }, - - injectionToken: onLoadOfApplicationInjectionToken, -}); - -export default setupDetectorRegistryInjectable; diff --git a/src/renderer/cluster/create-cluster.injectable.ts b/src/renderer/cluster/create-cluster.injectable.ts index 385dfe8d66..ff8ea38fff 100644 --- a/src/renderer/cluster/create-cluster.injectable.ts +++ b/src/renderer/cluster/create-cluster.injectable.ts @@ -29,9 +29,12 @@ const createClusterInjectable = getInjectable({ createAuthorizationReview: () => { throw new Error("Tried to access back-end feature in front-end."); }, requestNamespaceListPermissionsFor: () => { 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."); }, - requestApiResources: ()=> { throw new Error("Tried to access back-end feature in front-end."); }, - detectorRegistry: undefined as never, - createVersionDetector: () => { throw new Error("Tried to access back-end feature in front-end."); }, + requestApiResources: () => { throw new Error("Tried to access back-end feature in front-end."); }, + detectClusterMetadata: () => { throw new Error("Tried to access back-end feature in front-end."); }, + clusterVersionDetector: { + detect: () => { throw new Error("Tried to access back-end feature in front-end."); }, + key: "irrelavent", + }, }; return (model, configData) => new Cluster(dependencies, model, configData);