From 3847a917586c478b04e7cff1134f627dd0337abb Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Wed, 9 Jun 2021 16:24:58 +0300 Subject: [PATCH] Initial support for validating extension engines.lens version (#2884) --- src/common/vars.ts | 3 ++ .../__tests__/extension-discovery.test.ts | 1 + .../__tests__/lens-extension.test.ts | 3 +- src/extensions/extension-discovery.ts | 16 ++++-- src/extensions/extension-installer.ts | 9 +--- src/extensions/extension-loader.ts | 2 +- src/extensions/lens-extension.ts | 7 ++- .../__tests__/page-registry.test.ts | 6 ++- .../protocol-handler/__test__/router.test.ts | 4 ++ .../+extensions/__tests__/extensions.test.tsx | 3 +- .../installed-extensions.module.css | 6 ++- .../+extensions/installed-extensions.tsx | 53 +++++++++++-------- 12 files changed, 70 insertions(+), 43 deletions(-) diff --git a/src/common/vars.ts b/src/common/vars.ts index b8aa69e4d5..a49c8da910 100644 --- a/src/common/vars.ts +++ b/src/common/vars.ts @@ -21,6 +21,7 @@ // App's common configuration for any process (main, renderer, build pipeline, etc.) import path from "path"; +import { SemVer } from "semver"; import packageInfo from "../../package.json"; 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 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 appSemVer = new SemVer(packageInfo.version); export const docsUrl = `https://docs.k8slens.dev/main/` as string; diff --git a/src/extensions/__tests__/extension-discovery.test.ts b/src/extensions/__tests__/extension-discovery.test.ts index 5663a1c13a..347487e556 100644 --- a/src/extensions/__tests__/extension-discovery.test.ts +++ b/src/extensions/__tests__/extension-discovery.test.ts @@ -93,6 +93,7 @@ describe("ExtensionDiscovery", () => { id: path.normalize("node_modules/my-extension/package.json"), isBundled: false, isEnabled: false, + isCompatible: false, manifest: { name: "my-extension", }, diff --git a/src/extensions/__tests__/lens-extension.test.ts b/src/extensions/__tests__/lens-extension.test.ts index 57ae8b8a68..c9c52d1637 100644 --- a/src/extensions/__tests__/lens-extension.test.ts +++ b/src/extensions/__tests__/lens-extension.test.ts @@ -38,7 +38,8 @@ describe("lens extension", () => { absolutePath: "/absolute/fake/", manifestPath: "/this/is/fake/package.json", isBundled: false, - isEnabled: true + isEnabled: true, + isCompatible: true }); }); diff --git a/src/extensions/extension-discovery.ts b/src/extensions/extension-discovery.ts index 829e8a617f..bfe8dc1536 100644 --- a/src/extensions/extension-discovery.ts +++ b/src/extensions/extension-discovery.ts @@ -30,10 +30,13 @@ import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } fr import { Singleton, toJS } from "../common/utils"; import logger from "../main/logger"; 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 { ExtensionLoader } from "./extension-loader"; 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"; export interface InstalledExtension { @@ -48,6 +51,7 @@ export interface InstalledExtension { // Absolute to the symlinked package.json file readonly manifestPath: string; readonly isBundled: boolean; // defined in project root's package.json + readonly isCompatible: boolean; isEnabled: boolean; } @@ -349,12 +353,17 @@ export class ExtensionDiscovery extends Singleton { */ protected async getByManifest(manifestPath: string, { isBundled = false } = {}): Promise { try { - const manifest = await fse.readJson(manifestPath); + const manifest = await fse.readJson(manifestPath) as LensExtensionManifest; const installedManifestPath = this.getInstalledManifestPath(manifest.name); const isEnabled = isBundled || ExtensionsStore.getInstance().isEnabled(installedManifestPath); const extensionDir = path.dirname(manifestPath); const npmPackage = path.join(extensionDir, `${manifest.name}-${manifest.version}.tgz`); const absolutePath = (isProduction && await fse.pathExists(npmPackage)) ? npmPackage : extensionDir; + let isCompatible = isBundled; + + if (manifest.engines?.lens) { + isCompatible = semver.satisfies(appSemVer, manifest.engines.lens); + } return { id: installedManifestPath, @@ -362,7 +371,8 @@ export class ExtensionDiscovery extends Singleton { manifestPath: installedManifestPath, manifest, isBundled, - isEnabled + isEnabled, + isCompatible }; } catch (error) { if (error.code === "ENOTDIR") { diff --git a/src/extensions/extension-installer.ts b/src/extensions/extension-installer.ts index 797da91c5f..57e67183bf 100644 --- a/src/extensions/extension-installer.ts +++ b/src/extensions/extension-installer.ts @@ -25,17 +25,10 @@ import fs from "fs-extra"; import path from "path"; import logger from "../main/logger"; import { extensionPackagesRoot } from "./extension-loader"; +import type { PackageJson } from "type-fest"; 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 diff --git a/src/extensions/extension-loader.ts b/src/extensions/extension-loader.ts index da2669550f..af85d11e7f 100644 --- a/src/extensions/extension-loader.ts +++ b/src/extensions/extension-loader.ts @@ -301,7 +301,7 @@ export class ExtensionLoader extends Singleton { for (const [extId, extension] of installedExtensions) { const alreadyInit = this.instances.has(extId); - if (extension.isEnabled && !alreadyInit) { + if (extension.isCompatible && extension.isEnabled && !alreadyInit) { try { const LensExtensionClass = this.requireExtension(extension); diff --git a/src/extensions/lens-extension.ts b/src/extensions/lens-extension.ts index f3910308fd..ac77a3b229 100644 --- a/src/extensions/lens-extension.ts +++ b/src/extensions/lens-extension.ts @@ -24,18 +24,17 @@ import { action, observable, makeObservable } from "mobx"; import { FilesystemProvisionerStore } from "../main/extension-filesystem"; import logger from "../main/logger"; import type { ProtocolHandlerRegistration } from "./registries"; +import type { PackageJson } from "type-fest"; import { Disposer, disposer } from "../common/utils"; export type LensExtensionId = string; // path to manifest (package.json) export type LensExtensionConstructor = new (...args: ConstructorParameters) => LensExtension; -export interface LensExtensionManifest { +export interface LensExtensionManifest extends PackageJson { name: string; version: string; - description?: string; main?: string; // path to %ext/dist/main.js renderer?: string; // path to %ext/dist/renderer.js - lens?: object; // fixme: add more required fields for validation } export const Disposers = Symbol(); @@ -91,7 +90,7 @@ export class LensExtension { try { await this.onActivate(); this.isEnabled = true; - + this[Disposers].push(...await register(this)); logger.info(`[EXTENSION]: enabled ${this.name}@${this.version}`); } catch (error) { diff --git a/src/extensions/registries/__tests__/page-registry.test.ts b/src/extensions/registries/__tests__/page-registry.test.ts index 8c6582b437..497d92e723 100644 --- a/src/extensions/registries/__tests__/page-registry.test.ts +++ b/src/extensions/registries/__tests__/page-registry.test.ts @@ -40,7 +40,8 @@ describe("getPageUrl", () => { absolutePath: "/absolute/fake/", manifestPath: "/this/is/fake/package.json", isBundled: false, - isEnabled: true + isEnabled: true, + isCompatible: true }); globalPageRegistry.add({ id: "page-with-params", @@ -107,7 +108,8 @@ describe("globalPageRegistry", () => { absolutePath: "/absolute/fake/", manifestPath: "/this/is/fake/package.json", isBundled: false, - isEnabled: true + isEnabled: true, + isCompatible: true }); globalPageRegistry.add([ { diff --git a/src/main/protocol-handler/__test__/router.test.ts b/src/main/protocol-handler/__test__/router.test.ts index 597be0c124..6c1d9e777a 100644 --- a/src/main/protocol-handler/__test__/router.test.ts +++ b/src/main/protocol-handler/__test__/router.test.ts @@ -87,6 +87,7 @@ describe("protocol router tests", () => { }, isBundled: false, isEnabled: true, + isCompatible: true, absolutePath: "/foo/bar", }); const lpr = LensProtocolRouterMain.getInstance(); @@ -165,6 +166,7 @@ describe("protocol router tests", () => { }, isBundled: false, isEnabled: true, + isCompatible: true, absolutePath: "/foo/bar", }); @@ -206,6 +208,7 @@ describe("protocol router tests", () => { }, isBundled: false, isEnabled: true, + isCompatible: true, absolutePath: "/foo/bar", }); @@ -230,6 +233,7 @@ describe("protocol router tests", () => { }, isBundled: false, isEnabled: true, + isCompatible: true, absolutePath: "/foo/bar", }); diff --git a/src/renderer/components/+extensions/__tests__/extensions.test.tsx b/src/renderer/components/+extensions/__tests__/extensions.test.tsx index e092949ab7..3361362b4b 100644 --- a/src/renderer/components/+extensions/__tests__/extensions.test.tsx +++ b/src/renderer/components/+extensions/__tests__/extensions.test.tsx @@ -76,7 +76,8 @@ describe("Extensions", () => { absolutePath: "/absolute/path", manifestPath: "/symlinked/path/package.json", isBundled: false, - isEnabled: true + isEnabled: true, + isCompatible: true }); }); diff --git a/src/renderer/components/+extensions/installed-extensions.module.css b/src/renderer/components/+extensions/installed-extensions.module.css index 831ece9024..b685f029fe 100644 --- a/src/renderer/components/+extensions/installed-extensions.module.css +++ b/src/renderer/components/+extensions/installed-extensions.module.css @@ -11,6 +11,10 @@ color: var(--colorOk); } +.invalid { + color: var(--colorWarning); +} + .title { margin-bottom: 0!important; } @@ -22,4 +26,4 @@ .frozenRow { @apply opacity-30 pointer-events-none; -} \ No newline at end of file +} diff --git a/src/renderer/components/+extensions/installed-extensions.tsx b/src/renderer/components/+extensions/installed-extensions.tsx index f896f410fd..0da09e5ec9 100644 --- a/src/renderer/components/+extensions/installed-extensions.tsx +++ b/src/renderer/components/+extensions/installed-extensions.tsx @@ -39,14 +39,18 @@ interface Props { uninstall: (extension: InstalledExtension) => void; } -function getStatus(isEnabled: boolean) { - return isEnabled ? "Enabled" : "Disabled"; +function getStatus(extension: InstalledExtension) { + if (!extension.isCompatible) { + return "Incompatible"; + } + + return extension.isEnabled ? "Enabled" : "Disabled"; } export const InstalledExtensions = observer(({ extensions, uninstall, enable, disable }: Props) => { const filters = [ (extension: InstalledExtension) => extension.manifest.name, - (extension: InstalledExtension) => getStatus(extension.isEnabled), + (extension: InstalledExtension) => getStatus(extension), (extension: InstalledExtension) => extension.manifest.version, ]; @@ -87,7 +91,7 @@ export const InstalledExtensions = observer(({ extensions, uninstall, enable, di const data = useMemo( () => { return extensions.map(extension => { - const { id, isEnabled, manifest } = extension; + const { id, isEnabled, isCompatible, manifest } = extension; const { name, description, version } = manifest; const isUninstalling = ExtensionInstallationStateStore.isExtensionUninstalling(id); @@ -102,29 +106,34 @@ export const InstalledExtensions = observer(({ extensions, uninstall, enable, di ), version, status: ( -
- {getStatus(isEnabled)} +
+ {getStatus(extension)}
), actions: ( - {isEnabled ? ( - disable(id)} - > - - Disable - - ) : ( - enable(id)} - > - - Enable - + { isCompatible && ( + <> + {isEnabled ? ( + disable(id)} + > + + Disable + + ) : ( + enable(id)} + > + + Enable + + )} + )} + uninstall(extension)}