1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Introduce method to make store migrations injectable

- Use it for ClusterStore

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2022-11-30 15:34:06 -05:00
parent 3dce6f916e
commit ad814ebdf6
23 changed files with 395 additions and 280 deletions

View File

@ -4,8 +4,8 @@
*/ */
import mockFs from "mock-fs"; import mockFs from "mock-fs";
import type { BaseStoreDependencies } from "../base-store"; import type { BaseStoreDependencies } from "../base-store/base-store";
import { BaseStore } from "../base-store"; import { BaseStore } from "../base-store/base-store";
import { action, comparer, makeObservable, observable, toJS } from "mobx"; import { action, comparer, makeObservable, observable, toJS } from "mobx";
import { readFileSync } from "fs"; import { readFileSync } from "fs";
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable"; import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";

View File

@ -5,19 +5,19 @@
import path from "path"; import path from "path";
import type Config from "conf"; 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 { ipcMain, ipcRenderer } from "electron";
import type { IEqualsComparer } from "mobx"; import type { IEqualsComparer } from "mobx";
import { makeObservable, reaction, runInAction } from "mobx"; import { makeObservable, reaction, runInAction } from "mobx";
import type { Disposer } from "./utils"; import type { Disposer } from "../utils";
import { isPromiseLike, toJS } from "./utils"; import { isPromiseLike, toJS } from "../utils";
import { broadcastMessage, ipcMainOn, ipcRendererOn } from "./ipc"; import { broadcastMessage, ipcMainOn, ipcRendererOn } from "../ipc";
import isEqual from "lodash/isEqual"; import isEqual from "lodash/isEqual";
import { kebabCase } from "lodash"; import { kebabCase } from "lodash";
import type { GetConfigurationFileModel } from "./get-configuration-file-model/get-configuration-file-model.injectable"; import type { GetConfigurationFileModel } from "../get-configuration-file-model/get-configuration-file-model.injectable";
import type { Logger } from "./logger"; import type { Logger } from "../logger";
export interface BaseStoreParams<T> extends ConfOptions<T> { export interface BaseStoreParams<T> extends Omit<ConfOptions<T>, "migrations"> {
syncOptions?: { syncOptions?: {
fireImmediately?: boolean; fireImmediately?: boolean;
equals?: IEqualsComparer<T>; equals?: IEqualsComparer<T>;
@ -29,6 +29,7 @@ export interface BaseStoreDependencies {
readonly logger: Logger; readonly logger: Logger;
readonly storeMigrationVersion: string; readonly storeMigrationVersion: string;
readonly directoryForUserData: string; readonly directoryForUserData: string;
readonly migrations: Migrations<Record<string, unknown>>;
getConfigurationFileModel: GetConfigurationFileModel; getConfigurationFileModel: GetConfigurationFileModel;
} }
@ -39,7 +40,7 @@ export abstract class BaseStore<T extends object> {
protected storeConfig?: Config<T>; protected storeConfig?: Config<T>;
protected syncDisposers: Disposer[] = []; protected syncDisposers: Disposer[] = [];
readonly displayName: string; readonly displayName = this.params.configName;
protected constructor( protected constructor(
protected readonly dependencies: BaseStoreDependencies, protected readonly dependencies: BaseStoreDependencies,
@ -48,10 +49,6 @@ export abstract class BaseStore<T extends object> {
makeObservable(this); makeObservable(this);
this.displayName = this.params.configName; 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<T extends object> {
projectVersion: this.dependencies.storeMigrationVersion, projectVersion: this.dependencies.storeMigrationVersion,
cwd: this.cwd(), cwd: this.cwd(),
...this.params, ...this.params,
migrations: this.dependencies.migrations as Migrations<T>,
}); });
const res = this.fromStore(this.storeConfig.store); const res = this.fromStore(this.storeConfig.store);

View File

@ -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<Partial<Record<string, unknown>>>): void;
}
const storeMigrationsInjectable = getInjectable({
id: "store-migrations",
instantiate: (di, token): Migrations<Record<string, unknown>> => {
const logger = di.inject(loggerInjectable);
const declarations = di.injectMany(token);
const migrations = new Map<string, MigrationDeclaration["run"][]>();
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<MigrationDeclaration, void>) => token.id,
}),
});
export default storeMigrationsInjectable;

View File

@ -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 getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable";
import loggerInjectable from "../logger.injectable"; import loggerInjectable from "../logger.injectable";
import storeMigrationVersionInjectable from "../vars/store-migration-version.injectable"; import storeMigrationVersionInjectable from "../vars/store-migration-version.injectable";
import storeMigrationsInjectable from "../base-store/migrations.injectable";
import { clusterStoreMigrationInjectionToken } from "./migration-token";
const clusterStoreInjectable = getInjectable({ const clusterStoreInjectable = getInjectable({
id: "cluster-store", id: "cluster-store",
@ -23,6 +25,7 @@ const clusterStoreInjectable = getInjectable({
getConfigurationFileModel: di.inject(getConfigurationFileModelInjectable), getConfigurationFileModel: di.inject(getConfigurationFileModelInjectable),
logger: di.inject(loggerInjectable), logger: di.inject(loggerInjectable),
storeMigrationVersion: di.inject(storeMigrationVersionInjectable), storeMigrationVersion: di.inject(storeMigrationVersionInjectable),
migrations: di.inject(storeMigrationsInjectable, clusterStoreMigrationInjectionToken),
}), }),
}); });

View File

@ -6,10 +6,9 @@
import { ipcMain, ipcRenderer, webFrame } from "electron"; import { ipcMain, ipcRenderer, webFrame } from "electron";
import { action, comparer, computed, makeObservable, observable, reaction } from "mobx"; import { action, comparer, computed, makeObservable, observable, reaction } from "mobx";
import type { BaseStoreDependencies } from "../base-store"; import type { BaseStoreDependencies } from "../base-store/base-store";
import { BaseStore } from "../base-store"; import { BaseStore } from "../base-store/base-store";
import { Cluster } from "../cluster/cluster"; import { Cluster } from "../cluster/cluster";
import migrations from "../../migrations/cluster-store";
import { disposer, toJS } from "../utils"; import { disposer, toJS } from "../utils";
import type { ClusterModel, ClusterId, ClusterState } from "../cluster-types"; import type { ClusterModel, ClusterId, ClusterState } from "../cluster-types";
import { requestInitialClusterStates } from "../../renderer/ipc"; import { requestInitialClusterStates } from "../../renderer/ipc";
@ -31,7 +30,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
readonly displayName = "ClusterStore"; readonly displayName = "ClusterStore";
readonly clusters = observable.map<ClusterId, Cluster>(); readonly clusters = observable.map<ClusterId, Cluster>();
protected disposer = disposer(); protected readonly disposer = disposer();
constructor(protected readonly dependencies: Dependencies) { constructor(protected readonly dependencies: Dependencies) {
super(dependencies, { super(dependencies, {
@ -40,7 +39,6 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
syncOptions: { syncOptions: {
equals: comparer.structural, equals: comparer.structural,
}, },
migrations,
}); });
makeObservable(this); makeObservable(this);

View File

@ -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<MigrationDeclaration>({
id: "cluster-store-migration",
});

View File

@ -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;

View File

@ -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;

View File

@ -4,8 +4,8 @@
*/ */
import { action, comparer, observable, makeObservable, computed } from "mobx"; import { action, comparer, observable, makeObservable, computed } from "mobx";
import type { BaseStoreDependencies } from "../base-store"; import type { BaseStoreDependencies } from "../base-store/base-store";
import { BaseStore } from "../base-store"; import { BaseStore } from "../base-store/base-store";
import migrations from "../../migrations/hotbar-store"; import migrations from "../../migrations/hotbar-store";
import { toJS } from "../utils"; import { toJS } from "../utils";
import type { CatalogEntity } from "../catalog"; import type { CatalogEntity } from "../catalog";

View File

@ -5,8 +5,8 @@
import { app } from "electron"; import { app } from "electron";
import { action, observable, reaction, makeObservable, isObservableArray, isObservableSet, isObservableMap } from "mobx"; import { action, observable, reaction, makeObservable, isObservableArray, isObservableSet, isObservableMap } from "mobx";
import type { BaseStoreDependencies } from "../base-store"; import type { BaseStoreDependencies } from "../base-store/base-store";
import { BaseStore } from "../base-store"; import { BaseStore } from "../base-store/base-store";
import migrations from "../../migrations/user-store"; import migrations from "../../migrations/user-store";
import { getOrInsertSet, toggle, toJS, object } from "../../renderer/utils"; import { getOrInsertSet, toggle, toJS, object } from "../../renderer/utils";
import { DESCRIPTORS } from "./preferences-helpers"; import { DESCRIPTORS } from "./preferences-helpers";

View File

@ -4,8 +4,8 @@
*/ */
import { action, comparer, observable, makeObservable } from "mobx"; import { action, comparer, observable, makeObservable } from "mobx";
import type { BaseStoreDependencies } from "./base-store"; import type { BaseStoreDependencies } from "./base-store/base-store";
import { BaseStore } from "./base-store"; import { BaseStore } from "./base-store/base-store";
import migrations from "../migrations/weblinks-store"; import migrations from "../migrations/weblinks-store";
import * as uuid from "uuid"; import * as uuid from "uuid";
import { toJS } from "./utils"; import { toJS } from "./utils";

View File

@ -5,8 +5,8 @@
import { SHA256 } from "crypto-js"; import { SHA256 } from "crypto-js";
import { action, makeObservable, observable } from "mobx"; import { action, makeObservable, observable } from "mobx";
import type { BaseStoreDependencies } from "../../../common/base-store"; import type { BaseStoreDependencies } from "../../../common/base-store/base-store";
import { BaseStore } from "../../../common/base-store"; import { BaseStore } from "../../../common/base-store/base-store";
import type { LensExtensionId } from "../../lens-extension"; import type { LensExtensionId } from "../../lens-extension";
import { getOrInsertWithAsync, toJS } from "../../../common/utils"; import { getOrInsertWithAsync, toJS } from "../../../common/utils";
import type { EnsureDirectory } from "../../../common/fs/ensure-dir.injectable"; import type { EnsureDirectory } from "../../../common/fs/ensure-dir.injectable";

View File

@ -3,8 +3,8 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import type { BaseStoreParams } from "../common/base-store"; import type { BaseStoreParams } from "../common/base-store/base-store";
import { BaseStore } from "../common/base-store"; import { BaseStore } from "../common/base-store/base-store";
import * as path from "path"; import * as path from "path";
import type { LensExtension } from "./lens-extension"; import type { LensExtension } from "./lens-extension";
import assert from "assert"; 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 getConfigurationFileModelInjectable from "../common/get-configuration-file-model/get-configuration-file-model.injectable";
import loggerInjectable from "../common/logger.injectable"; import loggerInjectable from "../common/logger.injectable";
import storeMigrationVersionInjectable from "../common/vars/store-migration-version.injectable"; import storeMigrationVersionInjectable from "../common/vars/store-migration-version.injectable";
import type { Migrations } from "conf/dist/source/types";
export interface ExtensionStoreParams<T extends object> extends BaseStoreParams<T> {
migrations?: Migrations<T>;
}
export abstract class ExtensionStore<T extends object> extends BaseStore<T> { export abstract class ExtensionStore<T extends object> extends BaseStore<T> {
private static readonly instances = new WeakMap<object, ExtensionStore<object>>(); private static readonly instances = new WeakMap<object, ExtensionStore<object>>();
@ -39,7 +44,7 @@ export abstract class ExtensionStore<T extends object> extends BaseStore<T> {
return ExtensionStore.instances.get(this) as (T | undefined); return ExtensionStore.instances.get(this) as (T | undefined);
} }
constructor(params: BaseStoreParams<T>) { constructor({ migrations, ...params }: ExtensionStoreParams<T>) {
const di = getLegacyGlobalDiForExtensionApi(); const di = getLegacyGlobalDiForExtensionApi();
super({ super({
@ -47,6 +52,7 @@ export abstract class ExtensionStore<T extends object> extends BaseStore<T> {
getConfigurationFileModel: di.inject(getConfigurationFileModelInjectable), getConfigurationFileModel: di.inject(getConfigurationFileModelInjectable),
logger: di.inject(loggerInjectable), logger: di.inject(loggerInjectable),
storeMigrationVersion: di.inject(storeMigrationVersionInjectable), storeMigrationVersion: di.inject(storeMigrationVersionInjectable),
migrations: migrations as Migrations<Record<string, unknown>>,
}, params); }, params);
} }

View File

@ -6,8 +6,8 @@
import type { LensExtensionId } from "../lens-extension"; import type { LensExtensionId } from "../lens-extension";
import { action, computed, makeObservable, observable } from "mobx"; import { action, computed, makeObservable, observable } from "mobx";
import { toJS } from "../../common/utils"; import { toJS } from "../../common/utils";
import type { BaseStoreDependencies } from "../../common/base-store"; import type { BaseStoreDependencies } from "../../common/base-store/base-store";
import { BaseStore } from "../../common/base-store"; import { BaseStore } from "../../common/base-store/base-store";
export interface LensExtensionsStoreModel { export interface LensExtensionsStoreModel {
extensions: Record<LensExtensionId, LensExtensionState>; extensions: Record<LensExtensionId, LensExtensionState>;

View File

@ -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;

View File

@ -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<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) {
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;

View File

@ -3,15 +3,12 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import type { ClusterModel, ClusterPreferences, ClusterPrometheusPreferences } from "../../common/cluster-types"; import type { ClusterModel, ClusterPreferences, ClusterPrometheusPreferences } from "../../../common/cluster-types";
import type { MigrationDeclaration } from "../helpers";
import { migrationLog } from "../helpers";
import { generateNewIdFor } from "../utils";
import { moveSync, removeSync } from "fs-extra"; 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 directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import { isDefined } from "../../../common/utils";
import { isDefined } from "../../common/utils"; import joinPathsInjectable from "../../../common/path/join-paths.injectable";
import joinPathsInjectable from "../../common/path/join-paths.injectable"; import { generateNewIdFor } from "../../../migrations/utils";
function mergePrometheusPreferences(left: ClusterPrometheusPreferences, right: ClusterPrometheusPreferences): ClusterPrometheusPreferences { function mergePrometheusPreferences(left: ClusterPrometheusPreferences, right: ClusterPrometheusPreferences): ClusterPrometheusPreferences {
if (left.prometheus && left.prometheusProvider) { if (left.prometheus && left.prometheusProvider) {
@ -78,13 +75,16 @@ function mergeClusterModel(prev: ClusterModel, right: Omit<ClusterModel, "id">):
}; };
} }
export default { import { getInjectable } from "@ogre-tools/injectable";
version: "5.0.0-beta.13", import loggerInjectable from "../../../common/logger.injectable";
run(store) { import { clusterStoreMigrationInjectionToken } from "../../../common/cluster-store/migration-token";
const di = getLegacyGlobalDiForExtensionApi();
const v500Beta13ClusterStoreMigrationInjectable = getInjectable({
id: "v5.0.0-beta.13-cluster-store-migration",
instantiate: (di) => {
const userDataPath = di.inject(directoryForUserDataInjectable); const userDataPath = di.inject(directoryForUserDataInjectable);
const joinPaths = di.inject(joinPathsInjectable); const joinPaths = di.inject(joinPathsInjectable);
const logger = di.inject(loggerInjectable);
const moveStorageFolder = ({ folder, newId, oldId }: { folder: string; newId: string; oldId: string }): void => { const moveStorageFolder = ({ folder, newId, oldId }: { folder: string; newId: string; oldId: string }): void => {
const oldPath = joinPaths(folder, `${oldId}.json`); const oldPath = joinPaths(folder, `${oldId}.json`);
@ -94,34 +94,42 @@ export default {
moveSync(oldPath, newPath); moveSync(oldPath, newPath);
} catch (error) { } catch (error) {
if (String(error).includes("dest already exists")) { 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); removeSync(oldPath);
} }
} }
}; };
const folder = joinPaths(userDataPath, "lens-local-storage"); return {
const oldClusters: ClusterModel[] = store.get("clusters") ?? []; version: "5.0.0-beta.13",
const clusters = new Map<string, ClusterModel>(); 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) { for (const { id: oldId, ...cluster } of oldClusters) {
const newId = generateNewIdFor(cluster); const newId = generateNewIdFor(cluster);
const newCluster = clusters.get(newId); const newCluster = clusters.get(newId);
if (newCluster) { if (newCluster) {
migrationLog(`Duplicate entries for ${newId}`, { oldId }); logger.info(`Duplicate entries for ${newId}`, { oldId });
clusters.set(newId, mergeClusterModel(newCluster, cluster)); clusters.set(newId, mergeClusterModel(newCluster, cluster));
} else { } else {
migrationLog(`First entry for ${newId}`, { oldId }); logger.info(`First entry for ${newId}`, { oldId });
clusters.set(newId, { clusters.set(newId, {
...cluster, ...cluster,
id: newId, id: newId,
workspaces: [cluster.workspace].filter(isDefined), workspaces: [cluster.workspace].filter(isDefined),
}); });
moveStorageFolder({ folder, newId, oldId }); moveStorageFolder({ folder, newId, oldId });
} }
} }
store.set("clusters", [...clusters.values()]); store.set("clusters", [...clusters.values()]);
},
};
}, },
} as MigrationDeclaration; injectionToken: clusterStoreMigrationInjectionToken,
});
export default v500Beta13ClusterStoreMigrationInjectable;

View File

@ -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;

View File

@ -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;

View File

@ -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<string, string>(); // 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;

View File

@ -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,
);

View File

@ -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;

View File

@ -16,7 +16,7 @@ export function migrationLog(...args: any[]) {
export interface MigrationDeclaration { export interface MigrationDeclaration {
version: string; version: string;
run(store: Conf<any>): void; run(store: Conf<Partial<Record<string, unknown>>>): void;
} }
export function joinMigrations(...declarations: MigrationDeclaration[]): Migrations<any> { export function joinMigrations(...declarations: MigrationDeclaration[]): Migrations<any> {