From d5a124bd7925d8288f9db09150819cafc4b72be7 Mon Sep 17 00:00:00 2001 From: Juho Heikka Date: Thu, 9 Mar 2023 09:06:43 +0200 Subject: [PATCH] Copy detected metadata from cluster to catalog cluster (#7316) * Copy detected metadata from cluster to catalog cluster Signed-off-by: Juho Heikka * Remove duplicate copyright comment Signed-off-by: Juho Heikka * Lint fixes Signed-off-by: Juho Heikka * Typescript fix Signed-off-by: Juho Heikka --------- Signed-off-by: Juho Heikka --- packages/core/src/common/utils/enum.ts | 8 + .../src/main/cluster/manager.injectable.ts | 4 + packages/core/src/main/cluster/manager.ts | 43 +---- .../update-entity-metadata.injectable.ts | 42 +++++ .../cluster/update-entity-metadata.test.ts | 160 ++++++++++++++++++ .../cluster/update-entity-spec.injectable.ts | 41 +++++ .../main/cluster/update-entity-spec.test.ts | 154 +++++++++++++++++ 7 files changed, 415 insertions(+), 37 deletions(-) create mode 100644 packages/core/src/common/utils/enum.ts create mode 100644 packages/core/src/main/cluster/update-entity-metadata.injectable.ts create mode 100644 packages/core/src/main/cluster/update-entity-metadata.test.ts create mode 100644 packages/core/src/main/cluster/update-entity-spec.injectable.ts create mode 100644 packages/core/src/main/cluster/update-entity-spec.test.ts diff --git a/packages/core/src/common/utils/enum.ts b/packages/core/src/common/utils/enum.ts new file mode 100644 index 0000000000..64106927c2 --- /dev/null +++ b/packages/core/src/common/utils/enum.ts @@ -0,0 +1,8 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +export function enumKeys(obj: O): K[] { + return Object.keys(obj).filter(k => Number.isNaN(+k)) as K[]; +} diff --git a/packages/core/src/main/cluster/manager.injectable.ts b/packages/core/src/main/cluster/manager.injectable.ts index 69c1bea47c..28e55a0734 100644 --- a/packages/core/src/main/cluster/manager.injectable.ts +++ b/packages/core/src/main/cluster/manager.injectable.ts @@ -8,6 +8,8 @@ import loggerInjectable from "../../common/logger.injectable"; import catalogEntityRegistryInjectable from "../catalog/entity-registry.injectable"; import clustersThatAreBeingDeletedInjectable from "./are-being-deleted.injectable"; import { ClusterManager } from "./manager"; +import updateEntityMetadataInjectable from "./update-entity-metadata.injectable"; +import updateEntitySpecInjectable from "./update-entity-spec.injectable"; import visibleClusterInjectable from "./visible-cluster.injectable"; const clusterManagerInjectable = getInjectable({ @@ -19,6 +21,8 @@ const clusterManagerInjectable = getInjectable({ clustersThatAreBeingDeleted: di.inject(clustersThatAreBeingDeletedInjectable), visibleCluster: di.inject(visibleClusterInjectable), logger: di.inject(loggerInjectable), + updateEntityMetadata: di.inject(updateEntityMetadataInjectable), + updateEntitySpec: di.inject(updateEntitySpecInjectable), }), }); diff --git a/packages/core/src/main/cluster/manager.ts b/packages/core/src/main/cluster/manager.ts index 650a3b7752..6d84657a79 100644 --- a/packages/core/src/main/cluster/manager.ts +++ b/packages/core/src/main/cluster/manager.ts @@ -8,7 +8,6 @@ import type { IObservableValue, ObservableSet } from "mobx"; import { action, makeObservable, observe, reaction, toJS } from "mobx"; import type { Cluster } from "../../common/cluster/cluster"; import { isErrnoException } from "../../common/utils"; -import type { KubernetesClusterPrometheusMetrics } from "../../common/catalog-entities/kubernetes-cluster"; import { isKubernetesCluster, KubernetesCluster, LensKubernetesClusterStatus } from "../../common/catalog-entities/kubernetes-cluster"; import { ipcMainOn } from "../../common/ipc"; import { once } from "lodash"; @@ -16,6 +15,8 @@ import type { ClusterStore } from "../../common/cluster-store/cluster-store"; import type { ClusterId } from "../../common/cluster-types"; import type { CatalogEntityRegistry } from "../catalog"; import type { Logger } from "../../common/logger"; +import type { UpdateEntityMetadata } from "./update-entity-metadata.injectable"; +import type { UpdateEntitySpec } from "./update-entity-spec.injectable"; const logPrefix = "[CLUSTER-MANAGER]:"; @@ -27,6 +28,8 @@ interface Dependencies { readonly clustersThatAreBeingDeleted: ObservableSet; readonly visibleCluster: IObservableValue; readonly logger: Logger; + readonly updateEntityMetadata: UpdateEntityMetadata; + readonly updateEntitySpec: UpdateEntitySpec; } export class ClusterManager { @@ -97,42 +100,8 @@ export class ClusterManager { this.updateEntityStatus(entity, cluster); - entity.metadata.labels = { - ...entity.metadata.labels, - ...cluster.labels, - }; - entity.metadata.distro = cluster.distribution; - entity.metadata.kubeVersion = cluster.version; - - if (cluster.preferences?.clusterName) { - /** - * Only set the name if the it is overriden in preferences. If it isn't - * set then the name of the entity has been explicitly set by its source - */ - entity.metadata.name = cluster.preferences.clusterName; - } - - entity.spec.metrics ||= { source: "local" }; - - if (entity.spec.metrics.source === "local") { - const prometheus: KubernetesClusterPrometheusMetrics = entity.spec?.metrics?.prometheus || {}; - - prometheus.type = cluster.preferences.prometheusProvider?.type; - prometheus.address = cluster.preferences.prometheus; - entity.spec.metrics.prometheus = prometheus; - } - - if (cluster.preferences.icon) { - entity.spec.icon ??= {}; - entity.spec.icon.src = cluster.preferences.icon; - } else if (cluster.preferences.icon === null) { - /** - * NOTE: only clear the icon if set to `null` by ClusterIconSettings. - * We can then also clear that value too - */ - entity.spec.icon = undefined; - cluster.preferences.icon = undefined; - } + this.dependencies.updateEntityMetadata(entity, cluster); + this.dependencies.updateEntitySpec(entity, cluster); this.dependencies.catalogEntityRegistry.items.splice(index, 1, entity); } diff --git a/packages/core/src/main/cluster/update-entity-metadata.injectable.ts b/packages/core/src/main/cluster/update-entity-metadata.injectable.ts new file mode 100644 index 0000000000..102cd9ded0 --- /dev/null +++ b/packages/core/src/main/cluster/update-entity-metadata.injectable.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import type { KubernetesCluster } from "../../common/catalog-entities"; +import { ClusterMetadataKey } from "../../common/cluster-types"; +import type { Cluster } from "../../common/cluster/cluster"; +import { enumKeys } from "../../common/utils/enum"; + +export type UpdateEntityMetadata = (entity: KubernetesCluster, cluster: Cluster) => void; + +const updateEntityMetadataInjectable = getInjectable({ + id: "update-entity-metadata", + + instantiate: (): UpdateEntityMetadata => { + return (entity, cluster) => { + entity.metadata.labels = { + ...entity.metadata.labels, + ...cluster.labels, + }; + entity.metadata.distro = cluster.distribution; + entity.metadata.kubeVersion = cluster.version; + + enumKeys(ClusterMetadataKey).forEach((key) => { + const metadataKey = ClusterMetadataKey[key]; + + entity.metadata[metadataKey] = cluster.metadata[metadataKey]; + }); + + if (cluster.preferences?.clusterName) { + /** + * Only set the name if the it is overriden in preferences. If it isn't + * set then the name of the entity has been explicitly set by its source + */ + entity.metadata.name = cluster.preferences.clusterName; + } + }; + }, +}); + +export default updateEntityMetadataInjectable; diff --git a/packages/core/src/main/cluster/update-entity-metadata.test.ts b/packages/core/src/main/cluster/update-entity-metadata.test.ts new file mode 100644 index 0000000000..8583dc2702 --- /dev/null +++ b/packages/core/src/main/cluster/update-entity-metadata.test.ts @@ -0,0 +1,160 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { AppPaths } from "../../common/app-paths/app-path-injection-token"; +import appPathsStateInjectable from "../../common/app-paths/app-paths-state.injectable"; +import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; +import { KubernetesCluster } from "../../common/catalog-entities"; +import { ClusterMetadataKey } from "../../common/cluster-types"; +import type { Cluster } from "../../common/cluster/cluster"; +import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token"; +import { getDiForUnitTesting } from "../getDiForUnitTesting"; +import type { UpdateEntityMetadata } from "./update-entity-metadata.injectable"; +import updateEntityMetadataInjectable from "./update-entity-metadata.injectable"; + +describe("update-entity-metadata", () => { + let cluster: Cluster; + let entity: KubernetesCluster; + let updateEntityMetadata: UpdateEntityMetadata; + let detectedMetadata: Record; + + beforeEach(() => { + const di = getDiForUnitTesting(); + + di.override(directoryForUserDataInjectable, () => "/some-user-store-path"); + di.override(appPathsStateInjectable, () => ({ + get: () => ({} as AppPaths), + set: () => {}, + })); + const createCluster = di.inject(createClusterInjectionToken); + + updateEntityMetadata = di.inject(updateEntityMetadataInjectable); + + cluster = createCluster({ + id: "some-id", + contextName: "some-context", + kubeConfigPath: "minikube-config.yml", + }, { + clusterServerUrl: "foo", + }); + + detectedMetadata = { + [ClusterMetadataKey.CLUSTER_ID]: "some-cluster-id", + [ClusterMetadataKey.DISTRIBUTION]: "some-distribution", + [ClusterMetadataKey.VERSION]: "some-version", + [ClusterMetadataKey.LAST_SEEN]: "some-date", + [ClusterMetadataKey.NODES_COUNT]: 42, + [ClusterMetadataKey.PROMETHEUS]: { + "some-parameter": "some-value", + }, + }; + + cluster.metadata = { + ...cluster.metadata, + }; + + entity = new KubernetesCluster({ + metadata: { + uid: "some-uid", + name: "some-name", + labels: {}, + }, + spec: { + kubeconfigContext: "some-context", + kubeconfigPath: "/some/path/to/kubeconfig", + }, + status: { + phase: "connecting", + }, + }); + }); + + it("given cluster metadata has no some last seen timestamp, does not update entity metadata with last seen timestamp", () => { + updateEntityMetadata(entity, cluster); + expect(entity.metadata.lastSeen).toEqual(undefined); + }); + + it("given cluster metadata has some last seen timestamp, updates entity metadata with last seen timestamp", () => { + cluster.metadata[ClusterMetadataKey.LAST_SEEN] = detectedMetadata[ClusterMetadataKey.LAST_SEEN]; + updateEntityMetadata(entity, cluster); + expect(entity.metadata.lastSeen).toEqual("some-date"); + }); + + it("given cluster metadata has some version, updates entity metadata with version", () => { + cluster.metadata[ClusterMetadataKey.VERSION] = detectedMetadata[ClusterMetadataKey.VERSION]; + updateEntityMetadata(entity, cluster); + expect(entity.metadata.version).toEqual("some-version"); + }); + + it("given cluster metadata has nodes count, updates entity metadata with node count", () => { + cluster.metadata[ClusterMetadataKey.NODES_COUNT] = detectedMetadata[ClusterMetadataKey.NODES_COUNT]; + updateEntityMetadata(entity, cluster); + expect(entity.metadata.nodes).toEqual(42); + }); + + it("given cluster metadata has prometheus data, updates entity metadata with prometheus data", () => { + cluster.metadata[ClusterMetadataKey.PROMETHEUS] = detectedMetadata[ClusterMetadataKey.PROMETHEUS]; + updateEntityMetadata(entity, cluster); + expect(entity.metadata.prometheus).toEqual({ + "some-parameter": "some-value", + }); + }); + + it("given cluster metadata has distribution, updates entity metadata with distribution", () => { + cluster.metadata[ClusterMetadataKey.DISTRIBUTION] = detectedMetadata[ClusterMetadataKey.DISTRIBUTION]; + updateEntityMetadata(entity, cluster); + expect(entity.metadata.distribution).toEqual("some-distribution"); + }); + + it("given cluster metadata has cluster id, updates entity metadata with cluster id", () => { + cluster.metadata[ClusterMetadataKey.CLUSTER_ID] = detectedMetadata[ClusterMetadataKey.CLUSTER_ID]; + updateEntityMetadata(entity, cluster); + expect(entity.metadata.id).toEqual("some-cluster-id"); + }); + + it("given cluster metadata has no kubernetes version, updates entity metadata with 'unknown' kubernetes version", () => { + updateEntityMetadata(entity, cluster); + expect(entity.metadata.kubeVersion).toEqual("unknown"); + }); + + it("given cluster metadata has kubernetes version, updates entity metadata with kubernetes version", () => { + cluster.metadata.version = "some-kubernetes-version"; + updateEntityMetadata(entity, cluster); + expect(entity.metadata.kubeVersion).toEqual("some-kubernetes-version"); + }); + + it("given cluster has labels, updates entity metadata with labels", () => { + cluster.labels = { + "some-label": "some-value", + }; + entity.metadata.labels = { + "some-other-label": "some-other-value", + }; + updateEntityMetadata(entity, cluster); + expect(entity.metadata.labels).toEqual({ + "some-label": "some-value", + "some-other-label": "some-other-value", + }); + }); + + it("given cluster has labels, overwrites entity metadata with cluster labels", () => { + cluster.labels = { + "some-label": "some-cluster-value", + }; + entity.metadata.labels = { + "some-label": "some-entity-value", + }; + updateEntityMetadata(entity, cluster); + expect(entity.metadata.labels).toEqual({ + "some-label": "some-cluster-value", + }); + }); + + it("give cluster preferences has name, updates entity metadata with name", () => { + cluster.preferences.clusterName = "some-cluster-name"; + + updateEntityMetadata(entity, cluster); + expect(entity.metadata.name).toEqual("some-cluster-name"); + }); +}); diff --git a/packages/core/src/main/cluster/update-entity-spec.injectable.ts b/packages/core/src/main/cluster/update-entity-spec.injectable.ts new file mode 100644 index 0000000000..923b4724cc --- /dev/null +++ b/packages/core/src/main/cluster/update-entity-spec.injectable.ts @@ -0,0 +1,41 @@ +/** + * 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 { KubernetesCluster, KubernetesClusterPrometheusMetrics } from "../../common/catalog-entities"; +import type { Cluster } from "../../common/cluster/cluster"; + +export type UpdateEntitySpec = (entity: KubernetesCluster, cluster: Cluster) => void; + +const updateEntitySpecInjectable = getInjectable({ + id: "update-entity-spec", + + instantiate: (): UpdateEntitySpec => { + return (entity, cluster) => { + entity.spec.metrics ||= { source: "local" }; + + if (entity.spec.metrics.source === "local") { + const prometheus: KubernetesClusterPrometheusMetrics = entity.spec?.metrics?.prometheus || {}; + + prometheus.type = cluster.preferences.prometheusProvider?.type; + prometheus.address = cluster.preferences.prometheus; + entity.spec.metrics.prometheus = prometheus; + } + + if (cluster.preferences.icon) { + entity.spec.icon ??= {}; + entity.spec.icon.src = cluster.preferences.icon; + } else if (cluster.preferences.icon === null) { + /** + * NOTE: only clear the icon if set to `null` by ClusterIconSettings. + * We can then also clear that value too + */ + entity.spec.icon = undefined; + cluster.preferences.icon = undefined; + } + }; + }, +}); + +export default updateEntitySpecInjectable; diff --git a/packages/core/src/main/cluster/update-entity-spec.test.ts b/packages/core/src/main/cluster/update-entity-spec.test.ts new file mode 100644 index 0000000000..60be847ebe --- /dev/null +++ b/packages/core/src/main/cluster/update-entity-spec.test.ts @@ -0,0 +1,154 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { AppPaths } from "../../common/app-paths/app-path-injection-token"; +import appPathsStateInjectable from "../../common/app-paths/app-paths-state.injectable"; +import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; +import { KubernetesCluster } from "../../common/catalog-entities"; +import type { Cluster } from "../../common/cluster/cluster"; +import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token"; +import { getDiForUnitTesting } from "../getDiForUnitTesting"; +import type { UpdateEntitySpec } from "./update-entity-spec.injectable"; +import updateEntitySpecInjectable from "./update-entity-spec.injectable"; + +describe("update-entity-spec", () => { + let cluster: Cluster; + let entity: KubernetesCluster; + let updateEntitySpec: UpdateEntitySpec; + + beforeEach(() => { + const di = getDiForUnitTesting(); + + di.override(directoryForUserDataInjectable, () => "/some-user-store-path"); + di.override(appPathsStateInjectable, () => ({ + get: () => ({} as AppPaths), + set: () => {}, + })); + const createCluster = di.inject(createClusterInjectionToken); + + updateEntitySpec = di.inject(updateEntitySpecInjectable); + + cluster = createCluster({ + id: "some-id", + contextName: "some-context", + kubeConfigPath: "minikube-config.yml", + }, { + clusterServerUrl: "foo", + }); + + entity = new KubernetesCluster({ + metadata: { + uid: "some-uid", + name: "some-name", + labels: {}, + }, + spec: { + kubeconfigContext: "some-context", + kubeconfigPath: "/some/path/to/kubeconfig", + }, + status: { + phase: "connecting", + }, + }); + }); + + it("given cluster has icon, updates entity spec with icon", () => { + cluster.preferences.icon = "some-icon"; + updateEntitySpec(entity, cluster); + expect(entity.spec.icon?.src).toEqual("some-icon"); + }); + + it("given cluster icon is null, deletes icon from both", () => { + cluster.preferences.icon = null; + entity.spec.icon = { src : "some-icon" }; + + updateEntitySpec(entity, cluster); + expect(entity.spec.icon).toBeUndefined(); + expect(cluster.preferences.icon).toBeUndefined(); + }); + + it("given entity has no metrics, adds source as local", () => { + updateEntitySpec(entity, cluster); + expect(entity.spec.metrics?.source).toEqual("local"); + }); + + it("given entity has metrics, does not change source", () => { + entity.spec.metrics = { source: "some-source" }; + entity.spec.metrics.prometheus = { + address: { + namespace: "some-namespace", + port: 42, + service: "some-service", + prefix: "some-prefix", + }, + }; + + cluster.preferences.prometheus = { + namespace: "some-other-namespace", + port: 666, + service: "some-other-service", + prefix: "some-other-prefix", + }; + + updateEntitySpec(entity, cluster); + + expect(entity.spec.metrics?.source).toEqual("some-source"); + expect(entity.spec.metrics?.prometheus?.address).toEqual({ + namespace: "some-namespace", + port: 42, + service: "some-service", + prefix: "some-prefix", + }); + }); + + it("given entity has local prometheus source, updates entity spec with prometheus provider", () => { + entity.spec.metrics = { source: "local" }; + + cluster.preferences.prometheusProvider = { + type: "some-prometheus-provider-type", + }; + cluster.preferences.prometheus = { + namespace: "some-namespace", + port: 42, + service: "some-service", + prefix: "some-prefix", + }; + + updateEntitySpec(entity, cluster); + expect(entity.spec.metrics?.prometheus?.address).toEqual({ + namespace: "some-namespace", + port: 42, + service: "some-service", + prefix: "some-prefix", + }); + + expect(entity.spec.metrics?.prometheus?.type).toEqual("some-prometheus-provider-type"); + }); + + it("given entity has no metrics, updates entity spec with prometheus provider", () => { + expect(entity.spec.metrics).toBeUndefined(); + + cluster.preferences.prometheusProvider = { + type: "some-prometheus-provider-type", + }; + cluster.preferences.prometheus = { + namespace: "some-namespace", + port: 42, + service: "some-service", + prefix: "some-prefix", + }; + + updateEntitySpec(entity, cluster); + + expect(entity.spec.metrics?.prometheus?.address).toEqual({ + namespace: "some-namespace", + port: 42, + service: "some-service", + prefix: "some-prefix", + }); + + expect(entity.spec.metrics?.prometheus?.type).toEqual("some-prometheus-provider-type"); + }); + +});