From a6f7bc3c105eb35bfaa4460e393b0703b5cb5ce3 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Tue, 23 Jun 2020 12:09:17 -0400 Subject: [PATCH] Clean up electron-store based stores - Add some type script types for better editor experience - Remove redundent global of a singleton Signed-off-by: Sebastian Malton remove references to double statics Signed-off-by: Sebastian Malton fix mocking of user-store Signed-off-by: Sebastian Malton --- spec/src/main/kubectl_spec.ts | 25 ++ src/common/cluster-store.ts | 4 +- src/common/cluster-store_spec.ts | 6 +- src/common/request.ts | 8 +- src/common/tracker.ts | 6 +- src/common/user-store.ts | 20 +- src/common/user-store_spec.ts | 26 +- src/common/workspace-store.ts | 25 +- src/main/cluster-manager.ts | 16 +- src/main/cluster.ts | 15 +- src/main/index.ts | 4 +- src/main/kube-auth-proxy.ts | 10 +- src/main/kubectl.ts | 246 +++++++++--------- src/main/routes/config.ts | 6 +- src/main/routes/port-forward.ts | 4 +- .../_vue/components/MainMenu/MainMenu.vue | 5 +- src/renderer/_vue/store/index.js | 21 +- src/renderer/_vue/store/modules/clusters.ts | 14 +- src/renderer/_vue/store/modules/workspaces.ts | 18 +- 19 files changed, 252 insertions(+), 227 deletions(-) create mode 100644 spec/src/main/kubectl_spec.ts diff --git a/spec/src/main/kubectl_spec.ts b/spec/src/main/kubectl_spec.ts new file mode 100644 index 0000000000..b79098cf1a --- /dev/null +++ b/spec/src/main/kubectl_spec.ts @@ -0,0 +1,25 @@ +jest.mock("electron") +jest.mock("../../../src/common/user-store", () => ({ + UserStore: { + getInstance: () => ({ + getPreferences: () => ({ + downloadMirror: "default", + }) + }) + } +})) + +import { Kubectl } from "../../../src/main/kubectl" + +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 kubectl = new Kubectl("1.17.0") + expect(kubectl.kubectlVersion).toBe(Kubectl.bundled().kubectlVersion) + }) +}) diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index f7f323fad2..bf84cce76e 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store.ts @@ -11,7 +11,7 @@ import { getAppVersion } from "./utils/app-version"; export class ClusterStore { private static instance: ClusterStore; - public store: ElectronStore; + private store: ElectronStore; private constructor() { this.store = new ElectronStore({ @@ -116,5 +116,3 @@ export class ClusterStore { ClusterStore.instance = null } } - -export const clusterStore: ClusterStore = ClusterStore.getInstance(); diff --git a/src/common/cluster-store_spec.ts b/src/common/cluster-store_spec.ts index 3bc87a7cd8..707e3d32bb 100644 --- a/src/common/cluster-store_spec.ts +++ b/src/common/cluster-store_spec.ts @@ -235,7 +235,7 @@ describe("for a pre 2.4.1 config with an existing cluster", () => { it("migrates to modern format throwing out the state related data", async () => { const clusterStore = ClusterStore.getInstance() - const storedClusterData = clusterStore.store.get('clusters')[0] + const storedClusterData = clusterStore.getAllClusters()[0] expect(storedClusterData.hasOwnProperty('online')).toBe(false) expect(storedClusterData.hasOwnProperty('accessible')).toBe(false) expect(storedClusterData.hasOwnProperty('failureReason')).toBe(false) @@ -306,7 +306,7 @@ describe("for a pre 2.6.0 config with a cluster icon", () => { it("moves the icon into preferences", async () => { const clusterStore = ClusterStore.getInstance() - const storedClusterData = clusterStore.store.get('clusters')[0] + const storedClusterData = clusterStore.getAllClusters()[0] expect(storedClusterData.hasOwnProperty('icon')).toBe(false) expect(storedClusterData.preferences.hasOwnProperty('icon')).toBe(true) expect(storedClusterData.preferences.icon).toBe("icon path") @@ -343,7 +343,7 @@ describe("for a pre 2.7.0-beta.0 config without a workspace", () => { it("adds cluster to default workspace", async () => { const clusterStore = ClusterStore.getInstance() - const storedClusterData = clusterStore.store.get("clusters")[0] + const storedClusterData = clusterStore.getAllClusters()[0] expect(storedClusterData.workspace).toBe('default') }) }) diff --git a/src/common/request.ts b/src/common/request.ts index 092b1a756a..621f4bc39e 100644 --- a/src/common/request.ts +++ b/src/common/request.ts @@ -1,8 +1,8 @@ -import request from "request" -import { userStore } from "./user-store" +import { Options } from "request" +import { UserStore } from "../common/user-store" -export function globalRequestOpts(requestOpts: request.Options ) { - const userPrefs = userStore.getPreferences() +export function globalRequestOpts(requestOpts: Options): Options { + const userPrefs = UserStore.getInstance().getPreferences() if (userPrefs.httpsProxy) { requestOpts.proxy = userPrefs.httpsProxy } diff --git a/src/common/tracker.ts b/src/common/tracker.ts index 2476c47dbd..4928297c3d 100644 --- a/src/common/tracker.ts +++ b/src/common/tracker.ts @@ -1,6 +1,6 @@ import ua from "universal-analytics" -import { machineIdSync } from "node-machine-id" -import { userStore } from "./user-store" +import { machineIdSync } from 'node-machine-id' +import { UserStore } from "../common/user-store" const GA_ID = "UA-159377374-1" @@ -36,7 +36,7 @@ export class Tracker { } protected telemetryAllowed() { - const userPrefs = userStore.getPreferences() + const userPrefs = UserStore.getInstance().getPreferences() return !!userPrefs.allowTelemetry } } diff --git a/src/common/user-store.ts b/src/common/user-store.ts index fd3ea296fc..695808a0b1 100644 --- a/src/common/user-store.ts +++ b/src/common/user-store.ts @@ -16,7 +16,7 @@ export interface UserPreferences { export class UserStore { private static instance: UserStore; - public store: ElectronStore; + private store: ElectronStore; private constructor() { this.store = new ElectronStore({ @@ -56,17 +56,11 @@ export class UserStore { public getPreferences(): UserPreferences { const prefs = this.store.get("preferences", {}) - if (!prefs.colorTheme) { - prefs.colorTheme = "dark" - } - if (!prefs.downloadMirror) { - prefs.downloadMirror = "default" - } - if (prefs.allowTelemetry === undefined) { - prefs.allowTelemetry = true - } + prefs.colorTheme = prefs.colorTheme || "dark"; + prefs.downloadMirror = prefs.downloadMirror || "default"; + prefs.allowTelemetry = prefs.allowTelemetry ?? true; - return prefs + return prefs; } static getInstance(): UserStore { @@ -80,7 +74,3 @@ export class UserStore { UserStore.instance = null } } - -const userStore: UserStore = UserStore.getInstance(); - -export { userStore }; diff --git a/src/common/user-store_spec.ts b/src/common/user-store_spec.ts index 53901732c6..db758eaf80 100644 --- a/src/common/user-store_spec.ts +++ b/src/common/user-store_spec.ts @@ -1,5 +1,5 @@ import mockFs from "mock-fs" -import { userStore, UserStore } from "./user-store" +import { UserStore } from "./user-store" // Console.log needs to be called before fs-mocks, see https://github.com/tschaub/mock-fs/issues/234 console.log(""); @@ -13,7 +13,6 @@ describe("for an empty config", () => { } } mockFs(mockOpts) - const userStore = UserStore.getInstance() }) afterEach(() => { @@ -21,31 +20,31 @@ describe("for an empty config", () => { }) it("allows setting and retrieving lastSeenAppVersion", async () => { - userStore.setLastSeenAppVersion("1.2.3"); - expect(userStore.lastSeenAppVersion()).toBe("1.2.3"); + UserStore.getInstance().setLastSeenAppVersion("1.2.3"); + expect(UserStore.getInstance().lastSeenAppVersion()).toBe("1.2.3"); }) it("allows adding and listing seen contexts", async () => { - userStore.storeSeenContext(['foo']) - expect(userStore.getSeenContexts().length).toBe(1) - userStore.storeSeenContext(['foo', 'bar']) - const seenContexts = userStore.getSeenContexts() + UserStore.getInstance().storeSeenContext(['foo']) + expect(UserStore.getInstance().getSeenContexts().length).toBe(1) + UserStore.getInstance().storeSeenContext(['foo', 'bar']) + const seenContexts = UserStore.getInstance().getSeenContexts() expect(seenContexts.length).toBe(2) // check 'foo' isn't added twice expect(seenContexts[0]).toBe('foo') expect(seenContexts[1]).toBe('bar') }) it("allows setting and getting preferences", async () => { - userStore.setPreferences({ + UserStore.getInstance().setPreferences({ httpsProxy: 'abcd://defg', }) - const storedPreferences = userStore.getPreferences() + const storedPreferences = UserStore.getInstance().getPreferences() expect(storedPreferences.httpsProxy).toBe('abcd://defg') expect(storedPreferences.colorTheme).toBe('dark') // defaults to dark - userStore.setPreferences({ + UserStore.getInstance().setPreferences({ colorTheme: 'light' }) - expect(userStore.getPreferences().colorTheme).toBe('light') + expect(UserStore.getInstance().getPreferences().colorTheme).toBe('light') }) }) @@ -62,7 +61,6 @@ describe("migrations", () => { } } mockFs(mockOpts) - const userStore = UserStore.getInstance() }) afterEach(() => { @@ -70,6 +68,6 @@ describe("migrations", () => { }) it("sets last seen app version to 0.0.0", async () => { - expect(userStore.lastSeenAppVersion()).toBe('0.0.0') + expect(UserStore.getInstance().lastSeenAppVersion()).toBe('0.0.0') }) }) diff --git a/src/common/workspace-store.ts b/src/common/workspace-store.ts index daf0016b36..8ecd37b2dd 100644 --- a/src/common/workspace-store.ts +++ b/src/common/workspace-store.ts @@ -1,5 +1,5 @@ import ElectronStore from "electron-store" -import { clusterStore } from "./cluster-store" +import { ClusterStore } from "./cluster-store" export interface WorkspaceData { id: string; @@ -7,6 +7,10 @@ export interface WorkspaceData { description?: string; } +interface WorkspaceStoreData { + workspaces: WorkspaceData[]; +} + export class Workspace implements WorkspaceData { public id: string public name: string @@ -20,14 +24,21 @@ export class Workspace implements WorkspaceData { export class WorkspaceStore { public static defaultId = "default" private static instance: WorkspaceStore; - public store: ElectronStore; + private store: ElectronStore; private constructor() { this.store = new ElectronStore({ - name: "lens-workspace-store" + name: "lens-workspace-store", }) - } + if (this.store.get("workspaces", []).length === 0) { + this.store.set("workspaces", [{ + id: "default", + name: "default" + }]); + } + } + public storeWorkspace(workspace: WorkspaceData) { const workspaces = this.getAllWorkspaces() const index = workspaces.findIndex((w) => w.id === workspace.id) @@ -46,12 +57,16 @@ export class WorkspaceStore { const workspaces = this.getAllWorkspaces() const index = workspaces.findIndex((w) => w.id === workspace.id) if (index !== -1) { - clusterStore.removeClustersByWorkspace(workspace.id) + ClusterStore.getInstance().removeClustersByWorkspace(workspace.id) workspaces.splice(index, 1) this.store.set("workspaces", workspaces) } } + public getWorkspace(id: string): Workspace | null { + return this.store.get("workspaces", []).find(wsd => wsd.id == id) || null; + } + public getAllWorkspaces(): Array { const workspacesData: WorkspaceData[] = this.store.get("workspaces", []) diff --git a/src/main/cluster-manager.ts b/src/main/cluster-manager.ts index 2be043fefe..8d83e87b38 100644 --- a/src/main/cluster-manager.ts +++ b/src/main/cluster-manager.ts @@ -1,8 +1,8 @@ import { KubeConfig } from "@kubernetes/client-node" -import { PromiseIpc } from "electron-promise-ipc" +import PromiseIpc from "electron-promise-ipc" import http from "http" import { Cluster, ClusterBaseInfo } from "./cluster" -import { clusterStore } from "../common/cluster-store" +import { ClusterStore } from "../common/cluster-store" import * as k8s from "./k8s" import logger from "./logger" import { LensProxy } from "./proxy" @@ -39,7 +39,7 @@ export class ClusterManager { protected clusters: Map; constructor(clusters: Cluster[], port: number) { - this.promiseIpc = new PromiseIpc({ timeout: 2000 }) + this.promiseIpc = new PromiseIpc.PromiseIpc({ maxTimeoutMs: 2000 }) this.port = port this.clusters = new Map() clusters.forEach((clusterInfo) => { @@ -174,10 +174,10 @@ export class ClusterManager { } try { const clusterIcon = await this.uploadClusterIcon(cluster, fileUpload.name, fileUpload.path) - clusterStore.reloadCluster(cluster); + ClusterStore.getInstance().reloadCluster(cluster); if(!cluster.preferences) cluster.preferences = {}; cluster.preferences.icon = clusterIcon - clusterStore.storeCluster(cluster); + ClusterStore.getInstance().storeCluster(cluster); return {success: true, cluster: cluster.toClusterInfo(), message: ""} } catch(error) { return {success: false, message: error} @@ -189,7 +189,7 @@ export class ClusterManager { const cluster = this.getCluster(id) if (cluster && cluster.preferences) { cluster.preferences.icon = null; - clusterStore.storeCluster(cluster) + ClusterStore.getInstance().storeCluster(cluster) return {success: true, cluster: cluster.toClusterInfo(), message: ""} } else { return {success: false, message: "Cluster not found"} @@ -221,7 +221,7 @@ export class ClusterManager { logger.debug(`IPC: clusterStored: ${clusterId}`) const cluster = this.clusters.get(clusterId) if (cluster) { - clusterStore.reloadCluster(cluster); + ClusterStore.getInstance().reloadCluster(cluster); cluster.stopServer() } }); @@ -244,7 +244,7 @@ export class ClusterManager { const cluster = this.clusters.get(id) if (cluster) { cluster.stopServer() - clusterStore.removeCluster(cluster.id); + ClusterStore.getInstance().removeCluster(cluster.id); this.clusters.delete(cluster.id) } return Array.from(this.clusters.values()) diff --git a/src/main/cluster.ts b/src/main/cluster.ts index 4ab24adf5e..d8a9fada2b 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -1,13 +1,13 @@ import { ContextHandler } from "./context-handler" import { FeatureStatusMap } from "./feature" import * as k8s from "./k8s" -import { clusterStore } from "../common/cluster-store" +import { ClusterStore } from "../common/cluster-store" import logger from "./logger" import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node" import * as fm from "./feature-manager"; import { Kubectl } from "./kubectl"; import { KubeconfigManager } from "./kubeconfig-manager" -import { PromiseIpc } from "electron-promise-ipc" +import PromiseIpc from "electron-promise-ipc" import request from "request-promise-native" import { apiPrefix } from "../common/vars"; @@ -79,7 +79,7 @@ export class Cluster implements ClusterInfo { public preferences: ClusterPreferences; protected eventPoller: NodeJS.Timeout; - protected promiseIpc = new PromiseIpc({ timeout: 2000 }) + protected promiseIpc = new PromiseIpc.PromiseIpc({ maxTimeoutMs: 2000 }) protected kubeconfigManager: KubeconfigManager; @@ -128,7 +128,7 @@ export class Cluster implements ClusterInfo { } public async refreshCluster() { - clusterStore.reloadCluster(this) + ClusterStore.getInstance().reloadCluster(this) this.contextHandler.setClusterPreferences(this.preferences) const connectionStatus = await this.getConnectionStatus() @@ -147,14 +147,11 @@ export class Cluster implements ClusterInfo { } public getPrometheusApiPrefix() { - if (!this.preferences.prometheus?.prefix) { - return "" - } - return this.preferences.prometheus.prefix + return this.preferences.prometheus?.prefix || ""; } public save() { - clusterStore.storeCluster(this) + ClusterStore.getInstance().storeCluster(this) } public toClusterInfo(): ClusterInfo { diff --git a/src/main/index.ts b/src/main/index.ts index fec7f3e553..72a9739a3d 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -17,7 +17,7 @@ import logger from "./logger" import initMenu from "./menu" import * as proxy from "./proxy" import { WindowManager } from "./window-manager"; -import { clusterStore } from "../common/cluster-store" +import { ClusterStore } from "../common/cluster-store" import { tracker } from "./tracker" import { ClusterManager } from "./cluster-manager"; import AppUpdater from "./app-updater" @@ -70,7 +70,7 @@ async function main() { } // create cluster manager - clusterManager = new ClusterManager(clusterStore.getAllClusterObjects(), port) + clusterManager = new ClusterManager(ClusterStore.getInstance().getAllClusterObjects(), port) // run proxy try { proxyServer = proxy.listen(port, clusterManager) diff --git a/src/main/kube-auth-proxy.ts b/src/main/kube-auth-proxy.ts index 743bac88f5..15f0ed485b 100644 --- a/src/main/kube-auth-proxy.ts +++ b/src/main/kube-auth-proxy.ts @@ -1,9 +1,9 @@ import { spawn, ChildProcess } from "child_process" import logger from "./logger" import * as tcpPortUsed from "tcp-port-used" -import { Kubectl, bundledKubectl } from "./kubectl" +import { Kubectl } from "./kubectl" import { Cluster } from "./cluster" -import { PromiseIpc } from "electron-promise-ipc" +import PromiseIpc from "electron-promise-ipc" import { findMainWebContents } from "./webcontents" export class KubeAuthProxy { @@ -13,22 +13,20 @@ export class KubeAuthProxy { protected env: NodeJS.ProcessEnv = null protected proxyProcess: ChildProcess protected port: number - protected kubectl: Kubectl protected promiseIpc: any constructor(cluster: Cluster, port: number, env: NodeJS.ProcessEnv) { this.env = env this.port = port this.cluster = cluster - this.kubectl = bundledKubectl - this.promiseIpc = new PromiseIpc({ timeout: 2000 }) + this.promiseIpc = new PromiseIpc.PromiseIpc({ maxTimeoutMs: 2000 }) } public async run(): Promise { if (this.proxyProcess) { return; } - const proxyBin = await this.kubectl.kubectlPath() + const proxyBin = await Kubectl.bundled().kubectlPath() let args = [ "proxy", "-p", this.port.toString(), diff --git a/src/main/kubectl.ts b/src/main/kubectl.ts index 73efc507b3..e2f79c7ad1 100644 --- a/src/main/kubectl.ts +++ b/src/main/kubectl.ts @@ -8,8 +8,8 @@ import { ensureDir, pathExists } from "fs-extra" import { globalRequestOpts } from "../common/request" import * as lockFile from "proper-lockfile" import { helmCli } from "./helm-cli" -import { userStore } from "../common/user-store" import { getBundledKubectlVersion} from "../common/utils/app-version" +import { UserStore } from "../common/user-store" const bundledVersion = getBundledKubectlVersion() const kubectlMap: Map = new Map([ @@ -32,18 +32,19 @@ const packageMirrors: Map = new Map([ ["china", "https://mirror.azure.cn/kubernetes/kubectl"] ]) -const initScriptVersionString = "# lens-initscript v3\n" - -const isDevelopment = process.env.NODE_ENV !== "production" -let bundledPath: string = null - -if(isDevelopment) { - bundledPath = path.join(process.cwd(), "binaries", "client", process.platform, process.arch, "kubectl") -} else { - bundledPath = path.join(process.resourcesPath, process.arch, "kubectl") -} - -if(process.platform === "win32") bundledPath = `${bundledPath}.exe` +const initScriptVersionString = "# lens-initscript v3"; +const isDevelopment = process.env.NODE_ENV !== "production"; +const bundledPath = ( + isDevelopment + ? path.join(process.cwd(), "binaries", "client", process.platform, process.arch, "kubectl") + : path.join(process.resourcesPath, process.arch, "kubectl") +) + + (process.platform === "win32" ? ".exe" : ""); +const archMap: Record = { + "x64": "amd64", + "x86": "386", + "ia32": "386", +}; export class Kubectl { @@ -77,21 +78,11 @@ export class Kubectl { logger.debug("Set kubectl version " + this.kubectlVersion + " for cluster version " + clusterVersion + " using fallback") } - let arch = null - - if(process.arch == "x64") { - arch = "amd64" - } else if(process.arch == "x86" || process.arch == "ia32") { - arch = "386" - } else { - arch = process.arch - } - + const arch = archMap[process.arch] || process.arch; const platformName = process.platform === "win32" ? "windows" : process.platform const binaryName = process.platform === "win32" ? "kubectl.exe" : "kubectl" this.url = `${this.getDownloadMirror()}/v${this.kubectlVersion}/bin/${platformName}/${arch}/${binaryName}` - this.dirname = path.normalize(path.join(Kubectl.kubectlDir, this.kubectlVersion)) this.path = path.join(this.dirname, binaryName) } @@ -107,7 +98,7 @@ export class Kubectl { } } - public async binDir() { + public async binDir(): Promise { try { await this.ensureKubectl() return this.dirname @@ -117,70 +108,90 @@ export class Kubectl { } } - public async checkBinary(checkVersion = true) { + public async checkBinary(checkVersion = true): Promise { const exists = await pathExists(this.path) - if (exists) { - if (!checkVersion) { + if (!exists) { + return false; + } + + if (!checkVersion) { + return true + } + + try { + const { stdout } = await promiseExec(`"${this.path}" version --client=true -o json`) + const output = JSON.parse(stdout) + 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 } - - try { - const { stdout, stderr } = await promiseExec(`"${this.path}" version --client=true -o json`) - const output = JSON.parse(stdout) - 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`) - } - catch(err) { - logger.error(`Local kubectl failed to run properly (${err.message}), unlinking`) - } - await fs.promises.unlink(this.path) + logger.error(`Local kubectl is version ${version}, expected ${this.kubectlVersion}, unlinking`) } - return false + catch(err) { + logger.error(`Local kubectl failed to run properly (${err.message}), unlinking`) + } + + await fs.promises.unlink(this.path) + return false; } protected async checkBundled(): Promise { - if(this.kubectlVersion === Kubectl.bundledKubectlVersion) { - try { - const exist = await pathExists(this.path) - if (!exist) { - await fs.promises.copyFile(Kubectl.bundledKubectlPath, 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 + if (this.kubectlVersion !== Kubectl.bundledKubectlVersion) { + return false; + } + + try { + const exist = await pathExists(this.path) + if (!exist) { + await fs.promises.copyFile(Kubectl.bundledKubectlPath, this.path) + await fs.promises.chmod(this.path, 0o755) } - } else { + return true + } catch(err) { + logger.error("Could not copy the bundled kubectl to app-data: " + err) return false } } public async ensureKubectl(): Promise { - await ensureDir(this.dirname, 0o755) - return lockFile.lock(this.dirname).then(async (release) => { + await ensureDir(this.dirname, 0o755); + let release: () => Promise; + + try { + release = await lockFile.lock(this.dirname); logger.debug(`Acquired a lock for ${this.kubectlVersion}`) - const bundled = await this.checkBundled() - const isValid = await this.checkBinary(!bundled) - if(!isValid) { - await this.downloadKubectl().catch((error) => { logger.error(error) }); + + try { + if (!await this.checkBinary(!await this.checkBundled())) { + await this.downloadKubectl() + } + } catch (e) { + logger.error(e) } - await this.writeInitScripts().catch((error) => { logger.error("Failed to write init scripts"); logger.error(error) }) - logger.debug(`Releasing lock for ${this.kubectlVersion}`) - release() + + try { + await this.writeInitScripts(); + } catch (e) { + logger.error("Failed to write init scripts"); + logger.error(e) + } + return true - }).catch((e) => { - logger.error(`Failed to get a lock for ${this.kubectlVersion}`) + } catch (e) { logger.error(e) return false - }) + } finally { + if (release) { + logger.debug(`Releasing lock for ${this.kubectlVersion}`) + release(); + } else { + logger.error(`Failed to get a lock for ${this.kubectlVersion}`) + } + } } public async downloadKubectl() { @@ -229,52 +240,55 @@ export class Kubectl { protected async writeInitScripts() { const helmPath = helmCli.getBinaryDir() - const fsPromises = fs.promises; const bashScriptPath = path.join(this.dirname, '.bash_set_path') - const bashScriptIsLatest = await this.scriptIsLatest(bashScriptPath) - if(!bashScriptIsLatest) { - let bashScript = "" + initScriptVersionString - bashScript += "tempkubeconfig=\"$KUBECONFIG\"\n" - bashScript += "test -f \"/etc/profile\" && . \"/etc/profile\"\n" - bashScript += "if test -f \"$HOME/.bash_profile\"; then\n" - bashScript += " . \"$HOME/.bash_profile\"\n" - bashScript += "elif test -f \"$HOME/.bash_login\"; then\n" - bashScript += " . \"$HOME/.bash_login\"\n" - bashScript += "elif test -f \"$HOME/.profile\"; then\n" - bashScript += " . \"$HOME/.profile\"\n" - bashScript += "fi\n" - bashScript += `export PATH="${this.dirname}:${helmPath}:$PATH"\n` - bashScript += "export KUBECONFIG=\"$tempkubeconfig\"\n" - bashScript += "unset tempkubeconfig\n" - await fsPromises.writeFile(bashScriptPath, bashScript.toString(), { mode: 0o644 }) + + if (!await this.scriptIsLatest(bashScriptPath)) { + const bashScript = ` +${initScriptVersionString} +tempkubeconfig="$KUBECONFIG" +test -f "/etc/profile" && . "/etc/profile" +if test -f "$HOME/.bash_profile"; then + . "$HOME/.bash_profile" +elif test -f "$HOME/.bash_login"; then + . "$HOME/.bash_login" +elif test -f "$HOME/.profile"; then + . "$HOME/.profile" +fi +export PATH="${this.dirname}:${helmPath}:$PATH" +export KUBECONFIG="$tempkubeconfig" +unset tempkubeconfig +`.trimLeft(); + + await fs.promises.writeFile(bashScriptPath, bashScript.toString(), { mode: 0o644 }) } const zshScriptPath = path.join(this.dirname, '.zlogin') - const zshScriptIsLatest = await this.scriptIsLatest(zshScriptPath) - if(!zshScriptIsLatest) { - let zshScript = "" + initScriptVersionString + if (!await this.scriptIsLatest(zshScriptPath)) { + const zshScript = ` +${initScriptVersionString} +tempkubeconfig="$KUBECONFIG" - zshScript += "tempkubeconfig=\"$KUBECONFIG\"\n" - // restore previous ZDOTDIR - zshScript += "export ZDOTDIR=\"$OLD_ZDOTDIR\"\n" - // source all the files - zshScript += "test -f \"$OLD_ZDOTDIR/.zshenv\" && . \"$OLD_ZDOTDIR/.zshenv\"\n" - zshScript += "test -f \"$OLD_ZDOTDIR/.zprofile\" && . \"$OLD_ZDOTDIR/.zprofile\"\n" - zshScript += "test -f \"$OLD_ZDOTDIR/.zlogin\" && . \"$OLD_ZDOTDIR/.zlogin\"\n" - zshScript += "test -f \"$OLD_ZDOTDIR/.zshrc\" && . \"$OLD_ZDOTDIR/.zshrc\"\n" +${/* restore previous ZDOTDIR */""} +export ZDOTDIR="$OLD_ZDOTDIR" - // voodoo to replace any previous occurences of kubectl path in the PATH - zshScript += `kubectlpath=\"${this.dirname}"\n` - zshScript += `helmpath=\"${helmPath}"\n` - zshScript += "p=\":$kubectlpath:\"\n" - zshScript += "d=\":$PATH:\"\n" - zshScript += "d=${d//$p/:}\n" - zshScript += "d=${d/#:/}\n" - zshScript += "export PATH=\"$kubectlpath:$helmpath:${d/%:/}\"\n" - zshScript += "export KUBECONFIG=\"$tempkubeconfig\"\n" - zshScript += "unset tempkubeconfig\n" - zshScript += "unset OLD_ZDOTDIR\n" - await fsPromises.writeFile(zshScriptPath, zshScript.toString(), { mode: 0o644 }) +${/* source all the files */""} +test -f "$OLD_ZDOTDIR/.zshenv" && . "$OLD_ZDOTDIR/.zshenv" +test -f "$OLD_ZDOTDIR/.zprofile" && . "$OLD_ZDOTDIR/.zprofile" +test -f "$OLD_ZDOTDIR/.zlogin" && . "$OLD_ZDOTDIR/.zlogin" +test -f "$OLD_ZDOTDIR/.zshrc" && . "$OLD_ZDOTDIR/.zshrc" + +${/* voodoo to replace any previous occurences of kubectl path in the PATH */""} +kubectlpath="${this.dirname}" +p=":$kubectlpath:" +d=":$PATH:" +d=\${d//$p/:} +export PATH="$kubectlpath:$helmpath:\${d/%:/}" +export KUBECONFIG="$tempkubeconfig" +unset tempkubeconfig +unset OLD_ZDOTDIR +`.trimLeft(); + + await fs.promises.writeFile(zshScriptPath, zshScript.toString(), { mode: 0o644 }) } } @@ -285,13 +299,7 @@ export class Kubectl { } protected getDownloadMirror() { - const mirror = packageMirrors.get(userStore.getPreferences().downloadMirror) - if (mirror) { - return mirror - } - return packageMirrors.get("default") // MacOS packages are only available from default + return packageMirrors.get(UserStore.getInstance().getPreferences().downloadMirror) + || packageMirrors.get("default"); // MacOS packages are only available from default } } - -const bundledKubectl = Kubectl.bundled() -export { bundledKubectl } diff --git a/src/main/routes/config.ts b/src/main/routes/config.ts index d726a928f8..1632de4f54 100644 --- a/src/main/routes/config.ts +++ b/src/main/routes/config.ts @@ -1,8 +1,8 @@ import { app } from "electron" -import { CoreV1Api } from "@kubernetes/client-node" +import { CoreV1Api, AuthorizationV1Api } from "@kubernetes/client-node" import { LensApiRequest } from "../router" import { LensApi } from "../lens-api" -import { userStore } from "../../common/user-store" +import { UserStore } from "../../common/user-store" import { Cluster } from "../cluster" export interface IConfigRoutePayload { @@ -94,7 +94,7 @@ class ConfigRoute extends LensApi { const data: IConfigRoutePayload = { clusterName: cluster.contextName, lensVersion: app.getVersion(), - lensTheme: `kontena-${userStore.getPreferences().colorTheme}`, + lensTheme: `kontena-${UserStore.getInstance().getPreferences().colorTheme}`, kubeVersion: cluster.version, chartsEnabled: true, isClusterAdmin: cluster.isAdmin, diff --git a/src/main/routes/port-forward.ts b/src/main/routes/port-forward.ts index 27b1158700..164f7650b3 100644 --- a/src/main/routes/port-forward.ts +++ b/src/main/routes/port-forward.ts @@ -1,7 +1,7 @@ import { LensApiRequest } from "../router" import { LensApi } from "../lens-api" import { spawn, ChildProcessWithoutNullStreams } from "child_process" -import { bundledKubectl } from "../kubectl" +import { Kubectl } from "../kubectl" import { getFreePort } from "../port" import { shell } from "electron" import * as tcpPortUsed from "tcp-port-used" @@ -37,7 +37,7 @@ class PortForward { public async start() { this.localPort = await getFreePort() - const kubectlBin = await bundledKubectl.kubectlPath() + const kubectlBin = await Kubectl.bundled().kubectlPath() const args = [ "--kubeconfig", this.kubeConfig, "port-forward", diff --git a/src/renderer/_vue/components/MainMenu/MainMenu.vue b/src/renderer/_vue/components/MainMenu/MainMenu.vue index 5ab9391e05..afc65730c5 100644 --- a/src/renderer/_vue/components/MainMenu/MainMenu.vue +++ b/src/renderer/_vue/components/MainMenu/MainMenu.vue @@ -22,8 +22,9 @@ import ClusterMenuItem from "@/_vue/components/MainMenu/ClusterMenuItem"; import AddClusterMenuItem from "@/_vue/components/MainMenu/AddClusterMenuItem"; import draggable from 'vuedraggable' -import { clusterStore } from "../../../../common/cluster-store" import { isMac } from "../../../../common/vars" +import { ClusterStore } from "@/../common/cluster-store"; +import * as os from "os"; const {remote} = require('electron') const {Menu, MenuItem} = remote @@ -47,7 +48,7 @@ export default { }, set: function (clusters) { this.$store.commit("updateClusters", clusters); - clusterStore.storeClusters(clusters); + ClusterStore.getInstance().storeClusters(clusters); } } }, diff --git a/src/renderer/_vue/store/index.js b/src/renderer/_vue/store/index.js index 85b6e0eac8..7435fd1243 100644 --- a/src/renderer/_vue/store/index.js +++ b/src/renderer/_vue/store/index.js @@ -1,8 +1,8 @@ import Vue from 'vue' import Vuex from 'vuex' import semver from "semver" -import { userStore } from "../../../common/user-store" import { getAppVersion } from "../../../common/utils/app-version" +import { UserStore } from "../../common/user-store" import KubeContexts from './modules/kube-contexts' import Clusters from './modules/clusters' import HelmRepos from './modules/helm-repos' @@ -31,29 +31,24 @@ export default new Vuex.Store({ hud: { isMenuVisible: true, }, - seenContexts: userStore.getSeenContexts(), - lastSeenAppVersion: userStore.lastSeenAppVersion(), + seenContexts: UserStore.getInstance().getSeenContexts(), + lastSeenAppVersion: UserStore.getInstance().lastSeenAppVersion(), }, mutations: { storeSeenContexts(state, context) { - const seenContexts = userStore.storeSeenContext(context); - state.seenContexts = seenContexts + state.seenContexts = UserStore.getInstance().storeSeenContext(context) }, updateLastSeenAppVersion(state, appVersion) { state.lastSeenAppVersion = appVersion; - userStore.setLastSeenAppVersion(appVersion) + UserStore.getInstance().setLastSeenAppVersion(appVersion) }, loadPreferences(state) { - this.commit("savePreferences", userStore.getPreferences()); + this.commit("savePreferences", UserStore.getInstance().getPreferences()); }, savePreferences(state, prefs) { - if (prefs.allowTelemetry) { - tracker.event("telemetry", "enabled") - } else { - tracker.event("telemetry", "disabled") - } + tracker.event("telemetry", prefs.allowTelemetry ? "enabled" : "disabled") state.preferences = prefs; - userStore.setPreferences(prefs); + UserStore.getInstance().setPreferences(prefs); this.dispatch("destroyWebviews") promiseIpc.send("preferencesSaved") }, diff --git a/src/renderer/_vue/store/modules/clusters.ts b/src/renderer/_vue/store/modules/clusters.ts index 219a4d9393..93b607c1f8 100644 --- a/src/renderer/_vue/store/modules/clusters.ts +++ b/src/renderer/_vue/store/modules/clusters.ts @@ -1,13 +1,13 @@ import Vue from "vue" import { ClusterInfo } from "../../../../main/cluster" import { MutationTree, ActionTree, GetterTree } from "vuex" -import { PromiseIpc } from 'electron-promise-ipc' +import PromiseIpc from 'electron-promise-ipc' import { Tracker } from "../../../../common/tracker" import { remote } from "electron" -import { clusterStore } from "../../../../common/cluster-store" +import { ClusterStore } from "../../../../common/cluster-store" import { Workspace } from "../../../../common/workspace-store" -const promiseIpc = new PromiseIpc( { maxTimeoutMs: 120000 } ); +const promiseIpc = new PromiseIpc.PromiseIpc( { maxTimeoutMs: 120000 } ); const tracker = new Tracker(remote.app); export interface LensWebview { @@ -130,7 +130,7 @@ const actions: ActionTree = { // For data structure see: cluster-manager.ts / FeatureInstallRequest async installClusterFeature({commit}, data) { // Custom no timeout IPC as install can take very variable time - const ipc = new PromiseIpc(); + const ipc = new PromiseIpc.PromiseIpc(); const response = await ipc.send('installFeature', data) console.log("installer result:", response); const cluster = await ipc.send('refreshCluster', data.clusterId) @@ -142,7 +142,7 @@ const actions: ActionTree = { // For data structure see: cluster-manager.ts / FeatureInstallRequest async upgradeClusterFeature({commit}, data) { // Custom no timeout IPC as install can take very variable time - const ipc = new PromiseIpc(); + const ipc = new PromiseIpc.PromiseIpc(); const response = await ipc.send('upgradeFeature', data) console.log("upgrade result:", response); const cluster = await ipc.send('refreshCluster', data.clusterId) @@ -155,7 +155,7 @@ const actions: ActionTree = { // For data structure see: cluster-manager.ts / FeatureInstallRequest async uninstallClusterFeature({commit}, data) { // Custom no timeout IPC as uninstall can take very variable time - const ipc = new PromiseIpc(); + const ipc = new PromiseIpc.PromiseIpc(); const response = await ipc.send('uninstallFeature', data) console.log("uninstaller result:", response); const cluster = await ipc.send('refreshCluster', data.clusterId) @@ -217,7 +217,7 @@ const actions: ActionTree = { }) }, storeCluster({commit}, cluster: ClusterInfo) { - clusterStore.storeCluster(cluster); + ClusterStore.getInstance().storeCluster(cluster); commit("updateCluster", cluster) promiseIpc.send("clusterStored", cluster.id) } diff --git a/src/renderer/_vue/store/modules/workspaces.ts b/src/renderer/_vue/store/modules/workspaces.ts index ea075cb43f..0a73e78551 100644 --- a/src/renderer/_vue/store/modules/workspaces.ts +++ b/src/renderer/_vue/store/modules/workspaces.ts @@ -1,5 +1,5 @@ import { MutationTree, ActionTree, GetterTree } from "vuex" -import { workspaceStore, Workspace, WorkspaceData } from "../../../../common/workspace-store" +import { WorkspaceStore, Workspace, WorkspaceData } from "../../../../common/workspace-store" export interface WorkspaceState { workspaces: Array; @@ -7,8 +7,8 @@ export interface WorkspaceState { } const state: WorkspaceState = { - workspaces: workspaceStore.getAllWorkspaces(), - currentWorkspace: workspaceStore.getAllWorkspaces().find((w) => w.id === "default") + workspaces: WorkspaceStore.getInstance().getAllWorkspaces(), + currentWorkspace: WorkspaceStore.getInstance().getAllWorkspaces().find((w) => w.id === "default"), } const actions: ActionTree = { @@ -27,16 +27,16 @@ const mutations: MutationTree = { state.currentWorkspace = workspace }, addWorkspace(state, workspace: WorkspaceData) { - workspaceStore.storeWorkspace(workspace) - state.workspaces = workspaceStore.getAllWorkspaces() + WorkspaceStore.getInstance().storeWorkspace(workspace) + state.workspaces = WorkspaceStore.getInstance().getAllWorkspaces() }, updateWorkspace(state, workspace: WorkspaceData) { - workspaceStore.storeWorkspace(workspace) - state.workspaces = workspaceStore.getAllWorkspaces() + WorkspaceStore.getInstance().storeWorkspace(workspace) + state.workspaces = WorkspaceStore.getInstance().getAllWorkspaces() }, removeWorkspace(state, workspace: Workspace) { - workspaceStore.removeWorkspace(workspace) - state.workspaces = workspaceStore.getAllWorkspaces() + WorkspaceStore.getInstance().removeWorkspace(workspace) + state.workspaces = WorkspaceStore.getInstance().getAllWorkspaces() } }