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

Fix test flakiness because of path side effects, propagate uses to as many places

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2022-08-25 16:52:20 -04:00
parent 4d2ca3d8b5
commit 156de6138a
70 changed files with 1077 additions and 866 deletions

View File

@ -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",

View File

@ -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<KubeConfig> {
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<KubeConfig> {
const proxyKCPath = await this.getProxyKubeconfigPath();
const { config } = await loadConfigFromFile(proxyKCPath);
const { config } = await this.dependencies.loadConfigfromFile(proxyKCPath);
return config;
}

View File

@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { 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");
});

View File

@ -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<void>;
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;

View File

@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { 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");
});

View File

@ -0,0 +1,16 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { 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<void>;
const moveInjectable = getInjectable({
id: "move",
instantiate: (di): Move => di.inject(fsInjectable).move,
});
export default moveInjectable;

View File

@ -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");
});

View File

@ -29,9 +29,9 @@ export interface ReadDirectory {
): Promise<Dirent[]>;
}
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;

View File

@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { 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");
});

View File

@ -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<void>;
const removePathInjectable = getInjectable({
id: "remove-path",
instantiate: (di): RemovePath => di.inject(fsInjectable).remove,
});
export default removePathInjectable;

View File

@ -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",

View File

@ -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<void>;
interface Dependencies {
writeJson: (file: string, object: any, options?: WriteOptions | BufferEncoding | string) => Promise<void>;
ensureDir: (dir: string, options?: EnsureOptions | number) => Promise<void>;
}
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,
});
};
},
});

View File

@ -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<ConfigResult> {
const content = await fse.readFile(resolvePath(filePath), "utf-8");
return loadConfigFromString(content);
}
const clusterSchema = Joi.object({
name: Joi
.string()

View File

@ -0,0 +1,23 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { 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<ConfigResult>;
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;

View File

@ -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");

View File

@ -0,0 +1,14 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { homedir } from "os";
const homeDirectoryPathInjectable = getInjectable({
id: "home-directory-path",
instantiate: () => homedir(),
causesSideEffects: true,
});
export default homeDirectoryPathInjectable;

View File

@ -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");

View File

@ -0,0 +1,14 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { tmpdir } from "os";
const tempDirectoryPathInjectable = getInjectable({
id: "temp-directory-path",
instantiate: () => tmpdir(),
causesSideEffects: true,
});
export default tempDirectoryPathInjectable;

View File

@ -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;

View File

@ -0,0 +1,10 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import path from "path";
import { getGlobalOverride } from "../test-utils/get-global-override";
import parsePathInjectable from "./parse.injectable";
export default getGlobalOverride(parsePathInjectable, () => path.posix.parse);

View File

@ -0,0 +1,14 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import path from "path";
const parsePathInjectable = getInjectable({
id: "parse-path",
instantiate: () => path.parse,
causesSideEffects: true,
});
export default parsePathInjectable;

View File

@ -0,0 +1,21 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import 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;

View File

@ -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;

View File

@ -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<void>;
@ -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 {

View File

@ -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<UserStoreModel> /* 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
*/

View File

@ -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",

View File

@ -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";

View File

@ -1,55 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import 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;
}

View File

@ -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<string[]> {
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,
});
}

View File

@ -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),

View File

@ -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<void>;
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<void> {
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<Map<LensExtensionId, InstalledExtension>> {
@ -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/<username>/Library/Application Support/LensDev/extensions
await this.dependencies.deleteFile(this.inTreeTargetPath);
await this.dependencies.removePath(this.inTreeTargetPath);
// Create folder e.g. /Users/<username>/Library/Application Support/LensDev/extensions
await this.dependencies.ensureDirectory(this.inTreeTargetPath);

View File

@ -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),
}),
});

View File

@ -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<LensExtensionId, LensExtensionState>) => 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;

View File

@ -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);

View File

@ -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),

View File

@ -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);

View File

@ -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<string> {
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}`);

View File

@ -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;

View File

@ -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);

View File

@ -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<string, string>;
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\"",

View File

@ -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<string[]> {
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":

View File

@ -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) => {

View File

@ -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 {

View File

@ -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<string, string>(); // mapping from WorkspaceId to name
for (const { id, name } of workspaceData.workspaces) {

View File

@ -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<ClusterModel, "id">):
};
}
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<string, ClusterModel>();

View File

@ -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<string, PartialHotbar>(); // mapping from WorkspaceId to HotBar
for (const { id, name } of workspaceStoreData.workspaces) {

View File

@ -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`);

View File

@ -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<string, Option> {
@ -90,7 +92,7 @@ class NonInjectedAddCluster extends React.Component<Dependencies> {
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<Dependencies> {
export const AddCluster = withInjectables<Dependencies>(NonInjectedAddCluster, {
getProps: (di) => ({
getCustomKubeConfigDirectory: di.inject(
getCustomKubeConfigDirectoryInjectable,
),
getCustomKubeConfigDirectory: di.inject(getCustomKubeConfigDirectoryInjectable),
navigateToCatalog: di.inject(navigateToCatalogInjectable),
getDirnameOfPath: di.inject(getDirnameOfPathInjectable),
}),
});

View File

@ -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<InstallFromInput>;
let installExtensionFromInput: jest.MockedFunction<InstallExtensionFromInput>;
let extensionInstallationStateStore: ExtensionInstallationStateStore;
let render: DiRender;
let deleteFileMock: jest.MockedFunction<DeleteFile>;
@ -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();

View File

@ -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<void>;
interface Dependencies {
attemptInstall: (request: InstallRequest, d: ExtendableDisposer) => Promise<void>;
getBaseRegistryUrl: () => Promise<string>;
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((
<p>
{"The "}
<em>{name}</em>
{" extension does not have a version or tag "}
<code>{version}</code>
.
</p>
));
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((
<p>
{"The "}
<em>{name}</em>
{" extension does not have a version or tag "}
<code>{version}</code>
.
</p>
));
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: (
<p>
Are you sure you want to install
{" "}
<b>
{name}
@
{finalVersion}
</b>
?
</p>
),
labelCancel: "Cancel",
labelOk: "Install",
});
if (requireConfirmation) {
const proceed = await confirm({
message: (
<p>
Are you sure you want to install
{" "}
<b>
{name}
@
{finalVersion}
</b>
?
</p>
),
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;

View File

@ -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<boolean>;
unpackExtension: (
request: InstallRequestValidated,
disposeDownloading: Disposer,
) => Promise<void>;
createTempFilesAndValidate: (
installRequest: InstallRequest,
) => Promise<InstallRequestValidated | null>;
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<void> => {
console.log("Attempting to install extension");
const dispose = disposer(
extensionInstallationStateStore.startPreInstall(),
d,

View File

@ -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<InstallRequestValidated | null>;
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<InstallRequestValidated | null> => {
// 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(
<div className="flex column gaps">
<p>
{"Installing "}
<em>{fileName}</em>
{" has failed, skipping."}
</p>
<p>
{"Reason: "}
<em>{message}</em>
</p>
</div>,
);
}
return null;
};
},
});
export default createTempFilesAndValidateInjectable;

View File

@ -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<InstallRequestValidated | null> => {
// 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(
<div className="flex column gaps">
<p>
{"Installing "}
<em>{fileName}</em>
{" has failed, skipping."}
</p>
<p>
{"Reason: "}
<em>{message}</em>
</p>
</div>,
);
}
return null;
};
function getExtensionPackageTemp(fileName = "") {
return path.join(os.tmpdir(), "lens-extensions", fileName);
}

View File

@ -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;

View File

@ -1,16 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import 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));

View File

@ -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<void>;
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(
<p>
{"Extension "}
<b>{displayName}</b>
{" successfully installed!"}
</p>,
);
} catch (error) {
const message = getMessageFromError(error);
logger.info(
`[EXTENSION-INSTALLATION]: installing ${request.fileName} has failed: ${message}`,
{ error },
);
showErrorNotification(
<p>
{"Installing extension "}
<b>{displayName}</b>
{" has failed: "}
<em>{message}</em>
</p>,
);
} finally {
// Remove install state once finished
extensionInstallationStateStore.clearInstalling(id);
// clean up
removePath(unpackingTempFolder).catch(noop);
deleteFile(tempFile).catch(noop);
}
};
},
});
export default unpackExtensionInjectable;

View File

@ -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(
<p>
{"Extension "}
<b>{displayName}</b>
{" successfully installed!"}
</p>,
);
} catch (error) {
const message = getMessageFromError(error);
logger.info(
`[EXTENSION-INSTALLATION]: installing ${request.fileName} has failed: ${message}`,
{ error },
);
Notifications.error(
<p>
{"Installing extension "}
<b>{displayName}</b>
{" has failed: "}
<em>{message}</em>
</p>,
);
} finally {
// Remove install state once finished
extensionInstallationStateStore.clearInstalling(id);
// clean up
fse.remove(unpackingTempFolder).catch(noop);
fse.unlink(tempFile).catch(noop);
}
};

View File

@ -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<void>;
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;

View File

@ -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<void>;
}
export const attemptInstalls =
({ attemptInstall }: Dependencies) =>
async (filePaths: string[]): Promise<void> => {
const promises: Promise<void>[] = [];
for (const filePath of filePaths) {
promises.push(
attemptInstall({
fileName: path.basename(filePath),
dataP: readFileNotify(filePath),
}),
);
}
await Promise.allSettled(promises);
};

View File

@ -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<InstalledExtension[]>;
enableExtension: (id: LensExtensionId) => void;
disableExtension: (id: LensExtensionId) => void;
confirmUninstallExtension: ConfirmUninstallExtension;
installFromInput: InstallFromInput;
installExtensionFromInput: InstallExtensionFromInput;
installFromSelectFileDialog: () => Promise<void>;
installOnDrop: (files: File[]) => Promise<void>;
extensionInstallationStateStore: ExtensionInstallationStateStore;
@ -107,7 +107,7 @@ class NonInjectedExtensions extends React.Component<Dependencies> {
<Install
supportedFormats={supportedExtensionFormats}
onChange={value => (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<Dependencies>(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),

View File

@ -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<void>;
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((
<p>
{"Installation has failed: "}
<b>{message}</b>
</p>
));
} finally {
disposer?.();
}
};
},
});
export default installExtensionFromInputInjectable;

View File

@ -1,23 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
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;

View File

@ -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<void>;
interface Dependencies {
attemptInstall: (request: InstallRequest, disposer?: ExtendableDisposer) => Promise<void>;
attemptInstallByInfo: (extensionInfo: ExtensionInfo) => Promise<void>;
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((
<p>
{"Installation has failed: "}
<b>{message}</b>
</p>
));
} finally {
disposer?.();
}
}
);

View File

@ -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<Buffer | null>;
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;

View File

@ -1,23 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
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<Buffer | null> => {
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;
};

View File

@ -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<Dependencies, ClusterLocalTerminalSettingProps>(
NonInjectedClusterLocalTerminalSetting,
{
getProps: (di, props) => ({
showErrorNotification: di.inject(showErrorNotificationInjectable),
validateDirectory: di.inject(validateDirectoryInjectable),
...props,
}),
},
);
export const ClusterLocalTerminalSetting = withInjectables<Dependencies, ClusterLocalTerminalSettingProps>(NonInjectedClusterLocalTerminalSetting, {
getProps: (di, props) => ({
...props,
showErrorNotification: di.inject(showErrorNotificationInjectable),
validateDirectory: di.inject(validateDirectoryInjectable),
resolveTilde: di.inject(resolveTildeInjectable),
}),
});

View File

@ -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<RawTemplates> => {
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))]);
}
}

View File

@ -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<string, string>): RawTemplates[] {
const res = new Map<string, [string, string][]>();
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<RawTemplates[]> {
/**
* Map between filePaths and template contents
*/
const templates = observable.map<string, string>();
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<string, string>): RawTemplates[] => {
const res = new Map<string, [string, string][]>();
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<string, string>();
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;

View File

@ -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

View File

@ -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"