diff --git a/src/extensions/__tests__/extension-discovery.test.ts b/src/extensions/__tests__/extension-discovery.test.ts index 0317319329..d0066c3a7e 100644 --- a/src/extensions/__tests__/extension-discovery.test.ts +++ b/src/extensions/__tests__/extension-discovery.test.ts @@ -10,7 +10,7 @@ jest.mock("chokidar", () => ({ jest.mock("../extension-installer", () => ({ extensionInstaller: { extensionPackagesRoot: "", - installPackages: jest.fn() + installPackage: jest.fn() } })); @@ -41,7 +41,7 @@ describe("ExtensionDiscovery", () => { // Need to force isLoaded to be true so that the file watching is started extensionDiscovery.isLoaded = true; - await extensionDiscovery.initMain(); + await extensionDiscovery.watchExtensions(); extensionDiscovery.events.on("add", (extension: InstalledExtension) => { expect(extension).toEqual({ @@ -81,7 +81,7 @@ describe("ExtensionDiscovery", () => { // Need to force isLoaded to be true so that the file watching is started extensionDiscovery.isLoaded = true; - await extensionDiscovery.initMain(); + await extensionDiscovery.watchExtensions(); const onAdd = jest.fn(); diff --git a/src/extensions/extension-discovery.ts b/src/extensions/extension-discovery.ts index 7d0da112bb..0994f89995 100644 --- a/src/extensions/extension-discovery.ts +++ b/src/extensions/extension-discovery.ts @@ -55,6 +55,7 @@ export class ExtensionDiscovery { protected bundledFolderPath: string; private loadStarted = false; + private extensions: Map = new Map(); // True if extensions have been loaded from the disk after app startup @observable isLoaded = false; @@ -69,13 +70,6 @@ export class ExtensionDiscovery { this.events = new EventEmitter(); } - // Each extension is added as a single dependency to this object, which is written as package.json. - // Each dependency key is the name of the dependency, and - // each dependency value is the non-symlinked path to the dependency (folder). - protected packagesJson: PackageJson = { - dependencies: {} - }; - get localFolderPath(): string { return path.join(os.homedir(), ".k8slens", "extensions"); } @@ -119,7 +113,6 @@ export class ExtensionDiscovery { } async initMain() { - this.watchExtensions(); handleRequest(ExtensionDiscovery.extensionDiscoveryChannel, () => this.toJSON()); reaction(() => this.toJSON(), () => { @@ -141,6 +134,7 @@ export class ExtensionDiscovery { watch(this.localFolderPath, { // For adding and removing symlinks to work, the depth has to be 1. depth: 1, + ignoreInitial: true, // Try to wait until the file has been completely copied. // The OS might emit an event for added file even it's not completely written to the filesysten. awaitWriteFinish: { @@ -176,8 +170,9 @@ export class ExtensionDiscovery { await this.removeSymlinkByManifestPath(manifestPath); // Install dependencies for the new extension - await this.installPackages(); + await this.installPackage(extension.absolutePath); + this.extensions.set(extension.id, extension); logger.info(`${logModule} Added extension ${extension.manifest.name}`); this.events.emit("add", extension); } @@ -197,23 +192,19 @@ export class ExtensionDiscovery { const extensionFolderName = path.basename(filePath); if (path.relative(this.localFolderPath, filePath) === extensionFolderName) { - const extensionName: string | undefined = Object - .entries(this.packagesJson.dependencies) - .find(([, extensionFolder]) => filePath === extensionFolder)?.[0]; + const extension = Array.from(this.extensions.values()).find((extension) => extension.absolutePath === filePath); + + if (extension) { + const extensionName = extension.manifest.name; - if (extensionName !== undefined) { // If the extension is deleted manually while the application is running, also remove the symlink await this.removeSymlinkByPackageName(extensionName); - delete this.packagesJson.dependencies[extensionName]; - - // Reinstall dependencies to remove the extension from package.json - await this.installPackages(); - // The path to the manifest file is the lens extension id // Note that we need to use the symlinked path - const lensExtensionId = path.join(this.nodeModulesPath, extensionName, manifestFilename); + const lensExtensionId = extension.manifestPath; + this.extensions.delete(extension.id); logger.info(`${logModule} removed extension ${extensionName}`); this.events.emit("remove", lensExtensionId as LensExtensionId); } else { @@ -296,7 +287,7 @@ export class ExtensionDiscovery { await fs.ensureDir(this.nodeModulesPath); await fs.ensureDir(this.localFolderPath); - const extensions = await this.loadExtensions(); + const extensions = await this.ensureExtensions(); this.isLoaded = true; @@ -335,7 +326,6 @@ export class ExtensionDiscovery { manifestJson = __non_webpack_require__(manifestPath); const installedManifestPath = this.getInstalledManifestPath(manifestJson.name); - this.packagesJson.dependencies[manifestJson.name] = path.dirname(manifestPath); const isEnabled = isBundled || extensionsStore.isEnabled(installedManifestPath); return { @@ -347,29 +337,46 @@ export class ExtensionDiscovery { isEnabled }; } catch (error) { - logger.error(`${logModule}: can't install extension at ${manifestPath}: ${error}`, { manifestJson }); + logger.error(`${logModule}: can't load extension manifest at ${manifestPath}: ${error}`, { manifestJson }); return null; } } - async loadExtensions(): Promise> { + async ensureExtensions(): Promise> { const bundledExtensions = await this.loadBundledExtensions(); - await this.installPackages(); // install in-tree as a separate step - const localExtensions = await this.loadFromFolder(this.localFolderPath); + await this.installBundledPackages(this.packageJsonPath, bundledExtensions); - await this.installPackages(); - const extensions = bundledExtensions.concat(localExtensions); + const userExtensions = await this.loadFromFolder(this.localFolderPath); - return new Map(extensions.map(extension => [extension.id, extension])); + for (const extension of userExtensions) { + if (await fs.pathExists(extension.manifestPath) === false) { + await this.installPackage(extension.absolutePath); + } + } + const extensions = bundledExtensions.concat(userExtensions); + + return this.extensions = new Map(extensions.map(extension => [extension.id, extension])); } /** * Write package.json to file system and install dependencies. */ - installPackages() { - return extensionInstaller.installPackages(this.packageJsonPath, this.packagesJson); + async installBundledPackages(packageJsonPath: string, extensions: InstalledExtension[]) { + const packagesJson: PackageJson = { + dependencies: {} + }; + + extensions.forEach((extension) => { + packagesJson.dependencies[extension.manifest.name] = extension.absolutePath; + }); + + return await extensionInstaller.installPackages(packageJsonPath, packagesJson); + } + + async installPackage(name: string) { + return extensionInstaller.installPackage(name); } async loadBundledExtensions() { diff --git a/src/extensions/extension-installer.ts b/src/extensions/extension-installer.ts index 75b30d0b9a..04b78bbe1a 100644 --- a/src/extensions/extension-installer.ts +++ b/src/extensions/extension-installer.ts @@ -30,12 +30,49 @@ export class ExtensionInstaller { return __non_webpack_require__.resolve("npm/bin/npm-cli"); } - installDependencies(): Promise { - return new Promise((resolve, reject) => { + /** + * Write package.json to the file system and execute npm install for it. + */ + async installPackages(packageJsonPath: string, packagesJson: PackageJson): Promise { + // Mutual exclusion to install packages in sequence + await this.installLock.acquireAsync(); + + try { + // Write the package.json which will be installed in .installDependencies() + await fs.writeFile(path.join(packageJsonPath), JSON.stringify(packagesJson, null, 2), { + mode: 0o600 + }); + logger.info(`${logModule} installing dependencies at ${extensionPackagesRoot()}`); - const child = child_process.fork(this.npmPath, ["install", "--no-audit", "--only=prod", "--prefer-offline", "--no-package-lock"], { + await this.npm(["install", "--no-audit", "--only=prod", "--prefer-offline", "--no-package-lock"]); + logger.info(`${logModule} dependencies installed at ${extensionPackagesRoot()}`); + } finally { + this.installLock.release(); + } + } + + /** + * Install single package using npm + */ + async installPackage(name: string): Promise { + // Mutual exclusion to install packages in sequence + await this.installLock.acquireAsync(); + + try { + logger.info(`${logModule} installing package from ${name} to ${extensionPackagesRoot()}`); + await this.npm(["install", "--no-audit", "--only=prod", "--prefer-offline", "--no-package-lock", "--no-save", name]); + logger.info(`${logModule} package ${name} installed to ${extensionPackagesRoot()}`); + } finally { + this.installLock.release(); + } + } + + private npm(args: string[]): Promise { + return new Promise((resolve, reject) => { + const child = child_process.fork(this.npmPath, args, { cwd: extensionPackagesRoot(), - silent: true + silent: true, + env: {} }); let stderr = ""; @@ -56,25 +93,6 @@ export class ExtensionInstaller { }); }); } - - /** - * Write package.json to the file system and execute npm install for it. - */ - async installPackages(packageJsonPath: string, packagesJson: PackageJson): Promise { - // Mutual exclusion to install packages in sequence - await this.installLock.acquireAsync(); - - try { - // Write the package.json which will be installed in .installDependencies() - await fs.writeFile(path.join(packageJsonPath), JSON.stringify(packagesJson, null, 2), { - mode: 0o600 - }); - - await this.installDependencies(); - } finally { - this.installLock.release(); - } - } } export const extensionInstaller = new ExtensionInstaller(); diff --git a/src/extensions/extension-loader.ts b/src/extensions/extension-loader.ts index 966289f157..98697d252c 100644 --- a/src/extensions/extension-loader.ts +++ b/src/extensions/extension-loader.ts @@ -12,6 +12,7 @@ import type { LensExtension, LensExtensionConstructor, LensExtensionId } from ". import type { LensMainExtension } from "./lens-main-extension"; import type { LensRendererExtension } from "./lens-renderer-extension"; import * as registries from "./registries"; +import fs from "fs"; // lazy load so that we get correct userData export function extensionPackagesRoot() { @@ -71,7 +72,7 @@ export class ExtensionLoader { } await Promise.all([this.whenLoaded, extensionsStore.whenLoaded]); - + // save state on change `extension.isEnabled` reaction(() => this.storeState, extensionsState => { extensionsStore.mergeState(extensionsState); @@ -115,7 +116,6 @@ export class ExtensionLoader { protected async initMain() { this.isLoaded = true; this.loadOnMain(); - this.broadcastExtensions(); reaction(() => this.toJSON(), () => { this.broadcastExtensions(); @@ -136,7 +136,7 @@ export class ExtensionLoader { this.syncExtensions(extensions); const receivedExtensionIds = extensions.map(([lensExtensionId]) => lensExtensionId); - + // Remove deleted extensions in renderer side only this.extensions.forEach((_, lensExtensionId) => { if (!receivedExtensionIds.includes(lensExtensionId)) { @@ -276,6 +276,12 @@ export class ExtensionLoader { } if (extEntrypoint !== "") { + if (!fs.existsSync(extEntrypoint)) { + console.log(`${logModule}: entrypoint ${extEntrypoint} not found, skipping ...`); + + return; + } + return __non_webpack_require__(extEntrypoint).default; } } catch (err) { diff --git a/src/main/cluster-detectors/distribution-detector.ts b/src/main/cluster-detectors/distribution-detector.ts index f8de88b017..73d5541d17 100644 --- a/src/main/cluster-detectors/distribution-detector.ts +++ b/src/main/cluster-detectors/distribution-detector.ts @@ -39,15 +39,27 @@ export class DistributionDetector extends BaseClusterDetector { if (this.isK0s()) { return { value: "k0s", accuracy: 80}; } + + if (this.isVMWare()) { + return { value: "vmware", accuracy: 90}; + } if (this.isMirantis()) { return { value: "mirantis", accuracy: 90}; } + if (this.isAlibaba()) { + return { value: "alibaba", accuracy: 90}; + } + if (this.isHuawei()) { return { value: "huawei", accuracy: 90}; } + if (this.isTke()) { + return { value: "tencent", accuracy: 90}; + } + if (this.isMinikube()) { return { value: "minikube", accuracy: 80}; } @@ -123,10 +135,18 @@ export class DistributionDetector extends BaseClusterDetector { return this.cluster.contextName === "docker-desktop"; } + protected isTke() { + return this.version.includes("-tke."); + } + protected isCustom() { return this.version.includes("+"); } + protected isVMWare() { + return this.version.includes("+vmware"); + } + protected isRke() { return this.version.includes("-rancher"); } @@ -138,6 +158,10 @@ export class DistributionDetector extends BaseClusterDetector { protected isK0s() { return this.version.includes("-k0s"); } + + protected isAlibaba() { + return this.version.includes("-aliyun"); + } protected isHuawei() { return this.version.includes("-CCE"); diff --git a/src/main/index.ts b/src/main/index.ts index 8da9be1a01..b0ba60d029 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -103,7 +103,6 @@ app.on("ready", async () => { } extensionLoader.init(); - extensionDiscovery.init(); windowManager = WindowManager.getInstance(proxyPort); @@ -111,6 +110,9 @@ app.on("ready", async () => { try { const extensions = await extensionDiscovery.load(); + // Start watching after bundled extensions are loaded + extensionDiscovery.watchExtensions(); + // Subscribe to extensions that are copied or deleted to/from the extensions folder extensionDiscovery.events.on("add", (extension: InstalledExtension) => { extensionLoader.addExtension(extension); @@ -122,6 +124,8 @@ app.on("ready", async () => { extensionLoader.initExtensions(extensions); } catch (error) { dialog.showErrorBox("Lens Error", `Could not load extensions${error?.message ? `: ${error.message}` : ""}`); + console.error(error); + console.trace(); } setTimeout(() => { diff --git a/src/main/kubectl.ts b/src/main/kubectl.ts index fc35f4f70d..ebfd2a6a98 100644 --- a/src/main/kubectl.ts +++ b/src/main/kubectl.ts @@ -24,7 +24,7 @@ const kubectlMap: Map = new Map([ ["1.15", "1.15.11"], ["1.16", "1.16.15"], ["1.17", bundledVersion], - ["1.18", "1.18.15"], + ["1.18", "1.18.14"], ["1.19", "1.19.5"], ["1.20", "1.20.0"] ]); diff --git a/src/main/shell-session.ts b/src/main/shell-session.ts index 19170695fc..be04649a31 100644 --- a/src/main/shell-session.ts +++ b/src/main/shell-session.ts @@ -120,6 +120,7 @@ export class ShellSession extends EventEmitter { if(path.basename(env["PTYSHELL"]) === "zsh") { env["OLD_ZDOTDIR"] = env.ZDOTDIR || env.HOME; env["ZDOTDIR"] = this.kubectlBinDir; + env["DISABLE_AUTO_UPDATE"] = "true"; } env["PTYPID"] = process.pid.toString(); diff --git a/src/renderer/components/+workloads-overview/overview.tsx b/src/renderer/components/+workloads-overview/overview.tsx index bed38f99a3..318ad53f77 100644 --- a/src/renderer/components/+workloads-overview/overview.tsx +++ b/src/renderer/components/+workloads-overview/overview.tsx @@ -14,7 +14,6 @@ import { statefulSetStore } from "../+workloads-statefulsets/statefulset.store"; import { replicaSetStore } from "../+workloads-replicasets/replicasets.store"; import { jobStore } from "../+workloads-jobs/job.store"; import { cronJobStore } from "../+workloads-cronjobs/cronjob.store"; -import { Spinner } from "../spinner"; import { Events } from "../+events"; import { KubeObjectStore } from "../../kube-object.store"; import { isAllowedResource } from "../../../common/rbac"; @@ -24,7 +23,6 @@ interface Props extends RouteComponentProps { @observer export class WorkloadsOverview extends React.Component { - @observable isReady = false; @observable isUnmounting = false; async componentDidMount() { @@ -61,10 +59,13 @@ export class WorkloadsOverview extends React.Component { if (isAllowedResource("events")) { stores.push(eventStore); } - this.isReady = stores.every(store => store.isLoaded); - await Promise.all(stores.map(store => store.loadAll())); - this.isReady = true; - const unsubscribeList = stores.map(store => store.subscribe()); + + const unsubscribeList: Array<() => void> = []; + + for (const store of stores) { + await store.loadAll(); + unsubscribeList.push(store.subscribe()); + } await when(() => this.isUnmounting); unsubscribeList.forEach(dispose => dispose()); @@ -74,11 +75,7 @@ export class WorkloadsOverview extends React.Component { this.isUnmounting = true; } - renderContents() { - if (!this.isReady) { - return ; - } - + get contents() { return ( <> @@ -94,7 +91,7 @@ export class WorkloadsOverview extends React.Component { render() { return (
- {this.renderContents()} + {this.contents}
); } diff --git a/src/renderer/components/dock/info-panel.tsx b/src/renderer/components/dock/info-panel.tsx index 4f36d47fc3..34e456fdd6 100644 --- a/src/renderer/components/dock/info-panel.tsx +++ b/src/renderer/components/dock/info-panel.tsx @@ -27,6 +27,7 @@ interface OptionalProps { showSubmitClose?: boolean; showInlineInfo?: boolean; showNotifications?: boolean; + showStatusPanel?: boolean; } @observer @@ -38,6 +39,7 @@ export class InfoPanel extends Component { showSubmitClose: true, showInlineInfo: true, showNotifications: true, + showStatusPanel: true, }; @observable error = ""; @@ -93,7 +95,7 @@ export class InfoPanel extends Component { } render() { - const { className, controls, submitLabel, disableSubmit, error, submittingMessage, showButtons, showSubmitClose } = this.props; + const { className, controls, submitLabel, disableSubmit, error, submittingMessage, showButtons, showSubmitClose, showStatusPanel } = this.props; const { submit, close, submitAndClose, waiting } = this; const isDisabled = !!(disableSubmit || waiting || error); @@ -102,9 +104,11 @@ export class InfoPanel extends Component {
{controls}
-
- {waiting ? <> {submittingMessage} : this.renderErrorIcon()} -
+ {showStatusPanel && ( +
+ {waiting ? <> {submittingMessage} : this.renderErrorIcon()} +
+ )} {showButtons && ( <>