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

Initial support for validating extension engines.lens version (#2884)

This commit is contained in:
Jari Kolehmainen 2021-06-09 16:24:58 +03:00 committed by GitHub
parent d6e72ddc1c
commit 3847a91758
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 70 additions and 43 deletions

View File

@ -21,6 +21,7 @@
// App's common configuration for any process (main, renderer, build pipeline, etc.) // App's common configuration for any process (main, renderer, build pipeline, etc.)
import path from "path"; import path from "path";
import { SemVer } from "semver";
import packageInfo from "../../package.json"; import packageInfo from "../../package.json";
import { defineGlobal } from "./utils/defineGlobal"; import { defineGlobal } from "./utils/defineGlobal";
@ -66,4 +67,6 @@ export const apiKubePrefix = "/api-kube" as string; // k8s cluster apis
export const issuesTrackerUrl = "https://github.com/lensapp/lens/issues" as string; export const issuesTrackerUrl = "https://github.com/lensapp/lens/issues" as string;
export const slackUrl = "https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI" as string; export const slackUrl = "https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI" as string;
export const supportUrl = "https://docs.k8slens.dev/latest/support/" as string; export const supportUrl = "https://docs.k8slens.dev/latest/support/" as string;
export const appSemVer = new SemVer(packageInfo.version);
export const docsUrl = `https://docs.k8slens.dev/main/` as string; export const docsUrl = `https://docs.k8slens.dev/main/` as string;

View File

@ -93,6 +93,7 @@ describe("ExtensionDiscovery", () => {
id: path.normalize("node_modules/my-extension/package.json"), id: path.normalize("node_modules/my-extension/package.json"),
isBundled: false, isBundled: false,
isEnabled: false, isEnabled: false,
isCompatible: false,
manifest: { manifest: {
name: "my-extension", name: "my-extension",
}, },

View File

@ -38,7 +38,8 @@ describe("lens extension", () => {
absolutePath: "/absolute/fake/", absolutePath: "/absolute/fake/",
manifestPath: "/this/is/fake/package.json", manifestPath: "/this/is/fake/package.json",
isBundled: false, isBundled: false,
isEnabled: true isEnabled: true,
isCompatible: true
}); });
}); });

View File

@ -30,10 +30,13 @@ import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } fr
import { Singleton, toJS } from "../common/utils"; import { Singleton, toJS } from "../common/utils";
import logger from "../main/logger"; import logger from "../main/logger";
import { ExtensionInstallationStateStore } from "../renderer/components/+extensions/extension-install.store"; import { ExtensionInstallationStateStore } from "../renderer/components/+extensions/extension-install.store";
import { extensionInstaller, PackageJson } from "./extension-installer"; import { extensionInstaller } from "./extension-installer";
import { ExtensionsStore } from "./extensions-store"; import { ExtensionsStore } from "./extensions-store";
import { ExtensionLoader } from "./extension-loader"; import { ExtensionLoader } from "./extension-loader";
import type { LensExtensionId, LensExtensionManifest } from "./lens-extension"; import type { LensExtensionId, LensExtensionManifest } from "./lens-extension";
import type { PackageJson } from "type-fest";
import semver from "semver";
import { appSemVer } from "../common/vars";
import { isProduction } from "../common/vars"; import { isProduction } from "../common/vars";
export interface InstalledExtension { export interface InstalledExtension {
@ -48,6 +51,7 @@ export interface InstalledExtension {
// Absolute to the symlinked package.json file // Absolute to the symlinked package.json file
readonly manifestPath: string; readonly manifestPath: string;
readonly isBundled: boolean; // defined in project root's package.json readonly isBundled: boolean; // defined in project root's package.json
readonly isCompatible: boolean;
isEnabled: boolean; isEnabled: boolean;
} }
@ -349,12 +353,17 @@ export class ExtensionDiscovery extends Singleton {
*/ */
protected async getByManifest(manifestPath: string, { isBundled = false } = {}): Promise<InstalledExtension | null> { protected async getByManifest(manifestPath: string, { isBundled = false } = {}): Promise<InstalledExtension | null> {
try { try {
const manifest = await fse.readJson(manifestPath); const manifest = await fse.readJson(manifestPath) as LensExtensionManifest;
const installedManifestPath = this.getInstalledManifestPath(manifest.name); const installedManifestPath = this.getInstalledManifestPath(manifest.name);
const isEnabled = isBundled || ExtensionsStore.getInstance().isEnabled(installedManifestPath); const isEnabled = isBundled || ExtensionsStore.getInstance().isEnabled(installedManifestPath);
const extensionDir = path.dirname(manifestPath); const extensionDir = path.dirname(manifestPath);
const npmPackage = path.join(extensionDir, `${manifest.name}-${manifest.version}.tgz`); const npmPackage = path.join(extensionDir, `${manifest.name}-${manifest.version}.tgz`);
const absolutePath = (isProduction && await fse.pathExists(npmPackage)) ? npmPackage : extensionDir; const absolutePath = (isProduction && await fse.pathExists(npmPackage)) ? npmPackage : extensionDir;
let isCompatible = isBundled;
if (manifest.engines?.lens) {
isCompatible = semver.satisfies(appSemVer, manifest.engines.lens);
}
return { return {
id: installedManifestPath, id: installedManifestPath,
@ -362,7 +371,8 @@ export class ExtensionDiscovery extends Singleton {
manifestPath: installedManifestPath, manifestPath: installedManifestPath,
manifest, manifest,
isBundled, isBundled,
isEnabled isEnabled,
isCompatible
}; };
} catch (error) { } catch (error) {
if (error.code === "ENOTDIR") { if (error.code === "ENOTDIR") {

View File

@ -25,17 +25,10 @@ import fs from "fs-extra";
import path from "path"; import path from "path";
import logger from "../main/logger"; import logger from "../main/logger";
import { extensionPackagesRoot } from "./extension-loader"; import { extensionPackagesRoot } from "./extension-loader";
import type { PackageJson } from "type-fest";
const logModule = "[EXTENSION-INSTALLER]"; const logModule = "[EXTENSION-INSTALLER]";
type Dependencies = {
[name: string]: string;
};
// Type for the package.json file that is written by ExtensionInstaller
export type PackageJson = {
dependencies: Dependencies;
};
/** /**
* Installs dependencies for extensions * Installs dependencies for extensions

View File

@ -301,7 +301,7 @@ export class ExtensionLoader extends Singleton {
for (const [extId, extension] of installedExtensions) { for (const [extId, extension] of installedExtensions) {
const alreadyInit = this.instances.has(extId); const alreadyInit = this.instances.has(extId);
if (extension.isEnabled && !alreadyInit) { if (extension.isCompatible && extension.isEnabled && !alreadyInit) {
try { try {
const LensExtensionClass = this.requireExtension(extension); const LensExtensionClass = this.requireExtension(extension);

View File

@ -24,18 +24,17 @@ import { action, observable, makeObservable } from "mobx";
import { FilesystemProvisionerStore } from "../main/extension-filesystem"; import { FilesystemProvisionerStore } from "../main/extension-filesystem";
import logger from "../main/logger"; import logger from "../main/logger";
import type { ProtocolHandlerRegistration } from "./registries"; import type { ProtocolHandlerRegistration } from "./registries";
import type { PackageJson } from "type-fest";
import { Disposer, disposer } from "../common/utils"; import { Disposer, disposer } from "../common/utils";
export type LensExtensionId = string; // path to manifest (package.json) export type LensExtensionId = string; // path to manifest (package.json)
export type LensExtensionConstructor = new (...args: ConstructorParameters<typeof LensExtension>) => LensExtension; export type LensExtensionConstructor = new (...args: ConstructorParameters<typeof LensExtension>) => LensExtension;
export interface LensExtensionManifest { export interface LensExtensionManifest extends PackageJson {
name: string; name: string;
version: string; version: string;
description?: string;
main?: string; // path to %ext/dist/main.js main?: string; // path to %ext/dist/main.js
renderer?: string; // path to %ext/dist/renderer.js renderer?: string; // path to %ext/dist/renderer.js
lens?: object; // fixme: add more required fields for validation
} }
export const Disposers = Symbol(); export const Disposers = Symbol();
@ -91,7 +90,7 @@ export class LensExtension {
try { try {
await this.onActivate(); await this.onActivate();
this.isEnabled = true; this.isEnabled = true;
this[Disposers].push(...await register(this)); this[Disposers].push(...await register(this));
logger.info(`[EXTENSION]: enabled ${this.name}@${this.version}`); logger.info(`[EXTENSION]: enabled ${this.name}@${this.version}`);
} catch (error) { } catch (error) {

View File

@ -40,7 +40,8 @@ describe("getPageUrl", () => {
absolutePath: "/absolute/fake/", absolutePath: "/absolute/fake/",
manifestPath: "/this/is/fake/package.json", manifestPath: "/this/is/fake/package.json",
isBundled: false, isBundled: false,
isEnabled: true isEnabled: true,
isCompatible: true
}); });
globalPageRegistry.add({ globalPageRegistry.add({
id: "page-with-params", id: "page-with-params",
@ -107,7 +108,8 @@ describe("globalPageRegistry", () => {
absolutePath: "/absolute/fake/", absolutePath: "/absolute/fake/",
manifestPath: "/this/is/fake/package.json", manifestPath: "/this/is/fake/package.json",
isBundled: false, isBundled: false,
isEnabled: true isEnabled: true,
isCompatible: true
}); });
globalPageRegistry.add([ globalPageRegistry.add([
{ {

View File

@ -87,6 +87,7 @@ describe("protocol router tests", () => {
}, },
isBundled: false, isBundled: false,
isEnabled: true, isEnabled: true,
isCompatible: true,
absolutePath: "/foo/bar", absolutePath: "/foo/bar",
}); });
const lpr = LensProtocolRouterMain.getInstance(); const lpr = LensProtocolRouterMain.getInstance();
@ -165,6 +166,7 @@ describe("protocol router tests", () => {
}, },
isBundled: false, isBundled: false,
isEnabled: true, isEnabled: true,
isCompatible: true,
absolutePath: "/foo/bar", absolutePath: "/foo/bar",
}); });
@ -206,6 +208,7 @@ describe("protocol router tests", () => {
}, },
isBundled: false, isBundled: false,
isEnabled: true, isEnabled: true,
isCompatible: true,
absolutePath: "/foo/bar", absolutePath: "/foo/bar",
}); });
@ -230,6 +233,7 @@ describe("protocol router tests", () => {
}, },
isBundled: false, isBundled: false,
isEnabled: true, isEnabled: true,
isCompatible: true,
absolutePath: "/foo/bar", absolutePath: "/foo/bar",
}); });

View File

@ -76,7 +76,8 @@ describe("Extensions", () => {
absolutePath: "/absolute/path", absolutePath: "/absolute/path",
manifestPath: "/symlinked/path/package.json", manifestPath: "/symlinked/path/package.json",
isBundled: false, isBundled: false,
isEnabled: true isEnabled: true,
isCompatible: true
}); });
}); });

View File

@ -11,6 +11,10 @@
color: var(--colorOk); color: var(--colorOk);
} }
.invalid {
color: var(--colorWarning);
}
.title { .title {
margin-bottom: 0!important; margin-bottom: 0!important;
} }
@ -22,4 +26,4 @@
.frozenRow { .frozenRow {
@apply opacity-30 pointer-events-none; @apply opacity-30 pointer-events-none;
} }

View File

@ -39,14 +39,18 @@ interface Props {
uninstall: (extension: InstalledExtension) => void; uninstall: (extension: InstalledExtension) => void;
} }
function getStatus(isEnabled: boolean) { function getStatus(extension: InstalledExtension) {
return isEnabled ? "Enabled" : "Disabled"; if (!extension.isCompatible) {
return "Incompatible";
}
return extension.isEnabled ? "Enabled" : "Disabled";
} }
export const InstalledExtensions = observer(({ extensions, uninstall, enable, disable }: Props) => { export const InstalledExtensions = observer(({ extensions, uninstall, enable, disable }: Props) => {
const filters = [ const filters = [
(extension: InstalledExtension) => extension.manifest.name, (extension: InstalledExtension) => extension.manifest.name,
(extension: InstalledExtension) => getStatus(extension.isEnabled), (extension: InstalledExtension) => getStatus(extension),
(extension: InstalledExtension) => extension.manifest.version, (extension: InstalledExtension) => extension.manifest.version,
]; ];
@ -87,7 +91,7 @@ export const InstalledExtensions = observer(({ extensions, uninstall, enable, di
const data = useMemo( const data = useMemo(
() => { () => {
return extensions.map(extension => { return extensions.map(extension => {
const { id, isEnabled, manifest } = extension; const { id, isEnabled, isCompatible, manifest } = extension;
const { name, description, version } = manifest; const { name, description, version } = manifest;
const isUninstalling = ExtensionInstallationStateStore.isExtensionUninstalling(id); const isUninstalling = ExtensionInstallationStateStore.isExtensionUninstalling(id);
@ -102,29 +106,34 @@ export const InstalledExtensions = observer(({ extensions, uninstall, enable, di
), ),
version, version,
status: ( status: (
<div className={cssNames({[styles.enabled]: getStatus(isEnabled) == "Enabled"})}> <div className={cssNames({[styles.enabled]: isEnabled, [styles.invalid]: !isCompatible})}>
{getStatus(isEnabled)} {getStatus(extension)}
</div> </div>
), ),
actions: ( actions: (
<MenuActions usePortal toolbar={false}> <MenuActions usePortal toolbar={false}>
{isEnabled ? ( { isCompatible && (
<MenuItem <>
disabled={isUninstalling} {isEnabled ? (
onClick={() => disable(id)} <MenuItem
> disabled={isUninstalling}
<Icon material="unpublished"/> onClick={() => disable(id)}
<span className="title" aria-disabled={isUninstalling}>Disable</span> >
</MenuItem> <Icon material="unpublished"/>
) : ( <span className="title" aria-disabled={isUninstalling}>Disable</span>
<MenuItem </MenuItem>
disabled={isUninstalling} ) : (
onClick={() => enable(id)} <MenuItem
> disabled={isUninstalling}
<Icon material="check_circle"/> onClick={() => enable(id)}
<span className="title" aria-disabled={isUninstalling}>Enable</span> >
</MenuItem> <Icon material="check_circle"/>
<span className="title" aria-disabled={isUninstalling}>Enable</span>
</MenuItem>
)}
</>
)} )}
<MenuItem <MenuItem
disabled={isUninstalling} disabled={isUninstalling}
onClick={() => uninstall(extension)} onClick={() => uninstall(extension)}