diff --git a/src/common/__tests__/base-store.test.ts b/src/common/__tests__/base-store.test.ts index c91f4c5bfb..5ac1d4256a 100644 --- a/src/common/__tests__/base-store.test.ts +++ b/src/common/__tests__/base-store.test.ts @@ -4,8 +4,8 @@ */ import mockFs from "mock-fs"; -import type { BaseStoreDependencies } from "../base-store"; -import { BaseStore } from "../base-store"; +import type { BaseStoreDependencies } from "../base-store/base-store"; +import { BaseStore } from "../base-store/base-store"; import { action, comparer, makeObservable, observable, toJS } from "mobx"; import { readFileSync } from "fs"; import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable"; diff --git a/src/common/base-store.ts b/src/common/base-store/base-store.ts similarity index 88% rename from src/common/base-store.ts rename to src/common/base-store/base-store.ts index 2dccf5f593..ff6358ae86 100644 --- a/src/common/base-store.ts +++ b/src/common/base-store/base-store.ts @@ -5,19 +5,19 @@ import path from "path"; import type Config from "conf"; -import type { Options as ConfOptions } from "conf/dist/source/types"; +import type { Migrations, Options as ConfOptions } from "conf/dist/source/types"; import { ipcMain, ipcRenderer } from "electron"; import type { IEqualsComparer } from "mobx"; import { makeObservable, reaction, runInAction } from "mobx"; -import type { Disposer } from "./utils"; -import { isPromiseLike, toJS } from "./utils"; -import { broadcastMessage, ipcMainOn, ipcRendererOn } from "./ipc"; +import type { Disposer } from "../utils"; +import { isPromiseLike, toJS } from "../utils"; +import { broadcastMessage, ipcMainOn, ipcRendererOn } from "../ipc"; import isEqual from "lodash/isEqual"; import { kebabCase } from "lodash"; -import type { GetConfigurationFileModel } from "./get-configuration-file-model/get-configuration-file-model.injectable"; -import type { Logger } from "./logger"; +import type { GetConfigurationFileModel } from "../get-configuration-file-model/get-configuration-file-model.injectable"; +import type { Logger } from "../logger"; -export interface BaseStoreParams extends ConfOptions { +export interface BaseStoreParams extends Omit, "migrations"> { syncOptions?: { fireImmediately?: boolean; equals?: IEqualsComparer; @@ -29,6 +29,7 @@ export interface BaseStoreDependencies { readonly logger: Logger; readonly storeMigrationVersion: string; readonly directoryForUserData: string; + readonly migrations: Migrations>; getConfigurationFileModel: GetConfigurationFileModel; } @@ -39,7 +40,7 @@ export abstract class BaseStore { protected storeConfig?: Config; protected syncDisposers: Disposer[] = []; - readonly displayName: string; + readonly displayName = this.params.configName; protected constructor( protected readonly dependencies: BaseStoreDependencies, @@ -48,10 +49,6 @@ export abstract class BaseStore { makeObservable(this); this.displayName = this.params.configName; - - if (ipcRenderer) { - this.params.migrations = undefined; // don't run migrations on renderer - } } /** @@ -64,6 +61,7 @@ export abstract class BaseStore { projectVersion: this.dependencies.storeMigrationVersion, cwd: this.cwd(), ...this.params, + migrations: this.dependencies.migrations as Migrations, }); const res = this.fromStore(this.storeConfig.store); diff --git a/src/common/base-store/migrations.injectable.ts b/src/common/base-store/migrations.injectable.ts new file mode 100644 index 0000000000..27f7489dfa --- /dev/null +++ b/src/common/base-store/migrations.injectable.ts @@ -0,0 +1,46 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { InjectionToken } from "@ogre-tools/injectable"; +import { lifecycleEnum, getInjectable } from "@ogre-tools/injectable"; +import type Conf from "conf/dist/source"; +import type { Migrations } from "conf/dist/source/types"; +import loggerInjectable from "../logger.injectable"; +import { getOrInsert, iter } from "../utils"; + +export interface MigrationDeclaration { + version: string; + run(store: Conf>>): void; +} + +const storeMigrationsInjectable = getInjectable({ + id: "store-migrations", + instantiate: (di, token): Migrations> => { + const logger = di.inject(loggerInjectable); + const declarations = di.injectMany(token); + const migrations = new Map(); + + for (const decl of declarations) { + getOrInsert(migrations, decl.version, []).push(decl.run); + } + + return Object.fromEntries( + iter.map( + migrations, + ([v, fns]) => [v, (store) => { + logger.info(`Running ${v} migration for ${store.path}`); + + for (const fn of fns) { + fn(store); + } + }], + ), + ); + }, + lifecycle: lifecycleEnum.keyedSingleton({ + getInstanceKey: (di, token: InjectionToken) => token.id, + }), +}); + +export default storeMigrationsInjectable; diff --git a/src/common/cluster-store/cluster-store.injectable.ts b/src/common/cluster-store/cluster-store.injectable.ts index b1de070bdf..14b2b674c3 100644 --- a/src/common/cluster-store/cluster-store.injectable.ts +++ b/src/common/cluster-store/cluster-store.injectable.ts @@ -11,6 +11,8 @@ import directoryForUserDataInjectable from "../app-paths/directory-for-user-data import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable"; import loggerInjectable from "../logger.injectable"; import storeMigrationVersionInjectable from "../vars/store-migration-version.injectable"; +import storeMigrationsInjectable from "../base-store/migrations.injectable"; +import { clusterStoreMigrationInjectionToken } from "./migration-token"; const clusterStoreInjectable = getInjectable({ id: "cluster-store", @@ -23,6 +25,7 @@ const clusterStoreInjectable = getInjectable({ getConfigurationFileModel: di.inject(getConfigurationFileModelInjectable), logger: di.inject(loggerInjectable), storeMigrationVersion: di.inject(storeMigrationVersionInjectable), + migrations: di.inject(storeMigrationsInjectable, clusterStoreMigrationInjectionToken), }), }); diff --git a/src/common/cluster-store/cluster-store.ts b/src/common/cluster-store/cluster-store.ts index d40f948412..825db40b31 100644 --- a/src/common/cluster-store/cluster-store.ts +++ b/src/common/cluster-store/cluster-store.ts @@ -6,10 +6,9 @@ import { ipcMain, ipcRenderer, webFrame } from "electron"; import { action, comparer, computed, makeObservable, observable, reaction } from "mobx"; -import type { BaseStoreDependencies } from "../base-store"; -import { BaseStore } from "../base-store"; +import type { BaseStoreDependencies } from "../base-store/base-store"; +import { BaseStore } from "../base-store/base-store"; import { Cluster } from "../cluster/cluster"; -import migrations from "../../migrations/cluster-store"; import { disposer, toJS } from "../utils"; import type { ClusterModel, ClusterId, ClusterState } from "../cluster-types"; import { requestInitialClusterStates } from "../../renderer/ipc"; @@ -31,7 +30,7 @@ export class ClusterStore extends BaseStore { readonly displayName = "ClusterStore"; readonly clusters = observable.map(); - protected disposer = disposer(); + protected readonly disposer = disposer(); constructor(protected readonly dependencies: Dependencies) { super(dependencies, { @@ -40,7 +39,6 @@ export class ClusterStore extends BaseStore { syncOptions: { equals: comparer.structural, }, - migrations, }); makeObservable(this); diff --git a/src/common/cluster-store/migration-token.ts b/src/common/cluster-store/migration-token.ts new file mode 100644 index 0000000000..fda4ec751e --- /dev/null +++ b/src/common/cluster-store/migration-token.ts @@ -0,0 +1,11 @@ +/** + * 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 { MigrationDeclaration } from "../../migrations/helpers"; + +export const clusterStoreMigrationInjectionToken = getInjectionToken({ + id: "cluster-store-migration", +}); diff --git a/src/common/fs/read-file-buffer-sync.injectable.ts b/src/common/fs/read-file-buffer-sync.injectable.ts new file mode 100644 index 0000000000..98ba8e6d4b --- /dev/null +++ b/src/common/fs/read-file-buffer-sync.injectable.ts @@ -0,0 +1,19 @@ +/** + * 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 fsInjectable from "./fs.injectable"; + +export type ReadFileBufferSync = (filePath: string) => Buffer; + +const readFileBufferSyncInjectable = getInjectable({ + id: "read-file-buffer-sync", + instantiate: (di): ReadFileBufferSync => { + const { readFileSync } = di.inject(fsInjectable); + + return (filePath) => readFileSync(filePath); + }, +}); + +export default readFileBufferSyncInjectable; diff --git a/src/common/fs/read-json-sync.injectable.ts b/src/common/fs/read-json-sync.injectable.ts new file mode 100644 index 0000000000..81a9ef478f --- /dev/null +++ b/src/common/fs/read-json-sync.injectable.ts @@ -0,0 +1,13 @@ +/** + * 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 fsInjectable from "./fs.injectable"; + +const readJsonSyncInjectable = getInjectable({ + id: "read-json-sync", + instantiate: (di) => di.inject(fsInjectable).readJsonSync, +}); + +export default readJsonSyncInjectable; diff --git a/src/common/hotbars/store.ts b/src/common/hotbars/store.ts index be2b437e3f..30f7870494 100644 --- a/src/common/hotbars/store.ts +++ b/src/common/hotbars/store.ts @@ -4,8 +4,8 @@ */ import { action, comparer, observable, makeObservable, computed } from "mobx"; -import type { BaseStoreDependencies } from "../base-store"; -import { BaseStore } from "../base-store"; +import type { BaseStoreDependencies } from "../base-store/base-store"; +import { BaseStore } from "../base-store/base-store"; import migrations from "../../migrations/hotbar-store"; import { toJS } from "../utils"; import type { CatalogEntity } from "../catalog"; diff --git a/src/common/user-store/user-store.ts b/src/common/user-store/user-store.ts index 306bcda5a1..74f8f2a300 100644 --- a/src/common/user-store/user-store.ts +++ b/src/common/user-store/user-store.ts @@ -5,8 +5,8 @@ import { app } from "electron"; import { action, observable, reaction, makeObservable, isObservableArray, isObservableSet, isObservableMap } from "mobx"; -import type { BaseStoreDependencies } from "../base-store"; -import { BaseStore } from "../base-store"; +import type { BaseStoreDependencies } from "../base-store/base-store"; +import { BaseStore } from "../base-store/base-store"; import migrations from "../../migrations/user-store"; import { getOrInsertSet, toggle, toJS, object } from "../../renderer/utils"; import { DESCRIPTORS } from "./preferences-helpers"; diff --git a/src/common/weblink-store.ts b/src/common/weblink-store.ts index c8a50ba605..a1115581c4 100644 --- a/src/common/weblink-store.ts +++ b/src/common/weblink-store.ts @@ -4,8 +4,8 @@ */ import { action, comparer, observable, makeObservable } from "mobx"; -import type { BaseStoreDependencies } from "./base-store"; -import { BaseStore } from "./base-store"; +import type { BaseStoreDependencies } from "./base-store/base-store"; +import { BaseStore } from "./base-store/base-store"; import migrations from "../migrations/weblinks-store"; import * as uuid from "uuid"; import { toJS } from "./utils"; diff --git a/src/extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.ts b/src/extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.ts index 9be4c0529a..85e1a8bef9 100644 --- a/src/extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.ts +++ b/src/extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.ts @@ -5,8 +5,8 @@ import { SHA256 } from "crypto-js"; import { action, makeObservable, observable } from "mobx"; -import type { BaseStoreDependencies } from "../../../common/base-store"; -import { BaseStore } from "../../../common/base-store"; +import type { BaseStoreDependencies } from "../../../common/base-store/base-store"; +import { BaseStore } from "../../../common/base-store/base-store"; import type { LensExtensionId } from "../../lens-extension"; import { getOrInsertWithAsync, toJS } from "../../../common/utils"; import type { EnsureDirectory } from "../../../common/fs/ensure-dir.injectable"; diff --git a/src/extensions/extension-store.ts b/src/extensions/extension-store.ts index 2ebd1a6089..896d6e0f0b 100644 --- a/src/extensions/extension-store.ts +++ b/src/extensions/extension-store.ts @@ -3,8 +3,8 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { BaseStoreParams } from "../common/base-store"; -import { BaseStore } from "../common/base-store"; +import type { BaseStoreParams } from "../common/base-store/base-store"; +import { BaseStore } from "../common/base-store/base-store"; import * as path from "path"; import type { LensExtension } from "./lens-extension"; import assert from "assert"; @@ -15,6 +15,11 @@ import directoryForUserDataInjectable from "../common/app-paths/directory-for-us import getConfigurationFileModelInjectable from "../common/get-configuration-file-model/get-configuration-file-model.injectable"; import loggerInjectable from "../common/logger.injectable"; import storeMigrationVersionInjectable from "../common/vars/store-migration-version.injectable"; +import type { Migrations } from "conf/dist/source/types"; + +export interface ExtensionStoreParams extends BaseStoreParams { + migrations?: Migrations; +} export abstract class ExtensionStore extends BaseStore { private static readonly instances = new WeakMap>(); @@ -39,7 +44,7 @@ export abstract class ExtensionStore extends BaseStore { return ExtensionStore.instances.get(this) as (T | undefined); } - constructor(params: BaseStoreParams) { + constructor({ migrations, ...params }: ExtensionStoreParams) { const di = getLegacyGlobalDiForExtensionApi(); super({ @@ -47,6 +52,7 @@ export abstract class ExtensionStore extends BaseStore { getConfigurationFileModel: di.inject(getConfigurationFileModelInjectable), logger: di.inject(loggerInjectable), storeMigrationVersion: di.inject(storeMigrationVersionInjectable), + migrations: migrations as Migrations>, }, params); } diff --git a/src/extensions/extensions-store/extensions-store.ts b/src/extensions/extensions-store/extensions-store.ts index 51f56f531a..07451dc27a 100644 --- a/src/extensions/extensions-store/extensions-store.ts +++ b/src/extensions/extensions-store/extensions-store.ts @@ -6,8 +6,8 @@ import type { LensExtensionId } from "../lens-extension"; import { action, computed, makeObservable, observable } from "mobx"; import { toJS } from "../../common/utils"; -import type { BaseStoreDependencies } from "../../common/base-store"; -import { BaseStore } from "../../common/base-store"; +import type { BaseStoreDependencies } from "../../common/base-store/base-store"; +import { BaseStore } from "../../common/base-store/base-store"; export interface LensExtensionsStoreModel { extensions: Record; diff --git a/src/main/cluster/store-migrations/3.6.0-beta.1.injectable.ts b/src/main/cluster/store-migrations/3.6.0-beta.1.injectable.ts new file mode 100644 index 0000000000..49837f8c34 --- /dev/null +++ b/src/main/cluster/store-migrations/3.6.0-beta.1.injectable.ts @@ -0,0 +1,103 @@ +/** + * 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 directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; +import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; +import getCustomKubeConfigDirectoryInjectable 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"; +import { migrationLog } from "../../../migrations/helpers"; + +interface Pre360ClusterModel extends ClusterModel { + kubeConfig?: string; +} + +import { getInjectable } from "@ogre-tools/injectable"; +import { clusterStoreMigrationInjectionToken } from "../../../common/cluster-store/migration-token"; +import fsInjectable from "../../../common/fs/fs.injectable"; +import readFileBufferSyncInjectable from "../../../common/fs/read-file-buffer-sync.injectable"; + +const v360Beta1ClusterStoreMigrationInjectable = getInjectable({ + id: "v3.6.0-beta.1-cluster-store-migration", + instantiate: (di) => { + const userDataPath = di.inject(directoryForUserDataInjectable); + const kubeConfigsPath = di.inject(directoryForKubeConfigsInjectable); + const getCustomKubeConfigDirectory = di.inject(getCustomKubeConfigDirectoryInjectable); + const readFileSync = di.inject(readFileSyncInjectable); + const readFileBufferSync = di.inject(readFileBufferSyncInjectable); + const joinPaths = di.inject(joinPathsInjectable); + const { + ensureDirSync, + writeFileSync, + } = di.inject(fsInjectable); + + return { + version: "3.6.0-beta.1", + run: (store) => { + const storedClusters = (store.get("clusters") ?? []) as Pre360ClusterModel[]; + const migratedClusters: ClusterModel[] = []; + + ensureDirSync(kubeConfigsPath); + + migrationLog("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, { encoding: "utf-8", mode: 0o600 }); + + clusterModel.kubeConfigPath = absPath; + clusterModel.contextName = loadConfigFromString(readFileSync(absPath)).config.getCurrentContext(); + delete clusterModel.kubeConfig; + + } catch (error) { + migrationLog(`Failed to migrate Kubeconfig for cluster "${clusterModel.id}", removing clusterModel...`, error); + + continue; + } + + /** + * migrate cluster icon + */ + try { + if (clusterModel.preferences?.icon) { + migrationLog(`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) { + migrationLog(`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; diff --git a/src/main/cluster/store-migrations/5.0.0-beta.10.injectable.ts b/src/main/cluster/store-migrations/5.0.0-beta.10.injectable.ts new file mode 100644 index 0000000000..51309cebb9 --- /dev/null +++ b/src/main/cluster/store-migrations/5.0.0-beta.10.injectable.ts @@ -0,0 +1,63 @@ +/** + * 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(); // 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) { + if (isErrnoException(error) && !(error.code === "ENOENT" && error.path?.endsWith("lens-workspace-store.json"))) { + // ignore lens-workspace-store.json being missing + throw error; + } + } + }, + }; + }, + injectionToken: clusterStoreMigrationInjectionToken, +}); + +export default v500Beta10ClusterStoreMigrationInjectable; diff --git a/src/migrations/cluster-store/5.0.0-beta.13.ts b/src/main/cluster/store-migrations/5.0.0-beta.13.injectable.ts similarity index 59% rename from src/migrations/cluster-store/5.0.0-beta.13.ts rename to src/main/cluster/store-migrations/5.0.0-beta.13.injectable.ts index 87d88b143a..98cc147243 100644 --- a/src/migrations/cluster-store/5.0.0-beta.13.ts +++ b/src/main/cluster/store-migrations/5.0.0-beta.13.injectable.ts @@ -3,15 +3,12 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { ClusterModel, ClusterPreferences, ClusterPrometheusPreferences } from "../../common/cluster-types"; -import type { MigrationDeclaration } from "../helpers"; -import { migrationLog } from "../helpers"; -import { generateNewIdFor } from "../utils"; +import type { ClusterModel, ClusterPreferences, ClusterPrometheusPreferences } from "../../../common/cluster-types"; import { moveSync, removeSync } from "fs-extra"; -import { getLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; -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"; +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"; +import { generateNewIdFor } from "../../../migrations/utils"; function mergePrometheusPreferences(left: ClusterPrometheusPreferences, right: ClusterPrometheusPreferences): ClusterPrometheusPreferences { if (left.prometheus && left.prometheusProvider) { @@ -78,13 +75,16 @@ function mergeClusterModel(prev: ClusterModel, right: Omit): }; } -export default { - version: "5.0.0-beta.13", - run(store) { - const di = getLegacyGlobalDiForExtensionApi(); +import { getInjectable } from "@ogre-tools/injectable"; +import loggerInjectable from "../../../common/logger.injectable"; +import { clusterStoreMigrationInjectionToken } from "../../../common/cluster-store/migration-token"; +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`); @@ -94,34 +94,42 @@ export default { moveSync(oldPath, newPath); } catch (error) { if (String(error).includes("dest already exists")) { - migrationLog(`Multiple old lens-local-storage files for newId=${newId}. Removing ${oldId}.json`); + logger.info(`Multiple old lens-local-storage files for newId=${newId}. Removing ${oldId}.json`); removeSync(oldPath); } } }; - const folder = joinPaths(userDataPath, "lens-local-storage"); - const oldClusters: ClusterModel[] = store.get("clusters") ?? []; - const clusters = new Map(); + 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(); - for (const { id: oldId, ...cluster } of oldClusters) { - const newId = generateNewIdFor(cluster); - const newCluster = clusters.get(newId); + for (const { id: oldId, ...cluster } of oldClusters) { + const newId = generateNewIdFor(cluster); + const newCluster = clusters.get(newId); - if (newCluster) { - migrationLog(`Duplicate entries for ${newId}`, { oldId }); - clusters.set(newId, mergeClusterModel(newCluster, cluster)); - } else { - migrationLog(`First entry for ${newId}`, { oldId }); - clusters.set(newId, { - ...cluster, - id: newId, - workspaces: [cluster.workspace].filter(isDefined), - }); - moveStorageFolder({ folder, newId, oldId }); - } - } + 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()]); + store.set("clusters", [...clusters.values()]); + }, + }; }, -} as MigrationDeclaration; + injectionToken: clusterStoreMigrationInjectionToken, +}); + +export default v500Beta13ClusterStoreMigrationInjectable; diff --git a/src/main/cluster/store-migrations/snap.injectable.ts b/src/main/cluster/store-migrations/snap.injectable.ts new file mode 100644 index 0000000000..d9a7bcd7a2 --- /dev/null +++ b/src/main/cluster/store-migrations/snap.injectable.ts @@ -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 applicationInformationInjectable from "../../../common/vars/application-information.injectable"; +import { clusterStoreMigrationInjectionToken } from "../../../common/cluster-store/migration-token"; +import loggerInjectable from "../../../common/logger.injectable"; +import isSnapPackageInjectable from "../../../common/vars/is-snap-package.injectable"; +import fsInjectable from "../../../common/fs/fs.injectable"; +import type { ClusterModel } from "../../../common/cluster-types"; + +const clusterStoreSnapMigrationInjectable = getInjectable({ + id: "cluster-store-snap-migration", + instantiate: (di) => { + const { version } = di.inject(applicationInformationInjectable); + const logger = di.inject(loggerInjectable); + const isSnapPackage = di.inject(isSnapPackageInjectable); + const { existsSync } = di.inject(fsInjectable); + + 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 (!existsSync(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; + diff --git a/src/migrations/cluster-store/3.6.0-beta.1.ts b/src/migrations/cluster-store/3.6.0-beta.1.ts deleted file mode 100644 index 5f6bdbc9b7..0000000000 --- a/src/migrations/cluster-store/3.6.0-beta.1.ts +++ /dev/null @@ -1,93 +0,0 @@ -/** - * 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 fse from "fs-extra"; -import { loadConfigFromString } from "../../common/kube-helpers"; -import type { MigrationDeclaration } from "../helpers"; -import { migrationLog } from "../helpers"; -import type { ClusterModel } from "../../common/cluster-types"; -import { getLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; -import directoryForUserDataInjectable - from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; -import directoryForKubeConfigsInjectable - from "../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; -import getCustomKubeConfigDirectoryInjectable - from "../../common/app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable"; -import readFileSyncInjectable from "../../common/fs/read-file-sync.injectable"; -import joinPathsInjectable from "../../common/path/join-paths.injectable"; - -interface Pre360ClusterModel extends ClusterModel { - kubeConfig?: string; -} - -export default { - version: "3.6.0-beta.1", - run(store) { - const di = getLegacyGlobalDiForExtensionApi(); - - const userDataPath = di.inject(directoryForUserDataInjectable); - const kubeConfigsPath = di.inject(directoryForKubeConfigsInjectable); - const getCustomKubeConfigDirectory = di.inject(getCustomKubeConfigDirectoryInjectable); - const readFileSync = di.inject(readFileSyncInjectable); - const joinPaths = di.inject(joinPathsInjectable); - - const storedClusters: Pre360ClusterModel[] = store.get("clusters") ?? []; - const migratedClusters: ClusterModel[] = []; - - fse.ensureDirSync(kubeConfigsPath); - - migrationLog("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 - fse.writeFileSync(absPath, clusterModel.kubeConfig, { encoding: "utf-8", mode: 0o600 }); - - clusterModel.kubeConfigPath = absPath; - clusterModel.contextName = loadConfigFromString(readFileSync(absPath)).config.getCurrentContext(); - delete clusterModel.kubeConfig; - - } catch (error) { - migrationLog(`Failed to migrate Kubeconfig for cluster "${clusterModel.id}", removing clusterModel...`, error); - - continue; - } - - /** - * migrate cluster icon - */ - try { - if (clusterModel.preferences?.icon) { - migrationLog(`migrating ${clusterModel.preferences.icon} for ${clusterModel.preferences.clusterName}`); - const iconPath = clusterModel.preferences.icon.replace("store://", ""); - const fileData = fse.readFileSync(joinPaths(userDataPath, iconPath)); - - clusterModel.preferences.icon = `data:;base64,${fileData.toString("base64")}`; - } else { - delete clusterModel.preferences?.icon; - } - } catch (error) { - migrationLog(`Failed to migrate cluster icon for cluster "${clusterModel.id}"`, error); - delete clusterModel.preferences?.icon; - } - - migratedClusters.push(clusterModel); - } - - store.set("clusters", migratedClusters); - }, -} as MigrationDeclaration; diff --git a/src/migrations/cluster-store/5.0.0-beta.10.ts b/src/migrations/cluster-store/5.0.0-beta.10.ts deleted file mode 100644 index 97b94dc7f7..0000000000 --- a/src/migrations/cluster-store/5.0.0-beta.10.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import fse from "fs-extra"; -import type { ClusterModel } from "../../common/cluster-types"; -import type { MigrationDeclaration } from "../helpers"; -import { getLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; -import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; -import { isErrnoException } from "../../common/utils"; -import joinPathsInjectable from "../../common/path/join-paths.injectable"; - -interface Pre500WorkspaceStoreModel { - workspaces: { - id: string; - name: string; - }[]; -} - -export default { - version: "5.0.0-beta.10", - run(store) { - const di = getLegacyGlobalDiForExtensionApi(); - - const userDataPath = di.inject(directoryForUserDataInjectable); - const joinPaths = di.inject(joinPathsInjectable); - - try { - const workspaceData: Pre500WorkspaceStoreModel = fse.readJsonSync(joinPaths(userDataPath, "lens-workspace-store.json")); - const workspaces = new Map(); // mapping from WorkspaceId to name - - for (const { id, name } of workspaceData.workspaces) { - workspaces.set(id, name); - } - - const clusters: ClusterModel[] = store.get("clusters") ?? []; - - 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) { - if (isErrnoException(error) && !(error.code === "ENOENT" && error.path?.endsWith("lens-workspace-store.json"))) { - // ignore lens-workspace-store.json being missing - throw error; - } - } - }, -} as MigrationDeclaration; diff --git a/src/migrations/cluster-store/index.ts b/src/migrations/cluster-store/index.ts deleted file mode 100644 index 4851d01cae..0000000000 --- a/src/migrations/cluster-store/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -// Cluster store migrations - -import { joinMigrations } from "../helpers"; - -import version360Beta1 from "./3.6.0-beta.1"; -import version500Beta10 from "./5.0.0-beta.10"; -import version500Beta13 from "./5.0.0-beta.13"; -import snap from "./snap"; - -export default joinMigrations( - version360Beta1, - version500Beta10, - version500Beta13, - snap, -); diff --git a/src/migrations/cluster-store/snap.ts b/src/migrations/cluster-store/snap.ts deleted file mode 100644 index eb02cac81c..0000000000 --- a/src/migrations/cluster-store/snap.ts +++ /dev/null @@ -1,43 +0,0 @@ -/** - * 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 type { ClusterModel } from "../../common/cluster-types"; -import fs from "fs"; -import type { MigrationDeclaration } from "../helpers"; -import { migrationLog } from "../helpers"; -import packageJson from "../../../package.json"; - -export default { - // TODO: replace with injection once migrations are made as injectables - version: packageJson.version, // Run always after upgrade - run(store) { - if (!process.env["SNAP"]) return; - - migrationLog("Migrating embedded kubeconfig paths"); - const storedClusters: ClusterModel[] = store.get("clusters") || []; - - if (!storedClusters.length) return; - - migrationLog("Number of clusters to migrate: ", storedClusters.length); - const migratedClusters = storedClusters - .map(cluster => { - /** - * replace snap version with 'current' in kubeconfig path - */ - if (!fs.existsSync(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); - }, -} as MigrationDeclaration; diff --git a/src/migrations/helpers.ts b/src/migrations/helpers.ts index 6b35a86004..a3b2efe112 100644 --- a/src/migrations/helpers.ts +++ b/src/migrations/helpers.ts @@ -16,7 +16,7 @@ export function migrationLog(...args: any[]) { export interface MigrationDeclaration { version: string; - run(store: Conf): void; + run(store: Conf>>): void; } export function joinMigrations(...declarations: MigrationDeclaration[]): Migrations {