From b03eb443c9cc406863cf222f8ef17334ada25eae Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Wed, 7 Jul 2021 02:20:17 -0400 Subject: [PATCH 1/6] Await kubeconfigPathP for fish shell args (#3286) Signed-off-by: Sebastian Malton --- src/main/shell-session/local-shell-session.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/shell-session/local-shell-session.ts b/src/main/shell-session/local-shell-session.ts index 384e9317a8..35a1369339 100644 --- a/src/main/shell-session/local-shell-session.ts +++ b/src/main/shell-session/local-shell-session.ts @@ -51,7 +51,7 @@ export class LocalShellSession extends ShellSession { case "bash": return ["--init-file", path.join(await this.kubectlBinDirP, ".bash_set_path")]; case "fish": - return ["--login", "--init-command", `export PATH="${helmpath}:${kubectlPathDir}:$PATH"; export KUBECONFIG="${this.kubeconfigPathP}"`]; + return ["--login", "--init-command", `export PATH="${helmpath}:${kubectlPathDir}:$PATH"; export KUBECONFIG="${await this.kubeconfigPathP}"`]; case "zsh": return ["--login"]; default: From a301283adc862155791409a58c1a4cb86d3c61e9 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Wed, 7 Jul 2021 02:30:07 -0400 Subject: [PATCH 2/6] Fix unhandled exception in visibility handler (#3291) - If an KubernetesCluster entity is being deleted then the catalogEntityRegistry.getById() will return undefined. This leads to an unhandled exception in the handler because we tried to read apiVersion. This commit changes it so that we get the apiVersion and kind from KubernetesCluster class itself Signed-off-by: Sebastian Malton --- src/common/catalog-entities/kubernetes-cluster.ts | 7 +++++-- src/main/catalog/catalog-entity-registry.ts | 6 +++++- src/main/initializers/ipc.ts | 6 +++--- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/common/catalog-entities/kubernetes-cluster.ts b/src/common/catalog-entities/kubernetes-cluster.ts index 3b79c3ab56..db948df25a 100644 --- a/src/common/catalog-entities/kubernetes-cluster.ts +++ b/src/common/catalog-entities/kubernetes-cluster.ts @@ -62,8 +62,11 @@ export interface KubernetesClusterStatus extends CatalogEntityStatus { } export class KubernetesCluster extends CatalogEntity { - public readonly apiVersion = "entity.k8slens.dev/v1alpha1"; - public readonly kind = "KubernetesCluster"; + public static readonly apiVersion = "entity.k8slens.dev/v1alpha1"; + public static readonly kind = "KubernetesCluster"; + + public readonly apiVersion = KubernetesCluster.apiVersion; + public readonly kind = KubernetesCluster.kind; async connect(): Promise { if (app) { diff --git a/src/main/catalog/catalog-entity-registry.ts b/src/main/catalog/catalog-entity-registry.ts index 4cecf4f49b..f2188132eb 100644 --- a/src/main/catalog/catalog-entity-registry.ts +++ b/src/main/catalog/catalog-entity-registry.ts @@ -20,7 +20,7 @@ */ import { action, computed, IComputedValue, IObservableArray, makeObservable, observable } from "mobx"; -import { CatalogCategoryRegistry, catalogCategoryRegistry, CatalogEntity } from "../../common/catalog"; +import { CatalogCategoryRegistry, catalogCategoryRegistry, CatalogEntity, CatalogEntityKindData } from "../../common/catalog"; import { iter } from "../../common/utils"; export class CatalogEntityRegistry { @@ -62,6 +62,10 @@ export class CatalogEntityRegistry { return items as T[]; } + + getItemsByEntityClass({ apiVersion, kind }: CatalogEntityKindData): T[] { + return this.getItemsForApiKind(apiVersion, kind); + } } export const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry); diff --git a/src/main/initializers/ipc.ts b/src/main/initializers/ipc.ts index 95c4571a41..3614c602d7 100644 --- a/src/main/initializers/ipc.ts +++ b/src/main/initializers/ipc.ts @@ -20,7 +20,7 @@ */ import type { IpcMainInvokeEvent } from "electron"; -import type { KubernetesCluster } from "../../common/catalog-entities"; +import { KubernetesCluster } from "../../common/catalog-entities"; import { clusterFrameMap } from "../../common/cluster-frames"; import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterDeleteHandler } from "../../common/cluster-ipc"; import { ClusterId, ClusterStore } from "../../common/cluster-store"; @@ -50,9 +50,9 @@ export function initIpcMainHandlers() { }); ipcMainHandle(clusterVisibilityHandler, (event: IpcMainInvokeEvent, clusterId: ClusterId, visible: boolean) => { - const entity = catalogEntityRegistry.getById(clusterId); + const entity = catalogEntityRegistry.getById(clusterId); - for (const kubeEntity of catalogEntityRegistry.getItemsForApiKind(entity.apiVersion, entity.kind)) { + for (const kubeEntity of catalogEntityRegistry.getItemsByEntityClass(KubernetesCluster)) { kubeEntity.status.active = false; } From f7ad554108cbce5c82bb01dc03251d730700b21e Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Wed, 7 Jul 2021 09:35:40 +0300 Subject: [PATCH 3/6] Fixing initial scroll-to-bottom in pod logs (#3281) * Fixing scroll to bottom in pod logs Signed-off-by: Alex Andreev * Fixing invalidDate error if no timestamp provided Signed-off-by: Alex Andreev --- .../dock/__test__/to-bottom.test.tsx | 53 ++++++++++++ src/renderer/components/dock/log-list.scss | 13 --- src/renderer/components/dock/log-list.tsx | 80 +++++++------------ src/renderer/components/dock/log.store.ts | 2 +- src/renderer/components/dock/to-bottom.tsx | 38 +++++++++ 5 files changed, 122 insertions(+), 64 deletions(-) create mode 100644 src/renderer/components/dock/__test__/to-bottom.test.tsx create mode 100644 src/renderer/components/dock/to-bottom.tsx diff --git a/src/renderer/components/dock/__test__/to-bottom.test.tsx b/src/renderer/components/dock/__test__/to-bottom.test.tsx new file mode 100644 index 0000000000..c19a7b19e3 --- /dev/null +++ b/src/renderer/components/dock/__test__/to-bottom.test.tsx @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import React from "react"; +import "@testing-library/jest-dom/extend-expect"; +import { fireEvent, render } from "@testing-library/react"; +import { ToBottom } from "../to-bottom"; +import { noop } from "../../../utils"; + +describe("", () => { + it("renders w/o errors", () => { + const { container } = render(); + + expect(container).toBeInstanceOf(HTMLElement); + }); + + it("has 'To bottom' label", () => { + const { getByText } = render(); + + expect(getByText("To bottom")).toBeInTheDocument(); + }); + + it("has a arrow down icon", () => { + const { getByText } = render(); + + expect(getByText("expand_more")).toBeInTheDocument(); + }); + + it("fires an onclick event", () => { + const callback = jest.fn(); + const { getByText } = render(); + + fireEvent.click(getByText("To bottom")); + expect(callback).toBeCalled(); + }); +}); diff --git a/src/renderer/components/dock/log-list.scss b/src/renderer/components/dock/log-list.scss index edae55ad61..3bf5bfd833 100644 --- a/src/renderer/components/dock/log-list.scss +++ b/src/renderer/components/dock/log-list.scss @@ -84,17 +84,4 @@ overflow-x: hidden!important; // fixing scroll to bottom issues in PodLogs } } - - .JumpToBottom { - position: absolute; - right: 30px; - padding: 4px 9px; - border-radius: 20px; - z-index: 2; - top: 20px; - - .Icon { - --size: calc(var(--unit) * 2); - } - } } diff --git a/src/renderer/components/dock/log-list.tsx b/src/renderer/components/dock/log-list.tsx index 9437a854e2..211e66f936 100644 --- a/src/renderer/components/dock/log-list.tsx +++ b/src/renderer/components/dock/log-list.tsx @@ -25,20 +25,19 @@ import React from "react"; import AnsiUp from "ansi_up"; import DOMPurify from "dompurify"; import debounce from "lodash/debounce"; -import { action, computed, observable, makeObservable } from "mobx"; -import { observer } from "mobx-react"; +import { action, computed, observable, makeObservable, reaction } from "mobx"; +import { disposeOnUnmount, observer } from "mobx-react"; import moment from "moment-timezone"; import type { Align, ListOnScrollProps } from "react-window"; import { SearchStore, searchStore } from "../../../common/search-store"; import { UserStore } from "../../../common/user-store"; -import { cssNames } from "../../utils"; -import { Button } from "../button"; -import { Icon } from "../icon"; +import { boundMethod, cssNames } from "../../utils"; import { Spinner } from "../spinner"; import { VirtualList } from "../virtual-list"; import { logStore } from "./log.store"; import { logTabStore } from "./log-tab.store"; +import { ToBottom } from "./to-bottom"; interface Props { logs: string[] @@ -64,41 +63,44 @@ export class LogList extends React.Component { } componentDidMount() { - this.scrollToBottom(); + disposeOnUnmount(this, [ + reaction(() => this.props.logs, this.onLogsInitialLoad), + reaction(() => this.props.logs, this.onLogsUpdate), + reaction(() => this.props.logs, this.onUserScrolledUp) + ]); } - componentDidUpdate(prevProps: Props) { - const { logs, id } = this.props; - - if (id != prevProps.id) { + @boundMethod + onLogsInitialLoad(logs: string[], prevLogs: string[]) { + if (!prevLogs.length && logs.length) { this.isLastLineVisible = true; - - return; } + } - if (logs == prevProps.logs || !this.virtualListDiv.current) return; + @boundMethod + onLogsUpdate() { + if (this.isLastLineVisible) { + setTimeout(() => { + this.scrollToBottom(); + }, 500); // Giving some time to VirtualList to prepare its outerRef (this.virtualListDiv) element + } + } - const newLogsLoaded = prevProps.logs.length < logs.length; + @boundMethod + onUserScrolledUp(logs: string[], prevLogs: string[]) { + if (!this.virtualListDiv.current) return; + + const newLogsAdded = prevLogs.length < logs.length; const scrolledToBeginning = this.virtualListDiv.current.scrollTop === 0; - if (this.isLastLineVisible || prevProps.logs.length == 0) { - this.scrollToBottom(); // Scroll down to keep user watching/reading experience - - return; - } - - if (scrolledToBeginning && newLogsLoaded) { - const firstLineContents = prevProps.logs[0]; + if (newLogsAdded && scrolledToBeginning) { + const firstLineContents = prevLogs[0]; const lineToScroll = this.props.logs.findIndex((value) => value == firstLineContents); if (lineToScroll !== -1) { this.scrollToItem(lineToScroll, "start"); } } - - if (!logs.length) { - this.isLastLineVisible = false; - } } /** @@ -114,7 +116,7 @@ export class LogList extends React.Component { return this.props.logs .map(log => logStore.splitOutTimestamp(log)) - .map(([logTimestamp, log]) => (`${moment.tz(logTimestamp, UserStore.getInstance().localeTimezone).format()}${log}`)); + .map(([logTimestamp, log]) => (`${logTimestamp && moment.tz(logTimestamp, UserStore.getInstance().localeTimezone).format()}${log}`)); } /** @@ -158,7 +160,6 @@ export class LogList extends React.Component { } }; - @action scrollToBottom = () => { if (!this.virtualListDiv.current) return; this.virtualListDiv.current.scrollTop = this.virtualListDiv.current.scrollHeight; @@ -169,7 +170,6 @@ export class LogList extends React.Component { }; onScroll = (props: ListOnScrollProps) => { - if (!this.virtualListDiv.current) return; this.isLastLineVisible = false; this.onScrollDebounced(props); }; @@ -264,29 +264,9 @@ export class LogList extends React.Component { className="box grow" /> {this.isJumpButtonVisible && ( - + )} ); } } - -interface JumpToBottomProps { - onClick: () => void -} - -const JumpToBottom = ({ onClick }: JumpToBottomProps) => { - return ( - - ); -}; diff --git a/src/renderer/components/dock/log.store.ts b/src/renderer/components/dock/log.store.ts index 91682e8c0c..710ed3a690 100644 --- a/src/renderer/components/dock/log.store.ts +++ b/src/renderer/components/dock/log.store.ts @@ -183,7 +183,7 @@ export class LogStore { const extraction = /^(\d+\S+)(.*)/m.exec(logs); if (!extraction || extraction.length < 3) { - return ["", ""]; + return ["", logs]; } return [extraction[1], extraction[2]]; diff --git a/src/renderer/components/dock/to-bottom.tsx b/src/renderer/components/dock/to-bottom.tsx new file mode 100644 index 0000000000..f95ff6bebc --- /dev/null +++ b/src/renderer/components/dock/to-bottom.tsx @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import React from "react"; +import { Icon } from "../icon"; + +export function ToBottom({ onClick }: { onClick: () => void }) { + return ( + + ); +} From 17a80446474ae57a292afba822e0c14ebfaeb6ba Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Wed, 7 Jul 2021 09:42:16 -0400 Subject: [PATCH 4/6] Add logging on proxy error (#3303) Signed-off-by: Sebastian Malton --- src/main/proxy/lens-proxy.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/proxy/lens-proxy.ts b/src/main/proxy/lens-proxy.ts index 00ef5fbb48..66a00284aa 100644 --- a/src/main/proxy/lens-proxy.ts +++ b/src/main/proxy/lens-proxy.ts @@ -177,6 +177,8 @@ export class LensProxy extends Singleton { return; } + logger.error(`[LENS-PROXY]: http proxy errored for cluster: ${error}`, { url: req.url }); + if (target) { logger.debug(`Failed proxy to target: ${JSON.stringify(target, null, 2)}`); @@ -196,7 +198,7 @@ export class LensProxy extends Singleton { } try { - res.writeHead(500).end("Oops, something went wrong."); + res.writeHead(500).end(`Oops, something went wrong.\n${error}`); } catch (e) { logger.error(`[LENS-PROXY]: Failed to write headers: `, e); } From fe443f462b096b3d682aa044e957062ca3ec01d6 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Wed, 7 Jul 2021 15:48:00 -0400 Subject: [PATCH 5/6] Fix table columns being hidden after changing any user preference (#3302) Signed-off-by: Sebastian Malton --- src/common/user-store/user-store.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/user-store/user-store.ts b/src/common/user-store/user-store.ts index e4f9a8a007..9801405b27 100644 --- a/src/common/user-store/user-store.ts +++ b/src/common/user-store/user-store.ts @@ -118,13 +118,13 @@ export class UserStore extends BaseStore /* implements UserStore */ isTableColumnHidden(tableId: string, ...columnIds: string[]): boolean { if (columnIds.length === 0) { - return true; + return false; } const config = this.hiddenTableColumns.get(tableId); if (!config) { - return true; + return false; } return columnIds.some(columnId => config.has(columnId)); From 497e47b277b374e1e8c18131a4d6aea29a530ba5 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Wed, 7 Jul 2021 16:29:58 -0400 Subject: [PATCH 6/6] Automatically sync all kubeConfigPaths (#3227) --- .../package/package.json | 25 ------- package.json | 2 +- src/common/user-store/index.ts | 2 +- src/common/user-store/user-store.ts | 5 +- src/main/index.ts | 2 +- src/migrations/hotbar-store/5.0.0-beta.10.ts | 4 +- src/migrations/user-store/5.0.3-beta.1.ts | 75 +++++++++++++++++++ src/migrations/user-store/index.ts | 2 + .../+extensions/__tests__/extensions.test.tsx | 12 ++- 9 files changed, 90 insertions(+), 39 deletions(-) delete mode 100644 extensions/kube-object-event-status/package/package.json create mode 100644 src/migrations/user-store/5.0.3-beta.1.ts diff --git a/extensions/kube-object-event-status/package/package.json b/extensions/kube-object-event-status/package/package.json deleted file mode 100644 index e6ab1bb688..0000000000 --- a/extensions/kube-object-event-status/package/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "kube-object-event-status", - "version": "0.1.0", - "description": "Adds kube object status from events", - "renderer": "dist/renderer.js", - "lens": { - "metadata": {}, - "styles": [] - }, - "scripts": { - "build": "webpack && npm pack", - "dev": "webpack --watch", - "test": "echo NO TESTS" - }, - "files": [ - "dist/**/*" - ], - "dependencies": {}, - "devDependencies": { - "@k8slens/extensions": "file:../../src/extensions/npm/extensions", - "ts-loader": "^8.0.4", - "typescript": "^4.0.3", - "webpack": "^4.44.2" - } -} diff --git a/package.json b/package.json index 4844f6f10a..8f7af30980 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "productName": "OpenLens", "description": "OpenLens - Open Source IDE for Kubernetes", "homepage": "https://github.com/lensapp/lens", - "version": "5.0.2", + "version": "5.0.3-beta.1", "main": "static/build/main.js", "copyright": "© 2021 OpenLens Authors", "license": "MIT", diff --git a/src/common/user-store/index.ts b/src/common/user-store/index.ts index dc00ab6737..a8dc6eb028 100644 --- a/src/common/user-store/index.ts +++ b/src/common/user-store/index.ts @@ -20,4 +20,4 @@ */ export * from "./user-store"; -export type { KubeconfigSyncEntry, KubeconfigSyncValue } from "./preferences-helpers"; +export type { KubeconfigSyncEntry, KubeconfigSyncValue, UserPreferencesModel } from "./preferences-helpers"; diff --git a/src/common/user-store/user-store.ts b/src/common/user-store/user-store.ts index 9801405b27..9aa814890f 100644 --- a/src/common/user-store/user-store.ts +++ b/src/common/user-store/user-store.ts @@ -31,6 +31,7 @@ import path from "path"; import { fileNameMigration } from "../../migrations/user-store"; import { ObservableToggleSet, toJS } from "../../renderer/utils"; import { DESCRIPTORS, KubeconfigSyncValue, UserPreferencesModel } from "./preferences-helpers"; +import logger from "../../main/logger"; export interface UserStoreModel { lastSeenAppVersion: string; @@ -155,8 +156,8 @@ export class UserStore extends BaseStore /* implements UserStore } @action - protected fromStore(data: Partial = {}) { - const { lastSeenAppVersion, preferences } = data; + protected fromStore({ lastSeenAppVersion, preferences }: Partial = {}) { + logger.debug("UserStore.fromStore()", { lastSeenAppVersion, preferences }); if (lastSeenAppVersion) { this.lastSeenAppVersion = lastSeenAppVersion; diff --git a/src/main/index.ts b/src/main/index.ts index 0599f820cb..c94c561daa 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -138,8 +138,8 @@ app.on("ready", async () => { logger.info("💾 Loading stores"); - UserStore.createInstance().startMainReactions(); ClusterStore.createInstance().provideInitialFromMain(); + UserStore.createInstance().startMainReactions(); HotbarStore.createInstance(); ExtensionsStore.createInstance(); FilesystemProvisionerStore.createInstance(); diff --git a/src/migrations/hotbar-store/5.0.0-beta.10.ts b/src/migrations/hotbar-store/5.0.0-beta.10.ts index af6af5370c..f0a8fb4872 100644 --- a/src/migrations/hotbar-store/5.0.0-beta.10.ts +++ b/src/migrations/hotbar-store/5.0.0-beta.10.ts @@ -148,8 +148,8 @@ export default { store.set("hotbars", hotbars); } catch (error) { - if (!(error.code === "ENOENT" && error.path.endsWith("lens-workspace-store.json"))) { - // ignore lens-workspace-store.json being missing + if (error.code !== "ENOENT") { + // ignore files being missing throw error; } } diff --git a/src/migrations/user-store/5.0.3-beta.1.ts b/src/migrations/user-store/5.0.3-beta.1.ts new file mode 100644 index 0000000000..57271e30a4 --- /dev/null +++ b/src/migrations/user-store/5.0.3-beta.1.ts @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import { app } from "electron"; +import { existsSync, readJsonSync } from "fs-extra"; +import path from "path"; +import os from "os"; +import { ClusterStore, ClusterStoreModel } from "../../common/cluster-store"; +import type { KubeconfigSyncEntry, UserPreferencesModel } from "../../common/user-store"; +import { MigrationDeclaration, migrationLog } from "../helpers"; + +export default { + version: "5.0.3-beta.1", + run(store) { + try { + const { syncKubeconfigEntries = [], ...preferences }: UserPreferencesModel = store.get("preferences") ?? {}; + const { clusters = [] }: ClusterStoreModel = readJsonSync(path.resolve(app.getPath("userData"), "lens-cluster-store.json")) ?? {}; + const syncPaths = new Set(syncKubeconfigEntries.map(s => s.filePath)); + + syncPaths.add(path.join(os.homedir(), ".kube")); + + for (const cluster of clusters) { + const dirOfKubeconfig = path.dirname(cluster.kubeConfigPath); + + if (dirOfKubeconfig === ClusterStore.storedKubeConfigFolder) { + migrationLog(`Skipping ${cluster.id} because kubeConfigPath is under ClusterStore.storedKubeConfigFolder`); + continue; + } + + if (syncPaths.has(cluster.kubeConfigPath) || syncPaths.has(dirOfKubeconfig)) { + migrationLog(`Skipping ${cluster.id} because kubeConfigPath is already being synced`); + 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) { + console.log(error); + + if (error.code !== "ENOENT") { + // ignore files being missing + throw error; + } + } + }, +} as MigrationDeclaration; diff --git a/src/migrations/user-store/index.ts b/src/migrations/user-store/index.ts index c08ef2c5c5..87628daeef 100644 --- a/src/migrations/user-store/index.ts +++ b/src/migrations/user-store/index.ts @@ -25,6 +25,7 @@ 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"; import { fileNameMigration } from "./file-name-migration"; export { @@ -34,4 +35,5 @@ export { export default joinMigrations( version210Beta4, version500Alpha3, + version503Beta1, ); diff --git a/src/renderer/components/+extensions/__tests__/extensions.test.tsx b/src/renderer/components/+extensions/__tests__/extensions.test.tsx index 3c2484b92e..f51ba24575 100644 --- a/src/renderer/components/+extensions/__tests__/extensions.test.tsx +++ b/src/renderer/components/+extensions/__tests__/extensions.test.tsx @@ -59,14 +59,7 @@ describe("Extensions", () => { }); ExtensionInstallationStateStore.reset(); - UserStore.resetInstance(); - UserStore.createInstance(); - - ExtensionDiscovery.resetInstance(); - ExtensionDiscovery.createInstance().uninstallExtension = jest.fn(() => Promise.resolve()); - - ExtensionLoader.resetInstance(); ExtensionLoader.createInstance().addExtension({ id: "extensionId", manifest: { @@ -79,10 +72,15 @@ describe("Extensions", () => { isEnabled: true, isCompatible: true }); + ExtensionDiscovery.createInstance().uninstallExtension = jest.fn(() => Promise.resolve()); + UserStore.createInstance(); }); afterEach(() => { mockFs.restore(); + UserStore.resetInstance(); + ExtensionDiscovery.resetInstance(); + ExtensionLoader.resetInstance(); }); it("disables uninstall and disable buttons while uninstalling", async () => {