diff --git a/extensions/pod-menu/src/logs-menu.tsx b/extensions/pod-menu/src/logs-menu.tsx index dfe3870d12..706efcf128 100644 --- a/extensions/pod-menu/src/logs-menu.tsx +++ b/extensions/pod-menu/src/logs-menu.tsx @@ -47,7 +47,7 @@ export class PodLogsMenu extends React.Component { return ( this.showLogs(container))} className="flex align-center"> {brick} - {name} + {name} ); }) diff --git a/extensions/pod-menu/src/shell-menu.tsx b/extensions/pod-menu/src/shell-menu.tsx index a93739e89f..4a5562b836 100644 --- a/extensions/pod-menu/src/shell-menu.tsx +++ b/extensions/pod-menu/src/shell-menu.tsx @@ -54,7 +54,7 @@ export class PodShellMenu extends React.Component { return ( this.execShell(name))} className="flex align-center"> - {name} + {name} ); }) diff --git a/integration/__tests__/app.tests.ts b/integration/__tests__/app.tests.ts index 8672076226..285af70ceb 100644 --- a/integration/__tests__/app.tests.ts +++ b/integration/__tests__/app.tests.ts @@ -273,6 +273,12 @@ describe("Lens integration tests", () => { expectedSelector: "h5.title", expectedText: "Stateful Sets" }, + { + name: "ReplicaSets", + href: "replicasets", + expectedSelector: "h5.title", + expectedText: "Replica Sets" + }, { name: "Jobs", href: "jobs", diff --git a/locales/en/messages.po b/locales/en/messages.po index 8591a1235f..ec9ac00f25 100644 --- a/locales/en/messages.po +++ b/locales/en/messages.po @@ -140,6 +140,43 @@ msgstr "Add field" msgid "Adding helm branch <0>{0} has failed: {1}" msgstr "Adding helm branch <0>{0} has failed: {1}" + +#: src/renderer/components/+preferences/add-helm-repo-dialog.tsx:91 +msgid "Helm repository <0>{0} has added" +msgstr "Helm repository <0>{0} has added" + +#: src/renderer/components/+preferences/add-helm-repo-dialog.tsx:128 +msgid "Skip TLS certificate checks for the repository" +msgstr "Skip TLS certificate checks for the repository" + +#: src/renderer/components/+preferences/add-helm-repo-dialog.tsx:128 +msgid "Key file" +msgstr "Key file" + +#: src/renderer/components/+preferences/add-helm-repo-dialog.tsx:129 +msgid "Ca file" +msgstr "Ca file" + +#: src/renderer/components/+preferences/add-helm-repo-dialog.tsx:130 +msgid "Cerificate file" +msgstr "Cerificate file" + +#: src/renderer/components/+preferences/add-helm-repo-dialog.tsx:134 +msgid "Username" +msgstr "Username" + +#: src/renderer/components/+preferences/add-helm-repo-dialog.tsx:140 +msgid "Password" +msgstr "Password" + +#: src/renderer/components/+preferences/add-helm-repo-dialog.tsx:163 +msgid "Helm repo name" +msgstr "Helm repo name" + +#: src/renderer/components/+preferences/add-helm-repo-dialog.tsx:163 +msgid "More" +msgstr "More" + #: src/renderer/components/+preferences/preferences.tsx:108 #~ msgid "Adding repo <0>{0} has failed: {1}" #~ msgstr "Adding repo <0>{0} has failed: {1}" @@ -186,6 +223,7 @@ msgstr "Affinities" #: src/renderer/components/+workloads-pods/pods.tsx:78 #: src/renderer/components/+workloads-replicasets/replicasets.tsx:51 #: src/renderer/components/+workloads-statefulsets/statefulsets.tsx:41 +#: src/renderer/components/+workloads-replicasets/replicasets.tsx:56 msgid "Age" msgstr "Age" @@ -729,6 +767,10 @@ msgstr "Cron Jobs" msgid "CronJobs" msgstr "CronJobs" +#: src/renderer/components/+workloads-replicasets/replicasets.tsx:54 +msgid "Current" +msgstr "Current" + #: src/renderer/components/+config-autoscalers/hpa-details.tsx:50 msgid "Current / Target" msgstr "Current / Target" @@ -740,6 +782,7 @@ msgstr "Current Healthy" #: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:101 #: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:103 +#: src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.tsx:104 msgid "Current replica scale: {currentReplicas}" msgstr "Current replica scale: {currentReplicas}" @@ -824,6 +867,10 @@ msgstr "Deployments" msgid "Description" msgstr "Description" +#: src/renderer/components/+workloads-replicasets/replicasets.tsx:53 +msgid "Desired" +msgstr "Desired" + #: src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets-details.tsx:42 #: src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets.tsx:41 msgid "Desired Healthy" @@ -831,6 +878,7 @@ msgstr "Desired Healthy" #: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:105 #: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:107 +#: src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.tsx:108 msgid "Desired number of replicas" msgstr "Desired number of replicas" @@ -1095,6 +1143,7 @@ msgstr "Hide" #: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:127 #: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:116 +#: src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.tsx:131 msgid "High number of replicas may cause cluster performance issues" msgstr "High number of replicas may cause cluster performance issues" @@ -1546,6 +1595,7 @@ msgstr "Mounts" #: src/renderer/components/+workspaces/workspaces.tsx:135 #: src/renderer/components/dock/edit-resource.tsx:89 #: src/renderer/components/kube-object/kube-object-meta.tsx:20 +#: src/renderer/components/+workloads-replicasets/replicasets.tsx:50 msgid "Name" msgstr "Name" @@ -1595,6 +1645,7 @@ msgstr "Names" #: src/renderer/components/item-object-list/page-filters-select.tsx:57 #: src/renderer/components/kube-object/kube-object-meta.tsx:23 #: src/renderer/components/+workloads-pods/pod-details-list.tsx:144 +#: src/renderer/components/+workloads-replicasets/replicasets.tsx:52 msgid "Namespace" msgstr "Namespace" @@ -2004,6 +2055,7 @@ msgstr "Read-only Root Filesystem" msgid "Readiness" msgstr "Readiness" +#: src/renderer/components/+workloads-replicasets/replicasets.tsx:55 #: src/renderer/components/+workloads-pods/pod-details-list.tsx:145 msgid "Ready" msgstr "Ready" @@ -2119,6 +2171,10 @@ msgstr "Removing helm branch <0>{0} has failed: {1}" msgid "Replicas" msgstr "Replicas" +#: src/renderer/components/+workloads/workloads.tsx:70 +msgid "ReplicaSets" +msgstr "ReplicaSets" + #: src/renderer/components/dock/install-chart.tsx:119 msgid "Repo/Name" msgstr "Repo/Name" @@ -2310,6 +2366,7 @@ msgstr "Save" #: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:128 #: src/renderer/components/+workloads-deployments/deployments.tsx:83 #: src/renderer/components/+workloads-deployments/deployments.tsx:84 +#: src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.tsx:160 msgid "Scale" msgstr "Scale" @@ -2317,6 +2374,10 @@ msgstr "Scale" msgid "Scale Deployment <0>{deploymentName}" msgstr "Scale Deployment <0>{deploymentName}" +#: src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.tsx:143 +msgid "Scale Replica Set <0>{replicaSetName}" +msgstr "Scale Replica Set <0>{replicaSetName}" + #: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:139 msgid "Scale Stateful Set <0>{statefulSetName}" msgstr "Scale Stateful Set <0>{statefulSetName}" diff --git a/locales/fi/messages.po b/locales/fi/messages.po index 76f10f6baf..02995c74db 100644 --- a/locales/fi/messages.po +++ b/locales/fi/messages.po @@ -140,6 +140,42 @@ msgstr "" msgid "Adding helm branch <0>{0} has failed: {1}" msgstr "" +#: src/renderer/components/+preferences/add-helm-repo-dialog.tsx:91 +msgid "Helm repository <0>{0} has added" +msgstr "" + +#: src/renderer/components/+preferences/add-helm-repo-dialog.tsx:128 +msgid "Skip TLS certificate checks for the repository" +msgstr "" + +#: src/renderer/components/+preferences/add-helm-repo-dialog.tsx:128 +msgid "Key file" +msgstr "" + +#: src/renderer/components/+preferences/add-helm-repo-dialog.tsx:129 +msgid "Ca file" +msgstr "" + +#: src/renderer/components/+preferences/add-helm-repo-dialog.tsx:130 +msgid "Cerificate file" +msgstr "" + +#: src/renderer/components/+preferences/add-helm-repo-dialog.tsx:134 +msgid "Username" +msgstr "" + +#: src/renderer/components/+preferences/add-helm-repo-dialog.tsx:140 +msgid "Password" +msgstr "" + +#: src/renderer/components/+preferences/add-helm-repo-dialog.tsx:163 +msgid "Helm repo name" +msgstr "" + +#: src/renderer/components/+preferences/add-helm-repo-dialog.tsx:163 +msgid "More" +msgstr "" + #: src/renderer/components/+preferences/preferences.tsx:108 #~ msgid "Adding repo <0>{0} has failed: {1}" #~ msgstr "" @@ -186,6 +222,7 @@ msgstr "" #: src/renderer/components/+workloads-pods/pods.tsx:78 #: src/renderer/components/+workloads-replicasets/replicasets.tsx:51 #: src/renderer/components/+workloads-statefulsets/statefulsets.tsx:41 +#: src/renderer/components/+workloads-replicasets/replicasets.tsx:56 msgid "Age" msgstr "" @@ -725,6 +762,10 @@ msgstr "" msgid "CronJobs" msgstr "" +#: src/renderer/components/+workloads-replicasets/replicasets.tsx:54 +msgid "Current" +msgstr "" + #: src/renderer/components/+config-autoscalers/hpa-details.tsx:50 msgid "Current / Target" msgstr "" @@ -736,6 +777,7 @@ msgstr "" #: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:101 #: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:103 +#: src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.tsx:104 msgid "Current replica scale: {currentReplicas}" msgstr "" @@ -820,6 +862,10 @@ msgstr "" msgid "Description" msgstr "" +#: src/renderer/components/+workloads-replicasets/replicasets.tsx:53 +msgid "Desired" +msgstr "" + #: src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets-details.tsx:42 #: src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets.tsx:41 msgid "Desired Healthy" @@ -827,6 +873,7 @@ msgstr "" #: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:105 #: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:107 +#: src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.tsx:108 msgid "Desired number of replicas" msgstr "" @@ -1086,6 +1133,7 @@ msgstr "" #: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:127 #: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:116 +#: src/renderer/components/+workloads-replicaset/replicaset-scale-dialog.tsx:131 msgid "High number of replicas may cause cluster performance issues" msgstr "" @@ -1537,6 +1585,7 @@ msgstr "" #: src/renderer/components/+workspaces/workspaces.tsx:135 #: src/renderer/components/dock/edit-resource.tsx:89 #: src/renderer/components/kube-object/kube-object-meta.tsx:20 +#: src/renderer/components/+workloads-replicasets/replicasets.tsx:50 msgid "Name" msgstr "" @@ -1586,6 +1635,7 @@ msgstr "" #: src/renderer/components/item-object-list/page-filters-select.tsx:57 #: src/renderer/components/kube-object/kube-object-meta.tsx:23 #: src/renderer/components/+workloads-pods/pod-details-list.tsx:144 +#: src/renderer/components/+workloads-replicasets/replicasets.tsx:52 msgid "Namespace" msgstr "" @@ -1987,6 +2037,7 @@ msgstr "" msgid "Readiness" msgstr "" +#: src/renderer/components/+workloads-replicasets/replicasets.tsx:55 #: src/renderer/components/+workloads-pods/pod-details-list.tsx:145 msgid "Ready" msgstr "" @@ -2102,6 +2153,10 @@ msgstr "" msgid "Replicas" msgstr "" +#: src/renderer/components/+workloads/workloads.tsx:70 +msgid "ReplicaSets" +msgstr "" + #: src/renderer/components/dock/install-chart.tsx:119 msgid "Repo/Name" msgstr "" @@ -2293,6 +2348,7 @@ msgstr "" #: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:128 #: src/renderer/components/+workloads-deployments/deployments.tsx:83 #: src/renderer/components/+workloads-deployments/deployments.tsx:84 +#: src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.tsx:160 msgid "Scale" msgstr "" @@ -2300,6 +2356,10 @@ msgstr "" msgid "Scale Deployment <0>{deploymentName}" msgstr "" +#: src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.tsx:143 +msgid "Scale Replica Set <0>{replicaSetName}" +msgstr "" + #: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:139 msgid "Scale Stateful Set <0>{statefulSetName}" msgstr "" diff --git a/locales/ru/messages.po b/locales/ru/messages.po index 97f9226480..0f5fb2f9e2 100644 --- a/locales/ru/messages.po +++ b/locales/ru/messages.po @@ -141,6 +141,42 @@ msgstr "Добавить поле" msgid "Adding helm branch <0>{0} has failed: {1}" msgstr "" +#: src/renderer/components/+preferences/add-helm-repo-dialog.tsx:91 +msgid "Helm repository <0>{0} has added" +msgstr "Helm репозиторий <0>{0} добавлен" + +#: src/renderer/components/+preferences/add-helm-repo-dialog.tsx:128 +msgid "Skip TLS certificate checks for the repository" +msgstr "" + +#: src/renderer/components/+preferences/add-helm-repo-dialog.tsx:128 +msgid "Key file" +msgstr "" + +#: src/renderer/components/+preferences/add-helm-repo-dialog.tsx:129 +msgid "Ca file" +msgstr "" + +#: src/renderer/components/+preferences/add-helm-repo-dialog.tsx:130 +msgid "Cerificate file" +msgstr "" + +#: src/renderer/components/+preferences/add-helm-repo-dialog.tsx:134 +msgid "Username" +msgstr "" + +#: src/renderer/components/+preferences/add-helm-repo-dialog.tsx:140 +msgid "Password" +msgstr "" + +#: src/renderer/components/+preferences/add-helm-repo-dialog.tsx:163 +msgid "Helm repo name" +msgstr "" + +#: src/renderer/components/+preferences/add-helm-repo-dialog.tsx:163 +msgid "More" +msgstr "" + #: src/renderer/components/+preferences/preferences.tsx:108 #~ msgid "Adding repo <0>{0} has failed: {1}" #~ msgstr "" @@ -187,6 +223,7 @@ msgstr "Аффинитеты" #: src/renderer/components/+workloads-pods/pods.tsx:78 #: src/renderer/components/+workloads-replicasets/replicasets.tsx:51 #: src/renderer/components/+workloads-statefulsets/statefulsets.tsx:41 +#: src/renderer/components/+workloads-replicasets/replicasets.tsx:56 msgid "Age" msgstr "Возраст" @@ -730,6 +767,10 @@ msgstr "" msgid "CronJobs" msgstr "CronJobs" +#: src/renderer/components/+workloads-replicasets/replicasets.tsx:54 +msgid "Current" +msgstr "Текущее" + #: src/renderer/components/+config-autoscalers/hpa-details.tsx:50 msgid "Current / Target" msgstr "Текущее / Цель" @@ -741,6 +782,7 @@ msgstr "" #: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:101 #: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:103 +#: src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.tsx:104 msgid "Current replica scale: {currentReplicas}" msgstr "Текущий размер реплики: {currentReplicas}" @@ -825,6 +867,10 @@ msgstr "Deployments" msgid "Description" msgstr "Описание" +#: src/renderer/components/+workloads-replicasets/replicasets.tsx:53 +msgid "Desired" +msgstr "Желаемое" + #: src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets-details.tsx:42 #: src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets.tsx:41 msgid "Desired Healthy" @@ -832,6 +878,7 @@ msgstr "" #: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:105 #: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:107 +#: src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.tsx:108 msgid "Desired number of replicas" msgstr "Нужный уровень реплик" @@ -1096,6 +1143,7 @@ msgstr "Скрыть" #: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:127 #: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:116 +#: src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.tsx:131 msgid "High number of replicas may cause cluster performance issues" msgstr "Большое количество реплик может вызвать проблемы с производительностью кластера" @@ -1547,6 +1595,7 @@ msgstr "Установки" #: src/renderer/components/+workspaces/workspaces.tsx:135 #: src/renderer/components/dock/edit-resource.tsx:89 #: src/renderer/components/kube-object/kube-object-meta.tsx:20 +#: src/renderer/components/+workloads-replicasets/replicasets.tsx:50 msgid "Name" msgstr "Имя" @@ -1596,6 +1645,7 @@ msgstr "" #: src/renderer/components/item-object-list/page-filters-select.tsx:57 #: src/renderer/components/kube-object/kube-object-meta.tsx:23 #: src/renderer/components/+workloads-pods/pod-details-list.tsx:144 +#: src/renderer/components/+workloads-replicasets/replicasets.tsx:52 msgid "Namespace" msgstr "Namespace" @@ -2005,6 +2055,7 @@ msgstr "" msgid "Readiness" msgstr "Готовность" +#: src/renderer/components/+workloads-replicasets/replicasets.tsx:55 #: src/renderer/components/+workloads-pods/pod-details-list.tsx:145 msgid "Ready" msgstr "Готовы" @@ -2120,6 +2171,10 @@ msgstr "" msgid "Replicas" msgstr "Реплики" +#: src/renderer/components/+workloads/workloads.tsx:70 +msgid "ReplicaSets" +msgstr "ReplicaSets" + #: src/renderer/components/dock/install-chart.tsx:119 msgid "Repo/Name" msgstr "Репозиторий/Имя" @@ -2311,6 +2366,7 @@ msgstr "Сохранить" #: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:128 #: src/renderer/components/+workloads-deployments/deployments.tsx:83 #: src/renderer/components/+workloads-deployments/deployments.tsx:84 +#: src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.tsx:160 msgid "Scale" msgstr "Масштабировать" @@ -2318,6 +2374,10 @@ msgstr "Масштабировать" msgid "Scale Deployment <0>{deploymentName}" msgstr "Масштабировать Deployment <0>{deploymentName}" +#: src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.tsx:143 +msgid "Scale Replica Set <0>{replicaSetName}" +msgstr "Масштабировать Replica Set <0>{replicaSetName}" + #: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:139 msgid "Scale Stateful Set <0>{statefulSetName}" msgstr "Масштабировать Stateful Set <0>{statefulSetName}" diff --git a/src/common/rbac.ts b/src/common/rbac.ts index 702d87d394..0e0e5a780e 100644 --- a/src/common/rbac.ts +++ b/src/common/rbac.ts @@ -31,6 +31,7 @@ export const apiResources: KubeApiResource[] = [ { resource: "poddisruptionbudgets" }, { resource: "podsecuritypolicies" }, { resource: "resourcequotas" }, + { resource: "replicasets", group: "apps" }, { resource: "secrets" }, { resource: "services" }, { resource: "statefulsets", group: "apps" }, diff --git a/src/extensions/__tests__/extension-discovery.test.ts b/src/extensions/__tests__/extension-discovery.test.ts new file mode 100644 index 0000000000..0317319329 --- /dev/null +++ b/src/extensions/__tests__/extension-discovery.test.ts @@ -0,0 +1,97 @@ +import { watch } from "chokidar"; +import { join, normalize } from "path"; +import { ExtensionDiscovery, InstalledExtension } from "../extension-discovery"; + +jest.mock("../../common/ipc"); +jest.mock("fs-extra"); +jest.mock("chokidar", () => ({ + watch: jest.fn() +})); +jest.mock("../extension-installer", () => ({ + extensionInstaller: { + extensionPackagesRoot: "", + installPackages: jest.fn() + } +})); + +const mockedWatch = watch as jest.MockedFunction; + +describe("ExtensionDiscovery", () => { + it("emits add for added extension", async done => { + globalThis.__non_webpack_require__.mockImplementation(() => ({ + name: "my-extension" + })); + let addHandler: (filePath: string) => void; + + const mockWatchInstance: any = { + on: jest.fn((event: string, handler: typeof addHandler) => { + if (event === "add") { + addHandler = handler; + } + + return mockWatchInstance; + }) + }; + + mockedWatch.mockImplementationOnce(() => + (mockWatchInstance) as any + ); + const extensionDiscovery = new ExtensionDiscovery(); + + // Need to force isLoaded to be true so that the file watching is started + extensionDiscovery.isLoaded = true; + + await extensionDiscovery.initMain(); + + extensionDiscovery.events.on("add", (extension: InstalledExtension) => { + expect(extension).toEqual({ + absolutePath: expect.any(String), + id: normalize("node_modules/my-extension/package.json"), + isBundled: false, + isEnabled: false, + manifest: { + name: "my-extension", + }, + manifestPath: normalize("node_modules/my-extension/package.json"), + }); + done(); + }); + + addHandler(join(extensionDiscovery.localFolderPath, "/my-extension/package.json")); + }); + + it("doesn't emit add for added file under extension", async done => { + let addHandler: (filePath: string) => void; + + const mockWatchInstance: any = { + on: jest.fn((event: string, handler: typeof addHandler) => { + if (event === "add") { + addHandler = handler; + } + + return mockWatchInstance; + }) + }; + + mockedWatch.mockImplementationOnce(() => + (mockWatchInstance) as any + ); + const extensionDiscovery = new ExtensionDiscovery(); + + // Need to force isLoaded to be true so that the file watching is started + extensionDiscovery.isLoaded = true; + + await extensionDiscovery.initMain(); + + const onAdd = jest.fn(); + + extensionDiscovery.events.on("add", onAdd); + + addHandler(join(extensionDiscovery.localFolderPath, "/my-extension/node_modules/dep/package.json")); + + setTimeout(() => { + expect(onAdd).not.toHaveBeenCalled(); + done(); + }, 10); + }); +}); diff --git a/src/extensions/extension-discovery.ts b/src/extensions/extension-discovery.ts index ab52027f15..2b26d79a9c 100644 --- a/src/extensions/extension-discovery.ts +++ b/src/extensions/extension-discovery.ts @@ -1,4 +1,4 @@ -import chokidar from "chokidar"; +import { watch } from "chokidar"; import { ipcRenderer } from "electron"; import { EventEmitter } from "events"; import fs from "fs-extra"; @@ -138,7 +138,7 @@ export class ExtensionDiscovery { await this.whenLoaded; // chokidar works better than fs.watch - chokidar.watch(this.localFolderPath, { + watch(this.localFolderPath, { // For adding and removing symlinks to work, the depth has to be 1. depth: 1, // Try to wait until the file has been completely copied. @@ -155,14 +155,26 @@ export class ExtensionDiscovery { .on("unlinkDir", this.handleWatchUnlinkDir); } - handleWatchFileAdd = async (filePath: string) => { - if (path.basename(filePath) === manifestFilename) { + handleWatchFileAdd = async (manifestPath: string) => { + // e.g. "foo/package.json" + const relativePath = path.relative(this.localFolderPath, manifestPath); + + // Converts "foo/package.json" to ["foo", "package.json"], where length of 2 implies + // that the added file is in a folder under local folder path. + // This safeguards against a file watch being triggered under a sub-directory which is not an extension. + const isUnderLocalFolderPath = relativePath.split(path.sep).length === 2; + + if (path.basename(manifestPath) === manifestFilename && isUnderLocalFolderPath) { try { - const absPath = path.dirname(filePath); + const absPath = path.dirname(manifestPath); + // this.loadExtensionFromPath updates this.packagesJson - const extension = await this.loadExtensionFromPath(absPath); + const extension = await this.loadExtensionFromFolder(absPath); if (extension) { + // Remove a broken symlink left by a previous installation if it exists. + await this.removeSymlinkByManifestPath(manifestPath); + // Install dependencies for the new extension await this.installPackages(); @@ -190,6 +202,9 @@ export class ExtensionDiscovery { .find(([, extensionFolder]) => filePath === extensionFolder)?.[0]; if (extensionName !== undefined) { + // If the extension is deleted manually while the application is running, also remove the symlink + await this.removeSymlinkByPackageName(extensionName); + delete this.packagesJson.dependencies[extensionName]; // Reinstall dependencies to remove the extension from package.json @@ -197,7 +212,7 @@ export class ExtensionDiscovery { // The path to the manifest file is the lens extension id // Note that we need to use the symlinked path - const lensExtensionId = path.join(this.nodeModulesPath, extensionName, "package.json"); + const lensExtensionId = path.join(this.nodeModulesPath, extensionName, manifestFilename); logger.info(`${logModule} removed extension ${extensionName}`); this.events.emit("remove", lensExtensionId as LensExtensionId); @@ -208,18 +223,34 @@ export class ExtensionDiscovery { }; /** - * Uninstalls extension by path. - * The application will detect the folder unlink and remove the extension from the UI automatically. - * @param absolutePath Path to the non-symlinked folder of the extension + * Remove the symlink under node_modules if exists. + * If we don't remove the symlink, the uninstall would leave a non-working symlink, + * which wouldn't be fixed if the extension was reinstalled, causing the extension not to work. + * @param name e.g. "@mirantis/lens-extension-cc" */ - async uninstallExtension(absolutePath: string) { - logger.info(`${logModule} Uninstalling ${absolutePath}`); + removeSymlinkByPackageName(name: string) { + return fs.remove(this.getInstalledPath(name)); + } - const exists = await fs.pathExists(absolutePath); + /** + * Remove the symlink under node_modules if it exists. + * @param manifestPath Path to package.json + */ + removeSymlinkByManifestPath(manifestPath: string) { + const manifestJson = __non_webpack_require__(manifestPath); - if (!exists) { - throw new Error(`Extension path ${absolutePath} doesn't exist`); - } + return this.removeSymlinkByPackageName(manifestJson.name); + } + + /** + * Uninstalls extension. + * The application will detect the folder unlink and remove the extension from the UI automatically. + * @param extension Extension to unistall. + */ + async uninstallExtension({ absolutePath, manifest }: InstalledExtension) { + logger.info(`${logModule} Uninstalling ${manifest.name}`); + + await this.removeSymlinkByPackageName(manifest.name); // fs.remove does nothing if the path doesn't exist anymore await fs.remove(absolutePath); @@ -260,6 +291,26 @@ export class ExtensionDiscovery { return extensions; } + /** + * Returns the symlinked path to the extension folder, + * e.g. "/Users//Library/Application Support/Lens/node_modules/@publisher/extension" + */ + protected getInstalledPath(name: string) { + return path.join(this.nodeModulesPath, name); + } + + /** + * Returns the symlinked path to the package.json, + * e.g. "/Users//Library/Application Support/Lens/node_modules/@publisher/extension/package.json" + */ + protected getInstalledManifestPath(name: string) { + return path.join(this.getInstalledPath(name), manifestFilename); + } + + /** + * Returns InstalledExtension from path to package.json file. + * Also updates this.packagesJson. + */ protected async getByManifest(manifestPath: string, { isBundled = false }: { isBundled?: boolean; } = {}): Promise { @@ -270,7 +321,7 @@ export class ExtensionDiscovery { fs.accessSync(manifestPath, fs.constants.F_OK); manifestJson = __non_webpack_require__(manifestPath); - const installedManifestPath = path.join(this.nodeModulesPath, manifestJson.name, "package.json"); + const installedManifestPath = this.getInstalledManifestPath(manifestJson.name); this.packagesJson.dependencies[manifestJson.name] = path.dirname(manifestPath); const isEnabled = isBundled || extensionsStore.isEnabled(installedManifestPath); @@ -319,7 +370,7 @@ export class ExtensionDiscovery { } const absPath = path.resolve(folderPath, fileName); - const extension = await this.loadExtensionFromPath(absPath, { isBundled: true }); + const extension = await this.loadExtensionFromFolder(absPath, { isBundled: true }); if (extension) { extensions.push(extension); @@ -354,7 +405,7 @@ export class ExtensionDiscovery { continue; } - const extension = await this.loadExtensionFromPath(absPath); + const extension = await this.loadExtensionFromFolder(absPath); if (extension) { extensions.push(extension); @@ -368,8 +419,9 @@ export class ExtensionDiscovery { /** * Loads extension from absolute path, updates this.packagesJson to include it and returns the extension. + * @param absPath Folder path to extension */ - async loadExtensionFromPath(absPath: string, { isBundled = false }: { + async loadExtensionFromFolder(absPath: string, { isBundled = false }: { isBundled?: boolean; } = {}): Promise { const manifestPath = path.resolve(absPath, manifestFilename); diff --git a/src/jest.setup.ts b/src/jest.setup.ts index cccef274f0..ef6565c907 100644 --- a/src/jest.setup.ts +++ b/src/jest.setup.ts @@ -1,3 +1,6 @@ import fetchMock from "jest-fetch-mock"; // rewire global.fetch to call 'fetchMock' fetchMock.enableMocks(); + +// Mock __non_webpack_require__ for tests +globalThis.__non_webpack_require__ = jest.fn(); diff --git a/src/main/helm/helm-repo-manager.ts b/src/main/helm/helm-repo-manager.ts index ae8595dae9..74afb166b1 100644 --- a/src/main/helm/helm-repo-manager.ts +++ b/src/main/helm/helm-repo-manager.ts @@ -22,7 +22,7 @@ export interface HelmRepo { cacheFilePath?: string caFile?: string, certFile?: string, - insecure_skip_tls_verify?: boolean, + insecureSkipTlsVerify?: boolean, keyFile?: string, username?: string, password?: string, @@ -131,6 +131,25 @@ export class HelmRepoManager extends Singleton { return stdout; } + public async addСustomRepo(repoAttributes : HelmRepo) { + logger.info(`[HELM]: adding repo "${repoAttributes.name}" from ${repoAttributes.url}`); + const helm = await helmCli.binaryPath(); + + const insecureSkipTlsVerify = repoAttributes.insecureSkipTlsVerify ? " --insecure-skip-tls-verify" : ""; + const username = repoAttributes.username ? ` --username "${repoAttributes.username}"` : ""; + const password = repoAttributes.password ? ` --password "${repoAttributes.password}"` : ""; + const caFile = repoAttributes.caFile ? ` --ca-file "${repoAttributes.caFile}"` : ""; + const keyFile = repoAttributes.keyFile ? ` --key-file "${repoAttributes.keyFile}"` : ""; + const certFile = repoAttributes.certFile ? ` --cert-file "${repoAttributes.certFile}"` : ""; + + const addRepoCommand = `"${helm}" repo add ${repoAttributes.name} ${repoAttributes.url}${insecureSkipTlsVerify}${username}${password}${caFile}${keyFile}${certFile}`; + const { stdout } = await promiseExec(addRepoCommand).catch((error) => { + throw(error.stderr); + }); + + return stdout; + } + public async removeRepo({ name, url }: HelmRepo): Promise { logger.info(`[HELM]: removing repo "${name}" from ${url}`); const helm = await helmCli.binaryPath(); diff --git a/src/main/index.ts b/src/main/index.ts index cad2235743..ea3c53f9b9 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -115,7 +115,7 @@ app.on("activate", (event, hasVisibleWindows) => { logger.info("APP:ACTIVATE", { hasVisibleWindows }); if (!hasVisibleWindows) { - windowManager.initMainWindow(); + windowManager?.initMainWindow(false); } }); diff --git a/src/renderer/api/endpoints/pods.api.ts b/src/renderer/api/endpoints/pods.api.ts index 11b581db8f..447503558b 100644 --- a/src/renderer/api/endpoints/pods.api.ts +++ b/src/renderer/api/endpoints/pods.api.ts @@ -67,7 +67,7 @@ export interface IPodContainer { image: string; command?: string[]; args?: string[]; - ports: { + ports?: { name?: string; containerPort: number; protocol: string; @@ -137,7 +137,7 @@ interface IContainerProbe { export interface IPodContainerStatus { name: string; - state: { + state?: { [index: string]: object; running?: { startedAt: string; @@ -153,21 +153,28 @@ export interface IPodContainerStatus { reason: string; }; }; - lastState: { + lastState?: { [index: string]: object; + running?: { + startedAt: string; + }; + waiting?: { + reason: string; + message: string; + }; terminated?: { startedAt: string; finishedAt: string; exitCode: number; reason: string; - containerID: string; }; }; ready: boolean; restartCount: number; image: string; imageID: string; - containerID: string; + containerID?: string; + started?: boolean; } @autobind() @@ -196,28 +203,44 @@ export class Pod extends WorkloadKubeObject { }[]; initContainers: IPodContainer[]; containers: IPodContainer[]; - restartPolicy: string; - terminationGracePeriodSeconds: number; - dnsPolicy: string; + restartPolicy?: string; + terminationGracePeriodSeconds?: number; + activeDeadlineSeconds?: number; + dnsPolicy?: string; serviceAccountName: string; serviceAccount: string; - priority: number; - priorityClassName: string; - nodeName: string; + automountServiceAccountToken?: boolean; + priority?: number; + priorityClassName?: string; + nodeName?: string; nodeSelector?: { [selector: string]: string; }; - securityContext: {}; - schedulerName: string; - tolerations: { - key: string; - operator: string; - effect: string; - tolerationSeconds: number; + securityContext?: {}; + imagePullSecrets?: { + name: string; }[]; - affinity: IAffinity; + hostNetwork?: boolean; + hostPID?: boolean; + hostIPC?: boolean; + shareProcessNamespace?: boolean; + hostname?: string; + subdomain?: string; + schedulerName?: string; + tolerations?: { + key?: string; + operator?: string; + effect?: string; + tolerationSeconds?: number; + value?: string; + }[]; + hostAliases?: { + ip: string; + hostnames: string[]; + }; + affinity?: IAffinity; }; - status: { + status?: { phase: string; conditions: { type: string; @@ -230,7 +253,7 @@ export class Pod extends WorkloadKubeObject { startTime: string; initContainerStatuses?: IPodContainerStatus[]; containerStatuses?: IPodContainerStatus[]; - qosClass: string; + qosClass?: string; reason?: string; }; diff --git a/src/renderer/api/endpoints/replica-set.api.ts b/src/renderer/api/endpoints/replica-set.api.ts index 999de8c1ac..eb1131f645 100644 --- a/src/renderer/api/endpoints/replica-set.api.ts +++ b/src/renderer/api/endpoints/replica-set.api.ts @@ -1,51 +1,78 @@ import get from "lodash/get"; import { autobind } from "../../utils"; -import { IAffinity, WorkloadKubeObject } from "../workload-kube-object"; -import { IPodContainer } from "./pods.api"; +import { WorkloadKubeObject } from "../workload-kube-object"; +import { IPodContainer, Pod } from "./pods.api"; import { KubeApi } from "../kube-api"; +export class ReplicaSetApi extends KubeApi { + protected getScaleApiUrl(params: { namespace: string; name: string }) { + return `${this.getUrl(params)}/scale`; + } + + getReplicas(params: { namespace: string; name: string }): Promise { + return this.request + .get(this.getScaleApiUrl(params)) + .then(({ status }: any) => status?.replicas); + } + + scale(params: { namespace: string; name: string }, replicas: number) { + return this.request.put(this.getScaleApiUrl(params), { + data: { + metadata: params, + spec: { + replicas + } + } + }); + } +} + @autobind() export class ReplicaSet extends WorkloadKubeObject { static kind = "ReplicaSet"; static namespaced = true; static apiBase = "/apis/apps/v1/replicasets"; - spec: { replicas?: number; - selector?: { - matchLabels: { - [key: string]: string; - }; - }; - containers?: IPodContainer[]; + selector: { matchLabels: { [app: string]: string } }; template?: { - spec?: { - affinity?: IAffinity; - nodeSelector?: { - [selector: string]: string; + metadata: { + labels: { + app: string; }; - tolerations: { - key: string; - operator: string; - effect: string; - tolerationSeconds: number; - }[]; - containers: IPodContainer[]; }; + spec?: Pod["spec"]; }; - restartPolicy?: string; - terminationGracePeriodSeconds?: number; - dnsPolicy?: string; - schedulerName?: string; + minReadySeconds?: number; }; status: { replicas: number; - fullyLabeledReplicas: number; - readyReplicas: number; - availableReplicas: number; - observedGeneration: number; + fullyLabeledReplicas?: number; + readyReplicas?: number; + availableReplicas?: number; + observedGeneration?: number; + conditions?: { + type: string; + status: string; + lastUpdateTime: string; + lastTransitionTime: string; + reason: string; + message: string; + }[]; }; + getDesired() { + return this.spec.replicas || 0; + } + + getCurrent() { + return this.status.availableReplicas || 0; + } + + getReady() { + return this.status.readyReplicas || 0; + } + getImages() { const containers: IPodContainer[] = get(this, "spec.template.spec.containers", []); @@ -53,6 +80,6 @@ export class ReplicaSet extends WorkloadKubeObject { } } -export const replicaSetApi = new KubeApi({ +export const replicaSetApi = new ReplicaSetApi({ objectConstructor: ReplicaSet, }); diff --git a/src/renderer/components/+extensions/__tests__/extensions.test.tsx b/src/renderer/components/+extensions/__tests__/extensions.test.tsx index cb4db0fece..8899d9d74c 100644 --- a/src/renderer/components/+extensions/__tests__/extensions.test.tsx +++ b/src/renderer/components/+extensions/__tests__/extensions.test.tsx @@ -68,7 +68,7 @@ describe("Extensions", () => { // Approve confirm dialog fireEvent.click(screen.getByText("Yes")); - expect(extensionDiscovery.uninstallExtension).toHaveBeenCalledWith("/absolute/path"); + expect(extensionDiscovery.uninstallExtension).toHaveBeenCalled(); expect(screen.getByText("Disable").closest("button")).toBeDisabled(); expect(screen.getByText("Uninstall").closest("button")).toBeDisabled(); }); diff --git a/src/renderer/components/+extensions/extensions.tsx b/src/renderer/components/+extensions/extensions.tsx index 6a94b49480..cb38791a03 100644 --- a/src/renderer/components/+extensions/extensions.tsx +++ b/src/renderer/components/+extensions/extensions.tsx @@ -413,7 +413,7 @@ export class Extensions extends React.Component { displayName }); - await extensionDiscovery.uninstallExtension(extension.absolutePath); + await extensionDiscovery.uninstallExtension(extension); } catch (error) { Notifications.error(

Uninstalling extension {displayName} has failed: {error?.message ?? ""}

diff --git a/src/renderer/components/+preferences/add-helm-repo-dialog.scss b/src/renderer/components/+preferences/add-helm-repo-dialog.scss new file mode 100644 index 0000000000..ab3ef22f7e --- /dev/null +++ b/src/renderer/components/+preferences/add-helm-repo-dialog.scss @@ -0,0 +1,22 @@ +.AddHelmRepoDialog { + --flex-gap: #{$margin * 1.5}; + + .accordion { + font-weight: bold; + align-self: flex-start; + padding: 0; + &:focus:not(:active) { + box-shadow: none; + } + } + .Icon { + --color-active: black; + } + + .fields-title { + display: grid; + grid-template-columns: min-content auto; + align-items: center; + gap: $margin / 2; + } +} diff --git a/src/renderer/components/+preferences/add-helm-repo-dialog.tsx b/src/renderer/components/+preferences/add-helm-repo-dialog.tsx new file mode 100644 index 0000000000..5931df5f35 --- /dev/null +++ b/src/renderer/components/+preferences/add-helm-repo-dialog.tsx @@ -0,0 +1,178 @@ +import "./add-helm-repo-dialog.scss"; + +import React from "react"; +import { remote, FileFilter } from "electron"; +import { observable } from "mobx"; +import { observer } from "mobx-react"; +import { t, Trans } from "@lingui/macro"; +import { _i18n } from "../../i18n"; +import { Dialog, DialogProps } from "../dialog"; +import { Wizard, WizardStep } from "../wizard"; +import { Input } from "../input"; +import { Checkbox } from "../checkbox"; +import { Button } from "../button"; +import { systemName, isUrl, isPath } from "../input/input_validators"; +import { SubTitle } from "../layout/sub-title"; +import { Icon } from "../icon"; +import { Notifications } from "../notifications"; +import { HelmRepo, repoManager } from "../../../main/helm/helm-repo-manager"; + +interface Props extends Partial { + onAddRepo: Function +} + +enum FileType { + CaFile = "caFile", + KeyFile = "keyFile", + CertFile = "certFile", +} + +@observer +export class AddHelmRepoDialog extends React.Component { + private emptyRepo = {name: "", url: "", username: "", password: "", insecureSkipTlsVerify: false, caFile:"", keyFile: "", certFile: ""}; + + private static keyExtensions = ["key", "keystore", "jks", "p12", "pfx", "pem"]; + private static certExtensions = ["crt", "cer", "ca-bundle", "p7b", "p7c" , "p7s", "p12", "pfx", "pem"]; + + @observable static isOpen = false; + + static open() { + AddHelmRepoDialog.isOpen = true; + } + + static close() { + AddHelmRepoDialog.isOpen = false; + } + + @observable helmRepo : HelmRepo = this.emptyRepo; + @observable showOptions = false; + + close = () => { + AddHelmRepoDialog.close(); + this.helmRepo = this.emptyRepo; + this.showOptions = false; + }; + + setFilepath(type: FileType, value: string) { + this.helmRepo[type] = value; + } + + getFilePath(type: FileType) : string { + return this.helmRepo[type]; + } + + async selectFileDialog(type: FileType, fileFilter: FileFilter) { + const { dialog, BrowserWindow } = remote; + const { canceled, filePaths } = await dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), { + defaultPath: this.getFilePath(type), + properties: ["openFile", "showHiddenFiles"], + message: _i18n._(t`Select file`), + buttonLabel: _i18n._(t`Use file`), + filters: [ + fileFilter, + { name: "Any", extensions: ["*"]} + ] + }); + + if (!canceled && filePaths.length) { + this.setFilepath(type, filePaths[0]); + } + } + + async addCustomRepo() { + try { + await repoManager.addСustomRepo(this.helmRepo); + Notifications.ok(Helm repository {this.helmRepo.name} has added); + this.props.onAddRepo(); + this.close(); + } catch (err) { + Notifications.error(Adding helm branch {this.helmRepo.name} has failed: {String(err)}); + } + } + + renderFileInput(placeholder:string, fileType:FileType ,fileExtensions:string[]){ + return( +
+ this.setFilepath(fileType, v)} + /> + this.selectFileDialog(fileType, {name: placeholder, extensions: fileExtensions})} + tooltip={Browse} + /> +
); + } + + renderOptions() { + return ( + <> + Security settings} /> + this.helmRepo.insecureSkipTlsVerify = v} + /> + {this.renderFileInput(_i18n._(t`Key file`), FileType.KeyFile, AddHelmRepoDialog.keyExtensions)} + {this.renderFileInput(_i18n._(t`Ca file`), FileType.CaFile, AddHelmRepoDialog.certExtensions)} + {this.renderFileInput(_i18n._(t`Cerificate file`), FileType.CertFile, AddHelmRepoDialog.certExtensions)} + Chart Repository Credentials} /> + this.helmRepo.username = v} + /> + this.helmRepo.password = v} + /> + ); + } + + render() { + const { ...dialogProps } = this.props; + + const header =
Add custom Helm Repo
; + + return ( + + + Add} next={()=>{this.addCustomRepo();}}> +
+ this.helmRepo.name = v} + /> + this.helmRepo.url = v} + /> + + {this.showOptions && this.renderOptions()} +
+
+
+
+ ); + } +} diff --git a/src/renderer/components/+preferences/preferences.tsx b/src/renderer/components/+preferences/preferences.tsx index 69971acad5..68bfd7fa20 100644 --- a/src/renderer/components/+preferences/preferences.tsx +++ b/src/renderer/components/+preferences/preferences.tsx @@ -13,11 +13,13 @@ import { Input } from "../input"; import { Checkbox } from "../checkbox"; import { Notifications } from "../notifications"; import { Badge } from "../badge"; +import { Button } from "../button"; import { themeStore } from "../../theme.store"; import { Tooltip } from "../tooltip"; import { KubectlBinaries } from "./kubectl-binaries"; import { appPreferenceRegistry } from "../../../extensions/registries/app-preference-registry"; import { PageLayout } from "../layout/page-layout"; +import { AddHelmRepoDialog } from "./add-helm-repo-dialog"; @observer export class Preferences extends React.Component { @@ -134,16 +136,25 @@ export class Preferences extends React.Component {

Helm

- Repositories} + isLoading={this.helmLoading} + isDisabled={this.helmLoading} + options={this.helmOptions} + onChange={this.onRepoSelect} + formatOptionLabel={this.formatHelmOptionLabel} + controlShouldRenderValue={false} + className="box grow" + /> +