From cda8c6b06289445b9e6d2d17d88d6f9ef4024dd3 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Tue, 16 Feb 2021 13:11:25 -0500 Subject: [PATCH 01/37] return undefined instead of localhost from getClusterIdFromHost (#2143) * return undefined instead of localhost from getClusterIdFromHost Signed-off-by: Sebastian Malton --- src/common/__tests__/cluster-store.test.ts | 26 +++++++++++++++++++++- src/common/cluster-store.ts | 9 ++++---- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/common/__tests__/cluster-store.test.ts b/src/common/__tests__/cluster-store.test.ts index fcbd5ebd6b..6ee34388a2 100644 --- a/src/common/__tests__/cluster-store.test.ts +++ b/src/common/__tests__/cluster-store.test.ts @@ -2,7 +2,7 @@ import fs from "fs"; import mockFs from "mock-fs"; import yaml from "js-yaml"; import { Cluster } from "../../main/cluster"; -import { ClusterStore } from "../cluster-store"; +import { ClusterStore, getClusterIdFromHost } from "../cluster-store"; import { workspaceStore } from "../workspace-store"; const testDataIcon = fs.readFileSync("test-data/cluster-store-migration-icon.png"); @@ -446,3 +446,27 @@ describe("pre 3.6.0-beta.1 config with an existing cluster", () => { expect(icon.startsWith("data:;base64,")).toBe(true); }); }); + +describe("getClusterIdFromHost", () => { + const clusterFakeId = "fe540901-0bd6-4f6c-b472-bce1559d7c4a"; + + it("should return undefined for non cluster frame hosts", () => { + expect(getClusterIdFromHost("localhost:45345")).toBeUndefined(); + }); + + it("should return ClusterId for cluster frame hosts", () => { + expect(getClusterIdFromHost(`${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId); + }); + + it("should return ClusterId for cluster frame hosts with additional subdomains", () => { + expect(getClusterIdFromHost(`abc.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId); + expect(getClusterIdFromHost(`abc.def.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId); + expect(getClusterIdFromHost(`abc.def.ghi.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId); + expect(getClusterIdFromHost(`abc.def.ghi.jkl.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId); + expect(getClusterIdFromHost(`abc.def.ghi.jkl.mno.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId); + expect(getClusterIdFromHost(`abc.def.ghi.jkl.mno.pqr.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId); + expect(getClusterIdFromHost(`abc.def.ghi.jkl.mno.pqr.stu.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId); + expect(getClusterIdFromHost(`abc.def.ghi.jkl.mno.pqr.stu.vwx.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId); + expect(getClusterIdFromHost(`abc.def.ghi.jkl.mno.pqr.stu.vwx.yz.${clusterFakeId}.localhost:59110`)).toBe(clusterFakeId); + }); +}); diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index d8bd28f1e8..4000684d16 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store.ts @@ -354,10 +354,11 @@ export class ClusterStore extends BaseStore { export const clusterStore = ClusterStore.getInstance(); -export function getClusterIdFromHost(hostname: string): ClusterId { - const subDomains = hostname.split(":")[0].split("."); +export function getClusterIdFromHost(host: string): ClusterId | undefined { + // e.g host == "%clusterId.localhost:45345" + const subDomains = host.split(":")[0].split("."); - return subDomains.slice(-2)[0]; // e.g host == "%clusterId.localhost:45345" + return subDomains.slice(-2, -1)[0]; // ClusterId or undefined } export function getClusterFrameUrl(clusterId: ClusterId) { @@ -365,7 +366,7 @@ export function getClusterFrameUrl(clusterId: ClusterId) { } export function getHostedClusterId() { - return getClusterIdFromHost(location.hostname); + return getClusterIdFromHost(location.host); } export function getHostedCluster(): Cluster { From cd049b74ae8dd7c05db7192ecb25ae098eea68a4 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 17 Feb 2021 13:39:27 +0200 Subject: [PATCH 02/37] fix: keep last selected row during navigation in the details panel (#2165) Signed-off-by: Roman --- .../kube-object/kube-object-details.tsx | 30 ++++++++++++------- src/renderer/navigation/page-param.ts | 2 +- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/renderer/components/kube-object/kube-object-details.tsx b/src/renderer/components/kube-object/kube-object-details.tsx index 40a247c2d8..b07b0753fb 100644 --- a/src/renderer/components/kube-object/kube-object-details.tsx +++ b/src/renderer/components/kube-object/kube-object-details.tsx @@ -13,11 +13,21 @@ import { CrdResourceDetails } from "../+custom-resources"; import { KubeObjectMenu } from "./kube-object-menu"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; +/** + * Used to store `object.selfLink` to show more info about resource in the details panel. + */ export const kubeDetailsUrlParam = createPageParam({ name: "kube-details", isSystem: true, }); +/** + * Used to highlight last active/selected table row with the resource. + * + * @example + * If we go to "Nodes (page) -> Node (details) -> Pod (details)", + * last clicked Node should be "active" while Pod details are shown). + */ export const kubeSelectedUrlParam = createPageParam({ name: "kube-selected", isSystem: true, @@ -26,8 +36,8 @@ export const kubeSelectedUrlParam = createPageParam({ } }); -export function showDetails(details = "", resetSelected = true) { - const detailsUrl = getDetailsUrl(details, resetSelected); +export function showDetails(selfLink = "", resetSelected = true) { + const detailsUrl = getDetailsUrl(selfLink, resetSelected); navigation.merge({ search: detailsUrl }); } @@ -36,18 +46,18 @@ export function hideDetails() { showDetails(); } -export function getDetailsUrl(details: string, resetSelected = false) { - const detailsUrl = kubeDetailsUrlParam.toSearchString({ value: details }); +export function getDetailsUrl(selfLink: string, resetSelected = false, mergeGlobals = true) { + const params = new URLSearchParams(mergeGlobals ? navigation.searchParams : ""); + + params.set(kubeDetailsUrlParam.urlName, selfLink); if (resetSelected) { - const params = new URLSearchParams(detailsUrl); - - params.delete(kubeSelectedUrlParam.name); - - return `?${params.toString()}`; + params.delete(kubeSelectedUrlParam.urlName); + } else { + params.set(kubeSelectedUrlParam.urlName, kubeSelectedUrlParam.get()); } - return detailsUrl; + return `?${params}`; } export interface KubeObjectDetailsProps { diff --git a/src/renderer/navigation/page-param.ts b/src/renderer/navigation/page-param.ts index e73da03c44..50f6008738 100644 --- a/src/renderer/navigation/page-param.ts +++ b/src/renderer/navigation/page-param.ts @@ -20,7 +20,7 @@ export class PageParam { static SYSTEM_PREFIX = "lens-"; readonly name: string; - protected urlName: string; + readonly urlName: string; constructor(readonly init: PageParamInit | PageSystemParamInit, protected history: IObservableHistory) { const { isSystem, name } = init as PageSystemParamInit; From 5696ba3386ba5438b060fb62e04ede64dde8f157 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Wed, 17 Feb 2021 22:28:33 +0200 Subject: [PATCH 03/37] Fix isLoadingAll within KubeObjectStore.loadItems (#2174) * fix isLoadingAll within KubeObjectStore.loadItems Signed-off-by: Jari Kolehmainen * fix isLoadingAll within KubeObjectStore.loadItems Signed-off-by: Jari Kolehmainen --- src/renderer/kube-object.store.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/renderer/kube-object.store.ts b/src/renderer/kube-object.store.ts index 023048cb73..987112a25c 100644 --- a/src/renderer/kube-object.store.ts +++ b/src/renderer/kube-object.store.ts @@ -109,9 +109,11 @@ export abstract class KubeObjectStore extends ItemSt return api.list({}, this.query); } - const isLoadingAll = this.context.allNamespaces.every(ns => namespaces.includes(ns)); + const isLoadingAll = this.context.allNamespaces?.length > 1 + && this.context.cluster.accessibleNamespaces.length === 0 + && this.context.allNamespaces.every(ns => namespaces.includes(ns)); - if (isLoadingAll && this.context.cluster.accessibleNamespaces.length === 0) { + if (isLoadingAll) { this.loadedNamespaces = []; return api.list({}, this.query); From 88b4561112947e428266fb77b7777877fd010993 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Thu, 18 Feb 2021 07:30:48 +0200 Subject: [PATCH 04/37] Windows: use SHELL for terminal if set (#2168) Signed-off-by: Jari Kolehmainen --- src/main/shell-session.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/shell-session.ts b/src/main/shell-session.ts index be04649a31..9e5af371f7 100644 --- a/src/main/shell-session.ts +++ b/src/main/shell-session.ts @@ -108,7 +108,7 @@ export class ShellSession extends EventEmitter { if(isWindows) { env["SystemRoot"] = process.env.SystemRoot; - env["PTYSHELL"] = "powershell.exe"; + env["PTYSHELL"] = process.env.SHELL || "powershell.exe"; env["PATH"] = pathStr; } else if(typeof(process.env.SHELL) != "undefined") { env["PTYSHELL"] = process.env.SHELL; From a5fb28c29e7bbff8b64712d6fe86adcc288daad6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Feb 2021 08:36:29 +0200 Subject: [PATCH 05/37] Bump xterm from 4.6.0 to 4.10.0 (#2072) Bumps [xterm](https://github.com/xtermjs/xterm.js) from 4.6.0 to 4.10.0. - [Release notes](https://github.com/xtermjs/xterm.js/releases) - [Commits](https://github.com/xtermjs/xterm.js/compare/4.6.0...4.10.0) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index c76cdc2835..793ca9de47 100644 --- a/package.json +++ b/package.json @@ -359,7 +359,7 @@ "webpack-dev-server": "^3.11.0", "webpack-node-externals": "^1.7.2", "what-input": "^5.2.10", - "xterm": "^4.6.0", + "xterm": "^4.10.0", "xterm-addon-fit": "^0.4.0" } } diff --git a/yarn.lock b/yarn.lock index 16ac2ca4f8..2cc97b2383 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14384,10 +14384,10 @@ xterm-addon-fit@^0.4.0: resolved "https://registry.yarnpkg.com/xterm-addon-fit/-/xterm-addon-fit-0.4.0.tgz#06e0c5d0a6aaacfb009ef565efa1c81e93d90193" integrity sha512-p4BESuV/g2L6pZzFHpeNLLnep9mp/DkF3qrPglMiucSFtD8iJxtMufEoEJbN8LZwB4i+8PFpFvVuFrGOSpW05w== -xterm@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.6.0.tgz#1b49b32e546409c110fbe8ece0b4a388504a937d" - integrity sha512-98211RIDrAECqpsxs6gbilwMcxLtxSDIvtzZUIqP1xIByXtuccJ4pmMhHGJATZeEGe/reARPMqwPINK8T7jGZg== +xterm@^4.10.0: + version "4.10.0" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.10.0.tgz#fc4f554e3e718aff9b83622e858e64b0953067bb" + integrity sha512-Wn66I8YpSVkgP3R95GjABC6Eb21pFfnCSnyIqKIIoUI13ohvwd0KGVzUDfyEFfSAzKbPJfrT2+vt7SfUXBZQKQ== y18n@^3.2.1: version "3.2.1" From a8701d6ec7c42d044a5761ff3309a1a3d6a863b2 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Thu, 18 Feb 2021 08:44:52 +0200 Subject: [PATCH 06/37] v4.1.1 Signed-off-by: Jari Kolehmainen --- package.json | 2 +- static/RELEASE_NOTES.md | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 793ca9de47..9a939ae48b 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "kontena-lens", "productName": "Lens", "description": "Lens - The Kubernetes IDE", - "version": "4.1.0", + "version": "4.1.1", "main": "static/build/main.js", "copyright": "© 2020, Mirantis, Inc.", "license": "MIT", diff --git a/static/RELEASE_NOTES.md b/static/RELEASE_NOTES.md index 8c43fcda18..add0d44ab3 100644 --- a/static/RELEASE_NOTES.md +++ b/static/RELEASE_NOTES.md @@ -2,10 +2,16 @@ Here you can find description of changes we've built into each release. While we try our best to make each upgrade automatic and as smooth as possible, there may be some cases where you might need to do something to ensure the application works smoothly. So please read through the release highlights! -## 4.1.0 (current version) +## 4.1.1 (current version) **Upgrade note:** Where have all my pods gone? Namespaced Kubernetes resources are now initially shown only for the "default" namespace. Use the namespaces selector to add more. +- Fix an issue where users with rights to a single namespace were seeing an empty dashboard +- Windows: use SHELL for terminal if set +- Keep highlighted table row during navigation in the details panel + +## 4.1.0 + - Change: list views default to a namespace (instead of listing resources from all namespaces) - Command palette - Generic logs view with Pod selector From 5bd8f452b3a8d19a572063fc47e753cf1883207a Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Thu, 18 Feb 2021 13:36:35 +0200 Subject: [PATCH 07/37] Trigger quitAndInstall after update-downloaded event (#2181) Signed-off-by: Jari Kolehmainen --- src/main/app-updater.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/app-updater.ts b/src/main/app-updater.ts index 618f714b49..5d9509c6e4 100644 --- a/src/main/app-updater.ts +++ b/src/main/app-updater.ts @@ -9,9 +9,8 @@ function handleAutoUpdateBackChannel(event: Electron.IpcMainEvent, ...[arg]: Upd if (arg.doUpdate) { if (arg.now) { logger.info(`${AutoUpdateLogPrefix}: User chose to update now`); - autoUpdater.downloadUpdate() - .then(() => autoUpdater.quitAndInstall()) - .catch(error => logger.error(`${AutoUpdateLogPrefix}: Failed to download or install update`, { error })); + autoUpdater.on("update-downloaded", () => autoUpdater.quitAndInstall()); + autoUpdater.downloadUpdate().catch(error => logger.error(`${AutoUpdateLogPrefix}: Failed to download or install update`, { error })); } else { logger.info(`${AutoUpdateLogPrefix}: User chose to update on quit`); autoUpdater.autoInstallOnAppQuit = true; From 2d1183ff7b02bfdf9d5167cd0815217ad6ed75a9 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Thu, 18 Feb 2021 15:11:03 +0200 Subject: [PATCH 08/37] Wait for cluster to be ready before loading iframe (#2183) Signed-off-by: Jari Kolehmainen --- src/renderer/components/cluster-manager/lens-views.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/renderer/components/cluster-manager/lens-views.ts b/src/renderer/components/cluster-manager/lens-views.ts index da7efb377c..6e9ba6df6a 100644 --- a/src/renderer/components/cluster-manager/lens-views.ts +++ b/src/renderer/components/cluster-manager/lens-views.ts @@ -24,6 +24,9 @@ export async function initView(clusterId: ClusterId) { if (!cluster) { return; } + + logger.info(`[LENS-VIEW]: waiting cluster to be ready, clusterId=${clusterId}`); + await cluster.whenReady; logger.info(`[LENS-VIEW]: init dashboard, clusterId=${clusterId}`); const parentElem = document.getElementById("lens-views"); const iframe = document.createElement("iframe"); From d2ae2a5c326691c98bca0cbe5818100102421188 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Thu, 18 Feb 2021 19:20:58 +0200 Subject: [PATCH 09/37] Fix duplicate cluster iframes (#2190) Signed-off-by: Jari Kolehmainen --- src/renderer/components/cluster-manager/lens-views.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/renderer/components/cluster-manager/lens-views.ts b/src/renderer/components/cluster-manager/lens-views.ts index 6e9ba6df6a..a3a92e4f64 100644 --- a/src/renderer/components/cluster-manager/lens-views.ts +++ b/src/renderer/components/cluster-manager/lens-views.ts @@ -25,8 +25,6 @@ export async function initView(clusterId: ClusterId) { return; } - logger.info(`[LENS-VIEW]: waiting cluster to be ready, clusterId=${clusterId}`); - await cluster.whenReady; logger.info(`[LENS-VIEW]: init dashboard, clusterId=${clusterId}`); const parentElem = document.getElementById("lens-views"); const iframe = document.createElement("iframe"); @@ -39,6 +37,8 @@ export async function initView(clusterId: ClusterId) { }, { once: true }); lensViews.set(clusterId, { clusterId, view: iframe }); parentElem.appendChild(iframe); + logger.info(`[LENS-VIEW]: waiting cluster to be ready, clusterId=${clusterId}`); + await cluster.whenReady; await autoCleanOnRemove(clusterId, iframe); } From 79a65b1df21b02a282de4a0f0c45b4b05a9b6804 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Thu, 18 Feb 2021 15:32:19 +0200 Subject: [PATCH 10/37] v4.1.2 Signed-off-by: Jari Kolehmainen --- package.json | 2 +- static/RELEASE_NOTES.md | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 9a939ae48b..99340c44ed 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "kontena-lens", "productName": "Lens", "description": "Lens - The Kubernetes IDE", - "version": "4.1.1", + "version": "4.1.2", "main": "static/build/main.js", "copyright": "© 2020, Mirantis, Inc.", "license": "MIT", diff --git a/static/RELEASE_NOTES.md b/static/RELEASE_NOTES.md index add0d44ab3..bb855fab08 100644 --- a/static/RELEASE_NOTES.md +++ b/static/RELEASE_NOTES.md @@ -2,10 +2,15 @@ Here you can find description of changes we've built into each release. While we try our best to make each upgrade automatic and as smooth as possible, there may be some cases where you might need to do something to ensure the application works smoothly. So please read through the release highlights! -## 4.1.1 (current version) +## 4.1.2 (current version) **Upgrade note:** Where have all my pods gone? Namespaced Kubernetes resources are now initially shown only for the "default" namespace. Use the namespaces selector to add more. +- Fix an issue where a cluster gets stuck on "Connecting ..." phase +- Fix an issue with auto-update + +## 4.1.1 + - Fix an issue where users with rights to a single namespace were seeing an empty dashboard - Windows: use SHELL for terminal if set - Keep highlighted table row during navigation in the details panel From 136dac4a8ffa3f757437b1dc56c3dd1c1685198f Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 22 Feb 2021 14:44:21 +0200 Subject: [PATCH 11/37] - fix: don't reset selected "all namespaces" on page reload (#2185) - fix: don't reload namespaces on every page visit / NamespaceSelect.render() Signed-off-by: Roman --- .../+namespaces/namespace-select-filter.tsx | 2 +- .../components/+namespaces/namespace-select.tsx | 1 + .../components/+namespaces/namespace.store.ts | 12 ++++++------ 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/renderer/components/+namespaces/namespace-select-filter.tsx b/src/renderer/components/+namespaces/namespace-select-filter.tsx index 453849e534..85a6299b17 100644 --- a/src/renderer/components/+namespaces/namespace-select-filter.tsx +++ b/src/renderer/components/+namespaces/namespace-select-filter.tsx @@ -56,7 +56,7 @@ export class NamespaceSelectFilter extends React.Component { if (namespace) { namespaceStore.toggleContext(namespace); } else { - namespaceStore.resetContext(); // "All namespaces" clicked, empty list considered as "all" + namespaceStore.toggleAll(false); // "All namespaces" clicked } } diff --git a/src/renderer/components/+namespaces/namespace-select.tsx b/src/renderer/components/+namespaces/namespace-select.tsx index 207a274d2c..775cace4d3 100644 --- a/src/renderer/components/+namespaces/namespace-select.tsx +++ b/src/renderer/components/+namespaces/namespace-select.tsx @@ -29,6 +29,7 @@ export class NamespaceSelect extends React.Component { disposeOnUnmount(this, [ kubeWatchApi.subscribeStores([namespaceStore], { preload: true, + loadOnce: true, // skip reloading namespaces on every render / page visit }) ]); } diff --git a/src/renderer/components/+namespaces/namespace.store.ts b/src/renderer/components/+namespaces/namespace.store.ts index 1f928fe2f3..ad271d1302 100644 --- a/src/renderer/components/+namespaces/namespace.store.ts +++ b/src/renderer/components/+namespaces/namespace.store.ts @@ -5,7 +5,7 @@ import { Namespace, namespacesApi } from "../../api/endpoints/namespaces.api"; import { createPageParam } from "../../navigation"; import { apiManager } from "../../api/api-manager"; -const storage = createStorage("context_namespaces", []); +const storage = createStorage("context_namespaces"); export const namespaceUrlParam = createPageParam({ name: "namespaces", @@ -74,11 +74,11 @@ export class NamespaceStore extends KubeObjectStore { @computed private get initialNamespaces(): string[] { const namespaces = new Set(this.allowedNamespaces); - const prevSelected = storage.get().filter(namespace => namespaces.has(namespace)); + const prevSelectedNamespaces = storage.get(); - // return previously saved namespaces from local-storage - if (prevSelected.length > 0) { - return prevSelected; + // return previously saved namespaces from local-storage (if any) + if (prevSelectedNamespaces) { + return prevSelectedNamespaces.filter(namespace => namespaces.has(namespace)); } // otherwise select "default" or first allowed namespace @@ -166,7 +166,7 @@ export class NamespaceStore extends KubeObjectStore { if (showAll) { this.setContext(this.allowedNamespaces); } else { - this.contextNs.clear(); + this.resetContext(); // empty context considered as "All namespaces" } } else { this.toggleAll(!this.hasAllContexts); From 73cf2893aaeb6715dda6be965816e743f559a06c Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Mon, 22 Feb 2021 12:18:48 -0500 Subject: [PATCH 12/37] fix RoleBindings Namespace and Bindings field not displaying the correct data (#2203) Signed-off-by: Sebastian Malton --- .../+user-management-roles-bindings/role-bindings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/components/+user-management-roles-bindings/role-bindings.tsx b/src/renderer/components/+user-management-roles-bindings/role-bindings.tsx index f55e781e0e..9a5e079894 100644 --- a/src/renderer/components/+user-management-roles-bindings/role-bindings.tsx +++ b/src/renderer/components/+user-management-roles-bindings/role-bindings.tsx @@ -50,8 +50,8 @@ export class RoleBindings extends React.Component { renderTableContents={(binding: RoleBinding) => [ binding.getName(), , - binding.getSubjectNames(), binding.getNs() || "-", + binding.getSubjectNames(), binding.getAge(), ]} addRemoveButtons={{ From 613540670945d1e346e4aa30fd9d5123696b9d76 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 22 Feb 2021 20:29:26 +0200 Subject: [PATCH 13/37] fix: allow scrolling in main-layout's content area (#2201) Signed-off-by: Roman --- src/renderer/components/layout/main-layout.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/renderer/components/layout/main-layout.scss b/src/renderer/components/layout/main-layout.scss index 92f1173b7a..4e456e357f 100755 --- a/src/renderer/components/layout/main-layout.scss +++ b/src/renderer/components/layout/main-layout.scss @@ -44,6 +44,11 @@ > main { display: contents; + + > * { + grid-area: main; + overflow: auto; + } } footer { From d3ff3c9cc183df0e84a81d31346942ce43d97fb2 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Mon, 22 Feb 2021 16:53:34 -0500 Subject: [PATCH 14/37] Fix auto update on quit with newer version (#2128) - Fix auto update on quit with newer version - If the user specifies that Lens should auto update on quit to a specific version, and before they quit the auto-updater finds a newer version. Then disregard the previous request to update - reset broadcast-ed version if failed to broadcast Signed-off-by: Sebastian Malton --- src/main/app-updater.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/main/app-updater.ts b/src/main/app-updater.ts index 5d9509c6e4..ed2bf4250b 100644 --- a/src/main/app-updater.ts +++ b/src/main/app-updater.ts @@ -5,6 +5,8 @@ import { delay } from "../common/utils"; import { areArgsUpdateAvailableToBackchannel, AutoUpdateLogPrefix, broadcastMessage, onceCorrect, UpdateAvailableChannel, UpdateAvailableToBackchannel } from "../common/ipc"; import { ipcMain } from "electron"; +let installVersion: null | string = null; + function handleAutoUpdateBackChannel(event: Electron.IpcMainEvent, ...[arg]: UpdateAvailableToBackchannel) { if (arg.doUpdate) { if (arg.now) { @@ -37,6 +39,22 @@ export function startUpdateChecking(interval = 1000 * 60 * 60 * 24): void { autoUpdater .on("update-available", (args: UpdateInfo) => { + if (autoUpdater.autoInstallOnAppQuit) { + // a previous auto-update loop was completed with YES+LATER, check if same version + if (installVersion === args.version) { + // same version, don't broadcast + return; + } + } + + /** + * This should be always set to false here because it is the reasonable + * default. Namely, if a don't auto update to a version that the user + * didn't ask for. + */ + autoUpdater.autoInstallOnAppQuit = false; + installVersion = args.version; + try { const backchannel = `auto-update:${args.version}`; @@ -53,6 +71,7 @@ export function startUpdateChecking(interval = 1000 * 60 * 60 * 24): void { broadcastMessage(UpdateAvailableChannel, backchannel, args); } catch (error) { logger.error(`${AutoUpdateLogPrefix}: broadcasting failed`, { error }); + installVersion = undefined; } }); From d6561f4fbadf2b10cebaf4072035fc34c15c0856 Mon Sep 17 00:00:00 2001 From: Steven Martin Date: Tue, 23 Feb 2021 07:05:44 -0500 Subject: [PATCH 15/37] Fix Heading spelling (#2202) Repositories was misspelled in Managing Helm Repositories Signed-off-by: stevenGravy --- docs/helm/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/helm/README.md b/docs/helm/README.md index b46bead65c..f07bced7f0 100644 --- a/docs/helm/README.md +++ b/docs/helm/README.md @@ -4,7 +4,7 @@ Lens has integration to Helm making it easy to install and manage Helm charts an ![Helm Charts](images/helm-charts.png) -## Managing Helm Reporistories +## Managing Helm Repositories Used Helm repositories are possible to configure in the [Preferences](/getting-started/preferences). Lens app will fetch available Helm repositories from the [Artifact HUB](https://artifacthub.io/) and automatically add `bitnami` repository by default if no other repositories are already configured. If any other repositories are needed to add, those can be added manually via command line. **Note!** Configured Helm repositories are added globally to user's computer, so other processes can see those as well. @@ -18,4 +18,4 @@ Lens will list all charts from configured Helm repositries on Apps section. To i To update a Helm release, you can open the release details and modify the release values and click "Save" button. To upgrade or downgrade the release, click "Upgrade" button in the release details. In the release editor you can select a new chart version and edit the release values if needed and then click "Upgrade" or "Upgrade and Close" button. ## Deleting a Helm Release -To delete existing Helm release open the release details and click trash can icon on the top of the panel. Deletion removes all Kubernetes resources created by the Helm release. **Note!** If the release included any persistent volumes, those are required to remove manually! \ No newline at end of file +To delete existing Helm release open the release details and click trash can icon on the top of the panel. Deletion removes all Kubernetes resources created by the Helm release. **Note!** If the release included any persistent volumes, those are required to remove manually! From fd27bd6bb804e82198e630fb76daf6519de06452 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Tue, 23 Feb 2021 08:13:46 -0500 Subject: [PATCH 16/37] Fix removal of langci from repo (#2204) - Move all strings of the form {`...`} to use just "" - Fix RoleBindingDetails not rendering the name of the role binding Signed-off-by: Sebastian Malton --- .../+apps-helm-charts/helm-chart-details.tsx | 10 +++++----- .../components/+apps-helm-charts/helm-charts.tsx | 2 +- .../+apps-releases/release-details.tsx | 10 +++++----- .../components/+apps-releases/release-menu.tsx | 2 +- .../components/+cluster/cluster-pie-charts.tsx | 6 +++--- .../+config-limit-ranges/limit-range-details.tsx | 6 +++--- .../+config-resource-quotas/add-quota-dialog.tsx | 10 +++++----- .../+config-secrets/add-secret-dialog.tsx | 8 ++++---- .../+config-secrets/secret-details.tsx | 4 ++-- .../+namespaces/add-namespace-dialog.tsx | 2 +- .../components/+namespaces/namespace-details.tsx | 2 +- .../+network-policies/network-policy-details.tsx | 2 +- .../+network-services/service-details.tsx | 4 ++-- .../+network-services/service-port-component.tsx | 2 +- .../+preferences/add-helm-repo-dialog.tsx | 12 ++++++------ .../components/+preferences/preferences.tsx | 2 +- .../+storage-classes/storage-class-details.tsx | 2 +- .../volume-claim-details.tsx | 2 +- .../add-role-binding-dialog.tsx | 6 +++--- .../role-binding-details.tsx | 4 ++-- .../+user-management-roles/add-role-dialog.tsx | 2 +- .../create-service-account-dialog.tsx | 2 +- .../components/+workloads-cronjobs/cronjobs.tsx | 6 +++--- .../+workloads-deployments/deployments.tsx | 4 ++-- .../+workloads-pods/pod-container-port.tsx | 2 +- .../+workloads-replicasets/replicasets.tsx | 2 +- .../+workloads-statefulsets/statefulsets.tsx | 2 +- src/renderer/components/dock/create-resource.tsx | 2 +- src/renderer/components/dock/dock-tab.tsx | 2 +- src/renderer/components/dock/edit-resource.tsx | 4 ++-- src/renderer/components/dock/install-chart.tsx | 16 ++++++++-------- src/renderer/components/dock/log-search.tsx | 4 ++-- src/renderer/components/dock/upgrade-chart.tsx | 4 ++-- .../components/error-boundary/error-boundary.tsx | 2 +- src/renderer/components/menu/menu-actions.tsx | 4 ++-- 35 files changed, 78 insertions(+), 78 deletions(-) diff --git a/src/renderer/components/+apps-helm-charts/helm-chart-details.tsx b/src/renderer/components/+apps-helm-charts/helm-chart-details.tsx index a33e9d2503..d31efc5438 100644 --- a/src/renderer/components/+apps-helm-charts/helm-chart-details.tsx +++ b/src/renderer/components/+apps-helm-charts/helm-chart-details.tsx @@ -82,9 +82,9 @@ export class HelmChartDetails extends Component {
{selectedChart.getDescription()} -
- + this.quotaName = v.toLowerCase()} className="box grow" @@ -156,7 +156,7 @@ export class AddQuotaDialog extends React.Component { this.namespace = value} @@ -167,14 +167,14 @@ export class AddQuotaDialog extends React.Component { this.quotaInputValue = v} onKeyDown={this.onInputQuota} @@ -183,7 +183,7 @@ export class AddQuotaDialog extends React.Component {
diff --git a/src/renderer/components/+config-secrets/add-secret-dialog.tsx b/src/renderer/components/+config-secrets/add-secret-dialog.tsx index 5071c908bf..2fae17d30d 100644 --- a/src/renderer/components/+config-secrets/add-secret-dialog.tsx +++ b/src/renderer/components/+config-secrets/add-secret-dialog.tsx @@ -133,7 +133,7 @@ export class AddSecretDialog extends React.Component { this.addField(field)} /> @@ -146,7 +146,7 @@ export class AddSecretDialog extends React.Component {
{ multiLine maxRows={5} required={required} className="value" - placeholder={`Value`} + placeholder="Value" value={value} onChange={v => item.value = v} /> { this.name = v} /> diff --git a/src/renderer/components/+config-secrets/secret-details.tsx b/src/renderer/components/+config-secrets/secret-details.tsx index 92a58141ac..ab7bc59e46 100644 --- a/src/renderer/components/+config-secrets/secret-details.tsx +++ b/src/renderer/components/+config-secrets/secret-details.tsx @@ -69,7 +69,7 @@ export class SecretDetails extends React.Component { {!isEmpty(this.data) && ( <> - + { Object.entries(this.data).map(([name, value]) => { const revealSecret = this.revealSecret[name]; @@ -107,7 +107,7 @@ export class SecretDetails extends React.Component { }
diff --git a/src/renderer/components/+network-services/service-port-component.tsx b/src/renderer/components/+network-services/service-port-component.tsx index 6946137bde..77070390e0 100644 --- a/src/renderer/components/+network-services/service-port-component.tsx +++ b/src/renderer/components/+network-services/service-port-component.tsx @@ -37,7 +37,7 @@ export class ServicePortComponent extends React.Component { return (
- this.portForward() }> + this.portForward() }> {port.toString()} {this.waiting && ( diff --git a/src/renderer/components/+preferences/add-helm-repo-dialog.tsx b/src/renderer/components/+preferences/add-helm-repo-dialog.tsx index f2c8388247..a0ede38ece 100644 --- a/src/renderer/components/+preferences/add-helm-repo-dialog.tsx +++ b/src/renderer/components/+preferences/add-helm-repo-dialog.tsx @@ -111,7 +111,7 @@ export class AddHelmRepoDialog extends React.Component { <> this.helmRepo.insecureSkipTlsVerify = v} /> @@ -120,12 +120,12 @@ export class AddHelmRepoDialog extends React.Component { {this.renderFileInput(`Cerificate file`, FileType.CertFile, AddHelmRepoDialog.certExtensions)} this.helmRepo.username = v} /> this.helmRepo.password = v} /> ); @@ -148,13 +148,13 @@ export class AddHelmRepoDialog extends React.Component {
this.helmRepo.name = v} /> this.helmRepo.url = v} /> @@ -162,7 +162,7 @@ export class AddHelmRepoDialog extends React.Component { More diff --git a/src/renderer/components/+preferences/preferences.tsx b/src/renderer/components/+preferences/preferences.tsx index 56e881d9f5..b0442f45b6 100644 --- a/src/renderer/components/+preferences/preferences.tsx +++ b/src/renderer/components/+preferences/preferences.tsx @@ -122,7 +122,7 @@ export class Preferences extends React.Component {

HTTP Proxy

this.httpProxy = v} onBlur={() => preferences.httpsProxy = this.httpProxy} diff --git a/src/renderer/components/+storage-classes/storage-class-details.tsx b/src/renderer/components/+storage-classes/storage-class-details.tsx index 95f09bc6bf..4ee8a197f2 100644 --- a/src/renderer/components/+storage-classes/storage-class-details.tsx +++ b/src/renderer/components/+storage-classes/storage-class-details.tsx @@ -45,7 +45,7 @@ export class StorageClassDetails extends React.Component { )} {parameters && ( <> - + { Object.entries(parameters).map(([name, value]) => ( diff --git a/src/renderer/components/+storage-volume-claims/volume-claim-details.tsx b/src/renderer/components/+storage-volume-claims/volume-claim-details.tsx index ea1ec71778..cbf8a9d4fc 100644 --- a/src/renderer/components/+storage-volume-claims/volume-claim-details.tsx +++ b/src/renderer/components/+storage-volume-claims/volume-claim-details.tsx @@ -71,7 +71,7 @@ export class PersistentVolumeClaimDetails extends React.Component { {volumeClaim.getStatus()} - + {volumeClaim.getMatchLabels().map(label => )} diff --git a/src/renderer/components/+user-management-roles-bindings/add-role-binding-dialog.tsx b/src/renderer/components/+user-management-roles-bindings/add-role-binding-dialog.tsx index 85d27243c2..e7a676365c 100644 --- a/src/renderer/components/+user-management-roles-bindings/add-role-binding-dialog.tsx +++ b/src/renderer/components/+user-management-roles-bindings/add-role-binding-dialog.tsx @@ -205,7 +205,7 @@ export class AddRoleBindingDialog extends React.Component { this.bindingName = v} @@ -239,7 +239,7 @@ export class AddRoleBindingDialog extends React.Component { this.roleName = v} diff --git a/src/renderer/components/+user-management-service-accounts/create-service-account-dialog.tsx b/src/renderer/components/+user-management-service-accounts/create-service-account-dialog.tsx index c56888f6f4..e9a27979cf 100644 --- a/src/renderer/components/+user-management-service-accounts/create-service-account-dialog.tsx +++ b/src/renderer/components/+user-management-service-accounts/create-service-account-dialog.tsx @@ -66,7 +66,7 @@ export class CreateServiceAccountDialog extends React.Component { this.name = v.toLowerCase()} /> diff --git a/src/renderer/components/+workloads-cronjobs/cronjobs.tsx b/src/renderer/components/+workloads-cronjobs/cronjobs.tsx index 08b84c0671..5af1b5f248 100644 --- a/src/renderer/components/+workloads-cronjobs/cronjobs.tsx +++ b/src/renderer/components/+workloads-cronjobs/cronjobs.tsx @@ -87,7 +87,7 @@ export function CronJobMenu(props: KubeObjectMenuProps) { return ( <> CronJobTriggerDialog.open(object)}> - + Trigger @@ -106,7 +106,7 @@ export function CronJobMenu(props: KubeObjectMenuProps) { Resume CronJob {object.getName()}?

), })}> - + Resume @@ -124,7 +124,7 @@ export function CronJobMenu(props: KubeObjectMenuProps) { Suspend CronJob {object.getName()}?

), })}> - + Suspend } diff --git a/src/renderer/components/+workloads-deployments/deployments.tsx b/src/renderer/components/+workloads-deployments/deployments.tsx index 0147c238b9..5281307539 100644 --- a/src/renderer/components/+workloads-deployments/deployments.tsx +++ b/src/renderer/components/+workloads-deployments/deployments.tsx @@ -104,7 +104,7 @@ export function DeploymentMenu(props: KubeObjectMenuProps) { return ( <> DeploymentScaleDialog.open(object)}> - + Scale ConfirmDialog.open({ @@ -126,7 +126,7 @@ export function DeploymentMenu(props: KubeObjectMenuProps) {

), })}> - + Restart
diff --git a/src/renderer/components/+workloads-pods/pod-container-port.tsx b/src/renderer/components/+workloads-pods/pod-container-port.tsx index ce5c27c462..4f2124ec3d 100644 --- a/src/renderer/components/+workloads-pods/pod-container-port.tsx +++ b/src/renderer/components/+workloads-pods/pod-container-port.tsx @@ -43,7 +43,7 @@ export class PodContainerPort extends React.Component { return (
- this.portForward() }> + this.portForward() }> {text} {this.waiting && ( diff --git a/src/renderer/components/+workloads-replicasets/replicasets.tsx b/src/renderer/components/+workloads-replicasets/replicasets.tsx index fa6ee5cef4..160e5d1917 100644 --- a/src/renderer/components/+workloads-replicasets/replicasets.tsx +++ b/src/renderer/components/+workloads-replicasets/replicasets.tsx @@ -78,7 +78,7 @@ export function ReplicaSetMenu(props: KubeObjectMenuProps) { return ( <> ReplicaSetScaleDialog.open(object)}> - + Scale diff --git a/src/renderer/components/+workloads-statefulsets/statefulsets.tsx b/src/renderer/components/+workloads-statefulsets/statefulsets.tsx index 7c91c9905c..1438e936c7 100644 --- a/src/renderer/components/+workloads-statefulsets/statefulsets.tsx +++ b/src/renderer/components/+workloads-statefulsets/statefulsets.tsx @@ -83,7 +83,7 @@ export function StatefulSetMenu(props: KubeObjectMenuProps) { return ( <> StatefulSetScaleDialog.open(object)}> - + Scale diff --git a/src/renderer/components/dock/create-resource.tsx b/src/renderer/components/dock/create-resource.tsx index 100ef6a3ae..8ee859d2cf 100644 --- a/src/renderer/components/dock/create-resource.tsx +++ b/src/renderer/components/dock/create-resource.tsx @@ -77,7 +77,7 @@ export class CreateResource extends React.Component { tabId={tabId} error={error} submit={create} - submitLabel={`Create`} + submitLabel="Create" showNotifications={false} /> { {!pinned && ( )} diff --git a/src/renderer/components/dock/edit-resource.tsx b/src/renderer/components/dock/edit-resource.tsx index e33e379620..104f8f1ea3 100644 --- a/src/renderer/components/dock/edit-resource.tsx +++ b/src/renderer/components/dock/edit-resource.tsx @@ -98,8 +98,8 @@ export class EditResource extends React.Component { tabId={tabId} error={error} submit={save} - submitLabel={`Save`} - submittingMessage={`Applying..`} + submitLabel="Save" + submittingMessage="Applying.." controls={(
Kind: diff --git a/src/renderer/components/dock/install-chart.tsx b/src/renderer/components/dock/install-chart.tsx index b433874a75..4f651f7a3a 100644 --- a/src/renderer/components/dock/install-chart.tsx +++ b/src/renderer/components/dock/install-chart.tsx @@ -125,17 +125,17 @@ export class InstallChart extends Component {
this.showNotes = false} logs={this.releaseDetails.log} @@ -148,7 +148,7 @@ export class InstallChart extends Component { const panelControls = (
Chart - + Version { controls={panelControls} error={this.error} submit={install} - submitLabel={`Install`} - submittingMessage={`Installing...`} + submitLabel="Install" + submittingMessage="Installing..." showSubmitClose={false} /> { /> diff --git a/src/renderer/components/dock/upgrade-chart.tsx b/src/renderer/components/dock/upgrade-chart.tsx index 41ce5d295e..c5253cb5b1 100644 --- a/src/renderer/components/dock/upgrade-chart.tsx +++ b/src/renderer/components/dock/upgrade-chart.tsx @@ -123,8 +123,8 @@ export class UpgradeChart extends React.Component { tabId={tabId} error={error} submit={upgrade} - submitLabel={`Upgrade`} - submittingMessage={`Updating..`} + submitLabel="Upgrade" + submittingMessage="Updating.." controls={controlsAndInfo} /> {
diff --git a/src/renderer/components/menu/menu-actions.tsx b/src/renderer/components/menu/menu-actions.tsx index 4e46935aa4..96a88bdf88 100644 --- a/src/renderer/components/menu/menu-actions.tsx +++ b/src/renderer/components/menu/menu-actions.tsx @@ -106,13 +106,13 @@ export class MenuActions extends React.Component { {children} {updateAction && ( - + Edit )} {removeAction && ( - + Remove )} From bae8c76a730528114002ac25361aa6f79789a549 Mon Sep 17 00:00:00 2001 From: Lauri Nevala Date: Tue, 23 Feb 2021 15:47:40 +0200 Subject: [PATCH 17/37] Display environment variables coming from secret in pod details (#2167) Signed-off-by: Lauri Nevala --- .../+workloads-pods/pod-container-env.tsx | 59 +++++++++++++++---- 1 file changed, 48 insertions(+), 11 deletions(-) diff --git a/src/renderer/components/+workloads-pods/pod-container-env.tsx b/src/renderer/components/+workloads-pods/pod-container-env.tsx index 38af50a457..2e96b142f9 100644 --- a/src/renderer/components/+workloads-pods/pod-container-env.tsx +++ b/src/renderer/components/+workloads-pods/pod-container-env.tsx @@ -30,7 +30,11 @@ export const ContainerEnvironment = observer((props: Props) => { } }); envFrom && envFrom.forEach(item => { - const { configMapRef } = item; + const { configMapRef, secretRef } = item; + + if (secretRef && secretRef.name) { + secretsStore.load({ name: secretRef.name, namespace }); + } if (configMapRef && configMapRef.name) { configMapsStore.load({ name: configMapRef.name, namespace }); @@ -89,21 +93,54 @@ export const ContainerEnvironment = observer((props: Props) => { const renderEnvFrom = () => { const envVars = envFrom.map(vars => { - if (!vars.configMapRef || !vars.configMapRef.name) return; - const configMap = configMapsStore.getByName(vars.configMapRef.name, namespace); - - if (!configMap) return; - - return Object.entries(configMap.data).map(([name, value]) => ( -
- {name}: {value} -
- )); + if (vars.configMapRef?.name) { + return renderEnvFromConfigMap(vars.configMapRef.name); + } else if (vars.secretRef?.name ) { + return renderEnvFromSecret(vars.secretRef.name); + } }); return _.flatten(envVars); }; + const renderEnvFromConfigMap = (configMapName: string) => { + const configMap = configMapsStore.getByName(configMapName, namespace); + + if (!configMap) return; + + return Object.entries(configMap.data).map(([name, value]) => ( +
+ {name}: {value} +
+ )); + }; + + const renderEnvFromSecret = (secretName: string) => { + const secret = secretsStore.getByName(secretName, namespace); + + if (!secret) return; + + return Object.keys(secret.data).map(key => { + const secretKeyRef = { + name: secret.getName(), + key + }; + + const value = ( + + ); + + return ( +
+ {key}: {value} +
+ ); + }); + }; + return ( {env && renderEnv()} From cf7bed06f781c4000952f7f97415f60c131be348 Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Wed, 24 Feb 2021 10:20:06 +0300 Subject: [PATCH 18/37] Fix: deprecated helm chart filtering (#2158) * Refactor of excludeDeprecated helm service method Signed-off-by: Alex Andreev * Pick first helm chart from the list on load Signed-off-by: Alex Andreev * Removing helm filtering in UI Signed-off-by: Alex Andreev * Cleaning up Signed-off-by: Alex Andreev * Cleaning up type definitions Signed-off-by: Alex Andreev * Adding sorting charts by version Signed-off-by: Alex Andreev * Adding tests for methods that manipute chart listing Signed-off-by: Alex Andreev * Cleaning up tests a bit Signed-off-by: Alex Andreev * Adding semver coercion before comparing versions Signed-off-by: Alex Andreev --- src/main/helm/__mocks__/helm-chart-manager.ts | 108 ++++++++++++++++++ src/main/helm/__tests__/helm-service.test.ts | 104 +++++++++++++++++ src/main/helm/helm-chart-manager.ts | 9 +- src/main/helm/helm-service.ts | 42 ++++--- src/renderer/api/endpoints/helm-charts.api.ts | 11 +- .../+apps-helm-charts/helm-charts.tsx | 3 - 6 files changed, 246 insertions(+), 31 deletions(-) create mode 100644 src/main/helm/__mocks__/helm-chart-manager.ts create mode 100644 src/main/helm/__tests__/helm-service.test.ts diff --git a/src/main/helm/__mocks__/helm-chart-manager.ts b/src/main/helm/__mocks__/helm-chart-manager.ts new file mode 100644 index 0000000000..e832a937cb --- /dev/null +++ b/src/main/helm/__mocks__/helm-chart-manager.ts @@ -0,0 +1,108 @@ +import { HelmRepo, HelmRepoManager } from "../helm-repo-manager"; + +export class HelmChartManager { + private cache: any = {}; + private repo: HelmRepo; + + constructor(repo: HelmRepo){ + this.cache = HelmRepoManager.cache; + this.repo = repo; + } + + public async charts(): Promise { + switch (this.repo.name) { + case "stable": + return Promise.resolve({ + "apm-server": [ + { + apiVersion: "3.0.0", + name: "apm-server", + version: "2.1.7", + repo: "stable", + digest: "test" + }, + { + apiVersion: "3.0.0", + name: "apm-server", + version: "2.1.6", + repo: "stable", + digest: "test" + } + ], + "redis": [ + { + apiVersion: "3.0.0", + name: "apm-server", + version: "1.0.0", + repo: "stable", + digest: "test" + }, + { + apiVersion: "3.0.0", + name: "apm-server", + version: "0.0.9", + repo: "stable", + digest: "test" + } + ] + }); + case "experiment": + return Promise.resolve({ + "fairwind": [ + { + apiVersion: "3.0.0", + name: "fairwind", + version: "0.0.1", + repo: "experiment", + digest: "test" + }, + { + apiVersion: "3.0.0", + name: "fairwind", + version: "0.0.2", + repo: "experiment", + digest: "test", + deprecated: true + } + ] + }); + case "bitnami": + return Promise.resolve({ + "hotdog": [ + { + apiVersion: "3.0.0", + name: "hotdog", + version: "1.0.1", + repo: "bitnami", + digest: "test" + }, + { + apiVersion: "3.0.0", + name: "hotdog", + version: "1.0.2", + repo: "bitnami", + digest: "test", + } + ], + "pretzel": [ + { + apiVersion: "3.0.0", + name: "pretzel", + version: "1.0", + repo: "bitnami", + digest: "test", + }, + { + apiVersion: "3.0.0", + name: "pretzel", + version: "1.0.1", + repo: "bitnami", + digest: "test" + } + ] + }); + default: + return Promise.resolve({}); + } + } +} diff --git a/src/main/helm/__tests__/helm-service.test.ts b/src/main/helm/__tests__/helm-service.test.ts new file mode 100644 index 0000000000..8c1e82ef0a --- /dev/null +++ b/src/main/helm/__tests__/helm-service.test.ts @@ -0,0 +1,104 @@ +import { helmService } from "../helm-service"; +import { repoManager } from "../helm-repo-manager"; + +jest.spyOn(repoManager, "init").mockImplementation(); + +jest.mock("../helm-chart-manager"); + +describe("Helm Service tests", () => { + test("list charts without deprecated ones", async () => { + jest.spyOn(repoManager, "repositories").mockImplementation(async () => { + return [ + { name: "stable", url: "stableurl" }, + { name: "experiment", url: "experimenturl" } + ]; + }); + + const charts = await helmService.listCharts(); + + expect(charts).toEqual({ + stable: { + "apm-server": [ + { + apiVersion: "3.0.0", + name: "apm-server", + version: "2.1.7", + repo: "stable", + digest: "test" + }, + { + apiVersion: "3.0.0", + name: "apm-server", + version: "2.1.6", + repo: "stable", + digest: "test" + } + ], + "redis": [ + { + apiVersion: "3.0.0", + name: "apm-server", + version: "1.0.0", + repo: "stable", + digest: "test" + }, + { + apiVersion: "3.0.0", + name: "apm-server", + version: "0.0.9", + repo: "stable", + digest: "test" + } + ] + }, + experiment: {} + }); + }); + + test("list charts sorted by version in descending order", async () => { + jest.spyOn(repoManager, "repositories").mockImplementation(async () => { + return [ + { name: "bitnami", url: "bitnamiurl" } + ]; + }); + + const charts = await helmService.listCharts(); + + expect(charts).toEqual({ + bitnami: { + "hotdog": [ + { + apiVersion: "3.0.0", + name: "hotdog", + version: "1.0.2", + repo: "bitnami", + digest: "test", + }, + { + apiVersion: "3.0.0", + name: "hotdog", + version: "1.0.1", + repo: "bitnami", + digest: "test" + }, + ], + "pretzel": [ + { + apiVersion: "3.0.0", + name: "pretzel", + version: "1.0.1", + repo: "bitnami", + digest: "test", + }, + { + apiVersion: "3.0.0", + name: "pretzel", + version: "1.0", + repo: "bitnami", + digest: "test" + } + ] + } + }); + }); +}); diff --git a/src/main/helm/helm-chart-manager.ts b/src/main/helm/helm-chart-manager.ts index cf4a8e5ace..69619a56d4 100644 --- a/src/main/helm/helm-chart-manager.ts +++ b/src/main/helm/helm-chart-manager.ts @@ -4,9 +4,10 @@ import { HelmRepo, HelmRepoManager } from "./helm-repo-manager"; import logger from "../logger"; import { promiseExec } from "../promise-exec"; import { helmCli } from "./helm-cli"; +import type { RepoHelmChartList } from "../../renderer/api/endpoints/helm-charts.api"; type CachedYaml = { - entries: any; // todo: types + entries: RepoHelmChartList }; export class HelmChartManager { @@ -24,15 +25,15 @@ export class HelmChartManager { return charts[name]; } - public async charts(): Promise { + public async charts(): Promise { try { const cachedYaml = await this.cachedYaml(); return cachedYaml["entries"]; } catch(error) { - logger.error(error); + logger.error("HELM-CHART-MANAGER]: failed to list charts", { error }); - return []; + return {}; } } diff --git a/src/main/helm/helm-service.ts b/src/main/helm/helm-service.ts index 1918268075..f7445cebd4 100644 --- a/src/main/helm/helm-service.ts +++ b/src/main/helm/helm-service.ts @@ -1,8 +1,10 @@ +import semver from "semver"; import { Cluster } from "../cluster"; import logger from "../logger"; import { repoManager } from "./helm-repo-manager"; import { HelmChartManager } from "./helm-chart-manager"; import { releaseManager } from "./helm-release-manager"; +import { HelmChartList, RepoHelmChartList } from "../../renderer/api/endpoints/helm-charts.api"; class HelmService { public async installChart(cluster: Cluster, data: { chart: string; values: {}; name: string; namespace: string; version: string }) { @@ -10,7 +12,7 @@ class HelmService { } public async listCharts() { - const charts: any = {}; + const charts: HelmChartList = {}; await repoManager.init(); const repositories = await repoManager.repositories(); @@ -18,14 +20,10 @@ class HelmService { for (const repo of repositories) { charts[repo.name] = {}; const manager = new HelmChartManager(repo); - let entries = await manager.charts(); + const sortedCharts = this.sortChartsByVersion(await manager.charts()); + const enabledCharts = this.excludeDeprecatedChartGroups(sortedCharts); - entries = this.excludeDeprecated(entries); - - for (const key in entries) { - entries[key] = entries[key][0]; - } - charts[repo.name] = entries; + charts[repo.name] = enabledCharts; } return charts; @@ -96,20 +94,30 @@ class HelmService { return { message: output }; } - protected excludeDeprecated(entries: any) { - for (const key in entries) { - entries[key] = entries[key].filter((entry: any) => { - if (Array.isArray(entry)) { - return entry[0]["deprecated"] != true; - } + private excludeDeprecatedChartGroups(chartGroups: RepoHelmChartList) { + const groups = new Map(Object.entries(chartGroups)); - return entry["deprecated"] != true; + for (const [chartName, charts] of groups) { + if (charts[0].deprecated) { + groups.delete(chartName); + } + } + + return Object.fromEntries(groups); + } + + private sortChartsByVersion(chartGroups: RepoHelmChartList) { + for (const key in chartGroups) { + chartGroups[key] = chartGroups[key].sort((first, second) => { + const firstVersion = semver.coerce(first.version || 0); + const secondVersion = semver.coerce(second.version || 0); + + return semver.compare(secondVersion, firstVersion); }); } - return entries; + return chartGroups; } - } export const helmService = new HelmService(); diff --git a/src/renderer/api/endpoints/helm-charts.api.ts b/src/renderer/api/endpoints/helm-charts.api.ts index 8beff01779..02b5b0dbee 100644 --- a/src/renderer/api/endpoints/helm-charts.api.ts +++ b/src/renderer/api/endpoints/helm-charts.api.ts @@ -3,11 +3,8 @@ import { apiBase } from "../index"; import { stringify } from "querystring"; import { autobind } from "../../utils"; -interface IHelmChartList { - [repo: string]: { - [name: string]: HelmChart; - }; -} +export type RepoHelmChartList = Record; +export type HelmChartList = Record; export interface IHelmChartDetails { readme: string; @@ -22,12 +19,12 @@ const endpoint = compile(`/v2/charts/:repo?/:name?`) as (params?: { export const helmChartsApi = { list() { return apiBase - .get(endpoint()) + .get(endpoint()) .then(data => { return Object .values(data) .reduce((allCharts, repoCharts) => allCharts.concat(Object.values(repoCharts)), []) - .map(HelmChart.create); + .map(([chart]) => HelmChart.create(chart)); }); }, diff --git a/src/renderer/components/+apps-helm-charts/helm-charts.tsx b/src/renderer/components/+apps-helm-charts/helm-charts.tsx index 22d5ba3a2f..8e4afe8d83 100644 --- a/src/renderer/components/+apps-helm-charts/helm-charts.tsx +++ b/src/renderer/components/+apps-helm-charts/helm-charts.tsx @@ -72,9 +72,6 @@ export class HelmCharts extends Component { (chart: HelmChart) => chart.getAppVersion(), (chart: HelmChart) => chart.getKeywords(), ]} - filterItems={[ - (items: HelmChart[]) => items.filter(item => !item.deprecated) - ]} customizeHeader={() => ( )} From a0c9dcad971ba899ee7b7834510e2916d2f44efd Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Wed, 24 Feb 2021 09:05:14 -0500 Subject: [PATCH 19/37] StatusBarRegistration's component field must be optional for backwards compatability (#2211) Signed-off-by: Sebastian Malton --- src/extensions/registries/status-bar-registry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extensions/registries/status-bar-registry.ts b/src/extensions/registries/status-bar-registry.ts index 698c5f5f24..e0454fe77e 100644 --- a/src/extensions/registries/status-bar-registry.ts +++ b/src/extensions/registries/status-bar-registry.ts @@ -8,7 +8,7 @@ interface StatusBarComponents { } interface StatusBarRegistrationV2 { - components: StatusBarComponents; + components?: StatusBarComponents; // has to be optional for backwards compatability } export interface StatusBarRegistration extends StatusBarRegistrationV2 { From efd3c643de8b6988e561a6c5434370b864602e56 Mon Sep 17 00:00:00 2001 From: Lauri Nevala Date: Wed, 24 Feb 2021 16:10:47 +0200 Subject: [PATCH 20/37] Fix loading all namespaces for users with limited cluster access (#2217) Signed-off-by: Lauri Nevala --- src/renderer/api/kube-api.ts | 1 + src/renderer/components/+namespaces/namespace.store.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/renderer/api/kube-api.ts b/src/renderer/api/kube-api.ts index a880cc2406..448cd9da8f 100644 --- a/src/renderer/api/kube-api.ts +++ b/src/renderer/api/kube-api.ts @@ -272,6 +272,7 @@ export class KubeApi { } protected parseResponse(data: KubeJsonApiData | KubeJsonApiData[] | KubeJsonApiDataList, namespace?: string): any { + if (!data) return; const KubeObjectConstructor = this.objectConstructor; if (KubeObject.isJsonApiData(data)) { diff --git a/src/renderer/components/+namespaces/namespace.store.ts b/src/renderer/components/+namespaces/namespace.store.ts index ad271d1302..9995fbb7e5 100644 --- a/src/renderer/components/+namespaces/namespace.store.ts +++ b/src/renderer/components/+namespaces/namespace.store.ts @@ -120,7 +120,7 @@ export class NamespaceStore extends KubeObjectStore { protected async loadItems(params: KubeObjectStoreLoadingParams) { const { allowedNamespaces } = this; - let namespaces = await super.loadItems(params); + let namespaces = (await super.loadItems(params)) || []; namespaces = namespaces.filter(namespace => allowedNamespaces.includes(namespace.getName())); From ceeeb2473f58c77f5ee5a47e873737d7c3cf818d Mon Sep 17 00:00:00 2001 From: Rafed Ramzi <68276604+rafedramzi@users.noreply.github.com> Date: Thu, 18 Feb 2021 21:42:23 +0700 Subject: [PATCH 21/37] Fix Documentation: fix broken link in getting-started page (#2155) Signed-off-by: Rafed Ramzi --- docs/getting-started/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started/README.md b/docs/getting-started/README.md index 704e59d97b..bfb990378d 100644 --- a/docs/getting-started/README.md +++ b/docs/getting-started/README.md @@ -5,7 +5,7 @@ Lens is lightweight and simple to install. You'll be up and running in just a fe ## System Requirements -Review the [System Requirements](/supporting/requirements/) to check if your computer configuration is supported. +Review the [System Requirements](../supporting/requirements.md) to check if your computer configuration is supported. ## macOS From 0a7ace772fb56318868d150f734b4d5a10b17f4f Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Wed, 24 Feb 2021 16:51:28 +0200 Subject: [PATCH 22/37] v4.1.3 Signed-off-by: Jari Kolehmainen --- package.json | 2 +- static/RELEASE_NOTES.md | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 99340c44ed..3d4c660e74 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "kontena-lens", "productName": "Lens", "description": "Lens - The Kubernetes IDE", - "version": "4.1.2", + "version": "4.1.3", "main": "static/build/main.js", "copyright": "© 2020, Mirantis, Inc.", "license": "MIT", diff --git a/static/RELEASE_NOTES.md b/static/RELEASE_NOTES.md index bb855fab08..60dcf8ec4e 100644 --- a/static/RELEASE_NOTES.md +++ b/static/RELEASE_NOTES.md @@ -2,7 +2,17 @@ Here you can find description of changes we've built into each release. While we try our best to make each upgrade automatic and as smooth as possible, there may be some cases where you might need to do something to ensure the application works smoothly. So please read through the release highlights! -## 4.1.2 (current version) +## 4.1.3 (current version) + +- Don't reset selected namespaces to defaults in case of "All namespaces" on page reload +- Fix loading all namespaces for users with limited cluster access +- Display environment variables coming from secret in pod details +- Fix deprecated helm chart filtering +- Fix RoleBindings Namespace and Bindings field not displaying the correct data +- Fix RoleBindingDetails not rendering the name of the role binding +- Fix auto update on quit with newer version + +## 4.1.2 **Upgrade note:** Where have all my pods gone? Namespaced Kubernetes resources are now initially shown only for the "default" namespace. Use the namespaces selector to add more. From 61a8b3e5786dc6db4fcc1a68c7d84f3a16c2a237 Mon Sep 17 00:00:00 2001 From: Jim Ehrismann <40840436+jim-docker@users.noreply.github.com> Date: Thu, 25 Feb 2021 09:09:39 -0500 Subject: [PATCH 23/37] the select all checkbox should not select disabled items (#2151) * the select all checkbox should not select disabled items Signed-off-by: Jim Ehrismann * remove improper bullet-proofing Signed-off-by: Jim Ehrismann * added default for customizeTableRowProps Signed-off-by: Jim Ehrismann --- .../item-object-list/item-list-layout.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/renderer/components/item-object-list/item-list-layout.tsx b/src/renderer/components/item-object-list/item-list-layout.tsx index 59bd18cd5e..9485bc1403 100644 --- a/src/renderer/components/item-object-list/item-list-layout.tsx +++ b/src/renderer/components/item-object-list/item-list-layout.tsx @@ -89,7 +89,8 @@ const defaultProps: Partial = { filterItems: [], hasDetailsView: true, onDetails: noop, - virtual: true + virtual: true, + customizeTableRowProps: () => ({} as TableRowProps), }; interface ItemListLayoutUserSettings { @@ -241,7 +242,7 @@ export class ItemListLayout extends React.Component { sortItem={item} selected={detailsItem && detailsItem.getId() === itemId} onClick={hasDetailsView ? prevDefault(() => onDetails(item)) : undefined} - {...(customizeTableRowProps ? customizeTableRowProps(item) : {})} + {...customizeTableRowProps(item)} > {isSelectable && ( { } renderTableHeader() { - const { renderTableHeader, isSelectable, isConfigurable, store } = this.props; + const { customizeTableRowProps, renderTableHeader, isSelectable, isConfigurable, store } = this.props; if (!renderTableHeader) { return; } + const enabledItems = this.items.filter(item => !customizeTableRowProps(item).disabled); + return ( {isSelectable && ( store.toggleSelectionAll(this.items))} + isChecked={store.isSelectedAll(enabledItems)} + onClick={prevDefault(() => store.toggleSelectionAll(enabledItems))} /> )} {renderTableHeader.map((cellProps, index) => { From 8f103c3f20a7c1b40e11ba851edede0ddffbc6ed Mon Sep 17 00:00:00 2001 From: pashevskii <53330707+pashevskii@users.noreply.github.com> Date: Thu, 25 Feb 2021 19:09:07 +0400 Subject: [PATCH 24/37] Extract chart version ignoring numbers in chart name (#2226) Signed-off-by: Pavel Ashevskiy Co-authored-by: Pavel Ashevskiy --- src/renderer/api/endpoints/helm-releases.api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/api/endpoints/helm-releases.api.ts b/src/renderer/api/endpoints/helm-releases.api.ts index 6831c508cb..1642c8f3b2 100644 --- a/src/renderer/api/endpoints/helm-releases.api.ts +++ b/src/renderer/api/endpoints/helm-releases.api.ts @@ -187,7 +187,7 @@ export class HelmRelease implements ItemObject { } getVersion() { - const versions = this.chart.match(/(v?\d+)[^-].*$/); + const versions = this.chart.match(/(?<=-)(v?\d+)[^-].*$/); if (versions) { return versions[0]; From 8e454e9926f21f73e10addaecca5675b46b19079 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Fri, 26 Feb 2021 07:34:54 +0200 Subject: [PATCH 25/37] Flush response headers always when proxy gets a response (#2229) * flush response header always when proxy gets a response Signed-off-by: Jari Kolehmainen * force flush only when watch param exists Signed-off-by: Jari Kolehmainen --- src/main/lens-proxy.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/lens-proxy.ts b/src/main/lens-proxy.ts index 177e4d11d2..7e1aa98b7e 100644 --- a/src/main/lens-proxy.ts +++ b/src/main/lens-proxy.ts @@ -120,12 +120,18 @@ export class LensProxy { protected createProxy(): httpProxy { const proxy = httpProxy.createProxyServer(); - proxy.on("proxyRes", (proxyRes, req) => { + proxy.on("proxyRes", (proxyRes, req, res) => { const retryCounterId = this.getRequestId(req); if (this.retryCounters.has(retryCounterId)) { this.retryCounters.delete(retryCounterId); } + + if (!res.headersSent && req.url) { + const url = new URL(req.url, "http://localhost"); + + if (url.searchParams.has("watch")) res.flushHeaders(); + } }); proxy.on("error", (error, req, res, target) => { From 143f4331dfea5440d9c2ad74ff5cac2e5138705a Mon Sep 17 00:00:00 2001 From: Christoph Meier Date: Fri, 26 Feb 2021 07:39:54 +0100 Subject: [PATCH 26/37] Fix(rbac): pdp should have policy group (#2132) For rbac the `PodDisruptionBudget` should use the `policy` group. Signed-off-by: Christoph MEIER --- src/common/rbac.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/rbac.ts b/src/common/rbac.ts index de242b114a..7d02e3be51 100644 --- a/src/common/rbac.ts +++ b/src/common/rbac.ts @@ -31,7 +31,7 @@ export const apiResources: KubeApiResource[] = [ { kind: "PersistentVolume", apiName: "persistentvolumes" }, { kind: "PersistentVolumeClaim", apiName: "persistentvolumeclaims" }, { kind: "Pod", apiName: "pods" }, - { kind: "PodDisruptionBudget", apiName: "poddisruptionbudgets" }, + { kind: "PodDisruptionBudget", apiName: "poddisruptionbudgets", group: "policy" }, { kind: "PodSecurityPolicy", apiName: "podsecuritypolicies" }, { kind: "ResourceQuota", apiName: "resourcequotas" }, { kind: "ReplicaSet", apiName: "replicasets", group: "apps" }, From 1f910891bc22c8e7133371f9744da6bd5e9359a9 Mon Sep 17 00:00:00 2001 From: Lauri Nevala Date: Fri, 26 Feb 2021 14:56:51 +0200 Subject: [PATCH 27/37] Pass Lens wslenvs to terminal session on Windows (#2198) Signed-off-by: Lauri Nevala --- src/main/shell-session.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/shell-session.ts b/src/main/shell-session.ts index 9e5af371f7..10a2f9ed47 100644 --- a/src/main/shell-session.ts +++ b/src/main/shell-session.ts @@ -110,6 +110,14 @@ export class ShellSession extends EventEmitter { env["SystemRoot"] = process.env.SystemRoot; env["PTYSHELL"] = process.env.SHELL || "powershell.exe"; env["PATH"] = pathStr; + env["LENS_SESSION"] = "true"; + const lensWslEnv = "KUBECONFIG/up:LENS_SESSION/u"; + + if (process.env.WSLENV != undefined) { + env["WSLENV"] = `${process.env["WSLENV"]}:${lensWslEnv}`; + } else { + env["WSLENV"] = lensWslEnv; + } } else if(typeof(process.env.SHELL) != "undefined") { env["PTYSHELL"] = process.env.SHELL; env["PATH"] = pathStr; From 6d13f875759c9f3e60704822ae1ec6ec948f3580 Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Mon, 1 Mar 2021 12:35:00 +0300 Subject: [PATCH 28/37] Removing unused chart files (#2238) Signed-off-by: Alex Andreev --- .../chart/background-block.plugin.ts | 42 ----------------- .../components/chart/useRealTimeMetrics.ts | 45 ------------------- 2 files changed, 87 deletions(-) delete mode 100644 src/renderer/components/chart/background-block.plugin.ts delete mode 100644 src/renderer/components/chart/useRealTimeMetrics.ts diff --git a/src/renderer/components/chart/background-block.plugin.ts b/src/renderer/components/chart/background-block.plugin.ts deleted file mode 100644 index ff4816c4dd..0000000000 --- a/src/renderer/components/chart/background-block.plugin.ts +++ /dev/null @@ -1,42 +0,0 @@ -import ChartJS from "chart.js"; -import get from "lodash/get"; - -const defaultOptions = { - interval: 61, - coverBars: 3, - borderColor: "#44474A", - backgroundColor: "#00000033" -}; - -export const BackgroundBlock = { - options: {}, - - getOptions(chart: ChartJS) { - return get(chart, "options.plugins.BackgroundBlock"); - }, - - afterInit(chart: ChartJS) { - this.options = { - ...defaultOptions, - ...this.getOptions(chart) - }; - }, - - beforeDraw(chart: ChartJS) { - if (!chart.chartArea) return; - const { interval, coverBars, borderColor, backgroundColor } = this.options; - const { ctx, chartArea } = chart; - const { left, right, top, bottom } = chartArea; - const blockWidth = (right - left) / interval * coverBars; - - ctx.save(); - ctx.fillStyle = backgroundColor; - ctx.strokeStyle = borderColor; - ctx.fillRect(right - blockWidth, top, blockWidth, bottom - top); - ctx.beginPath(); - ctx.moveTo(right - blockWidth + 1.5, top); - ctx.lineTo(right - blockWidth + 1.5, bottom); - ctx.stroke(); - ctx.restore(); - } -}; diff --git a/src/renderer/components/chart/useRealTimeMetrics.ts b/src/renderer/components/chart/useRealTimeMetrics.ts deleted file mode 100644 index b01629e8e9..0000000000 --- a/src/renderer/components/chart/useRealTimeMetrics.ts +++ /dev/null @@ -1,45 +0,0 @@ -import moment from "moment"; -import { useState, useEffect } from "react"; -import { useInterval } from "../../hooks"; - -type IMetricValues = [number, string][]; -type IChartData = { x: number; y: string }[]; - -const defaultParams = { - fetchInterval: 15, - updateInterval: 5 -}; - -export function useRealTimeMetrics(metrics: IMetricValues, chartData: IChartData, params = defaultParams) { - const [index, setIndex] = useState(0); - const { fetchInterval, updateInterval } = params; - const rangeMetrics = metrics.slice(-updateInterval); - const steps = fetchInterval / updateInterval; - const data = [...chartData]; - - useEffect(() => { - setIndex(0); - }, [metrics]); - - useInterval(() => { - if (index < steps + 1) { - setIndex(index + steps - 1); - } - }, updateInterval * 1000); - - if (data.length && metrics.length) { - const lastTime = data[data.length - 1].x; - const values = []; - - for (let i = 0; i < 3; i++) { - values[i] = moment.unix(lastTime).add(i + 1, "m").unix(); - } - data.push( - { x: values[0], y: "0" }, - { x: values[1], y: parseFloat(rangeMetrics[index][1]).toFixed(3) }, - { x: values[2], y: "0" } - ); - } - - return data; -} From df20dd6b1116d2750d5910b435ef72ba9227aff6 Mon Sep 17 00:00:00 2001 From: Lauri Nevala Date: Mon, 1 Mar 2021 17:30:22 +0200 Subject: [PATCH 29/37] Ignore clusters with invalid kubeconfig (#1956) * Ignore clusters with invalid kubeconfig Signed-off-by: Lauri Nevala * Improve error message Signed-off-by: Lauri Nevala * Mark cluster as dead if kubeconfig loading fails Signed-off-by: Lauri Nevala * Fix tests Signed-off-by: Lauri Nevala * Validate cluster object in kubeconfig when constructing cluster Signed-off-by: Lauri Nevala * Add unit tests for validateKubeConfig Signed-off-by: Lauri Nevala * Refactor validateKubeconfig unit tests Signed-off-by: Lauri Nevala * Extract ValidationOpts type Signed-off-by: Lauri Nevala * Add default value to validationOpts param Signed-off-by: Lauri Nevala * Change isDead to property Signed-off-by: Lauri Nevala * Fix lint issues Signed-off-by: Lauri Nevala * Add missing new line Signed-off-by: Lauri Nevala * Update validateKubeConfig in-code documentation Signed-off-by: Lauri Nevala * Remove isDead property Signed-off-by: Lauri Nevala * Display warning notification if invalid kubeconfig detected (#2233) * Display warning notification if invalid kubeconfig detected Signed-off-by: Lauri Nevala --- src/common/__tests__/cluster-store.test.ts | 113 ++++++++++++++++-- src/common/__tests__/kube-helpers.test.ts | 101 ++++++++++++++++ src/common/cluster-store.ts | 4 +- src/common/ipc/index.ts | 1 + src/common/ipc/invalid-kubeconfig/index.ts | 3 + src/common/kube-helpers.ts | 43 +++++-- src/main/cluster.ts | 15 ++- .../components/+add-cluster/add-cluster.tsx | 2 +- .../components/dock/create-resource.tsx | 2 +- .../notifications/notifications.tsx | 5 +- src/renderer/ipc/index.tsx | 2 + .../ipc/invalid-kubeconfig-handler.tsx | 46 +++++++ 12 files changed, 307 insertions(+), 30 deletions(-) create mode 100644 src/common/__tests__/kube-helpers.test.ts create mode 100644 src/common/ipc/invalid-kubeconfig/index.ts create mode 100644 src/renderer/ipc/invalid-kubeconfig-handler.tsx diff --git a/src/common/__tests__/cluster-store.test.ts b/src/common/__tests__/cluster-store.test.ts index 6ee34388a2..d1d2f76603 100644 --- a/src/common/__tests__/cluster-store.test.ts +++ b/src/common/__tests__/cluster-store.test.ts @@ -6,6 +6,29 @@ import { ClusterStore, getClusterIdFromHost } from "../cluster-store"; import { workspaceStore } from "../workspace-store"; const testDataIcon = fs.readFileSync("test-data/cluster-store-migration-icon.png"); +const kubeconfig = ` +apiVersion: v1 +clusters: +- cluster: + server: https://localhost + name: test +contexts: +- context: + cluster: test + user: test + name: foo +- context: + cluster: test + user: test + name: foo2 +current-context: test +kind: Config +preferences: {} +users: +- name: test + user: + token: kubeconfig-user-q4lm4:xxxyyyy +`; jest.mock("electron", () => { return { @@ -47,13 +70,13 @@ describe("empty config", () => { clusterStore.addCluster( new Cluster({ id: "foo", - contextName: "minikube", + contextName: "foo", preferences: { terminalCWD: "/tmp", icon: "data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5", clusterName: "minikube" }, - kubeConfigPath: ClusterStore.embedCustomKubeConfig("foo", "fancy foo config"), + kubeConfigPath: ClusterStore.embedCustomKubeConfig("foo", kubeconfig), workspace: workspaceStore.currentWorkspaceId }) ); @@ -91,20 +114,20 @@ describe("empty config", () => { clusterStore.addClusters( new Cluster({ id: "prod", - contextName: "prod", + contextName: "foo", preferences: { clusterName: "prod" }, - kubeConfigPath: ClusterStore.embedCustomKubeConfig("prod", "fancy config"), + kubeConfigPath: ClusterStore.embedCustomKubeConfig("prod", kubeconfig), workspace: "workstation" }), new Cluster({ id: "dev", - contextName: "dev", + contextName: "foo2", preferences: { clusterName: "dev" }, - kubeConfigPath: ClusterStore.embedCustomKubeConfig("dev", "fancy config"), + kubeConfigPath: ClusterStore.embedCustomKubeConfig("dev", kubeconfig), workspace: "workstation" }) ); @@ -177,20 +200,20 @@ describe("config with existing clusters", () => { clusters: [ { id: "cluster1", - kubeConfig: "foo", + kubeConfigPath: kubeconfig, contextName: "foo", preferences: { terminalCWD: "/foo" }, workspace: "default" }, { id: "cluster2", - kubeConfig: "foo2", + kubeConfigPath: kubeconfig, contextName: "foo2", preferences: { terminalCWD: "/foo2" } }, { id: "cluster3", - kubeConfig: "foo", + kubeConfigPath: kubeconfig, contextName: "foo", preferences: { terminalCWD: "/foo" }, workspace: "foo", @@ -247,6 +270,78 @@ describe("config with existing clusters", () => { }); }); +describe("config with invalid cluster kubeconfig", () => { + beforeEach(() => { + const invalidKubeconfig = ` +apiVersion: v1 +clusters: +- cluster: + server: https://localhost + name: test2 +contexts: +- context: + cluster: test + user: test + name: test +current-context: test +kind: Config +preferences: {} +users: +- name: test + user: + token: kubeconfig-user-q4lm4:xxxyyyy +`; + + ClusterStore.resetInstance(); + const mockOpts = { + "tmp": { + "lens-cluster-store.json": JSON.stringify({ + __internal__: { + migrations: { + version: "99.99.99" + } + }, + clusters: [ + { + id: "cluster1", + kubeConfigPath: invalidKubeconfig, + contextName: "test", + preferences: { terminalCWD: "/foo" }, + workspace: "foo", + }, + { + id: "cluster2", + kubeConfigPath: kubeconfig, + contextName: "foo", + preferences: { terminalCWD: "/foo" }, + workspace: "default" + }, + + ] + }) + } + }; + + mockFs(mockOpts); + clusterStore = ClusterStore.getInstance(); + + return clusterStore.load(); + }); + + afterEach(() => { + mockFs.restore(); + }); + + it("does not enable clusters with invalid kubeconfig", () => { + const storedClusters = clusterStore.clustersList; + + expect(storedClusters.length).toBe(2); + expect(storedClusters[0].enabled).toBeFalsy; + expect(storedClusters[1].id).toBe("cluster2"); + expect(storedClusters[1].enabled).toBeTruthy; + }); +}); + describe("pre 2.0 config with an existing cluster", () => { beforeEach(() => { ClusterStore.resetInstance(); diff --git a/src/common/__tests__/kube-helpers.test.ts b/src/common/__tests__/kube-helpers.test.ts new file mode 100644 index 0000000000..a782772d34 --- /dev/null +++ b/src/common/__tests__/kube-helpers.test.ts @@ -0,0 +1,101 @@ +import { KubeConfig } from "@kubernetes/client-node"; +import { validateKubeConfig } from "../kube-helpers"; + +const kubeconfig = ` +apiVersion: v1 +clusters: +- cluster: + server: https://localhost + name: test +contexts: +- context: + cluster: test + user: test + name: valid +- context: + cluster: test2 + user: test + name: invalidCluster +- context: + cluster: test + user: test2 + name: invalidUser +- context: + cluster: test + user: invalidExec + name: invalidExec +current-context: test +kind: Config +preferences: {} +users: +- name: test + user: + exec: + command: echo +- name: invalidExec + user: + exec: + command: foo +`; + +const kc = new KubeConfig(); + +describe("validateKubeconfig", () => { + beforeAll(() => { + kc.loadFromString(kubeconfig); + }); + describe("with default validation options", () => { + describe("with valid kubeconfig", () => { + it("does not raise exceptions", () => { + expect(() => { validateKubeConfig(kc, "valid");}).not.toThrow(); + }); + }); + describe("with invalid context object", () => { + it("it raises exception", () => { + expect(() => { validateKubeConfig(kc, "invalid");}).toThrow("No valid context object provided in kubeconfig for context 'invalid'"); + }); + }); + + describe("with invalid cluster object", () => { + it("it raises exception", () => { + expect(() => { validateKubeConfig(kc, "invalidCluster");}).toThrow("No valid cluster object provided in kubeconfig for context 'invalidCluster'"); + }); + }); + + describe("with invalid user object", () => { + it("it raises exception", () => { + expect(() => { validateKubeConfig(kc, "invalidUser");}).toThrow("No valid user object provided in kubeconfig for context 'invalidUser'"); + }); + }); + + describe("with invalid exec command", () => { + it("it raises exception", () => { + expect(() => { validateKubeConfig(kc, "invalidExec");}).toThrow("User Exec command \"foo\" not found on host. Please ensure binary is found in PATH or use absolute path to binary in Kubeconfig"); + }); + }); + }); + + describe("with validateCluster as false", () => { + describe("with invalid cluster object", () => { + it("does not raise exception", () => { + expect(() => { validateKubeConfig(kc, "invalidCluster", { validateCluster: false });}).not.toThrow(); + }); + }); + }); + + describe("with validateUser as false", () => { + describe("with invalid user object", () => { + it("does not raise excpetions", () => { + expect(() => { validateKubeConfig(kc, "invalidUser", { validateUser: false });}).not.toThrow(); + }); + }); + }); + + describe("with validateExec as false", () => { + describe("with invalid exec object", () => { + it("does not raise excpetions", () => { + expect(() => { validateKubeConfig(kc, "invalidExec", { validateExec: false });}).not.toThrow(); + }); + }); + }); +}); diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index 4000684d16..6bf932f0f4 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store.ts @@ -323,7 +323,7 @@ export class ClusterStore extends BaseStore { } else { cluster = new Cluster(clusterModel); - if (!cluster.isManaged) { + if (!cluster.isManaged && cluster.apiUrl) { cluster.enabled = true; } } @@ -337,7 +337,7 @@ export class ClusterStore extends BaseStore { } }); - this.activeCluster = newClusters.has(activeCluster) ? activeCluster : null; + this.activeCluster = newClusters.get(activeCluster)?.enabled ? activeCluster : null; this.clusters.replace(newClusters); this.removedClusters.replace(removedClusters); } diff --git a/src/common/ipc/index.ts b/src/common/ipc/index.ts index a34890472e..c5e864dc75 100644 --- a/src/common/ipc/index.ts +++ b/src/common/ipc/index.ts @@ -1,3 +1,4 @@ export * from "./ipc"; +export * from "./invalid-kubeconfig"; export * from "./update-available"; export * from "./type-enforced-ipc"; diff --git a/src/common/ipc/invalid-kubeconfig/index.ts b/src/common/ipc/invalid-kubeconfig/index.ts new file mode 100644 index 0000000000..9e8e7921d7 --- /dev/null +++ b/src/common/ipc/invalid-kubeconfig/index.ts @@ -0,0 +1,3 @@ +export const InvalidKubeconfigChannel = "invalid-kubeconfig"; + +export type InvalidKubeConfigArgs = [clusterId: string]; diff --git a/src/common/kube-helpers.ts b/src/common/kube-helpers.ts index 02a9faef92..c2a2a8df93 100644 --- a/src/common/kube-helpers.ts +++ b/src/common/kube-helpers.ts @@ -7,6 +7,12 @@ import logger from "../main/logger"; import commandExists from "command-exists"; import { ExecValidationNotFoundError } from "./custom-errors"; +export type KubeConfigValidationOpts = { + validateCluster?: boolean; + validateUser?: boolean; + validateExec?: boolean; +}; + export const kubeConfigDefaultPath = path.join(os.homedir(), ".kube", "config"); function resolveTilde(filePath: string) { @@ -151,27 +157,42 @@ export function getNodeWarningConditions(node: V1Node) { } /** - * Validates kubeconfig supplied in the add clusters screen. At present this will just validate - * the User struct, specifically the command passed to the exec substructure. - */ -export function validateKubeConfig (config: KubeConfig) { + * Checks if `config` has valid `Context`, `User`, `Cluster`, and `exec` fields (if present when required) + */ +export function validateKubeConfig (config: KubeConfig, contextName: string, validationOpts: KubeConfigValidationOpts = {}) { // we only receive a single context, cluster & user object here so lets validate them as this // will be called when we add a new cluster to Lens - logger.debug(`validateKubeConfig: validating kubeconfig - ${JSON.stringify(config)}`); + + const { validateUser = true, validateCluster = true, validateExec = true } = validationOpts; + + const contextObject = config.getContextObject(contextName); + + // Validate the Context Object + if (!contextObject) { + throw new Error(`No valid context object provided in kubeconfig for context '${contextName}'`); + } + + // Validate the Cluster Object + if (validateCluster && !config.getCluster(contextObject.cluster)) { + throw new Error(`No valid cluster object provided in kubeconfig for context '${contextName}'`); + } + + const user = config.getUser(contextObject.user); // Validate the User Object - const user = config.getCurrentUser(); - - if (user.exec) { + if (validateUser && !user) { + throw new Error(`No valid user object provided in kubeconfig for context '${contextName}'`); + } + + // Validate exec command if present + if (validateExec && user?.exec) { const execCommand = user.exec["command"]; // check if the command is absolute or not const isAbsolute = path.isAbsolute(execCommand); // validate the exec struct in the user object, start with the command field - logger.debug(`validateKubeConfig: validating user exec command - ${JSON.stringify(execCommand)}`); - if (!commandExists.sync(execCommand)) { - logger.debug(`validateKubeConfig: exec command ${String(execCommand)} in kubeconfig ${config.currentContext} not found`); + logger.debug(`validateKubeConfig: exec command ${String(execCommand)} in kubeconfig ${contextName} not found`); throw new ExecValidationNotFoundError(execCommand, isAbsolute); } } diff --git a/src/main/cluster.ts b/src/main/cluster.ts index 13c74a285e..198d24c2f9 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -4,12 +4,12 @@ import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api"; import type { WorkspaceId } from "../common/workspace-store"; import { action, comparer, computed, observable, reaction, toJS, when } from "mobx"; import { apiKubePrefix } from "../common/vars"; -import { broadcastMessage } from "../common/ipc"; +import { broadcastMessage, InvalidKubeconfigChannel } from "../common/ipc"; import { ContextHandler } from "./context-handler"; import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"; import { Kubectl } from "./kubectl"; import { KubeconfigManager } from "./kubeconfig-manager"; -import { loadConfig } from "../common/kube-helpers"; +import { loadConfig, validateKubeConfig } from "../common/kube-helpers"; import request, { RequestPromiseOptions } from "request-promise-native"; import { apiResources, KubeApiResource } from "../common/rbac"; import logger from "./logger"; @@ -177,6 +177,7 @@ export class Cluster implements ClusterModel, ClusterState { * @observable */ @observable isAdmin = false; + /** * Global watch-api accessibility , e.g. "/api/v1/services?watch=1" * @@ -256,10 +257,16 @@ export class Cluster implements ClusterModel, ClusterState { constructor(model: ClusterModel) { this.updateModel(model); - const kubeconfig = this.getKubeconfig(); - if (kubeconfig.getContextObject(this.contextName)) { + try { + const kubeconfig = this.getKubeconfig(); + + validateKubeConfig(kubeconfig, this.contextName, { validateCluster: true, validateUser: false, validateExec: false}); this.apiUrl = kubeconfig.getCluster(kubeconfig.getContextObject(this.contextName).cluster).server; + } catch(err) { + logger.error(err); + logger.error(`[CLUSTER] Failed to load kubeconfig for the cluster '${this.name || this.contextName}' (context: ${this.contextName}, kubeconfig: ${this.kubeConfigPath}).`); + broadcastMessage(InvalidKubeconfigChannel, model.id); } } diff --git a/src/renderer/components/+add-cluster/add-cluster.tsx b/src/renderer/components/+add-cluster/add-cluster.tsx index ae4a3e6ace..38d03482e8 100644 --- a/src/renderer/components/+add-cluster/add-cluster.tsx +++ b/src/renderer/components/+add-cluster/add-cluster.tsx @@ -147,7 +147,7 @@ export class AddCluster extends React.Component { try { const kubeConfig = this.kubeContexts.get(context); - validateKubeConfig(kubeConfig); + validateKubeConfig(kubeConfig, context); return true; } catch (err) { diff --git a/src/renderer/components/dock/create-resource.tsx b/src/renderer/components/dock/create-resource.tsx index 8ee859d2cf..01e6002309 100644 --- a/src/renderer/components/dock/create-resource.tsx +++ b/src/renderer/components/dock/create-resource.tsx @@ -52,7 +52,7 @@ export class CreateResource extends React.Component { ); if (errors.length) { - errors.forEach(Notifications.error); + errors.forEach(error => Notifications.error(error)); if (!createdResources.length) throw errors[0]; } const successMessage = ( diff --git a/src/renderer/components/notifications/notifications.tsx b/src/renderer/components/notifications/notifications.tsx index 0c1ac692cf..206102b1a3 100644 --- a/src/renderer/components/notifications/notifications.tsx +++ b/src/renderer/components/notifications/notifications.tsx @@ -21,11 +21,12 @@ export class Notifications extends React.Component { }); } - static error(message: NotificationMessage) { + static error(message: NotificationMessage, customOpts: Partial = {}) { notificationsStore.add({ message, timeout: 10000, - status: NotificationStatus.ERROR + status: NotificationStatus.ERROR, + ...customOpts }); } diff --git a/src/renderer/ipc/index.tsx b/src/renderer/ipc/index.tsx index b9644f7404..544cefbf78 100644 --- a/src/renderer/ipc/index.tsx +++ b/src/renderer/ipc/index.tsx @@ -5,6 +5,7 @@ import { Notifications, notificationsStore } from "../components/notifications"; import { Button } from "../components/button"; import { isMac } from "../../common/vars"; import * as uuid from "uuid"; +import { invalidKubeconfigHandler } from "./invalid-kubeconfig-handler"; function sendToBackchannel(backchannel: string, notificationId: string, data: BackchannelArg): void { notificationsStore.remove(notificationId); @@ -58,4 +59,5 @@ export function registerIpcHandlers() { listener: UpdateAvailableHandler, verifier: areArgsUpdateAvailableFromMain, }); + onCorrect(invalidKubeconfigHandler); } diff --git a/src/renderer/ipc/invalid-kubeconfig-handler.tsx b/src/renderer/ipc/invalid-kubeconfig-handler.tsx new file mode 100644 index 0000000000..cadf7e4e3f --- /dev/null +++ b/src/renderer/ipc/invalid-kubeconfig-handler.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import { ipcRenderer, IpcRendererEvent, shell } from "electron"; +import { clusterStore } from "../../common/cluster-store"; +import { InvalidKubeConfigArgs, InvalidKubeconfigChannel } from "../../common/ipc/invalid-kubeconfig"; +import { Notifications, notificationsStore } from "../components/notifications"; +import { Button } from "../components/button"; + +export const invalidKubeconfigHandler = { + source: ipcRenderer, + channel: InvalidKubeconfigChannel, + listener: InvalidKubeconfigListener, + verifier: (args: [unknown]): args is InvalidKubeConfigArgs => { + return args.length === 1 && typeof args[0] === "string" && !!clusterStore.getById(args[0]); + }, +}; + +function InvalidKubeconfigListener(event: IpcRendererEvent, ...[clusterId]: InvalidKubeConfigArgs): void { + const notificationId = `invalid-kubeconfig:${clusterId}`; + const cluster = clusterStore.getById(clusterId); + const contextName = cluster.name !== cluster.contextName ? `(context: ${cluster.contextName})` : ""; + + Notifications.error( + ( +
+ Cluster with Invalid Kubeconfig Detected! +

Cluster {cluster.name} has invalid kubeconfig {contextName} and cannot be displayed. + Please fix the { e.preventDefault(); shell.showItemInFolder(cluster.kubeConfigPath); }}>kubeconfig manually and restart Lens + or remove the cluster.

+

Do you want to remove the cluster now?

+
+
+
+ ), + { + id: notificationId, + timeout: 0 + } + ); +} + + From 4c7c2d1084a7cff9ff46398c3c7ee3f643ee8598 Mon Sep 17 00:00:00 2001 From: Lauri Nevala Date: Wed, 3 Mar 2021 15:40:19 +0200 Subject: [PATCH 30/37] Render only secret name on pod details without access to secrets (#2244) * Render only secret name on pod details without access to secrets Signed-off-by: Lauri Nevala * Preserving layout when amount of secrets passed Signed-off-by: Alex Andreev * Refactor secrets to observable map Signed-off-by: Lauri Nevala Co-authored-by: Alex Andreev --- .../+workloads-pods/pod-details-secrets.scss | 4 +-- .../+workloads-pods/pod-details-secrets.tsx | 32 ++++++++++++++----- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/renderer/components/+workloads-pods/pod-details-secrets.scss b/src/renderer/components/+workloads-pods/pod-details-secrets.scss index d1ee08c51b..f20aced60d 100644 --- a/src/renderer/components/+workloads-pods/pod-details-secrets.scss +++ b/src/renderer/components/+workloads-pods/pod-details-secrets.scss @@ -1,7 +1,7 @@ .PodDetailsSecrets { - a { + > * { display: block; - margin-bottom: $margin; + margin-bottom: var(--margin); &:last-child { margin-bottom: 0; diff --git a/src/renderer/components/+workloads-pods/pod-details-secrets.tsx b/src/renderer/components/+workloads-pods/pod-details-secrets.tsx index af1515c1b4..b2c7cd14c3 100644 --- a/src/renderer/components/+workloads-pods/pod-details-secrets.tsx +++ b/src/renderer/components/+workloads-pods/pod-details-secrets.tsx @@ -13,33 +13,49 @@ interface Props { @observer export class PodDetailsSecrets extends Component { - @observable secrets: Secret[] = []; + @observable secrets: Map = observable.map(); @disposeOnUnmount secretsLoader = autorun(async () => { const { pod } = this.props; - this.secrets = await Promise.all( + const secrets = await Promise.all( pod.getSecrets().map(secretName => secretsApi.get({ name: secretName, namespace: pod.getNs(), })) ); + + secrets.forEach(secret => secret && this.secrets.set(secret.getName(), secret)); }); render() { + const { pod } = this.props; + return (
{ - this.secrets.map(secret => { - return ( - - {secret.getName()} - - ); + pod.getSecrets().map(secretName => { + const secret = this.secrets.get(secretName); + + if (secret) { + return this.renderSecretLink(secret); + } else { + return ( + {secretName} + ); + } }) }
); } + + protected renderSecretLink(secret: Secret) { + return ( + + {secret.getName()} + + ); + } } From c1a2c1e84971c5c7f03765bc5dbb7ffeaca96b02 Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Thu, 4 Mar 2021 15:12:28 +0300 Subject: [PATCH 31/37] Fix: preventing to render on cluster refresh (#2253) * Removing @observer decorator from Signed-off-by: Alex Andreev * Add observer wrapper to Signed-off-by: Alex Andreev * Fix eslint claim Signed-off-by: Alex Andreev * Moving extension route renderers to components Signed-off-by: Alex Andreev * Clean up Signed-off-by: Alex Andreev * Removing external observables out from App render() Signed-off-by: Alex Andreev * Fetching hosted cluster inside Command Palette Signed-off-by: Alex Andreev * Setting route lists explicitly To avoid using observable data within tabRoutes arrays Signed-off-by: Alex Andreev * Review fixes Signed-off-by: Alex Andreev --- .../components/+config/config.route.ts | 19 +++++-- .../components/+network/network.route.ts | 15 ++++-- .../+pod-security-policies/index.ts | 1 - .../pod-security-policies.route.ts | 8 --- .../components/+storage/storage.route.ts | 13 +++-- .../+user-management/user-management.route.ts | 20 ++++--- .../+user-management/user-management.tsx | 4 +- .../components/+workloads/workloads.route.ts | 20 ++++--- src/renderer/components/app.tsx | 54 ++----------------- .../command-palette/command-container.tsx | 7 ++- .../components/layout/main-layout-header.tsx | 5 +- 11 files changed, 70 insertions(+), 96 deletions(-) delete mode 100644 src/renderer/components/+pod-security-policies/pod-security-policies.route.ts diff --git a/src/renderer/components/+config/config.route.ts b/src/renderer/components/+config/config.route.ts index 8ea637c505..d269455566 100644 --- a/src/renderer/components/+config/config.route.ts +++ b/src/renderer/components/+config/config.route.ts @@ -1,12 +1,21 @@ import { RouteProps } from "react-router"; -import { Config } from "./config"; import { IURLParams } from "../../../common/utils/buildUrl"; -import { configMapsURL } from "../+config-maps/config-maps.route"; +import { configMapsRoute, configMapsURL } from "../+config-maps/config-maps.route"; +import { hpaRoute } from "../+config-autoscalers"; +import { limitRangesRoute } from "../+config-limit-ranges"; +import { pdbRoute } from "../+config-pod-disruption-budgets"; +import { resourceQuotaRoute } from "../+config-resource-quotas"; +import { secretsRoute } from "../+config-secrets"; export const configRoute: RouteProps = { - get path() { - return Config.tabRoutes.map(({ routePath }) => routePath).flat(); - } + path: [ + configMapsRoute, + secretsRoute, + resourceQuotaRoute, + limitRangesRoute, + hpaRoute, + pdbRoute + ].map(route => route.path.toString()) }; export const configURL = (params?: IURLParams) => configMapsURL(params); diff --git a/src/renderer/components/+network/network.route.ts b/src/renderer/components/+network/network.route.ts index de36bfc77a..2e5d5ffcb5 100644 --- a/src/renderer/components/+network/network.route.ts +++ b/src/renderer/components/+network/network.route.ts @@ -1,12 +1,17 @@ import { RouteProps } from "react-router"; -import { Network } from "./network"; -import { servicesURL } from "../+network-services"; +import { endpointRoute } from "../+network-endpoints"; +import { ingressRoute } from "../+network-ingresses"; +import { networkPoliciesRoute } from "../+network-policies"; +import { servicesRoute, servicesURL } from "../+network-services"; import { IURLParams } from "../../../common/utils/buildUrl"; export const networkRoute: RouteProps = { - get path() { - return Network.tabRoutes.map(({ routePath }) => routePath).flat(); - } + path: [ + servicesRoute, + endpointRoute, + ingressRoute, + networkPoliciesRoute + ].map(route => route.path.toString()) }; export const networkURL = (params?: IURLParams) => servicesURL(params); diff --git a/src/renderer/components/+pod-security-policies/index.ts b/src/renderer/components/+pod-security-policies/index.ts index c9379d3381..223affa147 100644 --- a/src/renderer/components/+pod-security-policies/index.ts +++ b/src/renderer/components/+pod-security-policies/index.ts @@ -1,3 +1,2 @@ -export * from "./pod-security-policies.route"; export * from "./pod-security-policies"; export * from "./pod-security-policy-details"; diff --git a/src/renderer/components/+pod-security-policies/pod-security-policies.route.ts b/src/renderer/components/+pod-security-policies/pod-security-policies.route.ts deleted file mode 100644 index 8bee44985e..0000000000 --- a/src/renderer/components/+pod-security-policies/pod-security-policies.route.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { RouteProps } from "react-router"; -import { buildURL } from "../../../common/utils/buildUrl"; - -export const podSecurityPoliciesRoute: RouteProps = { - path: "/pod-security-policies" -}; - -export const podSecurityPoliciesURL = buildURL(podSecurityPoliciesRoute.path); diff --git a/src/renderer/components/+storage/storage.route.ts b/src/renderer/components/+storage/storage.route.ts index 6ad17a4fdf..174eaea080 100644 --- a/src/renderer/components/+storage/storage.route.ts +++ b/src/renderer/components/+storage/storage.route.ts @@ -1,12 +1,15 @@ import { RouteProps } from "react-router"; -import { volumeClaimsURL } from "../+storage-volume-claims"; -import { Storage } from "./storage"; +import { storageClassesRoute } from "../+storage-classes"; +import { volumeClaimsRoute, volumeClaimsURL } from "../+storage-volume-claims"; +import { volumesRoute } from "../+storage-volumes"; import { IURLParams } from "../../../common/utils/buildUrl"; export const storageRoute: RouteProps = { - get path() { - return Storage.tabRoutes.map(({ routePath }) => routePath).flat(); - } + path: [ + volumeClaimsRoute, + volumesRoute, + storageClassesRoute + ].map(route => route.path.toString()) }; export const storageURL = (params?: IURLParams) => volumeClaimsURL(params); diff --git a/src/renderer/components/+user-management/user-management.route.ts b/src/renderer/components/+user-management/user-management.route.ts index 9dc17cbbd8..3acebb7899 100644 --- a/src/renderer/components/+user-management/user-management.route.ts +++ b/src/renderer/components/+user-management/user-management.route.ts @@ -1,12 +1,5 @@ import type { RouteProps } from "react-router"; import { buildURL, IURLParams } from "../../../common/utils/buildUrl"; -import { UserManagement } from "./user-management"; - -export const usersManagementRoute: RouteProps = { - get path() { - return UserManagement.tabRoutes.map(({ routePath }) => routePath).flat(); - } -}; // Routes export const serviceAccountsRoute: RouteProps = { @@ -18,6 +11,18 @@ export const rolesRoute: RouteProps = { export const roleBindingsRoute: RouteProps = { path: "/role-bindings" }; +export const podSecurityPoliciesRoute: RouteProps = { + path: "/pod-security-policies" +}; + +export const usersManagementRoute: RouteProps = { + path: [ + serviceAccountsRoute, + roleBindingsRoute, + rolesRoute, + podSecurityPoliciesRoute + ].map(route => route.path.toString()) +}; // Route params export interface IServiceAccountsRouteParams { @@ -34,3 +39,4 @@ export const usersManagementURL = (params?: IURLParams) => serviceAccountsURL(pa export const serviceAccountsURL = buildURL(serviceAccountsRoute.path); export const roleBindingsURL = buildURL(roleBindingsRoute.path); export const rolesURL = buildURL(rolesRoute.path); +export const podSecurityPoliciesURL = buildURL(podSecurityPoliciesRoute.path); diff --git a/src/renderer/components/+user-management/user-management.tsx b/src/renderer/components/+user-management/user-management.tsx index e851d50424..480808d6a1 100644 --- a/src/renderer/components/+user-management/user-management.tsx +++ b/src/renderer/components/+user-management/user-management.tsx @@ -5,9 +5,9 @@ import { TabLayout, TabLayoutRoute } from "../layout/tab-layout"; import { Roles } from "../+user-management-roles"; import { RoleBindings } from "../+user-management-roles-bindings"; import { ServiceAccounts } from "../+user-management-service-accounts"; -import { roleBindingsRoute, roleBindingsURL, rolesRoute, rolesURL, serviceAccountsRoute, serviceAccountsURL } from "./user-management.route"; +import { podSecurityPoliciesRoute, podSecurityPoliciesURL, roleBindingsRoute, roleBindingsURL, rolesRoute, rolesURL, serviceAccountsRoute, serviceAccountsURL } from "./user-management.route"; import { namespaceUrlParam } from "../+namespaces/namespace.store"; -import { PodSecurityPolicies, podSecurityPoliciesRoute, podSecurityPoliciesURL } from "../+pod-security-policies"; +import { PodSecurityPolicies } from "../+pod-security-policies"; import { isAllowedResource } from "../../../common/rbac"; @observer diff --git a/src/renderer/components/+workloads/workloads.route.ts b/src/renderer/components/+workloads/workloads.route.ts index 44c43c5ef9..14a0bbb07d 100644 --- a/src/renderer/components/+workloads/workloads.route.ts +++ b/src/renderer/components/+workloads/workloads.route.ts @@ -1,13 +1,6 @@ import type { RouteProps } from "react-router"; import { buildURL, IURLParams } from "../../../common/utils/buildUrl"; import { KubeResource } from "../../../common/rbac"; -import { Workloads } from "./workloads"; - -export const workloadsRoute: RouteProps = { - get path() { - return Workloads.tabRoutes.map(({ routePath }) => routePath).flat(); - } -}; // Routes export const overviewRoute: RouteProps = { @@ -35,6 +28,19 @@ export const cronJobsRoute: RouteProps = { path: "/cronjobs" }; +export const workloadsRoute: RouteProps = { + path: [ + overviewRoute, + podsRoute, + deploymentsRoute, + daemonSetsRoute, + statefulSetsRoute, + replicaSetsRoute, + jobsRoute, + cronJobsRoute + ].map(route => route.path.toString()) +}; + // Route params export interface IWorkloadsOverviewRouteParams { } diff --git a/src/renderer/components/app.tsx b/src/renderer/components/app.tsx index 8b7f8a527c..f5ad684595 100755 --- a/src/renderer/components/app.tsx +++ b/src/renderer/components/app.tsx @@ -91,27 +91,15 @@ export class App extends React.Component { reaction(() => this.warningsTotal, (count: number) => { broadcastMessage(`cluster-warning-event-count:${getHostedCluster().id}`, count); }), - - reaction(() => clusterPageMenuRegistry.getRootItems(), (rootItems) => { - this.generateExtensionTabLayoutRoutes(rootItems); - }, { - fireImmediately: true - }) ]); } + @observable startUrl = isAllowedResource(["events", "nodes", "pods"]) ? clusterURL() : workloadsURL(); + @computed get warningsTotal(): number { return nodesStore.getWarningsCount() + eventStore.getWarningsCount(); } - get startURL() { - if (isAllowedResource(["events", "nodes", "pods"])) { - return clusterURL(); - } - - return workloadsURL(); - } - getTabLayoutRoutes(menuItem: ClusterPageMenuRegistration) { const routes: TabLayoutRoute[] = []; @@ -152,38 +140,6 @@ export class App extends React.Component { }); } - @observable extensionRoutes: Map = new Map(); - - generateExtensionTabLayoutRoutes(rootItems: ClusterPageMenuRegistration[]) { - rootItems.forEach((menu, index) => { - let route = this.extensionRoutes.get(menu); - - if (!route) { - const tabRoutes = this.getTabLayoutRoutes(menu); - - if (tabRoutes.length > 0) { - const pageComponent = () => ; - - route = tab.routePath)}/>; - this.extensionRoutes.set(menu, route); - } else { - const page = clusterPageRegistry.getByPageTarget(menu.target); - - if (page) { - route = ; - this.extensionRoutes.set(menu, route); - } - } - } - }); - - for (const menu of this.extensionRoutes.keys()) { - if (!rootItems.includes(menu)) { - this.extensionRoutes.delete(menu); - } - } - } - renderExtensionRoutes() { return clusterPageRegistry.getItems().map((page, index) => { const menu = clusterPageMenuRegistry.getByPage(page); @@ -195,8 +151,6 @@ export class App extends React.Component { } render() { - const cluster = getHostedCluster(); - return ( @@ -215,7 +169,7 @@ export class App extends React.Component { {this.renderExtensionTabLayoutRoutes()} {this.renderExtensionRoutes()} - + @@ -228,7 +182,7 @@ export class App extends React.Component { - + ); diff --git a/src/renderer/components/command-palette/command-container.tsx b/src/renderer/components/command-palette/command-container.tsx index 34ab71ee67..c6950bc5b9 100644 --- a/src/renderer/components/command-palette/command-container.tsx +++ b/src/renderer/components/command-palette/command-container.tsx @@ -10,7 +10,6 @@ import { CommandDialog } from "./command-dialog"; import { CommandRegistration, commandRegistry } from "../../../extensions/registries/command-registry"; import { clusterStore } from "../../../common/cluster-store"; import { workspaceStore } from "../../../common/workspace-store"; -import { Cluster } from "../../../main/cluster"; export type CommandDialogEvent = { component: React.ReactElement @@ -29,7 +28,7 @@ export class CommandOverlay { } @observer -export class CommandContainer extends React.Component<{cluster?: Cluster}> { +export class CommandContainer extends React.Component<{ clusterId?: string }> { @observable.ref commandComponent: React.ReactElement; private escHandler(event: KeyboardEvent) { @@ -56,8 +55,8 @@ export class CommandContainer extends React.Component<{cluster?: Cluster}> { } componentDidMount() { - if (this.props.cluster) { - subscribeToBroadcast(`command-palette:run-action:${this.props.cluster.id}`, (event, commandId: string) => { + if (this.props.clusterId) { + subscribeToBroadcast(`command-palette:run-action:${this.props.clusterId}`, (event, commandId: string) => { const command = this.findCommandById(commandId); if (command) { diff --git a/src/renderer/components/layout/main-layout-header.tsx b/src/renderer/components/layout/main-layout-header.tsx index 6753f8d262..570c04a1f9 100644 --- a/src/renderer/components/layout/main-layout-header.tsx +++ b/src/renderer/components/layout/main-layout-header.tsx @@ -1,3 +1,4 @@ +import { observer } from "mobx-react"; import React from "react"; import { clusterSettingsURL } from "../+cluster-settings"; @@ -11,7 +12,7 @@ interface Props { className?: string } -export function MainLayoutHeader({ cluster, className }: Props) { +export const MainLayoutHeader = observer(({ cluster, className }: Props) => { return (
{cluster.name} @@ -29,4 +30,4 @@ export function MainLayoutHeader({ cluster, className }: Props) { />
); -} +}); From f3e0e0f25c5676dc1c17ca42f545292aa3162c48 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Thu, 4 Mar 2021 14:26:05 +0200 Subject: [PATCH 32/37] v4.1.4 Signed-off-by: Jari Kolehmainen --- package.json | 2 +- static/RELEASE_NOTES.md | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 3d4c660e74..5b2dcf58e6 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "kontena-lens", "productName": "Lens", "description": "Lens - The Kubernetes IDE", - "version": "4.1.3", + "version": "4.1.4", "main": "static/build/main.js", "copyright": "© 2020, Mirantis, Inc.", "license": "MIT", diff --git a/static/RELEASE_NOTES.md b/static/RELEASE_NOTES.md index 60dcf8ec4e..d4344f53f2 100644 --- a/static/RELEASE_NOTES.md +++ b/static/RELEASE_NOTES.md @@ -2,7 +2,18 @@ Here you can find description of changes we've built into each release. While we try our best to make each upgrade automatic and as smooth as possible, there may be some cases where you might need to do something to ensure the application works smoothly. So please read through the release highlights! -## 4.1.3 (current version) +## 4.1.4 (current version) + +- Ignore clusters with invalid kubeconfig +- Render only secret name on pod details without access to secrets +- Pass Lens wslenvs to terminal session on Windows +- Prevent top-level re-rendering on cluster refresh +- Extract chart version ignoring numbers in chart name +- The select all checkbox should not select disabled items +- Fix: Pdb should have policy group +- Fix: kubectl rollout not exiting properly on Lens terminal + +## 4.1.3 - Don't reset selected namespaces to defaults in case of "All namespaces" on page reload - Fix loading all namespaces for users with limited cluster access From 844b43f435c0026cfd4fbd0724e78d68a2fb93af Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Mon, 15 Mar 2021 11:28:14 -0400 Subject: [PATCH 33/37] Fix Lens not clearing other KUBECONFIG env vars (#2297) --- src/main/__test__/shell-session.test.ts | 19 +++++++++++++++++++ src/main/shell-session.ts | 19 ++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 src/main/__test__/shell-session.test.ts diff --git a/src/main/__test__/shell-session.test.ts b/src/main/__test__/shell-session.test.ts new file mode 100644 index 0000000000..fe1b0c4288 --- /dev/null +++ b/src/main/__test__/shell-session.test.ts @@ -0,0 +1,19 @@ +/** + * @jest-environment jsdom + */ + +import { clearKubeconfigEnvVars } from "../shell-session"; + +describe("clearKubeconfigEnvVars tests", () => { + it("should not touch non kubeconfig keys", () => { + expect(clearKubeconfigEnvVars({ a: 1 })).toStrictEqual({ a: 1 }); + }); + + it("should remove a single kubeconfig key", () => { + expect(clearKubeconfigEnvVars({ a: 1, kubeconfig: "1" })).toStrictEqual({ a: 1 }); + }); + + it("should remove a two kubeconfig key", () => { + expect(clearKubeconfigEnvVars({ a: 1, kubeconfig: "1", kUbeconfig: "1" })).toStrictEqual({ a: 1 }); + }); +}); diff --git a/src/main/shell-session.ts b/src/main/shell-session.ts index 10a2f9ed47..e11804ac16 100644 --- a/src/main/shell-session.ts +++ b/src/main/shell-session.ts @@ -12,6 +12,23 @@ import { isWindows } from "../common/vars"; import { appEventBus } from "../common/event-bus"; import { userStore } from "../common/user-store"; +const anyKubeconfig = /^kubeconfig$/i; + +/** + * This function deletes all keys of the form /^kubeconfig$/i, returning a new + * object. + * + * This is needed because `kubectl` checks for other version of kubeconfig + * before KUBECONFIG and we only set KUBECONFIG. + * @param env The current copy of env + */ +export function clearKubeconfigEnvVars(env: Record): Record { + return Object.fromEntries( + Object.entries(env) + .filter(([key]) => anyKubeconfig.exec(key) === null) + ); +} + export class ShellSession extends EventEmitter { static shellEnvs: Map = new Map(); @@ -103,7 +120,7 @@ export class ShellSession extends EventEmitter { } protected async getShellEnv() { - const env = JSON.parse(JSON.stringify(await shellEnv())); + const env = clearKubeconfigEnvVars(JSON.parse(JSON.stringify(await shellEnv()))); const pathStr = [this.kubectlBinDir, this.helmBinDir, process.env.PATH].join(path.delimiter); if(isWindows) { From 8ce487b999243fa5b05a8b54321c918210b66432 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Tue, 16 Mar 2021 08:05:28 -0400 Subject: [PATCH 34/37] Move clearKubeconfigEnvVars to separate file (#2337) Signed-off-by: Sebastian Malton --- src/main/__test__/shell-session.test.ts | 2 +- src/main/shell-session.ts | 18 +----------------- src/main/utils/clear-kube-env-vars.ts | 16 ++++++++++++++++ 3 files changed, 18 insertions(+), 18 deletions(-) create mode 100644 src/main/utils/clear-kube-env-vars.ts diff --git a/src/main/__test__/shell-session.test.ts b/src/main/__test__/shell-session.test.ts index fe1b0c4288..d16b5453b9 100644 --- a/src/main/__test__/shell-session.test.ts +++ b/src/main/__test__/shell-session.test.ts @@ -2,7 +2,7 @@ * @jest-environment jsdom */ -import { clearKubeconfigEnvVars } from "../shell-session"; +import { clearKubeconfigEnvVars } from "../utils/clear-kube-env-vars"; describe("clearKubeconfigEnvVars tests", () => { it("should not touch non kubeconfig keys", () => { diff --git a/src/main/shell-session.ts b/src/main/shell-session.ts index e11804ac16..5c7dca0cc6 100644 --- a/src/main/shell-session.ts +++ b/src/main/shell-session.ts @@ -11,23 +11,7 @@ import { helmCli } from "./helm/helm-cli"; import { isWindows } from "../common/vars"; import { appEventBus } from "../common/event-bus"; import { userStore } from "../common/user-store"; - -const anyKubeconfig = /^kubeconfig$/i; - -/** - * This function deletes all keys of the form /^kubeconfig$/i, returning a new - * object. - * - * This is needed because `kubectl` checks for other version of kubeconfig - * before KUBECONFIG and we only set KUBECONFIG. - * @param env The current copy of env - */ -export function clearKubeconfigEnvVars(env: Record): Record { - return Object.fromEntries( - Object.entries(env) - .filter(([key]) => anyKubeconfig.exec(key) === null) - ); -} +import { clearKubeconfigEnvVars } from "./utils/clear-kube-env-vars"; export class ShellSession extends EventEmitter { static shellEnvs: Map = new Map(); diff --git a/src/main/utils/clear-kube-env-vars.ts b/src/main/utils/clear-kube-env-vars.ts new file mode 100644 index 0000000000..ef14b1a331 --- /dev/null +++ b/src/main/utils/clear-kube-env-vars.ts @@ -0,0 +1,16 @@ +const anyKubeconfig = /^kubeconfig$/i; + +/** + * This function deletes all keys of the form /^kubeconfig$/i, returning a new + * object. + * + * This is needed because `kubectl` checks for other version of kubeconfig + * before KUBECONFIG and we only set KUBECONFIG. + * @param env The current copy of env + */ +export function clearKubeconfigEnvVars(env: Record): Record { + return Object.fromEntries( + Object.entries(env) + .filter(([key]) => anyKubeconfig.exec(key) === null) + ); +} From 75230732d15db60d77c027d673e4daddc7839daf Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Wed, 17 Mar 2021 15:13:03 +0200 Subject: [PATCH 35/37] Fix extension command palette loading (#2351) Signed-off-by: Jari Kolehmainen --- src/extensions/extension-loader.ts | 4 +++- src/extensions/registries/index.ts | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/extensions/extension-loader.ts b/src/extensions/extension-loader.ts index 98697d252c..24951f972b 100644 --- a/src/extensions/extension-loader.ts +++ b/src/extensions/extension-loader.ts @@ -192,6 +192,7 @@ export class ExtensionLoader { registries.appPreferenceRegistry.add(extension.appPreferences), registries.clusterFeatureRegistry.add(extension.clusterFeatures), registries.statusBarRegistry.add(extension.statusBarItems), + registries.commandRegistry.add(extension.commands), ]; this.events.on("remove", (removedExtension: LensRendererExtension) => { @@ -220,7 +221,8 @@ export class ExtensionLoader { registries.clusterPageMenuRegistry.add(extension.clusterPageMenus, extension), registries.kubeObjectMenuRegistry.add(extension.kubeObjectMenuItems), registries.kubeObjectDetailRegistry.add(extension.kubeObjectDetailItems), - registries.kubeObjectStatusRegistry.add(extension.kubeObjectStatusTexts) + registries.kubeObjectStatusRegistry.add(extension.kubeObjectStatusTexts), + registries.commandRegistry.add(extension.commands), ]; this.events.on("remove", (removedExtension: LensRendererExtension) => { diff --git a/src/extensions/registries/index.ts b/src/extensions/registries/index.ts index 8e343742ec..b6838f1bab 100644 --- a/src/extensions/registries/index.ts +++ b/src/extensions/registries/index.ts @@ -9,3 +9,4 @@ export * from "./kube-object-detail-registry"; export * from "./kube-object-menu-registry"; export * from "./cluster-feature-registry"; export * from "./kube-object-status-registry"; +export * from "./command-registry"; From a0fe42708306ef6b4aa213fffd1d588ab80e6df7 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Thu, 25 Mar 2021 12:50:13 +0200 Subject: [PATCH 36/37] Proxy should listen only on loopback device (#2388) Signed-off-by: Jari Kolehmainen --- src/main/lens-proxy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/lens-proxy.ts b/src/main/lens-proxy.ts index 7e1aa98b7e..0bc3528a33 100644 --- a/src/main/lens-proxy.ts +++ b/src/main/lens-proxy.ts @@ -28,7 +28,7 @@ export class LensProxy { } listen(port = this.port): this { - this.proxyServer = this.buildCustomProxy().listen(port); + this.proxyServer = this.buildCustomProxy().listen(port, "127.0.0.1"); logger.info(`[LENS-PROXY]: Proxy server has started at ${this.origin}`); return this; From f1d97c8974e4da595a20df342b22b035018c2eab Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Thu, 25 Mar 2021 13:10:40 +0200 Subject: [PATCH 37/37] v4.1.5 Signed-off-by: Jari Kolehmainen --- package.json | 2 +- static/RELEASE_NOTES.md | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 5b2dcf58e6..13b82b15a4 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "kontena-lens", "productName": "Lens", "description": "Lens - The Kubernetes IDE", - "version": "4.1.4", + "version": "4.1.5", "main": "static/build/main.js", "copyright": "© 2020, Mirantis, Inc.", "license": "MIT", diff --git a/static/RELEASE_NOTES.md b/static/RELEASE_NOTES.md index d4344f53f2..9ef56e507e 100644 --- a/static/RELEASE_NOTES.md +++ b/static/RELEASE_NOTES.md @@ -2,7 +2,14 @@ Here you can find description of changes we've built into each release. While we try our best to make each upgrade automatic and as smooth as possible, there may be some cases where you might need to do something to ensure the application works smoothly. So please read through the release highlights! -## 4.1.4 (current version) + +## 4.1.5 (current version) + +- Fix internal proxy listening on all network interfaces +- Fix extension command palette loading +- Fix Lens not clearing other KUBECONFIG env vars + +## 4.1.4 - Ignore clusters with invalid kubeconfig - Render only secret name on pod details without access to secrets