mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
window-manager refactoring
Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
parent
ce1cccc965
commit
cac4896517
@ -1,14 +1,15 @@
|
|||||||
|
import url from "url"
|
||||||
import type { ClusterId, ClusterModel, ClusterPreferences } from "../common/cluster-store"
|
import type { ClusterId, ClusterModel, ClusterPreferences } from "../common/cluster-store"
|
||||||
import type { FeatureStatusMap } from "./feature"
|
import type { FeatureStatusMap } from "./feature"
|
||||||
import { observable, toJS } from "mobx";
|
import { computed, observable, toJS } from "mobx";
|
||||||
import { apiPrefix } from "../common/vars";
|
import { apiPrefix } from "../common/vars";
|
||||||
import { ContextHandler } from "./context-handler"
|
import { ContextHandler } from "./context-handler"
|
||||||
import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"
|
import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"
|
||||||
import { Kubectl } from "./kubectl";
|
import { Kubectl } from "./kubectl";
|
||||||
import { KubeconfigManager } from "./kubeconfig-manager"
|
import { KubeconfigManager } from "./kubeconfig-manager"
|
||||||
import { getNodeWarningConditions, loadConfig, podHasIssues } from "./k8s"
|
import { getNodeWarningConditions, podHasIssues } from "./k8s"
|
||||||
import { getFeatures, installFeature, uninstallFeature, upgradeFeature } from "./feature-manager";
|
import { getFeatures, installFeature, uninstallFeature, upgradeFeature } from "./feature-manager";
|
||||||
import request from "request-promise-native"
|
import request, { RequestPromiseOptions } from "request-promise-native"
|
||||||
import logger from "./logger"
|
import logger from "./logger"
|
||||||
|
|
||||||
enum ClusterStatus {
|
enum ClusterStatus {
|
||||||
@ -43,7 +44,6 @@ export class Cluster implements ClusterModel {
|
|||||||
@observable contextName: string;
|
@observable contextName: string;
|
||||||
@observable url: string;
|
@observable url: string;
|
||||||
@observable port: number;
|
@observable port: number;
|
||||||
@observable apiUrl: string;
|
|
||||||
@observable online: boolean;
|
@observable online: boolean;
|
||||||
@observable accessible: boolean;
|
@observable accessible: boolean;
|
||||||
@observable failureReason: string;
|
@observable failureReason: string;
|
||||||
@ -63,21 +63,28 @@ export class Cluster implements ClusterModel {
|
|||||||
Object.assign(this, model)
|
Object.assign(this, model)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@computed get apiUrl() {
|
||||||
|
return url.parse(`http://${this.id}.localhost:${this.port}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed get apiServerUrl() {
|
||||||
|
return url.parse(`http://127.0.0.1:${this.port}${apiPrefix.KUBE_BASE}`)
|
||||||
|
}
|
||||||
|
|
||||||
async init(port: number) {
|
async init(port: number) {
|
||||||
const { contextName } = this
|
|
||||||
try {
|
try {
|
||||||
const kubeConfig = loadConfig(this.kubeConfigPath)
|
|
||||||
kubeConfig.setCurrentContext(contextName); // fixme: is it required, when if so?
|
|
||||||
this.port = port;
|
this.port = port;
|
||||||
this.apiUrl = kubeConfig.getCurrentCluster().server
|
this.contextHandler = new ContextHandler(this);
|
||||||
this.contextHandler = new ContextHandler(kubeConfig, this)
|
|
||||||
await this.contextHandler.init() // So we get the proxy port reserved
|
await this.contextHandler.init() // So we get the proxy port reserved
|
||||||
this.kubeconfigManager = new KubeconfigManager(this)
|
this.kubeconfigManager = new KubeconfigManager(this)
|
||||||
this.url = this.contextHandler.url
|
this.url = this.contextHandler.url
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
logger.debug(`[CLUSTER]: init done for "${this.id}", context ${contextName}`);
|
logger.debug(`[CLUSTER]: init done (id="${this.id}", context="${this.contextName}")`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`[CLUSTER]: init "${this.id}" has failed`, { err, contextName });
|
logger.error(`[CLUSTER]: init failed (id="${this.id}")`, {
|
||||||
|
contextName: this.contextName,
|
||||||
|
error: err
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,16 +139,15 @@ export class Cluster implements ClusterModel {
|
|||||||
return this.preferences.prometheus?.prefix || ""
|
return this.preferences.prometheus?.prefix || ""
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async k8sRequest(path: string, opts?: request.RequestPromiseOptions) {
|
k8sRequest(path: string, options: RequestPromiseOptions = {}) {
|
||||||
const options = Object.assign({
|
return request(this.apiServerUrl + path, {
|
||||||
json: true,
|
json: true,
|
||||||
timeout: 10000
|
timeout: 10000,
|
||||||
}, (opts || {}))
|
headers: {
|
||||||
if (!options.headers) {
|
...(options.headers || {}),
|
||||||
options.headers = {}
|
host: this.apiUrl.host,
|
||||||
}
|
}
|
||||||
options.headers.host = `${this.id}.localhost:${this.port}`
|
})
|
||||||
return request(`http://127.0.0.1:${this.port}${apiPrefix.KUBE_BASE}${path}`, options)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async getConnectionStatus() {
|
protected async getConnectionStatus() {
|
||||||
@ -202,8 +208,8 @@ export class Cluster implements ClusterModel {
|
|||||||
if (kubernetesVersion.includes("gke")) return "gke"
|
if (kubernetesVersion.includes("gke")) return "gke"
|
||||||
if (kubernetesVersion.includes("eks")) return "eks"
|
if (kubernetesVersion.includes("eks")) return "eks"
|
||||||
if (kubernetesVersion.includes("IKS")) return "iks"
|
if (kubernetesVersion.includes("IKS")) return "iks"
|
||||||
if (apiUrl.endsWith("azmk8s.io")) return "aks"
|
if (apiUrl.href.endsWith("azmk8s.io")) return "aks"
|
||||||
if (apiUrl.endsWith("k8s.ondigitalocean.com")) return "digitalocean"
|
if (apiUrl.href.endsWith("k8s.ondigitalocean.com")) return "digitalocean"
|
||||||
if (contextName.startsWith("minikube")) return "minikube"
|
if (contextName.startsWith("minikube")) return "minikube"
|
||||||
if (kubernetesVersion.includes("+")) return "custom"
|
if (kubernetesVersion.includes("+")) return "custom"
|
||||||
return "vanilla"
|
return "vanilla"
|
||||||
@ -271,7 +277,7 @@ export class Cluster implements ClusterModel {
|
|||||||
return toJS({
|
return toJS({
|
||||||
...storeModel,
|
...storeModel,
|
||||||
url: this.url,
|
url: this.url,
|
||||||
apiUrl: this.apiUrl,
|
apiUrl: this.apiUrl.href,
|
||||||
online: this.online,
|
online: this.online,
|
||||||
accessible: this.accessible,
|
accessible: this.accessible,
|
||||||
failureReason: this.failureReason,
|
failureReason: this.failureReason,
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { CoreV1Api, KubeConfig } from "@kubernetes/client-node"
|
import { CoreV1Api } from "@kubernetes/client-node"
|
||||||
import { ServerOptions } from "http-proxy"
|
import { ServerOptions } from "http-proxy"
|
||||||
import * as url from "url"
|
|
||||||
import logger from "./logger"
|
import logger from "./logger"
|
||||||
import { getFreePort } from "./port"
|
import { getFreePort } from "./port"
|
||||||
import { KubeAuthProxy } from "./kube-auth-proxy"
|
import { KubeAuthProxy } from "./kube-auth-proxy"
|
||||||
@ -15,31 +14,21 @@ export class ContextHandler {
|
|||||||
public contextName: string
|
public contextName: string
|
||||||
|
|
||||||
protected id: string
|
protected id: string
|
||||||
protected clusterUrl: url.UrlWithStringQuery
|
|
||||||
protected proxyServer: KubeAuthProxy
|
protected proxyServer: KubeAuthProxy
|
||||||
|
protected apiTarget: ServerOptions
|
||||||
protected certData: string
|
protected certData: string
|
||||||
protected authCertData: string
|
protected authCertData: string
|
||||||
protected cluster: Cluster
|
|
||||||
protected apiTarget: ServerOptions
|
|
||||||
protected proxyTarget: ServerOptions
|
protected proxyTarget: ServerOptions
|
||||||
protected clientCert: string
|
protected clientCert: string
|
||||||
protected clientKey: string
|
protected clientKey: string
|
||||||
protected secureApiConnection = true
|
|
||||||
protected defaultNamespace: string
|
|
||||||
protected kubernetesApi: string
|
|
||||||
protected prometheusProvider: string
|
protected prometheusProvider: string
|
||||||
protected prometheusPath: string
|
protected prometheusPath: string
|
||||||
protected clusterName: string
|
protected clusterName: string
|
||||||
|
|
||||||
constructor(kc: KubeConfig, cluster: Cluster) {
|
constructor(protected cluster: Cluster) {
|
||||||
this.id = cluster.id
|
this.id = cluster.id
|
||||||
this.cluster = cluster
|
this.url = cluster.apiUrl.href;
|
||||||
this.clusterUrl = url.parse(cluster.apiUrl)
|
|
||||||
this.contextName = cluster.contextName;
|
this.contextName = cluster.contextName;
|
||||||
this.defaultNamespace = kc.getContextObject(cluster.contextName).namespace
|
|
||||||
this.url = `http://${this.id}.localhost:${cluster.port}/`
|
|
||||||
this.kubernetesApi = `http://127.0.0.1:${cluster.port}/${this.id}`
|
|
||||||
|
|
||||||
this.setClusterPreferences(cluster.preferences)
|
this.setClusterPreferences(cluster.preferences)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,16 +68,12 @@ export class ContextHandler {
|
|||||||
return await provider.getPrometheusService(apiClient)
|
return await provider.getPrometheusService(apiClient)
|
||||||
})
|
})
|
||||||
const resolvedPrometheusServices = await Promise.all(prometheusPromises)
|
const resolvedPrometheusServices = await Promise.all(prometheusPromises)
|
||||||
const service = resolvedPrometheusServices.filter(n => n)[0]
|
const service = resolvedPrometheusServices.filter(n => n)[0];
|
||||||
if (service) {
|
return service || {
|
||||||
return service
|
id: "lens",
|
||||||
} else {
|
namespace: "lens-metrics",
|
||||||
return {
|
service: "prometheus",
|
||||||
id: "lens",
|
port: 80
|
||||||
namespace: "lens-metrics",
|
|
||||||
service: "prometheus",
|
|
||||||
port: 80
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,17 +97,18 @@ export class ContextHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async newApiTarget(timeout: number): Promise<ServerOptions> {
|
protected async newApiTarget(timeout: number): Promise<ServerOptions> {
|
||||||
|
const clusterUrl = this.cluster.apiUrl;
|
||||||
return {
|
return {
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
timeout: timeout,
|
timeout: timeout,
|
||||||
headers: {
|
headers: {
|
||||||
"Host": this.clusterUrl.hostname
|
"Host": clusterUrl.hostname
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
port: await this.resolveProxyPort(),
|
port: await this.resolveProxyPort(),
|
||||||
protocol: "http://",
|
protocol: "http://",
|
||||||
host: "localhost",
|
host: "localhost",
|
||||||
path: this.clusterUrl.path
|
path: clusterUrl.path,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import "../common/prometheus-providers"
|
|||||||
import { app, dialog } from "electron"
|
import { app, dialog } from "electron"
|
||||||
import { appName, appProto, isMac, staticDir, staticProto } from "../common/vars";
|
import { appName, appProto, isMac, staticDir, staticProto } from "../common/vars";
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { format as formatUrl } from "url"
|
|
||||||
import initMenu from "./menu"
|
import initMenu from "./menu"
|
||||||
import { LensProxy, listen } from "./proxy"
|
import { LensProxy, listen } from "./proxy"
|
||||||
import { WindowManager } from "./window-manager";
|
import { WindowManager } from "./window-manager";
|
||||||
@ -25,12 +24,6 @@ let windowManager: WindowManager;
|
|||||||
let clusterManager: ClusterManager;
|
let clusterManager: ClusterManager;
|
||||||
let proxyServer: LensProxy;
|
let proxyServer: LensProxy;
|
||||||
|
|
||||||
const vmURL = formatUrl({
|
|
||||||
pathname: path.join(__dirname, `${appName}.html`),
|
|
||||||
protocol: "file",
|
|
||||||
slashes: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
mangleProxyEnv()
|
mangleProxyEnv()
|
||||||
if (app.commandLine.getSwitchValue("proxy-server") !== "") {
|
if (app.commandLine.getSwitchValue("proxy-server") !== "") {
|
||||||
process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server")
|
process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server")
|
||||||
@ -81,9 +74,9 @@ async function main() {
|
|||||||
app.quit();
|
app.quit();
|
||||||
}
|
}
|
||||||
|
|
||||||
// manage lens windows
|
// create window manager and open app
|
||||||
windowManager = new WindowManager({showSplash: true});
|
windowManager = new WindowManager();
|
||||||
windowManager.loadURL(vmURL)
|
windowManager.showSplash();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Events
|
// Events
|
||||||
@ -95,19 +88,15 @@ app.on('window-all-closed', function () {
|
|||||||
if (!isMac) {
|
if (!isMac) {
|
||||||
app.quit();
|
app.quit();
|
||||||
} else {
|
} else {
|
||||||
windowManager = null
|
// windowManager.destroy();
|
||||||
if (clusterManager) clusterManager.stop()
|
// clusterManager.stop()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
app.on("activate", () => {
|
app.on("activate", (event, hasVisibleWindows) => {
|
||||||
if (!windowManager) {
|
// todo: something
|
||||||
windowManager = new WindowManager()
|
|
||||||
windowManager.loadURL(vmURL)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// fixme: app can't quit normally (Cmd+W/Q not working)
|
|
||||||
app.on("will-quit", async (event) => {
|
app.on("will-quit", async (event) => {
|
||||||
event.preventDefault(); // To allow mixpanel sending to be executed
|
event.preventDefault(); // To allow mixpanel sending to be executed
|
||||||
if (clusterManager) clusterManager.stop()
|
if (clusterManager) clusterManager.stop()
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import k8s from "@kubernetes/client-node"
|
import { KubeConfig, V1Node, V1Pod } from "@kubernetes/client-node"
|
||||||
import os from "os"
|
import os from "os"
|
||||||
import yaml from "js-yaml"
|
import yaml from "js-yaml"
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
@ -10,8 +10,8 @@ function resolveTilde(filePath: string) {
|
|||||||
return filePath;
|
return filePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadConfig(kubeConfigPath?: string): k8s.KubeConfig {
|
export function loadConfig(kubeConfigPath?: string): KubeConfig {
|
||||||
const kc = new k8s.KubeConfig()
|
const kc = new KubeConfig()
|
||||||
if (kubeConfigPath) {
|
if (kubeConfigPath) {
|
||||||
kc.loadFromFile(resolveTilde(kubeConfigPath))
|
kc.loadFromFile(resolveTilde(kubeConfigPath))
|
||||||
} else {
|
} else {
|
||||||
@ -27,38 +27,35 @@ export function loadConfig(kubeConfigPath?: string): k8s.KubeConfig {
|
|||||||
* - Context
|
* - Context
|
||||||
* @param config KubeConfig to check
|
* @param config KubeConfig to check
|
||||||
*/
|
*/
|
||||||
export function validateConfig(config: k8s.KubeConfig | string): k8s.KubeConfig {
|
export function validateConfig(config: KubeConfig | string): KubeConfig {
|
||||||
if(typeof config == "string") {
|
if (typeof config == "string") {
|
||||||
config = loadConfig(config);
|
config = loadConfig(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(`validating kube config: ${JSON.stringify(config)}`)
|
logger.debug(`validating kube config: ${JSON.stringify(config)}`)
|
||||||
if(!config.users || config.users.length == 0) {
|
if (!config.users || config.users.length == 0) {
|
||||||
throw new Error("No users provided in config")
|
throw new Error("No users provided in config")
|
||||||
}
|
}
|
||||||
if(!config.clusters || config.clusters.length == 0) {
|
if (!config.clusters || config.clusters.length == 0) {
|
||||||
throw new Error("No clusters provided in config")
|
throw new Error("No clusters provided in config")
|
||||||
}
|
}
|
||||||
if(!config.contexts || config.contexts.length == 0) {
|
if (!config.contexts || config.contexts.length == 0) {
|
||||||
throw new Error("No contexts provided in config")
|
throw new Error("No contexts provided in config")
|
||||||
}
|
}
|
||||||
|
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Breaks kube config into several configs. Each context as it own KubeConfig object
|
* Breaks kube config into several configs. Each context as it own KubeConfig object
|
||||||
*
|
|
||||||
* @param configString yaml string of kube config
|
|
||||||
*/
|
*/
|
||||||
export function splitConfig(kubeConfig: k8s.KubeConfig): k8s.KubeConfig[] {
|
export function splitConfig(kubeConfig: KubeConfig): KubeConfig[] {
|
||||||
const configs: k8s.KubeConfig[] = []
|
const configs: KubeConfig[] = []
|
||||||
if(!kubeConfig.contexts) {
|
if (!kubeConfig.contexts) {
|
||||||
return configs;
|
return configs;
|
||||||
}
|
}
|
||||||
kubeConfig.contexts.forEach(ctx => {
|
kubeConfig.contexts.forEach(ctx => {
|
||||||
const kc = new k8s.KubeConfig();
|
const kc = new KubeConfig();
|
||||||
kc.clusters = [kubeConfig.getCluster(ctx.cluster)].filter(n => n);
|
kc.clusters = [kubeConfig.getCluster(ctx.cluster)].filter(n => n);
|
||||||
kc.users = [kubeConfig.getUser(ctx.user)].filter(n => n)
|
kc.users = [kubeConfig.getUser(ctx.user)].filter(n => n)
|
||||||
kc.contexts = [kubeConfig.getContextObject(ctx.name)].filter(n => n)
|
kc.contexts = [kubeConfig.getContextObject(ctx.name)].filter(n => n)
|
||||||
@ -74,13 +71,13 @@ export function splitConfig(kubeConfig: k8s.KubeConfig): k8s.KubeConfig[] {
|
|||||||
*
|
*
|
||||||
* @param configPath path to kube config yaml file
|
* @param configPath path to kube config yaml file
|
||||||
*/
|
*/
|
||||||
export function loadAndSplitConfig(configPath: string): k8s.KubeConfig[] {
|
export function loadAndSplitConfig(configPath: string): KubeConfig[] {
|
||||||
const allConfigs = new k8s.KubeConfig();
|
const allConfigs = new KubeConfig();
|
||||||
allConfigs.loadFromFile(configPath);
|
allConfigs.loadFromFile(configPath);
|
||||||
return splitConfig(allConfigs);
|
return splitConfig(allConfigs);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function dumpConfigYaml(kc: k8s.KubeConfig): string {
|
export function dumpConfigYaml(kc: KubeConfig): string {
|
||||||
const config = {
|
const config = {
|
||||||
apiVersion: "v1",
|
apiVersion: "v1",
|
||||||
kind: "Config",
|
kind: "Config",
|
||||||
@ -128,10 +125,10 @@ export function dumpConfigYaml(kc: k8s.KubeConfig): string {
|
|||||||
console.log("dumping kc:", config);
|
console.log("dumping kc:", config);
|
||||||
|
|
||||||
// skipInvalid: true makes dump ignore undefined values
|
// skipInvalid: true makes dump ignore undefined values
|
||||||
return yaml.safeDump(config, {skipInvalid: true});
|
return yaml.safeDump(config, { skipInvalid: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function podHasIssues(pod: k8s.V1Pod) {
|
export function podHasIssues(pod: V1Pod) {
|
||||||
// Logic adapted from dashboard
|
// Logic adapted from dashboard
|
||||||
const notReady = !!pod.status.conditions.find(condition => {
|
const notReady = !!pod.status.conditions.find(condition => {
|
||||||
return condition.type == "Ready" && condition.status !== "True"
|
return condition.type == "Ready" && condition.status !== "True"
|
||||||
@ -146,7 +143,7 @@ export function podHasIssues(pod: k8s.V1Pod) {
|
|||||||
|
|
||||||
// Logic adapted from dashboard
|
// Logic adapted from dashboard
|
||||||
// see: https://github.com/kontena/kontena-k8s-dashboard/blob/7d8f9cb678cc817a22dd1886c5e79415b212b9bf/client/api/endpoints/nodes.api.ts#L147
|
// see: https://github.com/kontena/kontena-k8s-dashboard/blob/7d8f9cb678cc817a22dd1886c5e79415b212b9bf/client/api/endpoints/nodes.api.ts#L147
|
||||||
export function getNodeWarningConditions(node: k8s.V1Node) {
|
export function getNodeWarningConditions(node: V1Node) {
|
||||||
return node.status.conditions.filter(c =>
|
return node.status.conditions.filter(c =>
|
||||||
c.status.toLowerCase() === "true" && c.type !== "Ready" && c.type !== "HostUpgrades"
|
c.status.toLowerCase() === "true" && c.type !== "Ready" && c.type !== "HostUpgrades"
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { app } from "electron"
|
import { app } from "electron"
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import { ensureDir, randomFileName} from "./file-helpers"
|
import { ensureDir, randomFileName } from "./file-helpers"
|
||||||
import logger from "./logger"
|
import logger from "./logger"
|
||||||
import { Cluster } from "./cluster"
|
import { Cluster } from "./cluster"
|
||||||
import * as k8s from "./k8s"
|
import { dumpConfigYaml } from "./k8s"
|
||||||
import { KubeConfig } from "@kubernetes/client-node"
|
import { KubeConfig } from "@kubernetes/client-node"
|
||||||
|
|
||||||
export class KubeconfigManager {
|
export class KubeconfigManager {
|
||||||
@ -21,37 +21,35 @@ export class KubeconfigManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates new "temporary" kubeconfig that point to the kubectl-proxy.
|
* Creates new "temporary" kubeconfig that point to the kubectl-proxy.
|
||||||
* This way any user of the config does not need to know anything about the auth etc. details.
|
* This way any user of the config does not need to know anything about the auth etc. details.
|
||||||
*/
|
*/
|
||||||
protected createTemporaryKubeconfig(): string {
|
protected createTemporaryKubeconfig(): string {
|
||||||
ensureDir(this.configDir)
|
ensureDir(this.configDir);
|
||||||
const path = `${this.configDir}/${randomFileName("kubeconfig")}`
|
const path = `${this.configDir}/${randomFileName("kubeconfig")}`
|
||||||
const originalKc = new KubeConfig()
|
const { contextName, contextHandler, kubeConfigPath } = this.cluster;
|
||||||
originalKc.loadFromFile(this.cluster.kubeConfigPath)
|
const kubeConfig = new KubeConfig()
|
||||||
const kc = {
|
kubeConfig.loadFromFile(kubeConfigPath)
|
||||||
clusters: [
|
kubeConfig.clusters = [
|
||||||
{
|
{
|
||||||
name: this.cluster.contextName,
|
name: contextName,
|
||||||
server: `http://127.0.0.1:${this.cluster.contextHandler.proxyPort}`
|
server: `http://127.0.0.1:${contextHandler.proxyPort}`,
|
||||||
}
|
skipTLSVerify: true,
|
||||||
],
|
}
|
||||||
users: [
|
];
|
||||||
{
|
kubeConfig.users = [
|
||||||
name: "proxy"
|
{ name: "proxy" },
|
||||||
}
|
];
|
||||||
],
|
kubeConfig.currentContext = contextName;
|
||||||
contexts: [
|
kubeConfig.contexts = [
|
||||||
{
|
{
|
||||||
name: this.cluster.contextName,
|
name: contextName,
|
||||||
cluster: this.cluster.contextName,
|
cluster: contextName,
|
||||||
namespace: originalKc.getContextObject(this.cluster.contextName).namespace,
|
namespace: kubeConfig.getContextObject(contextName).namespace,
|
||||||
user: "proxy"
|
user: "proxy"
|
||||||
}
|
}
|
||||||
],
|
];
|
||||||
currentContext: this.cluster.contextName
|
fs.writeFileSync(path, dumpConfigYaml(kubeConfig));
|
||||||
} as KubeConfig
|
|
||||||
fs.writeFileSync(path, k8s.dumpConfigYaml(kc))
|
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,14 +4,10 @@ import http from "http"
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import { readFile } from "fs-extra"
|
import { readFile } from "fs-extra"
|
||||||
import { Cluster } from "./cluster"
|
import { Cluster } from "./cluster"
|
||||||
import { configRoute } from "./routes/config"
|
|
||||||
import { helmApi } from "./helm-api"
|
import { helmApi } from "./helm-api"
|
||||||
import { resourceApplierApi } from "./resource-applier-api"
|
import { resourceApplierApi } from "./resource-applier-api"
|
||||||
import { kubeconfigRoute } from "./routes/kubeconfig"
|
import { apiPrefix, appName, outDir } from "../common/vars";
|
||||||
import { metricsRoute } from "./routes/metrics"
|
import { configRoute, kubeconfigRoute, metricsRoute, portForwardRoute, watchRoute } from "./routes";
|
||||||
import { watchRoute } from "./routes/watch"
|
|
||||||
import { portForwardRoute } from "./routes/port-forward"
|
|
||||||
import { apiPrefix, outDir, appName } from "../common/vars";
|
|
||||||
|
|
||||||
const mimeTypes: Record<string, string> = {
|
const mimeTypes: Record<string, string> = {
|
||||||
"html": "text/html",
|
"html": "text/html",
|
||||||
@ -51,13 +47,13 @@ export class Router {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async route(cluster: Cluster, req: http.IncomingMessage, res: http.ServerResponse) {
|
public async route(cluster: Cluster, req: http.IncomingMessage, res: http.ServerResponse) {
|
||||||
const url = new URL(req.url, "http://localhost")
|
const reqUrl = new URL(req.url, "http://localhost")
|
||||||
const path = url.pathname
|
const path = reqUrl.pathname
|
||||||
const method = req.method.toLowerCase()
|
const method = req.method.toLowerCase()
|
||||||
const matchingRoute = this.router.route(method, path)
|
const matchingRoute = this.router.route(method, path)
|
||||||
|
|
||||||
if (matchingRoute.isBoom !== true) { // route() returns error if route not found -> object.isBoom === true
|
if (matchingRoute.isBoom !== true) { // route() returns error if route not found -> object.isBoom === true
|
||||||
const request = await this.getRequest({ req, res, cluster, url, params: matchingRoute.params })
|
const request = await this.getRequest({ req, res, cluster, url: reqUrl, params: matchingRoute.params })
|
||||||
await matchingRoute.route(request)
|
await matchingRoute.route(request)
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
5
src/main/routes/index.ts
Normal file
5
src/main/routes/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export * from "./config-route"
|
||||||
|
export * from "./kubeconfig-route"
|
||||||
|
export * from "./metrics-route"
|
||||||
|
export * from "./port-forward-route"
|
||||||
|
export * from "./watch-route"
|
||||||
@ -12,7 +12,7 @@ function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster
|
|||||||
{
|
{
|
||||||
'name': cluster.contextName,
|
'name': cluster.contextName,
|
||||||
'cluster': {
|
'cluster': {
|
||||||
'server': cluster.apiUrl,
|
'server': cluster.apiUrl.href,
|
||||||
'certificate-authority-data': secret.data["ca.crt"]
|
'certificate-authority-data': secret.data["ca.crt"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,8 +1,7 @@
|
|||||||
import { LensApiRequest } from "../router"
|
import { LensApiRequest } from "../router"
|
||||||
import { LensApi } from "../lens-api"
|
import { LensApi } from "../lens-api"
|
||||||
import requestPromise from "request-promise-native"
|
import requestPromise from "request-promise-native"
|
||||||
import { PrometheusProviderRegistry, PrometheusProvider, PrometheusNodeQuery, PrometheusClusterQuery, PrometheusPodQuery, PrometheusPvcQuery, PrometheusIngressQuery, PrometheusQueryOpts} from "../prometheus/provider-registry"
|
import { PrometheusClusterQuery, PrometheusIngressQuery, PrometheusNodeQuery, PrometheusPodQuery, PrometheusProvider, PrometheusPvcQuery, PrometheusQueryOpts } from "../prometheus/provider-registry"
|
||||||
import { apiPrefix } from "../../common/vars";
|
|
||||||
|
|
||||||
export type IMetricsQuery = string | string[] | {
|
export type IMetricsQuery = string | string[] | {
|
||||||
[metricName: string]: string;
|
[metricName: string]: string;
|
||||||
@ -11,11 +10,10 @@ export type IMetricsQuery = string | string[] | {
|
|||||||
class MetricsRoute extends LensApi {
|
class MetricsRoute extends LensApi {
|
||||||
|
|
||||||
public async routeMetrics(request: LensApiRequest) {
|
public async routeMetrics(request: LensApiRequest) {
|
||||||
const { response, cluster} = request
|
const { response, cluster } = request
|
||||||
const query: IMetricsQuery = request.payload;
|
const query: IMetricsQuery = request.payload;
|
||||||
const serverUrl = `http://127.0.0.1:${cluster.port}${apiPrefix.KUBE_BASE}`
|
const headers: Record<string, string> = {
|
||||||
const headers = {
|
"Host": cluster.apiUrl.host,
|
||||||
"Host": `${cluster.id}.localhost:${cluster.port}`,
|
|
||||||
"Content-type": "application/json",
|
"Content-type": "application/json",
|
||||||
}
|
}
|
||||||
const queryParams: IMetricsQuery = {}
|
const queryParams: IMetricsQuery = {}
|
||||||
@ -27,7 +25,7 @@ class MetricsRoute extends LensApi {
|
|||||||
let prometheusProvider: PrometheusProvider
|
let prometheusProvider: PrometheusProvider
|
||||||
try {
|
try {
|
||||||
const prometheusPath = await cluster.contextHandler.getPrometheusPath()
|
const prometheusPath = await cluster.contextHandler.getPrometheusPath()
|
||||||
metricsUrl = `${serverUrl}/api/v1/namespaces/${prometheusPath}/proxy${cluster.getPrometheusApiPrefix()}/api/v1/query_range`
|
metricsUrl = `${cluster.apiServerUrl}/api/v1/namespaces/${prometheusPath}/proxy${cluster.getPrometheusApiPrefix()}/api/v1/query_range`
|
||||||
prometheusProvider = await cluster.contextHandler.getPrometheusProvider()
|
prometheusProvider = await cluster.contextHandler.getPrometheusProvider()
|
||||||
} catch {
|
} catch {
|
||||||
this.respondJson(response, {})
|
this.respondJson(response, {})
|
||||||
@ -65,11 +63,9 @@ class MetricsRoute extends LensApi {
|
|||||||
let data: any;
|
let data: any;
|
||||||
if (typeof query === "string") {
|
if (typeof query === "string") {
|
||||||
data = await loadMetrics(query)
|
data = await loadMetrics(query)
|
||||||
}
|
} else if (Array.isArray(query)) {
|
||||||
else if (Array.isArray(query)) {
|
|
||||||
data = await Promise.all(query.map(loadMetrics));
|
data = await Promise.all(query.map(loadMetrics));
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
data = {};
|
data = {};
|
||||||
const result = await Promise.all(
|
const result = await Promise.all(
|
||||||
Object.entries(query).map((queryEntry: any) => {
|
Object.entries(query).map((queryEntry: any) => {
|
||||||
@ -1,133 +1,101 @@
|
|||||||
import { BrowserView, BrowserWindow, shell } from "electron"
|
|
||||||
import { reaction } from "mobx";
|
import { reaction } from "mobx";
|
||||||
|
import { BrowserWindow, shell } from "electron"
|
||||||
import windowStateKeeper from "electron-window-state"
|
import windowStateKeeper from "electron-window-state"
|
||||||
import type { ClusterId } from "../common/cluster-store";
|
import type { ClusterId } from "../common/cluster-store";
|
||||||
import { clusterStore } from "../common/cluster-store";
|
import { clusterStore } from "../common/cluster-store";
|
||||||
import { tracker } from "../common/tracker";
|
|
||||||
|
|
||||||
export interface WindowManagerParams {
|
|
||||||
showSplash?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class WindowManager {
|
export class WindowManager {
|
||||||
protected mainWindow: BrowserWindow;
|
protected views = new Map<ClusterId, BrowserWindow>();
|
||||||
protected splashWindow?: BrowserWindow;
|
protected disposers = this.bindReactions();
|
||||||
protected windowState: windowStateKeeper.State;
|
|
||||||
protected views = new Map<ClusterId, BrowserView>();
|
|
||||||
protected disposers: Function[] = [];
|
|
||||||
|
|
||||||
constructor(protected params: WindowManagerParams = {}) {
|
protected splashWindow = new BrowserWindow({
|
||||||
this.params = { showSplash: false, ...params };
|
width: 500,
|
||||||
|
height: 300,
|
||||||
|
backgroundColor: "#1e2124",
|
||||||
|
center: true,
|
||||||
|
frame: false,
|
||||||
|
resizable: false,
|
||||||
|
show: false,
|
||||||
|
});
|
||||||
|
|
||||||
// Manage main window size and position with state persistence
|
// Manage main window size and position with state persistence
|
||||||
this.windowState = windowStateKeeper({
|
protected windowState = windowStateKeeper({
|
||||||
defaultHeight: 900,
|
defaultHeight: 900,
|
||||||
defaultWidth: 1440,
|
defaultWidth: 1440,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.mainWindow = new BrowserWindow({
|
protected bindReactions() {
|
||||||
show: false,
|
return [
|
||||||
x: this.windowState.x,
|
// auto-destroy cluster-view when it's removed
|
||||||
y: this.windowState.y,
|
|
||||||
width: this.windowState.width,
|
|
||||||
height: this.windowState.height,
|
|
||||||
backgroundColor: "#1e2124",
|
|
||||||
titleBarStyle: "hidden",
|
|
||||||
webPreferences: {
|
|
||||||
nodeIntegration: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Splash-screen window with loading indicator
|
|
||||||
this.splashWindow = new BrowserWindow({
|
|
||||||
width: 500,
|
|
||||||
height: 300,
|
|
||||||
backgroundColor: "#1e2124",
|
|
||||||
center: true,
|
|
||||||
frame: false,
|
|
||||||
resizable: false,
|
|
||||||
show: false,
|
|
||||||
});
|
|
||||||
this.splashWindow.loadURL("static://splash.html")
|
|
||||||
|
|
||||||
// Hook window state manager into window lifecycle
|
|
||||||
this.windowState.manage(this.mainWindow);
|
|
||||||
|
|
||||||
// Disallow closing main window
|
|
||||||
this.mainWindow.on("close", (evt) => {
|
|
||||||
evt.preventDefault();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Open external links in default browser (target=_blank, window.open)
|
|
||||||
this.mainWindow.webContents.on("new-window", (event, url) => {
|
|
||||||
event.preventDefault();
|
|
||||||
shell.openExternal(url);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Track main window focus
|
|
||||||
this.mainWindow.on("focus", () => {
|
|
||||||
tracker.event("app", "focus")
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clean up views for removed clusters
|
|
||||||
this.disposers.push(
|
|
||||||
reaction(() => clusterStore.removedClusters.toJS(), removedClusters => {
|
reaction(() => clusterStore.removedClusters.toJS(), removedClusters => {
|
||||||
removedClusters.forEach(cluster => {
|
removedClusters.forEach(cluster => {
|
||||||
const lensView = this.getView(cluster.id);
|
this.destroyView(cluster.id);
|
||||||
if (lensView) {
|
|
||||||
lensView.destroy();
|
|
||||||
this.views.delete(cluster.id);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
);
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadURL(url: string) {
|
async showSplash() {
|
||||||
if (this.params.showSplash) {
|
await this.splashWindow.loadURL("static://splash.html")
|
||||||
this.splashWindow.show();
|
this.splashWindow.show();
|
||||||
}
|
}
|
||||||
await this.mainWindow.loadURL(url);
|
|
||||||
this.mainWindow.show();
|
hideSplash() {
|
||||||
this.splashWindow.hide();
|
this.splashWindow.hide();
|
||||||
|
|
||||||
this.setView("cluster-id-blabla");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async setView(clusterId: ClusterId) {
|
protected async showView(clusterId: ClusterId) {
|
||||||
const view = this.getView(clusterId)
|
const cluster = clusterStore.getById(clusterId);
|
||||||
this.mainWindow.addBrowserView(view);
|
if (!cluster) {
|
||||||
// await view.webContents.loadURL("http://ya.ru");
|
throw new Error(`Can't load view for non-existing cluster="${clusterId}"`);
|
||||||
// view.setBounds({
|
}
|
||||||
// x: 10,
|
const view = this.getView(clusterId);
|
||||||
// y: 10,
|
const url = cluster.apiUrl.href;
|
||||||
// width: this.windowState.width - 20,
|
if (view.webContents.getURL() !== url) {
|
||||||
// height: this.windowState.height - 20,
|
await view.loadURL(url);
|
||||||
// })
|
}
|
||||||
// view.setAutoResize({ horizontal: true, vertical: true });
|
view.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
getView(clusterId: ClusterId): BrowserView {
|
getView(clusterId: ClusterId) {
|
||||||
let view = this.views.get(clusterId);
|
let view = this.views.get(clusterId);
|
||||||
if (!view) {
|
if (!view) {
|
||||||
view = new BrowserView({
|
view = new BrowserWindow({
|
||||||
|
show: false,
|
||||||
|
x: this.windowState.x,
|
||||||
|
y: this.windowState.y,
|
||||||
|
width: this.windowState.width,
|
||||||
|
height: this.windowState.height,
|
||||||
|
backgroundColor: "#1e2124",
|
||||||
|
titleBarStyle: "hidden",
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true
|
nodeIntegration: true,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
// open external links in default browser (target=_blank, window.open)
|
||||||
|
view.webContents.on("new-window", (event, url) => {
|
||||||
|
event.preventDefault();
|
||||||
|
shell.openExternal(url);
|
||||||
|
});
|
||||||
this.views.set(clusterId, view);
|
this.views.set(clusterId, view);
|
||||||
}
|
}
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
destroyView(clusterId: ClusterId) {
|
||||||
|
const view = this.views.get(clusterId);
|
||||||
|
if (view) {
|
||||||
|
view.destroy();
|
||||||
|
this.views.delete(clusterId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this.disposers.forEach(dispose => dispose());
|
this.disposers.forEach(dispose => dispose());
|
||||||
this.disposers.length = 0;
|
this.disposers.length = 0;
|
||||||
this.views.forEach(view => view.destroy());
|
this.views.forEach(view => view.destroy());
|
||||||
this.views.clear();
|
this.views.clear();
|
||||||
this.mainWindow.destroy();
|
|
||||||
this.splashWindow.destroy();
|
this.splashWindow.destroy();
|
||||||
this.mainWindow = null;
|
|
||||||
this.splashWindow = null;
|
this.splashWindow = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
// App configuration api
|
// App configuration api
|
||||||
import type { IConfigRoutePayload } from "../../../main/routes/config";
|
import type { IConfigRoutePayload } from "../../../main/routes/config-route";
|
||||||
import { apiBase } from "../index";
|
import { apiBase } from "../index";
|
||||||
|
|
||||||
export const configApi = {
|
export const configApi = {
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { apiBase } from "../index";
|
import { apiBase } from "../index";
|
||||||
import type { IMetricsQuery } from "../../../main/routes/metrics";
|
import type { IMetricsQuery } from "../../../main/routes/metrics-route";
|
||||||
|
|
||||||
export interface IMetrics {
|
export interface IMetrics {
|
||||||
status: string;
|
status: string;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import type { IConfigRoutePayload } from "../main/routes/config";
|
import type { IConfigRoutePayload } from "../main/routes/config-route";
|
||||||
|
|
||||||
import { observable, when } from "mobx";
|
import { observable, when } from "mobx";
|
||||||
import { autobind, interval } from "./utils";
|
import { autobind, interval } from "./utils";
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user