diff --git a/lerna.json b/lerna.json index 789ba7a422c4..00d73a25c8b8 100644 --- a/lerna.json +++ b/lerna.json @@ -4,7 +4,7 @@ "packages": [ "packages/*" ], - "version": "6.4.5", + "version": "6.4.6", "npmClient": "yarn", "npmClientArgs": [ "--network-timeout=100000" diff --git a/packages/core/package.json b/packages/core/package.json index 27edf39fa04a..535c6e2d2f35 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -3,7 +3,7 @@ "productName": "", "description": "Lens Desktop Core", "homepage": "https://github.com/lensapp/lens", - "version": "6.4.5", + "version": "6.4.6", "repository": { "type": "git", "url": "git+https://github.com/lensapp/lens.git" diff --git a/packages/core/src/common/utils/enum.ts b/packages/core/src/common/utils/enum.ts new file mode 100644 index 000000000000..64106927c254 --- /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 69c1bea47ca9..28e55a073460 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 650a3b775291..6d84657a7938 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 000000000000..102cd9ded0bf --- /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 000000000000..e601d67abcd9 --- /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({ doGeneralOverrides: true }); + + 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 000000000000..923b4724cc9e --- /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 000000000000..f2490d7964ed --- /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({ doGeneralOverrides: true }); + + 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"); + }); + +}); diff --git a/packages/extension-api/package.json b/packages/extension-api/package.json index 1fec947ae800..115c215aeb92 100644 --- a/packages/extension-api/package.json +++ b/packages/extension-api/package.json @@ -2,7 +2,7 @@ "name": "@k8slens/extensions", "productName": "OpenLens extensions", "description": "OpenLens - Open Source Kubernetes IDE: extensions", - "version": "6.4.5", + "version": "6.4.6", "copyright": "© 2022 OpenLens Authors", "license": "MIT", "main": "dist/extension-api.js", @@ -26,7 +26,7 @@ "prepare:dev": "yarn run build" }, "dependencies": { - "@k8slens/core": "^6.4.5" + "@k8slens/core": "^6.4.6" }, "devDependencies": { "@types/node": "^16.18.6", diff --git a/packages/open-lens/package.json b/packages/open-lens/package.json index 49a81239aca9..4d591961d0ad 100644 --- a/packages/open-lens/package.json +++ b/packages/open-lens/package.json @@ -4,7 +4,7 @@ "productName": "OpenLens", "description": "OpenLens - Open Source IDE for Kubernetes", "homepage": "https://github.com/lensapp/lens", - "version": "6.4.5", + "version": "6.4.6", "repository": { "type": "git", "url": "git+https://github.com/lensapp/lens.git" @@ -192,7 +192,7 @@ } }, "dependencies": { - "@k8slens/core": "^6.4.5", + "@k8slens/core": "^6.4.6", "@k8slens/ensure-binaries": "^6.4.0-beta.16", "@k8slens/generate-tray-icons": "^6.4.0-beta.16", "@ogre-tools/fp": "^12.0.1",