diff --git a/jsonnet/custom-prometheus.jsonnet b/jsonnet/lens/custom-prometheus.jsonnet similarity index 100% rename from jsonnet/custom-prometheus.jsonnet rename to jsonnet/lens/custom-prometheus.jsonnet diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index 7ae402eefe..d1006f0a3d 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store.ts @@ -225,6 +225,12 @@ export class ClusterStore extends BaseStore { workspaceStore.setLastActiveClusterId(clusterId); } + deactivate(id: ClusterId) { + if (this.isActive(id)) { + this.setActive(null); + } + } + @action swapIconOrders(workspace: WorkspaceId, from: number, to: number) { const clusters = this.getByWorkspaceId(workspace); diff --git a/src/common/ipc/cluster.ipc.ts b/src/common/ipc/cluster.ipc.ts new file mode 100644 index 0000000000..a7b13ba290 --- /dev/null +++ b/src/common/ipc/cluster.ipc.ts @@ -0,0 +1,11 @@ +/** + * This channel is broadcast on whenever the cluster fails to list namespaces + * during a refresh and no `accessibleNamespaces` have been set. + */ +export const ClusterListNamespaceForbiddenChannel = "cluster:list-namespace-forbidden"; + +export type ListNamespaceForbiddenArgs = [clusterId: string]; + +export function isListNamespaceForbiddenArgs(args: unknown[]): args is ListNamespaceForbiddenArgs { + return args.length === 1 && typeof args[0] === "string"; +} diff --git a/src/common/ipc/index.ts b/src/common/ipc/index.ts index c5e864dc75..f67d794626 100644 --- a/src/common/ipc/index.ts +++ b/src/common/ipc/index.ts @@ -1,4 +1,5 @@ export * from "./ipc"; export * from "./invalid-kubeconfig"; -export * from "./update-available"; +export * from "./update-available.ipc"; +export * from "./cluster.ipc"; export * from "./type-enforced-ipc"; diff --git a/src/common/ipc/update-available/index.ts b/src/common/ipc/update-available.ipc.ts similarity index 84% rename from src/common/ipc/update-available/index.ts rename to src/common/ipc/update-available.ipc.ts index 1e3fcf1268..8571c08512 100644 --- a/src/common/ipc/update-available/index.ts +++ b/src/common/ipc/update-available.ipc.ts @@ -3,10 +3,7 @@ import { UpdateInfo } from "electron-updater"; export const UpdateAvailableChannel = "update-available"; export const AutoUpdateLogPrefix = "[UPDATE-CHECKER]"; -/** - * [, ] - */ -export type UpdateAvailableFromMain = [string, UpdateInfo]; +export type UpdateAvailableFromMain = [backChannel: string, updateInfo: UpdateInfo]; export function areArgsUpdateAvailableFromMain(args: unknown[]): args is UpdateAvailableFromMain { if (args.length !== 2) { @@ -32,7 +29,7 @@ export type BackchannelArg = { now: boolean; }; -export type UpdateAvailableToBackchannel = [BackchannelArg]; +export type UpdateAvailableToBackchannel = [updateDecision: BackchannelArg]; export function areArgsUpdateAvailableToBackchannel(args: unknown[]): args is UpdateAvailableToBackchannel { if (args.length !== 1) { diff --git a/src/common/user-store.ts b/src/common/user-store.ts index 97ba08f3cc..c7ad21f988 100644 --- a/src/common/user-store.ts +++ b/src/common/user-store.ts @@ -10,6 +10,7 @@ import { kubeConfigDefaultPath, loadConfig } from "./kube-helpers"; import { appEventBus } from "./event-bus"; import logger from "../main/logger"; import path from "path"; +import { fileNameMigration } from "../migrations/user-store"; export interface UserStoreModel { kubeConfigPath: string; @@ -37,7 +38,7 @@ export class UserStore extends BaseStore { private constructor() { super({ - // configName: "lens-user-store", // todo: migrate from default "config.json" + configName: "lens-user-store", migrations, }); @@ -85,6 +86,16 @@ export class UserStore extends BaseStore { } } + async load(): Promise { + /** + * This has to be here before the call to `new Config` in `super.load()` + * as we have to make sure that file is in the expected place for that call + */ + await fileNameMigration(); + + return super.load(); + } + get isNewVersion() { return semver.gt(getAppVersion(), this.lastSeenAppVersion); } diff --git a/src/main/__test__/kubeconfig-manager.test.ts b/src/main/__test__/kubeconfig-manager.test.ts index 89c882b109..d04f7492f2 100644 --- a/src/main/__test__/kubeconfig-manager.test.ts +++ b/src/main/__test__/kubeconfig-manager.test.ts @@ -85,8 +85,8 @@ describe("kubeconfig manager tests", () => { const kubeConfManager = await KubeconfigManager.create(cluster, contextHandler, port); expect(logger.error).not.toBeCalled(); - expect(kubeConfManager.getPath()).toBe(`tmp${path.sep}kubeconfig-foo`); - const file = await fse.readFile(kubeConfManager.getPath()); + expect(await kubeConfManager.getPath()).toBe(`tmp${path.sep}kubeconfig-foo`); + const file = await fse.readFile(await kubeConfManager.getPath()); const yml = loadYaml(file.toString()); expect(yml["current-context"]).toBe("minikube"); @@ -104,12 +104,12 @@ 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(); + const configPath = await kubeConfManager.getPath(); expect(await fse.pathExists(configPath)).toBe(true); await kubeConfManager.unlink(); expect(await fse.pathExists(configPath)).toBe(false); await kubeConfManager.unlink(); // doesn't throw - expect(kubeConfManager.getPath()).toBeUndefined(); + expect(await kubeConfManager.getPath()).toBeUndefined(); }); }); diff --git a/src/main/cluster.ts b/src/main/cluster.ts index 8920568c73..19f0945c3e 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -4,9 +4,9 @@ import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api"; import type { WorkspaceId } from "../common/workspace-store"; import { action, comparer, computed, observable, reaction, toJS, when } from "mobx"; import { apiKubePrefix } from "../common/vars"; -import { broadcastMessage, InvalidKubeconfigChannel } from "../common/ipc"; +import { broadcastMessage, InvalidKubeconfigChannel, ClusterListNamespaceForbiddenChannel } from "../common/ipc"; import { ContextHandler } from "./context-handler"; -import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"; +import { AuthorizationV1Api, CoreV1Api, HttpError, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"; import { Kubectl } from "./kubectl"; import { KubeconfigManager } from "./kubeconfig-manager"; import { loadConfig, validateKubeConfig } from "../common/kube-helpers"; @@ -482,14 +482,16 @@ export class Cluster implements ClusterModel, ClusterState { /** * @internal */ - getProxyKubeconfig(): KubeConfig { - return loadConfig(this.getProxyKubeconfigPath()); + async getProxyKubeconfig(): Promise { + const kubeconfigPath = await this.getProxyKubeconfigPath(); + + return loadConfig(kubeconfigPath); } /** * @internal */ - getProxyKubeconfigPath(): string { + async getProxyKubeconfigPath(): Promise { return this.kubeconfigManager.getPath(); } @@ -565,7 +567,7 @@ export class Cluster implements ClusterModel, ClusterState { * @param resourceAttributes resource attributes */ async canI(resourceAttributes: V1ResourceAttributes): Promise { - const authApi = this.getProxyKubeconfig().makeApiClient(AuthorizationV1Api); + const authApi = (await this.getProxyKubeconfig()).makeApiClient(AuthorizationV1Api); try { const accessReview = await authApi.createSelfSubjectAccessReview({ @@ -680,18 +682,22 @@ export class Cluster implements ClusterModel, ClusterState { return this.accessibleNamespaces; } - const api = this.getProxyKubeconfig().makeApiClient(CoreV1Api); + const api = (await this.getProxyKubeconfig()).makeApiClient(CoreV1Api); try { const namespaceList = await api.listNamespace(); return namespaceList.body.items.map(ns => ns.metadata.name); } catch (error) { - const ctx = this.getProxyKubeconfig().getContextObject(this.contextName); + const ctx = (await this.getProxyKubeconfig()).getContextObject(this.contextName); + const namespaceList = [ctx.namespace].filter(Boolean); - if (ctx.namespace) return [ctx.namespace]; + if (namespaceList.length === 0 && error instanceof HttpError && error.statusCode === 403) { + logger.info("[CLUSTER]: listing namespaces is forbidden, broadcasting", { clusterId: this.id }); + broadcastMessage(ClusterListNamespaceForbiddenChannel, this.id); + } - return []; + return namespaceList; } } diff --git a/src/main/context-handler.ts b/src/main/context-handler.ts index d67c495a84..e94520b9be 100644 --- a/src/main/context-handler.ts +++ b/src/main/context-handler.ts @@ -59,7 +59,7 @@ export class ContextHandler { async getPrometheusService(): Promise { const providers = this.prometheusProvider ? prometheusProviders.filter(provider => provider.id == this.prometheusProvider) : prometheusProviders; const prometheusPromises: Promise[] = providers.map(async (provider: PrometheusProvider): Promise => { - const apiClient = this.cluster.getProxyKubeconfig().makeApiClient(CoreV1Api); + const apiClient = (await this.cluster.getProxyKubeconfig()).makeApiClient(CoreV1Api); return await provider.getPrometheusService(apiClient); }); diff --git a/src/main/helm/helm-release-manager.ts b/src/main/helm/helm-release-manager.ts index 220d665ae0..58a4e99798 100644 --- a/src/main/helm/helm-release-manager.ts +++ b/src/main/helm/helm-release-manager.ts @@ -56,11 +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"}); + const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); 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);}); + const { stdout } = await promiseExec(`"${helm}" upgrade ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${proxyKubeconfig}`).catch((error) => { throw(error.stderr);}); return { log: stdout, @@ -73,7 +74,9 @@ export class HelmReleaseManager { public async getRelease(name: string, namespace: string, cluster: Cluster) { 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 proxyKubeconfig = await cluster.getProxyKubeconfigPath(); + + const { stdout } = await promiseExec(`"${helm}" status ${name} --output json --namespace ${namespace} --kubeconfig ${proxyKubeconfig}`).catch((error) => { throw(error.stderr);}); const release = JSON.parse(stdout); release.resources = await this.getResources(name, namespace, cluster); @@ -112,7 +115,7 @@ export class HelmReleaseManager { protected async getResources(name: string, namespace: string, cluster: Cluster) { const helm = await helmCli.binaryPath(); const kubectl = await cluster.kubeCtl.getPath(); - const pathToKubeconfig = cluster.getProxyKubeconfigPath(); + const pathToKubeconfig = await cluster.getProxyKubeconfigPath(); 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: []})}; }); diff --git a/src/main/helm/helm-service.ts b/src/main/helm/helm-service.ts index f7445cebd4..9682e58a84 100644 --- a/src/main/helm/helm-service.ts +++ b/src/main/helm/helm-service.ts @@ -8,7 +8,9 @@ import { HelmChartList, RepoHelmChartList } from "../../renderer/api/endpoints/h class HelmService { public async installChart(cluster: Cluster, data: { chart: string; values: {}; name: string; namespace: string; version: string }) { - return await releaseManager.installChart(data.chart, data.values, data.name, data.namespace, data.version, cluster.getProxyKubeconfigPath()); + const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); + + return await releaseManager.installChart(data.chart, data.values, data.name, data.namespace, data.version, proxyKubeconfig); } public async listCharts() { @@ -53,8 +55,9 @@ class HelmService { public async listReleases(cluster: Cluster, namespace: string = null) { await repoManager.init(); + const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); - return await releaseManager.listReleases(cluster.getProxyKubeconfigPath(), namespace); + return await releaseManager.listReleases(proxyKubeconfig, namespace); } public async getRelease(cluster: Cluster, releaseName: string, namespace: string) { @@ -64,21 +67,27 @@ class HelmService { } public async getReleaseValues(cluster: Cluster, releaseName: string, namespace: string) { + const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); + logger.debug("Fetch release values"); - return await releaseManager.getValues(releaseName, namespace, cluster.getProxyKubeconfigPath()); + return await releaseManager.getValues(releaseName, namespace, proxyKubeconfig); } public async getReleaseHistory(cluster: Cluster, releaseName: string, namespace: string) { + const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); + logger.debug("Fetch release history"); - return await releaseManager.getHistory(releaseName, namespace, cluster.getProxyKubeconfigPath()); + return await releaseManager.getHistory(releaseName, namespace, proxyKubeconfig); } public async deleteRelease(cluster: Cluster, releaseName: string, namespace: string) { + const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); + logger.debug("Delete release"); - return await releaseManager.deleteRelease(releaseName, namespace, cluster.getProxyKubeconfigPath()); + return await releaseManager.deleteRelease(releaseName, namespace, proxyKubeconfig); } public async updateRelease(cluster: Cluster, releaseName: string, namespace: string, data: { chart: string; values: {}; version: string }) { @@ -88,8 +97,10 @@ class HelmService { } public async rollback(cluster: Cluster, releaseName: string, namespace: string, revision: number) { + const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); + logger.debug("Rollback release"); - const output = await releaseManager.rollback(releaseName, namespace, revision, cluster.getProxyKubeconfigPath()); + const output = await releaseManager.rollback(releaseName, namespace, revision, proxyKubeconfig); return { message: output }; } diff --git a/src/main/index.ts b/src/main/index.ts index 79f2a239bc..0a946b9c36 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -29,6 +29,7 @@ import { LensProtocolRouterMain } from "./protocol-handler"; import { getAppVersion, getAppVersionFromProxyServer } from "../common/utils"; import { bindBroadcastHandlers } from "../common/ipc"; import { startUpdateChecking } from "./app-updater"; +import { IpcRendererNavigationEvents } from "../renderer/navigation/events"; const workingDir = path.join(app.getPath("appData"), appName); let proxyPort: number; @@ -162,7 +163,7 @@ app.on("ready", async () => { windowManager.initMainWindow(); } - ipcMain.on("renderer:loaded", () => { + ipcMain.on(IpcRendererNavigationEvents.LOADED, () => { startUpdateChecking(); LensProtocolRouterMain .getInstance() diff --git a/src/main/kubeconfig-manager.ts b/src/main/kubeconfig-manager.ts index bd1b32c1f5..f88ce87840 100644 --- a/src/main/kubeconfig-manager.ts +++ b/src/main/kubeconfig-manager.ts @@ -24,14 +24,24 @@ export class KubeconfigManager { protected async init() { try { await this.contextHandler.ensurePort(); - await this.createProxyKubeconfig(); + this.tempFile = await this.createProxyKubeconfig(); } catch (err) { logger.error(`Failed to created temp config for auth-proxy`, { err }); } } - getPath() { + async getPath() { + // create proxy kubeconfig if it is removed + if (this.tempFile !== undefined && !(await fs.pathExists(this.tempFile))) { + try { + this.tempFile = await this.createProxyKubeconfig(); + } catch (err) { + logger.error(`Failed to created temp config for auth-proxy`, { err }); + } + } + return this.tempFile; + } protected resolveProxyUrl() { @@ -71,9 +81,8 @@ export class KubeconfigManager { // write const configYaml = dumpConfigYaml(proxyConfig); - fs.ensureDir(path.dirname(tempFile)); - fs.writeFileSync(tempFile, configYaml, { mode: 0o600 }); - this.tempFile = tempFile; + await fs.ensureDir(path.dirname(tempFile)); + await fs.writeFile(tempFile, configYaml, { mode: 0o600 }); logger.debug(`Created temp kubeconfig "${contextName}" at "${tempFile}": \n${configYaml}`); return tempFile; diff --git a/src/main/node-shell-session.ts b/src/main/node-shell-session.ts index b67e776725..0799f9f892 100644 --- a/src/main/node-shell-session.ts +++ b/src/main/node-shell-session.ts @@ -17,10 +17,10 @@ export class NodeShellSession extends ShellSession { super(socket, cluster); this.nodeName = nodeName; this.podId = `node-shell-${uuid()}`; - this.kc = cluster.getProxyKubeconfig(); } public async open() { + this.kc = await this.cluster.getProxyKubeconfig(); const shell = await this.kubectl.getPath(); let args = []; diff --git a/src/main/resource-applier.ts b/src/main/resource-applier.ts index d4070f2378..6f1b0a8e0f 100644 --- a/src/main/resource-applier.ts +++ b/src/main/resource-applier.ts @@ -23,12 +23,13 @@ export class ResourceApplier { protected async kubectlApply(content: string): Promise { const { kubeCtl } = this.cluster; const kubectlPath = await kubeCtl.getPath(); + const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath(); return new Promise((resolve, reject) => { const fileName = tempy.file({ name: "resource.yaml" }); fs.writeFileSync(fileName, content); - const cmd = `"${kubectlPath}" apply --kubeconfig "${this.cluster.getProxyKubeconfigPath()}" -o json -f "${fileName}"`; + const cmd = `"${kubectlPath}" apply --kubeconfig "${proxyKubeconfigPath}" -o json -f "${fileName}"`; logger.debug(`shooting manifests with: ${cmd}`); const execEnv: NodeJS.ProcessEnv = Object.assign({}, process.env); @@ -54,6 +55,7 @@ export class ResourceApplier { public async kubectlApplyAll(resources: string[]): Promise { const { kubeCtl } = this.cluster; const kubectlPath = await kubeCtl.getPath(); + const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath(); return new Promise((resolve, reject) => { const tmpDir = tempy.directory(); @@ -62,7 +64,7 @@ export class ResourceApplier { resources.forEach((resource, index) => { fs.writeFileSync(path.join(tmpDir, `${index}.yaml`), resource); }); - const cmd = `"${kubectlPath}" apply --kubeconfig "${this.cluster.getProxyKubeconfigPath()}" -o json -f "${tmpDir}"`; + const cmd = `"${kubectlPath}" apply --kubeconfig "${proxyKubeconfigPath}" -o json -f "${tmpDir}"`; console.log("shooting manifests with:", cmd); exec(cmd, (error, stdout, stderr) => { diff --git a/src/main/routes/kubeconfig-route.ts b/src/main/routes/kubeconfig-route.ts index 7fe5bcb9bc..bad5ecd57a 100644 --- a/src/main/routes/kubeconfig-route.ts +++ b/src/main/routes/kubeconfig-route.ts @@ -44,7 +44,7 @@ class KubeconfigRoute extends LensApi { public async routeServiceAccountRoute(request: LensApiRequest) { const { params, response, cluster} = request; - const client = cluster.getProxyKubeconfig().makeApiClient(CoreV1Api); + const client = (await cluster.getProxyKubeconfig()).makeApiClient(CoreV1Api); const secretList = await client.listNamespacedSecret(params.namespace); const secret = secretList.body.items.find(secret => { const { annotations } = secret.metadata; diff --git a/src/main/routes/port-forward-route.ts b/src/main/routes/port-forward-route.ts index 33c34758a4..0b5954948d 100644 --- a/src/main/routes/port-forward-route.ts +++ b/src/main/routes/port-forward-route.ts @@ -92,7 +92,7 @@ class PortForwardRoute extends LensApi { namespace, name: resourceName, port, - kubeConfig: cluster.getProxyKubeconfigPath() + kubeConfig: await cluster.getProxyKubeconfigPath() }); const started = await portForward.start(); diff --git a/src/main/shell-session.ts b/src/main/shell-session.ts index 40b3981d07..1582ede43e 100644 --- a/src/main/shell-session.ts +++ b/src/main/shell-session.ts @@ -6,7 +6,6 @@ import shellEnv from "shell-env"; import { app } from "electron"; import { Kubectl } from "./kubectl"; import { Cluster } from "./cluster"; -import { ClusterPreferences } from "../common/cluster-store"; import { helmCli } from "./helm/helm-cli"; import { isWindows } from "../common/vars"; import { appEventBus } from "../common/event-bus"; @@ -24,20 +23,18 @@ export class ShellSession extends EventEmitter { protected kubectlBinDir: string; protected kubectlPathDir: string; protected helmBinDir: string; - protected preferences: ClusterPreferences; protected running = false; - protected clusterId: string; + protected cluster: Cluster; constructor(socket: WebSocket, cluster: Cluster) { super(); this.websocket = socket; - this.kubeconfigPath = cluster.getProxyKubeconfigPath(); this.kubectl = new Kubectl(cluster.version); - this.preferences = cluster.preferences || {}; - this.clusterId = cluster.id; + this.cluster = cluster; } public async open() { + this.kubeconfigPath = await this.cluster.getProxyKubeconfigPath(); this.kubectlBinDir = await this.kubectl.binDir(); const pathFromPreferences = userStore.preferences.kubectlBinariesPath || this.kubectl.getBundledPath(); @@ -65,11 +62,13 @@ export class ShellSession extends EventEmitter { } protected cwd(): string { - if(!this.preferences || !this.preferences.terminalCWD || this.preferences.terminalCWD === "") { + const { preferences } = this.cluster; + + if(!preferences || !preferences.terminalCWD || preferences.terminalCWD === "") { return null; } - return this.preferences.terminalCWD; + return preferences.terminalCWD; } protected async getShellArgs(shell: string): Promise> { @@ -88,15 +87,17 @@ export class ShellSession extends EventEmitter { } protected async getCachedShellEnv() { - let env = ShellSession.shellEnvs.get(this.clusterId); + const { id: clusterId } = this.cluster; + + let env = ShellSession.shellEnvs.get(clusterId); if (!env) { env = await this.getShellEnv(); - ShellSession.shellEnvs.set(this.clusterId, env); + ShellSession.shellEnvs.set(clusterId, env); } else { // refresh env in the background this.getShellEnv().then((shellEnv: any) => { - ShellSession.shellEnvs.set(this.clusterId, shellEnv); + ShellSession.shellEnvs.set(clusterId, shellEnv); }); } @@ -107,6 +108,7 @@ export class ShellSession extends EventEmitter { const env = clearKubeconfigEnvVars(JSON.parse(JSON.stringify(await shellEnv()))); const pathStr = [this.kubectlBinDir, this.helmBinDir, process.env.PATH].join(path.delimiter); const shell = userStore.preferences.shell || process.env.SHELL || process.env.PTYSHELL; + const { preferences } = this.cluster; if(isWindows) { env["SystemRoot"] = process.env.SystemRoot; @@ -138,8 +140,8 @@ export class ShellSession extends EventEmitter { env["TERM_PROGRAM"] = app.getName(); env["TERM_PROGRAM_VERSION"] = app.getVersion(); - if (this.preferences.httpsProxy) { - env["HTTPS_PROXY"] = this.preferences.httpsProxy; + if (preferences.httpsProxy) { + env["HTTPS_PROXY"] = preferences.httpsProxy; } const no_proxy = ["localhost", "127.0.0.1", env["NO_PROXY"]]; diff --git a/src/main/window-manager.ts b/src/main/window-manager.ts index 0b7272331e..afd6b03670 100644 --- a/src/main/window-manager.ts +++ b/src/main/window-manager.ts @@ -8,6 +8,7 @@ import { initMenu } from "./menu"; import { initTray } from "./tray"; import { Singleton } from "../common/utils"; import { ClusterFrameInfo, clusterFrameMap } from "../common/cluster-frames"; +import { IpcRendererNavigationEvents } from "../renderer/navigation/events"; import logger from "./logger"; export class WindowManager extends Singleton { @@ -65,13 +66,13 @@ export class WindowManager extends Singleton { shell.openExternal(url); }); this.mainWindow.webContents.on("dom-ready", () => { - appEventBus.emit({name: "app", action: "dom-ready"}); + appEventBus.emit({ name: "app", action: "dom-ready" }); }); this.mainWindow.on("focus", () => { - appEventBus.emit({name: "app", action: "focus"}); + appEventBus.emit({ name: "app", action: "focus" }); }); this.mainWindow.on("blur", () => { - appEventBus.emit({name: "app", action: "blur"}); + appEventBus.emit({ name: "app", action: "blur" }); }); // clean up @@ -115,7 +116,7 @@ export class WindowManager extends Singleton { protected bindEvents() { // track visible cluster from ui - subscribeToBroadcast("cluster-view:current-id", (event, clusterId: ClusterId) => { + subscribeToBroadcast(IpcRendererNavigationEvents.CLUSTER_VIEW_CURRENT_ID, (event, clusterId: ClusterId) => { this.activeClusterId = clusterId; }); } @@ -139,7 +140,9 @@ export class WindowManager extends Singleton { await this.ensureMainWindow(); const frameInfo = Array.from(clusterFrameMap.values()).find((frameInfo) => frameInfo.frameId === frameId); - const channel = frameInfo ? "renderer:navigate-in-cluster" : "renderer:navigate"; + const channel = frameInfo + ? IpcRendererNavigationEvents.NAVIGATE_IN_CLUSTER + : IpcRendererNavigationEvents.NAVIGATE_IN_APP; this.sendToView({ channel, @@ -152,7 +155,7 @@ export class WindowManager extends Singleton { const frameInfo = clusterFrameMap.get(this.activeClusterId); if (frameInfo) { - this.sendToView({ channel: "renderer:reload", frameInfo }); + this.sendToView({ channel: IpcRendererNavigationEvents.RELOAD_PAGE, frameInfo }); } else { webContents.getFocusedWebContents()?.reload(); } diff --git a/src/migrations/user-store/file-name-migration.ts b/src/migrations/user-store/file-name-migration.ts new file mode 100644 index 0000000000..0abe6b280b --- /dev/null +++ b/src/migrations/user-store/file-name-migration.ts @@ -0,0 +1,22 @@ +import fse from "fs-extra"; +import { app, remote } from "electron"; +import path from "path"; + +export async function fileNameMigration() { + const userDataPath = (app || remote.app).getPath("userData"); + const configJsonPath = path.join(userDataPath, "config.json"); + const lensUserStoreJsonPath = path.join(userDataPath, "lens-user-store.json"); + + try { + await fse.move(configJsonPath, lensUserStoreJsonPath); + } catch (error) { + if (error.code === "ENOENT" && error.path === configJsonPath) { // (No such file or directory) + return; // file already moved + } else if (error.message === "dest already exists.") { + await fse.remove(configJsonPath); + } else { + // pass other errors along + throw error; + } + } +} diff --git a/src/migrations/user-store/index.ts b/src/migrations/user-store/index.ts index e1e7b8ffc9..5f5085b475 100644 --- a/src/migrations/user-store/index.ts +++ b/src/migrations/user-store/index.ts @@ -1,6 +1,11 @@ // User store migrations import version210Beta4 from "./2.1.0-beta.4"; +import { fileNameMigration } from "./file-name-migration"; + +export { + fileNameMigration +}; export default { ...version210Beta4, diff --git a/src/renderer/components/+cluster-settings/cluster-settings.tsx b/src/renderer/components/+cluster-settings/cluster-settings.tsx index 4ec8e1ccdd..40472be1ec 100644 --- a/src/renderer/components/+cluster-settings/cluster-settings.tsx +++ b/src/renderer/components/+cluster-settings/cluster-settings.tsx @@ -15,6 +15,7 @@ import { clusterStore } from "../../../common/cluster-store"; import { PageLayout } from "../layout/page-layout"; import { requestMain } from "../../../common/ipc"; import { clusterActivateHandler, clusterRefreshHandler } from "../../../common/cluster-ipc"; +import { navigation } from "../../navigation"; interface Props extends RouteComponentProps { } @@ -30,6 +31,10 @@ export class ClusterSettings extends React.Component { } componentDidMount() { + const { hash } = navigation.location; + + document.getElementById(hash.slice(1))?.scrollIntoView(); + disposeOnUnmount(this, [ reaction(() => this.cluster, this.refreshCluster, { fireImmediately: true, diff --git a/src/renderer/components/+cluster-settings/components/cluster-accessible-namespaces.tsx b/src/renderer/components/+cluster-settings/components/cluster-accessible-namespaces.tsx index b538a59a95..d1ad7dbeef 100644 --- a/src/renderer/components/+cluster-settings/components/cluster-accessible-namespaces.tsx +++ b/src/renderer/components/+cluster-settings/components/cluster-accessible-namespaces.tsx @@ -16,7 +16,7 @@ export class ClusterAccessibleNamespaces extends React.Component { render() { return ( <> - +

This setting is useful for manually specifying which namespaces you have access to. This is useful when you do not have permissions to list namespaces.

+ broadcastMessage(IpcRendererNavigationEvents.NAVIGATE_IN_APP, route); + +/** + * Creates handlers for high-level actions + * that could be performed on an individual cluster + * @param cluster Cluster + */ +export const ClusterActions = (cluster: Cluster) => ({ + showSettings: () => navigate(clusterSettingsURL({ + params: { clusterId: cluster.id } + })), + disconnect: async () => { + clusterStore.deactivate(cluster.id); + navigate(landingURL()); + await requestMain(clusterDisconnectHandler, cluster.id); + }, + remove: () => { + const tooltipId = uniqueId("tooltip_target_"); + + return ConfirmDialog.open({ + okButtonProps: { + primary: false, + accent: true, + label: "Remove" + }, + ok: () => { + clusterStore.deactivate(cluster.id); + clusterStore.removeById(cluster.id); + navigate(landingURL()); + }, + message:

+ Are you sure want to remove cluster {cluster.name}? + {cluster.id} +

+ }); + } +}); diff --git a/src/renderer/components/cluster-manager/clusters-menu.tsx b/src/renderer/components/cluster-manager/clusters-menu.tsx index dd36e529e1..03711e41fc 100644 --- a/src/renderer/components/cluster-manager/clusters-menu.tsx +++ b/src/renderer/components/cluster-manager/clusters-menu.tsx @@ -2,7 +2,6 @@ import "./clusters-menu.scss"; import React from "react"; import { remote } from "electron"; -import { requestMain } from "../../../common/ipc"; import type { Cluster } from "../../../main/cluster"; import { DragDropContext, Draggable, DraggableProvided, Droppable, DroppableProvided, DropResult } from "react-beautiful-dnd"; import { observer } from "mobx-react"; @@ -13,12 +12,10 @@ import { Icon } from "../icon"; import { autobind, cssNames, IClassName } from "../../utils"; import { isActiveRoute, navigate } from "../../navigation"; import { addClusterURL } from "../+add-cluster"; -import { clusterSettingsURL } from "../+cluster-settings"; import { landingURL } from "../+landing-page"; -import { ConfirmDialog } from "../confirm-dialog"; import { clusterViewURL } from "./cluster-view.route"; +import { ClusterActions } from "./cluster-actions"; import { getExtensionPageUrl, globalPageMenuRegistry, globalPageRegistry } from "../../../extensions/registries"; -import { clusterDisconnectHandler } from "../../../common/cluster-ipc"; import { commandRegistry } from "../../../extensions/registries/command-registry"; import { CommandOverlay } from "../command-palette/command-container"; import { computed, observable } from "mobx"; @@ -40,51 +37,24 @@ export class ClustersMenu extends React.Component { showContextMenu = (cluster: Cluster) => { const { Menu, MenuItem } = remote; const menu = new Menu(); + const actions = ClusterActions(cluster); menu.append(new MenuItem({ label: `Settings`, - click: () => { - navigate(clusterSettingsURL({ - params: { - clusterId: cluster.id - } - })); - } + click: actions.showSettings })); if (cluster.online) { menu.append(new MenuItem({ label: `Disconnect`, - click: async () => { - if (clusterStore.isActive(cluster.id)) { - navigate(landingURL()); - clusterStore.setActive(null); - } - await requestMain(clusterDisconnectHandler, cluster.id); - } + click: actions.disconnect })); } if (!cluster.isManaged) { menu.append(new MenuItem({ label: `Remove`, - click: () => { - ConfirmDialog.open({ - okButtonProps: { - primary: false, - accent: true, - label: `Remove`, - }, - ok: () => { - if (clusterStore.activeClusterId === cluster.id) { - navigate(landingURL()); - clusterStore.setActive(null); - } - clusterStore.removeById(cluster.id); - }, - message:

Are you sure want to remove cluster {cluster.contextName}?

, - }); - } + click: actions.remove })); } menu.popup({ diff --git a/src/renderer/components/cluster-manager/index.tsx b/src/renderer/components/cluster-manager/index.tsx index 692f1676ef..74595583aa 100644 --- a/src/renderer/components/cluster-manager/index.tsx +++ b/src/renderer/components/cluster-manager/index.tsx @@ -1 +1,2 @@ export * from "./cluster-manager"; +export * from "./cluster-actions"; diff --git a/src/renderer/components/dock/edit-resource.tsx b/src/renderer/components/dock/edit-resource.tsx index 236a63d923..b84d8f9103 100644 --- a/src/renderer/components/dock/edit-resource.tsx +++ b/src/renderer/components/dock/edit-resource.tsx @@ -1,7 +1,7 @@ import "./edit-resource.scss"; import React from "react"; -import { observable, when } from "mobx"; +import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; import jsYaml from "js-yaml"; import { IDockTab } from "./dock.store"; @@ -11,6 +11,7 @@ import { InfoPanel } from "./info-panel"; import { Badge } from "../badge"; import { EditorPanel } from "./editor-panel"; import { Spinner } from "../spinner"; +import { KubeObject } from "../../api/kube-object"; interface Props { className?: string; @@ -21,14 +22,6 @@ interface Props { export class EditResource extends React.Component { @observable error = ""; - async componentDidMount() { - await when(() => this.isReady); - - if (!this.tabData.draft) { - this.saveDraft(this.resource); // make initial dump to editor - } - } - get tabId() { return this.props.tab.id; } @@ -37,20 +30,32 @@ export class EditResource extends React.Component { return editResourceStore.isReady(this.tabId); } - get tabData() { - return editResourceStore.getData(this.tabId); - } - - get resource() { + get resource(): KubeObject | undefined { return editResourceStore.getResource(this.tabId); } - saveDraft(draft: string | object) { + @computed get draft(): string { + if (!this.isReady) { + return ""; // wait until tab's data and kube-object resource are loaded + } + + const { draft } = editResourceStore.getData(this.tabId); + + if (typeof draft === "string") { + return draft; + } + + return jsYaml.dump(this.resource); // dump resource first time + } + + @action + saveDraft(draft: string | KubeObject) { if (typeof draft === "object") { draft = draft ? jsYaml.dump(draft) : undefined; } + editResourceStore.setData(this.tabId, { - ...this.tabData, + ...editResourceStore.getData(this.tabId), draft, }); } @@ -64,9 +69,8 @@ export class EditResource extends React.Component { if (this.error) { return; } - const { draft } = this.tabData; const store = editResourceStore.getStore(this.tabId); - const updatedResource = await store.update(this.resource, jsYaml.safeLoad(draft)); + const updatedResource = await store.update(this.resource, jsYaml.safeLoad(this.draft)); this.saveDraft(updatedResource); // update with new resourceVersion to avoid further errors on save const resourceType = updatedResource.kind; @@ -80,14 +84,12 @@ export class EditResource extends React.Component { }; render() { - if (!this.isReady) { + const { tabId, error, onChange, save, draft, isReady, resource } = this; + + if (!isReady) { return ; } - const { tabId, error, onChange, save } = this; - const { kind, getNs, getName } = this.resource; - const { draft } = this.tabData; - return (
{ submittingMessage="Applying.." controls={(
- Kind: - Name: - Namespace: + Kind: + Name: + Namespace:
)} /> diff --git a/src/renderer/components/layout/__test__/main-layout-header.test.tsx b/src/renderer/components/layout/__test__/main-layout-header.test.tsx index 499839072c..1c5e381ce2 100644 --- a/src/renderer/components/layout/__test__/main-layout-header.test.tsx +++ b/src/renderer/components/layout/__test__/main-layout-header.test.tsx @@ -7,15 +7,19 @@ import "@testing-library/jest-dom/extend-expect"; import { MainLayoutHeader } from "../main-layout-header"; import { Cluster } from "../../../../main/cluster"; import { workspaceStore } from "../../../../common/workspace-store"; -import { broadcastMessage } from "../../../../common/ipc"; +import { broadcastMessage, requestMain } from "../../../../common/ipc"; +import { clusterDisconnectHandler } from "../../../../common/cluster-ipc"; +import { ConfirmDialog } from "../../confirm-dialog"; +import { IpcRendererNavigationEvents } from "../../../navigation/events"; const mockBroadcastIpc = broadcastMessage as jest.MockedFunction; +const mockRequestMain = requestMain as jest.MockedFunction; const cluster: Cluster = new Cluster({ id: "foo", contextName: "minikube", kubeConfigPath: "minikube-config.yml", - workspace: workspaceStore.currentWorkspaceId + workspace: workspaceStore.currentWorkspaceId, }); describe("", () => { @@ -25,20 +29,12 @@ describe("", () => { expect(container).toBeInstanceOf(HTMLElement); }); - it("renders gear icon", () => { + it("renders three dots icon", () => { const { container } = render(); const icon = container.querySelector(".Icon .icon"); expect(icon).toBeInstanceOf(HTMLElement); - expect(icon).toHaveTextContent("settings"); - }); - - it("navigates to cluster settings", () => { - const { container } = render(); - const icon = container.querySelector(".Icon"); - - fireEvent.click(icon); - expect(mockBroadcastIpc).toBeCalledWith("renderer:navigate", "/cluster/foo/settings"); + expect(icon).toHaveTextContent("more_vert"); }); it("renders cluster name", () => { @@ -46,4 +42,60 @@ describe("", () => { expect(getByText("minikube")).toBeInTheDocument(); }); + + describe("Cluster Actions Menu", () => { + let settingsBtn: Element; + let disconnectBtn: Element; + let removeBtn: Element; + + beforeEach(() => { + const { container } = render(
+ + +
); + const icon = container.querySelector(".Icon"); + + cluster.online = true; + fireEvent.click(icon); + + [settingsBtn, disconnectBtn, removeBtn] = Array.from(document.querySelectorAll("ul.ClusterActionsMenu > li")) + .map(el => el.querySelector("span")); + }); + + afterEach(() => { + cluster.online = false; + }); + + it("renders cluster menu items", () => { + expect(settingsBtn).toBeDefined(); + expect(settingsBtn.textContent).toBe("Settings"); + expect(disconnectBtn).toBeDefined(); + expect(disconnectBtn.textContent).toBe("Disconnect"); + expect(removeBtn).toBeDefined(); + expect(removeBtn.textContent).toBe("Remove"); + }); + + it("navigates to cluster settings", () => { + fireEvent.click(settingsBtn); + expect(mockBroadcastIpc).toBeCalledWith(IpcRendererNavigationEvents.NAVIGATE_IN_APP, "/cluster/foo/settings"); + }); + + it("disconnects from cluster", () => { + fireEvent.click(disconnectBtn); + expect(mockRequestMain).toBeCalledWith(clusterDisconnectHandler, cluster.id); + }); + + it("opens 'Remove cluster' dialog", async () => { + fireEvent.click(removeBtn); + + const dialog = document.querySelector(".ConfirmDialog"); + + expect(dialog).toBeDefined(); + expect(dialog).not.toBe(null); + + const okBtn = dialog.querySelector("button.ok"); + + expect(okBtn.textContent).toBe("Remove"); + }); + }); }); diff --git a/src/renderer/components/layout/main-layout-header.tsx b/src/renderer/components/layout/main-layout-header.tsx index 570c04a1f9..4fb6b69f9b 100644 --- a/src/renderer/components/layout/main-layout-header.tsx +++ b/src/renderer/components/layout/main-layout-header.tsx @@ -1,11 +1,10 @@ -import { observer } from "mobx-react"; import React from "react"; +import { observer } from "mobx-react"; -import { clusterSettingsURL } from "../+cluster-settings"; -import { broadcastMessage } from "../../../common/ipc"; +import { ClusterActions } from "../cluster-manager"; import { Cluster } from "../../../main/cluster"; import { cssNames } from "../../utils"; -import { Icon } from "../icon"; +import { MenuActions, MenuItem } from "../menu"; interface Props { cluster: Cluster @@ -13,21 +12,28 @@ interface Props { } export const MainLayoutHeader = observer(({ cluster, className }: Props) => { + const actions = ClusterActions(cluster); + const renderMenu = () => ( + + + Settings + + + Disconnect + + { + !cluster.isManaged && ( + + Remove + + ) + } + ); + return (
{cluster.name} - { - broadcastMessage("renderer:navigate", clusterSettingsURL({ - params: { - clusterId: cluster.id - } - })); - }} - /> + {renderMenu()}
); }); diff --git a/src/renderer/components/layout/sub-title.tsx b/src/renderer/components/layout/sub-title.tsx index 467fc40bbd..dd4afdba8d 100644 --- a/src/renderer/components/layout/sub-title.tsx +++ b/src/renderer/components/layout/sub-title.tsx @@ -6,19 +6,18 @@ interface Props { className?: string; title: React.ReactNode; compact?: boolean; // no bottom padding + id?: string; } export class SubTitle extends React.Component { render() { - const { compact, title, children } = this.props; - let { className } = this.props; - - className = cssNames("SubTitle", className, { + const { className, compact, title, children, id } = this.props; + const classNames = cssNames("SubTitle", className, { compact, }); return ( -
+
{title} {children}
); diff --git a/src/renderer/ipc/index.tsx b/src/renderer/ipc/index.tsx index 5f5e04d9d2..ccbeef4797 100644 --- a/src/renderer/ipc/index.tsx +++ b/src/renderer/ipc/index.tsx @@ -1,10 +1,13 @@ import React from "react"; import { ipcRenderer, IpcRendererEvent } from "electron"; -import { areArgsUpdateAvailableFromMain, UpdateAvailableChannel, onCorrect, UpdateAvailableFromMain, BackchannelArg } from "../../common/ipc"; +import { areArgsUpdateAvailableFromMain, UpdateAvailableChannel, onCorrect, UpdateAvailableFromMain, BackchannelArg, ClusterListNamespaceForbiddenChannel, isListNamespaceForbiddenArgs, ListNamespaceForbiddenArgs } from "../../common/ipc"; import { Notifications, notificationsStore } from "../components/notifications"; import { Button } from "../components/button"; import { isMac } from "../../common/vars"; import { invalidKubeconfigHandler } from "./invalid-kubeconfig-handler"; +import { clusterStore } from "../../common/cluster-store"; +import { navigate } from "../navigation"; +import { clusterSettingsURL } from "../components/+cluster-settings"; function sendToBackchannel(backchannel: string, notificationId: string, data: BackchannelArg): void { notificationsStore.remove(notificationId); @@ -42,7 +45,8 @@ function UpdateAvailableHandler(event: IpcRendererEvent, ...[backchannel, update
- ), { + ), + { id: notificationId, onClose() { sendToBackchannel(backchannel, notificationId, { doUpdate: false }); @@ -51,6 +55,42 @@ function UpdateAvailableHandler(event: IpcRendererEvent, ...[backchannel, update ); } +const listNamespacesForbiddenHandlerDisplayedAt = new Map(); +const intervalBetweenNotifications = 1000 * 60; // 60s + +function ListNamespacesForbiddenHandler(event: IpcRendererEvent, ...[clusterId]: ListNamespaceForbiddenArgs): void { + const lastDisplayedAt = listNamespacesForbiddenHandlerDisplayedAt.get(clusterId); + const wasDisplayed = Boolean(lastDisplayedAt); + const now = Date.now(); + + if (!wasDisplayed || (now - lastDisplayedAt) > intervalBetweenNotifications) { + listNamespacesForbiddenHandlerDisplayedAt.set(clusterId, now); + } else { + // don't bother the user too often + return; + } + + const notificationId = `list-namespaces-forbidden:${clusterId}`; + + Notifications.info( + ( +
+ Add Accessible Namespaces +

Cluster {clusterStore.active.name} does not have permissions to list namespaces. Please add the namespaces you have access to.

+
+
+
+ ), + { + id: notificationId, + } + ); +} + export function registerIpcHandlers() { onCorrect({ source: ipcRenderer, @@ -59,4 +99,10 @@ export function registerIpcHandlers() { verifier: areArgsUpdateAvailableFromMain, }); onCorrect(invalidKubeconfigHandler); + onCorrect({ + source: ipcRenderer, + channel: ClusterListNamespaceForbiddenChannel, + listener: ListNamespacesForbiddenHandler, + verifier: isListNamespaceForbiddenArgs, + }); } diff --git a/src/renderer/lens-app.tsx b/src/renderer/lens-app.tsx index 74aff3781a..1e592c02a5 100644 --- a/src/renderer/lens-app.tsx +++ b/src/renderer/lens-app.tsx @@ -16,6 +16,7 @@ import { LensProtocolRouterRenderer, bindProtocolAddRouteHandlers } from "./prot import { registerIpcHandlers } from "./ipc"; import { ipcRenderer } from "electron"; import { CacheProvider } from "@emotion/react"; +import { IpcRendererNavigationEvents } from "./navigation/events"; import createCache from "@emotion/cache"; @observer @@ -32,7 +33,7 @@ export class LensApp extends React.Component { }); registerIpcHandlers(); - ipcRenderer.send("renderer:loaded"); + ipcRenderer.send(IpcRendererNavigationEvents.LOADED); } render() { diff --git a/src/renderer/navigation/events.ts b/src/renderer/navigation/events.ts index 646b1ba415..53c685c9f4 100644 --- a/src/renderer/navigation/events.ts +++ b/src/renderer/navigation/events.ts @@ -4,6 +4,14 @@ import { getMatchedClusterId, navigate } from "./helpers"; import { broadcastMessage, subscribeToBroadcast } from "../../common/ipc"; import logger from "../../main/logger"; +export const enum IpcRendererNavigationEvents { + RELOAD_PAGE = "renderer:page-reload", + CLUSTER_VIEW_CURRENT_ID = "renderer:cluster-id-of-active-view", + NAVIGATE_IN_APP = "renderer:navigate", + NAVIGATE_IN_CLUSTER = "renderer:navigate-in-cluster", + LOADED = "renderer:loaded", +} + export function bindEvents() { if (!ipcRenderer) { return; @@ -16,7 +24,7 @@ export function bindEvents() { } // Reload dashboard window - subscribeToBroadcast("renderer:reload", () => { + subscribeToBroadcast(IpcRendererNavigationEvents.RELOAD_PAGE, () => { location.reload(); }); } @@ -25,13 +33,13 @@ export function bindEvents() { function bindClusterManagerRouteEvents() { // Keep track of active cluster-id for handling IPC/menus/etc. reaction(() => getMatchedClusterId(), clusterId => { - broadcastMessage("cluster-view:current-id", clusterId); + broadcastMessage(IpcRendererNavigationEvents.CLUSTER_VIEW_CURRENT_ID, clusterId); }, { fireImmediately: true }); // Handle navigation via IPC - subscribeToBroadcast("renderer:navigate", (event, url: string) => { + subscribeToBroadcast(IpcRendererNavigationEvents.NAVIGATE_IN_APP, (event, url: string) => { logger.info(`[IPC]: ${event.type}: ${url}`, { currentLocation: location.href }); navigate(url); }); @@ -39,7 +47,7 @@ function bindClusterManagerRouteEvents() { // Handle cluster-view renderer process events within iframes function bindClusterFrameRouteEvents() { - subscribeToBroadcast("renderer:navigate-cluster-view", (event, url: string) => { + subscribeToBroadcast(IpcRendererNavigationEvents.NAVIGATE_IN_CLUSTER, (event, url: string) => { logger.info(`[IPC]: ${event.type}: ${url}`, { currentLocation: location.href }); navigate(url); }); diff --git a/troubleshooting/custom-prometheus.md b/troubleshooting/custom-prometheus.md index 6bab29b963..3f9b746394 100644 --- a/troubleshooting/custom-prometheus.md +++ b/troubleshooting/custom-prometheus.md @@ -31,23 +31,14 @@ metricRelabelings: ### Jsonnet -The required label replacements are bundled in [jsonnet/custom-prometheus](../jsonnet/custom-prometheus.jsonnet). To install it copy the file or use -[Jsonnet Bundler](https://github.com/jsonnet-bundler/jsonnet-bundler). For jsonnet bundler add the following dependency to your `jsonnetfile.json`: +The required label replacements are bundled in [jsonnet/lens/custom-prometheus](../jsonnet/lens/custom-prometheus.jsonnet). To install it copy the file or use +[Jsonnet Bundler](https://github.com/jsonnet-bundler/jsonnet-bundler). -``` -{ - "name": "lens", - "source": { - "git": { - "remote": "https://github.com/lensapp/lens", - "subdir": "jsonnet" - } - }, - "version": "master" -} +```bash +jb init && jb install https://github.com/lensapp/lens/jsonnet/lens@master ``` -and run `jb install`. When the installation was successful include it into your definitions. Using the [example](https://github.com/coreos/kube-prometheus#compiling) +When the installation was successful include it into your definitions. Using the [example](https://github.com/coreos/kube-prometheus#compiling) of kube-prometheus, e.g.: ```