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

Add designated folder for extensions (#1245)

* add store for mapping extension names to filesystem paths

- add extension mechinism for getting a folder to save files to

- add test to make sure that extensions are being loaded

- skip extension loading test

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2020-11-24 10:28:08 -05:00 committed by GitHub
parent b94e523ad5
commit 4e6b8ee10e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 119 additions and 32 deletions

View File

@ -10,9 +10,9 @@ export default class ClusterMetricsFeatureExtension extends LensRendererExtensio
Description: () => { Description: () => {
return ( return (
<span> <span>
Enable timeseries data visualization (Prometheus stack) for your cluster. Enable timeseries data visualization (Prometheus stack) for your cluster.
Install this only if you don't have existing Prometheus stack installed. Install this only if you don't have existing Prometheus stack installed.
You can see preview of manifests <a href="https://github.com/lensapp/lens/tree/master/extensions/lens-metrics/resources" target="_blank">here</a>. You can see preview of manifests <a href="https://github.com/lensapp/lens/tree/master/extensions/lens-metrics/resources" target="_blank">here</a>.
</span> </span>
); );
} }

View File

@ -16,8 +16,8 @@ jest.setTimeout(60000);
// FIXME (!): improve / simplify all css-selectors + use [data-test-id="some-id"] (already used in some tests below) // FIXME (!): improve / simplify all css-selectors + use [data-test-id="some-id"] (already used in some tests below)
describe("Lens integration tests", () => { describe("Lens integration tests", () => {
const TEST_NAMESPACE = "integration-tests"; const TEST_NAMESPACE = "integration-tests";
const BACKSPACE = "\uE003"; const BACKSPACE = "\uE003";
let app: Application; let app: Application;
const appStart = async () => { const appStart = async () => {
@ -37,23 +37,33 @@ describe("Lens integration tests", () => {
const minikubeReady = (): boolean => { const minikubeReady = (): boolean => {
// determine if minikube is running // determine if minikube is running
let status = spawnSync("minikube status", { shell: true }); {
if (status.status !== 0) { const { status } = spawnSync("minikube status", { shell: true });
console.warn("minikube not running"); if (status !== 0) {
return false; console.warn("minikube not running");
return false;
}
} }
// Remove TEST_NAMESPACE if it already exists // Remove TEST_NAMESPACE if it already exists
status = spawnSync(`minikube kubectl -- get namespace ${TEST_NAMESPACE}`, { shell: true }); {
if (status.status === 0) { const { status } = spawnSync(`minikube kubectl -- get namespace ${TEST_NAMESPACE}`, { shell: true });
console.warn(`Removing existing ${TEST_NAMESPACE} namespace`); if (status === 0) {
status = spawnSync(`minikube kubectl -- delete namespace ${TEST_NAMESPACE}`, { shell: true }); console.warn(`Removing existing ${TEST_NAMESPACE} namespace`);
if (status.status !== 0) {
console.warn(`Error removing ${TEST_NAMESPACE} namespace: ${status.stderr.toString()}`); const { status, stdout, stderr } = spawnSync(
return false; `minikube kubectl -- delete namespace ${TEST_NAMESPACE}`,
{ shell: true },
);
if (status !== 0) {
console.warn(`Error removing ${TEST_NAMESPACE} namespace: ${stderr.toString()}`);
return false;
}
console.log(stdout.toString());
} }
console.log(status.stdout.toString());
} }
return true; return true;
}; };
const ready = minikubeReady(); const ready = minikubeReady();
@ -62,8 +72,8 @@ describe("Lens integration tests", () => {
beforeAll(appStart, 20000); beforeAll(appStart, 20000);
afterAll(async () => { afterAll(async () => {
if (app && app.isRunning()) { if (app?.isRunning()) {
return util.tearDown(app); await util.tearDown(app);
} }
}); });

View File

@ -8,9 +8,8 @@ const AppPaths: Partial<Record<NodeJS.Platform, string>> = {
export function setup(): Application { export function setup(): Application {
return new Application({ return new Application({
// path to electron app path: AppPaths[process.platform], // path to electron app
args: [], args: [],
path: AppPaths[process.platform],
startTimeout: 30000, startTimeout: 30000,
waitTimeout: 60000, waitTimeout: 60000,
env: { env: {
@ -19,9 +18,10 @@ export function setup(): Application {
}); });
} }
type AsyncPidGetter = () => Promise<number>;
export async function tearDown(app: Application) { export async function tearDown(app: Application) {
const mpid: any = app.mainProcess.pid; const pid = await (app.mainProcess.pid as any as AsyncPidGetter)();
const pid = await mpid();
await app.stop(); await app.stop();
try { try {
process.kill(pid, "SIGKILL"); process.kill(pid, "SIGKILL");

View File

@ -6,8 +6,8 @@ import { defineGlobal } from "./utils/defineGlobal";
export const isMac = process.platform === "darwin"; export const isMac = process.platform === "darwin";
export const isWindows = process.platform === "win32"; export const isWindows = process.platform === "win32";
export const isLinux = process.platform === "linux"; export const isLinux = process.platform === "linux";
export const isDebugging = process.env.DEBUG === "true"; export const isDebugging = ["true", "1", "yes", "y", "on"].includes((process.env.DEBUG ?? "").toLowerCase());
export const isSnap = !!process.env["SNAP"]; export const isSnap = !!process.env.SNAP;
export const isProduction = process.env.NODE_ENV === "production"; export const isProduction = process.env.NODE_ENV === "production";
export const isTestEnv = !!process.env.JEST_WORKER_ID; export const isTestEnv = !!process.env.JEST_WORKER_ID;
export const isDevelopment = !isTestEnv && !isProduction; export const isDevelopment = !isTestEnv && !isProduction;

View File

@ -131,21 +131,25 @@ export class ExtensionLoader {
protected autoInitExtensions(register: (ext: LensExtension) => Promise<Function[]>) { protected autoInitExtensions(register: (ext: LensExtension) => Promise<Function[]>) {
return reaction(() => this.toJSON(), installedExtensions => { return reaction(() => this.toJSON(), installedExtensions => {
for (const [extId, ext] of installedExtensions) { for (const [extId, ext] of installedExtensions) {
let instance = this.instances.get(extId); const alreadyInit = this.instances.has(extId);
if (ext.isEnabled && !instance) {
if (ext.isEnabled && !alreadyInit) {
try { try {
const LensExtensionClass: LensExtensionConstructor = this.requireExtension(ext); const LensExtensionClass = this.requireExtension(ext);
if (!LensExtensionClass) continue; if (!LensExtensionClass) {
instance = new LensExtensionClass(ext); continue;
}
const instance = new LensExtensionClass(ext);
instance.whenEnabled(() => register(instance)); instance.whenEnabled(() => register(instance));
instance.enable(); instance.enable();
this.instances.set(extId, instance); this.instances.set(extId, instance);
} catch (err) { } catch (err) {
logger.error(`${logModule}: activation extension error`, { ext, err }); logger.error(`${logModule}: activation extension error`, { ext, err });
} }
} else if (!ext.isEnabled && instance) { } else if (!ext.isEnabled && alreadyInit) {
logger.info(`${logModule} deleting extension ${extId}`);
try { try {
const instance = this.instances.get(extId);
instance.disable(); instance.disable();
this.instances.delete(extId); this.instances.delete(extId);
} catch (err) { } catch (err) {
@ -158,7 +162,7 @@ export class ExtensionLoader {
}); });
} }
protected requireExtension(extension: InstalledExtension) { protected requireExtension(extension: InstalledExtension): LensExtensionConstructor {
let extEntrypoint = ""; let extEntrypoint = "";
try { try {
if (ipcRenderer && extension.manifest.renderer) { if (ipcRenderer && extension.manifest.renderer) {

View File

@ -1,5 +1,6 @@
import type { InstalledExtension } from "./extension-discovery"; import type { InstalledExtension } from "./extension-discovery";
import { action, observable, reaction } from "mobx"; import { action, observable, reaction } from "mobx";
import { filesystemProvisionerStore } from "../main/extension-filesystem";
import logger from "../main/logger"; import logger from "../main/logger";
export type LensExtensionId = string; // path to manifest (package.json) export type LensExtensionId = string; // path to manifest (package.json)
@ -39,6 +40,17 @@ export class LensExtension {
return this.manifest.version; return this.manifest.version;
} }
/**
* getExtensionFileFolder returns the path to an already created folder. This
* folder is for the sole use of this extension.
*
* Note: there is no security done on this folder, only obfiscation of the
* folder name.
*/
async getExtensionFileFolder(): Promise<string> {
return filesystemProvisionerStore.requestDirectory(this.id);
}
get description() { get description() {
return this.manifest.description; return this.manifest.description;
} }

View File

@ -0,0 +1,57 @@
import { randomBytes } from "crypto";
import { SHA256 } from "crypto-js";
import { app } from "electron";
import fse from "fs-extra";
import { action, observable, toJS } from "mobx";
import path from "path";
import { BaseStore } from "../common/base-store";
import { LensExtensionId } from "../extensions/lens-extension";
interface FSProvisionModel {
extensions: Record<string, string>; // extension names to paths
}
export class FilesystemProvisionerStore extends BaseStore<FSProvisionModel> {
@observable registeredExtensions = observable.map<LensExtensionId, string>();
private constructor() {
super({
configName: "lens-filesystem-provisioner-store",
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
});
}
/**
* This function retrieves the saved path to the folder which the extension
* can saves files to. If the folder is not present then it is created.
* @param extensionName the name of the extension requesting the path
* @returns path to the folder that the extension can safely write files to.
*/
async requestDirectory(extensionName: string): Promise<string> {
if (!this.registeredExtensions.has(extensionName)) {
const salt = randomBytes(32).toString("hex");
const hashedName = SHA256(`${extensionName}/${salt}`).toString();
const dirPath = path.resolve(app.getPath("userData"), "extension_data", hashedName);
this.registeredExtensions.set(extensionName, dirPath);
}
const dirPath = this.registeredExtensions.get(extensionName);
await fse.ensureDir(dirPath);
return dirPath;
}
@action
protected fromStore({ extensions }: FSProvisionModel = { extensions: {} }): void {
this.registeredExtensions.merge(extensions);
}
toJSON(): FSProvisionModel {
return toJS({
extensions: this.registeredExtensions.toJSON(),
}, {
recurseEverything: true
});
}
}
export const filesystemProvisionerStore = FilesystemProvisionerStore.getInstance<FilesystemProvisionerStore>();

View File

@ -25,6 +25,7 @@ import { extensionsStore } from "../extensions/extensions-store";
import { InstalledExtension, extensionDiscovery } from "../extensions/extension-discovery"; import { InstalledExtension, extensionDiscovery } from "../extensions/extension-discovery";
import type { LensExtensionId } from "../extensions/lens-extension"; import type { LensExtensionId } from "../extensions/lens-extension";
import { installDeveloperTools } from "./developer-tools"; import { installDeveloperTools } from "./developer-tools";
import { filesystemProvisionerStore } from "./extension-filesystem";
const workingDir = path.join(app.getPath("appData"), appName); const workingDir = path.join(app.getPath("appData"), appName);
let proxyPort: number; let proxyPort: number;
@ -59,6 +60,7 @@ app.on("ready", async () => {
clusterStore.load(), clusterStore.load(),
workspaceStore.load(), workspaceStore.load(),
extensionsStore.load(), extensionsStore.load(),
filesystemProvisionerStore.load(),
]); ]);
// find free port // find free port

View File

@ -15,6 +15,7 @@ import { i18nStore } from "./i18n";
import { themeStore } from "./theme.store"; import { themeStore } from "./theme.store";
import { extensionsStore } from "../extensions/extensions-store"; import { extensionsStore } from "../extensions/extensions-store";
import { extensionLoader } from "../extensions/extension-loader"; import { extensionLoader } from "../extensions/extension-loader";
import { filesystemProvisionerStore } from "../main/extension-filesystem";
type AppComponent = React.ComponentType & { type AppComponent = React.ComponentType & {
init?(): Promise<void>; init?(): Promise<void>;
@ -39,6 +40,7 @@ export async function bootstrap(App: AppComponent) {
workspaceStore.load(), workspaceStore.load(),
clusterStore.load(), clusterStore.load(),
extensionsStore.load(), extensionsStore.load(),
filesystemProvisionerStore.load(),
i18nStore.init(), i18nStore.init(),
themeStore.init(), themeStore.init(),
]); ]);