diff --git a/src/common/__tests__/user-store.test.ts b/src/common/__tests__/user-store.test.ts index 8ae213d412..7071fc5c17 100644 --- a/src/common/__tests__/user-store.test.ts +++ b/src/common/__tests__/user-store.test.ts @@ -2,28 +2,7 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ - -import mockFs from "mock-fs"; - -jest.mock("electron", () => ({ - app: { - getVersion: () => "99.99.99", - getName: () => "lens", - setName: jest.fn(), - setPath: jest.fn(), - getPath: () => "tmp", - getLocale: () => "en", - setLoginItemSettings: jest.fn(), - }, - ipcMain: { - on: jest.fn(), - handle: jest.fn(), - }, -})); - import type { UserStore } from "../user-store"; -import { Console } from "console"; -import { stdout, stderr } from "process"; import userStoreInjectable from "../user-store/user-store.injectable"; import type { DiContainer } from "@ogre-tools/injectable"; import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable"; @@ -31,13 +10,11 @@ import type { ClusterStoreModel } from "../cluster-store/cluster-store"; import { defaultThemeId } from "../vars"; import writeFileInjectable from "../fs/write-file.injectable"; import { getDiForUnitTesting } from "../../main/getDiForUnitTesting"; -import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable"; import storeMigrationVersionInjectable from "../vars/store-migration-version.injectable"; import releaseChannelInjectable from "../vars/release-channel.injectable"; import defaultUpdateChannelInjectable from "../../features/application-update/common/selected-update-channel/default-update-channel.injectable"; -import fsInjectable from "../fs/fs.injectable"; - -console = new Console(stdout, stderr); +import writeJsonSyncInjectable from "../fs/write-json-sync.injectable"; +import writeFileSyncInjectable from "../fs/write-file-sync.injectable"; describe("user store tests", () => { let userStore: UserStore; @@ -46,14 +23,8 @@ describe("user store tests", () => { beforeEach(async () => { di = getDiForUnitTesting({ doGeneralOverrides: true }); - mockFs(); - di.override(writeFileInjectable, () => () => Promise.resolve()); - di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); - - di.permitSideEffects(getConfigurationFileModelInjectable); - di.unoverride(getConfigurationFileModelInjectable); - di.permitSideEffects(fsInjectable); + di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data"); di.override(releaseChannelInjectable, () => ({ get: () => "latest" as const, @@ -64,13 +35,12 @@ describe("user store tests", () => { userStore = di.inject(userStoreInjectable); }); - afterEach(() => { - mockFs.restore(); - }); - describe("for an empty config", () => { beforeEach(() => { - mockFs({ "some-directory-for-user-data": { "lens-user-store.json": "{}", "kube_config": "{}" }}); + const writeJsonSync = di.inject(writeJsonSyncInjectable); + + writeJsonSync("/some-directory-for-user-data/lens-user-store.json", {}); + writeJsonSync("/some-directory-for-user-data/kube_config", {}); userStore.load(); }); @@ -94,40 +64,38 @@ describe("user store tests", () => { describe("migrations", () => { beforeEach(() => { - mockFs({ - "some-directory-for-user-data": { - "lens-user-store.json": JSON.stringify({ - preferences: { colorTheme: "light" }, - }), - "lens-cluster-store.json": JSON.stringify({ - clusters: [ - { - id: "foobar", - kubeConfigPath: "some-directory-for-user-data/extension_data/foo/bar", - }, - { - id: "barfoo", - kubeConfigPath: "some/other/path", - }, - ], - } as ClusterStoreModel), - "extension_data": {}, - }, - "some": { - "other": { - "path": "is file", - }, - }, + const writeJsonSync = di.inject(writeJsonSyncInjectable); + const writeFileSync = di.inject(writeFileSyncInjectable); + + writeJsonSync("/some-directory-for-user-data/lens-user-store.json", { + preferences: { colorTheme: "light" }, }); + writeJsonSync("/some-directory-for-user-data/lens-cluster-store.json", { + clusters: [ + { + id: "foobar", + kubeConfigPath: "/some-directory-for-user-data/extension_data/foo/bar", + }, + { + id: "barfoo", + kubeConfigPath: "/some/other/path", + }, + ], + } as ClusterStoreModel); + + writeJsonSync("/some-directory-for-user-data/extension_data", {}); + + writeFileSync("/some/other/path", "is file"); + di.override(storeMigrationVersionInjectable, () => "10.0.0"); userStore.load(); }); it("skips clusters for adding to kube-sync with files under extension_data/", () => { - expect(userStore.syncKubeconfigEntries.has("some-directory-for-user-data/extension_data/foo/bar")).toBe(false); - expect(userStore.syncKubeconfigEntries.has("some/other/path")).toBe(true); + expect(userStore.syncKubeconfigEntries.has("/some-directory-for-user-data/extension_data/foo/bar")).toBe(false); + expect(userStore.syncKubeconfigEntries.has("/some/other/path")).toBe(true); }); it("allows access to the colorTheme preference", () => { diff --git a/src/common/get-configuration-file-model/get-configuration-file-model.global-override-for-injectable.ts b/src/common/get-configuration-file-model/get-configuration-file-model.global-override-for-injectable.ts index fba8939880..5f51460a2b 100644 --- a/src/common/get-configuration-file-model/get-configuration-file-model.global-override-for-injectable.ts +++ b/src/common/get-configuration-file-model/get-configuration-file-model.global-override-for-injectable.ts @@ -10,6 +10,34 @@ import getConfigurationFileModelInjectable from "./get-configuration-file-model. import type Config from "conf"; import readJsonSyncInjectable from "../fs/read-json-sync.injectable"; import writeJsonSyncInjectable from "../fs/write-json-sync.injectable"; +import { get, set } from "lodash"; +import semver from "semver"; + +const MIGRATION_KEY = `__internal__.migrations.version`; + +const _isVersionInRangeFormat = (version: string) => { + return semver.clean(version) === null; +}; + +const _shouldPerformMigration = (candidateVersion: string, previousMigratedVersion: string, versionToMigrate: string) => { + if (_isVersionInRangeFormat(candidateVersion)) { + if (previousMigratedVersion !== "0.0.0" && semver.satisfies(previousMigratedVersion, candidateVersion)) { + return false; + } + + return semver.satisfies(versionToMigrate, candidateVersion); + } + + if (semver.lte(candidateVersion, previousMigratedVersion)) { + return false; + } + + if (semver.gt(candidateVersion, versionToMigrate)) { + return false; + } + + return true; +}; export default getGlobalOverride(getConfigurationFileModelInjectable, (di) => { const readJsonSync = di.inject(readJsonSyncInjectable); @@ -18,6 +46,7 @@ export default getGlobalOverride(getConfigurationFileModelInjectable, (di) => { return (options) => { assert(options.cwd, "Missing options.cwd"); assert(options.configName, "Missing options.configName"); + assert(options.projectVersion, "Missing options.projectVersion"); const configFilePath = path.posix.join(options.cwd, `${options.configName}.json`); let store: object = {}; @@ -28,11 +57,12 @@ export default getGlobalOverride(getConfigurationFileModelInjectable, (di) => { // ignore } - return { + const config = { get store() { return store; }, path: configFilePath, + get: (key: string) => get(store, key), set: (key: string, value: unknown) => { let currentState: object; @@ -49,5 +79,35 @@ export default getGlobalOverride(getConfigurationFileModelInjectable, (di) => { store = readJsonSync(configFilePath); }, } as Partial as Config; + + // Migrate + { + const migrations = options.migrations ?? []; + const versionToMigrate = options.projectVersion; + let previousMigratedVersion = get(store, MIGRATION_KEY) || "0.0.0"; + const newerVersions = Object.entries(migrations) + .filter(([candidateVersion]) => _shouldPerformMigration(candidateVersion, previousMigratedVersion, versionToMigrate)); + + let storeBackup = { ...store }; + + for (const [version, migration] of newerVersions) { + try { + migration(config); + set(store, MIGRATION_KEY, version); + previousMigratedVersion = version; + storeBackup = { ...store }; + } + catch (error) { + store = storeBackup; + throw new Error(`Something went wrong during the migration! Changes applied to the store until this failed migration will be restored. ${error}`); + } + } + + if (_isVersionInRangeFormat(previousMigratedVersion) || !semver.eq(previousMigratedVersion, versionToMigrate)) { + set(store, MIGRATION_KEY, versionToMigrate); + } + } + + return config; }; });