diff --git a/src/common/__tests__/cluster-store.test.ts b/src/common/__tests__/cluster-store.test.ts index 28357ae5ff..9dd1a29a5b 100644 --- a/src/common/__tests__/cluster-store.test.ts +++ b/src/common/__tests__/cluster-store.test.ts @@ -3,34 +3,33 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import fs from "fs"; -import mockFs from "mock-fs"; -import path from "path"; -import fse from "fs-extra"; import type { ClusterStore } from "../cluster-store/cluster-store"; -import { Console } from "console"; -import { stdout, stderr } from "process"; -import getCustomKubeConfigDirectoryInjectable from "../app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable"; +import type { GetCustomKubeConfigFilePath } from "../app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable"; +import getCustomKubeConfigFilePathInjectable from "../app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable"; import clusterStoreInjectable from "../cluster-store/cluster-store.injectable"; import type { DiContainer } from "@ogre-tools/injectable"; import type { CreateCluster } from "../cluster/create-cluster-injection-token"; import { createClusterInjectionToken } from "../cluster/create-cluster-injection-token"; import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable"; import { getDiForUnitTesting } from "../../main/getDiForUnitTesting"; -import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable"; import assert from "assert"; import directoryForTempInjectable from "../app-paths/directory-for-temp/directory-for-temp.injectable"; import kubectlBinaryNameInjectable from "../../main/kubectl/binary-name.injectable"; import kubectlDownloadingNormalizedArchInjectable from "../../main/kubectl/normalized-arch.injectable"; import normalizedPlatformInjectable from "../vars/normalized-platform.injectable"; -import fsInjectable from "../fs/fs.injectable"; import storeMigrationVersionInjectable from "../vars/store-migration-version.injectable"; +import type { WriteJsonSync } from "../fs/write-json-sync.injectable"; +import writeJsonSyncInjectable from "../fs/write-json-sync.injectable"; +import type { ReadFileSync } from "../fs/read-file-sync.injectable"; +import readFileSyncInjectable from "../fs/read-file-sync.injectable"; +import { readFileSync } from "fs"; +import type { WriteFileSync } from "../fs/write-file-sync.injectable"; +import writeFileSyncInjectable from "../fs/write-file-sync.injectable"; +import type { WriteBufferSync } from "../fs/write-buffer-sync.injectable"; +import writeBufferSyncInjectable from "../fs/write-buffer-sync.injectable"; -console = new Console(stdout, stderr); - -const testDataIcon = fs.readFileSync( - "test-data/cluster-store-migration-icon.png", -); +// NOTE: this is intended to read the actual file system +const testDataIcon = readFileSync("test-data/cluster-store-migration-icon.png"); const clusterServerUrl = "https://localhost"; const kubeconfig = ` apiVersion: v1 @@ -56,75 +55,41 @@ users: token: kubeconfig-user-q4lm4:xxxyyyy `; -const embed = (directoryName: string, contents: any): string => { - fse.ensureDirSync(path.dirname(directoryName)); - fse.writeFileSync(directoryName, contents, { - encoding: "utf-8", - mode: 0o600, - }); - - return directoryName; -}; - -jest.mock("electron", () => ({ - ipcMain: { - handle: jest.fn(), - on: jest.fn(), - removeAllListeners: jest.fn(), - off: jest.fn(), - send: jest.fn(), - }, -})); - describe("cluster-store", () => { - let mainDi: DiContainer; + let di: DiContainer; let clusterStore: ClusterStore; let createCluster: CreateCluster; + let writeJsonSync: WriteJsonSync; + let writeFileSync: WriteFileSync; + let writeBufferSync: WriteBufferSync; + let readFileSync: ReadFileSync; + let getCustomKubeConfigFilePath: GetCustomKubeConfigFilePath; + let writeFileSyncAndReturnPath: (filePath: string, contents: string) => string; beforeEach(async () => { - mainDi = getDiForUnitTesting({ doGeneralOverrides: true }); + di = getDiForUnitTesting({ doGeneralOverrides: true }); - mockFs(); - - mainDi.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); - mainDi.override(directoryForTempInjectable, () => "some-temp-directory"); - mainDi.override(kubectlBinaryNameInjectable, () => "kubectl"); - mainDi.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64"); - mainDi.override(normalizedPlatformInjectable, () => "darwin"); - - mainDi.permitSideEffects(getConfigurationFileModelInjectable); - mainDi.unoverride(getConfigurationFileModelInjectable); - - mainDi.permitSideEffects(fsInjectable); - }); - - afterEach(() => { - mockFs.restore(); + di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data"); + di.override(directoryForTempInjectable, () => "/some-temp-directory"); + di.override(kubectlBinaryNameInjectable, () => "kubectl"); + di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64"); + di.override(normalizedPlatformInjectable, () => "darwin"); + createCluster = di.inject(createClusterInjectionToken); + getCustomKubeConfigFilePath = di.inject(getCustomKubeConfigFilePathInjectable); + writeJsonSync = di.inject(writeJsonSyncInjectable); + writeFileSync = di.inject(writeFileSyncInjectable); + writeBufferSync = di.inject(writeBufferSyncInjectable); + readFileSync = di.inject(readFileSyncInjectable); + writeFileSyncAndReturnPath = (filePath, contents) => (writeFileSync(filePath, contents), filePath); }); describe("empty config", () => { - let getCustomKubeConfigDirectory: (directoryName: string) => string; - beforeEach(async () => { - getCustomKubeConfigDirectory = mainDi.inject(getCustomKubeConfigDirectoryInjectable); - - mockFs({ - "some-directory-for-user-data": { - "lens-cluster-store.json": JSON.stringify({}), - }, - }); - - createCluster = mainDi.inject(createClusterInjectionToken); - - clusterStore = mainDi.inject(clusterStoreInjectable); - + writeJsonSync("/some-directory-for-user-data/lens-cluster-store.json", {}); + clusterStore = di.inject(clusterStoreInjectable); clusterStore.load(); }); - afterEach(() => { - mockFs.restore(); - }); - describe("with foo cluster added", () => { beforeEach(() => { const cluster = createCluster({ @@ -135,8 +100,8 @@ describe("cluster-store", () => { icon: "data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5", clusterName: "minikube", }, - kubeConfigPath: embed( - getCustomKubeConfigDirectory("foo"), + kubeConfigPath: writeFileSyncAndReturnPath( + getCustomKubeConfigFilePath("foo"), kubeconfig, ), }, { @@ -169,8 +134,8 @@ describe("cluster-store", () => { preferences: { clusterName: "prod", }, - kubeConfigPath: embed( - getCustomKubeConfigDirectory("prod"), + kubeConfigPath: writeFileSyncAndReturnPath( + getCustomKubeConfigFilePath("prod"), kubeconfig, ), }); @@ -180,8 +145,8 @@ describe("cluster-store", () => { preferences: { clusterName: "dev", }, - kubeConfigPath: embed( - getCustomKubeConfigDirectory("dev"), + kubeConfigPath: writeFileSyncAndReturnPath( + getCustomKubeConfigFilePath("dev"), kubeconfig, ), }); @@ -193,61 +158,49 @@ describe("cluster-store", () => { }); it("check if cluster's kubeconfig file saved", () => { - const file = embed(getCustomKubeConfigDirectory("boo"), "kubeconfig"); + const file = writeFileSyncAndReturnPath(getCustomKubeConfigFilePath("boo"), "kubeconfig"); - expect(fs.readFileSync(file, "utf8")).toBe("kubeconfig"); + expect(readFileSync(file)).toBe("kubeconfig"); }); }); }); describe("config with existing clusters", () => { beforeEach(() => { - mockFs({ - "temp-kube-config": kubeconfig, - "some-directory-for-user-data": { - "lens-cluster-store.json": JSON.stringify({ - __internal__: { - migrations: { - version: "99.99.99", - }, - }, - clusters: [ - { - id: "cluster1", - kubeConfigPath: "./temp-kube-config", - contextName: "foo", - preferences: { terminalCWD: "/foo" }, - workspace: "default", - }, - { - id: "cluster2", - kubeConfigPath: "./temp-kube-config", - contextName: "foo2", - preferences: { terminalCWD: "/foo2" }, - }, - { - id: "cluster3", - kubeConfigPath: "./temp-kube-config", - contextName: "foo", - preferences: { terminalCWD: "/foo" }, - workspace: "foo", - ownerRef: "foo", - }, - ], - }), + writeFileSync("/temp-kube-config", kubeconfig); + writeJsonSync("/some-directory-for-user-data/lens-cluster-store.json", { + __internal__: { + migrations: { + version: "99.99.99", + }, }, + clusters: [ + { + id: "cluster1", + kubeConfigPath: "/temp-kube-config", + contextName: "foo", + preferences: { terminalCWD: "/foo" }, + workspace: "default", + }, + { + id: "cluster2", + kubeConfigPath: "/temp-kube-config", + contextName: "foo2", + preferences: { terminalCWD: "/foo2" }, + }, + { + id: "cluster3", + kubeConfigPath: "/temp-kube-config", + contextName: "foo", + preferences: { terminalCWD: "/foo" }, + workspace: "foo", + ownerRef: "foo", + }, + ], }); - - createCluster = mainDi.inject(createClusterInjectionToken); - - clusterStore = mainDi.inject(clusterStoreInjectable); + clusterStore = di.inject(clusterStoreInjectable); clusterStore.load(); }); - - afterEach(() => { - mockFs.restore(); - }); - it("allows to retrieve a cluster", () => { const storedCluster = clusterStore.getById("cluster1"); @@ -271,66 +224,35 @@ describe("cluster-store", () => { describe("config with invalid cluster kubeconfig", () => { beforeEach(() => { - const invalidKubeconfig = ` -apiVersion: v1 -clusters: -- cluster: - server: https://localhost - name: test2 -contexts: -- context: - cluster: test - user: test - name: test -current-context: test -kind: Config -preferences: {} -users: -- name: test - user: - token: kubeconfig-user-q4lm4:xxxyyyy -`; - - mockFs({ - "invalid-kube-config": invalidKubeconfig, - "valid-kube-config": kubeconfig, - "some-directory-for-user-data": { - "lens-cluster-store.json": JSON.stringify({ - __internal__: { - migrations: { - version: "99.99.99", - }, - }, - clusters: [ - { - id: "cluster1", - kubeConfigPath: "./invalid-kube-config", - contextName: "test", - preferences: { terminalCWD: "/foo" }, - workspace: "foo", - }, - { - id: "cluster2", - kubeConfigPath: "./valid-kube-config", - contextName: "foo", - preferences: { terminalCWD: "/foo" }, - workspace: "default", - }, - ], - }), + writeFileSync("/invalid-kube-config", invalidKubeconfig); + writeFileSync("/valid-kube-config", kubeconfig); + writeJsonSync("/some-directory-for-user-data/lens-cluster-store.json", { + __internal__: { + migrations: { + version: "99.99.99", + }, }, + clusters: [ + { + id: "cluster1", + kubeConfigPath: "/invalid-kube-config", + contextName: "test", + preferences: { terminalCWD: "/foo" }, + workspace: "foo", + }, + { + id: "cluster2", + kubeConfigPath: "/valid-kube-config", + contextName: "foo", + preferences: { terminalCWD: "/foo" }, + workspace: "default", + }, + ], }); - - createCluster = mainDi.inject(createClusterInjectionToken); - - clusterStore = mainDi.inject(clusterStoreInjectable); + clusterStore = di.inject(clusterStoreInjectable); clusterStore.load(); }); - afterEach(() => { - mockFs.restore(); - }); - it("does not enable clusters with invalid kubeconfig", () => { const storedClusters = clusterStore.clustersList; @@ -340,56 +262,69 @@ users: describe("pre 3.6.0-beta.1 config with an existing cluster", () => { beforeEach(() => { - mockFs({ - "some-directory-for-user-data": { - "lens-cluster-store.json": JSON.stringify({ - __internal__: { - migrations: { - version: "3.5.0", - }, - }, - clusters: [ - { - id: "cluster1", - kubeConfig: minimalValidKubeConfig, - contextName: "cluster", - preferences: { - icon: "store://icon_path", - }, - }, - ], - }), - icon_path: testDataIcon, + writeJsonSync("/some-directory-for-user-data/lens-cluster-store.json", { + __internal__: { + migrations: { + version: "3.5.0", + }, }, + clusters: [ + { + id: "cluster1", + kubeConfig: minimalValidKubeConfig, + contextName: "cluster", + preferences: { + icon: "store://icon_path", + }, + }, + ], }); + writeBufferSync("/some-directory-for-user-data/icon_path", testDataIcon); - mainDi.override(storeMigrationVersionInjectable, () => "3.6.0"); + di.override(storeMigrationVersionInjectable, () => "3.6.0"); - createCluster = mainDi.inject(createClusterInjectionToken); - - clusterStore = mainDi.inject(clusterStoreInjectable); + clusterStore = di.inject(clusterStoreInjectable); clusterStore.load(); }); - afterEach(() => { - mockFs.restore(); - }); - it("migrates to modern format with kubeconfig in a file", async () => { const config = clusterStore.clustersList[0].kubeConfigPath; - expect(fs.readFileSync(config, "utf8")).toBe(minimalValidKubeConfig); + expect(readFileSync(config)).toBe(minimalValidKubeConfig); }); it("migrates to modern format with icon not in file", async () => { - const { icon } = clusterStore.clustersList[0].preferences; - - assert(icon); - expect(icon.startsWith("data:;base64,")).toBe(true); + expect(clusterStore.clustersList[0].preferences.icon).toMatch(/data:;base64,/); }); }); }); +const invalidKubeconfig = JSON.stringify({ + apiVersion: "v1", + clusters: [{ + cluster: { + server: "https://localhost", + }, + name: "test2", + }], + contexts: [{ + context: { + cluster: "test", + user: "test", + }, + name: "test", + }], + "current-context": "test", + kind: "Config", + preferences: {}, + users: [{ + user: { + token: "kubeconfig-user-q4lm4:xxxyyyy", + }, + name: "test", + }], +}); + const minimalValidKubeConfig = JSON.stringify({ apiVersion: "v1", clusters: [ diff --git a/src/common/app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable.ts b/src/common/app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable.ts index 5d53e506bd..0580d372f1 100644 --- a/src/common/app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable.ts +++ b/src/common/app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable.ts @@ -6,15 +6,17 @@ import { getInjectable } from "@ogre-tools/injectable"; import directoryForKubeConfigsInjectable from "../directory-for-kube-configs/directory-for-kube-configs.injectable"; import joinPathsInjectable from "../../path/join-paths.injectable"; -const getCustomKubeConfigDirectoryInjectable = getInjectable({ +export type GetCustomKubeConfigFilePath = (fileName: string) => string; + +const getCustomKubeConfigFilePathInjectable = getInjectable({ id: "get-custom-kube-config-directory", - instantiate: (di) => { + instantiate: (di): GetCustomKubeConfigFilePath => { const directoryForKubeConfigs = di.inject(directoryForKubeConfigsInjectable); const joinPaths = di.inject(joinPathsInjectable); - return (directoryName: string) => joinPaths(directoryForKubeConfigs, directoryName); + return (fileName) => joinPaths(directoryForKubeConfigs, fileName); }, }); -export default getCustomKubeConfigDirectoryInjectable; +export default getCustomKubeConfigFilePathInjectable; diff --git a/src/common/fs/write-buffer-sync.injectable.ts b/src/common/fs/write-buffer-sync.injectable.ts new file mode 100644 index 0000000000..d4d253ae66 --- /dev/null +++ b/src/common/fs/write-buffer-sync.injectable.ts @@ -0,0 +1,29 @@ +/** + * 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 getDirnameOfPathInjectable from "../path/get-dirname.injectable"; +import fsInjectable from "./fs.injectable"; + +export type WriteBufferSync = (filePath: string, contents: Buffer) => void; + +const writeBufferSyncInjectable = getInjectable({ + id: "write-buffer-sync", + instantiate: (di): WriteBufferSync => { + const { + writeFileSync, + ensureDirSync, + } = di.inject(fsInjectable); + const getDirnameOfPath = di.inject(getDirnameOfPathInjectable); + + return (filePath, contents) => { + ensureDirSync(getDirnameOfPath(filePath), { + mode: 0o755, + }); + writeFileSync(filePath, contents); + }; + }, +}); + +export default writeBufferSyncInjectable; diff --git a/src/main/cluster/store-migrations/3.6.0-beta.1.injectable.ts b/src/main/cluster/store-migrations/3.6.0-beta.1.injectable.ts index 410bb23457..9b8bb83e2b 100644 --- a/src/main/cluster/store-migrations/3.6.0-beta.1.injectable.ts +++ b/src/main/cluster/store-migrations/3.6.0-beta.1.injectable.ts @@ -7,7 +7,7 @@ // convert file path cluster icons to their base64 encoded versions import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; -import getCustomKubeConfigDirectoryInjectable from "../../../common/app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable"; +import getCustomKubeConfigFilePathInjectable from "../../../common/app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable"; import type { ClusterModel } from "../../../common/cluster-types"; import readFileSyncInjectable from "../../../common/fs/read-file-sync.injectable"; import { loadConfigFromString } from "../../../common/kube-helpers"; @@ -27,7 +27,7 @@ const v360Beta1ClusterStoreMigrationInjectable = getInjectable({ id: "v3.6.0-beta.1-cluster-store-migration", instantiate: (di) => { const userDataPath = di.inject(directoryForUserDataInjectable); - const getCustomKubeConfigDirectory = di.inject(getCustomKubeConfigDirectoryInjectable); + const getCustomKubeConfigDirectory = di.inject(getCustomKubeConfigFilePathInjectable); const readFileSync = di.inject(readFileSyncInjectable); const readFileBufferSync = di.inject(readFileBufferSyncInjectable); const joinPaths = di.inject(joinPathsInjectable); @@ -44,8 +44,8 @@ const v360Beta1ClusterStoreMigrationInjectable = getInjectable({ for (const clusterModel of storedClusters) { /** - * migrate kubeconfig - */ + * migrate kubeconfig + */ try { const absPath = getCustomKubeConfigDirectory(clusterModel.id); @@ -80,6 +80,7 @@ const v360Beta1ClusterStoreMigrationInjectable = getInjectable({ delete clusterModel.preferences?.icon; } } catch (error) { + console.log(error); logger.info(`Failed to migrate cluster icon for cluster "${clusterModel.id}"`, error); delete clusterModel.preferences?.icon; } diff --git a/src/main/cluster/store-migrations/5.0.0-beta.10.injectable.ts b/src/main/cluster/store-migrations/5.0.0-beta.10.injectable.ts index 51309cebb9..0199e57c97 100644 --- a/src/main/cluster/store-migrations/5.0.0-beta.10.injectable.ts +++ b/src/main/cluster/store-migrations/5.0.0-beta.10.injectable.ts @@ -49,10 +49,17 @@ const v500Beta10ClusterStoreMigrationInjectable = getInjectable({ 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; + // KLUDGE: remove after https://github.com/streamich/memfs/pull/893 is released + if (process.env.JEST_WORKER_ID && (error as any).code === "ENOENT") { + return; } + + if (isErrnoException(error) && error.code === "ENOENT" && error.path?.endsWith("lens-workspace-store.json")) { + // ignore lens-workspace-store.json being missing + return; + } + + throw error; } }, }; diff --git a/src/renderer/components/+add-cluster/add-cluster.tsx b/src/renderer/components/+add-cluster/add-cluster.tsx index f8929f2861..92fa0b7a03 100644 --- a/src/renderer/components/+add-cluster/add-cluster.tsx +++ b/src/renderer/components/+add-cluster/add-cluster.tsx @@ -20,7 +20,7 @@ import type { ShowNotification } from "../notifications"; import { SettingLayout } from "../layout/setting-layout"; import { MonacoEditor } from "../monaco-editor"; import { withInjectables } from "@ogre-tools/injectable-react"; -import getCustomKubeConfigDirectoryInjectable from "../../../common/app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable"; +import getCustomKubeConfigFilePathInjectable from "../../../common/app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable"; import type { NavigateToCatalog } from "../../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable"; import navigateToCatalogInjectable from "../../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable"; import type { EmitAppEvent } from "../../../common/app-event-bus/emit-event.injectable"; @@ -163,7 +163,7 @@ class NonInjectedAddCluster extends React.Component { export const AddCluster = withInjectables(NonInjectedAddCluster, { getProps: (di) => ({ - getCustomKubeConfigDirectory: di.inject(getCustomKubeConfigDirectoryInjectable), + getCustomKubeConfigDirectory: di.inject(getCustomKubeConfigFilePathInjectable), navigateToCatalog: di.inject(navigateToCatalogInjectable), getDirnameOfPath: di.inject(getDirnameOfPathInjectable), emitAppEvent: di.inject(emitAppEventInjectable),