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

Remove isTestEnv

- Make all migration declarations injectable using a token

- Make all migrations injectable using a token for the renderer side
  (instead of clearing them based on the truthiness of ipcRenderer)

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2022-07-07 14:30:20 -04:00
parent afee784737
commit f0f06f53d2
77 changed files with 1330 additions and 1019 deletions

View File

@ -14,7 +14,6 @@ import { Singleton, toJS } from "./utils";
import logger from "../main/logger";
import { broadcastMessage, ipcMainOn, ipcRendererOn } from "./ipc";
import isEqual from "lodash/isEqual";
import { isTestEnv } from "./vars";
import { kebabCase } from "lodash";
import { getLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
import directoryForUserDataInjectable from "./app-paths/directory-for-user-data/directory-for-user-data.injectable";
@ -40,22 +39,15 @@ export abstract class BaseStore<T> extends Singleton {
protected constructor(protected params: BaseStoreParams<T>) {
super();
makeObservable(this);
if (ipcRenderer) {
params.migrations = undefined; // don't run migrations on renderer
}
}
/**
* This must be called after the last child's constructor is finished (or just before it finishes)
*/
load() {
if (!isTestEnv) {
logger.info(`[${kebabCase(this.displayName).toUpperCase()}]: LOADING from ${this.path} ...`);
}
logger.debug(`[${kebabCase(this.displayName).toUpperCase()}]: LOADING from ${this.path} ...`);
const di = getLegacyGlobalDiForExtensionApi();
const getConfigurationFileModel = di.inject(getConfigurationFileModelInjectable);
this.storeConfig = getConfigurationFileModel({
@ -72,10 +64,7 @@ export abstract class BaseStore<T> extends Singleton {
}
this.enableSync();
if (!isTestEnv) {
logger.info(`[${kebabCase(this.displayName).toUpperCase()}]: LOADED from ${this.path}`);
}
logger.debug(`[${kebabCase(this.displayName).toUpperCase()}]: LOADED from ${this.path}`);
}
get name() {

View File

@ -6,7 +6,7 @@
import type { CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
import { CatalogCategory, CatalogEntity, categoryVersion } from "../catalog/catalog-entity";
import { productName } from "../vars";
import { WeblinkStore } from "../weblink-store";
import { WeblinkStore } from "../weblinks/store";
export type WebLinkStatusPhase = "available" | "unavailable";

View File

@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import { ClusterStore } from "./cluster-store";
import { createClusterInjectionToken } from "../cluster/create-cluster-injection-token";
import readClusterConfigSyncInjectable from "./read-cluster-config.injectable";
import { clusterStoreMigrationsInjectionToken } from "./migrations";
const clusterStoreInjectable = getInjectable({
id: "cluster-store",
@ -16,6 +17,7 @@ const clusterStoreInjectable = getInjectable({
return ClusterStore.createInstance({
createCluster: di.inject(createClusterInjectionToken),
readClusterConfigSync: di.inject(readClusterConfigSyncInjectable),
migrations: di.inject(clusterStoreMigrationsInjectionToken),
});
},

View File

@ -8,7 +8,6 @@ import { ipcMain, ipcRenderer, webFrame } from "electron";
import { action, comparer, computed, makeObservable, observable, reaction } from "mobx";
import { BaseStore } from "../base-store";
import { Cluster } from "../cluster/cluster";
import migrations from "../../migrations/cluster-store";
import logger from "../../main/logger";
import { appEventBus } from "../app-event-bus/event-bus";
import { ipcMainHandle } from "../ipc";
@ -18,6 +17,7 @@ import { requestInitialClusterStates } from "../../renderer/ipc";
import { clusterStates } from "../ipc/cluster";
import type { CreateCluster } from "../cluster/create-cluster-injection-token";
import type { ReadClusterConfigSync } from "./read-cluster-config.injectable";
import type { Migrations } from "conf/dist/source/types";
export interface ClusterStoreModel {
clusters?: ClusterModel[];
@ -26,6 +26,7 @@ export interface ClusterStoreModel {
interface Dependencies {
createCluster: CreateCluster;
readClusterConfigSync: ReadClusterConfigSync;
readonly migrations: Migrations<ClusterStoreModel> | undefined;
}
export class ClusterStore extends BaseStore<ClusterStoreModel> {
@ -34,14 +35,14 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
protected disposer = disposer();
constructor(private dependencies: Dependencies) {
constructor(protected readonly dependencies: Dependencies) {
super({
configName: "lens-cluster-store",
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
syncOptions: {
equals: comparer.structural,
},
migrations,
migrations: dependencies.migrations,
});
makeObservable(this);

View File

@ -0,0 +1,12 @@
/**
* 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 { Migrations } from "conf/dist/source/types";
import type { ClusterStoreModel } from "./cluster-store";
export const clusterStoreMigrationsInjectionToken = getInjectionToken<Migrations<ClusterStoreModel> | undefined>({
id: "cluster-store-migrations-token",
});

View File

@ -5,14 +5,31 @@
import { getInjectable } from "@ogre-tools/injectable";
import fsInjectable from "./fs.injectable";
export type ReadFileSync = (filePath: string) => string;
export interface ReadFileAsBufferOptions {
asBuffer: true;
}
export interface ReadFileAsStringOptions {
asBuffer: false;
}
export interface ReadFileSync {
(filePath: string, options?: ReadFileAsStringOptions): string;
(filePath: string, options: ReadFileAsBufferOptions): Buffer;
}
const readFileSyncInjectable = getInjectable({
id: "read-file-sync",
instantiate: (di): ReadFileSync => {
instantiate: (di) => {
const { readFileSync } = di.inject(fsInjectable);
return (filePath) => readFileSync(filePath, "utf-8");
return ((filePath, options = { asBuffer: false }) => {
if (options.asBuffer) {
return readFileSync(filePath);
} else {
return readFileSync(filePath, "utf-8");
}
}) as ReadFileSync;
},
});

View File

@ -0,0 +1,12 @@
/**
* 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 { Migrations } from "conf/dist/source/types";
import type { HotbarStoreModel } from "./store";
export const hotbarStoreMigrationsInjectionToken = getInjectionToken<Migrations<HotbarStoreModel> | undefined>({
id: "hotbar-store-migrations-token",
});

View File

@ -5,7 +5,6 @@
import { action, comparer, observable, makeObservable, computed } from "mobx";
import { BaseStore } from "../base-store";
import migrations from "../../migrations/hotbar-store";
import { toJS } from "../utils";
import type { CatalogEntity } from "../catalog";
import { broadcastMessage } from "../ipc";
@ -15,6 +14,7 @@ import { hotbarTooManyItemsChannel } from "../ipc/hotbar";
import type { GeneralEntity } from "../catalog-entities";
import type { Logger } from "../logger";
import assert from "assert";
import type { Migrations } from "conf/dist/source/types";
export interface HotbarStoreModel {
hotbars: Hotbar[];
@ -24,6 +24,7 @@ export interface HotbarStoreModel {
interface Dependencies {
readonly catalogCatalogEntity: GeneralEntity;
readonly logger: Logger;
readonly migrations: Migrations<HotbarStoreModel> | undefined;
}
export class HotbarStore extends BaseStore<HotbarStoreModel> {
@ -38,7 +39,7 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
syncOptions: {
equals: comparer.structural,
},
migrations,
migrations: dependencies.migrations,
});
makeObservable(this);

View File

@ -17,12 +17,6 @@ import type { PartialDeep } from "type-fest";
export const kubeConfigDefaultPath = path.join(os.homedir(), ".kube", "config");
export function loadConfigFromFileSync(filePath: string): ConfigResult {
const content = fse.readFileSync(resolvePath(filePath), "utf-8");
return loadConfigFromString(content);
}
export async function loadConfigFromFile(filePath: string): Promise<ConfigResult> {
const content = await fse.readFile(resolvePath(filePath), "utf-8");

View File

@ -7,7 +7,7 @@ import { app, ipcMain } from "electron";
import winston, { format } from "winston";
import type Transport from "winston-transport";
import { consoleFormat } from "winston-console-format";
import { isDebugging, isTestEnv } from "./vars";
import { isDebugging, isProduction } from "./vars";
import BrowserConsole from "winston-transport-browserconsole";
export interface Logger {
@ -18,13 +18,8 @@ export interface Logger {
silly: (message: string, ...args: any) => void;
}
const logLevel = process.env.LOG_LEVEL
? process.env.LOG_LEVEL
: isDebugging
? "debug"
: isTestEnv
? "error"
: "info";
const defaultLogLevel = isDebugging ? "debug" : "info";
const logLevel = process.env.LOG_LEVEL || defaultLogLevel;
const transports: Transport[] = [];
@ -51,7 +46,8 @@ if (ipcMain) {
}),
);
if (!isTestEnv) {
// TODO: replace logger with injectable
if (isProduction) {
transports.push(
new winston.transports.File({
handleExceptions: false,

View File

@ -0,0 +1,16 @@
/**
* 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 { Migrations } from "conf/dist/source/types";
import type { UserStoreModel } from "./user-store";
export const userStoreMigrationsInjectionToken = getInjectionToken<Migrations<UserStoreModel> | undefined>({
id: "user-store-migrations-token",
});
export const userStorePreMigrationsInjectionToken = getInjectionToken<() => void>({
id: "user-store-pre-migrations-token",
});

View File

@ -3,23 +3,25 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { ipcMain } from "electron";
import userStoreFileNameMigrationInjectable from "./file-name-migration.injectable";
import { UserStore } from "./user-store";
import selectedUpdateChannelInjectable from "../application-update/selected-update-channel/selected-update-channel.injectable";
import { userStoreMigrationsInjectionToken, userStorePreMigrationsInjectionToken } from "./migrations";
const userStoreInjectable = getInjectable({
id: "user-store",
instantiate: (di) => {
const preMigrations = di.injectMany(userStorePreMigrationsInjectionToken);
UserStore.resetInstance();
if (ipcMain) {
di.inject(userStoreFileNameMigrationInjectable);
for (const preMigration of preMigrations) {
preMigration();
}
return UserStore.createInstance({
selectedUpdateChannel: di.inject(selectedUpdateChannelInjectable),
migrations: di.inject(userStoreMigrationsInjectionToken),
});
},

View File

@ -7,7 +7,6 @@ import { app } from "electron";
import semver from "semver";
import { action, computed, observable, reaction, makeObservable, isObservableArray, isObservableSet, isObservableMap } from "mobx";
import { BaseStore } from "../base-store";
import migrations from "../../migrations/user-store";
import { getAppVersion } from "../utils/app-version";
import { kubeConfigDefaultPath } from "../kube-helpers";
import { appEventBus } from "../app-event-bus/event-bus";
@ -17,6 +16,7 @@ import type { UserPreferencesModel, StoreType } from "./preferences-helpers";
import logger from "../../main/logger";
import type { SelectedUpdateChannel } from "../application-update/selected-update-channel/selected-update-channel.injectable";
import type { UpdateChannelId } from "../application-update/update-channels";
import type { Migrations } from "conf/dist/source/types";
export interface UserStoreModel {
lastSeenAppVersion: string;
@ -24,7 +24,8 @@ export interface UserStoreModel {
}
interface Dependencies {
selectedUpdateChannel: SelectedUpdateChannel;
readonly selectedUpdateChannel: SelectedUpdateChannel;
readonly migrations: Migrations<UserStoreModel> | undefined;
}
export class UserStore extends BaseStore<UserStoreModel> /* implements UserStoreFlatModel (when strict null is enabled) */ {
@ -33,7 +34,7 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
constructor(private readonly dependencies: Dependencies) {
super({
configName: "lens-user-store",
migrations,
migrations: dependencies.migrations,
});
makeObservable(this);

View File

@ -11,16 +11,13 @@ const environmentVariablesInjectable = getInjectable({
// IMPORTANT: The syntax needs to be exactly this in order to make environment variable values
// hard-coded at compile-time by Webpack.
const NODE_ENV = process.env.NODE_ENV;
const JEST_WORKER_ID = process.env.JEST_WORKER_ID;
const CICD = process.env.CICD;
return {
// Compile-time environment variables
NODE_ENV,
JEST_WORKER_ID,
CICD,
// Runtime environment variables
CICD: process.env.CICD,
LENS_DISABLE_GPU: process.env.LENS_DISABLE_GPU,
};
},

View File

@ -28,11 +28,6 @@ export const isLinux = process.platform === "linux";
export const isDebugging = ["true", "1", "yes", "y", "on"].includes((process.env.DEBUG ?? "").toLowerCase());
export const isSnap = !!process.env.SNAP;
/**
* @deprecated Switch to using isTestEnvInjectable
*/
export const isTestEnv = !!process.env.JEST_WORKER_ID;
/**
* @deprecated Switch to using isProductionInjectable
*/
@ -41,7 +36,7 @@ export const isProduction = process.env.NODE_ENV === "production";
/**
* @deprecated Switch to using isDevelopmentInjectable
*/
export const isDevelopment = !isTestEnv && !isProduction;
export const isDevelopment = !isProduction;
export const productName = packageInfo.productName;

View File

@ -4,17 +4,11 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import isProductionInjectable from "./is-production.injectable";
import isTestEnvInjectable from "./is-test-env.injectable";
const isDevelopmentInjectable = getInjectable({
id: "is-development",
instantiate: (di) => {
const isProduction = di.inject(isProductionInjectable);
const isTestEnv = di.inject(isTestEnvInjectable);
return !isTestEnv && !isProduction;
},
instantiate: (di) => !di.inject(isProductionInjectable),
});
export default isDevelopmentInjectable;

View File

@ -1,18 +0,0 @@
/**
* 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 environmentVariablesInjectable from "../utils/environment-variables.injectable";
const isTestEnvInjectable = getInjectable({
id: "is-test-env",
instantiate: (di) => {
const { JEST_WORKER_ID: jestWorkerId } = di.inject(environmentVariablesInjectable);
return !!jestWorkerId;
},
});
export default isTestEnvInjectable;

View File

@ -0,0 +1,12 @@
/**
* 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 { Migrations } from "conf/dist/source/types";
import type { WeblinkStoreModel } from "./store";
export const weblinksStoreMigrationsInjectionToken = getInjectionToken<Migrations<WeblinkStoreModel> | undefined>({
id: "weblinks-store-migrations-token",
});

View File

@ -3,15 +3,18 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { WeblinkStore } from "./weblink-store";
import { weblinksStoreMigrationsInjectionToken } from "./migrations";
import { WeblinkStore } from "./store";
const weblinkStoreInjectable = getInjectable({
id: "weblink-store",
instantiate: () => {
instantiate: (di) => {
WeblinkStore.resetInstance();
return WeblinkStore.createInstance();
return WeblinkStore.createInstance({
migrations: di.inject(weblinksStoreMigrationsInjectionToken),
});
},
});

View File

@ -4,10 +4,10 @@
*/
import { action, comparer, observable, makeObservable } from "mobx";
import { BaseStore } from "./base-store";
import migrations from "../migrations/weblinks-store";
import { BaseStore } from "../base-store";
import * as uuid from "uuid";
import { toJS } from "./utils";
import { toJS } from "../utils";
import type { Migrations } from "conf/dist/source/types";
export interface WeblinkData {
id: string;
@ -26,18 +26,22 @@ export interface WeblinkStoreModel {
weblinks: WeblinkData[];
}
export interface WeblinkStoreDependencies {
readonly migrations: Migrations<WeblinkStoreModel> | undefined;
}
export class WeblinkStore extends BaseStore<WeblinkStoreModel> {
readonly displayName = "WeblinkStore";
@observable weblinks: WeblinkData[] = [];
constructor() {
constructor(protected readonly dependencies: WeblinkStoreDependencies) {
super({
configName: "lens-weblink-store",
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
syncOptions: {
equals: comparer.structural,
},
migrations,
migrations: dependencies.migrations,
});
makeObservable(this);
this.load();

View File

@ -4,7 +4,7 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import { syncWeblinks } from "./weblinks";
import weblinkStoreInjectable from "../../common/weblink-store.injectable";
import weblinkStoreInjectable from "../../common/weblinks/store.injectable";
import catalogEntityRegistryInjectable from "../catalog/entity-registry.injectable";
const syncWeblinksInjectable = getInjectable({

View File

@ -4,7 +4,7 @@
*/
import { computed, observable, reaction } from "mobx";
import type { WeblinkStore } from "../../common/weblink-store";
import type { WeblinkStore } from "../../common/weblinks/store";
import { WebLink } from "../../common/catalog-entities";
import type { CatalogEntityRegistry } from "../catalog";
import got from "got";

View File

@ -101,6 +101,7 @@ import { registerMobX } from "@ogre-tools/injectable-extension-for-mobx";
import electronInjectable from "./utils/resolve-system-proxy/electron.injectable";
import type { HotbarStore } from "../common/hotbars/store";
import focusApplicationInjectable from "./electron-app/features/focus-application.injectable";
import migrationLogInjectable from "./migrations/log.injectable";
export function getDiForUnitTesting(opts: { doGeneralOverrides?: boolean } = {}) {
const {
@ -127,7 +128,7 @@ export function getDiForUnitTesting(opts: { doGeneralOverrides?: boolean } = {})
di.override(electronInjectable, () => ({}));
di.override(waitUntilBundledExtensionsAreLoadedInjectable, () => async () => {});
di.override(getRandomIdInjectable, () => () => "some-irrelevant-random-id");
di.override(migrationLogInjectable, () => noop);
di.override(hotbarStoreInjectable, () => ({
load: () => {},
getActive: () => ({ name: "some-hotbar", items: [] }),

View File

@ -0,0 +1,102 @@
/**
* 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 { loadConfigFromString } from "../../../common/kube-helpers";
import type { ClusterModel } from "../../../common/cluster-types";
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 { getInjectable } from "@ogre-tools/injectable";
import { clusterStoreMigrationDeclarationInjectionToken } from "./migration";
import migrationLogInjectable from "../log.injectable";
import fsInjectable from "../../../common/fs/fs.injectable";
import readFileSyncInjectable from "../../../common/fs/read-file-sync.injectable";
import joinPathsInjectable from "../../../common/path/join-paths.injectable";
interface Pre360ClusterModel extends ClusterModel {
kubeConfig?: string;
}
const clusterStoreV360Beta1MigrationInjectable = getInjectable({
id: "cluster-store-v3.6.0-beta.1-migration",
instantiate: (di) => {
const migrationLog = di.inject(migrationLogInjectable);
const userDataPath = di.inject(directoryForUserDataInjectable);
const kubeConfigsPath = di.inject(directoryForKubeConfigsInjectable);
const getCustomKubeConfigDirectory = di.inject(getCustomKubeConfigDirectoryInjectable);
const { ensureDirSync, writeFileSync } = di.inject(fsInjectable);
const readFileSync = di.inject(readFileSyncInjectable);
const joinPaths = di.inject(joinPathsInjectable);
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;
delete clusterModel.kubeConfig;
const clusterConfigData = readFileSync(clusterModel.kubeConfigPath);
const clusterConfig = loadConfigFromString(clusterConfigData);
clusterModel.contextName = clusterConfig.config.getCurrentContext();
} 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 = readFileSync(joinPaths(userDataPath, iconPath), { asBuffer: true });
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: clusterStoreMigrationDeclarationInjectionToken,
});
export default clusterStoreV360Beta1MigrationInjectable;

View File

@ -0,0 +1,65 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ClusterModel } from "../../../common/cluster-types";
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import { isErrnoException } from "../../../common/utils";
import { getInjectable } from "@ogre-tools/injectable";
import { clusterStoreMigrationDeclarationInjectionToken } from "./migration";
import fsInjectable from "../../../common/fs/fs.injectable";
import joinPathsInjectable from "../../../common/path/join-paths.injectable";
interface Pre500WorkspaceStoreModel {
workspaces: {
id: string;
name: string;
}[];
}
const clusterStoreV500Beta10MigrationInjectable = getInjectable({
id: "clutster-store-v5.0.0-beta.10-migration",
instantiate: (di) => {
const userDataPath = di.inject(directoryForUserDataInjectable);
const { readJsonSync } = di.inject(fsInjectable);
const joinPaths = di.inject(joinPathsInjectable);
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: clusterStoreMigrationDeclarationInjectionToken,
});
export default clusterStoreV500Beta10MigrationInjectable;

View File

@ -0,0 +1,137 @@
/**
* 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 { generateNewIdFor } from "../utils";
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import { isDefined } from "../../../common/utils";
import { getInjectable } from "@ogre-tools/injectable";
import { clusterStoreMigrationDeclarationInjectionToken } from "./migration";
import migrationLogInjectable from "../log.injectable";
import fsInjectable from "../../../common/fs/fs.injectable";
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 ?? []),
};
}
const clusterStoreV500Beta13MigrationInjectable = getInjectable({
id: "cluster-store-v5.0.0-beta.13-migration",
instantiate: (di) => {
const migrationLog = di.inject(migrationLogInjectable);
const userDataPath = di.inject(directoryForUserDataInjectable);
const joinPaths = di.inject(joinPathsInjectable);
const { moveSync, removeSync } = di.inject(fsInjectable);
const moveStorageFolder = ({ folder, newId, oldId }: { folder: string; newId: string; oldId: string }) => {
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")) {
migrationLog(`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) {
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 });
}
}
store.set("clusters", [...clusters.values()]);
},
};
},
injectionToken: clusterStoreMigrationDeclarationInjectionToken,
});
export default clusterStoreV500Beta13MigrationInjectable;

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 "../declaration";
export const clusterStoreMigrationDeclarationInjectionToken = getInjectionToken<MigrationDeclaration>({
id: "cluster-store-migration-declaration-token",
});

View File

@ -0,0 +1,23 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
// Cluster store migrations
import joinMigrationsInjectable from "../join.injectable";
import { getInjectable } from "@ogre-tools/injectable";
import { clusterStoreMigrationDeclarationInjectionToken } from "./migration";
const clusterStoreMigrationsInjectable = getInjectable({
id: "cluster-store-migrations",
instantiate: (di) => {
const joinMigrations = di.inject(joinMigrationsInjectable);
const migrationDeclarations = di.injectMany(clusterStoreMigrationDeclarationInjectionToken);
return joinMigrations(migrationDeclarations);
},
});
export default clusterStoreMigrationsInjectable;

View File

@ -0,0 +1,55 @@
/**
* 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 { getInjectable } from "@ogre-tools/injectable";
import { clusterStoreMigrationDeclarationInjectionToken } from "./migration";
import migrationLogInjectable from "../log.injectable";
import appVersionInjectable from "../../../common/get-configuration-file-model/app-version/app-version.injectable";
import fsInjectable from "../../../common/fs/fs.injectable";
const clusterStoreSnapMigrationInjectable = getInjectable({
id: "cluster-store-snap-migration",
instantiate: (di) => {
const migrationLog = di.inject(migrationLogInjectable);
const { existsSync } = di.inject(fsInjectable);
return {
version: di.inject(appVersionInjectable), // Run always after upgrade
run(store) {
if (!process.env.SNAP) return;
migrationLog("Migrating embedded kubeconfig paths");
const storedClusters = (store.get("clusters") || []) as ClusterModel[];
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 (!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: clusterStoreMigrationDeclarationInjectionToken,
});
export default clusterStoreSnapMigrationInjectable;

View File

@ -0,0 +1,10 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type Conf from "conf";
export interface MigrationDeclaration {
version: string;
run(store: Conf<Record<string, unknown>>): void;
}

View File

@ -0,0 +1,32 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getEmptyHotbar } from "../../../common/hotbars/types";
import catalogCatalogEntityInjectable from "../../../common/catalog-entities/general-catalog-entities/implementations/catalog-catalog-entity.injectable";
import { getInjectable } from "@ogre-tools/injectable";
import { hotbarStoreMigrationDeclarationInjectionToken } from "./migration";
const hotbarStoreV500Alpha0MigrationInjectable = getInjectable({
id: "hotbar-store-v5.0.0-alpha.0-migration",
instantiate: (di) => {
const catalogCatalogEntity = di.inject(catalogCatalogEntityInjectable);
return {
version: "5.0.0-alpha.0",
run(store) {
const hotbar = getEmptyHotbar("default");
const { metadata: { uid, name, source }} = catalogCatalogEntity;
hotbar.items[0] = { entity: { uid, name, source }};
store.set("hotbars", [hotbar]);
},
};
},
injectionToken: hotbarStoreMigrationDeclarationInjectionToken,
});
export default hotbarStoreV500Alpha0MigrationInjectable;

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Hotbar } from "../../../common/hotbars/types";
import * as uuid from "uuid";
import { getInjectable } from "@ogre-tools/injectable";
import { hotbarStoreMigrationDeclarationInjectionToken } from "./migration";
const hotbarStoreV500Alpha2MigrationInjectable = getInjectable({
id: "hotbar-store-v5.0.0-alpha.2-migration",
instantiate: () => ( {
version: "5.0.0-alpha.2",
run(store) {
const rawHotbars = store.get("hotbars");
const hotbars: Hotbar[] = Array.isArray(rawHotbars) ? rawHotbars : [];
store.set("hotbars", hotbars.map(({ id, ...rest }) => ({
id: id || uuid.v4(),
...rest,
})));
},
}),
injectionToken: hotbarStoreMigrationDeclarationInjectionToken,
});
export default hotbarStoreV500Alpha2MigrationInjectable;

View File

@ -0,0 +1,174 @@
/**
* 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 { isNull } from "lodash";
import path from "path";
import * as uuid from "uuid";
import type { ClusterStoreModel } from "../../../common/cluster-store/cluster-store";
import type { Hotbar, HotbarItem } from "../../../common/hotbars/types";
import { defaultHotbarCells, getEmptyHotbar } from "../../../common/hotbars/types";
import { generateNewIdFor } from "../utils";
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 catalogCatalogEntityInjectable from "../../../common/catalog-entities/general-catalog-entities/implementations/catalog-catalog-entity.injectable";
import { isDefined, isErrnoException } from "../../../common/utils";
interface Pre500WorkspaceStoreModel {
workspaces: {
id: string;
name: string;
}[];
}
interface PartialHotbar {
id: string;
name: string;
items: (null | HotbarItem)[];
}
import { getInjectable } from "@ogre-tools/injectable";
import migrationLogInjectable from "../log.injectable";
import { hotbarStoreMigrationDeclarationInjectionToken } from "./migration";
const hotbarStoreV500Beta10MigrationInjectable = getInjectable({
id: "hotbar-store-v5.0.0-beta.10-migration",
instantiate: (di) => {
const migrationLog = di.inject(migrationLogInjectable);
const userDataPath = di.inject(directoryForUserDataInjectable);
return {
version: "5.0.0-beta.10",
run(store) {
const rawHotbars = store.get("hotbars");
const hotbars: Hotbar[] = Array.isArray(rawHotbars) ? rawHotbars.filter(h => h && typeof h === "object") : [];
// Hotbars might be empty, if some of the previous migrations weren't run
if (hotbars.length === 0) {
const hotbar = getEmptyHotbar("default");
const di = getLegacyGlobalDiForExtensionApi();
const catalogCatalogEntity = di.inject(catalogCatalogEntityInjectable);
const { metadata: { uid, name, source }} = catalogCatalogEntity;
hotbar.items[0] = { entity: { uid, name, source }};
hotbars.push(hotbar);
}
try {
const workspaceStoreData: Pre500WorkspaceStoreModel = fse.readJsonSync(path.join(userDataPath, "lens-workspace-store.json"));
const { clusters = [] }: ClusterStoreModel = fse.readJSONSync(path.join(userDataPath, "lens-cluster-store.json"));
const workspaceHotbars = new Map<string, PartialHotbar>(); // mapping from WorkspaceId to HotBar
for (const { id, name } of workspaceStoreData.workspaces) {
migrationLog(`Creating new hotbar for ${name}`);
workspaceHotbars.set(id, {
id: uuid.v4(), // don't use the old IDs as they aren't necessarily UUIDs
items: [],
name: `Workspace: ${name}`,
});
}
{
// grab the default named hotbar or the first.
const defaultHotbarIndex = Math.max(0, hotbars.findIndex(hotbar => hotbar.name === "default"));
const [{ name, id, items }] = hotbars.splice(defaultHotbarIndex, 1);
workspaceHotbars.set("default", {
name,
id,
items: items.filter(isDefined),
});
}
for (const cluster of clusters) {
const uid = generateNewIdFor(cluster);
for (const workspaceId of cluster.workspaces ?? [cluster.workspace].filter(isDefined)) {
const workspaceHotbar = workspaceHotbars.get(workspaceId);
if (!workspaceHotbar) {
migrationLog(`Cluster ${uid} has unknown workspace ID, skipping`);
continue;
}
migrationLog(`Adding cluster ${uid} to ${workspaceHotbar.name}`);
if (workspaceHotbar?.items.length < defaultHotbarCells) {
workspaceHotbar.items.push({
entity: {
uid: generateNewIdFor(cluster),
name: cluster.preferences?.clusterName || cluster.contextName,
},
});
}
}
}
for (const hotbar of workspaceHotbars.values()) {
if (hotbar.items.length === 0) {
migrationLog(`Skipping ${hotbar.name} due to it being empty`);
continue;
}
while (hotbar.items.length < defaultHotbarCells) {
hotbar.items.push(null);
}
hotbars.push(hotbar as Hotbar);
}
/**
* Finally, make sure that the catalog entity hotbar item is in place.
* Just in case something else removed it.
*
* if every hotbar has elements that all not the `catalog-entity` item
*/
if (hotbars.every(hotbar => hotbar.items.every(item => item?.entity?.uid !== "catalog-entity"))) {
// note, we will add a new whole hotbar here called "default" if that was previously removed
const di = getLegacyGlobalDiForExtensionApi();
const catalogCatalogEntity = di.inject(catalogCatalogEntityInjectable);
const defaultHotbar = hotbars.find(hotbar => hotbar.name === "default");
const { metadata: { uid, name, source }} = catalogCatalogEntity;
if (defaultHotbar) {
const freeIndex = defaultHotbar.items.findIndex(isNull);
if (freeIndex === -1) {
// making a new hotbar is less destructive if the first hotbar
// called "default" is full than overriding a hotbar item
const hotbar = getEmptyHotbar("initial");
hotbar.items[0] = { entity: { uid, name, source }};
hotbars.unshift(hotbar);
} else {
defaultHotbar.items[freeIndex] = { entity: { uid, name, source }};
}
} else {
const hotbar = getEmptyHotbar("default");
hotbar.items[0] = { entity: { uid, name, source }};
hotbars.unshift(hotbar);
}
}
} catch (error) {
// ignore files being missing
if (isErrnoException(error) && error.code !== "ENOENT") {
throw error;
}
}
store.set("hotbars", hotbars);
},
};
},
injectionToken: hotbarStoreMigrationDeclarationInjectionToken,
});
export default hotbarStoreV500Beta10MigrationInjectable;

View File

@ -0,0 +1,54 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Hotbar } from "../../../common/hotbars/types";
import catalogEntityRegistryInjectable from "../../catalog/entity-registry.injectable";
import { getInjectable } from "@ogre-tools/injectable";
import { hotbarStoreMigrationDeclarationInjectionToken } from "./migration";
const hotbarStoreV500Beta5MigrationInjectable = getInjectable({
id: "hotbar-store-v5.0.0-beta.5-migration",
instantiate: (di) => {
const catalogEntityRegistry = di.inject(catalogEntityRegistryInjectable);
return {
version: "5.0.0-beta.5",
run(store) {
const rawHotbars = store.get("hotbars");
const hotbars: Hotbar[] = Array.isArray(rawHotbars) ? rawHotbars : [];
for (const hotbar of hotbars) {
for (let i = 0; i < hotbar.items.length; i += 1) {
const item = hotbar.items[i];
if (!item) {
continue;
}
const entity = catalogEntityRegistry.findById(item.entity.uid);
if (!entity) {
// Clear disabled item
hotbar.items[i] = null;
} else {
// Save additional data
item.entity = {
...item.entity,
name: entity.metadata.name,
source: entity.metadata.source,
};
}
}
}
store.set("hotbars", hotbars);
},
};
},
injectionToken: hotbarStoreMigrationDeclarationInjectionToken,
});
export default hotbarStoreV500Beta5MigrationInjectable;

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 "../declaration";
export const hotbarStoreMigrationDeclarationInjectionToken = getInjectionToken<MigrationDeclaration>({
id: "hotbar-store-migration-declaration-token",
});

View File

@ -0,0 +1,22 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import joinMigrationsInjectable from "../join.injectable";
import { getInjectable } from "@ogre-tools/injectable";
import { hotbarStoreMigrationsInjectionToken } from "../../../common/hotbars/migrations";
import { hotbarStoreMigrationDeclarationInjectionToken } from "./migration";
const hotbarStoreMigrationsInjectable = getInjectable({
id: "hotbar-store-migrations",
instantiate: (di) => {
const joinMigrations = di.inject(joinMigrationsInjectable);
const migrationDeclarations = di.injectMany(hotbarStoreMigrationDeclarationInjectionToken);
return joinMigrations(migrationDeclarations);
},
injectionToken: hotbarStoreMigrationsInjectionToken,
});
export default hotbarStoreMigrationsInjectable;

View File

@ -0,0 +1,41 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Migrations } from "conf/dist/source/types";
import { getOrInsert, iter } from "../../common/utils";
import type { MigrationDeclaration } from "./declaration";
import { getInjectable } from "@ogre-tools/injectable";
import migrationLogInjectable from "./log.injectable";
const joinMigrationsInjectable = getInjectable({
id: "join-migrations",
instantiate: (di) => {
const migrationLog = di.inject(migrationLogInjectable);
return (declarations: MigrationDeclaration[]): Migrations<any> => {
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) => {
migrationLog(`Running ${v} migration for ${store.path}`);
for (const fn of fns) {
fn(store);
}
}],
),
);
};
},
});
export default joinMigrationsInjectable;

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 loggerInjectable from "../../common/logger.injectable";
export type MigrationLog = (message: string, ...args: any[]) => void;
const migrationLogInjectable = getInjectable({
id: "migration-log",
instantiate: (di): MigrationLog => {
const logger = di.inject(loggerInjectable);
return (...args) => logger.info(...args);
},
});
export default migrationLogInjectable;

View File

@ -0,0 +1,21 @@
/**
* 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 { userStoreMigrationDeclarationInjectionToken } from "./migration";
const userStoreV210Beta4MigrationInjectable = getInjectable({
id: "user-store-v2.1.0-beta.4-migration",
instantiate: () => ({
version: "2.1.0-beta.4",
run(store) {
store.set("lastSeenAppVersion", "0.0.0");
},
}),
injectionToken: userStoreMigrationDeclarationInjectionToken,
});
export default userStoreV210Beta4MigrationInjectable;

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { hasTypedProperty, isObject } from "../../../common/utils";
import { getInjectable } from "@ogre-tools/injectable";
import { userStoreMigrationDeclarationInjectionToken } from "./migration";
const userStoreV500Alpha3MigrationInjectable = getInjectable({
id: "user-store-v5.0.0-alpha.3-migration",
instantiate: () => ({
version: "5.0.0-alpha.3",
run(store) {
const preferences = store.get("preferences");
if (!isObject(preferences)) {
store.delete("preferences");
} else if (hasTypedProperty(preferences, "hiddenTableColumns", isObject)) {
preferences.hiddenTableColumns = Object.entries(preferences.hiddenTableColumns);
store.set("preferences", preferences);
}
},
}),
injectionToken: userStoreMigrationDeclarationInjectionToken,
});
export default userStoreV500Alpha3MigrationInjectable;

View File

@ -0,0 +1,101 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { existsSync } from "fs";
import path from "path";
import os from "os";
import type { ClusterStoreModel } from "../../../common/cluster-store/cluster-store";
import type { KubeconfigSyncEntry } from "../../../common/user-store";
import { hasTypedProperty, isErrnoException, isLogicalChildPath } from "../../../common/utils";
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 { getInjectable } from "@ogre-tools/injectable";
import migrationLogInjectable from "../log.injectable";
import { userStoreMigrationDeclarationInjectionToken } from "./migration";
import { isObject } from "lodash";
import readFileSyncInjectable from "../../../common/fs/read-file-sync.injectable";
import joinPathsInjectable from "../../../common/path/join-paths.injectable";
const userStoreV503Beta1MigrationInjectable = getInjectable({
id: "user-store-v5.0.3-beta.1-migration",
instantiate: (di) => {
const migrationLog = di.inject(migrationLogInjectable);
const userDataPath = di.inject(directoryForUserDataInjectable);
const kubeConfigsPath = di.inject(directoryForKubeConfigsInjectable);
const readFileSync = di.inject(readFileSyncInjectable);
const joinPaths = di.inject(joinPathsInjectable);
return {
version: "5.0.3-beta.1",
run(store) {
try {
const preferences = store.get("preferences");
if (!isObject(preferences)) {
store.delete("preferences");
return;
}
if (!hasTypedProperty(preferences, "syncKubeconfigEntries", Array.isArray)) {
return;
}
const { syncKubeconfigEntries } = preferences;
const { clusters = [] }: ClusterStoreModel = JSON.parse(readFileSync(joinPaths(userDataPath, "lens-cluster-store.json"))) ?? {};
const extensionDataDir = joinPaths(userDataPath, "extension_data");
const syncPaths = new Set(syncKubeconfigEntries.map(s => s.filePath));
syncPaths.add(path.join(os.homedir(), ".kube"));
for (const cluster of clusters) {
if (!cluster.kubeConfigPath) {
continue;
}
const dirOfKubeconfig = path.dirname(cluster.kubeConfigPath);
if (dirOfKubeconfig === kubeConfigsPath) {
migrationLog(`Skipping ${cluster.id} because kubeConfigPath is under the stored KubeConfig folder`);
continue;
}
if (syncPaths.has(cluster.kubeConfigPath) || syncPaths.has(dirOfKubeconfig)) {
migrationLog(`Skipping ${cluster.id} because kubeConfigPath is already being synced`);
continue;
}
if (isLogicalChildPath(extensionDataDir, cluster.kubeConfigPath)) {
migrationLog(`Skipping ${cluster.id} because kubeConfigPath is placed under an extension_data folder`);
continue;
}
if (!existsSync(cluster.kubeConfigPath)) {
migrationLog(`Skipping ${cluster.id} because kubeConfigPath no longer exists`);
continue;
}
migrationLog(`Adding ${cluster.kubeConfigPath} from ${cluster.id} to sync paths`);
syncPaths.add(cluster.kubeConfigPath);
}
const updatedSyncEntries: KubeconfigSyncEntry[] = [...syncPaths].map(filePath => ({ filePath }));
migrationLog("Final list of synced paths", updatedSyncEntries);
store.set("preferences", { ...preferences, syncKubeconfigEntries: updatedSyncEntries });
} catch (error) {
if (isErrnoException(error) && error.code !== "ENOENT") {
// ignore files being missing
throw error;
}
}
},
};
},
injectionToken: userStoreMigrationDeclarationInjectionToken,
});
export default userStoreV503Beta1MigrationInjectable;

View File

@ -5,13 +5,14 @@
import fse from "fs-extra";
import path from "path";
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
import { isErrnoException } from "../utils";
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import { isErrnoException } from "../../../common/utils";
import { getInjectable } from "@ogre-tools/injectable";
import { userStorePreMigrationsInjectionToken } from "../../../common/user-store/migrations";
const userStoreFileNameMigrationInjectable = getInjectable({
id: "user-store-file-name-migration",
instantiate: (di) => {
instantiate: (di) => () => {
const userDataPath = di.inject(directoryForUserDataInjectable);
const configJsonPath = path.join(userDataPath, "config.json");
const lensUserStoreJsonPath = path.join(userDataPath, "lens-user-store.json");
@ -30,6 +31,7 @@ const userStoreFileNameMigrationInjectable = getInjectable({
}
}
},
injectionToken: userStorePreMigrationsInjectionToken,
});
export default userStoreFileNameMigrationInjectable;

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 "../declaration";
export const userStoreMigrationDeclarationInjectionToken = getInjectionToken<MigrationDeclaration>({
id: "user-store-migration-declaration-token",
});

View File

@ -0,0 +1,23 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import joinMigrationsInjectable from "../join.injectable";
import { getInjectable } from "@ogre-tools/injectable";
import { userStoreMigrationDeclarationInjectionToken } from "./migration";
import { userStoreMigrationsInjectionToken } from "../../../common/user-store/migrations";
const userStoreMigrationsInjectable = getInjectable({
id: "user-store-migrations",
instantiate: (di) => {
const joinMigrations = di.inject(joinMigrationsInjectable);
const migrationDeclarataions = di.injectMany(userStoreMigrationDeclarationInjectionToken);
return joinMigrations(migrationDeclarataions);
},
injectionToken: userStoreMigrationsInjectionToken,
});
export default userStoreMigrationsInjectable;

View File

@ -0,0 +1,42 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { docsUrl, slackUrl } from "../../../common/vars";
import type { WeblinkData } from "../../../common/weblinks/store";
import { getInjectable } from "@ogre-tools/injectable";
import { weblinksStoreMigrationDeclarationInjectionToken } from "./migration";
export const lensWebsiteLinkName = "Lens Website";
export const lensDocumentationWeblinkName = "Lens Documentation";
export const lensSlackWeblinkName = "Lens Community Slack";
export const lensTwitterWeblinkName = "Lens on Twitter";
export const lensBlogWeblinkName = "Lens Official Blog";
export const kubernetesDocumentationWeblinkName = "Kubernetes Documentation";
const weblinksStoreV514MigrationInjectable = getInjectable({
id: "weblinks-store-v5.1.4-migration",
instantiate: () => ({
version: "5.1.4",
run(store) {
const weblinksRaw = store.get("weblinks");
const weblinks = (Array.isArray(weblinksRaw) ? weblinksRaw : []) as WeblinkData[];
weblinks.push(
{ id: "https://k8slens.dev", name: lensWebsiteLinkName, url: "https://k8slens.dev" },
{ id: docsUrl, name: lensDocumentationWeblinkName, url: docsUrl },
{ id: slackUrl, name: lensSlackWeblinkName, url: slackUrl },
{ id: "https://twitter.com/k8slens", name: lensTwitterWeblinkName, url: "https://twitter.com/k8slens" },
{ id: "https://medium.com/k8slens", name: lensBlogWeblinkName, url: "https://medium.com/k8slens" },
{ id: "https://kubernetes.io/docs/home/", name: kubernetesDocumentationWeblinkName, url: "https://kubernetes.io/docs/home/" },
);
store.set("weblinks", weblinks);
},
}),
injectionToken: weblinksStoreMigrationDeclarationInjectionToken,
});
export default weblinksStoreV514MigrationInjectable;

View File

@ -0,0 +1,64 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { kubernetesDocumentationWeblinkId, lensBlogWeblinkId, lensDocumentationWeblinkId, lensSlackWeblinkId, lensTwitterWeblinkId, lensWebsiteWeblinkId } from "../../../common/vars";
import type { WeblinkData } from "../../../common/weblinks/store";
import { kubernetesDocumentationWeblinkName, lensBlogWeblinkName, lensDocumentationWeblinkName, lensSlackWeblinkName, lensTwitterWeblinkName, lensWebsiteLinkName } from "./5.1.4.injectable";
import { getInjectable } from "@ogre-tools/injectable";
import { weblinksStoreMigrationDeclarationInjectionToken } from "./migration";
const weblinksStoreV545Beta1MigrationInjectable = getInjectable({
id: "weblinks-store-v5.4.5-beta.1-migration",
instantiate: () => ({
version: "5.4.5-beta.1 || >=5.5.0-alpha.0",
run(store) {
const weblinksRaw = store.get("weblinks");
const weblinks = (Array.isArray(weblinksRaw) ? weblinksRaw : []) as WeblinkData[];
const lensWebsiteLink = weblinks.find(weblink => weblink.name === lensWebsiteLinkName);
if (lensWebsiteLink) {
lensWebsiteLink.id = lensWebsiteWeblinkId;
}
const lensDocumentationWeblinkLink = weblinks.find(weblink => weblink.name === lensDocumentationWeblinkName);
if (lensDocumentationWeblinkLink) {
lensDocumentationWeblinkLink.id = lensDocumentationWeblinkId;
}
const lensSlackWeblinkLink = weblinks.find(weblink => weblink.name === lensSlackWeblinkName);
if (lensSlackWeblinkLink) {
lensSlackWeblinkLink.id = lensSlackWeblinkId;
}
const lensTwitterWeblinkLink = weblinks.find(weblink => weblink.name === lensTwitterWeblinkName);
if (lensTwitterWeblinkLink) {
lensTwitterWeblinkLink.id = lensTwitterWeblinkId;
}
const lensBlogWeblinkLink = weblinks.find(weblink => weblink.name === lensBlogWeblinkName);
if (lensBlogWeblinkLink) {
lensBlogWeblinkLink.id = lensBlogWeblinkId;
}
const kubernetesDocumentationWeblinkLink = weblinks.find(weblink => weblink.name === kubernetesDocumentationWeblinkName);
if (kubernetesDocumentationWeblinkLink) {
kubernetesDocumentationWeblinkLink.id = kubernetesDocumentationWeblinkId;
}
store.set("weblinks", weblinks);
},
}),
injectionToken: weblinksStoreMigrationDeclarationInjectionToken,
});
export default weblinksStoreV545Beta1MigrationInjectable;

View File

@ -0,0 +1,32 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { lensSlackWeblinkId, slackUrl } from "../../../common/vars";
import type { WeblinkData } from "../../../common/weblinks/store";
import { getInjectable } from "@ogre-tools/injectable";
import { weblinksStoreMigrationDeclarationInjectionToken } from "./migration";
import appVersionInjectable from "../../../common/get-configuration-file-model/app-version/app-version.injectable";
const weblinksStoreCurrentVersionMigrationInjectable = getInjectable({
id: "weblinks-store-current-version-migration",
instantiate: (di) => ({
version: di.inject(appVersionInjectable), // Run always after upgrade
run(store) {
const weblinksRaw = store.get("weblinks");
const weblinks = (Array.isArray(weblinksRaw) ? weblinksRaw : []) as WeblinkData[];
const slackWeblink = weblinks.find(weblink => weblink.id === lensSlackWeblinkId);
if (slackWeblink) {
slackWeblink.url = slackUrl;
}
store.set("weblinks", weblinks);
},
}),
injectionToken: weblinksStoreMigrationDeclarationInjectionToken,
});
export default weblinksStoreCurrentVersionMigrationInjectable;

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 "../declaration";
export const weblinksStoreMigrationDeclarationInjectionToken = getInjectionToken<MigrationDeclaration>({
id: "weblinks-store-migration-declaration-token",
});

View File

@ -0,0 +1,23 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import joinMigrationsInjectable from "../join.injectable";
import { getInjectable } from "@ogre-tools/injectable";
import { weblinksStoreMigrationDeclarationInjectionToken } from "./migration";
import { weblinksStoreMigrationsInjectionToken } from "../../../common/weblinks/migrations";
const weblinksStoreMigrationsInjectable = getInjectable({
id: "weblinks-store-migrations",
instantiate: (di) => {
const joinMigrations = di.inject(joinMigrationsInjectable);
const migrationDeclarations = di.injectMany(weblinksStoreMigrationDeclarationInjectionToken);
return joinMigrations(migrationDeclarations);
},
injectionToken: weblinksStoreMigrationsInjectionToken,
});
export default weblinksStoreMigrationsInjectable;

View File

@ -1,90 +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 path from "path";
import fse from "fs-extra";
import { loadConfigFromFileSync } 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";
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 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 = loadConfigFromFileSync(clusterModel.kubeConfigPath).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(path.join(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,56 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import path from "path";
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";
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);
try {
const workspaceData: Pre500WorkspaceStoreModel = fse.readJsonSync(path.join(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,128 +0,0 @@
/**
* 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 type { MigrationDeclaration } from "../helpers";
import { migrationLog } from "../helpers";
import { generateNewIdFor } from "../utils";
import path from "path";
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";
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 ?? []),
};
}
function moveStorageFolder({ folder, newId, oldId }: { folder: string; newId: string; oldId: string }): void {
const oldPath = path.resolve(folder, `${oldId}.json`);
const newPath = path.resolve(folder, `${newId}.json`);
try {
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`);
removeSync(oldPath);
}
}
}
export default {
version: "5.0.0-beta.13",
run(store) {
const di = getLegacyGlobalDiForExtensionApi();
const userDataPath = di.inject(directoryForUserDataInjectable);
const folder = path.resolve(userDataPath, "lens-local-storage");
const oldClusters: ClusterModel[] = store.get("clusters") ?? [];
const clusters = new Map<string, ClusterModel>();
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 });
}
}
store.set("clusters", [...clusters.values()]);
},
} 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,42 +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 { getAppVersion } from "../../common/utils/app-version";
import fs from "fs";
import type { MigrationDeclaration } from "../helpers";
import { migrationLog } from "../helpers";
export default {
version: getAppVersion(), // 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

@ -1,41 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type Conf from "conf";
import type { Migrations } from "conf/dist/source/types";
import { getOrInsert, iter } from "../common/utils";
import { isTestEnv } from "../common/vars";
export function migrationLog(...args: any[]) {
if (!isTestEnv) {
console.log(...args);
}
}
export interface MigrationDeclaration {
version: string;
run(store: Conf<any>): void;
}
export function joinMigrations(...declarations: MigrationDeclaration[]): Migrations<any> {
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: Conf<any>) => {
migrationLog(`Running ${v} migration for ${store.path}`);
for (const fn of fns) {
fn(store);
}
}],
),
);
}

View File

@ -1,25 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
// Cleans up a store that had the state related data stored
import type { MigrationDeclaration } from "../helpers";
import { getEmptyHotbar } from "../../common/hotbars/types";
import { getLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
import catalogCatalogEntityInjectable from "../../common/catalog-entities/general-catalog-entities/implementations/catalog-catalog-entity.injectable";
export default {
version: "5.0.0-alpha.0",
run(store) {
const hotbar = getEmptyHotbar("default");
const di = getLegacyGlobalDiForExtensionApi();
const catalogCatalogEntity = di.inject(catalogCatalogEntityInjectable);
const { metadata: { uid, name, source }} = catalogCatalogEntity;
hotbar.items[0] = { entity: { uid, name, source }};
store.set("hotbars", [hotbar]);
},
} as MigrationDeclaration;

View File

@ -1,22 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
// Cleans up a store that had the state related data stored
import type { Hotbar } from "../../common/hotbars/types";
import * as uuid from "uuid";
import type { MigrationDeclaration } from "../helpers";
export default {
version: "5.0.0-alpha.2",
run(store) {
const rawHotbars = store.get("hotbars");
const hotbars: Hotbar[] = Array.isArray(rawHotbars) ? rawHotbars : [];
store.set("hotbars", hotbars.map(({ id, ...rest }) => ({
id: id || uuid.v4(),
...rest,
})));
},
} as MigrationDeclaration;

View File

@ -1,165 +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 { isNull } from "lodash";
import path from "path";
import * as uuid from "uuid";
import type { ClusterStoreModel } from "../../common/cluster-store/cluster-store";
import type { Hotbar, HotbarItem } from "../../common/hotbars/types";
import { defaultHotbarCells, getEmptyHotbar } from "../../common/hotbars/types";
import type { MigrationDeclaration } from "../helpers";
import { migrationLog } from "../helpers";
import { generateNewIdFor } from "../utils";
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 catalogCatalogEntityInjectable from "../../common/catalog-entities/general-catalog-entities/implementations/catalog-catalog-entity.injectable";
import { isDefined, isErrnoException } from "../../common/utils";
interface Pre500WorkspaceStoreModel {
workspaces: {
id: string;
name: string;
}[];
}
interface PartialHotbar {
id: string;
name: string;
items: (null | HotbarItem)[];
}
export default {
version: "5.0.0-beta.10",
run(store) {
const rawHotbars = store.get("hotbars");
const hotbars: Hotbar[] = Array.isArray(rawHotbars) ? rawHotbars.filter(h => h && typeof h === "object") : [];
const di = getLegacyGlobalDiForExtensionApi();
const userDataPath = di.inject(directoryForUserDataInjectable);
// Hotbars might be empty, if some of the previous migrations weren't run
if (hotbars.length === 0) {
const hotbar = getEmptyHotbar("default");
const di = getLegacyGlobalDiForExtensionApi();
const catalogCatalogEntity = di.inject(catalogCatalogEntityInjectable);
const { metadata: { uid, name, source }} = catalogCatalogEntity;
hotbar.items[0] = { entity: { uid, name, source }};
hotbars.push(hotbar);
}
try {
const workspaceStoreData: Pre500WorkspaceStoreModel = fse.readJsonSync(path.join(userDataPath, "lens-workspace-store.json"));
const { clusters = [] }: ClusterStoreModel = fse.readJSONSync(path.join(userDataPath, "lens-cluster-store.json"));
const workspaceHotbars = new Map<string, PartialHotbar>(); // mapping from WorkspaceId to HotBar
for (const { id, name } of workspaceStoreData.workspaces) {
migrationLog(`Creating new hotbar for ${name}`);
workspaceHotbars.set(id, {
id: uuid.v4(), // don't use the old IDs as they aren't necessarily UUIDs
items: [],
name: `Workspace: ${name}`,
});
}
{
// grab the default named hotbar or the first.
const defaultHotbarIndex = Math.max(0, hotbars.findIndex(hotbar => hotbar.name === "default"));
const [{ name, id, items }] = hotbars.splice(defaultHotbarIndex, 1);
workspaceHotbars.set("default", {
name,
id,
items: items.filter(isDefined),
});
}
for (const cluster of clusters) {
const uid = generateNewIdFor(cluster);
for (const workspaceId of cluster.workspaces ?? [cluster.workspace].filter(isDefined)) {
const workspaceHotbar = workspaceHotbars.get(workspaceId);
if (!workspaceHotbar) {
migrationLog(`Cluster ${uid} has unknown workspace ID, skipping`);
continue;
}
migrationLog(`Adding cluster ${uid} to ${workspaceHotbar.name}`);
if (workspaceHotbar?.items.length < defaultHotbarCells) {
workspaceHotbar.items.push({
entity: {
uid: generateNewIdFor(cluster),
name: cluster.preferences?.clusterName || cluster.contextName,
},
});
}
}
}
for (const hotbar of workspaceHotbars.values()) {
if (hotbar.items.length === 0) {
migrationLog(`Skipping ${hotbar.name} due to it being empty`);
continue;
}
while (hotbar.items.length < defaultHotbarCells) {
hotbar.items.push(null);
}
hotbars.push(hotbar as Hotbar);
}
/**
* Finally, make sure that the catalog entity hotbar item is in place.
* Just in case something else removed it.
*
* if every hotbar has elements that all not the `catalog-entity` item
*/
if (hotbars.every(hotbar => hotbar.items.every(item => item?.entity?.uid !== "catalog-entity"))) {
// note, we will add a new whole hotbar here called "default" if that was previously removed
const di = getLegacyGlobalDiForExtensionApi();
const catalogCatalogEntity = di.inject(catalogCatalogEntityInjectable);
const defaultHotbar = hotbars.find(hotbar => hotbar.name === "default");
const { metadata: { uid, name, source }} = catalogCatalogEntity;
if (defaultHotbar) {
const freeIndex = defaultHotbar.items.findIndex(isNull);
if (freeIndex === -1) {
// making a new hotbar is less destructive if the first hotbar
// called "default" is full than overriding a hotbar item
const hotbar = getEmptyHotbar("initial");
hotbar.items[0] = { entity: { uid, name, source }};
hotbars.unshift(hotbar);
} else {
defaultHotbar.items[freeIndex] = { entity: { uid, name, source }};
}
} else {
const hotbar = getEmptyHotbar("default");
hotbar.items[0] = { entity: { uid, name, source }};
hotbars.unshift(hotbar);
}
}
} catch (error) {
// ignore files being missing
if (isErrnoException(error) && error.code !== "ENOENT") {
throw error;
}
}
store.set("hotbars", hotbars);
},
} as MigrationDeclaration;

View File

@ -1,45 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Hotbar } from "../../common/hotbars/types";
import { getLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
import catalogEntityRegistryInjectable from "../../main/catalog/entity-registry.injectable";
import type { MigrationDeclaration } from "../helpers";
export default {
version: "5.0.0-beta.5",
run(store) {
const rawHotbars = store.get("hotbars");
const hotbars: Hotbar[] = Array.isArray(rawHotbars) ? rawHotbars : [];
const di = getLegacyGlobalDiForExtensionApi();
const catalogEntityRegistry = di.inject(catalogEntityRegistryInjectable);
for (const hotbar of hotbars) {
for (let i = 0; i < hotbar.items.length; i += 1) {
const item = hotbar.items[i];
if (!item) {
continue;
}
const entity = catalogEntityRegistry.findById(item.entity.uid);
if (!entity) {
// Clear disabled item
hotbar.items[i] = null;
} else {
// Save additional data
item.entity = {
...item.entity,
name: entity.metadata.name,
source: entity.metadata.source,
};
}
}
}
store.set("hotbars", hotbars);
},
} 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.
*/
// Hotbar store migrations
import { joinMigrations } from "../helpers";
import version500alpha0 from "./5.0.0-alpha.0";
import version500alpha2 from "./5.0.0-alpha.2";
import version500beta5 from "./5.0.0-beta.5";
import version500beta10 from "./5.0.0-beta.10";
export default joinMigrations(
version500alpha0,
version500alpha2,
version500beta5,
version500beta10,
);

View File

@ -1,14 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
// Add / reset "lastSeenAppVersion"
import type { MigrationDeclaration } from "../helpers";
export default {
version: "2.1.0-beta.4",
run(store) {
store.set("lastSeenAppVersion", "0.0.0");
},
} as MigrationDeclaration;

View File

@ -1,23 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
// Switch representation of hiddenTableColumns in store
import type { MigrationDeclaration } from "../helpers";
export default {
version: "5.0.0-alpha.3",
run(store) {
const preferences = store.get("preferences");
const oldHiddenTableColumns: Record<string, string[]> = preferences?.hiddenTableColumns;
if (!oldHiddenTableColumns) {
return;
}
preferences.hiddenTableColumns = Object.entries(oldHiddenTableColumns);
store.set("preferences", preferences);
},
} as MigrationDeclaration;

View File

@ -1,78 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { existsSync, readFileSync } from "fs";
import path from "path";
import os from "os";
import type { ClusterStoreModel } from "../../common/cluster-store/cluster-store";
import type { KubeconfigSyncEntry, UserPreferencesModel } from "../../common/user-store";
import type { MigrationDeclaration } from "../helpers";
import { migrationLog } from "../helpers";
import { isErrnoException, isLogicalChildPath } from "../../common/utils";
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";
export default {
version: "5.0.3-beta.1",
run(store) {
try {
const { syncKubeconfigEntries = [], ...preferences }: UserPreferencesModel = store.get("preferences") ?? {};
const di = getLegacyGlobalDiForExtensionApi();
const userDataPath = di.inject(directoryForUserDataInjectable);
const kubeConfigsPath = di.inject(directoryForKubeConfigsInjectable);
const { clusters = [] }: ClusterStoreModel = JSON.parse(readFileSync(path.resolve(userDataPath, "lens-cluster-store.json"), "utf-8")) ?? {};
const extensionDataDir = path.resolve(userDataPath, "extension_data");
const syncPaths = new Set(syncKubeconfigEntries.map(s => s.filePath));
syncPaths.add(path.join(os.homedir(), ".kube"));
for (const cluster of clusters) {
if (!cluster.kubeConfigPath) {
continue;
}
const dirOfKubeconfig = path.dirname(cluster.kubeConfigPath);
if (dirOfKubeconfig === kubeConfigsPath) {
migrationLog(`Skipping ${cluster.id} because kubeConfigPath is under the stored KubeConfig folder`);
continue;
}
if (syncPaths.has(cluster.kubeConfigPath) || syncPaths.has(dirOfKubeconfig)) {
migrationLog(`Skipping ${cluster.id} because kubeConfigPath is already being synced`);
continue;
}
if (isLogicalChildPath(extensionDataDir, cluster.kubeConfigPath)) {
migrationLog(`Skipping ${cluster.id} because kubeConfigPath is placed under an extension_data folder`);
continue;
}
if (!existsSync(cluster.kubeConfigPath)) {
migrationLog(`Skipping ${cluster.id} because kubeConfigPath no longer exists`);
continue;
}
migrationLog(`Adding ${cluster.kubeConfigPath} from ${cluster.id} to sync paths`);
syncPaths.add(cluster.kubeConfigPath);
}
const updatedSyncEntries: KubeconfigSyncEntry[] = [...syncPaths].map(filePath => ({ filePath }));
migrationLog("Final list of synced paths", updatedSyncEntries);
store.set("preferences", { ...preferences, syncKubeconfigEntries: updatedSyncEntries });
} catch (error) {
if (isErrnoException(error) && error.code !== "ENOENT") {
// ignore files being missing
throw error;
}
}
},
} as MigrationDeclaration;

View File

@ -1,18 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
// User store migrations
import { joinMigrations } from "../helpers";
import version210Beta4 from "./2.1.0-beta.4";
import version500Alpha3 from "./5.0.0-alpha.3";
import version503Beta1 from "./5.0.3-beta.1";
export default joinMigrations(
version210Beta4,
version500Alpha3,
version503Beta1,
);

View File

@ -1,34 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { docsUrl, slackUrl } from "../../common/vars";
import type { WeblinkData } from "../../common/weblink-store";
import type { MigrationDeclaration } from "../helpers";
export const lensWebsiteLinkName = "Lens Website";
export const lensDocumentationWeblinkName = "Lens Documentation";
export const lensSlackWeblinkName = "Lens Community Slack";
export const lensTwitterWeblinkName = "Lens on Twitter";
export const lensBlogWeblinkName = "Lens Official Blog";
export const kubernetesDocumentationWeblinkName = "Kubernetes Documentation";
export default {
version: "5.1.4",
run(store) {
const weblinksRaw: any = store.get("weblinks");
const weblinks = (Array.isArray(weblinksRaw) ? weblinksRaw : []) as WeblinkData[];
weblinks.push(
{ id: "https://k8slens.dev", name: lensWebsiteLinkName, url: "https://k8slens.dev" },
{ id: docsUrl, name: lensDocumentationWeblinkName, url: docsUrl },
{ id: slackUrl, name: lensSlackWeblinkName, url: slackUrl },
{ id: "https://twitter.com/k8slens", name: lensTwitterWeblinkName, url: "https://twitter.com/k8slens" },
{ id: "https://medium.com/k8slens", name: lensBlogWeblinkName, url: "https://medium.com/k8slens" },
{ id: "https://kubernetes.io/docs/home/", name: kubernetesDocumentationWeblinkName, url: "https://kubernetes.io/docs/home/" },
);
store.set("weblinks", weblinks);
},
} as MigrationDeclaration;

View File

@ -1,55 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { kubernetesDocumentationWeblinkId, lensBlogWeblinkId, lensDocumentationWeblinkId, lensSlackWeblinkId, lensTwitterWeblinkId, lensWebsiteWeblinkId } from "../../common/vars";
import type { WeblinkData } from "../../common/weblink-store";
import type { MigrationDeclaration } from "../helpers";
import { kubernetesDocumentationWeblinkName, lensBlogWeblinkName, lensDocumentationWeblinkName, lensSlackWeblinkName, lensTwitterWeblinkName, lensWebsiteLinkName } from "./5.1.4";
export default {
version: "5.4.5-beta.1 || >=5.5.0-alpha.0",
run(store) {
const weblinksRaw: any = store.get("weblinks");
const weblinks = (Array.isArray(weblinksRaw) ? weblinksRaw : []) as WeblinkData[];
const lensWebsiteLink = weblinks.find(weblink => weblink.name === lensWebsiteLinkName);
if (lensWebsiteLink) {
lensWebsiteLink.id = lensWebsiteWeblinkId;
}
const lensDocumentationWeblinkLink = weblinks.find(weblink => weblink.name === lensDocumentationWeblinkName);
if (lensDocumentationWeblinkLink) {
lensDocumentationWeblinkLink.id = lensDocumentationWeblinkId;
}
const lensSlackWeblinkLink = weblinks.find(weblink => weblink.name === lensSlackWeblinkName);
if (lensSlackWeblinkLink) {
lensSlackWeblinkLink.id = lensSlackWeblinkId;
}
const lensTwitterWeblinkLink = weblinks.find(weblink => weblink.name === lensTwitterWeblinkName);
if (lensTwitterWeblinkLink) {
lensTwitterWeblinkLink.id = lensTwitterWeblinkId;
}
const lensBlogWeblinkLink = weblinks.find(weblink => weblink.name === lensBlogWeblinkName);
if (lensBlogWeblinkLink) {
lensBlogWeblinkLink.id = lensBlogWeblinkId;
}
const kubernetesDocumentationWeblinkLink = weblinks.find(weblink => weblink.name === kubernetesDocumentationWeblinkName);
if (kubernetesDocumentationWeblinkLink) {
kubernetesDocumentationWeblinkLink.id = kubernetesDocumentationWeblinkId;
}
store.set("weblinks", weblinks);
},
} as MigrationDeclaration;

View File

@ -1,24 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getAppVersion } from "../../common/utils";
import { lensSlackWeblinkId, slackUrl } from "../../common/vars";
import type { WeblinkData } from "../../common/weblink-store";
import type { MigrationDeclaration } from "../helpers";
export default {
version: getAppVersion(), // Run always after upgrade
run(store) {
const weblinksRaw: any = store.get("weblinks");
const weblinks = (Array.isArray(weblinksRaw) ? weblinksRaw : []) as WeblinkData[];
const slackWeblink = weblinks.find(weblink => weblink.id === lensSlackWeblinkId);
if (slackWeblink) {
slackWeblink.url = slackUrl;
}
store.set("weblinks", weblinks);
},
} as MigrationDeclaration;

View File

@ -1,16 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { joinMigrations } from "../helpers";
import version514 from "./5.1.4";
import version545Beta1 from "./5.4.5-beta.1";
import currentVersion from "./currentVersion";
export default joinMigrations(
version514,
version545Beta1,
currentVersion,
);

View File

@ -19,7 +19,6 @@ import { DefaultProps } from "./mui-base-theme";
import configurePackages from "../common/configure-packages";
import * as initializers from "./initializers";
import logger from "../common/logger";
import { WeblinkStore } from "../common/weblink-store";
import { initializeSentryReporting } from "../common/sentry";
import { registerCustomThemes } from "./components/monaco-editor";
import { getDi } from "./getDi";
@ -46,6 +45,7 @@ import kubernetesClusterCategoryInjectable from "../common/catalog/categories/ku
import autoRegistrationInjectable from "../common/k8s-api/api-manager/auto-registration.injectable";
import assert from "assert";
import startFrameInjectable from "./start-frame/start-frame.injectable";
import weblinkStoreInjectable from "../common/weblinks/store.injectable";
configurePackages(); // global packages
registerCustomThemes(); // monaco editor themes
@ -141,7 +141,7 @@ export async function bootstrap(di: DiContainer) {
// TODO: Remove temporal dependencies
di.inject(themeStoreInjectable);
WeblinkStore.createInstance();
di.inject(weblinkStoreInjectable);
const extensionInstallationStateStore = di.inject(extensionInstallationStateStoreInjectable);

View File

@ -7,7 +7,7 @@ import React from "react";
import { observer } from "mobx-react";
import { Input } from "../input";
import { isUrl } from "../input/input_validators";
import { WeblinkStore } from "../../../common/weblink-store";
import { WeblinkStore } from "../../../common/weblinks/store";
import { computed, makeObservable, observable } from "mobx";
import { withInjectables } from "@ogre-tools/injectable-react";
import commandOverlayInjectable from "../command-palette/command-overlay.injectable";
@ -64,7 +64,7 @@ class NonInjectedWeblinkAddCommand extends React.Component<Dependencies> {
value={this.url}
onChange={(v) => this.onChangeUrl(v)}
onSubmit={(v) => this.onSubmitUrl(v)}
showValidationLine={true}
showValidationLine={true}
/>
{ this.nameHidden && (
<small className="hint">

View File

@ -101,7 +101,6 @@ export const getDiForUnitTesting = (opts: { doGeneralOverrides?: boolean } = {})
di.override(startTopbarStateSyncInjectable, () => ({
run: () => {},
}));
di.override(terminalSpawningPoolInjectable, () => document.createElement("div"));
di.override(hostedClusterIdInjectable, () => undefined);

View File

@ -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 { clusterStoreMigrationsInjectionToken } from "../../common/cluster-store/migrations";
const clusterStoreMigrationsInjectable = getInjectable({
id: "cluster-store-migrations",
instantiate: () => undefined,
injectionToken: clusterStoreMigrationsInjectionToken,
});
export default clusterStoreMigrationsInjectable;

View File

@ -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 { hotbarStoreMigrationsInjectionToken } from "../../common/hotbars/migrations";
const hotbarStoreMigrationsInjectable = getInjectable({
id: "hotbar-store-migrations",
instantiate: () => undefined,
injectionToken: hotbarStoreMigrationsInjectionToken,
});
export default hotbarStoreMigrationsInjectable;

View File

@ -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 { userStoreMigrationsInjectionToken } from "../../common/user-store/migrations";
const userStoreMigrationsInjectable = getInjectable({
id: "user-store-migrations",
instantiate: () => undefined,
injectionToken: userStoreMigrationsInjectionToken,
});
export default userStoreMigrationsInjectable;

View File

@ -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 { weblinksStoreMigrationsInjectionToken } from "../../common/weblinks/migrations";
const weblinksStoreMigrationsInjectable = getInjectable({
id: "weblinks-store-migrations",
instantiate: () => undefined,
injectionToken: weblinksStoreMigrationsInjectionToken,
});
export default weblinksStoreMigrationsInjectable;

View File

@ -8,7 +8,6 @@
import { comparer, reaction, toJS, when } from "mobx";
import type { StorageLayer } from "../storageHelper";
import { StorageHelper } from "../storageHelper";
import { isTestEnv } from "../../../common/vars";
import type { JsonObject, JsonValue } from "type-fest";
import type { Logger } from "../../../common/logger";
import type { GetAbsolutePath } from "../../../common/path/get-absolute-path.injectable";
@ -52,10 +51,7 @@ export const createStorage = ({
} catch {
// do nothing
} finally {
if (!isTestEnv) {
logger.info(`${logPrefix} loading finished for ${filePath}`);
}
logger.debug(`${logPrefix} loading finished for ${filePath}`);
storage.loaded = true;
}