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:
Jari Kolehmainen authored and GitHub committed 2023-01-24 10:46:26 -08:00
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;
+289
View File
@@ -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;