diff --git a/package.json b/package.json index 9c59041758..26ba378293 100644 --- a/package.json +++ b/package.json @@ -351,7 +351,7 @@ "@types/semver": "^7.3.12", "@types/sharp": "^0.31.0", "@types/spdy": "^3.4.5", - "@types/tar": "^4.0.5", + "@types/tar": "^6.1.2", "@types/tar-stream": "^2.2.2", "@types/tcp-port-used": "^1.0.1", "@types/tempy": "^0.3.0", diff --git a/src/common/cluster/cluster.ts b/src/common/cluster/cluster.ts index 9b0eed76aa..b0d7fbfbc3 100644 --- a/src/common/cluster/cluster.ts +++ b/src/common/cluster/cluster.ts @@ -9,7 +9,6 @@ import type { KubeConfig } from "@kubernetes/client-node"; import { HttpError } from "@kubernetes/client-node"; import type { Kubectl } from "../../main/kubectl/kubectl"; import type { KubeconfigManager } from "../../main/kubeconfig-manager/kubeconfig-manager"; -import { loadConfigFromFile } from "../kube-helpers"; import type { KubeApiResource, KubeResource } from "../rbac"; import { apiResourceRecord, apiResources } from "../rbac"; import type { VersionDetector } from "../../main/cluster-detectors/version-detector"; @@ -25,6 +24,7 @@ import type { ListNamespaces } from "./list-namespaces.injectable"; import assert from "assert"; import type { Logger } from "../logger"; import type { BroadcastMessage } from "../ipc/broadcast-message.injectable"; +import type { LoadConfigfromFile } from "../kube-helpers/load-config-from-file.injectable"; export interface ClusterDependencies { readonly directoryForKubeConfigs: string; @@ -37,6 +37,7 @@ export interface ClusterDependencies { createListNamespaces: (config: KubeConfig) => ListNamespaces; createVersionDetector: (cluster: Cluster) => VersionDetector; broadcastMessage: BroadcastMessage; + loadConfigfromFile: LoadConfigfromFile; } /** @@ -500,7 +501,7 @@ export class Cluster implements ClusterModel, ClusterState { } async getKubeconfig(): Promise { - const { config } = await loadConfigFromFile(this.kubeConfigPath); + const { config } = await this.dependencies.loadConfigfromFile(this.kubeConfigPath); return config; } @@ -510,7 +511,7 @@ export class Cluster implements ClusterModel, ClusterState { */ async getProxyKubeconfig(): Promise { const proxyKCPath = await this.getProxyKubeconfigPath(); - const { config } = await loadConfigFromFile(proxyKCPath); + const { config } = await this.dependencies.loadConfigfromFile(proxyKCPath); return config; } diff --git a/src/common/fs/extract-tar.global-override-for-injectable.ts b/src/common/fs/extract-tar.global-override-for-injectable.ts new file mode 100644 index 0000000000..02a46c1d6b --- /dev/null +++ b/src/common/fs/extract-tar.global-override-for-injectable.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getGlobalOverride } from "../test-utils/get-global-override"; +import extractTarInjectable from "./extract-tar.injectable"; + +export default getGlobalOverride(extractTarInjectable, () => async () => { + throw new Error("tried to extract a tar file without override"); +}); diff --git a/src/common/fs/extract-tar.injectable.ts b/src/common/fs/extract-tar.injectable.ts new file mode 100644 index 0000000000..410512139e --- /dev/null +++ b/src/common/fs/extract-tar.injectable.ts @@ -0,0 +1,26 @@ +/** + * 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 type { ExtractOptions } from "tar"; +import { extract } from "tar"; +import getDirnameOfPathInjectable from "../path/get-dirname.injectable"; + +export type ExtractTar = (filePath: string, opts?: ExtractOptions) => Promise; + +const extractTarInjectable = getInjectable({ + id: "extract-tar", + instantiate: (di): ExtractTar => { + const getDirnameOfPath = di.inject(getDirnameOfPathInjectable); + + return (filePath, opts = {}) => extract({ + file: filePath, + cwd: getDirnameOfPath(filePath), + ...opts, + }); + }, + causesSideEffects: true, +}); + +export default extractTarInjectable; diff --git a/src/common/fs/move.global-override-for-injectable.ts b/src/common/fs/move.global-override-for-injectable.ts new file mode 100644 index 0000000000..c39907ee6e --- /dev/null +++ b/src/common/fs/move.global-override-for-injectable.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getGlobalOverride } from "../test-utils/get-global-override"; +import moveInjectable from "./move.injectable"; + +export default getGlobalOverride(moveInjectable, () => async () => { + throw new Error("tried to move without override"); +}); diff --git a/src/common/fs/move.injectable.ts b/src/common/fs/move.injectable.ts new file mode 100644 index 0000000000..ff11120d80 --- /dev/null +++ b/src/common/fs/move.injectable.ts @@ -0,0 +1,16 @@ +/** + * 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 type { MoveOptions } from "fs-extra"; +import fsInjectable from "./fs.injectable"; + +export type Move = (src: string, dest: string, options?: MoveOptions) => Promise; + +const moveInjectable = getInjectable({ + id: "move", + instantiate: (di): Move => di.inject(fsInjectable).move, +}); + +export default moveInjectable; diff --git a/src/common/fs/read-dir.global-override-for-injectable.ts b/src/common/fs/read-directory.global-override-for-injectable.ts similarity index 67% rename from src/common/fs/read-dir.global-override-for-injectable.ts rename to src/common/fs/read-directory.global-override-for-injectable.ts index 7f595ba307..57c83ceffb 100644 --- a/src/common/fs/read-dir.global-override-for-injectable.ts +++ b/src/common/fs/read-directory.global-override-for-injectable.ts @@ -4,8 +4,8 @@ */ import { getGlobalOverride } from "../test-utils/get-global-override"; -import readDirInjectable from "./read-dir.injectable"; +import readDirectoryInjectable from "./read-directory.injectable"; -export default getGlobalOverride(readDirInjectable, () => async () => { +export default getGlobalOverride(readDirectoryInjectable, () => async () => { throw new Error("tried to read a directory's content without override"); }); diff --git a/src/common/fs/read-dir.injectable.ts b/src/common/fs/read-directory.injectable.ts similarity index 90% rename from src/common/fs/read-dir.injectable.ts rename to src/common/fs/read-directory.injectable.ts index 54835a1c24..57632bd4d7 100644 --- a/src/common/fs/read-dir.injectable.ts +++ b/src/common/fs/read-directory.injectable.ts @@ -29,9 +29,9 @@ export interface ReadDirectory { ): Promise; } -const readDirInjectable = getInjectable({ - id: "read-dir", +const readDirectoryInjectable = getInjectable({ + id: "read-directory", instantiate: (di): ReadDirectory => di.inject(fsInjectable).readdir, }); -export default readDirInjectable; +export default readDirectoryInjectable; diff --git a/src/common/fs/remove-path.global-override-for-injectable.ts b/src/common/fs/remove-path.global-override-for-injectable.ts new file mode 100644 index 0000000000..5b9720a837 --- /dev/null +++ b/src/common/fs/remove-path.global-override-for-injectable.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getGlobalOverride } from "../test-utils/get-global-override"; +import removePathInjectable from "./remove-path.injectable"; + +export default getGlobalOverride(removePathInjectable, () => async () => { + throw new Error("tried to remove a path without override"); +}); diff --git a/src/common/fs/remove-path.injectable.ts b/src/common/fs/remove-path.injectable.ts new file mode 100644 index 0000000000..02c8da0e1e --- /dev/null +++ b/src/common/fs/remove-path.injectable.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import fsInjectable from "./fs.injectable"; + +export type RemovePath = (path: string) => Promise; + +const removePathInjectable = getInjectable({ + id: "remove-path", + instantiate: (di): RemovePath => di.inject(fsInjectable).remove, +}); + +export default removePathInjectable; diff --git a/src/common/fs/write-file.injectable.ts b/src/common/fs/write-file.injectable.ts index 70dcb76373..44774bea7b 100644 --- a/src/common/fs/write-file.injectable.ts +++ b/src/common/fs/write-file.injectable.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import path from "path"; +import getDirnameOfPathInjectable from "../path/get-dirname.injectable"; import fsInjectable from "./fs.injectable"; const writeFileInjectable = getInjectable({ @@ -11,9 +11,10 @@ const writeFileInjectable = getInjectable({ instantiate: (di) => { const { writeFile, ensureDir } = di.inject(fsInjectable); + const getDirnameOfPath = di.inject(getDirnameOfPathInjectable); return async (filePath: string, content: string | Buffer) => { - await ensureDir(path.dirname(filePath), { mode: 0o755 }); + await ensureDir(getDirnameOfPath(filePath), { mode: 0o755 }); await writeFile(filePath, content, { encoding: "utf-8", diff --git a/src/common/fs/write-json-file.injectable.ts b/src/common/fs/write-json-file.injectable.ts index 6d05e01a7c..a7079d7f84 100644 --- a/src/common/fs/write-json-file.injectable.ts +++ b/src/common/fs/write-json-file.injectable.ts @@ -3,37 +3,27 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import type { EnsureOptions, WriteOptions } from "fs-extra"; -import path from "path"; import type { JsonValue } from "type-fest"; +import getDirnameOfPathInjectable from "../path/get-dirname.injectable"; import fsInjectable from "./fs.injectable"; export type WriteJson = (filePath: string, contents: JsonValue) => Promise; -interface Dependencies { - writeJson: (file: string, object: any, options?: WriteOptions | BufferEncoding | string) => Promise; - ensureDir: (dir: string, options?: EnsureOptions | number) => Promise; -} - -const writeJsonFile = ({ writeJson, ensureDir }: Dependencies): WriteJson => async (filePath, content) => { - await ensureDir(path.dirname(filePath), { mode: 0o755 }); - - await writeJson(filePath, content, { - encoding: "utf-8", - spaces: 2, - }); -}; - const writeJsonFileInjectable = getInjectable({ id: "write-json-file", - instantiate: (di) => { + instantiate: (di): WriteJson => { const { writeJson, ensureDir } = di.inject(fsInjectable); + const getDirnameOfPath = di.inject(getDirnameOfPathInjectable); - return writeJsonFile({ - writeJson, - ensureDir, - }); + return async (filePath, content) => { + await ensureDir(getDirnameOfPath(filePath), { mode: 0o755 }); + + await writeJson(filePath, content, { + encoding: "utf-8", + spaces: 2, + }); + }; }, }); diff --git a/src/common/kube-helpers.ts b/src/common/kube-helpers.ts index f24d4dbe2d..dc388efbc8 100644 --- a/src/common/kube-helpers.ts +++ b/src/common/kube-helpers.ts @@ -4,31 +4,14 @@ */ import { KubeConfig } from "@kubernetes/client-node"; -import fse from "fs-extra"; -import path from "path"; -import os from "os"; import yaml from "js-yaml"; import logger from "../main/logger"; import type { Cluster, Context, User } from "@kubernetes/client-node/dist/config_types"; import { newClusters, newContexts, newUsers } from "@kubernetes/client-node/dist/config_types"; -import { isDefined, resolvePath } from "./utils"; +import { isDefined } from "./utils"; import Joi from "joi"; 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 { - const content = await fse.readFile(resolvePath(filePath), "utf-8"); - - return loadConfigFromString(content); -} - const clusterSchema = Joi.object({ name: Joi .string() diff --git a/src/common/kube-helpers/load-config-from-file.injectable.ts b/src/common/kube-helpers/load-config-from-file.injectable.ts new file mode 100644 index 0000000000..afa9d3c070 --- /dev/null +++ b/src/common/kube-helpers/load-config-from-file.injectable.ts @@ -0,0 +1,23 @@ +/** + * 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 readFileInjectable from "../fs/read-file.injectable"; +import type { ConfigResult } from "../kube-helpers"; +import { loadConfigFromString } from "../kube-helpers"; +import resolveTildeInjectable from "../path/resolve-tilde.injectable"; + +export type LoadConfigfromFile = (filePath: string) => Promise; + +const loadConfigfromFileInjectable = getInjectable({ + id: "load-configfrom-file", + instantiate: (di): LoadConfigfromFile => { + const readFile = di.inject(readFileInjectable); + const resolveTilde = di.inject(resolveTildeInjectable); + + return async (filePath) => loadConfigFromString(await readFile(resolveTilde(filePath))); + }, +}); + +export default loadConfigfromFileInjectable; diff --git a/src/common/os/home-directory-path.global-override-for-injectable.ts b/src/common/os/home-directory-path.global-override-for-injectable.ts new file mode 100644 index 0000000000..f5869831a6 --- /dev/null +++ b/src/common/os/home-directory-path.global-override-for-injectable.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getGlobalOverride } from "../test-utils/get-global-override"; +import homeDirectoryPathInjectable from "./home-directory-path.injectable"; + +export default getGlobalOverride(homeDirectoryPathInjectable, () => "/some-home-directory"); diff --git a/src/common/os/home-directory-path.injectable.ts b/src/common/os/home-directory-path.injectable.ts new file mode 100644 index 0000000000..b6ba1dfee0 --- /dev/null +++ b/src/common/os/home-directory-path.injectable.ts @@ -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 { homedir } from "os"; + +const homeDirectoryPathInjectable = getInjectable({ + id: "home-directory-path", + instantiate: () => homedir(), + causesSideEffects: true, +}); + +export default homeDirectoryPathInjectable; diff --git a/src/common/os/temp-directory-path.global-override-for-injectable.ts b/src/common/os/temp-directory-path.global-override-for-injectable.ts new file mode 100644 index 0000000000..05615644f9 --- /dev/null +++ b/src/common/os/temp-directory-path.global-override-for-injectable.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getGlobalOverride } from "../test-utils/get-global-override"; +import tempDirectoryPathInjectable from "./temp-directory-path.injectable"; + +export default getGlobalOverride(tempDirectoryPathInjectable, () => "/some-temp-directory"); diff --git a/src/common/os/temp-directory-path.injectable.ts b/src/common/os/temp-directory-path.injectable.ts new file mode 100644 index 0000000000..46fc5db67d --- /dev/null +++ b/src/common/os/temp-directory-path.injectable.ts @@ -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 { tmpdir } from "os"; + +const tempDirectoryPathInjectable = getInjectable({ + id: "temp-directory-path", + instantiate: () => tmpdir(), + causesSideEffects: true, +}); + +export default tempDirectoryPathInjectable; diff --git a/src/common/path/is-logical-child-path.injectable.ts b/src/common/path/is-logical-child-path.injectable.ts new file mode 100644 index 0000000000..1a52b66c0f --- /dev/null +++ b/src/common/path/is-logical-child-path.injectable.ts @@ -0,0 +1,51 @@ +/** + * 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 getAbsolutePathInjectable from "./get-absolute-path.injectable"; +import getDirnameOfPathInjectable from "./get-dirname.injectable"; + +/** + * Checks if `testPath` represents a potential filesystem entry that would be + * logically "within" the `parentPath` directory. + * + * This function will return `true` in the above case, and `false` otherwise. + * It will return `false` if the two paths are the same (after resolving them). + * + * The function makes no FS calls and is platform dependant. Meaning that the + * results are only guaranteed to be correct for the platform you are running + * on. + * @param parentPath The known path of a directory + * @param testPath The path that is to be tested + */ +export type IsLogicalChildPath = (parentPath: string, testPath: string) => boolean; + +const isLogicalChildPathInjectable = getInjectable({ + id: "is-logical-child-path", + instantiate: (di): IsLogicalChildPath => { + const getAbsolutePath = di.inject(getAbsolutePathInjectable); + const getDirnameOfPath = di.inject(getDirnameOfPathInjectable); + + return (parentPath, testPath) => { + const resolvedParentPath = getAbsolutePath(parentPath); + let resolvedTestPath = getAbsolutePath(testPath); + + if (resolvedParentPath === resolvedTestPath) { + return false; + } + + while (resolvedTestPath.length >= resolvedParentPath.length) { + if (resolvedTestPath === resolvedParentPath) { + return true; + } + + resolvedTestPath = getDirnameOfPath(resolvedTestPath); + } + + return false; + }; + }, +}); + +export default isLogicalChildPathInjectable; diff --git a/src/common/path/parse.global-override-for-injectable.ts b/src/common/path/parse.global-override-for-injectable.ts new file mode 100644 index 0000000000..fad97db696 --- /dev/null +++ b/src/common/path/parse.global-override-for-injectable.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import path from "path"; +import { getGlobalOverride } from "../test-utils/get-global-override"; +import parsePathInjectable from "./parse.injectable"; + +export default getGlobalOverride(parsePathInjectable, () => path.posix.parse); diff --git a/src/common/path/parse.injectable.ts b/src/common/path/parse.injectable.ts new file mode 100644 index 0000000000..a32dfb3fa5 --- /dev/null +++ b/src/common/path/parse.injectable.ts @@ -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 path from "path"; + +const parsePathInjectable = getInjectable({ + id: "parse-path", + instantiate: () => path.parse, + causesSideEffects: true, +}); + +export default parsePathInjectable; diff --git a/src/common/path/resolve-path.injectable.ts b/src/common/path/resolve-path.injectable.ts new file mode 100644 index 0000000000..75a1e98c59 --- /dev/null +++ b/src/common/path/resolve-path.injectable.ts @@ -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 getAbsolutePathInjectable from "./get-absolute-path.injectable"; +import resolveTildeInjectable from "./resolve-tilde.injectable"; + +export type ResolvePath = (path: string) => string; + +const resolvePathInjectable = getInjectable({ + id: "resolve-path", + instantiate: (di): ResolvePath => { + const getAbsolutePath = di.inject(getAbsolutePathInjectable); + const resolveTilde = di.inject(resolveTildeInjectable); + + return (filePath) => getAbsolutePath(resolveTilde(filePath)); + }, +}); + +export default resolvePathInjectable; diff --git a/src/common/path/resolve-tilde.injectable.ts b/src/common/path/resolve-tilde.injectable.ts new file mode 100644 index 0000000000..86d267aa4f --- /dev/null +++ b/src/common/path/resolve-tilde.injectable.ts @@ -0,0 +1,31 @@ +/** + * 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 homeDirectoryPathInjectable from "../os/home-directory-path.injectable"; +import fileSystemSeparatorInjectable from "./separator.injectable"; + +export type ResolveTilde = (path: string) => string; + +const resolveTildeInjectable = getInjectable({ + id: "resolve-tilde", + instantiate: (di): ResolveTilde => { + const homeDirectoryPath = di.inject(homeDirectoryPathInjectable); + const fileSystemSeparator = di.inject(fileSystemSeparatorInjectable); + + return (filePath) => { + if (filePath === "~") { + return homeDirectoryPath; + } + + if (filePath === `~${fileSystemSeparator}`) { + return `${homeDirectoryPath}${filePath.slice(1)}`; + } + + return filePath; + }; + }, +}); + +export default resolveTildeInjectable; diff --git a/src/common/user-store/file-name-migration.injectable.ts b/src/common/user-store/file-name-migration.injectable.ts index caf94dc491..106f559ef0 100644 --- a/src/common/user-store/file-name-migration.injectable.ts +++ b/src/common/user-store/file-name-migration.injectable.ts @@ -4,10 +4,10 @@ */ 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 { getInjectable } from "@ogre-tools/injectable"; +import joinPathsInjectable from "../path/join-paths.injectable"; export type UserStoreFileNameMigration = () => Promise; @@ -15,8 +15,9 @@ const userStoreFileNameMigrationInjectable = getInjectable({ id: "user-store-file-name-migration", instantiate: (di): UserStoreFileNameMigration => { const userDataPath = di.inject(directoryForUserDataInjectable); - const configJsonPath = path.join(userDataPath, "config.json"); - const lensUserStoreJsonPath = path.join(userDataPath, "lens-user-store.json"); + const joinPaths = di.inject(joinPathsInjectable); + const configJsonPath = joinPaths(userDataPath, "config.json"); + const lensUserStoreJsonPath = joinPaths(userDataPath, "lens-user-store.json"); return async () => { try { diff --git a/src/common/user-store/user-store.ts b/src/common/user-store/user-store.ts index 6c995996bf..8a8ff6735c 100644 --- a/src/common/user-store/user-store.ts +++ b/src/common/user-store/user-store.ts @@ -7,7 +7,6 @@ import { app } from "electron"; import { action, computed, observable, reaction, makeObservable, isObservableArray, isObservableSet, isObservableMap } from "mobx"; import { BaseStore } from "../base-store"; import migrations from "../../migrations/user-store"; -import { kubeConfigDefaultPath } from "../kube-helpers"; import { getOrInsertSet, toggle, toJS, object } from "../../renderer/utils"; import { DESCRIPTORS } from "./preferences-helpers"; import type { UserPreferencesModel, StoreType } from "./preferences-helpers"; @@ -38,12 +37,6 @@ export class UserStore extends BaseStore /* implements UserStore @observable lastSeenAppVersion = "0.0.0"; - /** - * used in add-cluster page for providing context - * @deprecated No longer used - */ - @observable kubeConfigPath = kubeConfigDefaultPath; - /** * @deprecated No longer used */ diff --git a/src/common/utils/__tests__/paths.test.ts b/src/common/utils/__tests__/paths.test.ts index f5e8a7ccf5..5a52843432 100644 --- a/src/common/utils/__tests__/paths.test.ts +++ b/src/common/utils/__tests__/paths.test.ts @@ -3,12 +3,29 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { describeIf } from "../../../../integration/helpers/utils"; -import { isWindows } from "../../vars"; -import { isLogicalChildPath } from "../paths"; +import type { DiContainer } from "@ogre-tools/injectable"; +import path from "path"; +import { getDiForUnitTesting } from "../../../main/getDiForUnitTesting"; +import getAbsolutePathInjectable from "../../path/get-absolute-path.injectable"; +import getDirnameOfPathInjectable from "../../path/get-dirname.injectable"; +import type { IsLogicalChildPath } from "../../path/is-logical-child-path.injectable"; +import isLogicalChildPathInjectable from "../../path/is-logical-child-path.injectable"; describe("isLogicalChildPath", () => { - describeIf(isWindows)("windows tests", () => { + let di: DiContainer; + let isLogicalChildPath: IsLogicalChildPath; + + beforeEach(() => { + di = getDiForUnitTesting(); + }); + + describe("when using win32 paths", () => { + beforeEach(() => { + di.override(getAbsolutePathInjectable, () => path.win32.resolve); + di.override(getDirnameOfPathInjectable, () => path.win32.dirname); + isLogicalChildPath = di.inject(isLogicalChildPathInjectable); + }); + it.each([ { parentPath: "C:\\Foo", @@ -40,7 +57,13 @@ describe("isLogicalChildPath", () => { }); }); - describeIf(!isWindows)("posix tests", () => { + describe("when using posix paths", () => { + beforeEach(() => { + di.override(getAbsolutePathInjectable, () => path.posix.resolve); + di.override(getDirnameOfPathInjectable, () => path.posix.dirname); + isLogicalChildPath = di.inject(isLogicalChildPathInjectable); + }); + it.each([ { parentPath: "/foo", diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index e16f803c56..a9acaede86 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -22,7 +22,6 @@ export * from "./hash-set"; export * from "./n-fircate"; export * from "./noop"; export * from "./observable-crate/impl"; -export * from "./paths"; export * from "./promise-exec"; export * from "./readonly"; export * from "./reject-promise"; diff --git a/src/common/utils/paths.ts b/src/common/utils/paths.ts deleted file mode 100644 index ed31e612fb..0000000000 --- a/src/common/utils/paths.ts +++ /dev/null @@ -1,55 +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 os from "os"; - -export function resolveTilde(filePath: string) { - if (filePath === "~") { - return os.homedir(); - } - - if (filePath.startsWith("~/")) { - return `${os.homedir()}${filePath.slice(1)}`; - } - - return filePath; -} - -export function resolvePath(filePath: string): string { - return path.resolve(resolveTilde(filePath)); -} - -/** - * Checks if `testPath` represents a potential filesystem entry that would be - * logically "within" the `parentPath` directory. - * - * This function will return `true` in the above case, and `false` otherwise. - * It will return `false` if the two paths are the same (after resolving them). - * - * The function makes no FS calls and is platform dependant. Meaning that the - * results are only guaranteed to be correct for the platform you are running - * on. - * @param parentPath The known path of a directory - * @param testPath The path that is to be tested - */ -export function isLogicalChildPath(parentPath: string, testPath: string): boolean { - parentPath = path.resolve(parentPath); - testPath = path.resolve(testPath); - - if (parentPath === testPath) { - return false; - } - - while (testPath.length >= parentPath.length) { - if (testPath === parentPath) { - return true; - } - - testPath = path.dirname(testPath); - } - - return false; -} diff --git a/src/common/utils/tar.ts b/src/common/utils/tar.ts index d351ec2507..58008dfe9d 100644 --- a/src/common/utils/tar.ts +++ b/src/common/utils/tar.ts @@ -5,7 +5,7 @@ // Helper for working with tarball files (.tar, .tgz) // Docs: https://github.com/npm/node-tar -import type { ExtractOptions, FileStat } from "tar"; +import type { FileStat } from "tar"; import tar from "tar"; import path from "path"; import { parse } from "./json"; @@ -62,18 +62,10 @@ export async function listTarEntries(filePath: string): Promise { await tar.list({ file: filePath, - onentry: (entry: FileStat) => { + onentry: (entry) => { entries.push(path.normalize(entry.path as unknown as string)); }, }); return entries; } - -export function extractTar(filePath: string, opts: ExtractOptions & { sync?: boolean } = {}) { - return tar.extract({ - file: filePath, - cwd: path.dirname(filePath), - ...opts, - }); -} diff --git a/src/extensions/extension-discovery/extension-discovery.injectable.ts b/src/extensions/extension-discovery/extension-discovery.injectable.ts index 1a85f30597..a2bb9d3944 100644 --- a/src/extensions/extension-discovery/extension-discovery.injectable.ts +++ b/src/extensions/extension-discovery/extension-discovery.injectable.ts @@ -18,16 +18,16 @@ import pathExistsInjectable from "../../common/fs/path-exists.injectable"; import watchInjectable from "../../common/fs/watch/watch.injectable"; import accessPathInjectable from "../../common/fs/access-path.injectable"; import copyInjectable from "../../common/fs/copy.injectable"; -import deleteFileInjectable from "../../common/fs/delete-file.injectable"; import ensureDirInjectable from "../../common/fs/ensure-dir.injectable"; import isProductionInjectable from "../../common/vars/is-production.injectable"; import lstatInjectable from "../../common/fs/lstat.injectable"; -import readDirInjectable from "../../common/fs/read-dir.injectable"; +import readDirectoryInjectable from "../../common/fs/read-directory.injectable"; import fileSystemSeparatorInjectable from "../../common/path/separator.injectable"; import getBasenameOfPathInjectable from "../../common/path/get-basename.injectable"; import getDirnameOfPathInjectable from "../../common/path/get-dirname.injectable"; import getRelativePathInjectable from "../../common/path/get-relative-path.injectable"; import joinPathsInjectable from "../../common/path/join-paths.injectable"; +import removePathInjectable from "../../common/fs/remove-path.injectable"; const extensionDiscoveryInjectable = getInjectable({ id: "extension-discovery", @@ -47,11 +47,11 @@ const extensionDiscoveryInjectable = getInjectable({ logger: di.inject(loggerInjectable), accessPath: di.inject(accessPathInjectable), copy: di.inject(copyInjectable), - deleteFile: di.inject(deleteFileInjectable), + removePath: di.inject(removePathInjectable), ensureDirectory: di.inject(ensureDirInjectable), isProduction: di.inject(isProductionInjectable), lstat: di.inject(lstatInjectable), - readDirectory: di.inject(readDirInjectable), + readDirectory: di.inject(readDirectoryInjectable), fileSystemSeparator: di.inject(fileSystemSeparatorInjectable), getBasenameOfPath: di.inject(getBasenameOfPathInjectable), getDirnameOfPath: di.inject(getDirnameOfPathInjectable), diff --git a/src/extensions/extension-discovery/extension-discovery.ts b/src/extensions/extension-discovery/extension-discovery.ts index b6e8bd0aa0..ab54ba842a 100644 --- a/src/extensions/extension-discovery/extension-discovery.ts +++ b/src/extensions/extension-discovery/extension-discovery.ts @@ -22,9 +22,8 @@ import type { PathExists } from "../../common/fs/path-exists.injectable"; import type { Watch } from "../../common/fs/watch/watch.injectable"; import type { Stats } from "fs"; import { constants } from "fs"; -import type { DeleteFile } from "../../common/fs/delete-file.injectable"; import type { LStat } from "../../common/fs/lstat.injectable"; -import type { ReadDirectory } from "../../common/fs/read-dir.injectable"; +import type { ReadDirectory } from "../../common/fs/read-directory.injectable"; import type { EnsureDirectory } from "../../common/fs/ensure-dir.injectable"; import type { AccessPath } from "../../common/fs/access-path.injectable"; import type { Copy } from "../../common/fs/copy.injectable"; @@ -32,6 +31,7 @@ import type { JoinPaths } from "../../common/path/join-paths.injectable"; import type { GetBasenameOfPath } from "../../common/path/get-basename.injectable"; import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable"; import type { GetRelativePath } from "../../common/path/get-relative-path.injectable"; +import type { RemovePath } from "../../common/fs/remove-path.injectable"; interface Dependencies { readonly extensionLoader: ExtensionLoader; @@ -47,7 +47,7 @@ interface Dependencies { installExtensions: (packageJsonPath: string, packagesJson: PackageJson) => Promise; readJsonFile: ReadJson; pathExists: PathExists; - deleteFile: DeleteFile; + removePath: RemovePath; lstat: LStat; watch: Watch; readDirectory: ReadDirectory; @@ -222,7 +222,7 @@ export class ExtensionDiscovery { if (extension) { // Remove a broken symlink left by a previous installation if it exists. - await this.dependencies.deleteFile(extension.manifestPath); + await this.dependencies.removePath(extension.manifestPath); // Install dependencies for the new extension await this.dependencies.installExtension(extension.absolutePath); @@ -285,7 +285,7 @@ export class ExtensionDiscovery { * @param name e.g. "@mirantis/lens-extension-cc" */ removeSymlinkByPackageName(name: string): Promise { - return this.dependencies.deleteFile(this.getInstalledPath(name)); + return this.dependencies.removePath(this.getInstalledPath(name)); } /** @@ -307,7 +307,7 @@ export class ExtensionDiscovery { await this.removeSymlinkByPackageName(manifest.name); // fs.remove does nothing if the path doesn't exist anymore - await this.dependencies.deleteFile(absolutePath); + await this.dependencies.removePath(absolutePath); } async load(): Promise> { @@ -322,7 +322,7 @@ export class ExtensionDiscovery { `${logModule} loading extensions from ${this.dependencies.extensionPackageRootDirectory}`, ); - await this.dependencies.deleteFile(this.dependencies.joinPaths(this.dependencies.extensionPackageRootDirectory, "package-lock.json")); + await this.dependencies.removePath(this.dependencies.joinPaths(this.dependencies.extensionPackageRootDirectory, "package-lock.json")); const canWriteToInTreeFolder = await this.dependencies.accessPath(this.inTreeFolderPath, constants.W_OK); @@ -331,7 +331,7 @@ export class ExtensionDiscovery { this.bundledFolderPath = this.inTreeFolderPath; } else { // Remove e.g. /Users//Library/Application Support/LensDev/extensions - await this.dependencies.deleteFile(this.inTreeTargetPath); + await this.dependencies.removePath(this.inTreeTargetPath); // Create folder e.g. /Users//Library/Application Support/LensDev/extensions await this.dependencies.ensureDirectory(this.inTreeTargetPath); diff --git a/src/extensions/extension-loader/extension-loader.injectable.ts b/src/extensions/extension-loader/extension-loader.injectable.ts index 78e88a9cde..48edce4446 100644 --- a/src/extensions/extension-loader/extension-loader.injectable.ts +++ b/src/extensions/extension-loader/extension-loader.injectable.ts @@ -10,6 +10,8 @@ import extensionInstancesInjectable from "./extension-instances.injectable"; import type { LensExtension } from "../lens-extension"; import extensionInjectable from "./extension/extension.injectable"; import loggerInjectable from "../../common/logger.injectable"; +import joinPathsInjectable from "../../common/path/join-paths.injectable"; +import getDirnameOfPathInjectable from "../../common/path/get-dirname.injectable"; const extensionLoaderInjectable = getInjectable({ id: "extension-loader", @@ -20,6 +22,8 @@ const extensionLoaderInjectable = getInjectable({ extensionInstances: di.inject(extensionInstancesInjectable), getExtension: (instance: LensExtension) => di.inject(extensionInjectable, instance), logger: di.inject(loggerInjectable), + joinPaths: di.inject(joinPathsInjectable), + getDirnameOfPath: di.inject(getDirnameOfPathInjectable), }), }); diff --git a/src/extensions/extension-loader/extension-loader.ts b/src/extensions/extension-loader/extension-loader.ts index dcdd9dd219..5c63451276 100644 --- a/src/extensions/extension-loader/extension-loader.ts +++ b/src/extensions/extension-loader/extension-loader.ts @@ -7,7 +7,6 @@ import { ipcMain, ipcRenderer } from "electron"; import { isEqual } from "lodash"; import type { ObservableMap } from "mobx"; import { action, computed, makeObservable, observable, observe, reaction, when } from "mobx"; -import path from "path"; import { broadcastMessage, ipcMainOn, ipcRendererOn, ipcMainHandle } from "../../common/ipc"; import type { Disposer } from "../../common/utils"; import { isDefined, toJS } from "../../common/utils"; @@ -23,6 +22,8 @@ import { EventEmitter } from "../../common/event-emitter"; import type { CreateExtensionInstance } from "./create-extension-instance.token"; import type { Extension } from "./extension/extension.injectable"; import type { Logger } from "../../common/logger"; +import type { JoinPaths } from "../../common/path/join-paths.injectable"; +import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable"; const logModule = "[EXTENSIONS-LOADER]"; @@ -32,6 +33,8 @@ interface Dependencies { updateExtensionsState: (extensionsState: Record) => void; createExtensionInstance: CreateExtensionInstance; getExtension: (instance: LensExtension) => Extension; + joinPaths: JoinPaths; + getDirnameOfPath: GetDirnameOfPath; } export interface ExtensionLoading { @@ -371,7 +374,7 @@ export class ExtensionLoader { return null; } - const extAbsolutePath = path.resolve(path.join(path.dirname(extension.manifestPath), extRelativePath)); + const extAbsolutePath = this.dependencies.joinPaths(this.dependencies.getDirnameOfPath(extension.manifestPath), extRelativePath); try { return __non_webpack_require__(extAbsolutePath).default; diff --git a/src/main/create-cluster/create-cluster.injectable.ts b/src/main/create-cluster/create-cluster.injectable.ts index d8bf4209f0..5290e92db3 100644 --- a/src/main/create-cluster/create-cluster.injectable.ts +++ b/src/main/create-cluster/create-cluster.injectable.ts @@ -16,6 +16,7 @@ import loggerInjectable from "../../common/logger.injectable"; import detectorRegistryInjectable from "../cluster-detectors/detector-registry.injectable"; import createVersionDetectorInjectable from "../cluster-detectors/create-version-detector.injectable"; import broadcastMessageInjectable from "../../common/ipc/broadcast-message.injectable"; +import loadConfigfromFileInjectable from "../../common/kube-helpers/load-config-from-file.injectable"; const createClusterInjectable = getInjectable({ id: "create-cluster", @@ -32,6 +33,7 @@ const createClusterInjectable = getInjectable({ detectorRegistry: di.inject(detectorRegistryInjectable), createVersionDetector: di.inject(createVersionDetectorInjectable), broadcastMessage: di.inject(broadcastMessageInjectable), + loadConfigfromFile: di.inject(loadConfigfromFileInjectable), }; return (model, configData) => new Cluster(dependencies, model, configData); diff --git a/src/main/kube-auth-proxy/create-kube-auth-proxy.injectable.ts b/src/main/kube-auth-proxy/create-kube-auth-proxy.injectable.ts index 67cfd8c26e..f272d9e56f 100644 --- a/src/main/kube-auth-proxy/create-kube-auth-proxy.injectable.ts +++ b/src/main/kube-auth-proxy/create-kube-auth-proxy.injectable.ts @@ -6,7 +6,6 @@ import { getInjectable } from "@ogre-tools/injectable"; import type { KubeAuthProxyDependencies } from "./kube-auth-proxy"; import { KubeAuthProxy } from "./kube-auth-proxy"; import type { Cluster } from "../../common/cluster/cluster"; -import path from "path"; import selfsigned from "selfsigned"; import { getBinaryName } from "../../common/vars"; import spawnInjectable from "../child-process/spawn.injectable"; @@ -14,6 +13,7 @@ import { getKubeAuthProxyCertificate } from "./get-kube-auth-proxy-certificate"; import loggerInjectable from "../../common/logger.injectable"; import baseBundledBinariesDirectoryInjectable from "../../common/vars/base-bundled-binaries-dir.injectable"; import waitUntilPortIsUsedInjectable from "./wait-until-port-is-used/wait-until-port-is-used.injectable"; +import joinPathsInjectable from "../../common/path/join-paths.injectable"; export type CreateKubeAuthProxy = (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => KubeAuthProxy; @@ -22,11 +22,12 @@ const createKubeAuthProxyInjectable = getInjectable({ instantiate: (di): CreateKubeAuthProxy => { const binaryName = getBinaryName("lens-k8s-proxy"); + const joinPaths = di.inject(joinPathsInjectable); return (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => { const clusterUrl = new URL(cluster.apiUrl); const dependencies: KubeAuthProxyDependencies = { - proxyBinPath: path.join(di.inject(baseBundledBinariesDirectoryInjectable), binaryName), + proxyBinPath: joinPaths(di.inject(baseBundledBinariesDirectoryInjectable), binaryName), proxyCert: getKubeAuthProxyCertificate(clusterUrl.hostname, selfsigned.generate), spawn: di.inject(spawnInjectable), logger: di.inject(loggerInjectable), diff --git a/src/main/kubeconfig-manager/create-kubeconfig-manager.injectable.ts b/src/main/kubeconfig-manager/create-kubeconfig-manager.injectable.ts index e1f365f098..010e6e1446 100644 --- a/src/main/kubeconfig-manager/create-kubeconfig-manager.injectable.ts +++ b/src/main/kubeconfig-manager/create-kubeconfig-manager.injectable.ts @@ -9,6 +9,8 @@ import type { KubeconfigManagerDependencies } from "./kubeconfig-manager"; import { KubeconfigManager } from "./kubeconfig-manager"; import loggerInjectable from "../../common/logger.injectable"; import lensProxyPortInjectable from "../lens-proxy/lens-proxy-port.injectable"; +import joinPathsInjectable from "../../common/path/join-paths.injectable"; +import getDirnameOfPathInjectable from "../../common/path/get-dirname.injectable"; export interface KubeConfigManagerInstantiationParameter { cluster: Cluster; @@ -24,6 +26,8 @@ const createKubeconfigManagerInjectable = getInjectable({ directoryForTemp: di.inject(directoryForTempInjectable), logger: di.inject(loggerInjectable), lensProxyPort: di.inject(lensProxyPortInjectable), + joinPaths: di.inject(joinPathsInjectable), + getDirnameOfPath: di.inject(getDirnameOfPathInjectable), }; return (cluster) => new KubeconfigManager(dependencies, cluster); diff --git a/src/main/kubeconfig-manager/kubeconfig-manager.ts b/src/main/kubeconfig-manager/kubeconfig-manager.ts index d7a9794a0f..e2c1aa1f6d 100644 --- a/src/main/kubeconfig-manager/kubeconfig-manager.ts +++ b/src/main/kubeconfig-manager/kubeconfig-manager.ts @@ -6,17 +6,20 @@ import type { KubeConfig } from "@kubernetes/client-node"; import type { Cluster } from "../../common/cluster/cluster"; import type { ClusterContextHandler } from "../context-handler/context-handler"; -import path from "path"; import fs from "fs-extra"; import { dumpConfigYaml } from "../../common/kube-helpers"; import { isErrnoException } from "../../common/utils"; import type { PartialDeep } from "type-fest"; import type { Logger } from "../../common/logger"; +import type { JoinPaths } from "../../common/path/join-paths.injectable"; +import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable"; export interface KubeconfigManagerDependencies { readonly directoryForTemp: string; readonly logger: Logger; - lensProxyPort: { get: () => number }; + readonly lensProxyPort: { get: () => number }; + joinPaths: JoinPaths; + getDirnameOfPath: GetDirnameOfPath; } export class KubeconfigManager { @@ -93,7 +96,7 @@ export class KubeconfigManager { protected async createProxyKubeconfig(): Promise { const { cluster } = this; const { contextName, id } = cluster; - const tempFile = path.join( + const tempFile = this.dependencies.joinPaths( this.dependencies.directoryForTemp, `kubeconfig-${id}`, ); @@ -121,7 +124,7 @@ export class KubeconfigManager { // write const configYaml = dumpConfigYaml(proxyConfig); - await fs.ensureDir(path.dirname(tempFile)); + await fs.ensureDir(this.dependencies.getDirnameOfPath(tempFile)); await fs.writeFile(tempFile, configYaml, { mode: 0o600 }); this.dependencies.logger.debug(`[KUBECONFIG-MANAGER]: Created temp kubeconfig "${contextName}" at "${tempFile}": \n${configYaml}`); diff --git a/src/main/kubectl/bundled-binary-path.injectable.ts b/src/main/kubectl/bundled-binary-path.injectable.ts index df5ee63c04..f3321f7776 100644 --- a/src/main/kubectl/bundled-binary-path.injectable.ts +++ b/src/main/kubectl/bundled-binary-path.injectable.ts @@ -3,16 +3,20 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import path from "path"; +import joinPathsInjectable from "../../common/path/join-paths.injectable"; import baseBundledBinariesDirectoryInjectable from "../../common/vars/base-bundled-binaries-dir.injectable"; import kubectlBinaryNameInjectable from "./binary-name.injectable"; const bundledKubectlBinaryPathInjectable = getInjectable({ id: "bundled-kubectl-binary-path", - instantiate: (di) => path.join( - di.inject(baseBundledBinariesDirectoryInjectable), - di.inject(kubectlBinaryNameInjectable), - ), + instantiate: (di) => { + const joinPaths = di.inject(joinPathsInjectable); + + return joinPaths( + di.inject(baseBundledBinariesDirectoryInjectable), + di.inject(kubectlBinaryNameInjectable), + ); + }, }); export default bundledKubectlBinaryPathInjectable; diff --git a/src/main/kubectl/create-kubectl.injectable.ts b/src/main/kubectl/create-kubectl.injectable.ts index d0187f7ba4..f5a278ab7b 100644 --- a/src/main/kubectl/create-kubectl.injectable.ts +++ b/src/main/kubectl/create-kubectl.injectable.ts @@ -14,6 +14,9 @@ import bundledKubectlBinaryPathInjectable from "./bundled-binary-path.injectable import baseBundledBinariesDirectoryInjectable from "../../common/vars/base-bundled-binaries-dir.injectable"; import bundledKubectlVersionInjectable from "../../common/vars/bundled-kubectl-version.injectable"; import kubectlVersionMapInjectable from "./version-map.injectable"; +import getDirnameOfPathInjectable from "../../common/path/get-dirname.injectable"; +import joinPathsInjectable from "../../common/path/join-paths.injectable"; +import getBasenameOfPathInjectable from "../../common/path/get-basename.injectable"; const createKubectlInjectable = getInjectable({ id: "create-kubectl", @@ -29,6 +32,9 @@ const createKubectlInjectable = getInjectable({ baseBundeledBinariesDirectory: di.inject(baseBundledBinariesDirectoryInjectable), bundledKubectlVersion: di.inject(bundledKubectlVersionInjectable), kubectlVersionMap: di.inject(kubectlVersionMapInjectable), + getDirnameOfPath: di.inject(getDirnameOfPathInjectable), + joinPaths: di.inject(joinPathsInjectable), + getBasenameOfPath: di.inject(getBasenameOfPathInjectable), }; return (clusterVersion: string) => new Kubectl(dependencies, clusterVersion); diff --git a/src/main/kubectl/kubectl.ts b/src/main/kubectl/kubectl.ts index 2c99beec22..b77dbba97d 100644 --- a/src/main/kubectl/kubectl.ts +++ b/src/main/kubectl/kubectl.ts @@ -3,7 +3,6 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import path from "path"; import fs from "fs"; import { promiseExecFile } from "../../common/utils/promise-exec"; import logger from "../logger"; @@ -15,6 +14,9 @@ import got from "got/dist/source"; import { promisify } from "util"; import stream from "stream"; import { noop } from "lodash/fp"; +import type { JoinPaths } from "../../common/path/join-paths.injectable"; +import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable"; +import type { GetBasenameOfPath } from "../../common/path/get-basename.injectable"; const initScriptVersionString = "# lens-initscript v3"; @@ -33,6 +35,9 @@ export interface KubectlDependencies { }; readonly bundledKubectlVersion: string; readonly kubectlVersionMap: Map; + joinPaths: JoinPaths; + getDirnameOfPath: GetDirnameOfPath; + getBasenameOfPath: GetBasenameOfPath; } export class Kubectl { @@ -71,8 +76,8 @@ export class Kubectl { } this.url = `${this.getDownloadMirror()}/v${this.kubectlVersion}/bin/${this.dependencies.normalizedDownloadPlatform}/${this.dependencies.normalizedDownloadArch}/${this.dependencies.kubectlBinaryName}`; - this.dirname = path.normalize(path.join(this.getDownloadDir(), this.kubectlVersion)); - this.path = path.join(this.dirname, this.dependencies.kubectlBinaryName); + this.dirname = this.dependencies.joinPaths(this.getDownloadDir(), this.kubectlVersion); + this.path = this.dependencies.joinPaths(this.dirname, this.dependencies.kubectlBinaryName); } public getBundledPath() { @@ -85,7 +90,7 @@ export class Kubectl { protected getDownloadDir() { if (this.dependencies.userStore.downloadBinariesPath) { - return path.join(this.dependencies.userStore.downloadBinariesPath, "kubectl"); + return this.dependencies.joinPaths(this.dependencies.userStore.downloadBinariesPath, "kubectl"); } return this.dependencies.directoryForKubectlBinaries; @@ -104,7 +109,7 @@ export class Kubectl { if (!await this.checkBinary(this.getBundledPath(), false)) { Kubectl.invalidBundle = true; - return path.basename(this.getBundledPath()); + return this.dependencies.getBasenameOfPath(this.getBundledPath()); } try { @@ -246,7 +251,7 @@ export class Kubectl { } public async downloadKubectl() { - await ensureDir(path.dirname(this.path), 0o755); + await ensureDir(this.dependencies.getDirnameOfPath(this.path), 0o755); logger.info(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`); @@ -268,9 +273,9 @@ export class Kubectl { const binariesDir = this.dependencies.baseBundeledBinariesDirectory; const kubectlPath = this.dependencies.userStore.downloadKubectlBinaries ? this.dirname - : path.dirname(this.getPathFromPreferences()); + : this.dependencies.getDirnameOfPath(this.getPathFromPreferences()); - const bashScriptPath = path.join(this.dirname, ".bash_set_path"); + const bashScriptPath = this.dependencies.joinPaths(this.dirname, ".bash_set_path"); const bashScript = [ initScriptVersionString, "tempkubeconfig=\"$KUBECONFIG\"", @@ -292,7 +297,7 @@ export class Kubectl { "unset tempkubeconfig", ].join("\n"); - const zshScriptPath = path.join(this.dirname, ".zlogin"); + const zshScriptPath = this.dependencies.joinPaths(this.dirname, ".zlogin"); const zshScript = [ initScriptVersionString, "tempkubeconfig=\"$KUBECONFIG\"", diff --git a/src/main/shell-session/local-shell-session/local-shell-session.ts b/src/main/shell-session/local-shell-session/local-shell-session.ts index cae62affe2..c13bbeb5a2 100644 --- a/src/main/shell-session/local-shell-session/local-shell-session.ts +++ b/src/main/shell-session/local-shell-session/local-shell-session.ts @@ -3,16 +3,21 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import path from "path"; import type { UserStore } from "../../../common/user-store"; import type { ShellSessionArgs, ShellSessionDependencies } from "../shell-session"; import { ShellSession } from "../shell-session"; import type { ModifyTerminalShellEnv } from "../shell-env-modifier/modify-terminal-shell-env.injectable"; +import type { JoinPaths } from "../../../common/path/join-paths.injectable"; +import type { GetDirnameOfPath } from "../../../common/path/get-dirname.injectable"; +import type { GetBasenameOfPath } from "../../../common/path/get-basename.injectable"; export interface LocalShellSessionDependencies extends ShellSessionDependencies { - modifyTerminalShellEnv: ModifyTerminalShellEnv; readonly directoryForBinaries: string; readonly userStore: UserStore; + modifyTerminalShellEnv: ModifyTerminalShellEnv; + joinPaths: JoinPaths; + getDirnameOfPath: GetDirnameOfPath; + getBasenameOfPath: GetBasenameOfPath; } export class LocalShellSession extends ShellSession { @@ -49,13 +54,15 @@ export class LocalShellSession extends ShellSession { protected async getShellArgs(shell: string): Promise { const pathFromPreferences = this.dependencies.userStore.kubectlBinariesPath || this.kubectl.getBundledPath(); - const kubectlPathDir = this.dependencies.userStore.downloadKubectlBinaries ? await this.kubectlBinDirP : path.dirname(pathFromPreferences); + const kubectlPathDir = this.dependencies.userStore.downloadKubectlBinaries + ? await this.kubectlBinDirP + : this.dependencies.getDirnameOfPath(pathFromPreferences); - switch(path.basename(shell)) { + switch(this.dependencies.getBasenameOfPath(shell)) { case "powershell.exe": return ["-NoExit", "-command", `& {$Env:PATH="${kubectlPathDir};${this.dependencies.directoryForBinaries};$Env:PATH"}`]; case "bash": - return ["--init-file", path.join(await this.kubectlBinDirP, ".bash_set_path")]; + return ["--init-file", this.dependencies.joinPaths(await this.kubectlBinDirP, ".bash_set_path")]; case "fish": return ["--login", "--init-command", `export PATH="${kubectlPathDir}:${this.dependencies.directoryForBinaries}:$PATH"; export KUBECONFIG="${await this.kubeconfigPathP}"`]; case "zsh": diff --git a/src/main/shell-session/local-shell-session/open.injectable.ts b/src/main/shell-session/local-shell-session/open.injectable.ts index fefba0902c..0187ba97e7 100644 --- a/src/main/shell-session/local-shell-session/open.injectable.ts +++ b/src/main/shell-session/local-shell-session/open.injectable.ts @@ -14,6 +14,9 @@ import isWindowsInjectable from "../../../common/vars/is-windows.injectable"; import loggerInjectable from "../../../common/logger.injectable"; import userStoreInjectable from "../../../common/user-store/user-store.injectable"; import type WebSocket from "ws"; +import getDirnameOfPathInjectable from "../../../common/path/get-dirname.injectable"; +import joinPathsInjectable from "../../../common/path/join-paths.injectable"; +import getBasenameOfPathInjectable from "../../../common/path/get-basename.injectable"; export interface OpenLocalShellSessionArgs { websocket: WebSocket; @@ -35,6 +38,9 @@ const openLocalShellSessionInjectable = getInjectable({ isWindows: di.inject(isWindowsInjectable), logger: di.inject(loggerInjectable), userStore: di.inject(userStoreInjectable), + getDirnameOfPath: di.inject(getDirnameOfPathInjectable), + joinPaths: di.inject(joinPathsInjectable), + getBasenameOfPath: di.inject(getBasenameOfPathInjectable), }; return (args) => { diff --git a/src/migrations/cluster-store/3.6.0-beta.1.ts b/src/migrations/cluster-store/3.6.0-beta.1.ts index 09dbd1fd61..5f6bdbc9b7 100644 --- a/src/migrations/cluster-store/3.6.0-beta.1.ts +++ b/src/migrations/cluster-store/3.6.0-beta.1.ts @@ -6,9 +6,8 @@ // 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 { loadConfigFromString } from "../../common/kube-helpers"; import type { MigrationDeclaration } from "../helpers"; import { migrationLog } from "../helpers"; import type { ClusterModel } from "../../common/cluster-types"; @@ -19,6 +18,8 @@ import directoryForKubeConfigsInjectable from "../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; import getCustomKubeConfigDirectoryInjectable from "../../common/app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable"; +import readFileSyncInjectable from "../../common/fs/read-file-sync.injectable"; +import joinPathsInjectable from "../../common/path/join-paths.injectable"; interface Pre360ClusterModel extends ClusterModel { kubeConfig?: string; @@ -32,6 +33,8 @@ export default { const userDataPath = di.inject(directoryForUserDataInjectable); const kubeConfigsPath = di.inject(directoryForKubeConfigsInjectable); const getCustomKubeConfigDirectory = di.inject(getCustomKubeConfigDirectoryInjectable); + const readFileSync = di.inject(readFileSyncInjectable); + const joinPaths = di.inject(joinPathsInjectable); const storedClusters: Pre360ClusterModel[] = store.get("clusters") ?? []; const migratedClusters: ClusterModel[] = []; @@ -55,7 +58,7 @@ export default { fse.writeFileSync(absPath, clusterModel.kubeConfig, { encoding: "utf-8", mode: 0o600 }); clusterModel.kubeConfigPath = absPath; - clusterModel.contextName = loadConfigFromFileSync(clusterModel.kubeConfigPath).config.getCurrentContext(); + clusterModel.contextName = loadConfigFromString(readFileSync(absPath)).config.getCurrentContext(); delete clusterModel.kubeConfig; } catch (error) { @@ -71,7 +74,7 @@ export default { 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)); + const fileData = fse.readFileSync(joinPaths(userDataPath, iconPath)); clusterModel.preferences.icon = `data:;base64,${fileData.toString("base64")}`; } else { diff --git a/src/migrations/cluster-store/5.0.0-beta.10.ts b/src/migrations/cluster-store/5.0.0-beta.10.ts index 9172969a85..97b94dc7f7 100644 --- a/src/migrations/cluster-store/5.0.0-beta.10.ts +++ b/src/migrations/cluster-store/5.0.0-beta.10.ts @@ -3,13 +3,13 @@ * 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"; +import joinPathsInjectable from "../../common/path/join-paths.injectable"; interface Pre500WorkspaceStoreModel { workspaces: { @@ -24,9 +24,10 @@ export default { const di = getLegacyGlobalDiForExtensionApi(); const userDataPath = di.inject(directoryForUserDataInjectable); + const joinPaths = di.inject(joinPathsInjectable); try { - const workspaceData: Pre500WorkspaceStoreModel = fse.readJsonSync(path.join(userDataPath, "lens-workspace-store.json")); + const workspaceData: Pre500WorkspaceStoreModel = fse.readJsonSync(joinPaths(userDataPath, "lens-workspace-store.json")); const workspaces = new Map(); // mapping from WorkspaceId to name for (const { id, name } of workspaceData.workspaces) { diff --git a/src/migrations/cluster-store/5.0.0-beta.13.ts b/src/migrations/cluster-store/5.0.0-beta.13.ts index dc85a04718..87d88b143a 100644 --- a/src/migrations/cluster-store/5.0.0-beta.13.ts +++ b/src/migrations/cluster-store/5.0.0-beta.13.ts @@ -7,12 +7,11 @@ import type { ClusterModel, ClusterPreferences, ClusterPrometheusPreferences } f 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 directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import { isDefined } from "../../common/utils"; +import joinPathsInjectable from "../../common/path/join-paths.injectable"; function mergePrometheusPreferences(left: ClusterPrometheusPreferences, right: ClusterPrometheusPreferences): ClusterPrometheusPreferences { if (left.prometheus && left.prometheusProvider) { @@ -79,29 +78,29 @@ function mergeClusterModel(prev: ClusterModel, right: Omit): }; } -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 joinPaths = di.inject(joinPathsInjectable); - const folder = path.resolve(userDataPath, "lens-local-storage"); + const moveStorageFolder = ({ folder, newId, oldId }: { folder: string; newId: string; oldId: string }): void => { + 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); + } + } + }; + + const folder = joinPaths(userDataPath, "lens-local-storage"); const oldClusters: ClusterModel[] = store.get("clusters") ?? []; const clusters = new Map(); 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 95a4c616d4..771a37fa36 100644 --- a/src/migrations/hotbar-store/5.0.0-beta.10.ts +++ b/src/migrations/hotbar-store/5.0.0-beta.10.ts @@ -5,7 +5,6 @@ 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"; @@ -17,6 +16,7 @@ import { getLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-glo 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"; +import joinPathsInjectable from "../../common/path/join-paths.injectable"; interface Pre500WorkspaceStoreModel { workspaces: { @@ -40,6 +40,7 @@ export default { const di = getLegacyGlobalDiForExtensionApi(); const userDataPath = di.inject(directoryForUserDataInjectable); + const joinPaths = di.inject(joinPathsInjectable); // Hotbars might be empty, if some of the previous migrations weren't run if (hotbars.length === 0) { @@ -56,8 +57,8 @@ export default { } 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 workspaceStoreData: Pre500WorkspaceStoreModel = fse.readJsonSync(joinPaths(userDataPath, "lens-workspace-store.json")); + const { clusters = [] }: ClusterStoreModel = fse.readJSONSync(joinPaths(userDataPath, "lens-cluster-store.json")); const workspaceHotbars = new Map(); // mapping from WorkspaceId to HotBar for (const { id, name } of workspaceStoreData.workspaces) { diff --git a/src/migrations/user-store/5.0.3-beta.1.ts b/src/migrations/user-store/5.0.3-beta.1.ts index 40f93312cf..cf18ccd3e4 100644 --- a/src/migrations/user-store/5.0.3-beta.1.ts +++ b/src/migrations/user-store/5.0.3-beta.1.ts @@ -4,18 +4,18 @@ */ 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 { isErrnoException } 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"; +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 joinPathsInjectable from "../../common/path/join-paths.injectable"; +import isLogicalChildPathInjectable from "../../common/path/is-logical-child-path.injectable"; +import getDirnameOfPathInjectable from "../../common/path/get-dirname.injectable"; export default { version: "5.0.3-beta.1", @@ -27,18 +27,21 @@ export default { const userDataPath = di.inject(directoryForUserDataInjectable); const kubeConfigsPath = di.inject(directoryForKubeConfigsInjectable); + const joinPaths = di.inject(joinPathsInjectable); + const isLogicalChildPath = di.inject(isLogicalChildPathInjectable); + const getDirnameOfPath = di.inject(getDirnameOfPathInjectable); - const { clusters = [] }: ClusterStoreModel = JSON.parse(readFileSync(path.resolve(userDataPath, "lens-cluster-store.json"), "utf-8")) ?? {}; - const extensionDataDir = path.resolve(userDataPath, "extension_data"); + const { clusters = [] }: ClusterStoreModel = JSON.parse(readFileSync(joinPaths(userDataPath, "lens-cluster-store.json"), "utf-8")) ?? {}; + const extensionDataDir = joinPaths(userDataPath, "extension_data"); const syncPaths = new Set(syncKubeconfigEntries.map(s => s.filePath)); - syncPaths.add(path.join(os.homedir(), ".kube")); + syncPaths.add(joinPaths(os.homedir(), ".kube")); for (const cluster of clusters) { if (!cluster.kubeConfigPath) { continue; } - const dirOfKubeconfig = path.dirname(cluster.kubeConfigPath); + const dirOfKubeconfig = getDirnameOfPath(cluster.kubeConfigPath); if (dirOfKubeconfig === kubeConfigsPath) { migrationLog(`Skipping ${cluster.id} because kubeConfigPath is under the stored KubeConfig folder`); diff --git a/src/renderer/components/+add-cluster/add-cluster.tsx b/src/renderer/components/+add-cluster/add-cluster.tsx index 503632b51f..1d062dbd31 100644 --- a/src/renderer/components/+add-cluster/add-cluster.tsx +++ b/src/renderer/components/+add-cluster/add-cluster.tsx @@ -10,7 +10,6 @@ import fse from "fs-extra"; import { debounce } from "lodash"; import { action, computed, makeObservable, observable } from "mobx"; import { observer } from "mobx-react"; -import path from "path"; import React from "react"; import * as uuid from "uuid"; import { appEventBus } from "../../../common/app-event-bus/event-bus"; @@ -25,6 +24,8 @@ 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 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 { GetDirnameOfPath } from "../../../common/path/get-dirname.injectable"; +import getDirnameOfPathInjectable from "../../../common/path/get-dirname.injectable"; interface Option { config: KubeConfig; @@ -34,6 +35,7 @@ interface Option { interface Dependencies { getCustomKubeConfigDirectory: (directoryName: string) => string; navigateToCatalog: NavigateToCatalog; + getDirnameOfPath: GetDirnameOfPath; } function getContexts(config: KubeConfig): Map { @@ -90,7 +92,7 @@ class NonInjectedAddCluster extends React.Component { try { const absPath = this.props.getCustomKubeConfigDirectory(uuid.v4()); - await fse.ensureDir(path.dirname(absPath)); + await fse.ensureDir(this.props.getDirnameOfPath(absPath)); await fse.writeFile(absPath, this.customConfig.trim(), { encoding: "utf-8", mode: 0o600 }); Notifications.ok(`Successfully added ${this.kubeContexts.size} new cluster(s)`); @@ -155,10 +157,8 @@ class NonInjectedAddCluster extends React.Component { export const AddCluster = withInjectables(NonInjectedAddCluster, { getProps: (di) => ({ - getCustomKubeConfigDirectory: di.inject( - getCustomKubeConfigDirectoryInjectable, - ), - + getCustomKubeConfigDirectory: di.inject(getCustomKubeConfigDirectoryInjectable), navigateToCatalog: di.inject(navigateToCatalogInjectable), + getDirnameOfPath: di.inject(getDirnameOfPathInjectable), }), }); diff --git a/src/renderer/components/+extensions/__tests__/extensions.test.tsx b/src/renderer/components/+extensions/__tests__/extensions.test.tsx index 860dbc8ff2..52843d8b3d 100644 --- a/src/renderer/components/+extensions/__tests__/extensions.test.tsx +++ b/src/renderer/components/+extensions/__tests__/extensions.test.tsx @@ -18,8 +18,8 @@ import extensionDiscoveryInjectable from "../../../../extensions/extension-disco import directoryForUserDataInjectable from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import directoryForDownloadsInjectable from "../../../../common/app-paths/directory-for-downloads/directory-for-downloads.injectable"; import assert from "assert"; -import type { InstallFromInput } from "../install-from-input/install-from-input"; -import installFromInputInjectable from "../install-from-input/install-from-input.injectable"; +import type { InstallExtensionFromInput } from "../install-extension-from-input.injectable"; +import installExtensionFromInputInjectable from "../install-extension-from-input.injectable"; import type { ExtensionInstallationStateStore } from "../../../../extensions/extension-installation-state-store/extension-installation-state-store"; import extensionInstallationStateStoreInjectable from "../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; import { observable, when } from "mobx"; @@ -46,7 +46,7 @@ jest.mock("../../../../common/utils/tar"); describe("Extensions", () => { let extensionLoader: ExtensionLoader; let extensionDiscovery: ExtensionDiscovery; - let installFromInput: jest.MockedFunction; + let installExtensionFromInput: jest.MockedFunction; let extensionInstallationStateStore: ExtensionInstallationStateStore; let render: DiRender; let deleteFileMock: jest.MockedFunction; @@ -59,8 +59,8 @@ describe("Extensions", () => { render = renderFor(di); - installFromInput = jest.fn(); - di.override(installFromInputInjectable, () => installFromInput); + installExtensionFromInput = jest.fn(); + di.override(installExtensionFromInputInjectable, () => installExtensionFromInput); deleteFileMock = jest.fn(); di.override(deleteFileInjectable, () => deleteFileMock); @@ -126,7 +126,7 @@ describe("Extensions", () => { const resolveInstall = observable.box(false); deleteFileMock.mockReturnValue(Promise.resolve()); - installFromInput.mockImplementation(async (input) => { + installExtensionFromInput.mockImplementation(async (input) => { expect(input).toBe("https://test.extensionurl/package.tgz"); const clear = extensionInstallationStateStore.startPreInstall(); diff --git a/src/renderer/components/+extensions/attempt-install-by-info.injectable.tsx b/src/renderer/components/+extensions/attempt-install-by-info.injectable.tsx index dd320407a5..36f6b2afc4 100644 --- a/src/renderer/components/+extensions/attempt-install-by-info.injectable.tsx +++ b/src/renderer/components/+extensions/attempt-install-by-info.injectable.tsx @@ -2,22 +2,18 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { ExtendableDisposer } from "../../../common/utils"; import { downloadFile, downloadJson } from "../../../common/utils"; import { Notifications } from "../notifications"; import React from "react"; -import path from "path"; import { SemVer } from "semver"; import URLParse from "url-parse"; -import type { InstallRequest } from "./attempt-install/install-request"; -import type { ExtensionInstallationStateStore } from "../../../extensions/extension-installation-state-store/extension-installation-state-store"; -import type { Confirm } from "../confirm-dialog/confirm.injectable"; import { getInjectable } from "@ogre-tools/injectable"; import attemptInstallInjectable from "./attempt-install/attempt-install.injectable"; import getBaseRegistryUrlInjectable from "./get-base-registry-url/get-base-registry-url.injectable"; import extensionInstallationStateStoreInjectable from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; import confirmInjectable from "../confirm-dialog/confirm.injectable"; import { reduce } from "lodash"; +import getBasenameOfPathInjectable from "../../../common/path/get-basename.injectable"; export interface ExtensionInfo { name: string; @@ -27,126 +23,113 @@ export interface ExtensionInfo { export type AttemptInstallByInfo = (info: ExtensionInfo) => Promise; -interface Dependencies { - attemptInstall: (request: InstallRequest, d: ExtendableDisposer) => Promise; - getBaseRegistryUrl: () => Promise; - extensionInstallationStateStore: ExtensionInstallationStateStore; - confirm: Confirm; -} +const attemptInstallByInfoInjectable = getInjectable({ + id: "attempt-install-by-info", + instantiate: (di): AttemptInstallByInfo => { + const attemptInstall = di.inject(attemptInstallInjectable); + const getBaseRegistryUrl = di.inject(getBaseRegistryUrlInjectable); + const extensionInstallationStateStore = di.inject(extensionInstallationStateStoreInjectable); + const confirm = di.inject(confirmInjectable); + const getBasenameOfPath = di.inject(getBasenameOfPathInjectable); -const attemptInstallByInfo = ({ - attemptInstall, - getBaseRegistryUrl, - extensionInstallationStateStore, - confirm, -}: Dependencies): AttemptInstallByInfo => ( - async (info) => { - const { name, version, requireConfirmation = false } = info; - const disposer = extensionInstallationStateStore.startPreInstall(); - const baseUrl = await getBaseRegistryUrl(); - const registryUrl = new URLParse(baseUrl).set("pathname", name).toString(); - let json: any; - let finalVersion = version; + return async (info) => { + const { name, version, requireConfirmation = false } = info; + const disposer = extensionInstallationStateStore.startPreInstall(); + const baseUrl = await getBaseRegistryUrl(); + const registryUrl = new URLParse(baseUrl).set("pathname", name).toString(); + let json: any; + let finalVersion = version; - try { - json = await downloadJson({ url: registryUrl }).promise; + try { + json = await downloadJson({ url: registryUrl }).promise; - if (!json || json.error || typeof json.versions !== "object" || !json.versions) { - const message = json?.error ? `: ${json.error}` : ""; + if (!json || json.error || typeof json.versions !== "object" || !json.versions) { + const message = json?.error ? `: ${json.error}` : ""; - Notifications.error(`Failed to get registry information for that extension${message}`); - - return disposer(); - } - } catch (error) { - if (error instanceof SyntaxError) { - // assume invalid JSON - console.warn("Set registry has invalid json", { url: baseUrl }, error); - Notifications.error("Failed to get valid registry information for that extension. Registry did not return valid JSON"); - } else { - console.error("Failed to download registry information", error); - Notifications.error(`Failed to get valid registry information for that extension. ${error}`); - } - - return disposer(); - } - - if (version) { - if (!json.versions[version]) { - if (json["dist-tags"][version]) { - finalVersion = json["dist-tags"][version]; - } else { - Notifications.error(( -

- {"The "} - {name} - {" extension does not have a version or tag "} - {version} - . -

- )); + Notifications.error(`Failed to get registry information for that extension${message}`); return disposer(); } + } catch (error) { + if (error instanceof SyntaxError) { + // assume invalid JSON + console.warn("Set registry has invalid json", { url: baseUrl }, error); + Notifications.error("Failed to get valid registry information for that extension. Registry did not return valid JSON"); + } else { + console.error("Failed to download registry information", error); + Notifications.error(`Failed to get valid registry information for that extension. ${error}`); + } + + return disposer(); } - } else { - const versions = Object.keys(json.versions) - .map(version => new SemVer(version, { loose: true })) + + if (version) { + if (!json.versions[version]) { + if (json["dist-tags"][version]) { + finalVersion = json["dist-tags"][version]; + } else { + Notifications.error(( +

+ {"The "} + {name} + {" extension does not have a version or tag "} + {version} + . +

+ )); + + return disposer(); + } + } + } else { + const versions = Object.keys(json.versions) + .map(version => new SemVer(version, { loose: true })) // ignore pre-releases for auto picking the version - .filter(version => version.prerelease.length === 0); + .filter(version => version.prerelease.length === 0); - const latestVersion = reduce(versions, (prev, curr) => prev.compareMain(curr) === -1 ? curr : prev); + const latestVersion = reduce(versions, (prev, curr) => prev.compareMain(curr) === -1 ? curr : prev); - if (!latestVersion) { - console.error("No versions supplied for that extension", { name }); - Notifications.error(`No versions found for ${name}`); + if (!latestVersion) { + console.error("No versions supplied for that extension", { name }); + Notifications.error(`No versions found for ${name}`); - return disposer(); + return disposer(); + } + + finalVersion = latestVersion.format(); } - finalVersion = latestVersion.format(); - } + if (requireConfirmation) { + const proceed = await confirm({ + message: ( +

+ Are you sure you want to install + {" "} + + {name} + @ + {finalVersion} + + ? +

+ ), + labelCancel: "Cancel", + labelOk: "Install", + }); - if (requireConfirmation) { - const proceed = await confirm({ - message: ( -

- Are you sure you want to install - {" "} - - {name} - @ - {finalVersion} - - ? -

- ), - labelCancel: "Cancel", - labelOk: "Install", - }); - - if (!proceed) { - return disposer(); + if (!proceed) { + return disposer(); + } } - } - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const url = json.versions[finalVersion!].dist.tarball; - const fileName = path.basename(url); - const { promise: dataP } = downloadFile({ url, timeout: 10 * 60 * 1000 }); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const url = json.versions[finalVersion!].dist.tarball; + const fileName = getBasenameOfPath(url); + const { promise: dataP } = downloadFile({ url, timeout: 10 * 60 * 1000 }); - return attemptInstall({ fileName, dataP }, disposer); - } -); - -const attemptInstallByInfoInjectable = getInjectable({ - id: "attempt-install-by-info", - instantiate: (di) => attemptInstallByInfo({ - attemptInstall: di.inject(attemptInstallInjectable), - getBaseRegistryUrl: di.inject(getBaseRegistryUrlInjectable), - extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable), - confirm: di.inject(confirmInjectable), - }), + return attemptInstall({ fileName, dataP }, disposer); + }; + }, }); export default attemptInstallByInfoInjectable; diff --git a/src/renderer/components/+extensions/attempt-install/attempt-install.tsx b/src/renderer/components/+extensions/attempt-install/attempt-install.tsx index 9d83862357..de8c968201 100644 --- a/src/renderer/components/+extensions/attempt-install/attempt-install.tsx +++ b/src/renderer/components/+extensions/attempt-install/attempt-install.tsx @@ -2,12 +2,8 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { - Disposer, - ExtendableDisposer } from "../../../../common/utils"; -import { - disposer, -} from "../../../../common/utils"; +import type { ExtendableDisposer } from "../../../../common/utils"; +import { disposer } from "../../../../common/utils"; import { Notifications } from "../../notifications"; import { Button } from "../../button"; import type { ExtensionLoader } from "../../../../extensions/extension-loader"; @@ -15,29 +11,19 @@ import type { LensExtensionId } from "../../../../extensions/lens-extension"; import React from "react"; import { remove as removeDir } from "fs-extra"; import { shell } from "electron"; -import type { InstallRequestValidated } from "./create-temp-files-and-validate/create-temp-files-and-validate"; import type { InstallRequest } from "./install-request"; -import type { - ExtensionInstallationStateStore } from "../../../../extensions/extension-installation-state-store/extension-installation-state-store"; -import { - ExtensionInstallationState, -} from "../../../../extensions/extension-installation-state-store/extension-installation-state-store"; +import type { ExtensionInstallationStateStore } from "../../../../extensions/extension-installation-state-store/extension-installation-state-store"; +import { ExtensionInstallationState } from "../../../../extensions/extension-installation-state-store/extension-installation-state-store"; +import type { UnpackExtension } from "./unpack-extension/unpack-extension.injectable"; +import type { CreateTempFilesAndValidate } from "./create-temp-files-and-validate/create-temp-files-and-validate.injectable"; +import type { GetExtensionDestFolder } from "./get-extension-dest-folder/get-extension-dest-folder.injectable"; interface Dependencies { extensionLoader: ExtensionLoader; uninstallExtension: (id: LensExtensionId) => Promise; - - unpackExtension: ( - request: InstallRequestValidated, - disposeDownloading: Disposer, - ) => Promise; - - createTempFilesAndValidate: ( - installRequest: InstallRequest, - ) => Promise; - - getExtensionDestFolder: (name: string) => string; - + unpackExtension: UnpackExtension; + createTempFilesAndValidate: CreateTempFilesAndValidate; + getExtensionDestFolder: GetExtensionDestFolder; extensionInstallationStateStore: ExtensionInstallationStateStore; } @@ -51,6 +37,8 @@ export const attemptInstall = extensionInstallationStateStore, }: Dependencies) => async (request: InstallRequest, d?: ExtendableDisposer): Promise => { + console.log("Attempting to install extension"); + const dispose = disposer( extensionInstallationStateStore.startPreInstall(), d, diff --git a/src/renderer/components/+extensions/attempt-install/create-temp-files-and-validate/create-temp-files-and-validate.injectable.tsx b/src/renderer/components/+extensions/attempt-install/create-temp-files-and-validate/create-temp-files-and-validate.injectable.tsx index d0d71e6fbe..951c45d246 100644 --- a/src/renderer/components/+extensions/attempt-install/create-temp-files-and-validate/create-temp-files-and-validate.injectable.tsx +++ b/src/renderer/components/+extensions/attempt-install/create-temp-files-and-validate/create-temp-files-and-validate.injectable.tsx @@ -3,16 +3,102 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import { createTempFilesAndValidate } from "./create-temp-files-and-validate"; import extensionDiscoveryInjectable from "../../../../../extensions/extension-discovery/extension-discovery.injectable"; +import React from "react"; +import type { LensExtensionId, LensExtensionManifest } from "../../../../../extensions/lens-extension"; +import { getMessageFromError } from "../../get-message-from-error/get-message-from-error"; +import type { InstallRequest } from "../install-request"; +import { validatePackage } from "../validate-package/validate-package"; +import joinPathsInjectable from "../../../../../common/path/join-paths.injectable"; +import tempDirectoryPathInjectable from "../../../../../common/os/temp-directory-path.injectable"; +import ensureDirInjectable from "../../../../../common/fs/ensure-dir.injectable"; +import writeFileInjectable from "../../../../../common/fs/write-file.injectable"; +import loggerInjectable from "../../../../../common/logger.injectable"; +import showErrorNotificationInjectable from "../../../notifications/show-error-notification.injectable"; + +export interface InstallRequestValidated { + fileName: string; + data: Buffer; + id: LensExtensionId; + manifest: LensExtensionManifest; + tempFile: string; // temp system path to packed extension for unpacking +} + +export type CreateTempFilesAndValidate = (req: InstallRequest) => Promise; const createTempFilesAndValidateInjectable = getInjectable({ id: "create-temp-files-and-validate", - instantiate: (di) => - createTempFilesAndValidate({ - extensionDiscovery: di.inject(extensionDiscoveryInjectable), - }), + instantiate: (di) => { + const extensionDiscovery = di.inject(extensionDiscoveryInjectable); + const joinPaths = di.inject(joinPathsInjectable); + const tempDirectoryPath = di.inject(tempDirectoryPathInjectable); + const ensureDir = di.inject(ensureDirInjectable); + const writeFile = di.inject(writeFileInjectable); + const logger = di.inject(loggerInjectable); + const showErrorNotification = di.inject(showErrorNotificationInjectable); + + const baseTempExtensionsDirectory = joinPaths(tempDirectoryPath, "lens-extensions"); + const getExtensionPackageTemp = (fileName: string) => joinPaths(baseTempExtensionsDirectory, fileName); + + return async ({ + fileName, + dataP, + }: InstallRequest): Promise => { + // copy files to temp + await ensureDir(baseTempExtensionsDirectory); + + // validate packages + const tempFile = getExtensionPackageTemp(fileName); + + try { + const data = await dataP; + + if (!data) { + return null; + } + + await writeFile(tempFile, data); + logger.info("validating package", tempFile); + const manifest = await validatePackage(tempFile); + const id = joinPaths( + extensionDiscovery.nodeModulesPath, + manifest.name, + "package.json", + ); + + return { + fileName, + data, + manifest, + tempFile, + id, + }; + } catch (error) { + const message = getMessageFromError(error); + + logger.info( + `[EXTENSION-INSTALLATION]: installing ${fileName} has failed: ${message}`, + { error }, + ); + showErrorNotification( +
+

+ {"Installing "} + {fileName} + {" has failed, skipping."} +

+

+ {"Reason: "} + {message} +

+
, + ); + } + + return null; + }; + }, }); export default createTempFilesAndValidateInjectable; diff --git a/src/renderer/components/+extensions/attempt-install/create-temp-files-and-validate/create-temp-files-and-validate.tsx b/src/renderer/components/+extensions/attempt-install/create-temp-files-and-validate/create-temp-files-and-validate.tsx deleted file mode 100644 index b3df89ca92..0000000000 --- a/src/renderer/components/+extensions/attempt-install/create-temp-files-and-validate/create-temp-files-and-validate.tsx +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { validatePackage } from "../validate-package/validate-package"; -import type { ExtensionDiscovery } from "../../../../../extensions/extension-discovery/extension-discovery"; -import { getMessageFromError } from "../../get-message-from-error/get-message-from-error"; -import logger from "../../../../../main/logger"; -import { Notifications } from "../../../notifications"; -import path from "path"; -import fse from "fs-extra"; -import React from "react"; -import os from "os"; -import type { - LensExtensionId, - LensExtensionManifest, -} from "../../../../../extensions/lens-extension"; -import type { InstallRequest } from "../install-request"; - -export interface InstallRequestValidated { - fileName: string; - data: Buffer; - id: LensExtensionId; - manifest: LensExtensionManifest; - tempFile: string; // temp system path to packed extension for unpacking -} - -interface Dependencies { - extensionDiscovery: ExtensionDiscovery; -} - -export const createTempFilesAndValidate = - ({ extensionDiscovery }: Dependencies) => - async ({ - fileName, - dataP, - }: InstallRequest): Promise => { - // copy files to temp - await fse.ensureDir(getExtensionPackageTemp()); - - // validate packages - const tempFile = getExtensionPackageTemp(fileName); - - try { - const data = await dataP; - - if (!data) { - return null; - } - - await fse.writeFile(tempFile, data); - const manifest = await validatePackage(tempFile); - const id = path.join( - extensionDiscovery.nodeModulesPath, - manifest.name, - "package.json", - ); - - return { - fileName, - data, - manifest, - tempFile, - id, - }; - } catch (error) { - const message = getMessageFromError(error); - - logger.info( - `[EXTENSION-INSTALLATION]: installing ${fileName} has failed: ${message}`, - { error }, - ); - Notifications.error( -
-

- {"Installing "} - {fileName} - {" has failed, skipping."} -

-

- {"Reason: "} - {message} -

-
, - ); - } - - return null; - }; - - -function getExtensionPackageTemp(fileName = "") { - return path.join(os.tmpdir(), "lens-extensions", fileName); -} diff --git a/src/renderer/components/+extensions/attempt-install/get-extension-dest-folder/get-extension-dest-folder.injectable.ts b/src/renderer/components/+extensions/attempt-install/get-extension-dest-folder/get-extension-dest-folder.injectable.ts index 3062439e1b..2c46b595a4 100644 --- a/src/renderer/components/+extensions/attempt-install/get-extension-dest-folder/get-extension-dest-folder.injectable.ts +++ b/src/renderer/components/+extensions/attempt-install/get-extension-dest-folder/get-extension-dest-folder.injectable.ts @@ -3,18 +3,21 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; - +import joinPathsInjectable from "../../../../../common/path/join-paths.injectable"; import extensionDiscoveryInjectable from "../../../../../extensions/extension-discovery/extension-discovery.injectable"; +import { sanitizeExtensionName } from "../../../../../extensions/lens-extension"; -import { getExtensionDestFolder } from "./get-extension-dest-folder"; +export type GetExtensionDestFolder = (extensionName: string) => string; const getExtensionDestFolderInjectable = getInjectable({ id: "get-extension-dest-folder", - instantiate: (di) => - getExtensionDestFolder({ - extensionDiscovery: di.inject(extensionDiscoveryInjectable), - }), + instantiate: (di): GetExtensionDestFolder => { + const extensionDiscovery = di.inject(extensionDiscoveryInjectable); + const joinPaths = di.inject(joinPathsInjectable); + + return (name) => joinPaths(extensionDiscovery.localFolderPath, sanitizeExtensionName(name)); + }, }); export default getExtensionDestFolderInjectable; diff --git a/src/renderer/components/+extensions/attempt-install/get-extension-dest-folder/get-extension-dest-folder.ts b/src/renderer/components/+extensions/attempt-install/get-extension-dest-folder/get-extension-dest-folder.ts deleted file mode 100644 index 52e06d2cca..0000000000 --- a/src/renderer/components/+extensions/attempt-install/get-extension-dest-folder/get-extension-dest-folder.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import type { ExtensionDiscovery } from "../../../../../extensions/extension-discovery/extension-discovery"; -import { sanitizeExtensionName } from "../../../../../extensions/lens-extension"; -import path from "path"; - -interface Dependencies { - extensionDiscovery: ExtensionDiscovery; -} - -export const getExtensionDestFolder = - ({ extensionDiscovery }: Dependencies) => - (name: string) => - path.join(extensionDiscovery.localFolderPath, sanitizeExtensionName(name)); diff --git a/src/renderer/components/+extensions/attempt-install/unpack-extension/unpack-extension.injectable.tsx b/src/renderer/components/+extensions/attempt-install/unpack-extension/unpack-extension.injectable.tsx index ebbc3639c5..9a10062f7e 100644 --- a/src/renderer/components/+extensions/attempt-install/unpack-extension/unpack-extension.injectable.tsx +++ b/src/renderer/components/+extensions/attempt-install/unpack-extension/unpack-extension.injectable.tsx @@ -2,23 +2,129 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ +import React from "react"; import { getInjectable } from "@ogre-tools/injectable"; -import { unpackExtension } from "./unpack-extension"; import extensionLoaderInjectable from "../../../../../extensions/extension-loader/extension-loader.injectable"; -import getExtensionDestFolderInjectable - from "../get-extension-dest-folder/get-extension-dest-folder.injectable"; -import extensionInstallationStateStoreInjectable - from "../../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; +import getExtensionDestFolderInjectable from "../get-extension-dest-folder/get-extension-dest-folder.injectable"; +import extensionInstallationStateStoreInjectable from "../../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; +import type { InstallRequestValidated } from "../create-temp-files-and-validate/create-temp-files-and-validate.injectable"; +import type { Disposer } from "../../../../utils"; +import { noop } from "../../../../utils"; +import { extensionDisplayName } from "../../../../../extensions/lens-extension"; +import joinPathsInjectable from "../../../../../common/path/join-paths.injectable"; +import loggerInjectable from "../../../../../common/logger.injectable"; +import { when } from "mobx"; +import { getMessageFromError } from "../../get-message-from-error/get-message-from-error"; +import showSuccessNotificationInjectable from "../../../notifications/show-success-notification.injectable"; +import showErrorNotificationInjectable from "../../../notifications/show-error-notification.injectable"; +import getDirnameOfPathInjectable from "../../../../../common/path/get-dirname.injectable"; +import getBasenameOfPathInjectable from "../../../../../common/path/get-basename.injectable"; +import extractTarInjectable from "../../../../../common/fs/extract-tar.injectable"; +import ensureDirInjectable from "../../../../../common/fs/ensure-dir.injectable"; +import removePathInjectable from "../../../../../common/fs/remove-path.injectable"; +import deleteFileInjectable from "../../../../../common/fs/delete-file.injectable"; +import readDirectoryInjectable from "../../../../../common/fs/read-directory.injectable"; +import moveInjectable from "../../../../../common/fs/move.injectable"; + +export type UnpackExtension = (request: InstallRequestValidated, disposeDownloading?: Disposer) => Promise; const unpackExtensionInjectable = getInjectable({ id: "unpack-extension", - instantiate: (di) => - unpackExtension({ - extensionLoader: di.inject(extensionLoaderInjectable), - getExtensionDestFolder: di.inject(getExtensionDestFolderInjectable), - extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable), - }), + instantiate: (di): UnpackExtension => { + const extensionLoader = di.inject(extensionLoaderInjectable); + const getExtensionDestFolder = di.inject(getExtensionDestFolderInjectable); + const extensionInstallationStateStore = di.inject(extensionInstallationStateStoreInjectable); + const joinPaths = di.inject(joinPathsInjectable); + const getDirnameOfPath = di.inject(getDirnameOfPathInjectable); + const getBasenameOfPath = di.inject(getBasenameOfPathInjectable); + const logger = di.inject(loggerInjectable); + const showOkNotification = di.inject(showSuccessNotificationInjectable); + const showErrorNotification = di.inject(showErrorNotificationInjectable); + const extractTar = di.inject(extractTarInjectable); + const ensureDir = di.inject(ensureDirInjectable); + const removePath = di.inject(removePathInjectable); + const deleteFile = di.inject(deleteFileInjectable); + const readDirectory = di.inject(readDirectoryInjectable); + const move = di.inject(moveInjectable); + + return async (request, disposeDownloading) => { + const { + id, + fileName, + tempFile, + manifest: { name, version }, + } = request; + + extensionInstallationStateStore.setInstalling(id); + disposeDownloading?.(); + + const displayName = extensionDisplayName(name, version); + const extensionFolder = getExtensionDestFolder(name); + const unpackingTempFolder = joinPaths( + getDirnameOfPath(tempFile), + `${getBasenameOfPath(tempFile)}-unpacked`, + ); + + logger.info(`Unpacking extension ${displayName}`, { fileName, tempFile }); + + try { + // extract to temp folder first + await removePath(unpackingTempFolder).catch(noop); + await ensureDir(unpackingTempFolder); + await extractTar(tempFile, { cwd: unpackingTempFolder }); + + // move contents to extensions folder + const unpackedFiles = await readDirectory(unpackingTempFolder); + let unpackedRootFolder = unpackingTempFolder; + + if (unpackedFiles.length === 1) { + // check if %extension.tgz was packed with single top folder, + // e.g. "npm pack %ext_name" downloads file with "package" root folder within tarball + unpackedRootFolder = joinPaths(unpackingTempFolder, unpackedFiles[0]); + } + + await ensureDir(extensionFolder); + await move(unpackedRootFolder, extensionFolder, { overwrite: true }); + + // wait for the loader has actually install it + await when(() => extensionLoader.userExtensions.has(id)); + + // Enable installed extensions by default. + extensionLoader.setIsEnabled(id, true); + + showOkNotification( +

+ {"Extension "} + {displayName} + {" successfully installed!"} +

, + ); + } catch (error) { + const message = getMessageFromError(error); + + logger.info( + `[EXTENSION-INSTALLATION]: installing ${request.fileName} has failed: ${message}`, + { error }, + ); + showErrorNotification( +

+ {"Installing extension "} + {displayName} + {" has failed: "} + {message} +

, + ); + } finally { + // Remove install state once finished + extensionInstallationStateStore.clearInstalling(id); + + // clean up + removePath(unpackingTempFolder).catch(noop); + deleteFile(tempFile).catch(noop); + } + }; + }, }); export default unpackExtensionInjectable; diff --git a/src/renderer/components/+extensions/attempt-install/unpack-extension/unpack-extension.tsx b/src/renderer/components/+extensions/attempt-install/unpack-extension/unpack-extension.tsx deleted file mode 100644 index 44b02f6351..0000000000 --- a/src/renderer/components/+extensions/attempt-install/unpack-extension/unpack-extension.tsx +++ /dev/null @@ -1,106 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import type { InstallRequestValidated } from "../create-temp-files-and-validate/create-temp-files-and-validate"; -import type { Disposer } from "../../../../../common/utils"; -import { extractTar, noop } from "../../../../../common/utils"; -import { extensionDisplayName } from "../../../../../extensions/lens-extension"; -import logger from "../../../../../main/logger"; -import type { ExtensionLoader } from "../../../../../extensions/extension-loader"; -import { Notifications } from "../../../notifications"; -import { getMessageFromError } from "../../get-message-from-error/get-message-from-error"; -import path from "path"; -import fse from "fs-extra"; -import { when } from "mobx"; -import React from "react"; -import type { ExtensionInstallationStateStore } from "../../../../../extensions/extension-installation-state-store/extension-installation-state-store"; - -interface Dependencies { - extensionLoader: ExtensionLoader; - getExtensionDestFolder: (name: string) => string; - extensionInstallationStateStore: ExtensionInstallationStateStore; -} - -export const unpackExtension = - ({ - extensionLoader, - getExtensionDestFolder, - extensionInstallationStateStore, - }: Dependencies) => - async (request: InstallRequestValidated, disposeDownloading?: Disposer) => { - const { - id, - fileName, - tempFile, - manifest: { name, version }, - } = request; - - extensionInstallationStateStore.setInstalling(id); - disposeDownloading?.(); - - const displayName = extensionDisplayName(name, version); - const extensionFolder = getExtensionDestFolder(name); - const unpackingTempFolder = path.join( - path.dirname(tempFile), - `${path.basename(tempFile)}-unpacked`, - ); - - logger.info(`Unpacking extension ${displayName}`, { fileName, tempFile }); - - try { - // extract to temp folder first - await fse.remove(unpackingTempFolder).catch(noop); - await fse.ensureDir(unpackingTempFolder); - await extractTar(tempFile, { cwd: unpackingTempFolder }); - - // move contents to extensions folder - const unpackedFiles = await fse.readdir(unpackingTempFolder); - let unpackedRootFolder = unpackingTempFolder; - - if (unpackedFiles.length === 1) { - // check if %extension.tgz was packed with single top folder, - // e.g. "npm pack %ext_name" downloads file with "package" root folder within tarball - unpackedRootFolder = path.join(unpackingTempFolder, unpackedFiles[0]); - } - - await fse.ensureDir(extensionFolder); - await fse.move(unpackedRootFolder, extensionFolder, { overwrite: true }); - - // wait for the loader has actually install it - await when(() => extensionLoader.userExtensions.has(id)); - - // Enable installed extensions by default. - extensionLoader.setIsEnabled(id, true); - - Notifications.ok( -

- {"Extension "} - {displayName} - {" successfully installed!"} -

, - ); - } catch (error) { - const message = getMessageFromError(error); - - logger.info( - `[EXTENSION-INSTALLATION]: installing ${request.fileName} has failed: ${message}`, - { error }, - ); - Notifications.error( -

- {"Installing extension "} - {displayName} - {" has failed: "} - {message} -

, - ); - } finally { - // Remove install state once finished - extensionInstallationStateStore.clearInstalling(id); - - // clean up - fse.remove(unpackingTempFolder).catch(noop); - fse.unlink(tempFile).catch(noop); - } - }; diff --git a/src/renderer/components/+extensions/attempt-installs/attempt-installs.injectable.ts b/src/renderer/components/+extensions/attempt-installs/attempt-installs.injectable.ts index 407668989f..78939ef264 100644 --- a/src/renderer/components/+extensions/attempt-installs/attempt-installs.injectable.ts +++ b/src/renderer/components/+extensions/attempt-installs/attempt-installs.injectable.ts @@ -3,16 +3,29 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import { attemptInstalls } from "./attempt-installs"; +import getBasenameOfPathInjectable from "../../../../common/path/get-basename.injectable"; import attemptInstallInjectable from "../attempt-install/attempt-install.injectable"; +import readFileNotifyInjectable from "../read-file-notify/read-file-notify.injectable"; + +export type AttemptInstalls = (filePaths: string[]) => Promise; const attemptInstallsInjectable = getInjectable({ id: "attempt-installs", - instantiate: (di) => - attemptInstalls({ - attemptInstall: di.inject(attemptInstallInjectable), - }), + instantiate: (di): AttemptInstalls => { + const attemptInstall = di.inject(attemptInstallInjectable); + const getBasenameOfPath = di.inject(getBasenameOfPathInjectable); + const readFileNotify = di.inject(readFileNotifyInjectable); + + return async (filePaths) => { + await Promise.allSettled(filePaths.map(filePath => ( + attemptInstall({ + fileName: getBasenameOfPath(filePath), + dataP: readFileNotify(filePath), + }) + ))); + }; + }, }); export default attemptInstallsInjectable; diff --git a/src/renderer/components/+extensions/attempt-installs/attempt-installs.ts b/src/renderer/components/+extensions/attempt-installs/attempt-installs.ts deleted file mode 100644 index c40d9e78ca..0000000000 --- a/src/renderer/components/+extensions/attempt-installs/attempt-installs.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { readFileNotify } from "../read-file-notify/read-file-notify"; -import path from "path"; -import type { InstallRequest } from "../attempt-install/install-request"; - -interface Dependencies { - attemptInstall: (request: InstallRequest) => Promise; -} - -export const attemptInstalls = - ({ attemptInstall }: Dependencies) => - async (filePaths: string[]): Promise => { - const promises: Promise[] = []; - - for (const filePath of filePaths) { - promises.push( - attemptInstall({ - fileName: path.basename(filePath), - dataP: readFileNotify(filePath), - }), - ); - } - - await Promise.allSettled(promises); - }; diff --git a/src/renderer/components/+extensions/extensions.tsx b/src/renderer/components/+extensions/extensions.tsx index 446f6e0778..933dab8a9a 100644 --- a/src/renderer/components/+extensions/extensions.tsx +++ b/src/renderer/components/+extensions/extensions.tsx @@ -28,21 +28,21 @@ import enableExtensionInjectable from "./enable-extension/enable-extension.injec import disableExtensionInjectable from "./disable-extension/disable-extension.injectable"; import type { ConfirmUninstallExtension } from "./confirm-uninstall-extension.injectable"; import confirmUninstallExtensionInjectable from "./confirm-uninstall-extension.injectable"; -import installFromInputInjectable from "./install-from-input/install-from-input.injectable"; +import type { InstallExtensionFromInput } from "./install-extension-from-input.injectable"; +import installExtensionFromInputInjectable from "./install-extension-from-input.injectable"; import installFromSelectFileDialogInjectable from "./install-from-select-file-dialog.injectable"; import type { LensExtensionId } from "../../../extensions/lens-extension"; import installOnDropInjectable from "./install-on-drop/install-on-drop.injectable"; import { supportedExtensionFormats } from "./supported-extension-formats"; import extensionInstallationStateStoreInjectable from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; import type { ExtensionInstallationStateStore } from "../../../extensions/extension-installation-state-store/extension-installation-state-store"; -import type { InstallFromInput } from "./install-from-input/install-from-input"; interface Dependencies { userExtensions: IComputedValue; enableExtension: (id: LensExtensionId) => void; disableExtension: (id: LensExtensionId) => void; confirmUninstallExtension: ConfirmUninstallExtension; - installFromInput: InstallFromInput; + installExtensionFromInput: InstallExtensionFromInput; installFromSelectFileDialog: () => Promise; installOnDrop: (files: File[]) => Promise; extensionInstallationStateStore: ExtensionInstallationStateStore; @@ -107,7 +107,7 @@ class NonInjectedExtensions extends React.Component { (this.installPath = value)} - installFromInput={() => this.props.installFromInput(this.installPath)} + installFromInput={() => this.props.installExtensionFromInput(this.installPath)} installFromSelectFileDialog={this.props.installFromSelectFileDialog} installPath={this.installPath} /> @@ -131,7 +131,7 @@ export const Extensions = withInjectables(NonInjectedExtensions, { enableExtension: di.inject(enableExtensionInjectable), disableExtension: di.inject(disableExtensionInjectable), confirmUninstallExtension: di.inject(confirmUninstallExtensionInjectable), - installFromInput: di.inject(installFromInputInjectable), + installExtensionFromInput: di.inject(installExtensionFromInputInjectable), installOnDrop: di.inject(installOnDropInjectable), installFromSelectFileDialog: di.inject(installFromSelectFileDialogInjectable), extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable), diff --git a/src/renderer/components/+extensions/install-extension-from-input.injectable.tsx b/src/renderer/components/+extensions/install-extension-from-input.injectable.tsx new file mode 100644 index 0000000000..7b7d783283 --- /dev/null +++ b/src/renderer/components/+extensions/install-extension-from-input.injectable.tsx @@ -0,0 +1,82 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import React from "react"; +import type { ExtendableDisposer } from "../../../common/utils"; +import { downloadFile } from "../../../common/utils"; +import { InputValidators } from "../input"; +import { getMessageFromError } from "./get-message-from-error/get-message-from-error"; +import { getInjectable } from "@ogre-tools/injectable"; +import attemptInstallInjectable from "./attempt-install/attempt-install.injectable"; +import attemptInstallByInfoInjectable from "./attempt-install-by-info.injectable"; +import extensionInstallationStateStoreInjectable from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; +import readFileNotifyInjectable from "./read-file-notify/read-file-notify.injectable"; +import getBasenameOfPathInjectable from "../../../common/path/get-basename.injectable"; +import showErrorNotificationInjectable from "../notifications/show-error-notification.injectable"; +import loggerInjectable from "../../../common/logger.injectable"; + +export type InstallExtensionFromInput = (input: string) => Promise; + +const installExtensionFromInputInjectable = getInjectable({ + id: "install-extension-from-input", + + instantiate: (di): InstallExtensionFromInput => { + const attemptInstall = di.inject(attemptInstallInjectable); + const attemptInstallByInfo = di.inject(attemptInstallByInfoInjectable); + const extensionInstallationStateStore = di.inject(extensionInstallationStateStoreInjectable); + const readFileNotify = di.inject(readFileNotifyInjectable); + const getBasenameOfPath = di.inject(getBasenameOfPathInjectable); + const showErrorNotification = di.inject(showErrorNotificationInjectable); + const logger = di.inject(loggerInjectable); + + return async (input) => { + let disposer: ExtendableDisposer | undefined = undefined; + + try { + // fixme: improve error messages for non-tar-file URLs + if (InputValidators.isUrl.validate(input)) { + // install via url + disposer = extensionInstallationStateStore.startPreInstall(); + const { promise } = downloadFile({ url: input, timeout: 10 * 60 * 1000 }); + const fileName = getBasenameOfPath(input); + + return await attemptInstall({ fileName, dataP: promise }, disposer); + } + + try { + await InputValidators.isPath.validate(input); + + // install from system path + const fileName = getBasenameOfPath(input); + + return await attemptInstall({ fileName, dataP: readFileNotify(input) }); + } catch (error) { + const extNameCaptures = InputValidators.isExtensionNameInstallRegex.captures(input); + + if (extNameCaptures) { + const { name, version } = extNameCaptures; + + return await attemptInstallByInfo({ name, version }); + } + } + + throw new Error(`Unknown format of input: ${input}`); + } catch (error) { + const message = getMessageFromError(error); + + logger.info(`[EXTENSION-INSTALL]: installation has failed: ${message}`, { error, installPath: input }); + showErrorNotification(( +

+ {"Installation has failed: "} + {message} +

+ )); + } finally { + disposer?.(); + } + }; + }, +}); + +export default installExtensionFromInputInjectable; diff --git a/src/renderer/components/+extensions/install-from-input/install-from-input.injectable.ts b/src/renderer/components/+extensions/install-from-input/install-from-input.injectable.ts deleted file mode 100644 index 1f1a073293..0000000000 --- a/src/renderer/components/+extensions/install-from-input/install-from-input.injectable.ts +++ /dev/null @@ -1,23 +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 attemptInstallInjectable from "../attempt-install/attempt-install.injectable"; -import { installFromInput } from "./install-from-input"; -import attemptInstallByInfoInjectable from "../attempt-install-by-info.injectable"; -import extensionInstallationStateStoreInjectable - from "../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; - -const installFromInputInjectable = getInjectable({ - id: "install-extension-from-input", - - instantiate: (di) => - installFromInput({ - attemptInstall: di.inject(attemptInstallInjectable), - attemptInstallByInfo: di.inject(attemptInstallByInfoInjectable), - extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable), - }), -}); - -export default installFromInputInjectable; diff --git a/src/renderer/components/+extensions/install-from-input/install-from-input.tsx b/src/renderer/components/+extensions/install-from-input/install-from-input.tsx deleted file mode 100644 index eb5d6febe4..0000000000 --- a/src/renderer/components/+extensions/install-from-input/install-from-input.tsx +++ /dev/null @@ -1,77 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import type { ExtendableDisposer } from "../../../../common/utils"; -import { downloadFile } from "../../../../common/utils"; -import { InputValidators } from "../../input"; -import { getMessageFromError } from "../get-message-from-error/get-message-from-error"; -import logger from "../../../../main/logger"; -import { Notifications } from "../../notifications"; -import path from "path"; -import React from "react"; -import { readFileNotify } from "../read-file-notify/read-file-notify"; -import type { InstallRequest } from "../attempt-install/install-request"; -import type { ExtensionInfo } from "../attempt-install-by-info.injectable"; -import type { ExtensionInstallationStateStore } from "../../../../extensions/extension-installation-state-store/extension-installation-state-store"; - -export type InstallFromInput = (input: string) => Promise; - -interface Dependencies { - attemptInstall: (request: InstallRequest, disposer?: ExtendableDisposer) => Promise; - attemptInstallByInfo: (extensionInfo: ExtensionInfo) => Promise; - extensionInstallationStateStore: ExtensionInstallationStateStore; -} - -export const installFromInput = ({ - attemptInstall, - attemptInstallByInfo, - extensionInstallationStateStore, -}: Dependencies): InstallFromInput => ( - async (input) => { - let disposer: ExtendableDisposer | undefined = undefined; - - try { - // fixme: improve error messages for non-tar-file URLs - if (InputValidators.isUrl.validate(input)) { - // install via url - disposer = extensionInstallationStateStore.startPreInstall(); - const { promise } = downloadFile({ url: input, timeout: 10 * 60 * 1000 }); - const fileName = path.basename(input); - - return await attemptInstall({ fileName, dataP: promise }, disposer); - } - - try { - await InputValidators.isPath.validate(input); - - // install from system path - const fileName = path.basename(input); - - return await attemptInstall({ fileName, dataP: readFileNotify(input) }); - } catch (error) { - const extNameCaptures = InputValidators.isExtensionNameInstallRegex.captures(input); - - if (extNameCaptures) { - const { name, version } = extNameCaptures; - - return await attemptInstallByInfo({ name, version }); - } - } - - throw new Error(`Unknown format of input: ${input}`); - } catch (error) { - const message = getMessageFromError(error); - - logger.info(`[EXTENSION-INSTALL]: installation has failed: ${message}`, { error, installPath: input }); - Notifications.error(( -

- {"Installation has failed: "} - {message} -

- )); - } finally { - disposer?.(); - } - } -); diff --git a/src/renderer/components/+extensions/read-file-notify/read-file-notify.injectable.ts b/src/renderer/components/+extensions/read-file-notify/read-file-notify.injectable.ts new file mode 100644 index 0000000000..d907314b33 --- /dev/null +++ b/src/renderer/components/+extensions/read-file-notify/read-file-notify.injectable.ts @@ -0,0 +1,38 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getMessageFromError } from "../get-message-from-error/get-message-from-error"; +import { getInjectable } from "@ogre-tools/injectable"; +import showErrorNotificationInjectable from "../../notifications/show-error-notification.injectable"; +import loggerInjectable from "../../../../common/logger.injectable"; +import readFileBufferInjectable from "../../../../common/fs/read-file-buffer.injectable"; + +export type ReadFileNotify = (filePath: string, showError?: boolean) => Promise; + +const readFileNotifyInjectable = getInjectable({ + id: "read-file-notify", + instantiate: (di): ReadFileNotify => { + const showErrorNotification = di.inject(showErrorNotificationInjectable); + const logger = di.inject(loggerInjectable); + const readFileBuffer = di.inject(readFileBufferInjectable); + + return async (filePath, showError = true) => { + try { + return await readFileBuffer(filePath); + } catch (error) { + if (showError) { + const message = getMessageFromError(error); + + logger.info(`[EXTENSION-INSTALL]: preloading ${filePath} has failed: ${message}`, { error }); + showErrorNotification(`Error while reading "${filePath}": ${message}`); + } + } + + return null; + }; + }, +}); + +export default readFileNotifyInjectable; + diff --git a/src/renderer/components/+extensions/read-file-notify/read-file-notify.ts b/src/renderer/components/+extensions/read-file-notify/read-file-notify.ts deleted file mode 100644 index 1467f1bc33..0000000000 --- a/src/renderer/components/+extensions/read-file-notify/read-file-notify.ts +++ /dev/null @@ -1,23 +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 { getMessageFromError } from "../get-message-from-error/get-message-from-error"; -import logger from "../../../../main/logger"; -import { Notifications } from "../../notifications"; - -export const readFileNotify = async (filePath: string, showError = true): Promise => { - try { - return await fse.readFile(filePath); - } catch (error) { - if (showError) { - const message = getMessageFromError(error); - - logger.info(`[EXTENSION-INSTALL]: preloading ${filePath} has failed: ${message}`, { error }); - Notifications.error(`Error while reading "${filePath}": ${message}`); - } - } - - return null; -}; diff --git a/src/renderer/components/cluster-settings/components/cluster-local-terminal-settings.tsx b/src/renderer/components/cluster-settings/components/cluster-local-terminal-settings.tsx index 3a5c1c4d2d..228b9caeec 100644 --- a/src/renderer/components/cluster-settings/components/cluster-local-terminal-settings.tsx +++ b/src/renderer/components/cluster-settings/components/cluster-local-terminal-settings.tsx @@ -9,7 +9,6 @@ import type { Cluster } from "../../../../common/cluster/cluster"; import { Input } from "../../input"; import { SubTitle } from "../../layout/sub-title"; import type { ShowNotification } from "../../notifications"; -import { resolveTilde } from "../../../utils"; import { Icon } from "../../icon"; import { PathPicker } from "../../path-picker"; import { isWindows } from "../../../../common/vars"; @@ -17,6 +16,8 @@ import { withInjectables } from "@ogre-tools/injectable-react"; import showErrorNotificationInjectable from "../../notifications/show-error-notification.injectable"; import type { ValidateDirectory } from "../../../../common/fs/validate-directory.injectable"; import validateDirectoryInjectable from "../../../../common/fs/validate-directory.injectable"; +import type { ResolveTilde } from "../../../../common/path/resolve-tilde.injectable"; +import resolveTildeInjectable from "../../../../common/path/resolve-tilde.injectable"; export interface ClusterLocalTerminalSettingProps { cluster: Cluster; @@ -24,9 +25,15 @@ export interface ClusterLocalTerminalSettingProps { interface Dependencies { showErrorNotification: ShowNotification; validateDirectory: ValidateDirectory; + resolveTilde: ResolveTilde; } -const NonInjectedClusterLocalTerminalSetting = observer(({ cluster, showErrorNotification, validateDirectory }: Dependencies & ClusterLocalTerminalSettingProps) => { +const NonInjectedClusterLocalTerminalSetting = observer(({ + cluster, + showErrorNotification, + validateDirectory, + resolveTilde, +}: Dependencies & ClusterLocalTerminalSettingProps) => { if (!cluster) { return null; } @@ -150,15 +157,11 @@ const NonInjectedClusterLocalTerminalSetting = observer(({ cluster, showErrorNot ); }); -export const ClusterLocalTerminalSetting = withInjectables( - NonInjectedClusterLocalTerminalSetting, - - { - getProps: (di, props) => ({ - showErrorNotification: di.inject(showErrorNotificationInjectable), - validateDirectory: di.inject(validateDirectoryInjectable), - ...props, - }), - }, -); - +export const ClusterLocalTerminalSetting = withInjectables(NonInjectedClusterLocalTerminalSetting, { + getProps: (di, props) => ({ + ...props, + showErrorNotification: di.inject(showErrorNotificationInjectable), + validateDirectory: di.inject(validateDirectoryInjectable), + resolveTilde: di.inject(resolveTildeInjectable), + }), +}); diff --git a/src/renderer/components/dock/create-resource/lens-templates.injectable.ts b/src/renderer/components/dock/create-resource/lens-templates.injectable.ts index e5d88d9256..86c3e4fd3d 100644 --- a/src/renderer/components/dock/create-resource/lens-templates.injectable.ts +++ b/src/renderer/components/dock/create-resource/lens-templates.injectable.ts @@ -3,22 +3,23 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import path from "path"; import { hasCorrectExtension } from "./has-correct-extension"; import readFileInjectable from "../../../../common/fs/read-file.injectable"; -import readDirInjectable from "../../../../common/fs/read-dir.injectable"; +import readDirectoryInjectable from "../../../../common/fs/read-directory.injectable"; import type { RawTemplates } from "./create-resource-templates.injectable"; import staticFilesDirectoryInjectable from "../../../../common/vars/static-files-directory.injectable"; import joinPathsInjectable from "../../../../common/path/join-paths.injectable"; +import parsePathInjectable from "../../../../common/path/parse.injectable"; const lensCreateResourceTemplatesInjectable = getInjectable({ id: "lens-create-resource-templates", instantiate: async (di): Promise => { const readFile = di.inject(readFileInjectable); - const readDir = di.inject(readDirInjectable); + const readDir = di.inject(readDirectoryInjectable); const joinPaths = di.inject(joinPathsInjectable); const staticFilesDirectory = di.inject(staticFilesDirectoryInjectable); + const parsePath = di.inject(parsePathInjectable); /** * Mapping between file names and their contents @@ -28,7 +29,7 @@ const lensCreateResourceTemplatesInjectable = getInjectable({ for (const dirEntry of await readDir(templatesFolder)) { if (hasCorrectExtension(dirEntry)) { - templates.push([path.parse(dirEntry).name, await readFile(path.join(templatesFolder, dirEntry))]); + templates.push([parsePath(dirEntry).name, await readFile(joinPaths(templatesFolder, dirEntry))]); } } diff --git a/src/renderer/components/dock/create-resource/user-templates.injectable.ts b/src/renderer/components/dock/create-resource/user-templates.injectable.ts index 524dad3d86..65b896f95d 100644 --- a/src/renderer/components/dock/create-resource/user-templates.injectable.ts +++ b/src/renderer/components/dock/create-resource/user-templates.injectable.ts @@ -3,101 +3,108 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import type { IComputedValue } from "mobx"; import { computed, observable } from "mobx"; -import path from "path"; -import os from "os"; import { delay, getOrInsert, isErrnoException, waitForPath } from "../../../utils"; -import { watch } from "chokidar"; import { readFile } from "fs/promises"; -import logger from "../../../../common/logger"; import { hasCorrectExtension } from "./has-correct-extension"; import type { RawTemplates } from "./create-resource-templates.injectable"; - -const userTemplatesFolder = path.join(os.homedir(), ".k8slens", "templates"); - -function groupTemplates(templates: Map): RawTemplates[] { - const res = new Map(); - - for (const [filePath, contents] of templates) { - const rawRelative = path.dirname(path.relative(userTemplatesFolder, filePath)); - const title = rawRelative === "." - ? "ungrouped" - : rawRelative; - - getOrInsert(res, title, []).push([path.parse(filePath).name, contents]); - } - - return [...res.entries()]; -} - -function watchUserCreateResourceTemplates(): IComputedValue { - /** - * Map between filePaths and template contents - */ - const templates = observable.map(); - - const onAddOrChange = async (filePath: string) => { - if (!hasCorrectExtension(filePath)) { - // ignore non yaml or json files - return; - } - - try { - const contents = await readFile(filePath, "utf-8"); - - templates.set(filePath, contents); - } catch (error) { - if (isErrnoException(error) && error.code === "ENOENT") { - // ignore, file disappeared - } else { - logger.warn(`[USER-CREATE-RESOURCE-TEMPLATES]: encountered error while reading ${filePath}`, error); - } - } - }; - const onUnlink = (filePath: string) => { - templates.delete(filePath); - }; - - (async () => { - for (let i = 1;; i *= 2) { - try { - await waitForPath(userTemplatesFolder); - break; - } catch (error) { - logger.warn(`[USER-CREATE-RESOURCE-TEMPLATES]: encountered error while waiting for ${userTemplatesFolder} to exist, waiting and trying again`, error); - await delay(i * 1000); // exponential backoff in seconds - } - } - - /** - * NOTE: There is technically a race condition here of the form "time-of-check to time-of-use" - */ - watch(userTemplatesFolder, { - disableGlobbing: true, - ignorePermissionErrors: true, - usePolling: false, - awaitWriteFinish: { - pollInterval: 100, - stabilityThreshold: 1000, - }, - ignoreInitial: false, - atomic: 150, // for "atomic writes" - }) - .on("add", onAddOrChange) - .on("change", onAddOrChange) - .on("unlink", onUnlink) - .on("error", error => { - logger.warn(`[USER-CREATE-RESOURCE-TEMPLATES]: encountered error while watching files under ${userTemplatesFolder}`, error); - }); - })(); - - return computed(() => groupTemplates(templates)); -} +import joinPathsInjectable from "../../../../common/path/join-paths.injectable"; +import watchInjectable from "../../../../common/fs/watch/watch.injectable"; +import getRelativePathInjectable from "../../../../common/path/get-relative-path.injectable"; +import homeDirectoryPathInjectable from "../../../../common/os/home-directory-path.injectable"; +import getDirnameOfPathInjectable from "../../../../common/path/get-dirname.injectable"; +import loggerInjectable from "../../../../common/logger.injectable"; +import parsePathInjectable from "../../../../common/path/parse.injectable"; const userCreateResourceTemplatesInjectable = getInjectable({ id: "user-create-resource-templates", - instantiate: () => watchUserCreateResourceTemplates(), + instantiate: (di) => { + const joinPaths = di.inject(joinPathsInjectable); + const watch = di.inject(watchInjectable); + const getRelativePath = di.inject(getRelativePathInjectable); + const homeDirectoryPath = di.inject(homeDirectoryPathInjectable); + const getDirnameOfPath = di.inject(getDirnameOfPathInjectable); + const logger = di.inject(loggerInjectable); + const parsePath = di.inject(parsePathInjectable); + + const userTemplatesFolder = joinPaths(homeDirectoryPath, ".k8slens", "templates"); + const groupTemplates = (templates: Map): RawTemplates[] => { + const res = new Map(); + + for (const [filePath, contents] of templates) { + const rawRelative = getDirnameOfPath(getRelativePath(userTemplatesFolder, filePath)); + const title = rawRelative === "." + ? "ungrouped" + : rawRelative; + + getOrInsert(res, title, []).push([parsePath(filePath).name, contents]); + } + + return [...res.entries()]; + }; + + /** + * Map between filePaths and template contents + */ + const templates = observable.map(); + + const onAddOrChange = async (filePath: string) => { + if (!hasCorrectExtension(filePath)) { + // ignore non yaml or json files + return; + } + + try { + const contents = await readFile(filePath, "utf-8"); + + templates.set(filePath, contents); + } catch (error) { + if (isErrnoException(error) && error.code === "ENOENT") { + // ignore, file disappeared + } else { + logger.warn(`[USER-CREATE-RESOURCE-TEMPLATES]: encountered error while reading ${filePath}`, error); + } + } + }; + const onUnlink = (filePath: string) => { + templates.delete(filePath); + }; + + (async () => { + for (let i = 1;; i *= 2) { + try { + await waitForPath(userTemplatesFolder); + break; + } catch (error) { + logger.warn(`[USER-CREATE-RESOURCE-TEMPLATES]: encountered error while waiting for ${userTemplatesFolder} to exist, waiting and trying again`, error); + await delay(i * 1000); // exponential backoff in seconds + } + } + + /** + * NOTE: There is technically a race condition here of the form "time-of-check to time-of-use" + */ + watch(userTemplatesFolder, { + disableGlobbing: true, + ignorePermissionErrors: true, + usePolling: false, + awaitWriteFinish: { + pollInterval: 100, + stabilityThreshold: 1000, + }, + ignoreInitial: false, + atomic: 150, // for "atomic writes" + }) + .on("add", onAddOrChange) + .on("change", onAddOrChange) + .on("unlink", onUnlink) + .on("error", error => { + logger.warn(`[USER-CREATE-RESOURCE-TEMPLATES]: encountered error while watching files under ${userTemplatesFolder}`, error); + }); + })(); + + return computed(() => groupTemplates(templates)); + }, }); export default userCreateResourceTemplatesInjectable; diff --git a/src/renderer/create-cluster/create-cluster.injectable.ts b/src/renderer/create-cluster/create-cluster.injectable.ts index af64873fad..0c73eb82fa 100644 --- a/src/renderer/create-cluster/create-cluster.injectable.ts +++ b/src/renderer/create-cluster/create-cluster.injectable.ts @@ -9,6 +9,7 @@ import directoryForKubeConfigsInjectable from "../../common/app-paths/directory- import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token"; import loggerInjectable from "../../common/logger.injectable"; import broadcastMessageInjectable from "../../common/ipc/broadcast-message.injectable"; +import loadConfigfromFileInjectable from "../../common/kube-helpers/load-config-from-file.injectable"; const createClusterInjectable = getInjectable({ id: "create-cluster", @@ -18,6 +19,7 @@ const createClusterInjectable = getInjectable({ directoryForKubeConfigs: di.inject(directoryForKubeConfigsInjectable), logger: di.inject(loggerInjectable), broadcastMessage: di.inject(broadcastMessageInjectable), + loadConfigfromFile: di.inject(loadConfigfromFileInjectable), // TODO: Dismantle wrong abstraction // Note: "as never" to get around strictness in unnatural scenario diff --git a/yarn.lock b/yarn.lock index b6177f6a4f..52b0b64ce6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2250,13 +2250,6 @@ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40" integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ== -"@types/minipass@*": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@types/minipass/-/minipass-3.1.2.tgz#e2d7f9df0698aff421dcf145b4fc05b8183b9030" - integrity sha512-foLGjgrJkUjLG/o2t2ymlZGEoBNBa/TfoUZ7oCTkOjP1T43UGBJspovJou/l3ZuHvye2ewR5cZNtp2zyWgILMA== - dependencies: - "@types/node" "*" - "@types/mkdirp@^1.0.0": version "1.0.2" resolved "https://registry.yarnpkg.com/@types/mkdirp/-/mkdirp-1.0.2.tgz#8d0bad7aa793abe551860be1f7ae7f3198c16666" @@ -2561,13 +2554,13 @@ dependencies: "@types/node" "*" -"@types/tar@^4.0.5": - version "4.0.5" - resolved "https://registry.yarnpkg.com/@types/tar/-/tar-4.0.5.tgz#5f953f183e36a15c6ce3f336568f6051b7b183f3" - integrity sha512-cgwPhNEabHaZcYIy5xeMtux2EmYBitfqEceBUi2t5+ETy4dW6kswt6WX4+HqLeiiKOo42EXbGiDmVJ2x+vi37Q== +"@types/tar@^6.1.2": + version "6.1.2" + resolved "https://registry.yarnpkg.com/@types/tar/-/tar-6.1.2.tgz#e60108a7d1b08cc91bf2faf1286cc08fdad48bbe" + integrity sha512-bnX3RRm70/n1WMwmevdOAeDU4YP7f5JSubgnuU+yrO+xQQjwDboJj3u2NTJI5ngCQhXihqVVAH5h5J8YpdpEvg== dependencies: - "@types/minipass" "*" "@types/node" "*" + minipass "^3.3.5" "@types/tcp-port-used@^1.0.1": version "1.0.1" @@ -9108,6 +9101,13 @@ minipass@^3.0.0, minipass@^3.1.0, minipass@^3.1.1, minipass@^3.1.3, minipass@^3. dependencies: yallist "^4.0.0" +minipass@^3.3.5: + version "3.3.5" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.5.tgz#6da7e53a48db8a856eeb9153d85b230a2119e819" + integrity sha512-rQ/p+KfKBkeNwo04U15i+hOwoVBVmekmm/HcfTkTN2t9pbQKCMm4eN5gFeqgrrSp/kH/7BYYhTIHOxGqzbBPaA== + dependencies: + yallist "^4.0.0" + minizlib@^2.0.0, minizlib@^2.1.1, minizlib@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"