mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Restructure to monorepo (#6907)
* wip: restructure to monorepo Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * refactor create-release-pr to a package Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * build fixes Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * github workflow fixes Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix typo Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * add webpack-env types to core Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix github workflows Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * refactor/fix integration tests Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * lint fix Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * yarn run dev Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * eslint settings for vscode Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * move templates to right package Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * open-lens build fixes Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * integration test fix Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix nx task dependencies Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * use bash shell for unit tests in test workflow Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix test:unit for windows Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix win-ca webpack error in open-lens Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix win-ca webpack error in open-lens Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix build:app on windows Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * remove ELECTRON_BUILDER_EXTRA_ARGS Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * sync src/ from master Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * remove Makefile from core Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
1 parent
0066ca4ea2
commit
2657df2293
3321 files changed
+28699
-11447
No files matched your search
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 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 { observable } from "mobx";
|
||||
import type { ClusterId } from "../../common/cluster-types";
|
||||
|
||||
const clustersThatAreBeingDeletedInjectable = getInjectable({
|
||||
id: "clusters-that-are-being-deleted",
|
||||
instantiate: () => observable.set<ClusterId>(),
|
||||
});
|
||||
|
||||
export default clustersThatAreBeingDeletedInjectable;
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* 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 { onLoadOfApplicationInjectionToken } from "../start-main-application/runnable-tokens/on-load-of-application-injection-token";
|
||||
import clusterManagerInjectable from "./manager.injectable";
|
||||
|
||||
const initializeClusterManagerInjectable = getInjectable({
|
||||
id: "initialize-cluster-manager",
|
||||
instantiate: (di) => {
|
||||
const clusterManager = di.inject(clusterManagerInjectable);
|
||||
|
||||
return {
|
||||
id: "initialize-cluster-manager",
|
||||
run: () => {
|
||||
clusterManager.init();
|
||||
},
|
||||
};
|
||||
},
|
||||
injectionToken: onLoadOfApplicationInjectionToken,
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
export default initializeClusterManagerInjectable;
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* 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 clusterStoreInjectable from "../../common/cluster-store/cluster-store.injectable";
|
||||
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 visibleClusterInjectable from "./visible-cluster.injectable";
|
||||
|
||||
const clusterManagerInjectable = getInjectable({
|
||||
id: "cluster-manager",
|
||||
|
||||
instantiate: (di) => new ClusterManager({
|
||||
store: di.inject(clusterStoreInjectable),
|
||||
catalogEntityRegistry: di.inject(catalogEntityRegistryInjectable),
|
||||
clustersThatAreBeingDeleted: di.inject(clustersThatAreBeingDeletedInjectable),
|
||||
visibleCluster: di.inject(visibleClusterInjectable),
|
||||
logger: di.inject(loggerInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
export default clusterManagerInjectable;
|
||||
@@ -0,0 +1,289 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import "../../common/ipc/cluster";
|
||||
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";
|
||||
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";
|
||||
|
||||
const logPrefix = "[CLUSTER-MANAGER]:";
|
||||
|
||||
const lensSpecificClusterStatuses: Set<string> = new Set(Object.values(LensKubernetesClusterStatus));
|
||||
|
||||
interface Dependencies {
|
||||
readonly store: ClusterStore;
|
||||
readonly catalogEntityRegistry: CatalogEntityRegistry;
|
||||
readonly clustersThatAreBeingDeleted: ObservableSet<ClusterId>;
|
||||
readonly visibleCluster: IObservableValue<ClusterId | null>;
|
||||
readonly logger: Logger;
|
||||
}
|
||||
|
||||
export class ClusterManager {
|
||||
constructor(private readonly dependencies: Dependencies) {
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
init = once(() => {
|
||||
// reacting to every cluster's state change and total amount of items
|
||||
reaction(
|
||||
() => this.dependencies.store.clustersList.map(c => c.getState()),
|
||||
() => this.updateCatalog(this.dependencies.store.clustersList),
|
||||
{ fireImmediately: false },
|
||||
);
|
||||
|
||||
// reacting to every cluster's preferences change and total amount of items
|
||||
reaction(
|
||||
() => this.dependencies.store.clustersList.map(c => toJS(c.preferences)),
|
||||
() => this.updateCatalog(this.dependencies.store.clustersList),
|
||||
{ fireImmediately: false },
|
||||
);
|
||||
|
||||
reaction(
|
||||
() => this.dependencies.catalogEntityRegistry.filterItemsByPredicate(isKubernetesCluster),
|
||||
entities => this.syncClustersFromCatalog(entities),
|
||||
);
|
||||
|
||||
reaction(() => [
|
||||
this.dependencies.catalogEntityRegistry.filterItemsByPredicate(isKubernetesCluster),
|
||||
this.dependencies.visibleCluster.get(),
|
||||
] as const, ([entities, visibleCluster]) => {
|
||||
for (const entity of entities) {
|
||||
if (entity.getId() === visibleCluster) {
|
||||
entity.status.active = true;
|
||||
} else {
|
||||
entity.status.active = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
observe(this.dependencies.clustersThatAreBeingDeleted, change => {
|
||||
if (change.type === "add") {
|
||||
this.updateEntityStatus(this.dependencies.catalogEntityRegistry.findById(change.newValue) as KubernetesCluster);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMainOn("network:offline", this.onNetworkOffline);
|
||||
ipcMainOn("network:online", this.onNetworkOnline);
|
||||
});
|
||||
|
||||
@action
|
||||
protected updateCatalog(clusters: Cluster[]) {
|
||||
this.dependencies.logger.debug("[CLUSTER-MANAGER]: updating catalog from cluster store");
|
||||
|
||||
for (const cluster of clusters) {
|
||||
this.updateEntityFromCluster(cluster);
|
||||
}
|
||||
}
|
||||
|
||||
protected updateEntityFromCluster(cluster: Cluster) {
|
||||
const index = this.dependencies.catalogEntityRegistry.items.findIndex((entity) => entity.getId() === cluster.id);
|
||||
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entity = this.dependencies.catalogEntityRegistry.items[index] as KubernetesCluster;
|
||||
|
||||
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.catalogEntityRegistry.items.splice(index, 1, entity);
|
||||
}
|
||||
|
||||
@action
|
||||
protected updateEntityStatus(entity: KubernetesCluster, cluster?: Cluster) {
|
||||
if (this.dependencies.clustersThatAreBeingDeleted.has(entity.getId())) {
|
||||
entity.status.phase = LensKubernetesClusterStatus.DELETING;
|
||||
entity.status.enabled = false;
|
||||
} else {
|
||||
entity.status.phase = (() => {
|
||||
if (!cluster) {
|
||||
this.dependencies.logger.silly(`${logPrefix} setting entity ${entity.getName()} to DISCONNECTED, reason="no cluster"`);
|
||||
|
||||
return LensKubernetesClusterStatus.DISCONNECTED;
|
||||
}
|
||||
|
||||
if (cluster.accessible) {
|
||||
this.dependencies.logger.silly(`${logPrefix} setting entity ${entity.getName()} to CONNECTED, reason="cluster is accessible"`);
|
||||
|
||||
return LensKubernetesClusterStatus.CONNECTED;
|
||||
}
|
||||
|
||||
if (!cluster.disconnected) {
|
||||
this.dependencies.logger.silly(`${logPrefix} setting entity ${entity.getName()} to CONNECTING, reason="cluster is not disconnected"`);
|
||||
|
||||
return LensKubernetesClusterStatus.CONNECTING;
|
||||
}
|
||||
|
||||
// Extensions are not allowed to use the Lens specific status phases
|
||||
if (!lensSpecificClusterStatuses.has(entity?.status?.phase)) {
|
||||
this.dependencies.logger.silly(`${logPrefix} not clearing entity ${entity.getName()} status, reason="custom string"`);
|
||||
|
||||
return entity.status.phase;
|
||||
}
|
||||
|
||||
this.dependencies.logger.silly(`${logPrefix} setting entity ${entity.getName()} to DISCONNECTED, reason="fallthrough"`);
|
||||
|
||||
return LensKubernetesClusterStatus.DISCONNECTED;
|
||||
})();
|
||||
|
||||
entity.status.enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
protected syncClustersFromCatalog(entities: KubernetesCluster[]) {
|
||||
for (const entity of entities) {
|
||||
const cluster = this.dependencies.store.getById(entity.getId());
|
||||
|
||||
if (!cluster) {
|
||||
const model = {
|
||||
id: entity.getId(),
|
||||
kubeConfigPath: entity.spec.kubeconfigPath,
|
||||
contextName: entity.spec.kubeconfigContext,
|
||||
accessibleNamespaces: entity.spec.accessibleNamespaces ?? [],
|
||||
};
|
||||
|
||||
try {
|
||||
/**
|
||||
* Add the bare minimum of data to ClusterStore. And especially no
|
||||
* preferences, as those might be configured by the entity's source
|
||||
*/
|
||||
this.dependencies.store.addCluster(model);
|
||||
} catch (error) {
|
||||
if (isErrnoException(error) && error.code === "ENOENT" && error.path === entity.spec.kubeconfigPath) {
|
||||
this.dependencies.logger.warn(`${logPrefix} kubeconfig file disappeared`, model);
|
||||
} else {
|
||||
this.dependencies.logger.error(`${logPrefix} failed to add cluster: ${error}`, model);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cluster.kubeConfigPath = entity.spec.kubeconfigPath;
|
||||
cluster.contextName = entity.spec.kubeconfigContext;
|
||||
|
||||
if (entity.spec.accessibleNamespaces) {
|
||||
cluster.accessibleNamespaces.replace(entity.spec.accessibleNamespaces);
|
||||
}
|
||||
|
||||
if (entity.spec.metrics) {
|
||||
const { source, prometheus } = entity.spec.metrics;
|
||||
|
||||
if (source !== "local" && prometheus) {
|
||||
const { type, address } = prometheus;
|
||||
|
||||
if (type) {
|
||||
cluster.preferences.prometheusProvider = { type };
|
||||
}
|
||||
|
||||
if (address) {
|
||||
cluster.preferences.prometheus = address;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.updateEntityFromCluster(cluster);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected onNetworkOffline = () => {
|
||||
this.dependencies.logger.info(`${logPrefix} network is offline`);
|
||||
this.dependencies.store.clustersList.forEach((cluster) => {
|
||||
if (!cluster.disconnected) {
|
||||
cluster.online = false;
|
||||
cluster.accessible = false;
|
||||
cluster.refreshConnectionStatus().catch((e) => e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
protected onNetworkOnline = () => {
|
||||
this.dependencies.logger.info(`${logPrefix} network is online`);
|
||||
this.dependencies.store.clustersList.forEach((cluster) => {
|
||||
if (!cluster.disconnected) {
|
||||
cluster.refreshConnectionStatus().catch((e) => e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
stop() {
|
||||
this.dependencies.store.clusters.forEach((cluster: Cluster) => {
|
||||
cluster.disconnect();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function catalogEntityFromCluster(cluster: Cluster) {
|
||||
return new KubernetesCluster({
|
||||
metadata: {
|
||||
uid: cluster.id,
|
||||
name: cluster.name,
|
||||
source: "local",
|
||||
labels: {
|
||||
...cluster.labels,
|
||||
},
|
||||
distro: cluster.distribution,
|
||||
kubeVersion: cluster.version,
|
||||
},
|
||||
spec: {
|
||||
kubeconfigPath: cluster.kubeConfigPath,
|
||||
kubeconfigContext: cluster.contextName,
|
||||
icon: {},
|
||||
},
|
||||
status: {
|
||||
phase: cluster.disconnected
|
||||
? LensKubernetesClusterStatus.DISCONNECTED
|
||||
: LensKubernetesClusterStatus.CONNECTED,
|
||||
reason: "",
|
||||
message: "",
|
||||
active: !cluster.disconnected,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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<KubeApiResource[]>;
|
||||
|
||||
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;
|
||||
@@ -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<KubeResourceListGroup[]>;
|
||||
|
||||
export const requestApiVersionsInjectionToken = getInjectionToken<RequestApiVersions>({
|
||||
id: "request-api-versions-token",
|
||||
});
|
||||
@@ -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: "",
|
||||
path: `/api/${version}`,
|
||||
}));
|
||||
};
|
||||
},
|
||||
injectionToken: requestApiVersionsInjectionToken,
|
||||
});
|
||||
|
||||
export default requestCoreApiVersionsInjectable;
|
||||
@@ -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<KubeApiResource[]>;
|
||||
|
||||
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;
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { V1APIGroupList } from "@kubernetes/client-node";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { chain } from "../../common/utils/iter";
|
||||
import k8sRequestInjectable from "../k8s-request.injectable";
|
||||
import { requestApiVersionsInjectionToken } from "./request-api-versions";
|
||||
|
||||
const requestNonCoreApiVersionsInjectable = getInjectable({
|
||||
id: "request-non-core-api-versions",
|
||||
instantiate: (di) => {
|
||||
const k8sRequest = di.inject(k8sRequestInjectable);
|
||||
|
||||
return async (cluster) => {
|
||||
const { groups } = await k8sRequest(cluster, "/apis") as V1APIGroupList;
|
||||
|
||||
return chain(groups.values())
|
||||
.filterMap(group => group.preferredVersion?.groupVersion && ({
|
||||
group: group.name,
|
||||
path: `/apis/${group.preferredVersion.groupVersion}`,
|
||||
}))
|
||||
.collect(v => [...v]);
|
||||
};
|
||||
},
|
||||
injectionToken: requestApiVersionsInjectionToken,
|
||||
});
|
||||
|
||||
export default requestNonCoreApiVersionsInjectable;
|
||||
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
// Move embedded kubeconfig into separate file and add reference to it to cluster settings
|
||||
// convert file path cluster icons to their base64 encoded versions
|
||||
|
||||
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import getCustomKubeConfigFilePathInjectable from "../../../common/app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable";
|
||||
import type { ClusterModel } from "../../../common/cluster-types";
|
||||
import readFileSyncInjectable from "../../../common/fs/read-file-sync.injectable";
|
||||
import { loadConfigFromString } from "../../../common/kube-helpers";
|
||||
import joinPathsInjectable from "../../../common/path/join-paths.injectable";
|
||||
|
||||
interface Pre360ClusterModel extends ClusterModel {
|
||||
kubeConfig?: string;
|
||||
}
|
||||
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { clusterStoreMigrationInjectionToken } from "../../../common/cluster-store/migration-token";
|
||||
import readFileBufferSyncInjectable from "../../../common/fs/read-file-buffer-sync.injectable";
|
||||
import loggerInjectable from "../../../common/logger.injectable";
|
||||
import writeFileSyncInjectable from "../../../common/fs/write-file-sync.injectable";
|
||||
|
||||
const v360Beta1ClusterStoreMigrationInjectable = getInjectable({
|
||||
id: "v3.6.0-beta.1-cluster-store-migration",
|
||||
instantiate: (di) => {
|
||||
const userDataPath = di.inject(directoryForUserDataInjectable);
|
||||
const getCustomKubeConfigDirectory = di.inject(getCustomKubeConfigFilePathInjectable);
|
||||
const readFileSync = di.inject(readFileSyncInjectable);
|
||||
const readFileBufferSync = di.inject(readFileBufferSyncInjectable);
|
||||
const joinPaths = di.inject(joinPathsInjectable);
|
||||
const logger = di.inject(loggerInjectable);
|
||||
const writeFileSync = di.inject(writeFileSyncInjectable);
|
||||
|
||||
return {
|
||||
version: "3.6.0-beta.1",
|
||||
run: (store) => {
|
||||
const storedClusters = (store.get("clusters") ?? []) as Pre360ClusterModel[];
|
||||
const migratedClusters: ClusterModel[] = [];
|
||||
|
||||
logger.info("Number of clusters to migrate: ", storedClusters.length);
|
||||
|
||||
for (const clusterModel of storedClusters) {
|
||||
/**
|
||||
* migrate kubeconfig
|
||||
*/
|
||||
try {
|
||||
const absPath = getCustomKubeConfigDirectory(clusterModel.id);
|
||||
|
||||
if (!clusterModel.kubeConfig) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// take the embedded kubeconfig and dump it into a file
|
||||
writeFileSync(absPath, clusterModel.kubeConfig);
|
||||
|
||||
clusterModel.kubeConfigPath = absPath;
|
||||
clusterModel.contextName = loadConfigFromString(readFileSync(absPath)).config.getCurrentContext();
|
||||
delete clusterModel.kubeConfig;
|
||||
|
||||
} catch (error) {
|
||||
logger.info(`Failed to migrate Kubeconfig for cluster "${clusterModel.id}", removing clusterModel...`, error);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
/**
|
||||
* migrate cluster icon
|
||||
*/
|
||||
try {
|
||||
if (clusterModel.preferences?.icon) {
|
||||
logger.info(`migrating ${clusterModel.preferences.icon} for ${clusterModel.preferences.clusterName}`);
|
||||
const iconPath = clusterModel.preferences.icon.replace("store://", "");
|
||||
const fileData = readFileBufferSync(joinPaths(userDataPath, iconPath));
|
||||
|
||||
clusterModel.preferences.icon = `data:;base64,${fileData.toString("base64")}`;
|
||||
} else {
|
||||
delete clusterModel.preferences?.icon;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
logger.info(`Failed to migrate cluster icon for cluster "${clusterModel.id}"`, error);
|
||||
delete clusterModel.preferences?.icon;
|
||||
}
|
||||
|
||||
migratedClusters.push(clusterModel);
|
||||
}
|
||||
|
||||
store.set("clusters", migratedClusters);
|
||||
},
|
||||
};
|
||||
},
|
||||
injectionToken: clusterStoreMigrationInjectionToken,
|
||||
});
|
||||
|
||||
export default v360Beta1ClusterStoreMigrationInjectable;
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* 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 directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import { clusterStoreMigrationInjectionToken } from "../../../common/cluster-store/migration-token";
|
||||
import type { ClusterModel } from "../../../common/cluster-types";
|
||||
import readJsonSyncInjectable from "../../../common/fs/read-json-sync.injectable";
|
||||
import joinPathsInjectable from "../../../common/path/join-paths.injectable";
|
||||
import { isErrnoException } from "../../../common/utils";
|
||||
|
||||
interface Pre500WorkspaceStoreModel {
|
||||
workspaces: {
|
||||
id: string;
|
||||
name: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
const v500Beta10ClusterStoreMigrationInjectable = getInjectable({
|
||||
id: "v5.0.0-beta.10-cluster-store-migration",
|
||||
instantiate: (di) => {
|
||||
const userDataPath = di.inject(directoryForUserDataInjectable);
|
||||
const joinPaths = di.inject(joinPathsInjectable);
|
||||
const readJsonSync = di.inject(readJsonSyncInjectable);
|
||||
|
||||
return {
|
||||
version: "5.0.0-beta.10",
|
||||
run(store) {
|
||||
try {
|
||||
const workspaceData: Pre500WorkspaceStoreModel = readJsonSync(joinPaths(userDataPath, "lens-workspace-store.json"));
|
||||
const workspaces = new Map<string, string>(); // mapping from WorkspaceId to name
|
||||
|
||||
for (const { id, name } of workspaceData.workspaces) {
|
||||
workspaces.set(id, name);
|
||||
}
|
||||
|
||||
const clusters = (store.get("clusters") ?? []) as ClusterModel[];
|
||||
|
||||
for (const cluster of clusters) {
|
||||
if (cluster.workspace) {
|
||||
const workspace = workspaces.get(cluster.workspace);
|
||||
|
||||
if (workspace) {
|
||||
(cluster.labels ??= {}).workspace = workspace;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
store.set("clusters", clusters);
|
||||
} catch (error) {
|
||||
// KLUDGE: remove after https://github.com/streamich/memfs/pull/893 is released
|
||||
if (process.env.JEST_WORKER_ID && (error as any).code === "ENOENT") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isErrnoException(error) && error.code === "ENOENT" && error.path?.endsWith("lens-workspace-store.json")) {
|
||||
// ignore lens-workspace-store.json being missing
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
injectionToken: clusterStoreMigrationInjectionToken,
|
||||
});
|
||||
|
||||
export default v500Beta10ClusterStoreMigrationInjectable;
|
||||
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { ClusterModel, ClusterPreferences, ClusterPrometheusPreferences } from "../../../common/cluster-types";
|
||||
import { moveSync, removeSync } from "fs-extra";
|
||||
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import { isDefined } from "../../../common/utils";
|
||||
import joinPathsInjectable from "../../../common/path/join-paths.injectable";
|
||||
|
||||
function mergePrometheusPreferences(left: ClusterPrometheusPreferences, right: ClusterPrometheusPreferences): ClusterPrometheusPreferences {
|
||||
if (left.prometheus && left.prometheusProvider) {
|
||||
return {
|
||||
prometheus: left.prometheus,
|
||||
prometheusProvider: left.prometheusProvider,
|
||||
};
|
||||
}
|
||||
|
||||
if (right.prometheus && right.prometheusProvider) {
|
||||
return {
|
||||
prometheus: right.prometheus,
|
||||
prometheusProvider: right.prometheusProvider,
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
function mergePreferences(left: ClusterPreferences, right: ClusterPreferences): ClusterPreferences {
|
||||
return {
|
||||
terminalCWD: left.terminalCWD || right.terminalCWD || undefined,
|
||||
clusterName: left.clusterName || right.clusterName || undefined,
|
||||
iconOrder: left.iconOrder || right.iconOrder || undefined,
|
||||
icon: left.icon || right.icon || undefined,
|
||||
httpsProxy: left.httpsProxy || right.httpsProxy || undefined,
|
||||
hiddenMetrics: mergeSet(left.hiddenMetrics ?? [], right.hiddenMetrics ?? []),
|
||||
...mergePrometheusPreferences(left, right),
|
||||
};
|
||||
}
|
||||
|
||||
function mergeLabels(left: Record<string, string>, right: Record<string, string>): Record<string, string> {
|
||||
return {
|
||||
...right,
|
||||
...left,
|
||||
};
|
||||
}
|
||||
|
||||
function mergeSet(...iterables: Iterable<string | undefined>[]): string[] {
|
||||
const res = new Set<string>();
|
||||
|
||||
for (const iterable of iterables) {
|
||||
for (const val of iterable) {
|
||||
if (val) {
|
||||
res.add(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [...res];
|
||||
}
|
||||
|
||||
function mergeClusterModel(prev: ClusterModel, right: Omit<ClusterModel, "id">): ClusterModel {
|
||||
return {
|
||||
id: prev.id,
|
||||
kubeConfigPath: prev.kubeConfigPath,
|
||||
contextName: prev.contextName,
|
||||
preferences: mergePreferences(prev.preferences ?? {}, right.preferences ?? {}),
|
||||
metadata: prev.metadata,
|
||||
labels: mergeLabels(prev.labels ?? {}, right.labels ?? {}),
|
||||
accessibleNamespaces: mergeSet(prev.accessibleNamespaces ?? [], right.accessibleNamespaces ?? []),
|
||||
workspace: prev.workspace || right.workspace,
|
||||
workspaces: mergeSet([prev.workspace, right.workspace], prev.workspaces ?? [], right.workspaces ?? []),
|
||||
};
|
||||
}
|
||||
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import loggerInjectable from "../../../common/logger.injectable";
|
||||
import { clusterStoreMigrationInjectionToken } from "../../../common/cluster-store/migration-token";
|
||||
import { generateNewIdFor } from "../../../common/utils/generate-new-id-for";
|
||||
|
||||
const v500Beta13ClusterStoreMigrationInjectable = getInjectable({
|
||||
id: "v5.0.0-beta.13-cluster-store-migration",
|
||||
instantiate: (di) => {
|
||||
const userDataPath = di.inject(directoryForUserDataInjectable);
|
||||
const joinPaths = di.inject(joinPathsInjectable);
|
||||
const logger = di.inject(loggerInjectable);
|
||||
|
||||
const moveStorageFolder = ({ folder, newId, oldId }: { folder: string; newId: string; oldId: string }): void => {
|
||||
const oldPath = joinPaths(folder, `${oldId}.json`);
|
||||
const newPath = joinPaths(folder, `${newId}.json`);
|
||||
|
||||
try {
|
||||
moveSync(oldPath, newPath);
|
||||
} catch (error) {
|
||||
if (String(error).includes("dest already exists")) {
|
||||
logger.info(`Multiple old lens-local-storage files for newId=${newId}. Removing ${oldId}.json`);
|
||||
removeSync(oldPath);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
version: "5.0.0-beta.13",
|
||||
run(store) {
|
||||
const folder = joinPaths(userDataPath, "lens-local-storage");
|
||||
const oldClusters = (store.get("clusters") ?? []) as ClusterModel[];
|
||||
const clusters = new Map<string, ClusterModel>();
|
||||
|
||||
for (const { id: oldId, ...cluster } of oldClusters) {
|
||||
const newId = generateNewIdFor(cluster);
|
||||
const newCluster = clusters.get(newId);
|
||||
|
||||
if (newCluster) {
|
||||
logger.info(`Duplicate entries for ${newId}`, { oldId });
|
||||
clusters.set(newId, mergeClusterModel(newCluster, cluster));
|
||||
} else {
|
||||
logger.info(`First entry for ${newId}`, { oldId });
|
||||
clusters.set(newId, {
|
||||
...cluster,
|
||||
id: newId,
|
||||
workspaces: [cluster.workspace].filter(isDefined),
|
||||
});
|
||||
moveStorageFolder({ folder, newId, oldId });
|
||||
}
|
||||
}
|
||||
|
||||
store.set("clusters", [...clusters.values()]);
|
||||
},
|
||||
};
|
||||
},
|
||||
injectionToken: clusterStoreMigrationInjectionToken,
|
||||
});
|
||||
|
||||
export default v500Beta13ClusterStoreMigrationInjectable;
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
// Fix embedded kubeconfig paths under snap config
|
||||
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import applicationInformationToken from "../../../common/vars/application-information-token";
|
||||
import { clusterStoreMigrationInjectionToken } from "../../../common/cluster-store/migration-token";
|
||||
import loggerInjectable from "../../../common/logger.injectable";
|
||||
import isSnapPackageInjectable from "../../../common/vars/is-snap-package.injectable";
|
||||
import type { ClusterModel } from "../../../common/cluster-types";
|
||||
import pathExistsSyncInjectable from "../../../common/fs/path-exists-sync.injectable";
|
||||
|
||||
const clusterStoreSnapMigrationInjectable = getInjectable({
|
||||
id: "cluster-store-snap-migration",
|
||||
instantiate: (di) => {
|
||||
const { version } = di.inject(applicationInformationToken);
|
||||
const logger = di.inject(loggerInjectable);
|
||||
const isSnapPackage = di.inject(isSnapPackageInjectable);
|
||||
const pathExistsSync = di.inject(pathExistsSyncInjectable);
|
||||
|
||||
return {
|
||||
version, // Run always after upgrade
|
||||
run(store) {
|
||||
if (!isSnapPackage) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("Migrating embedded kubeconfig paths");
|
||||
const storedClusters = (store.get("clusters") || []) as ClusterModel[];
|
||||
|
||||
if (!storedClusters.length) return;
|
||||
|
||||
logger.info("Number of clusters to migrate: ", storedClusters.length);
|
||||
const migratedClusters = storedClusters
|
||||
.map(cluster => {
|
||||
/**
|
||||
* replace snap version with 'current' in kubeconfig path
|
||||
*/
|
||||
if (!pathExistsSync(cluster.kubeConfigPath)) {
|
||||
const kubeconfigPath = cluster.kubeConfigPath.replace(/\/snap\/kontena-lens\/[0-9]*\//, "/snap/kontena-lens/current/");
|
||||
|
||||
cluster.kubeConfigPath = kubeconfigPath;
|
||||
}
|
||||
|
||||
return cluster;
|
||||
});
|
||||
|
||||
|
||||
store.set("clusters", migratedClusters);
|
||||
},
|
||||
};
|
||||
},
|
||||
injectionToken: clusterStoreMigrationInjectionToken,
|
||||
});
|
||||
|
||||
export default clusterStoreSnapMigrationInjectable;
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { clusterVisibilityChannel } from "../../common/cluster/visibility-channel";
|
||||
import { getMessageChannelListenerInjectable } from "../../common/utils/channel/message-channel-listener-injection-token";
|
||||
import visibleClusterInjectable from "./visible-cluster.injectable";
|
||||
|
||||
const clusterVisibilityHandlerInjectable = getMessageChannelListenerInjectable({
|
||||
channel: clusterVisibilityChannel,
|
||||
id: "base",
|
||||
handler: (di) => {
|
||||
const visibleCluster = di.inject(visibleClusterInjectable);
|
||||
|
||||
return (clusterId) => visibleCluster.set(clusterId);
|
||||
},
|
||||
});
|
||||
|
||||
export default clusterVisibilityHandlerInjectable;
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 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 { observable } from "mobx";
|
||||
import type { ClusterId } from "../../common/cluster-types";
|
||||
|
||||
const visibleClusterInjectable = getInjectable({
|
||||
id: "visible-cluster",
|
||||
instantiate: () => observable.box<ClusterId | null>(null),
|
||||
});
|
||||
|
||||
export default visibleClusterInjectable;
|
||||
Reference in new issue
Block a user