From 66d5fdd8b5fa91e526869c355d4f31a62f3e89b1 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Thu, 29 Oct 2020 09:53:25 +0200 Subject: [PATCH 01/29] Optimize ExtensionManager#load (#1171) Signed-off-by: Jari Kolehmainen --- src/extensions/extension-manager.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/extensions/extension-manager.ts b/src/extensions/extension-manager.ts index eb4bad08fb..1a07140aea 100644 --- a/src/extensions/extension-manager.ts +++ b/src/extensions/extension-manager.ts @@ -47,7 +47,9 @@ export class ExtensionManager { async load() { logger.info("[EXTENSION-MANAGER] loading extensions from " + this.extensionPackagesRoot) - if (this.inTreeFolderPath !== this.inTreeTargetPath) { + try { + await fs.access(this.inTreeFolderPath, fs.constants.W_OK) + } catch { // we need to copy in-tree extensions so that we can symlink them properly on "npm install" await fs.remove(this.inTreeTargetPath) await fs.ensureDir(this.inTreeTargetPath) @@ -79,7 +81,7 @@ export class ExtensionManager { protected installPackages(): Promise { return new Promise((resolve, reject) => { - const child = child_process.fork(this.npmPath, ["install", "--silent"], { + const child = child_process.fork(this.npmPath, ["install", "--silent", "--no-audit", "--only=prod", "--prefer-offline"], { cwd: extensionPackagesRoot(), silent: true }) @@ -95,6 +97,7 @@ export class ExtensionManager { async loadExtensions() { const bundledExtensions = await this.loadBundledExtensions() const localExtensions = await this.loadFromFolder(this.localFolderPath) + await this.installPackages() const extensions = bundledExtensions.concat(localExtensions) return new Map(extensions.map(ext => [ext.id, ext])); } @@ -118,7 +121,6 @@ export class ExtensionManager { } logger.debug(`[EXTENSION-MANAGER]: ${extensions.length} extensions loaded`, { folderPath, extensions }); await fs.writeFile(path.join(this.extensionPackagesRoot, "package.json"), JSON.stringify(this.packagesJson), {mode: 0o600}) - await this.installPackages() return extensions } @@ -141,7 +143,6 @@ export class ExtensionManager { logger.debug(`[EXTENSION-MANAGER]: ${extensions.length} extensions loaded`, { folderPath, extensions }); await fs.writeFile(path.join(this.extensionPackagesRoot, "package.json"), JSON.stringify(this.packagesJson), {mode: 0o600}) - await this.installPackages() return extensions; } From a583dbe6d3c504b4858880e7d92fd69800ace30f Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Thu, 29 Oct 2020 11:23:54 +0300 Subject: [PATCH 02/29] Fix: path checks in custom extension loader (#1170) * Check for path existence and directory type Signed-off-by: Alex Andreev * Check for package.json availability Signed-off-by: Alex Andreev --- src/extensions/extension-manager.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/extensions/extension-manager.ts b/src/extensions/extension-manager.ts index 1a07140aea..be1cf322a4 100644 --- a/src/extensions/extension-manager.ts +++ b/src/extensions/extension-manager.ts @@ -63,6 +63,7 @@ export class ExtensionManager { async getExtensionByManifest(manifestPath: string): Promise { let manifestJson: ExtensionManifest; try { + fs.accessSync(manifestPath, fs.constants.F_OK); // check manifest file for existence manifestJson = __non_webpack_require__(manifestPath) this.packagesJson.dependencies[manifestJson.name] = path.dirname(manifestPath) @@ -113,7 +114,6 @@ export class ExtensionManager { } const absPath = path.resolve(folderPath, fileName); const manifestPath = path.resolve(absPath, "package.json"); - await fs.access(manifestPath, fs.constants.F_OK) const ext = await this.getExtensionByManifest(manifestPath).catch(() => null) if (ext) { extensions.push(ext) @@ -133,8 +133,10 @@ export class ExtensionManager { continue } const absPath = path.resolve(folderPath, fileName); + if (!fs.existsSync(absPath) || !fs.lstatSync(absPath).isDirectory()) { // skip non-directories + continue; + } const manifestPath = path.resolve(absPath, "package.json"); - await fs.access(manifestPath, fs.constants.F_OK) const ext = await this.getExtensionByManifest(manifestPath).catch(() => null) if (ext) { extensions.push(ext) From e4c56512e7e596efd6887031ca1d22619ae48e0a Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Thu, 29 Oct 2020 12:06:37 +0200 Subject: [PATCH 03/29] Fix regression caused by #1171 (#1172) Signed-off-by: Jari Kolehmainen --- src/extensions/extension-manager.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/extensions/extension-manager.ts b/src/extensions/extension-manager.ts index be1cf322a4..89879be602 100644 --- a/src/extensions/extension-manager.ts +++ b/src/extensions/extension-manager.ts @@ -17,6 +17,8 @@ type PackageJson = { export class ExtensionManager { + protected bundledFolderPath: string + protected packagesJson: PackageJson = { dependencies: {} } @@ -45,15 +47,24 @@ export class ExtensionManager { return __non_webpack_require__.resolve('npm/bin/npm-cli') } + get packageJsonPath() { + return path.join(this.extensionPackagesRoot, "package.json") + } + async load() { logger.info("[EXTENSION-MANAGER] loading extensions from " + this.extensionPackagesRoot) + if (fs.existsSync(path.join(this.extensionPackagesRoot, "package-lock.json"))) { + await fs.remove(path.join(this.extensionPackagesRoot, "package-lock.json")) + } try { await fs.access(this.inTreeFolderPath, fs.constants.W_OK) + this.bundledFolderPath = this.inTreeFolderPath } catch { // we need to copy in-tree extensions so that we can symlink them properly on "npm install" await fs.remove(this.inTreeTargetPath) await fs.ensureDir(this.inTreeTargetPath) await fs.copy(this.inTreeFolderPath, this.inTreeTargetPath) + this.bundledFolderPath = this.inTreeTargetPath } await fs.ensureDir(this.nodeModulesPath) await fs.ensureDir(this.localFolderPath) @@ -82,7 +93,7 @@ export class ExtensionManager { protected installPackages(): Promise { return new Promise((resolve, reject) => { - const child = child_process.fork(this.npmPath, ["install", "--silent", "--no-audit", "--only=prod", "--prefer-offline"], { + const child = child_process.fork(this.npmPath, ["install", "--silent", "--no-audit", "--only=prod", "--prefer-offline", "--no-package-lock"], { cwd: extensionPackagesRoot(), silent: true }) @@ -98,6 +109,7 @@ export class ExtensionManager { async loadExtensions() { const bundledExtensions = await this.loadBundledExtensions() const localExtensions = await this.loadFromFolder(this.localFolderPath) + await fs.writeFile(path.join(this.packageJsonPath), JSON.stringify(this.packagesJson, null, 2), {mode: 0o600}) await this.installPackages() const extensions = bundledExtensions.concat(localExtensions) return new Map(extensions.map(ext => [ext.id, ext])); @@ -105,7 +117,7 @@ export class ExtensionManager { async loadBundledExtensions() { const extensions: InstalledExtension[] = [] - const folderPath = this.inTreeTargetPath + const folderPath = this.bundledFolderPath const bundledExtensions = getBundledExtensions() const paths = await fs.readdir(folderPath); for (const fileName of paths) { @@ -120,7 +132,6 @@ export class ExtensionManager { } } logger.debug(`[EXTENSION-MANAGER]: ${extensions.length} extensions loaded`, { folderPath, extensions }); - await fs.writeFile(path.join(this.extensionPackagesRoot, "package.json"), JSON.stringify(this.packagesJson), {mode: 0o600}) return extensions } @@ -144,8 +155,6 @@ export class ExtensionManager { } logger.debug(`[EXTENSION-MANAGER]: ${extensions.length} extensions loaded`, { folderPath, extensions }); - await fs.writeFile(path.join(this.extensionPackagesRoot, "package.json"), JSON.stringify(this.packagesJson), {mode: 0o600}) - return extensions; } } From 4a6553967d78db2c34e9fa9a56bd900750cf6b1e Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Fri, 30 Oct 2020 09:45:09 +0200 Subject: [PATCH 04/29] Improve how extensions can manage cluster/workspace stores (#1176) Signed-off-by: Jari Kolehmainen --- src/common/__tests__/cluster-store.test.ts | 4 +- src/common/__tests__/workspace-store.test.ts | 44 +++---- src/common/cluster-store.ts | 83 ++++++++++--- src/common/workspace-store.ts | 115 ++++++++++++++++-- src/extensions/core-api/stores.ts | 4 +- src/main/cluster-manager.ts | 2 +- src/main/cluster.ts | 44 ++++--- src/main/tray.ts | 2 +- src/renderer/bootstrap.tsx | 1 + .../components/+add-cluster/add-cluster.tsx | 2 +- .../components/cluster-workspace-setting.tsx | 4 +- .../components/remove-cluster-button.tsx | 7 +- .../components/+workspaces/workspace-menu.tsx | 4 +- .../components/+workspaces/workspaces.tsx | 21 ++-- .../cluster-manager/clusters-menu.tsx | 14 ++- 15 files changed, 256 insertions(+), 95 deletions(-) diff --git a/src/common/__tests__/cluster-store.test.ts b/src/common/__tests__/cluster-store.test.ts index 724a83f0d7..8c911298cc 100644 --- a/src/common/__tests__/cluster-store.test.ts +++ b/src/common/__tests__/cluster-store.test.ts @@ -64,13 +64,13 @@ describe("empty config", () => { it("sets active cluster", () => { clusterStore.setActive("foo"); - expect(clusterStore.activeCluster.id).toBe("foo"); + expect(clusterStore.active.id).toBe("foo"); }) }) describe("with prod and dev clusters added", () => { beforeEach(() => { - clusterStore.addCluster( + clusterStore.addClusters( new Cluster({ id: "prod", contextName: "prod", diff --git a/src/common/__tests__/workspace-store.test.ts b/src/common/__tests__/workspace-store.test.ts index 8ac3ac599d..97edfa77cf 100644 --- a/src/common/__tests__/workspace-store.test.ts +++ b/src/common/__tests__/workspace-store.test.ts @@ -10,7 +10,7 @@ jest.mock("electron", () => { } }) -import { WorkspaceStore } from "../workspace-store" +import { Workspace, WorkspaceStore } from "../workspace-store" describe("workspace store tests", () => { describe("for an empty config", () => { @@ -35,16 +35,16 @@ describe("workspace store tests", () => { it("cannot remove the default workspace", () => { const ws = WorkspaceStore.getInstance(); - expect(() => ws.removeWorkspace(WorkspaceStore.defaultId)).toThrowError("Cannot remove"); + expect(() => ws.removeWorkspaceById(WorkspaceStore.defaultId)).toThrowError("Cannot remove"); }) it("can update default workspace name", () => { const ws = WorkspaceStore.getInstance(); - ws.saveWorkspace({ + ws.addWorkspace(new Workspace({ id: WorkspaceStore.defaultId, name: "foobar", - }); + })); expect(ws.currentWorkspace.name).toBe("foobar"); }) @@ -52,10 +52,10 @@ describe("workspace store tests", () => { it("can add workspaces", () => { const ws = WorkspaceStore.getInstance(); - ws.saveWorkspace({ + ws.addWorkspace(new Workspace({ id: "123", name: "foobar", - }); + })); expect(ws.getById("123").name).toBe("foobar"); }) @@ -69,10 +69,10 @@ describe("workspace store tests", () => { it("can set a existent workspace to be active", () => { const ws = WorkspaceStore.getInstance(); - ws.saveWorkspace({ + ws.addWorkspace(new Workspace({ id: "abc", name: "foobar", - }); + })); expect(() => ws.setActive("abc")).not.toThrowError(); }) @@ -80,15 +80,15 @@ describe("workspace store tests", () => { it("can remove a workspace", () => { const ws = WorkspaceStore.getInstance(); - ws.saveWorkspace({ + ws.addWorkspace(new Workspace({ id: "123", name: "foobar", - }); - ws.saveWorkspace({ + })); + ws.addWorkspace(new Workspace({ id: "1234", name: "foobar 1", - }); - ws.removeWorkspace("123"); + })); + ws.removeWorkspaceById("123"); expect(ws.workspaces.size).toBe(2); }) @@ -96,10 +96,10 @@ describe("workspace store tests", () => { it("cannot create workspace with existent name", () => { const ws = WorkspaceStore.getInstance(); - ws.saveWorkspace({ + ws.addWorkspace(new Workspace({ id: "someid", name: "default", - }); + })); expect(ws.workspacesList.length).toBe(1); // default workspace only }) @@ -107,10 +107,10 @@ describe("workspace store tests", () => { it("cannot create workspace with empty name", () => { const ws = WorkspaceStore.getInstance(); - ws.saveWorkspace({ + ws.addWorkspace(new Workspace({ id: "random", name: "", - }); + })); expect(ws.workspacesList.length).toBe(1); // default workspace only }) @@ -118,10 +118,10 @@ describe("workspace store tests", () => { it("cannot create workspace with ' ' name", () => { const ws = WorkspaceStore.getInstance(); - ws.saveWorkspace({ + ws.addWorkspace(new Workspace({ id: "random", name: " ", - }); + })); expect(ws.workspacesList.length).toBe(1); // default workspace only }) @@ -129,10 +129,10 @@ describe("workspace store tests", () => { it("trim workspace name", () => { const ws = WorkspaceStore.getInstance(); - ws.saveWorkspace({ + ws.addWorkspace(new Workspace({ id: "random", name: "default ", - }); + })); expect(ws.workspacesList.length).toBe(1); // default workspace only }) @@ -169,4 +169,4 @@ describe("workspace store tests", () => { expect(ws.currentWorkspaceId).toBe("abc"); }) }) -}) \ No newline at end of file +}) diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index fddd0f3be6..838e6cc119 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store.ts @@ -33,11 +33,12 @@ export type ClusterId = string; export interface ClusterModel { id: ClusterId; + kubeConfigPath: string; workspace?: WorkspaceId; contextName?: string; preferences?: ClusterPreferences; metadata?: ClusterMetadata; - kubeConfigPath: string; + ownerRef?: string; /** @deprecated */ kubeConfig?: string; // yaml @@ -72,25 +73,34 @@ export class ClusterStore extends BaseStore { return filePath; } + @observable activeCluster: ClusterId; + @observable removedClusters = observable.map(); + @observable clusters = observable.map(); + private constructor() { super({ configName: "lens-cluster-store", accessPropertiesByDotNotation: false, // To make dots safe in cluster context names migrations: migrations, }); + + this.pushStateToViewsPeriodically() } - @observable activeClusterId: ClusterId; - @observable removedClusters = observable.map(); - @observable clusters = observable.map(); + protected pushStateToViewsPeriodically() { + if (!ipcRenderer) { + // This is a bit of a hack, we need to do this because we might loose messages that are sent before a view is ready + setInterval(() => { + this.pushState() + }, 5000) + } + } registerIpcListener() { logger.info(`[CLUSTER-STORE] start to listen (${webFrame.routingId})`) - ipcRenderer.on("cluster:state", (event, model: ClusterState) => { - this.applyWithoutSync(() => { - logger.silly(`[CLUSTER-STORE]: received push-state at ${location.host} (${webFrame.routingId})`, model); - this.getById(model.id)?.updateModel(model); - }) + ipcRenderer.on("cluster:state", (event, clusterId: string, state: ClusterState) => { + logger.silly(`[CLUSTER-STORE]: received push-state at ${location.host} (${webFrame.routingId})`, clusterId, state); + this.getById(clusterId)?.setState(state) }) } @@ -99,21 +109,35 @@ export class ClusterStore extends BaseStore { ipcRenderer.removeAllListeners("cluster:state") } - @computed get activeCluster(): Cluster | null { - return this.getById(this.activeClusterId); + pushState() { + this.clusters.forEach((c) => { + c.pushState() + }) + } + + get activeClusterId() { + return this.activeCluster } @computed get clustersList(): Cluster[] { return Array.from(this.clusters.values()); } + @computed get enabledClustersList(): Cluster[] { + return this.clustersList.filter((c) => c.enabled) + } + + @computed get active(): Cluster | null { + return this.getById(this.activeCluster); + } + isActive(id: ClusterId) { - return this.activeClusterId === id; + return this.activeCluster === id; } @action setActive(id: ClusterId) { - this.activeClusterId = this.clusters.has(id) ? id : null; + this.activeCluster = this.clusters.has(id) ? id : null; } @action @@ -145,12 +169,28 @@ export class ClusterStore extends BaseStore { } @action - addCluster(...models: ClusterModel[]) { + addClusters(...models: ClusterModel[]): Cluster[] { + const clusters: Cluster[] = [] models.forEach(model => { - appEventBus.emit({name: "cluster", action: "add"}) - const cluster = new Cluster(model); - this.clusters.set(model.id, cluster); + clusters.push(this.addCluster(model)) }) + + return clusters + } + + @action + addCluster(model: ClusterModel | Cluster ): Cluster { + appEventBus.emit({name: "cluster", action: "add"}) + let cluster = model as Cluster; + if (!(model instanceof Cluster)) { + cluster = new Cluster(model) + } + this.clusters.set(model.id, cluster); + return cluster + } + + async removeCluster(model: ClusterModel) { + await this.removeById(model.id) } @action @@ -159,7 +199,7 @@ export class ClusterStore extends BaseStore { const cluster = this.getById(clusterId); if (cluster) { this.clusters.delete(clusterId); - if (this.activeClusterId === clusterId) { + if (this.activeCluster === clusterId) { this.setActive(null); } // remove only custom kubeconfigs (pasted as text) @@ -189,6 +229,9 @@ export class ClusterStore extends BaseStore { cluster.updateModel(clusterModel); } else { cluster = new Cluster(clusterModel); + if (!cluster.isManaged) { + cluster.enabled = true + } } newClusters.set(clusterModel.id, cluster); } @@ -200,14 +243,14 @@ export class ClusterStore extends BaseStore { } }); - this.activeClusterId = newClusters.has(activeCluster) ? activeCluster : null; + this.activeCluster = newClusters.has(activeCluster) ? activeCluster : null; this.clusters.replace(newClusters); this.removedClusters.replace(removedClusters); } toJSON(): ClusterStoreModel { return toJS({ - activeCluster: this.activeClusterId, + activeCluster: this.activeCluster, clusters: this.clustersList.map(cluster => cluster.toJSON()), }, { recurseEverything: true diff --git a/src/common/workspace-store.ts b/src/common/workspace-store.ts index b7f2467013..97611a01d3 100644 --- a/src/common/workspace-store.ts +++ b/src/common/workspace-store.ts @@ -1,19 +1,77 @@ -import { action, computed, observable, toJS } from "mobx"; +import { ipcRenderer } from "electron"; +import { action, computed, observable, toJS, reaction } from "mobx"; import { BaseStore } from "./base-store"; import { clusterStore } from "./cluster-store" import { appEventBus } from "./event-bus"; +import { broadcastIpc } from "../common/ipc"; +import logger from "../main/logger"; export type WorkspaceId = string; export interface WorkspaceStoreModel { currentWorkspace?: WorkspaceId; - workspaces: Workspace[] + workspaces: WorkspaceModel[] } -export interface Workspace { +export interface WorkspaceModel { id: WorkspaceId; name: string; description?: string; + ownerRef?: string; +} + +export interface WorkspaceState { + enabled: boolean; +} + +export class Workspace implements WorkspaceModel, WorkspaceState { + @observable id: WorkspaceId + @observable name: string + @observable description?: string + @observable ownerRef?: string + @observable enabled: boolean + + constructor(data: WorkspaceModel) { + Object.assign(this, data) + + if (!ipcRenderer) { + reaction(() => this.getState(), () => { + this.pushState() + }) + } + } + + get isManaged(): boolean { + return !!this.ownerRef + } + + getState(): WorkspaceState { + return { + enabled: this.enabled + } + } + + pushState(state = this.getState()) { + logger.silly("[WORKSPACE] pushing state", {...state, id: this.id}) + broadcastIpc({ + channel: "workspace:state", + args: [this.id, toJS(state)], + }); + } + + @action + setState(state: WorkspaceState) { + Object.assign(this, state) + } + + toJSON(): WorkspaceModel { + return toJS({ + id: this.id, + name: this.name, + description: this.description, + ownerRef: this.ownerRef + }) + } } export class WorkspaceStore extends BaseStore { @@ -23,15 +81,33 @@ export class WorkspaceStore extends BaseStore { super({ configName: "lens-workspace-store", }); + + if (!ipcRenderer) { + setInterval(() => { + this.pushState() + }, 5000) + } + } + + registerIpcListener() { + logger.info("[WORKSPACE-STORE] starting to listen state events") + ipcRenderer.on("workspace:state", (event, workspaceId: string, state: WorkspaceState) => { + this.getById(workspaceId)?.setState(state) + }) + } + + unregisterIpcListener() { + super.unregisterIpcListener() + ipcRenderer.removeAllListeners("workspace:state") } @observable currentWorkspaceId = WorkspaceStore.defaultId; @observable workspaces = observable.map({ - [WorkspaceStore.defaultId]: { + [WorkspaceStore.defaultId]: new Workspace({ id: WorkspaceStore.defaultId, name: "default" - } + }) }); @computed get currentWorkspace(): Workspace { @@ -42,6 +118,16 @@ export class WorkspaceStore extends BaseStore { return Array.from(this.workspaces.values()); } + @computed get enabledWorkspacesList() { + return this.workspacesList.filter((w) => w.enabled); + } + + pushState() { + this.workspaces.forEach((w) => { + w.pushState() + }) + } + isDefault(id: WorkspaceId) { return id === WorkspaceStore.defaultId; } @@ -61,11 +147,11 @@ export class WorkspaceStore extends BaseStore { throw new Error(`workspace ${id} doesn't exist`); } this.currentWorkspaceId = id; - clusterStore.activeClusterId = null; // fixme: handle previously selected cluster from current workspace + clusterStore.activeCluster = null; // fixme: handle previously selected cluster from current workspace } @action - saveWorkspace(workspace: Workspace) { + addWorkspace(workspace: Workspace) { const { id, name } = workspace; const existingWorkspace = this.getById(id); if (!name.trim() || this.getByName(name.trim())) { @@ -82,7 +168,12 @@ export class WorkspaceStore extends BaseStore { } @action - removeWorkspace(id: WorkspaceId) { + removeWorkspace(workspace: Workspace) { + this.removeWorkspaceById(workspace.id) + } + + @action + removeWorkspaceById(id: WorkspaceId) { const workspace = this.getById(id); if (!workspace) return; if (this.isDefault(id)) { @@ -103,7 +194,11 @@ export class WorkspaceStore extends BaseStore { } if (workspaces.length) { this.workspaces.clear(); - workspaces.forEach(workspace => { + workspaces.forEach(ws => { + const workspace = new Workspace(ws) + if (!workspace.isManaged) { + workspace.enabled = true + } this.workspaces.set(workspace.id, workspace) }) } @@ -112,7 +207,7 @@ export class WorkspaceStore extends BaseStore { toJSON(): WorkspaceStoreModel { return toJS({ currentWorkspace: this.currentWorkspaceId, - workspaces: this.workspacesList, + workspaces: this.workspacesList.map((w) => w.toJSON()), }, { recurseEverything: true }) diff --git a/src/extensions/core-api/stores.ts b/src/extensions/core-api/stores.ts index dfaae325af..d39314f762 100644 --- a/src/extensions/core-api/stores.ts +++ b/src/extensions/core-api/stores.ts @@ -1,4 +1,4 @@ export { ExtensionStore } from "../extension-store" export { clusterStore, ClusterModel } from "../../common/cluster-store" -export { workspaceStore} from "../../common/workspace-store" -export type { Cluster } from "../../main/cluster" +export { Cluster } from "../../main/cluster" +export { workspaceStore, Workspace, WorkspaceModel } from "../../common/workspace-store" diff --git a/src/main/cluster-manager.ts b/src/main/cluster-manager.ts index 21556180b4..0620356e9a 100644 --- a/src/main/cluster-manager.ts +++ b/src/main/cluster-manager.ts @@ -10,7 +10,7 @@ export class ClusterManager { constructor(public readonly port: number) { // auto-init clusters autorun(() => { - clusterStore.clusters.forEach(cluster => { + clusterStore.enabledClustersList.forEach(cluster => { if (!cluster.initialized) { logger.info(`[CLUSTER-MANAGER]: init cluster`, cluster.getMeta()); cluster.init(port); diff --git a/src/main/cluster.ts b/src/main/cluster.ts index 11964ba319..57b9e219c3 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -1,3 +1,4 @@ +import { ipcMain } from "electron" import type { ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences } from "../common/cluster-store" import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api"; import type { WorkspaceId } from "../common/workspace-store"; @@ -33,7 +34,7 @@ export type ClusterRefreshOptions = { refreshMetadata?: boolean } -export interface ClusterState extends ClusterModel { +export interface ClusterState { initialized: boolean; apiUrl: string; online: boolean; @@ -47,11 +48,12 @@ export interface ClusterState extends ClusterModel { allowedResources: string[] } -export class Cluster implements ClusterModel { +export class Cluster implements ClusterModel, ClusterState { public id: ClusterId; public frameId: number; public kubeCtl: Kubectl public contextHandler: ContextHandler; + public ownerRef: string; protected kubeconfigManager: KubeconfigManager; protected eventDisposers: Function[] = []; protected activated = false; @@ -65,6 +67,7 @@ export class Cluster implements ClusterModel { @observable kubeConfigPath: string; @observable apiUrl: string; // cluster server url @observable kubeProxyUrl: string; // lens-proxy to kube-api url + @observable enabled = false; @observable online = false; @observable accessible = false; @observable ready = false; @@ -81,6 +84,7 @@ export class Cluster implements ClusterModel { @computed get available() { return this.accessible && !this.disconnected; } + get version(): string { return String(this.metadata?.version) || "" } @@ -93,6 +97,10 @@ export class Cluster implements ClusterModel { } } + get isManaged(): boolean { + return !!this.ownerRef + } + @action updateModel(model: ClusterModel) { Object.assign(this, model); @@ -123,13 +131,15 @@ export class Cluster implements ClusterModel { const refreshTimer = setInterval(() => !this.disconnected && this.refresh(), 30000); // every 30s const refreshMetadataTimer = setInterval(() => !this.disconnected && this.refreshMetadata(), 900000); // every 15 minutes - this.eventDisposers.push( - reaction(this.getState, this.pushState), - () => { - clearInterval(refreshTimer); - clearInterval(refreshMetadataTimer); - }, - ); + if (ipcMain) { + this.eventDisposers.push( + reaction(() => this.getState(), () => this.pushState()), + () => { + clearInterval(refreshTimer); + clearInterval(refreshMetadataTimer); + }, + ); + } } protected unbindEvents() { @@ -361,6 +371,7 @@ export class Cluster implements ClusterModel { workspace: this.workspace, preferences: this.preferences, metadata: this.metadata, + ownerRef: this.ownerRef }; return toJS(model, { recurseEverything: true @@ -368,9 +379,8 @@ export class Cluster implements ClusterModel { } // serializable cluster-state used for sync btw main <-> renderer - getState = (): ClusterState => { + getState(): ClusterState { const state: ClusterState = { - ...this.toJSON(), initialized: this.initialized, apiUrl: this.apiUrl, online: this.online, @@ -388,14 +398,18 @@ export class Cluster implements ClusterModel { }) } - pushState = (state = this.getState()): ClusterState => { + @action + setState(state: ClusterState) { + Object.assign(this, state) + } + + pushState(state = this.getState()) { logger.silly(`[CLUSTER]: push-state`, state); broadcastIpc({ channel: "cluster:state", frameId: this.frameId, - args: [state], - }); - return state; + args: [this.id, state], + }) } // get cluster system meta, e.g. use in "logger" diff --git a/src/main/tray.ts b/src/main/tray.ts index 31cc99b314..428fd5cb3d 100644 --- a/src/main/tray.ts +++ b/src/main/tray.ts @@ -80,7 +80,7 @@ export function createTrayMenu(windowManager: WindowManager): Menu { }, { label: "Clusters", - submenu: workspaceStore.workspacesList + submenu: workspaceStore.enabledWorkspacesList .filter(workspace => clusterStore.getByWorkspaceId(workspace.id).length > 0) // hide empty workspaces .map(workspace => { const clusters = clusterStore.getByWorkspaceId(workspace.id); diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index 5d92dd624d..7311e0e0cf 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -40,6 +40,7 @@ export async function bootstrap(App: AppComponent) { // Register additional store listeners clusterStore.registerIpcListener(); + workspaceStore.registerIpcListener(); // init app's dependencies if any if (App.init) { diff --git a/src/renderer/components/+add-cluster/add-cluster.tsx b/src/renderer/components/+add-cluster/add-cluster.tsx index 0751dd2d8c..8acd3a51ea 100644 --- a/src/renderer/components/+add-cluster/add-cluster.tsx +++ b/src/renderer/components/+add-cluster/add-cluster.tsx @@ -163,7 +163,7 @@ export class AddCluster extends React.Component { }) runInAction(() => { - clusterStore.addCluster(...newClusters); + clusterStore.addClusters(...newClusters); if (newClusters.length === 1) { const clusterId = newClusters[0].id; clusterStore.setActive(clusterId); diff --git a/src/renderer/components/+cluster-settings/components/cluster-workspace-setting.tsx b/src/renderer/components/+cluster-settings/components/cluster-workspace-setting.tsx index 6cd933ca11..ea4ee5a571 100644 --- a/src/renderer/components/+cluster-settings/components/cluster-workspace-setting.tsx +++ b/src/renderer/components/+cluster-settings/components/cluster-workspace-setting.tsx @@ -26,11 +26,11 @@ export class ClusterWorkspaceSetting extends React.Component { this.search = value} + /> +
+ {this.renderExtensions()} +
+ + + ); + } +} \ No newline at end of file diff --git a/src/renderer/components/+extensions/index.ts b/src/renderer/components/+extensions/index.ts new file mode 100644 index 0000000000..8946a5f6fe --- /dev/null +++ b/src/renderer/components/+extensions/index.ts @@ -0,0 +1,2 @@ +export * from "./extensions.route" +export * from "./extensions" diff --git a/src/renderer/components/cluster-manager/cluster-manager.tsx b/src/renderer/components/cluster-manager/cluster-manager.tsx index 785fe56e27..4a42a0419d 100644 --- a/src/renderer/components/cluster-manager/cluster-manager.tsx +++ b/src/renderer/components/cluster-manager/cluster-manager.tsx @@ -16,6 +16,7 @@ import { clusterViewRoute, clusterViewURL } from "./cluster-view.route"; import { clusterStore } from "../../../common/cluster-store"; import { hasLoadedView, initView, lensViews, refreshViews } from "./lens-views"; import { globalPageRegistry } from "../../../extensions/registries/page-registry"; +import { Extensions, extensionsRoute } from "../+extensions"; import { getMatchedClusterId } from "../../navigation"; @observer @@ -63,6 +64,7 @@ export class ClusterManager extends React.Component { + diff --git a/src/renderer/components/layout/page-layout.scss b/src/renderer/components/layout/page-layout.scss index 53268f948f..0ae6f54139 100644 --- a/src/renderer/components/layout/page-layout.scss +++ b/src/renderer/components/layout/page-layout.scss @@ -1,5 +1,8 @@ .PageLayout { $spacing: $padding * 2; + --width: 60%; + --max-width: 1000px; + --min-width: 570px; position: relative; height: 100%; @@ -26,12 +29,15 @@ > .content-wrapper { @include custom-scrollbar-themed; padding: $spacing * 2; + display: flex; + flex-direction: column; > .content { + flex: 1; margin: 0 auto; - width: 60%; - min-width: 570px; - max-width: 1000px; + width: var(--width); + min-width: var(--min-width); + max-width: var(--max-width); } } diff --git a/src/renderer/components/tooltip/tooltip.tsx b/src/renderer/components/tooltip/tooltip.tsx index 393651001f..409d5f2bd7 100644 --- a/src/renderer/components/tooltip/tooltip.tsx +++ b/src/renderer/components/tooltip/tooltip.tsx @@ -167,7 +167,6 @@ export class Tooltip extends React.Component { top = topCenter; break; case "top_right": - default: left = targetBounds.right - tooltipBounds.width; top = topCenter; break; From d074e0499ffb3e6ce42e5de14a1b7adeb0fac90d Mon Sep 17 00:00:00 2001 From: pashevskii <53330707+pashevskii@users.noreply.github.com> Date: Thu, 5 Nov 2020 11:23:14 +0400 Subject: [PATCH 29/29] Restart deployment (#1175) Signed-off-by: Pavel Ashevskii --- locales/en/messages.po | 8 ++++ locales/fi/messages.po | 8 ++++ locales/ru/messages.po | 8 ++++ src/renderer/api/endpoints/deployment.api.ts | 22 +++++++++++ src/renderer/api/json-api.ts | 2 +- .../+workloads-deployments/deployments.tsx | 38 ++++++++++++++++--- 6 files changed, 79 insertions(+), 7 deletions(-) diff --git a/locales/en/messages.po b/locales/en/messages.po index c062c63db1..220b5c93a8 100644 --- a/locales/en/messages.po +++ b/locales/en/messages.po @@ -549,6 +549,14 @@ msgstr "Condition" msgid "Conditions" msgstr "Conditions" +#: src/renderer/components/+workloads-deployments/deployments.tsx: 118 +msgid "Restart" +msgstr "Restart" + +#: src/renderer/components/+workloads-deployments/deployments.tsx: 121 +msgid "Are you sure you want to restart deployment <0>{0}?" +msgstr "Are you sure you want to restart deployment <0>{0}?" + #: src/renderer/components/+config-maps/config-maps.tsx:33 msgid "Config Maps" msgstr "Config Maps" diff --git a/locales/fi/messages.po b/locales/fi/messages.po index ee19bf5187..ac1c8902a0 100644 --- a/locales/fi/messages.po +++ b/locales/fi/messages.po @@ -545,6 +545,14 @@ msgstr "" msgid "Conditions" msgstr "" +#: src/renderer/components/+workloads-deployments/deployments.tsx: 118 +msgid "Restart" +msgstr "" + +#: src/renderer/components/+workloads-deployments/deployments.tsx: 121 +msgid "Are you sure you want to restart deployment <0>{0}?" +msgstr "" + #: src/renderer/components/+config-maps/config-maps.tsx:33 msgid "Config Maps" msgstr "" diff --git a/locales/ru/messages.po b/locales/ru/messages.po index 01b8d777a3..9b4fbc99ba 100644 --- a/locales/ru/messages.po +++ b/locales/ru/messages.po @@ -550,6 +550,14 @@ msgstr "Состояние" msgid "Conditions" msgstr "Состояния" +#: src/renderer/components/+workloads-deployments/deployments.tsx: 118 +msgid "Restart" +msgstr "Перезагрузка" + +#: src/renderer/components/+workloads-deployments/deployments.tsx: 121 +msgid "Are you sure you want to restart deployment <0>{0}?" +msgstr "Выполнить перезагрузку деплоймента <0>{0}?" + #: src/renderer/components/+config-maps/config-maps.tsx:33 msgid "Config Maps" msgstr "" diff --git a/src/renderer/api/endpoints/deployment.api.ts b/src/renderer/api/endpoints/deployment.api.ts index 25164e10f9..b21495ecc1 100644 --- a/src/renderer/api/endpoints/deployment.api.ts +++ b/src/renderer/api/endpoints/deployment.api.ts @@ -1,3 +1,5 @@ +import moment from "moment"; + import { IAffinity, WorkloadKubeObject } from "../workload-kube-object"; import { autobind } from "../../utils"; import { KubeApi } from "../kube-api"; @@ -23,6 +25,25 @@ export class DeploymentApi extends KubeApi { } }) } + + restart(params: { namespace: string; name: string }) { + return this.request.patch(this.getUrl(params), { + data: { + spec: { + template: { + metadata: { + annotations: {"kubectl.kubernetes.io/restartedAt" : moment.utc().format()} + } + } + } + } + }, + { + headers: { + 'content-type': 'application/strategic-merge-patch+json' + } + }) + } } @autobind() @@ -38,6 +59,7 @@ export class Deployment extends WorkloadKubeObject { metadata: { creationTimestamp?: string; labels: { [app: string]: string }; + annotations?: { [app: string]: string }; }; spec: { containers: { diff --git a/src/renderer/api/json-api.ts b/src/renderer/api/json-api.ts index 027c201175..e42996c041 100644 --- a/src/renderer/api/json-api.ts +++ b/src/renderer/api/json-api.ts @@ -64,7 +64,7 @@ export class JsonApi { } patch(path: string, params?: P, reqInit: RequestInit = {}) { - return this.request(path, params, { ...reqInit, method: "patch" }); + return this.request(path, params, { ...reqInit, method: "PATCH" }); } del(path: string, params?: P, reqInit: RequestInit = {}) { diff --git a/src/renderer/components/+workloads-deployments/deployments.tsx b/src/renderer/components/+workloads-deployments/deployments.tsx index a7b9a01ab4..39047bf62a 100644 --- a/src/renderer/components/+workloads-deployments/deployments.tsx +++ b/src/renderer/components/+workloads-deployments/deployments.tsx @@ -4,11 +4,12 @@ import React from "react"; import { observer } from "mobx-react"; import { RouteComponentProps } from "react-router"; import { t, Trans } from "@lingui/macro"; -import { Deployment } from "../../api/endpoints"; +import { Deployment, deploymentApi } from "../../api/endpoints"; import { KubeObjectMenuProps } from "../kube-object/kube-object-menu"; import { MenuItem } from "../menu"; import { Icon } from "../icon"; import { DeploymentScaleDialog } from "./deployment-scale-dialog"; +import { ConfirmDialog } from "../confirm-dialog"; import { deploymentStore } from "./deployments.store"; import { replicaSetStore } from "../+workloads-replicasets/replicasets.store"; import { podsStore } from "../+workloads-pods/pods.store"; @@ -22,6 +23,8 @@ import kebabCase from "lodash/kebabCase"; import orderBy from "lodash/orderBy"; import { KubeEventIcon } from "../+events/kube-event-icon"; import { kubeObjectMenuRegistry } from "../../../extensions/registries/kube-object-menu-registry"; +import { apiManager } from "../../api/api-manager"; +import { Notifications } from "../notifications"; enum sortBy { name = "name", @@ -96,10 +99,34 @@ export class Deployments extends React.Component { export function DeploymentMenu(props: KubeObjectMenuProps) { const { object, toolbar } = props; return ( - DeploymentScaleDialog.open(object)}> - - Scale - + <> + DeploymentScaleDialog.open(object)}> + + Scale + + ConfirmDialog.open({ + ok: async () => + { + try { + await deploymentApi.restart({ + namespace: object.getNs(), + name: object.getName(), + }) + } catch (err) { + Notifications.error(err); + } + }, + labelOk: _i18n._(t`Restart`), + message: ( +

+ Are you sure you want to restart deployment {object.getName()}? +

+ ), + })}> + + Restart +
+ ) } @@ -110,4 +137,3 @@ kubeObjectMenuRegistry.add({ MenuItem: DeploymentMenu } }) -