diff --git a/docs/clusters/adding-clusters.md b/docs/clusters/adding-clusters.md
index 87a228567d..d153d8c9bf 100644
--- a/docs/clusters/adding-clusters.md
+++ b/docs/clusters/adding-clusters.md
@@ -1,6 +1,6 @@
# Adding Clusters
-Add clusters by clicking the **Add Cluster** button in the left-side menu.
+Add clusters by clicking the **Add Cluster** button in the left-side menu.
1. Click the **Add Cluster** button (indicated with a '+' icon).
2. Enter the path to your kubeconfig file. You'll need to have a kubeconfig file for the cluster you want to add. You can either browse for the path from the file system or or enter it directly.
@@ -13,4 +13,10 @@ Selected [cluster contexts](https://kubernetes.io/docs/concepts/configuration/or
For more information on kubeconfig see [Kubernetes docs](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/).
-To see your currently-enabled config with `kubectl`, enter `kubectl config view --minify --raw` in your terminal.
\ No newline at end of file
+To see your currently-enabled config with `kubectl`, enter `kubectl config view --minify --raw` in your terminal.
+
+When connecting to a cluster, make sure you have a valid and working kubeconfig for the cluster. Following lists known "gotchas" in some authentication types used in kubeconfig with Lens app.
+
+## Exec auth plugins
+
+When using [exec auth](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#configuration) plugins make sure the paths that are used to call any binaries are full paths as Lens app might not be able to call binaries with relative paths. Make also sure that you pass all needed information either as arguments or env variables in the config, Lens app might not have all login shell env variables set automatically.
diff --git a/docs/clusters/images/add-cluster.png b/docs/clusters/images/add-cluster.png
index b53c09f704..fa7b632026 100644
Binary files a/docs/clusters/images/add-cluster.png and b/docs/clusters/images/add-cluster.png differ
diff --git a/docs/extensions/guides/README.md b/docs/extensions/guides/README.md
index 2917090212..2f04edfea1 100644
--- a/docs/extensions/guides/README.md
+++ b/docs/extensions/guides/README.md
@@ -19,10 +19,11 @@ Each guide or sample will include:
| [Stores](stores.md) | |
| [Components](components.md) | |
| [KubeObjectListLayout](kube-object-list-layout.md) | |
+| [Working with mobx](working-with-mobx.md) | |
## Samples
| Sample | APIs |
| ----- | ----- |
[helloworld](https://github.com/lensapp/lens-extension-samples/tree/master/helloworld-sample) | LensMainExtension LensRendererExtension Component.Icon Component.IconProps |
-[minikube](https://github.com/lensapp/lens-extension-samples/tree/master/minikube-sample) | LensMainExtension Store.clusterStore Store.workspaceStore |
\ No newline at end of file
+[minikube](https://github.com/lensapp/lens-extension-samples/tree/master/minikube-sample) | LensMainExtension Store.clusterStore Store.workspaceStore |
diff --git a/docs/extensions/guides/working-with-mobx.md b/docs/extensions/guides/working-with-mobx.md
new file mode 100644
index 0000000000..5577ff6bdc
--- /dev/null
+++ b/docs/extensions/guides/working-with-mobx.md
@@ -0,0 +1,23 @@
+# Working with mobx
+
+## Introduction
+
+Lens uses `mobx` as its state manager on top of React's state management system.
+This helps with having a more declarative style of managing state, as opposed to `React`'s native `setState` mechanism.
+You should already have a basic understanding of how `React` handles state ([read here](https://reactjs.org/docs/faq-state.html) for more information).
+However, if you do not, here is a quick overview.
+
+- A `React.Component` is generic over both `Props` and `State` (with default empty object types).
+- `Props` should be considered read-only from the point of view of the component and is the mechanism for passing in "arguments" to a component.
+- `State` is a component's internal state and can be read by accessing the parent field `state`.
+- `State` **must** be updated using the `setState` parent method which merges the new data with the old state.
+- `React` does do some optimizations around re-rendering components after quick successions of `setState` calls.
+
+## How mobx works:
+
+`mobx` is a package that provides an abstraction over `React`'s state management. The three main concepts are:
+- `observable`: data stored in the component's `state`
+- `action`: a function that modifies any `observable` data
+- `computed`: data that is derived from `observable` data but is not actually stored. Think of this as computing `isEmpty` vs an `observable` field called `count`.
+
+Further reading is available from `mobx`'s [website](https://mobx.js.org/the-gist-of-mobx.html).
diff --git a/extensions/metrics-cluster-feature/src/metrics-feature.ts b/extensions/metrics-cluster-feature/src/metrics-feature.ts
index 4787280b61..f843fe1506 100644
--- a/extensions/metrics-cluster-feature/src/metrics-feature.ts
+++ b/extensions/metrics-cluster-feature/src/metrics-feature.ts
@@ -28,7 +28,7 @@ export class MetricsFeature extends ClusterFeature.Feature {
name = "metrics";
latestVersion = "v2.17.2-lens1";
- config: MetricsConfiguration = {
+ templateContext: MetricsConfiguration = {
persistence: {
enabled: false,
storageClass: null,
@@ -53,12 +53,12 @@ export class MetricsFeature extends ClusterFeature.Feature {
// Check if there are storageclasses
const storageClassApi = K8sApi.forCluster(cluster, K8sApi.StorageClass);
const scs = await storageClassApi.list();
- this.config.persistence.enabled = scs.some(sc => (
+ this.templateContext.persistence.enabled = scs.some(sc => (
sc.metadata?.annotations?.['storageclass.kubernetes.io/is-default-class'] === 'true' ||
sc.metadata?.annotations?.['storageclass.beta.kubernetes.io/is-default-class'] === 'true'
));
- super.applyResources(cluster, super.renderTemplates(path.join(__dirname, "../resources/")));
+ super.applyResources(cluster, path.join(__dirname, "../resources/"));
}
async upgrade(cluster: Store.Cluster): Promise {
diff --git a/extensions/telemetry/src/tracker.ts b/extensions/telemetry/src/tracker.ts
index 8c2fdab8e4..982ae146c4 100644
--- a/extensions/telemetry/src/tracker.ts
+++ b/extensions/telemetry/src/tracker.ts
@@ -118,7 +118,8 @@ export class Tracker extends Util.Singleton {
kubernetesVersion: cluster.metadata.version,
distribution: cluster.metadata.distribution,
nodesCount: cluster.metadata.nodes,
- lastSeen: cluster.metadata.lastSeen
+ lastSeen: cluster.metadata.lastSeen,
+ prometheus: cluster.metadata.prometheus
});
}
diff --git a/integration/__tests__/app.tests.ts b/integration/__tests__/app.tests.ts
index c8c7af7c8b..eb67fc9ee8 100644
--- a/integration/__tests__/app.tests.ts
+++ b/integration/__tests__/app.tests.ts
@@ -136,6 +136,7 @@ describe("Lens integration tests", () => {
it('adds cluster in test-workspace', async () => {
await app.client.click('#current-workspace .Icon');
+ await app.client.waitForVisible('.WorkspaceMenu li[title="test description"]');
await app.client.click('.WorkspaceMenu li[title="test description"]');
await addMinikubeCluster(app);
await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started");
@@ -144,6 +145,7 @@ describe("Lens integration tests", () => {
it('checks if default workspace has active cluster', async () => {
await app.client.click('#current-workspace .Icon');
+ await app.client.waitForVisible('.WorkspaceMenu > li:first-of-type');
await app.client.click('.WorkspaceMenu > li:first-of-type');
await app.client.waitForVisible(".ClustersMenu .ClusterIcon.active");
});
diff --git a/locales/en/messages.po b/locales/en/messages.po
index 02ddfdb227..c66819dd24 100644
--- a/locales/en/messages.po
+++ b/locales/en/messages.po
@@ -738,6 +738,7 @@ msgstr "Current / Target"
msgid "Current Healthy"
msgstr "Current Healthy"
+#: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:101
#: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:103
msgid "Current replica scale: {currentReplicas}"
msgstr "Current replica scale: {currentReplicas}"
@@ -828,6 +829,7 @@ msgstr "Description"
msgid "Desired Healthy"
msgstr "Desired Healthy"
+#: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:105
#: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:107
msgid "Desired number of replicas"
msgstr "Desired number of replicas"
@@ -1091,6 +1093,7 @@ msgstr "Helm branch <0>{0}0> already in use"
msgid "Hide"
msgstr "Hide"
+#: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:127
#: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:116
msgid "High number of replicas may cause cluster performance issues"
msgstr "High number of replicas may cause cluster performance issues"
@@ -2298,6 +2301,7 @@ msgstr "Runtime Class"
msgid "Save"
msgstr "Save"
+#: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:155
#: 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
@@ -2308,6 +2312,10 @@ msgstr "Scale"
msgid "Scale Deployment <0>{deploymentName}0>"
msgstr "Scale Deployment <0>{deploymentName}0>"
+#: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:139
+msgid "Scale Stateful Set <0>{statefulSetName}0>"
+msgstr "Scale Stateful Set <0>{statefulSetName}0>"
+
#: src/renderer/components/+workloads-cronjobs/cronjob-details.tsx:45
#: src/renderer/components/+workloads-cronjobs/cronjobs.tsx:46
msgid "Schedule"
diff --git a/locales/fi/messages.po b/locales/fi/messages.po
index 0b668b7605..c5192a7eb9 100644
--- a/locales/fi/messages.po
+++ b/locales/fi/messages.po
@@ -734,6 +734,7 @@ msgstr ""
msgid "Current Healthy"
msgstr ""
+#: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:101
#: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:103
msgid "Current replica scale: {currentReplicas}"
msgstr ""
@@ -824,6 +825,7 @@ msgstr ""
msgid "Desired Healthy"
msgstr ""
+#: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:105
#: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:107
msgid "Desired number of replicas"
msgstr ""
@@ -1082,6 +1084,7 @@ msgstr ""
msgid "Hide"
msgstr ""
+#: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:127
#: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:116
msgid "High number of replicas may cause cluster performance issues"
msgstr ""
@@ -2281,6 +2284,7 @@ msgstr ""
msgid "Save"
msgstr ""
+#: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:155
#: 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
@@ -2291,6 +2295,10 @@ msgstr ""
msgid "Scale Deployment <0>{deploymentName}0>"
msgstr ""
+#: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:139
+msgid "Scale Stateful Set <0>{statefulSetName}0>"
+msgstr ""
+
#: src/renderer/components/+workloads-cronjobs/cronjob-details.tsx:45
#: src/renderer/components/+workloads-cronjobs/cronjobs.tsx:46
msgid "Schedule"
diff --git a/locales/ru/messages.po b/locales/ru/messages.po
index dc947d724c..de4ee7c7fb 100644
--- a/locales/ru/messages.po
+++ b/locales/ru/messages.po
@@ -739,6 +739,7 @@ msgstr "Текущее / Цель"
msgid "Current Healthy"
msgstr ""
+#: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:101
#: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:103
msgid "Current replica scale: {currentReplicas}"
msgstr "Текущий размер реплики: {currentReplicas}"
@@ -829,6 +830,7 @@ msgstr "Описание"
msgid "Desired Healthy"
msgstr ""
+#: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:105
#: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:107
msgid "Desired number of replicas"
msgstr "Нужный уровень реплик"
@@ -1092,6 +1094,7 @@ msgstr ""
msgid "Hide"
msgstr "Скрыть"
+#: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:127
#: src/renderer/components/+workloads-deployments/deployment-scale-dialog.tsx:116
msgid "High number of replicas may cause cluster performance issues"
msgstr "Большое количество реплик может вызвать проблемы с производительностью кластера"
@@ -2299,6 +2302,7 @@ msgstr ""
msgid "Save"
msgstr "Сохранить"
+#: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:155
#: 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
@@ -2309,6 +2313,10 @@ msgstr "Масштабировать"
msgid "Scale Deployment <0>{deploymentName}0>"
msgstr "Масштабировать Deployment <0>{deploymentName}0>"
+#: src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.tsx:139
+msgid "Scale Stateful Set <0>{statefulSetName}0>"
+msgstr "Масштабировать Stateful Set <0>{statefulSetName}0>"
+
#: src/renderer/components/+workloads-cronjobs/cronjob-details.tsx:45
#: src/renderer/components/+workloads-cronjobs/cronjobs.tsx:46
msgid "Schedule"
diff --git a/mkdocs.yml b/mkdocs.yml
index f225d2250c..3e95eae065 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -33,6 +33,7 @@ nav:
- Main Extension: extensions/guides/main-extension.md
- Renderer Extension: extensions/guides/renderer-extension.md
- Generator: extensions/guides/generator.md
+ - Working with mobx: extensions/guides/working-with-mobx.md
- Testing and Publishing:
- Testing Extensions: extensions/testing-and-publishing/testing.md
- Publishing Extensions: extensions/testing-and-publishing/publishing.md
@@ -62,7 +63,6 @@ theme:
icon: material/toggle-switch-off-outline
name: Switch to dark mode
features:
- - navigation.instant
- toc.autohide
- search.suggest
- search.highlight
diff --git a/package.json b/package.json
index 53d67fb862..869f7453c3 100644
--- a/package.json
+++ b/package.json
@@ -21,7 +21,7 @@
"compile:main": "yarn run webpack --config webpack.main.ts",
"compile:renderer": "yarn run webpack --config webpack.renderer.ts",
"compile:i18n": "yarn run lingui compile",
- "compile:extension-types": "yarn run tsc -p ./tsconfig.extensions.json --outDir src/extensions/npm/extensions/dist",
+ "compile:extension-types": "yarn run webpack --config webpack.extensions.ts",
"npm:fix-package-version": "yarn run ts-node build/set_npm_version.ts",
"build:linux": "yarn run compile && electron-builder --linux --dir -c.productName=Lens",
"build:mac": "yarn run compile && electron-builder --mac --dir -c.productName=Lens",
@@ -225,7 +225,6 @@
"electron-devtools-installer": "^3.1.1",
"electron-updater": "^4.3.1",
"electron-window-state": "^5.0.3",
- "file-type": "^14.7.1",
"filenamify": "^4.1.0",
"fs-extra": "^9.0.1",
"handlebars": "^4.7.6",
@@ -362,6 +361,7 @@
"nodemon": "^2.0.4",
"patch-package": "^6.2.2",
"postinstall-postinstall": "^2.1.0",
+ "prettier": "^2.2.0",
"progress-bar-webpack-plugin": "^2.1.0",
"raw-loader": "^4.0.1",
"react": "^16.14.0",
diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts
index 907f9b7aad..35ec663a55 100644
--- a/src/common/cluster-store.ts
+++ b/src/common/cluster-store.ts
@@ -2,7 +2,7 @@ import { workspaceStore } from "./workspace-store";
import path from "path";
import { app, ipcRenderer, remote, webFrame } from "electron";
import { unlink } from "fs-extra";
-import { action, computed, observable, reaction, toJS } from "mobx";
+import { action, comparer, computed, observable, reaction, toJS } from "mobx";
import { BaseStore } from "./base-store";
import { Cluster, ClusterState } from "../main/cluster";
import migrations from "../migrations/cluster-store";
@@ -23,9 +23,15 @@ export interface ClusterIconUpload {
}
export interface ClusterMetadata {
- [key: string]: string | number | boolean;
+ [key: string]: string | number | boolean | object;
}
+export type ClusterPrometheusMetadata = {
+ success?: boolean;
+ provider?: string;
+ autoDetected?: boolean;
+};
+
export interface ClusterStoreModel {
activeCluster?: ClusterId; // last opened cluster
clusters?: ClusterModel[]
@@ -47,9 +53,15 @@ export interface ClusterModel {
kubeConfig?: string; // yaml
}
-export interface ClusterPreferences {
+export interface ClusterPreferences extends ClusterPrometheusPreferences{
terminalCWD?: string;
clusterName?: string;
+ iconOrder?: number;
+ icon?: string;
+ httpsProxy?: string;
+}
+
+export interface ClusterPrometheusPreferences {
prometheus?: {
namespace: string;
service: string;
@@ -59,9 +71,6 @@ export interface ClusterPreferences {
prometheusProvider?: {
type: string;
};
- iconOrder?: number;
- icon?: string;
- httpsProxy?: string;
}
export class ClusterStore extends BaseStore {
@@ -84,6 +93,9 @@ export class ClusterStore extends BaseStore {
super({
configName: "lens-cluster-store",
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
+ syncOptions: {
+ equals: comparer.structural,
+ },
migrations,
});
diff --git a/src/common/utils/downloadFile.ts b/src/common/utils/downloadFile.ts
index a58e9242b4..4c65901d3d 100644
--- a/src/common/utils/downloadFile.ts
+++ b/src/common/utils/downloadFile.ts
@@ -3,6 +3,7 @@ import request from "request";
export interface DownloadFileOptions {
url: string;
gzip?: boolean;
+ timeout?: number;
}
export interface DownloadFileTicket {
@@ -11,9 +12,9 @@ export interface DownloadFileTicket {
cancel(): void;
}
-export function downloadFile({ url, gzip = true }: DownloadFileOptions): DownloadFileTicket {
+export function downloadFile({ url, timeout, gzip = true }: DownloadFileOptions): DownloadFileTicket {
const fileChunks: Buffer[] = [];
- const req = request(url, { gzip });
+ const req = request(url, { gzip, timeout });
const promise: Promise = new Promise((resolve, reject) => {
req.on("data", (chunk: Buffer) => {
fileChunks.push(chunk);
diff --git a/src/common/utils/rectify-array.ts b/src/common/utils/rectify-array.ts
index 48feb3a165..0e4d701114 100644
--- a/src/common/utils/rectify-array.ts
+++ b/src/common/utils/rectify-array.ts
@@ -3,6 +3,6 @@
* @param items either one item or an array of items
* @returns a list of items
*/
-export function recitfy(items: T | T[]): T[] {
+export function rectify(items: T | T[]): T[] {
return Array.isArray(items) ? items : [items];
}
diff --git a/src/extensions/__tests__/extension-loader.test.ts b/src/extensions/__tests__/extension-loader.test.ts
new file mode 100644
index 0000000000..90aebfaee9
--- /dev/null
+++ b/src/extensions/__tests__/extension-loader.test.ts
@@ -0,0 +1,126 @@
+import { ExtensionLoader } from "../extension-loader";
+
+const manifestPath = "manifest/path";
+const manifestPath2 = "manifest/path2";
+const manifestPath3 = "manifest/path3";
+
+jest.mock(
+ "electron",
+ () => ({
+ ipcRenderer: {
+ invoke: jest.fn(async (channel: string, ...args: any[]) => {
+ if (channel === "extensions:loaded") {
+ return [
+ [
+ manifestPath,
+ {
+ manifest: {
+ name: "TestExtension",
+ version: "1.0.0",
+ },
+ absolutePath: "/test/1",
+ manifestPath,
+ isBundled: false,
+ isEnabled: true,
+ },
+ ],
+ [
+ manifestPath2,
+ {
+ manifest: {
+ name: "TestExtension2",
+ version: "2.0.0",
+ },
+ absolutePath: "/test/2",
+ manifestPath: manifestPath2,
+ isBundled: false,
+ isEnabled: true,
+ },
+ ],
+ ];
+ }
+ }),
+ on: jest.fn(
+ (channel: string, listener: (event: any, ...args: any[]) => void) => {
+ if (channel === "extensions:loaded") {
+ // First initialize with extensions 1 and 2
+ // and then broadcast event to remove extensioin 2 and add extension number 3
+ setTimeout(() => {
+ listener({}, [
+ [
+ manifestPath,
+ {
+ manifest: {
+ name: "TestExtension",
+ version: "1.0.0",
+ },
+ absolutePath: "/test/1",
+ manifestPath,
+ isBundled: false,
+ isEnabled: true,
+ },
+ ],
+ [
+ manifestPath3,
+ {
+ manifest: {
+ name: "TestExtension3",
+ version: "3.0.0",
+ },
+ absolutePath: "/test/3",
+ manifestPath: manifestPath3,
+ isBundled: false,
+ isEnabled: true,
+ },
+ ],
+ ]);
+ }, 10);
+ }
+ }
+ ),
+ },
+ }),
+ {
+ virtual: true,
+ }
+);
+
+describe("ExtensionLoader", () => {
+ it("renderer updates extension after ipc broadcast", async (done) => {
+ const extensionLoader = new ExtensionLoader();
+
+ expect(extensionLoader.userExtensions).toMatchInlineSnapshot(`Map {}`);
+
+ await extensionLoader.init();
+
+ setTimeout(() => {
+ // Assert the extensions after the extension broadcast event
+ expect(extensionLoader.userExtensions).toMatchInlineSnapshot(`
+ Map {
+ "manifest/path" => Object {
+ "absolutePath": "/test/1",
+ "isBundled": false,
+ "isEnabled": true,
+ "manifest": Object {
+ "name": "TestExtension",
+ "version": "1.0.0",
+ },
+ "manifestPath": "manifest/path",
+ },
+ "manifest/path3" => Object {
+ "absolutePath": "/test/3",
+ "isBundled": false,
+ "isEnabled": true,
+ "manifest": Object {
+ "name": "TestExtension3",
+ "version": "3.0.0",
+ },
+ "manifestPath": "manifest/path3",
+ },
+ }
+ `);
+
+ done();
+ }, 10);
+ });
+});
diff --git a/src/extensions/__tests__/lens-extension.test.ts b/src/extensions/__tests__/lens-extension.test.ts
index d6ba04cbb5..277a76b410 100644
--- a/src/extensions/__tests__/lens-extension.test.ts
+++ b/src/extensions/__tests__/lens-extension.test.ts
@@ -9,6 +9,7 @@ describe("lens extension", () => {
name: "foo-bar",
version: "0.1.1"
},
+ absolutePath: "/absolute/fake/",
manifestPath: "/this/is/fake/package.json",
isBundled: false,
isEnabled: true
diff --git a/src/extensions/cluster-feature.ts b/src/extensions/cluster-feature.ts
index 6381d267cc..4cb2c9bf5a 100644
--- a/src/extensions/cluster-feature.ts
+++ b/src/extensions/cluster-feature.ts
@@ -10,17 +10,27 @@ import { requestMain } from "../common/ipc";
import { clusterKubectlApplyAllHandler } from "../common/cluster-ipc";
export interface ClusterFeatureStatus {
+ /** feature's current version, as set by the implementation */
currentVersion: string;
- installed: boolean;
+ /** feature's latest version, as set by the implementation */
latestVersion: string;
+ /** whether the feature is installed or not, as set by the implementation */
+ installed: boolean;
+ /** whether the feature can be upgraded or not, as set by the implementation */
canUpgrade: boolean;
}
export abstract class ClusterFeature {
- name: string;
- latestVersion: string;
- config: any;
+ /**
+ * this field sets the template parameters that are to be applied to any templated kubernetes resources that are to be installed for the feature.
+ * See the renderTemplates() method for more details
+ */
+ templateContext: any;
+
+ /**
+ * this field holds the current feature status, is accessed directly by Lens
+ */
@observable status: ClusterFeatureStatus = {
currentVersion: null,
installed: false,
@@ -28,15 +38,59 @@ export abstract class ClusterFeature {
canUpgrade: false
};
+ /**
+ * to be implemented in the derived class, this method is typically called by Lens when a user has indicated that this feature is to be installed. The implementation
+ * of this method should install kubernetes resources using the applyResources() method, or by directly accessing the kubernetes api (K8sApi)
+ *
+ * @param cluster the cluster that the feature is to be installed on
+ */
abstract async install(cluster: Cluster): Promise;
+ /**
+ * to be implemented in the derived class, this method is typically called by Lens when a user has indicated that this feature is to be ugraded. The implementation
+ * of this method should upgrade the kubernetes resources already installed, if relevant to the feature
+ *
+ * @param cluster the cluster that the feature is to be upgraded on
+ */
abstract async upgrade(cluster: Cluster): Promise;
+ /**
+ * to be implemented in the derived class, this method is typically called by Lens when a user has indicated that this feature is to be uninstalled. The implementation
+ * of this method should install kubernetes resources using the kubernetes api (K8sApi)
+ *
+ * @param cluster the cluster that the feature is to be uninstalled from
+ */
abstract async uninstall(cluster: Cluster): Promise;
+ /**
+ * to be implemented in the derived class, this method is called periodically by Lens to determine details about the feature's current status. The implementation
+ * of this method should provide the current status information. The currentVersion and latestVersion fields may be displayed by Lens in describing the feature.
+ * The installed field should be set to true if the feature has been installed, otherwise false. Also, Lens relies on the canUpgrade field to determine if the feature
+ * can be upgraded so the implementation should set the canUpgrade field according to specific rules for the feature, if relevant.
+ *
+ * @param cluster the cluster that the feature may be installed on
+ *
+ * @return a promise, resolved with the updated ClusterFeatureStatus
+ */
abstract async updateStatus(cluster: Cluster): Promise;
- protected async applyResources(cluster: Cluster, resources: string[]) {
+ /**
+ * this is a helper method that conveniently applies kubernetes resources to the cluster.
+ *
+ * @param cluster the cluster that the resources are to be applied to
+ * @param resourceSpec as a string type this is a folder path that is searched for files specifying kubernetes resources. The files are read and if any of the resource
+ * files are templated, the template parameters are filled using the templateContext field (See renderTemplate() method). Finally the resources are applied to the
+ * cluster. As a string[] type resourceSpec is treated as an array of fully formed (not templated) kubernetes resources that are applied to the cluster
+ */
+ protected async applyResources(cluster: Cluster, resourceSpec: string | string[]) {
+ let resources: string[];
+
+ if ( typeof resourceSpec === "string" ) {
+ resources = this.renderTemplates(resourceSpec);
+ } else {
+ resources = resourceSpec;
+ }
+
if (app) {
await new ResourceApplier(cluster).kubectlApplyAll(resources);
} else {
@@ -44,6 +98,14 @@ export abstract class ClusterFeature {
}
}
+ /**
+ * this is a helper method that conveniently reads kubernetes resource files into a string array. It also fills templated resource files with the template parameter values
+ * specified by the templateContext field. Templated files must end with the extension '.hb' and the template syntax must be compatible with handlebars.js
+ *
+ * @param folderPath this is a folder path that is searched for files defining kubernetes resources.
+ *
+ * @return an array of strings, each string being the contents of a resource file found in the folder path. This can be passed directly to applyResources()
+ */
protected renderTemplates(folderPath: string): string[] {
const resources: string[] = [];
logger.info(`[FEATURE]: render templates from ${folderPath}`);
@@ -52,7 +114,7 @@ export abstract class ClusterFeature {
const raw = fs.readFileSync(file);
if (filename.endsWith('.hb')) {
const template = hb.compile(raw.toString());
- resources.push(template(this.config));
+ resources.push(template(this.templateContext));
} else {
resources.push(raw.toString());
}
diff --git a/src/extensions/extension-discovery.ts b/src/extensions/extension-discovery.ts
index ab17606e92..452bba4d65 100644
--- a/src/extensions/extension-discovery.ts
+++ b/src/extensions/extension-discovery.ts
@@ -11,6 +11,12 @@ import type { LensExtensionId, LensExtensionManifest } from "./lens-extension";
export interface InstalledExtension {
readonly manifest: LensExtensionManifest;
+
+ // Absolute path to the non-symlinked source folder,
+ // e.g. "/Users/user/.k8slens/extensions/helloworld"
+ readonly absolutePath: string;
+
+ // Absolute to the symlinked package.json file
readonly manifestPath: string;
readonly isBundled: boolean; // defined in project root's package.json
isEnabled: boolean;
@@ -174,6 +180,24 @@ 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
+ */
+ async uninstallExtension(absolutePath: string) {
+ logger.info(`${logModule} Uninstalling ${absolutePath}`);
+
+ const exists = await fs.pathExists(absolutePath);
+
+ if (!exists) {
+ throw new Error(`Extension path ${absolutePath} doesn't exist`);
+ }
+
+ // fs.remove does nothing if the path doesn't exist anymore
+ await fs.remove(absolutePath);
+ }
+
async load(): Promise