mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
1 parent
e810eab39f
commit
b7974827d2
786 files changed
+14567
-24066
No files matched your search
+14
-20
@@ -1,19 +1,18 @@
|
||||
import { KubeConfig } from "@kubernetes/client-node"
|
||||
import { PromiseIpc } from "electron-promise-ipc"
|
||||
import * as http from "http"
|
||||
import http from "http"
|
||||
import { Cluster, ClusterBaseInfo } from "./cluster"
|
||||
import { clusterStore } from "../common/cluster-store"
|
||||
import * as k8s from "./k8s"
|
||||
import logger from "./logger"
|
||||
import { LensProxy } from "./proxy"
|
||||
import { app } from "electron"
|
||||
import * as path from "path"
|
||||
import path from "path"
|
||||
import { promises } from "fs"
|
||||
import { ensureDir } from "fs-extra"
|
||||
import * as filenamify from "filenamify"
|
||||
import filenamify from "filenamify"
|
||||
import { v4 as uuid } from "uuid"
|
||||
|
||||
declare const __static: string;
|
||||
import { apiPrefix } from "../common/vars";
|
||||
|
||||
export type FeatureInstallRequest = {
|
||||
name: string;
|
||||
@@ -92,7 +91,7 @@ export class ClusterManager {
|
||||
reject("No cluster contexts defined")
|
||||
}
|
||||
configs.forEach(c => {
|
||||
k8s.valideConfig(c)
|
||||
k8s.validateConfig(c)
|
||||
const cluster = new Cluster({
|
||||
id: uuid(),
|
||||
port: this.port,
|
||||
@@ -117,15 +116,15 @@ export class ClusterManager {
|
||||
logger.debug(`IPC: addCluster`)
|
||||
const cluster = await this.addNewCluster(clusterData)
|
||||
return {
|
||||
addedCluster: this.clusterResponse(cluster),
|
||||
allClusters: Array.from(this.getClusters()).map((cluster: Cluster) => this.clusterResponse(cluster))
|
||||
addedCluster: cluster.toClusterInfo(),
|
||||
allClusters: Array.from(this.getClusters()).map((cluster: Cluster) => cluster.toClusterInfo())
|
||||
}
|
||||
});
|
||||
|
||||
this.promiseIpc.on("getClusters", async (workspaceId: string) => {
|
||||
logger.debug(`IPC: getClusters, workspace ${workspaceId}`)
|
||||
const workspaceClusters = Array.from(this.getClusters()).filter((cluster) => cluster.workspace === workspaceId)
|
||||
return workspaceClusters.map((cluster: Cluster) => this.clusterResponse(cluster))
|
||||
return workspaceClusters.map((cluster: Cluster) => cluster.toClusterInfo())
|
||||
});
|
||||
|
||||
this.promiseIpc.on("getCluster", async (id: string) => {
|
||||
@@ -133,7 +132,7 @@ export class ClusterManager {
|
||||
const cluster = this.getCluster(id)
|
||||
if (cluster) {
|
||||
await cluster.refreshCluster()
|
||||
return this.clusterResponse(cluster)
|
||||
return cluster.toClusterInfo()
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
@@ -181,7 +180,7 @@ export class ClusterManager {
|
||||
if(!cluster.preferences) cluster.preferences = {};
|
||||
cluster.preferences.icon = clusterIcon
|
||||
clusterStore.storeCluster(cluster);
|
||||
return {success: true, cluster: this.clusterResponse(cluster), message: ""}
|
||||
return {success: true, cluster: cluster.toClusterInfo(), message: ""}
|
||||
} catch(error) {
|
||||
return {success: false, message: error}
|
||||
}
|
||||
@@ -193,7 +192,7 @@ export class ClusterManager {
|
||||
if (cluster && cluster.preferences) {
|
||||
cluster.preferences.icon = null;
|
||||
clusterStore.storeCluster(cluster)
|
||||
return {success: true, cluster: this.clusterResponse(cluster), message: ""}
|
||||
return {success: true, cluster: cluster.toClusterInfo(), message: ""}
|
||||
} else {
|
||||
return {success: false, message: "Cluster not found"}
|
||||
}
|
||||
@@ -202,7 +201,7 @@ export class ClusterManager {
|
||||
this.promiseIpc.on("refreshCluster", async (clusterId: string) => {
|
||||
const cluster = this.clusters.get(clusterId)
|
||||
await cluster.refreshCluster()
|
||||
return this.clusterResponse(cluster)
|
||||
return cluster.toClusterInfo()
|
||||
});
|
||||
|
||||
this.promiseIpc.on("stopCluster", (clusterId: string) => {
|
||||
@@ -217,7 +216,7 @@ export class ClusterManager {
|
||||
|
||||
this.promiseIpc.on("removeCluster", (ctx: string) => {
|
||||
logger.debug(`IPC: removeCluster: ${ctx}`)
|
||||
return this.removeCluster(ctx).map((cluster: Cluster) => this.clusterResponse(cluster))
|
||||
return this.removeCluster(ctx).map((cluster: Cluster) => cluster.toClusterInfo())
|
||||
});
|
||||
|
||||
this.promiseIpc.on("clusterStored", (clusterId: string) => {
|
||||
@@ -263,7 +262,7 @@ export class ClusterManager {
|
||||
cluster = this.clusters.get(clusterId)
|
||||
if (cluster) {
|
||||
// we need to swap path prefix so that request is proxied to kube api
|
||||
req.url = req.url.replace(`/${clusterId}`, "/api-kube")
|
||||
req.url = req.url.replace(`/${clusterId}`, apiPrefix.KUBE_BASE)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -274,11 +273,6 @@ export class ClusterManager {
|
||||
return cluster;
|
||||
}
|
||||
|
||||
// TODO: remove this
|
||||
protected clusterResponse(cluster: Cluster) {
|
||||
return cluster.toClusterInfo()
|
||||
}
|
||||
|
||||
protected async uploadClusterIcon(cluster: Cluster, fileName: string, src: string): Promise<string> {
|
||||
await ensureDir(ClusterManager.clusterIconDir)
|
||||
fileName = filenamify(cluster.contextName + "-" + fileName)
|
||||
|
||||
+31
-34
@@ -3,12 +3,13 @@ import { FeatureStatusMap } from "./feature"
|
||||
import * as k8s from "./k8s"
|
||||
import { clusterStore } from "../common/cluster-store"
|
||||
import logger from "./logger"
|
||||
import { KubeConfig, CoreV1Api, AuthorizationV1Api, V1ResourceAttributes } from "@kubernetes/client-node"
|
||||
import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"
|
||||
import * as fm from "./feature-manager";
|
||||
import { Kubectl } from "./kubectl";
|
||||
import { PromiseIpc } from "electron-promise-ipc"
|
||||
import * as request from "request-promise-native"
|
||||
import request from "request-promise-native"
|
||||
import { KubeconfigManager } from "./kubeconfig-manager"
|
||||
import { apiPrefix } from "../common/vars";
|
||||
|
||||
enum ClusterStatus {
|
||||
AccessGranted = 2,
|
||||
@@ -123,16 +124,9 @@ export class Cluster implements ClusterInfo {
|
||||
this.contextHandler.setClusterPreferences(this.preferences)
|
||||
|
||||
const connectionStatus = await this.getConnectionStatus()
|
||||
if (connectionStatus == ClusterStatus.AccessGranted) {
|
||||
this.accessible = true
|
||||
} else {
|
||||
this.accessible = false
|
||||
}
|
||||
if (connectionStatus > ClusterStatus.Offline) {
|
||||
this.online = true
|
||||
} else {
|
||||
this.online = false
|
||||
}
|
||||
this.accessible = connectionStatus == ClusterStatus.AccessGranted;
|
||||
this.online = connectionStatus > ClusterStatus.Offline;
|
||||
|
||||
if (this.accessible) {
|
||||
this.distribution = this.detectKubernetesDistribution(this.version)
|
||||
this.features = await fm.getFeatures(this.contextHandler)
|
||||
@@ -146,7 +140,9 @@ export class Cluster implements ClusterInfo {
|
||||
|
||||
public updateKubeconfig(kubeconfig: string) {
|
||||
const storedCluster = clusterStore.getCluster(this.id)
|
||||
if (!storedCluster) { return }
|
||||
if (!storedCluster) {
|
||||
return
|
||||
}
|
||||
|
||||
this.kubeConfig = kubeconfig
|
||||
this.save()
|
||||
@@ -164,7 +160,7 @@ export class Cluster implements ClusterInfo {
|
||||
}
|
||||
|
||||
public toClusterInfo(): ClusterInfo {
|
||||
const clusterInfo: ClusterInfo = {
|
||||
return {
|
||||
id: this.id,
|
||||
workspace: this.workspace,
|
||||
url: this.url,
|
||||
@@ -179,20 +175,19 @@ export class Cluster implements ClusterInfo {
|
||||
isAdmin: this.isAdmin,
|
||||
features: this.features,
|
||||
kubeCtl: this.kubeCtl,
|
||||
kubeConfig: this.kubeConfig,
|
||||
kubeConfig: this.kubeConfig,
|
||||
preferences: this.preferences
|
||||
}
|
||||
return clusterInfo;
|
||||
}
|
||||
|
||||
protected async k8sRequest(path: string, opts?: request.RequestPromiseOptions) {
|
||||
const options = Object.assign({
|
||||
json: true, timeout: 10000
|
||||
json: true,
|
||||
timeout: 10000
|
||||
}, (opts || {}))
|
||||
if (!options.headers) { options.headers = {} }
|
||||
options.headers.host = `${this.id}.localhost:${this.port}`
|
||||
|
||||
return request(`http://127.0.0.1:${this.port}/api-kube${path}`, options)
|
||||
return request(`http://127.0.0.1:${this.port}${apiPrefix.KUBE_BASE}${path}`, options)
|
||||
}
|
||||
|
||||
protected async getConnectionStatus() {
|
||||
@@ -201,21 +196,24 @@ export class Cluster implements ClusterInfo {
|
||||
this.version = response.gitVersion
|
||||
this.failureReason = null
|
||||
return ClusterStatus.AccessGranted;
|
||||
} catch(error) {
|
||||
} catch (error) {
|
||||
logger.error(`Failed to connect to cluster ${this.contextName}: ${JSON.stringify(error)}`)
|
||||
if (error.statusCode) {
|
||||
if (error.statusCode >= 400 && error.statusCode < 500) {
|
||||
this.failureReason = "Invalid credentials";
|
||||
return ClusterStatus.AccessDenied;
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
this.failureReason = error.error || error.message;
|
||||
return ClusterStatus.Offline;
|
||||
}
|
||||
} else if (error.failed === true) {
|
||||
}
|
||||
else if (error.failed === true) {
|
||||
if (error.timedOut === true) {
|
||||
this.failureReason = "Connection timed out";
|
||||
return ClusterStatus.Offline;
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
this.failureReason = "Failed to fetch credentials";
|
||||
return ClusterStatus.AccessDenied;
|
||||
}
|
||||
@@ -234,7 +232,7 @@ export class Cluster implements ClusterInfo {
|
||||
spec: { resourceAttributes }
|
||||
})
|
||||
return accessReview.body.status.allowed === true
|
||||
} catch(error) {
|
||||
} catch (error) {
|
||||
logger.error(`failed to request selfSubjectAccessReview: ${error.message}`)
|
||||
return false
|
||||
}
|
||||
@@ -258,10 +256,10 @@ export class Cluster implements ClusterInfo {
|
||||
else if (kubernetesVersion.includes("IKS")) {
|
||||
return "iks"
|
||||
}
|
||||
else if(this.apiUrl.endsWith("azmk8s.io")) {
|
||||
else if (this.apiUrl.endsWith("azmk8s.io")) {
|
||||
return "aks"
|
||||
}
|
||||
else if(this.apiUrl.endsWith("k8s.ondigitalocean.com")) {
|
||||
else if (this.apiUrl.endsWith("k8s.ondigitalocean.com")) {
|
||||
return "digitalocean"
|
||||
}
|
||||
else if (this.contextHandler.contextName.startsWith("minikube")) {
|
||||
@@ -274,11 +272,11 @@ export class Cluster implements ClusterInfo {
|
||||
return "vanilla"
|
||||
}
|
||||
|
||||
protected async getNodeCount() {
|
||||
protected async getNodeCount() {
|
||||
try {
|
||||
const response = await this.k8sRequest("/api/v1/nodes")
|
||||
return response.items.length
|
||||
} catch(error) {
|
||||
} catch (error) {
|
||||
logger.debug(`failed to request node list: ${error.message}`)
|
||||
return null
|
||||
}
|
||||
@@ -294,18 +292,17 @@ export class Cluster implements ClusterInfo {
|
||||
const uniqEventSources = new Set();
|
||||
const warnings = response.body.items.filter(e => e.type !== 'Normal');
|
||||
for (const w of warnings) {
|
||||
if(w.involvedObject.kind === 'Pod') {
|
||||
if (w.involvedObject.kind === 'Pod') {
|
||||
try {
|
||||
const pod = (await client.readNamespacedPod(w.involvedObject.name, w.involvedObject.namespace)).body;
|
||||
logger.debug(`checking pod ${w.involvedObject.namespace}/${w.involvedObject.name}`)
|
||||
if(k8s.podHasIssues(pod)) {
|
||||
if (k8s.podHasIssues(pod)) {
|
||||
uniqEventSources.add(w.involvedObject.uid);
|
||||
}
|
||||
continue;
|
||||
} catch (error) {
|
||||
continue;
|
||||
} catch (err) {
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
uniqEventSources.add(w.involvedObject.uid);
|
||||
}
|
||||
}
|
||||
|
||||
+18
-19
@@ -1,6 +1,5 @@
|
||||
import { KubeConfig, CoreV1Api } from "@kubernetes/client-node"
|
||||
import { readFileSync } from "fs"
|
||||
import * as http from "http"
|
||||
import { CoreV1Api, KubeConfig } from "@kubernetes/client-node"
|
||||
import http from "http"
|
||||
import { ServerOptions } from "http-proxy"
|
||||
import * as url from "url"
|
||||
import logger from "./logger"
|
||||
@@ -8,8 +7,7 @@ import { getFreePort } from "./port"
|
||||
import { KubeAuthProxy } from "./kube-auth-proxy"
|
||||
import { Cluster, ClusterPreferences } from "./cluster"
|
||||
import { prometheusProviders } from "../common/prometheus-providers"
|
||||
import { PrometheusService, PrometheusProvider } from "./prometheus/provider-registry"
|
||||
import { PrometheusLens } from "./prometheus/lens"
|
||||
import { PrometheusProvider, PrometheusService } from "./prometheus/provider-registry"
|
||||
|
||||
export class ContextHandler {
|
||||
public contextName: string
|
||||
@@ -76,19 +74,21 @@ export class ContextHandler {
|
||||
if (clusterPreferences && clusterPreferences.prometheus) {
|
||||
const prom = clusterPreferences.prometheus
|
||||
this.prometheusPath = `${prom.namespace}/services/${prom.service}:${prom.port}`
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
this.prometheusPath = null
|
||||
}
|
||||
if(clusterPreferences && clusterPreferences.clusterName) {
|
||||
if (clusterPreferences && clusterPreferences.clusterName) {
|
||||
this.clusterName = clusterPreferences.clusterName;
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
this.clusterName = this.contextName;
|
||||
}
|
||||
}
|
||||
|
||||
protected async resolvePrometheusPath(): Promise<string> {
|
||||
const service = await this.getPrometheusService()
|
||||
return `${service.namespace}/services/${service.service}:${service.port}`
|
||||
const {service, namespace, port} = await this.getPrometheusService()
|
||||
return `${namespace}/services/${service}:${port}`
|
||||
}
|
||||
|
||||
public async getPrometheusProvider() {
|
||||
@@ -110,7 +110,8 @@ export class ContextHandler {
|
||||
const service = resolvedPrometheusServices.filter(n => n)[0]
|
||||
if (service) {
|
||||
return service
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
return {
|
||||
id: "lens",
|
||||
namespace: "lens-metrics",
|
||||
@@ -128,7 +129,7 @@ export class ContextHandler {
|
||||
return this.prometheusPath
|
||||
}
|
||||
|
||||
public async getApiTarget(isWatchRequest = false) {
|
||||
public async getApiTarget(isWatchRequest = false): Promise<ServerOptions> {
|
||||
if (this.apiTarget && !isWatchRequest) {
|
||||
return this.apiTarget
|
||||
}
|
||||
@@ -140,7 +141,7 @@ export class ContextHandler {
|
||||
return apiTarget
|
||||
}
|
||||
|
||||
protected async newApiTarget(timeout: number) {
|
||||
protected async newApiTarget(timeout: number): Promise<ServerOptions> {
|
||||
return {
|
||||
changeOrigin: true,
|
||||
timeout: timeout,
|
||||
@@ -162,7 +163,7 @@ export class ContextHandler {
|
||||
let serverPort: number = null
|
||||
try {
|
||||
serverPort = await getFreePort()
|
||||
} catch(error) {
|
||||
} catch (error) {
|
||||
logger.error(error)
|
||||
throw(error)
|
||||
}
|
||||
@@ -178,7 +179,7 @@ export class ContextHandler {
|
||||
public async withTemporaryKubeconfig(callback: (kubeconfig: string) => Promise<any>) {
|
||||
try {
|
||||
await callback(this.cluster.kubeconfigPath())
|
||||
} catch(error) {
|
||||
} catch (error) {
|
||||
throw(error)
|
||||
}
|
||||
}
|
||||
@@ -187,7 +188,7 @@ export class ContextHandler {
|
||||
if (!this.proxyServer) {
|
||||
const proxyPort = await this.resolveProxyPort()
|
||||
const proxyEnv = Object.assign({}, process.env)
|
||||
if (this.cluster.preferences && this.cluster.preferences.httpsProxy) {
|
||||
if (this.cluster?.preferences.httpsProxy) {
|
||||
proxyEnv.HTTPS_PROXY = this.cluster.preferences.httpsProxy
|
||||
}
|
||||
this.proxyServer = new KubeAuthProxy(this.cluster, proxyPort, proxyEnv)
|
||||
@@ -203,8 +204,6 @@ export class ContextHandler {
|
||||
}
|
||||
|
||||
public proxyServerError() {
|
||||
if (!this.proxyServer) { return null }
|
||||
|
||||
return this.proxyServer.lastError
|
||||
return this.proxyServer?.lastError || ""
|
||||
}
|
||||
}
|
||||
+7
-3
@@ -1,5 +1,5 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path"
|
||||
import fs from "fs";
|
||||
import path from "path"
|
||||
import * as hb from "handlebars"
|
||||
import { ResourceApplier } from "./resource-applier"
|
||||
import { KubeConfig, CoreV1Api, Watch } from "@kubernetes/client-node"
|
||||
@@ -99,6 +99,10 @@ export abstract class Feature {
|
||||
}
|
||||
|
||||
protected manifestPath() {
|
||||
return path.join(__dirname, '..', 'features', this.name);
|
||||
const devPath = path.join(__dirname, "..", 'src/features', this.name);
|
||||
if(fs.existsSync(devPath)) {
|
||||
return devPath;
|
||||
}
|
||||
return path.join(__dirname, "..", 'features', this.name);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as fs from "fs"
|
||||
import fs from "fs"
|
||||
|
||||
export function ensureDir(dirname: string) {
|
||||
if (!fs.existsSync(dirname)) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as fs from "fs";
|
||||
import fs from "fs";
|
||||
import * as yaml from "js-yaml";
|
||||
import { HelmRepo, HelmRepoManager } from "./helm-repo-manager"
|
||||
import logger from "./logger";
|
||||
|
||||
+10
-11
@@ -1,5 +1,7 @@
|
||||
import * as path from "path"
|
||||
import packageInfo from "../../package.json"
|
||||
import path from "path"
|
||||
import { LensBinary, LensBinaryOpts } from "./lens-binary"
|
||||
import { isProduction } from "../common/vars";
|
||||
|
||||
export class HelmCli extends LensBinary {
|
||||
|
||||
@@ -13,7 +15,7 @@ export class HelmCli extends LensBinary {
|
||||
super(opts)
|
||||
}
|
||||
|
||||
protected getTarName(): string|null {
|
||||
protected getTarName(): string | null {
|
||||
return `${this.binaryName}-v${this.binaryVersion}-${this.platformName}-${this.arch}.tar.gz`
|
||||
}
|
||||
|
||||
@@ -26,19 +28,16 @@ export class HelmCli extends LensBinary {
|
||||
}
|
||||
|
||||
protected getOriginalBinaryPath() {
|
||||
return path.join(this.dirname, this.platformName+"-"+this.arch, this.originalBinaryName)
|
||||
return path.join(this.dirname, this.platformName + "-" + this.arch, this.originalBinaryName)
|
||||
}
|
||||
}
|
||||
|
||||
const helmVersion = require("../../package.json").config.bundledHelmVersion
|
||||
const isDevelopment = process.env.NODE_ENV !== "production"
|
||||
let baseDir: string = null
|
||||
const helmVersion = packageInfo.config.bundledHelmVersion;
|
||||
let baseDir = process.resourcesPath;
|
||||
|
||||
if(isDevelopment) {
|
||||
baseDir = path.join(process.cwd(), "binaries", "client")
|
||||
} else {
|
||||
baseDir = path.join(process.resourcesPath)
|
||||
if (!isProduction) {
|
||||
baseDir = path.join(process.cwd(), "binaries", "client");
|
||||
}
|
||||
|
||||
export const helmCli = new HelmCli(baseDir, helmVersion)
|
||||
export const helmCli = new HelmCli(baseDir, helmVersion);
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
import * as tempy from "tempy";
|
||||
import * as fs from "fs";
|
||||
import fs from "fs";
|
||||
import * as yaml from "js-yaml";
|
||||
import * as camelcaseKeys from "camelcase-keys";
|
||||
import { promiseExec} from "./promise-exec"
|
||||
import { helmCli } from "./helm-cli";
|
||||
import { Cluster } from "./cluster";
|
||||
import { toCamelCase } from "../common/utils/camelCase";
|
||||
|
||||
export class HelmReleaseManager {
|
||||
|
||||
public async listReleases(pathToKubeconfig: string, namespace?: string) {
|
||||
const helm = await helmCli.binaryPath()
|
||||
const namespaceFlag = namespace ? `-n ${namespace}` : "--all-namespaces"
|
||||
const { stdout, stderr } = await promiseExec(`"${helm}" ls --output json ${namespaceFlag} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr)})
|
||||
const { stdout } = await promiseExec(`"${helm}" ls --output json ${namespaceFlag} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr)})
|
||||
|
||||
const output = JSON.parse(stdout)
|
||||
if (output.length == 0) {
|
||||
return output
|
||||
}
|
||||
const result: any = []
|
||||
output.forEach((release: any, index: number) => {
|
||||
output[index] = camelcaseKeys(release)
|
||||
output[index] = toCamelCase(release)
|
||||
});
|
||||
return output
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as fs from "fs";
|
||||
import fs from "fs";
|
||||
import logger from "./logger";
|
||||
import * as yaml from "js-yaml";
|
||||
import { promiseExec } from "./promise-exec";
|
||||
|
||||
+18
-11
@@ -1,7 +1,10 @@
|
||||
// Main process
|
||||
|
||||
import "../common/system-ca"
|
||||
import "../common/prometheus-providers"
|
||||
import { app, dialog, protocol } from "electron"
|
||||
import { PromiseIpc } from "electron-promise-ipc"
|
||||
import * as path from "path"
|
||||
import path from "path"
|
||||
import { format as formatUrl } from "url"
|
||||
import logger from "./logger"
|
||||
import initMenu from "./menu"
|
||||
@@ -15,20 +18,21 @@ import { shellSync } from "./shell-sync"
|
||||
import { getFreePort } from "./port"
|
||||
import { mangleProxyEnv } from "./proxy-env"
|
||||
import { findMainWebContents } from "./webcontents"
|
||||
import "../common/prometheus-providers"
|
||||
import { registerStaticProtocol } from "../common/register-static";
|
||||
import { isMac, vueAppName } from "../common/vars";
|
||||
|
||||
mangleProxyEnv()
|
||||
if (app.commandLine.getSwitchValue("proxy-server") !== "") {
|
||||
process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server")
|
||||
}
|
||||
const isDevelopment = process.env.NODE_ENV !== "production"
|
||||
const promiseIpc = new PromiseIpc({ timeout: 2000 })
|
||||
|
||||
const promiseIpc = new PromiseIpc({ timeout: 2000 })
|
||||
let windowManager: WindowManager = null;
|
||||
let clusterManager: ClusterManager = null;
|
||||
let proxyServer: proxy.LensProxy = null;
|
||||
const vmURL = (isDevelopment) ? `http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}` : formatUrl({
|
||||
pathname: path.join(__dirname, "index.html"),
|
||||
|
||||
const vmURL = formatUrl({
|
||||
pathname: path.join(__dirname, `${vueAppName}.html`),
|
||||
protocol: "file",
|
||||
slashes: true,
|
||||
})
|
||||
@@ -40,12 +44,15 @@ async function main() {
|
||||
updater.start();
|
||||
|
||||
tracker.event("app", "start");
|
||||
|
||||
registerStaticProtocol();
|
||||
protocol.registerFileProtocol('store', (request, callback) => {
|
||||
const url = request.url.substr(8)
|
||||
callback( path.normalize(`${app.getPath("userData")}/${url}`) )
|
||||
callback(path.normalize(`${app.getPath("userData")}/${url}`))
|
||||
}, (error) => {
|
||||
if (error) console.error('Failed to register protocol')
|
||||
})
|
||||
|
||||
let port: number = null
|
||||
// find free port
|
||||
try {
|
||||
@@ -80,7 +87,7 @@ async function main() {
|
||||
},
|
||||
showPreferencesHook: async () => {
|
||||
// IPC send needs webContents as we're sending it to renderer
|
||||
promiseIpc.send('navigate', findMainWebContents(), {name: 'preferences-page'}).then((data: any) => {
|
||||
promiseIpc.send('navigate', findMainWebContents(), { name: 'preferences-page' }).then((data: any) => {
|
||||
logger.debug("navigate: preferences IPC sent");
|
||||
})
|
||||
},
|
||||
@@ -103,10 +110,10 @@ async function main() {
|
||||
}
|
||||
|
||||
app.on("ready", main)
|
||||
app.on('window-all-closed', function() {
|
||||
app.on('window-all-closed', function () {
|
||||
// On OS X it is common for applications and their menu bar
|
||||
// to stay active until the user quits explicitly with Cmd + Q
|
||||
if (process.platform != 'darwin') {
|
||||
if (!isMac) {
|
||||
app.quit();
|
||||
} else {
|
||||
windowManager = null
|
||||
@@ -116,7 +123,7 @@ app.on('window-all-closed', function() {
|
||||
app.on("activate", () => {
|
||||
if (!windowManager) {
|
||||
logger.debug("activate main window")
|
||||
windowManager = new WindowManager(false)
|
||||
windowManager = new WindowManager({ showSplash: false })
|
||||
windowManager.showMain(vmURL)
|
||||
}
|
||||
})
|
||||
|
||||
+1
-3
@@ -1,8 +1,6 @@
|
||||
import * as k8s from "@kubernetes/client-node"
|
||||
import * as os from "os"
|
||||
import { all } from "q";
|
||||
import * as yaml from "js-yaml"
|
||||
import { V1beta1ValidatingWebhookConfiguration } from "@kubernetes/client-node";
|
||||
import logger from "./logger";
|
||||
|
||||
const kc = new k8s.KubeConfig()
|
||||
@@ -31,7 +29,7 @@ export function loadConfig(kubeconfig: string): k8s.KubeConfig {
|
||||
*
|
||||
* @param config KubeConfig to check
|
||||
*/
|
||||
export function valideConfig(config: k8s.KubeConfig): boolean {
|
||||
export function validateConfig(config: k8s.KubeConfig): boolean {
|
||||
logger.debug(`validating kube config: ${JSON.stringify(config)}`)
|
||||
if(!config.users || config.users.length == 0) {
|
||||
throw new Error("No users provided in config")
|
||||
|
||||
@@ -28,9 +28,7 @@ export class KubeAuthProxy {
|
||||
|
||||
public async run(): Promise<void> {
|
||||
if (this.proxyProcess) {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve()
|
||||
})
|
||||
return;
|
||||
}
|
||||
const proxyBin = await this.kubectl.kubectlPath()
|
||||
const configWatcher = watch(this.cluster.kubeconfigPath(), (eventType: string, filename: string) => {
|
||||
@@ -43,11 +41,10 @@ export class KubeAuthProxy {
|
||||
}
|
||||
}
|
||||
})
|
||||
configWatcher.on("error", () => {})
|
||||
const clusterUrl = url.parse(this.cluster.apiUrl)
|
||||
let args = [
|
||||
"proxy",
|
||||
"-p", this.port.toString(),
|
||||
"--port", this.port.toString(),
|
||||
"--kubeconfig", this.cluster.kubeconfigPath(),
|
||||
"--accept-hosts", clusterUrl.hostname,
|
||||
]
|
||||
@@ -59,7 +56,9 @@ export class KubeAuthProxy {
|
||||
})
|
||||
this.proxyProcess.on("exit", (code) => {
|
||||
logger.error(`proxy ${this.cluster.contextName} exited with code ${code}`)
|
||||
this.sendIpcLogMessage( `proxy exited with code ${code}`, "stderr").catch((_) => {})
|
||||
this.sendIpcLogMessage( `proxy exited with code ${code}`, "stderr").catch((err: Error) => {
|
||||
logger.debug("failed to send IPC log message: " + err.message)
|
||||
})
|
||||
this.proxyProcess = null
|
||||
configWatcher.close()
|
||||
})
|
||||
@@ -96,7 +95,7 @@ export class KubeAuthProxy {
|
||||
}
|
||||
|
||||
protected async sendIpcLogMessage(data: string, stream: string) {
|
||||
await this.promiseIpc.send(`kube-auth:${this.cluster.id}`, findMainWebContents(), { data: data, stream: stream })
|
||||
await this.promiseIpc.send(`kube-auth:${this.cluster.id}`, findMainWebContents(), { data, stream })
|
||||
}
|
||||
|
||||
public exit() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { app } from "electron"
|
||||
import * as fs from "fs"
|
||||
import fs from "fs"
|
||||
import { ensureDir, randomFileName} from "./file-helpers"
|
||||
import logger from "./logger"
|
||||
|
||||
|
||||
+12
-14
@@ -1,17 +1,17 @@
|
||||
import { app, remote } from "electron"
|
||||
import * as path from "path"
|
||||
import * as fs from "fs"
|
||||
import * as request from "request"
|
||||
import path from "path"
|
||||
import fs from "fs"
|
||||
import request from "request"
|
||||
import { promiseExec} from "./promise-exec"
|
||||
import logger from "./logger"
|
||||
import { ensureDir, pathExists } from "fs-extra"
|
||||
import * as md5File from "md5-file"
|
||||
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"
|
||||
|
||||
const bundledVersion = require("../../package.json").config.bundledKubectlVersion
|
||||
const bundledVersion = getBundledKubectlVersion()
|
||||
const kubectlMap: Map<string, string> = new Map([
|
||||
["1.7", "1.8.15"],
|
||||
["1.8", "1.9.10"],
|
||||
@@ -195,16 +195,16 @@ export class Kubectl {
|
||||
const file = fs.createWriteStream(this.path)
|
||||
stream.on("complete", () => {
|
||||
logger.debug("kubectl binary download finished")
|
||||
file.end(() => {})
|
||||
file.end()
|
||||
})
|
||||
stream.on("error", (error) => {
|
||||
logger.error(error)
|
||||
fs.unlink(this.path, () => {})
|
||||
fs.unlink(this.path, null)
|
||||
reject(error)
|
||||
})
|
||||
file.on("close", () => {
|
||||
logger.debug("kubectl binary download closed")
|
||||
fs.chmod(this.path, 0o755, () => {})
|
||||
fs.chmod(this.path, 0o755, null)
|
||||
resolve()
|
||||
})
|
||||
stream.pipe(file)
|
||||
@@ -285,13 +285,11 @@ export class Kubectl {
|
||||
}
|
||||
|
||||
protected getDownloadMirror() {
|
||||
if (process.platform == "darwin") {
|
||||
return packageMirrors.get("default") // MacOS packages are only available from default
|
||||
}
|
||||
const mirror = packageMirrors.get(userStore.getPreferences().downloadMirror)
|
||||
if (mirror) { return mirror }
|
||||
|
||||
return packageMirrors.get("default")
|
||||
if (mirror) {
|
||||
return mirror
|
||||
}
|
||||
return packageMirrors.get("default") // MacOS packages are only available from default
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import packageInfo from "../../package.json"
|
||||
import { bundledKubectl, Kubectl } from "../../src/main/kubectl";
|
||||
import { UserStore } from "../common/user-store";
|
||||
|
||||
jest.mock("../common/user-store", () => {
|
||||
const userStoreMock: Partial<UserStore> = {
|
||||
getPreferences() {
|
||||
return {
|
||||
downloadMirror: "default"
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
userStore: userStoreMock,
|
||||
}
|
||||
})
|
||||
|
||||
describe("kubectlVersion", () => {
|
||||
it("returns bundled version if exactly same version used", async () => {
|
||||
const kubectl = new Kubectl(bundledKubectl.kubectlVersion)
|
||||
expect(kubectl.kubectlVersion).toBe(bundledKubectl.kubectlVersion)
|
||||
})
|
||||
|
||||
it("returns bundled version if same major.minor version is used", async () => {
|
||||
const { bundledKubectlVersion } = packageInfo.config;
|
||||
const kubectl = new Kubectl(bundledKubectlVersion);
|
||||
expect(kubectl.kubectlVersion).toBe(bundledKubectl.kubectlVersion)
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as http from "http";
|
||||
import http from "http";
|
||||
|
||||
export abstract class LensApi {
|
||||
protected respondJson(res: http.ServerResponse, content: {}, status = 200) {
|
||||
|
||||
+27
-20
@@ -1,9 +1,10 @@
|
||||
import * as path from "path"
|
||||
import * as fs from "fs"
|
||||
import * as request from "request"
|
||||
import path from "path"
|
||||
import fs from "fs"
|
||||
import request from "request"
|
||||
import logger from "./logger"
|
||||
import { ensureDir, pathExists } from "fs-extra"
|
||||
import * as tar from "tar"
|
||||
import { isWindows } from "../common/vars";
|
||||
|
||||
export type LensBinaryOpts = {
|
||||
version: string;
|
||||
@@ -12,6 +13,7 @@ export type LensBinaryOpts = {
|
||||
newBinaryName?: string;
|
||||
requestOpts?: request.Options;
|
||||
}
|
||||
|
||||
export class LensBinary {
|
||||
|
||||
public binaryVersion: string
|
||||
@@ -35,19 +37,21 @@ export class LensBinary {
|
||||
|
||||
let arch = null
|
||||
|
||||
if(process.arch == "x64") {
|
||||
if (process.arch == "x64") {
|
||||
arch = "amd64"
|
||||
} else if(process.arch == "x86" || process.arch == "ia32") {
|
||||
}
|
||||
else if (process.arch == "x86" || process.arch == "ia32") {
|
||||
arch = "386"
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
arch = process.arch
|
||||
}
|
||||
this.arch = arch
|
||||
this.platformName = process.platform === "win32" ? "windows" : process.platform
|
||||
this.platformName = isWindows ? "windows" : process.platform
|
||||
this.dirname = path.normalize(path.join(baseDir, this.binaryName))
|
||||
if (process.platform === "win32") {
|
||||
this.binaryName = this.binaryName+".exe"
|
||||
this.originalBinaryName = this.originalBinaryName+".exe"
|
||||
if (isWindows) {
|
||||
this.binaryName = this.binaryName + ".exe"
|
||||
this.originalBinaryName = this.originalBinaryName + ".exe"
|
||||
}
|
||||
const tarName = this.getTarName()
|
||||
if (tarName) {
|
||||
@@ -64,7 +68,7 @@ export class LensBinary {
|
||||
return this.getBinaryPath()
|
||||
}
|
||||
|
||||
protected getTarName(): string|null {
|
||||
protected getTarName(): string | null {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -88,7 +92,7 @@ export class LensBinary {
|
||||
try {
|
||||
await this.ensureBinary()
|
||||
return this.dirname
|
||||
} catch(err) {
|
||||
} catch (err) {
|
||||
logger.error(err)
|
||||
return ""
|
||||
}
|
||||
@@ -101,10 +105,12 @@ export class LensBinary {
|
||||
|
||||
public async ensureBinary() {
|
||||
const isValid = await this.checkBinary()
|
||||
if(!isValid) {
|
||||
await this.downloadBinary().catch((error) => { logger.error(error) });
|
||||
if (!isValid) {
|
||||
await this.downloadBinary().catch((error) => {
|
||||
logger.error(error)
|
||||
});
|
||||
if (this.tarPath) await this.untarBinary()
|
||||
if(this.originalBinaryName != this.binaryName ) await this.renameBinary()
|
||||
if (this.originalBinaryName != this.binaryName) await this.renameBinary()
|
||||
logger.info(`${this.originalBinaryName} has been downloaded to ${this.getBinaryPath()}`)
|
||||
}
|
||||
}
|
||||
@@ -127,7 +133,8 @@ export class LensBinary {
|
||||
fs.rename(this.getOriginalBinaryPath(), this.getBinaryPath(), (err) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
@@ -135,7 +142,7 @@ export class LensBinary {
|
||||
}
|
||||
|
||||
protected async downloadBinary() {
|
||||
const binaryPath = this.tarPath || this.getBinaryPath()
|
||||
const binaryPath = this.tarPath || this.getBinaryPath()
|
||||
await ensureDir(this.getBinaryDir(), 0o755)
|
||||
|
||||
const file = fs.createWriteStream(binaryPath)
|
||||
@@ -152,18 +159,18 @@ export class LensBinary {
|
||||
|
||||
stream.on("complete", () => {
|
||||
logger.info(`Download of ${this.originalBinaryName} finished`)
|
||||
file.end(() => {})
|
||||
file.end()
|
||||
})
|
||||
|
||||
stream.on("error", (error) => {
|
||||
logger.error(error)
|
||||
fs.unlink(binaryPath, () => {})
|
||||
fs.unlink(binaryPath, null)
|
||||
throw(error)
|
||||
})
|
||||
return new Promise((resolve, reject) => {
|
||||
file.on("close", () => {
|
||||
logger.debug(`${this.originalBinaryName} binary download closed`)
|
||||
if(!this.tarPath) fs.chmod(binaryPath, 0o755, () => {})
|
||||
if (!this.tarPath) fs.chmod(binaryPath, 0o755, null)
|
||||
resolve()
|
||||
})
|
||||
stream.pipe(file)
|
||||
|
||||
+3
-2
@@ -1,10 +1,11 @@
|
||||
import * as winston from "winston"
|
||||
import winston from "winston"
|
||||
import { isDebugging } from "../common/vars";
|
||||
|
||||
const options = {
|
||||
colorize: true,
|
||||
handleExceptions: false,
|
||||
json: false,
|
||||
level: process.env.DEBUG === "true" ? "debug" : "info",
|
||||
level: isDebugging ? "debug" : "info",
|
||||
}
|
||||
|
||||
const logger = winston.createLogger({
|
||||
|
||||
+18
-17
@@ -1,4 +1,7 @@
|
||||
import {app, dialog, Menu, MenuItemConstructorOptions, shell, webContents, BrowserWindow, MenuItem} from "electron"
|
||||
import { app, BrowserWindow, dialog, Menu, MenuItem, MenuItemConstructorOptions, shell, webContents } from "electron"
|
||||
import { isDevelopment, isMac, issuesTrackerUrl, isWindows, slackUrl } from "../common/vars";
|
||||
|
||||
// todo: refactor + split menu sections to separated files, e.g. menus/file.menu.ts
|
||||
|
||||
export interface MenuOptions {
|
||||
logoutHook: any;
|
||||
@@ -10,7 +13,6 @@ export interface MenuOptions {
|
||||
}
|
||||
|
||||
function setClusterSettingsEnabled(enabled: boolean) {
|
||||
const isMac = process.platform === 'darwin';
|
||||
const menuIndex = isMac ? 1 : 0
|
||||
Menu.getApplicationMenu().items[menuIndex].submenu.items[1].enabled = enabled
|
||||
}
|
||||
@@ -21,7 +23,7 @@ function showAbout(_menuitem: MenuItem, browserWindow: BrowserWindow) {
|
||||
]
|
||||
appDetails.push(`Copyright 2020 Lakend Labs, Inc.`)
|
||||
let title = "Lens"
|
||||
if (process.platform === "win32") {
|
||||
if (isWindows) {
|
||||
title = ` ${title}`
|
||||
}
|
||||
dialog.showMessageBoxSync(browserWindow, {
|
||||
@@ -40,9 +42,6 @@ function showAbout(_menuitem: MenuItem, browserWindow: BrowserWindow) {
|
||||
* @param ipc the main promiceIpc handle. Needed to be able to hook IPC sending into logout click handler.
|
||||
*/
|
||||
export default function initMenu(opts: MenuOptions, promiseIpc: any) {
|
||||
const isMac = process.platform === 'darwin';
|
||||
const isDevelopment = process.env.NODE_ENV === 'development';
|
||||
|
||||
const mt: MenuItemConstructorOptions[] = [];
|
||||
const macAppMenu: MenuItemConstructorOptions = {
|
||||
label: app.getName(),
|
||||
@@ -67,12 +66,12 @@ export default function initMenu(opts: MenuOptions, promiseIpc: any) {
|
||||
{ role: 'quit' }
|
||||
]
|
||||
};
|
||||
if(isMac) {
|
||||
if (isMac) {
|
||||
mt.push(macAppMenu);
|
||||
}
|
||||
|
||||
let fileMenu: MenuItemConstructorOptions;
|
||||
if(isMac) {
|
||||
if (isMac) {
|
||||
fileMenu = {
|
||||
label: 'File',
|
||||
submenu: [{
|
||||
@@ -86,7 +85,8 @@ export default function initMenu(opts: MenuOptions, promiseIpc: any) {
|
||||
}
|
||||
]
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
fileMenu = {
|
||||
label: 'File',
|
||||
submenu: [
|
||||
@@ -134,27 +134,28 @@ export default function initMenu(opts: MenuOptions, promiseIpc: any) {
|
||||
{
|
||||
label: 'Back',
|
||||
accelerator: 'CmdOrCtrl+[',
|
||||
click () {
|
||||
click() {
|
||||
webContents.getFocusedWebContents().executeJavaScript('window.history.back()')
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Forward',
|
||||
accelerator: 'CmdOrCtrl+]',
|
||||
click () {
|
||||
click() {
|
||||
webContents.getFocusedWebContents().executeJavaScript('window.history.forward()')
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Reload',
|
||||
accelerator: 'CmdOrCtrl+R',
|
||||
click () {
|
||||
click() {
|
||||
webContents.getFocusedWebContents().reload()
|
||||
}
|
||||
},
|
||||
...(isDevelopment ? [
|
||||
{ role: 'toggleDevTools'} as MenuItemConstructorOptions,
|
||||
{ role: 'toggleDevTools' } as MenuItemConstructorOptions,
|
||||
{
|
||||
accelerator: "CmdOrCtrl+Shift+I",
|
||||
label: 'Open Dashboard Devtools',
|
||||
click() {
|
||||
webContents.getFocusedWebContents().openDevTools()
|
||||
@@ -183,20 +184,20 @@ export default function initMenu(opts: MenuOptions, promiseIpc: any) {
|
||||
{
|
||||
label: 'Community Slack',
|
||||
click: async () => {
|
||||
shell.openExternal('https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI');
|
||||
shell.openExternal(slackUrl);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Report an Issue',
|
||||
click: async () => {
|
||||
shell.openExternal('https://github.com/lensapp/lens/issues');
|
||||
shell.openExternal(issuesTrackerUrl);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "What's new?",
|
||||
click: opts.showWhatsNewHook,
|
||||
},
|
||||
...(process.platform !== "darwin" ? [{
|
||||
...(!isMac ? [{
|
||||
label: "About Lens",
|
||||
click: showAbout
|
||||
} as MenuItemConstructorOptions] : [])
|
||||
@@ -214,4 +215,4 @@ export default function initMenu(opts: MenuOptions, promiseIpc: any) {
|
||||
promiseIpc.on("disableClusterSettingsMenuItem", () => {
|
||||
setClusterSettingsEnabled(false)
|
||||
});
|
||||
};
|
||||
}
|
||||
+1
-2
@@ -1,6 +1,5 @@
|
||||
import logger from "./logger"
|
||||
import { createServer } from "net"
|
||||
import { AddressInfo } from "net"
|
||||
import { createServer, AddressInfo } from "net"
|
||||
|
||||
const getNextAvailablePort = () => {
|
||||
logger.debug("getNextAvailablePort() start")
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { EventEmitter } from 'events'
|
||||
import { getFreePort } from "./port"
|
||||
|
||||
jest.mock("net", () => {
|
||||
return {
|
||||
createServer() {
|
||||
return new class MockServer extends EventEmitter {
|
||||
listen = jest.fn(() => {
|
||||
this.emit('listening')
|
||||
return this
|
||||
})
|
||||
address = () => {
|
||||
return { port: 12345 }
|
||||
}
|
||||
unref = jest.fn()
|
||||
close = jest.fn(cb => cb())
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
describe("getFreePort", () => {
|
||||
it("finds the next free port", async () => {
|
||||
return expect(getFreePort()).resolves.toEqual(expect.any(Number))
|
||||
})
|
||||
})
|
||||
+14
-27
@@ -1,5 +1,5 @@
|
||||
import * as http from "http";
|
||||
import * as httpProxy from "http-proxy";
|
||||
import http from "http";
|
||||
import httpProxy from "http-proxy";
|
||||
import { Socket } from "net";
|
||||
import * as url from "url";
|
||||
import * as WebSocket from "ws"
|
||||
@@ -8,6 +8,7 @@ import logger from "./logger"
|
||||
import * as shell from "./node-shell-session"
|
||||
import { ClusterManager } from "./cluster-manager"
|
||||
import { Router } from "./router"
|
||||
import { apiPrefix } from "../common/vars";
|
||||
|
||||
export class LensProxy {
|
||||
public static readonly localShellSessions = true
|
||||
@@ -40,17 +41,15 @@ export class LensProxy {
|
||||
|
||||
protected buildProxyServer() {
|
||||
const proxy = this.createProxy();
|
||||
const proxyServer = http.createServer(function(req: http.IncomingMessage, res: http.ServerResponse) {
|
||||
const proxyServer = http.createServer((req: http.IncomingMessage, res: http.ServerResponse) => {
|
||||
this.handleRequest(proxy, req, res);
|
||||
}.bind(this));
|
||||
proxyServer.on("upgrade", function(req: http.IncomingMessage, socket: Socket, head: Buffer) {
|
||||
});
|
||||
proxyServer.on("upgrade", (req: http.IncomingMessage, socket: Socket, head: Buffer) => {
|
||||
this.handleWsUpgrade(req, socket, head)
|
||||
}.bind(this));
|
||||
|
||||
});
|
||||
proxyServer.on("error", (err) => {
|
||||
logger.error(err)
|
||||
});
|
||||
|
||||
return proxyServer;
|
||||
}
|
||||
|
||||
@@ -64,11 +63,10 @@ export class LensProxy {
|
||||
res.writeHead(proxyRes.statusCode, {
|
||||
"Content-Type": "text/plain"
|
||||
})
|
||||
res.end(cluster.contextHandler.proxyServerError().toString())
|
||||
res.end(cluster.contextHandler.proxyServerError())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (req.method !== "GET") {
|
||||
return
|
||||
}
|
||||
@@ -106,11 +104,11 @@ export class LensProxy {
|
||||
}
|
||||
|
||||
protected createWsListener() {
|
||||
const ws = new WebSocket.Server({ noServer: true})
|
||||
const ws = new WebSocket.Server({ noServer: true })
|
||||
ws.on("connection", ((con: WebSocket, req: http.IncomingMessage) => {
|
||||
const cluster = this.clusterManager.getClusterForRequest(req)
|
||||
const contextHandler = cluster.contextHandler
|
||||
const nodeParam = this.getNodeParam(req.url)
|
||||
const nodeParam = url.parse(req.url, true).query["node"]?.toString();
|
||||
|
||||
contextHandler.withTemporaryKubeconfig((kubeconfigPath) => {
|
||||
return new Promise<boolean>(async (resolve, reject) => {
|
||||
@@ -120,26 +118,15 @@ export class LensProxy {
|
||||
})
|
||||
})
|
||||
})
|
||||
}).bind(this))
|
||||
}))
|
||||
return ws
|
||||
}
|
||||
|
||||
protected getNodeParam(requestUrl: string) {
|
||||
const reqUrl = url.parse(requestUrl, true)
|
||||
const urlParams = reqUrl.query
|
||||
let nodeParam: string = null
|
||||
for (const [key, value] of Object.entries(urlParams)) {
|
||||
if (key === "node") {
|
||||
nodeParam = value.toString()
|
||||
}
|
||||
}
|
||||
return nodeParam
|
||||
}
|
||||
|
||||
protected async getProxyTarget(req: http.IncomingMessage, contextHandler: ContextHandler): Promise<httpProxy.ServerOptions> {
|
||||
if (req.url.startsWith("/api-kube/")) {
|
||||
const prefix = apiPrefix.KUBE_BASE;
|
||||
if (req.url.startsWith(prefix)) {
|
||||
delete req.headers.authorization
|
||||
req.url = req.url.replace("/api-kube", "")
|
||||
req.url = req.url.replace(prefix, "")
|
||||
const isWatchRequest = req.url.includes("watch=")
|
||||
return await contextHandler.getApiTarget(isWatchRequest)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { exec } from "child_process";
|
||||
import * as fs from "fs";
|
||||
import fs from "fs";
|
||||
import * as yaml from "js-yaml";
|
||||
import * as path from "path";
|
||||
import path from "path";
|
||||
import * as tempy from "tempy";
|
||||
import logger from "./logger"
|
||||
import { Cluster } from "./cluster";
|
||||
|
||||
+9
-15
@@ -1,5 +1,8 @@
|
||||
import * as http from "http"
|
||||
import * as path from "path"
|
||||
import Call from "@hapi/call"
|
||||
import Subtext from "@hapi/subtext"
|
||||
import http from "http"
|
||||
import path from "path"
|
||||
import { readFile } from "fs"
|
||||
import { Cluster } from "./cluster"
|
||||
import { configRoute } from "./routes/config"
|
||||
import { helmApi } from "./helm-api"
|
||||
@@ -8,16 +11,7 @@ import { kubeconfigRoute } from "./routes/kubeconfig"
|
||||
import { metricsRoute } from "./routes/metrics"
|
||||
import { watchRoute } from "./routes/watch"
|
||||
import { portForwardRoute } from "./routes/port-forward"
|
||||
import { readFile } from "fs"
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const Call = require('@hapi/call');
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const Subtext = require('@hapi/subtext');
|
||||
|
||||
declare const __static: string;
|
||||
|
||||
const assetsPath = path.join(__static, "build/client")
|
||||
import { outDir, reactAppName } from "../common/vars";
|
||||
|
||||
const mimeTypes: {[key: string]: string} = {
|
||||
"html": "text/html",
|
||||
@@ -88,12 +82,12 @@ export class Router {
|
||||
return request
|
||||
}
|
||||
|
||||
protected handleStaticFile(file: string, response: http.ServerResponse) {
|
||||
const asset = path.join(assetsPath, file)
|
||||
protected handleStaticFile(filePath: string, response: http.ServerResponse) {
|
||||
const asset = path.resolve(outDir, filePath);
|
||||
readFile(asset, (err, data) => {
|
||||
if (err) {
|
||||
// default to index.html so that react routes work when page is refreshed
|
||||
this.handleStaticFile("index.html", response)
|
||||
this.handleStaticFile(`${reactAppName}.html`, response)
|
||||
} else {
|
||||
const type = mimeTypes[path.extname(asset).slice(1)] || "text/plain";
|
||||
response.setHeader("Content-Type", type);
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
import { app } from "electron"
|
||||
import { CoreV1Api } from "@kubernetes/client-node"
|
||||
import { LensApiRequest } from "../router"
|
||||
import { LensApi } from "../lens-api"
|
||||
import { userStore } from "../../common/user-store"
|
||||
import { getAppVersion } from "../../common/app-utils"
|
||||
import { CoreV1Api, AuthorizationV1Api } from "@kubernetes/client-node"
|
||||
import { Cluster } from "../cluster"
|
||||
|
||||
export interface IConfigRoutePayload {
|
||||
kubeVersion?: string;
|
||||
clusterName?: string;
|
||||
lensVersion?: string;
|
||||
lensTheme?: string;
|
||||
username?: string;
|
||||
token?: string;
|
||||
allowedNamespaces?: string[];
|
||||
allowedResources?: string[];
|
||||
isClusterAdmin?: boolean;
|
||||
chartsEnabled: boolean;
|
||||
kubectlAccess?: boolean; // User accessed via kubectl-lens plugin
|
||||
}
|
||||
|
||||
// TODO: auto-populate all resources dynamically
|
||||
const apiResources = [
|
||||
{ resource: "configmaps" },
|
||||
@@ -44,12 +58,13 @@ async function getAllowedNamespaces(cluster: Cluster) {
|
||||
return namespaceList.body.items
|
||||
.filter((ns, i) => nsAccessStatuses[i])
|
||||
.map(ns => ns.metadata.name)
|
||||
} catch(error) {
|
||||
} catch (error) {
|
||||
const kc = cluster.contextHandler.kc
|
||||
const ctx = kc.getContextObject(kc.currentContext)
|
||||
if (ctx.namespace) {
|
||||
return [ctx.namespace]
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
@@ -67,20 +82,19 @@ async function getAllowedResources(cluster: Cluster, namespaces: string[]) {
|
||||
)
|
||||
return apiResources
|
||||
.filter((resource, i) => resourceAccessStatuses[i]).map(apiResource => apiResource.resource)
|
||||
} catch(error) {
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
class ConfigRoute extends LensApi {
|
||||
|
||||
public async routeConfig(request: LensApiRequest) {
|
||||
const { params, response, cluster} = request
|
||||
const { params, response, cluster } = request
|
||||
|
||||
const namespaces = await getAllowedNamespaces(cluster)
|
||||
const data = {
|
||||
const data: IConfigRoutePayload = {
|
||||
clusterName: cluster.contextName,
|
||||
lensVersion: getAppVersion(),
|
||||
lensVersion: app.getVersion(),
|
||||
lensTheme: `kontena-${userStore.getPreferences().colorTheme}`,
|
||||
kubeVersion: cluster.version,
|
||||
chartsEnabled: true,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { LensApiRequest } from "../router"
|
||||
import { LensApi } from "../lens-api"
|
||||
import * as 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 { apiPrefix } from "../../common/vars";
|
||||
|
||||
type MetricsQuery = string | string[] | {
|
||||
export type IMetricsQuery = string | string[] | {
|
||||
[metricName: string]: string;
|
||||
}
|
||||
|
||||
@@ -11,13 +12,13 @@ class MetricsRoute extends LensApi {
|
||||
|
||||
public async routeMetrics(request: LensApiRequest) {
|
||||
const { response, cluster} = request
|
||||
const query: MetricsQuery = request.payload;
|
||||
const serverUrl = `http://127.0.0.1:${cluster.port}/api-kube`
|
||||
const query: IMetricsQuery = request.payload;
|
||||
const serverUrl = `http://127.0.0.1:${cluster.port}${apiPrefix.KUBE_BASE}`
|
||||
const headers = {
|
||||
"Host": `${cluster.id}.localhost:${cluster.port}`,
|
||||
"Content-type": "application/json",
|
||||
}
|
||||
const queryParams: MetricsQuery = {}
|
||||
const queryParams: IMetricsQuery = {}
|
||||
request.query.forEach((value: string, key: string) => {
|
||||
queryParams[key] = value
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { LensApiRequest } from "../router"
|
||||
import { LensApi } from "../lens-api"
|
||||
import { Watch, KubeConfig, RuntimeRawExtension } from "@kubernetes/client-node"
|
||||
import { Watch, KubeConfig } from "@kubernetes/client-node"
|
||||
import { ServerResponse } from "http"
|
||||
import { Request } from "request"
|
||||
import logger from "../logger"
|
||||
@@ -41,7 +41,7 @@ class ApiWatcher {
|
||||
this.watchRequest.abort()
|
||||
}
|
||||
|
||||
private watchHandler(phase: string, obj: RuntimeRawExtension) {
|
||||
private watchHandler(phase: string, obj: any) {
|
||||
this.eventBuffer.push({
|
||||
type: phase,
|
||||
object: obj
|
||||
|
||||
+12
-15
@@ -1,13 +1,14 @@
|
||||
import * as pty from "node-pty"
|
||||
import * as WebSocket from "ws"
|
||||
import { EventEmitter } from "events";
|
||||
import * as path from "path"
|
||||
import shellEnv = require("shell-env")
|
||||
import path from "path"
|
||||
import shellEnv from "shell-env"
|
||||
import { app } from "electron"
|
||||
import { Kubectl } from "./kubectl"
|
||||
import { tracker } from "./tracker"
|
||||
import { Cluster, ClusterPreferences } from "./cluster"
|
||||
import { helmCli } from "./helm-cli"
|
||||
import { isWindows } from "../common/vars";
|
||||
|
||||
export class ShellSession extends EventEmitter {
|
||||
static shellEnvs: Map<string, any> = new Map()
|
||||
@@ -66,13 +67,10 @@ export class ShellSession extends EventEmitter {
|
||||
switch(path.basename(shell)) {
|
||||
case "powershell.exe":
|
||||
return ["-NoExit", "-command", `& {Set-Location $Env:USERPROFILE; $Env:PATH="${this.kubectlBinDir};${this.helmBinDir};$Env:PATH"}`]
|
||||
break
|
||||
case "bash":
|
||||
return ["--init-file", path.join(this.kubectlBinDir, '.bash_set_path')]
|
||||
break
|
||||
case "fish":
|
||||
return ["--login", "--init-command", `export PATH="${this.kubectlBinDir}:${this.helmBinDir}:$PATH"; export KUBECONFIG="${this.kubeconfigPath}"`]
|
||||
break
|
||||
case "zsh":
|
||||
return ["--login"]
|
||||
default:
|
||||
@@ -99,7 +97,7 @@ export class ShellSession extends EventEmitter {
|
||||
const env = JSON.parse(JSON.stringify(await shellEnv()))
|
||||
const pathStr = [this.kubectlBinDir, this.helmBinDir, process.env.PATH].join(path.delimiter)
|
||||
|
||||
if(process.platform === "win32") {
|
||||
if(isWindows) {
|
||||
env["SystemRoot"] = process.env.SystemRoot
|
||||
env["PTYSHELL"] = "powershell.exe"
|
||||
env["PATH"] = pathStr
|
||||
@@ -131,14 +129,14 @@ export class ShellSession extends EventEmitter {
|
||||
|
||||
protected pipeStdout() {
|
||||
// send shell output to websocket
|
||||
this.shellProcess.on("data", ((data: string) => {
|
||||
this.shellProcess.onData(((data: string) => {
|
||||
this.sendResponse(data)
|
||||
}).bind(this));
|
||||
}));
|
||||
}
|
||||
|
||||
protected pipeStdin() {
|
||||
// write websocket messages to shellProcess
|
||||
this.websocket.on("message", function(data: string) {
|
||||
this.websocket.on("message", (data: string) => {
|
||||
if (!this.running) { return }
|
||||
|
||||
const message = Buffer.from(data.slice(1, data.length), "base64").toString()
|
||||
@@ -151,11 +149,10 @@ export class ShellSession extends EventEmitter {
|
||||
this.shellProcess.resize(resizeMsgObj["Width"], resizeMsgObj["Height"])
|
||||
break;
|
||||
case "9":
|
||||
this.token = message
|
||||
this.emit('newToken', this.token)
|
||||
this.emit('newToken', message)
|
||||
break;
|
||||
}
|
||||
}.bind(this))
|
||||
})
|
||||
}
|
||||
|
||||
protected exit(code = 1000) {
|
||||
@@ -164,10 +161,10 @@ export class ShellSession extends EventEmitter {
|
||||
}
|
||||
|
||||
protected closeWebsocketOnProcessExit() {
|
||||
this.shellProcess.on("exit", (code) => {
|
||||
this.shellProcess.onExit(({ exitCode }) => {
|
||||
this.running = false
|
||||
let timeout = 0
|
||||
if (code > 0) {
|
||||
if (exitCode > 0) {
|
||||
this.sendResponse("Terminal will auto-close in 15 seconds ...")
|
||||
timeout = 15*1000
|
||||
}
|
||||
@@ -186,7 +183,7 @@ export class ShellSession extends EventEmitter {
|
||||
protected killShellProcess(){
|
||||
if(this.running) {
|
||||
// On Windows we need to kill the shell process by pid, since Lens won't respond after a while if using `this.shellProcess.kill()`
|
||||
if (process.platform == "win32") {
|
||||
if (isWindows) {
|
||||
try {
|
||||
process.kill(this.shellProcess.pid)
|
||||
} catch(e) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import shellEnv = require("shell-env")
|
||||
import logger from "./logger"
|
||||
import * as os from "os";
|
||||
import shellEnv from "shell-env"
|
||||
import os from "os";
|
||||
|
||||
interface Env {
|
||||
[key: string]: string;
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { BrowserWindow, shell, Menu } from "electron"
|
||||
import { BrowserWindow, shell } from "electron"
|
||||
import { PromiseIpc } from "electron-promise-ipc"
|
||||
import * as windowStateKeeper from "electron-window-state"
|
||||
import * as path from "path"
|
||||
import windowStateKeeper from "electron-window-state"
|
||||
import { tracker } from "./tracker";
|
||||
|
||||
declare const __static: string;
|
||||
import { getStaticUrl } from "../common/register-static";
|
||||
|
||||
export class WindowManager {
|
||||
public mainWindow: BrowserWindow = null;
|
||||
@@ -12,7 +10,7 @@ export class WindowManager {
|
||||
protected promiseIpc: any
|
||||
protected windowState: windowStateKeeper.State;
|
||||
|
||||
constructor(showSplash = true) {
|
||||
constructor({ showSplash = true } = {}) {
|
||||
this.promiseIpc = new PromiseIpc({ timeout: 2000 })
|
||||
// Manage main window size&position with persistence
|
||||
this.windowState = windowStateKeeper({
|
||||
@@ -33,11 +31,12 @@ export class WindowManager {
|
||||
}
|
||||
})
|
||||
if (showSplash) {
|
||||
this.splashWindow.loadFile(path.join(__static, "/splash.html"))
|
||||
this.splashWindow.loadURL(getStaticUrl("splash.html"))
|
||||
this.splashWindow.show()
|
||||
}
|
||||
|
||||
this.mainWindow = new BrowserWindow({
|
||||
show: false,
|
||||
x: this.windowState.x,
|
||||
y: this.windowState.y,
|
||||
width: this.windowState.width,
|
||||
@@ -48,7 +47,6 @@ export class WindowManager {
|
||||
nodeIntegration: true,
|
||||
webviewTag: true
|
||||
},
|
||||
show: false
|
||||
});
|
||||
|
||||
// Hook window state manager into window lifecycle
|
||||
@@ -68,22 +66,13 @@ export class WindowManager {
|
||||
shell.openExternal(link);
|
||||
})
|
||||
|
||||
// handle developer console
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
this.mainWindow.webContents.on("devtools-opened", () => {
|
||||
if (this.mainWindow.getBrowserView()) {
|
||||
this.mainWindow.getBrowserView().webContents.openDevTools({mode: "detach"})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
this.mainWindow.on("focus", () => {
|
||||
tracker.event("app", "focus")
|
||||
})
|
||||
}
|
||||
|
||||
public showMain(url: string) {
|
||||
this.mainWindow.loadURL( url ).then(() => {
|
||||
this.mainWindow.loadURL(url).then(() => {
|
||||
this.splashWindow.hide()
|
||||
this.splashWindow.loadURL("data:text/html;charset=utf-8,").then(() => {
|
||||
this.splashWindow.close()
|
||||
|
||||
Reference in new issue
Block a user