();
diff --git a/src/common/utils/buildUrl.ts b/src/common/utils/buildUrl.ts
index cdaf36720c..c26e054491 100644
--- a/src/common/utils/buildUrl.ts
+++ b/src/common/utils/buildUrl.ts
@@ -1,4 +1,4 @@
-import { compile } from "path-to-regexp"
+import { compile } from "path-to-regexp";
export interface IURLParams {
params?: P;
@@ -8,7 +8,7 @@ export interface IURLParams
{
export function buildURL
(path: string | any) {
const pathBuilder = compile(String(path));
return function ({ params, query }: IURLParams
= {}) {
- const queryParams = query ? new URLSearchParams(Object.entries(query)).toString() : ""
- return pathBuilder(params) + (queryParams ? `?${queryParams}` : "")
- }
+ const queryParams = query ? new URLSearchParams(Object.entries(query)).toString() : "";
+ return pathBuilder(params) + (queryParams ? `?${queryParams}` : "");
+ };
}
diff --git a/src/common/utils/defineGlobal.ts b/src/common/utils/defineGlobal.ts
index 29e2e60ea0..1a4c5993d9 100755
--- a/src/common/utils/defineGlobal.ts
+++ b/src/common/utils/defineGlobal.ts
@@ -5,7 +5,7 @@
export function defineGlobal(propName: string, descriptor: PropertyDescriptor) {
const scope = typeof global !== "undefined" ? global : window;
if (scope.hasOwnProperty(propName)) {
- console.info(`Global variable "${propName}" already exists. Skipping.`)
+ console.info(`Global variable "${propName}" already exists. Skipping.`);
return;
}
Object.defineProperty(scope, propName, descriptor);
diff --git a/src/common/utils/downloadFile.ts b/src/common/utils/downloadFile.ts
new file mode 100644
index 0000000000..4c65901d3d
--- /dev/null
+++ b/src/common/utils/downloadFile.ts
@@ -0,0 +1,36 @@
+import request from "request";
+
+export interface DownloadFileOptions {
+ url: string;
+ gzip?: boolean;
+ timeout?: number;
+}
+
+export interface DownloadFileTicket {
+ url: string;
+ promise: Promise;
+ cancel(): void;
+}
+
+export function downloadFile({ url, timeout, gzip = true }: DownloadFileOptions): DownloadFileTicket {
+ const fileChunks: Buffer[] = [];
+ const req = request(url, { gzip, timeout });
+ const promise: Promise = new Promise((resolve, reject) => {
+ req.on("data", (chunk: Buffer) => {
+ fileChunks.push(chunk);
+ });
+ req.once("error", err => {
+ reject({ url, err });
+ });
+ req.once("complete", () => {
+ resolve(Buffer.concat(fileChunks));
+ });
+ });
+ return {
+ url,
+ promise,
+ cancel() {
+ req.abort();
+ }
+ };
+}
diff --git a/src/common/utils/escapeRegExp.ts b/src/common/utils/escapeRegExp.ts
new file mode 100644
index 0000000000..dbf10e4bfb
--- /dev/null
+++ b/src/common/utils/escapeRegExp.ts
@@ -0,0 +1,5 @@
+// Helper to sanitize / escape special chars for passing to RegExp-constructor
+
+export function escapeRegExp(str: string) {
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
+}
diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts
index 0dfad2d15f..b1006b5f58 100644
--- a/src/common/utils/index.ts
+++ b/src/common/utils/index.ts
@@ -1,14 +1,20 @@
// Common utils (main OR renderer)
-export * from "./app-version"
-export * from "./autobind"
-export * from "./base64"
-export * from "./camelCase"
-export * from "./cloneJson"
-export * from "./debouncePromise"
-export * from "./defineGlobal"
-export * from "./getRandId"
-export * from "./splitArray"
-export * from "./saveToAppFiles"
-export * from "./singleton"
-export * from "./openExternal"
+export const noop: any = () => { /* empty */ };
+
+export * from "./app-version";
+export * from "./autobind";
+export * from "./base64";
+export * from "./camelCase";
+export * from "./cloneJson";
+export * from "./debouncePromise";
+export * from "./defineGlobal";
+export * from "./getRandId";
+export * from "./splitArray";
+export * from "./saveToAppFiles";
+export * from "./singleton";
+export * from "./openExternal";
+export * from "./rectify-array";
+export * from "./downloadFile";
+export * from "./escapeRegExp";
+export * from "./tar";
diff --git a/src/common/utils/openExternal.ts b/src/common/utils/openExternal.ts
index 02c4da6c3b..56d7f90ce6 100644
--- a/src/common/utils/openExternal.ts
+++ b/src/common/utils/openExternal.ts
@@ -1,5 +1,5 @@
// Opens a link in external browser
-import { shell } from "electron"
+import { shell } from "electron";
export function openExternal(url: string) {
return shell.openExternal(url);
diff --git a/src/common/utils/rectify-array.ts b/src/common/utils/rectify-array.ts
new file mode 100644
index 0000000000..48feb3a165
--- /dev/null
+++ b/src/common/utils/rectify-array.ts
@@ -0,0 +1,8 @@
+/**
+ * rectify condences the single item or array of T type, to an array.
+ * @param items either one item or an array of items
+ * @returns a list of items
+ */
+export function recitfy(items: T | T[]): T[] {
+ return Array.isArray(items) ? items : [items];
+}
diff --git a/src/common/utils/saveToAppFiles.ts b/src/common/utils/saveToAppFiles.ts
index e6fab1cfa9..57c47f0d70 100644
--- a/src/common/utils/saveToAppFiles.ts
+++ b/src/common/utils/saveToAppFiles.ts
@@ -2,7 +2,7 @@
import path from "path";
import { app, remote } from "electron";
import { ensureDirSync, writeFileSync } from "fs-extra";
-import { WriteFileOptions } from "fs"
+import { WriteFileOptions } from "fs";
export function saveToAppFiles(filePath: string, contents: any, options?: WriteFileOptions): string {
const absPath = path.resolve((app || remote.app).getPath("userData"), filePath);
diff --git a/src/common/utils/singleton.ts b/src/common/utils/singleton.ts
index 70347f9b42..ed3f0cc962 100644
--- a/src/common/utils/singleton.ts
+++ b/src/common/utils/singleton.ts
@@ -24,5 +24,5 @@ class Singleton {
}
}
-export { Singleton }
+export { Singleton };
export default Singleton;
\ No newline at end of file
diff --git a/src/common/utils/splitArray.ts b/src/common/utils/splitArray.ts
index 90c342827d..f93392f736 100644
--- a/src/common/utils/splitArray.ts
+++ b/src/common/utils/splitArray.ts
@@ -15,5 +15,5 @@ export function splitArray(array: T[], element: T): [T[], T[], boolean] {
if (index < 0) {
return [array, [], false];
}
- return [array.slice(0, index), array.slice(index + 1, array.length), true]
+ return [array.slice(0, index), array.slice(index + 1, array.length), true];
}
diff --git a/src/common/utils/tar.ts b/src/common/utils/tar.ts
new file mode 100644
index 0000000000..004fa354dc
--- /dev/null
+++ b/src/common/utils/tar.ts
@@ -0,0 +1,55 @@
+// Helper for working with tarball files (.tar, .tgz)
+// Docs: https://github.com/npm/node-tar
+import tar, { ExtractOptions, FileStat } from "tar";
+import path from "path";
+
+export interface ReadFileFromTarOpts {
+ tarPath: string;
+ filePath: string;
+ parseJson?: boolean;
+}
+
+export function readFileFromTar({ tarPath, filePath, parseJson }: ReadFileFromTarOpts): Promise {
+ return new Promise(async (resolve, reject) => {
+ const fileChunks: Buffer[] = [];
+
+ await tar.list({
+ file: tarPath,
+ filter: path => path === filePath,
+ onentry(entry: FileStat) {
+ entry.on("data", chunk => {
+ fileChunks.push(chunk);
+ });
+ entry.once("error", err => {
+ reject(new Error(`reading file has failed ${entry.path}: ${err}`));
+ });
+ entry.once("end", () => {
+ const data = Buffer.concat(fileChunks);
+ const result = parseJson ? JSON.parse(data.toString("utf8")) : data;
+ resolve(result);
+ });
+ },
+ });
+
+ if (!fileChunks.length) {
+ reject(new Error("Not found"));
+ }
+ });
+}
+
+export async function listTarEntries(filePath: string): Promise {
+ const entries: string[] = [];
+ await tar.list({
+ file: filePath,
+ onentry: (entry: FileStat) => entries.push(entry.path as any as string),
+ });
+ return entries;
+}
+
+export function extractTar(filePath: string, opts: ExtractOptions & { sync?: boolean } = {}) {
+ return tar.extract({
+ file: filePath,
+ cwd: path.dirname(filePath),
+ ...opts,
+ });
+}
diff --git a/src/common/vars.ts b/src/common/vars.ts
index dd792e3bed..ac9f1336ee 100644
--- a/src/common/vars.ts
+++ b/src/common/vars.ts
@@ -1,19 +1,19 @@
// App's common configuration for any process (main, renderer, build pipeline, etc.)
import path from "path";
-import packageInfo from "../../package.json"
+import packageInfo from "../../package.json";
import { defineGlobal } from "./utils/defineGlobal";
-export const isMac = process.platform === "darwin"
-export const isWindows = process.platform === "win32"
-export const isLinux = process.platform === "linux"
-export const isDebugging = process.env.DEBUG === "true";
-export const isSnap = !!process.env["SNAP"]
-export const isProduction = process.env.NODE_ENV === "production"
+export const isMac = process.platform === "darwin";
+export const isWindows = process.platform === "win32";
+export const isLinux = process.platform === "linux";
+export const isDebugging = ["true", "1", "yes", "y", "on"].includes((process.env.DEBUG ?? "").toLowerCase());
+export const isSnap = !!process.env.SNAP;
+export const isProduction = process.env.NODE_ENV === "production";
export const isTestEnv = !!process.env.JEST_WORKER_ID;
export const isDevelopment = !isTestEnv && !isProduction;
-export const appName = `${packageInfo.productName}${isDevelopment ? "Dev" : ""}`
-export const publicPath = "/build/"
+export const appName = `${packageInfo.productName}${isDevelopment ? "Dev" : ""}`;
+export const publicPath = "/build/";
// Webpack build paths
export const contextDir = process.cwd();
@@ -22,7 +22,7 @@ export const mainDir = path.join(contextDir, "src/main");
export const rendererDir = path.join(contextDir, "src/renderer");
export const htmlTemplate = path.resolve(rendererDir, "template.html");
export const sassCommonVars = path.resolve(rendererDir, "components/vars.scss");
-export const webpackDevServerPort = 9009
+export const webpackDevServerPort = 9009;
// Special runtime paths
defineGlobal("__static", {
@@ -30,14 +30,16 @@ defineGlobal("__static", {
if (isDevelopment) {
return path.resolve(contextDir, "static");
}
- return path.resolve(process.resourcesPath, "static")
+ return path.resolve(process.resourcesPath, "static");
}
-})
+});
// Apis
-export const apiPrefix = "/api" // local router apis
-export const apiKubePrefix = "/api-kube" // k8s cluster apis
+export const apiPrefix = "/api"; // local router apis
+export const apiKubePrefix = "/api-kube"; // k8s cluster apis
// Links
-export const issuesTrackerUrl = "https://github.com/lensapp/lens/issues"
-export const slackUrl = "https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI"
+export const issuesTrackerUrl = "https://github.com/lensapp/lens/issues";
+export const slackUrl = "https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI";
+export const docsUrl = "https://docs.k8slens.dev/";
+export const supportUrl = "https://docs.k8slens.dev/latest/support/";
diff --git a/src/common/workspace-store.ts b/src/common/workspace-store.ts
index 97611a01d3..75ab36f19e 100644
--- a/src/common/workspace-store.ts
+++ b/src/common/workspace-store.ts
@@ -1,16 +1,17 @@
import { ipcRenderer } from "electron";
import { action, computed, observable, toJS, reaction } from "mobx";
import { BaseStore } from "./base-store";
-import { clusterStore } from "./cluster-store"
+import { clusterStore } from "./cluster-store";
import { appEventBus } from "./event-bus";
-import { broadcastIpc } from "../common/ipc";
+import { broadcastMessage } from "../common/ipc";
import logger from "../main/logger";
+import type { ClusterId } from "./cluster-store";
export type WorkspaceId = string;
export interface WorkspaceStoreModel {
+ workspaces: WorkspaceModel[];
currentWorkspace?: WorkspaceId;
- workspaces: WorkspaceModel[]
}
export interface WorkspaceModel {
@@ -18,6 +19,7 @@ export interface WorkspaceModel {
name: string;
description?: string;
ownerRef?: string;
+ lastActiveClusterId?: ClusterId;
}
export interface WorkspaceState {
@@ -25,43 +27,41 @@ export interface WorkspaceState {
}
export class Workspace implements WorkspaceModel, WorkspaceState {
- @observable id: WorkspaceId
- @observable name: string
- @observable description?: string
- @observable ownerRef?: string
- @observable enabled: boolean
+ @observable id: WorkspaceId;
+ @observable name: string;
+ @observable description?: string;
+ @observable ownerRef?: string;
+ @observable enabled: boolean;
+ @observable lastActiveClusterId?: ClusterId;
constructor(data: WorkspaceModel) {
- Object.assign(this, data)
+ Object.assign(this, data);
if (!ipcRenderer) {
reaction(() => this.getState(), () => {
- this.pushState()
- })
+ this.pushState();
+ });
}
}
get isManaged(): boolean {
- return !!this.ownerRef
+ return !!this.ownerRef;
}
getState(): WorkspaceState {
return {
enabled: this.enabled
- }
+ };
}
pushState(state = this.getState()) {
- logger.silly("[WORKSPACE] pushing state", {...state, id: this.id})
- broadcastIpc({
- channel: "workspace:state",
- args: [this.id, toJS(state)],
- });
+ logger.silly("[WORKSPACE] pushing state", {...state, id: this.id});
+ broadcastMessage("workspace:state", this.id, toJS(state));
}
@action
setState(state: WorkspaceState) {
- Object.assign(this, state)
+ Object.assign(this, state);
}
toJSON(): WorkspaceModel {
@@ -69,13 +69,14 @@ export class Workspace implements WorkspaceModel, WorkspaceState {
id: this.id,
name: this.name,
description: this.description,
- ownerRef: this.ownerRef
- })
+ ownerRef: this.ownerRef,
+ lastActiveClusterId: this.lastActiveClusterId
+ });
}
}
export class WorkspaceStore extends BaseStore {
- static readonly defaultId: WorkspaceId = "default"
+ static readonly defaultId: WorkspaceId = "default";
private constructor() {
super({
@@ -84,21 +85,21 @@ export class WorkspaceStore extends BaseStore {
if (!ipcRenderer) {
setInterval(() => {
- this.pushState()
- }, 5000)
+ this.pushState();
+ }, 5000);
}
}
registerIpcListener() {
- logger.info("[WORKSPACE-STORE] starting to listen state events")
+ logger.info("[WORKSPACE-STORE] starting to listen state events");
ipcRenderer.on("workspace:state", (event, workspaceId: string, state: WorkspaceState) => {
- this.getById(workspaceId)?.setState(state)
- })
+ this.getById(workspaceId)?.setState(state);
+ });
}
unregisterIpcListener() {
- super.unregisterIpcListener()
- ipcRenderer.removeAllListeners("workspace:state")
+ super.unregisterIpcListener();
+ ipcRenderer.removeAllListeners("workspace:state");
}
@observable currentWorkspaceId = WorkspaceStore.defaultId;
@@ -124,8 +125,8 @@ export class WorkspaceStore extends BaseStore {
pushState() {
this.workspaces.forEach((w) => {
- w.pushState()
- })
+ w.pushState();
+ });
}
isDefault(id: WorkspaceId) {
@@ -141,35 +142,34 @@ export class WorkspaceStore extends BaseStore {
}
@action
- setActive(id = WorkspaceStore.defaultId, reset = true) {
+ setActive(id = WorkspaceStore.defaultId) {
if (id === this.currentWorkspaceId) return;
if (!this.getById(id)) {
throw new Error(`workspace ${id} doesn't exist`);
}
this.currentWorkspaceId = id;
- clusterStore.activeCluster = null; // fixme: handle previously selected cluster from current workspace
}
@action
addWorkspace(workspace: Workspace) {
const { id, name } = workspace;
- const existingWorkspace = this.getById(id);
if (!name.trim() || this.getByName(name.trim())) {
return;
}
- if (existingWorkspace) {
- Object.assign(existingWorkspace, workspace);
- appEventBus.emit({name: "workspace", action: "update"})
- } else {
- appEventBus.emit({name: "workspace", action: "add"})
- }
this.workspaces.set(id, workspace);
+ appEventBus.emit({name: "workspace", action: "add"});
return workspace;
}
+ @action
+ updateWorkspace(workspace: Workspace) {
+ this.workspaces.set(workspace.id, workspace);
+ appEventBus.emit({name: "workspace", action: "update"});
+ }
+
@action
removeWorkspace(workspace: Workspace) {
- this.removeWorkspaceById(workspace.id)
+ this.removeWorkspaceById(workspace.id);
}
@action
@@ -183,24 +183,29 @@ export class WorkspaceStore extends BaseStore {
this.currentWorkspaceId = WorkspaceStore.defaultId; // reset to default
}
this.workspaces.delete(id);
- appEventBus.emit({name: "workspace", action: "remove"})
- clusterStore.removeByWorkspaceId(id)
+ appEventBus.emit({name: "workspace", action: "remove"});
+ clusterStore.removeByWorkspaceId(id);
+ }
+
+ @action
+ setLastActiveClusterId(clusterId?: ClusterId, workspaceId = this.currentWorkspaceId) {
+ this.getById(workspaceId).lastActiveClusterId = clusterId;
}
@action
protected fromStore({ currentWorkspace, workspaces = [] }: WorkspaceStoreModel) {
if (currentWorkspace) {
- this.currentWorkspaceId = currentWorkspace
+ this.currentWorkspaceId = currentWorkspace;
}
if (workspaces.length) {
this.workspaces.clear();
workspaces.forEach(ws => {
- const workspace = new Workspace(ws)
+ const workspace = new Workspace(ws);
if (!workspace.isManaged) {
- workspace.enabled = true
+ workspace.enabled = true;
}
- this.workspaces.set(workspace.id, workspace)
- })
+ this.workspaces.set(workspace.id, workspace);
+ });
}
}
@@ -210,8 +215,8 @@ export class WorkspaceStore extends BaseStore {
workspaces: this.workspacesList.map((w) => w.toJSON()),
}, {
recurseEverything: true
- })
+ });
}
}
-export const workspaceStore = WorkspaceStore.getInstance()
+export const workspaceStore = WorkspaceStore.getInstance();
diff --git a/src/extensions/__tests__/lens-extension.test.ts b/src/extensions/__tests__/lens-extension.test.ts
index c7e9ed5513..d6ba04cbb5 100644
--- a/src/extensions/__tests__/lens-extension.test.ts
+++ b/src/extensions/__tests__/lens-extension.test.ts
@@ -1,6 +1,6 @@
-import { LensExtension } from "../lens-extension"
+import { LensExtension } from "../lens-extension";
-let ext: LensExtension = null
+let ext: LensExtension = null;
describe("lens extension", () => {
beforeEach(async () => {
@@ -12,12 +12,12 @@ describe("lens extension", () => {
manifestPath: "/this/is/fake/package.json",
isBundled: false,
isEnabled: true
- })
- })
+ });
+ });
describe("name", () => {
it("returns name", () => {
- expect(ext.name).toBe("foo-bar")
- })
- })
-})
+ expect(ext.name).toBe("foo-bar");
+ });
+ });
+});
diff --git a/src/extensions/cluster-feature.ts b/src/extensions/cluster-feature.ts
index 08904750e2..4cb2c9bf5a 100644
--- a/src/extensions/cluster-feature.ts
+++ b/src/extensions/cluster-feature.ts
@@ -1,48 +1,111 @@
import fs from "fs";
-import path from "path"
-import hb from "handlebars"
-import { observable } from "mobx"
-import { ResourceApplier } from "../main/resource-applier"
+import path from "path";
+import hb from "handlebars";
+import { observable } from "mobx";
+import { ResourceApplier } from "../main/resource-applier";
import { Cluster } from "../main/cluster";
import logger from "../main/logger";
-import { app } from "electron"
-import { clusterIpc } from "../common/cluster-ipc"
+import { app } from "electron";
+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,
latestVersion: null,
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[]) {
- if (app) {
- await new ResourceApplier(cluster).kubectlApplyAll(resources)
+ /**
+ * 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 {
- await clusterIpc.kubectlApplyAll.invokeFromRenderer(cluster.id, resources)
+ resources = resourceSpec;
+ }
+
+ if (app) {
+ await new ResourceApplier(cluster).kubectlApplyAll(resources);
+ } else {
+ await requestMain(clusterKubectlApplyAllHandler, cluster.id, resources);
}
}
+ /**
+ * 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}`);
@@ -51,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/core-api/app.ts b/src/extensions/core-api/app.ts
index f3e44ed001..2664711db4 100644
--- a/src/extensions/core-api/app.ts
+++ b/src/extensions/core-api/app.ts
@@ -1,4 +1,8 @@
import { getAppVersion } from "../../common/utils";
+import { extensionsStore } from "../extensions-store";
-export const version = getAppVersion()
-export { isSnap, isWindows, isMac, isLinux, appName, slackUrl, issuesTrackerUrl } from "../../common/vars"
\ No newline at end of file
+export const version = getAppVersion();
+export { isSnap, isWindows, isMac, isLinux, appName, slackUrl, issuesTrackerUrl } from "../../common/vars";
+export function getEnabledExtensions(): string[] {
+ return extensionsStore.enabledExtensions;
+}
diff --git a/src/extensions/core-api/cluster-feature.ts b/src/extensions/core-api/cluster-feature.ts
index 9f2d3b8a40..170f4543f3 100644
--- a/src/extensions/core-api/cluster-feature.ts
+++ b/src/extensions/core-api/cluster-feature.ts
@@ -1,2 +1,2 @@
-export { ClusterFeature as Feature } from "../cluster-feature"
-export type { ClusterFeatureStatus as FeatureStatus } from "../cluster-feature"
+export { ClusterFeature as Feature } from "../cluster-feature";
+export type { ClusterFeatureStatus as FeatureStatus } from "../cluster-feature";
diff --git a/src/extensions/core-api/event-bus.ts b/src/extensions/core-api/event-bus.ts
index c958d19129..1a7be58143 100644
--- a/src/extensions/core-api/event-bus.ts
+++ b/src/extensions/core-api/event-bus.ts
@@ -1,2 +1,2 @@
-export { appEventBus } from "../../common/event-bus"
-export type { AppEvent } from "../../common/event-bus"
+export { appEventBus } from "../../common/event-bus";
+export type { AppEvent } from "../../common/event-bus";
diff --git a/src/extensions/core-api/index.ts b/src/extensions/core-api/index.ts
index dc040a3355..19ede51817 100644
--- a/src/extensions/core-api/index.ts
+++ b/src/extensions/core-api/index.ts
@@ -1,14 +1,14 @@
// Lens-extensions api developer's kit
-export * from "../lens-main-extension"
-export * from "../lens-renderer-extension"
+export * from "../lens-main-extension";
+export * from "../lens-renderer-extension";
// APIs
-import * as App from "./app"
-import * as EventBus from "./event-bus"
-import * as Store from "./stores"
-import * as Util from "./utils"
-import * as ClusterFeature from "./cluster-feature"
-import * as Interface from "../interfaces"
+import * as App from "./app";
+import * as EventBus from "./event-bus";
+import * as Store from "./stores";
+import * as Util from "./utils";
+import * as ClusterFeature from "./cluster-feature";
+import * as Interface from "../interfaces";
export {
App,
@@ -17,4 +17,4 @@ export {
Interface,
Store,
Util,
-}
+};
diff --git a/src/extensions/core-api/stores.ts b/src/extensions/core-api/stores.ts
index d44536769f..d7e489e5c0 100644
--- a/src/extensions/core-api/stores.ts
+++ b/src/extensions/core-api/stores.ts
@@ -1,6 +1,6 @@
-export { ExtensionStore } from "../extension-store"
-export { clusterStore } from "../../common/cluster-store"
-export type { ClusterModel } from "../../common/cluster-store"
-export { Cluster } from "../../main/cluster"
-export { workspaceStore, Workspace } from "../../common/workspace-store"
-export type { WorkspaceModel } from "../../common/workspace-store"
+export { ExtensionStore } from "../extension-store";
+export { clusterStore } from "../../common/cluster-store";
+export type { ClusterModel } from "../../common/cluster-store";
+export { Cluster } from "../../main/cluster";
+export { workspaceStore, Workspace } from "../../common/workspace-store";
+export type { WorkspaceModel } from "../../common/workspace-store";
diff --git a/src/extensions/core-api/utils.ts b/src/extensions/core-api/utils.ts
index c70959f2ba..c249ff5238 100644
--- a/src/extensions/core-api/utils.ts
+++ b/src/extensions/core-api/utils.ts
@@ -1,3 +1,3 @@
-export { Singleton, openExternal } from "../../common/utils"
-export { prevDefault, stopPropagation } from "../../renderer/utils/prevDefault"
-export { cssNames } from "../../renderer/utils/cssNames"
+export { Singleton, openExternal } from "../../common/utils";
+export { prevDefault, stopPropagation } from "../../renderer/utils/prevDefault";
+export { cssNames } from "../../renderer/utils/cssNames";
diff --git a/src/extensions/extension-api.ts b/src/extensions/extension-api.ts
index b6be0b9f4e..e4a8a77334 100644
--- a/src/extensions/extension-api.ts
+++ b/src/extensions/extension-api.ts
@@ -1,4 +1,4 @@
// Extension-api types generation bundle
-export * from "./core-api"
-export * from "./renderer-api"
+export * from "./core-api";
+export * from "./renderer-api";
diff --git a/src/extensions/extension-discovery.ts b/src/extensions/extension-discovery.ts
new file mode 100644
index 0000000000..ab17606e92
--- /dev/null
+++ b/src/extensions/extension-discovery.ts
@@ -0,0 +1,330 @@
+import chokidar from "chokidar";
+import { EventEmitter } from "events";
+import fs from "fs-extra";
+import os from "os";
+import path from "path";
+import { getBundledExtensions } from "../common/utils/app-version";
+import logger from "../main/logger";
+import { extensionInstaller, PackageJson } from "./extension-installer";
+import { extensionsStore } from "./extensions-store";
+import type { LensExtensionId, LensExtensionManifest } from "./lens-extension";
+
+export interface InstalledExtension {
+ readonly manifest: LensExtensionManifest;
+ readonly manifestPath: string;
+ readonly isBundled: boolean; // defined in project root's package.json
+ isEnabled: boolean;
+ }
+
+const logModule = "[EXTENSION-DISCOVERY]";
+export const manifestFilename = "package.json";
+
+/**
+ * Returns true if the lstat is for a directory-like file (e.g. isDirectory or symbolic link)
+ * @param lstat the stats to compare
+ */
+const isDirectoryLike = (lstat: fs.Stats) => lstat.isDirectory() || lstat.isSymbolicLink();
+
+/**
+ * Discovers installed bundled and local extensions from the filesystem.
+ * Also watches for added and removed local extensions by watching the directory.
+ * Uses ExtensionInstaller to install dependencies for all of the extensions.
+ * This is also done when a new extension is copied to the local extensions directory.
+ * .init() must be called to start the directory watching.
+ * The class emits events for added and removed extensions:
+ * - "add": When extension is added. The event is of type InstalledExtension
+ * - "remove": When extension is removed. The event is of type LensExtensionId
+ */
+export class ExtensionDiscovery {
+ protected bundledFolderPath: string;
+
+ private loadStarted = false;
+
+ // This promise is resolved when .load() is finished.
+ // This allows operations to be added after .load() success.
+ private loaded: Promise;
+
+ // These are called to either resolve or reject this.loaded promise
+ private resolveLoaded: () => void;
+ private rejectLoaded: (error: any) => void;
+
+ public events: EventEmitter;
+
+ constructor() {
+ this.loaded = new Promise((resolve, reject) => {
+ this.resolveLoaded = resolve;
+ this.rejectLoaded = reject;
+ });
+
+ this.events = new EventEmitter();
+ }
+
+ // Each extension is added as a single dependency to this object, which is written as package.json.
+ // Each dependency key is the name of the dependency, and
+ // each dependency value is the non-symlinked path to the dependency (folder).
+ protected packagesJson: PackageJson = {
+ dependencies: {}
+ };
+
+ get localFolderPath(): string {
+ return path.join(os.homedir(), ".k8slens", "extensions");
+ }
+
+ get packageJsonPath() {
+ return path.join(extensionInstaller.extensionPackagesRoot, manifestFilename);
+ }
+
+ get inTreeTargetPath() {
+ return path.join(extensionInstaller.extensionPackagesRoot, "extensions");
+ }
+
+ get inTreeFolderPath(): string {
+ return path.resolve(__static, "../extensions");
+ }
+
+ get nodeModulesPath(): string {
+ return path.join(extensionInstaller.extensionPackagesRoot, "node_modules");
+ }
+
+ /**
+ * Initializes the class and setups the file watcher for added/removed local extensions.
+ */
+ init() {
+ this.watchExtensions();
+ }
+
+ /**
+ * Watches for added/removed local extensions.
+ * Dependencies are installed automatically after an extension folder is copied.
+ */
+ async watchExtensions() {
+ logger.info(`${logModule} watching extension add/remove in ${this.localFolderPath}`);
+
+ // Wait until .load() has been called and has been resolved
+ await this.loaded;
+
+ // chokidar works better than fs.watch
+ chokidar.watch(this.localFolderPath, {
+ // Dont watch recursively into subdirectories
+ depth: 0,
+ // Try to wait until the file has been completely copied.
+ // The OS might emit an event for added file even it's not completely written to the filesysten.
+ awaitWriteFinish: {
+ // Wait 300ms until the file size doesn't change to consider the file written.
+ // For a small file like package.json this should be plenty of time.
+ stabilityThreshold: 300
+ }
+ })
+ // Extension add is detected by watching "package.json" add
+ .on("add", this.handleWatchFileAdd)
+ // Extension remove is detected by watching " unlink
+ .on("unlinkDir", this.handleWatchUnlinkDir);
+ }
+
+ handleWatchFileAdd = async (filePath: string) => {
+ if (path.basename(filePath) === manifestFilename) {
+ try {
+ const absPath = path.dirname(filePath);
+
+ // this.loadExtensionFromPath updates this.packagesJson
+ const extension = await this.loadExtensionFromPath(absPath);
+
+ if (extension) {
+ // Install dependencies for the new extension
+ await this.installPackages();
+
+ logger.info(`${logModule} Added extension ${extension.manifest.name}`);
+ this.events.emit("add", extension);
+ }
+ } catch (error) {
+ console.error(error);
+ }
+ }
+ };
+
+ handleWatchUnlinkDir = async (filePath: string) => {
+ // filePath is the non-symlinked path to the extension folder
+ // this.packagesJson.dependencies value is the non-symlinked path to the extension folder
+ // LensExtensionId in extension-loader is the symlinked path to the extension folder manifest file
+
+ // Check that the removed path is directly under this.localFolderPath
+ // Note that the watcher can create unlink events for subdirectories of the extension
+ const extensionFolderName = path.basename(filePath);
+
+ if (path.relative(this.localFolderPath, filePath) === extensionFolderName) {
+ const extensionName: string | undefined = Object
+ .entries(this.packagesJson.dependencies)
+ .find(([_name, extensionFolder]) => filePath === extensionFolder)?.[0];
+
+ if (extensionName !== undefined) {
+ delete this.packagesJson.dependencies[extensionName];
+
+ // Reinstall dependencies to remove the extension from package.json
+ await this.installPackages();
+
+ // The path to the manifest file is the lens extension id
+ // Note that we need to use the symlinked path
+ const lensExtensionId = path.join(this.nodeModulesPath, extensionName, "package.json");
+
+ logger.info(`${logModule} removed extension ${extensionName}`);
+ this.events.emit("remove", lensExtensionId as LensExtensionId);
+ } else {
+ logger.warn(`${logModule} extension ${extensionFolderName} not found, can't remove`);
+ }
+ }
+ };
+
+ async load(): Promise> {
+ if (this.loadStarted) {
+ // The class is simplified by only supporting .load() to be called once
+ throw new Error("ExtensionDiscovery.load() can be only be called once");
+ }
+
+ this.loadStarted = true;
+
+ try {
+ logger.info(`${logModule} loading extensions from ${extensionInstaller.extensionPackagesRoot}`);
+
+ if (fs.existsSync(path.join(extensionInstaller.extensionPackagesRoot, "package-lock.json"))) {
+ await fs.remove(path.join(extensionInstaller.extensionPackagesRoot, "package-lock.json"));
+ }
+
+ try {
+ await fs.access(this.inTreeFolderPath, fs.constants.W_OK);
+ this.bundledFolderPath = this.inTreeFolderPath;
+ } catch {
+ // we need to copy in-tree extensions so that we can symlink them properly on "npm install"
+ await fs.remove(this.inTreeTargetPath);
+ await fs.ensureDir(this.inTreeTargetPath);
+ await fs.copy(this.inTreeFolderPath, this.inTreeTargetPath);
+ this.bundledFolderPath = this.inTreeTargetPath;
+ }
+
+ await fs.ensureDir(this.nodeModulesPath);
+ await fs.ensureDir(this.localFolderPath);
+
+ const extensions = await this.loadExtensions();
+
+ // resolve the loaded promise
+ this.resolveLoaded();
+
+ return extensions;
+ } catch (error) {
+ this.rejectLoaded(error);
+ }
+ }
+
+ protected async getByManifest(manifestPath: string, { isBundled = false }: {
+ isBundled?: boolean;
+ } = {}): Promise {
+ let manifestJson: LensExtensionManifest;
+ let isEnabled: boolean;
+
+ try {
+ // check manifest file for existence
+ fs.accessSync(manifestPath, fs.constants.F_OK);
+
+ manifestJson = __non_webpack_require__(manifestPath);
+ const installedManifestPath = path.join(this.nodeModulesPath, manifestJson.name, "package.json");
+ this.packagesJson.dependencies[manifestJson.name] = path.dirname(manifestPath);
+ const isEnabled = isBundled || extensionsStore.isEnabled(installedManifestPath);
+
+ return {
+ manifestPath: installedManifestPath,
+ manifest: manifestJson,
+ isBundled,
+ isEnabled
+ };
+ } catch (error) {
+ logger.error(`${logModule}: can't install extension at ${manifestPath}: ${error}`, { manifestJson });
+
+ return null;
+ }
+ }
+
+ async loadExtensions(): Promise> {
+ const bundledExtensions = await this.loadBundledExtensions();
+ const localExtensions = await this.loadFromFolder(this.localFolderPath);
+ await this.installPackages();
+ const extensions = bundledExtensions.concat(localExtensions);
+
+ return new Map(extensions.map(ext => [ext.manifestPath, ext]));
+ }
+
+ /**
+ * Write package.json to file system and install dependencies.
+ */
+ installPackages() {
+ return extensionInstaller.installPackages(this.packageJsonPath, this.packagesJson);
+ }
+
+ async loadBundledExtensions() {
+ const extensions: InstalledExtension[] = [];
+ const folderPath = this.bundledFolderPath;
+ const bundledExtensions = getBundledExtensions();
+ const paths = await fs.readdir(folderPath);
+
+ for (const fileName of paths) {
+ if (!bundledExtensions.includes(fileName)) {
+ continue;
+ }
+
+ const absPath = path.resolve(folderPath, fileName);
+ const extension = await this.loadExtensionFromPath(absPath, { isBundled: true });
+
+ if (extension) {
+ extensions.push(extension);
+ }
+ }
+ logger.debug(`${logModule}: ${extensions.length} extensions loaded`, { folderPath, extensions });
+
+ return extensions;
+ }
+
+ async loadFromFolder(folderPath: string): Promise {
+ const bundledExtensions = getBundledExtensions();
+ const extensions: InstalledExtension[] = [];
+ const paths = await fs.readdir(folderPath);
+
+ for (const fileName of paths) {
+ // do not allow to override bundled extensions
+ if (bundledExtensions.includes(fileName)) {
+ continue;
+ }
+
+ const absPath = path.resolve(folderPath, fileName);
+
+ if (!fs.existsSync(absPath)) {
+ continue;
+ }
+
+ const lstat = await fs.lstat(absPath);
+
+ // skip non-directories
+ if (!isDirectoryLike(lstat)) {
+ continue;
+ }
+
+ const extension = await this.loadExtensionFromPath(absPath);
+ if (extension) {
+ extensions.push(extension);
+ }
+ }
+
+ logger.debug(`${logModule}: ${extensions.length} extensions loaded`, { folderPath, extensions });
+ return extensions;
+ }
+
+ /**
+ * Loads extension from absolute path, updates this.packagesJson to include it and returns the extension.
+ */
+ async loadExtensionFromPath(absPath: string, { isBundled = false }: {
+ isBundled?: boolean;
+ } = {}): Promise {
+ const manifestPath = path.resolve(absPath, manifestFilename);
+
+ return this.getByManifest(manifestPath, { isBundled });
+ }
+}
+
+export const extensionDiscovery = new ExtensionDiscovery();
diff --git a/src/extensions/extension-installer.ts b/src/extensions/extension-installer.ts
new file mode 100644
index 0000000000..46a7a31e6f
--- /dev/null
+++ b/src/extensions/extension-installer.ts
@@ -0,0 +1,69 @@
+import AwaitLock from 'await-lock';
+import child_process from "child_process";
+import fs from "fs-extra";
+import path from "path";
+import logger from "../main/logger";
+import { extensionPackagesRoot } from "./extension-loader";
+
+const logModule = "[EXTENSION-INSTALLER]";
+
+type Dependencies = {
+ [name: string]: string;
+};
+
+// Type for the package.json file that is written by ExtensionInstaller
+export type PackageJson = {
+ dependencies: Dependencies;
+};
+
+/**
+ * Installs dependencies for extensions
+ */
+export class ExtensionInstaller {
+ private installLock = new AwaitLock();
+
+ get extensionPackagesRoot() {
+ return extensionPackagesRoot();
+ }
+
+ get npmPath() {
+ return __non_webpack_require__.resolve('npm/bin/npm-cli');
+ }
+
+ installDependencies(): Promise {
+ return new Promise((resolve, reject) => {
+ logger.info(`${logModule} installing dependencies at ${extensionPackagesRoot()}`);
+ const child = child_process.fork(this.npmPath, ["install", "--silent", "--no-audit", "--only=prod", "--prefer-offline", "--no-package-lock"], {
+ cwd: extensionPackagesRoot(),
+ silent: true
+ });
+ child.on("close", () => {
+ resolve();
+ });
+ child.on("error", (err) => {
+ reject(err);
+ });
+ });
+ }
+
+ /**
+ * Write package.json to the file system and execute npm install for it.
+ */
+ async installPackages(packageJsonPath: string, packagesJson: PackageJson): Promise {
+ // Mutual exclusion to install packages in sequence
+ await this.installLock.acquireAsync();
+
+ try {
+ // Write the package.json which will be installed in .installDependencies()
+ await fs.writeFile(path.join(packageJsonPath), JSON.stringify(packagesJson, null, 2), {
+ mode: 0o600
+ });
+
+ await this.installDependencies();
+ } finally {
+ this.installLock.release();
+ }
+ }
+}
+
+export const extensionInstaller = new ExtensionInstaller();
diff --git a/src/extensions/extension-loader.ts b/src/extensions/extension-loader.ts
index 969cff33b1..af0e9d6f86 100644
--- a/src/extensions/extension-loader.ts
+++ b/src/extensions/extension-loader.ts
@@ -1,69 +1,108 @@
-import type { LensExtension, LensExtensionConstructor, LensExtensionId } from "./lens-extension"
-import type { LensMainExtension } from "./lens-main-extension"
-import type { LensRendererExtension } from "./lens-renderer-extension"
-import type { InstalledExtension } from "./extension-manager";
-import path from "path"
-import { broadcastIpc } from "../common/ipc"
-import { action, computed, observable, reaction, toJS, when } from "mobx"
-import logger from "../main/logger"
-import { app, ipcRenderer, remote } from "electron"
-import * as registries from "./registries";
+import { app, ipcRenderer, remote } from "electron";
+import { action, computed, observable, reaction, toJS, when } from "mobx";
+import path from "path";
+import { getHostedCluster } from "../common/cluster-store";
+import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc";
+import logger from "../main/logger";
+import type { InstalledExtension } from "./extension-discovery";
import { extensionsStore } from "./extensions-store";
+import type { LensExtension, LensExtensionConstructor, LensExtensionId } from "./lens-extension";
+import type { LensMainExtension } from "./lens-main-extension";
+import type { LensRendererExtension } from "./lens-renderer-extension";
+import * as registries from "./registries";
// lazy load so that we get correct userData
export function extensionPackagesRoot() {
- return path.join((app || remote.app).getPath("userData"))
+ return path.join((app || remote.app).getPath("userData"));
}
+const logModule = "[EXTENSIONS-LOADER]";
+
+/**
+ * Loads installed extensions to the Lens application
+ */
export class ExtensionLoader {
protected extensions = observable.map();
protected instances = observable.map();
+ protected readonly requestExtensionsChannel = "extensions:loaded";
@observable isLoaded = false;
whenLoaded = when(() => this.isLoaded);
- constructor() {
- if (ipcRenderer) {
- ipcRenderer.on("extensions:loaded", (event, extensions: [LensExtensionId, InstalledExtension][]) => {
- this.isLoaded = true;
- extensions.forEach(([extId, ext]) => {
- if (!this.extensions.has(extId)) {
- this.extensions.set(extId, ext)
- }
- })
- });
- }
- extensionsStore.manageState(this);
- }
-
@computed get userExtensions(): Map {
const extensions = this.extensions.toJS();
extensions.forEach((ext, extId) => {
if (ext.isBundled) {
extensions.delete(extId);
}
- })
+ });
return extensions;
}
@action
- async init(extensions: Map) {
+ async init() {
+ if (ipcRenderer) {
+ this.initRenderer();
+ } else {
+ this.initMain();
+ }
+ extensionsStore.manageState(this);
+ }
+
+ initExtensions(extensions?: Map) {
this.extensions.replace(extensions);
+ }
+
+ addExtension(extension: InstalledExtension) {
+ this.extensions.set(extension.manifestPath as LensExtensionId, extension);
+ }
+
+ removeExtension(lensExtensionId: LensExtensionId) {
+ // TODO: Remove the extension properly (from menus etc.)
+ if (!this.extensions.delete(lensExtensionId)) {
+ throw new Error(`Can't remove extension ${lensExtensionId}, doesn't exist.`);
+ }
+ }
+
+ protected async initMain() {
this.isLoaded = true;
this.loadOnMain();
this.broadcastExtensions();
+
+ reaction(() => this.extensions.toJS(), () => {
+ this.broadcastExtensions();
+ });
+
+ handleRequest(this.requestExtensionsChannel, () => {
+ return Array.from(this.toJSON());
+ });
+ }
+
+ protected async initRenderer() {
+ const extensionListHandler = ( extensions: [LensExtensionId, InstalledExtension][]) => {
+ this.isLoaded = true;
+ extensions.forEach(([extId, ext]) => {
+ if (!this.extensions.has(extId)) {
+ this.extensions.set(extId, ext);
+ }
+ });
+ };
+ requestMain(this.requestExtensionsChannel).then(extensionListHandler);
+ subscribeToBroadcast(this.requestExtensionsChannel, (event, extensions: [LensExtensionId, InstalledExtension][]) => {
+ extensionListHandler(extensions);
+ });
}
loadOnMain() {
- logger.info('[EXTENSIONS-LOADER]: load on main')
- this.autoInitExtensions((ext: LensMainExtension) => [
+ logger.info(`${logModule}: load on main`);
+ this.autoInitExtensions(async (ext: LensMainExtension) => [
registries.menuRegistry.add(ext.appMenus)
]);
}
loadOnClusterManagerRenderer() {
- logger.info('[EXTENSIONS-LOADER]: load on main renderer (cluster manager)')
- this.autoInitExtensions((ext: LensRendererExtension) => [
+ logger.info(`${logModule}: load on main renderer (cluster manager)`);
+ this.autoInitExtensions(async (ext: LensRendererExtension) => [
registries.globalPageRegistry.add(ext.globalPages, ext),
registries.globalPageMenuRegistry.add(ext.globalPageMenus, ext),
registries.appPreferenceRegistry.add(ext.appPreferences),
@@ -73,59 +112,70 @@ export class ExtensionLoader {
}
loadOnClusterRenderer() {
- logger.info('[EXTENSIONS-LOADER]: load on cluster renderer (dashboard)')
- this.autoInitExtensions((ext: LensRendererExtension) => [
- registries.clusterPageRegistry.add(ext.clusterPages, ext),
- registries.clusterPageMenuRegistry.add(ext.clusterPageMenus, ext),
- registries.kubeObjectMenuRegistry.add(ext.kubeObjectMenuItems),
- registries.kubeObjectDetailRegistry.add(ext.kubeObjectDetailItems),
- registries.kubeObjectStatusRegistry.add(ext.kubeObjectStatusTexts)
- ])
+ logger.info(`${logModule}: load on cluster renderer (dashboard)`);
+ const cluster = getHostedCluster();
+ this.autoInitExtensions(async (ext: LensRendererExtension) => {
+ if (await ext.isEnabledForCluster(cluster) === false) {
+ return [];
+ }
+ return [
+ registries.clusterPageRegistry.add(ext.clusterPages, ext),
+ registries.clusterPageMenuRegistry.add(ext.clusterPageMenus, ext),
+ registries.kubeObjectMenuRegistry.add(ext.kubeObjectMenuItems),
+ registries.kubeObjectDetailRegistry.add(ext.kubeObjectDetailItems),
+ registries.kubeObjectStatusRegistry.add(ext.kubeObjectStatusTexts)
+ ];
+ });
}
- protected autoInitExtensions(register: (ext: LensExtension) => Function[]) {
+ protected autoInitExtensions(register: (ext: LensExtension) => Promise) {
return reaction(() => this.toJSON(), installedExtensions => {
for (const [extId, ext] of installedExtensions) {
- let instance = this.instances.get(extId);
- if (ext.isEnabled && !instance) {
+ const alreadyInit = this.instances.has(extId);
+
+ if (ext.isEnabled && !alreadyInit) {
try {
- const LensExtensionClass: LensExtensionConstructor = this.requireExtension(ext)
- if (!LensExtensionClass) continue;
- instance = new LensExtensionClass(ext);
+ const LensExtensionClass = this.requireExtension(ext);
+ if (!LensExtensionClass) {
+ continue;
+ }
+
+ const instance = new LensExtensionClass(ext);
instance.whenEnabled(() => register(instance));
instance.enable();
this.instances.set(extId, instance);
} catch (err) {
- logger.error(`[EXTENSION-LOADER]: activation extension error`, { ext, err })
+ logger.error(`${logModule}: activation extension error`, { ext, err });
}
- } else if (!ext.isEnabled && instance) {
+ } else if (!ext.isEnabled && alreadyInit) {
try {
+ const instance = this.instances.get(extId);
instance.disable();
this.instances.delete(extId);
} catch (err) {
- logger.error(`[EXTENSION-LOADER]: deactivation extension error`, { ext, err })
+ logger.error(`${logModule}: deactivation extension error`, { ext, err });
}
}
}
}, {
fireImmediately: true,
- })
+ });
}
- protected requireExtension(extension: InstalledExtension) {
- let extEntrypoint = ""
+ protected requireExtension(extension: InstalledExtension): LensExtensionConstructor {
+ let extEntrypoint = "";
try {
if (ipcRenderer && extension.manifest.renderer) {
- extEntrypoint = path.resolve(path.join(path.dirname(extension.manifestPath), extension.manifest.renderer))
+ extEntrypoint = path.resolve(path.join(path.dirname(extension.manifestPath), extension.manifest.renderer));
} else if (!ipcRenderer && extension.manifest.main) {
- extEntrypoint = path.resolve(path.join(path.dirname(extension.manifestPath), extension.manifest.main))
+ extEntrypoint = path.resolve(path.join(path.dirname(extension.manifestPath), extension.manifest.main));
}
if (extEntrypoint !== "") {
return __non_webpack_require__(extEntrypoint).default;
}
} catch (err) {
- console.error(`[EXTENSION-LOADER]: can't load extension main at ${extEntrypoint}: ${err}`, { extension });
- console.trace(err)
+ console.error(`${logModule}: can't load extension main at ${extEntrypoint}: ${err}`, { extension });
+ console.trace(err);
}
}
@@ -137,19 +187,11 @@ export class ExtensionLoader {
return toJS(this.extensions, {
exportMapsAsObjects: false,
recurseEverything: true,
- })
+ });
}
- async broadcastExtensions(frameId?: number) {
- await when(() => this.isLoaded);
- broadcastIpc({
- channel: "extensions:loaded",
- frameId: frameId,
- frameOnly: !!frameId,
- args: [
- Array.from(this.toJSON()),
- ],
- })
+ broadcastExtensions() {
+ broadcastMessage(this.requestExtensionsChannel, Array.from(this.toJSON()));
}
}
diff --git a/src/extensions/extension-manager.ts b/src/extensions/extension-manager.ts
deleted file mode 100644
index 7a10fa35f4..0000000000
--- a/src/extensions/extension-manager.ts
+++ /dev/null
@@ -1,172 +0,0 @@
-import type { LensExtensionId, LensExtensionManifest } from "./lens-extension"
-import path from "path"
-import os from "os"
-import fs from "fs-extra"
-import child_process from "child_process";
-import logger from "../main/logger"
-import { extensionPackagesRoot } from "./extension-loader"
-import { getBundledExtensions } from "../common/utils/app-version"
-
-export interface InstalledExtension {
- readonly manifest: LensExtensionManifest;
- readonly manifestPath: string;
- readonly isBundled: boolean; // defined in project root's package.json
- isEnabled: boolean;
-}
-
-type Dependencies = {
- [name: string]: string;
-}
-
-type PackageJson = {
- dependencies: Dependencies;
-}
-
-export class ExtensionManager {
-
- protected bundledFolderPath: string
-
- protected packagesJson: PackageJson = {
- dependencies: {}
- }
-
- get extensionPackagesRoot() {
- return extensionPackagesRoot()
- }
-
- get inTreeTargetPath() {
- return path.join(this.extensionPackagesRoot, "extensions")
- }
-
- get inTreeFolderPath(): string {
- return path.resolve(__static, "../extensions");
- }
-
- get nodeModulesPath(): string {
- return path.join(this.extensionPackagesRoot, "node_modules")
- }
-
- get localFolderPath(): string {
- return path.join(os.homedir(), ".k8slens", "extensions");
- }
-
- get npmPath() {
- return __non_webpack_require__.resolve('npm/bin/npm-cli')
- }
-
- get packageJsonPath() {
- return path.join(this.extensionPackagesRoot, "package.json")
- }
-
- async load(): Promise> {
- logger.info("[EXTENSION-MANAGER] loading extensions from " + this.extensionPackagesRoot)
- if (fs.existsSync(path.join(this.extensionPackagesRoot, "package-lock.json"))) {
- await fs.remove(path.join(this.extensionPackagesRoot, "package-lock.json"))
- }
- try {
- await fs.access(this.inTreeFolderPath, fs.constants.W_OK)
- this.bundledFolderPath = this.inTreeFolderPath
- } catch {
- // we need to copy in-tree extensions so that we can symlink them properly on "npm install"
- await fs.remove(this.inTreeTargetPath)
- await fs.ensureDir(this.inTreeTargetPath)
- await fs.copy(this.inTreeFolderPath, this.inTreeTargetPath)
- this.bundledFolderPath = this.inTreeTargetPath
- }
- await fs.ensureDir(this.nodeModulesPath)
- await fs.ensureDir(this.localFolderPath)
- return await this.loadExtensions();
- }
-
- protected async getByManifest(manifestPath: string, { isBundled = false } = {}): Promise {
- let manifestJson: LensExtensionManifest;
- try {
- fs.accessSync(manifestPath, fs.constants.F_OK); // check manifest file for existence
- manifestJson = __non_webpack_require__(manifestPath)
- this.packagesJson.dependencies[manifestJson.name] = path.dirname(manifestPath)
-
- logger.info("[EXTENSION-MANAGER] installed extension " + manifestJson.name)
- return {
- manifestPath: path.join(this.nodeModulesPath, manifestJson.name, "package.json"),
- manifest: manifestJson,
- isBundled: isBundled,
- isEnabled: isBundled,
- }
- } catch (err) {
- logger.error(`[EXTENSION-MANAGER]: can't install extension at ${manifestPath}: ${err}`, { manifestJson });
- }
- }
-
- protected installPackages(): Promise {
- return new Promise((resolve, reject) => {
- const child = child_process.fork(this.npmPath, ["install", "--silent", "--no-audit", "--only=prod", "--prefer-offline", "--no-package-lock"], {
- cwd: extensionPackagesRoot(),
- silent: true
- })
- child.on("close", () => {
- resolve()
- })
- child.on("error", (err) => {
- reject(err)
- })
- })
- }
-
- async loadExtensions() {
- const bundledExtensions = await this.loadBundledExtensions()
- const localExtensions = await this.loadFromFolder(this.localFolderPath)
- await fs.writeFile(path.join(this.packageJsonPath), JSON.stringify(this.packagesJson, null, 2), { mode: 0o600 })
- await this.installPackages()
- const extensions = bundledExtensions.concat(localExtensions)
- return new Map(extensions.map(ext => [ext.manifestPath, ext]));
- }
-
- async loadBundledExtensions() {
- const extensions: InstalledExtension[] = []
- const folderPath = this.bundledFolderPath
- const bundledExtensions = getBundledExtensions()
- const paths = await fs.readdir(folderPath);
- for (const fileName of paths) {
- if (!bundledExtensions.includes(fileName)) {
- continue
- }
- const absPath = path.resolve(folderPath, fileName);
- const manifestPath = path.resolve(absPath, "package.json");
- const ext = await this.getByManifest(manifestPath, { isBundled: true }).catch(() => null)
- if (ext) {
- extensions.push(ext)
- }
- }
- logger.debug(`[EXTENSION-MANAGER]: ${extensions.length} extensions loaded`, { folderPath, extensions });
- return extensions
- }
-
- async loadFromFolder(folderPath: string): Promise {
- const bundledExtensions = getBundledExtensions()
- const extensions: InstalledExtension[] = []
- const paths = await fs.readdir(folderPath);
- for (const fileName of paths) {
- if (bundledExtensions.includes(fileName)) { // do no allow to override bundled extensions
- continue
- }
- const absPath = path.resolve(folderPath, fileName);
- if (!fs.existsSync(absPath)) {
- continue
- }
- const lstat = await fs.lstat(absPath)
- if (!lstat.isDirectory() && !lstat.isSymbolicLink()) { // skip non-directories
- continue
- }
- const manifestPath = path.resolve(absPath, "package.json");
- const ext = await this.getByManifest(manifestPath).catch(() => null)
- if (ext) {
- extensions.push(ext)
- }
- }
-
- logger.debug(`[EXTENSION-MANAGER]: ${extensions.length} extensions loaded`, { folderPath, extensions });
- return extensions;
- }
-}
-
-export const extensionManager = new ExtensionManager()
diff --git a/src/extensions/extension-store.ts b/src/extensions/extension-store.ts
index d5372eceff..a8078994ca 100644
--- a/src/extensions/extension-store.ts
+++ b/src/extensions/extension-store.ts
@@ -1,21 +1,21 @@
-import { BaseStore } from "../common/base-store"
-import * as path from "path"
-import { LensExtension } from "./lens-extension"
+import { BaseStore } from "../common/base-store";
+import * as path from "path";
+import { LensExtension } from "./lens-extension";
-export class ExtensionStore extends BaseStore {
- protected extension: LensExtension
+export abstract class ExtensionStore extends BaseStore {
+ protected extension: LensExtension;
async loadExtension(extension: LensExtension) {
- this.extension = extension
- await super.load()
+ this.extension = extension;
+ return super.load();
}
async load() {
- if (!this.extension) { return }
- await super.load()
+ if (!this.extension) { return; }
+ return super.load();
}
protected cwd() {
- return path.join(super.cwd(), "extension-store", this.extension.name)
+ return path.join(super.cwd(), "extension-store", this.extension.name);
}
}
diff --git a/src/extensions/extensions-store.ts b/src/extensions/extensions-store.ts
index ba942c41d9..b81c415f85 100644
--- a/src/extensions/extensions-store.ts
+++ b/src/extensions/extensions-store.ts
@@ -1,7 +1,7 @@
import type { LensExtensionId } from "./lens-extension";
import type { ExtensionLoader } from "./extension-loader";
-import { BaseStore } from "../common/base-store"
-import { action, observable, reaction, toJS } from "mobx";
+import { BaseStore } from "../common/base-store";
+import { action, computed, observable, reaction, toJS } from "mobx";
export interface LensExtensionsStoreModel {
extensions: Record;
@@ -9,6 +9,7 @@ export interface LensExtensionsStoreModel {
export interface LensExtensionState {
enabled?: boolean;
+ name: string;
}
export class ExtensionsStore extends BaseStore {
@@ -18,6 +19,17 @@ export class ExtensionsStore extends BaseStore {
});
}
+ @computed
+ get enabledExtensions() {
+ const extensions: string[] = [];
+ this.state.forEach((state, id) => {
+ if (state.enabled) {
+ extensions.push(state.name);
+ }
+ });
+ return extensions;
+ }
+
protected state = observable.map();
protected getState(extensionLoader: ExtensionLoader) {
@@ -25,20 +37,16 @@ export class ExtensionsStore extends BaseStore {
return Array.from(extensionLoader.userExtensions).reduce((state, [extId, ext]) => {
state[extId] = {
enabled: ext.isEnabled,
- }
+ name: ext.manifest.name,
+ };
return state;
- }, state)
+ }, state);
}
async manageState(extensionLoader: ExtensionLoader) {
await extensionLoader.whenLoaded;
await this.whenLoaded;
- // activate user-extensions when state is ready
- extensionLoader.userExtensions.forEach((ext, extId) => {
- ext.isEnabled = this.isEnabled(extId);
- });
-
// apply state on changes from store
reaction(() => this.state.toJS(), extensionsState => {
extensionsState.forEach((state, extId) => {
@@ -46,18 +54,18 @@ export class ExtensionsStore extends BaseStore {
if (ext && !ext.isBundled) {
ext.isEnabled = state.enabled;
}
- })
- })
+ });
+ });
// save state on change `extension.isEnabled`
reaction(() => this.getState(extensionLoader), extensionsState => {
- this.state.merge(extensionsState)
- })
+ this.state.merge(extensionsState);
+ });
}
isEnabled(extId: LensExtensionId) {
const state = this.state.get(extId);
- return !state /* enabled by default */ || state.enabled;
+ return state && state.enabled; // by default false
}
@action
@@ -70,7 +78,7 @@ export class ExtensionsStore extends BaseStore {
extensions: this.state.toJSON(),
}, {
recurseEverything: true
- })
+ });
}
}
diff --git a/src/extensions/interfaces/index.ts b/src/extensions/interfaces/index.ts
index e3612bdb7f..c91d8cdd19 100644
--- a/src/extensions/interfaces/index.ts
+++ b/src/extensions/interfaces/index.ts
@@ -1 +1 @@
-export * from "./registrations"
\ No newline at end of file
+export * from "./registrations";
\ No newline at end of file
diff --git a/src/extensions/interfaces/registrations.ts b/src/extensions/interfaces/registrations.ts
index 14c9f66c22..a2ebb10290 100644
--- a/src/extensions/interfaces/registrations.ts
+++ b/src/extensions/interfaces/registrations.ts
@@ -1,8 +1,8 @@
-export type { AppPreferenceRegistration, AppPreferenceComponents } from "../registries/app-preference-registry"
-export type { ClusterFeatureRegistration, ClusterFeatureComponents } from "../registries/cluster-feature-registry"
-export type { KubeObjectDetailRegistration, KubeObjectDetailComponents } from "../registries/kube-object-detail-registry"
-export type { KubeObjectMenuRegistration, KubeObjectMenuComponents } from "../registries/kube-object-menu-registry"
-export type { KubeObjectStatusRegistration } from "../registries/kube-object-status-registry"
-export type { PageRegistration, PageComponents } from "../registries/page-registry"
-export type { PageMenuRegistration, PageMenuComponents } from "../registries/page-menu-registry"
-export type { StatusBarRegistration } from "../registries/status-bar-registry"
\ No newline at end of file
+export type { AppPreferenceRegistration, AppPreferenceComponents } from "../registries/app-preference-registry";
+export type { ClusterFeatureRegistration, ClusterFeatureComponents } from "../registries/cluster-feature-registry";
+export type { KubeObjectDetailRegistration, KubeObjectDetailComponents } from "../registries/kube-object-detail-registry";
+export type { KubeObjectMenuRegistration, KubeObjectMenuComponents } from "../registries/kube-object-menu-registry";
+export type { KubeObjectStatusRegistration } from "../registries/kube-object-status-registry";
+export type { PageRegistration, PageComponents } from "../registries/page-registry";
+export type { PageMenuRegistration, PageMenuComponents } from "../registries/page-menu-registry";
+export type { StatusBarRegistration } from "../registries/status-bar-registry";
\ No newline at end of file
diff --git a/src/extensions/lens-extension.ts b/src/extensions/lens-extension.ts
index f1ffb9184b..3c9f70eb49 100644
--- a/src/extensions/lens-extension.ts
+++ b/src/extensions/lens-extension.ts
@@ -1,5 +1,6 @@
-import type { InstalledExtension } from "./extension-manager";
+import type { InstalledExtension } from "./extension-discovery";
import { action, observable, reaction } from "mobx";
+import { filesystemProvisionerStore } from "../main/extension-filesystem";
import logger from "../main/logger";
export type LensExtensionId = string; // path to manifest (package.json)
@@ -11,6 +12,7 @@ export interface LensExtensionManifest {
description?: string;
main?: string; // path to %ext/dist/main.js
renderer?: string; // path to %ext/dist/renderer.js
+ lens?: object; // fixme: add more required fields for validation
}
export class LensExtension {
@@ -21,25 +23,37 @@ export class LensExtension {
@observable private isEnabled = false;
constructor({ manifest, manifestPath, isBundled }: InstalledExtension) {
- this.manifest = manifest
- this.manifestPath = manifestPath
- this.isBundled = !!isBundled
+ this.manifest = manifest;
+ this.manifestPath = manifestPath;
+ this.isBundled = !!isBundled;
}
get id(): LensExtensionId {
+ // This is the symlinked path under node_modules
return this.manifestPath;
}
get name() {
- return this.manifest.name
+ return this.manifest.name;
}
get version() {
- return this.manifest.version
+ return this.manifest.version;
+ }
+
+ /**
+ * getExtensionFileFolder returns the path to an already created folder. This
+ * folder is for the sole use of this extension.
+ *
+ * Note: there is no security done on this folder, only obfiscation of the
+ * folder name.
+ */
+ async getExtensionFileFolder(): Promise {
+ return filesystemProvisionerStore.requestDirectory(this.id);
}
get description() {
- return this.manifest.description
+ return this.manifest.description;
}
@action
@@ -60,31 +74,32 @@ export class LensExtension {
toggle(enable?: boolean) {
if (typeof enable === "boolean") {
- enable ? this.enable() : this.disable()
+ enable ? this.enable() : this.disable();
} else {
- this.isEnabled ? this.disable() : this.enable()
+ this.isEnabled ? this.disable() : this.enable();
}
}
- async whenEnabled(handlers: () => Function[]) {
+ async whenEnabled(handlers: () => Promise) {
const disposers: Function[] = [];
const unregisterHandlers = () => {
- disposers.forEach(unregister => unregister())
+ disposers.forEach(unregister => unregister());
disposers.length = 0;
- }
- const cancelReaction = reaction(() => this.isEnabled, isEnabled => {
+ };
+ const cancelReaction = reaction(() => this.isEnabled, async (isEnabled) => {
if (isEnabled) {
- disposers.push(...handlers());
+ const handlerDisposers = await handlers();
+ disposers.push(...handlerDisposers);
} else {
unregisterHandlers();
}
}, {
fireImmediately: true
- })
+ });
return () => {
unregisterHandlers();
cancelReaction();
- }
+ };
}
protected onActivate() {
@@ -95,3 +110,7 @@ export class LensExtension {
// mock
}
}
+
+export function sanitizeExtensionName(name: string) {
+ return name.replace("@", "").replace("/", "--");
+}
diff --git a/src/extensions/lens-main-extension.ts b/src/extensions/lens-main-extension.ts
index af091e0c1c..4947d76108 100644
--- a/src/extensions/lens-main-extension.ts
+++ b/src/extensions/lens-main-extension.ts
@@ -1,17 +1,17 @@
import type { MenuRegistration } from "./registries/menu-registry";
import { observable } from "mobx";
-import { LensExtension } from "./lens-extension"
+import { LensExtension } from "./lens-extension";
import { WindowManager } from "../main/window-manager";
-import { getExtensionPageUrl } from "./registries/page-registry"
+import { getExtensionPageUrl } from "./registries/page-registry";
export class LensMainExtension extends LensExtension {
- @observable.shallow appMenus: MenuRegistration[] = []
+ appMenus: MenuRegistration[] = [];
async navigate(pageId?: string, params?: P, frameId?: number) {
const windowManager = WindowManager.getInstance();
const pageUrl = getExtensionPageUrl({
extensionId: this.name,
- pageId: pageId,
+ pageId,
params: params ?? {}, // compile to url with params
});
await windowManager.navigate(pageUrl, frameId);
diff --git a/src/extensions/lens-renderer-extension.ts b/src/extensions/lens-renderer-extension.ts
index 0fb346e1a1..0ed286e94a 100644
--- a/src/extensions/lens-renderer-extension.ts
+++ b/src/extensions/lens-renderer-extension.ts
@@ -1,27 +1,36 @@
-import type { AppPreferenceRegistration, ClusterFeatureRegistration, KubeObjectDetailRegistration, KubeObjectMenuRegistration, KubeObjectStatusRegistration, PageMenuRegistration, PageRegistration, StatusBarRegistration, } from "./registries"
+import type { AppPreferenceRegistration, ClusterFeatureRegistration, KubeObjectDetailRegistration, KubeObjectMenuRegistration, KubeObjectStatusRegistration, PageMenuRegistration, PageRegistration, StatusBarRegistration, } from "./registries";
+import type { Cluster } from "../main/cluster";
import { observable } from "mobx";
-import { LensExtension } from "./lens-extension"
-import { getExtensionPageUrl } from "./registries/page-registry"
+import { LensExtension } from "./lens-extension";
+import { getExtensionPageUrl } from "./registries/page-registry";
+
export class LensRendererExtension extends LensExtension {
- @observable.shallow globalPages: PageRegistration[] = []
- @observable.shallow clusterPages: PageRegistration[] = []
- @observable.shallow globalPageMenus: PageMenuRegistration[] = []
- @observable.shallow clusterPageMenus: PageMenuRegistration[] = []
- @observable.shallow kubeObjectStatusTexts: KubeObjectStatusRegistration[] = []
- @observable.shallow appPreferences: AppPreferenceRegistration[] = []
- @observable.shallow clusterFeatures: ClusterFeatureRegistration[] = []
- @observable.shallow statusBarItems: StatusBarRegistration[] = []
- @observable.shallow kubeObjectDetailItems: KubeObjectDetailRegistration[] = []
- @observable.shallow kubeObjectMenuItems: KubeObjectMenuRegistration[] = []
+ globalPages: PageRegistration[] = [];
+ clusterPages: PageRegistration[] = [];
+ globalPageMenus: PageMenuRegistration[] = [];
+ clusterPageMenus: PageMenuRegistration[] = [];
+ kubeObjectStatusTexts: KubeObjectStatusRegistration[] = [];
+ appPreferences: AppPreferenceRegistration[] = [];
+ clusterFeatures: ClusterFeatureRegistration[] = [];
+ statusBarItems: StatusBarRegistration[] = [];
+ kubeObjectDetailItems: KubeObjectDetailRegistration[] = [];
+ kubeObjectMenuItems: KubeObjectMenuRegistration[] = [];
async navigate(pageId?: string, params?: P) {
const { navigate } = await import("../renderer/navigation");
const pageUrl = getExtensionPageUrl({
extensionId: this.name,
- pageId: pageId,
+ pageId,
params: params ?? {}, // compile to url with params
});
navigate(pageUrl);
}
+
+ /**
+ * Defines if extension is enabled for a given cluster. Defaults to `true`.
+ */
+ async isEnabledForCluster(cluster: Cluster): Promise {
+ return true;
+ }
}
diff --git a/src/extensions/registries/__tests__/page-registry.test.ts b/src/extensions/registries/__tests__/page-registry.test.ts
index 47a5f82d9d..fafc801cc4 100644
--- a/src/extensions/registries/__tests__/page-registry.test.ts
+++ b/src/extensions/registries/__tests__/page-registry.test.ts
@@ -1,8 +1,8 @@
-import { getExtensionPageUrl, globalPageRegistry, PageRegistration } from "../page-registry"
-import { LensExtension } from "../../lens-extension"
+import { getExtensionPageUrl, globalPageRegistry, PageRegistration } from "../page-registry";
+import { LensExtension } from "../../lens-extension";
import React from "react";
-let ext: LensExtension = null
+let ext: LensExtension = null;
describe("getPageUrl", () => {
beforeEach(async () => {
@@ -14,25 +14,25 @@ describe("getPageUrl", () => {
manifestPath: "/this/is/fake/package.json",
isBundled: false,
isEnabled: true
- })
- })
+ });
+ });
it("returns a page url for extension", () => {
- expect(getExtensionPageUrl({ extensionId: ext.name })).toBe("/extension/foo-bar")
- })
+ expect(getExtensionPageUrl({ extensionId: ext.name })).toBe("/extension/foo-bar");
+ });
it("allows to pass base url as parameter", () => {
- expect(getExtensionPageUrl({ extensionId: ext.name, pageId: "/test" })).toBe("/extension/foo-bar/test")
- })
+ expect(getExtensionPageUrl({ extensionId: ext.name, pageId: "/test" })).toBe("/extension/foo-bar/test");
+ });
- it("removes @", () => {
- expect(getExtensionPageUrl({ extensionId: "@foo/bar" })).toBe("/extension/foo-bar")
- })
+ it("removes @ and replace `/` to `--`", () => {
+ expect(getExtensionPageUrl({ extensionId: "@foo/bar" })).toBe("/extension/foo--bar");
+ });
it("adds / prefix", () => {
- expect(getExtensionPageUrl({ extensionId: ext.name, pageId: "test" })).toBe("/extension/foo-bar/test")
- })
-})
+ expect(getExtensionPageUrl({ extensionId: ext.name, pageId: "test" })).toBe("/extension/foo-bar/test");
+ });
+});
describe("globalPageRegistry", () => {
beforeEach(async () => {
@@ -44,7 +44,7 @@ describe("globalPageRegistry", () => {
manifestPath: "/this/is/fake/package.json",
isBundled: false,
isEnabled: true
- })
+ });
globalPageRegistry.add([
{
id: "test-page",
@@ -63,12 +63,12 @@ describe("globalPageRegistry", () => {
Page: () => React.createElement('Default')
}
},
- ], ext)
- })
+ ], ext);
+ });
describe("getByPageMenuTarget", () => {
it("matching to first registered page without id", () => {
- const page = globalPageRegistry.getByPageMenuTarget({ extensionId: ext.name })
+ const page = globalPageRegistry.getByPageMenuTarget({ extensionId: ext.name });
expect(page.id).toEqual(undefined);
expect(page.extensionId).toEqual(ext.name);
expect(page.routePath).toEqual(getExtensionPageUrl({ extensionId: ext.name }));
@@ -78,16 +78,16 @@ describe("globalPageRegistry", () => {
const page = globalPageRegistry.getByPageMenuTarget({
pageId: "test-page",
extensionId: ext.name
- })
- expect(page.id).toEqual("test-page")
- })
+ });
+ expect(page.id).toEqual("test-page");
+ });
it("returns null if target not found", () => {
const page = globalPageRegistry.getByPageMenuTarget({
pageId: "wrong-page",
extensionId: ext.name
- })
- expect(page).toBeNull()
- })
- })
-})
+ });
+ expect(page).toBeNull();
+ });
+ });
+});
diff --git a/src/extensions/registries/app-preference-registry.ts b/src/extensions/registries/app-preference-registry.ts
index 6c54911f82..338f93b5bc 100644
--- a/src/extensions/registries/app-preference-registry.ts
+++ b/src/extensions/registries/app-preference-registry.ts
@@ -1,4 +1,4 @@
-import type React from "react"
+import type React from "react";
import { BaseRegistry } from "./base-registry";
export interface AppPreferenceComponents {
@@ -14,4 +14,4 @@ export interface AppPreferenceRegistration {
export class AppPreferenceRegistry extends BaseRegistry {
}
-export const appPreferenceRegistry = new AppPreferenceRegistry()
+export const appPreferenceRegistry = new AppPreferenceRegistry();
diff --git a/src/extensions/registries/base-registry.ts b/src/extensions/registries/base-registry.ts
index e82c9d11bf..ff8760151c 100644
--- a/src/extensions/registries/base-registry.ts
+++ b/src/extensions/registries/base-registry.ts
@@ -1,26 +1,27 @@
// Base class for extensions-api registries
import { action, observable } from "mobx";
import { LensExtension } from "../lens-extension";
+import { recitfy } from "../../common/utils";
-export class BaseRegistry {
+export class BaseRegistry {
private items = observable([], { deep: false });
- getItems(): I[] {
- return this.items.toJS() as I[];
+ getItems(): T[] {
+ return this.items.toJS();
}
add(items: T | T[], ext?: LensExtension): () => void; // allow method overloading with required "ext"
@action
add(items: T | T[]) {
- const normalizedItems = (Array.isArray(items) ? items : [items])
- this.items.push(...normalizedItems);
- return () => this.remove(...normalizedItems);
+ const itemArray = recitfy(items);
+ this.items.push(...itemArray);
+ return () => this.remove(...itemArray);
}
@action
remove(...items: T[]) {
items.forEach(item => {
this.items.remove(item); // works because of {deep: false};
- })
+ });
}
}
diff --git a/src/extensions/registries/cluster-feature-registry.ts b/src/extensions/registries/cluster-feature-registry.ts
index 0e3363d0f0..5017ad27a6 100644
--- a/src/extensions/registries/cluster-feature-registry.ts
+++ b/src/extensions/registries/cluster-feature-registry.ts
@@ -1,4 +1,4 @@
-import type React from "react"
+import type React from "react";
import { BaseRegistry } from "./base-registry";
import { ClusterFeature } from "../cluster-feature";
@@ -15,4 +15,4 @@ export interface ClusterFeatureRegistration {
export class ClusterFeatureRegistry extends BaseRegistry {
}
-export const clusterFeatureRegistry = new ClusterFeatureRegistry()
+export const clusterFeatureRegistry = new ClusterFeatureRegistry();
diff --git a/src/extensions/registries/index.ts b/src/extensions/registries/index.ts
index cdcfb7124b..8e343742ec 100644
--- a/src/extensions/registries/index.ts
+++ b/src/extensions/registries/index.ts
@@ -1,11 +1,11 @@
// All registries managed by extensions api
-export * from "./page-registry"
-export * from "./page-menu-registry"
-export * from "./menu-registry"
-export * from "./app-preference-registry"
-export * from "./status-bar-registry"
+export * from "./page-registry";
+export * from "./page-menu-registry";
+export * from "./menu-registry";
+export * from "./app-preference-registry";
+export * from "./status-bar-registry";
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 "./cluster-feature-registry";
+export * from "./kube-object-status-registry";
diff --git a/src/extensions/registries/kube-object-detail-registry.ts b/src/extensions/registries/kube-object-detail-registry.ts
index 32fea66b81..2638e82ade 100644
--- a/src/extensions/registries/kube-object-detail-registry.ts
+++ b/src/extensions/registries/kube-object-detail-registry.ts
@@ -1,4 +1,4 @@
-import React from "react"
+import React from "react";
import { BaseRegistry } from "./base-registry";
export interface KubeObjectDetailComponents {
@@ -15,15 +15,15 @@ export interface KubeObjectDetailRegistration {
export class KubeObjectDetailRegistry extends BaseRegistry {
getItemsForKind(kind: string, apiVersion: string) {
const items = this.getItems().filter((item) => {
- return item.kind === kind && item.apiVersions.includes(apiVersion)
+ return item.kind === kind && item.apiVersions.includes(apiVersion);
}).map((item) => {
if (item.priority === null) {
- item.priority = 50
+ item.priority = 50;
}
- return item
- })
- return items.sort((a, b) => b.priority - a.priority)
+ return item;
+ });
+ return items.sort((a, b) => b.priority - a.priority);
}
}
-export const kubeObjectDetailRegistry = new KubeObjectDetailRegistry()
+export const kubeObjectDetailRegistry = new KubeObjectDetailRegistry();
diff --git a/src/extensions/registries/kube-object-menu-registry.ts b/src/extensions/registries/kube-object-menu-registry.ts
index 8f527d6a3d..25901f66ad 100644
--- a/src/extensions/registries/kube-object-menu-registry.ts
+++ b/src/extensions/registries/kube-object-menu-registry.ts
@@ -1,4 +1,4 @@
-import React from "react"
+import React from "react";
import { BaseRegistry } from "./base-registry";
export interface KubeObjectMenuComponents {
@@ -14,9 +14,9 @@ export interface KubeObjectMenuRegistration {
export class KubeObjectMenuRegistry extends BaseRegistry {
getItemsForKind(kind: string, apiVersion: string) {
return this.getItems().filter((item) => {
- return item.kind === kind && item.apiVersions.includes(apiVersion)
- })
+ return item.kind === kind && item.apiVersions.includes(apiVersion);
+ });
}
}
-export const kubeObjectMenuRegistry = new KubeObjectMenuRegistry()
+export const kubeObjectMenuRegistry = new KubeObjectMenuRegistry();
diff --git a/src/extensions/registries/kube-object-status-registry.ts b/src/extensions/registries/kube-object-status-registry.ts
index 74fd8145d2..5f7aab8d5d 100644
--- a/src/extensions/registries/kube-object-status-registry.ts
+++ b/src/extensions/registries/kube-object-status-registry.ts
@@ -10,8 +10,8 @@ export interface KubeObjectStatusRegistration {
export class KubeObjectStatusRegistry extends BaseRegistry {
getItemsForKind(kind: string, apiVersion: string) {
return this.getItems().filter((item) => {
- return item.kind === kind && item.apiVersions.includes(apiVersion)
- })
+ return item.kind === kind && item.apiVersions.includes(apiVersion);
+ });
}
}
diff --git a/src/extensions/registries/page-menu-registry.ts b/src/extensions/registries/page-menu-registry.ts
index ebaaa42dbb..53131f507b 100644
--- a/src/extensions/registries/page-menu-registry.ts
+++ b/src/extensions/registries/page-menu-registry.ts
@@ -1,9 +1,10 @@
// Extensions-api -> Register page menu items
import type { IconProps } from "../../renderer/components/icon";
import type React from "react";
-import { action } from "mobx";
+import { action, computed } from "mobx";
import { BaseRegistry } from "./base-registry";
import { LensExtension } from "../lens-extension";
+import { RegisteredPage } from "./page-registry";
export interface PageMenuTarget {
extensionId?: string;
@@ -17,11 +18,16 @@ export interface PageMenuRegistration {
components: PageMenuComponents;
}
+export interface ClusterPageMenuRegistration extends PageMenuRegistration {
+ id?: string;
+ parentId?: string;
+}
+
export interface PageMenuComponents {
Icon: React.ComponentType;
}
-export class PageMenuRegistry extends BaseRegistry> {
+export class GlobalPageMenuRegistry extends BaseRegistry {
@action
add(items: PageMenuRegistration[], ext: LensExtension) {
const normalizedItems = items.map(menuItem => {
@@ -29,11 +35,37 @@ export class PageMenuRegistry extends BaseRegistry {
+ @action
+ add(items: PageMenuRegistration[], ext: LensExtension) {
+ const normalizedItems = items.map(menuItem => {
+ menuItem.target = {
+ extensionId: ext.name,
+ ...(menuItem.target || {}),
+ };
+ return menuItem;
+ });
+ return super.add(normalizedItems);
+ }
+
+ getRootItems() {
+ return this.getItems().filter((item) => !item.parentId);
+ }
+
+ getSubItems(parent: ClusterPageMenuRegistration) {
+ return this.getItems().filter((item) => item.parentId === parent.id && item.target.extensionId === parent.target.extensionId);
+ }
+
+ getByPage(page: RegisteredPage) {
+ return this.getItems().find((item) => item.target?.pageId == page.id && item.target?.extensionId === page.extensionId);
+ }
+}
+
+export const globalPageMenuRegistry = new GlobalPageMenuRegistry();
+export const clusterPageMenuRegistry = new ClusterPageMenuRegistry();
diff --git a/src/extensions/registries/page-registry.ts b/src/extensions/registries/page-registry.ts
index bd265bc77c..6eb2194205 100644
--- a/src/extensions/registries/page-registry.ts
+++ b/src/extensions/registries/page-registry.ts
@@ -5,8 +5,9 @@ import path from "path";
import { action } from "mobx";
import { compile } from "path-to-regexp";
import { BaseRegistry } from "./base-registry";
-import { LensExtension } from "../lens-extension";
+import { LensExtension, sanitizeExtensionName } from "../lens-extension";
import logger from "../../main/logger";
+import { recitfy } from "../../common/utils";
export interface PageRegistration {
/**
@@ -15,11 +16,6 @@ export interface PageRegistration {
* When not provided, first registered page without "id" would be used for page-menus without target.pageId for same extension
*/
id?: string;
- /**
- * Alias to page ID which assume to be used as path with possible :param placeholders
- * @deprecated
- */
- routePath?: string;
/**
* Strict route matching to provided page-id, read also: https://reactrouter.com/web/api/NavLink/exact-bool
* In case when more than one page registered at same extension "pageId" is required to identify different pages,
@@ -44,10 +40,6 @@ export interface PageComponents {
Page: React.ComponentType;
}
-export function sanitizeExtensionName(name: string) {
- return name.replace("@", "").replace("/", "-")
-}
-
export function getExtensionPageUrl({ extensionId, pageId = "", params }: PageMenuTarget
): string {
const extensionBaseUrl = compile(`/extension/:name`)({
name: sanitizeExtensionName(extensionId), // compile only with extension-id first and define base path
@@ -59,22 +51,23 @@ export function getExtensionPageUrl
({ extensionId, pageId = ""
return extPageRoutePath;
}
-export class PageRegistry extends BaseRegistry {
+export class PageRegistry extends BaseRegistry {
@action
- add(items: PageRegistration[], ext: LensExtension) {
+ add(items: PageRegistration | PageRegistration[], ext: LensExtension) {
+ const itemArray = recitfy(items);
let registeredPages: RegisteredPage[] = [];
try {
- registeredPages = items.map(page => ({
+ registeredPages = itemArray.map(page => ({
...page,
extensionId: ext.name,
- routePath: getExtensionPageUrl({ extensionId: ext.name, pageId: page.id ?? page.routePath }),
- }))
+ routePath: getExtensionPageUrl({ extensionId: ext.name, pageId: page.id }),
+ }));
} catch (err) {
logger.error(`[EXTENSION]: page-registration failed`, {
items,
extension: ext,
error: String(err),
- })
+ });
}
return super.add(registeredPages);
}
diff --git a/src/extensions/renderer-api/components.ts b/src/extensions/renderer-api/components.ts
index c051ce13e3..242799c749 100644
--- a/src/extensions/renderer-api/components.ts
+++ b/src/extensions/renderer-api/components.ts
@@ -1,36 +1,36 @@
// Common UI components
// layouts
-export * from "../../renderer/components/layout/page-layout"
-export * from "../../renderer/components/layout/wizard-layout"
-export * from "../../renderer/components/layout/tab-layout"
+export * from "../../renderer/components/layout/page-layout";
+export * from "../../renderer/components/layout/wizard-layout";
+export * from "../../renderer/components/layout/tab-layout";
// form-controls
-export * from "../../renderer/components/button"
-export * from "../../renderer/components/checkbox"
-export * from "../../renderer/components/radio"
-export * from "../../renderer/components/select"
-export * from "../../renderer/components/slider"
-export * from "../../renderer/components/input/input"
+export * from "../../renderer/components/button";
+export * from "../../renderer/components/checkbox";
+export * from "../../renderer/components/radio";
+export * from "../../renderer/components/select";
+export * from "../../renderer/components/slider";
+export * from "../../renderer/components/input/input";
// other components
-export * from "../../renderer/components/icon"
-export * from "../../renderer/components/tooltip"
-export * from "../../renderer/components/tabs"
-export * from "../../renderer/components/table"
-export * from "../../renderer/components/badge"
-export * from "../../renderer/components/drawer"
-export * from "../../renderer/components/dialog"
+export * from "../../renderer/components/icon";
+export * from "../../renderer/components/tooltip";
+export * from "../../renderer/components/tabs";
+export * from "../../renderer/components/table";
+export * from "../../renderer/components/badge";
+export * from "../../renderer/components/drawer";
+export * from "../../renderer/components/dialog";
export * from "../../renderer/components/confirm-dialog";
-export * from "../../renderer/components/line-progress"
-export * from "../../renderer/components/menu"
-export * from "../../renderer/components/notifications"
-export * from "../../renderer/components/spinner"
-export * from "../../renderer/components/stepper"
+export * from "../../renderer/components/line-progress";
+export * from "../../renderer/components/menu";
+export * from "../../renderer/components/notifications";
+export * from "../../renderer/components/spinner";
+export * from "../../renderer/components/stepper";
// kube helpers
-export * from "../../renderer/components/kube-object"
-export * from "../../renderer/components/+events/kube-event-details"
+export * from "../../renderer/components/kube-object";
+export * from "../../renderer/components/+events/kube-event-details";
// specific exports
export * from "../../renderer/components/status-brick";
diff --git a/src/extensions/renderer-api/index.ts b/src/extensions/renderer-api/index.ts
index 009f49c366..c8e14c0951 100644
--- a/src/extensions/renderer-api/index.ts
+++ b/src/extensions/renderer-api/index.ts
@@ -1,14 +1,14 @@
// Lens-extensions apis, required in renderer process runtime
// APIs
-import * as Component from "./components"
-import * as K8sApi from "./k8s-api"
-import * as Navigation from "./navigation"
-import * as Theme from "./theming"
+import * as Component from "./components";
+import * as K8sApi from "./k8s-api";
+import * as Navigation from "./navigation";
+import * as Theme from "./theming";
export {
Component,
K8sApi,
Navigation,
Theme,
-}
+};
diff --git a/src/extensions/renderer-api/k8s-api.ts b/src/extensions/renderer-api/k8s-api.ts
index 2a26b49cdd..fe04550fb7 100644
--- a/src/extensions/renderer-api/k8s-api.ts
+++ b/src/extensions/renderer-api/k8s-api.ts
@@ -1,6 +1,6 @@
-export { isAllowedResource } from "../../common/rbac"
+export { isAllowedResource } from "../../common/rbac";
export { apiManager } from "../../renderer/api/api-manager";
-export { KubeObjectStore } from "../../renderer/kube-object.store"
+export { KubeObjectStore } from "../../renderer/kube-object.store";
export { KubeApi, forCluster, IKubeApiCluster } from "../../renderer/api/kube-api";
export { KubeObject } from "../../renderer/api/kube-object";
export { Pod, podsApi, PodsApi, IPodContainer, IPodContainerStatus } from "../../renderer/api/endpoints";
@@ -31,33 +31,33 @@ export { RoleBinding, roleBindingApi } from "../../renderer/api/endpoints";
export { ClusterRole, clusterRoleApi } from "../../renderer/api/endpoints";
export { ClusterRoleBinding, clusterRoleBindingApi } from "../../renderer/api/endpoints";
export { CustomResourceDefinition, crdApi } from "../../renderer/api/endpoints";
-export { KubeObjectStatus, KubeObjectStatusLevel } from "./kube-object-status"
+export { KubeObjectStatus, KubeObjectStatusLevel } from "./kube-object-status";
// stores
-export type { EventStore } from "../../renderer/components/+events/event.store"
-export type { PodsStore } from "../../renderer/components/+workloads-pods/pods.store"
-export type { NodesStore } from "../../renderer/components/+nodes/nodes.store"
-export type { DeploymentStore } from "../../renderer/components/+workloads-deployments/deployments.store"
-export type { DaemonSetStore } from "../../renderer/components/+workloads-daemonsets/daemonsets.store"
-export type { StatefulSetStore } from "../../renderer/components/+workloads-statefulsets/statefulset.store"
-export type { JobStore } from "../../renderer/components/+workloads-jobs/job.store"
-export type { CronJobStore } from "../../renderer/components/+workloads-cronjobs/cronjob.store"
-export type { ConfigMapsStore } from "../../renderer/components/+config-maps/config-maps.store"
-export type { SecretsStore } from "../../renderer/components/+config-secrets/secrets.store"
-export type { ReplicaSetStore } from "../../renderer/components/+workloads-replicasets/replicasets.store"
-export type { ResourceQuotasStore } from "../../renderer/components/+config-resource-quotas/resource-quotas.store"
-export type { HPAStore } from "../../renderer/components/+config-autoscalers/hpa.store"
-export type { PodDisruptionBudgetsStore } from "../../renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets.store"
-export type { ServiceStore } from "../../renderer/components/+network-services/services.store"
-export type { EndpointStore } from "../../renderer/components/+network-endpoints/endpoints.store"
-export type { IngressStore } from "../../renderer/components/+network-ingresses/ingress.store"
-export type { NetworkPolicyStore } from "../../renderer/components/+network-policies/network-policy.store"
-export type { PersistentVolumesStore } from "../../renderer/components/+storage-volumes/volumes.store"
-export type { VolumeClaimStore } from "../../renderer/components/+storage-volume-claims/volume-claim.store"
-export type { StorageClassStore } from "../../renderer/components/+storage-classes/storage-class.store"
-export type { NamespaceStore } from "../../renderer/components/+namespaces/namespace.store"
-export type { ServiceAccountsStore } from "../../renderer/components/+user-management-service-accounts/service-accounts.store"
-export type { RolesStore } from "../../renderer/components/+user-management-roles/roles.store"
-export type { RoleBindingsStore } from "../../renderer/components/+user-management-roles-bindings/role-bindings.store"
-export type { CRDStore } from "../../renderer/components/+custom-resources/crd.store"
-export type { CRDResourceStore } from "../../renderer/components/+custom-resources/crd-resource.store"
+export type { EventStore } from "../../renderer/components/+events/event.store";
+export type { PodsStore } from "../../renderer/components/+workloads-pods/pods.store";
+export type { NodesStore } from "../../renderer/components/+nodes/nodes.store";
+export type { DeploymentStore } from "../../renderer/components/+workloads-deployments/deployments.store";
+export type { DaemonSetStore } from "../../renderer/components/+workloads-daemonsets/daemonsets.store";
+export type { StatefulSetStore } from "../../renderer/components/+workloads-statefulsets/statefulset.store";
+export type { JobStore } from "../../renderer/components/+workloads-jobs/job.store";
+export type { CronJobStore } from "../../renderer/components/+workloads-cronjobs/cronjob.store";
+export type { ConfigMapsStore } from "../../renderer/components/+config-maps/config-maps.store";
+export type { SecretsStore } from "../../renderer/components/+config-secrets/secrets.store";
+export type { ReplicaSetStore } from "../../renderer/components/+workloads-replicasets/replicasets.store";
+export type { ResourceQuotasStore } from "../../renderer/components/+config-resource-quotas/resource-quotas.store";
+export type { HPAStore } from "../../renderer/components/+config-autoscalers/hpa.store";
+export type { PodDisruptionBudgetsStore } from "../../renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets.store";
+export type { ServiceStore } from "../../renderer/components/+network-services/services.store";
+export type { EndpointStore } from "../../renderer/components/+network-endpoints/endpoints.store";
+export type { IngressStore } from "../../renderer/components/+network-ingresses/ingress.store";
+export type { NetworkPolicyStore } from "../../renderer/components/+network-policies/network-policy.store";
+export type { PersistentVolumesStore } from "../../renderer/components/+storage-volumes/volumes.store";
+export type { VolumeClaimStore } from "../../renderer/components/+storage-volume-claims/volume-claim.store";
+export type { StorageClassStore } from "../../renderer/components/+storage-classes/storage-class.store";
+export type { NamespaceStore } from "../../renderer/components/+namespaces/namespace.store";
+export type { ServiceAccountsStore } from "../../renderer/components/+user-management-service-accounts/service-accounts.store";
+export type { RolesStore } from "../../renderer/components/+user-management-roles/roles.store";
+export type { RoleBindingsStore } from "../../renderer/components/+user-management-roles-bindings/role-bindings.store";
+export type { CRDStore } from "../../renderer/components/+custom-resources/crd.store";
+export type { CRDResourceStore } from "../../renderer/components/+custom-resources/crd-resource.store";
diff --git a/src/extensions/renderer-api/kube-object-status.ts b/src/extensions/renderer-api/kube-object-status.ts
index 22994ee85d..f609d736fe 100644
--- a/src/extensions/renderer-api/kube-object-status.ts
+++ b/src/extensions/renderer-api/kube-object-status.ts
@@ -2,7 +2,7 @@ export type KubeObjectStatus = {
level: KubeObjectStatusLevel;
text: string;
timestamp?: string;
-}
+};
export enum KubeObjectStatusLevel {
INFO = 1,
diff --git a/src/extensions/renderer-api/navigation.ts b/src/extensions/renderer-api/navigation.ts
index f923f6e152..a1191a4b30 100644
--- a/src/extensions/renderer-api/navigation.ts
+++ b/src/extensions/renderer-api/navigation.ts
@@ -1,3 +1,3 @@
export { navigate } from "../../renderer/navigation";
-export { hideDetails, showDetails, getDetailsUrl } from "../../renderer/navigation"
+export { hideDetails, showDetails, getDetailsUrl } from "../../renderer/navigation";
export { IURLParams } from "../../common/utils/buildUrl";
diff --git a/src/jest.setup.ts b/src/jest.setup.ts
index 08727bc910..7b4732930e 100644
--- a/src/jest.setup.ts
+++ b/src/jest.setup.ts
@@ -1,4 +1,4 @@
-import fetchMock from "jest-fetch-mock"
+import fetchMock from "jest-fetch-mock";
// rewire global.fetch to call 'fetchMock'
fetchMock.enableMocks();
diff --git a/src/main/__test__/cluster.test.ts b/src/main/__test__/cluster.test.ts
index 52e984b195..95177408af 100644
--- a/src/main/__test__/cluster.test.ts
+++ b/src/main/__test__/cluster.test.ts
@@ -21,33 +21,35 @@ jest.mock("winston", () => ({
Console: jest.fn(),
File: jest.fn(),
}
-}))
+}));
-jest.mock("../../common/ipc")
-jest.mock("../context-handler")
-jest.mock("request")
-jest.mock("request-promise-native")
+jest.mock("../../common/ipc");
+jest.mock("../context-handler");
+jest.mock("request");
+jest.mock("request-promise-native");
import { Console } from "console";
import mockFs from "mock-fs";
import { workspaceStore } from "../../common/workspace-store";
-import { Cluster } from "../cluster"
+import { Cluster } from "../cluster";
import { ContextHandler } from "../context-handler";
import { getFreePort } from "../port";
import { V1ResourceAttributes } from "@kubernetes/client-node";
import { apiResources } from "../../common/rbac";
-import request from "request-promise-native"
+import request from "request-promise-native";
import { Kubectl } from "../kubectl";
-const mockedRequest = request as jest.MockedFunction
+const mockedRequest = request as jest.MockedFunction;
-console = new Console(process.stdout, process.stderr) // fix mockFS
+console = new Console(process.stdout, process.stderr); // fix mockFS
describe("create clusters", () => {
beforeEach(() => {
- jest.clearAllMocks()
- })
+ jest.clearAllMocks();
+ });
+
+ let c: Cluster;
beforeEach(() => {
const mockOpts = {
@@ -72,66 +74,68 @@ describe("create clusters", () => {
kind: "Config",
preferences: {},
})
- }
- mockFs(mockOpts)
- jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValue(Promise.resolve(true))
- })
+ };
+ mockFs(mockOpts);
+ jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValue(Promise.resolve(true));
+ c = new Cluster({
+ id: "foo",
+ contextName: "minikube",
+ kubeConfigPath: "minikube-config.yml",
+ workspace: workspaceStore.currentWorkspaceId
+ });
+ });
afterEach(() => {
- mockFs.restore()
- })
+ mockFs.restore();
+ });
it("should be able to create a cluster from a cluster model and apiURL should be decoded", () => {
- const c = new Cluster({
- id: "foo",
- contextName: "minikube",
- kubeConfigPath: "minikube-config.yml",
- workspace: workspaceStore.currentWorkspaceId
- })
- expect(c.apiUrl).toBe("https://192.168.64.3:8443")
- })
+ expect(c.apiUrl).toBe("https://192.168.64.3:8443");
+ });
+
+ it("reconnect should not throw if contextHandler is missing", () => {
+ expect(() => c.reconnect()).not.toThrowError();
+ });
+
+ it("disconnect should not throw if contextHandler is missing", () => {
+ expect(() => c.disconnect()).not.toThrowError();
+ });
it("init should not throw if everything is in order", async () => {
- const c = new Cluster({
- id: "foo",
- contextName: "minikube",
- kubeConfigPath: "minikube-config.yml",
- workspace: workspaceStore.currentWorkspaceId
- })
- await c.init(await getFreePort())
+ await c.init(await getFreePort());
expect(logger.info).toBeCalledWith(expect.stringContaining("init success"), {
id: "foo",
apiUrl: "https://192.168.64.3:8443",
context: "minikube",
- })
- })
+ });
+ });
it("activating cluster should try to connect to cluster and do a refresh", async () => {
- const port = await getFreePort()
+ const port = await getFreePort();
jest.spyOn(ContextHandler.prototype, "ensureServer");
- const mockListNSs = jest.fn()
+ const mockListNSs = jest.fn();
const mockKC = {
makeApiClient() {
return {
listNamespace: mockListNSs,
- }
+ };
}
- }
- jest.spyOn(Cluster.prototype, "isClusterAdmin").mockReturnValue(Promise.resolve(true))
+ };
+ jest.spyOn(Cluster.prototype, "isClusterAdmin").mockReturnValue(Promise.resolve(true));
jest.spyOn(Cluster.prototype, "canI")
.mockImplementationOnce((attr: V1ResourceAttributes): Promise => {
- expect(attr.namespace).toBe("default")
- expect(attr.resource).toBe("pods")
- expect(attr.verb).toBe("list")
- return Promise.resolve(true)
+ expect(attr.namespace).toBe("default");
+ expect(attr.resource).toBe("pods");
+ expect(attr.verb).toBe("list");
+ return Promise.resolve(true);
})
.mockImplementation((attr: V1ResourceAttributes): Promise => {
- expect(attr.namespace).toBe("default")
- expect(attr.verb).toBe("list")
- return Promise.resolve(true)
- })
- jest.spyOn(Cluster.prototype, "getProxyKubeconfig").mockReturnValue(mockKC as any)
+ expect(attr.namespace).toBe("default");
+ expect(attr.verb).toBe("list");
+ return Promise.resolve(true);
+ });
+ jest.spyOn(Cluster.prototype, "getProxyKubeconfig").mockReturnValue(mockKC as any);
mockListNSs.mockImplementationOnce(() => ({
body: {
items: [{
@@ -140,28 +144,36 @@ describe("create clusters", () => {
}
}]
}
- }))
+ }));
mockedRequest.mockImplementationOnce(((uri: any, _options: any) => {
- expect(uri).toBe(`http://localhost:${port}/api-kube/version`)
- return Promise.resolve({ gitVersion: "1.2.3" })
- }) as any)
+ expect(uri).toBe(`http://localhost:${port}/api-kube/version`);
+ return Promise.resolve({ gitVersion: "1.2.3" });
+ }) as any);
- const c = new Cluster({
+ const c = new class extends Cluster {
+ // only way to mock protected methods, without these we leak promises
+ protected bindEvents() {
+ return;
+ }
+ protected async ensureKubectl() {
+ return Promise.resolve(true);
+ }
+ }({
id: "foo",
contextName: "minikube",
kubeConfigPath: "minikube-config.yml",
workspace: workspaceStore.currentWorkspaceId
- })
- await c.init(port)
- await c.activate()
+ });
+ await c.init(port);
+ await c.activate();
- expect(ContextHandler.prototype.ensureServer).toBeCalled()
- expect(mockedRequest).toBeCalled()
- expect(c.accessible).toBe(true)
- expect(c.allowedNamespaces.length).toBe(1)
- expect(c.allowedResources.length).toBe(apiResources.length)
- c.disconnect()
- jest.resetAllMocks()
- })
-})
+ expect(ContextHandler.prototype.ensureServer).toBeCalled();
+ expect(mockedRequest).toBeCalled();
+ expect(c.accessible).toBe(true);
+ expect(c.allowedNamespaces.length).toBe(1);
+ expect(c.allowedResources.length).toBe(apiResources.length);
+ c.disconnect();
+ jest.resetAllMocks();
+ });
+});
diff --git a/src/main/__test__/kube-auth-proxy.test.ts b/src/main/__test__/kube-auth-proxy.test.ts
index e7bee67c07..dbb3e308e3 100644
--- a/src/main/__test__/kube-auth-proxy.test.ts
+++ b/src/main/__test__/kube-auth-proxy.test.ts
@@ -21,110 +21,109 @@ jest.mock("winston", () => ({
Console: jest.fn(),
File: jest.fn(),
}
-}))
+}));
-jest.mock("../../common/ipc")
-jest.mock("child_process")
-jest.mock("tcp-port-used")
+jest.mock("../../common/ipc");
+jest.mock("child_process");
+jest.mock("tcp-port-used");
-import { Cluster } from "../cluster"
-import { KubeAuthProxy } from "../kube-auth-proxy"
-import { getFreePort } from "../port"
-import { broadcastIpc } from "../../common/ipc"
-import { ChildProcess, spawn, SpawnOptions } from "child_process"
-import { bundledKubectlPath, Kubectl } from "../kubectl"
+import { Cluster } from "../cluster";
+import { KubeAuthProxy } from "../kube-auth-proxy";
+import { getFreePort } from "../port";
+import { broadcastMessage } from "../../common/ipc";
+import { ChildProcess, spawn, SpawnOptions } from "child_process";
+import { bundledKubectlPath, Kubectl } from "../kubectl";
import { mock, MockProxy } from 'jest-mock-extended';
import { waitUntilUsed } from 'tcp-port-used';
-import { Readable } from "stream"
+import { Readable } from "stream";
-const mockBroadcastIpc = broadcastIpc as jest.MockedFunction
-const mockSpawn = spawn as jest.MockedFunction
-const mockWaitUntilUsed = waitUntilUsed as jest.MockedFunction
+const mockBroadcastIpc = broadcastMessage as jest.MockedFunction;
+const mockSpawn = spawn as jest.MockedFunction;
+const mockWaitUntilUsed = waitUntilUsed as jest.MockedFunction;
describe("kube auth proxy tests", () => {
beforeEach(() => {
- jest.clearAllMocks()
- })
+ jest.clearAllMocks();
+ });
it("calling exit multiple times shouldn't throw", async () => {
- const port = await getFreePort()
- const kap = new KubeAuthProxy(new Cluster({ id: "foobar", kubeConfigPath: "fake-path.yml" }), port, {})
- kap.exit()
- kap.exit()
- kap.exit()
- })
+ const port = await getFreePort();
+ const kap = new KubeAuthProxy(new Cluster({ id: "foobar", kubeConfigPath: "fake-path.yml" }), port, {});
+ kap.exit();
+ kap.exit();
+ kap.exit();
+ });
describe("spawn tests", () => {
- let port: number
- let mockedCP: MockProxy
- let listeners: Record void>
+ let port: number;
+ let mockedCP: MockProxy;
+ let listeners: Record void>;
+ let proxy: KubeAuthProxy;
beforeEach(async () => {
- port = await getFreePort()
- mockedCP = mock()
- listeners = {}
+ port = await getFreePort();
+ mockedCP = mock();
+ listeners = {};
- jest.spyOn(Kubectl.prototype, "checkBinary").mockReturnValueOnce(Promise.resolve(true))
- jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValueOnce(Promise.resolve(false))
+ jest.spyOn(Kubectl.prototype, "checkBinary").mockReturnValueOnce(Promise.resolve(true));
+ jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValueOnce(Promise.resolve(false));
mockedCP.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): ChildProcess => {
- listeners[event] = listener
- return mockedCP
- })
- mockedCP.stderr = mock()
+ listeners[event] = listener;
+ return mockedCP;
+ });
+ mockedCP.stderr = mock();
mockedCP.stderr.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): Readable => {
- listeners[`stderr/${event}`] = listener
- return mockedCP.stderr
- })
- mockedCP.stdout = mock()
+ listeners[`stderr/${event}`] = listener;
+ return mockedCP.stderr;
+ });
+ mockedCP.stdout = mock();
mockedCP.stdout.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): Readable => {
- listeners[`stdout/${event}`] = listener
- return mockedCP.stdout
- })
+ listeners[`stdout/${event}`] = listener;
+ return mockedCP.stdout;
+ });
mockSpawn.mockImplementationOnce((command: string, args: readonly string[], options: SpawnOptions): ChildProcess => {
- expect(command).toBe(bundledKubectlPath())
- return mockedCP
- })
- mockWaitUntilUsed.mockReturnValueOnce(Promise.resolve())
- })
+ expect(command).toBe(bundledKubectlPath());
+ return mockedCP;
+ });
+ mockWaitUntilUsed.mockReturnValueOnce(Promise.resolve());
+ const cluster = new Cluster({ id: "foobar", kubeConfigPath: "fake-path.yml" });
+ jest.spyOn(cluster, "apiUrl", "get").mockReturnValue("https://fake.k8s.internal");
+ proxy = new KubeAuthProxy(cluster, port, {});
+ });
it("should call spawn and broadcast errors", async () => {
- const kap = new KubeAuthProxy(new Cluster({ id: "foobar", kubeConfigPath: "fake-path.yml" }), port, {})
- await kap.run()
- listeners["error"]({ message: "foobarbat" })
+ await proxy.run();
+ listeners["error"]({ message: "foobarbat" });
- expect(mockBroadcastIpc).toBeCalledWith({ channel: "kube-auth:foobar", args: [{ data: "foobarbat", error: true }] })
- })
+ expect(mockBroadcastIpc).toBeCalledWith("kube-auth:foobar", { data: "foobarbat", error: true });
+ });
it("should call spawn and broadcast exit", async () => {
- const kap = new KubeAuthProxy(new Cluster({ id: "foobar", kubeConfigPath: "fake-path.yml" }), port, {})
- await kap.run()
- listeners["exit"](0)
+ await proxy.run();
+ listeners["exit"](0);
- expect(mockBroadcastIpc).toBeCalledWith({ channel: "kube-auth:foobar", args: [{ data: "proxy exited with code: 0", error: false }] })
- })
+ expect(mockBroadcastIpc).toBeCalledWith("kube-auth:foobar", { data: "proxy exited with code: 0", error: false });
+ });
it("should call spawn and broadcast errors from stderr", async () => {
- const kap = new KubeAuthProxy(new Cluster({ id: "foobar", kubeConfigPath: "fake-path.yml" }), port, {})
- await kap.run()
- listeners["stderr/data"]("an error")
+ await proxy.run();
+ listeners["stderr/data"]("an error");
- expect(mockBroadcastIpc).toBeCalledWith({ channel: "kube-auth:foobar", args: [{ data: "an error", error: true }] })
- })
+ expect(mockBroadcastIpc).toBeCalledWith("kube-auth:foobar", { data: "an error", error: true });
+ });
it("should call spawn and broadcast stdout serving info", async () => {
- const kap = new KubeAuthProxy(new Cluster({ id: "foobar", kubeConfigPath: "fake-path.yml" }), port, {})
- await kap.run()
- listeners["stdout/data"]("Starting to serve on")
+ await proxy.run();
+ listeners["stdout/data"]("Starting to serve on");
- expect(mockBroadcastIpc).toBeCalledWith({ channel: "kube-auth:foobar", args: [{ data: "Authentication proxy started\n" }] })
- })
+ expect(mockBroadcastIpc).toBeCalledWith("kube-auth:foobar", { data: "Authentication proxy started\n" });
+ });
it("should call spawn and broadcast stdout other info", async () => {
- const kap = new KubeAuthProxy(new Cluster({ id: "foobar", kubeConfigPath: "fake-path.yml" }), port, {})
- await kap.run()
- listeners["stdout/data"]("some info")
+ await proxy.run();
+ listeners["stdout/data"]("some info");
- expect(mockBroadcastIpc).toBeCalledWith({ channel: "kube-auth:foobar", args: [{ data: "some info" }] })
- })
- })
-})
+ expect(mockBroadcastIpc).toBeCalledWith("kube-auth:foobar", { data: "some info" });
+ });
+ });
+});
diff --git a/src/main/__test__/kubeconfig-manager.test.ts b/src/main/__test__/kubeconfig-manager.test.ts
index a0a4060111..152a13055d 100644
--- a/src/main/__test__/kubeconfig-manager.test.ts
+++ b/src/main/__test__/kubeconfig-manager.test.ts
@@ -21,24 +21,24 @@ jest.mock("winston", () => ({
Console: jest.fn(),
File: jest.fn(),
}
-}))
+}));
-import { KubeconfigManager } from "../kubeconfig-manager"
-import mockFs from "mock-fs"
+import { KubeconfigManager } from "../kubeconfig-manager";
+import mockFs from "mock-fs";
import { Cluster } from "../cluster";
import { workspaceStore } from "../../common/workspace-store";
import { ContextHandler } from "../context-handler";
import { getFreePort } from "../port";
-import fse from "fs-extra"
+import fse from "fs-extra";
import { loadYaml } from "@kubernetes/client-node";
import { Console } from "console";
-console = new Console(process.stdout, process.stderr) // fix mockFS
+console = new Console(process.stdout, process.stderr); // fix mockFS
describe("kubeconfig manager tests", () => {
beforeEach(() => {
- jest.clearAllMocks()
- })
+ jest.clearAllMocks();
+ });
beforeEach(() => {
const mockOpts = {
@@ -63,13 +63,13 @@ describe("kubeconfig manager tests", () => {
kind: "Config",
preferences: {},
})
- }
- mockFs(mockOpts)
- })
+ };
+ mockFs(mockOpts);
+ });
afterEach(() => {
- mockFs.restore()
- })
+ mockFs.restore();
+ });
it("should create 'temp' kube config with proxy", async () => {
const cluster = new Cluster({
@@ -77,19 +77,19 @@ describe("kubeconfig manager tests", () => {
contextName: "minikube",
kubeConfigPath: "minikube-config.yml",
workspace: workspaceStore.currentWorkspaceId
- })
- const contextHandler = new ContextHandler(cluster)
- const port = await getFreePort()
- const kubeConfManager = await KubeconfigManager.create(cluster, contextHandler, port)
+ });
+ const contextHandler = new ContextHandler(cluster);
+ const port = await getFreePort();
+ const kubeConfManager = await KubeconfigManager.create(cluster, contextHandler, port);
- expect(logger.error).not.toBeCalled()
- expect(kubeConfManager.getPath()).toBe("tmp/kubeconfig-foo")
- const file = await fse.readFile(kubeConfManager.getPath())
- const yml = loadYaml(file.toString())
- expect(yml["current-context"]).toBe("minikube")
- expect(yml["clusters"][0]["cluster"]["server"]).toBe(`http://127.0.0.1:${port}/foo`)
- expect(yml["users"][0]["name"]).toBe("proxy")
- })
+ expect(logger.error).not.toBeCalled();
+ expect(kubeConfManager.getPath()).toBe("tmp/kubeconfig-foo");
+ const file = await fse.readFile(kubeConfManager.getPath());
+ const yml = loadYaml(file.toString());
+ expect(yml["current-context"]).toBe("minikube");
+ expect(yml["clusters"][0]["cluster"]["server"]).toBe(`http://127.0.0.1:${port}/foo`);
+ expect(yml["users"][0]["name"]).toBe("proxy");
+ });
it("should remove 'temp' kube config on unlink and remove reference from inside class", async () => {
const cluster = new Cluster({
@@ -97,16 +97,16 @@ describe("kubeconfig manager tests", () => {
contextName: "minikube",
kubeConfigPath: "minikube-config.yml",
workspace: workspaceStore.currentWorkspaceId
- })
- const contextHandler = new ContextHandler(cluster)
- const port = await getFreePort()
- const kubeConfManager = await KubeconfigManager.create(cluster, contextHandler, port)
+ });
+ const contextHandler = new ContextHandler(cluster);
+ const port = await getFreePort();
+ const kubeConfManager = await KubeconfigManager.create(cluster, contextHandler, port);
- const configPath = kubeConfManager.getPath()
- expect(await fse.pathExists(configPath)).toBe(true)
- await kubeConfManager.unlink()
- expect(await fse.pathExists(configPath)).toBe(false)
- await kubeConfManager.unlink() // doesn't throw
- expect(kubeConfManager.getPath()).toBeUndefined()
- })
-})
+ const configPath = kubeConfManager.getPath();
+ expect(await fse.pathExists(configPath)).toBe(true);
+ await kubeConfManager.unlink();
+ expect(await fse.pathExists(configPath)).toBe(false);
+ await kubeConfManager.unlink(); // doesn't throw
+ expect(kubeConfManager.getPath()).toBeUndefined();
+ });
+});
diff --git a/src/main/app-updater.ts b/src/main/app-updater.ts
index 3613c3ef18..c7b6659149 100644
--- a/src/main/app-updater.ts
+++ b/src/main/app-updater.ts
@@ -1,19 +1,19 @@
-import { autoUpdater } from "electron-updater"
-import logger from "./logger"
+import { autoUpdater } from "electron-updater";
+import logger from "./logger";
export class AppUpdater {
- static readonly defaultUpdateIntervalMs = 1000 * 60 * 60 * 24 // once a day
+ static readonly defaultUpdateIntervalMs = 1000 * 60 * 60 * 24; // once a day
static checkForUpdates() {
- return autoUpdater.checkForUpdatesAndNotify()
+ return autoUpdater.checkForUpdatesAndNotify();
}
constructor(protected updateInterval = AppUpdater.defaultUpdateIntervalMs) {
- autoUpdater.logger = logger
+ autoUpdater.logger = logger;
}
public start() {
- setInterval(AppUpdater.checkForUpdates, this.updateInterval)
+ setInterval(AppUpdater.checkForUpdates, this.updateInterval);
return AppUpdater.checkForUpdates();
}
}
diff --git a/src/main/cluster-detectors/base-cluster-detector.ts b/src/main/cluster-detectors/base-cluster-detector.ts
index 8663313005..f73cc2ac81 100644
--- a/src/main/cluster-detectors/base-cluster-detector.ts
+++ b/src/main/cluster-detectors/base-cluster-detector.ts
@@ -1,21 +1,21 @@
-import request, { RequestPromiseOptions } from "request-promise-native"
+import request, { RequestPromiseOptions } from "request-promise-native";
import { Cluster } from "../cluster";
export type ClusterDetectionResult = {
value: string | number | boolean
accuracy: number
-}
+};
export class BaseClusterDetector {
- cluster: Cluster
- key: string
+ cluster: Cluster;
+ key: string;
constructor(cluster: Cluster) {
- this.cluster = cluster
+ this.cluster = cluster;
}
detect(): Promise {
- return null
+ return null;
}
protected async k8sRequest(path: string, options: RequestPromiseOptions = {}): Promise {
@@ -28,6 +28,6 @@ export class BaseClusterDetector {
Host: `${this.cluster.id}.${new URL(this.cluster.kubeProxyUrl).host}`, // required in ClusterManager.getClusterForRequest()
...(options.headers || {}),
},
- })
+ });
}
}
\ No newline at end of file
diff --git a/src/main/cluster-detectors/cluster-id-detector.ts b/src/main/cluster-detectors/cluster-id-detector.ts
index 558e52d43c..2605ca269f 100644
--- a/src/main/cluster-detectors/cluster-id-detector.ts
+++ b/src/main/cluster-detectors/cluster-id-detector.ts
@@ -1,23 +1,23 @@
import { BaseClusterDetector } from "./base-cluster-detector";
-import { createHash } from "crypto"
+import { createHash } from "crypto";
import { ClusterMetadataKey } from "../cluster";
export class ClusterIdDetector extends BaseClusterDetector {
- key = ClusterMetadataKey.CLUSTER_ID
+ key = ClusterMetadataKey.CLUSTER_ID;
public async detect() {
- let id: string
+ let id: string;
try {
- id = await this.getDefaultNamespaceId()
+ id = await this.getDefaultNamespaceId();
} catch(_) {
- id = this.cluster.apiUrl
+ id = this.cluster.apiUrl;
}
- const value = createHash("sha256").update(id).digest("hex")
- return { value: value, accuracy: 100 }
+ const value = createHash("sha256").update(id).digest("hex");
+ return { value, accuracy: 100 };
}
protected async getDefaultNamespaceId() {
- const response = await this.k8sRequest("/api/v1/namespaces/default")
- return response.metadata.uid
+ const response = await this.k8sRequest("/api/v1/namespaces/default");
+ return response.metadata.uid;
}
}
\ No newline at end of file
diff --git a/src/main/cluster-detectors/detector-registry.ts b/src/main/cluster-detectors/detector-registry.ts
index 577fdf2d85..d4abe01304 100644
--- a/src/main/cluster-detectors/detector-registry.ts
+++ b/src/main/cluster-detectors/detector-registry.ts
@@ -12,34 +12,34 @@ export class DetectorRegistry {
registry = observable.array([], { deep: false });
add(detectorClass: typeof BaseClusterDetector) {
- this.registry.push(detectorClass)
+ this.registry.push(detectorClass);
}
async detectForCluster(cluster: Cluster): Promise {
- const results: {[key: string]: ClusterDetectionResult } = {}
+ const results: {[key: string]: ClusterDetectionResult } = {};
for (const detectorClass of this.registry) {
- const detector = new detectorClass(cluster)
+ const detector = new detectorClass(cluster);
try {
- const data = await detector.detect()
+ const data = await detector.detect();
if (!data) continue;
- const existingValue = results[detector.key]
+ const existingValue = results[detector.key];
if (existingValue && existingValue.accuracy > data.accuracy) continue; // previous value exists and is more accurate
- results[detector.key] = data
+ results[detector.key] = data;
} catch (e) {
// detector raised error, do nothing
}
}
- const metadata: ClusterMetadata = {}
+ const metadata: ClusterMetadata = {};
for (const [key, result] of Object.entries(results)) {
- metadata[key] = result.value
+ metadata[key] = result.value;
}
- return metadata
+ return metadata;
}
}
-export const detectorRegistry = new DetectorRegistry()
-detectorRegistry.add(ClusterIdDetector)
-detectorRegistry.add(LastSeenDetector)
-detectorRegistry.add(VersionDetector)
-detectorRegistry.add(DistributionDetector)
-detectorRegistry.add(NodesCountDetector)
\ No newline at end of file
+export const detectorRegistry = new DetectorRegistry();
+detectorRegistry.add(ClusterIdDetector);
+detectorRegistry.add(LastSeenDetector);
+detectorRegistry.add(VersionDetector);
+detectorRegistry.add(DistributionDetector);
+detectorRegistry.add(NodesCountDetector);
\ No newline at end of file
diff --git a/src/main/cluster-detectors/distribution-detector.ts b/src/main/cluster-detectors/distribution-detector.ts
index b5895f8a71..181425cb26 100644
--- a/src/main/cluster-detectors/distribution-detector.ts
+++ b/src/main/cluster-detectors/distribution-detector.ts
@@ -2,79 +2,79 @@ import { BaseClusterDetector } from "./base-cluster-detector";
import { ClusterMetadataKey } from "../cluster";
export class DistributionDetector extends BaseClusterDetector {
- key = ClusterMetadataKey.DISTRIBUTION
- version: string
+ key = ClusterMetadataKey.DISTRIBUTION;
+ version: string;
public async detect() {
- this.version = await this.getKubernetesVersion()
+ this.version = await this.getKubernetesVersion();
if (await this.isRancher()) {
- return { value: "rancher", accuracy: 80}
+ return { value: "rancher", accuracy: 80};
}
if (this.isGKE()) {
- return { value: "gke", accuracy: 80}
+ return { value: "gke", accuracy: 80};
}
if (this.isEKS()) {
- return { value: "eks", accuracy: 80}
+ return { value: "eks", accuracy: 80};
}
if (this.isIKS()) {
- return { value: "iks", accuracy: 80}
+ return { value: "iks", accuracy: 80};
}
if (this.isAKS()) {
- return { value: "aks", accuracy: 80}
+ return { value: "aks", accuracy: 80};
}
if (this.isDigitalOcean()) {
- return { value: "digitalocean", accuracy: 90}
+ return { value: "digitalocean", accuracy: 90};
}
if (this.isMinikube()) {
- return { value: "minikube", accuracy: 80}
+ return { value: "minikube", accuracy: 80};
}
if (this.isCustom()) {
- return { value: "custom", accuracy: 10}
+ return { value: "custom", accuracy: 10};
}
- return { value: "unknown", accuracy: 10}
+ return { value: "unknown", accuracy: 10};
}
public async getKubernetesVersion() {
- if (this.cluster.version) return this.cluster.version
+ if (this.cluster.version) return this.cluster.version;
- const response = await this.k8sRequest("/version")
- return response.gitVersion
+ const response = await this.k8sRequest("/version");
+ return response.gitVersion;
}
protected isGKE() {
- return this.version.includes("gke")
+ return this.version.includes("gke");
}
protected isEKS() {
- return this.version.includes("eks")
+ return this.version.includes("eks");
}
protected isIKS() {
- return this.version.includes("IKS")
+ return this.version.includes("IKS");
}
protected isAKS() {
- return this.cluster.apiUrl.endsWith("azmk8s.io")
+ return this.cluster.apiUrl.endsWith("azmk8s.io");
}
protected isDigitalOcean() {
- return this.cluster.apiUrl.endsWith("k8s.ondigitalocean.com")
+ return this.cluster.apiUrl.endsWith("k8s.ondigitalocean.com");
}
protected isMinikube() {
- return this.cluster.contextName.startsWith("minikube")
+ return this.cluster.contextName.startsWith("minikube");
}
protected isCustom() {
- return this.version.includes("+")
+ return this.version.includes("+");
}
protected async isRancher() {
try {
- const response = await this.k8sRequest("")
- return response.data.find((api: any) => api?.apiVersion?.group === "meta.cattle.io") !== undefined
+ const response = await this.k8sRequest("");
+ return response.data.find((api: any) => api?.apiVersion?.group === "meta.cattle.io") !== undefined;
} catch (e) {
- return false
+ return false;
}
}
}
\ No newline at end of file
diff --git a/src/main/cluster-detectors/last-seen-detector.ts b/src/main/cluster-detectors/last-seen-detector.ts
index 0c231116fe..d56483625a 100644
--- a/src/main/cluster-detectors/last-seen-detector.ts
+++ b/src/main/cluster-detectors/last-seen-detector.ts
@@ -2,12 +2,12 @@ import { BaseClusterDetector } from "./base-cluster-detector";
import { ClusterMetadataKey } from "../cluster";
export class LastSeenDetector extends BaseClusterDetector {
- key = ClusterMetadataKey.LAST_SEEN
+ key = ClusterMetadataKey.LAST_SEEN;
public async detect() {
if (!this.cluster.accessible) return null;
- await this.k8sRequest("/version")
- return { value: new Date().toJSON(), accuracy: 100 }
+ await this.k8sRequest("/version");
+ return { value: new Date().toJSON(), accuracy: 100 };
}
}
\ No newline at end of file
diff --git a/src/main/cluster-detectors/nodes-count-detector.ts b/src/main/cluster-detectors/nodes-count-detector.ts
index 858ff43d9f..ba5fc93583 100644
--- a/src/main/cluster-detectors/nodes-count-detector.ts
+++ b/src/main/cluster-detectors/nodes-count-detector.ts
@@ -2,16 +2,16 @@ import { BaseClusterDetector } from "./base-cluster-detector";
import { ClusterMetadataKey } from "../cluster";
export class NodesCountDetector extends BaseClusterDetector {
- key = ClusterMetadataKey.NODES_COUNT
+ key = ClusterMetadataKey.NODES_COUNT;
public async detect() {
if (!this.cluster.accessible) return null;
- const nodeCount = await this.getNodeCount()
- return { value: nodeCount, accuracy: 100}
+ const nodeCount = await this.getNodeCount();
+ return { value: nodeCount, accuracy: 100};
}
protected async getNodeCount(): Promise {
- const response = await this.k8sRequest("/api/v1/nodes")
- return response.items.length
+ const response = await this.k8sRequest("/api/v1/nodes");
+ return response.items.length;
}
}
\ No newline at end of file
diff --git a/src/main/cluster-detectors/version-detector.ts b/src/main/cluster-detectors/version-detector.ts
index 4092b40b42..e59e6291b9 100644
--- a/src/main/cluster-detectors/version-detector.ts
+++ b/src/main/cluster-detectors/version-detector.ts
@@ -2,16 +2,16 @@ import { BaseClusterDetector } from "./base-cluster-detector";
import { ClusterMetadataKey } from "../cluster";
export class VersionDetector extends BaseClusterDetector {
- key = ClusterMetadataKey.VERSION
- value: string
+ key = ClusterMetadataKey.VERSION;
+ value: string;
public async detect() {
- const version = await this.getKubernetesVersion()
- return { value: version, accuracy: 100}
+ const version = await this.getKubernetesVersion();
+ return { value: version, accuracy: 100};
}
public async getKubernetesVersion() {
- const response = await this.k8sRequest("/version")
- return response.gitVersion
+ const response = await this.k8sRequest("/version");
+ return response.gitVersion;
}
}
\ No newline at end of file
diff --git a/src/main/cluster-manager.ts b/src/main/cluster-manager.ts
index 1792fb953b..9b2e88ef89 100644
--- a/src/main/cluster-manager.ts
+++ b/src/main/cluster-manager.ts
@@ -1,14 +1,16 @@
import "../common/cluster-ipc";
-import type http from "http"
-import { ipcMain } from "electron"
+import type http from "http";
+import { ipcMain } from "electron";
import { autorun } from "mobx";
-import { clusterStore, getClusterIdFromHost } from "../common/cluster-store"
-import { Cluster } from "./cluster"
+import { clusterStore, getClusterIdFromHost } from "../common/cluster-store";
+import { Cluster } from "./cluster";
import logger from "./logger";
import { apiKubePrefix } from "../common/vars";
+import { Singleton } from "../common/utils";
-export class ClusterManager {
+export class ClusterManager extends Singleton {
constructor(public readonly port: number) {
+ super();
// auto-init clusters
autorun(() => {
clusterStore.enabledClustersList.forEach(cluster => {
@@ -32,52 +34,52 @@ export class ClusterManager {
delay: 250
});
- ipcMain.on("network:offline", () => { this.onNetworkOffline() })
- ipcMain.on("network:online", () => { this.onNetworkOnline() })
+ ipcMain.on("network:offline", () => { this.onNetworkOffline(); });
+ ipcMain.on("network:online", () => { this.onNetworkOnline(); });
}
protected onNetworkOffline() {
- logger.info("[CLUSTER-MANAGER]: network is offline")
+ logger.info("[CLUSTER-MANAGER]: network is offline");
clusterStore.enabledClustersList.forEach((cluster) => {
if (!cluster.disconnected) {
- cluster.online = false
- cluster.accessible = false
- cluster.refreshConnectionStatus().catch((e) => e)
+ cluster.online = false;
+ cluster.accessible = false;
+ cluster.refreshConnectionStatus().catch((e) => e);
}
- })
+ });
}
protected onNetworkOnline() {
- logger.info("[CLUSTER-MANAGER]: network is online")
+ logger.info("[CLUSTER-MANAGER]: network is online");
clusterStore.enabledClustersList.forEach((cluster) => {
if (!cluster.disconnected) {
- cluster.refreshConnectionStatus().catch((e) => e)
+ cluster.refreshConnectionStatus().catch((e) => e);
}
- })
+ });
}
stop() {
clusterStore.clusters.forEach((cluster: Cluster) => {
cluster.disconnect();
- })
+ });
}
getClusterForRequest(req: http.IncomingMessage): Cluster {
- let cluster: Cluster = null
+ let cluster: Cluster = null;
// lens-server is connecting to 127.0.0.1:/
if (req.headers.host.startsWith("127.0.0.1")) {
- const clusterId = req.url.split("/")[1]
- cluster = clusterStore.getById(clusterId)
+ const clusterId = req.url.split("/")[1];
+ cluster = clusterStore.getById(clusterId);
if (cluster) {
// we need to swap path prefix so that request is proxied to kube api
- req.url = req.url.replace(`/${clusterId}`, apiKubePrefix)
+ req.url = req.url.replace(`/${clusterId}`, apiKubePrefix);
}
} else if (req.headers["x-cluster-id"]) {
- cluster = clusterStore.getById(req.headers["x-cluster-id"].toString())
+ cluster = clusterStore.getById(req.headers["x-cluster-id"].toString());
} else {
const clusterId = getClusterIdFromHost(req.headers.host);
- cluster = clusterStore.getById(clusterId)
+ cluster = clusterStore.getById(clusterId);
}
return cluster;
diff --git a/src/main/cluster.ts b/src/main/cluster.ts
index 600b377729..e2831e8c3f 100644
--- a/src/main/cluster.ts
+++ b/src/main/cluster.ts
@@ -1,18 +1,18 @@
-import { ipcMain } from "electron"
-import type { ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences } from "../common/cluster-store"
+import { ipcMain } from "electron";
+import type { ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences } from "../common/cluster-store";
import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api";
import type { WorkspaceId } from "../common/workspace-store";
import { action, computed, observable, reaction, toJS, when } from "mobx";
import { apiKubePrefix } from "../common/vars";
-import { broadcastIpc } from "../common/ipc";
-import { ContextHandler } from "./context-handler"
-import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"
+import { broadcastMessage } 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 { getNodeWarningConditions, loadConfig, podHasIssues } from "../common/kube-helpers"
-import request, { RequestPromiseOptions } from "request-promise-native"
+import { KubeconfigManager } from "./kubeconfig-manager";
+import { getNodeWarningConditions, loadConfig, podHasIssues } from "../common/kube-helpers";
+import request, { RequestPromiseOptions } from "request-promise-native";
import { apiResources } from "../common/rbac";
-import logger from "./logger"
+import logger from "./logger";
import { VersionDetector } from "./cluster-detectors/version-detector";
import { detectorRegistry } from "./cluster-detectors/detector-registry";
@@ -32,7 +32,7 @@ export enum ClusterMetadataKey {
export type ClusterRefreshOptions = {
refreshMetadata?: boolean
-}
+};
export interface ClusterState {
initialized: boolean;
@@ -50,8 +50,7 @@ export interface ClusterState {
export class Cluster implements ClusterModel, ClusterState {
public id: ClusterId;
- public frameId: number;
- public kubeCtl: Kubectl
+ public kubeCtl: Kubectl;
public contextHandler: ContextHandler;
public ownerRef: string;
protected kubeconfigManager: KubeconfigManager;
@@ -86,20 +85,24 @@ export class Cluster implements ClusterModel, ClusterState {
return this.accessible && !this.disconnected;
}
+ @computed get name() {
+ return this.preferences.clusterName || this.contextName;
+ }
+
get version(): string {
- return String(this.metadata?.version) || ""
+ return String(this.metadata?.version) || "";
}
constructor(model: ClusterModel) {
this.updateModel(model);
- const kubeconfig = this.getKubeconfig()
+ const kubeconfig = this.getKubeconfig();
if (kubeconfig.getContextObject(this.contextName)) {
- this.apiUrl = kubeconfig.getCluster(kubeconfig.getContextObject(this.contextName).cluster).server
+ this.apiUrl = kubeconfig.getCluster(kubeconfig.getContextObject(this.contextName).cluster).server;
}
}
get isManaged(): boolean {
- return !!this.ownerRef
+ return !!this.ownerRef;
}
@action
@@ -128,16 +131,16 @@ export class Cluster implements ClusterModel, ClusterState {
}
protected bindEvents() {
- logger.info(`[CLUSTER]: bind events`, this.getMeta())
- const refreshTimer = setInterval(() => !this.disconnected && this.refresh(), 30000) // every 30s
- const refreshMetadataTimer = setInterval(() => !this.disconnected && this.refreshMetadata(), 900000) // every 15 minutes
+ logger.info(`[CLUSTER]: bind events`, this.getMeta());
+ const refreshTimer = setInterval(() => !this.disconnected && this.refresh(), 30000); // every 30s
+ const refreshMetadataTimer = setInterval(() => !this.disconnected && this.refreshMetadata(), 900000); // every 15 minutes
if (ipcMain) {
this.eventDisposers.push(
reaction(() => this.getState(), () => this.pushState()),
() => {
- clearInterval(refreshTimer)
- clearInterval(refreshMetadataTimer)
+ clearInterval(refreshTimer);
+ clearInterval(refreshMetadataTimer);
},
);
}
@@ -162,23 +165,27 @@ export class Cluster implements ClusterModel, ClusterState {
if (this.disconnected || !this.accessible) {
await this.reconnect();
}
- await this.refreshConnectionStatus()
+ await this.refreshConnectionStatus();
if (this.accessible) {
- await this.refreshAllowedResources()
- this.isAdmin = await this.isClusterAdmin()
- this.ready = true
- this.kubeCtl = new Kubectl(this.version)
- this.kubeCtl.ensureKubectl() // download kubectl in background, so it's not blocking dashboard
+ await this.refreshAllowedResources();
+ this.isAdmin = await this.isClusterAdmin();
+ this.ready = true;
+ this.ensureKubectl();
}
- this.activated = true
+ this.activated = true;
return this.pushState();
}
+ protected async ensureKubectl() {
+ this.kubeCtl = new Kubectl(this.version);
+ return this.kubeCtl.ensureKubectl(); // download kubectl in background, so it's not blocking dashboard
+ }
+
@action
async reconnect() {
logger.info(`[CLUSTER]: reconnect`, this.getMeta());
- this.contextHandler.stopServer();
- await this.contextHandler.ensureServer();
+ this.contextHandler?.stopServer();
+ await this.contextHandler?.ensureServer();
this.disconnected = false;
}
@@ -186,7 +193,7 @@ export class Cluster implements ClusterModel, ClusterState {
disconnect() {
logger.info(`[CLUSTER]: disconnect`, this.getMeta());
this.unbindEvents();
- this.contextHandler.stopServer();
+ this.contextHandler?.stopServer();
this.disconnected = true;
this.online = false;
this.accessible = false;
@@ -207,9 +214,9 @@ export class Cluster implements ClusterModel, ClusterState {
this.refreshAllowedResources(),
]);
if (opts.refreshMetadata) {
- this.refreshMetadata()
+ this.refreshMetadata();
}
- this.ready = true
+ this.ready = true;
}
this.pushState();
}
@@ -217,9 +224,9 @@ export class Cluster implements ClusterModel, ClusterState {
@action
async refreshMetadata() {
logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta());
- const metadata = await detectorRegistry.detectForCluster(this)
- const existingMetadata = this.metadata
- this.metadata = Object.assign(existingMetadata, metadata)
+ const metadata = await detectorRegistry.detectForCluster(this);
+ const existingMetadata = this.metadata;
+ this.metadata = Object.assign(existingMetadata, metadata);
}
@action
@@ -249,16 +256,16 @@ export class Cluster implements ClusterModel, ClusterState {
}
getProxyKubeconfigPath(): string {
- return this.kubeconfigManager.getPath()
+ return this.kubeconfigManager.getPath();
}
protected async k8sRequest(path: string, options: RequestPromiseOptions = {}): Promise {
- options.headers ??= {}
- options.json ??= true
- options.timeout ??= 30000
- options.headers.Host = `${this.id}.${new URL(this.kubeProxyUrl).host}` // required in ClusterManager.getClusterForRequest()
+ options.headers ??= {};
+ options.json ??= true;
+ options.timeout ??= 30000;
+ options.headers.Host = `${this.id}.${new URL(this.kubeProxyUrl).host}`; // required in ClusterManager.getClusterForRequest()
- return request(this.kubeProxyUrl + path, options)
+ return request(this.kubeProxyUrl + path, options);
}
getMetrics(prometheusPath: string, queryParams: IMetricsReqParams & { query: string }) {
@@ -269,17 +276,17 @@ export class Cluster implements ClusterModel, ClusterState {
resolveWithFullResponse: false,
json: true,
qs: queryParams,
- })
+ });
}
protected async getConnectionStatus(): Promise {
try {
- const versionDetector = new VersionDetector(this)
- const versionData = await versionDetector.detect()
- this.metadata.version = versionData.value
+ const versionDetector = new VersionDetector(this);
+ const versionData = await versionDetector.detect();
+ this.metadata.version = versionData.value;
return ClusterStatus.AccessGranted;
} catch (error) {
- logger.error(`Failed to connect cluster "${this.contextName}": ${error}`)
+ logger.error(`Failed to connect cluster "${this.contextName}": ${error}`);
if (error.statusCode) {
if (error.statusCode >= 400 && error.statusCode < 500) {
this.failureReason = "Invalid credentials";
@@ -303,17 +310,17 @@ export class Cluster implements ClusterModel, ClusterState {
}
async canI(resourceAttributes: V1ResourceAttributes): Promise {
- const authApi = this.getProxyKubeconfig().makeApiClient(AuthorizationV1Api)
+ const authApi = this.getProxyKubeconfig().makeApiClient(AuthorizationV1Api);
try {
const accessReview = await authApi.createSelfSubjectAccessReview({
apiVersion: "authorization.k8s.io/v1",
kind: "SelfSubjectAccessReview",
spec: { resourceAttributes }
- })
- return accessReview.body.status.allowed
+ });
+ return accessReview.body.status.allowed;
} catch (error) {
- logger.error(`failed to request selfSubjectAccessReview: ${error}`)
- return false
+ logger.error(`failed to request selfSubjectAccessReview: ${error}`);
+ return false;
}
}
@@ -322,7 +329,7 @@ export class Cluster implements ClusterModel, ClusterState {
namespace: "kube-system",
resource: "*",
verb: "create",
- })
+ });
}
protected async getEventCount(): Promise {
@@ -338,7 +345,7 @@ export class Cluster implements ClusterModel, ClusterState {
if (w.involvedObject.kind === 'Pod') {
try {
const { body: pod } = await client.readNamespacedPod(w.involvedObject.name, w.involvedObject.namespace);
- logger.debug(`checking pod ${w.involvedObject.namespace}/${w.involvedObject.name}`)
+ logger.debug(`checking pod ${w.involvedObject.namespace}/${w.involvedObject.name}`);
if (podHasIssues(pod)) {
uniqEventSources.add(w.involvedObject.uid);
}
@@ -354,7 +361,7 @@ export class Cluster implements ClusterModel, ClusterState {
.reduce((sum, conditions) => sum + conditions.length, 0);
return uniqEventSources.size + nodeNotificationCount;
} catch (error) {
- logger.error("Failed to fetch event count: " + JSON.stringify(error))
+ logger.error("Failed to fetch event count: " + JSON.stringify(error));
return 0;
}
}
@@ -372,7 +379,7 @@ export class Cluster implements ClusterModel, ClusterState {
};
return toJS(model, {
recurseEverything: true
- })
+ });
}
// serializable cluster-state used for sync btw main <-> renderer
@@ -392,21 +399,17 @@ export class Cluster implements ClusterModel, ClusterState {
};
return toJS(state, {
recurseEverything: true
- })
+ });
}
@action
setState(state: ClusterState) {
- Object.assign(this, state)
+ Object.assign(this, state);
}
pushState(state = this.getState()) {
logger.silly(`[CLUSTER]: push-state`, state);
- broadcastIpc({
- channel: "cluster:state",
- frameId: this.frameId,
- args: [this.id, state],
- })
+ broadcastMessage("cluster:state", this.id, state);
}
// get cluster system meta, e.g. use in "logger"
@@ -419,30 +422,30 @@ export class Cluster implements ClusterModel, ClusterState {
online: this.online,
accessible: this.accessible,
disconnected: this.disconnected,
- }
+ };
}
protected async getAllowedNamespaces() {
if (this.accessibleNamespaces.length) {
- return this.accessibleNamespaces
+ return this.accessibleNamespaces;
}
- const api = this.getProxyKubeconfig().makeApiClient(CoreV1Api)
+ const api = this.getProxyKubeconfig().makeApiClient(CoreV1Api);
try {
- const namespaceList = await api.listNamespace()
+ const namespaceList = await api.listNamespace();
const nsAccessStatuses = await Promise.all(
namespaceList.body.items.map(ns => this.canI({
namespace: ns.metadata.name,
resource: "pods",
verb: "list",
}))
- )
+ );
return namespaceList.body.items
.filter((ns, i) => nsAccessStatuses[i])
- .map(ns => ns.metadata.name)
+ .map(ns => ns.metadata.name);
} catch (error) {
- const ctx = this.getProxyKubeconfig().getContextObject(this.contextName)
- if (ctx.namespace) return [ctx.namespace]
+ const ctx = this.getProxyKubeconfig().getContextObject(this.contextName);
+ if (ctx.namespace) return [ctx.namespace];
return [];
}
}
@@ -459,12 +462,12 @@ export class Cluster implements ClusterModel, ClusterState {
verb: "list",
namespace: this.allowedNamespaces[0]
}))
- )
+ );
return apiResources
.filter((resource, i) => resourceAccessStatuses[i])
- .map(apiResource => apiResource.resource)
+ .map(apiResource => apiResource.resource);
} catch (error) {
- return []
+ return [];
}
}
}
diff --git a/src/main/context-handler.ts b/src/main/context-handler.ts
index a3cf6185dd..a1ef58ad0f 100644
--- a/src/main/context-handler.ts
+++ b/src/main/context-handler.ts
@@ -1,21 +1,21 @@
-import type { PrometheusProvider, PrometheusService } from "./prometheus/provider-registry"
+import type { PrometheusProvider, PrometheusService } from "./prometheus/provider-registry";
import type { ClusterPreferences } from "../common/cluster-store";
-import type { Cluster } from "./cluster"
-import type httpProxy from "http-proxy"
+import type { Cluster } from "./cluster";
+import type httpProxy from "http-proxy";
import url, { UrlWithStringQuery } from "url";
-import { CoreV1Api } from "@kubernetes/client-node"
-import { prometheusProviders } from "../common/prometheus-providers"
-import logger from "./logger"
-import { getFreePort } from "./port"
-import { KubeAuthProxy } from "./kube-auth-proxy"
+import { CoreV1Api } from "@kubernetes/client-node";
+import { prometheusProviders } from "../common/prometheus-providers";
+import logger from "./logger";
+import { getFreePort } from "./port";
+import { KubeAuthProxy } from "./kube-auth-proxy";
export class ContextHandler {
public proxyPort: number;
public clusterUrl: UrlWithStringQuery;
- protected kubeAuthProxy: KubeAuthProxy
- protected apiTarget: httpProxy.ServerOptions
- protected prometheusProvider: string
- protected prometheusPath: string
+ protected kubeAuthProxy: KubeAuthProxy;
+ protected apiTarget: httpProxy.ServerOptions;
+ protected prometheusProvider: string;
+ protected prometheusPath: string;
constructor(protected cluster: Cluster) {
this.clusterUrl = url.parse(cluster.apiUrl);
@@ -26,64 +26,64 @@ export class ContextHandler {
this.prometheusProvider = preferences.prometheusProvider?.type;
this.prometheusPath = null;
if (preferences.prometheus) {
- const { namespace, service, port } = preferences.prometheus
- this.prometheusPath = `${namespace}/services/${service}:${port}`
+ const { namespace, service, port } = preferences.prometheus;
+ this.prometheusPath = `${namespace}/services/${service}:${port}`;
}
}
protected async resolvePrometheusPath(): Promise {
- const { service, namespace, port } = await this.getPrometheusService()
- return `${namespace}/services/${service}:${port}`
+ const { service, namespace, port } = await this.getPrometheusService();
+ return `${namespace}/services/${service}:${port}`;
}
async getPrometheusProvider() {
if (!this.prometheusProvider) {
- const service = await this.getPrometheusService()
- logger.info(`using ${service.id} as prometheus provider`)
- this.prometheusProvider = service.id
+ const service = await this.getPrometheusService();
+ logger.info(`using ${service.id} as prometheus provider`);
+ this.prometheusProvider = service.id;
}
- return prometheusProviders.find(p => p.id === this.prometheusProvider)
+ return prometheusProviders.find(p => p.id === this.prometheusProvider);
}
async getPrometheusService(): Promise {
const providers = this.prometheusProvider ? prometheusProviders.filter(provider => provider.id == this.prometheusProvider) : prometheusProviders;
const prometheusPromises: Promise[] = providers.map(async (provider: PrometheusProvider): Promise => {
- const apiClient = this.cluster.getProxyKubeconfig().makeApiClient(CoreV1Api)
- return await provider.getPrometheusService(apiClient)
- })
- const resolvedPrometheusServices = await Promise.all(prometheusPromises)
+ const apiClient = this.cluster.getProxyKubeconfig().makeApiClient(CoreV1Api);
+ return await provider.getPrometheusService(apiClient);
+ });
+ const resolvedPrometheusServices = await Promise.all(prometheusPromises);
const service = resolvedPrometheusServices.filter(n => n)[0];
return service || {
id: "lens",
namespace: "lens-metrics",
service: "prometheus",
port: 80
- }
+ };
}
async getPrometheusPath(): Promise {
if (!this.prometheusPath) {
- this.prometheusPath = await this.resolvePrometheusPath()
+ this.prometheusPath = await this.resolvePrometheusPath();
}
return this.prometheusPath;
}
async resolveAuthProxyUrl() {
const proxyPort = await this.ensurePort();
- const path = this.clusterUrl.path !== "/" ? this.clusterUrl.path : ""
+ const path = this.clusterUrl.path !== "/" ? this.clusterUrl.path : "";
return `http://127.0.0.1:${proxyPort}${path}`;
}
async getApiTarget(isWatchRequest = false): Promise {
if (this.apiTarget && !isWatchRequest) {
- return this.apiTarget
+ return this.apiTarget;
}
- const timeout = isWatchRequest ? 4 * 60 * 60 * 1000 : 30000 // 4 hours for watch request, 30 seconds for the rest
- const apiTarget = await this.newApiTarget(timeout)
+ const timeout = isWatchRequest ? 4 * 60 * 60 * 1000 : 30000; // 4 hours for watch request, 30 seconds for the rest
+ const apiTarget = await this.newApiTarget(timeout);
if (!isWatchRequest) {
- this.apiTarget = apiTarget
+ this.apiTarget = apiTarget;
}
- return apiTarget
+ return apiTarget;
}
protected async newApiTarget(timeout: number): Promise {
@@ -91,40 +91,40 @@ export class ContextHandler {
return {
target: proxyUrl,
changeOrigin: true,
- timeout: timeout,
+ timeout,
headers: {
"Host": this.clusterUrl.hostname,
},
- }
+ };
}
async ensurePort(): Promise {
if (!this.proxyPort) {
this.proxyPort = await getFreePort();
}
- return this.proxyPort
+ return this.proxyPort;
}
async ensureServer() {
if (!this.kubeAuthProxy) {
await this.ensurePort();
- const proxyEnv = Object.assign({}, process.env)
+ const proxyEnv = Object.assign({}, process.env);
if (this.cluster.preferences.httpsProxy) {
- proxyEnv.HTTPS_PROXY = this.cluster.preferences.httpsProxy
+ proxyEnv.HTTPS_PROXY = this.cluster.preferences.httpsProxy;
}
- this.kubeAuthProxy = new KubeAuthProxy(this.cluster, this.proxyPort, proxyEnv)
- await this.kubeAuthProxy.run()
+ this.kubeAuthProxy = new KubeAuthProxy(this.cluster, this.proxyPort, proxyEnv);
+ await this.kubeAuthProxy.run();
}
}
stopServer() {
if (this.kubeAuthProxy) {
- this.kubeAuthProxy.exit()
- this.kubeAuthProxy = null
+ this.kubeAuthProxy.exit();
+ this.kubeAuthProxy = null;
}
}
get proxyLastError(): string {
- return this.kubeAuthProxy?.lastError || ""
+ return this.kubeAuthProxy?.lastError || "";
}
}
diff --git a/src/main/developer-tools.ts b/src/main/developer-tools.ts
new file mode 100644
index 0000000000..274ff46e91
--- /dev/null
+++ b/src/main/developer-tools.ts
@@ -0,0 +1,11 @@
+/**
+ * Installs Electron developer tools in the development build.
+ * The dependency is not bundled to the production build.
+ */
+export const installDeveloperTools = async () => {
+ if (process.env.NODE_ENV === 'development') {
+ const { default: devToolsInstaller, REACT_DEVELOPER_TOOLS } = await import('electron-devtools-installer');
+
+ return devToolsInstaller([REACT_DEVELOPER_TOOLS]);
+ }
+};
diff --git a/src/main/exit-app.ts b/src/main/exit-app.ts
new file mode 100644
index 0000000000..b58a6e4dfc
--- /dev/null
+++ b/src/main/exit-app.ts
@@ -0,0 +1,18 @@
+import { app } from "electron";
+import { WindowManager } from "./window-manager";
+import { appEventBus } from "../common/event-bus";
+import { ClusterManager } from "./cluster-manager";
+import logger from "./logger";
+
+
+export function exitApp() {
+ const windowManager = WindowManager.getInstance();
+ const clusterManager = ClusterManager.getInstance();
+ appEventBus.emit({ name: "service", action: "close" });
+ windowManager.hide();
+ clusterManager.stop();
+ logger.info('SERVICE:QUIT');
+ setTimeout(() => {
+ app.exit();
+ }, 1000);
+}
\ No newline at end of file
diff --git a/src/main/extension-filesystem.ts b/src/main/extension-filesystem.ts
new file mode 100644
index 0000000000..fb3a4060be
--- /dev/null
+++ b/src/main/extension-filesystem.ts
@@ -0,0 +1,57 @@
+import { randomBytes } from "crypto";
+import { SHA256 } from "crypto-js";
+import { app } from "electron";
+import fse from "fs-extra";
+import { action, observable, toJS } from "mobx";
+import path from "path";
+import { BaseStore } from "../common/base-store";
+import { LensExtensionId } from "../extensions/lens-extension";
+
+interface FSProvisionModel {
+ extensions: Record; // extension names to paths
+}
+
+export class FilesystemProvisionerStore extends BaseStore {
+ @observable registeredExtensions = observable.map();
+
+ private constructor() {
+ super({
+ configName: "lens-filesystem-provisioner-store",
+ accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
+ });
+ }
+
+ /**
+ * This function retrieves the saved path to the folder which the extension
+ * can saves files to. If the folder is not present then it is created.
+ * @param extensionName the name of the extension requesting the path
+ * @returns path to the folder that the extension can safely write files to.
+ */
+ async requestDirectory(extensionName: string): Promise {
+ if (!this.registeredExtensions.has(extensionName)) {
+ const salt = randomBytes(32).toString("hex");
+ const hashedName = SHA256(`${extensionName}/${salt}`).toString();
+ const dirPath = path.resolve(app.getPath("userData"), "extension_data", hashedName);
+ this.registeredExtensions.set(extensionName, dirPath);
+ }
+
+ const dirPath = this.registeredExtensions.get(extensionName);
+ await fse.ensureDir(dirPath);
+ return dirPath;
+ }
+
+ @action
+ protected fromStore({ extensions }: FSProvisionModel = { extensions: {} }): void {
+ this.registeredExtensions.merge(extensions);
+ }
+
+ toJSON(): FSProvisionModel {
+ return toJS({
+ extensions: this.registeredExtensions.toJSON(),
+ }, {
+ recurseEverything: true
+ });
+ }
+}
+
+export const filesystemProvisionerStore = FilesystemProvisionerStore.getInstance();
diff --git a/src/main/helm/helm-chart-manager.ts b/src/main/helm/helm-chart-manager.ts
index e42f7a1aaf..42c1a30ed2 100644
--- a/src/main/helm/helm-chart-manager.ts
+++ b/src/main/helm/helm-chart-manager.ts
@@ -1,74 +1,74 @@
import fs from "fs";
import * as yaml from "js-yaml";
-import { HelmRepo, HelmRepoManager } from "./helm-repo-manager"
+import { HelmRepo, HelmRepoManager } from "./helm-repo-manager";
import logger from "../logger";
-import { promiseExec } from "../promise-exec"
-import { helmCli } from "./helm-cli"
+import { promiseExec } from "../promise-exec";
+import { helmCli } from "./helm-cli";
type CachedYaml = {
entries: any; // todo: types
-}
+};
export class HelmChartManager {
- protected cache: any = {}
- protected repo: HelmRepo
+ protected cache: any = {};
+ protected repo: HelmRepo;
constructor(repo: HelmRepo){
- this.cache = HelmRepoManager.cache
- this.repo = repo
+ this.cache = HelmRepoManager.cache;
+ this.repo = repo;
}
public async chart(name: string) {
- const charts = await this.charts()
- return charts[name]
+ const charts = await this.charts();
+ return charts[name];
}
public async charts(): Promise {
try {
- const cachedYaml = await this.cachedYaml()
- return cachedYaml["entries"]
+ const cachedYaml = await this.cachedYaml();
+ return cachedYaml["entries"];
} catch(error) {
- logger.error(error)
- return []
+ logger.error(error);
+ return [];
}
}
public async getReadme(name: string, version = "") {
- const helm = await helmCli.binaryPath()
+ const helm = await helmCli.binaryPath();
if(version && version != "") {
- const { stdout, stderr} = await promiseExec(`"${helm}" show readme ${this.repo.name}/${name} --version ${version}`).catch((error) => { throw(error.stderr)})
- return stdout
+ const { stdout, stderr} = await promiseExec(`"${helm}" show readme ${this.repo.name}/${name} --version ${version}`).catch((error) => { throw(error.stderr);});
+ return stdout;
} else {
- const { stdout, stderr} = await promiseExec(`"${helm}" show readme ${this.repo.name}/${name}`).catch((error) => { throw(error.stderr)})
- return stdout
+ const { stdout, stderr} = await promiseExec(`"${helm}" show readme ${this.repo.name}/${name}`).catch((error) => { throw(error.stderr);});
+ return stdout;
}
}
public async getValues(name: string, version = "") {
- const helm = await helmCli.binaryPath()
+ const helm = await helmCli.binaryPath();
if(version && version != "") {
- const { stdout, stderr} = await promiseExec(`"${helm}" show values ${this.repo.name}/${name} --version ${version}`).catch((error) => { throw(error.stderr)})
+ const { stdout, stderr} = await promiseExec(`"${helm}" show values ${this.repo.name}/${name} --version ${version}`).catch((error) => { throw(error.stderr);});
- return stdout
+ return stdout;
} else {
- const { stdout, stderr} = await promiseExec(`"${helm}" show values ${this.repo.name}/${name}`).catch((error) => { throw(error.stderr)})
+ const { stdout, stderr} = await promiseExec(`"${helm}" show values ${this.repo.name}/${name}`).catch((error) => { throw(error.stderr);});
- return stdout
+ return stdout;
}
}
protected async cachedYaml(): Promise {
if (!(this.repo.name in this.cache)) {
- const cacheFile = await fs.promises.readFile(this.repo.cacheFilePath, 'utf-8')
- const data = yaml.safeLoad(cacheFile)
+ const cacheFile = await fs.promises.readFile(this.repo.cacheFilePath, 'utf-8');
+ const data = yaml.safeLoad(cacheFile);
for(const key in data["entries"]) {
data["entries"][key].forEach((version: any) => {
- version['repo'] = this.repo.name
- version['created'] = Date.parse(version.created).toString()
- })
+ version['repo'] = this.repo.name;
+ version['created'] = Date.parse(version.created).toString();
+ });
}
- this.cache[this.repo.name] = Buffer.from(JSON.stringify(data))
+ this.cache[this.repo.name] = Buffer.from(JSON.stringify(data));
}
- return JSON.parse(this.cache[this.repo.name].toString())
+ return JSON.parse(this.cache[this.repo.name].toString());
}
}
diff --git a/src/main/helm/helm-cli.ts b/src/main/helm/helm-cli.ts
index 1484ceacf1..c06386a1a8 100644
--- a/src/main/helm/helm-cli.ts
+++ b/src/main/helm/helm-cli.ts
@@ -1,6 +1,6 @@
-import packageInfo from "../../../package.json"
-import path from "path"
-import { LensBinary, LensBinaryOpts } from "../lens-binary"
+import packageInfo from "../../../package.json";
+import path from "path";
+import { LensBinary, LensBinaryOpts } from "../lens-binary";
import { isProduction } from "../../common/vars";
export class HelmCli extends LensBinary {
@@ -8,27 +8,27 @@ export class HelmCli extends LensBinary {
public constructor(baseDir: string, version: string) {
const opts: LensBinaryOpts = {
version,
- baseDir: baseDir,
+ baseDir,
originalBinaryName: "helm",
newBinaryName: "helm3"
- }
- super(opts)
+ };
+ super(opts);
}
protected getTarName(): string | null {
- return `${this.binaryName}-v${this.binaryVersion}-${this.platformName}-${this.arch}.tar.gz`
+ return `${this.binaryName}-v${this.binaryVersion}-${this.platformName}-${this.arch}.tar.gz`;
}
protected getUrl() {
- return `https://get.helm.sh/helm-v${this.binaryVersion}-${this.platformName}-${this.arch}.tar.gz`
+ return `https://get.helm.sh/helm-v${this.binaryVersion}-${this.platformName}-${this.arch}.tar.gz`;
}
protected getBinaryPath() {
- return path.join(this.dirname, this.binaryName)
+ return path.join(this.dirname, this.binaryName);
}
protected getOriginalBinaryPath() {
- return path.join(this.dirname, this.platformName + "-" + this.arch, this.originalBinaryName)
+ return path.join(this.dirname, this.platformName + "-" + this.arch, this.originalBinaryName);
}
}
diff --git a/src/main/helm/helm-release-manager.ts b/src/main/helm/helm-release-manager.ts
index 80be023227..e468cdaef7 100644
--- a/src/main/helm/helm-release-manager.ts
+++ b/src/main/helm/helm-release-manager.ts
@@ -1,7 +1,7 @@
import * as tempy from "tempy";
import fs from "fs";
import * as yaml from "js-yaml";
-import { promiseExec} from "../promise-exec"
+import { promiseExec} from "../promise-exec";
import { helmCli } from "./helm-cli";
import { Cluster } from "../cluster";
import { toCamelCase } from "../../common/utils/camelCase";
@@ -9,103 +9,103 @@ import { toCamelCase } from "../../common/utils/camelCase";
export class HelmReleaseManager {
public async listReleases(pathToKubeconfig: string, namespace?: string) {
- const helm = await helmCli.binaryPath()
- const namespaceFlag = namespace ? `-n ${namespace}` : "--all-namespaces"
- const { stdout } = await promiseExec(`"${helm}" ls --output json ${namespaceFlag} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr)})
+ const helm = await helmCli.binaryPath();
+ const namespaceFlag = namespace ? `-n ${namespace}` : "--all-namespaces";
+ const { stdout } = await promiseExec(`"${helm}" ls --output json ${namespaceFlag} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);});
- const output = JSON.parse(stdout)
+ const output = JSON.parse(stdout);
if (output.length == 0) {
- return output
+ return output;
}
output.forEach((release: any, index: number) => {
- output[index] = toCamelCase(release)
+ output[index] = toCamelCase(release);
});
- return output
+ return output;
}
public async installChart(chart: string, values: any, name: string, namespace: string, version: string, pathToKubeconfig: string){
- const helm = await helmCli.binaryPath()
- const fileName = tempy.file({name: "values.yaml"})
- await fs.promises.writeFile(fileName, yaml.safeDump(values))
+ const helm = await helmCli.binaryPath();
+ const fileName = tempy.file({name: "values.yaml"});
+ await fs.promises.writeFile(fileName, yaml.safeDump(values));
try {
- let generateName = ""
+ let generateName = "";
if (!name) {
- generateName = "--generate-name"
- name = ""
+ generateName = "--generate-name";
+ name = "";
}
- const { stdout, stderr } = await promiseExec(`"${helm}" install ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} ${generateName}`).catch((error) => { throw(error.stderr)})
- const releaseName = stdout.split("\n")[0].split(' ')[1].trim()
+ const { stdout, stderr } = await promiseExec(`"${helm}" install ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} ${generateName}`).catch((error) => { throw(error.stderr);});
+ const releaseName = stdout.split("\n")[0].split(' ')[1].trim();
return {
log: stdout,
release: {
name: releaseName,
- namespace: namespace
+ namespace
}
- }
+ };
} finally {
- await fs.promises.unlink(fileName)
+ await fs.promises.unlink(fileName);
}
}
public async upgradeRelease(name: string, chart: string, values: any, namespace: string, version: string, cluster: Cluster){
- const helm = await helmCli.binaryPath()
- const fileName = tempy.file({name: "values.yaml"})
- await fs.promises.writeFile(fileName, yaml.safeDump(values))
+ const helm = await helmCli.binaryPath();
+ const fileName = tempy.file({name: "values.yaml"});
+ await fs.promises.writeFile(fileName, yaml.safeDump(values));
try {
- const { stdout, stderr } = await promiseExec(`"${helm}" upgrade ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${cluster.getProxyKubeconfigPath()}`).catch((error) => { throw(error.stderr)})
+ const { stdout, stderr } = await promiseExec(`"${helm}" upgrade ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${cluster.getProxyKubeconfigPath()}`).catch((error) => { throw(error.stderr);});
return {
log: stdout,
release: this.getRelease(name, namespace, cluster)
- }
+ };
} finally {
- await fs.promises.unlink(fileName)
+ await fs.promises.unlink(fileName);
}
}
public async getRelease(name: string, namespace: string, cluster: Cluster) {
- const helm = await helmCli.binaryPath()
- const {stdout, stderr} = await promiseExec(`"${helm}" status ${name} --output json --namespace ${namespace} --kubeconfig ${cluster.getProxyKubeconfigPath()}`).catch((error) => { throw(error.stderr)})
- const release = JSON.parse(stdout)
- release.resources = await this.getResources(name, namespace, cluster)
- return release
+ const helm = await helmCli.binaryPath();
+ const {stdout, stderr} = await promiseExec(`"${helm}" status ${name} --output json --namespace ${namespace} --kubeconfig ${cluster.getProxyKubeconfigPath()}`).catch((error) => { throw(error.stderr);});
+ const release = JSON.parse(stdout);
+ release.resources = await this.getResources(name, namespace, cluster);
+ return release;
}
public async deleteRelease(name: string, namespace: string, pathToKubeconfig: string) {
- const helm = await helmCli.binaryPath()
- const { stdout, stderr } = await promiseExec(`"${helm}" delete ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr)})
+ const helm = await helmCli.binaryPath();
+ const { stdout, stderr } = await promiseExec(`"${helm}" delete ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);});
- return stdout
+ return stdout;
}
public async getValues(name: string, namespace: string, pathToKubeconfig: string) {
- const helm = await helmCli.binaryPath()
- const { stdout, stderr } = await promiseExec(`"${helm}" get values ${name} --all --output yaml --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr)})
- return stdout
+ const helm = await helmCli.binaryPath();
+ const { stdout, stderr } = await promiseExec(`"${helm}" get values ${name} --all --output yaml --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);});
+ return stdout;
}
public async getHistory(name: string, namespace: string, pathToKubeconfig: string) {
- const helm = await helmCli.binaryPath()
- const {stdout, stderr} = await promiseExec(`"${helm}" history ${name} --output json --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr)})
- return JSON.parse(stdout)
+ const helm = await helmCli.binaryPath();
+ const {stdout, stderr} = await promiseExec(`"${helm}" history ${name} --output json --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);});
+ return JSON.parse(stdout);
}
public async rollback(name: string, namespace: string, revision: number, pathToKubeconfig: string) {
- const helm = await helmCli.binaryPath()
- const {stdout, stderr} = await promiseExec(`"${helm}" rollback ${name} ${revision} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr)})
- return stdout
+ const helm = await helmCli.binaryPath();
+ const {stdout, stderr} = await promiseExec(`"${helm}" rollback ${name} ${revision} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);});
+ return stdout;
}
protected async getResources(name: string, namespace: string, cluster: Cluster) {
- const helm = await helmCli.binaryPath()
- const kubectl = await cluster.kubeCtl.getPath()
- const pathToKubeconfig = cluster.getProxyKubeconfigPath()
+ const helm = await helmCli.binaryPath();
+ const kubectl = await cluster.kubeCtl.getPath();
+ const pathToKubeconfig = cluster.getProxyKubeconfigPath();
const { stdout } = await promiseExec(`"${helm}" get manifest ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} | "${kubectl}" get -n ${namespace} --kubeconfig ${pathToKubeconfig} -f - -o=json`).catch((error) => {
- return { stdout: JSON.stringify({items: []})}
- })
- return stdout
+ return { stdout: JSON.stringify({items: []})};
+ });
+ return stdout;
}
}
-export const releaseManager = new HelmReleaseManager()
+export const releaseManager = new HelmReleaseManager();
diff --git a/src/main/helm/helm-repo-manager.ts b/src/main/helm/helm-repo-manager.ts
index c2af9ea7ba..30ce8e3435 100644
--- a/src/main/helm/helm-repo-manager.ts
+++ b/src/main/helm/helm-repo-manager.ts
@@ -10,7 +10,7 @@ import logger from "../logger";
export type HelmEnv = Record & {
HELM_REPOSITORY_CACHE?: string;
HELM_REPOSITORY_CONFIG?: string;
-}
+};
export interface HelmRepoConfig {
repositories: HelmRepo[]
@@ -29,11 +29,11 @@ export interface HelmRepo {
}
export class HelmRepoManager extends Singleton {
- static cache = {} // todo: remove implicit updates in helm-chart-manager.ts
+ static cache = {}; // todo: remove implicit updates in helm-chart-manager.ts
protected repos: HelmRepo[];
- protected helmEnv: HelmEnv
- protected initialized: boolean
+ protected helmEnv: HelmEnv;
+ protected initialized: boolean;
async loadAvailableRepos(): Promise {
const res = await customRequestPromise({
@@ -46,34 +46,34 @@ export class HelmRepoManager extends Singleton {
}
async init() {
- helmCli.setLogger(logger)
+ helmCli.setLogger(logger);
await helmCli.ensureBinary();
if (!this.initialized) {
- this.helmEnv = await this.parseHelmEnv()
- await this.update()
- this.initialized = true
+ this.helmEnv = await this.parseHelmEnv();
+ await this.update();
+ this.initialized = true;
}
}
protected async parseHelmEnv() {
- const helm = await helmCli.binaryPath()
+ const helm = await helmCli.binaryPath();
const { stdout } = await promiseExec(`"${helm}" env`).catch((error) => {
- throw(error.stderr)
- })
- const lines = stdout.split(/\r?\n/) // split by new line feed
- const env: HelmEnv = {}
+ throw(error.stderr);
+ });
+ const lines = stdout.split(/\r?\n/); // split by new line feed
+ const env: HelmEnv = {};
lines.forEach((line: string) => {
- const [key, value] = line.split("=")
+ const [key, value] = line.split("=");
if (key && value) {
- env[key] = value.replace(/"/g, "") // strip quotas
+ env[key] = value.replace(/"/g, ""); // strip quotas
}
- })
- return env
+ });
+ return env;
}
public async repositories(): Promise {
if (!this.initialized) {
- await this.init()
+ await this.init();
}
try {
const repoConfigFile = this.helmEnv.HELM_REPOSITORY_CONFIG;
@@ -83,7 +83,7 @@ export class HelmRepoManager extends Singleton {
repositories: []
}));
if (!repositories.length) {
- await this.addRepo({ name: "stable", url: "https://kubernetes-charts.storage.googleapis.com/" });
+ await this.addRepo({ name: "bitnami", url: "https://charts.bitnami.com/bitnami" });
return await this.repositories();
}
return repositories.map(repo => ({
@@ -91,41 +91,41 @@ export class HelmRepoManager extends Singleton {
cacheFilePath: `${this.helmEnv.HELM_REPOSITORY_CACHE}/${repo.name}-index.yaml`
}));
} catch (error) {
- logger.error(`[HELM]: repositories listing error "${error}"`)
- return []
+ logger.error(`[HELM]: repositories listing error "${error}"`);
+ return [];
}
}
public async repository(name: string) {
- const repositories = await this.repositories()
+ const repositories = await this.repositories();
return repositories.find(repo => repo.name == name);
}
public async update() {
- const helm = await helmCli.binaryPath()
+ const helm = await helmCli.binaryPath();
const { stdout } = await promiseExec(`"${helm}" repo update`).catch((error) => {
- return { stdout: error.stdout }
- })
- return stdout
+ return { stdout: error.stdout };
+ });
+ return stdout;
}
public async addRepo({ name, url }: HelmRepo) {
logger.info(`[HELM]: adding repo "${name}" from ${url}`);
- const helm = await helmCli.binaryPath()
+ const helm = await helmCli.binaryPath();
const { stdout } = await promiseExec(`"${helm}" repo add ${name} ${url}`).catch((error) => {
- throw(error.stderr)
- })
- return stdout
+ 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()
+ const helm = await helmCli.binaryPath();
const { stdout, stderr } = await promiseExec(`"${helm}" repo remove ${name}`).catch((error) => {
- throw(error.stderr)
- })
- return stdout
+ throw(error.stderr);
+ });
+ return stdout;
}
}
-export const repoManager = HelmRepoManager.getInstance()
+export const repoManager = HelmRepoManager.getInstance();
diff --git a/src/main/helm/helm-service.ts b/src/main/helm/helm-service.ts
index 664a30358c..88ca4dda3e 100644
--- a/src/main/helm/helm-service.ts
+++ b/src/main/helm/helm-service.ts
@@ -6,93 +6,93 @@ import { releaseManager } from "./helm-release-manager";
class HelmService {
public async installChart(cluster: Cluster, data: { chart: string; values: {}; name: string; namespace: string; version: string }) {
- return await releaseManager.installChart(data.chart, data.values, data.name, data.namespace, data.version, cluster.getProxyKubeconfigPath())
+ return await releaseManager.installChart(data.chart, data.values, data.name, data.namespace, data.version, cluster.getProxyKubeconfigPath());
}
public async listCharts() {
- const charts: any = {}
- await repoManager.init()
- const repositories = await repoManager.repositories()
+ const charts: any = {};
+ await repoManager.init();
+ const repositories = await repoManager.repositories();
for (const repo of repositories) {
- charts[repo.name] = {}
- const manager = new HelmChartManager(repo)
- let entries = await manager.charts()
- entries = this.excludeDeprecated(entries)
+ charts[repo.name] = {};
+ const manager = new HelmChartManager(repo);
+ let entries = await manager.charts();
+ entries = this.excludeDeprecated(entries);
for (const key in entries) {
- entries[key] = entries[key][0]
+ entries[key] = entries[key][0];
}
- charts[repo.name] = entries
+ charts[repo.name] = entries;
}
- return charts
+ return charts;
}
public async getChart(repoName: string, chartName: string, version = "") {
const result = {
readme: "",
versions: {}
- }
- const repo = await repoManager.repository(repoName)
- const chartManager = new HelmChartManager(repo)
- const chart = await chartManager.chart(chartName)
- result.readme = await chartManager.getReadme(chartName, version)
- result.versions = chart
- return result
+ };
+ const repo = await repoManager.repository(repoName);
+ const chartManager = new HelmChartManager(repo);
+ const chart = await chartManager.chart(chartName);
+ result.readme = await chartManager.getReadme(chartName, version);
+ result.versions = chart;
+ return result;
}
public async getChartValues(repoName: string, chartName: string, version = "") {
- const repo = await repoManager.repository(repoName)
- const chartManager = new HelmChartManager(repo)
- return chartManager.getValues(chartName, version)
+ const repo = await repoManager.repository(repoName);
+ const chartManager = new HelmChartManager(repo);
+ return chartManager.getValues(chartName, version);
}
public async listReleases(cluster: Cluster, namespace: string = null) {
- await repoManager.init()
- return await releaseManager.listReleases(cluster.getProxyKubeconfigPath(), namespace)
+ await repoManager.init();
+ return await releaseManager.listReleases(cluster.getProxyKubeconfigPath(), namespace);
}
public async getRelease(cluster: Cluster, releaseName: string, namespace: string) {
- logger.debug("Fetch release")
- return await releaseManager.getRelease(releaseName, namespace, cluster)
+ logger.debug("Fetch release");
+ return await releaseManager.getRelease(releaseName, namespace, cluster);
}
public async getReleaseValues(cluster: Cluster, releaseName: string, namespace: string) {
- logger.debug("Fetch release values")
- return await releaseManager.getValues(releaseName, namespace, cluster.getProxyKubeconfigPath())
+ logger.debug("Fetch release values");
+ return await releaseManager.getValues(releaseName, namespace, cluster.getProxyKubeconfigPath());
}
public async getReleaseHistory(cluster: Cluster, releaseName: string, namespace: string) {
- logger.debug("Fetch release history")
- return await releaseManager.getHistory(releaseName, namespace, cluster.getProxyKubeconfigPath())
+ logger.debug("Fetch release history");
+ return await releaseManager.getHistory(releaseName, namespace, cluster.getProxyKubeconfigPath());
}
public async deleteRelease(cluster: Cluster, releaseName: string, namespace: string) {
- logger.debug("Delete release")
- return await releaseManager.deleteRelease(releaseName, namespace, cluster.getProxyKubeconfigPath())
+ logger.debug("Delete release");
+ return await releaseManager.deleteRelease(releaseName, namespace, cluster.getProxyKubeconfigPath());
}
public async updateRelease(cluster: Cluster, releaseName: string, namespace: string, data: { chart: string; values: {}; version: string }) {
- logger.debug("Upgrade release")
- return await releaseManager.upgradeRelease(releaseName, data.chart, data.values, namespace, data.version, cluster)
+ logger.debug("Upgrade release");
+ return await releaseManager.upgradeRelease(releaseName, data.chart, data.values, namespace, data.version, cluster);
}
public async rollback(cluster: Cluster, releaseName: string, namespace: string, revision: number) {
- logger.debug("Rollback release")
- const output = await releaseManager.rollback(releaseName, namespace, revision, cluster.getProxyKubeconfigPath())
- return { message: output }
+ logger.debug("Rollback release");
+ const output = await releaseManager.rollback(releaseName, namespace, revision, cluster.getProxyKubeconfigPath());
+ 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
+ return entry[0]['deprecated'] != true;
}
- return entry["deprecated"] != true
- })
+ return entry["deprecated"] != true;
+ });
}
- return entries
+ return entries;
}
}
-export const helmService = new HelmService()
+export const helmService = new HelmService();
diff --git a/src/main/index.ts b/src/main/index.ts
index 21acda801b..1d6aadfd43 100644
--- a/src/main/index.ts
+++ b/src/main/index.ts
@@ -1,28 +1,31 @@
// Main process
-import "../common/system-ca"
-import "../common/prometheus-providers"
-import * as Mobx from "mobx"
+import "../common/system-ca";
+import "../common/prometheus-providers";
+import * as Mobx from "mobx";
import * as LensExtensions from "../extensions/core-api";
-import { app, dialog } from "electron"
+import { app, dialog } from "electron";
import { appName } from "../common/vars";
-import path from "path"
-import { LensProxy } from "./lens-proxy"
+import path from "path";
+import { LensProxy } from "./lens-proxy";
import { WindowManager } from "./window-manager";
import { ClusterManager } from "./cluster-manager";
-import { AppUpdater } from "./app-updater"
-import { shellSync } from "./shell-sync"
-import { getFreePort } from "./port"
-import { mangleProxyEnv } from "./proxy-env"
+import { AppUpdater } from "./app-updater";
+import { shellSync } from "./shell-sync";
+import { getFreePort } from "./port";
+import { mangleProxyEnv } from "./proxy-env";
import { registerFileProtocol } from "../common/register-protocol";
-import logger from "./logger"
-import { clusterStore } from "../common/cluster-store"
+import logger from "./logger";
+import { clusterStore } from "../common/cluster-store";
import { userStore } from "../common/user-store";
import { workspaceStore } from "../common/workspace-store";
-import { appEventBus } from "../common/event-bus"
+import { appEventBus } from "../common/event-bus";
import { extensionLoader } from "../extensions/extension-loader";
-import { extensionManager } from "../extensions/extension-manager";
import { extensionsStore } from "../extensions/extensions-store";
+import { InstalledExtension, extensionDiscovery } from "../extensions/extension-discovery";
+import type { LensExtensionId } from "../extensions/lens-extension";
+import { installDeveloperTools } from "./developer-tools";
+import { filesystemProvisionerStore } from "./extension-filesystem";
const workingDir = path.join(app.getPath("appData"), appName);
let proxyPort: number;
@@ -35,59 +38,77 @@ if (!process.env.CICD) {
app.setPath("userData", workingDir);
}
-mangleProxyEnv()
+mangleProxyEnv();
if (app.commandLine.getSwitchValue("proxy-server") !== "") {
- process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server")
+ process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server");
}
app.on("ready", async () => {
- logger.info(`🚀 Starting Lens from "${workingDir}"`)
+ logger.info(`🚀 Starting Lens from "${workingDir}"`);
await shellSync();
- const updater = new AppUpdater()
+ const updater = new AppUpdater();
updater.start();
registerFileProtocol("static", __static);
+ await installDeveloperTools();
+
// preload
await Promise.all([
userStore.load(),
clusterStore.load(),
workspaceStore.load(),
extensionsStore.load(),
+ filesystemProvisionerStore.load(),
]);
// find free port
try {
- proxyPort = await getFreePort()
+ proxyPort = await getFreePort();
} catch (error) {
- logger.error(error)
- dialog.showErrorBox("Lens Error", "Could not find a free port for the cluster proxy")
+ logger.error(error);
+ dialog.showErrorBox("Lens Error", "Could not find a free port for the cluster proxy");
app.exit();
}
// create cluster manager
- clusterManager = new ClusterManager(proxyPort);
+ clusterManager = ClusterManager.getInstance(proxyPort);
// run proxy
try {
proxyServer = LensProxy.create(proxyPort, clusterManager);
} catch (error) {
- logger.error(`Could not start proxy (127.0.0:${proxyPort}): ${error.message}`)
- dialog.showErrorBox("Lens Error", `Could not start proxy (127.0.0:${proxyPort}): ${error.message || "unknown error"}`)
+ logger.error(`Could not start proxy (127.0.0:${proxyPort}): ${error.message}`);
+ dialog.showErrorBox("Lens Error", `Could not start proxy (127.0.0:${proxyPort}): ${error.message || "unknown error"}`);
app.exit();
}
+ extensionLoader.init();
+
+ extensionDiscovery.init();
windowManager = WindowManager.getInstance(proxyPort);
- extensionLoader.init(await extensionManager.load()); // call after windowManager to see splash earlier
+
+ // call after windowManager to see splash earlier
+ const extensions = await extensionDiscovery.load();
+
+ // Subscribe to extensions that are copied or deleted to/from the extensions folder
+ extensionDiscovery.events.on("add", (extension: InstalledExtension) => {
+ extensionLoader.addExtension(extension);
+ });
+ extensionDiscovery.events.on("remove", (lensExtensionId: LensExtensionId) => {
+ extensionLoader.removeExtension(lensExtensionId);
+ });
+
+ extensionLoader.initExtensions(extensions);
setTimeout(() => {
- appEventBus.emit({ name: "app", action: "start" })
- }, 1000)
+ appEventBus.emit({ name: "service", action: "start" });
+ }, 1000);
});
app.on("activate", (event, hasVisibleWindows) => {
- logger.info('APP:ACTIVATE', { hasVisibleWindows })
+ logger.info('APP:ACTIVATE', { hasVisibleWindows });
if (!hasVisibleWindows) {
windowManager.initMainWindow();
}
@@ -96,10 +117,11 @@ app.on("activate", (event, hasVisibleWindows) => {
// Quit app on Cmd+Q (MacOS)
app.on("will-quit", (event) => {
logger.info('APP:QUIT');
+ appEventBus.emit({name: "app", action: "close"});
event.preventDefault(); // prevent app's default shutdown (e.g. required for telemetry, etc.)
clusterManager?.stop(); // close cluster connections
return; // skip exit to make tray work, to quit go to app's global menu or tray's menu
-})
+});
// Extensions-api runtime exports
export const LensExtensionsApi = {
@@ -109,4 +131,4 @@ export const LensExtensionsApi = {
export {
Mobx,
LensExtensionsApi as LensExtensions,
-}
+};
diff --git a/src/main/kube-auth-proxy.ts b/src/main/kube-auth-proxy.ts
index 7192425466..3ad76e52b0 100644
--- a/src/main/kube-auth-proxy.ts
+++ b/src/main/kube-auth-proxy.ts
@@ -1,9 +1,10 @@
-import { ChildProcess, spawn } from "child_process"
+import { ChildProcess, spawn } from "child_process";
import { waitUntilUsed } from "tcp-port-used";
-import { broadcastIpc } from "../common/ipc";
-import type { Cluster } from "./cluster"
-import { Kubectl } from "./kubectl"
-import logger from "./logger"
+import { broadcastMessage } from "../common/ipc";
+import type { Cluster } from "./cluster";
+import { Kubectl } from "./kubectl";
+import logger from "./logger";
+import * as url from "url";
export interface KubeAuthProxyLog {
data: string;
@@ -11,90 +12,95 @@ export interface KubeAuthProxyLog {
}
export class KubeAuthProxy {
- public lastError: string
+ public lastError: string;
- protected cluster: Cluster
- protected env: NodeJS.ProcessEnv = null
- protected proxyProcess: ChildProcess
- protected port: number
- protected kubectl: Kubectl
+ protected cluster: Cluster;
+ protected env: NodeJS.ProcessEnv = null;
+ protected proxyProcess: ChildProcess;
+ protected port: number;
+ protected kubectl: Kubectl;
constructor(cluster: Cluster, port: number, env: NodeJS.ProcessEnv) {
- this.env = env
- this.port = port
- this.cluster = cluster
- this.kubectl = Kubectl.bundled()
+ this.env = env;
+ this.port = port;
+ this.cluster = cluster;
+ this.kubectl = Kubectl.bundled();
+ }
+
+ get acceptHosts() {
+ return url.parse(this.cluster.apiUrl).hostname;
}
public async run(): Promise {
if (this.proxyProcess) {
return;
}
- const proxyBin = await this.kubectl.getPath()
+
+ const proxyBin = await this.kubectl.getPath();
const args = [
"proxy",
"-p", `${this.port}`,
"--kubeconfig", `${this.cluster.kubeConfigPath}`,
"--context", `${this.cluster.contextName}`,
- "--accept-hosts", ".*",
+ "--accept-hosts", this.acceptHosts,
"--reject-paths", "^[^/]"
- ]
+ ];
if (process.env.DEBUG_PROXY === "true") {
- args.push("-v", "9")
+ args.push("-v", "9");
}
- logger.debug(`spawning kubectl proxy with args: ${args}`)
- this.proxyProcess = spawn(proxyBin, args, { env: this.env, })
+ logger.debug(`spawning kubectl proxy with args: ${args}`);
+ this.proxyProcess = spawn(proxyBin, args, { env: this.env, });
this.proxyProcess.on("error", (error) => {
- this.sendIpcLogMessage({ data: error.message, error: true })
- this.exit()
- })
+ this.sendIpcLogMessage({ data: error.message, error: true });
+ this.exit();
+ });
this.proxyProcess.on("exit", (code) => {
- this.sendIpcLogMessage({ data: `proxy exited with code: ${code}`, error: code > 0 })
+ this.sendIpcLogMessage({ data: `proxy exited with code: ${code}`, error: code > 0 });
this.exit();
- })
+ });
this.proxyProcess.stdout.on('data', (data) => {
- let logItem = data.toString()
+ let logItem = data.toString();
if (logItem.startsWith("Starting to serve on")) {
- logItem = "Authentication proxy started\n"
+ logItem = "Authentication proxy started\n";
}
- this.sendIpcLogMessage({ data: logItem })
- })
+ this.sendIpcLogMessage({ data: logItem });
+ });
this.proxyProcess.stderr.on('data', (data) => {
- this.lastError = this.parseError(data.toString())
- this.sendIpcLogMessage({ data: data.toString(), error: true })
- })
+ this.lastError = this.parseError(data.toString());
+ this.sendIpcLogMessage({ data: data.toString(), error: true });
+ });
- return waitUntilUsed(this.port, 500, 10000)
+ return waitUntilUsed(this.port, 500, 10000);
}
protected parseError(data: string) {
- const error = data.split("http: proxy error:").slice(1).join("").trim()
- let errorMsg = error
- const jsonError = error.split("Response: ")[1]
+ const error = data.split("http: proxy error:").slice(1).join("").trim();
+ let errorMsg = error;
+ const jsonError = error.split("Response: ")[1];
if (jsonError) {
try {
- const parsedError = JSON.parse(jsonError)
- errorMsg = parsedError.error_description || parsedError.error || jsonError
+ const parsedError = JSON.parse(jsonError);
+ errorMsg = parsedError.error_description || parsedError.error || jsonError;
} catch (_) {
- errorMsg = jsonError.trim()
+ errorMsg = jsonError.trim();
}
}
- return errorMsg
+ return errorMsg;
}
protected async sendIpcLogMessage(res: KubeAuthProxyLog) {
- const channel = `kube-auth:${this.cluster.id}`
+ const channel = `kube-auth:${this.cluster.id}`;
logger.info(`[KUBE-AUTH]: out-channel "${channel}"`, { ...res, meta: this.cluster.getMeta() });
- broadcastIpc({ channel: channel, args: [res] });
+ broadcastMessage(channel, res);
}
public exit() {
if (!this.proxyProcess) return;
- logger.debug("[KUBE-AUTH]: stopping local proxy", this.cluster.getMeta())
- this.proxyProcess.kill()
+ logger.debug("[KUBE-AUTH]: stopping local proxy", this.cluster.getMeta());
+ this.proxyProcess.kill();
this.proxyProcess.removeAllListeners();
this.proxyProcess.stderr.removeAllListeners();
this.proxyProcess.stdout.removeAllListeners();
diff --git a/src/main/kubeconfig-manager.ts b/src/main/kubeconfig-manager.ts
index fc84d00ddb..a8b3ae3fce 100644
--- a/src/main/kubeconfig-manager.ts
+++ b/src/main/kubeconfig-manager.ts
@@ -1,22 +1,22 @@
import type { KubeConfig } from "@kubernetes/client-node";
-import type { Cluster } from "./cluster"
+import type { Cluster } from "./cluster";
import type { ContextHandler } from "./context-handler";
-import { app } from "electron"
-import path from "path"
-import fs from "fs-extra"
-import { dumpConfigYaml, loadConfig } from "../common/kube-helpers"
-import logger from "./logger"
+import { app } from "electron";
+import path from "path";
+import fs from "fs-extra";
+import { dumpConfigYaml, loadConfig } from "../common/kube-helpers";
+import logger from "./logger";
export class KubeconfigManager {
- protected configDir = app.getPath("temp")
+ protected configDir = app.getPath("temp");
protected tempFile: string;
private constructor(protected cluster: Cluster, protected contextHandler: ContextHandler, protected port: number) { }
static async create(cluster: Cluster, contextHandler: ContextHandler, port: number) {
- const kcm = new KubeconfigManager(cluster, contextHandler, port)
- await kcm.init()
- return kcm
+ const kcm = new KubeconfigManager(cluster, contextHandler, port);
+ await kcm.init();
+ return kcm;
}
protected async init() {
@@ -24,7 +24,7 @@ export class KubeconfigManager {
await this.contextHandler.ensurePort();
await this.createProxyKubeconfig();
} catch (err) {
- logger.error(`Failed to created temp config for auth-proxy`, { err })
+ logger.error(`Failed to created temp config for auth-proxy`, { err });
}
}
@@ -33,7 +33,7 @@ export class KubeconfigManager {
}
protected resolveProxyUrl() {
- return `http://127.0.0.1:${this.port}/${this.cluster.id}`
+ return `http://127.0.0.1:${this.port}/${this.cluster.id}`;
}
/**
@@ -78,11 +78,11 @@ export class KubeconfigManager {
async unlink() {
if (!this.tempFile) {
- return
+ return;
}
- logger.info('Deleting temporary kubeconfig: ' + this.tempFile)
- await fs.unlink(this.tempFile)
- this.tempFile = undefined
+ logger.info('Deleting temporary kubeconfig: ' + this.tempFile);
+ await fs.unlink(this.tempFile);
+ this.tempFile = undefined;
}
}
diff --git a/src/main/kubectl.ts b/src/main/kubectl.ts
index 6b2f51476d..5d3f12746e 100644
--- a/src/main/kubectl.ts
+++ b/src/main/kubectl.ts
@@ -1,17 +1,17 @@
-import { app, remote } from "electron"
-import path from "path"
-import fs from "fs"
-import { promiseExec } from "./promise-exec"
-import logger from "./logger"
-import { ensureDir, pathExists } from "fs-extra"
-import * as lockFile from "proper-lockfile"
-import { helmCli } from "./helm/helm-cli"
-import { userStore } from "../common/user-store"
+import { app, remote } from "electron";
+import path from "path";
+import fs from "fs";
+import { promiseExec } from "./promise-exec";
+import logger from "./logger";
+import { ensureDir, pathExists } from "fs-extra";
+import * as lockFile from "proper-lockfile";
+import { helmCli } from "./helm/helm-cli";
+import { userStore } from "../common/user-store";
import { customRequest } from "../common/request";
-import { getBundledKubectlVersion } from "../common/utils/app-version"
+import { getBundledKubectlVersion } from "../common/utils/app-version";
import { isDevelopment, isWindows, isTestEnv } from "../common/vars";
-const bundledVersion = getBundledKubectlVersion()
+const bundledVersion = getBundledKubectlVersion();
const kubectlMap: Map = new Map([
["1.7", "1.8.15"],
["1.8", "1.9.10"],
@@ -26,314 +26,314 @@ const kubectlMap: Map = new Map([
["1.17", bundledVersion],
["1.18", "1.18.8"],
["1.19", "1.19.0"]
-])
+]);
const packageMirrors: Map = new Map([
["default", "https://storage.googleapis.com/kubernetes-release/release"],
["china", "https://mirror.azure.cn/kubernetes/kubectl"]
-])
+]);
-let bundledPath: string
-const initScriptVersionString = "# lens-initscript v3\n"
+let bundledPath: string;
+const initScriptVersionString = "# lens-initscript v3\n";
export function bundledKubectlPath(): string {
- if (bundledPath) { return bundledPath }
+ if (bundledPath) { return bundledPath; }
if (isDevelopment || isTestEnv) {
- const platformName = isWindows ? "windows" : process.platform
- bundledPath = path.join(process.cwd(), "binaries", "client", platformName, process.arch, "kubectl")
+ const platformName = isWindows ? "windows" : process.platform;
+ bundledPath = path.join(process.cwd(), "binaries", "client", platformName, process.arch, "kubectl");
} else {
- bundledPath = path.join(process.resourcesPath, process.arch, "kubectl")
+ bundledPath = path.join(process.resourcesPath, process.arch, "kubectl");
}
if (isWindows) {
- bundledPath = `${bundledPath}.exe`
+ bundledPath = `${bundledPath}.exe`;
}
- return bundledPath
+ return bundledPath;
}
export class Kubectl {
- public kubectlVersion: string
- protected directory: string
- protected url: string
- protected path: string
- protected dirname: string
+ public kubectlVersion: string;
+ protected directory: string;
+ protected url: string;
+ protected path: string;
+ protected dirname: string;
static get kubectlDir() {
- return path.join((app || remote.app).getPath("userData"), "binaries", "kubectl")
+ return path.join((app || remote.app).getPath("userData"), "binaries", "kubectl");
}
- public static readonly bundledKubectlVersion: string = bundledVersion
- public static invalidBundle = false
+ public static readonly bundledKubectlVersion: string = bundledVersion;
+ public static invalidBundle = false;
private static bundledInstance: Kubectl;
// Returns the single bundled Kubectl instance
public static bundled() {
- if (!Kubectl.bundledInstance) Kubectl.bundledInstance = new Kubectl(Kubectl.bundledKubectlVersion)
- return Kubectl.bundledInstance
+ if (!Kubectl.bundledInstance) Kubectl.bundledInstance = new Kubectl(Kubectl.bundledKubectlVersion);
+ return Kubectl.bundledInstance;
}
constructor(clusterVersion: string) {
- const versionParts = /^v?(\d+\.\d+)(.*)/.exec(clusterVersion)
- const minorVersion = versionParts[1]
+ const versionParts = /^v?(\d+\.\d+)(.*)/.exec(clusterVersion);
+ const minorVersion = versionParts[1];
/* minorVersion is the first two digits of kube server version
if the version map includes that, use that version, if not, fallback to the exact x.y.z of kube version */
if (kubectlMap.has(minorVersion)) {
- this.kubectlVersion = kubectlMap.get(minorVersion)
- logger.debug("Set kubectl version " + this.kubectlVersion + " for cluster version " + clusterVersion + " using version map")
+ this.kubectlVersion = kubectlMap.get(minorVersion);
+ logger.debug("Set kubectl version " + this.kubectlVersion + " for cluster version " + clusterVersion + " using version map");
} else {
- this.kubectlVersion = versionParts[1] + versionParts[2]
- logger.debug("Set kubectl version " + this.kubectlVersion + " for cluster version " + clusterVersion + " using fallback")
+ this.kubectlVersion = versionParts[1] + versionParts[2];
+ logger.debug("Set kubectl version " + this.kubectlVersion + " for cluster version " + clusterVersion + " using fallback");
}
- let arch = null
+ let arch = null;
if (process.arch == "x64") {
- arch = "amd64"
+ arch = "amd64";
} else if (process.arch == "x86" || process.arch == "ia32") {
- arch = "386"
+ arch = "386";
} else {
- arch = process.arch
+ arch = process.arch;
}
- const platformName = isWindows ? "windows" : process.platform
- const binaryName = isWindows ? "kubectl.exe" : "kubectl"
+ const platformName = isWindows ? "windows" : process.platform;
+ const binaryName = isWindows ? "kubectl.exe" : "kubectl";
- this.url = `${this.getDownloadMirror()}/v${this.kubectlVersion}/bin/${platformName}/${arch}/${binaryName}`
+ this.url = `${this.getDownloadMirror()}/v${this.kubectlVersion}/bin/${platformName}/${arch}/${binaryName}`;
- this.dirname = path.normalize(path.join(this.getDownloadDir(), this.kubectlVersion))
- this.path = path.join(this.dirname, binaryName)
+ this.dirname = path.normalize(path.join(this.getDownloadDir(), this.kubectlVersion));
+ this.path = path.join(this.dirname, binaryName);
}
public getBundledPath() {
- return bundledKubectlPath()
+ return bundledKubectlPath();
}
public getPathFromPreferences() {
- return userStore.preferences?.kubectlBinariesPath || this.getBundledPath()
+ return userStore.preferences?.kubectlBinariesPath || this.getBundledPath();
}
protected getDownloadDir() {
if (userStore.preferences?.downloadBinariesPath) {
- return path.join(userStore.preferences.downloadBinariesPath, "kubectl")
+ return path.join(userStore.preferences.downloadBinariesPath, "kubectl");
}
- return Kubectl.kubectlDir
+ return Kubectl.kubectlDir;
}
public async getPath(bundled = false): Promise {
if (userStore.preferences?.downloadKubectlBinaries === false) {
- return this.getPathFromPreferences()
+ return this.getPathFromPreferences();
}
// return binary name if bundled path is not functional
if (!await this.checkBinary(this.getBundledPath(), false)) {
- Kubectl.invalidBundle = true
- return path.basename(this.getBundledPath())
+ Kubectl.invalidBundle = true;
+ return path.basename(this.getBundledPath());
}
try {
if (!await this.ensureKubectl()) {
- logger.error("Failed to ensure kubectl, fallback to the bundled version")
- return this.getBundledPath()
+ logger.error("Failed to ensure kubectl, fallback to the bundled version");
+ return this.getBundledPath();
}
- return this.path
+ return this.path;
} catch (err) {
- logger.error("Failed to ensure kubectl, fallback to the bundled version")
- logger.error(err)
- return this.getBundledPath()
+ logger.error("Failed to ensure kubectl, fallback to the bundled version");
+ logger.error(err);
+ return this.getBundledPath();
}
}
public async binDir() {
try {
- await this.ensureKubectl()
- await this.writeInitScripts()
- return this.dirname
+ await this.ensureKubectl();
+ await this.writeInitScripts();
+ return this.dirname;
} catch (err) {
- logger.error(err)
- return ""
+ logger.error(err);
+ return "";
}
}
public async checkBinary(path: string, checkVersion = true) {
- const exists = await pathExists(path)
+ const exists = await pathExists(path);
if (exists) {
try {
- const { stdout } = await promiseExec(`"${path}" version --client=true -o json`)
- const output = JSON.parse(stdout)
+ const { stdout } = await promiseExec(`"${path}" version --client=true -o json`);
+ const output = JSON.parse(stdout);
if (!checkVersion) {
- return true
+ return true;
}
- let version: string = output.clientVersion.gitVersion
+ let version: string = output.clientVersion.gitVersion;
if (version[0] === 'v') {
- version = version.slice(1)
+ version = version.slice(1);
}
if (version === this.kubectlVersion) {
- logger.debug(`Local kubectl is version ${this.kubectlVersion}`)
- return true
+ logger.debug(`Local kubectl is version ${this.kubectlVersion}`);
+ return true;
}
- logger.error(`Local kubectl is version ${version}, expected ${this.kubectlVersion}, unlinking`)
+ logger.error(`Local kubectl is version ${version}, expected ${this.kubectlVersion}, unlinking`);
} catch (err) {
- logger.error(`Local kubectl failed to run properly (${err.message}), unlinking`)
+ logger.error(`Local kubectl failed to run properly (${err.message}), unlinking`);
}
- await fs.promises.unlink(this.path)
+ await fs.promises.unlink(this.path);
}
- return false
+ return false;
}
protected async checkBundled(): Promise {
if (this.kubectlVersion === Kubectl.bundledKubectlVersion) {
try {
- const exist = await pathExists(this.path)
+ const exist = await pathExists(this.path);
if (!exist) {
- await fs.promises.copyFile(this.getBundledPath(), this.path)
- await fs.promises.chmod(this.path, 0o755)
+ await fs.promises.copyFile(this.getBundledPath(), this.path);
+ await fs.promises.chmod(this.path, 0o755);
}
- return true
+ return true;
} catch (err) {
- logger.error("Could not copy the bundled kubectl to app-data: " + err)
- return false
+ logger.error("Could not copy the bundled kubectl to app-data: " + err);
+ return false;
}
} else {
- return false
+ return false;
}
}
public async ensureKubectl(): Promise {
if (userStore.preferences?.downloadKubectlBinaries === false) {
- return true
+ return true;
}
if (Kubectl.invalidBundle) {
- logger.error(`Detected invalid bundle binary, returning ...`)
- return false
+ logger.error(`Detected invalid bundle binary, returning ...`);
+ return false;
}
- await ensureDir(this.dirname, 0o755)
+ await ensureDir(this.dirname, 0o755);
return lockFile.lock(this.dirname).then(async (release) => {
- logger.debug(`Acquired a lock for ${this.kubectlVersion}`)
- const bundled = await this.checkBundled()
- let isValid = await this.checkBinary(this.path, !bundled)
+ logger.debug(`Acquired a lock for ${this.kubectlVersion}`);
+ const bundled = await this.checkBundled();
+ let isValid = await this.checkBinary(this.path, !bundled);
if (!isValid && !bundled) {
await this.downloadKubectl().catch((error) => {
- logger.error(error)
- logger.debug(`Releasing lock for ${this.kubectlVersion}`)
- release()
- return false
+ logger.error(error);
+ logger.debug(`Releasing lock for ${this.kubectlVersion}`);
+ release();
+ return false;
});
- isValid = !await this.checkBinary(this.path, false)
+ isValid = !await this.checkBinary(this.path, false);
}
if (!isValid) {
- logger.debug(`Releasing lock for ${this.kubectlVersion}`)
- release()
- return false
+ logger.debug(`Releasing lock for ${this.kubectlVersion}`);
+ release();
+ return false;
}
- logger.debug(`Releasing lock for ${this.kubectlVersion}`)
- release()
- return true
+ logger.debug(`Releasing lock for ${this.kubectlVersion}`);
+ release();
+ return true;
}).catch((e) => {
- logger.error(`Failed to get a lock for ${this.kubectlVersion}`)
- logger.error(e)
- return false
- })
+ logger.error(`Failed to get a lock for ${this.kubectlVersion}`);
+ logger.error(e);
+ return false;
+ });
}
public async downloadKubectl() {
- await ensureDir(path.dirname(this.path), 0o755)
+ await ensureDir(path.dirname(this.path), 0o755);
- logger.info(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`)
+ logger.info(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`);
return new Promise((resolve, reject) => {
const stream = customRequest({
url: this.url,
gzip: true,
});
- const file = fs.createWriteStream(this.path)
+ const file = fs.createWriteStream(this.path);
stream.on("complete", () => {
- logger.debug("kubectl binary download finished")
- file.end()
- })
+ logger.debug("kubectl binary download finished");
+ file.end();
+ });
stream.on("error", (error) => {
- logger.error(error)
+ logger.error(error);
fs.unlink(this.path, () => {
// do nothing
- })
- reject(error)
- })
+ });
+ reject(error);
+ });
file.on("close", () => {
- logger.debug("kubectl binary download closed")
+ logger.debug("kubectl binary download closed");
fs.chmod(this.path, 0o755, (err) => {
if (err) reject(err);
- })
- resolve()
- })
- stream.pipe(file)
- })
+ });
+ resolve();
+ });
+ stream.pipe(file);
+ });
}
protected async writeInitScripts() {
- const kubectlPath = userStore.preferences?.downloadKubectlBinaries ? this.dirname : path.dirname(this.getPathFromPreferences())
- const helmPath = helmCli.getBinaryDir()
+ const kubectlPath = userStore.preferences?.downloadKubectlBinaries ? this.dirname : path.dirname(this.getPathFromPreferences());
+ const helmPath = helmCli.getBinaryDir();
const fsPromises = fs.promises;
- const bashScriptPath = path.join(this.dirname, '.bash_set_path')
+ const bashScriptPath = path.join(this.dirname, '.bash_set_path');
- let bashScript = "" + initScriptVersionString
- bashScript += "tempkubeconfig=\"$KUBECONFIG\"\n"
- bashScript += "test -f \"/etc/profile\" && . \"/etc/profile\"\n"
- bashScript += "if test -f \"$HOME/.bash_profile\"; then\n"
- bashScript += " . \"$HOME/.bash_profile\"\n"
- bashScript += "elif test -f \"$HOME/.bash_login\"; then\n"
- bashScript += " . \"$HOME/.bash_login\"\n"
- bashScript += "elif test -f \"$HOME/.profile\"; then\n"
- bashScript += " . \"$HOME/.profile\"\n"
- bashScript += "fi\n"
- bashScript += `export PATH="${helmPath}:${kubectlPath}:$PATH"\n`
- bashScript += "export KUBECONFIG=\"$tempkubeconfig\"\n"
+ let bashScript = "" + initScriptVersionString;
+ bashScript += "tempkubeconfig=\"$KUBECONFIG\"\n";
+ bashScript += "test -f \"/etc/profile\" && . \"/etc/profile\"\n";
+ bashScript += "if test -f \"$HOME/.bash_profile\"; then\n";
+ bashScript += " . \"$HOME/.bash_profile\"\n";
+ bashScript += "elif test -f \"$HOME/.bash_login\"; then\n";
+ bashScript += " . \"$HOME/.bash_login\"\n";
+ bashScript += "elif test -f \"$HOME/.profile\"; then\n";
+ bashScript += " . \"$HOME/.profile\"\n";
+ bashScript += "fi\n";
+ bashScript += `export PATH="${helmPath}:${kubectlPath}:$PATH"\n`;
+ bashScript += "export KUBECONFIG=\"$tempkubeconfig\"\n";
- bashScript += "NO_PROXY=\",${NO_PROXY:-localhost},\"\n"
- bashScript += "NO_PROXY=\"${NO_PROXY//,localhost,/,}\"\n"
- bashScript += "NO_PROXY=\"${NO_PROXY//,127.0.0.1,/,}\"\n"
- bashScript += "NO_PROXY=\"localhost,127.0.0.1${NO_PROXY%,}\"\n"
- bashScript += "export NO_PROXY\n"
- bashScript += "unset tempkubeconfig\n"
- await fsPromises.writeFile(bashScriptPath, bashScript.toString(), { mode: 0o644 })
+ bashScript += "NO_PROXY=\",${NO_PROXY:-localhost},\"\n";
+ bashScript += "NO_PROXY=\"${NO_PROXY//,localhost,/,}\"\n";
+ bashScript += "NO_PROXY=\"${NO_PROXY//,127.0.0.1,/,}\"\n";
+ bashScript += "NO_PROXY=\"localhost,127.0.0.1${NO_PROXY%,}\"\n";
+ bashScript += "export NO_PROXY\n";
+ bashScript += "unset tempkubeconfig\n";
+ await fsPromises.writeFile(bashScriptPath, bashScript.toString(), { mode: 0o644 });
- const zshScriptPath = path.join(this.dirname, '.zlogin')
+ const zshScriptPath = path.join(this.dirname, '.zlogin');
- let zshScript = "" + initScriptVersionString
+ let zshScript = "" + initScriptVersionString;
- zshScript += "tempkubeconfig=\"$KUBECONFIG\"\n"
+ zshScript += "tempkubeconfig=\"$KUBECONFIG\"\n";
// restore previous ZDOTDIR
- zshScript += "export ZDOTDIR=\"$OLD_ZDOTDIR\"\n"
+ zshScript += "export ZDOTDIR=\"$OLD_ZDOTDIR\"\n";
// source all the files
- zshScript += "test -f \"$OLD_ZDOTDIR/.zshenv\" && . \"$OLD_ZDOTDIR/.zshenv\"\n"
- zshScript += "test -f \"$OLD_ZDOTDIR/.zprofile\" && . \"$OLD_ZDOTDIR/.zprofile\"\n"
- zshScript += "test -f \"$OLD_ZDOTDIR/.zlogin\" && . \"$OLD_ZDOTDIR/.zlogin\"\n"
- zshScript += "test -f \"$OLD_ZDOTDIR/.zshrc\" && . \"$OLD_ZDOTDIR/.zshrc\"\n"
+ zshScript += "test -f \"$OLD_ZDOTDIR/.zshenv\" && . \"$OLD_ZDOTDIR/.zshenv\"\n";
+ zshScript += "test -f \"$OLD_ZDOTDIR/.zprofile\" && . \"$OLD_ZDOTDIR/.zprofile\"\n";
+ zshScript += "test -f \"$OLD_ZDOTDIR/.zlogin\" && . \"$OLD_ZDOTDIR/.zlogin\"\n";
+ zshScript += "test -f \"$OLD_ZDOTDIR/.zshrc\" && . \"$OLD_ZDOTDIR/.zshrc\"\n";
// voodoo to replace any previous occurrences of kubectl path in the PATH
- zshScript += `kubectlpath=\"${kubectlPath}"\n`
- zshScript += `helmpath=\"${helmPath}"\n`
- zshScript += "p=\":$kubectlpath:\"\n"
- zshScript += "d=\":$PATH:\"\n"
- zshScript += "d=${d//$p/:}\n"
- zshScript += "d=${d/#:/}\n"
- zshScript += "export PATH=\"$helmpath:$kubectlpath:${d/%:/}\"\n"
- zshScript += "export KUBECONFIG=\"$tempkubeconfig\"\n"
- zshScript += "NO_PROXY=\",${NO_PROXY:-localhost},\"\n"
- zshScript += "NO_PROXY=\"${NO_PROXY//,localhost,/,}\"\n"
- zshScript += "NO_PROXY=\"${NO_PROXY//,127.0.0.1,/,}\"\n"
- zshScript += "NO_PROXY=\"localhost,127.0.0.1${NO_PROXY%,}\"\n"
- zshScript += "export NO_PROXY\n"
- zshScript += "unset tempkubeconfig\n"
- zshScript += "unset OLD_ZDOTDIR\n"
- await fsPromises.writeFile(zshScriptPath, zshScript.toString(), { mode: 0o644 })
+ zshScript += `kubectlpath=\"${kubectlPath}"\n`;
+ zshScript += `helmpath=\"${helmPath}"\n`;
+ zshScript += "p=\":$kubectlpath:\"\n";
+ zshScript += "d=\":$PATH:\"\n";
+ zshScript += "d=${d//$p/:}\n";
+ zshScript += "d=${d/#:/}\n";
+ zshScript += "export PATH=\"$helmpath:$kubectlpath:${d/%:/}\"\n";
+ zshScript += "export KUBECONFIG=\"$tempkubeconfig\"\n";
+ zshScript += "NO_PROXY=\",${NO_PROXY:-localhost},\"\n";
+ zshScript += "NO_PROXY=\"${NO_PROXY//,localhost,/,}\"\n";
+ zshScript += "NO_PROXY=\"${NO_PROXY//,127.0.0.1,/,}\"\n";
+ zshScript += "NO_PROXY=\"localhost,127.0.0.1${NO_PROXY%,}\"\n";
+ zshScript += "export NO_PROXY\n";
+ zshScript += "unset tempkubeconfig\n";
+ zshScript += "unset OLD_ZDOTDIR\n";
+ await fsPromises.writeFile(zshScriptPath, zshScript.toString(), { mode: 0o644 });
}
protected getDownloadMirror() {
- const mirror = packageMirrors.get(userStore.preferences?.downloadMirror)
+ const mirror = packageMirrors.get(userStore.preferences?.downloadMirror);
if (mirror) {
- return mirror
+ return mirror;
}
- return packageMirrors.get("default") // MacOS packages are only available from default
+ return packageMirrors.get("default"); // MacOS packages are only available from default
}
}
diff --git a/src/main/kubectl_spec.ts b/src/main/kubectl_spec.ts
index ade999c082..9d5a5d1e1d 100644
--- a/src/main/kubectl_spec.ts
+++ b/src/main/kubectl_spec.ts
@@ -1,5 +1,5 @@
-import packageInfo from "../../package.json"
-import path from "path"
+import packageInfo from "../../package.json";
+import path from "path";
import { Kubectl } from "../../src/main/kubectl";
import { isWindows } from "../common/vars";
@@ -7,39 +7,39 @@ jest.mock("../common/user-store");
describe("kubectlVersion", () => {
it("returns bundled version if exactly same version used", async () => {
- const kubectl = new Kubectl(Kubectl.bundled().kubectlVersion)
- expect(kubectl.kubectlVersion).toBe(Kubectl.bundled().kubectlVersion)
- })
+ const kubectl = new Kubectl(Kubectl.bundled().kubectlVersion);
+ expect(kubectl.kubectlVersion).toBe(Kubectl.bundled().kubectlVersion);
+ });
it("returns bundled version if same major.minor version is used", async () => {
const { bundledKubectlVersion } = packageInfo.config;
const kubectl = new Kubectl(bundledKubectlVersion);
- expect(kubectl.kubectlVersion).toBe(Kubectl.bundled().kubectlVersion)
- })
-})
+ expect(kubectl.kubectlVersion).toBe(Kubectl.bundled().kubectlVersion);
+ });
+});
describe("getPath()", () => {
it("returns path to downloaded kubectl binary", async () => {
const { bundledKubectlVersion } = packageInfo.config;
const kubectl = new Kubectl(bundledKubectlVersion);
- const kubectlPath = await kubectl.getPath()
- let binaryName = "kubectl"
+ const kubectlPath = await kubectl.getPath();
+ let binaryName = "kubectl";
if (isWindows) {
- binaryName += ".exe"
+ binaryName += ".exe";
}
- const expectedPath = path.join(Kubectl.kubectlDir, Kubectl.bundledKubectlVersion, binaryName)
- expect(kubectlPath).toBe(expectedPath)
- })
+ const expectedPath = path.join(Kubectl.kubectlDir, Kubectl.bundledKubectlVersion, binaryName);
+ expect(kubectlPath).toBe(expectedPath);
+ });
it("returns plain binary name if bundled kubectl is non-functional", async () => {
const { bundledKubectlVersion } = packageInfo.config;
const kubectl = new Kubectl(bundledKubectlVersion);
- jest.spyOn(kubectl, "getBundledPath").mockReturnValue("/invalid/path/kubectl")
- const kubectlPath = await kubectl.getPath()
- let binaryName = "kubectl"
+ jest.spyOn(kubectl, "getBundledPath").mockReturnValue("/invalid/path/kubectl");
+ const kubectlPath = await kubectl.getPath();
+ let binaryName = "kubectl";
if (isWindows) {
- binaryName += ".exe"
+ binaryName += ".exe";
}
- expect(kubectlPath).toBe(binaryName)
- })
-})
+ expect(kubectlPath).toBe(binaryName);
+ });
+});
diff --git a/src/main/lens-api.ts b/src/main/lens-api.ts
index a0a7361a68..fafeffce91 100644
--- a/src/main/lens-api.ts
+++ b/src/main/lens-api.ts
@@ -2,16 +2,16 @@ import http from "http";
export abstract class LensApi {
protected respondJson(res: http.ServerResponse, content: {}, status = 200) {
- this.respond(res, JSON.stringify(content), "application/json", status)
+ this.respond(res, JSON.stringify(content), "application/json", status);
}
protected respondText(res: http.ServerResponse, content: string, status = 200) {
- this.respond(res, content, "text/plain", status)
+ this.respond(res, content, "text/plain", status);
}
protected respond(res: http.ServerResponse, content: string, contentType: string, status = 200) {
- res.setHeader("Content-Type", contentType)
- res.statusCode = status
- res.end(content)
+ res.setHeader("Content-Type", contentType);
+ res.statusCode = status;
+ res.end(content);
}
}
diff --git a/src/main/lens-binary.ts b/src/main/lens-binary.ts
index dd6f2aa058..fc6b59ee74 100644
--- a/src/main/lens-binary.ts
+++ b/src/main/lens-binary.ts
@@ -1,10 +1,10 @@
-import path from "path"
-import fs from "fs"
-import request from "request"
-import { ensureDir, pathExists } from "fs-extra"
-import * as tar from "tar"
+import path from "path";
+import fs from "fs";
+import request from "request";
+import { ensureDir, pathExists } from "fs-extra";
+import * as tar from "tar";
import { isWindows } from "../common/vars";
-import winston from "winston"
+import winston from "winston";
export type LensBinaryOpts = {
version: string;
@@ -12,177 +12,177 @@ export type LensBinaryOpts = {
originalBinaryName: string;
newBinaryName?: string;
requestOpts?: request.Options;
-}
+};
export class LensBinary {
- public binaryVersion: string
- protected directory: string
- protected url: string
+ public binaryVersion: string;
+ protected directory: string;
+ protected url: string;
protected path: string;
protected tarPath: string;
- protected dirname: string
- protected binaryName: string
- protected platformName: string
- protected arch: string
- protected originalBinaryName: string
- protected requestOpts: request.Options
- protected logger: Console | winston.Logger
+ protected dirname: string;
+ protected binaryName: string;
+ protected platformName: string;
+ protected arch: string;
+ protected originalBinaryName: string;
+ protected requestOpts: request.Options;
+ protected logger: Console | winston.Logger;
constructor(opts: LensBinaryOpts) {
- const baseDir = opts.baseDir
- this.originalBinaryName = opts.originalBinaryName
- this.binaryName = opts.newBinaryName || opts.originalBinaryName
- this.binaryVersion = opts.version
- this.requestOpts = opts.requestOpts
- this.logger = console
- let arch = null
+ const baseDir = opts.baseDir;
+ this.originalBinaryName = opts.originalBinaryName;
+ this.binaryName = opts.newBinaryName || opts.originalBinaryName;
+ this.binaryVersion = opts.version;
+ this.requestOpts = opts.requestOpts;
+ this.logger = console;
+ let arch = null;
if (process.arch == "x64") {
- arch = "amd64"
+ arch = "amd64";
}
else if (process.arch == "x86" || process.arch == "ia32") {
- arch = "386"
+ arch = "386";
}
else {
- arch = process.arch
+ arch = process.arch;
}
- this.arch = arch
- this.platformName = isWindows ? "windows" : process.platform
- this.dirname = path.normalize(path.join(baseDir, this.binaryName))
+ this.arch = arch;
+ this.platformName = isWindows ? "windows" : process.platform;
+ this.dirname = path.normalize(path.join(baseDir, this.binaryName));
if (isWindows) {
- this.binaryName = this.binaryName + ".exe"
- this.originalBinaryName = this.originalBinaryName + ".exe"
+ this.binaryName = this.binaryName + ".exe";
+ this.originalBinaryName = this.originalBinaryName + ".exe";
}
- const tarName = this.getTarName()
+ const tarName = this.getTarName();
if (tarName) {
- this.tarPath = path.join(this.dirname, tarName)
+ this.tarPath = path.join(this.dirname, tarName);
}
}
public setLogger(logger: Console | winston.Logger) {
- this.logger = logger
+ this.logger = logger;
}
protected binaryDir() {
- throw new Error("binaryDir not implemented")
+ throw new Error("binaryDir not implemented");
}
public async binaryPath() {
- await this.ensureBinary()
- return this.getBinaryPath()
+ await this.ensureBinary();
+ return this.getBinaryPath();
}
protected getTarName(): string | null {
- return null
+ return null;
}
protected getUrl() {
- return ""
+ return "";
}
protected getBinaryPath() {
- return ""
+ return "";
}
protected getOriginalBinaryPath() {
- return ""
+ return "";
}
public getBinaryDir() {
- return path.dirname(this.getBinaryPath())
+ return path.dirname(this.getBinaryPath());
}
public async binDir() {
try {
- await this.ensureBinary()
- return this.dirname
+ await this.ensureBinary();
+ return this.dirname;
} catch (err) {
- this.logger.error(err)
- return ""
+ this.logger.error(err);
+ return "";
}
}
protected async checkBinary() {
- const exists = await pathExists(this.getBinaryPath())
- return exists
+ const exists = await pathExists(this.getBinaryPath());
+ return exists;
}
public async ensureBinary() {
- const isValid = await this.checkBinary()
+ const isValid = await this.checkBinary();
if (!isValid) {
await this.downloadBinary().catch((error) => {
- this.logger.error(error)
+ this.logger.error(error);
});
- if (this.tarPath) await this.untarBinary()
- if (this.originalBinaryName != this.binaryName) await this.renameBinary()
- this.logger.info(`${this.originalBinaryName} has been downloaded to ${this.getBinaryPath()}`)
+ if (this.tarPath) await this.untarBinary();
+ if (this.originalBinaryName != this.binaryName) await this.renameBinary();
+ this.logger.info(`${this.originalBinaryName} has been downloaded to ${this.getBinaryPath()}`);
}
}
protected async untarBinary() {
return new Promise((resolve, reject) => {
- this.logger.debug(`Extracting ${this.originalBinaryName} binary`)
+ this.logger.debug(`Extracting ${this.originalBinaryName} binary`);
tar.x({
file: this.tarPath,
cwd: this.dirname
}).then((_ => {
- resolve()
- }))
- })
+ resolve();
+ }));
+ });
}
protected async renameBinary() {
return new Promise((resolve, reject) => {
- this.logger.debug(`Renaming ${this.originalBinaryName} binary to ${this.binaryName}`)
+ this.logger.debug(`Renaming ${this.originalBinaryName} binary to ${this.binaryName}`);
fs.rename(this.getOriginalBinaryPath(), this.getBinaryPath(), (err) => {
if (err) {
- reject(err)
+ reject(err);
}
else {
- resolve()
+ resolve();
}
- })
- })
+ });
+ });
}
protected async downloadBinary() {
- const binaryPath = this.tarPath || this.getBinaryPath()
- await ensureDir(this.getBinaryDir(), 0o755)
+ const binaryPath = this.tarPath || this.getBinaryPath();
+ await ensureDir(this.getBinaryDir(), 0o755);
- const file = fs.createWriteStream(binaryPath)
- const url = this.getUrl()
+ const file = fs.createWriteStream(binaryPath);
+ const url = this.getUrl();
- this.logger.info(`Downloading ${this.originalBinaryName} ${this.binaryVersion} from ${url} to ${binaryPath}`)
+ this.logger.info(`Downloading ${this.originalBinaryName} ${this.binaryVersion} from ${url} to ${binaryPath}`);
const requestOpts: request.UriOptions & request.CoreOptions = {
uri: url,
gzip: true,
...this.requestOpts
- }
+ };
- const stream = request(requestOpts)
+ const stream = request(requestOpts);
stream.on("complete", () => {
- this.logger.info(`Download of ${this.originalBinaryName} finished`)
- file.end()
- })
+ this.logger.info(`Download of ${this.originalBinaryName} finished`);
+ file.end();
+ });
stream.on("error", (error) => {
- this.logger.error(error)
+ this.logger.error(error);
fs.unlink(binaryPath, () => {
// do nothing
- })
- throw(error)
- })
+ });
+ throw(error);
+ });
return new Promise((resolve, reject) => {
file.on("close", () => {
- this.logger.debug(`${this.originalBinaryName} binary download closed`)
+ this.logger.debug(`${this.originalBinaryName} binary download closed`);
if (!this.tarPath) fs.chmod(binaryPath, 0o755, (err) => {
if (err) reject(err);
- })
- resolve()
- })
- stream.pipe(file)
- })
+ });
+ resolve();
+ });
+ stream.pipe(file);
+ });
}
}
diff --git a/src/main/lens-proxy.ts b/src/main/lens-proxy.ts
index e791039287..03b1b15d29 100644
--- a/src/main/lens-proxy.ts
+++ b/src/main/lens-proxy.ts
@@ -3,27 +3,27 @@ import http from "http";
import spdy from "spdy";
import httpProxy from "http-proxy";
import url from "url";
-import * as WebSocket from "ws"
-import { apiPrefix, apiKubePrefix } from "../common/vars"
+import * as WebSocket from "ws";
+import { apiPrefix, apiKubePrefix } from "../common/vars";
import { openShell } from "./node-shell-session";
-import { Router } from "./router"
-import { ClusterManager } from "./cluster-manager"
+import { Router } from "./router";
+import { ClusterManager } from "./cluster-manager";
import { ContextHandler } from "./context-handler";
-import logger from "./logger"
+import logger from "./logger";
export class LensProxy {
- protected origin: string
- protected proxyServer: http.Server
- protected router: Router
- protected closed = false
- protected retryCounters = new Map()
+ protected origin: string;
+ protected proxyServer: http.Server;
+ protected router: Router;
+ protected closed = false;
+ protected retryCounters = new Map();
static create(port: number, clusterManager: ClusterManager) {
return new LensProxy(port, clusterManager).listen();
}
private constructor(protected port: number, protected clusterManager: ClusterManager) {
- this.origin = `http://localhost:${port}`
+ this.origin = `http://localhost:${port}`;
this.router = new Router();
}
@@ -35,8 +35,8 @@ export class LensProxy {
close() {
logger.info("Closing proxy server");
- this.proxyServer.close()
- this.closed = true
+ this.proxyServer.close();
+ this.closed = true;
}
protected buildCustomProxy(): http.Server {
@@ -47,66 +47,66 @@ export class LensProxy {
protocols: ["http/1.1", "spdy/3.1"]
}
}, (req: http.IncomingMessage, res: http.ServerResponse) => {
- this.handleRequest(proxy, req, res)
- })
+ this.handleRequest(proxy, req, res);
+ });
spdyProxy.on("upgrade", (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => {
if (req.url.startsWith(`${apiPrefix}?`)) {
- this.handleWsUpgrade(req, socket, head)
+ this.handleWsUpgrade(req, socket, head);
} else {
- this.handleProxyUpgrade(proxy, req, socket, head)
+ this.handleProxyUpgrade(proxy, req, socket, head);
}
- })
+ });
spdyProxy.on("error", (err) => {
- logger.error("proxy error", err)
- })
- return spdyProxy
+ logger.error("proxy error", err);
+ });
+ return spdyProxy;
}
protected async handleProxyUpgrade(proxy: httpProxy, req: http.IncomingMessage, socket: net.Socket, head: Buffer) {
- const cluster = this.clusterManager.getClusterForRequest(req)
+ const cluster = this.clusterManager.getClusterForRequest(req);
if (cluster) {
- const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, "")
- const apiUrl = url.parse(cluster.apiUrl)
- const pUrl = url.parse(proxyUrl)
- const connectOpts = { port: parseInt(pUrl.port), host: pUrl.hostname }
- const proxySocket = new net.Socket()
+ const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, "");
+ const apiUrl = url.parse(cluster.apiUrl);
+ const pUrl = url.parse(proxyUrl);
+ const connectOpts = { port: parseInt(pUrl.port), host: pUrl.hostname };
+ const proxySocket = new net.Socket();
proxySocket.connect(connectOpts, () => {
- proxySocket.write(`${req.method} ${pUrl.path} HTTP/1.1\r\n`)
- proxySocket.write(`Host: ${apiUrl.host}\r\n`)
+ proxySocket.write(`${req.method} ${pUrl.path} HTTP/1.1\r\n`);
+ proxySocket.write(`Host: ${apiUrl.host}\r\n`);
for (let i = 0; i < req.rawHeaders.length; i += 2) {
- const key = req.rawHeaders[i]
+ const key = req.rawHeaders[i];
if (key !== "Host" && key !== "Authorization") {
- proxySocket.write(`${req.rawHeaders[i]}: ${req.rawHeaders[i+1]}\r\n`)
+ proxySocket.write(`${req.rawHeaders[i]}: ${req.rawHeaders[i+1]}\r\n`);
}
}
- proxySocket.write("\r\n")
- proxySocket.write(head)
- })
+ proxySocket.write("\r\n");
+ proxySocket.write(head);
+ });
- proxySocket.setKeepAlive(true)
- socket.setKeepAlive(true)
- proxySocket.setTimeout(0)
- socket.setTimeout(0)
+ proxySocket.setKeepAlive(true);
+ socket.setKeepAlive(true);
+ proxySocket.setTimeout(0);
+ socket.setTimeout(0);
proxySocket.on('data', function (chunk) {
- socket.write(chunk)
- })
+ socket.write(chunk);
+ });
proxySocket.on('end', function () {
- socket.end()
- })
+ socket.end();
+ });
proxySocket.on('error', function (err) {
socket.write("HTTP/" + req.httpVersion + " 500 Connection error\r\n\r\n");
- socket.end()
- })
+ socket.end();
+ });
socket.on('data', function (chunk) {
- proxySocket.write(chunk)
- })
+ proxySocket.write(chunk);
+ });
socket.on('end', function () {
- proxySocket.end()
- })
+ proxySocket.end();
+ });
socket.on('error', function () {
- proxySocket.end()
- })
+ proxySocket.end();
+ });
}
}
@@ -120,25 +120,29 @@ export class LensProxy {
logger.debug("Failed proxy to target: " + JSON.stringify(target, null, 2));
if (req.method === "GET" && (!res.statusCode || res.statusCode >= 500)) {
const reqId = this.getRequestId(req);
- const retryCount = this.retryCounters.get(reqId) || 0
- const timeoutMs = retryCount * 250
+ const retryCount = this.retryCounters.get(reqId) || 0;
+ const timeoutMs = retryCount * 250;
if (retryCount < 20) {
- logger.debug(`Retrying proxy request to url: ${reqId}`)
+ logger.debug(`Retrying proxy request to url: ${reqId}`);
setTimeout(() => {
- this.retryCounters.set(reqId, retryCount + 1)
- this.handleRequest(proxy, req, res)
- }, timeoutMs)
+ this.retryCounters.set(reqId, retryCount + 1);
+ this.handleRequest(proxy, req, res);
+ }, timeoutMs);
}
}
}
- res.writeHead(500).end("Oops, something went wrong.")
- })
+ try {
+ res.writeHead(500).end("Oops, something went wrong.");
+ } catch (e) {
+ logger.error(`[LENS-PROXY]: Failed to write headers: `, e);
+ }
+ });
return proxy;
}
protected createWsListener(): WebSocket.Server {
- const ws = new WebSocket.Server({ noServer: true })
+ const ws = new WebSocket.Server({ noServer: true });
return ws.on("connection", ((socket: WebSocket, req: http.IncomingMessage) => {
const cluster = this.clusterManager.getClusterForRequest(req);
const nodeParam = url.parse(req.url, true).query["node"]?.toString();
@@ -148,10 +152,10 @@ export class LensProxy {
protected async getProxyTarget(req: http.IncomingMessage, contextHandler: ContextHandler): Promise {
if (req.url.startsWith(apiKubePrefix)) {
- delete req.headers.authorization
- req.url = req.url.replace(apiKubePrefix, "")
- const isWatchRequest = req.url.includes("watch=")
- return await contextHandler.getApiTarget(isWatchRequest)
+ delete req.headers.authorization;
+ req.url = req.url.replace(apiKubePrefix, "");
+ const isWatchRequest = req.url.includes("watch=");
+ return await contextHandler.getApiTarget(isWatchRequest);
}
}
@@ -160,9 +164,9 @@ export class LensProxy {
}
protected async handleRequest(proxy: httpProxy, req: http.IncomingMessage, res: http.ServerResponse) {
- const cluster = this.clusterManager.getClusterForRequest(req)
+ const cluster = this.clusterManager.getClusterForRequest(req);
if (cluster) {
- const proxyTarget = await this.getProxyTarget(req, cluster.contextHandler)
+ const proxyTarget = await this.getProxyTarget(req, cluster.contextHandler);
if (proxyTarget) {
// allow to fetch apis in "clusterId.localhost:port" from "localhost:port"
res.setHeader("Access-Control-Allow-Origin", this.origin);
diff --git a/src/main/logger.ts b/src/main/logger.ts
index f4e2707c27..81d61e8002 100644
--- a/src/main/logger.ts
+++ b/src/main/logger.ts
@@ -1,13 +1,13 @@
import { app, remote } from "electron";
-import winston from "winston"
+import winston from "winston";
import { isDebugging } from "../common/vars";
-const logLevel = process.env.LOG_LEVEL ? process.env.LOG_LEVEL : isDebugging ? "debug" : "info"
+const logLevel = process.env.LOG_LEVEL ? process.env.LOG_LEVEL : isDebugging ? "debug" : "info";
const consoleOptions: winston.transports.ConsoleTransportOptions = {
handleExceptions: false,
level: logLevel,
-}
+};
const fileOptions: winston.transports.FileTransportOptions = {
handleExceptions: false,
@@ -17,7 +17,7 @@ const fileOptions: winston.transports.FileTransportOptions = {
maxsize: 16 * 1024,
maxFiles: 16,
tailable: true,
-}
+};
const logger = winston.createLogger({
format: winston.format.combine(
@@ -30,4 +30,4 @@ const logger = winston.createLogger({
],
});
-export default logger
+export default logger;
diff --git a/src/main/menu.ts b/src/main/menu.ts
index fbd6af8d3c..06bd9095cb 100644
--- a/src/main/menu.ts
+++ b/src/main/menu.ts
@@ -1,7 +1,7 @@
-import { app, BrowserWindow, dialog, ipcMain, IpcMainEvent, Menu, MenuItem, MenuItemConstructorOptions, webContents, shell } from "electron"
+import { app, BrowserWindow, dialog, ipcMain, IpcMainEvent, Menu, MenuItem, MenuItemConstructorOptions, webContents, shell } from "electron";
import { autorun } from "mobx";
import { WindowManager } from "./window-manager";
-import { appName, isMac, isWindows, isTestEnv } from "../common/vars";
+import { appName, isMac, isWindows, isTestEnv, docsUrl, supportUrl } from "../common/vars";
import { addClusterURL } from "../renderer/components/+add-cluster/add-cluster.route";
import { preferencesURL } from "../renderer/components/+preferences/preferences.route";
import { whatsNewURL } from "../renderer/components/+whats-new/whats-new.route";
@@ -9,8 +9,9 @@ import { clusterSettingsURL } from "../renderer/components/+cluster-settings/clu
import { extensionsURL } from "../renderer/components/+extensions/extensions.route";
import { menuRegistry } from "../extensions/registries/menu-registry";
import logger from "./logger";
+import { exitApp } from "./exit-app";
-export type MenuTopId = "mac" | "file" | "edit" | "view" | "help"
+export type MenuTopId = "mac" | "file" | "edit" | "view" | "help";
export function initMenu(windowManager: WindowManager) {
return autorun(() => buildMenu(windowManager), {
@@ -23,15 +24,16 @@ export function showAbout(browserWindow: BrowserWindow) {
`${appName}: ${app.getVersion()}`,
`Electron: ${process.versions.electron}`,
`Chrome: ${process.versions.chrome}`,
+ `Node: ${process.versions.node}`,
`Copyright 2020 Mirantis, Inc.`,
- ]
+ ];
dialog.showMessageBoxSync(browserWindow, {
title: `${isWindows ? " ".repeat(2) : ""}${appName}`,
type: "info",
buttons: ["Close"],
message: `Lens`,
detail: appInfo.join("\r\n")
- })
+ });
}
export function buildMenu(windowManager: WindowManager) {
@@ -43,7 +45,7 @@ export function buildMenu(windowManager: WindowManager) {
function activeClusterOnly(menuItems: MenuItemConstructorOptions[]) {
if (!windowManager.activeClusterId) {
menuItems.forEach(item => {
- item.enabled = false
+ item.enabled = false;
});
}
return menuItems;
@@ -60,7 +62,7 @@ export function buildMenu(windowManager: WindowManager) {
{
label: "About Lens",
click(menuItem: MenuItem, browserWindow: BrowserWindow) {
- showAbout(browserWindow)
+ showAbout(browserWindow);
}
},
{ type: 'separator' },
@@ -68,14 +70,14 @@ export function buildMenu(windowManager: WindowManager) {
label: 'Preferences',
accelerator: 'CmdOrCtrl+,',
click() {
- navigate(preferencesURL())
+ navigate(preferencesURL());
}
},
{
label: 'Extensions',
accelerator: 'CmdOrCtrl+Shift+E',
click() {
- navigate(extensionsURL())
+ navigate(extensionsURL());
}
},
{ type: 'separator' },
@@ -89,7 +91,7 @@ export function buildMenu(windowManager: WindowManager) {
label: 'Quit',
accelerator: 'Cmd+Q',
click() {
- app.exit(); // force quit since might be blocked within app.on("will-quit")
+ exitApp();
}
}
]
@@ -102,7 +104,7 @@ export function buildMenu(windowManager: WindowManager) {
label: 'Add Cluster',
accelerator: 'CmdOrCtrl+Shift+A',
click() {
- navigate(addClusterURL())
+ navigate(addClusterURL());
}
},
...activeClusterOnly([
@@ -114,7 +116,7 @@ export function buildMenu(windowManager: WindowManager) {
params: {
clusterId: windowManager.activeClusterId
}
- }))
+ }));
}
}
]),
@@ -124,21 +126,32 @@ export function buildMenu(windowManager: WindowManager) {
label: 'Preferences',
accelerator: 'Ctrl+,',
click() {
- navigate(preferencesURL())
+ navigate(preferencesURL());
}
},
{
label: 'Extensions',
accelerator: 'Ctrl+Shift+E',
click() {
- navigate(extensionsURL())
+ navigate(extensionsURL());
}
- },
- { type: 'separator' },
- { role: 'quit' }
+ }
]),
{ type: 'separator' },
- { role: 'close' } // close current window
+ {
+ role: 'close',
+ label: "Close Window"
+ },
+ ...ignoreOnMac([
+ { type: 'separator' },
+ {
+ label: 'Exit',
+ accelerator: 'Alt+F4',
+ click() {
+ exitApp();
+ }
+ }
+ ])
]
};
@@ -171,7 +184,7 @@ export function buildMenu(windowManager: WindowManager) {
label: 'Forward',
accelerator: 'CmdOrCtrl+]',
click() {
- webContents.getFocusedWebContents()?.goForward()
+ webContents.getFocusedWebContents()?.goForward();
}
},
{
@@ -197,20 +210,26 @@ export function buildMenu(windowManager: WindowManager) {
{
label: "What's new?",
click() {
- navigate(whatsNewURL())
+ navigate(whatsNewURL());
},
},
{
label: "Documentation",
click: async () => {
- shell.openExternal('https://docs.k8slens.dev/');
+ shell.openExternal(docsUrl);
+ },
+ },
+ {
+ label: "Support",
+ click: async () => {
+ shell.openExternal(supportUrl);
},
},
...ignoreOnMac([
{
label: "About Lens",
click(menuItem: MenuItem, browserWindow: BrowserWindow) {
- showAbout(browserWindow)
+ showAbout(browserWindow);
}
}
])
@@ -224,7 +243,7 @@ export function buildMenu(windowManager: WindowManager) {
edit: editMenu,
view: viewMenu,
help: helpMenu,
- }
+ };
// Modify menu from extensions-api
menuRegistry.getItems().forEach(({ parentId, ...menuItem }) => {
@@ -232,12 +251,12 @@ export function buildMenu(windowManager: WindowManager) {
const topMenu = appMenu[parentId as MenuTopId].submenu as MenuItemConstructorOptions[];
topMenu.push(menuItem);
} catch (err) {
- logger.error(`[MENU]: can't register menu item, parentId=${parentId}`, { menuItem })
+ logger.error(`[MENU]: can't register menu item, parentId=${parentId}`, { menuItem });
}
- })
+ });
if (!isMac) {
- delete appMenu.mac
+ delete appMenu.mac;
}
const menu = Menu.buildFromTemplate(Object.values(appMenu));
@@ -247,9 +266,9 @@ export function buildMenu(windowManager: WindowManager) {
// this is a workaround for the test environment (spectron) not being able to directly access
// the application menus (https://github.com/electron-userland/spectron/issues/21)
ipcMain.on('test-menu-item-click', (event: IpcMainEvent, ...names: string[]) => {
- let menu: Menu = Menu.getApplicationMenu()
+ let menu: Menu = Menu.getApplicationMenu();
const parentLabels: string[] = [];
- let menuItem: MenuItem
+ let menuItem: MenuItem;
for (const name of names) {
parentLabels.push(name);
@@ -259,8 +278,8 @@ export function buildMenu(windowManager: WindowManager) {
}
menu = menuItem.submenu;
}
-
- const menuPath: string = parentLabels.join(" -> ")
+
+ const menuPath: string = parentLabels.join(" -> ");
if (!menuItem) {
logger.info(`[MENU:test-menu-item-click] Cannot find menu item ${menuPath}`);
return;
diff --git a/src/main/node-shell-session.ts b/src/main/node-shell-session.ts
index 9e97398327..3d48afebc5 100644
--- a/src/main/node-shell-session.ts
+++ b/src/main/node-shell-session.ts
@@ -1,36 +1,36 @@
-import * as WebSocket from "ws"
-import * as pty from "node-pty"
+import * as WebSocket from "ws";
+import * as pty from "node-pty";
import { ShellSession } from "./shell-session";
-import { v4 as uuid } from "uuid"
-import * as k8s from "@kubernetes/client-node"
-import { KubeConfig } from "@kubernetes/client-node"
-import { Cluster } from "./cluster"
+import { v4 as uuid } from "uuid";
+import * as k8s from "@kubernetes/client-node";
+import { KubeConfig } from "@kubernetes/client-node";
+import { Cluster } from "./cluster";
import logger from "./logger";
-import { appEventBus } from "../common/event-bus"
+import { appEventBus } from "../common/event-bus";
export class NodeShellSession extends ShellSession {
protected nodeName: string;
- protected podId: string
- protected kc: KubeConfig
+ protected podId: string;
+ protected kc: KubeConfig;
constructor(socket: WebSocket, cluster: Cluster, nodeName: string) {
- super(socket, cluster)
- this.nodeName = nodeName
- this.podId = `node-shell-${uuid()}`
- this.kc = cluster.getProxyKubeconfig()
+ super(socket, cluster);
+ this.nodeName = nodeName;
+ this.podId = `node-shell-${uuid()}`;
+ this.kc = cluster.getProxyKubeconfig();
}
public async open() {
- const shell = await this.kubectl.getPath()
- let args = []
+ const shell = await this.kubectl.getPath();
+ let args = [];
if (this.createNodeShellPod(this.podId, this.nodeName)) {
await this.waitForRunningPod(this.podId).catch((error) => {
- this.exit(1001)
- })
+ this.exit(1001);
+ });
}
- args = ["exec", "-i", "-t", "-n", "kube-system", this.podId, "--", "sh", "-c", "((clear && bash) || (clear && ash) || (clear && sh))"]
+ args = ["exec", "-i", "-t", "-n", "kube-system", this.podId, "--", "sh", "-c", "((clear && bash) || (clear && ash) || (clear && sh))"];
- const shellEnv = await this.getCachedShellEnv()
+ const shellEnv = await this.getCachedShellEnv();
this.shellProcess = pty.spawn(shell, args, {
cols: 80,
cwd: this.cwd() || shellEnv["HOME"],
@@ -39,19 +39,19 @@ export class NodeShellSession extends ShellSession {
rows: 30,
});
this.running = true;
- this.pipeStdout()
- this.pipeStdin()
- this.closeWebsocketOnProcessExit()
- this.exitProcessOnWebsocketClose()
+ this.pipeStdout();
+ this.pipeStdin();
+ this.closeWebsocketOnProcessExit();
+ this.exitProcessOnWebsocketClose();
- appEventBus.emit({name: "node-shell", action: "open"})
+ appEventBus.emit({name: "node-shell", action: "open"});
}
protected exit(code = 1000) {
if (this.podId) {
- this.deleteNodeShellPod()
+ this.deleteNodeShellPod();
}
- super.exit(code)
+ super.exit(code);
}
protected async createNodeShellPod(podId: string, nodeName: string) {
@@ -86,19 +86,19 @@ export class NodeShellSession extends ShellSession {
}
} as k8s.V1Pod;
await k8sApi.createNamespacedPod("kube-system", pod).catch((error) => {
- logger.error(error)
- return false
- })
- return true
+ logger.error(error);
+ return false;
+ });
+ return true;
}
protected getKubeConfig() {
if (this.kc) {
- return this.kc
+ return this.kc;
}
this.kc = new k8s.KubeConfig();
- this.kc.loadFromFile(this.kubeconfigPath)
- return this.kc
+ this.kc.loadFromFile(this.kubeconfigPath);
+ return this.kc;
}
protected waitForRunningPod(podId: string) {
@@ -110,36 +110,36 @@ export class NodeShellSession extends ShellSession {
// callback is called for each received object.
(_type, obj) => {
if (obj.metadata.name == podId && obj.status.phase === "Running") {
- resolve(true)
+ resolve(true);
}
},
// done callback is called if the watch terminates normally
(err) => {
- logger.error(err)
- reject(false)
+ logger.error(err);
+ reject(false);
}
);
setTimeout(() => {
req.abort();
reject(false);
}, 120 * 1000);
- })
+ });
}
protected deleteNodeShellPod() {
const kc = this.getKubeConfig();
const k8sApi = kc.makeApiClient(k8s.CoreV1Api);
- k8sApi.deleteNamespacedPod(this.podId, "kube-system")
+ k8sApi.deleteNamespacedPod(this.podId, "kube-system");
}
}
export async function openShell(socket: WebSocket, cluster: Cluster, nodeName?: string): Promise {
let shell: ShellSession;
if (nodeName) {
- shell = new NodeShellSession(socket, cluster, nodeName)
+ shell = new NodeShellSession(socket, cluster, nodeName);
} else {
shell = new ShellSession(socket, cluster);
}
- shell.open()
+ shell.open();
return shell;
}
diff --git a/src/main/port.ts b/src/main/port.ts
index b253d3590a..6ba8f71695 100644
--- a/src/main/port.ts
+++ b/src/main/port.ts
@@ -1,15 +1,15 @@
-import net, { AddressInfo } from "net"
-import logger from "./logger"
+import net, { AddressInfo } from "net";
+import logger from "./logger";
// todo: check https://github.com/http-party/node-portfinder ?
export async function getFreePort(): Promise {
logger.debug("Lookup new free port..");
return new Promise((resolve, reject) => {
- const server = net.createServer()
- server.unref()
+ const server = net.createServer();
+ server.unref();
server.on("listening", () => {
- const port = (server.address() as AddressInfo).port
+ const port = (server.address() as AddressInfo).port;
server.close(() => resolve(port));
logger.debug(`New port found: ${port}`);
});
@@ -17,6 +17,6 @@ export async function getFreePort(): Promise {
logger.error(`Can't resolve new port: "${error}"`);
reject(error);
});
- server.listen({ host: "127.0.0.1", port: 0 })
- })
+ server.listen({ host: "127.0.0.1", port: 0 });
+ });
}
diff --git a/src/main/port_spec.ts b/src/main/port_spec.ts
index c9be25e514..bf01eb5dde 100644
--- a/src/main/port_spec.ts
+++ b/src/main/port_spec.ts
@@ -1,5 +1,5 @@
-import { EventEmitter } from 'events'
-import { getFreePort } from "./port"
+import { EventEmitter } from 'events';
+import { getFreePort } from "./port";
let newPort = 0;
@@ -8,24 +8,24 @@ jest.mock("net", () => {
createServer() {
return new class MockServer extends EventEmitter {
listen = jest.fn(() => {
- this.emit('listening')
- return this
- })
+ this.emit('listening');
+ return this;
+ });
address = () => {
- newPort = Math.round(Math.random() * 10000)
+ newPort = Math.round(Math.random() * 10000);
return {
port: newPort
- }
- }
- unref = jest.fn()
- close = jest.fn(cb => cb())
- }
+ };
+ };
+ unref = jest.fn();
+ close = jest.fn(cb => cb());
+ };
},
- }
+ };
});
describe("getFreePort", () => {
it("finds the next free port", async () => {
return expect(getFreePort()).resolves.toEqual(newPort);
- })
-})
+ });
+});
diff --git a/src/main/prometheus/helm.ts b/src/main/prometheus/helm.ts
index f1462931df..56d739c630 100644
--- a/src/main/prometheus/helm.ts
+++ b/src/main/prometheus/helm.ts
@@ -1,29 +1,29 @@
-import { PrometheusLens } from "./lens"
-import { CoreV1Api } from "@kubernetes/client-node"
+import { PrometheusLens } from "./lens";
+import { CoreV1Api } from "@kubernetes/client-node";
import { PrometheusService } from "./provider-registry";
-import logger from "../logger"
+import logger from "../logger";
export class PrometheusHelm extends PrometheusLens {
- id = "helm"
- name = "Helm"
- rateAccuracy = "5m"
+ id = "helm";
+ name = "Helm";
+ rateAccuracy = "5m";
public async getPrometheusService(client: CoreV1Api): Promise {
- const labelSelector = "app=prometheus,component=server,heritage=Helm"
+ const labelSelector = "app=prometheus,component=server,heritage=Helm";
try {
- const serviceList = await client.listServiceForAllNamespaces(false, "", null, labelSelector)
- const service = serviceList.body.items[0]
- if (!service) return
+ const serviceList = await client.listServiceForAllNamespaces(false, "", null, labelSelector);
+ const service = serviceList.body.items[0];
+ if (!service) return;
return {
id: this.id,
namespace: service.metadata.namespace,
service: service.metadata.name,
port: service.spec.ports[0].port
- }
+ };
} catch(error) {
- logger.warn(`PrometheusHelm: failed to list services: ${error.toString()}`)
- return
+ logger.warn(`PrometheusHelm: failed to list services: ${error.toString()}`);
+ return;
}
}
}
diff --git a/src/main/prometheus/lens.ts b/src/main/prometheus/lens.ts
index 4db70bf45d..eddd2457a3 100644
--- a/src/main/prometheus/lens.ts
+++ b/src/main/prometheus/lens.ts
@@ -1,83 +1,83 @@
import { PrometheusProvider, PrometheusQueryOpts, PrometheusQuery, PrometheusService } from "./provider-registry";
import { CoreV1Api } from "@kubernetes/client-node";
-import logger from "../logger"
+import logger from "../logger";
export class PrometheusLens implements PrometheusProvider {
- id = "lens"
- name = "Lens"
- rateAccuracy = "1m"
+ id = "lens";
+ name = "Lens";
+ rateAccuracy = "1m";
public async getPrometheusService(client: CoreV1Api): Promise {
try {
- const resp = await client.readNamespacedService("prometheus", "lens-metrics")
- const service = resp.body
+ const resp = await client.readNamespacedService("prometheus", "lens-metrics");
+ const service = resp.body;
return {
id: this.id,
namespace: service.metadata.namespace,
service: service.metadata.name,
port: service.spec.ports[0].port
- }
+ };
} catch(error) {
- logger.warn(`PrometheusLens: failed to list services: ${error.response.body.message}`)
+ logger.warn(`PrometheusLens: failed to list services: ${error.response.body.message}`);
}
}
public getQueries(opts: PrometheusQueryOpts): PrometheusQuery {
switch(opts.category) {
- case 'cluster':
- return {
- memoryUsage: `
+ case 'cluster':
+ return {
+ memoryUsage: `
sum(
node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)
) by (kubernetes_name)
`.replace(/_bytes/g, `_bytes{kubernetes_node=~"${opts.nodes}"}`),
- memoryRequests: `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"}) by (component)`,
- memoryLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="memory"}) by (component)`,
- memoryCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="memory"}) by (component)`,
- cpuUsage: `sum(rate(node_cpu_seconds_total{kubernetes_node=~"${opts.nodes}", mode=~"user|system"}[${this.rateAccuracy}]))`,
- cpuRequests:`sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"}) by (component)`,
- cpuLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="cpu"}) by (component)`,
- cpuCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="cpu"}) by (component)`,
- podUsage: `sum(kubelet_running_pod_count{instance=~"${opts.nodes}"})`,
- podCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="pods"}) by (component)`,
- fsSize: `sum(node_filesystem_size_bytes{kubernetes_node=~"${opts.nodes}", mountpoint="/"}) by (kubernetes_node)`,
- fsUsage: `sum(node_filesystem_size_bytes{kubernetes_node=~"${opts.nodes}", mountpoint="/"} - node_filesystem_avail_bytes{kubernetes_node=~"${opts.nodes}", mountpoint="/"}) by (kubernetes_node)`
- }
- case 'nodes':
- return {
- memoryUsage: `sum (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (kubernetes_node)`,
- memoryCapacity: `sum(kube_node_status_capacity{resource="memory"}) by (node)`,
- cpuUsage: `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])) by(kubernetes_node)`,
- cpuCapacity: `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`,
- fsSize: `sum(node_filesystem_size_bytes{mountpoint="/"}) by (kubernetes_node)`,
- fsUsage: `sum(node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) by (kubernetes_node)`
- }
- case 'pods':
- return {
- cpuUsage: `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`,
- cpuRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`,
- cpuLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`,
- memoryUsage: `sum(container_memory_working_set_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`,
- memoryRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`,
- memoryLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`,
- fsUsage: `sum(container_fs_usage_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`,
- networkReceive: `sum(rate(container_network_receive_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`,
- networkTransmit: `sum(rate(container_network_transmit_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`
- }
- case 'pvc':
- return {
- diskUsage: `sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim="${opts.pvc}"}) by (persistentvolumeclaim, namespace)`,
- diskCapacity: `sum(kubelet_volume_stats_capacity_bytes{persistentvolumeclaim="${opts.pvc}"}) by (persistentvolumeclaim, namespace)`
- }
- case 'ingress':
- const bytesSent = (ingress: string, statuses: string) =>
- `sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}", status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress)`
- return {
- bytesSentSuccess: bytesSent(opts.igress, "^2\\\\d*"),
- bytesSentFailure: bytesSent(opts.ingres, "^5\\\\d*"),
- requestDurationSeconds: `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}"}[${this.rateAccuracy}])) by (ingress)`,
- responseDurationSeconds: `sum(rate(nginx_ingress_controller_response_duration_seconds_sum{ingress="${opts.ingress}"}[${this.rateAccuracy}])) by (ingress)`
- }
+ memoryRequests: `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"}) by (component)`,
+ memoryLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="memory"}) by (component)`,
+ memoryCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="memory"}) by (component)`,
+ cpuUsage: `sum(rate(node_cpu_seconds_total{kubernetes_node=~"${opts.nodes}", mode=~"user|system"}[${this.rateAccuracy}]))`,
+ cpuRequests:`sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"}) by (component)`,
+ cpuLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="cpu"}) by (component)`,
+ cpuCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="cpu"}) by (component)`,
+ podUsage: `sum({__name__=~"kubelet_running_pod_count|kubelet_running_pods", instance=~"${opts.nodes}"})`,
+ podCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="pods"}) by (component)`,
+ fsSize: `sum(node_filesystem_size_bytes{kubernetes_node=~"${opts.nodes}", mountpoint="/"}) by (kubernetes_node)`,
+ fsUsage: `sum(node_filesystem_size_bytes{kubernetes_node=~"${opts.nodes}", mountpoint="/"} - node_filesystem_avail_bytes{kubernetes_node=~"${opts.nodes}", mountpoint="/"}) by (kubernetes_node)`
+ };
+ case 'nodes':
+ return {
+ memoryUsage: `sum (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (kubernetes_node)`,
+ memoryCapacity: `sum(kube_node_status_capacity{resource="memory"}) by (node)`,
+ cpuUsage: `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])) by(kubernetes_node)`,
+ cpuCapacity: `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`,
+ fsSize: `sum(node_filesystem_size_bytes{mountpoint="/"}) by (kubernetes_node)`,
+ fsUsage: `sum(node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) by (kubernetes_node)`
+ };
+ case 'pods':
+ return {
+ cpuUsage: `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`,
+ cpuRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`,
+ cpuLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`,
+ memoryUsage: `sum(container_memory_working_set_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`,
+ memoryRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`,
+ memoryLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`,
+ fsUsage: `sum(container_fs_usage_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`,
+ networkReceive: `sum(rate(container_network_receive_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`,
+ networkTransmit: `sum(rate(container_network_transmit_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`
+ };
+ case 'pvc':
+ return {
+ diskUsage: `sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim="${opts.pvc}"}) by (persistentvolumeclaim, namespace)`,
+ diskCapacity: `sum(kubelet_volume_stats_capacity_bytes{persistentvolumeclaim="${opts.pvc}"}) by (persistentvolumeclaim, namespace)`
+ };
+ case 'ingress':
+ const bytesSent = (ingress: string, statuses: string) =>
+ `sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}", status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress)`;
+ return {
+ bytesSentSuccess: bytesSent(opts.igress, "^2\\\\d*"),
+ bytesSentFailure: bytesSent(opts.ingres, "^5\\\\d*"),
+ requestDurationSeconds: `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}"}[${this.rateAccuracy}])) by (ingress)`,
+ responseDurationSeconds: `sum(rate(nginx_ingress_controller_response_duration_seconds_sum{ingress="${opts.ingress}"}[${this.rateAccuracy}])) by (ingress)`
+ };
}
}
}
diff --git a/src/main/prometheus/operator.ts b/src/main/prometheus/operator.ts
index 3d335ff554..cb7b9944f7 100644
--- a/src/main/prometheus/operator.ts
+++ b/src/main/prometheus/operator.ts
@@ -3,89 +3,89 @@ import { CoreV1Api, V1Service } from "@kubernetes/client-node";
import logger from "../logger";
export class PrometheusOperator implements PrometheusProvider {
- rateAccuracy = "1m"
- id = "operator"
- name = "Prometheus Operator"
+ rateAccuracy = "1m";
+ id = "operator";
+ name = "Prometheus Operator";
public async getPrometheusService(client: CoreV1Api): Promise {
try {
- let service: V1Service
+ let service: V1Service;
for (const labelSelector of ["operated-prometheus=true", "self-monitor=true"]) {
if (!service) {
- const serviceList = await client.listServiceForAllNamespaces(null, null, null, labelSelector)
- service = serviceList.body.items[0]
+ const serviceList = await client.listServiceForAllNamespaces(null, null, null, labelSelector);
+ service = serviceList.body.items[0];
}
}
- if (!service) return
+ if (!service) return;
return {
id: this.id,
namespace: service.metadata.namespace,
service: service.metadata.name,
port: service.spec.ports[0].port
- }
+ };
} catch(error) {
- logger.warn(`PrometheusOperator: failed to list services: ${error.toString()}`)
- return
+ logger.warn(`PrometheusOperator: failed to list services: ${error.toString()}`);
+ return;
}
}
public getQueries(opts: PrometheusQueryOpts): PrometheusQuery {
switch(opts.category) {
- case 'cluster':
- return {
- memoryUsage: `
+ case 'cluster':
+ return {
+ memoryUsage: `
sum(
node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)
)
`.replace(/_bytes/g, `_bytes * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"}`),
- memoryRequests: `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"})`,
- memoryLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="memory"})`,
- memoryCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="memory"})`,
- cpuUsage: `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])* on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`,
- cpuRequests:`sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"})`,
- cpuLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="cpu"})`,
- cpuCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="cpu"})`,
- podUsage: `sum(kubelet_running_pod_count{node=~"${opts.nodes}"})`,
- podCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="pods"})`,
- fsSize: `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`,
- fsUsage: `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"} - node_filesystem_avail_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`
- }
- case 'nodes':
- return {
- memoryUsage: `sum((node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) * on (pod,namespace) group_left(node) kube_pod_info) by (node)`,
- memoryCapacity: `sum(kube_node_status_capacity{resource="memory"}) by (node)`,
- cpuUsage: `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}]) * on (pod,namespace) group_left(node) kube_pod_info) by (node)`,
- cpuCapacity: `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`,
- fsSize: `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info) by (node)`,
- fsUsage: `sum((node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) * on (pod,namespace) group_left(node) kube_pod_info) by (node)`
- }
- case 'pods':
- return {
- cpuUsage: `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",image!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`,
- cpuRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`,
- cpuLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`,
- memoryUsage: `sum(container_memory_working_set_bytes{container!="POD",container!="",image!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`,
- memoryRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`,
- memoryLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`,
- fsUsage: `sum(container_fs_usage_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`,
- networkReceive: `sum(rate(container_network_receive_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`,
- networkTransmit: `sum(rate(container_network_transmit_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`
- }
- case 'pvc':
- return {
- diskUsage: `sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim="${opts.pvc}"}) by (persistentvolumeclaim, namespace)`,
- diskCapacity: `sum(kubelet_volume_stats_capacity_bytes{persistentvolumeclaim="${opts.pvc}"}) by (persistentvolumeclaim, namespace)`
- }
- case 'ingress':
- const bytesSent = (ingress: string, statuses: string) =>
- `sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}", status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress)`;
- return {
- bytesSentSuccess: bytesSent(opts.igress, "^2\\\\d*"),
- bytesSentFailure: bytesSent(opts.ingres, "^5\\\\d*"),
- requestDurationSeconds: `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}"}[${this.rateAccuracy}])) by (ingress)`,
- responseDurationSeconds: `sum(rate(nginx_ingress_controller_response_duration_seconds_sum{ingress="${opts.ingress}"}[${this.rateAccuracy}])) by (ingress)`
- }
+ memoryRequests: `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"})`,
+ memoryLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="memory"})`,
+ memoryCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="memory"})`,
+ cpuUsage: `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])* on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`,
+ cpuRequests:`sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"})`,
+ cpuLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="cpu"})`,
+ cpuCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="cpu"})`,
+ podUsage: `sum({__name__=~"kubelet_running_pod_count|kubelet_running_pods", node=~"${opts.nodes}"})`,
+ podCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="pods"})`,
+ fsSize: `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`,
+ fsUsage: `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"} - node_filesystem_avail_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`
+ };
+ case 'nodes':
+ return {
+ memoryUsage: `sum((node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) * on (pod,namespace) group_left(node) kube_pod_info) by (node)`,
+ memoryCapacity: `sum(kube_node_status_capacity{resource="memory"}) by (node)`,
+ cpuUsage: `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}]) * on (pod,namespace) group_left(node) kube_pod_info) by (node)`,
+ cpuCapacity: `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`,
+ fsSize: `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info) by (node)`,
+ fsUsage: `sum((node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) * on (pod,namespace) group_left(node) kube_pod_info) by (node)`
+ };
+ case 'pods':
+ return {
+ cpuUsage: `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",image!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`,
+ cpuRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`,
+ cpuLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`,
+ memoryUsage: `sum(container_memory_working_set_bytes{container!="POD",container!="",image!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`,
+ memoryRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`,
+ memoryLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`,
+ fsUsage: `sum(container_fs_usage_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`,
+ networkReceive: `sum(rate(container_network_receive_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`,
+ networkTransmit: `sum(rate(container_network_transmit_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`
+ };
+ case 'pvc':
+ return {
+ diskUsage: `sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim="${opts.pvc}"}) by (persistentvolumeclaim, namespace)`,
+ diskCapacity: `sum(kubelet_volume_stats_capacity_bytes{persistentvolumeclaim="${opts.pvc}"}) by (persistentvolumeclaim, namespace)`
+ };
+ case 'ingress':
+ const bytesSent = (ingress: string, statuses: string) =>
+ `sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}", status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress)`;
+ return {
+ bytesSentSuccess: bytesSent(opts.igress, "^2\\\\d*"),
+ bytesSentFailure: bytesSent(opts.ingres, "^5\\\\d*"),
+ requestDurationSeconds: `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}"}[${this.rateAccuracy}])) by (ingress)`,
+ responseDurationSeconds: `sum(rate(nginx_ingress_controller_response_duration_seconds_sum{ingress="${opts.ingress}"}[${this.rateAccuracy}])) by (ingress)`
+ };
}
}
}
diff --git a/src/main/prometheus/provider-registry.ts b/src/main/prometheus/provider-registry.ts
index 0a4eb22e95..641b1b8cf2 100644
--- a/src/main/prometheus/provider-registry.ts
+++ b/src/main/prometheus/provider-registry.ts
@@ -1,4 +1,4 @@
-import { CoreV1Api } from "@kubernetes/client-node"
+import { CoreV1Api } from "@kubernetes/client-node";
export type PrometheusClusterQuery = {
memoryUsage: string;
@@ -11,7 +11,7 @@ export type PrometheusClusterQuery = {
cpuCapacity: string;
podUsage: string;
podCapacity: string;
-}
+};
export type PrometheusNodeQuery = {
memoryUsage: string;
@@ -20,7 +20,7 @@ export type PrometheusNodeQuery = {
cpuCapacity: string;
fsSize: string;
fsUsage: string;
-}
+};
export type PrometheusPodQuery = {
memoryUsage: string;
@@ -32,32 +32,32 @@ export type PrometheusPodQuery = {
fsUsage: string;
networkReceive: string;
networkTransmit: string;
-}
+};
export type PrometheusPvcQuery = {
diskUsage: string;
diskCapacity: string;
-}
+};
export type PrometheusIngressQuery = {
bytesSentSuccess: string;
bytesSentFailure: string;
requestDurationSeconds: string;
responseDurationSeconds: string;
-}
+};
export type PrometheusQueryOpts = {
[key: string]: string | any;
};
-export type PrometheusQuery = PrometheusNodeQuery | PrometheusClusterQuery | PrometheusPodQuery | PrometheusPvcQuery | PrometheusIngressQuery
+export type PrometheusQuery = PrometheusNodeQuery | PrometheusClusterQuery | PrometheusPodQuery | PrometheusPvcQuery | PrometheusIngressQuery;
export type PrometheusService = {
id: string;
namespace: string;
service: string;
port: number;
-}
+};
export interface PrometheusProvider {
id: string;
@@ -68,23 +68,23 @@ export interface PrometheusProvider {
export type PrometheusProviderList = {
[key: string]: PrometheusProvider;
-}
+};
export class PrometheusProviderRegistry {
- private static prometheusProviders: PrometheusProviderList = {}
+ private static prometheusProviders: PrometheusProviderList = {};
static getProvider(type: string): PrometheusProvider {
if (!this.prometheusProviders[type]) {
throw "Unknown Prometheus provider";
}
- return this.prometheusProviders[type]
+ return this.prometheusProviders[type];
}
static registerProvider(key: string, provider: PrometheusProvider) {
- this.prometheusProviders[key] = provider
+ this.prometheusProviders[key] = provider;
}
static getProviders(): PrometheusProvider[] {
- return Object.values(this.prometheusProviders)
+ return Object.values(this.prometheusProviders);
}
}
diff --git a/src/main/prometheus/stacklight.ts b/src/main/prometheus/stacklight.ts
index 4cb946c81d..116ad728bb 100644
--- a/src/main/prometheus/stacklight.ts
+++ b/src/main/prometheus/stacklight.ts
@@ -1,83 +1,83 @@
import { PrometheusProvider, PrometheusQueryOpts, PrometheusQuery, PrometheusService } from "./provider-registry";
import { CoreV1Api } from "@kubernetes/client-node";
-import logger from "../logger"
+import logger from "../logger";
export class PrometheusStacklight implements PrometheusProvider {
- id = "stacklight"
- name = "Stacklight"
- rateAccuracy = "1m"
+ id = "stacklight";
+ name = "Stacklight";
+ rateAccuracy = "1m";
public async getPrometheusService(client: CoreV1Api): Promise {
try {
- const resp = await client.readNamespacedService("prometheus-server", "stacklight")
- const service = resp.body
+ const resp = await client.readNamespacedService("prometheus-server", "stacklight");
+ const service = resp.body;
return {
id: this.id,
namespace: service.metadata.namespace,
service: service.metadata.name,
port: service.spec.ports[0].port
- }
+ };
} catch(error) {
- logger.warn(`PrometheusLens: failed to list services: ${error.response.body.message}`)
+ logger.warn(`PrometheusLens: failed to list services: ${error.response.body.message}`);
}
}
public getQueries(opts: PrometheusQueryOpts): PrometheusQuery {
switch(opts.category) {
- case 'cluster':
- return {
- memoryUsage: `
+ case 'cluster':
+ return {
+ memoryUsage: `
sum(
node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)
) by (kubernetes_name)
`.replace(/_bytes/g, `_bytes{node=~"${opts.nodes}"}`),
- memoryRequests: `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"}) by (component)`,
- memoryLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="memory"}) by (component)`,
- memoryCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="memory"}) by (component)`,
- cpuUsage: `sum(rate(node_cpu_seconds_total{node=~"${opts.nodes}", mode=~"user|system"}[${this.rateAccuracy}]))`,
- cpuRequests:`sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"}) by (component)`,
- cpuLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="cpu"}) by (component)`,
- cpuCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="cpu"}) by (component)`,
- podUsage: `sum(kubelet_running_pod_count{instance=~"${opts.nodes}"})`,
- podCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="pods"}) by (component)`,
- fsSize: `sum(node_filesystem_size_bytes{node=~"${opts.nodes}", mountpoint="/"}) by (node)`,
- fsUsage: `sum(node_filesystem_size_bytes{node=~"${opts.nodes}", mountpoint="/"} - node_filesystem_avail_bytes{node=~"${opts.nodes}", mountpoint="/"}) by (node)`
- }
- case 'nodes':
- return {
- memoryUsage: `sum (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (node)`,
- memoryCapacity: `sum(kube_node_status_capacity{resource="memory"}) by (node)`,
- cpuUsage: `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])) by(node)`,
- cpuCapacity: `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`,
- fsSize: `sum(node_filesystem_size_bytes{mountpoint="/"}) by (node)`,
- fsUsage: `sum(node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) by (node)`
- }
- case 'pods':
- return {
- cpuUsage: `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`,
- cpuRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`,
- cpuLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`,
- memoryUsage: `sum(container_memory_working_set_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`,
- memoryRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`,
- memoryLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`,
- fsUsage: `sum(container_fs_usage_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`,
- networkReceive: `sum(rate(container_network_receive_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`,
- networkTransmit: `sum(rate(container_network_transmit_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`
- }
- case 'pvc':
- return {
- diskUsage: `sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim="${opts.pvc}"}) by (persistentvolumeclaim, namespace)`,
- diskCapacity: `sum(kubelet_volume_stats_capacity_bytes{persistentvolumeclaim="${opts.pvc}"}) by (persistentvolumeclaim, namespace)`
- }
- case 'ingress':
- const bytesSent = (ingress: string, statuses: string) =>
- `sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}", status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress)`
- return {
- bytesSentSuccess: bytesSent(opts.igress, "^2\\\\d*"),
- bytesSentFailure: bytesSent(opts.ingres, "^5\\\\d*"),
- requestDurationSeconds: `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}"}[${this.rateAccuracy}])) by (ingress)`,
- responseDurationSeconds: `sum(rate(nginx_ingress_controller_response_duration_seconds_sum{ingress="${opts.ingress}"}[${this.rateAccuracy}])) by (ingress)`
- }
+ memoryRequests: `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"}) by (component)`,
+ memoryLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="memory"}) by (component)`,
+ memoryCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="memory"}) by (component)`,
+ cpuUsage: `sum(rate(node_cpu_seconds_total{node=~"${opts.nodes}", mode=~"user|system"}[${this.rateAccuracy}]))`,
+ cpuRequests:`sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"}) by (component)`,
+ cpuLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="cpu"}) by (component)`,
+ cpuCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="cpu"}) by (component)`,
+ podUsage: `sum({__name__=~"kubelet_running_pod_count|kubelet_running_pods", instance=~"${opts.nodes}"})`,
+ podCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="pods"}) by (component)`,
+ fsSize: `sum(node_filesystem_size_bytes{node=~"${opts.nodes}", mountpoint="/"}) by (node)`,
+ fsUsage: `sum(node_filesystem_size_bytes{node=~"${opts.nodes}", mountpoint="/"} - node_filesystem_avail_bytes{node=~"${opts.nodes}", mountpoint="/"}) by (node)`
+ };
+ case 'nodes':
+ return {
+ memoryUsage: `sum (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (node)`,
+ memoryCapacity: `sum(kube_node_status_capacity{resource="memory"}) by (node)`,
+ cpuUsage: `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])) by(node)`,
+ cpuCapacity: `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`,
+ fsSize: `sum(node_filesystem_size_bytes{mountpoint="/"}) by (node)`,
+ fsUsage: `sum(node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) by (node)`
+ };
+ case 'pods':
+ return {
+ cpuUsage: `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`,
+ cpuRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`,
+ cpuLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`,
+ memoryUsage: `sum(container_memory_working_set_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`,
+ memoryRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`,
+ memoryLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`,
+ fsUsage: `sum(container_fs_usage_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`,
+ networkReceive: `sum(rate(container_network_receive_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`,
+ networkTransmit: `sum(rate(container_network_transmit_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`
+ };
+ case 'pvc':
+ return {
+ diskUsage: `sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim="${opts.pvc}"}) by (persistentvolumeclaim, namespace)`,
+ diskCapacity: `sum(kubelet_volume_stats_capacity_bytes{persistentvolumeclaim="${opts.pvc}"}) by (persistentvolumeclaim, namespace)`
+ };
+ case 'ingress':
+ const bytesSent = (ingress: string, statuses: string) =>
+ `sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}", status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress)`;
+ return {
+ bytesSentSuccess: bytesSent(opts.igress, "^2\\\\d*"),
+ bytesSentFailure: bytesSent(opts.ingres, "^5\\\\d*"),
+ requestDurationSeconds: `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}"}[${this.rateAccuracy}])) by (ingress)`,
+ responseDurationSeconds: `sum(rate(nginx_ingress_controller_response_duration_seconds_sum{ingress="${opts.ingress}"}[${this.rateAccuracy}])) by (ingress)`
+ };
}
}
}
diff --git a/src/main/promise-exec.ts b/src/main/promise-exec.ts
index b3bc303165..426bca4c23 100644
--- a/src/main/promise-exec.ts
+++ b/src/main/promise-exec.ts
@@ -1,4 +1,4 @@
-import * as util from "util"
+import * as util from "util";
import { exec } from "child_process";
-export const promiseExec = util.promisify(exec)
+export const promiseExec = util.promisify(exec);
diff --git a/src/main/proxy-env.ts b/src/main/proxy-env.ts
index 04c62ef534..51c8286a7f 100644
--- a/src/main/proxy-env.ts
+++ b/src/main/proxy-env.ts
@@ -1,18 +1,18 @@
-import { app } from "electron"
+import { app } from "electron";
-const switchValue = app.commandLine.getSwitchValue("proxy-server")
+const switchValue = app.commandLine.getSwitchValue("proxy-server");
export function mangleProxyEnv() {
- let httpsProxy = process.env.HTTPS_PROXY || process.env.HTTP_PROXY || ""
+ let httpsProxy = process.env.HTTPS_PROXY || process.env.HTTP_PROXY || "";
- delete process.env.HTTPS_PROXY
- delete process.env.HTTP_PROXY
+ delete process.env.HTTPS_PROXY;
+ delete process.env.HTTP_PROXY;
if (switchValue !== "") {
- httpsProxy = switchValue
+ httpsProxy = switchValue;
}
if (httpsProxy !== "") {
- process.env.APP_HTTPS_PROXY = httpsProxy
+ process.env.APP_HTTPS_PROXY = httpsProxy;
}
}
diff --git a/src/main/resource-applier.ts b/src/main/resource-applier.ts
index 41441a3f90..0f4647a60f 100644
--- a/src/main/resource-applier.ts
+++ b/src/main/resource-applier.ts
@@ -1,12 +1,12 @@
import type { Cluster } from "./cluster";
-import { KubernetesObject } from "@kubernetes/client-node"
+import { KubernetesObject } from "@kubernetes/client-node";
import { exec } from "child_process";
import fs from "fs";
import * as yaml from "js-yaml";
import path from "path";
import * as tempy from "tempy";
-import logger from "./logger"
-import { appEventBus } from "../common/event-bus"
+import logger from "./logger";
+import { appEventBus } from "../common/event-bus";
import { cloneJsonObject } from "../common/utils";
export class ResourceApplier {
@@ -15,58 +15,58 @@ export class ResourceApplier {
async apply(resource: KubernetesObject | any): Promise {
resource = this.sanitizeObject(resource);
- appEventBus.emit({name: "resource", action: "apply"})
+ appEventBus.emit({name: "resource", action: "apply"});
return await this.kubectlApply(yaml.safeDump(resource));
}
protected async kubectlApply(content: string): Promise {
const { kubeCtl } = this.cluster;
- const kubectlPath = await kubeCtl.getPath()
+ const kubectlPath = await kubeCtl.getPath();
return new Promise((resolve, reject) => {
- const fileName = tempy.file({ name: "resource.yaml" })
- fs.writeFileSync(fileName, content)
- const cmd = `"${kubectlPath}" apply --kubeconfig "${this.cluster.getProxyKubeconfigPath()}" -o json -f "${fileName}"`
+ const fileName = tempy.file({ name: "resource.yaml" });
+ fs.writeFileSync(fileName, content);
+ const cmd = `"${kubectlPath}" apply --kubeconfig "${this.cluster.getProxyKubeconfigPath()}" -o json -f "${fileName}"`;
logger.debug("shooting manifests with: " + cmd);
- const execEnv: NodeJS.ProcessEnv = Object.assign({}, process.env)
- const httpsProxy = this.cluster.preferences?.httpsProxy
+ const execEnv: NodeJS.ProcessEnv = Object.assign({}, process.env);
+ const httpsProxy = this.cluster.preferences?.httpsProxy;
if (httpsProxy) {
- execEnv["HTTPS_PROXY"] = httpsProxy
+ execEnv["HTTPS_PROXY"] = httpsProxy;
}
exec(cmd, { env: execEnv },
(error, stdout, stderr) => {
if (stderr != "") {
- fs.unlinkSync(fileName)
- reject(stderr)
- return
+ fs.unlinkSync(fileName);
+ reject(stderr);
+ return;
}
- fs.unlinkSync(fileName)
- resolve(JSON.parse(stdout))
- })
- })
+ fs.unlinkSync(fileName);
+ resolve(JSON.parse(stdout));
+ });
+ });
}
public async kubectlApplyAll(resources: string[]): Promise {
const { kubeCtl } = this.cluster;
- const kubectlPath = await kubeCtl.getPath()
+ const kubectlPath = await kubeCtl.getPath();
return new Promise((resolve, reject) => {
- const tmpDir = tempy.directory()
+ const tmpDir = tempy.directory();
// Dump each resource into tmpDir
resources.forEach((resource, index) => {
fs.writeFileSync(path.join(tmpDir, `${index}.yaml`), resource);
- })
- const cmd = `"${kubectlPath}" apply --kubeconfig "${this.cluster.getProxyKubeconfigPath()}" -o json -f "${tmpDir}"`
+ });
+ const cmd = `"${kubectlPath}" apply --kubeconfig "${this.cluster.getProxyKubeconfigPath()}" -o json -f "${tmpDir}"`;
console.log("shooting manifests with:", cmd);
exec(cmd, (error, stdout, stderr) => {
if (error) {
reject("Error applying manifests:" + error);
}
if (stderr != "") {
- reject(stderr)
- return
+ reject(stderr);
+ return;
}
- resolve(stdout)
- })
- })
+ resolve(stdout);
+ });
+ });
}
protected sanitizeObject(resource: KubernetesObject | any) {
diff --git a/src/main/router.ts b/src/main/router.ts
index 230c93f09e..a386ef221b 100644
--- a/src/main/router.ts
+++ b/src/main/router.ts
@@ -1,12 +1,12 @@
-import Call from "@hapi/call"
-import Subtext from "@hapi/subtext"
-import http from "http"
-import path from "path"
-import { readFile } from "fs-extra"
-import { Cluster } from "./cluster"
+import Call from "@hapi/call";
+import Subtext from "@hapi/subtext";
+import http from "http";
+import path from "path";
+import { readFile } from "fs-extra";
+import { Cluster } from "./cluster";
import { apiPrefix, appName, publicPath, isDevelopment, webpackDevServerPort } from "../common/vars";
import { helmRoute, kubeconfigRoute, metricsRoute, portForwardRoute, resourceApplierRoute, watchRoute } from "./routes";
-import logger from "./logger"
+import logger from "./logger";
export interface RouterRequestOpts {
req: http.IncomingMessage;
@@ -43,40 +43,40 @@ export class Router {
public constructor() {
this.router = new Call.Router();
- this.addRoutes()
+ this.addRoutes();
}
public async route(cluster: Cluster, req: http.IncomingMessage, res: http.ServerResponse): Promise {
const url = new URL(req.url, "http://localhost");
- const path = url.pathname
- const method = req.method.toLowerCase()
+ const path = url.pathname;
+ const method = req.method.toLowerCase();
const matchingRoute = this.router.route(method, path);
const routeFound = !matchingRoute.isBoom;
if (routeFound) {
const request = await this.getRequest({ req, res, cluster, url, params: matchingRoute.params });
- await matchingRoute.route(request)
- return true
+ await matchingRoute.route(request);
+ return true;
}
return false;
}
protected async getRequest(opts: RouterRequestOpts): Promise {
- const { req, res, url, cluster, params } = opts
+ const { req, res, url, cluster, params } = opts;
const { payload } = await Subtext.parse(req, null, {
parse: true,
output: "data",
});
return {
- cluster: cluster,
+ cluster,
path: url.pathname,
raw: {
- req: req,
+ req,
},
response: res,
query: url.searchParams,
- payload: payload,
- params: params
- }
+ payload,
+ params
+ };
}
protected getMimeType(filename: string) {
@@ -92,7 +92,7 @@ export class Router {
woff2: "font/woff2",
ttf: "font/ttf"
};
- return mimeTypes[path.extname(filename).slice(1)] || "text/plain"
+ return mimeTypes[path.extname(filename).slice(1)] || "text/plain";
}
async handleStaticFile(filePath: string, res: http.ServerResponse, req: http.IncomingMessage, retryCount = 0) {
@@ -114,10 +114,10 @@ export class Router {
res.end();
} catch (err) {
if (retryCount > 5) {
- logger.error("handleStaticFile:", err.toString())
- res.statusCode = 404
- res.end()
- return
+ logger.error("handleStaticFile:", err.toString());
+ res.statusCode = 404;
+ res.end();
+ return;
}
this.handleStaticFile(`${publicPath}/${appName}.html`, res, req, Math.max(retryCount, 0) + 1);
}
@@ -131,32 +131,32 @@ export class Router {
this.handleStaticFile(params.path, response, req);
});
- this.router.add({ method: "get", path: `${apiPrefix}/kubeconfig/service-account/{namespace}/{account}` }, kubeconfigRoute.routeServiceAccountRoute.bind(kubeconfigRoute))
+ this.router.add({ method: "get", path: `${apiPrefix}/kubeconfig/service-account/{namespace}/{account}` }, kubeconfigRoute.routeServiceAccountRoute.bind(kubeconfigRoute));
// Watch API
- this.router.add({ method: "get", path: `${apiPrefix}/watch` }, watchRoute.routeWatch.bind(watchRoute))
+ this.router.add({ method: "get", path: `${apiPrefix}/watch` }, watchRoute.routeWatch.bind(watchRoute));
// Metrics API
- this.router.add({ method: "post", path: `${apiPrefix}/metrics` }, metricsRoute.routeMetrics.bind(metricsRoute))
+ this.router.add({ method: "post", path: `${apiPrefix}/metrics` }, metricsRoute.routeMetrics.bind(metricsRoute));
// Port-forward API
- this.router.add({ method: "post", path: `${apiPrefix}/pods/{namespace}/{resourceType}/{resourceName}/port-forward/{port}` }, portForwardRoute.routePortForward.bind(portForwardRoute))
+ this.router.add({ method: "post", path: `${apiPrefix}/pods/{namespace}/{resourceType}/{resourceName}/port-forward/{port}` }, portForwardRoute.routePortForward.bind(portForwardRoute));
// Helm API
- this.router.add({ method: "get", path: `${apiPrefix}/v2/charts` }, helmRoute.listCharts.bind(helmRoute))
- this.router.add({ method: "get", path: `${apiPrefix}/v2/charts/{repo}/{chart}` }, helmRoute.getChart.bind(helmRoute))
- this.router.add({ method: "get", path: `${apiPrefix}/v2/charts/{repo}/{chart}/values` }, helmRoute.getChartValues.bind(helmRoute))
+ this.router.add({ method: "get", path: `${apiPrefix}/v2/charts` }, helmRoute.listCharts.bind(helmRoute));
+ this.router.add({ method: "get", path: `${apiPrefix}/v2/charts/{repo}/{chart}` }, helmRoute.getChart.bind(helmRoute));
+ this.router.add({ method: "get", path: `${apiPrefix}/v2/charts/{repo}/{chart}/values` }, helmRoute.getChartValues.bind(helmRoute));
- this.router.add({ method: "post", path: `${apiPrefix}/v2/releases` }, helmRoute.installChart.bind(helmRoute))
- this.router.add({ method: `put`, path: `${apiPrefix}/v2/releases/{namespace}/{release}` }, helmRoute.updateRelease.bind(helmRoute))
- this.router.add({ method: `put`, path: `${apiPrefix}/v2/releases/{namespace}/{release}/rollback` }, helmRoute.rollbackRelease.bind(helmRoute))
- this.router.add({ method: "get", path: `${apiPrefix}/v2/releases/{namespace?}` }, helmRoute.listReleases.bind(helmRoute))
- this.router.add({ method: "get", path: `${apiPrefix}/v2/releases/{namespace}/{release}` }, helmRoute.getRelease.bind(helmRoute))
- this.router.add({ method: "get", path: `${apiPrefix}/v2/releases/{namespace}/{release}/values` }, helmRoute.getReleaseValues.bind(helmRoute))
- this.router.add({ method: "get", path: `${apiPrefix}/v2/releases/{namespace}/{release}/history` }, helmRoute.getReleaseHistory.bind(helmRoute))
- this.router.add({ method: "delete", path: `${apiPrefix}/v2/releases/{namespace}/{release}` }, helmRoute.deleteRelease.bind(helmRoute))
+ this.router.add({ method: "post", path: `${apiPrefix}/v2/releases` }, helmRoute.installChart.bind(helmRoute));
+ this.router.add({ method: `put`, path: `${apiPrefix}/v2/releases/{namespace}/{release}` }, helmRoute.updateRelease.bind(helmRoute));
+ this.router.add({ method: `put`, path: `${apiPrefix}/v2/releases/{namespace}/{release}/rollback` }, helmRoute.rollbackRelease.bind(helmRoute));
+ this.router.add({ method: "get", path: `${apiPrefix}/v2/releases/{namespace?}` }, helmRoute.listReleases.bind(helmRoute));
+ this.router.add({ method: "get", path: `${apiPrefix}/v2/releases/{namespace}/{release}` }, helmRoute.getRelease.bind(helmRoute));
+ this.router.add({ method: "get", path: `${apiPrefix}/v2/releases/{namespace}/{release}/values` }, helmRoute.getReleaseValues.bind(helmRoute));
+ this.router.add({ method: "get", path: `${apiPrefix}/v2/releases/{namespace}/{release}/history` }, helmRoute.getReleaseHistory.bind(helmRoute));
+ this.router.add({ method: "delete", path: `${apiPrefix}/v2/releases/{namespace}/{release}` }, helmRoute.deleteRelease.bind(helmRoute));
// Resource Applier API
- this.router.add({ method: "post", path: `${apiPrefix}/stack` }, resourceApplierRoute.applyResource.bind(resourceApplierRoute))
+ this.router.add({ method: "post", path: `${apiPrefix}/stack` }, resourceApplierRoute.applyResource.bind(resourceApplierRoute));
}
}
diff --git a/src/main/routes/helm-route.ts b/src/main/routes/helm-route.ts
index 0ffd7252d5..b9a6cf513b 100644
--- a/src/main/routes/helm-route.ts
+++ b/src/main/routes/helm-route.ts
@@ -1,114 +1,122 @@
-import { LensApiRequest } from "../router"
-import { helmService } from "../helm/helm-service"
-import { LensApi } from "../lens-api"
-import logger from "../logger"
+import { LensApiRequest } from "../router";
+import { helmService } from "../helm/helm-service";
+import { LensApi } from "../lens-api";
+import logger from "../logger";
class HelmApiRoute extends LensApi {
public async listCharts(request: LensApiRequest) {
- const { response } = request
- const charts = await helmService.listCharts()
- this.respondJson(response, charts)
+ const { response } = request;
+ const charts = await helmService.listCharts();
+ this.respondJson(response, charts);
}
public async getChart(request: LensApiRequest) {
- const { params, query, response } = request
- const chart = await helmService.getChart(params.repo, params.chart, query.get("version"))
- this.respondJson(response, chart)
+ const { params, query, response } = request;
+ try {
+ const chart = await helmService.getChart(params.repo, params.chart, query.get("version"));
+ this.respondJson(response, chart);
+ } catch (error) {
+ this.respondText(response, error, 422);
+ }
}
public async getChartValues(request: LensApiRequest) {
- const { params, query, response } = request
- const values = await helmService.getChartValues(params.repo, params.chart, query.get("version"))
- this.respondJson(response, values)
+ const { params, query, response } = request;
+ try {
+ const values = await helmService.getChartValues(params.repo, params.chart, query.get("version"));
+ this.respondJson(response, values);
+ } catch (error) {
+ this.respondText(response, error, 422);
+ }
}
public async installChart(request: LensApiRequest) {
- const { payload, cluster, response } = request
+ const { payload, cluster, response } = request;
try {
- const result = await helmService.installChart(cluster, payload)
- this.respondJson(response, result, 201)
+ const result = await helmService.installChart(cluster, payload);
+ this.respondJson(response, result, 201);
} catch (error) {
- logger.debug(error)
- this.respondText(response, error, 422)
+ logger.debug(error);
+ this.respondText(response, error, 422);
}
}
public async updateRelease(request: LensApiRequest) {
- const { cluster, params, payload, response } = request
+ const { cluster, params, payload, response } = request;
try {
- const result = await helmService.updateRelease(cluster, params.release, params.namespace, payload )
- this.respondJson(response, result)
+ const result = await helmService.updateRelease(cluster, params.release, params.namespace, payload );
+ this.respondJson(response, result);
} catch (error) {
- logger.debug(error)
- this.respondText(response, error, 422)
+ logger.debug(error);
+ this.respondText(response, error, 422);
}
}
public async rollbackRelease(request: LensApiRequest) {
- const { cluster, params, payload, response } = request
+ const { cluster, params, payload, response } = request;
try {
- const result = await helmService.rollback(cluster, params.release, params.namespace, payload.revision)
- this.respondJson(response, result)
+ const result = await helmService.rollback(cluster, params.release, params.namespace, payload.revision);
+ this.respondJson(response, result);
} catch (error) {
- logger.debug(error)
- this.respondText(response, error)
+ logger.debug(error);
+ this.respondText(response, error);
}
}
public async listReleases(request: LensApiRequest) {
- const { cluster, params, response } = request
+ const { cluster, params, response } = request;
try {
- const result = await helmService.listReleases(cluster, params.namespace)
- this.respondJson(response, result)
+ const result = await helmService.listReleases(cluster, params.namespace);
+ this.respondJson(response, result);
} catch(error) {
- logger.debug(error)
- this.respondText(response, error)
+ logger.debug(error);
+ this.respondText(response, error);
}
}
public async getRelease(request: LensApiRequest) {
- const { cluster, params, response } = request
+ const { cluster, params, response } = request;
try {
- const result = await helmService.getRelease(cluster, params.release, params.namespace)
- this.respondJson(response, result)
+ const result = await helmService.getRelease(cluster, params.release, params.namespace);
+ this.respondJson(response, result);
} catch (error) {
- logger.debug(error)
- this.respondText(response, error, 422)
+ logger.debug(error);
+ this.respondText(response, error, 422);
}
}
public async getReleaseValues(request: LensApiRequest) {
- const { cluster, params, response } = request
+ const { cluster, params, response } = request;
try {
- const result = await helmService.getReleaseValues(cluster, params.release, params.namespace)
- this.respondText(response, result)
+ const result = await helmService.getReleaseValues(cluster, params.release, params.namespace);
+ this.respondText(response, result);
} catch (error) {
- logger.debug(error)
- this.respondText(response, error, 422)
+ logger.debug(error);
+ this.respondText(response, error, 422);
}
}
public async getReleaseHistory(request: LensApiRequest) {
- const { cluster, params, response } = request
+ const { cluster, params, response } = request;
try {
- const result = await helmService.getReleaseHistory(cluster, params.release, params.namespace)
- this.respondJson(response, result)
+ const result = await helmService.getReleaseHistory(cluster, params.release, params.namespace);
+ this.respondJson(response, result);
} catch (error) {
- logger.debug(error)
- this.respondText(response, error, 422)
+ logger.debug(error);
+ this.respondText(response, error, 422);
}
}
public async deleteRelease(request: LensApiRequest) {
- const { cluster, params, response } = request
+ const { cluster, params, response } = request;
try {
- const result = await helmService.deleteRelease(cluster, params.release, params.namespace)
- this.respondJson(response, result)
+ const result = await helmService.deleteRelease(cluster, params.release, params.namespace);
+ this.respondJson(response, result);
} catch (error) {
- logger.debug(error)
- this.respondText(response, error, 422)
+ logger.debug(error);
+ this.respondText(response, error, 422);
}
}
}
-export const helmRoute = new HelmApiRoute()
+export const helmRoute = new HelmApiRoute();
diff --git a/src/main/routes/index.ts b/src/main/routes/index.ts
index 60a0423de4..5bc5b3f3dd 100644
--- a/src/main/routes/index.ts
+++ b/src/main/routes/index.ts
@@ -1,6 +1,6 @@
-export * from "./kubeconfig-route"
-export * from "./metrics-route"
-export * from "./port-forward-route"
-export * from "./watch-route"
-export * from "./helm-route"
-export * from "./resource-applier-route"
+export * from "./kubeconfig-route";
+export * from "./metrics-route";
+export * from "./port-forward-route";
+export * from "./watch-route";
+export * from "./helm-route";
+export * from "./resource-applier-route";
diff --git a/src/main/routes/kubeconfig-route.ts b/src/main/routes/kubeconfig-route.ts
index 09f1f061cf..1c04b9525d 100644
--- a/src/main/routes/kubeconfig-route.ts
+++ b/src/main/routes/kubeconfig-route.ts
@@ -1,10 +1,10 @@
-import { LensApiRequest } from "../router"
-import { LensApi } from "../lens-api"
-import { Cluster } from "../cluster"
-import { CoreV1Api, V1Secret } from "@kubernetes/client-node"
+import { LensApiRequest } from "../router";
+import { LensApi } from "../lens-api";
+import { Cluster } from "../cluster";
+import { CoreV1Api, V1Secret } from "@kubernetes/client-node";
function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster) {
- const tokenData = Buffer.from(secret.data["token"], "base64")
+ const tokenData = Buffer.from(secret.data["token"], "base64");
return {
'apiVersion': 'v1',
'kind': 'Config',
@@ -36,23 +36,23 @@ function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster
}
],
'current-context': cluster.contextName
- }
+ };
}
class KubeconfigRoute extends LensApi {
public async routeServiceAccountRoute(request: LensApiRequest) {
- const { params, response, cluster} = request
+ const { params, response, cluster} = request;
const client = cluster.getProxyKubeconfig().makeApiClient(CoreV1Api);
- const secretList = await client.listNamespacedSecret(params.namespace)
+ const secretList = await client.listNamespacedSecret(params.namespace);
const secret = secretList.body.items.find(secret => {
const { annotations } = secret.metadata;
return annotations && annotations["kubernetes.io/service-account.name"] == params.account;
});
const data = generateKubeConfig(params.account, secret, cluster);
- this.respondJson(response, data)
+ this.respondJson(response, data);
}
}
-export const kubeconfigRoute = new KubeconfigRoute()
+export const kubeconfigRoute = new KubeconfigRoute();
diff --git a/src/main/routes/metrics-route.ts b/src/main/routes/metrics-route.ts
index dc77f7fb9f..254abe188f 100644
--- a/src/main/routes/metrics-route.ts
+++ b/src/main/routes/metrics-route.ts
@@ -1,32 +1,32 @@
-import { LensApiRequest } from "../router"
-import { LensApi } from "../lens-api"
-import { Cluster } from "../cluster"
-import _ from "lodash"
+import { LensApiRequest } from "../router";
+import { LensApi } from "../lens-api";
+import { Cluster } from "../cluster";
+import _ from "lodash";
export type IMetricsQuery = string | string[] | {
[metricName: string]: string;
-}
+};
// This is used for backoff retry tracking.
-const MAX_ATTEMPTS = 5
-const ATTEMPTS = [...(_.fill(Array(MAX_ATTEMPTS - 1), false)), true]
+const MAX_ATTEMPTS = 5;
+const ATTEMPTS = [...(_.fill(Array(MAX_ATTEMPTS - 1), false)), true];
// prometheus metrics loader
async function loadMetrics(promQueries: string[], cluster: Cluster, prometheusPath: string, queryParams: Record): Promise {
- const queries = promQueries.map(p => p.trim())
- const loaders = new Map>()
+ const queries = promQueries.map(p => p.trim());
+ const loaders = new Map>();
async function loadMetric(query: string): Promise {
async function loadMetricHelper(): Promise {
for (const [attempt, lastAttempt] of ATTEMPTS.entries()) { // retry
try {
- return await cluster.getMetrics(prometheusPath, { query, ...queryParams })
+ return await cluster.getMetrics(prometheusPath, { query, ...queryParams });
} catch (error) {
if (lastAttempt || error?.statusCode === 404) {
return {
status: error.toString(),
data: { result: [] },
- }
+ };
}
await new Promise(resolve => setTimeout(resolve, (attempt + 1) * 1000)); // add delay before repeating request
@@ -34,41 +34,41 @@ async function loadMetrics(promQueries: string[], cluster: Cluster, prometheusPa
}
}
- return loaders.get(query) ?? loaders.set(query, loadMetricHelper()).get(query)
+ return loaders.get(query) ?? loaders.set(query, loadMetricHelper()).get(query);
}
- return Promise.all(queries.map(loadMetric))
+ return Promise.all(queries.map(loadMetric));
}
class MetricsRoute extends LensApi {
async routeMetrics({ response, cluster, payload, query }: LensApiRequest) {
- const queryParams: IMetricsQuery = Object.fromEntries(query.entries())
+ const queryParams: IMetricsQuery = Object.fromEntries(query.entries());
try {
const [prometheusPath, prometheusProvider] = await Promise.all([
cluster.contextHandler.getPrometheusPath(),
cluster.contextHandler.getPrometheusProvider()
- ])
+ ]);
// return data in same structure as query
if (typeof payload === "string") {
- const [data] = await loadMetrics([payload], cluster, prometheusPath, queryParams)
- this.respondJson(response, data)
+ const [data] = await loadMetrics([payload], cluster, prometheusPath, queryParams);
+ this.respondJson(response, data);
} else if (Array.isArray(payload)) {
- const data = await loadMetrics(payload, cluster, prometheusPath, queryParams)
- this.respondJson(response, data)
+ const data = await loadMetrics(payload, cluster, prometheusPath, queryParams);
+ this.respondJson(response, data);
} else {
const queries = Object.entries(payload).map(([queryName, queryOpts]) => (
(prometheusProvider.getQueries(queryOpts) as Record)[queryName]
- ))
- const result = await loadMetrics(queries, cluster, prometheusPath, queryParams)
- const data = Object.fromEntries(Object.keys(payload).map((metricName, i) => [metricName, result[i]]))
- this.respondJson(response, data)
+ ));
+ const result = await loadMetrics(queries, cluster, prometheusPath, queryParams);
+ const data = Object.fromEntries(Object.keys(payload).map((metricName, i) => [metricName, result[i]]));
+ this.respondJson(response, data);
}
} catch {
- this.respondJson(response, {})
+ this.respondJson(response, {});
}
}
}
-export const metricsRoute = new MetricsRoute()
+export const metricsRoute = new MetricsRoute();
diff --git a/src/main/routes/port-forward-route.ts b/src/main/routes/port-forward-route.ts
index 7ed79aa936..967783aa3f 100644
--- a/src/main/routes/port-forward-route.ts
+++ b/src/main/routes/port-forward-route.ts
@@ -1,14 +1,14 @@
-import { LensApiRequest } from "../router"
-import { LensApi } from "../lens-api"
-import { spawn, ChildProcessWithoutNullStreams } from "child_process"
-import { Kubectl } from "../kubectl"
-import { getFreePort } from "../port"
-import { shell } from "electron"
-import * as tcpPortUsed from "tcp-port-used"
-import logger from "../logger"
+import { LensApiRequest } from "../router";
+import { LensApi } from "../lens-api";
+import { spawn, ChildProcessWithoutNullStreams } from "child_process";
+import { Kubectl } from "../kubectl";
+import { getFreePort } from "../port";
+import { shell } from "electron";
+import * as tcpPortUsed from "tcp-port-used";
+import logger from "../logger";
class PortForward {
- public static portForwards: PortForward[] = []
+ public static portForwards: PortForward[] = [];
static getPortforward(forward: {clusterId: string; kind: string; name: string; namespace: string; port: string}) {
return PortForward.portForwards.find((pf) => {
@@ -18,91 +18,91 @@ class PortForward {
pf.name == forward.name &&
pf.namespace == forward.namespace &&
pf.port == forward.port
- )
- })
+ );
+ });
}
- public clusterId: string
- public process: ChildProcessWithoutNullStreams
- public kubeConfig: string
- public kind: string
- public namespace: string
- public name: string
- public port: string
- public localPort: number
+ public clusterId: string;
+ public process: ChildProcessWithoutNullStreams;
+ public kubeConfig: string;
+ public kind: string;
+ public namespace: string;
+ public name: string;
+ public port: string;
+ public localPort: number;
constructor(obj: any) {
- Object.assign(this, obj)
+ Object.assign(this, obj);
}
public async start() {
- this.localPort = await getFreePort()
- const kubectlBin = await Kubectl.bundled().getPath()
+ this.localPort = await getFreePort();
+ const kubectlBin = await Kubectl.bundled().getPath();
const args = [
"--kubeconfig", this.kubeConfig,
"port-forward",
"-n", this.namespace,
`${this.kind}/${this.name}`,
`${this.localPort}:${this.port}`
- ]
+ ];
this.process = spawn(kubectlBin, args, {
env: process.env
- })
- PortForward.portForwards.push(this)
+ });
+ PortForward.portForwards.push(this);
this.process.on("exit", () => {
- const index = PortForward.portForwards.indexOf(this)
+ const index = PortForward.portForwards.indexOf(this);
if (index > -1) {
- PortForward.portForwards.splice(index, 1)
+ PortForward.portForwards.splice(index, 1);
}
- })
+ });
try {
- await tcpPortUsed.waitUntilUsed(this.localPort, 500, 15000)
- return true
+ await tcpPortUsed.waitUntilUsed(this.localPort, 500, 15000);
+ return true;
} catch (error) {
- this.process.kill()
- return false
+ this.process.kill();
+ return false;
}
}
public open() {
- shell.openExternal(`http://localhost:${this.localPort}`)
+ shell.openExternal(`http://localhost:${this.localPort}`);
}
}
class PortForwardRoute extends LensApi {
public async routePortForward(request: LensApiRequest) {
- const { params, response, cluster} = request
- const { namespace, port, resourceType, resourceName } = params
+ const { params, response, cluster} = request;
+ const { namespace, port, resourceType, resourceName } = params;
let portForward = PortForward.getPortforward({
clusterId: cluster.id, kind: resourceType, name: resourceName,
- namespace: namespace, port: port
- })
+ namespace, port
+ });
if (!portForward) {
- logger.info(`Creating a new port-forward ${namespace}/${resourceType}/${resourceName}:${port}`)
+ logger.info(`Creating a new port-forward ${namespace}/${resourceType}/${resourceName}:${port}`);
portForward = new PortForward({
clusterId: cluster.id,
kind: resourceType,
- namespace: namespace,
+ namespace,
name: resourceName,
- port: port,
+ port,
kubeConfig: cluster.getProxyKubeconfigPath()
- })
- const started = await portForward.start()
+ });
+ const started = await portForward.start();
if (!started) {
this.respondJson(response, {
message: "Failed to open port-forward"
- }, 400)
- return
+ }, 400);
+ return;
}
}
- portForward.open()
+ portForward.open();
- this.respondJson(response, {})
+ this.respondJson(response, {});
}
}
-export const portForwardRoute = new PortForwardRoute()
+export const portForwardRoute = new PortForwardRoute();
diff --git a/src/main/routes/resource-applier-route.ts b/src/main/routes/resource-applier-route.ts
index 56125af8f3..8bbfec0d9c 100644
--- a/src/main/routes/resource-applier-route.ts
+++ b/src/main/routes/resource-applier-route.ts
@@ -1,17 +1,17 @@
-import { LensApiRequest } from "../router"
-import { LensApi } from "../lens-api"
-import { ResourceApplier } from "../resource-applier"
+import { LensApiRequest } from "../router";
+import { LensApi } from "../lens-api";
+import { ResourceApplier } from "../resource-applier";
class ResourceApplierApiRoute extends LensApi {
public async applyResource(request: LensApiRequest) {
- const { response, cluster, payload } = request
+ const { response, cluster, payload } = request;
try {
const resource = await new ResourceApplier(cluster).apply(payload);
- this.respondJson(response, [resource], 200)
+ this.respondJson(response, [resource], 200);
} catch (error) {
- this.respondText(response, error, 422)
+ this.respondText(response, error, 422);
}
}
}
-export const resourceApplierRoute = new ResourceApplierApiRoute()
+export const resourceApplierRoute = new ResourceApplierApiRoute();
diff --git a/src/main/routes/watch-route.ts b/src/main/routes/watch-route.ts
index d88276eaac..dd42460a9d 100644
--- a/src/main/routes/watch-route.ts
+++ b/src/main/routes/watch-route.ts
@@ -1,53 +1,53 @@
-import { LensApiRequest } from "../router"
-import { LensApi } from "../lens-api"
-import { Watch, KubeConfig } from "@kubernetes/client-node"
-import { ServerResponse } from "http"
-import { Request } from "request"
-import logger from "../logger"
+import { LensApiRequest } from "../router";
+import { LensApi } from "../lens-api";
+import { Watch, KubeConfig } from "@kubernetes/client-node";
+import { ServerResponse } from "http";
+import { Request } from "request";
+import logger from "../logger";
class ApiWatcher {
- private apiUrl: string
- private response: ServerResponse
- private watchRequest: Request
- private watch: Watch
- private processor: NodeJS.Timeout
- private eventBuffer: any[] = []
+ private apiUrl: string;
+ private response: ServerResponse;
+ private watchRequest: Request;
+ private watch: Watch;
+ private processor: NodeJS.Timeout;
+ private eventBuffer: any[] = [];
constructor(apiUrl: string, kubeConfig: KubeConfig, response: ServerResponse) {
- this.apiUrl = apiUrl
- this.watch = new Watch(kubeConfig)
- this.response = response
+ this.apiUrl = apiUrl;
+ this.watch = new Watch(kubeConfig);
+ this.response = response;
}
public async start() {
if (this.processor) {
- clearInterval(this.processor)
+ clearInterval(this.processor);
}
this.processor = setInterval(() => {
- const events = this.eventBuffer.splice(0)
- events.map(event => this.sendEvent(event))
- this.response.flushHeaders()
- }, 50)
- this.watchRequest = await this.watch.watch(this.apiUrl, {}, this.watchHandler.bind(this), this.doneHandler.bind(this))
+ const events = this.eventBuffer.splice(0);
+ events.map(event => this.sendEvent(event));
+ this.response.flushHeaders();
+ }, 50);
+ this.watchRequest = await this.watch.watch(this.apiUrl, {}, this.watchHandler.bind(this), this.doneHandler.bind(this));
}
public stop() {
- if (!this.watchRequest) { return }
+ if (!this.watchRequest) { return; }
if (this.processor) {
- clearInterval(this.processor)
+ clearInterval(this.processor);
}
- logger.debug("Stopping watcher for api: " + this.apiUrl)
+ logger.debug("Stopping watcher for api: " + this.apiUrl);
try {
- this.watchRequest.abort()
+ this.watchRequest.abort();
this.sendEvent({
type: "STREAM_END",
url: this.apiUrl,
status: 410,
- })
- logger.debug("watch aborted")
+ });
+ logger.debug("watch aborted");
} catch (error) {
- logger.error("Watch abort errored:" + error)
+ logger.error("Watch abort errored:" + error);
}
}
@@ -55,12 +55,12 @@ class ApiWatcher {
this.eventBuffer.push({
type: phase,
object: obj
- })
+ });
}
private doneHandler(error: Error) {
- if (error) logger.warn("watch ended: " + error.toString())
- this.watchRequest.abort()
+ if (error) logger.warn("watch ended: " + error.toString());
+ this.watchRequest.abort();
}
private sendEvent(evt: any) {
@@ -72,40 +72,40 @@ class ApiWatcher {
class WatchRoute extends LensApi {
public async routeWatch(request: LensApiRequest) {
- const { params, response, cluster} = request
- const apis: string[] = request.query.getAll("api")
- const watchers: ApiWatcher[] = []
+ const { params, response, cluster} = request;
+ const apis: string[] = request.query.getAll("api");
+ const watchers: ApiWatcher[] = [];
if (!apis.length) {
this.respondJson(response, {
message: "Empty request. Query params 'api' are not provided.",
example: "?api=/api/v1/pods&api=/api/v1/nodes",
- }, 400)
- return
+ }, 400);
+ return;
}
- response.setHeader("Content-Type", "text/event-stream")
- response.setHeader("Cache-Control", "no-cache")
- response.setHeader("Connection", "keep-alive")
- logger.debug("watch using kubeconfig:" + JSON.stringify(cluster.getProxyKubeconfig(), null, 2))
+ response.setHeader("Content-Type", "text/event-stream");
+ response.setHeader("Cache-Control", "no-cache");
+ response.setHeader("Connection", "keep-alive");
+ logger.debug("watch using kubeconfig:" + JSON.stringify(cluster.getProxyKubeconfig(), null, 2));
apis.forEach(apiUrl => {
- const watcher = new ApiWatcher(apiUrl, cluster.getProxyKubeconfig(), response)
- watcher.start()
- watchers.push(watcher)
- })
+ const watcher = new ApiWatcher(apiUrl, cluster.getProxyKubeconfig(), response);
+ watcher.start();
+ watchers.push(watcher);
+ });
request.raw.req.on("close", () => {
- logger.debug("Watch request closed")
- watchers.map(watcher => watcher.stop())
- })
+ logger.debug("Watch request closed");
+ watchers.map(watcher => watcher.stop());
+ });
request.raw.req.on("end", () => {
- logger.debug("Watch request ended")
- watchers.map(watcher => watcher.stop())
- })
+ logger.debug("Watch request ended");
+ watchers.map(watcher => watcher.stop());
+ });
}
}
-export const watchRoute = new WatchRoute()
+export const watchRoute = new WatchRoute();
diff --git a/src/main/shell-session.ts b/src/main/shell-session.ts
index 6ea9e4eede..3614abe28f 100644
--- a/src/main/shell-session.ts
+++ b/src/main/shell-session.ts
@@ -1,23 +1,23 @@
-import * as pty from "node-pty"
-import * as WebSocket from "ws"
+import * as pty from "node-pty";
+import * as WebSocket from "ws";
import { EventEmitter } from "events";
-import path from "path"
-import shellEnv from "shell-env"
-import { app } from "electron"
-import { Kubectl } from "./kubectl"
-import { Cluster } from "./cluster"
+import path from "path";
+import shellEnv from "shell-env";
+import { app } from "electron";
+import { Kubectl } from "./kubectl";
+import { Cluster } from "./cluster";
import { ClusterPreferences } from "../common/cluster-store";
-import { helmCli } from "./helm/helm-cli"
+import { helmCli } from "./helm/helm-cli";
import { isWindows } from "../common/vars";
-import { appEventBus } from "../common/event-bus"
+import { appEventBus } from "../common/event-bus";
import { userStore } from "../common/user-store";
export class ShellSession extends EventEmitter {
- static shellEnvs: Map = new Map()
+ static shellEnvs: Map = new Map();
- protected websocket: WebSocket
- protected shellProcess: pty.IPty
- protected kubeconfigPath: string
+ protected websocket: WebSocket;
+ protected shellProcess: pty.IPty;
+ protected kubeconfigPath: string;
protected nodeShellPod: string;
protected kubectl: Kubectl;
protected kubectlBinDir: string;
@@ -28,163 +28,163 @@ export class ShellSession extends EventEmitter {
protected clusterId: string;
constructor(socket: WebSocket, cluster: Cluster) {
- super()
- this.websocket = socket
- this.kubeconfigPath = cluster.getProxyKubeconfigPath()
- this.kubectl = new Kubectl(cluster.version)
- this.preferences = cluster.preferences || {}
- this.clusterId = cluster.id
+ super();
+ this.websocket = socket;
+ this.kubeconfigPath = cluster.getProxyKubeconfigPath();
+ this.kubectl = new Kubectl(cluster.version);
+ this.preferences = cluster.preferences || {};
+ this.clusterId = cluster.id;
}
public async open() {
- this.kubectlBinDir = await this.kubectl.binDir()
- const pathFromPreferences = userStore.preferences.kubectlBinariesPath || this.kubectl.getBundledPath()
- this.kubectlPathDir = userStore.preferences.downloadKubectlBinaries ? this.kubectlBinDir : path.dirname(pathFromPreferences)
- this.helmBinDir = helmCli.getBinaryDir()
- const env = await this.getCachedShellEnv()
- const shell = env.PTYSHELL
- const args = await this.getShellArgs(shell)
+ this.kubectlBinDir = await this.kubectl.binDir();
+ const pathFromPreferences = userStore.preferences.kubectlBinariesPath || this.kubectl.getBundledPath();
+ this.kubectlPathDir = userStore.preferences.downloadKubectlBinaries ? this.kubectlBinDir : path.dirname(pathFromPreferences);
+ this.helmBinDir = helmCli.getBinaryDir();
+ const env = await this.getCachedShellEnv();
+ const shell = env.PTYSHELL;
+ const args = await this.getShellArgs(shell);
this.shellProcess = pty.spawn(shell, args, {
cols: 80,
cwd: this.cwd() || env.HOME,
- env: env,
+ env,
name: "xterm-256color",
rows: 30,
});
this.running = true;
- this.pipeStdout()
- this.pipeStdin()
- this.closeWebsocketOnProcessExit()
- this.exitProcessOnWebsocketClose()
+ this.pipeStdout();
+ this.pipeStdin();
+ this.closeWebsocketOnProcessExit();
+ this.exitProcessOnWebsocketClose();
- appEventBus.emit({name: "shell", action: "open"})
+ appEventBus.emit({name: "shell", action: "open"});
}
protected cwd(): string {
if(!this.preferences || !this.preferences.terminalCWD || this.preferences.terminalCWD === "") {
- return null
+ return null;
}
- return this.preferences.terminalCWD
+ return this.preferences.terminalCWD;
}
protected async getShellArgs(shell: string): Promise> {
switch(path.basename(shell)) {
- case "powershell.exe":
- return ["-NoExit", "-command", `& {Set-Location $Env:USERPROFILE; $Env:PATH="${this.helmBinDir};${this.kubectlPathDir};$Env:PATH"}`]
- case "bash":
- return ["--init-file", path.join(this.kubectlBinDir, '.bash_set_path')]
- case "fish":
- return ["--login", "--init-command", `export PATH="${this.helmBinDir}:${this.kubectlPathDir}:$PATH"; export KUBECONFIG="${this.kubeconfigPath}"`]
- case "zsh":
- return ["--login"]
- default:
- return []
+ case "powershell.exe":
+ return ["-NoExit", "-command", `& {Set-Location $Env:USERPROFILE; $Env:PATH="${this.helmBinDir};${this.kubectlPathDir};$Env:PATH"}`];
+ case "bash":
+ return ["--init-file", path.join(this.kubectlBinDir, '.bash_set_path')];
+ case "fish":
+ return ["--login", "--init-command", `export PATH="${this.helmBinDir}:${this.kubectlPathDir}:$PATH"; export KUBECONFIG="${this.kubeconfigPath}"`];
+ case "zsh":
+ return ["--login"];
+ default:
+ return [];
}
}
protected async getCachedShellEnv() {
- let env = ShellSession.shellEnvs.get(this.clusterId)
+ let env = ShellSession.shellEnvs.get(this.clusterId);
if (!env) {
- env = await this.getShellEnv()
- ShellSession.shellEnvs.set(this.clusterId, env)
+ env = await this.getShellEnv();
+ ShellSession.shellEnvs.set(this.clusterId, env);
} else {
// refresh env in the background
this.getShellEnv().then((shellEnv: any) => {
- ShellSession.shellEnvs.set(this.clusterId, shellEnv)
- })
+ ShellSession.shellEnvs.set(this.clusterId, shellEnv);
+ });
}
- return env
+ return env;
}
protected async getShellEnv() {
- const env = JSON.parse(JSON.stringify(await shellEnv()))
- const pathStr = [this.kubectlBinDir, this.helmBinDir, process.env.PATH].join(path.delimiter)
+ const env = JSON.parse(JSON.stringify(await shellEnv()));
+ const pathStr = [this.kubectlBinDir, this.helmBinDir, process.env.PATH].join(path.delimiter);
if(isWindows) {
- env["SystemRoot"] = process.env.SystemRoot
- env["PTYSHELL"] = "powershell.exe"
- env["PATH"] = pathStr
+ env["SystemRoot"] = process.env.SystemRoot;
+ env["PTYSHELL"] = "powershell.exe";
+ env["PATH"] = pathStr;
} else if(typeof(process.env.SHELL) != "undefined") {
- env["PTYSHELL"] = process.env.SHELL
- env["PATH"] = pathStr
+ env["PTYSHELL"] = process.env.SHELL;
+ env["PATH"] = pathStr;
} else {
- env["PTYSHELL"] = "" // blank runs the system default shell
+ env["PTYSHELL"] = ""; // blank runs the system default shell
}
if(path.basename(env["PTYSHELL"]) === "zsh") {
- env["OLD_ZDOTDIR"] = env.ZDOTDIR || env.HOME
- env["ZDOTDIR"] = this.kubectlBinDir
+ env["OLD_ZDOTDIR"] = env.ZDOTDIR || env.HOME;
+ env["ZDOTDIR"] = this.kubectlBinDir;
}
- env["PTYPID"] = process.pid.toString()
- env["KUBECONFIG"] = this.kubeconfigPath
- env["TERM_PROGRAM"] = app.getName()
- env["TERM_PROGRAM_VERSION"] = app.getVersion()
+ env["PTYPID"] = process.pid.toString();
+ env["KUBECONFIG"] = this.kubeconfigPath;
+ env["TERM_PROGRAM"] = app.getName();
+ env["TERM_PROGRAM_VERSION"] = app.getVersion();
if (this.preferences.httpsProxy) {
- env["HTTPS_PROXY"] = this.preferences.httpsProxy
+ env["HTTPS_PROXY"] = this.preferences.httpsProxy;
}
- const no_proxy = ["localhost", "127.0.0.1", env["NO_PROXY"]]
- env["NO_PROXY"] = no_proxy.filter(address => !!address).join()
+ const no_proxy = ["localhost", "127.0.0.1", env["NO_PROXY"]];
+ env["NO_PROXY"] = no_proxy.filter(address => !!address).join();
if (env.DEBUG) { // do not pass debug option to bash
- delete env["DEBUG"]
+ delete env["DEBUG"];
}
- return(env)
+ return(env);
}
protected pipeStdout() {
// send shell output to websocket
this.shellProcess.onData(((data: string) => {
- this.sendResponse(data)
+ this.sendResponse(data);
}));
}
protected pipeStdin() {
// write websocket messages to shellProcess
this.websocket.on("message", (data: string) => {
- if (!this.running) { return }
+ if (!this.running) { return; }
- const message = Buffer.from(data.slice(1, data.length), "base64").toString()
+ const message = Buffer.from(data.slice(1, data.length), "base64").toString();
switch (data[0]) {
- case "0":
- this.shellProcess.write(message)
- break;
- case "4":
- const resizeMsgObj = JSON.parse(message)
- this.shellProcess.resize(resizeMsgObj["Width"], resizeMsgObj["Height"])
- break;
- case "9":
- this.emit('newToken', message)
- break;
+ case "0":
+ this.shellProcess.write(message);
+ break;
+ case "4":
+ const resizeMsgObj = JSON.parse(message);
+ this.shellProcess.resize(resizeMsgObj["Width"], resizeMsgObj["Height"]);
+ break;
+ case "9":
+ this.emit('newToken', message);
+ break;
}
- })
+ });
}
protected exit(code = 1000) {
- if (this.websocket.readyState == this.websocket.OPEN) this.websocket.close(code)
- this.emit('exit')
+ if (this.websocket.readyState == this.websocket.OPEN) this.websocket.close(code);
+ this.emit('exit');
}
protected closeWebsocketOnProcessExit() {
this.shellProcess.onExit(({ exitCode }) => {
- this.running = false
- let timeout = 0
+ this.running = false;
+ let timeout = 0;
if (exitCode > 0) {
- this.sendResponse("Terminal will auto-close in 15 seconds ...")
- timeout = 15*1000
+ this.sendResponse("Terminal will auto-close in 15 seconds ...");
+ timeout = 15*1000;
}
setTimeout(() => {
- this.exit()
- }, timeout)
+ this.exit();
+ }, timeout);
});
}
protected exitProcessOnWebsocketClose() {
this.websocket.on("close", () => {
- this.killShellProcess()
- })
+ this.killShellProcess();
+ });
}
protected killShellProcess(){
@@ -192,17 +192,17 @@ export class ShellSession extends EventEmitter {
// On Windows we need to kill the shell process by pid, since Lens won't respond after a while if using `this.shellProcess.kill()`
if (isWindows) {
try {
- process.kill(this.shellProcess.pid)
+ process.kill(this.shellProcess.pid);
} catch(e) {
- return
+ return;
}
} else {
- this.shellProcess.kill()
+ this.shellProcess.kill();
}
}
}
protected sendResponse(msg: string) {
- this.websocket.send("1" + Buffer.from(msg).toString("base64"))
+ this.websocket.send("1" + Buffer.from(msg).toString("base64"));
}
}
diff --git a/src/main/shell-sync.ts b/src/main/shell-sync.ts
index 373d0a36ba..46d5788c12 100644
--- a/src/main/shell-sync.ts
+++ b/src/main/shell-sync.ts
@@ -1,4 +1,4 @@
-import shellEnv from "shell-env"
+import shellEnv from "shell-env";
import os from "os";
import { app } from "electron";
import logger from "./logger";
@@ -19,7 +19,7 @@ export async function shellSync() {
try {
envVars = await shellEnv(shell);
} catch (error) {
- logger.error(`shellEnv: ${error}`)
+ logger.error(`shellEnv: ${error}`);
}
const env: Env = JSON.parse(JSON.stringify(envVars));
@@ -27,12 +27,12 @@ export async function shellSync() {
// the LANG env var expects an underscore instead of electron's dash
env.LANG = `${app.getLocale().replace('-', '_')}.UTF-8`;
} else if (!env.LANG.endsWith(".UTF-8")) {
- env.LANG += ".UTF-8"
+ env.LANG += ".UTF-8";
}
// Overwrite PATH on darwin
if (process.env.NODE_ENV === "production" && process.platform === "darwin") {
- process.env["PATH"] = env.PATH
+ process.env["PATH"] = env.PATH;
}
// The spread operator allows joining of objects. The precedence is last to first.
diff --git a/src/main/tray.ts b/src/main/tray.ts
index 5bc6ce7091..f98c064bdd 100644
--- a/src/main/tray.ts
+++ b/src/main/tray.ts
@@ -1,6 +1,6 @@
-import path from "path"
-import packageInfo from "../../package.json"
-import { app, dialog, Menu, NativeImage, nativeTheme, Tray } from "electron"
+import path from "path";
+import packageInfo from "../../package.json";
+import { dialog, Menu, NativeImage, nativeTheme, Tray } from "electron";
import { autorun } from "mobx";
import { showAbout } from "./menu";
import { AppUpdater } from "./app-updater";
@@ -11,6 +11,7 @@ import { preferencesURL } from "../renderer/components/+preferences/preferences.
import { clusterViewURL } from "../renderer/components/cluster-manager/cluster-view.route";
import logger from "./logger";
import { isDevelopment } from "../common/vars";
+import { exitApp } from "./exit-app";
// note: instance of Tray should be saved somewhere, otherwise it disappears
export let tray: Tray;
@@ -23,7 +24,7 @@ export function getTrayIcon(isDark = nativeTheme.shouldUseDarkColors): string {
__static,
isDevelopment ? "../build/tray" : "icons", // copied within electron-builder extras
`tray_icon${isDark ? "_dark" : ""}.png`
- )
+ );
}
export function initTray(windowManager: WindowManager) {
@@ -34,18 +35,18 @@ export function initTray(windowManager: WindowManager) {
} catch (err) {
logger.error(`[TRAY]: building failed: ${err}`);
}
- })
+ });
return () => {
dispose();
tray?.destroy();
tray = null;
- }
+ };
}
export function buildTray(icon: string | NativeImage, menu: Menu) {
if (!tray) {
- tray = new Tray(icon)
- tray.setToolTip(packageInfo.description)
+ tray = new Tray(icon);
+ tray.setToolTip(packageInfo.description);
tray.setIgnoreDoubleClickEvents(true);
}
@@ -69,7 +70,7 @@ export function createTrayMenu(windowManager: WindowManager): Menu {
{
label: "Open Lens",
async click() {
- await windowManager.ensureMainWindow()
+ await windowManager.ensureMainWindow();
},
},
{
@@ -88,18 +89,17 @@ export function createTrayMenu(windowManager: WindowManager): Menu {
label: workspace.name,
toolTip: workspace.description,
submenu: clusters.map(cluster => {
- const { id: clusterId, preferences: { clusterName: label }, online, workspace } = cluster;
+ const { id: clusterId, name: label, online, workspace } = cluster;
return {
label: `${online ? '✓' : '\x20'.repeat(3)/*offset*/}${label}`,
toolTip: clusterId,
async click() {
workspaceStore.setActive(workspace);
- clusterStore.setActive(clusterId);
windowManager.navigate(clusterViewURL({ params: { clusterId } }));
}
- }
+ };
})
- }
+ };
}),
},
{
@@ -111,7 +111,7 @@ export function createTrayMenu(windowManager: WindowManager): Menu {
dialog.showMessageBoxSync(browserWindow, {
message: "No updates available",
type: "info",
- })
+ });
}
},
},
@@ -119,7 +119,7 @@ export function createTrayMenu(windowManager: WindowManager): Menu {
{
label: 'Quit App',
click() {
- app.exit();
+ exitApp();
}
}
]);
diff --git a/src/main/window-manager.ts b/src/main/window-manager.ts
index fc61ca8345..1fa2614c50 100644
--- a/src/main/window-manager.ts
+++ b/src/main/window-manager.ts
@@ -1,13 +1,13 @@
import type { ClusterId } from "../common/cluster-store";
-import { clusterStore } from "../common/cluster-store";
import { observable } from "mobx";
-import { app, BrowserWindow, dialog, ipcMain, shell, webContents } from "electron"
-import windowStateKeeper from "electron-window-state"
-import { extensionLoader } from "../extensions/extension-loader";
-import { appEventBus } from "../common/event-bus"
+import { app, BrowserWindow, dialog, shell, webContents } from "electron";
+import windowStateKeeper from "electron-window-state";
+import { appEventBus } from "../common/event-bus";
+import { subscribeToBroadcast } from "../common/ipc";
import { initMenu } from "./menu";
import { initTray } from "./tray";
import { Singleton } from "../common/utils";
+import { clusterFrameMap } from "../common/cluster-frames";
export class WindowManager extends Singleton {
protected mainWindow: BrowserWindow;
@@ -26,7 +26,7 @@ export class WindowManager extends Singleton {
}
get mainUrl() {
- return `http://localhost:${this.proxyPort}`
+ return `http://localhost:${this.proxyPort}`;
}
async initMainWindow(showSplash = true) {
@@ -63,14 +63,14 @@ export class WindowManager extends Singleton {
shell.openExternal(url);
});
this.mainWindow.webContents.on("dom-ready", () => {
- extensionLoader.broadcastExtensions()
- })
+ appEventBus.emit({name: "app", action: "dom-ready"});
+ });
this.mainWindow.on("focus", () => {
- appEventBus.emit({name: "app", action: "focus"})
- })
+ appEventBus.emit({name: "app", action: "focus"});
+ });
this.mainWindow.on("blur", () => {
- appEventBus.emit({name: "app", action: "blur"})
- })
+ appEventBus.emit({name: "app", action: "blur"});
+ });
// clean up
this.mainWindow.on("closed", () => {
@@ -78,15 +78,16 @@ export class WindowManager extends Singleton {
this.mainWindow = null;
this.splashWindow = null;
app.dock?.hide(); // hide icon in dock (mac-os)
- })
+ });
}
try {
if (showSplash) await this.showSplash();
await this.mainWindow.loadURL(this.mainUrl);
this.mainWindow.show();
this.splashWindow?.close();
+ appEventBus.emit({ name: "app", action: "start" });
} catch (err) {
- dialog.showErrorBox("ERROR!", err.toString())
+ dialog.showErrorBox("ERROR!", err.toString());
}
}
@@ -100,7 +101,7 @@ export class WindowManager extends Singleton {
protected bindEvents() {
// track visible cluster from ui
- ipcMain.on("cluster-view:current-id", (event, clusterId: ClusterId) => {
+ subscribeToBroadcast("cluster-view:current-id", (event, clusterId: ClusterId) => {
this.activeClusterId = clusterId;
});
}
@@ -123,13 +124,13 @@ export class WindowManager extends Singleton {
await this.ensureMainWindow();
this.sendToView({
channel: "renderer:navigate",
- frameId: frameId,
+ frameId,
data: [url],
- })
+ });
}
reload() {
- const frameId = clusterStore.getById(this.activeClusterId)?.frameId;
+ const frameId = clusterFrameMap.get(this.activeClusterId);
if (frameId) {
this.sendToView({ channel: "renderer:reload", frameId });
} else {
@@ -156,6 +157,11 @@ export class WindowManager extends Singleton {
this.splashWindow.show();
}
+ hide() {
+ if (!this.mainWindow?.isDestroyed()) this.mainWindow.hide();
+ if (!this.splashWindow.isDestroyed()) this.splashWindow.hide();
+ }
+
destroy() {
this.mainWindow.destroy();
this.splashWindow.destroy();
@@ -163,7 +169,7 @@ export class WindowManager extends Singleton {
this.splashWindow = null;
Object.entries(this.disposers).forEach(([name, dispose]) => {
dispose();
- delete this.disposers[name]
+ delete this.disposers[name];
});
}
}
diff --git a/src/migrations/cluster-store/2.0.0-beta.2.ts b/src/migrations/cluster-store/2.0.0-beta.2.ts
index 8a01af5407..245e9c019c 100644
--- a/src/migrations/cluster-store/2.0.0-beta.2.ts
+++ b/src/migrations/cluster-store/2.0.0-beta.2.ts
@@ -13,4 +13,4 @@ export default migration({
store.set(contextName, { kubeConfig: value[1] });
}
}
-})
\ No newline at end of file
+});
\ No newline at end of file
diff --git a/src/migrations/cluster-store/2.4.1.ts b/src/migrations/cluster-store/2.4.1.ts
index 5789f6cc36..f9de1ed6d8 100644
--- a/src/migrations/cluster-store/2.4.1.ts
+++ b/src/migrations/cluster-store/2.4.1.ts
@@ -11,4 +11,4 @@ export default migration({
store.set(contextName, { kubeConfig: cluster.kubeConfig, icon: cluster.icon || null, preferences: cluster.preferences || {} });
}
}
-})
+});
diff --git a/src/migrations/cluster-store/2.6.0-beta.2.ts b/src/migrations/cluster-store/2.6.0-beta.2.ts
index 0e13afe7a9..3114202ed1 100644
--- a/src/migrations/cluster-store/2.6.0-beta.2.ts
+++ b/src/migrations/cluster-store/2.6.0-beta.2.ts
@@ -6,7 +6,7 @@ export default migration({
run(store, log) {
for (const value of store) {
const clusterKey = value[0];
- if (clusterKey === "__internal__") continue
+ if (clusterKey === "__internal__") continue;
const cluster = value[1];
if (!cluster.preferences) cluster.preferences = {};
if (cluster.icon) {
@@ -16,4 +16,4 @@ export default migration({
store.set(clusterKey, { contextName: clusterKey, kubeConfig: value[1].kubeConfig, preferences: value[1].preferences });
}
}
-})
+});
diff --git a/src/migrations/cluster-store/2.6.0-beta.3.ts b/src/migrations/cluster-store/2.6.0-beta.3.ts
index 11f1a3bce9..eae6a137be 100644
--- a/src/migrations/cluster-store/2.6.0-beta.3.ts
+++ b/src/migrations/cluster-store/2.6.0-beta.3.ts
@@ -1,38 +1,38 @@
import { migration } from "../migration-wrapper";
-import yaml from "js-yaml"
+import yaml from "js-yaml";
export default migration({
version: "2.6.0-beta.3",
run(store, log) {
for (const value of store) {
const clusterKey = value[0];
- if (clusterKey === "__internal__") continue
+ if (clusterKey === "__internal__") continue;
const cluster = value[1];
- if (!cluster.kubeConfig) continue
- const kubeConfig = yaml.safeLoad(cluster.kubeConfig)
- if (!kubeConfig.hasOwnProperty('users')) continue
- const userObj = kubeConfig.users[0]
+ if (!cluster.kubeConfig) continue;
+ const kubeConfig = yaml.safeLoad(cluster.kubeConfig);
+ if (!kubeConfig.hasOwnProperty('users')) continue;
+ const userObj = kubeConfig.users[0];
if (userObj) {
- const user = userObj.user
+ const user = userObj.user;
if (user["auth-provider"] && user["auth-provider"].config) {
- const authConfig = user["auth-provider"].config
+ const authConfig = user["auth-provider"].config;
if (authConfig["access-token"]) {
- authConfig["access-token"] = `${authConfig["access-token"]}`
+ authConfig["access-token"] = `${authConfig["access-token"]}`;
}
if (authConfig.expiry) {
- authConfig.expiry = `${authConfig.expiry}`
+ authConfig.expiry = `${authConfig.expiry}`;
}
- log(authConfig)
- user["auth-provider"].config = authConfig
+ log(authConfig);
+ user["auth-provider"].config = authConfig;
kubeConfig.users = [{
name: userObj.name,
- user: user
- }]
- cluster.kubeConfig = yaml.safeDump(kubeConfig)
- store.set(clusterKey, cluster)
+ user
+ }];
+ cluster.kubeConfig = yaml.safeDump(kubeConfig);
+ store.set(clusterKey, cluster);
}
}
}
}
-})
+});
diff --git a/src/migrations/cluster-store/2.7.0-beta.0.ts b/src/migrations/cluster-store/2.7.0-beta.0.ts
index 3e0ae9337f..f1af3de3c9 100644
--- a/src/migrations/cluster-store/2.7.0-beta.0.ts
+++ b/src/migrations/cluster-store/2.7.0-beta.0.ts
@@ -6,10 +6,10 @@ export default migration({
run(store, log) {
for (const value of store) {
const clusterKey = value[0];
- if (clusterKey === "__internal__") continue
+ if (clusterKey === "__internal__") continue;
const cluster = value[1];
- cluster.workspace = "default"
- store.set(clusterKey, cluster)
+ cluster.workspace = "default";
+ store.set(clusterKey, cluster);
}
}
-})
+});
diff --git a/src/migrations/cluster-store/2.7.0-beta.1.ts b/src/migrations/cluster-store/2.7.0-beta.1.ts
index de9e4506d1..52e60ba527 100644
--- a/src/migrations/cluster-store/2.7.0-beta.1.ts
+++ b/src/migrations/cluster-store/2.7.0-beta.1.ts
@@ -1,25 +1,25 @@
// Add id for clusters and store them to array
import { migration } from "../migration-wrapper";
-import { v4 as uuid } from "uuid"
+import { v4 as uuid } from "uuid";
export default migration({
version: "2.7.0-beta.1",
run(store, log) {
- const clusters: any[] = []
+ const clusters: any[] = [];
for (const value of store) {
const clusterKey = value[0];
- if (clusterKey === "__internal__") continue
- if (clusterKey === "clusters") continue
+ if (clusterKey === "__internal__") continue;
+ if (clusterKey === "clusters") continue;
const cluster = value[1];
- cluster.id = uuid()
+ cluster.id = uuid();
if (!cluster.preferences.clusterName) {
- cluster.preferences.clusterName = clusterKey
+ cluster.preferences.clusterName = clusterKey;
}
- clusters.push(cluster)
- store.delete(clusterKey)
+ clusters.push(cluster);
+ store.delete(clusterKey);
}
if (clusters.length > 0) {
- store.set("clusters", clusters)
+ store.set("clusters", clusters);
}
}
-})
+});
diff --git a/src/migrations/cluster-store/3.6.0-beta.1.ts b/src/migrations/cluster-store/3.6.0-beta.1.ts
index 412c77ab96..c7e5889cd9 100644
--- a/src/migrations/cluster-store/3.6.0-beta.1.ts
+++ b/src/migrations/cluster-store/3.6.0-beta.1.ts
@@ -1,24 +1,24 @@
// Move embedded kubeconfig into separate file and add reference to it to cluster settings
// convert file path cluster icons to their base64 encoded versions
-import path from "path"
-import { app, remote } from "electron"
+import path from "path";
+import { app, remote } from "electron";
import { migration } from "../migration-wrapper";
-import fse from "fs-extra"
+import fse from "fs-extra";
import { ClusterModel, ClusterStore } from "../../common/cluster-store";
import { loadConfig } from "../../common/kube-helpers";
export default migration({
version: "3.6.0-beta.1",
run(store, printLog) {
- const userDataPath = (app || remote.app).getPath("userData")
+ const userDataPath = (app || remote.app).getPath("userData");
const kubeConfigBase = ClusterStore.getCustomKubeConfigPath("");
const storedClusters: ClusterModel[] = store.get("clusters") || [];
if (!storedClusters.length) return;
fse.ensureDirSync(kubeConfigBase);
- printLog("Number of clusters to migrate: ", storedClusters.length)
+ printLog("Number of clusters to migrate: ", storedClusters.length);
const migratedClusters = storedClusters
.map(cluster => {
/**
@@ -31,7 +31,7 @@ export default migration({
delete cluster.kubeConfig;
} catch (error) {
- printLog(`Failed to migrate Kubeconfig for cluster "${cluster.id}", removing cluster...`, error)
+ printLog(`Failed to migrate Kubeconfig for cluster "${cluster.id}", removing cluster...`, error);
return undefined;
}
@@ -40,8 +40,8 @@ export default migration({
*/
try {
if (cluster.preferences?.icon) {
- printLog(`migrating ${cluster.preferences.icon} for ${cluster.preferences.clusterName}`)
- const iconPath = cluster.preferences.icon.replace("store://", "")
+ printLog(`migrating ${cluster.preferences.icon} for ${cluster.preferences.clusterName}`);
+ const iconPath = cluster.preferences.icon.replace("store://", "");
const fileData = fse.readFileSync(path.join(userDataPath, iconPath));
cluster.preferences.icon = `data:;base64,${fileData.toString('base64')}`;
@@ -49,7 +49,7 @@ export default migration({
delete cluster.preferences?.icon;
}
} catch (error) {
- printLog(`Failed to migrate cluster icon for cluster "${cluster.id}"`, error)
+ printLog(`Failed to migrate cluster icon for cluster "${cluster.id}"`, error);
delete cluster.preferences.icon;
}
@@ -59,7 +59,7 @@ export default migration({
// "overwrite" the cluster configs
if (migratedClusters.length > 0) {
- store.set("clusters", migratedClusters)
+ store.set("clusters", migratedClusters);
}
}
-})
+});
diff --git a/src/migrations/cluster-store/index.ts b/src/migrations/cluster-store/index.ts
index f35e8f6c9c..c546fdaeda 100644
--- a/src/migrations/cluster-store/index.ts
+++ b/src/migrations/cluster-store/index.ts
@@ -1,13 +1,13 @@
// Cluster store migrations
-import version200Beta2 from "./2.0.0-beta.2"
-import version241 from "./2.4.1"
-import version260Beta2 from "./2.6.0-beta.2"
-import version260Beta3 from "./2.6.0-beta.3"
-import version270Beta0 from "./2.7.0-beta.0"
-import version270Beta1 from "./2.7.0-beta.1"
-import version360Beta1 from "./3.6.0-beta.1"
-import snap from "./snap"
+import version200Beta2 from "./2.0.0-beta.2";
+import version241 from "./2.4.1";
+import version260Beta2 from "./2.6.0-beta.2";
+import version260Beta3 from "./2.6.0-beta.3";
+import version270Beta0 from "./2.7.0-beta.0";
+import version270Beta1 from "./2.7.0-beta.1";
+import version360Beta1 from "./3.6.0-beta.1";
+import snap from "./snap";
export default {
...version200Beta2,
@@ -18,4 +18,4 @@ export default {
...version270Beta1,
...version360Beta1,
...snap
-}
\ No newline at end of file
+};
\ No newline at end of file
diff --git a/src/migrations/cluster-store/snap.ts b/src/migrations/cluster-store/snap.ts
index a377ba4268..1136607cd7 100644
--- a/src/migrations/cluster-store/snap.ts
+++ b/src/migrations/cluster-store/snap.ts
@@ -3,31 +3,31 @@
import { migration } from "../migration-wrapper";
import { ClusterModel, ClusterStore } from "../../common/cluster-store";
import { getAppVersion } from "../../common/utils/app-version";
-import fs from "fs"
+import fs from "fs";
export default migration({
version: getAppVersion(), // Run always after upgrade
run(store, printLog) {
if (!process.env["SNAP"]) return;
- printLog("Migrating embedded kubeconfig paths")
+ printLog("Migrating embedded kubeconfig paths");
const storedClusters: ClusterModel[] = store.get("clusters") || [];
if (!storedClusters.length) return;
- printLog("Number of clusters to migrate: ", storedClusters.length)
+ printLog("Number of clusters to migrate: ", storedClusters.length);
const migratedClusters = storedClusters
.map(cluster => {
/**
* replace snap version with 'current' in kubeconfig path
*/
if (!fs.existsSync(cluster.kubeConfigPath)) {
- const kubeconfigPath = cluster.kubeConfigPath.replace(/\/snap\/kontena-lens\/[0-9]*\//, "/snap/kontena-lens/current/")
- cluster.kubeConfigPath = kubeconfigPath
+ const kubeconfigPath = cluster.kubeConfigPath.replace(/\/snap\/kontena-lens\/[0-9]*\//, "/snap/kontena-lens/current/");
+ cluster.kubeConfigPath = kubeconfigPath;
}
return cluster;
- })
+ });
- store.set("clusters", migratedClusters)
+ store.set("clusters", migratedClusters);
}
-})
+});
diff --git a/src/migrations/user-store/2.1.0-beta.4.ts b/src/migrations/user-store/2.1.0-beta.4.ts
index 24c4cde5e3..e8f6500b05 100644
--- a/src/migrations/user-store/2.1.0-beta.4.ts
+++ b/src/migrations/user-store/2.1.0-beta.4.ts
@@ -6,4 +6,4 @@ export default migration({
run(store) {
store.set("lastSeenAppVersion", "0.0.0");
}
-})
+});
diff --git a/src/migrations/user-store/index.ts b/src/migrations/user-store/index.ts
index 895bc5ee18..e1e7b8ffc9 100644
--- a/src/migrations/user-store/index.ts
+++ b/src/migrations/user-store/index.ts
@@ -1,7 +1,7 @@
// User store migrations
-import version210Beta4 from "./2.1.0-beta.4"
+import version210Beta4 from "./2.1.0-beta.4";
export default {
...version210Beta4,
-}
+};
diff --git a/src/renderer/api/__tests__/kube-api.test.ts b/src/renderer/api/__tests__/kube-api.test.ts
new file mode 100644
index 0000000000..41078e77a3
--- /dev/null
+++ b/src/renderer/api/__tests__/kube-api.test.ts
@@ -0,0 +1,82 @@
+import { KubeApi } from "../kube-api";
+
+describe("KubeApi", () => {
+ it("uses url from apiBase if apiBase contains the resource", async () => {
+ (fetch as any).mockResponse(async (request: any) => {
+ if (request.url === "/api-kube/apis/networking.k8s.io/v1") {
+ return {
+ body: JSON.stringify({
+ resources: [{
+ name: "ingresses"
+ }] as any []
+ })
+ };
+ } else if (request.url === "/api-kube/apis/extensions/v1beta1") {
+ // Even if the old API contains ingresses, KubeApi should prefer the apiBase url
+ return {
+ body: JSON.stringify({
+ resources: [{
+ name: "ingresses"
+ }] as any []
+ })
+ };
+ } else {
+ return {
+ body: JSON.stringify({
+ resources: [] as any []
+ })
+ };
+ }
+ });
+
+ const apiBase = "/apis/networking.k8s.io/v1/ingresses";
+ const fallbackApiBase = "/apis/extensions/v1beta1/ingresses";
+ const kubeApi = new KubeApi({
+ apiBase,
+ fallbackApiBases: [fallbackApiBase],
+ checkPreferredVersion: true,
+ });
+
+ await kubeApi.get();
+ expect(kubeApi.apiPrefix).toEqual("/apis");
+ expect(kubeApi.apiGroup).toEqual("networking.k8s.io");
+ });
+
+ it("uses url from fallbackApiBases if apiBase lacks the resource", async () => {
+ (fetch as any).mockResponse(async (request: any) => {
+ if (request.url === "/api-kube/apis/networking.k8s.io/v1") {
+ return {
+ body: JSON.stringify({
+ resources: [] as any []
+ })
+ };
+ } else if (request.url === "/api-kube/apis/extensions/v1beta1") {
+ return {
+ body: JSON.stringify({
+ resources: [{
+ name: "ingresses"
+ }] as any []
+ })
+ };
+ } else {
+ return {
+ body: JSON.stringify({
+ resources: [] as any []
+ })
+ };
+ }
+ });
+
+ const apiBase = "apis/networking.k8s.io/v1/ingresses";
+ const fallbackApiBase = "/apis/extensions/v1beta1/ingresses";
+ const kubeApi = new KubeApi({
+ apiBase,
+ fallbackApiBases: [fallbackApiBase],
+ checkPreferredVersion: true,
+ });
+
+ await kubeApi.get();
+ expect(kubeApi.apiPrefix).toEqual("/apis");
+ expect(kubeApi.apiGroup).toEqual("extensions");
+ });
+});
\ No newline at end of file
diff --git a/src/renderer/api/api-manager.ts b/src/renderer/api/api-manager.ts
index a0d90f4a13..98dbe206c7 100644
--- a/src/renderer/api/api-manager.ts
+++ b/src/renderer/api/api-manager.ts
@@ -24,7 +24,7 @@ export class ApiManager {
}
protected resolveApi(api: string | KubeApi): KubeApi {
- if (typeof api === "string") return this.getApi(api)
+ if (typeof api === "string") return this.getApi(api);
return api;
}
@@ -41,7 +41,7 @@ export class ApiManager {
registerStore(store: KubeObjectStore, apis: KubeApi[] = [store.api]) {
apis.forEach(api => {
this.stores.set(api, store);
- })
+ });
}
getStore(api: string | KubeApi): KubeObjectStore {
diff --git a/src/renderer/api/endpoints/cluster-role-binding.api.ts b/src/renderer/api/endpoints/cluster-role-binding.api.ts
index 35e4ded7e7..d566717bff 100644
--- a/src/renderer/api/endpoints/cluster-role-binding.api.ts
+++ b/src/renderer/api/endpoints/cluster-role-binding.api.ts
@@ -2,9 +2,9 @@ import { RoleBinding } from "./role-binding.api";
import { KubeApi } from "../kube-api";
export class ClusterRoleBinding extends RoleBinding {
- static kind = "ClusterRoleBinding"
- static namespaced = false
- static apiBase = "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings"
+ static kind = "ClusterRoleBinding";
+ static namespaced = false;
+ static apiBase = "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings";
}
export const clusterRoleBindingApi = new KubeApi({
diff --git a/src/renderer/api/endpoints/cluster-role.api.ts b/src/renderer/api/endpoints/cluster-role.api.ts
index 8a99f7ad27..9e3c90ca2e 100644
--- a/src/renderer/api/endpoints/cluster-role.api.ts
+++ b/src/renderer/api/endpoints/cluster-role.api.ts
@@ -4,9 +4,9 @@ import { KubeApi } from "../kube-api";
@autobind()
export class ClusterRole extends Role {
- static kind = "ClusterRole"
- static namespaced = false
- static apiBase = "/apis/rbac.authorization.k8s.io/v1/clusterroles"
+ static kind = "ClusterRole";
+ static namespaced = false;
+ static apiBase = "/apis/rbac.authorization.k8s.io/v1/clusterroles";
}
export const clusterRoleApi = new KubeApi({
diff --git a/src/renderer/api/endpoints/cluster.api.ts b/src/renderer/api/endpoints/cluster.api.ts
index 4386f28184..d3017c691a 100644
--- a/src/renderer/api/endpoints/cluster.api.ts
+++ b/src/renderer/api/endpoints/cluster.api.ts
@@ -3,12 +3,12 @@ import { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api";
export class ClusterApi extends KubeApi {
- static kind = "Cluster"
- static namespaced = true
+ static kind = "Cluster";
+ static namespaced = true;
async getMetrics(nodeNames: string[], params?: IMetricsReqParams): Promise {
const nodes = nodeNames.join("|");
- const opts = { category: "cluster", nodes: nodes }
+ const opts = { category: "cluster", nodes };
return metricsApi.getMetrics({
memoryUsage: opts,
@@ -52,7 +52,7 @@ export interface IClusterMetrics {
export class Cluster extends KubeObject {
static kind = "Cluster";
- static apiBase = "/apis/cluster.k8s.io/v1alpha1/clusters"
+ static apiBase = "/apis/cluster.k8s.io/v1alpha1/clusters";
spec: {
clusterNetwork?: {
@@ -69,7 +69,7 @@ export class Cluster extends KubeObject {
profile: string;
};
};
- }
+ };
status?: {
apiEndpoints: {
host: string;
@@ -84,7 +84,7 @@ export class Cluster extends KubeObject {
};
errorMessage?: string;
errorReason?: string;
- }
+ };
getStatus() {
if (this.metadata.deletionTimestamp) return ClusterStatus.REMOVING;
diff --git a/src/renderer/api/endpoints/component-status.api.ts b/src/renderer/api/endpoints/component-status.api.ts
index 7f7e04fe2a..fec4dda1da 100644
--- a/src/renderer/api/endpoints/component-status.api.ts
+++ b/src/renderer/api/endpoints/component-status.api.ts
@@ -8,11 +8,11 @@ export interface IComponentStatusCondition {
}
export class ComponentStatus extends KubeObject {
- static kind = "ComponentStatus"
- static namespaced = false
- static apiBase = "/api/v1/componentstatuses"
+ static kind = "ComponentStatus";
+ static namespaced = false;
+ static apiBase = "/api/v1/componentstatuses";
- conditions: IComponentStatusCondition[]
+ conditions: IComponentStatusCondition[];
getTruthyConditions() {
return this.conditions.filter(c => c.status === "True");
diff --git a/src/renderer/api/endpoints/configmap.api.ts b/src/renderer/api/endpoints/configmap.api.ts
index 59f3e3b090..042fb59d86 100644
--- a/src/renderer/api/endpoints/configmap.api.ts
+++ b/src/renderer/api/endpoints/configmap.api.ts
@@ -7,7 +7,7 @@ import { KubeApi } from "../kube-api";
export class ConfigMap extends KubeObject {
static kind = "ConfigMap";
static namespaced = true;
- static apiBase = "/api/v1/configmaps"
+ static apiBase = "/api/v1/configmaps";
constructor(data: KubeJsonApiData) {
super(data);
@@ -16,7 +16,7 @@ export class ConfigMap extends KubeObject {
data: {
[param: string]: string;
- }
+ };
getKeys(): string[] {
return Object.keys(this.data);
diff --git a/src/renderer/api/endpoints/crd.api.ts b/src/renderer/api/endpoints/crd.api.ts
index 1916d71f1b..02690a2afd 100644
--- a/src/renderer/api/endpoints/crd.api.ts
+++ b/src/renderer/api/endpoints/crd.api.ts
@@ -7,20 +7,20 @@ type AdditionalPrinterColumnsCommon = {
type: "integer" | "number" | "string" | "boolean" | "date";
priority: number;
description: string;
-}
+};
export type AdditionalPrinterColumnsV1 = AdditionalPrinterColumnsCommon & {
jsonPath: string;
-}
+};
type AdditionalPrinterColumnsV1Beta = AdditionalPrinterColumnsCommon & {
JSONPath: string;
-}
+};
export class CustomResourceDefinition extends KubeObject {
static kind = "CustomResourceDefinition";
static namespaced = false;
- static apiBase = "/apis/apiextensions.k8s.io/v1/customresourcedefinitions"
+ static apiBase = "/apis/apiextensions.k8s.io/v1/customresourcedefinitions";
spec: {
group: string;
@@ -45,7 +45,7 @@ export class CustomResourceDefinition extends KubeObject {
webhook?: any;
};
additionalPrinterColumns?: AdditionalPrinterColumnsV1Beta[]; // removed in v1
- }
+ };
status: {
conditions: {
lastTransitionTime: string;
@@ -62,7 +62,7 @@ export class CustomResourceDefinition extends KubeObject {
listKind: string;
};
storedVersions: string[];
- }
+ };
getResourceUrl() {
return crdResourcesURL({
@@ -70,25 +70,25 @@ export class CustomResourceDefinition extends KubeObject {
group: this.getGroup(),
name: this.getPluralName(),
}
- })
+ });
}
getResourceApiBase() {
const { group } = this.spec;
- return `/apis/${group}/${this.getVersion()}/${this.getPluralName()}`
+ return `/apis/${group}/${this.getVersion()}/${this.getPluralName()}`;
}
getPluralName() {
- return this.getNames().plural
+ return this.getNames().plural;
}
getResourceKind() {
- return this.spec.names.kind
+ return this.spec.names.kind;
}
getResourceTitle() {
const name = this.getPluralName();
- return name[0].toUpperCase() + name.substr(1)
+ return name[0].toUpperCase() + name.substr(1);
}
getGroup() {
@@ -141,7 +141,7 @@ export class CustomResourceDefinition extends KubeObject {
...condition,
isReady: status === "True",
tooltip: `${message || reason} (${lastTransitionTime})`
- }
+ };
});
}
}
diff --git a/src/renderer/api/endpoints/cron-job.api.ts b/src/renderer/api/endpoints/cron-job.api.ts
index b385647bb9..2cca8bfb3d 100644
--- a/src/renderer/api/endpoints/cron-job.api.ts
+++ b/src/renderer/api/endpoints/cron-job.api.ts
@@ -7,12 +7,12 @@ import { KubeApi } from "../kube-api";
@autobind()
export class CronJob extends KubeObject {
- static kind = "CronJob"
- static namespaced = true
- static apiBase = "/apis/batch/v1beta1/cronjobs"
+ static kind = "CronJob";
+ static namespaced = true;
+ static apiBase = "/apis/batch/v1beta1/cronjobs";
- kind: string
- apiVersion: string
+ kind: string;
+ apiVersion: string;
metadata: {
name: string;
namespace: string;
@@ -26,7 +26,7 @@ export class CronJob extends KubeObject {
annotations: {
[key: string]: string;
};
- }
+ };
spec: {
schedule: string;
concurrencyPolicy: string;
@@ -59,23 +59,23 @@ export class CronJob extends KubeObject {
};
successfulJobsHistoryLimit: number;
failedJobsHistoryLimit: number;
- }
+ };
status: {
lastScheduleTime?: string;
- }
+ };
getSuspendFlag() {
- return this.spec.suspend.toString()
+ return this.spec.suspend.toString();
}
getLastScheduleTime() {
- if (!this.status.lastScheduleTime) return "-"
- const diff = moment().diff(this.status.lastScheduleTime)
- return formatDuration(diff, true)
+ if (!this.status.lastScheduleTime) return "-";
+ const diff = moment().diff(this.status.lastScheduleTime);
+ return formatDuration(diff, true);
}
getSchedule() {
- return this.spec.schedule
+ return this.spec.schedule;
}
isNeverRun() {
diff --git a/src/renderer/api/endpoints/daemon-set.api.ts b/src/renderer/api/endpoints/daemon-set.api.ts
index d947293c1c..63fc6363e4 100644
--- a/src/renderer/api/endpoints/daemon-set.api.ts
+++ b/src/renderer/api/endpoints/daemon-set.api.ts
@@ -6,9 +6,9 @@ import { KubeApi } from "../kube-api";
@autobind()
export class DaemonSet extends WorkloadKubeObject {
- static kind = "DaemonSet"
- static namespaced = true
- static apiBase = "/apis/apps/v1/daemonsets"
+ static kind = "DaemonSet";
+ static namespaced = true;
+ static apiBase = "/apis/apps/v1/daemonsets";
spec: {
selector: {
@@ -51,7 +51,7 @@ export class DaemonSet extends WorkloadKubeObject {
};
};
revisionHistoryLimit: number;
- }
+ };
status: {
currentNumberScheduled: number;
numberMisscheduled: number;
@@ -61,12 +61,12 @@ export class DaemonSet extends WorkloadKubeObject {
updatedNumberScheduled: number;
numberAvailable: number;
numberUnavailable: number;
- }
+ };
getImages() {
- const containers: IPodContainer[] = get(this, "spec.template.spec.containers", [])
- const initContainers: IPodContainer[] = get(this, "spec.template.spec.initContainers", [])
- return [...containers, ...initContainers].map(container => container.image)
+ const containers: IPodContainer[] = get(this, "spec.template.spec.containers", []);
+ const initContainers: IPodContainer[] = get(this, "spec.template.spec.initContainers", []);
+ return [...containers, ...initContainers].map(container => container.image);
}
}
diff --git a/src/renderer/api/endpoints/deployment.api.ts b/src/renderer/api/endpoints/deployment.api.ts
index b21495ecc1..0279d63ed1 100644
--- a/src/renderer/api/endpoints/deployment.api.ts
+++ b/src/renderer/api/endpoints/deployment.api.ts
@@ -6,13 +6,13 @@ import { KubeApi } from "../kube-api";
export class DeploymentApi extends KubeApi {
protected getScaleApiUrl(params: { namespace: string; name: string }) {
- return this.getUrl(params) + "/scale"
+ return this.getUrl(params) + "/scale";
}
getReplicas(params: { namespace: string; name: string }): Promise