(constructor: T) {
Object.keys(descriptors).forEach(prop => {
if (skipMethod(prop)) return;
const boundDescriptor = bindMethod(proto, prop, descriptors[prop]);
+
Object.defineProperty(proto, prop, boundDescriptor);
});
}
@@ -38,6 +38,7 @@ function bindMethod(target: object, prop?: string, descriptor?: PropertyDescript
get() {
if (this === target) return func; // direct access from prototype
if (!boundFunc.has(this)) boundFunc.set(this, func.bind(this));
+
return boundFunc.get(this);
}
});
diff --git a/src/common/utils/buildUrl.ts b/src/common/utils/buildUrl.ts
index c26e054491..e3bad9b302 100644
--- a/src/common/utils/buildUrl.ts
+++ b/src/common/utils/buildUrl.ts
@@ -7,8 +7,10 @@ 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}` : "");
};
}
diff --git a/src/common/utils/camelCase.ts b/src/common/utils/camelCase.ts
index 90c048cad5..306cb45190 100644
--- a/src/common/utils/camelCase.ts
+++ b/src/common/utils/camelCase.ts
@@ -8,7 +8,9 @@ export function toCamelCase(obj: Record): any {
else if (isPlainObject(obj)) {
return Object.keys(obj).reduce((result, key) => {
const value = obj[key];
+
result[camelCase(key)] = typeof value === "object" ? toCamelCase(value) : value;
+
return result;
}, {} as any);
}
diff --git a/src/common/utils/debouncePromise.ts b/src/common/utils/debouncePromise.ts
index e03c0e76bd..b5ad88d000 100755
--- a/src/common/utils/debouncePromise.ts
+++ b/src/common/utils/debouncePromise.ts
@@ -2,6 +2,7 @@
export function debouncePromise(func: (...args: F) => T | Promise, timeout = 0): (...args: F) => Promise {
let timer: NodeJS.Timeout;
+
return (...params: any[]) => new Promise(resolve => {
clearTimeout(timer);
timer = global.setTimeout(() => resolve(func.apply(this, params)), timeout);
diff --git a/src/common/utils/downloadFile.ts b/src/common/utils/downloadFile.ts
index 4c65901d3d..dfa549da07 100644
--- a/src/common/utils/downloadFile.ts
+++ b/src/common/utils/downloadFile.ts
@@ -26,6 +26,7 @@ export function downloadFile({ url, timeout, gzip = true }: DownloadFileOptions)
resolve(Buffer.concat(fileChunks));
});
});
+
return {
url,
promise,
diff --git a/src/common/utils/getRandId.ts b/src/common/utils/getRandId.ts
index afe075085d..ef02e2f0eb 100644
--- a/src/common/utils/getRandId.ts
+++ b/src/common/utils/getRandId.ts
@@ -2,5 +2,6 @@
export function getRandId({ prefix = "", suffix = "", sep = "_" } = {}) {
const randId = () => Math.random().toString(16).substr(2);
+
return [prefix, randId(), suffix].filter(s => s).join(sep);
}
diff --git a/src/common/utils/saveToAppFiles.ts b/src/common/utils/saveToAppFiles.ts
index 57c47f0d70..87d09290c0 100644
--- a/src/common/utils/saveToAppFiles.ts
+++ b/src/common/utils/saveToAppFiles.ts
@@ -6,7 +6,9 @@ import { WriteFileOptions } from "fs";
export function saveToAppFiles(filePath: string, contents: any, options?: WriteFileOptions): string {
const absPath = path.resolve((app || remote.app).getPath("userData"), filePath);
+
ensureDirSync(path.dirname(absPath));
writeFileSync(absPath, contents, options);
+
return absPath;
}
diff --git a/src/common/utils/singleton.ts b/src/common/utils/singleton.ts
index ed3f0cc962..61269d10b1 100644
--- a/src/common/utils/singleton.ts
+++ b/src/common/utils/singleton.ts
@@ -16,6 +16,7 @@ class Singleton {
if (!Singleton.instances.has(this)) {
Singleton.instances.set(this, Reflect.construct(this, args));
}
+
return Singleton.instances.get(this) as T;
}
diff --git a/src/common/utils/splitArray.ts b/src/common/utils/splitArray.ts
index f93392f736..7be367ebe6 100644
--- a/src/common/utils/splitArray.ts
+++ b/src/common/utils/splitArray.ts
@@ -12,8 +12,10 @@
*/
export function splitArray(array: T[], element: T): [T[], T[], boolean] {
const index = array.indexOf(element);
+
if (index < 0) {
return [array, [], false];
}
+
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
index 004fa354dc..f9876e2b27 100644
--- a/src/common/utils/tar.ts
+++ b/src/common/utils/tar.ts
@@ -15,7 +15,7 @@ export function readFileFromTar({ tarPath, filePath, parseJson }: Re
await tar.list({
file: tarPath,
- filter: path => path === filePath,
+ filter: entryPath => path.normalize(entryPath) === filePath,
onentry(entry: FileStat) {
entry.on("data", chunk => {
fileChunks.push(chunk);
@@ -26,6 +26,7 @@ export function readFileFromTar({ tarPath, filePath, parseJson }: Re
entry.once("end", () => {
const data = Buffer.concat(fileChunks);
const result = parseJson ? JSON.parse(data.toString("utf8")) : data;
+
resolve(result);
});
},
@@ -39,10 +40,14 @@ export function readFileFromTar({ tarPath, filePath, parseJson }: Re
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),
+ onentry: (entry: FileStat) => {
+ entries.push(path.normalize(entry.path as any as string));
+ },
});
+
return entries;
}
diff --git a/src/common/vars.ts b/src/common/vars.ts
index ac9f1336ee..396a1077c5 100644
--- a/src/common/vars.ts
+++ b/src/common/vars.ts
@@ -30,6 +30,7 @@ defineGlobal("__static", {
if (isDevelopment) {
return path.resolve(contextDir, "static");
}
+
return path.resolve(process.resourcesPath, "static");
}
});
diff --git a/src/common/workspace-store.ts b/src/common/workspace-store.ts
index 3827ce0e2c..7688516af2 100644
--- a/src/common/workspace-store.ts
+++ b/src/common/workspace-store.ts
@@ -3,7 +3,7 @@ import { action, computed, observable, toJS, reaction } from "mobx";
import { BaseStore } from "./base-store";
import { clusterStore } from "./cluster-store";
import { appEventBus } from "./event-bus";
-import { broadcastMessage } from "../common/ipc";
+import { broadcastMessage, handleRequest, requestMain } from "../common/ipc";
import logger from "../main/logger";
import type { ClusterId } from "./cluster-store";
@@ -33,32 +33,44 @@ export interface WorkspaceState {
*/
export class Workspace implements WorkspaceModel, WorkspaceState {
/**
- * Unique id
+ * Unique id for workspace
+ *
* @observable
*/
@observable id: WorkspaceId;
/**
* Workspace name
+ *
* @observable
*/
@observable name: string;
/**
- * Description
+ * Workspace description
+ *
* @observable
*/
@observable description?: string;
/**
- * Owner reference
+ * Workspace owner reference
+ *
+ * If extension sets ownerRef then it needs to explicitly mark workspace as enabled onActivate (or when workspace is saved)
*
- * If an extension sets this then extension also needs explicitly to set workspace as enabled
* @observable
*/
@observable ownerRef?: string;
/**
- * Is workspace enabled (disabled workspaces are currently hidden)
+ * Is workspace enabled
+ *
+ * Workspaces that don't have ownerRef will be enabled by default. Workspaces with ownerRef need to explicitly enable a workspace.
+ *
* @observable
*/
@observable enabled: boolean;
+ /**
+ * Last active cluster id
+ *
+ * @observable
+ */
@observable lastActiveClusterId?: ClusterId;
constructor(data: WorkspaceModel) {
@@ -83,9 +95,9 @@ export class Workspace implements WorkspaceModel, WorkspaceState {
*
*/
getState(): WorkspaceState {
- return {
+ return toJS({
enabled: this.enabled
- };
+ });
}
/**
@@ -120,16 +132,45 @@ export class Workspace implements WorkspaceModel, WorkspaceState {
export class WorkspaceStore extends BaseStore {
static readonly defaultId: WorkspaceId = "default";
+ private static stateRequestChannel = "workspace:states";
private constructor() {
super({
configName: "lens-workspace-store",
});
+ }
- if (!ipcRenderer) {
- setInterval(() => {
- this.pushState();
- }, 5000);
+ async load() {
+ await super.load();
+ type workspaceStateSync = {
+ id: string;
+ state: WorkspaceState;
+ };
+
+ if (ipcRenderer) {
+ logger.info("[WORKSPACE-STORE] requesting initial state sync");
+ const workspaceStates: workspaceStateSync[] = await requestMain(WorkspaceStore.stateRequestChannel);
+
+ workspaceStates.forEach((workspaceState) => {
+ const workspace = this.getById(workspaceState.id);
+
+ if (workspace) {
+ workspace.setState(workspaceState.state);
+ }
+ });
+ } else {
+ handleRequest(WorkspaceStore.stateRequestChannel, (): workspaceStateSync[] => {
+ const states: workspaceStateSync[] = [];
+
+ this.workspacesList.forEach((workspace) => {
+ states.push({
+ state: workspace.getState(),
+ id: workspace.id
+ });
+ });
+
+ return states;
+ });
}
}
@@ -187,6 +228,7 @@ export class WorkspaceStore extends BaseStore {
@action
setActive(id = WorkspaceStore.defaultId) {
if (id === this.currentWorkspaceId) return;
+
if (!this.getById(id)) {
throw new Error(`workspace ${id} doesn't exist`);
}
@@ -196,11 +238,18 @@ export class WorkspaceStore extends BaseStore {
@action
addWorkspace(workspace: Workspace) {
const { id, name } = workspace;
+
if (!name.trim() || this.getByName(name.trim())) {
return;
}
this.workspaces.set(id, workspace);
+
+ if (!workspace.isManaged) {
+ workspace.enabled = true;
+ }
+
appEventBus.emit({name: "workspace", action: "add"});
+
return workspace;
}
@@ -218,10 +267,13 @@ export class WorkspaceStore extends BaseStore {
@action
removeWorkspaceById(id: WorkspaceId) {
const workspace = this.getById(id);
+
if (!workspace) return;
+
if (this.isDefault(id)) {
throw new Error("Cannot remove default workspace");
}
+
if (this.currentWorkspaceId === id) {
this.currentWorkspaceId = WorkspaceStore.defaultId; // reset to default
}
@@ -240,10 +292,12 @@ export class WorkspaceStore extends BaseStore {
if (currentWorkspace) {
this.currentWorkspaceId = currentWorkspace;
}
+
if (workspaces.length) {
this.workspaces.clear();
workspaces.forEach(ws => {
const workspace = new Workspace(ws);
+
if (!workspace.isManaged) {
workspace.enabled = true;
}
diff --git a/src/extensions/__tests__/extension-loader.test.ts b/src/extensions/__tests__/extension-loader.test.ts
index d2eec85a79..335eb50912 100644
--- a/src/extensions/__tests__/extension-loader.test.ts
+++ b/src/extensions/__tests__/extension-loader.test.ts
@@ -1,15 +1,24 @@
import { ExtensionLoader } from "../extension-loader";
+import { ipcRenderer } from "electron";
+import { extensionsStore } from "../extensions-store";
const manifestPath = "manifest/path";
const manifestPath2 = "manifest/path2";
const manifestPath3 = "manifest/path3";
+jest.mock("../extensions-store", () => ({
+ extensionsStore: {
+ whenLoaded: Promise.resolve(true),
+ mergeState: jest.fn()
+ }
+}));
+
jest.mock(
"electron",
() => ({
ipcRenderer: {
invoke: jest.fn(async (channel: string) => {
- if (channel === "extensions:loaded") {
+ if (channel === "extensions:main") {
return [
[
manifestPath,
@@ -44,7 +53,7 @@ jest.mock(
}),
on: jest.fn(
(channel: string, listener: (event: any, ...args: any[]) => void) => {
- if (channel === "extensions:loaded") {
+ if (channel === "extensions:main") {
// First initialize with extensions 1 and 2
// and then broadcast event to remove extensioin 2 and add extension number 3
setTimeout(() => {
@@ -129,4 +138,29 @@ describe("ExtensionLoader", () => {
done();
}, 10);
});
+
+ it("updates ExtensionsStore after isEnabled is changed", async () => {
+ (extensionsStore.mergeState as any).mockClear();
+
+ // Disable sending events in this test
+ (ipcRenderer.on as any).mockImplementation();
+
+ const extensionLoader = new ExtensionLoader();
+
+ await extensionLoader.init();
+
+ expect(extensionsStore.mergeState).not.toHaveBeenCalled();
+
+ Array.from(extensionLoader.userExtensions.values())[0].isEnabled = false;
+
+ expect(extensionsStore.mergeState).toHaveBeenCalledWith({
+ "manifest/path": {
+ enabled: false,
+ name: "TestExtension"
+ },
+ "manifest/path2": {
+ enabled: true,
+ name: "TestExtension2"
+ }});
+ });
});
diff --git a/src/extensions/cluster-feature.ts b/src/extensions/cluster-feature.ts
index bb60e9d9b4..625f2b5973 100644
--- a/src/extensions/cluster-feature.ts
+++ b/src/extensions/cluster-feature.ts
@@ -108,12 +108,15 @@ export abstract class ClusterFeature {
*/
protected renderTemplates(folderPath: string): string[] {
const resources: string[] = [];
+
logger.info(`[FEATURE]: render templates from ${folderPath}`);
fs.readdirSync(folderPath).forEach(filename => {
const file = path.join(folderPath, filename);
const raw = fs.readFileSync(file);
+
if (filename.endsWith(".hb")) {
const template = hb.compile(raw.toString());
+
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 2664711db4..2c3a7a4f59 100644
--- a/src/extensions/core-api/app.ts
+++ b/src/extensions/core-api/app.ts
@@ -3,6 +3,7 @@ import { extensionsStore } from "../extensions-store";
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/extension-discovery.ts b/src/extensions/extension-discovery.ts
index 10597c3ceb..105b8e2041 100644
--- a/src/extensions/extension-discovery.ts
+++ b/src/extensions/extension-discovery.ts
@@ -25,6 +25,7 @@ export interface InstalledExtension {
}
const logModule = "[EXTENSION-DISCOVERY]";
+
export const manifestFilename = "package.json";
/**
@@ -113,8 +114,8 @@ export class ExtensionDiscovery {
// chokidar works better than fs.watch
chokidar.watch(this.localFolderPath, {
- // Dont watch recursively into subdirectories
- depth: 0,
+ // For adding and removing symlinks to work, the depth has to be 1.
+ depth: 1,
// Try to wait until the file has been completely copied.
// The OS might emit an event for added file even it's not completely written to the filesysten.
awaitWriteFinish: {
@@ -123,7 +124,7 @@ export class ExtensionDiscovery {
stabilityThreshold: 300
}
})
- // Extension add is detected by watching "package.json" add
+ // Extension add is detected by watching "/package.json" add
.on("add", this.handleWatchFileAdd)
// Extension remove is detected by watching " unlink
.on("unlinkDir", this.handleWatchUnlinkDir);
@@ -133,7 +134,6 @@ export class ExtensionDiscovery {
if (path.basename(filePath) === manifestFilename) {
try {
const absPath = path.dirname(filePath);
-
// this.loadExtensionFromPath updates this.packagesJson
const extension = await this.loadExtensionFromPath(absPath);
@@ -189,7 +189,7 @@ export class ExtensionDiscovery {
*/
async uninstallExtension(absolutePath: string) {
logger.info(`${logModule} Uninstalling ${absolutePath}`);
-
+
const exists = await fs.pathExists(absolutePath);
if (!exists) {
@@ -251,6 +251,7 @@ export class ExtensionDiscovery {
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);
@@ -272,6 +273,7 @@ export class ExtensionDiscovery {
async loadExtensions(): Promise> {
const bundledExtensions = await this.loadBundledExtensions();
const localExtensions = await this.loadFromFolder(this.localFolderPath);
+
await this.installPackages();
const extensions = bundledExtensions.concat(localExtensions);
@@ -333,12 +335,14 @@ export class ExtensionDiscovery {
}
const extension = await this.loadExtensionFromPath(absPath);
+
if (extension) {
extensions.push(extension);
}
}
logger.debug(`${logModule}: ${extensions.length} extensions loaded`, { folderPath, extensions });
+
return extensions;
}
diff --git a/src/extensions/extension-installer.ts b/src/extensions/extension-installer.ts
index 42863fe60e..2143c62287 100644
--- a/src/extensions/extension-installer.ts
+++ b/src/extensions/extension-installer.ts
@@ -37,6 +37,7 @@ export class ExtensionInstaller {
cwd: extensionPackagesRoot(),
silent: true
});
+
child.on("close", () => {
resolve();
});
diff --git a/src/extensions/extension-loader.ts b/src/extensions/extension-loader.ts
index eb49021391..966289f157 100644
--- a/src/extensions/extension-loader.ts
+++ b/src/extensions/extension-loader.ts
@@ -1,5 +1,6 @@
import { app, ipcRenderer, remote } from "electron";
import { EventEmitter } from "events";
+import { isEqual } from "lodash";
import { action, computed, observable, reaction, toJS, when } from "mobx";
import path from "path";
import { getHostedCluster } from "../common/cluster-store";
@@ -25,7 +26,12 @@ const logModule = "[EXTENSIONS-LOADER]";
export class ExtensionLoader {
protected extensions = observable.map();
protected instances = observable.map();
- protected readonly requestExtensionsChannel = "extensions:loaded";
+
+ // IPC channel to broadcast changes to extensions from main
+ protected static readonly extensionsMainChannel = "extensions:main";
+
+ // IPC channel to broadcast changes to extensions from renderer
+ protected static readonly extensionsRendererChannel = "extensions:renderer";
// emits event "remove" of type LensExtension when the extension is removed
private events = new EventEmitter();
@@ -45,6 +51,17 @@ export class ExtensionLoader {
return extensions;
}
+ // Transform userExtensions to a state object for storing into ExtensionsStore
+ @computed get storeState() {
+ return Object.fromEntries(
+ Array.from(this.userExtensions)
+ .map(([extId, extension]) => [extId, {
+ enabled: extension.isEnabled,
+ name: extension.manifest.name,
+ }])
+ );
+ }
+
@action
async init() {
if (ipcRenderer) {
@@ -53,7 +70,12 @@ export class ExtensionLoader {
await this.initMain();
}
- extensionsStore.manageState(this);
+ await Promise.all([this.whenLoaded, extensionsStore.whenLoaded]);
+
+ // save state on change `extension.isEnabled`
+ reaction(() => this.storeState, extensionsState => {
+ extensionsStore.mergeState(extensionsState);
+ });
}
initExtensions(extensions?: Map) {
@@ -95,28 +117,27 @@ export class ExtensionLoader {
this.loadOnMain();
this.broadcastExtensions();
- reaction(() => this.extensions.toJS(), () => {
+ reaction(() => this.toJSON(), () => {
this.broadcastExtensions();
});
- handleRequest(this.requestExtensionsChannel, () => {
+ handleRequest(ExtensionLoader.extensionsMainChannel, () => {
return Array.from(this.toJSON());
});
+
+ subscribeToBroadcast(ExtensionLoader.extensionsRendererChannel, (_event, extensions: [LensExtensionId, InstalledExtension][]) => {
+ this.syncExtensions(extensions);
+ });
}
protected async initRenderer() {
const extensionListHandler = (extensions: [LensExtensionId, InstalledExtension][]) => {
this.isLoaded = true;
+ this.syncExtensions(extensions);
+
const receivedExtensionIds = extensions.map(([lensExtensionId]) => lensExtensionId);
-
- // Add new extensions
- extensions.forEach(([extId, ext]) => {
- if (!this.extensions.has(extId)) {
- this.extensions.set(extId, ext);
- }
- });
-
- // Remove deleted extensions
+
+ // Remove deleted extensions in renderer side only
this.extensions.forEach((_, lensExtensionId) => {
if (!receivedExtensionIds.includes(lensExtensionId)) {
this.removeExtension(lensExtensionId);
@@ -124,14 +145,26 @@ export class ExtensionLoader {
});
};
- requestMain(this.requestExtensionsChannel).then(extensionListHandler);
- subscribeToBroadcast(this.requestExtensionsChannel, (event, extensions: [LensExtensionId, InstalledExtension][]) => {
+ reaction(() => this.toJSON(), () => {
+ this.broadcastExtensions(false);
+ });
+
+ requestMain(ExtensionLoader.extensionsMainChannel).then(extensionListHandler);
+ subscribeToBroadcast(ExtensionLoader.extensionsMainChannel, (_event, extensions: [LensExtensionId, InstalledExtension][]) => {
extensionListHandler(extensions);
});
}
+ syncExtensions(extensions: [LensExtensionId, InstalledExtension][]) {
+ extensions.forEach(([lensExtensionId, extension]) => {
+ if (!isEqual(this.extensions.get(lensExtensionId), extension)) {
+ this.extensions.set(lensExtensionId, extension);
+ }
+ });
+ }
+
loadOnMain() {
- logger.info(`${logModule}: load on main`);
+ logger.debug(`${logModule}: load on main`);
this.autoInitExtensions(async (extension: LensMainExtension) => {
// Each .add returns a function to remove the item
const removeItems = [
@@ -151,7 +184,7 @@ export class ExtensionLoader {
}
loadOnClusterManagerRenderer() {
- logger.info(`${logModule}: load on main renderer (cluster manager)`);
+ logger.debug(`${logModule}: load on main renderer (cluster manager)`);
this.autoInitExtensions(async (extension: LensRendererExtension) => {
const removeItems = [
registries.globalPageRegistry.add(extension.globalPages, extension),
@@ -174,8 +207,9 @@ export class ExtensionLoader {
}
loadOnClusterRenderer() {
- logger.info(`${logModule}: load on cluster renderer (dashboard)`);
+ logger.debug(`${logModule}: load on cluster renderer (dashboard)`);
const cluster = getHostedCluster();
+
this.autoInitExtensions(async (extension: LensRendererExtension) => {
if (await extension.isEnabledForCluster(cluster) === false) {
return [];
@@ -203,24 +237,26 @@ export class ExtensionLoader {
protected autoInitExtensions(register: (ext: LensExtension) => Promise) {
return reaction(() => this.toJSON(), installedExtensions => {
- for (const [extId, ext] of installedExtensions) {
+ for (const [extId, extension] of installedExtensions) {
const alreadyInit = this.instances.has(extId);
- if (ext.isEnabled && !alreadyInit) {
+ if (extension.isEnabled && !alreadyInit) {
try {
- const LensExtensionClass = this.requireExtension(ext);
+ const LensExtensionClass = this.requireExtension(extension);
+
if (!LensExtensionClass) {
continue;
}
- const instance = new LensExtensionClass(ext);
+ const instance = new LensExtensionClass(extension);
+
instance.whenEnabled(() => register(instance));
instance.enable();
this.instances.set(extId, instance);
} catch (err) {
- logger.error(`${logModule}: activation extension error`, { ext, err });
+ logger.error(`${logModule}: activation extension error`, { ext: extension, err });
}
- } else if (!ext.isEnabled && alreadyInit) {
+ } else if (!extension.isEnabled && alreadyInit) {
this.removeInstance(extId);
}
}
@@ -231,12 +267,14 @@ export class ExtensionLoader {
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));
} else if (!ipcRenderer && extension.manifest.main) {
extEntrypoint = path.resolve(path.join(path.dirname(extension.manifestPath), extension.manifest.main));
}
+
if (extEntrypoint !== "") {
return __non_webpack_require__(extEntrypoint).default;
}
@@ -257,8 +295,8 @@ export class ExtensionLoader {
});
}
- broadcastExtensions() {
- broadcastMessage(this.requestExtensionsChannel, Array.from(this.toJSON()));
+ broadcastExtensions(main = true) {
+ broadcastMessage(main ? ExtensionLoader.extensionsMainChannel : ExtensionLoader.extensionsRendererChannel, Array.from(this.toJSON()));
}
}
diff --git a/src/extensions/extension-store.ts b/src/extensions/extension-store.ts
index a8078994ca..c1a1e62bd8 100644
--- a/src/extensions/extension-store.ts
+++ b/src/extensions/extension-store.ts
@@ -7,11 +7,13 @@ export abstract class ExtensionStore extends BaseStore {
async loadExtension(extension: LensExtension) {
this.extension = extension;
+
return super.load();
}
async load() {
if (!this.extension) { return; }
+
return super.load();
}
diff --git a/src/extensions/extensions-store.ts b/src/extensions/extensions-store.ts
index 2f865a165b..0885bbb730 100644
--- a/src/extensions/extensions-store.ts
+++ b/src/extensions/extensions-store.ts
@@ -1,7 +1,6 @@
import type { LensExtensionId } from "./lens-extension";
-import type { ExtensionLoader } from "./extension-loader";
import { BaseStore } from "../common/base-store";
-import { action, computed, observable, reaction, toJS } from "mobx";
+import { action, computed, observable, toJS } from "mobx";
export interface LensExtensionsStoreModel {
extensions: Record;
@@ -28,40 +27,17 @@ export class ExtensionsStore extends BaseStore {
protected state = observable.map();
- protected getState(extensionLoader: ExtensionLoader) {
- const state: Record = {};
- return Array.from(extensionLoader.userExtensions).reduce((state, [extId, ext]) => {
- state[extId] = {
- enabled: ext.isEnabled,
- name: ext.manifest.name,
- };
- return state;
- }, state);
- }
-
- async manageState(extensionLoader: ExtensionLoader) {
- await extensionLoader.whenLoaded;
- await this.whenLoaded;
-
- // apply state on changes from store
- reaction(() => this.state.toJS(), extensionsState => {
- extensionsState.forEach((state, extId) => {
- const ext = extensionLoader.getExtension(extId);
- if (ext && !ext.isBundled) {
- ext.isEnabled = state.enabled;
- }
- });
- });
-
- // save state on change `extension.isEnabled`
- reaction(() => this.getState(extensionLoader), extensionsState => {
- this.state.merge(extensionsState);
- });
- }
-
isEnabled(extId: LensExtensionId) {
const state = this.state.get(extId);
- return state && state.enabled; // by default false
+
+ // By default false, so that copied extensions are disabled by default.
+ // If user installs the extension from the UI, the Extensions component will specifically enable it.
+ return Boolean(state?.enabled);
+ }
+
+ @action
+ mergeState(extensionsState: Record) {
+ this.state.merge(extensionsState);
}
@action
diff --git a/src/extensions/lens-extension.ts b/src/extensions/lens-extension.ts
index 61dc6c0560..aaa6f60ac5 100644
--- a/src/extensions/lens-extension.ts
+++ b/src/extensions/lens-extension.ts
@@ -86,6 +86,7 @@ export class LensExtension {
const cancelReaction = reaction(() => this.isEnabled, async (isEnabled) => {
if (isEnabled) {
const handlerDisposers = await handlers();
+
disposers.push(...handlerDisposers);
} else {
unregisterHandlers();
@@ -93,6 +94,7 @@ export class LensExtension {
}, {
fireImmediately: true
});
+
return () => {
unregisterHandlers();
cancelReaction();
diff --git a/src/extensions/lens-main-extension.ts b/src/extensions/lens-main-extension.ts
index ab4d7a2a1f..f0e943540d 100644
--- a/src/extensions/lens-main-extension.ts
+++ b/src/extensions/lens-main-extension.ts
@@ -13,6 +13,7 @@ export class LensMainExtension extends LensExtension {
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 0c11306efd..b6c00d8353 100644
--- a/src/extensions/lens-renderer-extension.ts
+++ b/src/extensions/lens-renderer-extension.ts
@@ -23,6 +23,7 @@ export class LensRendererExtension extends LensExtension {
pageId,
params: params ?? {}, // compile to url with params
});
+
navigate(pageUrl);
}
diff --git a/src/extensions/registries/__tests__/page-registry.test.ts b/src/extensions/registries/__tests__/page-registry.test.ts
index b7f2cd1252..78db140ed7 100644
--- a/src/extensions/registries/__tests__/page-registry.test.ts
+++ b/src/extensions/registries/__tests__/page-registry.test.ts
@@ -73,6 +73,7 @@ describe("globalPageRegistry", () => {
describe("getByPageMenuTarget", () => {
it("matching to first registered page without id", () => {
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 }));
@@ -83,6 +84,7 @@ describe("globalPageRegistry", () => {
pageId: "test-page",
extensionId: ext.name
});
+
expect(page.id).toEqual("test-page");
});
@@ -91,6 +93,7 @@ describe("globalPageRegistry", () => {
pageId: "wrong-page",
extensionId: ext.name
});
+
expect(page).toBeNull();
});
});
diff --git a/src/extensions/registries/base-registry.ts b/src/extensions/registries/base-registry.ts
index 4bfb3f9cd2..6d5485b32b 100644
--- a/src/extensions/registries/base-registry.ts
+++ b/src/extensions/registries/base-registry.ts
@@ -14,7 +14,9 @@ export class BaseRegistry {
@action
add(items: T | T[]) {
const itemArray = rectify(items);
+
this.items.push(...itemArray);
+
return () => this.remove(...itemArray);
}
diff --git a/src/extensions/registries/kube-object-detail-registry.ts b/src/extensions/registries/kube-object-detail-registry.ts
index 2638e82ade..9c79b662ea 100644
--- a/src/extensions/registries/kube-object-detail-registry.ts
+++ b/src/extensions/registries/kube-object-detail-registry.ts
@@ -20,8 +20,10 @@ export class KubeObjectDetailRegistry extends BaseRegistry b.priority - a.priority);
}
}
diff --git a/src/extensions/registries/page-menu-registry.ts b/src/extensions/registries/page-menu-registry.ts
index 9f5c4861a4..8ccbc9cd6c 100644
--- a/src/extensions/registries/page-menu-registry.ts
+++ b/src/extensions/registries/page-menu-registry.ts
@@ -35,8 +35,10 @@ export class GlobalPageMenuRegistry extends BaseRegistry {
extensionId: ext.name,
...(menuItem.target || {}),
};
+
return menuItem;
});
+
return super.add(normalizedItems);
}
}
@@ -49,8 +51,10 @@ export class ClusterPageMenuRegistry extends BaseRegistry({ extensionId, pageId = ""
const extensionBaseUrl = compile(`/extension/:name`)({
name: sanitizeExtensionName(extensionId), // compile only with extension-id first and define base path
});
- const extPageRoutePath = path.join(extensionBaseUrl, pageId); // page-id might contain route :param-s, so don't compile yet
+ const extPageRoutePath = path.posix.join(extensionBaseUrl, pageId);
+
if (params) {
return compile(extPageRoutePath)(params); // might throw error when required params not passed
}
+
return extPageRoutePath;
}
@@ -56,6 +58,7 @@ export class PageRegistry extends BaseRegistry {
add(items: PageRegistration | PageRegistration[], ext: LensExtension) {
const itemArray = rectify(items);
let registeredPages: RegisteredPage[] = [];
+
try {
registeredPages = itemArray.map(page => ({
...page,
@@ -69,6 +72,7 @@ export class PageRegistry extends BaseRegistry {
error: String(err),
});
}
+
return super.add(registeredPages);
}
@@ -78,8 +82,10 @@ export class PageRegistry extends BaseRegistry {
getByPageMenuTarget(target: PageMenuTarget = {}): RegisteredPage | null {
const targetUrl = getExtensionPageUrl(target);
+
return this.getItems().find(({ id: pageId, extensionId }) => {
const pageUrl = getExtensionPageUrl({ extensionId, pageId, params: target.params }); // compiled with provided params
+
return targetUrl === pageUrl;
}) || null;
}
diff --git a/src/extensions/stores/cluster-store.ts b/src/extensions/stores/cluster-store.ts
index f074ef4a1a..b2aba41c06 100644
--- a/src/extensions/stores/cluster-store.ts
+++ b/src/extensions/stores/cluster-store.ts
@@ -42,6 +42,7 @@ export class ClusterStore extends Singleton {
if (!this.activeClusterId) {
return null;
}
+
return this.getById(this.activeClusterId);
}
diff --git a/src/main/__test__/cluster.test.ts b/src/main/__test__/cluster.test.ts
index c057501eac..b3f0442cc2 100644
--- a/src/main/__test__/cluster.test.ts
+++ b/src/main/__test__/cluster.test.ts
@@ -75,6 +75,7 @@ describe("create clusters", () => {
preferences: {},
})
};
+
mockFs(mockOpts);
jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValue(Promise.resolve(true));
c = new Cluster({
@@ -112,6 +113,7 @@ describe("create clusters", () => {
it("activating cluster should try to connect to cluster and do a refresh", async () => {
const port = await getFreePort();
+
jest.spyOn(ContextHandler.prototype, "ensureServer");
const mockListNSs = jest.fn();
@@ -122,17 +124,13 @@ describe("create clusters", () => {
};
}
};
+
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);
- })
.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);
@@ -148,6 +146,7 @@ describe("create clusters", () => {
mockedRequest.mockImplementationOnce(((uri: any) => {
expect(uri).toBe(`http://localhost:${port}/api-kube/version`);
+
return Promise.resolve({ gitVersion: "1.2.3" });
}) as any);
@@ -165,6 +164,7 @@ describe("create clusters", () => {
kubeConfigPath: "minikube-config.yml",
workspace: workspaceStore.currentWorkspaceId
});
+
await c.init(port);
await c.activate();
diff --git a/src/main/__test__/kube-auth-proxy.test.ts b/src/main/__test__/kube-auth-proxy.test.ts
index ac8322d75b..b161372555 100644
--- a/src/main/__test__/kube-auth-proxy.test.ts
+++ b/src/main/__test__/kube-auth-proxy.test.ts
@@ -49,6 +49,7 @@ describe("kube auth proxy tests", () => {
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();
@@ -69,24 +70,29 @@ describe("kube auth proxy tests", () => {
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();
mockedCP.stderr.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): Readable => {
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;
});
mockSpawn.mockImplementationOnce((command: string): ChildProcess => {
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, {});
});
diff --git a/src/main/__test__/kubeconfig-manager.test.ts b/src/main/__test__/kubeconfig-manager.test.ts
index 152a13055d..89c882b109 100644
--- a/src/main/__test__/kubeconfig-manager.test.ts
+++ b/src/main/__test__/kubeconfig-manager.test.ts
@@ -32,6 +32,7 @@ import { getFreePort } from "../port";
import fse from "fs-extra";
import { loadYaml } from "@kubernetes/client-node";
import { Console } from "console";
+import * as path from "path";
console = new Console(process.stdout, process.stderr); // fix mockFS
@@ -64,6 +65,7 @@ describe("kubeconfig manager tests", () => {
preferences: {},
})
};
+
mockFs(mockOpts);
});
@@ -83,9 +85,10 @@ describe("kubeconfig manager tests", () => {
const kubeConfManager = await KubeconfigManager.create(cluster, contextHandler, port);
expect(logger.error).not.toBeCalled();
- expect(kubeConfManager.getPath()).toBe("tmp/kubeconfig-foo");
+ expect(kubeConfManager.getPath()).toBe(`tmp${path.sep}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");
@@ -101,8 +104,8 @@ describe("kubeconfig manager tests", () => {
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);
diff --git a/src/main/app-updater.ts b/src/main/app-updater.ts
index c7b6659149..dd9ed97e69 100644
--- a/src/main/app-updater.ts
+++ b/src/main/app-updater.ts
@@ -14,6 +14,7 @@ export class AppUpdater {
public start() {
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 f73cc2ac81..9d52e1a70e 100644
--- a/src/main/cluster-detectors/base-cluster-detector.ts
+++ b/src/main/cluster-detectors/base-cluster-detector.ts
@@ -20,6 +20,7 @@ export class BaseClusterDetector {
protected async k8sRequest(path: string, options: RequestPromiseOptions = {}): Promise {
const apiUrl = this.cluster.kubeProxyUrl + path;
+
return request(apiUrl, {
json: true,
timeout: 30000,
diff --git a/src/main/cluster-detectors/cluster-id-detector.ts b/src/main/cluster-detectors/cluster-id-detector.ts
index 2605ca269f..2e0cc694ff 100644
--- a/src/main/cluster-detectors/cluster-id-detector.ts
+++ b/src/main/cluster-detectors/cluster-id-detector.ts
@@ -7,17 +7,20 @@ export class ClusterIdDetector extends BaseClusterDetector {
public async detect() {
let id: string;
+
try {
id = await this.getDefaultNamespaceId();
} catch(_) {
id = this.cluster.apiUrl;
}
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;
}
}
\ 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 d4abe01304..43c56153c9 100644
--- a/src/main/cluster-detectors/detector-registry.ts
+++ b/src/main/cluster-detectors/detector-registry.ts
@@ -17,12 +17,16 @@ export class DetectorRegistry {
async detectForCluster(cluster: Cluster): Promise {
const results: {[key: string]: ClusterDetectionResult } = {};
+
for (const detectorClass of this.registry) {
const detector = new detectorClass(cluster);
+
try {
const data = await detector.detect();
+
if (!data) continue;
const existingValue = results[detector.key];
+
if (existingValue && existingValue.accuracy > data.accuracy) continue; // previous value exists and is more accurate
results[detector.key] = data;
} catch (e) {
@@ -30,9 +34,11 @@ export class DetectorRegistry {
}
}
const metadata: ClusterMetadata = {};
+
for (const [key, result] of Object.entries(results)) {
metadata[key] = result.value;
}
+
return metadata;
}
}
diff --git a/src/main/cluster-detectors/distribution-detector.ts b/src/main/cluster-detectors/distribution-detector.ts
index 181425cb26..b496f2ce00 100644
--- a/src/main/cluster-detectors/distribution-detector.ts
+++ b/src/main/cluster-detectors/distribution-detector.ts
@@ -7,30 +7,59 @@ export class DistributionDetector extends BaseClusterDetector {
public async detect() {
this.version = await this.getKubernetesVersion();
- if (await this.isRancher()) {
- return { value: "rancher", accuracy: 80};
+
+ if (this.isRke()) {
+ return { value: "rke", accuracy: 80};
}
+
+ if (this.isK3s()) {
+ return { value: "k3s", accuracy: 80};
+ }
+
if (this.isGKE()) {
return { value: "gke", accuracy: 80};
}
+
if (this.isEKS()) {
return { value: "eks", accuracy: 80};
}
+
if (this.isIKS()) {
return { value: "iks", accuracy: 80};
}
+
if (this.isAKS()) {
return { value: "aks", accuracy: 80};
}
+
if (this.isDigitalOcean()) {
return { value: "digitalocean", accuracy: 90};
}
+
+ if (this.isMirantis()) {
+ return { value: "mirantis", accuracy: 90};
+ }
+
if (this.isMinikube()) {
return { value: "minikube", accuracy: 80};
}
+
+ if (this.isMicrok8s()) {
+ return { value: "microk8s", accuracy: 80};
+ }
+
+ if (this.isKind()) {
+ return { value: "kind", accuracy: 70};
+ }
+
+ if (this.isDockerDesktop()) {
+ return { value: "docker-desktop", accuracy: 80};
+ }
+
if (this.isCustom()) {
return { value: "custom", accuracy: 10};
}
+
return { value: "unknown", accuracy: 10};
}
@@ -38,6 +67,7 @@ export class DistributionDetector extends BaseClusterDetector {
if (this.cluster.version) return this.cluster.version;
const response = await this.k8sRequest("/version");
+
return response.gitVersion;
}
@@ -57,6 +87,10 @@ export class DistributionDetector extends BaseClusterDetector {
return this.cluster.apiUrl.endsWith("azmk8s.io");
}
+ protected isMirantis() {
+ return this.version.includes("-mirantis-") || this.version.includes("-docker-");
+ }
+
protected isDigitalOcean() {
return this.cluster.apiUrl.endsWith("k8s.ondigitalocean.com");
}
@@ -65,16 +99,27 @@ export class DistributionDetector extends BaseClusterDetector {
return this.cluster.contextName.startsWith("minikube");
}
+ protected isMicrok8s() {
+ return this.cluster.contextName.startsWith("microk8s");
+ }
+
+ protected isKind() {
+ return this.cluster.contextName.startsWith("kubernetes-admin@kind-");
+ }
+
+ protected isDockerDesktop() {
+ return this.cluster.contextName === "docker-desktop";
+ }
+
protected isCustom() {
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;
- } catch (e) {
- return false;
- }
+ protected isRke() {
+ return this.version.includes("-rancher");
}
-}
\ No newline at end of file
+
+ protected isK3s() {
+ return this.version.includes("+k3s");
+ }
+}
diff --git a/src/main/cluster-detectors/last-seen-detector.ts b/src/main/cluster-detectors/last-seen-detector.ts
index d56483625a..e648d5f2f9 100644
--- a/src/main/cluster-detectors/last-seen-detector.ts
+++ b/src/main/cluster-detectors/last-seen-detector.ts
@@ -8,6 +8,7 @@ export class LastSeenDetector extends BaseClusterDetector {
if (!this.cluster.accessible) return null;
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 ba5fc93583..0ece5dd080 100644
--- a/src/main/cluster-detectors/nodes-count-detector.ts
+++ b/src/main/cluster-detectors/nodes-count-detector.ts
@@ -7,11 +7,13 @@ export class NodesCountDetector extends BaseClusterDetector {
public async detect() {
if (!this.cluster.accessible) return null;
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;
}
}
\ 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 e59e6291b9..8080ef57a1 100644
--- a/src/main/cluster-detectors/version-detector.ts
+++ b/src/main/cluster-detectors/version-detector.ts
@@ -7,11 +7,13 @@ export class VersionDetector extends BaseClusterDetector {
public async detect() {
const version = await this.getKubernetesVersion();
+
return { value: version, accuracy: 100};
}
public async getKubernetesVersion() {
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 9b2e88ef89..5717c7278d 100644
--- a/src/main/cluster-manager.ts
+++ b/src/main/cluster-manager.ts
@@ -24,8 +24,10 @@ export class ClusterManager extends Singleton {
// auto-stop removed clusters
autorun(() => {
const removedClusters = Array.from(clusterStore.removedClusters.values());
+
if (removedClusters.length > 0) {
const meta = removedClusters.map(cluster => cluster.getMeta());
+
logger.info(`[CLUSTER-MANAGER]: removing clusters`, meta);
removedClusters.forEach(cluster => cluster.disconnect());
clusterStore.removedClusters.clear();
@@ -70,7 +72,9 @@ export class ClusterManager extends Singleton {
// 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);
+
if (cluster) {
// we need to swap path prefix so that request is proxied to kube api
req.url = req.url.replace(`/${clusterId}`, apiKubePrefix);
@@ -79,6 +83,7 @@ export class ClusterManager extends Singleton {
cluster = clusterStore.getById(req.headers["x-cluster-id"].toString());
} else {
const clusterId = getClusterIdFromHost(req.headers.host);
+
cluster = clusterStore.getById(clusterId);
}
diff --git a/src/main/cluster.ts b/src/main/cluster.ts
index cfd0f77bd9..79e28fa9c4 100644
--- a/src/main/cluster.ts
+++ b/src/main/cluster.ts
@@ -37,6 +37,7 @@ export type ClusterRefreshOptions = {
export interface ClusterState {
initialized: boolean;
+ enabled: boolean;
apiUrl: string;
online: boolean;
disconnected: boolean;
@@ -224,6 +225,7 @@ export class Cluster implements ClusterModel, ClusterState {
*/
@computed get prometheusPreferences(): ClusterPrometheusPreferences {
const { prometheus, prometheusProvider } = this.preferences;
+
return toJS({ prometheus, prometheusProvider }, {
recurseEverything: true,
});
@@ -239,6 +241,7 @@ export class Cluster implements ClusterModel, ClusterState {
constructor(model: ClusterModel) {
this.updateModel(model);
const kubeconfig = this.getKubeconfig();
+
if (kubeconfig.getContextObject(this.contextName)) {
this.apiUrl = kubeconfig.getCluster(kubeconfig.getContextObject(this.contextName).cluster).server;
}
@@ -324,13 +327,16 @@ export class Cluster implements ClusterModel, ClusterState {
}
logger.info(`[CLUSTER]: activate`, this.getMeta());
await this.whenInitialized;
+
if (!this.eventDisposers.length) {
this.bindEvents();
}
+
if (this.disconnected || !this.accessible) {
await this.reconnect();
}
await this.refreshConnectionStatus();
+
if (this.accessible) {
await this.refreshAllowedResources();
this.isAdmin = await this.isClusterAdmin();
@@ -338,6 +344,7 @@ export class Cluster implements ClusterModel, ClusterState {
this.ensureKubectl();
}
this.activated = true;
+
return this.pushState();
}
@@ -346,6 +353,7 @@ export class Cluster implements ClusterModel, ClusterState {
*/
protected async ensureKubectl() {
this.kubeCtl = new Kubectl(this.version);
+
return this.kubeCtl.ensureKubectl(); // download kubectl in background, so it's not blocking dashboard
}
@@ -382,9 +390,11 @@ export class Cluster implements ClusterModel, ClusterState {
logger.info(`[CLUSTER]: refresh`, this.getMeta());
await this.whenInitialized;
await this.refreshConnectionStatus();
+
if (this.accessible) {
this.isAdmin = await this.isClusterAdmin();
await this.refreshAllowedResources();
+
if (opts.refreshMetadata) {
this.refreshMetadata();
}
@@ -400,6 +410,7 @@ export class Cluster implements ClusterModel, ClusterState {
logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta());
const metadata = await detectorRegistry.detectForCluster(this);
const existingMetadata = this.metadata;
+
this.metadata = Object.assign(existingMetadata, metadata);
}
@@ -408,6 +419,7 @@ export class Cluster implements ClusterModel, ClusterState {
*/
@action async refreshConnectionStatus() {
const connectionStatus = await this.getConnectionStatus();
+
this.online = connectionStatus > ClusterStatus.Offline;
this.accessible = connectionStatus == ClusterStatus.AccessGranted;
}
@@ -456,6 +468,7 @@ export class Cluster implements ClusterModel, ClusterState {
getMetrics(prometheusPath: string, queryParams: IMetricsReqParams & { query: string }) {
const prometheusPrefix = this.preferences.prometheus?.prefix || "";
const metricsPath = `/api/v1/namespaces/${prometheusPath}/proxy${prometheusPrefix}/api/v1/query_range`;
+
return this.k8sRequest(metricsPath, {
timeout: 0,
resolveWithFullResponse: false,
@@ -468,28 +481,36 @@ export class Cluster implements ClusterModel, ClusterState {
try {
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}`);
+
if (error.statusCode) {
if (error.statusCode >= 400 && error.statusCode < 500) {
this.failureReason = "Invalid credentials";
+
return ClusterStatus.AccessDenied;
} else {
this.failureReason = error.error || error.message;
+
return ClusterStatus.Offline;
}
} else if (error.failed === true) {
if (error.timedOut === true) {
this.failureReason = "Connection timed out";
+
return ClusterStatus.Offline;
} else {
this.failureReason = "Failed to fetch credentials";
+
return ClusterStatus.AccessDenied;
}
}
this.failureReason = error.message;
+
return ClusterStatus.Offline;
}
}
@@ -500,15 +521,18 @@ export class Cluster implements ClusterModel, ClusterState {
*/
async canI(resourceAttributes: V1ResourceAttributes): Promise {
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;
} catch (error) {
logger.error(`failed to request selfSubjectAccessReview: ${error}`);
+
return false;
}
}
@@ -535,6 +559,7 @@ export class Cluster implements ClusterModel, ClusterState {
ownerRef: this.ownerRef,
accessibleNamespaces: this.accessibleNamespaces,
};
+
return toJS(model, {
recurseEverything: true
});
@@ -546,6 +571,7 @@ export class Cluster implements ClusterModel, ClusterState {
getState(): ClusterState {
const state: ClusterState = {
initialized: this.initialized,
+ enabled: this.enabled,
apiUrl: this.apiUrl,
online: this.online,
ready: this.ready,
@@ -556,6 +582,7 @@ export class Cluster implements ClusterModel, ClusterState {
allowedNamespaces: this.allowedNamespaces,
allowedResources: this.allowedResources,
};
+
return toJS(state, {
recurseEverything: true
});
@@ -597,21 +624,16 @@ export class Cluster implements ClusterModel, ClusterState {
}
const api = this.getProxyKubeconfig().makeApiClient(CoreV1Api);
+
try {
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);
+
+ return namespaceList.body.items.map(ns => ns.metadata.name);
} catch (error) {
const ctx = this.getProxyKubeconfig().getContextObject(this.contextName);
+
if (ctx.namespace) return [ctx.namespace];
+
return [];
}
}
@@ -629,6 +651,7 @@ export class Cluster implements ClusterModel, ClusterState {
namespace: this.allowedNamespaces[0]
}))
);
+
return apiResources
.filter((resource, i) => resourceAccessStatuses[i])
.map(apiResource => apiResource.resource);
diff --git a/src/main/context-handler.ts b/src/main/context-handler.ts
index 2c0c0b4e8d..d67c495a84 100644
--- a/src/main/context-handler.ts
+++ b/src/main/context-handler.ts
@@ -25,28 +25,34 @@ export class ContextHandler {
public setupPrometheus(preferences: ClusterPrometheusPreferences = {}) {
this.prometheusProvider = preferences.prometheusProvider?.type;
this.prometheusPath = null;
+
if (preferences.prometheus) {
const { namespace, service, port } = preferences.prometheus;
+
this.prometheusPath = `${namespace}/services/${service}:${port}`;
}
}
protected async resolvePrometheusPath(): Promise {
const prometheusService = await this.getPrometheusService();
+
if (!prometheusService) return null;
const { service, namespace, port } = prometheusService;
+
return `${namespace}/services/${service}:${port}`;
}
async getPrometheusProvider() {
if (!this.prometheusProvider) {
const service = await this.getPrometheusService();
+
if (!service) {
return null;
}
logger.info(`using ${service.id} as prometheus provider`);
this.prometheusProvider = service.id;
}
+
return prometheusProviders.find(p => p.id === this.prometheusProvider);
}
@@ -54,9 +60,11 @@ export class ContextHandler {
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);
+
return resolvedPrometheusServices.filter(n => n)[0];
}
@@ -64,12 +72,14 @@ export class ContextHandler {
if (!this.prometheusPath) {
this.prometheusPath = await this.resolvePrometheusPath();
}
+
return this.prometheusPath;
}
async resolveAuthProxyUrl() {
const proxyPort = await this.ensurePort();
const path = this.clusterUrl.path !== "/" ? this.clusterUrl.path : "";
+
return `http://127.0.0.1:${proxyPort}${path}`;
}
@@ -79,14 +89,17 @@ export class ContextHandler {
}
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;
}
+
return apiTarget;
}
protected async newApiTarget(timeout: number): Promise {
const proxyUrl = await this.resolveAuthProxyUrl();
+
return {
target: proxyUrl,
changeOrigin: true,
@@ -101,6 +114,7 @@ export class ContextHandler {
if (!this.proxyPort) {
this.proxyPort = await getFreePort();
}
+
return this.proxyPort;
}
@@ -108,6 +122,7 @@ export class ContextHandler {
if (!this.kubeAuthProxy) {
await this.ensurePort();
const proxyEnv = Object.assign({}, process.env);
+
if (this.cluster.preferences.httpsProxy) {
proxyEnv.HTTPS_PROXY = this.cluster.preferences.httpsProxy;
}
diff --git a/src/main/exit-app.ts b/src/main/exit-app.ts
index a998a2811f..eb035eff96 100644
--- a/src/main/exit-app.ts
+++ b/src/main/exit-app.ts
@@ -8,11 +8,12 @@ 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();
+ 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
index fb3a4060be..c4ee622e1d 100644
--- a/src/main/extension-filesystem.ts
+++ b/src/main/extension-filesystem.ts
@@ -32,11 +32,14 @@ export class FilesystemProvisionerStore extends BaseStore {
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;
}
diff --git a/src/main/helm/helm-chart-manager.ts b/src/main/helm/helm-chart-manager.ts
index a5920afb71..cf4a8e5ace 100644
--- a/src/main/helm/helm-chart-manager.ts
+++ b/src/main/helm/helm-chart-manager.ts
@@ -20,32 +20,39 @@ export class HelmChartManager {
public async chart(name: string) {
const charts = await this.charts();
+
return charts[name];
}
public async charts(): Promise {
try {
const cachedYaml = await this.cachedYaml();
+
return cachedYaml["entries"];
} catch(error) {
logger.error(error);
+
return [];
}
}
public async getReadme(name: string, version = "") {
const helm = await helmCli.binaryPath();
+
if(version && version != "") {
const { stdout } = await promiseExec(`"${helm}" show readme ${this.repo.name}/${name} --version ${version}`).catch((error) => { throw(error.stderr);});
+
return stdout;
} else {
const { stdout } = 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();
+
if(version && version != "") {
const { stdout } = await promiseExec(`"${helm}" show values ${this.repo.name}/${name} --version ${version}`).catch((error) => { throw(error.stderr);});
@@ -61,6 +68,7 @@ export class HelmChartManager {
if (!(this.repo.name in this.cache)) {
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;
@@ -69,6 +77,7 @@ export class HelmChartManager {
}
this.cache[this.repo.name] = Buffer.from(JSON.stringify(data));
}
+
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 90ad78ba1d..ca6f755896 100644
--- a/src/main/helm/helm-cli.ts
+++ b/src/main/helm/helm-cli.ts
@@ -12,6 +12,7 @@ export class HelmCli extends LensBinary {
originalBinaryName: "helm",
newBinaryName: "helm3"
};
+
super(opts);
}
diff --git a/src/main/helm/helm-release-manager.ts b/src/main/helm/helm-release-manager.ts
index a669ff2a6c..220d665ae0 100644
--- a/src/main/helm/helm-release-manager.ts
+++ b/src/main/helm/helm-release-manager.ts
@@ -12,14 +12,15 @@ export class HelmReleaseManager {
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);
+
if (output.length == 0) {
return output;
}
output.forEach((release: any, index: number) => {
output[index] = toCamelCase(release);
});
+
return output;
}
@@ -27,15 +28,19 @@ export class HelmReleaseManager {
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));
+
try {
let generateName = "";
+
if (!name) {
generateName = "--generate-name";
name = "";
}
const { stdout } = 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: {
@@ -51,10 +56,12 @@ export class HelmReleaseManager {
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));
try {
const { stdout } = 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)
@@ -68,7 +75,9 @@ export class HelmReleaseManager {
const helm = await helmCli.binaryPath();
const { stdout } = 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;
}
@@ -82,18 +91,21 @@ export class HelmReleaseManager {
public async getValues(name: string, namespace: string, pathToKubeconfig: string) {
const helm = await helmCli.binaryPath();
const { stdout, } = 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 } = 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 } = await promiseExec(`"${helm}" rollback ${name} ${revision} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);});
+
return stdout;
}
@@ -104,6 +116,7 @@ export class HelmReleaseManager {
const { stdout } = await promiseExec(`"${helm}" get manifest ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} | "${kubectl}" get -n ${namespace} --kubeconfig ${pathToKubeconfig} -f - -o=json`).catch(() => {
return { stdout: JSON.stringify({items: []})};
});
+
return stdout;
}
}
diff --git a/src/main/helm/helm-repo-manager.ts b/src/main/helm/helm-repo-manager.ts
index fea000fec2..ae8595dae9 100644
--- a/src/main/helm/helm-repo-manager.ts
+++ b/src/main/helm/helm-repo-manager.ts
@@ -42,12 +42,14 @@ export class HelmRepoManager extends Singleton {
resolveWithFullResponse: true,
timeout: 10000,
});
+
return orderBy(res.body, repo => repo.name);
}
async init() {
helmCli.setLogger(logger);
await helmCli.ensureBinary();
+
if (!this.initialized) {
this.helmEnv = await this.parseHelmEnv();
await this.update();
@@ -62,12 +64,15 @@ export class HelmRepoManager extends Singleton {
});
const lines = stdout.split(/\r?\n/); // split by new line feed
const env: HelmEnv = {};
+
lines.forEach((line: string) => {
const [key, value] = line.split("=");
+
if (key && value) {
env[key] = value.replace(/"/g, ""); // strip quotas
}
});
+
return env;
}
@@ -75,6 +80,7 @@ export class HelmRepoManager extends Singleton {
if (!this.initialized) {
await this.init();
}
+
try {
const repoConfigFile = this.helmEnv.HELM_REPOSITORY_CONFIG;
const { repositories }: HelmRepoConfig = await readFile(repoConfigFile, "utf8")
@@ -82,22 +88,27 @@ export class HelmRepoManager extends Singleton {
.catch(() => ({
repositories: []
}));
+
if (!repositories.length) {
await this.addRepo({ name: "bitnami", url: "https://charts.bitnami.com/bitnami" });
+
return await this.repositories();
}
+
return repositories.map(repo => ({
...repo,
cacheFilePath: `${this.helmEnv.HELM_REPOSITORY_CACHE}/${repo.name}-index.yaml`
}));
} catch (error) {
logger.error(`[HELM]: repositories listing error "${error}"`);
+
return [];
}
}
public async repository(name: string) {
const repositories = await this.repositories();
+
return repositories.find(repo => repo.name == name);
}
@@ -106,6 +117,7 @@ export class HelmRepoManager extends Singleton {
const { stdout } = await promiseExec(`"${helm}" repo update`).catch((error) => {
return { stdout: error.stdout };
});
+
return stdout;
}
@@ -115,6 +127,7 @@ export class HelmRepoManager extends Singleton {
const { stdout } = await promiseExec(`"${helm}" repo add ${name} ${url}`).catch((error) => {
throw(error.stderr);
});
+
return stdout;
}
@@ -124,6 +137,7 @@ export class HelmRepoManager extends Singleton {
const { stdout } = await promiseExec(`"${helm}" repo remove ${name}`).catch((error) => {
throw(error.stderr);
});
+
return stdout;
}
}
diff --git a/src/main/helm/helm-service.ts b/src/main/helm/helm-service.ts
index 0ccec256ed..1918268075 100644
--- a/src/main/helm/helm-service.ts
+++ b/src/main/helm/helm-service.ts
@@ -11,18 +11,23 @@ class HelmService {
public async listCharts() {
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);
+
for (const key in entries) {
entries[key] = entries[key][0];
}
charts[repo.name] = entries;
}
+
return charts;
}
@@ -34,50 +39,60 @@ class HelmService {
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);
}
public async listReleases(cluster: Cluster, namespace: string = null) {
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);
}
public async getReleaseValues(cluster: Cluster, releaseName: string, namespace: string) {
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());
}
public async deleteRelease(cluster: Cluster, releaseName: string, namespace: string) {
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);
}
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 };
}
@@ -87,9 +102,11 @@ class HelmService {
if (Array.isArray(entry)) {
return entry[0]["deprecated"] != true;
}
+
return entry["deprecated"] != true;
});
}
+
return entries;
}
diff --git a/src/main/index.ts b/src/main/index.ts
index 315526862c..cad2235743 100644
--- a/src/main/index.ts
+++ b/src/main/index.ts
@@ -34,11 +34,13 @@ let clusterManager: ClusterManager;
let windowManager: WindowManager;
app.setName(appName);
+
if (!process.env.CICD) {
app.setPath("userData", workingDir);
}
mangleProxyEnv();
+
if (app.commandLine.getSwitchValue("proxy-server") !== "") {
process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server");
}
@@ -48,6 +50,7 @@ app.on("ready", async () => {
await shellSync();
const updater = new AppUpdater();
+
updater.start();
registerFileProtocol("static", __static);
@@ -110,6 +113,7 @@ app.on("ready", async () => {
app.on("activate", (event, hasVisibleWindows) => {
logger.info("APP:ACTIVATE", { hasVisibleWindows });
+
if (!hasVisibleWindows) {
windowManager.initMainWindow();
}
@@ -121,6 +125,7 @@ app.on("will-quit", (event) => {
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
});
diff --git a/src/main/kube-auth-proxy.ts b/src/main/kube-auth-proxy.ts
index 791b242104..589fe8fa16 100644
--- a/src/main/kube-auth-proxy.ts
+++ b/src/main/kube-auth-proxy.ts
@@ -45,6 +45,7 @@ export class KubeAuthProxy {
"--accept-hosts", this.acceptHosts,
"--reject-paths", "^[^/]"
];
+
if (process.env.DEBUG_PROXY === "true") {
args.push("-v", "9");
}
@@ -62,6 +63,7 @@ export class KubeAuthProxy {
this.proxyProcess.stdout.on("data", (data) => {
let logItem = data.toString();
+
if (logItem.startsWith("Starting to serve on")) {
logItem = "Authentication proxy started\n";
}
@@ -80,19 +82,23 @@ export class KubeAuthProxy {
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;
} catch (_) {
errorMsg = jsonError.trim();
}
}
+
return errorMsg;
}
protected async sendIpcLogMessage(res: KubeAuthProxyLog) {
const channel = `kube-auth:${this.cluster.id}`;
+
logger.info(`[KUBE-AUTH]: out-channel "${channel}"`, { ...res, meta: this.cluster.getMeta() });
broadcastMessage(channel, res);
}
diff --git a/src/main/kubeconfig-manager.ts b/src/main/kubeconfig-manager.ts
index b0251264e7..bd1b32c1f5 100644
--- a/src/main/kubeconfig-manager.ts
+++ b/src/main/kubeconfig-manager.ts
@@ -15,7 +15,9 @@ export class KubeconfigManager {
static async create(cluster: Cluster, contextHandler: ContextHandler, port: number) {
const kcm = new KubeconfigManager(cluster, contextHandler, port);
+
await kcm.init();
+
return kcm;
}
@@ -66,13 +68,14 @@ export class KubeconfigManager {
}
]
};
-
// write
const configYaml = dumpConfigYaml(proxyConfig);
+
fs.ensureDir(path.dirname(tempFile));
fs.writeFileSync(tempFile, configYaml, { mode: 0o600 });
this.tempFile = tempFile;
logger.debug(`Created temp kubeconfig "${contextName}" at "${tempFile}": \n${configYaml}`);
+
return tempFile;
}
diff --git a/src/main/kubectl.ts b/src/main/kubectl.ts
index ee059bde5b..0a96ee354b 100644
--- a/src/main/kubectl.ts
+++ b/src/main/kubectl.ts
@@ -27,12 +27,10 @@ const kubectlMap: Map = new Map([
["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";
@@ -41,6 +39,7 @@ export function bundledKubectlPath(): string {
if (isDevelopment || isTestEnv) {
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");
@@ -71,12 +70,14 @@ export class Kubectl {
// Returns the single bundled Kubectl instance
public static bundled() {
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];
+
/* 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)) {
@@ -134,18 +135,22 @@ export class Kubectl {
// return binary name if bundled path is not functional
if (!await this.checkBinary(this.getBundledPath(), false)) {
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();
}
+
return this.path;
} catch (err) {
logger.error("Failed to ensure kubectl, fallback to the bundled version");
logger.error(err);
+
return this.getBundledPath();
}
}
@@ -154,28 +159,35 @@ export class Kubectl {
try {
await this.ensureKubectl();
await this.writeInitScripts();
+
return this.dirname;
} catch (err) {
logger.error(err);
+
return "";
}
}
public async checkBinary(path: string, checkVersion = true) {
const exists = await pathExists(path);
+
if (exists) {
try {
const { stdout } = await promiseExec(`"${path}" version --client=true -o json`);
const output = JSON.parse(stdout);
+
if (!checkVersion) {
return true;
}
let version: string = output.clientVersion.gitVersion;
+
if (version[0] === "v") {
version = version.slice(1);
}
+
if (version === this.kubectlVersion) {
logger.debug(`Local kubectl is version ${this.kubectlVersion}`);
+
return true;
}
logger.error(`Local kubectl is version ${version}, expected ${this.kubectlVersion}, unlinking`);
@@ -184,6 +196,7 @@ export class Kubectl {
}
await fs.promises.unlink(this.path);
}
+
return false;
}
@@ -191,13 +204,16 @@ export class Kubectl {
if (this.kubectlVersion === Kubectl.bundledKubectlVersion) {
try {
const exist = await pathExists(this.path);
+
if (!exist) {
await fs.promises.copyFile(this.getBundledPath(), this.path);
await fs.promises.chmod(this.path, 0o755);
}
+
return true;
} catch (err) {
logger.error(`Could not copy the bundled kubectl to app-data: ${err}`);
+
return false;
}
} else {
@@ -209,35 +225,44 @@ export class Kubectl {
if (userStore.preferences?.downloadKubectlBinaries === false) {
return true;
}
+
if (Kubectl.invalidBundle) {
logger.error(`Detected invalid bundle binary, returning ...`);
+
return false;
}
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);
+
if (!isValid && !bundled) {
await this.downloadKubectl().catch((error) => {
logger.error(error);
logger.debug(`Releasing lock for ${this.kubectlVersion}`);
release();
+
return 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 true;
}).catch((e) => {
logger.error(`Failed to get a lock for ${this.kubectlVersion}`);
logger.error(e);
+
return false;
});
}
@@ -246,12 +271,14 @@ export class Kubectl {
await ensureDir(path.dirname(this.path), 0o755);
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);
+
stream.on("complete", () => {
logger.debug("kubectl binary download finished");
file.end();
@@ -279,8 +306,8 @@ export class Kubectl {
const helmPath = helmCli.getBinaryDir();
const fsPromises = fs.promises;
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";
@@ -302,7 +329,6 @@ export class Kubectl {
await fsPromises.writeFile(bashScriptPath, bashScript.toString(), { mode: 0o644 });
const zshScriptPath = path.join(this.dirname, ".zlogin");
-
let zshScript = `${initScriptVersionString}`;
zshScript += "tempkubeconfig=\"$KUBECONFIG\"\n";
@@ -335,9 +361,11 @@ export class Kubectl {
protected getDownloadMirror() {
const mirror = packageMirrors.get(userStore.preferences?.downloadMirror);
+
if (mirror) {
return mirror;
}
+
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 9d5a5d1e1d..50d0b11374 100644
--- a/src/main/kubectl_spec.ts
+++ b/src/main/kubectl_spec.ts
@@ -8,12 +8,14 @@ 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);
});
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);
});
});
@@ -24,19 +26,23 @@ describe("getPath()", () => {
const kubectl = new Kubectl(bundledKubectlVersion);
const kubectlPath = await kubectl.getPath();
let binaryName = "kubectl";
+
if (isWindows) {
binaryName += ".exe";
}
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";
+
if (isWindows) {
binaryName += ".exe";
}
diff --git a/src/main/lens-binary.ts b/src/main/lens-binary.ts
index bdd4c7ee62..3cf5a5fce7 100644
--- a/src/main/lens-binary.ts
+++ b/src/main/lens-binary.ts
@@ -31,6 +31,7 @@ export class LensBinary {
constructor(opts: LensBinaryOpts) {
const baseDir = opts.baseDir;
+
this.originalBinaryName = opts.originalBinaryName;
this.binaryName = opts.newBinaryName || opts.originalBinaryName;
this.binaryVersion = opts.version;
@@ -50,11 +51,13 @@ export class LensBinary {
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`;
}
const tarName = this.getTarName();
+
if (tarName) {
this.tarPath = path.join(this.dirname, tarName);
}
@@ -70,6 +73,7 @@ export class LensBinary {
public async binaryPath() {
await this.ensureBinary();
+
return this.getBinaryPath();
}
@@ -96,20 +100,24 @@ export class LensBinary {
public async binDir() {
try {
await this.ensureBinary();
+
return this.dirname;
} catch (err) {
this.logger.error(err);
+
return "";
}
}
protected async checkBinary() {
const exists = await pathExists(this.getBinaryPath());
+
return exists;
}
public async ensureBinary() {
const isValid = await this.checkBinary();
+
if (!isValid) {
await this.downloadBinary().catch((error) => {
this.logger.error(error);
@@ -148,6 +156,7 @@ export class LensBinary {
protected async downloadBinary() {
const binaryPath = this.tarPath || this.getBinaryPath();
+
await ensureDir(this.getBinaryDir(), 0o755);
const file = fs.createWriteStream(binaryPath);
@@ -159,7 +168,6 @@ export class LensBinary {
gzip: true,
...this.requestOpts
};
-
const stream = request(requestOpts);
stream.on("complete", () => {
@@ -174,6 +182,7 @@ export class LensBinary {
});
throw(error);
});
+
return new Promise((resolve, reject) => {
file.on("close", () => {
this.logger.debug(`${this.originalBinaryName} binary download closed`);
diff --git a/src/main/lens-proxy.ts b/src/main/lens-proxy.ts
index 3f9433026c..5770429a7e 100644
--- a/src/main/lens-proxy.ts
+++ b/src/main/lens-proxy.ts
@@ -30,6 +30,7 @@ export class LensProxy {
listen(port = this.port): this {
this.proxyServer = this.buildCustomProxy().listen(port);
logger.info(`LensProxy server has started at ${this.origin}`);
+
return this;
}
@@ -49,6 +50,7 @@ export class LensProxy {
}, (req: http.IncomingMessage, res: http.ServerResponse) => {
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);
@@ -59,22 +61,27 @@ export class LensProxy {
spdyProxy.on("error", (err) => {
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);
+
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();
+
proxySocket.connect(connectOpts, () => {
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];
+
if (key !== "Host" && key !== "Authorization") {
proxySocket.write(`${req.rawHeaders[i]}: ${req.rawHeaders[i+1]}\r\n`);
}
@@ -112,16 +119,20 @@ export class LensProxy {
protected createProxy(): httpProxy {
const proxy = httpProxy.createProxyServer();
+
proxy.on("error", (error, req, res, target) => {
if (this.closed) {
return;
}
+
if (target) {
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;
+
if (retryCount < 20) {
logger.debug(`Retrying proxy request to url: ${reqId}`);
setTimeout(() => {
@@ -131,6 +142,7 @@ export class LensProxy {
}
}
}
+
try {
res.writeHead(500).end("Oops, something went wrong.");
} catch (e) {
@@ -143,9 +155,11 @@ export class LensProxy {
protected createWsListener(): WebSocket.Server {
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();
+
openShell(socket, cluster, nodeParam);
}));
}
@@ -155,6 +169,7 @@ export class LensProxy {
delete req.headers.authorization;
req.url = req.url.replace(apiKubePrefix, "");
const isWatchRequest = req.url.includes("watch=");
+
return await contextHandler.getApiTarget(isWatchRequest);
}
}
@@ -165,11 +180,14 @@ export class LensProxy {
protected async handleRequest(proxy: httpProxy, req: http.IncomingMessage, res: http.ServerResponse) {
const cluster = this.clusterManager.getClusterForRequest(req);
+
if (cluster) {
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);
+
return proxy.web(req, res, proxyTarget);
}
}
@@ -178,6 +196,7 @@ export class LensProxy {
protected async handleWsUpgrade(req: http.IncomingMessage, socket: net.Socket, head: Buffer) {
const wsServer = this.createWsListener();
+
wsServer.handleUpgrade(req, socket, head, (con) => {
wsServer.emit("connection", con, req);
});
diff --git a/src/main/logger.ts b/src/main/logger.ts
index 81d61e8002..0ddc7bb1f7 100644
--- a/src/main/logger.ts
+++ b/src/main/logger.ts
@@ -3,12 +3,10 @@ import winston from "winston";
import { isDebugging } from "../common/vars";
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,
level: logLevel,
@@ -18,7 +16,6 @@ const fileOptions: winston.transports.FileTransportOptions = {
maxFiles: 16,
tailable: true,
};
-
const logger = winston.createLogger({
format: winston.format.combine(
winston.format.colorize(),
diff --git a/src/main/menu.ts b/src/main/menu.ts
index feccbafa21..2cddbb1b01 100644
--- a/src/main/menu.ts
+++ b/src/main/menu.ts
@@ -27,6 +27,7 @@ export function showAbout(browserWindow: BrowserWindow) {
`Node: ${process.versions.node}`,
`Copyright 2020 Mirantis, Inc.`,
];
+
dialog.showMessageBoxSync(browserWindow, {
title: `${isWindows ? " ".repeat(2) : ""}${appName}`,
type: "info",
@@ -39,6 +40,7 @@ export function showAbout(browserWindow: BrowserWindow) {
export function buildMenu(windowManager: WindowManager) {
function ignoreOnMac(menuItems: MenuItemConstructorOptions[]) {
if (isMac) return [];
+
return menuItems;
}
@@ -48,6 +50,7 @@ export function buildMenu(windowManager: WindowManager) {
item.enabled = false;
});
}
+
return menuItems;
}
@@ -96,7 +99,6 @@ export function buildMenu(windowManager: WindowManager) {
}
]
};
-
const fileMenu: MenuItemConstructorOptions = {
label: "File",
submenu: [
@@ -154,7 +156,6 @@ export function buildMenu(windowManager: WindowManager) {
])
]
};
-
const editMenu: MenuItemConstructorOptions = {
label: "Edit",
submenu: [
@@ -169,7 +170,6 @@ export function buildMenu(windowManager: WindowManager) {
{ role: "selectAll" },
]
};
-
const viewMenu: MenuItemConstructorOptions = {
label: "View",
submenu: [
@@ -203,7 +203,6 @@ export function buildMenu(windowManager: WindowManager) {
{ role: "togglefullscreen" }
]
};
-
const helpMenu: MenuItemConstructorOptions = {
role: "help",
submenu: [
@@ -235,7 +234,6 @@ export function buildMenu(windowManager: WindowManager) {
])
]
};
-
// Prepare menu items order
const appMenu: Record = {
mac: macAppMenu,
@@ -249,6 +247,7 @@ export function buildMenu(windowManager: WindowManager) {
menuRegistry.getItems().forEach(({ parentId, ...menuItem }) => {
try {
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 });
@@ -260,6 +259,7 @@ export function buildMenu(windowManager: WindowManager) {
}
const menu = Menu.buildFromTemplate(Object.values(appMenu));
+
Menu.setApplicationMenu(menu);
if (isTestEnv) {
@@ -273,6 +273,7 @@ export function buildMenu(windowManager: WindowManager) {
for (const name of names) {
parentLabels.push(name);
menuItem = menu?.items?.find(item => item.label === name);
+
if (!menuItem) {
break;
}
@@ -280,14 +281,18 @@ export function buildMenu(windowManager: WindowManager) {
}
const menuPath: string = parentLabels.join(" -> ");
+
if (!menuItem) {
logger.info(`[MENU:test-menu-item-click] Cannot find menu item ${menuPath}`);
+
return;
}
const { enabled, visible, click } = menuItem;
+
if (enabled === false || visible === false || typeof click !== "function") {
logger.info(`[MENU:test-menu-item-click] Menu item ${menuPath} not clickable`);
+
return;
}
diff --git a/src/main/node-shell-session.ts b/src/main/node-shell-session.ts
index ea85b8ac2a..b67e776725 100644
--- a/src/main/node-shell-session.ts
+++ b/src/main/node-shell-session.ts
@@ -23,6 +23,7 @@ export class NodeShellSession extends ShellSession {
public async open() {
const shell = await this.kubectl.getPath();
let args = [];
+
if (this.createNodeShellPod(this.podId, this.nodeName)) {
await this.waitForRunningPod(this.podId).catch(() => {
this.exit(1001);
@@ -31,6 +32,7 @@ export class NodeShellSession extends ShellSession {
args = ["exec", "-i", "-t", "-n", "kube-system", this.podId, "--", "sh", "-c", "((clear && bash) || (clear && ash) || (clear && sh))"];
const shellEnv = await this.getCachedShellEnv();
+
this.shellProcess = pty.spawn(shell, args, {
cols: 80,
cwd: this.cwd() || shellEnv["HOME"],
@@ -85,10 +87,13 @@ export class NodeShellSession extends ShellSession {
}
}
} as k8s.V1Pod;
+
await k8sApi.createNamespacedPod("kube-system", pod).catch((error) => {
logger.error(error);
+
return false;
});
+
return true;
}
@@ -98,6 +103,7 @@ export class NodeShellSession extends ShellSession {
}
this.kc = new k8s.KubeConfig();
this.kc.loadFromFile(this.kubeconfigPath);
+
return this.kc;
}
@@ -105,7 +111,6 @@ export class NodeShellSession extends ShellSession {
return new Promise(async (resolve, reject) => {
const kc = this.getKubeConfig();
const watch = new k8s.Watch(kc);
-
const req = await watch.watch(`/api/v1/namespaces/kube-system/pods`, {},
// callback is called for each received object.
(type, obj) => {
@@ -119,6 +124,7 @@ export class NodeShellSession extends ShellSession {
reject(false);
}
);
+
setTimeout(() => {
req.abort();
reject(false);
@@ -129,17 +135,20 @@ export class NodeShellSession extends ShellSession {
protected deleteNodeShellPod() {
const kc = this.getKubeConfig();
const k8sApi = kc.makeApiClient(k8s.CoreV1Api);
+
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);
} else {
shell = new ShellSession(socket, cluster);
}
shell.open();
+
return shell;
}
diff --git a/src/main/port.ts b/src/main/port.ts
index 6ba8f71695..cd4c5701e8 100644
--- a/src/main/port.ts
+++ b/src/main/port.ts
@@ -5,11 +5,14 @@ import logger from "./logger";
export async function getFreePort(): Promise {
logger.debug("Lookup new free port..");
+
return new Promise((resolve, reject) => {
const server = net.createServer();
+
server.unref();
server.on("listening", () => {
const port = (server.address() as AddressInfo).port;
+
server.close(() => resolve(port));
logger.debug(`New port found: ${port}`);
});
diff --git a/src/main/port_spec.ts b/src/main/port_spec.ts
index 279bf5951e..43c2326b92 100644
--- a/src/main/port_spec.ts
+++ b/src/main/port_spec.ts
@@ -9,10 +9,12 @@ jest.mock("net", () => {
return new class MockServer extends EventEmitter {
listen = jest.fn(() => {
this.emit("listening");
+
return this;
});
address = () => {
newPort = Math.round(Math.random() * 10000);
+
return {
port: newPort
};
diff --git a/src/main/prometheus/helm.ts b/src/main/prometheus/helm.ts
index 56d739c630..438cc87a64 100644
--- a/src/main/prometheus/helm.ts
+++ b/src/main/prometheus/helm.ts
@@ -10,9 +10,11 @@ export class PrometheusHelm extends PrometheusLens {
public async getPrometheusService(client: CoreV1Api): Promise {
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;
return {
@@ -23,6 +25,7 @@ export class PrometheusHelm extends PrometheusLens {
};
} catch(error) {
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 725771a033..b829285f33 100644
--- a/src/main/prometheus/lens.ts
+++ b/src/main/prometheus/lens.ts
@@ -11,6 +11,7 @@ export class PrometheusLens implements PrometheusProvider {
try {
const resp = await client.readNamespacedService("prometheus", "lens-metrics");
const service = resp.body;
+
return {
id: this.id,
namespace: service.metadata.namespace,
@@ -72,6 +73,7 @@ export class PrometheusLens implements PrometheusProvider {
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*"),
diff --git a/src/main/prometheus/operator.ts b/src/main/prometheus/operator.ts
index 6880d4c27c..ee4c1e63bf 100644
--- a/src/main/prometheus/operator.ts
+++ b/src/main/prometheus/operator.ts
@@ -10,9 +10,11 @@ export class PrometheusOperator implements PrometheusProvider {
public async getPrometheusService(client: CoreV1Api): Promise {
try {
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];
}
}
@@ -26,6 +28,7 @@ export class PrometheusOperator implements PrometheusProvider {
};
} catch(error) {
logger.warn(`PrometheusOperator: failed to list services: ${error.toString()}`);
+
return;
}
}
@@ -80,6 +83,7 @@ export class PrometheusOperator implements PrometheusProvider {
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*"),
diff --git a/src/main/prometheus/provider-registry.ts b/src/main/prometheus/provider-registry.ts
index 641b1b8cf2..c649560c85 100644
--- a/src/main/prometheus/provider-registry.ts
+++ b/src/main/prometheus/provider-registry.ts
@@ -77,6 +77,7 @@ export class PrometheusProviderRegistry {
if (!this.prometheusProviders[type]) {
throw "Unknown Prometheus provider";
}
+
return this.prometheusProviders[type];
}
diff --git a/src/main/prometheus/stacklight.ts b/src/main/prometheus/stacklight.ts
index 84892214a1..5cb2773ca5 100644
--- a/src/main/prometheus/stacklight.ts
+++ b/src/main/prometheus/stacklight.ts
@@ -11,6 +11,7 @@ export class PrometheusStacklight implements PrometheusProvider {
try {
const resp = await client.readNamespacedService("prometheus-server", "stacklight");
const service = resp.body;
+
return {
id: this.id,
namespace: service.metadata.namespace,
@@ -72,6 +73,7 @@ export class PrometheusStacklight implements PrometheusProvider {
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*"),
diff --git a/src/main/resource-applier.ts b/src/main/resource-applier.ts
index c919185ae9..d4070f2378 100644
--- a/src/main/resource-applier.ts
+++ b/src/main/resource-applier.ts
@@ -16,19 +16,24 @@ export class ResourceApplier {
async apply(resource: KubernetesObject | any): Promise {
resource = this.sanitizeObject(resource);
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();
+
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}"`;
+
logger.debug(`shooting manifests with: ${cmd}`);
const execEnv: NodeJS.ProcessEnv = Object.assign({}, process.env);
const httpsProxy = this.cluster.preferences?.httpsProxy;
+
if (httpsProxy) {
execEnv["HTTPS_PROXY"] = httpsProxy;
}
@@ -37,6 +42,7 @@ export class ResourceApplier {
if (stderr != "") {
fs.unlinkSync(fileName);
reject(stderr);
+
return;
}
fs.unlinkSync(fileName);
@@ -48,20 +54,25 @@ export class ResourceApplier {
public async kubectlApplyAll(resources: string[]): Promise {
const { kubeCtl } = this.cluster;
const kubectlPath = await kubeCtl.getPath();
+
return new Promise((resolve, reject) => {
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}"`;
+
console.log("shooting manifests with:", cmd);
exec(cmd, (error, stdout, stderr) => {
if (error) {
reject(`Error applying manifests:${error}`);
}
+
if (stderr != "") {
reject(stderr);
+
return;
}
resolve(stdout);
@@ -74,9 +85,11 @@ export class ResourceApplier {
delete resource.status;
delete resource.metadata?.resourceVersion;
const annotations = resource.metadata?.annotations;
+
if (annotations) {
delete annotations["kubectl.kubernetes.io/last-applied-configuration"];
}
+
return resource;
}
}
diff --git a/src/main/router.ts b/src/main/router.ts
index f8aa76043d..896893a592 100644
--- a/src/main/router.ts
+++ b/src/main/router.ts
@@ -52,11 +52,15 @@ export class Router {
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;
}
+
return false;
}
@@ -66,6 +70,7 @@ export class Router {
parse: true,
output: "data",
});
+
return {
cluster,
path: url.pathname,
@@ -92,23 +97,29 @@ export class Router {
woff2: "font/woff2",
ttf: "font/ttf"
};
+
return mimeTypes[path.extname(filename).slice(1)] || "text/plain";
}
async handleStaticFile(filePath: string, res: http.ServerResponse, req: http.IncomingMessage, retryCount = 0) {
const asset = path.join(__static, filePath);
+
try {
const filename = path.basename(req.url);
// redirect requests to [appName].js, [appName].html /sockjs-node/ to webpack-dev-server (for hot-reload support)
const toWebpackDevServer = filename.includes(appName) || filename.includes("hot-update") || req.url.includes("sockjs-node");
+
if (isDevelopment && toWebpackDevServer) {
const redirectLocation = `http://localhost:${webpackDevServerPort}${req.url}`;
+
res.statusCode = 307;
res.setHeader("Location", redirectLocation);
res.end();
+
return;
}
const data = await readFile(asset);
+
res.setHeader("Content-Type", this.getMimeType(asset));
res.write(data);
res.end();
@@ -117,6 +128,7 @@ export class Router {
logger.error("handleStaticFile:", err.toString());
res.statusCode = 404;
res.end();
+
return;
}
this.handleStaticFile(`${publicPath}/${appName}.html`, res, req, Math.max(retryCount, 0) + 1);
diff --git a/src/main/routes/helm-route.ts b/src/main/routes/helm-route.ts
index b9a6cf513b..4d1cae8bc2 100644
--- a/src/main/routes/helm-route.ts
+++ b/src/main/routes/helm-route.ts
@@ -7,13 +7,16 @@ class HelmApiRoute extends LensApi {
public async listCharts(request: LensApiRequest) {
const { response } = request;
const charts = await helmService.listCharts();
+
this.respondJson(response, charts);
}
public async getChart(request: LensApiRequest) {
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);
@@ -22,8 +25,10 @@ class HelmApiRoute extends LensApi {
public async getChartValues(request: LensApiRequest) {
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);
@@ -32,8 +37,10 @@ class HelmApiRoute extends LensApi {
public async installChart(request: LensApiRequest) {
const { payload, cluster, response } = request;
+
try {
const result = await helmService.installChart(cluster, payload);
+
this.respondJson(response, result, 201);
} catch (error) {
logger.debug(error);
@@ -43,8 +50,10 @@ class HelmApiRoute extends LensApi {
public async updateRelease(request: LensApiRequest) {
const { cluster, params, payload, response } = request;
+
try {
const result = await helmService.updateRelease(cluster, params.release, params.namespace, payload );
+
this.respondJson(response, result);
} catch (error) {
logger.debug(error);
@@ -54,8 +63,10 @@ class HelmApiRoute extends LensApi {
public async rollbackRelease(request: LensApiRequest) {
const { cluster, params, payload, response } = request;
+
try {
const result = await helmService.rollback(cluster, params.release, params.namespace, payload.revision);
+
this.respondJson(response, result);
} catch (error) {
logger.debug(error);
@@ -65,8 +76,10 @@ class HelmApiRoute extends LensApi {
public async listReleases(request: LensApiRequest) {
const { cluster, params, response } = request;
+
try {
const result = await helmService.listReleases(cluster, params.namespace);
+
this.respondJson(response, result);
} catch(error) {
logger.debug(error);
@@ -76,8 +89,10 @@ class HelmApiRoute extends LensApi {
public async getRelease(request: LensApiRequest) {
const { cluster, params, response } = request;
+
try {
const result = await helmService.getRelease(cluster, params.release, params.namespace);
+
this.respondJson(response, result);
} catch (error) {
logger.debug(error);
@@ -87,8 +102,10 @@ class HelmApiRoute extends LensApi {
public async getReleaseValues(request: LensApiRequest) {
const { cluster, params, response } = request;
+
try {
const result = await helmService.getReleaseValues(cluster, params.release, params.namespace);
+
this.respondText(response, result);
} catch (error) {
logger.debug(error);
@@ -98,8 +115,10 @@ class HelmApiRoute extends LensApi {
public async getReleaseHistory(request: LensApiRequest) {
const { cluster, params, response } = request;
+
try {
const result = await helmService.getReleaseHistory(cluster, params.release, params.namespace);
+
this.respondJson(response, result);
} catch (error) {
logger.debug(error);
@@ -109,8 +128,10 @@ class HelmApiRoute extends LensApi {
public async deleteRelease(request: LensApiRequest) {
const { cluster, params, response } = request;
+
try {
const result = await helmService.deleteRelease(cluster, params.release, params.namespace);
+
this.respondJson(response, result);
} catch (error) {
logger.debug(error);
diff --git a/src/main/routes/kubeconfig-route.ts b/src/main/routes/kubeconfig-route.ts
index 088bf1167f..7fe5bcb9bc 100644
--- a/src/main/routes/kubeconfig-route.ts
+++ b/src/main/routes/kubeconfig-route.ts
@@ -5,6 +5,7 @@ import { CoreV1Api, V1Secret } from "@kubernetes/client-node";
function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster) {
const tokenData = Buffer.from(secret.data["token"], "base64");
+
return {
"apiVersion": "v1",
"kind": "Config",
@@ -43,14 +44,15 @@ class KubeconfigRoute extends LensApi {
public async routeServiceAccountRoute(request: LensApiRequest) {
const { params, response, cluster} = request;
-
const client = cluster.getProxyKubeconfig().makeApiClient(CoreV1Api);
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);
}
}
diff --git a/src/main/routes/metrics-route.ts b/src/main/routes/metrics-route.ts
index 4572030cca..d132f7b0ae 100644
--- a/src/main/routes/metrics-route.ts
+++ b/src/main/routes/metrics-route.ts
@@ -44,24 +44,31 @@ class MetricsRoute extends LensApi {
async routeMetrics({ response, cluster, payload, query }: LensApiRequest) {
const queryParams: IMetricsQuery = Object.fromEntries(query.entries());
const prometheusMetadata: ClusterPrometheusMetadata = {};
+
try {
const [prometheusPath, prometheusProvider] = await Promise.all([
cluster.contextHandler.getPrometheusPath(),
cluster.contextHandler.getPrometheusProvider()
]);
+
prometheusMetadata.provider = prometheusProvider?.id;
prometheusMetadata.autoDetected = !cluster.preferences.prometheusProvider?.type;
+
if (!prometheusPath) {
prometheusMetadata.success = false;
this.respondJson(response, {});
+
return;
}
+
// return data in same structure as query
if (typeof payload === "string") {
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);
} else {
const queries = Object.entries(payload).map(([queryName, queryOpts]) => (
@@ -69,6 +76,7 @@ class MetricsRoute extends LensApi {
));
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);
}
prometheusMetadata.success = true;
diff --git a/src/main/routes/port-forward-route.ts b/src/main/routes/port-forward-route.ts
index 967783aa3f..33c34758a4 100644
--- a/src/main/routes/port-forward-route.ts
+++ b/src/main/routes/port-forward-route.ts
@@ -52,15 +52,19 @@ class PortForward {
PortForward.portForwards.push(this);
this.process.on("exit", () => {
const index = PortForward.portForwards.indexOf(this);
+
if (index > -1) {
PortForward.portForwards.splice(index, 1);
}
});
+
try {
await tcpPortUsed.waitUntilUsed(this.localPort, 500, 15000);
+
return true;
} catch (error) {
this.process.kill();
+
return false;
}
}
@@ -75,11 +79,11 @@ class PortForwardRoute extends LensApi {
public async routePortForward(request: LensApiRequest) {
const { params, response, cluster} = request;
const { namespace, port, resourceType, resourceName } = params;
-
let portForward = PortForward.getPortforward({
clusterId: cluster.id, kind: resourceType, name: resourceName,
namespace, port
});
+
if (!portForward) {
logger.info(`Creating a new port-forward ${namespace}/${resourceType}/${resourceName}:${port}`);
portForward = new PortForward({
@@ -91,10 +95,12 @@ class PortForwardRoute extends LensApi {
kubeConfig: cluster.getProxyKubeconfigPath()
});
const started = await portForward.start();
+
if (!started) {
this.respondJson(response, {
message: "Failed to open port-forward"
}, 400);
+
return;
}
}
diff --git a/src/main/routes/resource-applier-route.ts b/src/main/routes/resource-applier-route.ts
index 8bbfec0d9c..1e532dde46 100644
--- a/src/main/routes/resource-applier-route.ts
+++ b/src/main/routes/resource-applier-route.ts
@@ -5,8 +5,10 @@ import { ResourceApplier } from "../resource-applier";
class ResourceApplierApiRoute extends LensApi {
public async applyResource(request: LensApiRequest) {
const { response, cluster, payload } = request;
+
try {
const resource = await new ResourceApplier(cluster).apply(payload);
+
this.respondJson(response, [resource], 200);
} catch (error) {
this.respondText(response, error, 422);
diff --git a/src/main/routes/watch-route.ts b/src/main/routes/watch-route.ts
index d44d5cae7b..eb9f007eae 100644
--- a/src/main/routes/watch-route.ts
+++ b/src/main/routes/watch-route.ts
@@ -25,6 +25,7 @@ class ApiWatcher {
}
this.processor = setInterval(() => {
const events = this.eventBuffer.splice(0);
+
events.map(event => this.sendEvent(event));
this.response.flushHeaders();
}, 50);
@@ -38,6 +39,7 @@ class ApiWatcher {
clearInterval(this.processor);
}
logger.debug(`Stopping watcher for api: ${this.apiUrl}`);
+
try {
this.watchRequest.abort();
this.sendEvent({
@@ -81,6 +83,7 @@ class WatchRoute extends LensApi {
message: "Empty request. Query params 'api' are not provided.",
example: "?api=/api/v1/pods&api=/api/v1/nodes",
}, 400);
+
return;
}
@@ -91,6 +94,7 @@ class WatchRoute extends LensApi {
apis.forEach(apiUrl => {
const watcher = new ApiWatcher(apiUrl, cluster.getProxyKubeconfig(), response);
+
watcher.start();
watchers.push(watcher);
});
diff --git a/src/main/shell-session.ts b/src/main/shell-session.ts
index 69979a51e3..19170695fc 100644
--- a/src/main/shell-session.ts
+++ b/src/main/shell-session.ts
@@ -39,11 +39,13 @@ export class ShellSession extends EventEmitter {
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.shellProcess = pty.spawn(shell, args, {
cols: 80,
cwd: this.cwd() || env.HOME,
@@ -65,6 +67,7 @@ export class ShellSession extends EventEmitter {
if(!this.preferences || !this.preferences.terminalCWD || this.preferences.terminalCWD === "") {
return null;
}
+
return this.preferences.terminalCWD;
}
@@ -85,6 +88,7 @@ export class ShellSession extends EventEmitter {
protected async getCachedShellEnv() {
let env = ShellSession.shellEnvs.get(this.clusterId);
+
if (!env) {
env = await this.getShellEnv();
ShellSession.shellEnvs.set(this.clusterId, env);
@@ -122,11 +126,14 @@ export class ShellSession extends EventEmitter {
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;
}
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"];
}
@@ -147,12 +154,14 @@ export class ShellSession extends EventEmitter {
if (!this.running) { return; }
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":
@@ -171,6 +180,7 @@ export class ShellSession extends EventEmitter {
this.shellProcess.onExit(({ exitCode }) => {
this.running = false;
let timeout = 0;
+
if (exitCode > 0) {
this.sendResponse("Terminal will auto-close in 15 seconds ...");
timeout = 15*1000;
diff --git a/src/main/shell-sync.ts b/src/main/shell-sync.ts
index 80e07a8ac5..b8b5bd81ea 100644
--- a/src/main/shell-sync.ts
+++ b/src/main/shell-sync.ts
@@ -14,8 +14,8 @@ interface Env {
*/
export async function shellSync() {
const { shell } = os.userInfo();
-
let envVars = {};
+
try {
envVars = await shellEnv(shell);
} catch (error) {
@@ -23,6 +23,7 @@ export async function shellSync() {
}
const env: Env = JSON.parse(JSON.stringify(envVars));
+
if (!env.LANG) {
// the LANG env var expects an underscore instead of electron's dash
env.LANG = `${app.getLocale().replace("-", "_")}.UTF-8`;
diff --git a/src/main/tray.ts b/src/main/tray.ts
index 2c5ba736f2..50df26cb8f 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 { dialog, Menu, NativeImage, nativeTheme, Tray } from "electron";
+import { dialog, Menu, NativeImage, Tray } from "electron";
import { autorun } from "mobx";
import { showAbout } from "./menu";
import { AppUpdater } from "./app-updater";
@@ -16,14 +16,11 @@ import { exitApp } from "./exit-app";
// note: instance of Tray should be saved somewhere, otherwise it disappears
export let tray: Tray;
-// refresh icon when MacOS dark/light theme has changed
-nativeTheme?.on("updated", () => tray?.setImage(getTrayIcon()));
-
-export function getTrayIcon(isDark = nativeTheme.shouldUseDarkColors): string {
+export function getTrayIcon(): string {
return path.resolve(
__static,
isDevelopment ? "../build/tray" : "icons", // copied within electron-builder extras
- `tray_icon${isDark ? "_dark" : ""}.png`
+ "trayIconTemplate.png"
);
}
@@ -31,11 +28,13 @@ export function initTray(windowManager: WindowManager) {
const dispose = autorun(() => {
try {
const menu = createTrayMenu(windowManager);
+
buildTray(getTrayIcon(), menu);
} catch (err) {
logger.error(`[TRAY]: building failed: ${err}`);
}
});
+
return () => {
dispose();
tray?.destroy();
@@ -63,6 +62,7 @@ export function createTrayMenu(windowManager: WindowManager): Menu {
async click() {
// note: argument[1] (browserWindow) not available when app is not focused / hidden
const browserWindow = await windowManager.ensureMainWindow();
+
showAbout(browserWindow);
},
},
@@ -85,11 +85,13 @@ export function createTrayMenu(windowManager: WindowManager): Menu {
.filter(workspace => clusterStore.getByWorkspaceId(workspace.id).length > 0) // hide empty workspaces
.map(workspace => {
const clusters = clusterStore.getByWorkspaceId(workspace.id);
+
return {
label: workspace.name,
toolTip: workspace.description,
submenu: clusters.map(cluster => {
const { id: clusterId, name: label, online, workspace } = cluster;
+
return {
label: `${online ? "✓" : "\x20".repeat(3)/*offset*/}${label}`,
toolTip: clusterId,
@@ -106,8 +108,10 @@ export function createTrayMenu(windowManager: WindowManager): Menu {
label: "Check for updates",
async click() {
const result = await AppUpdater.checkForUpdates();
+
if (!result) {
const browserWindow = await windowManager.ensureMainWindow();
+
dialog.showMessageBoxSync(browserWindow, {
message: "No updates available",
type: "info",
diff --git a/src/main/window-manager.ts b/src/main/window-manager.ts
index 1fa2614c50..7013966f08 100644
--- a/src/main/window-manager.ts
+++ b/src/main/window-manager.ts
@@ -37,11 +37,13 @@ export class WindowManager extends Singleton {
defaultWidth: 1440,
});
}
+
if (!this.mainWindow) {
// show icon in dock (mac-os only)
app.dock?.show();
const { width, height, x, y } = this.windowState;
+
this.mainWindow = new BrowserWindow({
x, y, width, height,
show: false,
@@ -80,6 +82,7 @@ export class WindowManager extends Singleton {
app.dock?.hide(); // hide icon in dock (mac-os)
});
}
+
try {
if (showSplash) await this.showSplash();
await this.mainWindow.loadURL(this.mainUrl);
@@ -109,6 +112,7 @@ export class WindowManager extends Singleton {
async ensureMainWindow(): Promise {
if (!this.mainWindow) await this.initMainWindow();
this.mainWindow.show();
+
return this.mainWindow;
}
@@ -131,6 +135,7 @@ export class WindowManager extends Singleton {
reload() {
const frameId = clusterFrameMap.get(this.activeClusterId);
+
if (frameId) {
this.sendToView({ channel: "renderer:reload", frameId });
} else {
@@ -158,8 +163,8 @@ export class WindowManager extends Singleton {
}
hide() {
- if (!this.mainWindow?.isDestroyed()) this.mainWindow.hide();
- if (!this.splashWindow.isDestroyed()) this.splashWindow.hide();
+ if (this.mainWindow && !this.mainWindow.isDestroyed()) this.mainWindow.hide();
+ if (this.splashWindow && !this.splashWindow.isDestroyed()) this.splashWindow.hide();
}
destroy() {
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 d7f1ef73f0..ccc8932e70 100644
--- a/src/migrations/cluster-store/2.0.0-beta.2.ts
+++ b/src/migrations/cluster-store/2.0.0-beta.2.ts
@@ -8,6 +8,7 @@ export default migration({
run(store) {
for (const value of store) {
const contextName = value[0];
+
// Looping all the keys gives out the store internal stuff too...
if (contextName === "__internal__" || value[1].hasOwnProperty("kubeConfig")) continue;
store.set(contextName, { kubeConfig: value[1] });
diff --git a/src/migrations/cluster-store/2.4.1.ts b/src/migrations/cluster-store/2.4.1.ts
index 2bde7755f1..aa6936e432 100644
--- a/src/migrations/cluster-store/2.4.1.ts
+++ b/src/migrations/cluster-store/2.4.1.ts
@@ -6,8 +6,10 @@ export default migration({
run(store) {
for (const value of store) {
const contextName = value[0];
+
if (contextName === "__internal__") continue;
const cluster = value[1];
+
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 ed4b648ce1..596d16c31f 100644
--- a/src/migrations/cluster-store/2.6.0-beta.2.ts
+++ b/src/migrations/cluster-store/2.6.0-beta.2.ts
@@ -6,9 +6,12 @@ export default migration({
run(store) {
for (const value of store) {
const clusterKey = value[0];
+
if (clusterKey === "__internal__") continue;
const cluster = value[1];
+
if (!cluster.preferences) cluster.preferences = {};
+
if (cluster.icon) {
cluster.preferences.icon = cluster.icon;
delete (cluster["icon"]);
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 63decfac1e..779fff7e7d 100644
--- a/src/migrations/cluster-store/2.6.0-beta.3.ts
+++ b/src/migrations/cluster-store/2.6.0-beta.3.ts
@@ -6,19 +6,26 @@ export default migration({
run(store, log) {
for (const value of store) {
const clusterKey = value[0];
+
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 (userObj) {
const user = userObj.user;
+
if (user["auth-provider"] && user["auth-provider"].config) {
const authConfig = user["auth-provider"].config;
+
if (authConfig["access-token"]) {
authConfig["access-token"] = `${authConfig["access-token"]}`;
}
+
if (authConfig.expiry) {
authConfig.expiry = `${authConfig.expiry}`;
}
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 a3c103769f..2f02a047f4 100644
--- a/src/migrations/cluster-store/2.7.0-beta.0.ts
+++ b/src/migrations/cluster-store/2.7.0-beta.0.ts
@@ -6,8 +6,10 @@ export default migration({
run(store) {
for (const value of store) {
const clusterKey = value[0];
+
if (clusterKey === "__internal__") continue;
const cluster = value[1];
+
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 73eb66ec2f..cb3934d422 100644
--- a/src/migrations/cluster-store/2.7.0-beta.1.ts
+++ b/src/migrations/cluster-store/2.7.0-beta.1.ts
@@ -6,18 +6,23 @@ export default migration({
version: "2.7.0-beta.1",
run(store) {
const clusters: any[] = [];
+
for (const value of store) {
const clusterKey = value[0];
+
if (clusterKey === "__internal__") continue;
if (clusterKey === "clusters") continue;
const cluster = value[1];
+
cluster.id = uuid();
+
if (!cluster.preferences.clusterName) {
cluster.preferences.clusterName = clusterKey;
}
clusters.push(cluster);
store.delete(clusterKey);
}
+
if (clusters.length > 0) {
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 597aab4713..ca2d0ccbed 100644
--- a/src/migrations/cluster-store/3.6.0-beta.1.ts
+++ b/src/migrations/cluster-store/3.6.0-beta.1.ts
@@ -32,6 +32,7 @@ export default migration({
} catch (error) {
printLog(`Failed to migrate Kubeconfig for cluster "${cluster.id}", removing cluster...`, error);
+
return undefined;
}
diff --git a/src/migrations/cluster-store/snap.ts b/src/migrations/cluster-store/snap.ts
index 699f88716f..74b89aad9c 100644
--- a/src/migrations/cluster-store/snap.ts
+++ b/src/migrations/cluster-store/snap.ts
@@ -12,6 +12,7 @@ export default migration({
printLog("Migrating embedded kubeconfig paths");
const storedClusters: ClusterModel[] = store.get("clusters") || [];
+
if (!storedClusters.length) return;
printLog("Number of clusters to migrate: ", storedClusters.length);
@@ -22,8 +23,10 @@ export default migration({
*/
if (!fs.existsSync(cluster.kubeConfigPath)) {
const kubeconfigPath = cluster.kubeConfigPath.replace(/\/snap\/kontena-lens\/[0-9]*\//, "/snap/kontena-lens/current/");
+
cluster.kubeConfigPath = kubeconfigPath;
}
+
return cluster;
});
diff --git a/src/renderer/api/api-manager.ts b/src/renderer/api/api-manager.ts
index 6c72f633f3..68d4773540 100644
--- a/src/renderer/api/api-manager.ts
+++ b/src/renderer/api/api-manager.ts
@@ -25,6 +25,7 @@ export class ApiManager {
protected resolveApi(api: string | KubeApi): KubeApi {
if (typeof api === "string") return this.getApi(api);
+
return api;
}
@@ -33,6 +34,7 @@ export class ApiManager {
else {
const apis = Array.from(this.apis.entries());
const entry = apis.find(entry => entry[1] === api);
+
if (entry) this.unregisterApi(entry[0]);
}
}
diff --git a/src/renderer/api/endpoints/cluster.api.ts b/src/renderer/api/endpoints/cluster.api.ts
index d3017c691a..e96ab7f082 100644
--- a/src/renderer/api/endpoints/cluster.api.ts
+++ b/src/renderer/api/endpoints/cluster.api.ts
@@ -90,6 +90,7 @@ export class Cluster extends KubeObject {
if (this.metadata.deletionTimestamp) return ClusterStatus.REMOVING;
if (!this.status || !this.status) return ClusterStatus.CREATING;
if (this.status.errorMessage) return ClusterStatus.ERROR;
+
return ClusterStatus.ACTIVE;
}
}
diff --git a/src/renderer/api/endpoints/crd.api.ts b/src/renderer/api/endpoints/crd.api.ts
index 02690a2afd..ad7d9d67ca 100644
--- a/src/renderer/api/endpoints/crd.api.ts
+++ b/src/renderer/api/endpoints/crd.api.ts
@@ -75,6 +75,7 @@ export class CustomResourceDefinition extends KubeObject {
getResourceApiBase() {
const { group } = this.spec;
+
return `/apis/${group}/${this.getVersion()}/${this.getPluralName()}`;
}
@@ -88,6 +89,7 @@ export class CustomResourceDefinition extends KubeObject {
getResourceTitle() {
const name = this.getPluralName();
+
return name[0].toUpperCase() + name.substr(1);
}
@@ -124,6 +126,7 @@ export class CustomResourceDefinition extends KubeObject {
const columns = this.spec.versions.find(a => this.getVersion() == a.name)?.additionalPrinterColumns
?? this.spec.additionalPrinterColumns?.map(({ JSONPath, ...rest }) => ({ ...rest, jsonPath: JSONPath })) // map to V1 shape
?? [];
+
return columns
.filter(column => column.name != "Age")
.filter(column => ignorePriority ? true : !column.priority);
@@ -135,8 +138,10 @@ export class CustomResourceDefinition extends KubeObject {
getConditions() {
if (!this.status?.conditions) return [];
+
return this.status.conditions.map(condition => {
const { message, reason, lastTransitionTime, status } = condition;
+
return {
...condition,
isReady: status === "True",
diff --git a/src/renderer/api/endpoints/cron-job.api.ts b/src/renderer/api/endpoints/cron-job.api.ts
index 2cca8bfb3d..6669f34736 100644
--- a/src/renderer/api/endpoints/cron-job.api.ts
+++ b/src/renderer/api/endpoints/cron-job.api.ts
@@ -71,6 +71,7 @@ export class CronJob extends KubeObject {
getLastScheduleTime() {
if (!this.status.lastScheduleTime) return "-";
const diff = moment().diff(this.status.lastScheduleTime);
+
return formatDuration(diff, true);
}
@@ -84,7 +85,9 @@ export class CronJob extends KubeObject {
const stamps = schedule.split(" ");
const day = Number(stamps[stamps.length - 3]); // 1-31
const month = Number(stamps[stamps.length - 2]); // 1-12
+
if (schedule.startsWith("@")) return false;
+
return day > daysInMonth[month - 1];
}
}
diff --git a/src/renderer/api/endpoints/daemon-set.api.ts b/src/renderer/api/endpoints/daemon-set.api.ts
index 63fc6363e4..8dab807517 100644
--- a/src/renderer/api/endpoints/daemon-set.api.ts
+++ b/src/renderer/api/endpoints/daemon-set.api.ts
@@ -66,6 +66,7 @@ export class DaemonSet extends WorkloadKubeObject {
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);
}
}
diff --git a/src/renderer/api/endpoints/deployment.api.ts b/src/renderer/api/endpoints/deployment.api.ts
index e3572a0bc4..d876616470 100644
--- a/src/renderer/api/endpoints/deployment.api.ts
+++ b/src/renderer/api/endpoints/deployment.api.ts
@@ -171,10 +171,13 @@ export class Deployment extends WorkloadKubeObject {
getConditions(activeOnly = false) {
const { conditions } = this.status;
+
if (!conditions) return [];
+
if (activeOnly) {
return conditions.filter(c => c.status === "True");
}
+
return conditions;
}
diff --git a/src/renderer/api/endpoints/endpoint.api.ts b/src/renderer/api/endpoints/endpoint.api.ts
index 121836a637..d19c2f127e 100644
--- a/src/renderer/api/endpoints/endpoint.api.ts
+++ b/src/renderer/api/endpoints/endpoint.api.ts
@@ -73,11 +73,13 @@ export class EndpointSubset implements IEndpointSubset {
getAddresses(): EndpointAddress[] {
const addresses = this.addresses || [];
+
return addresses.map(a => new EndpointAddress(a));
}
getNotReadyAddresses(): EndpointAddress[] {
const notReadyAddresses = this.notReadyAddresses || [];
+
return notReadyAddresses.map(a => new EndpointAddress(a));
}
@@ -85,10 +87,12 @@ export class EndpointSubset implements IEndpointSubset {
if(!this.addresses) {
return "";
}
+
return this.addresses.map(address => {
if (!this.ports) {
return address.ip;
}
+
return this.ports.map(port => {
return `${address.ip}:${port.port}`;
}).join(", ");
@@ -106,6 +110,7 @@ export class Endpoint extends KubeObject {
getEndpointSubsets(): EndpointSubset[] {
const subsets = this.subsets || [];
+
return subsets.map(s => new EndpointSubset(s));
}
diff --git a/src/renderer/api/endpoints/events.api.ts b/src/renderer/api/endpoints/events.api.ts
index 51dbf3c3b5..df9aa540a4 100644
--- a/src/renderer/api/endpoints/events.api.ts
+++ b/src/renderer/api/endpoints/events.api.ts
@@ -39,16 +39,19 @@ export class KubeEvent extends KubeObject {
getSource() {
const { component, host } = this.source;
+
return `${component} ${host || ""}`;
}
getFirstSeenTime() {
const diff = moment().diff(this.firstTimestamp);
+
return formatDuration(diff, true);
}
getLastSeenTime() {
const diff = moment().diff(this.lastTimestamp);
+
return formatDuration(diff, true);
}
}
diff --git a/src/renderer/api/endpoints/helm-charts.api.ts b/src/renderer/api/endpoints/helm-charts.api.ts
index 2964d18db2..a1fd497798 100644
--- a/src/renderer/api/endpoints/helm-charts.api.ts
+++ b/src/renderer/api/endpoints/helm-charts.api.ts
@@ -33,11 +33,13 @@ export const helmChartsApi = {
get(repo: string, name: string, readmeVersion?: string) {
const path = endpoint({ repo, name });
+
return apiBase
.get(`${path}?${stringify({ version: readmeVersion })}`)
.then(data => {
const versions = data.versions.map(HelmChart.create);
const readme = data.readme;
+
return {
readme,
versions,
diff --git a/src/renderer/api/endpoints/helm-releases.api.ts b/src/renderer/api/endpoints/helm-releases.api.ts
index b93d29caa9..84e095721b 100644
--- a/src/renderer/api/endpoints/helm-releases.api.ts
+++ b/src/renderer/api/endpoints/helm-releases.api.ts
@@ -76,9 +76,11 @@ export const helmReleasesApi = {
get(name: string, namespace: string) {
const path = endpoint({ name, namespace });
+
return apiBase.get(path).then(details => {
const items: KubeObject[] = JSON.parse(details.resources).items;
const resources = items.map(item => KubeObject.create(item));
+
return {
...details,
resources
@@ -88,35 +90,43 @@ export const helmReleasesApi = {
create(payload: IReleaseCreatePayload): Promise {
const { repo, ...data } = payload;
+
data.chart = `${repo}/${data.chart}`;
data.values = jsYaml.safeLoad(data.values);
+
return apiBase.post(endpoint(), { data });
},
update(name: string, namespace: string, payload: IReleaseUpdatePayload): Promise {
const { repo, ...data } = payload;
+
data.chart = `${repo}/${data.chart}`;
data.values = jsYaml.safeLoad(data.values);
+
return apiBase.put(endpoint({ name, namespace }), { data });
},
async delete(name: string, namespace: string) {
const path = endpoint({ name, namespace });
+
return apiBase.del(path);
},
getValues(name: string, namespace: string) {
const path = `${endpoint({ name, namespace })}/values`;
+
return apiBase.get(path);
},
getHistory(name: string, namespace: string): Promise {
const path = `${endpoint({ name, namespace })}/history`;
+
return apiBase.get(path);
},
rollback(name: string, namespace: string, revision: number) {
const path = `${endpoint({ name, namespace })}/rollback`;
+
return apiBase.put(path, {
data: {
revision
@@ -157,10 +167,13 @@ export class HelmRelease implements ItemObject {
getChart(withVersion = false) {
let chart = this.chart;
+
if(!withVersion && this.getVersion() != "" ) {
const search = new RegExp(`-${this.getVersion()}`);
+
chart = chart.replace(search, "");
}
+
return chart;
}
@@ -174,6 +187,7 @@ export class HelmRelease implements ItemObject {
getVersion() {
const versions = this.chart.match(/(v?\d+)[^-].*$/);
+
if (versions) {
return versions[0];
}
@@ -187,9 +201,11 @@ export class HelmRelease implements ItemObject {
const updated = this.updated.replace(/\s\w*$/, ""); // 2019-11-26 10:58:09 +0300 MSK -> 2019-11-26 10:58:09 +0300 to pass into Date()
const updatedDate = new Date(updated).getTime();
const diff = now - updatedDate;
+
if (humanize) {
return formatDuration(diff, compact);
}
+
return diff;
}
@@ -200,6 +216,7 @@ export class HelmRelease implements ItemObject {
const version = this.getVersion();
const versions = await helmChartStore.getVersions(chartName);
const chartVersion = versions.find(chartVersion => chartVersion.version === version);
+
return chartVersion ? chartVersion.repo : "";
}
}
diff --git a/src/renderer/api/endpoints/hpa.api.ts b/src/renderer/api/endpoints/hpa.api.ts
index 657e34e38c..4876ee43eb 100644
--- a/src/renderer/api/endpoints/hpa.api.ts
+++ b/src/renderer/api/endpoints/hpa.api.ts
@@ -80,8 +80,10 @@ export class HorizontalPodAutoscaler extends KubeObject {
getConditions() {
if (!this.status.conditions) return [];
+
return this.status.conditions.map(condition => {
const { message, reason, lastTransitionTime, status } = condition;
+
return {
...condition,
isReady: status === "True",
@@ -100,6 +102,7 @@ export class HorizontalPodAutoscaler extends KubeObject {
protected getMetricName(metric: IHpaMetric): string {
const { type, resource, pods, object, external } = metric;
+
switch (type) {
case HpaMetricType.Resource:
return resource.name;
@@ -122,14 +125,17 @@ export class HorizontalPodAutoscaler extends KubeObject {
const target = metric[metricType];
let currentValue = "unknown";
let targetValue = "unknown";
+
if (current) {
currentValue = current.currentAverageUtilization || current.currentAverageValue || current.currentValue;
if (current.currentAverageUtilization) currentValue += "%";
}
+
if (target) {
targetValue = target.targetAverageUtilization || target.targetAverageValue || target.targetValue;
if (target.targetAverageUtilization) targetValue += "%";
}
+
return `${currentValue} / ${targetValue}`;
}
}
diff --git a/src/renderer/api/endpoints/ingress.api.ts b/src/renderer/api/endpoints/ingress.api.ts
index 5832dc55db..7d035ad591 100644
--- a/src/renderer/api/endpoints/ingress.api.ts
+++ b/src/renderer/api/endpoints/ingress.api.ts
@@ -6,6 +6,7 @@ import { KubeApi } from "../kube-api";
export class IngressApi extends KubeApi {
getMetrics(ingress: string, namespace: string): Promise {
const opts = { category: "ingress", ingress };
+
return metricsApi.getMetrics({
bytesSentSuccess: opts,
bytesSentFailure: opts,
@@ -98,15 +99,18 @@ export class Ingress extends KubeObject {
getRoutes() {
const { spec: { tls, rules } } = this;
+
if (!rules) return [];
let protocol = "http";
const routes: string[] = [];
+
if (tls && tls.length > 0) {
protocol += "s";
}
rules.map(rule => {
const host = rule.host ? rule.host : "*";
+
if (rule.http && rule.http.paths) {
rule.http.paths.forEach(path => {
const { serviceName, servicePort } = getBackendServiceNamePort(path.backend);
@@ -132,7 +136,9 @@ export class Ingress extends KubeObject {
getHosts() {
const { spec: { rules } } = this;
+
if (!rules) return [];
+
return rules.filter(rule => rule.host).map(rule => rule.host);
}
@@ -141,7 +147,6 @@ export class Ingress extends KubeObject {
const { spec: { tls, rules, backend, defaultBackend } } = this;
const httpPort = 80;
const tlsPort = 443;
-
// Note: not using the port name (string)
const servicePort = defaultBackend?.service.port.number ?? backend?.servicePort;
@@ -152,6 +157,7 @@ export class Ingress extends KubeObject {
} else if (servicePort !== undefined) {
ports.push(Number(servicePort));
}
+
if (tls && tls.length > 0) {
ports.push(tlsPort);
}
diff --git a/src/renderer/api/endpoints/job.api.ts b/src/renderer/api/endpoints/job.api.ts
index 1dc78fdc94..65b9bcfdc3 100644
--- a/src/renderer/api/endpoints/job.api.ts
+++ b/src/renderer/api/endpoints/job.api.ts
@@ -86,12 +86,15 @@ export class Job extends WorkloadKubeObject {
// Type of Job condition could be only Complete or Failed
// https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.14/#jobcondition-v1-batch
const { conditions } = this.status;
+
if (!conditions) return;
+
return conditions.find(({ status }) => status === "True");
}
getImages() {
const containers: IPodContainer[] = get(this, "spec.template.spec.containers", []);
+
return [...containers].map(container => container.image);
}
@@ -99,6 +102,7 @@ export class Job extends WorkloadKubeObject {
const params: JsonApiParams = {
query: { propagationPolicy: "Background" }
};
+
return super.delete(params);
}
}
diff --git a/src/renderer/api/endpoints/metrics.api.ts b/src/renderer/api/endpoints/metrics.api.ts
index 86c829bcd6..a530c68506 100644
--- a/src/renderer/api/endpoints/metrics.api.ts
+++ b/src/renderer/api/endpoints/metrics.api.ts
@@ -41,6 +41,7 @@ export const metricsApi = {
if (!start && !end) {
const timeNow = Date.now() / 1000;
const now = moment.unix(timeNow).startOf("minute").unix(); // round date to minutes
+
start = now - range;
end = now;
}
@@ -76,8 +77,10 @@ export function normalizeMetrics(metrics: IMetrics, frames = 60): IMetrics {
// fill the gaps
result.forEach(res => {
if (!res.values || !res.values.length) return;
+
while (res.values.length < frames) {
const timestamp = moment.unix(res.values[0][0]).subtract(1, "minute").unix();
+
res.values.unshift([timestamp, "0"]);
}
});
@@ -101,14 +104,17 @@ export function isMetricsEmpty(metrics: { [key: string]: IMetrics }) {
export function getItemMetrics(metrics: { [key: string]: IMetrics }, itemName: string): { [key: string]: IMetrics } {
if (!metrics) return;
const itemMetrics = { ...metrics };
+
for (const metric in metrics) {
if (!metrics[metric]?.data?.result) {
continue;
}
const results = metrics[metric].data.result;
const result = results.find(res => Object.values(res.metric)[0] == itemName);
+
itemMetrics[metric].data.result = result ? [result] : [];
}
+
return itemMetrics;
}
@@ -118,11 +124,13 @@ export function getMetricLastPoints(metrics: { [key: string]: IMetrics }) {
Object.keys(metrics).forEach(metricName => {
try {
const metric = metrics[metricName];
+
if (metric.data.result.length) {
result[metricName] = +metric.data.result[0].values.slice(-1)[0][1];
}
} catch (e) {
}
+
return result;
}, {});
diff --git a/src/renderer/api/endpoints/network-policy.api.ts b/src/renderer/api/endpoints/network-policy.api.ts
index 4ecd333854..eb531990c2 100644
--- a/src/renderer/api/endpoints/network-policy.api.ts
+++ b/src/renderer/api/endpoints/network-policy.api.ts
@@ -55,6 +55,7 @@ export class NetworkPolicy extends KubeObject {
getMatchLabels(): string[] {
if (!this.spec.podSelector || !this.spec.podSelector.matchLabels) return [];
+
return Object
.entries(this.spec.podSelector.matchLabels)
.map(data => data.join(":"));
@@ -62,6 +63,7 @@ export class NetworkPolicy extends KubeObject {
getTypes(): string[] {
if (!this.spec.policyTypes) return [];
+
return this.spec.policyTypes;
}
}
diff --git a/src/renderer/api/endpoints/nodes.api.ts b/src/renderer/api/endpoints/nodes.api.ts
index c85cd8f9b0..d1794f0fb7 100644
--- a/src/renderer/api/endpoints/nodes.api.ts
+++ b/src/renderer/api/endpoints/nodes.api.ts
@@ -87,9 +87,12 @@ export class Node extends KubeObject {
getNodeConditionText() {
const { conditions } = this.status;
+
if (!conditions) return "";
+
return conditions.reduce((types, current) => {
if (current.status !== "True") return "";
+
return types += ` ${current.type}`;
}, "");
}
@@ -112,19 +115,23 @@ export class Node extends KubeObject {
getCpuCapacity() {
if (!this.status.capacity || !this.status.capacity.cpu) return 0;
+
return cpuUnitsToNumber(this.status.capacity.cpu);
}
getMemoryCapacity() {
if (!this.status.capacity || !this.status.capacity.memory) return 0;
+
return unitsToBytes(this.status.capacity.memory);
}
getConditions() {
const conditions = this.status.conditions || [];
+
if (this.isUnschedulable()) {
return [{ type: "SchedulingDisabled", status: "True" }, ...conditions];
}
+
return conditions;
}
@@ -134,6 +141,7 @@ export class Node extends KubeObject {
getWarningConditions() {
const goodConditions = ["Ready", "HostUpgrades", "SchedulingDisabled"];
+
return this.getActiveConditions().filter(condition => {
return !goodConditions.includes(condition.type);
});
@@ -145,6 +153,7 @@ export class Node extends KubeObject {
getOperatingSystem(): string {
const label = this.getLabels().find(label => label.startsWith("kubernetes.io/os="));
+
if (label) {
return label.split("=", 2)[1];
}
diff --git a/src/renderer/api/endpoints/persistent-volume-claims.api.ts b/src/renderer/api/endpoints/persistent-volume-claims.api.ts
index 529aefa951..1d9e1f1dce 100644
--- a/src/renderer/api/endpoints/persistent-volume-claims.api.ts
+++ b/src/renderer/api/endpoints/persistent-volume-claims.api.ts
@@ -52,6 +52,7 @@ export class PersistentVolumeClaim extends KubeObject {
getPods(allPods: Pod[]): Pod[] {
const pods = allPods.filter(pod => pod.getNs() === this.getNs());
+
return pods.filter(pod => {
return pod.getVolumes().filter(volume =>
volume.persistentVolumeClaim &&
@@ -62,22 +63,26 @@ export class PersistentVolumeClaim extends KubeObject {
getStorage(): string {
if (!this.spec.resources || !this.spec.resources.requests) return "-";
+
return this.spec.resources.requests.storage;
}
getMatchLabels(): string[] {
if (!this.spec.selector || !this.spec.selector.matchLabels) return [];
+
return Object.entries(this.spec.selector.matchLabels)
.map(([name, val]) => `${name}:${val}`);
}
getMatchExpressions() {
if (!this.spec.selector || !this.spec.selector.matchExpressions) return [];
+
return this.spec.selector.matchExpressions;
}
getStatus(): string {
if (this.status) return this.status.phase;
+
return "-";
}
}
diff --git a/src/renderer/api/endpoints/persistent-volume.api.ts b/src/renderer/api/endpoints/persistent-volume.api.ts
index 5e31eeb028..dd5dbb616e 100644
--- a/src/renderer/api/endpoints/persistent-volume.api.ts
+++ b/src/renderer/api/endpoints/persistent-volume.api.ts
@@ -47,20 +47,25 @@ export class PersistentVolume extends KubeObject {
getCapacity(inBytes = false) {
const capacity = this.spec.capacity;
+
if (capacity) {
if (inBytes) return unitsToBytes(capacity.storage);
+
return capacity.storage;
}
+
return 0;
}
getStatus() {
if (!this.status) return;
+
return this.status.phase || "-";
}
getClaimRefName() {
const { claimRef } = this.spec;
+
return claimRef ? claimRef.name : "";
}
}
diff --git a/src/renderer/api/endpoints/poddisruptionbudget.api.ts b/src/renderer/api/endpoints/poddisruptionbudget.api.ts
index b76260ae6f..50ab2d5b3d 100644
--- a/src/renderer/api/endpoints/poddisruptionbudget.api.ts
+++ b/src/renderer/api/endpoints/poddisruptionbudget.api.ts
@@ -22,6 +22,7 @@ export class PodDisruptionBudget extends KubeObject {
getSelectors() {
const selector = this.spec.selector;
+
return KubeObject.stringifyLabels(selector ? selector.matchLabels : null);
}
diff --git a/src/renderer/api/endpoints/pods.api.ts b/src/renderer/api/endpoints/pods.api.ts
index 3f62a3a952..11b581db8f 100644
--- a/src/renderer/api/endpoints/pods.api.ts
+++ b/src/renderer/api/endpoints/pods.api.ts
@@ -6,6 +6,7 @@ import { KubeApi } from "../kube-api";
export class PodsApi extends KubeApi {
async getLogs(params: { namespace: string; name: string }, query?: IPodLogsQuery): Promise {
const path = `${this.getUrl(params)}/log`;
+
return this.request.get(path, { query });
}
@@ -247,6 +248,7 @@ export class Pod extends WorkloadKubeObject {
getRunningContainers() {
const statuses = this.getContainerStatuses();
+
return this.getAllContainers().filter(container => {
return statuses.find(status => status.name === container.name && !!status.state["running"]);
}
@@ -256,18 +258,23 @@ export class Pod extends WorkloadKubeObject {
getContainerStatuses(includeInitContainers = true) {
const statuses: IPodContainerStatus[] = [];
const { containerStatuses, initContainerStatuses } = this.status;
+
if (containerStatuses) {
statuses.push(...containerStatuses);
}
+
if (includeInitContainers && initContainerStatuses) {
statuses.push(...initContainerStatuses);
}
+
return statuses;
}
getRestartsCount(): number {
const { containerStatuses } = this.status;
+
if (!containerStatuses) return 0;
+
return containerStatuses.reduce((count, item) => count + item.restartCount, 0);
}
@@ -290,18 +297,23 @@ export class Pod extends WorkloadKubeObject {
const goodConditions = ["Initialized", "Ready"].every(condition =>
!!this.getConditions().find(item => item.type === condition && item.status === "True")
);
+
if (reason === PodStatus.EVICTED) {
return PodStatus.EVICTED;
}
+
if (phase === PodStatus.FAILED) {
return PodStatus.FAILED;
}
+
if (phase === PodStatus.SUCCEEDED) {
return PodStatus.SUCCEEDED;
}
+
if (phase === PodStatus.RUNNING && goodConditions) {
return PodStatus.RUNNING;
}
+
return PodStatus.PENDING;
}
@@ -312,20 +324,26 @@ export class Pod extends WorkloadKubeObject {
let message = "";
const statuses = this.getContainerStatuses(false); // not including initContainers
+
if (statuses.length) {
statuses.forEach(status => {
const { state } = status;
+
if (state.waiting) {
const { reason } = state.waiting;
+
message = reason ? reason : "Waiting";
}
+
if (state.terminated) {
const { reason } = state.terminated;
+
message = reason ? reason : "Terminated";
}
});
}
if (message) return message;
+
return this.getStatusPhase();
}
@@ -349,7 +367,9 @@ export class Pod extends WorkloadKubeObject {
getNodeSelectors(): string[] {
const { nodeSelector } = this.spec;
+
if (!nodeSelector) return [];
+
return Object.entries(nodeSelector).map(values => values.join(": "));
}
@@ -367,8 +387,10 @@ export class Pod extends WorkloadKubeObject {
});
const crashLoop = !!this.getContainerStatuses().find(condition => {
const waiting = condition.state.waiting;
+
return (waiting && waiting.reason == "CrashLoopBackOff");
});
+
return (
notReady ||
crashLoop ||
@@ -391,18 +413,22 @@ export class Pod extends WorkloadKubeObject {
periodSeconds, successThreshold, failureThreshold
} = probeData;
const probe = [];
+
// HTTP Request
if (httpGet) {
const { path, port, host, scheme } = httpGet;
+
probe.push(
"http-get",
`${scheme.toLowerCase()}://${host || ""}:${port || ""}${path || ""}`,
);
}
+
// Command
if (exec && exec.command) {
probe.push(`exec [${exec.command.join(" ")}]`);
}
+
// TCP Probe
if (tcpSocket && tcpSocket.port) {
probe.push(`tcp-socket :${tcpSocket.port}`);
@@ -414,6 +440,7 @@ export class Pod extends WorkloadKubeObject {
`#success=${successThreshold || "0"}`,
`#failure=${failureThreshold || "0"}`,
);
+
return probe;
}
diff --git a/src/renderer/api/endpoints/podsecuritypolicy.api.ts b/src/renderer/api/endpoints/podsecuritypolicy.api.ts
index c7981f65be..dc5113625d 100644
--- a/src/renderer/api/endpoints/podsecuritypolicy.api.ts
+++ b/src/renderer/api/endpoints/podsecuritypolicy.api.ts
@@ -78,6 +78,7 @@ export class PodSecurityPolicy extends KubeObject {
getRules() {
const { fsGroup, runAsGroup, runAsUser, supplementalGroups, seLinux } = this.spec;
+
return {
fsGroup: fsGroup ? fsGroup.rule : "",
runAsGroup: runAsGroup ? runAsGroup.rule : "",
diff --git a/src/renderer/api/endpoints/replica-set.api.ts b/src/renderer/api/endpoints/replica-set.api.ts
index d1081811e9..999de8c1ac 100644
--- a/src/renderer/api/endpoints/replica-set.api.ts
+++ b/src/renderer/api/endpoints/replica-set.api.ts
@@ -48,6 +48,7 @@ export class ReplicaSet extends WorkloadKubeObject {
getImages() {
const containers: IPodContainer[] = get(this, "spec.template.spec.containers", []);
+
return [...containers].map(container => container.image);
}
}
diff --git a/src/renderer/api/endpoints/resource-applier.api.ts b/src/renderer/api/endpoints/resource-applier.api.ts
index a2843a6262..a397cfdda0 100644
--- a/src/renderer/api/endpoints/resource-applier.api.ts
+++ b/src/renderer/api/endpoints/resource-applier.api.ts
@@ -13,17 +13,20 @@ export const resourceApplierApi = {
if (typeof resource === "string") {
resource = jsYaml.safeLoad(resource);
}
+
return apiBase
.post("/stack", { data: resource })
.then(data => {
const items = data.map(obj => {
const api = apiManager.getApi(obj.metadata.selfLink);
+
if (api) {
return new api.objectConstructor(obj);
} else {
return new KubeObject(obj);
}
});
+
return items.length === 1 ? items[0] : items;
});
}
diff --git a/src/renderer/api/endpoints/resource-quota.api.ts b/src/renderer/api/endpoints/resource-quota.api.ts
index a19e4025c5..e2d37a9081 100644
--- a/src/renderer/api/endpoints/resource-quota.api.ts
+++ b/src/renderer/api/endpoints/resource-quota.api.ts
@@ -58,6 +58,7 @@ export class ResourceQuota extends KubeObject {
getScopeSelector() {
const { matchExpressions = [] } = this.spec.scopeSelector || {};
+
return matchExpressions;
}
}
diff --git a/src/renderer/api/endpoints/selfsubjectrulesreviews.api.ts b/src/renderer/api/endpoints/selfsubjectrulesreviews.api.ts
index 149a94e678..f47fc29a4e 100644
--- a/src/renderer/api/endpoints/selfsubjectrulesreviews.api.ts
+++ b/src/renderer/api/endpoints/selfsubjectrulesreviews.api.ts
@@ -38,16 +38,19 @@ export class SelfSubjectRulesReview extends KubeObject {
getResourceRules() {
const rules = this.status && this.status.resourceRules || [];
+
return rules.map(rule => this.normalize(rule));
}
getNonResourceRules() {
const rules = this.status && this.status.nonResourceRules || [];
+
return rules.map(rule => this.normalize(rule));
}
protected normalize(rule: ISelfSubjectReviewRule): ISelfSubjectReviewRule {
const { apiGroups = [], resourceNames = [], verbs = [], nonResourceURLs = [], resources = [] } = rule;
+
return {
apiGroups,
nonResourceURLs,
@@ -56,6 +59,7 @@ export class SelfSubjectRulesReview extends KubeObject {
resources: resources.map((resource, index) => {
const apiGroup = apiGroups.length >= index + 1 ? apiGroups[index] : apiGroups.slice(-1)[0];
const separator = apiGroup == "" ? "" : ".";
+
return resource + separator + apiGroup;
})
};
diff --git a/src/renderer/api/endpoints/service.api.ts b/src/renderer/api/endpoints/service.api.ts
index 219e02ab5d..6c02873139 100644
--- a/src/renderer/api/endpoints/service.api.ts
+++ b/src/renderer/api/endpoints/service.api.ts
@@ -61,9 +61,11 @@ export class Service extends KubeObject {
getExternalIps() {
const lb = this.getLoadBalancer();
+
if (lb && lb.ingress) {
return lb.ingress.map(val => val.ip || val.hostname);
}
+
return this.spec.externalIPs || [];
}
@@ -73,11 +75,13 @@ export class Service extends KubeObject {
getSelector(): string[] {
if (!this.spec.selector) return [];
+
return Object.entries(this.spec.selector).map(val => val.join("="));
}
getPorts(): ServicePort[] {
const ports = this.spec.ports || [];
+
return ports.map(p => new ServicePort(p));
}
diff --git a/src/renderer/api/endpoints/stateful-set.api.ts b/src/renderer/api/endpoints/stateful-set.api.ts
index 72c2ea5776..add2a554ba 100644
--- a/src/renderer/api/endpoints/stateful-set.api.ts
+++ b/src/renderer/api/endpoints/stateful-set.api.ts
@@ -102,6 +102,7 @@ export class StatefulSet extends WorkloadKubeObject {
getImages() {
const containers: IPodContainer[] = get(this, "spec.template.spec.containers", []);
+
return [...containers].map(container => container.image);
}
}
diff --git a/src/renderer/api/endpoints/storage-class.api.ts b/src/renderer/api/endpoints/storage-class.api.ts
index adb2059e4a..085701742c 100644
--- a/src/renderer/api/endpoints/storage-class.api.ts
+++ b/src/renderer/api/endpoints/storage-class.api.ts
@@ -18,6 +18,7 @@ export class StorageClass extends KubeObject {
isDefault() {
const annotations = this.metadata.annotations || {};
+
return (
annotations["storageclass.kubernetes.io/is-default-class"] === "true" ||
annotations["storageclass.beta.kubernetes.io/is-default-class"] === "true"
diff --git a/src/renderer/api/json-api.ts b/src/renderer/api/json-api.ts
index 177aa3c9c2..49c2cb1a28 100644
--- a/src/renderer/api/json-api.ts
+++ b/src/renderer/api/json-api.ts
@@ -75,11 +75,14 @@ export class JsonApi {
let reqUrl = this.config.apiBase + path;
const reqInit: RequestInit = { ...this.reqInit, ...init };
const { data, query } = params || {} as P;
+
if (data && !reqInit.body) {
reqInit.body = JSON.stringify(data);
}
+
if (query) {
const queryString = stringify(query);
+
reqUrl += (reqUrl.includes("?") ? "&" : "?") + queryString;
}
const infoLog: JsonApiLog = {
@@ -87,6 +90,7 @@ export class JsonApi {
reqUrl,
reqInit,
};
+
return cancelableFetch(reqUrl, reqInit).then(res => {
return this.parseResponse(res, infoLog);
});
@@ -94,21 +98,26 @@ export class JsonApi {
protected parseResponse(res: Response, log: JsonApiLog): Promise {
const { status } = res;
+
return res.text().then(text => {
let data;
+
try {
data = text ? JSON.parse(text) : ""; // DELETE-requests might not have response-body
} catch (e) {
data = text;
}
+
if (status >= 200 && status < 300) {
this.onData.emit(data, res);
this.writeLog({ ...log, data });
+
return data;
} else if (log.method === "GET" && res.status === 403) {
this.writeLog({ ...log, data });
} else {
const error = new JsonApiErrorParsed(data, this.parseError(data, res));
+
this.onError.emit(error, res);
this.writeLog({ ...log, error });
throw error;
@@ -126,6 +135,7 @@ export class JsonApi {
else if (error.message) {
return [error.message];
}
+
return [res.statusText || "Error!"];
}
@@ -133,6 +143,7 @@ export class JsonApi {
if (!this.config.debug) return;
const { method, reqUrl, ...params } = log;
let textStyle = "font-weight: bold;";
+
if (params.data) textStyle += "background: green; color: white;";
if (params.error) textStyle += "background: red; color: white;";
console.log(`%c${method} ${reqUrl}`, textStyle, params);
diff --git a/src/renderer/api/kube-api-parse.ts b/src/renderer/api/kube-api-parse.ts
index 174fdae918..285610bece 100644
--- a/src/renderer/api/kube-api-parse.ts
+++ b/src/renderer/api/kube-api-parse.ts
@@ -29,7 +29,6 @@ export function parseKubeApi(path: string): IKubeApiParsed {
path = new URL(path, location.origin).pathname;
const [, prefix, ...parts] = path.split("/");
const apiPrefix = `/${prefix}`;
-
const [left, right, namespaced] = splitArray(parts, "namespaces");
let apiGroup, apiVersion, namespace, resource, name;
@@ -107,9 +106,11 @@ export function parseKubeApi(path: string): IKubeApiParsed {
export function createKubeApiURL(ref: IKubeApiLinkRef): string {
const { apiPrefix = "/apis", resource, apiVersion, name } = ref;
let { namespace } = ref;
+
if (namespace) {
namespace = `namespaces/${namespace}`;
}
+
return [apiPrefix, apiVersion, namespace, resource, name]
.filter(v => v)
.join("/");
@@ -125,6 +126,7 @@ export function lookupApiLink(ref: IKubeObjectRef, parentObject: KubeObject): st
// search in registered apis by 'kind' & 'apiVersion'
const api = apiManager.getApi(api => api.kind === kind && api.apiVersionWithGroup == apiVersion);
+
if (api) {
return api.getUrl({ namespace, name });
}
@@ -132,8 +134,10 @@ export function lookupApiLink(ref: IKubeObjectRef, parentObject: KubeObject): st
// lookup api by generated resource link
const apiPrefixes = ["/apis", "/api"];
const resource = kind.endsWith("s") ? `${kind.toLowerCase()}es` : `${kind.toLowerCase()}s`;
+
for (const apiPrefix of apiPrefixes) {
const apiLink = createKubeApiURL({ apiPrefix, apiVersion, name, namespace, resource });
+
if (apiManager.getApi(apiLink)) {
return apiLink;
}
@@ -141,6 +145,7 @@ export function lookupApiLink(ref: IKubeObjectRef, parentObject: KubeObject): st
// resolve by kind only (hpa's might use refs to older versions of resources for example)
const apiByKind = apiManager.getApi(api => api.kind === kind);
+
if (apiByKind) {
return apiByKind.getUrl({ name, namespace });
}
diff --git a/src/renderer/api/kube-api.ts b/src/renderer/api/kube-api.ts
index 3ba1f37f8a..e7934675c6 100644
--- a/src/renderer/api/kube-api.ts
+++ b/src/renderer/api/kube-api.ts
@@ -72,6 +72,7 @@ export function forCluster(cluster: IKubeApiCluster, kubeC
"X-Cluster-ID": cluster.id
}
});
+
return new KubeApi({
objectConstructor: kubeClass,
request
@@ -83,6 +84,7 @@ export class KubeApi {
static watchAll(...apis: KubeApi[]) {
const disposers = apis.map(api => api.watch());
+
return () => disposers.forEach(unwatch => unwatch());
}
@@ -106,6 +108,7 @@ export class KubeApi {
kind = options.objectConstructor?.kind,
isNamespaced = options.objectConstructor?.namespaced
} = options || {};
+
if (!options.apiBase) {
options.apiBase = objectConstructor.apiBase;
}
@@ -205,6 +208,7 @@ export class KubeApi {
});
const res = await this.request.get(`${this.apiPrefix}/${this.apiGroup}`);
+
Object.defineProperty(this, "apiVersionPreferred", {
value: res?.preferredVersion?.version ?? null,
});
@@ -236,6 +240,7 @@ export class KubeApi {
namespace: this.isNamespaced ? namespace : undefined,
name,
});
+
return resourcePath + (query ? `?${stringify(this.normalizeQuery(query))}` : "");
}
@@ -243,14 +248,17 @@ export class KubeApi {
if (query.labelSelector) {
query.labelSelector = [query.labelSelector].flat().join(",");
}
+
if (query.fieldSelector) {
query.fieldSelector = [query.fieldSelector].flat().join(",");
}
+
return query;
}
protected parseResponse(data: KubeJsonApiData | KubeJsonApiData[] | KubeJsonApiDataList, namespace?: string): any {
const KubeObjectConstructor = this.objectConstructor;
+
if (KubeObject.isJsonApiData(data)) {
return new KubeObjectConstructor(data);
}
@@ -258,8 +266,10 @@ export class KubeApi {
// process items list response
if (KubeObject.isJsonApiDataList(data)) {
const { apiVersion, items, metadata } = data;
+
this.setResourceVersion(namespace, metadata.resourceVersion);
this.setResourceVersion("", metadata.resourceVersion);
+
return items.map(item => new KubeObjectConstructor({
kind: this.kind,
apiVersion,
@@ -277,6 +287,7 @@ export class KubeApi {
async list({ namespace = "" } = {}, query?: IKubeApiQueryParams): Promise {
await this.checkPreferredVersion();
+
return this.request
.get(this.getUrl({ namespace }), { query })
.then(data => this.parseResponse(data, namespace));
@@ -284,6 +295,7 @@ export class KubeApi {
async get({ name = "", namespace = "default" } = {}, query?: IKubeApiQueryParams): Promise {
await this.checkPreferredVersion();
+
return this.request
.get(this.getUrl({ namespace, name }), { query })
.then(this.parseResponse);
@@ -310,6 +322,7 @@ export class KubeApi {
async update({ name = "", namespace = "default" } = {}, data?: Partial): Promise {
await this.checkPreferredVersion();
const apiUrl = this.getUrl({ namespace, name });
+
return this.request
.put(apiUrl, { data })
.then(this.parseResponse);
@@ -318,6 +331,7 @@ export class KubeApi {
async delete({ name = "", namespace = "default" }) {
await this.checkPreferredVersion();
const apiUrl = this.getUrl({ namespace, name });
+
return this.request.del(apiUrl);
}
diff --git a/src/renderer/api/kube-json-api.ts b/src/renderer/api/kube-json-api.ts
index ce7da50826..3026a9a956 100644
--- a/src/renderer/api/kube-json-api.ts
+++ b/src/renderer/api/kube-json-api.ts
@@ -45,9 +45,11 @@ export interface KubeJsonApiError extends JsonApiError {
export class KubeJsonApi extends JsonApi {
protected parseError(error: KubeJsonApiError | any, res: Response): string[] {
const { status, reason, message } = error;
+
if (status && reason) {
return [message || `${status}: ${reason}`];
}
+
return super.parseError(error, res);
}
}
diff --git a/src/renderer/api/kube-object.ts b/src/renderer/api/kube-object.ts
index 8d0e6123f3..08bd6401b9 100644
--- a/src/renderer/api/kube-object.ts
+++ b/src/renderer/api/kube-object.ts
@@ -65,6 +65,7 @@ export class KubeObject implements ItemObject {
static stringifyLabels(labels: { [name: string]: string }): string[] {
if (!labels) return [];
+
return Object.entries(labels).map(([name, value]) => `${name}=${value}`);
}
@@ -104,9 +105,11 @@ export class KubeObject implements ItemObject {
return moment(this.metadata.creationTimestamp).fromNow();
}
const diff = new Date().getTime() - new Date(this.metadata.creationTimestamp).getTime();
+
if (humanize) {
return formatDuration(diff, compact);
}
+
return diff;
}
@@ -120,14 +123,17 @@ export class KubeObject implements ItemObject {
getAnnotations(filter = false): string[] {
const labels = KubeObject.stringifyLabels(this.metadata.annotations);
+
return filter ? labels.filter(label => {
const skip = resourceApplierApi.annotations.some(key => label.startsWith(key));
+
return !skip;
}) : labels;
}
getOwnerRefs() {
const refs = this.metadata.ownerReferences || [];
+
return refs.map(ownerRef => ({
...ownerRef,
namespace: this.getNs(),
@@ -136,6 +142,7 @@ export class KubeObject implements ItemObject {
getSearchFields() {
const { getName, getId, getNs, getAnnotations, getLabels } = this;
+
return [
getName(),
getNs(),
diff --git a/src/renderer/api/kube-watch-api.ts b/src/renderer/api/kube-watch-api.ts
index 8d5a318c3c..58665a11a1 100644
--- a/src/renderer/api/kube-watch-api.ts
+++ b/src/renderer/api/kube-watch-api.ts
@@ -53,8 +53,10 @@ export class KubeWatchApi {
apis.forEach(api => {
this.subscribers.set(api, this.getSubscribersCount(api) + 1);
});
+
return () => apis.forEach(api => {
const count = this.getSubscribersCount(api) - 1;
+
if (count <= 0) this.subscribers.delete(api);
else this.subscribers.set(api, count);
});
@@ -62,9 +64,11 @@ export class KubeWatchApi {
protected getQuery(): Partial {
const { isAdmin, allowedNamespaces } = getHostedCluster();
+
return {
api: this.activeApis.map(api => {
if (isAdmin) return api.getWatchUrl();
+
return allowedNamespaces.map(namespace => api.getWatchUrl(namespace));
}).flat()
};
@@ -74,11 +78,13 @@ export class KubeWatchApi {
@autobind()
protected connect() {
if (this.evtSource) this.disconnect(); // close previous connection
+
if (!this.activeApis.length) {
return;
}
const query = this.getQuery();
const apiUrl = `${apiPrefix}/watch?${stringify(query)}`;
+
this.evtSource = new EventSource(apiUrl);
this.evtSource.onmessage = this.onMessage;
this.evtSource.onerror = this.onError;
@@ -102,6 +108,7 @@ export class KubeWatchApi {
protected onMessage(evt: MessageEvent) {
if (!evt.data) return;
const data = JSON.parse(evt.data);
+
if ((data as IKubeWatchEvent).object) {
this.onData.emit(data);
} else {
@@ -114,12 +121,14 @@ export class KubeWatchApi {
this.disconnect();
const { apiBase, namespace } = KubeApi.parseApi(event.url);
const api = apiManager.getApi(apiBase);
+
if (api) {
try {
await api.refreshResourceVersion({ namespace });
this.reconnect();
} catch (error) {
console.error("failed to refresh resource version", error);
+
if (this.subscribers.size > 0) {
setTimeout(() => {
this.onRouteEvent(event);
@@ -132,6 +141,7 @@ export class KubeWatchApi {
protected onError(evt: MessageEvent) {
const { reconnectAttempts: attemptsRemain, reconnectTimeoutMs } = this;
+
if (evt.eventPhase === EventSource.CLOSED) {
if (attemptsRemain > 0) {
this.reconnectAttempts--;
@@ -150,13 +160,17 @@ export class KubeWatchApi {
const listener = (evt: IKubeWatchEvent) => {
const { selfLink, namespace, resourceVersion } = evt.object.metadata;
const api = apiManager.getApi(selfLink);
+
api.setResourceVersion(namespace, resourceVersion);
api.setResourceVersion("", resourceVersion);
+
if (store == apiManager.getStore(api)) {
callback(evt);
}
};
+
this.onData.addListener(listener);
+
return () => this.onData.removeListener(listener);
}
diff --git a/src/renderer/api/terminal-api.ts b/src/renderer/api/terminal-api.ts
index 39a86faae5..1a94052586 100644
--- a/src/renderer/api/terminal-api.ts
+++ b/src/renderer/api/terminal-api.ts
@@ -50,26 +50,32 @@ export class TerminalApi extends WebSocketApi {
const { id, node } = this.options;
const wss = `ws${protocol === "https:" ? "s" : ""}://`;
const query: TerminalApiQuery = { id };
+
if (port) {
port = `:${port}`;
}
+
if (node) {
query.node = node;
query.type = "node";
}
+
return `${wss}${hostname}${port}/api?${stringify(query)}`;
}
async connect() {
const apiUrl = await this.getUrl();
+
this.emitStatus("Connecting ...");
this.onData.addListener(this._onReady, { prepend: true });
+
return super.connect(apiUrl);
}
destroy() {
if (!this.socket) return;
const exitCode = String.fromCharCode(4); // ctrl+d
+
this.sendCommand(exitCode);
setTimeout(() => super.destroy(), 2000);
}
@@ -87,6 +93,7 @@ export class TerminalApi extends WebSocketApi {
this.onData.removeListener(this._onReady);
this.flush();
this.onData.emit(data); // re-emit data
+
return false; // prevent calling rest of listeners
}
@@ -100,6 +107,7 @@ export class TerminalApi extends WebSocketApi {
sendTerminalSize(cols: number, rows: number) {
const newSize = { Width: cols, Height: rows };
+
if (!isEqual(this.size, newSize)) {
this.sendCommand(JSON.stringify(newSize), TerminalChannels.TERMINAL_SIZE);
this.size = newSize;
@@ -108,6 +116,7 @@ export class TerminalApi extends WebSocketApi {
protected parseMessage(data: string) {
data = data.substr(1); // skip channel
+
return base64.decode(data);
}
@@ -125,10 +134,12 @@ export class TerminalApi extends WebSocketApi {
protected emitStatus(data: string, options: { color?: TerminalColor; showTime?: boolean } = {}) {
const { color, showTime } = options;
+
if (color) {
data = `${color}${data}${TerminalColor.NO_COLOR}`;
}
let time;
+
if (showTime) {
time = `${(new Date()).toLocaleString()} `;
}
diff --git a/src/renderer/api/websocket-api.ts b/src/renderer/api/websocket-api.ts
index b9e0c22c96..79a92cf99e 100644
--- a/src/renderer/api/websocket-api.ts
+++ b/src/renderer/api/websocket-api.ts
@@ -47,9 +47,11 @@ export class WebSocketApi {
constructor(protected params: IParams) {
this.params = Object.assign({}, WebSocketApi.defaultParams, params);
const { autoConnect, pingIntervalSeconds } = this.params;
+
if (autoConnect) {
setTimeout(() => this.connect());
}
+
if (pingIntervalSeconds) {
this.pingTimer = setInterval(() => this.ping(), pingIntervalSeconds * 1000);
}
@@ -57,6 +59,7 @@ export class WebSocketApi {
get isConnected() {
const state = this.socket ? this.socket.readyState : -1;
+
return state === WebSocket.OPEN && this.isOnline;
}
@@ -87,6 +90,7 @@ export class WebSocketApi {
reconnect() {
const { reconnectDelaySeconds } = this.params;
+
if (!reconnectDelaySeconds) return;
this.writeLog("reconnect after", `${reconnectDelaySeconds}ms`);
this.reconnectTimer = setTimeout(() => this.connect(), reconnectDelaySeconds * 1000);
@@ -115,6 +119,7 @@ export class WebSocketApi {
id: (Math.random() * Date.now()).toString(16).replace(".", ""),
data: command,
};
+
if (this.isConnected) {
this.socket.send(msg.data);
}
@@ -141,6 +146,7 @@ export class WebSocketApi {
protected _onMessage(evt: MessageEvent) {
const data = this.parseMessage(evt.data);
+
this.onData.emit(data);
this.writeLog("%cMESSAGE", "color:black;font-weight:bold;", data);
}
@@ -151,6 +157,7 @@ export class WebSocketApi {
protected _onClose(evt: CloseEvent) {
const error = evt.code !== 1000 || !evt.wasClean;
+
if (error) {
this.reconnect();
}
diff --git a/src/renderer/api/workload-kube-object.ts b/src/renderer/api/workload-kube-object.ts
index 51c0461f15..e0c6d3f121 100644
--- a/src/renderer/api/workload-kube-object.ts
+++ b/src/renderer/api/workload-kube-object.ts
@@ -51,16 +51,19 @@ export class WorkloadKubeObject extends KubeObject {
getSelectors(): string[] {
const selector = this.spec.selector;
+
return KubeObject.stringifyLabels(selector ? selector.matchLabels : null);
}
getNodeSelectors(): string[] {
const nodeSelector = get(this, "spec.template.spec.nodeSelector");
+
return KubeObject.stringifyLabels(nodeSelector);
}
getTemplateLabels(): string[] {
const labels = get(this, "spec.template.metadata.labels");
+
return KubeObject.stringifyLabels(labels);
}
@@ -74,7 +77,9 @@ export class WorkloadKubeObject extends KubeObject {
getAffinityNumber() {
const affinity = this.getAffinity();
+
if (!affinity) return 0;
+
return Object.keys(affinity).length;
}
}
\ No newline at end of file
diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx
index ef22e72736..f2369df0fd 100644
--- a/src/renderer/bootstrap.tsx
+++ b/src/renderer/bootstrap.tsx
@@ -30,6 +30,7 @@ export {
export async function bootstrap(App: AppComponent) {
const rootElem = document.getElementById("app");
+
rootElem.classList.toggle("is-mac", isMac);
extensionLoader.init();
diff --git a/src/renderer/components/+add-cluster/add-cluster.tsx b/src/renderer/components/+add-cluster/add-cluster.tsx
index e18a655f97..5122dfe02f 100644
--- a/src/renderer/components/+add-cluster/add-cluster.tsx
+++ b/src/renderer/components/+add-cluster/add-cluster.tsx
@@ -68,6 +68,7 @@ export class AddCluster extends React.Component {
Notifications.error(
Can't setup {filePath} as kubeconfig: {String(err)}
);
+
if (throwError) {
throw err;
}
@@ -82,12 +83,14 @@ export class AddCluster extends React.Component {
switch (this.sourceTab) {
case KubeConfigSourceTab.FILE:
const contexts = this.getContexts(this.kubeConfigLocal);
+
this.kubeContexts.replace(contexts);
break;
case KubeConfigSourceTab.TEXT:
try {
this.error = "";
const contexts = this.getContexts(loadConfig(this.customConfig || "{}"));
+
this.kubeContexts.replace(contexts);
} catch (err) {
this.error = String(err);
@@ -102,9 +105,11 @@ export class AddCluster extends React.Component {
getContexts(config: KubeConfig): Map {
const contexts = new Map();
+
splitConfig(config).forEach(config => {
contexts.set(config.currentContext, config);
});
+
return contexts;
}
@@ -116,6 +121,7 @@ export class AddCluster extends React.Component {
message: _i18n._(t`Select custom kubeconfig file`),
buttonLabel: _i18n._(t`Use configuration`),
});
+
if (!canceled && filePaths.length) {
this.setKubeConfig(filePaths[0]);
}
@@ -129,9 +135,11 @@ export class AddCluster extends React.Component {
@action
addClusters = () => {
let newClusters: ClusterModel[] = [];
+
try {
if (!this.selectedContexts.length) {
this.error = Please select at least one cluster context ;
+
return;
}
this.error = "";
@@ -140,12 +148,16 @@ export class AddCluster extends React.Component {
newClusters = this.selectedContexts.filter(context => {
try {
const kubeConfig = this.kubeContexts.get(context);
+
validateKubeConfig(kubeConfig);
+
return true;
} catch (err) {
this.error = String(err.message);
+
if (err instanceof ExecValidationNotFoundError) {
Notifications.error(Error while adding cluster(s): {this.error} );
+
return false;
} else {
throw new Error(err);
@@ -157,6 +169,7 @@ export class AddCluster extends React.Component {
const kubeConfigPath = this.sourceTab === KubeConfigSourceTab.FILE
? this.kubeConfigPath // save link to original kubeconfig in file-system
: ClusterStore.embedCustomKubeConfig(clusterId, kubeConfig); // save in app-files folder
+
return {
id: clusterId,
kubeConfigPath,
@@ -171,8 +184,10 @@ export class AddCluster extends React.Component {
runInAction(() => {
clusterStore.addClusters(...newClusters);
+
if (newClusters.length === 1) {
const clusterId = newClusters[0].id;
+
clusterStore.setActive(clusterId);
navigate(clusterViewURL({ params: { clusterId } }));
} else {
@@ -271,6 +286,7 @@ export class AddCluster extends React.Component {
const placeholder = this.selectedContexts.length > 0
? Selected contexts: {this.selectedContexts.length}
: Select contexts ;
+
return (
{
const isChanged = this.kubeConfigPath !== userStore.kubeConfigPath;
+
if (isChanged) {
this.kubeConfigPath = this.kubeConfigPath.replace("~", os.homedir());
+
try {
this.setKubeConfig(this.kubeConfigPath, { throwError: true });
} catch (err) {
@@ -321,6 +339,7 @@ export class AddCluster extends React.Component {
protected formatContextLabel = ({ value: context }: SelectOption) => {
const isNew = userStore.newContexts.has(context);
const isSelected = this.selectedContexts.includes(context);
+
return (
{context}
@@ -332,6 +351,7 @@ export class AddCluster extends React.Component {
render() {
const submitDisabled = this.selectedContexts.length === 0;
+
return (
Add Clusters}>
diff --git a/src/renderer/components/+apps-helm-charts/helm-chart-details.tsx b/src/renderer/components/+apps-helm-charts/helm-chart-details.tsx
index fb7657291d..dff5c8b050 100644
--- a/src/renderer/components/+apps-helm-charts/helm-chart-details.tsx
+++ b/src/renderer/components/+apps-helm-charts/helm-chart-details.tsx
@@ -37,6 +37,7 @@ export class HelmChartDetails extends Component {
chartUpdater = autorun(() => {
this.selectedChart = null;
const { chart: { name, repo, version } } = this.props;
+
helmChartsApi.get(repo, name, version).then(result => {
this.readme = result.readme;
this.chartVersions = result.versions;
@@ -56,6 +57,7 @@ export class HelmChartDetails extends Component {
this.chartPromise?.cancel();
const { chart: { name, repo } } = this.props;
const { readme } = await (this.chartPromise = helmChartsApi.get(repo, name, version));
+
this.readme = readme;
} catch (error) {
this.error = error;
@@ -71,6 +73,7 @@ export class HelmChartDetails extends Component {
renderIntroduction() {
const { selectedChart, chartVersions, onVersionChange } = this;
const placeholder = require("./helm-placeholder.svg");
+
return (
{
render() {
const { chart, hideDetails } = this.props;
const title = chart ?
Chart: {chart.getFullName()} : "";
+
return (
{
async getVersions(chartName: string, force?: boolean): Promise {
let versions = this.versions.get(chartName);
+
if (versions && !force) {
return versions;
}
+
const loadVersions = (repo: string) => {
return helmChartsApi.get(repo, chartName).then(({ versions }) => {
return versions.map(chart => ({
@@ -41,17 +43,20 @@ export class HelmChartStore extends ItemStore {
}));
});
};
+
if (!this.isLoaded) {
await this.loadAll();
}
const repos = this.items
.filter(chart => chart.getName() === chartName)
.map(chart => chart.getRepository());
+
versions = await Promise.all(repos.map(loadVersions))
.then(flatten)
.then(this.sortVersions);
this.versions.set(chartName, versions);
+
return versions;
}
diff --git a/src/renderer/components/+apps-helm-charts/helm-charts.tsx b/src/renderer/components/+apps-helm-charts/helm-charts.tsx
index d1e221240d..305e00623c 100644
--- a/src/renderer/components/+apps-helm-charts/helm-charts.tsx
+++ b/src/renderer/components/+apps-helm-charts/helm-charts.tsx
@@ -29,6 +29,7 @@ export class HelmCharts extends Component {
get selectedChart() {
const { match: { params: { chartName, repo } } } = this.props;
+
return helmChartStore.getByName(chartName, repo);
}
diff --git a/src/renderer/components/+apps-releases/release-details.tsx b/src/renderer/components/+apps-releases/release-details.tsx
index a27db5dc83..41ad5f1c8f 100644
--- a/src/renderer/components/+apps-releases/release-details.tsx
+++ b/src/renderer/components/+apps-releases/release-details.tsx
@@ -55,6 +55,7 @@ export class ReleaseDetails extends Component {
const { getReleaseSecret } = releaseStore;
const { release } = this.props;
const secret = getReleaseSecret(release);
+
if (this.releaseSecret) {
if (isEqual(this.releaseSecret.getLabels(), secret.getLabels())) return;
this.loadDetails();
@@ -64,12 +65,14 @@ export class ReleaseDetails extends Component {
async loadDetails() {
const { release } = this.props;
+
this.details = null;
this.details = await helmReleasesApi.get(release.getName(), release.getNs());
}
async loadValues() {
const { release } = this.props;
+
this.values = "";
this.values = await helmReleasesApi.getValues(release.getName(), release.getNs());
}
@@ -84,7 +87,9 @@ export class ReleaseDetails extends Component {
version: release.getVersion(),
values: this.values
};
+
this.saving = true;
+
try {
await releaseStore.update(name, namespace, data);
Notifications.ok(
@@ -98,12 +103,14 @@ export class ReleaseDetails extends Component {
upgradeVersion = () => {
const { release, hideDetails } = this.props;
+
createUpgradeChartTab(release);
hideDetails();
};
renderValues() {
const { values, saving } = this;
+
return (
@@ -127,6 +134,7 @@ export class ReleaseDetails extends Component {
renderNotes() {
if (!this.details.info?.notes) return null;
const { notes } = this.details.info;
+
return (
{notes}
@@ -136,6 +144,7 @@ export class ReleaseDetails extends Component
{
renderResources() {
const { resources } = this.details;
+
if (!resources) return null;
const groups = groupBy(resources, item => item.kind);
const tables = Object.entries(groups).map(([kind, items]) => {
@@ -156,6 +165,7 @@ export class ReleaseDetails extends Component {
name,
namespace,
})) : "";
+
return (
@@ -170,6 +180,7 @@ export class ReleaseDetails extends Component {
);
});
+
return (
{tables}
@@ -180,10 +191,13 @@ export class ReleaseDetails extends Component
{
renderContent() {
const { release } = this.props;
const { details } = this;
+
if (!release) return null;
+
if (!details) {
return ;
}
+
return (
Chart} className="chart">
@@ -229,6 +243,7 @@ export class ReleaseDetails extends Component {
const { release, hideDetails } = this.props;
const title = release ? Release: {release.getName()} : "";
const toolbar = ;
+
return (
{
@autobind()
upgrade() {
const { release, hideDetails } = this.props;
+
createUpgradeChartTab(release);
hideDetails && hideDetails();
}
@@ -35,8 +36,10 @@ export class HelmReleaseMenu extends React.Component {
renderContent() {
const { release, toolbar } = this.props;
+
if (!release) return;
const hasRollback = release && release.getRevision() > 1;
+
return (
<>
{hasRollback && (
@@ -51,6 +54,7 @@ export class HelmReleaseMenu extends React.Component {
render() {
const { className, release, ...menuProps } = this.props;
+
return (
{
this.isLoading = true;
const currentRevision = this.release.getRevision();
let releases = await helmReleasesApi.getHistory(this.release.getName(), this.release.getNs());
+
releases = releases.filter(item => item.revision !== currentRevision); // remove current
releases = orderBy(releases, "revision", "desc"); // sort
this.revisions.replace(releases);
@@ -50,6 +51,7 @@ export class ReleaseRollbackDialog extends React.Component {
rollback = async () => {
const revisionNumber = this.revision.revision;
+
try {
await releaseStore.rollback(this.release.getName(), this.release.getNs(), revisionNumber);
this.close();
@@ -64,9 +66,11 @@ export class ReleaseRollbackDialog extends React.Component {
renderContent() {
const { revision, revisions } = this;
+
if (!revision) {
return No revisions to rollback.
;
}
+
return (
Revision
@@ -85,6 +89,7 @@ export class ReleaseRollbackDialog extends React.Component
{
const { ...dialogProps } = this.props;
const releaseName = this.release ? this.release.getName() : "";
const header = Rollback {releaseName} ;
+
return (
{
const amountChanged = secrets.length !== this.releaseSecrets.length;
const labelsChanged = this.releaseSecrets.some(item => {
const secret = secrets.find(secret => secret.getId() == item.getId());
+
if (!secret) return;
+
return !isEqual(item.getLabels(), secret.getLabels());
});
+
if (amountChanged || labelsChanged) {
this.loadAll();
}
@@ -49,6 +52,7 @@ export class ReleaseStore extends ItemStore {
owner: "helm",
name: release.getName()
};
+
return secretsStore.getByLabel(labels)
.filter(secret => secret.getNs() == release.getNs())[0];
}
@@ -57,8 +61,10 @@ export class ReleaseStore extends ItemStore {
async loadAll() {
this.isLoading = true;
let items;
+
try {
const { isAdmin, allowedNamespaces } = getHostedCluster();
+
items = await this.loadItems(!isAdmin ? allowedNamespaces : null);
} finally {
if (items) {
@@ -82,19 +88,25 @@ export class ReleaseStore extends ItemStore {
async create(payload: IReleaseCreatePayload) {
const response = await helmReleasesApi.create(payload);
+
if (this.isLoaded) this.loadAll();
+
return response;
}
async update(name: string, namespace: string, payload: IReleaseUpdatePayload) {
const response = await helmReleasesApi.update(name, namespace, payload);
+
if (this.isLoaded) this.loadAll();
+
return response;
}
async rollback(name: string, namespace: string, revision: number) {
const response = await helmReleasesApi.rollback(name, namespace, revision);
+
if (this.isLoaded) this.loadAll();
+
return response;
}
@@ -104,6 +116,7 @@ export class ReleaseStore extends ItemStore {
async removeSelectedItems() {
if (!this.selectedItems.length) return;
+
return Promise.all(this.selectedItems.map(this.remove));
}
}
diff --git a/src/renderer/components/+apps-releases/releases.tsx b/src/renderer/components/+apps-releases/releases.tsx
index 94ca7ce935..bcc18965ac 100644
--- a/src/renderer/components/+apps-releases/releases.tsx
+++ b/src/renderer/components/+apps-releases/releases.tsx
@@ -41,6 +41,7 @@ export class HelmReleases extends Component {
get selectedRelease() {
const { match: { params: { name, namespace } } } = this.props;
+
return releaseStore.items.find(release => {
return release.getName() == name && release.getNs() == namespace;
});
@@ -66,6 +67,7 @@ export class HelmReleases extends Component {
renderRemoveDialogMessage(selectedItems: HelmRelease[]) {
const releaseNames = selectedItems.map(item => item.getName()).join(", ");
+
return (
Remove {releaseNames} ?
@@ -111,6 +113,7 @@ export class HelmReleases extends Component
{
]}
renderTableContents={(release: HelmRelease) => {
const version = release.getVersion();
+
return [
release.getName(),
release.getNs(),
diff --git a/src/renderer/components/+apps/apps.tsx b/src/renderer/components/+apps/apps.tsx
index 537d2c3caf..c863b93aab 100644
--- a/src/renderer/components/+apps/apps.tsx
+++ b/src/renderer/components/+apps/apps.tsx
@@ -10,6 +10,7 @@ import { namespaceStore } from "../+namespaces/namespace.store";
export class Apps extends React.Component {
static get tabRoutes(): TabLayoutRoute[] {
const query = namespaceStore.getContextParams();
+
return [
{
title: Charts ,
diff --git a/src/renderer/components/+cluster-settings/cluster-settings.tsx b/src/renderer/components/+cluster-settings/cluster-settings.tsx
index 6188e8867e..0cde390c47 100644
--- a/src/renderer/components/+cluster-settings/cluster-settings.tsx
+++ b/src/renderer/components/+cluster-settings/cluster-settings.tsx
@@ -49,6 +49,7 @@ export class ClusterSettings extends React.Component {
render() {
const cluster = this.cluster;
+
if (!cluster) return null;
const header = (
<>
@@ -56,6 +57,7 @@ export class ClusterSettings extends React.Component {
{cluster.preferences.clusterName}
>
);
+
return (
diff --git a/src/renderer/components/+cluster-settings/components/cluster-icon-setting.tsx b/src/renderer/components/+cluster-settings/components/cluster-icon-setting.tsx
index bc9d45818a..466afc14da 100644
--- a/src/renderer/components/+cluster-settings/components/cluster-icon-setting.tsx
+++ b/src/renderer/components/+cluster-settings/components/cluster-icon-setting.tsx
@@ -25,9 +25,11 @@ export class ClusterIconSetting extends React.Component {
@autobind()
async onIconPick([file]: File[]) {
const { cluster } = this.props;
+
try {
if (file) {
const buf = Buffer.from(await file.arrayBuffer());
+
cluster.preferences.icon = `data:${file.type};base64,${buf.toString("base64")}`;
} else {
// this has to be done as a seperate branch (and not always) because `cluster`
@@ -57,6 +59,7 @@ export class ClusterIconSetting extends React.Component {
{"Browse for new icon..."}
>
);
+
return (
<>
diff --git a/src/renderer/components/+cluster-settings/components/cluster-prometheus-setting.tsx b/src/renderer/components/+cluster-settings/components/cluster-prometheus-setting.tsx
index 90df9b780c..937f2feed4 100644
--- a/src/renderer/components/+cluster-settings/components/cluster-prometheus-setting.tsx
+++ b/src/renderer/components/+cluster-settings/components/cluster-prometheus-setting.tsx
@@ -23,6 +23,7 @@ export class ClusterPrometheusSetting extends React.Component {
@computed get canEditPrometheusPath() {
if (this.provider === "" || this.provider === "lens") return false;
+
return true;
}
@@ -30,12 +31,15 @@ export class ClusterPrometheusSetting extends React.Component {
disposeOnUnmount(this,
autorun(() => {
const { prometheus, prometheusProvider } = this.props.cluster.preferences;
+
if (prometheus) {
const prefix = prometheus.prefix || "";
+
this.path = `${prometheus.namespace}/${prometheus.service}:${prometheus.port}${prefix}`;
} else {
this.path = "";
}
+
if (prometheusProvider) {
this.provider = prometheusProvider.type;
} else {
@@ -51,9 +55,11 @@ export class ClusterPrometheusSetting extends React.Component {
}
const parsed = this.path.split(/\/|:/, 3);
const apiPrefix = this.path.substring(parsed.join("/").length);
+
if (!parsed[0] || !parsed[1] || !parsed[2]) {
return null;
}
+
return {
namespace: parsed[0],
service: parsed[1],
diff --git a/src/renderer/components/+cluster-settings/components/install-feature.tsx b/src/renderer/components/+cluster-settings/components/install-feature.tsx
index 729dd2b1cd..f47fc0c168 100644
--- a/src/renderer/components/+cluster-settings/components/install-feature.tsx
+++ b/src/renderer/components/+cluster-settings/components/install-feature.tsx
@@ -24,6 +24,7 @@ export class InstallFeature extends React.Component {
const statusUpdate = interval(20, () => {
feature.updateStatus(cluster);
});
+
statusUpdate.start(true);
disposeOnUnmount(this, () => {
@@ -42,6 +43,7 @@ export class InstallFeature extends React.Component {
const { cluster, feature } = this.props;
const disabled = !cluster.isAdmin || this.loading;
const loadingIcon = this.loading ? : null;
+
return (
{feature.status.canUpgrade &&
diff --git a/src/renderer/components/+cluster-settings/components/remove-cluster-button.tsx b/src/renderer/components/+cluster-settings/components/remove-cluster-button.tsx
index 5ae8d068f2..47c2131b17 100644
--- a/src/renderer/components/+cluster-settings/components/remove-cluster-button.tsx
+++ b/src/renderer/components/+cluster-settings/components/remove-cluster-button.tsx
@@ -16,6 +16,7 @@ export class RemoveClusterButton extends React.Component
{
@autobind()
confirmRemoveCluster() {
const { cluster } = this.props;
+
ConfirmDialog.open({
message: Are you sure you want to remove {cluster.preferences.clusterName} from Lens?
,
labelOk: Yes ,
@@ -28,6 +29,7 @@ export class RemoveClusterButton extends React.Component {
render() {
const { cluster } = this.props;
+
return (
Remove Cluster
diff --git a/src/renderer/components/+cluster-settings/features.tsx b/src/renderer/components/+cluster-settings/features.tsx
index d2a0720ecf..d43ecd9568 100644
--- a/src/renderer/components/+cluster-settings/features.tsx
+++ b/src/renderer/components/+cluster-settings/features.tsx
@@ -11,6 +11,7 @@ interface Props {
export class Features extends React.Component {
render() {
const { cluster } = this.props;
+
return (
Features
diff --git a/src/renderer/components/+cluster-settings/status.tsx b/src/renderer/components/+cluster-settings/status.tsx
index 319b42e104..7f21d19aba 100644
--- a/src/renderer/components/+cluster-settings/status.tsx
+++ b/src/renderer/components/+cluster-settings/status.tsx
@@ -14,6 +14,7 @@ export class Status extends React.Component
{
@autobind()
openKubeconfig() {
const { cluster } = this.props;
+
shell.showItemInFolder(cluster.kubeConfigPath);
}
@@ -26,6 +27,7 @@ export class Status extends React.Component {
["API Address", cluster.apiUrl || "N/A"],
["Nodes Count", cluster.metadata.nodes ? String(cluster.metadata.nodes) : "N/A"]
];
+
return (
{rows.map(([name, value]) => {
diff --git a/src/renderer/components/+cluster/cluster-issues.tsx b/src/renderer/components/+cluster/cluster-issues.tsx
index a2959cda20..aef925ef94 100644
--- a/src/renderer/components/+cluster/cluster-issues.tsx
+++ b/src/renderer/components/+cluster/cluster-issues.tsx
@@ -44,6 +44,7 @@ export class ClusterIssues extends React.Component {
// Node bad conditions
nodesStore.items.forEach(node => {
const { kind, selfLink, getId, getName } = node;
+
node.getWarningConditions().forEach(({ message }) => {
warnings.push({
kind,
@@ -57,9 +58,11 @@ export class ClusterIssues extends React.Component {
// Warning events for Workloads
const events = eventStore.getWarnings();
+
events.forEach(error => {
const { message, involvedObject } = error;
const { uid, name, kind } = involvedObject;
+
warnings.push({
getId: () => uid,
getName: () => name,
@@ -77,6 +80,7 @@ export class ClusterIssues extends React.Component {
const { warnings } = this;
const warning = warnings.find(warn => warn.getId() == uid);
const { getId, getName, message, kind, selfLink } = warning;
+
return (
{
renderContent() {
const { warnings } = this;
+
if (!eventStore.isLoaded) {
return (
);
}
+
if (!warnings.length) {
return (
@@ -113,6 +119,7 @@ export class ClusterIssues extends React.Component
{
);
}
+
return (
<>
diff --git a/src/renderer/components/+cluster/cluster-metric-switchers.tsx b/src/renderer/components/+cluster/cluster-metric-switchers.tsx
index 76c9ca6a0c..f2e090cdbb 100644
--- a/src/renderer/components/+cluster/cluster-metric-switchers.tsx
+++ b/src/renderer/components/+cluster/cluster-metric-switchers.tsx
@@ -14,6 +14,7 @@ export const ClusterMetricSwitchers = observer(() => {
const metricsValues = getMetricsValues(metrics);
const disableRoles = !masterNodes.length || !workerNodes.length;
const disableMetrics = !metricsValues.length;
+
return (
diff --git a/src/renderer/components/+cluster/cluster-metrics.tsx b/src/renderer/components/+cluster/cluster-metrics.tsx
index 52e1defc29..b049cfc2f4 100644
--- a/src/renderer/components/+cluster/cluster-metrics.tsx
+++ b/src/renderer/components/+cluster/cluster-metrics.tsx
@@ -42,6 +42,7 @@ export const ClusterMetrics = observer(() => {
callbacks: {
label: ({ index }, data) => {
const value = data.datasets[0].data[index] as ChartPoint;
+
return value.y.toString();
}
}
@@ -60,6 +61,7 @@ export const ClusterMetrics = observer(() => {
callbacks: {
label: ({ index }, data) => {
const value = data.datasets[0].data[index] as ChartPoint;
+
return bytesToUnits(parseInt(value.y as string), 3);
}
}
@@ -71,9 +73,11 @@ export const ClusterMetrics = observer(() => {
if ((!metricValues.length || !liveMetricValues.length) && !metricsLoaded) {
return
;
}
+
if (!memoryCapacity || !cpuCapacity) {
return
;
}
+
return (
{
`${i18n._(t`Capacity`)}: ${podCapacity}`,
]
};
+
return (
@@ -174,6 +175,7 @@ export const ClusterPieCharts = observer(() => {
const { masterNodes, workerNodes } = nodesStore;
const { metricNodeRole, metricsLoaded } = clusterStore;
const nodes = metricNodeRole === MetricNodeRole.MASTER ? masterNodes : workerNodes;
+
if (!nodes.length) {
return (
@@ -182,6 +184,7 @@ export const ClusterPieCharts = observer(() => {
);
}
+
if (!metricsLoaded) {
return (
@@ -190,9 +193,11 @@ export const ClusterPieCharts = observer(() => {
);
}
const { memoryCapacity, cpuCapacity, podCapacity } = getMetricLastPoints(clusterStore.metrics);
+
if (!memoryCapacity || !cpuCapacity || !podCapacity) {
return
;
}
+
return renderCharts();
};
diff --git a/src/renderer/components/+cluster/cluster.store.ts b/src/renderer/components/+cluster/cluster.store.ts
index 052b52fb29..04bc1d8658 100644
--- a/src/renderer/components/+cluster/cluster.store.ts
+++ b/src/renderer/components/+cluster/cluster.store.ts
@@ -32,9 +32,11 @@ export class ClusterStore extends KubeObjectStore
{
// sync user setting with local storage
const storage = createStorage("cluster_metric_switchers", {});
+
Object.assign(this, storage.get());
reaction(() => {
const { metricType, metricNodeRole } = this;
+
return { metricType, metricNodeRole };
},
settings => storage.set(settings)
@@ -52,6 +54,7 @@ export class ClusterStore extends KubeObjectStore {
// check which node type to select
reaction(() => nodesStore.items.length, () => {
const { masterNodes, workerNodes } = nodesStore;
+
if (!masterNodes.length) this.metricNodeRole = MetricNodeRole.WORKER;
if (!workerNodes.length) this.metricNodeRole = MetricNodeRole.MASTER;
});
@@ -61,6 +64,7 @@ export class ClusterStore extends KubeObjectStore {
await when(() => nodesStore.isLoaded);
const { masterNodes, workerNodes } = nodesStore;
const nodes = this.metricNodeRole === MetricNodeRole.MASTER && masterNodes.length ? masterNodes : workerNodes;
+
return clusterApi.getMetrics(nodes.map(node => node.getName()), params);
}
@@ -79,6 +83,7 @@ export class ClusterStore extends KubeObjectStore {
const range = 15;
const end = Date.now() / 1000;
const start = end - range;
+
this.liveMetrics = await this.loadMetrics({ start, end, step, range });
}
diff --git a/src/renderer/components/+cluster/cluster.tsx b/src/renderer/components/+cluster/cluster.tsx
index 588c483295..f99f65c479 100644
--- a/src/renderer/components/+cluster/cluster.tsx
+++ b/src/renderer/components/+cluster/cluster.tsx
@@ -32,6 +32,7 @@ export class Cluster extends React.Component {
// todo: refactor
async componentDidMount() {
const { dependentStores } = this;
+
if (!isAllowedResource("nodes")) {
dependentStores.splice(dependentStores.indexOf(nodesStore), 1);
}
@@ -54,6 +55,7 @@ export class Cluster extends React.Component {
render() {
const { isLoaded } = this;
+
return (
diff --git a/src/renderer/components/+config-autoscalers/hpa-details.tsx b/src/renderer/components/+config-autoscalers/hpa-details.tsx
index 5b4dad1fa3..7cf5e3142f 100644
--- a/src/renderer/components/+config-autoscalers/hpa-details.tsx
+++ b/src/renderer/components/+config-autoscalers/hpa-details.tsx
@@ -28,6 +28,7 @@ export class HpaDetails extends React.Component